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.
Files changed (35) hide show
  1. package/AGENT_SETUP.md +2 -2
  2. package/DOCUMENTATION.md +7 -5
  3. package/QUICKSTART.md +4 -3
  4. package/README.md +13 -13
  5. package/config/agents/examples/augustin.yaml +2 -2
  6. package/config/agents/examples/cartographers.yaml +4 -4
  7. package/config/agents/examples/council.yaml +1 -1
  8. package/config/agents/examples/critic.yaml +3 -3
  9. package/config/agents/examples/delegation.yaml +3 -3
  10. package/config/agents/examples/hierarchical.yaml +3 -3
  11. package/config/agents/examples/image_production.yaml +2 -2
  12. package/config/agents/examples/librarian.yaml +3 -3
  13. package/config/agents/examples/patient_advocate.yaml +3 -3
  14. package/config/agents/examples/scriptorium.yaml +5 -5
  15. package/config/agents/examples/style_council.yaml +7 -7
  16. package/config/agents/examples/tutor.yaml +63 -33
  17. package/dist/lib/config.d.ts +3 -3
  18. package/dist/lib/config.js +3 -3
  19. package/dist/lib/models/claudeLlm.js +9 -3
  20. package/dist/lib/models/gptLlm.js +1 -1
  21. package/dist/lib/models/ollamaLlm.js +1 -1
  22. package/dist/lib/models/openAiCompatibleLlm.d.ts +47 -0
  23. package/dist/lib/models/openAiCompatibleLlm.js +219 -4
  24. package/dist/lib/models/registry.js +1 -1
  25. package/dist/lib/observability/supabaseSpanExporter.d.ts +5 -0
  26. package/dist/lib/observability/supabaseSpanExporter.js +14 -3
  27. package/dist/lib/observability/tracer.d.ts +8 -0
  28. package/dist/lib/observability/tracer.js +67 -4
  29. package/dist/lib/tools/inspectImageTool.js +1 -1
  30. package/dist/lib/wiki/agentRun.d.ts +1 -1
  31. package/dist/lib/wiki/agentRun.js +1 -1
  32. package/dist/scripts/a2a_server.d.ts +10 -0
  33. package/dist/scripts/a2a_server.js +75 -3
  34. package/dist/scripts/syndicate_chat.js +41 -10
  35. 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. See server logs for details.`);
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. See server logs for details.`);
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
- requestContextStorage.run({ apiKey, provider, siteUserId }, () => {
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
- // Define syndicate variable bindings
123
- const bindings = {
124
- headline_count: 3,
125
- current_date: new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
126
- };
127
- // Merge CLI overrides on top (--bind/--bindings flags win)
128
- const cliBindings = parseCliBindings(argv);
129
- const mergedBindings = { ...bindings, ...cliBindings };
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",
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",