π«π· Version franΓ§aise
Scripting DSL β architecture
The Orkeon scripting DSL is a TypeScript-syntax language embedded in the runtime,
implemented by the Orkeon.Scripting project (and exposed via the orkeon CLI).
It is the recommended way to author crews when you don't want to write C#.
Why a DSL
Authors familiar with frontend tooling get a single-file authoring experience
(.ork.ts) without a compilation step on their side: the runtime strips
TypeScript types via esbuild and runs the resulting JavaScript inside Jint with
sandbox limits. The DSL surfaces the full Orkeon surface β agents, crews,
tasks, custom tools, state machines, graphs, events, locks, lifecycle hooks β
through fluent builders and literal declarations.
Where it lives in Clean Architecture
Orkeon.Scripting is a new project that depends on Orkeon.Domain,
Orkeon.Application, and Orkeon.Infrastructure. It does not modify those
layers: it is an opt-in adapter that translates JS-side calls into existing
domain invocations.
src/
βββ scripting/
βββ Orkeon.Scripting/ β this layer (Jint runtime + bindings)
β βββ ScriptHost.cs
β βββ JsEngineFactory.cs
β βββ Builders/ β JsAgentBuilder, JsCrewBuilder, JsTaskBuilder, JsToolBuilder
β βββ Bindings/ β global registrations (agentBuilder, crewBuilder, β¦)
β βββ Runtime/ β JsCrew, JsExecutionContext, JsAgentContext, JsLlmFacade, β¦
β βββ Orchestration/ β JsStateMachine, JsStateGraph
β βββ ErrorPolicy/ β JsErrorAction, ErrorCodeMapper
β βββ Telemetry/ β ScriptingActivitySource
β βββ Toolchain/ β EsbuildTranspiler, PassThroughTranspiler
βββ Orkeon.Scripting.Cli/ β `orkeon run <crew.ork.ts | crew.yaml>`
Quick start
The runnable tutorials live in examples/scripting/
(hello world through FSM/graph literals and RAG). End-to-end:
dotnet build src/scripting/Orkeon.Scripting.Cli/Orkeon.Scripting.Cli.csproj
dotnet run --project src/scripting/Orkeon.Scripting.Cli -- run examples/scripting/01-hello-world.ork.ts
The CLI emits the script result as JSON on stdout; exit codes follow the usual
convention (0 ok, 1 script error, 2 runtime error, 130 cancelled).
The same run verb also accepts a YAML crew β the target selects the
path (.yaml/.yml or a directory holding a multi-file crew β YAML crew
runner, .ork.ts/.js β Scripting DSL):
dotnet run --project src/scripting/Orkeon.Scripting.Cli -- run examples/09-experimental/llm-response-format/crew.yaml
For YAML crews the tool delegates to the shared one-shot runner
(Orkeon.Hosting's RunnerExecution β the exact code path of orkeon run),
so the YAML-only flags apply: -V/--var KEY=VALUE, --initial-context, plus
the shared --settings/--mount/--allow-external-mounts/--verbose/--llm-log[-path]
and the diagnostics/protocol flags (--validate, --list-tools, --events jsonl,
--stream, --client). The script-only flags (--inputs, --inputs-file,
--memory-limit-mb) are ignored on the YAML path. The YAML runner prints the crew output under a
=== Crew Output === banner instead of a JSON result.
API recap
The declarations are the reference. They ship with the DSL, they are what your editor
reads, and they live under src/scripting/Orkeon.Scripting/Typings/ β concatenated at build
into the orkeon.d.ts the CLI emits.
Two pages sit on top of them: Write a crew in TypeScript for the narrative, and the Scripting DSL reference for the method-by-method table β including the column the declarations cannot carry, which of the two script shapes actually honours each method.
| Concept | Where to look |
|---|---|
agentBuilder() / crewBuilder() / taskBuilder() / toolBuilder() |
agent.d.ts, crew.d.ts, task.d.ts, tool.d.ts |
ExecutionContext and AgentContext (ctx.llm, ctx.memory, A2A, locks, spawn) |
context.d.ts |
ctx.llm.act β the LLM β tool-call loop, and its ActOptions |
context.d.ts |
Events (ctx.events.queue / ctx.events.topic) |
events.d.ts |
stateMachine / stateGraph literal forms |
fsm.d.ts, graph.d.ts |
onError, ErrorAction, error codes |
agent.d.ts, errors.d.ts |
Lifecycle hooks (onAgentStart, onCrewComplete, β¦) |
agent.d.ts, crew.d.ts |
onCommand β answer dispatched CLI commands by name |
cli-ts-commands.md |
Built-in tools.X(...) namespace |
tools.d.ts |
LLM providers (llm.openai, llm.default, etc.) |
llm.d.ts |
RAG (rag.ingest, rag.query) |
rag.d.ts |
Coexistence with YAML
The YAML configuration path remains supported and unchanged. Scripts and YAML
crews can share the same hosting application: the DSL is one of several
authoring surfaces, not a replacement. The published orkeon tool now runs
both surfaces directly (orkeon run crew.yaml and orkeon run crew.ork.ts),
so an external consumer who depends only on the published packages no longer has
to compile a bespoke runner to execute YAML crews.
Blocking calls from scripts
Jint executes a script on a single thread: a host call that waits for its result blocks the whole script β and the REPL hosting it β until it returns. Two CLI bridges deliberately keep that synchronous contract (audited as ANT-007/ANT-010, "assumed blocking by design"):
ctx.services.get("script-host").runCrew(name, input?)β runs a crew and waits for its output. Short crews only. The wait is bounded by a configurable timeout (ScriptHostFacadeOptions.RunCrewTimeout, config sectionOrkeon:Cli:ScriptHost, default 10 minutes): on expiry the script receives a clearTimeoutException, the abandoned run is cancelled cooperatively, and the REPL thread is always released (a pure-JS busy loop that ignores cancellation is eventually reaped by the Jint sandbox's ownExecutionTimeout).ctx.services.get("commands").request(agent, intent, payload)β blocks until the agent replies; same guidance applies (see cli-ts-commands.md).
The nominal path for long work is the ticket cycle: runCrewAsync(name, input?)
(backed by CommandDispatchService.postWork, same mechanism as commands.post)
returns a ticket immediately and delivers the crew summary to a
defineAsyncCommand's completed(result) callback.
Threading model
A Jint engine is not bound to a thread, but it is single-drainer: one thread at a time
runs its event-loop jobs, and a drain started from inside a job cannot pump β it would wait
on jobs that only the thread it is blocking could run. The family of defects SCR-25 removed
(a crew run that hung after any top-level await, a state graph with suspending nodes that
timed out from a body, concurrent ctx.state.with calls that crashed the engine, a topic
handler or an FSM hook that suspends, an onDelta callback on a pool thread, runStream
not iterable) all came from CLR code re-entering the engine from the wrong side: after an
await, or by draining from inside a job. The runtime now follows one rule, and the
reproducers in tests/scripting/Orkeon.Scripting.Tests/Runtime/EngineThreadingContractTests.cs
pin it:
- Every loop that calls back into script code lives in JavaScript. The crew run
(
crew.run,crew.runAgent,crew.runStream), the state graph (run,runStream), the state machine (send),ctx.state.with, topic delivery (publish, the event'slock) and the script-facing side ofctx.llm.actare async functions β async generators for the streams, sofor awaitworks on them β built once from a JavaScript factory. The CLR supplies only synchronous helpers (snapshot the agents, open an attempt, record a result) andTasks the loop awaits (a semaphore acquisition, a retry delay, the run's cancellation). Every line of those loops runs as a promise reaction on whichever thread is draining. - A CLR callback never re-enters the engine after an
await. A delegate exposed to the script may return a value synchronously, aTaskwhose result is a CLR value (Jint settles it on the loop), or a JS promise obtained synchronously. It never callsInvoke,Evaluate,FromObjectorSetValuefrom a continuation, and never drains from inside a job. - A CLR helper reports failure as a JavaScript throw.
JsHostErrorwraps the exception in anErrorthat carries it onclr(its type name onclrType), so the script'scatchandfinallyrun, the loop releases what it holds, and the CLR side recovers the typed exception from the rejected value. - The CLR drives the engine at three root pumps only, each with the engine at rest, under
the per-engine gate, on one thread for the whole evaluation β the synchronous prefix and
every job after it:
ScriptHostfor the script itself,JsCrew.RunAsyncfor theglobalThis.crewhandoff and for C# hosts,JsTool.CallAsyncfor a script tool the orchestrator calls. From C#,JsCrew.run,runAgentandrunStreamare the JS functions themselves (JsValue);RunAsyncis the CLR entry, and it refuses to start from inside a CLR callback the script invoked β from a script, callcrew.run(). - Cancellation is the only bound on a wait;
ExecutionTimeoutbounds execution. No promise timeout remains in the runtime: a root pump drains until its promise settles or its token fires. The sandbox'sExecutionTimeout(wall-clock,Orkeon:Scripting:Limits) bounds the evaluation itself and now spans a whole CLR-driven run, as it already spanned a whole script (so doesMemoryLimitBytes). A cancelledRunAsyncleaves the loop a short grace to unwind β itsfinallyblocks,onCrewErrorβ before it abandons the run and releases what the CLR owns. A run a body opens without asignalof its own βcrew.runAgent, a sub-crew'srunβ is a child of the run that opened it: cancelled with it, unwound inside its unwind. The parent is the most recently opened attempt still open on the engine, exact under sequential nesting and best-effort under runs interleaved on one event loop;{ signal: ctx.signal }is the explicit form.
V1 limits
concurrency(N)capped at 1 (mutex). N-holders semaphore is V1.5.- Locks have no timeout.
LockTimeoutErroris V1.5. - Streaming through
ctx.llm.streamis per-token when the provider is anIStreamingLlmProvider(all 16 shipped providers are); the single full-text chunk is only the fallback for a non-streaming custom provider. ctx.llm.embedreturns a stub vector; integration with real embedders is a follow-up.- FSM/Graph hierarchical composability (sub-states, sub-graphs) is V1.5.
- Events are in-memory only (no Redis/NATS persistence).
Reference
This page says what the DSL is and where it sits; the typings say what it exposes. The crew
surface is orkeon.d.ts β built from the Typings/*.d.ts above, and emitted next to the CLI
build output. orkeon-cli.d.ts is a different file for a different surface: the *.cmd.ts
commands documented in cli-ts-commands.md.