agents 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +20 -1
  2. package/dist/{agent-routing-CnkaHb-v.d.ts → agent-routing-B2XLNMxq.d.ts} +1094 -443
  3. package/dist/agent-routing.d.ts +1 -1
  4. package/dist/agent-tool-types.d.ts +26 -26
  5. package/dist/{agent-tools-C0XJqWzB.d.ts → agent-tools-Dj5Mzlw7.d.ts} +7 -7
  6. package/dist/agent-tools.d.ts +21 -21
  7. package/dist/browser/ai.js +4 -1
  8. package/dist/browser/ai.js.map +1 -1
  9. package/dist/browser/tanstack-ai.js.map +1 -1
  10. package/dist/callable-decorator-DP__HhBA.d.ts +72 -0
  11. package/dist/callable-decorator.d.ts +18 -0
  12. package/dist/callable-decorator.js +71 -0
  13. package/dist/callable-decorator.js.map +1 -0
  14. package/dist/{capability-BjSKYpzg.js → capability-B4WbF81e.js} +2 -1
  15. package/dist/capability-B4WbF81e.js.map +1 -0
  16. package/dist/capability-runner-BUBa6Ake.d.ts +484 -0
  17. package/dist/channel-Bnm4S7T2.d.ts +491 -0
  18. package/dist/channels/ai-sdk.d.ts +46 -0
  19. package/dist/channels/ai-sdk.js +120 -0
  20. package/dist/channels/ai-sdk.js.map +1 -0
  21. package/dist/channels/email.d.ts +95 -0
  22. package/dist/channels/email.js +323 -0
  23. package/dist/channels/email.js.map +1 -0
  24. package/dist/channels/index.d.ts +233 -0
  25. package/dist/channels/index.js +608 -0
  26. package/dist/channels/index.js.map +1 -0
  27. package/dist/channels/slack.d.ts +140 -0
  28. package/dist/channels/slack.js +614 -0
  29. package/dist/channels/slack.js.map +1 -0
  30. package/dist/channels/tanstack-ai.d.ts +39 -0
  31. package/dist/channels/tanstack-ai.js +17 -0
  32. package/dist/channels/tanstack-ai.js.map +1 -0
  33. package/dist/channels/telegram.d.ts +106 -0
  34. package/dist/channels/telegram.js +427 -0
  35. package/dist/channels/telegram.js.map +1 -0
  36. package/dist/channels/voice.d.ts +45 -0
  37. package/dist/channels/voice.js +122 -0
  38. package/dist/channels/voice.js.map +1 -0
  39. package/dist/chat/index.d.ts +2319 -2010
  40. package/dist/chat/index.js +892 -518
  41. package/dist/chat/index.js.map +1 -1
  42. package/dist/chat-sdk/index.d.ts +7 -7
  43. package/dist/chat-sdk/index.js +1 -1
  44. package/dist/client-BxdkCuVy.d.ts +411 -0
  45. package/dist/client-C9DAjZIY.js +518 -0
  46. package/dist/client-C9DAjZIY.js.map +1 -0
  47. package/dist/{client-jagG8a9_.js → client-fyQrLGkD.js} +2 -2
  48. package/dist/{client-jagG8a9_.js.map → client-fyQrLGkD.js.map} +1 -1
  49. package/dist/client.d.ts +27 -283
  50. package/dist/client.js +2 -274
  51. package/dist/context/index.d.ts +216 -0
  52. package/dist/context/index.js +454 -0
  53. package/dist/context/index.js.map +1 -0
  54. package/dist/current-agent-C4w86kcN.d.ts +93 -0
  55. package/dist/current-agent-DhoDkSnH.js.map +1 -1
  56. package/dist/{diagnostics-CaBjfz4J.js → diagnostics-BzvaX2UT.js} +5 -1
  57. package/dist/diagnostics-BzvaX2UT.js.map +1 -0
  58. package/dist/diagnostics-C4jcz3VK.js +360 -0
  59. package/dist/diagnostics-C4jcz3VK.js.map +1 -0
  60. package/dist/durable-object-lifecycle-ei7M7lqB.d.ts +157 -0
  61. package/dist/index-BB0kqhIz.d.ts +101 -0
  62. package/dist/index-BVVgDSdq.d.ts +1 -0
  63. package/dist/index-Dlgtd3Mj.d.ts +78 -0
  64. package/dist/index-XDkuQ7zm.d.ts +89 -0
  65. package/dist/{index-DcSAZKsB.d.ts → index-YSKgfgg9.d.ts} +3 -1
  66. package/dist/index.d.ts +92 -83
  67. package/dist/index.js +4 -3
  68. package/dist/ingress-BfetZbMO.js +83 -0
  69. package/dist/ingress-BfetZbMO.js.map +1 -0
  70. package/dist/internal-CYlgHl1l.js +59 -0
  71. package/dist/internal-CYlgHl1l.js.map +1 -0
  72. package/dist/internal_context.d.ts +1 -1
  73. package/dist/lifecycle/index.d.ts +39 -19
  74. package/dist/lifecycle/index.js +2 -2
  75. package/dist/lifecycle-Mm_jQh7r.js +1314 -0
  76. package/dist/lifecycle-Mm_jQh7r.js.map +1 -0
  77. package/dist/mcp/client/index.d.ts +20 -20
  78. package/dist/mcp/client/index.js +1 -1
  79. package/dist/mcp/index.d.ts +35 -35
  80. package/dist/mcp/index.js +2 -2
  81. package/dist/observability/index.d.ts +1 -1
  82. package/dist/observability/index.js +1 -1
  83. package/dist/queue/index.d.ts +123 -0
  84. package/dist/queue/index.js +2 -0
  85. package/dist/queue-BC9F5cUo.js +406 -0
  86. package/dist/queue-BC9F5cUo.js.map +1 -0
  87. package/dist/react.d.ts +34 -16
  88. package/dist/react.js +31 -8
  89. package/dist/react.js.map +1 -1
  90. package/dist/{retries-CAvxtG9d.d.ts → retries-D9B2UCq3.d.ts} +23 -6
  91. package/dist/retries.d.ts +10 -6
  92. package/dist/retries.js +21 -1
  93. package/dist/retries.js.map +1 -1
  94. package/dist/routing/index.d.ts +137 -0
  95. package/dist/routing/index.js +244 -0
  96. package/dist/routing/index.js.map +1 -0
  97. package/dist/sanitize-D9TujEK8.js +79 -0
  98. package/dist/sanitize-D9TujEK8.js.map +1 -0
  99. package/dist/{scheduler-DQoTGoAW.d.ts → scheduler-BGq6M5Kd.d.ts} +20 -21
  100. package/dist/scheduler-D_KHqxBa.js +658 -0
  101. package/dist/scheduler-D_KHqxBa.js.map +1 -0
  102. package/dist/schedules/index.d.ts +1 -1
  103. package/dist/schedules/index.js +1 -1
  104. package/dist/sentence-chunker-BAidJ4DA.d.ts +68 -0
  105. package/dist/serializable.d.ts +1 -1
  106. package/dist/sessions/index.d.ts +441 -0
  107. package/dist/sessions/index.js +2063 -0
  108. package/dist/sessions/index.js.map +1 -0
  109. package/dist/skills/index.d.ts +99 -0
  110. package/dist/skills/index.js +254 -5
  111. package/dist/skills/index.js.map +1 -1
  112. package/dist/{src-5W6JNKVb.js → src-BNU3ZiJM.js} +1524 -1441
  113. package/dist/src-BNU3ZiJM.js.map +1 -0
  114. package/dist/state/index.d.ts +6 -0
  115. package/dist/state/index.js +137 -0
  116. package/dist/state/index.js.map +1 -0
  117. package/dist/streams/index.d.ts +120 -0
  118. package/dist/streams/index.js +107 -0
  119. package/dist/streams/index.js.map +1 -0
  120. package/dist/streams-BcE8JP2X.d.ts +370 -0
  121. package/dist/streams-CCPRV6dt.js +709 -0
  122. package/dist/streams-CCPRV6dt.js.map +1 -0
  123. package/dist/sub-routing.d.ts +12 -12
  124. package/dist/surface-bZZJqBka.js +17 -0
  125. package/dist/surface-bZZJqBka.js.map +1 -0
  126. package/dist/tasks/index.d.ts +64 -0
  127. package/dist/tasks/index.js +2 -0
  128. package/dist/tasks-D4nLqVSI.js +1656 -0
  129. package/dist/tasks-D4nLqVSI.js.map +1 -0
  130. package/dist/tasks-V2lCT8ZV.d.ts +517 -0
  131. package/dist/text-segment-joiner-BtAFQSA_.js +57 -0
  132. package/dist/text-segment-joiner-BtAFQSA_.js.map +1 -0
  133. package/dist/text-stream-CpdiKrJB.js +272 -0
  134. package/dist/text-stream-CpdiKrJB.js.map +1 -0
  135. package/dist/tokens-nHAKcN6M.js +52 -0
  136. package/dist/tokens-nHAKcN6M.js.map +1 -0
  137. package/dist/tool-schema-CBjGPrsQ.js +31 -0
  138. package/dist/tool-schema-CBjGPrsQ.js.map +1 -0
  139. package/dist/transport-protocol-BxyngQ_R.js +25 -0
  140. package/dist/transport-protocol-BxyngQ_R.js.map +1 -0
  141. package/dist/transport-protocol-ZTew5Wso.d.ts +27 -0
  142. package/dist/types-B7LojTe4.d.ts +202 -0
  143. package/dist/types-DCD6BNZH.d.ts +70 -0
  144. package/dist/types-_Faxb570.d.ts +439 -0
  145. package/dist/voice/client.d.ts +226 -0
  146. package/dist/voice/client.js +932 -0
  147. package/dist/voice/client.js.map +1 -0
  148. package/dist/voice/errors.d.ts +43 -0
  149. package/dist/voice/errors.js +41 -0
  150. package/dist/voice/errors.js.map +1 -0
  151. package/dist/voice/index.d.ts +271 -0
  152. package/dist/voice/index.js +1812 -0
  153. package/dist/voice/index.js.map +1 -0
  154. package/dist/voice/react.d.ts +167 -0
  155. package/dist/voice/react.js +234 -0
  156. package/dist/voice/react.js.map +1 -0
  157. package/dist/voice/sfu.d.ts +71 -0
  158. package/dist/voice/sfu.js +157 -0
  159. package/dist/voice/sfu.js.map +1 -0
  160. package/dist/voice/text.d.ts +6 -0
  161. package/dist/voice/text.js +2 -0
  162. package/dist/voice/types.d.ts +58 -0
  163. package/dist/voice/types.js +18 -0
  164. package/dist/voice/types.js.map +1 -0
  165. package/dist/voice/workers-ai.d.ts +136 -0
  166. package/dist/voice/workers-ai.js +568 -0
  167. package/dist/voice/workers-ai.js.map +1 -0
  168. package/dist/websockets/index.d.ts +301 -0
  169. package/dist/websockets/index.js +3 -0
  170. package/dist/websockets-D7IzWbZb.js +1005 -0
  171. package/dist/websockets-D7IzWbZb.js.map +1 -0
  172. package/dist/workflow-types.d.ts +25 -25
  173. package/dist/workflows.d.ts +21 -21
  174. package/dist/workflows.js +1 -1
  175. package/docs/agent-class.md +4 -4
  176. package/docs/agent-tools.md +2 -1
  177. package/docs/channels.md +323 -0
  178. package/docs/chat-agents.md +6 -13
  179. package/docs/client-sdk.md +30 -0
  180. package/docs/context.md +131 -0
  181. package/docs/index.md +16 -13
  182. package/docs/lifecycle.md +179 -57
  183. package/docs/long-running-agents.md +2 -2
  184. package/docs/mcp-servers.md +5 -1
  185. package/docs/queue.md +135 -241
  186. package/docs/resumable-streaming.md +1 -1
  187. package/docs/retries.md +1 -1
  188. package/docs/routing.md +105 -0
  189. package/docs/sessions.md +237 -871
  190. package/docs/streams.md +213 -0
  191. package/docs/sub-agents.md +184 -124
  192. package/docs/tasks.md +246 -0
  193. package/docs/voice.md +745 -0
  194. package/package.json +126 -14
  195. package/dist/capability-BjSKYpzg.js.map +0 -1
  196. package/dist/capability-runner-CvHGZqUu.d.ts +0 -150
  197. package/dist/client.js.map +0 -1
  198. package/dist/compaction-helpers-iiKMr2TQ.js +0 -340
  199. package/dist/compaction-helpers-iiKMr2TQ.js.map +0 -1
  200. package/dist/compaction-helpers-wUz6M3us.d.ts +0 -621
  201. package/dist/current-agent-CuMErtly.d.ts +0 -283
  202. package/dist/diagnostics-CaBjfz4J.js.map +0 -1
  203. package/dist/durable-object-lifecycle-D6nNQJJd.js +0 -862
  204. package/dist/durable-object-lifecycle-D6nNQJJd.js.map +0 -1
  205. package/dist/experimental/memory/session/index.d.ts +0 -671
  206. package/dist/experimental/memory/session/index.js +0 -2379
  207. package/dist/experimental/memory/session/index.js.map +0 -1
  208. package/dist/experimental/memory/utils/index.d.ts +0 -96
  209. package/dist/experimental/memory/utils/index.js +0 -79
  210. package/dist/experimental/memory/utils/index.js.map +0 -1
  211. package/dist/scheduler-CR9RHGos.js +0 -857
  212. package/dist/scheduler-CR9RHGos.js.map +0 -1
  213. package/dist/src-5W6JNKVb.js.map +0 -1
  214. package/dist/tool-output-truncation-CNnnGZQ3.js +0 -98
  215. package/dist/tool-output-truncation-CNnnGZQ3.js.map +0 -1
