Example 02: DAGBuilder
What It Is
Example 02 shows the Archivist parent DAG authored with DAGBuilder instead of a hand-written object literal. The output is still the same JSON-LD DAG the dispatcher consumes; the builder only makes authoring safer and easier to review.
Use this page when you want TypeScript to help with graph assembly: route exhaustiveness, typed output names, auto-entrypoint selection, scatter placement options, embedded DAG mappings, and one final .build() that returns the canonical document.
How It Works
Each builder call appends a placement to the DAG document. The node instance supplies the output union, and the route object must cover that union. If a node can return 'retry', the route map needs a 'retry' key. If it does not, TypeScript complains before the docs, tests, or browser demo get involved.
build() freezes the assembly into a plain DAG value with a DAG IRI, placement IRIs, labeled entrypoints, and output targets. From that point forward, the builder disappears. Registration, serialization, visualization, plugins, and execution all see normal JSON-LD.
Diagrams, Examples, and Outputs
The diagram below is generated from the built Archivist DAG, not from a separate drawing. That matters: every .node(), .embed(), and .scatter() call in the source has a corresponding shape in the rendered graph.
DAG registration and diagram
The diagram is the same Archivist parent DAG that the dispatcher consumes at runtime. .build() returns this JSON-LD document directly; there is no second runtime DSL or projection layer.
The Archivist parent DAG
55 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:the-archivist",
"@type": "DAG",
"name": "the-archivist",
"version": "6.0",
"entrypoints": {
"main": "urn:noocodec:dag:the-archivist/node/park-for-input"
},
"nodes": [
{
"@id": "urn:noocodec:dag:the-archivist/node/setup",
"@type": "PhaseNode",
"name": "setup",
"node": "urn:noocodec:node:pre-run-setup",
"phase": "pre"
},
{
"@id": "urn:noocodec:dag:the-archivist/node/park-for-input",
"@type": "SingleNode",
"name": "park-for-input",
"node": "urn:noocodec:node:park-for-input",
"outputs": {
"parked": "urn:noocodec:dag:the-archivist/node/park-for-input",
"resumed": "urn:noocodec:dag:the-archivist/node/recall-context"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recall-context",
"@type": "SingleNode",
"name": "recall-context",
"node": "urn:noocodec:node:recall-context",
"outputs": {
"recalled": "urn:noocodec:dag:the-archivist/node/classify-intent"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/classify-intent",
"@type": "SingleNode",
"name": "classify-intent",
"node": "urn:noocodec:node:classify-intent",
"outputs": {
"lookup-author": "urn:noocodec:dag:the-archivist/node/author-search",
"find-reviews": "urn:noocodec:dag:the-archivist/node/reviews-extract",
"describe-book": "urn:noocodec:dag:the-archivist/node/describe-extract",
"recommend-similar": "urn:noocodec:dag:the-archivist/node/recommend-similar",
"recall-memories": "urn:noocodec:dag:the-archivist/node/memory-recall",
"on-topic": "urn:noocodec:dag:the-archivist/node/on-topic-search",
"recommend-top-rated": "urn:noocodec:dag:the-archivist/node/recommend-extract",
"off-topic": "urn:noocodec:dag:the-archivist/node/decline-off-topic",
"retry": "urn:noocodec:dag:the-archivist/node/classify-intent",
"salvage": "urn:noocodec:dag:the-archivist/node/classify-intent-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/classify-intent-salvage",
"@type": "SingleNode",
"name": "classify-intent-salvage",
"node": "urn:noocodec:node:classify-intent-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/on-topic-search"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/on-topic-search",
"@type": "EmbeddedDAGNode",
"name": "on-topic-search",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/compose-loop",
"error": "urn:noocodec:dag:the-archivist/node/compose-empty"
},
"dag": "urn:noocodec:dag:book-search-scatter",
"stateMapping": {
"output": {
"terms": "terms",
"toolPlan": "toolPlan",
"candidates": "candidates",
"shortlist": "shortlist",
"priorContext": "priorContext",
"failureCause": "failureCause"
}
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/author-search",
"@type": "EmbeddedDAGNode",
"name": "author-search",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/group-by-year",
"error": "urn:noocodec:dag:the-archivist/node/compose-empty"
},
"dag": "urn:noocodec:dag:book-search-scatter",
"stateMapping": {
"output": {
"terms": "terms",
"toolPlan": "toolPlan",
"candidates": "candidates",
"shortlist": "shortlist",
"priorContext": "priorContext",
"failureCause": "failureCause"
}
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/group-by-year",
"@type": "SingleNode",
"name": "group-by-year",
"node": "urn:noocodec:node:group-by-year",
"outputs": {
"ordered": "urn:noocodec:dag:the-archivist/node/compose-loop"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-extract",
"@type": "SingleNode",
"name": "reviews-extract",
"node": "urn:noocodec:node:extract-query",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools",
"retry": "urn:noocodec:dag:the-archivist/node/reviews-extract",
"salvage": "urn:noocodec:dag:the-archivist/node/reviews-extract-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-extract-salvage",
"@type": "SingleNode",
"name": "reviews-extract-salvage",
"node": "urn:noocodec:node:extract-query-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools",
"@type": "SingleNode",
"name": "reviews-decide-tools",
"node": "urn:noocodec:node:decide-tools",
"outputs": {
"tools": "urn:noocodec:dag:the-archivist/node/reviews-build-worksets",
"no-tools": "urn:noocodec:dag:the-archivist/node/reviews-build-worksets",
"retry": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools",
"salvage": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools-salvage",
"@type": "SingleNode",
"name": "reviews-decide-tools-salvage",
"node": "urn:noocodec:node:decide-tools-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/reviews-build-worksets"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-build-worksets",
"@type": "SingleNode",
"name": "reviews-build-worksets",
"node": "urn:noocodec:node:build-book-worksets",
"outputs": {
"ready": "urn:noocodec:dag:the-archivist/node/reviews-scatter"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-scatter",
"@type": "ScatterNode",
"name": "reviews-scatter",
"source": "bookWorksets",
"body": {
"dag": {
"@type": "DagReference",
"from": "item",
"path": "dagIri",
"candidates": [
"urn:noocodec:tool:web_search_books",
"urn:noocodec:tool:google_books_search",
"urn:noocodec:tool:subject_search",
"urn:noocodec:tool:wikipedia_summary"
]
}
},
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/reviews-gather",
"error": "urn:noocodec:dag:the-archivist/node/reviews-gather",
"empty": "urn:noocodec:dag:the-archivist/node/reviews-rank"
},
"itemKey": "currentItem",
"reducer": "any-success",
"execution": {
"mode": "item",
"concurrency": 4
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-gather",
"@type": "GatherNode",
"name": "reviews-gather",
"sources": {
"urn:noocodec:dag:the-archivist/node/reviews-scatter": {}
},
"gather": {
"strategy": "tool-candidate-merge"
},
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/reviews-rank",
"error": "urn:noocodec:dag:the-archivist/node/reviews-rank",
"empty": "urn:noocodec:dag:the-archivist/node/reviews-rank"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-rank",
"@type": "SingleNode",
"name": "reviews-rank",
"node": "urn:noocodec:node:rank-by-rating",
"outputs": {
"ranked": "urn:noocodec:dag:the-archivist/node/reviews-merge"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-merge",
"@type": "SingleNode",
"name": "reviews-merge",
"node": "urn:noocodec:node:merge-candidates",
"outputs": {
"ranked": "urn:noocodec:dag:the-archivist/node/reviews-record",
"empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-record",
"@type": "SingleNode",
"name": "reviews-record",
"node": "urn:noocodec:node:record-findings",
"outputs": {
"recorded": "urn:noocodec:dag:the-archivist/node/reviews-gate"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-gate",
"@type": "SingleNode",
"name": "reviews-gate",
"node": "urn:noocodec:node:has-citations-gate",
"outputs": {
"pass": "urn:noocodec:dag:the-archivist/node/reviews-recall",
"fail": "urn:noocodec:dag:the-archivist/node/compose-empty"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/reviews-recall",
"@type": "SingleNode",
"name": "reviews-recall",
"node": "urn:noocodec:node:recall-past-visits",
"outputs": {
"recalled": "urn:noocodec:dag:the-archivist/node/compose-loop"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-extract",
"@type": "SingleNode",
"name": "recommend-extract",
"node": "urn:noocodec:node:extract-query",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools",
"retry": "urn:noocodec:dag:the-archivist/node/recommend-extract",
"salvage": "urn:noocodec:dag:the-archivist/node/recommend-extract-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-extract-salvage",
"@type": "SingleNode",
"name": "recommend-extract-salvage",
"node": "urn:noocodec:node:extract-query-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools",
"@type": "SingleNode",
"name": "recommend-decide-tools",
"node": "urn:noocodec:node:decide-tools",
"outputs": {
"tools": "urn:noocodec:dag:the-archivist/node/recommend-build-worksets",
"no-tools": "urn:noocodec:dag:the-archivist/node/recommend-build-worksets",
"retry": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools",
"salvage": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools-salvage",
"@type": "SingleNode",
"name": "recommend-decide-tools-salvage",
"node": "urn:noocodec:node:decide-tools-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/recommend-build-worksets"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-build-worksets",
"@type": "SingleNode",
"name": "recommend-build-worksets",
"node": "urn:noocodec:node:build-book-worksets",
"outputs": {
"ready": "urn:noocodec:dag:the-archivist/node/recommend-scatter"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-scatter",
"@type": "ScatterNode",
"name": "recommend-scatter",
"source": "bookWorksets",
"body": {
"dag": {
"@type": "DagReference",
"from": "item",
"path": "dagIri",
"candidates": [
"urn:noocodec:tool:web_search_books",
"urn:noocodec:tool:google_books_search",
"urn:noocodec:tool:subject_search",
"urn:noocodec:tool:wikipedia_summary"
]
}
},
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/recommend-gather",
"error": "urn:noocodec:dag:the-archivist/node/recommend-gather",
"empty": "urn:noocodec:dag:the-archivist/node/recommend-rank"
},
"itemKey": "currentItem",
"reducer": "any-success",
"execution": {
"mode": "item",
"concurrency": 4
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-gather",
"@type": "GatherNode",
"name": "recommend-gather",
"sources": {
"urn:noocodec:dag:the-archivist/node/recommend-scatter": {}
},
"gather": {
"strategy": "tool-candidate-merge"
},
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/recommend-rank",
"error": "urn:noocodec:dag:the-archivist/node/recommend-rank",
"empty": "urn:noocodec:dag:the-archivist/node/recommend-rank"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-rank",
"@type": "SingleNode",
"name": "recommend-rank",
"node": "urn:noocodec:node:rank-by-rating",
"outputs": {
"ranked": "urn:noocodec:dag:the-archivist/node/recommend-merge"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-merge",
"@type": "SingleNode",
"name": "recommend-merge",
"node": "urn:noocodec:node:merge-candidates",
"outputs": {
"ranked": "urn:noocodec:dag:the-archivist/node/recommend-record",
"empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-record",
"@type": "SingleNode",
"name": "recommend-record",
"node": "urn:noocodec:node:record-findings",
"outputs": {
"recorded": "urn:noocodec:dag:the-archivist/node/recommend-gate"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-gate",
"@type": "SingleNode",
"name": "recommend-gate",
"node": "urn:noocodec:node:has-citations-gate",
"outputs": {
"pass": "urn:noocodec:dag:the-archivist/node/recommend-recall",
"fail": "urn:noocodec:dag:the-archivist/node/compose-empty"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-recall",
"@type": "SingleNode",
"name": "recommend-recall",
"node": "urn:noocodec:node:recall-past-visits",
"outputs": {
"recalled": "urn:noocodec:dag:the-archivist/node/compose-loop"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-extract",
"@type": "SingleNode",
"name": "describe-extract",
"node": "urn:noocodec:node:extract-query",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/describe-decide-tools",
"retry": "urn:noocodec:dag:the-archivist/node/describe-extract",
"salvage": "urn:noocodec:dag:the-archivist/node/describe-extract-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-extract-salvage",
"@type": "SingleNode",
"name": "describe-extract-salvage",
"node": "urn:noocodec:node:extract-query-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/describe-decide-tools"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-decide-tools",
"@type": "SingleNode",
"name": "describe-decide-tools",
"node": "urn:noocodec:node:decide-tools",
"outputs": {
"tools": "urn:noocodec:dag:the-archivist/node/describe-build-worksets",
"no-tools": "urn:noocodec:dag:the-archivist/node/describe-build-worksets",
"retry": "urn:noocodec:dag:the-archivist/node/describe-decide-tools",
"salvage": "urn:noocodec:dag:the-archivist/node/describe-decide-tools-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-decide-tools-salvage",
"@type": "SingleNode",
"name": "describe-decide-tools-salvage",
"node": "urn:noocodec:node:decide-tools-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/describe-build-worksets"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-build-worksets",
"@type": "SingleNode",
"name": "describe-build-worksets",
"node": "urn:noocodec:node:build-book-worksets",
"outputs": {
"ready": "urn:noocodec:dag:the-archivist/node/describe-scatter"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-scatter",
"@type": "ScatterNode",
"name": "describe-scatter",
"source": "bookWorksets",
"body": {
"dag": {
"@type": "DagReference",
"from": "item",
"path": "dagIri",
"candidates": [
"urn:noocodec:tool:web_search_books",
"urn:noocodec:tool:google_books_search",
"urn:noocodec:tool:subject_search",
"urn:noocodec:tool:wikipedia_summary"
]
}
},
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/describe-gather",
"error": "urn:noocodec:dag:the-archivist/node/compose-empty",
"empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
},
"itemKey": "currentItem",
"reducer": "any-success",
"execution": {
"mode": "item",
"concurrency": 4
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-gather",
"@type": "GatherNode",
"name": "describe-gather",
"sources": {
"urn:noocodec:dag:the-archivist/node/describe-scatter": {}
},
"gather": {
"strategy": "tool-candidate-merge"
},
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/describe-pick",
"error": "urn:noocodec:dag:the-archivist/node/compose-empty",
"empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-pick",
"@type": "SingleNode",
"name": "describe-pick",
"node": "urn:noocodec:node:pick-best-match",
"outputs": {
"picked": "urn:noocodec:dag:the-archivist/node/describe-merge"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-merge",
"@type": "SingleNode",
"name": "describe-merge",
"node": "urn:noocodec:node:merge-candidates",
"outputs": {
"ranked": "urn:noocodec:dag:the-archivist/node/describe-record",
"empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-record",
"@type": "SingleNode",
"name": "describe-record",
"node": "urn:noocodec:node:record-findings",
"outputs": {
"recorded": "urn:noocodec:dag:the-archivist/node/describe-gate"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-gate",
"@type": "SingleNode",
"name": "describe-gate",
"node": "urn:noocodec:node:has-citations-gate",
"outputs": {
"pass": "urn:noocodec:dag:the-archivist/node/describe-recall",
"fail": "urn:noocodec:dag:the-archivist/node/compose-empty"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/describe-recall",
"@type": "SingleNode",
"name": "describe-recall",
"node": "urn:noocodec:node:recall-past-visits",
"outputs": {
"recalled": "urn:noocodec:dag:the-archivist/node/compose-loop"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/recommend-similar",
"@type": "SingleNode",
"name": "recommend-similar",
"node": "urn:noocodec:node:recommend-similar",
"outputs": {
"seeded": "urn:noocodec:dag:the-archivist/node/similar-search",
"empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/similar-search",
"@type": "EmbeddedDAGNode",
"name": "similar-search",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/compose-loop",
"error": "urn:noocodec:dag:the-archivist/node/compose-empty"
},
"dag": "urn:noocodec:dag:book-search-scatter",
"stateMapping": {
"output": {
"terms": "terms",
"toolPlan": "toolPlan",
"candidates": "candidates",
"shortlist": "shortlist",
"priorContext": "priorContext",
"failureCause": "failureCause"
}
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/compose-loop",
"@type": "EmbeddedDAGNode",
"name": "compose-loop",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/respond-to-visitor",
"error": "urn:noocodec:dag:the-archivist/node/compose-empty"
},
"dag": "urn:noocodec:dag:compose-retry-loop",
"stateMapping": {
"output": {
"draft": "draft",
"approvalState": "approvalState"
}
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/respond-to-visitor",
"@type": "SingleNode",
"name": "respond-to-visitor",
"node": "urn:noocodec:node:respond-to-visitor",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/end"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/memory-recall",
"@type": "SingleNode",
"name": "memory-recall",
"node": "urn:noocodec:node:recall-memories",
"outputs": {
"recalled": "urn:noocodec:dag:the-archivist/node/compose-memory-recall"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/compose-memory-recall",
"@type": "SingleNode",
"name": "compose-memory-recall",
"node": "urn:noocodec:node:compose-memory-response",
"outputs": {
"drafted": "urn:noocodec:dag:the-archivist/node/respond-to-visitor",
"retry": "urn:noocodec:dag:the-archivist/node/compose-memory-recall",
"salvage": "urn:noocodec:dag:the-archivist/node/compose-memory-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/compose-memory-salvage",
"@type": "SingleNode",
"name": "compose-memory-salvage",
"node": "urn:noocodec:node:compose-memory-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/respond-to-visitor"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/decline-off-topic",
"@type": "SingleNode",
"name": "decline-off-topic",
"node": "urn:noocodec:node:decline-off-topic",
"outputs": {
"success": "urn:noocodec:dag:the-archivist/node/end"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/compose-empty",
"@type": "SingleNode",
"name": "compose-empty",
"node": "urn:noocodec:node:compose-empty",
"outputs": {
"drafted": "urn:noocodec:dag:the-archivist/node/respond-to-visitor",
"retry": "urn:noocodec:dag:the-archivist/node/compose-empty",
"salvage": "urn:noocodec:dag:the-archivist/node/compose-empty-salvage"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/compose-empty-salvage",
"@type": "SingleNode",
"name": "compose-empty-salvage",
"node": "urn:noocodec:node:compose-empty-salvage",
"outputs": {
"done": "urn:noocodec:dag:the-archivist/node/respond-to-visitor"
}
},
{
"@id": "urn:noocodec:dag:the-archivist/node/end",
"@type": "TerminalNode",
"name": "end",
"outcome": "completed"
}
]
}Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
%% the-archivist (v6.0)
entry_main(["main"])
entry_main --> urn_noocodec_dag_the-archivist/node/park-for-input
urn_noocodec_dag_the-archivist/node/setup(["setup (pre)"])
urn_noocodec_dag_the-archivist/node/park-for-input["park-for-input"]
urn_noocodec_dag_the-archivist/node/park-for-input -->|parked| urn_noocodec_dag_the-archivist/node/park-for-input
urn_noocodec_dag_the-archivist/node/park-for-input -->|resumed| urn_noocodec_dag_the-archivist/node/recall-context
urn_noocodec_dag_the-archivist/node/recall-context["recall-context"]
urn_noocodec_dag_the-archivist/node/recall-context -->|recalled| urn_noocodec_dag_the-archivist/node/classify-intent
urn_noocodec_dag_the-archivist/node/classify-intent["classify-intent"]
urn_noocodec_dag_the-archivist/node/classify-intent -->|lookup-author| urn_noocodec_dag_the-archivist/node/author-search
urn_noocodec_dag_the-archivist/node/classify-intent -->|find-reviews| urn_noocodec_dag_the-archivist/node/reviews-extract
urn_noocodec_dag_the-archivist/node/classify-intent -->|describe-book| urn_noocodec_dag_the-archivist/node/describe-extract
urn_noocodec_dag_the-archivist/node/classify-intent -->|recommend-similar| urn_noocodec_dag_the-archivist/node/recommend-similar
urn_noocodec_dag_the-archivist/node/classify-intent -->|recall-memories| urn_noocodec_dag_the-archivist/node/memory-recall
urn_noocodec_dag_the-archivist/node/classify-intent -->|on-topic| urn_noocodec_dag_the-archivist/node/on-topic-search
urn_noocodec_dag_the-archivist/node/classify-intent -->|recommend-top-rated| urn_noocodec_dag_the-archivist/node/recommend-extract
urn_noocodec_dag_the-archivist/node/classify-intent -->|off-topic| urn_noocodec_dag_the-archivist/node/decline-off-topic
urn_noocodec_dag_the-archivist/node/classify-intent -->|retry| urn_noocodec_dag_the-archivist/node/classify-intent
urn_noocodec_dag_the-archivist/node/classify-intent -->|salvage| urn_noocodec_dag_the-archivist/node/classify-intent-salvage
urn_noocodec_dag_the-archivist/node/classify-intent-salvage["classify-intent-salvage"]
urn_noocodec_dag_the-archivist/node/classify-intent-salvage -->|done| urn_noocodec_dag_the-archivist/node/on-topic-search
urn_noocodec_dag_the-archivist/node/on-topic-search[["on-topic-search"]]
urn_noocodec_dag_the-archivist/node/on-topic-search -->|success| urn_noocodec_dag_the-archivist/node/compose-loop
urn_noocodec_dag_the-archivist/node/on-topic-search -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/author-search[["author-search"]]
urn_noocodec_dag_the-archivist/node/author-search -->|success| urn_noocodec_dag_the-archivist/node/group-by-year
urn_noocodec_dag_the-archivist/node/author-search -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/group-by-year["group-by-year"]
urn_noocodec_dag_the-archivist/node/group-by-year -->|ordered| urn_noocodec_dag_the-archivist/node/compose-loop
urn_noocodec_dag_the-archivist/node/reviews-extract["reviews-extract"]
urn_noocodec_dag_the-archivist/node/reviews-extract -->|success| urn_noocodec_dag_the-archivist/node/reviews-decide-tools
urn_noocodec_dag_the-archivist/node/reviews-extract -->|retry| urn_noocodec_dag_the-archivist/node/reviews-extract
urn_noocodec_dag_the-archivist/node/reviews-extract -->|salvage| urn_noocodec_dag_the-archivist/node/reviews-extract-salvage
urn_noocodec_dag_the-archivist/node/reviews-extract-salvage["reviews-extract-salvage"]
urn_noocodec_dag_the-archivist/node/reviews-extract-salvage -->|done| urn_noocodec_dag_the-archivist/node/reviews-decide-tools
urn_noocodec_dag_the-archivist/node/reviews-decide-tools["reviews-decide-tools"]
urn_noocodec_dag_the-archivist/node/reviews-decide-tools -->|tools| urn_noocodec_dag_the-archivist/node/reviews-build-worksets
urn_noocodec_dag_the-archivist/node/reviews-decide-tools -->|no-tools| urn_noocodec_dag_the-archivist/node/reviews-build-worksets
urn_noocodec_dag_the-archivist/node/reviews-decide-tools -->|retry| urn_noocodec_dag_the-archivist/node/reviews-decide-tools
urn_noocodec_dag_the-archivist/node/reviews-decide-tools -->|salvage| urn_noocodec_dag_the-archivist/node/reviews-decide-tools-salvage
urn_noocodec_dag_the-archivist/node/reviews-decide-tools-salvage["reviews-decide-tools-salvage"]
urn_noocodec_dag_the-archivist/node/reviews-decide-tools-salvage -->|done| urn_noocodec_dag_the-archivist/node/reviews-build-worksets
urn_noocodec_dag_the-archivist/node/reviews-build-worksets["reviews-build-worksets"]
urn_noocodec_dag_the-archivist/node/reviews-build-worksets -->|ready| urn_noocodec_dag_the-archivist/node/reviews-scatter
urn_noocodec_dag_the-archivist/node/reviews-scatter[/"reviews-scatter"/]
urn_noocodec_dag_the-archivist/node/reviews-scatter -->|success| urn_noocodec_dag_the-archivist/node/reviews-gather
urn_noocodec_dag_the-archivist/node/reviews-scatter -->|error| urn_noocodec_dag_the-archivist/node/reviews-gather
urn_noocodec_dag_the-archivist/node/reviews-scatter -->|empty| urn_noocodec_dag_the-archivist/node/reviews-rank
urn_noocodec_dag_the-archivist/node/reviews-gather{"reviews-gather"}
urn_noocodec_dag_the-archivist/node/reviews-gather -->|success| urn_noocodec_dag_the-archivist/node/reviews-rank
urn_noocodec_dag_the-archivist/node/reviews-gather -->|error| urn_noocodec_dag_the-archivist/node/reviews-rank
urn_noocodec_dag_the-archivist/node/reviews-gather -->|empty| urn_noocodec_dag_the-archivist/node/reviews-rank
urn_noocodec_dag_the-archivist/node/reviews-rank["reviews-rank"]
urn_noocodec_dag_the-archivist/node/reviews-rank -->|ranked| urn_noocodec_dag_the-archivist/node/reviews-merge
urn_noocodec_dag_the-archivist/node/reviews-merge["reviews-merge"]
urn_noocodec_dag_the-archivist/node/reviews-merge -->|ranked| urn_noocodec_dag_the-archivist/node/reviews-record
urn_noocodec_dag_the-archivist/node/reviews-merge -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/reviews-record["reviews-record"]
urn_noocodec_dag_the-archivist/node/reviews-record -->|recorded| urn_noocodec_dag_the-archivist/node/reviews-gate
urn_noocodec_dag_the-archivist/node/reviews-gate["reviews-gate"]
urn_noocodec_dag_the-archivist/node/reviews-gate -->|pass| urn_noocodec_dag_the-archivist/node/reviews-recall
urn_noocodec_dag_the-archivist/node/reviews-gate -->|fail| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/reviews-recall["reviews-recall"]
urn_noocodec_dag_the-archivist/node/reviews-recall -->|recalled| urn_noocodec_dag_the-archivist/node/compose-loop
urn_noocodec_dag_the-archivist/node/recommend-extract["recommend-extract"]
urn_noocodec_dag_the-archivist/node/recommend-extract -->|success| urn_noocodec_dag_the-archivist/node/recommend-decide-tools
urn_noocodec_dag_the-archivist/node/recommend-extract -->|retry| urn_noocodec_dag_the-archivist/node/recommend-extract
urn_noocodec_dag_the-archivist/node/recommend-extract -->|salvage| urn_noocodec_dag_the-archivist/node/recommend-extract-salvage
urn_noocodec_dag_the-archivist/node/recommend-extract-salvage["recommend-extract-salvage"]
urn_noocodec_dag_the-archivist/node/recommend-extract-salvage -->|done| urn_noocodec_dag_the-archivist/node/recommend-decide-tools
urn_noocodec_dag_the-archivist/node/recommend-decide-tools["recommend-decide-tools"]
urn_noocodec_dag_the-archivist/node/recommend-decide-tools -->|tools| urn_noocodec_dag_the-archivist/node/recommend-build-worksets
urn_noocodec_dag_the-archivist/node/recommend-decide-tools -->|no-tools| urn_noocodec_dag_the-archivist/node/recommend-build-worksets
urn_noocodec_dag_the-archivist/node/recommend-decide-tools -->|retry| urn_noocodec_dag_the-archivist/node/recommend-decide-tools
urn_noocodec_dag_the-archivist/node/recommend-decide-tools -->|salvage| urn_noocodec_dag_the-archivist/node/recommend-decide-tools-salvage
urn_noocodec_dag_the-archivist/node/recommend-decide-tools-salvage["recommend-decide-tools-salvage"]
urn_noocodec_dag_the-archivist/node/recommend-decide-tools-salvage -->|done| urn_noocodec_dag_the-archivist/node/recommend-build-worksets
urn_noocodec_dag_the-archivist/node/recommend-build-worksets["recommend-build-worksets"]
urn_noocodec_dag_the-archivist/node/recommend-build-worksets -->|ready| urn_noocodec_dag_the-archivist/node/recommend-scatter
urn_noocodec_dag_the-archivist/node/recommend-scatter[/"recommend-scatter"/]
urn_noocodec_dag_the-archivist/node/recommend-scatter -->|success| urn_noocodec_dag_the-archivist/node/recommend-gather
urn_noocodec_dag_the-archivist/node/recommend-scatter -->|error| urn_noocodec_dag_the-archivist/node/recommend-gather
urn_noocodec_dag_the-archivist/node/recommend-scatter -->|empty| urn_noocodec_dag_the-archivist/node/recommend-rank
urn_noocodec_dag_the-archivist/node/recommend-gather{"recommend-gather"}
urn_noocodec_dag_the-archivist/node/recommend-gather -->|success| urn_noocodec_dag_the-archivist/node/recommend-rank
urn_noocodec_dag_the-archivist/node/recommend-gather -->|error| urn_noocodec_dag_the-archivist/node/recommend-rank
urn_noocodec_dag_the-archivist/node/recommend-gather -->|empty| urn_noocodec_dag_the-archivist/node/recommend-rank
urn_noocodec_dag_the-archivist/node/recommend-rank["recommend-rank"]
urn_noocodec_dag_the-archivist/node/recommend-rank -->|ranked| urn_noocodec_dag_the-archivist/node/recommend-merge
urn_noocodec_dag_the-archivist/node/recommend-merge["recommend-merge"]
urn_noocodec_dag_the-archivist/node/recommend-merge -->|ranked| urn_noocodec_dag_the-archivist/node/recommend-record
urn_noocodec_dag_the-archivist/node/recommend-merge -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/recommend-record["recommend-record"]
urn_noocodec_dag_the-archivist/node/recommend-record -->|recorded| urn_noocodec_dag_the-archivist/node/recommend-gate
urn_noocodec_dag_the-archivist/node/recommend-gate["recommend-gate"]
urn_noocodec_dag_the-archivist/node/recommend-gate -->|pass| urn_noocodec_dag_the-archivist/node/recommend-recall
urn_noocodec_dag_the-archivist/node/recommend-gate -->|fail| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/recommend-recall["recommend-recall"]
urn_noocodec_dag_the-archivist/node/recommend-recall -->|recalled| urn_noocodec_dag_the-archivist/node/compose-loop
urn_noocodec_dag_the-archivist/node/describe-extract["describe-extract"]
urn_noocodec_dag_the-archivist/node/describe-extract -->|success| urn_noocodec_dag_the-archivist/node/describe-decide-tools
urn_noocodec_dag_the-archivist/node/describe-extract -->|retry| urn_noocodec_dag_the-archivist/node/describe-extract
urn_noocodec_dag_the-archivist/node/describe-extract -->|salvage| urn_noocodec_dag_the-archivist/node/describe-extract-salvage
urn_noocodec_dag_the-archivist/node/describe-extract-salvage["describe-extract-salvage"]
urn_noocodec_dag_the-archivist/node/describe-extract-salvage -->|done| urn_noocodec_dag_the-archivist/node/describe-decide-tools
urn_noocodec_dag_the-archivist/node/describe-decide-tools["describe-decide-tools"]
urn_noocodec_dag_the-archivist/node/describe-decide-tools -->|tools| urn_noocodec_dag_the-archivist/node/describe-build-worksets
urn_noocodec_dag_the-archivist/node/describe-decide-tools -->|no-tools| urn_noocodec_dag_the-archivist/node/describe-build-worksets
urn_noocodec_dag_the-archivist/node/describe-decide-tools -->|retry| urn_noocodec_dag_the-archivist/node/describe-decide-tools
urn_noocodec_dag_the-archivist/node/describe-decide-tools -->|salvage| urn_noocodec_dag_the-archivist/node/describe-decide-tools-salvage
urn_noocodec_dag_the-archivist/node/describe-decide-tools-salvage["describe-decide-tools-salvage"]
urn_noocodec_dag_the-archivist/node/describe-decide-tools-salvage -->|done| urn_noocodec_dag_the-archivist/node/describe-build-worksets
urn_noocodec_dag_the-archivist/node/describe-build-worksets["describe-build-worksets"]
urn_noocodec_dag_the-archivist/node/describe-build-worksets -->|ready| urn_noocodec_dag_the-archivist/node/describe-scatter
urn_noocodec_dag_the-archivist/node/describe-scatter[/"describe-scatter"/]
urn_noocodec_dag_the-archivist/node/describe-scatter -->|success| urn_noocodec_dag_the-archivist/node/describe-gather
urn_noocodec_dag_the-archivist/node/describe-scatter -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/describe-scatter -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/describe-gather{"describe-gather"}
urn_noocodec_dag_the-archivist/node/describe-gather -->|success| urn_noocodec_dag_the-archivist/node/describe-pick
urn_noocodec_dag_the-archivist/node/describe-gather -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/describe-gather -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/describe-pick["describe-pick"]
urn_noocodec_dag_the-archivist/node/describe-pick -->|picked| urn_noocodec_dag_the-archivist/node/describe-merge
urn_noocodec_dag_the-archivist/node/describe-merge["describe-merge"]
urn_noocodec_dag_the-archivist/node/describe-merge -->|ranked| urn_noocodec_dag_the-archivist/node/describe-record
urn_noocodec_dag_the-archivist/node/describe-merge -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/describe-record["describe-record"]
urn_noocodec_dag_the-archivist/node/describe-record -->|recorded| urn_noocodec_dag_the-archivist/node/describe-gate
urn_noocodec_dag_the-archivist/node/describe-gate["describe-gate"]
urn_noocodec_dag_the-archivist/node/describe-gate -->|pass| urn_noocodec_dag_the-archivist/node/describe-recall
urn_noocodec_dag_the-archivist/node/describe-gate -->|fail| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/describe-recall["describe-recall"]
urn_noocodec_dag_the-archivist/node/describe-recall -->|recalled| urn_noocodec_dag_the-archivist/node/compose-loop
urn_noocodec_dag_the-archivist/node/recommend-similar["recommend-similar"]
urn_noocodec_dag_the-archivist/node/recommend-similar -->|seeded| urn_noocodec_dag_the-archivist/node/similar-search
urn_noocodec_dag_the-archivist/node/recommend-similar -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/similar-search[["similar-search"]]
urn_noocodec_dag_the-archivist/node/similar-search -->|success| urn_noocodec_dag_the-archivist/node/compose-loop
urn_noocodec_dag_the-archivist/node/similar-search -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/compose-loop[["compose-loop"]]
urn_noocodec_dag_the-archivist/node/compose-loop -->|success| urn_noocodec_dag_the-archivist/node/respond-to-visitor
urn_noocodec_dag_the-archivist/node/compose-loop -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/respond-to-visitor["respond-to-visitor"]
urn_noocodec_dag_the-archivist/node/respond-to-visitor -->|success| urn_noocodec_dag_the-archivist/node/end
urn_noocodec_dag_the-archivist/node/memory-recall["memory-recall"]
urn_noocodec_dag_the-archivist/node/memory-recall -->|recalled| urn_noocodec_dag_the-archivist/node/compose-memory-recall
urn_noocodec_dag_the-archivist/node/compose-memory-recall["compose-memory-recall"]
urn_noocodec_dag_the-archivist/node/compose-memory-recall -->|drafted| urn_noocodec_dag_the-archivist/node/respond-to-visitor
urn_noocodec_dag_the-archivist/node/compose-memory-recall -->|retry| urn_noocodec_dag_the-archivist/node/compose-memory-recall
urn_noocodec_dag_the-archivist/node/compose-memory-recall -->|salvage| urn_noocodec_dag_the-archivist/node/compose-memory-salvage
urn_noocodec_dag_the-archivist/node/compose-memory-salvage["compose-memory-salvage"]
urn_noocodec_dag_the-archivist/node/compose-memory-salvage -->|done| urn_noocodec_dag_the-archivist/node/respond-to-visitor
urn_noocodec_dag_the-archivist/node/decline-off-topic["decline-off-topic"]
urn_noocodec_dag_the-archivist/node/decline-off-topic -->|success| urn_noocodec_dag_the-archivist/node/end
urn_noocodec_dag_the-archivist/node/compose-empty["compose-empty"]
urn_noocodec_dag_the-archivist/node/compose-empty -->|drafted| urn_noocodec_dag_the-archivist/node/respond-to-visitor
urn_noocodec_dag_the-archivist/node/compose-empty -->|retry| urn_noocodec_dag_the-archivist/node/compose-empty
urn_noocodec_dag_the-archivist/node/compose-empty -->|salvage| urn_noocodec_dag_the-archivist/node/compose-empty-salvage
urn_noocodec_dag_the-archivist/node/compose-empty-salvage["compose-empty-salvage"]
urn_noocodec_dag_the-archivist/node/compose-empty-salvage -->|done| urn_noocodec_dag_the-archivist/node/respond-to-visitor
urn_noocodec_dag_the-archivist/node/end((("end")))Run
npx tsx examples/the-archivist/runArchivist.tsWhat It Lets You Do
DAGBuilder lets teams keep graph authoring close to the node implementations while still shipping a portable JSON-LD artifact. It is the right tool when the DAG lives in TypeScript source and reviewers need compile-time help with route coverage.
Code Samples
This code is the builder-authored Archivist DAG. Read it as a route map first and TypeScript second: every chained call becomes one JSON-LD placement, and the final .build() returns the document rendered above.
Code
The complete archivistDAG, the parent DAG as a single DAGBuilder chain. The full source file includes inline branches for reviews and describe (which use distinct post-scout ranking steps):
/**
* The Archivist: canonical DAG, built with DAGBuilder. Version 6.0.
*
* Molecular composition: the parent DAG is composed of two reusable
* sub-DAGs that ship as independent components and are imported as
* `.embed(name, dagIri, routes)` placements. The sub-DAGs are registered
* separately and referenced by canonical IRI; the parent DAG never knows
* their internals.
*
* recall-context
* └─ recalled ──► classify-intent
*
* classify-intent
* ├─ off-topic ──► decline-off-topic ──► END
* │
* ├─ on-topic ──► [book-search-scatter] (extract+decide+4scouts+rank+merge+record+gate+recall)
* │ ├─ success ──► [compose-retry-loop] (compose+validate+retry)
* │ └─ error ──► compose-empty ──┐
* │ │
* ├─ lookup-author ──► [book-search-scatter] │
* │ ├─ success ──► group-by-year ──► [compose-retry-loop]
* │ └─ error ──► compose-empty ──┐
*
* │ ▼
* ├─ find-reviews ──► reviews-extract ──► [compose-retry-loop] (success) ──► respond-to-visitor ──► END
* │ (inline: decide+4scouts+rankByRating+merge+record+gate+recall) ▲
* │ ▲
* ├─ describe-book ──► describe-extract ──► [compose-retry-loop]
* │ (inline: decide+4scouts+pickBestMatch+merge+record+gate+recall)
* │
* ├─ recommend ──► recommend-extract ──► [compose-retry-loop] (success) ──► respond-to-visitor ──► END
* │ (inline: decide+4scouts+rankByRating+merge+record+gate+recall) ▲
* │ ▲
* ├─ recall-memories ──► memory-recall ──► compose-memory-recall ──────────────────────────────┐
* │ ▼
* └─ recommend-similar ──► recommend-similar-gate respond-to-visitor ──► END
* ├─ seeded ──► [book-search-scatter] ▲
* │ ├─ success ──► [compose-retry-loop] (success) ──────┘
* │ └─ error ──► compose-empty ──────────────────────►┘
* └─ empty ──► compose-empty ───────────────────────────────────────►┘
*
* Convergence policy (v6.0): all response-producing branches converge into ONE
* shared `respond-to-visitor` terminal at this (parent) level. The
* compose-retry-loop embedded-DAG exits with `success` after producing state.draft
* and does NOT contain respondToVisitor internally. This ensures exactly one
* terminal node fires per run with the full converged state.draft.
*
* Embedded-DAGs (molecular components):
* book-search-scatter: extract-query + decide-tools + 4-source parallel scouts
* (OpenLibrary, Google Books, Subject, Wikipedia) + rankCandidates
* + mergeCandidates + recordFindings + hasCitationsGate +
* recallPastVisits. Three placements in this DAG:
* on-topic-search, author-search, similar-search.
*
* compose-retry-loop: composeResponse + validateResponse (with bounded retry loop)
* + respondToVisitor. Four placements in this DAG:
* compose-loop (shared by all four convergent branches).
*
* Inlined branches (reviews, describe, recommend-top-rated):
* Reviews and recommend-top-rated both use `rankByRating` (deterministic,
* rating-weighted) instead of `rankCandidates` (LLM-driven); recommend-top-rated
* is the structural sibling of find-reviews, reusing the same node objects at
* `recommend-*` placements, for the vague "good book / good story" recommend
* intent that has no topic to rank by relevance. Describe uses `pickBestMatch`
* to narrow to the top-3 title-similar candidates before merge. All three are
* structurally identical to book-search-scatter except for the post-scout
* ranking step; keeping them inline makes the intentional distinction explicit
* rather than hiding it behind a embedded-DAG parameter.
*
* Empty-result handling (v5.2):
* Empty results route through `compose-empty` → `respond-to-visitor`.
* `compose-empty` calls the LLM with `state.failureCause` (accumulated by
* scouts) to produce an in-character message that acknowledges what was
* searched and offers a concrete next step.
*
* Builder output shape:
* DAGBuilder.node(name, dagNode, routes) emits a
* { type: 'single', name, node: dagNode.name, outputs: routes }
* object. build() returns a plain DAG passed straight to
* DAGDocument.load().
*
* DAG containment (WorkerThreadContainer) — why the archivist stays in-process:
* The container/worker feature is most natural for CPU-bound, self-contained
* per-item work: pure data transforms that only read from state and write a
* result back (see the-cartographer, which routes its canonical-event enrichment
* sub-DAG through a WorkerThreadContainer — haversine, GDPR redaction, pricing
* and ETA are pure arithmetic on serialisable data).
*
* The archivist's scatter items (the four scout providers) are LLM / network
* bound: each clone calls a live language model and external book APIs. Workers
* cannot share the LLM provider instance (it is not serialisable), and each
* scatter item's payload — prompt context, live API credentials, LLM adapter
* state — crosses the worker message channel as structured-clone, which drops
* functions, closures, class instances, and streams.
*
* Worker containment suits CPU / data DAGs. LLM-cascade DAGs — where nodes
* carry network clients, streaming responses, and rich provider objects — run
* in-process and rely on async concurrency (Promise.all, scatter concurrency)
* rather than OS-level thread isolation for parallelism.
*/
import type { ArchivistState } from './ArchivistState.ts';
import './nodes/scouts.ts'; // registers 'tool-candidate-merge' gather strategy
import { DAGBuilder, DAGIdentity, PlaceholderNode } from '@studnicky/dagonizer';
import type { DAGType } from '@studnicky/dagonizer';
// #region dispatcher-bundle
//
// IRI identity: DAGBuilder embeds the canonical DAG_CONTEXT in every built
// DAG's `@context` field. The archivist uses explicit placement IRIs via
// DAGIdentity.placementId(dagIri, placementIdentifier) so route targets,
// gather sources, and entrypoint wiring all stay on canonical IRIs while the
// placement `name` field remains display-only.
//
// @id values on each node placement follow the urn:noocodec:dag:<dagName>/node/<placementName>
// convention produced by DAGIdentity.placementId(), e.g.:
// urn:noocodec:dag:the-archivist/node/recall-context
//
// A plugin shipping nodes under its own namespace would declare a prefix in
// the bundle's `context` field — e.g. { context: { archivist: 'https://archivist.example.com/' } }
// — and use prefixed names like 'archivist:recallContext' to prevent collisions
// with other plugins that might register a node named 'recallContext'.
// See docs/guide/iri-identity.md for the full expansion rule set.
const BOOK_SEARCH_TOOL_DAGS = [
'urn:noocodec:tool:web_search_books',
'urn:noocodec:tool:google_books_search',
'urn:noocodec:tool:subject_search',
'urn:noocodec:tool:wikipedia_summary',
] as const;
const ARCHIVIST_DAG_IRI = 'urn:noocodec:dag:the-archivist';
const BOOK_SEARCH_SCATTER_DAG_IRI = 'urn:noocodec:dag:book-search-scatter';
const COMPOSE_RETRY_LOOP_DAG_IRI = 'urn:noocodec:dag:compose-retry-loop';
const placement = (placementIdentifier: string): string => DAGIdentity.placementId(ARCHIVIST_DAG_IRI, placementIdentifier);
const display = <T extends string>(name: T): { name: T } => ({ name });
const nodes = {
'preRunSetup': new PlaceholderNode<ArchivistState, 'ready'>('urn:noocodec:node:pre-run-setup', ['ready']),
'parkForInput': new PlaceholderNode<ArchivistState, 'parked' | 'resumed'>('urn:noocodec:node:park-for-input', ['parked', 'resumed']),
'recallContext': new PlaceholderNode<ArchivistState, 'recalled'>('urn:noocodec:node:recall-context', ['recalled']),
'classifyIntent': new PlaceholderNode<ArchivistState, 'lookup-author' | 'find-reviews' | 'describe-book' | 'recommend-similar' | 'recall-memories' | 'on-topic' | 'recommend-top-rated' | 'off-topic' | 'retry' | 'salvage'>('urn:noocodec:node:classify-intent', ['lookup-author', 'find-reviews', 'describe-book', 'recommend-similar', 'recall-memories', 'on-topic', 'recommend-top-rated', 'off-topic', 'retry', 'salvage']),
'classifyIntentSalvage': new PlaceholderNode<ArchivistState, 'done'>('urn:noocodec:node:classify-intent-salvage', ['done']),
'extractQuery': new PlaceholderNode<ArchivistState, 'success' | 'retry' | 'salvage'>('urn:noocodec:node:extract-query', ['success', 'retry', 'salvage']),
'extractQuerySalvage': new PlaceholderNode<ArchivistState, 'done'>('urn:noocodec:node:extract-query-salvage', ['done']),
'decideTools': new PlaceholderNode<ArchivistState, 'tools' | 'no-tools' | 'retry' | 'salvage'>('urn:noocodec:node:decide-tools', ['tools', 'no-tools', 'retry', 'salvage']),
'decideToolsSalvage': new PlaceholderNode<ArchivistState, 'done'>('urn:noocodec:node:decide-tools-salvage', ['done']),
'buildBookWorksets': new PlaceholderNode<ArchivistState, 'ready'>('urn:noocodec:node:build-book-worksets', ['ready']),
'rankByRating': new PlaceholderNode<ArchivistState, 'ranked'>('urn:noocodec:node:rank-by-rating', ['ranked']),
'pickBestMatch': new PlaceholderNode<ArchivistState, 'picked'>('urn:noocodec:node:pick-best-match', ['picked']),
'mergeCandidates': new PlaceholderNode<ArchivistState, 'ranked' | 'empty'>('urn:noocodec:node:merge-candidates', ['ranked', 'empty']),
'recordFindings': new PlaceholderNode<ArchivistState, 'recorded'>('urn:noocodec:node:record-findings', ['recorded']),
'hasCitationsGate': new PlaceholderNode<ArchivistState, 'pass' | 'fail'>('urn:noocodec:node:has-citations-gate', ['pass', 'fail']),
'groupByYear': new PlaceholderNode<ArchivistState, 'ordered'>('urn:noocodec:node:group-by-year', ['ordered']),
'recallPastVisits': new PlaceholderNode<ArchivistState, 'recalled'>('urn:noocodec:node:recall-past-visits', ['recalled']),
'recommendSimilar': new PlaceholderNode<ArchivistState, 'seeded' | 'empty'>('urn:noocodec:node:recommend-similar', ['seeded', 'empty']),
'recallMemories': new PlaceholderNode<ArchivistState, 'recalled'>('urn:noocodec:node:recall-memories', ['recalled']),
'composeMemoryResponse': new PlaceholderNode<ArchivistState, 'drafted' | 'retry' | 'salvage'>('urn:noocodec:node:compose-memory-response', ['drafted', 'retry', 'salvage']),
'composeMemoryResponseSalvage': new PlaceholderNode<ArchivistState, 'done'>('urn:noocodec:node:compose-memory-salvage', ['done']),
'respondToVisitor': new PlaceholderNode<ArchivistState, 'success'>('urn:noocodec:node:respond-to-visitor', ['success']),
'declineOffTopic': new PlaceholderNode<ArchivistState, 'success'>('urn:noocodec:node:decline-off-topic', ['success']),
'composeEmptyResponse': new PlaceholderNode<ArchivistState, 'drafted' | 'retry' | 'salvage'>('urn:noocodec:node:compose-empty', ['drafted', 'retry', 'salvage']),
'composeEmptyResponseSalvage': new PlaceholderNode<ArchivistState, 'done'>('urn:noocodec:node:compose-empty-salvage', ['done']),
} as const;
export const archivistDAG: DAGType = new DAGBuilder(ARCHIVIST_DAG_IRI, '6.0', display('the-archivist'))
// ── pre-phase: setup ─────────────────────────────────────────────────────
// Stamps state.runId and clears any stale draft before the main loop starts.
// PhaseNode placement: runs before the entrypoint node; errors abort the run.
// No routing: phase placements are out-of-band and never set the entrypoint.
.phase(placement('setup'), 'pre', nodes.preRunSetup, display('setup'))
// ── 0. park-for-input (HITL gate) ────────────────────────────────────────
// First added → auto-entrypoint. Parks the flow when state.query is empty,
// waiting for the human to supply input via the browser HITL banner. On
// resume, `state.query` is set by the caller before `dispatcher.resume()`;
// the node then routes `'resumed'` and proceeds to `recall-context`.
// The `'parked'` output routes to the engine park machinery (null wiring).
.node(placement('park-for-input'), nodes.parkForInput, {
'parked': placement('park-for-input'),
'resumed': placement('recall-context'),
}, display('park-for-input'))
// ── 1. recall-context ────────────────────────────────────────────────────
// Runs before classifyIntent so the classifier can benefit from
// prior-session continuity hints.
.node(placement('recall-context'), nodes.recallContext, {
'recalled': placement('classify-intent'),
}, display('recall-context'))
// #region intent-routes
// ── 1. classify-intent ───────────────────────────────────────────────────
// Wide output union routes to seven branches. EmbeddedDAG placements and inline
// branches share the same shared terminal: compose-loop and compose-empty.
// recall-memories routes directly to memory-recall → compose-memory-recall
// → memory-respond (no search needed; the memory store is the source).
.node(placement('classify-intent'), nodes.classifyIntent, {
'lookup-author': placement('author-search'),
'find-reviews': placement('reviews-extract'),
'describe-book': placement('describe-extract'),
'recommend-similar': placement('recommend-similar'),
'recall-memories': placement('memory-recall'),
'on-topic': placement('on-topic-search'),
'recommend-top-rated': placement('recommend-extract'),
'off-topic': placement('decline-off-topic'),
// Own timeout / classifier failure → retry budget decides. 'retry' loops
// back; 'salvage' defaults to the broadest on-topic search via a node.
'retry': placement('classify-intent'),
'salvage': placement('classify-intent-salvage'),
}, display('classify-intent'))
.node(placement('classify-intent-salvage'), nodes.classifyIntentSalvage, {
'done': placement('on-topic-search'),
}, display('classify-intent-salvage'))
// #endregion intent-routes
// #region embedded-dag-placements
// ── on-topic branch ──────────────────────────────────────────────────────
// EmbeddedDAGNode: book-search-scatter handles extract-query, decide-tools,
// all four scouts, rank-candidates, merge, record, gate, and recall.
// One packaged cluster; first of three placements of the same sub-DAG.
// gather.map copies the fields the sub-DAG writes back to the parent state
// so compose-loop and group-by-year can read them.
.embed(placement('on-topic-search'), BOOK_SEARCH_SCATTER_DAG_IRI, {
'success': placement('compose-loop'),
'error': placement('compose-empty'),
}, {
'name': 'on-topic-search',
'outputs': {
'terms': 'terms',
'toolPlan': 'toolPlan',
'candidates': 'candidates',
'shortlist': 'shortlist',
'priorContext': 'priorContext',
'failureCause': 'failureCause',
},
})
// ── lookup-author branch ─────────────────────────────────────────────────
// EmbeddedDAGNode: same book-search-scatter cluster, second placement.
// After success, group-by-year sorts results chronologically before the
// compose loop; author surveys read better in publication-timeline order.
.embed(placement('author-search'), BOOK_SEARCH_SCATTER_DAG_IRI, {
'success': placement('group-by-year'),
'error': placement('compose-empty'),
}, {
'name': 'author-search',
'outputs': {
'terms': 'terms',
'toolPlan': 'toolPlan',
'candidates': 'candidates',
'shortlist': 'shortlist',
'priorContext': 'priorContext',
'failureCause': 'failureCause',
},
})
// group-by-year is author-branch-specific: sorts shortlist chronologically.
.node(placement('group-by-year'), nodes.groupByYear, {
'ordered': placement('compose-loop'),
}, display('group-by-year'))
// ── find-reviews branch ───────────────────────────────────────────────────
// Inlined. Uses rankByRating (deterministic, rating-weighted) in place of
// rankCandidates (LLM-driven). The Google Books scout carries notes.rating /
// notes.ratingsCount; rankByRating weights those for reviews-style output.
.node(placement('reviews-extract'), nodes.extractQuery, {
'success': placement('reviews-decide-tools'),
'retry': placement('reviews-extract'),
'salvage': placement('reviews-extract-salvage'),
}, display('reviews-extract'))
.node(placement('reviews-extract-salvage'), nodes.extractQuerySalvage, {
'done': placement('reviews-decide-tools'),
}, display('reviews-extract-salvage'))
.node(placement('reviews-decide-tools'), nodes.decideTools, {
'tools': placement('reviews-build-worksets'),
'no-tools': placement('reviews-build-worksets'),
'retry': placement('reviews-decide-tools'),
'salvage': placement('reviews-decide-tools-salvage'),
}, display('reviews-decide-tools'))
.node(placement('reviews-decide-tools-salvage'), nodes.decideToolsSalvage, {
'done': placement('reviews-build-worksets'),
}, display('reviews-decide-tools-salvage'))
// Build scatter worksets: converts toolPlan into bookWorksets items so the
// scatter can dispatch to each declared tool DAG IRI.
.node(placement('reviews-build-worksets'), nodes.buildBookWorksets, {
'ready': placement('reviews-scatter'),
}, display('reviews-build-worksets'))
// Tool-registry scatter: each bookWorksets item carries its own tool DAG
// IRI. The following GatherNode reads each clone's output via
// accessor (no cast) and folds CandidateType[] into parent candidates.
.scatter(placement('reviews-scatter'), 'bookWorksets', { 'dag': { 'from': 'item', 'path': 'dagIri', 'candidates': BOOK_SEARCH_TOOL_DAGS } }, {
'success': placement('reviews-gather'),
'error': placement('reviews-gather'),
'empty': placement('reviews-rank'),
}, {
'name': 'reviews-scatter',
'execution': { 'mode': 'item', 'concurrency': 4 },
'reducer': 'any-success',
})
.gather(placement('reviews-gather'), { [placement('reviews-scatter')]: {} }, { 'strategy': 'tool-candidate-merge' }, {
'success': placement('reviews-rank'),
'error': placement('reviews-rank'),
'empty': placement('reviews-rank'),
}, display('reviews-gather'))
.node(placement('reviews-rank'), nodes.rankByRating, { 'ranked': placement('reviews-merge') }, display('reviews-rank'))
.node(placement('reviews-merge'), nodes.mergeCandidates, { 'ranked': placement('reviews-record'), 'empty': placement('compose-empty') }, display('reviews-merge'))
.node(placement('reviews-record'), nodes.recordFindings, { 'recorded': placement('reviews-gate') }, display('reviews-record'))
.node(placement('reviews-gate'), nodes.hasCitationsGate, { 'pass': placement('reviews-recall'), 'fail': placement('compose-empty') }, display('reviews-gate'))
.node(placement('reviews-recall'), nodes.recallPastVisits, { 'recalled': placement('compose-loop') }, display('reviews-recall'))
// ── recommend-top-rated branch ───────────────────────────────────────────
// Inlined, structural sibling of find-reviews. Reuses rankByRating
// (deterministic, rating-weighted) instead of rankCandidates (LLM-driven)
// because a vague "good book / good story" request carries no topic for
// relevance ranking — rating is the only signal that makes sense.
.node(placement('recommend-extract'), nodes.extractQuery, {
'success': placement('recommend-decide-tools'),
'retry': placement('recommend-extract'),
'salvage': placement('recommend-extract-salvage'),
}, display('recommend-extract'))
.node(placement('recommend-extract-salvage'), nodes.extractQuerySalvage, {
'done': placement('recommend-decide-tools'),
}, display('recommend-extract-salvage'))
.node(placement('recommend-decide-tools'), nodes.decideTools, {
'tools': placement('recommend-build-worksets'),
'no-tools': placement('recommend-build-worksets'),
'retry': placement('recommend-decide-tools'),
'salvage': placement('recommend-decide-tools-salvage'),
}, display('recommend-decide-tools'))
.node(placement('recommend-decide-tools-salvage'), nodes.decideToolsSalvage, {
'done': placement('recommend-build-worksets'),
}, display('recommend-decide-tools-salvage'))
// Build scatter worksets: converts toolPlan into bookWorksets items so the
// scatter can dispatch to each declared tool DAG IRI.
.node(placement('recommend-build-worksets'), nodes.buildBookWorksets, {
'ready': placement('recommend-scatter'),
}, display('recommend-build-worksets'))
// Tool-registry scatter: each bookWorksets item carries its own tool DAG
// IRI. The following GatherNode reads each clone's output via
// accessor (no cast) and folds CandidateType[] into parent candidates.
.scatter(placement('recommend-scatter'), 'bookWorksets', { 'dag': { 'from': 'item', 'path': 'dagIri', 'candidates': BOOK_SEARCH_TOOL_DAGS } }, {
'success': placement('recommend-gather'),
'error': placement('recommend-gather'),
'empty': placement('recommend-rank'),
}, {
'name': 'recommend-scatter',
'execution': { 'mode': 'item', 'concurrency': 4 },
'reducer': 'any-success',
})
.gather(placement('recommend-gather'), { [placement('recommend-scatter')]: {} }, { 'strategy': 'tool-candidate-merge' }, {
'success': placement('recommend-rank'),
'error': placement('recommend-rank'),
'empty': placement('recommend-rank'),
}, display('recommend-gather'))
.node(placement('recommend-rank'), nodes.rankByRating, { 'ranked': placement('recommend-merge') }, display('recommend-rank'))
.node(placement('recommend-merge'), nodes.mergeCandidates, { 'ranked': placement('recommend-record'), 'empty': placement('compose-empty') }, display('recommend-merge'))
.node(placement('recommend-record'), nodes.recordFindings, { 'recorded': placement('recommend-gate') }, display('recommend-record'))
.node(placement('recommend-gate'), nodes.hasCitationsGate, { 'pass': placement('recommend-recall'), 'fail': placement('compose-empty') }, display('recommend-gate'))
.node(placement('recommend-recall'), nodes.recallPastVisits, { 'recalled': placement('compose-loop') }, display('recommend-recall'))
// ── describe-book branch ─────────────────────────────────────────────────
// Inlined. Uses pickBestMatch to narrow multi-hit results to the top-3
// title-similar candidates before merge. Ensures the composer receives the
// specific book the visitor named, not arbitrary top-5 hits.
.node(placement('describe-extract'), nodes.extractQuery, { 'success': placement('describe-decide-tools'), 'retry': placement('describe-extract'), 'salvage': placement('describe-extract-salvage') }, display('describe-extract'))
.node(placement('describe-extract-salvage'), nodes.extractQuerySalvage, { 'done': placement('describe-decide-tools') }, display('describe-extract-salvage'))
.node(placement('describe-decide-tools'), nodes.decideTools, { 'tools': placement('describe-build-worksets'), 'no-tools': placement('describe-build-worksets'), 'retry': placement('describe-decide-tools'), 'salvage': placement('describe-decide-tools-salvage') }, display('describe-decide-tools'))
.node(placement('describe-decide-tools-salvage'), nodes.decideToolsSalvage, { 'done': placement('describe-build-worksets') }, display('describe-decide-tools-salvage'))
// Build scatter worksets before dispatch.
.node(placement('describe-build-worksets'), nodes.buildBookWorksets, {
'ready': placement('describe-scatter'),
}, display('describe-build-worksets'))
// Tool-registry scatter: DagReference resolves body DAG from each item's dagName.
// any-success reducer: 'success' → describe-pick, 'error' → compose-empty.
// 'error' fires when all tool scouts return empty.
.scatter(placement('describe-scatter'), 'bookWorksets', { 'dag': { 'from': 'item', 'path': 'dagIri', 'candidates': BOOK_SEARCH_TOOL_DAGS } }, {
'success': placement('describe-gather'),
'error': placement('compose-empty'),
'empty': placement('compose-empty'),
}, {
'name': 'describe-scatter',
'execution': { 'mode': 'item', 'concurrency': 4 },
'reducer': 'any-success',
})
.gather(placement('describe-gather'), { [placement('describe-scatter')]: {} }, { 'strategy': 'tool-candidate-merge' }, {
'success': placement('describe-pick'),
'error': placement('compose-empty'),
'empty': placement('compose-empty'),
}, display('describe-gather'))
.node(placement('describe-pick'), nodes.pickBestMatch, { 'picked': placement('describe-merge') }, display('describe-pick'))
.node(placement('describe-merge'), nodes.mergeCandidates, { 'ranked': placement('describe-record'), 'empty': placement('compose-empty') }, display('describe-merge'))
.node(placement('describe-record'), nodes.recordFindings, { 'recorded': placement('describe-gate') }, display('describe-record'))
.node(placement('describe-gate'), nodes.hasCitationsGate, { 'pass': placement('describe-recall'), 'fail': placement('compose-empty') }, display('describe-gate'))
.node(placement('describe-recall'), nodes.recallPastVisits, { 'recalled': placement('compose-loop') }, display('describe-recall'))
// ── recommend-similar branch ─────────────────────────────────────────────
// recommendSimilar seeds state.terms from prior-run shortlist memory.
// 'seeded' routes to the book-search-scatter sub-DAG; third placement of
// the same packaged cluster. 'empty' routes to the compose-empty terminal.
.node(placement('recommend-similar'), nodes.recommendSimilar, {
'seeded': placement('similar-search'),
'empty': placement('compose-empty'),
}, display('recommend-similar'))
// EmbeddedDAGNode: same book-search-scatter, third and final placement.
.embed(placement('similar-search'), BOOK_SEARCH_SCATTER_DAG_IRI, {
'success': placement('compose-loop'),
'error': placement('compose-empty'),
}, {
'name': 'similar-search',
'outputs': {
'terms': 'terms',
'toolPlan': 'toolPlan',
'candidates': 'candidates',
'shortlist': 'shortlist',
'priorContext': 'priorContext',
'failureCause': 'failureCause',
},
})
// ── compose-loop: shared compose/validate sub-DAG ──────────────────────────
// All branches that successfully find candidates converge here.
// composeResponse → validateResponse (retry loop, bounded by the retry budget on state (retriesFor('compose'))).
// One sub-DAG definition serves all four convergent branches.
// stateMapping.outputs copies the compose loop's writes back to the parent.
//
// Convergence policy: 'success' routes to the shared respond-to-visitor terminal
// at the parent level; the sub-DAG produces state.draft and exits cleanly;
// exactly ONE respond-to-visitor fires per run regardless of branch count.
// 'error' (retry budget exhausted) falls through to compose-empty so the
// visitor always receives an in-character response rather than a silent drop.
.embed(placement('compose-loop'), COMPOSE_RETRY_LOOP_DAG_IRI, {
'success': placement('respond-to-visitor'),
'error': placement('compose-empty'),
}, {
'name': 'compose-loop',
'outputs': {
'draft': 'draft',
'approvalState': 'approvalState',
},
})
// #endregion embedded-dag-placements
// ── respond-to-visitor: single shared happy-path terminal ───────────────
// Every branch that successfully composes a response converges here.
// compose-loop (success) and both memory + empty-result paths all route
// through this one placement. Convergence policy: exactly ONE respond-to-visitor
// fires per run with the full converged state.draft in context. Success routes
// to the canonical `end` TerminalNode rather than a bare null end-of-flow.
.node(placement('respond-to-visitor'), nodes.respondToVisitor, { 'success': placement('end') }, display('respond-to-visitor'))
// ── recall-memories branch ───────────────────────────────────────────────
// No search needed; the memory store is queried directly.
// recallMemories → composeMemoryResponse → respond-to-visitor (shared terminal).
.node(placement('memory-recall'), nodes.recallMemories, { 'recalled': placement('compose-memory-recall') }, display('memory-recall'))
.node(placement('compose-memory-recall'), nodes.composeMemoryResponse, {
'drafted': placement('respond-to-visitor'),
'retry': placement('compose-memory-recall'),
'salvage': placement('compose-memory-salvage'),
}, display('compose-memory-recall'))
.node(placement('compose-memory-salvage'), nodes.composeMemoryResponseSalvage, { 'done': placement('respond-to-visitor') }, display('compose-memory-salvage'))
// #region terminal-placements
// ── Terminal nodes ───────────────────────────────────────────────────────
.node(placement('decline-off-topic'), nodes.declineOffTopic, { 'success': placement('end') }, display('decline-off-topic'))
.node(placement('compose-empty'), nodes.composeEmptyResponse, {
'drafted': placement('respond-to-visitor'),
'retry': placement('compose-empty'),
'salvage': placement('compose-empty-salvage'),
}, display('compose-empty'))
.node(placement('compose-empty-salvage'), nodes.composeEmptyResponseSalvage, { 'done': placement('respond-to-visitor') }, display('compose-empty-salvage'))
// Canonical end-of-flow: every completed path (a composed answer or an
// off-topic decline) routes to this one `TerminalNode(completed)` instead of
// a bare `null` route. The flow ends explicitly, not by absence of a route.
.terminal(placement('end'), { outcome: 'completed', name: 'end' })
// #endregion terminal-placements
.build();
// #endregion dispatcher-bundleDetails for Nerds
The builder is deliberately not a second configuration language. There is no hidden builder runtime, no decorator metadata, and no post-build projection layer. The object returned by .build() is the document registerDAG validates.
That makes builder-authored DAGs easy to package as plugins: the plugin exports a normal DAG IRI/reference, and a parent flow embeds that reference exactly as it would embed a hand-authored DAG.
What it demonstrates
- Fluent chainable authoring. Every
.node()and.scatter()returnsthisfor fluent composition. The chain callsbuild()once at the end to produce the plainDAGobject. - Compile-time route exhaustiveness. The
routesargument is typed asRecord<TOutput, null | string>. TypeScript catches missing outputs (forgot'error') and stray outputs (typo in output name) at compile time. - Auto-entrypoint. The first
.node()call sets the DAG entrypoint automatically. Override with.entrypoints(...)when multiple labels should enter the same graph, as Cartographer does for source intake. - Embedded-DAG placements via
.embed().on-topic-search,author-search,similar-search, andcompose-loopareEmbeddedDAGNodeplacements. Each references a registered sub-DAG by IRI and declares itsstateMapping.outputs. - Scatter placements via
.scatter().reviews-scatteranddescribe-scatterscatter overstate.bookWorksetswith a dynamicDagReferencebody. Each clone readsitem.dagIri, validates it against explicit candidates, and executes the matching tool DAG; clone outputs then route into a first-class gather placement. - Same output as a literal
DAG..build()returns the identical wire shapeDAGDocument.load(json)validates. The builder is a convenience layer, not a separate runtime.
See this in action in the Archivist live demo.
Related Concepts
Read these next when you want to move between builder authoring, literal JSON-LD, embedded DAGs, and the reference shapes.
- Running domain: The Archivist
- DAGBuilder guide
- Example 03: Tool Schemas - the same topology loaded from a JSON file instead
- Example 05: Embedded DAGs - the embedded-DAG sub-DAG internals
- Reference: Entities,
DAG,SingleNode,ScatterNode