melchizedek-agents 1.0.3 → 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.
Files changed (172) hide show
  1. package/.env.example +29 -2
  2. package/AGENT_SETUP.md +3 -1
  3. package/CHANGELOG.md +524 -0
  4. package/DOCUMENTATION.md +178 -43
  5. package/ONBOARDING.md +339 -0
  6. package/QUICKSTART.md +15 -2
  7. package/README.md +6 -2
  8. package/config/agents/README.md +2 -2
  9. package/config/agents/examples/ares.yaml +6 -10
  10. package/config/agents/examples/assistant.yaml +6 -6
  11. package/config/agents/examples/augustin.yaml +9 -15
  12. package/config/agents/examples/cartographers.yaml +6 -6
  13. package/config/agents/examples/council.yaml +7 -7
  14. package/config/agents/examples/critic.yaml +24 -24
  15. package/config/agents/examples/harness.yaml +4 -4
  16. package/config/agents/examples/image_production.yaml +37 -35
  17. package/config/agents/examples/librarian.yaml +4 -4
  18. package/config/agents/examples/patient_advocate.yaml +6 -10
  19. package/config/agents/examples/pipeline.yaml +20 -19
  20. package/config/agents/examples/research.yaml +36 -43
  21. package/config/agents/examples/scribe.yaml +31 -32
  22. package/config/agents/examples/scriptorium.yaml +6 -6
  23. package/config/agents/examples/structured_critic.yaml +82 -0
  24. package/config/agents/examples/style_council.yaml +9 -11
  25. package/config/agents/examples/syndicate.yaml +3 -5
  26. package/config/agents/examples/tutor.yaml +2 -2
  27. package/config/agents/examples/weather.yaml +2 -2
  28. package/config/agents/syndicate.schema.json +859 -13
  29. package/config/agents/syndicateSchema.yaml +122 -73
  30. package/config/agents/templates/README.md +1 -1
  31. package/config/agents/templates/account_memory.yaml +4 -4
  32. package/config/agents/templates/case_desk.yaml +6 -6
  33. package/config/agents/templates/conversational.yaml +2 -2
  34. package/config/agents/templates/draft_review.yaml +6 -6
  35. package/config/agents/templates/intake_extractor.yaml +64 -62
  36. package/config/agents/templates/research_brief.yaml +6 -6
  37. package/config/agents/templates/research_desk.yaml +6 -6
  38. package/config/agents/templates/review_panel.yaml +8 -8
  39. package/config/agents/templates/support_triage.yaml +30 -29
  40. package/config/agents/templates/systems_operator.yaml +24 -5
  41. package/db/migrations/0014_durable_runs.sql +31 -0
  42. package/dist/lib/a2a/app.d.ts +27 -0
  43. package/dist/lib/a2a/app.js +64 -5
  44. package/dist/lib/a2a/executor.d.ts +7 -1
  45. package/dist/lib/a2a/executor.js +43 -5
  46. package/dist/lib/a2a/oauthSetup.d.ts +106 -0
  47. package/dist/lib/a2a/oauthSetup.js +259 -0
  48. package/dist/lib/agentDialect.d.ts +76 -0
  49. package/dist/lib/agentDialect.js +151 -0
  50. package/dist/lib/compile.d.ts +42 -6
  51. package/dist/lib/compile.js +97 -45
  52. package/dist/lib/compileNative.d.ts +11 -3
  53. package/dist/lib/compileNative.js +20 -1
  54. package/dist/lib/dispatch.d.ts +10 -5
  55. package/dist/lib/dispatch.js +10 -5
  56. package/dist/lib/doctor.d.ts +110 -1
  57. package/dist/lib/doctor.js +205 -9
  58. package/dist/lib/loadSyndicate.d.ts +54 -2
  59. package/dist/lib/loadSyndicate.js +22 -2
  60. package/dist/lib/memory/namespace.d.ts +6 -5
  61. package/dist/lib/memory/namespace.js +6 -5
  62. package/dist/lib/memory/providers.js +3 -4
  63. package/dist/lib/models/capabilities.d.ts +25 -1
  64. package/dist/lib/models/capabilities.js +79 -1
  65. package/dist/lib/models/chatCompletionsAdapter.d.ts +11 -3
  66. package/dist/lib/models/chatCompletionsAdapter.js +19 -5
  67. package/dist/lib/models/claudeAdapter.js +4 -0
  68. package/dist/lib/models/contract.d.ts +5 -2
  69. package/dist/lib/models/gateway.d.ts +2 -1
  70. package/dist/lib/models/gateway.js +4 -2
  71. package/dist/lib/models/gatewayAdapter.d.ts +7 -1
  72. package/dist/lib/models/gatewayAdapter.js +8 -0
  73. package/dist/lib/models/geminiAdapter.js +8 -2
  74. package/dist/lib/models/genaiMapping.d.ts +8 -4
  75. package/dist/lib/models/genaiMapping.js +23 -9
  76. package/dist/lib/models/gptAdapter.d.ts +4 -2
  77. package/dist/lib/models/gptAdapter.js +11 -4
  78. package/dist/lib/models/grokAdapter.d.ts +3 -1
  79. package/dist/lib/models/grokAdapter.js +11 -3
  80. package/dist/lib/models/kimiAdapter.d.ts +2 -1
  81. package/dist/lib/models/kimiAdapter.js +3 -3
  82. package/dist/lib/models/reasoning.d.ts +30 -2
  83. package/dist/lib/models/reasoning.js +56 -5
  84. package/dist/lib/observability/tracer.d.ts +8 -1
  85. package/dist/lib/observability/tracer.js +33 -5
  86. package/dist/lib/onboarding.d.ts +134 -0
  87. package/dist/lib/onboarding.js +539 -0
  88. package/dist/lib/runtime/approvals.d.ts +8 -0
  89. package/dist/lib/runtime/approvals.js +4 -3
  90. package/dist/lib/runtime/credentials.d.ts +6 -0
  91. package/dist/lib/runtime/native/agentLoop.d.ts +23 -3
  92. package/dist/lib/runtime/native/agentLoop.js +107 -11
  93. package/dist/lib/runtime/native/checkpoint.d.ts +171 -0
  94. package/dist/lib/runtime/native/checkpoint.js +359 -0
  95. package/dist/lib/runtime/native/delegate.d.ts +222 -18
  96. package/dist/lib/runtime/native/delegate.js +390 -39
  97. package/dist/lib/runtime/native/interrupts.d.ts +184 -2
  98. package/dist/lib/runtime/native/interrupts.js +391 -1
  99. package/dist/lib/runtime/native/request.d.ts +13 -3
  100. package/dist/lib/runtime/native/request.js +28 -4
  101. package/dist/lib/runtime/native/telemetry.js +10 -15
  102. package/dist/lib/runtime/questions.d.ts +7 -4
  103. package/dist/lib/runtime/sessions.d.ts +1 -1
  104. package/dist/lib/runtime/syndicateTurn.d.ts +11 -3
  105. package/dist/lib/runtime/syndicateTurn.js +353 -68
  106. package/dist/lib/runtime/turnControl.d.ts +56 -0
  107. package/dist/lib/runtime/turnControl.js +10 -0
  108. package/dist/lib/session/transcript.d.ts +9 -0
  109. package/dist/lib/session/transcript.js +20 -1
  110. package/dist/lib/skills.d.ts +18 -0
  111. package/dist/lib/skills.js +73 -1
  112. package/dist/lib/storage/postgres/index.d.ts +12 -2
  113. package/dist/lib/storage/postgres/index.js +8 -3
  114. package/dist/lib/storage/postgres/taskQueue.d.ts +20 -4
  115. package/dist/lib/storage/postgres/taskQueue.js +29 -5
  116. package/dist/lib/storage/postgres/taskStore.d.ts +26 -2
  117. package/dist/lib/storage/postgres/taskStore.js +74 -8
  118. package/dist/lib/syndicateSchema.d.ts +316 -1
  119. package/dist/lib/syndicateSchema.js +203 -29
  120. package/dist/lib/tools/auth.d.ts +20 -6
  121. package/dist/lib/tools/auth.js +21 -5
  122. package/dist/lib/tools/credentialEnv.d.ts +28 -0
  123. package/dist/lib/tools/credentialEnv.js +52 -0
  124. package/dist/lib/tools/credentialHosts.d.ts +88 -0
  125. package/dist/lib/tools/credentialHosts.js +165 -0
  126. package/dist/lib/tools/credentialStore.js +9 -2
  127. package/dist/lib/tools/credentialUses.d.ts +92 -0
  128. package/dist/lib/tools/credentialUses.js +112 -0
  129. package/dist/lib/tools/mcpToolFactory.d.ts +25 -2
  130. package/dist/lib/tools/mcpToolFactory.js +179 -21
  131. package/dist/lib/tools/oauthConsent.js +8 -1
  132. package/dist/lib/tools/oauthHosts.d.ts +73 -0
  133. package/dist/lib/tools/oauthHosts.js +154 -0
  134. package/dist/lib/tools/oauthTools.d.ts +242 -0
  135. package/dist/lib/tools/oauthTools.js +447 -0
  136. package/dist/lib/tools/openapiTools.d.ts +11 -6
  137. package/dist/lib/tools/openapiTools.js +88 -38
  138. package/dist/lib/tools/taskTools.d.ts +61 -6
  139. package/dist/lib/tools/taskTools.js +145 -21
  140. package/dist/lib/workflow/turn.d.ts +22 -3
  141. package/dist/lib/workflow/turn.js +151 -31
  142. package/dist/lib/workflow.d.ts +14 -7
  143. package/dist/lib/workflow.js +14 -7
  144. package/dist/lib/workflowConfig.d.ts +6 -0
  145. package/dist/scripts/a2a_server.d.ts +36 -0
  146. package/dist/scripts/a2a_server.js +131 -1
  147. package/dist/scripts/assistant_worker.d.ts +12 -0
  148. package/dist/scripts/assistant_worker.js +81 -22
  149. package/dist/scripts/doctor.d.ts +7 -2
  150. package/dist/scripts/doctor.js +9 -4
  151. package/dist/scripts/setup.d.ts +23 -0
  152. package/dist/scripts/setup.js +154 -0
  153. package/dist/scripts/skills_install.d.ts +3 -1
  154. package/dist/scripts/skills_install.js +17 -2
  155. package/dist/scripts/yaml_codemod.d.ts +62 -0
  156. package/dist/scripts/yaml_codemod.js +652 -0
  157. package/package.json +8 -2
  158. package/skills/README.md +11 -2
  159. package/skills/briefs/_shared.md +1 -1
  160. package/skills/briefs/melchizedek-author.md +7 -5
  161. package/skills/briefs/melchizedek-models.md +7 -4
  162. package/skills/briefs/melchizedek.md +1 -0
  163. package/skills/briefs/skills-readme.md +3 -1
  164. package/skills/melchizedek/SKILL.md +1 -0
  165. package/skills/melchizedek-author/SKILL.md +7 -4
  166. package/skills/melchizedek-author/assets/minimal.yaml +13 -13
  167. package/skills/melchizedek-models/SKILL.md +11 -4
  168. package/skills/melchizedek-onboard/SKILL.md +49 -0
  169. package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
  170. package/skills/melchizedek-onboard-keys/SKILL.md +35 -0
  171. package/skills/melchizedek-onboard-local/SKILL.md +41 -0
  172. package/skills/melchizedek-onboard-serve/SKILL.md +43 -0
