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.
- package/.env.example +19 -0
- package/AGENT_SETUP.md +3 -1
- package/CHANGELOG.md +373 -0
- package/DOCUMENTATION.md +193 -21
- package/ONBOARDING.md +378 -0
- package/QUICKSTART.md +15 -2
- package/README.md +26 -3
- package/config/agents/examples/connectors.yaml +82 -0
- package/config/agents/examples/council.yaml +1 -1
- package/config/agents/examples/critic.yaml +8 -8
- package/config/agents/examples/image_production.yaml +4 -3
- package/config/agents/syndicate.schema.json +389 -33
- package/config/agents/syndicateSchema.yaml +18 -6
- package/config/agents/templates/intake_extractor.yaml +3 -2
- package/config/agents/templates/systems_operator.yaml +5 -2
- package/dist/lib/a2a/app.d.ts +12 -0
- package/dist/lib/a2a/app.js +25 -11
- package/dist/lib/a2a/executor.js +14 -6
- package/dist/lib/a2a/oauthSetup.d.ts +16 -0
- package/dist/lib/a2a/oauthSetup.js +77 -8
- package/dist/lib/agentDialect.d.ts +18 -8
- package/dist/lib/agentDialect.js +36 -19
- package/dist/lib/chatgpt/adapter.d.ts +50 -0
- package/dist/lib/chatgpt/adapter.js +107 -0
- package/dist/lib/chatgpt/cli.d.ts +23 -0
- package/dist/lib/chatgpt/cli.js +77 -0
- package/dist/lib/chatgpt/oauth.d.ts +103 -0
- package/dist/lib/chatgpt/oauth.js +448 -0
- package/dist/lib/chatgpt/state.d.ts +98 -0
- package/dist/lib/chatgpt/state.js +145 -0
- package/dist/lib/chatgpt/store.d.ts +24 -0
- package/dist/lib/chatgpt/store.js +94 -0
- package/dist/lib/compile.d.ts +38 -7
- package/dist/lib/compile.js +97 -62
- package/dist/lib/compileNative.d.ts +11 -3
- package/dist/lib/compileNative.js +18 -1
- package/dist/lib/dispatch.js +6 -2
- package/dist/lib/doctor.d.ts +107 -0
- package/dist/lib/doctor.js +217 -7
- package/dist/lib/loadSyndicate.d.ts +15 -0
- package/dist/lib/mcp/server.d.ts +195 -0
- package/dist/lib/mcp/server.js +564 -0
- package/dist/lib/memory/supabaseMemoryService.js +2 -2
- package/dist/lib/models/adapterResolver.js +6 -1
- package/dist/lib/models/capabilities.d.ts +9 -0
- package/dist/lib/models/capabilities.js +24 -0
- package/dist/lib/models/chatCompletionsAdapter.d.ts +11 -3
- package/dist/lib/models/chatCompletionsAdapter.js +19 -5
- package/dist/lib/models/claudeAdapter.js +4 -0
- package/dist/lib/models/contract.d.ts +5 -2
- package/dist/lib/models/gateway.d.ts +3 -2
- package/dist/lib/models/gateway.js +8 -4
- package/dist/lib/models/gatewayAdapter.d.ts +7 -1
- package/dist/lib/models/gatewayAdapter.js +8 -0
- package/dist/lib/models/geminiAdapter.js +8 -2
- package/dist/lib/models/genaiMapping.d.ts +8 -4
- package/dist/lib/models/genaiMapping.js +23 -9
- package/dist/lib/models/gptAdapter.d.ts +4 -2
- package/dist/lib/models/gptAdapter.js +11 -4
- package/dist/lib/models/grokAdapter.d.ts +3 -1
- package/dist/lib/models/grokAdapter.js +11 -3
- package/dist/lib/models/kimiAdapter.d.ts +2 -1
- package/dist/lib/models/kimiAdapter.js +3 -3
- package/dist/lib/models/reasoning.d.ts +30 -2
- package/dist/lib/models/reasoning.js +56 -5
- package/dist/lib/onboarding.d.ts +135 -0
- package/dist/lib/onboarding.js +578 -0
- package/dist/lib/runtime/approvals.d.ts +2 -0
- package/dist/lib/runtime/approvals.js +4 -3
- package/dist/lib/runtime/credentials.d.ts +6 -0
- package/dist/lib/runtime/native/agentLoop.d.ts +5 -2
- package/dist/lib/runtime/native/agentLoop.js +2 -0
- package/dist/lib/runtime/native/checkpoint.d.ts +12 -5
- package/dist/lib/runtime/native/checkpoint.js +13 -19
- package/dist/lib/runtime/native/delegate.d.ts +82 -1
- package/dist/lib/runtime/native/delegate.js +157 -2
- package/dist/lib/runtime/native/interrupts.d.ts +88 -5
- package/dist/lib/runtime/native/interrupts.js +220 -30
- package/dist/lib/runtime/syndicateTurn.d.ts +4 -2
- package/dist/lib/runtime/syndicateTurn.js +324 -77
- package/dist/lib/runtime/turnControl.d.ts +41 -0
- package/dist/lib/session/transcript.d.ts +9 -0
- package/dist/lib/session/transcript.js +20 -1
- package/dist/lib/skills.d.ts +18 -0
- package/dist/lib/skills.js +73 -1
- package/dist/lib/storage/postgres/index.d.ts +6 -1
- package/dist/lib/storage/postgres/index.js +1 -1
- package/dist/lib/storage/postgres/taskQueue.d.ts +7 -3
- package/dist/lib/storage/postgres/taskQueue.js +10 -3
- package/dist/lib/syndicateSchema.d.ts +184 -9
- package/dist/lib/syndicateSchema.js +143 -29
- package/dist/lib/tools/auth.d.ts +13 -1
- package/dist/lib/tools/auth.js +10 -2
- package/dist/lib/tools/credentialHosts.d.ts +88 -0
- package/dist/lib/tools/credentialHosts.js +165 -0
- package/dist/lib/tools/credentialUses.d.ts +98 -0
- package/dist/lib/tools/credentialUses.js +120 -0
- package/dist/lib/tools/generateImageTool.js +2 -1
- package/dist/lib/tools/inspectImageTool.js +1 -1
- package/dist/lib/tools/mcpToolFactory.d.ts +29 -1
- package/dist/lib/tools/mcpToolFactory.js +72 -26
- package/dist/lib/tools/mcpTransports.d.ts +12 -0
- package/dist/lib/tools/mcpTransports.js +11 -0
- package/dist/lib/tools/oauthConsent.d.ts +18 -3
- package/dist/lib/tools/oauthConsent.js +57 -15
- package/dist/lib/tools/oauthDiscovery.d.ts +202 -0
- package/dist/lib/tools/oauthDiscovery.js +616 -0
- package/dist/lib/tools/oauthHosts.d.ts +2 -0
- package/dist/lib/tools/oauthHosts.js +2 -2
- package/dist/lib/tools/oauthTools.d.ts +54 -13
- package/dist/lib/tools/oauthTools.js +106 -23
- package/dist/lib/tools/openapiTools.d.ts +7 -0
- package/dist/lib/tools/openapiTools.js +49 -3
- package/dist/lib/tools/taskTools.d.ts +32 -2
- package/dist/lib/tools/taskTools.js +93 -33
- package/dist/lib/tools/xApiSearchTool.js +2 -2
- package/dist/lib/workflow/turn.d.ts +22 -3
- package/dist/lib/workflow/turn.js +151 -31
- package/dist/lib/workflow.d.ts +10 -7
- package/dist/lib/workflow.js +10 -7
- package/dist/scripts/a2a_server.d.ts +2 -2
- package/dist/scripts/a2a_server.js +19 -2
- package/dist/scripts/assistant_worker.d.ts +2 -0
- package/dist/scripts/assistant_worker.js +24 -5
- package/dist/scripts/doctor.d.ts +4 -2
- package/dist/scripts/doctor.js +6 -4
- package/dist/scripts/mcp_server.d.ts +12 -0
- package/dist/scripts/mcp_server.js +345 -0
- package/dist/scripts/setup.d.ts +25 -0
- package/dist/scripts/setup.js +181 -0
- package/dist/scripts/skills_install.d.ts +3 -1
- package/dist/scripts/skills_install.js +17 -2
- package/dist/scripts/syndicate_chat.js +12 -3
- package/dist/scripts/yaml_codemod.d.ts +12 -2
- package/dist/scripts/yaml_codemod.js +115 -33
- package/package.json +8 -2
- package/skills/README.md +11 -2
- package/skills/briefs/_shared.md +1 -1
- package/skills/briefs/melchizedek-models.md +4 -2
- package/skills/briefs/skills-readme.md +3 -1
- package/skills/melchizedek-models/SKILL.md +6 -2
- package/skills/melchizedek-onboard/SKILL.md +71 -0
- package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
- package/skills/melchizedek-onboard-keys/SKILL.md +47 -0
- package/skills/melchizedek-onboard-local/SKILL.md +41 -0
- 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
|
|
83
|
-
function
|
|
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,
|
|
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
|
-
/**
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
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]
|
|
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
|
|
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 {
|
|
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
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
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
|
|
93
|
-
const
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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({
|
package/dist/lib/workflow.d.ts
CHANGED
|
@@ -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
|
|
52
|
-
* route's
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
package/dist/lib/workflow.js
CHANGED
|
@@ -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
|
|
52
|
-
* route's
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
|
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
|
|
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 {};
|