agents 0.16.1 → 0.17.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 (127) hide show
  1. package/README.md +11 -8
  2. package/dist/{agent-tool-types-NofdbL9X.d.ts → agent-tool-types-Cd1TZPfB.d.ts} +595 -137
  3. package/dist/agent-tool-types.d.ts +34 -18
  4. package/dist/agent-tool-types.js +20 -1
  5. package/dist/agent-tool-types.js.map +1 -0
  6. package/dist/agent-tools-BXlsuX0d.js +304 -0
  7. package/dist/agent-tools-BXlsuX0d.js.map +1 -0
  8. package/dist/agent-tools-_E8wxUIK.d.ts +119 -0
  9. package/dist/agent-tools.d.ts +24 -18
  10. package/dist/agent-tools.js.map +1 -1
  11. package/dist/ai-chat-agent.d.ts +1 -1
  12. package/dist/ai-chat-agent.js +1 -2
  13. package/dist/ai-chat-agent.js.map +1 -1
  14. package/dist/ai-chat-v5-migration.d.ts +1 -1
  15. package/dist/ai-chat-v5-migration.js +1 -2
  16. package/dist/ai-chat-v5-migration.js.map +1 -1
  17. package/dist/ai-react.d.ts +1 -1
  18. package/dist/ai-react.js +1 -2
  19. package/dist/ai-react.js.map +1 -1
  20. package/dist/ai-types.d.ts +1 -7
  21. package/dist/ai-types.js +2 -8
  22. package/dist/ai-types.js.map +1 -1
  23. package/dist/browser/ai.js +1 -1
  24. package/dist/browser/index.js +1 -1
  25. package/dist/chat/index.d.ts +1923 -76
  26. package/dist/chat/index.js +1784 -278
  27. package/dist/chat/index.js.map +1 -1
  28. package/dist/chat/react.d.ts +602 -0
  29. package/dist/chat/react.js +1506 -0
  30. package/dist/chat/react.js.map +1 -0
  31. package/dist/chat-sdk/index.d.ts +4 -4
  32. package/dist/{classPrivateFieldGet2-CZ7QjTXN.js → classPrivateFieldGet2-DZBYAB34.js} +5 -5
  33. package/dist/{classPrivateMethodInitSpec-D-0__zd9.js → classPrivateMethodInitSpec-qMjJ6sHQ.js} +2 -2
  34. package/dist/{client-FUizKzj2.js → client-BZ-B3NhC.js} +19 -3
  35. package/dist/client-BZ-B3NhC.js.map +1 -0
  36. package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
  37. package/dist/client.d.ts +76 -57
  38. package/dist/client.js +33 -5
  39. package/dist/client.js.map +1 -1
  40. package/dist/{compaction-helpers-DVcu5lPN.d.ts → compaction-helpers-wUz6M3us.d.ts} +20 -1
  41. package/dist/{connector-CrKhowfD.js → connector-CdldGF3h.js} +5 -5
  42. package/dist/{connector-CrKhowfD.js.map → connector-CdldGF3h.js.map} +1 -1
  43. package/dist/email.js +1 -1
  44. package/dist/email.js.map +1 -1
  45. package/dist/experimental/memory/session/index.d.ts +1 -1
  46. package/dist/experimental/memory/session/index.js +20 -2
  47. package/dist/experimental/memory/session/index.js.map +1 -1
  48. package/dist/experimental/memory/utils/index.d.ts +1 -1
  49. package/dist/{index-B7IbEeze.d.ts → index-CcbnKkNh.d.ts} +188 -14
  50. package/dist/index.d.ts +91 -71
  51. package/dist/index.js +562 -24
  52. package/dist/index.js.map +1 -1
  53. package/dist/mcp/client.d.ts +18 -14
  54. package/dist/mcp/client.js +1 -1
  55. package/dist/mcp/index.d.ts +30 -30
  56. package/dist/mcp/index.js +7 -7
  57. package/dist/mcp/index.js.map +1 -1
  58. package/dist/{agent-tools-3zLG7MgA.js → message-builder-BymO4N_D.js} +45 -126
  59. package/dist/message-builder-BymO4N_D.js.map +1 -0
  60. package/dist/observability/index.d.ts +1 -1
  61. package/dist/observability/index.js +4 -2
  62. package/dist/observability/index.js.map +1 -1
  63. package/dist/react.d.ts +123 -110
  64. package/dist/react.js +44 -11
  65. package/dist/react.js.map +1 -1
  66. package/dist/serializable.d.ts +1 -1
  67. package/dist/skills/compile.js +1 -1
  68. package/dist/skills/compile.js.map +1 -1
  69. package/dist/skills/index.js +5 -5
  70. package/dist/skills/index.js.map +1 -1
  71. package/dist/sub-routing.d.ts +6 -6
  72. package/dist/utils.js +1 -1
  73. package/dist/utils.js.map +1 -1
  74. package/dist/vite.js +5 -5
  75. package/dist/vite.js.map +1 -1
  76. package/dist/wire-types-nflOzNuU.js +240 -0
  77. package/dist/wire-types-nflOzNuU.js.map +1 -0
  78. package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
  79. package/dist/workflow-types.d.ts +25 -21
  80. package/dist/workflow-types.js.map +1 -1
  81. package/dist/workflows.d.ts +22 -21
  82. package/dist/workflows.js +31 -7
  83. package/dist/workflows.js.map +1 -1
  84. package/docs/adding-to-existing-project.md +450 -0
  85. package/docs/agent-class.md +503 -0
  86. package/docs/agent-tools.md +552 -0
  87. package/docs/browse-the-web.md +430 -0
  88. package/docs/callable-methods.md +627 -0
  89. package/docs/chat-agents.md +1687 -0
  90. package/docs/chat-sdk.md +181 -0
  91. package/docs/client-sdk.md +520 -0
  92. package/docs/client-tools-continuation.md +177 -0
  93. package/docs/codemode.md +440 -0
  94. package/docs/configuration.md +775 -0
  95. package/docs/cross-domain-authentication.md +171 -0
  96. package/docs/durable-execution.md +537 -0
  97. package/docs/email.md +663 -0
  98. package/docs/get-current-agent.md +204 -0
  99. package/docs/getting-started.md +305 -0
  100. package/docs/http-websockets.md +668 -0
  101. package/docs/human-in-the-loop.md +661 -0
  102. package/docs/index.md +151 -0
  103. package/docs/long-running-agents.md +730 -0
  104. package/docs/mcp-client.md +620 -0
  105. package/docs/mcp-servers.md +526 -0
  106. package/docs/mcp-transports.md +308 -0
  107. package/docs/migration-to-ai-sdk-v5.md +96 -0
  108. package/docs/migration-to-ai-sdk-v6.md +163 -0
  109. package/docs/observability.md +261 -0
  110. package/docs/push-notifications.md +367 -0
  111. package/docs/queue.md +329 -0
  112. package/docs/readonly-connections.md +278 -0
  113. package/docs/resumable-streaming.md +127 -0
  114. package/docs/retries.md +444 -0
  115. package/docs/routing.md +749 -0
  116. package/docs/scheduling.md +898 -0
  117. package/docs/securing-mcp-servers.md +359 -0
  118. package/docs/server-driven-messages.md +477 -0
  119. package/docs/sessions.md +1024 -0
  120. package/docs/state.md +512 -0
  121. package/docs/sub-agents.md +389 -0
  122. package/docs/webhooks.md +604 -0
  123. package/docs/workflows.md +877 -0
  124. package/package.json +47 -15
  125. package/dist/agent-tools-3zLG7MgA.js.map +0 -1
  126. package/dist/agent-tools-DLquv-dp.d.ts +0 -14
  127. package/dist/client-FUizKzj2.js.map +0 -1
