@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 +89 -11
- package/dist/index.d.mts +49 -8
- package/dist/index.mjs +424 -149
- package/package.json +1 -1
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
|
|
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
|
|
589
|
-
| `
|
|
590
|
-
| `
|
|
591
|
-
| `
|
|
592
|
-
| `
|
|
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>(
|
|
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
|
-
|
|
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
|
|
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.
|
|
235
|
-
*
|
|
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(
|
|
298
|
-
|
|
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
|
|
966
|
-
if (
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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))
|
|
1238
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1792
|
-
if (
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
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
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
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
|
-
|
|
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({
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
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)
|
|
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
|
-
|
|
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 };
|