@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/README.md ADDED
@@ -0,0 +1,522 @@
1
+ # @dynamicagents/core
2
+
3
+ **The mandatory foundation for a Dynamic Agents agent on Cloudflare Workers.**
4
+
5
+ Zero-trust A2A (signed AgentCard, gatekeeper-JWT verification, no shared secrets), the
6
+ durable task lifecycle, the delegation and subagent runtime, and the test harness.
7
+
8
+ You bring the loop and the prompts. Core brings everything you cannot choose not to have.
9
+
10
+ ```bash
11
+ npm install @dynamicagents/core
12
+ ```
13
+
14
+ > Part of a three-package split:
15
+ > **`@dynamicagents/core`** (this) ·
16
+ > [`plugins`](https://github.com/dynamicagents/plugins) (optional, composable capabilities) ·
17
+ > [`starter`](https://github.com/dynamicagents/starter) (a working agent that composes them).
18
+
19
+ ---
20
+
21
+ ## Why this exists
22
+
23
+ An agent that talks to other agents has to answer one question before anything else:
24
+ _is the caller who they claim to be, and can they prove it without a shared secret?_
25
+ That answer — and the durable machinery for accepting a turn, decomposing it, and
26
+ delivering a result out of band — is identical for every agent. It is also the part
27
+ that is easy to get subtly and silently wrong.
28
+
29
+ So it ships once, here, with the security-critical paths pinned by tests. Anything
30
+ optional is a plugin. Anything opinionated belongs to your app.
31
+
32
+ ---
33
+
34
+ ## Quick start
35
+
36
+ ### 1. Generate a signing key
37
+
38
+ ```bash
39
+ npx da-keys
40
+ ```
41
+
42
+ Set the private JWK as `A2A_SIGNING_KEY` (`.env` locally; `wrangler deploy
43
+ --secrets-file .env` or `wrangler secret put` when deployed) and the origins you accept
44
+ calls from as `GATEKEEPER_ORIGINS`:
45
+
46
+ ```ini
47
+ # .env
48
+ A2A_SIGNING_KEY={"crv":"Ed25519","d":"…","x":"…","kty":"OKP","kid":"a2a-2026-08-01"}
49
+ GATEKEEPER_ORIGINS=["https://gatekeeper.example.com"]
50
+ ```
51
+
52
+ The public half is never configured anywhere — the Worker derives it from the private
53
+ key and serves it at the card's `jku`.
54
+
55
+ ### 2. Put the A2A edge in front of your Durable Object
56
+
57
+ ```ts
58
+ import { createA2AWorker } from "@dynamicagents/core/worker";
59
+
60
+ const manifest = {
61
+ name: "my-agent",
62
+ description: "Does a useful thing.",
63
+ version: "1.0.0",
64
+ capabilities: { streaming: false, pushNotifications: true, extensions: [] },
65
+ defaultInputModes: ["text/plain"],
66
+ defaultOutputModes: ["text/plain"],
67
+ skills: []
68
+ };
69
+
70
+ export default {
71
+ fetch: createA2AWorker({
72
+ // The stub card at /.well-known/agent-card.json. It describes the origin,
73
+ // not an agent — see below.
74
+ manifest: hostManifest,
75
+ tenants: {
76
+ "my-agent": {
77
+ manifest,
78
+ // One DO instance per verified caller — this is what makes a task
79
+ // unreachable from any other caller by construction.
80
+ resolveAgent: (identity) =>
81
+ env.MY_AGENT.get(env.MY_AGENT.idFromName(identity.key!)),
82
+ // Must be idempotent: the gatekeeper retries dispatch.
83
+ startTurn: async (turn) => {
84
+ await env.TURN_WORKFLOW.create({ id: turn.messageId, params: turn });
85
+ }
86
+ }
87
+ }
88
+ })
89
+ } satisfies ExportedHandler<Env>;
90
+ ```
91
+
92
+ That handler serves three routes: the public JWKS, a **signed** stub AgentCard at
93
+ `/.well-known/agent-card.json`, and gatekeeper-authenticated JSON-RPC. Every POST is verified
94
+ before a Durable Object is ever addressed.
95
+
96
+ #### Agents are tenants
97
+
98
+ Agents are keyed by **tenant id**, and one is required on every request — there is no
99
+ default agent and no implicit routing. This is the A2A mechanism for exactly this case:
100
+ `AgentInterface.tenant` is _"an opaque string used for routing requests to a specific agent
101
+ or tenant when multiple agents are served behind a single A2A endpoint"_, and §8.3.2
102
+ requires a client to send the value the interface it selected declared.
103
+
104
+ So one origin serves any number of agents over **one endpoint, one signing key and one
105
+ card** at the well-known path. It works the same for one agent as for twenty; nothing about
106
+ the shape changes.
107
+
108
+ The card is the reason it has to be this way rather than a path prefix per agent. Its
109
+ location is a **well-known URI**, which RFC 8615 defines per-authority, so exactly one card
110
+ per origin is discoverable at the path A2A registered with IANA. A gatekeeper resolving
111
+ `/.well-known/agent-card.json` against the origin finds that one card whatever prefix an
112
+ agent is mounted behind — and pins its key for all of them.
113
+
114
+ Which is why the card served there is a **stub**: it describes the deployment, advertises
115
+ the endpoint and `extendedAgentCard`, and names no tenant. A tenant's real card — its name,
116
+ skills and signature — comes from `GetExtendedAgentCard`, the spec's own tenant-aware card
117
+ method:
118
+
119
+ ```jsonc
120
+ // POST /a2a
121
+ {
122
+ "jsonrpc": "2.0",
123
+ "id": 1,
124
+ "method": "GetExtendedAgentCard",
125
+ "params": { "tenant": "my-agent" }
126
+ }
127
+ ```
128
+
129
+ A card carries one interface entry and clients take the first, so the stub cannot list its
130
+ siblings — put their names in `description` for a human, and register them out of band.
131
+
132
+ Two independent checks keep one tenant's traffic out of another's:
133
+
134
+ | check | proves |
135
+ | ------------ | -------------------------------------------- |
136
+ | `aud` | the token was minted for **this deployment** |
137
+ | tenant claim | …and for **this agent on it** |
138
+
139
+ The second is load-bearing. Every tenant shares one endpoint and therefore one audience, so
140
+ the audience cannot distinguish them: without the claim, `tenant` would be an
141
+ unauthenticated field in the request body and a token minted for one agent could be replayed
142
+ against any sibling. A token carrying no tenant claim is rejected rather than treated as a
143
+ wildcard.
144
+
145
+ > **Breaking.** Requires a gatekeeper that mints both the endpoint audience and the tenant
146
+ > claim, and registers agents with a tenant id — slack-gatekeeper
147
+ > [#62](https://github.com/dynamicagents/slack-gatekeeper/pull/62). The two sides do not
148
+ > interoperate across this change in either direction, so they deploy together and
149
+ > registered agents are re-registered.
150
+
151
+ ### 3. Write your Durable Object
152
+
153
+ `DynamicAgent` is the DO body every agent has: the runtime and database built once
154
+ per instance, one continuous Session per verified caller, the gatekeeper callback
155
+ channel, and the task lifecycle a Workflow drives. Three seams are yours.
156
+
157
+ ```ts
158
+ import { DynamicAgent, type PluginHost } from "@dynamicagents/core/host";
159
+
160
+ export class MyAgent extends DynamicAgent<Env> {
161
+ protected agentConfig() {
162
+ return {
163
+ model: {
164
+ chatModelId: "@cf/zai-org/glm-5.2",
165
+ fallbackChatModelId: "@cf/meta/llama-4-scout-17b-16e-instruct"
166
+ }
167
+ };
168
+ }
169
+ protected agentPlugins(host: PluginHost<Env>) {
170
+ return [scraper({ apiKey: host.env.SCRAPER_API_KEY })];
171
+ }
172
+ protected agentSoul(capabilities: string) {
173
+ return soulPrompt(capabilities);
174
+ }
175
+ }
176
+ ```
177
+
178
+ Everything that would otherwise be a module-level constant is resolved from those,
179
+ once per instance. Resolving a registry at _import_ time is the one thing this
180
+ package exists to prevent: it freezes the registry before `env` exists (which on
181
+ Workers is always), defeats tree-shaking, and makes runtime plugin selection
182
+ impossible.
183
+
184
+ `createAgentRuntime` and `AgentDB` are still exported and still work on a bare
185
+ `Agent<Env>` — but everything the base class does is lifecycle with an ordering
186
+ that is load-bearing and invisible (migrations awaited before the first RPC, the
187
+ guarded terminal write, the cancellation verdict that must be read and not
188
+ probed for), and hand-rolling it is how two agents in one repo drift apart.
189
+
190
+ ### 4. Delegate, if your agent delegates
191
+
192
+ `@dynamicagents/core/round` adds the other half: durable Subtasks, concurrent
193
+ execution, isolated subagents, and the round loop over them.
194
+
195
+ ```ts
196
+ import { RoundAgentBase, type RoundPolicy } from "@dynamicagents/core/round";
197
+
198
+ export class MyAgent extends RoundAgentBase<Env> {
199
+ // …the three seams above, plus:
200
+ protected roundPolicy(): RoundPolicy {
201
+ return policy;
202
+ }
203
+ protected subagentClass() {
204
+ return MySubagent;
205
+ }
206
+ }
207
+ ```
208
+
209
+ #### The round policy
210
+
211
+ Core ships the machine and none of the words. `RoundPolicy` is every string the
212
+ loop emits — the round contract the model is held to, the note appended when the
213
+ budget is spent, and the three user-facing messages. Nothing has a default: a
214
+ lent-out round contract is exactly the house prompt copy this package refuses to
215
+ have.
216
+
217
+ ---
218
+
219
+ ## Exports
220
+
221
+ No root barrel. Each area is its own subpath, so importing the delegation layer does
222
+ not drag in the A2A adapter, and the test harness cannot reach a production bundle.
223
+
224
+ | Subpath | What's in it |
225
+ | ---------------------------------- | ---------------------------------------------------------------------------- |
226
+ | `@dynamicagents/core` | `createAgentRuntime`, the plugin contract, config shapes, platform facts |
227
+ | `@dynamicagents/core/a2a` | card signing, JWKS, gatekeeper-JWT verify, push notify, task store, executor |
228
+ | `@dynamicagents/core/worker` | `createA2AWorker()` — the whole zero-trust edge |
229
+ | `@dynamicagents/core/agent` | session, history, models + Workers AI, inference, budget, control tools |
230
+ | `@dynamicagents/core/host` | `DynamicAgent` — the Durable Object body — and `PluginHost` |
231
+ | `@dynamicagents/core/round` | the delegating round loop: `RoundAgentBase`, `runHandleTask`, `runTurn` |
232
+ | `@dynamicagents/core/subtasks` | delegation types, decomposition, the `delegate` tool |
233
+ | `@dynamicagents/core/subagent` | `RecipeSubagentBase`, resumable runs, fingerprinting, workspace |
234
+ | `@dynamicagents/core/db` | `AgentDB`, `notify_tasks` + `subtasks` schema, migrations, `PluginStore` |
235
+ | `@dynamicagents/core/testing` | VCR, `FakeSession`, `mockModel`, DO helpers, JWK fixtures — _workerd realm_ |
236
+ | `@dynamicagents/core/testing/node` | the VCR recorder + cassette store — _Node realm, never import from a spec_ |
237
+ | `@dynamicagents/core/eslint` | the `no-deprecated-object-properties` rule |
238
+
239
+ `/testing*` and `/eslint` are structurally incapable of entering a runtime graph, and
240
+ `npm run verify:exports` asserts exactly that before every publish.
241
+
242
+ ---
243
+
244
+ ## The zero-trust model
245
+
246
+ No secret ever crosses the boundary, in either direction.
247
+
248
+ ```
249
+ Gatekeeper ──── EdDSA JWT, jku → its public JWKS ────▶ Agent "the agent knows the gatekeeper"
250
+ Agent ──── signed AgentCard, jku → its JWKS ────▶ Gatekeeper "the gatekeeper knows the agent"
251
+ ```
252
+
253
+ `verifyGatekeeperToken` runs four checks, in this order, on every single call:
254
+
255
+ 1. **`jku` present** in the protected header (RFC 7515 §4.1.2).
256
+ 2. **`jku` origin is allowlisted** — validated _before_ the fetch, so an attacker
257
+ cannot point `jku` at a JWKS they control.
258
+ 3. **`iss` origin equals `jku` origin** — one listed gatekeeper cannot impersonate another.
259
+ 4. **`jwtVerify` pinned to EdDSA.**
260
+
261
+ All four are load-bearing. Do not make any of them optional, and do not add a
262
+ local-development bypass — run a local gatekeeper instead. `verify.spec.ts` asserts each
263
+ one negatively, including that an unlisted `jku` is rejected _before_ any network
264
+ call happens.
265
+
266
+ The agent's card is signed over its **wire (protobuf-JSON) encoding**, which is what
267
+ makes the served document a fixed point under the repeated decoding a verifier
268
+ performs. A gatekeeper pins the card's `kid` + `jku` on first registration
269
+ (Trust-On-First-Use).
270
+
271
+ ### Calling out, and knowing your own origin
272
+
273
+ The same key proves this agent to services that are not the gatekeeper — another agent,
274
+ or any service that verifies against the published JWKS. `signCallerToken` mints the
275
+ short-lived token for that: `iss` is
276
+ this deployment's origin, `jku` is derived from it, and the audience is normalized to a
277
+ bare origin because the far side compares it byte-for-byte.
278
+
279
+ Its `iss` is **not** something to configure. Inside a Durable Object it is:
280
+
281
+ ```ts
282
+ protected override modelRuntime(model: ModelConfig): ModelRuntime {
283
+ return myProvider(this.env, model, () => this.requireSelfOrigin());
284
+ }
285
+ ```
286
+
287
+ `requireSelfOrigin()` (and `selfOrigin()`, which returns `undefined` instead of
288
+ throwing) answer with the origin core already delivers: the executor computes the
289
+ callback `jku` from `new URL(request.url).origin`, and it rides every turn into the DO
290
+ and on into each subagent facet. A `SELF_ORIGIN` secret only restates that, and has to
291
+ be kept byte-identical with the verifier's allowlist by hand in every environment.
292
+
293
+ The first turn an instance serves **pins** it, and nothing is persisted. Pinning is
294
+ what makes it safe to read: turns run concurrently in one Durable Object and a
295
+ credential thunk fires several frames below the turn that set the value, so a mutable
296
+ field could hand one turn another's origin. An agent has one endpoint anyway — the one
297
+ its card advertises and a verifier allowlists — and a fresh isolate on deploy re-learns
298
+ it.
299
+
300
+ It is known **inside a turn or a chunk**: `onStart`, a constructor and a scheduled
301
+ callback all run before any request has said what this deployment is called, and
302
+ `requireSelfOrigin()` throws there saying so.
303
+
304
+ ---
305
+
306
+ ## Plugins
307
+
308
+ A capability is a plugin. Core never imports one — your app registers it, which keeps
309
+ bundle size proportional to what you actually installed.
310
+
311
+ ```ts
312
+ import { definePlugin } from "@dynamicagents/core";
313
+
314
+ export const scraper = (config: { apiKey: string }) =>
315
+ definePlugin({
316
+ key: "scraper",
317
+ subtaskType: {
318
+ key: "scrape",
319
+ description: "fetch a page and summarize it",
320
+ params: z.object({ url: z.string().describe("page to fetch") }),
321
+ recipe
322
+ },
323
+ toolFamilies: { web: (ctx) => ({ tools: { fetchPage: /* … */ } }) },
324
+ capability: "You can scrape a page and summarize it.",
325
+ requires: { secrets: ["SCRAPER_API_KEY"] },
326
+ store: { plugin: "scraper", version: 1, ensureTables: (sql, from) => { /* … */ } }
327
+ });
328
+ ```
329
+
330
+ `createAgentRuntime` fails at DO start — never mid-request — on a duplicate plugin
331
+ key, a duplicate tool family, a missing declared binding, or a
332
+ `PLUGIN_CONTRACT_VERSION` mismatch. Because core, plugins, and starter publish from
333
+ separate repos, one of them is always briefly behind; that version assert turns the
334
+ skew into a readable sentence instead of a structural-type error several frames from
335
+ its cause.
336
+
337
+ The contract is **additive-only within a major**: new capabilities arrive as optional
338
+ fields on `AgentPlugin`.
339
+
340
+ ### Plugin-owned tables
341
+
342
+ A plugin owns its tables outright, through `store: PluginStore` — but it must stay out of
343
+ core's migration journal. `drizzle-orm/durable-sqlite/migrator` keeps one flat integer
344
+ journal and one global `__drizzle_migrations` table, and two independently-versioned
345
+ packages cannot share that index space.
346
+
347
+ That is a prohibition on exactly **one import**, not on drizzle. The query builder holds
348
+ no journal and no connection state, so a plugin declares its tables with `sqliteTable`,
349
+ writes idempotent DDL in `ensureTables`, and queries through its own handle:
350
+
351
+ ```ts
352
+ export const scrapes = sqliteTable("scraper_scrapes", { url: text("url").primaryKey() });
353
+
354
+ store: {
355
+ plugin: "scraper",
356
+ version: 1,
357
+ // Re-run on every hibernation wake-up, so it must be idempotent.
358
+ ensureTables: (sql) => sql.exec(`CREATE TABLE IF NOT EXISTS scraper_scrapes (…)`)
359
+ }
360
+
361
+ // …and anywhere the plugin queries:
362
+ const db = drizzle(storage, { schema: { scrapes } });
363
+ ```
364
+
365
+ Core records each store's version in a `plugin_migrations` row, so `ensureTables` receives
366
+ the version last seen on disk and an upgrade path can branch on it.
367
+
368
+ ### Session hooks
369
+
370
+ `onMessagesDisplaced` hands over the raw messages a compaction is about to fold into a
371
+ summary. Core performs the compaction, so core announces the loss; it neither stores the
372
+ messages nor knows who wants them. An episodic-memory plugin, an audit log, and a
373
+ cold-storage dump all want exactly this callback, and each gets it:
374
+
375
+ ```ts
376
+ // in your DO, wiring the runtime's fan-out into the session
377
+ buildAgentSession(this, model, {
378
+ …,
379
+ onMessagesDisplaced: this.runtime.onMessagesDisplaced
380
+ });
381
+ ```
382
+
383
+ Best-effort in both directions — a listener that throws never aborts compaction (history
384
+ must still shorten when a side store is down), and the fan-out is `Promise.allSettled`, so
385
+ one plugin's outage cannot cost another its notification.
386
+
387
+ `shouldHandleTurn` is the other side of the session: a gate that decides whether a turn
388
+ runs at all, before the loop builds or calls anything. An agent that sees every message in
389
+ its channels is mostly seeing messages that are not for it, and asking a model already
390
+ trying to be helpful to stay quiet degrades _invisibly_ — failing to call a decline-tool
391
+ looks identical to deciding not to. Every declaring plugin is consulted and the answers are
392
+ AND-ed, so any one gate may decline.
393
+
394
+ ```ts
395
+ if (!(await this.runtime.shouldHandleTurn({ history }))) return; // declined
396
+ ```
397
+
398
+ It **fails open**: a gate that throws is counted as `true`. The two mistakes are not
399
+ symmetric — a wrong reply is noise the user can see and ignore, while a wrong silence is
400
+ invisible to the person who needed an answer.
401
+
402
+ ### The workspace backend
403
+
404
+ Core declares the `WorkspaceBacking` shape and enforces the caps, but ships no backend —
405
+ the predecessor's was `@cloudflare/shell`, which is experimental, and an agent that never
406
+ delegates file work should not carry it. A plugin supplies one via `workspaceBacking`; at
407
+ most one may, and an agent that installs none gets `memoryWorkspaceBacking`. So
408
+ `runtime.workspaceBacking` is always defined and your `SubagentRuntime` never needs a null
409
+ check.
410
+
411
+ ---
412
+
413
+ ## Testing
414
+
415
+ The harness both predecessor agents grew, shipped so you don't grow it a third time.
416
+
417
+ ```ts
418
+ import {
419
+ FakeSession,
420
+ mockModel,
421
+ makeGatekeeperToken,
422
+ makeDoHelpers
423
+ } from "@dynamicagents/core/testing";
424
+
425
+ const { withDb } = makeDoHelpers(env.MY_AGENT);
426
+
427
+ await withDb("accepts a turn once", async (db) => {
428
+ await db.ensureReady();
429
+ db.tasks.begin({ messageId: "m1", taskId: "t1", contextId: "c1" });
430
+ });
431
+ ```
432
+
433
+ - **VCR** — record/replay real HTTP against on-disk cassettes, split across the Node
434
+ and workerd realms because specs run in workerd, which has no filesystem. The
435
+ recorder is a Miniflare `outboundService`, so it works on any
436
+ `@cloudflare/vitest-pool-workers` from 0.18 up and needs no `undici`:
437
+
438
+ ```ts
439
+ // vitest.config.ts
440
+ const vcr = createVcr({
441
+ snapshotsDir: path.resolve(import.meta.dirname, "test/snapshots"),
442
+ record: recordFromEnv(), // RECORD=1
443
+ excludeHeaders: ["authorization", "x-api-key"] // never written to a cassette
444
+ });
445
+
446
+ cloudflareTest({ miniflare: { outboundService: vcr.outboundService } });
447
+ ```
448
+
449
+ Then `setupRecording()` at the top of a spec gives every `it` its own cassette,
450
+ auto-named from the file + describe + test names. Cassettes match on method, URL
451
+ and body — never on headers, so a runtime upgrade cannot invalidate them — and a
452
+ request with no active cassette is blocked rather than reaching the network.
453
+ Point vitest's `globalSetup` at `@dynamicagents/core/testing/vcr-global-setup`.
454
+
455
+ - **Fakes** — `FakeSession` (a `SessionLike` reference implementation) and `mockModel`
456
+ (a scripted `LanguageModel`), so a loop can be driven with no model call at all.
457
+ - **Fixtures** — Ed25519 keypairs and a gatekeeper-JWT signer, so the zero-trust path is
458
+ exercisable end to end without a real gatekeeper.
459
+ - **`createAgentHarness`** — the assembly of all of the above: send one A2A turn the
460
+ way a gatekeeper does, and capture what comes back.
461
+
462
+ ```ts
463
+ const harness = createAgentHarness({ worker, env, tenant: "reactive" });
464
+ using _ = harness.interceptGatekeeper();
465
+
466
+ const accepted = await harness.send("what's the weather?");
467
+ expect(accepted.status.state).toBe(TaskState.TASK_STATE_SUBMITTED);
468
+ ```
469
+
470
+ It exists because the pieces above were never the hard part. The audience is the
471
+ **endpoint**, not the origin; the tenant claim has to match the tenant in the
472
+ body; `SendMessage` is refused without a push config; and the gatekeeper's JWKS has
473
+ to be reachable or every spec below it reports a 401 about something else. Four
474
+ facts, wrong the first time in every consumer that wrote this by hand.
475
+
476
+ ---
477
+
478
+ ## What core deliberately does _not_ contain
479
+
480
+ - **Prompt copy of any kind.** Not a soul, not a round contract, not a user-facing
481
+ failure message. `@dynamicagents/core/round` ships the whole delegating loop but takes
482
+ every word it says from a [`RoundPolicy`](#the-round-policy) you write, because a
483
+ run must never execute under an identity nobody chose.
484
+ - **A loop you cannot replace.** `/round` is opt-in and its own subpath. An agent
485
+ whose turn is a single inference extends `DynamicAgent` directly, writes its own
486
+ loop, and carries none of the delegation machinery in its bundle.
487
+ - The main agent's soul. Core ships no prompt copy.
488
+ - Config _values_ — model ids, budgets, limits. Core ships the shapes and safe
489
+ defaults, and `resolveConfig` validates your overrides.
490
+ - Vectorize recall, browser tools, shell. All optional → plugins. Core contains no
491
+ embedding code at all: it ships the `onMessagesDisplaced` hook and nothing about what
492
+ a listener does with the messages — no embedding model, no index, no dimension.
493
+
494
+ ---
495
+
496
+ ## Requirements
497
+
498
+ - **Node** ≥ 24 (for build and test only — the package itself runs on workerd)
499
+ - **Bindings:** `AI`, one Durable Object, one Workflow
500
+ - **Secrets:** `A2A_SIGNING_KEY`, `GATEKEEPER_ORIGINS`
501
+ - **Peers, never bundled:** `agents`, `ai`, `workers-ai-provider`
502
+
503
+ That last point is not stylistic: two copies of `agents` in one Worker breaks the
504
+ `Session` / `SessionMessage` types and every `instanceof`. For local development
505
+ across the three repos use `file:` overrides, or `npm pack` plus a tarball install —
506
+ **not `npm link`**, which duplicates peer dependencies.
507
+
508
+ ---
509
+
510
+ ## Contributing
511
+
512
+ [`AGENTS.md`](./AGENTS.md) documents the constraints this package is guardian of.
513
+
514
+ ```bash
515
+ npm run check # prettier + eslint + tsc (src) + tsc (test) + build
516
+ npm test # vitest, inside real workerd
517
+ npm run verify:exports # the publish gate: subpaths, ESM specifiers, realm isolation
518
+ ```
519
+
520
+ ## License
521
+
522
+ [Apache-2.0](./LICENSE).
@@ -0,0 +1,60 @@
1
+ import type { Task, TaskState } from "@a2a-js/sdk";
2
+ import type { GatekeeperIdentity } from "./verify.js";
3
+ import type { PlainTask } from "./task.js";
4
+ /**
5
+ * The task-lifecycle surface core calls on an agent Durable Object, declared
6
+ * structurally so core never imports a consumer's DO class.
7
+ *
8
+ * A consumer's `DurableObjectStub<MyAgent>` satisfies this by construction:
9
+ * Cloudflare's RPC type mapping wraps each return in `Promise<Serializable<T>>`,
10
+ * and every type below is already `Serializable` — that is precisely why
11
+ * {@link PlainTask} exists (see {@link file://./task.ts}).
12
+ *
13
+ * `listTasks` is optional. `ListTasks` is new in A2A v1.0 and an agent that does
14
+ * not keep a queryable task history simply omits it; {@link DurableTaskStore}
15
+ * then answers the RPC with `UnsupportedOperationError` instead of failing at
16
+ * the binding. Everything else is mandatory — they are the accept-and-notify
17
+ * contract.
18
+ */
19
+ export interface TaskAgent {
20
+ /**
21
+ * Record (or reuse) the `submitted` task for a turn. **Must be idempotent on
22
+ * `messageId`**: the gatekeeper retries dispatch, and the executor relies on this
23
+ * plus a deterministic workflow id to make a turn run exactly once.
24
+ */
25
+ beginTask(input: {
26
+ messageId: string;
27
+ taskId: string;
28
+ contextId: string;
29
+ }): Promise<PlainTask>;
30
+ getTask(taskId: string): Promise<PlainTask | null>;
31
+ /** Returns false when the write was refused (e.g. the task is already terminal). */
32
+ saveTask(task: Task): Promise<boolean>;
33
+ /** Marks the task canceled and returns it, or null when there is no such task. */
34
+ cancelTask(taskId: string): Promise<PlainTask | null>;
35
+ listTasks?(query: TaskListQuery): Promise<TaskListPage>;
36
+ }
37
+ export interface TaskListQuery {
38
+ contextId?: string;
39
+ state?: TaskState;
40
+ /** Epoch ms; matches the `updated_at` column stamped on every status write. */
41
+ updatedAfter?: number;
42
+ includeArtifacts: boolean;
43
+ historyLength?: number;
44
+ limit: number;
45
+ offset: number;
46
+ }
47
+ export interface TaskListPage {
48
+ tasks: Task[];
49
+ /** Total matching rows, ignoring `limit`/`offset` — drives `nextPageToken`. */
50
+ totalSize: number;
51
+ }
52
+ /**
53
+ * Resolve the agent DO stub for a verified caller.
54
+ *
55
+ * Supplied by the consumer because addressing is theirs: core does not know the
56
+ * binding name, and the identity→instance mapping is a product decision. The
57
+ * conventional implementation keys one DO instance per `identity.key`, which is
58
+ * what makes a task unreachable from any other caller by construction.
59
+ */
60
+ export type AgentResolver = (identity: GatekeeperIdentity) => TaskAgent;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ export interface CallerTokenOptions {
2
+ /** This agent's raw private JWK JSON — the same `A2A_SIGNING_KEY` its card is signed with. */
3
+ signingKey: string;
4
+ /**
5
+ * This agent's own origin. It becomes `iss`, and `jku` is derived from it, and
6
+ * the two must agree: a verifier that accepts a `jku` on a different origin
7
+ * than `iss` lets one allowlisted origin impersonate another.
8
+ *
9
+ * **Not something to configure.** Inside a Durable Object it is
10
+ * `requireSelfOrigin()` — see {@link file://./self-origin.ts SelfOrigin},
11
+ * which learns it from the `jku` every turn already carries. A `SELF_ORIGIN`
12
+ * secret restates what the request path knows and has to be kept
13
+ * byte-identical with the verifier's allowlist by hand.
14
+ */
15
+ issuer: string;
16
+ /**
17
+ * Who the token is for. Normalized to a bare origin, because a verifier
18
+ * typically derives what it expects from `new URL(request.url).origin` and
19
+ * `jose` compares `aud` byte-for-byte — a trailing slash or a stray path is a
20
+ * 401 on every request with nothing to catch it. Throws on a value that is not
21
+ * an absolute URL, which is the right moment for that to fail.
22
+ */
23
+ audience: string;
24
+ /** The identity this agent asserts. */
25
+ identity: Record<string, unknown>;
26
+ /** Which tenant of this deployment the token speaks for. */
27
+ tenant: string;
28
+ /** Lifetime in seconds. Defaults to 120. */
29
+ ttlSeconds?: number;
30
+ }
31
+ /**
32
+ * Sign a short-lived token identifying **this agent as a caller** to another
33
+ * service that trusts its card key.
34
+ *
35
+ * The production sibling of `makeGatekeeperToken`, which core previously shipped
36
+ * only from `/testing` — so an agent that had to call out mint-signed had to
37
+ * hand-write this shape, and every deployment that did so wrote its own subtly
38
+ * different version of the `iss`/`jku` agreement above.
39
+ *
40
+ * Distinct from {@link signCallbackJwt}, which carries **no** claims: that one
41
+ * proves "the agent you called is calling you back about this task", where this
42
+ * proves "this is who I am and which tenant I speak for".
43
+ */
44
+ export declare function signCallerToken(options: CallerTokenOptions): Promise<string>;