@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
package/dist/config.js ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The shapes of everything an agent tunes, plus a working set of defaults.
3
+ *
4
+ * In the predecessor repos this file held bare `export const`s that ~12 modules
5
+ * imported directly. That is what this package cannot do: a module-level constant
6
+ * read at import time is not overridable by a consumer, and it freezes the value
7
+ * into the module graph before `env` exists. So core owns the *shapes* and a
8
+ * baseline; a consuming agent passes overrides to {@link resolveConfig} once, at
9
+ * DO start, and the resolved object is threaded through explicitly.
10
+ *
11
+ * The distinction against {@link file://./platform.ts} stays sharp: nothing here
12
+ * is a platform fact, and nothing there is tunable.
13
+ */
14
+ import { STEP_TIMEOUT_MS } from "./platform.js";
15
+ /**
16
+ * Ceiling on {@link ModelConfig.maxRetries}.
17
+ *
18
+ * Derived, not chosen: each honoured `retry-after` can be up to 60s and they all
19
+ * run inside one `step.do`, so this holds the worst case to a small fraction of
20
+ * {@link STEP_TIMEOUT_MS} and leaves the round its own time to actually work.
21
+ */
22
+ const MAX_MODEL_RETRIES = 4;
23
+ /**
24
+ * A working baseline. Every value is overridable; none is a ceiling. These are
25
+ * the values both predecessor agents converged on in production, so they are a
26
+ * reasonable place to start rather than an opinion about your domain.
27
+ *
28
+ * **It carries no model ids.** See {@link ModelConfig.chatModelId}.
29
+ */
30
+ export const DEFAULT_CORE_CONFIG = {
31
+ model: {
32
+ // Not a model: an AI Gateway slug, and `"default"` is Cloudflare's own
33
+ // auto-provision behaviour rather than a choice core is making for anyone.
34
+ aiGatewayId: "default",
35
+ maxOutputTokens: 16_384,
36
+ reasoningEffort: "medium",
37
+ // Two, which is the AI SDK's own default and enough to ride out the kind of
38
+ // rate limit that clears in seconds. Raising it trades round latency for
39
+ // resilience; see {@link ModelConfig.maxRetries} for the ceiling and why.
40
+ maxRetries: 2
41
+ },
42
+ mainAgentLimits: { maxTurns: 20, maxWallMs: 60 * 60_000 },
43
+ subagentLimits: { maxTurns: 20, maxWallMs: 30 * 60_000 },
44
+ toolOutputWindow: 4,
45
+ maxSubtasks: 8,
46
+ session: {
47
+ memoryMaxTokens: 1200,
48
+ compactAfterTokens: 16_000,
49
+ compactTailTokens: 5_000,
50
+ memoryDescription: "Durable facts worth remembering across all of this caller's conversations — " +
51
+ "stable preferences, decisions, people, and context. Keep it concise."
52
+ }
53
+ };
54
+ export class ConfigError extends Error {
55
+ constructor(message) {
56
+ super(message);
57
+ this.name = "ConfigError";
58
+ }
59
+ }
60
+ /**
61
+ * Merge overrides onto {@link DEFAULT_CORE_CONFIG} and check the invariants that
62
+ * are cheap to get wrong and expensive to notice.
63
+ *
64
+ * Called once, at DO start, and the result threaded explicitly from there. Do
65
+ * not call it per-request: the point of resolving is that every module downstream
66
+ * reads the same object.
67
+ */
68
+ export function resolveConfig(overrides) {
69
+ const config = {
70
+ model: { ...DEFAULT_CORE_CONFIG.model, ...overrides.model },
71
+ mainAgentLimits: {
72
+ ...DEFAULT_CORE_CONFIG.mainAgentLimits,
73
+ ...overrides.mainAgentLimits
74
+ },
75
+ subagentLimits: {
76
+ ...DEFAULT_CORE_CONFIG.subagentLimits,
77
+ ...overrides.subagentLimits
78
+ },
79
+ toolOutputWindow: overrides.toolOutputWindow ?? DEFAULT_CORE_CONFIG.toolOutputWindow,
80
+ maxSubtasks: overrides.maxSubtasks ?? DEFAULT_CORE_CONFIG.maxSubtasks,
81
+ session: { ...DEFAULT_CORE_CONFIG.session, ...overrides.session }
82
+ };
83
+ const positive = (value, name) => {
84
+ if (!Number.isFinite(value) || value <= 0) {
85
+ throw new ConfigError(`${name} must be a positive number, got ${value}`);
86
+ }
87
+ };
88
+ // The type already requires both ids; this catches the JavaScript consumer,
89
+ // the `as CoreConfigOverrides` cast, and the empty string — which typechecks
90
+ // and then reaches Workers AI as a model that does not exist, failing on the
91
+ // first generation with a binding error that names nothing useful.
92
+ const modelId = (value, name) => {
93
+ if (typeof value !== "string" || value.trim() === "") {
94
+ throw new ConfigError(`model.${name} is required and must be a non-empty model id — core ` +
95
+ `ships no default, because which model an agent runs on is the agent's ` +
96
+ `most consequential choice and cannot be made on its behalf`);
97
+ }
98
+ };
99
+ modelId(config.model.chatModelId, "chatModelId");
100
+ modelId(config.model.fallbackChatModelId, "fallbackChatModelId");
101
+ // A fallback identical to the primary is a retry wearing a costume: it shares
102
+ // the outage, the rate limit and the deprecation you are falling back from.
103
+ if (config.model.chatModelId === config.model.fallbackChatModelId) {
104
+ throw new ConfigError(`model.fallbackChatModelId must differ from model.chatModelId (both are ` +
105
+ `'${config.model.chatModelId}') — a same-model fallback shares every ` +
106
+ `failure mode you are falling back from`);
107
+ }
108
+ positive(config.mainAgentLimits.maxTurns, "mainAgentLimits.maxTurns");
109
+ positive(config.mainAgentLimits.maxWallMs, "mainAgentLimits.maxWallMs");
110
+ positive(config.subagentLimits.maxTurns, "subagentLimits.maxTurns");
111
+ positive(config.subagentLimits.maxWallMs, "subagentLimits.maxWallMs");
112
+ positive(config.toolOutputWindow, "toolOutputWindow");
113
+ positive(config.maxSubtasks, "maxSubtasks");
114
+ positive(config.model.maxOutputTokens, "model.maxOutputTokens");
115
+ // Zero is legal (it opts out), so this is not `positive`. The ceiling is the
116
+ // step timeout: retries happen inside `step.do("turn:<round>")`, and the AI
117
+ // SDK honours a `retry-after` of up to 60s per attempt — so `maxRetries`
118
+ // minutes of waiting inside one step, before the round has spent a single
119
+ // token of its own. The message below reads the real number off `platform.ts`.
120
+ if (!Number.isInteger(config.model.maxRetries) ||
121
+ config.model.maxRetries < 0 ||
122
+ config.model.maxRetries > MAX_MODEL_RETRIES) {
123
+ throw new ConfigError(`model.maxRetries must be an integer between 0 and ${MAX_MODEL_RETRIES}, got ` +
124
+ `${config.model.maxRetries} — retries run inside one Workflow step, and each ` +
125
+ `honoured retry-after can be up to 60s against a ${STEP_TIMEOUT_MS / 60_000}-minute step timeout`);
126
+ }
127
+ // See SessionConfig.compactTailTokens — below this gap the fixed floor eats
128
+ // the headroom and compaction fires on nearly every append.
129
+ const headroom = config.session.compactAfterTokens - config.session.compactTailTokens;
130
+ if (headroom < 10_000) {
131
+ throw new ConfigError(`session.compactAfterTokens - session.compactTailTokens must be >= 10000, got ${headroom}. ` +
132
+ "Lower compactTailTokens along with compactAfterTokens.");
133
+ }
134
+ return config;
135
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The plugin contract — the only thing core and a plugin package both name.
3
+ *
4
+ * Re-exported from the package root as well, so a plugin author writes
5
+ * `import { definePlugin } from "@dynamicagents/core"` and nothing else.
6
+ */
7
+ export { PLUGIN_CONTRACT_VERSION, definePlugin, type AgentPlugin, type EmitProgress, type EnrichResultContext, type MainAgentToolContext, type PluginRequirements, type RecipeToolSet, type ResolveRuntimeContext, type ToolFamilyBuilder, type ToolFamilyContext, type TurnGateContext } from "./plugin.js";
8
+ export type { DelegationNames, RecipeLimits, ResolvedRecipe, SubtaskParams, SubtaskParamsSchema, SubtaskParamsShape, SubtaskTypeSpec, ValidatedRecipe } from "./recipe.js";
9
+ export { RecipeValidationError, resolveLimits, validateRecipe, type RecipePolicy } from "./validation.js";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The plugin contract — the only thing core and a plugin package both name.
3
+ *
4
+ * Re-exported from the package root as well, so a plugin author writes
5
+ * `import { definePlugin } from "@dynamicagents/core"` and nothing else.
6
+ */
7
+ export { PLUGIN_CONTRACT_VERSION, definePlugin } from "./plugin.js";
8
+ export { RecipeValidationError, resolveLimits, validateRecipe } from "./validation.js";
@@ -0,0 +1,324 @@
1
+ import type { ToolSet } from "ai";
2
+ import type { SessionMessage } from "agents/experimental/memory/session";
3
+ import type { SessionLike } from "../agent/session.js";
4
+ import type { PluginStore } from "../db/db.js";
5
+ import type { WorkspaceBacking, WorkspaceHandle } from "../subagent/workspace.js";
6
+ import type { ProgressEvent, RecipeExecutionRequest, RecipeExecutionResult, SubtaskRuntime } from "../subtasks/types.js";
7
+ import type { SubtaskParams, SubtaskTypeSpec } from "./recipe.js";
8
+ /**
9
+ * The plugin contract — everything an independently-packaged capability may
10
+ * contribute to an agent, and the only thing core knows about one.
11
+ *
12
+ * Nothing in core imports a plugin. A plugin imports core (type-only wherever it
13
+ * can) and is registered by the *host* at DO start. That direction is what makes
14
+ * bundle growth proportional to what an agent actually installs, and it is
15
+ * structural rather than a tree-shaker's opinion.
16
+ */
17
+ /**
18
+ * The contract version a plugin was built against.
19
+ *
20
+ * Three separate repos means a contract change is a three-repo publish train, and
21
+ * the failure mode of a skew is a structural-type mismatch several frames from
22
+ * its cause. `createAgentRuntime` asserts this instead, so a mismatched plugin
23
+ * fails at DO start with a sentence naming the plugin and both versions.
24
+ *
25
+ * The contract is **additive-only** within a major: new capabilities arrive as
26
+ * optional fields on {@link AgentPlugin}. Removing or re-typing an existing field
27
+ * requires a major and a bump here.
28
+ */
29
+ export declare const PLUGIN_CONTRACT_VERSION = 1;
30
+ /**
31
+ * Emit a user-facing progress note from inside a tool (e.g. a level-up in a
32
+ * game, a milestone in a long scrape). The resumable runner collects these and
33
+ * ends the current chunk so the parent can post them promptly. Best-effort — the
34
+ * runner never lets a progress note affect generation.
35
+ */
36
+ export type EmitProgress = (event: ProgressEvent) => void;
37
+ /**
38
+ * Everything a tool family needs to build its tools, closed over so none of it
39
+ * is ever model input.
40
+ *
41
+ * Note what is **not** here: the Worker `env`. The predecessor passed it, which
42
+ * a published package cannot do — `Env` is the ambient interface `wrangler
43
+ * types` generates into a consumer's `worker-configuration.d.ts` and does not
44
+ * exist outside their app. A plugin takes its secrets and bindings as *config at
45
+ * instantiation* instead, which is also the only thing that works on Workers,
46
+ * where `env` does not exist at module scope.
47
+ */
48
+ export interface ToolFamilyContext<TRuntime = SubtaskRuntime> {
49
+ /** The execution's durable file store. */
50
+ workspace: WorkspaceHandle;
51
+ emitProgress: EmitProgress;
52
+ /**
53
+ * The subtask's validated params — the ids its type declared it needs. Chosen
54
+ * by the delegating main agent and checked against its type's contract before
55
+ * this execution began, so a family may read them directly; they are not this
56
+ * model's input.
57
+ */
58
+ params: SubtaskParams;
59
+ /**
60
+ * Session state the parent resolved for this execution — what no model could
61
+ * supply and none should be asked to. Opaque to core; a plugin narrows it to
62
+ * whatever its own {@link AgentPlugin.resolveRuntime} wrote.
63
+ */
64
+ runtime: TRuntime;
65
+ }
66
+ /**
67
+ * A tool family's contribution: its tools, plus an optional `abort` hook the
68
+ * facet runs on cancellation to release external state the family acquired.
69
+ *
70
+ * Anything the hook needs must be reconstructible from the workspace, so it is
71
+ * safe to run on a fresh isolate after eviction.
72
+ */
73
+ export interface RecipeToolSet<TRuntime = SubtaskRuntime> {
74
+ tools: ToolSet;
75
+ abort?: (ctx: ToolFamilyContext<TRuntime>) => Promise<void>;
76
+ }
77
+ /** Builds one tool family's contribution for a single execution. */
78
+ export type ToolFamilyBuilder<TRuntime = SubtaskRuntime> = (ctx: ToolFamilyContext<TRuntime>) => RecipeToolSet<TRuntime>;
79
+ /** What the parent knows when resolving an execution's runtime state. */
80
+ export interface ResolveRuntimeContext {
81
+ taskId: string;
82
+ subtaskId: number;
83
+ type: string;
84
+ params: SubtaskParams;
85
+ toolFamilies: readonly string[];
86
+ }
87
+ /** What the parent knows when a plugin gets to enrich a terminal result. */
88
+ export interface EnrichResultContext<TRuntime = SubtaskRuntime> {
89
+ request: RecipeExecutionRequest;
90
+ runtime: TRuntime;
91
+ }
92
+ /**
93
+ * What a plugin knows when it builds the *main* agent's tools.
94
+ *
95
+ * Deliberately just the session, and deliberately not the caller's identity. A
96
+ * plugin that needs a per-caller value takes it as config at instantiation, like
97
+ * every other config value — the Durable Object is keyed 1:1 by the verified
98
+ * caller, so that value is constant for its life. Putting it here as well would
99
+ * give a plugin two ways to reach one fact, and the *other* hook that needs it
100
+ * ({@link AgentPlugin.onMessagesDisplaced}) has no context to read it from
101
+ * anyway.
102
+ *
103
+ * What the session gives that config cannot is **durable state the tool surface
104
+ * depends on** — whether history has ever been compacted, how many contexts are
105
+ * set. That is a question only the session can answer, and only at call time.
106
+ */
107
+ export interface MainAgentToolContext {
108
+ session: SessionLike;
109
+ }
110
+ /** What a plugin knows when deciding whether a turn should run at all. */
111
+ export interface TurnGateContext {
112
+ /**
113
+ * The conversation so far, **including the message being judged** — which is
114
+ * already appended when a gate runs, so the agent reads a message it declines.
115
+ * A bare message is frequently unclassifiable ("yes", "thanks", "and the
116
+ * second one?"), so the tail is what makes the judgement possible at all.
117
+ */
118
+ history: SessionMessage[];
119
+ }
120
+ /** Bindings and secrets a plugin needs the *host* to provide in `wrangler.jsonc`. */
121
+ export interface PluginRequirements {
122
+ /** Secret names, e.g. `["ARC_API_KEY"]`. */
123
+ secrets?: readonly string[];
124
+ /** Binding names, e.g. `["BROWSER"]`. */
125
+ bindings?: readonly string[];
126
+ }
127
+ export interface AgentPlugin<TRuntime = SubtaskRuntime> {
128
+ /** Stable identifier, unique across installed plugins. */
129
+ key: string;
130
+ /**
131
+ * The {@link PLUGIN_CONTRACT_VERSION} this plugin was built against. Set it
132
+ * from the imported constant, never as a literal — the point is that it moves
133
+ * with the core the plugin compiled against.
134
+ */
135
+ contractVersion: number;
136
+ /**
137
+ * The subtask type this plugin makes delegable, and the recipe it runs under.
138
+ * A plugin that only contributes main-agent tools declares none.
139
+ */
140
+ subtaskType?: SubtaskTypeSpec;
141
+ /**
142
+ * Tool families this plugin registers, keyed by family name. A recipe selects
143
+ * families by name; a name no installed plugin registers is dropped by
144
+ * `validateRecipe`, so the legal set is exactly what is installed.
145
+ */
146
+ toolFamilies?: Record<string, ToolFamilyBuilder<TRuntime>>;
147
+ /**
148
+ * Tools offered to the *main* agent (e.g. a catalogue lookup before
149
+ * delegating).
150
+ *
151
+ * May return a promise, so a plugin can shape its tool surface from durable
152
+ * state — offering a search tool only once there is something to search, say.
153
+ * A tool that can only ever return "nothing here yet" costs the model a call to
154
+ * find that out, and costs every round the tokens to describe it.
155
+ */
156
+ mainAgentTools?: (ctx: MainAgentToolContext) => ToolSet | Promise<ToolSet>;
157
+ /**
158
+ * What the main agent is told it can do with this domain, rendered into its
159
+ * soul alongside the other capability blocks.
160
+ *
161
+ * A plugin that declares a {@link subtaskType} should put its capability block
162
+ * on the *type* instead ({@link SubtaskTypeSpec.capability}) and leave this
163
+ * unset. Both are rendered, so declaring both makes the main agent read the
164
+ * same advice twice per round — the exact failure the type's own prompt
165
+ * fields were introduced to end.
166
+ */
167
+ capability?: string;
168
+ /**
169
+ * Resolve the session state an execution needs and no model can supply — a
170
+ * leased external resource, a session handle, a cookie jar.
171
+ *
172
+ * Called by the parent before each chunk, and deliberately outside the
173
+ * execution's fingerprint: what it returns can legitimately change between two
174
+ * chunks of one run, and must not make a retry look like different work.
175
+ */
176
+ resolveRuntime?: (ctx: ResolveRuntimeContext) => Promise<TRuntime>;
177
+ /**
178
+ * Amend a terminal result before it is persisted — e.g. append a score the
179
+ * subagent had no way to read. Returning the result unchanged is always valid.
180
+ */
181
+ enrichResult?: (ctx: EnrichResultContext<TRuntime>, result: RecipeExecutionResult) => Promise<RecipeExecutionResult>;
182
+ /** Release anything {@link resolveRuntime} acquired, when an execution is canceled. */
183
+ onAbort?: (ctx: ResolveRuntimeContext) => Promise<void>;
184
+ /**
185
+ * Decide whether a turn should run at all, before the loop builds or calls
186
+ * anything.
187
+ *
188
+ * An agent that sees every message in its channels is mostly seeing messages
189
+ * that are not for it. Left to the main loop that judgement is made by a model
190
+ * simultaneously trying to be helpful, with history and half a dozen tools in
191
+ * view, and it degrades exactly there — *invisibly*, because failing to call a
192
+ * decline-tool looks identical to deciding not to. A gate moves the decision
193
+ * somewhere it cannot be skipped.
194
+ *
195
+ * **Fails open, and the asymmetry is the whole design.** A gate that throws is
196
+ * counted as `true`, so an outage degrades to the previous behaviour (run the
197
+ * turn) and never to a silent agent: a wrong reply is noise the user can see
198
+ * and ignore, while a wrong silence is invisible — the person who needed the
199
+ * agent simply never hears back. Failing *synchronously* is as safe as
200
+ * rejecting.
201
+ *
202
+ * Every declaring plugin is consulted and the results are AND-ed: any one gate
203
+ * may decline the turn. Returning `true` is always valid.
204
+ */
205
+ shouldHandleTurn?: (ctx: TurnGateContext) => Promise<boolean>;
206
+ /**
207
+ * The raw messages a compaction is about to fold into a summary, handed over
208
+ * before they stop being readable as history.
209
+ *
210
+ * This is core being honest about a **lossy operation it performs**, not a
211
+ * write path for any one plugin: compaction is destructive, core is what
212
+ * destroys, and anything that wants the originals — an archive, an audit log,
213
+ * a cold-storage dump — needs to be told at exactly this moment. What a plugin
214
+ * does with them is entirely its own business; core neither knows nor cares.
215
+ *
216
+ * Best-effort in both directions. A throw here never aborts compaction —
217
+ * history must still shorten when a side store is briefly unavailable — and
218
+ * the runtime fans out with `Promise.allSettled`, so one plugin's failure can
219
+ * neither abort another plugin's write nor leave it unawaited. Failing
220
+ * *synchronously* is as safe as rejecting: this need not be an `async`
221
+ * function, and the runtime handles either.
222
+ */
223
+ onMessagesDisplaced?: (messages: SessionMessage[]) => Promise<void>;
224
+ /** Tables this plugin owns, outside core's migration journal. See {@link PluginStore}. */
225
+ store?: PluginStore;
226
+ /**
227
+ * The durable file store a subagent execution's workspace is built over.
228
+ *
229
+ * Core declares the {@link WorkspaceBacking} shape and enforces the caps, but
230
+ * ships no backend: the predecessor's was `@cloudflare/shell`, which is
231
+ * experimental ("expect breaking changes"), and an agent that never delegates
232
+ * file work should not carry it. So the backend arrives here, from a plugin,
233
+ * and an agent that installs none falls back to an in-memory one.
234
+ *
235
+ * At most one installed plugin may declare this — two backends would mean two
236
+ * answers to "where did that file go", and the file would be in whichever the
237
+ * runtime happened to pick.
238
+ *
239
+ * `sql` is the executing facet's own SQLite, so isolation per execution is
240
+ * free and deleting the child wipes the workspace with it. `name` is lazy
241
+ * because a facet's name is set after construction.
242
+ */
243
+ workspaceBacking?: (sql: SqlStorage, name: () => string | undefined) => WorkspaceBacking;
244
+ /**
245
+ * Bindings and secrets the host must declare in `wrangler.jsonc`. A plugin
246
+ * cannot add its own binding, so declaring them lets startup fail with a
247
+ * readable message instead of at the first tool call, in a request a user is
248
+ * waiting on.
249
+ */
250
+ requires?: PluginRequirements;
251
+ }
252
+ /**
253
+ * Identity helper that pins {@link AgentPlugin.contractVersion} for you and gives
254
+ * a plugin author inference on `TRuntime`.
255
+ *
256
+ * ```ts
257
+ * export function arcAgi(config: { apiKey: string }) {
258
+ * return definePlugin<ArcRuntime>({ key: "arc-agi", … });
259
+ * }
260
+ * ```
261
+ */
262
+ export declare function definePlugin<TRuntime = SubtaskRuntime>(plugin: Omit<AgentPlugin<TRuntime>, "contractVersion"> & {
263
+ contractVersion?: number;
264
+ }): AgentPlugin<TRuntime>;
265
+ /** See {@link restrictMainAgentTools}. */
266
+ export interface RestrictMainAgentToolsOptions {
267
+ /**
268
+ * The tool names the main agent keeps. `[]` removes the hook entirely, which
269
+ * is the "orchestrator with no hands" case.
270
+ *
271
+ * Names rather than a predicate, because the point is to state the surface
272
+ * explicitly at the install site: a reader of `plugins.ts` should be able to
273
+ * see what the main agent can do without opening the plugin.
274
+ */
275
+ allow: readonly string[];
276
+ /**
277
+ * What the main agent is told instead of the plugin's own
278
+ * {@link AgentPlugin.capability}.
279
+ *
280
+ * Required in spirit whenever `allow` is non-empty: the plugin's block
281
+ * describes its *whole* surface, so leaving it in place tells the model about
282
+ * tools it no longer has — which it then tries to call. Omit it to say
283
+ * nothing, which is right for `allow: []`.
284
+ */
285
+ capability?: string;
286
+ }
287
+ /**
288
+ * A plugin whose tool families stay registered but whose main-agent surface is
289
+ * narrowed, or removed.
290
+ *
291
+ * ## Why this has to exist
292
+ *
293
+ * `validateRecipe` runs on the **parent** and drops any tool family no installed
294
+ * plugin registered. So an agent that wants "my subagents can run a shell, I
295
+ * cannot" has a problem: uninstalling the sandbox plugin from the parent also
296
+ * deletes `sandbox` from every recipe the parent validates, and the subagents
297
+ * silently lose it. The parent has to install the plugin and decline its tools,
298
+ * which is exactly what this expresses.
299
+ *
300
+ * ```ts
301
+ * // The parent registers the family, and gets three read-only tools from it.
302
+ * restrictMainAgentTools(sandbox(cfg), {
303
+ * allow: ["sb_read", "sb_ls", "sb_exists"],
304
+ * capability: "You can read the checkout, not change it."
305
+ * })
306
+ * ```
307
+ *
308
+ * ## What passes through untouched
309
+ *
310
+ * Everything else: `toolFamilies`, `subtaskType`, `resolveRuntime`, `store`,
311
+ * `requires`, and the lifecycle hooks. In particular `requires` must survive, or
312
+ * a parent that stops offering `sb_exec` also stops asserting that the `Sandbox`
313
+ * binding exists — and the failure moves from DO start to a subagent's first
314
+ * tool call.
315
+ *
316
+ * ## Why a missing name logs instead of throwing
317
+ *
318
+ * `mainAgentTools` is async and called per turn, so a throw here lands inside a
319
+ * request someone is waiting on — a bad trade for what is always a typo. The
320
+ * error names the plugin and the tool so it is greppable, and the honest place
321
+ * to catch it is a test over the assembled surface, which is cheap to write and
322
+ * fails the build instead.
323
+ */
324
+ export declare function restrictMainAgentTools<TRuntime = SubtaskRuntime>(plugin: AgentPlugin<TRuntime>, options: RestrictMainAgentToolsOptions): AgentPlugin<TRuntime>;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The plugin contract — everything an independently-packaged capability may
3
+ * contribute to an agent, and the only thing core knows about one.
4
+ *
5
+ * Nothing in core imports a plugin. A plugin imports core (type-only wherever it
6
+ * can) and is registered by the *host* at DO start. That direction is what makes
7
+ * bundle growth proportional to what an agent actually installs, and it is
8
+ * structural rather than a tree-shaker's opinion.
9
+ */
10
+ /**
11
+ * The contract version a plugin was built against.
12
+ *
13
+ * Three separate repos means a contract change is a three-repo publish train, and
14
+ * the failure mode of a skew is a structural-type mismatch several frames from
15
+ * its cause. `createAgentRuntime` asserts this instead, so a mismatched plugin
16
+ * fails at DO start with a sentence naming the plugin and both versions.
17
+ *
18
+ * The contract is **additive-only** within a major: new capabilities arrive as
19
+ * optional fields on {@link AgentPlugin}. Removing or re-typing an existing field
20
+ * requires a major and a bump here.
21
+ */
22
+ export const PLUGIN_CONTRACT_VERSION = 1;
23
+ /**
24
+ * Identity helper that pins {@link AgentPlugin.contractVersion} for you and gives
25
+ * a plugin author inference on `TRuntime`.
26
+ *
27
+ * ```ts
28
+ * export function arcAgi(config: { apiKey: string }) {
29
+ * return definePlugin<ArcRuntime>({ key: "arc-agi", … });
30
+ * }
31
+ * ```
32
+ */
33
+ export function definePlugin(plugin) {
34
+ return {
35
+ ...plugin,
36
+ contractVersion: plugin.contractVersion ?? PLUGIN_CONTRACT_VERSION
37
+ };
38
+ }
39
+ /**
40
+ * A plugin whose tool families stay registered but whose main-agent surface is
41
+ * narrowed, or removed.
42
+ *
43
+ * ## Why this has to exist
44
+ *
45
+ * `validateRecipe` runs on the **parent** and drops any tool family no installed
46
+ * plugin registered. So an agent that wants "my subagents can run a shell, I
47
+ * cannot" has a problem: uninstalling the sandbox plugin from the parent also
48
+ * deletes `sandbox` from every recipe the parent validates, and the subagents
49
+ * silently lose it. The parent has to install the plugin and decline its tools,
50
+ * which is exactly what this expresses.
51
+ *
52
+ * ```ts
53
+ * // The parent registers the family, and gets three read-only tools from it.
54
+ * restrictMainAgentTools(sandbox(cfg), {
55
+ * allow: ["sb_read", "sb_ls", "sb_exists"],
56
+ * capability: "You can read the checkout, not change it."
57
+ * })
58
+ * ```
59
+ *
60
+ * ## What passes through untouched
61
+ *
62
+ * Everything else: `toolFamilies`, `subtaskType`, `resolveRuntime`, `store`,
63
+ * `requires`, and the lifecycle hooks. In particular `requires` must survive, or
64
+ * a parent that stops offering `sb_exec` also stops asserting that the `Sandbox`
65
+ * binding exists — and the failure moves from DO start to a subagent's first
66
+ * tool call.
67
+ *
68
+ * ## Why a missing name logs instead of throwing
69
+ *
70
+ * `mainAgentTools` is async and called per turn, so a throw here lands inside a
71
+ * request someone is waiting on — a bad trade for what is always a typo. The
72
+ * error names the plugin and the tool so it is greppable, and the honest place
73
+ * to catch it is a test over the assembled surface, which is cheap to write and
74
+ * fails the build instead.
75
+ */
76
+ export function restrictMainAgentTools(plugin, options) {
77
+ const allow = new Set(options.allow);
78
+ // Spread first so anything added to `AgentPlugin` later keeps flowing through
79
+ // without an edit here — the failure mode of an explicit field list is a
80
+ // capability that silently stops reaching the parent.
81
+ const restricted = { ...plugin };
82
+ if (allow.size === 0) {
83
+ delete restricted.mainAgentTools;
84
+ }
85
+ else {
86
+ // Installed even when the plugin offers no `mainAgentTools` at all, rather
87
+ // than left `undefined`. A plugin with no main-agent surface is not the
88
+ // "nothing to narrow" case it looks like — it is an allowlist naming tools
89
+ // that do not exist, which is precisely the typo this helper promises to
90
+ // report, and the one shape of it that is hardest to spot: a whole surface
91
+ // removed or renamed disappears without a word. The assembled tool set is
92
+ // identical either way, because `runtime.mainAgentTools` merges each
93
+ // plugin's result and an empty one contributes nothing.
94
+ const inner = plugin.mainAgentTools;
95
+ restricted.mainAgentTools = async (ctx) => {
96
+ const all = inner ? await inner(ctx) : {};
97
+ const kept = {};
98
+ for (const name of allow) {
99
+ if (name in all)
100
+ kept[name] = all[name];
101
+ else
102
+ console.error(`[plugin] "${plugin.key}" offers no main-agent tool "${name}" — ` +
103
+ "the allowlist names a tool that does not exist, so the main " +
104
+ "agent is quietly missing it. Check for a rename.");
105
+ }
106
+ return kept;
107
+ };
108
+ }
109
+ if (options.capability === undefined)
110
+ delete restricted.capability;
111
+ else
112
+ restricted.capability = options.capability;
113
+ return restricted;
114
+ }