melchizedek-agents 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/.env.example +19 -0
  2. package/AGENT_SETUP.md +3 -1
  3. package/CHANGELOG.md +373 -0
  4. package/DOCUMENTATION.md +193 -21
  5. package/ONBOARDING.md +378 -0
  6. package/QUICKSTART.md +15 -2
  7. package/README.md +26 -3
  8. package/config/agents/examples/connectors.yaml +82 -0
  9. package/config/agents/examples/council.yaml +1 -1
  10. package/config/agents/examples/critic.yaml +8 -8
  11. package/config/agents/examples/image_production.yaml +4 -3
  12. package/config/agents/syndicate.schema.json +389 -33
  13. package/config/agents/syndicateSchema.yaml +18 -6
  14. package/config/agents/templates/intake_extractor.yaml +3 -2
  15. package/config/agents/templates/systems_operator.yaml +5 -2
  16. package/dist/lib/a2a/app.d.ts +12 -0
  17. package/dist/lib/a2a/app.js +25 -11
  18. package/dist/lib/a2a/executor.js +14 -6
  19. package/dist/lib/a2a/oauthSetup.d.ts +16 -0
  20. package/dist/lib/a2a/oauthSetup.js +77 -8
  21. package/dist/lib/agentDialect.d.ts +18 -8
  22. package/dist/lib/agentDialect.js +36 -19
  23. package/dist/lib/chatgpt/adapter.d.ts +50 -0
  24. package/dist/lib/chatgpt/adapter.js +107 -0
  25. package/dist/lib/chatgpt/cli.d.ts +23 -0
  26. package/dist/lib/chatgpt/cli.js +77 -0
  27. package/dist/lib/chatgpt/oauth.d.ts +103 -0
  28. package/dist/lib/chatgpt/oauth.js +448 -0
  29. package/dist/lib/chatgpt/state.d.ts +98 -0
  30. package/dist/lib/chatgpt/state.js +145 -0
  31. package/dist/lib/chatgpt/store.d.ts +24 -0
  32. package/dist/lib/chatgpt/store.js +94 -0
  33. package/dist/lib/compile.d.ts +38 -7
  34. package/dist/lib/compile.js +97 -62
  35. package/dist/lib/compileNative.d.ts +11 -3
  36. package/dist/lib/compileNative.js +18 -1
  37. package/dist/lib/dispatch.js +6 -2
  38. package/dist/lib/doctor.d.ts +107 -0
  39. package/dist/lib/doctor.js +217 -7
  40. package/dist/lib/loadSyndicate.d.ts +15 -0
  41. package/dist/lib/mcp/server.d.ts +195 -0
  42. package/dist/lib/mcp/server.js +564 -0
  43. package/dist/lib/memory/supabaseMemoryService.js +2 -2
  44. package/dist/lib/models/adapterResolver.js +6 -1
  45. package/dist/lib/models/capabilities.d.ts +9 -0
  46. package/dist/lib/models/capabilities.js +24 -0
  47. package/dist/lib/models/chatCompletionsAdapter.d.ts +11 -3
  48. package/dist/lib/models/chatCompletionsAdapter.js +19 -5
  49. package/dist/lib/models/claudeAdapter.js +4 -0
  50. package/dist/lib/models/contract.d.ts +5 -2
  51. package/dist/lib/models/gateway.d.ts +3 -2
  52. package/dist/lib/models/gateway.js +8 -4
  53. package/dist/lib/models/gatewayAdapter.d.ts +7 -1
  54. package/dist/lib/models/gatewayAdapter.js +8 -0
  55. package/dist/lib/models/geminiAdapter.js +8 -2
  56. package/dist/lib/models/genaiMapping.d.ts +8 -4
  57. package/dist/lib/models/genaiMapping.js +23 -9
  58. package/dist/lib/models/gptAdapter.d.ts +4 -2
  59. package/dist/lib/models/gptAdapter.js +11 -4
  60. package/dist/lib/models/grokAdapter.d.ts +3 -1
  61. package/dist/lib/models/grokAdapter.js +11 -3
  62. package/dist/lib/models/kimiAdapter.d.ts +2 -1
  63. package/dist/lib/models/kimiAdapter.js +3 -3
  64. package/dist/lib/models/reasoning.d.ts +30 -2
  65. package/dist/lib/models/reasoning.js +56 -5
  66. package/dist/lib/onboarding.d.ts +135 -0
  67. package/dist/lib/onboarding.js +578 -0
  68. package/dist/lib/runtime/approvals.d.ts +2 -0
  69. package/dist/lib/runtime/approvals.js +4 -3
  70. package/dist/lib/runtime/credentials.d.ts +6 -0
  71. package/dist/lib/runtime/native/agentLoop.d.ts +5 -2
  72. package/dist/lib/runtime/native/agentLoop.js +2 -0
  73. package/dist/lib/runtime/native/checkpoint.d.ts +12 -5
  74. package/dist/lib/runtime/native/checkpoint.js +13 -19
  75. package/dist/lib/runtime/native/delegate.d.ts +82 -1
  76. package/dist/lib/runtime/native/delegate.js +157 -2
  77. package/dist/lib/runtime/native/interrupts.d.ts +88 -5
  78. package/dist/lib/runtime/native/interrupts.js +220 -30
  79. package/dist/lib/runtime/syndicateTurn.d.ts +4 -2
  80. package/dist/lib/runtime/syndicateTurn.js +324 -77
  81. package/dist/lib/runtime/turnControl.d.ts +41 -0
  82. package/dist/lib/session/transcript.d.ts +9 -0
  83. package/dist/lib/session/transcript.js +20 -1
  84. package/dist/lib/skills.d.ts +18 -0
  85. package/dist/lib/skills.js +73 -1
  86. package/dist/lib/storage/postgres/index.d.ts +6 -1
  87. package/dist/lib/storage/postgres/index.js +1 -1
  88. package/dist/lib/storage/postgres/taskQueue.d.ts +7 -3
  89. package/dist/lib/storage/postgres/taskQueue.js +10 -3
  90. package/dist/lib/syndicateSchema.d.ts +184 -9
  91. package/dist/lib/syndicateSchema.js +143 -29
  92. package/dist/lib/tools/auth.d.ts +13 -1
  93. package/dist/lib/tools/auth.js +10 -2
  94. package/dist/lib/tools/credentialHosts.d.ts +88 -0
  95. package/dist/lib/tools/credentialHosts.js +165 -0
  96. package/dist/lib/tools/credentialUses.d.ts +98 -0
  97. package/dist/lib/tools/credentialUses.js +120 -0
  98. package/dist/lib/tools/generateImageTool.js +2 -1
  99. package/dist/lib/tools/inspectImageTool.js +1 -1
  100. package/dist/lib/tools/mcpToolFactory.d.ts +29 -1
  101. package/dist/lib/tools/mcpToolFactory.js +72 -26
  102. package/dist/lib/tools/mcpTransports.d.ts +12 -0
  103. package/dist/lib/tools/mcpTransports.js +11 -0
  104. package/dist/lib/tools/oauthConsent.d.ts +18 -3
  105. package/dist/lib/tools/oauthConsent.js +57 -15
  106. package/dist/lib/tools/oauthDiscovery.d.ts +202 -0
  107. package/dist/lib/tools/oauthDiscovery.js +616 -0
  108. package/dist/lib/tools/oauthHosts.d.ts +2 -0
  109. package/dist/lib/tools/oauthHosts.js +2 -2
  110. package/dist/lib/tools/oauthTools.d.ts +54 -13
  111. package/dist/lib/tools/oauthTools.js +106 -23
  112. package/dist/lib/tools/openapiTools.d.ts +7 -0
  113. package/dist/lib/tools/openapiTools.js +49 -3
  114. package/dist/lib/tools/taskTools.d.ts +32 -2
  115. package/dist/lib/tools/taskTools.js +93 -33
  116. package/dist/lib/tools/xApiSearchTool.js +2 -2
  117. package/dist/lib/workflow/turn.d.ts +22 -3
  118. package/dist/lib/workflow/turn.js +151 -31
  119. package/dist/lib/workflow.d.ts +10 -7
  120. package/dist/lib/workflow.js +10 -7
  121. package/dist/scripts/a2a_server.d.ts +2 -2
  122. package/dist/scripts/a2a_server.js +19 -2
  123. package/dist/scripts/assistant_worker.d.ts +2 -0
  124. package/dist/scripts/assistant_worker.js +24 -5
  125. package/dist/scripts/doctor.d.ts +4 -2
  126. package/dist/scripts/doctor.js +6 -4
  127. package/dist/scripts/mcp_server.d.ts +12 -0
  128. package/dist/scripts/mcp_server.js +345 -0
  129. package/dist/scripts/setup.d.ts +25 -0
  130. package/dist/scripts/setup.js +181 -0
  131. package/dist/scripts/skills_install.d.ts +3 -1
  132. package/dist/scripts/skills_install.js +17 -2
  133. package/dist/scripts/syndicate_chat.js +12 -3
  134. package/dist/scripts/yaml_codemod.d.ts +12 -2
  135. package/dist/scripts/yaml_codemod.js +115 -33
  136. package/package.json +8 -2
  137. package/skills/README.md +11 -2
  138. package/skills/briefs/_shared.md +1 -1
  139. package/skills/briefs/melchizedek-models.md +4 -2
  140. package/skills/briefs/skills-readme.md +3 -1
  141. package/skills/melchizedek-models/SKILL.md +6 -2
  142. package/skills/melchizedek-onboard/SKILL.md +71 -0
  143. package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
  144. package/skills/melchizedek-onboard-keys/SKILL.md +47 -0
  145. package/skills/melchizedek-onboard-local/SKILL.md +41 -0
  146. package/skills/melchizedek-onboard-serve/SKILL.md +43 -0
