melchizedek-agents 1.1.0 → 1.2.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 +10 -0
- package/AGENT_SETUP.md +3 -1
- package/CHANGELOG.md +232 -0
- package/DOCUMENTATION.md +91 -16
- package/ONBOARDING.md +339 -0
- package/QUICKSTART.md +15 -2
- package/README.md +5 -2
- 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 +65 -7
- 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 +22 -11
- package/dist/lib/a2a/executor.js +14 -6
- package/dist/lib/a2a/oauthSetup.d.ts +16 -0
- package/dist/lib/a2a/oauthSetup.js +57 -3
- package/dist/lib/agentDialect.d.ts +18 -8
- package/dist/lib/agentDialect.js +36 -19
- package/dist/lib/compile.d.ts +38 -7
- package/dist/lib/compile.js +57 -37
- package/dist/lib/compileNative.d.ts +11 -3
- package/dist/lib/compileNative.js +18 -1
- package/dist/lib/doctor.d.ts +76 -0
- package/dist/lib/doctor.js +159 -7
- package/dist/lib/loadSyndicate.d.ts +9 -0
- 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 +2 -1
- package/dist/lib/models/gateway.js +4 -2
- 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 +134 -0
- package/dist/lib/onboarding.js +539 -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 +295 -62
- 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 +38 -1
- package/dist/lib/syndicateSchema.js +25 -18
- 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 +92 -0
- package/dist/lib/tools/credentialUses.js +112 -0
- package/dist/lib/tools/mcpToolFactory.js +1 -1
- package/dist/lib/tools/oauthConsent.js +8 -1
- package/dist/lib/tools/oauthHosts.d.ts +2 -0
- package/dist/lib/tools/oauthHosts.js +2 -2
- package/dist/lib/tools/oauthTools.d.ts +10 -1
- package/dist/lib/tools/oauthTools.js +29 -3
- 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/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 +11 -2
- package/dist/scripts/assistant_worker.d.ts +2 -0
- package/dist/scripts/assistant_worker.js +13 -4
- package/dist/scripts/doctor.d.ts +4 -2
- package/dist/scripts/doctor.js +6 -4
- package/dist/scripts/setup.d.ts +23 -0
- package/dist/scripts/setup.js +154 -0
- package/dist/scripts/skills_install.d.ts +3 -1
- package/dist/scripts/skills_install.js +17 -2
- package/dist/scripts/yaml_codemod.d.ts +12 -2
- package/dist/scripts/yaml_codemod.js +115 -33
- package/package.json +4 -1
- 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 +49 -0
- package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
- package/skills/melchizedek-onboard-keys/SKILL.md +35 -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
|
|
@@ -129,6 +132,31 @@ export interface TaskBackend {
|
|
|
129
132
|
/** The checkpoint a re-queued job left, or null. */
|
|
130
133
|
loadCheckpoint?(job: OwnedTask): Promise<object | null>;
|
|
131
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The largest checkpoint a backend stores by default: 5 MiB of serialized
|
|
137
|
+
* JSON (ADR 0113). A save above the cap is skipped and the job's previous
|
|
138
|
+
* checkpoint kept, so a huge run resumes from an older step boundary instead
|
|
139
|
+
* of failing or bloating the table.
|
|
140
|
+
*/
|
|
141
|
+
export declare const DEFAULT_CHECKPOINT_MAX_BYTES: number;
|
|
142
|
+
/** The worker's variable that moves the cap, in bytes (scripts/assistant_worker.ts). */
|
|
143
|
+
export declare const CHECKPOINT_MAX_BYTES_ENV = "MELCHIZEDEK_CHECKPOINT_MAX_BYTES";
|
|
144
|
+
/** How a backend stores checkpoints (fileTaskBackendWith, postgresTaskBackend). */
|
|
145
|
+
export interface TaskBackendOptions {
|
|
146
|
+
/** The largest checkpoint stored, in bytes of serialized JSON. Default DEFAULT_CHECKPOINT_MAX_BYTES; a value that is not a positive integer is the default. */
|
|
147
|
+
checkpointMaxBytes?: number;
|
|
148
|
+
/** Where a skipped save is reported: one line naming the job id and the sizes, never the checkpoint. Default console.warn. */
|
|
149
|
+
log?: (line: string) => void;
|
|
150
|
+
}
|
|
151
|
+
/** The cap a variable's value sets: a positive whole number of bytes, else the default. */
|
|
152
|
+
export declare function checkpointMaxBytesSetting(raw: string | undefined): number;
|
|
153
|
+
/**
|
|
154
|
+
* The checkpoint serialized once, as the backend stores it, or null when it
|
|
155
|
+
* is above the cap: then one line is logged with the job id and the sizes
|
|
156
|
+
* only (the checkpoint carries the user's text and tool results), and the
|
|
157
|
+
* caller keeps the previous checkpoint.
|
|
158
|
+
*/
|
|
159
|
+
export declare function checkpointJson(job: Pick<OwnedTask, 'id'>, checkpoint: object, options?: TaskBackendOptions): string | null;
|
|
132
160
|
/** Records kept at most; finished ones are pruned oldest-first to make room. */
|
|
133
161
|
export declare const MAX_TASKS = 500;
|
|
134
162
|
/** A worker result is stored up to this length, then cut with a marker. */
|
|
@@ -154,7 +182,9 @@ export declare function finishJob(id: string, outcome: JobOutcome): void;
|
|
|
154
182
|
export declare function applyFinish(store: TaskStore, id: string, outcome: JobOutcome): void;
|
|
155
183
|
/** The interrupted-job rule for one record: back to the queue, or failed. */
|
|
156
184
|
export declare function applyInterrupted(t: TaskRecord): 'requeued' | 'failed';
|
|
157
|
-
/** The single-user JSON file (the default backend). */
|
|
185
|
+
/** The single-user JSON file, its checkpoints capped by `options` (the default backend is fileTaskBackend). */
|
|
186
|
+
export declare function fileTaskBackendWith(options?: TaskBackendOptions): TaskBackend;
|
|
187
|
+
/** The single-user JSON file (the default backend), checkpoints capped at DEFAULT_CHECKPOINT_MAX_BYTES. */
|
|
158
188
|
export declare const fileTaskBackend: TaskBackend;
|
|
159
189
|
/** Plugs in where tasks live (postgresStorage().taskQueue); default the JSON file. */
|
|
160
190
|
export declare function setTaskBackend(backend: TaskBackend): void;
|
|
@@ -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) {
|
|
@@ -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';
|
|
@@ -109,6 +114,7 @@ import { loadSyndicate } from '../lib/loadSyndicate.js';
|
|
|
109
114
|
import { callbackIdentity, callbackIdentityProblem, oauthServerSetup } from '../lib/a2a/oauthSetup.js';
|
|
110
115
|
import { postgresCredentialRows } from '../lib/storage/postgres/credentialStore.js';
|
|
111
116
|
import { setOAuthHosts } from '../lib/tools/oauthHosts.js';
|
|
117
|
+
import { setCredentialHosts } from '../lib/tools/credentialHosts.js';
|
|
112
118
|
// Re-exported for existing importers (tests/failNarration.test.ts).
|
|
113
119
|
export { describeTurnError } from '../lib/a2a/executor.js';
|
|
114
120
|
/** Minimum bearer-secret length below which the server warns. */
|
|
@@ -200,8 +206,8 @@ export function servedFileSyndicates(syndicateName, servedAgents, registryAgents
|
|
|
200
206
|
}
|
|
201
207
|
/**
|
|
202
208
|
* Tool credentials and the consent step for the served syndicates, from
|
|
203
|
-
* MELCHIZEDEK_CREDENTIAL_KEY, OAUTH_REDIRECT_URI, OAUTH_CALLBACK_IDENTITY
|
|
204
|
-
* and
|
|
209
|
+
* MELCHIZEDEK_CREDENTIAL_KEY, OAUTH_REDIRECT_URI, OAUTH_CALLBACK_IDENTITY,
|
|
210
|
+
* MELCHIZEDEK_OAUTH_HOSTS and MELCHIZEDEK_CREDENTIAL_HOSTS (lib/a2a/oauthSetup.ts). Throws, naming
|
|
205
211
|
* variables only, on a configuration that is unsafe or cannot work.
|
|
206
212
|
*/
|
|
207
213
|
export function serverOAuth(options) {
|
|
@@ -425,6 +431,8 @@ export async function startServer(syndicateName = 'syndicate.yaml', extensions =
|
|
|
425
431
|
// An extension's allowlist wins over the environment's here too, as createA2AApp applies it.
|
|
426
432
|
if (extensions.options?.oauthHosts)
|
|
427
433
|
setOAuthHosts(extensions.options.oauthHosts);
|
|
434
|
+
if (extensions.options?.credentialHosts)
|
|
435
|
+
setCredentialHosts(extensions.options.credentialHosts);
|
|
428
436
|
oauth = serverOAuth({
|
|
429
437
|
syndicateName,
|
|
430
438
|
servedAgents,
|
|
@@ -509,6 +517,7 @@ export async function startServer(syndicateName = 'syndicate.yaml', extensions =
|
|
|
509
517
|
if (budgetLabel)
|
|
510
518
|
emit('info', `[A2A] budgets ${budgetLabel}`);
|
|
511
519
|
emit('info', `[A2A] oauth ${oauth.summary}`);
|
|
520
|
+
emit('info', `[A2A] creds ${oauth.credentialSummary}`);
|
|
512
521
|
if (metricsToken)
|
|
513
522
|
emit('info', `[A2A] metrics ${base}/metrics (bearer A2A_METRICS_TOKEN)`);
|
|
514
523
|
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 {};
|
|
@@ -40,6 +40,8 @@
|
|
|
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
|
import { randomUUID } from 'node:crypto';
|
|
45
47
|
import { loadEnv } from '../lib/loadEnv.js';
|
|
@@ -48,7 +50,7 @@ import { runDurableTurn } from '../lib/runtime/native/checkpoint.js';
|
|
|
48
50
|
import { runtimeSetting } from '../lib/runtime/runtimeFlag.js';
|
|
49
51
|
import { setLogLevel } from '../lib/runtime/logging.js';
|
|
50
52
|
import { PROVIDERS, providerForModel, providerKeyPresent, } from '../lib/models/registry.js';
|
|
51
|
-
import { applyInterrupted, getTaskBackend, setTaskBackend, taskStorePath } from '../lib/tools/taskTools.js';
|
|
53
|
+
import { CHECKPOINT_MAX_BYTES_ENV, applyInterrupted, checkpointMaxBytesSetting, fileTaskBackendWith, getTaskBackend, setTaskBackend, taskStorePath, } from '../lib/tools/taskTools.js';
|
|
52
54
|
import { postgresStorage } from '../lib/storage/postgres/index.js';
|
|
53
55
|
import { dbSchema } from '../lib/storage/schema.js';
|
|
54
56
|
import { hostname } from 'node:os';
|
|
@@ -90,10 +92,17 @@ if (!providerKeyPresent(provider)) {
|
|
|
90
92
|
const log = (message) => console.log(`[worker] ${message}`);
|
|
91
93
|
// The queue: the JSON file, or Postgres when DATABASE_URL is set, where any
|
|
92
94
|
// number of workers may run (each claim is FOR UPDATE SKIP LOCKED).
|
|
95
|
+
// A checkpoint above MELCHIZEDEK_CHECKPOINT_MAX_BYTES (bytes of JSON, default
|
|
96
|
+
// 5 MiB) is not saved: the job keeps its previous one (ADR 0113).
|
|
97
|
+
const rawCap = process.env[CHECKPOINT_MAX_BYTES_ENV];
|
|
98
|
+
const checkpointMaxBytes = checkpointMaxBytesSetting(rawCap);
|
|
99
|
+
if (rawCap?.trim() && Number(rawCap.trim()) !== checkpointMaxBytes) {
|
|
100
|
+
log(`${CHECKPOINT_MAX_BYTES_ENV} is not a positive whole number of bytes; using ${checkpointMaxBytes}`);
|
|
101
|
+
}
|
|
102
|
+
const taskQueue = { checkpointMaxBytes, log };
|
|
93
103
|
const databaseUrl = process.env.DATABASE_URL?.trim();
|
|
94
|
-
const pg = databaseUrl ? postgresStorage({ connectionString: databaseUrl, schema: dbSchema() }) : undefined;
|
|
95
|
-
|
|
96
|
-
setTaskBackend(pg.taskQueue);
|
|
104
|
+
const pg = databaseUrl ? postgresStorage({ connectionString: databaseUrl, schema: dbSchema(), taskQueue }) : undefined;
|
|
105
|
+
setTaskBackend(pg ? pg.taskQueue : fileTaskBackendWith(taskQueue));
|
|
97
106
|
const backend = getTaskBackend();
|
|
98
107
|
/** A claimed job stays this worker's while it renews; the job timeout bounds a turn. */
|
|
99
108
|
const lease = { workerId: `${hostname()}:${process.pid}:${randomUUID().slice(0, 8)}`, leaseMs: 60_000 };
|