agents 0.8.7 → 0.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.
Files changed (50) hide show
  1. package/dist/chat/index.d.ts +603 -0
  2. package/dist/chat/index.js +1285 -0
  3. package/dist/chat/index.js.map +1 -0
  4. package/dist/{client-BwgM3cRz.js → client-QBjFV5de.js} +161 -49
  5. package/dist/client-QBjFV5de.js.map +1 -0
  6. package/dist/client.d.ts +2 -2
  7. package/dist/{compaction-helpers-BFTBIzpK.js → compaction-helpers-BPE1_ziA.js} +1 -1
  8. package/dist/{compaction-helpers-BFTBIzpK.js.map → compaction-helpers-BPE1_ziA.js.map} +1 -1
  9. package/dist/{compaction-helpers-DkJreaDR.d.ts → compaction-helpers-CHNQeyRm.d.ts} +1 -1
  10. package/dist/{do-oauth-client-provider-C2jurFjW.d.ts → do-oauth-client-provider-31gqR33H.d.ts} +1 -1
  11. package/dist/{email-DwPlM0bQ.d.ts → email-Cql45SKP.d.ts} +1 -1
  12. package/dist/email.d.ts +2 -2
  13. package/dist/experimental/memory/session/index.d.ts +298 -73
  14. package/dist/experimental/memory/session/index.js +754 -66
  15. package/dist/experimental/memory/session/index.js.map +1 -1
  16. package/dist/experimental/memory/utils/index.d.ts +1 -1
  17. package/dist/experimental/memory/utils/index.js +1 -1
  18. package/dist/{index-C-6EMK-E.d.ts → index-BPkkIqMn.d.ts} +209 -76
  19. package/dist/{index-Ua2Nfvbm.d.ts → index-DDSX-g7W.d.ts} +11 -1
  20. package/dist/index.d.ts +30 -26
  21. package/dist/index.js +2 -3049
  22. package/dist/{internal_context-DT8RxmAN.d.ts → internal_context-DuQZFvWI.d.ts} +1 -1
  23. package/dist/internal_context.d.ts +1 -1
  24. package/dist/mcp/client.d.ts +2 -2
  25. package/dist/mcp/client.js +1 -1
  26. package/dist/mcp/do-oauth-client-provider.d.ts +1 -1
  27. package/dist/mcp/index.d.ts +1 -1
  28. package/dist/mcp/index.js +2 -2
  29. package/dist/observability/index.d.ts +1 -1
  30. package/dist/react.d.ts +3 -1
  31. package/dist/react.js +3 -0
  32. package/dist/react.js.map +1 -1
  33. package/dist/{retries-DXMQGhG3.d.ts → retries-B_CN5KM9.d.ts} +1 -1
  34. package/dist/retries.d.ts +1 -1
  35. package/dist/{serializable-8Jt1B04R.d.ts → serializable-DGdO8CDh.d.ts} +1 -1
  36. package/dist/serializable.d.ts +1 -1
  37. package/dist/src-B8NZxxsO.js +3217 -0
  38. package/dist/src-B8NZxxsO.js.map +1 -0
  39. package/dist/{types-C-m0II8i.d.ts → types-B9A8AU7B.d.ts} +1 -1
  40. package/dist/types.d.ts +1 -1
  41. package/dist/{workflow-types-CZNXKj_D.d.ts → workflow-types-XmOkuI7A.d.ts} +1 -1
  42. package/dist/workflow-types.d.ts +1 -1
  43. package/dist/workflows.d.ts +2 -2
  44. package/dist/workflows.js +1 -1
  45. package/package.json +20 -18
  46. package/dist/client-BwgM3cRz.js.map +0 -1
  47. package/dist/experimental/forever.d.ts +0 -64
  48. package/dist/experimental/forever.js +0 -338
  49. package/dist/experimental/forever.js.map +0 -1
  50. package/dist/index.js.map +0 -1
@@ -1,6 +1,178 @@
1
1
  import { MessageType } from "../../../types.js";
2
- import { m as estimateStringTokens, p as estimateMessageTokens, t as COMPACTION_PREFIX } from "../../../compaction-helpers-BFTBIzpK.js";
2
+ import { m as estimateStringTokens, p as estimateMessageTokens, t as COMPACTION_PREFIX } from "../../../compaction-helpers-BPE1_ziA.js";
3
3
  import { jsonSchema } from "ai";
