agents 0.8.6 → 0.8.7

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.
Files changed (40) hide show
  1. package/dist/client.d.ts +2 -2
  2. package/dist/compaction-helpers-BFTBIzpK.js +312 -0
  3. package/dist/compaction-helpers-BFTBIzpK.js.map +1 -0
  4. package/dist/compaction-helpers-DkJreaDR.d.ts +139 -0
  5. package/dist/{do-oauth-client-provider-D7F2Pw40.d.ts → do-oauth-client-provider-C2jurFjW.d.ts} +1 -1
  6. package/dist/{email-YAQhwwXb.d.ts → email-DwPlM0bQ.d.ts} +1 -1
  7. package/dist/email.d.ts +2 -2
  8. package/dist/experimental/forever.d.ts +1 -1
  9. package/dist/experimental/memory/session/index.d.ts +193 -203
  10. package/dist/experimental/memory/session/index.js +673 -294
  11. package/dist/experimental/memory/session/index.js.map +1 -1
  12. package/dist/experimental/memory/utils/index.d.ts +59 -0
  13. package/dist/experimental/memory/utils/index.js +69 -0
  14. package/dist/experimental/memory/utils/index.js.map +1 -0
  15. package/dist/{index-DynYigzs.d.ts → index-C-6EMK-E.d.ts} +11 -11
  16. package/dist/{index-OtkSCU2A.d.ts → index-Ua2Nfvbm.d.ts} +1 -1
  17. package/dist/index.d.ts +7 -5
  18. package/dist/index.js +1 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/{internal_context-DgcmHqS1.d.ts → internal_context-DT8RxmAN.d.ts} +1 -1
  21. package/dist/internal_context.d.ts +1 -1
  22. package/dist/mcp/client.d.ts +1 -1
  23. package/dist/mcp/do-oauth-client-provider.d.ts +1 -1
  24. package/dist/mcp/index.d.ts +1 -1
  25. package/dist/mcp/index.js +37 -1
  26. package/dist/mcp/index.js.map +1 -1
  27. package/dist/observability/index.d.ts +1 -1
  28. package/dist/react.d.ts +1 -1
  29. package/dist/{retries-JxhDYtYL.d.ts → retries-DXMQGhG3.d.ts} +1 -1
  30. package/dist/retries.d.ts +1 -1
  31. package/dist/{serializable-Ch19yA6_.d.ts → serializable-8Jt1B04R.d.ts} +1 -1
  32. package/dist/serializable.d.ts +1 -1
  33. package/dist/{types-2lHHE_uh.d.ts → types-C-m0II8i.d.ts} +3 -1
  34. package/dist/types.d.ts +1 -1
  35. package/dist/types.js +2 -0
  36. package/dist/types.js.map +1 -1
  37. package/dist/{workflow-types-BVKtSaA7.d.ts → workflow-types-CZNXKj_D.d.ts} +1 -1
  38. package/dist/workflow-types.d.ts +1 -1
  39. package/dist/workflows.d.ts +2 -2
  40. package/package.json +10 -5
@@ -1,376 +1,755 @@
1
- //#region src/experimental/memory/utils/compaction.ts
2
- /** Default thresholds for microCompaction rules (in chars) */
3
- const DEFAULTS = {
4
- truncateToolOutputs: 3e4,
5
- truncateText: 1e4,
6
- keepRecent: 4
7
- };
8
- /**
9
- * Parse microCompaction config into resolved rules.
10
- * Returns null if disabled.
11
- */
12
- function parseMicroCompactionRules(config) {
13
- if (config === false) return null;
14
- if (config === true) return {
15
- truncateToolOutputs: DEFAULTS.truncateToolOutputs,
16
- truncateText: DEFAULTS.truncateText,
17
- keepRecent: DEFAULTS.keepRecent
18
- };
19
- const keepRecent = config.keepRecent ?? DEFAULTS.keepRecent;
20
- if (!Number.isInteger(keepRecent) || keepRecent < 0) throw new Error("keepRecent must be a non-negative integer");
21
- const truncateToolOutputs = config.truncateToolOutputs === false ? false : config.truncateToolOutputs === true || config.truncateToolOutputs === void 0 ? DEFAULTS.truncateToolOutputs : config.truncateToolOutputs;
22
- if (typeof truncateToolOutputs === "number" && truncateToolOutputs <= 0) throw new Error("truncateToolOutputs must be a positive number");
23
- const truncateText = config.truncateText === false ? false : config.truncateText === true || config.truncateText === void 0 ? DEFAULTS.truncateText : config.truncateText;
24
- if (typeof truncateText === "number" && truncateText <= 0) throw new Error("truncateText must be a positive number");
25
- return {
26
- truncateToolOutputs,
27
- truncateText,
28
- keepRecent
29
- };
30
- }
1
+ import { MessageType } from "../../../types.js";
2
+ import { m as estimateStringTokens, p as estimateMessageTokens, t as COMPACTION_PREFIX } from "../../../compaction-helpers-BFTBIzpK.js";
3
+ import { jsonSchema } from "ai";
4
+ //#region src/experimental/memory/session/context.ts
31
5
  /**
32
- * Truncate oversized parts in a single message.
33
- * Returns the same reference if nothing changed (allows callers to skip no-op updates).
34
- */
35
- function truncateMessageParts(msg, rules) {
36
- let changed = false;
37
- const compactedParts = msg.parts.map((part) => {
38
- if (rules.truncateToolOutputs !== false && (part.type.startsWith("tool-") || part.type === "dynamic-tool") && "output" in part) {
39
- const toolPart = part;
40
- if (toolPart.output !== void 0) {
41
- const outputJson = JSON.stringify(toolPart.output);
42
- if (outputJson.length > rules.truncateToolOutputs) {
43
- changed = true;
44
- return {
45
- ...part,
46
- output: `[Truncated ${outputJson.length} bytes] ${outputJson.slice(0, 500)}...`
47
- };
48
- }
49
- }
50
- }
51
- if (rules.truncateText !== false && part.type === "text" && "text" in part) {
52
- const textPart = part;
53
- if (textPart.text.length > rules.truncateText) {
54
- changed = true;
55
- return {
56
- ...part,
57
- text: `${textPart.text.slice(0, rules.truncateText)}... [truncated ${textPart.text.length} chars]`
58
- };
59
- }
60
- }
61
- return part;
62
- });
63
- return changed ? {
64
- ...msg,
65
- parts: compactedParts
66
- } : msg;
67
- }
68
- /**
69
- * Apply microCompaction to an array of messages.
70
- * Returns same reference for unchanged messages (enables skip-update optimization).
6
+ * Context Block Management
71
7
  *
72
- * No keepRecent logic the caller decides which messages to pass.
8
+ * Persistent key-value blocks (MEMORY, USER, SOUL, etc.) that are:
9
+ * - Loaded from their providers at init
10
+ * - Frozen into a snapshot when toSystemPrompt() is called
11
+ * - Updated via setBlock() which writes to the provider immediately
12
+ * but does NOT update the frozen snapshot (preserves LLM prefix cache)
13
+ * - Re-snapshotted on next toSystemPrompt() call
73
14
  */