@@ -520,13 +520,9 @@ their cleanup and skipped continuations return immediately.
520
520
 
521
521
  #### Overriding the clear handler
522
522
 
523
- The SDK's built-in `CF_AGENT_CHAT_CLEAR` handler calls `resetTurnState()`
524
- automatically. If your `onMessage` override intercepts `CF_AGENT_CHAT_CLEAR`
525
- and returns before the SDK sees the message — for example, to scope the delete
526
- to a specific workflow — the built-in handler never runs. The active stream
527
- continues and queued continuations persist into the newly-cleared conversation.
523
+ The SDK's built-in `CF_AGENT_CHAT_CLEAR` handler calls `resetTurnState()` and clears the default Sessions handle. If an `onMessage` override intercepts this frame and returns early, the built-in handler cannot stop the active stream or clear history.
528
524
 
529
- Call `this.resetTurnState()` before performing your scoped delete:
525
+ Perform authorization or logging, then pass the frame to the original handler:
530
526
 
531
527
  ```typescript
532
528
  import { MessageType } from "@cloudflare/ai-chat/types";
@@ -536,19 +532,16 @@ this.onMessage = async (connection, message) => {
536
532
  if (typeof message === "string") {
537
533
  const data = JSON.parse(message);
538
534
  if (data.type === MessageType.CF_AGENT_CHAT_CLEAR) {
539
- this.resetTurnState();
540
- this.sql`
541
- DELETE FROM cf_ai_chat_agent_messages
542
- WHERE workflow_id = ${this.workflowId}
543
- `;
544
- await this.saveMessages([]);
545
- return;
535
+ await this.authorizeClear(connection);
536
+ console.log("clearing conversation");
546
537
  }
547
538
  }
548
539
  return _onMessage(connection, message);
549
540
  };
550
541
  ```