4
+ //#region src/experimental/memory/session/search.ts
5
+ /**
6
+ * Check if a provider is a SearchProvider (has a `search` method).
7
+ */
8
+ function isSearchProvider(provider) {
9
+ return typeof provider === "object" && provider !== null && "search" in provider && typeof provider.search === "function";
10
+ }
11
+ /**
12
+ * SearchProvider backed by Durable Object SQLite with FTS5.
13
+ *
14
+ * - `get()` returns a count of indexed entries
15
+ * - `search(query)` full-text search using FTS5
16
+ * - `set(key, content)` indexes or replaces content under a key
17
+ *
18
+ * Each instance uses a namespaced FTS5 table to avoid collisions
19
+ * with the session message search.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * Session.create(this)
24
+ * .withContext("knowledge", {
25
+ * provider: new AgentSearchProvider(this)
26
+ * })
27
+ * ```
28
+ */
29
+ var AgentSearchProvider = class {
30
+ constructor(agent) {
31
+ this.label = "";
32
+ this.initialized = false;
33
+ this.agent = agent;
34
+ }
35
+ init(label) {
36
+ this.label = label;
37
+ }
38
+ ensureTable() {
39
+ if (this.initialized) return;
40
+ this.agent.sql`
41
+ CREATE TABLE IF NOT EXISTS cf_agents_search_entries (
42
+ label TEXT NOT NULL,
43
+ key TEXT NOT NULL,
44
+ content TEXT NOT NULL,
45
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
46
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
47
+ PRIMARY KEY (label, key)
48
+ )
49
+ `;
50
+ this.agent.sql`
51
+ CREATE VIRTUAL TABLE IF NOT EXISTS cf_agents_search_fts
52
+ USING fts5(
53
+ label UNINDEXED,
54
+ key UNINDEXED,
55
+ content,
56
+ tokenize='porter unicode61'
57
+ )
58
+ `;
59
+ this.initialized = true;
60
+ }
61
+ async get() {
62
+ this.ensureTable();
63
+ const count = this.agent.sql`
64
+ SELECT COUNT(*) as count FROM cf_agents_search_entries
65
+ WHERE label = ${this.label}
66
+ `[0]?.count ?? 0;
67
+ if (count === 0) return null;
68
+ return `${count} entries indexed. Recent:\n${this.agent.sql`
69
+ SELECT key FROM cf_agents_search_entries
70
+ WHERE label = ${this.label}
71
+ ORDER BY updated_at DESC
72
+ LIMIT 20
73
+ `.map((r) => `- ${r.key}`).join("\n")}`;
74
+ }
75
+ async search(query) {
76
+ this.ensureTable();
77
+ const sanitized = query.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, "\"\"")}"`).join(" ");
78
+ if (!sanitized) return null;
79
+ try {
80
+ const rows = this.agent.sql`
81
+ SELECT f.key, f.content
82
+ FROM cf_agents_search_fts f
83
+ WHERE cf_agents_search_fts MATCH ${sanitized}
84
+ AND f.label = ${this.label}
85
+ ORDER BY rank
86
+ LIMIT 10
87
+ `;
88
+ if (rows.length === 0) return null;
89
+ return rows.map((r) => `[${r.key}]\n${r.content}`).join("\n\n");
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+ async set(key, content) {
95
+ this.ensureTable();
96
+ this.deleteFTS(key);
97
+ this.agent.sql`
98
+ INSERT INTO cf_agents_search_entries (label, key, content)
99
+ VALUES (${this.label}, ${key}, ${content})
100
+ ON CONFLICT(label, key) DO UPDATE SET
101
+ content = ${content},
102
+ updated_at = CURRENT_TIMESTAMP
103
+ `;
104
+ this.agent.sql`
105
+ INSERT INTO cf_agents_search_fts (label, key, content)
106
+ VALUES (${this.label}, ${key}, ${content})
107
+ `;
108
+ }
109
+ deleteFTS(key) {
110
+ const rows = this.agent.sql`
111
+ SELECT rowid FROM cf_agents_search_fts
112
+ WHERE key = ${key} AND label = ${this.label}
113
+ `;
114
+ for (const row of rows) this.agent.sql`DELETE FROM cf_agents_search_fts WHERE rowid = ${row.rowid}`;
115
+ }
116
+ };
117
+ //#endregion
118
+ //#region src/experimental/memory/session/skills.ts
119
+ /**
120
+ * Check if a provider is a SkillProvider (has a `load` method).
121
+ */
122
+ function isSkillProvider(provider) {
123
+ return typeof provider === "object" && provider !== null && "load" in provider && typeof provider.load === "function";
124
+ }
125
+ /**
126
+ * SkillProvider backed by an R2 bucket.
127
+ *
128
+ * - `get()` returns a metadata listing of all skills (key + description)
129
+ * - `load(key)` fetches a skill's full content
130
+ * - `set(key, content, description?)` writes a skill
131
+ *
132
+ * Descriptions are pulled from R2 custom metadata (`description` key).
133
+ * If a prefix is provided, it is prepended on storage operations and
134
+ * stripped from keys in metadata.
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * const skills = new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" });
139
+ * ```
140
+ */
141
+ var R2SkillProvider = class {
142
+ constructor(bucket, options) {
143
+ this.bucket = bucket;
144
+ this.prefix = options?.prefix ?? "";
145
+ }
146
+ async get() {
147
+ const entries = [];
148
+ let cursor;
149
+ let truncated = true;
150
+ while (truncated) {
151
+ const listed = await this.bucket.list({
152
+ prefix: this.prefix,
153
+ cursor,
154
+ include: ["customMetadata"]
155
+ });
156
+ for (const obj of listed.objects) {
157
+ const key = obj.key.slice(this.prefix.length);
158
+ const desc = obj.customMetadata?.description;
159
+ entries.push(`- ${key}${desc ? `: ${desc}` : ""}`);
160
+ }
161
+ truncated = listed.truncated;
162
+ cursor = listed.truncated ? listed.cursor : void 0;
163
+ }
164
+ return entries.length > 0 ? entries.join("\n") : null;
165
+ }
166
+ async load(key) {
167
+ const obj = await this.bucket.get(this.prefix + key);
168
+ if (!obj) return null;
169
+ return obj.text();
170
+ }
171
+ async set(key, content, description) {
172
+ await this.bucket.put(this.prefix + key, content, { customMetadata: description ? { description } : void 0 });
173
+ }
174
+ };
175
+ //#endregion
4
176
  //#region src/experimental/memory/session/context.ts
