agents 0.21.0 → 0.23.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 (258) hide show
  1. package/README.md +21 -1
  2. package/dist/{agent-tool-types-CzGGB-20.d.ts → agent-routing-DE5zmCQ8.d.ts} +1844 -1336
  3. package/dist/agent-routing.d.ts +14 -0
  4. package/dist/agent-routing.js +187 -0
  5. package/dist/agent-routing.js.map +1 -0
  6. package/dist/agent-tool-types.d.ts +26 -26
  7. package/dist/{agent-tools-zR2d5uij.d.ts → agent-tools-DtXMTDGM.d.ts} +7 -7
  8. package/dist/agent-tools.d.ts +21 -21
  9. package/dist/agent-tools.js +2 -1
  10. package/dist/agent-tools.js.map +1 -1
  11. package/dist/browser/ai.js +6 -2
  12. package/dist/browser/ai.js.map +1 -1
  13. package/dist/browser/tanstack-ai.js.map +1 -1
  14. package/dist/callable-decorator-DP__HhBA.d.ts +72 -0
  15. package/dist/callable-decorator.d.ts +18 -0
  16. package/dist/callable-decorator.js +71 -0
  17. package/dist/callable-decorator.js.map +1 -0
  18. package/dist/capability-BjSKYpzg.js +42 -0
  19. package/dist/capability-BjSKYpzg.js.map +1 -0
  20. package/dist/capability-runner-Be_-PLR1.d.ts +459 -0
  21. package/dist/channel-Bnm4S7T2.d.ts +491 -0
  22. package/dist/channels/ai-sdk.d.ts +46 -0
  23. package/dist/channels/ai-sdk.js +120 -0
  24. package/dist/channels/ai-sdk.js.map +1 -0
  25. package/dist/channels/email.d.ts +95 -0
  26. package/dist/channels/email.js +323 -0
  27. package/dist/channels/email.js.map +1 -0
  28. package/dist/channels/index.d.ts +233 -0
  29. package/dist/channels/index.js +608 -0
  30. package/dist/channels/index.js.map +1 -0
  31. package/dist/channels/slack.d.ts +140 -0
  32. package/dist/channels/slack.js +614 -0
  33. package/dist/channels/slack.js.map +1 -0
  34. package/dist/channels/tanstack-ai.d.ts +39 -0
  35. package/dist/channels/tanstack-ai.js +17 -0
  36. package/dist/channels/tanstack-ai.js.map +1 -0
  37. package/dist/channels/telegram.d.ts +106 -0
  38. package/dist/channels/telegram.js +427 -0
  39. package/dist/channels/telegram.js.map +1 -0
  40. package/dist/channels/voice.d.ts +45 -0
  41. package/dist/channels/voice.js +122 -0
  42. package/dist/channels/voice.js.map +1 -0
  43. package/dist/chat/index.d.ts +2328 -2015
  44. package/dist/chat/index.js +891 -521
  45. package/dist/chat/index.js.map +1 -1
  46. package/dist/chat/react.d.ts +14 -1
  47. package/dist/chat/react.js +82 -52
  48. package/dist/chat/react.js.map +1 -1
  49. package/dist/chat/transport.js +1 -1
  50. package/dist/chat-sdk/index.d.ts +7 -7
  51. package/dist/chat-sdk/index.js +1 -1
  52. package/dist/{client-zqKcsyFa.js → client-jagG8a9_.js} +129 -37
  53. package/dist/client-jagG8a9_.js.map +1 -0
  54. package/dist/client.d.ts +1 -1
  55. package/dist/client.js +1 -1
  56. package/dist/{cloudflare-BduZwmYK.js → cloudflare-Dzvc7V2N.js} +10 -3
  57. package/dist/{cloudflare-BduZwmYK.js.map → cloudflare-Dzvc7V2N.js.map} +1 -1
  58. package/dist/context/index.d.ts +216 -0
  59. package/dist/context/index.js +454 -0
  60. package/dist/context/index.js.map +1 -0
  61. package/dist/current-agent-Da_C9a3b.d.ts +266 -0
  62. package/dist/current-agent-DhoDkSnH.js +51 -0
  63. package/dist/current-agent-DhoDkSnH.js.map +1 -0
  64. package/dist/diagnostics-BzvaX2UT.js +45 -0
  65. package/dist/diagnostics-BzvaX2UT.js.map +1 -0
  66. package/dist/diagnostics-C4jcz3VK.js +360 -0
  67. package/dist/diagnostics-C4jcz3VK.js.map +1 -0
  68. package/dist/{do-oauth-client-provider-VTZj2VtM.d.ts → do-oauth-client-provider-Tmf1vgKz.d.ts} +2 -2
  69. package/dist/{email-CL27preh.d.ts → email-7TatiTnl.d.ts} +38 -9
  70. package/dist/email-send.d.ts +15 -0
  71. package/dist/email-send.js +32 -0
  72. package/dist/email-send.js.map +1 -0
  73. package/dist/email.d.ts +14 -10
  74. package/dist/email.js.map +1 -1
  75. package/dist/{handler-stateless-C_bo-Ytq.d.ts → handler-stateless-DxYpJ_XF.d.ts} +3 -3
  76. package/dist/{handler-stateless-CIkKPETH.js → handler-stateless-VvrWSAVA.js} +5 -5
  77. package/dist/handler-stateless-VvrWSAVA.js.map +1 -0
  78. package/dist/index-BB0kqhIz.d.ts +101 -0
  79. package/dist/index-XDkuQ7zm.d.ts +89 -0
  80. package/dist/{index-BRnybD6X.d.ts → index-YSKgfgg9.d.ts} +21 -31
  81. package/dist/index.d.ts +113 -101
  82. package/dist/index.js +11 -7234
  83. package/dist/ingress-BfetZbMO.js +83 -0
  84. package/dist/ingress-BfetZbMO.js.map +1 -0
  85. package/dist/internal-CYlgHl1l.js +59 -0
  86. package/dist/internal-CYlgHl1l.js.map +1 -0
  87. package/dist/internal_context-BlxFEWfn.d.ts +19 -0
  88. package/dist/internal_context.d.ts +10 -4
  89. package/dist/internal_context.js +1 -10
  90. package/dist/{client-invoker-BNSZxAkv.d.ts → invoker-CG0_p_Wq.d.ts} +2 -2
  91. package/dist/{client-invoker-VNZ7X0nn.js → invoker-CHMnoxIA.js} +2 -2
  92. package/dist/invoker-CHMnoxIA.js.map +1 -0
  93. package/dist/lifecycle/index.d.ts +66 -0
  94. package/dist/lifecycle/index.js +4 -0
  95. package/dist/lifecycle-CMRGjZdw.js +1299 -0
  96. package/dist/lifecycle-CMRGjZdw.js.map +1 -0
  97. package/dist/mcp/{do-oauth-client-provider.d.ts → client/do-oauth-client-provider.d.ts} +1 -1
  98. package/dist/mcp/{do-oauth-client-provider.js → client/do-oauth-client-provider.js} +1 -1
  99. package/dist/mcp/client/do-oauth-client-provider.js.map +1 -0
  100. package/dist/mcp/client/index.d.ts +42 -0
  101. package/dist/mcp/{client.js → client/index.js} +1 -1
  102. package/dist/mcp/{x402.d.ts → client/x402.d.ts} +2 -2
  103. package/dist/mcp/{x402.js → client/x402.js} +2 -2
  104. package/dist/mcp/client/x402.js.map +1 -0
  105. package/dist/mcp/index.d.ts +36 -36
  106. package/dist/mcp/index.js +14 -16
  107. package/dist/mcp/index.js.map +1 -1
  108. package/dist/mcp/{server.d.ts → server/index.d.ts} +1 -1
  109. package/dist/mcp/{server.js → server/index.js} +1 -1
  110. package/dist/observability/ai/index.js +50 -35
  111. package/dist/observability/ai/index.js.map +1 -1
  112. package/dist/observability/index.d.ts +4 -4
  113. package/dist/observability/index.js +3 -50
  114. package/dist/observability/index.js.map +1 -1
  115. package/dist/{protocol-Dqc2MQxo.js → protocol-B0nh6KNf.js} +19 -21
  116. package/dist/protocol-B0nh6KNf.js.map +1 -0
  117. package/dist/react.d.ts +4 -4
  118. package/dist/react.js +1 -1
  119. package/dist/{retries-CAvxtG9d.d.ts → retries-D9Ds-1lz.d.ts} +17 -6
  120. package/dist/retries.d.ts +8 -6
  121. package/dist/retries.js +13 -1
  122. package/dist/retries.js.map +1 -1
  123. package/dist/routing/index.d.ts +137 -0
  124. package/dist/routing/index.js +244 -0
  125. package/dist/routing/index.js.map +1 -0
  126. package/dist/sanitize-D9TujEK8.js +79 -0
  127. package/dist/sanitize-D9TujEK8.js.map +1 -0
  128. package/dist/schedule.d.ts +25 -94
  129. package/dist/schedule.js +1 -98
  130. package/dist/schedule.js.map +1 -1
  131. package/dist/scheduler-DD9NdYbF.js +665 -0
  132. package/dist/scheduler-DD9NdYbF.js.map +1 -0
  133. package/dist/scheduler-Dwh85ZGl.d.ts +223 -0
  134. package/dist/schedules/index.d.ts +22 -0
  135. package/dist/schedules/index.js +2 -0
  136. package/dist/schedules/parser.d.ts +79 -0
  137. package/dist/schedules/parser.js +103 -0
  138. package/dist/schedules/parser.js.map +1 -0
  139. package/dist/sentence-chunker-BAidJ4DA.d.ts +68 -0
  140. package/dist/serializable.d.ts +1 -1
  141. package/dist/sessions/index.d.ts +441 -0
  142. package/dist/sessions/index.js +2063 -0
  143. package/dist/sessions/index.js.map +1 -0
  144. package/dist/skills/index.d.ts +99 -0
  145. package/dist/skills/index.js +254 -5
  146. package/dist/skills/index.js.map +1 -1
  147. package/dist/sql-error-CPY-GXyI.d.ts +12 -0
  148. package/dist/sql-error.d.ts +2 -0
  149. package/dist/sql-error.js +16 -0
  150. package/dist/sql-error.js.map +1 -0
  151. package/dist/src-DlSHshb2.js +6963 -0
  152. package/dist/src-DlSHshb2.js.map +1 -0
  153. package/dist/streams/index.d.ts +120 -0
  154. package/dist/streams/index.js +107 -0
  155. package/dist/streams/index.js.map +1 -0
  156. package/dist/streams-D6tJ0NN9.d.ts +370 -0
  157. package/dist/streams-DZKgAj9b.js +709 -0
  158. package/dist/streams-DZKgAj9b.js.map +1 -0
  159. package/dist/sub-routing.d.ts +12 -12
  160. package/dist/surface-bZZJqBka.js +17 -0
  161. package/dist/surface-bZZJqBka.js.map +1 -0
  162. package/dist/tasks/index.d.ts +64 -0
  163. package/dist/tasks/index.js +2 -0
  164. package/dist/tasks-BRJ5zgya.d.ts +517 -0
  165. package/dist/tasks-ylZgBjhj.js +1656 -0
  166. package/dist/tasks-ylZgBjhj.js.map +1 -0
  167. package/dist/text-segment-joiner-BtAFQSA_.js +57 -0
  168. package/dist/text-segment-joiner-BtAFQSA_.js.map +1 -0
  169. package/dist/text-stream-CpdiKrJB.js +272 -0
  170. package/dist/text-stream-CpdiKrJB.js.map +1 -0
  171. package/dist/tokens-nHAKcN6M.js +52 -0
  172. package/dist/tokens-nHAKcN6M.js.map +1 -0
  173. package/dist/tool-schema-CBjGPrsQ.js +31 -0
  174. package/dist/tool-schema-CBjGPrsQ.js.map +1 -0
  175. package/dist/types-B7LojTe4.d.ts +202 -0
  176. package/dist/types-_Faxb570.d.ts +439 -0
  177. package/dist/voice/client.d.ts +226 -0
  178. package/dist/voice/client.js +932 -0
  179. package/dist/voice/client.js.map +1 -0
  180. package/dist/voice/errors.d.ts +43 -0
  181. package/dist/voice/errors.js +41 -0
  182. package/dist/voice/errors.js.map +1 -0
  183. package/dist/voice/index.d.ts +271 -0
  184. package/dist/voice/index.js +1812 -0
  185. package/dist/voice/index.js.map +1 -0
  186. package/dist/voice/react.d.ts +167 -0
  187. package/dist/voice/react.js +234 -0
  188. package/dist/voice/react.js.map +1 -0
  189. package/dist/voice/sfu.d.ts +71 -0
  190. package/dist/voice/sfu.js +157 -0
  191. package/dist/voice/sfu.js.map +1 -0
  192. package/dist/voice/text.d.ts +6 -0
  193. package/dist/voice/text.js +2 -0
  194. package/dist/voice/types.d.ts +58 -0
  195. package/dist/voice/types.js +18 -0
  196. package/dist/voice/types.js.map +1 -0
  197. package/dist/voice/workers-ai.d.ts +136 -0
  198. package/dist/voice/workers-ai.js +568 -0
  199. package/dist/voice/workers-ai.js.map +1 -0
  200. package/dist/websockets/index.d.ts +192 -0
  201. package/dist/websockets/index.js +2 -0
  202. package/dist/websockets-DUfRHPRq.js +502 -0
  203. package/dist/websockets-DUfRHPRq.js.map +1 -0
  204. package/dist/workflow-types.d.ts +25 -25
  205. package/dist/workflows.d.ts +22 -22
  206. package/dist/workflows.js +2 -1
  207. package/dist/workflows.js.map +1 -1
  208. package/dist/{ws-chat-transport-CIoOBbO7.js → ws-chat-transport-rWwta645.js} +152 -15
  209. package/dist/ws-chat-transport-rWwta645.js.map +1 -0
  210. package/docs/agent-class.md +29 -87
  211. package/docs/agent-tools.md +2 -1
  212. package/docs/channels.md +323 -0
  213. package/docs/chat-agents.md +19 -25
  214. package/docs/context.md +131 -0
  215. package/docs/durable-execution.md +1 -1
  216. package/docs/http-websockets.md +1 -11
  217. package/docs/human-in-the-loop.md +1 -1
  218. package/docs/index.md +16 -12
  219. package/docs/lifecycle.md +370 -0
  220. package/docs/long-running-agents.md +4 -6
  221. package/docs/mcp-client.md +55 -0
  222. package/docs/mcp-servers.md +5 -1
  223. package/docs/observability.md +11 -11
  224. package/docs/resumable-streaming.md +2 -2
  225. package/docs/routing.md +105 -0
  226. package/docs/scheduling.md +175 -15
  227. package/docs/server-driven-messages.md +1 -1
  228. package/docs/sessions.md +237 -871
  229. package/docs/streams.md +213 -0
  230. package/docs/sub-agents.md +185 -125
  231. package/docs/tasks.md +246 -0
  232. package/docs/voice.md +745 -0
  233. package/package.json +144 -33
  234. package/dist/cli/index.js +0 -26
  235. package/dist/cli/index.js.map +0 -1
  236. package/dist/client-invoker-VNZ7X0nn.js.map +0 -1
  237. package/dist/client-zqKcsyFa.js.map +0 -1
  238. package/dist/compaction-helpers-iiKMr2TQ.js +0 -340
  239. package/dist/compaction-helpers-iiKMr2TQ.js.map +0 -1
  240. package/dist/compaction-helpers-wUz6M3us.d.ts +0 -621
  241. package/dist/experimental/memory/session/index.d.ts +0 -670
  242. package/dist/experimental/memory/session/index.js +0 -2374
  243. package/dist/experimental/memory/session/index.js.map +0 -1
  244. package/dist/experimental/memory/utils/index.d.ts +0 -96
  245. package/dist/experimental/memory/utils/index.js +0 -79
  246. package/dist/experimental/memory/utils/index.js.map +0 -1
  247. package/dist/handler-stateless-CIkKPETH.js.map +0 -1
  248. package/dist/index.js.map +0 -1
  249. package/dist/internal_context-Dg4Cgjcu.d.ts +0 -37
  250. package/dist/internal_context.js.map +0 -1
  251. package/dist/mcp/client.d.ts +0 -42
  252. package/dist/mcp/do-oauth-client-provider.js.map +0 -1
  253. package/dist/mcp/x402.js.map +0 -1
  254. package/dist/protocol-Dqc2MQxo.js.map +0 -1
  255. package/dist/tool-output-truncation-CNnnGZQ3.js +0 -98
  256. package/dist/tool-output-truncation-CNnnGZQ3.js.map +0 -1
  257. package/dist/ws-chat-transport-CIoOBbO7.js.map +0 -1
  258. /package/dist/{cli/index.d.ts → index-BVVgDSdq.d.ts} +0 -0
