@economic/agents 2.7.2 → 3.0.0-alpha.32271766282.1.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/README.md +458 -108
- package/dist/index.d.mts +282 -164
- package/dist/index.mjs +1741 -675
- package/dist/providers.d.mts +4 -4
- package/dist/providers.mjs +47 -18
- package/package.json +38 -15
- package/dist/route-agent-request-D2ll--nF.d.mts +0 -21
- package/dist/route-agent-request-DBGSXu2O.mjs +0 -468
- package/dist/v1.d.mts +0 -264
- package/dist/v1.mjs +0 -1060
- package/schema/v1/0001_initial.sql +0 -35
- package/schema/v1/0002_add_rating_comment.sql +0 -1
- package/schema/v1/0003_drop_events.sql +0 -5
package/README.md
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
# @economic/agents
|
|
2
2
|
|
|
3
|
-
Our agents SDK for building AI agents on Cloudflare Workers. Each agent is a Durable Object running an LLM loop — model, system prompt, tools, skills, auth,
|
|
3
|
+
Our agents SDK for building AI agents on Cloudflare Workers. Each agent is a Durable Object running an LLM loop — model, system prompt, tools, skills, auth, analytics, and audit logging — on [`@cloudflare/think`](https://www.npmjs.com/package/@cloudflare/think) and the [`ai`](https://www.npmjs.com/package/ai) SDK.
|
|
4
4
|
|
|
5
5
|
React client: [`@economic/agents-react`](../react/README.md).
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Upgrading from version 2: see the [v3 migration guide](./MIGRATION.md).
|
|
8
8
|
|
|
9
9
|
## The three classes
|
|
10
10
|
|
|
11
|
-
- **`
|
|
12
|
-
- **`
|
|
13
|
-
- **`Assistant`** — per-user shell over `ChatAgent`: create/list/delete chats, titles, summaries, retention.
|
|
11
|
+
- **`ChatAgent`** — persistent conversational agent built on `Think`, with tools, skills, authentication, compaction, and message feedback. Drive it over a WebSocket or programmatically.
|
|
12
|
+
- **`TaskAgent`** — one bounded, at-most-once input-to-output run without chat history.
|
|
13
|
+
- **`Assistant`** — per-user shell over `ChatAgent`: create/list/delete chats, titles, summaries, and retention.
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
ChatAgent ← one persistent conversation
|
|
17
|
+
Assistant → ChatAgent ← one user, many conversations
|
|
18
|
+
TaskAgent ← one bounded run, no conversation
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Install
|
|
@@ -36,9 +36,9 @@ npm install -D wrangler vite @cloudflare/vite-plugin @cloudflare/worker-bundler
|
|
|
36
36
|
Routes Claude models through Cloudflare AI Gateway → Google Vertex AI.
|
|
37
37
|
|
|
38
38
|
```typescript
|
|
39
|
-
import {
|
|
39
|
+
import { createAnthropicProvider } from "@economic/agents/providers";
|
|
40
40
|
|
|
41
|
-
const anthropic =
|
|
41
|
+
const anthropic = createAnthropicProvider({
|
|
42
42
|
cloudflareAccountId: env.CLOUDFLARE_ACCOUNT_ID,
|
|
43
43
|
cloudflareAiGatewayId: env.AI_GATEWAY_ID,
|
|
44
44
|
cloudflareApiToken: env.CLOUDFLARE_API_TOKEN,
|
|
@@ -87,21 +87,22 @@ const { anthropic, gemini } = createAiGatewayVertexProviders({
|
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
When these providers are used inside an SDK agent, the SDK automatically adds
|
|
90
|
-
the
|
|
91
|
-
|
|
92
|
-
be grouped by agent without application configuration or duplicated analytics.
|
|
90
|
+
the agent's explicit `agentName` as `agentId` in `cf-aig-metadata`. This allows
|
|
91
|
+
usage from a shared gateway and shared Worker to be grouped by logical agent.
|
|
93
92
|
|
|
94
|
-
## Quick start:
|
|
93
|
+
## Quick start: a chat agent
|
|
95
94
|
|
|
96
|
-
Subclass `
|
|
95
|
+
Subclass `ChatAgent`, set `agentName` to the stable logical agent name, and implement `getModel` and `getSystemPrompt`. `agentName` identifies the configured agent across audit logs, analytics, observability, and AI Gateway metadata; it is deliberately not inferred from the implementation class or Durable Object name. A config-driven harness can implement it as a getter. Add tools with `getTools`, skills with `getSkills`. Expose a `@callable` method to run a turn — `saveMessages` injects a message, runs the turn, and persists the result:
|
|
97
96
|
|
|
98
97
|
```typescript
|
|
99
98
|
import { openai } from "@ai-sdk/openai";
|
|
100
99
|
import { callable } from "agents";
|
|
101
|
-
import {
|
|
100
|
+
import { ChatAgent, tool, type ToolSet } from "@economic/agents";
|
|
102
101
|
import { z } from "zod";
|
|
103
102
|
|
|
104
|
-
export class SupportAgent extends
|
|
103
|
+
export class SupportAgent extends ChatAgent {
|
|
104
|
+
agentName = "customer-support";
|
|
105
|
+
|
|
105
106
|
getModel() {
|
|
106
107
|
return openai("gpt-4o");
|
|
107
108
|
}
|
|
@@ -170,6 +171,16 @@ export default {
|
|
|
170
171
|
}
|
|
171
172
|
```
|
|
172
173
|
|
|
174
|
+
Analytics is opt-in. To send the SDK's typed product events to that binding:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { createAnalyticsEngineHandler } from "@economic/agents";
|
|
178
|
+
|
|
179
|
+
protected getAnalyticsHandlers() {
|
|
180
|
+
return [createAnalyticsEngineHandler(this.env.AGENTS_ANALYTICS)];
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
173
184
|
Run `wrangler types` for a typed `Env`.
|
|
174
185
|
|
|
175
186
|
### Calling it from the client
|
|
@@ -188,9 +199,9 @@ await agent.call("checkOrder", ["1234"]);
|
|
|
188
199
|
|
|
189
200
|
You can also drive turns server-side — call `saveMessages` from a schedule or alarm.
|
|
190
201
|
|
|
191
|
-
##
|
|
202
|
+
## Using a chat UI
|
|
192
203
|
|
|
193
|
-
|
|
204
|
+
`ChatAgent` connects to a browser with [`useChat`](../react/README.md#usechat) — no `Assistant` required. The DO name is the chat; address one per user, ticket, or whatever fits.
|
|
194
205
|
|
|
195
206
|
```typescript
|
|
196
207
|
import { openai } from "@ai-sdk/openai";
|
|
@@ -198,6 +209,8 @@ import { ChatAgent } from "@economic/agents";
|
|
|
198
209
|
import { weatherSkill } from "./skills/weather";
|
|
199
210
|
|
|
200
211
|
export class MyChatAgent extends ChatAgent {
|
|
212
|
+
agentName = "weather-assistant";
|
|
213
|
+
|
|
201
214
|
getModel() {
|
|
202
215
|
return openai("gpt-4o");
|
|
203
216
|
}
|
|
@@ -220,15 +233,15 @@ const { chat } = useChat({
|
|
|
220
233
|
});
|
|
221
234
|
```
|
|
222
235
|
|
|
223
|
-
|
|
236
|
+
Built-in chat features:
|
|
224
237
|
|
|
225
238
|
- **Compaction** — past 100,000 tokens, older messages are summarised (with `getModel()`) while recent ones are kept verbatim. Storage keeps the full history.
|
|
226
239
|
- **Message feedback** — thumbs up/down with an optional comment, in the chat's SQLite (`assistant_messages_feedback`, created automatically):
|
|
227
240
|
|
|
228
|
-
| Method | Description
|
|
229
|
-
| ---------------------------------------------------- |
|
|
230
|
-
| `submitMessageFeedback(messageId, rating, comment?)` | `
|
|
231
|
-
| `getMessageFeedback()` | All feedback for the chat, keyed by message id.
|
|
241
|
+
| Method | Description |
|
|
242
|
+
| ---------------------------------------------------- | ---------------------------------------------------------- |
|
|
243
|
+
| `submitMessageFeedback(messageId, rating, comment?)` | `1` is up, `-1` is down, and `0` clears existing feedback. |
|
|
244
|
+
| `getMessageFeedback()` | All feedback for the chat, keyed by message id. |
|
|
232
245
|
|
|
233
246
|
Surfaced by the client as `chat.submitMessageFeedback` / `chat.getMessageFeedback`.
|
|
234
247
|
|
|
@@ -242,6 +255,7 @@ import { Assistant } from "@economic/agents";
|
|
|
242
255
|
import { MyChatAgent } from "./chat-agent";
|
|
243
256
|
|
|
244
257
|
export class MyAssistant extends Assistant {
|
|
258
|
+
agentName = "weather-assistant";
|
|
245
259
|
protected agent = MyChatAgent;
|
|
246
260
|
protected fastModel = openai("gpt-4o-mini"); // titles and summaries
|
|
247
261
|
}
|
|
@@ -259,27 +273,117 @@ const { status, chats, assistant, chat } = useAssistant({
|
|
|
259
273
|
|
|
260
274
|
The `Assistant` keeps the chat list in its own SQLite (a `chats` table, created automatically) and exposes four callable methods. A newly created chat facet is added to this table when its first turn starts.
|
|
261
275
|
|
|
262
|
-
| Method
|
|
263
|
-
|
|
|
264
|
-
| `createChat()`
|
|
265
|
-
| `getChats()`
|
|
266
|
-
| `deleteChat(id)`
|
|
267
|
-
| `renameChat(id)` | `void` | Renames a registered chat. |
|
|
276
|
+
| Method | Returns | Description |
|
|
277
|
+
| ----------------------- | -------- | ------------------------------------------- |
|
|
278
|
+
| `createChat()` | `string` | Creates a chat facet and returns its id. |
|
|
279
|
+
| `getChats()` | `Chat[]` | Started chats, most recently updated first. |
|
|
280
|
+
| `deleteChat(id)` | `void` | Deletes a chat facet and its record. |
|
|
281
|
+
| `renameChat(id, title)` | `void` | Renames a registered chat. |
|
|
268
282
|
|
|
269
283
|
- **Titles and summaries** — generated with `fastModel` after each turn (on the first turn, then refreshed as the chat grows).
|
|
270
284
|
- **Empty chat cleanup** — a newly created facet is scheduled for deletion after one day. Its first completed turn replaces that schedule with the normal sliding retention period.
|
|
271
285
|
- **Retention** — inactive chats are deleted after 90 days, via a Durable Object alarm.
|
|
272
286
|
|
|
287
|
+
## TaskAgent
|
|
288
|
+
|
|
289
|
+
Use `TaskAgent` for one input-to-output run without conversation history. Implement the model,
|
|
290
|
+
system prompt, and an HTTP transport that calls `runTask`:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { openai } from "@ai-sdk/openai";
|
|
294
|
+
import { TaskAgent } from "@economic/agents";
|
|
295
|
+
|
|
296
|
+
type SummariseContext = {
|
|
297
|
+
tenantId: string;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
type SummariseRequest = {
|
|
301
|
+
input: string;
|
|
302
|
+
context: SummariseContext;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
export class SummariseTask extends TaskAgent<SummariseContext> {
|
|
306
|
+
agentName = "summarise-task";
|
|
307
|
+
|
|
308
|
+
getModel() {
|
|
309
|
+
return openai("gpt-4o-mini");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
getSystemPrompt() {
|
|
313
|
+
return "Summarise the supplied text concisely.";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
override async onRequest(request: Request): Promise<Response> {
|
|
317
|
+
const authentication = await this.authenticateRequest(request);
|
|
318
|
+
if (authentication instanceof Response) return authentication;
|
|
319
|
+
|
|
320
|
+
const { input, context } = (await request.json()) as SummariseRequest;
|
|
321
|
+
return Response.json(await this.runTask(input, context, authentication));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Export and bind the class as a normal SQLite-backed Cloudflare Durable Object, then route requests
|
|
327
|
+
with `routeAgentRequest` as shown in the [chat-agent quick start](#quick-start-a-chat-agent).
|
|
328
|
+
`TaskAgent` requires the `AGENTS_AUDIT_LOGS` binding and refuses to start without it.
|
|
329
|
+
`TaskAgent.onRequest` returns `501` until the subclass defines its transport.
|
|
330
|
+
`runTask` trusts the authentication value passed by that transport; do not expose
|
|
331
|
+
an RPC endpoint that lets callers construct the `authentication` argument themselves.
|
|
332
|
+
|
|
333
|
+
The Durable Object name is the run id. The first call to `runTask` claims it before inference
|
|
334
|
+
starts, so another call using the same name returns `{ status: "duplicate" }` rather than running
|
|
335
|
+
and billing twice. `retentionDays` controls how long that claim is retained and defaults to one day.
|
|
336
|
+
Use a fresh, stable id for each intended run.
|
|
337
|
+
|
|
338
|
+
`runTask(input, context?, authentication?)` accepts either a string or `UIMessage[]` and returns:
|
|
339
|
+
|
|
340
|
+
| Status | Meaning |
|
|
341
|
+
| ----------- | ----------------------------------------------------------------------- |
|
|
342
|
+
| `completed` | Contains `output` and an `outputType` of `"text"` or `"json"`. |
|
|
343
|
+
| `error` | The run failed and contains a combined error description. |
|
|
344
|
+
| `duplicate` | That Durable Object name has already been claimed within its retention. |
|
|
345
|
+
|
|
346
|
+
The task may use tools and skills through `getTools()` and `getSkills()` just like `ChatAgent`.
|
|
347
|
+
Tool and skill authorization receives the request context plus `_token` and `_userContext` from
|
|
348
|
+
request-scoped authentication. `maxSteps` limits the model/tool loop and defaults to 10.
|
|
349
|
+
|
|
350
|
+
For structured output, return a JSON Schema from `getOutputSchema`. The completed `output` remains
|
|
351
|
+
a JSON string and `outputType` is `"json"`. `validateOutput` can reject values that pass the schema
|
|
352
|
+
but fail application-specific checks:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
protected getOutputSchema() {
|
|
356
|
+
return {
|
|
357
|
+
type: "object",
|
|
358
|
+
properties: {
|
|
359
|
+
summary: { type: "string" },
|
|
360
|
+
confidence: { type: "number", minimum: 0, maximum: 1 },
|
|
361
|
+
},
|
|
362
|
+
required: ["summary", "confidence"],
|
|
363
|
+
additionalProperties: false,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
protected validateOutput(value: unknown) {
|
|
368
|
+
const result = value as { summary?: string };
|
|
369
|
+
return result.summary?.trim() ? null : "Summary must not be empty";
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
When a structured task also has tools or skills, the SDK adds a terminal `submit_result` tool and
|
|
374
|
+
instructs the model to call it with the final value. Without tools, it uses the model's native JSON
|
|
375
|
+
output mode.
|
|
376
|
+
|
|
273
377
|
## Bindings
|
|
274
378
|
|
|
275
|
-
|
|
379
|
+
Validated when the relevant agent starts:
|
|
276
380
|
|
|
277
|
-
| Binding | Type | Required
|
|
278
|
-
| ------------------- | ---------------- |
|
|
279
|
-
| `
|
|
280
|
-
| `
|
|
281
|
-
| `
|
|
282
|
-
| `LOADER` | Worker Loader | No
|
|
381
|
+
| Binding | Type | Required | Notes |
|
|
382
|
+
| ------------------- | ---------------- | --------- | ------------------------------------------------------------------ |
|
|
383
|
+
| `AGENTS_DB` | D1 | Chat | Global registry for top-level agent instances. |
|
|
384
|
+
| `AGENTS_AUDIT_LOGS` | R2 | Chat/task | Startup fails if missing. One [audit log](#audit-logging) per run. |
|
|
385
|
+
| `AGENTS_ANALYTICS` | Analytics Engine | No | Optional destination for [product analytics](#analytics). |
|
|
386
|
+
| `LOADER` | Worker Loader | No | Enables [native skill scripts](#skill-scripts) when bound. |
|
|
283
387
|
|
|
284
388
|
## Tools
|
|
285
389
|
|
|
@@ -301,7 +405,9 @@ getTools(): ToolSet {
|
|
|
301
405
|
}
|
|
302
406
|
```
|
|
303
407
|
|
|
304
|
-
`authorize` returning `false`
|
|
408
|
+
`authorize` returning `false` keeps the tool visible to the model but replaces its execution
|
|
409
|
+
with an explicit permission refusal. This lets the model explain the missing access instead of
|
|
410
|
+
silently trying another tool. Unauthorized skills cannot be activated.
|
|
305
411
|
|
|
306
412
|
### Tool context
|
|
307
413
|
|
|
@@ -325,7 +431,7 @@ const call_api = tool({
|
|
|
325
431
|
});
|
|
326
432
|
```
|
|
327
433
|
|
|
328
|
-
`ToolContext<RequestContext, UserContext>`
|
|
434
|
+
`ToolContext<RequestContext, UserContext>` adds `_token` and `_userContext` to the request context. Request context comes from the client (`toolContext` in the hooks); authenticated values come from the agent's configured `Authenticator`.
|
|
329
435
|
|
|
330
436
|
## Skills
|
|
331
437
|
|
|
@@ -424,7 +530,7 @@ Skill scripts are enabled when a Worker Loader binding named `LOADER` is present
|
|
|
424
530
|
}
|
|
425
531
|
```
|
|
426
532
|
|
|
427
|
-
`
|
|
533
|
+
`ChatAgent` automatically creates a script runner when `env.LOADER` is bound. Without the binding, script execution is unavailable.
|
|
428
534
|
|
|
429
535
|
TypeScript scripts use the function-style contract:
|
|
430
536
|
|
|
@@ -466,7 +572,7 @@ import { skills } from "@cloudflare/think";
|
|
|
466
572
|
|
|
467
573
|
getSkills() {
|
|
468
574
|
return [
|
|
469
|
-
skills.r2(this.env.
|
|
575
|
+
skills.r2(this.env.REMOTE_SKILLS, {
|
|
470
576
|
skills: ["top-customers-by-revenue", "vat-code-review"],
|
|
471
577
|
}),
|
|
472
578
|
];
|
|
@@ -484,82 +590,318 @@ skills/top-customers-by-revenue/
|
|
|
484
590
|
|
|
485
591
|
Source order matters: if two sources define the same skill name, the first source wins.
|
|
486
592
|
|
|
593
|
+
Remote skill storage is owned by the application. `REMOTE_SKILLS` above is an example
|
|
594
|
+
consumer binding, not a binding reserved by this SDK or included in `AgentEnv`.
|
|
595
|
+
|
|
487
596
|
## Authentication
|
|
488
597
|
|
|
489
|
-
|
|
598
|
+
Authentication is generic. An agent can implement `getAuthenticator(env)` to return an `Authenticator`. The SDK does not know what kind of credentials the authenticator accepts. If the method is absent, the agent remains accessible without SDK-level authentication.
|
|
599
|
+
|
|
600
|
+
Each `ChatAgent`, `Assistant`, or `TaskAgent` class owns its authentication hook. An `Assistant` does not inherit authentication from its child `ChatAgent`; configure the two boundaries independently. For example, an assistant can verify access to the chat list while each chat separately verifies and exchanges its token for tool calls.
|
|
601
|
+
|
|
602
|
+
For `ChatAgent` and `Assistant`, an authenticator runs once when a WebSocket connects. `TaskAgent` runs it once per request. In either case it returns a denial or the authenticated values the agent needs:
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
return {
|
|
606
|
+
authenticated: true,
|
|
607
|
+
token,
|
|
608
|
+
userContext: { userId, role },
|
|
609
|
+
};
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
- `token` is an optional downstream credential. When returned, it is available as `ctx._token`.
|
|
613
|
+
- `userContext` becomes `ctx._userContext` in prompts, models, tool authorization, and skill authorization.
|
|
614
|
+
- A denial uses `{ authenticated: false, status: 401 | 403, message }`.
|
|
615
|
+
- Unexpected operational failures should throw. They produce an `"error"` status and close code `1011` rather than being reported as bad user credentials.
|
|
490
616
|
|
|
491
|
-
|
|
617
|
+
### JWT verification and token exchange
|
|
618
|
+
|
|
619
|
+
`createJwtAuthenticator` supplies the normal bearer-JWT implementation. It reads the token from `Authorization: Bearer …` or `Sec-WebSocket-Protocol: bearer, <token>`, verifies it once, and optionally calls the consumer's `getExchangedToken` once.
|
|
492
620
|
|
|
493
621
|
```typescript
|
|
494
|
-
|
|
495
|
-
|
|
622
|
+
import { ChatAgent, createJwtAuthenticator } from "@economic/agents";
|
|
623
|
+
|
|
624
|
+
export class SupportAgent extends ChatAgent {
|
|
625
|
+
agentName = "customer-support";
|
|
626
|
+
|
|
627
|
+
protected getAuthenticator(env: Cloudflare.Env) {
|
|
628
|
+
return createJwtAuthenticator({
|
|
629
|
+
verificationConfig: {
|
|
630
|
+
allowedIssuers: [env.IDENTITY_ENDPOINT],
|
|
631
|
+
audience: "eva",
|
|
632
|
+
requiredScopes: ["eva.read"],
|
|
633
|
+
getClaims: (payload) => ({
|
|
634
|
+
userGuid: payload.user_guid as string,
|
|
635
|
+
agreementNumber: payload.agreement_number as number,
|
|
636
|
+
}),
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
async getExchangedToken({ token, claims }) {
|
|
640
|
+
// The consumer owns the exchange protocol and HTTP request.
|
|
641
|
+
return exchangeForBackendToken({
|
|
642
|
+
token,
|
|
643
|
+
userGuid: claims.userGuid,
|
|
644
|
+
env,
|
|
645
|
+
});
|
|
646
|
+
},
|
|
647
|
+
|
|
648
|
+
async getUserContext({ token, claims }) {
|
|
649
|
+
// `token` is now the value returned by getExchangedToken.
|
|
650
|
+
const profile = await fetchProfile(token);
|
|
651
|
+
return { ...profile, agreementNumber: claims.agreementNumber };
|
|
652
|
+
},
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
The resulting flow is:
|
|
659
|
+
|
|
660
|
+
```text
|
|
661
|
+
bearer token → verify once → getExchangedToken once → authenticated result
|
|
662
|
+
├─ token / ctx._token
|
|
663
|
+
└─ userContext
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
`verificationConfig` is consumed by the SDK's built-in JWT verification. `getExchangedToken` is entirely consumer-defined: it receives `{ token, claims }` and returns the exchanged token string. The SDK does not prescribe an identity provider, HTTP endpoint, grant type, or exchange protocol.
|
|
667
|
+
|
|
668
|
+
The final token is exposed as `ToolContext._token`. Without `getExchangedToken`, it is the verified source JWT. Tools decide how that value maps to an HTTP header or another downstream transport; the authentication API contains no header names.
|
|
669
|
+
|
|
670
|
+
Errors thrown by `getExchangedToken` are operational failures and close the socket with `1011`. A consumer can deliberately reject credentials by throwing an error with `closeCode: 4001` or `4003`.
|
|
671
|
+
|
|
672
|
+
### Custom authentication
|
|
673
|
+
|
|
674
|
+
Any authenticator can validate arbitrary credentials. This example validates an app-secret/agreement-grant pair:
|
|
675
|
+
|
|
676
|
+
```typescript
|
|
677
|
+
export class IntegrationAgent extends ChatAgent {
|
|
678
|
+
agentName = "accounting-integration";
|
|
679
|
+
|
|
680
|
+
protected getAuthenticator(env: Cloudflare.Env) {
|
|
496
681
|
return {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
682
|
+
async authenticate(request: Request) {
|
|
683
|
+
const appSecret = request.headers.get("X-AppSecretToken");
|
|
684
|
+
const agreementGrant = request.headers.get("X-AgreementGrantToken");
|
|
685
|
+
|
|
686
|
+
if (!appSecret || !agreementGrant) {
|
|
687
|
+
return {
|
|
688
|
+
authenticated: false as const,
|
|
689
|
+
status: 401 as const,
|
|
690
|
+
message: "Unauthorized",
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const identity = await validateCredentials({ appSecret, agreementGrant }, env);
|
|
695
|
+
|
|
696
|
+
if (!identity) {
|
|
697
|
+
return {
|
|
698
|
+
authenticated: false as const,
|
|
699
|
+
status: 401 as const,
|
|
700
|
+
message: "Unauthorized",
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
authenticated: true as const,
|
|
706
|
+
userContext: identity,
|
|
707
|
+
};
|
|
708
|
+
},
|
|
504
709
|
};
|
|
505
710
|
}
|
|
711
|
+
}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
The agent does not distinguish this from JWT authentication. Tool credentials supplied in the request context remain a consumer concern and pass through unchanged.
|
|
715
|
+
|
|
716
|
+
A custom authenticator may omit `token`, as the example above does. In that case
|
|
717
|
+
`ctx._token` is `undefined`; tools that require a downstream credential must check
|
|
718
|
+
for it and fail safely. The built-in JWT authenticator always returns the verified
|
|
719
|
+
source JWT, or the exchanged token when `getExchangedToken` is configured.
|
|
720
|
+
|
|
721
|
+
### Tool context and credential lifetime
|
|
506
722
|
|
|
507
|
-
|
|
723
|
+
After authentication succeeds, the result is stored in the server-side state attached to that WebSocket connection. Connection state survives Durable Object hibernation and is discarded when the connection closes. Authentication is not written to Durable Object KV, SQLite, or the agent's persisted `state`.
|
|
724
|
+
|
|
725
|
+
Before every turn, the SDK builds the tool context as follows:
|
|
726
|
+
|
|
727
|
+
```typescript
|
|
728
|
+
toolContext._token = authentication.token; // string | undefined
|
|
729
|
+
toolContext._userContext = authentication.userContext;
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
When present, `_token` is the normalized verified or exchanged token. It is `undefined` when a custom authenticator does not provide a downstream credential. Ordinary request-context fields, including any consumer-defined `tokens`, pass through unchanged. The completed context is used by `getModel`, `getSystemPrompt`, tool `authorize`, skill `authorize`, normal tool execution, and approved-tool continuations through `beforeTurn`, `beforeStep`, and `beforeToolCall`.
|
|
733
|
+
|
|
734
|
+
`beforeTurn` does not authenticate, check expiry, or manage connections. It only reads the authenticated result and copies it into the tool context. JWT expiry is validated by `verifyJwt` when the connection is established. Updating `authToken` in the React hooks replaces the WebSocket, so the new token is verified and exchanged on the new connection.
|
|
735
|
+
|
|
736
|
+
Authentication is scoped to the individual WebSocket connection. Turns use the state belonging to the connection that initiated them, including after the Durable Object hibernates and wakes again. Think's automatic chat recovery runs without a current connection, so the SDK restores authentication from the most recently authenticated live WebSocket for that recovery only. Other connectionless calls, including RPC calls, do not inherit WebSocket authentication and must provide and validate their own credentials.
|
|
737
|
+
|
|
738
|
+
### TaskAgent authentication
|
|
739
|
+
|
|
740
|
+
`TaskAgent` uses the same `getAuthenticator` contract, but authentication is request-scoped rather than connection-scoped. A custom `onRequest` transport calls `authenticateRequest(request)`, rejects a returned `Response`, and passes a successful result to `runTask`:
|
|
741
|
+
|
|
742
|
+
```typescript
|
|
743
|
+
override async onRequest(request: Request) {
|
|
744
|
+
const authentication = await this.authenticateRequest(request);
|
|
745
|
+
if (authentication instanceof Response) return authentication;
|
|
746
|
+
|
|
747
|
+
const { input, context } = await request.json();
|
|
748
|
+
return Response.json(await this.runTask(input, context, authentication));
|
|
508
749
|
}
|
|
509
750
|
```
|
|
510
751
|
|
|
511
|
-
|
|
752
|
+
## Analytics
|
|
512
753
|
|
|
513
|
-
|
|
754
|
+
The SDK emits typed product facts from the lifecycle that produces them:
|
|
514
755
|
|
|
515
|
-
|
|
756
|
+
- `prompt.received`
|
|
757
|
+
- `turn.completed`
|
|
758
|
+
- `llm.completed` (model, finish reason, and token usage from the model response)
|
|
759
|
+
- `tool.completed` (tool, active skill, duration, success, and payload sizes)
|
|
760
|
+
- `feedback.submitted`
|
|
761
|
+
|
|
762
|
+
These events are inputs for analytics questions such as which tools or skills are used most. The SDK does not encode particular reports or business questions. Override `getAnalyticsAttributes()` to attach query dimensions that only the consumer knows, such as a tenant or user category:
|
|
516
763
|
|
|
517
764
|
```typescript
|
|
518
|
-
protected
|
|
519
|
-
|
|
520
|
-
|
|
765
|
+
protected getAnalyticsAttributes() {
|
|
766
|
+
return {
|
|
767
|
+
tenantId: this.userContext?.tenantId ?? "unknown",
|
|
768
|
+
userCategory: this.userContext?.category ?? "unknown",
|
|
769
|
+
};
|
|
521
770
|
}
|
|
522
771
|
```
|
|
523
772
|
|
|
524
|
-
|
|
773
|
+
Nothing is sent anywhere unless the consumer configures a handler. Multiple handlers can receive the same event, and a failing handler cannot fail or delay the agent turn:
|
|
525
774
|
|
|
526
775
|
```typescript
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
776
|
+
import {
|
|
777
|
+
createAnalyticsEngineHandler,
|
|
778
|
+
type AnalyticsHandler,
|
|
779
|
+
} from "@economic/agents";
|
|
780
|
+
|
|
781
|
+
protected getAnalyticsHandlers() {
|
|
782
|
+
const classificationHandler: AnalyticsHandler = {
|
|
783
|
+
name: "conversation classifier",
|
|
784
|
+
handle: (event) => {
|
|
785
|
+
if (event.type !== "prompt.received") return;
|
|
786
|
+
|
|
787
|
+
// Forward to an application-owned Queue or classifier agent. That service
|
|
788
|
+
// owns the semantic buckets and sends its result to Snowplow or Analytics Engine.
|
|
789
|
+
return this.env.CLASSIFICATION_QUEUE.send({
|
|
790
|
+
eventId: event.eventId,
|
|
791
|
+
messages: event.messages,
|
|
792
|
+
attributes: event.attributes,
|
|
793
|
+
});
|
|
794
|
+
},
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
return [
|
|
798
|
+
createAnalyticsEngineHandler(this.env.AGENTS_ANALYTICS),
|
|
799
|
+
classificationHandler,
|
|
800
|
+
];
|
|
801
|
+
}
|
|
530
802
|
```
|
|
531
803
|
|
|
532
|
-
|
|
804
|
+
The built-in Analytics Engine handler stores metrics and dimensions, but never prompt content. A custom handler receives the typed event, including prompt content on `prompt.received`, so sending that content requires an explicit consumer decision. For reliable asynchronous processing, a handler should enqueue or forward the event to a Durable Object; handlers themselves are intentionally non-blocking.
|
|
533
805
|
|
|
534
|
-
|
|
806
|
+
The SDK emits these events from `beforeTurn`, `onStepFinish`, `afterToolCall`, and `onChatResponse`. Handlers run in the background under `keepAliveWhile`, so their work is protected from idle eviction without delaying the turn response. If an agent overrides one of those lifecycle hooks, it must call the corresponding `super` method to retain SDK analytics (the same rule already applies to the SDK's turn setup in `beforeTurn`).
|
|
535
807
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
808
|
+
## Operational observability
|
|
809
|
+
|
|
810
|
+
`ChatAgent` and `TaskAgent` preserve Cloudflare Agents' diagnostics-channel events and mirror failures and degraded conditions into structured Worker logs. Successful internal lifecycle events are not logged. The exception is a completed chat turn, whose duration and status are needed for latency distributions and error-rate denominators.
|
|
811
|
+
|
|
812
|
+
The existing AI SDK telemetry stream also produces one operational record for each model call and executed tool:
|
|
813
|
+
|
|
814
|
+
- `agent.llm.completed` / `agent.llm.failed`: provider, model, finish reason, duration, time to first chunk, and token usage.
|
|
815
|
+
- `agent.tool.completed` / `agent.tool.failed`: tool name, duration, and error details when applicable.
|
|
816
|
+
|
|
817
|
+
Prompts, responses, tool arguments, and tool results are deliberately excluded. The same underlying model and tool facts may also be sent through product-analytics handlers, but observability records contain no analytics attributes or prompt content and are intended for service-level metrics and alerts.
|
|
818
|
+
|
|
819
|
+
Each record is a single JSON log with a stable source, event name, severity, agent identity, and the event-specific attributes. This lets a Workers Trace Events Logpush job create Datadog log-based counts, error rates, latency distributions, and alerts without a Datadog client in the agent:
|
|
820
|
+
|
|
821
|
+
```json
|
|
822
|
+
{
|
|
823
|
+
"source": "@economic/agents",
|
|
824
|
+
"event": "chat:turn:finish",
|
|
825
|
+
"level": "info",
|
|
826
|
+
"timestamp": 1787086800000,
|
|
827
|
+
"agentName": "customer-support",
|
|
828
|
+
"agentClass": "ChatHarness",
|
|
829
|
+
"durableObjectName": "user-123:support",
|
|
830
|
+
"attributes": {
|
|
831
|
+
"status": "completed",
|
|
832
|
+
"durationMs": 825
|
|
546
833
|
}
|
|
547
834
|
}
|
|
548
835
|
```
|
|
549
836
|
|
|
550
|
-
|
|
837
|
+
The account-level Logpush destination is configured outside the SDK. Opt the deployed Worker into an existing job in `wrangler.jsonc`:
|
|
838
|
+
|
|
839
|
+
```jsonc
|
|
840
|
+
{
|
|
841
|
+
"logpush": true,
|
|
842
|
+
}
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
Cloudflare runtime signals such as invocation outcome, CPU time, wall time, uncaught exceptions, and resource-limit failures are added by the platform rather than the SDK.
|
|
846
|
+
|
|
847
|
+
## Audit logging
|
|
551
848
|
|
|
552
|
-
-
|
|
553
|
-
- Renewal is connection-based. The client token rides in the WebSocket protocols, so presenting a renewed token always means a reconnect, which re-verifies and re-exchanges. Keep the `authToken` passed to the hooks current (refresh it before expiry) and the socket rotates silently — the `unauthorized` close (`4001`) then only happens when the session is genuinely dead (logout, revocation).
|
|
554
|
-
- The exchanged token expires on the identity server's clock (with a 30s margin). A turn arriving after expiry closes the connection `4001` to force a reconnect; a client that refreshes its token on time never hits this.
|
|
555
|
-
- Exchange failures at connect: `invalid_grant` (dead session token) → status `"unauthorized"` + close `4001`; anything else (identity server down, misconfigured client) → status `"error"` + close `1011`. Partial configuration is the app's concern — throw from `getTokenExchangeConfig` if you consider it a deployment fault, or return `undefined` to disable the exchange.
|
|
849
|
+
The SDK writes one versioned audit-log object per chat turn to `AGENTS_AUDIT_LOGS`. Each object contains the logical agent and harness identity, model, actor and IP, the rendered system prompt, the user message that initiated that turn, the final response, and the tool calls made during that turn. Turn content and tool activity carry their actual span timestamps.
|
|
556
850
|
|
|
557
|
-
|
|
851
|
+
Tool inputs are retained so the attempted action can be established. Tool outputs are deliberately excluded because they may contain credentials, large API responses, or unrelated sensitive data. A tool failure retains its timestamp, error type, and message.
|
|
558
852
|
|
|
559
|
-
|
|
853
|
+
Completed records are written directly to R2 inside the Agent SDK's `keepAliveWhile`, which prevents the Durable Object from being evicted while the write is pending. Tool-span collection is scoped to the individual turn tracer, so simultaneous agents cannot share pending tool calls.
|
|
854
|
+
|
|
855
|
+
```json
|
|
856
|
+
{
|
|
857
|
+
"schemaVersion": 2,
|
|
858
|
+
"id": "accounts-payable/user-123_chat-456/2026-08-18T20-14-30.000Z-ab12cd34.json",
|
|
859
|
+
"startedAt": "2026-08-18T20:14:25.000Z",
|
|
860
|
+
"completedAt": "2026-08-18T20:14:30.000Z",
|
|
861
|
+
"durableObjectName": "user-123:chat-456",
|
|
862
|
+
"agent": {
|
|
863
|
+
"name": "accounts-payable",
|
|
864
|
+
"class": "ChatHarness",
|
|
865
|
+
"llm": {
|
|
866
|
+
"model": "claude-sonnet-4-6",
|
|
867
|
+
"provider": "anthropic"
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
"actor": {
|
|
871
|
+
"id": "user-123",
|
|
872
|
+
"ip": {
|
|
873
|
+
"client": "192.0.2.1",
|
|
874
|
+
"forwardedFor": ["192.0.2.1"]
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
"systemPrompt": {
|
|
878
|
+
"timestamp": "2026-08-18T20:14:25.000Z",
|
|
879
|
+
"content": "You are the accounts payable assistant."
|
|
880
|
+
},
|
|
881
|
+
"prompt": {
|
|
882
|
+
"timestamp": "2026-08-18T20:14:25.000Z",
|
|
883
|
+
"content": "Email the first overdue invoice."
|
|
884
|
+
},
|
|
885
|
+
"response": {
|
|
886
|
+
"timestamp": "2026-08-18T20:14:30.000Z",
|
|
887
|
+
"content": "Email drafted.",
|
|
888
|
+
"finishReason": "stop"
|
|
889
|
+
},
|
|
890
|
+
"status": "success",
|
|
891
|
+
"tools": [
|
|
892
|
+
{
|
|
893
|
+
"id": "call-789",
|
|
894
|
+
"name": "create_email",
|
|
895
|
+
"input": { "invoiceId": "invoice-123" },
|
|
896
|
+
"status": "success",
|
|
897
|
+
"startedAt": "2026-08-18T20:14:27.000Z",
|
|
898
|
+
"completedAt": "2026-08-18T20:14:28.000Z"
|
|
899
|
+
}
|
|
900
|
+
]
|
|
901
|
+
}
|
|
902
|
+
```
|
|
560
903
|
|
|
561
|
-
|
|
562
|
-
- **Analytics** → per-turn and per-tool data points in `AGENTS_ANALYTICS`.
|
|
904
|
+
Operational observability is separate from both audit logging and product analytics. Cloudflare diagnostics, logs, traces, and any future Datadog export are not analytics handlers.
|
|
563
905
|
|
|
564
906
|
## API reference
|
|
565
907
|
|
|
@@ -567,36 +909,44 @@ Imported from `@economic/agents` unless noted.
|
|
|
567
909
|
|
|
568
910
|
### Classes
|
|
569
911
|
|
|
570
|
-
| Export | Description
|
|
571
|
-
| ----------- |
|
|
572
|
-
| `
|
|
573
|
-
| `
|
|
574
|
-
| `Assistant` | Per-user manager of `ChatAgent` chats.
|
|
912
|
+
| Export | Description |
|
|
913
|
+
| ----------- | -------------------------------------------------------------------- |
|
|
914
|
+
| `ChatAgent` | Persistent Think agent with tools, skills, compaction, and feedback. |
|
|
915
|
+
| `TaskAgent` | Bounded, at-most-once task without conversational state. |
|
|
916
|
+
| `Assistant` | Per-user manager of `ChatAgent` chats. |
|
|
575
917
|
|
|
576
918
|
### Helpers and types
|
|
577
919
|
|
|
578
|
-
| Export
|
|
579
|
-
|
|
|
580
|
-
| `tool(def)` / `Tool` / `ToolSet`
|
|
581
|
-
| `skill(def)` / `Skill`
|
|
582
|
-
| `ToolContext`
|
|
583
|
-
| `
|
|
584
|
-
| `
|
|
920
|
+
| Export | Description |
|
|
921
|
+
| ---------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
922
|
+
| `tool(def)` / `Tool` / `ToolSet` | Define a tool with an optional `authorize(ctx)` guard. |
|
|
923
|
+
| `skill(def)` / `Skill` | Define a skill (`name`, `description`, `instructions`, `tools?`, `authorize?`). |
|
|
924
|
+
| `ToolContext` | Request context plus `_token` and `_userContext`. |
|
|
925
|
+
| `TaskRunInput` / `TaskRunResult` / `TaskAuthentication` | Task input, result states, and request-scoped authenticated values. |
|
|
926
|
+
| `createJwtAuthenticator(config)` | JWT verification with an optional consumer-defined token exchange. |
|
|
927
|
+
| `Authenticator` / `AuthenticationResult` / `Authenticated` | Generic authentication contract and normalized results. |
|
|
928
|
+
| `AnalyticsHandler` / `AnalyticsEvent` | Typed, consumer-configured product-analytics pipeline. |
|
|
929
|
+
| `createAnalyticsEngineHandler(dataset)` | Optional Cloudflare Analytics Engine destination; excludes prompt content. |
|
|
930
|
+
| `AgentEnv` | The bindings the runtime expects. |
|
|
931
|
+
| `AgentConnectionStatus` / `AgentConnectionType` | Chat/assistant connection status and type shared with the client. |
|
|
585
932
|
|
|
586
933
|
### Members
|
|
587
934
|
|
|
588
|
-
| Member
|
|
589
|
-
|
|
|
590
|
-
| `
|
|
591
|
-
| `
|
|
592
|
-
| `
|
|
593
|
-
| `
|
|
594
|
-
| `
|
|
595
|
-
| `
|
|
596
|
-
| `
|
|
597
|
-
| `
|
|
598
|
-
| `
|
|
599
|
-
| `
|
|
935
|
+
| Member | On | Description |
|
|
936
|
+
| ------------------------------------------------------- | ---------------- | ------------------------------------------------ |
|
|
937
|
+
| `agentName` | All agents | Required stable identity for shared deployments. |
|
|
938
|
+
| `getModel(ctx?)` | Chat/task agents | Required. Model for inference. |
|
|
939
|
+
| `getSystemPrompt(ctx?)` | Chat/task agents | Required. System prompt. |
|
|
940
|
+
| `getTools()` | Chat/task agents | Always-on tools (default `{}`). |
|
|
941
|
+
| `getSkills()` | Chat/task agents | Skills (default `[]`). |
|
|
942
|
+
| `getAuthenticator(env)` | All agents | Optional generic authenticator. |
|
|
943
|
+
| `runTask(input, context?, authentication?)` | `TaskAgent` | Executes the instance's at-most-once task. |
|
|
944
|
+
| `getOutputSchema()` / `validateOutput(value)` | `TaskAgent` | Optional structured-output contract. |
|
|
945
|
+
| `maxSteps` / `retentionDays` | `TaskAgent` | Task loop and run-id retention limits. |
|
|
946
|
+
| `submitMessageFeedback` / `getMessageFeedback` | `ChatAgent` | Callable message feedback. |
|
|
947
|
+
| `agent` | `Assistant` | Required. Your `ChatAgent` subclass. |
|
|
948
|
+
| `fastModel` | `Assistant` | Required. Cheap model for titles/summaries. |
|
|
949
|
+
| `createChat` / `getChats` / `deleteChat` / `renameChat` | `Assistant` | Callable chat management. |
|
|
600
950
|
|
|
601
951
|
### Package root (`@economic/agents`)
|
|
602
952
|
|