@economic/agents 2.1.3 → 2.1.5
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/dist/index.d.mts +17 -0
- package/dist/index.mjs +39 -0
- package/dist/v2.d.mts +97 -8
- package/dist/v2.mjs +187 -27
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -135,6 +135,10 @@ interface MessageRating {
|
|
|
135
135
|
* `@economic/agents` inside `onChatMessage`.
|
|
136
136
|
*/
|
|
137
137
|
declare abstract class ChatAgent<Env extends Cloudflare.Env = Cloudflare.Env, TUserContext extends Record<string, unknown> = Record<string, unknown>> extends AIChatAgent<Env & ChatAgentEnv> {
|
|
138
|
+
initialState: {
|
|
139
|
+
status: string;
|
|
140
|
+
type: string;
|
|
141
|
+
};
|
|
138
142
|
/**
|
|
139
143
|
* The binding of the Durable Object instance for this agent.
|
|
140
144
|
*/
|
|
@@ -196,6 +200,8 @@ declare abstract class ChatAgent<Env extends Cloudflare.Env = Cloudflare.Env, TU
|
|
|
196
200
|
* Returns the user ID from the durable object name.
|
|
197
201
|
*/
|
|
198
202
|
protected getUserId(): string;
|
|
203
|
+
onStart(): void;
|
|
204
|
+
onClose(): Promise<void>;
|
|
199
205
|
onConnect(connection: Connection, ctx: ConnectionContext): Promise<void>;
|
|
200
206
|
protected _pendingUserContextRequest?: Promise<void>;
|
|
201
207
|
/**
|
|
@@ -217,6 +223,17 @@ declare abstract class ChatAgent<Env extends Cloudflare.Env = Cloudflare.Env, TU
|
|
|
217
223
|
rateMessage(messageId: string, rating: number, comment?: string): Promise<void>;
|
|
218
224
|
getMessageRatings(): Promise<Record<string, MessageRating>>;
|
|
219
225
|
getConversations(): Promise<Record<string, unknown>[]>;
|
|
226
|
+
/**
|
|
227
|
+
* Exports this conversation's persisted message history for the v1 -> v2
|
|
228
|
+
* migration. Called over DO RPC by the v2 `Assistant` while migrating a
|
|
229
|
+
* user's chats into facets. `this.messages` is loaded from the
|
|
230
|
+
* `cf_ai_chat_agent_messages` table when the DO wakes.
|
|
231
|
+
*
|
|
232
|
+
* Read-only: does not mutate or delete any state.
|
|
233
|
+
*/
|
|
234
|
+
exportForMigration(): Promise<{
|
|
235
|
+
messages: UIMessage[];
|
|
236
|
+
}>;
|
|
220
237
|
deleteConversation(id: string): Promise<boolean>;
|
|
221
238
|
destroy(): Promise<void>;
|
|
222
239
|
private deleteConversationCallback;
|
package/dist/index.mjs
CHANGED
|
@@ -667,6 +667,10 @@ async function getMessageRatings(db, durableObjectName) {
|
|
|
667
667
|
* `@economic/agents` inside `onChatMessage`.
|
|
668
668
|
*/
|
|
669
669
|
var ChatAgent = class extends AIChatAgent {
|
|
670
|
+
initialState = {
|
|
671
|
+
status: "connecting",
|
|
672
|
+
type: "chat"
|
|
673
|
+
};
|
|
670
674
|
/**
|
|
671
675
|
* Number of days of inactivity before the full conversation is deleted.
|
|
672
676
|
*
|
|
@@ -697,6 +701,18 @@ var ChatAgent = class extends AIChatAgent {
|
|
|
697
701
|
getUserId() {
|
|
698
702
|
return this.name.split(":")[0];
|
|
699
703
|
}
|
|
704
|
+
onStart() {
|
|
705
|
+
this.setState({
|
|
706
|
+
...this.initialState,
|
|
707
|
+
status: "connecting"
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
async onClose() {
|
|
711
|
+
this.setState({
|
|
712
|
+
type: "chat",
|
|
713
|
+
status: "disconnected"
|
|
714
|
+
});
|
|
715
|
+
}
|
|
700
716
|
async onConnect(connection, ctx) {
|
|
701
717
|
this.clientIp = ctx.request.headers.get("CF-Connecting-IP") ?? ctx.request.headers.get("X-Forwarded-For")?.split(",")[0]?.trim();
|
|
702
718
|
this.forwardedFor = ctx.request.headers.get("X-Forwarded-For") ?? void 0;
|
|
@@ -725,10 +741,18 @@ var ChatAgent = class extends AIChatAgent {
|
|
|
725
741
|
} catch (error) {
|
|
726
742
|
console.error("[Agent] JWT verification error", error);
|
|
727
743
|
connection.close(4001, "Unauthorized");
|
|
744
|
+
this.setState({
|
|
745
|
+
type: "chat",
|
|
746
|
+
status: "unauthorized"
|
|
747
|
+
});
|
|
728
748
|
return;
|
|
729
749
|
}
|
|
730
750
|
if (!result.success) {
|
|
731
751
|
connection.close(result.status === 401 ? 4001 : 4003, result.message);
|
|
752
|
+
this.setState({
|
|
753
|
+
type: "chat",
|
|
754
|
+
status: "unauthorized"
|
|
755
|
+
});
|
|
732
756
|
return;
|
|
733
757
|
}
|
|
734
758
|
}
|
|
@@ -737,6 +761,10 @@ var ChatAgent = class extends AIChatAgent {
|
|
|
737
761
|
if (userContext) connection.setState({ userContext });
|
|
738
762
|
});
|
|
739
763
|
}
|
|
764
|
+
this.setState({
|
|
765
|
+
type: "chat",
|
|
766
|
+
status: "connected"
|
|
767
|
+
});
|
|
740
768
|
return super.onConnect(connection, ctx);
|
|
741
769
|
}
|
|
742
770
|
_pendingUserContextRequest;
|
|
@@ -809,6 +837,17 @@ var ChatAgent = class extends AIChatAgent {
|
|
|
809
837
|
@callable({ description: "Returns all conversations for the current user" }) async getConversations() {
|
|
810
838
|
return getConversations(this.env.AGENT_DB, this.getUserId());
|
|
811
839
|
}
|
|
840
|
+
/**
|
|
841
|
+
* Exports this conversation's persisted message history for the v1 -> v2
|
|
842
|
+
* migration. Called over DO RPC by the v2 `Assistant` while migrating a
|
|
843
|
+
* user's chats into facets. `this.messages` is loaded from the
|
|
844
|
+
* `cf_ai_chat_agent_messages` table when the DO wakes.
|
|
845
|
+
*
|
|
846
|
+
* Read-only: does not mutate or delete any state.
|
|
847
|
+
*/
|
|
848
|
+
async exportForMigration() {
|
|
849
|
+
return { messages: this.messages };
|
|
850
|
+
}
|
|
812
851
|
@callable({ description: "Delete a conversation by its id" }) async deleteConversation(id) {
|
|
813
852
|
if (!id.startsWith(`${this.getUserId()}:`)) {
|
|
814
853
|
console.error("[Agent] Failed to delete conversation: Not owned by current user", {
|
package/dist/v2.d.mts
CHANGED
|
@@ -2,7 +2,6 @@ import { t as JwtAuthConfig } from "./index-DzOC3mNl.mjs";
|
|
|
2
2
|
import { JSONValue, LanguageModel, Tool as Tool$1, UIMessage } from "ai";
|
|
3
3
|
import { Agent as Agent$1, Connection, ConnectionContext, SubAgentClass } from "agents";
|
|
4
4
|
import { ChatResponseResult, Session, StepConfig, Think, ToolCallContext, ToolCallDecision, TurnConfig, TurnContext } from "@cloudflare/think";
|
|
5
|
-
|
|
6
5
|
//#region src/server/v2/types.d.ts
|
|
7
6
|
/**
|
|
8
7
|
* The context object available throughout an agent's lifetime — passed via
|
|
@@ -19,6 +18,7 @@ interface AgentEnv {
|
|
|
19
18
|
AGENTS_ANALYTICS: AnalyticsEngineDataset;
|
|
20
19
|
SKILLS_BUCKET: R2Bucket;
|
|
21
20
|
}
|
|
21
|
+
type SqlFn = InstanceType<typeof Agent$1>["sql"];
|
|
22
22
|
type AgentConnectionStatus = "connecting" | "connected" | "disconnected" | "unauthorized";
|
|
23
23
|
type AgentConnectionType = "agent" | "chat" | "assistant";
|
|
24
24
|
type AgentConnectionState = {
|
|
@@ -118,6 +118,65 @@ declare abstract class Agent<RequestContext extends Record<string, unknown> = Re
|
|
|
118
118
|
protected getUserContext?(jwtToken: string): Promise<UserContext>;
|
|
119
119
|
}
|
|
120
120
|
//#endregion
|
|
121
|
+
//#region src/server/v2/features/messages.d.ts
|
|
122
|
+
type MessageFeedback = {
|
|
123
|
+
message_id: string;
|
|
124
|
+
rating: number;
|
|
125
|
+
comment?: string;
|
|
126
|
+
created_at: number;
|
|
127
|
+
updated_at: number;
|
|
128
|
+
};
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/server/v2/features/migration.d.ts
|
|
131
|
+
/** Per-message feedback carried over from a v1 conversation during migration. */
|
|
132
|
+
type LegacyMessageFeedback = {
|
|
133
|
+
messageId: string;
|
|
134
|
+
rating: number;
|
|
135
|
+
comment?: string;
|
|
136
|
+
};
|
|
137
|
+
/** Minimal RPC surface of a v1 chat DO needed to read its history. */
|
|
138
|
+
interface LegacyChatStub {
|
|
139
|
+
exportForMigration(): Promise<{
|
|
140
|
+
messages: UIMessage[];
|
|
141
|
+
}>;
|
|
142
|
+
}
|
|
143
|
+
/** Minimal RPC surface of a freshly created v2 chat facet needed to seed history. */
|
|
144
|
+
interface FacetStub {
|
|
145
|
+
importLegacyMessages(messages: UIMessage[], feedback: LegacyMessageFeedback[]): Promise<void>;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Dependencies for {@link migrateUserFromV1}, injected by the `Assistant` so
|
|
149
|
+
* this module never needs to touch the DO's protected members directly.
|
|
150
|
+
*/
|
|
151
|
+
type MigrationDeps = {
|
|
152
|
+
/** The `Assistant` DO's bound `sql` tag (used to register migrated chats). */sql: SqlFn; /** Shared D1 database holding v1 `conversations` and `message_ratings`. */
|
|
153
|
+
db: D1Database; /** The user being migrated (the `Assistant` DO name). */
|
|
154
|
+
userId: string; /** Resolves a v1 chat DO stub by its `userId:chatId` name. */
|
|
155
|
+
legacyNamespace: {
|
|
156
|
+
getByName(name: string): LegacyChatStub;
|
|
157
|
+
}; /** Creates (or resolves) a v2 chat facet for the given chat id and returns its stub. */
|
|
158
|
+
createFacet: (chatId: string) => Promise<FacetStub>;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Migrates all of a user's v1 chats into v2 facets, lazily and idempotently.
|
|
162
|
+
*
|
|
163
|
+
* v1 chats are enumerated from the shared D1 `conversations` table (DOs cannot
|
|
164
|
+
* be listed directly). For each chat we read its persisted messages from the v1
|
|
165
|
+
* DO over RPC, create a fresh facet, seed its history and feedback, register it
|
|
166
|
+
* on the parent `Assistant` with its original title/summary/timestamps, and
|
|
167
|
+
* finally delete the v1 `conversations` (+ `message_ratings`) rows.
|
|
168
|
+
*
|
|
169
|
+
* The conversations row IS the to-do marker: deleting it last means a migrated
|
|
170
|
+
* chat never reappears, while a chat that errored keeps its row and is simply
|
|
171
|
+
* retried on the next connection. No extra bookkeeping tables are needed. The
|
|
172
|
+
* v1 DO is left untouched and self-expires via v1 retention, so its messages
|
|
173
|
+
* remain as a backstop until then.
|
|
174
|
+
*/
|
|
175
|
+
declare function migrateUserFromV1(deps: MigrationDeps): Promise<{
|
|
176
|
+
migrated: number;
|
|
177
|
+
failed: number;
|
|
178
|
+
}>;
|
|
179
|
+
//#endregion
|
|
121
180
|
//#region src/server/v2/agents/ChatAgent.d.ts
|
|
122
181
|
declare abstract class ChatAgent<RequestContext extends Record<string, unknown> = Record<string, unknown>, UserContext extends Record<string, unknown> = Record<string, unknown>> extends Agent<RequestContext, UserContext> {
|
|
123
182
|
initialState: AgentConnectionState;
|
|
@@ -125,17 +184,30 @@ declare abstract class ChatAgent<RequestContext extends Record<string, unknown>
|
|
|
125
184
|
configureSession(session: Session): Session;
|
|
126
185
|
onChatResponse(_result: ChatResponseResult): Promise<void>;
|
|
127
186
|
/**
|
|
128
|
-
*
|
|
129
|
-
* @param messageId - The id of the message to
|
|
187
|
+
* Submit feedback for a message by its id.
|
|
188
|
+
* @param messageId - The id of the message to give feedback on.
|
|
130
189
|
* @param rating - The rating to give the message. 1 = thumbs up, -1 = thumbs down.
|
|
131
190
|
* @returns The message id and the rating.
|
|
132
191
|
*/
|
|
133
|
-
|
|
192
|
+
submitMessageFeedback(messageId: string, rating: number, comment?: string): Promise<void>;
|
|
134
193
|
/**
|
|
135
|
-
* Returns all message
|
|
136
|
-
* @returns All message
|
|
194
|
+
* Returns all message feedback for the current chat.
|
|
195
|
+
* @returns All message feedback for the current chat.
|
|
196
|
+
*/
|
|
197
|
+
getMessageFeedback(): Promise<Record<string, MessageFeedback>>;
|
|
198
|
+
/**
|
|
199
|
+
* Imports a v1 conversation's history into this facet's session storage.
|
|
200
|
+
*
|
|
201
|
+
* Called over DO RPC by the v2 `Assistant` during the lazy v1 -> v2
|
|
202
|
+
* migration. Messages are appended in order as a single linear thread
|
|
203
|
+
* (each message parented to the previous one) using
|
|
204
|
+
* `appendMessageToHistory`, which writes durably to the session WITHOUT
|
|
205
|
+
* triggering a model turn. Any carried-over feedback is then written to
|
|
206
|
+
* `assistant_messages_feedback`.
|
|
207
|
+
*
|
|
208
|
+
* Safe to skip persisting if there is nothing to import.
|
|
137
209
|
*/
|
|
138
|
-
|
|
210
|
+
importLegacyMessages(messages: UIMessage[], feedback?: LegacyMessageFeedback[]): Promise<void>;
|
|
139
211
|
}
|
|
140
212
|
//#endregion
|
|
141
213
|
//#region src/server/v2/features/chats.d.ts
|
|
@@ -153,9 +225,26 @@ declare abstract class Assistant extends Agent$1<Cloudflare.Env, AgentConnection
|
|
|
153
225
|
initialState: AgentConnectionState;
|
|
154
226
|
protected abstract agent: SubAgentClass<ChatAgent>;
|
|
155
227
|
protected abstract fastModel: LanguageModel;
|
|
228
|
+
/**
|
|
229
|
+
* Binding name of the legacy v1 chat Durable Object class, used to migrate a
|
|
230
|
+
* user's v1 chats into facets the first time they connect. Set this on the
|
|
231
|
+
* concrete subclass to enable lazy v1 -> v2 migration; leave undefined to
|
|
232
|
+
* disable it (e.g. for greenfield deployments with no v1 data).
|
|
233
|
+
*/
|
|
234
|
+
protected legacyBinding?: keyof Cloudflare.Env;
|
|
235
|
+
/** In-flight migration, shared across concurrent connections to this DO. */
|
|
236
|
+
private _migrationPromise?;
|
|
156
237
|
onStart(): void;
|
|
157
238
|
onClose(): Promise<void>;
|
|
158
239
|
onConnect(connection: Connection, ctx: ConnectionContext): Promise<void>;
|
|
240
|
+
/**
|
|
241
|
+
* Runs the lazy v1 -> v2 migration for this user. Concurrent connections to
|
|
242
|
+
* this DO share a single in-flight run. Idempotency across runs/restarts is
|
|
243
|
+
* handled by `migrateUserFromV1` deleting each chat's v1 `conversations` row,
|
|
244
|
+
* so an already-migrated chat is never re-enumerated.
|
|
245
|
+
*/
|
|
246
|
+
private ensureMigrated;
|
|
247
|
+
private runMigration;
|
|
159
248
|
createChat(): Promise<string>;
|
|
160
249
|
deleteChat(id: string): Promise<void>;
|
|
161
250
|
getChats(): Promise<Chat[]>;
|
|
@@ -164,4 +253,4 @@ declare abstract class Assistant extends Agent$1<Cloudflare.Env, AgentConnection
|
|
|
164
253
|
private scheduleChatForAutoDeletion;
|
|
165
254
|
}
|
|
166
255
|
//#endregion
|
|
167
|
-
export { Agent, type AgentConnectionState, type AgentConnectionStatus, type AgentConnectionType, type AgentEnv, Assistant, ChatAgent, type Skill, type Tool, type ToolContext, type ToolSet, getCurrentToolContext, skill, tool };
|
|
256
|
+
export { Agent, type AgentConnectionState, type AgentConnectionStatus, type AgentConnectionType, type AgentEnv, Assistant, ChatAgent, type FacetStub, type LegacyChatStub, type LegacyMessageFeedback, type MigrationDeps, type Skill, type Tool, type ToolContext, type ToolSet, getCurrentToolContext, migrateUserFromV1, skill, tool };
|
package/dist/v2.mjs
CHANGED
|
@@ -297,6 +297,21 @@ function registerChat(sql, durableObjectName, dateTime) {
|
|
|
297
297
|
sql`INSERT INTO chats (durable_object_name, created_at, updated_at)
|
|
298
298
|
VALUES (${durableObjectName}, ${dateTime}, ${dateTime})`;
|
|
299
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Registers a chat while preserving metadata carried over from a v1
|
|
302
|
+
* conversation during migration: its title, summary, and original
|
|
303
|
+
* timestamps. Used so migrated chats keep their titles instead of waiting
|
|
304
|
+
* for the AI summariser to regenerate them.
|
|
305
|
+
*
|
|
306
|
+
* Idempotent: an existing row for the same chat id is left untouched.
|
|
307
|
+
*/
|
|
308
|
+
function registerChatWithMetadata(sql, durableObjectName, metadata) {
|
|
309
|
+
const { title, summary, createdAt } = metadata;
|
|
310
|
+
const updatedAt = metadata.updatedAt ?? createdAt;
|
|
311
|
+
sql`INSERT INTO chats (durable_object_name, title, summary, created_at, updated_at)
|
|
312
|
+
VALUES (${durableObjectName}, ${title ?? null}, ${summary ?? null}, ${createdAt}, ${updatedAt})
|
|
313
|
+
ON CONFLICT (durable_object_name) DO NOTHING`;
|
|
314
|
+
}
|
|
300
315
|
function deleteChat(sql, durableObjectName) {
|
|
301
316
|
sql`DELETE FROM chats WHERE durable_object_name = ${durableObjectName}`;
|
|
302
317
|
}
|
|
@@ -368,12 +383,99 @@ function getChatRetentionMs(days) {
|
|
|
368
383
|
return Math.floor(days * 24 * 60 * 60 * 1e3);
|
|
369
384
|
}
|
|
370
385
|
//#endregion
|
|
386
|
+
//#region src/server/v2/features/migration.ts
|
|
387
|
+
async function listLegacyConversations(db, userId) {
|
|
388
|
+
const { results } = await db.prepare(`SELECT durable_object_name, title, summary, created_at, updated_at
|
|
389
|
+
FROM conversations WHERE durable_object_name LIKE ? ORDER BY updated_at ASC`).bind(`${userId}:%`).all();
|
|
390
|
+
return results ?? [];
|
|
391
|
+
}
|
|
392
|
+
async function listLegacyFeedback(db, durableObjectName) {
|
|
393
|
+
const { results } = await db.prepare(`SELECT message_id, rating, comment FROM message_ratings WHERE durable_object_name = ?`).bind(durableObjectName).all();
|
|
394
|
+
return (results ?? []).map((row) => ({
|
|
395
|
+
messageId: row.message_id,
|
|
396
|
+
rating: row.rating,
|
|
397
|
+
...row.comment != null ? { comment: row.comment } : {}
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Removes a migrated v1 chat's D1 rows. Deleting the `conversations` row is what
|
|
402
|
+
* makes the migration idempotent: it drops out of the enumeration so it is never
|
|
403
|
+
* migrated again. The v1 DO itself is left to self-expire via v1 retention.
|
|
404
|
+
*/
|
|
405
|
+
async function deleteLegacyConversation(db, durableObjectName) {
|
|
406
|
+
await db.prepare(`DELETE FROM conversations WHERE durable_object_name = ?`).bind(durableObjectName).run();
|
|
407
|
+
await db.prepare(`DELETE FROM message_ratings WHERE durable_object_name = ?`).bind(durableObjectName).run();
|
|
408
|
+
}
|
|
409
|
+
function toEpochMs(value) {
|
|
410
|
+
if (!value) return Date.now();
|
|
411
|
+
const parsed = Date.parse(value);
|
|
412
|
+
return Number.isNaN(parsed) ? Date.now() : parsed;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Migrates all of a user's v1 chats into v2 facets, lazily and idempotently.
|
|
416
|
+
*
|
|
417
|
+
* v1 chats are enumerated from the shared D1 `conversations` table (DOs cannot
|
|
418
|
+
* be listed directly). For each chat we read its persisted messages from the v1
|
|
419
|
+
* DO over RPC, create a fresh facet, seed its history and feedback, register it
|
|
420
|
+
* on the parent `Assistant` with its original title/summary/timestamps, and
|
|
421
|
+
* finally delete the v1 `conversations` (+ `message_ratings`) rows.
|
|
422
|
+
*
|
|
423
|
+
* The conversations row IS the to-do marker: deleting it last means a migrated
|
|
424
|
+
* chat never reappears, while a chat that errored keeps its row and is simply
|
|
425
|
+
* retried on the next connection. No extra bookkeeping tables are needed. The
|
|
426
|
+
* v1 DO is left untouched and self-expires via v1 retention, so its messages
|
|
427
|
+
* remain as a backstop until then.
|
|
428
|
+
*/
|
|
429
|
+
async function migrateUserFromV1(deps) {
|
|
430
|
+
const { sql, db, userId, legacyNamespace, createFacet } = deps;
|
|
431
|
+
const conversations = await listLegacyConversations(db, userId);
|
|
432
|
+
let migrated = 0;
|
|
433
|
+
let failed = 0;
|
|
434
|
+
for (const conversation of conversations) {
|
|
435
|
+
const legacyName = conversation.durable_object_name;
|
|
436
|
+
try {
|
|
437
|
+
const { messages } = await legacyNamespace.getByName(legacyName).exportForMigration();
|
|
438
|
+
const feedback = await listLegacyFeedback(db, legacyName);
|
|
439
|
+
const newChatId = nanoid();
|
|
440
|
+
const facet = await createFacet(newChatId);
|
|
441
|
+
if (messages.length > 0) await facet.importLegacyMessages(messages, feedback);
|
|
442
|
+
registerChatWithMetadata(sql, newChatId, {
|
|
443
|
+
title: conversation.title ?? void 0,
|
|
444
|
+
summary: conversation.summary ?? void 0,
|
|
445
|
+
createdAt: toEpochMs(conversation.created_at),
|
|
446
|
+
updatedAt: toEpochMs(conversation.updated_at)
|
|
447
|
+
});
|
|
448
|
+
await deleteLegacyConversation(db, legacyName);
|
|
449
|
+
migrated++;
|
|
450
|
+
} catch (error) {
|
|
451
|
+
failed++;
|
|
452
|
+
console.error("[Migration] Failed to migrate v1 chat", {
|
|
453
|
+
legacyName,
|
|
454
|
+
error
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
migrated,
|
|
460
|
+
failed
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
//#endregion
|
|
371
464
|
//#region src/server/v2/agents/Assistant.ts
|
|
372
465
|
var Assistant = class extends Agent$1 {
|
|
373
466
|
initialState = {
|
|
374
467
|
status: "connecting",
|
|
375
468
|
type: "assistant"
|
|
376
469
|
};
|
|
470
|
+
/**
|
|
471
|
+
* Binding name of the legacy v1 chat Durable Object class, used to migrate a
|
|
472
|
+
* user's v1 chats into facets the first time they connect. Set this on the
|
|
473
|
+
* concrete subclass to enable lazy v1 -> v2 migration; leave undefined to
|
|
474
|
+
* disable it (e.g. for greenfield deployments with no v1 data).
|
|
475
|
+
*/
|
|
476
|
+
legacyBinding;
|
|
477
|
+
/** In-flight migration, shared across concurrent connections to this DO. */
|
|
478
|
+
_migrationPromise;
|
|
377
479
|
onStart() {
|
|
378
480
|
this.setState({
|
|
379
481
|
...this.initialState,
|
|
@@ -423,12 +525,53 @@ var Assistant = class extends Agent$1 {
|
|
|
423
525
|
});
|
|
424
526
|
}
|
|
425
527
|
}
|
|
528
|
+
await this.ensureMigrated();
|
|
426
529
|
this.setState({
|
|
427
530
|
...this.initialState,
|
|
428
531
|
status: "connected",
|
|
429
532
|
subAgentName: this.agent.name
|
|
430
533
|
});
|
|
431
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* Runs the lazy v1 -> v2 migration for this user. Concurrent connections to
|
|
537
|
+
* this DO share a single in-flight run. Idempotency across runs/restarts is
|
|
538
|
+
* handled by `migrateUserFromV1` deleting each chat's v1 `conversations` row,
|
|
539
|
+
* so an already-migrated chat is never re-enumerated.
|
|
540
|
+
*/
|
|
541
|
+
async ensureMigrated() {
|
|
542
|
+
if (!this.legacyBinding) return;
|
|
543
|
+
this._migrationPromise ??= this.runMigration().finally(() => {
|
|
544
|
+
this._migrationPromise = void 0;
|
|
545
|
+
});
|
|
546
|
+
await this._migrationPromise;
|
|
547
|
+
}
|
|
548
|
+
async runMigration() {
|
|
549
|
+
const legacyNamespace = this.env[this.legacyBinding];
|
|
550
|
+
if (!legacyNamespace?.getByName) {
|
|
551
|
+
console.error("[Assistant] Migration skipped: legacy binding not found", { legacyBinding: this.legacyBinding });
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
const result = await migrateUserFromV1({
|
|
556
|
+
sql: this.sql.bind(this),
|
|
557
|
+
db: this.env.AGENT_DB,
|
|
558
|
+
userId: this.name,
|
|
559
|
+
legacyNamespace,
|
|
560
|
+
createFacet: async (chatId) => {
|
|
561
|
+
return await this.subAgent(this.agent, chatId);
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
if (result.migrated > 0 || result.failed > 0) console.info("[Assistant] v1 -> v2 migration complete", {
|
|
565
|
+
userId: this.name,
|
|
566
|
+
...result
|
|
567
|
+
});
|
|
568
|
+
} catch (error) {
|
|
569
|
+
console.error("[Assistant] v1 -> v2 migration failed", {
|
|
570
|
+
userId: this.name,
|
|
571
|
+
error
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
432
575
|
@callable() async createChat() {
|
|
433
576
|
const id = nanoid();
|
|
434
577
|
const now = Date.now();
|
|
@@ -466,12 +609,12 @@ const createCompactFn = (model) => createCompactFunction({ summarize: (prompt) =
|
|
|
466
609
|
prompt
|
|
467
610
|
}).then((r) => r.text) });
|
|
468
611
|
/**
|
|
469
|
-
* Ensures that the
|
|
612
|
+
* Ensures that the feedback table exists.
|
|
470
613
|
* @param sql - The SQL function to use to execute the query.
|
|
471
614
|
*/
|
|
472
|
-
function
|
|
615
|
+
function ensureFeedbackTableExists(sql) {
|
|
473
616
|
try {
|
|
474
|
-
sql`CREATE TABLE IF NOT EXISTS
|
|
617
|
+
sql`CREATE TABLE IF NOT EXISTS assistant_messages_feedback (
|
|
475
618
|
message_id TEXT NOT NULL,
|
|
476
619
|
rating INTEGER,
|
|
477
620
|
comment TEXT,
|
|
@@ -480,40 +623,37 @@ function ensureRatingsTableExists(sql) {
|
|
|
480
623
|
PRIMARY KEY (message_id)
|
|
481
624
|
)`;
|
|
482
625
|
} catch (error) {
|
|
483
|
-
console.error("[Agent] Failed to create
|
|
626
|
+
console.error("[Agent] Failed to create feedback table", error);
|
|
484
627
|
}
|
|
485
628
|
}
|
|
486
629
|
/**
|
|
487
|
-
*
|
|
630
|
+
* Submits feedback for a message.
|
|
488
631
|
* @param sql - The SQL function to use to execute the query.
|
|
489
|
-
* @param messageId - The ID of the message to
|
|
632
|
+
* @param messageId - The ID of the message to give feedback on.
|
|
490
633
|
* @param rating - The rating to give the message.
|
|
491
634
|
* @param now - The date and time to use for the created_at and updated_at columns.
|
|
492
635
|
*/
|
|
493
|
-
function
|
|
636
|
+
function submitMessageFeedback(sql, messageId, rating, comment, now = /* @__PURE__ */ new Date()) {
|
|
494
637
|
try {
|
|
495
|
-
sql`INSERT INTO
|
|
638
|
+
sql`INSERT INTO assistant_messages_feedback (message_id, rating, comment, created_at, updated_at)
|
|
496
639
|
VALUES (${messageId}, ${rating}, ${comment ?? null}, ${now.getTime()}, ${now.getTime()})
|
|
497
640
|
ON CONFLICT (message_id) DO UPDATE SET
|
|
498
641
|
rating = excluded.rating,
|
|
499
642
|
comment = excluded.comment,
|
|
500
643
|
updated_at = excluded.updated_at`;
|
|
501
644
|
} catch (error) {
|
|
502
|
-
console.error("[Agent] Failed to
|
|
645
|
+
console.error("[Agent] Failed to submit message feedback", error);
|
|
503
646
|
}
|
|
504
647
|
}
|
|
505
648
|
/**
|
|
506
|
-
* Gets the
|
|
649
|
+
* Gets the feedback for all messages.
|
|
507
650
|
* @param sql - The SQL function to use to execute the query.
|
|
508
|
-
* @returns A
|
|
651
|
+
* @returns A dictionary of message feedback keyed by message id.
|
|
509
652
|
*/
|
|
510
|
-
function
|
|
653
|
+
function getMessageFeedback(sql) {
|
|
511
654
|
try {
|
|
512
|
-
const
|
|
513
|
-
return Object.fromEntries(
|
|
514
|
-
rating: row.rating,
|
|
515
|
-
comment: row.comment
|
|
516
|
-
}]));
|
|
655
|
+
const feedback = sql`SELECT message_id, rating, comment, created_at, updated_at FROM assistant_messages_feedback`;
|
|
656
|
+
return Object.fromEntries(feedback.map((row) => [row.message_id, row]));
|
|
517
657
|
} catch {
|
|
518
658
|
return {};
|
|
519
659
|
}
|
|
@@ -527,7 +667,7 @@ var ChatAgent = class extends Agent {
|
|
|
527
667
|
};
|
|
528
668
|
async onStart() {
|
|
529
669
|
await super.onStart();
|
|
530
|
-
|
|
670
|
+
ensureFeedbackTableExists(this.sql.bind(this));
|
|
531
671
|
}
|
|
532
672
|
configureSession(session) {
|
|
533
673
|
return super.configureSession(session).onCompaction(createCompactFn(this.getModel())).compactAfter(COMPACTION_TOKEN_THRESHOLD);
|
|
@@ -537,20 +677,40 @@ var ChatAgent = class extends Agent {
|
|
|
537
677
|
if (parent?.recordChatTurn) await parent.recordChatTurn(this.name, this.messages);
|
|
538
678
|
}
|
|
539
679
|
/**
|
|
540
|
-
*
|
|
541
|
-
* @param messageId - The id of the message to
|
|
680
|
+
* Submit feedback for a message by its id.
|
|
681
|
+
* @param messageId - The id of the message to give feedback on.
|
|
542
682
|
* @param rating - The rating to give the message. 1 = thumbs up, -1 = thumbs down.
|
|
543
683
|
* @returns The message id and the rating.
|
|
544
684
|
*/
|
|
545
|
-
@callable({ description: "
|
|
546
|
-
return
|
|
685
|
+
@callable({ description: "Submit feedback for a message by its id" }) async submitMessageFeedback(messageId, rating, comment) {
|
|
686
|
+
return submitMessageFeedback(this.sql.bind(this), messageId, rating, comment);
|
|
547
687
|
}
|
|
548
688
|
/**
|
|
549
|
-
* Returns all message
|
|
550
|
-
* @returns All message
|
|
689
|
+
* Returns all message feedback for the current chat.
|
|
690
|
+
* @returns All message feedback for the current chat.
|
|
551
691
|
*/
|
|
552
|
-
@callable({ description: "Returns all message
|
|
553
|
-
return
|
|
692
|
+
@callable({ description: "Returns all message feedback for the current chat" }) async getMessageFeedback() {
|
|
693
|
+
return getMessageFeedback(this.sql.bind(this));
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Imports a v1 conversation's history into this facet's session storage.
|
|
697
|
+
*
|
|
698
|
+
* Called over DO RPC by the v2 `Assistant` during the lazy v1 -> v2
|
|
699
|
+
* migration. Messages are appended in order as a single linear thread
|
|
700
|
+
* (each message parented to the previous one) using
|
|
701
|
+
* `appendMessageToHistory`, which writes durably to the session WITHOUT
|
|
702
|
+
* triggering a model turn. Any carried-over feedback is then written to
|
|
703
|
+
* `assistant_messages_feedback`.
|
|
704
|
+
*
|
|
705
|
+
* Safe to skip persisting if there is nothing to import.
|
|
706
|
+
*/
|
|
707
|
+
async importLegacyMessages(messages, feedback = []) {
|
|
708
|
+
let parentId = null;
|
|
709
|
+
for (const message of messages) {
|
|
710
|
+
await this.appendMessageToHistory(message, parentId);
|
|
711
|
+
parentId = message.id;
|
|
712
|
+
}
|
|
713
|
+
for (const item of feedback) submitMessageFeedback(this.sql.bind(this), item.messageId, item.rating, item.comment);
|
|
554
714
|
}
|
|
555
715
|
};
|
|
556
716
|
//#endregion
|
|
@@ -562,4 +722,4 @@ function skill(definition) {
|
|
|
562
722
|
return definition;
|
|
563
723
|
}
|
|
564
724
|
//#endregion
|
|
565
|
-
export { Agent, Assistant, ChatAgent, getCurrentToolContext, skill, tool };
|
|
725
|
+
export { Agent, Assistant, ChatAgent, getCurrentToolContext, migrateUserFromV1, skill, tool };
|