agents 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/{agent-routing-CnkaHb-v.d.ts → agent-routing-DE5zmCQ8.d.ts} +1038 -394
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-tool-types.d.ts +26 -26
- package/dist/{agent-tools-C0XJqWzB.d.ts → agent-tools-DtXMTDGM.d.ts} +7 -7
- package/dist/agent-tools.d.ts +21 -21
- package/dist/browser/ai.js +4 -1
- package/dist/browser/ai.js.map +1 -1
- package/dist/browser/tanstack-ai.js.map +1 -1
- package/dist/callable-decorator-DP__HhBA.d.ts +72 -0
- package/dist/callable-decorator.d.ts +18 -0
- package/dist/callable-decorator.js +71 -0
- package/dist/callable-decorator.js.map +1 -0
- package/dist/capability-BjSKYpzg.js.map +1 -1
- package/dist/capability-runner-Be_-PLR1.d.ts +459 -0
- package/dist/channel-Bnm4S7T2.d.ts +491 -0
- package/dist/channels/ai-sdk.d.ts +46 -0
- package/dist/channels/ai-sdk.js +120 -0
- package/dist/channels/ai-sdk.js.map +1 -0
- package/dist/channels/email.d.ts +95 -0
- package/dist/channels/email.js +323 -0
- package/dist/channels/email.js.map +1 -0
- package/dist/channels/index.d.ts +233 -0
- package/dist/channels/index.js +608 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/slack.d.ts +140 -0
- package/dist/channels/slack.js +614 -0
- package/dist/channels/slack.js.map +1 -0
- package/dist/channels/tanstack-ai.d.ts +39 -0
- package/dist/channels/tanstack-ai.js +17 -0
- package/dist/channels/tanstack-ai.js.map +1 -0
- package/dist/channels/telegram.d.ts +106 -0
- package/dist/channels/telegram.js +427 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/voice.d.ts +45 -0
- package/dist/channels/voice.js +122 -0
- package/dist/channels/voice.js.map +1 -0
- package/dist/chat/index.d.ts +2325 -2019
- package/dist/chat/index.js +888 -518
- package/dist/chat/index.js.map +1 -1
- package/dist/chat-sdk/index.d.ts +7 -7
- package/dist/chat-sdk/index.js +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/context/index.d.ts +216 -0
- package/dist/context/index.js +454 -0
- package/dist/context/index.js.map +1 -0
- package/dist/{current-agent-CuMErtly.d.ts → current-agent-Da_C9a3b.d.ts} +90 -107
- package/dist/current-agent-DhoDkSnH.js.map +1 -1
- package/dist/{diagnostics-CaBjfz4J.js → diagnostics-BzvaX2UT.js} +5 -1
- package/dist/diagnostics-BzvaX2UT.js.map +1 -0
- package/dist/diagnostics-C4jcz3VK.js +360 -0
- package/dist/diagnostics-C4jcz3VK.js.map +1 -0
- package/dist/index-BB0kqhIz.d.ts +101 -0
- package/dist/index-BVVgDSdq.d.ts +1 -0
- package/dist/index-XDkuQ7zm.d.ts +89 -0
- package/dist/{index-DcSAZKsB.d.ts → index-YSKgfgg9.d.ts} +3 -1
- package/dist/index.d.ts +91 -82
- package/dist/index.js +3 -2
- package/dist/ingress-BfetZbMO.js +83 -0
- package/dist/ingress-BfetZbMO.js.map +1 -0
- package/dist/internal-CYlgHl1l.js +59 -0
- package/dist/internal-CYlgHl1l.js.map +1 -0
- package/dist/internal_context.d.ts +1 -1
- package/dist/lifecycle/index.d.ts +35 -17
- package/dist/lifecycle/index.js +1 -1
- package/dist/lifecycle-CMRGjZdw.js +1299 -0
- package/dist/lifecycle-CMRGjZdw.js.map +1 -0
- package/dist/mcp/client/index.d.ts +20 -20
- package/dist/mcp/index.d.ts +35 -35
- package/dist/mcp/index.js +1 -1
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/react.d.ts +4 -4
- package/dist/{retries-CAvxtG9d.d.ts → retries-D9Ds-1lz.d.ts} +17 -6
- package/dist/retries.d.ts +8 -6
- package/dist/retries.js +13 -1
- package/dist/retries.js.map +1 -1
- package/dist/routing/index.d.ts +137 -0
- package/dist/routing/index.js +244 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/sanitize-D9TujEK8.js +79 -0
- package/dist/sanitize-D9TujEK8.js.map +1 -0
- package/dist/scheduler-DD9NdYbF.js +665 -0
- package/dist/scheduler-DD9NdYbF.js.map +1 -0
- package/dist/{scheduler-DQoTGoAW.d.ts → scheduler-Dwh85ZGl.d.ts} +21 -22
- package/dist/schedules/index.d.ts +1 -1
- package/dist/schedules/index.js +1 -1
- package/dist/sentence-chunker-BAidJ4DA.d.ts +68 -0
- package/dist/serializable.d.ts +1 -1
- package/dist/sessions/index.d.ts +441 -0
- package/dist/sessions/index.js +2063 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/skills/index.d.ts +99 -0
- package/dist/skills/index.js +254 -5
- package/dist/skills/index.js.map +1 -1
- package/dist/{src-5W6JNKVb.js → src-DlSHshb2.js} +1460 -1110
- package/dist/src-DlSHshb2.js.map +1 -0
- package/dist/streams/index.d.ts +120 -0
- package/dist/streams/index.js +107 -0
- package/dist/streams/index.js.map +1 -0
- package/dist/streams-D6tJ0NN9.d.ts +370 -0
- package/dist/streams-DZKgAj9b.js +709 -0
- package/dist/streams-DZKgAj9b.js.map +1 -0
- package/dist/sub-routing.d.ts +12 -12
- package/dist/surface-bZZJqBka.js +17 -0
- package/dist/surface-bZZJqBka.js.map +1 -0
- package/dist/tasks/index.d.ts +64 -0
- package/dist/tasks/index.js +2 -0
- package/dist/tasks-BRJ5zgya.d.ts +517 -0
- package/dist/tasks-ylZgBjhj.js +1656 -0
- package/dist/tasks-ylZgBjhj.js.map +1 -0
- package/dist/text-segment-joiner-BtAFQSA_.js +57 -0
- package/dist/text-segment-joiner-BtAFQSA_.js.map +1 -0
- package/dist/text-stream-CpdiKrJB.js +272 -0
- package/dist/text-stream-CpdiKrJB.js.map +1 -0
- package/dist/tokens-nHAKcN6M.js +52 -0
- package/dist/tokens-nHAKcN6M.js.map +1 -0
- package/dist/tool-schema-CBjGPrsQ.js +31 -0
- package/dist/tool-schema-CBjGPrsQ.js.map +1 -0
- package/dist/types-B7LojTe4.d.ts +202 -0
- package/dist/types-_Faxb570.d.ts +439 -0
- package/dist/voice/client.d.ts +226 -0
- package/dist/voice/client.js +932 -0
- package/dist/voice/client.js.map +1 -0
- package/dist/voice/errors.d.ts +43 -0
- package/dist/voice/errors.js +41 -0
- package/dist/voice/errors.js.map +1 -0
- package/dist/voice/index.d.ts +271 -0
- package/dist/voice/index.js +1812 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/react.d.ts +167 -0
- package/dist/voice/react.js +234 -0
- package/dist/voice/react.js.map +1 -0
- package/dist/voice/sfu.d.ts +71 -0
- package/dist/voice/sfu.js +157 -0
- package/dist/voice/sfu.js.map +1 -0
- package/dist/voice/text.d.ts +6 -0
- package/dist/voice/text.js +2 -0
- package/dist/voice/types.d.ts +58 -0
- package/dist/voice/types.js +18 -0
- package/dist/voice/types.js.map +1 -0
- package/dist/voice/workers-ai.d.ts +136 -0
- package/dist/voice/workers-ai.js +568 -0
- package/dist/voice/workers-ai.js.map +1 -0
- package/dist/websockets/index.d.ts +192 -0
- package/dist/websockets/index.js +2 -0
- package/dist/websockets-DUfRHPRq.js +502 -0
- package/dist/websockets-DUfRHPRq.js.map +1 -0
- package/dist/workflow-types.d.ts +25 -25
- package/dist/workflows.d.ts +21 -21
- package/dist/workflows.js +1 -1
- package/docs/agent-class.md +2 -2
- package/docs/agent-tools.md +2 -1
- package/docs/channels.md +323 -0
- package/docs/chat-agents.md +6 -13
- package/docs/context.md +131 -0
- package/docs/index.md +15 -12
- package/docs/lifecycle.md +102 -55
- package/docs/long-running-agents.md +2 -2
- package/docs/mcp-servers.md +5 -1
- package/docs/resumable-streaming.md +1 -1
- package/docs/routing.md +105 -0
- package/docs/sessions.md +237 -871
- package/docs/streams.md +213 -0
- package/docs/sub-agents.md +184 -124
- package/docs/tasks.md +246 -0
- package/docs/voice.md +745 -0
- package/package.json +115 -13
- package/dist/capability-runner-CvHGZqUu.d.ts +0 -150
- package/dist/compaction-helpers-iiKMr2TQ.js +0 -340
- package/dist/compaction-helpers-iiKMr2TQ.js.map +0 -1
- package/dist/compaction-helpers-wUz6M3us.d.ts +0 -621
- package/dist/diagnostics-CaBjfz4J.js.map +0 -1
- package/dist/durable-object-lifecycle-D6nNQJJd.js +0 -862
- package/dist/durable-object-lifecycle-D6nNQJJd.js.map +0 -1
- package/dist/experimental/memory/session/index.d.ts +0 -671
- package/dist/experimental/memory/session/index.js +0 -2379
- package/dist/experimental/memory/session/index.js.map +0 -1
- package/dist/experimental/memory/utils/index.d.ts +0 -96
- package/dist/experimental/memory/utils/index.js +0 -79
- package/dist/experimental/memory/utils/index.js.map +0 -1
- package/dist/scheduler-CR9RHGos.js +0 -857
- package/dist/scheduler-CR9RHGos.js.map +0 -1
- package/dist/src-5W6JNKVb.js.map +0 -1
- package/dist/tool-output-truncation-CNnnGZQ3.js +0 -98
- package/dist/tool-output-truncation-CNnnGZQ3.js.map +0 -1
package/docs/sessions.md
CHANGED
|
@@ -1,1024 +1,390 @@
|
|
|
1
|
-
# Sessions
|
|
1
|
+
# Sessions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Experimental.** Everything exported from `agents/sessions` may change between releases while the API stabilizes.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`agents/sessions` stores durable conversation history in a [Lifecycle Object](./lifecycle.md). It provides tree-structured messages, streamed and byte-budgeted reads, compaction overlays, optional full-text search, and lossless payload storage.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Sessions stores messages. It is not a file store.** Media you attach to a message is kept out of the message row and stored separately, addressed by content, and put back verbatim when you read. That keeps a message row small however large its attachments are — but it does not reclaim database space, because the payload lives in the same Durable Object. A Durable Object's 10 GB ceiling is therefore the real bound on how much media one conversation can hold: roughly 39,000 200 KB images, measured. An application that handles files should keep them in a file store and put a reference in the message. Think does exactly that with its [Workspace](../think/index.md), which spills to R2.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
import { Agent } from "agents";
|
|
11
|
-
import { Session } from "agents/experimental/memory/session";
|
|
9
|
+
Prompt assembly lives in [`agents/context`](./context.md) and composes with a session handle rather than living inside it.
|
|
12
10
|
|
|
13
|
-
|
|
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
|
|
11
|
+
`Think` and `AIChatAgent` use this capability for message persistence. You can also install it on a plain Durable Object.
|
|
35
12
|
|
|
36
|
-
|
|
13
|
+
## Install the capability
|
|
37
14
|
|
|
38
|
-
|
|
15
|
+
```ts
|
|
16
|
+
import { DurableObject } from "cloudflare:workers";
|
|
17
|
+
import { Lifecycle } from "agents/lifecycle";
|
|
18
|
+
import { Sessions } from "agents/sessions";
|
|
39
19
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
});
|
|
20
|
+
export class ConversationObject extends DurableObject<Env> {
|
|
21
|
+
readonly sessions = new Sessions();
|
|
22
|
+
readonly lifecycle = Lifecycle.install(this).use(this.sessions);
|
|
23
|
+
readonly session = this.sessions.session();
|
|
24
|
+
}
|
|
72
25
|
```
|
|
73
26
|
|
|
74
|
-
|
|
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);
|
|
27
|
+
On an `Agent`, install it in the constructor:
|
|
96
28
|
|
|
97
|
-
|
|
98
|
-
|
|
29
|
+
```ts
|
|
30
|
+
import { Agent, type AgentContext } from "agents";
|
|
31
|
+
import { Sessions } from "agents/sessions";
|
|
99
32
|
|
|
100
|
-
|
|
101
|
-
|
|
33
|
+
export class ConversationAgent extends Agent<Env> {
|
|
34
|
+
readonly sessions = new Sessions();
|
|
35
|
+
readonly session = this.sessions.session();
|
|
102
36
|
|
|
103
|
-
|
|
104
|
-
|
|
37
|
+
constructor(ctx: AgentContext, env: Env) {
|
|
38
|
+
super(ctx, env);
|
|
39
|
+
this.lifecycle.use(this.sessions);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
105
42
|
```
|
|
106
43
|
|
|
107
|
-
|
|
44
|
+
The default session ID is an empty string. This is the primary path when one Durable Object owns one conversation.
|
|
108
45
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
// Linear history from root to the latest leaf
|
|
113
|
-
const messages = await session.getHistory();
|
|
46
|
+
Sessions needs no alarm, so it also works on facets, which have isolated SQLite but no independent alarm slot.
|
|
114
47
|
|
|
115
|
-
|
|
116
|
-
const branch = await session.getHistory(leafId);
|
|
48
|
+
## Write messages
|
|
117
49
|
|
|
118
|
-
|
|
119
|
-
const msg = await session.getMessage("msg-1");
|
|
50
|
+
`UIMessage` from the AI SDK is structurally compatible with `SessionMessage`.
|
|
120
51
|
|
|
121
|
-
|
|
122
|
-
const
|
|
52
|
+
```ts
|
|
53
|
+
const result = await this.session.appendMessage({
|
|
54
|
+
id: crypto.randomUUID(),
|
|
55
|
+
role: "user",
|
|
56
|
+
parts: [{ type: "text", text: "Hello" }]
|
|
57
|
+
});
|
|
123
58
|
|
|
124
|
-
//
|
|
125
|
-
|
|
59
|
+
result.inserted; // false when this ID already existed
|
|
60
|
+
result.message; // exact stored form
|
|
126
61
|
```
|
|
127
62
|
|
|
128
|
-
|
|
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:
|
|
63
|
+
An append without `parentId` attaches to the active leaf. Pass `null` to create a root or pass a message ID to create a branch:
|
|
131
64
|
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
|
|
65
|
+
```ts
|
|
66
|
+
await this.session.appendMessage(alternativeReply, {
|
|
67
|
+
parentId: userMessage.id
|
|
68
|
+
});
|
|
135
69
|
```
|
|
136
70
|
|
|
137
|
-
|
|
71
|
+
Every write runs the same pipeline: sanitize provider metadata, strip reserved metadata on client input, then commit the row and any continuation rows in one synchronous SQLite transaction.
|
|
138
72
|
|
|
139
|
-
|
|
73
|
+
Mark untrusted input with `source: "client"`:
|
|
140
74
|
|
|
141
|
-
|
|
75
|
+
```ts
|
|
76
|
+
const sessions = new Sessions({
|
|
77
|
+
reservedMetadataKeys: ["channel", "turnMetadata"]
|
|
78
|
+
});
|
|
142
79
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
80
|
+
await sessions.session().appendMessage(clientMessage, {
|
|
81
|
+
source: "client"
|
|
82
|
+
});
|
|
146
83
|
```
|
|
147
84
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
> **Note:** `search()` throws if the session provider does not support search. The built-in `AgentSessionProvider` and `PostgresSessionProvider` support it.
|
|
85
|
+
This strips reserved metadata keys. It does not limit message size — see [There is no upper bound on a message](#there-is-no-upper-bound-on-a-message).
|
|
151
86
|
|
|
152
|
-
|
|
87
|
+
Other writes:
|
|
153
88
|
|
|
154
|
-
|
|
89
|
+
```ts
|
|
90
|
+
await session.updateMessage(message); // SessionMessage | null
|
|
91
|
+
await session.upsertMessage(message);
|
|
92
|
+
await session.deleteMessages([messageId]);
|
|
93
|
+
await session.clearMessages();
|
|
94
|
+
```
|
|
155
95
|
|
|
156
|
-
|
|
157
|
-
- **`CF_AGENT_SESSION_ERROR`** — emitted on compaction failure
|
|
96
|
+
`updateMessage()` returns the stored form of the message, or `null` when the ID is not in this session. It does not throw for an absent row. An unchanged message writes nothing and dispatches no change event.
|
|
158
97
|
|
|
159
|
-
|
|
98
|
+
Deleting a message splices its children to its parent. Removing a message in the middle of a chain does not make older history unreachable.
|
|
160
99
|
|
|
161
|
-
|
|
100
|
+
### Row budget
|
|
162
101
|
|
|
163
|
-
|
|
102
|
+
Sessions stores messages. One SQLite row holds up to 1.5 MiB of serialized JSON, which is more than the overwhelming majority of messages need: they occupy exactly one row, and cost exactly one billed row write.
|
|
164
103
|
|
|
165
|
-
|
|
104
|
+
A message that does not fit is split across continuation rows and reassembled on read. Nothing is truncated, nothing is summarized, and no message is too large to store — a 5 MB message is one message row plus three continuation rows, and reads back byte for byte. There is no error to catch and nothing to configure.
|
|
166
105
|
|
|
167
|
-
|
|
106
|
+
Slices are cut on UTF-8 **byte** boundaries, never in the middle of a surrogate pair, so a message full of emoji or CJK text round-trips exactly like an ASCII one.
|
|
168
107
|
|
|
169
|
-
There
|
|
108
|
+
### There is no upper bound on a message
|
|
170
109
|
|
|
171
|
-
|
|
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` |
|
|
110
|
+
This is a deliberate position, not an omission: Sessions imposes no maximum message size. A message is split across as many continuation rows as it needs, so a single very large write can consume a meaningful share of the Durable Object's 10 GB.
|
|
177
111
|
|
|
178
|
-
|
|
112
|
+
Bounding the size of untrusted input is the application's job. In particular, `appendMessage(message, { source: "client" })` sanitizes provider metadata and strips reserved metadata keys — it does **not** limit size. If clients can write to a session, check the size of what they send before you append it.
|
|
179
113
|
|
|
180
|
-
|
|
114
|
+
An application that handles files should keep them in a file store (R2, or the Workspace) and put a reference in the message.
|
|
181
115
|
|
|
182
|
-
|
|
116
|
+
## Read history
|
|
183
117
|
|
|
184
|
-
|
|
185
|
-
import { AgentContextProvider } from "agents/experimental/memory/session";
|
|
118
|
+
Prefer streamed reads for unbounded history:
|
|
186
119
|
|
|
187
|
-
|
|
188
|
-
|
|
120
|
+
```ts
|
|
121
|
+
for await (const message of session.history()) {
|
|
122
|
+
await consume(message);
|
|
123
|
+
}
|
|
189
124
|
```
|
|
190
125
|
|
|
191
|
-
|
|
126
|
+
The capability first reads a content-free path of IDs and row sizes. It then fetches content in queries bounded to 50 rows and 4 MiB. Earlier chunks are not retained by the iterator.
|
|
192
127
|
|
|
193
|
-
|
|
194
|
-
import { R2SkillProvider } from "agents/experimental/memory/session";
|
|
128
|
+
Read from the leaf when you are looking for something recent. The newest content window is fetched first, so breaking out of the loop leaves every older row unread:
|
|
195
129
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
130
|
+
```ts
|
|
131
|
+
for await (const message of session.history({ newestFirst: true })) {
|
|
132
|
+
if (ownsToolCall(message, toolCallId)) {
|
|
133
|
+
await session.updateMessage(applyResult(message));
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
199
137
|
```
|
|
200
138
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
**`AgentSearchProvider`** — SQLite FTS5 searchable context. Entries are indexed and searchable by the model via `search_context`.
|
|
139
|
+
A newest-first read follows parent pointers from the leaf, one row per message the loop actually takes, so its cost has no transcript term. Compaction overlays are honored: the walk stays row-by-row until it reaches the end of a compacted span, and only then plans the overlays over the remaining prefix and streams it leaf-first in eight-row windows.
|
|
204
140
|
|
|
205
|
-
|
|
206
|
-
import { AgentSearchProvider } from "agents/experimental/memory/session";
|
|
141
|
+
Use `historyBatches()` when each downstream operation has a fixed cost:
|
|
207
142
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
143
|
+
```ts
|
|
144
|
+
for await (const batch of session.historyBatches({
|
|
145
|
+
batchSize: 25,
|
|
146
|
+
maxBatchBytes: 2 * 1024 * 1024
|
|
147
|
+
})) {
|
|
148
|
+
await sendBatch(batch);
|
|
149
|
+
}
|
|
212
150
|
```
|
|
213
151
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
Blocks can be added and removed dynamically after initialization — useful for extensions:
|
|
152
|
+
Use a byte-budgeted recent window during Durable Object startup:
|
|
217
153
|
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
await session.addContext("extension-notes", {
|
|
221
|
-
description: "From extension X",
|
|
222
|
-
maxTokens: 500
|
|
223
|
-
});
|
|
154
|
+
```ts
|
|
155
|
+
const recent = await session.getRecentHistory(8 * 1024 * 1024, 4);
|
|
224
156
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
// Rebuild the system prompt to reflect changes
|
|
229
|
-
await session.refreshSystemPrompt();
|
|
157
|
+
recent.messages;
|
|
158
|
+
recent.truncated;
|
|
159
|
+
recent.totalContentBytes;
|
|
230
160
|
```
|
|
231
161
|
|
|
232
|
-
|
|
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
|
-
```
|
|
162
|
+
The second argument is a minimum number of recent messages. The minimum can exceed the byte budget, so choose it deliberately.
|
|
244
163
|
|
|
245
|
-
|
|
164
|
+
The budget counts what hydration actually costs. Each row is charged its full stored size — the message row plus every continuation row it was split across — so a 12 MB message is charged 12 MB, not the 1.5 MiB its first slice occupies. That is what makes the budget a bound on isolate memory.
|
|
246
165
|
|
|
247
|
-
|
|
248
|
-
// Replace content entirely
|
|
249
|
-
await session.replaceContextBlock("memory", "User likes coffee.");
|
|
166
|
+
`getHistory()` materializes the selected path and exists for consumers that require an array:
|
|
250
167
|
|
|
251
|
-
|
|
252
|
-
await session.
|
|
168
|
+
```ts
|
|
169
|
+
const messages = await session.getHistory();
|
|
253
170
|
```
|
|
254
171
|
|
|
255
|
-
|
|
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
|
-
```
|
|
172
|
+
Do not use `getHistory()` for an unbounded transcript inside a memory-constrained Durable Object. Prefer `history()`, `historyBatches()`, or `getRecentHistory()`.
|
|
273
173
|
|
|
274
|
-
|
|
275
|
-
// Freeze — first call renders and persists, subsequent calls return the cached value
|
|
276
|
-
const prompt = await session.freezeSystemPrompt();
|
|
174
|
+
Other reads:
|
|
277
175
|
|
|
278
|
-
|
|
279
|
-
|
|
176
|
+
```ts
|
|
177
|
+
await session.getMessage(id);
|
|
178
|
+
await session.getLatestLeaf();
|
|
179
|
+
await session.getBranches(parentId);
|
|
180
|
+
await session.getHistoryRowStats();
|
|
280
181
|
```
|
|
281
182
|
|
|
282
|
-
|
|
183
|
+
`getHistoryRowStats()` returns per-row stored bytes (row, continuation rows, and attachments) and stamped token estimates, without loading message content.
|
|
283
184
|
|
|
284
|
-
|
|
185
|
+
## Branches
|
|
285
186
|
|
|
286
|
-
|
|
187
|
+
A message can have multiple children. Read one root-to-leaf path by selecting its leaf:
|
|
287
188
|
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
await session.
|
|
291
|
-
|
|
292
|
-
// Check what's currently loaded
|
|
293
|
-
const loaded = await session.getLoadedSkillKeys(); // Set<"skills:api-reference">
|
|
189
|
+
```ts
|
|
190
|
+
const branch = await session.getHistory({ leafId: alternativeReply.id });
|
|
191
|
+
const alternatives = await session.getBranches(userMessage.id);
|
|
294
192
|
```
|
|
295
193
|
|
|
296
|
-
|
|
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.
|
|
194
|
+
To move a conversation between Durable Objects, export the source with `history()` and replay it with `importMessage()` on the destination:
|
|
305
195
|
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
196
|
+
```ts
|
|
197
|
+
let parentId: string | null = null;
|
|
198
|
+
for await (const message of source.history()) {
|
|
199
|
+
await destination.importMessage(message, {
|
|
200
|
+
parentId,
|
|
201
|
+
createdAt: message.createdAt?.getTime() ?? Date.now()
|
|
202
|
+
});
|
|
203
|
+
parentId = message.id;
|
|
204
|
+
}
|
|
310
205
|
```
|
|
311
206
|
|
|
312
|
-
|
|
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.
|
|
207
|
+
`importMessage()` writes one historical message verbatim: explicit parent and timestamp, no change-feed event. It splits over-budget messages the same way an append does.
|
|
334
208
|
|
|
335
|
-
-
|
|
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
|
-
---
|
|
209
|
+
For one-Durable-Object-per-conversation applications, keep the conversation directory in a parent or router Durable Object.
|
|
358
210
|
|
|
359
211
|
## Compaction
|
|
360
212
|
|
|
361
|
-
Compaction
|
|
213
|
+
Compaction overlays replace a range at read time without deleting the original rows:
|
|
362
214
|
|
|
363
|
-
|
|
215
|
+
```ts
|
|
216
|
+
import { createCompactFunction } from "agents/sessions";
|
|
364
217
|
|
|
365
|
-
|
|
366
|
-
import { createCompactFunction } from "agents/experimental/memory/utils/compaction-helpers";
|
|
367
|
-
|
|
368
|
-
const session = Session.create(this)
|
|
369
|
-
.withContext("memory", { maxTokens: 1100 })
|
|
218
|
+
session
|
|
370
219
|
.onCompaction(
|
|
371
220
|
createCompactFunction({
|
|
372
|
-
summarize: (prompt) =>
|
|
373
|
-
|
|
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 })
|
|
221
|
+
summarize: async (prompt) => summarize(prompt),
|
|
222
|
+
keepRecentTokens: 20_000
|
|
378
223
|
})
|
|
379
224
|
)
|
|
380
|
-
.compactAfter(
|
|
225
|
+
.compactAfter(80_000);
|
|
381
226
|
```
|
|
382
227
|
|
|
383
|
-
|
|
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
|
|
228
|
+
`createCompactFunction` takes exactly two options. `summarize` calls the model with a prompt and returns its text. `keepRecentTokens` is the token budget for the recent tail kept verbatim, defaulting to 20,000. The first three messages are kept verbatim as the head, at least the last two are kept as the tail, and the boundaries are aligned so a tool call is never separated from its result.
|
|
391
229
|
|
|
392
|
-
|
|
230
|
+
Run it explicitly:
|
|
393
231
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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();
|
|
232
|
+
```ts
|
|
233
|
+
await session.compact();
|
|
234
|
+
await session.compact(leafId); // compact a specific branch
|
|
403
235
|
```
|
|
404
236
|
|
|
405
|
-
|
|
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:
|
|
237
|
+
Or store an already-produced summary:
|
|
414
238
|
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
});
|
|
239
|
+
```ts
|
|
240
|
+
await session.addCompaction(summary, fromMessageId, toMessageId);
|
|
439
241
|
```
|
|
440
242
|
|
|
441
|
-
|
|
243
|
+
Sessions stamps each message with a token estimate when the row is written. `compactAfter()` gates on that O(1) aggregate and never reads the transcript to decide whether to compact. Auto-compaction failures are non-fatal: they log, emit `session:error`, and leave the transcript alone.
|
|
442
244
|
|
|
443
|
-
|
|
245
|
+
To trim a transcript before handing it to a model, `truncateOlderMessages` is exported from [`agents/chat`](./chat-agents.md), not from `agents/sessions`.
|
|
444
246
|
|
|
445
|
-
|
|
247
|
+
## Large messages
|
|
446
248
|
|
|
447
|
-
|
|
249
|
+
A message is stored as one JSON string. When that string exceeds the row budget it is cut into slices: slice 0 lives in the message row, the rest become numbered continuation rows in `cf_agents_session_message_chunks`. A read concatenates them back, so what you append is exactly what you read.
|
|
448
250
|
|
|
449
|
-
|
|
251
|
+
This is invisible from the outside. There is no pointer, no reconstruction mode, and no read option:
|
|
450
252
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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);
|
|
253
|
+
```ts
|
|
254
|
+
await session.appendMessage(message); // any size
|
|
255
|
+
const stored = await session.getMessage(message.id); // byte-identical
|
|
512
256
|
```
|
|
513
257
|
|
|
514
|
-
|
|
258
|
+
The common path pays nothing for it. A window of messages with no continuations issues exactly the same queries it always did; the extra query for continuation rows runs only for the ids in the window that actually have them.
|
|
515
259
|
|
|
516
|
-
|
|
260
|
+
### What it costs
|
|
517
261
|
|
|
518
|
-
|
|
519
|
-
// Append a single message
|
|
520
|
-
await manager.append(sessionId, message, parentId?);
|
|
262
|
+
Splitting is not a way to shrink the database. Continuation rows live in the same Durable Object as the message they belong to, inside the same 10 GB. Billing counts rows written, not bytes, so a 500 KB message costs the same one billed row as a tiny one, and a 2 MB message costs two.
|
|
521
263
|
|
|
522
|
-
|
|
523
|
-
await manager.upsert(sessionId, message, parentId?);
|
|
264
|
+
`SessionRowStat.bytes` and the `totalContentBytes` returned by `getRecentHistory()` report the whole message — row plus continuations — so a byte budget passed to `getRecentHistory()` bounds the memory a hydration will actually take.
|
|
524
265
|
|
|
525
|
-
|
|
526
|
-
await manager.appendAll(sessionId, messages, parentId?);
|
|
266
|
+
### Files belong in a file store
|
|
527
267
|
|
|
528
|
-
|
|
529
|
-
const history = await manager.getHistory(sessionId, leafId?);
|
|
268
|
+
Sessions is a message store, not a file store. Attaching a file is convenient and exact, but it is stored in the conversation forever and counted against the Durable Object's 10 GB. An application that handles files should write them to R2 or the Workspace and put a path or URL in the message instead.
|
|
530
269
|
|
|
531
|
-
|
|
532
|
-
const count = await manager.getMessageCount(sessionId);
|
|
270
|
+
## Attachments
|
|
533
271
|
|
|
534
|
-
|
|
535
|
-
await manager.clearMessages(sessionId);
|
|
272
|
+
A part that declares a non-text media type and carries its bytes inline — an image, an audio clip, a PDF — is stored outside the message. The part keeps its shape and its `mediaType`; only the payload is replaced, by an `attachment:sha256:<hex>` pointer. Reads put the payload back, so this is invisible from the outside:
|
|
536
273
|
|
|
537
|
-
|
|
538
|
-
await
|
|
274
|
+
```ts
|
|
275
|
+
await session.appendMessage(messageWithImage);
|
|
276
|
+
const stored = await session.getMessage(messageWithImage.id); // byte-identical
|
|
539
277
|
```
|
|
540
278
|
|
|
541
|
-
|
|
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
|
|
279
|
+
The rule is about **type, not size**. An image is stored this way whether it is 8 KB or 8 MB, and text is never stored this way at any size — long prose is split across continuation rows instead. The two mechanisms never interact: media leaves the message before the row is measured, so a message carrying a large image usually has no continuation rows at all.
|
|
553
280
|
|
|
554
|
-
|
|
555
|
-
// Add a compaction overlay
|
|
556
|
-
await manager.addCompaction(sessionId, summary, fromId, toId);
|
|
281
|
+
Identical payloads are stored once. That is a consequence of addressing bytes by their hash, which mainly means a retried write costs nothing; do not rely on it as a space optimization.
|
|
557
282
|
|
|
558
|
-
|
|
559
|
-
const compactions = await manager.getCompactions(sessionId);
|
|
283
|
+
A payload lives as long as some message references it. Deleting the last message that points at one deletes the bytes; clearing a session deletes all of them.
|
|
560
284
|
|
|
561
|
-
|
|
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"`.
|
|
285
|
+
### What it costs
|
|
572
286
|
|
|
573
|
-
|
|
287
|
+
Keeping media out of the message is not free. A 200 KB image bills four row writes — the message, one payload chunk, its metadata, and one reference — where inlining it would bill one. A 2 MiB image bills five. Text messages are unaffected and still bill exactly one row.
|
|
574
288
|
|
|
575
|
-
|
|
576
|
-
manager.addUsage(sessionId, inputTokens, outputTokens, cost);
|
|
577
|
-
// Increments input_tokens, output_tokens, and estimated_cost on the session row
|
|
578
|
-
```
|
|
289
|
+
What you get is a message row that stays a few hundred bytes however large the payload is. `SessionRowStat.bytes` still charges each message for the payloads it points at, so the byte budget you pass to `getRecentHistory()` remains a bound on the memory a hydration actually takes.
|
|
579
290
|
|
|
580
|
-
|
|
291
|
+
That budget is a hard ceiling with no message-count floor beneath it. `getRecentHistory()` returns the longest recent suffix that fits, and always at least the newest message; a window of unusually large messages is simply shorter. A floor that admitted rows regardless of size would defeat the bound it sits under, which is why there is no longer a `minRecentMessages` argument.
|
|
581
292
|
|
|
582
|
-
|
|
583
|
-
// Search across all sessions (FTS5)
|
|
584
|
-
const results = manager.search("deployment Friday", { limit: 20 });
|
|
585
|
-
// Returns: Array<{ id, role, content, createdAt }>
|
|
293
|
+
## Full-text search
|
|
586
294
|
|
|
587
|
-
|
|
588
|
-
const
|
|
295
|
+
```ts
|
|
296
|
+
const results = await session.search("deployment failed");
|
|
589
297
|
```
|
|
590
298
|
|
|
591
|
-
|
|
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.
|
|
299
|
+
The index is built by the first `search()` call on an object, with a one-time SQL-only backfill of the messages already stored, and maintained on every write from then on. An object that never searches never pays for an index: maintaining one costs an extra billed row on every append, so it exists only where something actually reads it.
|
|
615
300
|
|
|
616
|
-
|
|
301
|
+
Search indexes text parts only. File contents, reasoning, and tool payloads are not indexed automatically.
|
|
617
302
|
|
|
618
|
-
|
|
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 |
|
|
303
|
+
## Storage economics
|
|
626
304
|
|
|
627
|
-
|
|
305
|
+
On Durable Object SQLite a row write costs roughly 1000 times a row read, so the schema is built to keep one logical write to one billed row.
|
|
628
306
|
|
|
629
|
-
|
|
307
|
+
Every Sessions table is `WITHOUT ROWID` with a composite primary key and no secondary index:
|
|
630
308
|
|
|
631
|
-
|
|
|
632
|
-
|
|
|
633
|
-
| `
|
|
634
|
-
| `
|
|
635
|
-
| `
|
|
636
|
-
| `
|
|
637
|
-
| `
|
|
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 |
|
|
309
|
+
| Table | Primary key | Contents |
|
|
310
|
+
| ---------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------- |
|
|
311
|
+
| `cf_agents_session_messages` | `(session_id, id)` | `seq`, `parent_id`, `type`, `role`, JSON content, continuation count, token estimate, timestamp |
|
|
312
|
+
| `cf_agents_session_message_chunks` | `(session_id, id, idx)` | Continuation slices of a message too large for one row |
|
|
313
|
+
| `cf_agents_session_compactions` | `(session_id, id)` | Non-destructive summary ranges |
|
|
314
|
+
| `cf_agents_session_config` | `(session_id, key)` | Lifted session configuration |
|
|
315
|
+
| `cf_agents_session_fts` | virtual | FTS5 index, created by the first `search()` |
|
|
644
316
|
|
|
645
|
-
|
|
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
|
-
```
|
|
317
|
+
A text append bills exactly one row write on an object that has never searched. The `seq` column carries ordering and `type` distinguishes row kinds, so neither needs an index. A continuation row carries its slice and nothing else — no hash, no media type, no size — so an over-budget message costs one billed row per slice and nothing more.
|
|
701
318
|
|
|
702
|
-
|
|
319
|
+
State is derived rather than maintained. The active leaf is the session's max-`seq` row and token totals come from per-row stamped estimates. No counter row, registry row, or refcount is written on the append path.
|
|
703
320
|
|
|
704
|
-
##
|
|
321
|
+
## Observe changes
|
|
705
322
|
|
|
706
|
-
|
|
323
|
+
`Sessions.subscribe()` reports ordered writes in the active isolate:
|
|
707
324
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
);
|
|
325
|
+
```ts
|
|
326
|
+
const unsubscribe = sessions.subscribe(async (event) => {
|
|
327
|
+
if (event.sessionId !== "") return;
|
|
328
|
+
switch (event.type) {
|
|
329
|
+
case "append":
|
|
330
|
+
case "update":
|
|
331
|
+
await updateLocalProjection(event.message);
|
|
332
|
+
break;
|
|
333
|
+
case "delete":
|
|
334
|
+
case "clear":
|
|
335
|
+
case "compact":
|
|
336
|
+
case "compaction":
|
|
337
|
+
await refreshProjection();
|
|
338
|
+
break;
|
|
339
|
+
case "import":
|
|
340
|
+
markProjectionStale();
|
|
341
|
+
break;
|
|
837
342
|
}
|
|
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
|
-
}
|
|
343
|
+
});
|
|
870
344
|
```
|
|
871
345
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
### Search
|
|
875
|
-
|
|
876
|
-
Two levels of search are available:
|
|
346
|
+
`import` fires once per `importMessage()` and carries the row; it is deliberately not an `append`, so a projection does not patch itself per imported row during a migration — it marks itself stale and re-derives once. `compaction` fires when an overlay is stored directly through `addCompaction()`; `compact()` reports its own overlay as `compact`.
|
|
877
347
|
|
|
878
|
-
-
|
|
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.
|
|
348
|
+
This is a local cache-coherence feed, not a cross-object event log. Capability diagnostics are also emitted through Lifecycle observability.
|
|
880
349
|
|
|
881
|
-
|
|
350
|
+
## Errors
|
|
882
351
|
|
|
883
|
-
|
|
352
|
+
Sessions defines no error classes. A message that is not JSON-serializable fails with the `TypeError` that `JSON.stringify` throws, and there is no "message too large" error. A message that exceeds the row budget is split across continuation rows instead.
|
|
884
353
|
|
|
885
|
-
##
|
|
354
|
+
## Multiple sessions
|
|
886
355
|
|
|
887
|
-
|
|
356
|
+
Multiple handles can share one Durable Object:
|
|
888
357
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
|
358
|
+
```ts
|
|
359
|
+
const support = sessions.session("support");
|
|
360
|
+
const sales = sessions.session("sales");
|
|
899
361
|
```
|
|
900
362
|
|
|
901
|
-
|
|
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.
|
|
363
|
+
Handles are cached, so per-session configuration such as the compaction trigger survives repeated `session()` calls. There is no registry table: a directory of conversations belongs to the application.
|
|
1003
364
|
|
|
1004
|
-
|
|
365
|
+
For user-facing chat applications, prefer one Durable Object per conversation. This isolates storage and failure domains and allows conversations to hibernate independently. Use multiple handles inside one object for local branches, drafts, or application-specific namespaces.
|
|
1005
366
|
|
|
1006
|
-
|
|
367
|
+
## Chat hosts
|
|
1007
368
|
|
|
1008
|
-
|
|
369
|
+
`Think` and `AIChatAgent` use Sessions internally.
|
|
1009
370
|
|
|
1010
|
-
|
|
371
|
+
- Think uses branches, compaction, search, and the change feed. Its prompt context comes from [`agents/context`](./context.md). Aged media leaves the conversation through Think's own media eviction, which is a context-window technique and unrelated to how Sessions stores a row.
|
|
372
|
+
- AIChatAgent uses the default handle as a linear chain. Its existing destructive regeneration, mutable `messages` array, retention option, and wire protocol remain unchanged.
|
|
1011
373
|
|
|
1012
|
-
|
|
374
|
+
Existing `assistant_*` Session tables and `cf_ai_chat_agent_messages` are lifted automatically, then dropped once every row is verified copied.
|
|
1013
375
|
|
|
1014
|
-
|
|
376
|
+
## Memory boundaries and future streaming
|
|
1015
377
|
|
|
1016
|
-
|
|
378
|
+
The capability streams history, but some consumers still require arrays.
|
|
1017
379
|
|
|
1018
|
-
|
|
380
|
+
| Operation | Current shape | Future direction |
|
|
381
|
+
| ----------------------------------------- | --------------------------------------------- | ---------------------------------------------- |
|
|
382
|
+
| History export and indexing | Stream or bounded batches | Already streamable |
|
|
383
|
+
| Tool-call lookup and reconciliation scans | Host-specific arrays in current chat packages | Can scan batches and stop early |
|
|
384
|
+
| Model request input | AI SDK message array | Must materialize a bounded model window |
|
|
385
|
+
| Compaction summarizer input | Message array | Must materialize a bounded compaction range |
|
|
386
|
+
| Legacy full-transcript client snapshots | Message array | Can move to paginated history plus live deltas |
|
|
1019
387
|
|
|
1020
|
-
|
|
388
|
+
The chat packages retain their current public arrays and wire behavior in this release. Moving reconciliation and client history protocols to streaming requires separate protocol work.
|
|
1021
389
|
|
|
1022
|
-
|
|
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).
|
|
390
|
+
See `examples/next/sessions` for a runnable server-only example.
|