Table of Contents

🇬🇧 English version

DSL de scripting — architecture

Le DSL de scripting d'Orkeon est un langage à syntaxe TypeScript embarqué dans le runtime, implémenté par le projet Orkeon.Scripting (et exposé via la CLI orkeon). C'est la manière recommandée d'écrire des crews quand on ne veut pas écrire de C#.

Pourquoi un DSL

Les auteurs familiers de l'outillage frontend bénéficient d'une expérience d'écriture en fichier unique (.ork.ts) sans étape de compilation de leur côté : le runtime supprime les types TypeScript via esbuild et exécute le JavaScript résultant dans Jint avec des limites de sandbox. Le DSL expose toute la surface d'Orkeon — agents, crews, tâches, outils personnalisés, machines à états, graphes, événements, locks, hooks de cycle de vie — via des builders fluents et des déclarations littérales.

Sa place dans la Clean Architecture

Orkeon.Scripting est un nouveau projet qui dépend de Orkeon.Domain, Orkeon.Application et Orkeon.Infrastructure. Il ne modifie pas ces couches : c'est un adaptateur opt-in qui traduit les appels côté JS en invocations de domaine existantes.

src/
└── scripting/
    ├── Orkeon.Scripting/         ← cette couche (runtime Jint + bindings)
    │   ├── ScriptHost.cs
    │   ├── JsEngineFactory.cs
    │   ├── Builders/             ← JsAgentBuilder, JsCrewBuilder, JsTaskBuilder, JsToolBuilder
    │   ├── Bindings/             ← enregistrements globaux (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>`

Démarrage rapide

Les tutoriels exécutables vivent dans examples/scripting/ (du hello world aux littéraux FSM/graphe et au RAG). De bout en bout :

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

La CLI émet le résultat du script en JSON sur stdout ; les codes de sortie suivent la convention habituelle (0 ok, 1 erreur de script, 2 erreur runtime, 130 annulé).

Le même verbe run accepte aussi un crew YAML — la cible sélectionne la voie (.yaml/.yml ou un dossier portant une crew multi-fichiers → runner de crew YAML, .ork.ts/.js → DSL de scripting) :

dotnet run --project src/scripting/Orkeon.Scripting.Cli -- run examples/09-experimental/llm-response-format/crew.yaml

Pour les crews YAML, l'outil délègue au runner one-shot partagé (le RunnerExecution d'Orkeon.Hosting — le chemin de code exact d'orkeon run), donc les drapeaux propres au YAML s'appliquent : -V/--var KEY=VALUE, --initial-context, plus les drapeaux partagés --settings/--mount/--allow-external-mounts/--verbose/--llm-log[-path] et les drapeaux diagnostics/protocole (--validate, --list-tools, --events jsonl, --stream, --client). Les drapeaux propres aux scripts (--inputs, --inputs-file, --memory-limit-mb) sont ignorés sur la voie YAML. Le runner YAML affiche la sortie du crew sous une bannière === Crew Output === au lieu d'un result JSON.

Récapitulatif de l'API

Deux pages s'appuient dessus : Écrire une crew en TypeScript pour le récit, et la Référence du DSL de scripting pour le tableau méthode par méthode — dont la colonne que les déclarations ne peuvent pas porter : laquelle des deux formes de script honore réellement chaque méthode.

Les déclarations sont la référence. Elles sont livrées avec le DSL, ce sont elles que lit votre éditeur, et elles vivent sous src/scripting/Orkeon.Scripting/Typings/ — concaténées au build dans le orkeon.d.ts que la CLI émet.

Concept Où regarder
agentBuilder() / crewBuilder() / taskBuilder() / toolBuilder() agent.d.ts, crew.d.ts, task.d.ts, tool.d.ts
ExecutionContext et AgentContext (ctx.llm, ctx.memory, A2A, locks, spawn) context.d.ts
ctx.llm.act — la boucle LLM ⇄ appels d'outils, et ses ActOptions context.d.ts
Événements (ctx.events.queue / ctx.events.topic) events.d.ts
Formes littérales stateMachine / stateGraph fsm.d.ts, graph.d.ts
onError, ErrorAction, codes d'erreur agent.d.ts, errors.d.ts
Hooks de cycle de vie (onAgentStart, onCrewComplete, …) agent.d.ts, crew.d.ts
onCommand — répondre par nom aux commandes CLI dispatchées cli-ts-commands.md
Namespace intégré tools.X(...) tools.d.ts
Providers LLM (llm.openai, llm.default, etc.) llm.d.ts
RAG (rag.ingest, rag.query) rag.d.ts

Coexistence avec YAML

La voie de configuration YAML reste supportée et inchangée. Les scripts et les crews YAML peuvent partager la même application hôte : le DSL est l'une des surfaces d'écriture parmi d'autres, pas un remplacement. L'outil orkeon publié exécute désormais les deux surfaces directement (orkeon run crew.yaml et orkeon run crew.ork.ts), si bien qu'un consommateur externe qui ne dépend que des packages publiés n'a plus à compiler un runner sur mesure pour exécuter des crews YAML.

Appels bloquants depuis les scripts

Jint exécute un script sur un seul thread : un appel hôte qui attend son résultat bloque tout le script — et le REPL qui l'héberge — jusqu'à son retour. Deux ponts CLI conservent délibérément ce contrat synchrone (audités comme ANT-007/ANT-010, « bloquants assumés ») :

  • ctx.services.get("script-host").runCrew(name, input?) — exécute un crew et attend sa sortie. Crews courts uniquement. L'attente est bornée par un timeout configurable (ScriptHostFacadeOptions.RunCrewTimeout, section de config Orkeon:Cli:ScriptHost, défaut 10 minutes) : à expiration, le script reçoit une TimeoutException claire, le run abandonné est annulé de manière coopérative, et le thread du REPL est toujours libéré (une boucle JS pure qui ignore l'annulation finit par être stoppée par l'ExecutionTimeout propre au bac à sable Jint).
  • ctx.services.get("commands").request(agent, intent, payload) — bloque jusqu'à la réponse de l'agent ; la même consigne s'applique (voir cli-ts-commands.md).

La voie nominale pour le travail long est le cycle à ticket : runCrewAsync(name, input?) (adossé à CommandDispatchService.postWork, même mécanisme que commands.post) retourne un ticket immédiatement et livre le résumé du crew au callback completed(result) d'un defineAsyncCommand.

Modèle de threading

Un moteur Jint n'est pas lié à un thread, mais il n'a qu'un seul draineur : un thread à la fois exécute les jobs de sa boucle d'événements, et un drain lancé depuis l'intérieur d'un job ne peut pas pomper — il attendrait des jobs que seul le thread qu'il bloque pourrait exécuter. La famille de défauts que SCR-25 a supprimée (un run de crew qui pendait après n'importe quel await de tête, un graphe d'états dont les nœuds suspendent qui expirait depuis un corps, des appels ctx.state.with concurrents qui faisaient planter le moteur, un handler de topic ou un hook FSM qui suspend, un callback onDelta sur un thread du pool, un runStream non itérable) venait toute de code CLR qui rentrait dans le moteur du mauvais côté : après un await, ou en drainant depuis l'intérieur d'un job. Le runtime suit désormais une règle, que les reproducteurs de tests/scripting/Orkeon.Scripting.Tests/Runtime/EngineThreadingContractTests.cs fixent :

  • Toute boucle qui rappelle du code de script vit en JavaScript. Le run de crew (crew.run, crew.runAgent, crew.runStream), le graphe d'états (run, runStream), la machine à états (send), ctx.state.with, la livraison des topics (publish, le lock de l'événement) et le côté script de ctx.llm.act sont des fonctions async — des générateurs async pour les flux, pour que for await fonctionne dessus — construites une fois depuis une fabrique JavaScript. Le CLR ne fournit que des aides synchrones (prendre l'instantané des agents, ouvrir une tentative, enregistrer un résultat) et des Task que la boucle attend (l'acquisition d'un sémaphore, un délai de retry, l'annulation du run). Chaque ligne de ces boucles s'exécute comme une réaction de promesse sur le thread qui draine, quel qu'il soit.
  • Un rappel CLR ne rentre jamais dans le moteur après un await. Un délégué exposé au script peut rendre une valeur synchrone, une Task dont le résultat est une valeur CLR (Jint la règle sur la boucle), ou une promesse JS obtenue de manière synchrone. Il n'appelle jamais Invoke, Evaluate, FromObject ni SetValue depuis une continuation, et ne draine jamais depuis l'intérieur d'un job.
  • Une aide CLR signale un échec par un throw JavaScript. JsHostError enveloppe l'exception dans une Error qui la porte sur clr (son nom de type sur clrType) : le catch et le finally du script s'exécutent, la boucle libère ce qu'elle tient, et le côté CLR retrouve l'exception typée depuis la valeur rejetée.
  • Le CLR ne pilote le moteur qu'à trois pompes racines, chacune moteur au repos, sous le verrou par moteur, sur un seul thread pour toute l'évaluation — le préfixe synchrone et chaque job qui suit : ScriptHost pour le script lui-même, JsCrew.RunAsync pour le handoff globalThis.crew et pour les hôtes C#, JsTool.CallAsync pour un outil script appelé par l'orchestrateur. Depuis C#, JsCrew.run, runAgent et runStream sont les fonctions JS elles-mêmes (JsValue) ; RunAsync est l'entrée CLR, et elle refuse de démarrer depuis un rappel CLR que le script a invoqué — depuis un script, appelez crew.run().
  • L'annulation est la seule borne d'une attente ; ExecutionTimeout borne l'exécution. Aucun timeout de promesse ne subsiste dans le runtime : une pompe racine draine jusqu'à ce que sa promesse se règle ou que son jeton se déclenche. L'ExecutionTimeout du bac à sable (temps mural, Orkeon:Scripting:Limits) borne l'évaluation elle-même et couvre désormais un run entier piloté par le CLR, comme il couvrait déjà un script entier (MemoryLimitBytes aussi). Un RunAsync annulé laisse à la boucle un court délai de grâce pour se dérouler — ses blocs finally, onCrewError — avant d'abandonner le run et de libérer ce que le CLR possède. Un run qu'un corps ouvre sans signal propre — crew.runAgent, le run d'une sous-crew — est l'enfant du run qui l'a ouvert : annulé avec lui, déroulé dans son déroulement. Le parent est la tentative ouverte la plus récente encore ouverte sur le moteur, exact en imbrication séquentielle et au mieux sous des runs entrelacés sur une même boucle d'événements ; { signal: ctx.signal } est la forme explicite.

Limites V1

  • concurrency(N) plafonné à 1 (mutex). Le sémaphore à N détenteurs est prévu en V1.5.
  • Les locks n'ont pas de timeout. LockTimeoutError est prévu en V1.5.
  • Le streaming via ctx.llm.stream est par token dès que le provider est un IStreamingLlmProvider (les 16 providers livrés le sont) ; le chunk unique en texte plein n'est que le repli d'un provider custom non-streaming.
  • ctx.llm.embed renvoie un vecteur stub ; l'intégration avec de vrais embedders est un chantier ultérieur.
  • La composabilité hiérarchique FSM/Graph (sub-states, sub-graphs) est prévue en V1.5.
  • Les événements sont uniquement en mémoire (pas de persistance Redis/NATS).

Référence

Cette page dit ce qu'est le DSL et où il se situe ; les typings disent ce qu'il expose. La surface des crews est orkeon.d.ts — construit depuis les Typings/*.d.ts ci-dessus, et émis à côté de la sortie de build de la CLI. orkeon-cli.d.ts est un autre fichier pour une autre surface : les commandes *.cmd.ts documentées dans cli-ts-commands.md.