@@ -33,7 +33,10 @@
33
33
  * read-modify-write, not a lock. A running job's step checkpoints
34
34
  * (ADR 0113) sit in a sidecar, <store>.checkpoints.json, keyed by id;
35
35
  * every write of the store drops those of jobs no longer running or
36
- * queued.
36
+ * queued. A checkpoint above the cap (TaskBackendOptions.
37
+ * checkpointMaxBytes, default 5 MiB of JSON) is not saved, on either
38
+ * store: the job keeps its previous one and one line names the job id
39
+ * and the sizes.
37
40
  * - With Postgres (postgresStorage().taskQueue, migration 0009) each
38
41
  * caller has its own list, scoped by the caller the tool call carries
39
42
  * (the A2A server's scope key), and any number of workers claim jobs
@@ -54,6 +57,43 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from '
54
57
  import { dirname, join, resolve } from 'node:path';
55
58
  import { z } from 'zod';
56
59
  import { defineTool } from './toolContract.js';
60
+ /**
61
+ * The largest checkpoint a backend stores by default: 5 MiB of serialized
62
+ * JSON (ADR 0113). A save above the cap is skipped and the job's previous
63
+ * checkpoint kept, so a huge run resumes from an older step boundary instead
64
+ * of failing or bloating the table.
65
+ */
66
+ export const DEFAULT_CHECKPOINT_MAX_BYTES = 5 * 1024 * 1024;
67
+ /** The worker's variable that moves the cap, in bytes (scripts/assistant_worker.ts). */
68
+ export const CHECKPOINT_MAX_BYTES_ENV = 'MELCHIZEDEK_CHECKPOINT_MAX_BYTES';
69
+ const validCap = (n) => typeof n === 'number' && Number.isSafeInteger(n) && n > 0;
70
+ /** The cap a variable's value sets: a positive whole number of bytes, else the default. */
71
+ export function checkpointMaxBytesSetting(raw) {
72
+ const value = raw?.trim() ?? '';
73
+ if (!/^\d+$/.test(value))
74
+ return DEFAULT_CHECKPOINT_MAX_BYTES;
75
+ const n = Number(value);
76
+ return validCap(n) ? n : DEFAULT_CHECKPOINT_MAX_BYTES;
77
+ }
78
+ /**
79
+ * The checkpoint serialized once, as the backend stores it, or null when it
80
+ * is above the cap: then one line is logged with the job id and the sizes
81
+ * only (the checkpoint carries the user's text and tool results), and the
82
+ * caller keeps the previous checkpoint.
83
+ */
84
+ export function checkpointJson(job, checkpoint, options = {}) {
85
+ const cap = validCap(options.checkpointMaxBytes) ? options.checkpointMaxBytes : DEFAULT_CHECKPOINT_MAX_BYTES;
86
+ const json = JSON.stringify(checkpoint);
87
+ // One UTF-16 unit is at most three UTF-8 bytes: a short string is under the cap without counting.
88
+ if (json.length * 3 <= cap)
89
+ return json;
90
+ const bytes = Buffer.byteLength(json, 'utf8');
91
+ if (bytes <= cap)
92
+ return json;
93
+ const log = options.log ?? ((line) => console.warn(`[tasks] ${line}`));
94
+ log(`checkpoint for job ${job.id} not saved: ${bytes} bytes is over the ${cap}-byte cap; the previous checkpoint is kept`);
95
+ return null;
96
+ }
57
97
  /** Records kept at most; finished ones are pruned oldest-first to make room. */
58
98
  export const MAX_TASKS = 500;
59
99
  /** A worker result is stored up to this length, then cut with a marker. */
@@ -79,13 +119,17 @@ function readStore(path = taskStorePath()) {
79
119
  }
80
120
  return { version: 1, next_id: parsed.next_id, tasks: parsed.tasks };
81
121
  }