@@ -0,0 +1,359 @@
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. The held opening session
54
+ * is empty at that open, so the copy is too; the route's append of the
55
+ * message is answered with the stored events, and the projection takes
56
+ * every event the real session gained, unprojected (the current turn),
57
+ * so the route resumes like a single agent. A workflow route writes its
58
+ * own child session and resumes. Checkpoints saved before this, which
59
+ * 1.1.0 set aside, restore the same way.
60
+ * - A task backend stores a checkpoint only up to its cap
61
+ * (TaskBackendOptions.checkpointMaxBytes, lib/tools/taskTools.ts,
62
+ * default 5 MiB of JSON); a save above it keeps the previous checkpoint
63
+ * and still answers whether the claim holds, so a huge run resumes from
64
+ * an older boundary.
65
+ * - Each resumed attempt has its own step budget (max_steps, the loop's
66
+ * call ceiling); attempts are bounded by the queue's MAX_ATTEMPTS.
67
+ * - `temp:` state lasts one invocation and is never stored, so it does not
68
+ * survive a resume.
69
+ * - A workflow walk resumes through its own rules (ADR 0094); its
70
+ * checkpoint is the sessions it walks.
71
+ * - A checkpoint holds the run's sessions, so it carries the user's text,
72
+ * model output and tool results: it lives only where the run's record
73
+ * lives, under the same owner, and is deleted with it. This module never
74
+ * clears one; the caller does when the run finishes.
75
+ */
76
+ import { createHash, randomUUID } from 'node:crypto';
77
+ import { getFunctionCalls, getFunctionResponses, parseTurnEvents } from '../events.js';
78
+ import { InProcessSessionService } from '../sessions.js';
79
+ import { runSyndicateTurn } from '../syndicateTurn.js';
80
+ // ── Shapes ───────────────────────────────────────────────────────────────────
81
+ export const CHECKPOINT_VERSION = 1;
82
+ // ── Helpers ──────────────────────────────────────────────────────────────────
83
+ /** The parts as runSyndicateTurn stores them: a bare string becomes `{ text }`. */
84
+ function normalizedParts(parts) {
85
+ return parts.map((p) => (typeof p === 'string' ? { text: p } : p));
86
+ }
87
+ /** sha256 hex of the message's parts, as a checkpoint names its run's message. */
88
+ export function openingHashOf(parts) {
89
+ return createHash('sha256').update(JSON.stringify(normalizedParts(parts))).digest('hex');
90
+ }
91
+ const keyOf = (appName, userId, id) => JSON.stringify([appName, userId, id]);
92
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
93
+ /**
94
+ * The checkpoint, checked, when it belongs to this run and this message;
95
+ * null otherwise. A stored checkpoint is data read back from a table or a
96
+ * file, so every field the restore reads is checked before it is trusted.
97
+ */
98
+ export function usableCheckpoint(cp, runId, parts) {
99
+ if (!isRecord(cp) || cp.version !== CHECKPOINT_VERSION)
100
+ return null;
101
+ if (cp.runId !== runId || cp.openingHash !== openingHashOf(parts))
102
+ return null;
103
+ if (typeof cp.sessionId !== 'string' || !cp.sessionId)
104
+ return null;
105
+ if (typeof cp.steps !== 'number' || !Number.isInteger(cp.steps) || cp.steps < 0)
106
+ return null;
107
+ if (!Array.isArray(cp.sessions))
108
+ return null;
109
+ try {
110
+ for (const [i, s] of cp.sessions.entries()) {
111
+ if (!isRecord(s))
112
+ return null;
113
+ if (typeof s.appName !== 'string' || typeof s.userId !== 'string' || typeof s.id !== 'string')
114
+ return null;
115
+ if (!isRecord(s.createdState) || typeof s.opening !== 'boolean')
116
+ return null;
117
+ parseTurnEvents(s.events, `sessions[${i}].events`);
118
+ if (s.opening && s.events.length === 0)
119
+ return null;
120
+ }
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ return cp;
126
+ }
127
+ /** Content compared as stored JSON. */
128
+ const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
129
+ class CheckpointingStore {
130
+ inner;
131
+ opts;
132
+ openingParts;
133
+ tracked = new Map();
134
+ /** Call ids with no response yet, across every session. */
135
+ openCalls = new Set();
136
+ seq = 0;
137
+ /** The order of the first event an agent wrote; -1 when a restored run already had one. */
138
+ firstAgentSeq;
139
+ savesLost = false;
140
+ saving = Promise.resolve();
141
+ stepCount = 0;
142
+ /** The eager restore; every method waits on it first. */
143
+ ready = Promise.resolve();
144
+ runSessionId;
145
+ restored;
146
+ constructor(opts) {
147
+ this.opts = opts;
148
+ this.inner = opts.inner ?? new InProcessSessionService();
149
+ this.openingParts = JSON.stringify(normalizedParts(opts.parts));
150
+ this.runSessionId = opts.sessionId;
151
+ const from = opts.from ? usableCheckpoint(opts.from, opts.runId, opts.parts) : null;
152
+ this.restored = !!from && from.sessionId === opts.sessionId;
153
+ if (this.restored && from)
154
+ this.load(from);
155
+ }
156
+ get steps() {
157
+ return this.stepCount;
158
+ }
159
+ /** Records the checkpoint: subagent sessions restored now, opening sessions held for the runner. */
160
+ load(from) {
161
+ this.stepCount = from.steps;
162
+ const copy = structuredClone(from.sessions);
163
+ if (copy.some((s) => s.events.some((e) => e.author !== 'user')))
164
+ this.firstAgentSeq = -1;
165
+ const eager = [];
166
+ for (const s of copy) {
167
+ const t = { appName: s.appName, userId: s.userId, id: s.id, createdState: s.createdState, opening: s.opening };
168
+ if (s.opening)
169
+ t.held = s.events;
170
+ else
171
+ eager.push(s);
172
+ this.tracked.set(keyOf(s.appName, s.userId, s.id), t);
173
+ for (const e of s.events)
174
+ this.track(e);
175
+ }
176
+ this.ready = (async () => {
177
+ for (const s of eager) {
178
+ const session = await this.inner.create({ appName: s.appName, userId: s.userId, sessionId: s.id, state: s.createdState });
179
+ for (const e of s.events)
180
+ await this.inner.append(session, e);
181
+ }
182
+ })();
183
+ }
184
+ /** Keeps the set of unanswered calls up to date with one stored event. */
185
+ track(event) {
186
+ const waiting = new Set(event.longRunningToolIds ?? []);
187
+ for (const call of getFunctionCalls(event))
188
+ if (call.id && !waiting.has(call.id))
189
+ this.openCalls.add(call.id);
190
+ for (const response of getFunctionResponses(event))
191
+ if (response.id)
192
+ this.openCalls.delete(response.id);
193
+ }
194
+ async create(request) {
195
+ await this.ready;
196
+ const session = await this.inner.create(request);
197
+ const key = keyOf(session.appName, session.userId, session.id);
198
+ const known = this.tracked.get(key);
199
+ if (!known) {
200
+ this.tracked.set(key, { appName: session.appName, userId: session.userId, id: session.id, createdState: structuredClone(session.state) });
201
+ }
202
+ else if (known.held && session.events.length === 0) {
203
+ known.createdState = structuredClone(session.state);
204
+ }
205
+ return session;
206
+ }
207
+ async get(key, options) {
208
+ await this.ready;
209
+ return this.inner.get(key, options);
210
+ }
211
+ async list(request) {
212
+ await this.ready;
213
+ return this.inner.list(request);
214
+ }
215
+ async delete(key) {
216
+ await this.ready;
217
+ this.tracked.delete(keyOf(key.appName, key.userId, key.sessionId));
218
+ return this.inner.delete(key);
219
+ }
220
+ async append(session, event) {
221
+ await this.ready;
222
+ if (event.partial)
223
+ return this.inner.append(session, event);
224
+ const key = keyOf(session.appName, session.userId, session.id);
225
+ let t = this.tracked.get(key);
226
+ if (!t) {
227
+ // A session the store holds but this run did not create (not expected): tracked from here.
228
+ t = { appName: session.appName, userId: session.userId, id: session.id, createdState: structuredClone(session.state) };
229
+ this.tracked.set(key, t);
230
+ }
231
+ if (t.held) {
232
+ const held = t.held;
233
+ delete t.held;
234
+ const opening = held[0];
235
+ if (opening && event.author === 'user' && sameJson(event.content, opening.content)) {
236
+ // The runner stores the message again: the stored events answer it, onto the caller's session.
237
+ let first;
238
+ for (const e of held) {
239
+ const stored = await this.inner.append(session, e);
240
+ first ??= stored;
241
+ }
242
+ return first;
243
+ }
244
+ // Some other first write: the hold is dropped and the session starts as the runner writes it.
245
+ t.opening = undefined;
246
+ }
247
+ const stored = await this.inner.append(session, event);
248
+ const order = this.seq++;
249
+ t.firstSeq ??= order;
250
+ if (stored.author !== 'user' && this.firstAgentSeq === undefined)
251
+ this.firstAgentSeq = order;
252
+ this.track(stored);
253
+ if (getFunctionResponses(stored).length > 0 && this.openCalls.size === 0)
254
+ await this.boundary();
255
+ return stored;
256
+ }
257
+ /** A session is the run's opening one when its first event is the run's message, stored before any agent wrote. */
258
+ openingOf(t, events) {
259
+ if (t.opening !== undefined)
260
+ return t.opening;
261
+ const first = events[0];
262
+ if (!first || first.author !== 'user')
263
+ return false;
264
+ if (JSON.stringify(first.content?.parts ?? null) !== this.openingParts)
265
+ return false;
266
+ if (this.firstAgentSeq === -1)
267
+ return false;
268
+ return this.firstAgentSeq === undefined || (t.firstSeq !== undefined && t.firstSeq < this.firstAgentSeq);
269
+ }
270
+ async snapshot() {
271
+ const sessions = [];
272
+ for (const t of this.tracked.values()) {
273
+ const events = t.held ?? (await this.inner.get({ appName: t.appName, userId: t.userId, sessionId: t.id }))?.events;
274
+ if (!events)
275
+ continue;
276
+ sessions.push(structuredClone({ appName: t.appName, userId: t.userId, id: t.id, createdState: t.createdState, events, opening: this.openingOf(t, events) }));
277
+ }
278
+ return {
279
+ version: CHECKPOINT_VERSION,
280
+ runId: this.opts.runId,
281
+ sessionId: this.runSessionId,
282
+ openingHash: openingHashOf(this.opts.parts),
283
+ steps: this.stepCount,
284
+ savedAt: new Date().toISOString(),
285
+ sessions,
286
+ };
287
+ }
288
+ /** One step boundary: counted, snapshotted, saved; saves run one after another. */
289
+ async boundary() {
290
+ if (this.savesLost)
291
+ return;
292
+ this.stepCount += 1;
293
+ const checkpoint = await this.snapshot();
294
+ const run = this.saving.then(async () => {
295
+ if (this.savesLost)
296
+ return;
297
+ try {
298
+ const kept = await this.opts.save(checkpoint);
299
+ if (kept === false) {
300
+ this.savesLost = true;
301
+ this.opts.onLost?.();
302
+ }
303
+ }
304
+ catch (err) {
305
+ this.opts.onSaveError?.(err);
306
+ }
307
+ });
308
+ this.saving = run;
309
+ await run;
310
+ }
311
+ }
312
+ /**
313
+ * A session store that checkpoints a run: the in-process store `inner`,
314
+ * restored from `from` when it is this run's checkpoint for this message,
315
+ * saving a snapshot of every session at each step boundary (see the header).
316
+ */
317
+ export function checkpointingSessions(opts) {
318
+ return new CheckpointingStore(opts);
319
+ }
320
+ /**
321
+ * runSyndicateTurn, durable: the run's sessions live in a fresh in-process
322
+ * store, restored from the sink's checkpoint when it is this run's for this
323
+ * message, and checkpointed at every step boundary. A save that resolves
324
+ * false aborts the run (`lostClaim`). The caller clears the checkpoint when
325
+ * the run finishes; this never does.
326
+ */
327
+ export async function runDurableTurn(opts) {
328
+ const { runId, checkpoints, onLost, onSaveError, sessions: inner, signal, ...turn } = opts;
329
+ const from = usableCheckpoint(await checkpoints.load(), runId, turn.parts);
330
+ const sessionId = from ? from.sessionId : randomUUID();
331
+ const controller = new AbortController();
332
+ const forward = () => controller.abort(signal?.reason);
333
+ if (signal?.aborted)
334
+ forward();
335
+ else
336
+ signal?.addEventListener('abort', forward, { once: true });
337
+ let lostClaim = false;
338
+ const sessionService = checkpointingSessions({
339
+ runId,
340
+ sessionId,
341
+ parts: turn.parts,
342
+ from,
343
+ save: (cp) => checkpoints.save(cp),
344
+ onLost: () => {
345
+ lostClaim = true;
346
+ onLost?.();
347
+ controller.abort(new Error('the run no longer holds its claim'));
348
+ },
349
+ ...(onSaveError ? { onSaveError } : {}),
350
+ ...(inner ? { inner } : {}),
351
+ });
352
+ try {
353
+ const result = await runSyndicateTurn({ ...turn, sessionId, sessionService, signal: controller.signal });
354
+ return { ...result, resumedFromStep: sessionService.restored && from ? from.steps : 0, lostClaim, sessionId };
355
+ }
356
+ finally {
357
+ signal?.removeEventListener('abort', forward);
358
+ }
359
+ }
@@ -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,34 @@
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.
60
+ * A skill script's approval is the same pause, and so is an OAuth consent
61
+ * request (ADR 0118): the child gets the caller's consent step, its flows
62
+ * bound to the caller's app (consentPinnedTo), and a grant's answer travels
63
+ * down the open call as a decision does.
41
64
  *
42
- * Calls to subagents in one step run one after another, in call order, as
43
- * ADK runs them; running them concurrently is WS6.
65
+ * CALLS TO SUBAGENTS IN ONE STEP RUN CONCURRENTLY (ADR 0116), under the
66
+ * caller's `max_concurrency` (the syndicate's root key, default
67
+ * DEFAULT_MAX_CONCURRENCY): agentLoop.ts runs each delegated call through
68
+ * the step's DelegationGate, which starts them in call order as slots free
69
+ * up, and runs two calls to the same subagent (one child session) one after
70
+ * the other. The step's responses are stored once every call has answered,
71
+ * in call order, whatever order the children finished in; a child that
72
+ * pauses leaves its call open while the others finish and are stored. ADK
73
+ * ran them one after another; `max_concurrency: 1` keeps that order.
44
74
  *