@@ -1,2374 +0,0 @@
1
- import "../../../types.js";
2
- import { m as estimateStringTokens, p as estimateMessageTokens, t as COMPACTION_PREFIX } from "../../../compaction-helpers-iiKMr2TQ.js";
3
- import { z } from "zod";
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.`;
69
- }
70
- async search(query) {
71
- this.ensureTable();
72
- const sanitized = query.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, "\"\"")}"`).join(" ");
73
- if (!sanitized) return null;
74
- try {
75
- const rows = this.agent.sql`
76
- SELECT f.key, f.content
77
- FROM cf_agents_search_fts f
78
- WHERE cf_agents_search_fts MATCH ${sanitized}
79
- AND f.label = ${this.label}
80
- ORDER BY rank
81
- LIMIT 10
82
- `;
83
- if (rows.length === 0) return null;
84
- return rows.map((r) => `[${r.key}]\n${r.content}`).join("\n\n");
85
- } catch {
86
- return null;
87
- }
88
- }
89
- async set(key, content) {
90
- this.ensureTable();
91
- this.deleteFTS(key);
92
- this.agent.sql`
93
- INSERT INTO cf_agents_search_entries (label, key, content)
94
- VALUES (${this.label}, ${key}, ${content})
95
- ON CONFLICT(label, key) DO UPDATE SET
96
- content = ${content},
97
- updated_at = CURRENT_TIMESTAMP
98
- `;
99
- this.agent.sql`
100
- INSERT INTO cf_agents_search_fts (label, key, content)
101
- VALUES (${this.label}, ${key}, ${content})
102
- `;
103
- }
104
- deleteFTS(key) {
105
- const rows = this.agent.sql`
106
- SELECT rowid FROM cf_agents_search_fts
107
- WHERE key = ${key} AND label = ${this.label}
108
- `;
109
- for (const row of rows) this.agent.sql`DELETE FROM cf_agents_search_fts WHERE rowid = ${row.rowid}`;
110
- }
111
- };
112
- //#endregion
113
- //#region src/experimental/memory/session/skills.ts
114
- /**
115
- * Check if a provider is a SkillProvider (has a `load` method).
116
- */
117
- function isSkillProvider(provider) {
118
- return typeof provider === "object" && provider !== null && "load" in provider && typeof provider.load === "function";
119
- }
120
- /**
121
- * SkillProvider backed by an R2 bucket.
122
- *
123
- * - `get()` returns a metadata listing of all skills (key + description)
124
- * - `load(key)` fetches a skill's full content
125
- * - `set(key, content, description?)` writes a skill
126
- *
127
- * Descriptions are pulled from R2 custom metadata (`description` key).
128
- * If a prefix is provided, it is prepended on storage operations and
129
- * stripped from keys in metadata. `keys`, when provided, is matched against
130
- * these prefix-relative keys.
131
- *
132
- * @example
133
- * ```ts
134
- * const skills = new R2SkillProvider(env.SKILLS_BUCKET, {
135
- * prefix: "skills/",
136
- * keys: ["code-review", "debugging"]
137
- * });
138
- * ```
139
- */
140
- var R2SkillProvider = class {
141
- constructor(bucket, options) {
142
- this.bucket = bucket;
143
- this.prefix = options?.prefix ?? "";
144
- this.keys = options?.keys?.length ? new Set(options.keys) : null;
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
- if (!this.allowsKey(key)) continue;
159
- const desc = obj.customMetadata?.description;
160
- entries.push(`- ${key}${desc ? `: ${desc}` : ""}`);
161
- }
162
- truncated = listed.truncated;
163
- cursor = listed.truncated ? listed.cursor : void 0;
164
- }
165
- return entries.length > 0 ? entries.join("\n") : null;
166
- }
167
- async load(key) {
168
- if (!this.allowsKey(key)) return null;
169
- const obj = await this.bucket.get(this.prefix + key);
170
- if (!obj) return null;
171
- return obj.text();
172
- }
173
- async set(key, content, description) {
174
- await this.bucket.put(this.prefix + key, content, { customMetadata: description ? { description } : void 0 });
175
- }
176
- allowsKey(key) {
177
- return this.keys === null || this.keys.has(key);
178
- }
179
- };
180
- //#endregion
181
- //#region src/experimental/memory/session/context.ts
182
- function slugify(text) {
183
- return text.slice(0, 60).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
184
- }
185
- function stableHash(text) {
186
- let hash = 2166136261;
187
- for (let i = 0; i < text.length; i++) {
188
- hash ^= text.charCodeAt(i);
189
- hash = Math.imul(hash, 16777619);
190
- }
191
- return (hash >>> 0).toString(36);
192
- }
193
- function contextEntryKey(metadataTitle, content) {
194
- if (metadataTitle?.trim()) return slugify(metadataTitle) || `entry-${stableHash(metadataTitle)}`;
195
- return `${slugify(content) || "entry"}-${stableHash(content)}`;
196
- }
197
- /**
198
- * Check if a provider is writable (has a `set` method).
199
- */
200
- function isWritableProvider(provider) {
201
- return typeof provider === "object" && provider !== null && "set" in provider && typeof provider.set === "function";
202
- }
203
- /**
204
- * Manages context blocks with frozen snapshot support.
205
- */
206
- var ContextBlocks = class {
207
- constructor(configs, promptStore) {
208
- this.blocks = /* @__PURE__ */ new Map();
209
- this.snapshot = null;
210
- this.loaded = false;
211
- this._loadedSkills = /* @__PURE__ */ new Set();
212
- this._onUnloadSkill = null;
213
- this.configs = configs;
214
- this.promptStore = promptStore ?? null;
215
- }
216
- /**
217
- * Register a callback invoked when a skill is unloaded.
218
- * Session uses this to update the stored tool result message.
219
- */
220
- setUnloadCallback(cb) {
221
- this._onUnloadSkill = cb;
222
- }
223
- isLoaded() {
224
- return this.loaded;
225
- }
226
- /**
227
- * Load all blocks from their providers.
228
- * Called once at session init.
229
- */
230
- async load() {
231
- for (const config of this.configs) {
232
- if (config.provider?.init) config.provider.init(config.label);
233
- const content = config.provider ? await config.provider.get() ?? "" : "";
234
- const skill = config.provider ? isSkillProvider(config.provider) : false;
235
- const searchable = config.provider ? isSearchProvider(config.provider) : false;
236
- const writable = config.provider ? isWritableProvider(config.provider) || skill && !!config.provider.set || searchable && !!config.provider.set : false;
237
- this.blocks.set(config.label, {
238
- label: config.label,
239
- description: config.description,
240
- content,
241
- tokens: estimateStringTokens(content),
242
- maxTokens: config.maxTokens,
243
- writable,
244
- isSkill: skill,
245
- isSearchable: searchable
246
- });
247
- }
248
- this.loaded = true;
249
- }
250
- /**
251
- * Dynamically register a new context block after initialization.
252
- * Used by extensions to contribute context at runtime.
253
- *
254
- * If blocks have already been loaded, the new block's provider is
255
- * initialized and loaded immediately. The snapshot is NOT updated
256
- * automatically — call `refreshSystemPrompt()` to rebuild.
257
- */
258
- async addBlock(config) {
259
- if (!this.loaded) await this.load();
260
- if (this.configs.some((c) => c.label === config.label)) throw new Error(`Block "${config.label}" already exists`);
261
- this.configs.push(config);
262
- if (config.provider?.init) config.provider.init(config.label);
263
- const content = config.provider ? await config.provider.get() ?? "" : "";
264
- const skill = config.provider ? isSkillProvider(config.provider) : false;
265
- const searchable = config.provider ? isSearchProvider(config.provider) : false;
266
- const writable = config.provider ? isWritableProvider(config.provider) || skill && !!config.provider.set || searchable && !!config.provider.set : false;
267
- const block = {
268
- label: config.label,
269
- description: config.description,
270
- content,
271
- tokens: estimateStringTokens(content),
272
- maxTokens: config.maxTokens,
273
- writable,
274
- isSkill: skill,
275
- isSearchable: searchable
276
- };
277
- this.blocks.set(config.label, block);
278
- return block;
279
- }
280
- /**
281
- * Remove a dynamically registered context block.
282
- * Used during extension unload cleanup.
283
- *
284
- * Returns true if the block existed and was removed.
285
- * The snapshot is NOT updated automatically — call
286
- * `refreshSystemPrompt()` to rebuild.
287
- *
288
- * Note: loaded skills for this block are cleaned up from the
289
- * tracking set but the skill unload callback is NOT fired
290
- * (history reclamation is skipped — appropriate for full
291
- * extension removal).
292
- */
293
- removeBlock(label) {
294
- const idx = this.configs.findIndex((c) => c.label === label);
295
- if (idx === -1) return false;
296
- this.configs.splice(idx, 1);
297
- this.blocks.delete(label);
298
- for (const id of this._loadedSkills) if (id.startsWith(`${label}:`)) this._loadedSkills.delete(id);
299
- return true;
300
- }
301
- /**
302
- * Get a block by label.
303
- */
304
- getBlock(label) {
305
- return this.blocks.get(label) ?? null;
306
- }
307
- /**
308
- * Get all blocks.
309
- */
310
- getBlocks() {
311
- return Array.from(this.blocks.values());
312
- }
313
- /**
314
- * Set block content. Writes to provider immediately.
315
- * Does NOT update the frozen snapshot.
316
- */
317
- async setBlock(label, content) {
318
- if (!this.loaded) await this.load();
319
- const config = this.configs.find((c) => c.label === label);
320
- const existing = this.blocks.get(label);
321
- if (!existing?.writable) throw new Error(`Block "${label}" is readonly`);
322
- if (existing.isSkill || existing.isSearchable) throw new Error(`Block "${label}" is a keyed provider. Use setSkill() or setSearchEntry() instead.`);
323
- const tokens = estimateStringTokens(content);
324
- const maxTokens = config?.maxTokens ?? existing?.maxTokens;
325
- if (maxTokens !== void 0 && tokens > maxTokens) throw new Error(`Block "${label}" exceeds maxTokens: ${tokens} > ${maxTokens}`);
326
- const block = {
327
- label,
328
- description: config?.description ?? existing?.description,
329
- content,
330
- tokens,
331
- maxTokens,
332
- writable: true,
333
- isSkill: false,
334
- isSearchable: false
335
- };
336
- this.blocks.set(label, block);
337
- if (config?.provider && isWritableProvider(config.provider)) await config.provider.set(content);
338
- return block;
339
- }
340
- /**
341
- * Set a skill entry within a skill block.
342
- */
343
- async setSkill(label, key, content, description) {
344
- if (!this.loaded) await this.load();
345
- const config = this.configs.find((c) => c.label === label);
346
- const existing = this.blocks.get(label);
347
- if (!existing?.isSkill) throw new Error(`Block "${label}" is not a skill provider`);
348
- const provider = config?.provider;
349
- if (!provider || !isSkillProvider(provider) || !provider.set) throw new Error(`Block "${label}" does not support writes`);
350
- await provider.set(key, content, description);
351
- const metadata = await provider.get();
352
- if (metadata) {
353
- existing.content = metadata;
354
- existing.tokens = estimateStringTokens(metadata);
355
- }
356
- }
357
- /**
358
- * Load a skill's full content from a skill block.
359
- */
360
- async loadSkill(label, key) {
361
- if (!this.loaded) await this.load();
362
- const config = this.configs.find((c) => c.label === label);
363
- if (!config?.provider || !isSkillProvider(config.provider)) throw new Error(`Block "${label}" is not a skill provider`);
364
- const content = await config.provider.load(key);
365
- if (content !== null) this._loadedSkills.add(`${label}:${key}`);
366
- return content;
367
- }
368
- /**
369
- * Unload a previously loaded skill. Updates the stored tool result
370
- * message via the unload callback (set by Session).
371
- */
372
- unloadSkill(label, key) {
373
- const id = `${label}:${key}`;
374
- if (!this._loadedSkills.has(id)) return false;
375
- this._loadedSkills.delete(id);
376
- this._onUnloadSkill?.(label, key);
377
- return true;
378
- }
379
- /**
380
- * Get the set of currently loaded skill keys (as "label:key" strings).
381
- */
382
- getLoadedSkillKeys() {
383
- return this._loadedSkills;
384
- }
385
- /**
386
- * Restore loaded skill tracking from a set of "label:key" strings.
387
- * Used by Session to reconstruct state after hibernation.
388
- */
389
- restoreLoadedSkills(skillIds) {
390
- this._loadedSkills = new Set(skillIds);
391
- }
392
- /**
393
- * Clear all loaded skill tracking. Called when messages are cleared.
394
- */
395
- clearSkillState() {
396
- this._loadedSkills.clear();
397
- }
398
- /**
399
- * Index a search entry within a searchable block.
400
- */
401
- async setSearchEntry(label, key, content) {
402
- if (!this.loaded) await this.load();
403
- const config = this.configs.find((c) => c.label === label);
404
- const existing = this.blocks.get(label);
405
- if (!existing?.isSearchable) throw new Error(`Block "${label}" is not a search provider`);
406
- const provider = config?.provider;
407
- if (!provider || !isSearchProvider(provider) || !provider.set) throw new Error(`Block "${label}" does not support writes`);
408
- await provider.set(key, content);
409
- existing.content = await provider.get() ?? "";
410
- existing.tokens = estimateStringTokens(existing.content);
411
- }
412
- /**
413
- * Search a searchable block.
414
- */
415
- async searchContext(label, query) {
416
- if (!this.loaded) await this.load();
417
- const config = this.configs.find((c) => c.label === label);
418
- if (!config?.provider || !isSearchProvider(config.provider)) throw new Error(`Block "${label}" is not a search provider`);
419
- return config.provider.search(query);
420
- }
421
- /**
422
- * Append content to a block.
423
- */
424
- async appendToBlock(label, content) {
425
- if (!this.loaded) await this.load();
426
- const existing = this.blocks.get(label);
427
- if (!existing) throw new Error(`Block "${label}" not found`);
428
- const needsSep = existing.content.length > 0 && !content.startsWith("\n");
429
- return this.setBlock(label, existing.content + (needsSep ? "\n" : "") + content);
430
- }
431
- /**
432
- * Get the system prompt string with context blocks.
433
- *
434
- * Returns a frozen snapshot: first call renders and caches,
435
- * subsequent calls return the same string (preserves LLM prefix cache).
436
- * Call refreshSnapshot() to re-render after block changes take effect.
437
- */
438
- toSystemPrompt() {
439
- if (!this.loaded) throw new Error("Context blocks not loaded. Call load() first.");
440
- if (this.snapshot !== null) return this.snapshot;
441
- return this.captureSnapshot();
442
- }
443
- /**
444
- * Force re-render the snapshot from current block state.
445
- */
446
- refreshSnapshot() {
447
- return this.captureSnapshot();
448
- }
449
- renderPrompt() {
450
- const parts = [];
451
- const sep = "═".repeat(46);
452
- for (const block of this.blocks.values()) {
453
- if (!block.content && !block.writable && !block.isSearchable && !block.isSkill) continue;
454
- let header = block.label.toUpperCase();
455
- if (block.description) header += ` (${block.description})`;
456
- if (block.maxTokens) {
457
- const pct = Math.round(block.tokens / block.maxTokens * 100);
458
- header += ` [${pct}% — ${block.tokens}/${block.maxTokens} tokens]`;
459
- }
460
- if (block.isSearchable) header += " [searchable]";
461
- else if (block.isSkill) header += " [loadable]";
462
- else if (!block.writable) header += " [readonly]";
463
- else header += " [writable]";
464
- parts.push(`${sep}\n${header}\n${sep}\n${block.content}`);
465
- }
466
- return parts.join("\n\n");
467
- }
468
- captureSnapshot() {
469
- this.snapshot = this.renderPrompt();
470
- return this.snapshot;
471
- }
472
- /**
473
- * Get writable blocks (for tool description).
474
- */
475
- getWritableBlocks() {
476
- return Array.from(this.blocks.values()).filter((b) => b.writable);
477
- }
478
- /**
479
- * Check if any skill providers are registered.
480
- */
481
- hasSkillBlocks() {
482
- return Array.from(this.blocks.values()).some((b) => b.isSkill);
483
- }
484
- /**
485
- * Check whether any CONFIGURED provider is skill-capable, without
486
- * requiring `load()` to have run. Used by Session to decide whether the
487
- * init-time loaded-skill restore scan is needed at all.
488
- */
489
- hasSkillCapableConfigs() {
490
- return this.configs.some((c) => c.provider !== void 0 && isSkillProvider(c.provider));
491
- }
492
- /**
493
- * Get skill block labels.
494
- */
495
- getSkillLabels() {
496
- return Array.from(this.blocks.values()).filter((b) => b.isSkill).map((b) => b.label);
497
- }
498
- /**
499
- * Check if any search providers are registered.
500
- */
501
- hasSearchBlocks() {
502
- return Array.from(this.blocks.values()).some((b) => b.isSearchable);
503
- }
504
- /**
505
- * Get searchable block labels.
506
- */
507
- getSearchLabels() {
508
- return Array.from(this.blocks.values()).filter((b) => b.isSearchable).map((b) => b.label);
509
- }
510
- /**
511
- * Return the cached system prompt. If no cached prompt exists,
512
- * loads blocks from providers, renders, and persists to the store.
513
- * Subsequent calls return the stored value without re-rendering.
514
- */
515
- async freezeSystemPrompt() {
516
- if (this.promptStore) {
517
- const stored = await this.promptStore.get();
518
- if (stored !== null) return stored;
519
- }
520
- if (!this.loaded) await this.load();
521
- const prompt = this.toSystemPrompt();
522
- if (this.promptStore) await this.promptStore.set(prompt);
523
- return prompt;
524
- }
525
- /**
526
- * Return the prompt text used for token estimation without persisting a new
527
- * frozen prompt to the prompt store.
528
- *
529
- * This still reads an existing cached prompt when present, so estimates match
530
- * the prompt that inference would reuse. If no cached prompt exists, it loads
531
- * providers and renders the current blocks without freezing the snapshot.
532
- */
533
- async getSystemPromptForEstimate() {
534
- if (this.snapshot !== null) return this.snapshot;
535
- if (this.promptStore) {
536
- const stored = await this.promptStore.get();
537
- if (stored !== null) return stored;
538
- }
539
- if (!this.loaded) await this.load();
540
- return this.renderPrompt();
541
- }
542
- /**
543
- * Force reload blocks from providers, re-render the system prompt,
544
- * and persist to the store. Use this after block content has changed
545
- * or to invalidate the cached prompt.
546
- */
547
- async refreshSystemPrompt() {
548
- this.loaded = false;
549
- await this.load();
550
- const prompt = this.refreshSnapshot();
551
- if (this.promptStore) await this.promptStore.set(prompt);
552
- return prompt;
553
- }
554
- /**
555
- * AI tools for context blocks.
556
- *
557
- * Auto-wired based on provider capabilities:
558
- * - `set_context` — when any block is writable
559
- * - `load_context` — when any block is a skill provider
560
- * - `search_context` — when any block is a search provider
561
- */
562
- async tools() {
563
- if (!this.loaded) await this.load();
564
- const writable = this.getWritableBlocks();
565
- const hasSkills = this.hasSkillBlocks();
566
- const hasSearch = this.hasSearchBlocks();
567
- const toolSet = {};
568
- if (writable.length > 0) {
569
- const blockDescriptions = writable.map((b) => {
570
- const kind = b.isSkill ? "skill collection, keyed entries" : b.isSearchable ? "searchable, keyed entries" : "writable";
571
- return `- "${b.label}" (${kind}): ${b.description ?? "no description"}`;
572
- });
573
- const keyedBlocks = writable.filter((b) => b.isSkill || b.isSearchable);
574
- const properties = {
575
- label: {
576
- type: "string",
577
- enum: writable.map((b) => b.label),
578
- description: "Block label to write to"
579
- },
580
- content: {
581
- type: "string",
582
- description: "The main content to write to the block."
583
- },
584
- action: {
585
- type: "string",
586
- enum: ["replace", "append"],
587
- description: "replace (default) or append"
588
- }
589
- };
590
- if (keyedBlocks.length > 0) properties.metadata = {
591
- type: "object",
592
- description: "Optional metadata for keyed entries (skill collections, searchable blocks: " + keyedBlocks.map((b) => `"${b.label}"`).join(", ") + "). Short content doesn't need metadata; longer loadable entries (skills) benefit from a title and description so the model can pick the right one without loading it.",
593
- properties: {
594
- title: {
595
- type: "string",
596
- description: "Short title. Used as a stable identifier — entries with the same title are updated in place, different titles create new entries."
597
- },
598
- description: {
599
- type: "string",
600
- description: "One-line summary shown alongside the title in the system prompt so the model can decide when to load the entry."
601
- }
602
- }
603
- };
604
- const metadataHint = keyedBlocks.length > 0 ? "\n\nFor keyed blocks (skill collections / searchable), pass `metadata: { title, description }` — title stabilises updates, description helps the model pick entries. Metadata is optional; short content rarely needs it, long loadable entries benefit most." : "";
605
- toolSet.set_context = {
606
- description: `Write to a context block. Available blocks:\n${blockDescriptions.join("\n")}\n\nWrites are durable and persist across sessions.${metadataHint}`,
607
- inputSchema: z.fromJSONSchema({
608
- type: "object",
609
- properties,
610
- required: ["label", "content"]
611
- }),
612
- execute: async ({ label, content, metadata, action }) => {
613
- try {
614
- const block = this.blocks.get(label);
615
- if (!block) return `Error: block "${label}" not found`;
616
- if (block.isSkill || block.isSearchable) {
617
- const title = metadata?.title;
618
- const description = metadata?.description;
619
- const key = contextEntryKey(title, content);
620
- if (block.isSkill) await this.setSkill(label, key, content, description ?? title);
621
- else await this.setSearchEntry(label, key, content);
622
- return `Indexed "${key}" in ${label}.`;
623
- }
624
- const updated = action === "append" ? await this.appendToBlock(label, content) : await this.setBlock(label, content);
625
- return `Written to ${label}. Usage: ${updated.maxTokens ? `${Math.round(updated.tokens / updated.maxTokens * 100)}% (${updated.tokens}/${updated.maxTokens} tokens)` : `${updated.tokens} tokens`}`;
626
- } catch (err) {
627
- return `Error: ${err instanceof Error ? err.message : String(err)}`;
628
- }
629
- }
630
- };
631
- }
632
- if (hasSkills) {
633
- const skillLabels = this.getSkillLabels();
634
- toolSet.load_context = {
635
- 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.",
636
- inputSchema: z.fromJSONSchema({
637
- type: "object",
638
- properties: {
639
- label: {
640
- type: "string",
641
- enum: skillLabels,
642
- description: "Skill block label"
643
- },
644
- key: {
645
- type: "string",
646
- description: "Skill key to load"
647
- }
648
- },
649
- required: ["label", "key"]
650
- }),
651
- execute: async ({ label, key }) => {
652
- try {
653
- if (!skillLabels.includes(label)) return `Error: "${label}" is not a skill block. Skill blocks: ${skillLabels.join(", ")}`;
654
- return await this.loadSkill(label, key) ?? `Not found: ${key}`;
655
- } catch (err) {
656
- return `Error: ${err instanceof Error ? err.message : String(err)}`;
657
- }
658
- }
659
- };
660
- const loadedList = [...this._loadedSkills];
661
- toolSet.unload_context = {
662
- description: "Unload a previously loaded skill to free context space. The skill remains available for re-loading." + (loadedList.length > 0 ? " Currently loaded: " + loadedList.join(", ") + "." : " No skills currently loaded."),
663
- inputSchema: z.fromJSONSchema({
664
- type: "object",
665
- properties: {
666
- label: {
667
- type: "string",
668
- enum: skillLabels,
669
- description: "Skill block label"
670
- },
671
- key: {
672
- type: "string",
673
- description: "Skill key to unload"
674
- }
675
- },
676
- required: ["label", "key"]
677
- }),
678
- execute: async ({ label, key }) => {
679
- if (!skillLabels.includes(label)) return `Error: "${label}" is not a skill block. Skill blocks: ${skillLabels.join(", ")}`;
680
- if (!this.unloadSkill(label, key)) return `Skill "${key}" is not currently loaded in "${label}".`;
681
- return `Unloaded "${key}" from ${label}. Context reclaimed.`;
682
- }
683
- };
684
- }
685
- if (hasSearch) {
686
- const searchLabels = this.getSearchLabels();
687
- toolSet.search_context = {
688
- description: "Search for information in a searchable context block. ONLY these blocks are searchable: " + searchLabels.map((l) => `"${l}"`).join(", ") + ". Other blocks cannot be searched.",
689
- inputSchema: z.fromJSONSchema({
690
- type: "object",
691
- properties: {
692
- label: {
693
- type: "string",
694
- enum: searchLabels,
695
- description: "Searchable block label"
696
- },
697
- query: {
698
- type: "string",
699
- description: "Search query"
700
- }
701
- },
702
- required: ["label", "query"]
703
- }),
704
- execute: async ({ label, query }) => {
705
- try {
706
- if (!searchLabels.includes(label)) return `Error: "${label}" is not searchable. Searchable blocks: ${searchLabels.join(", ")}`;
707
- return await this.searchContext(label, query) ?? "No results found.";
708
- } catch (err) {
709
- return `Error: ${err instanceof Error ? err.message : String(err)}`;
710
- }
711
- }
712
- };
713
- }
714
- return toolSet;
715
- }
716
- };
717
- //#endregion
718
- //#region src/experimental/memory/session/providers/agent.ts
719
- /**
720
- * Bounds for each content-hydration query on a history path.
721
- *
722
- * Message rows can be up to ~1.8MB each (see ROW_MAX_BYTES in agents/chat),
723
- * so content is fetched in bounded batches rather than one statement to keep
724
- * any per-statement buffering in the SQLite layer small. In workerd the
725
- * SQLite allocator shares the isolate's memory budget with the JS heap —
726
- * oversized transient result sets surface as SQLITE_NOMEM (#1710).
727
- *
728
- * Chunks are bounded by BOTH row count and cumulative stored bytes (sizes
729
- * come from the path row stats, so no content is read to compute them).
730
- * Without the byte bound, 50 near-cap rows could still materialize ~90MB
731
- * in a single statement.
732
- */
733
- const HISTORY_CONTENT_CHUNK_SIZE = 50;
734
- const HISTORY_CONTENT_CHUNK_BYTES = 4 * 1024 * 1024;
735
- var AgentSessionProvider = class {
736
- /**
737
- * @param agent - Agent or any object with a `sql` tagged template method
738
- * @param sessionId - Optional session ID to isolate multiple sessions in the same DO.
739
- * Messages are filtered by session_id within shared tables.
740
- */
741
- constructor(agent, sessionId) {
742
- this.initialized = false;
743
- this.activeLeafId = void 0;
744
- this.agent = agent;
745
- this.sessionId = sessionId ?? "";
746
- }
747
- ensureTable() {
748
- if (this.initialized) return;
749
- this.agent.sql`
750
- CREATE TABLE IF NOT EXISTS assistant_messages (
751
- id TEXT PRIMARY KEY,
752
- session_id TEXT NOT NULL DEFAULT '',
753
- parent_id TEXT,
754
- role TEXT NOT NULL,
755
- content TEXT NOT NULL,
756
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
757
- )
758
- `;
759
- this.agent.sql`
760
- CREATE INDEX IF NOT EXISTS idx_assistant_msg_parent
761
- ON assistant_messages(parent_id)
762
- `;
763
- this.agent.sql`
764
- CREATE INDEX IF NOT EXISTS idx_assistant_msg_session
765
- ON assistant_messages(session_id)
766
- `;
767
- this.agent.sql`
768
- CREATE TABLE IF NOT EXISTS assistant_compactions (
769
- id TEXT PRIMARY KEY,
770
- session_id TEXT NOT NULL DEFAULT '',
771
- summary TEXT NOT NULL,
772
- from_message_id TEXT NOT NULL,
773
- to_message_id TEXT NOT NULL,
774
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
775
- )
776
- `;
777
- this.agent.sql`
778
- CREATE VIRTUAL TABLE IF NOT EXISTS assistant_fts
779
- USING fts5(id UNINDEXED, session_id UNINDEXED, role UNINDEXED, content, tokenize='porter unicode61')
780
- `;
781
- this.agent.sql`
782
- CREATE TABLE IF NOT EXISTS assistant_config (
783
- session_id TEXT NOT NULL,
784
- key TEXT NOT NULL,
785
- value TEXT NOT NULL,
786
- PRIMARY KEY (session_id, key)
787
- )
788
- `;
789
- this.initialized = true;
790
- }
791
- getMessage(id) {
792
- this.ensureTable();
793
- const rows = this.agent.sql`
794
- SELECT content FROM assistant_messages WHERE id = ${id} AND session_id = ${this.sessionId}
795
- `;
796
- return rows.length > 0 ? this.parse(rows[0].content) : null;
797
- }
798
- getHistory(leafId) {
799
- this.ensureTable();
800
- const leaf = leafId ? this.leafRowById(leafId) : this.latestLeafRow();
801
- if (!leaf) return [];
802
- const messages = this.messagesByPathStats(this.pathRowStats(leaf.id));
803
- const compactions = this.getCompactions();
804
- if (compactions.length === 0) return messages;
805
- return this.applyCompactions(messages, compactions);
806
- }
807
- getRecentHistory(leafId, maxContentBytes, minRecentMessages = 1) {
808
- this.ensureTable();
809
- const leaf = leafId ? this.leafRowById(leafId) : this.latestLeafRow();
810
- if (!leaf) return {
811
- messages: [],
812
- truncated: false,
813
- totalContentBytes: 0
814
- };
815
- const stats = this.pathRowStats(leaf.id);
816
- const totalContentBytes = stats.reduce((sum, row) => sum + row.bytes, 0);
817
- const minRecent = Math.max(1, Math.floor(minRecentMessages));
818
- let start = stats.length - 1;
819
- let used = stats[start]?.bytes ?? 0;
820
- while (start > 0 && (stats.length - start < minRecent || used + stats[start - 1].bytes <= maxContentBytes)) {
821
- start--;
822
- used += stats[start].bytes;
823
- }
824
- const messages = this.messagesByPathStats(stats.slice(start));
825
- const compactions = this.getCompactions();
826
- return {
827
- messages: compactions.length === 0 ? messages : this.applyCompactions(messages, compactions),
828
- truncated: start > 0,
829
- totalContentBytes
830
- };
831
- }
832
- getHistoryRowStats(leafId) {
833
- this.ensureTable();
834
- const leaf = leafId ? this.leafRowById(leafId) : this.latestLeafRow();
835
- return leaf ? this.pathRowStats(leaf.id) : [];
836
- }
837
- getLatestLeaf() {
838
- this.ensureTable();
839
- const row = this.latestLeafRow();
840
- return row ? this.getMessage(row.id) : null;
841
- }
842
- getBranches(messageId) {
843
- this.ensureTable();
844
- const rows = this.agent.sql`
845
- SELECT content FROM assistant_messages
846
- WHERE parent_id = ${messageId} AND session_id = ${this.sessionId} ORDER BY created_at ASC
847
- `;
848
- return this.parseRows(rows);
849
- }
850
- getPathLength(leafId) {
851
- this.ensureTable();
852
- const leaf = leafId ? this.agent.sql`
853
- SELECT id FROM assistant_messages WHERE id = ${leafId} AND session_id = ${this.sessionId}
854
- `[0] : this.latestLeafRow();
855
- if (!leaf) return 0;
856
- return this.agent.sql`
857
- WITH RECURSIVE path AS (
858
- SELECT id, parent_id, 0 as depth FROM assistant_messages WHERE id = ${leaf.id}
859
- UNION ALL
860
- SELECT m.id, m.parent_id, p.depth + 1 FROM assistant_messages m
861
- JOIN path p ON m.id = p.parent_id
862
- WHERE m.session_id = ${this.sessionId} AND p.depth < 10000
863
- )
864
- SELECT COUNT(*) as count FROM path
865
- `[0]?.count ?? 0;
866
- }
867
- appendMessage(message, parentId) {
868
- this.ensureTable();
869
- if (this.agent.sql`
870
- SELECT id FROM assistant_messages WHERE id = ${message.id} AND session_id = ${this.sessionId}
871
- `.length > 0) return;
872
- let parent = parentId !== void 0 ? parentId : this.latestLeafRow()?.id ?? null;
873
- if (parent) {
874
- if (this.agent.sql`
875
- SELECT id FROM assistant_messages WHERE id = ${parent} AND session_id = ${this.sessionId}
876
- `.length === 0) parent = null;
877
- }
878
- const json = JSON.stringify(message);
879
- this.agent.sql`
880
- INSERT INTO assistant_messages (id, session_id, parent_id, role, content)
881
- VALUES (${message.id}, ${this.sessionId}, ${parent}, ${message.role}, ${json})
882
- `;
883
- this.indexFTS(message);
884
- this.activeLeafId = message.id;
885
- }
886
- updateMessage(message) {
887
- this.ensureTable();
888
- this.agent.sql`
889
- UPDATE assistant_messages SET content = ${JSON.stringify(message)}
890
- WHERE id = ${message.id} AND session_id = ${this.sessionId}
891
- `;
892
- this.indexFTS(message);
893
- }
894
- deleteMessages(messageIds) {
895
- this.ensureTable();
896
- for (const id of messageIds) {
897
- this.agent.sql`DELETE FROM assistant_messages WHERE id = ${id} AND session_id = ${this.sessionId}`;
898
- this.deleteFTS(id);
899
- }
900
- if (typeof this.activeLeafId === "string" && messageIds.includes(this.activeLeafId)) this.activeLeafId = void 0;
901
- }
902
- clearMessages() {
903
- this.ensureTable();
904
- this.agent.sql`DELETE FROM assistant_messages WHERE session_id = ${this.sessionId}`;
905
- this.agent.sql`DELETE FROM assistant_compactions WHERE session_id = ${this.sessionId}`;
906
- const ftsRows = this.agent.sql`
907
- SELECT rowid FROM assistant_fts WHERE session_id = ${this.sessionId}
908
- `;
909
- for (const row of ftsRows) this.agent.sql`DELETE FROM assistant_fts WHERE rowid = ${row.rowid}`;
910
- this.activeLeafId = void 0;
911
- }
912
- addCompaction(summary, fromMessageId, toMessageId) {
913
- this.ensureTable();
914
- const id = crypto.randomUUID();
915
- this.agent.sql`
916
- INSERT INTO assistant_compactions (id, session_id, summary, from_message_id, to_message_id)
917
- VALUES (${id}, ${this.sessionId}, ${summary}, ${fromMessageId}, ${toMessageId})
918
- `;
919
- return {
920
- id,
921
- summary,
922
- fromMessageId,
923
- toMessageId,
924
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
925
- };
926
- }
927
- getCompactions() {
928
- this.ensureTable();
929
- return this.agent.sql`
930
- SELECT * FROM assistant_compactions WHERE session_id = ${this.sessionId} ORDER BY created_at ASC
931
- `.map((r) => ({
932
- id: r.id,
933
- summary: r.summary,
934
- fromMessageId: r.from_message_id,
935
- toMessageId: r.to_message_id,
936
- createdAt: r.created_at
937
- }));
938
- }
939
- searchMessages(query, limit = 20) {
940
- this.ensureTable();
941
- const sanitized = `"${query.replace(/"/g, "\"\"")}"`;
942
- try {
943
- return this.agent.sql`
944
- SELECT f.id, f.role, f.content FROM assistant_fts f
945
- INNER JOIN assistant_messages m ON m.id = f.id AND m.session_id = f.session_id
946
- WHERE assistant_fts MATCH ${sanitized} AND f.session_id = ${this.sessionId}
947
- ORDER BY rank LIMIT ${limit}
948
- `.map((r) => ({
949
- id: r.id,
950
- role: r.role,
951
- content: r.content
952
- }));
953
- } catch {
954
- return [];
955
- }
956
- }
957
- latestLeafRow() {
958
- if (this.activeLeafId !== void 0) {
959
- if (this.agent.sql`
960
- SELECT m.id FROM assistant_messages m
961
- WHERE m.id = ${this.activeLeafId} AND m.session_id = ${this.sessionId}
962
- AND NOT EXISTS (
963
- SELECT 1 FROM assistant_messages c
964
- WHERE c.parent_id = ${this.activeLeafId}
965
- AND c.session_id = ${this.sessionId}
966
- )
967
- `.length > 0) return { id: this.activeLeafId };
968
- this.activeLeafId = void 0;
969
- }
970
- const rows = this.agent.sql`
971
- SELECT m.id FROM assistant_messages m
972
- LEFT JOIN assistant_messages c ON c.parent_id = m.id AND c.session_id = ${this.sessionId}
973
- WHERE c.id IS NULL AND m.session_id = ${this.sessionId}
974
- ORDER BY m.created_at DESC, m.rowid DESC LIMIT 1
975
- `;
976
- this.activeLeafId = rows[0]?.id;
977
- return rows[0] ?? null;
978
- }
979
- leafRowById(leafId) {
980
- return this.agent.sql`
981
- SELECT id FROM assistant_messages WHERE id = ${leafId} AND session_id = ${this.sessionId}
982
- `[0] ?? null;
983
- }
984
- /**
985
- * The active branch path as (id, role, content size) rows, root → leaf.
986
- *
987
- * Recurses over (id, parent_id) only. Carrying `content` through the
988
- * recursive queue AND the ORDER BY sorter materializes the entire
989
- * transcript several times over inside SQLite's allocator, which in
990
- * workerd shares the isolate's memory budget with the JS heap — large
991
- * media-heavy sessions then fail with SQLITE_NOMEM on wake (#1710).
992
- * Content is fetched separately in bounded chunks (`messagesByPathStats`).
993
- */
994
- pathRowStats(leafId) {
995
- return this.agent.sql`
996
- WITH RECURSIVE path(id, parent_id, depth) AS (
997
- SELECT id, parent_id, 0 FROM assistant_messages WHERE id = ${leafId}
998
- UNION ALL
999
- SELECT m.id, m.parent_id, p.depth + 1 FROM assistant_messages m
1000
- JOIN path p ON m.id = p.parent_id
1001
- WHERE m.session_id = ${this.sessionId} AND p.depth < 10000
1002
- )
1003
- SELECT path.id AS id, am.role AS role, LENGTH(CAST(am.content AS BLOB)) AS bytes
1004
- FROM path JOIN assistant_messages am ON am.id = path.id
1005
- ORDER BY path.depth DESC
1006
- `;
1007
- }
1008
- /**
1009
- * Fetch and parse message content for an ordered list of path rows.
1010
- *
1011
- * Content is read in chunks bounded by both row count and cumulative
1012
- * stored bytes (no ORDER BY — SQLite streams rows without materializing
1013
- * the result set) and reassembled in path order. Rows that fail to parse
1014
- * are skipped, matching previous behavior.
1015
- */
1016
- messagesByPathStats(rows) {
1017
- const contentById = /* @__PURE__ */ new Map();
1018
- const fetchChunk = (ids) => {
1019
- const fetched = this.agent.sql`
1020
- SELECT id, content FROM assistant_messages
1021
- WHERE session_id = ${this.sessionId}
1022
- AND id IN (SELECT value FROM json_each(${JSON.stringify(ids)}))
1023
- `;
1024
- for (const row of fetched) contentById.set(row.id, row.content);
1025
- };
1026
- let chunk = [];
1027
- let chunkBytes = 0;
1028
- for (const row of rows) {
1029
- if (chunk.length > 0 && (chunk.length >= HISTORY_CONTENT_CHUNK_SIZE || chunkBytes + row.bytes > HISTORY_CONTENT_CHUNK_BYTES)) {
1030
- fetchChunk(chunk);
1031
- chunk = [];
1032
- chunkBytes = 0;
1033
- }
1034
- chunk.push(row.id);
1035
- chunkBytes += row.bytes;
1036
- }
1037
- if (chunk.length > 0) fetchChunk(chunk);
1038
- const result = [];
1039
- for (const row of rows) {
1040
- const content = contentById.get(row.id);
1041
- if (content === void 0) continue;
1042
- const msg = this.parse(content);
1043
- if (msg) result.push(msg);
1044
- }
1045
- return result;
1046
- }
1047
- indexFTS(message) {
1048
- const text = message.parts.filter((p) => p.type === "text").map((p) => p.text).join(" ");
1049
- this.deleteFTS(message.id);
1050
- if (text) this.agent.sql`
1051
- INSERT INTO assistant_fts (id, session_id, role, content)
1052
- VALUES (${message.id}, ${this.sessionId}, ${message.role}, ${text})
1053
- `;
1054
- }
1055
- deleteFTS(id) {
1056
- const rows = this.agent.sql`
1057
- SELECT rowid FROM assistant_fts WHERE id = ${id} AND session_id = ${this.sessionId}
1058
- `;
1059
- for (const row of rows) this.agent.sql`DELETE FROM assistant_fts WHERE rowid = ${row.rowid}`;
1060
- }
1061
- applyCompactions(messages, compactions) {
1062
- const ids = messages.map((m) => m.id);
1063
- const result = [];
1064
- let i = 0;
1065
- while (i < messages.length) {
1066
- const matching = compactions.filter((c) => c.fromMessageId === ids[i]);
1067
- const comp = matching.length > 1 ? matching[matching.length - 1] : matching[0];
1068
- if (comp) {
1069
- const endIdx = ids.indexOf(comp.toMessageId);
1070
- if (endIdx >= i) {
1071
- result.push({
1072
- id: `${COMPACTION_PREFIX}${comp.id}`,
1073
- role: "assistant",
1074
- parts: [{
1075
- type: "text",
1076
- text: comp.summary
1077
- }],
1078
- createdAt: /* @__PURE__ */ new Date()
1079
- });
1080
- i = endIdx + 1;
1081
- continue;
1082
- }
1083
- }
1084
- result.push(messages[i]);
1085
- i++;
1086
- }
1087
- return result;
1088
- }
1089
- parse(json) {
1090
- try {
1091
- const msg = JSON.parse(json);
1092
- if (typeof msg?.id === "string" && typeof msg?.role === "string" && Array.isArray(msg?.parts)) return msg;
1093
- } catch {}
1094
- return null;
1095
- }
1096
- parseRows(rows) {
1097
- const result = [];
1098
- for (const row of rows) {
1099
- const msg = this.parse(row.content);
1100
- if (msg) result.push(msg);
1101
- }
1102
- return result;
1103
- }
1104
- };
1105
- //#endregion
1106
- //#region src/experimental/memory/session/providers/agent-context.ts
1107
- var AgentContextProvider = class {
1108
- constructor(agent, label) {
1109
- this.initialized = false;
1110
- this.agent = agent;
1111
- this.label = label ?? "";
1112
- }
1113
- init(label) {
1114
- if (!this.label) this.label = label;
1115
- }
1116
- ensureTable() {
1117
- if (this.initialized) return;
1118
- this.agent.sql`
1119
- CREATE TABLE IF NOT EXISTS cf_agents_context_blocks (
1120
- label TEXT PRIMARY KEY,
1121
- content TEXT NOT NULL,
1122
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
1123
- )
1124
- `;
1125
- this.initialized = true;
1126
- }
1127
- async get() {
1128
- this.ensureTable();
1129
- return this.agent.sql`
1130
- SELECT content FROM cf_agents_context_blocks WHERE label = ${this.label}
1131
- `[0]?.content ?? null;
1132
- }
1133
- async set(content) {
1134
- this.ensureTable();
1135
- this.agent.sql`
1136
- INSERT INTO cf_agents_context_blocks (label, content)
1137
- VALUES (${this.label}, ${content})
1138
- ON CONFLICT(label) DO UPDATE SET content = ${content}, updated_at = CURRENT_TIMESTAMP
1139
- `;
1140
- }
1141
- };
1142
- //#endregion
1143
- //#region src/experimental/memory/session/session.ts
1144
- function isBroadcaster(obj) {
1145
- return typeof obj === "object" && obj !== null && "broadcast" in obj && typeof obj.broadcast === "function";
1146
- }
1147
- function isSqlProvider(arg) {
1148
- return "sql" in arg && typeof arg.sql === "function";
1149
- }
1150
- var Session = class Session {
1151
- constructor(storage, options) {
1152
- this._warnedCompactionNoOp = false;
1153
- this._ready = false;
1154
- this._skillScanRan = false;
1155
- this._warnedNoRecentHistorySupport = false;
1156
- this.storage = storage;
1157
- this.context = new ContextBlocks(options?.context ?? [], options?.promptStore);
1158
- this._tokenCounter = options?.tokenCounter;
1159
- this._compactionErrorHandler = options?.onCompactionError;
1160
- this._ready = true;
1161
- }
1162
- /**
1163
- * Chainable session creation with auto-wired providers.
1164
- *
1165
- * Pass a `SqlProvider` (Agent with `sql` method) for auto-wired SQLite,
1166
- * or a `SessionProvider` directly for custom storage (Postgres, etc.).
1167
- *
1168
- * @example
1169
- * ```ts
1170
- * // Auto-wired SQLite (DO Agent)
1171
- * const session = Session.create(this)
1172
- * .withContext("soul", { provider: { get: async () => "You are helpful." } })
1173
- * .withContext("memory", { description: "Learned facts", maxTokens: 1100 })
1174
- * .withCachedPrompt();
1175
- *
1176
- * // Skills from R2 (on-demand loading via load_context tool)
1177
- * const session = Session.create(this)
1178
- * .withContext("skills", {
1179
- * provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" })
1180
- * })
1181
- * .withCachedPrompt();
1182
- *
1183
- * // Custom storage provider (Postgres, etc.)
1184
- * const session = Session.create(postgresProvider)
1185
- * .withContext("memory", {
1186
- * maxTokens: 1100,
1187
- * provider: new PostgresContextProvider(conn, "memory")
1188
- * })
1189
- * .withCachedPrompt(new PostgresContextProvider(conn, "_prompt"));
1190
- * ```
1191
- */
1192
- static create(provider) {
1193
- const session = Object.create(Session.prototype);
1194
- if (isSqlProvider(provider)) {
1195
- session._agent = provider;
1196
- if (isBroadcaster(provider)) session._broadcaster = provider;
1197
- } else session._storageProvider = provider;
1198
- session._pending = [];
1199
- session._ready = false;
1200
- return session;
1201
- }
1202
- forSession(sessionId) {
1203
- this._sessionId = sessionId;
1204
- return this;
1205
- }
1206
- withContext(label, options) {
1207
- this._pending.push({
1208
- label,
1209
- options: options ?? {}
1210
- });
1211
- return this;
1212
- }
1213
- withCachedPrompt(provider) {
1214
- this._cachedPrompt = provider ?? true;
1215
- return this;
1216
- }
1217
- /**
1218
- * Register a compaction function. Called by `compact()` to compress
1219
- * message history into a summary overlay.
1220
- */
1221
- onCompaction(fn) {
1222
- this._compactionFn = fn;
1223
- return this;
1224
- }
1225
- /**
1226
- * Auto-compact when estimated token count exceeds the threshold.
1227
- * Checked after each `appendMessage`. Requires `onCompaction()`.
1228
- *
1229
- * By default this uses a Workers-safe heuristic over stored messages plus
1230
- * the Session-managed frozen system prompt. Provide `tokenCounter` when you
1231
- * have model-reported usage or a tokenizer and need a stricter budget.
1232
- */
1233
- compactAfter(tokenThreshold, options) {
1234
- this._tokenThreshold = tokenThreshold;
1235
- if (options?.tokenCounter) this._tokenCounter = options.tokenCounter;
1236
- return this;
1237
- }
1238
- /**
1239
- * Handle failures from the automatic `compactAfter()` trigger.
1240
- *
1241
- * Manual `compact()` still reports errors through the existing session error
1242
- * broadcast path.
1243
- */
1244
- onCompactionError(handler) {
1245
- this._compactionErrorHandler = handler;
1246
- return this;
1247
- }
1248
- /**
1249
- * @internal
1250
- * Framework hook for cache-owning callers that need to mirror message
1251
- * storage changes. Application code should use the normal Session methods.
1252
- */
1253
- internal_onMessagesChanged(listener) {
1254
- this._messageChangeListener = listener ?? void 0;
1255
- return this;
1256
- }
1257
- _ensureReady() {
1258
- if (this._ready) return;
1259
- const configs = (this._pending ?? []).map(({ label, options: opts }) => {
1260
- let provider = opts.provider;
1261
- if (!provider && this._agent) {
1262
- const key = this._sessionId ? `${label}_${this._sessionId}` : label;
1263
- provider = new AgentContextProvider(this._agent, key);
1264
- }
1265
- return {
1266
- label,
1267
- description: opts.description,
1268
- maxTokens: opts.maxTokens,
1269
- provider
1270
- };
1271
- });
1272
- let promptStore;
1273
- if (this._cachedPrompt === true && this._agent) {
1274
- const key = this._sessionId ? `_system_prompt_${this._sessionId}` : "_system_prompt";
1275
- promptStore = new AgentContextProvider(this._agent, key);
1276
- } else if (this._cachedPrompt && this._cachedPrompt !== true) promptStore = this._cachedPrompt;
1277
- if (this._storageProvider) this.storage = this._storageProvider;
1278
- else if (this._agent) this.storage = new AgentSessionProvider(this._agent, this._sessionId);
1279
- else throw new Error("Session.create() requires a SqlProvider or SessionProvider");
1280
- this.context = new ContextBlocks(configs, promptStore);
1281
- this.context.setUnloadCallback((label, key) => {
1282
- this._reclaimLoadedSkill(label, key).catch(() => {});
1283
- });
1284
- this._ready = true;
1285
- this._restorePromise = this._restoreLoadedSkills().catch(() => {});
1286
- }
1287
- /**
1288
- * Await the background skill-restore kicked off by `_ensureReady()`.
1289
- * Idempotent and cheap — every async public method calls this so that
1290
- * `_loadedSkills` reflects conversation history before any read or write.
1291
- */
1292
- async _ensureRestored() {
1293
- this._ensureReady();
1294
- if (this._restorePromise) await this._restorePromise;
1295
- }
1296
- async _notifyMessagesChanged(event) {
1297
- await this._messageChangeListener?.(event);
1298
- }
1299
- /**
1300
- * Reconstruct which skills are loaded by scanning conversation history
1301
- * for load_context tool results that haven't been unloaded.
1302
- * Runs once per init to survive hibernation / eviction, including for
1303
- * async SessionProviders (e.g. Postgres) where we must `await` history.
1304
- *
1305
- * Skipped entirely when no skill-capable provider is configured —
1306
- * `load_context` results can only exist when a skill block was registered,
1307
- * and the scan would otherwise read the whole transcript on every wake,
1308
- * bypassing byte-budgeted hydration (#1710). A skill block added later via
1309
- * `addContext()` triggers the scan at that point instead.
1310
- */
1311
- async _restoreLoadedSkills() {
1312
- if (!this.context.hasSkillCapableConfigs()) return;
1313
- await this._scanHistoryForLoadedSkills();
1314
- }
1315
- /**
1316
- * Scan stored history for load/unload_context tool results and restore
1317
- * the loaded-skill tracking set.
1318
- *
1319
- * Memory-bounded when the provider supports `getHistoryRowStats`: rows
1320
- * are enumerated without content, then only assistant rows are fetched
1321
- * and scanned ONE AT A TIME — peak memory is a single message instead of
1322
- * the whole transcript. Falls back to a full `getHistory()` read for
1323
- * providers without row stats (e.g. Postgres).
1324
- *
1325
- * Note: the bounded path scans raw path rows, so `load_context` results
1326
- * inside compacted ranges are still seen (the full-read path hides them
1327
- * behind compaction overlays). That superset is intentional — the stored
1328
- * tool result still exists and can be reclaimed by `unloadSkill`.
1329
- */
1330
- async _scanHistoryForLoadedSkills() {
1331
- this._skillScanRan = true;
1332
- const loaded = /* @__PURE__ */ new Set();
1333
- const scanMessage = (msg) => {
1334
- if (msg.role !== "assistant") return;
1335
- for (const part of msg.parts) if (part.toolName === "load_context" && part.state === "output-available") {
1336
- const input = part.input;
1337
- if (input?.label && input?.key) {
1338
- const id = `${input.label}:${input.key}`;
1339
- if (typeof part.output === "string" && part.output.startsWith("[skill unloaded:")) loaded.delete(id);
1340
- else loaded.add(id);
1341
- }
1342
- } else if (part.toolName === "unload_context" && part.state === "output-available") {
1343
- const input = part.input;
1344
- if (input?.label && input?.key) loaded.delete(`${input.label}:${input.key}`);
1345
- }
1346
- };
1347
- if (this.storage.getHistoryRowStats) {
1348
- const stats = await this.storage.getHistoryRowStats();
1349
- for (const row of stats) {
1350
- if (row.role !== "assistant") continue;
1351
- const msg = await this.storage.getMessage(row.id);
1352
- if (msg) scanMessage(msg);
1353
- }
1354
- } else for (const msg of await this.storage.getHistory()) scanMessage(msg);
1355
- if (loaded.size > 0) this.context.restoreLoadedSkills(loaded);
1356
- }
1357
- /**
1358
- * Reclaim context-window tokens consumed by a previously loaded skill.
1359
- *
1360
- * When a skill is loaded via the `load_context` tool, its full body is
1361
- * embedded as that tool call's `output-available` result inside the
1362
- * assistant message — which means every subsequent turn replays the
1363
- * entire skill as part of the conversation history and pays for it in
1364
- * input tokens.
1365
- *
1366
- * This method walks back through history, finds the matching
1367
- * `load_context` tool result for `(label, key)`, and replaces its bulky
1368
- * `output` with a short marker `[skill unloaded: <key>]`. The skill
1369
- * content is dropped from future turns and the tokens are reclaimed.
1370
- * The skill itself stays available to reload via `load_context`.
1371
- */
1372
- async _reclaimLoadedSkill(label, key) {
1373
- const history = await this.storage.getHistory();
1374
- for (let i = history.length - 1; i >= 0; i--) {
1375
- const msg = history[i];
1376
- if (msg.role !== "assistant") continue;
1377
- let changed = false;
1378
- const newParts = msg.parts.map((part) => {
1379
- if (part.toolName === "load_context" && part.state === "output-available") {
1380
- const input = part.input;
1381
- if (input?.label === label && input?.key === key) {
1382
- changed = true;
1383
- return {
1384
- ...part,
1385
- output: `[skill unloaded: ${key}]`
1386
- };
1387
- }
1388
- }
1389
- return part;
1390
- });
1391
- if (changed) {
1392
- await this.updateMessage({
1393
- ...msg,
1394
- parts: newParts
1395
- });
1396
- return;
1397
- }
1398
- }
1399
- }
1400
- async getHistory(leafId) {
1401
- await this._ensureRestored();
1402
- return this.storage.getHistory(leafId);
1403
- }
1404
- /**
1405
- * Byte-budgeted read of the most recent messages on the active branch
1406
- * path (always at least the leaf message, and at least
1407
- * `minRecentMessages` when the path is long enough). Lets hosts hydrate
1408
- * a bounded window instead of the full transcript so wake-time memory
1409
- * scales with the budget rather than total session history (#1710).
1410
- *
1411
- * Falls back to a full (untruncated) read when the provider doesn't
1412
- * implement `getRecentHistory`. The fallback reports honest metadata
1413
- * (`truncated: false` and the real serialized size) and warns once so a
1414
- * host relying on the budget knows it is not being enforced.
1415
- */
1416
- async getRecentHistory(maxContentBytes, minRecentMessages = 1) {
1417
- await this._ensureRestored();
1418
- if (this.storage.getRecentHistory) return this.storage.getRecentHistory(null, maxContentBytes, minRecentMessages);
1419
- if (!this._warnedNoRecentHistorySupport) {
1420
- this._warnedNoRecentHistorySupport = true;
1421
- console.warn("[Session] The configured SessionProvider does not implement getRecentHistory; the requested byte budget cannot be enforced and the FULL history was loaded. Implement getRecentHistory (and getHistoryRowStats) on the provider to bound hydration.");
1422
- }
1423
- const messages = await this.storage.getHistory();
1424
- let totalContentBytes = 0;
1425
- for (const message of messages) totalContentBytes += JSON.stringify(message).length;
1426
- return {
1427
- messages,
1428
- truncated: false,
1429
- totalContentBytes
1430
- };
1431
- }
1432
- /**
1433
- * Per-row stored sizes for the active branch path (root → leaf) WITHOUT
1434
- * loading message content, or `null` when the provider doesn't support it.
1435
- * Lets hosts find oversized rows (e.g. inline base64 media) and process
1436
- * them one at a time with bounded memory.
1437
- */
1438
- async getHistoryRowStats() {
1439
- await this._ensureRestored();
1440
- if (!this.storage.getHistoryRowStats) return null;
1441
- return this.storage.getHistoryRowStats();
1442
- }
1443
- async getMessage(id) {
1444
- await this._ensureRestored();
1445
- return this.storage.getMessage(id);
1446
- }
1447
- async getLatestLeaf() {
1448
- await this._ensureRestored();
1449
- return this.storage.getLatestLeaf();
1450
- }
1451
- async getBranches(messageId) {
1452
- await this._ensureRestored();
1453
- return this.storage.getBranches(messageId);
1454
- }
1455
- async getPathLength(leafId) {
1456
- await this._ensureRestored();
1457
- return this.storage.getPathLength(leafId);
1458
- }
1459
- _broadcast(type, data) {
1460
- if (!this._broadcaster) return;
1461
- this._broadcaster.broadcast(JSON.stringify({
1462
- type,
1463
- ...data
1464
- }));
1465
- }
1466
- _shouldEstimateTokens() {
1467
- return Boolean(this._broadcaster || this._tokenThreshold != null && this._compactionFn);
1468
- }
1469
- async _estimateTokenCount() {
1470
- const messages = await this.getHistory();
1471
- const systemPrompt = await this.context.getSystemPromptForEstimate();
1472
- if (this._tokenCounter) {
1473
- if (!this.context.isLoaded()) await this.context.load();
1474
- const contextBlocks = this.context.getBlocks();
1475
- const estimate = await this._tokenCounter({
1476
- messages,
1477
- systemPrompt,
1478
- contextBlocks
1479
- });
1480
- return Number.isFinite(estimate) ? Math.max(0, Math.ceil(estimate)) : 0;
1481
- }
1482
- return estimateMessageTokens(messages) + estimateStringTokens(systemPrompt);
1483
- }
1484
- async _handleAutoCompactionError(error) {
1485
- const message = error instanceof Error ? error.message : String(error);
1486
- if (this._compactionErrorHandler) try {
1487
- await this._compactionErrorHandler(error);
1488
- } catch (handlerError) {
1489
- const handlerMessage = handlerError instanceof Error ? handlerError.message : String(handlerError);
1490
- console.warn(`Session auto-compaction error handler failed: ${handlerMessage}`);
1491
- }
1492
- else console.warn(`Session auto-compaction failed: ${message}`);
1493
- this._emitError(message);
1494
- }
1495
- async _emitStatus(phase, extra) {
1496
- let tokenEstimate = 0;
1497
- if (this._shouldEstimateTokens()) try {
1498
- tokenEstimate = await this._estimateTokenCount();
1499
- } catch (err) {
1500
- await this._handleAutoCompactionError(err);
1501
- }
1502
- this._broadcast("cf_agent_session", {
1503
- phase,
1504
- tokenEstimate,
1505
- tokenThreshold: this._tokenThreshold ?? null,
1506
- ...extra
1507
- });
1508
- return tokenEstimate;
1509
- }
1510
- _emitError(error) {
1511
- this._broadcast("cf_agent_session_error", { error });
1512
- }
1513
- async appendMessage(message, parentId) {
1514
- await this._appendMessage(message, parentId);
1515
- }
1516
- async _appendMessage(message, parentId) {
1517
- await this._ensureRestored();
1518
- if (await this.storage.getMessage(message.id)) {
1519
- await this._emitStatus("idle");
1520
- await this._notifyMessagesChanged({
1521
- type: "append",
1522
- message,
1523
- parentId,
1524
- inserted: false
1525
- });
1526
- return;
1527
- }
1528
- await this.storage.appendMessage(message, parentId);
1529
- const tokenEstimate = await this._emitStatus("idle");
1530
- let compacted = false;
1531
- if (this._tokenThreshold != null && this._compactionFn && tokenEstimate > this._tokenThreshold) try {
1532
- compacted = Boolean(await this.compact());
1533
- if (!compacted && !this._warnedCompactionNoOp) {
1534
- this._warnedCompactionNoOp = true;
1535
- console.warn(`[Session] Auto-compaction fired (~${tokenEstimate} tokens > ${this._tokenThreshold}) but the compaction function returned null, so history was not shortened. ` + (this._tokenCounter ? `A tokenCounter is configured and now flows to the boundary logic, but it is invoked per-message there — a whole-prompt/usage counter (e.g. returning a fixed usage.inputTokens regardless of which messages are passed) degrades the tail budget to minTailMessages and can still no-op. Pass a per-message CompactOptions.tokenCounter for precise tail budgeting.` : `If your history is tool-heavy, configure a tokenCounter on compactAfter() — it flows to createCompactFunction's boundary logic automatically.`));
1536
- } else if (compacted) this._warnedCompactionNoOp = false;
1537
- } catch (err) {
1538
- await this._handleAutoCompactionError(err);
1539
- }
1540
- if (!compacted) await this._notifyMessagesChanged({
1541
- type: "append",
1542
- message,
1543
- parentId,
1544
- inserted: true
1545
- });
1546
- }
1547
- async updateMessage(message) {
1548
- await this._ensureRestored();
1549
- await this.storage.updateMessage(message);
1550
- await this._emitStatus("idle");
1551
- await this._notifyMessagesChanged({
1552
- type: "update",
1553
- message
1554
- });
1555
- }
1556
- /**
1557
- * @internal
1558
- * Rewrite a stored message WITHOUT the public-write side effects: no
1559
- * token-estimate status broadcast (which reads the FULL history) and no
1560
- * auto-compaction check. For framework maintenance passes that rewrite
1561
- * many rows with bounded memory — e.g. media eviction (#1710) — where the
1562
- * per-row full-history estimate would reintroduce the memory pressure the
1563
- * pass exists to remove. The message-change listener still fires so a
1564
- * cache-owning host stays coherent. Application code should use
1565
- * `updateMessage`.
1566
- */
1567
- async internal_rewriteMessage(message) {
1568
- await this._ensureRestored();
1569
- await this.storage.updateMessage(message);
1570
- await this._notifyMessagesChanged({
1571
- type: "update",
1572
- message
1573
- });
1574
- }
1575
- async deleteMessages(messageIds) {
1576
- await this._ensureRestored();
1577
- await this.storage.deleteMessages(messageIds);
1578
- await this._emitStatus("idle");
1579
- await this._notifyMessagesChanged({
1580
- type: "delete",
1581
- messageIds
1582
- });
1583
- }
1584
- async clearMessages() {
1585
- await this._ensureRestored();
1586
- await this.storage.clearMessages();
1587
- this.context.clearSkillState();
1588
- await this.context.refreshSystemPrompt();
1589
- await this._emitStatus("idle");
1590
- await this._notifyMessagesChanged({ type: "clear" });
1591
- }
1592
- async addCompaction(summary, fromMessageId, toMessageId) {
1593
- await this._ensureRestored();
1594
- return this.storage.addCompaction(summary, fromMessageId, toMessageId);
1595
- }
1596
- async getCompactions() {
1597
- await this._ensureRestored();
1598
- return this.storage.getCompactions();
1599
- }
1600
- /**
1601
- * Run the registered compaction function and store the result as an overlay.
1602
- * Requires `onCompaction()` to be called first.
1603
- */
1604
- async compact() {
1605
- await this._ensureRestored();
1606
- if (!this._compactionFn) throw new Error("No compaction function registered. Call onCompaction() first.");
1607
- const tokensBefore = await this._emitStatus("compacting");
1608
- let result;
1609
- try {
1610
- result = await this._compactionFn(await this.getHistory(), { tokenCounter: this._tokenCounter });
1611
- } catch (err) {
1612
- this._emitError(err instanceof Error ? err.message : String(err));
1613
- return null;
1614
- }
1615
- if (!result) {
1616
- await this._emitStatus("idle");
1617
- return null;
1618
- }
1619
- if (!new Set((await this.getHistory()).map((m) => m.id)).has(result.toMessageId)) {
1620
- await this._emitStatus("idle");
1621
- return null;
1622
- }
1623
- const existing = await this.getCompactions();
1624
- const fromId = existing.length > 0 ? existing[0].fromMessageId : result.fromMessageId;
1625
- await this.addCompaction(result.summary, fromId, result.toMessageId);
1626
- await this.refreshSystemPrompt();
1627
- await this._emitStatus("idle", { compacted: { tokensBefore } });
1628
- await this._notifyMessagesChanged({ type: "compact" });
1629
- return {
1630
- ...result,
1631
- fromMessageId: fromId
1632
- };
1633
- }
1634
- getContextBlock(label) {
1635
- this._ensureReady();
1636
- return this.context.getBlock(label);
1637
- }
1638
- getContextBlocks() {
1639
- this._ensureReady();
1640
- return this.context.getBlocks();
1641
- }
1642
- async replaceContextBlock(label, content) {
1643
- await this._ensureRestored();
1644
- return this.context.setBlock(label, content);
1645
- }
1646
- async appendContextBlock(label, content) {
1647
- await this._ensureRestored();
1648
- return this.context.appendToBlock(label, content);
1649
- }
1650
- /**
1651
- * Dynamically register a new context block after session initialization.
1652
- *
1653
- * This is a **builder / runtime API**, not an LLM tool. The LLM writes
1654
- * into existing context blocks via the `set_context` tool (see
1655
- * `ContextBlocks.tools()`); it cannot declare new blocks itself. This
1656
- * method is how extension / host code contributes blocks at runtime
1657
- * (e.g. an extension's `onLoad` handler registering its own memory block).
1658
- *
1659
- * The block's provider is initialized and loaded immediately.
1660
- * Call `refreshSystemPrompt()` afterward to include the new block
1661
- * in the system prompt.
1662
- *
1663
- * Note: When called without a provider, auto-wires to SQLite via
1664
- * AgentContextProvider. Requires the session to have been created
1665
- * via `Session.create(agent)` (not the direct constructor).
1666
- */
1667
- async addContext(label, options) {
1668
- await this._ensureRestored();
1669
- const opts = options ?? {};
1670
- let provider = opts.provider;
1671
- if (!provider) {
1672
- if (!this._agent) throw new Error(`addContext("${label}") requires an explicit provider when Session uses a SessionProvider`);
1673
- const key = this._sessionId ? `${label}_${this._sessionId}` : label;
1674
- provider = new AgentContextProvider(this._agent, key);
1675
- }
1676
- const block = await this.context.addBlock({
1677
- label,
1678
- description: opts.description,
1679
- maxTokens: opts.maxTokens,
1680
- provider
1681
- });
1682
- if (block.isSkill && !this._skillScanRan) await this._scanHistoryForLoadedSkills();
1683
- return block;
1684
- }
1685
- /**
1686
- * Remove a dynamically registered context block.
1687
- * Used during extension unload cleanup.
1688
- *
1689
- * Returns true if the block existed and was removed.
1690
- * Call `refreshSystemPrompt()` afterward to rebuild the prompt
1691
- * without the removed block.
1692
- */
1693
- removeContext(label) {
1694
- this._ensureReady();
1695
- return this.context.removeBlock(label);
1696
- }
1697
- /**
1698
- * Unload a previously loaded skill, reclaiming context space.
1699
- * The tool result in conversation history is replaced with a short marker.
1700
- *
1701
- * Async so that the session's background skill-state restore (which
1702
- * reads conversation history) is awaited first — otherwise a freshly
1703
- * rehydrated DO could report "not loaded" for a skill that's actually
1704
- * present in history.
1705
- */
1706
- async unloadSkill(label, key) {
1707
- await this._ensureRestored();
1708
- return this.context.unloadSkill(label, key);
1709
- }
1710
- /**
1711
- * Get currently loaded skill keys (as "label:key" strings).
1712
- * Async for the same reason as `unloadSkill` — must wait for restore.
1713
- */
1714
- async getLoadedSkillKeys() {
1715
- await this._ensureRestored();
1716
- return this.context.getLoadedSkillKeys();
1717
- }
1718
- async freezeSystemPrompt() {
1719
- await this._ensureRestored();
1720
- return this.context.freezeSystemPrompt();
1721
- }
1722
- async refreshSystemPrompt() {
1723
- await this._ensureRestored();
1724
- return this.context.refreshSystemPrompt();
1725
- }
1726
- async search(query, options) {
1727
- await this._ensureRestored();
1728
- if (!this.storage.searchMessages) throw new Error("Session provider does not support search");
1729
- return this.storage.searchMessages(query, options?.limit ?? 20);
1730
- }
1731
- /** Returns set_context and load_context tools. */
1732
- async tools() {
1733
- await this._ensureRestored();
1734
- return this.context.tools();
1735
- }
1736
- };
1737
- //#endregion
1738
- //#region src/experimental/memory/session/manager.ts
1739
- var SessionManager = class SessionManager {
1740
- constructor(agent, _options = {}) {
1741
- this._pending = [];
1742
- this._sessions = /* @__PURE__ */ new Map();
1743
- this._tableReady = false;
1744
- this._ready = false;
1745
- this.agent = agent;
1746
- this._ready = true;
1747
- this._ensureTable();
1748
- }
1749
- /**
1750
- * Chainable SessionManager creation with auto-wired context for all sessions.
1751
- *
1752
- * @example
1753
- * ```ts
1754
- * const manager = SessionManager.create(this)
1755
- * .withContext("soul", { provider: { get: async () => "You are helpful." } })
1756
- * .withContext("memory", { description: "Learned facts", maxTokens: 1100 })
1757
- * .withCachedPrompt()
1758
- * .compactAfter(100_000);
1759
- *
1760
- * // Each getSession(id) auto-creates namespaced providers:
1761
- * // memory key: "memory_<sessionId>"
1762
- * // prompt key: "_system_prompt_<sessionId>"
1763
- * const session = manager.getSession("chat-123");
1764
- * ```
1765
- */
1766
- static create(agent) {
1767
- const mgr = Object.create(SessionManager.prototype);
1768
- mgr.agent = agent;
1769
- mgr._pending = [];
1770
- mgr._compactionFn = null;
1771
- mgr._tokenThreshold = void 0;
1772
- mgr._tokenCounter = void 0;
1773
- mgr._compactionErrorHandler = void 0;
1774
- mgr._sessions = /* @__PURE__ */ new Map();
1775
- mgr._tableReady = false;
1776
- mgr._ready = false;
1777
- return mgr;
1778
- }
1779
- withContext(label, options) {
1780
- this._pending.push({
1781
- label,
1782
- options: options ?? {}
1783
- });
1784
- return this;
1785
- }
1786
- withCachedPrompt(provider) {
1787
- this._cachedPrompt = provider ?? true;
1788
- return this;
1789
- }
1790
- /**
1791
- * Register a compaction function propagated to all sessions.
1792
- * Called by `Session.compact()` to compress message history.
1793
- */
1794
- onCompaction(fn) {
1795
- this._compactionFn = fn;
1796
- return this;
1797
- }
1798
- /**
1799
- * Auto-compact when estimated token count exceeds the threshold.
1800
- * Propagated to all sessions. Requires `onCompaction()`.
1801
- */
1802
- compactAfter(tokenThreshold, options) {
1803
- this._tokenThreshold = tokenThreshold;
1804
- if (options?.tokenCounter) this._tokenCounter = options.tokenCounter;
1805
- return this;
1806
- }
1807
- /**
1808
- * Handle failures from automatic compaction in managed sessions.
1809
- */
1810
- onCompactionError(handler) {
1811
- this._compactionErrorHandler = handler;
1812
- return this;
1813
- }
1814
- /**
1815
- * Add a searchable context block that searches conversation history
1816
- * across all sessions managed by this manager.
1817
- *
1818
- * The model can use `search_context` to find relevant messages from
1819
- * any session. The block is readonly (no `set`).
1820
- *
1821
- * @example
1822
- * ```ts
1823
- * SessionManager.create(this)
1824
- * .withContext("memory", { maxTokens: 1100 })
1825
- * .withSearchableHistory("history")
1826
- * .withCachedPrompt();
1827
- * ```
1828
- */
1829
- withSearchableHistory(label) {
1830
- this._historyLabel = label;
1831
- return this;
1832
- }
1833
- _ensureReady() {
1834
- if (this._ready) return;
1835
- this._ready = true;
1836
- this._ensureTable();
1837
- }
1838
- _ensureTable() {
1839
- if (this._tableReady) return;
1840
- this.agent.sql`
1841
- CREATE TABLE IF NOT EXISTS assistant_sessions (
1842
- id TEXT PRIMARY KEY,
1843
- name TEXT NOT NULL,
1844
- parent_session_id TEXT,
1845
- model TEXT,
1846
- source TEXT,
1847
- input_tokens INTEGER DEFAULT 0,
1848
- output_tokens INTEGER DEFAULT 0,
1849
- estimated_cost REAL DEFAULT 0,
1850
- end_reason TEXT,
1851
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1852
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
1853
- )
1854
- `;
1855
- this.agent.sql`
1856
- CREATE VIRTUAL TABLE IF NOT EXISTS assistant_fts
1857
- USING fts5(id UNINDEXED, session_id UNINDEXED, role UNINDEXED, content, tokenize='porter unicode61')
1858
- `;
1859
- this._tableReady = true;
1860
- }
1861
- _createHistoryProvider() {
1862
- const mgr = this;
1863
- return {
1864
- async get() {
1865
- const sessions = mgr.list();
1866
- if (sessions.length === 0) return null;
1867
- return `${sessions.length} session${sessions.length === 1 ? "" : "s"} available for search.`;
1868
- },
1869
- async search(query) {
1870
- const results = mgr.search(query, { limit: 10 });
1871
- if (results.length === 0) return null;
1872
- return results.map((r) => `[${r.role}] ${r.content}`).join("\n---\n");
1873
- }
1874
- };
1875
- }
1876
- /** Get or create the Session instance for a session ID. */
1877
- getSession(sessionId) {
1878
- this._ensureReady();
1879
- let session = this._sessions.get(sessionId);
1880
- if (!session) {
1881
- const s = Session.create(this.agent).forSession(sessionId);
1882
- for (const { label, options } of this._pending) s.withContext(label, options);
1883
- if (this._cachedPrompt === true) s.withCachedPrompt();
1884
- else if (this._cachedPrompt) s.withCachedPrompt(this._cachedPrompt);
1885
- if (this._historyLabel) s.withContext(this._historyLabel, {
1886
- description: "Cross-session conversation history",
1887
- provider: this._createHistoryProvider()
1888
- });
1889
- if (this._compactionFn) s.onCompaction(this._compactionFn);
1890
- if (this._tokenThreshold != null) s.compactAfter(this._tokenThreshold, { tokenCounter: this._tokenCounter });
1891
- if (this._compactionErrorHandler) s.onCompactionError(this._compactionErrorHandler);
1892
- session = s;
1893
- this._sessions.set(sessionId, session);
1894
- }
1895
- return session;
1896
- }
1897
- create(name, opts) {
1898
- this._ensureReady();
1899
- const id = crypto.randomUUID();
1900
- this.agent.sql`
1901
- INSERT INTO assistant_sessions (id, name, parent_session_id, model, source)
1902
- VALUES (${id}, ${name}, ${opts?.parentSessionId ?? null}, ${opts?.model ?? null}, ${opts?.source ?? null})
1903
- `;
1904
- return this.get(id);
1905
- }
1906
- get(sessionId) {
1907
- this._ensureReady();
1908
- return this.agent.sql`
1909
- SELECT * FROM assistant_sessions WHERE id = ${sessionId}
1910
- `[0] ?? null;
1911
- }
1912
- list() {
1913
- this._ensureReady();
1914
- return this.agent.sql`
1915
- SELECT * FROM assistant_sessions ORDER BY updated_at DESC
1916
- `;
1917
- }
1918
- async delete(sessionId) {
1919
- await this.getSession(sessionId).clearMessages();
1920
- this.agent.sql`DELETE FROM assistant_sessions WHERE id = ${sessionId}`;
1921
- this._sessions.delete(sessionId);
1922
- }
1923
- rename(sessionId, name) {
1924
- this._ensureReady();
1925
- this.agent.sql`
1926
- UPDATE assistant_sessions SET name = ${name}, updated_at = CURRENT_TIMESTAMP
1927
- WHERE id = ${sessionId}
1928
- `;
1929
- }
1930
- async append(sessionId, message, parentId) {
1931
- await this.getSession(sessionId).appendMessage(message, parentId);
1932
- this._touch(sessionId);
1933
- return message.id;
1934
- }
1935
- async upsert(sessionId, message, parentId) {
1936
- const session = this.getSession(sessionId);
1937
- if (await session.getMessage(message.id)) await session.updateMessage(message);
1938
- else await session.appendMessage(message, parentId);
1939
- this._touch(sessionId);
1940
- return message.id;
1941
- }
1942
- async appendAll(sessionId, messages, parentId) {
1943
- const session = this.getSession(sessionId);
1944
- let lastParent = parentId ?? null;
1945
- for (const msg of messages) {
1946
- await session.appendMessage(msg, lastParent);
1947
- lastParent = msg.id;
1948
- }
1949
- this._touch(sessionId);
1950
- return lastParent;
1951
- }
1952
- async getHistory(sessionId, leafId) {
1953
- return this.getSession(sessionId).getHistory(leafId);
1954
- }
1955
- async getMessageCount(sessionId) {
1956
- return this.getSession(sessionId).getPathLength();
1957
- }
1958
- async clearMessages(sessionId) {
1959
- await this.getSession(sessionId).clearMessages();
1960
- this._touch(sessionId);
1961
- }
1962
- async deleteMessages(sessionId, messageIds) {
1963
- await this.getSession(sessionId).deleteMessages(messageIds);
1964
- this._touch(sessionId);
1965
- }
1966
- async getBranches(sessionId, messageId) {
1967
- return this.getSession(sessionId).getBranches(messageId);
1968
- }
1969
- /**
1970
- * Fork a session at a specific message, creating a new session
1971
- * with the history up to that point copied over.
1972
- */
1973
- async fork(sessionId, atMessageId, newName) {
1974
- const info = this.create(newName, { parentSessionId: sessionId });
1975
- const history = await this.getSession(sessionId).getHistory(atMessageId);
1976
- const newSession = this.getSession(info.id);
1977
- let parentId = null;
1978
- for (const msg of history) {
1979
- const newId = crypto.randomUUID();
1980
- const copy = {
1981
- ...msg,
1982
- id: newId
1983
- };
1984
- await newSession.appendMessage(copy, parentId);
1985
- parentId = newId;
1986
- }
1987
- this._touch(info.id);
1988
- return info;
1989
- }
1990
- async addCompaction(sessionId, summary, fromId, toId) {
1991
- return this.getSession(sessionId).addCompaction(summary, fromId, toId);
1992
- }
1993
- async getCompactions(sessionId) {
1994
- return this.getSession(sessionId).getCompactions();
1995
- }
1996
- async compactAndSplit(sessionId, summary, newName) {
1997
- const old = this.get(sessionId);
1998
- this.agent.sql`
1999
- UPDATE assistant_sessions SET end_reason = 'compaction', updated_at = CURRENT_TIMESTAMP
2000
- WHERE id = ${sessionId}
2001
- `;
2002
- const info = this.create(newName ?? old?.name ?? "Compacted", {
2003
- parentSessionId: sessionId,
2004
- model: old?.model ?? void 0,
2005
- source: old?.source ?? void 0
2006
- });
2007
- await this.append(info.id, {
2008
- id: crypto.randomUUID(),
2009
- role: "assistant",
2010
- parts: [{
2011
- type: "text",
2012
- text: `[Context from previous session]\n\n${summary}`
2013
- }]
2014
- });
2015
- return info;
2016
- }
2017
- addUsage(sessionId, inputTokens, outputTokens, cost) {
2018
- this._ensureReady();
2019
- this.agent.sql`
2020
- UPDATE assistant_sessions SET
2021
- input_tokens = input_tokens + ${inputTokens},
2022
- output_tokens = output_tokens + ${outputTokens},
2023
- estimated_cost = estimated_cost + ${cost},
2024
- updated_at = CURRENT_TIMESTAMP
2025
- WHERE id = ${sessionId}
2026
- `;
2027
- }
2028
- search(query, options) {
2029
- this._ensureReady();
2030
- const limit = options?.limit ?? 20;
2031
- const sanitized = query.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, "\"\"")}"`).join(" ");
2032
- if (!sanitized) return [];
2033
- try {
2034
- return this.agent.sql`
2035
- SELECT id, role, content FROM assistant_fts
2036
- WHERE assistant_fts MATCH ${sanitized}
2037
- ORDER BY rank LIMIT ${limit}
2038
- `.map((r) => ({
2039
- id: r.id,
2040
- role: r.role,
2041
- content: r.content,
2042
- createdAt: ""
2043
- }));
2044
- } catch {
2045
- return [];
2046
- }
2047
- }
2048
- tools() {
2049
- return { session_search: {
2050
- description: "Search past conversations for relevant context. Searches across all sessions.",
2051
- inputSchema: z.fromJSONSchema({
2052
- type: "object",
2053
- properties: { query: {
2054
- type: "string",
2055
- description: "Search query"
2056
- } },
2057
- required: ["query"]
2058
- }),
2059
- execute: async ({ query }) => {
2060
- try {
2061
- const results = this.search(query, { limit: 10 });
2062
- if (results.length === 0) return "No results found.";
2063
- return results.map((r) => `[${r.role}] ${r.content}`).join("\n---\n");
2064
- } catch (err) {
2065
- return `Error: ${err instanceof Error ? err.message : String(err)}`;
2066
- }
2067
- }
2068
- } };
2069
- }
2070
- _touch(sessionId) {
2071
- this.agent.sql`
2072
- UPDATE assistant_sessions SET updated_at = CURRENT_TIMESTAMP
2073
- WHERE id = ${sessionId}
2074
- `;
2075
- }
2076
- };
2077
- //#endregion
2078
- //#region src/experimental/memory/session/providers/postgres-adapter.ts
2079
- function isPostgresConnection(client) {
2080
- return typeof client.execute === "function";
2081
- }
2082
- /**
2083
- * Normalise an incoming client into a `PostgresConnection`. When given a
2084
- * `pg`-style client we translate `?` placeholders to `$1, $2, ...` so the
2085
- * providers can keep using the portable `?` syntax internally.
2086
- */
2087
- function toPostgresConnection(client) {
2088
- if (isPostgresConnection(client)) return client;
2089
- const pg = client;
2090
- return { async execute(query, args) {
2091
- let idx = 0;
2092
- const pgQuery = query.replace(/\?/g, () => `$${++idx}`);
2093
- return { rows: (await pg.query(pgQuery, args ?? [])).rows };
2094
- } };
2095
- }
2096
- //#endregion
2097
- //#region src/experimental/memory/session/providers/postgres.ts
2098
- var PostgresSessionProvider = class {
2099
- /**
2100
- * @param client A raw `pg.Client` (recommended) or any `PostgresConnection`.
2101
- * Must already be connected — this provider never opens or closes the
2102
- * underlying client.
2103
- * @param sessionId Session identifier. Different ids are fully isolated
2104
- * rows within the shared tables. Defaults to `""`.
2105
- */
2106
- constructor(client, sessionId) {
2107
- this.conn = toPostgresConnection(client);
2108
- this.sessionId = sessionId ?? "";
2109
- }
2110
- async getMessage(id) {
2111
- const { rows } = await this.conn.execute("SELECT content FROM assistant_messages WHERE id = ? AND session_id = ?", [id, this.sessionId]);
2112
- return rows.length > 0 ? this.parse(rows[0].content) : null;
2113
- }
2114
- async getHistory(leafId) {
2115
- const leaf = leafId ? (await this.conn.execute("SELECT id FROM assistant_messages WHERE id = ? AND session_id = ?", [leafId, this.sessionId])).rows[0] : await this.latestLeafRow();
2116
- if (!leaf) return [];
2117
- const { rows } = await this.conn.execute(`WITH RECURSIVE path AS (
2118
- SELECT id, parent_id, content, 0 as depth FROM assistant_messages WHERE id = ? AND session_id = ?
2119
- UNION ALL
2120
- SELECT m.id, m.parent_id, m.content, p.depth + 1 FROM assistant_messages m
2121
- JOIN path p ON m.id = p.parent_id
2122
- WHERE m.session_id = ? AND p.depth < 10000
2123
- )
2124
- SELECT content FROM path ORDER BY depth DESC`, [
2125
- leaf.id,
2126
- this.sessionId,
2127
- this.sessionId
2128
- ]);
2129
- const messages = this.parseRows(rows);
2130
- const compactions = await this.getCompactions();
2131
- if (compactions.length === 0) return messages;
2132
- return this.applyCompactions(messages, compactions);
2133
- }
2134
- async getLatestLeaf() {
2135
- const row = await this.latestLeafRow();
2136
- return row ? this.parse(row.content) : null;
2137
- }
2138
- async getBranches(messageId) {
2139
- const { rows } = await this.conn.execute("SELECT content FROM assistant_messages WHERE parent_id = ? AND session_id = ? ORDER BY created_at ASC", [messageId, this.sessionId]);
2140
- return this.parseRows(rows);
2141
- }
2142
- async getPathLength(leafId) {
2143
- const leaf = leafId ? (await this.conn.execute("SELECT id FROM assistant_messages WHERE id = ? AND session_id = ?", [leafId, this.sessionId])).rows[0] : await this.latestLeafRow();
2144
- if (!leaf) return 0;
2145
- const { rows } = await this.conn.execute(`WITH RECURSIVE path AS (
2146
- SELECT id, parent_id, 0 as depth FROM assistant_messages WHERE id = ? AND session_id = ?
2147
- UNION ALL
2148
- SELECT m.id, m.parent_id, p.depth + 1 FROM assistant_messages m
2149
- JOIN path p ON m.id = p.parent_id
2150
- WHERE m.session_id = ? AND p.depth < 10000
2151
- )
2152
- SELECT COUNT(*) as count FROM path`, [
2153
- leaf.id,
2154
- this.sessionId,
2155
- this.sessionId
2156
- ]);
2157
- return Number(rows[0]?.count ?? 0);
2158
- }
2159
- async appendMessage(message, parentId) {
2160
- let parent = parentId !== void 0 ? parentId : (await this.latestLeafRow())?.id ?? null;
2161
- if (parent) {
2162
- const { rows } = await this.conn.execute("SELECT id FROM assistant_messages WHERE id = ? AND session_id = ?", [parent, this.sessionId]);
2163
- if (rows.length === 0) parent = null;
2164
- }
2165
- const json = JSON.stringify(message);
2166
- const text = this.extractSearchableText(json);
2167
- await this.conn.execute(`INSERT INTO assistant_messages (id, session_id, parent_id, role, content, text_content)
2168
- VALUES (?, ?, ?, ?, ?, ?)
2169
- ON CONFLICT (session_id, id) DO NOTHING`, [
2170
- message.id,
2171
- this.sessionId,
2172
- parent,
2173
- message.role,
2174
- json,
2175
- text
2176
- ]);
2177
- }
2178
- async updateMessage(message) {
2179
- const json = JSON.stringify(message);
2180
- await this.conn.execute("UPDATE assistant_messages SET content = ?, text_content = ? WHERE id = ? AND session_id = ?", [
2181
- json,
2182
- this.extractSearchableText(json),
2183
- message.id,
2184
- this.sessionId
2185
- ]);
2186
- }
2187
- async deleteMessages(messageIds) {
2188
- for (const id of messageIds) await this.conn.execute("DELETE FROM assistant_messages WHERE id = ? AND session_id = ?", [id, this.sessionId]);
2189
- }
2190
- async clearMessages() {
2191
- await this.conn.execute("DELETE FROM assistant_messages WHERE session_id = ?", [this.sessionId]);
2192
- await this.conn.execute("DELETE FROM assistant_compactions WHERE session_id = ?", [this.sessionId]);
2193
- }
2194
- async addCompaction(summary, fromMessageId, toMessageId) {
2195
- const id = crypto.randomUUID();
2196
- await this.conn.execute("INSERT INTO assistant_compactions (id, session_id, summary, from_message_id, to_message_id) VALUES (?, ?, ?, ?, ?)", [
2197
- id,
2198
- this.sessionId,
2199
- summary,
2200
- fromMessageId,
2201
- toMessageId
2202
- ]);
2203
- return {
2204
- id,
2205
- summary,
2206
- fromMessageId,
2207
- toMessageId,
2208
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
2209
- };
2210
- }
2211
- async getCompactions() {
2212
- const { rows } = await this.conn.execute("SELECT * FROM assistant_compactions WHERE session_id = ? ORDER BY created_at ASC", [this.sessionId]);
2213
- return rows.map((r) => ({
2214
- id: r.id,
2215
- summary: r.summary,
2216
- fromMessageId: r.from_message_id,
2217
- toMessageId: r.to_message_id,
2218
- createdAt: r.created_at instanceof Date ? r.created_at.toISOString() : String(r.created_at)
2219
- }));
2220
- }
2221
- async searchMessages(query, limit = 20) {
2222
- const { rows } = await this.conn.execute(`SELECT id, role, text_content FROM assistant_messages
2223
- WHERE session_id = ? AND content_tsv @@ plainto_tsquery('english', ?)
2224
- ORDER BY ts_rank(content_tsv, plainto_tsquery('english', ?)) DESC
2225
- LIMIT ?`, [
2226
- this.sessionId,
2227
- query,
2228
- query,
2229
- limit
2230
- ]);
2231
- return rows.map((r) => ({
2232
- id: r.id,
2233
- role: r.role,
2234
- content: r.text_content ?? "",
2235
- createdAt: ""
2236
- }));
2237
- }
2238
- async latestLeafRow() {
2239
- const { rows } = await this.conn.execute(`SELECT m.id, m.content FROM assistant_messages m
2240
- LEFT JOIN assistant_messages c ON c.parent_id = m.id AND c.session_id = ?
2241
- WHERE c.id IS NULL AND m.session_id = ?
2242
- ORDER BY m.created_at DESC LIMIT 1`, [this.sessionId, this.sessionId]);
2243
- return rows[0] ?? null;
2244
- }
2245
- applyCompactions(messages, compactions) {
2246
- const ids = messages.map((m) => m.id);
2247
- const result = [];
2248
- let i = 0;
2249
- while (i < messages.length) {
2250
- const matching = compactions.filter((c) => c.fromMessageId === ids[i]);
2251
- const comp = matching.length > 1 ? matching[matching.length - 1] : matching[0];
2252
- if (comp) {
2253
- const endIdx = ids.indexOf(comp.toMessageId);
2254
- if (endIdx >= i) {
2255
- result.push({
2256
- id: `compaction_${comp.id}`,
2257
- role: "assistant",
2258
- parts: [{
2259
- type: "text",
2260
- text: comp.summary
2261
- }],
2262
- createdAt: /* @__PURE__ */ new Date()
2263
- });
2264
- i = endIdx + 1;
2265
- continue;
2266
- }
2267
- }
2268
- result.push(messages[i]);
2269
- i++;
2270
- }
2271
- return result;
2272
- }
2273
- parse(json) {
2274
- try {
2275
- const msg = JSON.parse(json);
2276
- if (typeof msg?.id === "string" && typeof msg?.role === "string" && Array.isArray(msg?.parts)) return msg;
2277
- } catch {}
2278
- return null;
2279
- }
2280
- parseRows(rows) {
2281
- const result = [];
2282
- for (const row of rows) {
2283
- const msg = this.parse(row.content);
2284
- if (msg) result.push(msg);
2285
- }
2286
- return result;
2287
- }
2288
- /**
2289
- * Extract just the human-readable text from a message's JSON blob
2290
- * and store it in `text_content`, which feeds the generated `content_tsv`
2291
- * column used for FTS. The full structured message (parts, tool calls,
2292
- * metadata) is still stored verbatim in `content` — this is the source
2293
- * of truth. Indexing the raw JSON would return FTS hits on keys like
2294
- * `"role"`, `"parts"`, `"dynamic-tool"`, etc.
2295
- */
2296
- extractSearchableText(json) {
2297
- const msg = this.parse(json);
2298
- if (!msg) return json;
2299
- return msg.parts.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
2300
- }
2301
- };
2302
- //#endregion
2303
- //#region src/experimental/memory/session/providers/postgres-context.ts
2304
- var PostgresContextProvider = class {
2305
- /**
2306
- * @param client A raw `pg.Client` (recommended) or any `PostgresConnection`.
2307
- * Must already be connected.
2308
- * @param label Block label used as the primary key row in
2309
- * `cf_agents_context_blocks`. Pass a session-scoped label (e.g.
2310
- * `` `memory_${sessionId}` ``) for per-session state.
2311
- */
2312
- constructor(client, label) {
2313
- this.conn = toPostgresConnection(client);
2314
- this.label = label;
2315
- }
2316
- async get() {
2317
- const { rows } = await this.conn.execute("SELECT content FROM cf_agents_context_blocks WHERE label = ?", [this.label]);
2318
- return rows[0]?.content ?? null;
2319
- }
2320
- async set(content) {
2321
- await this.conn.execute(`INSERT INTO cf_agents_context_blocks (label, content)
2322
- VALUES (?, ?)
2323
- ON CONFLICT (label) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW()`, [this.label, content]);
2324
- }
2325
- };
2326
- //#endregion
2327
- //#region src/experimental/memory/session/providers/postgres-search.ts
2328
- var PostgresSearchProvider = class {
2329
- /**
2330
- * @param client A raw `pg.Client` (recommended) or any `PostgresConnection`.
2331
- * Must already be connected.
2332
- */
2333
- constructor(client) {
2334
- this.label = "";
2335
- this.conn = toPostgresConnection(client);
2336
- }
2337
- init(label) {
2338
- this.label = label;
2339
- }
2340
- async get() {
2341
- const { rows } = await this.conn.execute("SELECT COUNT(*) as count FROM cf_agents_search_entries WHERE label = ?", [this.label]);
2342
- const count = Number(rows[0]?.count ?? 0);
2343
- if (count === 0) return null;
2344
- return `${count} entries indexed.`;
2345
- }
2346
- async search(query) {
2347
- if (!query.trim()) return null;
2348
- const { rows } = await this.conn.execute(`SELECT key, content FROM cf_agents_search_entries
2349
- WHERE label = ? AND content_tsv @@ plainto_tsquery('english', ?)
2350
- ORDER BY ts_rank(content_tsv, plainto_tsquery('english', ?)) DESC
2351
- LIMIT 10`, [
2352
- this.label,
2353
- query,
2354
- query
2355
- ]);
2356
- if (rows.length === 0) return "No results found.";
2357
- return rows.map((r) => `[${r.key}]\n${r.content}`).join("\n\n");
2358
- }
2359
- async set(key, content) {
2360
- await this.conn.execute(`INSERT INTO cf_agents_search_entries (label, key, content)
2361
- VALUES (?, ?, ?)
2362
- ON CONFLICT (label, key) DO UPDATE SET
2363
- content = EXCLUDED.content,
2364
- updated_at = NOW()`, [
2365
- this.label,
2366
- key,
2367
- content
2368
- ]);
2369
- }
2370
- };
2371
- //#endregion
2372
- export { AgentContextProvider, AgentSearchProvider, AgentSessionProvider, PostgresContextProvider, PostgresSearchProvider, PostgresSessionProvider, R2SkillProvider, Session, SessionManager, isSearchProvider, isSkillProvider, isWritableProvider };
2373
-
2374
- //# sourceMappingURL=index.js.map