melchizedek-agents 0.9.3 → 0.9.5
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 +7 -5
- package/QUICKSTART.md +4 -3
- package/README.md +13 -13
- package/config/agents/examples/augustin.yaml +2 -2
- package/config/agents/examples/cartographers.yaml +4 -4
- package/config/agents/examples/council.yaml +1 -1
- package/config/agents/examples/critic.yaml +3 -3
- package/config/agents/examples/delegation.yaml +3 -3
- package/config/agents/examples/hierarchical.yaml +3 -3
- package/config/agents/examples/image_production.yaml +2 -2
- package/config/agents/examples/librarian.yaml +3 -3
- package/config/agents/examples/patient_advocate.yaml +3 -3
- package/config/agents/examples/scriptorium.yaml +5 -5
- package/config/agents/examples/style_council.yaml +7 -7
- package/config/agents/examples/tutor.yaml +63 -33
- package/dist/lib/config.d.ts +3 -3
- package/dist/lib/config.js +3 -3
- package/dist/lib/models/claudeLlm.js +9 -3
- package/dist/lib/models/gptLlm.js +1 -1
- package/dist/lib/models/ollamaLlm.js +1 -1
- package/dist/lib/models/openAiCompatibleLlm.d.ts +47 -0
- package/dist/lib/models/openAiCompatibleLlm.js +219 -4
- package/dist/lib/models/registry.js +1 -1
- package/dist/lib/observability/supabaseSpanExporter.d.ts +5 -0
- package/dist/lib/observability/supabaseSpanExporter.js +14 -3
- package/dist/lib/observability/tracer.d.ts +8 -0
- package/dist/lib/observability/tracer.js +67 -4
- package/dist/lib/tools/inspectImageTool.js +1 -1
- package/dist/lib/wiki/agentRun.d.ts +1 -1
- package/dist/lib/wiki/agentRun.js +1 -1
- package/dist/scripts/a2a_server.d.ts +10 -0
- package/dist/scripts/a2a_server.js +75 -3
- package/dist/scripts/syndicate_chat.js +41 -10
- package/package.json +1 -1
|
@@ -23,6 +23,33 @@ 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,6 +132,27 @@ 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;
|
|
@@ -176,6 +224,7 @@ class SyndicateExecutor {
|
|
|
176
224
|
taskId,
|
|
177
225
|
stage: params.stage,
|
|
178
226
|
configHash: this.configHashFor(),
|
|
227
|
+
attributes: surfaceAttributes(requestContextStorage.getStore()),
|
|
179
228
|
onEnd: () => ({
|
|
180
229
|
'syndicate.relay_fallback': params.stage === 'delegate' && relayFallbackFired(),
|
|
181
230
|
}),
|
|
@@ -518,7 +567,7 @@ class SyndicateExecutor {
|
|
|
518
567
|
sessionService: new ProjectedSessionService(this.sessionService, routeCfg.name)
|
|
519
568
|
});
|
|
520
569
|
if (turn.error) {
|
|
521
|
-
failTask(`Error: [${turn.error.code}] ${resolution.route} failed to answer
|
|
570
|
+
failTask(`Error: [${turn.error.code}] ${resolution.route} failed to answer — ${describeTurnError(turn.error)}`);
|
|
522
571
|
return;
|
|
523
572
|
}
|
|
524
573
|
tokenInfo = turn.tokenInfo;
|
|
@@ -548,7 +597,7 @@ class SyndicateExecutor {
|
|
|
548
597
|
publishToolStatus: true
|
|
549
598
|
});
|
|
550
599
|
if (turn.error) {
|
|
551
|
-
failTask(`Error: [${turn.error.code}] The agent run failed
|
|
600
|
+
failTask(`Error: [${turn.error.code}] The agent run failed — ${describeTurnError(turn.error)}`);
|
|
552
601
|
return;
|
|
553
602
|
}
|
|
554
603
|
tokenInfo = turn.tokenInfo;
|
|
@@ -794,7 +843,30 @@ export async function startServer(syndicateName = 'syndicate.yaml') {
|
|
|
794
843
|
}
|
|
795
844
|
siteUserId = rawUserId;
|
|
796
845
|
}
|
|
797
|
-
|
|
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 }, () => {
|
|
798
870
|
next();
|
|
799
871
|
});
|
|
800
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "melchizedek-agents",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
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",
|