@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
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
|
+
}
|