551
542
 
543
+ Do not write directly to Sessions tables. Use `this.sessions.session().clearMessages()` when implementing a separate server-side history operation outside the chat protocol.
544
+
552
545
  ### Lifecycle Hooks
553
546
 
554
547
  Override `onConnect` and `onClose` to add custom logic. Stream resumption and message sync are handled for you automatically — you do not need to call `super`:
@@ -120,6 +120,35 @@ useAgent({
120
120
  });
121
121
  ```
122
122
 
123
+ ### Transport
124
+
125
+ `useAgent` speaks the Agent protocol over one of two wires. The default,
126
+ `"cf-websocket"`, is a hibernating WebSocket managed by PartySocket. The
127
+ experimental `"capnweb"` transport carries protocol frames through a single
128
+ [Cap'n Web](https://github.com/cloudflare/capnweb) RPC session whose root
129
+ also serves the host's callables natively:
130
+
131
+ ```typescript
132
+ useAgent({
133
+ agent: "ChatAgent",
134
+ name: "room-123",
135
+ transport: "capnweb" // default: "cf-websocket"
136
+ });
137
+ ```
138
+
139
+ Identity, state sync, reconnection with backoff, and terminal close codes are
140
+ identical on both wires; PartySocket still owns the connection and the
141
+ transport only swaps the socket class it instantiates. `call()` and `stub`
142
+ differ by design. On `cf-websocket` they send JSON `rpc` frames. On `capnweb`
143
+ they invoke the host's `callables` natively on the Cap'n Web session root: a
144
+ method that returns an `RpcTarget` hands you a live stub you can keep
145
+ calling, a `ReadableStream` result streams, and chained calls pipeline.
146
+ `@callable()` decorators on an `Agent` are a JSON-wire feature and are not
147
+ available on `capnweb` unless the Agent also passes a `callables` target.
148
+ The trade-off is that a Cap'n Web connection does not hibernate: the Durable
149
+ Object stays in memory while one is open. `AgentClient` accepts the same
150
+ `transport` option.
151
+
123
152
  ### Async Query Parameters
124
153
 
125
154
  For authentication tokens or other async data, pass a function that returns a Promise:
@@ -351,6 +380,7 @@ type UseAgentOptions<State> = {
351
380
  name?: string; // Instance name (default: "default")
352
381
  host?: string; // Custom host
353
382
  path?: string; // Custom path prefix
383
+ transport?: "cf-websocket" | "capnweb"; // Wire (default: "cf-websocket")
354
384
 
355
385
  // Query parameters
356
386
  query?:
@@ -0,0 +1,131 @@
1
+ # Context
2
+
3
+ > **Experimental.** Everything exported from `agents/context` may change between releases while the API stabilizes.
4
+
5
+ `agents/context` assembles an agent's system prompt from labelled blocks. A block is a piece of prompt text with a storage provider behind it. What the provider can do decides how the block behaves and which tools the model gets for it.
6
+
7
+ Context is prompt assembly. It is not conversation storage. It composes with [`agents/sessions`](./sessions.md) rather than living inside it, so an agent can have a prompt without a transcript, or a transcript without a prompt.
8
+
9
+ ## Blocks
10
+
11
+ ```ts
12
+ import { ContextBlocks } from "agents/context";
13
+
14
+ const context = new ContextBlocks([
15
+ {
16
+ label: "soul",
17
+ provider: { get: async () => "You are a helpful assistant." }
18
+ },
19
+ {
20
+ label: "memory",
21
+ description: "Facts learned about the user",
22
+ maxTokens: 1_100,
23
+ provider: memoryProvider
24
+ }
25
+ ]);
26
+
27
+ const system = await context.freezeSystemPrompt();
28
+ const tools = await context.tools();
29
+ ```
30
+
31
+ Each block renders as a labelled section of the system prompt. The header carries the label, the description, a token-usage percentage when `maxTokens` is set, and a capability marker (`[readonly]`, `[writable]`, `[loadable]`, or `[searchable]`).
32
+
33
+ An empty read-only block is skipped. Writable, loadable, and searchable blocks always render so the model knows which tools can address them.
34
+
35
+ ## Providers
36
+
37
+ The provider decides the block's behavior. The checks are structural, not nominal.
38
+
39
+ | Provider shape | Block behavior |
40
+ | ----------------------- | -------------------------------------------- |
41
+ | `get()` | Read-only text in the prompt |
42
+ | `get()` + `set()` | Writable through the `set_context` tool |
43
+ | `get()` + `search(key)` | Summary in the prompt, `search_context` tool |
44
+
45
+ `get()` returns the block's current content, or `null` when it has none. An optional `init(label)` receives the block label before first use, so one provider class can serve several labels.
46
+
47
+ `ContextBlocks` also accepts a `defaultProvider` factory. A block declared without a `provider` is then wired to whatever that factory returns for its label, which is how a host offers durable writable blocks by label alone.
48
+
49
+ ### Durable SQLite blocks
50
+
51
+ `AgentContextProvider` stores one block per row in `cf_agents_context_blocks` in the Durable Object's own SQLite database:
52
+
53
+ ```ts
54
+ import { AgentContextProvider } from "agents/context";
55
+
56
+ const context = new ContextBlocks([
57
+ { label: "memory", provider: new AgentContextProvider(this, "memory") }
58
+ ]);
59
+ ```
60
+
61
+ The constructor takes anything with a tagged-template `sql` method, which an `Agent` already has. The label argument is optional: `init()` fills it in from the block declaration.
62
+
63
+ ### Searchable blocks
64
+
65
+ `AgentSearchProvider` backs a block with a Durable Object FTS5 table:
66
+
67
+ ```ts
68
+ import { AgentSearchProvider } from "agents/context";
69
+
70
+ const context = new ContextBlocks([
71
+ { label: "knowledge", provider: new AgentSearchProvider(this) }
72
+ ]);
73
+ ```
74
+
75
+ `get()` renders a count of indexed entries rather than the entries themselves. `search(query)` returns up to 10 ranked matches through the `search_context` tool. `set(key, content)` replaces one keyed entry.
76
+
77
+ The FTS5 table is the only store for these entries. A mirror row table would double the billed writes of every indexed entry to serve a count and a lookup the index already answers. Entries live in `cf_agents_search_fts`, namespaced by label, separate from the Sessions message index.
78
+
79
+ ## Frozen prompts
80
+
81
+ `freezeSystemPrompt()` renders once and returns the same string on every later call, so the provider's prefix cache stays warm across turns. `setBlock()` writes to the provider immediately but deliberately does not change the frozen prompt; call `refreshSystemPrompt()` to re-render from current block state.
82
+
83
+ Pass a `promptStore` (any writable provider) as the second constructor argument and the frozen prompt is persisted:
84
+
85
+ ```ts
86
+ const context = new ContextBlocks(
87
+ configs,
88
+ new AgentContextProvider(this, "_system_prompt"),
89
+ (label) => new AgentContextProvider(this, label)
90
+ );
91
+
92
+ const system = await context.freezeSystemPrompt();
93
+ ```
94
+
95
+ `freezeSystemPrompt()` returns the stored prompt when one exists, and otherwise loads providers, renders, and persists. So a cold wake reuses the exact prompt string the model already cached instead of re-rendering a subtly different one.
96
+
97
+ `refreshSystemPrompt()` reloads every provider, re-renders, and overwrites the stored prompt.
98
+
99
+ ## Tools
100
+
101
+ `tools()` returns an AI SDK `ToolSet` wired from what the blocks can do:
102
+
103
+ - `set_context` when any block is writable
104
+ - `search_context` when any block is backed by a search provider
105
+
106
+ An agent with only read-only blocks gets no tools at all.
107
+
108
+ ## Think
109
+
110
+ `Think` builds its `ContextBlocks` from `configureContext()` during startup:
111
+
112
+ ```ts
113
+ import type { ContextConfig } from "agents/context";
114
+
115
+ class MyAgent extends Think<Env> {
116
+ configureContext(): ContextConfig[] {
117
+ return [
118
+ { label: "soul", provider: { get: async () => "You are helpful." } },
119
+ { label: "memory", description: "Learned facts", maxTokens: 2_000 }
120
+ ];
121
+ }
122
+ }
123
+ ```
124
+
125
+ A block declared without a provider is auto-wired to durable per-agent SQLite. The frozen system prompt is always persisted, in `_system_prompt`, so there is nothing to opt into.
126
+
127
+ The assembled blocks are available as `this.context` after `onStart()`.
128
+
129
+ ## Related
130
+
131
+ - [Sessions](./sessions.md) - durable message trees, streamed history, compaction, and attachment offload
package/docs/index.md CHANGED
@@ -11,13 +11,13 @@ Build stateful AI agents on Cloudflare Workers. Every agent is a Durable Object
11
11
 
12
12
  Pick the base class that matches what you are building. They share the same Durable Object foundation, so you can start small and move up without re-platforming.
13
13
 
14
- | You are building... | Use | Why |
15
- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
16
- | Stateful backend logic, real-time sync, custom protocols | [`Agent`](./agent-class.md) | The core class: state, WebSockets, scheduling, SQL, and sub-agents. No opinions about chat or LLMs. |
17
- | A chat UI where you own the loop, the stream, and the response | [`AIChatAgent`](./chat-agents.md) | A thin chat-protocol adapter for `useAgentChat`. Bring your own agentic loop and custom streaming. |
18
- | A durable, general-purpose reasoning agent | [`Think`](https://github.com/cloudflare/agents/blob/main/docs/think/index.md) | Opinionated runtime: agentic loop, sessions, tools, memory, compaction, recovery, and multi-channel delivery built in. |
19
- | A voice agent (speech in, speech out) | [Voice mixins](https://github.com/cloudflare/agents/blob/main/docs/voice/index.md) | `withVoice` adds real-time STT/TTS, interruption and barge-in, and conversation persistence to an agent. |
20
- | Durable multi-step processes (not chat) | [Workflows](./workflows.md) | Long-running, retryable step orchestration with Cloudflare Workflows. |
14
+ | You are building... | Use | Why |
15
+ | -------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
16
+ | Stateful backend logic, real-time sync, custom protocols | [`Agent`](./agent-class.md) | The core class: state, WebSockets, scheduling, SQL, and sub-agents. No opinions about chat or LLMs. |
17
+ | A chat UI where you own the loop, the stream, and the response | [`AIChatAgent`](./chat-agents.md) | A thin chat-protocol adapter for `useAgentChat`. Bring your own agentic loop and custom streaming. |
18
+ | A durable, general-purpose reasoning agent | [`Think`](https://github.com/cloudflare/agents/blob/main/docs/think/index.md) | Opinionated runtime: agentic loop, sessions, tools, memory, compaction, recovery, and multi-channel delivery built in. |
19
+ | A voice agent (speech in, speech out) | [Voice mixins](./voice.md) | `withVoice` adds real-time STT/TTS, interruption and barge-in, and conversation persistence to an agent. |
20
+ | Durable multi-step processes (not chat) | [Workflows](./workflows.md) | Long-running, retryable step orchestration with Cloudflare Workflows. |
21
21
 
22
22
  Not sure? Start with [`Agent`](./agent-class.md) for raw building blocks, or [`Think`](https://github.com/cloudflare/agents/blob/main/docs/think/index.md) if you want a chat or reasoning agent that already handles the hard parts.
23
23
 
@@ -40,8 +40,8 @@ The differentiator is not "we have durable state" — it is what happens when a
40
40
  ## Core Concepts
41
41
 
42
42
  - [State Management](./state.md) - Managing agent state with `setState()`, `initialState`, and `onStateChanged()`
43
- - [Routing](./routing.md) - How `routeAgentRequest()` and agent naming works
44
- - [Sub-agents](./sub-agents.md) - Parent/child DO composition via facets, nested routing, and direct child connections
43
+ - [Routing](./routing.md) - How `routeAgentRequest()` and agent naming works, plus `RoutedAgents` for a hub that routes to many independent Agents
44
+ - [Dynamic agents](./sub-agents.md) - Facet-backed child agents for code the parent supervises (dynamic/generated code, per-run tool agents, sandboxes) — not the recommended primitive for many independent peers like chats
45
45
  - [HTTP & WebSockets](./http-websockets.md) - Request handling and real-time connections
46
46
  - [Callable Methods](./callable-methods.md) - The `@callable` decorator and client-server method calls
47
47
  - [Readonly Connections](./readonly-connections.md) - Restricting which connections can modify state
@@ -57,13 +57,13 @@ The differentiator is not "we have durable state" — it is what happens when a
57
57
  - [Webhooks](./webhooks.md) - Receiving and sending webhook events
58
58
  - [Push Notifications](./push-notifications.md) - Browser push notifications via Web Push API and scheduled delivery
59
59
  - TODO: [SMS](./sms.md) - Text message integration (Twilio, etc.)
60
- - [Voice Agents](https://github.com/cloudflare/agents/blob/main/docs/voice/index.md) - Build voice agents with real-time speech-to-text, text-to-speech, and conversation persistence
60
+ - [Voice Agents](./voice.md) - Build voice agents with real-time speech-to-text, text-to-speech, and conversation persistence
61
+ - [Channels](./channels.md) - Send and receive messages through Slack, Telegram, email, browser voice, or custom adapters
61
62
  - [Chat SDK State](./chat-sdk.md) - Store Chat SDK subscriptions, locks, queues, and history in Agents sub-agents
62
- - TODO: [Messengers](./messengers.md) - Slack, Discord, Telegram, and other chat platforms
63
63
 
64
64
  ## Background Processing
65
65
 
66
- - [Queue](./queue.md) - Immediate background task execution
66
+ - [Queue](./queue.md) - Durable background task execution
67
67
  - [Scheduling](./scheduling.md) - Delayed, scheduled, and cron-based tasks
68
68
  - [Retries](./retries.md) - Automatic retries with exponential backoff and jitter
69
69
  - [Durable Execution](./durable-execution.md) - `runFiber()`, `startFiber()`, `stash()`, and crash recovery for long tasks
@@ -80,7 +80,8 @@ The differentiator is not "we have durable state" — it is what happens when a
80
80
  - [Server-Driven Messages](./server-driven-messages.md) - Autonomous agent workflows: scheduled follow-ups, queue processing, webhooks, chained reasoning
81
81
  - TODO: [Using AI Models](./using-ai-models.md) - OpenAI, Anthropic, Workers AI, and other providers
82
82
  - TODO: [RAG (Retrieval Augmented Generation)](./rag.md) - Vector search with Vectorize
83
- - [Sessions (Experimental)](./sessions.md) - Persistent conversation storage with tree-structured messages, context blocks, compaction, and search
83
+ - [Sessions (Experimental)](./sessions.md) - Durable message trees, streamed history, compaction, search, and lossless attachment offload
84
+ - [Context (Experimental)](./context.md) - System-prompt blocks, frozen prompts, writable/searchable/loadable providers, and their tools
84
85
  - [Workspace (Experimental)](https://github.com/cloudflare/agents/blob/main/docs/shell/index.md) - Durable virtual filesystem backed by SQLite + R2
85
86
  - [Codemode (Experimental)](https://github.com/cloudflare/agents/blob/main/docs/agents/codemode.md) - LLM-generated executable code for tool orchestration
86
87
  - [Client Tools Continuation](./client-tools-continuation.md) - Handling tool calls across client/server
@@ -141,6 +142,8 @@ The differentiator is not "we have durable state" — it is what happens when a
141
142
  ## Reference
142
143
 
143
144
  - [Durable Object Lifecycle](./lifecycle.md) - Compose reusable durable components outside the Agent base class
145
+ - [Tasks](./tasks.md) - Durable, replayable background work with journaled steps and durable sleeps (experimental)
146
+ - [Streams](./streams.md) - Durable incremental output: chunk log, cursor, replay-then-tail reads (experimental)
144
147
  - TODO: [API Reference](./api-reference.md) - Complete API documentation
145
148
  - TODO: [FAQ / How is this different from Durable Objects?](./faq.md)
146
149
  - TODO: [Resources & Further Reading](./resources.md)
package/docs/lifecycle.md CHANGED
@@ -26,7 +26,7 @@ export class MyObject extends DurableObject<Env> {
26
26
  }
27
27
 
28
28
  onAlarm(): void {
29
- // Runs after lifecycle capabilities process the alarm.
29
+ // Runs once per alarm invocation, after due jobs are driven.
30
30
  }
31
31
  }
32
32
  ```
