@animalabs/agent-framework 0.22.1 → 0.22.2

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 (359) hide show
  1. package/README.md +1 -1
  2. package/dist/src/agent.d.ts +192 -19
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +404 -65
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/mcp-server-core.d.ts +41 -0
  7. package/dist/src/api/mcp-server-core.d.ts.map +1 -0
  8. package/dist/src/api/mcp-server-core.js +593 -0
  9. package/dist/src/api/mcp-server-core.js.map +1 -0
  10. package/dist/src/api/mcp-server.d.ts +2 -36
  11. package/dist/src/api/mcp-server.d.ts.map +1 -1
  12. package/dist/src/api/mcp-server.js +15 -595
  13. package/dist/src/api/mcp-server.js.map +1 -1
  14. package/dist/src/api/server.d.ts +9 -1
  15. package/dist/src/api/server.d.ts.map +1 -1
  16. package/dist/src/api/server.js +50 -26
  17. package/dist/src/api/server.js.map +1 -1
  18. package/dist/src/api/types.d.ts +9 -1
  19. package/dist/src/api/types.d.ts.map +1 -1
  20. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  21. package/dist/src/code-execution/py-runner.js.map +1 -1
  22. package/dist/src/framework.d.ts +393 -113
  23. package/dist/src/framework.d.ts.map +1 -1
  24. package/dist/src/framework.js +1748 -582
  25. package/dist/src/framework.js.map +1 -1
  26. package/dist/src/gate/event-gate.d.ts.map +1 -1
  27. package/dist/src/gate/gate-script.d.ts.map +1 -1
  28. package/dist/src/gate/gate-script.js.map +1 -1
  29. package/dist/src/index.d.ts +2 -2
  30. package/dist/src/index.d.ts.map +1 -1
  31. package/dist/src/index.js +2 -2
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
  34. package/dist/src/mcpl/channel-registry.d.ts +26 -7
  35. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  36. package/dist/src/mcpl/channel-registry.js +102 -22
  37. package/dist/src/mcpl/channel-registry.js.map +1 -1
  38. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  39. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  40. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  41. package/dist/src/mcpl/errors.js.map +1 -1
  42. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  43. package/dist/src/mcpl/feature-set-manager.js +4 -3
  44. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  45. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  46. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  47. package/dist/src/mcpl/log-value.d.ts +54 -0
  48. package/dist/src/mcpl/log-value.d.ts.map +1 -0
  49. package/dist/src/mcpl/log-value.js +109 -0
  50. package/dist/src/mcpl/log-value.js.map +1 -0
  51. package/dist/src/mcpl/prose-stream-router.d.ts +6 -6
  52. package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
  53. package/dist/src/mcpl/prose-stream-router.js +4 -4
  54. package/dist/src/mcpl/prose-stream-router.js.map +1 -1
  55. package/dist/src/mcpl/push-coalescer.d.ts +38 -3
  56. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  57. package/dist/src/mcpl/push-coalescer.js +81 -16
  58. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  59. package/dist/src/mcpl/push-handler.d.ts +26 -3
  60. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  61. package/dist/src/mcpl/push-handler.js +87 -23
  62. package/dist/src/mcpl/push-handler.js.map +1 -1
  63. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  64. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  65. package/dist/src/mcpl/references.d.ts +2 -2
  66. package/dist/src/mcpl/references.d.ts.map +1 -1
  67. package/dist/src/mcpl/references.js.map +1 -1
  68. package/dist/src/mcpl/refusal-lines.d.ts +37 -0
  69. package/dist/src/mcpl/refusal-lines.d.ts.map +1 -0
  70. package/dist/src/mcpl/refusal-lines.js +61 -0
  71. package/dist/src/mcpl/refusal-lines.js.map +1 -0
  72. package/dist/src/mcpl/scope-manager.d.ts.map +1 -1
  73. package/dist/src/mcpl/server-connection.d.ts +18 -2
  74. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  75. package/dist/src/mcpl/server-connection.js +66 -11
  76. package/dist/src/mcpl/server-connection.js.map +1 -1
  77. package/dist/src/mcpl/server-registry.d.ts.map +1 -1
  78. package/dist/src/mcpl/tool-classes.d.ts +1 -1
  79. package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
  80. package/dist/src/mcpl/tool-classes.js +3 -2
  81. package/dist/src/mcpl/tool-classes.js.map +1 -1
  82. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  83. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  84. package/dist/src/mcpl/transport.d.ts +23 -5
  85. package/dist/src/mcpl/transport.d.ts.map +1 -1
  86. package/dist/src/mcpl/transport.js +103 -10
  87. package/dist/src/mcpl/transport.js.map +1 -1
  88. package/dist/src/mcpl/types.d.ts +30 -26
  89. package/dist/src/mcpl/types.d.ts.map +1 -1
  90. package/dist/src/mcpl/types.js.map +1 -1
  91. package/dist/src/module-registry.d.ts.map +1 -1
  92. package/dist/src/module-registry.js.map +1 -1
  93. package/dist/src/modules/api/index.d.ts.map +1 -1
  94. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
  95. package/dist/src/modules/discord/discord-js-client.js.map +1 -1
  96. package/dist/src/modules/discord/index.d.ts.map +1 -1
  97. package/dist/src/modules/discord/index.js +4 -2
  98. package/dist/src/modules/discord/index.js.map +1 -1
  99. package/dist/src/modules/health/index.d.ts.map +1 -1
  100. package/dist/src/modules/health/index.js.map +1 -1
  101. package/dist/src/modules/history/index.d.ts.map +1 -1
  102. package/dist/src/modules/history/index.js.map +1 -1
  103. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  104. package/dist/src/modules/history/semantic.js.map +1 -1
  105. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  106. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  107. package/dist/src/modules/workspace/disk-agreement.d.ts +22 -4
  108. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  109. package/dist/src/modules/workspace/disk-agreement.js +63 -25
  110. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  111. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  112. package/dist/src/modules/workspace/index.d.ts +8 -4
  113. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  114. package/dist/src/modules/workspace/index.js +39 -12
  115. package/dist/src/modules/workspace/index.js.map +1 -1
  116. package/dist/src/modules/workspace/observe.d.ts +43 -8
  117. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  118. package/dist/src/modules/workspace/observe.js +264 -39
  119. package/dist/src/modules/workspace/observe.js.map +1 -1
  120. package/dist/src/modules/workspace/reconcile.d.ts +17 -1
  121. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  122. package/dist/src/modules/workspace/reconcile.js +57 -14
  123. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  124. package/dist/src/modules/workspace/types.d.ts +27 -2
  125. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  126. package/dist/src/modules/workspace/types.js.map +1 -1
  127. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  128. package/dist/src/modules/workspace/watcher.js.map +1 -1
  129. package/dist/src/operator-log.d.ts.map +1 -1
  130. package/dist/src/operator-log.js.map +1 -1
  131. package/dist/src/provider-waits.d.ts.map +1 -1
  132. package/dist/src/provider-waits.js.map +1 -1
  133. package/dist/src/queue.d.ts.map +1 -1
  134. package/dist/src/record-journal.d.ts.map +1 -1
  135. package/dist/src/record-journal.js.map +1 -1
  136. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  137. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  138. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  139. package/dist/src/recovery/offline-branch.js +29 -24
  140. package/dist/src/recovery/offline-branch.js.map +1 -1
  141. package/dist/src/recovery/recover-cli.js.map +1 -1
  142. package/dist/src/runtime/liveness-watchdog.d.ts.map +1 -1
  143. package/dist/src/runtime/liveness-watchdog.js.map +1 -1
  144. package/dist/src/shutdown.d.ts.map +1 -1
  145. package/dist/src/shutdown.js.map +1 -1
  146. package/dist/src/stored-removal.d.ts +73 -0
  147. package/dist/src/stored-removal.d.ts.map +1 -0
  148. package/dist/src/stored-removal.js +100 -0
  149. package/dist/src/stored-removal.js.map +1 -0
  150. package/dist/src/thinking-token-stamp.js.map +1 -1
  151. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  152. package/dist/src/tool-image-ledger.js.map +1 -1
  153. package/dist/src/tool-presentation.d.ts.map +1 -1
  154. package/dist/src/tool-presentation.js.map +1 -1
  155. package/dist/src/tool-result-guard.d.ts.map +1 -1
  156. package/dist/src/tool-result-guard.js.map +1 -1
  157. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  158. package/dist/src/tune-out/coordinator.js.map +1 -1
  159. package/dist/src/tune-out/tools.d.ts +1 -1
  160. package/dist/src/tune-out/tools.d.ts.map +1 -1
  161. package/dist/src/types/agent.d.ts +42 -6
  162. package/dist/src/types/agent.d.ts.map +1 -1
  163. package/dist/src/types/framework.d.ts +10 -0
  164. package/dist/src/types/framework.d.ts.map +1 -1
  165. package/dist/src/types/module.d.ts +8 -4
  166. package/dist/src/types/module.d.ts.map +1 -1
  167. package/dist/src/types/trace.d.ts +61 -10
  168. package/dist/src/types/trace.d.ts.map +1 -1
  169. package/dist/src/usage/usage-tracker.d.ts.map +1 -1
  170. package/dist/test/agent-runtime-settings.test.js +1139 -3
  171. package/dist/test/agent-runtime-settings.test.js.map +1 -1
  172. package/dist/test/batched-wake-locus.test.js.map +1 -1
  173. package/dist/test/channel-label-history.test.js.map +1 -1
  174. package/dist/test/code-execution.test.js.map +1 -1
  175. package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
  176. package/dist/test/conversation-router.test.js.map +1 -1
  177. package/dist/test/conversation-routing.test.js +240 -6
  178. package/dist/test/conversation-routing.test.js.map +1 -1
  179. package/dist/test/discord-awareness-outbox.test.js +104 -0
  180. package/dist/test/discord-awareness-outbox.test.js.map +1 -1
  181. package/dist/test/discord-typing-keepalive.test.d.ts +2 -0
  182. package/dist/test/discord-typing-keepalive.test.d.ts.map +1 -0
  183. package/dist/test/discord-typing-keepalive.test.js +77 -0
  184. package/dist/test/discord-typing-keepalive.test.js.map +1 -0
  185. package/dist/test/ephemeral-settle.test.js.map +1 -1
  186. package/dist/test/event-gate-passthrough.test.js.map +1 -1
  187. package/dist/test/event-gate-policy-mutation.test.js.map +1 -1
  188. package/dist/test/event-gate-rate-limit.test.js.map +1 -1
  189. package/dist/test/event-gate-script.test.js.map +1 -1
  190. package/dist/test/event-gate-selfwake.test.js.map +1 -1
  191. package/dist/test/event-gate-tags.test.js.map +1 -1
  192. package/dist/test/event-gate.test.js.map +1 -1
  193. package/dist/test/explicit-prose-routing.test.js +363 -42
  194. package/dist/test/explicit-prose-routing.test.js.map +1 -1
  195. package/dist/test/framework.test.js.map +1 -1
  196. package/dist/test/helpers/coalescing-fixture.d.ts +2 -0
  197. package/dist/test/helpers/coalescing-fixture.d.ts.map +1 -1
  198. package/dist/test/helpers/coalescing-fixture.js +8 -2
  199. package/dist/test/helpers/coalescing-fixture.js.map +1 -1
  200. package/dist/test/helpers/mock-membrane.d.ts.map +1 -1
  201. package/dist/test/helpers/mock-membrane.js.map +1 -1
  202. package/dist/test/hide-then-turn.test.d.ts +2 -0
  203. package/dist/test/hide-then-turn.test.d.ts.map +1 -0
  204. package/dist/test/hide-then-turn.test.js +101 -0
  205. package/dist/test/hide-then-turn.test.js.map +1 -0
  206. package/dist/test/history-module-overview.test.js.map +1 -1
  207. package/dist/test/history-module-semantic.test.js.map +1 -1
  208. package/dist/test/history-module.test.js.map +1 -1
  209. package/dist/test/history-semantic-review-findings.test.js.map +1 -1
  210. package/dist/test/host-quiesce.test.js +125 -0
  211. package/dist/test/host-quiesce.test.js.map +1 -1
  212. package/dist/test/inference-exhaustion-rewind.test.js +81 -3
  213. package/dist/test/inference-exhaustion-rewind.test.js.map +1 -1
  214. package/dist/test/inference-failure-bounds.test.js +1 -0
  215. package/dist/test/inference-failure-bounds.test.js.map +1 -1
  216. package/dist/test/kv-unified-open-flight-retry.test.js.map +1 -1
  217. package/dist/test/liveness-watchdog.test.js.map +1 -1
  218. package/dist/test/maintenance-timer.test.js +1564 -2
  219. package/dist/test/maintenance-timer.test.js.map +1 -1
  220. package/dist/test/mcp-server-bin.test.d.ts +2 -0
  221. package/dist/test/mcp-server-bin.test.d.ts.map +1 -0
  222. package/dist/test/mcp-server-bin.test.js +142 -0
  223. package/dist/test/mcp-server-bin.test.js.map +1 -0
  224. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
  225. package/dist/test/mcpl-child-env.test.js +1 -1
  226. package/dist/test/mcpl-child-env.test.js.map +1 -1
  227. package/dist/test/mcpl-coalesced-content-parity.test.d.ts +2 -0
  228. package/dist/test/mcpl-coalesced-content-parity.test.d.ts.map +1 -0
  229. package/dist/test/mcpl-coalesced-content-parity.test.js +532 -0
  230. package/dist/test/mcpl-coalesced-content-parity.test.js.map +1 -0
  231. package/dist/test/mcpl-coalescing.test.js.map +1 -1
  232. package/dist/test/mcpl-config-diagnostics.test.js.map +1 -1
  233. package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
  234. package/dist/test/mcpl-manifest-changed.test.js.map +1 -1
  235. package/dist/test/mcpl-reconnect-events.test.js.map +1 -1
  236. package/dist/test/mcpl-reconnect-reregistration.test.js +2 -0
  237. package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
  238. package/dist/test/mcpl-reference-fetcher.test.js.map +1 -1
  239. package/dist/test/mcpl-reference-fields.test.js.map +1 -1
  240. package/dist/test/mcpl-rejection-log-lines.test.d.ts +2 -0
  241. package/dist/test/mcpl-rejection-log-lines.test.d.ts.map +1 -0
  242. package/dist/test/mcpl-rejection-log-lines.test.js +349 -0
  243. package/dist/test/mcpl-rejection-log-lines.test.js.map +1 -0
  244. package/dist/test/mcpl-stderr-tail.test.d.ts +2 -0
  245. package/dist/test/mcpl-stderr-tail.test.d.ts.map +1 -0
  246. package/dist/test/mcpl-stderr-tail.test.js +210 -0
  247. package/dist/test/mcpl-stderr-tail.test.js.map +1 -0
  248. package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +1 -1
  249. package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -1
  250. package/dist/test/mcpl-tool-lifecycle-script.test.js.map +1 -1
  251. package/dist/test/mcpl-tool-lifecycle.test.js.map +1 -1
  252. package/dist/test/mcpl-transport-close.test.d.ts +2 -0
  253. package/dist/test/mcpl-transport-close.test.d.ts.map +1 -0
  254. package/dist/test/mcpl-transport-close.test.js +373 -0
  255. package/dist/test/mcpl-transport-close.test.js.map +1 -0
  256. package/dist/test/mcpl-transport-spawn.test.d.ts +2 -0
  257. package/dist/test/mcpl-transport-spawn.test.d.ts.map +1 -0
  258. package/dist/test/mcpl-transport-spawn.test.js +58 -0
  259. package/dist/test/mcpl-transport-spawn.test.js.map +1 -0
  260. package/dist/test/mcpl-ws-transport.test.js +1 -0
  261. package/dist/test/mcpl-ws-transport.test.js.map +1 -1
  262. package/dist/test/offline-recovery-branch.test.js +344 -0
  263. package/dist/test/offline-recovery-branch.test.js.map +1 -1
  264. package/dist/test/operator-surgery.test.js +197 -0
  265. package/dist/test/operator-surgery.test.js.map +1 -1
  266. package/dist/test/overbudget-drain-breaker.test.js +1 -0
  267. package/dist/test/overbudget-drain-breaker.test.js.map +1 -1
  268. package/dist/test/pack-installs-alone.test.d.ts +2 -0
  269. package/dist/test/pack-installs-alone.test.d.ts.map +1 -0
  270. package/dist/test/pack-installs-alone.test.js +284 -0
  271. package/dist/test/pack-installs-alone.test.js.map +1 -0
  272. package/dist/test/physical-window-restart.test.d.ts +6 -4
  273. package/dist/test/physical-window-restart.test.d.ts.map +1 -1
  274. package/dist/test/physical-window-restart.test.js +275 -10
  275. package/dist/test/physical-window-restart.test.js.map +1 -1
  276. package/dist/test/placed-reaction-baseline.test.js.map +1 -1
  277. package/dist/test/present-while-acting.test.js +949 -6
  278. package/dist/test/present-while-acting.test.js.map +1 -1
  279. package/dist/test/provider-acceleration-cooldown.test.js.map +1 -1
  280. package/dist/test/provider-waits.test.js +67 -4
  281. package/dist/test/provider-waits.test.js.map +1 -1
  282. package/dist/test/puppet-tool-call.test.js +2 -0
  283. package/dist/test/puppet-tool-call.test.js.map +1 -1
  284. package/dist/test/reaction-baseline-platform.test.js.map +1 -1
  285. package/dist/test/record-journal.test.js.map +1 -1
  286. package/dist/test/refusal-rewind.test.js +9 -0
  287. package/dist/test/refusal-rewind.test.js.map +1 -1
  288. package/dist/test/refusal-single-call.test.js.map +1 -1
  289. package/dist/test/release-bump.test.d.ts +2 -0
  290. package/dist/test/release-bump.test.d.ts.map +1 -0
  291. package/dist/test/release-bump.test.js +248 -0
  292. package/dist/test/release-bump.test.js.map +1 -0
  293. package/dist/test/release-changelog.test.js.map +1 -1
  294. package/dist/test/release-holds.test.d.ts +2 -0
  295. package/dist/test/release-holds.test.d.ts.map +1 -0
  296. package/dist/test/release-holds.test.js +456 -0
  297. package/dist/test/release-holds.test.js.map +1 -0
  298. package/dist/test/same-round-think-policy.test.js.map +1 -1
  299. package/dist/test/shutdown-errors.test.js.map +1 -1
  300. package/dist/test/silent-heartbeat-rows.test.js.map +1 -1
  301. package/dist/test/stop-with-host-operations.test.d.ts +20 -0
  302. package/dist/test/stop-with-host-operations.test.d.ts.map +1 -0
  303. package/dist/test/stop-with-host-operations.test.js +1174 -0
  304. package/dist/test/stop-with-host-operations.test.js.map +1 -0
  305. package/dist/test/stop-with-work-in-flight.test.js +162 -50
  306. package/dist/test/stop-with-work-in-flight.test.js.map +1 -1
  307. package/dist/test/surgery-marks-receipt.test.js.map +1 -1
  308. package/dist/test/tool-batch-hook.test.js.map +1 -1
  309. package/dist/test/tool-result-guard.test.js +131 -9
  310. package/dist/test/tool-result-guard.test.js.map +1 -1
  311. package/dist/test/tool-result-spill.test.js.map +1 -1
  312. package/dist/test/tool-wrapper-prose-guard-integration.test.js.map +1 -1
  313. package/dist/test/trunk-channel-routing.test.js +330 -7
  314. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  315. package/dist/test/tune-out-e2e.test.js.map +1 -1
  316. package/dist/test/unconvertible-tool-result.test.js.map +1 -1
  317. package/dist/test/unsent-compiles.test.d.ts +2 -0
  318. package/dist/test/unsent-compiles.test.d.ts.map +1 -0
  319. package/dist/test/unsent-compiles.test.js +139 -0
  320. package/dist/test/unsent-compiles.test.js.map +1 -0
  321. package/dist/test/workspace-branch-continuation.test.js.map +1 -1
  322. package/dist/test/workspace-materialize-freshness.test.js.map +1 -1
  323. package/dist/test/workspace-read-text.test.js.map +1 -1
  324. package/dist/test/workspace-three-way.test.js +907 -7
  325. package/dist/test/workspace-three-way.test.js.map +1 -1
  326. package/dist/test/workspace-watcher.test.js.map +1 -1
  327. package/dist/test/xml-round-persist.test.js.map +1 -1
  328. package/dist/tsconfig.tsbuildinfo +1 -1
  329. package/package.json +8 -6
  330. package/src/agent.ts +411 -66
  331. package/src/api/mcp-server-core.ts +681 -0
  332. package/src/api/mcp-server.ts +16 -685
  333. package/src/api/server.ts +51 -29
  334. package/src/api/types.ts +9 -1
  335. package/src/framework.ts +1899 -641
  336. package/src/index.ts +2 -2
  337. package/src/mcpl/channel-registry.ts +126 -31
  338. package/src/mcpl/feature-set-manager.ts +4 -3
  339. package/src/mcpl/log-value.ts +116 -0
  340. package/src/mcpl/prose-stream-router.ts +6 -6
  341. package/src/mcpl/push-coalescer.ts +76 -12
  342. package/src/mcpl/push-handler.ts +95 -23
  343. package/src/mcpl/refusal-lines.ts +59 -0
  344. package/src/mcpl/server-connection.ts +70 -11
  345. package/src/mcpl/tool-classes.ts +3 -2
  346. package/src/mcpl/transport.ts +105 -14
  347. package/src/mcpl/types.ts +5 -1
  348. package/src/modules/discord/index.ts +4 -2
  349. package/src/modules/workspace/disk-agreement.ts +74 -18
  350. package/src/modules/workspace/index.ts +41 -11
  351. package/src/modules/workspace/observe.ts +265 -39
  352. package/src/modules/workspace/reconcile.ts +71 -14
  353. package/src/modules/workspace/types.ts +26 -1
  354. package/src/recovery/offline-branch.ts +32 -28
  355. package/src/stored-removal.ts +126 -0
  356. package/src/types/agent.ts +42 -6
  357. package/src/types/framework.ts +10 -0
  358. package/src/types/module.ts +8 -4
  359. package/src/types/trace.ts +62 -11
