@loopingai/core 0.5.1 → 0.6.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/README.md +6 -4
- package/dist/a2a/notify.d.ts +4 -3
- package/dist/a2a/notify.js +4 -3
- package/dist/agent/anthropic/index.d.ts +15 -0
- package/dist/agent/anthropic/index.js +19 -0
- package/dist/agent/anthropic/language-model.d.ts +59 -0
- package/dist/agent/anthropic/language-model.js +442 -0
- package/dist/agent/anthropic/prompt.d.ts +84 -0
- package/dist/agent/anthropic/prompt.js +541 -0
- package/dist/agent/anthropic/runtime.d.ts +79 -0
- package/dist/agent/anthropic/runtime.js +130 -0
- package/dist/agent/control.js +10 -9
- package/dist/agent/errors.d.ts +85 -0
- package/dist/agent/errors.js +64 -0
- package/dist/agent/final-reply.d.ts +14 -13
- package/dist/agent/final-reply.js +28 -11
- package/dist/agent/history.d.ts +3 -3
- package/dist/agent/history.js +2 -2
- package/dist/agent/index.d.ts +4 -2
- package/dist/agent/index.js +4 -2
- package/dist/agent/inference.d.ts +58 -1
- package/dist/agent/inference.js +44 -0
- package/dist/agent/model.d.ts +42 -25
- package/dist/agent/model.js +1 -48
- package/dist/agent/session.d.ts +6 -7
- package/dist/agent/session.js +3 -3
- 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/config.d.ts +49 -15
- package/dist/config.js +30 -1
- package/dist/contract/plugin.d.ts +63 -3
- package/dist/contract/plugin.js +76 -0
- package/dist/contract/recipe.d.ts +16 -17
- package/dist/db/db.d.ts +0 -1
- package/dist/db/migrations/index.js +8 -1
- package/dist/db/models/subtasks.d.ts +24 -25
- package/dist/db/models/subtasks.js +33 -76
- package/dist/db/schema.d.ts +2 -21
- package/dist/db/schema.js +2 -4
- package/dist/host/agent.d.ts +58 -4
- package/dist/host/agent.js +63 -9
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/platform.d.ts +74 -11
- package/dist/platform.js +76 -13
- package/dist/round/agent.d.ts +36 -31
- package/dist/round/agent.js +61 -89
- package/dist/round/index.d.ts +3 -2
- package/dist/round/index.js +2 -2
- package/dist/round/policy.d.ts +2 -2
- package/dist/round/subagent.d.ts +19 -1
- package/dist/round/subagent.js +22 -5
- package/dist/round/turn.d.ts +32 -13
- package/dist/round/turn.js +83 -16
- package/dist/round/workflow.d.ts +23 -7
- package/dist/round/workflow.js +132 -65
- package/dist/runtime/index.d.ts +4 -2
- package/dist/runtime/index.js +6 -0
- package/dist/subagent/fingerprint.d.ts +2 -2
- package/dist/subagent/fingerprint.js +8 -17
- package/dist/subagent/index.d.ts +6 -4
- package/dist/subagent/index.js +8 -6
- package/dist/subagent/prompt.d.ts +4 -5
- package/dist/subagent/prompt.js +0 -8
- package/dist/subagent/run.d.ts +8 -1
- package/dist/subagent/run.js +59 -9
- package/dist/subtasks/catalog.d.ts +1 -1
- package/dist/subtasks/catalog.js +1 -1
- package/dist/subtasks/decomposition.d.ts +16 -20
- package/dist/subtasks/decomposition.js +27 -75
- package/dist/subtasks/delegate.d.ts +20 -1
- package/dist/subtasks/delegate.js +21 -16
- package/dist/subtasks/index.d.ts +1 -2
- package/dist/subtasks/index.js +1 -2
- package/dist/subtasks/subtask-types.d.ts +0 -8
- package/dist/subtasks/subtask-types.js +0 -7
- package/dist/subtasks/types.d.ts +45 -70
- package/dist/testing/mock-model.d.ts +35 -0
- package/dist/testing/mock-model.js +75 -0
- package/dist/testing/vcr-global-setup.d.ts +1 -3
- package/dist/testing/vcr-global-setup.js +1 -3
- package/dist/worker/index.d.ts +5 -12
- package/dist/worker/index.js +5 -12
- package/package.json +19 -1
- package/dist/subtasks/scheduler.d.ts +0 -48
- package/dist/subtasks/scheduler.js +0 -47
package/dist/round/workflow.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { WorkflowStep } from "cloudflare:workers";
|
|
2
2
|
import type { CoreConfig } from "../config.js";
|
|
3
3
|
import type { GatewayIdentity } from "../a2a/verify.js";
|
|
4
|
+
import type { RoundFailureKind } from "../agent/inference.js";
|
|
4
5
|
import type { RoundAgentBase } from "./agent.js";
|
|
5
6
|
import type { RoundPolicy } from "./policy.js";
|
|
6
7
|
/**
|
|
@@ -15,10 +16,10 @@ import type { RoundPolicy } from "./policy.js";
|
|
|
15
16
|
* 1. **Round** — one main-agent inference that either answers the user (the Task
|
|
16
17
|
* is done) or delegates durable Subtasks plus the acknowledgment the user sees
|
|
17
18
|
* while they run.
|
|
18
|
-
* 2. **Execute** — a delegating round's
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* 2. **Execute** — a delegating round's Subtasks all run at once, each in an
|
|
20
|
+
* isolated managed subagent. Then the loop returns to 1, where the model sees
|
|
21
|
+
* the results and decides again — answer, or delegate once more. Sequencing
|
|
22
|
+
* lives here, in the loop, not inside a round.
|
|
22
23
|
* 3. **Deliver** — persist the terminal Task, then POST a signed callback.
|
|
23
24
|
*
|
|
24
25
|
* The main agent is never forced either way. A round that has run out of budget —
|
|
@@ -78,6 +79,22 @@ export interface HandleTaskDeps {
|
|
|
78
79
|
config: CoreConfig;
|
|
79
80
|
/** The user-facing copy. Only `copy.taskFailed` is read out here. */
|
|
80
81
|
policy: RoundPolicy;
|
|
82
|
+
/**
|
|
83
|
+
* Terminal copy for a round that produced no answer, by {@link
|
|
84
|
+
* RoundFailureKind} — an expired credential, models that could not do it, and
|
|
85
|
+
* whatever that union grows to cover.
|
|
86
|
+
*
|
|
87
|
+
* A hook rather than more `RoundPolicy` copy, because the useful words are
|
|
88
|
+
* deployment-specific ("run `claude setup-token`, then
|
|
89
|
+
* `wrangler secret put …`") and most agents cannot hit these conditions at
|
|
90
|
+
* all. Returning `undefined` — or omitting this — falls back to
|
|
91
|
+
* `policy.copy.taskFailed`, so an agent that does not care changes nothing,
|
|
92
|
+
* and one that only cares about *some* kinds answers for those alone.
|
|
93
|
+
*
|
|
94
|
+
* Core still owns the delivery: this supplies only the message, so the
|
|
95
|
+
* guarded write that doubles as the cancellation check stays in one place.
|
|
96
|
+
*/
|
|
97
|
+
failureCopy?: (kind: RoundFailureKind, detail: string) => string | undefined;
|
|
81
98
|
/**
|
|
82
99
|
* The deployment's Ed25519 private JWK, for the terminal callback. Passed
|
|
83
100
|
* rather than read off a module-scope `env` so this stays a pure function of
|
|
@@ -106,9 +123,8 @@ type AgentStub = DurableObjectStub<RoundAgentBase>;
|
|
|
106
123
|
* references to them.
|
|
107
124
|
*
|
|
108
125
|
* **Step names are durable cache keys.** Everything inside the round loop carries
|
|
109
|
-
* its round for that reason: `turn:<round>`, `deadline:<round>`,
|
|
110
|
-
* `
|
|
111
|
-
* its effect on replay.
|
|
126
|
+
* its round for that reason: `turn:<round>`, `deadline:<round>`, `scan:<round>`,
|
|
127
|
+
* `cancel:<round>`. Renaming one silently re-runs its effect on replay.
|
|
112
128
|
*/
|
|
113
129
|
export declare function runHandleTask(p: HandleTaskParams, step: WorkflowStep, deps: HandleTaskDeps): Promise<void>;
|
|
114
130
|
export {};
|
package/dist/round/workflow.js
CHANGED
|
@@ -1,7 +1,65 @@
|
|
|
1
|
-
import { MAX_CHUNKS_PER_BRANCH } from "../platform.js";
|
|
1
|
+
import { MAX_CHUNKS_PER_BRANCH, STEP_TIMEOUT_MS } from "../platform.js";
|
|
2
2
|
import { buildCompletedTask, buildFailedTask } from "../a2a/notify.js";
|
|
3
3
|
import { createPushChannel } from "../a2a/push.js";
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* What the long steps configure instead of inheriting.
|
|
6
|
+
*
|
|
7
|
+
* Both halves of this were previously left at Workflows' defaults, and both
|
|
8
|
+
* defaults were wrong for this workload.
|
|
9
|
+
*
|
|
10
|
+
* **`timeout`.** The default is ten minutes. Nothing here passed a config, so that
|
|
11
|
+
* default silently became the ceiling {@link CHUNK_SOFT_MS} was sized against —
|
|
12
|
+
* see the note on `STEP_TIMEOUT_MS` in `platform.ts` for what that cost. A step
|
|
13
|
+
* here holds a model call and its provider retries, or a container command running
|
|
14
|
+
* a project's test suite; neither fits in ten minutes reliably and neither uses
|
|
15
|
+
* meaningful CPU while it waits.
|
|
16
|
+
*
|
|
17
|
+
* **`retries`.** The default is five attempts with exponential backoff from ten
|
|
18
|
+
* seconds. The failure documented on {@link ResolveAgent} is what that produces
|
|
19
|
+
* when the fault is not transient: five retries against a severed stub, each
|
|
20
|
+
* failing in under 10ms, spread across 160 seconds of backoff that bought nothing.
|
|
21
|
+
* Three attempts still cover a genuinely transient fault — the model call has its
|
|
22
|
+
* own provider-level retry underneath this — and a flat five-second delay stops a
|
|
23
|
+
* fast, permanent failure from being paid for at exponential rates.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately applied only to the chunk steps — see {@link turnStep} for why a
|
|
26
|
+
* round does not share it. The short bookkeeping steps (`working`, `deadline:`,
|
|
27
|
+
* `scan:`, `complete`, `notify`) are sub-second projections where the defaults
|
|
28
|
+
* are fine and a shared config would only hide that.
|
|
29
|
+
*/
|
|
30
|
+
const CHUNK_STEP = {
|
|
31
|
+
timeout: STEP_TIMEOUT_MS,
|
|
32
|
+
retries: { limit: 3, delay: 5_000, backoff: "constant" }
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The same retries, and a timeout a **round** can actually be measured against.
|
|
36
|
+
*
|
|
37
|
+
* A chunk and a round are bounded by different things, and sharing one constant
|
|
38
|
+
* hid that. A chunk has {@link CHUNK_SOFT_MS}: it checkpoints and hands back a
|
|
39
|
+
* fresh step, so `STEP_TIMEOUT_MS` is a ceiling it is sized to stay under. A
|
|
40
|
+
* round has no soft deadline at all — `runTurn` runs up to
|
|
41
|
+
* `mainAgentLimits.maxTurns` sequential model-plus-tool steps in one
|
|
42
|
+
* `generateText`, and its only bound is that step count. Twenty turns whose
|
|
43
|
+
* tools each take the {@link file://../platform.ts MAX_TOOL_CALL_MS} they are
|
|
44
|
+
* permitted is hours, not half an hour, so a perfectly legal round could be
|
|
45
|
+
* killed and replayed whole.
|
|
46
|
+
*
|
|
47
|
+
* So the ceiling comes from the agent's own patience: a round cannot usefully
|
|
48
|
+
* outlive the wall clock its Task is allowed, because the `deadline:` step fails
|
|
49
|
+
* the Task at that point anyway. Floored at `STEP_TIMEOUT_MS` so a deliberately
|
|
50
|
+
* tight `maxWallMs` cannot produce a step timeout shorter than the single tool
|
|
51
|
+
* call core tells hosts they may install.
|
|
52
|
+
*
|
|
53
|
+
* This remains a backstop against a hang, not a budget. What actually bounds
|
|
54
|
+
* what a round *spends* is `TurnBudget`, and what bounds the Task is
|
|
55
|
+
* `mainAgentLimits` — both of which are checked whatever this says.
|
|
56
|
+
*/
|
|
57
|
+
function turnStep(config) {
|
|
58
|
+
return {
|
|
59
|
+
...CHUNK_STEP,
|
|
60
|
+
timeout: Math.max(config.mainAgentLimits.maxWallMs, STEP_TIMEOUT_MS)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
5
63
|
/**
|
|
6
64
|
* The orchestration itself, split from the `WorkflowEntrypoint` wiring so it can
|
|
7
65
|
* be driven with a fake `step` in tests (workerd forbids constructing a
|
|
@@ -14,9 +72,8 @@ import { selectWave } from "../subtasks/scheduler.js";
|
|
|
14
72
|
* references to them.
|
|
15
73
|
*
|
|
16
74
|
* **Step names are durable cache keys.** Everything inside the round loop carries
|
|
17
|
-
* its round for that reason: `turn:<round>`, `deadline:<round>`,
|
|
18
|
-
* `
|
|
19
|
-
* its effect on replay.
|
|
75
|
+
* its round for that reason: `turn:<round>`, `deadline:<round>`, `scan:<round>`,
|
|
76
|
+
* `cancel:<round>`. Renaming one silently re-runs its effect on replay.
|
|
20
77
|
*/
|
|
21
78
|
export async function runHandleTask(p, step, deps) {
|
|
22
79
|
const limits = deps.config.mainAgentLimits;
|
|
@@ -80,7 +137,7 @@ export async function runHandleTask(p, step, deps) {
|
|
|
80
137
|
// durable work to fall back on) and routes to failed delivery; a transient
|
|
81
138
|
// fault throws and the step retries, recovering from the durable rows with no
|
|
82
139
|
// second inference.
|
|
83
|
-
const turn = await step.do(`turn:${round}`, async () => {
|
|
140
|
+
const turn = await step.do(`turn:${round}`, turnStep(deps.config), async () => {
|
|
84
141
|
// Projected to a plain object: an RPC return carries a `Disposable` brand a
|
|
85
142
|
// step result cannot serialize. Every branch must carry `turns` — a field
|
|
86
143
|
// this projection drops is a field the budget never sees.
|
|
@@ -102,6 +159,7 @@ export async function runHandleTask(p, step, deps) {
|
|
|
102
159
|
if (result.status === "failed")
|
|
103
160
|
return {
|
|
104
161
|
status: result.status,
|
|
162
|
+
kind: result.kind,
|
|
105
163
|
error: result.error,
|
|
106
164
|
turns: result.turns
|
|
107
165
|
};
|
|
@@ -110,27 +168,33 @@ export async function runHandleTask(p, step, deps) {
|
|
|
110
168
|
turnsUsed += turn.turns;
|
|
111
169
|
if (turn.status === "canceled")
|
|
112
170
|
return;
|
|
171
|
+
// The round produced no answer. `kind` is the whole difference between the
|
|
172
|
+
// two ways that happens — models that could not do it, versus a fault that
|
|
173
|
+
// stopped the round on its first attempt and that only a human can clear —
|
|
174
|
+
// and it exists to be turned into words the reader can act on. Same
|
|
175
|
+
// delivery either way; the diagnostic is logged, never shown.
|
|
113
176
|
if (turn.status === "failed") {
|
|
114
177
|
console.error("[handle-task] round failed", {
|
|
115
178
|
taskId: p.taskId,
|
|
116
179
|
round,
|
|
180
|
+
kind: turn.kind,
|
|
117
181
|
error: turn.error
|
|
118
182
|
});
|
|
119
|
-
await deliver(p, step, agent, null, deps
|
|
183
|
+
await deliver(p, step, agent, null, deps, {
|
|
184
|
+
kind: turn.kind,
|
|
185
|
+
detail: turn.error
|
|
186
|
+
});
|
|
120
187
|
return;
|
|
121
188
|
}
|
|
122
189
|
if (turn.status === "replied") {
|
|
123
190
|
await deliver(p, step, agent, turn.reply, deps);
|
|
124
191
|
return;
|
|
125
192
|
}
|
|
126
|
-
// Delegated: run this round's
|
|
127
|
-
|
|
193
|
+
// Delegated: run this round's Subtasks, then loop and let the model decide
|
|
194
|
+
// again.
|
|
195
|
+
const executed = await executeSubtasks(p, step, agent, round, push);
|
|
128
196
|
if (executed === "canceled")
|
|
129
197
|
return;
|
|
130
|
-
if (executed === "stuck") {
|
|
131
|
-
await deliver(p, step, agent, null, deps);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
198
|
}
|
|
135
199
|
// Unreachable: a `final` round is handed only `final_reply`, so it either
|
|
136
200
|
// answers or fails, and both return above. Reaching here means a round
|
|
@@ -141,58 +205,49 @@ export async function runHandleTask(p, step, deps) {
|
|
|
141
205
|
await deliver(p, step, agent, null, deps);
|
|
142
206
|
}
|
|
143
207
|
/**
|
|
144
|
-
*
|
|
208
|
+
* Run every Subtask one round delegated, concurrently, to termination.
|
|
145
209
|
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
210
|
+
* **One pass is the whole thing.** A round's Subtasks are independent of one
|
|
211
|
+
* another, so they are all runnable the moment they exist, and `runBranch` is
|
|
212
|
+
* contractually obliged to leave its row terminal — it resolves a deterministic
|
|
213
|
+
* failure itself and has a `fail:<id>` backstop once the retries are gone. So
|
|
214
|
+
* there is nothing left to re-scan afterwards, and no way for this to make no
|
|
215
|
+
* progress. Sequencing between units of work is the round loop's job.
|
|
151
216
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
217
|
+
* Both step names carry the round, because step names are durable cache keys: two
|
|
218
|
+
* rounds of the same Task reusing `scan` would replay the first round's cached
|
|
219
|
+
* answer into the second.
|
|
155
220
|
*/
|
|
156
|
-
async function
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
221
|
+
async function executeSubtasks(p, step, agent, round, push) {
|
|
222
|
+
// One durable step: `scanSubtasks` reports cancellation and returns the ids
|
|
223
|
+
// still owing an outcome — one round trip, one consistent answer. It writes
|
|
224
|
+
// nothing, so a replay that re-runs it costs only the read.
|
|
225
|
+
const scan = await step.do(`scan:${round}`, async () => {
|
|
226
|
+
const result = await agent().scanSubtasks(p.taskId, round);
|
|
227
|
+
return result.canceled
|
|
228
|
+
? { canceled: true, ids: [] }
|
|
229
|
+
: { canceled: false, ids: result.ids };
|
|
230
|
+
});
|
|
231
|
+
if (scan.canceled) {
|
|
232
|
+
await step.do(`cancel:${round}`, async () => {
|
|
233
|
+
await agent().cancelPendingSubtasks(p.taskId);
|
|
166
234
|
});
|
|
167
|
-
|
|
168
|
-
await step.do(`cancel:${round}:${wave}`, async () => {
|
|
169
|
-
await agent().cancelPendingSubtasks(p.taskId);
|
|
170
|
-
});
|
|
171
|
-
return "canceled";
|
|
172
|
-
}
|
|
173
|
-
const decision = selectWave(scan.nodes);
|
|
174
|
-
if (decision.kind === "done")
|
|
175
|
-
return "done";
|
|
176
|
-
if (decision.kind === "stuck") {
|
|
177
|
-
console.error("[handle-task] subtask DAG made no progress", {
|
|
178
|
-
taskId: p.taskId,
|
|
179
|
-
round,
|
|
180
|
-
wave,
|
|
181
|
-
active: decision.active
|
|
182
|
-
});
|
|
183
|
-
return "stuck";
|
|
184
|
-
}
|
|
185
|
-
// Every dependency-ready node runs concurrently — the per-round Subtask
|
|
186
|
-
// maximum is the only fan-out bound. `runBranch` never rejects, so a single
|
|
187
|
-
// branch cannot fast-fail `Promise.all` and strand its siblings' durable
|
|
188
|
-
// results.
|
|
189
|
-
await Promise.all(decision.ids.map((id) => runBranch(p, step, agent, id, push)));
|
|
235
|
+
return "canceled";
|
|
190
236
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
237
|
+
// Every Subtask runs concurrently — the per-round Subtask maximum is the only
|
|
238
|
+
// fan-out bound. `runBranch` never rejects, so a single branch cannot fast-fail
|
|
239
|
+
// `Promise.all` and strand its siblings' durable results.
|
|
240
|
+
//
|
|
241
|
+
// A cancellation arriving mid-pass is still honored, just not from here:
|
|
242
|
+
// `onTaskCanceled` aborts the live children *and* transitions every row still
|
|
243
|
+
// `pending` in the same sweep, and `executeSubtaskChunk` re-checks before
|
|
244
|
+
// publishing. That transition is what lets this pass end without a second
|
|
245
|
+
// scan. Without it, a branch whose RPC had not yet claimed its row when the
|
|
246
|
+
// cancellation landed would return terminal while leaving the row `pending`,
|
|
247
|
+
// and — since the next round's turn reports `canceled` and the workflow exits
|
|
248
|
+
// — nothing would resolve it before the 30-day cleanup.
|
|
249
|
+
await Promise.all(scan.ids.map((id) => runBranch(p, step, agent, id, push)));
|
|
250
|
+
return "done";
|
|
196
251
|
}
|
|
197
252
|
/**
|
|
198
253
|
* Run one Subtask to termination as a sequence of durable **chunk** steps, and
|
|
@@ -202,7 +257,9 @@ async function executeDag(p, step, agent, round, push, deps) {
|
|
|
202
257
|
* `done` on chunk 0 (step `execute:<id>`); a long recipe yields `done: false` and
|
|
203
258
|
* the loop runs the next chunk (`execute:<id>:chunk:<n>`) until it terminates.
|
|
204
259
|
* Each chunk is its own retryable step, and the child resumes from its
|
|
205
|
-
* checkpoint — so no step approaches the
|
|
260
|
+
* checkpoint — so no step approaches the {@link CHUNK_STEP} timeout. `CHUNK_SOFT_MS`
|
|
261
|
+
* is what holds that true, and is sized against it rather than the other way
|
|
262
|
+
* round; a boundary here is not free, so it wants to be rare, not frequent.
|
|
206
263
|
*
|
|
207
264
|
* It resolves a deterministic branch failure into a `failed` row itself and
|
|
208
265
|
* throws only on a transient fault (retry me) or a lifecycle bug. So a throw that
|
|
@@ -226,7 +283,7 @@ async function runBranch(p, step, agent, id, push) {
|
|
|
226
283
|
// Chunk 0 keeps the plain `execute:<id>` step name so single-chunk branches
|
|
227
284
|
// replay identically; later chunks append `:chunk:<n>`.
|
|
228
285
|
const stepName = chunk === 0 ? `execute:${id}` : `execute:${id}:chunk:${chunk}`;
|
|
229
|
-
const done = await step.do(stepName, async () => {
|
|
286
|
+
const done = await step.do(stepName, CHUNK_STEP, async () => {
|
|
230
287
|
// The DO posts any progress itself; the step returns only the verdict.
|
|
231
288
|
const outcome = await agent().executeSubtaskChunk(id, chunk, push);
|
|
232
289
|
return outcome.done;
|
|
@@ -258,7 +315,13 @@ async function runBranch(p, step, agent, id, push) {
|
|
|
258
315
|
/**
|
|
259
316
|
* Persist the terminal Task, then notify the gateway. A null `reply` delivers a
|
|
260
317
|
* `failed` Task with the policy's user-safe text; the diagnostic is already
|
|
261
|
-
* logged.
|
|
318
|
+
* logged. Given a `failure`, the host's {@link HandleTaskDeps.failureCopy} may
|
|
319
|
+
* replace that text — same delivery, different words.
|
|
320
|
+
*
|
|
321
|
+
* `failure` is optional because only a round's own inference carries a kind. The
|
|
322
|
+
* other path here — a budget that ran out mid-delegation — is not a model failure
|
|
323
|
+
* and is deliberately not given a kind of its own until something needs to tell
|
|
324
|
+
* it apart.
|
|
262
325
|
*
|
|
263
326
|
* The Task is built **inside** the step and returned, so `notify` posts exactly
|
|
264
327
|
* what was persisted: building it in the body would re-stamp `new Date()` on
|
|
@@ -271,11 +334,15 @@ async function runBranch(p, step, agent, id, push) {
|
|
|
271
334
|
* `notify` — in which a `tasks/cancel` lands and the gateway still receives a
|
|
272
335
|
* `completed` callback. Keying the notify on "did the write apply" closes it.
|
|
273
336
|
*/
|
|
274
|
-
async function deliver(p, step, agent, reply, deps) {
|
|
337
|
+
async function deliver(p, step, agent, reply, deps, failure) {
|
|
338
|
+
// Resolved outside the step body so a replay cannot take a different branch
|
|
339
|
+
// than the write it is replaying.
|
|
340
|
+
const failedText = (failure && deps.failureCopy?.(failure.kind, failure.detail)) ||
|
|
341
|
+
deps.policy.copy.taskFailed;
|
|
275
342
|
const task = await step.do("complete", async () => {
|
|
276
343
|
const terminal = reply !== null
|
|
277
344
|
? buildCompletedTask(p.taskId, p.contextId, reply)
|
|
278
|
-
: buildFailedTask(p.taskId, p.contextId,
|
|
345
|
+
: buildFailedTask(p.taskId, p.contextId, failedText);
|
|
279
346
|
return (await agent().saveTask(terminal)) ? terminal : null;
|
|
280
347
|
});
|
|
281
348
|
if (!task)
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -59,8 +59,10 @@ export interface AgentRuntime {
|
|
|
59
59
|
/** Tools the installed plugins offer the *main* agent, merged. */
|
|
60
60
|
mainAgentTools(ctx: MainAgentToolContext): Promise<ToolSet>;
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
62
|
+
* The capability blocks for the main agent's soul, in plugin declaration
|
|
63
|
+
* order: each plugin's own {@link AgentPlugin.capability} and the one on its
|
|
64
|
+
* {@link AgentPlugin.subtaskType}, if it declares either. Returns `""` when
|
|
65
|
+
* none does, so a call site can append unconditionally.
|
|
64
66
|
*/
|
|
65
67
|
renderCapabilities(): string;
|
|
66
68
|
/**
|
package/dist/runtime/index.js
CHANGED
|
@@ -109,8 +109,14 @@ export function createAgentRuntime(options) {
|
|
|
109
109
|
renderCapabilities() {
|
|
110
110
|
const blocks = [];
|
|
111
111
|
for (const plugin of plugins) {
|
|
112
|
+
// Both blocks a plugin may declare, emitted adjacently: a plugin that
|
|
113
|
+
// declares a subtask type puts its capability on the *type*, and one
|
|
114
|
+
// that only offers main-agent tools puts it on the plugin. Declaring
|
|
115
|
+
// both is legal and means the model reads both.
|
|
112
116
|
if (plugin.capability)
|
|
113
117
|
blocks.push(plugin.capability);
|
|
118
|
+
if (plugin.subtaskType?.capability)
|
|
119
|
+
blocks.push(plugin.subtaskType.capability);
|
|
114
120
|
}
|
|
115
121
|
return blocks.join("\n\n");
|
|
116
122
|
},
|
|
@@ -11,8 +11,8 @@ export declare const FINGERPRINT_VERSION = 1;
|
|
|
11
11
|
* Canonical JSON of the fields that define an execution's identity, rebuilt as
|
|
12
12
|
* literals in fixed key order so `JSON.stringify` is deterministic (object
|
|
13
13
|
* insertion order). Array order is semantic and preserved: the parent builds
|
|
14
|
-
* references
|
|
15
|
-
*
|
|
14
|
+
* references from ordinal-ordered rows, so a retry of the same execution is
|
|
15
|
+
* byte-identical.
|
|
16
16
|
*
|
|
17
17
|
* **Limits are canonicalized as *declared*, not as merged.** The predecessor
|
|
18
18
|
* merged them against the house baseline first, on the reasoning that `{}` and
|
|
@@ -10,8 +10,8 @@ export const FINGERPRINT_VERSION = 1;
|
|
|
10
10
|
* Canonical JSON of the fields that define an execution's identity, rebuilt as
|
|
11
11
|
* literals in fixed key order so `JSON.stringify` is deterministic (object
|
|
12
12
|
* insertion order). Array order is semantic and preserved: the parent builds
|
|
13
|
-
* references
|
|
14
|
-
*
|
|
13
|
+
* references from ordinal-ordered rows, so a retry of the same execution is
|
|
14
|
+
* byte-identical.
|
|
15
15
|
*
|
|
16
16
|
* **Limits are canonicalized as *declared*, not as merged.** The predecessor
|
|
17
17
|
* merged them against the house baseline first, on the reasoning that `{}` and
|
|
@@ -36,13 +36,12 @@ export function canonicalRequest(request) {
|
|
|
36
36
|
recipe: {
|
|
37
37
|
key: request.recipe.key,
|
|
38
38
|
version: request.recipe.version,
|
|
39
|
-
// No model ids.
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// from conversation state, which is model-independent.
|
|
39
|
+
// No model ids. Their absence is the same trade this file already makes
|
|
40
|
+
// for `limits`: the host's own configuration stays out of the fingerprint,
|
|
41
|
+
// so changing it does not invalidate every checkpoint at once and restart
|
|
42
|
+
// every in-flight run from turn zero with its budget already spent. A run
|
|
43
|
+
// that resumes on a newly configured model resumes from conversation
|
|
44
|
+
// state, which is model-independent.
|
|
46
45
|
soul: request.recipe.soul,
|
|
47
46
|
toolFamilies: request.recipe.toolFamilies,
|
|
48
47
|
enabled: request.recipe.enabled,
|
|
@@ -55,14 +54,6 @@ export function canonicalRequest(request) {
|
|
|
55
54
|
role: ref.role,
|
|
56
55
|
text: ref.text
|
|
57
56
|
})),
|
|
58
|
-
dependencyResults: request.dependencyResults.map((dep) => ({
|
|
59
|
-
subtaskId: dep.subtaskId,
|
|
60
|
-
type: dep.type,
|
|
61
|
-
resultParts: dep.resultParts.map((part) => ({
|
|
62
|
-
kind: part.kind,
|
|
63
|
-
text: part.text
|
|
64
|
-
}))
|
|
65
|
-
})),
|
|
66
57
|
// Params ARE identity: the same prompt against a different external resource
|
|
67
58
|
// is different work, and must not replay a cached result. Key order is fixed
|
|
68
59
|
// by sorting, so an equivalent params object always canonicalizes identically.
|
package/dist/subagent/index.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface SubagentRuntime {
|
|
|
25
25
|
toolOutputWindow: number;
|
|
26
26
|
/** `CoreConfig.model.maxOutputTokens`. */
|
|
27
27
|
maxOutputTokens: number;
|
|
28
|
+
/** `CoreConfig.model.maxRetries`. */
|
|
29
|
+
maxRetries: number;
|
|
28
30
|
/**
|
|
29
31
|
* Build the durable file store over this facet's own SQLite.
|
|
30
32
|
*
|
|
@@ -43,14 +45,14 @@ export interface SubagentRuntime {
|
|
|
43
45
|
* execution — and a Workflow retry after the parent's cleanup will succeed.
|
|
44
46
|
*/
|
|
45
47
|
export declare const FINGERPRINT_MISMATCH = "recipe-subagent: request fingerprint mismatch";
|
|
46
|
-
/** Deterministic managed-child name for one Subtask execution
|
|
48
|
+
/** Deterministic managed-child name for one Subtask execution. */
|
|
47
49
|
export declare function subagentName(taskId: string, subtaskId: SubtaskId): string;
|
|
48
50
|
/**
|
|
49
51
|
* `RecipeSubagent` — the isolated, stateless managed child that executes one
|
|
50
52
|
* Subtask under a resolved Recipe. Created as an Agents SDK sub-agent (facet)
|
|
51
|
-
* beneath the
|
|
52
|
-
*
|
|
53
|
-
*
|
|
53
|
+
* beneath the calling agent, so it needs no wrangler Durable Object binding and
|
|
54
|
+
* no `new_sqlite_classes` entry; it must only be exported from the consuming
|
|
55
|
+
* Worker's entry so `ctx.exports` can resolve it by class name.
|
|
54
56
|
*
|
|
55
57
|
* It never constructs a Session, never reads parent history beyond the
|
|
56
58
|
* references supplied on its request, never reaches durable memory, and never
|
package/dist/subagent/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { runResumableChunk } from "./run.js";
|
|
|
16
16
|
* execution — and a Workflow retry after the parent's cleanup will succeed.
|
|
17
17
|
*/
|
|
18
18
|
export const FINGERPRINT_MISMATCH = "recipe-subagent: request fingerprint mismatch";
|
|
19
|
-
/** Deterministic managed-child name for one Subtask execution
|
|
19
|
+
/** Deterministic managed-child name for one Subtask execution. */
|
|
20
20
|
export function subagentName(taskId, subtaskId) {
|
|
21
21
|
return `subtask:${taskId}:${subtaskId}`;
|
|
22
22
|
}
|
|
@@ -38,9 +38,9 @@ const cachedResultSchema = z.discriminatedUnion("status", [
|
|
|
38
38
|
/**
|
|
39
39
|
* `RecipeSubagent` — the isolated, stateless managed child that executes one
|
|
40
40
|
* Subtask under a resolved Recipe. Created as an Agents SDK sub-agent (facet)
|
|
41
|
-
* beneath the
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* beneath the calling agent, so it needs no wrangler Durable Object binding and
|
|
42
|
+
* no `new_sqlite_classes` entry; it must only be exported from the consuming
|
|
43
|
+
* Worker's entry so `ctx.exports` can resolve it by class name.
|
|
44
44
|
*
|
|
45
45
|
* It never constructs a Session, never reads parent history beyond the
|
|
46
46
|
* references supplied on its request, never reaches durable memory, and never
|
|
@@ -158,8 +158,9 @@ export class RecipeSubagentBase extends Agent {
|
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
// Re-check the type's param contract, the same defensive posture as
|
|
161
|
-
// `validateRecipe`: a
|
|
162
|
-
// here costs no model call and gives the parent a real
|
|
161
|
+
// `validateRecipe`: a subtask missing a param its type requires cannot
|
|
162
|
+
// succeed, and failing here costs no model call and gives the parent a real
|
|
163
|
+
// diagnostic.
|
|
163
164
|
try {
|
|
164
165
|
rt.types.validateParams(request.type, request.params);
|
|
165
166
|
}
|
|
@@ -213,6 +214,7 @@ export class RecipeSubagentBase extends Agent {
|
|
|
213
214
|
toolOutputWindow: rt.toolOutputWindow,
|
|
214
215
|
reportMetrics: recipe.reportMetrics,
|
|
215
216
|
maxOutputTokens: rt.maxOutputTokens,
|
|
217
|
+
maxRetries: rt.maxRetries,
|
|
216
218
|
now: () => Date.now(),
|
|
217
219
|
progress,
|
|
218
220
|
checkpoint: (s) => this.saveRunState(fingerprint, s),
|
|
@@ -2,11 +2,10 @@ import type { RecipeExecutionRequest } from "../subtasks/types.js";
|
|
|
2
2
|
import type { ValidatedRecipe } from "../contract/recipe.js";
|
|
3
3
|
/**
|
|
4
4
|
* Deterministic rendering of one subagent invocation. Pure — no model, no
|
|
5
|
-
* Session, no lookups: everything comes verbatim from the request. The
|
|
5
|
+
* Session, no lookups: everything comes verbatim from the request. The three
|
|
6
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, the
|
|
8
|
-
* conversation reference snapshots
|
|
9
|
-
* explicitly marked as generated and never presented as conversation evidence.
|
|
7
|
+
* them apart: the execution's budget, the main-agent instruction, and the
|
|
8
|
+
* verbatim conversation reference snapshots.
|
|
10
9
|
*/
|
|
11
10
|
/**
|
|
12
11
|
* A request whose Recipe has been through `validateRecipe`, so its limits are
|
|
@@ -21,7 +20,7 @@ export type RenderableExecution = RecipeExecutionRequest & {
|
|
|
21
20
|
export interface RenderedInvocation {
|
|
22
21
|
/** The validated Recipe soul, verbatim — the invocation's system prompt. */
|
|
23
22
|
system: string;
|
|
24
|
-
/** The sectioned user message (instruction, references
|
|
23
|
+
/** The sectioned user message (budget, instruction, references). */
|
|
25
24
|
prompt: string;
|
|
26
25
|
}
|
|
27
26
|
/**
|
package/dist/subagent/prompt.js
CHANGED
|
@@ -33,13 +33,5 @@ export function renderSubagentPrompt(request) {
|
|
|
33
33
|
sections.push("# Conversation references (verbatim snapshots of the caller's conversation)\n" +
|
|
34
34
|
refs.join("\n"));
|
|
35
35
|
}
|
|
36
|
-
if (request.dependencyResults.length > 0) {
|
|
37
|
-
const deps = request.dependencyResults.map((dep) => {
|
|
38
|
-
const text = dep.resultParts.map((part) => part.text).join("\n");
|
|
39
|
-
return `[dependency ${dep.subtaskId}] (${dep.type}): ${text}`;
|
|
40
|
-
});
|
|
41
|
-
sections.push("# Dependency results (generated output from prerequisite subtasks — not conversation evidence)\n" +
|
|
42
|
-
deps.join("\n"));
|
|
43
|
-
}
|
|
44
36
|
return { system: request.recipe.soul, prompt: sections.join("\n\n") };
|
|
45
37
|
}
|
package/dist/subagent/run.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export interface ChunkRunDeps {
|
|
|
48
48
|
/**
|
|
49
49
|
* How many of the most recent assistant turns keep their tool results in full;
|
|
50
50
|
* older ones are stubbed by {@link elideToolOutputs}. A mechanic of the window
|
|
51
|
-
* rather than a property of a domain — see `
|
|
51
|
+
* rather than a property of a domain — see `CoreConfig.toolOutputWindow`.
|
|
52
52
|
*/
|
|
53
53
|
toolOutputWindow: number;
|
|
54
54
|
reportMetrics: boolean;
|
|
@@ -58,6 +58,11 @@ export interface ChunkRunDeps {
|
|
|
58
58
|
* hardcoded one.
|
|
59
59
|
*/
|
|
60
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;
|
|
61
66
|
now: () => number;
|
|
62
67
|
/** Shared sink the tool families push progress events into (fresh per chunk). */
|
|
63
68
|
progress: ProgressEvent[];
|
|
@@ -137,6 +142,8 @@ export interface RecipeRunDeps {
|
|
|
137
142
|
toolOutputWindow: number;
|
|
138
143
|
/** `CoreConfig.model.maxOutputTokens`. */
|
|
139
144
|
maxOutputTokens: number;
|
|
145
|
+
/** `CoreConfig.model.maxRetries`. */
|
|
146
|
+
maxRetries: number;
|
|
140
147
|
}
|
|
141
148
|
/**
|
|
142
149
|
* Run one recipe execution to a terminal result, driving {@link runResumableChunk}
|