@dynamicagents/core 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +522 -0
  3. package/dist/a2a/agent-stub.d.ts +60 -0
  4. package/dist/a2a/agent-stub.js +1 -0
  5. package/dist/a2a/caller-token.d.ts +44 -0
  6. package/dist/a2a/caller-token.js +61 -0
  7. package/dist/a2a/caller.d.ts +23 -0
  8. package/dist/a2a/caller.js +33 -0
  9. package/dist/a2a/card.d.ts +158 -0
  10. package/dist/a2a/card.js +163 -0
  11. package/dist/a2a/context.d.ts +19 -0
  12. package/dist/a2a/context.js +61 -0
  13. package/dist/a2a/deliver.d.ts +159 -0
  14. package/dist/a2a/deliver.js +185 -0
  15. package/dist/a2a/executor.d.ts +84 -0
  16. package/dist/a2a/executor.js +106 -0
  17. package/dist/a2a/index.d.ts +35 -0
  18. package/dist/a2a/index.js +34 -0
  19. package/dist/a2a/notify.d.ts +106 -0
  20. package/dist/a2a/notify.js +180 -0
  21. package/dist/a2a/parts.d.ts +45 -0
  22. package/dist/a2a/parts.js +94 -0
  23. package/dist/a2a/push.d.ts +70 -0
  24. package/dist/a2a/push.js +53 -0
  25. package/dist/a2a/self-origin.d.ts +91 -0
  26. package/dist/a2a/self-origin.js +114 -0
  27. package/dist/a2a/task-store.d.ts +44 -0
  28. package/dist/a2a/task-store.js +99 -0
  29. package/dist/a2a/task.d.ts +85 -0
  30. package/dist/a2a/task.js +15 -0
  31. package/dist/a2a/verify.d.ts +80 -0
  32. package/dist/a2a/verify.js +143 -0
  33. package/dist/agent/budget.d.ts +46 -0
  34. package/dist/agent/budget.js +42 -0
  35. package/dist/agent/control.d.ts +109 -0
  36. package/dist/agent/control.js +115 -0
  37. package/dist/agent/errors.d.ts +85 -0
  38. package/dist/agent/errors.js +64 -0
  39. package/dist/agent/final-reply.d.ts +49 -0
  40. package/dist/agent/final-reply.js +68 -0
  41. package/dist/agent/history.d.ts +97 -0
  42. package/dist/agent/history.js +133 -0
  43. package/dist/agent/index.d.ts +29 -0
  44. package/dist/agent/index.js +29 -0
  45. package/dist/agent/inference.d.ts +110 -0
  46. package/dist/agent/inference.js +120 -0
  47. package/dist/agent/model.d.ts +90 -0
  48. package/dist/agent/model.js +1 -0
  49. package/dist/agent/session.d.ts +100 -0
  50. package/dist/agent/session.js +82 -0
  51. package/dist/agent/workers-ai/index.d.ts +23 -0
  52. package/dist/agent/workers-ai/index.js +23 -0
  53. package/dist/agent/workers-ai/runtime.d.ts +42 -0
  54. package/dist/agent/workers-ai/runtime.js +63 -0
  55. package/dist/alarm/index.d.ts +77 -0
  56. package/dist/alarm/index.js +116 -0
  57. package/dist/config.d.ts +202 -0
  58. package/dist/config.js +135 -0
  59. package/dist/contract/index.d.ts +9 -0
  60. package/dist/contract/index.js +8 -0
  61. package/dist/contract/plugin.d.ts +324 -0
  62. package/dist/contract/plugin.js +114 -0
  63. package/dist/contract/recipe.d.ts +180 -0
  64. package/dist/contract/recipe.js +1 -0
  65. package/dist/contract/validation.d.ts +91 -0
  66. package/dist/contract/validation.js +84 -0
  67. package/dist/db/db.d.ts +147 -0
  68. package/dist/db/db.js +90 -0
  69. package/dist/db/index.d.ts +8 -0
  70. package/dist/db/index.js +8 -0
  71. package/dist/db/migrations/index.d.ts +20 -0
  72. package/dist/db/migrations/index.js +60 -0
  73. package/dist/db/models/subtasks.d.ts +100 -0
  74. package/dist/db/models/subtasks.js +241 -0
  75. package/dist/db/models/tasks.d.ts +118 -0
  76. package/dist/db/models/tasks.js +274 -0
  77. package/dist/db/schema.d.ts +468 -0
  78. package/dist/db/schema.js +88 -0
  79. package/dist/env.d.ts +53 -0
  80. package/dist/env.js +47 -0
  81. package/dist/host/agent.d.ts +305 -0
  82. package/dist/host/agent.js +400 -0
  83. package/dist/host/index.d.ts +20 -0
  84. package/dist/host/index.js +19 -0
  85. package/dist/host/plugin-host.d.ts +42 -0
  86. package/dist/host/plugin-host.js +1 -0
  87. package/dist/index.d.ts +19 -0
  88. package/dist/index.js +17 -0
  89. package/dist/job/index.d.ts +21 -0
  90. package/dist/job/index.js +21 -0
  91. package/dist/job/lifecycle.d.ts +176 -0
  92. package/dist/job/lifecycle.js +230 -0
  93. package/dist/job/state.d.ts +92 -0
  94. package/dist/job/state.js +40 -0
  95. package/dist/platform.d.ts +138 -0
  96. package/dist/platform.js +140 -0
  97. package/dist/round/agent.d.ts +271 -0
  98. package/dist/round/agent.js +678 -0
  99. package/dist/round/index.d.ts +25 -0
  100. package/dist/round/index.js +23 -0
  101. package/dist/round/policy.d.ts +98 -0
  102. package/dist/round/policy.js +1 -0
  103. package/dist/round/subagent.d.ts +87 -0
  104. package/dist/round/subagent.js +108 -0
  105. package/dist/round/turn.d.ts +249 -0
  106. package/dist/round/turn.js +564 -0
  107. package/dist/round/workflow.d.ts +147 -0
  108. package/dist/round/workflow.js +387 -0
  109. package/dist/runtime/index.d.ts +127 -0
  110. package/dist/runtime/index.js +186 -0
  111. package/dist/runtime/tool-families.d.ts +32 -0
  112. package/dist/runtime/tool-families.js +61 -0
  113. package/dist/subagent/fingerprint.d.ts +37 -0
  114. package/dist/subagent/fingerprint.js +92 -0
  115. package/dist/subagent/index.d.ts +169 -0
  116. package/dist/subagent/index.js +330 -0
  117. package/dist/subagent/prompt.d.ts +32 -0
  118. package/dist/subagent/prompt.js +37 -0
  119. package/dist/subagent/run.d.ts +157 -0
  120. package/dist/subagent/run.js +540 -0
  121. package/dist/subagent/workspace.d.ts +85 -0
  122. package/dist/subagent/workspace.js +127 -0
  123. package/dist/subtasks/catalog.d.ts +40 -0
  124. package/dist/subtasks/catalog.js +36 -0
  125. package/dist/subtasks/decomposition.d.ts +85 -0
  126. package/dist/subtasks/decomposition.js +156 -0
  127. package/dist/subtasks/delegate.d.ts +120 -0
  128. package/dist/subtasks/delegate.js +131 -0
  129. package/dist/subtasks/index.d.ts +9 -0
  130. package/dist/subtasks/index.js +9 -0
  131. package/dist/subtasks/subtask-types.d.ts +91 -0
  132. package/dist/subtasks/subtask-types.js +103 -0
  133. package/dist/subtasks/types.d.ts +295 -0
  134. package/dist/subtasks/types.js +15 -0
  135. package/dist/testing/auth.d.ts +34 -0
  136. package/dist/testing/auth.js +35 -0
  137. package/dist/testing/do.d.ts +29 -0
  138. package/dist/testing/do.js +25 -0
  139. package/dist/testing/fake-session.d.ts +26 -0
  140. package/dist/testing/fake-session.js +37 -0
  141. package/dist/testing/fixtures.d.ts +64 -0
  142. package/dist/testing/fixtures.js +104 -0
  143. package/dist/testing/harness.d.ts +97 -0
  144. package/dist/testing/harness.js +138 -0
  145. package/dist/testing/index.d.ts +31 -0
  146. package/dist/testing/index.js +35 -0
  147. package/dist/testing/mock-model.d.ts +77 -0
  148. package/dist/testing/mock-model.js +136 -0
  149. package/dist/testing/node.d.ts +56 -0
  150. package/dist/testing/node.js +56 -0
  151. package/dist/testing/vcr-global-setup.d.ts +12 -0
  152. package/dist/testing/vcr-global-setup.js +15 -0
  153. package/dist/testing/vcr-shared.d.ts +38 -0
  154. package/dist/testing/vcr-shared.js +33 -0
  155. package/dist/testing/vcr-spec.d.ts +25 -0
  156. package/dist/testing/vcr-spec.js +124 -0
  157. package/dist/testing/vcr-store.d.ts +86 -0
  158. package/dist/testing/vcr-store.js +191 -0
  159. package/dist/testing/vcr.d.ts +117 -0
  160. package/dist/testing/vcr.js +275 -0
  161. package/dist/worker/define-agent.d.ts +123 -0
  162. package/dist/worker/define-agent.js +20 -0
  163. package/dist/worker/index.d.ts +218 -0
  164. package/dist/worker/index.js +369 -0
  165. package/eslint-rules/index.js +31 -0
  166. package/eslint-rules/no-deprecated-object-properties.js +81 -0
  167. package/package.json +178 -0
  168. package/scripts/generate-keys.mjs +48 -0
@@ -0,0 +1,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
+ }