45
75
  * NESTED SYNDICATES: a `yaml_reference` subagent is the nested syndicate's
46
76
  * orchestrator, named as the entry, listing its own subagent tools. It runs
@@ -49,6 +79,13 @@
49
79
  *
50
80
  * A NESTED WORKFLOW (ADR 0098): a `yaml_reference` to a workflow syndicate
51
81
  * is a workflowSubagentTool, holding the whole graph as a WorkflowSubagent.
82
+ * A walk that ends paused (an ask_user node's question, an agent node's
83
+ * approval request) leaves the call open as a child loop's pause does
84
+ * (ADR 0111): the call resolves to a SubagentPause naming the walk's open
85
+ * interrupts, and resumeWorkflowSubagent walks the graph again on the
86
+ * answer, which the walk's own resume (lib/workflow/resume.ts) reads as it
87
+ * reads a top-level workflow's. An `ask_user` answer reaches the walk as
88
+ * ADK's explicit reply to its `adk_request_input` call.
52
89
  * A call runs as steps 1 to 6 say, with the walk in place of the loop: the
53
90
  * graph's walk (lib/workflow/turn.ts, runNativeWorkflow, which
54
91
  * lib/compileNative.ts hands over) stores the message itself and yields the
@@ -67,8 +104,8 @@
67
104
  */
