Table of Contents

🇬🇧 English version

Le bus d'événements du run

PĂ©rimĂštre : orkeon run --events jsonl, le protocole qu'il parle, et le siĂšge qu'il donne Ă  un processus observateur sur l'EventHub du run. Public : quiconque pilote Orkeon depuis un autre programme — Studio, une passerelle, un job de CI, votre propre outillage.

Un run sans --events imprime du texte pour un humain. Avec, il parle un protocole versionnĂ© : un document JSON par ligne sur stdout, un par ligne sur stdin en retour. Rien d'autre ne change — mĂȘme crew, mĂȘme hĂŽte, mĂȘmes codes de sortie.


1. Pourquoi un protocole plutÎt qu'une sortie analysée

Le texte destiné à une personne change dÚs que quelqu'un en améliore la formulation. Un programme qui le lit casse sur une virgule. Le protocole existe pour que les deux publics cessent de partager un canal : les humains gardent le rendu, les programmes obtiennent un contrat.

Le contrat, c'est le flux sortant. L'entrant est dĂ©libĂ©rĂ©ment tolĂ©rant : une ligne malformĂ©e, un verbe inconnu, une charge utile inutilisable sont ignorĂ©s plutĂŽt que fatals. Un client qui envoie n'importe quoi ne doit pas arrĂȘter une crew qui fonctionnait.


2. L'enveloppe

Chaque ligne sortante est un objet portant ces clés, et elles seules comme noms réservés :

Clé Signification
v Version du protocole. Actuellement 2.
seq Numéro de séquence monotone dans le run, à partir de 1.
ts Horodatage ISO-8601 UTC.
kind Ce qui s'est passé (voir §3).
crewId, agentId Qui est concerné, quand le run le sait.
correlationId Lie une question Ă  sa rĂ©ponse, une requĂȘte Ă  sa rĂ©plique.
causationId L'événement qui a causé celui-ci, pour qu'un client reconstruise l'arbre.

Une clé absente est omise, jamais écrite à null. Un client traite tout champ d'identité comme optionnel.

Les huit noms ci-dessus sont rĂ©servĂ©s : un champ de charge utile qui entre en collision avec l'un d'eux est supprimĂ© plutĂŽt qu'autorisĂ© Ă  se faire passer pour l'enveloppe. Ce n'est pas thĂ©orique — la charge utile de l'entrĂ©e humaine appelait d'abord son champ kind et le perdait, d'oĂč le nom inputKind sur le fil.

Les champs de charge utile sont à plat à cÎté de l'enveloppe, pas imbriqués sous une clé payload. Seule exception : hub.message, dont la charge est opaque au protocole et transportée telle quelle.


3. Sortant — ce qu'un run dit

