@economic/agents 2.6.0 → 2.7.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 +9 -7
- package/dist/index.d.mts +4 -87
- package/dist/index.mjs +130 -260
- package/dist/v1.d.mts +0 -11
- package/dist/v1.mjs +0 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Our agents SDK for building AI agents on Cloudflare Workers. Each agent is a Dur
|
|
|
4
4
|
|
|
5
5
|
React client: [`@economic/agents-react`](../react/README.md).
|
|
6
6
|
|
|
7
|
-
> The v1 (`@economic/agents/v1`) API (`ChatAgentHarness`, `buildLLMParams`) is deprecated
|
|
7
|
+
> The v1 (`@economic/agents/v1`) API (`ChatAgentHarness`, `buildLLMParams`) is deprecated and will be removed in v3.
|
|
8
8
|
|
|
9
9
|
## The three classes
|
|
10
10
|
|
|
@@ -252,15 +252,17 @@ const { status, chats, assistant, chat } = useAssistant({
|
|
|
252
252
|
});
|
|
253
253
|
```
|
|
254
254
|
|
|
255
|
-
The `Assistant` keeps the chat list in its own SQLite (a `chats` table, created automatically) and exposes
|
|
255
|
+
The `Assistant` keeps the chat list in its own SQLite (a `chats` table, created automatically) and exposes four callable methods. A newly created chat facet is added to this table when its first turn starts.
|
|
256
256
|
|
|
257
|
-
| Method | Returns | Description
|
|
258
|
-
| ---------------- | -------- |
|
|
259
|
-
| `createChat()` | `string` | Creates a chat and returns its id.
|
|
260
|
-
| `getChats()` | `Chat[]` |
|
|
261
|
-
| `deleteChat(id)` | `void` | Deletes a chat and its record.
|
|
257
|
+
| Method | Returns | Description |
|
|
258
|
+
| ---------------- | -------- | ------------------------------------------- |
|
|
259
|
+
| `createChat()` | `string` | Creates a chat facet and returns its id. |
|
|
260
|
+
| `getChats()` | `Chat[]` | Started chats, most recently updated first. |
|
|
261
|
+
| `deleteChat(id)` | `void` | Deletes a chat facet and its record. |
|
|
262
|
+
| `renameChat(id)` | `void` | Renames a registered chat. |
|
|
262
263
|
|
|
263
264
|
- **Titles and summaries** — generated with `fastModel` after each turn (on the first turn, then refreshed as the chat grows).
|
|
265
|
+
- **Empty chat cleanup** — a newly created facet is scheduled for deletion after one day. Its first completed turn replaces that schedule with the normal sliding retention period.
|
|
264
266
|
- **Retention** — inactive chats are deleted after 90 days, via a Durable Object alarm.
|
|
265
267
|
|
|
266
268
|
## Bindings
|
package/dist/index.d.mts
CHANGED
|
@@ -22,7 +22,6 @@ interface AgentEnv {
|
|
|
22
22
|
SKILLS_BUCKET: R2Bucket;
|
|
23
23
|
LOCAL_AGENT_DB?: D1Database;
|
|
24
24
|
}
|
|
25
|
-
type SqlFn = InstanceType<typeof Agent$1>["sql"];
|
|
26
25
|
type AgentConnectionStatus = "connecting" | "connected" | "disconnected" | "unauthorized" | "error";
|
|
27
26
|
type AgentConnectionType = "agent" | "chat" | "assistant";
|
|
28
27
|
//#endregion
|
|
@@ -225,10 +224,7 @@ declare abstract class Agent<RequestContext extends Record<string, unknown> = Re
|
|
|
225
224
|
private _getExchangedToken;
|
|
226
225
|
/**
|
|
227
226
|
* Swaps the client-sent token for the exchanged one in a copy of the turn
|
|
228
|
-
* body. No-op when token exchange is not configured.
|
|
229
|
-
* token is missing or expired the turn cannot run: only a reconnect can
|
|
230
|
-
* deliver a fresh subject token, so the current connection (if any) is
|
|
231
|
-
* closed to force one, and the turn fails.
|
|
227
|
+
* body. No-op when token exchange is not configured.
|
|
232
228
|
*/
|
|
233
229
|
private _applyTokenExchange;
|
|
234
230
|
/**
|
|
@@ -290,59 +286,12 @@ type MessageFeedback = {
|
|
|
290
286
|
updated_at: number;
|
|
291
287
|
};
|
|
292
288
|
//#endregion
|
|
293
|
-
//#region src/server/features/migration.d.ts
|
|
294
|
-
/** Per-message feedback carried over from a v1 conversation during migration. */
|
|
295
|
-
type LegacyMessageFeedback = {
|
|
296
|
-
messageId: string;
|
|
297
|
-
rating: number;
|
|
298
|
-
comment?: string;
|
|
299
|
-
};
|
|
300
|
-
/** Minimal RPC surface of a v1 chat DO needed to read its history. */
|
|
301
|
-
interface LegacyChatStub {
|
|
302
|
-
exportForMigration(): Promise<{
|
|
303
|
-
messages: UIMessage[];
|
|
304
|
-
}>;
|
|
305
|
-
}
|
|
306
|
-
/** Minimal RPC surface of a freshly created v2 chat facet needed to seed history. */
|
|
307
|
-
interface FacetStub {
|
|
308
|
-
importLegacyMessages(messages: UIMessage[], feedback: LegacyMessageFeedback[]): Promise<void>;
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Dependencies for {@link migrateUserFromV1}, injected by the `Assistant` so
|
|
312
|
-
* this module never needs to touch the DO's protected members directly.
|
|
313
|
-
*/
|
|
314
|
-
type MigrationDeps = {
|
|
315
|
-
/** The `Assistant` DO's bound `sql` tag (used to register migrated chats). */sql: SqlFn; /** Shared D1 database holding v1 `conversations` and `message_ratings`. */
|
|
316
|
-
db: D1Database; /** The user being migrated (the `Assistant` DO name). */
|
|
317
|
-
userId: string; /** Resolves a v1 chat DO stub by its `userId:chatId` name. */
|
|
318
|
-
durableObjectBinding: DurableObjectNamespace; /** Creates (or resolves) a v2 chat facet for the given chat id and returns its stub. */
|
|
319
|
-
createFacet: (chatId: string) => Promise<FacetStub>;
|
|
320
|
-
};
|
|
321
|
-
/**
|
|
322
|
-
* Migrates all of a user's v1 chats into v2 facets, lazily and idempotently.
|
|
323
|
-
*
|
|
324
|
-
* v1 chats are enumerated from the shared D1 `conversations` table (DOs cannot
|
|
325
|
-
* be listed directly). For each chat we read its persisted messages from the v1
|
|
326
|
-
* DO over RPC, create a fresh facet, seed its history and feedback, register it
|
|
327
|
-
* on the parent `Assistant` with its original title/summary/timestamps, and
|
|
328
|
-
* finally delete the v1 `conversations` (+ `message_ratings`) rows.
|
|
329
|
-
*
|
|
330
|
-
* The conversations row IS the to-do marker: deleting it last means a migrated
|
|
331
|
-
* chat never reappears, while a chat that errored keeps its row and is simply
|
|
332
|
-
* retried on the next connection. No extra bookkeeping tables are needed. The
|
|
333
|
-
* v1 DO is left untouched and self-expires via v1 retention, so its messages
|
|
334
|
-
* remain as a backstop until then.
|
|
335
|
-
*/
|
|
336
|
-
declare function migrateUserFromV1(deps: MigrationDeps): Promise<{
|
|
337
|
-
migrated: number;
|
|
338
|
-
failed: number;
|
|
339
|
-
}>;
|
|
340
|
-
//#endregion
|
|
341
289
|
//#region src/server/agents/ChatAgent.d.ts
|
|
342
290
|
declare abstract class ChatAgent<RequestContext extends Record<string, unknown> = Record<string, unknown>, UserContext extends Record<string, unknown> = Record<string, unknown>, State = unknown> extends Agent<RequestContext, UserContext, State> {
|
|
343
291
|
protected agentType: AgentConnectionType;
|
|
344
292
|
onStart(): Promise<void>;
|
|
345
293
|
configureSession(session: Session): Session;
|
|
294
|
+
beforeTurn(ctx: TurnContext): Promise<void | TurnConfig>;
|
|
346
295
|
onChatResponse(_result: ChatResponseResult): Promise<void>;
|
|
347
296
|
/**
|
|
348
297
|
* Submit feedback for a message by its id.
|
|
@@ -356,19 +305,6 @@ declare abstract class ChatAgent<RequestContext extends Record<string, unknown>
|
|
|
356
305
|
* @returns All message feedback for the current chat.
|
|
357
306
|
*/
|
|
358
307
|
getMessageFeedback(): Promise<Record<string, MessageFeedback>>;
|
|
359
|
-
/**
|
|
360
|
-
* Imports a v1 conversation's history into this facet's session storage.
|
|
361
|
-
*
|
|
362
|
-
* Called over DO RPC by the v2 `Assistant` during the lazy v1 -> v2
|
|
363
|
-
* migration. Messages are appended in order as a single linear thread
|
|
364
|
-
* (each message parented to the previous one) using
|
|
365
|
-
* `appendMessageToHistory`, which writes durably to the session WITHOUT
|
|
366
|
-
* triggering a model turn. Any carried-over feedback is then written to
|
|
367
|
-
* `assistant_messages_feedback`.
|
|
368
|
-
*
|
|
369
|
-
* Safe to skip persisting if there is nothing to import.
|
|
370
|
-
*/
|
|
371
|
-
importLegacyMessages(messages: UIMessage[], feedback?: LegacyMessageFeedback[]): Promise<void>;
|
|
372
308
|
}
|
|
373
309
|
//#endregion
|
|
374
310
|
//#region src/server/features/chats.d.ts
|
|
@@ -396,6 +332,7 @@ declare abstract class Assistant<State = unknown> extends Agent$1<Cloudflare.Env
|
|
|
396
332
|
renameChat(id: string, title: string): Promise<void>;
|
|
397
333
|
getChats(): Promise<Chat[]>;
|
|
398
334
|
getChatAgentClassName(): Promise<string>;
|
|
335
|
+
registerChat(durableObjectName: string): Promise<void>;
|
|
399
336
|
recordChatTurn(durableObjectName: string, messages: UIMessage[]): Promise<void>;
|
|
400
337
|
recordChatFeedback(durableObjectName: string, messageId: string, rating: number): Promise<void>;
|
|
401
338
|
private [DELETE_CHAT_CALLBACK];
|
|
@@ -416,26 +353,6 @@ declare abstract class Assistant<State = unknown> extends Agent$1<Cloudflare.Env
|
|
|
416
353
|
* Best-effort and non-blocking.
|
|
417
354
|
*/
|
|
418
355
|
private touchInstance;
|
|
419
|
-
/**
|
|
420
|
-
* Binding of the legacy v1 chat Durable Object class, used to migrate a
|
|
421
|
-
* user's v1 chats into facets the first time they connect. Set this on the
|
|
422
|
-
* concrete subclass to enable lazy v1 -> v2 migration; leave undefined to
|
|
423
|
-
* disable it (e.g. for greenfield deployments with no v1 data).
|
|
424
|
-
*/
|
|
425
|
-
protected getMigrationBinding(_env: Cloudflare.Env & AgentEnv): {
|
|
426
|
-
binding: DurableObjectNamespace;
|
|
427
|
-
db: D1Database;
|
|
428
|
-
} | undefined;
|
|
429
|
-
/** In-flight migration, shared across concurrent connections to this DO. */
|
|
430
|
-
private _migrationPromise?;
|
|
431
|
-
/**
|
|
432
|
-
* Runs the lazy v1 -> v2 migration for this user. Concurrent connections to
|
|
433
|
-
* this DO share a single in-flight run. Idempotency across runs/restarts is
|
|
434
|
-
* handled by `migrateUserFromV1` deleting each chat's v1 `conversations` row,
|
|
435
|
-
* so an already-migrated chat is never re-enumerated.
|
|
436
|
-
*/
|
|
437
|
-
private ensureMigrated;
|
|
438
|
-
private runMigration;
|
|
439
356
|
}
|
|
440
357
|
//#endregion
|
|
441
358
|
//#region src/server/util/skills.d.ts
|
|
@@ -455,4 +372,4 @@ declare function skill<Context extends Record<string, unknown> = Record<string,
|
|
|
455
372
|
//#region src/server/index.d.ts
|
|
456
373
|
declare function getCurrentToolContext(): any;
|
|
457
374
|
//#endregion
|
|
458
|
-
export { Agent, type AgentConnectionStatus, type AgentConnectionType, type AgentEnv, Assistant, ChatAgent, type ExchangedToken, type
|
|
375
|
+
export { Agent, type AgentConnectionStatus, type AgentConnectionType, type AgentEnv, Assistant, ChatAgent, type ExchangedToken, type Skill, type SkillSource, type TokenExchangeConfig, type TokenExchangeResult, type Tool, type ToolContext, type ToolSet, type TurnSnapshot, exchangeToken, getCurrentToolContext, routeAgentRequest, skill, tool };
|
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,37 @@ import { Think, skills } from "@cloudflare/think";
|
|
|
4
4
|
import { Output, convertToModelMessages, generateText, jsonSchema, pruneMessages, tool as tool$1 } from "ai";
|
|
5
5
|
import { nanoid } from "nanoid";
|
|
6
6
|
import { createCompactFunction } from "agents/experimental/memory/utils";
|
|
7
|
+
//#region src/server/util/agent.ts
|
|
8
|
+
function sendConnectionStatus(connection, status) {
|
|
9
|
+
connection.send(JSON.stringify({
|
|
10
|
+
type: "connection_status",
|
|
11
|
+
status
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
function sendAgentType(connection, agentType) {
|
|
15
|
+
connection.send(JSON.stringify({
|
|
16
|
+
type: "agent_type",
|
|
17
|
+
agentType
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
function sendSubAgentName(connection, subAgentName) {
|
|
21
|
+
connection.send(JSON.stringify({
|
|
22
|
+
type: "sub_agent_name",
|
|
23
|
+
subAgentName
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Close code for a getUserContext failure. Subclasses signal an intentional
|
|
28
|
+
* auth-style rejection by throwing an Error carrying a numeric `closeCode`
|
|
29
|
+
* in the WebSocket application range (4000-4999); anything else is treated
|
|
30
|
+
* as an internal error (1011).
|
|
31
|
+
*/
|
|
32
|
+
function getConnectCloseCode(error) {
|
|
33
|
+
const closeCode = error?.closeCode;
|
|
34
|
+
if (typeof closeCode === "number" && closeCode >= 4e3 && closeCode <= 4999) return closeCode;
|
|
35
|
+
return 1011;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
7
38
|
//#region src/server/features/auth/tokenExchange.ts
|
|
8
39
|
/**
|
|
9
40
|
* OAuth 2.0 Token Exchange (RFC 8693) against an identity server.
|
|
@@ -94,6 +125,56 @@ function withExchangedToken(body, tokenHeader, accessToken) {
|
|
|
94
125
|
}
|
|
95
126
|
};
|
|
96
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Connect-time exchange. On failure, signals the client and closes the
|
|
130
|
+
* connection — `unauthorized`/4001 when the session token itself was rejected
|
|
131
|
+
* (only a re-login fixes that), `error`/1011 for identity-server or
|
|
132
|
+
* configuration faults — and returns undefined so the caller abandons the
|
|
133
|
+
* connect.
|
|
134
|
+
*/
|
|
135
|
+
async function exchangeConnectToken(subjectToken, config, connection) {
|
|
136
|
+
let result;
|
|
137
|
+
try {
|
|
138
|
+
result = await exchangeToken(subjectToken, config);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
sendConnectionStatus(connection, "error");
|
|
141
|
+
console.error(`[Agent] Token exchange error - ${error}`);
|
|
142
|
+
connection.close(1011, "Token exchange failed");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!result.success) {
|
|
146
|
+
console.error(`[Agent] Token exchange failed - ${result.message}`);
|
|
147
|
+
if (result.subjectTokenRejected) {
|
|
148
|
+
sendConnectionStatus(connection, "unauthorized");
|
|
149
|
+
connection.close(4001, "Unauthorized");
|
|
150
|
+
} else {
|
|
151
|
+
sendConnectionStatus(connection, "error");
|
|
152
|
+
connection.close(1011, "Token exchange failed");
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
accessToken: result.accessToken,
|
|
158
|
+
expiresAt: result.expiresAt
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Turn-time injection: returns a body copy with the held token under
|
|
163
|
+
* `tokens[config.tokenHeader]`. When the held token is missing or expired the
|
|
164
|
+
* turn cannot run — only a re-authenticating reconnect delivers a fresh
|
|
165
|
+
* subject token — so the current connection (if any) is closed to force one,
|
|
166
|
+
* and this throws to fail the turn.
|
|
167
|
+
*/
|
|
168
|
+
function injectExchangedToken(body, config, token, connection) {
|
|
169
|
+
if (!token || token.expiresAt <= Date.now()) {
|
|
170
|
+
if (connection) {
|
|
171
|
+
sendConnectionStatus(connection, "unauthorized");
|
|
172
|
+
connection.close(4001, "Unauthorized");
|
|
173
|
+
}
|
|
174
|
+
throw new Error("Exchanged token missing or expired - the client must reconnect");
|
|
175
|
+
}
|
|
176
|
+
return withExchangedToken(body, config.tokenHeader ?? "X-EconomicToken", token.accessToken);
|
|
177
|
+
}
|
|
97
178
|
//#endregion
|
|
98
179
|
//#region src/server/util/tools.ts
|
|
99
180
|
function tool(tool) {
|
|
@@ -183,26 +264,6 @@ async function touchAgentInstance(db, input) {
|
|
|
183
264
|
WHERE agent_name = ? AND durable_object_name = ?`).bind(input.updatedAt, input.agentName, input.durableObjectName).run();
|
|
184
265
|
}
|
|
185
266
|
//#endregion
|
|
186
|
-
//#region src/server/util/agent.ts
|
|
187
|
-
function sendConnectionStatus(connection, status) {
|
|
188
|
-
connection.send(JSON.stringify({
|
|
189
|
-
type: "connection_status",
|
|
190
|
-
status
|
|
191
|
-
}));
|
|
192
|
-
}
|
|
193
|
-
function sendAgentType(connection, agentType) {
|
|
194
|
-
connection.send(JSON.stringify({
|
|
195
|
-
type: "agent_type",
|
|
196
|
-
agentType
|
|
197
|
-
}));
|
|
198
|
-
}
|
|
199
|
-
function sendSubAgentName(connection, subAgentName) {
|
|
200
|
-
connection.send(JSON.stringify({
|
|
201
|
-
type: "sub_agent_name",
|
|
202
|
-
subAgentName
|
|
203
|
-
}));
|
|
204
|
-
}
|
|
205
|
-
//#endregion
|
|
206
267
|
//#region src/server/util/llm.ts
|
|
207
268
|
/**
|
|
208
269
|
* Convention: tools can include a `sources` array in their return value to surface
|
|
@@ -240,17 +301,6 @@ function buildSourcesTransform(additional) {
|
|
|
240
301
|
//#endregion
|
|
241
302
|
//#region src/server/agents/Agent.ts
|
|
242
303
|
const EXCHANGED_TOKEN_STORAGE_KEY = "economic:exchangedToken";
|
|
243
|
-
/**
|
|
244
|
-
* Close code for a getUserContext failure. Subclasses signal an intentional
|
|
245
|
-
* auth-style rejection by throwing an Error carrying a numeric `closeCode`
|
|
246
|
-
* in the WebSocket application range (4000-4999); anything else is treated
|
|
247
|
-
* as an internal error (1011).
|
|
248
|
-
*/
|
|
249
|
-
function getConnectCloseCode(error) {
|
|
250
|
-
const closeCode = error?.closeCode;
|
|
251
|
-
if (typeof closeCode === "number" && closeCode >= 4e3 && closeCode <= 4999) return closeCode;
|
|
252
|
-
return 1011;
|
|
253
|
-
}
|
|
254
304
|
var Agent = class extends Think {
|
|
255
305
|
agentType = "agent";
|
|
256
306
|
clientIp;
|
|
@@ -314,31 +364,10 @@ var Agent = class extends Think {
|
|
|
314
364
|
const getTokenExchangeConfig = this.constructor.getTokenExchangeConfig;
|
|
315
365
|
const exchangeConfig = getTokenExchangeConfig?.(this.env);
|
|
316
366
|
if (token && exchangeConfig) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
sendConnectionStatus(connection, "error");
|
|
322
|
-
console.error(`[Agent] Token exchange error - ${error}`);
|
|
323
|
-
connection.close(1011, "Token exchange failed");
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
if (!result.success) {
|
|
327
|
-
console.error(`[Agent] Token exchange failed - ${result.message}`);
|
|
328
|
-
if (result.subjectTokenRejected) {
|
|
329
|
-
sendConnectionStatus(connection, "unauthorized");
|
|
330
|
-
connection.close(4001, "Unauthorized");
|
|
331
|
-
} else {
|
|
332
|
-
sendConnectionStatus(connection, "error");
|
|
333
|
-
connection.close(1011, "Token exchange failed");
|
|
334
|
-
}
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
await this._storeExchangedToken({
|
|
338
|
-
accessToken: result.accessToken,
|
|
339
|
-
expiresAt: result.expiresAt
|
|
340
|
-
});
|
|
341
|
-
effectiveToken = result.accessToken;
|
|
367
|
+
const exchanged = await exchangeConnectToken(token, exchangeConfig, connection);
|
|
368
|
+
if (!exchanged) return;
|
|
369
|
+
await this._storeExchangedToken(exchanged);
|
|
370
|
+
effectiveToken = exchanged.accessToken;
|
|
342
371
|
}
|
|
343
372
|
if (effectiveToken && this.getUserContext) try {
|
|
344
373
|
connection.setState({ userContext: await this.getUserContext(effectiveToken) });
|
|
@@ -587,25 +616,13 @@ var Agent = class extends Think {
|
|
|
587
616
|
}
|
|
588
617
|
/**
|
|
589
618
|
* Swaps the client-sent token for the exchanged one in a copy of the turn
|
|
590
|
-
* body. No-op when token exchange is not configured.
|
|
591
|
-
* token is missing or expired the turn cannot run: only a reconnect can
|
|
592
|
-
* deliver a fresh subject token, so the current connection (if any) is
|
|
593
|
-
* closed to force one, and the turn fails.
|
|
619
|
+
* body. No-op when token exchange is not configured.
|
|
594
620
|
*/
|
|
595
621
|
async _applyTokenExchange(body) {
|
|
596
622
|
const getTokenExchangeConfig = this.constructor.getTokenExchangeConfig;
|
|
597
623
|
const exchangeConfig = getTokenExchangeConfig?.(this.env);
|
|
598
624
|
if (!exchangeConfig) return body;
|
|
599
|
-
|
|
600
|
-
if (!token) {
|
|
601
|
-
const { connection } = getCurrentAgent();
|
|
602
|
-
if (connection) {
|
|
603
|
-
sendConnectionStatus(connection, "unauthorized");
|
|
604
|
-
connection.close(4001, "Unauthorized");
|
|
605
|
-
}
|
|
606
|
-
throw new Error("Exchanged token missing or expired - the client must reconnect");
|
|
607
|
-
}
|
|
608
|
-
return withExchangedToken(body, exchangeConfig.tokenHeader ?? "X-EconomicToken", token.accessToken);
|
|
625
|
+
return injectExchangedToken(body, exchangeConfig, await this._getExchangedToken(), getCurrentAgent().connection);
|
|
609
626
|
}
|
|
610
627
|
/**
|
|
611
628
|
* The user context for the session.
|
|
@@ -726,21 +743,7 @@ function ensureChatMessageFeedbackTableExists(sql) {
|
|
|
726
743
|
}
|
|
727
744
|
function registerChat(sql, durableObjectName, dateTime) {
|
|
728
745
|
sql`INSERT INTO chats (durable_object_name, created_at, updated_at)
|
|
729
|
-
VALUES (${durableObjectName}, ${dateTime}, ${dateTime})
|
|
730
|
-
}
|
|
731
|
-
/**
|
|
732
|
-
* Registers a chat while preserving metadata carried over from a v1
|
|
733
|
-
* conversation during migration: its title, summary, and original
|
|
734
|
-
* timestamps. Used so migrated chats keep their titles instead of waiting
|
|
735
|
-
* for the AI summariser to regenerate them.
|
|
736
|
-
*
|
|
737
|
-
* Idempotent: an existing row for the same chat id is left untouched.
|
|
738
|
-
*/
|
|
739
|
-
function registerChatWithMetadata(sql, durableObjectName, metadata) {
|
|
740
|
-
const { title, summary, createdAt } = metadata;
|
|
741
|
-
const updatedAt = metadata.updatedAt ?? createdAt;
|
|
742
|
-
sql`INSERT INTO chats (durable_object_name, title, summary, created_at, updated_at)
|
|
743
|
-
VALUES (${durableObjectName}, ${title ?? null}, ${summary ?? null}, ${createdAt}, ${updatedAt})
|
|
746
|
+
VALUES (${durableObjectName}, ${dateTime}, ${dateTime})
|
|
744
747
|
ON CONFLICT (durable_object_name) DO NOTHING`;
|
|
745
748
|
}
|
|
746
749
|
/**
|
|
@@ -783,8 +786,8 @@ function recordChatFeedback(sql, durableObjectName, messageId, rating, dateTime
|
|
|
783
786
|
negative_feedback_count = ${Number(negativeRows[0]?.count ?? 0)}
|
|
784
787
|
WHERE durable_object_name = ${durableObjectName}`;
|
|
785
788
|
}
|
|
786
|
-
function getDeleteChatScheduleIds(schedules) {
|
|
787
|
-
return schedules.filter((schedule) => schedule.callback ===
|
|
789
|
+
function getDeleteChatScheduleIds(schedules, durableObjectName) {
|
|
790
|
+
return schedules.filter((schedule) => schedule.callback === "deleteChatCallback" && schedule.payload === durableObjectName).map((schedule) => schedule.id);
|
|
788
791
|
}
|
|
789
792
|
/**
|
|
790
793
|
* Number of recent messages passed to `generateSummary` for rolling
|
|
@@ -845,92 +848,6 @@ function getChatRetentionMs(days) {
|
|
|
845
848
|
return Math.floor(days * 24 * 60 * 60 * 1e3);
|
|
846
849
|
}
|
|
847
850
|
//#endregion
|
|
848
|
-
//#region src/server/features/migration.ts
|
|
849
|
-
async function listLegacyConversations(db, userId) {
|
|
850
|
-
try {
|
|
851
|
-
const { results } = await db.prepare(`SELECT durable_object_name, title, summary, created_at, updated_at
|
|
852
|
-
FROM conversations WHERE durable_object_name LIKE ? ORDER BY updated_at ASC`).bind(`${userId}:%`).all();
|
|
853
|
-
return results ?? [];
|
|
854
|
-
} catch (error) {
|
|
855
|
-
if (error instanceof Error && error.message.includes(`no such table: conversations`)) {
|
|
856
|
-
console.warn("[Migration] Legacy conversations table missing; skipping v1 migration", { userId });
|
|
857
|
-
return [];
|
|
858
|
-
}
|
|
859
|
-
throw error;
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
async function listLegacyFeedback(db, durableObjectName) {
|
|
863
|
-
const { results } = await db.prepare(`SELECT message_id, rating, comment FROM message_ratings WHERE durable_object_name = ?`).bind(durableObjectName).all();
|
|
864
|
-
return (results ?? []).map((row) => ({
|
|
865
|
-
messageId: row.message_id,
|
|
866
|
-
rating: row.rating,
|
|
867
|
-
...row.comment != null ? { comment: row.comment } : {}
|
|
868
|
-
}));
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Removes a migrated v1 chat's D1 rows. Deleting the `conversations` row is what
|
|
872
|
-
* makes the migration idempotent: it drops out of the enumeration so it is never
|
|
873
|
-
* migrated again. The v1 DO itself is left to self-expire via v1 retention.
|
|
874
|
-
*/
|
|
875
|
-
async function deleteLegacyConversation(db, durableObjectName) {
|
|
876
|
-
await db.prepare(`DELETE FROM conversations WHERE durable_object_name = ?`).bind(durableObjectName).run();
|
|
877
|
-
await db.prepare(`DELETE FROM message_ratings WHERE durable_object_name = ?`).bind(durableObjectName).run();
|
|
878
|
-
}
|
|
879
|
-
function toEpochMs(value) {
|
|
880
|
-
if (!value) return Date.now();
|
|
881
|
-
const parsed = Date.parse(value);
|
|
882
|
-
return Number.isNaN(parsed) ? Date.now() : parsed;
|
|
883
|
-
}
|
|
884
|
-
/**
|
|
885
|
-
* Migrates all of a user's v1 chats into v2 facets, lazily and idempotently.
|
|
886
|
-
*
|
|
887
|
-
* v1 chats are enumerated from the shared D1 `conversations` table (DOs cannot
|
|
888
|
-
* be listed directly). For each chat we read its persisted messages from the v1
|
|
889
|
-
* DO over RPC, create a fresh facet, seed its history and feedback, register it
|
|
890
|
-
* on the parent `Assistant` with its original title/summary/timestamps, and
|
|
891
|
-
* finally delete the v1 `conversations` (+ `message_ratings`) rows.
|
|
892
|
-
*
|
|
893
|
-
* The conversations row IS the to-do marker: deleting it last means a migrated
|
|
894
|
-
* chat never reappears, while a chat that errored keeps its row and is simply
|
|
895
|
-
* retried on the next connection. No extra bookkeeping tables are needed. The
|
|
896
|
-
* v1 DO is left untouched and self-expires via v1 retention, so its messages
|
|
897
|
-
* remain as a backstop until then.
|
|
898
|
-
*/
|
|
899
|
-
async function migrateUserFromV1(deps) {
|
|
900
|
-
const { sql, db, userId, durableObjectBinding, createFacet } = deps;
|
|
901
|
-
const conversations = await listLegacyConversations(db, userId);
|
|
902
|
-
let migrated = 0;
|
|
903
|
-
let failed = 0;
|
|
904
|
-
for (const conversation of conversations) {
|
|
905
|
-
const legacyName = conversation.durable_object_name;
|
|
906
|
-
try {
|
|
907
|
-
const { messages } = await durableObjectBinding.getByName(legacyName).exportForMigration();
|
|
908
|
-
const feedback = await listLegacyFeedback(db, legacyName);
|
|
909
|
-
const newChatId = nanoid();
|
|
910
|
-
const facet = await createFacet(newChatId);
|
|
911
|
-
if (messages.length > 0) await facet.importLegacyMessages(messages, feedback);
|
|
912
|
-
registerChatWithMetadata(sql, newChatId, {
|
|
913
|
-
title: conversation.title ?? void 0,
|
|
914
|
-
summary: conversation.summary ?? void 0,
|
|
915
|
-
createdAt: toEpochMs(conversation.created_at),
|
|
916
|
-
updatedAt: toEpochMs(conversation.updated_at)
|
|
917
|
-
});
|
|
918
|
-
await deleteLegacyConversation(db, legacyName);
|
|
919
|
-
migrated++;
|
|
920
|
-
} catch (error) {
|
|
921
|
-
failed++;
|
|
922
|
-
console.error("[Migration] Failed to migrate v1 chat", {
|
|
923
|
-
legacyName,
|
|
924
|
-
error
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
return {
|
|
929
|
-
migrated,
|
|
930
|
-
failed
|
|
931
|
-
};
|
|
932
|
-
}
|
|
933
|
-
//#endregion
|
|
934
851
|
//#region src/server/agents/Assistant.ts
|
|
935
852
|
let _initProto$1;
|
|
936
853
|
function _applyDecs$1(e, t, n, r, o, i) {
|
|
@@ -1127,14 +1044,25 @@ var Assistant = class extends Agent$1 {
|
|
|
1127
1044
|
}
|
|
1128
1045
|
}
|
|
1129
1046
|
}
|
|
1130
|
-
await this.ensureMigrated();
|
|
1131
1047
|
sendConnectionStatus(connection, "connected");
|
|
1132
1048
|
}
|
|
1133
1049
|
async createChat() {
|
|
1134
1050
|
const id = nanoid();
|
|
1135
1051
|
const now = Date.now();
|
|
1136
1052
|
await this.subAgent(this.agent, id);
|
|
1137
|
-
|
|
1053
|
+
try {
|
|
1054
|
+
await this.scheduleChatForAutoDeletion(id, 1);
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
try {
|
|
1057
|
+
await this.deleteSubAgent(this.agent, id);
|
|
1058
|
+
} catch (cleanupError) {
|
|
1059
|
+
console.error("[Assistant] Failed to clean up an unscheduled chat facet", {
|
|
1060
|
+
durableObjectName: id,
|
|
1061
|
+
error: cleanupError
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
throw error;
|
|
1065
|
+
}
|
|
1138
1066
|
this.touchInstance(now);
|
|
1139
1067
|
return id;
|
|
1140
1068
|
}
|
|
@@ -1151,9 +1079,17 @@ var Assistant = class extends Agent$1 {
|
|
|
1151
1079
|
async getChatAgentClassName() {
|
|
1152
1080
|
return this.agent.name;
|
|
1153
1081
|
}
|
|
1082
|
+
async registerChat(durableObjectName) {
|
|
1083
|
+
registerChat(this.sql.bind(this), durableObjectName, Date.now());
|
|
1084
|
+
}
|
|
1154
1085
|
async recordChatTurn(durableObjectName, messages) {
|
|
1155
1086
|
summariseChatWithAI(this.sql.bind(this), durableObjectName, messages, this.fastModel);
|
|
1156
|
-
this.scheduleChatForAutoDeletion(durableObjectName)
|
|
1087
|
+
this.scheduleChatForAutoDeletion(durableObjectName).catch((error) => {
|
|
1088
|
+
console.error("[Assistant] Failed to reschedule chat deletion", {
|
|
1089
|
+
durableObjectName,
|
|
1090
|
+
error
|
|
1091
|
+
});
|
|
1092
|
+
});
|
|
1157
1093
|
}
|
|
1158
1094
|
async recordChatFeedback(durableObjectName, messageId, rating) {
|
|
1159
1095
|
recordChatFeedback(this.sql.bind(this), durableObjectName, messageId, rating);
|
|
@@ -1161,12 +1097,19 @@ var Assistant = class extends Agent$1 {
|
|
|
1161
1097
|
async [DELETE_CHAT_CALLBACK](durableObjectName) {
|
|
1162
1098
|
await this.deleteChat(durableObjectName);
|
|
1163
1099
|
}
|
|
1164
|
-
async scheduleChatForAutoDeletion(durableObjectName) {
|
|
1165
|
-
const retentionMs = getChatRetentionMs(
|
|
1100
|
+
async scheduleChatForAutoDeletion(durableObjectName, retentionDays = 90) {
|
|
1101
|
+
const retentionMs = getChatRetentionMs(retentionDays);
|
|
1166
1102
|
if (retentionMs === null) return;
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1103
|
+
let lastError;
|
|
1104
|
+
for (let attempt = 0; attempt < 3; attempt++) try {
|
|
1105
|
+
const scheduleIds = getDeleteChatScheduleIds(await this.listSchedules(), durableObjectName);
|
|
1106
|
+
await Promise.all(scheduleIds.map((scheduleId) => this.cancelSchedule(scheduleId)));
|
|
1107
|
+
await this.schedule(new Date(Date.now() + retentionMs), DELETE_CHAT_CALLBACK, durableObjectName, { idempotent: true });
|
|
1108
|
+
return;
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
lastError = error;
|
|
1111
|
+
}
|
|
1112
|
+
throw lastError;
|
|
1170
1113
|
}
|
|
1171
1114
|
/**
|
|
1172
1115
|
* Self-registers this Assistant in the global `agent_registry`. An Assistant
|
|
@@ -1205,52 +1148,6 @@ var Assistant = class extends Agent$1 {
|
|
|
1205
1148
|
console.error("[Assistant] Failed to update agent_registry activity", error);
|
|
1206
1149
|
}
|
|
1207
1150
|
}
|
|
1208
|
-
/**
|
|
1209
|
-
* Binding of the legacy v1 chat Durable Object class, used to migrate a
|
|
1210
|
-
* user's v1 chats into facets the first time they connect. Set this on the
|
|
1211
|
-
* concrete subclass to enable lazy v1 -> v2 migration; leave undefined to
|
|
1212
|
-
* disable it (e.g. for greenfield deployments with no v1 data).
|
|
1213
|
-
*/
|
|
1214
|
-
getMigrationBinding(_env) {}
|
|
1215
|
-
/** In-flight migration, shared across concurrent connections to this DO. */
|
|
1216
|
-
_migrationPromise;
|
|
1217
|
-
/**
|
|
1218
|
-
* Runs the lazy v1 -> v2 migration for this user. Concurrent connections to
|
|
1219
|
-
* this DO share a single in-flight run. Idempotency across runs/restarts is
|
|
1220
|
-
* handled by `migrateUserFromV1` deleting each chat's v1 `conversations` row,
|
|
1221
|
-
* so an already-migrated chat is never re-enumerated.
|
|
1222
|
-
*/
|
|
1223
|
-
async ensureMigrated() {
|
|
1224
|
-
if (!this.getMigrationBinding(this.env)) return;
|
|
1225
|
-
this._migrationPromise ??= this.runMigration().finally(() => {
|
|
1226
|
-
this._migrationPromise = void 0;
|
|
1227
|
-
});
|
|
1228
|
-
await this._migrationPromise;
|
|
1229
|
-
}
|
|
1230
|
-
async runMigration() {
|
|
1231
|
-
const migrationBinding = this.getMigrationBinding(this.env);
|
|
1232
|
-
if (!migrationBinding) return;
|
|
1233
|
-
try {
|
|
1234
|
-
const result = await migrateUserFromV1({
|
|
1235
|
-
sql: this.sql.bind(this),
|
|
1236
|
-
db: migrationBinding.db,
|
|
1237
|
-
userId: this.name,
|
|
1238
|
-
durableObjectBinding: migrationBinding.binding,
|
|
1239
|
-
createFacet: async (chatId) => {
|
|
1240
|
-
return await this.subAgent(this.agent, chatId);
|
|
1241
|
-
}
|
|
1242
|
-
});
|
|
1243
|
-
if (result.migrated > 0 || result.failed > 0) console.info("[Assistant] v1 -> v2 migration complete", {
|
|
1244
|
-
userId: this.name,
|
|
1245
|
-
...result
|
|
1246
|
-
});
|
|
1247
|
-
} catch (error) {
|
|
1248
|
-
console.error("[Assistant] v1 -> v2 migration failed", {
|
|
1249
|
-
userId: this.name,
|
|
1250
|
-
error
|
|
1251
|
-
});
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
1151
|
};
|
|
1255
1152
|
//#endregion
|
|
1256
1153
|
//#region src/server/features/messages.ts
|
|
@@ -1460,6 +1357,11 @@ var ChatAgent = class extends Agent {
|
|
|
1460
1357
|
configureSession(session) {
|
|
1461
1358
|
return super.configureSession(session).onCompaction(createCompactFn(this.getModel())).compactAfter(COMPACTION_TOKEN_THRESHOLD);
|
|
1462
1359
|
}
|
|
1360
|
+
async beforeTurn(ctx) {
|
|
1361
|
+
const parent = await this.getParentAgent();
|
|
1362
|
+
if (parent?.registerChat) await parent.registerChat(this.name);
|
|
1363
|
+
return super.beforeTurn(ctx);
|
|
1364
|
+
}
|
|
1463
1365
|
async onChatResponse(_result) {
|
|
1464
1366
|
const parent = await this.getParentAgent();
|
|
1465
1367
|
if (parent?.recordChatTurn) await parent.recordChatTurn(this.name, this.messages);
|
|
@@ -1496,38 +1398,6 @@ var ChatAgent = class extends Agent {
|
|
|
1496
1398
|
async getMessageFeedback() {
|
|
1497
1399
|
return getMessageFeedback(this.sql.bind(this));
|
|
1498
1400
|
}
|
|
1499
|
-
/**
|
|
1500
|
-
* Imports a v1 conversation's history into this facet's session storage.
|
|
1501
|
-
*
|
|
1502
|
-
* Called over DO RPC by the v2 `Assistant` during the lazy v1 -> v2
|
|
1503
|
-
* migration. Messages are appended in order as a single linear thread
|
|
1504
|
-
* (each message parented to the previous one) using
|
|
1505
|
-
* `appendMessageToHistory`, which writes durably to the session WITHOUT
|
|
1506
|
-
* triggering a model turn. Any carried-over feedback is then written to
|
|
1507
|
-
* `assistant_messages_feedback`.
|
|
1508
|
-
*
|
|
1509
|
-
* Safe to skip persisting if there is nothing to import.
|
|
1510
|
-
*/
|
|
1511
|
-
async importLegacyMessages(messages, feedback = []) {
|
|
1512
|
-
let parentId = null;
|
|
1513
|
-
const parent = await this.getParentAgent();
|
|
1514
|
-
for (const message of messages) {
|
|
1515
|
-
await this.appendMessageToHistory(message, parentId);
|
|
1516
|
-
parentId = message.id;
|
|
1517
|
-
}
|
|
1518
|
-
for (const item of feedback) {
|
|
1519
|
-
submitMessageFeedback(this.sql.bind(this), item.messageId, item.rating, item.comment);
|
|
1520
|
-
try {
|
|
1521
|
-
await parent?.recordChatFeedback?.(this.name, item.messageId, item.rating);
|
|
1522
|
-
} catch (error) {
|
|
1523
|
-
console.error("[ChatAgent] Failed to import parent chat feedback summary", {
|
|
1524
|
-
durableObjectName: this.name,
|
|
1525
|
-
messageId: item.messageId,
|
|
1526
|
-
error
|
|
1527
|
-
});
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
1401
|
};
|
|
1532
1402
|
//#endregion
|
|
1533
1403
|
//#region src/server/util/skills.ts
|
|
@@ -1562,4 +1432,4 @@ function getCurrentToolContext() {
|
|
|
1562
1432
|
return getCurrentAgent().agent._lastBody;
|
|
1563
1433
|
}
|
|
1564
1434
|
//#endregion
|
|
1565
|
-
export { Agent, Assistant, ChatAgent, exchangeToken, getCurrentToolContext,
|
|
1435
|
+
export { Agent, Assistant, ChatAgent, exchangeToken, getCurrentToolContext, routeAgentRequest, skill, tool };
|
package/dist/v1.d.mts
CHANGED
|
@@ -220,17 +220,6 @@ declare abstract class ChatAgent<Env extends Cloudflare.Env = Cloudflare.Env, TU
|
|
|
220
220
|
rateMessage(messageId: string, rating: number, comment?: string): Promise<void>;
|
|
221
221
|
getMessageRatings(): Promise<Record<string, MessageRating>>;
|
|
222
222
|
getConversations(): Promise<Record<string, unknown>[]>;
|
|
223
|
-
/**
|
|
224
|
-
* Exports this conversation's persisted message history for the v1 -> v2
|
|
225
|
-
* migration. Called over DO RPC by the v2 `Assistant` while migrating a
|
|
226
|
-
* user's chats into facets. `this.messages` is loaded from the
|
|
227
|
-
* `cf_ai_chat_agent_messages` table when the DO wakes.
|
|
228
|
-
*
|
|
229
|
-
* Read-only: does not mutate or delete any state.
|
|
230
|
-
*/
|
|
231
|
-
exportForMigration(): Promise<{
|
|
232
|
-
messages: UIMessage[];
|
|
233
|
-
}>;
|
|
234
223
|
deleteConversation(id: string): Promise<boolean>;
|
|
235
224
|
destroy(): Promise<void>;
|
|
236
225
|
private deleteConversationCallback;
|
package/dist/v1.mjs
CHANGED
|
@@ -977,17 +977,6 @@ var ChatAgent = class extends AIChatAgent {
|
|
|
977
977
|
async getConversations() {
|
|
978
978
|
return getConversations(this.env.AGENT_DB, this.getUserId());
|
|
979
979
|
}
|
|
980
|
-
/**
|
|
981
|
-
* Exports this conversation's persisted message history for the v1 -> v2
|
|
982
|
-
* migration. Called over DO RPC by the v2 `Assistant` while migrating a
|
|
983
|
-
* user's chats into facets. `this.messages` is loaded from the
|
|
984
|
-
* `cf_ai_chat_agent_messages` table when the DO wakes.
|
|
985
|
-
*
|
|
986
|
-
* Read-only: does not mutate or delete any state.
|
|
987
|
-
*/
|
|
988
|
-
async exportForMigration() {
|
|
989
|
-
return { messages: this.messages };
|
|
990
|
-
}
|
|
991
980
|
async deleteConversation(id) {
|
|
992
981
|
if (!id.startsWith(`${this.getUserId()}:`)) {
|
|
993
982
|
console.error("[Agent] Failed to delete conversation: Not owned by current user", {
|