68
105
  import type { ModelAdapter } from '../../models/contract.ts';
69
106
  import type { Tool, ToolContext } from '../../tools/tool.ts';
70
- import type { TurnEvent } from '../events.ts';
71
- import type { SessionService } from '../sessions.ts';
107
+ import type { TurnEvent, TurnFunctionCall, TurnPart } from '../events.ts';
108
+ import type { Session, SessionService } from '../sessions.ts';
72
109
  import type { AgentLoopContext, AgentLoopEnd } from './agentLoop.ts';
73
110
  import type { NativeAgent } from './request.ts';
74
111
  import { SelfCorrection } from './selfCorrection.ts';
@@ -101,12 +138,26 @@ export interface WorkflowSubagentRun {
101
138
  selfCorrection: SelfCorrection;
102
139
  credentials?: AgentLoopContext['credentials'];
103
140
  }
141
+ /** How a nested workflow's walk ended, as far as a delegated call reads it (lib/workflow/turn.ts NativeWorkflowEnd). */
142
+ export interface WorkflowSubagentEnd {
143
+ /** The walk's result; `interruptIds` holds what it paused on. */
144
+ run?: {
145
+ interruptIds: readonly string[];
146
+ };
147
+ stopped?: boolean;
148
+ /**
149
+ * The request calls it paused on, by id, when they are not in its own
150
+ * session: a nested dispatch syndicate's (ADR 0120) may wait in a route's
151
+ * delegation below it. A workflow node raises these again on its caller's walk.
152
+ */
153
+ requests?: TurnFunctionCall[];
154
+ }
104
155
  /** A workflow syndicate delegated to as a subagent (ADR 0098): its name and description, and the walk one call runs. */
