@dynamicagents/core 0.9.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 (168) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +522 -0
  3. package/dist/a2a/agent-stub.d.ts +60 -0
  4. package/dist/a2a/agent-stub.js +1 -0
  5. package/dist/a2a/caller-token.d.ts +44 -0
  6. package/dist/a2a/caller-token.js +61 -0
  7. package/dist/a2a/caller.d.ts +23 -0
  8. package/dist/a2a/caller.js +33 -0
  9. package/dist/a2a/card.d.ts +158 -0
  10. package/dist/a2a/card.js +163 -0
  11. package/dist/a2a/context.d.ts +19 -0
  12. package/dist/a2a/context.js +61 -0
  13. package/dist/a2a/deliver.d.ts +159 -0
  14. package/dist/a2a/deliver.js +185 -0
  15. package/dist/a2a/executor.d.ts +84 -0
  16. package/dist/a2a/executor.js +106 -0
  17. package/dist/a2a/index.d.ts +35 -0
  18. package/dist/a2a/index.js +34 -0
  19. package/dist/a2a/notify.d.ts +106 -0
  20. package/dist/a2a/notify.js +180 -0
  21. package/dist/a2a/parts.d.ts +45 -0
  22. package/dist/a2a/parts.js +94 -0
  23. package/dist/a2a/push.d.ts +70 -0
  24. package/dist/a2a/push.js +53 -0
  25. package/dist/a2a/self-origin.d.ts +91 -0
  26. package/dist/a2a/self-origin.js +114 -0
  27. package/dist/a2a/task-store.d.ts +44 -0
  28. package/dist/a2a/task-store.js +99 -0
  29. package/dist/a2a/task.d.ts +85 -0
  30. package/dist/a2a/task.js +15 -0
  31. package/dist/a2a/verify.d.ts +80 -0
  32. package/dist/a2a/verify.js +143 -0
  33. package/dist/agent/budget.d.ts +46 -0
  34. package/dist/agent/budget.js +42 -0
  35. package/dist/agent/control.d.ts +109 -0
  36. package/dist/agent/control.js +115 -0
  37. package/dist/agent/errors.d.ts +85 -0
  38. package/dist/agent/errors.js +64 -0
  39. package/dist/agent/final-reply.d.ts +49 -0
  40. package/dist/agent/final-reply.js +68 -0
  41. package/dist/agent/history.d.ts +97 -0
  42. package/dist/agent/history.js +133 -0
  43. package/dist/agent/index.d.ts +29 -0
  44. package/dist/agent/index.js +29 -0
  45. package/dist/agent/inference.d.ts +110 -0
  46. package/dist/agent/inference.js +120 -0
  47. package/dist/agent/model.d.ts +90 -0
  48. package/dist/agent/model.js +1 -0
  49. package/dist/agent/session.d.ts +100 -0
  50. package/dist/agent/session.js +82 -0
  51. package/dist/agent/workers-ai/index.d.ts +23 -0
  52. package/dist/agent/workers-ai/index.js +23 -0
  53. package/dist/agent/workers-ai/runtime.d.ts +42 -0
  54. package/dist/agent/workers-ai/runtime.js +63 -0
  55. package/dist/alarm/index.d.ts +77 -0
  56. package/dist/alarm/index.js +116 -0
  57. package/dist/config.d.ts +202 -0
  58. package/dist/config.js +135 -0
  59. package/dist/contract/index.d.ts +9 -0
  60. package/dist/contract/index.js +8 -0
  61. package/dist/contract/plugin.d.ts +324 -0
  62. package/dist/contract/plugin.js +114 -0
  63. package/dist/contract/recipe.d.ts +180 -0
  64. package/dist/contract/recipe.js +1 -0
  65. package/dist/contract/validation.d.ts +91 -0
  66. package/dist/contract/validation.js +84 -0
  67. package/dist/db/db.d.ts +147 -0
  68. package/dist/db/db.js +90 -0
  69. package/dist/db/index.d.ts +8 -0
  70. package/dist/db/index.js +8 -0
  71. package/dist/db/migrations/index.d.ts +20 -0
  72. package/dist/db/migrations/index.js +60 -0
  73. package/dist/db/models/subtasks.d.ts +100 -0
  74. package/dist/db/models/subtasks.js +241 -0
  75. package/dist/db/models/tasks.d.ts +118 -0
  76. package/dist/db/models/tasks.js +274 -0
  77. package/dist/db/schema.d.ts +468 -0
  78. package/dist/db/schema.js +88 -0
  79. package/dist/env.d.ts +53 -0
  80. package/dist/env.js +47 -0
  81. package/dist/host/agent.d.ts +305 -0
  82. package/dist/host/agent.js +400 -0
  83. package/dist/host/index.d.ts +20 -0
  84. package/dist/host/index.js +19 -0
  85. package/dist/host/plugin-host.d.ts +42 -0
  86. package/dist/host/plugin-host.js +1 -0
  87. package/dist/index.d.ts +19 -0
  88. package/dist/index.js +17 -0
  89. package/dist/job/index.d.ts +21 -0
  90. package/dist/job/index.js +21 -0
  91. package/dist/job/lifecycle.d.ts +176 -0
  92. package/dist/job/lifecycle.js +230 -0
  93. package/dist/job/state.d.ts +92 -0
  94. package/dist/job/state.js +40 -0
  95. package/dist/platform.d.ts +138 -0
  96. package/dist/platform.js +140 -0
  97. package/dist/round/agent.d.ts +271 -0
  98. package/dist/round/agent.js +678 -0
  99. package/dist/round/index.d.ts +25 -0
  100. package/dist/round/index.js +23 -0
  101. package/dist/round/policy.d.ts +98 -0
  102. package/dist/round/policy.js +1 -0
  103. package/dist/round/subagent.d.ts +87 -0
  104. package/dist/round/subagent.js +108 -0
  105. package/dist/round/turn.d.ts +249 -0
  106. package/dist/round/turn.js +564 -0
  107. package/dist/round/workflow.d.ts +147 -0
  108. package/dist/round/workflow.js +387 -0
  109. package/dist/runtime/index.d.ts +127 -0
  110. package/dist/runtime/index.js +186 -0
  111. package/dist/runtime/tool-families.d.ts +32 -0
  112. package/dist/runtime/tool-families.js +61 -0
  113. package/dist/subagent/fingerprint.d.ts +37 -0
  114. package/dist/subagent/fingerprint.js +92 -0
  115. package/dist/subagent/index.d.ts +169 -0
  116. package/dist/subagent/index.js +330 -0
  117. package/dist/subagent/prompt.d.ts +32 -0
  118. package/dist/subagent/prompt.js +37 -0
  119. package/dist/subagent/run.d.ts +157 -0
  120. package/dist/subagent/run.js +540 -0
  121. package/dist/subagent/workspace.d.ts +85 -0
  122. package/dist/subagent/workspace.js +127 -0
  123. package/dist/subtasks/catalog.d.ts +40 -0
  124. package/dist/subtasks/catalog.js +36 -0
  125. package/dist/subtasks/decomposition.d.ts +85 -0
  126. package/dist/subtasks/decomposition.js +156 -0
  127. package/dist/subtasks/delegate.d.ts +120 -0
  128. package/dist/subtasks/delegate.js +131 -0
  129. package/dist/subtasks/index.d.ts +9 -0
  130. package/dist/subtasks/index.js +9 -0
  131. package/dist/subtasks/subtask-types.d.ts +91 -0
  132. package/dist/subtasks/subtask-types.js +103 -0
  133. package/dist/subtasks/types.d.ts +295 -0
  134. package/dist/subtasks/types.js +15 -0
  135. package/dist/testing/auth.d.ts +34 -0
  136. package/dist/testing/auth.js +35 -0
  137. package/dist/testing/do.d.ts +29 -0
  138. package/dist/testing/do.js +25 -0
  139. package/dist/testing/fake-session.d.ts +26 -0
  140. package/dist/testing/fake-session.js +37 -0
  141. package/dist/testing/fixtures.d.ts +64 -0
  142. package/dist/testing/fixtures.js +104 -0
  143. package/dist/testing/harness.d.ts +97 -0
  144. package/dist/testing/harness.js +138 -0
  145. package/dist/testing/index.d.ts +31 -0
  146. package/dist/testing/index.js +35 -0
  147. package/dist/testing/mock-model.d.ts +77 -0
  148. package/dist/testing/mock-model.js +136 -0
  149. package/dist/testing/node.d.ts +56 -0
  150. package/dist/testing/node.js +56 -0
  151. package/dist/testing/vcr-global-setup.d.ts +12 -0
  152. package/dist/testing/vcr-global-setup.js +15 -0
  153. package/dist/testing/vcr-shared.d.ts +38 -0
  154. package/dist/testing/vcr-shared.js +33 -0
  155. package/dist/testing/vcr-spec.d.ts +25 -0
  156. package/dist/testing/vcr-spec.js +124 -0
  157. package/dist/testing/vcr-store.d.ts +86 -0
  158. package/dist/testing/vcr-store.js +191 -0
  159. package/dist/testing/vcr.d.ts +117 -0
  160. package/dist/testing/vcr.js +275 -0
  161. package/dist/worker/define-agent.d.ts +123 -0
  162. package/dist/worker/define-agent.js +20 -0
  163. package/dist/worker/index.d.ts +218 -0
  164. package/dist/worker/index.js +369 -0
  165. package/eslint-rules/index.js +31 -0
  166. package/eslint-rules/no-deprecated-object-properties.js +81 -0
  167. package/package.json +178 -0
  168. package/scripts/generate-keys.mjs +48 -0
