@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/host/agent.js
CHANGED
|
@@ -6,7 +6,7 @@ import { AgentDB, stateOf } from "../db/index.js";
|
|
|
6
6
|
import { callerContext } from "../a2a/caller.js";
|
|
7
7
|
import { createPushChannel } from "../a2a/push.js";
|
|
8
8
|
import { buildAgentSession } from "../agent/session.js";
|
|
9
|
-
import {
|
|
9
|
+
import { workersAIModels } from "../agent/workers-ai/index.js";
|
|
10
10
|
/**
|
|
11
11
|
* The Durable Object body every Looping agent has, whatever loop it runs.
|
|
12
12
|
*
|
|
@@ -59,6 +59,9 @@ export class LoopingAgent extends Agent {
|
|
|
59
59
|
* `onStart` runs before any request, so it is not known when `agentPlugins()`
|
|
60
60
|
* is built — which is why anything per-caller takes a thunk. The DO is keyed
|
|
61
61
|
* 1:1 by this value, so it is constant once set.
|
|
62
|
+
*
|
|
63
|
+
* In-memory, and deliberately so — see {@link requireIdentityKey}, which does
|
|
64
|
+
* not depend on it surviving.
|
|
62
65
|
*/
|
|
63
66
|
identityKey;
|
|
64
67
|
/**
|
|
@@ -101,12 +104,34 @@ export class LoopingAgent extends Agent {
|
|
|
101
104
|
stores: this.runtime.stores
|
|
102
105
|
}));
|
|
103
106
|
}
|
|
104
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Which provider this agent's loops run on. Defaults to Workers AI; override
|
|
109
|
+
* to run on something else.
|
|
110
|
+
*
|
|
111
|
+
* The seam is here rather than in `models` because `models` memoizes, and a
|
|
112
|
+
* subclass overriding a memoized getter has to remember to keep the caching —
|
|
113
|
+
* a trap that only shows up as a performance bug. This is called once.
|
|
114
|
+
*
|
|
115
|
+
* `ModelRuntime` is the whole contract: return anything satisfying it and
|
|
116
|
+
* every loop in core keeps working unchanged. Core ships two implementations,
|
|
117
|
+
* one directory each — {@link file://../agent/workers-ai/index.ts
|
|
118
|
+
* `agent/workers-ai`} (the default below) and `@loopingai/core/anthropic` —
|
|
119
|
+
* and a third provider is a third directory exporting one
|
|
120
|
+
* {@link file://../agent/model.ts ModelRuntimeFactory}, not a change to
|
|
121
|
+
* anything on this path.
|
|
122
|
+
*
|
|
123
|
+
* Takes the resolved {@link ModelConfig} rather than reading `this.config`, so
|
|
124
|
+
* that this signature matches
|
|
125
|
+
* {@link file://../round/subagent.ts RecipeSubagentHost.modelRuntime} — an
|
|
126
|
+
* agent and its facet **must** run the same provider, and identical seams are
|
|
127
|
+
* what let one factory serve both instead of two hand-copied bodies.
|
|
128
|
+
*/
|
|
129
|
+
modelRuntime(model) {
|
|
130
|
+
return workersAIModels(this.env, model);
|
|
131
|
+
}
|
|
132
|
+
/** The model runtime for this instance, built lazily and memoized. */
|
|
105
133
|
get models() {
|
|
106
|
-
return (this._models ??=
|
|
107
|
-
ai: this.env.AI,
|
|
108
|
-
config: this.config.model
|
|
109
|
-
}));
|
|
134
|
+
return (this._models ??= this.modelRuntime(this.config.model));
|
|
110
135
|
}
|
|
111
136
|
/**
|
|
112
137
|
* What this agent's plugins are handed. Built from
|
|
@@ -193,12 +218,41 @@ export class LoopingAgent extends Agent {
|
|
|
193
218
|
onMessagesDisplaced: this.runtime.onMessagesDisplaced
|
|
194
219
|
}));
|
|
195
220
|
}
|
|
196
|
-
/**
|
|
221
|
+
/**
|
|
222
|
+
* The caller key, which is present on every path that can reach a plugin.
|
|
223
|
+
*
|
|
224
|
+
* ## Why this does not just read the field
|
|
225
|
+
*
|
|
226
|
+
* `identityKey` is set on the first turn and lives in the isolate. An isolate
|
|
227
|
+
* does not live as long as the work does: it can be evicted between two rounds
|
|
228
|
+
* of the same task, and it can be reset outright — "Durable Object connection
|
|
229
|
+
* closed because the object was reset" — while a Workflow step is mid-flight.
|
|
230
|
+
* The next call arrives on a fresh instance where the field is empty, and
|
|
231
|
+
* every per-caller thunk built off it throws.
|
|
232
|
+
*
|
|
233
|
+
* That failure is disproportionate to its cause. A plugin asking which caller
|
|
234
|
+
* it is serving gets an exception, mid-task, on an object whose entire purpose
|
|
235
|
+
* is to be that caller's — and because the throw happens inside a tool or a
|
|
236
|
+
* runtime resolution rather than at the edge, it surfaces as a failed branch
|
|
237
|
+
* rather than as anything an operator can read.
|
|
238
|
+
*
|
|
239
|
+
* So the object answers from itself. `define-agent` routes with
|
|
240
|
+
* `ns.get(ns.idFromName(identity.key))`, which means the caller key *is* this
|
|
241
|
+
* object's name and the platform hands it back on `ctx.id.name` — durable, free
|
|
242
|
+
* and correct by construction: an object cannot disagree with the name it was
|
|
243
|
+
* addressed by.
|
|
244
|
+
*
|
|
245
|
+
* The field still wins when it is set. `id.name` is undefined for an object
|
|
246
|
+
* addressed by `newUniqueId()` or a raw id string, so it is a fallback rather
|
|
247
|
+
* than the source of truth, and the throw is kept for the case where neither
|
|
248
|
+
* exists.
|
|
249
|
+
*/
|
|
197
250
|
requireIdentityKey() {
|
|
198
|
-
|
|
251
|
+
const key = this.identityKey ?? this.ctx.id.name;
|
|
252
|
+
if (!key) {
|
|
199
253
|
throw new Error("identity.key is required for per-caller isolation");
|
|
200
254
|
}
|
|
201
|
-
return this.identityKey;
|
|
255
|
+
return (this.identityKey = key);
|
|
202
256
|
}
|
|
203
257
|
/** The gateway callback channel for one turn. See {@link PushChannel}. */
|
|
204
258
|
push(context) {
|
package/dist/index.d.ts
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
* production bundle.
|
|
10
10
|
*/
|
|
11
11
|
export { createAgentRuntime, RuntimeSetupError, buildRecipeTools, collectToolFamilies, type AgentRuntime, type CreateAgentRuntimeOptions } from "./runtime/index.js";
|
|
12
|
-
export { PLUGIN_CONTRACT_VERSION, definePlugin, type AgentPlugin, type EmitProgress, type EnrichResultContext, type MainAgentToolContext, type PluginRequirements, type RecipeToolSet, type ResolveRuntimeContext, type ToolFamilyBuilder, type ToolFamilyContext, type TurnGateContext } from "./contract/plugin.js";
|
|
12
|
+
export { PLUGIN_CONTRACT_VERSION, definePlugin, restrictMainAgentTools, type AgentPlugin, type RestrictMainAgentToolsOptions, type EmitProgress, type EnrichResultContext, type MainAgentToolContext, type PluginRequirements, type RecipeToolSet, type ResolveRuntimeContext, type ToolFamilyBuilder, type ToolFamilyContext, type TurnGateContext } from "./contract/plugin.js";
|
|
13
13
|
export type { DelegationNames, RecipeLimits, ResolvedRecipe, SubtaskParams, SubtaskParamsSchema, SubtaskParamsShape, SubtaskTypeSpec, ValidatedRecipe } from "./contract/recipe.js";
|
|
14
14
|
export { RecipeValidationError, resolveLimits, validateRecipe, type RecipePolicy } from "./contract/validation.js";
|
|
15
15
|
export { ConfigError, DEFAULT_CORE_CONFIG, resolveConfig, type AgentLimits, type CoreConfig, type CoreConfigOverrides, type ModelConfig, type SessionConfig } from "./config.js";
|
|
16
16
|
export { parseGatewayOrigins, type A2ASecretsEnv, type AiEnv, type CoreEnv } from "./env.js";
|
|
17
|
-
export { CHUNK_SOFT_MS, MAX_CHUNKS_PER_BRANCH, STEP_TIMEOUT_MS, STEPS_PER_INSTANCE } from "./platform.js";
|
|
17
|
+
export { CHUNK_SOFT_MS, MAX_CHUNKS_PER_BRANCH, MAX_TOOL_CALL_MS, STEP_TIMEOUT_MS, STEPS_PER_INSTANCE } from "./platform.js";
|
|
18
18
|
export type { PluginStore } from "./db/db.js";
|
|
19
19
|
export { makeWorkspaceHandle, memoryWorkspaceBacking, WorkspaceLimitError, WORKSPACE_MAX_FILES, WORKSPACE_MAX_FILE_BYTES, type WorkspaceBacking, type WorkspaceEntry, type WorkspaceHandle } from "./subagent/workspace.js";
|
package/dist/index.js
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* production bundle.
|
|
10
10
|
*/
|
|
11
11
|
export { createAgentRuntime, RuntimeSetupError, buildRecipeTools, collectToolFamilies } from "./runtime/index.js";
|
|
12
|
-
export { PLUGIN_CONTRACT_VERSION, definePlugin } from "./contract/plugin.js";
|
|
12
|
+
export { PLUGIN_CONTRACT_VERSION, definePlugin, restrictMainAgentTools } from "./contract/plugin.js";
|
|
13
13
|
export { RecipeValidationError, resolveLimits, validateRecipe } from "./contract/validation.js";
|
|
14
14
|
export { ConfigError, DEFAULT_CORE_CONFIG, resolveConfig } from "./config.js";
|
|
15
15
|
export { parseGatewayOrigins } from "./env.js";
|
|
16
|
-
export { CHUNK_SOFT_MS, MAX_CHUNKS_PER_BRANCH, STEP_TIMEOUT_MS, STEPS_PER_INSTANCE } from "./platform.js";
|
|
16
|
+
export { CHUNK_SOFT_MS, MAX_CHUNKS_PER_BRANCH, MAX_TOOL_CALL_MS, STEP_TIMEOUT_MS, STEPS_PER_INSTANCE } from "./platform.js";
|
|
17
17
|
export { makeWorkspaceHandle, memoryWorkspaceBacking, WorkspaceLimitError, WORKSPACE_MAX_FILES, WORKSPACE_MAX_FILE_BYTES } from "./subagent/workspace.js";
|
package/dist/platform.d.ts
CHANGED
|
@@ -4,20 +4,38 @@
|
|
|
4
4
|
* for those, and note that no Recipe can reach these. They change when the
|
|
5
5
|
* platform changes, and for no other reason.
|
|
6
6
|
*
|
|
7
|
-
* The distinction is worth keeping sharp, because collapsing it
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
11
|
*/
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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`.
|
|
15
31
|
*/
|
|
16
32
|
export declare const STEP_TIMEOUT_MS: number;
|
|
17
33
|
/**
|
|
18
|
-
* Platform fact: a single Workflow instance may run
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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`.
|
|
21
39
|
*/
|
|
22
40
|
export declare const STEPS_PER_INSTANCE = 10000;
|
|
23
41
|
/**
|
|
@@ -28,8 +46,52 @@ export declare const STEPS_PER_INSTANCE = 10000;
|
|
|
28
46
|
* This is the *only* thing keeping a step under the timeout. A subagent otherwise
|
|
29
47
|
* runs until its turn or wall-clock budget is spent, however many turns that takes
|
|
30
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.
|
|
31
78
|
*/
|
|
32
79
|
export declare const CHUNK_SOFT_MS: number;
|
|
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 `@loopingai/plugins/computer` in looping-starter.
|
|
93
|
+
*/
|
|
94
|
+
export declare const MAX_TOOL_CALL_MS: number;
|
|
33
95
|
/**
|
|
34
96
|
* Hard ceiling on durable chunk steps for one Subtask branch. A backstop, not a
|
|
35
97
|
* budget: the Workflow *fails* a branch that reaches it, so reaching it is a bug.
|
|
@@ -39,8 +101,9 @@ export declare const CHUNK_SOFT_MS: number;
|
|
|
39
101
|
* 1. It exceeds every Recipe's `maxTurns`. A chunk that yields always advanced at
|
|
40
102
|
* least one turn, so a run takes at most `maxTurns` chunks however short they
|
|
41
103
|
* are — and they do get short, because `CHUNK_SOFT_MS` and progress events both
|
|
42
|
-
* end one early.
|
|
43
|
-
* estimate
|
|
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.
|
|
44
107
|
* 2. The worst-case step product stays under {@link STEPS_PER_INSTANCE}.
|
|
45
108
|
*/
|
|
46
109
|
export declare const MAX_CHUNKS_PER_BRANCH = 40;
|
package/dist/platform.js
CHANGED
|
@@ -4,20 +4,38 @@
|
|
|
4
4
|
* for those, and note that no Recipe can reach these. They change when the
|
|
5
5
|
* platform changes, and for no other reason.
|
|
6
6
|
*
|
|
7
|
-
* The distinction is worth keeping sharp, because collapsing it
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
11
|
*/
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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`.
|
|
15
31
|
*/
|
|
16
|
-
export const STEP_TIMEOUT_MS =
|
|
32
|
+
export const STEP_TIMEOUT_MS = 30 * 60_000;
|
|
17
33
|
/**
|
|
18
|
-
* Platform fact: a single Workflow instance may run
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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`.
|
|
21
39
|
*/
|
|
22
40
|
export const STEPS_PER_INSTANCE = 10_000;
|
|
23
41
|
/**
|
|
@@ -28,8 +46,52 @@ export const STEPS_PER_INSTANCE = 10_000;
|
|
|
28
46
|
* This is the *only* thing keeping a step under the timeout. A subagent otherwise
|
|
29
47
|
* runs until its turn or wall-clock budget is spent, however many turns that takes
|
|
30
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 `@loopingai/plugins/computer` in looping-starter.
|
|
31
93
|
*/
|
|
32
|
-
export const
|
|
94
|
+
export const MAX_TOOL_CALL_MS = 10 * 60_000;
|
|
33
95
|
/**
|
|
34
96
|
* Hard ceiling on durable chunk steps for one Subtask branch. A backstop, not a
|
|
35
97
|
* budget: the Workflow *fails* a branch that reaches it, so reaching it is a bug.
|
|
@@ -39,8 +101,9 @@ export const CHUNK_SOFT_MS = 4 * 60_000;
|
|
|
39
101
|
* 1. It exceeds every Recipe's `maxTurns`. A chunk that yields always advanced at
|
|
40
102
|
* least one turn, so a run takes at most `maxTurns` chunks however short they
|
|
41
103
|
* are — and they do get short, because `CHUNK_SOFT_MS` and progress events both
|
|
42
|
-
* end one early.
|
|
43
|
-
* estimate
|
|
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.
|
|
44
107
|
* 2. The worst-case step product stays under {@link STEPS_PER_INSTANCE}.
|
|
45
108
|
*/
|
|
46
109
|
export const MAX_CHUNKS_PER_BRANCH = 40;
|
package/dist/round/agent.d.ts
CHANGED
|
@@ -7,14 +7,13 @@ import type { SubagentClass } from "./subagent.js";
|
|
|
7
7
|
import type { RoundPolicy } from "./policy.js";
|
|
8
8
|
import { type RoundMode } from "./turn.js";
|
|
9
9
|
/**
|
|
10
|
-
* A **delegating** agent: the round loop, the durable
|
|
11
|
-
* isolated subagent execution beneath
|
|
10
|
+
* A **delegating** agent: the round loop, the durable Subtasks it hands out, and
|
|
11
|
+
* the isolated subagent execution beneath them.
|
|
12
12
|
*
|
|
13
13
|
* Everything {@link LoopingAgent} gives every agent, plus the half that only a
|
|
14
14
|
* delegating one needs — and it is all mechanism. A Workflow drives it through
|
|
15
|
-
* native Cloudflare RPC (`runTaskTurn`, `
|
|
16
|
-
*
|
|
17
|
-
* detail of the Worker.
|
|
15
|
+
* native Cloudflare RPC (`runTaskTurn`, `scanSubtasks`, `executeSubtaskChunk`,
|
|
16
|
+
* …), never HTTP: the DO is a private implementation detail of the Worker.
|
|
18
17
|
*
|
|
19
18
|
* ## Why core owns this
|
|
20
19
|
*
|
|
@@ -73,8 +72,8 @@ export declare abstract class RoundAgentBase<TEnv extends Cloudflare.Env & AiEnv
|
|
|
73
72
|
/** Age out this agent's subtask rows alongside the task rows. */
|
|
74
73
|
protected cleanupAgentState(): void;
|
|
75
74
|
/**
|
|
76
|
-
* One main-agent round: answer the user, or delegate a durable
|
|
77
|
-
* return the acknowledgment the user sees while it runs.
|
|
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.
|
|
78
77
|
*
|
|
79
78
|
* This is the RPC boundary, so it is where the round's cost becomes a field. The
|
|
80
79
|
* budget is created here, handed to {@link decideRound} to be spent, and read
|
|
@@ -120,29 +119,33 @@ export declare abstract class RoundAgentBase<TEnv extends Cloudflare.Env & AiEnv
|
|
|
120
119
|
* Every round's branches for a Task, in stable ordinal order — what a round
|
|
121
120
|
* needs to reunite each earlier `delegate` call with its result. Built inside
|
|
122
121
|
* the DO and consumed here, so the 1 MiB Workflow-step cap that keeps
|
|
123
|
-
* {@link
|
|
122
|
+
* {@link SubtaskScan} down to ids does not apply.
|
|
124
123
|
*/
|
|
125
124
|
private compositionBranches;
|
|
126
125
|
/** A Task's Subtasks, every round, in stable ordinal order. */
|
|
127
126
|
listSubtasks(taskId: string): Promise<Subtask[]>;
|
|
128
127
|
/**
|
|
129
|
-
* The Workflow's
|
|
130
|
-
*
|
|
131
|
-
* return the refreshed DAG as scheduler {@link SubtaskNode}s.
|
|
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.
|
|
132
130
|
*
|
|
133
|
-
* Scoped to the round because
|
|
134
|
-
* round's rows are already terminal and
|
|
135
|
-
*
|
|
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.
|
|
136
134
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
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.
|
|
141
144
|
*
|
|
142
145
|
* The cancellation verdict rides along rather than being probed separately, so
|
|
143
|
-
*
|
|
146
|
+
* the scan costs one round trip and cannot act on a stale answer.
|
|
144
147
|
*/
|
|
145
|
-
|
|
148
|
+
scanSubtasks(taskId: string, round: number): Promise<SubtaskScan>;
|
|
146
149
|
/** Parent cancellation: cancel every still-pending Subtask. Returns the count. */
|
|
147
150
|
cancelPendingSubtasks(taskId: string): Promise<number>;
|
|
148
151
|
/**
|
|
@@ -182,8 +185,9 @@ export declare abstract class RoundAgentBase<TEnv extends Cloudflare.Env & AiEnv
|
|
|
182
185
|
* delete; that now happens strictly later.
|
|
183
186
|
*
|
|
184
187
|
* Throws on a transient fault (the step retries and the child resumes from its
|
|
185
|
-
* checkpoint) and
|
|
186
|
-
*
|
|
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.
|
|
187
191
|
*/
|
|
188
192
|
executeSubtaskChunk(id: SubtaskId, chunk: number, push?: TurnPushContext): Promise<SubtaskChunkOutcome>;
|
|
189
193
|
/**
|
|
@@ -230,14 +234,6 @@ export declare abstract class RoundAgentBase<TEnv extends Cloudflare.Env & AiEnv
|
|
|
230
234
|
private abortChildQuietly;
|
|
231
235
|
/** Persist a child's terminal outcome. Returns whether the guarded write applied. */
|
|
232
236
|
private persistResult;
|
|
233
|
-
/**
|
|
234
|
-
* Load a Subtask's dependency results, in ordinal order.
|
|
235
|
-
*
|
|
236
|
-
* Order is semantic: it feeds the child's request fingerprint, so a retry must
|
|
237
|
-
* build the identical array or the cache misses. A dependency that has not
|
|
238
|
-
* completed means the scheduler ran this node too early.
|
|
239
|
-
*/
|
|
240
|
-
private loadDependencyResults;
|
|
241
237
|
/** Re-read a Subtask that must exist (it was just written). */
|
|
242
238
|
private requireSubtask;
|
|
243
239
|
/** Delete a managed child, swallowing failures (used on best-effort sweeps). */
|
|
@@ -248,7 +244,7 @@ export declare abstract class RoundAgentBase<TEnv extends Cloudflare.Env & AiEnv
|
|
|
248
244
|
* Interrupt a canceled Task's live children: each `running` Subtask's managed
|
|
249
245
|
* child gets `abortRun`, so a long recipe stops at its current model call
|
|
250
246
|
* instead of at the next chunk boundary (up to `chunkSoftMs` later). A subtask
|
|
251
|
-
* that already finished (e.g. one branch
|
|
247
|
+
* that already finished (e.g. one branch completed while another was
|
|
252
248
|
* still running) is deliberately retained until the terminal-delivery sweep —
|
|
253
249
|
* but a canceled Task never reaches delivery, so its idle child is deleted
|
|
254
250
|
* here instead, or it would leak until the 30-day row cleanup regardless of
|
|
@@ -261,6 +257,15 @@ export declare abstract class RoundAgentBase<TEnv extends Cloudflare.Env & AiEnv
|
|
|
261
257
|
* status. Bounded by `maxSubtasks`. Best-effort throughout: a child that
|
|
262
258
|
* cannot be reached is logged, never fatal — cancellation must not fail
|
|
263
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.
|
|
264
269
|
*/
|
|
265
270
|
protected onTaskCanceled(taskId: string): Promise<void>;
|
|
266
271
|
}
|