@animalabs/agent-framework 0.29.0 → 0.31.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 (236) hide show
  1. package/README.md +71 -8
  2. package/dist/src/admission/admit.d.ts +90 -0
  3. package/dist/src/admission/admit.d.ts.map +1 -0
  4. package/dist/src/admission/admit.js +43 -0
  5. package/dist/src/admission/admit.js.map +1 -0
  6. package/dist/src/agent.d.ts +50 -9
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +125 -23
  9. package/dist/src/agent.js.map +1 -1
  10. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  11. package/dist/src/api/mcp-server-core.js +10 -3
  12. package/dist/src/api/mcp-server-core.js.map +1 -1
  13. package/dist/src/author-presentation.d.ts +50 -0
  14. package/dist/src/author-presentation.d.ts.map +1 -0
  15. package/dist/src/author-presentation.js +58 -0
  16. package/dist/src/author-presentation.js.map +1 -0
  17. package/dist/src/code-execution/isolate/render.d.ts.map +1 -1
  18. package/dist/src/code-execution/isolate/render.js +7 -6
  19. package/dist/src/code-execution/isolate/render.js.map +1 -1
  20. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  21. package/dist/src/code-execution/isolate/scripts.js +5 -4
  22. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  23. package/dist/src/dendrite/index.d.ts +6 -0
  24. package/dist/src/dendrite/index.d.ts.map +1 -0
  25. package/dist/src/dendrite/index.js +3 -0
  26. package/dist/src/dendrite/index.js.map +1 -0
  27. package/dist/src/dendrite/presets.d.ts +60 -0
  28. package/dist/src/dendrite/presets.d.ts.map +1 -0
  29. package/dist/src/dendrite/presets.js +117 -0
  30. package/dist/src/dendrite/presets.js.map +1 -0
  31. package/dist/src/dendrite/registry.d.ts +260 -0
  32. package/dist/src/dendrite/registry.d.ts.map +1 -0
  33. package/dist/src/dendrite/registry.js +742 -0
  34. package/dist/src/dendrite/registry.js.map +1 -0
  35. package/dist/src/dendrite/types.d.ts +392 -0
  36. package/dist/src/dendrite/types.d.ts.map +1 -0
  37. package/dist/src/dendrite/types.js +24 -0
  38. package/dist/src/dendrite/types.js.map +1 -0
  39. package/dist/src/framework.d.ts +806 -118
  40. package/dist/src/framework.d.ts.map +1 -1
  41. package/dist/src/framework.js +4180 -1121
  42. package/dist/src/framework.js.map +1 -1
  43. package/dist/src/gate/event-gate.d.ts +36 -21
  44. package/dist/src/gate/event-gate.d.ts.map +1 -1
  45. package/dist/src/gate/event-gate.js +223 -149
  46. package/dist/src/gate/event-gate.js.map +1 -1
  47. package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
  48. package/dist/src/gate/recipe-reconcile.js +6 -5
  49. package/dist/src/gate/recipe-reconcile.js.map +1 -1
  50. package/dist/src/host-presentation.d.ts +4 -0
  51. package/dist/src/host-presentation.d.ts.map +1 -1
  52. package/dist/src/host-presentation.js +12 -10
  53. package/dist/src/host-presentation.js.map +1 -1
  54. package/dist/src/index.d.ts +6 -2
  55. package/dist/src/index.d.ts.map +1 -1
  56. package/dist/src/index.js +7 -1
  57. package/dist/src/index.js.map +1 -1
  58. package/dist/src/inference-bounds.d.ts +37 -0
  59. package/dist/src/inference-bounds.d.ts.map +1 -0
  60. package/dist/src/inference-bounds.js +84 -0
  61. package/dist/src/inference-bounds.js.map +1 -0
  62. package/dist/src/line-reader.d.ts +7 -1
  63. package/dist/src/line-reader.d.ts.map +1 -1
  64. package/dist/src/line-reader.js +17 -6
  65. package/dist/src/line-reader.js.map +1 -1
  66. package/dist/src/mcpl/channel-registry.d.ts +84 -9
  67. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  68. package/dist/src/mcpl/channel-registry.js +136 -58
  69. package/dist/src/mcpl/channel-registry.js.map +1 -1
  70. package/dist/src/mcpl/errors.d.ts +1 -0
  71. package/dist/src/mcpl/errors.d.ts.map +1 -1
  72. package/dist/src/mcpl/errors.js +3 -0
  73. package/dist/src/mcpl/errors.js.map +1 -1
  74. package/dist/src/mcpl/inbound-source.d.ts +2 -2
  75. package/dist/src/mcpl/inbound-source.js +2 -2
  76. package/dist/src/mcpl/inference-router.d.ts +11 -1
  77. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  78. package/dist/src/mcpl/inference-router.js +65 -9
  79. package/dist/src/mcpl/inference-router.js.map +1 -1
  80. package/dist/src/mcpl/modern-connection.d.ts +10 -7
  81. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  82. package/dist/src/mcpl/modern-connection.js +85 -74
  83. package/dist/src/mcpl/modern-connection.js.map +1 -1
  84. package/dist/src/mcpl/protocol-family.d.ts +1 -1
  85. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  86. package/dist/src/mcpl/protocol-family.js +45 -3
  87. package/dist/src/mcpl/protocol-family.js.map +1 -1
  88. package/dist/src/mcpl/push-coalescer.d.ts +11 -7
  89. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  90. package/dist/src/mcpl/push-coalescer.js +21 -0
  91. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  92. package/dist/src/mcpl/references.d.ts +1 -2
  93. package/dist/src/mcpl/references.d.ts.map +1 -1
  94. package/dist/src/mcpl/references.js +3 -3
  95. package/dist/src/mcpl/references.js.map +1 -1
  96. package/dist/src/mcpl/server-connection.d.ts +14 -0
  97. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  98. package/dist/src/mcpl/server-connection.js +26 -22
  99. package/dist/src/mcpl/server-connection.js.map +1 -1
  100. package/dist/src/mcpl/tool-signature.d.ts +1 -0
  101. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  102. package/dist/src/mcpl/tool-signature.js +16 -11
  103. package/dist/src/mcpl/tool-signature.js.map +1 -1
  104. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  105. package/dist/src/mcpl/toolsets-module.js +7 -0
  106. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  107. package/dist/src/mcpl/transport.d.ts +44 -21
  108. package/dist/src/mcpl/transport.d.ts.map +1 -1
  109. package/dist/src/mcpl/transport.js +283 -64
  110. package/dist/src/mcpl/transport.js.map +1 -1
  111. package/dist/src/mcpl/types.d.ts +17 -1
  112. package/dist/src/mcpl/types.d.ts.map +1 -1
  113. package/dist/src/mcpl/types.js.map +1 -1
  114. package/dist/src/module-registry.d.ts +32 -4
  115. package/dist/src/module-registry.d.ts.map +1 -1
  116. package/dist/src/module-registry.js +189 -25
  117. package/dist/src/module-registry.js.map +1 -1
  118. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
  119. package/dist/src/modules/discord/discord-js-client.js +24 -16
  120. package/dist/src/modules/discord/discord-js-client.js.map +1 -1
  121. package/dist/src/modules/sender-name.d.ts +5 -4
  122. package/dist/src/modules/sender-name.d.ts.map +1 -1
  123. package/dist/src/modules/sender-name.js +7 -5
  124. package/dist/src/modules/sender-name.js.map +1 -1
  125. package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
  126. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  127. package/dist/src/modules/workspace/disk-agreement.js +33 -12
  128. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  129. package/dist/src/modules/workspace/index.d.ts +6 -3
  130. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  131. package/dist/src/modules/workspace/index.js +61 -15
  132. package/dist/src/modules/workspace/index.js.map +1 -1
  133. package/dist/src/modules/workspace/observe.d.ts +9 -1
  134. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  135. package/dist/src/modules/workspace/observe.js +50 -10
  136. package/dist/src/modules/workspace/observe.js.map +1 -1
  137. package/dist/src/modules/workspace/reconcile.d.ts +11 -1
  138. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  139. package/dist/src/modules/workspace/reconcile.js +19 -8
  140. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  141. package/dist/src/modules/workspace/types.d.ts +8 -0
  142. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  143. package/dist/src/modules/workspace/types.js.map +1 -1
  144. package/dist/src/modules/workspace/watcher.d.ts +7 -0
  145. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  146. package/dist/src/modules/workspace/watcher.js +7 -5
  147. package/dist/src/modules/workspace/watcher.js.map +1 -1
  148. package/dist/src/notice-tags.d.ts +2 -0
  149. package/dist/src/notice-tags.d.ts.map +1 -1
  150. package/dist/src/notice-tags.js +2 -0
  151. package/dist/src/notice-tags.js.map +1 -1
  152. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  153. package/dist/src/recovery/discord-awareness-outbox.js +1 -9
  154. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  155. package/dist/src/silent-heartbeat.d.ts +4 -2
  156. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  157. package/dist/src/silent-heartbeat.js +7 -4
  158. package/dist/src/silent-heartbeat.js.map +1 -1
  159. package/dist/src/stored-injections.d.ts +4 -0
  160. package/dist/src/stored-injections.d.ts.map +1 -1
  161. package/dist/src/stored-injections.js +6 -0
  162. package/dist/src/stored-injections.js.map +1 -1
  163. package/dist/src/supplied-text.d.ts +2 -0
  164. package/dist/src/supplied-text.d.ts.map +1 -1
  165. package/dist/src/supplied-text.js +16 -7
  166. package/dist/src/supplied-text.js.map +1 -1
  167. package/dist/src/tool-result-guard.js +1 -1
  168. package/dist/src/tool-result-guard.js.map +1 -1
  169. package/dist/src/tune-out/coordinator.d.ts +368 -16
  170. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  171. package/dist/src/tune-out/coordinator.js +1224 -136
  172. package/dist/src/tune-out/coordinator.js.map +1 -1
  173. package/dist/src/tune-out/tools.d.ts +115 -7
  174. package/dist/src/tune-out/tools.d.ts.map +1 -1
  175. package/dist/src/tune-out/tools.js +55 -18
  176. package/dist/src/tune-out/tools.js.map +1 -1
  177. package/dist/src/types/agent.d.ts +27 -1
  178. package/dist/src/types/agent.d.ts.map +1 -1
  179. package/dist/src/types/framework.d.ts +153 -15
  180. package/dist/src/types/framework.d.ts.map +1 -1
  181. package/dist/src/types/module.d.ts +42 -3
  182. package/dist/src/types/module.d.ts.map +1 -1
  183. package/dist/src/types/trace.d.ts +140 -3
  184. package/dist/src/types/trace.d.ts.map +1 -1
  185. package/package.json +4 -4
  186. package/src/admission/admit.ts +113 -0
  187. package/src/agent.ts +138 -31
  188. package/src/api/mcp-server-core.ts +10 -3
  189. package/src/author-presentation.ts +81 -0
  190. package/src/code-execution/isolate/render.ts +7 -6
  191. package/src/code-execution/isolate/scripts.ts +5 -4
  192. package/src/dendrite/index.ts +32 -0
  193. package/src/dendrite/presets.ts +149 -0
  194. package/src/dendrite/registry.ts +808 -0
  195. package/src/dendrite/types.ts +397 -0
  196. package/src/framework.ts +4344 -1052
  197. package/src/gate/event-gate.ts +251 -150
  198. package/src/gate/recipe-reconcile.ts +7 -5
  199. package/src/host-presentation.ts +14 -10
  200. package/src/index.ts +16 -2
  201. package/src/inference-bounds.ts +111 -0
  202. package/src/line-reader.ts +22 -5
  203. package/src/mcpl/channel-registry.ts +170 -60
  204. package/src/mcpl/errors.ts +4 -0
  205. package/src/mcpl/inbound-source.ts +2 -2
  206. package/src/mcpl/inference-router.ts +67 -10
  207. package/src/mcpl/modern-connection.ts +83 -68
  208. package/src/mcpl/protocol-family.ts +37 -4
  209. package/src/mcpl/push-coalescer.ts +21 -7
  210. package/src/mcpl/references.ts +3 -4
  211. package/src/mcpl/server-connection.ts +30 -21
  212. package/src/mcpl/tool-signature.ts +18 -11
  213. package/src/mcpl/toolsets-module.ts +7 -0
  214. package/src/mcpl/transport.ts +261 -60
  215. package/src/mcpl/types.ts +18 -1
  216. package/src/module-registry.ts +178 -25
  217. package/src/modules/discord/discord-js-client.ts +33 -29
  218. package/src/modules/sender-name.ts +8 -5
  219. package/src/modules/workspace/disk-agreement.ts +39 -15
  220. package/src/modules/workspace/index.ts +65 -14
  221. package/src/modules/workspace/observe.ts +51 -10
  222. package/src/modules/workspace/reconcile.ts +22 -8
  223. package/src/modules/workspace/types.ts +5 -0
  224. package/src/modules/workspace/watcher.ts +8 -6
  225. package/src/notice-tags.ts +2 -0
  226. package/src/recovery/discord-awareness-outbox.ts +1 -11
  227. package/src/silent-heartbeat.ts +7 -4
  228. package/src/stored-injections.ts +7 -0
  229. package/src/supplied-text.ts +17 -6
  230. package/src/tool-result-guard.ts +1 -1
  231. package/src/tune-out/coordinator.ts +1374 -141
  232. package/src/tune-out/tools.ts +166 -13
  233. package/src/types/agent.ts +30 -1
  234. package/src/types/framework.ts +157 -15
  235. package/src/types/module.ts +42 -3
  236. package/src/types/trace.ts +154 -3
