melchizedek-agents 0.9.6 → 0.16.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 (246) hide show
  1. package/.env.example +272 -0
  2. package/AGENT_SETUP.md +39 -23
  3. package/CHANGELOG.md +623 -0
  4. package/DOCUMENTATION.md +429 -109
  5. package/QUICKSTART.md +92 -18
  6. package/README.md +45 -5
  7. package/config/agents/README.md +46 -1
  8. package/config/agents/examples/ares.yaml +117 -40
  9. package/config/agents/examples/assistant.yaml +159 -0
  10. package/config/agents/examples/augustin.yaml +154 -51
  11. package/config/agents/examples/cartographers.yaml +91 -23
  12. package/config/agents/examples/claude.yaml +45 -6
  13. package/config/agents/examples/council.yaml +23 -8
  14. package/config/agents/examples/critic.yaml +57 -21
  15. package/config/agents/examples/delegation.yaml +76 -9
  16. package/config/agents/examples/hierarchical.yaml +55 -8
  17. package/config/agents/examples/image_production.yaml +54 -16
  18. package/config/agents/examples/librarian.yaml +70 -14
  19. package/config/agents/examples/model_zoo.yaml +105 -29
  20. package/config/agents/examples/patient_advocate.yaml +102 -34
  21. package/config/agents/examples/research.yaml +250 -0
  22. package/config/agents/examples/scribe.yaml +242 -0
  23. package/config/agents/examples/scriptorium.yaml +87 -24
  24. package/config/agents/examples/style_council.yaml +31 -9
  25. package/config/agents/examples/syndicate.yaml +29 -10
  26. package/config/agents/examples/tutor.yaml +10 -3
  27. package/config/agents/syndicateSchema.yaml +44 -9
  28. package/config/agents/templates/README.md +58 -0
  29. package/config/agents/templates/account_memory.yaml +153 -0
  30. package/config/agents/templates/case_desk.yaml +188 -0
  31. package/config/agents/templates/conversational.yaml +110 -0
  32. package/config/agents/templates/draft_review.yaml +130 -0
  33. package/config/agents/templates/intake_extractor.yaml +152 -0
  34. package/config/agents/templates/research_brief.yaml +169 -0
  35. package/config/agents/templates/research_desk.yaml +152 -0
  36. package/config/agents/templates/review_panel.yaml +188 -0
  37. package/config/agents/templates/support_triage.yaml +273 -0
  38. package/config/agents/templates/systems_operator.yaml +157 -0
  39. package/db/hardening.sql +202 -0
  40. package/db/memory_v2.sql +77 -0
  41. package/db/migrations/0001_base.sql +167 -0
  42. package/db/migrations/0002_erase_scope.sql +172 -0
  43. package/db/migrations/0003_postgres_storage.sql +58 -0
  44. package/db/migrations/0004_usage.sql +94 -0
  45. package/db/migrations/0005_agent_registry.sql +200 -0
  46. package/db/migrations/0006_task_leases.sql +25 -0
  47. package/db/migrations/0007_memory_commit.sql +134 -0
  48. package/db/migrations/0008_memory_retention.sql +37 -0
  49. package/db/migrations/0009_task_queue.sql +51 -0
  50. package/db/migrations/0010_erase_complete.sql +180 -0
  51. package/db/telemetry.sql +401 -0
  52. package/demo/README.md +77 -0
  53. package/demo/a2a_demo.mjs +95 -0
  54. package/dist/lib/a2a/app.d.ts +253 -0
  55. package/dist/lib/a2a/app.js +751 -0
  56. package/dist/lib/a2a/executor.d.ts +167 -0
  57. package/dist/lib/a2a/executor.js +548 -0
  58. package/dist/lib/a2a/identity.d.ts +122 -0
  59. package/dist/lib/a2a/identity.js +254 -0
  60. package/dist/lib/a2a/policy.d.ts +110 -0
  61. package/dist/lib/a2a/policy.js +178 -0
  62. package/dist/lib/a2a/redisLimits.d.ts +26 -0
  63. package/dist/lib/a2a/redisLimits.js +31 -0
  64. package/dist/lib/a2a/remoteAgent.d.ts +91 -0
  65. package/dist/lib/a2a/remoteAgent.js +268 -0
  66. package/dist/lib/a2a/turnLock.d.ts +28 -0
  67. package/dist/lib/a2a/turnLock.js +76 -0
  68. package/dist/lib/compile.d.ts +11 -1
  69. package/dist/lib/compile.js +109 -8
  70. package/dist/lib/config.d.ts +7 -7
  71. package/dist/lib/config.js +7 -7
  72. package/dist/lib/doctor.d.ts +145 -0
  73. package/dist/lib/doctor.js +517 -0
  74. package/dist/lib/grounding.d.ts +2 -2
  75. package/dist/lib/grounding.js +2 -2
  76. package/dist/lib/guards/index.d.ts +37 -0
  77. package/dist/lib/guards/index.js +66 -0
  78. package/dist/lib/guards/science.d.ts +117 -0
  79. package/dist/lib/guards/science.js +444 -0
  80. package/dist/lib/index.d.ts +37 -2
  81. package/dist/lib/index.js +35 -4
  82. package/dist/lib/loadEnv.d.ts +23 -11
  83. package/dist/lib/loadEnv.js +77 -29
  84. package/dist/lib/loadSyndicate.d.ts +97 -5
  85. package/dist/lib/loadSyndicate.js +176 -76
  86. package/dist/lib/memory/erase.d.ts +24 -0
  87. package/dist/lib/memory/erase.js +47 -0
  88. package/dist/lib/memory/namespace.d.ts +37 -0
  89. package/dist/lib/memory/namespace.js +74 -0
  90. package/dist/lib/memory/providers.d.ts +99 -0
  91. package/dist/lib/memory/providers.js +240 -0
  92. package/dist/lib/memory/store.d.ts +74 -0
  93. package/dist/lib/memory/store.js +108 -0
  94. package/dist/lib/memory/supabaseMemoryService.d.ts +69 -9
  95. package/dist/lib/memory/supabaseMemoryService.js +239 -125
  96. package/dist/lib/models/capabilities.d.ts +103 -0
  97. package/dist/lib/models/capabilities.js +291 -0
  98. package/dist/lib/models/claudeLlm.d.ts +10 -1
  99. package/dist/lib/models/claudeLlm.js +57 -27
  100. package/dist/lib/models/endpoints.d.ts +100 -0
  101. package/dist/lib/models/endpoints.js +282 -0
  102. package/dist/lib/models/gateway.d.ts +107 -0
  103. package/dist/lib/models/gateway.js +157 -0
  104. package/dist/lib/models/gatewayLlm.d.ts +57 -0
  105. package/dist/lib/models/gatewayLlm.js +105 -0
  106. package/dist/lib/models/gptLlm.d.ts +51 -2
  107. package/dist/lib/models/gptLlm.js +169 -26
  108. package/dist/lib/models/grokLlm.d.ts +19 -10
  109. package/dist/lib/models/grokLlm.js +29 -14
  110. package/dist/lib/models/ollamaLlm.d.ts +26 -0
  111. package/dist/lib/models/ollamaLlm.js +43 -1
  112. package/dist/lib/models/openAiCompatibleLlm.d.ts +67 -2
  113. package/dist/lib/models/openAiCompatibleLlm.js +200 -37
  114. package/dist/lib/models/providerMap.js +9 -2
  115. package/dist/lib/models/registry.d.ts +42 -5
  116. package/dist/lib/models/registry.js +140 -29
  117. package/dist/lib/models/retry.d.ts +146 -0
  118. package/dist/lib/models/retry.js +305 -0
  119. package/dist/lib/models/schemaNormalize.d.ts +20 -0
  120. package/dist/lib/models/schemaNormalize.js +43 -0
  121. package/dist/lib/net/addressGuard.d.ts +60 -0
  122. package/dist/lib/net/addressGuard.js +216 -0
  123. package/dist/lib/observability/embeddings.d.ts +11 -2
  124. package/dist/lib/observability/embeddings.js +18 -14
  125. package/dist/lib/observability/metrics.d.ts +36 -0
  126. package/dist/lib/observability/metrics.js +86 -0
  127. package/dist/lib/observability/redact.d.ts +29 -0
  128. package/dist/lib/observability/redact.js +102 -0
  129. package/dist/lib/observability/supabaseSpanExporter.d.ts +2 -0
  130. package/dist/lib/observability/supabaseSpanExporter.js +22 -6
  131. package/dist/lib/observability/tracer.d.ts +10 -0
  132. package/dist/lib/observability/tracer.js +70 -3
  133. package/dist/lib/persistence/supabaseProvider.d.ts +11 -0
  134. package/dist/lib/persistence/supabaseProvider.js +32 -6
  135. package/dist/lib/registry.d.ts +87 -0
  136. package/dist/lib/registry.js +146 -0
  137. package/dist/lib/runtime/approvals.d.ts +44 -0
  138. package/dist/lib/runtime/approvals.js +67 -0
  139. package/dist/lib/runtime/syndicateTurn.d.ts +233 -0
  140. package/dist/lib/runtime/syndicateTurn.js +597 -0
  141. package/dist/lib/runtime/turnControl.d.ts +90 -0
  142. package/dist/lib/runtime/turnControl.js +131 -0
  143. package/dist/lib/session/supabaseSessionService.js +1 -1
  144. package/dist/lib/session/transcript.d.ts +9 -1
  145. package/dist/lib/session/transcript.js +15 -7
  146. package/dist/lib/skills.d.ts +50 -0
  147. package/dist/lib/skills.js +138 -0
  148. package/dist/lib/storage/postgres/index.d.ts +98 -0
  149. package/dist/lib/storage/postgres/index.js +146 -0
  150. package/dist/lib/storage/postgres/memoryStore.d.ts +11 -0
  151. package/dist/lib/storage/postgres/memoryStore.js +82 -0
  152. package/dist/lib/storage/postgres/sessionService.d.ts +51 -0
  153. package/dist/lib/storage/postgres/sessionService.js +225 -0
  154. package/dist/lib/storage/postgres/taskQueue.d.ts +22 -0
  155. package/dist/lib/storage/postgres/taskQueue.js +101 -0
  156. package/dist/lib/storage/postgres/taskStore.d.ts +55 -0
  157. package/dist/lib/storage/postgres/taskStore.js +168 -0
  158. package/dist/lib/storage/schema.d.ts +27 -0
  159. package/dist/lib/storage/schema.js +50 -0
  160. package/dist/lib/storage/schemaVersion.d.ts +25 -0
  161. package/dist/lib/storage/schemaVersion.js +46 -0
  162. package/dist/lib/syndicateSchema.d.ts +275 -0
  163. package/dist/lib/syndicateSchema.js +534 -0
  164. package/dist/lib/toolRegistry.d.ts +15 -0
  165. package/dist/lib/toolRegistry.js +59 -10
  166. package/dist/lib/tools/collectionsSearchTool.js +1 -1
  167. package/dist/lib/tools/mcpServe.d.ts +45 -0
  168. package/dist/lib/tools/mcpServe.js +88 -0
  169. package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
  170. package/dist/lib/tools/mcpToolFactory.js +41 -21
  171. package/dist/lib/tools/science/corrections.d.ts +42 -0
  172. package/dist/lib/tools/science/corrections.js +115 -0
  173. package/dist/lib/tools/science/europepmc.d.ts +51 -0
  174. package/dist/lib/tools/science/europepmc.js +127 -0
  175. package/dist/lib/tools/science/http.d.ts +31 -0
  176. package/dist/lib/tools/science/http.js +104 -0
  177. package/dist/lib/tools/science/openalex.d.ts +30 -0
  178. package/dist/lib/tools/science/openalex.js +81 -0
  179. package/dist/lib/tools/science/trials.d.ts +58 -0
  180. package/dist/lib/tools/science/trials.js +203 -0
  181. package/dist/lib/tools/science/types.d.ts +69 -0
  182. package/dist/lib/tools/science/types.js +48 -0
  183. package/dist/lib/tools/scienceTools.d.ts +100 -0
  184. package/dist/lib/tools/scienceTools.js +277 -0
  185. package/dist/lib/tools/taskTools.d.ts +204 -0
  186. package/dist/lib/tools/taskTools.js +374 -0
  187. package/dist/lib/tools/toolContract.d.ts +17 -4
  188. package/dist/lib/tools/toolContract.js +13 -5
  189. package/dist/lib/tools/webExtractTool.d.ts +13 -10
  190. package/dist/lib/tools/webExtractTool.js +23 -55
  191. package/dist/lib/tools/webSearchTool.d.ts +2 -1
  192. package/dist/lib/tools/webSearchTool.js +4 -3
  193. package/dist/lib/tools/wikiTools.d.ts +2 -2
  194. package/dist/lib/tools/wikiTools.js +12 -12
  195. package/dist/lib/tools/xApiSearchTool.d.ts +194 -0
  196. package/dist/lib/tools/xApiSearchTool.js +583 -0
  197. package/dist/lib/tools/xSearchTool.js +1 -1
  198. package/dist/lib/wiki/agentRun.js +1 -1
  199. package/dist/lib/wiki/builder.js +21 -6
  200. package/dist/lib/wiki/dive.js +2 -2
  201. package/dist/lib/wiki/entities.d.ts +2 -2
  202. package/dist/lib/wiki/entities.js +7 -7
  203. package/dist/lib/wiki/extract.js +5 -1
  204. package/dist/lib/wiki/fill.js +5 -5
  205. package/dist/lib/wiki/format.d.ts +7 -5
  206. package/dist/lib/wiki/format.js +2 -2
  207. package/dist/lib/wiki/graph.js +2 -2
  208. package/dist/lib/wiki/lint.js +2 -2
  209. package/dist/lib/wiki/markdown.d.ts +1 -1
  210. package/dist/lib/wiki/markdown.js +6 -1
  211. package/dist/lib/wiki/pipeline.js +2 -2
  212. package/dist/lib/wiki/vault.js +2 -2
  213. package/dist/scripts/a2a_server.d.ts +17 -11
  214. package/dist/scripts/a2a_server.js +405 -1028
  215. package/dist/scripts/assistant_worker.d.ts +33 -0
  216. package/dist/scripts/assistant_worker.js +193 -0
  217. package/dist/scripts/db.d.ts +26 -0
  218. package/dist/scripts/db.js +179 -0
  219. package/dist/scripts/doctor.d.ts +23 -0
  220. package/dist/scripts/doctor.js +90 -0
  221. package/dist/scripts/init.d.ts +43 -0
  222. package/dist/scripts/init.js +152 -0
  223. package/dist/scripts/registry.d.ts +25 -0
  224. package/dist/scripts/registry.js +182 -0
  225. package/dist/scripts/skills_install.d.ts +7 -0
  226. package/dist/scripts/skills_install.js +137 -0
  227. package/dist/scripts/syndicate_chat.js +159 -276
  228. package/package.json +90 -16
  229. package/skills/README.md +74 -0
  230. package/skills/briefs/README.md +16 -0
  231. package/skills/briefs/_shared.md +16 -0
  232. package/skills/briefs/melchizedek-author.md +60 -0
  233. package/skills/briefs/melchizedek-memory.md +47 -0
  234. package/skills/briefs/melchizedek-models.md +47 -0
  235. package/skills/briefs/melchizedek-scribe.md +37 -0
  236. package/skills/briefs/melchizedek-serve.md +56 -0
  237. package/skills/briefs/melchizedek.md +69 -0
  238. package/skills/briefs/skills-readme.md +33 -0
  239. package/skills/melchizedek/SKILL.md +159 -0
  240. package/skills/melchizedek-author/SKILL.md +99 -0
  241. package/skills/melchizedek-author/templates/minimal.yaml +60 -0
  242. package/skills/melchizedek-memory/SKILL.md +138 -0
  243. package/skills/melchizedek-models/SKILL.md +137 -0
  244. package/skills/melchizedek-scribe/SKILL.md +72 -0
  245. package/skills/melchizedek-scribe/templates/brief.md +27 -0
  246. package/skills/melchizedek-serve/SKILL.md +119 -0
