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.
Files changed (293) hide show
  1. package/.env.example +12 -0
  2. package/CHANGELOG.md +1001 -0
  3. package/DOCUMENTATION.md +252 -65
  4. package/QUICKSTART.md +9 -6
  5. package/README.md +42 -4
  6. package/config/agents/examples/harness.yaml +3 -1
  7. package/config/agents/examples/model_zoo.yaml +10 -9
  8. package/config/agents/syndicateSchema.yaml +53 -16
  9. package/config/agents/templates/account_memory.yaml +2 -6
  10. package/config/agents/templates/case_desk.yaml +3 -9
  11. package/config/agents/templates/draft_review.yaml +3 -9
  12. package/config/agents/templates/intake_extractor.yaml +1 -3
  13. package/config/agents/templates/research_brief.yaml +3 -9
  14. package/config/agents/templates/research_desk.yaml +3 -9
  15. package/config/agents/templates/review_panel.yaml +4 -12
  16. package/config/agents/templates/support_triage.yaml +4 -12
  17. package/config/agents/templates/systems_operator.yaml +2 -6
  18. package/db/hardening.sql +7 -1
  19. package/db/migrations/0013_tool_credentials.sql +260 -0
  20. package/dist/lib/a2a/app.d.ts +70 -6
  21. package/dist/lib/a2a/app.js +107 -2
  22. package/dist/lib/a2a/executor.d.ts +20 -3
  23. package/dist/lib/a2a/executor.js +66 -7
  24. package/dist/lib/a2a/remoteAgent.d.ts +24 -9
  25. package/dist/lib/a2a/remoteAgent.js +31 -14
  26. package/dist/lib/adkPeer.d.ts +63 -0
  27. package/dist/lib/adkPeer.js +240 -0
  28. package/dist/lib/compile.d.ts +159 -30
  29. package/dist/lib/compile.js +215 -134
  30. package/dist/lib/compileAdk.d.ts +32 -0
  31. package/dist/lib/compileAdk.js +142 -0
  32. package/dist/lib/compileNative.d.ts +111 -0
  33. package/dist/lib/compileNative.js +278 -0
  34. package/dist/lib/config.d.ts +6 -5
  35. package/dist/lib/config.js +6 -5
  36. package/dist/lib/doctor.d.ts +28 -1
  37. package/dist/lib/doctor.js +64 -2
  38. package/dist/lib/index.d.ts +6 -1
  39. package/dist/lib/index.js +7 -1
  40. package/dist/lib/loadSyndicate.d.ts +18 -0
  41. package/dist/lib/memory/erase.d.ts +5 -3
  42. package/dist/lib/memory/erase.js +5 -2
  43. package/dist/lib/memory/namespace.d.ts +8 -1
  44. package/dist/lib/memory/namespace.js +12 -6
  45. package/dist/lib/memory/providers.js +2 -1
  46. package/dist/lib/memory/supabaseMemoryService.d.ts +37 -3
  47. package/dist/lib/memory/supabaseMemoryService.js +42 -2
  48. package/dist/lib/model.d.ts +47 -0
  49. package/dist/lib/model.js +37 -0
  50. package/dist/lib/models/adapterResolver.d.ts +115 -0
  51. package/dist/lib/models/adapterResolver.js +137 -0
  52. package/dist/lib/models/adkGeminiAdapter.d.ts +90 -0
  53. package/dist/lib/models/adkGeminiAdapter.js +369 -0
  54. package/dist/lib/models/adkShim.d.ts +111 -0
  55. package/dist/lib/models/adkShim.js +136 -0
  56. package/dist/lib/models/capabilities.d.ts +3 -0
  57. package/dist/lib/models/capabilities.js +33 -23
  58. package/dist/lib/models/chatCompletionsAdapter.d.ts +279 -0
  59. package/dist/lib/models/chatCompletionsAdapter.js +854 -0
  60. package/dist/lib/models/circuitBreaker.d.ts +42 -0
  61. package/dist/lib/models/circuitBreaker.js +70 -0
  62. package/dist/lib/models/claudeAdapter.d.ts +103 -0
  63. package/dist/lib/models/claudeAdapter.js +669 -0
  64. package/dist/lib/models/claudeLlm.d.ts +39 -62
  65. package/dist/lib/models/claudeLlm.js +45 -350
  66. package/dist/lib/models/claudeModels.d.ts +127 -0
  67. package/dist/lib/models/claudeModels.js +237 -0
  68. package/dist/lib/models/contract.d.ts +395 -0
  69. package/dist/lib/models/contract.js +39 -0
  70. package/dist/lib/models/endpoints.js +2 -1
  71. package/dist/lib/models/errorResponse.d.ts +50 -0
  72. package/dist/lib/models/errorResponse.js +48 -0
  73. package/dist/lib/models/fallback.d.ts +18 -11
  74. package/dist/lib/models/fallback.js +54 -43
  75. package/dist/lib/models/fallbackAdapter.d.ts +55 -0
  76. package/dist/lib/models/fallbackAdapter.js +62 -0
  77. package/dist/lib/models/gateway.js +2 -1
  78. package/dist/lib/models/gatewayAdapter.d.ts +51 -0
  79. package/dist/lib/models/gatewayAdapter.js +81 -0
  80. package/dist/lib/models/gatewayLlm.d.ts +12 -33
  81. package/dist/lib/models/gatewayLlm.js +16 -77
  82. package/dist/lib/models/geminiAdapter.d.ts +125 -0
  83. package/dist/lib/models/geminiAdapter.js +854 -0
  84. package/dist/lib/models/geminiState.d.ts +46 -0
  85. package/dist/lib/models/geminiState.js +51 -0
  86. package/dist/lib/models/genaiMapping.d.ts +283 -0
  87. package/dist/lib/models/genaiMapping.js +965 -0
  88. package/dist/lib/models/gptAdapter.d.ts +240 -0
  89. package/dist/lib/models/gptAdapter.js +804 -0
  90. package/dist/lib/models/gptLlm.d.ts +63 -123
  91. package/dist/lib/models/gptLlm.js +89 -603
  92. package/dist/lib/models/grokAdapter.d.ts +86 -0
  93. package/dist/lib/models/grokAdapter.js +142 -0
  94. package/dist/lib/models/grokLlm.d.ts +22 -45
  95. package/dist/lib/models/grokLlm.js +24 -69
  96. package/dist/lib/models/kimiAdapter.d.ts +157 -0
  97. package/dist/lib/models/kimiAdapter.js +203 -0
  98. package/dist/lib/models/kimiLlm.d.ts +15 -107
  99. package/dist/lib/models/kimiLlm.js +19 -155
  100. package/dist/lib/models/ollamaAdapter.d.ts +125 -0
  101. package/dist/lib/models/ollamaAdapter.js +155 -0
  102. package/dist/lib/models/ollamaLlm.d.ts +11 -107
  103. package/dist/lib/models/ollamaLlm.js +15 -153
  104. package/dist/lib/models/openAiCompatibleLlm.d.ts +73 -216
  105. package/dist/lib/models/openAiCompatibleLlm.js +109 -671
  106. package/dist/lib/models/providerState.d.ts +63 -0
  107. package/dist/lib/models/providerState.js +61 -0
  108. package/dist/lib/models/reasoning.d.ts +26 -0
  109. package/dist/lib/models/reasoning.js +59 -0
  110. package/dist/lib/models/registry.d.ts +62 -35
  111. package/dist/lib/models/registry.js +100 -124
  112. package/dist/lib/models/retry.d.ts +2 -1
  113. package/dist/lib/models/retry.js +3 -2
  114. package/dist/lib/models/schemaNormalize.d.ts +98 -0
  115. package/dist/lib/models/schemaNormalize.js +321 -1
  116. package/dist/lib/models/tracedGemini.d.ts +41 -0
  117. package/dist/lib/models/tracedGemini.js +98 -0
  118. package/dist/lib/models/urls.d.ts +11 -0
  119. package/dist/lib/models/urls.js +16 -0
  120. package/dist/lib/net/redirects.d.ts +21 -2
  121. package/dist/lib/net/redirects.js +11 -34
  122. package/dist/lib/observability/audit.d.ts +5 -1
  123. package/dist/lib/observability/lineage.d.ts +14 -0
  124. package/dist/lib/observability/lineage.js +33 -0
  125. package/dist/lib/observability/supabaseSpanExporter.d.ts +10 -4
  126. package/dist/lib/observability/supabaseSpanExporter.js +17 -9
  127. package/dist/lib/observability/tracer.d.ts +26 -8
  128. package/dist/lib/observability/tracer.js +43 -18
  129. package/dist/lib/runtime/adkMemoryBridge.d.ts +46 -0
  130. package/dist/lib/runtime/adkMemoryBridge.js +68 -0
  131. package/dist/lib/runtime/adkSessionBridge.d.ts +78 -0
  132. package/dist/lib/runtime/adkSessionBridge.js +176 -0
  133. package/dist/lib/runtime/approvals.d.ts +5 -0
  134. package/dist/lib/runtime/approvals.js +39 -8
  135. package/dist/lib/runtime/credentials.d.ts +61 -0
  136. package/dist/lib/runtime/credentials.js +67 -0
  137. package/dist/lib/runtime/events.d.ts +229 -0
  138. package/dist/lib/runtime/events.js +274 -0
  139. package/dist/lib/runtime/logging.d.ts +32 -0
  140. package/dist/lib/runtime/logging.js +55 -0
  141. package/dist/lib/runtime/memoryService.d.ts +72 -0
  142. package/dist/lib/runtime/memoryService.js +25 -0
  143. package/dist/lib/runtime/native/agentLoop.d.ts +209 -0
  144. package/dist/lib/runtime/native/agentLoop.js +705 -0
  145. package/dist/lib/runtime/native/compaction.d.ts +103 -0
  146. package/dist/lib/runtime/native/compaction.js +191 -0
  147. package/dist/lib/runtime/native/delegate.d.ts +151 -0
  148. package/dist/lib/runtime/native/delegate.js +256 -0
  149. package/dist/lib/runtime/native/history.d.ts +83 -0
  150. package/dist/lib/runtime/native/history.js +312 -0
  151. package/dist/lib/runtime/native/interrupts.d.ts +129 -0
  152. package/dist/lib/runtime/native/interrupts.js +310 -0
  153. package/dist/lib/runtime/native/request.d.ts +215 -0
  154. package/dist/lib/runtime/native/request.js +585 -0
  155. package/dist/lib/runtime/native/selfCorrection.d.ts +164 -0
  156. package/dist/lib/runtime/native/selfCorrection.js +327 -0
  157. package/dist/lib/runtime/native/step.d.ts +156 -0
  158. package/dist/lib/runtime/native/step.js +263 -0
  159. package/dist/lib/runtime/native/taskMode.d.ts +64 -0
  160. package/dist/lib/runtime/native/taskMode.js +126 -0
  161. package/dist/lib/runtime/native/telemetry.d.ts +75 -0
  162. package/dist/lib/runtime/native/telemetry.js +299 -0
  163. package/dist/lib/runtime/native/tempState.d.ts +31 -0
  164. package/dist/lib/runtime/native/tempState.js +56 -0
  165. package/dist/lib/runtime/nativeTurn.d.ts +86 -0
  166. package/dist/lib/runtime/nativeTurn.js +107 -0
  167. package/dist/lib/runtime/questions.d.ts +29 -7
  168. package/dist/lib/runtime/questions.js +47 -32
  169. package/dist/lib/runtime/runtimeFlag.d.ts +38 -0
  170. package/dist/lib/runtime/runtimeFlag.js +58 -0
  171. package/dist/lib/runtime/sessions.d.ts +166 -0
  172. package/dist/lib/runtime/sessions.js +201 -0
  173. package/dist/lib/runtime/syndicateTurn.d.ts +46 -5
  174. package/dist/lib/runtime/syndicateTurn.js +233 -36
  175. package/dist/lib/runtime/valueDepth.d.ts +23 -0
  176. package/dist/lib/runtime/valueDepth.js +41 -0
  177. package/dist/lib/session/supabaseSessionService.d.ts +65 -18
  178. package/dist/lib/session/supabaseSessionService.js +148 -105
  179. package/dist/lib/session/transcript.d.ts +69 -23
  180. package/dist/lib/session/transcript.js +105 -29
  181. package/dist/lib/skills.js +33 -7
  182. package/dist/lib/storage/postgres/credentialStore.d.ts +20 -0
  183. package/dist/lib/storage/postgres/credentialStore.js +82 -0
  184. package/dist/lib/storage/postgres/index.d.ts +12 -0
  185. package/dist/lib/storage/postgres/index.js +6 -1
  186. package/dist/lib/storage/postgres/sessionService.d.ts +44 -16
  187. package/dist/lib/storage/postgres/sessionService.js +128 -66
  188. package/dist/lib/syndicateSchema.d.ts +49 -0
  189. package/dist/lib/syndicateSchema.js +99 -9
  190. package/dist/lib/toolRegistry.d.ts +26 -2
  191. package/dist/lib/toolRegistry.js +73 -17
  192. package/dist/lib/tools/adkTool.d.ts +81 -0
  193. package/dist/lib/tools/adkTool.js +268 -0
  194. package/dist/lib/tools/auth.d.ts +118 -0
  195. package/dist/lib/tools/auth.js +89 -0
  196. package/dist/lib/tools/collectionsSearchTool.d.ts +8 -7
  197. package/dist/lib/tools/collectionsSearchTool.js +12 -27
  198. package/dist/lib/tools/credentialCipher.d.ts +58 -0
  199. package/dist/lib/tools/credentialCipher.js +116 -0
  200. package/dist/lib/tools/credentialStore.d.ts +77 -0
  201. package/dist/lib/tools/credentialStore.js +246 -0
  202. package/dist/lib/tools/examples.d.ts +43 -0
  203. package/dist/lib/tools/examples.js +62 -0
  204. package/dist/lib/tools/generateImageTool.d.ts +15 -6
  205. package/dist/lib/tools/generateImageTool.js +19 -29
  206. package/dist/lib/tools/inspectImageTool.d.ts +9 -6
  207. package/dist/lib/tools/inspectImageTool.js +13 -17
  208. package/dist/lib/tools/mcpToolFactory.d.ts +42 -1
  209. package/dist/lib/tools/mcpToolFactory.js +46 -47
  210. package/dist/lib/tools/memoryTools.d.ts +61 -0
  211. package/dist/lib/tools/memoryTools.js +119 -0
  212. package/dist/lib/tools/nativeTools.d.ts +38 -0
  213. package/dist/lib/tools/nativeTools.js +43 -0
  214. package/dist/lib/tools/oauthConsent.d.ts +181 -0
  215. package/dist/lib/tools/oauthConsent.js +330 -0
  216. package/dist/lib/tools/openapi/call.d.ts +108 -0
  217. package/dist/lib/tools/openapi/call.js +295 -0
  218. package/dist/lib/tools/openapi/parse.d.ts +147 -0
  219. package/dist/lib/tools/openapi/parse.js +570 -0
  220. package/dist/lib/tools/openapiTools.d.ts +25 -17
  221. package/dist/lib/tools/openapiTools.js +124 -121
  222. package/dist/lib/tools/scienceTools.d.ts +14 -14
  223. package/dist/lib/tools/skillToolset.d.ts +58 -87
  224. package/dist/lib/tools/skillToolset.js +61 -142
  225. package/dist/lib/tools/skills/env.d.ts +58 -0
  226. package/dist/lib/tools/skills/env.js +165 -0
  227. package/dist/lib/tools/skills/executor.d.ts +114 -0
  228. package/dist/lib/tools/skills/executor.js +331 -0
  229. package/dist/lib/tools/skills/frontmatter.d.ts +79 -0
  230. package/dist/lib/tools/skills/frontmatter.js +215 -0
  231. package/dist/lib/tools/skills/loader.d.ts +91 -0
  232. package/dist/lib/tools/skills/loader.js +257 -0
  233. package/dist/lib/tools/skills/tools.d.ts +187 -0
  234. package/dist/lib/tools/skills/tools.js +386 -0
  235. package/dist/lib/tools/taskTools.d.ts +15 -15
  236. package/dist/lib/tools/tool.d.ts +354 -0
  237. package/dist/lib/tools/tool.js +304 -0
  238. package/dist/lib/tools/toolContract.d.ts +89 -33
  239. package/dist/lib/tools/toolContract.js +104 -82
  240. package/dist/lib/tools/urlContextTool.d.ts +4 -1
  241. package/dist/lib/tools/urlContextTool.js +6 -2
  242. package/dist/lib/tools/webExtractTool.d.ts +3 -3
  243. package/dist/lib/tools/webExtractTool.js +2 -1
  244. package/dist/lib/tools/webSearchTool.d.ts +7 -10
  245. package/dist/lib/tools/webSearchTool.js +15 -60
  246. package/dist/lib/tools/wikiTools.d.ts +19 -19
  247. package/dist/lib/tools/wikiTools.js +13 -9
  248. package/dist/lib/tools/xApiSearchTool.d.ts +3 -3
  249. package/dist/lib/tools/xApiSearchTool.js +2 -1
  250. package/dist/lib/tools/xSearchTool.d.ts +8 -10
  251. package/dist/lib/tools/xSearchTool.js +12 -63
  252. package/dist/lib/tools/xaiSearchParams.d.ts +41 -0
  253. package/dist/lib/tools/xaiSearchParams.js +126 -0
  254. package/dist/lib/wiki/agentRun.d.ts +12 -0
  255. package/dist/lib/wiki/agentRun.js +74 -35
  256. package/dist/lib/wiki/entities.d.ts +12 -0
  257. package/dist/lib/wiki/entities.js +45 -3
  258. package/dist/lib/workflow/agentNode.d.ts +153 -0
  259. package/dist/lib/workflow/agentNode.js +365 -0
  260. package/dist/lib/workflow/graph.d.ts +178 -0
  261. package/dist/lib/workflow/graph.js +468 -0
  262. package/dist/lib/workflow/nodeEvents.d.ts +50 -0
  263. package/dist/lib/workflow/nodeEvents.js +101 -0
  264. package/dist/lib/workflow/pause.d.ts +122 -0
  265. package/dist/lib/workflow/pause.js +180 -0
  266. package/dist/lib/workflow/resume.d.ts +166 -0
  267. package/dist/lib/workflow/resume.js +441 -0
  268. package/dist/lib/workflow/route.d.ts +58 -0
  269. package/dist/lib/workflow/route.js +69 -0
  270. package/dist/lib/workflow/scheduler.d.ts +418 -0
  271. package/dist/lib/workflow/scheduler.js +756 -0
  272. package/dist/lib/workflow/toolNode.d.ts +127 -0
  273. package/dist/lib/workflow/toolNode.js +236 -0
  274. package/dist/lib/workflow/turn.d.ts +118 -0
  275. package/dist/lib/workflow/turn.js +216 -0
  276. package/dist/lib/workflow.d.ts +33 -11
  277. package/dist/lib/workflow.js +49 -27
  278. package/dist/lib/workflowConfig.d.ts +7 -2
  279. package/dist/lib/workflowConfig.js +7 -12
  280. package/dist/scripts/a2a_server.js +4 -3
  281. package/dist/scripts/assistant_worker.d.ts +2 -1
  282. package/dist/scripts/assistant_worker.js +9 -4
  283. package/dist/scripts/doctor.d.ts +3 -1
  284. package/dist/scripts/doctor.js +4 -2
  285. package/dist/scripts/syndicate_chat.js +19 -10
  286. package/package.json +9 -3
  287. package/skills/briefs/melchizedek-author.md +3 -2
  288. package/skills/briefs/melchizedek-models.md +13 -9
  289. package/skills/briefs/melchizedek.md +3 -3
  290. package/skills/melchizedek/SKILL.md +2 -1
  291. package/skills/melchizedek-author/SKILL.md +2 -1
  292. package/skills/melchizedek-author/assets/minimal.yaml +2 -0
  293. package/skills/melchizedek-models/SKILL.md +23 -6