@@ -0,0 +1,330 @@
1
+ import { Agent } from "agents";
2
+ import { z } from "zod";
3
+ import { CHUNK_SOFT_MS } from "../platform.js";
4
+ import { buildRecipeTools } from "../runtime/tool-families.js";
5
+ import { RecipeValidationError, validateRecipe } from "../contract/validation.js";
6
+ import { SubtaskParamsError } from "../subtasks/subtask-types.js";
7
+ import { SelfOrigin } from "../a2a/self-origin.js";
8
+ import { renderSubagentPrompt } from "./prompt.js";
9
+ import { makeWorkspaceHandle } from "./workspace.js";
10
+ import { fingerprintRequest } from "./fingerprint.js";
11
+ import { runResumableChunk } from "./run.js";
12
+ /**
13
+ * Message prefix of the error thrown when a child that already holds a cached
14
+ * terminal result receives a *different* request. Custom error classes don't
15
+ * survive DO RPC, so this prefix is the cross-boundary contract: it signals a
16
+ * parent lifecycle bug — stale children must be deleted before a genuinely new
17
+ * execution — and a Workflow retry after the parent's cleanup will succeed.
18
+ */
19
+ export const FINGERPRINT_MISMATCH = "recipe-subagent: request fingerprint mismatch";
20
+ /** Deterministic managed-child name for one Subtask execution. */
21
+ export function subagentName(taskId, subtaskId) {
22
+ return `subtask:${taskId}:${subtaskId}`;
23
+ }
24
+ /** Zod mirror of {@link RecipeExecutionResult} for parsing the cached JSON. */
25
+ const cachedResultSchema = z.discriminatedUnion("status", [
26
+ z.object({
27
+ status: z.literal("completed"),
28
+ resultParts: z
29
+ .array(z.object({ kind: z.literal("text"), text: z.string() }))
30
+ .min(1),
31
+ modelId: z.string()
32
+ }),
33
+ z.object({
34
+ status: z.literal("failed"),
35
+ error: z.string(),
36
+ modelId: z.string().nullable()
37
+ })
38
+ ]);
39
+ /**
40
+ * `RecipeSubagent` — the isolated, stateless managed child that executes one
41
+ * Subtask under a resolved Recipe. Created as an Agents SDK sub-agent (facet)
42
+ * beneath the calling agent, so it needs no wrangler Durable Object binding and
43
+ * no `new_sqlite_classes` entry; it must only be exported from the consuming
44
+ * Worker's entry so `ctx.exports` can resolve it by class name.
45
+ *
46
+ * It never constructs a Session, never reads parent history beyond the
47
+ * references supplied on its request, never reaches durable memory, and never
48
+ * resolves a Recipe itself — it defensively re-validates the resolved Recipe the
49
+ * parent sends and accepts no configuration beyond it.
50
+ *
51
+ * Retry safety: the child persists at most one terminal result in its own
52
+ * SQLite, keyed by the deterministic request fingerprint, plus the rolling
53
+ * `run_state` of an in-progress multi-chunk run. A retry with the same
54
+ * fingerprint replays the terminal result or resumes the run without repeating
55
+ * completed work; a different request for the same child name is rejected
56
+ * ({@link FINGERPRINT_MISMATCH}). Transient platform faults throw and cache
57
+ * nothing, so the enclosing Workflow step can retry. The parent deletes the child
58
+ * (`deleteSubAgent`) only after its durable copy of the result succeeds, which
59
+ * wipes this storage — the workspace and run state included.
60
+ *
61
+ * Not "stateless" like the single-shot original: it owns per-execution durable
62
+ * state (the workspace and the run checkpoint), scoped to one execution and swept
63
+ * with the child.
64
+ */
65
+ export class RecipeSubagentBase extends Agent {
66
+ /**
67
+ * Test-only `ModelPair` injection (a field, so never on the RPC stub).
68
+ * A whole pair — rather than model instances — so error-path tests can throw
69
+ * synchronously from the pair's factories, the repo convention (a rejecting
70
+ * `doGenerate` inside `generateText` leaks an unhandled rejection through
71
+ * the AI SDK telemetry span that workerd flags as a failure).
72
+ */
73
+ modelsOverride;
74
+ _workspace;
75
+ /**
76
+ * The chunk currently executing here, if any. In memory only — it exists to be
77
+ * interrupted mid-call, and an isolate that lost it has no in-flight call left
78
+ * to interrupt. See {@link abortRun}.
79
+ */
80
+ inflight;
81
+ /**
82
+ * This deployment's own public origin, as the parent DO passes it on every
83
+ * chunk, pinned from the first. In memory for the same reason {@link inflight}
84
+ * is: a facet is reached only through {@link executeChunk}, so an instance that
85
+ * lost it is an instance that will be told again before it can run anything.
86
+ * See {@link SelfOrigin}.
87
+ */
88
+ selfOriginMemo = new SelfOrigin();
89
+ async onStart() {
90
+ this.ensureTables();
91
+ }
92
+ /**
93
+ * Idempotent schema bootstrap. Also called lazily from the RPCs so
94
+ * `runInDurableObject`-style tests reach ready tables without RPC dispatch
95
+ * (mirroring how `AgentDB` migrates on construction).
96
+ */
97
+ ensureTables() {
98
+ this.sql `
99
+ CREATE TABLE IF NOT EXISTS execution_cache (
100
+ slot INTEGER PRIMARY KEY CHECK (slot = 1),
101
+ fingerprint TEXT NOT NULL,
102
+ result_json TEXT NOT NULL,
103
+ created_at INTEGER NOT NULL
104
+ )
105
+ `;
106
+ this.sql `
107
+ CREATE TABLE IF NOT EXISTS run_state (
108
+ slot INTEGER PRIMARY KEY CHECK (slot = 1),
109
+ fingerprint TEXT NOT NULL,
110
+ state_json TEXT NOT NULL,
111
+ updated_at INTEGER NOT NULL
112
+ )
113
+ `;
114
+ }
115
+ /** The recipe's durable workspace, backed by this facet's own SQLite storage. */
116
+ workspace() {
117
+ return (this._workspace ??= this.subagentRuntime().workspaceBacking(this.ctx.storage.sql, () => this.name));
118
+ }
119
+ /**
120
+ * This deployment's own public origin, if the parent has passed it to this
121
+ * instance yet. See {@link SelfOrigin}.
122
+ */
123
+ selfOrigin() {
124
+ return this.selfOriginMemo.peek();
125
+ }
126
+ /**
127
+ * The same, for a caller that cannot proceed without it — a facet that signs
128
+ * its own caller tokens, above all. Mirrors `DynamicAgent.requireSelfOrigin`,
129
+ * because a facet must run on the same provider, and so the same credential
130
+ * path, as the parent that delegated to it.
131
+ */
132
+ requireSelfOrigin() {
133
+ return this.selfOriginMemo.require();
134
+ }
135
+ /**
136
+ * Execute one durable chunk of a Subtask under the parent's resolved Recipe.
137
+ *
138
+ * A terminal outcome (completed / failed) is cached and replayed on retry. A
139
+ * mid-run chunk persists its rolling state to `run_state` and returns a
140
+ * `done: false` yield for the Workflow to run another chunk. `chunk` and
141
+ * `selfOrigin` are separate arguments — never part of `request` — so every
142
+ * chunk fingerprints identically and the cache/resume keys line up. Only
143
+ * transient platform faults throw (nothing cached), so a Workflow retry
144
+ * resumes from the last checkpoint.
145
+ */
146
+ async executeChunk(request, _chunk, runtime = {}, selfOrigin) {
147
+ this.ensureTables();
148
+ // Before `subagentRuntime()`, which is where a host builds its model runtime
149
+ // — and a facet running on a provider it authenticates to mint-signed reads
150
+ // this origin from there.
151
+ this.selfOriginMemo.note(selfOrigin);
152
+ const rt = this.subagentRuntime();
153
+ const fingerprint = await fingerprintRequest(request);
154
+ // A terminal result already exists → replay it (idempotent retry).
155
+ const cached = this.sql `
156
+ SELECT fingerprint, result_json FROM execution_cache WHERE slot = 1
157
+ `[0];
158
+ if (cached) {
159
+ if (cached.fingerprint !== fingerprint)
160
+ throw mismatch("terminal");
161
+ return {
162
+ done: true,
163
+ result: cachedResultSchema.parse(JSON.parse(cached.result_json)),
164
+ progress: []
165
+ };
166
+ }
167
+ // Validate the recipe up front; an unusable recipe (disabled, or with no
168
+ // soul) and an empty prompt are deterministic, cacheable terminal failures
169
+ // with no model call.
170
+ let recipe;
171
+ try {
172
+ recipe = validateRecipe(request.recipe, rt.policy);
173
+ }
174
+ catch (error) {
175
+ if (!(error instanceof RecipeValidationError))
176
+ throw error;
177
+ return this.cacheTerminal(fingerprint, {
178
+ status: "failed",
179
+ error: error.message,
180
+ modelId: null
181
+ });
182
+ }
183
+ if (request.prompt.trim() === "") {
184
+ return this.cacheTerminal(fingerprint, {
185
+ status: "failed",
186
+ error: "empty subtask prompt",
187
+ modelId: null
188
+ });
189
+ }
190
+ // Re-check the type's param contract, the same defensive posture as
191
+ // `validateRecipe`: a subtask missing a param its type requires cannot
192
+ // succeed, and failing here costs no model call and gives the parent a real
193
+ // diagnostic.
194
+ try {
195
+ rt.types.validateParams(request.type, request.params);
196
+ }
197
+ catch (error) {
198
+ if (!(error instanceof SubtaskParamsError))
199
+ throw error;
200
+ return this.cacheTerminal(fingerprint, {
201
+ status: "failed",
202
+ error: error.message,
203
+ modelId: null
204
+ });
205
+ }
206
+ // Resume an in-progress run, guarding against a stale child holding a
207
+ // *different* run (the same reuse hazard the terminal cache guards).
208
+ const saved = this.sql `
209
+ SELECT fingerprint, state_json FROM run_state WHERE slot = 1
210
+ `[0];
211
+ if (saved && saved.fingerprint !== fingerprint)
212
+ throw mismatch("in-progress");
213
+ const prev = saved
214
+ ? JSON.parse(saved.state_json)
215
+ : null;
216
+ const models = this.modelsOverride ??
217
+ rt.models.createModelPair({
218
+ primaryModelId: recipe.primaryModelId,
219
+ fallbackModelId: recipe.fallbackModelId,
220
+ // AI Gateway correlation: tie this child's model calls to its Subtask.
221
+ metadata: { taskId: request.taskId, subtaskId: request.subtaskId }
222
+ });
223
+ const workspace = makeWorkspaceHandle(this.workspace());
224
+ const progress = [];
225
+ const { tools } = buildRecipeTools(recipe.toolFamilies, rt.toolFamilies, {
226
+ workspace,
227
+ emitProgress: (event) => progress.push(event),
228
+ params: request.params,
229
+ runtime
230
+ });
231
+ const { system, prompt } = renderSubagentPrompt({ ...request, recipe });
232
+ const controller = new AbortController();
233
+ this.inflight = controller;
234
+ let outcome, state;
235
+ try {
236
+ ({ outcome, state } = await runResumableChunk(prev, {
237
+ system,
238
+ seedPrompt: prompt,
239
+ models,
240
+ tools,
241
+ limits: recipe.limits,
242
+ chunkSoftMs: CHUNK_SOFT_MS,
243
+ historyWindow: recipe.historyWindow,
244
+ toolOutputWindow: rt.toolOutputWindow,
245
+ reportMetrics: recipe.reportMetrics,
246
+ maxOutputTokens: rt.maxOutputTokens,
247
+ maxRetries: rt.maxRetries,
248
+ now: () => Date.now(),
249
+ progress,
250
+ checkpoint: (s) => this.saveRunState(fingerprint, s),
251
+ abortSignal: controller.signal
252
+ }));
253
+ }
254
+ finally {
255
+ this.inflight = undefined;
256
+ }
257
+ // The per-step checkpoint already ran; persist the final state too so a chunk
258
+ // that yielded without a completed step still advances durably.
259
+ this.saveRunState(fingerprint, state);
260
+ if (outcome.done) {
261
+ return this.cacheTerminal(fingerprint, outcome.result, outcome.progress);
262
+ }
263
+ return { done: false, progress: outcome.progress };
264
+ }
265
+ /**
266
+ * Interrupt the chunk running here right now, so a cancellation lands on the
267
+ * current model call instead of at the next chunk boundary (up to `chunkSoftMs`
268
+ * later — minutes, for a long recipe). Returns whether there was one to stop.
269
+ *
270
+ * Distinct from {@link abortExecution}, which releases *external* state after
271
+ * the fact; this only stops local work. An aborted run yields rather than
272
+ * producing a terminal result, so nothing is cached and the parent resolves the
273
+ * row itself. Reaching a facet mid-`executeChunk` works because it is awaiting
274
+ * a model `fetch` at the time, which does not hold the input gate closed.
275
+ */
276
+ async abortRun() {
277
+ if (!this.inflight)
278
+ return false;
279
+ this.inflight.abort();
280
+ return true;
281
+ }
282
+ /**
283
+ * Best-effort cleanup on cancellation: rebuild the recipe's tool families and
284
+ * run their `abort` hooks (e.g. release an external resource recorded in the
285
+ * workspace session file). Reconstructible from the workspace, so it is safe on a fresh
286
+ * isolate. The parent supplies the validated tool families it resolved.
287
+ */
288
+ async abortExecution(toolFamilies) {
289
+ this.ensureTables();
290
+ const rt = this.subagentRuntime();
291
+ const ctx = {
292
+ workspace: makeWorkspaceHandle(this.workspace()),
293
+ emitProgress: () => { },
294
+ params: {},
295
+ runtime: {}
296
+ };
297
+ const { abort } = buildRecipeTools(toolFamilies, rt.toolFamilies, ctx);
298
+ if (abort)
299
+ await abort(ctx);
300
+ }
301
+ /** Persist a terminal result to the cache and return it as a done chunk. */
302
+ cacheTerminal(fingerprint, result, progress = []) {
303
+ this.sql `
304
+ INSERT INTO execution_cache (slot, fingerprint, result_json, created_at)
305
+ VALUES (1, ${fingerprint}, ${JSON.stringify(result)}, ${Date.now()})
306
+ ON CONFLICT (slot) DO UPDATE SET
307
+ fingerprint = excluded.fingerprint,
308
+ result_json = excluded.result_json,
309
+ created_at = excluded.created_at
310
+ `;
311
+ return { done: true, result, progress };
312
+ }
313
+ /** Persist the rolling run state (called after every model turn). */
314
+ saveRunState(fingerprint, state) {
315
+ this.sql `
316
+ INSERT INTO run_state (slot, fingerprint, state_json, updated_at)
317
+ VALUES (1, ${fingerprint}, ${JSON.stringify(state)}, ${Date.now()})
318
+ ON CONFLICT (slot) DO UPDATE SET
319
+ fingerprint = excluded.fingerprint,
320
+ state_json = excluded.state_json,
321
+ updated_at = excluded.updated_at
322
+ `;
323
+ }
324
+ }
325
+ /** The cross-RPC stale-child error (see {@link FINGERPRINT_MISMATCH}). */
326
+ function mismatch(phase) {
327
+ return new Error(`${FINGERPRINT_MISMATCH}: this child already holds a ${phase} state for a ` +
328
+ "different request; the parent must delete a stale child before starting a " +
329
+ "genuinely new execution");
330
+ }
@@ -0,0 +1,32 @@
1
+ import type { RecipeExecutionRequest } from "../subtasks/types.js";
2
+ import type { ValidatedRecipe } from "../contract/recipe.js";
3
+ /**
4
+ * Deterministic rendering of one subagent invocation. Pure — no model, no
5
+ * Session, no lookups: everything comes verbatim from the request. The three
6
+ * sections stay clearly separated and labeled so tests (and the model) can tell
7
+ * them apart: the execution's budget, the main-agent instruction, and the
8
+ * verbatim conversation reference snapshots.
9
+ */
10
+ /**
11
+ * A request whose Recipe has been through `validateRecipe`, so its limits are
12
+ * merged rather than partial. Required rather than convenient: the budget line
13
+ * below states a number to the model, and stating "up to undefined turns" — or
14
+ * quietly rendering a Recipe's override as the whole budget — is worse than not
15
+ * telling it at all.
16
+ */
17
+ export type RenderableExecution = RecipeExecutionRequest & {
18
+ recipe: ValidatedRecipe;
19
+ };
20
+ export interface RenderedInvocation {
21
+ /** The validated Recipe soul, verbatim — the invocation's system prompt. */
22
+ system: string;
23
+ /** The sectioned user message (budget, instruction, references). */
24
+ prompt: string;
25
+ }
26
+ /**
27
+ * Render the sectioned user message for one execution. Reference snapshots are
28
+ * emitted exactly as captured at decomposition — `[ref N]` labels with the
29
+ * message role, no summarizing, rewriting, or interpolation. Sections with no
30
+ * content are omitted entirely.
31
+ */
32
+ export declare function renderSubagentPrompt(request: RenderableExecution): RenderedInvocation;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The budget, in the terms the runner actually enforces.
3
+ *
4
+ * Worth its own section because a subagent that does not know its budget invents
5
+ * one, and a *main* agent writing the prompt invents one for it: in one logged
6
+ * play the instruction said "you have up to 20 actions total" — 20 was the turn
7
+ * budget — and the subagent stopped after eleven game moves reasoning that it
8
+ * must be out. Turns and actions are not the same currency and this is the line
9
+ * that says so.
10
+ */
11
+ function renderBudget(limits) {
12
+ const minutes = Math.round(limits.maxWallMs / 60_000);
13
+ return (`# Budget\nUp to ${limits.maxTurns} turns and about ${minutes} minutes. ` +
14
+ "One turn is one tool call, however much that call does — anything a tool " +
15
+ "counts internally is its own budget, not this one. Reaching either ceiling " +
16
+ "does not drop your work: you get one last turn, with no tools, to write up " +
17
+ "what you have.");
18
+ }
19
+ /**
20
+ * Render the sectioned user message for one execution. Reference snapshots are
21
+ * emitted exactly as captured at decomposition — `[ref N]` labels with the
22
+ * message role, no summarizing, rewriting, or interpolation. Sections with no
23
+ * content are omitted entirely.
24
+ */
25
+ export function renderSubagentPrompt(request) {
26
+ const sections = [renderBudget(request.recipe.limits)];
27
+ const prompt = request.prompt.trim();
28
+ if (prompt !== "") {
29
+ sections.push(`# Task\n${prompt}`);
30
+ }
31
+ if (request.references.length > 0) {
32
+ const refs = request.references.map((ref, i) => `[ref ${i + 1}] (${ref.role}): ${ref.text}`);
33
+ sections.push("# Conversation references (verbatim snapshots of the caller's conversation)\n" +
34
+ refs.join("\n"));
35
+ }
36
+ return { system: request.recipe.soul, prompt: sections.join("\n\n") };
37
+ }
@@ -0,0 +1,157 @@
1
+ import type { ModelMessage, ToolSet } from "ai";
2
+ import { type RecipePolicy } from "../contract/validation.js";
3
+ import type { ModelPair } from "../agent/model.js";
4
+ import type { ProgressEvent, RecipeChunkResult, RecipeExecutionRequest, RecipeExecutionResult } from "../subtasks/types.js";
5
+ import type { RecipeLimits } from "../contract/recipe.js";
6
+ /**
7
+ * The resumable execution runner — ONE loop for every Recipe, from a single-shot
8
+ * general Subtask to a long game. It runs the model/tool loop in durable
9
+ * **chunks**: each call advances as far as `chunkSoftMs` allows (or until the run
10
+ * spends its budget, or until a tool emits progress), checkpoints its rolling state
11
+ * after every turn, and returns either a terminal result or a "not done" yield. The
12
+ * facet persists the state between chunks and the Workflow runs each chunk as its
13
+ * own durable, retryable step — so no single step ever approaches the platform
14
+ * step timeout, and a crash loses at most the in-flight turn.
15
+ *
16
+ * Domain behavior lives entirely in the tool families; this runner is agnostic of
17
+ * what work happens beneath it. State that must outlive the small rolling context
18
+ * window is the recipe's responsibility to persist to its workspace.
19
+ */
20
+ /** The rolling state carried across a run's chunks (persisted by the facet). */
21
+ export interface ChunkRunState {
22
+ /** Windowed conversation so far (system is supplied separately, not stored here). */
23
+ messages: ModelMessage[];
24
+ /** Total model turns (tool-loop steps) across every chunk — bounds `maxTurns`. */
25
+ turns: number;
26
+ /** Total `generateText` invocations (including fallbacks and summarization). */
27
+ llmCalls: number;
28
+ /** Wall-clock start of the whole execution (for the metrics footer). */
29
+ startedAtMs: number;
30
+ }
31
+ /** Everything one chunk needs, assembled by the facet (or a test) each call. */
32
+ export interface ChunkRunDeps {
33
+ system: string;
34
+ /** The rendered initial user message; seeds a fresh run's first chunk. */
35
+ seedPrompt: string;
36
+ models: ModelPair;
37
+ tools: ToolSet;
38
+ /** The run's budget: turns and wall clock. Nothing else. */
39
+ limits: RecipeLimits;
40
+ /**
41
+ * How long this chunk may run before it checkpoints and yields a fresh durable
42
+ * step — the Workers step-timeout guard, not a budget, and identical for every
43
+ * Recipe (`CHUNK_SOFT_MS` in `platform.ts`). Injected rather than imported so
44
+ * the runner stays testable with a fake clock.
45
+ */
46
+ chunkSoftMs: number;
47
+ historyWindow: number;
48
+ /**
49
+ * How many of the most recent assistant turns keep their tool results in full;
50
+ * older ones are stubbed by {@link elideToolOutputs}. A mechanic of the window
51
+ * rather than a property of a domain — see `CoreConfig.toolOutputWindow`.
52
+ */
53
+ toolOutputWindow: number;
54
+ reportMetrics: boolean;
55
+ /**
56
+ * Output-token ceiling for every model call in this chunk. Injected rather
57
+ * than imported: it is host config, and a published runner must not carry a
58
+ * hardcoded one.
59
+ */
60
+ maxOutputTokens: number;
61
+ /**
62
+ * `CoreConfig.model.maxRetries` — retries on *this* model, honouring the
63
+ * provider's `retry-after`, before the slot hands over to the fallback.
64
+ */
65
+ maxRetries: number;
66
+ now: () => number;
67
+ /** Shared sink the tool families push progress events into (fresh per chunk). */
68
+ progress: ProgressEvent[];
69
+ /** Persist rolling state after every model turn — the crash-safety checkpoint. */
70
+ checkpoint: (state: ChunkRunState) => void | Promise<void>;
71
+ /**
72
+ * Interrupts the in-flight model call when the parent Task is canceled. Without
73
+ * it a cancellation is only observed at the next chunk boundary — up to
74
+ * `chunkSoftMs` of unwanted play. See {@link ChunkAttempt}'s `aborted` case for
75
+ * why an abort is emphatically *not* a model failure.
76
+ */
77
+ abortSignal?: AbortSignal;
78
+ }
79
+ export interface ChunkRunOutput {
80
+ outcome: RecipeChunkResult;
81
+ state: ChunkRunState;
82
+ }
83
+ /**
84
+ * Trim the conversation to the most recent `window` turns (plus the seed message),
85
+ * cutting at an assistant boundary so no tool-result message is left orphaned.
86
+ * Older turns fall out of context — the recipe's soul directs the model to persist
87
+ * anything durable to its workspace, which the window never touches.
88
+ */
89
+ export declare function windowMessages(messages: ModelMessage[], window: number): ModelMessage[];
90
+ /** What replaces a tool result that has aged out of the detail window. */
91
+ export declare const ELIDED_TOOL_OUTPUT = "[output from an earlier turn, trimmed to save context]";
92
+ /**
93
+ * Shrink the window without shortening it: stub out the *payloads* of tool
94
+ * results the model has moved past, leaving every message, every tool call — and
95
+ * therefore every note the model wrote itself — exactly where it was.
96
+ *
97
+ * This is the other half of {@link windowMessages}, and it exists because the two
98
+ * things a rolling window holds have opposite value curves. A recipe's own
99
+ * reasoning and its tool-call *inputs* stay useful for as long as the run does; a
100
+ * tool *result* is a snapshot of a world that has since moved, and it is also
101
+ * where nearly all the tokens are. Dropping whole turns to bound context throws
102
+ * both away together, which is why a short window makes a model re-derive what it
103
+ * already knew.
104
+ *
105
+ * A result's output survives if any of:
106
+ *
107
+ * 1. it is within the last `keepRecent` assistant messages — the same unit
108
+ * `windowMessages` counts in, so the two knobs are commensurable;
109
+ * 2. **it is the newest result for its tool**, at any age. Tools may hold state
110
+ * about what they have already shown this chunk and answer a repeat with
111
+ * "unchanged since you last looked" — a real optimization that becomes a lie
112
+ * the moment the render it points at is gone. Keeping the newest per tool is
113
+ * what makes "your latest view" a thing the model can still see, and it is why
114
+ * `keepRecent` can be small;
115
+ * 3. it reports a failure or a denial — short, diagnostic, and losing *why* a
116
+ * call failed costs a retry to rediscover for no saving;
117
+ * 4. it is already small enough that stubbing it saves nothing.
118
+ *
119
+ * Idempotent: a stubbed part is small, so a later pass leaves it alone.
120
+ *
121
+ * `keepRecent` is normalized rather than trusted — see the note on `keep` below.
122
+ * Zero is a legal, meaningful value: no turn is recent, so rule 1 protects
123
+ * nothing and rules 2-4 carry the whole of what survives.
124
+ */
125
+ export declare function elideToolOutputs(messages: ModelMessage[], keepRecent: number): ModelMessage[];
126
+ /**
127
+ * Run one durable chunk. Returns a terminal result (natural completion, budget
128
+ * exhaustion, or exhausted models) or a `done: false` yield with the progress
129
+ * emitted this chunk. Throws only on a transient platform fault, so the Workflow
130
+ * step retries and resumes from the last checkpoint.
131
+ */
132
+ export declare function runResumableChunk(prev: ChunkRunState | null, deps: ChunkRunDeps): Promise<ChunkRunOutput>;
133
+ /** Everything a whole-run (non-chunked) execution needs — for tests and callers
134
+ * that want a single terminal result rather than driving chunks themselves. */
135
+ export interface RecipeRunDeps {
136
+ models: ModelPair;
137
+ tools: ToolSet;
138
+ now?: () => number;
139
+ /** The capability boundary a recipe is re-validated against inside the child. */
140
+ policy: RecipePolicy;
141
+ /** `CoreConfig.toolOutputWindow`. */
142
+ toolOutputWindow: number;
143
+ /** `CoreConfig.model.maxOutputTokens`. */
144
+ maxOutputTokens: number;
145
+ /** `CoreConfig.model.maxRetries`. */
146
+ maxRetries: number;
147
+ }
148
+ /**
149
+ * Run one recipe execution to a terminal result, driving {@link runResumableChunk}
150
+ * chunk by chunk in memory. A run that fits its budget finishes in one chunk;
151
+ * otherwise it loops until the budget yields a summary. Used by tests and any
152
+ * caller wanting the whole outcome; the facet drives chunks durably instead, for
153
+ * crash-safety across the Workflow.
154
+ *
155
+ * Throws only on a transient platform fault (as {@link runResumableChunk} does).
156
+ */
157
+ export declare function runRecipeExecution(request: RecipeExecutionRequest, deps: RecipeRunDeps): Promise<RecipeExecutionResult>;