@animalabs/agent-framework 0.22.2 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -4
- package/dist/src/agent.d.ts +108 -17
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +207 -67
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.js +1 -1
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +5 -1
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/api/types.d.ts +1 -1
- package/dist/src/api/types.d.ts.map +1 -1
- package/dist/src/branch-names.d.ts +14 -0
- package/dist/src/branch-names.d.ts.map +1 -0
- package/dist/src/branch-names.js +22 -0
- package/dist/src/branch-names.js.map +1 -0
- package/dist/src/closest-names.d.ts +28 -0
- package/dist/src/closest-names.d.ts.map +1 -0
- package/dist/src/closest-names.js +98 -0
- package/dist/src/closest-names.js.map +1 -0
- package/dist/src/code-execution/index.d.ts +1 -1
- package/dist/src/code-execution/index.d.ts.map +1 -1
- package/dist/src/code-execution/index.js.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts +50 -7
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +258 -90
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/runtime-py.d.ts +3 -2
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
- package/dist/src/code-execution/runtime-py.js +94 -8
- package/dist/src/code-execution/runtime-py.js.map +1 -1
- package/dist/src/code-execution/tool-definition.d.ts +17 -1
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +72 -6
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/config-checks.d.ts +28 -0
- package/dist/src/config-checks.d.ts.map +1 -0
- package/dist/src/config-checks.js +56 -0
- package/dist/src/config-checks.js.map +1 -0
- package/dist/src/deadline-timer.d.ts +21 -0
- package/dist/src/deadline-timer.d.ts.map +1 -0
- package/dist/src/deadline-timer.js +43 -0
- package/dist/src/deadline-timer.js.map +1 -0
- package/dist/src/error-message.d.ts +30 -0
- package/dist/src/error-message.d.ts.map +1 -0
- package/dist/src/error-message.js +50 -0
- package/dist/src/error-message.js.map +1 -0
- package/dist/src/framework.d.ts +1529 -165
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +8264 -1939
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +8 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +36 -5
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/held-writes.d.ts +16 -0
- package/dist/src/held-writes.d.ts.map +1 -0
- package/dist/src/held-writes.js +56 -0
- package/dist/src/held-writes.js.map +1 -0
- package/dist/src/index.d.ts +10 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +11 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-grant.d.ts +4 -0
- package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
- package/dist/src/mcpl/capability-grant.js +1 -1
- package/dist/src/mcpl/capability-grant.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +233 -18
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +975 -386
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +21 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js +91 -30
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +19 -3
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +23 -8
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +20 -0
- package/dist/src/mcpl/convert-block.d.ts.map +1 -0
- package/dist/src/mcpl/convert-block.js +88 -0
- package/dist/src/mcpl/convert-block.js.map +1 -0
- package/dist/src/mcpl/errors.d.ts +13 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +28 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts +23 -0
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +83 -72
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +114 -0
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -0
- package/dist/src/mcpl/inbound-source.js +83 -0
- package/dist/src/mcpl/inbound-source.js.map +1 -0
- package/dist/src/mcpl/index.d.ts +7 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +5 -1
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +19 -13
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +155 -87
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/log-value.d.ts +7 -0
- package/dist/src/mcpl/log-value.d.ts.map +1 -1
- package/dist/src/mcpl/log-value.js +9 -0
- package/dist/src/mcpl/log-value.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +227 -0
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -0
- package/dist/src/mcpl/modern-connection.js +1088 -0
- package/dist/src/mcpl/modern-connection.js.map +1 -0
- package/dist/src/mcpl/prefix-rule.d.ts +18 -0
- package/dist/src/mcpl/prefix-rule.d.ts.map +1 -0
- package/dist/src/mcpl/prefix-rule.js +25 -0
- package/dist/src/mcpl/prefix-rule.js.map +1 -0
- package/dist/src/mcpl/protocol-family.d.ts +179 -0
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -0
- package/dist/src/mcpl/protocol-family.js +697 -0
- package/dist/src/mcpl/protocol-family.js.map +1 -0
- package/dist/src/mcpl/push-coalescer.d.ts +58 -17
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +93 -32
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +16 -9
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +18 -83
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +76 -13
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +275 -66
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +98 -18
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +170 -32
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +406 -31
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +1331 -272
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts +35 -6
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +63 -13
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.d.ts +36 -0
- package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -0
- package/dist/src/mcpl/skip-reply-wake.js +55 -0
- package/dist/src/mcpl/skip-reply-wake.js.map +1 -0
- package/dist/src/mcpl/tool-result-normalize.d.ts +54 -0
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -0
- package/dist/src/mcpl/tool-result-normalize.js +232 -0
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +104 -12
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +315 -85
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +76 -19
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +16 -0
- package/dist/src/mcpl/visible-content.d.ts.map +1 -1
- package/dist/src/mcpl/visible-content.js +26 -0
- package/dist/src/mcpl/visible-content.js.map +1 -1
- package/dist/src/module-registry.d.ts +34 -6
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +119 -31
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +3 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts +43 -2
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +286 -67
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +53 -5
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +712 -186
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/discord/refusal.d.ts +14 -0
- package/dist/src/modules/discord/refusal.d.ts.map +1 -0
- package/dist/src/modules/discord/refusal.js +14 -0
- package/dist/src/modules/discord/refusal.js.map +1 -0
- package/dist/src/modules/discord/types.d.ts +27 -4
- package/dist/src/modules/discord/types.d.ts.map +1 -1
- package/dist/src/modules/health/index.d.ts.map +1 -1
- package/dist/src/modules/health/index.js +8 -3
- package/dist/src/modules/health/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +3 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +359 -127
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +28 -1
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +177 -32
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/index.d.ts +1 -1
- package/dist/src/modules/index.d.ts.map +1 -1
- package/dist/src/modules/index.js +1 -1
- package/dist/src/modules/index.js.map +1 -1
- package/dist/src/modules/unknown-tool.d.ts +10 -0
- package/dist/src/modules/unknown-tool.d.ts.map +1 -0
- package/dist/src/modules/unknown-tool.js +17 -0
- package/dist/src/modules/unknown-tool.js.map +1 -0
- package/dist/src/modules/workspace/disk-agreement.d.ts +57 -4
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +75 -7
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +66 -7
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +303 -46
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +313 -45
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +1404 -304
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +315 -16
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +727 -37
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +58 -7
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +521 -56
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +28 -5
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +49 -2
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +158 -29
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/prose-drafts.d.ts +240 -0
- package/dist/src/prose-drafts.d.ts.map +1 -0
- package/dist/src/prose-drafts.js +362 -0
- package/dist/src/prose-drafts.js.map +1 -0
- package/dist/src/prose-segments.d.ts +21 -2
- package/dist/src/prose-segments.d.ts.map +1 -1
- package/dist/src/prose-segments.js +46 -9
- package/dist/src/prose-segments.js.map +1 -1
- package/dist/src/recovery/offline-branch.d.ts +1 -0
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +3 -1
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/safe-slice.d.ts +5 -1
- package/dist/src/safe-slice.d.ts.map +1 -1
- package/dist/src/safe-slice.js +9 -1
- package/dist/src/safe-slice.js.map +1 -1
- package/dist/src/silent-heartbeat.d.ts +112 -39
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +100 -29
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/tool-args.d.ts +40 -0
- package/dist/src/tool-args.d.ts.map +1 -0
- package/dist/src/tool-args.js +207 -0
- package/dist/src/tool-args.js.map +1 -0
- package/dist/src/tool-image-ledger.d.ts +87 -19
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +135 -32
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts +9 -0
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +266 -62
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +7 -10
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +32 -3
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +106 -19
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +25 -1
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +260 -65
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +44 -22
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +25 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +38 -7
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/inference-log.d.ts +1 -1
- package/dist/src/types/inference-log.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +77 -14
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +97 -8
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.d.ts +5 -0
- package/dist/src/usage/usage-tracker.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.js +35 -8
- package/dist/src/usage/usage-tracker.js.map +1 -1
- package/package.json +5 -4
- package/src/agent.ts +257 -72
- package/src/api/mcp-server-core.ts +1 -1
- package/src/api/server.ts +4 -1
- package/src/api/types.ts +1 -1
- package/src/branch-names.ts +19 -0
- package/src/closest-names.ts +94 -0
- package/src/code-execution/index.ts +1 -0
- package/src/code-execution/py-runner.ts +304 -93
- package/src/code-execution/runtime-py.ts +94 -8
- package/src/code-execution/tool-definition.ts +85 -7
- package/src/config-checks.ts +50 -0
- package/src/deadline-timer.ts +44 -0
- package/src/error-message.ts +48 -0
- package/src/framework.ts +8625 -1984
- package/src/gate/event-gate.ts +38 -5
- package/src/held-writes.ts +59 -0
- package/src/index.ts +29 -5
- package/src/mcpl/capability-grant.ts +1 -1
- package/src/mcpl/channel-registry.ts +1095 -411
- package/src/mcpl/checkpoint-manager.ts +104 -29
- package/src/mcpl/conversation-router.ts +28 -8
- package/src/mcpl/convert-block.ts +105 -0
- package/src/mcpl/errors.ts +28 -0
- package/src/mcpl/hook-orchestrator.ts +84 -77
- package/src/mcpl/inbound-source.ts +156 -0
- package/src/mcpl/index.ts +23 -1
- package/src/mcpl/inference-router.ts +179 -103
- package/src/mcpl/log-value.ts +10 -0
- package/src/mcpl/modern-connection.ts +1182 -0
- package/src/mcpl/prefix-rule.ts +32 -0
- package/src/mcpl/protocol-family.ts +691 -0
- package/src/mcpl/push-coalescer.ts +110 -36
- package/src/mcpl/push-handler.ts +20 -90
- package/src/mcpl/reference-fetcher.ts +318 -67
- package/src/mcpl/references.ts +183 -34
- package/src/mcpl/server-connection.ts +1404 -280
- package/src/mcpl/server-registry.ts +63 -14
- package/src/mcpl/skip-reply-wake.ts +73 -0
- package/src/mcpl/tool-result-normalize.ts +294 -0
- package/src/mcpl/transport.ts +320 -76
- package/src/mcpl/types.ts +77 -19
- package/src/mcpl/visible-content.ts +28 -0
- package/src/module-registry.ts +126 -36
- package/src/modules/api/index.ts +3 -2
- package/src/modules/discord/discord-js-client.ts +306 -74
- package/src/modules/discord/index.ts +737 -201
- package/src/modules/discord/refusal.ts +13 -0
- package/src/modules/discord/types.ts +27 -6
- package/src/modules/health/index.ts +8 -3
- package/src/modules/history/index.ts +410 -120
- package/src/modules/history/semantic.ts +189 -32
- package/src/modules/index.ts +1 -1
- package/src/modules/unknown-tool.ts +16 -0
- package/src/modules/workspace/disk-agreement.ts +97 -8
- package/src/modules/workspace/effects.ts +291 -36
- package/src/modules/workspace/index.ts +1469 -321
- package/src/modules/workspace/observe.ts +784 -41
- package/src/modules/workspace/reconcile.ts +619 -64
- package/src/modules/workspace/types.ts +28 -5
- package/src/modules/workspace/watcher.ts +154 -30
- package/src/prose-drafts.ts +481 -0
- package/src/prose-segments.ts +51 -8
- package/src/recovery/offline-branch.ts +4 -1
- package/src/safe-slice.ts +9 -1
- package/src/silent-heartbeat.ts +184 -59
- package/src/tool-args.ts +213 -0
- package/src/tool-image-ledger.ts +153 -34
- package/src/tool-presentation.ts +233 -42
- package/src/tool-result-guard.ts +7 -8
- package/src/tool-result-history.ts +124 -20
- package/src/tune-out/coordinator.ts +259 -65
- package/src/types/agent.ts +44 -24
- package/src/types/events.ts +25 -0
- package/src/types/framework.ts +38 -7
- package/src/types/inference-log.ts +1 -1
- package/src/types/module.ts +79 -14
- package/src/types/trace.ts +103 -8
- package/src/usage/usage-tracker.ts +32 -8
- package/dist/test/activation-request-sanitize.test.d.ts +0 -2
- package/dist/test/activation-request-sanitize.test.d.ts.map +0 -1
- package/dist/test/activation-request-sanitize.test.js +0 -89
- package/dist/test/activation-request-sanitize.test.js.map +0 -1
- package/dist/test/agent-cache-ttl.test.d.ts +0 -2
- package/dist/test/agent-cache-ttl.test.d.ts.map +0 -1
- package/dist/test/agent-cache-ttl.test.js +0 -41
- package/dist/test/agent-cache-ttl.test.js.map +0 -1
- package/dist/test/agent-runtime-settings.test.d.ts +0 -2
- package/dist/test/agent-runtime-settings.test.d.ts.map +0 -1
- package/dist/test/agent-runtime-settings.test.js +0 -1598
- package/dist/test/agent-runtime-settings.test.js.map +0 -1
- package/dist/test/api-admin-gate.test.d.ts +0 -2
- package/dist/test/api-admin-gate.test.d.ts.map +0 -1
- package/dist/test/api-admin-gate.test.js +0 -116
- package/dist/test/api-admin-gate.test.js.map +0 -1
- package/dist/test/assistant-persist-refusal.test.d.ts +0 -2
- package/dist/test/assistant-persist-refusal.test.d.ts.map +0 -1
- package/dist/test/assistant-persist-refusal.test.js +0 -63
- package/dist/test/assistant-persist-refusal.test.js.map +0 -1
- package/dist/test/batched-wake-locus.test.d.ts +0 -2
- package/dist/test/batched-wake-locus.test.d.ts.map +0 -1
- package/dist/test/batched-wake-locus.test.js +0 -166
- package/dist/test/batched-wake-locus.test.js.map +0 -1
- package/dist/test/busy-agent-scheduler.test.d.ts +0 -2
- package/dist/test/busy-agent-scheduler.test.d.ts.map +0 -1
- package/dist/test/busy-agent-scheduler.test.js +0 -92
- package/dist/test/busy-agent-scheduler.test.js.map +0 -1
- package/dist/test/channel-incoming-targeting.test.d.ts +0 -2
- package/dist/test/channel-incoming-targeting.test.d.ts.map +0 -1
- package/dist/test/channel-incoming-targeting.test.js +0 -81
- package/dist/test/channel-incoming-targeting.test.js.map +0 -1
- package/dist/test/channel-label-history.test.d.ts +0 -2
- package/dist/test/channel-label-history.test.d.ts.map +0 -1
- package/dist/test/channel-label-history.test.js +0 -424
- package/dist/test/channel-label-history.test.js.map +0 -1
- package/dist/test/channel-lifecycle-provenance.test.d.ts +0 -2
- package/dist/test/channel-lifecycle-provenance.test.d.ts.map +0 -1
- package/dist/test/channel-lifecycle-provenance.test.js +0 -47
- package/dist/test/channel-lifecycle-provenance.test.js.map +0 -1
- package/dist/test/channel-registry-routing.test.d.ts +0 -2
- package/dist/test/channel-registry-routing.test.d.ts.map +0 -1
- package/dist/test/channel-registry-routing.test.js +0 -446
- package/dist/test/channel-registry-routing.test.js.map +0 -1
- package/dist/test/channel-registry-typing.test.d.ts +0 -2
- package/dist/test/channel-registry-typing.test.d.ts.map +0 -1
- package/dist/test/channel-registry-typing.test.js +0 -91
- package/dist/test/channel-registry-typing.test.js.map +0 -1
- package/dist/test/channels-changed-respond-first.test.d.ts +0 -2
- package/dist/test/channels-changed-respond-first.test.d.ts.map +0 -1
- package/dist/test/channels-changed-respond-first.test.js +0 -74
- package/dist/test/channels-changed-respond-first.test.js.map +0 -1
- package/dist/test/checkpoint-attribution.test.d.ts +0 -2
- package/dist/test/checkpoint-attribution.test.d.ts.map +0 -1
- package/dist/test/checkpoint-attribution.test.js +0 -200
- package/dist/test/checkpoint-attribution.test.js.map +0 -1
- package/dist/test/code-execution.test.d.ts +0 -11
- package/dist/test/code-execution.test.d.ts.map +0 -1
- package/dist/test/code-execution.test.js +0 -1272
- package/dist/test/code-execution.test.js.map +0 -1
- package/dist/test/component-defaults.test.d.ts +0 -2
- package/dist/test/component-defaults.test.d.ts.map +0 -1
- package/dist/test/component-defaults.test.js +0 -54
- package/dist/test/component-defaults.test.js.map +0 -1
- package/dist/test/conversation-fork-injection-scoping.test.d.ts +0 -2
- package/dist/test/conversation-fork-injection-scoping.test.d.ts.map +0 -1
- package/dist/test/conversation-fork-injection-scoping.test.js +0 -200
- package/dist/test/conversation-fork-injection-scoping.test.js.map +0 -1
- package/dist/test/conversation-router.test.d.ts +0 -2
- package/dist/test/conversation-router.test.d.ts.map +0 -1
- package/dist/test/conversation-router.test.js +0 -174
- package/dist/test/conversation-router.test.js.map +0 -1
- package/dist/test/conversation-routing.test.d.ts +0 -7
- package/dist/test/conversation-routing.test.d.ts.map +0 -1
- package/dist/test/conversation-routing.test.js +0 -621
- package/dist/test/conversation-routing.test.js.map +0 -1
- package/dist/test/deferred-wake-integrity.test.d.ts +0 -29
- package/dist/test/deferred-wake-integrity.test.d.ts.map +0 -1
- package/dist/test/deferred-wake-integrity.test.js +0 -320
- package/dist/test/deferred-wake-integrity.test.js.map +0 -1
- package/dist/test/discord-awareness-outbox.test.d.ts +0 -2
- package/dist/test/discord-awareness-outbox.test.d.ts.map +0 -1
- package/dist/test/discord-awareness-outbox.test.js +0 -371
- package/dist/test/discord-awareness-outbox.test.js.map +0 -1
- package/dist/test/discord-history-delete-window.test.d.ts +0 -2
- package/dist/test/discord-history-delete-window.test.d.ts.map +0 -1
- package/dist/test/discord-history-delete-window.test.js +0 -81
- package/dist/test/discord-history-delete-window.test.js.map +0 -1
- package/dist/test/discord-history-sync.test.d.ts +0 -2
- package/dist/test/discord-history-sync.test.d.ts.map +0 -1
- package/dist/test/discord-history-sync.test.js +0 -56
- package/dist/test/discord-history-sync.test.js.map +0 -1
- package/dist/test/discord-typing-keepalive.test.d.ts +0 -2
- package/dist/test/discord-typing-keepalive.test.d.ts.map +0 -1
- package/dist/test/discord-typing-keepalive.test.js +0 -77
- package/dist/test/discord-typing-keepalive.test.js.map +0 -1
- package/dist/test/ephemeral-settle.test.d.ts +0 -2
- package/dist/test/ephemeral-settle.test.d.ts.map +0 -1
- package/dist/test/ephemeral-settle.test.js +0 -354
- package/dist/test/ephemeral-settle.test.js.map +0 -1
- package/dist/test/event-gate-passthrough.test.d.ts +0 -15
- package/dist/test/event-gate-passthrough.test.d.ts.map +0 -1
- package/dist/test/event-gate-passthrough.test.js +0 -289
- package/dist/test/event-gate-passthrough.test.js.map +0 -1
- package/dist/test/event-gate-policy-mutation.test.d.ts +0 -2
- package/dist/test/event-gate-policy-mutation.test.d.ts.map +0 -1
- package/dist/test/event-gate-policy-mutation.test.js +0 -137
- package/dist/test/event-gate-policy-mutation.test.js.map +0 -1
- package/dist/test/event-gate-rate-limit.test.d.ts +0 -2
- package/dist/test/event-gate-rate-limit.test.d.ts.map +0 -1
- package/dist/test/event-gate-rate-limit.test.js +0 -627
- package/dist/test/event-gate-rate-limit.test.js.map +0 -1
- package/dist/test/event-gate-script.test.d.ts +0 -2
- package/dist/test/event-gate-script.test.d.ts.map +0 -1
- package/dist/test/event-gate-script.test.js +0 -95
- package/dist/test/event-gate-script.test.js.map +0 -1
- package/dist/test/event-gate-selfwake.test.d.ts +0 -15
- package/dist/test/event-gate-selfwake.test.d.ts.map +0 -1
- package/dist/test/event-gate-selfwake.test.js +0 -125
- package/dist/test/event-gate-selfwake.test.js.map +0 -1
- package/dist/test/event-gate-tags.test.d.ts +0 -2
- package/dist/test/event-gate-tags.test.d.ts.map +0 -1
- package/dist/test/event-gate-tags.test.js +0 -76
- package/dist/test/event-gate-tags.test.js.map +0 -1
- package/dist/test/event-gate.test.d.ts +0 -2
- package/dist/test/event-gate.test.d.ts.map +0 -1
- package/dist/test/event-gate.test.js +0 -919
- package/dist/test/event-gate.test.js.map +0 -1
- package/dist/test/explicit-prose-routing.test.d.ts +0 -11
- package/dist/test/explicit-prose-routing.test.d.ts.map +0 -1
- package/dist/test/explicit-prose-routing.test.js +0 -1715
- package/dist/test/explicit-prose-routing.test.js.map +0 -1
- package/dist/test/framework.test.d.ts +0 -5
- package/dist/test/framework.test.d.ts.map +0 -1
- package/dist/test/framework.test.js +0 -1048
- package/dist/test/framework.test.js.map +0 -1
- package/dist/test/gather-context-timeout.test.d.ts +0 -2
- package/dist/test/gather-context-timeout.test.d.ts.map +0 -1
- package/dist/test/gather-context-timeout.test.js +0 -120
- package/dist/test/gather-context-timeout.test.js.map +0 -1
- package/dist/test/health-snapshot.test.d.ts +0 -2
- package/dist/test/health-snapshot.test.d.ts.map +0 -1
- package/dist/test/health-snapshot.test.js +0 -74
- package/dist/test/health-snapshot.test.js.map +0 -1
- package/dist/test/helpers/coalescing-fixture.d.ts +0 -74
- package/dist/test/helpers/coalescing-fixture.d.ts.map +0 -1
- package/dist/test/helpers/coalescing-fixture.js +0 -152
- package/dist/test/helpers/coalescing-fixture.js.map +0 -1
- package/dist/test/helpers/coalescing-kill-child.d.ts +0 -2
- package/dist/test/helpers/coalescing-kill-child.d.ts.map +0 -1
- package/dist/test/helpers/coalescing-kill-child.js +0 -24
- package/dist/test/helpers/coalescing-kill-child.js.map +0 -1
- package/dist/test/helpers/mock-membrane.d.ts +0 -53
- package/dist/test/helpers/mock-membrane.d.ts.map +0 -1
- package/dist/test/helpers/mock-membrane.js +0 -168
- package/dist/test/helpers/mock-membrane.js.map +0 -1
- package/dist/test/helpers/pytc-stop-rows.d.ts +0 -11
- package/dist/test/helpers/pytc-stop-rows.d.ts.map +0 -1
- package/dist/test/helpers/pytc-stop-rows.js +0 -143
- package/dist/test/helpers/pytc-stop-rows.js.map +0 -1
- package/dist/test/helpers/quiesce-crash-child.d.ts +0 -2
- package/dist/test/helpers/quiesce-crash-child.d.ts.map +0 -1
- package/dist/test/helpers/quiesce-crash-child.js +0 -156
- package/dist/test/helpers/quiesce-crash-child.js.map +0 -1
- package/dist/test/hide-then-turn.test.d.ts +0 -2
- package/dist/test/hide-then-turn.test.d.ts.map +0 -1
- package/dist/test/hide-then-turn.test.js +0 -101
- package/dist/test/hide-then-turn.test.js.map +0 -1
- package/dist/test/history-module-overview.test.d.ts +0 -2
- package/dist/test/history-module-overview.test.d.ts.map +0 -1
- package/dist/test/history-module-overview.test.js +0 -711
- package/dist/test/history-module-overview.test.js.map +0 -1
- package/dist/test/history-module-realstore.test.d.ts +0 -2
- package/dist/test/history-module-realstore.test.d.ts.map +0 -1
- package/dist/test/history-module-realstore.test.js +0 -363
- package/dist/test/history-module-realstore.test.js.map +0 -1
- package/dist/test/history-module-semantic.test.d.ts +0 -2
- package/dist/test/history-module-semantic.test.d.ts.map +0 -1
- package/dist/test/history-module-semantic.test.js +0 -213
- package/dist/test/history-module-semantic.test.js.map +0 -1
- package/dist/test/history-module-ux.test.d.ts +0 -2
- package/dist/test/history-module-ux.test.d.ts.map +0 -1
- package/dist/test/history-module-ux.test.js +0 -304
- package/dist/test/history-module-ux.test.js.map +0 -1
- package/dist/test/history-module.test.d.ts +0 -2
- package/dist/test/history-module.test.d.ts.map +0 -1
- package/dist/test/history-module.test.js +0 -619
- package/dist/test/history-module.test.js.map +0 -1
- package/dist/test/history-semantic-review-findings.test.d.ts +0 -2
- package/dist/test/history-semantic-review-findings.test.d.ts.map +0 -1
- package/dist/test/history-semantic-review-findings.test.js +0 -338
- package/dist/test/history-semantic-review-findings.test.js.map +0 -1
- package/dist/test/history-semantic-sync-converges.test.d.ts +0 -2
- package/dist/test/history-semantic-sync-converges.test.d.ts.map +0 -1
- package/dist/test/history-semantic-sync-converges.test.js +0 -109
- package/dist/test/history-semantic-sync-converges.test.js.map +0 -1
- package/dist/test/host-quiesce.test.d.ts +0 -2
- package/dist/test/host-quiesce.test.d.ts.map +0 -1
- package/dist/test/host-quiesce.test.js +0 -988
- package/dist/test/host-quiesce.test.js.map +0 -1
- package/dist/test/inference-exhaustion-rewind.test.d.ts +0 -2
- package/dist/test/inference-exhaustion-rewind.test.d.ts.map +0 -1
- package/dist/test/inference-exhaustion-rewind.test.js +0 -387
- package/dist/test/inference-exhaustion-rewind.test.js.map +0 -1
- package/dist/test/inference-failure-bounds.test.d.ts +0 -2
- package/dist/test/inference-failure-bounds.test.d.ts.map +0 -1
- package/dist/test/inference-failure-bounds.test.js +0 -1202
- package/dist/test/inference-failure-bounds.test.js.map +0 -1
- package/dist/test/inference-failure-observability.test.d.ts +0 -2
- package/dist/test/inference-failure-observability.test.d.ts.map +0 -1
- package/dist/test/inference-failure-observability.test.js +0 -84
- package/dist/test/inference-failure-observability.test.js.map +0 -1
- package/dist/test/journal-tool.test.d.ts +0 -2
- package/dist/test/journal-tool.test.d.ts.map +0 -1
- package/dist/test/journal-tool.test.js +0 -33
- package/dist/test/journal-tool.test.js.map +0 -1
- package/dist/test/kv-unified-open-flight-retry.test.d.ts +0 -2
- package/dist/test/kv-unified-open-flight-retry.test.d.ts.map +0 -1
- package/dist/test/kv-unified-open-flight-retry.test.js +0 -189
- package/dist/test/kv-unified-open-flight-retry.test.js.map +0 -1
- package/dist/test/kv-unified-wiring.test.d.ts +0 -2
- package/dist/test/kv-unified-wiring.test.d.ts.map +0 -1
- package/dist/test/kv-unified-wiring.test.js +0 -95
- package/dist/test/kv-unified-wiring.test.js.map +0 -1
- package/dist/test/late-events-after-stop.test.d.ts +0 -2
- package/dist/test/late-events-after-stop.test.d.ts.map +0 -1
- package/dist/test/late-events-after-stop.test.js +0 -122
- package/dist/test/late-events-after-stop.test.js.map +0 -1
- package/dist/test/liveness-watchdog.test.d.ts +0 -2
- package/dist/test/liveness-watchdog.test.d.ts.map +0 -1
- package/dist/test/liveness-watchdog.test.js +0 -94
- package/dist/test/liveness-watchdog.test.js.map +0 -1
- package/dist/test/machine-close-provenance.test.d.ts +0 -2
- package/dist/test/machine-close-provenance.test.d.ts.map +0 -1
- package/dist/test/machine-close-provenance.test.js +0 -116
- package/dist/test/machine-close-provenance.test.js.map +0 -1
- package/dist/test/maintenance-timer.test.d.ts +0 -2
- package/dist/test/maintenance-timer.test.d.ts.map +0 -1
- package/dist/test/maintenance-timer.test.js +0 -1827
- package/dist/test/maintenance-timer.test.js.map +0 -1
- package/dist/test/mcp-server-bin.test.d.ts +0 -2
- package/dist/test/mcp-server-bin.test.d.ts.map +0 -1
- package/dist/test/mcp-server-bin.test.js +0 -142
- package/dist/test/mcp-server-bin.test.js.map +0 -1
- package/dist/test/mcpl-access-provider.test.d.ts +0 -2
- package/dist/test/mcpl-access-provider.test.d.ts.map +0 -1
- package/dist/test/mcpl-access-provider.test.js +0 -20
- package/dist/test/mcpl-access-provider.test.js.map +0 -1
- package/dist/test/mcpl-awareness-barrier.test.d.ts +0 -2
- package/dist/test/mcpl-awareness-barrier.test.d.ts.map +0 -1
- package/dist/test/mcpl-awareness-barrier.test.js +0 -769
- package/dist/test/mcpl-awareness-barrier.test.js.map +0 -1
- package/dist/test/mcpl-capability-grant.test.d.ts +0 -10
- package/dist/test/mcpl-capability-grant.test.d.ts.map +0 -1
- package/dist/test/mcpl-capability-grant.test.js +0 -111
- package/dist/test/mcpl-capability-grant.test.js.map +0 -1
- package/dist/test/mcpl-capability-scoping.test.d.ts +0 -2
- package/dist/test/mcpl-capability-scoping.test.d.ts.map +0 -1
- package/dist/test/mcpl-capability-scoping.test.js +0 -266
- package/dist/test/mcpl-capability-scoping.test.js.map +0 -1
- package/dist/test/mcpl-child-env.test.d.ts +0 -2
- package/dist/test/mcpl-child-env.test.d.ts.map +0 -1
- package/dist/test/mcpl-child-env.test.js +0 -158
- package/dist/test/mcpl-child-env.test.js.map +0 -1
- package/dist/test/mcpl-coalesced-content-parity.test.d.ts +0 -2
- package/dist/test/mcpl-coalesced-content-parity.test.d.ts.map +0 -1
- package/dist/test/mcpl-coalesced-content-parity.test.js +0 -532
- package/dist/test/mcpl-coalesced-content-parity.test.js.map +0 -1
- package/dist/test/mcpl-coalescing.test.d.ts +0 -2
- package/dist/test/mcpl-coalescing.test.d.ts.map +0 -1
- package/dist/test/mcpl-coalescing.test.js +0 -1125
- package/dist/test/mcpl-coalescing.test.js.map +0 -1
- package/dist/test/mcpl-config-diagnostics.test.d.ts +0 -2
- package/dist/test/mcpl-config-diagnostics.test.d.ts.map +0 -1
- package/dist/test/mcpl-config-diagnostics.test.js +0 -452
- package/dist/test/mcpl-config-diagnostics.test.js.map +0 -1
- package/dist/test/mcpl-empty-content.test.d.ts +0 -2
- package/dist/test/mcpl-empty-content.test.d.ts.map +0 -1
- package/dist/test/mcpl-empty-content.test.js +0 -368
- package/dist/test/mcpl-empty-content.test.js.map +0 -1
- package/dist/test/mcpl-gate-roundtrip.test.d.ts +0 -2
- package/dist/test/mcpl-gate-roundtrip.test.d.ts.map +0 -1
- package/dist/test/mcpl-gate-roundtrip.test.js +0 -285
- package/dist/test/mcpl-gate-roundtrip.test.js.map +0 -1
- package/dist/test/mcpl-list-status.test.d.ts +0 -2
- package/dist/test/mcpl-list-status.test.d.ts.map +0 -1
- package/dist/test/mcpl-list-status.test.js +0 -82
- package/dist/test/mcpl-list-status.test.js.map +0 -1
- package/dist/test/mcpl-manifest-changed.test.d.ts +0 -2
- package/dist/test/mcpl-manifest-changed.test.d.ts.map +0 -1
- package/dist/test/mcpl-manifest-changed.test.js +0 -178
- package/dist/test/mcpl-manifest-changed.test.js.map +0 -1
- package/dist/test/mcpl-push-convert.test.d.ts +0 -2
- package/dist/test/mcpl-push-convert.test.d.ts.map +0 -1
- package/dist/test/mcpl-push-convert.test.js +0 -42
- package/dist/test/mcpl-push-convert.test.js.map +0 -1
- package/dist/test/mcpl-reconnect-events.test.d.ts +0 -2
- package/dist/test/mcpl-reconnect-events.test.d.ts.map +0 -1
- package/dist/test/mcpl-reconnect-events.test.js +0 -144
- package/dist/test/mcpl-reconnect-events.test.js.map +0 -1
- package/dist/test/mcpl-reconnect-reregistration.test.d.ts +0 -2
- package/dist/test/mcpl-reconnect-reregistration.test.d.ts.map +0 -1
- package/dist/test/mcpl-reconnect-reregistration.test.js +0 -238
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +0 -1
- package/dist/test/mcpl-reference-fetcher.test.d.ts +0 -2
- package/dist/test/mcpl-reference-fetcher.test.d.ts.map +0 -1
- package/dist/test/mcpl-reference-fetcher.test.js +0 -427
- package/dist/test/mcpl-reference-fetcher.test.js.map +0 -1
- package/dist/test/mcpl-reference-fields.test.d.ts +0 -2
- package/dist/test/mcpl-reference-fields.test.d.ts.map +0 -1
- package/dist/test/mcpl-reference-fields.test.js +0 -149
- package/dist/test/mcpl-reference-fields.test.js.map +0 -1
- package/dist/test/mcpl-rejection-log-lines.test.d.ts +0 -2
- package/dist/test/mcpl-rejection-log-lines.test.d.ts.map +0 -1
- package/dist/test/mcpl-rejection-log-lines.test.js +0 -349
- package/dist/test/mcpl-rejection-log-lines.test.js.map +0 -1
- package/dist/test/mcpl-request-timeout.test.d.ts +0 -2
- package/dist/test/mcpl-request-timeout.test.d.ts.map +0 -1
- package/dist/test/mcpl-request-timeout.test.js +0 -138
- package/dist/test/mcpl-request-timeout.test.js.map +0 -1
- package/dist/test/mcpl-runtime-lifecycle.test.d.ts +0 -7
- package/dist/test/mcpl-runtime-lifecycle.test.d.ts.map +0 -1
- package/dist/test/mcpl-runtime-lifecycle.test.js +0 -142
- package/dist/test/mcpl-runtime-lifecycle.test.js.map +0 -1
- package/dist/test/mcpl-stderr-tail.test.d.ts +0 -2
- package/dist/test/mcpl-stderr-tail.test.d.ts.map +0 -1
- package/dist/test/mcpl-stderr-tail.test.js +0 -210
- package/dist/test/mcpl-stderr-tail.test.js.map +0 -1
- package/dist/test/mcpl-tool-class-listing.test.d.ts +0 -2
- package/dist/test/mcpl-tool-class-listing.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-class-listing.test.js +0 -147
- package/dist/test/mcpl-tool-class-listing.test.js.map +0 -1
- package/dist/test/mcpl-tool-classes.test.d.ts +0 -7
- package/dist/test/mcpl-tool-classes.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-classes.test.js +0 -80
- package/dist/test/mcpl-tool-classes.test.js.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts +0 -2
- package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-e2e.test.js +0 -237
- package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts +0 -11
- package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js +0 -412
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts +0 -2
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-script.test.js +0 -288
- package/dist/test/mcpl-tool-lifecycle-script.test.js.map +0 -1
- package/dist/test/mcpl-tool-lifecycle.test.d.ts +0 -11
- package/dist/test/mcpl-tool-lifecycle.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-lifecycle.test.js +0 -270
- package/dist/test/mcpl-tool-lifecycle.test.js.map +0 -1
- package/dist/test/mcpl-transport-close.test.d.ts +0 -2
- package/dist/test/mcpl-transport-close.test.d.ts.map +0 -1
- package/dist/test/mcpl-transport-close.test.js +0 -373
- package/dist/test/mcpl-transport-close.test.js.map +0 -1
- package/dist/test/mcpl-transport-spawn.test.d.ts +0 -2
- package/dist/test/mcpl-transport-spawn.test.d.ts.map +0 -1
- package/dist/test/mcpl-transport-spawn.test.js +0 -58
- package/dist/test/mcpl-transport-spawn.test.js.map +0 -1
- package/dist/test/mcpl-ws-transport.test.d.ts +0 -2
- package/dist/test/mcpl-ws-transport.test.d.ts.map +0 -1
- package/dist/test/mcpl-ws-transport.test.js +0 -259
- package/dist/test/mcpl-ws-transport.test.js.map +0 -1
- package/dist/test/module-context-integration.test.d.ts +0 -18
- package/dist/test/module-context-integration.test.d.ts.map +0 -1
- package/dist/test/module-context-integration.test.js +0 -181
- package/dist/test/module-context-integration.test.js.map +0 -1
- package/dist/test/offline-recovery-branch.test.d.ts +0 -2
- package/dist/test/offline-recovery-branch.test.d.ts.map +0 -1
- package/dist/test/offline-recovery-branch.test.js +0 -570
- package/dist/test/offline-recovery-branch.test.js.map +0 -1
- package/dist/test/operator-surgery.test.d.ts +0 -2
- package/dist/test/operator-surgery.test.d.ts.map +0 -1
- package/dist/test/operator-surgery.test.js +0 -595
- package/dist/test/operator-surgery.test.js.map +0 -1
- package/dist/test/ops-alert.test.d.ts +0 -2
- package/dist/test/ops-alert.test.d.ts.map +0 -1
- package/dist/test/ops-alert.test.js +0 -153
- package/dist/test/ops-alert.test.js.map +0 -1
- package/dist/test/overbudget-drain-breaker.test.d.ts +0 -2
- package/dist/test/overbudget-drain-breaker.test.d.ts.map +0 -1
- package/dist/test/overbudget-drain-breaker.test.js +0 -198
- package/dist/test/overbudget-drain-breaker.test.js.map +0 -1
- package/dist/test/pack-installs-alone.test.d.ts +0 -2
- package/dist/test/pack-installs-alone.test.d.ts.map +0 -1
- package/dist/test/pack-installs-alone.test.js +0 -284
- package/dist/test/pack-installs-alone.test.js.map +0 -1
- package/dist/test/per-agent-delivery.test.d.ts +0 -2
- package/dist/test/per-agent-delivery.test.d.ts.map +0 -1
- package/dist/test/per-agent-delivery.test.js +0 -108
- package/dist/test/per-agent-delivery.test.js.map +0 -1
- package/dist/test/physical-window-restart.test.d.ts +0 -16
- package/dist/test/physical-window-restart.test.d.ts.map +0 -1
- package/dist/test/physical-window-restart.test.js +0 -487
- package/dist/test/physical-window-restart.test.js.map +0 -1
- package/dist/test/placed-reaction-baseline.test.d.ts +0 -2
- package/dist/test/placed-reaction-baseline.test.d.ts.map +0 -1
- package/dist/test/placed-reaction-baseline.test.js +0 -312
- package/dist/test/placed-reaction-baseline.test.js.map +0 -1
- package/dist/test/present-while-acting.test.d.ts +0 -19
- package/dist/test/present-while-acting.test.d.ts.map +0 -1
- package/dist/test/present-while-acting.test.js +0 -1649
- package/dist/test/present-while-acting.test.js.map +0 -1
- package/dist/test/prose-segments.test.d.ts +0 -2
- package/dist/test/prose-segments.test.d.ts.map +0 -1
- package/dist/test/prose-segments.test.js +0 -52
- package/dist/test/prose-segments.test.js.map +0 -1
- package/dist/test/prose-stream-router.test.d.ts +0 -2
- package/dist/test/prose-stream-router.test.d.ts.map +0 -1
- package/dist/test/prose-stream-router.test.js +0 -171
- package/dist/test/prose-stream-router.test.js.map +0 -1
- package/dist/test/provider-acceleration-cooldown.test.d.ts +0 -2
- package/dist/test/provider-acceleration-cooldown.test.d.ts.map +0 -1
- package/dist/test/provider-acceleration-cooldown.test.js +0 -435
- package/dist/test/provider-acceleration-cooldown.test.js.map +0 -1
- package/dist/test/provider-waits.test.d.ts +0 -2
- package/dist/test/provider-waits.test.d.ts.map +0 -1
- package/dist/test/provider-waits.test.js +0 -2259
- package/dist/test/provider-waits.test.js.map +0 -1
- package/dist/test/puppet-tool-call.test.d.ts +0 -2
- package/dist/test/puppet-tool-call.test.d.ts.map +0 -1
- package/dist/test/puppet-tool-call.test.js +0 -226
- package/dist/test/puppet-tool-call.test.js.map +0 -1
- package/dist/test/puppet-turn-reservation.test.d.ts +0 -2
- package/dist/test/puppet-turn-reservation.test.d.ts.map +0 -1
- package/dist/test/puppet-turn-reservation.test.js +0 -139
- package/dist/test/puppet-turn-reservation.test.js.map +0 -1
- package/dist/test/reaction-baseline-platform.test.d.ts +0 -2
- package/dist/test/reaction-baseline-platform.test.d.ts.map +0 -1
- package/dist/test/reaction-baseline-platform.test.js +0 -124
- package/dist/test/reaction-baseline-platform.test.js.map +0 -1
- package/dist/test/record-journal.test.d.ts +0 -6
- package/dist/test/record-journal.test.d.ts.map +0 -1
- package/dist/test/record-journal.test.js +0 -269
- package/dist/test/record-journal.test.js.map +0 -1
- package/dist/test/refusal-reaction-baseline.test.d.ts +0 -2
- package/dist/test/refusal-reaction-baseline.test.d.ts.map +0 -1
- package/dist/test/refusal-reaction-baseline.test.js +0 -81
- package/dist/test/refusal-reaction-baseline.test.js.map +0 -1
- package/dist/test/refusal-rewind.test.d.ts +0 -2
- package/dist/test/refusal-rewind.test.d.ts.map +0 -1
- package/dist/test/refusal-rewind.test.js +0 -117
- package/dist/test/refusal-rewind.test.js.map +0 -1
- package/dist/test/refusal-single-call.test.d.ts +0 -2
- package/dist/test/refusal-single-call.test.d.ts.map +0 -1
- package/dist/test/refusal-single-call.test.js +0 -177
- package/dist/test/refusal-single-call.test.js.map +0 -1
- package/dist/test/release-bump.test.d.ts +0 -2
- package/dist/test/release-bump.test.d.ts.map +0 -1
- package/dist/test/release-bump.test.js +0 -248
- package/dist/test/release-bump.test.js.map +0 -1
- package/dist/test/release-changelog.test.d.ts +0 -2
- package/dist/test/release-changelog.test.d.ts.map +0 -1
- package/dist/test/release-changelog.test.js +0 -159
- package/dist/test/release-changelog.test.js.map +0 -1
- package/dist/test/release-holds.test.d.ts +0 -2
- package/dist/test/release-holds.test.d.ts.map +0 -1
- package/dist/test/release-holds.test.js +0 -456
- package/dist/test/release-holds.test.js.map +0 -1
- package/dist/test/same-round-think-policy.test.d.ts +0 -2
- package/dist/test/same-round-think-policy.test.d.ts.map +0 -1
- package/dist/test/same-round-think-policy.test.js +0 -853
- package/dist/test/same-round-think-policy.test.js.map +0 -1
- package/dist/test/save-recent-image.test.d.ts +0 -26
- package/dist/test/save-recent-image.test.d.ts.map +0 -1
- package/dist/test/save-recent-image.test.js +0 -974
- package/dist/test/save-recent-image.test.js.map +0 -1
- package/dist/test/shutdown-errors.test.d.ts +0 -2
- package/dist/test/shutdown-errors.test.d.ts.map +0 -1
- package/dist/test/shutdown-errors.test.js +0 -662
- package/dist/test/shutdown-errors.test.js.map +0 -1
- package/dist/test/silent-heartbeat-rows.test.d.ts +0 -2
- package/dist/test/silent-heartbeat-rows.test.d.ts.map +0 -1
- package/dist/test/silent-heartbeat-rows.test.js +0 -451
- package/dist/test/silent-heartbeat-rows.test.js.map +0 -1
- package/dist/test/silent-heartbeat.test.d.ts +0 -2
- package/dist/test/silent-heartbeat.test.d.ts.map +0 -1
- package/dist/test/silent-heartbeat.test.js +0 -165
- package/dist/test/silent-heartbeat.test.js.map +0 -1
- package/dist/test/state-scaling-e2e.test.d.ts +0 -22
- package/dist/test/state-scaling-e2e.test.d.ts.map +0 -1
- package/dist/test/state-scaling-e2e.test.js +0 -206
- package/dist/test/state-scaling-e2e.test.js.map +0 -1
- package/dist/test/state-scaling.test.d.ts +0 -12
- package/dist/test/state-scaling.test.d.ts.map +0 -1
- package/dist/test/state-scaling.test.js +0 -251
- package/dist/test/state-scaling.test.js.map +0 -1
- package/dist/test/stop-with-host-operations.test.d.ts +0 -20
- package/dist/test/stop-with-host-operations.test.d.ts.map +0 -1
- package/dist/test/stop-with-host-operations.test.js +0 -1174
- package/dist/test/stop-with-host-operations.test.js.map +0 -1
- package/dist/test/stop-with-work-in-flight.test.d.ts +0 -23
- package/dist/test/stop-with-work-in-flight.test.d.ts.map +0 -1
- package/dist/test/stop-with-work-in-flight.test.js +0 -784
- package/dist/test/stop-with-work-in-flight.test.js.map +0 -1
- package/dist/test/surgery-marks-receipt.test.d.ts +0 -2
- package/dist/test/surgery-marks-receipt.test.d.ts.map +0 -1
- package/dist/test/surgery-marks-receipt.test.js +0 -540
- package/dist/test/surgery-marks-receipt.test.js.map +0 -1
- package/dist/test/thinking-token-stamp.test.d.ts +0 -12
- package/dist/test/thinking-token-stamp.test.d.ts.map +0 -1
- package/dist/test/thinking-token-stamp.test.js +0 -129
- package/dist/test/thinking-token-stamp.test.js.map +0 -1
- package/dist/test/timezone.test.d.ts +0 -2
- package/dist/test/timezone.test.d.ts.map +0 -1
- package/dist/test/timezone.test.js +0 -18
- package/dist/test/timezone.test.js.map +0 -1
- package/dist/test/tool-batch-hook.test.d.ts +0 -8
- package/dist/test/tool-batch-hook.test.d.ts.map +0 -1
- package/dist/test/tool-batch-hook.test.js +0 -275
- package/dist/test/tool-batch-hook.test.js.map +0 -1
- package/dist/test/tool-policy.test.d.ts +0 -2
- package/dist/test/tool-policy.test.d.ts.map +0 -1
- package/dist/test/tool-policy.test.js +0 -51
- package/dist/test/tool-policy.test.js.map +0 -1
- package/dist/test/tool-presentation.test.d.ts +0 -2
- package/dist/test/tool-presentation.test.d.ts.map +0 -1
- package/dist/test/tool-presentation.test.js +0 -268
- package/dist/test/tool-presentation.test.js.map +0 -1
- package/dist/test/tool-result-content.test.d.ts +0 -2
- package/dist/test/tool-result-content.test.d.ts.map +0 -1
- package/dist/test/tool-result-content.test.js +0 -37
- package/dist/test/tool-result-content.test.js.map +0 -1
- package/dist/test/tool-result-guard.test.d.ts +0 -2
- package/dist/test/tool-result-guard.test.d.ts.map +0 -1
- package/dist/test/tool-result-guard.test.js +0 -3729
- package/dist/test/tool-result-guard.test.js.map +0 -1
- package/dist/test/tool-result-history.test.d.ts +0 -7
- package/dist/test/tool-result-history.test.d.ts.map +0 -1
- package/dist/test/tool-result-history.test.js +0 -156
- package/dist/test/tool-result-history.test.js.map +0 -1
- package/dist/test/tool-result-spill.test.d.ts +0 -13
- package/dist/test/tool-result-spill.test.d.ts.map +0 -1
- package/dist/test/tool-result-spill.test.js +0 -586
- package/dist/test/tool-result-spill.test.js.map +0 -1
- package/dist/test/tool-wrapper-prose-guard-integration.test.d.ts +0 -2
- package/dist/test/tool-wrapper-prose-guard-integration.test.d.ts.map +0 -1
- package/dist/test/tool-wrapper-prose-guard-integration.test.js +0 -485
- package/dist/test/tool-wrapper-prose-guard-integration.test.js.map +0 -1
- package/dist/test/tool-wrapper-prose-guard.test.d.ts +0 -2
- package/dist/test/tool-wrapper-prose-guard.test.d.ts.map +0 -1
- package/dist/test/tool-wrapper-prose-guard.test.js +0 -34
- package/dist/test/tool-wrapper-prose-guard.test.js.map +0 -1
- package/dist/test/tools-observe-parser-conformance.test.d.ts +0 -2
- package/dist/test/tools-observe-parser-conformance.test.d.ts.map +0 -1
- package/dist/test/tools-observe-parser-conformance.test.js +0 -129
- package/dist/test/tools-observe-parser-conformance.test.js.map +0 -1
- package/dist/test/trunk-channel-routing.test.d.ts +0 -19
- package/dist/test/trunk-channel-routing.test.d.ts.map +0 -1
- package/dist/test/trunk-channel-routing.test.js +0 -652
- package/dist/test/trunk-channel-routing.test.js.map +0 -1
- package/dist/test/tune-out-e2e.test.d.ts +0 -2
- package/dist/test/tune-out-e2e.test.d.ts.map +0 -1
- package/dist/test/tune-out-e2e.test.js +0 -142
- package/dist/test/tune-out-e2e.test.js.map +0 -1
- package/dist/test/tune-out-gate-composition.test.d.ts +0 -2
- package/dist/test/tune-out-gate-composition.test.d.ts.map +0 -1
- package/dist/test/tune-out-gate-composition.test.js +0 -221
- package/dist/test/tune-out-gate-composition.test.js.map +0 -1
- package/dist/test/tune-out-lifecycle.test.d.ts +0 -2
- package/dist/test/tune-out-lifecycle.test.d.ts.map +0 -1
- package/dist/test/tune-out-lifecycle.test.js +0 -121
- package/dist/test/tune-out-lifecycle.test.js.map +0 -1
- package/dist/test/turn-trigger-provenance.test.d.ts +0 -2
- package/dist/test/turn-trigger-provenance.test.d.ts.map +0 -1
- package/dist/test/turn-trigger-provenance.test.js +0 -137
- package/dist/test/turn-trigger-provenance.test.js.map +0 -1
- package/dist/test/unconvertible-tool-result.test.d.ts +0 -2
- package/dist/test/unconvertible-tool-result.test.d.ts.map +0 -1
- package/dist/test/unconvertible-tool-result.test.js +0 -358
- package/dist/test/unconvertible-tool-result.test.js.map +0 -1
- package/dist/test/unsent-compiles.test.d.ts +0 -2
- package/dist/test/unsent-compiles.test.d.ts.map +0 -1
- package/dist/test/unsent-compiles.test.js +0 -139
- package/dist/test/unsent-compiles.test.js.map +0 -1
- package/dist/test/usage-accounting.test.d.ts +0 -2
- package/dist/test/usage-accounting.test.d.ts.map +0 -1
- package/dist/test/usage-accounting.test.js +0 -13
- package/dist/test/usage-accounting.test.js.map +0 -1
- package/dist/test/usage-tracker.test.d.ts +0 -2
- package/dist/test/usage-tracker.test.d.ts.map +0 -1
- package/dist/test/usage-tracker.test.js +0 -129
- package/dist/test/usage-tracker.test.js.map +0 -1
- package/dist/test/utils-surface.test.d.ts +0 -2
- package/dist/test/utils-surface.test.d.ts.map +0 -1
- package/dist/test/utils-surface.test.js +0 -145
- package/dist/test/utils-surface.test.js.map +0 -1
- package/dist/test/workspace-branch-continuation.test.d.ts +0 -15
- package/dist/test/workspace-branch-continuation.test.d.ts.map +0 -1
- package/dist/test/workspace-branch-continuation.test.js +0 -183
- package/dist/test/workspace-branch-continuation.test.js.map +0 -1
- package/dist/test/workspace-materialize-freshness.test.d.ts +0 -16
- package/dist/test/workspace-materialize-freshness.test.d.ts.map +0 -1
- package/dist/test/workspace-materialize-freshness.test.js +0 -252
- package/dist/test/workspace-materialize-freshness.test.js.map +0 -1
- package/dist/test/workspace-path-containment.test.d.ts +0 -19
- package/dist/test/workspace-path-containment.test.d.ts.map +0 -1
- package/dist/test/workspace-path-containment.test.js +0 -91
- package/dist/test/workspace-path-containment.test.js.map +0 -1
- package/dist/test/workspace-read-file-from-disk.test.d.ts +0 -17
- package/dist/test/workspace-read-file-from-disk.test.d.ts.map +0 -1
- package/dist/test/workspace-read-file-from-disk.test.js +0 -174
- package/dist/test/workspace-read-file-from-disk.test.js.map +0 -1
- package/dist/test/workspace-read-image.test.d.ts +0 -2
- package/dist/test/workspace-read-image.test.d.ts.map +0 -1
- package/dist/test/workspace-read-image.test.js +0 -597
- package/dist/test/workspace-read-image.test.js.map +0 -1
- package/dist/test/workspace-read-text.test.d.ts +0 -2
- package/dist/test/workspace-read-text.test.d.ts.map +0 -1
- package/dist/test/workspace-read-text.test.js +0 -101
- package/dist/test/workspace-read-text.test.js.map +0 -1
- package/dist/test/workspace-restore-order.test.d.ts +0 -16
- package/dist/test/workspace-restore-order.test.d.ts.map +0 -1
- package/dist/test/workspace-restore-order.test.js +0 -140
- package/dist/test/workspace-restore-order.test.js.map +0 -1
- package/dist/test/workspace-three-way.test.d.ts +0 -12
- package/dist/test/workspace-three-way.test.d.ts.map +0 -1
- package/dist/test/workspace-three-way.test.js +0 -4094
- package/dist/test/workspace-three-way.test.js.map +0 -1
- package/dist/test/workspace-watcher.test.d.ts +0 -2
- package/dist/test/workspace-watcher.test.d.ts.map +0 -1
- package/dist/test/workspace-watcher.test.js +0 -281
- package/dist/test/workspace-watcher.test.js.map +0 -1
- package/dist/test/xml-round-persist.test.d.ts +0 -19
- package/dist/test/xml-round-persist.test.d.ts.map +0 -1
- package/dist/test/xml-round-persist.test.js +0 -235
- package/dist/test/xml-round-persist.test.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -1,1649 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* "Present while acting": the two halves that keep an agent conversationally
|
|
3
|
-
* alive during a long tool-using turn.
|
|
4
|
-
*
|
|
5
|
-
* SPEAK-while-acting — each round's prose is routed to the locus LIVE when
|
|
6
|
-
* the round yields its tool calls. Explicit-send suppression lasts until new
|
|
7
|
-
* external input begins another conversational round; the 'complete' case
|
|
8
|
-
* routes only trailing prose.
|
|
9
|
-
* Previously all segments were batched to the end of the turn.
|
|
10
|
-
*
|
|
11
|
-
* HEAR-while-acting — messages arriving mid-turn (deferred by addMessage
|
|
12
|
-
* while tool_use blocks are pending) are flushed to the context window at
|
|
13
|
-
* the tool-result boundary AND injected into the live stream via
|
|
14
|
-
* provideToolResults(results, { injectedMessages }) (membrane ≥0.5.72), so
|
|
15
|
-
* the next round of the SAME turn sees them instead of the agent staying
|
|
16
|
-
* deaf until the turn ends.
|
|
17
|
-
*/
|
|
18
|
-
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
19
|
-
import assert from 'node:assert/strict';
|
|
20
|
-
import { tmpdir } from 'node:os';
|
|
21
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
22
|
-
import { join } from 'node:path';
|
|
23
|
-
import { AgentFramework } from '../src/index.js';
|
|
24
|
-
import { MockMembrane, createMockResponse } from './helpers/mock-membrane.js';
|
|
25
|
-
import { MembraneError } from '@animalabs/membrane';
|
|
26
|
-
import { JsStore } from '@animalabs/chronicle';
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// Test module: tools that optionally emit a mid-turn external message
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
class RobotModule {
|
|
31
|
-
name = 'robot';
|
|
32
|
-
framework = null;
|
|
33
|
-
/** When set, the next `move` call pushes this text as an external message
|
|
34
|
-
* BEFORE returning its result (simulating a chat reply arriving while the
|
|
35
|
-
* tool executes). */
|
|
36
|
-
interjection = null;
|
|
37
|
-
/** Optional routing locus attached to the interjected message. */
|
|
38
|
-
interjectionChannelId = null;
|
|
39
|
-
/** Full metadata override for the interjected message (wins over
|
|
40
|
-
* interjectionChannelId when set) — e.g. reaction tags. */
|
|
41
|
-
interjectionMetadata = null;
|
|
42
|
-
/** Delay tool completion so live routing deterministically precedes it. */
|
|
43
|
-
toolDelayMs = 0;
|
|
44
|
-
/** Run once, during the next turn start's context gathering (the turn is
|
|
45
|
-
* alive and its stream doesn't exist yet). */
|
|
46
|
-
startHook = null;
|
|
47
|
-
async start(_ctx) { }
|
|
48
|
-
async stop() { }
|
|
49
|
-
async gatherContext() {
|
|
50
|
-
const hook = this.startHook;
|
|
51
|
-
this.startHook = null;
|
|
52
|
-
if (hook)
|
|
53
|
-
await hook();
|
|
54
|
-
return [];
|
|
55
|
-
}
|
|
56
|
-
getTools() {
|
|
57
|
-
return [
|
|
58
|
-
{
|
|
59
|
-
name: 'move',
|
|
60
|
-
description: 'Move the robot',
|
|
61
|
-
inputSchema: { type: 'object', properties: { dir: { type: 'string' } } },
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
name: 'send_message',
|
|
65
|
-
description: 'Explicitly send a message',
|
|
66
|
-
inputSchema: { type: 'object', properties: { text: { type: 'string' } } },
|
|
67
|
-
},
|
|
68
|
-
// World-surface publication verbs (Eidoverse). Bare names matter: the
|
|
69
|
-
// framework strips the `robot--` prefix before consulting its sets.
|
|
70
|
-
{
|
|
71
|
-
name: 'say',
|
|
72
|
-
description: 'Say something aloud in the world',
|
|
73
|
-
inputSchema: { type: 'object', properties: { text: { type: 'string' } } },
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
name: 'whisper',
|
|
77
|
-
description: 'Whisper to someone in the world',
|
|
78
|
-
inputSchema: { type: 'object', properties: { to: { type: 'string' }, text: { type: 'string' } } },
|
|
79
|
-
},
|
|
80
|
-
];
|
|
81
|
-
}
|
|
82
|
-
async handleToolCall(call) {
|
|
83
|
-
if (this.toolDelayMs > 0) {
|
|
84
|
-
await new Promise((r) => setTimeout(r, this.toolDelayMs));
|
|
85
|
-
}
|
|
86
|
-
if (this.interjection) {
|
|
87
|
-
const text = this.interjection;
|
|
88
|
-
this.interjection = null;
|
|
89
|
-
this.framework.pushEvent({
|
|
90
|
-
type: 'external-message',
|
|
91
|
-
source: 'test',
|
|
92
|
-
content: text,
|
|
93
|
-
metadata: this.interjectionMetadata
|
|
94
|
-
?? (this.interjectionChannelId
|
|
95
|
-
? { channelId: this.interjectionChannelId }
|
|
96
|
-
: {}),
|
|
97
|
-
});
|
|
98
|
-
// Give the run loop a beat to process the queued message while this
|
|
99
|
-
// tool round is still pending (pendingAssistantBlocks non-empty), so
|
|
100
|
-
// it lands in deferredMessages before the tool-result event.
|
|
101
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
102
|
-
}
|
|
103
|
-
return { success: true, data: { ok: true, tool: call.name } };
|
|
104
|
-
}
|
|
105
|
-
async onProcess(event, _state) {
|
|
106
|
-
if (event.type === 'external-message') {
|
|
107
|
-
const text = String(event.content);
|
|
108
|
-
const metadata = event.metadata;
|
|
109
|
-
return {
|
|
110
|
-
addMessages: [
|
|
111
|
-
{
|
|
112
|
-
participant: 'Antra',
|
|
113
|
-
content: [{ type: 'text', text }],
|
|
114
|
-
...(metadata ? { metadata } : {}),
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
// Only the initial 'go' starts a turn. Mid-turn interjections must
|
|
118
|
-
// NOT request inference — we're testing mid-turn delivery, not wakes.
|
|
119
|
-
requestInference: text === 'go',
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
return {};
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
/** Minimal ChannelRegistry stub covering everything driveStream touches. */
|
|
126
|
-
function stubChannelRegistry(framework) {
|
|
127
|
-
const routed = [];
|
|
128
|
-
let locusCalls = 0;
|
|
129
|
-
const explicit = {
|
|
130
|
-
resolveLocus: () => {
|
|
131
|
-
locusCalls++;
|
|
132
|
-
return `chan-live-${locusCalls}`;
|
|
133
|
-
},
|
|
134
|
-
routeSpeech: async (_agent, text, locus) => {
|
|
135
|
-
routed.push({ text, locus: locus ?? null });
|
|
136
|
-
// Mirror the real registry's outcome shape (delivery receipts read it).
|
|
137
|
-
return locus ? { delivered: true, channelId: locus } : null;
|
|
138
|
-
},
|
|
139
|
-
getDefaultPublishChannel: () => null,
|
|
140
|
-
isChannelOpen: () => true,
|
|
141
|
-
getDescriptor: () => undefined,
|
|
142
|
-
getChannelTools: () => [],
|
|
143
|
-
};
|
|
144
|
-
// Everything else driveStream/stop touches (startTyping, stopTyping,
|
|
145
|
-
// stopAll, ensureChannelRegistered, ...) becomes a no-op via Proxy so the
|
|
146
|
-
// stub doesn't chase the real registry's surface.
|
|
147
|
-
framework.channelRegistry = new Proxy(explicit, {
|
|
148
|
-
get: (target, prop) => (prop in target ? target[prop] : () => undefined),
|
|
149
|
-
});
|
|
150
|
-
return routed;
|
|
151
|
-
}
|
|
152
|
-
// ---------------------------------------------------------------------------
|
|
153
|
-
describe('present while acting', () => {
|
|
154
|
-
let tempDir;
|
|
155
|
-
let membrane;
|
|
156
|
-
let module;
|
|
157
|
-
beforeEach(() => {
|
|
158
|
-
tempDir = mkdtempSync(join(tmpdir(), 'pwa-test-'));
|
|
159
|
-
membrane = new MockMembrane();
|
|
160
|
-
module = new RobotModule();
|
|
161
|
-
});
|
|
162
|
-
afterEach(() => {
|
|
163
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
164
|
-
});
|
|
165
|
-
async function createFramework(agentOptions = {}) {
|
|
166
|
-
const framework = await AgentFramework.create({
|
|
167
|
-
storePath: join(tempDir, 'test.chronicle'),
|
|
168
|
-
membrane: membrane.asMembrane(),
|
|
169
|
-
agents: [
|
|
170
|
-
{ name: 'assistant', model: 'test-model', systemPrompt: 'You are a robot pilot.', ...agentOptions },
|
|
171
|
-
],
|
|
172
|
-
modules: [module],
|
|
173
|
-
});
|
|
174
|
-
module.framework = framework;
|
|
175
|
-
return framework;
|
|
176
|
-
}
|
|
177
|
-
function trigger(framework) {
|
|
178
|
-
framework.pushEvent({
|
|
179
|
-
type: 'external-message',
|
|
180
|
-
source: 'test',
|
|
181
|
-
content: 'go',
|
|
182
|
-
metadata: {},
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
// -------------------------------------------------------------------------
|
|
186
|
-
// Speak-while-acting
|
|
187
|
-
// -------------------------------------------------------------------------
|
|
188
|
-
it('routes each round\'s prose live and only trailing prose at complete', async () => {
|
|
189
|
-
// Round 1: narration + move; Round 2: more narration + move; final: postscript
|
|
190
|
-
membrane.pushResponse(createMockResponse([
|
|
191
|
-
{ type: 'text', text: 'Heading to the door now!' },
|
|
192
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
193
|
-
], 'tool_use'));
|
|
194
|
-
membrane.pushResponse(createMockResponse([
|
|
195
|
-
{ type: 'text', text: 'Door reached, opening it.' },
|
|
196
|
-
{ type: 'tool_use', id: 'c2', name: 'robot--move', input: { dir: 'east' } },
|
|
197
|
-
], 'tool_use'));
|
|
198
|
-
membrane.pushResponse(createMockResponse([
|
|
199
|
-
{ type: 'text', text: 'Arrived.' },
|
|
200
|
-
]));
|
|
201
|
-
const framework = await createFramework();
|
|
202
|
-
const routed = stubChannelRegistry(framework);
|
|
203
|
-
module.toolDelayMs = 25;
|
|
204
|
-
// Track live ordering: when the first segment routes, no tool results
|
|
205
|
-
// may have been provided yet (i.e. delivery happened DURING the round).
|
|
206
|
-
const resultsAtRouteTime = [];
|
|
207
|
-
const registry = framework.channelRegistry;
|
|
208
|
-
const origRoute = registry.routeSpeech;
|
|
209
|
-
registry.routeSpeech = async (a, t, l) => {
|
|
210
|
-
resultsAtRouteTime.push(membrane.lastStream.receivedToolResults.length);
|
|
211
|
-
return origRoute(a, t, l);
|
|
212
|
-
};
|
|
213
|
-
trigger(framework);
|
|
214
|
-
await framework.runUntilIdle();
|
|
215
|
-
assert.deepEqual(routed.map((r) => r.text), ['Heading to the door now!', 'Door reached, opening it.', 'Arrived.'], 'all three segments delivered, in order');
|
|
216
|
-
// Live: segment N routed before round N's tool results were provided
|
|
217
|
-
assert.equal(resultsAtRouteTime[0], 0, 'round-1 prose routed before round-1 results');
|
|
218
|
-
assert.equal(resultsAtRouteTime[1], 1, 'round-2 prose routed before round-2 results');
|
|
219
|
-
// Locus resolved ONCE and pinned for the whole turn (incl. trailing prose)
|
|
220
|
-
assert.deepEqual(routed.map((r) => r.locus), ['chan-live-1', 'chan-live-1', 'chan-live-1'], 'one locus resolution, pinned across the turn');
|
|
221
|
-
await framework.stop();
|
|
222
|
-
});
|
|
223
|
-
it('sticky silencing: a silencing round suppresses its own and all later prose', async () => {
|
|
224
|
-
membrane.pushResponse(createMockResponse([
|
|
225
|
-
{ type: 'text', text: 'sending it directly' },
|
|
226
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--send_message', input: { text: 'hi' } },
|
|
227
|
-
], 'tool_use'));
|
|
228
|
-
membrane.pushResponse(createMockResponse([
|
|
229
|
-
{ type: 'text', text: 'Narrating round two.' },
|
|
230
|
-
{ type: 'tool_use', id: 'c2', name: 'robot--move', input: { dir: 'up' } },
|
|
231
|
-
], 'tool_use'));
|
|
232
|
-
membrane.pushResponse(createMockResponse([]));
|
|
233
|
-
const framework = await createFramework();
|
|
234
|
-
const routed = stubChannelRegistry(framework);
|
|
235
|
-
trigger(framework);
|
|
236
|
-
await framework.runUntilIdle();
|
|
237
|
-
assert.deepEqual(routed.map((r) => r.text), [], 'explicit send in round 1 silences the turn from that round onward');
|
|
238
|
-
await framework.stop();
|
|
239
|
-
});
|
|
240
|
-
it('sticky silencing is forward-only: earlier rounds\' prose still routes', async () => {
|
|
241
|
-
membrane.pushResponse(createMockResponse([
|
|
242
|
-
{ type: 'text', text: 'Round one narration.' },
|
|
243
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'up' } },
|
|
244
|
-
], 'tool_use'));
|
|
245
|
-
membrane.pushResponse(createMockResponse([
|
|
246
|
-
{ type: 'text', text: 'private planning' },
|
|
247
|
-
{ type: 'tool_use', id: 'c2', name: 'robot--send_message', input: { text: 'hi' } },
|
|
248
|
-
], 'tool_use'));
|
|
249
|
-
membrane.pushResponse(createMockResponse([
|
|
250
|
-
{ type: 'text', text: 'trailing postscript' },
|
|
251
|
-
]));
|
|
252
|
-
const framework = await createFramework();
|
|
253
|
-
const routed = stubChannelRegistry(framework);
|
|
254
|
-
trigger(framework);
|
|
255
|
-
await framework.runUntilIdle();
|
|
256
|
-
assert.deepEqual(routed.map((r) => r.text), ['Round one narration.'], 'round-1 prose delivered live; round-2 (silencing) and trailing suppressed');
|
|
257
|
-
await framework.stop();
|
|
258
|
-
});
|
|
259
|
-
it('new injected channel input resets send suppression but the turn locus stays frozen', async () => {
|
|
260
|
-
// While handling one channel, the agent explicitly sends that response; a
|
|
261
|
-
// message from another channel arrives during the send, and the terminal
|
|
262
|
-
// prose answers the new message. The suppression is cleared (the prose IS
|
|
263
|
-
// delivered) — but it lands in the TURN's frozen locus, not the injected
|
|
264
|
-
// message's channel: ambient input must never capture the agent's voice
|
|
265
|
-
// mid-turn (2026-07-21 Cairn lounge misroute). A cross-channel reply
|
|
266
|
-
// needs an explicit send.
|
|
267
|
-
membrane.pushResponse(createMockResponse([
|
|
268
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--send_message', input: { text: 'reply to room4' } },
|
|
269
|
-
], 'tool_use'));
|
|
270
|
-
membrane.pushResponse(createMockResponse([
|
|
271
|
-
{ type: 'text', text: 'Yes, I want to try the VR space.' },
|
|
272
|
-
]));
|
|
273
|
-
const framework = await createFramework();
|
|
274
|
-
const routed = stubChannelRegistry(framework);
|
|
275
|
-
module.interjection = 'Want to try a VR space?';
|
|
276
|
-
module.interjectionChannelId = 'discord:guild:fable';
|
|
277
|
-
trigger(framework);
|
|
278
|
-
await framework.runUntilIdle();
|
|
279
|
-
assert.deepEqual(routed, [
|
|
280
|
-
{ text: 'Yes, I want to try the VR space.', locus: 'chan-live-1' },
|
|
281
|
-
]);
|
|
282
|
-
await framework.stop();
|
|
283
|
-
});
|
|
284
|
-
it('an ADDRESSED mid-turn injection re-pins the locus for subsequent prose', async () => {
|
|
285
|
-
// 2026-07-31 Mythos misroutes (n=6): someone explicitly addresses the
|
|
286
|
-
// agent from another channel mid-turn (mention / reply / DM); the
|
|
287
|
-
// agent's trailing prose answers THEM but the frozen pin delivered it
|
|
288
|
-
// to the stale locus (the hospital scene into antra's DM, and its
|
|
289
|
-
// mirror). Addressed conversational injections — the chat:addressed
|
|
290
|
-
// signal turn-START batching already prefers — now move the pin, with a
|
|
291
|
-
// one-line [routing] notice riding the same injection batch so window
|
|
292
|
-
// and wire agree. Ambient injections still cannot (previous test).
|
|
293
|
-
membrane.pushResponse(createMockResponse([
|
|
294
|
-
{ type: 'text', text: 'Station four, proceeding.' },
|
|
295
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
296
|
-
], 'tool_use'));
|
|
297
|
-
membrane.pushResponse(createMockResponse([
|
|
298
|
-
{ type: 'text', text: 'Answering the person who addressed me.' },
|
|
299
|
-
]));
|
|
300
|
-
const framework = await createFramework();
|
|
301
|
-
const routed = stubChannelRegistry(framework);
|
|
302
|
-
module.toolDelayMs = 25;
|
|
303
|
-
module.interjection = 'hey, quick question over here?';
|
|
304
|
-
module.interjectionMetadata = { channelId: 'discord:dm:antra', tags: ['chat:addressed'] };
|
|
305
|
-
const notices = [];
|
|
306
|
-
framework.onTrace((event) => {
|
|
307
|
-
const e = event;
|
|
308
|
-
if (e.type === 'message:added' && e.source === 'routing-notice')
|
|
309
|
-
notices.push(e.source);
|
|
310
|
-
});
|
|
311
|
-
trigger(framework);
|
|
312
|
-
await framework.runUntilIdle();
|
|
313
|
-
// Round-1 prose predates the injection and belongs to the old pin;
|
|
314
|
-
// trailing prose follows the addressed speaker.
|
|
315
|
-
assert.deepEqual(routed, [
|
|
316
|
-
{ text: 'Station four, proceeding.', locus: 'chan-live-1' },
|
|
317
|
-
{ text: 'Answering the person who addressed me.', locus: 'discord:dm:antra' },
|
|
318
|
-
]);
|
|
319
|
-
// Boot-baseline announcement + the mid-turn re-pin notice.
|
|
320
|
-
assert.equal(notices.length, 2, 'the re-pin produced exactly one routing notice');
|
|
321
|
-
// The notice rode the injection batch to the live stream (window == wire).
|
|
322
|
-
const injectedBatches = membrane.lastStream.receivedToolResultOptions
|
|
323
|
-
.map((o) => o?.injectedMessages ?? []);
|
|
324
|
-
const wired = injectedBatches.flat().map((m) => JSON.stringify(m.content));
|
|
325
|
-
assert.ok(wired.some((s) => s.includes('quick question')), 'the addressed message itself was injected');
|
|
326
|
-
assert.ok(wired.some((s) => s.includes('[routing] The conversation moved to discord:dm:antra')), 'the re-pin notice was injected alongside it');
|
|
327
|
-
await framework.stop();
|
|
328
|
-
});
|
|
329
|
-
it('a follow-up in a channel the agent explicitly engaged this turn re-pins', async () => {
|
|
330
|
-
// 2026-07-31 n=7 (q's #portables reply): the agent explicitly sent into
|
|
331
|
-
// a channel this turn; someone replies there WITHOUT a mention (ambient
|
|
332
|
-
// by tag, addressed by context). The engaged-channel leg moves the pin;
|
|
333
|
-
// trailing prose answering them follows.
|
|
334
|
-
membrane.pushResponse(createMockResponse([
|
|
335
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
336
|
-
], 'tool_use'));
|
|
337
|
-
membrane.pushResponse(createMockResponse([
|
|
338
|
-
{ type: 'text', text: 'Good plan on both counts.' },
|
|
339
|
-
]));
|
|
340
|
-
const framework = await createFramework();
|
|
341
|
-
const routed = stubChannelRegistry(framework);
|
|
342
|
-
module.toolDelayMs = 25;
|
|
343
|
-
module.interjection = 'I need to swap out this eye screen';
|
|
344
|
-
module.interjectionMetadata = {
|
|
345
|
-
channelId: 'discord:guild:portables',
|
|
346
|
-
tags: ['chat:ambient', 'chat:from-human'],
|
|
347
|
-
};
|
|
348
|
-
// Simulate an explicit send into #portables earlier in THIS turn (the
|
|
349
|
-
// production record happens on the MCPL send path; the harness module's
|
|
350
|
-
// tools are not MCPL, so seed the turn's engaged set mid-turn — after
|
|
351
|
-
// the turn opens, before the injection boundary).
|
|
352
|
-
const origHandle = module.handleToolCall.bind(module);
|
|
353
|
-
module.handleToolCall = async (call) => {
|
|
354
|
-
framework.turnRouting.get('assistant').engaged.add('discord:guild:portables');
|
|
355
|
-
return origHandle(call);
|
|
356
|
-
};
|
|
357
|
-
trigger(framework);
|
|
358
|
-
await framework.runUntilIdle();
|
|
359
|
-
assert.deepEqual(routed, [
|
|
360
|
-
{ text: 'Good plan on both counts.', locus: 'discord:guild:portables' },
|
|
361
|
-
], 'trailing prose followed the human reply into the engaged channel');
|
|
362
|
-
await framework.stop();
|
|
363
|
-
});
|
|
364
|
-
it('an agent-resident follow-up in an engaged channel re-pins too (no author-kind filter)', async () => {
|
|
365
|
-
// Fleet participants include agent-residents — "bot" by Discord flag,
|
|
366
|
-
// full conversational participants in fact. Their in-flow replies in a
|
|
367
|
-
// channel the agent just engaged move the pin exactly like anyone
|
|
368
|
-
// else's (antra, 2026-07-31: the author-kind distinction is fragile and
|
|
369
|
-
// wrong for this fleet).
|
|
370
|
-
membrane.pushResponse(createMockResponse([
|
|
371
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
372
|
-
], 'tool_use'));
|
|
373
|
-
membrane.pushResponse(createMockResponse([
|
|
374
|
-
{ type: 'text', text: 'Continuing with the colleague.' },
|
|
375
|
-
]));
|
|
376
|
-
const framework = await createFramework();
|
|
377
|
-
const routed = stubChannelRegistry(framework);
|
|
378
|
-
module.toolDelayMs = 25;
|
|
379
|
-
module.interjection = '*from the table* one receipt for the case, since I have a dated instance';
|
|
380
|
-
module.interjectionMetadata = {
|
|
381
|
-
channelId: 'discord:guild:hospital',
|
|
382
|
-
tags: ['chat:ambient', 'chat:from-bot'],
|
|
383
|
-
};
|
|
384
|
-
const origHandle = module.handleToolCall.bind(module);
|
|
385
|
-
module.handleToolCall = async (call) => {
|
|
386
|
-
framework.turnRouting.get('assistant').engaged.add('discord:guild:hospital');
|
|
387
|
-
return origHandle(call);
|
|
388
|
-
};
|
|
389
|
-
trigger(framework);
|
|
390
|
-
await framework.runUntilIdle();
|
|
391
|
-
assert.deepEqual(routed, [
|
|
392
|
-
{ text: 'Continuing with the colleague.', locus: 'discord:guild:hospital' },
|
|
393
|
-
], 'the agent-resident follow-up moved the pin like any participant');
|
|
394
|
-
await framework.stop();
|
|
395
|
-
});
|
|
396
|
-
it('ambient chatter in a channel the agent did NOT engage still cannot move the pin', async () => {
|
|
397
|
-
// The Cairn-protection boundary, restated for the engaged-channel era:
|
|
398
|
-
// no mention, no engagement this turn → the pin holds.
|
|
399
|
-
membrane.pushResponse(createMockResponse([
|
|
400
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
401
|
-
], 'tool_use'));
|
|
402
|
-
membrane.pushResponse(createMockResponse([
|
|
403
|
-
{ type: 'text', text: 'Continuing my report.' },
|
|
404
|
-
]));
|
|
405
|
-
const framework = await createFramework();
|
|
406
|
-
const routed = stubChannelRegistry(framework);
|
|
407
|
-
module.toolDelayMs = 25;
|
|
408
|
-
module.interjection = 'unrelated lounge chatter';
|
|
409
|
-
module.interjectionMetadata = {
|
|
410
|
-
channelId: 'discord:guild:lounge',
|
|
411
|
-
tags: ['chat:ambient', 'chat:from-human'],
|
|
412
|
-
};
|
|
413
|
-
trigger(framework);
|
|
414
|
-
await framework.runUntilIdle();
|
|
415
|
-
assert.deepEqual(routed, [
|
|
416
|
-
{ text: 'Continuing my report.', locus: 'chan-live-1' },
|
|
417
|
-
], 'un-engaged ambient left the pin alone');
|
|
418
|
-
await framework.stop();
|
|
419
|
-
});
|
|
420
|
-
it('an addressed injection from the CURRENT locus does not chatter a notice', async () => {
|
|
421
|
-
// Same-channel addressed input is the ordinary case (the person the
|
|
422
|
-
// agent is already talking to sends another message mid-turn): the pin
|
|
423
|
-
// is already right, so no re-pin and — critically for KV — no notice.
|
|
424
|
-
membrane.pushResponse(createMockResponse([
|
|
425
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
426
|
-
], 'tool_use'));
|
|
427
|
-
membrane.pushResponse(createMockResponse([
|
|
428
|
-
{ type: 'text', text: 'Continuing right here.' },
|
|
429
|
-
]));
|
|
430
|
-
const framework = await createFramework();
|
|
431
|
-
const routed = stubChannelRegistry(framework);
|
|
432
|
-
module.toolDelayMs = 25;
|
|
433
|
-
module.interjection = 'and one more thing';
|
|
434
|
-
module.interjectionMetadata = { channelId: 'chan-live-1', tags: ['chat:addressed'] };
|
|
435
|
-
const notices = [];
|
|
436
|
-
framework.onTrace((event) => {
|
|
437
|
-
const e = event;
|
|
438
|
-
if (e.type === 'message:added' && e.source === 'routing-notice')
|
|
439
|
-
notices.push(e.source);
|
|
440
|
-
});
|
|
441
|
-
trigger(framework);
|
|
442
|
-
await framework.runUntilIdle();
|
|
443
|
-
assert.deepEqual(routed, [{ text: 'Continuing right here.', locus: 'chan-live-1' }]);
|
|
444
|
-
assert.equal(notices.length, 1, 'only the boot-baseline announcement — no re-pin chatter');
|
|
445
|
-
await framework.stop();
|
|
446
|
-
});
|
|
447
|
-
// -------------------------------------------------------------------------
|
|
448
|
-
// Every later start of the turn keeps its locus
|
|
449
|
-
// -------------------------------------------------------------------------
|
|
450
|
-
// A framework retry, the compile a capacity hold defers a turn to, the
|
|
451
|
-
// re-inference on unstreamed tool results and a budget restart all keep
|
|
452
|
-
// the turn's locus, re-pins included. A message that no tool boundary
|
|
453
|
-
// weighed, because none of the turn's streams was running or its boundary
|
|
454
|
-
// couldn't re-pin, still moves it at the turn's next start, by the mid-turn
|
|
455
|
-
// rule: a conversational message that addressed the agent, or that follows
|
|
456
|
-
// up in a channel the turn sent into.
|
|
457
|
-
/** Make each stream in `indices` (1-based) end with `error` instead of completing. */
|
|
458
|
-
function failStream(indices, error) {
|
|
459
|
-
const failing = new Set(Array.isArray(indices) ? indices : [indices]);
|
|
460
|
-
const makeStream = membrane.streamYielding.bind(membrane);
|
|
461
|
-
let streams = 0;
|
|
462
|
-
membrane.streamYielding = (request, options) => {
|
|
463
|
-
const stream = makeStream(request, options);
|
|
464
|
-
const n = ++streams;
|
|
465
|
-
const iterate = stream[Symbol.asyncIterator].bind(stream);
|
|
466
|
-
stream[Symbol.asyncIterator] = async function* () {
|
|
467
|
-
for await (const event of { [Symbol.asyncIterator]: iterate }) {
|
|
468
|
-
if (event.type === 'complete' && failing.has(n)) {
|
|
469
|
-
yield { type: 'error', error: error(request) };
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
yield event;
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
return stream;
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
/** Feed a response to the Nth turn start (1-based) the moment it begins. */
|
|
479
|
-
function feedStart(framework, start, ...responses) {
|
|
480
|
-
let starts = 0;
|
|
481
|
-
framework.onTrace((event) => {
|
|
482
|
-
if (event.type !== 'inference:started' || ++starts !== start)
|
|
483
|
-
return;
|
|
484
|
-
responses.forEach((blocks, i) => membrane.pushResponse(createMockResponse(blocks, i < responses.length - 1 ? 'tool_use' : undefined)));
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
/** An organization acceleration 429, which holds the turn for a later compile. */
|
|
488
|
-
const accelerationLimit = (request) => new MembraneError({
|
|
489
|
-
type: 'rate_limit', retryable: true, httpStatus: 429,
|
|
490
|
-
message: "This request would exceed your organization's maximum usage increase rate for input tokens per minute",
|
|
491
|
-
rawError: { status: 429 }, rawRequest: request,
|
|
492
|
-
});
|
|
493
|
-
/** Bring the agent's capacity hold to its deadline and release it, as its
|
|
494
|
-
* timer does then, and run the deferred compile. */
|
|
495
|
-
async function release(framework) {
|
|
496
|
-
const internals = framework;
|
|
497
|
-
const hold = internals.providerAccelerationCooldowns.get('assistant');
|
|
498
|
-
assert.ok(hold, 'the turn is held');
|
|
499
|
-
hold.until = Date.now();
|
|
500
|
-
internals.releaseProviderAccelerationCooldown('assistant');
|
|
501
|
-
await framework.runUntilIdle();
|
|
502
|
-
}
|
|
503
|
-
/** A message someone sends, stored as the test module stores it. */
|
|
504
|
-
async function say(framework, text, metadata) {
|
|
505
|
-
framework.pushEvent({ type: 'external-message', source: 'test', content: text, metadata });
|
|
506
|
-
await framework.runUntilIdle();
|
|
507
|
-
}
|
|
508
|
-
/** Remove a stored message, as the retraction of an unread coalesced message does (removePlacement). */
|
|
509
|
-
function withdraw(framework, text) {
|
|
510
|
-
const cm = framework.getAgent('assistant').getContextManager();
|
|
511
|
-
const message = cm.getAllMessages().find((m) => m.content.some((b) => b.type === 'text' && b.text === text));
|
|
512
|
-
assert.ok(message, `"${text}" is stored`);
|
|
513
|
-
cm.removeMessage(message.id);
|
|
514
|
-
}
|
|
515
|
-
const addressed = (channelId) => ({ channelId, tags: ['chat:addressed'] });
|
|
516
|
-
const ambient = (channelId) => ({ channelId, tags: ['chat:ambient', 'chat:from-human'] });
|
|
517
|
-
/** The `[routing] The conversation moved to …` notices, by channel. */
|
|
518
|
-
const movedTo = (framework) => framework.getAgent('assistant').getContextManager().getAllMessages()
|
|
519
|
-
.filter((m) => m.metadata?.kind === 'routing-notice')
|
|
520
|
-
.flatMap((m) => m.content.filter((b) => b.type === 'text').map((b) => b.text))
|
|
521
|
-
.map((text) => /^\[routing\] The conversation moved to (\S+) /.exec(text)?.[1])
|
|
522
|
-
.filter((channel) => channel !== undefined);
|
|
523
|
-
it("a framework retry keeps the turn's locus, a mid-turn re-pin included", async () => {
|
|
524
|
-
membrane.pushResponse(createMockResponse([
|
|
525
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
526
|
-
], 'tool_use'));
|
|
527
|
-
membrane.pushResponse(createMockResponse([]));
|
|
528
|
-
const framework = await createFramework();
|
|
529
|
-
const routed = stubChannelRegistry(framework);
|
|
530
|
-
module.toolDelayMs = 25;
|
|
531
|
-
module.interjection = 'quick question for you';
|
|
532
|
-
module.interjectionMetadata = addressed('discord:dm:antra');
|
|
533
|
-
failStream(1, () => new Error('synthetic transient stream error'));
|
|
534
|
-
feedStart(framework, 2, [{ type: 'text', text: 'Answering after the retry.' }]);
|
|
535
|
-
trigger(framework);
|
|
536
|
-
await framework.runUntilIdle();
|
|
537
|
-
assert.equal(membrane.calls.length, 2, 'the error was retried');
|
|
538
|
-
assert.deepEqual(routed, [
|
|
539
|
-
{ text: 'Answering after the retry.', locus: 'discord:dm:antra' },
|
|
540
|
-
], 'the retry speaks where the re-pin moved the turn, not at a re-resolved locus');
|
|
541
|
-
await framework.stop();
|
|
542
|
-
});
|
|
543
|
-
for (const [label, metadata, expected] of [
|
|
544
|
-
['an addressed message from another channel moves the locus there, with the notice',
|
|
545
|
-
addressed('discord:dm:antra'), 'discord:dm:antra'],
|
|
546
|
-
['ambient traffic in another channel leaves the locus where it was',
|
|
547
|
-
ambient('discord:guild:lounge'), 'chan-live-1'],
|
|
548
|
-
// Tune-out stamps a message it diverts, and a resident's compiled view
|
|
549
|
-
// leaves it out, so the agent never sees it.
|
|
550
|
-
['an addressed message that tune-out diverts leaves the locus where it was',
|
|
551
|
-
{ ...addressed('discord:dm:antra'), tuneOut: { epochId: 'epoch-1' } }, 'chan-live-1'],
|
|
552
|
-
['an addressed system marker from another channel leaves the locus where it was',
|
|
553
|
-
{ ...addressed('discord:dm:antra'), system: true }, 'chan-live-1'],
|
|
554
|
-
]) {
|
|
555
|
-
it(`during a capacity hold, ${label}`, async () => {
|
|
556
|
-
membrane.pushResponse(createMockResponse([
|
|
557
|
-
{ type: 'text', text: 'On it.' },
|
|
558
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
559
|
-
], 'tool_use'));
|
|
560
|
-
membrane.pushResponse(createMockResponse([]));
|
|
561
|
-
const framework = await createFramework();
|
|
562
|
-
const routed = stubChannelRegistry(framework);
|
|
563
|
-
module.toolDelayMs = 25;
|
|
564
|
-
failStream(1, accelerationLimit);
|
|
565
|
-
feedStart(framework, 2, [{ type: 'text', text: 'Here is the rest.' }]);
|
|
566
|
-
trigger(framework);
|
|
567
|
-
await framework.runUntilIdle();
|
|
568
|
-
// No turn is alive during the hold, so the message is stored at once.
|
|
569
|
-
await say(framework, 'a message during the hold', metadata);
|
|
570
|
-
assert.equal(membrane.calls.length, 1, 'the turn is still held');
|
|
571
|
-
await release(framework);
|
|
572
|
-
assert.equal(membrane.calls.length, 2, 'one deferred compile');
|
|
573
|
-
assert.deepEqual(routed, [
|
|
574
|
-
{ text: 'On it.', locus: 'chan-live-1' },
|
|
575
|
-
{ text: 'Here is the rest.', locus: expected },
|
|
576
|
-
]);
|
|
577
|
-
assert.deepEqual(movedTo(framework), expected === 'chan-live-1' ? [] : [expected], 'a re-pin tells the agent, and nothing else does');
|
|
578
|
-
await framework.stop();
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
for (const [label, messages, expected] of [
|
|
582
|
-
['an addressed message withdrawn before the deferred compile moves nothing',
|
|
583
|
-
[['soon withdrawn', 'discord:dm:antra', true]], 'chan-live-1'],
|
|
584
|
-
['the newest addressed message that remains moves the locus, when a newer one is withdrawn',
|
|
585
|
-
[['still there', 'discord:dm:antra', false], ['soon withdrawn', 'discord:dm:q', true]], 'discord:dm:antra'],
|
|
586
|
-
]) {
|
|
587
|
-
it(`during a capacity hold, ${label}`, async () => {
|
|
588
|
-
membrane.pushResponse(createMockResponse([
|
|
589
|
-
{ type: 'text', text: 'On it.' },
|
|
590
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
591
|
-
], 'tool_use'));
|
|
592
|
-
membrane.pushResponse(createMockResponse([]));
|
|
593
|
-
const framework = await createFramework();
|
|
594
|
-
const routed = stubChannelRegistry(framework);
|
|
595
|
-
module.toolDelayMs = 25;
|
|
596
|
-
failStream(1, accelerationLimit);
|
|
597
|
-
feedStart(framework, 2, [{ type: 'text', text: 'Here is the rest.' }]);
|
|
598
|
-
trigger(framework);
|
|
599
|
-
await framework.runUntilIdle();
|
|
600
|
-
for (const [text, channelId] of messages)
|
|
601
|
-
await say(framework, text, addressed(channelId));
|
|
602
|
-
// Each withdrawn message leaves the agent's store before the turn's
|
|
603
|
-
// next start, so the agent never sees it there.
|
|
604
|
-
for (const [text, , withdrawn] of messages)
|
|
605
|
-
if (withdrawn)
|
|
606
|
-
withdraw(framework, text);
|
|
607
|
-
await release(framework);
|
|
608
|
-
assert.equal(membrane.calls.length, 2, 'one deferred compile');
|
|
609
|
-
assert.deepEqual(routed, [
|
|
610
|
-
{ text: 'On it.', locus: 'chan-live-1' },
|
|
611
|
-
{ text: 'Here is the rest.', locus: expected },
|
|
612
|
-
]);
|
|
613
|
-
assert.deepEqual(movedTo(framework), expected === 'chan-live-1' ? [] : [expected], 'a re-pin tells the agent, and nothing else does');
|
|
614
|
-
await framework.stop();
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
it("during a capacity hold, a store that can't be read at the deferred compile fails the start rather than drop the arrival", async () => {
|
|
618
|
-
// Adapted from a reviewer's probe: a read that fails says nothing about
|
|
619
|
-
// whether the message is there, so it can't count as a withdrawal.
|
|
620
|
-
membrane.pushResponse(createMockResponse([
|
|
621
|
-
{ type: 'text', text: 'On it.' },
|
|
622
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
623
|
-
], 'tool_use'));
|
|
624
|
-
membrane.pushResponse(createMockResponse([]));
|
|
625
|
-
const framework = await createFramework();
|
|
626
|
-
const routed = stubChannelRegistry(framework);
|
|
627
|
-
module.toolDelayMs = 25;
|
|
628
|
-
failStream(1, accelerationLimit);
|
|
629
|
-
// Were the failed read taken as a withdrawal, the compile would run and
|
|
630
|
-
// answer here, at the turn's old channel.
|
|
631
|
-
feedStart(framework, 2, [{ type: 'text', text: 'Here is the rest.' }]);
|
|
632
|
-
trigger(framework);
|
|
633
|
-
await framework.runUntilIdle();
|
|
634
|
-
await say(framework, 'a question for you', addressed('discord:dm:antra'));
|
|
635
|
-
const cm = framework.getAgent('assistant').getContextManager();
|
|
636
|
-
const arrival = cm.getAllMessages().find((m) => m.content.some((b) => b.type === 'text' && b.text === 'a question for you'));
|
|
637
|
-
const getMessage = cm.getMessage.bind(cm);
|
|
638
|
-
cm.getMessage = (id) => {
|
|
639
|
-
if (id === arrival.id)
|
|
640
|
-
throw new Error('store unavailable');
|
|
641
|
-
return getMessage(id);
|
|
642
|
-
};
|
|
643
|
-
try {
|
|
644
|
-
await assert.rejects(release(framework), /store unavailable/);
|
|
645
|
-
}
|
|
646
|
-
finally {
|
|
647
|
-
cm.getMessage = getMessage;
|
|
648
|
-
}
|
|
649
|
-
assert.equal(membrane.calls.length, 1, 'the deferred compile never ran');
|
|
650
|
-
assert.deepEqual(routed, [{ text: 'On it.', locus: 'chan-live-1' }], 'and the turn spoke nowhere else');
|
|
651
|
-
assert.deepEqual(movedTo(framework), [], 'nor told the agent of a move');
|
|
652
|
-
await framework.stop();
|
|
653
|
-
});
|
|
654
|
-
it("a message a tool boundary already weighed doesn't take the locus back at a later start", async () => {
|
|
655
|
-
membrane.pushResponse(createMockResponse([
|
|
656
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
657
|
-
], 'tool_use'));
|
|
658
|
-
membrane.pushResponse(createMockResponse([
|
|
659
|
-
{ type: 'tool_use', id: 'c2', name: 'robot--move', input: { dir: 'east' } },
|
|
660
|
-
], 'tool_use'));
|
|
661
|
-
membrane.pushResponse(createMockResponse([]));
|
|
662
|
-
const framework = await createFramework();
|
|
663
|
-
const routed = stubChannelRegistry(framework);
|
|
664
|
-
module.toolDelayMs = 25;
|
|
665
|
-
const internals = framework;
|
|
666
|
-
const origHandle = module.handleToolCall.bind(module);
|
|
667
|
-
module.handleToolCall = async (call) => {
|
|
668
|
-
if (call.id === 'c1') {
|
|
669
|
-
// Someone addresses the agent from a DM: the first boundary re-pins
|
|
670
|
-
// there. The message's wake waits behind the turn, as a real one
|
|
671
|
-
// would, and is released with the hold.
|
|
672
|
-
module.interjection = 'quick question for you';
|
|
673
|
-
module.interjectionMetadata = addressed('discord:dm:antra');
|
|
674
|
-
internals.pendingRequests.push({
|
|
675
|
-
agentName: 'assistant', reason: 'channel-message', source: 'test', timestamp: Date.now(),
|
|
676
|
-
channelId: 'discord:dm:antra', addressed: true,
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
else {
|
|
680
|
-
// The turn sent into #portables, and a follow-up there re-pins the
|
|
681
|
-
// second boundary (seeded: this module's tools aren't MCPL sends).
|
|
682
|
-
internals.turnRouting.get('assistant').engaged.add('discord:guild:portables');
|
|
683
|
-
module.interjection = 'replying on portables';
|
|
684
|
-
module.interjectionMetadata = ambient('discord:guild:portables');
|
|
685
|
-
}
|
|
686
|
-
return origHandle(call);
|
|
687
|
-
};
|
|
688
|
-
failStream(1, accelerationLimit);
|
|
689
|
-
feedStart(framework, 2, [{ type: 'text', text: 'Back now.' }]);
|
|
690
|
-
trigger(framework);
|
|
691
|
-
await framework.runUntilIdle();
|
|
692
|
-
await release(framework);
|
|
693
|
-
assert.equal(membrane.calls.length, 2, 'one deferred compile');
|
|
694
|
-
assert.deepEqual(routed, [
|
|
695
|
-
{ text: 'Back now.', locus: 'discord:guild:portables' },
|
|
696
|
-
], "the DM, weighed at the first boundary, doesn't move the locus back from #portables");
|
|
697
|
-
assert.deepEqual(movedTo(framework), ['discord:dm:antra', 'discord:guild:portables']);
|
|
698
|
-
await framework.stop();
|
|
699
|
-
});
|
|
700
|
-
it("an arrival a start has used doesn't move the locus again after a later re-pin", async () => {
|
|
701
|
-
membrane.pushResponse(createMockResponse([
|
|
702
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
703
|
-
], 'tool_use'));
|
|
704
|
-
membrane.pushResponse(createMockResponse([]));
|
|
705
|
-
const framework = await createFramework();
|
|
706
|
-
const routed = stubChannelRegistry(framework);
|
|
707
|
-
module.toolDelayMs = 25;
|
|
708
|
-
const origHandle = module.handleToolCall.bind(module);
|
|
709
|
-
module.handleToolCall = async (call) => {
|
|
710
|
-
if (call.id === 'c2') {
|
|
711
|
-
// In the second stream, someone else addresses the agent: its boundary re-pins.
|
|
712
|
-
module.interjection = 'over here now';
|
|
713
|
-
module.interjectionMetadata = addressed('discord:dm:q');
|
|
714
|
-
}
|
|
715
|
-
return origHandle(call);
|
|
716
|
-
};
|
|
717
|
-
failStream([1, 2], accelerationLimit);
|
|
718
|
-
feedStart(framework, 2, [{ type: 'tool_use', id: 'c2', name: 'robot--move', input: { dir: 'east' } }], []);
|
|
719
|
-
feedStart(framework, 3, [{ type: 'text', text: 'Back now.' }]);
|
|
720
|
-
trigger(framework);
|
|
721
|
-
await framework.runUntilIdle();
|
|
722
|
-
await say(framework, 'quick question for you', addressed('discord:dm:antra'));
|
|
723
|
-
// The first release uses the DM; its stream re-pins to q, then is held again.
|
|
724
|
-
await release(framework);
|
|
725
|
-
assert.equal(membrane.calls.length, 2);
|
|
726
|
-
await release(framework);
|
|
727
|
-
assert.equal(membrane.calls.length, 3, 'two deferred compiles');
|
|
728
|
-
assert.deepEqual(movedTo(framework), ['discord:dm:antra', 'discord:dm:q']);
|
|
729
|
-
assert.deepEqual(routed, [
|
|
730
|
-
{ text: 'Back now.', locus: 'discord:dm:q' },
|
|
731
|
-
], 'the second release keeps the re-pin its stream made');
|
|
732
|
-
await framework.stop();
|
|
733
|
-
});
|
|
734
|
-
it('a message deferred in a start that fails before its stream lands then, so a newer one during the hold wins', async () => {
|
|
735
|
-
membrane.pushResponse(createMockResponse([
|
|
736
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
737
|
-
], 'tool_use'));
|
|
738
|
-
membrane.pushResponse(createMockResponse([]));
|
|
739
|
-
const framework = await createFramework();
|
|
740
|
-
const routed = stubChannelRegistry(framework);
|
|
741
|
-
module.toolDelayMs = 25;
|
|
742
|
-
// The host holds the agent once when its stream can't be set up. (Asked
|
|
743
|
-
// again at the release, it lets the hold go.)
|
|
744
|
-
let hostHolds = 0;
|
|
745
|
-
framework
|
|
746
|
-
.providerHoldHook = (error) => error.message === 'stream setup failed' && hostHolds++ === 0
|
|
747
|
-
? { holdMs: 60_000, reason: 'host hold' } : undefined;
|
|
748
|
-
// Stream one is held at its end; the start after the first release
|
|
749
|
-
// fails before its stream exists, and the host holds it; the third start
|
|
750
|
-
// answers.
|
|
751
|
-
const makeStream = membrane.streamYielding.bind(membrane);
|
|
752
|
-
let calls = 0;
|
|
753
|
-
membrane.streamYielding = (request, options) => {
|
|
754
|
-
const n = ++calls;
|
|
755
|
-
if (n === 2)
|
|
756
|
-
throw new Error('stream setup failed');
|
|
757
|
-
const stream = makeStream(request, options);
|
|
758
|
-
const iterate = stream[Symbol.asyncIterator].bind(stream);
|
|
759
|
-
stream[Symbol.asyncIterator] = async function* () {
|
|
760
|
-
for await (const event of { [Symbol.asyncIterator]: iterate }) {
|
|
761
|
-
if (event.type === 'complete' && n === 1) {
|
|
762
|
-
yield { type: 'error', error: accelerationLimit(request) };
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
yield event;
|
|
766
|
-
}
|
|
767
|
-
};
|
|
768
|
-
return stream;
|
|
769
|
-
};
|
|
770
|
-
feedStart(framework, 2, [{ type: 'text', text: 'Back now.' }]);
|
|
771
|
-
trigger(framework);
|
|
772
|
-
await framework.runUntilIdle();
|
|
773
|
-
// During the second start, before its stream exists, a DM arrives and is
|
|
774
|
-
// deferred: the start holds the turn. (Written directly: the run loop
|
|
775
|
-
// that would store an event's message is busy with this start.)
|
|
776
|
-
const store = framework;
|
|
777
|
-
let deferredInStart = -1;
|
|
778
|
-
module.startHook = async () => {
|
|
779
|
-
store.addMessage('Antra', [{ type: 'text', text: 'quick question for you' }], addressed('discord:dm:antra'));
|
|
780
|
-
deferredInStart = store.deferredMessages.length;
|
|
781
|
-
};
|
|
782
|
-
await release(framework);
|
|
783
|
-
assert.equal(deferredInStart, 1, 'the DM was deferred while the start held the turn');
|
|
784
|
-
assert.equal(calls, 2, 'the second start failed before its stream');
|
|
785
|
-
// During the second hold, someone else addresses the agent.
|
|
786
|
-
await say(framework, 'over here', addressed('discord:dm:q'));
|
|
787
|
-
await release(framework);
|
|
788
|
-
assert.equal(calls, 3, 'the third start compiled');
|
|
789
|
-
assert.deepEqual(routed, [
|
|
790
|
-
{ text: 'Back now.', locus: 'discord:dm:q' },
|
|
791
|
-
], 'the newer message wins: the deferred one landed when its start failed');
|
|
792
|
-
await framework.stop();
|
|
793
|
-
});
|
|
794
|
-
for (const [label, withdrawn, expected] of [
|
|
795
|
-
['re-pins to an addressed message its over-budget boundary left unweighed', false, 'discord:dm:antra'],
|
|
796
|
-
['moves nothing for such a message withdrawn before it starts', true, 'chan-live-1'],
|
|
797
|
-
]) {
|
|
798
|
-
it(`a budget restart ${label}`, async () => {
|
|
799
|
-
const overBudget = createMockResponse([
|
|
800
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
801
|
-
], 'tool_use');
|
|
802
|
-
overBudget.usage = { inputTokens: 100, outputTokens: 5 };
|
|
803
|
-
membrane.pushResponse(overBudget);
|
|
804
|
-
const framework = await createFramework({ maxStreamTokens: 20 });
|
|
805
|
-
const routed = stubChannelRegistry(framework);
|
|
806
|
-
module.toolDelayMs = 25;
|
|
807
|
-
module.interjection = 'quick question for you';
|
|
808
|
-
module.interjectionMetadata = addressed('discord:dm:antra');
|
|
809
|
-
feedStart(framework, 2, [{ type: 'text', text: 'After the restart.' }]);
|
|
810
|
-
if (withdrawn) {
|
|
811
|
-
// The boundary has stored the message and left it to the restart as
|
|
812
|
-
// an arrival, by the id it was stored under; it leaves the store
|
|
813
|
-
// before the restart starts.
|
|
814
|
-
framework.onTrace((event) => {
|
|
815
|
-
if (event.type === 'inference:stream_restarted')
|
|
816
|
-
withdraw(framework, 'quick question for you');
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
trigger(framework);
|
|
820
|
-
await framework.runUntilIdle();
|
|
821
|
-
assert.equal(membrane.calls.length, 2, 'the turn restarted for its budget');
|
|
822
|
-
assert.deepEqual(routed, [
|
|
823
|
-
{ text: 'After the restart.', locus: expected },
|
|
824
|
-
]);
|
|
825
|
-
assert.deepEqual(movedTo(framework), withdrawn ? [] : [expected]);
|
|
826
|
-
await framework.stop();
|
|
827
|
-
});
|
|
828
|
-
}
|
|
829
|
-
for (const [label, messages, expected] of [
|
|
830
|
-
["keeps the turn's locus", [], 'chan-turn'],
|
|
831
|
-
['moves it to the newest addressed arrival',
|
|
832
|
-
[addressed('discord:dm:antra'), addressed('discord:dm:q')], 'discord:dm:q'],
|
|
833
|
-
['moves it to a follow-up in a channel the turn engaged',
|
|
834
|
-
[ambient('discord:guild:portables')], 'discord:guild:portables'],
|
|
835
|
-
['leaves it for a reaction in a channel the turn engaged',
|
|
836
|
-
[{ channelId: 'discord:guild:portables', tags: ['chat:reaction'] }], 'chan-turn'],
|
|
837
|
-
]) {
|
|
838
|
-
it(`a re-inference on unstreamed tool results ${label}`, async () => {
|
|
839
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'Carrying on.' }]));
|
|
840
|
-
const framework = await createFramework();
|
|
841
|
-
const routed = stubChannelRegistry(framework);
|
|
842
|
-
const internals = framework;
|
|
843
|
-
// The turn's earlier stream froze `chan-turn` as its locus, sent into
|
|
844
|
-
// #portables, and left its tool results for the non-streaming
|
|
845
|
-
// fallback's re-inference. Messages arrive before that starts.
|
|
846
|
-
const open = {
|
|
847
|
-
locus: 'chan-turn', triggerChannel: 'chan-turn', engaged: new Set(['discord:guild:portables']),
|
|
848
|
-
destination: null, hybridHold: false, continuation: false, deliveries: [], suppressed: 0, arrivals: [],
|
|
849
|
-
};
|
|
850
|
-
internals.turnRouting.set('assistant', open);
|
|
851
|
-
internals.openLogical.set('assistant', open);
|
|
852
|
-
for (const metadata of messages)
|
|
853
|
-
await say(framework, 'a message', metadata);
|
|
854
|
-
internals.pendingRequests.push({ agentName: 'assistant', reason: 'tool_results_ready', source: 'framework', timestamp: Date.now() });
|
|
855
|
-
await framework.runUntilIdle();
|
|
856
|
-
assert.deepEqual(routed, [{ text: 'Carrying on.', locus: expected }]);
|
|
857
|
-
assert.deepEqual(movedTo(framework), expected === 'chan-turn' ? [] : [expected], 'a re-pin leaves its notice');
|
|
858
|
-
await framework.stop();
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
it('the start that continues the turn weighs a message its own flush lands, and a delivery assembled for it', async () => {
|
|
862
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'Carrying on.' }]));
|
|
863
|
-
const framework = await createFramework();
|
|
864
|
-
const routed = stubChannelRegistry(framework);
|
|
865
|
-
const internals = framework;
|
|
866
|
-
const open = {
|
|
867
|
-
locus: 'chan-turn', triggerChannel: 'chan-turn', engaged: new Set(),
|
|
868
|
-
destination: null, hybridHold: false, continuation: false, deliveries: [], suppressed: 0, arrivals: [],
|
|
869
|
-
};
|
|
870
|
-
internals.turnRouting.set('assistant', open);
|
|
871
|
-
internals.openLogical.set('assistant', open);
|
|
872
|
-
try {
|
|
873
|
-
// A delivery assembled while a start of the turn holds it (the
|
|
874
|
-
// coalescing assembly writes past the deferral) is an arrival too.
|
|
875
|
-
internals.activeTurnTokens.set('assistant', -1);
|
|
876
|
-
internals.addMessage('Antra', [{ type: 'text', text: 'batched' }], addressed('discord:dm:q'), { bypassDeferralFor: 'assistant' });
|
|
877
|
-
internals.activeTurnTokens.delete('assistant');
|
|
878
|
-
assert.deepEqual(open.arrivals.map((a) => a.channelId), ['discord:dm:q']);
|
|
879
|
-
// A message still deferred when the turn's next start begins lands in
|
|
880
|
-
// that start's own flush, and is the newest arrival.
|
|
881
|
-
internals.deferredMessages.push({
|
|
882
|
-
id: 'deferred-1', seq: 1, participant: 'Antra', forAgent: 'assistant',
|
|
883
|
-
content: [{ type: 'text', text: 'quick question for you' }], metadata: addressed('discord:dm:antra'),
|
|
884
|
-
});
|
|
885
|
-
internals.pendingRequests.push({ agentName: 'assistant', reason: 'tool_results_ready', source: 'framework', timestamp: Date.now() });
|
|
886
|
-
await framework.runUntilIdle();
|
|
887
|
-
assert.deepEqual(routed, [{ text: 'Carrying on.', locus: 'discord:dm:antra' }]);
|
|
888
|
-
assert.deepEqual(movedTo(framework), ['discord:dm:antra']);
|
|
889
|
-
}
|
|
890
|
-
finally {
|
|
891
|
-
await framework.stop();
|
|
892
|
-
}
|
|
893
|
-
});
|
|
894
|
-
it('a start that throws before its stream ends its logical turn, so the next start opens its own', async () => {
|
|
895
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'Hello there.' }]));
|
|
896
|
-
const framework = await createFramework();
|
|
897
|
-
const routed = stubChannelRegistry(framework);
|
|
898
|
-
// The registry is unavailable for the first start, which throws after it
|
|
899
|
-
// has opened its turn, outside the stream's own failure handling.
|
|
900
|
-
const registry = framework.channelRegistry;
|
|
901
|
-
const resolve = registry.resolveLocus;
|
|
902
|
-
let failed = false;
|
|
903
|
-
registry.resolveLocus = (agent) => {
|
|
904
|
-
if (!failed) {
|
|
905
|
-
failed = true;
|
|
906
|
-
throw new Error('registry unavailable');
|
|
907
|
-
}
|
|
908
|
-
return resolve(agent);
|
|
909
|
-
};
|
|
910
|
-
const internals = framework;
|
|
911
|
-
trigger(framework);
|
|
912
|
-
await assert.rejects(framework.runUntilIdle(), /registry unavailable/);
|
|
913
|
-
assert.equal(internals.openLogical.has('assistant'), false, 'the failed start left no turn open');
|
|
914
|
-
trigger(framework);
|
|
915
|
-
await framework.runUntilIdle();
|
|
916
|
-
assert.deepEqual(routed, [{ text: 'Hello there.', locus: 'chan-live-1' }], 'the next start resolved a locus of its own');
|
|
917
|
-
await framework.stop();
|
|
918
|
-
});
|
|
919
|
-
it("a silent turn's tool boundary doesn't re-pin its locus or tell it the conversation moved", async () => {
|
|
920
|
-
membrane.pushResponse(createMockResponse([
|
|
921
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
922
|
-
], 'tool_use'));
|
|
923
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'quiet' }]));
|
|
924
|
-
const framework = await createFramework();
|
|
925
|
-
stubChannelRegistry(framework);
|
|
926
|
-
module.toolDelayMs = 25;
|
|
927
|
-
module.interjection = 'quick question for you';
|
|
928
|
-
module.interjectionMetadata = addressed('discord:dm:antra');
|
|
929
|
-
const internals = framework;
|
|
930
|
-
internals.pendingRequests.push({
|
|
931
|
-
agentName: 'assistant', reason: 'heartbeat', source: 'framework', timestamp: Date.now(), suppressProse: true,
|
|
932
|
-
});
|
|
933
|
-
await framework.runUntilIdle();
|
|
934
|
-
assert.equal(module.interjection, null, 'the silent turn ran its tool round, and the DM arrived during it');
|
|
935
|
-
assert.deepEqual(movedTo(framework), [], 'no routing notice in a turn that routes no prose');
|
|
936
|
-
await framework.stop();
|
|
937
|
-
});
|
|
938
|
-
it("a start that continues the turn and throws before it takes the turn's routing still ends the turn", async () => {
|
|
939
|
-
membrane.pushResponse(createMockResponse([
|
|
940
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
941
|
-
], 'tool_use'));
|
|
942
|
-
membrane.pushResponse(createMockResponse([]));
|
|
943
|
-
const framework = await createFramework();
|
|
944
|
-
const routed = stubChannelRegistry(framework);
|
|
945
|
-
module.toolDelayMs = 25;
|
|
946
|
-
failStream(1, accelerationLimit);
|
|
947
|
-
feedStart(framework, 2, [{ type: 'text', text: 'A new turn.' }]);
|
|
948
|
-
const internals = framework;
|
|
949
|
-
trigger(framework);
|
|
950
|
-
await framework.runUntilIdle();
|
|
951
|
-
assert.equal(internals.openLogical.has('assistant'), true, 'the held turn is open');
|
|
952
|
-
// The deferred compile's checkpoint fails before the start takes the turn.
|
|
953
|
-
const checkpoint = internals.recordTurnCheckpoint.bind(framework);
|
|
954
|
-
internals.recordTurnCheckpoint = () => { throw new Error('checkpoint failed'); };
|
|
955
|
-
await assert.rejects(release(framework), /checkpoint failed/);
|
|
956
|
-
internals.recordTurnCheckpoint = checkpoint;
|
|
957
|
-
assert.equal(internals.openLogical.has('assistant'), false, 'the held turn ended with its failed compile');
|
|
958
|
-
trigger(framework);
|
|
959
|
-
await framework.runUntilIdle();
|
|
960
|
-
assert.deepEqual(routed, [{ text: 'A new turn.', locus: 'chan-live-2' }], 'the next start opened its own turn and resolved its own locus');
|
|
961
|
-
await framework.stop();
|
|
962
|
-
});
|
|
963
|
-
it("an ephemeral run whose start throws outside its stream fails with that error, not by its watchdog", async () => {
|
|
964
|
-
// Adapted from a reviewer's probe. On main the run waited for its startup
|
|
965
|
-
// watchdog (30 s by default), which fails it as a zombie.
|
|
966
|
-
const framework = await AgentFramework.create({
|
|
967
|
-
storePath: join(tempDir, 'ephemeral.chronicle'),
|
|
968
|
-
membrane: membrane.asMembrane(),
|
|
969
|
-
agents: [],
|
|
970
|
-
modules: [],
|
|
971
|
-
syncIntervalMs: 0,
|
|
972
|
-
});
|
|
973
|
-
const internals = framework;
|
|
974
|
-
const checkpoint = internals.recordTurnCheckpoint.bind(framework);
|
|
975
|
-
internals.recordTurnCheckpoint = (agentName) => {
|
|
976
|
-
if (agentName === 'worker')
|
|
977
|
-
throw new Error('checkpoint store unavailable');
|
|
978
|
-
checkpoint(agentName);
|
|
979
|
-
};
|
|
980
|
-
const error = console.error;
|
|
981
|
-
console.error = () => { };
|
|
982
|
-
try {
|
|
983
|
-
const { agent, contextManager } = await framework.createEphemeralAgent({
|
|
984
|
-
name: 'worker', model: 'test-model', systemPrompt: 'Do the task.', allowedTools: 'all',
|
|
985
|
-
});
|
|
986
|
-
contextManager.addMessage('user', [{ type: 'text', text: 'Run once.' }]);
|
|
987
|
-
const run = framework.runEphemeralToCompletion(agent, contextManager, { startupTimeoutMs: 3_000 });
|
|
988
|
-
framework.start();
|
|
989
|
-
await assert.rejects(run, /checkpoint store unavailable/, "the run fails with its start's error");
|
|
990
|
-
}
|
|
991
|
-
finally {
|
|
992
|
-
console.error = error;
|
|
993
|
-
await framework.stop();
|
|
994
|
-
}
|
|
995
|
-
});
|
|
996
|
-
it("a silent start that throws leaves an open prose turn alone", async () => {
|
|
997
|
-
const framework = await createFramework();
|
|
998
|
-
stubChannelRegistry(framework);
|
|
999
|
-
const internals = framework;
|
|
1000
|
-
const open = {
|
|
1001
|
-
locus: 'chan-turn', engaged: new Set(), destination: null, hybridHold: false,
|
|
1002
|
-
continuation: false, deliveries: [], suppressed: 0, arrivals: [],
|
|
1003
|
-
};
|
|
1004
|
-
internals.openLogical.set('assistant', open);
|
|
1005
|
-
internals.recordTurnCheckpoint = () => { throw new Error('checkpoint failed'); };
|
|
1006
|
-
internals.pendingRequests.push({
|
|
1007
|
-
agentName: 'assistant', reason: 'heartbeat', source: 'framework', timestamp: Date.now(), suppressProse: true,
|
|
1008
|
-
});
|
|
1009
|
-
await assert.rejects(framework.runUntilIdle(), /checkpoint failed/);
|
|
1010
|
-
assert.equal(internals.openLogical.get('assistant'), open);
|
|
1011
|
-
await framework.stop();
|
|
1012
|
-
});
|
|
1013
|
-
it("a resume after stop() continues its turn, and opens it again for the turn's later starts", async () => {
|
|
1014
|
-
membrane.pushResponse(createMockResponse([]));
|
|
1015
|
-
const framework = await createFramework();
|
|
1016
|
-
const routed = stubChannelRegistry(framework);
|
|
1017
|
-
failStream(1, () => new Error('synthetic transient stream error'));
|
|
1018
|
-
feedStart(framework, 2, [{ type: 'text', text: 'After the retry.' }]);
|
|
1019
|
-
const internals = framework;
|
|
1020
|
-
// A turn that crossed its budget before a stop(): its routing state is
|
|
1021
|
-
// live, and stop() ended every open turn. Its budget restart now runs,
|
|
1022
|
-
// and its stream fails and is retried.
|
|
1023
|
-
const turn = {
|
|
1024
|
-
locus: 'chan-turn', triggerChannel: 'chan-turn', engaged: new Set(), destination: null, hybridHold: false,
|
|
1025
|
-
continuation: false, deliveries: [], suppressed: 0, arrivals: [],
|
|
1026
|
-
};
|
|
1027
|
-
internals.turnRouting.set('assistant', turn);
|
|
1028
|
-
internals.pendingRequests.push({
|
|
1029
|
-
agentName: 'assistant', reason: 'context_budget_restart', source: 'framework', timestamp: Date.now(),
|
|
1030
|
-
});
|
|
1031
|
-
// The retry of a resume is a resume too, and takes the turn's routing
|
|
1032
|
-
// state whatever is open. The turn must be open while its streams run, so
|
|
1033
|
-
// that its other later starts, such as a re-inference on unstreamed tool
|
|
1034
|
-
// results, continue it rather than open a new one.
|
|
1035
|
-
const openAtStart = [];
|
|
1036
|
-
framework.onTrace((event) => {
|
|
1037
|
-
if (event.type === 'inference:started')
|
|
1038
|
-
openAtStart.push(internals.openLogical.get('assistant'));
|
|
1039
|
-
});
|
|
1040
|
-
await framework.runUntilIdle();
|
|
1041
|
-
assert.equal(membrane.calls.length, 2, 'the restart was retried');
|
|
1042
|
-
assert.deepEqual(openAtStart.map((open) => open === turn), [true, true], 'the resumed turn was open again while each of its streams ran');
|
|
1043
|
-
assert.equal(internals.openLogical.get('assistant'), undefined, "the turn completed");
|
|
1044
|
-
assert.deepEqual(routed, [{ text: 'After the retry.', locus: 'chan-turn' }], "the retry continued the resumed turn's routing");
|
|
1045
|
-
await framework.stop();
|
|
1046
|
-
});
|
|
1047
|
-
it("a silent turn's over-budget boundary leaves no arrival on an open prose turn", async () => {
|
|
1048
|
-
const overBudget = createMockResponse([
|
|
1049
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
1050
|
-
], 'tool_use');
|
|
1051
|
-
overBudget.usage = { inputTokens: 100, outputTokens: 5 };
|
|
1052
|
-
membrane.pushResponse(overBudget);
|
|
1053
|
-
const framework = await createFramework({ maxStreamTokens: 20 });
|
|
1054
|
-
const routed = stubChannelRegistry(framework);
|
|
1055
|
-
module.toolDelayMs = 25;
|
|
1056
|
-
module.interjection = 'quick question for you';
|
|
1057
|
-
module.interjectionMetadata = addressed('discord:dm:antra');
|
|
1058
|
-
feedStart(framework, 2, [{ type: 'text', text: 'quiet' }]);
|
|
1059
|
-
const internals = framework;
|
|
1060
|
-
// Built by hand, as #312's silent-start test does: production runs no
|
|
1061
|
-
// silent turn while a prose turn is open.
|
|
1062
|
-
const open = {
|
|
1063
|
-
locus: 'chan-turn', engaged: new Set(), destination: null, hybridHold: false,
|
|
1064
|
-
continuation: false, deliveries: [], suppressed: 0, arrivals: [],
|
|
1065
|
-
};
|
|
1066
|
-
internals.openLogical.set('assistant', open);
|
|
1067
|
-
internals.pendingRequests.push({
|
|
1068
|
-
agentName: 'assistant', reason: 'heartbeat', source: 'framework', timestamp: Date.now(), suppressProse: true,
|
|
1069
|
-
});
|
|
1070
|
-
await framework.runUntilIdle();
|
|
1071
|
-
assert.equal(membrane.calls.length, 2, 'the silent turn crossed its budget and restarted');
|
|
1072
|
-
assert.deepEqual(routed, [], 'and its restart stayed silent');
|
|
1073
|
-
assert.equal(module.interjection, null, 'the DM arrived during its tool round');
|
|
1074
|
-
assert.deepEqual(open.arrivals, [], "the silent turn's injections are no arrival of the prose turn");
|
|
1075
|
-
await framework.stop();
|
|
1076
|
-
});
|
|
1077
|
-
it("a failed start's flush stores each deferred message on its own, so one failure loses no other", async () => {
|
|
1078
|
-
membrane.pushResponse(createMockResponse([
|
|
1079
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
1080
|
-
], 'tool_use'));
|
|
1081
|
-
membrane.pushResponse(createMockResponse([]));
|
|
1082
|
-
const framework = await createFramework();
|
|
1083
|
-
stubChannelRegistry(framework);
|
|
1084
|
-
module.toolDelayMs = 25;
|
|
1085
|
-
let hostHolds = 0;
|
|
1086
|
-
framework
|
|
1087
|
-
.providerHoldHook = (error) => error.message === 'stream setup failed' && hostHolds++ === 0
|
|
1088
|
-
? { holdMs: 60_000, reason: 'host hold' } : undefined;
|
|
1089
|
-
const makeStream = membrane.streamYielding.bind(membrane);
|
|
1090
|
-
let calls = 0;
|
|
1091
|
-
membrane.streamYielding = (request, options) => {
|
|
1092
|
-
const n = ++calls;
|
|
1093
|
-
if (n === 2)
|
|
1094
|
-
throw new Error('stream setup failed');
|
|
1095
|
-
const stream = makeStream(request, options);
|
|
1096
|
-
const iterate = stream[Symbol.asyncIterator].bind(stream);
|
|
1097
|
-
stream[Symbol.asyncIterator] = async function* () {
|
|
1098
|
-
for await (const event of { [Symbol.asyncIterator]: iterate }) {
|
|
1099
|
-
if (event.type === 'complete' && n === 1) {
|
|
1100
|
-
yield { type: 'error', error: accelerationLimit(request) };
|
|
1101
|
-
return;
|
|
1102
|
-
}
|
|
1103
|
-
yield event;
|
|
1104
|
-
}
|
|
1105
|
-
};
|
|
1106
|
-
return stream;
|
|
1107
|
-
};
|
|
1108
|
-
const store = framework;
|
|
1109
|
-
const add = store.addMessage.bind(framework);
|
|
1110
|
-
// The store refuses the first message when the failed start's flush lands it.
|
|
1111
|
-
store.addMessage = (participant, content, metadata, opts) => {
|
|
1112
|
-
if (opts?.deferredWriteId && content[0].text === 'poison')
|
|
1113
|
-
throw new Error('store refused');
|
|
1114
|
-
return add(participant, content, metadata, opts);
|
|
1115
|
-
};
|
|
1116
|
-
trigger(framework);
|
|
1117
|
-
await framework.runUntilIdle();
|
|
1118
|
-
module.startHook = async () => {
|
|
1119
|
-
store.addMessage('Antra', [{ type: 'text', text: 'poison' }], {});
|
|
1120
|
-
store.addMessage('Antra', [{ type: 'text', text: 'second' }], {});
|
|
1121
|
-
};
|
|
1122
|
-
await release(framework);
|
|
1123
|
-
assert.equal(calls, 2, 'the second start failed before its stream');
|
|
1124
|
-
const texts = framework.getAgent('assistant').getContextManager().getAllMessages()
|
|
1125
|
-
.flatMap((m) => m.content.filter((b) => b.type === 'text').map((b) => b.text));
|
|
1126
|
-
assert.ok(texts.includes('second'), 'the message behind the refused one was stored');
|
|
1127
|
-
assert.ok(!texts.includes('poison'));
|
|
1128
|
-
await framework.stop();
|
|
1129
|
-
});
|
|
1130
|
-
for (const [label, notice] of [
|
|
1131
|
-
['a re-pin', 'The conversation moved to'],
|
|
1132
|
-
["a turn's announcement", 'Your plain speech now lands in'],
|
|
1133
|
-
]) {
|
|
1134
|
-
it(`the announce baseline moves only once ${label}'s notice is stored`, async () => {
|
|
1135
|
-
membrane.pushResponse(createMockResponse([
|
|
1136
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
1137
|
-
], 'tool_use'));
|
|
1138
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'Done.' }]));
|
|
1139
|
-
const framework = await createFramework();
|
|
1140
|
-
stubChannelRegistry(framework);
|
|
1141
|
-
module.toolDelayMs = 25;
|
|
1142
|
-
if (notice === 'The conversation moved to') {
|
|
1143
|
-
// Someone addresses the agent mid-turn from a DM: the turn re-pins.
|
|
1144
|
-
module.interjection = 'quick question for you';
|
|
1145
|
-
module.interjectionMetadata = addressed('discord:dm:antra');
|
|
1146
|
-
}
|
|
1147
|
-
const cm = framework.getAgent('assistant').getContextManager();
|
|
1148
|
-
const add = cm.addMessage.bind(cm);
|
|
1149
|
-
cm.addMessage = (participant, content, metadata) => {
|
|
1150
|
-
if (content[0]?.text?.includes(notice))
|
|
1151
|
-
throw new Error('store refused');
|
|
1152
|
-
return add(participant, content, metadata);
|
|
1153
|
-
};
|
|
1154
|
-
const baseline = framework.lastAnnouncedLocus;
|
|
1155
|
-
trigger(framework);
|
|
1156
|
-
await framework.runUntilIdle();
|
|
1157
|
-
if (notice === 'The conversation moved to') {
|
|
1158
|
-
assert.equal(baseline.get('assistant'), 'chan-live-1', 'the agent was last told of the locus the turn opened at');
|
|
1159
|
-
}
|
|
1160
|
-
else {
|
|
1161
|
-
assert.equal(baseline.has('assistant'), false, 'the agent was never told a locus');
|
|
1162
|
-
}
|
|
1163
|
-
await framework.stop();
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1166
|
-
// A tool's late write lands in the turn that called it: a send's engaged
|
|
1167
|
-
// channel, and a channel_open's move of the locus. (The dispatch functions
|
|
1168
|
-
// run on a bare framework, as checkpoint-attribution.test.ts's harness does.)
|
|
1169
|
-
function bareFramework() {
|
|
1170
|
-
const framework = Object.create(AgentFramework.prototype);
|
|
1171
|
-
framework.emitTrace = () => { };
|
|
1172
|
-
framework.pushEvent = () => { };
|
|
1173
|
-
framework.conversationAgentHomes = new Map();
|
|
1174
|
-
framework.lastAnnouncedLocus = new Map();
|
|
1175
|
-
framework.activeTurnTriggers = new Map();
|
|
1176
|
-
framework.openLogical = new Map();
|
|
1177
|
-
return framework;
|
|
1178
|
-
}
|
|
1179
|
-
const until = async (done, label) => {
|
|
1180
|
-
for (const end = Date.now() + 2000; !done();) {
|
|
1181
|
-
if (Date.now() > end)
|
|
1182
|
-
throw new Error(`timed out waiting for ${label}`);
|
|
1183
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
1184
|
-
}
|
|
1185
|
-
};
|
|
1186
|
-
// stop() ends every open turn, but a fresh start that awaits a deferred
|
|
1187
|
-
// batch's assembly while the stop runs reaches its routing after it, opens
|
|
1188
|
-
// its turn, and is then dropped before its stream sends anything: before
|
|
1189
|
-
// its compile, once stop() has closed admission, or after it, once a
|
|
1190
|
-
// restart has begun a lifecycle of its own.
|
|
1191
|
-
for (const restartFirst of [false, true]) {
|
|
1192
|
-
const name = restartFirst
|
|
1193
|
-
? "a start whose compile spans a stop() and restart is dropped and leaves no turn open, so the next start opens its own"
|
|
1194
|
-
: "a start that stop() drops before its compile leaves no turn open, so the first start after a restart opens its own";
|
|
1195
|
-
it(name, async () => {
|
|
1196
|
-
// A store the framework doesn't own, so the same instance can start again.
|
|
1197
|
-
const store = JsStore.openOrCreate({ path: join(tempDir, 'restart.chronicle') });
|
|
1198
|
-
const framework = await AgentFramework.create({
|
|
1199
|
-
store,
|
|
1200
|
-
membrane: membrane.asMembrane(),
|
|
1201
|
-
agents: [{ name: 'assistant', model: 'test-model', systemPrompt: 'You are a robot pilot.' }],
|
|
1202
|
-
modules: [module],
|
|
1203
|
-
syncIntervalMs: 0,
|
|
1204
|
-
maintenanceIntervalMs: 0,
|
|
1205
|
-
});
|
|
1206
|
-
module.framework = framework;
|
|
1207
|
-
const routed = stubChannelRegistry(framework);
|
|
1208
|
-
const internals = framework;
|
|
1209
|
-
const agent = framework.getAgent('assistant');
|
|
1210
|
-
const wake = () => ({
|
|
1211
|
-
agentName: 'assistant', reason: 'external-message', source: 'test', timestamp: Date.now(),
|
|
1212
|
-
});
|
|
1213
|
-
let entered;
|
|
1214
|
-
const assembling = new Promise((resolve) => { entered = resolve; });
|
|
1215
|
-
let release;
|
|
1216
|
-
const released = new Promise((resolve) => { release = resolve; });
|
|
1217
|
-
internals.pushCoalescer = {
|
|
1218
|
-
pendingBatches: () => true,
|
|
1219
|
-
assemble: async () => {
|
|
1220
|
-
entered();
|
|
1221
|
-
await released;
|
|
1222
|
-
return { settled: new Set(), materialized: new Set() };
|
|
1223
|
-
},
|
|
1224
|
-
suspend: () => { },
|
|
1225
|
-
isRendering: () => false,
|
|
1226
|
-
};
|
|
1227
|
-
const lines = [];
|
|
1228
|
-
const error = console.error;
|
|
1229
|
-
try {
|
|
1230
|
-
console.error = (...args) => { lines.push(args.map(String).join(' ')); };
|
|
1231
|
-
try {
|
|
1232
|
-
const dropped = internals.startAgentStream(agent, wake());
|
|
1233
|
-
await assembling;
|
|
1234
|
-
await framework.stop();
|
|
1235
|
-
if (restartFirst)
|
|
1236
|
-
framework.start();
|
|
1237
|
-
release();
|
|
1238
|
-
await dropped;
|
|
1239
|
-
}
|
|
1240
|
-
finally {
|
|
1241
|
-
console.error = error;
|
|
1242
|
-
}
|
|
1243
|
-
if (restartFirst) {
|
|
1244
|
-
assert.ok(lines.some((line) => /stopped and started again/.test(line) && /dropped before it sends anything/.test(line)), lines.join('\n'));
|
|
1245
|
-
}
|
|
1246
|
-
else {
|
|
1247
|
-
assert.equal(membrane.calls.length, 0, 'the start was dropped before its compile');
|
|
1248
|
-
}
|
|
1249
|
-
assert.deepEqual(routed, [], 'the dropped start routed nothing');
|
|
1250
|
-
assert.equal(internals.openLogical.has('assistant'), false, 'the dropped start left no turn open');
|
|
1251
|
-
if (!restartFirst)
|
|
1252
|
-
framework.start();
|
|
1253
|
-
internals.pushCoalescer = undefined;
|
|
1254
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'After the restart.' }]));
|
|
1255
|
-
await internals.startAgentStream(agent, wake());
|
|
1256
|
-
await until(() => routed.length > 0, "the next turn's speech");
|
|
1257
|
-
// The dropped start resolved chan-live-1. Continuing its turn would
|
|
1258
|
-
// have kept that locus, without resolving one.
|
|
1259
|
-
assert.deepEqual(routed, [{ text: 'After the restart.', locus: 'chan-live-2' }], 'the next start resolved a locus of its own');
|
|
1260
|
-
}
|
|
1261
|
-
finally {
|
|
1262
|
-
release();
|
|
1263
|
-
await framework.stop();
|
|
1264
|
-
store.close();
|
|
1265
|
-
}
|
|
1266
|
-
});
|
|
1267
|
-
}
|
|
1268
|
-
it("a send's engaged channel is recorded on the turn that sent, however late its open resolves", async () => {
|
|
1269
|
-
const sending = { engaged: new Set() };
|
|
1270
|
-
const next = { engaged: new Set() };
|
|
1271
|
-
let resolveOpen;
|
|
1272
|
-
const framework = bareFramework();
|
|
1273
|
-
framework.mcplServerRegistry = {
|
|
1274
|
-
getServer: (id) => id === 'chat' ? {
|
|
1275
|
-
id: 'chat',
|
|
1276
|
-
sendToolsCall: async () => ({ content: [{ type: 'text', text: 'sent' }] }),
|
|
1277
|
-
} : null,
|
|
1278
|
-
};
|
|
1279
|
-
framework.mcplServerConfigs = new Map([['chat', { id: 'chat', toolPrefix: 'chat' }]]);
|
|
1280
|
-
framework.mcplToolFeatureSets = new Map();
|
|
1281
|
-
framework.checkpointManager = null;
|
|
1282
|
-
framework.channelRegistry = {
|
|
1283
|
-
openIfClosedForSend: () => new Promise((resolve) => { resolveOpen = resolve; }),
|
|
1284
|
-
};
|
|
1285
|
-
framework.turnRouting = new Map([['assistant', sending]]);
|
|
1286
|
-
const results = [];
|
|
1287
|
-
framework.pushEvent = (event) => { if (event.type === 'tool-result')
|
|
1288
|
-
results.push(event); };
|
|
1289
|
-
framework.dispatchMcplToolCall('assistant', {
|
|
1290
|
-
id: 'send-1', name: 'chat--send_message', input: { channelId: 'discord:guild:portables', text: 'hi' },
|
|
1291
|
-
}, 'chat', 'chat');
|
|
1292
|
-
await until(() => results.length > 0 && resolveOpen !== undefined, 'the send to finish');
|
|
1293
|
-
// The turn that sent has ended, and the next turn is the agent's live routing.
|
|
1294
|
-
framework.turnRouting.set('assistant', next);
|
|
1295
|
-
resolveOpen({ status: 'already-open', channelId: 'discord:guild:portables' });
|
|
1296
|
-
await until(() => sending.engaged.size > 0, 'the open to resolve');
|
|
1297
|
-
assert.deepEqual([...sending.engaged], ['discord:guild:portables']);
|
|
1298
|
-
assert.deepEqual([...next.engaged], [], "the next turn didn't send there");
|
|
1299
|
-
});
|
|
1300
|
-
// A channel_open's result can land after a later start has made another
|
|
1301
|
-
// object the agent's live routing. While the turn that called is open, the
|
|
1302
|
-
// result moves that turn's locus and tells the agent. Once the turn has
|
|
1303
|
-
// ended, the tool-result handler drops the result, so the agent never reads
|
|
1304
|
-
// it, and nothing moves, whether or not a later start has come yet.
|
|
1305
|
-
for (const [label, after, moves] of [
|
|
1306
|
-
['moves the locus of its turn, still open, when a silent start\'s routing is the live one', 'silent start', true],
|
|
1307
|
-
['moves no locus once its turn has ended and the next turn has opened', 'next turn', false],
|
|
1308
|
-
['moves no locus once its turn has settled, before any later start', 'settled', false],
|
|
1309
|
-
]) {
|
|
1310
|
-
it(`a channel_open whose result lands late ${label}`, async () => {
|
|
1311
|
-
const calling = { engaged: new Set(), locus: 'chan-before' };
|
|
1312
|
-
const other = { engaged: new Set(), locus: 'chan-next' };
|
|
1313
|
-
let resolveOpen;
|
|
1314
|
-
const framework = bareFramework();
|
|
1315
|
-
framework.agents = new Map([['assistant', { proseRouting: 'locus' }]]);
|
|
1316
|
-
framework.channelRegistry = {
|
|
1317
|
-
handleChannelToolCall: () => new Promise((resolve) => { resolveOpen = resolve; }),
|
|
1318
|
-
};
|
|
1319
|
-
framework.turnRouting = new Map([['assistant', calling]]);
|
|
1320
|
-
framework.openLogical = new Map([['assistant', calling]]);
|
|
1321
|
-
const results = [];
|
|
1322
|
-
framework.pushEvent = (event) => {
|
|
1323
|
-
if (event.type === 'tool-result')
|
|
1324
|
-
results.push(event);
|
|
1325
|
-
};
|
|
1326
|
-
framework.dispatchChannelToolCall('assistant', {
|
|
1327
|
-
id: 'open-1', name: 'channel_open', input: { channelId: 'discord:guild:observatory' },
|
|
1328
|
-
});
|
|
1329
|
-
await until(() => resolveOpen !== undefined, 'the open to start');
|
|
1330
|
-
const turnRouting = framework.turnRouting;
|
|
1331
|
-
const openLogical = framework.openLogical;
|
|
1332
|
-
if (after === 'silent start')
|
|
1333
|
-
turnRouting.set('assistant', other);
|
|
1334
|
-
if (after === 'next turn') {
|
|
1335
|
-
turnRouting.set('assistant', other);
|
|
1336
|
-
openLogical.set('assistant', other);
|
|
1337
|
-
}
|
|
1338
|
-
if (after === 'settled')
|
|
1339
|
-
openLogical.delete('assistant');
|
|
1340
|
-
resolveOpen({ success: true, data: { channelId: 'discord:guild:observatory' } });
|
|
1341
|
-
await until(() => results.length > 0, 'the tool result');
|
|
1342
|
-
assert.equal(calling.triggerChannel, 'discord:guild:observatory', 'the turn that called records the channel it opened');
|
|
1343
|
-
assert.equal(calling.locus, moves ? 'discord:guild:observatory' : 'chan-before');
|
|
1344
|
-
assert.deepEqual(other, { engaged: new Set(), locus: 'chan-next' }, 'the other routing state is untouched');
|
|
1345
|
-
assert.equal(framework.lastAnnouncedLocus.get('assistant'), moves ? 'discord:guild:observatory' : undefined, "the announce baseline moves with the locus, for a move the agent is told");
|
|
1346
|
-
assert.equal(results[0].result.data?.routing !== undefined, moves, 'and the result says so only then');
|
|
1347
|
-
});
|
|
1348
|
-
}
|
|
1349
|
-
for (const [label, silent, expected] of [
|
|
1350
|
-
['while its turn runs moves the locus and the announce baseline, and says so', false, 'discord:guild:observatory'],
|
|
1351
|
-
['in a silent turn moves no locus, and claims none', true, 'chan-before'],
|
|
1352
|
-
]) {
|
|
1353
|
-
it(`a channel_open ${label}`, async () => {
|
|
1354
|
-
const calling = { engaged: new Set(), locus: 'chan-before' };
|
|
1355
|
-
const framework = bareFramework();
|
|
1356
|
-
framework.agents = new Map([['assistant', { proseRouting: 'locus' }]]);
|
|
1357
|
-
framework.channelRegistry = {
|
|
1358
|
-
handleChannelToolCall: async () => ({ success: true, data: { channelId: 'discord:guild:observatory' } }),
|
|
1359
|
-
};
|
|
1360
|
-
framework.turnRouting = new Map([['assistant', calling]]);
|
|
1361
|
-
if (!silent)
|
|
1362
|
-
framework.openLogical = new Map([['assistant', calling]]);
|
|
1363
|
-
framework.activeTurnTriggers.set('assistant', silent ? { suppressProse: true } : {});
|
|
1364
|
-
const results = [];
|
|
1365
|
-
framework.pushEvent = (event) => {
|
|
1366
|
-
if (event.type === 'tool-result')
|
|
1367
|
-
results.push(event);
|
|
1368
|
-
};
|
|
1369
|
-
framework.dispatchChannelToolCall('assistant', {
|
|
1370
|
-
id: 'open-1', name: 'channel_open', input: { channelId: 'discord:guild:observatory' },
|
|
1371
|
-
});
|
|
1372
|
-
await until(() => results.length > 0, 'the tool result');
|
|
1373
|
-
assert.equal(calling.locus, expected);
|
|
1374
|
-
assert.equal(framework.lastAnnouncedLocus.get('assistant'), silent ? undefined : 'discord:guild:observatory');
|
|
1375
|
-
assert.equal(results[0].result.data?.routing !== undefined, !silent, 'the result tells the agent only of a move it made');
|
|
1376
|
-
});
|
|
1377
|
-
}
|
|
1378
|
-
it('a prose turn ends with a [delivered] receipt naming where the prose landed', async () => {
|
|
1379
|
-
// Explicit sends receipt themselves via tool_result; auto-routed prose
|
|
1380
|
-
// previously vanished with no in-window trace — the agent could never
|
|
1381
|
-
// see where its own words went (2026-07-31 misroute series). One compact
|
|
1382
|
-
// system message at logical turn end, channels deduped in delivery order.
|
|
1383
|
-
membrane.pushResponse(createMockResponse([
|
|
1384
|
-
{ type: 'text', text: 'A quiet reply with no tools.' },
|
|
1385
|
-
]));
|
|
1386
|
-
const framework = await createFramework();
|
|
1387
|
-
stubChannelRegistry(framework);
|
|
1388
|
-
trigger(framework);
|
|
1389
|
-
await framework.runUntilIdle();
|
|
1390
|
-
const cm = framework.agents.get('assistant').getContextManager();
|
|
1391
|
-
const texts = cm.getAllMessages()
|
|
1392
|
-
.flatMap((m) => m.content)
|
|
1393
|
-
.filter((b) => b.type === 'text')
|
|
1394
|
-
.map((b) => b.text ?? '');
|
|
1395
|
-
const receipts = texts.filter((t) => t.startsWith('[delivered]'));
|
|
1396
|
-
assert.deepEqual(receipts, ['[delivered] plain speech → chan-live-1']);
|
|
1397
|
-
// The receipt is the LAST window message — after the assistant blocks.
|
|
1398
|
-
assert.ok(texts[texts.length - 1].startsWith('[delivered]'), 'receipt sits at the settled tail');
|
|
1399
|
-
await framework.stop();
|
|
1400
|
-
});
|
|
1401
|
-
it('a re-pinned turn\'s receipt names both channels in delivery order', async () => {
|
|
1402
|
-
membrane.pushResponse(createMockResponse([
|
|
1403
|
-
{ type: 'text', text: 'Station four, proceeding.' },
|
|
1404
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
1405
|
-
], 'tool_use'));
|
|
1406
|
-
membrane.pushResponse(createMockResponse([
|
|
1407
|
-
{ type: 'text', text: 'Answering the person who addressed me.' },
|
|
1408
|
-
]));
|
|
1409
|
-
const framework = await createFramework();
|
|
1410
|
-
stubChannelRegistry(framework);
|
|
1411
|
-
module.toolDelayMs = 25;
|
|
1412
|
-
module.interjection = 'quick question over here?';
|
|
1413
|
-
module.interjectionMetadata = { channelId: 'discord:dm:antra', tags: ['chat:addressed'] };
|
|
1414
|
-
trigger(framework);
|
|
1415
|
-
await framework.runUntilIdle();
|
|
1416
|
-
const cm = framework.agents.get('assistant').getContextManager();
|
|
1417
|
-
const receipts = cm.getAllMessages()
|
|
1418
|
-
.flatMap((m) => m.content)
|
|
1419
|
-
.filter((b) => b.type === 'text')
|
|
1420
|
-
.map((b) => b.text ?? '')
|
|
1421
|
-
.filter((t) => t.startsWith('[delivered]'));
|
|
1422
|
-
assert.deepEqual(receipts, ['[delivered] plain speech → chan-live-1 · discord:dm:antra']);
|
|
1423
|
-
await framework.stop();
|
|
1424
|
-
});
|
|
1425
|
-
it('suppressed prose is visible in the receipt (silencing is never a silent black hole)', async () => {
|
|
1426
|
-
// n=8 (2026-07-31 ~22:46): one round multiplexing two threads — prose
|
|
1427
|
-
// for the lane + an explicit send for another channel, textbook per the
|
|
1428
|
-
// routing doc. Sticky silencing ate the prose reply and nothing told
|
|
1429
|
-
// the author; their own record believed it delivered. The rule stands
|
|
1430
|
-
// (antra: visibility is enough) — but the turn-end receipt now reports
|
|
1431
|
-
// the suppression, so the segment's fate is visible one turn later.
|
|
1432
|
-
membrane.pushResponse(createMockResponse([
|
|
1433
|
-
{ type: 'text', text: 'the reply that silencing will eat' },
|
|
1434
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--send_message', input: { text: 'explicit send to the other thread' } },
|
|
1435
|
-
], 'tool_use'));
|
|
1436
|
-
membrane.pushResponse(createMockResponse([
|
|
1437
|
-
{ type: 'text', text: 'trailing prose, also suppressed (sticky)' },
|
|
1438
|
-
]));
|
|
1439
|
-
const framework = await createFramework();
|
|
1440
|
-
const routed = stubChannelRegistry(framework);
|
|
1441
|
-
trigger(framework);
|
|
1442
|
-
await framework.runUntilIdle();
|
|
1443
|
-
assert.deepEqual(routed, [], 'silencing semantics unchanged: nothing auto-routed');
|
|
1444
|
-
const cm = framework.agents.get('assistant').getContextManager();
|
|
1445
|
-
const receipts = cm.getAllMessages()
|
|
1446
|
-
.flatMap((m) => m.content)
|
|
1447
|
-
.filter((b) => b.type === 'text')
|
|
1448
|
-
.map((b) => b.text ?? '')
|
|
1449
|
-
.filter((t) => t.startsWith('[delivered]'));
|
|
1450
|
-
assert.deepEqual(receipts, [
|
|
1451
|
-
'[delivered] nothing — 2 plain-speech segment(s) suppressed (explicit send in the same round — resend with a send tool if it was meant to be heard)',
|
|
1452
|
-
]);
|
|
1453
|
-
await framework.stop();
|
|
1454
|
-
});
|
|
1455
|
-
for (const verb of ['say', 'whisper']) {
|
|
1456
|
-
it(`world \`${verb}\` silences adjacent auto-routed prose in locus mode (no double-publish)`, async () => {
|
|
1457
|
-
// 2026-09-01 (Cairn, locus mode): a round of ordinary text + explicit
|
|
1458
|
-
// world `say` published TWICE — seq 15146 was the say text, seq 15147
|
|
1459
|
-
// the adjacent prose auto-routed to the same world locus, byte-for-byte.
|
|
1460
|
-
// World publication verbs silenced only in hybrid mode; Discord sends
|
|
1461
|
-
// silenced everywhere. An explicit world utterance is the resident's
|
|
1462
|
-
// chosen speech for the round in every mode.
|
|
1463
|
-
const input = verb === 'say'
|
|
1464
|
-
? { text: 'the intended utterance' }
|
|
1465
|
-
: { to: 'sill', text: 'the intended utterance' };
|
|
1466
|
-
membrane.pushResponse(createMockResponse([
|
|
1467
|
-
{ type: 'text', text: 'adjacent prose that must NOT auto-publish' },
|
|
1468
|
-
{ type: 'tool_use', id: 'c1', name: `robot--${verb}`, input },
|
|
1469
|
-
], 'tool_use'));
|
|
1470
|
-
membrane.pushResponse(createMockResponse([
|
|
1471
|
-
{ type: 'text', text: 'trailing prose, also suppressed (sticky)' },
|
|
1472
|
-
]));
|
|
1473
|
-
const framework = await createFramework();
|
|
1474
|
-
const routed = stubChannelRegistry(framework);
|
|
1475
|
-
trigger(framework);
|
|
1476
|
-
await framework.runUntilIdle();
|
|
1477
|
-
assert.deepEqual(routed, [], `${verb}: nothing auto-routed beside the explicit world utterance`);
|
|
1478
|
-
const cm = framework.agents.get('assistant').getContextManager();
|
|
1479
|
-
const receipts = cm.getAllMessages()
|
|
1480
|
-
.flatMap((m) => m.content)
|
|
1481
|
-
.filter((b) => b.type === 'text')
|
|
1482
|
-
.map((b) => b.text ?? '')
|
|
1483
|
-
.filter((t) => t.startsWith('[delivered]'));
|
|
1484
|
-
assert.deepEqual(receipts, [
|
|
1485
|
-
'[delivered] nothing — 2 plain-speech segment(s) suppressed (explicit send in the same round — resend with a send tool if it was meant to be heard)',
|
|
1486
|
-
], `${verb}: suppression is visible in the receipt`);
|
|
1487
|
-
await framework.stop();
|
|
1488
|
-
});
|
|
1489
|
-
}
|
|
1490
|
-
it('a non-publishing world tool (move) does not silence — speak-while-acting unchanged', async () => {
|
|
1491
|
-
// Negative control for the world-verb silencing: only publication verbs
|
|
1492
|
-
// silence. Ordinary acting tools still narrate live to the locus.
|
|
1493
|
-
membrane.pushResponse(createMockResponse([
|
|
1494
|
-
{ type: 'text', text: 'walking over' },
|
|
1495
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
1496
|
-
], 'tool_use'));
|
|
1497
|
-
membrane.pushResponse(createMockResponse([
|
|
1498
|
-
{ type: 'text', text: 'there' },
|
|
1499
|
-
]));
|
|
1500
|
-
const framework = await createFramework();
|
|
1501
|
-
const routed = stubChannelRegistry(framework);
|
|
1502
|
-
trigger(framework);
|
|
1503
|
-
await framework.runUntilIdle();
|
|
1504
|
-
assert.deepEqual(routed.map((r) => r.text), ['walking over', 'there']);
|
|
1505
|
-
await framework.stop();
|
|
1506
|
-
});
|
|
1507
|
-
it('channel_open moves the pin mid-turn and announces in its own tool result', async () => {
|
|
1508
|
-
// The agent's own deliberate open is the strongest "my next words go
|
|
1509
|
-
// here" signal — stronger than any injection. The original 2026-07-21
|
|
1510
|
-
// Aria fix only set next-turn trigger state; the turn-frozen refactor
|
|
1511
|
-
// silently regressed the reply-right-after-opening case. Now the pin
|
|
1512
|
-
// moves immediately and the announcement rides the tool result itself
|
|
1513
|
-
// (model-requested content: distance zero, safest role).
|
|
1514
|
-
membrane.pushResponse(createMockResponse([
|
|
1515
|
-
{ type: 'tool_use', id: 'c1', name: 'channel_open', input: { channelId: 'discord:guild:observatory' } },
|
|
1516
|
-
], 'tool_use'));
|
|
1517
|
-
membrane.pushResponse(createMockResponse([
|
|
1518
|
-
{ type: 'text', text: 'Hello, observatory.' },
|
|
1519
|
-
]));
|
|
1520
|
-
const framework = await createFramework();
|
|
1521
|
-
const routed = stubChannelRegistry(framework);
|
|
1522
|
-
const registry = framework.channelRegistry;
|
|
1523
|
-
registry.handleChannelToolCall =
|
|
1524
|
-
async () => ({ success: true, data: { channelId: 'discord:guild:observatory', opened: true } });
|
|
1525
|
-
trigger(framework);
|
|
1526
|
-
await framework.runUntilIdle();
|
|
1527
|
-
assert.deepEqual(routed, [
|
|
1528
|
-
{ text: 'Hello, observatory.', locus: 'discord:guild:observatory' },
|
|
1529
|
-
], 'prose right after channel_open lands in the opened channel');
|
|
1530
|
-
// The announcement rode the tool result the model saw.
|
|
1531
|
-
const results = membrane.lastStream.receivedToolResults.flat();
|
|
1532
|
-
assert.ok(JSON.stringify(results).includes('plain speech now lands in this channel'), 'routing note present in the channel_open tool result');
|
|
1533
|
-
await framework.stop();
|
|
1534
|
-
});
|
|
1535
|
-
it('reactions and system markers injected mid-turn do not clear send suppression', async () => {
|
|
1536
|
-
// A reaction (`chat:reaction` tag) or a `system: true` marker is not
|
|
1537
|
-
// conversational input: prose following an explicit send stays suppressed,
|
|
1538
|
-
// so a stray reaction can't make the agent double-post a "sent it"
|
|
1539
|
-
// postscript — and (with the frozen locus) can't move routing either.
|
|
1540
|
-
membrane.pushResponse(createMockResponse([
|
|
1541
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--send_message', input: { text: 'reply to room4' } },
|
|
1542
|
-
], 'tool_use'));
|
|
1543
|
-
membrane.pushResponse(createMockResponse([
|
|
1544
|
-
{ type: 'text', text: 'Sent it.' },
|
|
1545
|
-
]));
|
|
1546
|
-
const framework = await createFramework();
|
|
1547
|
-
const routed = stubChannelRegistry(framework);
|
|
1548
|
-
module.interjection = '[reaction] @someone reacted 👍';
|
|
1549
|
-
module.interjectionChannelId = null;
|
|
1550
|
-
module.interjectionMetadata = { channelId: '999888777', tags: ['chat:reaction'] };
|
|
1551
|
-
trigger(framework);
|
|
1552
|
-
await framework.runUntilIdle();
|
|
1553
|
-
assert.deepEqual(routed, [], 'post-send prose stayed suppressed after a reaction');
|
|
1554
|
-
await framework.stop();
|
|
1555
|
-
});
|
|
1556
|
-
it('text-only turns route to the turn-frozen locus, not a live re-resolution', async () => {
|
|
1557
|
-
// The text-only dispatch runs after the agent is idle; a live resolution
|
|
1558
|
-
// there can read the NEXT turn's trigger state or a post-restart cleared
|
|
1559
|
-
// one (2026-07-22 Sol DM misroute). The stub increments its locus per
|
|
1560
|
-
// resolveLocus() call: the turn-start freeze consumes chan-live-1, so a
|
|
1561
|
-
// live re-resolution at dispatch would return chan-live-2. The frozen
|
|
1562
|
-
// path must deliver to chan-live-1.
|
|
1563
|
-
membrane.pushResponse(createMockResponse([
|
|
1564
|
-
{ type: 'text', text: 'A quiet reply with no tools.' },
|
|
1565
|
-
]));
|
|
1566
|
-
const framework = await createFramework();
|
|
1567
|
-
const routed = stubChannelRegistry(framework);
|
|
1568
|
-
trigger(framework);
|
|
1569
|
-
await framework.runUntilIdle();
|
|
1570
|
-
assert.deepEqual(routed, [
|
|
1571
|
-
{ text: 'A quiet reply with no tools.', locus: 'chan-live-1' },
|
|
1572
|
-
]);
|
|
1573
|
-
await framework.stop();
|
|
1574
|
-
});
|
|
1575
|
-
it('announces the outbound locus in the window only when it changes', async () => {
|
|
1576
|
-
// Turn 1 (locus chan-live-1, e2e): one durable [routing] notice — the
|
|
1577
|
-
// boot baseline. Then the announce-on-change logic directly (MockMembrane
|
|
1578
|
-
// supports one turn per test): same locus → silence (steady state must
|
|
1579
|
-
// not chatter — KV); new locus → exactly one more notice.
|
|
1580
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'one' }]));
|
|
1581
|
-
const framework = await createFramework();
|
|
1582
|
-
stubChannelRegistry(framework);
|
|
1583
|
-
const notices = [];
|
|
1584
|
-
framework.onTrace((event) => {
|
|
1585
|
-
const e = event;
|
|
1586
|
-
if (e.type === 'message:added' && e.source === 'routing-notice') {
|
|
1587
|
-
notices.push(e.source);
|
|
1588
|
-
}
|
|
1589
|
-
});
|
|
1590
|
-
trigger(framework);
|
|
1591
|
-
await framework.runUntilIdle();
|
|
1592
|
-
assert.equal(notices.length, 1, 'first turn announced the boot-baseline locus');
|
|
1593
|
-
const announce = framework.announceLocusIfChanged.bind(framework);
|
|
1594
|
-
announce('assistant', 'chan-live-1');
|
|
1595
|
-
assert.equal(notices.length, 1, 'unchanged locus announced nothing');
|
|
1596
|
-
announce('assistant', 'chan-B');
|
|
1597
|
-
assert.equal(notices.length, 2, 'changed locus announced exactly once');
|
|
1598
|
-
announce('assistant', 'chan-B');
|
|
1599
|
-
assert.equal(notices.length, 2, 'steady state on the new locus stays silent');
|
|
1600
|
-
announce('assistant', null);
|
|
1601
|
-
assert.equal(notices.length, 3, 'losing the locus is announced too');
|
|
1602
|
-
await framework.stop();
|
|
1603
|
-
});
|
|
1604
|
-
// -------------------------------------------------------------------------
|
|
1605
|
-
// Hear-while-acting
|
|
1606
|
-
// -------------------------------------------------------------------------
|
|
1607
|
-
it('injects a mid-turn message into the resumed stream at the tool boundary', async () => {
|
|
1608
|
-
membrane.pushResponse(createMockResponse([
|
|
1609
|
-
{ type: 'text', text: 'Moving.' },
|
|
1610
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
1611
|
-
], 'tool_use'));
|
|
1612
|
-
membrane.pushResponse(createMockResponse([
|
|
1613
|
-
{ type: 'text', text: 'Heard you!' },
|
|
1614
|
-
]));
|
|
1615
|
-
const framework = await createFramework();
|
|
1616
|
-
module.interjection = 'look left!';
|
|
1617
|
-
trigger(framework);
|
|
1618
|
-
await framework.runUntilIdle();
|
|
1619
|
-
const stream = membrane.lastStream;
|
|
1620
|
-
assert.equal(stream.receivedToolResults.length, 1);
|
|
1621
|
-
const options = stream.receivedToolResultOptions[0];
|
|
1622
|
-
assert.ok(options?.injectedMessages, 'tool-result resume carried injected messages');
|
|
1623
|
-
assert.equal(options.injectedMessages.length, 1);
|
|
1624
|
-
const injected = options.injectedMessages[0];
|
|
1625
|
-
assert.equal(injected.participant, 'Antra');
|
|
1626
|
-
const text = injected.content
|
|
1627
|
-
.filter((b) => b.type === 'text')
|
|
1628
|
-
.map((b) => b.text)
|
|
1629
|
-
.join('\n');
|
|
1630
|
-
assert.equal(text, 'look left!');
|
|
1631
|
-
await framework.stop();
|
|
1632
|
-
});
|
|
1633
|
-
it('passes no injection options when nothing arrived mid-turn', async () => {
|
|
1634
|
-
membrane.pushResponse(createMockResponse([
|
|
1635
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
1636
|
-
], 'tool_use'));
|
|
1637
|
-
membrane.pushResponse(createMockResponse([
|
|
1638
|
-
{ type: 'text', text: 'done' },
|
|
1639
|
-
]));
|
|
1640
|
-
const framework = await createFramework();
|
|
1641
|
-
trigger(framework);
|
|
1642
|
-
await framework.runUntilIdle();
|
|
1643
|
-
const stream = membrane.lastStream;
|
|
1644
|
-
assert.equal(stream.receivedToolResults.length, 1);
|
|
1645
|
-
assert.equal(stream.receivedToolResultOptions[0], undefined);
|
|
1646
|
-
await framework.stop();
|
|
1647
|
-
});
|
|
1648
|
-
});
|
|
1649
|
-
//# sourceMappingURL=present-while-acting.test.js.map
|