5
177
  /**
6
178
  * Context Block Management
@@ -11,8 +183,20 @@ import { jsonSchema } from "ai";
11
183
  * - Updated via setBlock() which writes to the provider immediately
12
184
  * but does NOT update the frozen snapshot (preserves LLM prefix cache)
13
185
  * - Re-snapshotted on next toSystemPrompt() call
186
+ *
187
+ * Provider type determines behavior:
188
+ * - ContextProvider (get only) → readonly block in system prompt
189
+ * - WritableContextProvider (get+set) → writable via set_context tool
190
+ * - SkillProvider (get+load+set?) → metadata in prompt, load_context tool
191
+ * - SearchProvider (get+search+set?) → searchable via search_context tool
14
192
  */
15
193
  /**
194
+ * Check if a provider is writable (has a `set` method).
195
+ */
196
+ function isWritableProvider(provider) {
197
+ return typeof provider === "object" && provider !== null && "set" in provider && typeof provider.set === "function";
198
+ }
199
+ /**
16
200
  * Manages context blocks with frozen snapshot support.
17
201
  */
18
202
  var ContextBlocks = class {
@@ -32,16 +216,20 @@ var ContextBlocks = class {
32
216
  */
33
217
  async load() {
34
218
  for (const config of this.configs) {
35
- let content = null;
36
- if (config.provider) content = await config.provider.get();
37
- content = content ?? config.initialContent ?? "";
219
+ if (config.provider?.init) config.provider.init(config.label);
220
+ const content = config.provider ? await config.provider.get() ?? "" : "";
221
+ const skill = config.provider ? isSkillProvider(config.provider) : false;
222
+ const searchable = config.provider ? isSearchProvider(config.provider) : false;
223
+ const writable = config.provider ? isWritableProvider(config.provider) || skill && !!config.provider.set || searchable && !!config.provider.set : false;
38
224
  this.blocks.set(config.label, {
39
225
  label: config.label,
40
226
  description: config.description,
41
227
  content,
42
228
  tokens: estimateStringTokens(content),
43
229
  maxTokens: config.maxTokens,
44
- readonly: config.readonly
230
+ writable,
231
+ isSkill: skill,
232
+ isSearchable: searchable
45
233
  });
46
234
  }
47
235
  this.loaded = true;
@@ -66,7 +254,8 @@ var ContextBlocks = class {
66
254
  if (!this.loaded) await this.load();
67
255
  const config = this.configs.find((c) => c.label === label);
68
256
  const existing = this.blocks.get(label);
69
- if (existing?.readonly || config?.readonly) throw new Error(`Block "${label}" is readonly`);
257
+ if (!existing?.writable) throw new Error(`Block "${label}" is readonly`);
258
+ if (existing.isSkill || existing.isSearchable) throw new Error(`Block "${label}" is a keyed provider. Use setSkill() or setSearchEntry() instead.`);
70
259
  const tokens = estimateStringTokens(content);
71
260
  const maxTokens = config?.maxTokens ?? existing?.maxTokens;
72
261
  if (maxTokens !== void 0 && tokens > maxTokens) throw new Error(`Block "${label}" exceeds maxTokens: ${tokens} > ${maxTokens}`);
@@ -76,13 +265,64 @@ var ContextBlocks = class {
76
265
  content,
77
266
  tokens,
78
267
  maxTokens,
79
- readonly: false
268
+ writable: true,
269
+ isSkill: false,
270
+ isSearchable: false
80
271
  };
81
272
  this.blocks.set(label, block);
82
- if (config?.provider?.set) await config.provider.set(content);
273
+ if (config?.provider && isWritableProvider(config.provider)) await config.provider.set(content);
83
274
  return block;
84
275
  }
85
276
  /**
277
+ * Set a skill entry within a skill block.
278
+ */
279
+ async setSkill(label, key, content, description) {
280
+ if (!this.loaded) await this.load();
281
+ const config = this.configs.find((c) => c.label === label);
282
+ const existing = this.blocks.get(label);
283
+ if (!existing?.isSkill) throw new Error(`Block "${label}" is not a skill provider`);
284
+ const provider = config?.provider;
285
+ if (!provider || !isSkillProvider(provider) || !provider.set) throw new Error(`Block "${label}" does not support writes`);
286
+ await provider.set(key, content, description);
287
+ const metadata = await provider.get();
288
+ if (metadata) {
289
+ existing.content = metadata;
290
+ existing.tokens = estimateStringTokens(metadata);
291
+ }
292
+ }
293
+ /**
294
+ * Load a skill's full content from a skill block.
295
+ */
296
+ async loadSkill(label, key) {
297
+ if (!this.loaded) await this.load();
298
+ const config = this.configs.find((c) => c.label === label);
299
+ if (!config?.provider || !isSkillProvider(config.provider)) throw new Error(`Block "${label}" is not a skill provider`);
300
+ return config.provider.load(key);
301
+ }
302
+ /**
303
+ * Index a search entry within a searchable block.
304
+ */
305
+ async setSearchEntry(label, key, content) {
306
+ if (!this.loaded) await this.load();
307
+ const config = this.configs.find((c) => c.label === label);
308
+ const existing = this.blocks.get(label);
309
+ if (!existing?.isSearchable) throw new Error(`Block "${label}" is not a search provider`);
310
+ const provider = config?.provider;
311
+ if (!provider || !isSearchProvider(provider) || !provider.set) throw new Error(`Block "${label}" does not support writes`);
312
+ await provider.set(key, content);
313
+ existing.content = await provider.get() ?? "";
314
+ existing.tokens = estimateStringTokens(existing.content);
315
+ }
316
+ /**
317
+ * Search a searchable block.
318
+ */
319
+ async searchContext(label, query) {
320
+ if (!this.loaded) await this.load();
321
+ const config = this.configs.find((c) => c.label === label);
322
+ if (!config?.provider || !isSearchProvider(config.provider)) throw new Error(`Block "${label}" is not a search provider`);
323
+ return config.provider.search(query);
324
+ }
325
+ /**
86
326
  * Append content to a block.
87
327
  */
88
328
  async appendToBlock(label, content) {
@@ -105,8 +345,6 @@ var ContextBlocks = class {
105
345
  }
106
346
  /**
107
347
  * 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
348
  */
111
349
  refreshSnapshot() {
112
350
  return this.captureSnapshot();
@@ -115,14 +353,18 @@ var ContextBlocks = class {
115
353
  const parts = [];
116
354
  const sep = "═".repeat(46);
117
355
  for (const block of this.blocks.values()) {
118
- if (!block.content) continue;
356
+ if (!block.content && !block.isSearchable) continue;
119
357
  let header = block.label.toUpperCase();
120
- if (block.description) header += ` (${block.description})`;
358
+ const hints = [];
359
+ if (block.description) hints.push(block.description);
360
+ if (block.isSkill) hints.push("use load_context to load");
361
+ if (block.isSearchable) hints.push("use search_context to search");
362
+ if (hints.length > 0) header += ` (${hints.join(" — ")})`;
121
363
  if (block.maxTokens) {
122
364
  const pct = Math.round(block.tokens / block.maxTokens * 100);
123
365
  header += ` [${pct}% — ${block.tokens}/${block.maxTokens} tokens]`;
124
366
  }
125
- if (block.readonly) header += " [readonly]";
367
+ if (!block.writable) header += " [readonly]";
126
368
  parts.push(`${sep}\n${header}\n${sep}\n${block.content}`);
127
369
  }
128
370
  this.snapshot = parts.join("\n\n");
@@ -132,20 +374,36 @@ var ContextBlocks = class {
132
374
  * Get writable blocks (for tool description).
133
375
  */
134
376
  getWritableBlocks() {
135
- return Array.from(this.blocks.values()).filter((b) => !b.readonly);
377
+ return Array.from(this.blocks.values()).filter((b) => b.writable);
136
378
  }
137
379
  /**
138
- * Get writable block configs doesn't require blocks to be loaded.
380
+ * Check if any skill providers are registered.
139
381
  */
140
- getWritableConfigs() {
141
- return this.configs.filter((c) => !c.readonly);
382
+ hasSkillBlocks() {
383
+ return Array.from(this.blocks.values()).some((b) => b.isSkill);
384
+ }
385
+ /**
386
+ * Get skill block labels.
387
+ */
388
+ getSkillLabels() {
389
+ return Array.from(this.blocks.values()).filter((b) => b.isSkill).map((b) => b.label);
390
+ }
391
+ /**
392
+ * Check if any search providers are registered.
393
+ */
394
+ hasSearchBlocks() {
395
+ return Array.from(this.blocks.values()).some((b) => b.isSearchable);
396
+ }
397
+ /**
398
+ * Get searchable block labels.
399
+ */
400
+ getSearchLabels() {
401
+ return Array.from(this.blocks.values()).filter((b) => b.isSearchable).map((b) => b.label);
142
402
  }
143
403
  /**
144
404
  * Frozen system prompt. On first call:
145
405
  * 1. Checks store for a persisted prompt (survives DO eviction)
146
406
  * 2. If none, loads blocks from providers, renders, and persists
147
- *
148
- * Subsequent calls return the stored version — true prefix cache stability.
149
407
  */
150
408
  async freezeSystemPrompt() {
151
409
  if (this.promptStore) {
@@ -154,59 +412,152 @@ var ContextBlocks = class {
154
412
  }
155
413
  if (!this.loaded) await this.load();
156
414
  const prompt = this.toSystemPrompt();
157
- if (this.promptStore?.set) await this.promptStore.set(prompt);
415
+ if (this.promptStore) await this.promptStore.set(prompt);
158
416
  return prompt;
159
417
  }
160
418
  /**
161
419
  * Re-render the system prompt from current block state and persist.
162
- * Call after compaction or at session boundaries to pick up writes.
163
420
  */
164
421
  async refreshSystemPrompt() {
165
422
  if (!this.loaded) await this.load();
166
423
  const prompt = this.refreshSnapshot();
167
- if (this.promptStore?.set) await this.promptStore.set(prompt);
424
+ if (this.promptStore) await this.promptStore.set(prompt);
168
425
  return prompt;
169
426
  }
170
427
  /**
171
- * AI tool for updating context blocks. Loads blocks lazily on first execute.
428
+ * AI tools for context blocks.
429
+ *
430
+ * Auto-wired based on provider capabilities:
431
+ * - `set_context` — when any block is writable
432
+ * - `load_context` — when any block is a skill provider
433
+ * - `search_context` — when any block is a search provider
172
434
  */
173
435
  async tools() {
174
436
  if (!this.loaded) await this.load();
175
437
  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"
438
+ const hasSkills = this.hasSkillBlocks();
439
+ const hasSearch = this.hasSearchBlocks();
440
+ const toolSet = {};
441
+ if (writable.length > 0) {
442
+ const regularBlocks = writable.filter((b) => !b.isSkill && !b.isSearchable);
443
+ const keyedBlocks = writable.filter((b) => b.isSkill || b.isSearchable);
444
+ const blockDescriptions = [];
445
+ for (const b of regularBlocks) blockDescriptions.push(`- "${b.label}": ${b.description ?? "no description"}`);
446
+ for (const b of keyedBlocks) {
447
+ const kind = b.isSkill ? "skill collection (requires key and optional description)" : "searchable (requires key)";
448
+ blockDescriptions.push(`- "${b.label}": ${kind}`);
449
+ }
450
+ const properties = {
451
+ label: {
452
+ type: "string",
453
+ enum: writable.map((b) => b.label),
454
+ description: "Block label to write to"
455
+ },
456
+ content: {
457
+ type: "string",
458
+ description: "Content to write"
459
+ },
460
+ action: {
461
+ type: "string",
462
+ enum: ["replace", "append"],
463
+ description: "replace (default) or append"
464
+ }
465
+ };
466
+ const required = ["label", "content"];
467
+ if (keyedBlocks.length > 0) properties.key = {
468
+ type: "string",
469
+ description: "Entry key (required for keyed blocks: " + keyedBlocks.map((b) => `"${b.label}"`).join(", ") + ")"
470
+ };
471
+ if (keyedBlocks.some((b) => b.isSkill)) properties.description = {
472
+ type: "string",
473
+ description: "Short description for the skill entry"
474
+ };
475
+ toolSet.set_context = {
476
+ description: `Write to a context block. Available blocks:\n${blockDescriptions.join("\n")}\n\nWrites are durable and persist across sessions.`,
477
+ inputSchema: jsonSchema({
478
+ type: "object",
479
+ properties,
480
+ required
481
+ }),
482
+ execute: async ({ label, content, key, description, action }) => {
483
+ try {
484
+ const block = this.blocks.get(label);
485
+ if (!block) return `Error: block "${label}" not found`;
486
+ if (block.isSkill) {
487
+ if (!key) return `Error: key is required for skill block "${label}"`;
488
+ await this.setSkill(label, key, content, description);
489
+ return `Written skill "${key}" to ${label}.`;
490
+ }
491
+ if (block.isSearchable) {
492
+ if (!key) return `Error: key is required for searchable block "${label}"`;
493
+ await this.setSearchEntry(label, key, content);
494
+ return `Indexed "${key}" in ${label}.`;
495
+ }
496
+ const updated = action === "append" ? await this.appendToBlock(label, content) : await this.setBlock(label, content);
497
+ return `Written to ${label}. Usage: ${updated.maxTokens ? `${Math.round(updated.tokens / updated.maxTokens * 100)}% (${updated.tokens}/${updated.maxTokens} tokens)` : `${updated.tokens} tokens`}`;
498
+ } catch (err) {
499
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
500
+ }
501
+ }
502
+ };
503
+ }
504
+ if (hasSkills) {
505
+ const skillLabels = this.getSkillLabels();
506
+ toolSet.load_context = {
507
+ description: "Load a document from a skill block by key. Available skill blocks: " + skillLabels.map((l) => `"${l}"`).join(", ") + ". Check the system prompt for available keys.",
508
+ inputSchema: jsonSchema({
509
+ type: "object",
510
+ properties: {
511
+ label: {
512
+ type: "string",
513
+ enum: skillLabels,
514
+ description: "Skill block label"
515
+ },
516
+ key: {
517
+ type: "string",
518
+ description: "Skill key to load"
519
+ }
188
520
  },
189
- content: {
190
- type: "string",
191
- description: "Content to write"
521
+ required: ["label", "key"]
522
+ }),
523
+ execute: async ({ label, key }) => {
524
+ try {
525
+ return await this.loadSkill(label, key) ?? `Not found: ${key}`;
526
+ } catch (err) {
527
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
528
+ }
529
+ }
530
+ };
531
+ }
532
+ if (hasSearch) {
533
+ const searchLabels = this.getSearchLabels();
534
+ toolSet.search_context = {
535
+ description: "Search for information in a searchable context block. Available searchable blocks: " + searchLabels.map((l) => `"${l}"`).join(", ") + ".",
536
+ inputSchema: jsonSchema({
537
+ type: "object",
538
+ properties: {
539
+ label: {
540
+ type: "string",
541
+ enum: searchLabels,
542
+ description: "Searchable block label"
543
+ },
544
+ query: {
545
+ type: "string",
546
+ description: "Search query"
547
+ }
192
548
  },
193
- action: {
194
- type: "string",
195
- enum: ["replace", "append"],
196
- description: "replace (default) or append"
549
+ required: ["label", "query"]
550
+ }),
551
+ execute: async ({ label, query }) => {
552
+ try {
553
+ return await this.searchContext(label, query) ?? "No results found.";
554
+ } catch (err) {
555
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
197
556
  }
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
557
  }
208
- }
209
- } };
558
+ };
559
+ }
560
+ return toolSet;
210
561
  }
211
562
  };
212
563
  //#endregion
@@ -483,7 +834,10 @@ var AgentContextProvider = class {
483
834
  constructor(agent, label) {
484
835
  this.initialized = false;
485
836
  this.agent = agent;
486
- this.label = label;
837
+ this.label = label ?? "";
838
+ }
839
+ init(label) {
840
+ if (!this.label) this.label = label;
487
841
  }
488
842
  ensureTable() {
489
843
  if (this.initialized) return;
@@ -530,17 +884,14 @@ var Session = class Session {
530
884
  * @example
531
885
  * ```ts
532
886
  * const session = Session.create(this)
533
- * .withContext("soul", { initialContent: "You are helpful.", readonly: true })
887
+ * .withContext("soul", { provider: { get: async () => "You are helpful." } })
534
888
  * .withContext("memory", { description: "Learned facts", maxTokens: 1100 })
535
889
  * .withCachedPrompt();
536
890
  *
537
- * // Custom storage (R2, KV, etc.)
891
+ * // Skills from R2 (on-demand loading via load_context tool)
538
892
  * 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
- * }
893
+ * .withContext("skills", {
894
+ * provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" })
544
895
  * })
545
896
  * .withCachedPrompt();
546
897
  * ```
@@ -588,16 +939,14 @@ var Session = class Session {
588
939
  if (this._ready) return;
589
940
  const configs = (this._pending ?? []).map(({ label, options: opts }) => {
590
941
  let provider = opts.provider;
591
- if (!provider && !opts.readonly) {
942
+ if (!provider) {
592
943
  const key = this._sessionId ? `${label}_${this._sessionId}` : label;
593
944
  provider = new AgentContextProvider(this._agent, key);
594
945
  }
595
946
  return {
596
947
  label,
597
948
  description: opts.description,
598
- initialContent: opts.initialContent,
599
949
  maxTokens: opts.maxTokens,
600
- readonly: opts.readonly,
601
950
  provider
602
951
  };
603
952
  });
@@ -743,13 +1092,352 @@ var Session = class Session {
743
1092
  if (!this.storage.searchMessages) throw new Error("Session provider does not support search");
744
1093
  return this.storage.searchMessages(query, options?.limit ?? 20);
745
1094
  }
746
- /** Returns update_context tool for writing to context blocks. */
1095
+ /** Returns set_context and load_context tools. */
747
1096
  async tools() {
748
1097
  this._ensureReady();
749
1098
  return this.context.tools();
750
1099
  }
751
1100
  };
752
1101
  //#endregion
753
- export { AgentContextProvider, AgentSessionProvider, Session };
1102
+ //#region src/experimental/memory/session/manager.ts
1103
+ var SessionManager = class SessionManager {
1104
+ constructor(agent, options = {}) {
1105
+ this._maxContextMessages = 100;
1106
+ this._pending = [];
1107
+ this._sessions = /* @__PURE__ */ new Map();
1108
+ this._tableReady = false;
1109
+ this._ready = false;
1110
+ this.agent = agent;
1111
+ this._maxContextMessages = options.maxContextMessages ?? 100;
1112
+ this._ready = true;
1113
+ this._ensureTable();
1114
+ }
1115
+ /**
1116
+ * Chainable SessionManager creation with auto-wired context for all sessions.
1117
+ *
1118
+ * @example
1119
+ * ```ts
1120
+ * const manager = SessionManager.create(this)
1121
+ * .withContext("soul", { provider: { get: async () => "You are helpful." } })
1122
+ * .withContext("memory", { description: "Learned facts", maxTokens: 1100 })
1123
+ * .withCachedPrompt()
1124
+ * .maxContextMessages(50);
1125
+ *
1126
+ * // Each getSession(id) auto-creates namespaced providers:
1127
+ * // memory key: "memory_<sessionId>"
1128
+ * // prompt key: "_system_prompt_<sessionId>"
1129
+ * const session = manager.getSession("chat-123");
1130
+ * ```
1131
+ */
1132
+ static create(agent) {
1133
+ const mgr = Object.create(SessionManager.prototype);
1134
+ mgr.agent = agent;
1135
+ mgr._maxContextMessages = 100;
1136
+ mgr._pending = [];
1137
+ mgr._compactionFn = null;
1138
+ mgr._tokenThreshold = void 0;
1139
+ mgr._sessions = /* @__PURE__ */ new Map();
1140
+ mgr._tableReady = false;
1141
+ mgr._ready = false;
1142
+ return mgr;
1143
+ }
1144
+ withContext(label, options) {
1145
+ this._pending.push({
1146
+ label,
1147
+ options: options ?? {}
1148
+ });
1149
+ return this;
1150
+ }
1151
+ withCachedPrompt(provider) {
1152
+ this._cachedPrompt = provider ?? true;
1153
+ return this;
1154
+ }
1155
+ maxContextMessages(count) {
1156
+ this._maxContextMessages = count;
1157
+ return this;
1158
+ }
1159
+ /**
1160
+ * Register a compaction function propagated to all sessions.
1161
+ * Called by `Session.compact()` to compress message history.
1162
+ */
1163
+ onCompaction(fn) {
1164
+ this._compactionFn = fn;
1165
+ return this;
1166
+ }
1167
+ /**
1168
+ * Auto-compact when estimated token count exceeds the threshold.
1169
+ * Propagated to all sessions. Requires `onCompaction()`.
1170
+ */
1171
+ compactAfter(tokenThreshold) {
1172
+ this._tokenThreshold = tokenThreshold;
1173
+ return this;
1174
+ }
1175
+ /**
1176
+ * Add a searchable context block that searches conversation history
1177
+ * across all sessions managed by this manager.
1178
+ *
1179
+ * The model can use `search_context` to find relevant messages from
1180
+ * any session. The block is readonly (no `set`).
1181
+ *
1182
+ * @example
1183
+ * ```ts
1184
+ * SessionManager.create(this)
1185
+ * .withContext("memory", { maxTokens: 1100 })
1186
+ * .withSearchableHistory("history")
1187
+ * .withCachedPrompt();
1188
+ * ```
1189
+ */
1190
+ withSearchableHistory(label) {
1191
+ this._historyLabel = label;
1192
+ return this;
1193
+ }
1194
+ _ensureReady() {
1195
+ if (this._ready) return;
1196
+ this._ready = true;
1197
+ this._ensureTable();
1198
+ }
1199
+ _ensureTable() {
1200
+ if (this._tableReady) return;
1201
+ this.agent.sql`
1202
+ CREATE TABLE IF NOT EXISTS assistant_sessions (
1203
+ id TEXT PRIMARY KEY,
1204
+ name TEXT NOT NULL,
1205
+ parent_session_id TEXT,
1206
+ model TEXT,
1207
+ source TEXT,
1208
+ input_tokens INTEGER DEFAULT 0,
1209
+ output_tokens INTEGER DEFAULT 0,
1210
+ estimated_cost REAL DEFAULT 0,
1211
+ end_reason TEXT,
1212
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1213
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
1214
+ )
1215
+ `;
1216
+ this.agent.sql`
1217
+ CREATE VIRTUAL TABLE IF NOT EXISTS assistant_fts
1218
+ USING fts5(id UNINDEXED, session_id UNINDEXED, role UNINDEXED, content, tokenize='porter unicode61')
1219
+ `;
1220
+ this._tableReady = true;
1221
+ }
1222
+ _createHistoryProvider() {
1223
+ const mgr = this;
1224
+ return {
1225
+ async get() {
1226
+ const sessions = mgr.list();
1227
+ if (sessions.length === 0) return null;
1228
+ return `${sessions.length} session${sessions.length === 1 ? "" : "s"} available for search.`;
1229
+ },
1230
+ async search(query) {
1231
+ const results = mgr.search(query, { limit: 10 });
1232
+ if (results.length === 0) return null;
1233
+ return results.map((r) => `[${r.role}] ${r.content}`).join("\n---\n");
1234
+ }
1235
+ };
1236
+ }
1237
+ /** Get or create the Session instance for a session ID. */
1238
+ getSession(sessionId) {
1239
+ this._ensureReady();
1240
+ let session = this._sessions.get(sessionId);
1241
+ if (!session) {
1242
+ const s = Session.create(this.agent).forSession(sessionId);
1243
+ for (const { label, options } of this._pending) s.withContext(label, options);
1244
+ if (this._cachedPrompt === true) s.withCachedPrompt();
1245
+ else if (this._cachedPrompt) s.withCachedPrompt(this._cachedPrompt);
1246
+ if (this._historyLabel) s.withContext(this._historyLabel, {
1247
+ description: "Cross-session conversation history",
1248
+ provider: this._createHistoryProvider()
1249
+ });
1250
+ if (this._compactionFn) s.onCompaction(this._compactionFn);
1251
+ if (this._tokenThreshold != null) s.compactAfter(this._tokenThreshold);
1252
+ session = s;
1253
+ this._sessions.set(sessionId, session);
1254
+ }
1255
+ return session;
1256
+ }
1257
+ create(name, opts) {
1258
+ this._ensureReady();
1259
+ const id = crypto.randomUUID();
1260
+ this.agent.sql`
1261
+ INSERT INTO assistant_sessions (id, name, parent_session_id, model, source)
1262
+ VALUES (${id}, ${name}, ${opts?.parentSessionId ?? null}, ${opts?.model ?? null}, ${opts?.source ?? null})
1263
+ `;
1264
+ return this.get(id);
1265
+ }
1266
+ get(sessionId) {
1267
+ this._ensureReady();
1268
+ return this.agent.sql`
1269
+ SELECT * FROM assistant_sessions WHERE id = ${sessionId}
1270
+ `[0] ?? null;
1271
+ }
1272
+ list() {
1273
+ this._ensureReady();
1274
+ return this.agent.sql`
1275
+ SELECT * FROM assistant_sessions ORDER BY updated_at DESC
1276
+ `;
1277
+ }
1278
+ delete(sessionId) {
1279
+ this.getSession(sessionId).clearMessages();
1280
+ this.agent.sql`DELETE FROM assistant_sessions WHERE id = ${sessionId}`;
1281
+ this._sessions.delete(sessionId);
1282
+ }
1283
+ rename(sessionId, name) {
1284
+ this._ensureReady();
1285
+ this.agent.sql`
1286
+ UPDATE assistant_sessions SET name = ${name}, updated_at = CURRENT_TIMESTAMP
1287
+ WHERE id = ${sessionId}
1288
+ `;
1289
+ }
1290
+ async append(sessionId, message, parentId) {
1291
+ await this.getSession(sessionId).appendMessage(message, parentId);
1292
+ this._touch(sessionId);
1293
+ return message.id;
1294
+ }
1295
+ async upsert(sessionId, message, parentId) {
1296
+ const session = this.getSession(sessionId);
1297
+ if (session.getMessage(message.id)) session.updateMessage(message);
1298
+ else await session.appendMessage(message, parentId);
1299
+ this._touch(sessionId);
1300
+ return message.id;
1301
+ }
1302
+ async appendAll(sessionId, messages, parentId) {
1303
+ const session = this.getSession(sessionId);
1304
+ let lastParent = parentId ?? null;
1305
+ for (const msg of messages) {
1306
+ await session.appendMessage(msg, lastParent);
1307
+ lastParent = msg.id;
1308
+ }
1309
+ this._touch(sessionId);
1310
+ return lastParent;
1311
+ }
1312
+ getHistory(sessionId, leafId) {
1313
+ return this.getSession(sessionId).getHistory(leafId);
1314
+ }
1315
+ getMessageCount(sessionId) {
1316
+ return this.getSession(sessionId).getPathLength();
1317
+ }
1318
+ clearMessages(sessionId) {
1319
+ this.getSession(sessionId).clearMessages();
1320
+ this._touch(sessionId);
1321
+ }
1322
+ deleteMessages(sessionId, messageIds) {
1323
+ this.getSession(sessionId).deleteMessages(messageIds);
1324
+ this._touch(sessionId);
1325
+ }
1326
+ getBranches(sessionId, messageId) {
1327
+ return this.getSession(sessionId).getBranches(messageId);
1328
+ }
1329
+ /**
1330
+ * Fork a session at a specific message, creating a new session
1331
+ * with the history up to that point copied over.
1332
+ */
1333
+ async fork(sessionId, atMessageId, newName) {
1334
+ const info = this.create(newName, { parentSessionId: sessionId });
1335
+ const history = this.getSession(sessionId).getHistory(atMessageId);
1336
+ const newSession = this.getSession(info.id);
1337
+ let parentId = null;
1338
+ for (const msg of history) {
1339
+ const newId = crypto.randomUUID();
1340
+ const copy = {
1341
+ ...msg,
1342
+ id: newId
1343
+ };
1344
+ await newSession.appendMessage(copy, parentId);
1345
+ parentId = newId;
1346
+ }
1347
+ this._touch(info.id);
1348
+ return info;
1349
+ }
1350
+ needsCompaction(sessionId) {
1351
+ return this.getSession(sessionId).getPathLength() > this._maxContextMessages;
1352
+ }
1353
+ addCompaction(sessionId, summary, fromId, toId) {
1354
+ return this.getSession(sessionId).addCompaction(summary, fromId, toId);
1355
+ }
1356
+ getCompactions(sessionId) {
1357
+ return this.getSession(sessionId).getCompactions();
1358
+ }
1359
+ async compactAndSplit(sessionId, summary, newName) {
1360
+ const old = this.get(sessionId);
1361
+ this.agent.sql`
1362
+ UPDATE assistant_sessions SET end_reason = 'compaction', updated_at = CURRENT_TIMESTAMP
1363
+ WHERE id = ${sessionId}
1364
+ `;
1365
+ const info = this.create(newName ?? old?.name ?? "Compacted", {
1366
+ parentSessionId: sessionId,
1367
+ model: old?.model ?? void 0,
1368
+ source: old?.source ?? void 0
1369
+ });
1370
+ await this.append(info.id, {
1371
+ id: crypto.randomUUID(),
1372
+ role: "assistant",
1373
+ parts: [{
1374
+ type: "text",
1375
+ text: `[Context from previous session]\n\n${summary}`
1376
+ }]
1377
+ });
1378
+ return info;
1379
+ }
1380
+ addUsage(sessionId, inputTokens, outputTokens, cost) {
1381
+ this._ensureReady();
1382
+ this.agent.sql`
1383
+ UPDATE assistant_sessions SET
1384
+ input_tokens = input_tokens + ${inputTokens},
1385
+ output_tokens = output_tokens + ${outputTokens},
1386
+ estimated_cost = estimated_cost + ${cost},
1387
+ updated_at = CURRENT_TIMESTAMP
1388
+ WHERE id = ${sessionId}
1389
+ `;
1390
+ }
1391
+ search(query, options) {
1392
+ this._ensureReady();
1393
+ const limit = options?.limit ?? 20;
1394
+ const sanitized = query.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, "\"\"")}"`).join(" ");
1395
+ if (!sanitized) return [];
1396
+ try {
1397
+ return this.agent.sql`
1398
+ SELECT id, role, content FROM assistant_fts
1399
+ WHERE assistant_fts MATCH ${sanitized}
1400
+ ORDER BY rank LIMIT ${limit}
1401
+ `.map((r) => ({
1402
+ id: r.id,
1403
+ role: r.role,
1404
+ content: r.content,
1405
+ createdAt: ""
1406
+ }));
1407
+ } catch {
1408
+ return [];
1409
+ }
1410
+ }
1411
+ tools() {
1412
+ return { session_search: {
1413
+ description: "Search past conversations for relevant context. Searches across all sessions.",
1414
+ inputSchema: jsonSchema({
1415
+ type: "object",
1416
+ properties: { query: {
1417
+ type: "string",
1418
+ description: "Search query"
1419
+ } },
1420
+ required: ["query"]
1421
+ }),
1422
+ execute: async ({ query }) => {
1423
+ try {
1424
+ const results = this.search(query, { limit: 10 });
1425
+ if (results.length === 0) return "No results found.";
1426
+ return results.map((r) => `[${r.role}] ${r.content}`).join("\n---\n");
1427
+ } catch (err) {
1428
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
1429
+ }
1430
+ }
1431
+ } };
1432
+ }
1433
+ _touch(sessionId) {
1434
+ this.agent.sql`
1435
+ UPDATE assistant_sessions SET updated_at = CURRENT_TIMESTAMP
1436
+ WHERE id = ${sessionId}
1437
+ `;
1438
+ }
1439
+ };
1440
+ //#endregion
1441
+ export { AgentContextProvider, AgentSearchProvider, AgentSessionProvider, R2SkillProvider, Session, SessionManager, isSearchProvider, isSkillProvider, isWritableProvider };
754
1442
 
755
1443
  //# sourceMappingURL=index.js.map