melchizedek-agents 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +12 -0
- package/CHANGELOG.md +1001 -0
- package/DOCUMENTATION.md +252 -65
- package/QUICKSTART.md +9 -6
- package/README.md +42 -4
- package/config/agents/examples/harness.yaml +3 -1
- package/config/agents/examples/model_zoo.yaml +10 -9
- package/config/agents/syndicateSchema.yaml +53 -16
- package/config/agents/templates/account_memory.yaml +2 -6
- package/config/agents/templates/case_desk.yaml +3 -9
- package/config/agents/templates/draft_review.yaml +3 -9
- package/config/agents/templates/intake_extractor.yaml +1 -3
- package/config/agents/templates/research_brief.yaml +3 -9
- package/config/agents/templates/research_desk.yaml +3 -9
- package/config/agents/templates/review_panel.yaml +4 -12
- package/config/agents/templates/support_triage.yaml +4 -12
- package/config/agents/templates/systems_operator.yaml +2 -6
- package/db/hardening.sql +7 -1
- package/db/migrations/0013_tool_credentials.sql +260 -0
- package/dist/lib/a2a/app.d.ts +70 -6
- package/dist/lib/a2a/app.js +107 -2
- package/dist/lib/a2a/executor.d.ts +20 -3
- package/dist/lib/a2a/executor.js +66 -7
- package/dist/lib/a2a/remoteAgent.d.ts +24 -9
- package/dist/lib/a2a/remoteAgent.js +31 -14
- package/dist/lib/adkPeer.d.ts +63 -0
- package/dist/lib/adkPeer.js +240 -0
- package/dist/lib/compile.d.ts +159 -30
- package/dist/lib/compile.js +215 -134
- package/dist/lib/compileAdk.d.ts +32 -0
- package/dist/lib/compileAdk.js +142 -0
- package/dist/lib/compileNative.d.ts +111 -0
- package/dist/lib/compileNative.js +278 -0
- package/dist/lib/config.d.ts +6 -5
- package/dist/lib/config.js +6 -5
- package/dist/lib/doctor.d.ts +28 -1
- package/dist/lib/doctor.js +64 -2
- package/dist/lib/index.d.ts +6 -1
- package/dist/lib/index.js +7 -1
- package/dist/lib/loadSyndicate.d.ts +18 -0
- package/dist/lib/memory/erase.d.ts +5 -3
- package/dist/lib/memory/erase.js +5 -2
- package/dist/lib/memory/namespace.d.ts +8 -1
- package/dist/lib/memory/namespace.js +12 -6
- package/dist/lib/memory/providers.js +2 -1
- package/dist/lib/memory/supabaseMemoryService.d.ts +37 -3
- package/dist/lib/memory/supabaseMemoryService.js +42 -2
- package/dist/lib/model.d.ts +47 -0
- package/dist/lib/model.js +37 -0
- package/dist/lib/models/adapterResolver.d.ts +115 -0
- package/dist/lib/models/adapterResolver.js +137 -0
- package/dist/lib/models/adkGeminiAdapter.d.ts +90 -0
- package/dist/lib/models/adkGeminiAdapter.js +369 -0
- package/dist/lib/models/adkShim.d.ts +111 -0
- package/dist/lib/models/adkShim.js +136 -0
- package/dist/lib/models/capabilities.d.ts +3 -0
- package/dist/lib/models/capabilities.js +33 -23
- package/dist/lib/models/chatCompletionsAdapter.d.ts +279 -0
- package/dist/lib/models/chatCompletionsAdapter.js +854 -0
- package/dist/lib/models/circuitBreaker.d.ts +42 -0
- package/dist/lib/models/circuitBreaker.js +70 -0
- package/dist/lib/models/claudeAdapter.d.ts +103 -0
- package/dist/lib/models/claudeAdapter.js +669 -0
- package/dist/lib/models/claudeLlm.d.ts +39 -62
- package/dist/lib/models/claudeLlm.js +45 -350
- package/dist/lib/models/claudeModels.d.ts +127 -0
- package/dist/lib/models/claudeModels.js +237 -0
- package/dist/lib/models/contract.d.ts +395 -0
- package/dist/lib/models/contract.js +39 -0
- package/dist/lib/models/endpoints.js +2 -1
- package/dist/lib/models/errorResponse.d.ts +50 -0
- package/dist/lib/models/errorResponse.js +48 -0
- package/dist/lib/models/fallback.d.ts +18 -11
- package/dist/lib/models/fallback.js +54 -43
- package/dist/lib/models/fallbackAdapter.d.ts +55 -0
- package/dist/lib/models/fallbackAdapter.js +62 -0
- package/dist/lib/models/gateway.js +2 -1
- package/dist/lib/models/gatewayAdapter.d.ts +51 -0
- package/dist/lib/models/gatewayAdapter.js +81 -0
- package/dist/lib/models/gatewayLlm.d.ts +12 -33
- package/dist/lib/models/gatewayLlm.js +16 -77
- package/dist/lib/models/geminiAdapter.d.ts +125 -0
- package/dist/lib/models/geminiAdapter.js +854 -0
- package/dist/lib/models/geminiState.d.ts +46 -0
- package/dist/lib/models/geminiState.js +51 -0
- package/dist/lib/models/genaiMapping.d.ts +283 -0
- package/dist/lib/models/genaiMapping.js +965 -0
- package/dist/lib/models/gptAdapter.d.ts +240 -0
- package/dist/lib/models/gptAdapter.js +804 -0
- package/dist/lib/models/gptLlm.d.ts +63 -123
- package/dist/lib/models/gptLlm.js +89 -603
- package/dist/lib/models/grokAdapter.d.ts +86 -0
- package/dist/lib/models/grokAdapter.js +142 -0
- package/dist/lib/models/grokLlm.d.ts +22 -45
- package/dist/lib/models/grokLlm.js +24 -69
- package/dist/lib/models/kimiAdapter.d.ts +157 -0
- package/dist/lib/models/kimiAdapter.js +203 -0
- package/dist/lib/models/kimiLlm.d.ts +15 -107
- package/dist/lib/models/kimiLlm.js +19 -155
- package/dist/lib/models/ollamaAdapter.d.ts +125 -0
- package/dist/lib/models/ollamaAdapter.js +155 -0
- package/dist/lib/models/ollamaLlm.d.ts +11 -107
- package/dist/lib/models/ollamaLlm.js +15 -153
- package/dist/lib/models/openAiCompatibleLlm.d.ts +73 -216
- package/dist/lib/models/openAiCompatibleLlm.js +109 -671
- package/dist/lib/models/providerState.d.ts +63 -0
- package/dist/lib/models/providerState.js +61 -0
- package/dist/lib/models/reasoning.d.ts +26 -0
- package/dist/lib/models/reasoning.js +59 -0
- package/dist/lib/models/registry.d.ts +62 -35
- package/dist/lib/models/registry.js +100 -124
- package/dist/lib/models/retry.d.ts +2 -1
- package/dist/lib/models/retry.js +3 -2
- package/dist/lib/models/schemaNormalize.d.ts +98 -0
- package/dist/lib/models/schemaNormalize.js +321 -1
- package/dist/lib/models/tracedGemini.d.ts +41 -0
- package/dist/lib/models/tracedGemini.js +98 -0
- package/dist/lib/models/urls.d.ts +11 -0
- package/dist/lib/models/urls.js +16 -0
- package/dist/lib/net/redirects.d.ts +21 -2
- package/dist/lib/net/redirects.js +11 -34
- package/dist/lib/observability/audit.d.ts +5 -1
- package/dist/lib/observability/lineage.d.ts +14 -0
- package/dist/lib/observability/lineage.js +33 -0
- package/dist/lib/observability/supabaseSpanExporter.d.ts +10 -4
- package/dist/lib/observability/supabaseSpanExporter.js +17 -9
- package/dist/lib/observability/tracer.d.ts +26 -8
- package/dist/lib/observability/tracer.js +43 -18
- package/dist/lib/runtime/adkMemoryBridge.d.ts +46 -0
- package/dist/lib/runtime/adkMemoryBridge.js +68 -0
- package/dist/lib/runtime/adkSessionBridge.d.ts +78 -0
- package/dist/lib/runtime/adkSessionBridge.js +176 -0
- package/dist/lib/runtime/approvals.d.ts +5 -0
- package/dist/lib/runtime/approvals.js +39 -8
- package/dist/lib/runtime/credentials.d.ts +61 -0
- package/dist/lib/runtime/credentials.js +67 -0
- package/dist/lib/runtime/events.d.ts +229 -0
- package/dist/lib/runtime/events.js +274 -0
- package/dist/lib/runtime/logging.d.ts +32 -0
- package/dist/lib/runtime/logging.js +55 -0
- package/dist/lib/runtime/memoryService.d.ts +72 -0
- package/dist/lib/runtime/memoryService.js +25 -0
- package/dist/lib/runtime/native/agentLoop.d.ts +209 -0
- package/dist/lib/runtime/native/agentLoop.js +705 -0
- package/dist/lib/runtime/native/compaction.d.ts +103 -0
- package/dist/lib/runtime/native/compaction.js +191 -0
- package/dist/lib/runtime/native/delegate.d.ts +151 -0
- package/dist/lib/runtime/native/delegate.js +256 -0
- package/dist/lib/runtime/native/history.d.ts +83 -0
- package/dist/lib/runtime/native/history.js +312 -0
- package/dist/lib/runtime/native/interrupts.d.ts +129 -0
- package/dist/lib/runtime/native/interrupts.js +310 -0
- package/dist/lib/runtime/native/request.d.ts +215 -0
- package/dist/lib/runtime/native/request.js +585 -0
- package/dist/lib/runtime/native/selfCorrection.d.ts +164 -0
- package/dist/lib/runtime/native/selfCorrection.js +327 -0
- package/dist/lib/runtime/native/step.d.ts +156 -0
- package/dist/lib/runtime/native/step.js +263 -0
- package/dist/lib/runtime/native/taskMode.d.ts +64 -0
- package/dist/lib/runtime/native/taskMode.js +126 -0
- package/dist/lib/runtime/native/telemetry.d.ts +75 -0
- package/dist/lib/runtime/native/telemetry.js +299 -0
- package/dist/lib/runtime/native/tempState.d.ts +31 -0
- package/dist/lib/runtime/native/tempState.js +56 -0
- package/dist/lib/runtime/nativeTurn.d.ts +86 -0
- package/dist/lib/runtime/nativeTurn.js +107 -0
- package/dist/lib/runtime/questions.d.ts +29 -7
- package/dist/lib/runtime/questions.js +47 -32
- package/dist/lib/runtime/runtimeFlag.d.ts +38 -0
- package/dist/lib/runtime/runtimeFlag.js +58 -0
- package/dist/lib/runtime/sessions.d.ts +166 -0
- package/dist/lib/runtime/sessions.js +201 -0
- package/dist/lib/runtime/syndicateTurn.d.ts +46 -5
- package/dist/lib/runtime/syndicateTurn.js +233 -36
- package/dist/lib/runtime/valueDepth.d.ts +23 -0
- package/dist/lib/runtime/valueDepth.js +41 -0
- package/dist/lib/session/supabaseSessionService.d.ts +65 -18
- package/dist/lib/session/supabaseSessionService.js +148 -105
- package/dist/lib/session/transcript.d.ts +69 -23
- package/dist/lib/session/transcript.js +105 -29
- package/dist/lib/skills.js +33 -7
- package/dist/lib/storage/postgres/credentialStore.d.ts +20 -0
- package/dist/lib/storage/postgres/credentialStore.js +82 -0
- package/dist/lib/storage/postgres/index.d.ts +12 -0
- package/dist/lib/storage/postgres/index.js +6 -1
- package/dist/lib/storage/postgres/sessionService.d.ts +44 -16
- package/dist/lib/storage/postgres/sessionService.js +128 -66
- package/dist/lib/syndicateSchema.d.ts +49 -0
- package/dist/lib/syndicateSchema.js +99 -9
- package/dist/lib/toolRegistry.d.ts +26 -2
- package/dist/lib/toolRegistry.js +73 -17
- package/dist/lib/tools/adkTool.d.ts +81 -0
- package/dist/lib/tools/adkTool.js +268 -0
- package/dist/lib/tools/auth.d.ts +118 -0
- package/dist/lib/tools/auth.js +89 -0
- package/dist/lib/tools/collectionsSearchTool.d.ts +8 -7
- package/dist/lib/tools/collectionsSearchTool.js +12 -27
- package/dist/lib/tools/credentialCipher.d.ts +58 -0
- package/dist/lib/tools/credentialCipher.js +116 -0
- package/dist/lib/tools/credentialStore.d.ts +77 -0
- package/dist/lib/tools/credentialStore.js +246 -0
- package/dist/lib/tools/examples.d.ts +43 -0
- package/dist/lib/tools/examples.js +62 -0
- package/dist/lib/tools/generateImageTool.d.ts +15 -6
- package/dist/lib/tools/generateImageTool.js +19 -29
- package/dist/lib/tools/inspectImageTool.d.ts +9 -6
- package/dist/lib/tools/inspectImageTool.js +13 -17
- package/dist/lib/tools/mcpToolFactory.d.ts +42 -1
- package/dist/lib/tools/mcpToolFactory.js +46 -47
- package/dist/lib/tools/memoryTools.d.ts +61 -0
- package/dist/lib/tools/memoryTools.js +119 -0
- package/dist/lib/tools/nativeTools.d.ts +38 -0
- package/dist/lib/tools/nativeTools.js +43 -0
- package/dist/lib/tools/oauthConsent.d.ts +181 -0
- package/dist/lib/tools/oauthConsent.js +330 -0
- package/dist/lib/tools/openapi/call.d.ts +108 -0
- package/dist/lib/tools/openapi/call.js +295 -0
- package/dist/lib/tools/openapi/parse.d.ts +147 -0
- package/dist/lib/tools/openapi/parse.js +570 -0
- package/dist/lib/tools/openapiTools.d.ts +25 -17
- package/dist/lib/tools/openapiTools.js +124 -121
- package/dist/lib/tools/scienceTools.d.ts +14 -14
- package/dist/lib/tools/skillToolset.d.ts +58 -87
- package/dist/lib/tools/skillToolset.js +61 -142
- package/dist/lib/tools/skills/env.d.ts +58 -0
- package/dist/lib/tools/skills/env.js +165 -0
- package/dist/lib/tools/skills/executor.d.ts +114 -0
- package/dist/lib/tools/skills/executor.js +331 -0
- package/dist/lib/tools/skills/frontmatter.d.ts +79 -0
- package/dist/lib/tools/skills/frontmatter.js +215 -0
- package/dist/lib/tools/skills/loader.d.ts +91 -0
- package/dist/lib/tools/skills/loader.js +257 -0
- package/dist/lib/tools/skills/tools.d.ts +187 -0
- package/dist/lib/tools/skills/tools.js +386 -0
- package/dist/lib/tools/taskTools.d.ts +15 -15
- package/dist/lib/tools/tool.d.ts +354 -0
- package/dist/lib/tools/tool.js +304 -0
- package/dist/lib/tools/toolContract.d.ts +89 -33
- package/dist/lib/tools/toolContract.js +104 -82
- package/dist/lib/tools/urlContextTool.d.ts +4 -1
- package/dist/lib/tools/urlContextTool.js +6 -2
- package/dist/lib/tools/webExtractTool.d.ts +3 -3
- package/dist/lib/tools/webExtractTool.js +2 -1
- package/dist/lib/tools/webSearchTool.d.ts +7 -10
- package/dist/lib/tools/webSearchTool.js +15 -60
- package/dist/lib/tools/wikiTools.d.ts +19 -19
- package/dist/lib/tools/wikiTools.js +13 -9
- package/dist/lib/tools/xApiSearchTool.d.ts +3 -3
- package/dist/lib/tools/xApiSearchTool.js +2 -1
- package/dist/lib/tools/xSearchTool.d.ts +8 -10
- package/dist/lib/tools/xSearchTool.js +12 -63
- package/dist/lib/tools/xaiSearchParams.d.ts +41 -0
- package/dist/lib/tools/xaiSearchParams.js +126 -0
- package/dist/lib/wiki/agentRun.d.ts +12 -0
- package/dist/lib/wiki/agentRun.js +74 -35
- package/dist/lib/wiki/entities.d.ts +12 -0
- package/dist/lib/wiki/entities.js +45 -3
- package/dist/lib/workflow/agentNode.d.ts +153 -0
- package/dist/lib/workflow/agentNode.js +365 -0
- package/dist/lib/workflow/graph.d.ts +178 -0
- package/dist/lib/workflow/graph.js +468 -0
- package/dist/lib/workflow/nodeEvents.d.ts +50 -0
- package/dist/lib/workflow/nodeEvents.js +101 -0
- package/dist/lib/workflow/pause.d.ts +122 -0
- package/dist/lib/workflow/pause.js +180 -0
- package/dist/lib/workflow/resume.d.ts +166 -0
- package/dist/lib/workflow/resume.js +441 -0
- package/dist/lib/workflow/route.d.ts +58 -0
- package/dist/lib/workflow/route.js +69 -0
- package/dist/lib/workflow/scheduler.d.ts +418 -0
- package/dist/lib/workflow/scheduler.js +756 -0
- package/dist/lib/workflow/toolNode.d.ts +127 -0
- package/dist/lib/workflow/toolNode.js +236 -0
- package/dist/lib/workflow/turn.d.ts +118 -0
- package/dist/lib/workflow/turn.js +216 -0
- package/dist/lib/workflow.d.ts +33 -11
- package/dist/lib/workflow.js +49 -27
- package/dist/lib/workflowConfig.d.ts +7 -2
- package/dist/lib/workflowConfig.js +7 -12
- package/dist/scripts/a2a_server.js +4 -3
- package/dist/scripts/assistant_worker.d.ts +2 -1
- package/dist/scripts/assistant_worker.js +9 -4
- package/dist/scripts/doctor.d.ts +3 -1
- package/dist/scripts/doctor.js +4 -2
- package/dist/scripts/syndicate_chat.js +19 -10
- package/package.json +9 -3
- package/skills/briefs/melchizedek-author.md +3 -2
- package/skills/briefs/melchizedek-models.md +13 -9
- package/skills/briefs/melchizedek.md +3 -3
- package/skills/melchizedek/SKILL.md +2 -1
- package/skills/melchizedek-author/SKILL.md +2 -1
- package/skills/melchizedek-author/assets/minimal.yaml +2 -0
- package/skills/melchizedek-models/SKILL.md +23 -6
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/workflow/scheduler.ts — the engine's own walk of a workflow graph.
|
|
3
|
+
*
|
|
4
|
+
* `runWorkflowGraph(graph, options)` runs the `WorkflowGraph` that
|
|
5
|
+
* `buildWorkflowGraph` (lib/workflow/graph.ts) builds: a node runs when a
|
|
6
|
+
* predecessor's completion triggers it, a node with several successors fans
|
|
7
|
+
* out, a `join` waits until every predecessor has completed, a `map` runs its
|
|
8
|
+
* agent once per item under `maxParallel`, and each node's output is the
|
|
9
|
+
* input its successors receive. It imports nothing from ADK.
|
|
10
|
+
*
|
|
11
|
+
* What a node DOES is not this module's business. Agent, tool and ask_user
|
|
12
|
+
* nodes, and each item of a map, are run by the `runNode` function the caller
|
|
13
|
+
* passes in (WS4-3, WS4-5 and WS4-4a supply them). The scheduler runs the
|
|
14
|
+
* kinds that are pure graph mechanics itself: the start node seeds the walk,
|
|
15
|
+
* a `join` outputs `{ <predecessor>: <output> }`, a `route` step re-emits its
|
|
16
|
+
* input with the route `routeOf` reads from it, and a `map` is the worker
|
|
17
|
+
* pool around its items.
|
|
18
|
+
*
|
|
19
|
+
* ── The same walk as ADK's Workflow ──────────────────────────────────────
|
|
20
|
+
* The loop is ADK 2.2's (`Workflow.runLoop`, workflow/workflow.js), step for
|
|
21
|
+
* step, so a graph completes in the order ADK records for it
|
|
22
|
+
* (tests/workflowScheduler.test.ts runs both and compares):
|
|
23
|
+
*
|
|
24
|
+
* - Triggers are buffered per node, in the order they were pushed. Each
|
|
25
|
+
* pass starts every buffered node that is not already running, in the
|
|
26
|
+
* buffer's order, until `maxConcurrency` nodes are pending.
|
|
27
|
+
* - The loop then waits for the first pending node to settle, by
|
|
28
|
+
* `Promise.race` over the pending runs in the order they started, so of
|
|
29
|
+
* two runs already settled the one started first is handled first.
|
|
30
|
+
* - A completed node pushes one trigger to each successor its route
|
|
31
|
+
* selects (`always` edges, the edges keyed to its route compared as
|
|
32
|
+
* strings in ADK's spelling of the key, else the `default` edge). A join
|
|
33
|
+
* is triggered only once every predecessor has COMPLETED, with their
|
|
34
|
+
* outputs keyed in edge order. A node triggered while it runs runs again
|
|
35
|
+
* afterwards, once per trigger, as ADK does: only a join waits for all.
|
|
36
|
+
* - Branches and node paths are ADK's (`Planner@1`, `Six.Fan.Agent@0`), so
|
|
37
|
+
* the events a node runner writes can carry the values ADK writes.
|
|
38
|
+
* - The workflow's output is the one terminal node's output; two terminal
|
|
39
|
+
* outputs fail the run with ADK's message.
|
|
40
|
+
*
|
|
41
|
+
* ── The controls (ADK's node_runner.js, ADR 0089) ─────────────────────────
|
|
42
|
+
* Every node run goes through `executeNode`, and every attempt of it through
|
|
43
|
+
* `withControls`, which is ADK's `runChildNode` loop:
|
|
44
|
+
*
|
|
45
|
+
* - **Timeout.** A node with `timeout` races each attempt against a timer;
|
|
46
|
+
* the runner's signal aborts when it fires and the attempt fails with
|
|
47
|
+
* `NodeTimeoutError`. Without a timeout the attempt is awaited, as ADK
|
|
48
|
+
* awaits it: the runner gets the signal and must settle when it aborts.
|
|
49
|
+
* - **Retry.** A failed attempt is retried while the node's `retry` allows
|
|
50
|
+
* (`max_attempts`, default 5; an `exceptions` list of error names when
|
|
51
|
+
* one is given), after ADK's backoff with its jitter. An abort and a map
|
|
52
|
+
* item's failure are never retried; an abort cuts a backoff short.
|
|
53
|
+
* - **Node errors.** A runner reports an error by returning `error`: the
|
|
54
|
+
* walk emits it (`node_error`, source `node`) and, when the result has no
|
|
55
|
+
* output and no route, the attempt fails with `NodeReportedError`. A node
|
|
56
|
+
* that gives up fails the walk; the walk emits that error once (source
|
|
57
|
+
* `workflow`) unless the node already reported it or the walk was
|
|
58
|
+
* aborted, then aborts the other runs, lets them settle and rethrows the
|
|
59
|
+
* error unchanged. Every `node_error` is collected in `nodeErrors`, in
|
|
60
|
+
* the shape runSyndicateTurn's drain collects ADK's error events.
|
|
61
|
+
* - **Abort and deadline.** The walk's signal is the caller's, else the
|
|
62
|
+
* current turn's (lib/runtime/turnControl.ts), so a cancel or a deadline
|
|
63
|
+
* reaches every run. Once it fires no node starts, and the walk rejects
|
|
64
|
+
* with `InvocationAbortedError` when nothing is left running.
|
|
65
|
+
* - **max_concurrency** counts every pending run, a retrying one included.
|
|
66
|
+
*
|
|
67
|
+
* A map item runs under its agent's own modifiers (`nodes.<agent>`), and an
|
|
68
|
+
* item that gives up fails the map with `DynamicNodeFailError`, as ADK's
|
|
69
|
+
* ParallelWorker does; the map entry's own `retry` and `timeout` are not
|
|
70
|
+
* applied, because ADK's compile does not apply them.
|
|
71
|
+
*
|
|
72
|
+
* ── Interrupts (ADK's handleCompletion and finalize, ADR 0092) ───────────
|
|
73
|
+
* A runner that pauses its node on a person returns `interruptIds` (the ids
|
|
74
|
+
* of the input requests its events carry; lib/workflow/pause.ts raises them
|
|
75
|
+
* for an ask_user node). The node then WAITS, as ADK's does: it is not
|
|
76
|
+
* completed, records no output, triggers no successor, and is not started
|
|
77
|
+
* again by a later trigger in the same walk. The rest of the graph runs on.
|
|
78
|
+
* A walk that ends with a node waiting resolves with every open id in
|
|
79
|
+
* `interruptIds`, in the order the nodes paused, and no output (ADK skips
|
|
80
|
+
* the terminal check). A reported error with an interrupt is not the node's
|
|
81
|
+
* failure. A map item's interrupts are not carried (an agent node's own
|
|
82
|
+
* pause does not run here yet).
|
|
83
|
+
*
|
|
84
|
+
* ── Resume (ADK's startNodeTask on rehydrated state, ADR 0094) ───────────
|
|
85
|
+
* A walk given `resume` (lib/workflow/resume.ts builds it from the stored
|
|
86
|
+
* events) starts from START as ADK's does, and each node's first activation
|
|
87
|
+
* takes that node's next prior run:
|
|
88
|
+
*
|
|
89
|
+
* - a prior run with an output or a route and no open interrupt is done:
|
|
90
|
+
* the node completes at once with that output, route and branch, emits
|
|
91
|
+
* `node_resumed` (not `node_start` or `node_end`; ADK writes no event
|
|
92
|
+
* for it) and triggers its successors as a completion does;
|
|
93
|
+
* - a paused prior run of a node that does not rerun on resume
|
|
94
|
+
* (`rerunsOnResume`) completes with its answers, the same way;
|
|
95
|
+
* - any other node runs, a paused one on the input it recorded, with the
|
|
96
|
+
* answers in `NodeRun.resumeInputs` (none on a repeat activation in the
|
|
97
|
+
* same walk). Neither shortcut counts as a run of the node.
|
|
98
|
+
*
|
|
99
|
+
* A task-mode node that waits for its output (WS4-3) is a later ticket.
|
|
100
|
+
*
|
|
101
|
+
* ── Tracing (ADR 0095) ───────────────────────────────────────────────────
|
|
102
|
+
* The scheduler opens no span. A caller that passes `traceNode` has each
|
|
103
|
+
* node run (every attempt and backoff inside it) and each map item's run
|
|
104
|
+
* wrapped, as ADK's node runner opens `execute_node` around them; the
|
|
105
|
+
* native turn (lib/workflow/turn.ts) passes the engine's `node.execute`
|
|
106
|
+
* span, so an agent node's own spans nest under it.
|
|
107
|
+
*/
|
|
108
|
+
import { routeOf } from '../workflowConfig.js';
|
|
109
|
+
import { currentTurnSignal } from '../runtime/turnControl.js';
|
|
110
|
+
import { createTurnEvent } from '../runtime/events.js';
|
|
111
|
+
import { START_NODE, adkRouteString } from './graph.js';
|
|
112
|
+
import { isFastForwardable, rerunsOnResume } from './resume.js';
|
|
113
|
+
/** ADK's ParallelWorker pool size when `max_parallel` is not set. */
|
|
114
|
+
export const DEFAULT_MAX_PARALLEL = 8;
|
|
115
|
+
// ── Errors (ADK's workflow/errors.js; the names are ADK's, so errorType matches) ─
|
|
116
|
+
/** A node ran past its `timeout`. */
|
|
117
|
+
export class NodeTimeoutError extends Error {
|
|
118
|
+
nodeName;
|
|
119
|
+
timeout;
|
|
120
|
+
constructor(options) {
|
|
121
|
+
super(`Node '${options.nodeName}' timed out after ${options.timeout} seconds.`);
|
|
122
|
+
this.name = 'NodeTimeoutError';
|
|
123
|
+
this.nodeName = options.nodeName;
|
|
124
|
+
this.timeout = options.timeout;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** A node reported an error and produced no output and no route. */
|
|
128
|
+
export class NodeReportedError extends Error {
|
|
129
|
+
code;
|
|
130
|
+
nodeName;
|
|
131
|
+
constructor(options) {
|
|
132
|
+
const code = options.errorCode ?? 'UNKNOWN_ERROR';
|
|
133
|
+
const detail = options.errorMessage ?? code;
|
|
134
|
+
super(`Node '${options.nodeName}' failed: ${code === 'UNKNOWN_ERROR' ? detail : `${code}: ${detail}`}`);
|
|
135
|
+
this.name = 'NodeReportedError';
|
|
136
|
+
this.code = code;
|
|
137
|
+
this.nodeName = options.nodeName;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** The walk, or one node's run, stopped because its signal fired. */
|
|
141
|
+
export class InvocationAbortedError extends Error {
|
|
142
|
+
constructor(message = 'Invocation aborted.', options) {
|
|
143
|
+
super(message, options);
|
|
144
|
+
this.name = 'InvocationAbortedError';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** A map item gave up; the map fails with this. */
|
|
148
|
+
export class DynamicNodeFailError extends Error {
|
|
149
|
+
error;
|
|
150
|
+
errorNodePath;
|
|
151
|
+
constructor(options) {
|
|
152
|
+
super(options.message);
|
|
153
|
+
this.name = 'DynamicNodeFailError';
|
|
154
|
+
this.error = options.error;
|
|
155
|
+
this.errorNodePath = options.errorNodePath;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const isNamed = (error, name) => error instanceof Error && error.name === name;
|
|
159
|
+
const isWaiting = (state) => state?.status === 'waiting' && state.interrupts.length > 0;
|
|
160
|
+
// ── Retry (ADK's retry_utils.js) ─────────────────────────────────────────────
|
|
161
|
+
/** ADK's defaults when a `retry` block leaves a field out. */
|
|
162
|
+
export const RETRY_DEFAULTS = { maxAttempts: 5, initialDelay: 1, maxDelay: 60, backoffFactor: 2, jitter: 1 };
|
|
163
|
+
/** The names an error answers to: its class's and its `name`, as ADK matches `exceptions`. */
|
|
164
|
+
export function errorNames(error) {
|
|
165
|
+
if (error instanceof Error)
|
|
166
|
+
return [...new Set([error.constructor?.name, error.name].filter((n) => !!n))];
|
|
167
|
+
if (typeof error === 'object' && error !== null)
|
|
168
|
+
return [error.constructor.name];
|
|
169
|
+
return [typeof error];
|
|
170
|
+
}
|
|
171
|
+
/** ADK's errorType: the class name, or the assigned name of a plain Error. */
|
|
172
|
+
export function errorName(error) {
|
|
173
|
+
const [className, assigned] = errorNames(error);
|
|
174
|
+
return className === 'Error' && assigned ? assigned : className;
|
|
175
|
+
}
|
|
176
|
+
/** Whether a node that has made `attempts` attempts and failed with `error` runs again. */
|
|
177
|
+
export function shouldRetry(error, retry, attempts) {
|
|
178
|
+
if (attempts >= (retry.max_attempts ?? RETRY_DEFAULTS.maxAttempts))
|
|
179
|
+
return false;
|
|
180
|
+
const exceptions = retry.exceptions;
|
|
181
|
+
return exceptions === undefined || errorNames(error).some((name) => exceptions.includes(name));
|
|
182
|
+
}
|
|
183
|
+
/** Seconds to wait before the attempt after `attempts`: exponential backoff, capped, with ADK's jitter. */
|
|
184
|
+
export function retryDelaySeconds(retry, attempts, random = Math.random) {
|
|
185
|
+
const initialDelay = retry.initial_delay ?? RETRY_DEFAULTS.initialDelay;
|
|
186
|
+
const maxDelay = retry.max_delay ?? RETRY_DEFAULTS.maxDelay;
|
|
187
|
+
const backoffFactor = retry.backoff_factor ?? RETRY_DEFAULTS.backoffFactor;
|
|
188
|
+
const jitter = retry.jitter ?? RETRY_DEFAULTS.jitter;
|
|
189
|
+
let delay = initialDelay * Math.pow(backoffFactor, Math.max(0, (attempts || 1) - 1));
|
|
190
|
+
if (jitter > 0) {
|
|
191
|
+
delay = Math.min(delay, maxDelay / (1 + jitter));
|
|
192
|
+
const span = jitter * delay;
|
|
193
|
+
delay = Math.max(0, delay - span + random() * (2 * span));
|
|
194
|
+
}
|
|
195
|
+
return Math.min(delay, maxDelay);
|
|
196
|
+
}
|
|
197
|
+
// ── Branches (ADK's branch_path.js) ──────────────────────────────────────────
|
|
198
|
+
const segmentsOf = (branch) => (branch ? branch.split('.') : []);
|
|
199
|
+
function subBranch(base, name, runId) {
|
|
200
|
+
return [...segmentsOf(base), `${name}@${runId}`].join('.');
|
|
201
|
+
}
|
|
202
|
+
function commonPrefix(branches) {
|
|
203
|
+
if (branches.length === 0)
|
|
204
|
+
return '';
|
|
205
|
+
const all = branches.map(segmentsOf);
|
|
206
|
+
const common = [];
|
|
207
|
+
const min = Math.min(...all.map((s) => s.length));
|
|
208
|
+
for (let i = 0; i < min; i++) {
|
|
209
|
+
const seg = all[0][i];
|
|
210
|
+
if (all.every((s) => s[i] === seg))
|
|
211
|
+
common.push(seg);
|
|
212
|
+
else
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
return common.join('.');
|
|
216
|
+
}
|
|
217
|
+
// ── The walk ─────────────────────────────────────────────────────────────────
|
|
218
|
+
/**
|
|
219
|
+
* Run a workflow graph to its end. Resolves with the workflow's output, every
|
|
220
|
+
* node's output and the completion order; rejects with the first node error,
|
|
221
|
+
* after every run still pending has settled.
|
|
222
|
+
*/
|
|
223
|
+
export async function runWorkflowGraph(graph, options) {
|
|
224
|
+
const workflowPath = options.nodePath ?? graph.name;
|
|
225
|
+
const parentBranch = options.branch;
|
|
226
|
+
const onEvent = options.onEvent ?? (() => { });
|
|
227
|
+
const nodeErrors = [];
|
|
228
|
+
const emit = (event) => {
|
|
229
|
+
if (event.type === 'node_error')
|
|
230
|
+
nodeErrors.push({ node: event.node, code: event.code, message: event.message });
|
|
231
|
+
onEvent(event);
|
|
232
|
+
};
|
|
233
|
+
const walk = { runNode: options.runNode, traceNode: options.traceNode ?? ((_node, run) => run()), emit, claimed: new WeakSet() };
|
|
234
|
+
// The caller's signal, else the turn's: a cancel or the turn's deadline stops the walk.
|
|
235
|
+
const parentSignal = options.signal ?? currentTurnSignal();
|
|
236
|
+
const controller = new AbortController();
|
|
237
|
+
/** True once the walk was stopped from outside (not by a node's failure). */
|
|
238
|
+
let stopped = false;
|
|
239
|
+
const onParentAbort = () => {
|
|
240
|
+
stopped = true;
|
|
241
|
+
controller.abort(parentSignal?.reason);
|
|
242
|
+
};
|
|
243
|
+
if (parentSignal?.aborted)
|
|
244
|
+
onParentAbort();
|
|
245
|
+
else
|
|
246
|
+
parentSignal?.addEventListener('abort', onParentAbort, { once: true });
|
|
247
|
+
const nodes = new Map();
|
|
248
|
+
const outputs = new Map();
|
|
249
|
+
const branches = new Map();
|
|
250
|
+
const triggers = new Map();
|
|
251
|
+
const pending = new Map();
|
|
252
|
+
const order = [];
|
|
253
|
+
const nodeOf = (name) => {
|
|
254
|
+
const node = graph.nodes.get(name);
|
|
255
|
+
if (!node)
|
|
256
|
+
throw new Error(`Node ${name} not found in graph.`);
|
|
257
|
+
return node;
|
|
258
|
+
};
|
|
259
|
+
const pushTrigger = (name, trigger) => {
|
|
260
|
+
const buffer = triggers.get(name);
|
|
261
|
+
if (buffer)
|
|
262
|
+
buffer.push(trigger);
|
|
263
|
+
else
|
|
264
|
+
triggers.set(name, [trigger]);
|
|
265
|
+
};
|
|
266
|
+
const popTrigger = (name) => {
|
|
267
|
+
const buffer = triggers.get(name);
|
|
268
|
+
if (!buffer || buffer.length === 0)
|
|
269
|
+
return undefined;
|
|
270
|
+
const trigger = buffer.shift();
|
|
271
|
+
if (buffer.length === 0)
|
|
272
|
+
triggers.delete(name);
|
|
273
|
+
return trigger;
|
|
274
|
+
};
|
|
275
|
+
const atConcurrencyLimit = () => graph.maxConcurrency !== undefined && pending.size >= graph.maxConcurrency;
|
|
276
|
+
// ADK's startNodeTask on a resumed walk: each node's first activation takes its next prior run.
|
|
277
|
+
const activated = new Set();
|
|
278
|
+
const resumeStart = (name, node) => {
|
|
279
|
+
const resume = options.resume;
|
|
280
|
+
if (!resume)
|
|
281
|
+
return {};
|
|
282
|
+
const repeat = activated.has(name);
|
|
283
|
+
activated.add(name);
|
|
284
|
+
const prior = resume.priorRuns.get(name)?.shift();
|
|
285
|
+
if (prior && isFastForwardable(prior)) {
|
|
286
|
+
return { shortcut: { name, resumed: 'stored', result: { output: prior.output, route: prior.route, branch: prior.branch ?? parentBranch } } };
|
|
287
|
+
}
|
|
288
|
+
if (prior && !rerunsOnResume(node) && prior.output === undefined && prior.interruptIds.size > 0) {
|
|
289
|
+
const values = [...prior.interruptIds].map((id) => resume.resumeInputs[id]);
|
|
290
|
+
if (values.every((v) => v !== undefined)) {
|
|
291
|
+
return { shortcut: { name, resumed: 'answers', result: { output: values.length === 1 ? values[0] : values, branch: prior.branch ?? parentBranch } } };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const resuming = prior !== undefined && prior.interruptIds.size > 0 && prior.input !== undefined;
|
|
295
|
+
const waited = prior !== undefined && prior.interruptIds.size > 0 && !repeat;
|
|
296
|
+
return {
|
|
297
|
+
...(resuming ? { input: prior.input } : {}),
|
|
298
|
+
resumeInputs: repeat ? {} : resume.resumeInputs,
|
|
299
|
+
...(waited ? { resumedInterruptIds: [...prior.interruptIds] } : {}),
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
// Seed: one trigger per START edge, each on its own branch when there are several.
|
|
303
|
+
const startEdges = graph.edges.filter((e) => e.from === START_NODE);
|
|
304
|
+
for (const edge of startEdges)
|
|
305
|
+
pushTrigger(edge.to, { input: options.input, useSubBranch: startEdges.length > 1 });
|
|
306
|
+
const scheduleReadyNodes = () => {
|
|
307
|
+
// A stopped walk starts nothing; the runs in flight have their signal.
|
|
308
|
+
if (controller.signal.aborted)
|
|
309
|
+
return;
|
|
310
|
+
for (const name of [...triggers.keys()]) {
|
|
311
|
+
if (pending.has(name))
|
|
312
|
+
continue;
|
|
313
|
+
if (nodes.get(name)?.status === 'running')
|
|
314
|
+
continue;
|
|
315
|
+
// ADK: a node waiting on a person is not started again until it is answered.
|
|
316
|
+
if (isWaiting(nodes.get(name)))
|
|
317
|
+
continue;
|
|
318
|
+
if (atConcurrencyLimit())
|
|
319
|
+
break;
|
|
320
|
+
const trigger = popTrigger(name);
|
|
321
|
+
if (!trigger)
|
|
322
|
+
continue;
|
|
323
|
+
const state = { status: 'running', runCounter: nodes.get(name)?.runCounter ?? 0, attempts: { count: 1 }, interrupts: [] };
|
|
324
|
+
nodes.set(name, state);
|
|
325
|
+
const node = nodeOf(name);
|
|
326
|
+
const start = resumeStart(name, node);
|
|
327
|
+
if (start.shortcut) {
|
|
328
|
+
pending.set(name, Promise.resolve(start.shortcut));
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
state.runCounter += 1;
|
|
332
|
+
const runId = String(state.runCounter);
|
|
333
|
+
const branch = trigger.branch !== undefined ? trigger.branch : trigger.useSubBranch ? subBranch(parentBranch, name, runId) : parentBranch;
|
|
334
|
+
const path = `${workflowPath}.${name}`;
|
|
335
|
+
const input = start.input !== undefined ? start.input : trigger.input;
|
|
336
|
+
emit({ type: 'node_start', node: name, kind: node.kind, runId, path, branch, input });
|
|
337
|
+
const ctx = {
|
|
338
|
+
input,
|
|
339
|
+
runId,
|
|
340
|
+
path,
|
|
341
|
+
branch,
|
|
342
|
+
signal: controller.signal,
|
|
343
|
+
...(start.resumeInputs ? { resumeInputs: start.resumeInputs } : {}),
|
|
344
|
+
...(start.resumedInterruptIds ? { resumedInterruptIds: start.resumedInterruptIds } : {}),
|
|
345
|
+
};
|
|
346
|
+
const traced = { name, kind: node.kind, path, runId, attempts: state.attempts };
|
|
347
|
+
const run = walk.traceNode(traced, () => executeNode(node, ctx, walk, state.attempts)).then((result) => ({ name, result: { ...result, branch } }), (error) => ({ name, error }));
|
|
348
|
+
pending.set(name, run);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const bufferDownstreamTriggers = (name, result, branch) => {
|
|
352
|
+
const next = nextNodes(graph, name, result.route);
|
|
353
|
+
const useSubBranch = next.length > 1;
|
|
354
|
+
for (const target of next) {
|
|
355
|
+
if (nodeOf(target).kind !== 'join') {
|
|
356
|
+
pushTrigger(target, { input: result.output, useSubBranch, ...(branch !== undefined ? { branch } : {}) });
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const predecessors = [...new Set(graph.edges.filter((e) => e.to === target).map((e) => e.from))];
|
|
360
|
+
if (!predecessors.every((p) => nodes.get(p)?.status === 'completed'))
|
|
361
|
+
continue;
|
|
362
|
+
const joined = {};
|
|
363
|
+
for (const p of predecessors)
|
|
364
|
+
joined[p] = outputs.get(p);
|
|
365
|
+
const common = commonPrefix(predecessors.map((p) => branches.get(p) ?? ''));
|
|
366
|
+
pushTrigger(target, { input: joined, useSubBranch: false, ...(common ? { branch: common } : {}) });
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
// ADK's reportNodeError: the error a node gave up with, once, unless the
|
|
370
|
+
// node already reported it or the walk was stopped from outside.
|
|
371
|
+
const reportNodeError = (name, error, attempts) => {
|
|
372
|
+
if (isNamed(error, 'InvocationAbortedError') || controller.signal.aborted)
|
|
373
|
+
return;
|
|
374
|
+
if (typeof error === 'object' && error !== null) {
|
|
375
|
+
if (walk.claimed.has(error))
|
|
376
|
+
return;
|
|
377
|
+
walk.claimed.add(error);
|
|
378
|
+
}
|
|
379
|
+
emit({
|
|
380
|
+
type: 'node_error',
|
|
381
|
+
source: 'workflow',
|
|
382
|
+
node: name,
|
|
383
|
+
path: `${workflowPath}.${name}`,
|
|
384
|
+
branch: parentBranch,
|
|
385
|
+
code: errorCodeOf(error),
|
|
386
|
+
message: errorMessageOf(error),
|
|
387
|
+
errorType: errorName(error),
|
|
388
|
+
attempt: attempts,
|
|
389
|
+
});
|
|
390
|
+
};
|
|
391
|
+
/** Record a completed run and emit its end; returns what its successors need. */
|
|
392
|
+
const complete = (name, settledResult, resumed) => {
|
|
393
|
+
const state = nodes.get(name);
|
|
394
|
+
const { branch, ...result } = settledResult;
|
|
395
|
+
state.status = 'completed';
|
|
396
|
+
if (result.output !== undefined)
|
|
397
|
+
outputs.set(name, result.output);
|
|
398
|
+
branches.set(name, branch ?? '');
|
|
399
|
+
order.push(name);
|
|
400
|
+
if (resumed) {
|
|
401
|
+
const route = result.route !== undefined ? { route: result.route } : {};
|
|
402
|
+
emit({ type: 'node_resumed', node: name, kind: nodeOf(name).kind, path: `${workflowPath}.${name}`, branch, output: result.output, ...route, from: resumed });
|
|
403
|
+
return [result, branch];
|
|
404
|
+
}
|
|
405
|
+
emit({
|
|
406
|
+
type: 'node_end',
|
|
407
|
+
node: name,
|
|
408
|
+
kind: nodeOf(name).kind,
|
|
409
|
+
runId: String(state.runCounter),
|
|
410
|
+
path: `${workflowPath}.${name}`,
|
|
411
|
+
branch,
|
|
412
|
+
output: result.output,
|
|
413
|
+
...(result.route !== undefined ? { route: result.route } : {}),
|
|
414
|
+
});
|
|
415
|
+
return [result, branch];
|
|
416
|
+
};
|
|
417
|
+
/** Every open interrupt, in the order the nodes paused (ADK's loop.interruptIds). */
|
|
418
|
+
const interrupted = new Set();
|
|
419
|
+
/** ADK's handleCompletion for a run that paused: the node waits, nothing downstream runs. */
|
|
420
|
+
const wait = (name, settledResult) => {
|
|
421
|
+
const state = nodes.get(name);
|
|
422
|
+
state.status = 'waiting';
|
|
423
|
+
state.interrupts = [...settledResult.interruptIds];
|
|
424
|
+
for (const id of state.interrupts)
|
|
425
|
+
interrupted.add(id);
|
|
426
|
+
emit({
|
|
427
|
+
type: 'node_waiting',
|
|
428
|
+
node: name,
|
|
429
|
+
kind: nodeOf(name).kind,
|
|
430
|
+
runId: String(state.runCounter),
|
|
431
|
+
path: `${workflowPath}.${name}`,
|
|
432
|
+
branch: settledResult.branch,
|
|
433
|
+
interruptIds: [...state.interrupts],
|
|
434
|
+
});
|
|
435
|
+
};
|
|
436
|
+
const paused = (result) => (result.interruptIds?.length ?? 0) > 0;
|
|
437
|
+
try {
|
|
438
|
+
for (;;) {
|
|
439
|
+
scheduleReadyNodes();
|
|
440
|
+
if (pending.size === 0)
|
|
441
|
+
break;
|
|
442
|
+
const settled = await Promise.race(pending.values());
|
|
443
|
+
pending.delete(settled.name);
|
|
444
|
+
const state = nodes.get(settled.name);
|
|
445
|
+
if (!('result' in settled)) {
|
|
446
|
+
state.status = 'failed';
|
|
447
|
+
reportNodeError(settled.name, settled.error, state.attempts.count);
|
|
448
|
+
controller.abort(settled.error);
|
|
449
|
+
const outstanding = [...pending.values()];
|
|
450
|
+
pending.clear();
|
|
451
|
+
// A run that still finishes during the shutdown has written its output
|
|
452
|
+
// on ADK (its node emits it), so it ends here too, in settle order,
|
|
453
|
+
// and triggers nothing.
|
|
454
|
+
await Promise.all(outstanding.map((run) => run.then((late) => ('result' in late ? (paused(late.result) ? wait(late.name, late.result) : complete(late.name, late.result, late.resumed)) : undefined))));
|
|
455
|
+
throw settled.error;
|
|
456
|
+
}
|
|
457
|
+
if (paused(settled.result))
|
|
458
|
+
wait(settled.name, settled.result);
|
|
459
|
+
else
|
|
460
|
+
bufferDownstreamTriggers(settled.name, ...complete(settled.name, settled.result, settled.resumed));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
finally {
|
|
464
|
+
parentSignal?.removeEventListener('abort', onParentAbort);
|
|
465
|
+
}
|
|
466
|
+
// Stopped from outside: the nodes in flight finished, but the walk did not.
|
|
467
|
+
if (stopped)
|
|
468
|
+
throw new InvocationAbortedError(`Workflow ${graph.name} aborted.`, { cause: controller.signal.reason });
|
|
469
|
+
// ADK's collectRemainingInterrupts and finalize: a paused walk has no output.
|
|
470
|
+
const interruptIds = [...interrupted];
|
|
471
|
+
if (interruptIds.length > 0)
|
|
472
|
+
return { output: undefined, outputs, order, nodeErrors, interruptIds };
|
|
473
|
+
const terminalOutputs = graph.terminals.filter((n) => outputs.has(n)).map((n) => outputs.get(n));
|
|
474
|
+
if (terminalOutputs.length > 1) {
|
|
475
|
+
throw new Error(`Workflow ${graph.name}: multiple terminal nodes produced output (${terminalOutputs.length}). A workflow must have at most one terminal output.`);
|
|
476
|
+
}
|
|
477
|
+
return { output: terminalOutputs[0], outputs, order, nodeErrors, interruptIds: [] };
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* The event ADK stores for a `node_error` the walk reported (source
|
|
481
|
+
* `workflow`): ADK's `createNodeErrorEvent`, field for field and in its key
|
|
482
|
+
* order, so a turn runner can write it where ADK writes it. An error a node
|
|
483
|
+
* reported itself (source `node`) is already on the event its runner wrote.
|
|
484
|
+
*/
|
|
485
|
+
export function nodeErrorEvent(event, invocationId) {
|
|
486
|
+
return {
|
|
487
|
+
...createTurnEvent({ author: event.node, invocationId, nodeInfo: { path: event.path }, branch: event.branch }),
|
|
488
|
+
isNodeError: true,
|
|
489
|
+
errorType: event.errorType ?? 'Error',
|
|
490
|
+
errorCode: event.code,
|
|
491
|
+
errorMessage: event.message,
|
|
492
|
+
attemptCount: event.attempt,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
/** ADK's errorCodeOf: an error's `code` when it is a string or a number, else UNKNOWN_ERROR. */
|
|
496
|
+
function errorCodeOf(error) {
|
|
497
|
+
const code = error?.code;
|
|
498
|
+
return typeof code === 'string' || typeof code === 'number' ? String(code) : 'UNKNOWN_ERROR';
|
|
499
|
+
}
|
|
500
|
+
function errorMessageOf(error) {
|
|
501
|
+
return error instanceof Error ? error.message : String(error);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* The walk as a stream: every event as it happens, then the run as the
|
|
505
|
+
* generator's return value. The walk does not wait for the consumer, so a
|
|
506
|
+
* slow reader never changes the order.
|
|
507
|
+
*/
|
|
508
|
+
export async function* streamWorkflowGraph(graph, options) {
|
|
509
|
+
const queue = [];
|
|
510
|
+
let wake;
|
|
511
|
+
let finished = false;
|
|
512
|
+
const done = runWorkflowGraph(graph, {
|
|
513
|
+
...options,
|
|
514
|
+
onEvent: (event) => {
|
|
515
|
+
queue.push(event);
|
|
516
|
+
wake?.();
|
|
517
|
+
},
|
|
518
|
+
}).finally(() => {
|
|
519
|
+
finished = true;
|
|
520
|
+
wake?.();
|
|
521
|
+
});
|
|
522
|
+
done.catch(() => { }); // surfaced below, after the queued events
|
|
523
|
+
for (;;) {
|
|
524
|
+
while (queue.length > 0)
|
|
525
|
+
yield queue.shift();
|
|
526
|
+
if (finished)
|
|
527
|
+
return await done;
|
|
528
|
+
await new Promise((resolve) => (wake = resolve));
|
|
529
|
+
wake = undefined;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* The successors a completed node triggers, as ADK's `getNextPendingNodes`
|
|
534
|
+
* selects them: every `always` edge; every keyed edge whose key (in ADK's
|
|
535
|
+
* spelling) equals the emitted route, or one of them, compared as strings;
|
|
536
|
+
* and the `default` edge when no keyed edge matched.
|
|
537
|
+
*/
|
|
538
|
+
export function nextNodes(graph, name, route) {
|
|
539
|
+
const next = [];
|
|
540
|
+
const emitted = route === undefined || route === null ? [] : (Array.isArray(route) ? route : [route]).map(String);
|
|
541
|
+
let matched = false;
|
|
542
|
+
let fallback;
|
|
543
|
+
for (const edge of graph.edges) {
|
|
544
|
+
if (edge.from !== name)
|
|
545
|
+
continue;
|
|
546
|
+
if (edge.route.kind === 'always') {
|
|
547
|
+
next.push(edge.to);
|
|
548
|
+
}
|
|
549
|
+
else if (edge.route.kind === 'default') {
|
|
550
|
+
fallback = edge.to;
|
|
551
|
+
}
|
|
552
|
+
else if (emitted.includes(adkRouteString(edge.route))) {
|
|
553
|
+
next.push(edge.to);
|
|
554
|
+
matched = true;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (!matched && fallback !== undefined)
|
|
558
|
+
next.push(fallback);
|
|
559
|
+
return next;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* The modifiers a node runs under. A map's own are not applied (ADK's compile
|
|
563
|
+
* does not hand them to its ParallelWorker); its items run under the agent's.
|
|
564
|
+
*/
|
|
565
|
+
function settingsFor(node) {
|
|
566
|
+
switch (node.kind) {
|
|
567
|
+
case 'agent':
|
|
568
|
+
case 'tool':
|
|
569
|
+
case 'ask_user':
|
|
570
|
+
case 'join':
|
|
571
|
+
return node.settings;
|
|
572
|
+
default:
|
|
573
|
+
return {};
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Runs one node: the single place a node executes, under its retry and
|
|
578
|
+
* timeout. Always asynchronous, so a node the scheduler runs itself settles a
|
|
579
|
+
* tick later as one the runner runs does.
|
|
580
|
+
*/
|
|
581
|
+
async function executeNode(node, ctx, walk, attempts) {
|
|
582
|
+
return withControls(node.name, settingsFor(node), ctx, walk, attempts, (signal, attempt) => runOnce(node, { ...ctx, signal }, attempt, walk));
|
|
583
|
+
}
|
|
584
|
+
/** One attempt of a node, without its controls. */
|
|
585
|
+
async function runOnce(node, ctx, attempt, walk) {
|
|
586
|
+
switch (node.kind) {
|
|
587
|
+
case 'agent':
|
|
588
|
+
case 'tool':
|
|
589
|
+
case 'ask_user':
|
|
590
|
+
return await walk.runNode({ target: node, ...ctx, attempt });
|
|
591
|
+
case 'join':
|
|
592
|
+
return { output: ctx.input };
|
|
593
|
+
case 'route':
|
|
594
|
+
return { output: ctx.input, route: routeOf(ctx.input, node.routeKey) };
|
|
595
|
+
case 'map':
|
|
596
|
+
return { output: await runMap(node, ctx, walk) };
|
|
597
|
+
case 'start':
|
|
598
|
+
throw new Error(`Node ${node.name} is the start node and never runs.`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* ADK's runChildNode loop around one run: each attempt under the timeout; a
|
|
603
|
+
* reported error emitted, and fatal when the attempt produced nothing; a
|
|
604
|
+
* failure retried while the retry allows, after the backoff. An abort and a
|
|
605
|
+
* map item's failure are thrown at once. `attempts` is the run's counter,
|
|
606
|
+
* which the walk reads when it reports the error the node gave up with.
|
|
607
|
+
*/
|
|
608
|
+
async function withControls(name, settings, ctx, walk, attempts, attemptOnce) {
|
|
609
|
+
for (;;) {
|
|
610
|
+
try {
|
|
611
|
+
const { error, ...result } = await underTimeout(name, settings.timeout, ctx.signal, (signal) => attemptOnce(signal, attempts.count));
|
|
612
|
+
// ADK carries no output for a node that yields null: no output event,
|
|
613
|
+
// nothing recorded, and its successors run on undefined.
|
|
614
|
+
if (result.output === null)
|
|
615
|
+
delete result.output;
|
|
616
|
+
if (error) {
|
|
617
|
+
walk.emit({ type: 'node_error', source: 'node', node: name, path: ctx.path, branch: ctx.branch, code: String(error.code), message: String(error.message), attempt: attempts.count });
|
|
618
|
+
// ADK's failIfNodeReportedError: an output, a route or an interrupt keeps the node alive.
|
|
619
|
+
if (result.output === undefined && result.route === undefined && !(result.interruptIds?.length)) {
|
|
620
|
+
const failure = new NodeReportedError({ nodeName: name, errorCode: error.code, errorMessage: error.message });
|
|
621
|
+
walk.claimed.add(failure);
|
|
622
|
+
throw failure;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return result;
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
if (isNamed(error, 'InvocationAbortedError') || isNamed(error, 'DynamicNodeFailError'))
|
|
629
|
+
throw error;
|
|
630
|
+
const retry = settings.retry;
|
|
631
|
+
if (!retry || !shouldRetry(error, retry, attempts.count))
|
|
632
|
+
throw error;
|
|
633
|
+
const seconds = retryDelaySeconds(retry, attempts.count);
|
|
634
|
+
attempts.count += 1;
|
|
635
|
+
await abortableDelay(seconds * 1000, ctx.signal);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* One attempt under a timeout, as ADK's runOnce: the attempt's own signal
|
|
641
|
+
* follows the walk's and aborts when the timer fires, and the attempt fails
|
|
642
|
+
* at once (`NodeTimeoutError`, or `InvocationAbortedError` on the walk's
|
|
643
|
+
* abort) without waiting for the runner. Without a timeout the attempt is
|
|
644
|
+
* awaited with the walk's signal.
|
|
645
|
+
*/
|
|
646
|
+
async function underTimeout(name, timeout, signal, attempt) {
|
|
647
|
+
if (!(typeof timeout === 'number' && timeout > 0))
|
|
648
|
+
return await attempt(signal);
|
|
649
|
+
const controller = new AbortController();
|
|
650
|
+
let deadlineFired = false;
|
|
651
|
+
const onParentAbort = () => controller.abort(signal.reason);
|
|
652
|
+
if (signal.aborted)
|
|
653
|
+
controller.abort(signal.reason);
|
|
654
|
+
else
|
|
655
|
+
signal.addEventListener('abort', onParentAbort, { once: true });
|
|
656
|
+
const timer = setTimeout(() => {
|
|
657
|
+
deadlineFired = true;
|
|
658
|
+
controller.abort(new NodeTimeoutError({ nodeName: name, timeout }));
|
|
659
|
+
}, timeout * 1000);
|
|
660
|
+
// Registered before the runner starts, so it settles the race first.
|
|
661
|
+
const aborted = new Promise((_, reject) => {
|
|
662
|
+
const fail = () => reject(deadlineFired ? new NodeTimeoutError({ nodeName: name, timeout }) : new InvocationAbortedError(`Invocation aborted while running node '${name}'.`, { cause: signal.reason }));
|
|
663
|
+
if (controller.signal.aborted)
|
|
664
|
+
fail();
|
|
665
|
+
else
|
|
666
|
+
controller.signal.addEventListener('abort', fail, { once: true });
|
|
667
|
+
});
|
|
668
|
+
aborted.catch(() => { });
|
|
669
|
+
const run = Promise.resolve().then(() => attempt(controller.signal));
|
|
670
|
+
run.catch(() => { }); // an abandoned attempt's late failure is nobody's
|
|
671
|
+
try {
|
|
672
|
+
return await Promise.race([run, aborted]);
|
|
673
|
+
}
|
|
674
|
+
finally {
|
|
675
|
+
clearTimeout(timer);
|
|
676
|
+
signal.removeEventListener('abort', onParentAbort);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/** A retry's backoff; the walk's abort cuts it short. */
|
|
680
|
+
function abortableDelay(ms, signal) {
|
|
681
|
+
return new Promise((resolve, reject) => {
|
|
682
|
+
if (signal.aborted) {
|
|
683
|
+
reject(new InvocationAbortedError('Invocation aborted during retry.', { cause: signal.reason }));
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const onAbort = () => {
|
|
687
|
+
clearTimeout(timer);
|
|
688
|
+
reject(new InvocationAbortedError('Invocation aborted during retry.', { cause: signal.reason }));
|
|
689
|
+
};
|
|
690
|
+
const timer = setTimeout(() => {
|
|
691
|
+
signal.removeEventListener('abort', onAbort);
|
|
692
|
+
resolve();
|
|
693
|
+
}, ms);
|
|
694
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* A map: ADK's ParallelWorker. A list input runs the agent once per item (a
|
|
699
|
+
* non-list input is one item), on a pool of `min(maxParallel ?? 8, items)`
|
|
700
|
+
* workers that each take the next index; the output is the list of results
|
|
701
|
+
* by index. An empty list outputs `[]`. Each item runs under the agent's own
|
|
702
|
+
* retry and timeout. The first item that gives up stops the pool taking new
|
|
703
|
+
* items, and once every worker has stopped the map fails with
|
|
704
|
+
* `DynamicNodeFailError` naming the agent, as ADK's dynamic-node scheduler
|
|
705
|
+
* wraps it. A map whose signal aborted outputs nothing, as ADK's yields
|
|
706
|
+
* nothing then.
|
|
707
|
+
*/
|
|
708
|
+
async function runMap(node, ctx, walk) {
|
|
709
|
+
const items = Array.isArray(ctx.input) ? ctx.input : [ctx.input];
|
|
710
|
+
if (items.length === 0)
|
|
711
|
+
return [];
|
|
712
|
+
const results = new Array(items.length);
|
|
713
|
+
const poolSize = Math.min(node.maxParallel ?? DEFAULT_MAX_PARALLEL, items.length);
|
|
714
|
+
let nextIndex = 0;
|
|
715
|
+
let failed = false;
|
|
716
|
+
let firstError;
|
|
717
|
+
const worker = async () => {
|
|
718
|
+
while (!failed && !ctx.signal.aborted) {
|
|
719
|
+
const index = nextIndex++;
|
|
720
|
+
if (index >= items.length)
|
|
721
|
+
break;
|
|
722
|
+
const path = `${ctx.path}.${node.agent}@${index}`;
|
|
723
|
+
const branch = subBranch(ctx.branch, node.agent, String(index));
|
|
724
|
+
const base = { node: node.name, agent: node.agent, index, path, branch };
|
|
725
|
+
try {
|
|
726
|
+
walk.emit({ type: 'item_start', ...base, input: items[index] });
|
|
727
|
+
const itemCtx = { input: items[index], runId: String(index), path, branch, signal: ctx.signal };
|
|
728
|
+
const target = { kind: 'map_item', agent: node.agent, map: node, index };
|
|
729
|
+
const attempts = { count: 1 };
|
|
730
|
+
const traced = { name: node.agent, kind: 'map_item', path, runId: String(index), attempts };
|
|
731
|
+
const result = await walk.traceNode(traced, () => withControls(node.agent, node.agentSettings, itemCtx, walk, attempts, async (signal, attempt) => walk.runNode({ target, ...itemCtx, signal, attempt }))).catch((error) => {
|
|
732
|
+
if (isNamed(error, 'DynamicNodeFailError'))
|
|
733
|
+
throw error;
|
|
734
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
735
|
+
throw new DynamicNodeFailError({ message: `Dynamic node ${node.agent} failed: ${cause.message}`, error: cause, errorNodePath: path });
|
|
736
|
+
});
|
|
737
|
+
results[index] = result.output;
|
|
738
|
+
walk.emit({ type: 'item_end', ...base, output: result.output });
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
if (!failed) {
|
|
742
|
+
failed = true;
|
|
743
|
+
firstError = error;
|
|
744
|
+
}
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
await Promise.all(Array.from({ length: poolSize }, () => worker()));
|
|
750
|
+
if (failed)
|
|
751
|
+
throw firstError;
|
|
752
|
+
// ADK's ParallelWorker yields nothing once aborted: no output, so no map event.
|
|
753
|
+
if (ctx.signal.aborted)
|
|
754
|
+
return undefined;
|
|
755
|
+
return results;
|
|
756
|
+
}
|