105
156
  export interface WorkflowSubagent {
106
157
  name: string;
107
158
  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>;
159
+ /** 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. */
160
+ walk(run: WorkflowSubagentRun): AsyncGenerator<TurnEvent, WorkflowSubagentEnd | undefined>;
110
161
  }
111
162
  /** 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
163
  export interface WorkflowSubagentTool extends Tool {
@@ -121,19 +172,136 @@ export declare function workflowSubagentOf(tool: unknown): WorkflowSubagent | un
121
172
  * carrying SUBAGENT) holds, else undefined.
122
173
  */
123
174
  export declare function subagentOf(tool: unknown): NativeAgent | undefined;
175
+ /**
176
+ * A delegated call whose child run ended paused (ADR 0110): the ids the
177
+ * child waits on. The caller stores no response to the call and ends its
178
+ * own run paused.
179
+ */
180
+ export declare class SubagentPause {
181
+ readonly pending: readonly string[];
182
+ constructor(pending: readonly string[]);
183
+ }
124
184
  /** What a delegated call needs from its caller's step. */
125
185
  export interface DelegationScope {
126
186
  /** The caller's loop context: its session, store, adapters, memory, signal. */
127
187
  ctx: AgentLoopContext;
188
+ /** The caller's name: a segment of the path a top-level caller's subagents are filed under (childAppName). */
189
+ caller: string;
128
190
  /** The caller's session state as the step left it. */
129
191
  stateBase: Readonly<Record<string, unknown>>;
130
192
  /** The turn's signal, as the step sent it. */
131
193
  signal?: AbortSignal;
132
194
  /** The loop the child runs on (agentLoop.ts passes its own runAgentLoop). */
133
195
  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;
136
196
  }
