@animalabs/agent-framework 0.7.4 → 0.9.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 (237) hide show
  1. package/dist/src/agent.d.ts +10 -0
  2. package/dist/src/agent.d.ts.map +1 -1
  3. package/dist/src/agent.js +33 -3
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/code-execution/index.d.ts +1 -1
  6. package/dist/src/code-execution/index.d.ts.map +1 -1
  7. package/dist/src/code-execution/index.js.map +1 -1
  8. package/dist/src/code-execution/py-runner.d.ts +23 -1
  9. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  10. package/dist/src/code-execution/py-runner.js +40 -4
  11. package/dist/src/code-execution/py-runner.js.map +1 -1
  12. package/dist/src/code-execution/runtime-py.d.ts +15 -2
  13. package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
  14. package/dist/src/code-execution/runtime-py.js +156 -7
  15. package/dist/src/code-execution/runtime-py.js.map +1 -1
  16. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  17. package/dist/src/code-execution/tool-definition.js +27 -2
  18. package/dist/src/code-execution/tool-definition.js.map +1 -1
  19. package/dist/src/framework.d.ts +234 -5
  20. package/dist/src/framework.d.ts.map +1 -1
  21. package/dist/src/framework.js +1818 -188
  22. package/dist/src/framework.js.map +1 -1
  23. package/dist/src/gate/event-gate.d.ts +11 -0
  24. package/dist/src/gate/event-gate.d.ts.map +1 -1
  25. package/dist/src/gate/event-gate.js +66 -0
  26. package/dist/src/gate/event-gate.js.map +1 -1
  27. package/dist/src/index.d.ts +1 -0
  28. package/dist/src/index.d.ts.map +1 -1
  29. package/dist/src/index.js +1 -0
  30. package/dist/src/index.js.map +1 -1
  31. package/dist/src/mcpl/capability-grant.d.ts +115 -0
  32. package/dist/src/mcpl/capability-grant.d.ts.map +1 -0
  33. package/dist/src/mcpl/capability-grant.js +291 -0
  34. package/dist/src/mcpl/capability-grant.js.map +1 -0
  35. package/dist/src/mcpl/capability-mask.d.ts +51 -0
  36. package/dist/src/mcpl/capability-mask.d.ts.map +1 -0
  37. package/dist/src/mcpl/capability-mask.js +190 -0
  38. package/dist/src/mcpl/capability-mask.js.map +1 -0
  39. package/dist/src/mcpl/channel-registry.d.ts +30 -2
  40. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  41. package/dist/src/mcpl/channel-registry.js +216 -43
  42. package/dist/src/mcpl/channel-registry.js.map +1 -1
  43. package/dist/src/mcpl/checkpoint-manager.d.ts +8 -3
  44. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  45. package/dist/src/mcpl/checkpoint-manager.js +28 -8
  46. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  47. package/dist/src/mcpl/errors.d.ts +6 -0
  48. package/dist/src/mcpl/errors.d.ts.map +1 -1
  49. package/dist/src/mcpl/errors.js +6 -0
  50. package/dist/src/mcpl/errors.js.map +1 -1
  51. package/dist/src/mcpl/feature-set-manager.d.ts +10 -1
  52. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  53. package/dist/src/mcpl/feature-set-manager.js +45 -4
  54. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  55. package/dist/src/mcpl/hook-orchestrator.d.ts +10 -14
  56. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  57. package/dist/src/mcpl/hook-orchestrator.js +58 -67
  58. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  59. package/dist/src/mcpl/index.d.ts +1 -1
  60. package/dist/src/mcpl/index.d.ts.map +1 -1
  61. package/dist/src/mcpl/index.js.map +1 -1
  62. package/dist/src/mcpl/prose-stream-router.d.ts +9 -0
  63. package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
  64. package/dist/src/mcpl/prose-stream-router.js +16 -0
  65. package/dist/src/mcpl/prose-stream-router.js.map +1 -1
  66. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  67. package/dist/src/mcpl/push-handler.js +20 -2
  68. package/dist/src/mcpl/push-handler.js.map +1 -1
  69. package/dist/src/mcpl/server-connection.d.ts +93 -9
  70. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  71. package/dist/src/mcpl/server-connection.js +231 -32
  72. package/dist/src/mcpl/server-connection.js.map +1 -1
  73. package/dist/src/mcpl/server-registry.d.ts.map +1 -1
  74. package/dist/src/mcpl/server-registry.js +4 -3
  75. package/dist/src/mcpl/server-registry.js.map +1 -1
  76. package/dist/src/mcpl/tags.d.ts +24 -0
  77. package/dist/src/mcpl/tags.d.ts.map +1 -0
  78. package/dist/src/mcpl/tags.js +52 -0
  79. package/dist/src/mcpl/tags.js.map +1 -0
  80. package/dist/src/mcpl/transport.d.ts +1 -1
  81. package/dist/src/mcpl/transport.d.ts.map +1 -1
  82. package/dist/src/mcpl/transport.js +35 -7
  83. package/dist/src/mcpl/transport.js.map +1 -1
  84. package/dist/src/mcpl/types.d.ts +169 -64
  85. package/dist/src/mcpl/types.d.ts.map +1 -1
  86. package/dist/src/mcpl/types.js +16 -2
  87. package/dist/src/mcpl/types.js.map +1 -1
  88. package/dist/src/module-registry.d.ts +12 -0
  89. package/dist/src/module-registry.d.ts.map +1 -1
  90. package/dist/src/module-registry.js +26 -0
  91. package/dist/src/module-registry.js.map +1 -1
  92. package/dist/src/modules/workspace/index.d.ts +20 -0
  93. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  94. package/dist/src/modules/workspace/index.js +48 -15
  95. package/dist/src/modules/workspace/index.js.map +1 -1
  96. package/dist/src/modules/workspace/sync.d.ts +4 -0
  97. package/dist/src/modules/workspace/sync.d.ts.map +1 -1
  98. package/dist/src/modules/workspace/sync.js +1 -1
  99. package/dist/src/modules/workspace/sync.js.map +1 -1
  100. package/dist/src/refusal-reactions.d.ts +27 -0
  101. package/dist/src/refusal-reactions.d.ts.map +1 -0
  102. package/dist/src/refusal-reactions.js +35 -0
  103. package/dist/src/refusal-reactions.js.map +1 -0
  104. package/dist/src/tool-result-history.d.ts +10 -0
  105. package/dist/src/tool-result-history.d.ts.map +1 -1
  106. package/dist/src/tool-result-history.js +16 -5
  107. package/dist/src/tool-result-history.js.map +1 -1
  108. package/dist/src/types/agent.d.ts +35 -0
  109. package/dist/src/types/agent.d.ts.map +1 -1
  110. package/dist/src/types/framework.d.ts +26 -0
  111. package/dist/src/types/framework.d.ts.map +1 -1
  112. package/dist/src/types/module.d.ts +22 -0
  113. package/dist/src/types/module.d.ts.map +1 -1
  114. package/dist/src/types/trace.d.ts +24 -0
  115. package/dist/src/types/trace.d.ts.map +1 -1
  116. package/dist/test/busy-agent-scheduler.test.js +1 -0
  117. package/dist/test/busy-agent-scheduler.test.js.map +1 -1
  118. package/dist/test/channel-registry-routing.test.js +38 -10
  119. package/dist/test/channel-registry-routing.test.js.map +1 -1
  120. package/dist/test/channel-registry-typing.test.js +10 -1
  121. package/dist/test/channel-registry-typing.test.js.map +1 -1
  122. package/dist/test/checkpoint-attribution.test.js +168 -8
  123. package/dist/test/checkpoint-attribution.test.js.map +1 -1
  124. package/dist/test/code-execution.test.js +327 -1
  125. package/dist/test/code-execution.test.js.map +1 -1
  126. package/dist/test/conversation-fork-injection-scoping.test.js +8 -3
  127. package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
  128. package/dist/test/deferred-wake-integrity.test.d.ts +29 -0
  129. package/dist/test/deferred-wake-integrity.test.d.ts.map +1 -0
  130. package/dist/test/deferred-wake-integrity.test.js +320 -0
  131. package/dist/test/deferred-wake-integrity.test.js.map +1 -0
  132. package/dist/test/event-gate-selfwake.test.d.ts +15 -0
  133. package/dist/test/event-gate-selfwake.test.d.ts.map +1 -0
  134. package/dist/test/event-gate-selfwake.test.js +125 -0
  135. package/dist/test/event-gate-selfwake.test.js.map +1 -0
  136. package/dist/test/machine-close-provenance.test.d.ts +2 -0
  137. package/dist/test/machine-close-provenance.test.d.ts.map +1 -0
  138. package/dist/test/machine-close-provenance.test.js +116 -0
  139. package/dist/test/machine-close-provenance.test.js.map +1 -0
  140. package/dist/test/mcpl-access-provider.test.d.ts +2 -0
  141. package/dist/test/mcpl-access-provider.test.d.ts.map +1 -0
  142. package/dist/test/mcpl-access-provider.test.js +20 -0
  143. package/dist/test/mcpl-access-provider.test.js.map +1 -0
  144. package/dist/test/mcpl-awareness-barrier.test.js +10 -0
  145. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
  146. package/dist/test/mcpl-capability-grant.test.d.ts +10 -0
  147. package/dist/test/mcpl-capability-grant.test.d.ts.map +1 -0
  148. package/dist/test/mcpl-capability-grant.test.js +109 -0
  149. package/dist/test/mcpl-capability-grant.test.js.map +1 -0
  150. package/dist/test/mcpl-capability-scoping.test.d.ts +2 -0
  151. package/dist/test/mcpl-capability-scoping.test.d.ts.map +1 -0
  152. package/dist/test/mcpl-capability-scoping.test.js +266 -0
  153. package/dist/test/mcpl-capability-scoping.test.js.map +1 -0
  154. package/dist/test/mcpl-gate-roundtrip.test.js +13 -0
  155. package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
  156. package/dist/test/mcpl-list-status.test.d.ts +2 -0
  157. package/dist/test/mcpl-list-status.test.d.ts.map +1 -0
  158. package/dist/test/mcpl-list-status.test.js +78 -0
  159. package/dist/test/mcpl-list-status.test.js.map +1 -0
  160. package/dist/test/mcpl-manifest-changed.test.d.ts +2 -0
  161. package/dist/test/mcpl-manifest-changed.test.d.ts.map +1 -0
  162. package/dist/test/mcpl-manifest-changed.test.js +177 -0
  163. package/dist/test/mcpl-manifest-changed.test.js.map +1 -0
  164. package/dist/test/mcpl-reconnect-events.test.js +27 -2
  165. package/dist/test/mcpl-reconnect-events.test.js.map +1 -1
  166. package/dist/test/mcpl-reconnect-reregistration.test.js +53 -17
  167. package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
  168. package/dist/test/mcpl-request-timeout.test.js +1 -1
  169. package/dist/test/mcpl-request-timeout.test.js.map +1 -1
  170. package/dist/test/module-context-integration.test.d.ts +18 -0
  171. package/dist/test/module-context-integration.test.d.ts.map +1 -0
  172. package/dist/test/module-context-integration.test.js +181 -0
  173. package/dist/test/module-context-integration.test.js.map +1 -0
  174. package/dist/test/physical-window-restart.test.d.ts +14 -0
  175. package/dist/test/physical-window-restart.test.d.ts.map +1 -0
  176. package/dist/test/physical-window-restart.test.js +222 -0
  177. package/dist/test/physical-window-restart.test.js.map +1 -0
  178. package/dist/test/present-while-acting.test.js +270 -0
  179. package/dist/test/present-while-acting.test.js.map +1 -1
  180. package/dist/test/refusal-reaction-baseline.test.d.ts +2 -0
  181. package/dist/test/refusal-reaction-baseline.test.d.ts.map +1 -0
  182. package/dist/test/refusal-reaction-baseline.test.js +74 -0
  183. package/dist/test/refusal-reaction-baseline.test.js.map +1 -0
  184. package/dist/test/state-scaling-e2e.test.js +7 -1
  185. package/dist/test/state-scaling-e2e.test.js.map +1 -1
  186. package/dist/test/state-scaling.test.js +9 -1
  187. package/dist/test/state-scaling.test.js.map +1 -1
  188. package/dist/test/tool-result-spill.test.d.ts +13 -0
  189. package/dist/test/tool-result-spill.test.d.ts.map +1 -0
  190. package/dist/test/tool-result-spill.test.js +463 -0
  191. package/dist/test/tool-result-spill.test.js.map +1 -0
  192. package/dist/test/trunk-channel-routing.test.js +45 -0
  193. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  194. package/dist/test/utils-surface.test.d.ts +2 -0
  195. package/dist/test/utils-surface.test.d.ts.map +1 -0
  196. package/dist/test/utils-surface.test.js +145 -0
  197. package/dist/test/utils-surface.test.js.map +1 -0
  198. package/dist/test/workspace-read-image.test.js +71 -0
  199. package/dist/test/workspace-read-image.test.js.map +1 -1
  200. package/dist/test/workspace-restore-order.test.d.ts +16 -0
  201. package/dist/test/workspace-restore-order.test.d.ts.map +1 -0
  202. package/dist/test/workspace-restore-order.test.js +140 -0
  203. package/dist/test/workspace-restore-order.test.js.map +1 -0
  204. package/dist/tsconfig.tsbuildinfo +1 -1
  205. package/package.json +4 -4
  206. package/src/agent.ts +37 -3
  207. package/src/code-execution/index.ts +1 -0
  208. package/src/code-execution/py-runner.ts +56 -4
  209. package/src/code-execution/runtime-py.ts +156 -7
  210. package/src/code-execution/tool-definition.ts +27 -2
  211. package/src/framework.ts +2080 -203
  212. package/src/gate/event-gate.ts +72 -0
  213. package/src/index.ts +1 -0
  214. package/src/mcpl/capability-grant.ts +319 -0
  215. package/src/mcpl/capability-mask.ts +228 -0
  216. package/src/mcpl/channel-registry.ts +265 -43
  217. package/src/mcpl/checkpoint-manager.ts +28 -8
  218. package/src/mcpl/errors.ts +7 -0
  219. package/src/mcpl/feature-set-manager.ts +50 -5
  220. package/src/mcpl/hook-orchestrator.ts +64 -86
  221. package/src/mcpl/index.ts +0 -2
  222. package/src/mcpl/prose-stream-router.ts +17 -0
  223. package/src/mcpl/push-handler.ts +18 -2
  224. package/src/mcpl/server-connection.ts +262 -34
  225. package/src/mcpl/server-registry.ts +4 -3
  226. package/src/mcpl/tags.ts +50 -0
  227. package/src/mcpl/transport.ts +37 -7
  228. package/src/mcpl/types.ts +187 -78
  229. package/src/module-registry.ts +34 -0
  230. package/src/modules/workspace/index.ts +49 -15
  231. package/src/modules/workspace/sync.ts +1 -1
  232. package/src/refusal-reactions.ts +37 -0
  233. package/src/tool-result-history.ts +17 -5
  234. package/src/types/agent.ts +36 -0
  235. package/src/types/framework.ts +27 -0
  236. package/src/types/module.ts +24 -0
  237. package/src/types/trace.ts +22 -0
