@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.
- package/LICENSE +201 -0
- package/README.md +522 -0
- package/dist/a2a/agent-stub.d.ts +60 -0
- package/dist/a2a/agent-stub.js +1 -0
- package/dist/a2a/caller-token.d.ts +44 -0
- package/dist/a2a/caller-token.js +61 -0
- package/dist/a2a/caller.d.ts +23 -0
- package/dist/a2a/caller.js +33 -0
- package/dist/a2a/card.d.ts +158 -0
- package/dist/a2a/card.js +163 -0
- package/dist/a2a/context.d.ts +19 -0
- package/dist/a2a/context.js +61 -0
- package/dist/a2a/deliver.d.ts +159 -0
- package/dist/a2a/deliver.js +185 -0
- package/dist/a2a/executor.d.ts +84 -0
- package/dist/a2a/executor.js +106 -0
- package/dist/a2a/index.d.ts +35 -0
- package/dist/a2a/index.js +34 -0
- package/dist/a2a/notify.d.ts +106 -0
- package/dist/a2a/notify.js +180 -0
- package/dist/a2a/parts.d.ts +45 -0
- package/dist/a2a/parts.js +94 -0
- package/dist/a2a/push.d.ts +70 -0
- package/dist/a2a/push.js +53 -0
- package/dist/a2a/self-origin.d.ts +91 -0
- package/dist/a2a/self-origin.js +114 -0
- package/dist/a2a/task-store.d.ts +44 -0
- package/dist/a2a/task-store.js +99 -0
- package/dist/a2a/task.d.ts +85 -0
- package/dist/a2a/task.js +15 -0
- package/dist/a2a/verify.d.ts +80 -0
- package/dist/a2a/verify.js +143 -0
- package/dist/agent/budget.d.ts +46 -0
- package/dist/agent/budget.js +42 -0
- package/dist/agent/control.d.ts +109 -0
- package/dist/agent/control.js +115 -0
- package/dist/agent/errors.d.ts +85 -0
- package/dist/agent/errors.js +64 -0
- package/dist/agent/final-reply.d.ts +49 -0
- package/dist/agent/final-reply.js +68 -0
- package/dist/agent/history.d.ts +97 -0
- package/dist/agent/history.js +133 -0
- package/dist/agent/index.d.ts +29 -0
- package/dist/agent/index.js +29 -0
- package/dist/agent/inference.d.ts +110 -0
- package/dist/agent/inference.js +120 -0
- package/dist/agent/model.d.ts +90 -0
- package/dist/agent/model.js +1 -0
- package/dist/agent/session.d.ts +100 -0
- package/dist/agent/session.js +82 -0
- package/dist/agent/workers-ai/index.d.ts +23 -0
- package/dist/agent/workers-ai/index.js +23 -0
- package/dist/agent/workers-ai/runtime.d.ts +42 -0
- package/dist/agent/workers-ai/runtime.js +63 -0
- package/dist/alarm/index.d.ts +77 -0
- package/dist/alarm/index.js +116 -0
- package/dist/config.d.ts +202 -0
- package/dist/config.js +135 -0
- package/dist/contract/index.d.ts +9 -0
- package/dist/contract/index.js +8 -0
- package/dist/contract/plugin.d.ts +324 -0
- package/dist/contract/plugin.js +114 -0
- package/dist/contract/recipe.d.ts +180 -0
- package/dist/contract/recipe.js +1 -0
- package/dist/contract/validation.d.ts +91 -0
- package/dist/contract/validation.js +84 -0
- package/dist/db/db.d.ts +147 -0
- package/dist/db/db.js +90 -0
- package/dist/db/index.d.ts +8 -0
- package/dist/db/index.js +8 -0
- package/dist/db/migrations/index.d.ts +20 -0
- package/dist/db/migrations/index.js +60 -0
- package/dist/db/models/subtasks.d.ts +100 -0
- package/dist/db/models/subtasks.js +241 -0
- package/dist/db/models/tasks.d.ts +118 -0
- package/dist/db/models/tasks.js +274 -0
- package/dist/db/schema.d.ts +468 -0
- package/dist/db/schema.js +88 -0
- package/dist/env.d.ts +53 -0
- package/dist/env.js +47 -0
- package/dist/host/agent.d.ts +305 -0
- package/dist/host/agent.js +400 -0
- package/dist/host/index.d.ts +20 -0
- package/dist/host/index.js +19 -0
- package/dist/host/plugin-host.d.ts +42 -0
- package/dist/host/plugin-host.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +17 -0
- package/dist/job/index.d.ts +21 -0
- package/dist/job/index.js +21 -0
- package/dist/job/lifecycle.d.ts +176 -0
- package/dist/job/lifecycle.js +230 -0
- package/dist/job/state.d.ts +92 -0
- package/dist/job/state.js +40 -0
- package/dist/platform.d.ts +138 -0
- package/dist/platform.js +140 -0
- package/dist/round/agent.d.ts +271 -0
- package/dist/round/agent.js +678 -0
- package/dist/round/index.d.ts +25 -0
- package/dist/round/index.js +23 -0
- package/dist/round/policy.d.ts +98 -0
- package/dist/round/policy.js +1 -0
- package/dist/round/subagent.d.ts +87 -0
- package/dist/round/subagent.js +108 -0
- package/dist/round/turn.d.ts +249 -0
- package/dist/round/turn.js +564 -0
- package/dist/round/workflow.d.ts +147 -0
- package/dist/round/workflow.js +387 -0
- package/dist/runtime/index.d.ts +127 -0
- package/dist/runtime/index.js +186 -0
- package/dist/runtime/tool-families.d.ts +32 -0
- package/dist/runtime/tool-families.js +61 -0
- package/dist/subagent/fingerprint.d.ts +37 -0
- package/dist/subagent/fingerprint.js +92 -0
- package/dist/subagent/index.d.ts +169 -0
- package/dist/subagent/index.js +330 -0
- package/dist/subagent/prompt.d.ts +32 -0
- package/dist/subagent/prompt.js +37 -0
- package/dist/subagent/run.d.ts +157 -0
- package/dist/subagent/run.js +540 -0
- package/dist/subagent/workspace.d.ts +85 -0
- package/dist/subagent/workspace.js +127 -0
- package/dist/subtasks/catalog.d.ts +40 -0
- package/dist/subtasks/catalog.js +36 -0
- package/dist/subtasks/decomposition.d.ts +85 -0
- package/dist/subtasks/decomposition.js +156 -0
- package/dist/subtasks/delegate.d.ts +120 -0
- package/dist/subtasks/delegate.js +131 -0
- package/dist/subtasks/index.d.ts +9 -0
- package/dist/subtasks/index.js +9 -0
- package/dist/subtasks/subtask-types.d.ts +91 -0
- package/dist/subtasks/subtask-types.js +103 -0
- package/dist/subtasks/types.d.ts +295 -0
- package/dist/subtasks/types.js +15 -0
- package/dist/testing/auth.d.ts +34 -0
- package/dist/testing/auth.js +35 -0
- package/dist/testing/do.d.ts +29 -0
- package/dist/testing/do.js +25 -0
- package/dist/testing/fake-session.d.ts +26 -0
- package/dist/testing/fake-session.js +37 -0
- package/dist/testing/fixtures.d.ts +64 -0
- package/dist/testing/fixtures.js +104 -0
- package/dist/testing/harness.d.ts +97 -0
- package/dist/testing/harness.js +138 -0
- package/dist/testing/index.d.ts +31 -0
- package/dist/testing/index.js +35 -0
- package/dist/testing/mock-model.d.ts +77 -0
- package/dist/testing/mock-model.js +136 -0
- package/dist/testing/node.d.ts +56 -0
- package/dist/testing/node.js +56 -0
- package/dist/testing/vcr-global-setup.d.ts +12 -0
- package/dist/testing/vcr-global-setup.js +15 -0
- package/dist/testing/vcr-shared.d.ts +38 -0
- package/dist/testing/vcr-shared.js +33 -0
- package/dist/testing/vcr-spec.d.ts +25 -0
- package/dist/testing/vcr-spec.js +124 -0
- package/dist/testing/vcr-store.d.ts +86 -0
- package/dist/testing/vcr-store.js +191 -0
- package/dist/testing/vcr.d.ts +117 -0
- package/dist/testing/vcr.js +275 -0
- package/dist/worker/define-agent.d.ts +123 -0
- package/dist/worker/define-agent.js +20 -0
- package/dist/worker/index.d.ts +218 -0
- package/dist/worker/index.js +369 -0
- package/eslint-rules/index.js +31 -0
- package/eslint-rules/no-deprecated-object-properties.js +81 -0
- package/package.json +178 -0
- 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>;
|