@economic/agents 2.10.5 → 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
@@ -7,7 +7,7 @@ React client: [`@economic/agents-react`](../react/README.md).
7
7
  ## The three classes
8
8
 
9
9
  - **`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.
10
- - **`ChatAgent`** — `Agent` plus chat features: compaction and message feedback.
10
+ - **`ChatAgent`** — `Agent` plus chat features: model-only compaction that preserves the client transcript, and message feedback.
11
11
  - **`Assistant`** — per-user shell over `ChatAgent`: create/list/delete chats, titles, summaries, retention.
12
12
 
13
13
  ```
@@ -188,7 +188,7 @@ You can also drive turns server-side — call `saveMessages` from a schedule or
188
188
 
189
189
  ## A chat agent
190
190
 
191
- For a chat UI, extend `ChatAgent` instead of `Agent`. It adds compaction and message feedback, and 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.
191
+ For a chat UI, extend `ChatAgent` instead of `Agent`. It adds model-only compaction (the stored/client transcript remains unchanged) and message feedback, and 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.
192
192
 
193
193
  ```typescript
194
194
  import { openai } from "@ai-sdk/openai";
@@ -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";
@@ -578,18 +655,19 @@ Imported from `@economic/agents` unless noted.
578
655
  | Export | Description |
579
656
  | ----------- | -------------------------------------------------------- |
580
657
  | `Agent` | Core agent base. Implement `getModel`/`getSystemPrompt`. |
581
- | `ChatAgent` | `Agent` + compaction and message feedback. |
658
+ | `ChatAgent` | `Agent` + model-only compaction and message feedback. |
582
659
  | `Assistant` | Per-user manager of `ChatAgent` chats. |
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
@@ -186,7 +197,7 @@ declare abstract class Agent<RequestContext extends Record<string, unknown> = Re
186
197
  * @returns The remote skills to be loaded from SKILLS_BUCKET.
187
198
  */
188
199
  protected getRemoteSkills(): string[];
189
- private _pruneTurnMessages;
200
+ protected pruneTurnMessages(messages: ModelMessage[]): ModelMessage[];
190
201
  private _getTurnSystemPrompt;
191
202
  private _prepareTools;
192
203
  /**
@@ -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,11 +1,11 @@
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";
6
6
  import { genericObservability } from "agents/observability";
7
+ import { createCompactFunction, estimateMessageTokens, estimateStringTokens, sanitizeToolPairs, truncateOlderMessages } from "agents/experimental/memory/utils";
7
8
  import { nanoid } from "nanoid";
8
- import { createCompactFunction } from "agents/experimental/memory/utils";
9
9
  //#region src/server/features/auth/index.ts
10
10
  function createJwtAuthConfig(config) {
11
11
  return config;
@@ -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
  /**
@@ -1058,7 +985,7 @@ var Agent = class extends Think {
1058
985
  return {
1059
986
  system: this._getTurnSystemPrompt(),
1060
987
  model: this.getModel(this._turn.context),
1061
- messages: this._pruneTurnMessages(ctx.messages),
988
+ messages: this.pruneTurnMessages(ctx.messages),
1062
989
  tools: this._withTurnContextFallback(tools),
1063
990
  activeTools,
1064
991
  experimental_transform: buildSourcesTransform(),
@@ -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;
@@ -1165,7 +1100,7 @@ var Agent = class extends Think {
1165
1100
  tools: withFallbackToolContext(tools, () => snapshotContext),
1166
1101
  activeTools,
1167
1102
  frameworkToolNames: Object.keys(frameworkTools),
1168
- messages: this._pruneTurnMessages(messages)
1103
+ messages: this.pruneTurnMessages(messages)
1169
1104
  };
1170
1105
  } finally {
1171
1106
  this._turn = previousTurn;
@@ -1183,7 +1118,7 @@ var Agent = class extends Think {
1183
1118
  getRemoteSkills() {
1184
1119
  return [];
1185
1120
  }
1186
- _pruneTurnMessages(messages) {
1121
+ pruneTurnMessages(messages) {
1187
1122
  return pruneMessages({
1188
1123
  messages,
1189
1124
  toolCalls: [{
@@ -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();
@@ -1978,15 +2026,130 @@ var Assistant = class extends Agent$1 {
1978
2026
  agentClass: this.constructor.name
1979
2027
  }) });
1980
2028
  };
1981
- //#endregion
1982
- //#region src/server/features/messages.ts
1983
- const COMPACTION_TOKEN_THRESHOLD = 1e5;
1984
2029
  const COMPACTION_MAX_OUTPUT_TOKENS = 2e3;
1985
- const createCompactFn = (model) => createCompactFunction({ summarize: (prompt) => generateText({
1986
- model,
1987
- prompt,
1988
- maxOutputTokens: COMPACTION_MAX_OUTPUT_TOKENS
1989
- }).then((result) => result.text) });
2030
+ const createCompactFn = (model) => createCompactFunction({
2031
+ protectHead: 2,
2032
+ minTailMessages: 1,
2033
+ summarize: (prompt) => generateText({
2034
+ model,
2035
+ prompt,
2036
+ maxOutputTokens: COMPACTION_MAX_OUTPUT_TOKENS
2037
+ }).then((result) => result.text)
2038
+ });
2039
+ const MODEL_COMPACTION_MESSAGE_ID = "compaction_model_context";
2040
+ /**
2041
+ * Model compaction is deliberately stored separately from assistant_messages.
2042
+ * The transcript remains exact for clients; only model input uses the summary.
2043
+ */
2044
+ function ensureModelCompactionTable(sql) {
2045
+ sql`CREATE TABLE IF NOT EXISTS economic_chat_model_compaction (
2046
+ id INTEGER PRIMARY KEY CHECK (id = 1),
2047
+ summary TEXT NOT NULL,
2048
+ from_message_id TEXT NOT NULL,
2049
+ to_message_id TEXT NOT NULL,
2050
+ updated_at INTEGER NOT NULL
2051
+ )`;
2052
+ }
2053
+ function getModelCompaction(sql) {
2054
+ const row = sql`SELECT summary, from_message_id, to_message_id
2055
+ FROM economic_chat_model_compaction
2056
+ WHERE id = 1`[0];
2057
+ return row ? {
2058
+ summary: row.summary,
2059
+ fromMessageId: row.from_message_id,
2060
+ toMessageId: row.to_message_id
2061
+ } : null;
2062
+ }
2063
+ function setModelCompaction(sql, state) {
2064
+ sql`INSERT INTO economic_chat_model_compaction (
2065
+ id, summary, from_message_id, to_message_id, updated_at
2066
+ ) VALUES (
2067
+ 1, ${state.summary}, ${state.fromMessageId}, ${state.toMessageId}, ${Date.now()}
2068
+ ) ON CONFLICT (id) DO UPDATE SET
2069
+ summary = excluded.summary,
2070
+ from_message_id = excluded.from_message_id,
2071
+ to_message_id = excluded.to_message_id,
2072
+ updated_at = excluded.updated_at`;
2073
+ }
2074
+ function clearModelCompaction(sql) {
2075
+ sql`DELETE FROM economic_chat_model_compaction WHERE id = 1`;
2076
+ }
2077
+ /**
2078
+ * Move summaries created by the old Session overlay implementation into the
2079
+ * model-only store, then remove the overlays. Session retained the original
2080
+ * rows, so removing an overlay restores the exact transcript.
2081
+ */
2082
+ function migrateSessionCompaction(sql) {
2083
+ const legacy = sql`SELECT
2084
+ summary, from_message_id, to_message_id
2085
+ FROM assistant_compactions
2086
+ WHERE session_id = ''
2087
+ ORDER BY created_at DESC, rowid DESC
2088
+ LIMIT 1`[0];
2089
+ if (!legacy) return false;
2090
+ if (!getModelCompaction(sql)) setModelCompaction(sql, {
2091
+ summary: legacy.summary,
2092
+ fromMessageId: legacy.from_message_id,
2093
+ toMessageId: legacy.to_message_id
2094
+ });
2095
+ sql`DELETE FROM assistant_compactions WHERE session_id = ''`;
2096
+ return true;
2097
+ }
2098
+ function applyModelCompaction(messages, state) {
2099
+ const fromIndex = messages.findIndex((message) => message.id === state.fromMessageId);
2100
+ const toIndex = messages.findIndex((message) => message.id === state.toMessageId);
2101
+ if (fromIndex === -1 || toIndex < fromIndex) return null;
2102
+ return [
2103
+ ...messages.slice(0, fromIndex),
2104
+ {
2105
+ id: MODEL_COMPACTION_MESSAGE_ID,
2106
+ role: "assistant",
2107
+ parts: [{
2108
+ type: "text",
2109
+ text: state.summary
2110
+ }]
2111
+ },
2112
+ ...messages.slice(toIndex + 1)
2113
+ ];
2114
+ }
2115
+ /** Build the compacted projection used only for the next model call. */
2116
+ async function prepareModelHistory(messages, existingState, compact, systemPrompt = "") {
2117
+ let state = existingState;
2118
+ let projected = state ? applyModelCompaction(messages, state) : messages;
2119
+ let stateChanged = false;
2120
+ if (!projected) {
2121
+ state = null;
2122
+ projected = messages;
2123
+ stateChanged = true;
2124
+ }
2125
+ if (estimateMessageTokens(projected) + estimateStringTokens(systemPrompt) <= 1e5) return {
2126
+ messages: projected,
2127
+ state,
2128
+ stateChanged
2129
+ };
2130
+ const result = await compact(projected);
2131
+ if (!result) return {
2132
+ messages: projected,
2133
+ state,
2134
+ stateChanged
2135
+ };
2136
+ const nextState = {
2137
+ summary: result.summary,
2138
+ fromMessageId: state?.fromMessageId ?? result.fromMessageId,
2139
+ toMessageId: result.toMessageId
2140
+ };
2141
+ const nextProjection = applyModelCompaction(messages, nextState);
2142
+ if (!nextProjection) return {
2143
+ messages: projected,
2144
+ state,
2145
+ stateChanged
2146
+ };
2147
+ return {
2148
+ messages: nextProjection,
2149
+ state: nextState,
2150
+ stateChanged: true
2151
+ };
2152
+ }
1990
2153
  /**
1991
2154
  * Ensures that the feedback table exists.
1992
2155
  * @param sql - The SQL function to use to execute the query.
@@ -2191,14 +2354,38 @@ var ChatAgent = class extends Agent {
2191
2354
  async onStart() {
2192
2355
  await super.onStart();
2193
2356
  ensureFeedbackTableExists(this.sql.bind(this));
2357
+ ensureModelCompactionTable(this.sql.bind(this));
2358
+ if (migrateSessionCompaction(this.sql.bind(this))) await this.syncMessagesFromStorage();
2194
2359
  }
2195
2360
  configureSession(session) {
2196
- return super.configureSession(session).onCompaction(createCompactFn(this.getModel())).compactAfter(COMPACTION_TOKEN_THRESHOLD);
2361
+ return super.configureSession(session);
2197
2362
  }
2198
2363
  async beforeTurn(ctx) {
2199
2364
  const parent = await this.getParentAgent();
2200
2365
  if (parent?.registerChat) await parent.registerChat(this.name);
2201
- return super.beforeTurn(ctx);
2366
+ const config = await super.beforeTurn(ctx) ?? {};
2367
+ try {
2368
+ const prepared = await prepareModelHistory(this.messages, getModelCompaction(this.sql.bind(this)), createCompactFn(config.model ?? ctx.model), config.system ?? ctx.system);
2369
+ if (prepared.stateChanged) if (prepared.state) setModelCompaction(this.sql.bind(this), prepared.state);
2370
+ else clearModelCompaction(this.sql.bind(this));
2371
+ if (prepared.state) {
2372
+ const tools = config.tools ? {
2373
+ ...ctx.tools,
2374
+ ...config.tools
2375
+ } : ctx.tools;
2376
+ const modelMessages = await convertToModelMessages(truncateOlderMessages(sanitizeToolPairs(prepared.messages)), {
2377
+ tools,
2378
+ ignoreIncompleteToolCalls: true
2379
+ });
2380
+ return {
2381
+ ...config,
2382
+ messages: this.pruneTurnMessages(modelMessages)
2383
+ };
2384
+ }
2385
+ } catch (error) {
2386
+ console.error("[ChatAgent] Failed to prepare model-only compaction", error);
2387
+ }
2388
+ return config;
2202
2389
  }
2203
2390
  async onChatResponse(result) {
2204
2391
  recordTurnCompletedAnalytics(this.env.AGENTS_ANALYTICS, {
@@ -2209,6 +2396,65 @@ var ChatAgent = class extends Agent {
2209
2396
  const parent = await this.getParentAgent();
2210
2397
  if (parent?.recordChatTurn) await parent.recordChatTurn(this.name, this.messages);
2211
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
+ }
2212
2458
  /**
2213
2459
  * Submit feedback for a message by its id.
2214
2460
  * @param messageId - The id of the message to give feedback on.
@@ -2243,6 +2489,34 @@ var ChatAgent = class extends Agent {
2243
2489
  }
2244
2490
  };
2245
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
2246
2520
  //#region src/server/util/skills.ts
2247
2521
  function skill(definition) {
2248
2522
  if (!definition.name) throw new Error("Skill name is required");
@@ -2266,6 +2540,7 @@ function skill(definition) {
2266
2540
  };
2267
2541
  },
2268
2542
  tools: definition.tools,
2543
+ actions: definition.actions,
2269
2544
  authorize: definition.authorize
2270
2545
  };
2271
2546
  }
@@ -2288,4 +2563,4 @@ function getCurrentToolContext() {
2288
2563
  return getCurrentAgent().agent._lastBody;
2289
2564
  }
2290
2565
  //#endregion
2291
- 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.5",
3
+ "version": "2.11.0",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "license": "MIT",
6
6
  "files": [