@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,186 @@
|
|
|
1
|
+
import { resolveConfig } from "../config.js";
|
|
2
|
+
import { PLUGIN_CONTRACT_VERSION } from "../contract/plugin.js";
|
|
3
|
+
import { memoryWorkspaceBacking } from "../subagent/workspace.js";
|
|
4
|
+
import { makeSubtaskTypes } from "../subtasks/subtask-types.js";
|
|
5
|
+
import { collectToolFamilies } from "./tool-families.js";
|
|
6
|
+
export { buildRecipeTools, collectToolFamilies } from "./tool-families.js";
|
|
7
|
+
/**
|
|
8
|
+
* Thrown when the installed plugins and the host disagree — a contract-version
|
|
9
|
+
* skew, a duplicate key, or a missing binding. Always at DO start, never mid-request.
|
|
10
|
+
*/
|
|
11
|
+
export class RuntimeSetupError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "RuntimeSetupError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function createAgentRuntime(options) {
|
|
18
|
+
const config = resolveConfig(options.config);
|
|
19
|
+
const plugins = options.plugins;
|
|
20
|
+
// --- contract + identity checks, all up front -----------------------------
|
|
21
|
+
const byKey = new Map();
|
|
22
|
+
for (const plugin of plugins) {
|
|
23
|
+
if (byKey.has(plugin.key)) {
|
|
24
|
+
throw new RuntimeSetupError(`duplicate plugin key "${plugin.key}" — two installed plugins claim it`);
|
|
25
|
+
}
|
|
26
|
+
byKey.set(plugin.key, plugin);
|
|
27
|
+
if (plugin.contractVersion !== PLUGIN_CONTRACT_VERSION) {
|
|
28
|
+
throw new RuntimeSetupError(`plugin "${plugin.key}" was built against plugin contract v${plugin.contractVersion}, ` +
|
|
29
|
+
`but this @dynamicagents/core speaks v${PLUGIN_CONTRACT_VERSION}. ` +
|
|
30
|
+
"Upgrade whichever of the two is behind — they publish from separate repos, " +
|
|
31
|
+
"so a version train can leave one lagging.");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// --- what the plugins contribute ------------------------------------------
|
|
35
|
+
const specs = plugins.flatMap((p) => (p.subtaskType ? [p.subtaskType] : []));
|
|
36
|
+
const types = makeSubtaskTypes(specs);
|
|
37
|
+
const toolFamilies = collectToolFamilies(plugins);
|
|
38
|
+
const typeOwner = new Map();
|
|
39
|
+
for (const plugin of plugins) {
|
|
40
|
+
if (plugin.subtaskType)
|
|
41
|
+
typeOwner.set(plugin.subtaskType.key, plugin);
|
|
42
|
+
}
|
|
43
|
+
const stores = plugins.flatMap((p) => (p.store ? [p.store] : []));
|
|
44
|
+
const displacementListeners = plugins.flatMap((p) => p.onMessagesDisplaced
|
|
45
|
+
? [{ key: p.key, notify: p.onMessagesDisplaced.bind(p) }]
|
|
46
|
+
: []);
|
|
47
|
+
const turnGates = plugins.flatMap((p) => p.shouldHandleTurn ? [{ key: p.key, gate: p.shouldHandleTurn.bind(p) }] : []);
|
|
48
|
+
// At most one backend: two would mean two answers to "where did that file go".
|
|
49
|
+
const backings = plugins.flatMap((p) => p.workspaceBacking ? [{ key: p.key, make: p.workspaceBacking.bind(p) }] : []);
|
|
50
|
+
if (backings.length > 1) {
|
|
51
|
+
throw new RuntimeSetupError(`plugins ${backings.map((b) => `"${b.key}"`).join(" and ")} both declare a ` +
|
|
52
|
+
"workspaceBacking — an execution has one workspace, so only one plugin may back it");
|
|
53
|
+
}
|
|
54
|
+
const workspaceBacking = backings[0]?.make ?? memoryWorkspaceBacking;
|
|
55
|
+
const secrets = [
|
|
56
|
+
...new Set(plugins.flatMap((p) => [...(p.requires?.secrets ?? [])]))
|
|
57
|
+
];
|
|
58
|
+
const bindings = [
|
|
59
|
+
...new Set(plugins.flatMap((p) => [...(p.requires?.bindings ?? [])]))
|
|
60
|
+
];
|
|
61
|
+
if (options.env) {
|
|
62
|
+
const env = options.env;
|
|
63
|
+
const missing = [];
|
|
64
|
+
for (const plugin of plugins) {
|
|
65
|
+
for (const name of [
|
|
66
|
+
...(plugin.requires?.secrets ?? []),
|
|
67
|
+
...(plugin.requires?.bindings ?? [])
|
|
68
|
+
]) {
|
|
69
|
+
const value = env[name];
|
|
70
|
+
if (value === undefined || value === null || value === "") {
|
|
71
|
+
missing.push(`${name} (required by "${plugin.key}")`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (missing.length > 0) {
|
|
76
|
+
throw new RuntimeSetupError(`missing bindings or secrets: ${missing.join(", ")}. ` +
|
|
77
|
+
"A plugin cannot add its own wrangler binding — declare these in wrangler.jsonc " +
|
|
78
|
+
"(and `wrangler secret put` the secrets).");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const policy = {
|
|
82
|
+
primaryModelId: config.model.chatModelId,
|
|
83
|
+
fallbackModelId: config.model.fallbackChatModelId,
|
|
84
|
+
knownToolFamilies: new Set(toolFamilies.keys()),
|
|
85
|
+
baselineLimits: config.subagentLimits
|
|
86
|
+
};
|
|
87
|
+
const pluginForType = (type) => typeOwner.get(type) ?? null;
|
|
88
|
+
return {
|
|
89
|
+
config,
|
|
90
|
+
plugins,
|
|
91
|
+
types,
|
|
92
|
+
toolFamilies,
|
|
93
|
+
policy,
|
|
94
|
+
stores,
|
|
95
|
+
requirements: { secrets, bindings },
|
|
96
|
+
workspaceBacking,
|
|
97
|
+
pluginForType,
|
|
98
|
+
async mainAgentTools(ctx) {
|
|
99
|
+
const tools = {};
|
|
100
|
+
// Sequential rather than fanned out: this is a handful of plugins reading
|
|
101
|
+
// one session, and merging in declaration order is what makes a name
|
|
102
|
+
// collision resolve the same way on every call.
|
|
103
|
+
for (const plugin of plugins) {
|
|
104
|
+
if (plugin.mainAgentTools)
|
|
105
|
+
Object.assign(tools, await plugin.mainAgentTools(ctx));
|
|
106
|
+
}
|
|
107
|
+
return tools;
|
|
108
|
+
},
|
|
109
|
+
renderCapabilities() {
|
|
110
|
+
const blocks = [];
|
|
111
|
+
for (const plugin of plugins) {
|
|
112
|
+
// Both blocks a plugin may declare, emitted adjacently: a plugin that
|
|
113
|
+
// declares a subtask type puts its capability on the *type*, and one
|
|
114
|
+
// that only offers main-agent tools puts it on the plugin. Declaring
|
|
115
|
+
// both is legal and means the model reads both.
|
|
116
|
+
if (plugin.capability)
|
|
117
|
+
blocks.push(plugin.capability);
|
|
118
|
+
if (plugin.subtaskType?.capability)
|
|
119
|
+
blocks.push(plugin.subtaskType.capability);
|
|
120
|
+
}
|
|
121
|
+
return blocks.join("\n\n");
|
|
122
|
+
},
|
|
123
|
+
async shouldHandleTurn(ctx) {
|
|
124
|
+
if (turnGates.length === 0)
|
|
125
|
+
return true;
|
|
126
|
+
// Same `allSettled` + `async`-wrapped-callback discipline as
|
|
127
|
+
// `onMessagesDisplaced` below, and for the same two reasons: every gate is
|
|
128
|
+
// consulted even when one throws, and a gate that throws *synchronously*
|
|
129
|
+
// (reading a binding before its first await) is caught rather than
|
|
130
|
+
// escaping past the aggregation.
|
|
131
|
+
//
|
|
132
|
+
// A rejection resolves to `true`. That is not leniency — it is the only
|
|
133
|
+
// safe default here. A wrong reply is noise the user sees and ignores; a
|
|
134
|
+
// wrong silence is invisible to the person who needed an answer, so a
|
|
135
|
+
// broken gate must degrade to "run the turn" and never to a mute agent.
|
|
136
|
+
const results = await Promise.allSettled(turnGates.map(async (g) => g.gate(ctx)));
|
|
137
|
+
return results.every((result, i) => {
|
|
138
|
+
if (result.status === "rejected") {
|
|
139
|
+
console.warn(`[runtime] plugin "${turnGates[i].key}" turn gate failed, handling the turn`, result.reason);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return result.value;
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
async onMessagesDisplaced(messages) {
|
|
146
|
+
// allSettled, not all: `all` rejects at the first listener to throw and
|
|
147
|
+
// stops awaiting the rest, so every other plugin's write is still in
|
|
148
|
+
// flight when this returns — and on a Durable Object the isolate can be
|
|
149
|
+
// evicted before it lands. One plugin's outage would silently cost the
|
|
150
|
+
// others their writes. Each rejection is logged against the key that
|
|
151
|
+
// caused it; an anonymous aggregate is not debuggable across plugins.
|
|
152
|
+
//
|
|
153
|
+
// The `async` on the map callback is load-bearing, not style. A listener
|
|
154
|
+
// is typed `(messages) => Promise<void>`, which does not oblige it to be
|
|
155
|
+
// `async` — one that reads a binding before starting its async work can
|
|
156
|
+
// throw *synchronously*, during this very `.map()`. Without the `async`
|
|
157
|
+
// that throw escapes before `Promise.allSettled` exists to catch it: this
|
|
158
|
+
// method rejects despite documenting that it never does, and `.map()`
|
|
159
|
+
// aborts mid-iteration so every listener after the thrower is never even
|
|
160
|
+
// invoked — strictly worse than the `Promise.all` failure mode above.
|
|
161
|
+
const results = await Promise.allSettled(displacementListeners.map(async (l) => l.notify(messages)));
|
|
162
|
+
results.forEach((result, i) => {
|
|
163
|
+
if (result.status === "rejected") {
|
|
164
|
+
console.error(`[runtime] plugin "${displacementListeners[i].key}" failed on displaced messages`, result.reason);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
async resolveRuntime(ctx) {
|
|
169
|
+
const plugin = pluginForType(ctx.type);
|
|
170
|
+
if (!plugin?.resolveRuntime)
|
|
171
|
+
return {};
|
|
172
|
+
return (await plugin.resolveRuntime(ctx));
|
|
173
|
+
},
|
|
174
|
+
async enrichResult(ctx, result) {
|
|
175
|
+
const plugin = pluginForType(ctx.request.type);
|
|
176
|
+
if (!plugin?.enrichResult)
|
|
177
|
+
return result;
|
|
178
|
+
return plugin.enrichResult(ctx, result);
|
|
179
|
+
},
|
|
180
|
+
async onAbort(ctx) {
|
|
181
|
+
const plugin = pluginForType(ctx.type);
|
|
182
|
+
if (plugin?.onAbort)
|
|
183
|
+
await plugin.onAbort(ctx);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { RecipeToolSet, ToolFamilyBuilder, ToolFamilyContext } from "../contract/plugin.js";
|
|
2
|
+
import type { SubtaskRuntime } from "../subtasks/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Assemble a recipe's toolset from the installed families.
|
|
5
|
+
*
|
|
6
|
+
* This replaces the predecessor's dispatcher, which was an `if / else if` chain
|
|
7
|
+
* naming three families inline — including a domain one — inside otherwise
|
|
8
|
+
* generic code, with static imports of that domain's modules at the top of the
|
|
9
|
+
* file. That is the single edit that made every agent's bundle carry every
|
|
10
|
+
* domain, and it is why this takes a map instead.
|
|
11
|
+
*
|
|
12
|
+
* A family a recipe names but no plugin registered is skipped rather than
|
|
13
|
+
* throwing: `validateRecipe` already dropped unknown families, so reaching one
|
|
14
|
+
* here means the policy and the registry disagreed, and a subagent that silently
|
|
15
|
+
* runs with fewer tools degrades better than one that fails a whole branch. The
|
|
16
|
+
* skipped names are returned so a caller can log them.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildRecipeTools<TRuntime = SubtaskRuntime>(families: readonly string[], registry: ReadonlyMap<string, ToolFamilyBuilder<TRuntime>>, ctx: ToolFamilyContext<TRuntime>): RecipeToolSet<TRuntime> & {
|
|
19
|
+
skipped: string[];
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Collect every family builder the installed plugins registered, refusing
|
|
23
|
+
* duplicates.
|
|
24
|
+
*
|
|
25
|
+
* Two plugins claiming one family name is a misconfiguration whose symptom would
|
|
26
|
+
* otherwise be a subagent silently getting the wrong domain's tools — so it fails
|
|
27
|
+
* at DO start, named, instead.
|
|
28
|
+
*/
|
|
29
|
+
export declare function collectToolFamilies<TRuntime = SubtaskRuntime>(plugins: readonly {
|
|
30
|
+
key: string;
|
|
31
|
+
toolFamilies?: Record<string, ToolFamilyBuilder<TRuntime>>;
|
|
32
|
+
}[]): Map<string, ToolFamilyBuilder<TRuntime>>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assemble a recipe's toolset from the installed families.
|
|
3
|
+
*
|
|
4
|
+
* This replaces the predecessor's dispatcher, which was an `if / else if` chain
|
|
5
|
+
* naming three families inline — including a domain one — inside otherwise
|
|
6
|
+
* generic code, with static imports of that domain's modules at the top of the
|
|
7
|
+
* file. That is the single edit that made every agent's bundle carry every
|
|
8
|
+
* domain, and it is why this takes a map instead.
|
|
9
|
+
*
|
|
10
|
+
* A family a recipe names but no plugin registered is skipped rather than
|
|
11
|
+
* throwing: `validateRecipe` already dropped unknown families, so reaching one
|
|
12
|
+
* here means the policy and the registry disagreed, and a subagent that silently
|
|
13
|
+
* runs with fewer tools degrades better than one that fails a whole branch. The
|
|
14
|
+
* skipped names are returned so a caller can log them.
|
|
15
|
+
*/
|
|
16
|
+
export function buildRecipeTools(families, registry, ctx) {
|
|
17
|
+
const tools = {};
|
|
18
|
+
const aborts = [];
|
|
19
|
+
const skipped = [];
|
|
20
|
+
for (const family of families) {
|
|
21
|
+
const build = registry.get(family);
|
|
22
|
+
if (!build) {
|
|
23
|
+
skipped.push(family);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const built = build(ctx);
|
|
27
|
+
Object.assign(tools, built.tools);
|
|
28
|
+
if (built.abort)
|
|
29
|
+
aborts.push(built.abort);
|
|
30
|
+
}
|
|
31
|
+
const abort = aborts.length === 0
|
|
32
|
+
? undefined
|
|
33
|
+
: async (c) => {
|
|
34
|
+
for (const run of aborts)
|
|
35
|
+
await run(c);
|
|
36
|
+
};
|
|
37
|
+
return { tools, abort, skipped };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Collect every family builder the installed plugins registered, refusing
|
|
41
|
+
* duplicates.
|
|
42
|
+
*
|
|
43
|
+
* Two plugins claiming one family name is a misconfiguration whose symptom would
|
|
44
|
+
* otherwise be a subagent silently getting the wrong domain's tools — so it fails
|
|
45
|
+
* at DO start, named, instead.
|
|
46
|
+
*/
|
|
47
|
+
export function collectToolFamilies(plugins) {
|
|
48
|
+
const registry = new Map();
|
|
49
|
+
const owner = new Map();
|
|
50
|
+
for (const plugin of plugins) {
|
|
51
|
+
for (const [family, build] of Object.entries(plugin.toolFamilies ?? {})) {
|
|
52
|
+
const existing = owner.get(family);
|
|
53
|
+
if (existing) {
|
|
54
|
+
throw new Error(`tool family "${family}" is registered by both "${existing}" and "${plugin.key}"`);
|
|
55
|
+
}
|
|
56
|
+
owner.set(family, plugin.key);
|
|
57
|
+
registry.set(family, build);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return registry;
|
|
61
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { RecipeExecutionRequest } from "../subtasks/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Version of the canonicalization format below.
|
|
4
|
+
*
|
|
5
|
+
* Bump it **only** when a change to `canonicalRequest` would make two equal
|
|
6
|
+
* executions hash differently. Bumping invalidates every cached result and every
|
|
7
|
+
* in-flight run's checkpoint, so it is a deliberate act, not a side effect.
|
|
8
|
+
*/
|
|
9
|
+
export declare const FINGERPRINT_VERSION = 1;
|
|
10
|
+
/**
|
|
11
|
+
* Canonical JSON of the fields that define an execution's identity, rebuilt as
|
|
12
|
+
* literals in fixed key order so `JSON.stringify` is deterministic (object
|
|
13
|
+
* insertion order). Array order is semantic and preserved: the parent builds
|
|
14
|
+
* references from ordinal-ordered rows, so a retry of the same execution is
|
|
15
|
+
* byte-identical.
|
|
16
|
+
*
|
|
17
|
+
* **Limits are canonicalized as *declared*, not as merged.** The predecessor
|
|
18
|
+
* merged them against the house baseline first, on the reasoning that `{}` and
|
|
19
|
+
* an explicit restatement of the baseline are the same execution and should hash
|
|
20
|
+
* alike. That reasoning is sound but its cost is not payable once the baseline
|
|
21
|
+
* lives in a versioned package: a patch release that nudges a default would
|
|
22
|
+
* change every fingerprint at once, and every in-flight subagent run would miss
|
|
23
|
+
* its checkpoint and restart from turn zero having already spent its budget.
|
|
24
|
+
*
|
|
25
|
+
* Hashing the sparse declaration inverts that trade. A recipe that spells out
|
|
26
|
+
* the baseline explicitly gets a different key from one that omits it — worth
|
|
27
|
+
* exactly one redundant execution, once, for a recipe nobody writes — and the
|
|
28
|
+
* host's baseline becomes free to move. {@link FINGERPRINT_VERSION} is the
|
|
29
|
+
* deliberate lever for the invalidation the merge used to cause accidentally.
|
|
30
|
+
*/
|
|
31
|
+
export declare function canonicalRequest(request: RecipeExecutionRequest): string;
|
|
32
|
+
/**
|
|
33
|
+
* SHA-256 hex digest of the canonical request — the deterministic key for the
|
|
34
|
+
* subagent's single cached terminal result. The raw (pre-validation) request is
|
|
35
|
+
* fingerprinted, so the key matches exactly what the parent re-sends on retry.
|
|
36
|
+
*/
|
|
37
|
+
export declare function fingerprintRequest(request: RecipeExecutionRequest): Promise<string>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version of the canonicalization format below.
|
|
3
|
+
*
|
|
4
|
+
* Bump it **only** when a change to `canonicalRequest` would make two equal
|
|
5
|
+
* executions hash differently. Bumping invalidates every cached result and every
|
|
6
|
+
* in-flight run's checkpoint, so it is a deliberate act, not a side effect.
|
|
7
|
+
*/
|
|
8
|
+
export const FINGERPRINT_VERSION = 1;
|
|
9
|
+
/**
|
|
10
|
+
* Canonical JSON of the fields that define an execution's identity, rebuilt as
|
|
11
|
+
* literals in fixed key order so `JSON.stringify` is deterministic (object
|
|
12
|
+
* insertion order). Array order is semantic and preserved: the parent builds
|
|
13
|
+
* references from ordinal-ordered rows, so a retry of the same execution is
|
|
14
|
+
* byte-identical.
|
|
15
|
+
*
|
|
16
|
+
* **Limits are canonicalized as *declared*, not as merged.** The predecessor
|
|
17
|
+
* merged them against the house baseline first, on the reasoning that `{}` and
|
|
18
|
+
* an explicit restatement of the baseline are the same execution and should hash
|
|
19
|
+
* alike. That reasoning is sound but its cost is not payable once the baseline
|
|
20
|
+
* lives in a versioned package: a patch release that nudges a default would
|
|
21
|
+
* change every fingerprint at once, and every in-flight subagent run would miss
|
|
22
|
+
* its checkpoint and restart from turn zero having already spent its budget.
|
|
23
|
+
*
|
|
24
|
+
* Hashing the sparse declaration inverts that trade. A recipe that spells out
|
|
25
|
+
* the baseline explicitly gets a different key from one that omits it — worth
|
|
26
|
+
* exactly one redundant execution, once, for a recipe nobody writes — and the
|
|
27
|
+
* host's baseline becomes free to move. {@link FINGERPRINT_VERSION} is the
|
|
28
|
+
* deliberate lever for the invalidation the merge used to cause accidentally.
|
|
29
|
+
*/
|
|
30
|
+
export function canonicalRequest(request) {
|
|
31
|
+
const canonical = {
|
|
32
|
+
v: FINGERPRINT_VERSION,
|
|
33
|
+
taskId: request.taskId,
|
|
34
|
+
subtaskId: request.subtaskId,
|
|
35
|
+
type: request.type,
|
|
36
|
+
recipe: {
|
|
37
|
+
key: request.recipe.key,
|
|
38
|
+
version: request.recipe.version,
|
|
39
|
+
// No model ids. Their absence is the same trade this file already makes
|
|
40
|
+
// for `limits`: the host's own configuration stays out of the fingerprint,
|
|
41
|
+
// so changing it does not invalidate every checkpoint at once and restart
|
|
42
|
+
// every in-flight run from turn zero with its budget already spent. A run
|
|
43
|
+
// that resumes on a newly configured model resumes from conversation
|
|
44
|
+
// state, which is model-independent.
|
|
45
|
+
soul: request.recipe.soul,
|
|
46
|
+
toolFamilies: request.recipe.toolFamilies,
|
|
47
|
+
enabled: request.recipe.enabled,
|
|
48
|
+
limits: canonicalLimits(request.recipe.limits),
|
|
49
|
+
historyWindow: request.recipe.historyWindow,
|
|
50
|
+
reportMetrics: request.recipe.reportMetrics
|
|
51
|
+
},
|
|
52
|
+
prompt: request.prompt,
|
|
53
|
+
references: request.references.map((ref) => ({
|
|
54
|
+
role: ref.role,
|
|
55
|
+
text: ref.text
|
|
56
|
+
})),
|
|
57
|
+
// Params ARE identity: the same prompt against a different external resource
|
|
58
|
+
// is different work, and must not replay a cached result. Key order is fixed
|
|
59
|
+
// by sorting, so an equivalent params object always canonicalizes identically.
|
|
60
|
+
params: Object.fromEntries(Object.entries(request.params).sort(([a], [b]) => (a < b ? -1 : 1)))
|
|
61
|
+
};
|
|
62
|
+
return JSON.stringify(canonical);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A recipe's declared budget, normalized: only positive integers count (matching
|
|
66
|
+
* what `resolveLimits` would honor), and fields appear in fixed order with
|
|
67
|
+
* absent ones omitted. So `{}`, `{ maxTurns: 0 }`, and `{ maxTurns: 1.5 }` all
|
|
68
|
+
* canonicalize alike — they all mean "inherit the baseline".
|
|
69
|
+
*/
|
|
70
|
+
function canonicalLimits(limits) {
|
|
71
|
+
const keep = (n) => typeof n === "number" && Number.isInteger(n) && n > 0 ? n : undefined;
|
|
72
|
+
const out = {};
|
|
73
|
+
const maxTurns = keep(limits?.maxTurns);
|
|
74
|
+
if (maxTurns !== undefined)
|
|
75
|
+
out.maxTurns = maxTurns;
|
|
76
|
+
const maxWallMs = keep(limits?.maxWallMs);
|
|
77
|
+
if (maxWallMs !== undefined)
|
|
78
|
+
out.maxWallMs = maxWallMs;
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* SHA-256 hex digest of the canonical request — the deterministic key for the
|
|
83
|
+
* subagent's single cached terminal result. The raw (pre-validation) request is
|
|
84
|
+
* fingerprinted, so the key matches exactly what the parent re-sends on retry.
|
|
85
|
+
*/
|
|
86
|
+
export async function fingerprintRequest(request) {
|
|
87
|
+
const bytes = new TextEncoder().encode(canonicalRequest(request));
|
|
88
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
89
|
+
return [...new Uint8Array(digest)]
|
|
90
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
91
|
+
.join("");
|
|
92
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Agent } from "agents";
|
|
2
|
+
import type { ModelPair, ModelRuntime } from "../agent/model.js";
|
|
3
|
+
import type { ToolFamilyBuilder } from "../contract/plugin.js";
|
|
4
|
+
import { type RecipePolicy } from "../contract/validation.js";
|
|
5
|
+
import type { RecipeChunkResult, RecipeExecutionRequest, SubtaskId, SubtaskRuntime } from "../subtasks/types.js";
|
|
6
|
+
import { type SubtaskTypeRegistry } from "../subtasks/subtask-types.js";
|
|
7
|
+
import { type WorkspaceBacking } from "./workspace.js";
|
|
8
|
+
/**
|
|
9
|
+
* Everything the facet needs that comes from the *host*: resolved config, the
|
|
10
|
+
* capability policy, the installed subtask types and tool families, the model
|
|
11
|
+
* runtime, and a workspace backend.
|
|
12
|
+
*
|
|
13
|
+
* A Durable Object class is constructed by the runtime, so it cannot take
|
|
14
|
+
* constructor arguments — which is exactly the problem a library version of this
|
|
15
|
+
* facet has to solve. {@link RecipeSubagentBase} takes it from an abstract
|
|
16
|
+
* method instead: the consumer subclasses once, and everything host-specific
|
|
17
|
+
* arrives through that one seam.
|
|
18
|
+
*/
|
|
19
|
+
export interface SubagentRuntime {
|
|
20
|
+
policy: RecipePolicy;
|
|
21
|
+
types: SubtaskTypeRegistry;
|
|
22
|
+
models: ModelRuntime;
|
|
23
|
+
toolFamilies: ReadonlyMap<string, ToolFamilyBuilder>;
|
|
24
|
+
/** `CoreConfig.toolOutputWindow`. */
|
|
25
|
+
toolOutputWindow: number;
|
|
26
|
+
/** `CoreConfig.model.maxOutputTokens`. */
|
|
27
|
+
maxOutputTokens: number;
|
|
28
|
+
/** `CoreConfig.model.maxRetries`. */
|
|
29
|
+
maxRetries: number;
|
|
30
|
+
/**
|
|
31
|
+
* Build the durable file store over this facet's own SQLite.
|
|
32
|
+
*
|
|
33
|
+
* Supplied by the host because the backend is a plugin's: core declares the
|
|
34
|
+
* {@link WorkspaceBacking} shape and the caps, `@dynamicagents/plugins/workspace`
|
|
35
|
+
* supplies the `@cloudflare/shell` implementation, and an agent that never
|
|
36
|
+
* delegates file work installs neither.
|
|
37
|
+
*/
|
|
38
|
+
workspaceBacking(sql: SqlStorage, name: () => string | undefined): WorkspaceBacking;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Message prefix of the error thrown when a child that already holds a cached
|
|
42
|
+
* terminal result receives a *different* request. Custom error classes don't
|
|
43
|
+
* survive DO RPC, so this prefix is the cross-boundary contract: it signals a
|
|
44
|
+
* parent lifecycle bug — stale children must be deleted before a genuinely new
|
|
45
|
+
* execution — and a Workflow retry after the parent's cleanup will succeed.
|
|
46
|
+
*/
|
|
47
|
+
export declare const FINGERPRINT_MISMATCH = "recipe-subagent: request fingerprint mismatch";
|
|
48
|
+
/** Deterministic managed-child name for one Subtask execution. */
|
|
49
|
+
export declare function subagentName(taskId: string, subtaskId: SubtaskId): string;
|
|
50
|
+
/**
|
|
51
|
+
* `RecipeSubagent` — the isolated, stateless managed child that executes one
|
|
52
|
+
* Subtask under a resolved Recipe. Created as an Agents SDK sub-agent (facet)
|
|
53
|
+
* beneath the calling agent, so it needs no wrangler Durable Object binding and
|
|
54
|
+
* no `new_sqlite_classes` entry; it must only be exported from the consuming
|
|
55
|
+
* Worker's entry so `ctx.exports` can resolve it by class name.
|
|
56
|
+
*
|
|
57
|
+
* It never constructs a Session, never reads parent history beyond the
|
|
58
|
+
* references supplied on its request, never reaches durable memory, and never
|
|
59
|
+
* resolves a Recipe itself — it defensively re-validates the resolved Recipe the
|
|
60
|
+
* parent sends and accepts no configuration beyond it.
|
|
61
|
+
*
|
|
62
|
+
* Retry safety: the child persists at most one terminal result in its own
|
|
63
|
+
* SQLite, keyed by the deterministic request fingerprint, plus the rolling
|
|
64
|
+
* `run_state` of an in-progress multi-chunk run. A retry with the same
|
|
65
|
+
* fingerprint replays the terminal result or resumes the run without repeating
|
|
66
|
+
* completed work; a different request for the same child name is rejected
|
|
67
|
+
* ({@link FINGERPRINT_MISMATCH}). Transient platform faults throw and cache
|
|
68
|
+
* nothing, so the enclosing Workflow step can retry. The parent deletes the child
|
|
69
|
+
* (`deleteSubAgent`) only after its durable copy of the result succeeds, which
|
|
70
|
+
* wipes this storage — the workspace and run state included.
|
|
71
|
+
*
|
|
72
|
+
* Not "stateless" like the single-shot original: it owns per-execution durable
|
|
73
|
+
* state (the workspace and the run checkpoint), scoped to one execution and swept
|
|
74
|
+
* with the child.
|
|
75
|
+
*/
|
|
76
|
+
export declare abstract class RecipeSubagentBase<TEnv extends Cloudflare.Env = Cloudflare.Env> extends Agent<TEnv> {
|
|
77
|
+
/**
|
|
78
|
+
* Supply the host runtime. Called per RPC, not memoized here — an
|
|
79
|
+
* implementation that builds something expensive should memoize its own.
|
|
80
|
+
*
|
|
81
|
+
* ```ts
|
|
82
|
+
* export class RecipeSubagent extends RecipeSubagentBase<Env> {
|
|
83
|
+
* protected subagentRuntime() {
|
|
84
|
+
* return (this._rt ??= buildSubagentRuntime(this.env));
|
|
85
|
+
* }
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
protected abstract subagentRuntime(): SubagentRuntime;
|
|
90
|
+
/**
|
|
91
|
+
* Test-only `ModelPair` injection (a field, so never on the RPC stub).
|
|
92
|
+
* A whole pair — rather than model instances — so error-path tests can throw
|
|
93
|
+
* synchronously from the pair's factories, the repo convention (a rejecting
|
|
94
|
+
* `doGenerate` inside `generateText` leaks an unhandled rejection through
|
|
95
|
+
* the AI SDK telemetry span that workerd flags as a failure).
|
|
96
|
+
*/
|
|
97
|
+
modelsOverride?: ModelPair;
|
|
98
|
+
private _workspace?;
|
|
99
|
+
/**
|
|
100
|
+
* The chunk currently executing here, if any. In memory only — it exists to be
|
|
101
|
+
* interrupted mid-call, and an isolate that lost it has no in-flight call left
|
|
102
|
+
* to interrupt. See {@link abortRun}.
|
|
103
|
+
*/
|
|
104
|
+
private inflight?;
|
|
105
|
+
/**
|
|
106
|
+
* This deployment's own public origin, as the parent DO passes it on every
|
|
107
|
+
* chunk, pinned from the first. In memory for the same reason {@link inflight}
|
|
108
|
+
* is: a facet is reached only through {@link executeChunk}, so an instance that
|
|
109
|
+
* lost it is an instance that will be told again before it can run anything.
|
|
110
|
+
* See {@link SelfOrigin}.
|
|
111
|
+
*/
|
|
112
|
+
private readonly selfOriginMemo;
|
|
113
|
+
onStart(): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* Idempotent schema bootstrap. Also called lazily from the RPCs so
|
|
116
|
+
* `runInDurableObject`-style tests reach ready tables without RPC dispatch
|
|
117
|
+
* (mirroring how `AgentDB` migrates on construction).
|
|
118
|
+
*/
|
|
119
|
+
private ensureTables;
|
|
120
|
+
/** The recipe's durable workspace, backed by this facet's own SQLite storage. */
|
|
121
|
+
private workspace;
|
|
122
|
+
/**
|
|
123
|
+
* This deployment's own public origin, if the parent has passed it to this
|
|
124
|
+
* instance yet. See {@link SelfOrigin}.
|
|
125
|
+
*/
|
|
126
|
+
protected selfOrigin(): string | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* The same, for a caller that cannot proceed without it — a facet that signs
|
|
129
|
+
* its own caller tokens, above all. Mirrors `DynamicAgent.requireSelfOrigin`,
|
|
130
|
+
* because a facet must run on the same provider, and so the same credential
|
|
131
|
+
* path, as the parent that delegated to it.
|
|
132
|
+
*/
|
|
133
|
+
protected requireSelfOrigin(): string;
|
|
134
|
+
/**
|
|
135
|
+
* Execute one durable chunk of a Subtask under the parent's resolved Recipe.
|
|
136
|
+
*
|
|
137
|
+
* A terminal outcome (completed / failed) is cached and replayed on retry. A
|
|
138
|
+
* mid-run chunk persists its rolling state to `run_state` and returns a
|
|
139
|
+
* `done: false` yield for the Workflow to run another chunk. `chunk` and
|
|
140
|
+
* `selfOrigin` are separate arguments — never part of `request` — so every
|
|
141
|
+
* chunk fingerprints identically and the cache/resume keys line up. Only
|
|
142
|
+
* transient platform faults throw (nothing cached), so a Workflow retry
|
|
143
|
+
* resumes from the last checkpoint.
|
|
144
|
+
*/
|
|
145
|
+
executeChunk(request: RecipeExecutionRequest, _chunk: number, runtime?: SubtaskRuntime, selfOrigin?: string): Promise<RecipeChunkResult>;
|
|
146
|
+
/**
|
|
147
|
+
* Interrupt the chunk running here right now, so a cancellation lands on the
|
|
148
|
+
* current model call instead of at the next chunk boundary (up to `chunkSoftMs`
|
|
149
|
+
* later — minutes, for a long recipe). Returns whether there was one to stop.
|
|
150
|
+
*
|
|
151
|
+
* Distinct from {@link abortExecution}, which releases *external* state after
|
|
152
|
+
* the fact; this only stops local work. An aborted run yields rather than
|
|
153
|
+
* producing a terminal result, so nothing is cached and the parent resolves the
|
|
154
|
+
* row itself. Reaching a facet mid-`executeChunk` works because it is awaiting
|
|
155
|
+
* a model `fetch` at the time, which does not hold the input gate closed.
|
|
156
|
+
*/
|
|
157
|
+
abortRun(): Promise<boolean>;
|
|
158
|
+
/**
|
|
159
|
+
* Best-effort cleanup on cancellation: rebuild the recipe's tool families and
|
|
160
|
+
* run their `abort` hooks (e.g. release an external resource recorded in the
|
|
161
|
+
* workspace session file). Reconstructible from the workspace, so it is safe on a fresh
|
|
162
|
+
* isolate. The parent supplies the validated tool families it resolved.
|
|
163
|
+
*/
|
|
164
|
+
abortExecution(toolFamilies: string[]): Promise<void>;
|
|
165
|
+
/** Persist a terminal result to the cache and return it as a done chunk. */
|
|
166
|
+
private cacheTerminal;
|
|
167
|
+
/** Persist the rolling run state (called after every model turn). */
|
|
168
|
+
private saveRunState;
|
|
169
|
+
}
|