agents 0.16.2 → 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.
- package/README.md +11 -8
- package/dist/{agent-tool-types-CTw3UJUP.d.ts → agent-tool-types-Cd1TZPfB.d.ts} +587 -137
- package/dist/agent-tool-types.d.ts +34 -18
- package/dist/agent-tool-types.js +20 -1
- package/dist/agent-tool-types.js.map +1 -0
- package/dist/agent-tools-BXlsuX0d.js +304 -0
- package/dist/agent-tools-BXlsuX0d.js.map +1 -0
- package/dist/agent-tools-_E8wxUIK.d.ts +119 -0
- package/dist/agent-tools.d.ts +24 -18
- package/dist/agent-tools.js.map +1 -1
- package/dist/ai-chat-agent.d.ts +1 -1
- package/dist/ai-chat-agent.js +1 -2
- package/dist/ai-chat-agent.js.map +1 -1
- package/dist/ai-chat-v5-migration.d.ts +1 -1
- package/dist/ai-chat-v5-migration.js +1 -2
- package/dist/ai-chat-v5-migration.js.map +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +1 -2
- package/dist/ai-react.js.map +1 -1
- package/dist/ai-types.d.ts +1 -7
- package/dist/ai-types.js +2 -8
- package/dist/ai-types.js.map +1 -1
- package/dist/browser/ai.js +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/chat/index.d.ts +1918 -72
- package/dist/chat/index.js +1730 -245
- package/dist/chat/index.js.map +1 -1
- package/dist/chat/react.d.ts +602 -0
- package/dist/chat/react.js +1506 -0
- package/dist/chat/react.js.map +1 -0
- package/dist/chat-sdk/index.d.ts +4 -4
- package/dist/{classPrivateFieldGet2-CZ7QjTXN.js → classPrivateFieldGet2-DZBYAB34.js} +5 -5
- package/dist/{classPrivateMethodInitSpec-D-0__zd9.js → classPrivateMethodInitSpec-qMjJ6sHQ.js} +2 -2
- package/dist/{client-BXJ9n2f7.js → client-BZ-B3NhC.js} +2 -2
- package/dist/client-BZ-B3NhC.js.map +1 -0
- package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
- package/dist/client.d.ts +76 -57
- package/dist/client.js +33 -5
- package/dist/client.js.map +1 -1
- package/dist/{connector-CrKhowfD.js → connector-CdldGF3h.js} +5 -5
- package/dist/{connector-CrKhowfD.js.map → connector-CdldGF3h.js.map} +1 -1
- package/dist/email.js +1 -1
- package/dist/email.js.map +1 -1
- package/dist/{index-B7IbEeze.d.ts → index-CcbnKkNh.d.ts} +188 -14
- package/dist/index.d.ts +91 -71
- package/dist/index.js +562 -24
- package/dist/index.js.map +1 -1
- package/dist/mcp/client.d.ts +18 -14
- package/dist/mcp/client.js +1 -1
- package/dist/mcp/index.d.ts +30 -30
- package/dist/mcp/index.js +7 -7
- package/dist/mcp/index.js.map +1 -1
- package/dist/{agent-tools-3zLG7MgA.js → message-builder-BymO4N_D.js} +45 -126
- package/dist/message-builder-BymO4N_D.js.map +1 -0
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +4 -2
- package/dist/observability/index.js.map +1 -1
- package/dist/react.d.ts +123 -110
- package/dist/react.js +44 -11
- package/dist/react.js.map +1 -1
- package/dist/serializable.d.ts +1 -1
- package/dist/skills/compile.js +1 -1
- package/dist/skills/compile.js.map +1 -1
- package/dist/skills/index.js +5 -5
- package/dist/skills/index.js.map +1 -1
- package/dist/sub-routing.d.ts +6 -6
- package/dist/utils.js +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/vite.js +5 -5
- package/dist/vite.js.map +1 -1
- package/dist/wire-types-nflOzNuU.js +240 -0
- package/dist/wire-types-nflOzNuU.js.map +1 -0
- package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
- package/dist/workflow-types.d.ts +25 -21
- package/dist/workflow-types.js.map +1 -1
- package/dist/workflows.d.ts +22 -21
- package/dist/workflows.js +31 -7
- package/dist/workflows.js.map +1 -1
- package/docs/adding-to-existing-project.md +450 -0
- package/docs/agent-class.md +503 -0
- package/docs/agent-tools.md +552 -0
- package/docs/browse-the-web.md +430 -0
- package/docs/callable-methods.md +627 -0
- package/docs/chat-agents.md +1687 -0
- package/docs/chat-sdk.md +181 -0
- package/docs/client-sdk.md +520 -0
- package/docs/client-tools-continuation.md +177 -0
- package/docs/codemode.md +440 -0
- package/docs/configuration.md +775 -0
- package/docs/cross-domain-authentication.md +171 -0
- package/docs/durable-execution.md +537 -0
- package/docs/email.md +663 -0
- package/docs/get-current-agent.md +204 -0
- package/docs/getting-started.md +305 -0
- package/docs/http-websockets.md +668 -0
- package/docs/human-in-the-loop.md +661 -0
- package/docs/index.md +151 -0
- package/docs/long-running-agents.md +730 -0
- package/docs/mcp-client.md +620 -0
- package/docs/mcp-servers.md +526 -0
- package/docs/mcp-transports.md +308 -0
- package/docs/migration-to-ai-sdk-v5.md +96 -0
- package/docs/migration-to-ai-sdk-v6.md +163 -0
- package/docs/observability.md +261 -0
- package/docs/push-notifications.md +367 -0
- package/docs/queue.md +329 -0
- package/docs/readonly-connections.md +278 -0
- package/docs/resumable-streaming.md +127 -0
- package/docs/retries.md +444 -0
- package/docs/routing.md +749 -0
- package/docs/scheduling.md +898 -0
- package/docs/securing-mcp-servers.md +359 -0
- package/docs/server-driven-messages.md +477 -0
- package/docs/sessions.md +1024 -0
- package/docs/state.md +512 -0
- package/docs/sub-agents.md +389 -0
- package/docs/webhooks.md +604 -0
- package/docs/workflows.md +877 -0
- package/package.json +41 -14
- package/dist/agent-tools-3zLG7MgA.js.map +0 -1
- package/dist/agent-tools-DZhI5F6Q.d.ts +0 -14
- package/dist/client-BXJ9n2f7.js.map +0 -1
package/docs/sessions.md
ADDED
|
@@ -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).
|