package/src/mcpl/types.ts CHANGED
@@ -99,22 +99,32 @@ export interface McplResourceContent {
99
99
  * Parsed from the server's `initialize` response.
100
100
  */
101
101
  export interface McplCapabilities {
102
- /** MCPL protocol version (e.g., "0.4") */
102
+ /** MCPL protocol version (e.g., "0.5") */
103
103
  version: string;
104
104
 
105
+ /** §17.2 canonical content digest; absent = manifest fixed at initialize. */
106
+ revision?: string;
107
+
105
108
  /** Server supports push/event */
106
109
  pushEvents?: boolean;
107
110
 
108
- /** Context hook capabilities */
111
+ /**
112
+ * Context hooks — §5.1 recursive shape. Boolean `true` at any level is
113
+ * shorthand for every leaf beneath it. `afterInference` is REMOVED in
114
+ * 0.5.0 (replaced by metadata-only inference/lifecycle, §10.5).
115
+ */
109
116
  contextHooks?: {
110
- beforeInference?: boolean;
111
- afterInference?: boolean | { blocking: boolean };
117
+ beforeInference?: boolean | {
118
+ observe?: boolean;
119
+ inject?: { system?: boolean; beforeUser?: boolean; afterUser?: boolean };
120
+ };
112
121
  };
113
122
 
114
- /** Server-initiated inference capabilities */
115
- inferenceRequest?: {
116
- streaming?: boolean;
117
- };
123
+ /** Server-initiated inference. `true` = the capability with no refinements. */
124
+ inferenceRequest?: boolean | { streaming?: boolean };
125
+
126
+ /** Server consumes inference/lifecycle notifications (§10.5). */
127
+ inferenceLifecycle?: boolean;
118
128
 
119
129
  /** Server supports model/info requests */
120
130
  modelInfo?: boolean;
@@ -122,8 +132,8 @@ export interface McplCapabilities {
122
132
  /** Declared feature sets (keyed by feature set name) */
123
133
  featureSets?: Record<string, FeatureSetDeclaration>;
124
134
 
125
- /** Channel capabilities */
126
- channels?: McplChannelCapabilities;
135
+ /** Channel capabilities — §14.1 object of leaves, or `true` for all. */
136
+ channels?: boolean | McplChannelCapabilities;
127
137
  }
128
138
 
129
139
  /**
@@ -134,23 +144,28 @@ export interface McplHostCapabilities {
134
144
  version: string;
135
145
  pushEvents?: boolean;
136
146
  contextHooks?: {
137
- beforeInference?: boolean;
138
- afterInference?: boolean | { blocking: boolean };
139
- };
140
- inferenceRequest?: {
141
- streaming?: boolean;
147
+ beforeInference?: boolean | {
148
+ observe?: boolean;
149
+ inject?: { system?: boolean; beforeUser?: boolean; afterUser?: boolean };
150
+ };
142
151
  };
152
+ inferenceRequest?: boolean | { streaming?: boolean };
153
+ inferenceLifecycle?: boolean;
154
+ modelInfo?: boolean;
143
155
  featureSets?: boolean;
144
- channels?: McplChannelCapabilities;
156
+ channels?: boolean | McplChannelCapabilities;
145
157
  }
146
158
 
147
- /** Channel-specific capability flags. */
159
+ /** Channel capability leaves (§14.1). `observe` was the pre-0.5 name for
160
+ * what split into `incoming` — removed, not aliased. */
148
161
  export interface McplChannelCapabilities {
149
162
  register?: boolean;
150
- publish?: boolean;
151
- observe?: boolean;
152
163
  lifecycle?: boolean;
164
+ publish?: boolean;
165
+ incoming?: boolean;
153
166
  streaming?: boolean;
167
+ acknowledge?: boolean;
168
+ typing?: boolean;
154
169
  }
155
170
 
156
171
  // ============================================================================
@@ -198,6 +213,26 @@ export interface McplServerConfig {
198
213
  */
199
214
  token?: string;
200
215
 
216
+ /**
217
+ * Lazily resolves the connection credential at EVERY dial — first connect
218
+ * and each background reconnect — overriding `token` when it returns a
219
+ * value (null/throw falls back to `token`). Host-attached plumbing, never
220
+ * serialized: the point is that credentials stay out of the agent's
221
+ * context entirely (the agent asks for access by name; the host fetches
222
+ * something fresh each redial, which also makes short-lived credentials
223
+ * viable where a static `token` forced long ones).
224
+ */
225
+ accessProvider?: () => Promise<string | null>;
226
+
227
+ /**
228
+ * Host-owned authority for the `host/command` admin surface (undo / hide /
229
+ * unstick). DEFAULT FALSE: no §6.2 capability path exists for it, and the
230
+ * grant cannot confer it — only this config, decided by the operator, can
231
+ * (PR #79 review blocker 9). The one legitimate fleet user is
232
+ * discord-mcpl's slash-command relay.
233
+ */
234
+ allowHostCommands?: boolean;
235
+
201
236
  /** Feature sets to enable on connect */
202
237
  enabledFeatureSets?: string[];
203
238
 
@@ -222,6 +257,37 @@ export interface McplServerConfig {
222
257
  */
223
258
  disabledTools?: string[];
224
259
 
260
+ /**
261
+ * Capability allow-list: dotted paths as the server advertises them in its
262
+ * initialize response (`pushEvents`, `contextHooks.beforeInference`,
263
+ * `contextHooks.afterInference`, `inferenceRequest`, `modelInfo`,
264
+ * `channels`, `channels.streaming`, …). `*` matches one dot-segment
265
+ * (feature-set pattern rules), and a pattern matching a parent
266
+ * (`contextHooks`) covers every flag beneath it.
267
+ *
268
+ * If set, only advertised capabilities matching at least one pattern
269
+ * survive the handshake; everything else behaves as if the server had
270
+ * never advertised it. This is the host-side gate on hook fan-out: a
271
+ * server whose `contextHooks.afterInference` is masked never receives
272
+ * turn text, no matter what it self-advertises. Masked server-initiated
273
+ * capabilities (`pushEvents`, `inferenceRequest`, whole-`channels`) are
274
+ * also enforced inbound — the connection rejects those methods with
275
+ * CAPABILITY_DISABLED (-32002).
276
+ *
277
+ * `disabledCapabilities` takes precedence on conflict. `version` and
278
+ * `featureSets` are not maskable here — feature sets have their own
279
+ * enable/disable knobs above.
280
+ */
281
+ enabledCapabilities?: string[];
282
+
283
+ /**
284
+ * Capability deny-list (same paths and wildcard syntax as
285
+ * enabledCapabilities). Wins over enabledCapabilities on conflict.
286
+ * `disabledCapabilities: ['contextHooks.*']` is the one-line "this server
287
+ * gets no hook visibility" switch.
288
+ */
289
+ disabledCapabilities?: string[];
290
+
225
291
  /** Scope configurations per feature set */
226
292
  scopes?: Record<string, ScopeConfig>;
227
293
 
@@ -341,16 +407,51 @@ export interface FeatureSetDeclaration {
341
407
  * Spec Section 6.7.
342
408
  */
343
409
  export interface FeatureSetsUpdateParams {
344
- /** Feature sets to enable */
410
+ /**
411
+ * The effective capability grant (§5.4) — the sole normative allowlist.
412
+ * Full §6.2 paths. Absence of a path is denial. In the Request form an
413
+ * absent FIELD is a grant of nothing (§5.3 as pinned 2026-08-02), never
414
+ * "no change".
415
+ */
416
+ effectiveCapabilities?: string[];
417
+
418
+ /** Advertised-but-denied paths. Diagnostic only (§5.4) — MUST NOT
419
+ * participate in any authorization decision on either side. */
420
+ deniedCapabilities?: string[];
421
+
422
+ /** Feature sets to enable. Absent = no constraint (derivation governs);
423
+ * present = allowlist (§5.3 as pinned). */
345
424
  enabled?: string[];
346
425
 
347
426
  /** Feature sets to disable */
348
427
  disabled?: string[];
349
-
350
- /** Scope configurations per feature set */
351
- scopes?: Record<string, ScopeConfig>;
352
428
  }
353
429
 
430
+ /**
431
+ * featureSets/update result — the degradation receipt (§6.7). Consequence
432
+ * testimony, never policy authority: the host MUST NOT widen any grant in
433
+ * response to anything in here.
434
+ */
435
+ export type FeatureSetsUpdateResult =
436
+ | {
437
+ accepted: true;
438
+ /** Omitted when nothing degraded (§6.7). */
439
+ mode?: 'degraded';
440
+ unavailableFeatures?: Array<{
441
+ featureSet: string;
442
+ missingCapabilities: string[];
443
+ effect: string;
444
+ }>;
445
+ notes?: string[];
446
+ }
447
+ | {
448
+ accepted: false;
449
+ /** REQUIRED on refusal (§6.7): the server names which applies. */
450
+ fallback: 'mcp-only' | 'close';
451
+ missingCapabilities?: string[];
452
+ reason?: string;
453
+ };
454
+
354
455
  /**
355
456
  * featureSets/changed params (Server → Host, Notification).
356
457
  * Spec Section 6.7.
@@ -545,18 +646,48 @@ export interface PushEventResult {
545
646
  * Distinct from membrane's ModelInfo which tracks requested vs actual model.
546
647
  * Spec Section 10.1.
547
648
  */
649
+ /**
650
+ * inference/lifecycle params (Host → Server, Notification). SPEC §10.5.
651
+ *
652
+ * BEST-EFFORT: the host attempts exactly one terminal phase per `started`
653
+ * on every exit path it controls, but an unacknowledged Notification cannot
654
+ * guarantee delivery — consumers MUST dedupe by inferenceId and MUST retain
655
+ * a safety timeout. Carries no content: `modifiedResponse` and the blocking
656
+ * hook form are gone with context/afterInference.
657
+ */
658
+ export interface InferenceLifecycleParams {
659
+ inferenceId: string;
660
+ conversationId: string;
661
+ turnIndex: number;
662
+ phase: 'started' | 'completed' | 'aborted' | 'failed';
663
+ }
664
+
665
+ /** mcpl/manifestChanged params (Server → Host, Notification). §17.3. */
666
+ export interface ManifestChangedParams {
667
+ /** §17.2 canonical content digest. Untrusted; equality-only. */
668
+ revision: string;
669
+ /** Subset of the three §17.1 domains. A hint the host MAY ignore. */
670
+ domains: Array<'capabilities' | 'featureSets' | 'tagOntology'>;
671
+ }
672
+
548
673
  export interface McplModelInfo {
549
674
  /** Model identifier (e.g., "claude-opus-4-5-20251101") */
550
675
  id: string;
551
676
 
552
- /** Model vendor (e.g., "anthropic") */
553
- vendor: string;
677
+ /** Model vendor (e.g., "anthropic"). Omitted when the host has no
678
+ * truthful source — never fabricated. */
679
+ vendor?: string;
554
680
 
555
- /** Context window size in tokens */
556
- contextWindow: number;
681
+ /** Context window size in tokens. Omitted when unknown (a numeric field
682
+ * cannot express "unknown", and a fabricated 200000 was false for
683
+ * residents configured at 300k/600k — PR #79 review). The §12.2
684
+ * model/info RESULT requires it, which is why this host does not
685
+ * advertise modelInfo; hook params carry honesty-over-completeness. */
686
+ contextWindow?: number;
557
687
 
558
- /** Model capabilities (e.g., ["vision", "tools"]) */
559
- capabilities: string[];
688
+ /** Model capabilities (e.g., ["vision", "tools"]). Omitted when the
689
+ * host has no truthful source. */
690
+ capabilities?: string[];
560
691
  }
561
692
 
562
693
  /**
@@ -618,53 +749,6 @@ export interface BeforeInferenceResult {
618
749
  contextInjections: McplContextInjection[];
619
750
  }
620
751
 
621
- /**
622
- * context/afterInference params (Host → Server, Request or Notification).
623
- * Spec Section 10.5.
624
- */
625
- export interface AfterInferenceParams {
626
- /** Inference identifier (matches beforeInference) */
627
- inferenceId: string;
628
-
629
- /** Persistent across turns */
630
- conversationId: string;
631
-
632
- /** 0-indexed turn number */
633
- turnIndex: number;
634
-
635
- /** Original user input */
636
- userMessage: string | null;
637
-
638
- /** Generated assistant response */
639
- assistantMessage: string;
640
-
641
- /** Model metadata */
642
- model: McplModelInfo;
643
-
644
- /** Token usage */
645
- usage: {
646
- inputTokens: number;
647
- outputTokens: number;
648
- cacheCreationTokens?: number;
649
- cacheReadTokens?: number;
650
- };
651
- }
652
-
653
- /**
654
- * context/afterInference result (Server → Host, only for blocking hooks).
655
- * Spec Section 10.5.
656
- */
657
- export interface AfterInferenceResult {
658
- /** Feature set that provided this response */
659
- featureSet: string;
660
-
661
- /** Modified response text (replaces assistantMessage if present) */
662
- modifiedResponse?: string;
663
-
664
- /** Arbitrary metadata */
665
- metadata?: Record<string, unknown>;
666
- }
667
-
668
752
  // ============================================================================
669
753
  // Section 11 — Server-Initiated Inference
670
754
  // ============================================================================
@@ -850,7 +934,14 @@ export interface ChannelsRegisterParams {
850
934
  * channels/register result (Host → Server).
851
935
  */
852
936
  export interface ChannelsRegisterResult {
937
+ /** Legacy pre-0.5 field: ids accepted. Kept for un-migrated servers. */
853
938
  registered: string[];
939
+ /**
940
+ * §14.5 itemized results — one entry per SUBMITTED descriptor. 0.5
941
+ * servers key off this: a missing verdict is not a verdict, and a result
942
+ * without `results` is read by strict servers as accepting nothing.
943
+ */
944
+ results: Array<{ id: string; accepted: boolean; reason?: string }>;
854
945
  }
855
946
 
856
947
  /**
@@ -1022,6 +1113,8 @@ export interface ChannelIncomingMessageResult {
1022
1113
  messageId: string;
1023
1114
  accepted: boolean;
1024
1115
  conversationId?: string;
1116
+ /** §14.5: why a message was rejected (e.g. unknown/unregistered channel). */
1117
+ reason?: string;
1025
1118
  }
1026
1119
 
1027
1120
  // ============================================================================
@@ -1055,9 +1148,13 @@ export const McplMethod = {
1055
1148
  // Push events (Server → Host)
1056
1149
  PushEvent: 'push/event',
1057
1150
 
1058
- // Context hooks (Host → Server)
1151
+ // Context hooks (Host → Server). context/afterInference is gone with the
1152
+ // rest of its surface: removed from the spec in 0.5.0 (§10.5, replaced by
1153
+ // inference/lifecycle), never sent by the runtime, and not part of the
1154
+ // package's public API. Pre-0.5 SERVERS' wire handlers for it are their
1155
+ // own compatibility surface, retired on the fleet-on-0.5 evidence
1156
+ // schedule, not here.
1059
1157
  BeforeInference: 'context/beforeInference',
1060
- AfterInference: 'context/afterInference',
1061
1158
 
1062
1159
  // Server-initiated inference (Server → Host / Host → Server)
1063
1160
  InferenceRequest: 'inference/request',
@@ -1066,6 +1163,18 @@ export const McplMethod = {
1066
1163
  // Model info (Server → Host)
1067
1164
  ModelInfo: 'model/info',
1068
1165
 
1166
+ // Inference lifecycle (Host → Server, Notification) — §10.5, replaces
1167
+ // context/afterInference. Metadata only; BEST-EFFORT delivery.
1168
+ InferenceLifecycle: 'inference/lifecycle',
1169
+
1170
+ // Server manifest changes (§17). manifestChanged is S→H Notification —
1171
+ // an opaque revision plus changed domains, NO payload, deliberately
1172
+ // ungated (§17.3: gating it would silence exactly the servers whose
1173
+ // grants just narrowed). manifest is H→S Request returning the complete
1174
+ // current experimental.mcpl object, never a delta.
1175
+ ManifestChanged: 'mcpl/manifestChanged',
1176
+ Manifest: 'mcpl/manifest',
1177
+
1069
1178
  // Feature sets
1070
1179
  FeatureSetsUpdate: 'featureSets/update',
1071
1180
  FeatureSetsChanged: 'featureSets/changed',
@@ -75,6 +75,15 @@ export class ModuleRegistry {
75
75
  private pushEventFn: (event: ProcessEvent) => void;
76
76
  private onTraceFn: (listener: TraceEventListener) => () => void;
77
77
  private callToolFn: (call: ToolCall) => Promise<ToolResult>;
78
+ /** Framework ops-alert channel, surfaced to modules via
79
+ * ModuleContext.notifyOps. Readonly-public so ModuleContextImpl can
80
+ * delegate without another positional constructor parameter. */
81
+ readonly notifyOpsFn?: (
82
+ kind: string,
83
+ agentName: string,
84
+ message: string,
85
+ data?: Record<string, unknown>,
86
+ ) => void;
78
87
 
79
88
  constructor(
80
89
  store: JsStore,
@@ -89,6 +98,7 @@ export class ModuleRegistry {
89
98
  pushEvent: (event: ProcessEvent) => void;
90
99
  onTrace: (listener: TraceEventListener) => () => void;
91
100
  callTool: (call: ToolCall) => Promise<ToolResult>;
101
+ notifyOps?: (kind: string, agentName: string, message: string, data?: Record<string, unknown>) => void;
92
102
  }
93
103
  ) {
94
104
  this.store = store;
@@ -102,6 +112,7 @@ export class ModuleRegistry {
102
112
  this.pushEventFn = options.pushEvent;
103
113
  this.onTraceFn = options.onTrace;
104
114
  this.callToolFn = options.callTool;
115
+ this.notifyOpsFn = options.notifyOps;
105
116
  }
106
117
 
107
118
  /**
@@ -230,6 +241,25 @@ export class ModuleRegistry {
230
241
  return tools;
231
242
  }
232
243
 
244
+ /**
245
+ * All utilities from all modules, namespaced exactly like tools
246
+ * (`module--name`). See Module.getUtilities — these are deliberately kept
247
+ * out of the model-facing tool list and surfaced through the framework's
248
+ * `utils` meta-tool; dispatch goes through the same handleToolCall.
249
+ */
250
+ getAllUtilities(): ToolDefinition[] {
251
+ const utilities: ToolDefinition[] = [];
252
+ for (const module of this.modules.values()) {
253
+ for (const util of module.getUtilities?.() ?? []) {
254
+ utilities.push({
255
+ ...util,
256
+ name: `${module.name}--${util.name}`,
257
+ });
258
+ }
259
+ }
260
+ return utilities;
261
+ }
262
+
233
263
  /**
234
264
  * Handle a tool call by routing to the appropriate module.
235
265
  */
@@ -556,6 +586,10 @@ class ModuleContextImpl implements ModuleContext {
556
586
  return this.callToolFn(call);
557
587
  }
558
588
 
589
+ notifyOps(kind: string, agentName: string, message: string, data?: Record<string, unknown>): void {
590
+ this.registry.notifyOpsFn?.(kind, agentName, message, data);
591
+ }
592
+
559
593
  addMessage(
560
594
  participant: string,
561
595
  content: ContentBlock[],
@@ -34,7 +34,7 @@ import type {
34
34
  } from './types.js';
35
35
  import { WORKSPACE_FS_EVENT_TYPES, opToEventType } from './types.js';
36
36
  import { MountWatcher, type FsChange } from './watcher.js';
37
- import { syncFromFs, materializeToFs, hashContent, DEFAULT_MAX_FILE_SIZE, type ConflictInfo } from './sync.js';
37
+ import { syncFromFs, materializeToFs, hashContent, isBinary, DEFAULT_MAX_FILE_SIZE, type ConflictInfo } from './sync.js';
38
38
 
39
39
  export type {
40
40
  WorkspaceConfig,
@@ -525,6 +525,11 @@ export class WorkspaceModule implements Module {
525
525
  private config: WorkspaceConfig;
526
526
  private mounts = new Map<string, MountState>();
527
527
  private watchers = new Map<string, MountWatcher>();
528
+ /** Persisted state decoded in start(), held until mounts exist to apply it
529
+ * to — in the Host ordering start() runs before initStore() creates the
530
+ * mounts, so restoration must be second-callback-safe like watcher
531
+ * startup already is (issue #72). Cleared after application. */
532
+ private savedState: WorkspaceModuleState | null = null;
528
533
 
529
534
  constructor(config: WorkspaceConfig) {
530
535
  // Detect overlapping mount paths: if mount A contains mount B,
@@ -593,32 +598,47 @@ export class WorkspaceModule implements Module {
593
598
  this.mounts.set(mount.name, mountState);
594
599
  }
595
600
 
601
+ this.applySavedState();
596
602
  this.ensureRunning();
597
603
  }
598
604
 
599
605
  async start(ctx: ModuleContext): Promise<void> {
600
606
  this.ctx = ctx;
601
607
 
602
- // Restore persisted state if restarting
608
+ // Decode persisted state if restarting; application waits until mounts
609
+ // exist (see applySavedState) — in the Host ordering they don't yet.
603
610
  if (ctx.isRestart) {
604
- const saved = ctx.getState<WorkspaceModuleState>();
605
- if (saved) {
606
- for (const [name, meta] of Object.entries(saved.mounts)) {
607
- const mount = this.mounts.get(name);
608
- if (mount) {
609
- mount.lastMaterializedSeq = meta.lastMaterializedSeq;
610
- mount.lastMaterializedBranchId = meta.lastMaterializedBranchId ?? null;
611
- // watcherReadyAt intentionally not restored — each session must
612
- // observe its own watcher attach, otherwise a stale timestamp
613
- // would hide a new-session attach failure.
614
- }
615
- }
616
- }
611
+ this.savedState = ctx.getState<WorkspaceModuleState>() ?? null;
617
612
  }
618
613
 
614
+ this.applySavedState();
619
615
  this.ensureRunning();
620
616
  }
621
617
 
618
+ /**
619
+ * Apply persisted per-mount state once both halves of the lifecycle have
620
+ * happened. Like watcher startup, either callback may fire second, so both
621
+ * start() and initStore() call this; it runs to effect exactly once — the
622
+ * saved payload is cleared after application so stale persisted values can
623
+ * never overwrite newer runtime state (e.g. a materialization that already
624
+ * happened this session).
625
+ */
626
+ private applySavedState(): void {
627
+ if (!this.savedState || this.mounts.size === 0) return;
628
+
629
+ for (const [name, meta] of Object.entries(this.savedState.mounts)) {
630
+ const mount = this.mounts.get(name);
631
+ if (mount) {
632
+ mount.lastMaterializedSeq = meta.lastMaterializedSeq;
633
+ mount.lastMaterializedBranchId = meta.lastMaterializedBranchId ?? null;
634
+ // watcherReadyAt intentionally not restored — each session must
635
+ // observe its own watcher attach, otherwise a stale timestamp
636
+ // would hide a new-session attach failure.
637
+ }
638
+ }
639
+ this.savedState = null;
640
+ }
641
+
622
642
  /**
623
643
  * Start chokidar watchers and emit workspace:mounted events.
624
644
  * Idempotent: safe to call from both start() and initStore(), fires once
@@ -725,6 +745,15 @@ export class WorkspaceModule implements Module {
725
745
  this.ctx = null;
726
746
  }
727
747
 
748
+ /** Mount names + modes, for peer callers (framework spill/journal paths)
749
+ * that need a writable mount without reaching into private state. */
750
+ getMounts(): Array<{ name: string; mode: 'read-write' | 'read-only' }> {
751
+ return [...this.mounts.values()].map((m) => ({
752
+ name: m.config.name,
753
+ mode: m.config.mode === 'read-only' ? 'read-only' : 'read-write',
754
+ }));
755
+ }
756
+
728
757
  // ==========================================================================
729
758
  // Tool Definitions
730
759
  // ==========================================================================
@@ -1308,6 +1337,11 @@ export class WorkspaceModule implements Module {
1308
1337
  if (!fileStat.isFile() || fileStat.size > maxSize) return;
1309
1338
 
1310
1339
  const buffer = await readFile(absolutePath);
1340
+ // Binaries are never synced into the tree (mirrors syncFromFs): the old
1341
+ // utf-8 round-trip here silently replaced non-UTF-8 bytes with U+FFFD,
1342
+ // permanently mangling images in the blob store. Binary reads are served
1343
+ // straight from disk (readImageFromFilesystem / read_image fallback).
1344
+ if (isBinary(buffer)) return;
1311
1345
  const content = buffer.toString('utf-8');
1312
1346
  const blobHash = store.storeBlob(Buffer.from(content, 'utf-8'), 'text/plain');
1313
1347
 
@@ -40,7 +40,7 @@ export function hashContent(content: string | Buffer): string {
40
40
  /**
41
41
  * Check if content appears to be binary.
42
42
  */
43
- function isBinary(buffer: Buffer): boolean {
43
+ export function isBinary(buffer: Buffer): boolean {
44
44
  // Check for null bytes in first 8KB
45
45
  const check = buffer.subarray(0, 8192);
46
46
  for (let i = 0; i < check.length; i++) {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Refusal-reaction markers — the single source of truth for the emoji the
3
+ * framework places on a message when inference is refused (see
4
+ * AgentFramework.reactToRefusal), exported so host composition can derive
5
+ * a protective suppression baseline from the exact set the framework emits.
6
+ *
7
+ * These annotations are placed by the framework on the resident's account
8
+ * and must never re-enter a resident's context as reaction events — that is
9
+ * the self-amplifying loop behind the 8/3 Mythos incident. The Discord
10
+ * adapter suppresses them when its operator config or the host-injected
11
+ * `DISCORD_SUPPRESSED_REACTIONS_BASELINE` names them; keeping the emitted
12
+ * set and the exported baseline as one constant is what makes drift between
13
+ * "what we stamp" and "what we suppress" structurally impossible, rather
14
+ * than a promise kept in two files.
15
+ */
16
+
17
+ /** Refusal category → Discord reaction emoji. Unknown categories get the
18
+ * fallback marker. */
19
+ export const REFUSAL_REACTIONS: Readonly<Record<string, string>> = {
20
+ bio: '☣️',
21
+ chem: '🧪',
22
+ nuclear: '☢️',
23
+ cyber: '💻',
24
+ reasoning_extraction: '🧠',
25
+ };
26
+
27
+ /** Marker used when the refusal category has no dedicated emoji. */
28
+ export const REFUSAL_REACTION_FALLBACK = '🛑';
29
+
30
+ /** Every marker the framework can emit — the category map plus the
31
+ * fallback, deduplicated, in stable declaration order. This IS the
32
+ * protective baseline: host composition serializes it (comma-joined) into
33
+ * `DISCORD_SUPPRESSED_REACTIONS_BASELINE` for adapters that render
34
+ * reactions. */
35
+ export const REFUSAL_REACTION_BASELINE: readonly string[] = [
36
+ ...new Set([...Object.values(REFUSAL_REACTIONS), REFUSAL_REACTION_FALLBACK]),
37
+ ];
@@ -16,14 +16,26 @@
16
16
 
17
17
  import { safeSlice } from './safe-slice.js';
18
18
 
19
+ /**
20
+ * House-safe default inline cap (chars) for tool results, error results, and
21
+ * background-script wake payloads (issue #89: a resident should never eat a
22
+ * 42k blob they didn't ask for). Durable per-residence value comes from
23
+ * `FrameworkConfig.toolResultInlineMaxChars`; a temporary per-agent lift from
24
+ * agent_settings `tool_result_inline_max_chars`.
25
+ */
26
+ export const DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS = 5000;
27
+
19
28
  export function toolResultDataToHistoryString(data: unknown, maxChars?: number): string {
20
29
  const fromArray = tryHistoryStringFromContentArray(data);
21
30
  const str = fromArray ?? JSON.stringify(data);
22
- if (maxChars && str.length > maxChars) {
23
- return safeSlice(str, 0, maxChars)
24
- + '\n\n[truncated — original was ' + str.length + ' chars]';
25
- }
26
- return str;
31
+ return maxChars ? truncateForHistory(str, maxChars) : str;
32
+ }
33
+
34
+ /** Bounded copy of an arbitrary string with the standard truncation notice. */
35
+ export function truncateForHistory(str: string, maxChars: number): string {
36
+ if (str.length <= maxChars) return str;
37
+ return safeSlice(str, 0, maxChars)
38
+ + '\n\n[truncated — original was ' + str.length + ' chars]';
27
39
  }
28
40
 
29
41
  /**
@@ -48,6 +48,17 @@ export interface AgentConfig {
48
48
  * restarts with recompiled (compressed) context. Default: 150000. */
49
49
  maxStreamTokens?: number;
50
50
 
51
+ /**
52
+ * The model's PHYSICAL context window (provider hard cap, e.g. 200000).
53
+ * When set, the framework projects each continuation round's real size
54
+ * (cache-inclusive input of the prior round + the blocks about to be
55
+ * appended + reserve for response) and restarts the stream through a fresh
56
+ * compile instead of dispatching a request the provider will 400
57
+ * (issue #92: a legal compile can walk past the physical window mid-turn).
58
+ * Unset → no projection; only the maxStreamTokens restart applies.
59
+ */
60
+ physicalWindowTokens?: number;
61
+
51
62
  /** Per-agent context compile budget (input tokens). When unset, the
52
63
  * ContextManager's built-in default (100k) applies. */
53
64
  contextBudgetTokens?: number;
@@ -122,6 +133,31 @@ export interface AgentConfig {
122
133
  * a poison turn. Default: off (a refusal just surfaces a marker + reaction).
123
134
  */
124
135
  refusalHandling?: {
136
+ /**
137
+ * Same-model retries on a content-policy refusal, performed by MEMBRANE
138
+ * at the provider seam (passed through as `refusalRetries`). Default 0.
139
+ *
140
+ * Near the classifier's threshold a refusal is probabilistic rather than a
141
+ * property of the payload: identical bytes pass and refuse minutes apart
142
+ * (mythos, 2026-07-27 replay reps). So the first response is to ask again
143
+ * — no context surgery, no fallback model. Membrane replays the SAME
144
+ * request immediately and cache-warm, so only the discarded output tokens
145
+ * are real spend.
146
+ *
147
+ * Retrying in the framework instead was tried and removed: a requeue
148
+ * recompiles, so the attempts are correlated rather than fresh draws, and
149
+ * it cannot tell a streaming consumer to drop the abandoned attempt.
150
+ * Membrane announces each retry with a `retrying` stream event, which
151
+ * driveStream handles by discarding what was streamed.
152
+ *
153
+ * Retries are spent before the framework sees the refusal at all, so the
154
+ * escalation order is: membrane retries -> autoRewind (if on) -> the
155
+ * refusal reaction, which therefore means "the border is close" rather
156
+ * than firing on every near-threshold flip.
157
+ *
158
+ * Native tool mode only — see the XML-mode warning in Membrane.
159
+ */
160
+ retries?: number;
125
161
  /** Auto-rewind the triggering turn on refusal and retry. Default false. */
126
162
  autoRewind?: boolean;
127
163
  /** Max consecutive rewinds before giving up a turn. Default 3. */