@@ -0,0 +1,597 @@
1
+ /**
2
+ * lib/runtime/syndicateTurn.ts — ONE implementation of "run a syndicate for
3
+ * one user message". The A2A server, the terminal REPL, the background
4
+ * worker and the observatory's eval harness all call `runSyndicateTurn`, and
5
+ * so does a package consumer embedding the engine.
6
+ *
7
+ * ── Why this exists ───────────────────────────────────────────────────────
8
+ * The turn semantics that make the framework work — plan-dispatch with
9
+ * deterministic overrides, the classifier's transcript digest, the projected
10
+ * session a dispatch route reads, the DELEGATE relay fallback, post-answer
11
+ * guards — used to live inside the A2A server's executor, welded to the A2A
12
+ * event bus. The eval harness carried a second copy ("reimplemented, same
13
+ * rule"), and the REPL a third compiler that ignored dispatch, nesting and
14
+ * guards altogether. Three copies of one behaviour drift; this file is the
15
+ * one copy. Surfaces differ only in what they do with the result: the server
16
+ * publishes A2A events, the REPL prints, the harness records.
17
+ *
18
+ * It is also the seam that keeps Google ADK an implementation detail: a
19
+ * caller hands in YAML config and plain text parts and gets plain data back.
20
+ * The ADK Runner, LlmAgent and event shapes stay on this side of the line.
21
+ *
22
+ * ── Controls ──────────────────────────────────────────────────────────────
23
+ * Every turn runs under a TurnControl (lib/runtime/turnControl.ts): the
24
+ * YAML's `max_steps` caps model calls across the WHOLE turn (orchestrator,
25
+ * subagents, nested syndicates), an optional deadline bounds wall-clock
26
+ * time, and an outer AbortSignal cancels it. All three abort the provider
27
+ * request in flight, not just the loop around it.
28
+ */
29
+ import { InMemorySessionService, Runner, StreamingMode, getFunctionCalls, getFunctionResponses } from '@google/adk';
30
+ import { compileGraph, compileSubagent } from '../compile.js';
31
+ import { isDispatchSyndicate, matchRouteOverride, resolveRoute } from '../dispatch.js';
32
+ import { collectGrounding, describeGrounding, newGroundingState, webSourcesLine } from '../grounding.js';
33
+ import { resolveGuards } from '../guards/index.js';
34
+ import { collectGuards, nestedLoader } from '../loadSyndicate.js';
35
+ import { registerAvailableProviders } from '../models/registry.js';
36
+ import { traceAgentRun } from '../observability/tracer.js';
37
+ import { ProjectedSessionService, renderTranscriptDigest } from '../session/transcript.js';
38
+ import { approvalDecisionIn, describeApproval, interruptedTurnStart, pendingApproval } from './approvals.js';
39
+ export { approvalResponsePart, pendingApproval } from './approvals.js';
40
+ import { RemoteA2AAgent, remoteContextId, remoteToolOutput } from '../a2a/remoteAgent.js';
41
+ import { createTurnControl, runWithTurnControl, stopCode, stopMessage } from './turnControl.js';
42
+ const THOUGHTS_PREVIEW_CHARS = 600;
43
+ const CLASSIFIER_PREAMBLE = `RECENT CONVERSATION, oldest first — context for your classification only. `
44
+ + `Never answer it, and never route on it alone; it is here so you can tell a `
45
+ + `follow-up, a redo request, or a challenge to a previous answer apart from small talk.\n`;
46
+ // ── Draining one agent's stream ──────────────────────────────────────────────
47
+ function formatToolArgs(args) {
48
+ if (!args || Object.keys(args).length === 0)
49
+ return '';
50
+ return Object.entries(args)
51
+ .map(([k, v]) => {
52
+ const raw = typeof v === 'string' ? v : JSON.stringify(v);
53
+ return `${k}=${raw.length > 60 ? `${raw.slice(0, 60)}...` : raw}`;
54
+ })
55
+ .join(', ');
56
+ }
57
+ /**
58
+ * Drains one ADK event stream into a DrainedRun. The answer is the LAST
59
+ * event's text, not every event's: a tool-using agent narrates between
60
+ * calls, and accumulating glued that narration to the front of its report.
61
+ * Within one event parts are joined, and `partial` events (streaming chunks)
62
+ * append. Thinking never counts as answer text.
63
+ */
64
+ export async function drainAgentStream(stream, opts = {}) {
65
+ const ev = opts.events ?? {};
66
+ const d = {
67
+ text: '',
68
+ lastToolResultText: '',
69
+ toolResultTexts: [],
70
+ invokedToolNames: new Set(),
71
+ toolCalls: [],
72
+ toolResponses: 0,
73
+ delegations: [],
74
+ agents: new Set(),
75
+ thoughts: '',
76
+ tokens: { input: 0, output: 0, thinking: 0 },
77
+ eventCount: 0,
78
+ };
79
+ const grounding = newGroundingState();
80
+ let groundingAnnounced = false;
81
+ let streamed = false;
82
+ for await (const event of stream) {
83
+ const e = event;
84
+ d.eventCount++;
85
+ ev.onEvent?.(event);
86
+ if (e.author)
87
+ d.agents.add(e.author);
88
+ // Native search grounding rides on the model event, not on a tool call.
89
+ if (collectGrounding(e, grounding)) {
90
+ const firstSight = grounding.queries.size + grounding.sources.size > 0 && !groundingAnnounced;
91
+ ev.log?.(`⌕ Grounding: ${describeGrounding(grounding)}`);
92
+ // Same shape as a function-tool line so consumers that parse
93
+ // "Invoking tool:" status lines list it too.
94
+ if (opts.publishToolStatus && firstSight)
95
+ ev.onProgress?.('Invoking tool: web_search');
96
+ groundingAnnounced = true;
97
+ }
98
+ if ((e.errorCode || e.errorMessage) && e.errorCode !== 'STOP') {
99
+ d.error = { code: String(e.errorCode ?? 'ERROR'), message: String(e.errorMessage ?? '') };
100
+ ev.warn?.(`Error [${d.error.code}]: ${d.error.message}`);
101
+ break;
102
+ }
103
+ if (e.usageMetadata) {
104
+ // Same aggregation as traceAgentRun: peak prompt, summed completions.
105
+ d.tokens.input = Math.max(d.tokens.input, e.usageMetadata.promptTokenCount ?? 0);
106
+ d.tokens.output += e.usageMetadata.candidatesTokenCount ?? 0;
107
+ d.tokens.thinking += e.usageMetadata.thoughtsTokenCount ?? 0;
108
+ }
109
+ const calls = getFunctionCalls(event) ?? [];
110
+ // Text streamed before a tool call was narration, not the answer.
111
+ if (streamed && calls.length) {
112
+ ev.onTextReset?.();
113
+ streamed = false;
114
+ }
115
+ for (const call of calls) {
116
+ const name = call.name ?? '';
117
+ const args = (call.args ?? {});
118
+ if (name === 'transfer_to_agent') {
119
+ const target = String(args.agentName ?? 'unknown');
120
+ d.delegations.push(target);
121
+ ev.log?.(`→ Delegating to: ${target}`);
122
+ if (opts.publishToolStatus)
123
+ ev.onProgress?.(`Delegating to subagent: ${target}`);
124
+ continue;
125
+ }
126
+ if (!name)
127
+ continue;
128
+ d.invokedToolNames.add(name);
129
+ if (opts.subagentNames?.has(name))
130
+ d.delegations.push(name);
131
+ d.toolCalls.push({ agent: e.author ?? 'unknown', name, args });
132
+ ev.log?.(`→ Tool: ${name}(${formatToolArgs(args)})`);
133
+ if (opts.publishToolStatus)
134
+ ev.onProgress?.(`Invoking tool: ${name}`);
135
+ }
136
+ for (const resp of getFunctionResponses(event) ?? []) {
137
+ const r = resp;
138
+ const name = r.name ?? r.functionResponse?.name ?? '';
139
+ const content = r.response ?? r.functionResponse?.response ?? {};
140
+ d.toolResponses++;
141
+ if (name)
142
+ d.invokedToolNames.add(name);
143
+ const asString = typeof content === 'string' ? content : JSON.stringify(content);
144
+ ev.log?.(`← Result: ${name || 'unknown'} — ${asString.length.toLocaleString()} chars`);
145
+ // Only plain text results are relayable answers — a structured or
146
+ // base64 payload (image tools) is not.
147
+ const resultText = typeof content === 'string'
148
+ ? content
149
+ : typeof content?.result === 'string'
150
+ ? content.result
151
+ : '';
152
+ if (resultText.trim()) {
153
+ d.lastToolResultText = resultText.trim();
154
+ d.toolResultTexts.push(resultText.trim());
155
+ }
156
+ }
157
+ let eventText = '';
158
+ for (const part of event.content?.parts ?? []) {
159
+ const p = part;
160
+ if (!p.text)
161
+ continue;
162
+ if (p.thought === true) {
163
+ if (d.thoughts.length < THOUGHTS_PREVIEW_CHARS)
164
+ d.thoughts += p.text;
165
+ }
166
+ else {
167
+ eventText += p.text;
168
+ }
169
+ }
170
+ if (eventText)
171
+ d.text = e.partial === true ? d.text + eventText : eventText;
172
+ if (opts.streamText && e.partial === true && eventText) {
173
+ ev.onTextDelta?.(eventText);
174
+ streamed = true;
175
+ }
176
+ }
177
+ const sourcesLine = webSourcesLine(grounding);
178
+ // A consumer contract clients parse: "Web sources: a, b",
179
+ // published once, after the stream, with the full set.
180
+ if (opts.publishToolStatus && sourcesLine)
181
+ ev.onProgress?.(sourcesLine);
182
+ if (grounding.queries.size || grounding.sources.size) {
183
+ d.grounding = { queries: [...grounding.queries], sources: [...grounding.sources] };
184
+ }
185
+ d.text = d.text.trim();
186
+ return d;
187
+ }
188
+ /** True when an orchestrator's text is just the name of a tool it called —
189
+ * the degenerate relay ("FinancialAnalyst", 3 tokens). Deliberately narrow:
190
+ * short real answers are legitimate, so no length heuristic. */
191
+ export function echoesToolName(text, toolNames) {
192
+ const normalize = (s) => s.replace(/[^a-z0-9]/gi, '').toLowerCase();
193
+ if (!text)
194
+ return false;
195
+ for (const name of toolNames)
196
+ if (normalize(name) === normalize(text))
197
+ return true;
198
+ return false;
199
+ }
200
+ // ── The turn ─────────────────────────────────────────────────────────────────
201
+ /**
202
+ * Runs one user message through a syndicate and returns what the user
203
+ * receives. Never throws for model or tool failures — those come back as
204
+ * `status: 'failed'` with an error. It throws only for programming errors
205
+ * (a session service that rejects, a config naming a route it does not
206
+ * declare), which a surface should treat as an internal error.
207
+ */
208
+ export async function runSyndicateTurn(opts) {
209
+ const { config } = opts;
210
+ // A model id given as a string resolves through ADK's LLMRegistry. Without
211
+ // the framework's adapters registered there, Gemini would be ADK's own
212
+ // class, which bypasses traceLlmGeneration: no max_steps, no cancel. A
213
+ // caller that resolves models itself (the A2A server) is left alone.
214
+ if (!opts.compile?.resolveModel)
215
+ registerAvailableProviders();
216
+ const control = createTurnControl({
217
+ maxLlmCalls: opts.maxLlmCalls ?? config.max_steps,
218
+ deadlineMs: opts.deadlineMs,
219
+ signal: opts.signal,
220
+ });
221
+ try {
222
+ return await runWithTurnControl(control, () => runTurnInner(opts, control));
223
+ }
224
+ finally {
225
+ control.dispose();
226
+ }
227
+ }
228
+ async function runTurnInner(opts, control) {
229
+ const { config, appName, userId, sessionId, sessionService } = opts;
230
+ const ev = opts.events ?? {};
231
+ // Nested references load from the definition's bundle when it has one
232
+ // (a registry version, ADR 0018 item 6), else from files.
233
+ const compileOpts = { ...opts.compile, loadNested: opts.compile?.loadNested ?? nestedLoader(config) };
234
+ const transform = opts.transformAgent ?? ((a) => a);
235
+ const subagentNames = new Set((config.subagents ?? []).map((s) => s.name));
236
+ const trace = opts.trace === false ? undefined : opts.trace ?? {};
237
+ const parts = opts.parts.map((p) => (typeof p === 'string' ? { text: p } : p));
238
+ const messageText = parts.map((p) => (typeof p.text === 'string' ? p.text : '')).join('\n');
239
+ const result = {
240
+ status: 'completed',
241
+ text: '',
242
+ relayFallback: false,
243
+ guardNotes: [],
244
+ resumedSession: false,
245
+ llmCalls: 0,
246
+ usage: { llmCalls: 0, inputTokens: 0, outputTokens: 0, thinkingTokens: 0 },
247
+ };
248
+ const finish = () => {
249
+ result.llmCalls = control.llmCalls;
250
+ result.usage = {
251
+ llmCalls: control.llmCalls,
252
+ inputTokens: control.inputTokens,
253
+ outputTokens: control.outputTokens,
254
+ thinkingTokens: control.thinkingTokens,
255
+ };
256
+ if (control.stopReason) {
257
+ result.stopReason = control.stopReason;
258
+ result.status = control.stopReason === 'canceled' ? 'canceled' : 'failed';
259
+ result.error = { code: stopCode(control.stopReason), message: stopMessage(control.stopReason, control) };
260
+ }
261
+ return result;
262
+ };
263
+ // ── Session ────────────────────────────────────────────────────────────────
264
+ const existing = await sessionService.getSession({ appName, userId, sessionId });
265
+ if (existing) {
266
+ result.resumedSession = true;
267
+ }
268
+ else {
269
+ await sessionService.createSession({ appName, userId, sessionId });
270
+ }
271
+ // A guard rewrites the answer after it is complete, so a guarded
272
+ // syndicate never streams: nothing may leave before the guard has read it.
273
+ const guarded = collectGuards(config, compileOpts.loadNested).length > 0;
274
+ // ── Approvals (ADR 0028) ───────────────────────────────────────────────────
275
+ // A message answering an approval resumes the agent that asked; the answer
276
+ // must name the request still open in this conversation.
277
+ const decision = approvalDecisionIn(parts);
278
+ let resuming;
279
+ if (decision) {
280
+ resuming = pendingApproval(existing?.events ?? []);
281
+ if (!resuming || resuming.id !== decision.id) {
282
+ result.status = 'failed';
283
+ result.error = { code: 'NO_PENDING_APPROVAL', message: `No approval ${decision.id} is waiting in this conversation.` };
284
+ return finish();
285
+ }
286
+ // The log names the call, not its arguments: they are user content.
287
+ ev.log?.(`✓ Approval ${decision.approved ? 'granted' : 'refused'}: ${resuming.agent} → ${resuming.tool}`);
288
+ }
289
+ /** After the answering run: is a gated call now waiting? */
290
+ const awaitingApproval = async (agentName) => {
291
+ const after = await sessionService.getSession({ appName, userId, sessionId });
292
+ const pending = pendingApproval(after?.events ?? []);
293
+ return pending && pending.agent === agentName ? pending : undefined;
294
+ };
295
+ const pause = (pending) => {
296
+ result.status = 'input-required';
297
+ result.approval = pending;
298
+ result.text = `Approval needed: ${describeApproval(pending)}.`;
299
+ ev.log?.(`⏸ Approval needed: ${pending.agent} → ${pending.tool}`);
300
+ return finish();
301
+ };
302
+ /** Run ONE agent against ONE session, under the turn's controls. */
303
+ const runAgent = async (params) => {
304
+ const runner = new Runner({
305
+ agent: params.agent,
306
+ appName,
307
+ sessionService: params.sessions,
308
+ ...(opts.memoryService ? { memoryService: opts.memoryService } : {}),
309
+ });
310
+ let stream = runner.runAsync({
311
+ userId,
312
+ sessionId: params.sid,
313
+ newMessage: { role: 'user', parts: params.userParts },
314
+ abortSignal: control.signal,
315
+ ...(opts.streaming ? { runConfig: { streamingMode: StreamingMode.SSE } } : {}),
316
+ });
317
+ if (trace) {
318
+ stream = traceAgentRun(stream, {
319
+ syndicateName: trace.syndicateName ?? config.syndicate_name ?? appName,
320
+ bindings: trace.bindings ?? config.variables ?? {},
321
+ input: params.userParts,
322
+ route: params.route,
323
+ // Identity: the CONVERSATION id, not the classifier's throwaway lane —
324
+ // every stage of one turn shares session and task ids.
325
+ sessionId,
326
+ userId,
327
+ taskId: trace.taskId,
328
+ stage: params.stage,
329
+ configHash: trace.configHash,
330
+ attributes: trace.attributes,
331
+ onSpanStart: trace.onSpanStart,
332
+ onEnd: () => ({
333
+ 'syndicate.relay_fallback': params.stage === 'delegate' && !!params.relayFallback?.(),
334
+ ...(control.stopReason ? { 'syndicate.stop_reason': control.stopReason } : {}),
335
+ 'syndicate.llm_calls': control.llmCalls,
336
+ }),
337
+ });
338
+ }
339
+ try {
340
+ return await drainAgentStream(stream, {
341
+ events: params.stage === 'classify' ? { ...ev, onEvent: undefined } : ev,
342
+ publishToolStatus: params.publishToolStatus,
343
+ subagentNames,
344
+ streamText: params.stage !== 'classify' && !guarded,
345
+ });
346
+ }
347
+ catch (err) {
348
+ // A provider call aborted by cancel / deadline surfaces as a thrown
349
+ // AbortError. That is a stop, not a crash.
350
+ if (control.stopReason) {
351
+ return emptyRun({ code: stopCode(control.stopReason), message: stopMessage(control.stopReason, control) });
352
+ }
353
+ throw err;
354
+ }
355
+ };
356
+ let answer;
357
+ if (isDispatchSyndicate(config)) {
358
+ // ══ PLAN ═════════════════════════════════════════════════════════════
359
+ // Deterministic overrides first: when the message itself decides the
360
+ // route there is nothing to classify, and the classifier call is skipped.
361
+ const warnings = [];
362
+ let resolution = resuming ? null : matchRouteOverride(messageText, config, warnings);
363
+ let decidedBy = 'override';
364
+ for (const w of warnings)
365
+ ev.warn?.(w);
366
+ if (resuming) {
367
+ if (!subagentNames.has(resuming.agent)) {
368
+ throw new Error(`Approval ${resuming.id} was raised by '${resuming.agent}', which is not a route of this syndicate.`);
369
+ }
370
+ resolution = { route: resuming.agent, reason: 'resuming an approval', fellBack: false, fallbackReason: '', viaOverride: false };
371
+ decidedBy = 'approval';
372
+ }
373
+ else if (resolution) {
374
+ ev.log?.(`⇄ Route pinned by override: ${resolution.route}`);
375
+ }
376
+ else if (opts.forceRoute) {
377
+ if (!subagentNames.has(opts.forceRoute)) {
378
+ throw new Error(`forceRoute '${opts.forceRoute}' is not a declared subagent`);
379
+ }
380
+ resolution = { route: opts.forceRoute, reason: 'forced by the caller', fellBack: false, fallbackReason: '', viaOverride: false };
381
+ decidedBy = 'forced';
382
+ }
383
+ else {
384
+ decidedBy = 'classifier';
385
+ // The classifier reads the SHARED transcript as an input digest and runs
386
+ // in a throwaway in-memory lane, so its JSON verdicts never enter the
387
+ // conversation the next specialist reads.
388
+ const routerAgent = transform(await compileGraph(config, compileOpts));
389
+ const routerSid = `${sessionId}::route`;
390
+ const routerSessions = new InMemorySessionService();
391
+ await routerSessions.createSession({ appName, userId, sessionId: routerSid });
392
+ const shared = await sessionService.getSession({ appName, userId, sessionId });
393
+ const digest = renderTranscriptDigest(shared?.events ?? []);
394
+ const routerParts = digest
395
+ ? [{ text: `${CLASSIFIER_PREAMBLE}${digest}\n\n--- MESSAGE TO CLASSIFY ---\n${messageText}` }]
396
+ : parts;
397
+ const plan = await runAgent({
398
+ agent: routerAgent,
399
+ sid: routerSid,
400
+ userParts: routerParts,
401
+ sessions: routerSessions,
402
+ stage: 'classify',
403
+ publishToolStatus: false,
404
+ });
405
+ result.classifier = plan;
406
+ if (control.stopReason)
407
+ return finish();
408
+ // Fail-static: a dead classifier costs the user a good route, never
409
+ // their answer. An empty payload resolves to default_route.
410
+ resolution = resolveRoute(plan.error ? '' : plan.text, config);
411
+ if (plan.error) {
412
+ ev.warn?.(`Router failed [${plan.error.code}] — defaulting to '${resolution.route}'.`);
413
+ }
414
+ else if (resolution.fellBack) {
415
+ ev.warn?.(`Routing fell back to '${resolution.route}': ${resolution.fallbackReason}`);
416
+ }
417
+ }
418
+ const routeCfg = (config.subagents ?? []).find((s) => s.name === resolution.route);
419
+ if (!routeCfg)
420
+ throw new Error(`Dispatch failed: no subagent named '${resolution.route}' is declared.`);
421
+ result.route = { ...resolution, decidedBy };
422
+ const routeNote = resolution.reason ? ` — ${resolution.reason}` : '';
423
+ ev.log?.(`⇄ Route: ${resolution.route}${routeNote}`);
424
+ ev.onProgress?.(`Routed to ${resolution.route}${routeNote}`);
425
+ // ══ DISPATCH ═════════════════════════════════════════════════════════
426
+ if (routeCfg.a2a_agent_url) {
427
+ // A remote route answers over A2A; its conversation id is derived from
428
+ // this one, so follow-ups reach the same remote conversation.
429
+ answer = await runRemoteRoute(routeCfg.name, routeCfg.a2a_agent_url, messageText, sessionId, control, ev);
430
+ }
431
+ else {
432
+ // The route answers directly in the SHARED session, read through a
433
+ // projection: ADK would otherwise render other agents' turns as user
434
+ // speech mixed with their tool payloads (lib/session/transcript.ts).
435
+ const routeAgent = transform(await compileSubagent(routeCfg, compileOpts));
436
+ answer = await runAgent({
437
+ agent: routeAgent,
438
+ sid: sessionId,
439
+ userParts: parts,
440
+ // Resuming an approval: the interrupted turn is replayed raw, so ADK
441
+ // finds the call the approval answers (ADR 0028).
442
+ sessions: new ProjectedSessionService(sessionService, routeCfg.name, resuming ? { rawFrom: (events) => interruptedTurnStart(events, resuming.id) } : {}),
443
+ stage: 'dispatch',
444
+ route: resolution,
445
+ publishToolStatus: true,
446
+ });
447
+ }
448
+ result.answer = answer;
449
+ if (answer.error || control.stopReason) {
450
+ result.status = 'failed';
451
+ result.failedStage = 'dispatch';
452
+ result.error = answer.error;
453
+ return finish();
454
+ }
455
+ if (!routeCfg.a2a_agent_url && routeCfg.require_approval?.length) {
456
+ const pending = await awaitingApproval(routeCfg.name);
457
+ if (pending)
458
+ return pause(pending);
459
+ }
460
+ result.text = answer.text;
461
+ if (!result.text) {
462
+ // Naming the route turns a blank reply into a lead (the XScout outage
463
+ // shape: the specialist ran and returned nothing because its provider
464
+ // rejected the call upstream).
465
+ ev.warn?.(`Route '${resolution.route}' produced no output.`);
466
+ result.text = `${resolution.route} returned no output — the server logs carry the upstream error.`;
467
+ }
468
+ }
469
+ else {
470
+ // ══ DELEGATE ═════════════════════════════════════════════════════════
471
+ // Subagents are AgentTools; the orchestrator relays the answer it got.
472
+ const orchestrator = transform(await compileGraph(config, compileOpts));
473
+ let drained;
474
+ answer = await runAgent({
475
+ agent: orchestrator,
476
+ sid: sessionId,
477
+ userParts: parts,
478
+ sessions: sessionService,
479
+ stage: 'delegate',
480
+ publishToolStatus: true,
481
+ relayFallback: () => !!drained && (!drained.text || echoesToolName(drained.text, drained.invokedToolNames)) && !!drained.lastToolResultText,
482
+ });
483
+ drained = answer;
484
+ result.answer = answer;
485
+ if (answer.error || control.stopReason) {
486
+ result.status = 'failed';
487
+ result.failedStage = 'delegate';
488
+ result.error = answer.error;
489
+ return finish();
490
+ }
491
+ if (config.orchestrator.require_approval?.length) {
492
+ const pending = await awaitingApproval(config.orchestrator.name);
493
+ if (pending)
494
+ return pause(pending);
495
+ }
496
+ result.text = answer.text;
497
+ // Failed-relay fallback: an orchestrator can botch the hop that relays a
498
+ // specialist's answer — STOP with no text, or the bare tool NAME as its
499
+ // text. Both discard a fully-formed result, so the last tool result is
500
+ // returned deterministically instead of retrying the relay.
501
+ const echoed = echoesToolName(result.text, answer.invokedToolNames);
502
+ if ((!result.text || echoed) && answer.lastToolResultText) {
503
+ ev.warn?.(`Orchestrator ${echoed ? `echoed the tool name ("${result.text}")` : 'emitted no text'} — `
504
+ + `relaying last tool result verbatim (${answer.lastToolResultText.length.toLocaleString()} chars).`);
505
+ result.text = answer.lastToolResultText;
506
+ result.relayFallback = true;
507
+ }
508
+ }
509
+ // ══ GUARDS ═══════════════════════════════════════════════════════════════
510
+ // Named in the syndicate's `guards:` list (and any nested syndicate's). They
511
+ // REWRITE rather than retry, on the answering turn's text with every tool
512
+ // result it produced. The classifier never reaches here.
513
+ const guardNames = collectGuards(config, compileOpts.loadNested);
514
+ if (result.text && guardNames.length) {
515
+ const inputs = answer.toolResultTexts;
516
+ for (const guard of resolveGuards(guardNames, (n) => ev.warn?.(`Unknown guard '${n}' — ignored`))) {
517
+ try {
518
+ const out = await guard.run(result.text, inputs);
519
+ // Always logged, notes or not: a guard that ran clean must be
520
+ // distinguishable from one that never ran.
521
+ ev.log?.(`⛨ ${guard.name}: checked ${result.text.length.toLocaleString()} chars against ${inputs.length} tool result${inputs.length === 1 ? '' : 's'} — ${out.notes.length} note${out.notes.length === 1 ? '' : 's'}`);
522
+ for (const note of out.notes) {
523
+ ev.log?.(`⛨ ${guard.name}: ${note}`);
524
+ ev.onProgress?.(`Guard ${guard.name}: ${note}`);
525
+ result.guardNotes.push(`${guard.name}: ${note}`);
526
+ }
527
+ result.text = out.text;
528
+ }
529
+ catch (guardErr) {
530
+ const msg = guardErr instanceof Error ? guardErr.message : String(guardErr);
531
+ ev.warn?.(`Guard '${guard.name}' failed (answer shipped unguarded): ${msg}`);
532
+ ev.onProgress?.(`Guard ${guard.name}: did not run (${msg})`);
533
+ result.guardNotes.push(`${guard.name}: did not run (${msg})`);
534
+ }
535
+ }
536
+ }
537
+ return finish();
538
+ }
539
+ /** A plan-dispatch route that is a remote A2A agent (`a2a_agent_url`). */
540
+ async function runRemoteRoute(name, url, messageText, sessionId, control, ev) {
541
+ ev.onProgress?.(`Invoking remote agent: ${name}`);
542
+ try {
543
+ const answer = await new RemoteA2AAgent(url).send(messageText, {
544
+ contextId: remoteContextId(sessionId, name),
545
+ signal: control.signal,
546
+ });
547
+ const run = emptyRun();
548
+ run.agents.add(name);
549
+ run.delegations.push(name);
550
+ if (answer.state === 'completed') {
551
+ run.text = answer.text;
552
+ }
553
+ else {
554
+ run.error = { code: `REMOTE_${answer.state.toUpperCase().replace(/-/g, '_')}`, message: remoteToolOutput(name, answer) };
555
+ }
556
+ return run;
557
+ }
558
+ catch (err) {
559
+ if (control.stopReason)
560
+ return emptyRun({ code: stopCode(control.stopReason), message: stopMessage(control.stopReason, control) });
561
+ return emptyRun({ code: 'REMOTE_UNREACHABLE', message: `${name}: ${err instanceof Error ? err.message : String(err)}` });
562
+ }
563
+ }
564
+ function emptyRun(error) {
565
+ return {
566
+ text: '',
567
+ lastToolResultText: '',
568
+ toolResultTexts: [],
569
+ invokedToolNames: new Set(),
570
+ toolCalls: [],
571
+ toolResponses: 0,
572
+ delegations: [],
573
+ agents: new Set(),
574
+ thoughts: '',
575
+ tokens: { input: 0, output: 0, thinking: 0 },
576
+ eventCount: 0,
577
+ ...(error ? { error } : {}),
578
+ };
579
+ }
580
+ /**
581
+ * Distils the conversation into long-term memory. Call it after the answer
582
+ * has been delivered — it never delays the reply, and a failure here never
583
+ * fails the turn. Returns false when there was nothing to ingest.
584
+ */
585
+ export async function ingestTurnMemory(params) {
586
+ const session = await params.sessionService.getSession({
587
+ appName: params.appName,
588
+ userId: params.userId,
589
+ sessionId: params.sessionId,
590
+ });
591
+ if (!session || session.events.length === 0)
592
+ return false;
593
+ await params.memoryService.addSessionToMemory(session, params.extractionRules, {
594
+ extractionModel: params.extractionModel,
595
+ });
596
+ return true;
597
+ }