@@ -65,24 +65,34 @@ export default {
65
65
  The default URL shape is `/agents/:binding/:name`. Direct
66
66
  `env.MY_OBJECT.getByName(name).fetch(request)` calls work as well.
67
67
 
68
- `Agent` already constructs this lifecycle. Existing Agent classes continue to
69
- override `onStart`, `onRequest`, `onConnect`, `onMessage`, `onClose`, and
70
- `onError` normally.
68
+ `Agent` already constructs this lifecycle (and installs the `WebSockets`
69
+ capability for its connections). Existing Agent classes continue to override
70
+ `onStart`, `onRequest`, `onConnect`, `onMessage`, `onClose`, and `onError`
71
+ normally.
71
72
 
72
73
  ## Request call path
73
74
 
75
+ The lifecycle-installed `fetch` is the request handler. It offloads each
76
+ request to the installed capabilities, which act as middleware: the first
77
+ capability registered that matches the request handles it by returning a
78
+ `Response`. A capability that returns `undefined` passes the request on to
79
+ the next capability, and a request no capability claims falls through to the
80
+ host's `onRequest`.
81
+
74
82
  ```text
75
83
  routeAgentRequest(request)
76
84
  └─ named Durable Object stub.fetch(request)
77
85
  └─ lifecycle-installed fetch
78
86
  ├─ lifecycle startup capabilities
79
87
  ├─ host onStart
80
- ├─ lifecycle request capabilities
81
- │ └─ first Response wins
88
+ ├─ capability middleware, in registration order
89
+ │ └─ first Response handles the request
82
90
  └─ host onRequest
83
91
  ```
84
92
 
85
- A warm object skips startup but still offers every request to its capabilities.
93
+ A warm object skips startup but still offers every request to its middleware.
94
+ There is no `next()` today: a capability either handles a request or declines
95
+ it, and cannot wrap or observe a downstream response.
86
96
 
87
97
  ## Reusable capabilities
88
98
 
@@ -107,10 +117,6 @@ class AuditLog implements DurableObjectCapability {
107
117
  return new Response("ok");
108
118
  }
109
119
  }