@@ -1,3 +1,6 @@
1
+ import type { ContentBlock } from '@animalabs/membrane';
2
+ import { authorLine } from '../author-presentation.js';
3
+ import type { StoredMessage } from '@animalabs/context-manager';
1
4
  /**
2
5
  * The subconscious's tool surface (issue #77).
3
6
  *
@@ -11,7 +14,7 @@
11
14
  * never leak bare prose to the default locus.
12
15
  */
13
16
 
14
- import type { ToolDefinition } from '../types/index.js';
17
+ import type { AgentConfig, ToolDefinition } from '../types/index.js';
15
18
 
16
19
  export const SUBCONSCIOUS_TOOL_NAMES = [
17
20
  'deliver_summary',
@@ -51,11 +54,13 @@ export const SUBCONSCIOUS_TOOLS: ToolDefinition[] = [
51
54
  'End the tune-out on a channel now — use when something needs the ' +
52
55
  'resident\'s full attention. The diverted backlog is delivered to ' +
53
56
  'them (capped), and normal attention resumes. Add your own note in ' +
54
- '`text`; it arrives alongside the backlog, in your voice.',
57
+ '`text`; it arrives alongside the backlog, in your voice. If you were ' +
58
+ 'started to read one hold, you can end only that hold.',
55
59
  inputSchema: {
56
60
  type: 'object',
57
61
  properties: {
58
- channelId: { type: 'string' },
62
+ channelId: { type: 'string', description: 'A tuned-out channel, or a channel held by the all-except hold (which ends that hold, unless scope is "channel").' },
63
+ scope: { type: 'string', enum: ['channel', 'all-except'], description: '"all-except" ends the all-except hold outright; "channel" ends only the channel\'s own tune-out.' },
59
64
  text: {
60
65
  type: 'string',
61
66
  description: 'Your accompanying note to the resident (optional).',
@@ -86,10 +91,14 @@ export const SUBCONSCIOUS_TOOLS: ToolDefinition[] = [
86
91
  {
87
92
  name: 'speak_in_channel',
88
93
  description:
89
- 'Post a message into a tuned-out channel, clearly as yourself (not ' +
90
- 'as the resident). Use sparingly — e.g. to tell someone the resident ' +
91
- 'is tuned out and when to expect them. Disabled unless the resident\'s ' +
92
- 'configuration allows it.',
94
+ 'Post a message into a tuned-out channel, in your own words and ' +
95
+ 'clearly as yourself (not as the resident); it goes out through the ' +
96
+ 'resident\'s publish path, so the channel sees it under their account. ' +
97
+ 'Use sparingly — e.g. to tell someone the resident is tuned out and ' +
98
+ 'when to expect them. If you were started to read one hold, you can ' +
99
+ 'post only into a channel that hold still holds, and a post made once ' +
100
+ 'it has ended is refused; your report can say it instead. Disabled unless ' +
101
+ 'the resident\'s configuration allows it.',
93
102
  inputSchema: {
94
103
  type: 'object',
95
104
  properties: {
@@ -102,11 +111,53 @@ export const SUBCONSCIOUS_TOOLS: ToolDefinition[] = [
102
111
  ];
103
112
 
104
113
  /**
105
- * Configuration for the subconscious resident (FrameworkConfig.subconscious).
114
+ * Configuration for the subconscious (FrameworkConfig.subconscious). Two
115
+ * shapes, by `reader`:
116
+ *
117
+ * - `'persistent'` (default): one persistent side-agent with an isolated
118
+ * slot and a windowed read-only view of the residents' timeline, as
119
+ * issue #77 built it. Its `systemPrompt` is a system prompt.
120
+ * - `'forks'`: a succession of forks of the resident (Dendrite). Each
121
+ * cadence tick, coalesced wake and cancel derives a short-lived fork at
122
+ * the resident's head — the resident's whole prefix, its refusals
123
+ * included, ending with it — hands it the traffic held since the last
124
+ * look as the first message after the fork point, and lets it report
125
+ * back as attributed mail. No agent persists between invocations;
126
+ * dispositions do, in the coordinator's state. Its `voice` is framing,
127
+ * not a system prompt, and residents are shown the reader's four tools
128
+ * in their tool block (refused at dispatch), so the fork's request is
129
+ * the resident's up to the fork point.
106
130
  */
107
- export interface SubconsciousConfig {
131
+ export type SubconsciousConfig = PersistentReaderConfig | ForkReaderConfig;
132
+
133
+ interface SubconsciousConfigBase {
108
134
  /** Master switch. */
109
135
  enabled: boolean;
136
+ /**
137
+ * Allow speak_in_channel. Default false until the voice block has passed
138
+ * its canary round. A reader's post goes out through the resident's own
139
+ * publish path, so the channel sees it under the resident's account, in
140
+ * the reader's words; nothing in the request names the reader. With it
141
+ * off, a reader fork can post nowhere: every tool but its own is refused
142
+ * at dispatch (the block it is shown stays the resident's, for the
143
+ * shared prefix).
144
+ */
145
+ allowChannelSpeech?: boolean;
146
+ /** The reader's model's context window, as AgentConfig.physicalWindowTokens,
147
+ * in place of the primary's. Left out or null, a reader on the primary's
148
+ * model has the primary's; on another model, the persistent reader has
149
+ * none and a fork has the primary's. */
150
+ physicalWindowTokens?: AgentConfig['physicalWindowTokens'];
151
+ /** The reader's model's thinking binding, as AgentConfig.thinkingBinding,
152
+ * in place of the primary's. Left out, null or false, a reader on the
153
+ * primary's model has the primary's; on another model, the persistent
154
+ * reader has none and a fork has the primary's. `{ binds: false }` gives
155
+ * a reader none. */
156
+ thinkingBinding?: AgentConfig['thinkingBinding'];
157
+ }
158
+
159
+ export interface PersistentReaderConfig extends SubconsciousConfigBase {
160
+ reader?: 'persistent';
110
161
  /**
111
162
  * Registry + participant name. Default 'Subconscious' — following the
112
163
  * Context Manager precedent: a title-case functional voice, not an
@@ -116,7 +167,8 @@ export interface SubconsciousConfig {
116
167
  /** Model id; defaults to the primary agent's (same-model side-process).
117
168
  * Another id, compared exactly, makes the subconscious leave out the
118
169
  * fields that describe the primary's model, its window and its thinking
119
- * binding (SUBCONSCIOUS_MODEL_FIELDS); nothing here sets its own. */
170
+ * binding (SUBCONSCIOUS_MODEL_FIELDS); `physicalWindowTokens` and
171
+ * `thinkingBinding` set its own. */
120
172
  model?: string;
121
173
  /**
122
174
  * The voice/criteria mode block — recipe-side and co-authored with the
@@ -124,9 +176,110 @@ export interface SubconsciousConfig {
124
176
  * person toward the resident. Canary before fleet use (issue #77).
125
177
  */
126
178
  systemPrompt: string;
127
- /** Allow speak_in_channel. Default false until the voice block has
128
- * passed its canary round. */
129
- allowChannelSpeech?: boolean;
130
179
  /** WindowedPassthroughStrategy re-anchor fraction (default 0.5). */
131
180
  reAnchorFraction?: number;
132
181
  }
182
+
183
+ export interface ForkReaderConfig extends SubconsciousConfigBase {
184
+ reader: 'forks';
185
+ /**
186
+ * Whose weights run the fork. Required, never defaulted: a fork holds the
187
+ * resident's whole prefix, and which model reads it is the one fact the
188
+ * resident's consent names. The resident's own model is the copy; another
189
+ * model is allowed when stated here. `create` refuses the omission.
190
+ */
191
+ model: string;
192
+ /**
193
+ * The reader's voice block — report-shaped, second person toward the
194
+ * resident, co-authored with them. Delivered as the first message after
195
+ * the fork point, with the notice, the standing dispositions and the held
196
+ * traffic; never a system prompt.
197
+ */
198
+ voice: string;
199
+ /**
200
+ * A fresh context strategy instance for each fork, same class and
201
+ * configuration as the resident's, so the fork reuses the resident's fold
202
+ * state and rendering. Required unless the resident runs a passthrough
203
+ * strategy.
204
+ */
205
+ strategyFactory?: () => import('@animalabs/context-manager').ContextStrategy;
206
+ /** Idle timeout for one reader fork (default 10 minutes). */
207
+ forkIdleTimeoutMs?: number;
208
+ /**
209
+ * How a reader fork is prompted: the content of the one message it
210
+ * receives after the fork point. Default `defaultReaderFraming`. The
211
+ * framework stores whatever this returns as a user message with
212
+ * `metadata.kind = 'tune-out-reader-framing'`; the notice, voice,
213
+ * dispositions and held traffic are in the context, and nothing else
214
+ * about the fork's prompt is fixed by the path.
215
+ */
216
+ framing?: (context: ReaderFramingContext) => ContentBlock[];
217
+ }
218
+
219
+ /** What a reader fork's framing is built from. */
220
+ export interface ReaderFramingContext {
221
+ /** The resident the fork was derived from. */
222
+ resident: string;
223
+ /** The fork's own name. */
224
+ fork: string;
225
+ /** `ForkReaderConfig.voice`. */
226
+ voice: string;
227
+ channelId: string;
228
+ epochId: string;
229
+ trigger: 'cadence' | 'wake' | 'cancel';
230
+ /** The coordinator's notice for this invocation (`[Tune-out wake: …]` etc.). */
231
+ notice: string;
232
+ /** Standing dispositions recorded on the epoch, or null. */
233
+ dispositions: string | null;
234
+ /**
235
+ * The traffic held since the last look, oldest first; empty when nothing
236
+ * arrived. It includes the messages still queued behind the resident's
237
+ * turn (admitted, not yet stored), last: their ids are held handles and
238
+ * their sequences provisional, as a module's held rows are.
239
+ */
240
+ backlog: StoredMessage[];
241
+ /** How many of `backlog`'s last entries are still queued behind the resident's turn. Absent is none. */
242
+ queuedCount?: number;
243
+ }
244
+
245
+ /**
246
+ * The default reader-fork framing: a header naming the channel, epoch and
247
+ * trigger; the notice; the voice block; standing dispositions if any; then
248
+ * the held traffic as `author: text` lines with non-text blocks (images,
249
+ * documents) preserved in order as the source supplied them.
250
+ */
251
+ export function defaultReaderFraming(ctx: ReaderFramingContext): ContentBlock[] {
252
+ const header =
253
+ `[Tune-out reader — ${ctx.channelId}, epoch ${ctx.epochId.slice(0, 8)}, ${ctx.trigger}]\n` +
254
+ `${ctx.notice}\n\n${ctx.voice}` +
255
+ (ctx.dispositions ? `\n\n[Standing dispositions]\n${ctx.dispositions}` : '');
256
+ const content: ContentBlock[] = [{ type: 'text', text: header }];
257
+ if (ctx.backlog.length === 0) {
258
+ content.push({ type: 'text', text: '\n[Nothing new has been held since your last look.]' });
259
+ return content;
260
+ }
261
+ const n = ctx.backlog.length;
262
+ const q = Math.min(Math.max(0, Math.floor(ctx.queuedCount ?? 0)), n);
263
+ const lines: string[] = [`\n[Held in ${ctx.channelId} since your last look: ${n} message${n === 1 ? '' : 's'}]`];
264
+ const flush = () => {
265
+ if (lines.length > 0) content.push({ type: 'text', text: lines.splice(0).join('\n') });
266
+ };
267
+ const which = q === n ? (q === 1 ? 'It' : 'They') : (q === 1 ? 'The last one' : `The last ${q}`);
268
+ const queuedNote = `[${which} arrived while ${ctx.resident}'s turn was running, so ${q === 1 ? 'it is' : 'they are'} still queued, not yet stored.]`;
269
+ for (const [i, message] of ctx.backlog.entries()) {
270
+ if (q > 0 && i === n - q) lines.push(queuedNote);
271
+ const author = (message.metadata as { author?: { name?: string } } | undefined)?.author?.name ?? message.participant;
272
+ const text = message.content
273
+ .filter((b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text')
274
+ .map((b) => b.text)
275
+ .join('\n');
276
+ lines.push(authorLine(author, text));
277
+ const media = message.content.filter((b) => b.type !== 'text');
278
+ if (media.length > 0) {
279
+ flush();
280
+ content.push(...media);
281
+ }
282
+ }
283
+ flush();
284
+ return content;
285
+ }
@@ -340,6 +340,34 @@ export interface AgentConfig {
340
340
  */
341
341
  proseRouting?: 'locus' | 'explicit' | 'hybrid' | 'disabled';
342
342
 
343
+ /**
344
+ * Stored participants that are this agent's own voice, in addition to its
345
+ * name: their turns are presented to the model as this agent's assistant
346
+ * turns. A derived agent sets this to the agent it inherited from, so the
347
+ * parent's turns read as its own history. Model-facing role assignment
348
+ * only, applied at request assembly — stored authorship is never rewritten.
349
+ */
350
+ selfParticipants?: string[];
351
+
352
+ /**
353
+ * The participant name this agent's own turns are presented under at
354
+ * request assembly. Defaults to `name`. A derived agent that should share
355
+ * its parent's provider prefix sets the parent's name, so the two
356
+ * requests are identical in formatters that render participant names as
357
+ * well as in those that only assign roles. New turns are still STORED
358
+ * under the agent's own name.
359
+ */
360
+ presentAs?: string;
361
+
362
+ /**
363
+ * Tools refused when this agent calls them, although they stay advertised.
364
+ * Restricts a derived agent without changing the tool block at the front
365
+ * of its request — removing a tool from `allowedTools` instead would
366
+ * change those bytes and forfeit the provider prefix it shares with its
367
+ * parent.
368
+ */
369
+ denyToolsAtDispatch?: string[];
370
+
343
371
  /**
344
372
  * Fail-closed containment for a text response whose entire visible prose is
345
373
  * an invocation-shaped wrapper naming a tool registered on that exact turn.
@@ -511,7 +539,8 @@ export interface InferenceOptions {
511
539
  * `supportsInjectedMessages` is what the stream declared with the round's
512
540
  * tool calls (membrane's `ToolContext.supportsInjectedMessages`): whether it
513
541
  * can carry messages injected with that round's results. It's absent when
514
- * the stream doesn't say, as on membrane 0.5.87 and earlier.
542
+ * the stream doesn't say, which reads as a stream that can't, and when there
543
+ * is no stream, as for the direct API.
515
544
  */
516
545
  export type AgentState =
517
546
  | { status: 'idle' }
@@ -6,6 +6,7 @@ import type { ProcessEvent } from './events.js';
6
6
  import type { McplServerConfig, InferenceRoutingPolicy } from '../mcpl/types.js';
7
7
  import type { ConversationRouterConfig } from '../mcpl/conversation-router.js';
8
8
  import type { GateOptions } from '../gate/types.js';
9
+ import type { InferenceBoundsOptions } from '../inference-bounds.js';
9
10
 
10
11
  // Re-export trace types
11
12
  export type { TraceEvent, TraceEventListener } from './trace.js';
@@ -116,6 +117,48 @@ export type ProviderHoldHook = (
116
117
  ) => ProviderHold | undefined;
117
118
 
118
119
  export interface FrameworkConfig {
120
+ /**
121
+ * Dendrite agent lifecycle options.
122
+ */
123
+ dendrite?: {
124
+ /**
125
+ * When an agent ends while task work it spawned is still running, the
126
+ * orphan is always told. With this on (the default), the first agent
127
+ * that could adopt it — its nearest live ancestor, else the primary —
128
+ * also receives a factual notice, which is what makes the reparenting
129
+ * offer visible to a model. Turn it off when the host makes adoption
130
+ * decisions itself from the `dendrite:agent-orphaned` trace.
131
+ */
132
+ announceOrphans?: boolean;
133
+ /**
134
+ * By default every derivation tells the agent it was made from, in one
135
+ * host-framed line. After this many derivations of one kind from one
136
+ * agent, that agent hears a count line every so many instead of a line
137
+ * per fork, so a reader loop that forks a hundred times a session doesn't
138
+ * fill its source's context with stamps; every derivation still emits its
139
+ * `dendrite:agent-created` trace. Default 10; 0 never collapses.
140
+ */
141
+ collapseDerivationNoticesAfter?: number;
142
+ /**
143
+ * A host's say over each derivation, ephemeral spawn and resume, before the
144
+ * agent or its new incarnation exists, given the spec of what is about to
145
+ * exist (name, kind, spawner, route, inheritance, metadata). A shared
146
+ * derivation's record then adds the child's branch, and names the
147
+ * parent's namespace as its own. Return nothing to allow, or `{ refuse }`
148
+ * with the reason, which the caller gets as the error. The framework
149
+ * attaches no meaning to the decision: consent, budget, hours, a mind's
150
+ * standing instruction, a use not yet imagined are all the host's.
151
+ */
152
+ beforeDerive?: (spec: import('../dendrite/types.js').AgentSpec) => void | { refuse: string };
153
+ /**
154
+ * What an agent is told when something is derived from it. Given the
155
+ * record, the agent it was derived from and the framework's default line,
156
+ * return the text to add, or null to add nothing. Absent, the default
157
+ * line is used. The `dendrite:agent-created` trace is emitted either way.
158
+ */
159
+ derivationNotice?: (record: import('../dendrite/types.js').AgentRecord, from: string, defaultText: string) => string | null;
160
+ };
161
+
119
162
  /**
120
163
  * The subconscious resident (issue #77): a persistent side-agent that
121
164
  * receives diverted traffic from tuned-out channels and reports to the
@@ -123,6 +166,46 @@ export interface FrameworkConfig {
123
166
  */
124
167
  subconscious?: import('../tune-out/tools.js').SubconsciousConfig;
125
168
 
169
+ /**
170
+ * Tune-out without a reader (issue #232 §3.3). The hold machinery always
171
+ * runs when MCPL channels exist, so a persisted epoch is resumed and
172
+ * released whatever the configuration; this switch only decides whether
173
+ * residents are offered `tune_out` when no reader is configured. Without
174
+ * a reader a tune-out is a silent hold: no cadence, no wake budget,
175
+ * mentions are acknowledged, the backlog is released on cancel.
176
+ */
177
+ tuneOut?: {
178
+ /** Offer `tune_out` without a subconscious. Default: only with one. */
179
+ enabled?: boolean;
180
+ /**
181
+ * Announce the all-except scope in the excepted channel: one bracketed
182
+ * line under the resident's name when it opens (who, for how long) and
183
+ * one when it closes. Default true — a hold the room cannot see is the
184
+ * failure the scope exists to prevent. `false` keeps both quiet.
185
+ */
186
+ announce?: boolean;
187
+ };
188
+
189
+ /**
190
+ * Where a turn's plain prose lands when nothing names a destination
191
+ * (architecture channel, 2026-10-06: the §3 carve-out accepted by the
192
+ * residents).
193
+ *
194
+ * - `'delivery'` (default): the locus comes only from the delivery that
195
+ * woke the turn, a conversation fork's home channel, or a channel the
196
+ * agent opened during the turn. Incoming traffic never moves it — not
197
+ * between turns, not mid-turn. A turn with no such locus keeps its
198
+ * prose private, and says so in a `[routing]` notice.
199
+ * - `'latest-inbound'`: the previous rule. The most recent delivered
200
+ * inbound message is a global fallback locus for turns without one, and
201
+ * an addressed message injected mid-turn re-pins the turn's locus.
202
+ *
203
+ * Held traffic never moves the locus under either rule.
204
+ */
205
+ speech?: {
206
+ locus?: 'delivery' | 'latest-inbound';
207
+ };
208
+
126
209
  /**
127
210
  * IANA zone of the agent's wall clock: it renders wall-clock times for the
128
211
  * agent, and it sets where clock rows' boundaries fall (AgentConfig.clock,
@@ -160,6 +243,26 @@ export interface FrameworkConfig {
160
243
  */
161
244
  deferredWritesPath?: string;
162
245
 
246
+ /**
247
+ * The Dendrite agent registry's recovery file: its live records and the
248
+ * results it holds for delivery, written atomically and fsynced, and read
249
+ * first at boot. Defaults to `<storePath>/recovery/dendrite-registry.json`.
250
+ * Outside the chronicle for the reason `hostModePath` is: a branch switch
251
+ * or historical rollback would otherwise take back held results. Set
252
+ * explicitly when providing an app-owned `store` without `storePath`;
253
+ * without either, the registry persists only in the branch-local store,
254
+ * with a warning at create.
255
+ */
256
+ agentRegistryPath?: string;
257
+
258
+ /**
259
+ * The registry's ended-record log recovery file (its newest records, as
260
+ * many as a restart reads). Defaults to
261
+ * `<storePath>/recovery/dendrite-ended.json`; same rationale and fallback
262
+ * as `agentRegistryPath`.
263
+ */
264
+ agentRegistryEndedPath?: string;
265
+
163
266
  /** Emoji used for branch-awareness markers (default: 💤). */
164
267
  discordAwarenessEmoji?: string;
165
268
 
@@ -197,6 +300,30 @@ export interface FrameworkConfig {
197
300
  /** Custom error policy */
198
301
  errorPolicy?: ErrorPolicy;
199
302
 
303
+ /**
304
+ * How long a model call may go without progress before it is cut. A
305
+ * resident's streamed call, and an MCPL server's, gets membrane's progress
306
+ * bounds where its adapter's stream reports content (Anthropic, Bedrock),
307
+ * and on every adapter a flat bound from its last provider event: the
308
+ * larger of a setup term (the larger of `beforeContentMs` and
309
+ * `betweenEventsMs`) plus the request's `maxTokens` at
310
+ * `thinkTokensPerSecond`, and `flatMinimumMs`. An auxiliary call
311
+ * (compression, the reply judge) and an MCPL server's non-streamed call get
312
+ * a bound on the whole call from its send, the setup term plus that
313
+ * allowance (the larger of the setup term and `flatMinimumMs` when it
314
+ * names no `maxTokens`), unless its
315
+ * caller passed a `progressTimeoutMs` of its own; where an adapter's
316
+ * `complete()` streams under the hood, membrane renews it at each parsed
317
+ * provider event. A call a bound cuts fails
318
+ * with type `timeout`, and on an agent's turn DefaultErrorPolicy gives each
319
+ * such call one fresh call, except in a turn that re-arms an exhausted one,
320
+ * where `exhaustedTurnRearmMs` says what a cut does. One setting covers
321
+ * every agent. `false` sets
322
+ * none of these bounds. Default: membrane's PROGRESS_BOUNDS and
323
+ * FLAT_MINIMUM_MS.
324
+ */
325
+ inferenceBounds?: false | InferenceBoundsOptions;
326
+
200
327
  /**
201
328
  * Host verdict on a failed inference: "this cannot succeed until later".
202
329
  * Consulted before the error policy and before the built-in
@@ -235,20 +362,32 @@ export interface FrameworkConfig {
235
362
  * channel isn't sent again: one whose turn was woken for a channel id that
236
363
  * names no one registered channel and never found one, and whose request
237
364
  * names none of its own. A failure that states a wait is held for that wait
238
- * first, as any turn's is, and a deadline ends the turn as before, whether
239
- * the host set it or membrane marks one as run out: the runtime's or
240
- * undici's on a sent request, the Anthropic adapter's watchdogs, or the
241
- * Anthropic SDK's timeout, which covers its connect timeout too. Until one
242
- * of the re-armed turn's rounds has run or had its tool calls cut off, its
243
- * failure gets no retries from the error policy: on another transient
244
- * failure the next delay follows, until the list runs out, and a failure of
245
- * any other kind ends the re-arming. A wake for the agent while it waits
246
- * starts its turn at once, with the re-armed trigger merged in. That turn is
247
- * the wake's: the error policy retries its failures. A completed turn starts
248
- * the count again, and so does a message said to the agent: not a system
249
- * message or notice; not an edit, reaction or deletion, unless an edit is
250
- * the first version of a message the agent sees; not a message its own view
251
- * leaves out, such as one a tune-out holds from it; and not the agent's own
365
+ * first, as any turn's is, and a turn that ends on a deadline, whether the
366
+ * host set it or membrane marks one as run out (the runtime's or undici's
367
+ * on a sent request, the Anthropic adapter's watchdogs, or the Anthropic
368
+ * SDK's timeout, which covers its connect timeout too), isn't re-armed,
369
+ * save the exception below. Until one of the re-armed turn's rounds has run
370
+ * or had its tool calls cut off, its failure gets no retries from the error
371
+ * policy: on another transient failure the next delay follows, until the
372
+ * list runs out, and a failure of any other kind ends the re-arming. The
373
+ * one exception is a re-armed turn's send that fails with a deadline marked
374
+ * as run out, membrane's or a bound the host sets on the call
375
+ * (`inferenceBounds`), as when a stream stalls. Such a cut counts as a
376
+ * transient failure, so a turn that ends on it is re-armed as for a
377
+ * transient failure, unless it follows another such cut with no transient
378
+ * failure, answered round or message said to the agent between them: two
379
+ * such cuts in a row end the re-arming, and the second gets no retry from
380
+ * the error policy, even in a re-armed turn that got partway. A transient
381
+ * failure or an answered round in any of the agent's turns clears an
382
+ * earlier cut, and so does a message said to the agent, or the agent's end.
383
+ * Every such cut counts, including one the error policy retries or a host's
384
+ * provider hold takes. A wake for the agent while it waits starts its turn
385
+ * at once, with the re-armed trigger merged in. That turn is the
386
+ * wake's: the error policy retries its failures. A completed turn starts the
387
+ * count again, and so does a message said to the agent: not a system message
388
+ * or notice; not an edit, reaction or deletion, unless an edit is the first
389
+ * version of a message the agent sees; not a message its own view leaves
390
+ * out, such as one a tune-out holds from it; and not the agent's own
252
391
  * schedule, the heartbeat's tick or reminder, a self-wake timer it set, or a
253
392
  * script's wake. A notice a module or an MCPL server stores as a message
254
393
  * counts. A message in the residents' shared slot counts for each resident
@@ -456,7 +595,10 @@ export interface InferencePolicy {
456
595
  */
457
596
  export interface ErrorPolicy {
458
597
  /**
459
- * Handle an inference error.
598
+ * Handle an inference error. `attempt` is how many times the turn's
599
+ * request has failed before this one; for a deadline expiry (a progress
600
+ * bound's cut, or one membrane marks), how many times that call's request
601
+ * has expired in a row before this one.
460
602
  */
461
603
  onInferenceError(
462
604
  error: Error,
@@ -120,7 +120,10 @@ export interface Module {
120
120
 
121
121
  /**
122
122
  * Start the module.
123
- * Called when the framework starts or when the module is added.
123
+ * Called when the module is added: create() adds the modules it is given,
124
+ * so for those it runs before create() returns. Its own removal and the
125
+ * framework's stop() wait for it to settle before the module's stop()
126
+ * runs, so it mustn't await either.
124
127
  */
125
128
  start(ctx: ModuleContext): Promise<void>;
126
129
 
@@ -132,6 +135,8 @@ export interface Module {
132
135
  * a host wires up after create() returns, such as a WorkspaceModule's
133
136
  * initStore(). A stop() that saves state can therefore run before that
134
137
  * wiring, and shouldn't then save over state it was handed and hasn't used.
138
+ * It isn't called before the module's start() has settled, so it can end
139
+ * what start() set up.
135
140
  */
136
141
  stop(): Promise<void>;
137
142
 
@@ -259,6 +264,41 @@ export interface AgentSettingsExtension {
259
264
 
260
265
  /**
261
266
  * Context provided to modules for interacting with the framework.
267
+ *
268
+ * Once the module's `stop()` has completed, through `removeModule()` or the
269
+ * framework's `stop()`, its context has no effect on the framework:
270
+ * - each push is dropped with a log line, as a push after the framework's
271
+ * `stop()` is;
272
+ * - `setState`, `registerLogState`, `appendToLog`, `editLogItem`,
273
+ * `editMessage`, `removeMessage`, `notifyOps`, `registerSpeechHandler` and
274
+ * `unregisterSpeechHandler` do nothing; `addMessage` returns `''` and adds
275
+ * nothing; `callTool` resolves with an error result and runs nothing;
276
+ * `getModule` returns `null`; and `onTrace` subscribes nothing, while the
277
+ * subscriptions made through the context end. None of these throws, and
278
+ * the first such call of each method logs one line naming the module and
279
+ * the method;
280
+ * - `findMessageByExternalId` answers without updating its mapping, and logs
281
+ * nothing.
282
+ *
283
+ * Its reads (`getState`, `getLog`, `getLogLength`, `getMessage`,
284
+ * `queryMessages`, `getAgents`, `getActiveTools`, `findMessageByExternalId`)
285
+ * answer, and fail, as before; one that meets a held write may record that
286
+ * the write has landed, as any reader's does. The framework calls none of the
287
+ * module's `onProcess`, `onAgentSpeech` and `onToolBatchComplete` again; it
288
+ * drops whole a response of its that hadn't been applied by then, with a line
289
+ * naming what the response asked for if it asked for anything; and it leaves
290
+ * its `gatherContext` answer out of a turn whose answers it hadn't merged by
291
+ * then, with a line if it held injections. After `removeModule()`, the result
292
+ * its `handleToolCall` returns for a call it was running when its `stop()`
293
+ * completed is still delivered while the framework runs. The framework's
294
+ * `stop()` closes its event queue before it stops any module, so through the
295
+ * framework's `stop()` that result is dropped, with the line any tool result
296
+ * arriving after the framework's `stop()` gets. A result it pushes through
297
+ * its context after its `stop()` completes is dropped, as its other pushes
298
+ * are. A module whose `stop()` fails stays registered, and its context keeps
299
+ * working.
300
+ * References the module already holds, to other modules or to its own
301
+ * timers, stay as they are.
262
302
  */
263
303
  export interface ModuleContext {
264
304
  /**
@@ -366,8 +406,7 @@ export interface ModuleContext {
366
406
  * and a tool round moves when it injects messages into a stream that
367
407
  * declares it carries them. So one that waited for its target's end (a
368
408
  * run's end, or a fork's last turn), one stored while it was idle and
369
- * never sent, one stored at a tool round that ended the turn, and one
370
- * injected into a stream that didn't declare it carries injections go to
409
+ * never sent, and one stored at a tool round that ended the turn go to
371
410
  * the primary. The framework's own
372
411
  * writes for a run or a fork are unchanged: they stay in its store,
373
412
  * unless they're notices the primary already takes in their place (a