melchizedek-agents 0.9.2 → 0.9.4
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/AGENT_SETUP.md +2 -2
- package/DOCUMENTATION.md +17 -24
- package/QUICKSTART.md +3 -2
- package/README.md +11 -9
- package/config/agents/examples/ares.yaml +0 -2
- package/config/agents/examples/augustin.yaml +0 -2
- package/config/agents/examples/cartographers.yaml +2 -5
- package/config/agents/examples/council.yaml +1 -1
- package/config/agents/examples/image_production.yaml +0 -1
- package/config/agents/examples/librarian.yaml +2 -4
- package/config/agents/examples/patient_advocate.yaml +2 -4
- package/config/agents/examples/scriptorium.yaml +2 -5
- package/config/agents/examples/style_council.yaml +2 -6
- package/config/agents/examples/syndicate.yaml +0 -3
- package/config/agents/examples/tutor.yaml +62 -32
- package/config/agents/syndicateSchema.yaml +4 -2
- package/dist/lib/compile.d.ts +64 -0
- package/dist/lib/compile.js +105 -0
- package/dist/lib/models/claudeLlm.js +34 -4
- package/dist/lib/models/gptLlm.js +6 -3
- package/dist/lib/models/ollamaLlm.d.ts +2 -0
- package/dist/lib/models/ollamaLlm.js +5 -1
- package/dist/lib/models/openAiCompatibleLlm.d.ts +49 -0
- package/dist/lib/models/openAiCompatibleLlm.js +233 -9
- package/dist/lib/models/registry.js +1 -1
- package/dist/lib/models/schemaNormalize.d.ts +9 -0
- package/dist/lib/models/schemaNormalize.js +34 -0
- package/dist/lib/observability/embeddings.d.ts +21 -0
- package/dist/lib/observability/embeddings.js +47 -0
- package/dist/lib/observability/lineage.d.ts +35 -0
- package/dist/lib/observability/lineage.js +96 -0
- package/dist/lib/observability/supabaseSpanExporter.d.ts +191 -18
- package/dist/lib/observability/supabaseSpanExporter.js +388 -80
- package/dist/lib/observability/tracer.d.ts +82 -0
- package/dist/lib/observability/tracer.js +348 -4
- package/dist/scripts/a2a_server.d.ts +10 -0
- package/dist/scripts/a2a_server.js +132 -75
- package/dist/scripts/syndicate_chat.js +41 -10
- package/package.json +8 -4
|
@@ -10,19 +10,46 @@ import { loadEnv } from "../lib/loadEnv.js";
|
|
|
10
10
|
import { DefaultRequestHandler, InMemoryTaskStore } from '@a2a-js/sdk/server';
|
|
11
11
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
12
12
|
import { agentCardHandler, jsonRpcHandler, restHandler, UserBuilder } from '@a2a-js/sdk/server/express';
|
|
13
|
-
import { Runner,
|
|
13
|
+
import { Runner, InMemorySessionService, getFunctionCalls, getFunctionResponses, setLogLevel, LogLevel } from '@google/adk';
|
|
14
14
|
import { loadSyndicate } from "../lib/loadSyndicate.js";
|
|
15
15
|
import { isDispatchSyndicate, matchRouteOverride, resolveRoute } from "../lib/dispatch.js";
|
|
16
16
|
import { traceAgentRun } from "../lib/observability/tracer.js";
|
|
17
|
+
import { configDigest } from "../lib/observability/lineage.js";
|
|
17
18
|
import { resolveModel } from "../lib/models/registry.js";
|
|
18
|
-
import {
|
|
19
|
-
import { createMcpTools } from "../lib/tools/mcpToolFactory.js";
|
|
19
|
+
import { compileGraph as compileGraphShared, compileSubagent as compileSubagentShared } from "../lib/compile.js";
|
|
20
20
|
import { hasSupabaseCredentials, createSupabaseServices } from "../lib/persistence/supabaseProvider.js";
|
|
21
21
|
import { ProjectedSessionService, renderTranscriptDigest } from "../lib/session/transcript.js";
|
|
22
22
|
const requestContextStorage = new AsyncLocalStorage();
|
|
23
23
|
const A2A_APP_NAME = 'melchizedek-a2a';
|
|
24
24
|
/** X-User-Id must be short and filesystem/key-safe; anything else is rejected. */
|
|
25
25
|
const SITE_USER_ID_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
26
|
+
/** X-Surface-* values obey the same charset rule, for the same reason. */
|
|
27
|
+
const SURFACE_VALUE_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
28
|
+
const SURFACE_HEADERS = [
|
|
29
|
+
['x-surface', 'name'],
|
|
30
|
+
['x-surface-guild', 'guild'],
|
|
31
|
+
['x-surface-channel', 'channel'],
|
|
32
|
+
['x-surface-user', 'user'],
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Root-span attributes for the caller's surface. Absent headers produce no
|
|
36
|
+
* attributes at all, so a caller that names nothing writes NULLs rather than
|
|
37
|
+
* a row full of 'unknown' — an unnamed surface and a surface named 'unknown'
|
|
38
|
+
* are different facts and the ledger should not conflate them.
|
|
39
|
+
*/
|
|
40
|
+
function surfaceAttributes(ctx) {
|
|
41
|
+
const s = ctx?.surface;
|
|
42
|
+
if (!s)
|
|
43
|
+
return {};
|
|
44
|
+
const attrs = { 'surface.name': s.name };
|
|
45
|
+
if (s.guild)
|
|
46
|
+
attrs['surface.guild'] = s.guild;
|
|
47
|
+
if (s.channel)
|
|
48
|
+
attrs['surface.channel'] = s.channel;
|
|
49
|
+
if (s.user)
|
|
50
|
+
attrs['surface.user'] = s.user;
|
|
51
|
+
return attrs;
|
|
52
|
+
}
|
|
26
53
|
/**
|
|
27
54
|
* Memory/session silo derivation.
|
|
28
55
|
* Base silo: a hash of the caller's API key (one bucket per credential).
|
|
@@ -105,16 +132,50 @@ function publishWorking(eventBus, taskId, contextId, text) {
|
|
|
105
132
|
final: false
|
|
106
133
|
});
|
|
107
134
|
}
|
|
135
|
+
/** One human-readable line naming WHY a turn failed, for the failed task's
|
|
136
|
+
* status message — the text the Discord surface renders verbatim. Provider
|
|
137
|
+
* errors sometimes arrive as a raw JSON blob (Gemini ApiError:
|
|
138
|
+
* `{"error":{"code":503,"message":"...","status":"UNAVAILABLE"}}`), so the
|
|
139
|
+
* inner message is dug out before falling back to the string itself. An
|
|
140
|
+
* empty message keeps the old pointer at the server logs. */
|
|
141
|
+
export function describeTurnError(error) {
|
|
142
|
+
let msg = (error.message ?? '').trim();
|
|
143
|
+
if (msg.startsWith('{')) {
|
|
144
|
+
try {
|
|
145
|
+
const parsed = JSON.parse(msg);
|
|
146
|
+
const inner = parsed?.error?.message ?? parsed?.message;
|
|
147
|
+
if (typeof inner === 'string' && inner.trim())
|
|
148
|
+
msg = inner.trim();
|
|
149
|
+
}
|
|
150
|
+
catch { /* not JSON — use as-is */ }
|
|
151
|
+
}
|
|
152
|
+
if (!msg)
|
|
153
|
+
return 'see server logs for details.';
|
|
154
|
+
return msg.length > 300 ? `${msg.slice(0, 297)}...` : msg;
|
|
155
|
+
}
|
|
108
156
|
// ── Native ADK AgentExecutor Bridge ───────────────────────────────────────
|
|
109
157
|
class SyndicateExecutor {
|
|
110
158
|
config;
|
|
111
159
|
sessionService;
|
|
112
160
|
memoryService;
|
|
161
|
+
/** Provenance stamp for every turn this executor serves (lineage.ts). */
|
|
162
|
+
configHash;
|
|
113
163
|
constructor(config, sessionService, memoryService) {
|
|
114
164
|
this.config = config;
|
|
115
165
|
this.sessionService = sessionService;
|
|
116
166
|
this.memoryService = memoryService;
|
|
117
167
|
}
|
|
168
|
+
configHashFor() {
|
|
169
|
+
if (!this.configHash) {
|
|
170
|
+
try {
|
|
171
|
+
this.configHash = configDigest(this.config, (ref) => loadSyndicate(ref));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
this.configHash = 'unhashable';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return this.configHash;
|
|
178
|
+
}
|
|
118
179
|
/**
|
|
119
180
|
* Run ONE agent for ONE turn against a named session, draining its event
|
|
120
181
|
* stream. DELEGATE mode calls this once (the orchestrator); PLAN-DISPATCH
|
|
@@ -138,16 +199,37 @@ class SyndicateExecutor {
|
|
|
138
199
|
newMessage: { role: 'user', parts: userParts },
|
|
139
200
|
...(this.config.max_steps !== undefined ? { maxSteps: this.config.max_steps } : {})
|
|
140
201
|
});
|
|
202
|
+
let combinedText = '';
|
|
203
|
+
let lastToolResultText = '';
|
|
204
|
+
const invokedToolNames = new Set();
|
|
205
|
+
// The DELEGATE relay-fallback predicate, evaluated by the tracer in its
|
|
206
|
+
// finally (after the last event) so the turn row records whether the
|
|
207
|
+
// orchestrator's relay failed. Same rule as the substitution below.
|
|
208
|
+
const normalizeName = (s) => s.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
209
|
+
const relayFallbackFired = () => {
|
|
210
|
+
const text = combinedText.trim();
|
|
211
|
+
const echoed = text.length > 0 && [...invokedToolNames].some((n) => normalizeName(n) === normalizeName(text));
|
|
212
|
+
return (!text || echoed) && !!lastToolResultText;
|
|
213
|
+
};
|
|
141
214
|
if (params.trace) {
|
|
142
215
|
stream = traceAgentRun(stream, {
|
|
143
216
|
syndicateName: this.config.syndicate_name || 'melchizedek-syndicate',
|
|
144
217
|
bindings: this.config.variables || {},
|
|
145
|
-
input: userParts
|
|
218
|
+
input: userParts,
|
|
219
|
+
route: params.route,
|
|
220
|
+
// Identity: the CONVERSATION id (contextId), not the classifier's
|
|
221
|
+
// throwaway lane — every stage of one turn shares session/task ids.
|
|
222
|
+
sessionId: contextId,
|
|
223
|
+
userId,
|
|
224
|
+
taskId,
|
|
225
|
+
stage: params.stage,
|
|
226
|
+
configHash: this.configHashFor(),
|
|
227
|
+
attributes: surfaceAttributes(requestContextStorage.getStore()),
|
|
228
|
+
onEnd: () => ({
|
|
229
|
+
'syndicate.relay_fallback': params.stage === 'delegate' && relayFallbackFired(),
|
|
230
|
+
}),
|
|
146
231
|
});
|
|
147
232
|
}
|
|
148
|
-
let combinedText = '';
|
|
149
|
-
let lastToolResultText = '';
|
|
150
|
-
const invokedToolNames = new Set();
|
|
151
233
|
let lastTokenTotal = 0;
|
|
152
234
|
let lastThinkingTokens = 0;
|
|
153
235
|
let error;
|
|
@@ -309,71 +391,19 @@ class SyndicateExecutor {
|
|
|
309
391
|
apiKey: authContext.apiKey,
|
|
310
392
|
defaultProvider: authContext.provider,
|
|
311
393
|
});
|
|
312
|
-
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const nestedConfig = loadSyndicate(subCfg.yaml_reference);
|
|
323
|
-
return compileGraph(nestedConfig, subCfg.name, subCfg.description);
|
|
324
|
-
}
|
|
325
|
-
const subTools = resolveTools(subCfg.tools);
|
|
326
|
-
if (subCfg.mcp_server_url) {
|
|
327
|
-
console.log(`[A2A] Loading MCP tools: ${subCfg.mcp_server_url}`);
|
|
328
|
-
const mcpTools = await createMcpTools(subCfg.mcp_server_url);
|
|
329
|
-
for (const mcpTool of mcpTools) {
|
|
330
|
-
if (!subTools.some(t => t.name === mcpTool.name)) {
|
|
331
|
-
subTools.push(mcpTool);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
return new LlmAgent({
|
|
336
|
-
name: subCfg.name,
|
|
337
|
-
description: subCfg.description,
|
|
338
|
-
model: resolveModelForRequest(subCfg.model),
|
|
339
|
-
instruction: subCfg.instruction,
|
|
340
|
-
tools: subTools.length > 0 ? subTools : undefined,
|
|
341
|
-
outputSchema: subCfg.outputSchema,
|
|
342
|
-
generateContentConfig: {
|
|
343
|
-
...subCfg.generateContentConfig,
|
|
344
|
-
toolConfig: {
|
|
345
|
-
...subCfg.generateContentConfig?.toolConfig,
|
|
346
|
-
includeServerSideToolInvocations: true
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
};
|
|
351
|
-
const compileGraph = async (configObj, overrideName, overrideDesc) => {
|
|
352
|
-
// PLAN-DISPATCH: the orchestrator gets NO subagent tools. ADK refuses
|
|
353
|
-
// to combine outputSchema with AgentTool delegation on one agent (see
|
|
354
|
-
// config/agents/critic.yaml), and the classifier needs the schema —
|
|
355
|
-
// code, not the model, performs the hand-off.
|
|
356
|
-
const compiledTools = isDispatchSyndicate(configObj)
|
|
357
|
-
? []
|
|
358
|
-
: await Promise.all((configObj.subagents || []).map(async (subCfg) => new AgentTool({ agent: await compileSubagent(subCfg) })));
|
|
359
|
-
const orchestratorDirectTools = resolveTools(configObj.orchestrator.tools);
|
|
360
|
-
compiledTools.push(...orchestratorDirectTools);
|
|
361
|
-
return new LlmAgent({
|
|
362
|
-
name: overrideName || configObj.orchestrator.name,
|
|
363
|
-
description: overrideDesc || configObj.orchestrator.description,
|
|
364
|
-
model: resolveModelForRequest(configObj.orchestrator.model),
|
|
365
|
-
instruction: configObj.orchestrator.instruction,
|
|
366
|
-
tools: compiledTools.length > 0 ? compiledTools : undefined,
|
|
367
|
-
outputSchema: configObj.orchestrator.outputSchema,
|
|
368
|
-
generateContentConfig: {
|
|
369
|
-
...configObj.orchestrator.generateContentConfig,
|
|
370
|
-
toolConfig: {
|
|
371
|
-
...configObj.orchestrator.generateContentConfig?.toolConfig,
|
|
372
|
-
includeServerSideToolInvocations: true
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
});
|
|
394
|
+
// Compile ADK nodes through the shared compiler (lib/compile.ts) — the
|
|
395
|
+
// observatory runs the same function, so the graph an eval measures is
|
|
396
|
+
// the graph this server serves. compileSubagent builds ONE runnable
|
|
397
|
+
// agent from a subagent entry; in DELEGATE mode compileGraph wraps it in
|
|
398
|
+
// an AgentTool, in PLAN-DISPATCH the same agent is run directly as the
|
|
399
|
+
// chosen route, so both methods dispatch to identical agents.
|
|
400
|
+
const compileOpts = {
|
|
401
|
+
resolveModel: resolveModelForRequest,
|
|
402
|
+
onUnknownTool: (name) => console.warn(`[A2A] WARNING: Unknown tool '${name}' — skipping.`),
|
|
403
|
+
log: (message) => console.log(`[A2A] ${message}`),
|
|
376
404
|
};
|
|
405
|
+
const compileSubagent = (subCfg) => compileSubagentShared(subCfg, compileOpts);
|
|
406
|
+
const compileGraph = (configObj) => compileGraphShared(configObj, compileOpts);
|
|
377
407
|
const ensureSession = async (sessionId) => {
|
|
378
408
|
const existing = await this.sessionService.getSession({
|
|
379
409
|
sessionId,
|
|
@@ -481,7 +511,8 @@ class SyndicateExecutor {
|
|
|
481
511
|
taskId,
|
|
482
512
|
contextId,
|
|
483
513
|
eventBus,
|
|
484
|
-
trace:
|
|
514
|
+
trace: true, // one row per classification: misroutes are diagnosable from stored data
|
|
515
|
+
stage: 'classify',
|
|
485
516
|
publishToolStatus: false, // a dispatch classifier holds no tools
|
|
486
517
|
sessionService: routerSessions
|
|
487
518
|
});
|
|
@@ -530,11 +561,13 @@ class SyndicateExecutor {
|
|
|
530
561
|
contextId,
|
|
531
562
|
eventBus,
|
|
532
563
|
trace: true,
|
|
564
|
+
stage: 'dispatch',
|
|
533
565
|
publishToolStatus: true,
|
|
566
|
+
route: resolution,
|
|
534
567
|
sessionService: new ProjectedSessionService(this.sessionService, routeCfg.name)
|
|
535
568
|
});
|
|
536
569
|
if (turn.error) {
|
|
537
|
-
failTask(`Error: [${turn.error.code}] ${resolution.route} failed to answer
|
|
570
|
+
failTask(`Error: [${turn.error.code}] ${resolution.route} failed to answer — ${describeTurnError(turn.error)}`);
|
|
538
571
|
return;
|
|
539
572
|
}
|
|
540
573
|
tokenInfo = turn.tokenInfo;
|
|
@@ -560,10 +593,11 @@ class SyndicateExecutor {
|
|
|
560
593
|
contextId,
|
|
561
594
|
eventBus,
|
|
562
595
|
trace: true,
|
|
596
|
+
stage: 'delegate',
|
|
563
597
|
publishToolStatus: true
|
|
564
598
|
});
|
|
565
599
|
if (turn.error) {
|
|
566
|
-
failTask(`Error: [${turn.error.code}] The agent run failed
|
|
600
|
+
failTask(`Error: [${turn.error.code}] The agent run failed — ${describeTurnError(turn.error)}`);
|
|
567
601
|
return;
|
|
568
602
|
}
|
|
569
603
|
tokenInfo = turn.tokenInfo;
|
|
@@ -809,7 +843,30 @@ export async function startServer(syndicateName = 'syndicate.yaml') {
|
|
|
809
843
|
}
|
|
810
844
|
siteUserId = rawUserId;
|
|
811
845
|
}
|
|
812
|
-
|
|
846
|
+
// X-Surface-* (optional): where this request came from, for telemetry
|
|
847
|
+
// only. Same validation as X-User-Id, and a bad value is refused rather
|
|
848
|
+
// than dropped — a silently ignored header makes a dashboard lie about
|
|
849
|
+
// coverage. `X-Surface` is required for the others to mean anything.
|
|
850
|
+
let surface;
|
|
851
|
+
for (const [header, field] of SURFACE_HEADERS) {
|
|
852
|
+
const raw = req.headers[header];
|
|
853
|
+
if (raw === undefined || raw === '')
|
|
854
|
+
continue;
|
|
855
|
+
if (!SURFACE_VALUE_PATTERN.test(raw)) {
|
|
856
|
+
res.status(400).json({ error: `Invalid ${header}: must match [A-Za-z0-9._-]{1,64}` });
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (field === 'name') {
|
|
860
|
+
surface = { name: raw };
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (!surface) {
|
|
864
|
+
res.status(400).json({ error: `${header} requires X-Surface to name the surface` });
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
surface[field] = raw;
|
|
868
|
+
}
|
|
869
|
+
requestContextStorage.run({ apiKey, provider, siteUserId, surface }, () => {
|
|
813
870
|
next();
|
|
814
871
|
});
|
|
815
872
|
});
|
|
@@ -5,7 +5,7 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { loadSyndicate, parseCliBindings } from "../lib/loadSyndicate.js";
|
|
6
6
|
import { traceAgentRun } from "../lib/observability/tracer.js";
|
|
7
7
|
// ADK Imports
|
|
8
|
-
import { LlmAgent, Runner, InMemorySessionService, getFunctionCalls, getFunctionResponses, AgentTool, setLogLevel, LogLevel } from '@google/adk';
|
|
8
|
+
import { LlmAgent, Runner, InMemorySessionService, getFunctionCalls, getFunctionResponses, AgentTool, setLogLevel, LogLevel, StreamingMode } from '@google/adk';
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
10
10
|
// Tool resolution (shared registry) + MCP factory
|
|
11
11
|
import { resolveTools as resolveNamedTools } from "../lib/toolRegistry.js";
|
|
@@ -94,6 +94,12 @@ function banner(config, persistence, bindings = {}, sessionId) {
|
|
|
94
94
|
}
|
|
95
95
|
async function main() {
|
|
96
96
|
loadEnv(import.meta.url);
|
|
97
|
+
// Interactive chat is for reading the agent, not its telemetry: the
|
|
98
|
+
// [OTEL_SPAN_JSON] lines are off here unless OTEL_CONSOLE_SPANS is set
|
|
99
|
+
// (in .env, which loadEnv() has just read, or in the shell). Spans still
|
|
100
|
+
// reach the in-process listeners and the Supabase sink either way.
|
|
101
|
+
if (process.env.OTEL_CONSOLE_SPANS === undefined)
|
|
102
|
+
process.env.OTEL_CONSOLE_SPANS = 'false';
|
|
97
103
|
// ── Register LLM providers ───────────────────────────────────────────────
|
|
98
104
|
// WHY: Must run AFTER loadEnv() so that API keys from .env are available.
|
|
99
105
|
// One call registers every adapter whose credentials exist (Ollama needs
|
|
@@ -119,14 +125,17 @@ async function main() {
|
|
|
119
125
|
break;
|
|
120
126
|
}
|
|
121
127
|
}
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
// ONLY what the caller actually asked for on the command line.
|
|
129
|
+
//
|
|
130
|
+
// WHY nothing is seeded here: loadSyndicate merges these OVER a syndicate's
|
|
131
|
+
// own `variables:` block, so a hardcoded default is not a fallback — it is
|
|
132
|
+
// an override applied to every syndicate. `headline_count: 3` used to ride
|
|
133
|
+
// along into unrelated agents (a Tutor turn announced it in the banner and
|
|
134
|
+
// recorded it in syndicate.bindings telemetry) while silently beating the
|
|
135
|
+
// 5 that syndicate.yaml declares for itself. Universal tokens belong to
|
|
136
|
+
// defaultBindings() in lib/loadSyndicate.ts, which the loader applies UNDER
|
|
137
|
+
// the YAML — that is where current_date lives, timezone-pinned.
|
|
138
|
+
const mergedBindings = parseCliBindings(argv);
|
|
130
139
|
// Load the syndicate definition
|
|
131
140
|
const config = loadSyndicate(syndicateFile, {
|
|
132
141
|
bindings: mergedBindings,
|
|
@@ -162,6 +171,9 @@ async function main() {
|
|
|
162
171
|
// people) — otherwise every CLI session shares the 'local-user' bucket.
|
|
163
172
|
const SESSION_USER_ID = process.env.MELCHIZEDEK_USER_ID?.trim() || 'local-user';
|
|
164
173
|
const SESSION_ID = randomUUID();
|
|
174
|
+
// Live token-by-token output. Off gives one block per turn — useful when
|
|
175
|
+
// piping a transcript, or for an agent whose reply is structured JSON.
|
|
176
|
+
const STREAM_REPLIES = process.env.CHAT_STREAMING !== 'false';
|
|
165
177
|
// ── Detect persistence mode ──────────────────────────────────────────────
|
|
166
178
|
const persistence = detectPersistenceConfig(config.memory_system);
|
|
167
179
|
// ── Tool resolver ─────────────────────────────────────────────────────────
|
|
@@ -327,7 +339,13 @@ async function main() {
|
|
|
327
339
|
let stream = runner.runAsync({
|
|
328
340
|
userId: SESSION_USER_ID,
|
|
329
341
|
sessionId: SESSION_ID,
|
|
330
|
-
newMessage: { role: 'user', parts: [{ text: trimmed }] }
|
|
342
|
+
newMessage: { role: 'user', parts: [{ text: trimmed }] },
|
|
343
|
+
// SSE makes the adapters emit display-only partials as tokens
|
|
344
|
+
// land, so thinking and the reply appear as they are produced
|
|
345
|
+
// rather than in one block when the call returns.
|
|
346
|
+
runConfig: {
|
|
347
|
+
streamingMode: STREAM_REPLIES ? StreamingMode.SSE : StreamingMode.NONE
|
|
348
|
+
}
|
|
331
349
|
});
|
|
332
350
|
stream = traceAgentRun(stream, {
|
|
333
351
|
syndicateName: config.syndicate_name || 'melchizedek-syndicate',
|
|
@@ -335,7 +353,13 @@ async function main() {
|
|
|
335
353
|
input: trimmed
|
|
336
354
|
});
|
|
337
355
|
let currentMode = 'none';
|
|
356
|
+
// Under SSE the reply arrives as partials and is then REPEATED whole
|
|
357
|
+
// on the final event — the one ADK persists to session history. Show
|
|
358
|
+
// the partials, skip the repeat. With streaming off nothing is
|
|
359
|
+
// partial, so this stays false and every event prints as before.
|
|
360
|
+
let streamedText = false;
|
|
338
361
|
for await (const event of stream) {
|
|
362
|
+
const isPartial = event.partial === true;
|
|
339
363
|
const calls = getFunctionCalls(event);
|
|
340
364
|
if (calls && calls.length > 0) {
|
|
341
365
|
for (const call of calls) {
|
|
@@ -371,6 +395,8 @@ async function main() {
|
|
|
371
395
|
process.stdout.write(c.dim + part.text + c.reset);
|
|
372
396
|
}
|
|
373
397
|
else if (part.text) {
|
|
398
|
+
if (!isPartial && streamedText)
|
|
399
|
+
continue;
|
|
374
400
|
if (currentMode === 'thinking') {
|
|
375
401
|
console.log(`\n\n${c.magenta}${c.bold}${event.author}${c.reset} › `);
|
|
376
402
|
currentMode = 'text';
|
|
@@ -380,6 +406,8 @@ async function main() {
|
|
|
380
406
|
currentMode = 'text';
|
|
381
407
|
}
|
|
382
408
|
process.stdout.write(part.text);
|
|
409
|
+
if (isPartial)
|
|
410
|
+
streamedText = true;
|
|
383
411
|
}
|
|
384
412
|
else if (part.inlineData) {
|
|
385
413
|
// WHY: Image generation models return binary data as base64-encoded
|
|
@@ -396,6 +424,9 @@ async function main() {
|
|
|
396
424
|
}
|
|
397
425
|
}
|
|
398
426
|
}
|
|
427
|
+
// A complete event ends the turn; the next one starts fresh.
|
|
428
|
+
if (!isPartial)
|
|
429
|
+
streamedText = false;
|
|
399
430
|
}
|
|
400
431
|
console.log('\n');
|
|
401
432
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "melchizedek-agents",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "A multi-model, multi-agent orchestration framework on Google ADK
|
|
3
|
+
"version": "0.9.4",
|
|
4
|
+
"description": "A multi-model, multi-agent orchestration framework on Google ADK \u2014 YAML-defined syndicates, Supabase session persistence, pgvector long-term memory. Companion repo to the lyceumagents.com curriculum.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"//": "adk + genai are pinned exact in dev: newer genai minors change response part shapes and silently break the chat printer. Bump deliberately, verify a live run, then update the pin. adk itself is a PEER dependency
|
|
7
|
+
"//": "adk + genai are pinned exact in dev: newer genai minors change response part shapes and silently break the chat printer. Bump deliberately, verify a live run, then update the pin. adk itself is a PEER dependency \u2014 the consumer's app owns the ADK instance so the model registry is a singleton.",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=22.0.0"
|
|
10
10
|
},
|
|
@@ -67,7 +67,10 @@
|
|
|
67
67
|
"start:a2a": "node --disable-warning=DEP0040 --experimental-strip-types scripts/a2a_server.ts",
|
|
68
68
|
"test": "node --disable-warning=DEP0040 --experimental-strip-types --test tests/**/*.test.ts",
|
|
69
69
|
"memory": "node --disable-warning=DEP0040 --experimental-strip-types scripts/memory_admin.ts",
|
|
70
|
-
"db:purge": "node --disable-warning=DEP0040 --experimental-strip-types scripts/purge_supabase.ts"
|
|
70
|
+
"db:purge": "node --disable-warning=DEP0040 --experimental-strip-types scripts/purge_supabase.ts",
|
|
71
|
+
"telemetry:stats": "node --disable-warning=DEP0040 --experimental-strip-types scripts/telemetry_admin.ts stats",
|
|
72
|
+
"telemetry:prune": "node --disable-warning=DEP0040 --experimental-strip-types scripts/telemetry_admin.ts prune",
|
|
73
|
+
"telemetry:replay": "node --disable-warning=DEP0040 --experimental-strip-types scripts/telemetry_admin.ts replay"
|
|
71
74
|
},
|
|
72
75
|
"peerDependencies": {
|
|
73
76
|
"@google/adk": "^1.3.0"
|
|
@@ -78,6 +81,7 @@
|
|
|
78
81
|
"@google/genai": "2.2.0",
|
|
79
82
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
80
83
|
"@opentelemetry/api": "^1.9.1",
|
|
84
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.221.0",
|
|
81
85
|
"@opentelemetry/resources": "^2.7.1",
|
|
82
86
|
"@opentelemetry/sdk-trace-base": "^2.7.1",
|
|
83
87
|
"@opentelemetry/sdk-trace-node": "^2.7.1",
|