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.
- package/.env.example +272 -0
- package/AGENT_SETUP.md +39 -23
- package/CHANGELOG.md +623 -0
- package/DOCUMENTATION.md +429 -109
- package/QUICKSTART.md +92 -18
- package/README.md +45 -5
- package/config/agents/README.md +46 -1
- package/config/agents/examples/ares.yaml +117 -40
- package/config/agents/examples/assistant.yaml +159 -0
- package/config/agents/examples/augustin.yaml +154 -51
- package/config/agents/examples/cartographers.yaml +91 -23
- package/config/agents/examples/claude.yaml +45 -6
- package/config/agents/examples/council.yaml +23 -8
- package/config/agents/examples/critic.yaml +57 -21
- package/config/agents/examples/delegation.yaml +76 -9
- package/config/agents/examples/hierarchical.yaml +55 -8
- package/config/agents/examples/image_production.yaml +54 -16
- package/config/agents/examples/librarian.yaml +70 -14
- package/config/agents/examples/model_zoo.yaml +105 -29
- package/config/agents/examples/patient_advocate.yaml +102 -34
- package/config/agents/examples/research.yaml +250 -0
- package/config/agents/examples/scribe.yaml +242 -0
- package/config/agents/examples/scriptorium.yaml +87 -24
- package/config/agents/examples/style_council.yaml +31 -9
- package/config/agents/examples/syndicate.yaml +29 -10
- package/config/agents/examples/tutor.yaml +10 -3
- package/config/agents/syndicateSchema.yaml +44 -9
- package/config/agents/templates/README.md +58 -0
- package/config/agents/templates/account_memory.yaml +153 -0
- package/config/agents/templates/case_desk.yaml +188 -0
- package/config/agents/templates/conversational.yaml +110 -0
- package/config/agents/templates/draft_review.yaml +130 -0
- package/config/agents/templates/intake_extractor.yaml +152 -0
- package/config/agents/templates/research_brief.yaml +169 -0
- package/config/agents/templates/research_desk.yaml +152 -0
- package/config/agents/templates/review_panel.yaml +188 -0
- package/config/agents/templates/support_triage.yaml +273 -0
- package/config/agents/templates/systems_operator.yaml +157 -0
- package/db/hardening.sql +202 -0
- package/db/memory_v2.sql +77 -0
- package/db/migrations/0001_base.sql +167 -0
- package/db/migrations/0002_erase_scope.sql +172 -0
- package/db/migrations/0003_postgres_storage.sql +58 -0
- package/db/migrations/0004_usage.sql +94 -0
- package/db/migrations/0005_agent_registry.sql +200 -0
- package/db/migrations/0006_task_leases.sql +25 -0
- package/db/migrations/0007_memory_commit.sql +134 -0
- package/db/migrations/0008_memory_retention.sql +37 -0
- package/db/migrations/0009_task_queue.sql +51 -0
- package/db/migrations/0010_erase_complete.sql +180 -0
- package/db/telemetry.sql +401 -0
- package/demo/README.md +77 -0
- package/demo/a2a_demo.mjs +95 -0
- package/dist/lib/a2a/app.d.ts +253 -0
- package/dist/lib/a2a/app.js +751 -0
- package/dist/lib/a2a/executor.d.ts +167 -0
- package/dist/lib/a2a/executor.js +548 -0
- package/dist/lib/a2a/identity.d.ts +122 -0
- package/dist/lib/a2a/identity.js +254 -0
- package/dist/lib/a2a/policy.d.ts +110 -0
- package/dist/lib/a2a/policy.js +178 -0
- package/dist/lib/a2a/redisLimits.d.ts +26 -0
- package/dist/lib/a2a/redisLimits.js +31 -0
- package/dist/lib/a2a/remoteAgent.d.ts +91 -0
- package/dist/lib/a2a/remoteAgent.js +268 -0
- package/dist/lib/a2a/turnLock.d.ts +28 -0
- package/dist/lib/a2a/turnLock.js +76 -0
- package/dist/lib/compile.d.ts +11 -1
- package/dist/lib/compile.js +109 -8
- package/dist/lib/config.d.ts +7 -7
- package/dist/lib/config.js +7 -7
- package/dist/lib/doctor.d.ts +145 -0
- package/dist/lib/doctor.js +517 -0
- package/dist/lib/grounding.d.ts +2 -2
- package/dist/lib/grounding.js +2 -2
- package/dist/lib/guards/index.d.ts +37 -0
- package/dist/lib/guards/index.js +66 -0
- package/dist/lib/guards/science.d.ts +117 -0
- package/dist/lib/guards/science.js +444 -0
- package/dist/lib/index.d.ts +37 -2
- package/dist/lib/index.js +35 -4
- package/dist/lib/loadEnv.d.ts +23 -11
- package/dist/lib/loadEnv.js +77 -29
- package/dist/lib/loadSyndicate.d.ts +97 -5
- package/dist/lib/loadSyndicate.js +176 -76
- package/dist/lib/memory/erase.d.ts +24 -0
- package/dist/lib/memory/erase.js +47 -0
- package/dist/lib/memory/namespace.d.ts +37 -0
- package/dist/lib/memory/namespace.js +74 -0
- package/dist/lib/memory/providers.d.ts +99 -0
- package/dist/lib/memory/providers.js +240 -0
- package/dist/lib/memory/store.d.ts +74 -0
- package/dist/lib/memory/store.js +108 -0
- package/dist/lib/memory/supabaseMemoryService.d.ts +69 -9
- package/dist/lib/memory/supabaseMemoryService.js +239 -125
- package/dist/lib/models/capabilities.d.ts +103 -0
- package/dist/lib/models/capabilities.js +291 -0
- package/dist/lib/models/claudeLlm.d.ts +10 -1
- package/dist/lib/models/claudeLlm.js +57 -27
- package/dist/lib/models/endpoints.d.ts +100 -0
- package/dist/lib/models/endpoints.js +282 -0
- package/dist/lib/models/gateway.d.ts +107 -0
- package/dist/lib/models/gateway.js +157 -0
- package/dist/lib/models/gatewayLlm.d.ts +57 -0
- package/dist/lib/models/gatewayLlm.js +105 -0
- package/dist/lib/models/gptLlm.d.ts +51 -2
- package/dist/lib/models/gptLlm.js +169 -26
- package/dist/lib/models/grokLlm.d.ts +19 -10
- package/dist/lib/models/grokLlm.js +29 -14
- package/dist/lib/models/ollamaLlm.d.ts +26 -0
- package/dist/lib/models/ollamaLlm.js +43 -1
- package/dist/lib/models/openAiCompatibleLlm.d.ts +67 -2
- package/dist/lib/models/openAiCompatibleLlm.js +200 -37
- package/dist/lib/models/providerMap.js +9 -2
- package/dist/lib/models/registry.d.ts +42 -5
- package/dist/lib/models/registry.js +140 -29
- package/dist/lib/models/retry.d.ts +146 -0
- package/dist/lib/models/retry.js +305 -0
- package/dist/lib/models/schemaNormalize.d.ts +20 -0
- package/dist/lib/models/schemaNormalize.js +43 -0
- package/dist/lib/net/addressGuard.d.ts +60 -0
- package/dist/lib/net/addressGuard.js +216 -0
- package/dist/lib/observability/embeddings.d.ts +11 -2
- package/dist/lib/observability/embeddings.js +18 -14
- package/dist/lib/observability/metrics.d.ts +36 -0
- package/dist/lib/observability/metrics.js +86 -0
- package/dist/lib/observability/redact.d.ts +29 -0
- package/dist/lib/observability/redact.js +102 -0
- package/dist/lib/observability/supabaseSpanExporter.d.ts +2 -0
- package/dist/lib/observability/supabaseSpanExporter.js +22 -6
- package/dist/lib/observability/tracer.d.ts +10 -0
- package/dist/lib/observability/tracer.js +70 -3
- package/dist/lib/persistence/supabaseProvider.d.ts +11 -0
- package/dist/lib/persistence/supabaseProvider.js +32 -6
- package/dist/lib/registry.d.ts +87 -0
- package/dist/lib/registry.js +146 -0
- package/dist/lib/runtime/approvals.d.ts +44 -0
- package/dist/lib/runtime/approvals.js +67 -0
- package/dist/lib/runtime/syndicateTurn.d.ts +233 -0
- package/dist/lib/runtime/syndicateTurn.js +597 -0
- package/dist/lib/runtime/turnControl.d.ts +90 -0
- package/dist/lib/runtime/turnControl.js +131 -0
- package/dist/lib/session/supabaseSessionService.js +1 -1
- package/dist/lib/session/transcript.d.ts +9 -1
- package/dist/lib/session/transcript.js +15 -7
- package/dist/lib/skills.d.ts +50 -0
- package/dist/lib/skills.js +138 -0
- package/dist/lib/storage/postgres/index.d.ts +98 -0
- package/dist/lib/storage/postgres/index.js +146 -0
- package/dist/lib/storage/postgres/memoryStore.d.ts +11 -0
- package/dist/lib/storage/postgres/memoryStore.js +82 -0
- package/dist/lib/storage/postgres/sessionService.d.ts +51 -0
- package/dist/lib/storage/postgres/sessionService.js +225 -0
- package/dist/lib/storage/postgres/taskQueue.d.ts +22 -0
- package/dist/lib/storage/postgres/taskQueue.js +101 -0
- package/dist/lib/storage/postgres/taskStore.d.ts +55 -0
- package/dist/lib/storage/postgres/taskStore.js +168 -0
- package/dist/lib/storage/schema.d.ts +27 -0
- package/dist/lib/storage/schema.js +50 -0
- package/dist/lib/storage/schemaVersion.d.ts +25 -0
- package/dist/lib/storage/schemaVersion.js +46 -0
- package/dist/lib/syndicateSchema.d.ts +275 -0
- package/dist/lib/syndicateSchema.js +534 -0
- package/dist/lib/toolRegistry.d.ts +15 -0
- package/dist/lib/toolRegistry.js +59 -10
- package/dist/lib/tools/collectionsSearchTool.js +1 -1
- package/dist/lib/tools/mcpServe.d.ts +45 -0
- package/dist/lib/tools/mcpServe.js +88 -0
- package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
- package/dist/lib/tools/mcpToolFactory.js +41 -21
- package/dist/lib/tools/science/corrections.d.ts +42 -0
- package/dist/lib/tools/science/corrections.js +115 -0
- package/dist/lib/tools/science/europepmc.d.ts +51 -0
- package/dist/lib/tools/science/europepmc.js +127 -0
- package/dist/lib/tools/science/http.d.ts +31 -0
- package/dist/lib/tools/science/http.js +104 -0
- package/dist/lib/tools/science/openalex.d.ts +30 -0
- package/dist/lib/tools/science/openalex.js +81 -0
- package/dist/lib/tools/science/trials.d.ts +58 -0
- package/dist/lib/tools/science/trials.js +203 -0
- package/dist/lib/tools/science/types.d.ts +69 -0
- package/dist/lib/tools/science/types.js +48 -0
- package/dist/lib/tools/scienceTools.d.ts +100 -0
- package/dist/lib/tools/scienceTools.js +277 -0
- package/dist/lib/tools/taskTools.d.ts +204 -0
- package/dist/lib/tools/taskTools.js +374 -0
- package/dist/lib/tools/toolContract.d.ts +17 -4
- package/dist/lib/tools/toolContract.js +13 -5
- package/dist/lib/tools/webExtractTool.d.ts +13 -10
- package/dist/lib/tools/webExtractTool.js +23 -55
- package/dist/lib/tools/webSearchTool.d.ts +2 -1
- package/dist/lib/tools/webSearchTool.js +4 -3
- package/dist/lib/tools/wikiTools.d.ts +2 -2
- package/dist/lib/tools/wikiTools.js +12 -12
- package/dist/lib/tools/xApiSearchTool.d.ts +194 -0
- package/dist/lib/tools/xApiSearchTool.js +583 -0
- package/dist/lib/tools/xSearchTool.js +1 -1
- package/dist/lib/wiki/agentRun.js +1 -1
- package/dist/lib/wiki/builder.js +21 -6
- package/dist/lib/wiki/dive.js +2 -2
- package/dist/lib/wiki/entities.d.ts +2 -2
- package/dist/lib/wiki/entities.js +7 -7
- package/dist/lib/wiki/extract.js +5 -1
- package/dist/lib/wiki/fill.js +5 -5
- package/dist/lib/wiki/format.d.ts +7 -5
- package/dist/lib/wiki/format.js +2 -2
- package/dist/lib/wiki/graph.js +2 -2
- package/dist/lib/wiki/lint.js +2 -2
- package/dist/lib/wiki/markdown.d.ts +1 -1
- package/dist/lib/wiki/markdown.js +6 -1
- package/dist/lib/wiki/pipeline.js +2 -2
- package/dist/lib/wiki/vault.js +2 -2
- package/dist/scripts/a2a_server.d.ts +17 -11
- package/dist/scripts/a2a_server.js +405 -1028
- package/dist/scripts/assistant_worker.d.ts +33 -0
- package/dist/scripts/assistant_worker.js +193 -0
- package/dist/scripts/db.d.ts +26 -0
- package/dist/scripts/db.js +179 -0
- package/dist/scripts/doctor.d.ts +23 -0
- package/dist/scripts/doctor.js +90 -0
- package/dist/scripts/init.d.ts +43 -0
- package/dist/scripts/init.js +152 -0
- package/dist/scripts/registry.d.ts +25 -0
- package/dist/scripts/registry.js +182 -0
- package/dist/scripts/skills_install.d.ts +7 -0
- package/dist/scripts/skills_install.js +137 -0
- package/dist/scripts/syndicate_chat.js +159 -276
- package/package.json +90 -16
- package/skills/README.md +74 -0
- package/skills/briefs/README.md +16 -0
- package/skills/briefs/_shared.md +16 -0
- package/skills/briefs/melchizedek-author.md +60 -0
- package/skills/briefs/melchizedek-memory.md +47 -0
- package/skills/briefs/melchizedek-models.md +47 -0
- package/skills/briefs/melchizedek-scribe.md +37 -0
- package/skills/briefs/melchizedek-serve.md +56 -0
- package/skills/briefs/melchizedek.md +69 -0
- package/skills/briefs/skills-readme.md +33 -0
- package/skills/melchizedek/SKILL.md +159 -0
- package/skills/melchizedek-author/SKILL.md +99 -0
- package/skills/melchizedek-author/templates/minimal.yaml +60 -0
- package/skills/melchizedek-memory/SKILL.md +138 -0
- package/skills/melchizedek-models/SKILL.md +137 -0
- package/skills/melchizedek-scribe/SKILL.md +72 -0
- package/skills/melchizedek-scribe/templates/brief.md +27 -0
- 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
|
+
}
|