82
- /** Atomic JSON write: temp file + rename, so a reader never sees half a file. */
83
- function writeJson(value, path) {
122
+ /** Atomic write: temp file + rename, so a reader never sees half a file. */
123
+ function writeText(text, path) {
84
124
  mkdirSync(dirname(path), { recursive: true });
85
125
  const tmp = `${path}.${process.pid}.tmp`;
86
- writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
126
+ writeFileSync(tmp, text);
87
127
  renameSync(tmp, path);
88
128
  }
129
+ /** Atomic JSON write. */
130
+ function writeJson(value, path) {
131
+ writeText(`${JSON.stringify(value, null, 2)}\n`, path);
132
+ }
89
133
  function writeStore(store, path = taskStorePath()) {
90
134
  writeJson(store, path);
91
135
  }
@@ -250,35 +294,51 @@ export function applyInterrupted(t) {
250
294
  t.status = 'queued';
251
295
  return 'requeued';
252
296
  }
253
- /** The single-user JSON file (the default backend). */
254
- export const fileTaskBackend = {
255
- read: async () => readStore(),
256
- mutate: async (_owner, change) => mutate(change),
257
- claimNext: async () => {
258
- const job = claimNextJob();
259
- return job ? { ...job, owner: '' } : null;
260
- },
261
- // No leases on the single-user file: the claim holds while the record is running.
262
- renew: async (_worker, job) => readStore().tasks.find((t) => t.id === job.id)?.status === 'running',
263
- finish: async (job, outcome) => finishJob(job.id, outcome),
264
- recover: async () => recoverInterruptedJobs(),
265
- saveCheckpoint: async (_worker, job, checkpoint) => {
266
- const path = taskStorePath();
267
- const record = readStore(path).tasks.find((t) => t.id === job.id);
268
- if (!record || record.kind !== 'background' || record.status !== 'running')
269
- return false;
270
- const entries = readCheckpoints(path);
271
- entries[job.id] = { checkpoint, at: now() };
272
- writeJson(entries, taskCheckpointPath(path));
273
- return true;
274
- },
275
- loadCheckpoint: async (job) => {
276
- const path = taskStorePath();
277
- if (!keepsCheckpoint(readStore(path).tasks.find((t) => t.id === job.id)))
278
- return null;
279
- return readCheckpoints(path)[job.id]?.checkpoint ?? null;
280
- },
281
- };
297
+ /**
298
+ * The sidecar with one job's checkpoint set from its serialized JSON, which
299
+ * is spliced in as it is rather than parsed and serialized again. The other
300
+ * entries are written compact; both shapes read back the same.
301
+ */
302
+ function writeCheckpointEntry(storePath, id, json) {
303
+ const { [id]: _replaced, ...rest } = readCheckpoints(storePath);
304
+ const others = JSON.stringify(rest);
305
+ const entry = `${JSON.stringify(id)}:{"checkpoint":${json},"at":${JSON.stringify(now())}}`;
306
+ writeText(`${others === '{}' ? `{${entry}}` : `${others.slice(0, -1)},${entry}}`}\n`, taskCheckpointPath(storePath));
307
+ }
308
+ /** The single-user JSON file, its checkpoints capped by `options` (the default backend is fileTaskBackend). */
309
+ export function fileTaskBackendWith(options = {}) {
310
+ return {
311
+ read: async () => readStore(),
312
+ mutate: async (_owner, change) => mutate(change),
313
+ claimNext: async () => {
314
+ const job = claimNextJob();
315
+ return job ? { ...job, owner: '' } : null;
316
+ },
317
+ // No leases on the single-user file: the claim holds while the record is running.
318
+ renew: async (_worker, job) => readStore().tasks.find((t) => t.id === job.id)?.status === 'running',
319
+ finish: async (job, outcome) => finishJob(job.id, outcome),
320
+ recover: async () => recoverInterruptedJobs(),
321
+ saveCheckpoint: async (_worker, job, checkpoint) => {
322
+ const path = taskStorePath();
323
+ const record = readStore(path).tasks.find((t) => t.id === job.id);
324
+ if (!record || record.kind !== 'background' || record.status !== 'running')
325
+ return false;
326
+ // Over the cap: nothing written, the previous checkpoint kept, the claim still held.
327
+ const json = checkpointJson(job, checkpoint, options);
328
+ if (json !== null)
329
+ writeCheckpointEntry(path, job.id, json);
330
+ return true;
331
+ },
332
+ loadCheckpoint: async (job) => {
333
+ const path = taskStorePath();
334
+ if (!keepsCheckpoint(readStore(path).tasks.find((t) => t.id === job.id)))
335
+ return null;
336
+ return readCheckpoints(path)[job.id]?.checkpoint ?? null;
337
+ },
338
+ };
339
+ }
340
+ /** The single-user JSON file (the default backend), checkpoints capped at DEFAULT_CHECKPOINT_MAX_BYTES. */
341
+ export const fileTaskBackend = fileTaskBackendWith();
282
342
  let activeBackend = fileTaskBackend;
