@economic/agents
v2.3.23
Published
A starter for creating a TypeScript package.
Keywords
Readme
@economic/agents
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, telemetry — on @cloudflare/think and the ai SDK.
React client: @economic/agents-react.
The v1 (
@economic/agents/v1) API (ChatAgentHarness,buildLLMParams) is deprecated, kept only for migration, and will be removed in v3.
The three classes
Agent— the core. Runs the agent loop and keeps message history. Drive it over a WebSocket or programmatically (schedule, alarm, RPC). Most agents stop here.ChatAgent—Agentplus chat features: compaction and message feedback.Assistant— per-user shell overChatAgent: create/list/delete chats, titles, summaries, retention.
Agent ← LLM + tools + skills (most agents stop here)
└─ ChatAgent ← one persistent chat
└─ Assistant ← one user, many chatsInstall
npm install @economic/agents @cloudflare/think agents
npm install -D wrangler vite @cloudflare/vite-plugin @cloudflare/worker-bundler@economic/agents provides the agent runtime. Worker apps install the Cloudflare/Agents host packages they import directly, including the Vite plugin stack used by native skills.
Providers
@economic/agents/providers ships two pre-configured AI SDK providers that route through Cloudflare AI Gateway to Google Vertex AI and Google AI Studio. Both providers require @ai-sdk/anthropic and @ai-sdk/google to be installed.
Anthropic via Vertex AI
Routes Claude models through Cloudflare AI Gateway → Google Vertex AI.
import { createAnthropicVertexProvider } from "@economic/agents/providers";
const anthropic = createAnthropicVertexProvider({
cloudflareAccountId: env.CLOUDFLARE_ACCOUNT_ID,
cloudflareAiGatewayId: env.AI_GATEWAY_ID,
cloudflareApiToken: env.CLOUDFLARE_API_TOKEN,
googleCloudProjectId: env.GOOGLE_CLOUD_PROJECT_ID,
location: "europe-west1", // optional, defaults to "europe-west1"
});
getModel() {
return anthropic("claude-3-7-sonnet-20250219");
}Gemini via Vertex AI
Routes Gemini models through Cloudflare AI Gateway → Google Vertex AI.
import { createGeminiProvider } from "@economic/agents/providers";
const gemini = createGeminiProvider({
cloudflareAccountId: env.CLOUDFLARE_ACCOUNT_ID,
cloudflareAiGatewayId: env.AI_GATEWAY_ID,
cloudflareApiToken: env.CLOUDFLARE_API_TOKEN,
googleCloudProjectId: env.GOOGLE_CLOUD_PROJECT_ID,
location: "europe-west1", // optional, defaults to "europe-west1"
});
getModel() {
return gemini("gemini-2.0-flash");
}Both together
createAiGatewayVertexProviders returns both providers from a single options object:
import { createAiGatewayVertexProviders } from "@economic/agents/providers";
const { anthropic, gemini } = createAiGatewayVertexProviders({
cloudflareAccountId: env.CLOUDFLARE_ACCOUNT_ID,
cloudflareAiGatewayId: env.AI_GATEWAY_ID,
cloudflareApiToken: env.CLOUDFLARE_API_TOKEN,
googleCloudProjectId: env.GOOGLE_CLOUD_PROJECT_ID,
});Quick start: an agent
Subclass Agent and implement getModel and getSystemPrompt. 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:
import { openai } from "@ai-sdk/openai";
import { callable } from "agents";
import { Agent, tool, type ToolSet } from "@economic/agents";
import { z } from "zod";
export class SupportAgent extends Agent {
getModel() {
return openai("gpt-4o");
}
getSystemPrompt() {
return "You help customers with their orders. Be concise.";
}
getTools(): ToolSet {
return {
get_order: tool({
description: "Look up an order by id",
inputSchema: z.object({ orderId: z.string() }),
execute: async ({ orderId }) => fetchOrder(orderId),
}),
};
}
@callable()
async checkOrder(orderId: string) {
await this.saveMessages([
{
id: crypto.randomUUID(),
role: "user",
parts: [{ type: "text", text: `What's the status of order ${orderId}?` }],
},
]);
}
}Route requests and export the class:
import { routeAgentRequest } from "@economic/agents";
export { SupportAgent } from "./agent";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
return (await routeAgentRequest(request, env)) ?? new Response("Not found", { status: 404 });
},
};// wrangler.jsonc — binding name must match the class name
{
"compatibility_date": "2026-04-16",
"compatibility_flags": ["nodejs_compat", "experimental"],
"durable_objects": {
"bindings": [{ "name": "SupportAgent", "class_name": "SupportAgent" }],
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["SupportAgent"] }],
// see "Bindings"
"d1_databases": [
{
"binding": "AGENTS_DB",
"database_name": "agents",
"database_id": "agents",
"migrations_dir": "node_modules/@economic/agents/schema",
},
],
"r2_buckets": [{ "binding": "AGENTS_AUDIT_LOGS", "bucket_name": "agents-audit-logs" }],
"analytics_engine_datasets": [{ "binding": "AGENTS_ANALYTICS", "dataset": "agents-analytics" }],
}Run wrangler types for a typed Env.
Calling it from the client
Connect with useAgent and invoke any @callable method with agent.call:
const agent = useAgent({
host: "localhost:8787",
agentName: "SupportAgent",
name: `${userId}:support`,
});
await agent.call("checkOrder", ["1234"]);You can also drive turns server-side — call saveMessages from a schedule or alarm.
A chat agent
For a chat UI, extend ChatAgent instead of Agent. It adds compaction and message feedback, and connects to a browser with useChat — no Assistant required. The DO name is the chat; address one per user, ticket, or whatever fits.
import { openai } from "@ai-sdk/openai";
import { ChatAgent } from "@economic/agents";
import { weatherSkill } from "./skills/weather";
export class MyChatAgent extends ChatAgent {
getModel() {
return openai("gpt-4o");
}
getSystemPrompt() {
return "You are a helpful assistant.";
}
getSkills() {
return [weatherSkill];
}
}const { chat } = useChat({
host: "localhost:8787",
agentName: "MyChatAgent",
name: `${userId}:weather`,
});What ChatAgent adds over Agent:
Compaction — past 100,000 tokens, older messages are summarised (with
getModel()) while recent ones are kept verbatim. Storage keeps the full history.Message feedback — thumbs up/down with an optional comment, in the chat's SQLite (
assistant_messages_feedback, created automatically):| Method | Description | | ---------------------------------------------------- | ------------------------------------------------------------ | |
submitMessageFeedback(messageId, rating, comment?)|ratingis1(up) or-1(down). Upserts on the message. | |getMessageFeedback()| All feedback for the chat, keyed by message id. |Surfaced by the client as
chat.submitMessageFeedback/chat.getMessageFeedback.
Many chats per user: Assistant
When one user needs a list of chats — a sidebar, titles, summaries — add an Assistant. It owns the chat list and routes to a ChatAgent per chat.
import { openai } from "@ai-sdk/openai";
import { Assistant } from "@economic/agents";
import { MyChatAgent } from "./chat-agent";
export class MyAssistant extends Assistant {
protected agent = MyChatAgent;
protected fastModel = openai("gpt-4o-mini"); // titles and summaries
}Bind both classes (binding name = class name) with a new_sqlite_classes migration each. On the client, use useAssistant — it manages the chat list and connects to the active chat:
const { status, chats, assistant, chat } = useAssistant({
host: "localhost:8787",
agentName: "MyAssistant",
name: userId, // the Assistant is keyed by user
});The Assistant keeps the chat list in its own SQLite (a chats table, created automatically) and exposes three callable methods:
| Method | Returns | Description |
| ---------------- | -------- | ---------------------------------------------- |
| createChat() | string | Creates a chat and returns its id. |
| getChats() | Chat[] | The user's chats, most recently updated first. |
| deleteChat(id) | void | Deletes a chat and its record. |
- Titles and summaries — generated with
fastModelafter each turn (on the first turn, then refreshed as the chat grows). - Retention — inactive chats are deleted after 90 days, via a Durable Object alarm.
Bindings
Checked in Agent.onStart when a connection opens:
| Binding | Type | Required | Notes |
| ------------------- | ---------------- | -------- | ------------------------------------------------------------------------- |
| AGENTS_AUDIT_LOGS | R2 | Yes | Connection rejected if missing. One audit log per turn. |
| AGENTS_ANALYTICS | Analytics Engine | No | Per-turn/per-tool analytics. Warns if missing. |
| SKILLS_BUCKET | R2 | No | Source for remote skills. |
| LOADER | Worker Loader | No | Enables native skill scripts when bound. |
Tools
tool() wraps an ai SDK tool with an optional authorize(ctx). Return always-on tools from getTools():
import { tool, type ToolSet } from "@economic/agents";
import { z } from "zod";
getTools(): ToolSet {
return {
search_web: tool({
description: "Search the web",
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => search(query),
authorize: (ctx) => ctx._userContext?.canSearch !== false,
}),
};
}authorize returning false hides the tool for that request.
Tool context
The tool context is the second argument to execute — experimental_context:
import { tool, type ToolContext } from "@economic/agents";
import { z } from "zod";
interface RequestBody {
tokens: Record<string, string>;
}
const call_api = tool({
description: "Call the API for the user",
inputSchema: z.object({ path: z.string() }),
execute: async ({ path }, { experimental_context }) => {
const ctx = experimental_context as ToolContext<RequestBody>;
return fetchWithAuth(ctx.tokens, path);
},
});ToolContext<RequestContext, UserContext> is RequestContext & { _userContext?: UserContext }. Request context comes from the client (toolContext in the hooks); _userContext comes from getUserContext when using JWT Authentication (via getJwtAuthConfig).
Skills
The SDK supports two skill styles:
- Code-defined skills with the local
skill()helper. - Native Agent Skills loaded from
SKILL.mdfolders viaagents:skillsor remote sources.
Use skills when a capability should be loaded on demand instead of living in the always-on system prompt.
Code-defined skills
A code-defined skill bundles markdown instructions with optional tools. Only description sits in the system prompt; the model loads the instructions and tools on demand.
import { skill, tool } from "@economic/agents";
import { z } from "zod";
export const weatherSkill = skill({
name: "weather",
description: "Look up current weather and forecasts. Use for any weather question.",
instructions: `
# Weather
## When to use
Use this skill whenever the user asks about current conditions or a forecast.
## Workflow
1. Resolve the location to coordinates if needed.
2. Call \`get_forecast\` with the coordinates.
3. Summarise the result; never invent values.
`,
tools: {
get_forecast: tool({
description: "Get the forecast for a set of coordinates",
inputSchema: z.object({ lat: z.number(), lon: z.number() }),
execute: async ({ lat, lon }) => fetchForecast(lat, lon),
}),
},
});Return skills from getSkills(). Add authorize(ctx) to gate a skill (and its tools). skill() throws if description or instructions is missing.
Native Agent Skills
Native Agent Skills are folders containing a SKILL.md file, with optional references and scripts:
src/skills/top-customers-by-revenue/
├── SKILL.md
└── scripts/
└── top-customers.py
src/skills/vat-code-review/
├── SKILL.md
├── references/
│ └── accounting-rules.md
└── scripts/
└── review-invoice-vat.tsLoad bundled skills with the Agents Vite plugin:
import bundledSkills from "agents:skills";
getSkills() {
return [bundledSkills];
}SKILL.md should describe the business workflow, not the implementation mechanism:
---
name: top-customers-by-revenue
description: Find the top customers by revenue from customer and sales data.
---
# Top Customers By Revenue
1. Call `request` with `{ "endpoint": "customers" }`.
2. Call `request` with `{ "endpoint": "sales" }`.
3. Run `scripts/top-customers.py` with the returned customer and sales arrays.
4. Summarize the top customers by revenue.Skill scripts
Skill scripts are enabled when a Worker Loader binding named LOADER is present:
{
"worker_loaders": [{ "binding": "LOADER" }],
}Agent automatically creates a script runner when env.LOADER is bound. Without the binding, script execution is unavailable.
TypeScript scripts use the function-style contract:
import type { SkillRunContext } from "@cloudflare/think";
export default async function run(input: unknown, ctx: SkillRunContext) {
const rules = ctx.files["references/accounting-rules.md"];
return {
ok: true,
rulesLoaded: Boolean(rules),
};
}Python scripts use the path-based Dynamic Workers contract:
import json
from pathlib import Path
input_data = json.loads(Path("/input.json").read_text())
print(json.dumps({
"ok": True,
"result": input_data,
}))Use tools for I/O and scripts for deterministic processing. For example, a skill can call a shared request tool to fetch customers and sales, then pass the returned arrays into a Python script that ranks customers by revenue.
Remote skills
Store skills in R2 to edit without redeploying and share complex skills across agents. Add an R2 skill source from getSkills():
import { skills } from "@cloudflare/think";
getSkills() {
return [
skills.r2(this.env.SKILLS_BUCKET, {
skills: ["top-customers-by-revenue", "vat-code-review"],
}),
];
}Remote skills use the same folder shape as bundled skills under the configured R2 prefix:
skills/top-customers-by-revenue/
├── SKILL.md
└── scripts/
└── top-customers.pySource order matters: if two sources define the same skill name, the first source wins.
Authentication
JWT
Override the static getJwtAuthConfig(env) to verify a JWT on connect (static so an Assistant can check before routing). Return undefined to skip.
export class SupportAgent extends Agent {
static getJwtAuthConfig(env: Cloudflare.Env) {
return {
allowedIssuers: [env.IDENTITY_ENDPOINT], // strings or RegExp
audience: "my-api",
requiredScopes: ["support.read"],
getClaims: (payload) => ({
userGuid: payload.user_guid as string,
agreementNumber: payload.agreement_number as number,
}),
};
}
// ...
}On failure the socket closes (4001 unauthorized, 4003 forbidden) and status becomes "unauthorized". The token is read from Authorization: Bearer …, then the Sec-WebSocket-Protocol: bearer, <token> header.
User context
Implement getUserContext(jwtToken) to load per-user data after auth. It's exposed as ctx._userContext in getModel, getSystemPrompt, tools, and skill authorize. Runs only when JWT auth is configured.
protected async getUserContext(jwtToken: string) {
const profile = await fetchProfile(jwtToken);
return { role: profile.role, tokens: profile.tokens };
}Observability
Emitted per turn from the ai SDK's OpenTelemetry spans, no setup beyond bindings:
- Audit logs → one JSON object per turn in
AGENTS_AUDIT_LOGS(model, actor, IP, prompt, response, tool calls). - Analytics → per-turn and per-tool data points in
AGENTS_ANALYTICS.
API reference
Imported from @economic/agents unless noted.
Classes
| Export | Description |
| ----------- | -------------------------------------------------------- |
| Agent | Core agent base. Implement getModel/getSystemPrompt. |
| ChatAgent | Agent + compaction and message feedback. |
| Assistant | Per-user manager of ChatAgent chats. |
Helpers and types
| Export | Description |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
| tool(def) / Tool / ToolSet | Define a tool with an optional authorize(ctx) guard. |
| skill(def) / Skill | Define a skill (name, description, instructions, tools?, authorize?). |
| ToolContext | RequestContext & { _userContext?: UserContext }. |
| AgentEnv | The bindings the runtime expects. |
| AgentConnectionState / AgentConnectionStatus / AgentConnectionType | Connection state shared with the client. |
Members
| Member | On | Description |
| ---------------------------------------------- | ----------- | ------------------------------------------- |
| getModel(ctx?) | Agent | Required. Model for inference. |
| getSystemPrompt(ctx?) | Agent | Required. System prompt. |
| getTools() | Agent | Always-on tools (default {}). |
| getSkills() | Agent | Local skills (default []). |
| static getJwtAuthConfig(env) | Agent | Optional JWT verification config. |
| getUserContext(jwtToken) | Agent | Optional per-user context after auth. |
| submitMessageFeedback / getMessageFeedback | ChatAgent | Callable message feedback. |
| agent | Assistant | Required. Your ChatAgent subclass. |
| fastModel | Assistant | Required. Cheap model for titles/summaries. |
| createChat / getChats / deleteChat | Assistant | Callable chat management. |
Package root (@economic/agents)
| Export | Description |
| ------------------- | ------------------------------------------------------------------------ |
| routeAgentRequest | Routes a request to the right Durable Object; echoes the WS subprotocol. |
Development
npm install
npm test
npm run build