@dynamicagents/core 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +522 -0
- package/dist/a2a/agent-stub.d.ts +60 -0
- package/dist/a2a/agent-stub.js +1 -0
- package/dist/a2a/caller-token.d.ts +44 -0
- package/dist/a2a/caller-token.js +61 -0
- package/dist/a2a/caller.d.ts +23 -0
- package/dist/a2a/caller.js +33 -0
- package/dist/a2a/card.d.ts +158 -0
- package/dist/a2a/card.js +163 -0
- package/dist/a2a/context.d.ts +19 -0
- package/dist/a2a/context.js +61 -0
- package/dist/a2a/deliver.d.ts +159 -0
- package/dist/a2a/deliver.js +185 -0
- package/dist/a2a/executor.d.ts +84 -0
- package/dist/a2a/executor.js +106 -0
- package/dist/a2a/index.d.ts +35 -0
- package/dist/a2a/index.js +34 -0
- package/dist/a2a/notify.d.ts +106 -0
- package/dist/a2a/notify.js +180 -0
- package/dist/a2a/parts.d.ts +45 -0
- package/dist/a2a/parts.js +94 -0
- package/dist/a2a/push.d.ts +70 -0
- package/dist/a2a/push.js +53 -0
- package/dist/a2a/self-origin.d.ts +91 -0
- package/dist/a2a/self-origin.js +114 -0
- package/dist/a2a/task-store.d.ts +44 -0
- package/dist/a2a/task-store.js +99 -0
- package/dist/a2a/task.d.ts +85 -0
- package/dist/a2a/task.js +15 -0
- package/dist/a2a/verify.d.ts +80 -0
- package/dist/a2a/verify.js +143 -0
- package/dist/agent/budget.d.ts +46 -0
- package/dist/agent/budget.js +42 -0
- package/dist/agent/control.d.ts +109 -0
- package/dist/agent/control.js +115 -0
- package/dist/agent/errors.d.ts +85 -0
- package/dist/agent/errors.js +64 -0
- package/dist/agent/final-reply.d.ts +49 -0
- package/dist/agent/final-reply.js +68 -0
- package/dist/agent/history.d.ts +97 -0
- package/dist/agent/history.js +133 -0
- package/dist/agent/index.d.ts +29 -0
- package/dist/agent/index.js +29 -0
- package/dist/agent/inference.d.ts +110 -0
- package/dist/agent/inference.js +120 -0
- package/dist/agent/model.d.ts +90 -0
- package/dist/agent/model.js +1 -0
- package/dist/agent/session.d.ts +100 -0
- package/dist/agent/session.js +82 -0
- package/dist/agent/workers-ai/index.d.ts +23 -0
- package/dist/agent/workers-ai/index.js +23 -0
- package/dist/agent/workers-ai/runtime.d.ts +42 -0
- package/dist/agent/workers-ai/runtime.js +63 -0
- package/dist/alarm/index.d.ts +77 -0
- package/dist/alarm/index.js +116 -0
- package/dist/config.d.ts +202 -0
- package/dist/config.js +135 -0
- package/dist/contract/index.d.ts +9 -0
- package/dist/contract/index.js +8 -0
- package/dist/contract/plugin.d.ts +324 -0
- package/dist/contract/plugin.js +114 -0
- package/dist/contract/recipe.d.ts +180 -0
- package/dist/contract/recipe.js +1 -0
- package/dist/contract/validation.d.ts +91 -0
- package/dist/contract/validation.js +84 -0
- package/dist/db/db.d.ts +147 -0
- package/dist/db/db.js +90 -0
- package/dist/db/index.d.ts +8 -0
- package/dist/db/index.js +8 -0
- package/dist/db/migrations/index.d.ts +20 -0
- package/dist/db/migrations/index.js +60 -0
- package/dist/db/models/subtasks.d.ts +100 -0
- package/dist/db/models/subtasks.js +241 -0
- package/dist/db/models/tasks.d.ts +118 -0
- package/dist/db/models/tasks.js +274 -0
- package/dist/db/schema.d.ts +468 -0
- package/dist/db/schema.js +88 -0
- package/dist/env.d.ts +53 -0
- package/dist/env.js +47 -0
- package/dist/host/agent.d.ts +305 -0
- package/dist/host/agent.js +400 -0
- package/dist/host/index.d.ts +20 -0
- package/dist/host/index.js +19 -0
- package/dist/host/plugin-host.d.ts +42 -0
- package/dist/host/plugin-host.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +17 -0
- package/dist/job/index.d.ts +21 -0
- package/dist/job/index.js +21 -0
- package/dist/job/lifecycle.d.ts +176 -0
- package/dist/job/lifecycle.js +230 -0
- package/dist/job/state.d.ts +92 -0
- package/dist/job/state.js +40 -0
- package/dist/platform.d.ts +138 -0
- package/dist/platform.js +140 -0
- package/dist/round/agent.d.ts +271 -0
- package/dist/round/agent.js +678 -0
- package/dist/round/index.d.ts +25 -0
- package/dist/round/index.js +23 -0
- package/dist/round/policy.d.ts +98 -0
- package/dist/round/policy.js +1 -0
- package/dist/round/subagent.d.ts +87 -0
- package/dist/round/subagent.js +108 -0
- package/dist/round/turn.d.ts +249 -0
- package/dist/round/turn.js +564 -0
- package/dist/round/workflow.d.ts +147 -0
- package/dist/round/workflow.js +387 -0
- package/dist/runtime/index.d.ts +127 -0
- package/dist/runtime/index.js +186 -0
- package/dist/runtime/tool-families.d.ts +32 -0
- package/dist/runtime/tool-families.js +61 -0
- package/dist/subagent/fingerprint.d.ts +37 -0
- package/dist/subagent/fingerprint.js +92 -0
- package/dist/subagent/index.d.ts +169 -0
- package/dist/subagent/index.js +330 -0
- package/dist/subagent/prompt.d.ts +32 -0
- package/dist/subagent/prompt.js +37 -0
- package/dist/subagent/run.d.ts +157 -0
- package/dist/subagent/run.js +540 -0
- package/dist/subagent/workspace.d.ts +85 -0
- package/dist/subagent/workspace.js +127 -0
- package/dist/subtasks/catalog.d.ts +40 -0
- package/dist/subtasks/catalog.js +36 -0
- package/dist/subtasks/decomposition.d.ts +85 -0
- package/dist/subtasks/decomposition.js +156 -0
- package/dist/subtasks/delegate.d.ts +120 -0
- package/dist/subtasks/delegate.js +131 -0
- package/dist/subtasks/index.d.ts +9 -0
- package/dist/subtasks/index.js +9 -0
- package/dist/subtasks/subtask-types.d.ts +91 -0
- package/dist/subtasks/subtask-types.js +103 -0
- package/dist/subtasks/types.d.ts +295 -0
- package/dist/subtasks/types.js +15 -0
- package/dist/testing/auth.d.ts +34 -0
- package/dist/testing/auth.js +35 -0
- package/dist/testing/do.d.ts +29 -0
- package/dist/testing/do.js +25 -0
- package/dist/testing/fake-session.d.ts +26 -0
- package/dist/testing/fake-session.js +37 -0
- package/dist/testing/fixtures.d.ts +64 -0
- package/dist/testing/fixtures.js +104 -0
- package/dist/testing/harness.d.ts +97 -0
- package/dist/testing/harness.js +138 -0
- package/dist/testing/index.d.ts +31 -0
- package/dist/testing/index.js +35 -0
- package/dist/testing/mock-model.d.ts +77 -0
- package/dist/testing/mock-model.js +136 -0
- package/dist/testing/node.d.ts +56 -0
- package/dist/testing/node.js +56 -0
- package/dist/testing/vcr-global-setup.d.ts +12 -0
- package/dist/testing/vcr-global-setup.js +15 -0
- package/dist/testing/vcr-shared.d.ts +38 -0
- package/dist/testing/vcr-shared.js +33 -0
- package/dist/testing/vcr-spec.d.ts +25 -0
- package/dist/testing/vcr-spec.js +124 -0
- package/dist/testing/vcr-store.d.ts +86 -0
- package/dist/testing/vcr-store.js +191 -0
- package/dist/testing/vcr.d.ts +117 -0
- package/dist/testing/vcr.js +275 -0
- package/dist/worker/define-agent.d.ts +123 -0
- package/dist/worker/define-agent.js +20 -0
- package/dist/worker/index.d.ts +218 -0
- package/dist/worker/index.js +369 -0
- package/eslint-rules/index.js +31 -0
- package/eslint-rules/no-deprecated-object-properties.js +81 -0
- package/package.json +178 -0
- package/scripts/generate-keys.mjs +48 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { WorkflowStep } from "cloudflare:workers";
|
|
2
|
+
import type { CoreConfig } from "../config.js";
|
|
3
|
+
import type { GatekeeperIdentity } from "../a2a/verify.js";
|
|
4
|
+
import type { RoundFailureKind } from "../agent/inference.js";
|
|
5
|
+
import type { RoundAgentBase } from "./agent.js";
|
|
6
|
+
import type { RoundPolicy } from "./policy.js";
|
|
7
|
+
/**
|
|
8
|
+
* The async task controller. The gatekeeper does not wait for a synchronous reply:
|
|
9
|
+
* the Worker accepts a turn (returns a `submitted` Task) and hands the actual work
|
|
10
|
+
* to this durable Workflow, which orchestrates it end to end and delivers the
|
|
11
|
+
* reply to the gatekeeper's push-notification webhook.
|
|
12
|
+
*
|
|
13
|
+
* The shape is a **round loop**, not a fixed sequence of phases:
|
|
14
|
+
*
|
|
15
|
+
* 0. **Pre-work** — resolve the caller's agent, mark the Task working.
|
|
16
|
+
* 1. **Round** — one main-agent inference that either answers the user (the Task
|
|
17
|
+
* is done) or delegates durable Subtasks plus the acknowledgment the user sees
|
|
18
|
+
* while they run.
|
|
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.
|
|
23
|
+
* 3. **Deliver** — persist the terminal Task, then POST a signed callback.
|
|
24
|
+
*
|
|
25
|
+
* The main agent is never forced either way. A round that has run out of budget —
|
|
26
|
+
* `mainAgentLimits`, in turns or in wall clock — is handed no tools but the
|
|
27
|
+
* answer, so it has to give one; every other round chooses. That is the whole
|
|
28
|
+
* reason this is a loop, and the whole termination argument.
|
|
29
|
+
*
|
|
30
|
+
* Why a Workflow (not a DO alarm or `waitUntil`): `step.do(...)` gives durable,
|
|
31
|
+
* independently-retried steps that survive isolate eviction, and a future
|
|
32
|
+
* `escalate` decision (ask the human, then continue) slots in cleanly as another
|
|
33
|
+
* branch of the loop built on `step.waitForEvent(...)`.
|
|
34
|
+
*
|
|
35
|
+
* A Workflow is a separate entrypoint and cannot touch the agent DO's SQLite
|
|
36
|
+
* directly, so: the task inputs travel as the workflow **payload**, and the agent
|
|
37
|
+
* runtime plus task state are reached only through **native DO RPC**.
|
|
38
|
+
*
|
|
39
|
+
* Idempotency: the instance id is derived from the gatekeeper's `messageId`
|
|
40
|
+
* (deterministic across dispatch retries), so a re-dispatch never starts a second
|
|
41
|
+
* run. Within a run, every step is re-runnable: the Subtask rows and the Session
|
|
42
|
+
* are the source of truth, and each round recovers from them rather than
|
|
43
|
+
* re-inferring.
|
|
44
|
+
*/
|
|
45
|
+
export interface HandleTaskParams {
|
|
46
|
+
/** The accepted task id (echoed back to the gatekeeper on the callback). */
|
|
47
|
+
taskId: string;
|
|
48
|
+
/** The user turn text to answer. */
|
|
49
|
+
text: string;
|
|
50
|
+
/** The verified calling gatekeeper-agent identity (keys the DO + the Session). */
|
|
51
|
+
identity: GatekeeperIdentity;
|
|
52
|
+
/** A2A context id, echoed on the completed Task. */
|
|
53
|
+
contextId: string;
|
|
54
|
+
/** Gatekeeper push-notification webhook (also the callback JWT `aud`). */
|
|
55
|
+
pushUrl: string;
|
|
56
|
+
/** Per-task validation token the gatekeeper set; echoed in the callback header. */
|
|
57
|
+
pushToken: string;
|
|
58
|
+
/** This agent's card-signing JWKS URL — the callback JWT `jku` (pinned key). */
|
|
59
|
+
jku: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* What distinguishes one agent's use of this loop from another's.
|
|
63
|
+
*
|
|
64
|
+
* The whole body below is agent-agnostic — it names no soul, no plugin and no
|
|
65
|
+
* model. Two delegating agents differ only in these values, which is why they
|
|
66
|
+
* share one workflow body behind two thin entrypoints rather than two copies of a
|
|
67
|
+
* 250-line orchestration.
|
|
68
|
+
*/
|
|
69
|
+
export interface HandleTaskDeps {
|
|
70
|
+
/**
|
|
71
|
+
* Route to the right DO class for the verified caller.
|
|
72
|
+
*
|
|
73
|
+
* Called **once per step body**, not once per run, so it must stay a cheap
|
|
74
|
+
* pure lookup — a namespace `get`, nothing cached and nothing awaited. See
|
|
75
|
+
* {@link ResolveAgent} for why the result must never be hoisted.
|
|
76
|
+
*/
|
|
77
|
+
resolveAgent: (identity: GatekeeperIdentity) => AgentStub;
|
|
78
|
+
/** Resolved config — the loop reads `mainAgentLimits` and `maxSubtasks`. */
|
|
79
|
+
config: CoreConfig;
|
|
80
|
+
/** The user-facing copy. Only `copy.taskFailed` is read out here. */
|
|
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;
|
|
98
|
+
/**
|
|
99
|
+
* The deployment's Ed25519 private JWK, for the terminal callback. Passed
|
|
100
|
+
* rather than read off a module-scope `env` so this stays a pure function of
|
|
101
|
+
* its arguments — and so a Worker whose secret is named something else works
|
|
102
|
+
* with no change here.
|
|
103
|
+
*/
|
|
104
|
+
signingKey: string;
|
|
105
|
+
/**
|
|
106
|
+
* Log prefix for the abandoned-task line, conventionally the agent's tenant
|
|
107
|
+
* id. Optional because nothing here needs it to work — but a deployment that
|
|
108
|
+
* mounts several agents on one Worker gets one log stream, and without this
|
|
109
|
+
* every one of them reports going quiet under the same name.
|
|
110
|
+
*/
|
|
111
|
+
label?: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The caller's agent DO stub — every phase runs through it.
|
|
115
|
+
*
|
|
116
|
+
* Typed on the abstract base rather than a concrete class: the orchestration
|
|
117
|
+
* below calls only methods the base declares, and every delegating agent's stub
|
|
118
|
+
* satisfies it.
|
|
119
|
+
*/
|
|
120
|
+
type AgentStub = DurableObjectStub<RoundAgentBase>;
|
|
121
|
+
/**
|
|
122
|
+
* The orchestration, split from the `WorkflowEntrypoint` wiring so it can be
|
|
123
|
+
* driven with a fake `step` in tests (workerd forbids constructing a
|
|
124
|
+
* `WorkflowEntrypoint` outside the runtime) — and so a second agent can reuse it
|
|
125
|
+
* with different deps.
|
|
126
|
+
*
|
|
127
|
+
* ## What the wrapper adds, and why it is not the host's job
|
|
128
|
+
*
|
|
129
|
+
* Core distinguishes two ways a turn ends badly. A **typed** failure is a value
|
|
130
|
+
* and {@link deliver} carries it. A **transient** fault throws, so the step
|
|
131
|
+
* retries and recovers from the durable rows without paying for a second
|
|
132
|
+
* inference. Neither covers a transient fault that never stops being one: the
|
|
133
|
+
* step exhausts its retries, {@link orchestrate} unwinds, the delivery below is
|
|
134
|
+
* never reached, and the instance errors with the Task still in `working` — the
|
|
135
|
+
* user told nothing, and the runtime recording a hang. See
|
|
136
|
+
* {@link deliverAbandonedTask}, which was written for a deployed agent that did
|
|
137
|
+
* exactly this on 2026-08-19.
|
|
138
|
+
*
|
|
139
|
+
* This is caught **here** rather than left to each `WorkflowEntrypoint` because
|
|
140
|
+
* everything the recovery needs is already in {@link HandleTaskDeps}: the stub
|
|
141
|
+
* (typed on `RoundAgentBase`, so `saveTask` and `sweepTaskChildren` are both
|
|
142
|
+
* reachable), `policy.copy.taskFailed`, and `signingKey`. A host has nothing to
|
|
143
|
+
* add — so asking it to remember buys nothing and costs exactly what it cost the
|
|
144
|
+
* starter, where three of four agents never wrote the `catch` at all.
|
|
145
|
+
*/
|
|
146
|
+
export declare function runHandleTask(p: HandleTaskParams, step: WorkflowStep, deps: HandleTaskDeps): Promise<void>;
|
|
147
|
+
export {};
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { CHUNK_STEP, MAX_CHUNKS_PER_BRANCH, STEP_TIMEOUT_MS } from "../platform.js";
|
|
2
|
+
import { buildCompletedTask, buildFailedTask } from "../a2a/notify.js";
|
|
3
|
+
import { deliverAbandonedTask, deliverTerminalTask } from "../a2a/deliver.js";
|
|
4
|
+
/**
|
|
5
|
+
* The same retries, and a timeout a **round** can actually be measured against.
|
|
6
|
+
*
|
|
7
|
+
* A chunk and a round are bounded by different things, and sharing one constant
|
|
8
|
+
* hid that. A chunk has {@link CHUNK_SOFT_MS}: it checkpoints and hands back a
|
|
9
|
+
* fresh step, so `STEP_TIMEOUT_MS` is a ceiling it is sized to stay under. A
|
|
10
|
+
* round has no soft deadline at all — `runTurn` runs up to
|
|
11
|
+
* `mainAgentLimits.maxTurns` sequential model-plus-tool steps in one
|
|
12
|
+
* `generateText`, and its only bound is that step count. Twenty turns whose
|
|
13
|
+
* tools each take the {@link file://../platform.ts MAX_TOOL_CALL_MS} they are
|
|
14
|
+
* permitted is hours, not half an hour, so a perfectly legal round could be
|
|
15
|
+
* killed and replayed whole.
|
|
16
|
+
*
|
|
17
|
+
* So the ceiling comes from the agent's own patience: a round cannot usefully
|
|
18
|
+
* outlive the wall clock its Task is allowed, because the `deadline:` step fails
|
|
19
|
+
* the Task at that point anyway. Floored at `STEP_TIMEOUT_MS` so a deliberately
|
|
20
|
+
* tight `maxWallMs` cannot produce a step timeout shorter than the single tool
|
|
21
|
+
* call core tells hosts they may install.
|
|
22
|
+
*
|
|
23
|
+
* This remains a backstop against a hang, not a budget. What actually bounds
|
|
24
|
+
* what a round *spends* is `TurnBudget`, and what bounds the Task is
|
|
25
|
+
* `mainAgentLimits` — both of which are checked whatever this says.
|
|
26
|
+
*/
|
|
27
|
+
function turnStep(config) {
|
|
28
|
+
return {
|
|
29
|
+
...CHUNK_STEP,
|
|
30
|
+
timeout: Math.max(config.mainAgentLimits.maxWallMs, STEP_TIMEOUT_MS)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The orchestration, split from the `WorkflowEntrypoint` wiring so it can be
|
|
35
|
+
* driven with a fake `step` in tests (workerd forbids constructing a
|
|
36
|
+
* `WorkflowEntrypoint` outside the runtime) — and so a second agent can reuse it
|
|
37
|
+
* with different deps.
|
|
38
|
+
*
|
|
39
|
+
* ## What the wrapper adds, and why it is not the host's job
|
|
40
|
+
*
|
|
41
|
+
* Core distinguishes two ways a turn ends badly. A **typed** failure is a value
|
|
42
|
+
* and {@link deliver} carries it. A **transient** fault throws, so the step
|
|
43
|
+
* retries and recovers from the durable rows without paying for a second
|
|
44
|
+
* inference. Neither covers a transient fault that never stops being one: the
|
|
45
|
+
* step exhausts its retries, {@link orchestrate} unwinds, the delivery below is
|
|
46
|
+
* never reached, and the instance errors with the Task still in `working` — the
|
|
47
|
+
* user told nothing, and the runtime recording a hang. See
|
|
48
|
+
* {@link deliverAbandonedTask}, which was written for a deployed agent that did
|
|
49
|
+
* exactly this on 2026-08-19.
|
|
50
|
+
*
|
|
51
|
+
* This is caught **here** rather than left to each `WorkflowEntrypoint` because
|
|
52
|
+
* everything the recovery needs is already in {@link HandleTaskDeps}: the stub
|
|
53
|
+
* (typed on `RoundAgentBase`, so `saveTask` and `sweepTaskChildren` are both
|
|
54
|
+
* reachable), `policy.copy.taskFailed`, and `signingKey`. A host has nothing to
|
|
55
|
+
* add — so asking it to remember buys nothing and costs exactly what it cost the
|
|
56
|
+
* starter, where three of four agents never wrote the `catch` at all.
|
|
57
|
+
*/
|
|
58
|
+
export async function runHandleTask(p, step, deps) {
|
|
59
|
+
try {
|
|
60
|
+
await orchestrate(p, step, deps);
|
|
61
|
+
}
|
|
62
|
+
catch (cause) {
|
|
63
|
+
// Everything this needs is already in `deps` — which is the argument for it
|
|
64
|
+
// living here rather than in each host's `catch`. Four agents in the starter
|
|
65
|
+
// called this function and only one had written that `catch`; the other three
|
|
66
|
+
// carried the 2026-08-19 failure silently. A guard nobody can forget is worth
|
|
67
|
+
// more than a helper everybody must remember.
|
|
68
|
+
await deliverAbandonedTask(step, cause, {
|
|
69
|
+
push: {
|
|
70
|
+
taskId: p.taskId,
|
|
71
|
+
contextId: p.contextId,
|
|
72
|
+
pushUrl: p.pushUrl,
|
|
73
|
+
pushToken: p.pushToken,
|
|
74
|
+
jku: p.jku
|
|
75
|
+
},
|
|
76
|
+
signingKey: deps.signingKey,
|
|
77
|
+
// Resolved inside each closure, never hoisted — see {@link ResolveAgent}.
|
|
78
|
+
saveTask: (task) => deps.resolveAgent(p.identity).saveTask(task),
|
|
79
|
+
// The round never got far enough to say *which* credential or model was at
|
|
80
|
+
// fault, so `failureCopy` has nothing to answer and the policy's own words
|
|
81
|
+
// are the honest ones. The diagnostic is logged instead.
|
|
82
|
+
text: deps.policy.copy.taskFailed,
|
|
83
|
+
sweep: async () => {
|
|
84
|
+
await deps.resolveAgent(p.identity).sweepTaskChildren(p.taskId);
|
|
85
|
+
},
|
|
86
|
+
label: deps.label
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The orchestration proper — every ordinary outcome ends inside here, and
|
|
92
|
+
* anything that escapes is what {@link runHandleTask} turns into a delivered
|
|
93
|
+
* failure.
|
|
94
|
+
*
|
|
95
|
+
* Every `step.do` return here is a small projection — a status, an id, a reply.
|
|
96
|
+
* Never a Subtask row: a step return is capped at 1 MiB and a Subtask carries
|
|
97
|
+
* verbatim history snapshots, so the rows stay in the DO and the Workflow carries
|
|
98
|
+
* references to them.
|
|
99
|
+
*
|
|
100
|
+
* **Step names are durable cache keys.** Everything inside the round loop carries
|
|
101
|
+
* its round for that reason: `turn:<round>`, `deadline:<round>`, `scan:<round>`,
|
|
102
|
+
* `cancel:<round>`. Renaming one silently re-runs its effect on replay — and the
|
|
103
|
+
* recovery path in {@link runHandleTask} runs under its own prefix for the same
|
|
104
|
+
* reason, so a second delivery cannot be handed this one's cached results.
|
|
105
|
+
*/
|
|
106
|
+
async function orchestrate(p, step, deps) {
|
|
107
|
+
const limits = deps.config.mainAgentLimits;
|
|
108
|
+
// Pre-work. Routing is pure, so it needs no step of its own — but it is
|
|
109
|
+
// deliberately *not* resolved here into a value the steps below close over.
|
|
110
|
+
// See {@link ResolveAgent}.
|
|
111
|
+
const agent = () => deps.resolveAgent(p.identity);
|
|
112
|
+
const push = {
|
|
113
|
+
taskId: p.taskId,
|
|
114
|
+
contextId: p.contextId,
|
|
115
|
+
pushUrl: p.pushUrl,
|
|
116
|
+
pushToken: p.pushToken,
|
|
117
|
+
jku: p.jku
|
|
118
|
+
};
|
|
119
|
+
const started = await step.do("working", async () => (await agent().markWorking(p.taskId)) === "ok");
|
|
120
|
+
if (!started)
|
|
121
|
+
return;
|
|
122
|
+
// Main-agent turns spent so far, across every round. Summed from cached step
|
|
123
|
+
// returns, so a replay reconstructs the identical number and the `mode` input
|
|
124
|
+
// below stays deterministic.
|
|
125
|
+
let turnsUsed = 0;
|
|
126
|
+
// The Task's own start, in a step so replays read the original instant rather
|
|
127
|
+
// than restarting the clock — otherwise a Workflow that retried its way through
|
|
128
|
+
// the night would never observe the deadline it had long since passed.
|
|
129
|
+
//
|
|
130
|
+
// When escalation lands, this is the line that needs care: a Task suspended on
|
|
131
|
+
// `step.waitForEvent(...)` must **rebase** it on resume, or a human's thinking
|
|
132
|
+
// time is charged to the agent and a Task that asked a question is dead before
|
|
133
|
+
// the answer arrives. `turnsUsed` needs no such handling — waiting costs none.
|
|
134
|
+
const startedAtMs = await step.do("started", async () => Date.now());
|
|
135
|
+
// At most one round per turn of the budget, **plus one**: an `open` round always
|
|
136
|
+
// spends at least one turn, so `maxTurns` of them exhaust the budget — and the
|
|
137
|
+
// forced-answer round that follows needs an iteration of its own to happen in.
|
|
138
|
+
// Off by one here and a Task of cheap rounds would fall out of the loop with no
|
|
139
|
+
// reply instead of being made to give one.
|
|
140
|
+
for (let round = 0; round <= limits.maxTurns; round++) {
|
|
141
|
+
// The clock is read *inside a step* so its answer is cached with the round:
|
|
142
|
+
// `mode` is a step input, and a replay that re-read `Date.now()` would
|
|
143
|
+
// reconstruct a different one. Time is the budget a Task can spend without
|
|
144
|
+
// spending the other — a round waiting on slow subtasks moves it while
|
|
145
|
+
// `turnsUsed` does not.
|
|
146
|
+
const overdue = await step.do(`deadline:${round}`, async () => Date.now() - startedAtMs >= limits.maxWallMs);
|
|
147
|
+
// Out of turns or out of time ⇒ this round gets no tools at all and must
|
|
148
|
+
// answer. Not a failure mode: it is how a ceiling returns the work instead of
|
|
149
|
+
// dropping it.
|
|
150
|
+
const mode = turnsUsed >= limits.maxTurns || overdue ? "final" : "open";
|
|
151
|
+
if (mode === "final") {
|
|
152
|
+
// Worth its own line: from the outside, a round the budget ended is
|
|
153
|
+
// indistinguishable from a model that simply chose to answer.
|
|
154
|
+
console.warn("[handle-task] task budget spent, forcing an answer", {
|
|
155
|
+
taskId: p.taskId,
|
|
156
|
+
round,
|
|
157
|
+
turnsUsed,
|
|
158
|
+
overdue
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// The main agent decides. `runTaskTurn` persists whatever the round produced
|
|
162
|
+
// — a final reply, or the Subtask rows plus the acknowledgment it already
|
|
163
|
+
// pushed — so this step returns only the verdict plus what it cost. A typed
|
|
164
|
+
// `failed` is a real outcome (both models produced unusable output, with no
|
|
165
|
+
// durable work to fall back on) and routes to failed delivery; a transient
|
|
166
|
+
// fault throws and the step retries, recovering from the durable rows with no
|
|
167
|
+
// second inference.
|
|
168
|
+
const turn = await step.do(`turn:${round}`, turnStep(deps.config), async () => {
|
|
169
|
+
// Projected to a plain object: an RPC return carries a `Disposable` brand a
|
|
170
|
+
// step result cannot serialize. Every branch must carry `turns` — a field
|
|
171
|
+
// this projection drops is a field the budget never sees.
|
|
172
|
+
const result = await agent().runTaskTurn({
|
|
173
|
+
taskId: p.taskId,
|
|
174
|
+
text: p.text,
|
|
175
|
+
identity: p.identity,
|
|
176
|
+
round,
|
|
177
|
+
mode,
|
|
178
|
+
turnsRemaining: limits.maxTurns - turnsUsed,
|
|
179
|
+
push
|
|
180
|
+
});
|
|
181
|
+
if (result.status === "replied")
|
|
182
|
+
return {
|
|
183
|
+
status: result.status,
|
|
184
|
+
reply: result.reply,
|
|
185
|
+
turns: result.turns
|
|
186
|
+
};
|
|
187
|
+
if (result.status === "failed")
|
|
188
|
+
return {
|
|
189
|
+
status: result.status,
|
|
190
|
+
kind: result.kind,
|
|
191
|
+
error: result.error,
|
|
192
|
+
turns: result.turns
|
|
193
|
+
};
|
|
194
|
+
return { status: result.status, turns: result.turns };
|
|
195
|
+
});
|
|
196
|
+
turnsUsed += turn.turns;
|
|
197
|
+
if (turn.status === "canceled")
|
|
198
|
+
return;
|
|
199
|
+
// The round produced no answer. `kind` is the whole difference between the
|
|
200
|
+
// two ways that happens — models that could not do it, versus a fault that
|
|
201
|
+
// stopped the round on its first attempt and that only a human can clear —
|
|
202
|
+
// and it exists to be turned into words the reader can act on. Same
|
|
203
|
+
// delivery either way; the diagnostic is logged, never shown.
|
|
204
|
+
if (turn.status === "failed") {
|
|
205
|
+
console.error("[handle-task] round failed", {
|
|
206
|
+
taskId: p.taskId,
|
|
207
|
+
round,
|
|
208
|
+
kind: turn.kind,
|
|
209
|
+
error: turn.error
|
|
210
|
+
});
|
|
211
|
+
await deliver(p, step, agent, null, deps, {
|
|
212
|
+
kind: turn.kind,
|
|
213
|
+
detail: turn.error
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (turn.status === "replied") {
|
|
218
|
+
await deliver(p, step, agent, turn.reply, deps);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// Delegated: run this round's Subtasks, then loop and let the model decide
|
|
222
|
+
// again.
|
|
223
|
+
const executed = await executeSubtasks(p, step, agent, round, push);
|
|
224
|
+
if (executed === "canceled")
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Unreachable: a `final` round is handed only `final_reply`, so it either
|
|
228
|
+
// answers or fails, and both return above. Reaching here means a round
|
|
229
|
+
// delegated with no turns left to do it with.
|
|
230
|
+
console.error("[handle-task] round budget exhausted without a reply", {
|
|
231
|
+
taskId: p.taskId
|
|
232
|
+
});
|
|
233
|
+
await deliver(p, step, agent, null, deps);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Run every Subtask one round delegated, concurrently, to termination.
|
|
237
|
+
*
|
|
238
|
+
* **One pass is the whole thing.** A round's Subtasks are independent of one
|
|
239
|
+
* another, so they are all runnable the moment they exist, and `runBranch` is
|
|
240
|
+
* contractually obliged to leave its row terminal — it resolves a deterministic
|
|
241
|
+
* failure itself and has a `fail:<id>` backstop once the retries are gone. So
|
|
242
|
+
* there is nothing left to re-scan afterwards, and no way for this to make no
|
|
243
|
+
* progress. Sequencing between units of work is the round loop's job.
|
|
244
|
+
*
|
|
245
|
+
* Both step names carry the round, because step names are durable cache keys: two
|
|
246
|
+
* rounds of the same Task reusing `scan` would replay the first round's cached
|
|
247
|
+
* answer into the second.
|
|
248
|
+
*/
|
|
249
|
+
async function executeSubtasks(p, step, agent, round, push) {
|
|
250
|
+
// One durable step: `scanSubtasks` reports cancellation and returns the ids
|
|
251
|
+
// still owing an outcome — one round trip, one consistent answer. It writes
|
|
252
|
+
// nothing, so a replay that re-runs it costs only the read.
|
|
253
|
+
const scan = await step.do(`scan:${round}`, async () => {
|
|
254
|
+
const result = await agent().scanSubtasks(p.taskId, round);
|
|
255
|
+
return result.canceled
|
|
256
|
+
? { canceled: true, ids: [] }
|
|
257
|
+
: { canceled: false, ids: result.ids };
|
|
258
|
+
});
|
|
259
|
+
if (scan.canceled) {
|
|
260
|
+
await step.do(`cancel:${round}`, async () => {
|
|
261
|
+
await agent().cancelPendingSubtasks(p.taskId);
|
|
262
|
+
});
|
|
263
|
+
return "canceled";
|
|
264
|
+
}
|
|
265
|
+
// Every Subtask runs concurrently — the per-round Subtask maximum is the only
|
|
266
|
+
// fan-out bound. `runBranch` never rejects, so a single branch cannot fast-fail
|
|
267
|
+
// `Promise.all` and strand its siblings' durable results.
|
|
268
|
+
//
|
|
269
|
+
// A cancellation arriving mid-pass is still honored, just not from here:
|
|
270
|
+
// `onTaskCanceled` aborts the live children *and* transitions every row still
|
|
271
|
+
// `pending` in the same sweep, and `executeSubtaskChunk` re-checks before
|
|
272
|
+
// publishing. That transition is what lets this pass end without a second
|
|
273
|
+
// scan. Without it, a branch whose RPC had not yet claimed its row when the
|
|
274
|
+
// cancellation landed would return terminal while leaving the row `pending`,
|
|
275
|
+
// and — since the next round's turn reports `canceled` and the workflow exits
|
|
276
|
+
// — nothing would resolve it before the 30-day cleanup.
|
|
277
|
+
await Promise.all(scan.ids.map((id) => runBranch(p, step, agent, id, push)));
|
|
278
|
+
return "done";
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Run one Subtask to termination as a sequence of durable **chunk** steps, and
|
|
282
|
+
* make sure the row ends terminal either way.
|
|
283
|
+
*
|
|
284
|
+
* `executeSubtaskChunk(id, chunk)` advances one chunk: a single-chunk recipe is
|
|
285
|
+
* `done` on chunk 0 (step `execute:<id>`); a long recipe yields `done: false` and
|
|
286
|
+
* the loop runs the next chunk (`execute:<id>:chunk:<n>`) until it terminates.
|
|
287
|
+
* Each chunk is its own retryable step, and the child resumes from its
|
|
288
|
+
* checkpoint — so no step approaches the {@link CHUNK_STEP} timeout. `CHUNK_SOFT_MS`
|
|
289
|
+
* is what holds that true, and is sized against it rather than the other way
|
|
290
|
+
* round; a boundary here is not free, so it wants to be rare, not frequent.
|
|
291
|
+
*
|
|
292
|
+
* It resolves a deterministic branch failure into a `failed` row itself and
|
|
293
|
+
* throws only on a transient fault (retry me) or a lifecycle bug. So a throw that
|
|
294
|
+
* survives every retry — or a run that never terminates within the chunk budget —
|
|
295
|
+
* means nobody is left to resolve this row: fail *the branch* and let the next
|
|
296
|
+
* round disclose the gap, rather than discarding the durable work its siblings
|
|
297
|
+
* finished.
|
|
298
|
+
*
|
|
299
|
+
* What bounds a branch is its Recipe's turns and wall clock, both enforced inside
|
|
300
|
+
* the child, both ending in a report rather than a kill. `MAX_CHUNKS_PER_BRANCH`
|
|
301
|
+
* is a platform backstop held unreachable by design, so the `failSubtask` below
|
|
302
|
+
* should never fire — if it does, a Recipe has been given more turns than the cap
|
|
303
|
+
* allows.
|
|
304
|
+
*
|
|
305
|
+
* Step ids are unique across rounds (SQLite assigns them), so these names need no
|
|
306
|
+
* round prefix.
|
|
307
|
+
*/
|
|
308
|
+
async function runBranch(p, step, agent, id, push) {
|
|
309
|
+
try {
|
|
310
|
+
for (let chunk = 0; chunk < MAX_CHUNKS_PER_BRANCH; chunk++) {
|
|
311
|
+
// Chunk 0 keeps the plain `execute:<id>` step name so single-chunk branches
|
|
312
|
+
// replay identically; later chunks append `:chunk:<n>`.
|
|
313
|
+
const stepName = chunk === 0 ? `execute:${id}` : `execute:${id}:chunk:${chunk}`;
|
|
314
|
+
const done = await step.do(stepName, CHUNK_STEP, async () => {
|
|
315
|
+
// The DO posts any progress itself; the step returns only the verdict.
|
|
316
|
+
const outcome = await agent().executeSubtaskChunk(id, chunk, push);
|
|
317
|
+
return outcome.done;
|
|
318
|
+
});
|
|
319
|
+
if (done)
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// Unreachable while every Recipe's `maxTurns` stays under the cap: a chunk
|
|
323
|
+
// that yields always advanced a turn, so the budget summary comes first.
|
|
324
|
+
console.error("[handle-task] subtask exceeded its chunk budget", {
|
|
325
|
+
taskId: p.taskId,
|
|
326
|
+
subtaskId: id
|
|
327
|
+
});
|
|
328
|
+
await step.do(`fail:${id}`, async () => {
|
|
329
|
+
await agent().failSubtask(id, `execution exceeded ${MAX_CHUNKS_PER_BRANCH} chunks`);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
console.error("[handle-task] subtask execution exhausted retries", {
|
|
334
|
+
taskId: p.taskId,
|
|
335
|
+
subtaskId: id,
|
|
336
|
+
err: String(err)
|
|
337
|
+
});
|
|
338
|
+
await step.do(`fail:${id}`, async () => {
|
|
339
|
+
await agent().failSubtask(id, `execution exhausted retries: ${String(err)}`);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Persist the terminal Task, then notify the gatekeeper. A null `reply` delivers a
|
|
345
|
+
* `failed` Task with the policy's user-safe text; the diagnostic is already
|
|
346
|
+
* logged. Given a `failure`, the host's {@link HandleTaskDeps.failureCopy} may
|
|
347
|
+
* replace that text — same delivery, different words.
|
|
348
|
+
*
|
|
349
|
+
* `failure` is optional because only a round's own inference carries a kind. The
|
|
350
|
+
* other path here — a budget that ran out mid-delegation — is not a model failure
|
|
351
|
+
* and is deliberately not given a kind of its own until something needs to tell
|
|
352
|
+
* it apart.
|
|
353
|
+
*
|
|
354
|
+
* The delivery itself is {@link deliverTerminalTask}, which is shared with agents
|
|
355
|
+
* that never delegate. What is a round's own is the two things passed to it: the
|
|
356
|
+
* choice of terminal Task, and the child sweep.
|
|
357
|
+
*/
|
|
358
|
+
async function deliver(p, step, agent, reply, deps, failure) {
|
|
359
|
+
// Resolved outside the step body so a replay cannot take a different branch
|
|
360
|
+
// than the write it is replaying.
|
|
361
|
+
const failedText = (failure && deps.failureCopy?.(failure.kind, failure.detail)) ||
|
|
362
|
+
deps.policy.copy.taskFailed;
|
|
363
|
+
await deliverTerminalTask(step, {
|
|
364
|
+
push: {
|
|
365
|
+
taskId: p.taskId,
|
|
366
|
+
contextId: p.contextId,
|
|
367
|
+
pushUrl: p.pushUrl,
|
|
368
|
+
pushToken: p.pushToken,
|
|
369
|
+
jku: p.jku
|
|
370
|
+
},
|
|
371
|
+
signingKey: deps.signingKey,
|
|
372
|
+
// `agent()` inside the body, never hoisted: a stub is a live connection and
|
|
373
|
+
// a severed one never reconnects.
|
|
374
|
+
saveTask: (task) => agent().saveTask(task),
|
|
375
|
+
terminal: () => reply !== null
|
|
376
|
+
? buildCompletedTask(p.taskId, p.contextId, reply)
|
|
377
|
+
: buildFailedTask(p.taskId, p.contextId, failedText),
|
|
378
|
+
// Sweep this Task's managed children now that it is terminal and every
|
|
379
|
+
// `execute` step has unwound. Deleting them here — rather than right after
|
|
380
|
+
// each successful chunk — keeps `deleteSubAgent`'s facet-abort from landing
|
|
381
|
+
// on a still-open `executeChunk` RPC, which telemetry mis-records as a
|
|
382
|
+
// failure. Best-effort and idempotent, so it is safe on replay.
|
|
383
|
+
sweep: async () => {
|
|
384
|
+
await agent().sweepTaskChildren(p.taskId);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { ToolSet } from "ai";
|
|
2
|
+
import type { SessionMessage } from "agents/experimental/memory/session";
|
|
3
|
+
import { type CoreConfig, type CoreConfigOverrides } from "../config.js";
|
|
4
|
+
import { type AgentPlugin, type EnrichResultContext, type MainAgentToolContext, type ResolveRuntimeContext, type ToolFamilyBuilder, type TurnGateContext } from "../contract/plugin.js";
|
|
5
|
+
import type { PluginStore } from "../db/db.js";
|
|
6
|
+
import { type WorkspaceBacking } from "../subagent/workspace.js";
|
|
7
|
+
import type { RecipePolicy } from "../contract/validation.js";
|
|
8
|
+
import { type SubtaskTypeRegistry } from "../subtasks/subtask-types.js";
|
|
9
|
+
import type { RecipeExecutionResult, SubtaskRuntime } from "../subtasks/types.js";
|
|
10
|
+
export { buildRecipeTools, collectToolFamilies } from "./tool-families.js";
|
|
11
|
+
/**
|
|
12
|
+
* The agent runtime: everything that used to be a module-level constant,
|
|
13
|
+
* resolved once per Durable Object instance from the host's config and its
|
|
14
|
+
* installed plugins.
|
|
15
|
+
*
|
|
16
|
+
* This is the whole point of the package split. In the predecessor repo the
|
|
17
|
+
* subtask registry was imported at module scope and every derived value — the
|
|
18
|
+
* type map, the delegate tool's enum and description, the round contract, the
|
|
19
|
+
* known-tool-family allowlist — was computed at *import time*. That made the
|
|
20
|
+
* registry unoverridable, pulled every domain's module into every bundle, and
|
|
21
|
+
* could not read `env`, which does not exist at module scope on Workers.
|
|
22
|
+
*
|
|
23
|
+
* Build it in `onStart`:
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* async onStart() {
|
|
27
|
+
* this.runtime = createAgentRuntime({
|
|
28
|
+
* config: { model: { chatModelId: "…" } },
|
|
29
|
+
* plugins: plugins(this.env)
|
|
30
|
+
* });
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export interface AgentRuntime {
|
|
35
|
+
config: CoreConfig;
|
|
36
|
+
plugins: readonly AgentPlugin[];
|
|
37
|
+
/** The installed subtask types — what `delegate` may name. */
|
|
38
|
+
types: SubtaskTypeRegistry;
|
|
39
|
+
/** Every tool family the installed plugins registered, by name. */
|
|
40
|
+
toolFamilies: ReadonlyMap<string, ToolFamilyBuilder>;
|
|
41
|
+
/** The capability boundary `validateRecipe` enforces. */
|
|
42
|
+
policy: RecipePolicy;
|
|
43
|
+
/** Plugin-owned stores, to hand to `new AgentDB(storage, { stores })`. */
|
|
44
|
+
stores: readonly PluginStore[];
|
|
45
|
+
/** Every binding and secret the installed plugins require of the host. */
|
|
46
|
+
requirements: {
|
|
47
|
+
secrets: string[];
|
|
48
|
+
bindings: string[];
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The subagent workspace backend — the one plugin that declared it, or an
|
|
52
|
+
* in-memory fallback when none did. Always defined, so a host writes
|
|
53
|
+
* `workspaceBacking: runtime.workspaceBacking` into its `SubagentRuntime`
|
|
54
|
+
* unconditionally.
|
|
55
|
+
*/
|
|
56
|
+
workspaceBacking: (sql: SqlStorage, name: () => string | undefined) => WorkspaceBacking;
|
|
57
|
+
/** The plugin that declared a subtask type, or null. */
|
|
58
|
+
pluginForType(type: string): AgentPlugin | null;
|
|
59
|
+
/** Tools the installed plugins offer the *main* agent, merged. */
|
|
60
|
+
mainAgentTools(ctx: MainAgentToolContext): Promise<ToolSet>;
|
|
61
|
+
/**
|
|
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.
|
|
66
|
+
*/
|
|
67
|
+
renderCapabilities(): string;
|
|
68
|
+
/**
|
|
69
|
+
* Ask every plugin declaring {@link AgentPlugin.shouldHandleTurn} whether this
|
|
70
|
+
* turn should run. `true` when none declares one, and `false` if any single
|
|
71
|
+
* gate declines.
|
|
72
|
+
*
|
|
73
|
+
* Never rejects: a gate that fails is logged against its plugin key and
|
|
74
|
+
* counted as `true`, because the failure mode of a broken gate must be a noisy
|
|
75
|
+
* agent, never a silent one.
|
|
76
|
+
*/
|
|
77
|
+
shouldHandleTurn(ctx: TurnGateContext): Promise<boolean>;
|
|
78
|
+
/**
|
|
79
|
+
* Announce the messages a compaction is folding into a summary to every plugin
|
|
80
|
+
* declaring {@link AgentPlugin.onMessagesDisplaced}. Pass it straight to
|
|
81
|
+
* `buildAgentSession`'s option of the same name — it reads no `this`, so the
|
|
82
|
+
* bare reference works.
|
|
83
|
+
*
|
|
84
|
+
* Never rejects: listeners are fanned out with `Promise.allSettled` and each
|
|
85
|
+
* rejection is logged against the plugin key that caused it.
|
|
86
|
+
*/
|
|
87
|
+
onMessagesDisplaced(messages: SessionMessage[]): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the session state an execution needs, by asking the plugin that owns
|
|
90
|
+
* its type. Returns `{}` for a type whose plugin declares no
|
|
91
|
+
* `resolveRuntime` — most of them.
|
|
92
|
+
*/
|
|
93
|
+
resolveRuntime(ctx: ResolveRuntimeContext): Promise<SubtaskRuntime>;
|
|
94
|
+
/** Let the owning plugin amend a terminal result before it is persisted. */
|
|
95
|
+
enrichResult(ctx: EnrichResultContext, result: RecipeExecutionResult): Promise<RecipeExecutionResult>;
|
|
96
|
+
/** Let the owning plugin release whatever `resolveRuntime` acquired. */
|
|
97
|
+
onAbort(ctx: ResolveRuntimeContext): Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
export interface CreateAgentRuntimeOptions {
|
|
100
|
+
plugins: readonly AgentPlugin[];
|
|
101
|
+
/**
|
|
102
|
+
* Required, because {@link CoreConfigOverrides} requires a model pair and core
|
|
103
|
+
* ships no default for it. Everything else in it stays optional.
|
|
104
|
+
*/
|
|
105
|
+
config: CoreConfigOverrides;
|
|
106
|
+
/**
|
|
107
|
+
* Verify that every secret and binding the plugins declared is actually
|
|
108
|
+
* present, given the Worker `env`. Off by default because core cannot know
|
|
109
|
+
* which of a consumer's bindings are optional; pass `env` to switch it on.
|
|
110
|
+
*
|
|
111
|
+
* Typed `object`, not `Record<string, unknown>`, and that is not looseness.
|
|
112
|
+
* `Env` is the ambient interface `wrangler types` generates into a consumer's
|
|
113
|
+
* `worker-configuration.d.ts`; an interface has no index signature, so it does
|
|
114
|
+
* not satisfy `Record<string, unknown>` and every consumer would have to cast
|
|
115
|
+
* their own `this.env` to pass it. Requiring a cast to opt into a *check* is
|
|
116
|
+
* how the check goes unused.
|
|
117
|
+
*/
|
|
118
|
+
env?: object;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Thrown when the installed plugins and the host disagree — a contract-version
|
|
122
|
+
* skew, a duplicate key, or a missing binding. Always at DO start, never mid-request.
|
|
123
|
+
*/
|
|
124
|
+
export declare class RuntimeSetupError extends Error {
|
|
125
|
+
constructor(message: string);
|
|
126
|
+
}
|
|
127
|
+
export declare function createAgentRuntime(options: CreateAgentRuntimeOptions): AgentRuntime;
|