110
-
111
- onAlarm(): void {
112
- this.storage.sql.exec("DELETE FROM audit_log");
113
- }
114
120
  }
115
121
  ```
116
122
 
@@ -127,12 +133,24 @@ export class MyObject extends DurableObject<Env> {
127
133
  }
128
134
  ```
129
135
 
130
- Capabilities run in registration order. Startup and alarms run every hook
131
- sequentially. Request handling stops at the first returned `Response`. A phase
136
+ Capabilities run in registration order. Startup runs every hook
137
+ sequentially. Request handling is middleware dispatch: it stops at the first
138
+ returned `Response`, and returning `undefined` passes the request on. A phase
132
139
  failure propagates, and failed startup can be retried.
133
140
 
141
+ A capability declares how it claims traffic with `claims`, which defaults to
142
+ `"selective"`. A `"catch-all"` capability dispatches after every other
143
+ capability, whenever it was installed. Catch-alls are unique per dispatch
144
+ hook: Lifecycle refuses a second catch-all for `onRequest` or for
145
+ `onWebSocketUpgrade`, since it could never be reached, but one of each
146
+ coexists. The `WebSockets` capability is a catch-all for upgrades only, so
147
+ an HTTP catch-all installs beside it, and selective HTTP capabilities that
148
+ target their own routes run before both. A subclass that installs request or
149
+ upgrade middleware from its own constructor still runs first, even though
150
+ `Agent`'s constructor ran earlier.
151
+
134
152
  Capabilities extending `LifecycleCapability` receive one standard service
