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.
Files changed (122) hide show
  1. package/.env.example +19 -2
  2. package/CHANGELOG.md +333 -0
  3. package/DOCUMENTATION.md +99 -39
  4. package/README.md +1 -0
  5. package/config/agents/README.md +2 -2
  6. package/config/agents/examples/ares.yaml +6 -10
  7. package/config/agents/examples/assistant.yaml +6 -6
  8. package/config/agents/examples/augustin.yaml +9 -15
  9. package/config/agents/examples/cartographers.yaml +6 -6
  10. package/config/agents/examples/council.yaml +6 -6
  11. package/config/agents/examples/critic.yaml +16 -16
  12. package/config/agents/examples/harness.yaml +4 -4
  13. package/config/agents/examples/image_production.yaml +33 -32
  14. package/config/agents/examples/librarian.yaml +4 -4
  15. package/config/agents/examples/patient_advocate.yaml +6 -10
  16. package/config/agents/examples/pipeline.yaml +20 -19
  17. package/config/agents/examples/research.yaml +36 -43
  18. package/config/agents/examples/scribe.yaml +31 -32
  19. package/config/agents/examples/scriptorium.yaml +6 -6
  20. package/config/agents/examples/structured_critic.yaml +82 -0
  21. package/config/agents/examples/style_council.yaml +9 -11
  22. package/config/agents/examples/syndicate.yaml +3 -5
  23. package/config/agents/examples/tutor.yaml +2 -2
  24. package/config/agents/examples/weather.yaml +2 -2
  25. package/config/agents/syndicate.schema.json +1979 -0
  26. package/config/agents/syndicateSchema.yaml +109 -72
  27. package/config/agents/templates/README.md +1 -1
  28. package/config/agents/templates/account_memory.yaml +4 -4
  29. package/config/agents/templates/case_desk.yaml +6 -6
  30. package/config/agents/templates/conversational.yaml +2 -2
  31. package/config/agents/templates/draft_review.yaml +6 -6
  32. package/config/agents/templates/intake_extractor.yaml +61 -60
  33. package/config/agents/templates/research_brief.yaml +6 -6
  34. package/config/agents/templates/research_desk.yaml +6 -6
  35. package/config/agents/templates/review_panel.yaml +8 -8
  36. package/config/agents/templates/support_triage.yaml +30 -29
  37. package/config/agents/templates/systems_operator.yaml +21 -5
  38. package/db/migrations/0014_durable_runs.sql +31 -0
  39. package/dist/lib/a2a/app.d.ts +15 -0
  40. package/dist/lib/a2a/app.js +53 -5
  41. package/dist/lib/a2a/executor.d.ts +7 -1
  42. package/dist/lib/a2a/executor.js +34 -4
  43. package/dist/lib/a2a/oauthSetup.d.ts +90 -0
  44. package/dist/lib/a2a/oauthSetup.js +205 -0
  45. package/dist/lib/agentDialect.d.ts +66 -0
  46. package/dist/lib/agentDialect.js +134 -0
  47. package/dist/lib/compile.d.ts +10 -5
  48. package/dist/lib/compile.js +69 -37
  49. package/dist/lib/compileNative.js +2 -0
  50. package/dist/lib/dispatch.d.ts +10 -5
  51. package/dist/lib/dispatch.js +10 -5
  52. package/dist/lib/doctor.d.ts +34 -1
  53. package/dist/lib/doctor.js +47 -3
  54. package/dist/lib/loadSyndicate.d.ts +45 -2
  55. package/dist/lib/loadSyndicate.js +22 -2
  56. package/dist/lib/memory/namespace.d.ts +6 -5
  57. package/dist/lib/memory/namespace.js +6 -5
  58. package/dist/lib/memory/providers.js +3 -4
  59. package/dist/lib/models/capabilities.d.ts +16 -1
  60. package/dist/lib/models/capabilities.js +55 -1
  61. package/dist/lib/observability/tracer.d.ts +8 -1
  62. package/dist/lib/observability/tracer.js +33 -5
  63. package/dist/lib/runtime/approvals.d.ts +6 -0
  64. package/dist/lib/runtime/native/agentLoop.d.ts +18 -1
  65. package/dist/lib/runtime/native/agentLoop.js +105 -11
  66. package/dist/lib/runtime/native/checkpoint.d.ts +164 -0
  67. package/dist/lib/runtime/native/checkpoint.js +365 -0
  68. package/dist/lib/runtime/native/delegate.d.ts +141 -18
  69. package/dist/lib/runtime/native/delegate.js +235 -39
  70. package/dist/lib/runtime/native/interrupts.d.ts +101 -2
  71. package/dist/lib/runtime/native/interrupts.js +201 -1
  72. package/dist/lib/runtime/native/request.d.ts +13 -3
  73. package/dist/lib/runtime/native/request.js +28 -4
  74. package/dist/lib/runtime/native/telemetry.js +10 -15
  75. package/dist/lib/runtime/questions.d.ts +7 -4
  76. package/dist/lib/runtime/sessions.d.ts +1 -1
  77. package/dist/lib/runtime/syndicateTurn.d.ts +7 -1
  78. package/dist/lib/runtime/syndicateTurn.js +74 -22
  79. package/dist/lib/runtime/turnControl.d.ts +15 -0
  80. package/dist/lib/runtime/turnControl.js +10 -0
  81. package/dist/lib/storage/postgres/index.d.ts +6 -1
  82. package/dist/lib/storage/postgres/index.js +7 -2
  83. package/dist/lib/storage/postgres/taskQueue.d.ts +14 -2
  84. package/dist/lib/storage/postgres/taskQueue.js +20 -3
  85. package/dist/lib/storage/postgres/taskStore.d.ts +26 -2
  86. package/dist/lib/storage/postgres/taskStore.js +74 -8
  87. package/dist/lib/syndicateSchema.d.ts +278 -0
  88. package/dist/lib/syndicateSchema.js +182 -15
  89. package/dist/lib/tools/auth.d.ts +7 -5
  90. package/dist/lib/tools/auth.js +12 -4
  91. package/dist/lib/tools/credentialEnv.d.ts +28 -0
  92. package/dist/lib/tools/credentialEnv.js +52 -0
  93. package/dist/lib/tools/credentialStore.js +9 -2
  94. package/dist/lib/tools/mcpToolFactory.d.ts +25 -2
  95. package/dist/lib/tools/mcpToolFactory.js +179 -21
  96. package/dist/lib/tools/oauthHosts.d.ts +71 -0
  97. package/dist/lib/tools/oauthHosts.js +154 -0
  98. package/dist/lib/tools/oauthTools.d.ts +233 -0
  99. package/dist/lib/tools/oauthTools.js +421 -0
  100. package/dist/lib/tools/openapiTools.d.ts +4 -6
  101. package/dist/lib/tools/openapiTools.js +40 -36
  102. package/dist/lib/tools/taskTools.d.ts +30 -5
  103. package/dist/lib/tools/taskTools.js +74 -10
  104. package/dist/lib/workflow.d.ts +8 -4
  105. package/dist/lib/workflow.js +8 -4
  106. package/dist/lib/workflowConfig.d.ts +6 -0
  107. package/dist/scripts/a2a_server.d.ts +36 -0
  108. package/dist/scripts/a2a_server.js +122 -1
  109. package/dist/scripts/assistant_worker.d.ts +10 -0
  110. package/dist/scripts/assistant_worker.js +69 -19
  111. package/dist/scripts/doctor.d.ts +4 -1
  112. package/dist/scripts/doctor.js +5 -2
  113. package/dist/scripts/yaml_codemod.d.ts +52 -0
  114. package/dist/scripts/yaml_codemod.js +570 -0
  115. package/package.json +11 -7
  116. package/skills/briefs/melchizedek-author.md +7 -5
  117. package/skills/briefs/melchizedek-models.md +4 -3
  118. package/skills/briefs/melchizedek.md +1 -0
  119. package/skills/melchizedek/SKILL.md +1 -0
  120. package/skills/melchizedek-author/SKILL.md +7 -4
  121. package/skills/melchizedek-author/assets/minimal.yaml +13 -13
  122. 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: the one under the subagent's own name as the
17
- * app name, with the caller's user id and session id. It is created on
18
- * the first call, with the caller's state as it stands (the session's,
19
- * then this call's writes; `temp:` keys dropped), and kept: a later
20
- * call, in this turn or another, continues it. This is not a branch of
21
- * the caller's session; ADK's AgentTool runs a Runner of its own.
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) cannot
38
- * reach the caller, exactly as under ADK (ADR 0028): the child run ends
39
- * paused, its last event carries no text, the call answers '', and the gated
40
- * tool never runs. WS6-2a lifts this.
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
- * Calls to subagents in one step run one after another, in call order, as
43
- * ADK runs them; running them concurrently is WS6.
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, unknown>;
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
- /** Calls sharing this key run one after another (agentLoop.ts passes the step's scope). */
135
- queue: object;
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>;