@@ -0,0 +1,178 @@
1
+ /**
2
+ * lib/workflow/graph.ts — the `workflow:` block as an engine-owned graph.
3
+ *
4
+ * The YAML (lib/workflowConfig.ts) says what runs after what in chains of
5
+ * names; this module turns those chains into the graph a scheduler runs:
6
+ * typed nodes (`start`, `agent`, `join`, `map`, `tool`, `ask_user`, and the
7
+ * hidden `route` step after a node a routing map follows), directed edges
8
+ * that are unconditional, keyed to a route, or the `default` route, and the
9
+ * workflow's `max_concurrency`. It imports nothing from ADK.
10
+ *
11
+ * It is the same graph `compileWorkflow` (lib/workflow.ts) hands ADK's
12
+ * `Workflow`: the same node names in the same order, the same edges in the
13
+ * same order with the same routes (tests/workflowGraph.test.ts holds the
14
+ * two together on every workflow fixture). The engine's own scheduler,
15
+ * lib/workflow/scheduler.ts, runs it (ADR 0087).
16
+ *
17
+ * ── Validation ────────────────────────────────────────────────────────────
18
+ * Two passes, each raising the message today's path raises:
19
+ * 1. The block's cross-field rules — every name an agent or a declared
20
+ * node, `START` opening a chain, a routing map after the node it
21
+ * routes, a declared node exactly one kind, the reserved `__route`
22
+ * suffix, `workflow` without `dispatch`, `retry` and `timeout` refused on
23
+ * a map entry, and the pauses and remotes a node cannot carry yet. Same paths and messages as the schema's
24
+ * (lib/syndicateSchema.ts, `workflowProblems`), thrown together as a
25
+ * `WorkflowGraphError`.
26
+ * 2. The graph's own rules, on the built edges — no empty routing map,
27
+ * `START` present, without routes and without incoming edges, every
28
+ * node reachable from `START`, no duplicate edge, one `default` per
29
+ * node, no unconditional cycle. Same messages ADK's graph validation
30
+ * throws when `compileWorkflow` builds the `Workflow`, first problem
31
+ * only, as ADK does.
32
+ * The schema's type checks (an edge chain of at least two elements, a name
33
+ * that is a non-empty string) are not repeated: the input is a syndicate
34
+ * that has passed `validateSyndicateConfig`, or one shaped like it. Whether
35
+ * a `tool` node's tool is registered is a compile-time question (the
36
+ * registry), not a graph one.
37
+ */
38
+ import type { SyndicateYamlConfig } from '../loadSyndicate.ts';
39
+ import type { RetryYaml } from '../workflowConfig.ts';
40
+ /**
41
+ * The entry node's name in the graph. The YAML writes it `START`; the graph
42
+ * names it as the ADK compile does, so a node path read from an ADK-written
43
+ * workflow event names the same node here.
44
+ */
45
+ export declare const START_NODE = "__START__";
46
+ /** What a node is. `route` is the hidden step after a node a routing map follows. */
47
+ export type GraphNodeKind = 'start' | 'agent' | 'join' | 'map' | 'tool' | 'ask_user' | 'route';
48
+ /** The modifiers a node entry carries (`retry`, `timeout`), in the YAML spelling. */
49
+ export interface GraphNodeSettings {
50
+ retry?: RetryYaml;
51
+ /** Seconds this node may run before it fails. */
52
+ timeout?: number;
53
+ }
54
+ export interface StartNode {
55
+ kind: 'start';
56
+ name: typeof START_NODE;
57
+ }
58
+ export interface AgentNode {
59
+ kind: 'agent';
60
+ /** The agent's YAML name: the orchestrator or a subagent. */
61
+ name: string;
62
+ settings: GraphNodeSettings;
63
+ }
64
+ export interface JoinNode {
65
+ kind: 'join';
66
+ name: string;
67
+ settings: GraphNodeSettings;
68
+ }
69
+ export interface MapNode {
70
+ kind: 'map';
71
+ name: string;
72
+ /** The agent run once per item of the list input. It is not itself a graph node. */
73
+ agent: string;
74
+ /** Concurrency of the map; absent means the runtime's default (8). */
75
+ maxParallel?: number;
76
+ /**
77
+ * The map entry's own modifiers: always empty, since the schema refuses
78
+ * `retry` and `timeout` on a map entry (ADR 0103). ADK's compile hands
79
+ * neither to its ParallelWorker.
80
+ */
81
+ settings: GraphNodeSettings;
82
+ /** The mapped agent's own node modifiers (`nodes.<agent>`), which ADK applies to each item's run. */
83
+ agentSettings: GraphNodeSettings;
84
+ }
85
+ export interface ToolNode {
86
+ kind: 'tool';
87
+ name: string;
88
+ /** The registry tool run with the node input as its arguments. */
89
+ tool: string;
90
+ settings: GraphNodeSettings;
91
+ }
92
+ export interface AskUserNode {
93
+ kind: 'ask_user';
94
+ name: string;
95
+ /** The question. */
96
+ message: string;
97
+ /** JSON Schema a structured reply must satisfy. */
98
+ schema?: Record<string, unknown>;
99
+ settings: GraphNodeSettings;
100
+ }
101
+ export interface RouteNode {
102
+ kind: 'route';
103
+ /** `<source>__route`. */
104
+ name: string;
105
+ /** The node whose output this step reads the route from. */
106
+ source: string;
107
+ /** Property of a JSON output holding the route (`route_key`, default "route"); else the trimmed text. */
108
+ routeKey: string;
109
+ }
110
+ export type GraphNode = StartNode | AgentNode | JoinNode | MapNode | ToolNode | AskUserNode | RouteNode;
111
+ /**
112
+ * When an edge fires: always (`always`), when the source's route is `key`
113
+ * (`key`, the YAML routing-map key verbatim), or when no keyed edge from the
114
+ * same source matched (`default`).
115
+ */
116
+ export type EdgeRoute = {
117
+ kind: 'always';
118
+ } | {
119
+ kind: 'key';
120
+ key: string;
121
+ } | {
122
+ kind: 'default';
123
+ };
124
+ export interface GraphEdge {
125
+ from: string;
126
+ to: string;
127
+ route: EdgeRoute;
128
+ }
129
+ export interface WorkflowGraph {
130
+ /** The syndicate's name, which names the workflow. */
131
+ name: string;
132
+ /** Every node, by name, in order of first appearance in `edges` (`START_NODE` first). */
133
+ nodes: Map<string, GraphNode>;
134
+ /** Every edge, chain by chain, element pair by element pair. */
135
+ edges: GraphEdge[];
136
+ /** Nodes that may run at once; absent means unbounded. */
137
+ maxConcurrency?: number;
138
+ /** Nodes with no outgoing edge: the workflow's output is theirs. */
139
+ terminals: string[];
140
+ /** Every agent the syndicate declares (orchestrator first), including one reached only through a map. */
141
+ agents: string[];
142
+ }
143
+ /** One problem the block's rules found, with the key path the schema reports it at. */
144
+ export interface GraphProblem {
145
+ path: readonly PropertyKey[];
146
+ message: string;
147
+ }
148
+ /**
149
+ * Thrown by `buildWorkflowGraph`. For the block's rules, `problems` holds one
150
+ * `<key.path> — <what>` line per problem and the message reads as
151
+ * `SyndicateValidationError`'s does without a file; for a graph rule, it
152
+ * holds the one message, which is the error's message.
153
+ */
154
+ export declare class WorkflowGraphError extends Error {
155
+ readonly problems: string[];
156
+ constructor(problems: string[], message?: string);
157
+ }
158
+ /**
159
+ * The graph a workflow syndicate declares. Throws `WorkflowGraphError` on a
160
+ * rule the block breaks, and an `Error` when the syndicate has no
161
+ * `workflow:` block, with `compileWorkflow`'s message.
162
+ */
163
+ export declare function buildWorkflowGraph(config: SyndicateYamlConfig): WorkflowGraph;
164
+ /**
165
+ * The cross-field rules of a `workflow:` block, with the paths and messages
166
+ * `validateSyndicateConfig` reports for them, in the same order. Empty for a
167
+ * block that keeps them all.
168
+ */
169
+ export declare function workflowConfigProblems(raw: Record<string, unknown>, subs: unknown[]): GraphProblem[];
170
+ /**
171
+ * A routing-map key as the ADK compile stores it and prints it: an integer
172
+ * spelling becomes a number and `true`/`false` a boolean, compared as
173
+ * strings, so `01` and `1` name the same route there. Used for the
174
+ * duplicate-edge rule and its message, and by the scheduler
175
+ * (lib/workflow/scheduler.ts) to match an emitted route as ADK matches it
176
+ * (ADR 0087); the graph keeps the key verbatim.
177
+ */
178
+ export declare function adkRouteString(route: EdgeRoute): string | null;
@@ -0,0 +1,468 @@
1
+ /**
2
+ * lib/workflow/graph.ts — the `workflow:` block as an engine-owned graph.
3
+ *
4
+ * The YAML (lib/workflowConfig.ts) says what runs after what in chains of
5
+ * names; this module turns those chains into the graph a scheduler runs:
6
+ * typed nodes (`start`, `agent`, `join`, `map`, `tool`, `ask_user`, and the
7
+ * hidden `route` step after a node a routing map follows), directed edges
8
+ * that are unconditional, keyed to a route, or the `default` route, and the
9
+ * workflow's `max_concurrency`. It imports nothing from ADK.
10
+ *
11
+ * It is the same graph `compileWorkflow` (lib/workflow.ts) hands ADK's
12
+ * `Workflow`: the same node names in the same order, the same edges in the
13
+ * same order with the same routes (tests/workflowGraph.test.ts holds the
14
+ * two together on every workflow fixture). The engine's own scheduler,
15
+ * lib/workflow/scheduler.ts, runs it (ADR 0087).
16
+ *
17
+ * ── Validation ────────────────────────────────────────────────────────────
18
+ * Two passes, each raising the message today's path raises:
19
+ * 1. The block's cross-field rules — every name an agent or a declared
20
+ * node, `START` opening a chain, a routing map after the node it
21
+ * routes, a declared node exactly one kind, the reserved `__route`
22
+ * suffix, `workflow` without `dispatch`, `retry` and `timeout` refused on
23
+ * a map entry, and the pauses and remotes a node cannot carry yet. Same paths and messages as the schema's
24
+ * (lib/syndicateSchema.ts, `workflowProblems`), thrown together as a
25
+ * `WorkflowGraphError`.
26
+ * 2. The graph's own rules, on the built edges — no empty routing map,
27
+ * `START` present, without routes and without incoming edges, every
28
+ * node reachable from `START`, no duplicate edge, one `default` per
29
+ * node, no unconditional cycle. Same messages ADK's graph validation
30
+ * throws when `compileWorkflow` builds the `Workflow`, first problem
31
+ * only, as ADK does.
32
+ * The schema's type checks (an edge chain of at least two elements, a name
33
+ * that is a non-empty string) are not repeated: the input is a syndicate
34
+ * that has passed `validateSyndicateConfig`, or one shaped like it. Whether
35
+ * a `tool` node's tool is registered is a compile-time question (the
36
+ * registry), not a graph one.
37
+ */
38
+ import { suggest } from '../syndicateSchema.js';
39
+ import { DEFAULT_ROUTE_KEY, NODE_KINDS, ROUTE_STEP_SUFFIX, START_NAME, elementNames, isWorkflowSyndicate, nodeKind } from '../workflowConfig.js';
40
+ // ── The model ────────────────────────────────────────────────────────────────
41
+ /**
42
+ * The entry node's name in the graph. The YAML writes it `START`; the graph
43
+ * names it as the ADK compile does, so a node path read from an ADK-written
44
+ * workflow event names the same node here.
45
+ */
46
+ export const START_NODE = '__START__';
47
+ /**
48
+ * Thrown by `buildWorkflowGraph`. For the block's rules, `problems` holds one
49
+ * `<key.path> — <what>` line per problem and the message reads as
50
+ * `SyndicateValidationError`'s does without a file; for a graph rule, it
51
+ * holds the one message, which is the error's message.
52
+ */
53
+ export class WorkflowGraphError extends Error {
54
+ problems;
55
+ constructor(problems, message) {
56
+ super(message ?? (problems.length === 1 ? problems[0] : `${problems.length} problems in syndicate config:\n ${problems.join('\n ')}`));
57
+ this.name = 'WorkflowGraphError';
58
+ this.problems = problems;
59
+ }
60
+ }
61
+ // ── Building ─────────────────────────────────────────────────────────────────
62
+ /**
63
+ * The graph a workflow syndicate declares. Throws `WorkflowGraphError` on a
64
+ * rule the block breaks, and an `Error` when the syndicate has no
65
+ * `workflow:` block, with `compileWorkflow`'s message.
66
+ */
67
+ export function buildWorkflowGraph(config) {
68
+ if (!isWorkflowSyndicate(config))
69
+ throw new Error(`${config.syndicate_name}: no workflow block`);
70
+ const raw = config;
71
+ const subs = Array.isArray(raw.subagents) ? raw.subagents : [];
72
+ const problems = workflowConfigProblems(raw, subs);
73
+ if (problems.length)
74
+ throw new WorkflowGraphError(problems.map((p) => `${formatPath(p.path)} — ${p.message}`));
75
+ const wf = config.workflow;
76
+ const nodeYaml = wf.nodes ?? {};
77
+ const agents = [config.orchestrator.name, ...(config.subagents ?? []).map((s) => s.name)];
78
+ const nodes = declaredNodes(agents, nodeYaml);
79
+ const nodeOf = (name) => {
80
+ const node = nodes.get(name);
81
+ if (!node)
82
+ throw new Error(`workflow: '${name}' is not an agent or a declared node of this syndicate`);
83
+ return node;
84
+ };
85
+ const routeStepAfter = (name) => {
86
+ const stepName = `${name}${ROUTE_STEP_SUFFIX}`;
87
+ const existing = nodes.get(stepName);
88
+ if (existing?.kind === 'route')
89
+ return existing;
90
+ const step = { kind: 'route', name: stepName, source: name, routeKey: nodeYaml[name]?.route_key ?? 'route' };
91
+ nodes.set(stepName, step);
92
+ return step;
93
+ };
94
+ const edges = [];
95
+ for (const chain of wf.edges) {
96
+ const out = [];
97
+ chain.forEach((element, i) => {
98
+ if (typeof element === 'string') {
99
+ out.push({ names: [element === START_NAME ? START_NODE : nodeOf(element).name] });
100
+ }
101
+ else if (Array.isArray(element)) {
102
+ out.push({ names: element.map((n) => nodeOf(n).name) });
103
+ }
104
+ else {
105
+ const previous = chain[i - 1];
106
+ if (typeof previous !== 'string' || previous === START_NAME) {
107
+ throw new Error(`workflow: a routing map must follow the name of the node whose output it routes`);
108
+ }
109
+ out.push({ names: [routeStepAfter(previous).name] });
110
+ out.push({ map: Object.entries(element).map(([key, target]) => [key, (Array.isArray(target) ? target : [target]).map((n) => nodeOf(n).name)]) });
111
+ }
112
+ });
113
+ for (let i = 0; i < out.length - 1; i++) {
114
+ const from = out[i];
115
+ const to = out[i + 1];
116
+ if ('map' in to) {
117
+ if ('map' in from)
118
+ throw new WorkflowGraphError([CONSECUTIVE_MAPS], CONSECUTIVE_MAPS);
119
+ if (to.map.length === 0)
120
+ throw new WorkflowGraphError([EMPTY_MAP], EMPTY_MAP);
121
+ for (const [key, targets] of to.map) {
122
+ const route = key === DEFAULT_ROUTE_KEY ? { kind: 'default' } : { kind: 'key', key };
123
+ for (const f of from.names)
124
+ for (const t of targets)
125
+ edges.push({ from: f, to: t, route });
126
+ }
127
+ }
128
+ else if ('names' in from) {
129
+ for (const f of from.names)
130
+ for (const t of to.names)
131
+ edges.push({ from: f, to: t, route: { kind: 'always' } });
132
+ }
133
+ }
134
+ }
135
+ // Nodes in order of first appearance, as a graph of edges orders them.
136
+ const ordered = new Map();
137
+ for (const edge of edges) {
138
+ for (const name of [edge.from, edge.to]) {
139
+ if (ordered.has(name))
140
+ continue;
141
+ ordered.set(name, name === START_NODE ? { kind: 'start', name: START_NODE } : nodes.get(name));
142
+ }
143
+ }
144
+ const graphProblem = graphRuleProblem([...ordered.keys()], edges);
145
+ if (graphProblem)
146
+ throw new WorkflowGraphError([graphProblem], graphProblem);
147
+ const sources = new Set(edges.map((e) => e.from));
148
+ return {
149
+ name: config.syndicate_name,
150
+ nodes: ordered,
151
+ edges,
152
+ ...(wf.max_concurrency !== undefined ? { maxConcurrency: wf.max_concurrency } : {}),
153
+ terminals: [...ordered.keys()].filter((n) => n !== START_NODE && !sources.has(n)),
154
+ agents,
155
+ };
156
+ }
157
+ /** Every agent as a node, then every declared node, from the YAML. */
158
+ function declaredNodes(agents, nodeYaml) {
159
+ const nodes = new Map();
160
+ for (const name of agents)
161
+ nodes.set(name, { kind: 'agent', name, settings: settingsOf(nodeYaml[name]) });
162
+ for (const [name, entry] of Object.entries(nodeYaml)) {
163
+ const kind = nodeKind(entry);
164
+ if (!kind)
165
+ continue; // modifiers on an agent
166
+ const settings = settingsOf(entry);
167
+ if (kind === 'ask_user') {
168
+ nodes.set(name, { kind, name, message: entry.ask_user ?? '', ...(entry.schema ? { schema: entry.schema } : {}), settings });
169
+ }
170
+ else if (kind === 'join') {
171
+ nodes.set(name, { kind, name, settings });
172
+ }
173
+ else if (kind === 'map') {
174
+ if (!agents.includes(entry.map))
175
+ throw new Error(`workflow node '${name}': map names '${entry.map}', which is not an agent of this syndicate`);
176
+ nodes.set(name, { kind, name, agent: entry.map, ...(entry.max_parallel !== undefined ? { maxParallel: entry.max_parallel } : {}), settings, agentSettings: settingsOf(nodeYaml[entry.map]) });
177
+ }
178
+ else if (kind === 'tool') {
179
+ nodes.set(name, { kind, name, tool: entry.tool, settings });
180
+ }
181
+ }
182
+ return nodes;
183
+ }
184
+ function settingsOf(entry) {
185
+ const out = {};
186
+ if (entry?.retry)
187
+ out.retry = entry.retry;
188
+ if (entry?.timeout !== undefined)
189
+ out.timeout = entry.timeout;
190
+ return out;
191
+ }
192
+ // ── The block's rules (the schema's messages) ────────────────────────────────
193
+ const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
194
+ function formatPath(p) {
195
+ let out = '';
196
+ for (const seg of p) {
197
+ if (typeof seg === 'number')
198
+ out += `[${seg}]`;
199
+ else
200
+ out += out ? `.${String(seg)}` : String(seg);
201
+ }
202
+ return out || '(root)';
203
+ }
204
+ /**
205
+ * The cross-field rules of a `workflow:` block, with the paths and messages
206
+ * `validateSyndicateConfig` reports for them, in the same order. Empty for a
207
+ * block that keeps them all.
208
+ */
209
+ export function workflowConfigProblems(raw, subs) {
210
+ const out = [];
211
+ const wf = isObj(raw.workflow) ? raw.workflow : {};
212
+ if (isObj(raw.dispatch)) {
213
+ out.push({ path: ['workflow'], message: 'a syndicate is a workflow or a plan-dispatch router, not both; remove `dispatch`' });
214
+ }
215
+ const agentNames = [];
216
+ if (isObj(raw.orchestrator) && typeof raw.orchestrator.name === 'string')
217
+ agentNames.push(raw.orchestrator.name);
218
+ for (const sub of subs)
219
+ if (isObj(sub) && typeof sub.name === 'string')
220
+ agentNames.push(sub.name);
221
+ const nodes = isObj(wf.nodes) ? wf.nodes : {};
222
+ const declared = Object.keys(nodes);
223
+ const known = new Set([...agentNames, ...declared]);
224
+ const mapped = new Set();
225
+ // Declared nodes: an agent takes modifiers only; anything else is one kind.
226
+ for (const [name, entry] of Object.entries(nodes)) {
227
+ if (!isObj(entry))
228
+ continue;
229
+ const kinds = NODE_KINDS.filter((k) => entry[k] !== undefined);
230
+ const isAgent = agentNames.includes(name);
231
+ if (isAgent && kinds.length > 0) {
232
+ out.push({ path: ['workflow', 'nodes', name], message: `'${name}' is an agent; its node entry may carry only route_key, retry and timeout` });
233
+ }
234
+ else if (!isAgent && kinds.length !== 1) {
235
+ out.push({ path: ['workflow', 'nodes', name], message: `a declared node is exactly one of ${NODE_KINDS.join(', ')}${kinds.length ? ` (has ${kinds.join(', ')})` : ''}` });
236
+ }
237
+ if (name.endsWith(ROUTE_STEP_SUFFIX) || name === START_NAME) {
238
+ out.push({ path: ['workflow', 'nodes', name], message: `'${name}' is reserved` });
239
+ }
240
+ const kind = nodeKind(entry);
241
+ if (kind === 'map') {
242
+ const target = entry.map;
243
+ if (!agentNames.includes(target)) {
244
+ const hint = suggest(target, agentNames);
245
+ out.push({ path: ['workflow', 'nodes', name, 'map'], message: `'${target}' is not an agent of this syndicate${hint ? ` (did you mean "${hint}"?)` : ''}` });
246
+ }
247
+ else {
248
+ mapped.add(target);
249
+ }
250
+ }
251
+ if (kind !== 'ask_user' && entry.schema !== undefined) {
252
+ out.push({ path: ['workflow', 'nodes', name, 'schema'], message: 'schema applies to ask_user only' });
253
+ }
254
+ if (kind !== 'map' && entry.max_parallel !== undefined) {
255
+ out.push({ path: ['workflow', 'nodes', name, 'max_parallel'], message: 'max_parallel applies to map only' });
256
+ }
257
+ // A map item runs under its agent's own modifiers, as on ADK (ADR 0089, ADR 0103): the map entry's would be applied by neither runtime.
258
+ if (kind === 'map') {
259
+ for (const key of ['retry', 'timeout']) {
260
+ if (entry[key] === undefined)
261
+ continue;
262
+ const target = entry.map;
263
+ out.push({ path: ['workflow', 'nodes', name, key], message: `${key} on a map node is not applied: each item runs under its agent's own ${key}; set it on nodes.${target}` });
264
+ }
265
+ }
266
+ }
267
+ for (const name of agentNames) {
268
+ if (name.endsWith(ROUTE_STEP_SUFFIX))
269
+ out.push({ path: ['workflow'], message: `agent name '${name}' ends with the reserved suffix ${ROUTE_STEP_SUFFIX}` });
270
+ }
271
+ // Edges.
272
+ const edges = Array.isArray(wf.edges) ? wf.edges : [];
273
+ let starts = 0;
274
+ const referenced = new Set();
275
+ const checkName = (name, path) => {
276
+ if (known.has(name)) {
277
+ referenced.add(name);
278
+ return;
279
+ }
280
+ const hint = suggest(name, [...known]);
281
+ out.push({ path, message: `'${name}' is not an agent or a declared node${hint ? ` (did you mean "${hint}"?)` : ''}` });
282
+ };
283
+ edges.forEach((chain, i) => {
284
+ if (!Array.isArray(chain))
285
+ return;
286
+ chain.forEach((element, j) => {
287
+ const path = ['workflow', 'edges', i, j];
288
+ if (typeof element === 'string') {
289
+ if (element === START_NAME) {
290
+ if (j !== 0)
291
+ out.push({ path, message: 'START opens a chain; it cannot follow a node' });
292
+ else
293
+ starts++;
294
+ return;
295
+ }
296
+ checkName(element, path);
297
+ }
298
+ else if (Array.isArray(element)) {
299
+ element.forEach((name, k) => typeof name === 'string' && checkName(name, [...path, k]));
300
+ }
301
+ else if (isObj(element)) {
302
+ const previous = chain[j - 1];
303
+ if (typeof previous !== 'string' || previous === START_NAME) {
304
+ out.push({ path, message: 'a routing map follows the name of the node whose output it routes' });
305
+ }
306
+ else if (!agentNames.includes(previous) && nodeKind(nodes[previous]) !== 'tool') {
307
+ out.push({ path, message: `'${previous}' is a ${nodeKind(nodes[previous]) ?? 'node'}; only an agent or a tool node emits a route` });
308
+ }
309
+ if (j !== chain.length - 1)
310
+ out.push({ path, message: 'a routing map ends its chain; start another chain from each target' });
311
+ for (const [key, target] of Object.entries(element)) {
312
+ const targets = Array.isArray(target) ? target : [target];
313
+ targets.forEach((name, k) => typeof name === 'string' && checkName(name, [...path, key, k]));
314
+ }
315
+ }
316
+ for (const name of isObj(element) || Array.isArray(element) || typeof element === 'string' ? elementNames(element) : []) {
317
+ if (mapped.has(name))
318
+ out.push({ path, message: `'${name}' is run by a map node; it cannot also appear in an edge` });
319
+ }
320
+ });
321
+ });
322
+ if (edges.length && starts === 0)
323
+ out.push({ path: ['workflow', 'edges'], message: `no chain begins with ${START_NAME}` });
324
+ for (const name of declared) {
325
+ if (!referenced.has(name) && !agentNames.includes(name))
326
+ out.push({ path: ['workflow', 'nodes', name], message: 'declared but used in no edge' });
327
+ }
328
+ // What a node cannot carry yet (lib/workflow.ts, "Not in this version").
329
+ const agents = [
330
+ [['orchestrator'], raw.orchestrator],
331
+ ...subs.map((sub, i) => [['subagents', i], sub]),
332
+ ];
333
+ for (const [path, agent] of agents) {
334
+ if (!isObj(agent))
335
+ continue;
336
+ // A map item cannot pause: the walk resumes a paused agent node, not an item of a map (ADR 0094, ADR 0098).
337
+ if (typeof agent.name === 'string' && mapped.has(agent.name) && Array.isArray(agent.require_approval) && agent.require_approval.length) {
338
+ out.push({ path: [...path, 'require_approval'], message: 'approval gates are not supported on an agent a map node runs: a map item cannot pause the walk' });
339
+ }
340
+ if (isObj(agent.skills) && agent.skills.scripts === 'local') {
341
+ out.push({ path: [...path, 'skills', 'scripts'], message: 'skill scripts (an approval pause) are not supported inside a workflow yet' });
342
+ }
343
+ if (typeof agent.a2a_agent_url === 'string') {
344
+ out.push({ path: [...path, 'a2a_agent_url'], message: 'a remote agent cannot be a workflow node yet' });
345
+ }
346
+ }
347
+ return out;
348
+ }
349
+ // ── The graph's rules (ADK's graph-validation messages) ──────────────────────
350
+ const CONSECUTIVE_MAPS = 'Consecutive routing maps are not allowed in a chain. Split them into separate edge items.';
351
+ const EMPTY_MAP = 'Routing map must not be empty. Provide at least one route -> node mapping.';
352
+ /** The route value ADK stores for the `default` key. */
353
+ const ADK_DEFAULT_ROUTE = '__DEFAULT__';
354
+ /**
355
+ * A routing-map key as the ADK compile stores it and prints it: an integer
356
+ * spelling becomes a number and `true`/`false` a boolean, compared as
357
+ * strings, so `01` and `1` name the same route there. Used for the
358
+ * duplicate-edge rule and its message, and by the scheduler
359
+ * (lib/workflow/scheduler.ts) to match an emitted route as ADK matches it
360
+ * (ADR 0087); the graph keeps the key verbatim.
361
+ */
362
+ export function adkRouteString(route) {
363
+ if (route.kind === 'always')
364
+ return null;
365
+ if (route.kind === 'default')
366
+ return ADK_DEFAULT_ROUTE;
367
+ if (/^-?\d+$/.test(route.key))
368
+ return String(Number(route.key));
369
+ return route.key;
370
+ }
371
+ /**
372
+ * The first rule the built graph breaks, with ADK's message, in ADK's order
373
+ * (`validateGraph`): START present, START edges unrouted, reachability,
374
+ * START without incoming edges, duplicate edges, one default per node,
375
+ * no unconditional cycle. Undefined when it keeps them all.
376
+ */
377
+ function graphRuleProblem(nodeNames, edges) {
378
+ const names = new Set(nodeNames);
379
+ if (!names.has(START_NODE))
380
+ return `Graph validation failed. START node (name: '${START_NODE}') not found in graph nodes.`;
381
+ for (const edge of edges) {
382
+ if (edge.from === START_NODE && edge.route.kind !== 'always') {
383
+ return `Graph validation failed. Edges from START must not have routes (edge to ${edge.to} has route ${adkRouteString(edge.route)}).`;
384
+ }
385
+ }
386
+ // Reachability from START.
387
+ const adj = new Map();
388
+ for (const name of names)
389
+ adj.set(name, new Set());
390
+ const toNodes = new Set();
391
+ for (const edge of edges) {
392
+ adj.get(edge.from).add(edge.to);
393
+ toNodes.add(edge.to);
394
+ }
395
+ const reachable = new Set();
396
+ const stack = [START_NODE];
397
+ while (stack.length > 0) {
398
+ const node = stack.pop();
399
+ if (reachable.has(node))
400
+ continue;
401
+ reachable.add(node);
402
+ for (const next of adj.get(node) ?? [])
403
+ if (!reachable.has(next))
404
+ stack.push(next);
405
+ }
406
+ const unreachable = [...names].filter((n) => !reachable.has(n)).sort();
407
+ if (unreachable.length > 0)
408
+ return `Graph validation failed. The following nodes are unreachable from START: ${JSON.stringify(unreachable)}`;
409
+ if (toNodes.has(START_NODE))
410
+ return 'Graph validation failed. START node must not have incoming edges.';
411
+ // Duplicate edges.
412
+ const seen = new Set();
413
+ for (const edge of edges) {
414
+ const route = adkRouteString(edge.route);
415
+ const key = JSON.stringify([edge.from, edge.to, route]);
416
+ if (seen.has(key)) {
417
+ const unconditional = route === null;
418
+ return `Graph validation failed. Duplicate edge found: from=${edge.from}, to=${edge.to}${unconditional ? '' : `, route=${JSON.stringify(route)}`}. ${unconditional ? 'The same pair is already connected unconditionally' : 'That route already points at this node'}, so the target would be triggered twice.`;
419
+ }
420
+ seen.add(key);
421
+ }
422
+ // One default per node.
423
+ const defaults = new Map();
424
+ for (const edge of edges) {
425
+ if (edge.route.kind !== 'default')
426
+ continue;
427
+ const first = defaults.get(edge.from);
428
+ if (first !== undefined)
429
+ return `Graph validation failed. Multiple DEFAULT_ROUTE edges found from node ${edge.from} to ${first} and ${edge.to}`;
430
+ defaults.set(edge.from, edge.to);
431
+ }
432
+ // No unconditional cycle (depth-first, in node order, as ADK walks it).
433
+ const always = new Map();
434
+ for (const name of names)
435
+ always.set(name, []);
436
+ for (const edge of edges)
437
+ if (edge.route.kind === 'always')
438
+ always.get(edge.from).push(edge.to);
439
+ const inStack = new Set();
440
+ const done = new Set();
441
+ const dfs = (node, path) => {
442
+ inStack.add(node);
443
+ path.push(node);
444
+ for (const neighbor of always.get(node) ?? []) {
445
+ if (inStack.has(neighbor)) {
446
+ const cycle = [...path.slice(path.indexOf(neighbor)), neighbor];
447
+ return `Graph validation failed. Unconditional cycle detected: ${cycle.join(' -> ')}. An unconditional cycle has no exit and would loop forever; break it with at least one conditional (routed) edge so a node can leave the cycle by not emitting that route. (A routed cycle can still loop if a node keeps emitting the route — bounding that is the node's responsibility, not this validator's.)`;
448
+ }
449
+ if (!done.has(neighbor)) {
450
+ const found = dfs(neighbor, path);
451
+ if (found)
452
+ return found;
453
+ }
454
+ }
455
+ path.pop();
456
+ inStack.delete(node);
457
+ done.add(node);
458
+ return undefined;
459
+ };
460
+ for (const name of names) {
461
+ if (done.has(name))
462
+ continue;
463
+ const found = dfs(name, []);
464
+ if (found)
465
+ return found;
466
+ }
467
+ return undefined;
468
+ }