@economic/agents 2.10.6 → 2.11.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 CHANGED
@@ -301,6 +301,17 @@ getTools(): ToolSet {
301
301
 
302
302
  `authorize` returning `false` hides the tool for that request.
303
303
 
304
+ Use `approval` when an ordinary tool needs the AI SDK's basic approval gate. The SDK translates it to the upstream `needsApproval` option; `needsApproval` remains supported for backwards compatibility, but do not supply both.
305
+
306
+ ```typescript
307
+ const export_report = tool({
308
+ description: "Export a report",
309
+ inputSchema: z.object({ reportId: z.string() }),
310
+ approval: true,
311
+ execute: async ({ reportId }) => exportReport(reportId),
312
+ });
313
+ ```
314
+
304
315
  ### Tool context
305
316
 
306
317
  The tool context is the second argument to `execute` — `experimental_context`:
@@ -325,6 +336,72 @@ const call_api = tool({
325
336
 
326
337
  `ToolContext<RequestContext, UserContext>` is `RequestContext & { _userContext?: UserContext }`. Request context comes from the client (`toolContext` in the hooks); `_userContext` comes from [`getUserContext`](#user-context) when using JWT Authentication (via `getJwtAuthConfig`).
327
338
 
339
+ ## Actions
340
+
341
+ Use `action()` instead of `tool()` for operations with side effects. Actions use Cloudflare Think's durable action ledger for idempotent execution, settled-result replay, pending-action handling, recovery, timeouts, cancellation, and structured errors.
342
+
343
+ The SDK enables approval by default. `approval` may be `true`, `false`, or a predicate; `approvalSummary` and `approvalRisk` provide the metadata shown by an approval UI.
344
+
345
+ ```typescript
346
+ import { action, skill, type ToolContext } from "@economic/agents";
347
+ import { z } from "zod";
348
+
349
+ interface RequestContext extends Record<string, unknown> {
350
+ tokens: Record<string, string>;
351
+ }
352
+
353
+ const deleteInput = z.object({ invoiceId: z.string() });
354
+
355
+ const deleteInvoice = action<ToolContext<RequestContext>, typeof deleteInput>({
356
+ description: "Delete a draft invoice",
357
+ inputSchema: deleteInput,
358
+
359
+ // This identifies the business operation across retries. Do not use a
360
+ // timestamp, random value, or request id.
361
+ idempotencyKey: ({ input }) => `invoice:${input.invoiceId}:delete`,
362
+
363
+ approvalSummary: "Delete draft invoice",
364
+ approvalRisk: "high",
365
+
366
+ execute: async (input, ctx) => {
367
+ const response = await fetch(`/api/invoices/${input.invoiceId}`, {
368
+ method: "DELETE",
369
+ headers: {
370
+ Authorization: ctx.experimental_context.tokens.Authorization ?? "",
371
+ // Think uses this key for its ledger. Forward it when the downstream
372
+ // API also supports idempotency.
373
+ "Idempotency-Key": ctx.idempotencyKey,
374
+ },
375
+ signal: ctx.signal,
376
+ });
377
+
378
+ return { deleted: response.ok };
379
+ },
380
+ });
381
+
382
+ export const invoiceSkill = skill({
383
+ name: "invoices",
384
+ description: "View and manage invoices.",
385
+ instructions: "Use delete_draft_invoice only for draft invoices.",
386
+ actions: {
387
+ delete_draft_invoice: deleteInvoice,
388
+ },
389
+ });
390
+ ```
391
+
392
+ An action receives the same tool execution data plus Cloudflare's native action fields:
393
+
394
+ - `experimental_context` — the typed `ToolContext` supplied to ordinary tools.
395
+ - `toolCallId`, `messages`, and `abortSignal` — the ordinary tool execution fields.
396
+ - `idempotencyKey` — the resolved domain key, or the tool-call fallback when none was declared.
397
+ - `requestId`, `signal`, `agent`, `env`, and `attachReply` — native Think action fields.
398
+
399
+ `authorize` and `onUnauthorized` behave exactly as they do on `tool()`. Native action options such as `permissions`, `kind`, `timeoutMs`, `approval`, `approvalSummary`, and `approvalRisk` pass through unchanged.
400
+
401
+ Define an explicit `idempotencyKey` for actions that must deduplicate across recovery retries. Without one, Think falls back to `toolCallId`, which only deduplicates that particular tool call. The action ledger does not make an external API idempotent: forward `ctx.idempotencyKey` to the API, adapting its format if required. If one action performs several writes, derive a stable child key for each write rather than reusing one downstream key.
402
+
403
+ Cloudflare currently marks Think Actions as experimental, so this SDK surface may evolve with the upstream API.
404
+
328
405
  ## Skills
329
406
 
330
407
  The SDK supports two skill styles:
@@ -336,7 +413,7 @@ Use skills when a capability should be loaded on demand instead of living in the
336
413
 
337
414
  ### Code-defined skills
338
415
 
339
- 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.
416
+ A code-defined skill bundles markdown `instructions` with optional tools and actions. Only `description` sits in the system prompt; the model loads the instructions, tools, and actions on demand.
340
417
 
341
418
  ```typescript
342
419
  import { skill, tool } from "@economic/agents";
@@ -583,13 +660,14 @@ Imported from `@economic/agents` unless noted.
583
660
 
584
661
  ### Helpers and types
585
662
 
586
- | Export | Description |
587
- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
588
- | `tool(def)` / `Tool` / `ToolSet` | Define a tool with an optional `authorize(ctx)` guard. |
589
- | `skill(def)` / `Skill` | Define a skill (`name`, `description`, `instructions`, `tools?`, `authorize?`). |
590
- | `ToolContext` | `RequestContext & { _userContext?: UserContext }`. |
591
- | `AgentEnv` | The bindings the runtime expects. |
592
- | `AgentConnectionState` / `AgentConnectionStatus` / `AgentConnectionType` | Connection state shared with the client. |
663
+ | Export | Description |
664
+ | ------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
665
+ | `tool(def)` / `Tool` / `ToolSet` | Define a tool with optional `approval`, `authorize`, and `onUnauthorized`. |
666
+ | `action(def)` / `Action` / `ActionConfig` / `ActionContext` | Define a ledger-backed action with approval and idempotency support. |
667
+ | `skill(def)` / `Skill` | Define a skill with optional `tools`, `actions`, and authorization. |
668
+ | `ToolContext` | `RequestContext & { _userContext?: UserContext }`. |
669
+ | `AgentEnv` | The bindings the runtime expects. |
670
+ | `AgentConnectionState` / `AgentConnectionStatus` / `AgentConnectionType` | Connection state shared with the client. |
593
671
 
594
672
  ### Members
595
673
 
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Agent as Agent$1, AgentOptions, Connection, ConnectionContext, SubAgentClass } from "agents";
2
- import { ChatResponseResult, PrepareStepContext, Session, Think, ToolCallContext, ToolCallDecision, TurnConfig, TurnContext, skills } from "@cloudflare/think";
3
- import { JSONValue, LanguageModel, ModelMessage, Tool as Tool$1, UIMessage } from "ai";
2
+ import { Action as Action$1, ActionConfig as ActionConfig$1, ActionContext as ActionContext$1, ChatResponseResult, PrepareStepContext, Session, Think, ToolCallContext, ToolCallDecision, TurnConfig, TurnContext, skills } from "@cloudflare/think";
3
+ import { FlexibleSchema, InferSchema, JSONValue, LanguageModel, ModelMessage, Tool as Tool$1, ToolExecutionOptions, UIMessage } from "ai";
4
4
  import { Observability } from "agents/observability";
5
5
  import { createCompactFunction } from "agents/experimental/memory/utils";
6
6
  import { SkillSource as SkillSource$1 } from "agents/skills";
@@ -110,10 +110,11 @@ interface UnauthorizedToolPolicy<Context extends Record<string, unknown> = Recor
110
110
  reason: string | ((ctx?: Context) => string);
111
111
  }
112
112
  type Tool<Context extends Record<string, unknown> = Record<string, unknown>, INPUT extends JSONValue | unknown | never = any, OUTPUT extends JSONValue | unknown | never = any> = Tool$1<INPUT, OUTPUT> & {
113
+ /** SDK-consistent alias for the AI SDK's `needsApproval` option. */approval?: Tool$1<INPUT, OUTPUT>["needsApproval"];
113
114
  authorize?: (ctx?: Context) => boolean;
114
115
  onUnauthorized?: UnauthorizedToolPolicy<Context>;
115
116
  };
116
- declare function tool<Context extends Record<string, unknown> = Record<string, unknown>, INPUT extends JSONValue | unknown | never = any, OUTPUT extends JSONValue | unknown | never = any>(tool: Tool<Context, INPUT, OUTPUT>): Tool$1<INPUT, OUTPUT>;
117
+ declare function tool<Context extends Record<string, unknown> = Record<string, unknown>, INPUT extends JSONValue | unknown | never = any, OUTPUT extends JSONValue | unknown | never = any>(definition: Tool<Context, INPUT, OUTPUT>): Tool$1<INPUT, OUTPUT>;
117
118
  //#endregion
118
119
  //#region src/server/agents/Agent.d.ts
119
120
  /** What the model would see on its next step — returned by {@link Agent.inspectTurn}. */
@@ -125,6 +126,14 @@ interface TurnSnapshot {
125
126
  frameworkToolNames: string[];
126
127
  messages: ModelMessage[];
127
128
  }
129
+ type Authentication<UserContext> = {
130
+ success: true;
131
+ userContext?: UserContext;
132
+ } | {
133
+ success: false;
134
+ status: 401 | 403 | 500;
135
+ message: string;
136
+ };
128
137
  declare abstract class Agent<RequestContext extends Record<string, unknown> = Record<string, unknown>, UserContext extends Record<string, unknown> = Record<string, unknown>, State = unknown> extends Think<Cloudflare.Env & AgentEnv, State> {
129
138
  protected agentType: AgentConnectionType;
130
139
  protected clientIp?: string;
@@ -166,6 +175,8 @@ declare abstract class Agent<RequestContext extends Record<string, unknown> = Re
166
175
  }>;
167
176
  beforeToolCall(ctx: ToolCallContext): Promise<ToolCallDecision | void>;
168
177
  getTools(): ToolSet;
178
+ /** Collects the actions declared by this agent's skills for Think to compile. */
179
+ getActions(): Promise<Record<string, Action$1>>;
169
180
  /**
170
181
  * Read-only: what the model would see on its next step for the given history
171
182
  * — the assembled system prompt, the skill-gated toolset, and pruned
@@ -215,8 +226,10 @@ declare abstract class Agent<RequestContext extends Record<string, unknown> = Re
215
226
  */
216
227
  private _withTurnContextFallback;
217
228
  private _getLastActivatedSkillName;
229
+ /** Verifies the request and prepares the same user context for WebSocket and HTTP requests. */
230
+ protected authenticate(request: Request): Promise<Authentication<UserContext>>;
218
231
  /**
219
- * Override to enable JWT authentication on WebSocket connections.
232
+ * Override to enable JWT authentication on WebSocket and HTTP requests.
220
233
  * Return the auth config based on the incoming request, or undefined to skip auth.
221
234
  * Do not add side effects to this method - return a single JwtAuthConfig from it.
222
235
  *
@@ -231,8 +244,8 @@ declare abstract class Agent<RequestContext extends Record<string, unknown> = Re
231
244
  * When configured, the exchanged token is passed to {@link getUserContext}
232
245
  * and injected into the tool context every turn under
233
246
  * `tokens[config.tokenHeader]` — client-sent request-body tokens for that
234
- * key are ignored. Renewal is connection-based: a client presenting a new
235
- * token reconnects, and the connect flow re-verifies and re-exchanges.
247
+ * key are ignored. Each authenticated WebSocket connection or HTTP request
248
+ * re-verifies and re-exchanges the client token.
236
249
  *
237
250
  * Do not add side effects to this method - return a single config from it,
238
251
  * or undefined to disable the exchange.
@@ -323,6 +336,12 @@ declare abstract class ChatAgent<RequestContext extends Record<string, unknown>
323
336
  configureSession(session: Session): Session;
324
337
  beforeTurn(ctx: TurnContext): Promise<void | TurnConfig>;
325
338
  onChatResponse(result: ChatResponseResult): Promise<void>;
339
+ onRequest(request: Request): Promise<Response>;
340
+ /**
341
+ * Runs the turn from the stream's start callback so the response can be
342
+ * returned immediately and each event reaches the client as it is produced.
343
+ */
344
+ private streamMessage;
326
345
  /**
327
346
  * Submit feedback for a message by its id.
328
347
  * @param messageId - The id of the message to give feedback on.
@@ -357,6 +376,8 @@ declare abstract class Assistant<State = unknown> extends Agent$1<Cloudflare.Env
357
376
  protected abstract agent: SubAgentClass<ChatAgent>;
358
377
  onStart(): void;
359
378
  onConnect(connection: Connection, ctx: ConnectionContext): Promise<void>;
379
+ private authenticate;
380
+ onRequest(request: Request): Promise<Response>;
360
381
  createChat(): Promise<string>;
361
382
  deleteChat(id: string): Promise<void>;
362
383
  renameChat(id: string, title: string): Promise<void>;
@@ -388,16 +409,36 @@ declare abstract class Assistant<State = unknown> extends Agent$1<Cloudflare.Env
388
409
  observability?: Observability;
389
410
  }
390
411
  //#endregion
412
+ //#region src/server/util/actions.d.ts
413
+ /** Tool execution options plus the additional context supplied by native Think actions. */
414
+ type ActionContext<Context extends Record<string, unknown> = Record<string, unknown>> = Omit<ToolExecutionOptions, "experimental_context"> & ActionContext$1 & {
415
+ experimental_context: Context; /** The action's resolved domain key, or its tool-call fallback when none was declared. */
416
+ idempotencyKey: string;
417
+ };
418
+ type ActionConfig<Context extends Record<string, unknown>, InputSchema extends FlexibleSchema, Output = unknown> = Omit<ActionConfig$1<InputSchema, Output>, "execute"> & {
419
+ authorize?: (ctx?: Context) => boolean;
420
+ onUnauthorized?: UnauthorizedToolPolicy<Context>;
421
+ execute(input: InferSchema<InputSchema>, ctx: ActionContext<Context>): Promise<Output> | Output;
422
+ };
423
+ type Action<Context extends Record<string, unknown> = Record<string, unknown>, InputSchema extends FlexibleSchema = FlexibleSchema, Output = unknown> = Action$1<InputSchema, Output> & {
424
+ authorize?: (ctx?: Context) => boolean;
425
+ onUnauthorized?: UnauthorizedToolPolicy<Context>;
426
+ };
427
+ /** A native Think action with the SDK's standard authorization policy. */
428
+ declare function action<Context extends Record<string, unknown> = Record<string, unknown>, const InputSchema extends FlexibleSchema = FlexibleSchema, Output = unknown>(definition: ActionConfig<Context, InputSchema, Output>): Action<Context, InputSchema, Output>;
429
+ //#endregion
391
430
  //#region src/server/util/skills.d.ts
392
431
  interface Skill<Context extends Record<string, unknown> = Record<string, unknown>> {
393
432
  name: string;
394
433
  description: string;
395
434
  instructions: string;
396
435
  tools?: Record<string, Tool<Context>>;
436
+ actions?: Record<string, Action<Context, any, any>>;
397
437
  authorize?: (ctx?: Context) => boolean;
398
438
  }
399
439
  interface SkillSource<Context extends Record<string, unknown> = Record<string, unknown>> extends SkillSource$1 {
400
440
  tools?: Record<string, Tool<Context>>;
441
+ actions?: Record<string, Action<Context, any, any>>;
401
442
  authorize?: (ctx?: Context | undefined) => boolean;
402
443
  }
403
444
  declare function skill<Context extends Record<string, unknown> = Record<string, unknown>>(definition: Skill<Context>): SkillSource<Context>;
@@ -408,4 +449,4 @@ declare function routeAgentRequest<Env>(request: Request, env: Env, options?: Ag
408
449
  //#region src/server/index.d.ts
409
450
  declare function getCurrentToolContext(): any;
410
451
  //#endregion
411
- export { Agent, type AgentConnectionStatus, type AgentConnectionType, type AgentEnv, Assistant, ChatAgent, type ExchangedToken, type JwtAuthConfig, type Skill, type SkillSource, type TokenExchangeConfig, type TokenExchangeResult, type Tool, type ToolContext, type ToolSet, type TurnSnapshot, type UnauthorizedToolPolicy, createJwtAuthConfig, exchangeToken, getCurrentToolContext, routeAgentRequest, skill, tool };
452
+ export { type Action, type ActionConfig, type ActionContext, Agent, type AgentConnectionStatus, type AgentConnectionType, type AgentEnv, Assistant, ChatAgent, type ExchangedToken, type JwtAuthConfig, type Skill, type SkillSource, type TokenExchangeConfig, type TokenExchangeResult, type Tool, type ToolContext, type ToolSet, type TurnSnapshot, type UnauthorizedToolPolicy, action, createJwtAuthConfig, exchangeToken, getCurrentToolContext, routeAgentRequest, skill, tool };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Agent as Agent$1, callable, getCurrentAgent, routeAgentRequest as routeAgentRequest$1 } from "agents";
2
- import { Think, skills } from "@cloudflare/think";
2
+ import { Think, action as action$1, skills } from "@cloudflare/think";
3
3
  import { Output, convertToModelMessages, generateText, jsonSchema, pruneMessages, tool as tool$1 } from "ai";
4
4
  import { createRemoteJWKSet, decodeJwt, errors, jwtVerify } from "jose";
5
5
  import { BasicTracerProvider, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
@@ -243,39 +243,6 @@ function withExchangedToken(body, tokenHeader, accessToken) {
243
243
  };
244
244
  }
245
245
  /**
246
- * Connect-time exchange. On failure, signals the client and closes the
247
- * connection — `unauthorized`/4001 when the session token itself was rejected
248
- * (only a re-login fixes that), `error`/1011 for identity-server or
249
- * configuration faults — and returns undefined so the caller abandons the
250
- * connect.
251
- */
252
- async function exchangeConnectToken(subjectToken, config, connection) {
253
- let result;
254
- try {
255
- result = await exchangeToken(subjectToken, config);
256
- } catch (error) {
257
- sendConnectionStatus(connection, "error");
258
- console.error(`[Agent] Token exchange error - ${error}`);
259
- connection.close(1011, "Token exchange failed");
260
- return;
261
- }
262
- if (!result.success) {
263
- console.error(`[Agent] Token exchange failed - ${result.message}`);
264
- if (result.subjectTokenRejected) {
265
- sendConnectionStatus(connection, "unauthorized");
266
- connection.close(4001, "Unauthorized");
267
- } else {
268
- sendConnectionStatus(connection, "error");
269
- connection.close(1011, "Token exchange failed");
270
- }
271
- return;
272
- }
273
- return {
274
- accessToken: result.accessToken,
275
- expiresAt: result.expiresAt
276
- };
277
- }
278
- /**
279
246
  * Turn-time injection: returns a body copy with the held token under
280
247
  * `tokens[config.tokenHeader]`. When the held token is missing or expired the
281
248
  * turn cannot run — only a re-authenticating reconnect delivers a fresh
@@ -294,8 +261,13 @@ function injectExchangedToken(body, config, token, connection) {
294
261
  }
295
262
  //#endregion
296
263
  //#region src/server/util/tools.ts
297
- function tool(tool) {
298
- return tool$1(tool);
264
+ function tool(definition) {
265
+ const { approval, ...tool } = definition;
266
+ if (approval !== void 0 && tool.needsApproval !== void 0) throw new Error("tool() accepts either approval or needsApproval, not both");
267
+ return tool$1({
268
+ ...tool,
269
+ ...approval !== void 0 ? { needsApproval: approval } : {}
270
+ });
299
271
  }
300
272
  /**
301
273
  * Wrap each tool's `execute` so it falls back to `getFallbackContext()` for
@@ -962,61 +934,16 @@ var Agent = class extends Think {
962
934
  sendConnectionStatus(connection, "connecting");
963
935
  this.clientIp = ctx.request.headers.get("CF-Connecting-IP") ?? ctx.request.headers.get("X-Forwarded-For")?.split(",")[0]?.trim();
964
936
  this.forwardedFor = ctx.request.headers.get("X-Forwarded-For") ?? void 0;
965
- const getJwtAuthConfig = this.constructor.getJwtAuthConfig;
966
- if (getJwtAuthConfig) {
967
- const config = getJwtAuthConfig(this.env);
968
- if (config) {
969
- let result;
970
- try {
971
- result = await verifyJwt(ctx.request, config);
972
- } catch (error) {
973
- sendConnectionStatus(connection, "unauthorized");
974
- console.error(`[Agent] JWT verification error - ${error}`);
975
- connection.close(4001, "Unauthorized");
976
- return;
977
- }
978
- if (!result.success) {
979
- sendConnectionStatus(connection, "unauthorized");
980
- console.error(`[Agent] JWT verification error - ${result.message}`);
981
- connection.close(result.status === 401 ? 4001 : 4003, result.message);
982
- return;
983
- }
984
- if (!isJwtActorAuthorized(config, result.claims, {
985
- durableObjectName: this.name,
986
- parentPath: this.parentPath
987
- })) {
988
- sendConnectionStatus(connection, "unauthorized");
989
- console.error("[Agent] Authenticated actor does not own the requested agent", {
990
- authenticatedActorId: config.getActorId(result.claims),
991
- requiredActorId: getTargetActorId({
992
- durableObjectName: this.name,
993
- parentPath: this.parentPath
994
- }),
995
- durableObjectName: this.name
996
- });
997
- connection.close(4003, "Forbidden");
998
- return;
999
- }
1000
- const token = extractTokenFromConnectRequest(ctx.request);
1001
- let effectiveToken = token;
1002
- const getTokenExchangeConfig = this.constructor.getTokenExchangeConfig;
1003
- const exchangeConfig = getTokenExchangeConfig?.(this.env);
1004
- if (token && exchangeConfig) {
1005
- const exchanged = await exchangeConnectToken(token, exchangeConfig, connection);
1006
- if (!exchanged) return;
1007
- await this._storeExchangedToken(exchanged);
1008
- effectiveToken = exchanged.accessToken;
1009
- }
1010
- if (effectiveToken && this.getUserContext) try {
1011
- connection.setState({ userContext: await this.getUserContext(effectiveToken) });
1012
- } catch (error) {
1013
- const closeCode = getConnectCloseCode(error);
1014
- sendConnectionStatus(connection, closeCode === 1011 ? "error" : "unauthorized");
1015
- console.error(`[Agent] getUserContext error - ${error}`);
1016
- connection.close(closeCode, "Could not establish user context");
1017
- return;
1018
- }
1019
- }
937
+ const auth = await this.authenticate(ctx.request);
938
+ if (!auth.success) {
939
+ const closeCode = auth.status === 401 ? 4001 : auth.status === 403 ? 4003 : 1011;
940
+ sendConnectionStatus(connection, closeCode === 1011 ? "error" : "unauthorized");
941
+ connection.close(closeCode, auth.message);
942
+ return;
943
+ }
944
+ if (auth.userContext) {
945
+ this.userContext = auth.userContext;
946
+ connection.setState({ userContext: auth.userContext });
1020
947
  }
1021
948
  sendConnectionStatus(connection, "connected");
1022
949
  }
@@ -1031,7 +958,7 @@ var Agent = class extends Think {
1031
958
  const connectionState = getCurrentAgent().connection?.state;
1032
959
  return {
1033
960
  ...requestContext,
1034
- _userContext: connectionState?.userContext
961
+ _userContext: connectionState?.userContext ?? this.userContext
1035
962
  };
1036
963
  }
1037
964
  /**
@@ -1121,6 +1048,12 @@ var Agent = class extends Think {
1121
1048
  getTools() {
1122
1049
  return {};
1123
1050
  }
1051
+ /** Collects the actions declared by this agent's skills for Think to compile. */
1052
+ async getActions() {
1053
+ const actions = {};
1054
+ for (const skill of await this.getSkills()) Object.assign(actions, skill.actions);
1055
+ return actions;
1056
+ }
1124
1057
  /**
1125
1058
  * Read-only: what the model would see on its next step for the given history
1126
1059
  * — the assembled system prompt, the skill-gated toolset, and pruned
@@ -1151,8 +1084,10 @@ var Agent = class extends Think {
1151
1084
  ...think._skillRegistry?.tools(),
1152
1085
  ...think.mcp?.getAITools?.()
1153
1086
  };
1087
+ const actionTools = await think._compileActionTools?.() ?? {};
1154
1088
  const inputTools = {
1155
1089
  ...this.getTools(),
1090
+ ...actionTools,
1156
1091
  ...frameworkTools
1157
1092
  };
1158
1093
  this._turn.inputTools = inputTools;
@@ -1207,7 +1142,18 @@ var Agent = class extends Think {
1207
1142
  const unavailableSkillIds = /* @__PURE__ */ new Set();
1208
1143
  let hasActionTool = false;
1209
1144
  let hasAuthorizedActionTool = false;
1210
- const configureTool = (name, tool, trackRestriction = true) => {
1145
+ const skillSources = await this.getSkills();
1146
+ const registeredActions = await this.getActions();
1147
+ const actionsByToolName = /* @__PURE__ */ new Map();
1148
+ const skillActionNames = /* @__PURE__ */ new Set();
1149
+ for (const [registrationName, definition] of Object.entries(registeredActions)) actionsByToolName.set(definition.config.name ?? registrationName, definition);
1150
+ for (const skill of skillSources) for (const [registrationName, definition] of Object.entries(skill.actions ?? {})) skillActionNames.add(definition.config.name ?? registrationName);
1151
+ const configureTool = (name, tool, trackRestriction = true, authorization = tool) => {
1152
+ tool = {
1153
+ ...tool,
1154
+ authorize: authorization.authorize,
1155
+ onUnauthorized: authorization.onUnauthorized
1156
+ };
1211
1157
  const authorized = tool.authorize?.(ctx) !== false;
1212
1158
  if (tool.needsApproval) {
1213
1159
  hasActionTool = true;
@@ -1234,11 +1180,18 @@ var Agent = class extends Think {
1234
1180
  tools[name] = tool;
1235
1181
  return true;
1236
1182
  };
1237
- for (const [name, tool] of Object.entries(inputTools)) if (configureTool(name, tool)) activeTools.push(name);
1238
- const skillSources = await this.getSkills();
1183
+ for (const [name, tool] of Object.entries(inputTools)) {
1184
+ if (skillActionNames.has(name)) continue;
1185
+ if (configureTool(name, tool, true, actionsByToolName.get(name))) activeTools.push(name);
1186
+ }
1239
1187
  for (const skill of skillSources) {
1240
1188
  const skillAuthorized = skill.authorize?.(ctx) !== false;
1241
- const skillTools = Object.entries(skill.tools ?? {});
1189
+ const skillTools = [...Object.entries(skill.tools ?? {})];
1190
+ for (const [registrationName, definition] of Object.entries(skill.actions ?? {})) {
1191
+ const name = definition.config.name ?? registrationName;
1192
+ const compiled = inputTools[name];
1193
+ if (compiled) skillTools.push([name, compiled]);
1194
+ }
1242
1195
  let hasVisibleTool = false;
1243
1196
  for (const [name, tool] of skillTools) {
1244
1197
  if (!skillAuthorized) {
@@ -1247,13 +1200,14 @@ var Agent = class extends Think {
1247
1200
  continue;
1248
1201
  }
1249
1202
  const needsApproval = tool.needsApproval;
1250
- const visible = configureTool(name, tool.authorize && needsApproval ? {
1203
+ const preparedTool = tool.authorize && needsApproval ? {
1251
1204
  ...tool,
1252
1205
  needsApproval: async (input, options) => {
1253
1206
  if (tool.authorize?.(ctx) === false) return false;
1254
1207
  return typeof needsApproval === "function" ? needsApproval(input, options) : needsApproval;
1255
1208
  }
1256
- } : tool, false);
1209
+ } : tool;
1210
+ const visible = configureTool(name, preparedTool, false, actionsByToolName.get(name) ?? preparedTool);
1257
1211
  hasVisibleTool ||= visible;
1258
1212
  }
1259
1213
  if (skillTools.length > 0 && !hasVisibleTool) unavailableSkillIds.add(skill.id);
@@ -1261,7 +1215,13 @@ var Agent = class extends Think {
1261
1215
  this._turn.unavailableSkillIds = unavailableSkillIds;
1262
1216
  const activeSkill = skillSources.find((skill) => skill.id === activeSkillName);
1263
1217
  if (activeSkill && activeSkill.authorize?.(ctx) !== false) {
1264
- for (const [name, tool] of Object.entries(activeSkill.tools ?? {})) if (configureTool(name, tool)) activeTools.push(name);
1218
+ const activeSkillTools = [...Object.entries(activeSkill.tools ?? {})];
1219
+ for (const [registrationName, definition] of Object.entries(activeSkill.actions ?? {})) {
1220
+ const name = definition.config.name ?? registrationName;
1221
+ const compiled = inputTools[name];
1222
+ if (compiled) activeSkillTools.push([name, compiled]);
1223
+ }
1224
+ for (const [name, tool] of activeSkillTools) if (configureTool(name, tool, true, actionsByToolName.get(name) ?? tool)) activeTools.push(name);
1265
1225
  }
1266
1226
  this._turn.preparedTools = tools;
1267
1227
  this._turn.unavailableTools = Array.from(unavailableTools, ([name, reason]) => ({
@@ -1324,6 +1284,72 @@ var Agent = class extends Think {
1324
1284
  }
1325
1285
  }
1326
1286
  }
1287
+ /** Verifies the request and prepares the same user context for WebSocket and HTTP requests. */
1288
+ async authenticate(request) {
1289
+ const config = this.constructor.getJwtAuthConfig?.(this.env);
1290
+ if (!config) return { success: true };
1291
+ let result;
1292
+ try {
1293
+ result = await verifyJwt(request, config);
1294
+ } catch (error) {
1295
+ console.error(`[Agent] JWT verification error - ${error}`);
1296
+ return {
1297
+ success: false,
1298
+ status: 401,
1299
+ message: "Unauthorized"
1300
+ };
1301
+ }
1302
+ if (!result.success) return result;
1303
+ if (!isJwtActorAuthorized(config, result.claims, {
1304
+ durableObjectName: this.name,
1305
+ parentPath: this.parentPath
1306
+ })) return {
1307
+ success: false,
1308
+ status: 403,
1309
+ message: "Forbidden"
1310
+ };
1311
+ let token = extractTokenFromConnectRequest(request);
1312
+ const exchangeConfig = this.constructor.getTokenExchangeConfig?.(this.env);
1313
+ if (exchangeConfig) {
1314
+ let exchanged;
1315
+ try {
1316
+ exchanged = await exchangeToken(token, exchangeConfig);
1317
+ } catch (error) {
1318
+ console.error(`[Agent] Token exchange error - ${error}`);
1319
+ return {
1320
+ success: false,
1321
+ status: 500,
1322
+ message: "Token exchange failed"
1323
+ };
1324
+ }
1325
+ if (!exchanged.success) {
1326
+ console.error(`[Agent] Token exchange failed - ${exchanged.message}`);
1327
+ return {
1328
+ success: false,
1329
+ status: exchanged.subjectTokenRejected ? 401 : 500,
1330
+ message: exchanged.subjectTokenRejected ? "Unauthorized" : "Token exchange failed"
1331
+ };
1332
+ }
1333
+ await this._storeExchangedToken(exchanged);
1334
+ token = exchanged.accessToken;
1335
+ }
1336
+ if (!this.getUserContext) return { success: true };
1337
+ try {
1338
+ this.userContext = await this.getUserContext(token);
1339
+ return {
1340
+ success: true,
1341
+ userContext: this.userContext
1342
+ };
1343
+ } catch (error) {
1344
+ console.error(`[Agent] getUserContext error - ${error}`);
1345
+ const closeCode = getConnectCloseCode(error);
1346
+ return {
1347
+ success: false,
1348
+ status: closeCode === 4003 ? 403 : closeCode === 4001 ? 401 : 500,
1349
+ message: "Could not establish user context"
1350
+ };
1351
+ }
1352
+ }
1327
1353
  /**
1328
1354
  * The exchanged token for this agent, refreshed on every authenticated
1329
1355
  * connect. Kept in memory for the hot path and mirrored to DO storage so
@@ -1788,44 +1814,66 @@ var Assistant = class extends Agent$1 {
1788
1814
  sendAgentType(connection, "assistant");
1789
1815
  sendSubAgentName(connection, this.agent.name);
1790
1816
  sendConnectionStatus(connection, "connecting");
1791
- const getJwtAuthConfig = this.agent.getJwtAuthConfig;
1792
- if (getJwtAuthConfig) {
1793
- const config = getJwtAuthConfig(this.env);
1794
- if (config) {
1795
- let result;
1796
- try {
1797
- result = await verifyJwt(ctx.request, config);
1798
- } catch (error) {
1799
- sendConnectionStatus(connection, "unauthorized");
1800
- console.error(`[Assistant] JWT verification error - ${error}`);
1801
- connection.close(4001, "Unauthorized");
1802
- return;
1803
- }
1804
- if (!result.success) {
1805
- sendConnectionStatus(connection, "unauthorized");
1806
- console.error(`[Assistant] JWT verification error - ${result.message}`);
1807
- connection.close(result.status === 401 ? 4001 : 4003, result.message);
1808
- return;
1809
- }
1810
- if (!isJwtActorAuthorized(config, result.claims, {
1817
+ const auth = await this.authenticate(ctx.request);
1818
+ if (!auth.success) {
1819
+ sendConnectionStatus(connection, "unauthorized");
1820
+ connection.close(auth.status === 401 ? 4001 : 4003, auth.message);
1821
+ return;
1822
+ }
1823
+ sendConnectionStatus(connection, "connected");
1824
+ }
1825
+ async authenticate(request) {
1826
+ const config = this.agent.getJwtAuthConfig?.(this.env);
1827
+ if (!config) return { success: true };
1828
+ let result;
1829
+ try {
1830
+ result = await verifyJwt(request, config);
1831
+ } catch (error) {
1832
+ console.error(`[Assistant] JWT verification error - ${error}`);
1833
+ return {
1834
+ success: false,
1835
+ status: 401,
1836
+ message: "Unauthorized"
1837
+ };
1838
+ }
1839
+ if (!result.success) return result;
1840
+ if (!isJwtActorAuthorized(config, result.claims, {
1841
+ durableObjectName: this.name,
1842
+ parentPath: this.parentPath
1843
+ })) {
1844
+ console.error("[Assistant] Authenticated actor does not own the requested assistant", {
1845
+ authenticatedActorId: config.getActorId(result.claims),
1846
+ requiredActorId: getTargetActorId({
1811
1847
  durableObjectName: this.name,
1812
1848
  parentPath: this.parentPath
1813
- })) {
1814
- sendConnectionStatus(connection, "unauthorized");
1815
- console.error("[Assistant] Authenticated actor does not own the requested assistant", {
1816
- authenticatedActorId: config.getActorId(result.claims),
1817
- requiredActorId: getTargetActorId({
1818
- durableObjectName: this.name,
1819
- parentPath: this.parentPath
1820
- }),
1821
- durableObjectName: this.name
1822
- });
1823
- connection.close(4003, "Forbidden");
1824
- return;
1825
- }
1826
- }
1849
+ }),
1850
+ durableObjectName: this.name
1851
+ });
1852
+ return {
1853
+ success: false,
1854
+ status: 403,
1855
+ message: "Forbidden"
1856
+ };
1827
1857
  }
1828
- sendConnectionStatus(connection, "connected");
1858
+ return { success: true };
1859
+ }
1860
+ async onRequest(request) {
1861
+ const auth = await this.authenticate(request);
1862
+ if (!auth.success) return Response.json({ error: auth.message }, { status: auth.status });
1863
+ const action = new URL(request.url).pathname.split("/").pop();
1864
+ if (action === "getChats" && request.method === "GET") return Response.json(await this.getChats());
1865
+ if (action === "createChat" && request.method === "POST") return Response.json({ id: await this.createChat() }, { status: 201 });
1866
+ if (action === "deleteChat" && request.method === "POST") {
1867
+ const { id } = await request.json();
1868
+ await this.deleteChat(id);
1869
+ return new Response(null, { status: 204 });
1870
+ }
1871
+ if (action === "renameChat" && request.method === "POST") {
1872
+ const { id, title } = await request.json();
1873
+ await this.renameChat(id, title);
1874
+ return new Response(null, { status: 204 });
1875
+ }
1876
+ return super.onRequest(request);
1829
1877
  }
1830
1878
  async createChat() {
1831
1879
  const id = nanoid();
@@ -2348,6 +2396,65 @@ var ChatAgent = class extends Agent {
2348
2396
  const parent = await this.getParentAgent();
2349
2397
  if (parent?.recordChatTurn) await parent.recordChatTurn(this.name, this.messages);
2350
2398
  }
2399
+ async onRequest(request) {
2400
+ const auth = await this.authenticate(request);
2401
+ if (!auth.success) return Response.json({ error: auth.message }, { status: auth.status });
2402
+ const action = new URL(request.url).pathname.split("/").pop();
2403
+ if (action === "getMessages" && request.method === "GET") return Response.json(await this.getMessages());
2404
+ if (action === "sendMessage" && request.method === "POST") return this.streamMessage(request);
2405
+ if (action === "cancelChat" && request.method === "POST") {
2406
+ const { requestId } = await request.json();
2407
+ this.cancelChat(requestId);
2408
+ return new Response(null, { status: 204 });
2409
+ }
2410
+ return super.onRequest(request);
2411
+ }
2412
+ /**
2413
+ * Runs the turn from the stream's start callback so the response can be
2414
+ * returned immediately and each event reaches the client as it is produced.
2415
+ */
2416
+ async streamMessage(request) {
2417
+ let message;
2418
+ try {
2419
+ ({message} = await request.json());
2420
+ } catch {
2421
+ return Response.json({ error: "Invalid request body" }, { status: 400 });
2422
+ }
2423
+ if (typeof message === "string") message = {
2424
+ id: crypto.randomUUID(),
2425
+ role: "user",
2426
+ parts: [{
2427
+ type: "text",
2428
+ text: message
2429
+ }]
2430
+ };
2431
+ else if (!message || message.role !== "user" || !Array.isArray(message.parts)) return Response.json({ error: "message must be a user message" }, { status: 400 });
2432
+ const encoder = new TextEncoder();
2433
+ const readable = new ReadableStream({ start: async (controller) => {
2434
+ const send = (event, data) => controller.enqueue(encoder.encode(`event: ${event}\ndata: ${data}\n\n`));
2435
+ try {
2436
+ await this.runTurn({
2437
+ mode: "stream",
2438
+ input: message,
2439
+ callback: {
2440
+ onStart: (event) => send("start", JSON.stringify(event)),
2441
+ onEvent: (json) => send("chunk", json),
2442
+ onDone: () => send("done", "{}"),
2443
+ onError: (error) => send("error", JSON.stringify({ error })),
2444
+ onInterrupted: () => send("interrupted", "{}")
2445
+ }
2446
+ });
2447
+ } catch (error) {
2448
+ send("error", JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
2449
+ } finally {
2450
+ controller.close();
2451
+ }
2452
+ } });
2453
+ return new Response(readable, { headers: {
2454
+ "Content-Type": "text/event-stream",
2455
+ "Cache-Control": "no-cache"
2456
+ } });
2457
+ }
2351
2458
  /**
2352
2459
  * Submit feedback for a message by its id.
2353
2460
  * @param messageId - The id of the message to give feedback on.
@@ -2382,6 +2489,34 @@ var ChatAgent = class extends Agent {
2382
2489
  }
2383
2490
  };
2384
2491
  //#endregion
2492
+ //#region src/server/util/actions.ts
2493
+ /** A native Think action with the SDK's standard authorization policy. */
2494
+ function action(definition) {
2495
+ const { authorize, onUnauthorized, execute, ...config } = definition;
2496
+ const descriptor = action$1({
2497
+ ...config,
2498
+ approval: config.approval ?? true,
2499
+ execute: async (input, ctx) => {
2500
+ const idempotencyKey = typeof config.idempotencyKey === "function" ? await config.idempotencyKey({
2501
+ input,
2502
+ ctx
2503
+ }) : config.idempotencyKey ?? ctx.toolCallId;
2504
+ return execute(input, {
2505
+ ...ctx,
2506
+ messages: [...ctx.messages],
2507
+ abortSignal: ctx.signal,
2508
+ experimental_context: getCurrentAgent().agent._lastBody,
2509
+ idempotencyKey
2510
+ });
2511
+ }
2512
+ });
2513
+ return Object.freeze({
2514
+ ...descriptor,
2515
+ authorize,
2516
+ onUnauthorized
2517
+ });
2518
+ }
2519
+ //#endregion
2385
2520
  //#region src/server/util/skills.ts
2386
2521
  function skill(definition) {
2387
2522
  if (!definition.name) throw new Error("Skill name is required");
@@ -2405,6 +2540,7 @@ function skill(definition) {
2405
2540
  };
2406
2541
  },
2407
2542
  tools: definition.tools,
2543
+ actions: definition.actions,
2408
2544
  authorize: definition.authorize
2409
2545
  };
2410
2546
  }
@@ -2427,4 +2563,4 @@ function getCurrentToolContext() {
2427
2563
  return getCurrentAgent().agent._lastBody;
2428
2564
  }
2429
2565
  //#endregion
2430
- export { Agent, Assistant, ChatAgent, createJwtAuthConfig, exchangeToken, getCurrentToolContext, routeAgentRequest, skill, tool };
2566
+ export { Agent, Assistant, ChatAgent, action, createJwtAuthConfig, exchangeToken, getCurrentToolContext, routeAgentRequest, skill, tool };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@economic/agents",
3
- "version": "2.10.6",
3
+ "version": "2.11.0",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "license": "MIT",
6
6
  "files": [