135
- surface: storage, readiness, startup state, alarm coordination, a host
153
+ surface: storage, readiness, startup state, the job queue, a host
136
154
  invocation boundary, best-effort events, and capability routing.
137
155
  Host-specific bindings, authentication, and protocol adapters remain explicit
138
156
  constructor dependencies. Lifecycle never grants a capability the complete
@@ -140,49 +158,62 @@ host implicitly.
140
158
 
141
159
  Capability hooks run outside host context, but user callbacks run through
142
160
  `this.lifecycle.runInHostContext(fn)` inside the host invocation context.
143
- Scheduler dispatches its registered callbacks through this boundary, and a
144
- future capability that calls user code should do the same.
161
+ Scheduler and Queue dispatch their registered callbacks through this
162
+ boundary, and a future capability that calls user code should do the same.
145
163
 
146
- ## Shared alarm ownership
164
+ ## The job queue
147
165
 
148
- Lifecycle owns the Durable Object's single physical alarm. A capability that
149
- needs a future wake-up keeps its work in its own durable storage and implements
150
- `getNextAlarm()`:
166
+ Lifecycle owns the Durable Object's queue of durable work and its single
167
+ physical alarm. A job is a serialisable callback address — the owning
168
+ capability plus a function name — with a due time and a payload. A capability
169
+ that needs future work pushes a job and implements `onJob()`:
151
170
 
