@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,140 @@
1
+ /**
2
+ * What the Cloudflare Workflows runtime imposes, and the two numbers derived from
3
+ * it. Nothing here is a budget or a preference — see {@link file://./config.ts}
4
+ * for those, and note that no Recipe can reach these. They change when the
5
+ * platform changes, and for no other reason.
6
+ *
7
+ * The distinction is worth keeping sharp, because collapsing it produces a
8
+ * specific bug: using a *turn count* to keep a step under the step timeout only
9
+ * works if you can predict how long a turn takes. You cannot. Time bounds time
10
+ * here; turns bound cost, over in `config.ts`.
11
+ */
12
+ /**
13
+ * The step timeout **we configure**, passed as `WorkflowStepConfig.timeout` on
14
+ * every step that can hold a model call or a container command.
15
+ *
16
+ * This is the one value in this file that is **not** a platform fact, and
17
+ * treating it as one is the mistake to avoid: ten minutes is Workflows'
18
+ * *default* step timeout, not its ceiling, and a step that never passes a config
19
+ * inherits it silently. Sizing {@link CHUNK_SOFT_MS} against that inherited
20
+ * default is what once ran a production task as ten four-minute slices, one of
21
+ * which blew the ten minutes anyway and was retried whole.
22
+ *
23
+ * The genuine platform facts are narrower and do not bind us: wall-clock time per
24
+ * step is effectively unlimited, and a step is bounded by **CPU** time. Measured
25
+ * over a 59-minute task, the chunk steps used ~100 ms of CPU against 3,146 s of
26
+ * wall — this ceiling is nowhere near the real one.
27
+ *
28
+ * It is still a ceiling worth having, because it is what turns a hung container
29
+ * into a retry rather than a task that never ends. {@link CHUNK_SOFT_MS} is sized
30
+ * against it, and that relationship is asserted in `platform.spec.ts`.
31
+ */
32
+ export const STEP_TIMEOUT_MS = 30 * 60_000;
33
+ /**
34
+ * Platform fact: a single Workflow instance may run 10,000 steps by default on the
35
+ * paid plan. Cloudflare will raise it to 25,000 on request — worth knowing, and
36
+ * worth not relying on: {@link MAX_CHUNKS_PER_BRANCH} is sized against the default
37
+ * so nothing here needs an account-level exception to be correct. See the
38
+ * worst-case product asserted in `platform.spec.ts`.
39
+ */
40
+ export const STEPS_PER_INSTANCE = 10_000;
41
+ /**
42
+ * How long one durable chunk may run before it checkpoints and yields a fresh
43
+ * step. Comfortably inside {@link STEP_TIMEOUT_MS} so a slow model turn in flight
44
+ * when the soft limit trips still has room to finish.
45
+ *
46
+ * This is the *only* thing keeping a step under the timeout. A subagent otherwise
47
+ * runs until its turn or wall-clock budget is spent, however many turns that takes
48
+ * — which is the point: the runner no longer guesses at turn duration.
49
+ *
50
+ * ## Why this is 15 minutes and not 4
51
+ *
52
+ * A chunk boundary is not free. It checkpoints, returns through two RPC hops,
53
+ * starts a fresh step, and re-hydrates the subagent — and for a coding agent it
54
+ * also means the container connection is re-established. Four minutes bought a
55
+ * boundary roughly every third tool call: a task that edited one README line spent
56
+ * 59 minutes across **ten** chunks, and the model was idle for most of each one,
57
+ * blocked on a single `sb_exec` running the project's test gate.
58
+ *
59
+ * ## Why it is not larger, which is the part that bit us
60
+ *
61
+ * This is a **soft** deadline, checked between turns (`stopWhen` in
62
+ * `subagent/run.ts`). A turn that starts one millisecond before it trips still runs
63
+ * to completion, so the real worst case is:
64
+ *
65
+ * chunk wall ≤ CHUNK_SOFT_MS + one whole turn
66
+ *
67
+ * and one turn is a model call plus a tool call. The old pair ignored that: four
68
+ * minutes soft under a ten-minute timeout looked like six minutes of headroom, but
69
+ * a single `sb_exec` may run for {@link MAX_TOOL_CALL_MS}, so a turn could add ten.
70
+ * That is not a hypothetical — it is the `WorkflowTimeoutError` that cost a
71
+ * production task ten minutes and a full chunk replay.
72
+ *
73
+ * So the headroom is sized against a whole turn, not against a guess:
74
+ * `STEP_TIMEOUT_MS - CHUNK_SOFT_MS` is 15 minutes, covering
75
+ * {@link MAX_TOOL_CALL_MS} of tool call plus five minutes for the model call and
76
+ * its provider retries. Asserted in `platform.spec.ts` — raise the step timeout
77
+ * before raising this.
78
+ */
79
+ export const CHUNK_SOFT_MS = 15 * 60_000;
80
+ /**
81
+ * The longest a **single tool call** may run, and a contract rather than a
82
+ * mechanism: core has no way to enforce it, because core installs no tools.
83
+ *
84
+ * It exists because {@link CHUNK_SOFT_MS} cannot be reasoned about without it. The
85
+ * soft deadline is checked between turns, so a host that lets one tool block for
86
+ * longer than the headroom under {@link STEP_TIMEOUT_MS} reintroduces exactly the
87
+ * step-timeout kill this pair is sized to prevent — and it reintroduces it
88
+ * invisibly, in a plugin, a long way from this file.
89
+ *
90
+ * A host installing a tool that can block (a shell, a container command, a fetch
91
+ * with no ceiling of its own) must bound it at or below this. See the `timeoutMs`
92
+ * passed to `@dynamicagents/plugins/computer` in starter.
93
+ */
94
+ export const MAX_TOOL_CALL_MS = 10 * 60_000;
95
+ /**
96
+ * Hard ceiling on durable chunk steps for one Subtask branch. A backstop, not a
97
+ * budget: the Workflow *fails* a branch that reaches it, so reaching it is a bug.
98
+ * It is held unreachable by two constraints, both asserted in
99
+ * `platform.spec.ts`:
100
+ *
101
+ * 1. It exceeds every Recipe's `maxTurns`. A chunk that yields always advanced at
102
+ * least one turn, so a run takes at most `maxTurns` chunks however short they
103
+ * are — and they do get short, because `CHUNK_SOFT_MS` and progress events both
104
+ * end one early. Counting turns is what makes the bound survive that; any
105
+ * estimate of turns-per-chunk would not, since neither of those two is
106
+ * predictable.
107
+ * 2. The worst-case step product stays under {@link STEPS_PER_INSTANCE}.
108
+ */
109
+ export const MAX_CHUNKS_PER_BRANCH = 40;
110
+ /**
111
+ * What a step holding a model call or a container command configures instead of
112
+ * inheriting Workflows' defaults. Both defaults were measured wrong for this
113
+ * workload.
114
+ *
115
+ * **`timeout`.** The default is ten minutes. A step here holds a model call and
116
+ * its provider retries, or a container command running a project's test suite;
117
+ * neither fits in ten minutes reliably, and neither uses meaningful CPU while it
118
+ * waits. Left inherited, that default silently became the ceiling
119
+ * {@link CHUNK_SOFT_MS} was sized against.
120
+ *
121
+ * **`retries`.** The default is five attempts with exponential backoff from ten
122
+ * seconds. Against a fault that is not transient — a severed Durable Object stub
123
+ * — that produced five failures in under 10ms each, spread across 160 seconds of
124
+ * backoff that bought nothing. Three attempts still cover a genuinely transient
125
+ * fault, since the model call has its own provider-level retry underneath this,
126
+ * and a flat five-second delay stops a fast permanent failure being paid for at
127
+ * exponential rates.
128
+ *
129
+ * Here rather than in `/round` because the agent that most needs it may not be a
130
+ * round agent: a single-inference agent runs one model call in one step and has
131
+ * the same two problems, and importing this from `/round` would put the whole
132
+ * delegation engine in its bundle.
133
+ *
134
+ * For the **short** bookkeeping steps — `working`, `complete`, `notify` and
135
+ * friends — the defaults are fine and a shared config would only hide that.
136
+ */
137
+ export const CHUNK_STEP = {
138
+ timeout: STEP_TIMEOUT_MS,
139
+ retries: { limit: 3, delay: 5_000, backoff: "constant" }
140
+ };
@@ -0,0 +1,271 @@
1
+ import type { AiEnv, A2ASecretsEnv } from "../env.js";
2
+ import type { GatekeeperIdentity } from "../a2a/verify.js";
3
+ import type { TurnPushContext } from "../a2a/push.js";
4
+ import type { Subtask, SubtaskChunkOutcome, SubtaskId, SubtaskScan, TurnTaskResult } from "../subtasks/types.js";
5
+ import { DynamicAgent } from "../host/agent.js";
6
+ import type { SubagentClass } from "./subagent.js";
7
+ import type { RoundPolicy } from "./policy.js";
8
+ import { type RoundMode } from "./turn.js";
9
+ /**
10
+ * A **delegating** agent: the round loop, the durable Subtasks it hands out, and
11
+ * the isolated subagent execution beneath them.
12
+ *
13
+ * Everything {@link DynamicAgent} gives every agent, plus the half that only a
14
+ * delegating one needs — and it is all mechanism. A Workflow drives it through
15
+ * native Cloudflare RPC (`runTaskTurn`, `scanSubtasks`, `executeSubtaskChunk`,
16
+ * …), never HTTP: the DO is a private implementation detail of the Worker.
17
+ *
18
+ * ## Why core owns this
19
+ *
20
+ * Every method below is either idempotent recovery, cancellation ordering, or
21
+ * child lifecycle, and each one has an ordering that is load-bearing and
22
+ * invisible: the acknowledgment is appended *before* the rows exist; a fresh
23
+ * execution deletes its stale child but an ambiguous retry must not; a successful
24
+ * chunk defers its child's deletion to a single post-delivery sweep. Get any of
25
+ * them backwards and nothing fails to compile, nothing fails a lint, and the
26
+ * damage shows up as a duplicated reply or a false-positive error weeks later.
27
+ *
28
+ * None of it varies between agents. What varies is the {@link RoundPolicy} — the
29
+ * words — and the plugins.
30
+ *
31
+ * ## What this class does not know
32
+ *
33
+ * Nothing here names a domain. `resolveRuntime`, `enrichResult` and `onAbort` are
34
+ * hooks on `AgentPlugin`, so a plugin that leases an external session or scores a
35
+ * result gets that without a branch anywhere in this file. That inversion is what
36
+ * lets one class body serve every delegating agent.
37
+ */
38
+ export declare abstract class RoundAgentBase<TEnv extends Cloudflare.Env & AiEnv & A2ASecretsEnv = Cloudflare.Env & AiEnv & A2ASecretsEnv> extends DynamicAgent<TEnv> {
39
+ private _instructions?;
40
+ /**
41
+ * The words: the round contract, the budget-spent note, and the strings a user
42
+ * reads. Core ships none of them — see {@link RoundPolicy}.
43
+ */
44
+ protected abstract roundPolicy(): RoundPolicy;
45
+ /**
46
+ * The facet class this agent's subtasks execute in.
47
+ *
48
+ * A seam because each agent's children must reach that agent's plugins, and a
49
+ * facet cannot be handed a runtime: `parentAgent()` is an RPC stub, and a
50
+ * `SubtaskRuntime` is mostly functions. So the class itself carries the
51
+ * binding — see {@link file://./subagent.ts RecipeSubagentHost}.
52
+ */
53
+ protected abstract subagentClass(): SubagentClass;
54
+ /** The prompt suffixes, built once from this agent's installed subtask types. */
55
+ private get instructions();
56
+ /**
57
+ * The main agent's **work tools** for this caller — the `execute`-bearing tools
58
+ * every round runs its loop over. The control tools that *end* a round are not
59
+ * here; `runTurn` adds those.
60
+ *
61
+ * The Session's own `set_context`/`load_context` come first, with the installed
62
+ * plugins' tools layered over them: a soul that instructs the model to record
63
+ * durable facts with `set_context` needs it actually on the call.
64
+ *
65
+ * Which plugin tools appear is the plugins' business, not this class's. A
66
+ * plugin may shape its surface from durable state — offering a search only once
67
+ * there is something to search, because a tool whose only possible answer is
68
+ * "nothing here yet" costs a call to discover that and costs every round the
69
+ * tokens to describe it.
70
+ */
71
+ private mainAgentTools;
72
+ /** Age out this agent's subtask rows alongside the task rows. */
73
+ protected cleanupAgentState(): void;
74
+ /**
75
+ * One main-agent round: answer the user, or delegate a durable set of Subtasks
76
+ * and return the acknowledgment the user sees while it runs.
77
+ *
78
+ * This is the RPC boundary, so it is where the round's cost becomes a field. The
79
+ * budget is created here, handed to {@link decideRound} to be spent, and read
80
+ * back exactly once — so no branch of the round has to remember to report a
81
+ * number, and none can report the wrong one.
82
+ */
83
+ runTaskTurn(input: {
84
+ taskId: string;
85
+ text: string;
86
+ identity: GatekeeperIdentity;
87
+ round: number;
88
+ mode: RoundMode;
89
+ /** What the Task has left. Bounds this round. */
90
+ turnsRemaining: number;
91
+ push?: TurnPushContext;
92
+ }): Promise<TurnTaskResult>;
93
+ /**
94
+ * The round itself, charging `budget` as it goes.
95
+ *
96
+ * Idempotent, and the recovery order is the contract:
97
+ *
98
+ * 1. A canceled Task stops here.
99
+ * 2. A durable **final reply** means some round already answered — return it
100
+ * without inference. Re-answering could produce different words for a reply
101
+ * the user may already have received.
102
+ * 3. Durable **rows for this round** mean this round already delegated —
103
+ * recover its acknowledgment from the Session, with no inference and no
104
+ * duplicate rows.
105
+ * 4. Otherwise, infer.
106
+ *
107
+ * Cancellation is re-read **after** inference too, not just before it: the model
108
+ * call is the widest window in the round, and neither the Subtask rows nor the
109
+ * callback may land for a Task the caller already gave up on. The reply is
110
+ * already in the Session by then (`runTurn` appends under deterministic ids
111
+ * before returning) — that is durable history, not output the user sees.
112
+ *
113
+ * Returns a typed `failed` result when both models produce unusable output and
114
+ * no durable work exists to fall back on (the Workflow routes it to failed
115
+ * delivery); throws only on a transient fault, for the step to retry.
116
+ */
117
+ private decideRound;
118
+ /**
119
+ * Every round's branches for a Task, in stable ordinal order — what a round
120
+ * needs to reunite each earlier `delegate` call with its result. Built inside
121
+ * the DO and consumed here, so the 1 MiB Workflow-step cap that keeps
122
+ * {@link SubtaskScan} down to ids does not apply.
123
+ */
124
+ private compositionBranches;
125
+ /** A Task's Subtasks, every round, in stable ordinal order. */
126
+ listSubtasks(taskId: string): Promise<Subtask[]>;
127
+ /**
128
+ * The Workflow's scan for **one round's** Subtasks: report a cancellation, or
129
+ * return the ids that still owe an outcome, in ordinal order.
130
+ *
131
+ * Scoped to the round because the Workflow drives one round at a time: an
132
+ * earlier round's rows are already terminal and would only widen a projection
133
+ * that has a size cap.
134
+ *
135
+ * `running` counts alongside `pending` on purpose. `executeSubtaskChunk`
136
+ * accepts a row that is either: the latter is its ambiguous-retry path, where a
137
+ * previous attempt crashed mid-execution and the managed child's fingerprint
138
+ * cache may still hold the terminal result that makes the retry free. So a row
139
+ * stranded `running` is re-runnable, and omitting it here would abandon it.
140
+ *
141
+ * Ordinal order comes from {@link listRound} and is not incidental: these ids
142
+ * become durable Workflow step names, so the traversal that produces them has
143
+ * to be deterministic.
144
+ *
145
+ * The cancellation verdict rides along rather than being probed separately, so
146
+ * the scan costs one round trip and cannot act on a stale answer.
147
+ */
148
+ scanSubtasks(taskId: string, round: number): Promise<SubtaskScan>;
149
+ /** Parent cancellation: cancel every still-pending Subtask. Returns the count. */
150
+ cancelPendingSubtasks(taskId: string): Promise<number>;
151
+ /**
152
+ * Force one branch terminal after the Workflow gave up on it: its
153
+ * `execute:<id>` step exhausted every retry, so `executeSubtaskChunk` will not
154
+ * be called again and no one else will resolve the row.
155
+ *
156
+ * The Workflow fails the *branch* rather than the Task so composition can
157
+ * disclose the gap while sibling branches keep their durable results. The
158
+ * managed child releases its external state and is then swept, both
159
+ * best-effort — nothing will read its cache now, but an abandoned run may still
160
+ * hold something outside this system, and dropping the child is not a reason to
161
+ * leak it. Idempotent: a no-op once the row is terminal.
162
+ */
163
+ failSubtask(id: SubtaskId, error: string): Promise<void>;
164
+ /**
165
+ * Run **one durable chunk** of a Subtask in an isolated, managed subagent,
166
+ * posting any progress the chunk emitted and durably recording a terminal
167
+ * outcome.
168
+ *
169
+ * The Workflow calls this repeatedly (chunk 0, 1, …) until it returns
170
+ * `done: true` — a single-chunk recipe finishes on chunk 0, a long one spans
171
+ * many. The row status distinguishes the cases with no chunk-number bookkeeping:
172
+ * chunk 0 claims `pending → running` (fresh — delete any stale child); every
173
+ * later chunk (and every retry) finds the row already `running` and leaves the
174
+ * child alone so its checkpointed run state resumes.
175
+ *
176
+ * The lifecycle rules that make it safe to re-run:
177
+ *
178
+ * - A terminal row short-circuits: the result is already durable.
179
+ * - A **fresh** execution deletes any stale child first.
180
+ * - An **ambiguous retry** (row already `running`) must *not* delete the child.
181
+ * - A **successful** chunk does *not* delete its child here — deletion is
182
+ * deferred to a single post-delivery {@link sweepTaskChildren}, so a facet is
183
+ * never aborted in the same tick its RPC returned (telemetry would mis-record
184
+ * that as a failure). The result is still copied into the parent before any
185
+ * delete; that now happens strictly later.
186
+ *
187
+ * Throws on a transient fault (the step retries and the child resumes from its
188
+ * checkpoint) and when the row is in a status this cannot accept — a subtask
189
+ * that is neither `pending` nor `running` nor already terminal. Both are bugs,
190
+ * not outcomes.
191
+ */
192
+ executeSubtaskChunk(id: SubtaskId, chunk: number, push?: TurnPushContext): Promise<SubtaskChunkOutcome>;
193
+ /**
194
+ * Delete every managed child this Task created — called **once**, from the
195
+ * Workflow's delivery step, after the Task is terminal.
196
+ *
197
+ * Per-Subtask deletion is deferred to here rather than run right after each
198
+ * successful chunk because `deleteSubAgent` aborts the facet: aborting a child
199
+ * in the same tick its `executeChunk` RPC returned records that
200
+ * already-successful invocation as `outcome:exception`, which is pure
201
+ * false-positive error noise (one per completed Subtask). By delivery every
202
+ * `execute` step has unwound, so these deletes hit **idle** facets and record
203
+ * nothing. Best-effort and idempotent — a name with no live facet is a silent
204
+ * no-op — so a Workflow replay of the sweep step is safe.
205
+ *
206
+ * Cancellation paths do their own child cleanup, so a canceled Task that never
207
+ * reaches delivery does not leak.
208
+ */
209
+ sweepTaskChildren(taskId: string): Promise<void>;
210
+ /**
211
+ * The shared front half of a chunk: resolve terminal/cancel short-circuits,
212
+ * validate the Recipe, claim the row (fresh-vs-retry), and assemble the
213
+ * execution request. Deterministic every chunk, so the request — and thus its
214
+ * fingerprint — is identical across a run's chunks and their retries.
215
+ */
216
+ private prepareChunk;
217
+ /**
218
+ * Invoke the managed child for one chunk, recreating it once on a fingerprint
219
+ * mismatch (a stale child from a *different* request — recoverable exactly once;
220
+ * a second mismatch is a genuine lifecycle bug and must surface).
221
+ */
222
+ private executeChunkInChild;
223
+ /** Let the owning plugin release whatever `resolveRuntime` acquired. */
224
+ private releaseRuntime;
225
+ /** The same, from a durable row rather than a built request. Best-effort. */
226
+ private releaseRuntimeQuietly;
227
+ /** The validated tool families for a Subtask type, or none if unusable. */
228
+ private toolFamiliesForType;
229
+ /**
230
+ * Best-effort release of a child's external state on cancellation (e.g. close a
231
+ * leased resource recorded in its workspace). Swallows failures — an unreleased
232
+ * resource is a documented residual, not a reason to fail cancellation.
233
+ */
234
+ private abortChildQuietly;
235
+ /** Persist a child's terminal outcome. Returns whether the guarded write applied. */
236
+ private persistResult;
237
+ /** Re-read a Subtask that must exist (it was just written). */
238
+ private requireSubtask;
239
+ /** Delete a managed child, swallowing failures (used on best-effort sweeps). */
240
+ private deleteChildQuietly;
241
+ /** Whether the parent Task has been canceled (checked before and after work). */
242
+ private isTaskCanceled;
243
+ /**
244
+ * Interrupt a canceled Task's live children: each `running` Subtask's managed
245
+ * child gets `abortRun`, so a long recipe stops at its current model call
246
+ * instead of at the next chunk boundary (up to `chunkSoftMs` later). A subtask
247
+ * that already finished (e.g. one branch completed while another was
248
+ * still running) is deliberately retained until the terminal-delivery sweep —
249
+ * but a canceled Task never reaches delivery, so its idle child is deleted
250
+ * here instead, or it would leak until the 30-day row cleanup regardless of
251
+ * that row's own age.
252
+ *
253
+ * Only `running` rows have a live RPC to abort. `subAgent` *creates* a facet
254
+ * that does not exist, so calling it for a `pending` row (no facet was ever
255
+ * made) would materialize one just to delete it — `deleteChildQuietly` is a
256
+ * silent no-op there, so it is called unconditionally instead of branching on
257
+ * status. Bounded by `maxSubtasks`. Best-effort throughout: a child that
258
+ * cannot be reached is logged, never fatal — cancellation must not fail
259
+ * because cleanup did.
260
+ *
261
+ * The `pending` rows are transitioned here too, and that is not bookkeeping:
262
+ * it is the only thing that resolves them. Nothing else is coming back to a
263
+ * pending row once a Task is canceled — the Workflow's scheduler runs a
264
+ * single pass and does not re-scan, and `prepareChunk` reports a canceled
265
+ * Task's pending row as terminal *without* claiming it, so a branch whose RPC
266
+ * had not yet reached the claim when the cancellation landed simply returns.
267
+ * Left to the loop below, which only deletes the child, the row would sit
268
+ * non-terminal until the 30-day cleanup.
269
+ */
270
+ protected onTaskCanceled(taskId: string): Promise<void>;
271
+ }