@@ -0,0 +1,1024 @@
1
+ # Sessions (Experimental)
2
+
3
+ The Session API provides persistent conversation storage for agents, with tree-structured messages, context blocks, compaction, full-text search, and AI-controllable tools. By default it uses Durable Object SQLite; external Postgres storage is also available for apps that need shared database access, analytics, or cross-DO queries.
4
+
5
+ > **Experimental.** The Session API is under `agents/experimental/memory/session`. The API surface is stable but may evolve before graduating to the main package.
6
+
7
+ ## Quick Start
8
+
9
+ ```typescript
10
+ import { Agent } from "agents";
11
+ import { Session } from "agents/experimental/memory/session";
12
+
13
+ class MyAgent extends Agent {
14
+ session = Session.create(this)
15
+ .withContext("soul", {
16
+ provider: { get: async () => "You are a helpful assistant." }
17
+ })
18
+ .withContext("memory", {
19
+ description: "Learned facts about the user",
20
+ maxTokens: 1100
21
+ })
22
+ .withCachedPrompt();
23
+
24
+ async onMessage(message) {
25
+ await this.session.appendMessage(message);
26
+ const history = await this.session.getHistory();
27
+ const system = await this.session.freezeSystemPrompt();
28
+ const tools = await this.session.tools();
29
+ // Pass history, system prompt, and tools to your LLM
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## Session
35
+
36
+ `Session` manages a single conversation's messages, context blocks, and compaction state.
37
+
38
+ ### Creating a Session
39
+
40
+ There are two ways to create a Session:
41
+
42
+ **Builder API (recommended)** — uses `Session.create(agent)` with a chainable builder. Context providers without an explicit `provider` option are auto-wired to SQLite.
43
+
44
+ ```typescript
45
+ const session = Session.create(this)
46
+ .withContext("soul", { provider: { get: async () => "You are helpful." } })
47
+ .withContext("memory", { description: "Learned facts", maxTokens: 1100 })
48
+ .withCachedPrompt()
49
+ .onCompaction(myCompactFn)
50
+ .compactAfter(100_000);
51
+ ```
52
+
53
+ **Direct constructor** — takes a `SessionProvider` and options directly. Used when you want full control over providers.
54
+
55
+ ```typescript
56
+ import {
57
+ AgentSessionProvider,
58
+ AgentContextProvider
59
+ } from "agents/experimental/memory/session";
60
+
61
+ const session = new Session(new AgentSessionProvider(this), {
62
+ context: [
63
+ {
64
+ label: "memory",
65
+ description: "Notes",
66
+ maxTokens: 500,
67
+ provider: new AgentContextProvider(this, "memory")
68
+ },
69
+ { label: "soul", provider: { get: async () => "You are helpful." } }
70
+ ]
71
+ });
72
+ ```
73
+
74
+ ### Builder Methods
75
+
76
+ All builder methods return `this` for chaining. Order does not matter — providers are resolved lazily on first use.
77
+
78
+ | Method | Description |
79
+ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
80
+ | `Session.create(agent)` | Static factory. `agent` is any object with a `sql` tagged template method (i.e. your Agent/DO). |
81
+ | `.forSession(sessionId)` | Namespace this session by ID. Required for multi-session isolation when not using SessionManager. Context provider keys and storage are scoped to this ID. |
82
+ | `.withContext(label, options?)` | Add a context block. See [Context Blocks](#context-blocks). |
83
+ | `.withCachedPrompt(provider?)` | Enable system prompt persistence. The prompt is frozen on first use and survives DO hibernation/eviction. Without an explicit provider, auto-wires to SQLite. |
84
+ | `.onCompaction(fn)` | Register a compaction function. See [Compaction](#compaction). |
85
+ | `.compactAfter(tokenThreshold, options?)` | Auto-compact when estimated token count exceeds the threshold. Checked after each `appendMessage()`. Requires `.onCompaction()`. |
86
+ | `.onCompactionError(handler)` | Handle errors from automatic compaction. Handler failures are swallowed so message writes remain non-fatal. |
87
+
88
+ ### Messages
89
+
90
+ Messages use the `SessionMessage` type — a minimal shape with `id`, `role`, `parts`, and optional `createdAt`. The Vercel AI SDK's `UIMessage` is structurally compatible and can be passed directly without conversion. The session stores messages in a tree structure via `parent_id`, enabling branching conversations.
91
+
92
+ ```typescript
93
+ // Append — auto-parents to the latest leaf unless parentId is specified
94
+ await session.appendMessage(message);
95
+ await session.appendMessage(message, parentId);
96
+
97
+ // Update an existing message (matched by message.id)
98
+ await session.updateMessage(message);
99
+
100
+ // Delete specific messages
101
+ await session.deleteMessages(["msg-1", "msg-2"]);
102
+
103
+ // Clear all messages and skill state
104
+ await session.clearMessages();
105
+ ```
106
+
107
+ > **Note:** Session methods are async. SQLite-backed sessions are usually fast, but external providers may perform network I/O, and `appendMessage()` may also trigger auto-compaction.
108
+
109
+ #### Reading History
110
+
111
+ ```typescript
112
+ // Linear history from root to the latest leaf
113
+ const messages = await session.getHistory();
114
+
115
+ // History to a specific leaf (for branching)
116
+ const branch = await session.getHistory(leafId);
117
+
118
+ // Get a single message
119
+ const msg = await session.getMessage("msg-1");
120
+
121
+ // Get the newest message
122
+ const latest = await session.getLatestLeaf();
123
+
124
+ // Count messages in path
125
+ const count = await session.getPathLength();
126
+ ```
127
+
128
+ #### Branching
129
+
130
+ Messages form a tree. When you `appendMessage` with a `parentId` that already has children, you create a branch. Use `getBranches()` to get all child messages branching from a given point:
131
+
132
+ ```typescript
133
+ // Get all child messages that branch from messageId (e.g. multiple responses to a user message)
134
+ const branches = await session.getBranches(messageId);
135
+ ```
136
+
137
+ This powers features like response regeneration — pass the user message ID to get both the original and regenerated responses. `getHistory(leafId)` walks the chosen path.
138
+
139
+ ### Search
140
+
141
+ Full-text search over the conversation history. SQLite-backed sessions use FTS5; Postgres-backed sessions use the provider's Postgres full-text index.
142
+
143
+ ```typescript
144
+ const results = await session.search("deployment Friday", { limit: 10 });
145
+ // Returns: Array<{ id, role, content, createdAt? }>
146
+ ```
147
+
148
+ Uses porter stemming and unicode tokenization. The search covers all messages in the session.
149
+
150
+ > **Note:** `search()` throws if the session provider does not support search. The built-in `AgentSessionProvider` and `PostgresSessionProvider` support it.
151
+
152
+ ### WebSocket Broadcasts
153
+
154
+ When the Session's `agent` object has a `broadcast()` method (all `Agent` subclasses do), the Session automatically broadcasts status events over WebSocket after each write operation:
155
+
156
+ - **`CF_AGENT_SESSION`** — phase (`"idle"` or `"compacting"`), `tokenEstimate`, `tokenThreshold`
157
+ - **`CF_AGENT_SESSION_ERROR`** — emitted on compaction failure
158
+
159
+ This allows connected clients to display real-time token usage and compaction status.
160
+
161
+ ---
162
+
163
+ ## Context Blocks
164
+
165
+ Context blocks are persistent key-value sections injected into the system prompt. Each block has a **label**, optional **description**, and a **provider** that determines its behavior.
166
+
167
+ ### Provider Types
168
+
169
+ There are four provider types, detected by duck-typing:
170
+
171
+ | Provider | Interface | Behavior | AI Tool |
172
+ | --------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------- |
173
+ | **ContextProvider** | `get()` | Read-only block in system prompt | — |
174
+ | **WritableContextProvider** | `get()` + `set()` | Writable via AI | `set_context` |
175
+ | **SkillProvider** | `get()` + `load()` + `set?()` | On-demand keyed documents. `get()` returns a metadata listing; `load(key)` fetches full content. | `load_context`, `unload_context`, `set_context` |
176
+ | **SearchProvider** | `get()` + `search()` + `set?()` | Full-text searchable entries. `get()` returns a summary; `search(query)` runs FTS5. | `search_context`, `set_context` |
177
+
178
+ All providers also support an optional `init(label)` method, called before first use with the block's label.
179
+
180
+ ### Built-in Providers
181
+
182
+ **`AgentContextProvider`** — SQLite-backed writable context. This is what you get by default when using the builder without an explicit provider.
183
+
184
+ ```typescript
185
+ import { AgentContextProvider } from "agents/experimental/memory/session";
186
+
187
+ // Explicit usage — key determines the SQLite row
188
+ new AgentContextProvider(this, "memory");
189
+ ```
190
+
191
+ **`R2SkillProvider`** — Cloudflare R2 bucket for on-demand document loading. Skills are listed in the system prompt as metadata; the model loads full content on demand via `load_context`.
192
+
193
+ ```typescript
194
+ import { R2SkillProvider } from "agents/experimental/memory/session";
195
+
196
+ Session.create(this).withContext("skills", {
197
+ provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" })
198
+ });
199
+ ```
200
+
201
+ Descriptions are stored in R2 custom metadata (`description` key).
202
+
203
+ **`AgentSearchProvider`** — SQLite FTS5 searchable context. Entries are indexed and searchable by the model via `search_context`.
204
+
205
+ ```typescript
206
+ import { AgentSearchProvider } from "agents/experimental/memory/session";
207
+
208
+ Session.create(this).withContext("knowledge", {
209
+ description: "Searchable knowledge base",
210
+ provider: new AgentSearchProvider(this)
211
+ });
212
+ ```
213
+
214
+ ### Adding and Removing Context at Runtime
215
+
216
+ Blocks can be added and removed dynamically after initialization — useful for extensions:
217
+
218
+ ```typescript
219
+ // Add a new block (auto-wires to SQLite if no provider given)
220
+ await session.addContext("extension-notes", {
221
+ description: "From extension X",
222
+ maxTokens: 500
223
+ });
224
+
225
+ // Remove it
226
+ session.removeContext("extension-notes");
227
+
228
+ // Rebuild the system prompt to reflect changes
229
+ await session.refreshSystemPrompt();
230
+ ```
231
+
232
+ > **Note:** `addContext` and `removeContext` do NOT automatically update the frozen system prompt. You must call `refreshSystemPrompt()` afterward.
233
+
234
+ ### Reading Context Blocks
235
+
236
+ ```typescript
237
+ // Single block
238
+ const block = session.getContextBlock("memory");
239
+ // block: { label, description?, content, tokens, maxTokens?, writable, isSkill, isSearchable }
240
+
241
+ // All blocks
242
+ const blocks = session.getContextBlocks();
243
+ ```
244
+
245
+ ### Writing to Context Blocks
246
+
247
+ ```typescript
248
+ // Replace content entirely
249
+ await session.replaceContextBlock("memory", "User likes coffee.");
250
+
251
+ // Append content
252
+ await session.appendContextBlock("memory", "\nUser prefers dark roast.");
253
+ ```
254
+
255
+ > **Note:** Writing to a context block updates the provider immediately but does NOT update the frozen system prompt snapshot. This is intentional — it preserves the LLM prefix cache. Call `refreshSystemPrompt()` when you want changes reflected in the prompt.
256
+
257
+ ### System Prompt
258
+
259
+ The system prompt is built from all context blocks with headers and metadata:
260
+
261
+ ```
262
+ ══════════════════════════════════════════════
263
+ SOUL (Identity) [readonly]
264
+ ══════════════════════════════════════════════
265
+ You are a helpful assistant.
266
+
267
+ ══════════════════════════════════════════════
268
+ MEMORY (Learned facts) [45% — 495/1100 tokens]
269
+ ══════════════════════════════════════════════
270
+ User likes coffee.
271
+ User prefers dark roast.
272
+ ```
273
+
274
+ ```typescript
275
+ // Freeze — first call renders and persists, subsequent calls return the cached value
276
+ const prompt = await session.freezeSystemPrompt();
277
+
278
+ // Refresh — re-render from current block state and persist
279
+ const updated = await session.refreshSystemPrompt();
280
+ ```
281
+
282
+ The frozen prompt survives DO hibernation and eviction when `withCachedPrompt()` is enabled. After eviction, the next `freezeSystemPrompt()` call loads from the configured prompt store rather than re-rendering.
283
+
284
+ ### Skills (Load/Unload)
285
+
286
+ Skills are on-demand documents stored in a `SkillProvider` (e.g. R2). The model sees a metadata listing in the system prompt and can load full content on demand:
287
+
288
+ ```typescript
289
+ // Unload a skill to free context space (rewrites the tool result in history)
290
+ await session.unloadSkill("skills", "api-reference");
291
+
292
+ // Check what's currently loaded
293
+ const loaded = await session.getLoadedSkillKeys(); // Set<"skills:api-reference">
294
+ ```
295
+
296
+ After hibernation/eviction, loaded skills are reconstructed by scanning conversation history for `load_context` tool results. This means skill state survives restarts without additional storage.
297
+
298
+ > **Gotcha:** Skill restoration scans the entire conversation history looking for `load_context` tool invocations in assistant messages with `state: "output-available"`. When you unload a skill, it does not delete the tool result — it rewrites the `output` field to `"[skill unloaded: key]"` in-place. This means the original loaded content is permanently lost from history after unload.
299
+
300
+ ---
301
+
302
+ ## AI Tools
303
+
304
+ Session automatically generates tools based on the provider types of your context blocks. Pass these to your LLM alongside your own tools.
305
+
306
+ ```typescript
307
+ const tools = await session.tools();
308
+ // Merge with your own tools:
309
+ const allTools = { ...tools, ...myTools };
310
+ ```
311
+
312
+ ### `set_context`
313
+
314
+ Generated when any writable block exists. Writes to regular blocks, skill blocks (keyed), or search blocks (keyed).
315
+
316
+ - For regular blocks: `{ label, content, action: "replace" | "append" }`
317
+ - For skill blocks: `{ label, content, metadata?: { title, description } }`
318
+ - For search blocks: `{ label, content, metadata?: { title } }`
319
+
320
+ Enforces `maxTokens` limits. Returns a usage string like `"Written to memory. Usage: 45% (495/1100 tokens)"`.
321
+
322
+ For keyed blocks, `metadata.title` becomes the stable entry key. If title is omitted, the key is generated from the content plus a short deterministic hash to avoid silent collisions; provide a title when you want later writes to update the same entry.
323
+
324
+ ### `load_context`
325
+
326
+ Generated when any skill block exists. Loads full content by key from a `SkillProvider`.
327
+
328
+ - Input: `{ label, key }`
329
+ - Returns the document content, or `"Not found: key"`
330
+
331
+ ### `unload_context`
332
+
333
+ Generated alongside `load_context`. Frees context space by unloading a previously loaded skill.
334
+
335
+ - Input: `{ label, key }`
336
+ - Rewrites the tool result in conversation history to a short marker
337
+ - The skill remains available for re-loading
338
+
339
+ The tool's description dynamically lists currently loaded skills.
340
+
341
+ ### `search_context`
342
+
343
+ Generated when any search block exists. Full-text search within a searchable context block.
344
+
345
+ - Input: `{ label, query }`
346
+ - Returns top 10 results by FTS5 rank, or `"No results found."`
347
+
348
+ ### `session_search`
349
+
350
+ Available on `SessionManager` only (not on individual sessions). Searches across all sessions.
351
+
352
+ - Input: `{ query }`
353
+ - Returns results from all sessions, or `"No results found."`
354
+
355
+ Use `{ ...sessionTools, ...manager.tools() }` to give the model both per-session and cross-session tools.
356
+
357
+ ---
358
+
359
+ ## Compaction
360
+
361
+ Compaction summarizes older messages to keep conversations within token limits. Original messages are preserved in the underlying message store — the summary is a non-destructive overlay applied at read time.
362
+
363
+ ### Setup
364
+
365
+ ```typescript
366
+ import { createCompactFunction } from "agents/experimental/memory/utils/compaction-helpers";
367
+
368
+ const session = Session.create(this)
369
+ .withContext("memory", { maxTokens: 1100 })
370
+ .onCompaction(
371
+ createCompactFunction({
372
+ summarize: (prompt) =>
373
+ generateText({ model: myModel, prompt }).then((r) => r.text),
374
+ protectHead: 3, // Keep first 3 messages (default: 3)
375
+ tailTokenBudget: 20000, // Protect ~20K tokens at the tail (default: 20000)
376
+ minTailMessages: 2, // Always keep at least 2 tail messages (default: 2)
377
+ tokenCounter: async (messages) => estimateWithYourTokenizer({ messages })
378
+ })
379
+ )
380
+ .compactAfter(100_000); // Auto-compact at 100K estimated tokens
381
+ ```
382
+
383
+ ### How It Works
384
+
385
+ 1. **Protect head** — first N messages are never compacted (default 3)
386
+ 2. **Protect tail** — walk backward from the end, accumulating tokens up to a budget (default 20K tokens)
387
+ 3. **Align boundaries** — shift boundaries to avoid splitting tool call/result pairs
388
+ 4. **Summarize middle** — send the middle section to an LLM with a structured format (Topic, Key Points, Current State, Open Items)
389
+ 5. **Store overlay** — saved in `assistant_compactions` table, keyed by `fromMessageId` and `toMessageId`
390
+ 6. **Iterative** — on subsequent compactions, the existing summary is passed to the LLM to update rather than replace
391
+
392
+ When `getHistory()` is called, compaction overlays are applied transparently — the compacted range is replaced by a synthetic message with id `compaction_<id>`.
393
+
394
+ ### Manual Compaction
395
+
396
+ ```typescript
397
+ // Run registered compaction function
398
+ const result = await session.compact();
399
+
400
+ // Or manage overlays directly
401
+ await session.addCompaction("Summary of messages 1-50", "msg-1", "msg-50");
402
+ const overlays = await session.getCompactions();
403
+ ```
404
+
405
+ ### Auto-Compaction
406
+
407
+ When `.compactAfter(threshold)` is set, `appendMessage()` checks the estimated token count after each write. If it exceeds the threshold, `compact()` is called automatically. Auto-compaction failure is non-fatal — the message is already saved.
408
+
409
+ > Auto-compaction is checked **between turns** (on each `appendMessage()`), not within a turn. A single long, tool-heavy turn can grow past the model's context window mid-flight, before the next check. `@cloudflare/think` adds opt-in mid-turn recovery on top of this — see [Context-window overflow recovery](https://github.com/cloudflare/agents/blob/main/docs/think/index.md#context-window-overflow-recovery).
410
+
411
+ By default, the estimate includes stored message parts plus the Session-managed frozen system prompt. That means context blocks and cached prompts managed by `Session` contribute to the threshold. The estimate does not include framework-specific prompt additions or tool schema serialization that happen outside `Session`, such as Think's final capability prompt and tool catalog.
412
+
413
+ There are two token-counting decisions:
414
+
415
+ - `.compactAfter(threshold, { tokenCounter })` controls when automatic compaction is triggered after writes. It can include the frozen system prompt and context blocks.
416
+ - `createCompactFunction({ tokenCounter })` controls which tail messages are protected from summarization. Use this when tool-heavy histories are much larger than the Workers-safe heuristic can estimate.
417
+
418
+ You usually only need to configure one counter. The `.compactAfter()` counter now also flows into `createCompactFunction`'s boundary walk (via `CompactContext`) when no explicit `createCompactFunction({ tokenCounter })` is given, so a single counter drives both "should we compact?" and "what should we compact?". Without this, a fire-only counter plus the under-counting heuristic could make compaction trigger every turn but silently no-op on tool-heavy histories (fixed in [#1593](https://github.com/cloudflare/agents/issues/1593)).
419
+
420
+ Caveat: the flowed counter is invoked **per message** during the boundary walk. A tokenizer-style counter budgets accurately; a usage-only counter that returns a fixed whole-prompt total (e.g. `usage.inputTokens` regardless of which messages are passed) degrades the tail budget to `minTailMessages` — compaction still runs and context stays bounded, but the byte budget is effectively ignored. Pass an explicit per-message `createCompactFunction({ tokenCounter })` for precise tail budgeting.
421
+
422
+ Use a custom counter when you have model-reported usage or your own tokenizer:
423
+
424
+ ```typescript
425
+ const session = Session.create(this)
426
+ .onCompaction(myCompactFn)
427
+ .compactAfter(100_000, {
428
+ tokenCounter: async ({ messages, systemPrompt, contextBlocks }) => {
429
+ return estimateWithYourTokenizer({
430
+ messages,
431
+ systemPrompt,
432
+ contextBlocks
433
+ });
434
+ }
435
+ })
436
+ .onCompactionError((err) => {
437
+ console.warn("Auto-compaction failed", err);
438
+ });
439
+ ```
440
+
441
+ > **Note:** The default token estimation is heuristic (not tiktoken). It uses `max(chars/4, words*1.3)` with 4 tokens per-message overhead, and also applies the string heuristic to the Session-managed system prompt. This is intentional — tiktoken would add 80-120MB heap overhead, which exceeds Cloudflare Workers' 128MB limit.
442
+
443
+ > **Gotcha:** Compaction is iterative but single-overlay. Each new compaction extends from the earliest existing compaction's `fromMessageId` to the new end. So you always have at most one active compaction overlay per session, and it keeps growing. The previous compaction rows remain in the database but are superseded by the latest one (which covers a wider range). `getCompactions()` returns all of them, but `getHistory()` applies the latest one.
444
+
445
+ ---
446
+
447
+ ## SessionManager
448
+
449
+ `SessionManager` is a registry for multiple named sessions within a single Durable Object. It provides lifecycle management, convenience methods, and cross-session search.
450
+
451
+ ### Creating a SessionManager
452
+
453
+ ```typescript
454
+ import { SessionManager } from "agents/experimental/memory/session";
455
+
456
+ const manager = SessionManager.create(this)
457
+ .withContext("soul", { provider: { get: async () => "You are helpful." } })
458
+ .withContext("memory", { description: "Learned facts", maxTokens: 1100 })
459
+ .withCachedPrompt()
460
+ .onCompaction(myCompactFn)
461
+ .compactAfter(100_000)
462
+ .withSearchableHistory("history");
463
+ ```
464
+
465
+ Context blocks, prompt caching, and compaction settings are propagated to all sessions created through the manager. Provider keys are automatically namespaced by session ID (e.g. `memory_<sessionId>`).
466
+
467
+ ### Builder Methods
468
+
469
+ | Method | Description |
470
+ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
471
+ | `SessionManager.create(agent)` | Static factory. |
472
+ | `.withContext(label, options?)` | Add context block template for all sessions. |
473
+ | `.withCachedPrompt(provider?)` | Enable prompt persistence for all sessions. |
474
+ | `.onCompaction(fn)` | Register compaction function for all sessions. |
475
+ | `.compactAfter(tokenThreshold, options?)` | Auto-compact threshold for all sessions. Supports the same `tokenCounter` option as `Session`. |
476
+ | `.onCompactionError(handler)` | Handle automatic compaction errors for managed sessions. |
477
+ | `.withSearchableHistory(label)` | Add a cross-session searchable history block to every session. The model can search past conversations from any session. |
478
+
479
+ ### Session Lifecycle
480
+
481
+ ```typescript
482
+ // Create a new session
483
+ const info = manager.create("My Chat");
484
+ // info: { id, name, parent_session_id, model, source, input_tokens, output_tokens, estimated_cost, end_reason, created_at, updated_at }
485
+
486
+ // Create with metadata
487
+ const info2 = manager.create("My Chat", {
488
+ parentSessionId: "parent-id",
489
+ model: "claude-sonnet-4-20250514",
490
+ source: "web"
491
+ });
492
+
493
+ // Get session metadata (null if not found)
494
+ const session = manager.get(sessionId);
495
+
496
+ // List all sessions (ordered by updated_at DESC)
497
+ const sessions = manager.list();
498
+
499
+ // Rename
500
+ manager.rename(sessionId, "New Name");
501
+
502
+ // Delete (clears messages too)
503
+ await manager.delete(sessionId);
504
+ ```
505
+
506
+ ### Accessing Sessions
507
+
508
+ ```typescript
509
+ // Get or create the Session instance for an ID
510
+ // Lazy — creates on first access, caches for subsequent calls
511
+ const session = manager.getSession(sessionId);
512
+ ```
513
+
514
+ ### Message Convenience Methods
515
+
516
+ These delegate to the underlying Session but also update the session's `updated_at` timestamp:
517
+
518
+ ```typescript
519
+ // Append a single message
520
+ await manager.append(sessionId, message, parentId?);
521
+
522
+ // Add or update (upsert)
523
+ await manager.upsert(sessionId, message, parentId?);
524
+
525
+ // Batch append (auto-chains parent IDs)
526
+ await manager.appendAll(sessionId, messages, parentId?);
527
+
528
+ // Read history
529
+ const history = await manager.getHistory(sessionId, leafId?);
530
+
531
+ // Message count
532
+ const count = await manager.getMessageCount(sessionId);
533
+
534
+ // Clear messages
535
+ await manager.clearMessages(sessionId);
536
+
537
+ // Delete specific messages
538
+ await manager.deleteMessages(sessionId, ["msg-1"]);
539
+ ```
540
+
541
+ ### Forking
542
+
543
+ Fork a session at a specific message — copies history up to that point into a new session:
544
+
545
+ ```typescript
546
+ const forked = await manager.fork(sessionId, atMessageId, "Forked Chat");
547
+ // forked.parent_session_id === sessionId
548
+ ```
549
+
550
+ > **Gotcha:** Fork copies messages with new UUIDs, not the original IDs. This means message IDs in the forked session will not match the original. The fork also does not copy compaction overlays — the forked session starts clean with the materialized history.
551
+
552
+ ### Compaction
553
+
554
+ ```typescript
555
+ // Add a compaction overlay
556
+ await manager.addCompaction(sessionId, summary, fromId, toId);
557
+
558
+ // Get overlays
559
+ const compactions = await manager.getCompactions(sessionId);
560
+
561
+ // Compact and split — marks old session as ended, creates a continuation
562
+ const continuation = await manager.compactAndSplit(
563
+ sessionId,
564
+ summary,
565
+ "Continued Chat"
566
+ );
567
+ // continuation.parent_session_id === sessionId
568
+ // Old session gets end_reason = "compaction"
569
+ ```
570
+
571
+ `compactAndSplit` is different from regular compaction — it creates a new session with a summary message instead of an in-place overlay. The original session is marked with `end_reason: "compaction"`.
572
+
573
+ ### Usage Tracking
574
+
575
+ ```typescript
576
+ manager.addUsage(sessionId, inputTokens, outputTokens, cost);
577
+ // Increments input_tokens, output_tokens, and estimated_cost on the session row
578
+ ```
579
+
580
+ ### Cross-Session Search
581
+
582
+ ```typescript
583
+ // Search across all sessions (FTS5)
584
+ const results = manager.search("deployment Friday", { limit: 20 });
585
+ // Returns: Array<{ id, role, content, createdAt }>
586
+
587
+ // Get tools for the model (includes session_search)
588
+ const tools = manager.tools();
589
+ ```
590
+
591
+ > **Note:** `manager.search()` uses a separate FTS5 index (`assistant_fts`) from per-session search. Messages are indexed into this table by the `AgentSessionProvider` when appended. The `session_search` tool limits results to 10.
592
+
593
+ > **Gotcha:** `manager.search()` silently returns an empty array on FTS5 query errors (malformed queries, etc.) rather than throwing.
594
+
595
+ ---
596
+
597
+ ## Storage
598
+
599
+ By default, storage is in Durable Object SQLite and tables are created lazily on first use. Postgres-backed sessions use the external tables shown in the Postgres section below.
600
+
601
+ ### Tables
602
+
603
+ **`assistant_messages`** — Tree-structured messages.
604
+
605
+ | Column | Type | Notes |
606
+ | ------------ | -------- | ------------------------------------------------------ |
607
+ | `id` | TEXT | Message ID |
608
+ | `session_id` | TEXT | Empty string for single-session; set for multi-session |
609
+ | `parent_id` | TEXT | Parent message ID (null for roots) |
610
+ | `role` | TEXT | `user`, `assistant`, `system` |
611
+ | `content` | TEXT | JSON-serialized `SessionMessage` |
612
+ | `created_at` | DATETIME | Auto-set |
613
+
614
+ For Postgres, messages use `PRIMARY KEY (session_id, id)` so caller-provided IDs only need to be unique within a session.
615
+
616
+ **`assistant_compactions`** — Compaction overlays.
617
+
618
+ | Column | Type | Notes |
619
+ | ----------------- | -------- | ------------------------ |
620
+ | `id` | TEXT PK | Random UUID |
621
+ | `session_id` | TEXT | Scoped to session |
622
+ | `summary` | TEXT | LLM-generated summary |
623
+ | `from_message_id` | TEXT | Start of compacted range |
624
+ | `to_message_id` | TEXT | End of compacted range |
625
+ | `created_at` | DATETIME | Auto-set |
626
+
627
+ **`assistant_fts`** — FTS5 virtual table for message search. Tokenizer: `porter unicode61`.
628
+
629
+ **`assistant_sessions`** — Session registry (SessionManager only).
630
+
631
+ | Column | Type | Notes |
632
+ | ------------------- | -------- | -------------------------- |
633
+ | `id` | TEXT PK | Random UUID |
634
+ | `name` | TEXT | Display name |
635
+ | `parent_session_id` | TEXT | For forks/splits |
636
+ | `model` | TEXT | Optional model identifier |
637
+ | `source` | TEXT | Optional source identifier |
638
+ | `input_tokens` | INTEGER | Cumulative input tokens |
639
+ | `output_tokens` | INTEGER | Cumulative output tokens |
640
+ | `estimated_cost` | REAL | Cumulative cost |
641
+ | `end_reason` | TEXT | `"compaction"` when split |
642
+ | `created_at` | DATETIME | Auto-set |
643
+ | `updated_at` | DATETIME | Updated on message ops |
644
+
645
+ **`cf_agents_context_blocks`** — Persistent context block storage (`AgentContextProvider`).
646
+
647
+ **`cf_agents_search_entries`** + **`cf_agents_search_fts`** — Searchable context entries and FTS5 index (`AgentSearchProvider`).
648
+
649
+ ---
650
+
651
+ ## Custom Providers
652
+
653
+ You can implement any of the four provider interfaces to plug in your own storage:
654
+
655
+ ```typescript
656
+ // Read-only context
657
+ const myProvider: ContextProvider = {
658
+ get: async () => "Static content here"
659
+ };
660
+
661
+ // Writable context (enables set_context tool)
662
+ const myWritable: WritableContextProvider = {
663
+ get: async () => fetchFromMyDB(),
664
+ set: async (content) => saveToMyDB(content)
665
+ };
666
+
667
+ // Skill provider (enables load_context tool)
668
+ const mySkills: SkillProvider = {
669
+ get: async () => "- api-ref: API Reference\n- guide: User Guide",
670
+ load: async (key) => fetchDocument(key),
671
+ set: async (key, content, description) =>
672
+ saveDocument(key, content, description) // optional
673
+ };
674
+
675
+ // Search provider (enables search_context tool)
676
+ const mySearch: SearchProvider = {
677
+ get: async () => "42 entries indexed",
678
+ search: async (query) => searchMyIndex(query),
679
+ set: async (key, content) => indexContent(key, content) // optional
680
+ };
681
+ ```
682
+
683
+ You can also implement `SessionProvider` to replace the SQLite storage entirely:
684
+
685
+ ```typescript
686
+ const myStorage: SessionProvider = {
687
+ getMessage(id) { ... },
688
+ getHistory(leafId?) { ... },
689
+ getLatestLeaf() { ... },
690
+ getBranches(messageId) { ... },
691
+ getPathLength(leafId?) { ... },
692
+ appendMessage(message, parentId?) { ... },
693
+ updateMessage(message) { ... },
694
+ deleteMessages(messageIds) { ... },
695
+ clearMessages() { ... },
696
+ addCompaction(summary, fromId, toId) { ... },
697
+ getCompactions() { ... },
698
+ searchMessages(query, limit) { ... } // optional
699
+ };
700
+ ```
701
+
702
+ ---
703
+
704
+ ## Postgres (External Database)
705
+
706
+ The default providers use Durable Object SQLite. If you need session data in an external Postgres database — for cross-DO queries, analytics, or shared state — use `PostgresSessionProvider`, `PostgresContextProvider`, and `PostgresSearchProvider`.
707
+
708
+ These work with any Postgres-compatible database (Neon, Supabase, PlanetScale, etc.) via [Cloudflare Hyperdrive](https://developers.cloudflare.com/hyperdrive/) for connection pooling.
709
+
710
+ ### Setup
711
+
712
+ #### 1. Create a Postgres database
713
+
714
+ Use any Postgres provider and copy the connection string.
715
+
716
+ #### 2. Create a Hyperdrive config
717
+
718
+ ```bash
719
+ npx wrangler hyperdrive create my-session-db \
720
+ --connection-string="postgresql://user:password@host:port/dbname"
721
+ ```
722
+
723
+ Copy the returned Hyperdrive ID.
724
+
725
+ #### 3. Create the tables
726
+
727
+ The Postgres user might not have `CREATE TABLE` permissions. Run this once in your database console:
728
+
729
+ ```sql
730
+ CREATE TABLE IF NOT EXISTS assistant_messages (
731
+ id TEXT NOT NULL,
732
+ session_id TEXT NOT NULL DEFAULT '',
733
+ parent_id TEXT,
734
+ role TEXT NOT NULL,
735
+ content TEXT NOT NULL,
736
+ text_content TEXT NOT NULL DEFAULT '',
737
+ created_at TIMESTAMPTZ DEFAULT NOW(),
738
+ content_tsv TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', text_content)) STORED,
739
+ PRIMARY KEY (session_id, id)
740
+ );
741
+ CREATE INDEX IF NOT EXISTS idx_assistant_msg_parent ON assistant_messages (parent_id);
742
+ CREATE INDEX IF NOT EXISTS idx_assistant_msg_session ON assistant_messages (session_id);
743
+ CREATE INDEX IF NOT EXISTS idx_assistant_msg_fts ON assistant_messages USING GIN (content_tsv);
744
+
745
+ CREATE TABLE IF NOT EXISTS assistant_compactions (
746
+ id TEXT PRIMARY KEY,
747
+ session_id TEXT NOT NULL DEFAULT '',
748
+ summary TEXT NOT NULL,
749
+ from_message_id TEXT NOT NULL,
750
+ to_message_id TEXT NOT NULL,
751
+ created_at TIMESTAMPTZ DEFAULT NOW()
752
+ );
753
+
754
+ CREATE TABLE IF NOT EXISTS cf_agents_context_blocks (
755
+ label TEXT PRIMARY KEY,
756
+ content TEXT NOT NULL,
757
+ updated_at TIMESTAMPTZ DEFAULT NOW()
758
+ );
759
+
760
+ CREATE TABLE IF NOT EXISTS cf_agents_search_entries (
761
+ label TEXT NOT NULL,
762
+ key TEXT NOT NULL,
763
+ content TEXT NOT NULL,
764
+ content_tsv TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
765
+ created_at TIMESTAMPTZ DEFAULT NOW(),
766
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
767
+ PRIMARY KEY (label, key)
768
+ );
769
+ CREATE INDEX IF NOT EXISTS idx_search_entries_fts ON cf_agents_search_entries USING GIN (content_tsv);
770
+ ```
771
+
772
+ #### 4. Configure wrangler
773
+
774
+ ```jsonc
775
+ {
776
+ "compatibility_flags": ["nodejs_compat"],
777
+ "hyperdrive": [
778
+ {
779
+ "binding": "HYPERDRIVE",
780
+ "id": "<your-hyperdrive-id>"
781
+ }
782
+ ],
783
+ "placement": {
784
+ "region": "aws:us-east-1" // match your database region
785
+ }
786
+ }
787
+ ```
788
+
789
+ #### 5. Wire it up
790
+
791
+ ```typescript
792
+ import { Agent, callable } from "agents";
793
+ import {
794
+ Session,
795
+ PostgresSessionProvider,
796
+ PostgresContextProvider,
797
+ PostgresSearchProvider
798
+ } from "agents/experimental/memory/session";
799
+ import { Client } from "pg";
800
+
801
+ class MyAgent extends Agent<Env> {
802
+ private _session?: Session;
803
+ private _pgClient?: Client;
804
+
805
+ /**
806
+ * Initialize Hyperdrive and Session when the Durable Object starts.
807
+ * The providers take the raw pg.Client directly — no wrapper needed.
808
+ */
809
+ async onStart(): Promise<void> {
810
+ const client = new Client({
811
+ connectionString: this.env.HYPERDRIVE.connectionString
812
+ });
813
+ await client.connect();
814
+ this._pgClient = client;
815
+
816
+ const sessionId = this.ctx.id.toString();
817
+ this._session = Session.create(
818
+ new PostgresSessionProvider(client, sessionId)
819
+ )
820
+ .withContext("soul", {
821
+ provider: {
822
+ get: async () => "You are a helpful assistant."
823
+ }
824
+ })
825
+ .withContext("memory", {
826
+ description: "Short facts",
827
+ maxTokens: 1100,
828
+ provider: new PostgresContextProvider(client, `memory_${sessionId}`)
829
+ })
830
+ .withContext("knowledge", {
831
+ description: "Searchable knowledge base",
832
+ provider: new PostgresSearchProvider(client)
833
+ })
834
+ .withCachedPrompt(
835
+ new PostgresContextProvider(client, `_prompt_${sessionId}`)
836
+ );
837
+ }
838
+ }
839
+ ```
840
+
841
+ ### How it works
842
+
843
+ When `Session.create()` receives a `SessionProvider` instead of a `SqlProvider`, it skips all SQLite auto-wiring. This means:
844
+
845
+ - **Context blocks need explicit providers.** No auto-wiring to SQLite — each `withContext()` call needs a `provider` option, or the block will be read-only with no storage.
846
+ - **`withCachedPrompt()` needs an explicit provider.** Pass a `PostgresContextProvider` to persist the frozen system prompt.
847
+ - **Broadcaster is skipped.** WebSocket status broadcasts (`CF_AGENT_SESSION` events) only work with `SqlProvider`-based sessions.
848
+ - **All Session methods are async.** `getHistory()`, `getMessage()`, etc. return Promises since the underlying storage is async.
849
+
850
+ ### System prompt lifecycle
851
+
852
+ - **`freezeSystemPrompt()`** — returns the cached prompt from the store. On first call (cache miss), loads blocks from providers, renders, and persists. Subsequent calls return the stored value without re-rendering. This preserves LLM prefix cache hits.
853
+ - **`refreshSystemPrompt()`** — force reloads blocks from providers, re-renders, and updates the store. Call this to invalidate the cached prompt (e.g. after `clearMessages`).
854
+
855
+ ### Connection types
856
+
857
+ The Postgres providers accept either of:
858
+
859
+ - A raw `pg.Client` (or any object with a compatible `query(text, values)` method) — the recommended path for Hyperdrive.
860
+ - Any object implementing `PostgresConnection` — useful for tests or custom drivers.
861
+
862
+ ```typescript
863
+ // For tests or custom drivers
864
+ interface PostgresConnection {
865
+ execute(
866
+ query: string,
867
+ args?: (string | number | boolean | null)[]
868
+ ): Promise<{ rows: Record<string, unknown>[] }>;
869
+ }
870
+ ```
871
+
872
+ Internally the providers use `?` placeholders; when a `pg`-style client is passed, those are rewritten to `$1, $2, …` automatically.
873
+
874
+ ### Search
875
+
876
+ Two levels of search are available:
877
+
878
+ - **Message search** — `PostgresSessionProvider.searchMessages()` searches conversation history via the `content_tsv` column on `assistant_messages`.
879
+ - **Knowledge search** — `PostgresSearchProvider` provides a searchable context block backed by `cf_agents_search_entries`. The LLM can index content via `set_context` and query it via `search_context`. Uses `tsvector` + GIN index with English stemming and `ts_rank` for relevance ranking.
880
+
881
+ The migration SQL above includes both tables with tsvector columns and GIN indexes — search works out of the box.
882
+
883
+ ---
884
+
885
+ ## Utilities
886
+
887
+ Exported from `agents/experimental/memory/utils`:
888
+
889
+ ### Token Estimation
890
+
891
+ ```typescript
892
+ import {
893
+ estimateStringTokens,
894
+ estimateMessageTokens
895
+ } from "agents/experimental/memory/utils/tokens";
896
+
897
+ estimateStringTokens("Hello world"); // heuristic: max(chars/4, words*1.3)
898
+ estimateMessageTokens(messages); // sum with 4 tokens per-message overhead
899
+ ```
900
+
901
+ ### Compaction Helpers
902
+
903
+ ```typescript
904
+ import {
905
+ createCompactFunction,
906
+ isCompactionMessage,
907
+ sanitizeToolPairs,
908
+ alignBoundaryForward,
909
+ alignBoundaryBackward,
910
+ findTailCutByTokens,
911
+ computeSummaryBudget,
912
+ buildSummaryPrompt,
913
+ COMPACTION_PREFIX
914
+ } from "agents/experimental/memory/utils/compaction-helpers";
915
+ ```
916
+
917
+ - `createCompactFunction(options)` — Full compaction implementation. See [Compaction](#compaction).
918
+ - `isCompactionMessage(msg)` — Check if a message is a compaction overlay (id starts with `compaction_`).
919
+ - `sanitizeToolPairs(messages)` — Fix orphaned tool call/result pairs after compaction. Removes orphaned results and adds stub results for calls whose results were dropped.
920
+ - `alignBoundaryForward/Backward(messages, idx)` — Shift a boundary index to avoid splitting tool call/result groups.
921
+ - `findTailCutByTokens(messages, headEnd, budget, minMessages)` — Find where to stop compressing using a token budget.
922
+ - `computeSummaryBudget(messages)` — 20% of compressed content tokens (minimum 100).
923
+ - `buildSummaryPrompt(messages, previousSummary, budget)` — Structured prompt for LLM summarization.
924
+
925
+ ---
926
+
927
+ ## Exports
928
+
929
+ Everything is exported from `agents/experimental/memory/session`:
930
+
931
+ ```typescript
932
+ import {
933
+ // Core
934
+ Session,
935
+ SessionManager,
936
+
937
+ // Providers
938
+ AgentSessionProvider,
939
+ AgentContextProvider,
940
+ AgentSearchProvider,
941
+ R2SkillProvider,
942
+ PostgresSessionProvider,
943
+ PostgresContextProvider,
944
+ PostgresSearchProvider,
945
+
946
+ // Type guards
947
+ isWritableProvider,
948
+ isSkillProvider,
949
+ isSearchProvider,
950
+
951
+ // Types
952
+ type SessionMessage,
953
+ type SessionMessagePart,
954
+ type SessionContextOptions,
955
+ type SessionInfo,
956
+ type SessionManagerOptions,
957
+ type SessionOptions,
958
+ type ContextBlock,
959
+ type ContextConfig,
960
+ type ContextProvider,
961
+ type WritableContextProvider,
962
+ type SkillProvider,
963
+ type SearchProvider,
964
+ type SearchResult,
965
+ type SessionProvider,
966
+ type StoredCompaction,
967
+ type SqlProvider,
968
+ type PostgresConnection
969
+ } from "agents/experimental/memory/session";
970
+ ```
971
+
972
+ Compaction utilities from `agents/experimental/memory/utils/compaction-helpers`:
973
+
974
+ ```typescript
975
+ import {
976
+ createCompactFunction,
977
+ isCompactionMessage,
978
+ sanitizeToolPairs,
979
+ COMPACTION_PREFIX,
980
+ type CompactResult,
981
+ type CompactOptions
982
+ } from "agents/experimental/memory/utils/compaction-helpers";
983
+ ```
984
+
985
+ Token utilities from `agents/experimental/memory/utils/tokens`:
986
+
987
+ ```typescript
988
+ import {
989
+ estimateStringTokens,
990
+ estimateMessageTokens
991
+ } from "agents/experimental/memory/utils/tokens";
992
+ ```
993
+
994
+ ---
995
+
996
+ ## Gotchas and Quirks
997
+
998
+ Things that might surprise you:
999
+
1000
+ 1. **Lazy initialization.** Sessions created with the builder do not initialize until first use. The first call to any method (e.g. `getHistory()`) triggers `_ensureReady()`, which creates SQLite tables or initializes the configured provider, resolves providers, loads context blocks, and restores skill state from history. This means the first operation is slower than subsequent ones.
1001
+
1002
+ 2. **Snapshot freezing is sticky.** `freezeSystemPrompt()` caches the result. Writing to a context block does NOT update the cached snapshot — you must explicitly call `refreshSystemPrompt()`. This is deliberate (LLM prefix cache optimization), but easy to miss.
1003
+
1004
+ 3. **Session methods are async.** Always `await` reads and writes. SQLite-backed storage is local and fast, but external providers may perform network I/O, and `appendMessage` can trigger auto-compaction.
1005
+
1006
+ 4. **Skills survive hibernation via history scanning.** On initialization, the session scans the entire conversation history looking for `load_context` tool results to reconstruct which skills are loaded. This is clever but means initialization cost scales with conversation length.
1007
+
1008
+ 5. **Compaction overlays are superseding, not stacking.** Each compaction extends from the earliest existing `fromMessageId`. So you always have one effective overlay that keeps growing. Old compaction rows remain in the database but are unused. `getCompactions()` returns all rows, which can be confusing.
1009
+
1010
+ 6. **Search is silently absent.** `session.search()` throws if the provider does not support search, but `manager.search()` swallows FTS5 errors and returns `[]`. The `searchMessages` method on `SessionProvider` is optional (`searchMessages?`).
1011
+
1012
+ 7. **Fork copies with new IDs.** When forking via `SessionManager.fork()`, all messages get new UUIDs. If you are storing message IDs externally (e.g. for bookmarks), they will not survive a fork.
1013
+
1014
+ 8. **`removeContext` does not fire skill unload callbacks.** If you remove a context block that had loaded skills, the skill tracking is cleaned up but the conversation history is NOT rewritten. The tool results from those skills remain in history with their full content.
1015
+
1016
+ 9. **FTS5 query sanitization.** Both `AgentSearchProvider.search()` and `SessionManager.search()` quote individual words to prevent FTS5 syntax injection. This means you cannot use FTS5 operators like `OR`, `NOT`, or `NEAR` — they will be treated as literal search terms.
1017
+
1018
+ 10. **Auto-compaction failure is silent.** When `compactAfter` triggers and the compaction function throws, the error is emitted via WebSocket broadcast but the `appendMessage` call still succeeds. The message is saved; only the compaction is skipped.
1019
+
1020
+ ## Acknowledgments
1021
+
1022
+ - Session's tree-structured messages are inspired by [Pi](https://pi.dev).
1023
+ - Context blocks are inspired by [Letta AI memory blocks](https://www.letta.com/blog/memory-blocks).
1024
+ - Formatting of blocks is inspired by [Hermes Agent](https://github.com/nousresearch/hermes-agent).