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
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/runtime/native/checkpoint.ts — durable long runs: the run's sessions
|
|
3
|
+
* snapshotted at every step boundary, and a run restored from the last
|
|
4
|
+
* snapshot taking the next step (ADR 0113, ADR 0015).
|
|
5
|
+
*
|
|
6
|
+
* WHY this file exists:
|
|
7
|
+
* A background job (ADR 0015) runs as one turn on an in-process session
|
|
8
|
+
* store. A worker that stopped mid-job lost everything the job had done,
|
|
9
|
+
* and the next claim started again from the first model call. The turn
|
|
10
|
+
* runner's signature is pinned by every surface and the boundary suite
|
|
11
|
+
* (ADR 0024), so durability lives at the one seam the runner already
|
|
12
|
+
* takes: the SessionService. checkpointingSessions wraps the in-process
|
|
13
|
+
* store a run writes, hands a snapshot of every session to a sink at each
|
|
14
|
+
* step boundary, and restores a snapshot into a fresh store so that the
|
|
15
|
+
* runner, unchanged, continues where the snapshot stood. runDurableTurn
|
|
16
|
+
* is runSyndicateTurn with that store and a sink.
|
|
17
|
+
*
|
|
18
|
+
* A STEP BOUNDARY is an append that carries function responses after which
|
|
19
|
+
* every function call any session of the run holds has its response (calls
|
|
20
|
+
* listed in their event's longRunningToolIds, which wait on a person, aside).
|
|
21
|
+
* So a parent waiting on a delegated subagent is never at a boundary, the
|
|
22
|
+
* subagent's own steps are not either, and a final answer never is: a
|
|
23
|
+
* resumed run redoes the step that was in flight, and the last one.
|
|
24
|
+
*
|
|
25
|
+
* WHAT IT GUARANTEES:
|
|
26
|
+
* - A resumed run's stored events equal an uninterrupted run's, ids and
|
|
27
|
+
* times of the steps run after the resume aside: restored events keep
|
|
28
|
+
* their ids, times and invocation ids, and their state deltas are
|
|
29
|
+
* applied again in order (InProcessSessionService.append, applyEvent).
|
|
30
|
+
* - The opening message is stored once. A session whose first event is
|
|
31
|
+
* the run's own message (the run's session, a workflow route's child)
|
|
32
|
+
* is held back on restore: it does not exist until the runner creates
|
|
33
|
+
* it, so `resumedSession` reads as on a fresh run, and the runner's
|
|
34
|
+
* append of the message is answered with the stored events, the
|
|
35
|
+
* opening one returned in its place, onto the caller's own session
|
|
36
|
+
* object. Every other session (a delegated subagent's) is restored at
|
|
37
|
+
* once, before the runner starts.
|
|
38
|
+
* - A checkpoint is used only by the run that wrote it, for the message
|
|
39
|
+
* it was written for: its runId and the sha256 of the message's parts
|
|
40
|
+
* must match, and its events must parse (parseTurnEvents). Otherwise
|
|
41
|
+
* the run starts fresh.
|
|
42
|
+
* - The sink's save resolving false means the run no longer holds its
|
|
43
|
+
* claim (the job was cancelled or leased elsewhere): runDurableTurn
|
|
44
|
+
* aborts the run and reports lostClaim. A save that throws is reported
|
|
45
|
+
* to onSaveError and the run goes on; a later attempt resumes from the
|
|
46
|
+
* older checkpoint.
|
|
47
|
+
*
|
|
48
|
+
* LIMITS:
|
|
49
|
+
* - A dispatch syndicate's route classifier runs in a throwaway store, so
|
|
50
|
+
* its model call is not checkpointed and runs again on resume.
|
|
51
|
+
* - A dispatch route that is an agent answers through the transcript
|
|
52
|
+
* projection (ProjectedSessionService, lib/session/transcript.ts), which
|
|
53
|
+
* copies the session when the route opens it and applies only the
|
|
54
|
+
* route's own appends to that copy. Restored events cannot reach that
|
|
55
|
+
* copy through the store, so a checkpoint taken inside such a route is
|
|
56
|
+
* not resumed: runDurableTurn starts that run fresh. A workflow route
|
|
57
|
+
* writes its own child session and resumes.
|
|
58
|
+
* - Each resumed attempt has its own step budget (max_steps, the loop's
|
|
59
|
+
* call ceiling); attempts are bounded by the queue's MAX_ATTEMPTS.
|
|
60
|
+
* - `temp:` state lasts one invocation and is never stored, so it does not
|
|
61
|
+
* survive a resume.
|
|
62
|
+
* - A workflow walk resumes through its own rules (ADR 0094); its
|
|
63
|
+
* checkpoint is the sessions it walks.
|
|
64
|
+
* - A checkpoint holds the run's sessions, so it carries the user's text,
|
|
65
|
+
* model output and tool results: it lives only where the run's record
|
|
66
|
+
* lives, under the same owner, and is deleted with it. This module never
|
|
67
|
+
* clears one; the caller does when the run finishes.
|
|
68
|
+
*/
|
|
69
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
70
|
+
import { isDispatchSyndicate } from '../../dispatch.js';
|
|
71
|
+
import { getFunctionCalls, getFunctionResponses, parseTurnEvents } from '../events.js';
|
|
72
|
+
import { InProcessSessionService } from '../sessions.js';
|
|
73
|
+
import { runSyndicateTurn } from '../syndicateTurn.js';
|
|
74
|
+
// ── Shapes ───────────────────────────────────────────────────────────────────
|
|
75
|
+
export const CHECKPOINT_VERSION = 1;
|
|
76
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
77
|
+
/** The parts as runSyndicateTurn stores them: a bare string becomes `{ text }`. */
|
|
78
|
+
function normalizedParts(parts) {
|
|
79
|
+
return parts.map((p) => (typeof p === 'string' ? { text: p } : p));
|
|
80
|
+
}
|
|
81
|
+
/** sha256 hex of the message's parts, as a checkpoint names its run's message. */
|
|
82
|
+
export function openingHashOf(parts) {
|
|
83
|
+
return createHash('sha256').update(JSON.stringify(normalizedParts(parts))).digest('hex');
|
|
84
|
+
}
|
|
85
|
+
const keyOf = (appName, userId, id) => JSON.stringify([appName, userId, id]);
|
|
86
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
87
|
+
/**
|
|
88
|
+
* The checkpoint, checked, when it belongs to this run and this message;
|
|
89
|
+
* null otherwise. A stored checkpoint is data read back from a table or a
|
|
90
|
+
* file, so every field the restore reads is checked before it is trusted.
|
|
91
|
+
*/
|
|
92
|
+
export function usableCheckpoint(cp, runId, parts) {
|
|
93
|
+
if (!isRecord(cp) || cp.version !== CHECKPOINT_VERSION)
|
|
94
|
+
return null;
|
|
95
|
+
if (cp.runId !== runId || cp.openingHash !== openingHashOf(parts))
|
|
96
|
+
return null;
|
|
97
|
+
if (typeof cp.sessionId !== 'string' || !cp.sessionId)
|
|
98
|
+
return null;
|
|
99
|
+
if (typeof cp.steps !== 'number' || !Number.isInteger(cp.steps) || cp.steps < 0)
|
|
100
|
+
return null;
|
|
101
|
+
if (!Array.isArray(cp.sessions))
|
|
102
|
+
return null;
|
|
103
|
+
try {
|
|
104
|
+
for (const [i, s] of cp.sessions.entries()) {
|
|
105
|
+
if (!isRecord(s))
|
|
106
|
+
return null;
|
|
107
|
+
if (typeof s.appName !== 'string' || typeof s.userId !== 'string' || typeof s.id !== 'string')
|
|
108
|
+
return null;
|
|
109
|
+
if (!isRecord(s.createdState) || typeof s.opening !== 'boolean')
|
|
110
|
+
return null;
|
|
111
|
+
parseTurnEvents(s.events, `sessions[${i}].events`);
|
|
112
|
+
if (s.opening && s.events.length === 0)
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return cp;
|
|
120
|
+
}
|
|
121
|
+
/** Content compared as stored JSON. */
|
|
122
|
+
const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
123
|
+
class CheckpointingStore {
|
|
124
|
+
inner;
|
|
125
|
+
opts;
|
|
126
|
+
openingParts;
|
|
127
|
+
tracked = new Map();
|
|
128
|
+
/** Call ids with no response yet, across every session. */
|
|
129
|
+
openCalls = new Set();
|
|
130
|
+
seq = 0;
|
|
131
|
+
/** The order of the first event an agent wrote; -1 when a restored run already had one. */
|
|
132
|
+
firstAgentSeq;
|
|
133
|
+
savesLost = false;
|
|
134
|
+
saving = Promise.resolve();
|
|
135
|
+
stepCount = 0;
|
|
136
|
+
/** The eager restore; every method waits on it first. */
|
|
137
|
+
ready = Promise.resolve();
|
|
138
|
+
runSessionId;
|
|
139
|
+
restored;
|
|
140
|
+
constructor(opts) {
|
|
141
|
+
this.opts = opts;
|
|
142
|
+
this.inner = opts.inner ?? new InProcessSessionService();
|
|
143
|
+
this.openingParts = JSON.stringify(normalizedParts(opts.parts));
|
|
144
|
+
this.runSessionId = opts.sessionId;
|
|
145
|
+
const from = opts.from ? usableCheckpoint(opts.from, opts.runId, opts.parts) : null;
|
|
146
|
+
this.restored = !!from && from.sessionId === opts.sessionId;
|
|
147
|
+
if (this.restored && from)
|
|
148
|
+
this.load(from);
|
|
149
|
+
}
|
|
150
|
+
get steps() {
|
|
151
|
+
return this.stepCount;
|
|
152
|
+
}
|
|
153
|
+
/** Records the checkpoint: subagent sessions restored now, opening sessions held for the runner. */
|
|
154
|
+
load(from) {
|
|
155
|
+
this.stepCount = from.steps;
|
|
156
|
+
const copy = structuredClone(from.sessions);
|
|
157
|
+
if (copy.some((s) => s.events.some((e) => e.author !== 'user')))
|
|
158
|
+
this.firstAgentSeq = -1;
|
|
159
|
+
const eager = [];
|
|
160
|
+
for (const s of copy) {
|
|
161
|
+
const t = { appName: s.appName, userId: s.userId, id: s.id, createdState: s.createdState, opening: s.opening };
|
|
162
|
+
if (s.opening)
|
|
163
|
+
t.held = s.events;
|
|
164
|
+
else
|
|
165
|
+
eager.push(s);
|
|
166
|
+
this.tracked.set(keyOf(s.appName, s.userId, s.id), t);
|
|
167
|
+
for (const e of s.events)
|
|
168
|
+
this.track(e);
|
|
169
|
+
}
|
|
170
|
+
this.ready = (async () => {
|
|
171
|
+
for (const s of eager) {
|
|
172
|
+
const session = await this.inner.create({ appName: s.appName, userId: s.userId, sessionId: s.id, state: s.createdState });
|
|
173
|
+
for (const e of s.events)
|
|
174
|
+
await this.inner.append(session, e);
|
|
175
|
+
}
|
|
176
|
+
})();
|
|
177
|
+
}
|
|
178
|
+
/** Keeps the set of unanswered calls up to date with one stored event. */
|
|
179
|
+
track(event) {
|
|
180
|
+
const waiting = new Set(event.longRunningToolIds ?? []);
|
|
181
|
+
for (const call of getFunctionCalls(event))
|
|
182
|
+
if (call.id && !waiting.has(call.id))
|
|
183
|
+
this.openCalls.add(call.id);
|
|
184
|
+
for (const response of getFunctionResponses(event))
|
|
185
|
+
if (response.id)
|
|
186
|
+
this.openCalls.delete(response.id);
|
|
187
|
+
}
|
|
188
|
+
async create(request) {
|
|
189
|
+
await this.ready;
|
|
190
|
+
const session = await this.inner.create(request);
|
|
191
|
+
const key = keyOf(session.appName, session.userId, session.id);
|
|
192
|
+
const known = this.tracked.get(key);
|
|
193
|
+
if (!known) {
|
|
194
|
+
this.tracked.set(key, { appName: session.appName, userId: session.userId, id: session.id, createdState: structuredClone(session.state) });
|
|
195
|
+
}
|
|
196
|
+
else if (known.held && session.events.length === 0) {
|
|
197
|
+
known.createdState = structuredClone(session.state);
|
|
198
|
+
}
|
|
199
|
+
return session;
|
|
200
|
+
}
|
|
201
|
+
async get(key, options) {
|
|
202
|
+
await this.ready;
|
|
203
|
+
return this.inner.get(key, options);
|
|
204
|
+
}
|
|
205
|
+
async list(request) {
|
|
206
|
+
await this.ready;
|
|
207
|
+
return this.inner.list(request);
|
|
208
|
+
}
|
|
209
|
+
async delete(key) {
|
|
210
|
+
await this.ready;
|
|
211
|
+
this.tracked.delete(keyOf(key.appName, key.userId, key.sessionId));
|
|
212
|
+
return this.inner.delete(key);
|
|
213
|
+
}
|
|
214
|
+
async append(session, event) {
|
|
215
|
+
await this.ready;
|
|
216
|
+
if (event.partial)
|
|
217
|
+
return this.inner.append(session, event);
|
|
218
|
+
const key = keyOf(session.appName, session.userId, session.id);
|
|
219
|
+
let t = this.tracked.get(key);
|
|
220
|
+
if (!t) {
|
|
221
|
+
// A session the store holds but this run did not create (not expected): tracked from here.
|
|
222
|
+
t = { appName: session.appName, userId: session.userId, id: session.id, createdState: structuredClone(session.state) };
|
|
223
|
+
this.tracked.set(key, t);
|
|
224
|
+
}
|
|
225
|
+
if (t.held) {
|
|
226
|
+
const held = t.held;
|
|
227
|
+
delete t.held;
|
|
228
|
+
const opening = held[0];
|
|
229
|
+
if (opening && event.author === 'user' && sameJson(event.content, opening.content)) {
|
|
230
|
+
// The runner stores the message again: the stored events answer it, onto the caller's session.
|
|
231
|
+
let first;
|
|
232
|
+
for (const e of held) {
|
|
233
|
+
const stored = await this.inner.append(session, e);
|
|
234
|
+
first ??= stored;
|
|
235
|
+
}
|
|
236
|
+
return first;
|
|
237
|
+
}
|
|
238
|
+
// Some other first write: the hold is dropped and the session starts as the runner writes it.
|
|
239
|
+
t.opening = undefined;
|
|
240
|
+
}
|
|
241
|
+
const stored = await this.inner.append(session, event);
|
|
242
|
+
const order = this.seq++;
|
|
243
|
+
t.firstSeq ??= order;
|
|
244
|
+
if (stored.author !== 'user' && this.firstAgentSeq === undefined)
|
|
245
|
+
this.firstAgentSeq = order;
|
|
246
|
+
this.track(stored);
|
|
247
|
+
if (getFunctionResponses(stored).length > 0 && this.openCalls.size === 0)
|
|
248
|
+
await this.boundary();
|
|
249
|
+
return stored;
|
|
250
|
+
}
|
|
251
|
+
/** A session is the run's opening one when its first event is the run's message, stored before any agent wrote. */
|
|
252
|
+
openingOf(t, events) {
|
|
253
|
+
if (t.opening !== undefined)
|
|
254
|
+
return t.opening;
|
|
255
|
+
const first = events[0];
|
|
256
|
+
if (!first || first.author !== 'user')
|
|
257
|
+
return false;
|
|
258
|
+
if (JSON.stringify(first.content?.parts ?? null) !== this.openingParts)
|
|
259
|
+
return false;
|
|
260
|
+
if (this.firstAgentSeq === -1)
|
|
261
|
+
return false;
|
|
262
|
+
return this.firstAgentSeq === undefined || (t.firstSeq !== undefined && t.firstSeq < this.firstAgentSeq);
|
|
263
|
+
}
|
|
264
|
+
async snapshot() {
|
|
265
|
+
const sessions = [];
|
|
266
|
+
for (const t of this.tracked.values()) {
|
|
267
|
+
const events = t.held ?? (await this.inner.get({ appName: t.appName, userId: t.userId, sessionId: t.id }))?.events;
|
|
268
|
+
if (!events)
|
|
269
|
+
continue;
|
|
270
|
+
sessions.push(structuredClone({ appName: t.appName, userId: t.userId, id: t.id, createdState: t.createdState, events, opening: this.openingOf(t, events) }));
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
version: CHECKPOINT_VERSION,
|
|
274
|
+
runId: this.opts.runId,
|
|
275
|
+
sessionId: this.runSessionId,
|
|
276
|
+
openingHash: openingHashOf(this.opts.parts),
|
|
277
|
+
steps: this.stepCount,
|
|
278
|
+
savedAt: new Date().toISOString(),
|
|
279
|
+
sessions,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/** One step boundary: counted, snapshotted, saved; saves run one after another. */
|
|
283
|
+
async boundary() {
|
|
284
|
+
if (this.savesLost)
|
|
285
|
+
return;
|
|
286
|
+
this.stepCount += 1;
|
|
287
|
+
const checkpoint = await this.snapshot();
|
|
288
|
+
const run = this.saving.then(async () => {
|
|
289
|
+
if (this.savesLost)
|
|
290
|
+
return;
|
|
291
|
+
try {
|
|
292
|
+
const kept = await this.opts.save(checkpoint);
|
|
293
|
+
if (kept === false) {
|
|
294
|
+
this.savesLost = true;
|
|
295
|
+
this.opts.onLost?.();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
this.opts.onSaveError?.(err);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
this.saving = run;
|
|
303
|
+
await run;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* A session store that checkpoints a run: the in-process store `inner`,
|
|
308
|
+
* restored from `from` when it is this run's checkpoint for this message,
|
|
309
|
+
* saving a snapshot of every session at each step boundary (see the header).
|
|
310
|
+
*/
|
|
311
|
+
export function checkpointingSessions(opts) {
|
|
312
|
+
return new CheckpointingStore(opts);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* A dispatch route's checkpoint the store cannot resume: the run's own
|
|
316
|
+
* session holds route events past the message, which the route reads
|
|
317
|
+
* through the transcript projection (see LIMITS in the header).
|
|
318
|
+
*/
|
|
319
|
+
function projectedRouteCheckpoint(config, cp, appName, userId) {
|
|
320
|
+
if (!isDispatchSyndicate(config))
|
|
321
|
+
return false;
|
|
322
|
+
const own = cp.sessions.find((s) => s.appName === appName && s.userId === userId && s.id === cp.sessionId);
|
|
323
|
+
return !!own && own.events.length > 1;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* runSyndicateTurn, durable: the run's sessions live in a fresh in-process
|
|
327
|
+
* store, restored from the sink's checkpoint when it is this run's for this
|
|
328
|
+
* message, and checkpointed at every step boundary. A save that resolves
|
|
329
|
+
* false aborts the run (`lostClaim`). The caller clears the checkpoint when
|
|
330
|
+
* the run finishes; this never does.
|
|
331
|
+
*/
|
|
332
|
+
export async function runDurableTurn(opts) {
|
|
333
|
+
const { runId, checkpoints, onLost, onSaveError, sessions: inner, signal, ...turn } = opts;
|
|
334
|
+
const loaded = usableCheckpoint(await checkpoints.load(), runId, turn.parts);
|
|
335
|
+
const from = loaded && !projectedRouteCheckpoint(turn.config, loaded, turn.appName, turn.userId) ? loaded : null;
|
|
336
|
+
const sessionId = from ? from.sessionId : randomUUID();
|
|
337
|
+
const controller = new AbortController();
|
|
338
|
+
const forward = () => controller.abort(signal?.reason);
|
|
339
|
+
if (signal?.aborted)
|
|
340
|
+
forward();
|
|
341
|
+
else
|
|
342
|
+
signal?.addEventListener('abort', forward, { once: true });
|
|
343
|
+
let lostClaim = false;
|
|
344
|
+
const sessionService = checkpointingSessions({
|
|
345
|
+
runId,
|
|
346
|
+
sessionId,
|
|
347
|
+
parts: turn.parts,
|
|
348
|
+
from,
|
|
349
|
+
save: (cp) => checkpoints.save(cp),
|
|
350
|
+
onLost: () => {
|
|
351
|
+
lostClaim = true;
|
|
352
|
+
onLost?.();
|
|
353
|
+
controller.abort(new Error('the run no longer holds its claim'));
|
|
354
|
+
},
|
|
355
|
+
...(onSaveError ? { onSaveError } : {}),
|
|
356
|
+
...(inner ? { inner } : {}),
|
|
357
|
+
});
|
|
358
|
+
try {
|
|
359
|
+
const result = await runSyndicateTurn({ ...turn, sessionId, sessionService, signal: controller.signal });
|
|
360
|
+
return { ...result, resumedFromStep: sessionService.restored && from ? from.steps : 0, lostClaim, sessionId };
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
signal?.removeEventListener('abort', forward);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -13,12 +13,21 @@
|
|
|
13
13
|
*
|
|
14
14
|
* ONE CALL, AS ADK'S AgentTool RAN IT:
|
|
15
15
|
* 1. The message: `{ role: 'user', parts: [{ text: args.request }] }`.
|
|
16
|
-
* 2. The subagent's session:
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* the
|
|
16
|
+
* 2. The subagent's session: filed under the agent path (childAppName,
|
|
17
|
+
* ADR 0111), with the caller's user id and session id: the caller's
|
|
18
|
+
* app name, the caller's name and the subagent's at the top
|
|
19
|
+
* (`<app>/<caller>/<subagent>`), the caller's app name and the
|
|
20
|
+
* subagent's below it (`<app>/<caller>/<subagent>/<inner>`). It is
|
|
21
|
+
* created on the first call, with the caller's state as it stands (the
|
|
22
|
+
* session's, then this call's writes; `temp:` keys dropped), and kept:
|
|
23
|
+
* a later call, in this turn or another, continues it. This is not a
|
|
24
|
+
* branch of the caller's session; ADK's AgentTool runs a Runner of its
|
|
25
|
+
* own. ADK filed it under the subagent's name alone, so two syndicates
|
|
26
|
+
* with a same-named subagent on one conversation shared it. A session
|
|
27
|
+
* stored under that old key is still read (legacyChild): when no
|
|
28
|
+
* session exists under the path and the caller has called the
|
|
29
|
+
* subagent before (or is resuming a pause found there), the old one is
|
|
30
|
+
* continued, so a stored conversation resumes where it stopped.
|
|
22
31
|
* 3. A turn already stopped answers an empty text, before or after the
|
|
23
32
|
* message is stored.
|
|
24
33
|
* 4. The message is stored as a user event under a fresh invocation id
|
|
@@ -34,13 +43,30 @@
|
|
|
34
43
|
* schema it is parsed as JSON; text that does not parse fails the call
|
|
35
44
|
* with the parser's message, as on ADK.
|
|
36
45
|
*
|
|
37
|
-
* A PAUSE INSIDE A SUBAGENT (an ask_user call, an approval request
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
46
|
+
* A PAUSE INSIDE A SUBAGENT (an ask_user call, an approval request, or a
|
|
47
|
+
* pause inside its own delegated call) reaches the caller (ADR 0110, which
|
|
48
|
+
* lifts ADR 0028's refusal). The child run ends paused; the call resolves to
|
|
49
|
+
* a SubagentPause naming the ids it waits on, and the caller stores no
|
|
50
|
+
* response to it: the call stays open, and the caller's run ends paused too
|
|
51
|
+
* (agentLoop.ts). The turn finds the pause by walking down the open calls
|
|
52
|
+
* (interrupts.ts delegatedPauses) and reports it with the agent path. The
|
|
53
|
+
* answer comes back as the caller's next user message; the caller resumes
|
|
54
|
+
* the open call (resumeSubagent), which stores the same answer parts as
|
|
55
|
+
* the child's next message, under a fresh invocation id, and runs the
|
|
56
|
+
* child's loop on: it reads the answer from its own session as a top-level
|
|
57
|
+
* agent reads it (an approval runs or refuses its pinned call, a question's
|
|
58
|
+
* answer is the call's response), finishes, and its answer is the open
|
|
59
|
+
* call's response. A child that pauses again leaves the call open again.
|
|
41
60
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
61
|
+
* CALLS TO SUBAGENTS IN ONE STEP RUN CONCURRENTLY (ADR 0116), under the
|
|
62
|
+
* caller's `max_concurrency` (the syndicate's root key, default
|
|
63
|
+
* DEFAULT_MAX_CONCURRENCY): agentLoop.ts runs each delegated call through
|
|
64
|
+
* the step's DelegationGate, which starts them in call order as slots free
|
|
65
|
+
* up, and runs two calls to the same subagent (one child session) one after
|
|
66
|
+
* the other. The step's responses are stored once every call has answered,
|
|
67
|
+
* in call order, whatever order the children finished in; a child that
|
|
68
|
+
* pauses leaves its call open while the others finish and are stored. ADK
|
|
69
|
+
* ran them one after another; `max_concurrency: 1` keeps that order.
|
|
44
70
|
*
|
|
45
71
|
* NESTED SYNDICATES: a `yaml_reference` subagent is the nested syndicate's
|
|
46
72
|
* orchestrator, named as the entry, listing its own subagent tools. It runs
|
|
@@ -49,6 +75,13 @@
|
|
|
49
75
|
*
|
|
50
76
|
* A NESTED WORKFLOW (ADR 0098): a `yaml_reference` to a workflow syndicate
|
|
51
77
|
* is a workflowSubagentTool, holding the whole graph as a WorkflowSubagent.
|
|
78
|
+
* A walk that ends paused (an ask_user node's question, an agent node's
|
|
79
|
+
* approval request) leaves the call open as a child loop's pause does
|
|
80
|
+
* (ADR 0111): the call resolves to a SubagentPause naming the walk's open
|
|
81
|
+
* interrupts, and resumeWorkflowSubagent walks the graph again on the
|
|
82
|
+
* answer, which the walk's own resume (lib/workflow/resume.ts) reads as it
|
|
83
|
+
* reads a top-level workflow's. An `ask_user` answer reaches the walk as
|
|
84
|
+
* ADK's explicit reply to its `adk_request_input` call.
|
|
52
85
|
* A call runs as steps 1 to 6 say, with the walk in place of the loop: the
|
|
53
86
|
* graph's walk (lib/workflow/turn.ts, runNativeWorkflow, which
|
|
54
87
|
* lib/compileNative.ts hands over) stores the message itself and yields the
|
|
@@ -67,8 +100,8 @@
|
|
|
67
100
|
*/
|
|
68
101
|
import type { ModelAdapter } from '../../models/contract.ts';
|
|
69
102
|
import type { Tool, ToolContext } from '../../tools/tool.ts';
|
|
70
|
-
import type { TurnEvent } from '../events.ts';
|
|
71
|
-
import type { SessionService } from '../sessions.ts';
|
|
103
|
+
import type { TurnEvent, TurnPart } from '../events.ts';
|
|
104
|
+
import type { Session, SessionService } from '../sessions.ts';
|
|
72
105
|
import type { AgentLoopContext, AgentLoopEnd } from './agentLoop.ts';
|
|
73
106
|
import type { NativeAgent } from './request.ts';
|
|
74
107
|
import { SelfCorrection } from './selfCorrection.ts';
|
|
@@ -101,12 +134,20 @@ export interface WorkflowSubagentRun {
|
|
|
101
134
|
selfCorrection: SelfCorrection;
|
|
102
135
|
credentials?: AgentLoopContext['credentials'];
|
|
103
136
|
}
|
|
137
|
+
/** How a nested workflow's walk ended, as far as a delegated call reads it (lib/workflow/turn.ts NativeWorkflowEnd). */
|
|
138
|
+
export interface WorkflowSubagentEnd {
|
|
139
|
+
/** The walk's result; `interruptIds` holds what it paused on. */
|
|
140
|
+
run?: {
|
|
141
|
+
interruptIds: readonly string[];
|
|
142
|
+
};
|
|
143
|
+
stopped?: boolean;
|
|
144
|
+
}
|
|
104
145
|
/** A workflow syndicate delegated to as a subagent (ADR 0098): its name and description, and the walk one call runs. */
|
|
105
146
|
export interface WorkflowSubagent {
|
|
106
147
|
name: string;
|
|
107
148
|
description?: string;
|
|
108
|
-
/** Stores the message on the child session and walks the whole graph, yielding each event ADK's Runner yields. Throws what a node gave up with. */
|
|
109
|
-
walk(run: WorkflowSubagentRun): AsyncGenerator<TurnEvent,
|
|
149
|
+
/** Stores the message on the child session and walks the whole graph (a resume when it holds a paused walk), yielding each event ADK's Runner yields. Throws what a node gave up with. */
|
|
150
|
+
walk(run: WorkflowSubagentRun): AsyncGenerator<TurnEvent, WorkflowSubagentEnd | undefined>;
|
|
110
151
|
}
|
|
111
152
|
/** A nested workflow listed on its caller as a tool: the declaration ADK's AgentTool gave a Workflow, and the graph the call walks. */
|
|
112
153
|
export interface WorkflowSubagentTool extends Tool {
|
|
@@ -121,19 +162,83 @@ export declare function workflowSubagentOf(tool: unknown): WorkflowSubagent | un
|
|
|
121
162
|
* carrying SUBAGENT) holds, else undefined.
|
|
122
163
|
*/
|
|
123
164
|
export declare function subagentOf(tool: unknown): NativeAgent | undefined;
|
|
165
|
+
/**
|
|
166
|
+
* A delegated call whose child run ended paused (ADR 0110): the ids the
|
|
167
|
+
* child waits on. The caller stores no response to the call and ends its
|
|
168
|
+
* own run paused.
|
|
169
|
+
*/
|
|
170
|
+
export declare class SubagentPause {
|
|
171
|
+
readonly pending: readonly string[];
|
|
172
|
+
constructor(pending: readonly string[]);
|
|
173
|
+
}
|
|
124
174
|
/** What a delegated call needs from its caller's step. */
|
|
125
175
|
export interface DelegationScope {
|
|
126
176
|
/** The caller's loop context: its session, store, adapters, memory, signal. */
|
|
127
177
|
ctx: AgentLoopContext;
|
|
178
|
+
/** The caller's name: a segment of the path a top-level caller's subagents are filed under (childAppName). */
|
|
179
|
+
caller: string;
|
|
128
180
|
/** The caller's session state as the step left it. */
|
|
129
181
|
stateBase: Readonly<Record<string, unknown>>;
|
|
130
182
|
/** The turn's signal, as the step sent it. */
|
|
131
183
|
signal?: AbortSignal;
|
|
132
184
|
/** The loop the child runs on (agentLoop.ts passes its own runAgentLoop). */
|
|
133
185
|
runLoop: (agent: NativeAgent, ctx: AgentLoopContext) => AsyncGenerator<TurnEvent, AgentLoopEnd>;
|
|
134
|
-
|
|
135
|
-
|
|
186
|
+
}
|
|
187
|
+
/** Delegated calls one step runs at once when the syndicate sets no `max_concurrency` (ADR 0116). */
|
|
188
|
+
export declare const DEFAULT_MAX_CONCURRENCY = 4;
|
|
189
|
+
/** One delegated call's place in its step's DelegationGate. */
|
|
190
|
+
export interface DelegationTicket {
|
|
191
|
+
/** Resolves once the call may start: a slot is free, and every earlier call under its key has released. */
|
|
192
|
+
readonly ready: Promise<void>;
|
|
193
|
+
/** Frees the slot and the key's lane. Idempotent; call it whether the call ran, failed or never started. */
|
|
194
|
+
release(): void;
|
|
136
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* The delegated calls of one step (ADR 0116): at most `limit` run at once,
|
|
198
|
+
* started in the order they entered (agentLoop.ts enters them in call
|
|
199
|
+
* order, synchronously, before any of them runs), and calls sharing a key
|
|
200
|
+
* (the subagent's name, so one child session) run one after another in
|
|
201
|
+
* that order. A slot is held from the call's start to its release, which
|
|
202
|
+
* comes as soon as the child's run ends: what the step does after (self-
|
|
203
|
+
* correction's bookkeeping, which waits on earlier calls) holds no slot. A
|
|
204
|
+
* call waiting on an earlier same-key call holds its slot meanwhile: the
|
|
205
|
+
* earlier call already holds one, so the wait always ends.
|
|
206
|
+
*/
|
|
207
|
+
export declare class DelegationGate {
|
|
208
|
+
readonly limit: number;
|
|
209
|
+
private active;
|
|
210
|
+
private readonly waiting;
|
|
211
|
+
private readonly lanes;
|
|
212
|
+
constructor(limit?: number);
|
|
213
|
+
/** Enters one call under `key`, behind every call entered before it. */
|
|
214
|
+
enter(key: string): DelegationTicket;
|
|
215
|
+
/** Runs `run` under a ticket for `key`: once ready, the ticket released when it settles. */
|
|
216
|
+
run<T>(key: string, run: () => Promise<T>): Promise<T>;
|
|
217
|
+
private pump;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The app name a delegated call's child session is filed under (ADR 0111):
|
|
221
|
+
* the agent path. Below a top-level agent, its app name, its name and the
|
|
222
|
+
* subagent's (`<app>/<caller>/<subagent>`); below a delegated subagent,
|
|
223
|
+
* whose own session is already filed under its path, that app name and the
|
|
224
|
+
* subagent's. Agent names are identifiers (lib/syndicateSchema.ts), so
|
|
225
|
+
* never hold a `/`.
|
|
226
|
+
*/
|
|
227
|
+
export declare function childAppName(parent: {
|
|
228
|
+
appName: string;
|
|
229
|
+
delegated?: boolean;
|
|
230
|
+
}, caller: string, name: string): string;
|
|
231
|
+
/**
|
|
232
|
+
* The session a delegated call ran in before ADR 0111, filed under the
|
|
233
|
+
* subagent's name alone, when it is the one to continue (`continues`): the
|
|
234
|
+
* caller of a stored conversation called the subagent there before, or a
|
|
235
|
+
* pause was found waiting in it. Read only when no session exists under the
|
|
236
|
+
* agent path.
|
|
237
|
+
*/
|
|
238
|
+
export declare function legacyChild(sessions: Pick<SessionService, 'get'>, key: {
|
|
239
|
+
userId: string;
|
|
240
|
+
sessionId: string;
|
|
241
|
+
}, name: string, continues: boolean): Promise<Session | undefined>;
|
|
137
242
|
/**
|
|
138
243
|
* Runs one delegated call: `agent` on its own session with the call's
|
|
139
244
|
* `request` as its message, the child's state writes recorded in `context`.
|
|
@@ -141,6 +246,14 @@ export interface DelegationScope {
|
|
|
141
246
|
* throws, and for an output-schema answer that does not parse.
|
|
142
247
|
*/
|
|
143
248
|
export declare function runSubagent(agent: NativeAgent, args: Record<string, unknown>, context: ToolContext, scope: DelegationScope): Promise<unknown>;
|
|
249
|
+
/**
|
|
250
|
+
* Resumes a delegated call the child left paused (ADR 0110): `answer` (the
|
|
251
|
+
* caller's user message parts that answer what waits below the call) is
|
|
252
|
+
* stored as the child's next message and the child's loop runs on, as
|
|
253
|
+
* runSubagent runs it. Resolves as runSubagent does, a SubagentPause again
|
|
254
|
+
* when the child pauses again.
|
|
255
|
+
*/
|
|
256
|
+
export declare function resumeSubagent(agent: NativeAgent, answer: TurnPart[], context: ToolContext, scope: DelegationScope): Promise<unknown>;
|
|
144
257
|
/**
|
|
145
258
|
* Runs one delegated call to a nested workflow (ADR 0098): the whole graph
|
|
146
259
|
* walked on the child session with the call's `request` as its message,
|
|
@@ -148,3 +261,13 @@ export declare function runSubagent(agent: NativeAgent, args: Record<string, unk
|
|
|
148
261
|
* yielded event's text, '' for none. Throws what the walk throws.
|
|
149
262
|
*/
|
|
150
263
|
export declare function runWorkflowSubagent(workflow: WorkflowSubagent, args: Record<string, unknown>, context: ToolContext, scope: DelegationScope): Promise<unknown>;
|
|
264
|
+
/**
|
|
265
|
+
* Resumes a delegated call whose nested workflow ended paused (ADR 0111):
|
|
266
|
+
* the graph is walked again on `answer` (the caller's user message parts
|
|
267
|
+
* that answer the walk's open interrupt), which the walk's resume reads as
|
|
268
|
+
* it reads a top-level workflow's. An approval decision goes as it came; an
|
|
269
|
+
* `ask_user` answer goes as ADK's explicit reply to the `adk_request_input`
|
|
270
|
+
* call it answers, under that call's name. Resolves as runWorkflowSubagent
|
|
271
|
+
* does, a SubagentPause again when the walk pauses again.
|
|
272
|
+
*/
|
|
273
|
+
export declare function resumeWorkflowSubagent(workflow: WorkflowSubagent, answer: TurnPart[], context: ToolContext, scope: DelegationScope): Promise<unknown>;
|