152
171
  ```ts
153
- import { LifecycleCapability, type AlarmContribution } from "agents/lifecycle";
172
+ import {
173
+ LifecycleCapability,
174
+ type LifecycleJobContext
175
+ } from "agents/lifecycle";
154
176
 
155
177
  class Cleanup extends LifecycleCapability {
156
178
  constructor() {
157
179
  super("cleanup");
158
180
  }
159
181
 
160
- async getNextAlarm(): Promise<AlarmContribution> {
161
- return (await this.lifecycle.storage.get<number>("cleanup:next")) ?? null;
162
- }
163
-
164
- async onAlarm(): Promise<void> {
165
- const next = await this.lifecycle.storage.get<number>("cleanup:next");
166
- if (next === undefined || next > Date.now()) return;
167
- await this.lifecycle.storage.delete("cleanup:next");
182
+ async scheduleCleanup(time: number): Promise<void> {
183
+ await this.lifecycle.jobs.push({ id: "cleanup", fn: "sweep", time });
168
184
  }
169
185
 
170
- async scheduleCleanup(time: number): Promise<void> {
171
- await this.lifecycle.storage.put("cleanup:next", time);
172
- await this.lifecycle.alarms.rearm();
186
+ async onJob({ job }: LifecycleJobContext): Promise<void> {
187
+ // job.fn === "sweep"; returning nothing completes the job.
188
+ await this.lifecycle.storage.delete("cleanup:marker");
173
189
  }
174
190
  }
175
191
  ```
176
192
 
177
- Lifecycle selects the earliest contribution from every capability and the
178
- host. It runs all capability `onAlarm()` hooks, then host `onAlarm()`, then
179
- recalculates the physical alarm. Capabilities do not depend on Scheduler or on
180
- each other merely to receive alarm wakes.
181
- A contribution can be `{ time, exclusive: true }` when its wake time must
182
- replace ordinary wake candidates, such as a pending teardown. This changes only
183
- which physical alarm is armed; when that alarm fires, normal capability and host
184
- hook order still applies. Hosts can implement `getNextAlarm()` for alarm work
185
- that has not yet been extracted into a capability.
193
+ The queue is ordered by timestamp, and every queue mutation re-arms the
194
+ physical alarm automatically — there is no explicit rearm call. When the
195
+ alarm fires, Lifecycle drives due jobs in due order as an event loop, then
196
+ runs host `onAlarm()`, then re-arms from queue state. Before driving any job
197
+ it arms a deadman pre-alarm so an isolate death mid-drive still wakes the
198
+ object to resume.
199
+
200
+ A job's drive result decides what happens next: returning nothing completes
201
+ and deletes it, `{ rescheduleAt }` suspends it until a future time, and
202
+ `"yield"` leaves it due so the object wakes again immediately. Lifecycle also
203
+ owns dispatch retries: a job's `retry` options bound in-process attempts,
204
+ platform-class failures (a superseded isolate after a deploy, a memory-limit
205
+ reset) preserve the job for a fresh invocation, and a terminal application
206
+ failure reaches the owner's `onJobError()`, whose result decides advancement.
207
+
208
+ A job pushed with `exclusive: true` suppresses ordinary alarm candidates
209
+ while it is pending — Agent's deferred destroy uses this so a condemned
210
+ object cannot be kept alive by other work. A `singleflight` job is skipped
211
+ while a previous run is still in flight, until it crosses its hung timeout.
212
+ The host pushes jobs through `lifecycle.jobs` and implements the same
213
+ `onJob()` hook (a host job's terminal failure completes it; the host
214
+ re-derives its jobs from durable state). Capabilities do not depend on
215
+ Scheduler or
216
+ on each other merely to receive wakes.
186
217
 
187
218
  ## Capability events
188
219
 
@@ -217,7 +248,7 @@ dispatch. A host with child objects supplies the transport internally.
217
248
 
218
249
  Agent uses this for facet schedules: Scheduler sends owner-scoped CRUD to the
219
250
  root Scheduler and routes due callbacks back to the matching facet Scheduler.
220
- Existing rows remain in the root `cf_agents_schedules` table. Scheduler does not
251
+ Facet schedules live as jobs in the root's queue. Scheduler does not
221
252
  implement facet traversal, and Agent exposes only one internal generic Lifecycle
222
253
  route aperture.
223
254
 
@@ -267,33 +298,124 @@ Host context values follow the invocation:
267
298
 
268
299
  - `onStart` and `onAlarm`: object;
269
300
  - `onRequest`: object and request;
270
- - `onConnect`: object, connection, and upgrade request;
271
- - `onMessage`, `onClose`, and `onError`: object and connection.
301
+ - `WebSockets` capability handlers `onConnect`: object, connection, and
302
+ upgrade request;
303
+ - `WebSockets` capability handlers `onMessage`, `onClose`, and `onError`:
304
+ object and connection.
272
305
 
273
306
  `getConnectionTags(connection, { request })` remains argument-driven because it
274
307
  already receives both values explicitly. The root `agents` package continues
275
308
  to export `getCurrentAgent()` for the `Agent` class as a compatibility alias.
276
309
 
277
- ## WebSockets always hibernate
310
+ ## WebSockets are an opt-in capability
278
311
 
279
- The lifecycle always uses Cloudflare's WebSocket Hibernation API. Idle clients
280
- remain connected while the Durable Object can leave memory. When a message
281
- wakes the object, its constructor and lifecycle startup run again before
282
- `onMessage`.
283
-
284
- State needed after a wake must be stored durably or through connection state:
312
+ Lifecycle itself does not model WebSockets. Hosts that want connections
313
+ install the `WebSockets` capability, which owns the subsystem end to end —
314
+ it claims upgrades, dispatches handlers inside the host invocation boundary,
315
+ and answers `getConnections()`:
285
316
 
286
317
  ```ts
287
- onConnect(connection: Connection): void {
288
- connection.setState({ authenticated: true });
289
- }
318
+ import { WebSockets } from "agents/websockets";
290
319
 
291
- onMessage(connection: Connection<{ authenticated: boolean }>): void {
292
- console.log(connection.state?.authenticated);
320
+ export class MyObject extends DurableObject<Env> {
321
+ readonly webSockets = new WebSockets({
322
+ handlers: {
323
+ onConnect: (connection) => {
324
+ connection.setState({ authenticated: true });
325
+ },
326
+ onMessage: (connection, message) => {
327
+ connection.send(`echo:${message}`);
328
+ }
329
+ },
330
+ callables: new MyCallables(this)
331
+ });
332
+ readonly lifecycle = Lifecycle.install(this).use(this.webSockets);
293
333
  }
294
334
  ```
295
335
 