74
- function microCompact(messages, rules) {
75
- return messages.map((msg) => truncateMessageParts(msg, rules));
76
- }
77
- /** Approximate token multiplier per whitespace-separated word */
78
- const WORDS_TOKEN_MULTIPLIER = 1.3;
79
15
  /**
80
- * Estimate token count for a string using a hybrid heuristic.
81
- *
82
- * Takes the max of two estimates:
83
- * - Character-based: `length / 4` — better for dense content (JSON, code, URLs)
84
- * - Word-based: `words * 1.3` — better for natural language prose
85
- *
86
- * This is a heuristic. Do not use where exact counts are required.
16
+ * Manages context blocks with frozen snapshot support.
87
17
  */
88
- function estimateStringTokens(text) {
89
- if (!text) return 0;
90
- const charEstimate = text.length / 4;
91
- const wordEstimate = text.split(/\s+/).filter(Boolean).length * WORDS_TOKEN_MULTIPLIER;
92
- return Math.ceil(Math.max(charEstimate, wordEstimate));
93
- }
94
- /**
95
- * Estimate total token count for an array of UIMessages.
96
- *
97
- * Walks each message's parts (text, tool invocations, tool results)
98
- * and applies per-message overhead.
99
- *
100
- * This is a heuristic. Do not use where exact counts are required.
101
- */
102
- function estimateMessageTokens(messages) {
103
- let tokens = 0;
104
- for (const msg of messages) {
105
- tokens += 4;
106
- for (const part of msg.parts) if (part.type === "text") tokens += estimateStringTokens(part.text);
107
- else if (part.type.startsWith("tool-") || part.type === "dynamic-tool") {
108
- const toolPart = part;
109
- if (toolPart.input) tokens += estimateStringTokens(JSON.stringify(toolPart.input));
110
- if (toolPart.output) tokens += estimateStringTokens(JSON.stringify(toolPart.output));
18
+ var ContextBlocks = class {
19
+ constructor(configs, promptStore) {
20
+ this.blocks = /* @__PURE__ */ new Map();
21
+ this.snapshot = null;
22
+ this.loaded = false;
23
+ this.configs = configs;
24
+ this.promptStore = promptStore ?? null;
25
+ }
26
+ isLoaded() {
27
+ return this.loaded;
28
+ }
29
+ /**
30
+ * Load all blocks from their providers.
31
+ * Called once at session init.
32
+ */
33
+ async load() {
34
+ for (const config of this.configs) {
35
+ let content = null;
36
+ if (config.provider) content = await config.provider.get();
37
+ content = content ?? config.initialContent ?? "";
38
+ this.blocks.set(config.label, {
39
+ label: config.label,
40
+ description: config.description,
41
+ content,
42
+ tokens: estimateStringTokens(content),
43
+ maxTokens: config.maxTokens,
44
+ readonly: config.readonly
45
+ });
111
46
  }
47
+ this.loaded = true;
112
48
  }
113
- return tokens;
114
- }
115
- //#endregion
116
- //#region src/experimental/memory/session/session.ts
117
- var Session = class {
118
- constructor(storage, options) {
119
- this.storage = storage;
120
- this.microCompactionRules = parseMicroCompactionRules(options?.microCompaction ?? true);
121
- this.compactionConfig = options?.compaction ?? null;
49
+ /**
50
+ * Get a block by label.
51
+ */
52
+ getBlock(label) {
53
+ return this.blocks.get(label) ?? null;
122
54
  }
123
- getMessages(options) {
124
- return this.storage.getMessages(options);
55
+ /**
56
+ * Get all blocks.
57
+ */
58
+ getBlocks() {
59
+ return Array.from(this.blocks.values());
125
60
  }
126
- getMessage(id) {
127
- return this.storage.getMessage(id);
61
+ /**
62
+ * Set block content. Writes to provider immediately.
63
+ * Does NOT update the frozen snapshot.
64
+ */
65
+ async setBlock(label, content) {
66
+ if (!this.loaded) await this.load();
67
+ const config = this.configs.find((c) => c.label === label);
68
+ const existing = this.blocks.get(label);
69
+ if (existing?.readonly || config?.readonly) throw new Error(`Block "${label}" is readonly`);
70
+ const tokens = estimateStringTokens(content);
71
+ const maxTokens = config?.maxTokens ?? existing?.maxTokens;
72
+ if (maxTokens !== void 0 && tokens > maxTokens) throw new Error(`Block "${label}" exceeds maxTokens: ${tokens} > ${maxTokens}`);
73
+ const block = {
74
+ label,
75
+ description: config?.description ?? existing?.description,
76
+ content,
77
+ tokens,
78
+ maxTokens,
79
+ readonly: false
80
+ };
81
+ this.blocks.set(label, block);
82
+ if (config?.provider?.set) await config.provider.set(content);
83
+ return block;
128
84
  }
129
- getLastMessages(n) {
130
- return this.storage.getLastMessages(n);
85
+ /**
86
+ * Append content to a block.
87
+ */
88
+ async appendToBlock(label, content) {
89
+ if (!this.loaded) await this.load();
90
+ const existing = this.blocks.get(label);
91
+ if (!existing) throw new Error(`Block "${label}" not found`);
92
+ return this.setBlock(label, existing.content + content);
131
93
  }
132
- async append(messages) {
133
- await this.storage.appendMessages(messages);
134
- if (this.shouldAutoCompact()) {
135
- if ((await this.compact()).success) return;
136
- }
137
- if (this.microCompactionRules) {
138
- const rules = this.microCompactionRules;
139
- const older = this.storage.getOlderMessages(rules.keepRecent);
140
- if (older.length > 0) {
141
- const compacted = microCompact(older, rules);
142
- for (let i = 0; i < older.length; i++) if (compacted[i] !== older[i]) this.storage.updateMessage(compacted[i]);
94
+ /**
95
+ * Get the system prompt string with context blocks.
96
+ *
97
+ * Returns a frozen snapshot: first call renders and caches,
98
+ * subsequent calls return the same string (preserves LLM prefix cache).
99
+ * Call refreshSnapshot() to re-render after block changes take effect.
100
+ */
101
+ toSystemPrompt() {
102
+ if (!this.loaded) throw new Error("Context blocks not loaded. Call load() first.");
103
+ if (this.snapshot !== null) return this.snapshot;
104
+ return this.captureSnapshot();
105
+ }
106
+ /**
107
+ * Force re-render the snapshot from current block state.
108
+ * Call this at the start of a new session to pick up changes
109
+ * made by setBlock() during the previous session.
110
+ */
111
+ refreshSnapshot() {
112
+ return this.captureSnapshot();
113
+ }
114
+ captureSnapshot() {
115
+ const parts = [];
116
+ const sep = "═".repeat(46);
117
+ for (const block of this.blocks.values()) {
118
+ if (!block.content) continue;
119
+ let header = block.label.toUpperCase();
120
+ if (block.description) header += ` (${block.description})`;
121
+ if (block.maxTokens) {
122
+ const pct = Math.round(block.tokens / block.maxTokens * 100);
123
+ header += ` [${pct}% — ${block.tokens}/${block.maxTokens} tokens]`;
143
124
  }
125
+ if (block.readonly) header += " [readonly]";
126
+ parts.push(`${sep}\n${header}\n${sep}\n${block.content}`);
144
127
  }
128
+ this.snapshot = parts.join("\n\n");
129
+ return this.snapshot;
145
130
  }
146
- updateMessage(message) {
147
- this.storage.updateMessage(message);
148
- }
149
- deleteMessages(messageIds) {
150
- this.storage.deleteMessages(messageIds);
131
+ /**
132
+ * Get writable blocks (for tool description).
133
+ */
134
+ getWritableBlocks() {
135
+ return Array.from(this.blocks.values()).filter((b) => !b.readonly);
151
136
  }
152
- clearMessages() {
153
- this.storage.clearMessages();
137
+ /**
138
+ * Get writable block configs — doesn't require blocks to be loaded.
139
+ */
140
+ getWritableConfigs() {
141
+ return this.configs.filter((c) => !c.readonly);
154
142
  }
155
- async compact() {
156
- const messages = this.storage.getMessages();
157
- if (messages.length === 0) return { success: true };
158
- try {
159
- let result = messages;
160
- if (this.compactionConfig?.fn) result = await this.compactionConfig.fn(result);
161
- await this.storage.replaceMessages(result);
162
- return { success: true };
163
- } catch (err) {
164
- return {
165
- success: false,
166
- error: err instanceof Error ? err.message : String(err)
167
- };
143
+ /**
144
+ * Frozen system prompt. On first call:
145
+ * 1. Checks store for a persisted prompt (survives DO eviction)
146
+ * 2. If none, loads blocks from providers, renders, and persists
147
+ *
148
+ * Subsequent calls return the stored version — true prefix cache stability.
149
+ */
150
+ async freezeSystemPrompt() {
151
+ if (this.promptStore) {
152
+ const stored = await this.promptStore.get();
153
+ if (stored !== null) return stored;
168
154
  }
155
+ if (!this.loaded) await this.load();
156
+ const prompt = this.toSystemPrompt();
157
+ if (this.promptStore?.set) await this.promptStore.set(prompt);
158
+ return prompt;
169
159
  }
170
160
  /**
171
- * Pre-check for auto-compaction using token estimate heuristic.
161
+ * Re-render the system prompt from current block state and persist.
162
+ * Call after compaction or at session boundaries to pick up writes.
172
163
  */
173
- shouldAutoCompact() {
174
- if (!this.compactionConfig?.tokenThreshold) return false;
175
- return estimateMessageTokens(this.storage.getMessages()) > this.compactionConfig.tokenThreshold;
164
+ async refreshSystemPrompt() {
165
+ if (!this.loaded) await this.load();
166
+ const prompt = this.refreshSnapshot();
167
+ if (this.promptStore?.set) await this.promptStore.set(prompt);
168
+ return prompt;
169
+ }
170
+ /**
171
+ * AI tool for updating context blocks. Loads blocks lazily on first execute.
172
+ */
173
+ async tools() {
174
+ if (!this.loaded) await this.load();
175
+ const writable = this.getWritableBlocks();
176
+ if (writable.length === 0) return {};
177
+ const blockDescriptions = writable.map((b) => `- "${b.label}": ${b.description ?? "no description"}`).join("\n");
178
+ const ctx = this;
179
+ return { update_context: {
180
+ description: `Update a context block. Available blocks:\n${blockDescriptions}\n\nWrites are durable and persist across sessions.`,
181
+ inputSchema: jsonSchema({
182
+ type: "object",
183
+ properties: {
184
+ label: {
185
+ type: "string",
186
+ enum: writable.map((b) => b.label),
187
+ description: "Block label to update"
188
+ },
189
+ content: {
190
+ type: "string",
191
+ description: "Content to write"
192
+ },
193
+ action: {
194
+ type: "string",
195
+ enum: ["replace", "append"],
196
+ description: "replace (default) or append"
197
+ }
198
+ },
199
+ required: ["label", "content"]
200
+ }),
201
+ execute: async ({ label, content, action }) => {
202
+ try {
203
+ const block = action === "append" ? await ctx.appendToBlock(label, content) : await ctx.setBlock(label, content);
204
+ return `Written to ${label}. Usage: ${block.maxTokens ? `${Math.round(block.tokens / block.maxTokens * 100)}% (${block.tokens}/${block.maxTokens} tokens)` : `${block.tokens} tokens`}`;
205
+ } catch (err) {
206
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
207
+ }
208
+ }
209
+ } };
176
210
  }
177
211
  };
178
212
  //#endregion
179
213
  //#region src/experimental/memory/session/providers/agent.ts
180
- /**
181
- * Session provider that wraps an Agent's SQLite storage.
182
- * Provides pure CRUD — compaction is handled by the Session wrapper.
183
- *
184
- * @example
185
- * ```typescript
186
- * import { Session, AgentSessionProvider } from "agents/experimental/memory/session";
187
- *
188
- * // In your Agent class:
189
- * session = new Session(new AgentSessionProvider(this));
190
- *
191
- * // With compaction options:
192
- * session = new Session(new AgentSessionProvider(this), {
193
- * microCompaction: { truncateToolOutputs: 2000, keepRecent: 10 },
194
- * compaction: { tokenThreshold: 20000, fn: summarize }
195
- * });
196
- * ```
197
- */
198
214
  var AgentSessionProvider = class {
199
- constructor(agent) {
215
+ /**
216
+ * @param agent - Agent or any object with a `sql` tagged template method
217
+ * @param sessionId - Optional session ID to isolate multiple sessions in the same DO.
218
+ * Messages are filtered by session_id within shared tables.
219
+ */
220
+ constructor(agent, sessionId) {
200
221
  this.initialized = false;
201
222
  this.agent = agent;
223
+ this.sessionId = sessionId ?? "";
202
224
  }
203
- /**
204
- * Ensure the messages table exists
205
- */
206
225
  ensureTable() {
207
226
  if (this.initialized) return;
208
227
  this.agent.sql`
209
- CREATE TABLE IF NOT EXISTS cf_agents_session_messages (
228
+ CREATE TABLE IF NOT EXISTS assistant_messages (
229
+ id TEXT PRIMARY KEY,
230
+ session_id TEXT NOT NULL DEFAULT '',
231
+ parent_id TEXT,
232
+ role TEXT NOT NULL,
233
+ content TEXT NOT NULL,
234
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
235
+ )
236
+ `;
237
+ this.agent.sql`
238
+ CREATE INDEX IF NOT EXISTS idx_assistant_msg_parent
239
+ ON assistant_messages(parent_id)
240
+ `;
241
+ this.agent.sql`
242
+ CREATE INDEX IF NOT EXISTS idx_assistant_msg_session
243
+ ON assistant_messages(session_id)
244
+ `;
245
+ this.agent.sql`
246
+ CREATE TABLE IF NOT EXISTS assistant_compactions (
210
247
  id TEXT PRIMARY KEY,
211
- message TEXT NOT NULL,
248
+ session_id TEXT NOT NULL DEFAULT '',
249
+ summary TEXT NOT NULL,
250
+ from_message_id TEXT NOT NULL,
251
+ to_message_id TEXT NOT NULL,
212
252
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
213
253
  )
254
+ `;
255
+ this.agent.sql`
256
+ CREATE VIRTUAL TABLE IF NOT EXISTS assistant_fts
257
+ USING fts5(id UNINDEXED, session_id UNINDEXED, role UNINDEXED, content, tokenize='porter unicode61')
258
+ `;
259
+ this.agent.sql`
260
+ CREATE TABLE IF NOT EXISTS assistant_config (
261
+ session_id TEXT NOT NULL,
262
+ key TEXT NOT NULL,
263
+ value TEXT NOT NULL,
264
+ PRIMARY KEY (session_id, key)
265
+ )
214
266
  `;
215
267
  this.initialized = true;
216
268
  }
217
- /**
218
- * Get all messages in AI SDK format
219
- */
220
- getMessages(options) {
269
+ getMessage(id) {
270
+ this.ensureTable();
271
+ const rows = this.agent.sql`
272
+ SELECT content FROM assistant_messages WHERE id = ${id} AND session_id = ${this.sessionId}
273
+ `;
274
+ return rows.length > 0 ? this.parse(rows[0].content) : null;
275
+ }
276
+ getHistory(leafId) {
277
+ this.ensureTable();
278
+ const leaf = leafId ? this.agent.sql`
279
+ SELECT id FROM assistant_messages WHERE id = ${leafId} AND session_id = ${this.sessionId}
280
+ `[0] : this.latestLeafRow();
281
+ if (!leaf) return [];
282
+ const path = this.agent.sql`
283
+ WITH RECURSIVE path AS (
284
+ SELECT *, 0 as depth FROM assistant_messages WHERE id = ${leaf.id}
285
+ UNION ALL
286
+ SELECT m.*, p.depth + 1 FROM assistant_messages m
287
+ JOIN path p ON m.id = p.parent_id
288
+ WHERE m.session_id = ${this.sessionId} AND p.depth < 10000
289
+ )
290
+ SELECT content FROM path ORDER BY depth DESC
291
+ `;
292
+ const messages = this.parseRows(path);
293
+ const compactions = this.getCompactions();
294
+ if (compactions.length === 0) return messages;
295
+ return this.applyCompactions(messages, compactions);
296
+ }
297
+ getLatestLeaf() {
298
+ this.ensureTable();
299
+ const row = this.latestLeafRow();
300
+ return row ? this.parse(row.content) : null;
301
+ }
302
+ getBranches(messageId) {
221
303
  this.ensureTable();
222
- if (options?.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 0)) throw new Error("limit must be a non-negative integer");
223
- if (options?.offset !== void 0 && (!Number.isInteger(options.offset) || options.offset < 0)) throw new Error("offset must be a non-negative integer");
224
- const role = options?.role ?? null;
225
- const before = options?.before?.toISOString() ?? null;
226
- const after = options?.after?.toISOString() ?? null;
227
- const limit = options?.limit ?? -1;
228
- const offset = options?.offset ?? 0;
229
304
  const rows = this.agent.sql`
230
- SELECT id, message, created_at FROM cf_agents_session_messages
231
- WHERE (${role} IS NULL OR json_extract(message, '$.role') = ${role})
232
- AND (${before} IS NULL OR created_at < ${before})
233
- AND (${after} IS NULL OR created_at > ${after})
234
- ORDER BY created_at ASC, rowid ASC
235
- LIMIT ${limit} OFFSET ${offset}
305
+ SELECT content FROM assistant_messages
306
+ WHERE parent_id = ${messageId} AND session_id = ${this.sessionId} ORDER BY created_at ASC
236
307
  `;
237
308
  return this.parseRows(rows);
238
309
  }
239
- /**
240
- * Append one or more messages to storage.
241
- */
242
- async appendMessages(messages) {
310
+ getPathLength(leafId) {
243
311
  this.ensureTable();
244
- const messageArray = Array.isArray(messages) ? messages : [messages];
245
- const now = (/* @__PURE__ */ new Date()).toISOString();
246
- for (const message of messageArray) {
247
- const json = JSON.stringify(message);
248
- this.agent.sql`
249
- INSERT INTO cf_agents_session_messages (id, message, created_at)
250
- VALUES (${message.id}, ${json}, ${now})
251
- ON CONFLICT(id) DO UPDATE SET message = excluded.message
252
- `;
312
+ const leaf = leafId ? this.agent.sql`
313
+ SELECT id FROM assistant_messages WHERE id = ${leafId} AND session_id = ${this.sessionId}
314
+ `[0] : this.latestLeafRow();
315
+ if (!leaf) return 0;
316
+ return this.agent.sql`
317
+ WITH RECURSIVE path AS (
318
+ SELECT id, parent_id, 0 as depth FROM assistant_messages WHERE id = ${leaf.id}
319
+ UNION ALL
320
+ SELECT m.id, m.parent_id, p.depth + 1 FROM assistant_messages m
321
+ JOIN path p ON m.id = p.parent_id
322
+ WHERE m.session_id = ${this.sessionId} AND p.depth < 10000
323
+ )
324
+ SELECT COUNT(*) as count FROM path
325
+ `[0]?.count ?? 0;
326
+ }
327
+ appendMessage(message, parentId) {
328
+ this.ensureTable();
329
+ if (this.agent.sql`
330
+ SELECT id FROM assistant_messages WHERE id = ${message.id} AND session_id = ${this.sessionId}
331
+ `.length > 0) return;
332
+ let parent = parentId ?? this.latestLeafRow()?.id ?? null;
333
+ if (parent) {
334
+ if (this.agent.sql`
335
+ SELECT id FROM assistant_messages WHERE id = ${parent} AND session_id = ${this.sessionId}
336
+ `.length === 0) parent = null;
253
337
  }
338
+ const json = JSON.stringify(message);
339
+ this.agent.sql`
340
+ INSERT INTO assistant_messages (id, session_id, parent_id, role, content)
341
+ VALUES (${message.id}, ${this.sessionId}, ${parent}, ${message.role}, ${json})
342
+ `;
343
+ this.indexFTS(message);
254
344
  }
255
- /**
256
- * Update an existing message
257
- */
258
345
  updateMessage(message) {
259
346
  this.ensureTable();
260
- const json = JSON.stringify(message);
261
347
  this.agent.sql`
262
- UPDATE cf_agents_session_messages
263
- SET message = ${json}
264
- WHERE id = ${message.id}
348
+ UPDATE assistant_messages SET content = ${JSON.stringify(message)}
349
+ WHERE id = ${message.id} AND session_id = ${this.sessionId}
265
350
  `;
351
+ this.indexFTS(message);
266
352
  }
267
- /**
268
- * Delete messages by their IDs
269
- */
270
353
  deleteMessages(messageIds) {
271
354
  this.ensureTable();
272
- for (const id of messageIds) this.agent.sql`DELETE FROM cf_agents_session_messages WHERE id = ${id}`;
355
+ for (const id of messageIds) {
356
+ this.agent.sql`DELETE FROM assistant_messages WHERE id = ${id} AND session_id = ${this.sessionId}`;
357
+ this.deleteFTS(id);
358
+ }
273
359
  }
274
- /**
275
- * Clear all messages from the session
276
- */
277
360
  clearMessages() {
278
361
  this.ensureTable();
279
- this.agent.sql`DELETE FROM cf_agents_session_messages`;
362
+ this.agent.sql`DELETE FROM assistant_messages WHERE session_id = ${this.sessionId}`;
363
+ this.agent.sql`DELETE FROM assistant_compactions WHERE session_id = ${this.sessionId}`;
364
+ const ftsRows = this.agent.sql`
365
+ SELECT rowid FROM assistant_fts WHERE session_id = ${this.sessionId}
366
+ `;
367
+ for (const row of ftsRows) this.agent.sql`DELETE FROM assistant_fts WHERE rowid = ${row.rowid}`;
280
368
  }
281
- /**
282
- * Get a single message by ID
283
- */
284
- getMessage(id) {
369
+ addCompaction(summary, fromMessageId, toMessageId) {
285
370
  this.ensureTable();
286
- const rows = this.agent.sql`
287
- SELECT message FROM cf_agents_session_messages WHERE id = ${id}
371
+ const id = crypto.randomUUID();
372
+ this.agent.sql`
373
+ INSERT INTO assistant_compactions (id, session_id, summary, from_message_id, to_message_id)
374
+ VALUES (${id}, ${this.sessionId}, ${summary}, ${fromMessageId}, ${toMessageId})
288
375
  `;
289
- if (rows.length === 0) return null;
376
+ return {
377
+ id,
378
+ summary,
379
+ fromMessageId,
380
+ toMessageId,
381
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
382
+ };
383
+ }
384
+ getCompactions() {
385
+ this.ensureTable();
386
+ return this.agent.sql`
387
+ SELECT * FROM assistant_compactions WHERE session_id = ${this.sessionId} ORDER BY created_at ASC
388
+ `.map((r) => ({
389
+ id: r.id,
390
+ summary: r.summary,
391
+ fromMessageId: r.from_message_id,
392
+ toMessageId: r.to_message_id,
393
+ createdAt: r.created_at
394
+ }));
395
+ }
396
+ searchMessages(query, limit = 20) {
397
+ this.ensureTable();
398
+ const sanitized = `"${query.replace(/"/g, "\"\"")}"`;
290
399
  try {
291
- const parsed = JSON.parse(rows[0].message);
292
- return this.isValidMessage(parsed) ? parsed : null;
400
+ return this.agent.sql`
401
+ SELECT f.id, f.role, f.content FROM assistant_fts f
402
+ INNER JOIN assistant_messages m ON m.id = f.id AND m.session_id = f.session_id
403
+ WHERE assistant_fts MATCH ${sanitized} AND f.session_id = ${this.sessionId}
404
+ ORDER BY rank LIMIT ${limit}
405
+ `.map((r) => ({
406
+ id: r.id,
407
+ role: r.role,
408
+ content: r.content
409
+ }));
293
410
  } catch {
294
- return null;
411
+ return [];
295
412
  }
296
413
  }
297
- /**
298
- * Get the last N messages (most recent)
299
- */
300
- getLastMessages(n) {
301
- this.ensureTable();
414
+ latestLeafRow() {
415
+ return this.agent.sql`
416
+ SELECT m.id, m.content FROM assistant_messages m
417
+ LEFT JOIN assistant_messages c ON c.parent_id = m.id AND c.session_id = ${this.sessionId}
418
+ WHERE c.id IS NULL AND m.session_id = ${this.sessionId}
419
+ ORDER BY m.created_at DESC, m.rowid DESC LIMIT 1
420
+ `[0] ?? null;
421
+ }
422
+ indexFTS(message) {
423
+ const text = message.parts.filter((p) => p.type === "text").map((p) => p.text).join(" ");
424
+ this.deleteFTS(message.id);
425
+ if (text) this.agent.sql`
426
+ INSERT INTO assistant_fts (id, session_id, role, content)
427
+ VALUES (${message.id}, ${this.sessionId}, ${message.role}, ${text})
428
+ `;
429
+ }
430
+ deleteFTS(id) {
302
431
  const rows = this.agent.sql`
303
- SELECT message FROM cf_agents_session_messages
304
- ORDER BY created_at DESC, rowid DESC
305
- LIMIT ${n}
432
+ SELECT rowid FROM assistant_fts WHERE id = ${id} AND session_id = ${this.sessionId}
306
433
  `;
307
- return this.parseRows([...rows].reverse());
434
+ for (const row of rows) this.agent.sql`DELETE FROM assistant_fts WHERE rowid = ${row.rowid}`;
308
435
  }
309
- /**
310
- * Fetch messages outside the recent window (for microCompaction).
311
- * Returns all messages except the most recent `keepRecent`.
312
- */
313
- getOlderMessages(keepRecent) {
314
- this.ensureTable();
315
- const rows = this.agent.sql`
316
- SELECT id, message FROM cf_agents_session_messages
317
- WHERE rowid NOT IN (
318
- SELECT rowid FROM cf_agents_session_messages
319
- ORDER BY created_at DESC, rowid DESC
320
- LIMIT ${keepRecent}
436
+ applyCompactions(messages, compactions) {
437
+ const ids = messages.map((m) => m.id);
438
+ const result = [];
439
+ let i = 0;
440
+ while (i < messages.length) {
441
+ const matching = compactions.filter((c) => c.fromMessageId === ids[i]);
442
+ const comp = matching.length > 1 ? matching[matching.length - 1] : matching[0];
443
+ if (comp) {
444
+ const endIdx = ids.indexOf(comp.toMessageId);
445
+ if (endIdx >= i) {
446
+ result.push({
447
+ id: `${COMPACTION_PREFIX}${comp.id}`,
448
+ role: "assistant",
449
+ parts: [{
450
+ type: "text",
451
+ text: comp.summary
452
+ }],
453
+ createdAt: /* @__PURE__ */ new Date()
454
+ });
455
+ i = endIdx + 1;
456
+ continue;
457
+ }
458
+ }
459
+ result.push(messages[i]);
460
+ i++;
461
+ }
462
+ return result;
463
+ }
464
+ parse(json) {
465
+ try {
466
+ const msg = JSON.parse(json);
467
+ if (typeof msg?.id === "string" && typeof msg?.role === "string" && Array.isArray(msg?.parts)) return msg;
468
+ } catch {}
469
+ return null;
470
+ }
471
+ parseRows(rows) {
472
+ const result = [];
473
+ for (const row of rows) {
474
+ const msg = this.parse(row.content);
475
+ if (msg) result.push(msg);
476
+ }
477
+ return result;
478
+ }
479
+ };
480
+ //#endregion
481
+ //#region src/experimental/memory/session/providers/agent-context.ts
482
+ var AgentContextProvider = class {
483
+ constructor(agent, label) {
484
+ this.initialized = false;
485
+ this.agent = agent;
486
+ this.label = label;
487
+ }
488
+ ensureTable() {
489
+ if (this.initialized) return;
490
+ this.agent.sql`
491
+ CREATE TABLE IF NOT EXISTS cf_agents_context_blocks (
492
+ label TEXT PRIMARY KEY,
493
+ content TEXT NOT NULL,
494
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
321
495
  )
322
496
  `;
323
- return this.parseRows(rows);
497
+ this.initialized = true;
324
498
  }
325
- /**
326
- * Bulk replace all messages.
327
- * Preserves original created_at timestamps for surviving messages.
328
- */
329
- async replaceMessages(messages) {
499
+ async get() {
500
+ this.ensureTable();
501
+ return this.agent.sql`
502
+ SELECT content FROM cf_agents_context_blocks WHERE label = ${this.label}
503
+ `[0]?.content ?? null;
504
+ }
505
+ async set(content) {
330
506
  this.ensureTable();
331
- const existingRows = this.agent.sql`
332
- SELECT id, created_at FROM cf_agents_session_messages
507
+ this.agent.sql`
508
+ INSERT INTO cf_agents_context_blocks (label, content)
509
+ VALUES (${this.label}, ${content})
510
+ ON CONFLICT(label) DO UPDATE SET content = ${content}, updated_at = CURRENT_TIMESTAMP
333
511
  `;
334
- const timestampMap = /* @__PURE__ */ new Map();
335
- for (const row of existingRows) timestampMap.set(row.id, row.created_at);
336
- this.agent.sql`DELETE FROM cf_agents_session_messages`;
337
- const now = (/* @__PURE__ */ new Date()).toISOString();
338
- for (const message of messages) {
339
- const json = JSON.stringify(message);
340
- const created_at = timestampMap.get(message.id) ?? now;
341
- this.agent.sql`
342
- INSERT INTO cf_agents_session_messages (id, message, created_at)
343
- VALUES (${message.id}, ${json}, ${created_at})
344
- ON CONFLICT(id) DO UPDATE SET message = excluded.message
345
- `;
346
- }
512
+ }
513
+ };
514
+ //#endregion
515
+ //#region src/experimental/memory/session/session.ts
516
+ function isBroadcaster(obj) {
517
+ return typeof obj === "object" && obj !== null && "broadcast" in obj && typeof obj.broadcast === "function";
518
+ }
519
+ var Session = class Session {
520
+ constructor(storage, options) {
521
+ this._ready = false;
522
+ this.storage = storage;
523
+ this.context = new ContextBlocks(options?.context ?? [], options?.promptStore);
524
+ this._ready = true;
347
525
  }
348
526
  /**
349
- * Validate message structure
527
+ * Chainable session creation with auto-wired SQLite providers.
528
+ * Chain methods in any order — providers are resolved lazily on first use.
529
+ *
530
+ * @example
531
+ * ```ts
532
+ * const session = Session.create(this)
533
+ * .withContext("soul", { initialContent: "You are helpful.", readonly: true })
534
+ * .withContext("memory", { description: "Learned facts", maxTokens: 1100 })
535
+ * .withCachedPrompt();
536
+ *
537
+ * // Custom storage (R2, KV, etc.)
538
+ * const session = Session.create(this)
539
+ * .withContext("workspace", {
540
+ * provider: {
541
+ * get: () => env.BUCKET.get("ws.md").then(o => o?.text() ?? null),
542
+ * set: (c) => env.BUCKET.put("ws.md", c),
543
+ * }
544
+ * })
545
+ * .withCachedPrompt();
546
+ * ```
350
547
  */
351
- isValidMessage(msg) {
352
- if (typeof msg !== "object" || msg === null) return false;
353
- const m = msg;
354
- if (typeof m.id !== "string" || m.id.length === 0) return false;
355
- if (m.role !== "user" && m.role !== "assistant" && m.role !== "system") return false;
356
- if (!Array.isArray(m.parts)) return false;
357
- return true;
548
+ static create(agent) {
549
+ const session = Object.create(Session.prototype);
550
+ session._agent = agent;
551
+ if (isBroadcaster(agent)) session._broadcaster = agent;
552
+ session._pending = [];
553
+ session._ready = false;
554
+ return session;
555
+ }
556
+ forSession(sessionId) {
557
+ this._sessionId = sessionId;
558
+ return this;
559
+ }
560
+ withContext(label, options) {
561
+ this._pending.push({
562
+ label,
563
+ options: options ?? {}
564
+ });
565
+ return this;
566
+ }
567
+ withCachedPrompt(provider) {
568
+ this._cachedPrompt = provider ?? true;
569
+ return this;
358
570
  }
359
571
  /**
360
- * Parse message rows from SQL results into UIMessages.
572
+ * Register a compaction function. Called by `compact()` to compress
573
+ * message history into a summary overlay.
361
574
  */
362
- parseRows(rows) {
363
- const messages = [];
364
- for (const row of rows) try {
365
- const parsed = JSON.parse(row.message);
366
- if (this.isValidMessage(parsed)) messages.push(parsed);
367
- } catch {
368
- if (row.id) console.warn(`[AgentSessionProvider] Skipping malformed message ${row.id}`);
575
+ onCompaction(fn) {
576
+ this._compactionFn = fn;
577
+ return this;
578
+ }
579
+ /**
580
+ * Auto-compact when estimated token count exceeds the threshold.
581
+ * Checked after each `appendMessage`. Requires `onCompaction()`.
582
+ */
583
+ compactAfter(tokenThreshold) {
584
+ this._tokenThreshold = tokenThreshold;
585
+ return this;
586
+ }
587
+ _ensureReady() {
588
+ if (this._ready) return;
589
+ const configs = (this._pending ?? []).map(({ label, options: opts }) => {
590
+ let provider = opts.provider;
591
+ if (!provider && !opts.readonly) {
592
+ const key = this._sessionId ? `${label}_${this._sessionId}` : label;
593
+ provider = new AgentContextProvider(this._agent, key);
594
+ }
595
+ return {
596
+ label,
597
+ description: opts.description,
598
+ initialContent: opts.initialContent,
599
+ maxTokens: opts.maxTokens,
600
+ readonly: opts.readonly,
601
+ provider
602
+ };
603
+ });
604
+ let promptStore;
605
+ if (this._cachedPrompt === true) {
606
+ const key = this._sessionId ? `_system_prompt_${this._sessionId}` : "_system_prompt";
607
+ promptStore = new AgentContextProvider(this._agent, key);
608
+ } else if (this._cachedPrompt) promptStore = this._cachedPrompt;
609
+ this.storage = new AgentSessionProvider(this._agent, this._sessionId);
610
+ this.context = new ContextBlocks(configs, promptStore);
611
+ this._ready = true;
612
+ }
613
+ getHistory(leafId) {
614
+ this._ensureReady();
615
+ return this.storage.getHistory(leafId);
616
+ }
617
+ getMessage(id) {
618
+ this._ensureReady();
619
+ return this.storage.getMessage(id);
620
+ }
621
+ getLatestLeaf() {
622
+ this._ensureReady();
623
+ return this.storage.getLatestLeaf();
624
+ }
625
+ getBranches(messageId) {
626
+ this._ensureReady();
627
+ return this.storage.getBranches(messageId);
628
+ }
629
+ getPathLength(leafId) {
630
+ this._ensureReady();
631
+ return this.storage.getPathLength(leafId);
632
+ }
633
+ _broadcast(type, data) {
634
+ if (!this._broadcaster) return;
635
+ this._broadcaster.broadcast(JSON.stringify({
636
+ type,
637
+ ...data
638
+ }));
639
+ }
640
+ _emitStatus(phase, extra) {
641
+ const tokenEstimate = estimateMessageTokens(this.getHistory());
642
+ this._broadcast(MessageType.CF_AGENT_SESSION, {
643
+ phase,
644
+ tokenEstimate,
645
+ tokenThreshold: this._tokenThreshold ?? null,
646
+ ...extra
647
+ });
648
+ return tokenEstimate;
649
+ }
650
+ _emitError(error) {
651
+ this._broadcast(MessageType.CF_AGENT_SESSION_ERROR, { error });
652
+ }
653
+ async appendMessage(message, parentId) {
654
+ this._ensureReady();
655
+ this.storage.appendMessage(message, parentId);
656
+ const tokenEstimate = this._emitStatus("idle");
657
+ if (this._tokenThreshold != null && this._compactionFn && tokenEstimate > this._tokenThreshold) try {
658
+ await this.compact();
659
+ } catch {}
660
+ }
661
+ updateMessage(message) {
662
+ this._ensureReady();
663
+ this.storage.updateMessage(message);
664
+ this._emitStatus("idle");
665
+ }
666
+ deleteMessages(messageIds) {
667
+ this._ensureReady();
668
+ this.storage.deleteMessages(messageIds);
669
+ this._emitStatus("idle");
670
+ }
671
+ clearMessages() {
672
+ this._ensureReady();
673
+ this.storage.clearMessages();
674
+ this._emitStatus("idle");
675
+ }
676
+ addCompaction(summary, fromMessageId, toMessageId) {
677
+ this._ensureReady();
678
+ return this.storage.addCompaction(summary, fromMessageId, toMessageId);
679
+ }
680
+ getCompactions() {
681
+ this._ensureReady();
682
+ return this.storage.getCompactions();
683
+ }
684
+ /**
685
+ * Run the registered compaction function and store the result as an overlay.
686
+ * Requires `onCompaction()` to be called first.
687
+ */
688
+ async compact() {
689
+ this._ensureReady();
690
+ if (!this._compactionFn) throw new Error("No compaction function registered. Call onCompaction() first.");
691
+ const tokensBefore = this._emitStatus("compacting");
692
+ let result;
693
+ try {
694
+ result = await this._compactionFn(this.getHistory());
695
+ } catch (err) {
696
+ this._emitError(err instanceof Error ? err.message : String(err));
697
+ return null;
698
+ }
699
+ if (!result) {
700
+ this._emitStatus("idle");
701
+ return null;
369
702
  }
370
- return messages;
703
+ if (!new Set(this.getHistory().map((m) => m.id)).has(result.toMessageId)) {
704
+ this._emitStatus("idle");
705
+ return null;
706
+ }
707
+ const existing = this.getCompactions();
708
+ const fromId = existing.length > 0 ? existing[0].fromMessageId : result.fromMessageId;
709
+ this.addCompaction(result.summary, fromId, result.toMessageId);
710
+ await this.refreshSystemPrompt();
711
+ this._emitStatus("idle", { compacted: { tokensBefore } });
712
+ return {
713
+ ...result,
714
+ fromMessageId: fromId
715
+ };
716
+ }
717
+ getContextBlock(label) {
718
+ this._ensureReady();
719
+ return this.context.getBlock(label);
720
+ }
721
+ getContextBlocks() {
722
+ this._ensureReady();
723
+ return this.context.getBlocks();
724
+ }
725
+ async replaceContextBlock(label, content) {
726
+ this._ensureReady();
727
+ return this.context.setBlock(label, content);
728
+ }
729
+ async appendContextBlock(label, content) {
730
+ this._ensureReady();
731
+ return this.context.appendToBlock(label, content);
732
+ }
733
+ async freezeSystemPrompt() {
734
+ this._ensureReady();
735
+ return this.context.freezeSystemPrompt();
736
+ }
737
+ async refreshSystemPrompt() {
738
+ this._ensureReady();
739
+ return this.context.refreshSystemPrompt();
740
+ }
741
+ search(query, options) {
742
+ this._ensureReady();
743
+ if (!this.storage.searchMessages) throw new Error("Session provider does not support search");
744
+ return this.storage.searchMessages(query, options?.limit ?? 20);
745
+ }
746
+ /** Returns update_context tool for writing to context blocks. */
747
+ async tools() {
748
+ this._ensureReady();
749
+ return this.context.tools();
371
750
  }
372
751
  };
373
752
  //#endregion
374
- export { AgentSessionProvider, Session };
753
+ export { AgentContextProvider, AgentSessionProvider, Session };
375
754
 
376
755
  //# sourceMappingURL=index.js.map