kind Charge utile Quand
run.started target, stream Le run commence.
task.started taskId, agentRole Une tĂąche commence, dans les six modes d'orchestration — au moment oĂč son agent est choisi, avant qu'on ne lui demande quoi que ce soit. Entre ce kind et son task.completed, un observateur montre la tĂąche en cours ; la charge utile est volontairement mince, rien n'a encore Ă©tĂ© mesurĂ©. Un CLI plus ancien ne dit rien ici, et un client ne doit pas refuser un task.completed dont il n'a jamais vu le dĂ©part.
task.completed taskId, agentRole, success, durationMs, tokens, toolCalls Chaque tĂąche se termine, dans les six modes d'orchestration — Ă©chec et annulation compris (voir error). tokens et toolCalls valent 0 quand le mode ne les mesure pas, jamais absents.
tool.called toolName, argsSummary? Un outil est invoqué. argsSummary résume les noms d'arguments, jamais leur contenu : un appel peut porter un fichier entier.
tool.returned toolName, success, durationMs L'outil a fini — y compris s'il a levĂ©, pour qu'un observateur n'affiche jamais une Ă©tape Ă©ternellement en cours. CorrĂ©lĂ© Ă  son tool.called.
delegation.started toRole? Un agent a confié du travail à un autre (l'outil delegate_work_to_coworker). La description de la tùche reste hors du flux, comme toute valeur d'argument.
agent.spawned role?, reason? L'Ă©quipe a grandi en cours d'exĂ©cution — Ă©mis quand un appel Ă  l'outil spawn_agent est observĂ©. rc.2 ne cĂąble cet outil sur aucun agent par dĂ©faut : ce kind n'apparaĂźt que dans les dĂ©ploiements qui l'attachent eux-mĂȘmes.
cost.updated tokens, model?, provider? Le compteur de jetons bouge. tokens est cumulatif ; model quand le fournisseur le rapporte ; provider seulement sur les appels de la façade de scripting. Aucun champ de prix : le framework n'a pas de table de prix, et en inventer une serait pire que l'omettre.
llm.delta text Un fragment de texte généré. Seulement sous --stream.
input.needed inputKind (text|confirm|choice), prompt, choices?, defaultValue?, taskDescription? Une tùche déclarée humanInput: true pose une question.
hub.message from?, topic?, payload?, expectsReply? Le hub du run a relayĂ© quelque chose Ă  ce processus. from est l'adresse hub de l'expĂ©diteur (agent://{crew}/{agent}, crew://{crew}), pour que le pair puisse attribuer et rĂ©pondre ; absent quand le hub ne la connaĂźt pas (la rĂ©ponse Ă  un send, appariĂ©e par correlationId). expectsReply: true n'apparaĂźt que sur le send d'un agent : l'expĂ©diteur est bloquĂ© en attente d'un reply sous son propre timeout, et le silence au-delĂ  est un refus. Le pair n'a pas Ă  deviner quelles lignes corrĂ©lĂ©es sont des questions — un relais de topic peut porter un correlationId lui aussi.
error code, message, recoverable Quelque chose a Ă©chouĂ©. Un run qui s'arrĂȘte — annulĂ© ou en Ă©chec, dans n'importe quel mode — se termine par code: crew_cancelled ou crew_failed avant run.finished.
run.finished success, exitCode, tokens Le run se termine.

Un run qui ne rapporte rien n'est pas un run qui se passe bien — c'est un run qui ne rapporte rien. Un client doit montrer la diffĂ©rence, pas la masquer.


4. Entrant — ce qu'un processus observateur peut dire

Un document JSON par ligne sur stdin. Chaque verbe se projette sur un membre d'IEventHub, sauf la réponse humaine :

kind Charge Effet
input.given correlationId?, value Répond à un input.needed en attente.
post to, payload IEventHub.PostAsync — Ă©crit dans une boĂźte aux lettres.
send to, payload, timeoutMs?, correlationId? SendAsync ; la réponse revient en hub.message corrélé.
publish topic, payload, retainAs? PublishAsync.
reply correlationId, payload Répond à une question qu'un agent a posée à ce processus.
subscribe / unsubscribe topic Ouvre ou ferme un relais de ce topic vers hub.message.

Un input.given sans identifiant de corrélation répond à la question en attente : un humain qui tape dans un terminal n'a pas d'identifiant à citer.

Le silence ne vaut pas consentement

Sans --events, une tĂąche dĂ©clarĂ©e humanInput: true est approuvĂ©e d'office — un repli dĂ©fendable pour un run non surveillĂ©, et la mauvaise rĂ©ponse dĂšs qu'un Ă©cran regarde. Demander le protocole remplace ce fournisseur : la question part sur le flux et le run attend.

Si aucune rĂ©ponse ne vient — canal fermĂ©, run annulĂ© — la confirmation est refusĂ©e, jamais accordĂ©e.


5. Le siĂšge au hub

Un processus observateur est adressable en client://{nom} (--client, défaut studio). Les agents lui écrivent exactement comme à un autre agent, et il peut écrire, publier et s'abonner en retour.

Qui a le droit de l'atteindre est la décision de la crew, pas celle du protocole. Une crew autorise l'échange en nommant le pair dans son bloc links: :

name: billing-crew
links:
  - to: "client:studio"
    direction: bidirectional
    allowed_topics: [run.progress]

Sans lien dĂ©clarĂ©, la politique par dĂ©faut de l'ACL laisse quand mĂȘme passer — le hub a Ă©tĂ© livrĂ© sans ACL, et refuser le trafic non dĂ©clarĂ© casserait toutes les crews existantes — mais une crew qui dĂ©clare un lien est tenue Ă  ce qu'elle a dĂ©clarĂ©. Les rĂšgles complĂštes sont dans EventHub §10.


6. Piloter un run depuis un autre programme

orkeon run crew.yaml --events jsonl --client mon-observateur

Lisez stdout ligne par ligne, analysez chaque ligne en JSON, aiguillez sur kind. Écrivez rĂ©ponses et commandes sur stdin, un document JSON par ligne, avec vidage du tampon. Trois faits sur lesquels un pilote peut compter :

  • Les deux dialectes le parlent. Une cible .ork.ts est observĂ©e par les mĂȘmes coutures qu'une crew YAML — outils, compteur de jetons, pont du hub et fournisseur de rĂ©ponses humaines arrivent tous jusqu'Ă  l'hĂŽte de script.
  • stdout porte le protocole et rien d'autre. Sur un run observĂ©, chaque ligne de log part sur stderr ; un log entre deux documents JSONL serait une erreur de parsing chez vous.
  • jsonl est la seule valeur que --events accepte, et il le dit plutĂŽt que de deviner ; --client sans --events avertit au lieu d'ĂȘtre ignorĂ© en silence.

Un échange minimal :

→ {"v":2,"seq":1,"ts":"
","kind":"run.started","target":"crew.yaml","stream":false}
→ {"v":2,"seq":2,"ts":"
","correlationId":"c-1","kind":"input.needed","inputKind":"confirm","prompt":"Publier le rapport ?"}
← {"kind":"input.given","correlationId":"c-1","value":"yes"}
→ {"v":2,"seq":3,"ts":"
","kind":"task.started","taskId":"t1","agentRole":"writer"}
→ {"v":2,"seq":4,"ts":"
","kind":"task.completed","taskId":"t1","agentRole":"writer","success":true,"durationMs":4200,"tokens":1840,"toolCalls":3}
→ {"v":2,"seq":5,"ts":"
","kind":"run.finished","success":true,"exitCode":0,"tokens":1840}

Deux rĂšgles Ă  respecter en construisant votre client. Ignorez un kind que vous ne connaissez pas — un Orkeon plus rĂ©cent en dit plus qu'un client plus ancien n'en comprend, et planter sur une ligne non lue est pire qu'en afficher un peu moins. Et gardez ce que vous n'avez pas su analyser : une ligne non protocolaire reste quelque chose que le run a dit, et la perdre perd le diagnostic.


7. Qui lit ceci aujourd'hui

  • Orkeon Studio, dont l'Ă©cran « Lancer » montre la progression — la tĂąche en cours et l'outil au travail, depuis task.started et tool.called, autant que les tĂąches terminĂ©es — le coĂ»t et les questions du run au lieu d'un dĂ©filement — voir Studio. L'Ă©cran « Lancer » occupe aussi le siĂšge du hub : le send d'un agent vers client://studio (marquĂ© expectsReply) apparaĂźt comme un panneau de demande auquel l'utilisateur rĂ©pond, et la rĂ©ponse repart par stdin ; les posts du hub sont listĂ©s au lieu d'ĂȘtre perdus. Le silence au-delĂ  du timeout propre Ă  l'agent reste un refus — la rĂšgle que le silence suit partout sur ce bus — l'Ă©cran donne simplement Ă  un humain la chance de parler avant.
  • Orkeon.Studio.Core.Run — RunClient et RunProgressModel, un client de rĂ©fĂ©rence en ~460 lignes, sans aucune dĂ©pendance Ă  Infrastructure ni Ă  un LLM. RunClient est la forme Ă  copier pour un pair qui prend le siĂšge sans Ă©cran : subscribe, post, reply.

La mĂȘme enveloppe porte le flux de l'Atelier, donc un client qui lit l'un lit l'autre.