Chat Event Orchestration
What It Is
Chat event orchestration is the host pattern for agent applications: register the DAG once, then run a fresh execution for every inbound message, queue event, browser action, or HTTP request turn.
The DAG remains the canonical assembly. DAGBuilder emits your graph as JSON-LD, Dagonizer registers it, DagRunner seeds and projects state, and EventTrigger or RequestTrigger adapts the outside world into runner.run(...).
How It Works
The host owns transport concerns. The DAG owns control flow. Each inbound event becomes:
- A raw host message or request.
- A typed runner input.
- A fresh state instance with correlation data, user text, and injected context.
- One
dispatcher.execute(dagIri, state, options)call throughDagRunner. - A projected output returned to the host, published to a socket, or written to a stream.
Long-lived subscriptions use EventTrigger. Request/response APIs use RequestTrigger. Both surfaces run the same registered DAG; they differ only in how the host decides when to fire.
Diagrams, Examples, and Outputs
The inner loop is a ReAct-style agent DAG authored with plain DAGBuilder: build request, call model, normalize the response, dispatch tools through scatter, collect observations, and loop until the assistant response is final.
/**
* 29-agent-dag/dags: explicit DAGBuilder 8-node agent loop.
*
* The exported `dag` is a JSON-LD document built from the same DAGBuilder
* surface applications use for custom graph authoring. Runnable examples
* provide concrete node instances with the same names and register this
* topology by name.
*/
import { DAGBuilder, DAGIdentity, PlaceholderNode } from '@studnicky/dagonizer';
import type { DAGType, NodeStateInterface } from '@studnicky/dagonizer';
export const dagIri = 'urn:noocodec:dag:my-agent' as const;
const placement = (placementIdentifier: string): string => DAGIdentity.placementId(dagIri, placementIdentifier);
const topologyNodes = {
'chatRequest': new PlaceholderNode<NodeStateInterface, 'ready' | 'error'>('urn:noocodec:node:build-request', ['ready', 'error']),
'callModel': new PlaceholderNode<NodeStateInterface, 'text' | 'tools' | 'mixed' | 'error'>('urn:noocodec:node:call-model', ['text', 'tools', 'mixed', 'error']),
'normalizeResponse': new PlaceholderNode<NodeStateInterface, 'text' | 'tools' | 'mixed' | 'empty' | 'error'>('urn:noocodec:node:normalize-response', ['text', 'tools', 'mixed', 'empty', 'error']),
'decodeTextToolCalls': new PlaceholderNode<NodeStateInterface, 'decoded' | 'empty' | 'error'>('urn:noocodec:node:decode-tools', ['decoded', 'empty', 'error']),
'normalizeToolCalls': new PlaceholderNode<NodeStateInterface, 'valid' | 'empty' | 'error'>('urn:noocodec:node:normalize-tools', ['valid', 'empty', 'error']),
'toolWorksets': new PlaceholderNode<NodeStateInterface, 'ready' | 'empty' | 'error'>('urn:noocodec:node:build-worksets', ['ready', 'empty', 'error']),
'collectToolResults': new PlaceholderNode<NodeStateInterface, 'done' | 'empty' | 'error'>('urn:noocodec:node:collect-results', ['done', 'empty', 'error']),
'appendAssistant': new PlaceholderNode<NodeStateInterface, 'done' | 'error'>('urn:noocodec:node:append-assistant', ['done', 'error']),
};
export const dag: DAGType = new DAGBuilder(dagIri, '1')
.node(placement('build-request'), topologyNodes.chatRequest, {
'ready': placement('call-model'),
'error': placement('end-error'),
})
.node(placement('call-model'), topologyNodes.callModel, {
'text': placement('normalize-response'),
'tools': placement('normalize-response'),
'mixed': placement('normalize-response'),
'error': placement('end-error'),
})
.node(placement('normalize-response'), topologyNodes.normalizeResponse, {
'text': placement('append-assistant'),
'tools': placement('decode-tools'),
'mixed': placement('decode-tools'),
'empty': placement('end-error'),
'error': placement('end-error'),
})
.node(placement('append-assistant'), topologyNodes.appendAssistant, {
'done': placement('end-done'),
'error': placement('end-error'),
})
.node(placement('decode-tools'), topologyNodes.decodeTextToolCalls, {
'decoded': placement('normalize-tools'),
'empty': placement('end-error'),
'error': placement('end-error'),
})
.node(placement('normalize-tools'), topologyNodes.normalizeToolCalls, {
'valid': placement('worksets'),
'empty': placement('end-error'),
'error': placement('end-error'),
})
.node(placement('worksets'), topologyNodes.toolWorksets, {
'ready': placement('dispatch-tools'),
'empty': placement('end-error'),
'error': placement('end-error'),
})
.scatter(
placement('dispatch-tools'),
'safeWorkset',
{ 'dag': { 'from': 'item', 'path': 'dagIri', 'candidates': ['urn:noocodec:tool:calculator'] } },
{
'all-success': placement('join-tool-results'),
'partial': placement('join-tool-results'),
'all-error': placement('join-tool-results'),
'empty': placement('join-tool-results'),
},
{
'itemKey': 'currentItem',
},
)
.gather(placement('join-tool-results'), { [placement('dispatch-tools')]: {} }, {
'strategy': 'map',
'mapping': { 'output': 'toolOutputs' },
}, {
'success': placement('collect-results'),
'error': placement('end-error'),
'empty': placement('collect-results'),
})
.node(placement('collect-results'), topologyNodes.collectToolResults, {
'done': placement('build-request'),
'empty': placement('build-request'),
'error': placement('end-error'),
})
.terminal(placement('end-done'))
.terminal(placement('end-error'), { 'outcome': 'failed' })
.build();Agent event loop DAG
12 placements{
"@context": {
"@version": 1.1,
"name": {
"@id": "https://noocodec.dev/ontology/dag/name"
},
"version": {
"@id": "https://noocodec.dev/ontology/dag/version"
},
"entrypoints": {
"@id": "https://noocodec.dev/ontology/dag/entrypoints",
"@container": "@index"
},
"nodes": {
"@id": "https://noocodec.dev/ontology/dag/nodes",
"@container": "@set"
},
"outputs": {
"@id": "https://noocodec.dev/ontology/dag/outputs"
},
"node": {
"@id": "https://noocodec.dev/ontology/dag/node"
},
"dag": {
"@id": "https://noocodec.dev/ontology/dag/dag"
},
"body": {
"@id": "https://noocodec.dev/ontology/dag/body"
},
"source": {
"@id": "https://noocodec.dev/ontology/dag/source"
},
"sources": {
"@id": "https://noocodec.dev/ontology/dag/sources",
"@container": "@index"
},
"itemKey": {
"@id": "https://noocodec.dev/ontology/dag/itemKey"
},
"execution": {
"@id": "https://noocodec.dev/ontology/dag/execution"
},
"concurrency": {
"@id": "https://noocodec.dev/ontology/dag/concurrency"
},
"throttle": {
"@id": "https://noocodec.dev/ontology/dag/throttle"
},
"reservoir": {
"@id": "https://noocodec.dev/ontology/dag/reservoir"
},
"gather": {
"@id": "https://noocodec.dev/ontology/dag/gather"
},
"dagReference": {
"@id": "https://noocodec.dev/ontology/dag/dagReference",
"@type": "@id"
},
"DagReference": {
"@id": "https://noocodec.dev/ontology/dag/DagReference"
},
"from": {
"@id": "https://noocodec.dev/ontology/dag/from"
},
"path": {
"@id": "https://noocodec.dev/ontology/dag/path"
},
"candidates": {
"@id": "https://noocodec.dev/ontology/dag/candidates",
"@container": "@set"
},
"candidateDag": {
"@id": "https://noocodec.dev/ontology/dag/candidateDag",
"@type": "@id"
},
"selectedDag": {
"@id": "https://noocodec.dev/ontology/dag/selectedDag",
"@type": "@id"
},
"resultField": {
"@id": "https://noocodec.dev/ontology/dag/resultField"
},
"policy": {
"@id": "https://noocodec.dev/ontology/dag/policy"
},
"reducer": {
"@id": "https://noocodec.dev/ontology/dag/reducer"
},
"outcome": {
"@id": "https://noocodec.dev/ontology/dag/outcome"
},
"phase": {
"@id": "https://noocodec.dev/ontology/dag/phase"
},
"stateMapping": {
"@id": "https://noocodec.dev/ontology/dag/stateMapping"
},
"container": {
"@id": "https://noocodec.dev/ontology/dag/container"
},
"DAG": {
"@id": "https://noocodec.dev/ontology/dag/DAG"
},
"Placement": {
"@id": "https://noocodec.dev/ontology/dag/Placement"
},
"SingleNode": {
"@id": "https://noocodec.dev/ontology/dag/SingleNode"
},
"ScatterNode": {
"@id": "https://noocodec.dev/ontology/dag/ScatterNode"
},
"EmbeddedDAGNode": {
"@id": "https://noocodec.dev/ontology/dag/EmbeddedDAGNode"
},
"GatherNode": {
"@id": "https://noocodec.dev/ontology/dag/GatherNode"
},
"TerminalNode": {
"@id": "https://noocodec.dev/ontology/dag/TerminalNode"
},
"PhaseNode": {
"@id": "https://noocodec.dev/ontology/dag/PhaseNode"
}
},
"@id": "urn:noocodec:dag:react-agent",
"@type": "DAG",
"name": "react-agent",
"version": "1",
"entrypoints": {
"main": "urn:noocodec:dag:react-agent/node/build-request"
},
"nodes": [
{
"@id": "urn:noocodec:dag:react-agent/node/build-request",
"@type": "SingleNode",
"name": "build-request",
"node": "urn:noocodec:node:build-request",
"outputs": {
"ready": "urn:noocodec:dag:react-agent/node/call-model",
"error": "urn:noocodec:dag:react-agent/node/end-error"
}
},
{
"@id": "urn:noocodec:dag:react-agent/node/call-model",
"@type": "SingleNode",
"name": "call-model",
"node": "urn:noocodec:node:call-model",
"outputs": {
"text": "urn:noocodec:dag:react-agent/node/normalize-response",
"tools": "urn:noocodec:dag:react-agent/node/normalize-response",
"mixed": "urn:noocodec:dag:react-agent/node/normalize-response",
"error": "urn:noocodec:dag:react-agent/node/end-error"
}
},
{
"@id": "urn:noocodec:dag:react-agent/node/normalize-response",
"@type": "SingleNode",
"name": "normalize-response",
"node": "urn:noocodec:node:normalize-response",
"outputs": {
"text": "urn:noocodec:dag:react-agent/node/append-assistant",
"tools": "urn:noocodec:dag:react-agent/node/decode-tools",
"mixed": "urn:noocodec:dag:react-agent/node/decode-tools",
"empty": "urn:noocodec:dag:react-agent/node/end-error",
"error": "urn:noocodec:dag:react-agent/node/end-error"
}
},
{
"@id": "urn:noocodec:dag:react-agent/node/append-assistant",
"@type": "SingleNode",
"name": "append-assistant",
"node": "urn:noocodec:node:append-assistant",
"outputs": {
"done": "urn:noocodec:dag:react-agent/node/end-done",
"error": "urn:noocodec:dag:react-agent/node/end-error"
}
},
{
"@id": "urn:noocodec:dag:react-agent/node/decode-tools",
"@type": "SingleNode",
"name": "decode-tools",
"node": "urn:noocodec:node:decode-tools",
"outputs": {
"decoded": "urn:noocodec:dag:react-agent/node/normalize-tools",
"empty": "urn:noocodec:dag:react-agent/node/end-error",
"error": "urn:noocodec:dag:react-agent/node/end-error"
}
},
{
"@id": "urn:noocodec:dag:react-agent/node/normalize-tools",
"@type": "SingleNode",
"name": "normalize-tools",
"node": "urn:noocodec:node:normalize-tools",
"outputs": {
"valid": "urn:noocodec:dag:react-agent/node/worksets",
"empty": "urn:noocodec:dag:react-agent/node/end-error",
"error": "urn:noocodec:dag:react-agent/node/end-error"
}
},
{
"@id": "urn:noocodec:dag:react-agent/node/worksets",
"@type": "SingleNode",
"name": "worksets",
"node": "urn:noocodec:node:build-worksets",
"outputs": {
"ready": "urn:noocodec:dag:react-agent/node/dispatch-tools",
"empty": "urn:noocodec:dag:react-agent/node/end-error",
"error": "urn:noocodec:dag:react-agent/node/end-error"
}
},
{
"@id": "urn:noocodec:dag:react-agent/node/dispatch-tools",
"@type": "ScatterNode",
"name": "dispatch-tools",
"source": "safeWorkset",
"body": {
"dag": {
"@type": "DagReference",
"from": "item",
"path": "dagIri",
"candidates": [
"urn:noocodec:tool:lookup"
]
}
},
"outputs": {
"all-success": "urn:noocodec:dag:react-agent/node/join-tool-results",
"partial": "urn:noocodec:dag:react-agent/node/join-tool-results",
"all-error": "urn:noocodec:dag:react-agent/node/join-tool-results",
"empty": "urn:noocodec:dag:react-agent/node/join-tool-results"
},
"itemKey": "currentItem",
"reducer": "aggregate"
},
{
"@id": "urn:noocodec:dag:react-agent/node/join-tool-results",
"@type": "GatherNode",
"name": "join-tool-results",
"sources": {
"urn:noocodec:dag:react-agent/node/dispatch-tools": {}
},
"gather": {
"strategy": "map",
"mapping": {
"output": "toolOutputs"
}
},
"outputs": {
"success": "urn:noocodec:dag:react-agent/node/collect-results",
"error": "urn:noocodec:dag:react-agent/node/end-error",
"empty": "urn:noocodec:dag:react-agent/node/collect-results"
}
},
{
"@id": "urn:noocodec:dag:react-agent/node/collect-results",
"@type": "SingleNode",
"name": "collect-results",
"node": "urn:noocodec:node:collect-results",
"outputs": {
"done": "urn:noocodec:dag:react-agent/node/build-request",
"empty": "urn:noocodec:dag:react-agent/node/build-request",
"error": "urn:noocodec:dag:react-agent/node/end-error"
}
},
{
"@id": "urn:noocodec:dag:react-agent/node/end-done",
"@type": "TerminalNode",
"name": "end-done",
"outcome": "completed"
},
{
"@id": "urn:noocodec:dag:react-agent/node/end-error",
"@type": "TerminalNode",
"name": "end-error",
"outcome": "failed"
}
]
}Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
%% react-agent (v1)
entry_main(["main"])
entry_main --> urn_noocodec_dag_react-agent/node/build-request
urn_noocodec_dag_react-agent/node/build-request["build-request"]
urn_noocodec_dag_react-agent/node/build-request -->|ready| urn_noocodec_dag_react-agent/node/call-model
urn_noocodec_dag_react-agent/node/build-request -->|error| urn_noocodec_dag_react-agent/node/end-error
urn_noocodec_dag_react-agent/node/call-model["call-model"]
urn_noocodec_dag_react-agent/node/call-model -->|text| urn_noocodec_dag_react-agent/node/normalize-response
urn_noocodec_dag_react-agent/node/call-model -->|tools| urn_noocodec_dag_react-agent/node/normalize-response
urn_noocodec_dag_react-agent/node/call-model -->|mixed| urn_noocodec_dag_react-agent/node/normalize-response
urn_noocodec_dag_react-agent/node/call-model -->|error| urn_noocodec_dag_react-agent/node/end-error
urn_noocodec_dag_react-agent/node/normalize-response["normalize-response"]
urn_noocodec_dag_react-agent/node/normalize-response -->|text| urn_noocodec_dag_react-agent/node/append-assistant
urn_noocodec_dag_react-agent/node/normalize-response -->|tools| urn_noocodec_dag_react-agent/node/decode-tools
urn_noocodec_dag_react-agent/node/normalize-response -->|mixed| urn_noocodec_dag_react-agent/node/decode-tools
urn_noocodec_dag_react-agent/node/normalize-response -->|empty| urn_noocodec_dag_react-agent/node/end-error
urn_noocodec_dag_react-agent/node/normalize-response -->|error| urn_noocodec_dag_react-agent/node/end-error
urn_noocodec_dag_react-agent/node/append-assistant["append-assistant"]
urn_noocodec_dag_react-agent/node/append-assistant -->|done| urn_noocodec_dag_react-agent/node/end-done
urn_noocodec_dag_react-agent/node/append-assistant -->|error| urn_noocodec_dag_react-agent/node/end-error
urn_noocodec_dag_react-agent/node/decode-tools["decode-tools"]
urn_noocodec_dag_react-agent/node/decode-tools -->|decoded| urn_noocodec_dag_react-agent/node/normalize-tools
urn_noocodec_dag_react-agent/node/decode-tools -->|empty| urn_noocodec_dag_react-agent/node/end-error
urn_noocodec_dag_react-agent/node/decode-tools -->|error| urn_noocodec_dag_react-agent/node/end-error
urn_noocodec_dag_react-agent/node/normalize-tools["normalize-tools"]
urn_noocodec_dag_react-agent/node/normalize-tools -->|valid| urn_noocodec_dag_react-agent/node/worksets
urn_noocodec_dag_react-agent/node/normalize-tools -->|empty| urn_noocodec_dag_react-agent/node/end-error
urn_noocodec_dag_react-agent/node/normalize-tools -->|error| urn_noocodec_dag_react-agent/node/end-error
urn_noocodec_dag_react-agent/node/worksets["worksets"]
urn_noocodec_dag_react-agent/node/worksets -->|ready| urn_noocodec_dag_react-agent/node/dispatch-tools
urn_noocodec_dag_react-agent/node/worksets -->|empty| urn_noocodec_dag_react-agent/node/end-error
urn_noocodec_dag_react-agent/node/worksets -->|error| urn_noocodec_dag_react-agent/node/end-error
urn_noocodec_dag_react-agent/node/dispatch-tools[/"dispatch-tools"/]
urn_noocodec_dag_react-agent/node/dispatch-tools -->|all-success| urn_noocodec_dag_react-agent/node/join-tool-results
urn_noocodec_dag_react-agent/node/dispatch-tools -->|partial| urn_noocodec_dag_react-agent/node/join-tool-results
urn_noocodec_dag_react-agent/node/dispatch-tools -->|all-error| urn_noocodec_dag_react-agent/node/join-tool-results
urn_noocodec_dag_react-agent/node/dispatch-tools -->|empty| urn_noocodec_dag_react-agent/node/join-tool-results
urn_noocodec_dag_react-agent/node/join-tool-results{"join-tool-results"}
urn_noocodec_dag_react-agent/node/join-tool-results -->|success| urn_noocodec_dag_react-agent/node/collect-results
urn_noocodec_dag_react-agent/node/join-tool-results -->|error| urn_noocodec_dag_react-agent/node/end-error
urn_noocodec_dag_react-agent/node/join-tool-results -->|empty| urn_noocodec_dag_react-agent/node/collect-results
urn_noocodec_dag_react-agent/node/collect-results["collect-results"]
urn_noocodec_dag_react-agent/node/collect-results -->|done| urn_noocodec_dag_react-agent/node/build-request
urn_noocodec_dag_react-agent/node/collect-results -->|empty| urn_noocodec_dag_react-agent/node/build-request
urn_noocodec_dag_react-agent/node/collect-results -->|error| urn_noocodec_dag_react-agent/node/end-error
urn_noocodec_dag_react-agent/node/end-done((("end-done")))
urn_noocodec_dag_react-agent/node/end-error>"end-error"]The Dispatcher runnable shows how a real in-browser support flow surrounds that style of agent work: classify an inbound message, compose a response or park for an operator, converge on send-response, and end the turn.
const setup = new PlaceholderNode<DispatcherState, 'ready'>('urn:noocodec:node:dispatcher-setup', ['ready']);
const classifyMessage = new PlaceholderNode<DispatcherState, 'routine' | 'escalate' | 'off-topic'>('urn:noocodec:node:classify-message', ['routine', 'escalate', 'off-topic']);
const aiCompose = new PlaceholderNode<DispatcherState, 'drafted'>('urn:noocodec:node:ai-compose', ['drafted']);
const parkForOperator = new PlaceholderNode<DispatcherState, 'parked' | 'ready'>('urn:noocodec:node:park-for-operator', ['parked', 'ready']);
const sendResponse = new PlaceholderNode<DispatcherState, 'sent'>('urn:noocodec:node:send-response', ['sent']);
const decline = new PlaceholderNode<DispatcherState, 'declined'>('urn:noocodec:node:decline', ['declined']);
const supportDispatcherDagIri = 'urn:noocodec:dag:support-dispatcher' as const;
const placement = (placementIdentifier: string): string =>
DAGIdentity.placementId(supportDispatcherDagIri, placementIdentifier);
export const supportDispatcherDAG: DAGType = new DAGBuilder(supportDispatcherDagIri, '1')
// Pre-phase: stamps runId before the entrypoint runs.
.phase(placement('setup'), 'pre', setup)
// Entrypoint: classify the inbound message.
.node(placement('classify-message'), classifyMessage, {
'routine': placement('ai-compose'),
'escalate': placement('park-for-operator'),
'off-topic': placement('decline'),
})
// Routine branch: AI composes a canned reply -> send -> done.
.node(placement('ai-compose'), aiCompose, {
'drafted': placement('send-response'),
})
// Escalation branch: HITL suspension point.
.node(placement('park-for-operator'), parkForOperator, {
'parked': placement('end'),
'ready': placement('send-response'),
})
// Shared convergence: both routine and escalated paths flow through send-response.
.node(placement('send-response'), sendResponse, {
'sent': placement('end'),
})
// Off-topic branch: decline and close.
.node(placement('decline'), decline, {
'declined': placement('end'),
})
.terminal(placement('end'), { 'outcome': 'completed' })
.build();Dispatcher support conversation DAG
7 placements{
"@context": {
"@version": 1.1,
"name": {
"@id": "https://noocodec.dev/ontology/dag/name"
},
"version": {
"@id": "https://noocodec.dev/ontology/dag/version"
},
"entrypoints": {
"@id": "https://noocodec.dev/ontology/dag/entrypoints",
"@container": "@index"
},
"nodes": {
"@id": "https://noocodec.dev/ontology/dag/nodes",
"@container": "@set"
},
"outputs": {
"@id": "https://noocodec.dev/ontology/dag/outputs"
},
"node": {
"@id": "https://noocodec.dev/ontology/dag/node"
},
"dag": {
"@id": "https://noocodec.dev/ontology/dag/dag"
},
"body": {
"@id": "https://noocodec.dev/ontology/dag/body"
},
"source": {
"@id": "https://noocodec.dev/ontology/dag/source"
},
"sources": {
"@id": "https://noocodec.dev/ontology/dag/sources",
"@container": "@index"
},
"itemKey": {
"@id": "https://noocodec.dev/ontology/dag/itemKey"
},
"execution": {
"@id": "https://noocodec.dev/ontology/dag/execution"
},
"concurrency": {
"@id": "https://noocodec.dev/ontology/dag/concurrency"
},
"throttle": {
"@id": "https://noocodec.dev/ontology/dag/throttle"
},
"reservoir": {
"@id": "https://noocodec.dev/ontology/dag/reservoir"
},
"gather": {
"@id": "https://noocodec.dev/ontology/dag/gather"
},
"dagReference": {
"@id": "https://noocodec.dev/ontology/dag/dagReference",
"@type": "@id"
},
"DagReference": {
"@id": "https://noocodec.dev/ontology/dag/DagReference"
},
"from": {
"@id": "https://noocodec.dev/ontology/dag/from"
},
"path": {
"@id": "https://noocodec.dev/ontology/dag/path"
},
"candidates": {
"@id": "https://noocodec.dev/ontology/dag/candidates",
"@container": "@set"
},
"candidateDag": {
"@id": "https://noocodec.dev/ontology/dag/candidateDag",
"@type": "@id"
},
"selectedDag": {
"@id": "https://noocodec.dev/ontology/dag/selectedDag",
"@type": "@id"
},
"resultField": {
"@id": "https://noocodec.dev/ontology/dag/resultField"
},
"policy": {
"@id": "https://noocodec.dev/ontology/dag/policy"
},
"reducer": {
"@id": "https://noocodec.dev/ontology/dag/reducer"
},
"outcome": {
"@id": "https://noocodec.dev/ontology/dag/outcome"
},
"phase": {
"@id": "https://noocodec.dev/ontology/dag/phase"
},
"stateMapping": {
"@id": "https://noocodec.dev/ontology/dag/stateMapping"
},
"container": {
"@id": "https://noocodec.dev/ontology/dag/container"
},
"DAG": {
"@id": "https://noocodec.dev/ontology/dag/DAG"
},
"Placement": {
"@id": "https://noocodec.dev/ontology/dag/Placement"
},
"SingleNode": {
"@id": "https://noocodec.dev/ontology/dag/SingleNode"
},
"ScatterNode": {
"@id": "https://noocodec.dev/ontology/dag/ScatterNode"
},
"EmbeddedDAGNode": {
"@id": "https://noocodec.dev/ontology/dag/EmbeddedDAGNode"
},
"GatherNode": {
"@id": "https://noocodec.dev/ontology/dag/GatherNode"
},
"TerminalNode": {
"@id": "https://noocodec.dev/ontology/dag/TerminalNode"
},
"PhaseNode": {
"@id": "https://noocodec.dev/ontology/dag/PhaseNode"
}
},
"@id": "urn:noocodec:dag:support-dispatcher",
"@type": "DAG",
"name": "dag:support-dispatcher",
"version": "1",
"entrypoints": {
"main": "urn:noocodec:dag:support-dispatcher/node/classify-message"
},
"nodes": [
{
"@id": "urn:noocodec:dag:support-dispatcher/node/setup",
"@type": "PhaseNode",
"name": "dag:support-dispatcher/node/setup",
"node": "urn:noocodec:node:dispatcher-setup",
"phase": "pre"
},
{
"@id": "urn:noocodec:dag:support-dispatcher/node/classify-message",
"@type": "SingleNode",
"name": "dag:support-dispatcher/node/classify-message",
"node": "urn:noocodec:node:classify-message",
"outputs": {
"routine": "urn:noocodec:dag:support-dispatcher/node/ai-compose",
"escalate": "urn:noocodec:dag:support-dispatcher/node/park-for-operator",
"off-topic": "urn:noocodec:dag:support-dispatcher/node/decline"
}
},
{
"@id": "urn:noocodec:dag:support-dispatcher/node/ai-compose",
"@type": "SingleNode",
"name": "dag:support-dispatcher/node/ai-compose",
"node": "urn:noocodec:node:ai-compose",
"outputs": {
"drafted": "urn:noocodec:dag:support-dispatcher/node/send-response"
}
},
{
"@id": "urn:noocodec:dag:support-dispatcher/node/park-for-operator",
"@type": "SingleNode",
"name": "dag:support-dispatcher/node/park-for-operator",
"node": "urn:noocodec:node:park-for-operator",
"outputs": {
"parked": "urn:noocodec:dag:support-dispatcher/node/end",
"ready": "urn:noocodec:dag:support-dispatcher/node/send-response"
}
},
{
"@id": "urn:noocodec:dag:support-dispatcher/node/send-response",
"@type": "SingleNode",
"name": "dag:support-dispatcher/node/send-response",
"node": "urn:noocodec:node:send-response",
"outputs": {
"sent": "urn:noocodec:dag:support-dispatcher/node/end"
}
},
{
"@id": "urn:noocodec:dag:support-dispatcher/node/decline",
"@type": "SingleNode",
"name": "dag:support-dispatcher/node/decline",
"node": "urn:noocodec:node:decline",
"outputs": {
"declined": "urn:noocodec:dag:support-dispatcher/node/end"
}
},
{
"@id": "urn:noocodec:dag:support-dispatcher/node/end",
"@type": "TerminalNode",
"name": "dag:support-dispatcher/node/end",
"outcome": "completed"
}
]
}Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
%% dag:support-dispatcher (v1)
entry_main(["main"])
entry_main --> urn_noocodec_dag_support-dispatcher/node/classify-message
urn_noocodec_dag_support-dispatcher/node/setup(["dag:support-dispatcher/node/setup (pre)"])
urn_noocodec_dag_support-dispatcher/node/classify-message["dag:support-dispatcher/node/classify-message"]
urn_noocodec_dag_support-dispatcher/node/classify-message -->|routine| urn_noocodec_dag_support-dispatcher/node/ai-compose
urn_noocodec_dag_support-dispatcher/node/classify-message -->|escalate| urn_noocodec_dag_support-dispatcher/node/park-for-operator
urn_noocodec_dag_support-dispatcher/node/classify-message -->|off-topic| urn_noocodec_dag_support-dispatcher/node/decline
urn_noocodec_dag_support-dispatcher/node/ai-compose["dag:support-dispatcher/node/ai-compose"]
urn_noocodec_dag_support-dispatcher/node/ai-compose -->|drafted| urn_noocodec_dag_support-dispatcher/node/send-response
urn_noocodec_dag_support-dispatcher/node/park-for-operator["dag:support-dispatcher/node/park-for-operator"]
urn_noocodec_dag_support-dispatcher/node/park-for-operator -->|parked| urn_noocodec_dag_support-dispatcher/node/end
urn_noocodec_dag_support-dispatcher/node/park-for-operator -->|ready| urn_noocodec_dag_support-dispatcher/node/send-response
urn_noocodec_dag_support-dispatcher/node/send-response["dag:support-dispatcher/node/send-response"]
urn_noocodec_dag_support-dispatcher/node/send-response -->|sent| urn_noocodec_dag_support-dispatcher/node/end
urn_noocodec_dag_support-dispatcher/node/decline["dag:support-dispatcher/node/decline"]
urn_noocodec_dag_support-dispatcher/node/decline -->|declined| urn_noocodec_dag_support-dispatcher/node/end
urn_noocodec_dag_support-dispatcher/node/end((("dag:support-dispatcher/node/end")))The ReAct memory and routing examples show the two pieces chat hosts usually need after the first turn: trace persistence and concurrent stream demultiplexing.
export const traceDag: DAGType = {
'@context': DAG_CONTEXT,
'@id': 'urn:noocodec:dag:react-agent-memory-trace',
'@type': 'DAG',
'name': 'react-agent-memory-trace',
'version': '1',
'entrypoints': { 'main': 'urn:noocodec:dag:react-agent-memory-trace/node/scatter-steps' },
'nodes': [
{
'@id': 'urn:noocodec:dag:react-agent-memory-trace/node/scatter-steps',
'@type': 'ScatterNode',
'name': 'scatter-steps',
'body': { 'node': 'urn:noocodec:node:record-step' },
'source': 'source',
'itemKey': REASONING_ITEM_KEY,
// A free performance choice, not a correctness requirement:
// RecordReasoningStepNode derives the wasInformedBy chain from each
// item's own `ordinal` field, so raising this is safe at any value.
'execution': { 'mode': 'item', 'concurrency': 1 },
'outputs': {
'all-success': 'urn:noocodec:dag:react-agent-memory-trace/node/collect-steps',
'partial': 'urn:noocodec:dag:react-agent-memory-trace/node/collect-steps',
'all-error': 'urn:noocodec:dag:react-agent-memory-trace/node/collect-steps',
'empty': 'urn:noocodec:dag:react-agent-memory-trace/node/end',
},
},
{
'@id': 'urn:noocodec:dag:react-agent-memory-trace/node/collect-steps',
'@type': 'GatherNode',
'name': 'collect-steps',
sources: { 'urn:noocodec:dag:react-agent-memory-trace/node/scatter-steps': {} },
'gather': { 'strategy': 'append', 'target': 'steps' },
'outputs': {
'success': 'urn:noocodec:dag:react-agent-memory-trace/node/end',
'error': 'urn:noocodec:dag:react-agent-memory-trace/node/end',
'empty': 'urn:noocodec:dag:react-agent-memory-trace/node/end',
},
},
{
'@id': 'urn:noocodec:dag:react-agent-memory-trace/node/end',
'@type': 'TerminalNode',
'name': 'end',
'outcome': 'completed',
},
],
};Reasoning trace memory DAG
3 placements{
"@context": {
"@version": 1.1,
"name": {
"@id": "https://noocodec.dev/ontology/dag/name"
},
"version": {
"@id": "https://noocodec.dev/ontology/dag/version"
},
"entrypoints": {
"@id": "https://noocodec.dev/ontology/dag/entrypoints",
"@container": "@index"
},
"nodes": {
"@id": "https://noocodec.dev/ontology/dag/nodes",
"@container": "@set"
},
"outputs": {
"@id": "https://noocodec.dev/ontology/dag/outputs"
},
"node": {
"@id": "https://noocodec.dev/ontology/dag/node"
},
"dag": {
"@id": "https://noocodec.dev/ontology/dag/dag"
},
"body": {
"@id": "https://noocodec.dev/ontology/dag/body"
},
"source": {
"@id": "https://noocodec.dev/ontology/dag/source"
},
"sources": {
"@id": "https://noocodec.dev/ontology/dag/sources",
"@container": "@index"
},
"itemKey": {
"@id": "https://noocodec.dev/ontology/dag/itemKey"
},
"execution": {
"@id": "https://noocodec.dev/ontology/dag/execution"
},
"concurrency": {
"@id": "https://noocodec.dev/ontology/dag/concurrency"
},
"throttle": {
"@id": "https://noocodec.dev/ontology/dag/throttle"
},
"reservoir": {
"@id": "https://noocodec.dev/ontology/dag/reservoir"
},
"gather": {
"@id": "https://noocodec.dev/ontology/dag/gather"
},
"dagReference": {
"@id": "https://noocodec.dev/ontology/dag/dagReference",
"@type": "@id"
},
"DagReference": {
"@id": "https://noocodec.dev/ontology/dag/DagReference"
},
"from": {
"@id": "https://noocodec.dev/ontology/dag/from"
},
"path": {
"@id": "https://noocodec.dev/ontology/dag/path"
},
"candidates": {
"@id": "https://noocodec.dev/ontology/dag/candidates",
"@container": "@set"
},
"candidateDag": {
"@id": "https://noocodec.dev/ontology/dag/candidateDag",
"@type": "@id"
},
"selectedDag": {
"@id": "https://noocodec.dev/ontology/dag/selectedDag",
"@type": "@id"
},
"resultField": {
"@id": "https://noocodec.dev/ontology/dag/resultField"
},
"policy": {
"@id": "https://noocodec.dev/ontology/dag/policy"
},
"reducer": {
"@id": "https://noocodec.dev/ontology/dag/reducer"
},
"outcome": {
"@id": "https://noocodec.dev/ontology/dag/outcome"
},
"phase": {
"@id": "https://noocodec.dev/ontology/dag/phase"
},
"stateMapping": {
"@id": "https://noocodec.dev/ontology/dag/stateMapping"
},
"container": {
"@id": "https://noocodec.dev/ontology/dag/container"
},
"DAG": {
"@id": "https://noocodec.dev/ontology/dag/DAG"
},
"Placement": {
"@id": "https://noocodec.dev/ontology/dag/Placement"
},
"SingleNode": {
"@id": "https://noocodec.dev/ontology/dag/SingleNode"
},
"ScatterNode": {
"@id": "https://noocodec.dev/ontology/dag/ScatterNode"
},
"EmbeddedDAGNode": {
"@id": "https://noocodec.dev/ontology/dag/EmbeddedDAGNode"
},
"GatherNode": {
"@id": "https://noocodec.dev/ontology/dag/GatherNode"
},
"TerminalNode": {
"@id": "https://noocodec.dev/ontology/dag/TerminalNode"
},
"PhaseNode": {
"@id": "https://noocodec.dev/ontology/dag/PhaseNode"
}
},
"@id": "urn:noocodec:dag:react-agent-memory-trace",
"@type": "DAG",
"name": "react-agent-memory-trace",
"version": "1",
"entrypoints": {
"main": "urn:noocodec:dag:react-agent-memory-trace/node/scatter-steps"
},
"nodes": [
{
"@id": "urn:noocodec:dag:react-agent-memory-trace/node/scatter-steps",
"@type": "ScatterNode",
"name": "scatter-steps",
"body": {
"node": "urn:noocodec:node:record-step"
},
"source": "source",
"itemKey": "reasoning-step-item",
"execution": {
"mode": "item",
"concurrency": 1
},
"outputs": {
"all-success": "urn:noocodec:dag:react-agent-memory-trace/node/collect-steps",
"partial": "urn:noocodec:dag:react-agent-memory-trace/node/collect-steps",
"all-error": "urn:noocodec:dag:react-agent-memory-trace/node/collect-steps",
"empty": "urn:noocodec:dag:react-agent-memory-trace/node/end"
}
},
{
"@id": "urn:noocodec:dag:react-agent-memory-trace/node/collect-steps",
"@type": "GatherNode",
"name": "collect-steps",
"sources": {
"urn:noocodec:dag:react-agent-memory-trace/node/scatter-steps": {}
},
"gather": {
"strategy": "append",
"target": "steps"
},
"outputs": {
"success": "urn:noocodec:dag:react-agent-memory-trace/node/end",
"error": "urn:noocodec:dag:react-agent-memory-trace/node/end",
"empty": "urn:noocodec:dag:react-agent-memory-trace/node/end"
}
},
{
"@id": "urn:noocodec:dag:react-agent-memory-trace/node/end",
"@type": "TerminalNode",
"name": "end",
"outcome": "completed"
}
]
}Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
%% react-agent-memory-trace (v1)
entry_main(["main"])
entry_main --> urn_noocodec_dag_react-agent-memory-trace/node/scatter-steps
urn_noocodec_dag_react-agent-memory-trace/node/scatter-steps[/"scatter-steps"/]
urn_noocodec_dag_react-agent-memory-trace/node/scatter-steps -->|all-success| urn_noocodec_dag_react-agent-memory-trace/node/collect-steps
urn_noocodec_dag_react-agent-memory-trace/node/scatter-steps -->|partial| urn_noocodec_dag_react-agent-memory-trace/node/collect-steps
urn_noocodec_dag_react-agent-memory-trace/node/scatter-steps -->|all-error| urn_noocodec_dag_react-agent-memory-trace/node/collect-steps
urn_noocodec_dag_react-agent-memory-trace/node/scatter-steps -->|empty| urn_noocodec_dag_react-agent-memory-trace/node/end
urn_noocodec_dag_react-agent-memory-trace/node/collect-steps{"collect-steps"}
urn_noocodec_dag_react-agent-memory-trace/node/collect-steps -->|success| urn_noocodec_dag_react-agent-memory-trace/node/end
urn_noocodec_dag_react-agent-memory-trace/node/collect-steps -->|error| urn_noocodec_dag_react-agent-memory-trace/node/end
urn_noocodec_dag_react-agent-memory-trace/node/collect-steps -->|empty| urn_noocodec_dag_react-agent-memory-trace/node/end
urn_noocodec_dag_react-agent-memory-trace/node/end((("end")))export const routingDag: DAGType = {
'@context': DAG_CONTEXT,
'@id': 'urn:noocodec:dag:react-agent-routing',
'@type': 'DAG',
'name': 'react-agent-routing',
'version': '1',
'entrypoints': { 'main': 'urn:noocodec:dag:react-agent-routing/node/scatter-chunks' },
'nodes': [
{
'@id': 'urn:noocodec:dag:react-agent-routing/node/scatter-chunks',
'@type': 'ScatterNode',
'name': 'scatter-chunks',
'body': { 'node': 'urn:noocodec:node:route-chunk' },
'source': 'source',
'itemKey': ROUTE_ITEM_KEY,
// Chunks route by their own `routeKey`; TranscriptStore.append is safe
// under any interleaving (single-threaded event loop), so raising this
// is a free performance choice, not a correctness requirement.
'execution': { 'mode': 'item', 'concurrency': 4 },
'outputs': {
'all-success': 'urn:noocodec:dag:react-agent-routing/node/collect-chunks',
'partial': 'urn:noocodec:dag:react-agent-routing/node/collect-chunks',
'all-error': 'urn:noocodec:dag:react-agent-routing/node/collect-chunks',
'empty': 'urn:noocodec:dag:react-agent-routing/node/end',
},
},
{
'@id': 'urn:noocodec:dag:react-agent-routing/node/collect-chunks',
'@type': 'GatherNode',
'name': 'collect-chunks',
sources: { 'urn:noocodec:dag:react-agent-routing/node/scatter-chunks': {} },
'gather': { 'strategy': 'append', 'target': 'chunks' },
'outputs': {
'success': 'urn:noocodec:dag:react-agent-routing/node/end',
'error': 'urn:noocodec:dag:react-agent-routing/node/end',
'empty': 'urn:noocodec:dag:react-agent-routing/node/end',
},
},
{
'@id': 'urn:noocodec:dag:react-agent-routing/node/end',
'@type': 'TerminalNode',
'name': 'end',
'outcome': 'completed',
},
],
};Routed stream sink DAG
3 placements{
"@context": {
"@version": 1.1,
"name": {
"@id": "https://noocodec.dev/ontology/dag/name"
},
"version": {
"@id": "https://noocodec.dev/ontology/dag/version"
},
"entrypoints": {
"@id": "https://noocodec.dev/ontology/dag/entrypoints",
"@container": "@index"
},
"nodes": {
"@id": "https://noocodec.dev/ontology/dag/nodes",
"@container": "@set"
},
"outputs": {
"@id": "https://noocodec.dev/ontology/dag/outputs"
},
"node": {
"@id": "https://noocodec.dev/ontology/dag/node"
},
"dag": {
"@id": "https://noocodec.dev/ontology/dag/dag"
},
"body": {
"@id": "https://noocodec.dev/ontology/dag/body"
},
"source": {
"@id": "https://noocodec.dev/ontology/dag/source"
},
"sources": {
"@id": "https://noocodec.dev/ontology/dag/sources",
"@container": "@index"
},
"itemKey": {
"@id": "https://noocodec.dev/ontology/dag/itemKey"
},
"execution": {
"@id": "https://noocodec.dev/ontology/dag/execution"
},
"concurrency": {
"@id": "https://noocodec.dev/ontology/dag/concurrency"
},
"throttle": {
"@id": "https://noocodec.dev/ontology/dag/throttle"
},
"reservoir": {
"@id": "https://noocodec.dev/ontology/dag/reservoir"
},
"gather": {
"@id": "https://noocodec.dev/ontology/dag/gather"
},
"dagReference": {
"@id": "https://noocodec.dev/ontology/dag/dagReference",
"@type": "@id"
},
"DagReference": {
"@id": "https://noocodec.dev/ontology/dag/DagReference"
},
"from": {
"@id": "https://noocodec.dev/ontology/dag/from"
},
"path": {
"@id": "https://noocodec.dev/ontology/dag/path"
},
"candidates": {
"@id": "https://noocodec.dev/ontology/dag/candidates",
"@container": "@set"
},
"candidateDag": {
"@id": "https://noocodec.dev/ontology/dag/candidateDag",
"@type": "@id"
},
"selectedDag": {
"@id": "https://noocodec.dev/ontology/dag/selectedDag",
"@type": "@id"
},
"resultField": {
"@id": "https://noocodec.dev/ontology/dag/resultField"
},
"policy": {
"@id": "https://noocodec.dev/ontology/dag/policy"
},
"reducer": {
"@id": "https://noocodec.dev/ontology/dag/reducer"
},
"outcome": {
"@id": "https://noocodec.dev/ontology/dag/outcome"
},
"phase": {
"@id": "https://noocodec.dev/ontology/dag/phase"
},
"stateMapping": {
"@id": "https://noocodec.dev/ontology/dag/stateMapping"
},
"container": {
"@id": "https://noocodec.dev/ontology/dag/container"
},
"DAG": {
"@id": "https://noocodec.dev/ontology/dag/DAG"
},
"Placement": {
"@id": "https://noocodec.dev/ontology/dag/Placement"
},
"SingleNode": {
"@id": "https://noocodec.dev/ontology/dag/SingleNode"
},
"ScatterNode": {
"@id": "https://noocodec.dev/ontology/dag/ScatterNode"
},
"EmbeddedDAGNode": {
"@id": "https://noocodec.dev/ontology/dag/EmbeddedDAGNode"
},
"GatherNode": {
"@id": "https://noocodec.dev/ontology/dag/GatherNode"
},
"TerminalNode": {
"@id": "https://noocodec.dev/ontology/dag/TerminalNode"
},
"PhaseNode": {
"@id": "https://noocodec.dev/ontology/dag/PhaseNode"
}
},
"@id": "urn:noocodec:dag:react-agent-routing",
"@type": "DAG",
"name": "react-agent-routing",
"version": "1",
"entrypoints": {
"main": "urn:noocodec:dag:react-agent-routing/node/scatter-chunks"
},
"nodes": [
{
"@id": "urn:noocodec:dag:react-agent-routing/node/scatter-chunks",
"@type": "ScatterNode",
"name": "scatter-chunks",
"body": {
"node": "urn:noocodec:node:route-chunk"
},
"source": "source",
"itemKey": "routed-chunk-item",
"execution": {
"mode": "item",
"concurrency": 4
},
"outputs": {
"all-success": "urn:noocodec:dag:react-agent-routing/node/collect-chunks",
"partial": "urn:noocodec:dag:react-agent-routing/node/collect-chunks",
"all-error": "urn:noocodec:dag:react-agent-routing/node/collect-chunks",
"empty": "urn:noocodec:dag:react-agent-routing/node/end"
}
},
{
"@id": "urn:noocodec:dag:react-agent-routing/node/collect-chunks",
"@type": "GatherNode",
"name": "collect-chunks",
"sources": {
"urn:noocodec:dag:react-agent-routing/node/scatter-chunks": {}
},
"gather": {
"strategy": "append",
"target": "chunks"
},
"outputs": {
"success": "urn:noocodec:dag:react-agent-routing/node/end",
"error": "urn:noocodec:dag:react-agent-routing/node/end",
"empty": "urn:noocodec:dag:react-agent-routing/node/end"
}
},
{
"@id": "urn:noocodec:dag:react-agent-routing/node/end",
"@type": "TerminalNode",
"name": "end",
"outcome": "completed"
}
]
}Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
%% react-agent-routing (v1)
entry_main(["main"])
entry_main --> urn_noocodec_dag_react-agent-routing/node/scatter-chunks
urn_noocodec_dag_react-agent-routing/node/scatter-chunks[/"scatter-chunks"/]
urn_noocodec_dag_react-agent-routing/node/scatter-chunks -->|all-success| urn_noocodec_dag_react-agent-routing/node/collect-chunks
urn_noocodec_dag_react-agent-routing/node/scatter-chunks -->|partial| urn_noocodec_dag_react-agent-routing/node/collect-chunks
urn_noocodec_dag_react-agent-routing/node/scatter-chunks -->|all-error| urn_noocodec_dag_react-agent-routing/node/collect-chunks
urn_noocodec_dag_react-agent-routing/node/scatter-chunks -->|empty| urn_noocodec_dag_react-agent-routing/node/end
urn_noocodec_dag_react-agent-routing/node/collect-chunks{"collect-chunks"}
urn_noocodec_dag_react-agent-routing/node/collect-chunks -->|success| urn_noocodec_dag_react-agent-routing/node/end
urn_noocodec_dag_react-agent-routing/node/collect-chunks -->|error| urn_noocodec_dag_react-agent-routing/node/end
urn_noocodec_dag_react-agent-routing/node/collect-chunks -->|empty| urn_noocodec_dag_react-agent-routing/node/end
urn_noocodec_dag_react-agent-routing/node/end((("end")))What It Lets You Do
Use this pattern when an app needs to accept chat events from several host shapes without making the DAG depend on sockets, HTTP frameworks, queues, or UI components.
The same agent graph can serve:
- A browser demo button through a request-like trigger.
- A WebSocket or event-bus subscription through
EventTrigger. - An HTTP route through
RequestTrigger. - A CLI or worker command through the same
DagRunnersubclass. - Concurrent conversations through per-run
conversationIdorrouteKeyfields on state.
Code Samples
Author the agent DAG
The framework does not own a prebuilt agent loop. Use DAGBuilder to author the graph your application needs, then register concrete node subclasses referenced by that JSON-LD. Core gives you the assembly language (DAGBuilder and JSON-LD), the registry, and the host trigger surfaces; your application owns the topology.
Example 29: Agent DAG with JSON-LD is the runnable reference for this section. It shows the full agent loop, explicit placement IRIs, the first-class GatherNode after tool scatter, and the registered tool DAG candidates. The diagram above is generated from that same source, so the topology and prose share one grimoire instead of a hand-copied spell.
The tool scatter reads safeWorkset, dispatches each item through a dynamic DagReference, gathers output into toolOutputs via a GatherNode, and loops through collect-results -> build-request. State field names are application-owned; DAG and placement identity stays IRI-owned.
Put host code behind DagRunner
DagRunner is the only place the host converts inbound input into state and final state into output.
import { DagRunner } from '@studnicky/dagonizer/runner';
import type { ExecutionResultType } from '@studnicky/dagonizer';
type ChatTurnInput = {
conversationId: string;
message: string;
};
type ChatTurnOutput = {
conversationId: string;
reply: string;
completed: boolean;
};
class ChatTurnRunner extends DagRunner<ChatTurnInput, AgentState, ChatTurnOutput> {
protected override seedState(input: ChatTurnInput): AgentState {
const state = new AgentState();
state.conversationId = input.conversationId;
state.prompt = input.message;
return state;
}
protected override projectResult(result: ExecutionResultType<AgentState>): ChatTurnOutput {
return {
conversationId: result.state.conversationId,
reply: result.state.assistantText,
completed: result.state.lifecycle.variant === 'completed',
};
}
}The runner is deliberately boring. Keep authentication, sockets, framework request objects, and UI handles outside the DAG. Put only serializable per-run facts on state.
Fire one run per subscription event
Use EventTrigger when the host is a subscription: WebSocket messages, queue items, DOM events, app-level event buses, or server push.
import { EventTrigger } from '@studnicky/dagonizer/runner';
type InboundChatMessage = {
conversationId: string;
body: string;
};
class ChatMessageTrigger extends EventTrigger<InboundChatMessage, ChatTurnInput, AgentState, ChatTurnOutput> {
constructor(private readonly bus: ChatBus) {
super();
}
protected override subscribe(onMessage: (message: InboundChatMessage) => void): () => void {
return this.bus.subscribe('message', onMessage);
}
protected override toInput(message: InboundChatMessage): ChatTurnInput {
return {
conversationId: message.conversationId,
message: message.body,
};
}
protected override selectDag(_message: InboundChatMessage): string {
return 'workflow:support-agent';
}
}EventTrigger intentionally does not await each run inside the subscription handler. That makes inbound events independent. If the host needs strict per-conversation ordering, enforce it in the host queue keyed by conversationId, not by sharing mutable node state.
Fire one run per request turn
Use RequestTrigger when the caller expects a returned value for the current request.
import { RequestTrigger } from '@studnicky/dagonizer/runner';
import type { ExecuteOptionsType } from '@studnicky/dagonizer';
type ChatHttpRequest = {
body: { conversationId: string; message: string };
signal: AbortSignal;
};
class ChatRequestTrigger extends RequestTrigger<ChatHttpRequest, ChatTurnInput, AgentState, ChatTurnOutput> {
protected override toInput(request: ChatHttpRequest): ChatTurnInput {
return {
conversationId: request.body.conversationId,
message: request.body.message,
};
}
protected override selectDag(_request: ChatHttpRequest): string {
return 'workflow:support-agent';
}
protected override requestOptions(request: ChatHttpRequest): ExecuteOptionsType {
return {
signal: request.signal,
deadlineMs: 30_000,
};
}
}RequestTrigger.fire(request) returns the runner's projected output. This is the right fit for browser demos, server routes, chat webhooks, and turn-based APIs.
Details for Nerds
Event trigger vs. request trigger
| Host shape | Trigger | Execution model | Output path |
|---|---|---|---|
| WebSocket, queue, app event bus | EventTrigger | Subscribes once, fires parallel runner.run(...) calls per message | Host publishes projected output asynchronously |
| HTTP route, webhook, browser action | RequestTrigger | Stores the runner on attach, fires when the route calls fire(request) | fire(...) resolves to the projected output |
| CLI or one-shot worker | CliTrigger / OnceTrigger | Fires once | Trigger stores the result after attach |
The DAG should not care which trigger invokes it. If the topology changes when the transport changes, the app is mixing host concerns into workflow concerns.
Correlation and stream routing
Every concurrent chat run needs a stable key on state: conversationId, runId, tenant id, or route key. Nodes read that key when they emit progress, stream model tokens, or write trace records.
The ReAct routing example demonstrates the sink side. One shared stream sink receives chunks from multiple conversations, and the routing DAG scatters over those chunks by routeKey so the transcripts do not bleed together.
Abort and deadline propagation
RequestTrigger.requestOptions(request) is the host boundary for cancellation. Forward the request's AbortSignal and set a deadline there. The engine passes that signal through node context and embedded/scatter execution, so nodes and adapters can stop work without importing the host framework.
Registration checklist
- Register every concrete node instance once on the dispatcher.
- Register tool DAGs or plugin bundles before the agent DAG if
dispatch-toolsresolves them from an item field such asdagIri; the field value is a DAG IRI/CURIE. - Return the DAG IRI from
selectDag; the TypeScript parameter is nameddagName, but registry lookup expands it as an IRI/CURIE. - Seed a fresh state per event or request; do not reuse a state object across concurrent runs.
- Put per-run ids on state and use them for stream routing, trace persistence, and response correlation.
Related Concepts
- Conversational Agents - turn structure, HITL, and the agent-loop node responsibilities
- Runner -
DagRunner,EventTrigger, andRequestTriggerAPI reference - ReAct Agent - trace streaming, token streaming, provenance recall, and route-key demultiplexing
- The Dispatcher - runnable in-browser support flow
- Example 29: Agent DAG with JSON-LD - runnable
DAGBuilderagent DAG