197
+ /** Delegated calls one step runs at once when the syndicate sets no `max_concurrency` (ADR 0116). */
198
+ export declare const DEFAULT_MAX_CONCURRENCY = 4;
199
+ /** One delegated call's place in its step's DelegationGate. */
200
+ export interface DelegationTicket {
201
+ /** Resolves once the call may start: a slot is free, and every earlier call under its key has released. */
202
+ readonly ready: Promise<void>;
203
+ /** Frees the slot and the key's lane. Idempotent; call it whether the call ran, failed or never started. */
204
+ release(): void;
205
+ }
206
+ /**
207
+ * The delegated calls of one step (ADR 0116): at most `limit` run at once,
208
+ * started in the order they entered (agentLoop.ts enters them in call
209
+ * order, synchronously, before any of them runs), and calls sharing a key
210
+ * (the subagent's name, so one child session) run one after another in
211
+ * that order. A slot is held from the call's start to its release, which
212
+ * comes as soon as the child's run ends: what the step does after (self-
213
+ * correction's bookkeeping, which waits on earlier calls) holds no slot. A
214
+ * call waiting on an earlier same-key call holds its slot meanwhile: the
215
+ * earlier call already holds one, so the wait always ends.
216
+ */
217
+ export declare class DelegationGate {
218
+ readonly limit: number;
219
+ private active;
220
+ private readonly waiting;
221
+ private readonly lanes;
222
+ constructor(limit?: number);
223
+ /** Enters one call under `key`, behind every call entered before it. */
224
+ enter(key: string): DelegationTicket;
225
+ /** Runs `run` under a ticket for `key`: once ready, the ticket released when it settles. */
226
+ run<T>(key: string, run: () => Promise<T>): Promise<T>;
227
+ private pump;
228
+ }
229
+ /**
230
+ * The app name a delegated call's child session is filed under (ADR 0111):
231
+ * the agent path. Below a top-level agent, its app name, its name and the
232
+ * subagent's (`<app>/<caller>/<subagent>`); below a delegated subagent,
233
+ * whose own session is already filed under its path, that app name and the
234
+ * subagent's. Agent names are identifiers (lib/syndicateSchema.ts), so
235
+ * never hold a `/`.
236
+ */
237
+ export declare function childAppName(parent: {
238
+ appName: string;
239
+ delegated?: boolean;
240
+ }, caller: string, name: string): string;
241
+ /**
242
+ * What runs a child session filed below a route or a node (ADR 0120): a
243
+ * dispatch route, or a workflow node. Its segment carries the kind, so a
244
+ * route's or a node's session never shares a key with a delegated call's.
245
+ */
246
+ export type EntryKind = 'route' | 'node';
247
+ /**
248
+ * The app name a nested syndicate run as a dispatch route or a workflow
249
+ * node is filed under (ADR 0119, ADR 0120): the session that runs it, and
250
+ * the entry's name behind its kind (`<app>/route:<route>`,
251
+ * `<walk's app>/node:<node>`). Agent names are identifiers, so a `:` never
252
+ * occurs in one, and a delegated call's key (childAppName: names only)
253
+ * never equals a route's or a node's: a delegate orchestrator `X` calling
254
+ * `Y` files `<app>/X/Y`, a route `X` with a nested node `Y` files
255
+ * `<app>/route:X/node:Y`. F3 (ADR 0119) filed it under `<app>/<entry>`
256
+ * (entryAppNameF3) and ADR 0106 under the entry's name alone; entrySession
257
+ * and the pause walk still read both.
258
+ */
259
+ export declare function entryAppName(parentAppName: string, name: string, kind: EntryKind): string;
260
+ /** Where ADR 0119 filed a route's or a node's session before the kind segment (ADR 0120): `<app>/<entry>`. */
261
+ export declare function entryAppNameF3(parentAppName: string, name: string): string;
262
+ /**
263
+ * The session a delegated call ran in before ADR 0111, filed under the
264
+ * subagent's name alone, when it is the one to continue (`continues`): the
265
+ * caller of a stored conversation called the subagent there before, or a
266
+ * pause was found waiting in it. Read only when no session exists under the
267
+ * agent path.
268
+ */
269
+ export declare function legacyChild(sessions: Pick<SessionService, 'get'>, key: {
270
+ userId: string;
271
+ sessionId: string;
272
+ }, name: string, continues: boolean): Promise<Session | undefined>;
273
+ /**
274
+ * The session a route's or a node's entry ran in before the kind segment
275
+ * (ADR 0120), when it is the one to continue (`continues`): the one F3
276
+ * filed under `<app>/<entry>`, else the one ADR 0106 filed under the
277
+ * entry's name alone. Read only when none exists under entryAppName.
278
+ */
279
+ export declare function legacyEntry(sessions: Pick<SessionService, 'get'>, key: {
280
+ appName: string;
281
+ userId: string;
282
+ sessionId: string;
283
+ }, name: string, continues: boolean): Promise<Session | undefined>;
284
+ /**
285
+ * The child session a nested syndicate run as a dispatch route or a
286
+ * workflow node runs on (ADR 0119, ADR 0120): the one filed under
287
+ * entryAppName, else, when the entry ran in `parent` before (an event it
288
+ * authored there) or a pause was found waiting below it (`resuming`), the
289
+ * one filed under an older key (legacyEntry), else a new one from `state`.
290
+ */
291
+ export declare function entrySession(sessions: Pick<SessionService, 'get' | 'create'>, parent: {
292
+ appName: string;
293
+ userId: string;
294
+ sessionId: string;
295
+ events: readonly TurnEvent[];
296
+ }, name: string, kind: EntryKind, state: Record<string, unknown>, resuming?: boolean): Promise<Session>;
297
+ /**
298
+ * The consent step as a child run reaches it (ADR 0118): a flow it begins
299
+ * is bound to `appName`, the caller's app, so the callback stores the grant
300
+ * under the app the run's pinned credentials read (the root's), never under
301
+ * the child session's agent path. A grandchild pins its caller's pinned
302
+ * step again; the first pin is applied last, so the root's app wins.
303
+ */
304
+ export declare function consentPinnedTo(consent: NonNullable<AgentLoopContext['consent']>, appName: string): NonNullable<AgentLoopContext['consent']>;
137
305
  /**
138
306
  * Runs one delegated call: `agent` on its own session with the call's
139
307
  * `request` as its message, the child's state writes recorded in `context`.
@@ -141,6 +309,14 @@ export interface DelegationScope {
141
309
  * throws, and for an output-schema answer that does not parse.
142
310
  */