296
- There is no non-hibernating mode.
336
+ Without the capability installed, WebSocket upgrades are declined.
337
+
338
+ ### A plain host works with `useAgent`
339
+
340
+ The capability speaks the Agent protocol on every connection, so a plain
341
+ Durable Object is reachable from `useAgent` and `AgentClient` exactly like an
342
+ `Agent`:
343
+
344
+ - On connect it sends the identity frame, which resolves the client's
345
+ `ready` and `identified`, then the current state when `state` is set.
346
+ `protocol` controls this: `true` (default) for every connection, a
347
+ function to decide per connection — `false` marks it no-protocol, so it
348
+ gets no protocol text frames on connect or by broadcast but still sends
349
+ and receives ordinary messages and callables — or `false` to have the
350
+ host drive the sequence itself with `sendIdentity()` and `sendState()`.
351
+ `Agent` passes `false`, because it must decide whether a connection
352
+ belongs to a facet before any frame is sent.
353
+ - `readonly` decides per connection whether state writes over the wire are
354
+ refused; `setReadonly()` flips it later. Both flags are stored in the
355
+ connection's own state under `_cf_` keys, hidden from `connection.state`
356
+ and preserved across `setState`, so they survive hibernation. `Agent`'s
357
+ `isConnectionReadonly`, `setConnectionReadonly`, and
358
+ `isConnectionProtocolEnabled` delegate to the same storage.
359
+ - `callables` is an `RpcTarget` whose prototype methods are the host's
360
+ complete remote interface, reached through `call()` and `stub`. On the
361
+ `cf-websocket` wire the capability answers them as JSON `rpc` frames. On
362
+ the `capnweb` wire they are native Cap'n Web methods on the session root:
363
+ a returned `RpcTarget` arrives as a live stub the client can keep
364
+ calling, a `ReadableStream` streams, and chained calls pipeline. Methods
365
+ run through the host invocation boundary with the calling connection in
366
+ scope.
367
+ - `state` takes a `State` capability (from `agents/state`) and syncs it over
368
+ connections, so `useAgent().state` and `setState()` work against a plain
369
+ host:
370
+
371
+ ```ts
372
+ readonly state = new State({
373
+ initialState: { count: 0 },
374
+ // The state owner decides who hears about a change.
375
+ onChanged: (_state, source) => this.webSockets.broadcastState(source)
376
+ });
377
+ readonly webSockets = new WebSockets({ state: this.state });
378
+ readonly lifecycle = Lifecycle.install(this)
379
+ .use(this.state)
380
+ .use(this.webSockets);
381
+ ```
382
+
383
+ The current value is pushed to each new connection after identity. A
384
+ client's `cf_agent_state` frame goes through the `State` capability, so
385
+ the host's own `validateStateChange` decides; a readonly connection is
386
+ refused, and a rejected change is logged server-side and answered with a
387
+ generic `cf_agent_state_error`. `broadcastState(source)` pushes the
388
+ current value to every protocol-enabled connection except `source`. This
389
+ is distinct from `connection.setState()`, which is per-connection and
390
+ never leaves the host. Without the option, state is never sent or
391
+ accepted over connections.
392
+
393
+ - Any other frame goes to `handlers.onMessage`.
394
+
395
+ An `Agent` adds no new surface for this: its `@callable()`-decorated methods
396
+ are its JSON-wire interface, answered by its own message handler. They are
397
+ not mirrored onto the Cap'n Web root; an Agent that wants native calls on
398
+ `capnweb` passes a `callables` target like any other host.
399
+
400
+ ### Two wires
401
+
402
+ The client chooses how frames travel:
403
+
404
+ - **`cf-websocket`** (default) — accepted with Cloudflare's WebSocket Hibernation
405
+ API. Idle clients remain connected while the Durable Object leaves memory;
406
+ when a message wakes it, the constructor and lifecycle startup run again
407
+ before `onMessage`. State needed after a wake belongs in storage or
408
+ `connection.setState()`.
409
+ - **Cap'n Web** (`?__agents_transport=capnweb`, or
410
+ `useAgent({ transport: "capnweb" })`) — protocol frames travel through one
411
+ pipe method on a Cap'n Web session whose root also carries the host's
412
+ `callables` natively. The connection is an in-memory socket and does not
413
+ hibernate: the object stays pinned while it is open, and clients reconnect
414
+ after an eviction.
415
+
416
+ Handlers are wire-agnostic. Both kinds of connection dispatch the same
417
+ `onConnect`/`onMessage`/`onClose`/`onError`, appear in `getConnections()`,
418
+ and honour `connection.close(code, reason)`.
297
419
 
298
420
  ## Native RPC
299
421
 
@@ -505,7 +505,7 @@ This pattern has several advantages for long-running agents:
505
505
 
506
506
  ## Delegating to sub-agents
507
507
 
508
- A project manager does not do everything itself. It delegates specialized work to sub-agents — child Durable Objects (facets) spawned under the parent. Each facet has its own isolated SQLite state and runs in parallel, but stays colocated on the same machine as the parent.
508
+ A project manager does not do everything itself. It delegates specialized work to sub-agents — child Durable Objects (facets) spawned under the parent. Each facet has its own isolated SQLite state and runs in parallel, but stays colocated on the same machine as the parent. This per-run, parent-supervised delegation is the use case facets are for; see [When to use dynamic agents](./sub-agents.md#when-to-use-dynamic-agents) before reaching for one to model long-lived independent peers.
509
509
 
510
510
  ```typescript
511
511
  export class ProjectManager extends Agent<Env, ProjectState> {
@@ -623,7 +623,7 @@ The [Session API](./sessions.md) addresses this directly:
623
623
 
624
624
  - **Compaction** — automatically summarizes older messages when the estimated token count exceeds a threshold. The summary replaces the middle of the conversation as a non-destructive overlay. Original messages remain in SQLite for audit.
625
625
  - **Context blocks** — persistent structured sections injected into the system prompt (identity, memory, learned facts). The agent or the LLM can write to these blocks, and they survive hibernation and eviction.
626
- - **Multi-session management** — `SessionManager` provides a registry of named sessions within a single agent, with forking, cross-session search, and `compactAndSplit` for splitting long conversations into linked continuations.
626
+ - **Conversation isolation** — use one Durable Object per user-facing conversation and keep the conversation directory in a parent Durable Object. Sessions can still fork paths or use named handles for local drafts and namespaces.
627
627
 
628
628
  For simpler cases: keep only the last N messages in the active context (sliding window), or selectively retain messages that contain decisions and approvals while pruning routine exchanges.
629
629