283
343
  /** Plugs in where tasks live (postgresStorage().taskQueue); default the JSON file. */
284
344
  export function setTaskBackend(backend) {
@@ -442,7 +442,7 @@ async function lookupPost(id, token, input, deps) {
442
442
  const picked = wantImages ? pickImages(posts, imageMax(deps.env)) : [];
443
443
  const readings = picked.length ? await readImages(picked, deps) : new Map();
444
444
  const imagesRead = [...readings.values()].filter((r) => r.ok).length;
445
- console.log(`[x_api_search] lookup ${id} → ${posts.length} post (≈$${PRICE.postRead.toFixed(3)}), ${imagesRead}/${picked.length} image(s) transcribed on ${visionModel(deps.env)}${imageFailures(readings)}`);
445
+ console.log(`[x_api_search] lookup → ${posts.length} post (≈$${PRICE.postRead.toFixed(3)}), ${imagesRead}/${picked.length} image(s) transcribed on ${visionModel(deps.env)}${imageFailures(readings)}`);
446
446
  const photos = (posts[0].media ?? []).filter((m) => m.type === 'photo').length;
447
447
  return [
448
448
  `${head} — the post itself, as the API returns it.${photos ? ` ${photos} photo${photos === 1 ? '' : 's'} attached, ${imagesRead} transcribed beneath it.` : ''} For the replies and quotes around it, search conversation_id:${id}.`,
@@ -523,7 +523,7 @@ export async function runXApiSearch(input, deps = DEFAULT_DEPS) {
523
523
  const picked = wantImages ? pickImages(posts, imageMax(deps.env)) : [];
524
524
  const readings = picked.length ? await readImages(picked, deps) : new Map();
525
525
  const imagesRead = [...readings.values()].filter((r) => r.ok).length;
526
- console.log(`[x_api_search] "${query}" → ${posts.length} post(s) (≈$${(max * PRICE.postRead).toFixed(3)} at the ${max}-slot ceiling), ` +
526
+ console.log(`[x_api_search] search (${query.length} chars) → ${posts.length} post(s) (≈$${(max * PRICE.postRead).toFixed(3)} at the ${max}-slot ceiling), ` +
527
527
  `${imagesRead}/${picked.length} image(s) transcribed on ${visionModel(deps.env)}${imageFailures(readings)}`);
528
528
  return renderBlock({ ...outcome, ok: true, posts }, readings, new Set(picked.map((p) => p.media.key)), imagesRead);
529
529
  }
@@ -63,6 +63,7 @@
63
63
  */
64
64
  import type { ModelAdapter } from '../models/contract.ts';
65
65
  import type { TurnEvent } from '../runtime/events.ts';
66
+ import type { WorkflowSubagentEnd } from '../runtime/native/delegate.ts';
66
67
  import type { MemoryService } from '../runtime/memoryService.ts';
67
68
  import type { NativeAgent } from '../runtime/native/request.ts';
68
69
  import type { SelfCorrection } from '../runtime/native/selfCorrection.ts';
@@ -93,15 +94,33 @@ export interface NativeWorkflowParams {
93
94
  selfCorrection?: SelfCorrection;
94
95
  /** The run's tool credentials, pinned to its app (ADR 0072). */
95
96
  credentials?: Pick<CredentialStore, 'get'>;
96
- /** The agent nodes that are a nested workflow syndicate, by YAML name (ADR 0106). */
97
- workflows?: ReadonlyMap<string, NestedWorkflow>;
97
+ /** The agent nodes that are a nested workflow syndicate (ADR 0106) or a nested dispatch syndicate (ADR 0120), by YAML name. */
98
+ workflows?: ReadonlyMap<string, NestedWorkflow | NestedRun>;
98
99
  }
99
100
  /** A workflow syndicate run as a node of another (ADR 0106): its graph, its agents, its tool nodes' lookup, and its own nested nodes. */
100
101
  export interface NestedWorkflow {
101
102
  graph: WorkflowGraph;
102
103
  agents: ReadonlyMap<string, NativeAgent>;
103
104
  resolveTool: (name: string) => unknown;
104
- workflows?: ReadonlyMap<string, NestedWorkflow>;
105
+ workflows?: ReadonlyMap<string, NestedWorkflow | NestedRun>;
106
+ }
107
+ /** Where a node's own run goes: its child session, the message, the turn's signal. */
108
+ export interface NestedRunParams {
109
+ sessions: SessionService;
110
+ appName: string;
111
+ userId: string;
112
+ sessionId: string;
113
+ userParts: unknown[];
114
+ signal?: AbortSignal;
115
+ }
116
+ /**
117
+ * A node that runs as something other than a graph (ADR 0120): a nested
118
+ * dispatch syndicate, its own turn (lib/runtime/native/delegate.ts
119
+ * nestedDispatchWalk). It runs where a nested workflow's walk runs, on the
120
+ * same child session, and ends as a walk ends.
121
+ */
122
+ export interface NestedRun {
123
+ walk(run: NestedRunParams): AsyncGenerator<TurnEvent, WorkflowSubagentEnd | undefined>;
105
124
  }
106
125
  /** How the walk ended. */
107
126
  export interface NativeWorkflowEnd {
@@ -62,7 +62,10 @@
62
62
  * carries, never instructions this module acts on.
63
63
  */
64
64
  import { randomUUID } from 'node:crypto';
65
- import { createTurnEvent } from '../runtime/events.js';
65
+ import { APPROVAL_REQUEST } from '../runtime/approvals.js';
66
+ import { createTurnEvent, getFunctionCalls } from '../runtime/events.js';
67
+ import { entrySession } from '../runtime/native/delegate.js';
68
+ import { INPUT_REQUEST } from '../workflowConfig.js';
66
69
  import { traceNodeExecution, traceToolNodeCall, traceWorkflowInvocation } from '../runtime/native/telemetry.js';
67
70
  import { TEMP_STATE_PREFIX } from '../runtime/sessions.js';
68
71
  import { NodeStoppedError, agentNodeRuntime, nodeInputContent } from './agentNode.js';
@@ -78,41 +81,150 @@ function lastText(event) {
78
81
  .filter((t) => t)
79
82
  .join('\n');
80
83
  }
84
+ /** The requests a walk's interrupts are, as a node raises them: an approval request or an input request. */
85
+ const RAISED = new Set([APPROVAL_REQUEST, INPUT_REQUEST]);
86
+ /** The latest request call, by id, among `ids` that `events` hold (an agent's or a node's, never the user's). */
87
+ function requestCalls(events, ids, author) {
88
+ const wanted = new Set(ids);
89
+ const calls = new Map();
90
+ for (let i = events.length - 1; i >= 0 && calls.size < wanted.size; i--) {
91
+ const event = events[i];
92
+ if (!event.author || event.author === 'user' || (author !== undefined && event.author !== author))
93
+ continue;
94
+ for (const call of getFunctionCalls(event)) {
95
+ if (call.id && call.name && RAISED.has(call.name) && wanted.has(call.id) && !calls.has(call.id))
96
+ calls.set(call.id, call);
97
+ }
98
+ }
99
+ return calls;
100
+ }
101
+ /**
102
+ * Whether a resumed walk's answer to `call` answers it: a decision for an
103
+ * approval request (a ToolConfirmation, or ADK's `{ response: <json> }`
104
+ * form, as lib/workflow/agentNode.ts reads one), any answer for an input
105
+ * request.
106
+ */
107
+ function answers(call, value) {
108
+ if (value === undefined || !call)
109
+ return false;
110
+ if (call.name !== APPROVAL_REQUEST)
111
+ return true;
112
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
113
+ return false;
114
+ const answer = value;
115
+ return typeof answer.confirmed === 'boolean' || typeof answer.response === 'string';
116
+ }
117
+ /** The part that carries `value` down to the nested walk, as the answer to `call`: its explicit reply, under the request's name. */
118
+ function answerPart(call, value) {
119
+ const response = call.name === APPROVAL_REQUEST ? value : { result: value };
120
+ return { functionResponse: { id: call.id, name: call.name, response } };
121
+ }
122
+ /**
123
+ * A node whose answered interrupts an earlier resume already carried down
124
+ * (another node's answer was the one since): the output it stored after
125
+ * the last event that raised them, else undefined.
126
+ */
127
+ function outputAfterRaise(events, path, ids) {
128
+ const open = new Set(ids);
129
+ let start = -1;
130
+ events.forEach((event, i) => {
131
+ if (event.nodeInfo?.path === path && (event.longRunningToolIds ?? []).some((id) => open.has(id)))
132
+ start = i;
133
+ });
134
+ if (start === -1)
135
+ return undefined;
136
+ for (let i = events.length - 1; i > start; i--) {
137
+ const event = events[i];
138
+ if (event.nodeInfo?.path === path && event.output !== undefined && !event.partial)
139
+ return event.output;
140
+ }
141
+ return undefined;
142
+ }
143
+ /**
144
+ * The node's pause on its caller's walk (ADR 0119): every request its own
145
+ * walk waits on, raised again as one event of the node's (a copy of each
146
+ * call, stamped with the node's path, the ids in `longRunningToolIds`, the
147
+ * node's input recorded for the resume), so the caller's walk pauses on
148
+ * the same ids, its resume reruns the node with their answers, and the
149
+ * turn finds the request (lib/runtime/native/interrupts.ts follows it down
150
+ * by id to the node that asked). Carries the walk's state writes so far.
151
+ */
152
+ function raiseAgain(name, run, invocationId, calls, stateDelta) {
153
+ const event = createTurnEvent({
154
+ author: name,
155
+ invocationId,
156
+ content: { role: 'model', parts: calls.map((call) => ({ functionCall: structuredClone(call) })) },
157
+ ...(Object.keys(stateDelta).length ? { actions: { stateDelta } } : {}),
158
+ });
159
+ event.longRunningToolIds = calls.map((call) => call.id);
160
+ enrichNodeEvent(event, run, { invocationId });
161
+ event.actions = { ...event.actions, agentState: { ...(event.actions?.agentState ?? {}), input: run.input } };
162
+ return event;
163
+ }
81
164
  /**
82
165
  * Runs `workflow` as the node `run` names (ADR 0106), as a delegated nested
83
166
  * workflow runs (ADR 0098): the whole graph walked on the child session
84
- * under the node's name (created from the caller's state the first time,
85
- * `temp:` keys dropped, and kept), the node's input as its message, its last
86
- * yielded event's text the node's output. The caller stores one event for
87
- * the node, carrying that output and the walk's state writes, so a resumed
88
- * walk completes the node from it. A walk that ends paused is refused by
89
- * name: a pause cannot reach the caller's walk yet (WS6-2).
167
+ * filed under the walk's app name and the node's behind its kind
168
+ * (`<walk's app>/node:<node>`, entryAppName, ADR 0119, ADR 0120; the one
169
+ * under an older key is continued when it ran there before), created from the caller's state the first time, `temp:` keys
170
+ * dropped, and kept; the node's input as its message, its last yielded
171
+ * event's text the node's output. The caller stores one event for the
172
+ * node, carrying that output and the walk's state writes, so a resumed
173
+ * walk completes the node from it.
174
+ *
175
+ * A walk that ends paused (an ask_user node, a gated agent node, a node of
176
+ * its own that is a nested workflow) pauses the node on the same ids
177
+ * (raiseAgain), and the caller's walk with it. Rerun on the resume, the
178
+ * node walks its graph again on the answers among its ids, each as its
179
+ * explicit reply (an approval decision as it came, an input request's
180
+ * answer as `{ result }`), which the nested walk's resume reads as a
181
+ * top-level one's; with none of them answered it waits again without
182
+ * walking; once an earlier resume finished it, its stored output is the
183
+ * node's.
90
184
  */
91
185
  async function runWorkflowNode(name, workflow, run, params, parent) {
92
- const { sessions, userId, sessionId } = params;
93
- const key = { appName: name, userId, sessionId };
186
+ const { sessions } = params;
187
+ const resumed = run.resumedInterruptIds ?? [];
188
+ let userParts;
189
+ if (resumed.length > 0) {
190
+ const raised = requestCalls(parent.session.events, resumed, name);
191
+ const answered = resumed.filter((id) => answers(raised.get(id), run.resumeInputs?.[id]));
192
+ if (answered.length === 0) {
193
+ parent.store(raiseAgain(name, run, parent.invocationId, resumed.map((id) => raised.get(id)).filter((c) => !!c), {}));
194
+ return { interruptIds: [...resumed] };
195
+ }
196
+ const finished = answered.length === resumed.length ? outputAfterRaise(parent.session.events, run.path, resumed) : undefined;
197
+ if (finished !== undefined)
198
+ return { output: finished };
199
+ userParts = answered.map((id) => answerPart(raised.get(id), run.resumeInputs[id]));
200
+ }
201
+ else {
202
+ const input = run.input === undefined || run.input === null ? parent.userContent : nodeInputContent(run.input);
203
+ userParts = input.parts ?? [];
204
+ }
94
205
  const state = Object.fromEntries(Object.entries(parent.session.state ?? {}).filter(([k]) => !k.startsWith(TEMP_STATE_PREFIX)));
95
- if (!(await sessions.get(key)))
96
- await sessions.create({ ...key, state });
97
- const input = run.input === undefined || run.input === null ? parent.userContent : nodeInputContent(run.input);
98
- const walk = runNativeWorkflow({
99
- graph: workflow.graph,
100
- agents: workflow.agents,
101
- resolveTool: workflow.resolveTool,
102
- ...(workflow.workflows ? { workflows: workflow.workflows } : {}),
103
- sessions,
104
- appName: name,
105
- userId,
106
- sessionId,
107
- userParts: input.parts ?? [],
108
- adapterFor: params.adapterFor,
109
- signal: run.signal,
110
- stream: false,
111
- ...(params.memory ? { memory: params.memory } : {}),
112
- ...(params.log ? { log: params.log } : {}),
113
- ...(params.selfCorrection ? { selfCorrection: params.selfCorrection } : {}),
114
- ...(params.credentials ? { credentials: params.credentials } : {}),
115
- });
206
+ const child = await entrySession(sessions, { appName: parent.session.appName, userId: parent.session.userId, sessionId: parent.session.id, events: parent.session.events }, name, 'node', state, resumed.length > 0);
207
+ // A nested dispatch syndicate runs its own turn there (ADR 0120); a nested workflow walks its graph.
208
+ const walk = 'walk' in workflow
209
+ ? workflow.walk({ sessions, appName: child.appName, userId: child.userId, sessionId: child.id, userParts, ...(run.signal ? { signal: run.signal } : {}) })
210
+ : runNativeWorkflow({
211
+ graph: workflow.graph,
212
+ agents: workflow.agents,
213
+ resolveTool: workflow.resolveTool,
214
+ ...(workflow.workflows ? { workflows: workflow.workflows } : {}),
215
+ sessions,
216
+ appName: child.appName,
217
+ userId: child.userId,
218
+ sessionId: child.id,
219
+ userParts,
220
+ adapterFor: params.adapterFor,
221
+ signal: run.signal,
222
+ stream: false,
223
+ ...(params.memory ? { memory: params.memory } : {}),
224
+ ...(params.log ? { log: params.log } : {}),
225
+ ...(params.selfCorrection ? { selfCorrection: params.selfCorrection } : {}),
226
+ ...(params.credentials ? { credentials: params.credentials } : {}),
227
+ });
116
228
  let last;
117
229
  const stateDelta = {};
118
230
  let end;
@@ -133,7 +245,15 @@ async function runWorkflowNode(name, workflow, run, params, parent) {
133
245
  throw new NodeStoppedError(name, undefined);
134
246
  const paused = end.run?.interruptIds ?? [];
135
247
  if (paused.length > 0) {
136
- throw new Error(`Node '${name}': the workflow it runs paused on ${paused.join(', ')}; a pause inside a workflow run as a node cannot reach the walk yet.`);
248
+ const walked = (await sessions.get({ appName: child.appName, userId: child.userId, sessionId: child.id }))?.events ?? [];
249
+ // A nested dispatch syndicate names the calls it waits on: they may sit in a route's delegation below its conversation (ADR 0120).
250
+ const named = 'requests' in end && end.requests ? new Map(end.requests.map((call) => [call.id, call])) : undefined;
251
+ const raised = named ?? requestCalls(walked, paused);
252
+ const missing = paused.filter((id) => !raised.has(id));
253
+ if (missing.length > 0)
254
+ throw new Error(`Node '${name}': the ${'walk' in workflow ? 'syndicate' : 'workflow'} it runs paused on ${missing.join(', ')}, which is not an approval request or a question.`);
255
+ parent.store(raiseAgain(name, run, parent.invocationId, paused.map((id) => raised.get(id)), stateDelta));
256
+ return { interruptIds: [...paused] };
137
257
  }
138
258
  const output = lastText(last);
139
259
  const event = createTurnEvent({
@@ -48,13 +48,16 @@
48
48
  *
49
49
  * ── As a dispatch route or a workflow node (ADR 0106) ────────────────────
50
50
  * A `yaml_reference` to a workflow syndicate runs its whole graph there too,
51
- * on the child session filed under the entry's name, as a subagent does. A
52
- * route's answer is what the graph would answer as its own syndicate
53
- * (lib/runtime/syndicateTurn.ts); a node's is the nested walk's
54
- * (lib/workflow/turn.ts). A map over one is refused (lib/compile.ts), and
55
- * so are its pauses (an `ask_user` node, a gate): they cannot reach the
56
- * turn from a route or a node yet (lib/compile.ts compileWorkflowSpec,
57
- * loadNestedSyndicate).
51
+ * on the child session filed under the agent path and its kind
52
+ * (`<app>/route:<entry>`, `<walk's app>/node:<entry>`, ADR 0119, ADR 0120),
53
+ * as a subagent does. A route's answer is what the graph would
54
+ * answer as its own syndicate (lib/runtime/syndicateTurn.ts); a node's is
55
+ * the nested walk's (lib/workflow/turn.ts). A map over one is refused
56
+ * (lib/compile.ts). Its pauses (an `ask_user` node, a gate) reach the turn
57
+ * with the path from the entry down to the node that asked: a route's
58
+ * through the route's pause record in the conversation, a node's through
59
+ * its own pause on the caller's walk; the answer walks the nested graph
60
+ * again (ADR 0119).
58
61
  *
59
62
  * ── Approval gates (ADR 0098, ADR 0106) ──────────────────────────────────
60
63
  * A tool in a node agent's `require_approval`, and a skill script
@@ -48,13 +48,16 @@
48
48
  *
49
49
  * ── As a dispatch route or a workflow node (ADR 0106) ────────────────────
50
50
  * A `yaml_reference` to a workflow syndicate runs its whole graph there too,
51
- * on the child session filed under the entry's name, as a subagent does. A
52
- * route's answer is what the graph would answer as its own syndicate
53
- * (lib/runtime/syndicateTurn.ts); a node's is the nested walk's
54
- * (lib/workflow/turn.ts). A map over one is refused (lib/compile.ts), and
55
- * so are its pauses (an `ask_user` node, a gate): they cannot reach the
56
- * turn from a route or a node yet (lib/compile.ts compileWorkflowSpec,
57
- * loadNestedSyndicate).
51
+ * on the child session filed under the agent path and its kind
52
+ * (`<app>/route:<entry>`, `<walk's app>/node:<entry>`, ADR 0119, ADR 0120),
53
+ * as a subagent does. A route's answer is what the graph would
54
+ * answer as its own syndicate (lib/runtime/syndicateTurn.ts); a node's is
55
+ * the nested walk's (lib/workflow/turn.ts). A map over one is refused
56
+ * (lib/compile.ts). Its pauses (an `ask_user` node, a gate) reach the turn
57
+ * with the path from the entry down to the node that asked: a route's
58
+ * through the route's pause record in the conversation, a node's through
59
+ * its own pause on the caller's walk; the answer walks the nested graph
60
+ * again (ADR 0119).
58
61
  *
59
62
  * ── Approval gates (ADR 0098, ADR 0106) ──────────────────────────────────
60
63
  * A tool in a node agent's `require_approval`, and a skill script
@@ -37,8 +37,8 @@ export interface ServerOAuth extends OAuthServerSetup {
37
37
  }
38
38
  /**
39
39
  * Tool credentials and the consent step for the served syndicates, from
40
- * MELCHIZEDEK_CREDENTIAL_KEY, OAUTH_REDIRECT_URI, OAUTH_CALLBACK_IDENTITY
41
- * and MELCHIZEDEK_OAUTH_HOSTS (lib/a2a/oauthSetup.ts). Throws, naming
40
+ * MELCHIZEDEK_CREDENTIAL_KEY, OAUTH_REDIRECT_URI, OAUTH_CALLBACK_IDENTITY,
41
+ * MELCHIZEDEK_OAUTH_HOSTS and MELCHIZEDEK_CREDENTIAL_HOSTS (lib/a2a/oauthSetup.ts). Throws, naming
42
42
  * variables only, on a configuration that is unsafe or cannot work.
43
43
  */
44
44
  export declare function serverOAuth(options: {
@@ -89,6 +89,11 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
89
89
  * MELCHIZEDEK_OAUTH_HOSTS provider=host,host;…: the hosts each provider's tokens may
90
90
  * be sent to. Unset: authorization_code grants are refused,
91
91
  * client_credentials allowed (ADR 0114)
92
+ * MELCHIZEDEK_CREDENTIAL_HOSTS VARIABLE=host,host;…: the hosts each static credential
93
+ * variable a YAML sends (bearer_env, api_key.env,
94
+ * client_secret_env) may be sent to. Set, it is the whole
95
+ * list. Unset: sent where the YAML says, and the boot names
96
+ * each unbound variable (ADR 0122)
92
97
  */
93
98
  import { randomBytes } from 'node:crypto';
94
99
  import { readdirSync, realpathSync, writeFileSync } from 'node:fs';
@@ -103,12 +108,14 @@ import { redisRateLimitStore } from '../lib/a2a/redisLimits.js';
103
108
  import { dbSchema } from '../lib/storage/schema.js';
104
109
  import { postgresStorage } from '../lib/storage/postgres/index.js';
105
110
  import { isPlaceholderValue, loadEnv } from '../lib/loadEnv.js';
111
+ import { refuseChatGptSignInOnServedSurface } from '../lib/chatgpt/state.js';
106
112
  import { flushTracing } from '../lib/observability/tracer.js';
107
113
  import { setLogLevel } from '../lib/runtime/logging.js';
108
114
  import { loadSyndicate } from '../lib/loadSyndicate.js';
109
115
  import { callbackIdentity, callbackIdentityProblem, oauthServerSetup } from '../lib/a2a/oauthSetup.js';
110
116
  import { postgresCredentialRows } from '../lib/storage/postgres/credentialStore.js';
111
117
  import { setOAuthHosts } from '../lib/tools/oauthHosts.js';
118
+ import { setCredentialHosts } from '../lib/tools/credentialHosts.js';
112
119
  // Re-exported for existing importers (tests/failNarration.test.ts).
113
120
  export { describeTurnError } from '../lib/a2a/executor.js';
114
121
  /** Minimum bearer-secret length below which the server warns. */
@@ -200,8 +207,8 @@ export function servedFileSyndicates(syndicateName, servedAgents, registryAgents
200
207
  }
201
208
  /**
202
209
  * Tool credentials and the consent step for the served syndicates, from
203
- * MELCHIZEDEK_CREDENTIAL_KEY, OAUTH_REDIRECT_URI, OAUTH_CALLBACK_IDENTITY
204
- * and MELCHIZEDEK_OAUTH_HOSTS (lib/a2a/oauthSetup.ts). Throws, naming
210
+ * MELCHIZEDEK_CREDENTIAL_KEY, OAUTH_REDIRECT_URI, OAUTH_CALLBACK_IDENTITY,
211
+ * MELCHIZEDEK_OAUTH_HOSTS and MELCHIZEDEK_CREDENTIAL_HOSTS (lib/a2a/oauthSetup.ts). Throws, naming
205
212
  * variables only, on a configuration that is unsafe or cannot work.
206
213
  */
207
214
  export function serverOAuth(options) {
@@ -240,6 +247,13 @@ export async function startServer(syndicateName = 'syndicate.yaml', extensions =
240
247
  process.env.OTEL_CONSOLE_SPANS = 'false';
241
248
  // The engine's level: INFO/DEBUG lines carry raw event JSON, so keep them quiet.
242
249
  setLogLevel('warn');
250
+ // Sign in with ChatGPT is local only (ADR 0126): stop before anything else starts.
251
+ try {
252
+ refuseChatGptSignInOnServedSurface('melchizedek-serve');
253
+ }
254
+ catch (err) {
255
+ fatal(err instanceof Error ? err.message : String(err));
256
+ }
243
257
  const publicUrl = process.env.PUBLIC_URL?.trim() || undefined;
244
258
  const port = envInt('PORT', 4000);
245
259
  // ── Authentication posture ─────────────────────────────────────────────────
@@ -425,6 +439,8 @@ export async function startServer(syndicateName = 'syndicate.yaml', extensions =
425
439
  // An extension's allowlist wins over the environment's here too, as createA2AApp applies it.
426
440
  if (extensions.options?.oauthHosts)
427
441
  setOAuthHosts(extensions.options.oauthHosts);
442
+ if (extensions.options?.credentialHosts)
443
+ setCredentialHosts(extensions.options.credentialHosts);
428
444
  oauth = serverOAuth({
429
445
  syndicateName,
430
446
  servedAgents,
@@ -509,6 +525,7 @@ export async function startServer(syndicateName = 'syndicate.yaml', extensions =
509
525
  if (budgetLabel)
510
526
  emit('info', `[A2A] budgets ${budgetLabel}`);
511
527
  emit('info', `[A2A] oauth ${oauth.summary}`);
528
+ emit('info', `[A2A] creds ${oauth.credentialSummary}`);
512
529
  if (metricsToken)
513
530
  emit('info', `[A2A] metrics ${base}/metrics (bearer A2A_METRICS_TOKEN)`);
514
531
  emit('info', `[A2A] health ${base}/healthz ${base}/readyz`);
@@ -40,5 +40,7 @@
40
40
  * --agent names a subagent of the syndicate, or its orchestrator.
41
41
  * MELCHIZEDEK_TASKS_FILE moves the store (default: outputs/tasks.json),
42
42
  * and must match the chat process's value. Run ONE worker per store.
43
+ * MELCHIZEDEK_CHECKPOINT_MAX_BYTES caps a stored checkpoint (bytes of
44
+ * JSON, default 5 MiB); a larger one is skipped and the previous kept.
43
45
  */
44
46
  export {};