melchizedek-agents 1.0.2 → 1.1.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 -2
- package/CHANGELOG.md +333 -0
- package/DOCUMENTATION.md +99 -39
- package/README.md +1 -0
- package/config/agents/README.md +2 -2
- package/config/agents/examples/ares.yaml +6 -10
- package/config/agents/examples/assistant.yaml +6 -6
- package/config/agents/examples/augustin.yaml +9 -15
- package/config/agents/examples/cartographers.yaml +6 -6
- package/config/agents/examples/council.yaml +6 -6
- package/config/agents/examples/critic.yaml +16 -16
- package/config/agents/examples/harness.yaml +4 -4
- package/config/agents/examples/image_production.yaml +33 -32
- package/config/agents/examples/librarian.yaml +4 -4
- package/config/agents/examples/patient_advocate.yaml +6 -10
- package/config/agents/examples/pipeline.yaml +20 -19
- package/config/agents/examples/research.yaml +36 -43
- package/config/agents/examples/scribe.yaml +31 -32
- package/config/agents/examples/scriptorium.yaml +6 -6
- package/config/agents/examples/structured_critic.yaml +82 -0
- package/config/agents/examples/style_council.yaml +9 -11
- package/config/agents/examples/syndicate.yaml +3 -5
- package/config/agents/examples/tutor.yaml +2 -2
- package/config/agents/examples/weather.yaml +2 -2
- package/config/agents/syndicate.schema.json +1979 -0
- package/config/agents/syndicateSchema.yaml +109 -72
- package/config/agents/templates/README.md +1 -1
- package/config/agents/templates/account_memory.yaml +4 -4
- package/config/agents/templates/case_desk.yaml +6 -6
- package/config/agents/templates/conversational.yaml +2 -2
- package/config/agents/templates/draft_review.yaml +6 -6
- package/config/agents/templates/intake_extractor.yaml +61 -60
- package/config/agents/templates/research_brief.yaml +6 -6
- package/config/agents/templates/research_desk.yaml +6 -6
- package/config/agents/templates/review_panel.yaml +8 -8
- package/config/agents/templates/support_triage.yaml +30 -29
- package/config/agents/templates/systems_operator.yaml +21 -5
- package/db/migrations/0014_durable_runs.sql +31 -0
- package/dist/lib/a2a/app.d.ts +15 -0
- package/dist/lib/a2a/app.js +53 -5
- package/dist/lib/a2a/executor.d.ts +7 -1
- package/dist/lib/a2a/executor.js +34 -4
- package/dist/lib/a2a/oauthSetup.d.ts +90 -0
- package/dist/lib/a2a/oauthSetup.js +205 -0
- package/dist/lib/agentDialect.d.ts +66 -0
- package/dist/lib/agentDialect.js +134 -0
- package/dist/lib/compile.d.ts +10 -5
- package/dist/lib/compile.js +69 -37
- package/dist/lib/compileNative.js +2 -0
- package/dist/lib/dispatch.d.ts +10 -5
- package/dist/lib/dispatch.js +10 -5
- package/dist/lib/doctor.d.ts +34 -1
- package/dist/lib/doctor.js +47 -3
- package/dist/lib/loadSyndicate.d.ts +45 -2
- package/dist/lib/loadSyndicate.js +22 -2
- package/dist/lib/memory/namespace.d.ts +6 -5
- package/dist/lib/memory/namespace.js +6 -5
- package/dist/lib/memory/providers.js +3 -4
- package/dist/lib/models/capabilities.d.ts +16 -1
- package/dist/lib/models/capabilities.js +55 -1
- package/dist/lib/observability/tracer.d.ts +8 -1
- package/dist/lib/observability/tracer.js +33 -5
- package/dist/lib/runtime/approvals.d.ts +6 -0
- package/dist/lib/runtime/native/agentLoop.d.ts +18 -1
- package/dist/lib/runtime/native/agentLoop.js +105 -11
- package/dist/lib/runtime/native/checkpoint.d.ts +164 -0
- package/dist/lib/runtime/native/checkpoint.js +365 -0
- package/dist/lib/runtime/native/delegate.d.ts +141 -18
- package/dist/lib/runtime/native/delegate.js +235 -39
- package/dist/lib/runtime/native/interrupts.d.ts +101 -2
- package/dist/lib/runtime/native/interrupts.js +201 -1
- package/dist/lib/runtime/native/request.d.ts +13 -3
- package/dist/lib/runtime/native/request.js +28 -4
- package/dist/lib/runtime/native/telemetry.js +10 -15
- package/dist/lib/runtime/questions.d.ts +7 -4
- package/dist/lib/runtime/sessions.d.ts +1 -1
- package/dist/lib/runtime/syndicateTurn.d.ts +7 -1
- package/dist/lib/runtime/syndicateTurn.js +74 -22
- package/dist/lib/runtime/turnControl.d.ts +15 -0
- package/dist/lib/runtime/turnControl.js +10 -0
- package/dist/lib/storage/postgres/index.d.ts +6 -1
- package/dist/lib/storage/postgres/index.js +7 -2
- package/dist/lib/storage/postgres/taskQueue.d.ts +14 -2
- package/dist/lib/storage/postgres/taskQueue.js +20 -3
- package/dist/lib/storage/postgres/taskStore.d.ts +26 -2
- package/dist/lib/storage/postgres/taskStore.js +74 -8
- package/dist/lib/syndicateSchema.d.ts +278 -0
- package/dist/lib/syndicateSchema.js +182 -15
- package/dist/lib/tools/auth.d.ts +7 -5
- package/dist/lib/tools/auth.js +12 -4
- package/dist/lib/tools/credentialEnv.d.ts +28 -0
- package/dist/lib/tools/credentialEnv.js +52 -0
- package/dist/lib/tools/credentialStore.js +9 -2
- package/dist/lib/tools/mcpToolFactory.d.ts +25 -2
- package/dist/lib/tools/mcpToolFactory.js +179 -21
- package/dist/lib/tools/oauthHosts.d.ts +71 -0
- package/dist/lib/tools/oauthHosts.js +154 -0
- package/dist/lib/tools/oauthTools.d.ts +233 -0
- package/dist/lib/tools/oauthTools.js +421 -0
- package/dist/lib/tools/openapiTools.d.ts +4 -6
- package/dist/lib/tools/openapiTools.js +40 -36
- package/dist/lib/tools/taskTools.d.ts +30 -5
- package/dist/lib/tools/taskTools.js +74 -10
- package/dist/lib/workflow.d.ts +8 -4
- package/dist/lib/workflow.js +8 -4
- package/dist/lib/workflowConfig.d.ts +6 -0
- package/dist/scripts/a2a_server.d.ts +36 -0
- package/dist/scripts/a2a_server.js +122 -1
- package/dist/scripts/assistant_worker.d.ts +10 -0
- package/dist/scripts/assistant_worker.js +69 -19
- package/dist/scripts/doctor.d.ts +4 -1
- package/dist/scripts/doctor.js +5 -2
- package/dist/scripts/yaml_codemod.d.ts +52 -0
- package/dist/scripts/yaml_codemod.js +570 -0
- package/package.json +11 -7
- package/skills/briefs/melchizedek-author.md +7 -5
- package/skills/briefs/melchizedek-models.md +4 -3
- package/skills/briefs/melchizedek.md +1 -0
- package/skills/melchizedek/SKILL.md +1 -0
- package/skills/melchizedek-author/SKILL.md +7 -4
- package/skills/melchizedek-author/assets/minimal.yaml +13 -13
- package/skills/melchizedek-models/SKILL.md +6 -3
|
@@ -32,7 +32,10 @@
|
|
|
32
32
|
* OPENAPI_CREDENTIAL_ENVS, when the operator sets it, is the exact list
|
|
33
33
|
* of variables an `auth` may name. A credential is held by the tool,
|
|
34
34
|
* never written to session state, and its value never appears in an
|
|
35
|
-
* error the model reads.
|
|
35
|
+
* error the model reads. An `auth: { oauth2 }` sends an OAuth token
|
|
36
|
+
* instead, fetched per call: the run's user's own (authorization_code,
|
|
37
|
+
* through `ctx.accessToken` and the consent pause) or the server's own
|
|
38
|
+
* (client_credentials), lib/tools/oauthTools.ts, ADR 0112.
|
|
36
39
|
* - THE SSRF GUARD. Every server URL the tools will call must be http(s)
|
|
37
40
|
* and pass lib/net/addressGuard.ts: its literal rules at compile time
|
|
38
41
|
* (offline), and the full check with DNS before each call (a name can
|
|
@@ -62,6 +65,10 @@ import { extname, resolve } from 'node:path';
|
|
|
62
65
|
import { MAX_RESULT_CHARS as TOOL_RESULT_CHARS, capResult } from './tool.js';
|
|
63
66
|
import { callOperation, hostProblem } from './openapi/call.js';
|
|
64
67
|
import { MAX_SPEC_BYTES, operationNamed, parseOpenApiSpec } from './openapi/parse.js';
|
|
68
|
+
import { ToolCredentialError } from './auth.js';
|
|
69
|
+
import { readCredentialEnv } from './credentialEnv.js';
|
|
70
|
+
import { oauthTokenSource, tokenTransportProblem } from './oauthTools.js';
|
|
71
|
+
export { credentialEnvProblem } from './credentialEnv.js';
|
|
65
72
|
export { namesTool, toSnake } from './openapi/parse.js';
|
|
66
73
|
/** Characters a single API result may carry into the conversation: every tool's shared limit (lib/tools/tool.ts). */
|
|
67
74
|
export const MAX_RESULT_CHARS = TOOL_RESULT_CHARS;
|
|
@@ -70,45 +77,17 @@ export const MAX_RESULT_CHARS = TOOL_RESULT_CHARS;
|
|
|
70
77
|
* operationId (lib/compile.ts). Its value is the operationId.
|
|
71
78
|
*/
|
|
72
79
|
export const OPENAPI_TOOL = Symbol.for('melchizedek.openapiTool');
|
|
73
|
-
/** Prefixes and names of variables the framework itself reads: never an API credential. */
|
|
74
|
-
const FRAMEWORK_ENV_PREFIXES = [
|
|
75
|
-
'A2A_', 'SUPABASE_', 'DATABASE_', 'MCP_', 'MODEL_', 'MEMORY_', 'OTEL_', 'TELEMETRY_', 'MELCHIZEDEK_',
|
|
76
|
-
'GOOGLE_', 'GEMINI_', 'ANTHROPIC_', 'OPENAI_', 'AZURE_', 'AWS_', 'XAI_', 'MOONSHOT_', 'OLLAMA_', 'WIKI_',
|
|
77
|
-
'ALLOW_', 'OPENAPI_',
|
|
78
|
-
];
|
|
79
|
-
const FRAMEWORK_ENV_NAMES = new Set(['PUBLIC_URL', 'HOST', 'PORT', 'PATH', 'HOME', 'NODE_OPTIONS', 'WEB_EXTRACT_CHAR_LIMIT']);
|
|
80
|
-
/**
|
|
81
|
-
* Why an `auth` may not read this variable, or null. With
|
|
82
|
-
* OPENAPI_CREDENTIAL_ENVS set, only the names it lists are allowed; without
|
|
83
|
-
* it, anything but the framework's own variables is.
|
|
84
|
-
*/
|
|
85
|
-
export function credentialEnvProblem(name, env = process.env) {
|
|
86
|
-
const allow = env.OPENAPI_CREDENTIAL_ENVS?.split(',').map((s) => s.trim()).filter(Boolean);
|
|
87
|
-
if (allow?.length) {
|
|
88
|
-
return allow.includes(name) ? null : `${name} is not in OPENAPI_CREDENTIAL_ENVS (${allow.join(', ')})`;
|
|
89
|
-
}
|
|
90
|
-
if (FRAMEWORK_ENV_NAMES.has(name) || FRAMEWORK_ENV_PREFIXES.some((p) => name.startsWith(p))) {
|
|
91
|
-
return `${name} is one of the framework's own settings and may not be sent to an API; give the API its own variable (or list it in OPENAPI_CREDENTIAL_ENVS)`;
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
80
|
/**
|
|
96
81
|
* The credential an `auth` names, read from the environment once, at
|
|
97
82
|
* compile. Throws (failing the compile) on a variable the allowlist refuses
|
|
98
83
|
* or one that is not set; the message names the variable, never its value.
|
|
84
|
+
* An `oauth2` block is not read here: its token is fetched per call
|
|
85
|
+
* (oauthTokenSource, lib/tools/oauthTools.ts).
|
|
99
86
|
*/
|
|
100
87
|
function credentialFor(auth, spec) {
|
|
101
88
|
if (!auth)
|
|
102
89
|
return undefined;
|
|
103
|
-
const read = (env) => {
|
|
104
|
-
const refused = credentialEnvProblem(env);
|
|
105
|
-
if (refused)
|
|
106
|
-
throw new Error(`openapi ${spec}: ${refused}`);
|
|
107
|
-
const value = process.env[env]?.trim();
|
|
108
|
-
if (!value)
|
|
109
|
-
throw new Error(`openapi ${spec}: ${env} is not set (auth reads it from the environment)`);
|
|
110
|
-
return value;
|
|
111
|
-
};
|
|
90
|
+
const read = (env) => readCredentialEnv(env, `openapi ${spec}`);
|
|
112
91
|
if (auth.bearer_env)
|
|
113
92
|
return { kind: 'bearer', token: read(auth.bearer_env) };
|
|
114
93
|
if (auth.api_key)
|
|
@@ -152,13 +131,28 @@ function readSpec(specPath, source) {
|
|
|
152
131
|
closeSync(fd);
|
|
153
132
|
}
|
|
154
133
|
}
|
|
155
|
-
/**
|
|
156
|
-
|
|
134
|
+
/**
|
|
135
|
+
* An own Tool that calls one operation (lib/tools/openapi/call.ts), its
|
|
136
|
+
* result cut to MAX_RESULT_CHARS. With `oauth`, each call fetches its token
|
|
137
|
+
* first; a token that cannot be had is answered as `{ error }` naming the
|
|
138
|
+
* provider and what to do, never a value (and, for a user who has not
|
|
139
|
+
* granted it, the run's consent step has already asked).
|
|
140
|
+
*/
|
|
141
|
+
function operationTool(op, credential, oauth) {
|
|
157
142
|
const tool = {
|
|
158
143
|
name: op.name,
|
|
159
144
|
declaration: () => op.declaration,
|
|
160
145
|
async execute(args, ctx) {
|
|
161
|
-
|
|
146
|
+
let sent = credential;
|
|
147
|
+
if (oauth) {
|
|
148
|
+
try {
|
|
149
|
+
sent = { kind: 'bearer', token: await oauth(ctx, op.baseUrl) };
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
return { error: error instanceof ToolCredentialError ? error.message : 'The authorization for this API could not be obtained.' };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return capResult(await callOperation(op, args ?? {}, { credential: sent, signal: ctx?.signal }));
|
|
162
156
|
},
|
|
163
157
|
};
|
|
164
158
|
Object.defineProperty(tool, OPENAPI_TOOL, { value: op.operationId });
|
|
@@ -199,7 +193,17 @@ export async function buildOpenApiOwnTools(entry, baseDir = process.cwd()) {
|
|
|
199
193
|
if (problem)
|
|
200
194
|
throw new Error(`openapi ${entry.spec}: ${problem}`);
|
|
201
195
|
}
|
|
202
|
-
|
|
196
|
+
let oauth;
|
|
197
|
+
if (entry.auth?.oauth2 && chosen.length) {
|
|
198
|
+
// Every server the token goes to takes it over https (or loopback http).
|
|
199
|
+
for (const server of new Set(chosen.map((o) => o.baseUrl))) {
|
|
200
|
+
const problem = tokenTransportProblem(server);
|
|
201
|
+
if (problem)
|
|
202
|
+
throw new Error(`openapi ${entry.spec}: ${problem}`);
|
|
203
|
+
}
|
|
204
|
+
oauth = oauthTokenSource(entry.auth.oauth2, `openapi ${entry.spec}`, [...new Set(chosen.map((o) => o.baseUrl))], { allowPrivate: process.env.ALLOW_PRIVATE_OPENAPI === 'true' });
|
|
205
|
+
}
|
|
206
|
+
return chosen.map((o) => operationTool(o, credential, oauth));
|
|
203
207
|
}
|
|
204
208
|
/**
|
|
205
209
|
* The tools one `openapi:` entry gives an agent: buildOpenApiOwnTools' own
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* a list of records with a status — so both live in one store:
|
|
11
11
|
*
|
|
12
12
|
* kind: todo the user's own tasks. open → done | cancelled.
|
|
13
|
-
* kind: background a job for the worker. queued → running → done | failed
|
|
13
|
+
* kind: background a job for the worker. queued → running → done | failed,
|
|
14
|
+
* and cancelled from queued or running (a running job's
|
|
15
|
+
* worker sees it at its next renewal and stops).
|
|
14
16
|
*
|
|
15
17
|
* The tools only WRITE the queue. They never run a job: a tool that
|
|
16
18
|
* orchestrates agents is exactly what the tool-contract doctrine refuses
|
|
@@ -28,11 +30,15 @@
|
|
|
28
30
|
* gitignored). Every operation re-reads the file, so the chat process
|
|
29
31
|
* and the worker see each other's writes. It is SINGLE-USER: every
|
|
30
32
|
* caller shares one list. Run ONE worker: the claim is a
|
|
31
|
-
* read-modify-write, not a lock.
|
|
33
|
+
* read-modify-write, not a lock. A running job's step checkpoints
|
|
34
|
+
* (ADR 0113) sit in a sidecar, <store>.checkpoints.json, keyed by id;
|
|
35
|
+
* every write of the store drops those of jobs no longer running or
|
|
36
|
+
* queued.
|
|
32
37
|
* - With Postgres (postgresStorage().taskQueue, migration 0009) each
|
|
33
38
|
* caller has its own list, scoped by the caller the tool call carries
|
|
34
39
|
* (the A2A server's scope key), and any number of workers claim jobs
|
|
35
|
-
* with FOR UPDATE SKIP LOCKED under a renewed lease (ADR 0021).
|
|
40
|
+
* with FOR UPDATE SKIP LOCKED under a renewed lease (ADR 0021). The
|
|
41
|
+
* checkpoint is a column beside the record (migration 0014).
|
|
36
42
|
*
|
|
37
43
|
* SECURITY:
|
|
38
44
|
* On the file store every caller of a shared endpoint shares one list: do
|
|
@@ -95,8 +101,13 @@ export interface TaskBackend {
|
|
|
95
101
|
mutate<T>(owner: string, change: (store: TaskStore) => T): Promise<T>;
|
|
96
102
|
/** The oldest queued background job of any owner, now running and leased. */
|
|
97
103
|
claimNext(worker: WorkerLease): Promise<OwnedTask | null>;
|
|
98
|
-
/**
|
|
99
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Keeps a claimed job leased while it runs. Resolves false when the claim
|
|
106
|
+
* is gone (the job left `running`, for instance cancelled with
|
|
107
|
+
* task_update, or another worker holds it): the worker must stop the run.
|
|
108
|
+
* A backend that resolves void makes no such claim.
|
|
109
|
+
*/
|
|
110
|
+
renew(worker: WorkerLease, job: OwnedTask): Promise<void | boolean>;
|
|
100
111
|
/** Records a job's outcome; a record no longer running is left alone. */
|
|
101
112
|
finish(job: OwnedTask, outcome: JobOutcome): Promise<void>;
|
|
102
113
|
/** Jobs whose worker died: queued again, or failed after MAX_ATTEMPTS. */
|
|
@@ -104,6 +115,19 @@ export interface TaskBackend {
|
|
|
104
115
|
requeued: string[];
|
|
105
116
|
failed: string[];
|
|
106
117
|
}>;
|
|
118
|
+
/**
|
|
119
|
+
* Durable runs (ADR 0113): stores the run's latest step checkpoint beside
|
|
120
|
+
* the job. Resolves false when the job is no longer this worker's running
|
|
121
|
+
* job (cancelled, finished, or re-leased): the run must stop.
|
|
122
|
+
*
|
|
123
|
+
* A checkpoint lives only while its job is `running` or `queued` (a job
|
|
124
|
+
* re-queued after an interruption keeps it, so the next claim resumes);
|
|
125
|
+
* any other status drops it, and so does a pruned or deleted record. It is
|
|
126
|
+
* kept beside the record, never in it, so task_get never shows it.
|
|
127
|
+
*/
|
|
128
|
+
saveCheckpoint?(worker: WorkerLease, job: OwnedTask, checkpoint: object): Promise<boolean>;
|
|
129
|
+
/** The checkpoint a re-queued job left, or null. */
|
|
130
|
+
loadCheckpoint?(job: OwnedTask): Promise<object | null>;
|
|
107
131
|
}
|
|
108
132
|
/** Records kept at most; finished ones are pruned oldest-first to make room. */
|
|
109
133
|
export declare const MAX_TASKS = 500;
|
|
@@ -112,6 +136,7 @@ export declare const MAX_RESULT_CHARS = 20000;
|
|
|
112
136
|
/** A job interrupted this many times is marked failed instead of re-queued. */
|
|
113
137
|
export declare const MAX_ATTEMPTS = 2;
|
|
114
138
|
export declare function taskStorePath(): string;
|
|
139
|
+
export declare function taskCheckpointPath(storePath?: string): string;
|
|
115
140
|
/**
|
|
116
141
|
* Called once at worker start: a record left `running` means a worker died
|
|
117
142
|
* mid-job. It goes back to the queue, or fails once it has been tried
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* a list of records with a status — so both live in one store:
|
|
11
11
|
*
|
|
12
12
|
* kind: todo the user's own tasks. open → done | cancelled.
|
|
13
|
-
* kind: background a job for the worker. queued → running → done | failed
|
|
13
|
+
* kind: background a job for the worker. queued → running → done | failed,
|
|
14
|
+
* and cancelled from queued or running (a running job's
|
|
15
|
+
* worker sees it at its next renewal and stops).
|
|
14
16
|
*
|
|
15
17
|
* The tools only WRITE the queue. They never run a job: a tool that
|
|
16
18
|
* orchestrates agents is exactly what the tool-contract doctrine refuses
|
|
@@ -28,11 +30,15 @@
|
|
|
28
30
|
* gitignored). Every operation re-reads the file, so the chat process
|
|
29
31
|
* and the worker see each other's writes. It is SINGLE-USER: every
|
|
30
32
|
* caller shares one list. Run ONE worker: the claim is a
|
|
31
|
-
* read-modify-write, not a lock.
|
|
33
|
+
* read-modify-write, not a lock. A running job's step checkpoints
|
|
34
|
+
* (ADR 0113) sit in a sidecar, <store>.checkpoints.json, keyed by id;
|
|
35
|
+
* every write of the store drops those of jobs no longer running or
|
|
36
|
+
* queued.
|
|
32
37
|
* - With Postgres (postgresStorage().taskQueue, migration 0009) each
|
|
33
38
|
* caller has its own list, scoped by the caller the tool call carries
|
|
34
39
|
* (the A2A server's scope key), and any number of workers claim jobs
|
|
35
|
-
* with FOR UPDATE SKIP LOCKED under a renewed lease (ADR 0021).
|
|
40
|
+
* with FOR UPDATE SKIP LOCKED under a renewed lease (ADR 0021). The
|
|
41
|
+
* checkpoint is a column beside the record (migration 0014).
|
|
36
42
|
*
|
|
37
43
|
* SECURITY:
|
|
38
44
|
* On the file store every caller of a shared endpoint shares one list: do
|
|
@@ -73,18 +79,58 @@ function readStore(path = taskStorePath()) {
|
|
|
73
79
|
}
|
|
74
80
|
return { version: 1, next_id: parsed.next_id, tasks: parsed.tasks };
|
|
75
81
|
}
|
|
76
|
-
|
|
82
|
+
/** Atomic JSON write: temp file + rename, so a reader never sees half a file. */
|
|
83
|
+
function writeJson(value, path) {
|
|
77
84
|
mkdirSync(dirname(path), { recursive: true });
|
|
78
85
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
79
|
-
writeFileSync(tmp, `${JSON.stringify(
|
|
86
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
|
|
80
87
|
renameSync(tmp, path);
|
|
81
88
|
}
|
|
89
|
+
function writeStore(store, path = taskStorePath()) {
|
|
90
|
+
writeJson(store, path);
|
|
91
|
+
}
|
|
92
|
+
/** Statuses a job's checkpoint survives in. */
|
|
93
|
+
const CHECKPOINTED = ['running', 'queued'];
|
|
94
|
+
export function taskCheckpointPath(storePath = taskStorePath()) {
|
|
95
|
+
return `${storePath}.checkpoints.json`;
|
|
96
|
+
}
|
|
97
|
+
/** The sidecar, or empty when missing or unreadable (a checkpoint is an optimisation, never the record). */
|
|
98
|
+
function readCheckpoints(storePath) {
|
|
99
|
+
const path = taskCheckpointPath(storePath);
|
|
100
|
+
if (!existsSync(path))
|
|
101
|
+
return {};
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
104
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const keepsCheckpoint = (t) => !!t && t.kind === 'background' && CHECKPOINTED.includes(t.status);
|
|
111
|
+
/** Drops sidecar entries whose job is gone or no longer running/queued. */
|
|
112
|
+
function pruneCheckpoints(store, storePath) {
|
|
113
|
+
if (!existsSync(taskCheckpointPath(storePath)))
|
|
114
|
+
return;
|
|
115
|
+
const entries = readCheckpoints(storePath);
|
|
116
|
+
const byId = new Map(store.tasks.map((t) => [t.id, t]));
|
|
117
|
+
let changed = false;
|
|
118
|
+
for (const id of Object.keys(entries)) {
|
|
119
|
+
if (!keepsCheckpoint(byId.get(id))) {
|
|
120
|
+
delete entries[id];
|
|
121
|
+
changed = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (changed)
|
|
125
|
+
writeJson(entries, taskCheckpointPath(storePath));
|
|
126
|
+
}
|
|
82
127
|
/** Read, change, write — one synchronous step, so no await splits it. */
|
|
83
128
|
function mutate(change) {
|
|
84
129
|
const path = taskStorePath();
|
|
85
130
|
const store = readStore(path);
|
|
86
131
|
const out = change(store);
|
|
87
132
|
writeStore(store, path);
|
|
133
|
+
pruneCheckpoints(store, path);
|
|
88
134
|
return out;
|
|
89
135
|
}
|
|
90
136
|
const now = () => new Date().toISOString();
|
|
@@ -212,9 +258,26 @@ export const fileTaskBackend = {
|
|
|
212
258
|
const job = claimNextJob();
|
|
213
259
|
return job ? { ...job, owner: '' } : null;
|
|
214
260
|
},
|
|
215
|
-
|
|
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',
|
|
216
263
|
finish: async (job, outcome) => finishJob(job.id, outcome),
|
|
217
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
|
+
},
|
|
218
281
|
};
|
|
219
282
|
let activeBackend = fileTaskBackend;
|
|
220
283
|
/** Plugs in where tasks live (postgresStorage().taskQueue); default the JSON file. */
|
|
@@ -318,7 +381,8 @@ export const taskGetContract = defineTool({
|
|
|
318
381
|
export const taskUpdateContract = defineTool({
|
|
319
382
|
name: 'task_update',
|
|
320
383
|
description: 'Change a task: mark it done, cancel it, reopen it, or edit its title, notes, or due date. ' +
|
|
321
|
-
'For a background job, "cancelled" withdraws it before it runs
|
|
384
|
+
'For a background job, "cancelled" withdraws it before it runs, or stops it if it is running now ' +
|
|
385
|
+
'(the worker stops at its next step and keeps no result); "queued" retries a failed or cancelled job. ' +
|
|
322
386
|
'Call task_list first if you do not have the id.',
|
|
323
387
|
schema: z.object({
|
|
324
388
|
id: taskId,
|
|
@@ -340,9 +404,9 @@ export const taskUpdateContract = defineTool({
|
|
|
340
404
|
return `Error: a ${task.kind} task cannot be set to "${status}". Allowed: ${allowed[task.kind].join(', ')}.`;
|
|
341
405
|
}
|
|
342
406
|
if (task.kind === 'background') {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
407
|
+
// Cancelling a running job is allowed: its worker's next lease
|
|
408
|
+
// renewal or checkpoint sees the record left `running` and stops
|
|
409
|
+
// the run, and the worker's outcome is then left alone (applyFinish).
|
|
346
410
|
if (status === 'queued' && !['failed', 'cancelled'].includes(task.status)) {
|
|
347
411
|
return `Error: only a failed or cancelled job can be queued again; ${id} is ${task.status}.`;
|
|
348
412
|
}
|
package/dist/lib/workflow.d.ts
CHANGED
|
@@ -41,16 +41,20 @@
|
|
|
41
41
|
* A delegated `yaml_reference` to a workflow syndicate is the whole graph,
|
|
42
42
|
* walked under the subagent entry's name and description on a child
|
|
43
43
|
* session (lib/compileNative.ts workflowSubagentOf); its answer is the
|
|
44
|
-
* graph's last event's text. A
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* graph's last event's text. A walk that pauses (an `ask_user` node, a
|
|
45
|
+
* gated agent node) leaves the call open, and the turn ends input-required
|
|
46
|
+
* with the agent path down to the node; the answer walks the graph again
|
|
47
|
+
* (lib/runtime/native/delegate.ts resumeWorkflowSubagent, ADR 0111).
|
|
47
48
|
*
|
|
48
49
|
* ── As a dispatch route or a workflow node (ADR 0106) ────────────────────
|
|
49
50
|
* A `yaml_reference` to a workflow syndicate runs its whole graph there too,
|
|
50
51
|
* on the child session filed under the entry's name, as a subagent does. A
|
|
51
52
|
* route's answer is what the graph would answer as its own syndicate
|
|
52
53
|
* (lib/runtime/syndicateTurn.ts); a node's is the nested walk's
|
|
53
|
-
* (lib/workflow/turn.ts). A map over one is refused (lib/compile.ts)
|
|
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).
|
|
54
58
|
*
|
|
55
59
|
* ── Approval gates (ADR 0098, ADR 0106) ──────────────────────────────────
|
|
56
60
|
* A tool in a node agent's `require_approval`, and a skill script
|
package/dist/lib/workflow.js
CHANGED
|
@@ -41,16 +41,20 @@
|
|
|
41
41
|
* A delegated `yaml_reference` to a workflow syndicate is the whole graph,
|
|
42
42
|
* walked under the subagent entry's name and description on a child
|
|
43
43
|
* session (lib/compileNative.ts workflowSubagentOf); its answer is the
|
|
44
|
-
* graph's last event's text. A
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* graph's last event's text. A walk that pauses (an `ask_user` node, a
|
|
45
|
+
* gated agent node) leaves the call open, and the turn ends input-required
|
|
46
|
+
* with the agent path down to the node; the answer walks the graph again
|
|
47
|
+
* (lib/runtime/native/delegate.ts resumeWorkflowSubagent, ADR 0111).
|
|
47
48
|
*
|
|
48
49
|
* ── As a dispatch route or a workflow node (ADR 0106) ────────────────────
|
|
49
50
|
* A `yaml_reference` to a workflow syndicate runs its whole graph there too,
|
|
50
51
|
* on the child session filed under the entry's name, as a subagent does. A
|
|
51
52
|
* route's answer is what the graph would answer as its own syndicate
|
|
52
53
|
* (lib/runtime/syndicateTurn.ts); a node's is the nested walk's
|
|
53
|
-
* (lib/workflow/turn.ts). A map over one is refused (lib/compile.ts)
|
|
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).
|
|
54
58
|
*
|
|
55
59
|
* ── Approval gates (ADR 0098, ADR 0106) ──────────────────────────────────
|
|
56
60
|
* A tool in a node agent's `require_approval`, and a skill script
|
|
@@ -80,6 +80,12 @@ export interface PendingInput {
|
|
|
80
80
|
payload?: unknown;
|
|
81
81
|
/** JSON Schema a structured answer must satisfy, when the node declared one. */
|
|
82
82
|
schema?: unknown;
|
|
83
|
+
/**
|
|
84
|
+
* For a question asked inside a delegated subagent (ADR 0110): the agents
|
|
85
|
+
* from the turn's own agent down to the one that asked, `node` last.
|
|
86
|
+
* Absent when the turn's own agent (or a workflow node) asked.
|
|
87
|
+
*/
|
|
88
|
+
path?: string[];
|
|
83
89
|
}
|
|
84
90
|
/** ADK's function-call name for a workflow input request. */
|
|
85
91
|
export declare const INPUT_REQUEST = "adk_request_input";
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import type { Server } from 'node:http';
|
|
3
3
|
import { createA2AApp } from '../lib/a2a/app.ts';
|
|
4
|
+
import type { SyndicateYamlConfig } from '../lib/loadSyndicate.ts';
|
|
5
|
+
import type { OAuthServerSetup } from '../lib/a2a/oauthSetup.ts';
|
|
6
|
+
import type { AuditSink } from '../lib/observability/audit.ts';
|
|
7
|
+
import type { CredentialRows } from '../lib/tools/credentialStore.ts';
|
|
4
8
|
export { describeTurnError } from '../lib/a2a/executor.ts';
|
|
5
9
|
/**
|
|
6
10
|
* What a deployment adds on top of the environment-driven server: its own
|
|
@@ -15,4 +19,36 @@ export interface ServerExtensions {
|
|
|
15
19
|
/** Any other createA2AApp option; wins over what the environment sets. */
|
|
16
20
|
options?: Partial<Parameters<typeof createA2AApp>[0]>;
|
|
17
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* The syndicates this server serves from FILES: the default (unless it is a
|
|
24
|
+
* registry row), each served id that is not a registry id, and, when every
|
|
25
|
+
* file is served (A2A_SERVED_AGENTS unset or *), every syndicate at the
|
|
26
|
+
* agents directory's root. The consent step's OAuth clients come from these
|
|
27
|
+
* only (ADR 0114): a registry row may name a provider they declare, never
|
|
28
|
+
* define one. A file that does not load is skipped with a warning (its route
|
|
29
|
+
* fails on its own).
|
|
30
|
+
*/
|
|
31
|
+
export declare function servedFileSyndicates(syndicateName: string, servedAgents: string[] | undefined, registryAgents: string[] | undefined, warn?: (m: string) => void): SyndicateYamlConfig[];
|
|
32
|
+
/** The server's tool credentials, from the environment: what createA2AApp takes, and what to print. */
|
|
33
|
+
export interface ServerOAuth extends OAuthServerSetup {
|
|
34
|
+
toolCredentials?: OAuthServerSetup['toolCredentials'] & {
|
|
35
|
+
requireCallerIdentity?: boolean;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
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
|
|
42
|
+
* variables only, on a configuration that is unsafe or cannot work.
|
|
43
|
+
*/
|
|
44
|
+
export declare function serverOAuth(options: {
|
|
45
|
+
syndicateName: string;
|
|
46
|
+
servedAgents?: string[];
|
|
47
|
+
registryAgents?: string[];
|
|
48
|
+
rows?: CredentialRows;
|
|
49
|
+
audit?: AuditSink;
|
|
50
|
+
env?: NodeJS.ProcessEnv;
|
|
51
|
+
fetch?: typeof fetch;
|
|
52
|
+
warn?: (m: string) => void;
|
|
53
|
+
}): ServerOAuth;
|
|
18
54
|
export declare function startServer(syndicateName?: string, extensions?: ServerExtensions): Promise<Server>;
|
|
@@ -75,9 +75,24 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
75
75
|
* running turn (default 20). Behind a session-mode
|
|
76
76
|
* pooler (Supabase :5432) keep the two sums under its
|
|
77
77
|
* client limit, per instance.
|
|
78
|
+
* MELCHIZEDEK_CREDENTIAL_KEY 32 random bytes (base64 or hex) that seal each user's
|
|
79
|
+
* third-party OAuth tokens (ADR 0072). Unset = no tool
|
|
80
|
+
* credentials. Rows live in Postgres with DATABASE_URL,
|
|
81
|
+
* else in process memory
|
|
82
|
+
* OAUTH_REDIRECT_URI the consent callback, exactly as registered at every
|
|
83
|
+
* provider (https://agents.example.com/oauth/callback);
|
|
84
|
+
* mounts the callback and lets a run ask a user to
|
|
85
|
+
* connect (ADR 0085). Needs MELCHIZEDEK_CREDENTIAL_KEY
|
|
86
|
+
* OAUTH_CALLBACK_IDENTITY required (default): the callback's browser must carry the
|
|
87
|
+
* flow's user's identity (A2A_AUTH=header behind a gateway);
|
|
88
|
+
* state: the single-use state nonce alone binds it (ADR 0114)
|
|
89
|
+
* MELCHIZEDEK_OAUTH_HOSTS provider=host,host;…: the hosts each provider's tokens may
|
|
90
|
+
* be sent to. Unset: authorization_code grants are refused,
|
|
91
|
+
* client_credentials allowed (ADR 0114)
|
|
78
92
|
*/
|
|
79
93
|
import { randomBytes } from 'node:crypto';
|
|
80
|
-
import { realpathSync, writeFileSync } from 'node:fs';
|
|
94
|
+
import { readdirSync, realpathSync, writeFileSync } from 'node:fs';
|
|
95
|
+
import path from 'node:path';
|
|
81
96
|
import { pathToFileURL } from 'node:url';
|
|
82
97
|
import { createA2AApp } from '../lib/a2a/app.js';
|
|
83
98
|
import { callerTokens, firstOf, hashCallerToken, jwtIdentity, parseCallers, sharedSecret, trustedHeader, } from '../lib/a2a/identity.js';
|
|
@@ -90,6 +105,10 @@ import { postgresStorage } from '../lib/storage/postgres/index.js';
|
|
|
90
105
|
import { isPlaceholderValue, loadEnv } from '../lib/loadEnv.js';
|
|
91
106
|
import { flushTracing } from '../lib/observability/tracer.js';
|
|
92
107
|
import { setLogLevel } from '../lib/runtime/logging.js';
|
|
108
|
+
import { loadSyndicate } from '../lib/loadSyndicate.js';
|
|
109
|
+
import { callbackIdentity, callbackIdentityProblem, oauthServerSetup } from '../lib/a2a/oauthSetup.js';
|
|
110
|
+
import { postgresCredentialRows } from '../lib/storage/postgres/credentialStore.js';
|
|
111
|
+
import { setOAuthHosts } from '../lib/tools/oauthHosts.js';
|
|
93
112
|
// Re-exported for existing importers (tests/failNarration.test.ts).
|
|
94
113
|
export { describeTurnError } from '../lib/a2a/executor.js';
|
|
95
114
|
/** Minimum bearer-secret length below which the server warns. */
|
|
@@ -132,6 +151,87 @@ function fatal(message) {
|
|
|
132
151
|
emit('error', `[A2A] ✗ FATAL: ${message}`);
|
|
133
152
|
process.exit(1);
|
|
134
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* The syndicates this server serves from FILES: the default (unless it is a
|
|
156
|
+
* registry row), each served id that is not a registry id, and, when every
|
|
157
|
+
* file is served (A2A_SERVED_AGENTS unset or *), every syndicate at the
|
|
158
|
+
* agents directory's root. The consent step's OAuth clients come from these
|
|
159
|
+
* only (ADR 0114): a registry row may name a provider they declare, never
|
|
160
|
+
* define one. A file that does not load is skipped with a warning (its route
|
|
161
|
+
* fails on its own).
|
|
162
|
+
*/
|
|
163
|
+
export function servedFileSyndicates(syndicateName, servedAgents, registryAgents, warn = () => { }) {
|
|
164
|
+
const registry = new Set(registryAgents ?? []);
|
|
165
|
+
const files = new Set();
|
|
166
|
+
const add = (id) => {
|
|
167
|
+
if (id.startsWith('registry:') || registry.has(id))
|
|
168
|
+
return;
|
|
169
|
+
files.add(id.endsWith('.yaml') || id.endsWith('.yml') ? id : `${id}.yaml`);
|
|
170
|
+
};
|
|
171
|
+
add(syndicateName);
|
|
172
|
+
if (servedAgents) {
|
|
173
|
+
for (const id of servedAgents)
|
|
174
|
+
add(id);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const agentsDir = path.resolve(process.env.MELCHIZEDEK_AGENTS_DIR ?? path.join(process.cwd(), 'config', 'agents'));
|
|
178
|
+
let names = [];
|
|
179
|
+
try {
|
|
180
|
+
names = readdirSync(agentsDir);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
names = [];
|
|
184
|
+
}
|
|
185
|
+
for (const f of names.sort())
|
|
186
|
+
if ((f.endsWith('.yaml') || f.endsWith('.yml')) && f !== 'syndicateSchema.yaml')
|
|
187
|
+
files.add(f);
|
|
188
|
+
}
|
|
189
|
+
const out = [];
|
|
190
|
+
for (const file of files) {
|
|
191
|
+
try {
|
|
192
|
+
out.push(loadSyndicate(file, { shippedFallback: servedAgents !== undefined || file === syndicateName }));
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
if (err?.code !== 'ENOENT')
|
|
196
|
+
warn(`OAuth setup skipped ${file}: ${err instanceof Error ? err.message.split('\n')[0] : String(err)}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* 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
|
|
205
|
+
* variables only, on a configuration that is unsafe or cannot work.
|
|
206
|
+
*/
|
|
207
|
+
export function serverOAuth(options) {
|
|
208
|
+
const env = options.env ?? process.env;
|
|
209
|
+
const identity = callbackIdentity(env);
|
|
210
|
+
const setup = oauthServerSetup({
|
|
211
|
+
configs: servedFileSyndicates(options.syndicateName, options.servedAgents, options.registryAgents, options.warn),
|
|
212
|
+
load: (ref) => loadSyndicate(ref),
|
|
213
|
+
env,
|
|
214
|
+
...(options.rows ? { rows: options.rows } : {}),
|
|
215
|
+
...(options.audit ? { audit: options.audit } : {}),
|
|
216
|
+
allowPrivate: env.ALLOW_PRIVATE_OPENAPI === 'true' || env.ALLOW_PRIVATE_MCP === 'true',
|
|
217
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
218
|
+
});
|
|
219
|
+
if (!setup.toolCredentials?.consent)
|
|
220
|
+
return setup;
|
|
221
|
+
const warnings = [...setup.warnings];
|
|
222
|
+
const refused = callbackIdentityProblem(env);
|
|
223
|
+
if (identity === 'state') {
|
|
224
|
+
warnings.push('OAUTH_CALLBACK_IDENTITY=state: the single-use state alone binds a consent callback to its user, so a forwarded authorization link can connect the forwarder\'s account to the user who was asked.');
|
|
225
|
+
}
|
|
226
|
+
else if (refused) {
|
|
227
|
+
warnings.push(`${refused.charAt(0).toUpperCase()}${refused.slice(1)}.`);
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
...setup,
|
|
231
|
+
warnings,
|
|
232
|
+
toolCredentials: { ...setup.toolCredentials, requireCallerIdentity: identity === 'required' },
|
|
233
|
+
};
|
|
234
|
+
}
|
|
135
235
|
export async function startServer(syndicateName = 'syndicate.yaml', extensions = {}) {
|
|
136
236
|
loadEnv(import.meta.url);
|
|
137
237
|
// Conversation content stays out of stdout unless the operator opts in:
|
|
@@ -319,6 +419,25 @@ export async function startServer(syndicateName = 'syndicate.yaml', extensions =
|
|
|
319
419
|
if (metricsToken && (isPlaceholderValue(metricsToken) || metricsToken.length < 24)) {
|
|
320
420
|
fatal('A2A_METRICS_TOKEN must be a long random value (openssl rand -hex 32).');
|
|
321
421
|
}
|
|
422
|
+
// ── Tool credentials and the OAuth consent step (ADR 0072, 0085, 0114) ────
|
|
423
|
+
let oauth;
|
|
424
|
+
try {
|
|
425
|
+
// An extension's allowlist wins over the environment's here too, as createA2AApp applies it.
|
|
426
|
+
if (extensions.options?.oauthHosts)
|
|
427
|
+
setOAuthHosts(extensions.options.oauthHosts);
|
|
428
|
+
oauth = serverOAuth({
|
|
429
|
+
syndicateName,
|
|
430
|
+
servedAgents,
|
|
431
|
+
registryAgents,
|
|
432
|
+
...(pgStorage ? { rows: postgresCredentialRows(pgStorage.pool), audit: pgStorage.audit } : {}),
|
|
433
|
+
warn: (m) => emit('warn', `[A2A] ⚠ ${m}`),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
438
|
+
}
|
|
439
|
+
for (const w of oauth.warnings)
|
|
440
|
+
emit('warn', `[A2A] ⚠ ${w}`);
|
|
322
441
|
let built;
|
|
323
442
|
try {
|
|
324
443
|
built = await createA2AApp({
|
|
@@ -345,6 +464,7 @@ export async function startServer(syndicateName = 'syndicate.yaml', extensions =
|
|
|
345
464
|
keyMode,
|
|
346
465
|
...(pgStorage ? { storage: pgStorage } : {}),
|
|
347
466
|
...(policy ? { policy } : {}),
|
|
467
|
+
...(oauth.toolCredentials ? { toolCredentials: oauth.toolCredentials } : {}),
|
|
348
468
|
metricsToken,
|
|
349
469
|
log: (m) => emit('info', `[A2A] ${m}`),
|
|
350
470
|
warn: (m) => emit('warn', `[A2A] ⚠ ${m}`),
|
|
@@ -388,6 +508,7 @@ export async function startServer(syndicateName = 'syndicate.yaml', extensions =
|
|
|
388
508
|
}
|
|
389
509
|
if (budgetLabel)
|
|
390
510
|
emit('info', `[A2A] budgets ${budgetLabel}`);
|
|
511
|
+
emit('info', `[A2A] oauth ${oauth.summary}`);
|
|
391
512
|
if (metricsToken)
|
|
392
513
|
emit('info', `[A2A] metrics ${base}/metrics (bearer A2A_METRICS_TOKEN)`);
|
|
393
514
|
emit('info', `[A2A] health ${base}/healthz ${base}/readyz`);
|
|
@@ -21,6 +21,16 @@
|
|
|
21
21
|
* instead of abandoning it while it keeps spending. Any syndicate and any
|
|
22
22
|
* subagent in it can serve as the worker.
|
|
23
23
|
*
|
|
24
|
+
* A run is durable (ADR 0113): runDurableTurn (lib/runtime/native/
|
|
25
|
+
* checkpoint.ts) checkpoints the run's sessions at every step boundary
|
|
26
|
+
* beside the job (TaskBackend.saveCheckpoint), and a job claimed again
|
|
27
|
+
* after its worker stopped resumes from its last checkpoint instead of
|
|
28
|
+
* starting over. SIGTERM aborts the step in flight and puts the job back
|
|
29
|
+
* in the queue with its checkpoint; the next worker finishes it. A job
|
|
30
|
+
* cancelled while it runs (task_update) loses its claim: the next lease
|
|
31
|
+
* renewal or checkpoint save sees it, and the run is aborted without
|
|
32
|
+
* writing a result over the cancellation.
|
|
33
|
+
*
|
|
24
34
|
* Usage:
|
|
25
35
|
* npm run assistant:worker poll every 30 s until Ctrl-C
|
|
26
36
|
* npm run assistant:worker -- --once drain the queue, then exit (cron)
|