@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 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, kept only for migration, and will be removed in v3.
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 three callable methods:
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[]` | The user's chats, most recently updated first. |
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. When the exchanged
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 FacetStub, type LegacyChatStub, type LegacyMessageFeedback, type MigrationDeps, type Skill, type SkillSource, type TokenExchangeConfig, type TokenExchangeResult, type Tool, type ToolContext, type ToolSet, type TurnSnapshot, exchangeToken, getCurrentToolContext, migrateUserFromV1, routeAgentRequest, skill, tool };
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
- let result;
318
- try {
319
- result = await exchangeToken(token, exchangeConfig);
320
- } catch (error) {
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. When the exchanged
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
- const token = await this._getExchangedToken();
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 === DELETE_CHAT_CALLBACK).map((schedule) => schedule.id);
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
- registerChat(this.sql.bind(this), id, now);
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(90);
1100
+ async scheduleChatForAutoDeletion(durableObjectName, retentionDays = 90) {
1101
+ const retentionMs = getChatRetentionMs(retentionDays);
1166
1102
  if (retentionMs === null) return;
1167
- const scheduleIds = getDeleteChatScheduleIds(await this.listSchedules());
1168
- await Promise.all(scheduleIds.map((scheduleId) => this.cancelSchedule(scheduleId)));
1169
- await this.schedule(new Date(Date.now() + retentionMs), DELETE_CHAT_CALLBACK, durableObjectName, { idempotent: true });
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, migrateUserFromV1, routeAgentRequest, skill, tool };
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", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@economic/agents",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "license": "MIT",
6
6
  "files": [