@@ -15,6 +15,7 @@ import { join } from 'node:path';
15
15
  import { AgentFramework } from '../src/index.js';
16
16
  import { MockMembrane, createMockResponse } from './helpers/mock-membrane.js';
17
17
  import { ProseStreamRouter } from '../src/mcpl/prose-stream-router.js';
18
+ import { MembraneError } from '@animalabs/membrane';
18
19
  class ToolboxModule {
19
20
  name = 'robot';
20
21
  framework = null;
@@ -289,6 +290,11 @@ describe('explicit prose routing', () => {
289
290
  assert.equal(turns, 1, "the round's ' !' starts no continuation after skip_reply ended the turn");
290
291
  await framework.stop();
291
292
  });
293
+ /** A turn's routing state, as the framework makes a fresh one. */
294
+ const routingState = (fields = {}) => ({
295
+ destination: null, hybridHold: false, continuation: false, deliveries: [], suppressed: 0,
296
+ engaged: new Set(), arrivals: [], ...fields,
297
+ });
292
298
  async function within(promise, label, ms = 5000) {
293
299
  let timer;
294
300
  try {
@@ -425,8 +431,8 @@ describe('explicit prose routing', () => {
425
431
  const routed = stubRegistry(framework);
426
432
  const gateway = framework;
427
433
  const agent = framework.getAgent('assistant');
428
- const earlier = { destination: null, hybridHold: false, logical: { continuation: false, deliveries: [], suppressed: 0 } };
429
- const next = { ...earlier, logical: { continuation: false, deliveries: [], suppressed: 0 } };
434
+ const earlier = routingState();
435
+ const next = routingState();
430
436
  gateway.turnRouting.set('assistant', earlier);
431
437
  const registry = framework.channelRegistry;
432
438
  const route = registry.routeSpeech;
@@ -485,15 +491,15 @@ describe('explicit prose routing', () => {
485
491
  await withQueuedTurns('explicit', '>>#alpha ! late', [
486
492
  createMockResponse([{ type: 'text', text: '>>#cafe next' }]),
487
493
  ], async ({ earlier, next, turns, release, drain }) => {
488
- assert.equal(earlier.logical.continuation, false, 'the continuation has not run before release');
494
+ assert.equal(earlier.continuation, false, 'the continuation has not run before release');
489
495
  release();
490
496
  for (const end = Date.now() + 5000; turns() < 3 && Date.now() < end;) {
491
497
  await new Promise((resolve) => setTimeout(resolve, 5));
492
498
  }
493
499
  await drain();
494
500
  assert.equal(turns(), 3, 'the continuation starts its own turn, once');
495
- assert.equal(earlier.logical.continuation, false, "the earlier turn's request was taken from its own object");
496
- assert.equal(next.logical.continuation, false, 'the next turn never receives that continuation');
501
+ assert.equal(earlier.continuation, false, "the earlier turn's request was taken from its own object");
502
+ assert.equal(next.continuation, false, 'the next turn never receives that continuation');
497
503
  });
498
504
  });
499
505
  for (const mode of ['explicit', 'hybrid']) {
@@ -613,11 +619,11 @@ describe('explicit prose routing', () => {
613
619
  // its queued delivery runs; the turn goes on in a re-inference, which
614
620
  // starts a new stream with a fresh routing state while the earlier
615
621
  // stream's logical turn is still open.
616
- const earlier = { destination: null, hybridHold: false, logical: { continuation: when === 'already set', deliveries: [], suppressed: 0 } };
622
+ const earlier = routingState({ continuation: when === 'already set', locus: 'world:commons' });
617
623
  internals.turnRouting.set('assistant', earlier);
618
- internals.openLogical.set('assistant', earlier.logical);
624
+ internals.openLogical.set('assistant', earlier);
619
625
  if (when === 'set late') {
620
- internals.proseSpeechChains.set('assistant', held.then(() => { earlier.logical.continuation = true; }));
626
+ internals.proseSpeechChains.set('assistant', held.then(() => { earlier.continuation = true; }));
621
627
  }
622
628
  internals.pendingRequests.push({ agentName: 'assistant', reason: 'tool_results_ready', source: 'framework', timestamp: Date.now() });
623
629
  const running = framework.runUntilIdle();
@@ -626,7 +632,7 @@ describe('explicit prose routing', () => {
626
632
  await new Promise((resolve) => setTimeout(resolve, 5));
627
633
  }
628
634
  assert.equal(turns, 1, 'the re-inference has started');
629
- assert.equal(earlier.logical.continuation, false, 'the earlier delivery has not run as the re-inference starts');
635
+ assert.equal(earlier.continuation, false, 'the earlier delivery has not run as the re-inference starts');
630
636
  release();
631
637
  }
632
638
  await within(running, 'the re-inference to settle');
@@ -636,7 +642,7 @@ describe('explicit prose routing', () => {
636
642
  }
637
643
  assert.deepEqual(routed.map((r) => r.text.trim()), ['carrying on']);
638
644
  assert.equal(turns, 2, 'the re-inference completes, then the continuation it carried starts');
639
- assert.equal(earlier.logical.continuation, false, "the check took the earlier stream's flag");
645
+ assert.equal(earlier.continuation, false, "the check took the earlier stream's flag");
640
646
  }
641
647
  finally {
642
648
  release();
@@ -659,18 +665,19 @@ describe('explicit prose routing', () => {
659
665
  try {
660
666
  // The earlier stream left its tool results pending, its logical turn
661
667
  // still open; its delivery lands only after the re-inference has
662
- // begun, into the logical turn the two share.
663
- const earlier = { destination: null, hybridHold: false, logical: { continuation: false, deliveries: [], suppressed: 0 } };
668
+ // begun, into the logical turn the two share. The turn froze the
669
+ // registry's locus when it opened.
670
+ const earlier = routingState({ locus: 'world:commons' });
664
671
  internals.turnRouting.set('assistant', earlier);
665
- internals.openLogical.set('assistant', earlier.logical);
666
- internals.proseSpeechChains.set('assistant', held.then(() => { earlier.logical.deliveries.push('chan-alpha'); }));
672
+ internals.openLogical.set('assistant', earlier);
673
+ internals.proseSpeechChains.set('assistant', held.then(() => { earlier.deliveries.push('chan-alpha'); }));
667
674
  internals.pendingRequests.push({ agentName: 'assistant', reason: 'tool_results_ready', source: 'framework', timestamp: Date.now() });
668
675
  const running = framework.runUntilIdle();
669
676
  for (const end = Date.now() + 5000; turns < 1 && Date.now() < end;) {
670
677
  await new Promise((resolve) => setTimeout(resolve, 5));
671
678
  }
672
679
  assert.equal(turns, 1, 'the re-inference has started');
673
- assert.deepEqual(earlier.logical.deliveries, [], 'the earlier delivery has not landed yet');
680
+ assert.deepEqual(earlier.deliveries, [], 'the earlier delivery has not landed yet');
674
681
  release();
675
682
  await within(running, 'the re-inference to settle');
676
683
  assert.deepEqual(routed.map((r) => r.text.trim()), ['carrying on']);
@@ -778,7 +785,7 @@ describe('explicit prose routing', () => {
778
785
  stubRegistry(framework);
779
786
  const agent = framework.getAgent('assistant');
780
787
  const internals = framework;
781
- const origin = internals.routingOf('assistant').logical;
788
+ const origin = internals.routingOf('assistant');
782
789
  origin.continuation = true;
783
790
  origin.deliveries.push('chan-alpha');
784
791
  // The turn that ran the tools is open, as its fresh start left it.
@@ -807,8 +814,8 @@ describe('explicit prose routing', () => {
807
814
  it("a settle closes the open logical turn only when it is the settling stream's own, and stop() clears open turns", async () => {
808
815
  const framework = await createFramework('hybrid');
809
816
  const internals = framework;
810
- const open = { continuation: true, deliveries: [], suppressed: 0 };
811
- const stale = { continuation: false, deliveries: [], suppressed: 0 };
817
+ const open = routingState({ continuation: true });
818
+ const stale = routingState();
812
819
  internals.openLogical.set('assistant', open);
813
820
  // A settle from a stream a newer one replaced passes its own logical
814
821
  // turn, or none; neither closes the open one.
@@ -818,10 +825,12 @@ describe('explicit prose routing', () => {
818
825
  internals.settleAgent('assistant', { stopReason: 'completed', speech: '' }, open);
819
826
  assert.equal(internals.openLogical.has('assistant'), false, "the turn's own settle closes it");
820
827
  internals.openLogical.set('assistant', open);
828
+ internals.turnRouting.set('assistant', open);
821
829
  await framework.stop();
822
830
  assert.equal(internals.openLogical.size, 0, 'stop() clears every open logical turn');
831
+ assert.equal(internals.turnRouting.get('assistant'), open, 'and keeps the live routing state, for a resume that continues its turn');
823
832
  });
824
- it('a silent start leaves an open logical turn to the next start that routes prose', async () => {
833
+ it('a silent start neither takes nor disturbs an open logical turn, and leaves it to the next start that routes prose', async () => {
825
834
  membrane.pushResponse(createMockResponse([{ type: 'text', text: 'quiet' }]));
826
835
  const framework = await createFramework('hybrid');
827
836
  stubRegistry(framework);
@@ -832,16 +841,23 @@ describe('explicit prose routing', () => {
832
841
  });
833
842
  const internals = framework;
834
843
  try {
835
- const open = { continuation: true, deliveries: [], suppressed: 0 };
844
+ // Everything the open turn has decided about where its speech goes.
845
+ const decided = () => routingState({
846
+ continuation: true, pin: 'chan-alpha', hybridHold: true, triggerChannel: 'chan-alpha',
847
+ locus: 'world:commons', engaged: new Set(['discord:g:cafe']), arrivals: [{ messageId: 'cafe-message', channelId: 'discord:g:cafe' }],
848
+ });
849
+ const open = decided();
836
850
  internals.openLogical.set('assistant', open);
837
851
  internals.pendingRequests.push({ agentName: 'assistant', reason: 'heartbeat', source: 'framework', timestamp: Date.now(), suppressProse: true });
838
852
  // Bounded: a silent start that took the open turn's ' !' would start a
839
853
  // continuation with no response queued, and this would wait for good.
840
854
  await within(framework.runUntilIdle(), 'the silent turn to settle', 3000);
841
855
  assert.equal(turns, 1);
842
- assert.notEqual(internals.turnRouting.get('assistant')?.logical, open, 'the silent start kept a logical turn of its own');
856
+ const silent = internals.turnRouting.get('assistant');
857
+ assert.notEqual(silent, open, 'the silent start kept a logical turn of its own');
858
+ assert.equal(silent?.locus, undefined, 'with no locus');
843
859
  assert.equal(internals.openLogical.get('assistant'), open, 'and neither took nor closed the open one');
844
- assert.equal(open.continuation, true, "the open turn's ' !' is untouched");
860
+ assert.deepEqual(open, decided(), "everything the open turn decided is untouched, its ' !' included");
845
861
  // The next start that routes prose continues it, and its ' !' runs.
846
862
  // (feedTurn counts starts from here: the prose turn is 1, the
847
863
  // continuation 2.)
@@ -859,6 +875,32 @@ describe('explicit prose routing', () => {
859
875
  await framework.stop();
860
876
  }
861
877
  });
878
+ it("a silent turn's resume shows no locus, whatever a channel_open in that turn left on its routing state", async () => {
879
+ membrane.pushResponse(createMockResponse([{ type: 'text', text: 'quiet' }]));
880
+ const framework = await createFramework('hybrid');
881
+ stubRegistry(framework);
882
+ const registry = framework.channelRegistry;
883
+ const typing = [];
884
+ registry.startTyping = (channelId) => { typing.push(channelId); };
885
+ const started = [];
886
+ framework.onTrace((event) => {
887
+ if (event.type === 'inference:started')
888
+ started.push(event.channelId);
889
+ });
890
+ const internals = framework;
891
+ try {
892
+ // A silent turn opened a channel, which moved its routing state's
893
+ // locus, and its stream then crossed the budget.
894
+ internals.turnRouting.set('assistant', routingState({ locus: 'discord:g:cafe' }));
895
+ internals.pendingRequests.push({ agentName: 'assistant', reason: 'context_budget_restart', source: 'framework', timestamp: Date.now(), suppressProse: true });
896
+ await within(framework.runUntilIdle(), 'the silent restart', 3000);
897
+ assert.deepEqual(started, [undefined], 'the restart reports no locus');
898
+ assert.deepEqual(typing, [], 'and shows no typing anywhere');
899
+ }
900
+ finally {
901
+ await framework.stop();
902
+ }
903
+ });
862
904
  for (const mode of ['explicit', 'hybrid']) {
863
905
  it(`${mode}: the wake that consumes unstreamed tool results continues the turn that scheduled them`, async () => {
864
906
  const p = mode === 'explicit' ? '>>' : '>>>';
@@ -876,9 +918,9 @@ describe('explicit prose routing', () => {
876
918
  // The scheduling turn asked for a continuation and left its tool
877
919
  // results pending; the scheduler then starts the agent with another
878
920
  // wake, which consumes them.
879
- const scheduling = { destination: null, hybridHold: false, logical: { continuation: true, deliveries: [], suppressed: 0 } };
880
- internals.openLogical.set('assistant', scheduling.logical);
881
- internals.turnRouting.set('assistant', { destination: null, hybridHold: false, logical: { continuation: false, deliveries: [], suppressed: 0 } });
921
+ const scheduling = routingState({ continuation: true });
922
+ internals.openLogical.set('assistant', scheduling);
923
+ internals.turnRouting.set('assistant', routingState());
882
924
  trigger(framework);
883
925
  await framework.runUntilIdle();
884
926
  for (const end = Date.now() + 5000; turns < 2 && Date.now() < end;) {
@@ -886,7 +928,7 @@ describe('explicit prose routing', () => {
886
928
  await framework.runUntilIdle();
887
929
  }
888
930
  assert.equal(turns, 2, "the consuming turn took the scheduling turn's ' !'");
889
- assert.equal(scheduling.logical.continuation, false);
931
+ assert.equal(scheduling.continuation, false);
890
932
  assert.equal(internals.openLogical.has('assistant'), false, 'and the turns it ran have settled');
891
933
  }
892
934
  finally {
@@ -922,7 +964,7 @@ describe('explicit prose routing', () => {
922
964
  await within(reached, 'the queued delivery to reach routing');
923
965
  const chain = internals.proseSpeechChains.get('assistant');
924
966
  const routing = internals.turnRouting.get('assistant');
925
- assert.equal(routing.logical.continuation, true, "the held delivery's ' !' is on its turn's object");
967
+ assert.equal(routing.continuation, true, "the held delivery's ' !' is on its turn's object");
926
968
  assert.equal(internals.pendingRequests.length, 0);
927
969
  if (boundary === 'stop')
928
970
  await framework.stop();
@@ -932,7 +974,7 @@ describe('explicit prose routing', () => {
932
974
  await within(chain, 'the queued delivery');
933
975
  await new Promise((resolve) => setImmediate(resolve));
934
976
  assert.equal(internals.pendingRequests.filter((r) => r.reason === 'prose-continuation').length, 0, 'the late check adds no request');
935
- assert.equal(routing.logical.continuation, false, 'it took the flag and dropped it');
977
+ assert.equal(routing.continuation, false, 'it took the flag and dropped it');
936
978
  }
937
979
  finally {
938
980
  release();
@@ -981,11 +1023,11 @@ describe('explicit prose routing', () => {
981
1023
  // so the round's delivery can run once the next turn has begun.
982
1024
  await internals.deliverProse(agent, '>>#alpha ! kept private', { keepPrivate: true });
983
1025
  assert.equal(routed.length, 0, 'nothing is delivered');
984
- assert.equal(internals.turnRouting.get('assistant')?.logical.continuation ?? false, false, 'and no continuation is asked for');
1026
+ assert.equal(internals.turnRouting.get('assistant')?.continuation ?? false, false, 'and no continuation is asked for');
985
1027
  // The same envelope outside a private round asks for one.
986
1028
  await internals.deliverProse(agent, '>>#alpha ! sent');
987
1029
  assert.equal(routed.length, 1);
988
- assert.equal(internals.turnRouting.get('assistant')?.logical.continuation, true);
1030
+ assert.equal(internals.turnRouting.get('assistant')?.continuation, true);
989
1031
  await framework.stop();
990
1032
  });
991
1033
  it('a skip_reply round that sets the retained text aside with >>skip_reply {{unsent}} clears it', async () => {
@@ -1065,8 +1107,7 @@ describe('explicit prose routing', () => {
1065
1107
  try {
1066
1108
  for (const segments of rounds) {
1067
1109
  for (const start of [null, 'chan-alpha']) {
1068
- internals.turnRouting.set('assistant', { pin: start ?? undefined, destination: null, hybridHold: false,
1069
- logical: { continuation: false, deliveries: [], suppressed: 0 } });
1110
+ internals.turnRouting.set('assistant', routingState({ pin: start ?? undefined }));
1070
1111
  for (const segment of segments)
1071
1112
  await internals.deliverProse(agent, segment);
1072
1113
  const admitted = segments.flatMap((segment) => internals.admitProse(segment));
@@ -1089,9 +1130,8 @@ describe('explicit prose routing', () => {
1089
1130
  try {
1090
1131
  for (const text of cases) {
1091
1132
  routed.length = 0;
1092
- const queued = { destination: null, hybridHold: false,
1093
- logical: { continuation: false, deliveries: [], suppressed: 0 } };
1094
- const live = { ...queued, logical: { continuation: false, deliveries: [], suppressed: 0 } };
1133
+ const queued = routingState();
1134
+ const live = routingState();
1095
1135
  gateway.turnRouting.set('assistant', live);
1096
1136
  await gateway.deliverHybridProse(agent, text, 'chan-locus', true, queued);
1097
1137
  assert.equal(queued.hybridHold, true, 'the supplied turn receives the hold');
@@ -1115,17 +1155,16 @@ describe('explicit prose routing', () => {
1115
1155
  try {
1116
1156
  for (const prefix of ['>>>', '>>> ', '>>>\t']) {
1117
1157
  routed.length = 0;
1118
- const queued = { destination: null, hybridHold: false,
1119
- logical: { continuation: false, deliveries: [], suppressed: 0 } };
1120
- const live = { ...queued, logical: { continuation: false, deliveries: [], suppressed: 0 } };
1158
+ const queued = routingState();
1159
+ const live = routingState();
1121
1160
  gateway.turnRouting.set('assistant', live);
1122
1161
  await gateway.deliverHybridProse(agent, '>>>#alpha first', 'chan-locus', true, queued);
1123
1162
  await gateway.deliverHybridProse(agent, prefix, 'chan-locus', true, queued);
1124
1163
  await gateway.deliverHybridProse(agent, 'secret', 'chan-locus', true, queued);
1125
- assert.deepEqual(queued.logical.deliveries, ['chan-alpha'], 'the supplied turn records its delivery');
1126
- assert.equal(queued.logical.suppressed, 1, 'the supplied turn records its suppression');
1127
- assert.deepEqual(live.logical.deliveries, [], 'the live turn receives no delivery');
1128
- assert.equal(live.logical.suppressed, 0, 'the live turn receives no suppression');
1164
+ assert.deepEqual(queued.deliveries, ['chan-alpha'], 'the supplied turn records its delivery');
1165
+ assert.equal(queued.suppressed, 1, 'the supplied turn records its suppression');
1166
+ assert.deepEqual(live.deliveries, [], 'the live turn receives no delivery');
1167
+ assert.equal(live.suppressed, 0, 'the live turn receives no suppression');
1129
1168
  const router = new ProseStreamRouter({ mode: 'hybrid', initialTarget: 'chan-locus', resolve });
1130
1169
  const streamed = [
1131
1170
  ...router.feed('>>>#alpha first'), ...router.endSegment(),
@@ -1390,5 +1429,287 @@ describe('explicit prose routing', () => {
1390
1429
  assert.ok(texts.some(t => t.includes('[delivered] nothing') && t.includes('2 plain-speech segment(s) suppressed')), 'one receipt accounts for mid-turn and trailing prose');
1391
1430
  await framework.stop();
1392
1431
  });
1432
+ // ---- A start that continues a logical turn keeps the turn's routing ----
1433
+ // A framework retry after a stream error, the compile a provider-capacity
1434
+ // hold defers a turn to, and the re-inference that consumes unstreamed tool
1435
+ // results each continue the turn: the sticky destination, explicit mode's
1436
+ // turn destination and hybrid's hold carry into the new stream.
1437
+ /** An organization acceleration 429, which holds the turn for a later compile. */
1438
+ const accelerationLimit = (request) => new MembraneError({
1439
+ type: 'rate_limit', retryable: true, httpStatus: 429,
1440
+ message: "This request would exceed your organization's maximum usage increase rate for input tokens per minute",
1441
+ rawError: { status: 429 }, rawRequest: request,
1442
+ });
1443
+ /**
1444
+ * A turn whose first stream writes `first` and calls a tool, and then
1445
+ * fails instead of completing: with a transient error, which the framework
1446
+ * retries, or with an acceleration 429, which holds the turn until a later
1447
+ * compile. That later stream answers `carrying on` with no prefix, as a
1448
+ * model continuing its turn would.
1449
+ */
1450
+ async function continueTurn(how, mode, first) {
1451
+ membrane.pushResponse(createMockResponse([
1452
+ { type: 'text', text: first },
1453
+ { type: 'tool_use', id: 'continued-tool', name: 'robot--move', input: { dir: 'north' } },
1454
+ ], 'tool_use'));
1455
+ membrane.pushResponse(createMockResponse([]));
1456
+ const framework = await createFramework(mode);
1457
+ const routed = stubRegistry(framework);
1458
+ const { chunks } = captureStream(framework);
1459
+ const internals = framework;
1460
+ let starts = 0;
1461
+ let laterChunks = 0;
1462
+ framework.onTrace((event) => {
1463
+ if (event.type !== 'inference:started')
1464
+ return;
1465
+ starts++;
1466
+ if (starts === 2) {
1467
+ laterChunks = chunks.length;
1468
+ membrane.pushResponse(createMockResponse([{ type: 'text', text: 'carrying on' }]));
1469
+ }
1470
+ // A bounce wakes the resident; that turn stays quiet.
1471
+ if (starts >= 3)
1472
+ membrane.pushResponse(createMockResponse([{ type: 'text', text: mode === 'explicit' ? '>>skip_reply' : '>>>skip_reply' }]));
1473
+ });
1474
+ const makeStream = membrane.streamYielding.bind(membrane);
1475
+ let streams = 0;
1476
+ membrane.streamYielding = (request, options) => {
1477
+ const stream = makeStream(request, options);
1478
+ const index = ++streams;
1479
+ const iterate = stream[Symbol.asyncIterator].bind(stream);
1480
+ stream[Symbol.asyncIterator] = async function* () {
1481
+ for await (const event of { [Symbol.asyncIterator]: iterate }) {
1482
+ if (event.type === 'complete' && index === 1) {
1483
+ yield {
1484
+ type: 'error',
1485
+ error: how === 'retry' ? new Error('synthetic transient stream error') : accelerationLimit(request),
1486
+ };
1487
+ return;
1488
+ }
1489
+ yield event;
1490
+ }
1491
+ };
1492
+ return stream;
1493
+ };
1494
+ trigger(framework);
1495
+ await within(framework.runUntilIdle(), 'the first stream', 10000);
1496
+ if (how === 'capacity') {
1497
+ assert.equal(starts, 1, 'the hold defers the turn rather than retrying it at once');
1498
+ const hold = internals.providerAccelerationCooldowns.get('assistant');
1499
+ assert.ok(hold, 'the turn is held');
1500
+ // What the cooldown's timer does when it fires, at the hold's deadline.
1501
+ hold.until = Date.now();
1502
+ internals.releaseProviderAccelerationCooldown('assistant');
1503
+ await within(framework.runUntilIdle(), 'the deferred compile', 10000);
1504
+ }
1505
+ assert.ok(starts >= 2, 'the turn went on in a later stream');
1506
+ return { framework, routed, bounces: context(framework).bounces, laterChunks: chunks.slice(laterChunks) };
1507
+ }
1508
+ for (const how of ['retry', 'capacity']) {
1509
+ const start = how === 'retry' ? 'an error retry' : 'the compile a capacity hold defers the turn to';
1510
+ for (const [label, mode, first, expected] of [
1511
+ ['hybrid >>>skip_reply keeps later unprefixed prose private', 'hybrid', '>>>skip_reply an aside', []],
1512
+ ['hybrid >>>#alpha is sticky', 'hybrid', '>>>#alpha first', [{ text: 'first', locus: 'chan-alpha' }, { text: 'carrying on', locus: 'chan-alpha' }]],
1513
+ ['explicit >>#alpha is sticky, and the stream starts at the turn\'s destination', 'explicit', '>>#alpha first', [{ text: 'first', locus: 'chan-alpha' }, { text: 'carrying on', locus: 'chan-alpha' }]],
1514
+ ]) {
1515
+ it(`across ${start}, ${label}`, async () => {
1516
+ const { framework, routed, bounces, laterChunks } = await continueTurn(how, mode, first);
1517
+ try {
1518
+ // Publication as the registry stub saw it; bounce notices by their kind.
1519
+ assert.deepEqual(routed.map((r) => ({ text: r.text.trim(), locus: r.locus })), expected);
1520
+ assert.equal(bounces, 0, 'nothing bounced');
1521
+ if (mode === 'explicit') {
1522
+ // The later stream streams its unprefixed text where delivery sends it.
1523
+ assert.deepEqual(laterChunks.map((c) => c.channelId), laterChunks.map(() => 'chan-alpha'));
1524
+ assert.equal(laterChunks.map((c) => c.delta).join(''), 'carrying on');
1525
+ }
1526
+ }
1527
+ finally {
1528
+ await framework.stop();
1529
+ }
1530
+ });
1531
+ }
1532
+ }
1533
+ for (const [label, mode, left, expected] of [
1534
+ ['hybrid: the hold keeps later unprefixed prose private', 'hybrid', { hybridHold: true, locus: 'world:commons' }, []],
1535
+ ['hybrid: the sticky destination', 'hybrid', { pin: 'chan-alpha', locus: 'world:commons' }, [{ text: 'carrying on', locus: 'chan-alpha' }]],
1536
+ ['explicit: the sticky destination, and the stream starts at the turn\'s destination', 'explicit', { pin: 'chan-alpha', destination: 'chan-alpha' }, [{ text: 'carrying on', locus: 'chan-alpha' }]],
1537
+ ]) {
1538
+ it(`a re-inference on unstreamed tool results keeps the turn's routing: ${label}`, async () => {
1539
+ membrane.pushResponse(createMockResponse([{ type: 'text', text: 'carrying on' }]));
1540
+ const framework = await createFramework(mode);
1541
+ const routed = stubRegistry(framework);
1542
+ const { chunks } = captureStream(framework);
1543
+ // A bounce wakes the resident; that turn stays quiet.
1544
+ feedTurn(framework, 2, [{ type: 'text', text: mode === 'explicit' ? '>>skip_reply' : '>>>skip_reply' }]);
1545
+ const internals = framework;
1546
+ try {
1547
+ // The turn's earlier stream left this routing state, and its tool
1548
+ // results pending for the non-streaming fallback's re-inference.
1549
+ const earlier = routingState(left);
1550
+ internals.turnRouting.set('assistant', earlier);
1551
+ internals.openLogical.set('assistant', earlier);
1552
+ internals.pendingRequests.push({ agentName: 'assistant', reason: 'tool_results_ready', source: 'framework', timestamp: Date.now() });
1553
+ await within(framework.runUntilIdle(), 'the re-inference', 10000);
1554
+ assert.deepEqual(routed.map((r) => ({ text: r.text.trim(), locus: r.locus })), expected);
1555
+ assert.equal(context(framework).bounces, 0, 'nothing bounced');
1556
+ if (mode === 'explicit') {
1557
+ assert.deepEqual(chunks.map((c) => c.channelId), chunks.map(() => 'chan-alpha'));
1558
+ assert.equal(chunks.map((c) => c.delta).join(''), 'carrying on');
1559
+ }
1560
+ }
1561
+ finally {
1562
+ await framework.stop();
1563
+ }
1564
+ });
1565
+ }
1566
+ for (const unreadable of [false, true]) {
1567
+ const name = unreadable
1568
+ ? "explicit: a turn with no locus doesn't read its arrival's message, so a store that can't be read leaves its next start alone"
1569
+ : "explicit: an addressed message between the turn's streams moves nothing, since explicit mode has no locus";
1570
+ it(name, async () => {
1571
+ membrane.pushResponse(createMockResponse([{ type: 'text', text: 'carrying on' }]));
1572
+ const framework = await createFramework('explicit');
1573
+ const routed = stubRegistry(framework);
1574
+ feedTurn(framework, 2, [{ type: 'text', text: '>>skip_reply' }]);
1575
+ const internals = framework;
1576
+ const cm = framework.getAgent('assistant').getContextManager();
1577
+ const getMessage = cm.getMessage.bind(cm);
1578
+ try {
1579
+ const earlier = routingState({ pin: 'chan-alpha', destination: 'chan-alpha' });
1580
+ internals.turnRouting.set('assistant', earlier);
1581
+ internals.openLogical.set('assistant', earlier);
1582
+ // Someone addresses the agent from another channel while the turn waits.
1583
+ internals.addMessage('Antra', [{ type: 'text', text: 'quick question for you' }], { channelId: 'discord:g:cafe', tags: ['chat:addressed'] });
1584
+ assert.deepEqual(earlier.arrivals.map((a) => a.channelId), ['discord:g:cafe'], 'it is the turn\'s arrival');
1585
+ if (unreadable) {
1586
+ const arrival = earlier.arrivals[0].messageId;
1587
+ cm.getMessage = (id) => {
1588
+ if (id === arrival)
1589
+ throw new Error('store unavailable');
1590
+ return getMessage(id);
1591
+ };
1592
+ }
1593
+ internals.pendingRequests.push({ agentName: 'assistant', reason: 'tool_results_ready', source: 'framework', timestamp: Date.now() });
1594
+ await within(framework.runUntilIdle(), 'the re-inference', 10000);
1595
+ assert.deepEqual(routed.map((r) => ({ text: r.text.trim(), locus: r.locus })), [{ text: 'carrying on', locus: 'chan-alpha' }]);
1596
+ assert.equal(earlier.locus, undefined, 'no locus');
1597
+ assert.deepEqual(earlier.arrivals, [], 'the arrival is used up');
1598
+ assert.ok(!context(framework).texts.some((t) => t.includes('The conversation moved to')), 'and no routing notice');
1599
+ }
1600
+ finally {
1601
+ cm.getMessage = getMessage;
1602
+ await framework.stop();
1603
+ }
1604
+ });
1605
+ }
1606
+ for (const mode of ['explicit', 'hybrid']) {
1607
+ it(`${mode}: a retried turn's text waits for the replaced stream's late deliveries, and follows what they set`, async () => {
1608
+ const p = mode === 'explicit' ? '>>' : '>>>';
1609
+ // The first stream's round one posts to #cafe, and the post is held.
1610
+ // Round two sets where the turn goes next (explicit: #alpha; hybrid: a
1611
+ // skip_reply hold); its delivery is queued behind the held post. Then
1612
+ // the stream errors, and its retry starts while both are pending.
1613
+ membrane.pushResponse(createMockResponse([
1614
+ { type: 'text', text: `${p}#cafe zero` },
1615
+ { type: 'tool_use', id: 'late-one', name: 'robot--move', input: { dir: 'north' } },
1616
+ ], 'tool_use'));
1617
+ membrane.pushResponse(createMockResponse([
1618
+ { type: 'text', text: mode === 'explicit' ? '>>#alpha first' : '>>>skip_reply an aside' },
1619
+ { type: 'tool_use', id: 'late-two', name: 'robot--move', input: { dir: 'south' } },
1620
+ ], 'tool_use'));
1621
+ membrane.pushResponse(createMockResponse([]));
1622
+ const framework = await createFramework(mode);
1623
+ const routed = stubRegistry(framework);
1624
+ const { chunks } = captureStream(framework);
1625
+ const internals = framework;
1626
+ let release;
1627
+ const held = new Promise((resolve) => { release = resolve; });
1628
+ const route = internals.channelRegistry.routeSpeech;
1629
+ internals.channelRegistry.routeSpeech = async (agent, text, locus) => {
1630
+ const outcome = await route(agent, text, locus);
1631
+ if (text.trim() === 'zero')
1632
+ await held;
1633
+ return outcome;
1634
+ };
1635
+ let starts = 0;
1636
+ let retryChunks = 0;
1637
+ framework.onTrace((event) => {
1638
+ if (event.type !== 'inference:started')
1639
+ return;
1640
+ starts++;
1641
+ if (starts === 2) {
1642
+ retryChunks = chunks.length;
1643
+ membrane.pushResponse(createMockResponse([{ type: 'text', text: 'carrying on' }]));
1644
+ }
1645
+ if (starts >= 3)
1646
+ membrane.pushResponse(createMockResponse([{ type: 'text', text: `${p}skip_reply` }]));
1647
+ });
1648
+ let retryCompleted;
1649
+ const retryComplete = new Promise((resolve) => { retryCompleted = resolve; });
1650
+ const makeStream = membrane.streamYielding.bind(membrane);
1651
+ let streams = 0;
1652
+ membrane.streamYielding = (request, options) => {
1653
+ const stream = makeStream(request, options);
1654
+ const index = ++streams;
1655
+ const iterate = stream[Symbol.asyncIterator].bind(stream);
1656
+ stream[Symbol.asyncIterator] = async function* () {
1657
+ for await (const event of { [Symbol.asyncIterator]: iterate }) {
1658
+ if (event.type === 'complete' && index === 1) {
1659
+ yield { type: 'error', error: new Error('synthetic transient stream error') };
1660
+ return;
1661
+ }
1662
+ if (event.type === 'complete' && index === 2)
1663
+ retryCompleted();
1664
+ yield event;
1665
+ }
1666
+ };
1667
+ return stream;
1668
+ };
1669
+ let running;
1670
+ try {
1671
+ trigger(framework);
1672
+ running = framework.runUntilIdle();
1673
+ void running.catch(() => { });
1674
+ await within(retryComplete, 'the retry to finish its response', 10000);
1675
+ await new Promise((resolve) => setTimeout(resolve, 20));
1676
+ assert.equal(starts, 2, 'the retry has started');
1677
+ assert.deepEqual(routed.map((r) => r.text.trim()), ['zero'], "the retry's text waits behind the held post");
1678
+ const routing = internals.turnRouting.get('assistant');
1679
+ if (mode === 'explicit') {
1680
+ assert.equal(routing.pin, 'discord:g:cafe', "round two's delivery hasn't run yet");
1681
+ assert.equal(routing.destination, 'chan-alpha', 'though round two set the destination as it ended');
1682
+ }
1683
+ else {
1684
+ assert.equal(routing.hybridHold, false, "round two's delivery hasn't run yet");
1685
+ }
1686
+ release();
1687
+ await within(running, 'the retried turn to settle', 10000);
1688
+ const posts = routed.map((r) => ({ text: r.text.trim(), locus: r.locus }));
1689
+ if (mode === 'explicit') {
1690
+ assert.deepEqual(posts, [
1691
+ { text: 'zero', locus: 'discord:g:cafe' },
1692
+ { text: 'first', locus: 'chan-alpha' },
1693
+ { text: 'carrying on', locus: 'chan-alpha' },
1694
+ ], "the replaced stream's posts go first, and the retry's text follows the destination they set");
1695
+ assert.deepEqual(chunks.slice(retryChunks).map((c) => c.channelId), chunks.slice(retryChunks).map(() => 'chan-alpha'), "the retry's stream starts at the destination round two set");
1696
+ }
1697
+ else {
1698
+ assert.deepEqual(posts, [{ text: 'zero', locus: 'discord:g:cafe' }], "the retry's unprefixed text stays private under the hold the late delivery set");
1699
+ const receipt = context(framework).texts.filter((t) => t.startsWith('[delivered]'));
1700
+ assert.equal(receipt.length, 1, `one receipt for the logical turn: ${receipt.join(' | ')}`);
1701
+ assert.match(receipt[0], /discord:g:cafe/, "it reports the replaced stream's late post");
1702
+ assert.match(receipt[0], /1 plain-speech segment\(s\) suppressed/, "and the retry's held segment");
1703
+ }
1704
+ assert.equal(context(framework).bounces, 0, 'nothing bounced');
1705
+ }
1706
+ finally {
1707
+ release();
1708
+ if (running)
1709
+ await running;
1710
+ await framework.stop();
1711
+ }
1712
+ });
1713
+ }
1393
1714
  });
1394
1715
  //# sourceMappingURL=explicit-prose-routing.test.js.map