@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,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dynamicagents/core/round` — the delegating round loop.
|
|
3
|
+
*
|
|
4
|
+
* A round agent answers the user or hands work to isolated subagents, waits for
|
|
5
|
+
* their durable results, and decides again. This subpath ships the whole
|
|
6
|
+
* mechanism of that: the round loop and its primary→fallback→repair ladder
|
|
7
|
+
* ({@link runTurn}), the durable Workflow orchestration that runs a round's
|
|
8
|
+
* subtasks concurrently ({@link runHandleTask}), the Durable Object body ({@link
|
|
9
|
+
* RoundAgentBase}), and the subagent facet host ({@link RecipeSubagentHost}).
|
|
10
|
+
*
|
|
11
|
+
* **Opt-in, and its own subpath on purpose.** An agent whose turn is a single
|
|
12
|
+
* inference — one that never delegates — imports none of this and carries none of
|
|
13
|
+
* it in its bundle. Core's root barrel does not re-export it.
|
|
14
|
+
*
|
|
15
|
+
* **Core still ships no prompt copy.** Everything the model and the user read
|
|
16
|
+
* comes from the {@link RoundPolicy} an agent supplies: the round contract, the
|
|
17
|
+
* budget-spent note, and the three user-facing strings. That is the line — core
|
|
18
|
+
* owns the machine, you own the words.
|
|
19
|
+
*/
|
|
20
|
+
export type { RoundPolicy } from "./policy.js";
|
|
21
|
+
export { RoundAgentBase } from "./agent.js";
|
|
22
|
+
export { RecipeSubagentHost, type SubagentClass } from "./subagent.js";
|
|
23
|
+
export { runHandleTask, type HandleTaskDeps, type HandleTaskParams } from "./workflow.js";
|
|
24
|
+
export type { NonRecoverableKind, RoundFailureKind } from "../agent/inference.js";
|
|
25
|
+
export { buildTurnInstructions, joinSuccessfulBranches, renderTurnMessages, runTurn, type RoundMode, type RunTurnArgs, type RunTurnOutcome, type TurnInstructions } from "./turn.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dynamicagents/core/round` — the delegating round loop.
|
|
3
|
+
*
|
|
4
|
+
* A round agent answers the user or hands work to isolated subagents, waits for
|
|
5
|
+
* their durable results, and decides again. This subpath ships the whole
|
|
6
|
+
* mechanism of that: the round loop and its primary→fallback→repair ladder
|
|
7
|
+
* ({@link runTurn}), the durable Workflow orchestration that runs a round's
|
|
8
|
+
* subtasks concurrently ({@link runHandleTask}), the Durable Object body ({@link
|
|
9
|
+
* RoundAgentBase}), and the subagent facet host ({@link RecipeSubagentHost}).
|
|
10
|
+
*
|
|
11
|
+
* **Opt-in, and its own subpath on purpose.** An agent whose turn is a single
|
|
12
|
+
* inference — one that never delegates — imports none of this and carries none of
|
|
13
|
+
* it in its bundle. Core's root barrel does not re-export it.
|
|
14
|
+
*
|
|
15
|
+
* **Core still ships no prompt copy.** Everything the model and the user read
|
|
16
|
+
* comes from the {@link RoundPolicy} an agent supplies: the round contract, the
|
|
17
|
+
* budget-spent note, and the three user-facing strings. That is the line — core
|
|
18
|
+
* owns the machine, you own the words.
|
|
19
|
+
*/
|
|
20
|
+
export { RoundAgentBase } from "./agent.js";
|
|
21
|
+
export { RecipeSubagentHost } from "./subagent.js";
|
|
22
|
+
export { runHandleTask } from "./workflow.js";
|
|
23
|
+
export { buildTurnInstructions, joinSuccessfulBranches, renderTurnMessages, runTurn } from "./turn.js";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { AgentLimits } from "../config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Everything about a round loop that is **yours**, not core's.
|
|
4
|
+
*
|
|
5
|
+
* The round loop in this subpath is mechanism: concurrent subtask execution,
|
|
6
|
+
* chunked subagent runs, cancellation ordering, idempotent recovery, the
|
|
7
|
+
* primary→fallback→repair ladder. None of it varies between agents, and every
|
|
8
|
+
* place it *did* vary between two agents in one repo turned out to be a bug.
|
|
9
|
+
*
|
|
10
|
+
* What genuinely varies is what the model is told and what the user reads — and
|
|
11
|
+
* core ships no prompt copy, deliberately, so that no run ever executes under an
|
|
12
|
+
* identity nobody chose. This interface is that boundary, made explicit: supply
|
|
13
|
+
* the words, get the machine.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* export const policy: RoundPolicy = {
|
|
17
|
+
* roundContract: ({ typeKeys, maxSubtasks }) => `…`,
|
|
18
|
+
* finalRoundNote: (limits) => `…`,
|
|
19
|
+
* copy: {
|
|
20
|
+
* taskFailed: "Sorry — something went wrong handling that request.",
|
|
21
|
+
* recoveredReply: "Working on your request.",
|
|
22
|
+
* partialNote: "Some parts of this request could not be completed…"
|
|
23
|
+
* }
|
|
24
|
+
* };
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Nothing here is optional and nothing has a default. A stub that returned an
|
|
28
|
+
* empty contract would produce a round the model has no way to end correctly,
|
|
29
|
+
* and a lending default would be exactly the house prompt copy core refuses to
|
|
30
|
+
* have.
|
|
31
|
+
*
|
|
32
|
+
* ## Each prompt string owns its own leading separator
|
|
33
|
+
*
|
|
34
|
+
* {@link roundContract} and {@link finalRoundNote} are concatenated directly
|
|
35
|
+
* onto text that came from somewhere else — the soul, the caller context, and
|
|
36
|
+
* for `finalRoundNote` the open contract itself. The composition adds nothing
|
|
37
|
+
* between them, so **start each with a blank line** (`\n\n`, or a template
|
|
38
|
+
* literal opening on an empty line, which is what the starter does):
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* roundContract: () => `
|
|
42
|
+
*
|
|
43
|
+
* # Answering this request
|
|
44
|
+
* …`
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* Return `"# Answering this request…"` with no leading newline and the model
|
|
48
|
+
* reads `Calling workspace: 1.# Answering this request` — a run-together line
|
|
49
|
+
* that costs nothing to produce and is invisible in every test that does not
|
|
50
|
+
* assert on the rendered prompt.
|
|
51
|
+
*
|
|
52
|
+
* Core does not insert the separator for you, because these are *your* sections
|
|
53
|
+
* and a section that cannot control its own spacing cannot control its shape —
|
|
54
|
+
* a policy that legitimately wants a single newline, or continues the previous
|
|
55
|
+
* paragraph, has no way to say so once core has decided.
|
|
56
|
+
*/
|
|
57
|
+
export interface RoundPolicy {
|
|
58
|
+
/**
|
|
59
|
+
* The round contract: how a round ends, and what `delegate` takes. Appended to
|
|
60
|
+
* the soul and the caller context on every round.
|
|
61
|
+
*
|
|
62
|
+
* `typeKeys` is the installed subtask types — the only values `delegate` may
|
|
63
|
+
* name. Per-type guidance is *not* written here: each type declares its own
|
|
64
|
+
* via `SubtaskTypeSpec.delegationGuidance`, and the loop appends it, so this
|
|
65
|
+
* text names no domain.
|
|
66
|
+
*/
|
|
67
|
+
roundContract(ctx: {
|
|
68
|
+
typeKeys: readonly string[];
|
|
69
|
+
maxSubtasks: number;
|
|
70
|
+
}): string;
|
|
71
|
+
/**
|
|
72
|
+
* Appended when the task has spent its budget. That round is handed no work
|
|
73
|
+
* tools and no `delegate`, so this explains a constraint the model can already
|
|
74
|
+
* see rather than imposing one.
|
|
75
|
+
*/
|
|
76
|
+
finalRoundNote(limits: AgentLimits): string;
|
|
77
|
+
/** The strings a user can actually read. */
|
|
78
|
+
copy: {
|
|
79
|
+
/**
|
|
80
|
+
* User-facing text on a failed Task. The diagnostic is logged separately —
|
|
81
|
+
* this is what the person sees.
|
|
82
|
+
*/
|
|
83
|
+
taskFailed: string;
|
|
84
|
+
/**
|
|
85
|
+
* Stand-in acknowledgement for the unreachable case where a round's subtasks
|
|
86
|
+
* are durable but its acknowledgement is not in the Session. Neutral by
|
|
87
|
+
* design: the work is valid and running, so the user gets an honest
|
|
88
|
+
* acknowledgement rather than a failed Task.
|
|
89
|
+
*/
|
|
90
|
+
recoveredReply: string;
|
|
91
|
+
/**
|
|
92
|
+
* Appended when a deterministic join has to disclose gaps — both models
|
|
93
|
+
* failed, but earlier branches succeeded and their results are worth
|
|
94
|
+
* delivering.
|
|
95
|
+
*/
|
|
96
|
+
partialNote: string;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { AgentPlugin } from "../contract/plugin.js";
|
|
2
|
+
import type { CoreConfigOverrides, ModelConfig } from "../config.js";
|
|
3
|
+
import type { AiEnv } from "../env.js";
|
|
4
|
+
import type { ModelRuntime } from "../agent/model.js";
|
|
5
|
+
import { RecipeSubagentBase, type SubagentRuntime } from "../subagent/index.js";
|
|
6
|
+
import type { PluginHost } from "../host/plugin-host.js";
|
|
7
|
+
/**
|
|
8
|
+
* The host half of a subagent facet: the same plugins as its parent, assembled
|
|
9
|
+
* into the {@link SubagentRuntime} the base class executes recipes with.
|
|
10
|
+
*
|
|
11
|
+
* `RecipeSubagentBase` already ships the whole body — resumable chunks, the
|
|
12
|
+
* terminal-result cache keyed by request fingerprint, workspace wiring,
|
|
13
|
+
* cancellation, the fingerprint-mismatch contract. All that was ever missing is
|
|
14
|
+
* this assembly, and a Durable Object class is constructed by the runtime, so it
|
|
15
|
+
* cannot take constructor arguments. The two abstract methods are that seam.
|
|
16
|
+
*
|
|
17
|
+
* ## Why the facet builds its own runtime instead of asking its parent
|
|
18
|
+
*
|
|
19
|
+
* The obvious move is to read it off the parent, which already has one. It does
|
|
20
|
+
* not work: the SDK reaches a parent through `this.parentAgent(Cls)`, which is an
|
|
21
|
+
* **RPC stub**, and a `SubagentRuntime` is mostly functions — `models`, the
|
|
22
|
+
* `toolFamilies` builder map, `workspaceBacking`. None of that survives
|
|
23
|
+
* serialization, so the call would return a shape that type-checks and is inert.
|
|
24
|
+
*
|
|
25
|
+
* So each agent gets its own facet class over this base, building the same
|
|
26
|
+
* runtime from the same `plugins.ts` its parent uses:
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* export class MySubagent extends RecipeSubagentHost<Env> {
|
|
30
|
+
* protected agentConfig() { return MY_CONFIG; }
|
|
31
|
+
* protected agentPlugins(host: PluginHost<Env>) { return plugins(host); }
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* It must be a **named, exported** class, not one produced by a factory: the
|
|
36
|
+
* framework resolves a facet through `ctx.exports[this.constructor.name]`, so an
|
|
37
|
+
* anonymous class breaks the lookup, and so does a bundler that minifies class
|
|
38
|
+
* names.
|
|
39
|
+
*
|
|
40
|
+
* ## Not a bound Durable Object
|
|
41
|
+
*
|
|
42
|
+
* A facet is created beneath its calling agent, so it needs no wrangler binding
|
|
43
|
+
* and no `new_sqlite_classes` entry — only an export from the Worker entry so
|
|
44
|
+
* `ctx.exports` can resolve it. It *does* need a test-only binding, because the
|
|
45
|
+
* Vitest pool only marks bound classes as DO classes.
|
|
46
|
+
*/
|
|
47
|
+
export declare abstract class RecipeSubagentHost<TEnv extends Cloudflare.Env & AiEnv = Cloudflare.Env & AiEnv> extends RecipeSubagentBase<TEnv> {
|
|
48
|
+
private _rt?;
|
|
49
|
+
/** This facet's agent config — the same object its parent DO passes. */
|
|
50
|
+
protected abstract agentConfig(): CoreConfigOverrides;
|
|
51
|
+
/** This facet's plugins — the same list its parent DO installs. */
|
|
52
|
+
protected abstract agentPlugins(host: PluginHost<TEnv>): AgentPlugin[];
|
|
53
|
+
/**
|
|
54
|
+
* Built once per facet instance. The base calls this per RPC and documents
|
|
55
|
+
* that an implementation building anything expensive should memoize its own.
|
|
56
|
+
*/
|
|
57
|
+
protected subagentRuntime(): SubagentRuntime;
|
|
58
|
+
/**
|
|
59
|
+
* Which provider this facet's chunks run on. Mirrors
|
|
60
|
+
* {@link file://../host/agent.ts DynamicAgent.modelRuntime}, and **must be
|
|
61
|
+
* overridden to match it** — a facet that keeps the Workers AI default while
|
|
62
|
+
* its parent runs on another provider would silently execute every subtask on
|
|
63
|
+
* a different model than the round that delegated it.
|
|
64
|
+
*
|
|
65
|
+
* The two seams take the same arguments precisely so that keeping them in step
|
|
66
|
+
* needs no discipline: write the provider once as a
|
|
67
|
+
* {@link file://../agent/model.ts ModelRuntimeFactory} and have both return
|
|
68
|
+
* it. Two hand-copied runtime-construction bodies is what this shape exists to
|
|
69
|
+
* stop, because nothing type-checks their agreement.
|
|
70
|
+
*
|
|
71
|
+
* Note the cheapest way to satisfy this is to override *neither* seam, which
|
|
72
|
+
* is what an agent on core's default does.
|
|
73
|
+
*
|
|
74
|
+
* Takes the model config rather than reading `this.config`, because the facet
|
|
75
|
+
* resolves its config inside `buildRuntime` and this is called from there.
|
|
76
|
+
*/
|
|
77
|
+
protected modelRuntime(model: ModelConfig): ModelRuntime;
|
|
78
|
+
private buildRuntime;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A **concrete** facet constructor, as `subAgent()` requires one.
|
|
82
|
+
*
|
|
83
|
+
* {@link RecipeSubagentHost} is abstract, and `subAgent()` rightly refuses an
|
|
84
|
+
* abstract class — it is what constructs one. So the seam on the agent side is
|
|
85
|
+
* typed as this: any named subclass below it.
|
|
86
|
+
*/
|
|
87
|
+
export type SubagentClass = new (...args: ConstructorParameters<typeof RecipeSubagentBase>) => RecipeSubagentBase;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createAgentRuntime } from "../runtime/index.js";
|
|
2
|
+
import { workersAIModels } from "../agent/workers-ai/index.js";
|
|
3
|
+
import { RecipeSubagentBase } from "../subagent/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* The host half of a subagent facet: the same plugins as its parent, assembled
|
|
6
|
+
* into the {@link SubagentRuntime} the base class executes recipes with.
|
|
7
|
+
*
|
|
8
|
+
* `RecipeSubagentBase` already ships the whole body — resumable chunks, the
|
|
9
|
+
* terminal-result cache keyed by request fingerprint, workspace wiring,
|
|
10
|
+
* cancellation, the fingerprint-mismatch contract. All that was ever missing is
|
|
11
|
+
* this assembly, and a Durable Object class is constructed by the runtime, so it
|
|
12
|
+
* cannot take constructor arguments. The two abstract methods are that seam.
|
|
13
|
+
*
|
|
14
|
+
* ## Why the facet builds its own runtime instead of asking its parent
|
|
15
|
+
*
|
|
16
|
+
* The obvious move is to read it off the parent, which already has one. It does
|
|
17
|
+
* not work: the SDK reaches a parent through `this.parentAgent(Cls)`, which is an
|
|
18
|
+
* **RPC stub**, and a `SubagentRuntime` is mostly functions — `models`, the
|
|
19
|
+
* `toolFamilies` builder map, `workspaceBacking`. None of that survives
|
|
20
|
+
* serialization, so the call would return a shape that type-checks and is inert.
|
|
21
|
+
*
|
|
22
|
+
* So each agent gets its own facet class over this base, building the same
|
|
23
|
+
* runtime from the same `plugins.ts` its parent uses:
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* export class MySubagent extends RecipeSubagentHost<Env> {
|
|
27
|
+
* protected agentConfig() { return MY_CONFIG; }
|
|
28
|
+
* protected agentPlugins(host: PluginHost<Env>) { return plugins(host); }
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* It must be a **named, exported** class, not one produced by a factory: the
|
|
33
|
+
* framework resolves a facet through `ctx.exports[this.constructor.name]`, so an
|
|
34
|
+
* anonymous class breaks the lookup, and so does a bundler that minifies class
|
|
35
|
+
* names.
|
|
36
|
+
*
|
|
37
|
+
* ## Not a bound Durable Object
|
|
38
|
+
*
|
|
39
|
+
* A facet is created beneath its calling agent, so it needs no wrangler binding
|
|
40
|
+
* and no `new_sqlite_classes` entry — only an export from the Worker entry so
|
|
41
|
+
* `ctx.exports` can resolve it. It *does* need a test-only binding, because the
|
|
42
|
+
* Vitest pool only marks bound classes as DO classes.
|
|
43
|
+
*/
|
|
44
|
+
export class RecipeSubagentHost extends RecipeSubagentBase {
|
|
45
|
+
_rt;
|
|
46
|
+
/**
|
|
47
|
+
* Built once per facet instance. The base calls this per RPC and documents
|
|
48
|
+
* that an implementation building anything expensive should memoize its own.
|
|
49
|
+
*/
|
|
50
|
+
subagentRuntime() {
|
|
51
|
+
return (this._rt ??= this.buildRuntime());
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Which provider this facet's chunks run on. Mirrors
|
|
55
|
+
* {@link file://../host/agent.ts DynamicAgent.modelRuntime}, and **must be
|
|
56
|
+
* overridden to match it** — a facet that keeps the Workers AI default while
|
|
57
|
+
* its parent runs on another provider would silently execute every subtask on
|
|
58
|
+
* a different model than the round that delegated it.
|
|
59
|
+
*
|
|
60
|
+
* The two seams take the same arguments precisely so that keeping them in step
|
|
61
|
+
* needs no discipline: write the provider once as a
|
|
62
|
+
* {@link file://../agent/model.ts ModelRuntimeFactory} and have both return
|
|
63
|
+
* it. Two hand-copied runtime-construction bodies is what this shape exists to
|
|
64
|
+
* stop, because nothing type-checks their agreement.
|
|
65
|
+
*
|
|
66
|
+
* Note the cheapest way to satisfy this is to override *neither* seam, which
|
|
67
|
+
* is what an agent on core's default does.
|
|
68
|
+
*
|
|
69
|
+
* Takes the model config rather than reading `this.config`, because the facet
|
|
70
|
+
* resolves its config inside `buildRuntime` and this is called from there.
|
|
71
|
+
*/
|
|
72
|
+
modelRuntime(model) {
|
|
73
|
+
return workersAIModels(this.env, model);
|
|
74
|
+
}
|
|
75
|
+
buildRuntime() {
|
|
76
|
+
const config = this.agentConfig();
|
|
77
|
+
const runtime = createAgentRuntime({
|
|
78
|
+
config,
|
|
79
|
+
plugins: this.agentPlugins({
|
|
80
|
+
env: this.env,
|
|
81
|
+
storage: this.ctx.storage,
|
|
82
|
+
// No caller identity exists down here, and nothing reads one: the
|
|
83
|
+
// per-caller hooks are the parent's surface, never a subagent's. A
|
|
84
|
+
// throwing thunk is the honest encoding — it can only fire if a plugin
|
|
85
|
+
// starts reading caller state on the execution path, which is a design
|
|
86
|
+
// question, not a missing value.
|
|
87
|
+
callerKey: () => {
|
|
88
|
+
throw new Error("a subagent execution has no caller identity — this plugin reads " +
|
|
89
|
+
"per-caller state on a path where none exists");
|
|
90
|
+
},
|
|
91
|
+
aiGatewayId: config.model?.aiGatewayId ?? ""
|
|
92
|
+
}),
|
|
93
|
+
env: this.env
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
policy: runtime.policy,
|
|
97
|
+
types: runtime.types,
|
|
98
|
+
models: this.modelRuntime(runtime.config.model),
|
|
99
|
+
toolFamilies: runtime.toolFamilies,
|
|
100
|
+
toolOutputWindow: runtime.config.toolOutputWindow,
|
|
101
|
+
maxOutputTokens: runtime.config.model.maxOutputTokens,
|
|
102
|
+
maxRetries: runtime.config.model.maxRetries,
|
|
103
|
+
// Always defined: the plugin that declared a backend, or core's in-memory
|
|
104
|
+
// fallback. So this needs no null check.
|
|
105
|
+
workspaceBacking: runtime.workspaceBacking
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type { ModelMessage, ToolSet } from "ai";
|
|
2
|
+
import type { SessionMessage } from "agents/experimental/memory/session";
|
|
3
|
+
import type { AgentLimits } from "../config.js";
|
|
4
|
+
import type { SubtaskTypeRegistry } from "../subtasks/subtask-types.js";
|
|
5
|
+
import { type SessionLike } from "../agent/session.js";
|
|
6
|
+
import { type OnContent, type RoundFailureKind } from "../agent/inference.js";
|
|
7
|
+
import { type TurnBudget } from "../agent/budget.js";
|
|
8
|
+
import type { ModelPair } from "../agent/model.js";
|
|
9
|
+
import { type ReferenceCatalogEntry } from "../subtasks/catalog.js";
|
|
10
|
+
import type { CompositionBranch, SubtaskDraft } from "../subtasks/types.js";
|
|
11
|
+
import type { RoundPolicy } from "./policy.js";
|
|
12
|
+
/**
|
|
13
|
+
* One **round** of the main agent: a single inference over the agent's continuous
|
|
14
|
+
* Session that ends in one of two decisions — answer the user, or delegate.
|
|
15
|
+
*
|
|
16
|
+
* This is the whole task pipeline's control point. The Workflow runs rounds in a
|
|
17
|
+
* loop: a round that delegates gets its Subtasks executed and is followed by
|
|
18
|
+
* another round; a round that answers ends the Task. So "compose" is not a
|
|
19
|
+
* separate phase with its own rules — it is simply the round in which the model
|
|
20
|
+
* decides it has enough to answer.
|
|
21
|
+
*
|
|
22
|
+
* Two layers of tools, and the difference is the design:
|
|
23
|
+
*
|
|
24
|
+
* - **Work tools** (whatever the installed plugins offer the main agent, plus the
|
|
25
|
+
* Session's own `set_context`) carry an `execute` and run *inside* the round's
|
|
26
|
+
* tool loop. They never end a round; the model keeps reasoning over their
|
|
27
|
+
* results. Every round gets them except the one the budget forced — looking
|
|
28
|
+
* something up before answering is ordinary work, not a special phase, right up
|
|
29
|
+
* until there is nothing left to spend on it (see {@link RoundMode}).
|
|
30
|
+
* - **Control tools** — `delegate` and `final_reply` — have no `execute`. The call
|
|
31
|
+
* *is* the round's output: the loop halts on it, and for `delegate` the Workflow
|
|
32
|
+
* performs it durably. Because the loop halts, the SDK never validates their
|
|
33
|
+
* input either, so each one checks its own and the round repairs what it rejects
|
|
34
|
+
* — see `agent/control.ts`. A future `escalate` (ask the human) is the same
|
|
35
|
+
* shape: another entry there, another variant of {@link TurnDecision}, another
|
|
36
|
+
* `case` in the Workflow's switch.
|
|
37
|
+
*
|
|
38
|
+
* Nothing forces the *choice*, and that is deliberate. An earlier design pinned
|
|
39
|
+
* `toolChoice` to a specific tool to force delegation in one phase and forbid it in
|
|
40
|
+
* another, which meant a request the main agent was best placed to answer got
|
|
41
|
+
* shipped to a memoryless subagent, and material that came back could only ever be
|
|
42
|
+
* turned into prose. That is still rejected: the model picks its own ending, and
|
|
43
|
+
* delegating twice is allowed.
|
|
44
|
+
*
|
|
45
|
+
* What *is* forced is that the round end in a control call at all —
|
|
46
|
+
* `toolChoice: "required"`, with both endings declared as tools. Prose is not an
|
|
47
|
+
* outcome: see {@link file://../agent/final-reply.ts final-reply.ts} for why not.
|
|
48
|
+
*
|
|
49
|
+
* Narration survived that fix by moving house. A round whose results have just come
|
|
50
|
+
* back can still announce its next step *inside* a `final_reply` — "now sending the
|
|
51
|
+
* second one" — and that ends the Task as surely as prose did, having done nothing,
|
|
52
|
+
* while telling the user the opposite. No mechanism can catch it: `final_reply` is a
|
|
53
|
+
* legitimate ending for exactly this round, and only the model knows whether the
|
|
54
|
+
* request is finished. So it is the **round contract** that has to close it — which
|
|
55
|
+
* is why that text is a {@link RoundPolicy} the agent writes, not something core
|
|
56
|
+
* ships.
|
|
57
|
+
*
|
|
58
|
+
* The model reasons over the whole conversation but references it by **catalog
|
|
59
|
+
* index only** — see {@link renderTurnMessages}.
|
|
60
|
+
*/
|
|
61
|
+
/**
|
|
62
|
+
* The prompt copy for one configured agent, built once per Durable Object
|
|
63
|
+
* instance.
|
|
64
|
+
*
|
|
65
|
+
* A builder rather than a module-level `const`, because the type enum is a
|
|
66
|
+
* function of the plugins this agent installed, which is a function of `env`,
|
|
67
|
+
* which does not exist at module scope on Workers. The DO memoizes the result.
|
|
68
|
+
*/
|
|
69
|
+
export interface TurnInstructions {
|
|
70
|
+
/** Contract + per-type delegation guidance. Appended to soul + caller context. */
|
|
71
|
+
open: string;
|
|
72
|
+
/** Further appended when the budget is spent. */
|
|
73
|
+
final: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build both prompt suffixes for one configured agent.
|
|
77
|
+
*
|
|
78
|
+
* The agent's contract, then whatever the delegable types have to say about being
|
|
79
|
+
* delegated — each declared by the type that owns it (`SubtaskTypeSpec`) and
|
|
80
|
+
* collected by the runtime's registry, so no domain is named by the policy. That
|
|
81
|
+
* is the rule the two prompt fields on a subtask type exist to hold: everything
|
|
82
|
+
* the main agent is told about a domain is declared by the plugin that owns it,
|
|
83
|
+
* never written inside the loop.
|
|
84
|
+
*/
|
|
85
|
+
export declare function buildTurnInstructions(policy: RoundPolicy, types: SubtaskTypeRegistry, maxSubtasks: number, limits: AgentLimits): TurnInstructions;
|
|
86
|
+
/**
|
|
87
|
+
* Render the model's view for one round: the conversation, with every earlier
|
|
88
|
+
* round's delegation restored as the call-and-result it actually was, and every
|
|
89
|
+
* referenceable turn marked with the catalog index the model selects it by.
|
|
90
|
+
*
|
|
91
|
+
* One pass, two jobs, because they have to agree. The `[ref N]` markers use
|
|
92
|
+
* `isCatalogEligible` — the same predicate the catalog is numbered with — so a
|
|
93
|
+
* marker and its entry can never drift: compaction summaries (`assistant` role,
|
|
94
|
+
* generated) stay in the messages unmarked, readable for context but structurally
|
|
95
|
+
* uncitable as conversation evidence, which is exactly the intent.
|
|
96
|
+
*
|
|
97
|
+
* A round's acknowledgment is stored as plain assistant text (history is
|
|
98
|
+
* text-only, and stays that way — `sessionText`, the catalog, compaction, and
|
|
99
|
+
* recall all read text parts). So its `delegate` call is re-attached to that
|
|
100
|
+
* message here, and the result appended after it, for this one inference call. The
|
|
101
|
+
* pair is emitted together, anchored on the ack's deterministic id, so a `tool`
|
|
102
|
+
* message can never be orphaned from its call — and an ack that has been compacted
|
|
103
|
+
* away still gets its pair, appended at the end minus the acknowledgment text: a
|
|
104
|
+
* result the model cannot place beats a malformed history.
|
|
105
|
+
*
|
|
106
|
+
* Acks are deliberately **not** catalog-eligible: they are the agent's own
|
|
107
|
+
* scaffolding, and a subtask referencing "I'm on it" as verbatim conversation
|
|
108
|
+
* evidence would be noise. That holds for every ack in the Session, not only the
|
|
109
|
+
* ones this render can pair with branches — see `parseRoundAckMessageId`.
|
|
110
|
+
*
|
|
111
|
+
* Everything here is ephemeral — scaffolding for this call only. Reference text is
|
|
112
|
+
* snapshotted from the catalog, so no `[ref N]` prefix ever reaches a Subtask, and
|
|
113
|
+
* the Session never sees any of this markup.
|
|
114
|
+
*/
|
|
115
|
+
export declare function renderTurnMessages(history: SessionMessage[], taskId: string, branches: CompositionBranch[]): {
|
|
116
|
+
messages: ModelMessage[];
|
|
117
|
+
catalog: ReferenceCatalogEntry[];
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Deterministic fallback reply: the successful branches' text in ordinal order,
|
|
121
|
+
* plus the policy's short note when some branches did not succeed.
|
|
122
|
+
*
|
|
123
|
+
* Used when a round's inference is unavailable but its predecessors' work is
|
|
124
|
+
* durable. Failing the whole Task because the answering model is down would throw
|
|
125
|
+
* away good results the user asked for.
|
|
126
|
+
*/
|
|
127
|
+
export declare function joinSuccessfulBranches(branches: CompositionBranch[], partialNote: string): string;
|
|
128
|
+
/**
|
|
129
|
+
* How much rope this round gets, decided by the Workflow from the Task's spent
|
|
130
|
+
* budget.
|
|
131
|
+
*
|
|
132
|
+
* - `open` — the normal round: `delegate`, `final_reply`, and every work tool.
|
|
133
|
+
* It may spend whatever is left of the turn budget.
|
|
134
|
+
* - `final` — the Task has spent its turns or its wall clock. **No work tools and
|
|
135
|
+
* no `delegate`**: the only thing on the table is the answer. This is not a
|
|
136
|
+
* punishment but the shape of the ceiling — a budget that ends in a forced
|
|
137
|
+
* answer returns the work, where one that simply stopped would discard it.
|
|
138
|
+
* Costs one turn, or two if the primary model fails and the fallback has to
|
|
139
|
+
* produce the answer instead; a fallback with no step to spend could not answer
|
|
140
|
+
* at all.
|
|
141
|
+
*/
|
|
142
|
+
export type RoundMode = "open" | "final";
|
|
143
|
+
export interface RunTurnArgs {
|
|
144
|
+
/** The DO's one continuous Session. */
|
|
145
|
+
session: SessionLike;
|
|
146
|
+
/** Parent Task id — derives the deterministic Session message ids. */
|
|
147
|
+
taskId: string;
|
|
148
|
+
/** 0-based round within this Task. */
|
|
149
|
+
round: number;
|
|
150
|
+
/** The inbound user text (keeps its `<turn>` provenance wrapper verbatim). Appended on round 0 only. */
|
|
151
|
+
text: string;
|
|
152
|
+
/** What this round may do — see {@link RoundMode}. */
|
|
153
|
+
mode: RoundMode;
|
|
154
|
+
/**
|
|
155
|
+
* The Task's unspent turns, and the tally this round writes back into them.
|
|
156
|
+
* Mutated in place as the model works, so the primary and the fallback draw on
|
|
157
|
+
* one allowance rather than one each — see `TurnBudget`.
|
|
158
|
+
*
|
|
159
|
+
* There is no per-round allowance beyond what the Task has left: an early round
|
|
160
|
+
* that dithers spends what a later one would have had, and is then handed a
|
|
161
|
+
* `final` round to answer in. The caller reads `spent` when the round returns.
|
|
162
|
+
*/
|
|
163
|
+
budget: TurnBudget;
|
|
164
|
+
/** Per-request system-prompt suffix (verified caller context). */
|
|
165
|
+
systemSuffix: string;
|
|
166
|
+
/** The main agent's gated **work** tools, merged over the session's own tools. */
|
|
167
|
+
tools: ToolSet;
|
|
168
|
+
/** Primary + fallback model pair. */
|
|
169
|
+
models: ModelPair;
|
|
170
|
+
/** Every earlier round's branches, all rounds, in stable ordinal order. */
|
|
171
|
+
branches: CompositionBranch[];
|
|
172
|
+
/** The installed subtask types — what `delegate` may name. */
|
|
173
|
+
types: SubtaskTypeRegistry;
|
|
174
|
+
/** `CoreConfig.maxSubtasks`, the per-round fan-out bound. */
|
|
175
|
+
maxSubtasks: number;
|
|
176
|
+
/** `CoreConfig.model.maxOutputTokens`. */
|
|
177
|
+
maxOutputTokens: number;
|
|
178
|
+
/** `CoreConfig.model.maxRetries` — retries on *this* model before the fallback. */
|
|
179
|
+
maxRetries: number;
|
|
180
|
+
/** The prompt suffixes, memoized by the DO. See {@link buildTurnInstructions}. */
|
|
181
|
+
instructions: TurnInstructions;
|
|
182
|
+
/** The note a deterministic join appends when it has to disclose gaps. */
|
|
183
|
+
partialNote: string;
|
|
184
|
+
/** Streams intermediate content while the model reasons. Best-effort. */
|
|
185
|
+
onContent?: OnContent;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Terminal outcome of one round. `failed` means the round produced no answer and
|
|
189
|
+
* there was no durable work to fall back on — the parent Task fails rather than
|
|
190
|
+
* running a synthesized subtask nobody asked for. Transient faults throw instead
|
|
191
|
+
* (the Workflow step retries).
|
|
192
|
+
*
|
|
193
|
+
* `kind` is *why*, not a second outcome. The round is over either way and the
|
|
194
|
+
* Task it delivers has the same shape; what the kind decides is what a human is
|
|
195
|
+
* told — `exhausted` is "the models could not do it", a credential kind is "a
|
|
196
|
+
* human must fix the deployment". Both were once separate statuses, and every
|
|
197
|
+
* consumer promptly bundled them back together to do the same three things. See
|
|
198
|
+
* {@link RoundFailureKind}.
|
|
199
|
+
*
|
|
200
|
+
* What the round cost is not here: it is in the caller's `TurnBudget`, which every
|
|
201
|
+
* exit has already charged — including the ones that failed. A round that burned
|
|
202
|
+
* the primary and recovered on the fallback spent both, and there is no variant
|
|
203
|
+
* that could quietly forgive the expensive half of a bad round.
|
|
204
|
+
*/
|
|
205
|
+
export type RunTurnOutcome = {
|
|
206
|
+
status: "replied";
|
|
207
|
+
reply: string;
|
|
208
|
+
} | {
|
|
209
|
+
status: "delegated";
|
|
210
|
+
reply: string;
|
|
211
|
+
drafts: SubtaskDraft[];
|
|
212
|
+
} | {
|
|
213
|
+
status: "failed";
|
|
214
|
+
kind: RoundFailureKind;
|
|
215
|
+
error: string;
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* Run one round against the continuous Session: append the user turn (round 0),
|
|
219
|
+
* let the model decide over the indexed history, validate any delegation against
|
|
220
|
+
* this round's catalog, and persist what the user will see.
|
|
221
|
+
*
|
|
222
|
+
* Every append uses a deterministic id, so a Workflow-step re-run neither
|
|
223
|
+
* duplicates the turn nor changes an already-delivered reply.
|
|
224
|
+
*
|
|
225
|
+
* Two nested recoveries, and they answer different failures. Within a slot, a
|
|
226
|
+
* decomposition the catalog rejects is handed back to the *same* model as a failed
|
|
227
|
+
* tool result, up to {@link MAX_REPAIR_ATTEMPTS} times — a shape error is the one
|
|
228
|
+
* thing a model can fix once it sees it. Across slots, an attempt that produced no
|
|
229
|
+
* decision at all moves to the fallback model, which is what that slot is for.
|
|
230
|
+
*
|
|
231
|
+
* Throws only on a transient platform fault (for the Workflow step to retry).
|
|
232
|
+
* A deterministic failure that outlasts every repair on both slots, with durable
|
|
233
|
+
* work behind it, degrades to {@link joinSuccessfulBranches} rather than discarding
|
|
234
|
+
* completed branches; with nothing behind it, it resolves to
|
|
235
|
+
* `{ status: "failed", kind: "exhausted" }`.
|
|
236
|
+
*
|
|
237
|
+
* The third failure is neither, and it short-circuits the *model* recoveries
|
|
238
|
+
* above: a {@link nonRecoverableKind} error ends the round from wherever it
|
|
239
|
+
* happens, carrying that kind — without repairing and **without reaching the
|
|
240
|
+
* fallback slot**, both of which would only present the same dead credential
|
|
241
|
+
* again. See that function for why the transient/deterministic split cannot
|
|
242
|
+
* express it.
|
|
243
|
+
*
|
|
244
|
+
* It does **not** skip the deterministic join. That path needs no credential —
|
|
245
|
+
* it is string concatenation over rows that are already durable — so completed
|
|
246
|
+
* branches are still delivered, and the credential fault reaches the operator
|
|
247
|
+
* through the log rather than by throwing away finished work.
|
|
248
|
+
*/
|
|
249
|
+
export declare function runTurn(args: RunTurnArgs): Promise<RunTurnOutcome>;
|