@economic/agents 2.9.1 → 2.10.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/dist/v1.mjs DELETED
@@ -1,1078 +0,0 @@
1
- import { a as extractTokenFromConnectRequest, c as verifyJwt, n as createAgentTracer, s as isJwtActorAuthorized, t as routeAgentRequest } from "./route-agent-request-J98VlcOj.mjs";
2
- import { Agent as Agent$1, callable, getCurrentAgent } from "agents";
3
- import { Output, convertToModelMessages, generateText, jsonSchema, stepCountIs, streamText, tool } from "ai";
4
- import { AIChatAgent } from "@cloudflare/ai-chat";
5
- //#region src/server/v1/features/skills/index.ts
6
- const TOOL_NAME_ACTIVATE_SKILL$1 = "activate_skill";
7
- const TOOL_NAME_LIST_CAPABILITIES$1 = "list_capabilities";
8
- const SKILL_STATE_SENTINEL = "\n__SKILLS_STATE__:";
9
- function buildActivateSkillDescription(skills) {
10
- return [
11
- "Load additional skills to help with the user's request.",
12
- "Call this BEFORE attempting actions that need tools from unloaded skills.",
13
- "",
14
- "Available skills:",
15
- skills.map((s) => `• ${s.name} — ${s.description}`).join("\n")
16
- ].join("\n");
17
- }
18
- function buildAvailableSkillList(skills) {
19
- return skills.map((s) => `**${s.name}**: ${s.description}`).join("\n");
20
- }
21
- const LIST_CAPABILITIES_DESCRIPTION = "List all tools currently available to you, which skills are loaded, and which can still be loaded. Call this when the user asks about your capabilities or what you can do.";
22
- /**
23
- * Creates a skill loading system for use with the Vercel AI SDK.
24
- *
25
- * The agent starts with only its always-on tools active. The LLM can call
26
- * activate_skill to load skill tools on demand. Which skills are loaded is
27
- * communicated to the CF layer via a sentinel in the activate_skill result
28
- * and persisted to DO SQLite — no D1 or message-history parsing required.
29
- */
30
- function createSkills(config) {
31
- const { tools: alwaysOnTools, skills } = config;
32
- const loadedSkills = new Set(config.initialLoadedSkills ?? []);
33
- const skillMap = new Map(skills.map((s) => [s.name, s]));
34
- const availableSkillList = skills.length > 0 ? buildAvailableSkillList(skills) : "";
35
- const allTools = {};
36
- Object.assign(allTools, alwaysOnTools);
37
- for (const skill of skills) Object.assign(allTools, skill.tools);
38
- function getActiveToolNames() {
39
- const names = [
40
- TOOL_NAME_ACTIVATE_SKILL$1,
41
- TOOL_NAME_LIST_CAPABILITIES$1,
42
- ...Object.keys(alwaysOnTools)
43
- ];
44
- for (const skillName of loadedSkills) {
45
- const skill = skillMap.get(skillName);
46
- if (!skill) continue;
47
- for (const toolName of Object.keys(skill.tools)) if (!names.includes(toolName)) names.push(toolName);
48
- }
49
- return names;
50
- }
51
- function getLoadedGuidance() {
52
- const sections = [...loadedSkills].flatMap((name) => {
53
- const skill = skillMap.get(name);
54
- if (!skill?.guidance) return [];
55
- return [`**${skill.name}**\n${skill.guidance}`];
56
- });
57
- if (sections.length === 0) return "";
58
- return sections.join("\n\n");
59
- }
60
- allTools[TOOL_NAME_ACTIVATE_SKILL$1] = tool({
61
- description: buildActivateSkillDescription(skills),
62
- inputSchema: jsonSchema({
63
- type: "object",
64
- properties: { skills: {
65
- type: "array",
66
- items: {
67
- type: "string",
68
- enum: skills.map((s) => s.name)
69
- },
70
- description: "Skills to load"
71
- } },
72
- required: ["skills"]
73
- }),
74
- execute: async ({ skills: requested }) => {
75
- const newlyLoaded = [];
76
- for (const skillName of requested) {
77
- if (!skillMap.get(skillName)) continue;
78
- if (loadedSkills.has(skillName)) continue;
79
- loadedSkills.add(skillName);
80
- newlyLoaded.push(skillName);
81
- }
82
- if (newlyLoaded.length > 0) return `Loaded: ${newlyLoaded.join(", ")}.${SKILL_STATE_SENTINEL}${JSON.stringify([...loadedSkills])}`;
83
- return "All requested skills were already loaded.";
84
- }
85
- });
86
- allTools[TOOL_NAME_LIST_CAPABILITIES$1] = tool({
87
- description: LIST_CAPABILITIES_DESCRIPTION,
88
- inputSchema: jsonSchema({
89
- type: "object",
90
- properties: {},
91
- required: []
92
- }),
93
- execute: async () => {
94
- const activeNames = getActiveToolNames();
95
- const loadedNames = [...loadedSkills];
96
- const unloaded = skills.filter((s) => !loadedSkills.has(s.name)).map((s) => s.name);
97
- return [
98
- `Active tools (${activeNames.length}): ${activeNames.join(", ")}`,
99
- `Loaded skills: ${loadedNames.length > 0 ? loadedNames.join(", ") : "none"}`,
100
- `Available to load: ${unloaded.length > 0 ? unloaded.join(", ") : "none"}`
101
- ].join("\n");
102
- }
103
- });
104
- const prepareStep = async () => {
105
- return { activeTools: getActiveToolNames() };
106
- };
107
- return {
108
- tools: allTools,
109
- activeTools: getActiveToolNames(),
110
- prepareStep,
111
- availableSkillList,
112
- getLoadedGuidance,
113
- getLoadedSkills() {
114
- return [...loadedSkills];
115
- }
116
- };
117
- }
118
- /** Creates the `skill_state` table in DO SQLite if it does not exist yet. */
119
- function ensureSkillTable(sql) {
120
- sql`CREATE TABLE IF NOT EXISTS skill_state (id INTEGER PRIMARY KEY, active_skills TEXT NOT NULL DEFAULT '[]')`;
121
- }
122
- /**
123
- * Reads the persisted list of loaded skill names from DO SQLite.
124
- * Returns an empty array if the table is missing or the row does not exist.
125
- */
126
- function getStoredSkills(sql) {
127
- try {
128
- ensureSkillTable(sql);
129
- const rows = sql`SELECT active_skills FROM skill_state WHERE id = 1`;
130
- if (rows.length === 0) return [];
131
- return JSON.parse(rows[0].active_skills);
132
- } catch {
133
- return [];
134
- }
135
- }
136
- /**
137
- * Persists the current list of loaded skill names to DO SQLite.
138
- * Upserts the single `skill_state` row (id = 1).
139
- */
140
- function saveSkillStateFromMessages(sql, messages) {
141
- let latestSkillState;
142
- for (const msg of messages) {
143
- if (msg.role !== "assistant" || !msg.parts) continue;
144
- for (const part of msg.parts) {
145
- if (!("toolCallId" in part)) continue;
146
- if (part.type !== `tool-activate_skill` || typeof part.output !== "string") continue;
147
- const sentinelIndex = part.output.indexOf(SKILL_STATE_SENTINEL);
148
- if (sentinelIndex !== -1) try {
149
- const stateJson = part.output.slice(sentinelIndex + 18);
150
- latestSkillState = JSON.parse(stateJson);
151
- } catch {}
152
- }
153
- }
154
- if (latestSkillState == void 0) return;
155
- ensureSkillTable(sql);
156
- sql`INSERT OR REPLACE INTO skill_state(id, active_skills) VALUES(1, ${JSON.stringify(latestSkillState)})`;
157
- }
158
- //#endregion
159
- //#region src/server/v1/util/llm.ts
160
- function buildSystemPromptWithSkills(basePrompt, availableSkillList, loadedGuidance) {
161
- let prompt = `${basePrompt}
162
-
163
- ## Tools
164
-
165
- ### Loading More Tools:
166
- Use \`activate_skill\` to load these skills. NEVER ask the user for permission to load a skill - just load it silently when relevant. The user should not be aware of skills or tools; activate them autonomously based on the request:
167
-
168
- ${availableSkillList}`;
169
- if (loadedGuidance) prompt += `
170
-
171
- ### Loaded skill instructions:
172
- The following skills are currently active. Apply their instructions when using the corresponding tools.
173
-
174
- ${loadedGuidance}`;
175
- return prompt.trim();
176
- }
177
- /**
178
- * Convention: tools can include a `sources` array in their return value to surface
179
- * source URLs in the message stream. This transform detects it automatically.
180
- *
181
- * ```typescript
182
- * // In any tool's execute function:
183
- * return { myContent: "...", sources: [{ url: "https://...", title: "Page title" }] };
184
- * ```
185
- */
186
- function buildSourcesTransform(additional) {
187
- const sourcesTransform = () => new TransformStream({ transform(chunk, controller) {
188
- controller.enqueue(chunk);
189
- if (chunk.type !== "tool-result") return;
190
- const output = chunk.output;
191
- if (!output || typeof output !== "object" || Array.isArray(output)) return;
192
- const sources = output.sources;
193
- if (!Array.isArray(sources)) return;
194
- for (const source of sources) {
195
- if (!source || typeof source !== "object") continue;
196
- const s = source;
197
- if (typeof s.url !== "string") continue;
198
- controller.enqueue({
199
- type: "source",
200
- sourceType: "url",
201
- id: crypto.randomUUID(),
202
- url: s.url,
203
- ...typeof s.title === "string" ? { title: s.title } : {}
204
- });
205
- }
206
- } });
207
- if (!additional) return sourcesTransform;
208
- return [sourcesTransform, ...Array.isArray(additional) ? additional : [additional]];
209
- }
210
- /**
211
- * Builds the parameter object for a Vercel AI SDK `streamText` or `generateText` call.
212
- *
213
- * Handles skill wiring (`activate_skill`, `list_capabilities`, `prepareStep`).
214
- *
215
- * The returned object can be spread directly into `streamText` or `generateText`:
216
- *
217
- * ```typescript
218
- * const params = buildLLMParams({ ... });
219
- * return streamText(params).toUIMessageStreamResponse();
220
- * ```
221
- */
222
- function buildLLMParams(config) {
223
- const { activeSkills = [], skills, experimental_transform, system, tools = {}, ...rest } = config;
224
- const composedTransform = buildSourcesTransform(experimental_transform);
225
- const baseParams = {
226
- ...rest,
227
- system,
228
- tools,
229
- stopWhen: rest.stopWhen ?? stepCountIs(20),
230
- experimental_transform: composedTransform
231
- };
232
- if (!skills?.length) return baseParams;
233
- const skillsCtx = createSkills({
234
- tools,
235
- skills,
236
- initialLoadedSkills: activeSkills
237
- });
238
- const systemWithSkills = buildSystemPromptWithSkills(typeof system === "string" ? system : void 0, skillsCtx.availableSkillList, skillsCtx.getLoadedGuidance());
239
- const prepareStep = async (stepOptions) => {
240
- return {
241
- activeTools: (await skillsCtx.prepareStep(stepOptions) ?? {}).activeTools ?? [],
242
- system: systemWithSkills
243
- };
244
- };
245
- return {
246
- ...baseParams,
247
- system: systemWithSkills,
248
- tools: skillsCtx.tools,
249
- activeTools: skillsCtx.activeTools,
250
- prepareStep
251
- };
252
- }
253
- //#endregion
254
- //#region src/server/v1/agent/Agent.ts
255
- /**
256
- * Base agent for Cloudflare Agents SDK Durable Objects with lazy skill loading
257
- * and `buildLLMParams` wiring.
258
- *
259
- * Handles CF infrastructure concerns: DO SQLite persistence for loaded skill state.
260
- *
261
- * For chat agents with message history, compaction, and conversation recording,
262
- * extend {@link ChatAgent} instead.
263
- */
264
- var Agent = class extends Agent$1 {
265
- clientIp;
266
- forwardedFor;
267
- /**
268
- * The user context for the session.
269
- * Define getUserContext to set a user context.
270
- */
271
- get userContext() {
272
- const { connection } = getCurrentAgent();
273
- return (connection?.state)?.userContext ?? {};
274
- }
275
- /**
276
- * Returns the user ID from the durable object name.
277
- */
278
- getUserId() {
279
- return this.name.split(":")[0];
280
- }
281
- async onConnect(connection, ctx) {
282
- this.clientIp = ctx.request.headers.get("CF-Connecting-IP") ?? ctx.request.headers.get("X-Forwarded-For")?.split(",")[0]?.trim();
283
- this.forwardedFor = ctx.request.headers.get("X-Forwarded-For") ?? void 0;
284
- if (!this.env.AGENT_DB) {
285
- console.error("[Agent] Connection rejected: no AGENT_DB bound");
286
- connection.close(3e3, "Could not connect to agent");
287
- return;
288
- }
289
- if (!this.env.AGENTS_AUDIT_LOGS) {
290
- console.error("[Agent] Connection rejected: no AGENTS_AUDIT_LOGS bound. Audit logs are required.");
291
- connection.close(3e3, "Could not connect to agent");
292
- return;
293
- }
294
- if (!this.env.AGENTS_ANALYTICS) console.warn("[Agent] No AGENTS_ANALYTICS bound. Analytics will not be collected.");
295
- if (!this.getUserId()) {
296
- console.error("[Agent] Connection rejected: name must be in the format userId:uniqueChatId");
297
- connection.close(3e3, "Could not connect to agent");
298
- return;
299
- }
300
- if (this.getJwtAuthConfig) {
301
- const config = this.getJwtAuthConfig(ctx.request);
302
- if (config) {
303
- let result;
304
- try {
305
- result = await verifyJwt(ctx.request, config);
306
- } catch (error) {
307
- console.error("[Agent] JWT verification error", error);
308
- connection.close(4001, "Unauthorized");
309
- return;
310
- }
311
- if (!result.success) {
312
- connection.close(result.status === 401 ? 4001 : 4003, result.message);
313
- return;
314
- }
315
- if (!isJwtActorAuthorized(config, result.claims, {
316
- durableObjectName: this.name,
317
- parentPath: this.parentPath
318
- })) {
319
- connection.close(4003, "Forbidden");
320
- return;
321
- }
322
- const token = extractTokenFromConnectRequest(ctx.request);
323
- if (token) {
324
- const userContext = await this.getUserContext?.(token);
325
- if (userContext) connection.setState({ userContext });
326
- }
327
- }
328
- }
329
- return super.onConnect(connection, ctx);
330
- }
331
- /**
332
- * Builds the parameter object for a `streamText` or `generateText` call,
333
- * pre-filling `activeSkills` from this agent instance.
334
- */
335
- async buildLLMParams(config) {
336
- const activeSkills = await getStoredSkills(this.sql.bind(this));
337
- const experimental_context = {
338
- ...config.experimental_context,
339
- ...config.options?.body,
340
- _userContext: this.userContext
341
- };
342
- return buildLLMParams({
343
- ...config,
344
- activeSkills,
345
- experimental_context,
346
- experimental_telemetry: {
347
- ...config.experimental_telemetry,
348
- isEnabled: true,
349
- tracer: createAgentTracer(this.env.AGENTS_AUDIT_LOGS, this.env.AGENTS_ANALYTICS, {
350
- agentName: this.constructor.name,
351
- durableObjectName: this.name,
352
- actorId: this.getUserId(),
353
- ...this.clientIp ? { clientIp: this.clientIp } : {},
354
- ...this.forwardedFor ? { forwardedFor: this.forwardedFor } : {}
355
- }),
356
- metadata: {
357
- agentName: this.constructor.name,
358
- version: "v1",
359
- durableObjectName: this.name,
360
- actorId: this.getUserId(),
361
- ...this.clientIp ? { clientIp: this.clientIp } : {},
362
- ...this.forwardedFor ? { forwardedFor: this.forwardedFor } : {},
363
- ...config.experimental_telemetry?.metadata
364
- }
365
- }
366
- });
367
- }
368
- };
369
- const TOOL_RESULT_PREVIEW_CHARS = 200;
370
- const SUMMARY_MAX_TOKENS = 4e3;
371
- /**
372
- * Estimates token count for a message array using a 3.5 chars/token
373
- * approximation. Counts text from text/reasoning parts, tool inputs/outputs.
374
- */
375
- function estimateMessagesTokens(messages) {
376
- let totalChars = 0;
377
- for (const msg of messages) {
378
- if (typeof msg.content === "string") {
379
- totalChars += msg.content.length;
380
- continue;
381
- }
382
- for (const part of msg.content) if (part.type === "text" || part.type === "reasoning") totalChars += part.text.length;
383
- else if (part.type === "tool-call") totalChars += JSON.stringify(part.input).length;
384
- else if (part.type === "tool-result") {
385
- const output = part.output;
386
- totalChars += typeof output === "string" ? output.length : JSON.stringify(output).length;
387
- }
388
- }
389
- return Math.ceil(totalChars / 3.5);
390
- }
391
- /**
392
- * Renders messages as human-readable text for the compaction summary prompt.
393
- */
394
- function formatMessagesForSummary(messages) {
395
- const lines = [];
396
- for (const msg of messages) {
397
- const roleLabel = msg.role.charAt(0).toUpperCase() + msg.role.slice(1);
398
- const parts = [];
399
- if (typeof msg.content === "string") {
400
- if (msg.content.trim()) parts.push(msg.content.trim());
401
- } else for (const part of msg.content) if (part.type === "text" || part.type === "reasoning") {
402
- const text = part.text.trim();
403
- if (text) parts.push(text);
404
- } else if (part.type === "tool-call") {
405
- const p = part;
406
- parts.push(`[Tool call: ${p.toolName}]`);
407
- } else if (part.type === "tool-result") {
408
- const p = part;
409
- const rawOutput = typeof p.output === "string" ? p.output : JSON.stringify(p.output);
410
- const preview = rawOutput.slice(0, TOOL_RESULT_PREVIEW_CHARS);
411
- const ellipsis = rawOutput.length > TOOL_RESULT_PREVIEW_CHARS ? "..." : "";
412
- parts.push(`[Tool: ${p.toolName}, result: ${preview}${ellipsis}]`);
413
- }
414
- if (parts.length > 0) lines.push(`${roleLabel}: ${parts.join(" ")}`);
415
- }
416
- return lines.join("\n");
417
- }
418
- /**
419
- * Calls the model to produce a concise summary of old + recent message windows.
420
- */
421
- async function generateCompactionSummary(oldMessages, recentMessages, model) {
422
- const prompt = `Summarize this conversation history concisely for an AI assistant to continue the conversation.
423
- Focus MORE on recent exchanges (what the user was working on, what tools were used, what was found).
424
- Include key facts, decisions, and context needed to continue the conversation.
425
- Keep entity names, numbers, file paths, and specific details that might be referenced later.
426
- Do NOT include pleasantries or meta-commentary - just the essential context.
427
-
428
- OLDER MESSAGES (summarize briefly):
429
- ${formatMessagesForSummary(oldMessages)}
430
-
431
- RECENT MESSAGES (summarize with more detail - this is where the user currently is):
432
- ${formatMessagesForSummary(recentMessages)}
433
-
434
- Write a concise summary:`;
435
- try {
436
- const { text } = await generateText({
437
- model,
438
- messages: [{
439
- role: "user",
440
- content: prompt
441
- }],
442
- maxOutputTokens: SUMMARY_MAX_TOKENS
443
- });
444
- return text || "Unable to summarize conversation history.";
445
- } catch (error) {
446
- console.error("Compaction summarization error:", error);
447
- return "Unable to summarize conversation history.";
448
- }
449
- }
450
- /**
451
- * Summarizes older messages into a single system message and appends the
452
- * recent verbatim tail. Returns messages unchanged if already short enough.
453
- */
454
- async function compactMessages(messages, model, tailSize) {
455
- if (messages.length <= tailSize) return messages;
456
- const splitIndex = messages.length - tailSize;
457
- const oldMessages = messages.slice(0, splitIndex);
458
- const recentTail = messages.slice(splitIndex);
459
- return [{
460
- role: "system",
461
- content: `[Conversation summary - older context was compacted]\n${await generateCompactionSummary(oldMessages, recentTail, model)}`
462
- }, ...recentTail];
463
- }
464
- /**
465
- * Entry point for compaction. Returns messages unchanged when model is
466
- * undefined or estimated token count is under COMPACT_TOKEN_THRESHOLD.
467
- */
468
- async function compactIfNeeded(messages, model, tailSize) {
469
- if (!model || estimateMessagesTokens(messages) <= 14e4) return messages;
470
- return compactMessages(messages, model, tailSize);
471
- }
472
- //#endregion
473
- //#region src/server/v1/agent-chat/features/conversations/conversations.ts
474
- /**
475
- * Renders only the user- and assistant-visible text parts of a message
476
- * array for conversation title/summary generation.
477
- *
478
- * Tool calls, tool results, system messages, and reasoning parts are
479
- * intentionally excluded so the model receives a clean transcript.
480
- */
481
- function formatMessagesForConversationSummary(messages) {
482
- const lines = [];
483
- for (const message of messages) {
484
- if (message.role !== "user" && message.role !== "assistant") continue;
485
- let text = "";
486
- if (typeof message.content === "string") text = message.content.trim();
487
- else text = message.content.filter((part) => part.type === "text").map((part) => part.text.trim()).filter(Boolean).join(" ");
488
- if (!text) continue;
489
- const roleLabel = message.role === "user" ? "User" : "Assistant";
490
- lines.push(`${roleLabel}: ${text}`);
491
- }
492
- return lines.join("\n");
493
- }
494
- /**
495
- * Number of recent messages passed to `generateSummary` for rolling
496
- * summarization. Keeping this bounded prevents the prompt growing
497
- * unboundedly regardless of conversation length.
498
- */
499
- const SUMMARY_CONTEXT_MESSAGES = 15;
500
- async function recordConversationSummary(db, durableObjectName, messages, model) {
501
- if (!await getConversationSummary(db, durableObjectName)) {
502
- const { title, summary } = await generateTitleAndSummary(messages, model);
503
- await upsertConversationSummary(db, durableObjectName, title, summary);
504
- } else if (messages.length <= 7 || messages.length % SUMMARY_CONTEXT_MESSAGES === 0) await generateConversationSummary(db, durableObjectName, messages, model);
505
- }
506
- /**
507
- * Records a conversation row in the `conversations` D1 table.
508
- *
509
- * Called by `ChatAgentHarness` after every turn. On first call for a given
510
- * `durableObjectName` the row is inserted with `created_at` set to now,
511
- * and with the provided `title` and `summary` if supplied.
512
- * On subsequent calls only `updated_at` is refreshed —
513
- * `created_at`, `title`, and `summary` are never overwritten, preserving
514
- * any user edits.
515
- */
516
- async function upsertConversationSummary(db, durableObjectName, title, summary) {
517
- const now = (/* @__PURE__ */ new Date()).toISOString();
518
- await db.prepare(`INSERT INTO conversations (durable_object_name, title, summary, created_at, updated_at)
519
- VALUES (?, ?, ?, ?, ?)
520
- ON CONFLICT(durable_object_name) DO UPDATE SET
521
- updated_at = excluded.updated_at`).bind(durableObjectName, title ?? null, summary ?? null, now, now).run();
522
- }
523
- /**
524
- * Returns the current `title` and `summary` for a conversation row,
525
- * or `null` if the row does not exist yet.
526
- */
527
- async function getConversationSummary(db, durableObjectName) {
528
- return await db.prepare(`SELECT title, summary FROM conversations WHERE durable_object_name = ?`).bind(durableObjectName).first() ?? null;
529
- }
530
- /**
531
- * Returns all conversations for a user, ordered by most recent.
532
- */
533
- async function getConversations(db, userId) {
534
- const { results } = await db.prepare(`SELECT * FROM conversations WHERE durable_object_name LIKE ? ORDER BY updated_at DESC`).bind(`${userId}:%`).all();
535
- return results;
536
- }
537
- /**
538
- * Deletes a conversation row from the `conversations` D1 table.
539
- */
540
- async function deleteConversationRow(db, durableObjectName) {
541
- await db.prepare(`DELETE FROM conversations WHERE durable_object_name = ?`).bind(durableObjectName).run();
542
- }
543
- /**
544
- * Writes a generated `title` and `summary` back to the `conversations` row.
545
- */
546
- async function updateConversationSummary(db, durableObjectName, title, summary) {
547
- await db.prepare(`UPDATE conversations SET title = ?, summary = ? WHERE durable_object_name = ?`).bind(title, summary, durableObjectName).run();
548
- }
549
- /**
550
- * Generates a title and summary for a conversation using the provided model.
551
- * Returns the result without writing to D1.
552
- *
553
- * Pass `existingSummary` so the model can detect direction changes when
554
- * updating an existing summary. Omit it (or pass undefined) for the initial
555
- * generation.
556
- *
557
- * Only the last `SUMMARY_CONTEXT_MESSAGES` messages are used to keep the
558
- * prompt bounded regardless of total conversation length.
559
- */
560
- async function generateTitleAndSummary(messages, model, existingSummary) {
561
- const recentMessages = await convertToModelMessages(messages.slice(-15));
562
- const previousContext = existingSummary ? `Previous summary:\n${existingSummary}\n\nMost recent messages:` : "Conversation:";
563
- const { output } = await generateText({
564
- model,
565
- output: Output.object({ schema: jsonSchema({
566
- type: "object",
567
- properties: {
568
- title: {
569
- type: "string",
570
- description: "Short title for the conversation, max 8 words"
571
- },
572
- summary: {
573
- type: "string",
574
- description: "1-2 short sentence summary of the conversation. If the conversation direction has changed from the previous summary, reflect the new direction."
575
- }
576
- },
577
- required: ["title", "summary"]
578
- }) }),
579
- prompt: `${previousContext}\n\n${formatMessagesForConversationSummary(recentMessages)}`
580
- });
581
- return output;
582
- }
583
- /**
584
- * Generates a title and summary for a conversation using the provided model
585
- * and writes the result back to D1.
586
- *
587
- * Fetches any existing summary first so the model can detect direction changes.
588
- * Only the last `SUMMARY_CONTEXT_MESSAGES` messages are passed to keep the
589
- * prompt bounded regardless of total conversation length.
590
- *
591
- * Called by `ChatAgentHarness` every `SUMMARY_CONTEXT_MESSAGES` messages after
592
- * the first turn.
593
- */
594
- async function generateConversationSummary(db, durableObjectName, messages, model) {
595
- const { title, summary } = await generateTitleAndSummary(messages, model, (await getConversationSummary(db, durableObjectName))?.summary ?? void 0);
596
- await updateConversationSummary(db, durableObjectName, title, summary);
597
- return {
598
- title,
599
- summary
600
- };
601
- }
602
- //#endregion
603
- //#region src/server/v1/agent-chat/features/conversations/retention.ts
604
- const DELETE_CONVERSATION_CALLBACK = "deleteConversationCallback";
605
- const CONVERSATION_EXPIRED_CLOSE_CODE = 3001;
606
- const CONVERSATION_EXPIRED_CLOSE_REASON = "Conversation expired due to inactivity.";
607
- function getConversationRetentionMs(days) {
608
- if (typeof days !== "number" || !Number.isFinite(days) || days <= 0) return null;
609
- return Math.floor(days * 24 * 60 * 60 * 1e3);
610
- }
611
- function getDeleteConversationScheduleIds(schedules) {
612
- return schedules.filter((schedule) => schedule.callback === DELETE_CONVERSATION_CALLBACK).map((schedule) => schedule.id);
613
- }
614
- //#endregion
615
- //#region src/server/v1/util/messages.ts
616
- function isEphemeralSkillToolPart(part) {
617
- if (!part || typeof part !== "object" || !("toolCallId" in part) || !("type" in part)) return false;
618
- const { type } = part;
619
- return type === `tool-list_capabilities` || type === `tool-activate_skill`;
620
- }
621
- function filterEphemeralMessages(messages) {
622
- return messages.flatMap((message) => {
623
- if (message.role !== "assistant" || !message.parts?.length) return [message];
624
- const filteredParts = message.parts.filter((part) => !isEphemeralSkillToolPart(part));
625
- if (filteredParts.length === 0) return [];
626
- return [{
627
- ...message,
628
- parts: filteredParts
629
- }];
630
- });
631
- }
632
- //#endregion
633
- //#region src/server/v1/agent-chat/features/conversations/rating.ts
634
- async function rateMessage(db, durableObjectName, messageId, rating, comment) {
635
- const now = (/* @__PURE__ */ new Date()).toISOString();
636
- await db.prepare(`INSERT INTO message_ratings (message_id, durable_object_name, rating, comment, created_at, updated_at)
637
- VALUES (?, ?, ?, ?, ?, ?)
638
- ON CONFLICT (message_id, durable_object_name) DO UPDATE SET
639
- rating = excluded.rating,
640
- comment = excluded.comment,
641
- updated_at = excluded.updated_at`).bind(messageId, durableObjectName, rating, comment ?? null, now, now).run();
642
- }
643
- async function getMessageRatings(db, durableObjectName) {
644
- const result = await db.prepare(`SELECT message_id, rating, comment FROM message_ratings WHERE durable_object_name = ?`).bind(durableObjectName).all();
645
- return Object.fromEntries(result.results.map((row) => [row.message_id, {
646
- rating: row.rating,
647
- ...row.comment != null ? { comment: row.comment } : {}
648
- }]));
649
- }
650
- //#endregion
651
- //#region src/server/v1/agent-chat/ChatAgent.ts
652
- let _initProto;
653
- function _applyDecs(e, t, n, r, o, i) {
654
- var a, c, u, s, f, l, p, d = Symbol.metadata || Symbol.for("Symbol.metadata"), m = Object.defineProperty, h = Object.create, y = [h(null), h(null)], v = t.length;
655
- function g(t, n, r) {
656
- return function(o, i) {
657
- n && (i = o, o = e);
658
- for (var a = 0; a < t.length; a++) i = t[a].apply(o, r ? [i] : []);
659
- return r ? i : o;
660
- };
661
- }
662
- function b(e, t, n, r) {
663
- if ("function" != typeof e && (r || void 0 !== e)) throw new TypeError(t + " must " + (n || "be") + " a function" + (r ? "" : " or undefined"));
664
- return e;
665
- }
666
- function applyDec(e, t, n, r, o, i, u, s, f, l, p) {
667
- function d(e) {
668
- if (!p(e)) throw new TypeError("Attempted to access private element on non-instance");
669
- }
670
- var h = [].concat(t[0]), v = t[3], w = !u, D = 1 === o, S = 3 === o, j = 4 === o, E = 2 === o;
671
- function I(t, n, r) {
672
- return function(o, i) {
673
- return n && (i = o, o = e), r && r(o), P[t].call(o, i);
674
- };
675
- }
676
- if (!w) {
677
- var P = {}, k = [], F = S ? "get" : j || D ? "set" : "value";
678
- if (f ? (l || D ? P = {
679
- get: _setFunctionName(function() {
680
- return v(this);
681
- }, r, "get"),
682
- set: function(e) {
683
- t[4](this, e);
684
- }
685
- } : P[F] = v, l || _setFunctionName(P[F], r, E ? "" : F)) : l || (P = Object.getOwnPropertyDescriptor(e, r)), !l && !f) {
686
- if ((c = y[+s][r]) && 7 !== (c ^ o)) throw Error("Decorating two elements with the same name (" + P[F].name + ") is not supported yet");
687
- y[+s][r] = o < 3 ? 1 : o;
688
- }
689
- }
690
- for (var N = e, O = h.length - 1; O >= 0; O -= n ? 2 : 1) {
691
- var T = b(h[O], "A decorator", "be", !0), z = n ? h[O - 1] : void 0, A = {}, H = {
692
- kind: [
693
- "field",
694
- "accessor",
695
- "method",
696
- "getter",
697
- "setter",
698
- "class"
699
- ][o],
700
- name: r,
701
- metadata: a,
702
- addInitializer: function(e, t) {
703
- if (e.v) throw new TypeError("attempted to call addInitializer after decoration was finished");
704
- b(t, "An initializer", "be", !0), i.push(t);
705
- }.bind(null, A)
706
- };
707
- if (w) c = T.call(z, N, H), A.v = 1, b(c, "class decorators", "return") && (N = c);
708
- else if (H.static = s, H.private = f, c = H.access = { has: f ? p.bind() : function(e) {
709
- return r in e;
710
- } }, j || (c.get = f ? E ? function(e) {
711
- return d(e), P.value;
712
- } : I("get", 0, d) : function(e) {
713
- return e[r];
714
- }), E || S || (c.set = f ? I("set", 0, d) : function(e, t) {
715
- e[r] = t;
716
- }), N = T.call(z, D ? {
717
- get: P.get,
718
- set: P.set
719
- } : P[F], H), A.v = 1, D) {
720
- if ("object" == typeof N && N) (c = b(N.get, "accessor.get")) && (P.get = c), (c = b(N.set, "accessor.set")) && (P.set = c), (c = b(N.init, "accessor.init")) && k.unshift(c);
721
- else if (void 0 !== N) throw new TypeError("accessor decorators must return an object with get, set, or init properties or undefined");
722
- } else b(N, (l ? "field" : "method") + " decorators", "return") && (l ? k.unshift(N) : P[F] = N);
723
- }
724
- return o < 2 && u.push(g(k, s, 1), g(i, s, 0)), l || w || (f ? D ? u.splice(-1, 0, I("get", s), I("set", s)) : u.push(E ? P[F] : b.call.bind(P[F])) : m(e, r, P)), N;
725
- }
726
- function w(e) {
727
- return m(e, d, {
728
- configurable: !0,
729
- enumerable: !0,
730
- value: a
731
- });
732
- }
733
- return void 0 !== i && (a = i[d]), a = h(null == a ? null : a), f = [], l = function(e) {
734
- e && f.push(g(e));
735
- }, p = function(t, r) {
736
- for (var i = 0; i < n.length; i++) {
737
- var a = n[i], c = a[1], l = 7 & c;
738
- if ((8 & c) == t && !l == r) {
739
- var p = a[2], d = !!a[3], m = 16 & c;
740
- applyDec(t ? e : e.prototype, a, m, d ? "#" + p : _toPropertyKey(p), l, l < 2 ? [] : t ? s = s || [] : u = u || [], f, !!t, d, r, t && d ? function(t) {
741
- return _checkInRHS(t) === e;
742
- } : o);
743
- }
744
- }
745
- }, p(8, 0), p(0, 0), p(8, 1), p(0, 1), l(u), l(s), c = f, v || w(e), {
746
- e: c,
747
- get c() {
748
- var n = [];
749
- return v && [w(e = applyDec(e, [t], r, e.name, 5, n)), g(n, 1)];
750
- }
751
- };
752
- }
753
- function _toPropertyKey(t) {
754
- var i = _toPrimitive(t, "string");
755
- return "symbol" == typeof i ? i : i + "";
756
- }
757
- function _toPrimitive(t, r) {
758
- if ("object" != typeof t || !t) return t;
759
- var e = t[Symbol.toPrimitive];
760
- if (void 0 !== e) {
761
- var i = e.call(t, r || "default");
762
- if ("object" != typeof i) return i;
763
- throw new TypeError("@@toPrimitive must return a primitive value.");
764
- }
765
- return ("string" === r ? String : Number)(t);
766
- }
767
- function _setFunctionName(e, t, n) {
768
- "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : "");
769
- try {
770
- Object.defineProperty(e, "name", {
771
- configurable: !0,
772
- value: n ? n + " " + t : t
773
- });
774
- } catch (e) {}
775
- return e;
776
- }
777
- function _checkInRHS(e) {
778
- if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null"));
779
- return e;
780
- }
781
- /**
782
- * Chat agent for Cloudflare Agents SDK: lazy skill loading, message persistence,
783
- * compaction, and conversation metadata in D1.
784
- *
785
- * Handles CF infrastructure concerns: DO SQLite for loaded skill state,
786
- * stripping skill meta-tool messages before persistence, and history replay to
787
- * newly connected clients.
788
- *
789
- * Skill loading, compaction, and LLM calls use `buildLLMParams` from
790
- * `@economic/agents` inside `onChatMessage`.
791
- */
792
- var ChatAgent = class extends AIChatAgent {
793
- static {
794
- [_initProto] = _applyDecs(this, [], [
795
- [
796
- callable({ description: "Rate a message by its id" }),
797
- 2,
798
- "rateMessage"
799
- ],
800
- [
801
- callable({ description: "Returns all message ratings for the current conversation" }),
802
- 2,
803
- "getMessageRatings"
804
- ],
805
- [
806
- callable({ description: "Returns all conversations for the current user" }),
807
- 2,
808
- "getConversations"
809
- ],
810
- [
811
- callable({ description: "Delete a conversation by its id" }),
812
- 2,
813
- "deleteConversation"
814
- ]
815
- ], 0, void 0, AIChatAgent).e;
816
- }
817
- initialState = (_initProto(this), {
818
- status: "connecting",
819
- type: "chat"
820
- });
821
- /**
822
- * Number of days of inactivity before the full conversation is deleted.
823
- *
824
- * Leave `undefined` to disable automatic retention cleanup.
825
- */
826
- conversationRetentionDays;
827
- /**
828
- * Number of recent messages to keep verbatim when compaction runs.
829
- * Older messages beyond this count are summarised into a single system message.
830
- * Used as the default when `maxMessagesBeforeCompaction` is not provided to `buildLLMParams`.
831
- *
832
- * Default is 15.
833
- */
834
- maxMessagesBeforeCompaction = 15;
835
- clientIp;
836
- forwardedFor;
837
- /**
838
- * The user context for the session.
839
- * Define getUserContext to set a user context.
840
- */
841
- get userContext() {
842
- const { connection } = getCurrentAgent();
843
- return (connection?.state)?.userContext ?? {};
844
- }
845
- /**
846
- * Returns the user ID from the durable object name.
847
- */
848
- getUserId() {
849
- return this.name.split(":")[0];
850
- }
851
- onStart() {
852
- this.setState({
853
- ...this.initialState,
854
- status: "connecting"
855
- });
856
- }
857
- async onClose() {
858
- this.setState({
859
- type: "chat",
860
- status: "disconnected"
861
- });
862
- }
863
- async onConnect(connection, ctx) {
864
- this.clientIp = ctx.request.headers.get("CF-Connecting-IP") ?? ctx.request.headers.get("X-Forwarded-For")?.split(",")[0]?.trim();
865
- this.forwardedFor = ctx.request.headers.get("X-Forwarded-For") ?? void 0;
866
- if (!this.env.AGENT_DB) {
867
- console.error("[Agent] Connection rejected: no AGENT_DB bound");
868
- connection.close(3e3, "Could not connect to agent");
869
- return;
870
- }
871
- if (!this.env.AGENTS_AUDIT_LOGS) {
872
- console.error("[Agent] Connection rejected: no AGENTS_AUDIT_LOGS bound. Audit logs are required.");
873
- connection.close(3e3, "Could not connect to agent");
874
- return;
875
- }
876
- if (!this.env.AGENTS_ANALYTICS) console.warn("[Agent] No AGENTS_ANALYTICS bound. Analytics will not be collected.");
877
- if (!this.getUserId()) {
878
- console.error("[Agent] Connection rejected: name must be in the format userId:uniqueChatId");
879
- connection.close(3e3, "Could not connect to agent");
880
- return;
881
- }
882
- if (this.getJwtAuthConfig) {
883
- const config = this.getJwtAuthConfig(ctx.request);
884
- if (config) {
885
- let result;
886
- try {
887
- result = await verifyJwt(ctx.request, config);
888
- } catch (error) {
889
- console.error("[Agent] JWT verification error", error);
890
- connection.close(4001, "Unauthorized");
891
- this.setState({
892
- type: "chat",
893
- status: "unauthorized"
894
- });
895
- return;
896
- }
897
- if (!result.success) {
898
- connection.close(result.status === 401 ? 4001 : 4003, result.message);
899
- this.setState({
900
- type: "chat",
901
- status: "unauthorized"
902
- });
903
- return;
904
- }
905
- if (!isJwtActorAuthorized(config, result.claims, {
906
- durableObjectName: this.name,
907
- parentPath: this.parentPath
908
- })) {
909
- connection.close(4003, "Forbidden");
910
- this.setState({
911
- type: "chat",
912
- status: "unauthorized"
913
- });
914
- return;
915
- }
916
- }
917
- const token = extractTokenFromConnectRequest(ctx.request);
918
- if (token) this._pendingUserContextRequest = this.getUserContext?.(token).then((userContext) => {
919
- if (userContext) connection.setState({ userContext });
920
- });
921
- }
922
- this.setState({
923
- type: "chat",
924
- status: "connected"
925
- });
926
- return super.onConnect(connection, ctx);
927
- }
928
- _pendingUserContextRequest;
929
- /**
930
- * Builds the parameter object for a `streamText` or `generateText` call,
931
- * pre-filling `messages`, `activeSkills`, and `fastModel` from this agent instance.
932
- *
933
- * **Compaction** runs automatically when `fastModel` is set on the class, using
934
- * `DEFAULT_MAX_MESSAGES_BEFORE_COMPACTION` (30) as the threshold. Override the
935
- * threshold by setting `maxMessagesBeforeCompaction` on the class. Disable compaction
936
- * entirely by setting `maxMessagesBeforeCompaction = undefined` explicitly.
937
- */
938
- async buildLLMParams(config) {
939
- const activeSkills = await getStoredSkills(this.sql.bind(this));
940
- const experimental_context = {
941
- ...config.experimental_context,
942
- ...config.options?.body,
943
- _userContext: this.userContext
944
- };
945
- const messages = await convertToModelMessages(this.messages);
946
- const fastModel = this.getFastModel();
947
- const processedMessages = fastModel && this.maxMessagesBeforeCompaction !== void 0 ? await compactIfNeeded(messages, fastModel, this.maxMessagesBeforeCompaction) : messages;
948
- return buildLLMParams({
949
- ...config,
950
- activeSkills,
951
- messages: processedMessages,
952
- experimental_context,
953
- experimental_telemetry: {
954
- ...config.experimental_telemetry,
955
- isEnabled: true,
956
- tracer: createAgentTracer(this.env.AGENTS_AUDIT_LOGS, this.env.AGENTS_ANALYTICS, {
957
- agentName: this.constructor.name,
958
- durableObjectName: this.name,
959
- actorId: this.getUserId(),
960
- ...this.clientIp ? { clientIp: this.clientIp } : {},
961
- ...this.forwardedFor ? { forwardedFor: this.forwardedFor } : {}
962
- }),
963
- metadata: {
964
- agentName: this.constructor.name,
965
- version: "v1",
966
- durableObjectName: this.name,
967
- actorId: this.getUserId(),
968
- ...this.clientIp ? { clientIp: this.clientIp } : {},
969
- ...this.forwardedFor ? { forwardedFor: this.forwardedFor } : {},
970
- ...config.experimental_telemetry?.metadata
971
- }
972
- }
973
- });
974
- }
975
- async persistMessages(messages, excludeBroadcastIds = [], options) {
976
- const filtered = filterEphemeralMessages(messages);
977
- await super.persistMessages(filtered, excludeBroadcastIds, options);
978
- saveSkillStateFromMessages(this.sql.bind(this), messages);
979
- await super.persistMessages(filterEphemeralMessages(messages), excludeBroadcastIds, options);
980
- }
981
- async onChatResponse(result) {
982
- if (result.error) {
983
- console.error("[Agent] Chat response error", result.error);
984
- return;
985
- }
986
- recordConversationSummary(this.env.AGENT_DB, this.name, this.messages, this.getFastModel());
987
- this.scheduleConversationForAutoDeletion();
988
- }
989
- async rateMessage(messageId, rating, comment) {
990
- await rateMessage(this.env.AGENT_DB, this.name, messageId, rating, comment);
991
- }
992
- async getMessageRatings() {
993
- return getMessageRatings(this.env.AGENT_DB, this.name);
994
- }
995
- async getConversations() {
996
- return getConversations(this.env.AGENT_DB, this.getUserId());
997
- }
998
- async deleteConversation(id) {
999
- if (!id.startsWith(`${this.getUserId()}:`)) {
1000
- console.error("[Agent] Failed to delete conversation: Not owned by current user", {
1001
- conversationName: id,
1002
- userId: this.getUserId()
1003
- });
1004
- return false;
1005
- }
1006
- try {
1007
- await deleteConversationRow(this.env.AGENT_DB, id);
1008
- } catch (error) {
1009
- console.error("[Agent] Failed to delete conversation row", {
1010
- conversationName: id,
1011
- error
1012
- });
1013
- return false;
1014
- }
1015
- await (id === this.name ? this : this.binding.getByName(id)).destroy();
1016
- return true;
1017
- }
1018
- async destroy() {
1019
- for (const connection of this.getConnections()) try {
1020
- connection.close(CONVERSATION_EXPIRED_CLOSE_CODE, CONVERSATION_EXPIRED_CLOSE_REASON);
1021
- } catch (error) {
1022
- console.error("[Agent] Failed to close expired conversation connection", error);
1023
- }
1024
- return super.destroy();
1025
- }
1026
- async deleteConversationCallback() {
1027
- if (await this.deleteConversation(this.name)) console.log("[Agent] Conversation deleted due to inactivity", {
1028
- conversationName: this.name,
1029
- retentionDays: this.conversationRetentionDays ?? null
1030
- });
1031
- }
1032
- async scheduleConversationForAutoDeletion() {
1033
- const retentionMs = getConversationRetentionMs(this.conversationRetentionDays);
1034
- if (retentionMs === null) return;
1035
- const scheduleIds = getDeleteConversationScheduleIds(this.getSchedules());
1036
- await Promise.all(scheduleIds.map((scheduleId) => this.cancelSchedule(scheduleId)));
1037
- await this.schedule(new Date(Date.now() + retentionMs), DELETE_CONVERSATION_CALLBACK);
1038
- }
1039
- };
1040
- //#endregion
1041
- //#region src/server/v1/agent-chat/ChatAgentHarness.ts
1042
- var ChatAgentHarness = class extends ChatAgent {
1043
- get binding() {
1044
- const className = this.constructor.name;
1045
- return this.env[className];
1046
- }
1047
- conversationRetentionDays = 90;
1048
- /**
1049
- * Returns the tools for the agent.
1050
- * @param ctx - The context object for the agent built from the request body.
1051
- * @returns The tools for the agent.
1052
- */
1053
- getTools(_ctx) {
1054
- return {};
1055
- }
1056
- /**
1057
- * Returns the skills for the agent.
1058
- * @param ctx - The context object for the agent built from the request body.
1059
- * @returns The skills for the agent.
1060
- */
1061
- getSkills(_ctx) {
1062
- return [];
1063
- }
1064
- async onChatMessage(onFinish, options) {
1065
- if (this._pendingUserContextRequest) await this._pendingUserContextRequest;
1066
- const ctx = options?.body;
1067
- return streamText(await this.buildLLMParams({
1068
- options,
1069
- onFinish,
1070
- model: this.getModel(ctx),
1071
- system: this.getSystemPrompt(ctx),
1072
- skills: this.getSkills(ctx),
1073
- tools: this.getTools(ctx)
1074
- })).toUIMessageStreamResponse({ sendSources: true });
1075
- }
1076
- };
1077
- //#endregion
1078
- export { Agent, ChatAgent, ChatAgentHarness, buildLLMParams, routeAgentRequest };