143
311
  export declare function runSubagent(agent: NativeAgent, args: Record<string, unknown>, context: ToolContext, scope: DelegationScope): Promise<unknown>;
312
+ /**
313
+ * Resumes a delegated call the child left paused (ADR 0110): `answer` (the
314
+ * caller's user message parts that answer what waits below the call) is
315
+ * stored as the child's next message and the child's loop runs on, as
316
+ * runSubagent runs it. Resolves as runSubagent does, a SubagentPause again
317
+ * when the child pauses again.
318
+ */
319
+ export declare function resumeSubagent(agent: NativeAgent, answer: TurnPart[], context: ToolContext, scope: DelegationScope): Promise<unknown>;
144
320
  /**
145
321
  * Runs one delegated call to a nested workflow (ADR 0098): the whole graph
146
322
  * walked on the child session with the call's `request` as its message,
@@ -148,3 +324,31 @@ export declare function runSubagent(agent: NativeAgent, args: Record<string, unk
148
324
  * yielded event's text, '' for none. Throws what the walk throws.
149
325
  */
150
326
  export declare function runWorkflowSubagent(workflow: WorkflowSubagent, args: Record<string, unknown>, context: ToolContext, scope: DelegationScope): Promise<unknown>;
327
+ /**
328
+ * Resumes a delegated call whose nested workflow ended paused (ADR 0111):
329
+ * the graph is walked again on `answer` (the caller's user message parts
330
+ * that answer the walk's open interrupt), which the walk's resume reads as
331
+ * it reads a top-level workflow's. An approval decision goes as it came; an
332
+ * `ask_user` answer goes as ADK's explicit reply to the `adk_request_input`
333
+ * call it answers, under that call's name. Resolves as runWorkflowSubagent
334
+ * does, a SubagentPause again when the walk pauses again.
335
+ */
336
+ export declare function resumeWorkflowSubagent(workflow: WorkflowSubagent, answer: TurnPart[], context: ToolContext, scope: DelegationScope): Promise<unknown>;
337
+ /** A nested dispatch syndicate as its entry runs it: its name, the loaded file, and the options its routes compile with (lib/compile.ts DispatchSpec). */
338
+ export interface NestedDispatchEntry {
339
+ name: string;
340
+ config: unknown;
341
+ compile: unknown;
342
+ }
343
+ /**
344
+ * Runs a nested dispatch syndicate as a walk (ADR 0120): its own turn, on
345
+ * the conversation its caller opened (`run`), through the turn runner the
346
+ * current turn carries (TurnControl.nestedDispatch), under the turn's
347
+ * controls. Completed, it yields one event, never stored, that carries the
348
+ * route's final text and the conversation's state writes: the caller reads
349
+ * the answer and records the writes from it, as from a walk's last event.
350
+ * Paused, it ends with the request's id, and the request call itself for a
351
+ * workflow node to raise again. Failed, it throws the turn's error. A
352
+ * delegated call, a dispatch route and a workflow node all run it.
353
+ */
354
+ export declare function nestedDispatchWalk(entry: NestedDispatchEntry, run: Pick<WorkflowSubagentRun, 'sessions' | 'appName' | 'userId' | 'sessionId' | 'userParts' | 'signal'>): AsyncGenerator<TurnEvent, WorkflowSubagentEnd | undefined>;