Example 27: Runtime DAG Dispatch
What It Is
Runtime DAG Dispatch lets one placement choose a registered child DAG from state or item data at execution time. The Archivist uses a dynamic DagReference inside its book-search scatter so each workset can run the tool DAG referenced by that item.
This is the embedding interface for heterogeneous work. The parent graph stays stable, while data selects whether a clone runs Open Library search, Google Books search, Wikipedia enrichment, or another registered tool flow.
How It Works
A dynamic DagReference names a source (state or item), a dotted path, and the finite candidate DAG set. For each clone, the dispatcher reads that path, expands the value through the DAG registry, validates that it is one of the declared candidates, and executes it as the clone body. Parent topology stays stable while item data selects the concrete child flow.
Because lookup goes through the registry, runtime dispatch still has a closed world: only registered DAGs can execute. That keeps plugin-provided flows and local flows on the same assembly surface.
Diagrams, Examples, and Outputs
DAG registration and diagram
The Archivist uses a dynamic DagReference in its book-search scatter. build-book-worksets prepares worksets where each item carries the registered tool DAG reference. The scatter then resolves the embedded body from item state at runtime, so the same placement can fan out to Open Library, Google Books, Wikipedia, or subject search without a plugin-specific node type.
Archivist runtime DagReference scatter
16 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:book-search-scatter",
"@type": "DAG",
"name": "book-search-scatter",
"version": "1.0",
"entrypoints": {
"main": "urn:noocodec:dag:book-search-scatter/node/extract-query"
},
"nodes": [
{
"@id": "urn:noocodec:dag:book-search-scatter/node/extract-query",
"@type": "SingleNode",
"name": "extract-query",
"node": "urn:noocodec:node:extract-query",
"outputs": {
"success": "urn:noocodec:dag:book-search-scatter/node/decide-tools",
"retry": "urn:noocodec:dag:book-search-scatter/node/extract-query",
"salvage": "urn:noocodec:dag:book-search-scatter/node/extract-query-salvage"
}
},
{
"@id": "urn:noocodec:dag:book-search-scatter/node/extract-query-salvage",
"@type": "SingleNode",
"name": "extract-query-salvage",
"node": "urn:noocodec:node:extract-query-salvage",
"outputs": {
"done": "urn:noocodec:dag:book-search-scatter/node/decide-tools"
}
},
{
"@id": "urn:noocodec:dag:book-search-scatter/node/decide-tools",
"@type": "SingleNode",
"name": "decide-tools",
"node": "urn:noocodec:node:decide-tools",
"outputs": {
"tools": "urn:noocodec:dag:book-search-scatter/node/recall-candidates",
"no-tools": "urn:noocodec:dag:book-search-scatter/node/recall-candidates",
"retry": "urn:noocodec:dag:book-search-scatter/node/decide-tools",
"salvage": "urn:noocodec:dag:book-search-scatter/node/decide-tools-salvage"
}
},
{
"@id": "urn:noocodec:dag:book-search-scatter/node/decide-tools-salvage",
"@type": "SingleNode",
"name": "decide-tools-salvage",
"node": "urn:noocodec:node:decide-tools-salvage",
"outputs": {
"done": "urn:noocodec:dag:book-search-scatter/node/recall-candidates"
}
},
{
"@id": "urn:noocodec:dag:book-search-scatter/node/recall-candidates",
"@type": "SingleNode",
"name": "recall-candidates",
"node": "urn:noocodec:node:recall-candidates",
"outputs": {
"recalled": "urn:noocodec:dag:book-search-scatter/node/build-book-worksets"
}
},
{
"@id": "urn:noocodec:dag:book-search-scatter/node/build-book-worksets",
"@type": "SingleNode",
"name": "build-book-worksets",
"node": "urn:noocodec:node:build-book-worksets",
"outputs": {
"ready": "urn:noocodec:dag:book-search-scatter/node/book-search-scatter"
}
},
{
"@id": "urn:noocodec:dag:book-search-scatter/node/book-search-scatter",
"@type": "ScatterNode",
"name": "book-search-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:book-search-scatter/node/book-search-gather",
"error": "urn:noocodec:dag:book-search-scatter/node/book-search-gather",
"empty": "urn:noocodec:dag:book-search-scatter/node/rank-candidates"
},
"itemKey": "currentItem",
"reducer": "any-success",
"execution": {
"mode": "item",
"concurrency": 4
}
},
{
"@id": "urn:noocodec:dag:book-search-scatter/node/book-search-gather",
"@type": "GatherNode",
"name": "book-search-gather",
"sources": {
"urn:noocodec:dag:book-search-scatter/node/book-search-scatter": {}
},
"gather": {
"strategy": "tool-candidate-merge"
},
"outputs": {
"success": "urn:noocodec:dag:book-search-scatter/node/rank-candidates",
"error": "urn:noocodec:dag:book-search-scatter/node/rank-candidates",
"empty": "urn:noocodec:dag:book-search-scatter/node/rank-candidates"
}
},
{
"@id": "urn:noocodec:dag:book-search-scatter/node/rank-candidates",
"@type": "SingleNode",
"name": "rank-candidates",
"node": "urn:noocodec:node:rank-candidates",
"outputs": {
"ranked": "urn:noocodec:dag:book-search-scatter/node/merge-candidates",
"retry": "urn:noocodec:dag:book-search-scatter/node/rank-candidates",
"salvage": "urn:noocodec:dag:book-search-scatter/node/rank-candidates-salvage"
}
},
{
"@id": "urn:noocodec:dag:book-search-scatter/node/rank-candidates-salvage",
"@type": "SingleNode",
"name": "rank-candidates-salvage",
"node": "urn:noocodec:node:rank-candidates-salvage",
"outputs": {
"done": "urn:noocodec:dag:book-search-scatter/node/merge-candidates"
}
},
{
"@id": "urn:noocodec:dag:book-search-scatter/node/merge-candidates",
"@type": "SingleNode",
"name": "merge-candidates",
"node": "urn:noocodec:node:merge-candidates",
"outputs": {
"ranked": "urn:noocodec:dag:book-search-scatter/node/record-findings",
"empty": "urn:noocodec:dag:book-search-scatter/node/no-results"
}
},
{
"@id": "urn:noocodec:dag:book-search-scatter/node/record-findings",
"@type": "SingleNode",
"name": "record-findings",
"node": "urn:noocodec:node:record-findings",
"outputs": {
"recorded": "urn:noocodec:dag:book-search-scatter/node/has-citations-gate"
}
},
{
"@id": "urn:noocodec:dag:book-search-scatter/node/has-citations-gate",
"@type": "SingleNode",
"name": "has-citations-gate",
"node": "urn:noocodec:node:has-citations-gate",
"outputs": {
"pass": "urn:noocodec:dag:book-search-scatter/node/recall-past-visits",
"fail": "urn:noocodec:dag:book-search-scatter/node/no-results"
}
},
{
"@id": "urn:noocodec:dag:book-search-scatter/node/recall-past-visits",
"@type": "SingleNode",
"name": "recall-past-visits",
"node": "urn:noocodec:node:recall-past-visits",
"outputs": {
"recalled": "urn:noocodec:dag:book-search-scatter/node/found"
}
},
{
"@id": "urn:noocodec:dag:book-search-scatter/node/found",
"@type": "TerminalNode",
"name": "found",
"outcome": "completed"
},
{
"@id": "urn:noocodec:dag:book-search-scatter/node/no-results",
"@type": "TerminalNode",
"name": "no-results",
"outcome": "failed"
}
]
}Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
%% book-search-scatter (v1.0)
entry_main(["main"])
entry_main --> urn_noocodec_dag_book-search-scatter/node/extract-query
urn_noocodec_dag_book-search-scatter/node/extract-query["extract-query"]
urn_noocodec_dag_book-search-scatter/node/extract-query -->|success| urn_noocodec_dag_book-search-scatter/node/decide-tools
urn_noocodec_dag_book-search-scatter/node/extract-query -->|retry| urn_noocodec_dag_book-search-scatter/node/extract-query
urn_noocodec_dag_book-search-scatter/node/extract-query -->|salvage| urn_noocodec_dag_book-search-scatter/node/extract-query-salvage
urn_noocodec_dag_book-search-scatter/node/extract-query-salvage["extract-query-salvage"]
urn_noocodec_dag_book-search-scatter/node/extract-query-salvage -->|done| urn_noocodec_dag_book-search-scatter/node/decide-tools
urn_noocodec_dag_book-search-scatter/node/decide-tools["decide-tools"]
urn_noocodec_dag_book-search-scatter/node/decide-tools -->|tools| urn_noocodec_dag_book-search-scatter/node/recall-candidates
urn_noocodec_dag_book-search-scatter/node/decide-tools -->|no-tools| urn_noocodec_dag_book-search-scatter/node/recall-candidates
urn_noocodec_dag_book-search-scatter/node/decide-tools -->|retry| urn_noocodec_dag_book-search-scatter/node/decide-tools
urn_noocodec_dag_book-search-scatter/node/decide-tools -->|salvage| urn_noocodec_dag_book-search-scatter/node/decide-tools-salvage
urn_noocodec_dag_book-search-scatter/node/decide-tools-salvage["decide-tools-salvage"]
urn_noocodec_dag_book-search-scatter/node/decide-tools-salvage -->|done| urn_noocodec_dag_book-search-scatter/node/recall-candidates
urn_noocodec_dag_book-search-scatter/node/recall-candidates["recall-candidates"]
urn_noocodec_dag_book-search-scatter/node/recall-candidates -->|recalled| urn_noocodec_dag_book-search-scatter/node/build-book-worksets
urn_noocodec_dag_book-search-scatter/node/build-book-worksets["build-book-worksets"]
urn_noocodec_dag_book-search-scatter/node/build-book-worksets -->|ready| urn_noocodec_dag_book-search-scatter/node/book-search-scatter
urn_noocodec_dag_book-search-scatter/node/book-search-scatter[/"book-search-scatter"/]
urn_noocodec_dag_book-search-scatter/node/book-search-scatter -->|success| urn_noocodec_dag_book-search-scatter/node/book-search-gather
urn_noocodec_dag_book-search-scatter/node/book-search-scatter -->|error| urn_noocodec_dag_book-search-scatter/node/book-search-gather
urn_noocodec_dag_book-search-scatter/node/book-search-scatter -->|empty| urn_noocodec_dag_book-search-scatter/node/rank-candidates
urn_noocodec_dag_book-search-scatter/node/book-search-gather{"book-search-gather"}
urn_noocodec_dag_book-search-scatter/node/book-search-gather -->|success| urn_noocodec_dag_book-search-scatter/node/rank-candidates
urn_noocodec_dag_book-search-scatter/node/book-search-gather -->|error| urn_noocodec_dag_book-search-scatter/node/rank-candidates
urn_noocodec_dag_book-search-scatter/node/book-search-gather -->|empty| urn_noocodec_dag_book-search-scatter/node/rank-candidates
urn_noocodec_dag_book-search-scatter/node/rank-candidates["rank-candidates"]
urn_noocodec_dag_book-search-scatter/node/rank-candidates -->|ranked| urn_noocodec_dag_book-search-scatter/node/merge-candidates
urn_noocodec_dag_book-search-scatter/node/rank-candidates -->|retry| urn_noocodec_dag_book-search-scatter/node/rank-candidates
urn_noocodec_dag_book-search-scatter/node/rank-candidates -->|salvage| urn_noocodec_dag_book-search-scatter/node/rank-candidates-salvage
urn_noocodec_dag_book-search-scatter/node/rank-candidates-salvage["rank-candidates-salvage"]
urn_noocodec_dag_book-search-scatter/node/rank-candidates-salvage -->|done| urn_noocodec_dag_book-search-scatter/node/merge-candidates
urn_noocodec_dag_book-search-scatter/node/merge-candidates["merge-candidates"]
urn_noocodec_dag_book-search-scatter/node/merge-candidates -->|ranked| urn_noocodec_dag_book-search-scatter/node/record-findings
urn_noocodec_dag_book-search-scatter/node/merge-candidates -->|empty| urn_noocodec_dag_book-search-scatter/node/no-results
urn_noocodec_dag_book-search-scatter/node/record-findings["record-findings"]
urn_noocodec_dag_book-search-scatter/node/record-findings -->|recorded| urn_noocodec_dag_book-search-scatter/node/has-citations-gate
urn_noocodec_dag_book-search-scatter/node/has-citations-gate["has-citations-gate"]
urn_noocodec_dag_book-search-scatter/node/has-citations-gate -->|pass| urn_noocodec_dag_book-search-scatter/node/recall-past-visits
urn_noocodec_dag_book-search-scatter/node/has-citations-gate -->|fail| urn_noocodec_dag_book-search-scatter/node/no-results
urn_noocodec_dag_book-search-scatter/node/recall-past-visits["recall-past-visits"]
urn_noocodec_dag_book-search-scatter/node/recall-past-visits -->|recalled| urn_noocodec_dag_book-search-scatter/node/found
urn_noocodec_dag_book-search-scatter/node/found((("found")))
urn_noocodec_dag_book-search-scatter/node/no-results>"no-results"]This is the production shape behind recursive or heterogeneous embedded calls:
- The parent DAG registers all candidate child DAGs once.
- Each scattered item provides a
dagIri. - The scatter body declares
{ dag: { from: 'item', path: 'dagIri', candidates: [...] } }. - The engine resolves the child DAG from the registry for that item.
- State mapping keeps child work isolated and copies only declared outputs back.
Run
npm run docs:devOpen The Archivist and ask a book question that needs external search or tool-backed lookup.
What It Lets You Do
Runtime DAG dispatch lets applications select an embedded child DAG from state at execution time. Use it when one placement fans out heterogeneous work items, plugin-provided DAGs, or tool-specific flows that share the same parent scatter and gather contract.
This is also the bridge between plugins and embedding: if a plugin registers a DAG IRI/reference, a host DAG can place it statically with a literal dag or select it dynamically with a DagReference.
Code Samples
The embedded DAG owns runtime DAG-reference resolution:
/**
* BookSearchScatterDAG: reusable query-extract + tool-registry scatter cluster.
*
* Internal flow:
*
* extract-query
* └─ success ──► decide-tools
* decide-tools
* └─ (tools | no-tools) ──► recall-candidates
* recall-candidates
* └─ recalled ──► build-book-worksets
* build-book-worksets
* └─ ready ──► book-search-scatter (scatter over bookWorksets, concurrency 4)
* body: DagReference(item.dagIri) (resolves declared tool DAG IRI per item)
* book-search-gather: tool-candidate-merge (reads clone output via accessor, no cast)
* reducer: any-success (routes 'success' if any tool found results)
* └─ rank-candidates
* └─ merge-candidates
* ├─ ranked ──► record-findings
* └─ empty ──► no-results (TerminalNode(failed) → parent EmbeddedDAGNode routes parent error)
* └─ record-findings
* └─ has-citations-gate
* ├─ pass ──► recall-past-visits ──► END (success)
* └─ fail ──► no-results (TerminalNode(failed) → parent EmbeddedDAGNode routes parent error)
*
* Outputs:
* success: query extracted, candidates found, ranked, recorded, and recalled
* error: no candidates after merge, or citations gate failed;
* signalled by the no-results TerminalNode(failed) placement so
* the parent EmbeddedDAGNode routes the parent placement to its
* 'error' branch
*
* Molecular import pattern:
* import { bookSearchScatterDAG } from './embedded-dags/BookSearchScatterDAG.ts';
* const nodes = ArchivistNodes.build(services);
* dispatcher.registerBundle(toolRegistry.bundle<ArchivistServices>());
* dispatcher.registerBundle({ nodes: nodes.bookSearchScatterNodes, dags: [bookSearchScatterDAG] });
*
* The sub-DAG reads `state.query` directly (no input stateMapping; the field
* names already align with the parent). Each parent placement supplies an
* `outputs` stateMapping that copies the fields the sub-DAG writes:
* `terms`, `toolPlan`, `candidates`, `shortlist`, `priorContext`,
* `failureCause` back onto the parent state so the downstream compose,
* group-by-year, and recall steps can read them.
*
* Three EmbeddedDAGNode placements in the parent `the-archivist` DAG reference
* this one definition. One definition, three usages:
* on-topic-search: general web book search
* author-search: author body-of-work search
* similar-search: recommend-similar search
*
* Reviews and describe branches are inlined in the parent because they use
* distinct post-scout steps (rankByRating and pickBestMatch respectively).
*/
import type { ArchivistState } from '../ArchivistState.ts';
import { DAGBuilder, DAGIdentity, PlaceholderNode } from '@studnicky/dagonizer';
import type { DAGType } from '@studnicky/dagonizer';
const BOOK_SEARCH_SCATTER_DAG_IRI = 'urn:noocodec:dag:book-search-scatter';
const placement = (placementIdentifier: string): string => DAGIdentity.placementId(BOOK_SEARCH_SCATTER_DAG_IRI, placementIdentifier);
const display = <T extends string>(name: T): { name: T } => ({ name });
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 extractQuery = new PlaceholderNode<ArchivistState, 'success' | 'retry' | 'salvage'>('urn:noocodec:node:extract-query', ['success', 'retry', 'salvage']);
const extractQuerySalvage = new PlaceholderNode<ArchivistState, 'done'>('urn:noocodec:node:extract-query-salvage', ['done']);
const decideTools = new PlaceholderNode<ArchivistState, 'tools' | 'no-tools' | 'retry' | 'salvage'>('urn:noocodec:node:decide-tools', ['tools', 'no-tools', 'retry', 'salvage']);
const decideToolsSalvage = new PlaceholderNode<ArchivistState, 'done'>('urn:noocodec:node:decide-tools-salvage', ['done']);
const recallCandidates = new PlaceholderNode<ArchivistState, 'recalled'>('urn:noocodec:node:recall-candidates', ['recalled']);
const buildBookWorksets = new PlaceholderNode<ArchivistState, 'ready'>('urn:noocodec:node:build-book-worksets', ['ready']);
const rankCandidates = new PlaceholderNode<ArchivistState, 'ranked' | 'retry' | 'salvage'>('urn:noocodec:node:rank-candidates', ['ranked', 'retry', 'salvage']);
const rankCandidatesSalvage = new PlaceholderNode<ArchivistState, 'done'>('urn:noocodec:node:rank-candidates-salvage', ['done']);
const mergeCandidates = new PlaceholderNode<ArchivistState, 'ranked' | 'empty'>('urn:noocodec:node:merge-candidates', ['ranked', 'empty']);
const recordFindings = new PlaceholderNode<ArchivistState, 'recorded'>('urn:noocodec:node:record-findings', ['recorded']);
const hasCitationsGate = new PlaceholderNode<ArchivistState, 'pass' | 'fail'>('urn:noocodec:node:has-citations-gate', ['pass', 'fail']);
const recallPastVisits = new PlaceholderNode<ArchivistState, 'recalled'>('urn:noocodec:node:recall-past-visits', ['recalled']);
export const bookSearchScatterDAG: DAGType = new DAGBuilder(BOOK_SEARCH_SCATTER_DAG_IRI, '1.0', display('book-search-scatter'))
// ── 1. extract-query ─────────────────────────────────────────────────────
// LLM parses the raw visitor question into structured search terms.
// Writes state.terms for the scouts and decide-tools to consume.
// 'retry' loops back (bounded by the state retry budget); 'salvage' routes to
// a deterministic recovery node; never a fabricated term list on the node.
// #region retry-salvage-wiring
.node(placement('extract-query'), extractQuery, {
'success': placement('decide-tools'),
'retry': placement('extract-query'), // flow-shape retry loop (self-edge)
'salvage': placement('extract-query-salvage'), // recovery route
}, display('extract-query'))
.node(placement('extract-query-salvage'), extractQuerySalvage, {
'done': placement('decide-tools'), // deterministic recovery rejoins the happy path
}, display('extract-query-salvage'))
// #endregion retry-salvage-wiring
// ── 2. decide-tools ──────────────────────────────────────────────────────
// LLM decides which external sources to invoke. Both outputs route into
// recall-candidates so prior memory is loaded before scouts fire.
// 'retry' loops back (bounded); 'salvage' routes to the minimal-plan node.
.node(placement('decide-tools'), decideTools, {
'tools': placement('recall-candidates'),
'no-tools': placement('recall-candidates'),
'retry': placement('decide-tools'),
'salvage': placement('decide-tools-salvage'),
}, display('decide-tools'))
.node(placement('decide-tools-salvage'), decideToolsSalvage, {
'done': placement('recall-candidates'),
}, display('decide-tools-salvage'))
// ── 2b. recall-candidates ────────────────────────────────────────────────
// Pre-loads state.priorCandidates from memory: shortlisted books from prior
// runs whose visitor query has Jaccard >= 0.35 overlap with the current
// query. Cap 10. Always routes 'recalled', even when no prior runs match.
.node(placement('recall-candidates'), recallCandidates, {
'recalled': placement('build-book-worksets'),
}, display('recall-candidates'))
// ── 2c. build-book-worksets ──────────────────────────────────────────────
// Converts state.toolPlan into a bookWorksets array where each entry
// carries { dagIri: 'urn:noocodec:tool:<name>', arguments: {...} }. The scatter
// placement reads dagIri through an item-scoped DagReference to resolve
// the body DAG at runtime.
.node(placement('build-book-worksets'), buildBookWorksets, {
'ready': placement('book-search-scatter'),
}, display('build-book-worksets'))
// ── 3. book-search-scatter ───────────────────────────────────────────────
// Tool-registry scatter: bookWorksets items fan out concurrently. Each item
// carries its own tool DAG IRI via dagIri; the DagReference
// resolves the body DAG at runtime from the item. ToolInvokeNode reads the
// item's arguments field and calls the bound tool. The following GatherNode
// reads each clone's ToolInvocationState.output (via accessor, no cast)
// and folds the CandidateType[] into the parent state's candidates.
// any-success reducer: 'success' → rank-candidates when at least one tool hit;
// 'error' → rank-candidates to allow graceful empty-candidates handling.
.scatter(placement('book-search-scatter'), 'bookWorksets', { 'dag': { 'from': 'item', 'path': 'dagIri', 'candidates': BOOK_SEARCH_TOOL_DAGS } }, {
'success': placement('book-search-gather'),
'error': placement('book-search-gather'),
'empty': placement('rank-candidates'),
}, {
'name': 'book-search-scatter',
'execution': { 'mode': 'item', 'concurrency': 4 },
'reducer': 'any-success',
})
.gather(placement('book-search-gather'), { [placement('book-search-scatter')]: {} }, { 'strategy': 'tool-candidate-merge' }, {
'success': placement('rank-candidates'),
'error': placement('rank-candidates'),
'empty': placement('rank-candidates'),
}, display('book-search-gather'))
// ── 4. rank-candidates ───────────────────────────────────────────────────
// LLM-driven relevance scoring. Routes 'ranked' on success (an empty set is
// still a valid ranking, so merge can soft-gate on zero candidates).
// 'retry' loops back (bounded); 'salvage' passes candidates through unranked
// via a dedicated node rather than emitting them as if they were ranked.
.node(placement('rank-candidates'), rankCandidates, {
'ranked': placement('merge-candidates'),
'retry': placement('rank-candidates'),
'salvage': placement('rank-candidates-salvage'),
}, display('rank-candidates'))
.node(placement('rank-candidates-salvage'), rankCandidatesSalvage, {
'done': placement('merge-candidates'),
}, display('rank-candidates-salvage'))
// ── 5. merge-candidates ──────────────────────────────────────────────────
// Cross-source dedupe via CanonicalId, top-5. Routes 'empty' to
// no-results (TerminalNode(failed)) so the parent EmbeddedDAGNode's
// terminal outcome routes the parent placement to its 'error' branch.
.node(placement('merge-candidates'), mergeCandidates, {
'ranked': placement('record-findings'),
'empty': placement('no-results'),
}, display('merge-candidates'))
// ── 6. record-findings ───────────────────────────────────────────────────
// Deterministic RDF write: same input always produces the same triples.
.node(placement('record-findings'), recordFindings, {
'recorded': placement('has-citations-gate'),
}, display('record-findings'))
// ── 7. has-citations-gate ────────────────────────────────────────────────
// SPARQL ASK over the per-run state graph. Symbolic fence for the LLM.
// 'fail' routes to no-results (TerminalNode(failed)) so the parent
// EmbeddedDAGNode routes the parent placement to 'error'.
.node(placement('has-citations-gate'), hasCitationsGate, {
'pass': placement('recall-past-visits'),
'fail': placement('no-results'),
}, display('has-citations-gate'))
// ── 8. recall-past-visits ────────────────────────────────────────────────
// Injects prior-session context (prior queries + shortlisted titles) into
// state.priorContext, then routes to the canonical `found` TerminalNode
// (completed) so the parent EmbeddedDAGNode resolves its 'success' branch.
.node(placement('recall-past-visits'), recallPastVisits, {
'recalled': placement('found'),
}, display('recall-past-visits'))
// ── 9. Terminal nodes ────────────────────────────────────────────────────
// Both sub-DAG exits are canonical TerminalNode placements (no bare null
// routes): `found` (completed) drives the parent EmbeddedDAGNode's 'success'
// branch; `no-results` (failed) drives its 'error' branch.
.terminal(placement('found'), { outcome: 'completed', name: 'found' })
.terminal(placement('no-results'), { outcome: 'failed', name: 'no-results' })
.build();The browser demo registers the same tool DAGs before registering the parent Archivist DAG:
// Stable tool instances for the checkpoint resume path and archivistToolRegistry.
// One instance each: the HTTP tools are stateless.
const webSearchTool = new OpenLibrarySearchTool();
const googleBooksTool = new GoogleBooksTool();
const subjectSearchTool = new SubjectSearchTool();
const wikipediaSummaryTool = new WikipediaSummaryTool();
// Tool registry: each tool becomes an embeddable `tool:<name>` DAG that the
// book-search scatter resolves at runtime via `{ dagFrom: 'dagName' }`. Must be
// registered before bookSearchScatterDAG or every scatter item fails to
// resolve its body DAG and routes to 'error'.
const archivistToolRegistry = new ToolRegistry();
archivistToolRegistry.register(webSearchTool);
archivistToolRegistry.register(googleBooksTool);
archivistToolRegistry.register(subjectSearchTool);
archivistToolRegistry.register(wikipediaSummaryTool);Details for Nerds
- Runtime child selection.
DagReferencereads the child DAG reference from state or item data instead of baking one literaldagstring into the placement. - One placement, many bodies. The graph has one scatter placement even though different items can execute different registered DAGs.
- Registry as assembly boundary. Tool DAGs are registered before the parent DAG, so JSON-LD stays canonical and runtime lookup is deterministic.
- Embedded isolation. Each item runs in a child state; declared outputs merge back into the parent workset flow.
Related Concepts
- Example 05: Embedded DAGs - the stateMapping.input / stateMapping.output pattern
- Example 04: Scatter Scout - parallel fan-out with embedded DAG bodies
- Example 26: Tool Use - Archivist tool registry and search DAGs
- Reference: Execution