@animalabs/agent-framework 0.22.2 → 0.22.3
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 +3 -1
- package/dist/src/agent.d.ts +77 -12
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +136 -40
- 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/py-runner.d.ts +16 -5
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +186 -74
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
- package/dist/src/code-execution/runtime-py.js +49 -3
- package/dist/src/code-execution/runtime-py.js.map +1 -1
- package/dist/src/code-execution/tool-definition.d.ts +12 -1
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +21 -3
- 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 +52 -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 +828 -88
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +5011 -1326
- 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 +27 -3
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +144 -6
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +424 -263
- 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 +89 -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 +3 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +3 -1
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +15 -11
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +153 -84
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +33 -11
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +52 -20
- 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 +14 -2
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +72 -30
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +47 -12
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +74 -21
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +89 -10
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +405 -129
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts +10 -1
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +14 -3
- 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/transport.d.ts +51 -2
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +114 -20
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +14 -3
- 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 +15 -0
- package/dist/src/mcpl/visible-content.d.ts.map +1 -1
- package/dist/src/mcpl/visible-content.js +25 -0
- package/dist/src/mcpl/visible-content.js.map +1 -1
- package/dist/src/module-registry.d.ts +19 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +54 -25
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.js +1 -1
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts +1 -0
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +25 -9
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +29 -3
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +379 -143
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/discord/types.d.ts +14 -1
- 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 +2 -1
- 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/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 +32 -2
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +30 -2
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +64 -6
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +291 -44
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +119 -23
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +793 -247
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +75 -8
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +174 -14
- 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 +18 -4
- 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 +7 -2
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +28 -4
- 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/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 +257 -60
- 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 +17 -5
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +17 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +21 -5
- 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 +23 -4
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +95 -6
- 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 +3 -4
- package/src/agent.ts +162 -40
- 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/py-runner.ts +200 -76
- package/src/code-execution/runtime-py.ts +49 -3
- package/src/code-execution/tool-definition.ts +28 -4
- package/src/config-checks.ts +48 -0
- package/src/deadline-timer.ts +44 -0
- package/src/error-message.ts +48 -0
- package/src/framework.ts +5352 -1388
- package/src/gate/event-gate.ts +28 -3
- package/src/index.ts +11 -2
- package/src/mcpl/channel-registry.ts +517 -286
- package/src/mcpl/checkpoint-manager.ts +104 -29
- package/src/mcpl/conversation-router.ts +28 -8
- package/src/mcpl/convert-block.ts +97 -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 +10 -1
- package/src/mcpl/inference-router.ts +174 -99
- package/src/mcpl/push-coalescer.ts +60 -24
- package/src/mcpl/push-handler.ts +20 -90
- package/src/mcpl/reference-fetcher.ts +71 -26
- package/src/mcpl/references.ts +81 -22
- package/src/mcpl/server-connection.ts +427 -132
- package/src/mcpl/server-registry.ts +15 -4
- package/src/mcpl/skip-reply-wake.ts +73 -0
- package/src/mcpl/transport.ts +120 -18
- package/src/mcpl/types.ts +14 -3
- package/src/mcpl/visible-content.ts +27 -0
- package/src/module-registry.ts +58 -23
- package/src/modules/api/index.ts +1 -1
- package/src/modules/discord/discord-js-client.ts +26 -10
- package/src/modules/discord/index.ts +391 -158
- package/src/modules/discord/types.ts +15 -2
- package/src/modules/health/index.ts +2 -1
- package/src/modules/history/index.ts +410 -120
- package/src/modules/history/semantic.ts +189 -32
- package/src/modules/unknown-tool.ts +16 -0
- package/src/modules/workspace/disk-agreement.ts +51 -3
- package/src/modules/workspace/effects.ts +279 -35
- package/src/modules/workspace/index.ts +810 -260
- package/src/modules/workspace/observe.ts +194 -20
- package/src/modules/workspace/reconcile.ts +619 -64
- package/src/modules/workspace/types.ts +18 -4
- package/src/modules/workspace/watcher.ts +27 -4
- 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/tool-image-ledger.ts +153 -34
- package/src/tool-presentation.ts +224 -40
- 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 +17 -7
- package/src/types/events.ts +17 -0
- package/src/types/framework.ts +21 -5
- package/src/types/inference-log.ts +1 -1
- package/src/types/module.ts +24 -4
- package/src/types/trace.ts +101 -6
- 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,1715 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Explicit prose routing (docs/explicit-prose-routing.md).
|
|
3
|
-
*
|
|
4
|
-
* The model names every destination in-band (`>>` prefixes); unprefixed prose
|
|
5
|
-
* is NEVER delivered — it bounces to a clipboard for a prefixed resend. The
|
|
6
|
-
* laria regression (2026-07-24): a DM reply generated while the host-inferred
|
|
7
|
-
* locus pointed at a guild channel was misdelivered there. In explicit mode
|
|
8
|
-
* that class is structurally impossible: the worst case is a bounce.
|
|
9
|
-
*/
|
|
10
|
-
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
11
|
-
import assert from 'node:assert/strict';
|
|
12
|
-
import { tmpdir } from 'node:os';
|
|
13
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
14
|
-
import { join } from 'node:path';
|
|
15
|
-
import { AgentFramework } from '../src/index.js';
|
|
16
|
-
import { MockMembrane, createMockResponse } from './helpers/mock-membrane.js';
|
|
17
|
-
import { ProseStreamRouter } from '../src/mcpl/prose-stream-router.js';
|
|
18
|
-
import { MembraneError } from '@animalabs/membrane';
|
|
19
|
-
class ToolboxModule {
|
|
20
|
-
name = 'robot';
|
|
21
|
-
framework = null;
|
|
22
|
-
calls = [];
|
|
23
|
-
async start(_ctx) { }
|
|
24
|
-
async stop() { }
|
|
25
|
-
getTools() {
|
|
26
|
-
return [{
|
|
27
|
-
name: 'move',
|
|
28
|
-
description: 'Move the robot',
|
|
29
|
-
inputSchema: { type: 'object', properties: { dir: { type: 'string' } } },
|
|
30
|
-
}, {
|
|
31
|
-
name: 'say',
|
|
32
|
-
description: 'Publish speech into the world',
|
|
33
|
-
inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
|
|
34
|
-
}];
|
|
35
|
-
}
|
|
36
|
-
async handleToolCall(call) {
|
|
37
|
-
this.calls.push(call);
|
|
38
|
-
return { success: true, data: { ok: true, published: call.name.endsWith('--say') } };
|
|
39
|
-
}
|
|
40
|
-
async onProcess(event, _state) {
|
|
41
|
-
if (event.type === 'external-message') {
|
|
42
|
-
const text = String(event.content);
|
|
43
|
-
return {
|
|
44
|
-
addMessages: [{ participant: 'Antra', content: [{ type: 'text', text }] }],
|
|
45
|
-
requestInference: text === 'go',
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
return {};
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
/** Registry stub: two known channels (#alpha guild, laria's DM) + capture. */
|
|
52
|
-
function stubRegistry(framework, plan = []) {
|
|
53
|
-
const routed = [];
|
|
54
|
-
const known = {
|
|
55
|
-
'#alpha': { channelId: 'chan-alpha', label: '#alpha' },
|
|
56
|
-
'#cafe': { channelId: 'discord:g:cafe', label: '#cafe' },
|
|
57
|
-
'world:commons': { channelId: 'world:commons', label: 'world:commons' },
|
|
58
|
-
'alpha': { channelId: 'chan-alpha', label: '#alpha' },
|
|
59
|
-
'chan-alpha': { channelId: 'chan-alpha', label: '#alpha' },
|
|
60
|
-
'@laria': { channelId: 'discord:dm:99', label: 'DM: laria' },
|
|
61
|
-
'discord:dm:99': { channelId: 'discord:dm:99', label: 'DM: laria' },
|
|
62
|
-
};
|
|
63
|
-
const explicit = {
|
|
64
|
-
resolveProseTarget: (spec) => known[spec] ?? { error: `no channel matches "${spec}"` },
|
|
65
|
-
routeSpeech: async (_agent, text, locus) => {
|
|
66
|
-
routed.push({ text, locus: locus ?? null });
|
|
67
|
-
const behavior = plan.shift() ?? 'delivered';
|
|
68
|
-
if (behavior === 'throw')
|
|
69
|
-
throw new Error('synthetic publication failure');
|
|
70
|
-
return { delivered: behavior === 'delivered', channelId: locus ?? '' };
|
|
71
|
-
},
|
|
72
|
-
resolveLocus: () => 'world:commons',
|
|
73
|
-
getDefaultPublishChannel: () => null,
|
|
74
|
-
isChannelOpen: () => true,
|
|
75
|
-
getDescriptor: () => undefined,
|
|
76
|
-
getChannelTools: () => [],
|
|
77
|
-
};
|
|
78
|
-
framework.channelRegistry = new Proxy(explicit, {
|
|
79
|
-
get: (target, prop) => (prop in target ? target[prop] : () => undefined),
|
|
80
|
-
});
|
|
81
|
-
return routed;
|
|
82
|
-
}
|
|
83
|
-
describe('explicit prose routing', () => {
|
|
84
|
-
let tempDir;
|
|
85
|
-
let membrane;
|
|
86
|
-
let module;
|
|
87
|
-
beforeEach(() => {
|
|
88
|
-
tempDir = mkdtempSync(join(tmpdir(), 'epr-test-'));
|
|
89
|
-
membrane = new MockMembrane();
|
|
90
|
-
module = new ToolboxModule();
|
|
91
|
-
});
|
|
92
|
-
afterEach(() => {
|
|
93
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
94
|
-
});
|
|
95
|
-
async function createFramework(mode = 'explicit') {
|
|
96
|
-
const framework = await AgentFramework.create({
|
|
97
|
-
storePath: join(tempDir, 'test.chronicle'),
|
|
98
|
-
membrane: membrane.asMembrane(),
|
|
99
|
-
agents: [{
|
|
100
|
-
name: 'assistant',
|
|
101
|
-
model: 'test-model',
|
|
102
|
-
systemPrompt: 'You are a test agent.',
|
|
103
|
-
proseRouting: mode,
|
|
104
|
-
}],
|
|
105
|
-
modules: [module],
|
|
106
|
-
});
|
|
107
|
-
module.framework = framework;
|
|
108
|
-
return framework;
|
|
109
|
-
}
|
|
110
|
-
function trigger(framework) {
|
|
111
|
-
framework.pushEvent({
|
|
112
|
-
type: 'external-message',
|
|
113
|
-
source: 'test',
|
|
114
|
-
content: 'go',
|
|
115
|
-
metadata: {},
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
/** Feed a response for the Nth turn (1-based) the moment it starts —
|
|
119
|
-
* MockMembrane hands ALL remaining responses to the first stream, so
|
|
120
|
-
* later turns must receive theirs at inference:started time. */
|
|
121
|
-
function feedTurn(framework, turn, blocks, stop) {
|
|
122
|
-
let count = 0;
|
|
123
|
-
const off = framework.onTrace((e) => {
|
|
124
|
-
if (e.type !== 'inference:started')
|
|
125
|
-
return;
|
|
126
|
-
count++;
|
|
127
|
-
if (count === turn) {
|
|
128
|
-
membrane.pushResponse(createMockResponse(blocks, stop));
|
|
129
|
-
off();
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
it('unprefixed prose NEVER delivers — bounces to clipboard, then a prefixed {{unsent}} resend delivers verbatim (laria regression)', async () => {
|
|
134
|
-
const original = 'laria.\n\nYes, I remember. Two words through the keyhole.';
|
|
135
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: original }]));
|
|
136
|
-
const framework = await createFramework();
|
|
137
|
-
const routed = stubRegistry(framework);
|
|
138
|
-
const bounceNotices = [];
|
|
139
|
-
framework.onTrace((e) => {
|
|
140
|
-
const ev = e;
|
|
141
|
-
if (ev.type === 'message:added' && ev.source === 'prose-bounce')
|
|
142
|
-
bounceNotices.push(1);
|
|
143
|
-
});
|
|
144
|
-
// Turn 2 = the bounce-wake: model resends with a destination.
|
|
145
|
-
feedTurn(framework, 2, [{ type: 'text', text: '>>@laria {{unsent}}' }]);
|
|
146
|
-
trigger(framework);
|
|
147
|
-
await framework.runUntilIdle();
|
|
148
|
-
assert.equal(bounceNotices.length, 1, 'exactly one bounce notice');
|
|
149
|
-
assert.equal(routed.length, 1, 'only the prefixed resend delivered');
|
|
150
|
-
assert.equal(routed[0].locus, 'discord:dm:99', 'delivered to the DM the model named');
|
|
151
|
-
assert.equal(routed[0].text, original, 'clipboard substitution is verbatim');
|
|
152
|
-
await framework.stop();
|
|
153
|
-
});
|
|
154
|
-
it('resolves label prefixes and applies the sticky turn target to later unprefixed segments', async () => {
|
|
155
|
-
membrane.pushResponse(createMockResponse([
|
|
156
|
-
{ type: 'text', text: '>>#alpha first segment' },
|
|
157
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
158
|
-
], 'tool_use'));
|
|
159
|
-
membrane.pushResponse(createMockResponse([
|
|
160
|
-
{ type: 'text', text: 'second segment, no prefix' },
|
|
161
|
-
]));
|
|
162
|
-
const framework = await createFramework();
|
|
163
|
-
const routed = stubRegistry(framework);
|
|
164
|
-
trigger(framework);
|
|
165
|
-
await framework.runUntilIdle();
|
|
166
|
-
assert.deepEqual(routed, [
|
|
167
|
-
{ text: 'first segment', locus: 'chan-alpha' },
|
|
168
|
-
{ text: 'second segment, no prefix', locus: 'chan-alpha' },
|
|
169
|
-
], 'prefix stripped on wire; sticky target carries the second segment');
|
|
170
|
-
await framework.stop();
|
|
171
|
-
});
|
|
172
|
-
it('>>skip_reply keeps the text in context — nothing delivered, no bounce', async () => {
|
|
173
|
-
membrane.pushResponse(createMockResponse([
|
|
174
|
-
{ type: 'text', text: '>>skip_reply\nWorking through the plan here.' },
|
|
175
|
-
]));
|
|
176
|
-
const framework = await createFramework();
|
|
177
|
-
const routed = stubRegistry(framework);
|
|
178
|
-
const bounceNotices = [];
|
|
179
|
-
framework.onTrace((e) => {
|
|
180
|
-
const ev = e;
|
|
181
|
-
if (ev.type === 'message:added' && ev.source === 'prose-bounce')
|
|
182
|
-
bounceNotices.push(1);
|
|
183
|
-
});
|
|
184
|
-
trigger(framework);
|
|
185
|
-
await framework.runUntilIdle();
|
|
186
|
-
assert.equal(routed.length, 0, 'skip_reply text is never delivered');
|
|
187
|
-
assert.equal(bounceNotices.length, 0, 'skip_reply text never bounces');
|
|
188
|
-
await framework.stop();
|
|
189
|
-
});
|
|
190
|
-
/** The registry stub answers skip_reply as the real registry does: the turn ends, and nothing is sent. */
|
|
191
|
-
function answerSkipReply(framework) {
|
|
192
|
-
const answered = { calls: 0 };
|
|
193
|
-
const registry = framework.channelRegistry;
|
|
194
|
-
registry.handleChannelToolCall = async (name) => {
|
|
195
|
-
if (name === 'skip_reply')
|
|
196
|
-
answered.calls++;
|
|
197
|
-
return { success: true, endTurn: true, data: { skipped: true, note: 'Turn ended; nothing sent.' } };
|
|
198
|
-
};
|
|
199
|
-
return answered;
|
|
200
|
-
}
|
|
201
|
-
/** What the registry stub's streaming surface receives (MCPL Spec 14.3 chunk and complete). */
|
|
202
|
-
function captureStream(framework) {
|
|
203
|
-
const chunks = [];
|
|
204
|
-
const completes = [];
|
|
205
|
-
const registry = framework.channelRegistry;
|
|
206
|
-
registry.sendOutgoingChunk = (channelId, _agent, _id, _index, delta) => {
|
|
207
|
-
chunks.push({ channelId, delta });
|
|
208
|
-
};
|
|
209
|
-
registry.sendOutgoingComplete = (channelId, _agent, _id, text) => {
|
|
210
|
-
completes.push({ channelId, text });
|
|
211
|
-
};
|
|
212
|
-
return { chunks, completes };
|
|
213
|
-
}
|
|
214
|
-
/** The texts of the agent's context, and the bounce notices among them. */
|
|
215
|
-
function context(framework) {
|
|
216
|
-
const messages = framework.getAgent('assistant').getContextManager().getAllMessages();
|
|
217
|
-
return {
|
|
218
|
-
texts: messages.flatMap((m) => m.content.filter((b) => b.type === 'text').map((b) => b.text ?? '')),
|
|
219
|
-
bounces: messages.filter((m) => m.metadata?.kind === 'prose-bounce').length,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
const clipboardOf = (framework) => framework.proseClipboards.get('assistant');
|
|
223
|
-
it('a round that calls skip_reply keeps its unprefixed prose private: nothing delivered, bounced, retained or woken for', async () => {
|
|
224
|
-
membrane.pushResponse(createMockResponse([
|
|
225
|
-
{ type: 'text', text: 'Weighing whether this needs an answer from me.' },
|
|
226
|
-
{ type: 'tool_use', id: 'sr1', name: 'skip_reply', input: { reason: 'nothing to add' } },
|
|
227
|
-
], 'tool_use'));
|
|
228
|
-
const framework = await createFramework();
|
|
229
|
-
const routed = stubRegistry(framework);
|
|
230
|
-
const skip = answerSkipReply(framework);
|
|
231
|
-
// Were the text bounced, its wake would start a second turn: give that
|
|
232
|
-
// turn a response, so the test fails on its count instead of hanging.
|
|
233
|
-
feedTurn(framework, 2, [{ type: 'text', text: '>>skip_reply' }]);
|
|
234
|
-
let turns = 0;
|
|
235
|
-
framework.onTrace((e) => {
|
|
236
|
-
if (e.type === 'inference:started')
|
|
237
|
-
turns++;
|
|
238
|
-
});
|
|
239
|
-
trigger(framework);
|
|
240
|
-
await framework.runUntilIdle();
|
|
241
|
-
assert.equal(skip.calls, 1, 'the skip_reply round ran');
|
|
242
|
-
assert.equal(routed.length, 0, 'nothing is delivered');
|
|
243
|
-
const seen = context(framework);
|
|
244
|
-
assert.equal(seen.bounces, 0, 'no bounce notice reaches the context');
|
|
245
|
-
assert.ok(seen.texts.some((t) => t.includes('Weighing whether this needs an answer')), 'the text stays in the context');
|
|
246
|
-
assert.equal(clipboardOf(framework), undefined, 'nothing is retained for {{unsent}}');
|
|
247
|
-
assert.equal(turns, 1, 'no bounce wake starts another turn');
|
|
248
|
-
await framework.stop();
|
|
249
|
-
});
|
|
250
|
-
it('a round that calls skip_reply keeps its unprefixed prose from a sticky destination', async () => {
|
|
251
|
-
membrane.pushResponse(createMockResponse([
|
|
252
|
-
{ type: 'text', text: '>>#alpha first, deliberately' },
|
|
253
|
-
{ type: 'tool_use', id: 'c1', name: 'robot--move', input: { dir: 'north' } },
|
|
254
|
-
], 'tool_use'));
|
|
255
|
-
membrane.pushResponse(createMockResponse([
|
|
256
|
-
{ type: 'text', text: 'Second thoughts I keep to myself.' },
|
|
257
|
-
{ type: 'tool_use', id: 'sr2', name: 'skip_reply', input: {} },
|
|
258
|
-
], 'tool_use'));
|
|
259
|
-
const framework = await createFramework();
|
|
260
|
-
const routed = stubRegistry(framework);
|
|
261
|
-
const skip = answerSkipReply(framework);
|
|
262
|
-
trigger(framework);
|
|
263
|
-
await framework.runUntilIdle();
|
|
264
|
-
assert.equal(skip.calls, 1, 'the skip_reply round ran');
|
|
265
|
-
assert.deepEqual(routed, [{ text: 'first, deliberately', locus: 'chan-alpha' }], 'only the earlier, deliberately addressed segment goes out');
|
|
266
|
-
assert.ok(context(framework).texts.some((t) => t.includes('Second thoughts I keep to myself.')), 'the text stays in the context');
|
|
267
|
-
await framework.stop();
|
|
268
|
-
});
|
|
269
|
-
it('a round that calls skip_reply sends nothing, a prefixed segment included, and asks for no continuation', async () => {
|
|
270
|
-
membrane.pushResponse(createMockResponse([
|
|
271
|
-
{ type: 'text', text: '>>#alpha ! addressed, then thought better of' },
|
|
272
|
-
{ type: 'tool_use', id: 'sr3', name: 'skip_reply', input: {} },
|
|
273
|
-
], 'tool_use'));
|
|
274
|
-
const framework = await createFramework();
|
|
275
|
-
const routed = stubRegistry(framework);
|
|
276
|
-
const skip = answerSkipReply(framework);
|
|
277
|
-
// A continuation would start a second turn: give it a response, so the
|
|
278
|
-
// test fails on its count instead of hanging.
|
|
279
|
-
feedTurn(framework, 2, [{ type: 'text', text: '>>skip_reply' }]);
|
|
280
|
-
let turns = 0;
|
|
281
|
-
framework.onTrace((e) => {
|
|
282
|
-
if (e.type === 'inference:started')
|
|
283
|
-
turns++;
|
|
284
|
-
});
|
|
285
|
-
trigger(framework);
|
|
286
|
-
await framework.runUntilIdle();
|
|
287
|
-
assert.equal(skip.calls, 1, 'the skip_reply round ran');
|
|
288
|
-
assert.equal(routed.length, 0, 'skip_reply ends the turn without sending anything');
|
|
289
|
-
assert.equal(context(framework).bounces, 0);
|
|
290
|
-
assert.equal(turns, 1, "the round's ' !' starts no continuation after skip_reply ended the turn");
|
|
291
|
-
await framework.stop();
|
|
292
|
-
});
|
|
293
|
-
/** A turn's routing state, as the framework makes a fresh one. */
|
|
294
|
-
const routingState = (fields = {}) => ({
|
|
295
|
-
destination: null, hybridHold: false, continuation: false, deliveries: [], suppressed: 0,
|
|
296
|
-
engaged: new Set(), arrivals: [], ...fields,
|
|
297
|
-
});
|
|
298
|
-
async function within(promise, label, ms = 5000) {
|
|
299
|
-
let timer;
|
|
300
|
-
try {
|
|
301
|
-
return await Promise.race([promise, new Promise((_, reject) => {
|
|
302
|
-
timer = setTimeout(() => reject(new Error('Timed out waiting for ' + label)), ms);
|
|
303
|
-
})]);
|
|
304
|
-
}
|
|
305
|
-
finally {
|
|
306
|
-
clearTimeout(timer);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
function nextTrace(framework, type) {
|
|
310
|
-
let resolve;
|
|
311
|
-
const promise = new Promise((done) => { resolve = done; });
|
|
312
|
-
const off = framework.onTrace((event) => {
|
|
313
|
-
if (event.type === type && event.agentName === 'assistant') {
|
|
314
|
-
off();
|
|
315
|
-
resolve();
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
return { promise, dispose: off };
|
|
319
|
-
}
|
|
320
|
-
async function withQueuedTurns(mode, lateText, nextResponses, inspect) {
|
|
321
|
-
const arrows = mode === 'explicit' ? '>>' : '>>>';
|
|
322
|
-
membrane.pushResponse(createMockResponse([
|
|
323
|
-
{ type: 'text', text: arrows + '#alpha first' },
|
|
324
|
-
{ type: 'tool_use', id: 'q1', name: 'robot--move', input: { dir: 'north' } },
|
|
325
|
-
], 'tool_use'));
|
|
326
|
-
membrane.pushResponse(createMockResponse([
|
|
327
|
-
{ type: 'text', text: lateText },
|
|
328
|
-
{ type: 'tool_use', id: 'q2', name: 'robot--move', input: { dir: 'south' } },
|
|
329
|
-
], 'tool_use'));
|
|
330
|
-
membrane.pushResponse(createMockResponse([
|
|
331
|
-
{ type: 'tool_use', id: 'q3', name: 'skip_reply', input: {} },
|
|
332
|
-
], 'tool_use'));
|
|
333
|
-
const framework = await createFramework(mode);
|
|
334
|
-
const routed = stubRegistry(framework);
|
|
335
|
-
const skip = answerSkipReply(framework);
|
|
336
|
-
const registry = framework.channelRegistry;
|
|
337
|
-
const route = registry.routeSpeech;
|
|
338
|
-
let release;
|
|
339
|
-
const held = new Promise((resolve) => { release = resolve; });
|
|
340
|
-
registry.routeSpeech = async (agent, text, locus) => {
|
|
341
|
-
const outcome = await route(agent, text, locus);
|
|
342
|
-
if (text.trim() === 'first')
|
|
343
|
-
await held;
|
|
344
|
-
return outcome;
|
|
345
|
-
};
|
|
346
|
-
let turns = 0;
|
|
347
|
-
framework.onTrace((event) => {
|
|
348
|
-
if (event.type !== 'inference:started')
|
|
349
|
-
return;
|
|
350
|
-
turns++;
|
|
351
|
-
if (turns === 2)
|
|
352
|
-
for (const response of nextResponses)
|
|
353
|
-
membrane.pushResponse(response);
|
|
354
|
-
// Serve a bounce wake or an unexpected continuation without hanging.
|
|
355
|
-
if (turns === 3)
|
|
356
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: arrows + 'skip_reply' }]));
|
|
357
|
-
});
|
|
358
|
-
const internals = framework;
|
|
359
|
-
const processRequests = internals.processInferenceRequests.bind(framework);
|
|
360
|
-
let nextWakeQueued = false;
|
|
361
|
-
let resolveBlocked;
|
|
362
|
-
const blocked = new Promise((resolve) => { resolveBlocked = resolve; });
|
|
363
|
-
internals.processInferenceRequests = async () => {
|
|
364
|
-
await processRequests();
|
|
365
|
-
if (nextWakeQueued && turns === 1 && internals.pendingRequests.some((r) => r.agentName === 'assistant')) {
|
|
366
|
-
resolveBlocked();
|
|
367
|
-
}
|
|
368
|
-
};
|
|
369
|
-
const ended = nextTrace(framework, 'inference:turn_ended');
|
|
370
|
-
let completed;
|
|
371
|
-
let started;
|
|
372
|
-
let stopped = false;
|
|
373
|
-
let driving;
|
|
374
|
-
const drain = async () => {
|
|
375
|
-
stopped = true;
|
|
376
|
-
if (driving)
|
|
377
|
-
await within(driving, 'the queued turns to drain', 10000);
|
|
378
|
-
};
|
|
379
|
-
try {
|
|
380
|
-
trigger(framework);
|
|
381
|
-
driving = (async () => {
|
|
382
|
-
while (!stopped) {
|
|
383
|
-
await framework.runUntilIdle();
|
|
384
|
-
if (!stopped)
|
|
385
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
386
|
-
}
|
|
387
|
-
})();
|
|
388
|
-
void driving.catch(() => { });
|
|
389
|
-
await within(ended.promise, 'the first turn to end');
|
|
390
|
-
assert.equal(skip.calls, 1, 'skip_reply ended the first turn');
|
|
391
|
-
const earlier = internals.turnRouting.get('assistant');
|
|
392
|
-
completed = nextTrace(framework, 'inference:completed');
|
|
393
|
-
started = nextTrace(framework, 'inference:started');
|
|
394
|
-
nextWakeQueued = true;
|
|
395
|
-
trigger(framework);
|
|
396
|
-
if (mode === 'hybrid') {
|
|
397
|
-
const disposition = await within(Promise.race([
|
|
398
|
-
blocked.then(() => 'blocked'), started.promise.then(() => 'started'),
|
|
399
|
-
]), 'the next wake to reach scheduling');
|
|
400
|
-
assert.equal(disposition, 'blocked', 'hybrid retains the earlier turn token until its receipt tail drains');
|
|
401
|
-
assert.equal(internals.turnRouting.get('assistant'), earlier, 'the earlier turn still owns routing');
|
|
402
|
-
assert.deepEqual(routed.map((r) => r.text.trim()), ['first']);
|
|
403
|
-
release();
|
|
404
|
-
await within(completed.promise, 'the next inference after release');
|
|
405
|
-
assert.equal(turns, 2, 'hybrid starts the next turn after the earlier teardown');
|
|
406
|
-
}
|
|
407
|
-
else {
|
|
408
|
-
await within(completed.promise, 'the next inference to complete');
|
|
409
|
-
assert.equal(turns, 2, 'explicit starts the next turn without waiting');
|
|
410
|
-
assert.deepEqual(routed.map((r) => r.text.trim()), ['first'], "only the first post has entered routing while its confirmation is held");
|
|
411
|
-
}
|
|
412
|
-
await inspect({ framework, routed, earlier, next: internals.turnRouting.get('assistant'),
|
|
413
|
-
turns: () => turns, release, drain });
|
|
414
|
-
}
|
|
415
|
-
finally {
|
|
416
|
-
ended.dispose();
|
|
417
|
-
completed?.dispose();
|
|
418
|
-
started?.dispose();
|
|
419
|
-
internals.processInferenceRequests = processRequests;
|
|
420
|
-
release();
|
|
421
|
-
try {
|
|
422
|
-
await drain();
|
|
423
|
-
}
|
|
424
|
-
finally {
|
|
425
|
-
await framework.stop();
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
async function withHeldHybridCallback(lateText, inspect) {
|
|
430
|
-
const framework = await createFramework('hybrid');
|
|
431
|
-
const routed = stubRegistry(framework);
|
|
432
|
-
const gateway = framework;
|
|
433
|
-
const agent = framework.getAgent('assistant');
|
|
434
|
-
const earlier = routingState();
|
|
435
|
-
const next = routingState();
|
|
436
|
-
gateway.turnRouting.set('assistant', earlier);
|
|
437
|
-
const registry = framework.channelRegistry;
|
|
438
|
-
const route = registry.routeSpeech;
|
|
439
|
-
let release;
|
|
440
|
-
let resolveEntered;
|
|
441
|
-
const held = new Promise((resolve) => { release = resolve; });
|
|
442
|
-
const entered = new Promise((resolve) => { resolveEntered = resolve; });
|
|
443
|
-
registry.routeSpeech = async (name, text, locus) => {
|
|
444
|
-
const outcome = await route(name, text, locus);
|
|
445
|
-
resolveEntered();
|
|
446
|
-
await held;
|
|
447
|
-
return outcome;
|
|
448
|
-
};
|
|
449
|
-
const first = gateway.deliverHybridProse(agent, '>>>#alpha first', 'world:commons', true, earlier);
|
|
450
|
-
const pending = lateText === null ? first
|
|
451
|
-
: first.then(() => gateway.deliverHybridProse(agent, lateText, 'world:commons', true, earlier));
|
|
452
|
-
void pending.catch(() => { });
|
|
453
|
-
try {
|
|
454
|
-
await within(entered, 'the earlier callback to enter routing');
|
|
455
|
-
// Exercise callback ownership directly. Normal hybrid scheduling keeps
|
|
456
|
-
// its turn token until the receipt tail drains, so fresh turns serialize.
|
|
457
|
-
gateway.turnRouting.set('assistant', next);
|
|
458
|
-
await inspect({ framework, routed, gateway, earlier, next, agent, pending, release });
|
|
459
|
-
}
|
|
460
|
-
finally {
|
|
461
|
-
release();
|
|
462
|
-
try {
|
|
463
|
-
await within(pending, 'the held callback to drain');
|
|
464
|
-
}
|
|
465
|
-
finally {
|
|
466
|
-
await framework.stop();
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
for (const mode of ['explicit', 'hybrid']) {
|
|
471
|
-
const title = mode === 'explicit'
|
|
472
|
-
? "explicit: prose a turn leaves queued follows that turn's destination, and the next turn's posts wait behind it"
|
|
473
|
-
: "hybrid: the next turn waits for the ended turn's receipt tail, and posts keep their destinations and order";
|
|
474
|
-
it(title, async () => {
|
|
475
|
-
const arrows = mode === 'explicit' ? '>>' : '>>>';
|
|
476
|
-
await withQueuedTurns(mode, 'and then this', [
|
|
477
|
-
createMockResponse([{ type: 'text', text: arrows + '#cafe next' }]),
|
|
478
|
-
], async ({ framework, routed, release, drain }) => {
|
|
479
|
-
release();
|
|
480
|
-
await drain();
|
|
481
|
-
assert.deepEqual(routed.map((r) => ({ text: r.text.trim(), locus: r.locus })), [
|
|
482
|
-
{ text: 'first', locus: 'chan-alpha' },
|
|
483
|
-
{ text: 'and then this', locus: 'chan-alpha' },
|
|
484
|
-
{ text: 'next', locus: 'discord:g:cafe' },
|
|
485
|
-
], "each post follows its own turn's destination, in delivery order");
|
|
486
|
-
assert.equal(context(framework).bounces, 0, 'nothing bounces into the next turn');
|
|
487
|
-
});
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
it("a queued round's late continuation starts once its tool-ended turn's deliveries have run", async () => {
|
|
491
|
-
await withQueuedTurns('explicit', '>>#alpha ! late', [
|
|
492
|
-
createMockResponse([{ type: 'text', text: '>>#cafe next' }]),
|
|
493
|
-
], async ({ earlier, next, turns, release, drain }) => {
|
|
494
|
-
assert.equal(earlier.continuation, false, 'the continuation has not run before release');
|
|
495
|
-
release();
|
|
496
|
-
for (const end = Date.now() + 5000; turns() < 3 && Date.now() < end;) {
|
|
497
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
498
|
-
}
|
|
499
|
-
await drain();
|
|
500
|
-
assert.equal(turns(), 3, 'the continuation starts its own turn, once');
|
|
501
|
-
assert.equal(earlier.continuation, false, "the earlier turn's request was taken from its own object");
|
|
502
|
-
assert.equal(next.continuation, false, 'the next turn never receives that continuation');
|
|
503
|
-
});
|
|
504
|
-
});
|
|
505
|
-
for (const mode of ['explicit', 'hybrid']) {
|
|
506
|
-
it(`${mode}: a ' !' an earlier round asked for starts its continuation when skip_reply ends the turn`, async () => {
|
|
507
|
-
const arrows = mode === 'explicit' ? '>>' : '>>>';
|
|
508
|
-
membrane.pushResponse(createMockResponse([
|
|
509
|
-
{ type: 'text', text: arrows + '#alpha ! keep going' },
|
|
510
|
-
{ type: 'tool_use', id: 'k1', name: 'robot--move', input: { dir: 'north' } },
|
|
511
|
-
], 'tool_use'));
|
|
512
|
-
membrane.pushResponse(createMockResponse([
|
|
513
|
-
{ type: 'tool_use', id: 'k2', name: 'skip_reply', input: {} },
|
|
514
|
-
], 'tool_use'));
|
|
515
|
-
const framework = await createFramework(mode);
|
|
516
|
-
const routed = stubRegistry(framework);
|
|
517
|
-
const skip = answerSkipReply(framework);
|
|
518
|
-
let turns = 0;
|
|
519
|
-
framework.onTrace((e) => {
|
|
520
|
-
if (e.type === 'inference:started')
|
|
521
|
-
turns++;
|
|
522
|
-
});
|
|
523
|
-
feedTurn(framework, 2, [{ type: 'text', text: arrows + 'skip_reply' }]);
|
|
524
|
-
const reasons = [];
|
|
525
|
-
const internals = framework;
|
|
526
|
-
const process = internals.processInferenceRequests.bind(framework);
|
|
527
|
-
internals.processInferenceRequests = async () => {
|
|
528
|
-
for (const request of internals.pendingRequests)
|
|
529
|
-
reasons.push(request.reason);
|
|
530
|
-
await process();
|
|
531
|
-
};
|
|
532
|
-
trigger(framework);
|
|
533
|
-
await framework.runUntilIdle();
|
|
534
|
-
for (const end = Date.now() + 5000; turns < 2 && Date.now() < end;) {
|
|
535
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
536
|
-
await framework.runUntilIdle();
|
|
537
|
-
}
|
|
538
|
-
assert.equal(skip.calls, 1, 'skip_reply ended the first turn');
|
|
539
|
-
assert.deepEqual(routed.map((r) => r.text.trim()), ['keep going']);
|
|
540
|
-
assert.ok(reasons.includes('prose-continuation'), 'the continuation was requested');
|
|
541
|
-
assert.equal(turns, 2, 'and it started one more turn');
|
|
542
|
-
await framework.stop();
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
for (const mode of ['explicit', 'hybrid']) {
|
|
546
|
-
it(`${mode}: a tool-ended turn's ' !' queued behind a held post starts its continuation once that post settles`, async () => {
|
|
547
|
-
const arrows = mode === 'explicit' ? '>>' : '>>>';
|
|
548
|
-
membrane.pushResponse(createMockResponse([
|
|
549
|
-
{ type: 'text', text: `${arrows}#alpha first\n${arrows}#alpha ! second` },
|
|
550
|
-
{ type: 'tool_use', id: 'w1', name: 'robot--move', input: { dir: 'north' } },
|
|
551
|
-
], 'tool_use'));
|
|
552
|
-
membrane.pushResponse(createMockResponse([
|
|
553
|
-
{ type: 'tool_use', id: 'w2', name: 'skip_reply', input: {} },
|
|
554
|
-
], 'tool_use'));
|
|
555
|
-
const framework = await createFramework(mode);
|
|
556
|
-
const routed = stubRegistry(framework);
|
|
557
|
-
answerSkipReply(framework);
|
|
558
|
-
let turns = 0;
|
|
559
|
-
framework.onTrace((e) => {
|
|
560
|
-
if (e.type === 'inference:started')
|
|
561
|
-
turns++;
|
|
562
|
-
});
|
|
563
|
-
feedTurn(framework, 2, [{ type: 'text', text: arrows + 'skip_reply' }]);
|
|
564
|
-
const registry = framework.channelRegistry;
|
|
565
|
-
const route = registry.routeSpeech;
|
|
566
|
-
let release;
|
|
567
|
-
const held = new Promise((resolve) => { release = resolve; });
|
|
568
|
-
let entered;
|
|
569
|
-
const reached = new Promise((resolve) => { entered = resolve; });
|
|
570
|
-
registry.routeSpeech = async (agent, text, locus) => {
|
|
571
|
-
const outcome = await route(agent, text, locus);
|
|
572
|
-
if (text.trim() === 'first') {
|
|
573
|
-
entered();
|
|
574
|
-
await held;
|
|
575
|
-
}
|
|
576
|
-
return outcome;
|
|
577
|
-
};
|
|
578
|
-
const internals = framework;
|
|
579
|
-
const ended = nextTrace(framework, 'inference:turn_ended');
|
|
580
|
-
trigger(framework);
|
|
581
|
-
const running = framework.runUntilIdle();
|
|
582
|
-
try {
|
|
583
|
-
await within(reached, "the first envelope's post to start");
|
|
584
|
-
await within(ended.promise, 'skip_reply to end the turn');
|
|
585
|
-
assert.equal(turns, 1);
|
|
586
|
-
assert.ok(!internals.pendingRequests.some((r) => r.reason === 'prose-continuation'), "no continuation while the ' !' envelope waits behind the held post");
|
|
587
|
-
release();
|
|
588
|
-
await within(running, 'the turn to settle');
|
|
589
|
-
for (const end = Date.now() + 5000; turns < 2 && Date.now() < end;) {
|
|
590
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
591
|
-
await framework.runUntilIdle();
|
|
592
|
-
}
|
|
593
|
-
assert.deepEqual(routed.map((r) => r.text.trim()), ['first', 'second']);
|
|
594
|
-
assert.equal(turns, 2, 'the continuation starts once the queued envelope has run');
|
|
595
|
-
}
|
|
596
|
-
finally {
|
|
597
|
-
ended.dispose();
|
|
598
|
-
release();
|
|
599
|
-
await framework.stop();
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
for (const when of ['already set', 'set late']) {
|
|
604
|
-
it(`hybrid: a ' !' an earlier stream asked for (${when}) still starts its continuation after a re-inference on unstreamed tool results`, async () => {
|
|
605
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'carrying on' }]));
|
|
606
|
-
const framework = await createFramework('hybrid');
|
|
607
|
-
const routed = stubRegistry(framework);
|
|
608
|
-
let turns = 0;
|
|
609
|
-
framework.onTrace((e) => {
|
|
610
|
-
if (e.type === 'inference:started')
|
|
611
|
-
turns++;
|
|
612
|
-
});
|
|
613
|
-
feedTurn(framework, 2, [{ type: 'text', text: '>>>skip_reply' }]);
|
|
614
|
-
const internals = framework;
|
|
615
|
-
let release;
|
|
616
|
-
const held = new Promise((resolve) => { release = resolve; });
|
|
617
|
-
try {
|
|
618
|
-
// The turn's earlier stream asked for a continuation, or will once
|
|
619
|
-
// its queued delivery runs; the turn goes on in a re-inference, which
|
|
620
|
-
// starts a new stream with a fresh routing state while the earlier
|
|
621
|
-
// stream's logical turn is still open.
|
|
622
|
-
const earlier = routingState({ continuation: when === 'already set', locus: 'world:commons' });
|
|
623
|
-
internals.turnRouting.set('assistant', earlier);
|
|
624
|
-
internals.openLogical.set('assistant', earlier);
|
|
625
|
-
if (when === 'set late') {
|
|
626
|
-
internals.proseSpeechChains.set('assistant', held.then(() => { earlier.continuation = true; }));
|
|
627
|
-
}
|
|
628
|
-
internals.pendingRequests.push({ agentName: 'assistant', reason: 'tool_results_ready', source: 'framework', timestamp: Date.now() });
|
|
629
|
-
const running = framework.runUntilIdle();
|
|
630
|
-
if (when === 'set late') {
|
|
631
|
-
for (const end = Date.now() + 5000; turns < 1 && Date.now() < end;) {
|
|
632
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
633
|
-
}
|
|
634
|
-
assert.equal(turns, 1, 'the re-inference has started');
|
|
635
|
-
assert.equal(earlier.continuation, false, 'the earlier delivery has not run as the re-inference starts');
|
|
636
|
-
release();
|
|
637
|
-
}
|
|
638
|
-
await within(running, 'the re-inference to settle');
|
|
639
|
-
for (const end = Date.now() + 5000; turns < 2 && Date.now() < end;) {
|
|
640
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
641
|
-
await framework.runUntilIdle();
|
|
642
|
-
}
|
|
643
|
-
assert.deepEqual(routed.map((r) => r.text.trim()), ['carrying on']);
|
|
644
|
-
assert.equal(turns, 2, 'the re-inference completes, then the continuation it carried starts');
|
|
645
|
-
assert.equal(earlier.continuation, false, "the check took the earlier stream's flag");
|
|
646
|
-
}
|
|
647
|
-
finally {
|
|
648
|
-
release();
|
|
649
|
-
await framework.stop();
|
|
650
|
-
}
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
it("hybrid: a re-inference on unstreamed tool results receipts the earlier stream's late delivery with its own", async () => {
|
|
654
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'carrying on' }]));
|
|
655
|
-
const framework = await createFramework('hybrid');
|
|
656
|
-
const routed = stubRegistry(framework);
|
|
657
|
-
let turns = 0;
|
|
658
|
-
framework.onTrace((e) => {
|
|
659
|
-
if (e.type === 'inference:started')
|
|
660
|
-
turns++;
|
|
661
|
-
});
|
|
662
|
-
const internals = framework;
|
|
663
|
-
let release;
|
|
664
|
-
const held = new Promise((resolve) => { release = resolve; });
|
|
665
|
-
try {
|
|
666
|
-
// The earlier stream left its tool results pending, its logical turn
|
|
667
|
-
// still open; its delivery lands only after the re-inference has
|
|
668
|
-
// begun, into the logical turn the two share. The turn froze the
|
|
669
|
-
// registry's locus when it opened.
|
|
670
|
-
const earlier = routingState({ locus: 'world:commons' });
|
|
671
|
-
internals.turnRouting.set('assistant', earlier);
|
|
672
|
-
internals.openLogical.set('assistant', earlier);
|
|
673
|
-
internals.proseSpeechChains.set('assistant', held.then(() => { earlier.deliveries.push('chan-alpha'); }));
|
|
674
|
-
internals.pendingRequests.push({ agentName: 'assistant', reason: 'tool_results_ready', source: 'framework', timestamp: Date.now() });
|
|
675
|
-
const running = framework.runUntilIdle();
|
|
676
|
-
for (const end = Date.now() + 5000; turns < 1 && Date.now() < end;) {
|
|
677
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
678
|
-
}
|
|
679
|
-
assert.equal(turns, 1, 'the re-inference has started');
|
|
680
|
-
assert.deepEqual(earlier.deliveries, [], 'the earlier delivery has not landed yet');
|
|
681
|
-
release();
|
|
682
|
-
await within(running, 'the re-inference to settle');
|
|
683
|
-
assert.deepEqual(routed.map((r) => r.text.trim()), ['carrying on']);
|
|
684
|
-
const receipt = context(framework).texts.filter((t) => t.startsWith('[delivered]'));
|
|
685
|
-
assert.equal(receipt.length, 1, `one receipt for the logical turn: ${receipt.join(' | ')}`);
|
|
686
|
-
assert.match(receipt[0], /chan-alpha/, "it reports the earlier stream's late delivery");
|
|
687
|
-
assert.match(receipt[0], /world:commons/, "and the re-inference's own");
|
|
688
|
-
}
|
|
689
|
-
finally {
|
|
690
|
-
release();
|
|
691
|
-
await framework.stop();
|
|
692
|
-
}
|
|
693
|
-
});
|
|
694
|
-
for (const mode of ['explicit', 'hybrid']) {
|
|
695
|
-
const p = mode === 'explicit' ? '>>' : '>>>';
|
|
696
|
-
it(`${mode}: an error retry keeps a ' !' whose envelope is delivered after the retry starts`, async () => {
|
|
697
|
-
membrane.pushResponse(createMockResponse([
|
|
698
|
-
{ type: 'text', text: `${p}#alpha first\n${p}#alpha ! second` },
|
|
699
|
-
{ type: 'tool_use', id: 'retry-held', name: 'robot--move', input: { dir: 'north' } },
|
|
700
|
-
], 'tool_use'));
|
|
701
|
-
membrane.pushResponse(createMockResponse([]));
|
|
702
|
-
const framework = await createFramework(mode);
|
|
703
|
-
const routed = stubRegistry(framework);
|
|
704
|
-
const internals = framework;
|
|
705
|
-
let release;
|
|
706
|
-
const held = new Promise((resolve) => { release = resolve; });
|
|
707
|
-
let reached;
|
|
708
|
-
const entered = new Promise((resolve) => { reached = resolve; });
|
|
709
|
-
const route = internals.channelRegistry.routeSpeech;
|
|
710
|
-
internals.channelRegistry.routeSpeech = async (...args) => {
|
|
711
|
-
const result = await route(...args);
|
|
712
|
-
if (String(args[1]).trim() === 'first') {
|
|
713
|
-
reached();
|
|
714
|
-
await held;
|
|
715
|
-
}
|
|
716
|
-
return result;
|
|
717
|
-
};
|
|
718
|
-
let firstChain;
|
|
719
|
-
let restarted;
|
|
720
|
-
const retryStarted = new Promise((resolve) => { restarted = resolve; });
|
|
721
|
-
let starts = 0;
|
|
722
|
-
framework.onTrace((event) => {
|
|
723
|
-
if (event.type === 'inference:failed')
|
|
724
|
-
firstChain = internals.proseSpeechChains.get('assistant');
|
|
725
|
-
if (event.type === 'inference:started') {
|
|
726
|
-
starts++;
|
|
727
|
-
if (starts === 2) {
|
|
728
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: `${p}skip_reply retry done` }]));
|
|
729
|
-
restarted();
|
|
730
|
-
}
|
|
731
|
-
if (starts === 3)
|
|
732
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: `${p}skip_reply continuation done` }]));
|
|
733
|
-
}
|
|
734
|
-
});
|
|
735
|
-
// The first stream ends in a retryable error instead of completing; the
|
|
736
|
-
// retry's own end waits for the first stream's queued deliveries, so the
|
|
737
|
-
// held ' !' envelope runs after the retry has started.
|
|
738
|
-
const makeStream = membrane.streamYielding.bind(membrane);
|
|
739
|
-
let streams = 0;
|
|
740
|
-
membrane.streamYielding = (request, options) => {
|
|
741
|
-
const stream = makeStream(request, options);
|
|
742
|
-
const index = ++streams;
|
|
743
|
-
const iterate = stream[Symbol.asyncIterator].bind(stream);
|
|
744
|
-
stream[Symbol.asyncIterator] = async function* () {
|
|
745
|
-
for await (const event of { [Symbol.asyncIterator]: iterate }) {
|
|
746
|
-
if (event.type === 'complete' && index === 1) {
|
|
747
|
-
yield { type: 'error', error: new Error('synthetic transient stream error') };
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
if (event.type === 'complete' && index === 2) {
|
|
751
|
-
assert.ok(firstChain, 'the first stream queued a delivery chain');
|
|
752
|
-
await firstChain;
|
|
753
|
-
}
|
|
754
|
-
yield event;
|
|
755
|
-
}
|
|
756
|
-
};
|
|
757
|
-
return stream;
|
|
758
|
-
};
|
|
759
|
-
let running;
|
|
760
|
-
try {
|
|
761
|
-
trigger(framework);
|
|
762
|
-
running = framework.runUntilIdle();
|
|
763
|
-
void running.catch(() => { });
|
|
764
|
-
await within(entered, 'the held first post');
|
|
765
|
-
await within(retryStarted, 'the error retry to start', 10000);
|
|
766
|
-
assert.equal(starts, 2);
|
|
767
|
-
release();
|
|
768
|
-
await within(running, 'the retry and continuation to finish', 10000);
|
|
769
|
-
assert.deepEqual(routed.map((r) => r.text.trim()), ['first', 'second']);
|
|
770
|
-
assert.equal(membrane.calls.length, 3, "the late ' !' survives the error retry and starts a continuation");
|
|
771
|
-
}
|
|
772
|
-
finally {
|
|
773
|
-
release();
|
|
774
|
-
if (running)
|
|
775
|
-
await running;
|
|
776
|
-
await framework.stop();
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
it("the non-streaming fallback leaves the scheduling turn's logical turn open for the re-inference", async () => {
|
|
781
|
-
// Adapted from a reviewer's producer test: the agent's direct inference
|
|
782
|
-
// API leaves tools pending with no yielding stream, and the tool result
|
|
783
|
-
// goes through the framework's real event handling.
|
|
784
|
-
const framework = await createFramework('hybrid');
|
|
785
|
-
stubRegistry(framework);
|
|
786
|
-
const agent = framework.getAgent('assistant');
|
|
787
|
-
const internals = framework;
|
|
788
|
-
const origin = internals.routingOf('assistant');
|
|
789
|
-
origin.continuation = true;
|
|
790
|
-
origin.deliveries.push('chan-alpha');
|
|
791
|
-
// The turn that ran the tools is open, as its fresh start left it.
|
|
792
|
-
internals.openLogical.set('assistant', origin);
|
|
793
|
-
const response = createMockResponse([
|
|
794
|
-
{ type: 'tool_use', id: 'unstreamed-one', name: 'robot--move', input: { dir: 'north' } },
|
|
795
|
-
], 'tool_use');
|
|
796
|
-
membrane.stream = async () => response;
|
|
797
|
-
try {
|
|
798
|
-
agent.getContextManager().addMessage('user', [{ type: 'text', text: 'Take the direct inference path.' }]);
|
|
799
|
-
await agent.runInference(framework.getAllTools());
|
|
800
|
-
assert.equal(agent.state.status, 'waiting_for_tools');
|
|
801
|
-
assert.equal(agent.state.stream, undefined, 'the direct API has no yielding stream');
|
|
802
|
-
agent.addAssistantResponse(response.content);
|
|
803
|
-
await internals.handleProcessEvent({
|
|
804
|
-
type: 'tool-result', source: 'robot', moduleName: 'robot', agentName: 'assistant', callId: 'unstreamed-one',
|
|
805
|
-
result: { success: true, data: { ok: true } },
|
|
806
|
-
});
|
|
807
|
-
assert.ok(internals.pendingRequests.some((r) => r.reason === 'tool_results_ready'), 'the fallback scheduled the re-inference');
|
|
808
|
-
assert.equal(internals.openLogical.get('assistant'), origin, 'without settling the turn it continues');
|
|
809
|
-
}
|
|
810
|
-
finally {
|
|
811
|
-
await framework.stop();
|
|
812
|
-
}
|
|
813
|
-
});
|
|
814
|
-
it("a settle closes the open logical turn only when it is the settling stream's own, and stop() clears open turns", async () => {
|
|
815
|
-
const framework = await createFramework('hybrid');
|
|
816
|
-
const internals = framework;
|
|
817
|
-
const open = routingState({ continuation: true });
|
|
818
|
-
const stale = routingState();
|
|
819
|
-
internals.openLogical.set('assistant', open);
|
|
820
|
-
// A settle from a stream a newer one replaced passes its own logical
|
|
821
|
-
// turn, or none; neither closes the open one.
|
|
822
|
-
internals.settleAgent('assistant', { stopReason: 'completed', speech: '' }, stale);
|
|
823
|
-
internals.settleAgent('assistant', { stopReason: 'completed', speech: '' }, undefined);
|
|
824
|
-
assert.equal(internals.openLogical.get('assistant'), open, "a stale settle leaves the newer turn open");
|
|
825
|
-
internals.settleAgent('assistant', { stopReason: 'completed', speech: '' }, open);
|
|
826
|
-
assert.equal(internals.openLogical.has('assistant'), false, "the turn's own settle closes it");
|
|
827
|
-
internals.openLogical.set('assistant', open);
|
|
828
|
-
internals.turnRouting.set('assistant', open);
|
|
829
|
-
await framework.stop();
|
|
830
|
-
assert.equal(internals.openLogical.size, 0, 'stop() clears every open logical turn');
|
|
831
|
-
assert.equal(internals.turnRouting.get('assistant'), open, 'and keeps the live routing state, for a resume that continues its turn');
|
|
832
|
-
});
|
|
833
|
-
it('a silent start neither takes nor disturbs an open logical turn, and leaves it to the next start that routes prose', async () => {
|
|
834
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'quiet' }]));
|
|
835
|
-
const framework = await createFramework('hybrid');
|
|
836
|
-
stubRegistry(framework);
|
|
837
|
-
let turns = 0;
|
|
838
|
-
framework.onTrace((e) => {
|
|
839
|
-
if (e.type === 'inference:started')
|
|
840
|
-
turns++;
|
|
841
|
-
});
|
|
842
|
-
const internals = framework;
|
|
843
|
-
try {
|
|
844
|
-
// Everything the open turn has decided about where its speech goes.
|
|
845
|
-
const decided = () => routingState({
|
|
846
|
-
continuation: true, pin: 'chan-alpha', hybridHold: true, triggerChannel: 'chan-alpha',
|
|
847
|
-
locus: 'world:commons', engaged: new Set(['discord:g:cafe']), arrivals: [{ messageId: 'cafe-message', channelId: 'discord:g:cafe' }],
|
|
848
|
-
});
|
|
849
|
-
const open = decided();
|
|
850
|
-
internals.openLogical.set('assistant', open);
|
|
851
|
-
internals.pendingRequests.push({ agentName: 'assistant', reason: 'heartbeat', source: 'framework', timestamp: Date.now(), suppressProse: true });
|
|
852
|
-
// Bounded: a silent start that took the open turn's ' !' would start a
|
|
853
|
-
// continuation with no response queued, and this would wait for good.
|
|
854
|
-
await within(framework.runUntilIdle(), 'the silent turn to settle', 3000);
|
|
855
|
-
assert.equal(turns, 1);
|
|
856
|
-
const silent = internals.turnRouting.get('assistant');
|
|
857
|
-
assert.notEqual(silent, open, 'the silent start kept a logical turn of its own');
|
|
858
|
-
assert.equal(silent?.locus, undefined, 'with no locus');
|
|
859
|
-
assert.equal(internals.openLogical.get('assistant'), open, 'and neither took nor closed the open one');
|
|
860
|
-
assert.deepEqual(open, decided(), "everything the open turn decided is untouched, its ' !' included");
|
|
861
|
-
// The next start that routes prose continues it, and its ' !' runs.
|
|
862
|
-
// (feedTurn counts starts from here: the prose turn is 1, the
|
|
863
|
-
// continuation 2.)
|
|
864
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: '>>>skip_reply' }]));
|
|
865
|
-
feedTurn(framework, 2, [{ type: 'text', text: '>>>skip_reply' }]);
|
|
866
|
-
trigger(framework);
|
|
867
|
-
await framework.runUntilIdle();
|
|
868
|
-
for (const end = Date.now() + 5000; turns < 3 && Date.now() < end;) {
|
|
869
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
870
|
-
await framework.runUntilIdle();
|
|
871
|
-
}
|
|
872
|
-
assert.equal(turns, 3, "the next prose turn continued the open turn and took its ' !'");
|
|
873
|
-
}
|
|
874
|
-
finally {
|
|
875
|
-
await framework.stop();
|
|
876
|
-
}
|
|
877
|
-
});
|
|
878
|
-
it("a silent turn's resume shows no locus, whatever a channel_open in that turn left on its routing state", async () => {
|
|
879
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'quiet' }]));
|
|
880
|
-
const framework = await createFramework('hybrid');
|
|
881
|
-
stubRegistry(framework);
|
|
882
|
-
const registry = framework.channelRegistry;
|
|
883
|
-
const typing = [];
|
|
884
|
-
registry.startTyping = (channelId) => { typing.push(channelId); };
|
|
885
|
-
const started = [];
|
|
886
|
-
framework.onTrace((event) => {
|
|
887
|
-
if (event.type === 'inference:started')
|
|
888
|
-
started.push(event.channelId);
|
|
889
|
-
});
|
|
890
|
-
const internals = framework;
|
|
891
|
-
try {
|
|
892
|
-
// A silent turn opened a channel, which moved its routing state's
|
|
893
|
-
// locus, and its stream then crossed the budget.
|
|
894
|
-
internals.turnRouting.set('assistant', routingState({ locus: 'discord:g:cafe' }));
|
|
895
|
-
internals.pendingRequests.push({ agentName: 'assistant', reason: 'context_budget_restart', source: 'framework', timestamp: Date.now(), suppressProse: true });
|
|
896
|
-
await within(framework.runUntilIdle(), 'the silent restart', 3000);
|
|
897
|
-
assert.deepEqual(started, [undefined], 'the restart reports no locus');
|
|
898
|
-
assert.deepEqual(typing, [], 'and shows no typing anywhere');
|
|
899
|
-
}
|
|
900
|
-
finally {
|
|
901
|
-
await framework.stop();
|
|
902
|
-
}
|
|
903
|
-
});
|
|
904
|
-
for (const mode of ['explicit', 'hybrid']) {
|
|
905
|
-
it(`${mode}: the wake that consumes unstreamed tool results continues the turn that scheduled them`, async () => {
|
|
906
|
-
const p = mode === 'explicit' ? '>>' : '>>>';
|
|
907
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: `${p}skip_reply carrying on` }]));
|
|
908
|
-
const framework = await createFramework(mode);
|
|
909
|
-
stubRegistry(framework);
|
|
910
|
-
let turns = 0;
|
|
911
|
-
framework.onTrace((e) => {
|
|
912
|
-
if (e.type === 'inference:started')
|
|
913
|
-
turns++;
|
|
914
|
-
});
|
|
915
|
-
feedTurn(framework, 2, [{ type: 'text', text: `${p}skip_reply` }]);
|
|
916
|
-
const internals = framework;
|
|
917
|
-
try {
|
|
918
|
-
// The scheduling turn asked for a continuation and left its tool
|
|
919
|
-
// results pending; the scheduler then starts the agent with another
|
|
920
|
-
// wake, which consumes them.
|
|
921
|
-
const scheduling = routingState({ continuation: true });
|
|
922
|
-
internals.openLogical.set('assistant', scheduling);
|
|
923
|
-
internals.turnRouting.set('assistant', routingState());
|
|
924
|
-
trigger(framework);
|
|
925
|
-
await framework.runUntilIdle();
|
|
926
|
-
for (const end = Date.now() + 5000; turns < 2 && Date.now() < end;) {
|
|
927
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
928
|
-
await framework.runUntilIdle();
|
|
929
|
-
}
|
|
930
|
-
assert.equal(turns, 2, "the consuming turn took the scheduling turn's ' !'");
|
|
931
|
-
assert.equal(scheduling.continuation, false);
|
|
932
|
-
assert.equal(internals.openLogical.has('assistant'), false, 'and the turns it ran have settled');
|
|
933
|
-
}
|
|
934
|
-
finally {
|
|
935
|
-
await framework.stop();
|
|
936
|
-
}
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
for (const boundary of ['stop', 'dispose']) {
|
|
940
|
-
it(`a tool-ended explicit turn's late ' !' takes no new turn after ${boundary === 'stop' ? 'stop()' : 'its agent is disposed'}`, async () => {
|
|
941
|
-
membrane.pushResponse(createMockResponse([
|
|
942
|
-
{ type: 'text', text: '>>#alpha ! first' },
|
|
943
|
-
{ type: 'tool_use', id: 'p1', name: 'robot--move', input: { dir: 'north' } },
|
|
944
|
-
], 'tool_use'));
|
|
945
|
-
membrane.pushResponse(createMockResponse([
|
|
946
|
-
{ type: 'tool_use', id: 'p2', name: 'skip_reply', input: {} },
|
|
947
|
-
], 'tool_use'));
|
|
948
|
-
const framework = await createFramework();
|
|
949
|
-
stubRegistry(framework);
|
|
950
|
-
answerSkipReply(framework);
|
|
951
|
-
const internals = framework;
|
|
952
|
-
let release;
|
|
953
|
-
const held = new Promise((resolve) => { release = resolve; });
|
|
954
|
-
let entered;
|
|
955
|
-
const reached = new Promise((resolve) => { entered = resolve; });
|
|
956
|
-
internals.channelRegistry.routeSpeech = async () => {
|
|
957
|
-
entered();
|
|
958
|
-
await held;
|
|
959
|
-
return { delivered: true, channelId: 'chan-alpha' };
|
|
960
|
-
};
|
|
961
|
-
try {
|
|
962
|
-
trigger(framework);
|
|
963
|
-
await within(framework.runUntilIdle(), 'the tool-ended turn to settle');
|
|
964
|
-
await within(reached, 'the queued delivery to reach routing');
|
|
965
|
-
const chain = internals.proseSpeechChains.get('assistant');
|
|
966
|
-
const routing = internals.turnRouting.get('assistant');
|
|
967
|
-
assert.equal(routing.continuation, true, "the held delivery's ' !' is on its turn's object");
|
|
968
|
-
assert.equal(internals.pendingRequests.length, 0);
|
|
969
|
-
if (boundary === 'stop')
|
|
970
|
-
await framework.stop();
|
|
971
|
-
else
|
|
972
|
-
internals.disposeConversationAgent('assistant');
|
|
973
|
-
release();
|
|
974
|
-
await within(chain, 'the queued delivery');
|
|
975
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
976
|
-
assert.equal(internals.pendingRequests.filter((r) => r.reason === 'prose-continuation').length, 0, 'the late check adds no request');
|
|
977
|
-
assert.equal(routing.continuation, false, 'it took the flag and dropped it');
|
|
978
|
-
}
|
|
979
|
-
finally {
|
|
980
|
-
release();
|
|
981
|
-
await framework.stop();
|
|
982
|
-
}
|
|
983
|
-
});
|
|
984
|
-
}
|
|
985
|
-
it("hybrid: a held callback's late malformed envelope holds only its supplied turn", async () => {
|
|
986
|
-
await withHeldHybridCallback('>>>', async ({ earlier, next, routed, gateway, agent, pending, release }) => {
|
|
987
|
-
assert.equal(earlier.hybridHold, false, 'the malformed envelope has not run before release');
|
|
988
|
-
release();
|
|
989
|
-
await within(pending, 'the late malformed envelope');
|
|
990
|
-
assert.equal(earlier.hybridHold, true, 'the late hold belongs to the supplied earlier turn');
|
|
991
|
-
assert.equal(next.hybridHold, false, 'the newer object stays unheld');
|
|
992
|
-
await gateway.deliverHybridProse(agent, 'next', 'world:commons', true, next);
|
|
993
|
-
assert.deepEqual(routed.map((r) => ({ text: r.text.trim(), locus: r.locus })), [
|
|
994
|
-
{ text: 'first', locus: 'chan-alpha' },
|
|
995
|
-
{ text: 'next', locus: 'world:commons' },
|
|
996
|
-
], 'the newer callback still publishes to its own locus');
|
|
997
|
-
});
|
|
998
|
-
});
|
|
999
|
-
it("hybrid callbacks and receipts keep each turn's counts while a post is held", async () => {
|
|
1000
|
-
await withHeldHybridCallback(null, async ({ framework, earlier, next, gateway, agent, pending, release }) => {
|
|
1001
|
-
await gateway.deliverHybridProse(agent, 'quiet without a destination', 'world:commons', false, next);
|
|
1002
|
-
release();
|
|
1003
|
-
await within(pending, 'the earlier delivery confirmation');
|
|
1004
|
-
gateway.appendProseDeliveryReceipt(agent, earlier);
|
|
1005
|
-
gateway.appendProseDeliveryReceipt(agent, next);
|
|
1006
|
-
const receipts = context(framework).texts.filter((text) => text.startsWith('[delivered]'));
|
|
1007
|
-
assert.equal(receipts.length, 2, 'each supplied turn gets its own receipt');
|
|
1008
|
-
const delivered = receipts.find((text) => text.includes('chan-alpha'));
|
|
1009
|
-
const suppressed = receipts.find((text) => text.includes('suppressed'));
|
|
1010
|
-
assert.ok(delivered, "the earlier turn's delivery is acknowledged");
|
|
1011
|
-
assert.doesNotMatch(delivered, /suppressed/, "the earlier receipt excludes the newer suppression");
|
|
1012
|
-
assert.ok(suppressed, "the newer turn's suppression is acknowledged");
|
|
1013
|
-
assert.match(suppressed, /1 plain-speech segment.*suppressed/);
|
|
1014
|
-
assert.doesNotMatch(suppressed, /chan-alpha/, "the newer receipt excludes the earlier delivery");
|
|
1015
|
-
});
|
|
1016
|
-
});
|
|
1017
|
-
it("a private round's ' !' leaves no continuation request behind", async () => {
|
|
1018
|
-
const framework = await createFramework();
|
|
1019
|
-
const routed = stubRegistry(framework);
|
|
1020
|
-
const internals = framework;
|
|
1021
|
-
const agent = framework.getAgent('assistant');
|
|
1022
|
-
// When skip_reply ends an explicit turn, the speech chain isn't awaited,
|
|
1023
|
-
// so the round's delivery can run once the next turn has begun.
|
|
1024
|
-
await internals.deliverProse(agent, '>>#alpha ! kept private', { keepPrivate: true });
|
|
1025
|
-
assert.equal(routed.length, 0, 'nothing is delivered');
|
|
1026
|
-
assert.equal(internals.turnRouting.get('assistant')?.continuation ?? false, false, 'and no continuation is asked for');
|
|
1027
|
-
// The same envelope outside a private round asks for one.
|
|
1028
|
-
await internals.deliverProse(agent, '>>#alpha ! sent');
|
|
1029
|
-
assert.equal(routed.length, 1);
|
|
1030
|
-
assert.equal(internals.turnRouting.get('assistant')?.continuation, true);
|
|
1031
|
-
await framework.stop();
|
|
1032
|
-
});
|
|
1033
|
-
it('a skip_reply round that sets the retained text aside with >>skip_reply {{unsent}} clears it', async () => {
|
|
1034
|
-
// Turn 1 bounces its text, which retains it and wakes turn 2.
|
|
1035
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'An answer with nowhere to go.' }]));
|
|
1036
|
-
const framework = await createFramework();
|
|
1037
|
-
const routed = stubRegistry(framework);
|
|
1038
|
-
const skip = answerSkipReply(framework);
|
|
1039
|
-
feedTurn(framework, 2, [
|
|
1040
|
-
{ type: 'text', text: '>>skip_reply {{unsent}}' },
|
|
1041
|
-
{ type: 'tool_use', id: 'sr4', name: 'skip_reply', input: {} },
|
|
1042
|
-
], 'tool_use');
|
|
1043
|
-
trigger(framework);
|
|
1044
|
-
await framework.runUntilIdle();
|
|
1045
|
-
assert.equal(skip.calls, 1, 'the bounce wake ran the skip_reply round');
|
|
1046
|
-
assert.equal(routed.length, 0);
|
|
1047
|
-
assert.equal(clipboardOf(framework), undefined, 'the retained text is set aside, as >>skip_reply {{unsent}} says');
|
|
1048
|
-
await framework.stop();
|
|
1049
|
-
});
|
|
1050
|
-
it('explicit mode streams nothing before a destination: unprefixed text never reaches the trigger channel', async () => {
|
|
1051
|
-
membrane.pushResponse(createMockResponse([
|
|
1052
|
-
{ type: 'text', text: 'Unprefixed musing.\n>>#alpha streamed after the prefix' },
|
|
1053
|
-
]));
|
|
1054
|
-
const framework = await createFramework();
|
|
1055
|
-
stubRegistry(framework);
|
|
1056
|
-
const stream = captureStream(framework);
|
|
1057
|
-
// The unprefixed line bounces, and its wake starts a second turn.
|
|
1058
|
-
feedTurn(framework, 2, [{ type: 'text', text: '>>skip_reply' }]);
|
|
1059
|
-
// A turn the trigger channel woke: chan-alpha is where its typing shows.
|
|
1060
|
-
const internals = framework;
|
|
1061
|
-
internals.pendingRequests.push({
|
|
1062
|
-
agentName: 'assistant', reason: 'mcpl:channel-incoming', source: 'discord', timestamp: Date.now(),
|
|
1063
|
-
channelId: 'chan-alpha', addressed: true,
|
|
1064
|
-
});
|
|
1065
|
-
await internals.processInferenceRequests();
|
|
1066
|
-
await framework.runUntilIdle();
|
|
1067
|
-
const streamed = stream.chunks.map((c) => c.delta).join('');
|
|
1068
|
-
assert.ok(streamed.includes('streamed after the prefix'), 'prefixed text streams to its destination');
|
|
1069
|
-
assert.equal(streamed.includes('Unprefixed musing.'), false, 'unprefixed text is never streamed');
|
|
1070
|
-
assert.equal(stream.completes.some((c) => c.text.includes('Unprefixed musing.')), false, 'nor finalized by the stream complete');
|
|
1071
|
-
await framework.stop();
|
|
1072
|
-
});
|
|
1073
|
-
it('the stream reads leading whitespace and a token\'s end as delivery does', async () => {
|
|
1074
|
-
const framework = await createFramework();
|
|
1075
|
-
const routed = stubRegistry(framework);
|
|
1076
|
-
const gateway = framework;
|
|
1077
|
-
const agent = framework.getAgent('assistant');
|
|
1078
|
-
const cases = [' >>#alpha hello', '\u00a0>>#alpha hello', '\u2003>>#alpha hello', '\r>>#alpha hello', '\t>>#alpha\r\nhello'];
|
|
1079
|
-
try {
|
|
1080
|
-
for (const text of cases) {
|
|
1081
|
-
routed.length = 0;
|
|
1082
|
-
await gateway.deliverProse(agent, text);
|
|
1083
|
-
const router = new ProseStreamRouter({
|
|
1084
|
-
mode: 'explicit', initialTarget: null, resolve: (spec) => (spec === '#alpha' ? 'chan-alpha' : null),
|
|
1085
|
-
});
|
|
1086
|
-
const streamed = [...router.feed(text), ...router.finish()];
|
|
1087
|
-
assert.deepEqual(routed.map((r) => r.locus), ['chan-alpha'], `delivery sends ${JSON.stringify(text)} to #alpha`);
|
|
1088
|
-
assert.ok(streamed.length > 0 && streamed.every((d) => d.channelId === 'chan-alpha'), `the stream sends ${JSON.stringify(text)} there too`);
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
finally {
|
|
1092
|
-
await framework.stop();
|
|
1093
|
-
}
|
|
1094
|
-
});
|
|
1095
|
-
it("a round's destination, decided at its boundary, is the sticky destination its delivery leaves", async () => {
|
|
1096
|
-
const framework = await createFramework();
|
|
1097
|
-
stubRegistry(framework);
|
|
1098
|
-
const internals = framework;
|
|
1099
|
-
const agent = framework.getAgent('assistant');
|
|
1100
|
-
const rounds = [
|
|
1101
|
-
['>>#alpha first\n>>#alpha'],
|
|
1102
|
-
[' >>#alpha indented'],
|
|
1103
|
-
['>>#nowhere bounced'],
|
|
1104
|
-
['>>skip_reply kept\nand unprefixed'],
|
|
1105
|
-
['unprefixed', '>>#alpha second segment'],
|
|
1106
|
-
];
|
|
1107
|
-
try {
|
|
1108
|
-
for (const segments of rounds) {
|
|
1109
|
-
for (const start of [null, 'chan-alpha']) {
|
|
1110
|
-
internals.turnRouting.set('assistant', routingState({ pin: start ?? undefined }));
|
|
1111
|
-
for (const segment of segments)
|
|
1112
|
-
await internals.deliverProse(agent, segment);
|
|
1113
|
-
const admitted = segments.flatMap((segment) => internals.admitProse(segment));
|
|
1114
|
-
assert.equal(internals.roundDestination(start, admitted), internals.turnRouting.get('assistant')?.pin ?? null, `${JSON.stringify(segments)} from ${start}`);
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
finally {
|
|
1119
|
-
await framework.stop();
|
|
1120
|
-
}
|
|
1121
|
-
});
|
|
1122
|
-
it('hybrid: a `>>>` envelope with no target streams nothing, as delivery bounces it', async () => {
|
|
1123
|
-
const framework = await createFramework('hybrid');
|
|
1124
|
-
const routed = stubRegistry(framework);
|
|
1125
|
-
const gateway = framework;
|
|
1126
|
-
const agent = framework.getAgent('assistant');
|
|
1127
|
-
// The registry's resolver trims its argument, as the router's must here.
|
|
1128
|
-
const resolve = (spec) => (spec.trim() === '#alpha' ? 'chan-alpha' : null);
|
|
1129
|
-
const cases = ['>>>\r\nsecret', '>>>\nsecret', '>>>\u00a0#alpha secret', '>>> \u00a0#alpha secret'];
|
|
1130
|
-
try {
|
|
1131
|
-
for (const text of cases) {
|
|
1132
|
-
routed.length = 0;
|
|
1133
|
-
const queued = routingState();
|
|
1134
|
-
const live = routingState();
|
|
1135
|
-
gateway.turnRouting.set('assistant', live);
|
|
1136
|
-
await gateway.deliverHybridProse(agent, text, 'chan-locus', true, queued);
|
|
1137
|
-
assert.equal(queued.hybridHold, true, 'the supplied turn receives the hold');
|
|
1138
|
-
assert.equal(live.hybridHold, false, 'the live turn stays unheld');
|
|
1139
|
-
const router = new ProseStreamRouter({ mode: 'hybrid', initialTarget: 'chan-locus', resolve });
|
|
1140
|
-
const streamed = [...router.feed(text), ...router.finish()];
|
|
1141
|
-
assert.equal(routed.length, 0, `delivery bounces ${JSON.stringify(text)}`);
|
|
1142
|
-
assert.deepEqual(streamed, [], `and the stream sends none of ${JSON.stringify(text)}`);
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
finally {
|
|
1146
|
-
await framework.stop();
|
|
1147
|
-
}
|
|
1148
|
-
});
|
|
1149
|
-
it('hybrid: a segment that is only `>>>`, or `>>>` and a gap, holds back the next segment, as delivery does', async () => {
|
|
1150
|
-
const framework = await createFramework('hybrid');
|
|
1151
|
-
const routed = stubRegistry(framework);
|
|
1152
|
-
const gateway = framework;
|
|
1153
|
-
const agent = framework.getAgent('assistant');
|
|
1154
|
-
const resolve = (spec) => (spec.trim() === '#alpha' ? 'chan-alpha' : null);
|
|
1155
|
-
try {
|
|
1156
|
-
for (const prefix of ['>>>', '>>> ', '>>>\t']) {
|
|
1157
|
-
routed.length = 0;
|
|
1158
|
-
const queued = routingState();
|
|
1159
|
-
const live = routingState();
|
|
1160
|
-
gateway.turnRouting.set('assistant', live);
|
|
1161
|
-
await gateway.deliverHybridProse(agent, '>>>#alpha first', 'chan-locus', true, queued);
|
|
1162
|
-
await gateway.deliverHybridProse(agent, prefix, 'chan-locus', true, queued);
|
|
1163
|
-
await gateway.deliverHybridProse(agent, 'secret', 'chan-locus', true, queued);
|
|
1164
|
-
assert.deepEqual(queued.deliveries, ['chan-alpha'], 'the supplied turn records its delivery');
|
|
1165
|
-
assert.equal(queued.suppressed, 1, 'the supplied turn records its suppression');
|
|
1166
|
-
assert.deepEqual(live.deliveries, [], 'the live turn receives no delivery');
|
|
1167
|
-
assert.equal(live.suppressed, 0, 'the live turn receives no suppression');
|
|
1168
|
-
const router = new ProseStreamRouter({ mode: 'hybrid', initialTarget: 'chan-locus', resolve });
|
|
1169
|
-
const streamed = [
|
|
1170
|
-
...router.feed('>>>#alpha first'), ...router.endSegment(),
|
|
1171
|
-
...router.feed(prefix), ...router.endSegment(),
|
|
1172
|
-
...router.feed('secret'), ...router.finish(),
|
|
1173
|
-
];
|
|
1174
|
-
assert.deepEqual(routed.map((r) => r.text), ['first'], `delivery sends only first, after ${JSON.stringify(prefix)}`);
|
|
1175
|
-
assert.equal(streamed.filter((d) => d.delta.includes('secret')).length, 0, `and the stream holds secret back after ${JSON.stringify(prefix)}`);
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
finally {
|
|
1179
|
-
await framework.stop();
|
|
1180
|
-
}
|
|
1181
|
-
});
|
|
1182
|
-
it('admission turns a resolver that throws into an envelope that bounces', async () => {
|
|
1183
|
-
const framework = await createFramework();
|
|
1184
|
-
const routed = stubRegistry(framework);
|
|
1185
|
-
const internals = framework;
|
|
1186
|
-
const resolve = internals.channelRegistry.resolveProseTarget.bind(internals.channelRegistry);
|
|
1187
|
-
internals.channelRegistry.resolveProseTarget = (spec) => {
|
|
1188
|
-
if (spec === '#boom')
|
|
1189
|
-
throw new Error('directory unavailable');
|
|
1190
|
-
return resolve(spec);
|
|
1191
|
-
};
|
|
1192
|
-
try {
|
|
1193
|
-
const admitted = internals.admitProse('>>#boom one\n>>#alpha two');
|
|
1194
|
-
assert.match(admitted[0].resolved?.error ?? '', /directory unavailable/);
|
|
1195
|
-
await internals.deliverAdmittedProse(framework.getAgent('assistant'), admitted, { routing: internals.routingOf('assistant') });
|
|
1196
|
-
assert.deepEqual(routed.map((r) => r.text), ['two'], 'the other envelope still goes out');
|
|
1197
|
-
}
|
|
1198
|
-
finally {
|
|
1199
|
-
await framework.stop();
|
|
1200
|
-
}
|
|
1201
|
-
});
|
|
1202
|
-
it("' !' continuation starts another turn immediately after prose ends the current one", async () => {
|
|
1203
|
-
membrane.pushResponse(createMockResponse([
|
|
1204
|
-
{ type: 'text', text: '>>#alpha ! step one done, continuing' },
|
|
1205
|
-
]));
|
|
1206
|
-
const framework = await createFramework();
|
|
1207
|
-
const routed = stubRegistry(framework);
|
|
1208
|
-
feedTurn(framework, 2, [{ type: 'text', text: '>>#alpha step two done' }]);
|
|
1209
|
-
trigger(framework);
|
|
1210
|
-
await framework.runUntilIdle();
|
|
1211
|
-
assert.equal(membrane.calls.length, 2, 'a second inference ran without any external event');
|
|
1212
|
-
assert.deepEqual(routed.map((r) => r.text), ['step one done, continuing', 'step two done']);
|
|
1213
|
-
assert.deepEqual(routed.map((r) => r.locus), ['chan-alpha', 'chan-alpha']);
|
|
1214
|
-
await framework.stop();
|
|
1215
|
-
});
|
|
1216
|
-
it('appends the mode primer exactly once across turns', async () => {
|
|
1217
|
-
membrane.pushResponse(createMockResponse([
|
|
1218
|
-
{ type: 'text', text: '>>#alpha hello' },
|
|
1219
|
-
]));
|
|
1220
|
-
const framework = await createFramework();
|
|
1221
|
-
stubRegistry(framework);
|
|
1222
|
-
const primers = [];
|
|
1223
|
-
framework.onTrace((e) => {
|
|
1224
|
-
const ev = e;
|
|
1225
|
-
if (ev.type === 'message:added' && ev.source === 'prose-routing-primer')
|
|
1226
|
-
primers.push(1);
|
|
1227
|
-
});
|
|
1228
|
-
trigger(framework);
|
|
1229
|
-
await framework.runUntilIdle();
|
|
1230
|
-
assert.equal(primers.length, 1, 'primer on first explicit turn');
|
|
1231
|
-
membrane.pushResponse(createMockResponse([
|
|
1232
|
-
{ type: 'text', text: '>>#alpha again' },
|
|
1233
|
-
]));
|
|
1234
|
-
trigger(framework);
|
|
1235
|
-
await framework.runUntilIdle();
|
|
1236
|
-
assert.equal(primers.length, 1, 'no re-priming on later turns');
|
|
1237
|
-
await framework.stop();
|
|
1238
|
-
});
|
|
1239
|
-
it('prose_help tool returns the routing reference on demand', async () => {
|
|
1240
|
-
membrane.pushResponse(createMockResponse([
|
|
1241
|
-
{ type: 'tool_use', id: 'c1', name: 'prose_help', input: {} },
|
|
1242
|
-
], 'tool_use'));
|
|
1243
|
-
membrane.pushResponse(createMockResponse([
|
|
1244
|
-
{ type: 'text', text: '>>#alpha got it' },
|
|
1245
|
-
]));
|
|
1246
|
-
const framework = await createFramework();
|
|
1247
|
-
const routed = stubRegistry(framework);
|
|
1248
|
-
trigger(framework);
|
|
1249
|
-
await framework.runUntilIdle();
|
|
1250
|
-
const stream = membrane.lastStream;
|
|
1251
|
-
assert.equal(stream.receivedToolResults.length, 1);
|
|
1252
|
-
const result = JSON.stringify(stream.receivedToolResults[0]);
|
|
1253
|
-
assert.ok(result.includes('destination'), 'help text served as tool result');
|
|
1254
|
-
assert.deepEqual(routed, [{ text: 'got it', locus: 'chan-alpha' }]);
|
|
1255
|
-
await framework.stop();
|
|
1256
|
-
});
|
|
1257
|
-
it('multi-envelope prose: each >> line routes independently (Tilde scenario)', async () => {
|
|
1258
|
-
membrane.pushResponse(createMockResponse([
|
|
1259
|
-
{ type: 'text', text: '>>#alpha part one for the channel\n>>@laria part two, just for you\nwith a second line\n>> quoted arrow stays in body' },
|
|
1260
|
-
]));
|
|
1261
|
-
const framework = await createFramework();
|
|
1262
|
-
const routed = stubRegistry(framework);
|
|
1263
|
-
trigger(framework);
|
|
1264
|
-
await framework.runUntilIdle();
|
|
1265
|
-
assert.deepEqual(routed, [
|
|
1266
|
-
{ text: 'part one for the channel', locus: 'chan-alpha' },
|
|
1267
|
-
{ text: 'part two, just for you\nwith a second line\n>> quoted arrow stays in body', locus: 'discord:dm:99' },
|
|
1268
|
-
], 'two envelopes, two destinations; quoted arrow not split');
|
|
1269
|
-
await framework.stop();
|
|
1270
|
-
});
|
|
1271
|
-
it('hybrid preserves authored >>> envelope, publishes only body cross-surface, and records canonical receipt', async () => {
|
|
1272
|
-
const authored = '>>>#cafe\nA message meant for the café.';
|
|
1273
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: authored }]));
|
|
1274
|
-
const framework = await createFramework('hybrid');
|
|
1275
|
-
const routed = stubRegistry(framework);
|
|
1276
|
-
trigger(framework);
|
|
1277
|
-
await framework.runUntilIdle();
|
|
1278
|
-
assert.deepEqual(routed, [{ text: 'A message meant for the café.', locus: 'discord:g:cafe' }]);
|
|
1279
|
-
const cm = framework.getAgent('assistant').getContextManager();
|
|
1280
|
-
const texts = cm.getAllMessages().flatMap(m => m.content)
|
|
1281
|
-
.filter(b => b.type === 'text').map(b => b.text);
|
|
1282
|
-
assert.ok(texts.includes(authored), 'source/author view retains exact envelope');
|
|
1283
|
-
assert.ok(texts.includes('[delivered] plain speech → discord:g:cafe'), 'canonical destination receipt enters context');
|
|
1284
|
-
await framework.stop();
|
|
1285
|
-
});
|
|
1286
|
-
it('hybrid leaves unprefixed prose on the frozen locus', async () => {
|
|
1287
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'Ordinary field speech.' }]));
|
|
1288
|
-
const framework = await createFramework('hybrid');
|
|
1289
|
-
const routed = stubRegistry(framework);
|
|
1290
|
-
trigger(framework);
|
|
1291
|
-
await framework.runUntilIdle();
|
|
1292
|
-
assert.deepEqual(routed, [{ text: 'Ordinary field speech.', locus: 'world:commons' }]);
|
|
1293
|
-
await framework.stop();
|
|
1294
|
-
});
|
|
1295
|
-
it('hybrid accepts permissive whitespace but missing targets bounce and publish nowhere', async () => {
|
|
1296
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: ' >>> #missing\nDo not guess.' }]));
|
|
1297
|
-
const framework = await createFramework('hybrid');
|
|
1298
|
-
const routed = stubRegistry(framework);
|
|
1299
|
-
feedTurn(framework, 2, [{ type: 'text', text: '>>>skip_reply {{unsent}}' }]);
|
|
1300
|
-
trigger(framework);
|
|
1301
|
-
await framework.runUntilIdle();
|
|
1302
|
-
assert.deepEqual(routed, []);
|
|
1303
|
-
const cm = framework.getAgent('assistant').getContextManager();
|
|
1304
|
-
const texts = cm.getAllMessages().flatMap(m => m.content)
|
|
1305
|
-
.filter(b => b.type === 'text').map(b => b.text);
|
|
1306
|
-
assert.ok(texts.some(t => t.includes('[prose-routing]') && t.includes('not delivered') && t.includes('>>>#channel {{unsent}}')));
|
|
1307
|
-
await framework.stop();
|
|
1308
|
-
});
|
|
1309
|
-
it('hybrid keeps an explicit target sticky across tool-round segments in the same turn', async () => {
|
|
1310
|
-
membrane.pushResponse(createMockResponse([
|
|
1311
|
-
{ type: 'text', text: '>>>#cafe first segment' },
|
|
1312
|
-
{ type: 'tool_use', id: 'hc1', name: 'robot--move', input: { dir: 'north' } },
|
|
1313
|
-
], 'tool_use'));
|
|
1314
|
-
membrane.pushResponse(createMockResponse([
|
|
1315
|
-
{ type: 'text', text: 'second segment, no repeated envelope' },
|
|
1316
|
-
]));
|
|
1317
|
-
const framework = await createFramework('hybrid');
|
|
1318
|
-
const routed = stubRegistry(framework);
|
|
1319
|
-
trigger(framework);
|
|
1320
|
-
await framework.runUntilIdle();
|
|
1321
|
-
assert.deepEqual(routed, [
|
|
1322
|
-
{ text: 'first segment', locus: 'discord:g:cafe' },
|
|
1323
|
-
{ text: 'second segment, no repeated envelope', locus: 'discord:g:cafe' },
|
|
1324
|
-
]);
|
|
1325
|
-
await framework.stop();
|
|
1326
|
-
});
|
|
1327
|
-
it('hybrid target-only envelope establishes the sticky destination for the next tool-round segment', async () => {
|
|
1328
|
-
membrane.pushResponse(createMockResponse([
|
|
1329
|
-
{ type: 'text', text: '>>>#cafe' },
|
|
1330
|
-
{ type: 'tool_use', id: 'hc2', name: 'robot--move', input: { dir: 'north' } },
|
|
1331
|
-
], 'tool_use'));
|
|
1332
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'body follows' }]));
|
|
1333
|
-
const framework = await createFramework('hybrid');
|
|
1334
|
-
const routed = stubRegistry(framework);
|
|
1335
|
-
trigger(framework);
|
|
1336
|
-
await framework.runUntilIdle();
|
|
1337
|
-
assert.deepEqual(routed, [{ text: 'body follows', locus: 'discord:g:cafe' }]);
|
|
1338
|
-
await framework.stop();
|
|
1339
|
-
});
|
|
1340
|
-
it('hybrid >>>skip_reply clears a sticky target until a fresh valid envelope', async () => {
|
|
1341
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: '>>>#cafe first' }, { type: 'tool_use', id: 'sk1', name: 'robot--move', input: { dir: 'north' } }], 'tool_use'));
|
|
1342
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: '>>>skip_reply hidden' }, { type: 'tool_use', id: 'sk2', name: 'robot--move', input: { dir: 'north' } }], 'tool_use'));
|
|
1343
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'plain text must remain suppressed' }, { type: 'tool_use', id: 'sk3', name: 'robot--move', input: { dir: 'north' } }], 'tool_use'));
|
|
1344
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: '>>>world:commons recovered' }]));
|
|
1345
|
-
const framework = await createFramework('hybrid');
|
|
1346
|
-
const routed = stubRegistry(framework);
|
|
1347
|
-
trigger(framework);
|
|
1348
|
-
await framework.runUntilIdle();
|
|
1349
|
-
assert.deepEqual(routed, [{ text: 'first', locus: 'discord:g:cafe' }, { text: 'recovered', locus: 'world:commons' }]);
|
|
1350
|
-
await framework.stop();
|
|
1351
|
-
});
|
|
1352
|
-
for (const failure of ['false', 'throw']) {
|
|
1353
|
-
it(`hybrid ${failure} delivery clears sticky authority, suppresses later prose, and a fresh target resumes`, async () => {
|
|
1354
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: '>>>#cafe first' }, { type: 'tool_use', id: 'fl1', name: 'robot--move', input: { dir: 'north' } }], 'tool_use'));
|
|
1355
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: '>>>#alpha fails' }, { type: 'tool_use', id: 'fl2', name: 'robot--move', input: { dir: 'north' } }], 'tool_use'));
|
|
1356
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'plain text must remain suppressed' }, { type: 'tool_use', id: 'fl3', name: 'robot--move', input: { dir: 'north' } }], 'tool_use'));
|
|
1357
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: '>>>world:commons recovered' }]));
|
|
1358
|
-
const framework = await createFramework('hybrid');
|
|
1359
|
-
const routed = stubRegistry(framework, ['delivered', failure, 'delivered']);
|
|
1360
|
-
feedTurn(framework, 2, [{ type: 'text', text: '>>>skip_reply {{unsent}}' }]);
|
|
1361
|
-
trigger(framework);
|
|
1362
|
-
await framework.runUntilIdle();
|
|
1363
|
-
assert.equal(routed.some(r => r.text.includes('plain text must remain suppressed')), false);
|
|
1364
|
-
assert.deepEqual(routed.filter(r => r.text !== 'fails'), [{ text: 'first', locus: 'discord:g:cafe' }, { text: 'recovered', locus: 'world:commons' }]);
|
|
1365
|
-
const cm = framework.getAgent('assistant').getContextManager();
|
|
1366
|
-
const texts = cm.getAllMessages().flatMap(m => m.content).filter(b => b.type === 'text').map(b => b.text);
|
|
1367
|
-
assert.ok(texts.some(t => t.includes('[prose-routing]') && t.includes('>>>#channel {{unsent}}')));
|
|
1368
|
-
await framework.stop();
|
|
1369
|
-
});
|
|
1370
|
-
}
|
|
1371
|
-
it('hybrid same-round successful say tool outranks a contradictory prose envelope', async () => {
|
|
1372
|
-
const authored = '>>>#cafe wrong-locus duplicate';
|
|
1373
|
-
membrane.pushResponse(createMockResponse([
|
|
1374
|
-
{ type: 'text', text: authored },
|
|
1375
|
-
{ type: 'tool_use', id: 'say1', name: 'robot--say', input: { text: 'Hello Sill in the world' } },
|
|
1376
|
-
], 'tool_use'));
|
|
1377
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'tool completed' }]));
|
|
1378
|
-
const framework = await createFramework('hybrid');
|
|
1379
|
-
const routed = stubRegistry(framework);
|
|
1380
|
-
trigger(framework);
|
|
1381
|
-
await framework.runUntilIdle();
|
|
1382
|
-
assert.equal(module.calls.filter(c => c.name === 'say' || c.name.endsWith('--say')).length, 1);
|
|
1383
|
-
assert.deepEqual(routed, []);
|
|
1384
|
-
const cm = framework.getAgent('assistant').getContextManager();
|
|
1385
|
-
const texts = cm.getAllMessages().flatMap(m => m.content).filter(b => b.type === 'text').map(b => b.text);
|
|
1386
|
-
assert.ok(texts.includes(authored));
|
|
1387
|
-
assert.ok(texts.some(t => t.startsWith('[delivered] nothing') && t.includes('suppressed')));
|
|
1388
|
-
await framework.stop();
|
|
1389
|
-
});
|
|
1390
|
-
it('disabled mode suppresses text-only prose without a bounce, primer, or channel delivery', async () => {
|
|
1391
|
-
const authored = 'Internal continuity summary that must never publish.';
|
|
1392
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: authored }]));
|
|
1393
|
-
const framework = await createFramework('disabled');
|
|
1394
|
-
const routed = stubRegistry(framework);
|
|
1395
|
-
const sources = [];
|
|
1396
|
-
framework.onTrace((e) => {
|
|
1397
|
-
const ev = e;
|
|
1398
|
-
if (ev.type === 'message:added' && ev.source)
|
|
1399
|
-
sources.push(ev.source);
|
|
1400
|
-
});
|
|
1401
|
-
trigger(framework);
|
|
1402
|
-
await framework.runUntilIdle();
|
|
1403
|
-
assert.deepEqual(routed, [], 'disabled prose never reaches a channel');
|
|
1404
|
-
assert.equal(sources.includes('prose-bounce'), false, 'disabled prose does not bounce');
|
|
1405
|
-
assert.equal(sources.includes('prose-routing-primer'), false, 'disabled mode injects no model-visible primer');
|
|
1406
|
-
const texts = framework.getAgent('assistant').getContextManager().getAllMessages()
|
|
1407
|
-
.flatMap(m => m.content).filter(b => b.type === 'text').map(b => b.text);
|
|
1408
|
-
assert.ok(texts.includes(authored), 'authored prose remains in Chronicle');
|
|
1409
|
-
assert.ok(texts.some(t => t.includes('[delivered] nothing') && t.includes('proseRouting=disabled')), 'private suppression receipt explains the unsent prose');
|
|
1410
|
-
await framework.stop();
|
|
1411
|
-
});
|
|
1412
|
-
it('disabled mode ignores destination prefixes but explicit publish tools still execute', async () => {
|
|
1413
|
-
membrane.pushResponse(createMockResponse([
|
|
1414
|
-
{ type: 'text', text: '>>#alpha this prefix must not bypass disabled mode' },
|
|
1415
|
-
{ type: 'tool_use', id: 'd1', name: 'robot--say', input: { text: 'published explicitly' } },
|
|
1416
|
-
], 'tool_use'));
|
|
1417
|
-
membrane.pushResponse(createMockResponse([
|
|
1418
|
-
{ type: 'text', text: 'trailing prose must also remain private' },
|
|
1419
|
-
]));
|
|
1420
|
-
const framework = await createFramework('disabled');
|
|
1421
|
-
const routed = stubRegistry(framework);
|
|
1422
|
-
trigger(framework);
|
|
1423
|
-
await framework.runUntilIdle();
|
|
1424
|
-
assert.deepEqual(routed, [], 'neither prefixed nor trailing prose routed');
|
|
1425
|
-
assert.equal(module.calls.length, 1, 'explicit tool call executed');
|
|
1426
|
-
assert.equal(module.calls[0].name, 'say');
|
|
1427
|
-
const texts = framework.getAgent('assistant').getContextManager().getAllMessages()
|
|
1428
|
-
.flatMap(m => m.content).filter(b => b.type === 'text').map(b => b.text);
|
|
1429
|
-
assert.ok(texts.some(t => t.includes('[delivered] nothing') && t.includes('2 plain-speech segment(s) suppressed')), 'one receipt accounts for mid-turn and trailing prose');
|
|
1430
|
-
await framework.stop();
|
|
1431
|
-
});
|
|
1432
|
-
// ---- A start that continues a logical turn keeps the turn's routing ----
|
|
1433
|
-
// A framework retry after a stream error, the compile a provider-capacity
|
|
1434
|
-
// hold defers a turn to, and the re-inference that consumes unstreamed tool
|
|
1435
|
-
// results each continue the turn: the sticky destination, explicit mode's
|
|
1436
|
-
// turn destination and hybrid's hold carry into the new stream.
|
|
1437
|
-
/** An organization acceleration 429, which holds the turn for a later compile. */
|
|
1438
|
-
const accelerationLimit = (request) => new MembraneError({
|
|
1439
|
-
type: 'rate_limit', retryable: true, httpStatus: 429,
|
|
1440
|
-
message: "This request would exceed your organization's maximum usage increase rate for input tokens per minute",
|
|
1441
|
-
rawError: { status: 429 }, rawRequest: request,
|
|
1442
|
-
});
|
|
1443
|
-
/**
|
|
1444
|
-
* A turn whose first stream writes `first` and calls a tool, and then
|
|
1445
|
-
* fails instead of completing: with a transient error, which the framework
|
|
1446
|
-
* retries, or with an acceleration 429, which holds the turn until a later
|
|
1447
|
-
* compile. That later stream answers `carrying on` with no prefix, as a
|
|
1448
|
-
* model continuing its turn would.
|
|
1449
|
-
*/
|
|
1450
|
-
async function continueTurn(how, mode, first) {
|
|
1451
|
-
membrane.pushResponse(createMockResponse([
|
|
1452
|
-
{ type: 'text', text: first },
|
|
1453
|
-
{ type: 'tool_use', id: 'continued-tool', name: 'robot--move', input: { dir: 'north' } },
|
|
1454
|
-
], 'tool_use'));
|
|
1455
|
-
membrane.pushResponse(createMockResponse([]));
|
|
1456
|
-
const framework = await createFramework(mode);
|
|
1457
|
-
const routed = stubRegistry(framework);
|
|
1458
|
-
const { chunks } = captureStream(framework);
|
|
1459
|
-
const internals = framework;
|
|
1460
|
-
let starts = 0;
|
|
1461
|
-
let laterChunks = 0;
|
|
1462
|
-
framework.onTrace((event) => {
|
|
1463
|
-
if (event.type !== 'inference:started')
|
|
1464
|
-
return;
|
|
1465
|
-
starts++;
|
|
1466
|
-
if (starts === 2) {
|
|
1467
|
-
laterChunks = chunks.length;
|
|
1468
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'carrying on' }]));
|
|
1469
|
-
}
|
|
1470
|
-
// A bounce wakes the resident; that turn stays quiet.
|
|
1471
|
-
if (starts >= 3)
|
|
1472
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: mode === 'explicit' ? '>>skip_reply' : '>>>skip_reply' }]));
|
|
1473
|
-
});
|
|
1474
|
-
const makeStream = membrane.streamYielding.bind(membrane);
|
|
1475
|
-
let streams = 0;
|
|
1476
|
-
membrane.streamYielding = (request, options) => {
|
|
1477
|
-
const stream = makeStream(request, options);
|
|
1478
|
-
const index = ++streams;
|
|
1479
|
-
const iterate = stream[Symbol.asyncIterator].bind(stream);
|
|
1480
|
-
stream[Symbol.asyncIterator] = async function* () {
|
|
1481
|
-
for await (const event of { [Symbol.asyncIterator]: iterate }) {
|
|
1482
|
-
if (event.type === 'complete' && index === 1) {
|
|
1483
|
-
yield {
|
|
1484
|
-
type: 'error',
|
|
1485
|
-
error: how === 'retry' ? new Error('synthetic transient stream error') : accelerationLimit(request),
|
|
1486
|
-
};
|
|
1487
|
-
return;
|
|
1488
|
-
}
|
|
1489
|
-
yield event;
|
|
1490
|
-
}
|
|
1491
|
-
};
|
|
1492
|
-
return stream;
|
|
1493
|
-
};
|
|
1494
|
-
trigger(framework);
|
|
1495
|
-
await within(framework.runUntilIdle(), 'the first stream', 10000);
|
|
1496
|
-
if (how === 'capacity') {
|
|
1497
|
-
assert.equal(starts, 1, 'the hold defers the turn rather than retrying it at once');
|
|
1498
|
-
const hold = internals.providerAccelerationCooldowns.get('assistant');
|
|
1499
|
-
assert.ok(hold, 'the turn is held');
|
|
1500
|
-
// What the cooldown's timer does when it fires, at the hold's deadline.
|
|
1501
|
-
hold.until = Date.now();
|
|
1502
|
-
internals.releaseProviderAccelerationCooldown('assistant');
|
|
1503
|
-
await within(framework.runUntilIdle(), 'the deferred compile', 10000);
|
|
1504
|
-
}
|
|
1505
|
-
assert.ok(starts >= 2, 'the turn went on in a later stream');
|
|
1506
|
-
return { framework, routed, bounces: context(framework).bounces, laterChunks: chunks.slice(laterChunks) };
|
|
1507
|
-
}
|
|
1508
|
-
for (const how of ['retry', 'capacity']) {
|
|
1509
|
-
const start = how === 'retry' ? 'an error retry' : 'the compile a capacity hold defers the turn to';
|
|
1510
|
-
for (const [label, mode, first, expected] of [
|
|
1511
|
-
['hybrid >>>skip_reply keeps later unprefixed prose private', 'hybrid', '>>>skip_reply an aside', []],
|
|
1512
|
-
['hybrid >>>#alpha is sticky', 'hybrid', '>>>#alpha first', [{ text: 'first', locus: 'chan-alpha' }, { text: 'carrying on', locus: 'chan-alpha' }]],
|
|
1513
|
-
['explicit >>#alpha is sticky, and the stream starts at the turn\'s destination', 'explicit', '>>#alpha first', [{ text: 'first', locus: 'chan-alpha' }, { text: 'carrying on', locus: 'chan-alpha' }]],
|
|
1514
|
-
]) {
|
|
1515
|
-
it(`across ${start}, ${label}`, async () => {
|
|
1516
|
-
const { framework, routed, bounces, laterChunks } = await continueTurn(how, mode, first);
|
|
1517
|
-
try {
|
|
1518
|
-
// Publication as the registry stub saw it; bounce notices by their kind.
|
|
1519
|
-
assert.deepEqual(routed.map((r) => ({ text: r.text.trim(), locus: r.locus })), expected);
|
|
1520
|
-
assert.equal(bounces, 0, 'nothing bounced');
|
|
1521
|
-
if (mode === 'explicit') {
|
|
1522
|
-
// The later stream streams its unprefixed text where delivery sends it.
|
|
1523
|
-
assert.deepEqual(laterChunks.map((c) => c.channelId), laterChunks.map(() => 'chan-alpha'));
|
|
1524
|
-
assert.equal(laterChunks.map((c) => c.delta).join(''), 'carrying on');
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
finally {
|
|
1528
|
-
await framework.stop();
|
|
1529
|
-
}
|
|
1530
|
-
});
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
for (const [label, mode, left, expected] of [
|
|
1534
|
-
['hybrid: the hold keeps later unprefixed prose private', 'hybrid', { hybridHold: true, locus: 'world:commons' }, []],
|
|
1535
|
-
['hybrid: the sticky destination', 'hybrid', { pin: 'chan-alpha', locus: 'world:commons' }, [{ text: 'carrying on', locus: 'chan-alpha' }]],
|
|
1536
|
-
['explicit: the sticky destination, and the stream starts at the turn\'s destination', 'explicit', { pin: 'chan-alpha', destination: 'chan-alpha' }, [{ text: 'carrying on', locus: 'chan-alpha' }]],
|
|
1537
|
-
]) {
|
|
1538
|
-
it(`a re-inference on unstreamed tool results keeps the turn's routing: ${label}`, async () => {
|
|
1539
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'carrying on' }]));
|
|
1540
|
-
const framework = await createFramework(mode);
|
|
1541
|
-
const routed = stubRegistry(framework);
|
|
1542
|
-
const { chunks } = captureStream(framework);
|
|
1543
|
-
// A bounce wakes the resident; that turn stays quiet.
|
|
1544
|
-
feedTurn(framework, 2, [{ type: 'text', text: mode === 'explicit' ? '>>skip_reply' : '>>>skip_reply' }]);
|
|
1545
|
-
const internals = framework;
|
|
1546
|
-
try {
|
|
1547
|
-
// The turn's earlier stream left this routing state, and its tool
|
|
1548
|
-
// results pending for the non-streaming fallback's re-inference.
|
|
1549
|
-
const earlier = routingState(left);
|
|
1550
|
-
internals.turnRouting.set('assistant', earlier);
|
|
1551
|
-
internals.openLogical.set('assistant', earlier);
|
|
1552
|
-
internals.pendingRequests.push({ agentName: 'assistant', reason: 'tool_results_ready', source: 'framework', timestamp: Date.now() });
|
|
1553
|
-
await within(framework.runUntilIdle(), 'the re-inference', 10000);
|
|
1554
|
-
assert.deepEqual(routed.map((r) => ({ text: r.text.trim(), locus: r.locus })), expected);
|
|
1555
|
-
assert.equal(context(framework).bounces, 0, 'nothing bounced');
|
|
1556
|
-
if (mode === 'explicit') {
|
|
1557
|
-
assert.deepEqual(chunks.map((c) => c.channelId), chunks.map(() => 'chan-alpha'));
|
|
1558
|
-
assert.equal(chunks.map((c) => c.delta).join(''), 'carrying on');
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
finally {
|
|
1562
|
-
await framework.stop();
|
|
1563
|
-
}
|
|
1564
|
-
});
|
|
1565
|
-
}
|
|
1566
|
-
for (const unreadable of [false, true]) {
|
|
1567
|
-
const name = unreadable
|
|
1568
|
-
? "explicit: a turn with no locus doesn't read its arrival's message, so a store that can't be read leaves its next start alone"
|
|
1569
|
-
: "explicit: an addressed message between the turn's streams moves nothing, since explicit mode has no locus";
|
|
1570
|
-
it(name, async () => {
|
|
1571
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'carrying on' }]));
|
|
1572
|
-
const framework = await createFramework('explicit');
|
|
1573
|
-
const routed = stubRegistry(framework);
|
|
1574
|
-
feedTurn(framework, 2, [{ type: 'text', text: '>>skip_reply' }]);
|
|
1575
|
-
const internals = framework;
|
|
1576
|
-
const cm = framework.getAgent('assistant').getContextManager();
|
|
1577
|
-
const getMessage = cm.getMessage.bind(cm);
|
|
1578
|
-
try {
|
|
1579
|
-
const earlier = routingState({ pin: 'chan-alpha', destination: 'chan-alpha' });
|
|
1580
|
-
internals.turnRouting.set('assistant', earlier);
|
|
1581
|
-
internals.openLogical.set('assistant', earlier);
|
|
1582
|
-
// Someone addresses the agent from another channel while the turn waits.
|
|
1583
|
-
internals.addMessage('Antra', [{ type: 'text', text: 'quick question for you' }], { channelId: 'discord:g:cafe', tags: ['chat:addressed'] });
|
|
1584
|
-
assert.deepEqual(earlier.arrivals.map((a) => a.channelId), ['discord:g:cafe'], 'it is the turn\'s arrival');
|
|
1585
|
-
if (unreadable) {
|
|
1586
|
-
const arrival = earlier.arrivals[0].messageId;
|
|
1587
|
-
cm.getMessage = (id) => {
|
|
1588
|
-
if (id === arrival)
|
|
1589
|
-
throw new Error('store unavailable');
|
|
1590
|
-
return getMessage(id);
|
|
1591
|
-
};
|
|
1592
|
-
}
|
|
1593
|
-
internals.pendingRequests.push({ agentName: 'assistant', reason: 'tool_results_ready', source: 'framework', timestamp: Date.now() });
|
|
1594
|
-
await within(framework.runUntilIdle(), 'the re-inference', 10000);
|
|
1595
|
-
assert.deepEqual(routed.map((r) => ({ text: r.text.trim(), locus: r.locus })), [{ text: 'carrying on', locus: 'chan-alpha' }]);
|
|
1596
|
-
assert.equal(earlier.locus, undefined, 'no locus');
|
|
1597
|
-
assert.deepEqual(earlier.arrivals, [], 'the arrival is used up');
|
|
1598
|
-
assert.ok(!context(framework).texts.some((t) => t.includes('The conversation moved to')), 'and no routing notice');
|
|
1599
|
-
}
|
|
1600
|
-
finally {
|
|
1601
|
-
cm.getMessage = getMessage;
|
|
1602
|
-
await framework.stop();
|
|
1603
|
-
}
|
|
1604
|
-
});
|
|
1605
|
-
}
|
|
1606
|
-
for (const mode of ['explicit', 'hybrid']) {
|
|
1607
|
-
it(`${mode}: a retried turn's text waits for the replaced stream's late deliveries, and follows what they set`, async () => {
|
|
1608
|
-
const p = mode === 'explicit' ? '>>' : '>>>';
|
|
1609
|
-
// The first stream's round one posts to #cafe, and the post is held.
|
|
1610
|
-
// Round two sets where the turn goes next (explicit: #alpha; hybrid: a
|
|
1611
|
-
// skip_reply hold); its delivery is queued behind the held post. Then
|
|
1612
|
-
// the stream errors, and its retry starts while both are pending.
|
|
1613
|
-
membrane.pushResponse(createMockResponse([
|
|
1614
|
-
{ type: 'text', text: `${p}#cafe zero` },
|
|
1615
|
-
{ type: 'tool_use', id: 'late-one', name: 'robot--move', input: { dir: 'north' } },
|
|
1616
|
-
], 'tool_use'));
|
|
1617
|
-
membrane.pushResponse(createMockResponse([
|
|
1618
|
-
{ type: 'text', text: mode === 'explicit' ? '>>#alpha first' : '>>>skip_reply an aside' },
|
|
1619
|
-
{ type: 'tool_use', id: 'late-two', name: 'robot--move', input: { dir: 'south' } },
|
|
1620
|
-
], 'tool_use'));
|
|
1621
|
-
membrane.pushResponse(createMockResponse([]));
|
|
1622
|
-
const framework = await createFramework(mode);
|
|
1623
|
-
const routed = stubRegistry(framework);
|
|
1624
|
-
const { chunks } = captureStream(framework);
|
|
1625
|
-
const internals = framework;
|
|
1626
|
-
let release;
|
|
1627
|
-
const held = new Promise((resolve) => { release = resolve; });
|
|
1628
|
-
const route = internals.channelRegistry.routeSpeech;
|
|
1629
|
-
internals.channelRegistry.routeSpeech = async (agent, text, locus) => {
|
|
1630
|
-
const outcome = await route(agent, text, locus);
|
|
1631
|
-
if (text.trim() === 'zero')
|
|
1632
|
-
await held;
|
|
1633
|
-
return outcome;
|
|
1634
|
-
};
|
|
1635
|
-
let starts = 0;
|
|
1636
|
-
let retryChunks = 0;
|
|
1637
|
-
framework.onTrace((event) => {
|
|
1638
|
-
if (event.type !== 'inference:started')
|
|
1639
|
-
return;
|
|
1640
|
-
starts++;
|
|
1641
|
-
if (starts === 2) {
|
|
1642
|
-
retryChunks = chunks.length;
|
|
1643
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'carrying on' }]));
|
|
1644
|
-
}
|
|
1645
|
-
if (starts >= 3)
|
|
1646
|
-
membrane.pushResponse(createMockResponse([{ type: 'text', text: `${p}skip_reply` }]));
|
|
1647
|
-
});
|
|
1648
|
-
let retryCompleted;
|
|
1649
|
-
const retryComplete = new Promise((resolve) => { retryCompleted = resolve; });
|
|
1650
|
-
const makeStream = membrane.streamYielding.bind(membrane);
|
|
1651
|
-
let streams = 0;
|
|
1652
|
-
membrane.streamYielding = (request, options) => {
|
|
1653
|
-
const stream = makeStream(request, options);
|
|
1654
|
-
const index = ++streams;
|
|
1655
|
-
const iterate = stream[Symbol.asyncIterator].bind(stream);
|
|
1656
|
-
stream[Symbol.asyncIterator] = async function* () {
|
|
1657
|
-
for await (const event of { [Symbol.asyncIterator]: iterate }) {
|
|
1658
|
-
if (event.type === 'complete' && index === 1) {
|
|
1659
|
-
yield { type: 'error', error: new Error('synthetic transient stream error') };
|
|
1660
|
-
return;
|
|
1661
|
-
}
|
|
1662
|
-
if (event.type === 'complete' && index === 2)
|
|
1663
|
-
retryCompleted();
|
|
1664
|
-
yield event;
|
|
1665
|
-
}
|
|
1666
|
-
};
|
|
1667
|
-
return stream;
|
|
1668
|
-
};
|
|
1669
|
-
let running;
|
|
1670
|
-
try {
|
|
1671
|
-
trigger(framework);
|
|
1672
|
-
running = framework.runUntilIdle();
|
|
1673
|
-
void running.catch(() => { });
|
|
1674
|
-
await within(retryComplete, 'the retry to finish its response', 10000);
|
|
1675
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1676
|
-
assert.equal(starts, 2, 'the retry has started');
|
|
1677
|
-
assert.deepEqual(routed.map((r) => r.text.trim()), ['zero'], "the retry's text waits behind the held post");
|
|
1678
|
-
const routing = internals.turnRouting.get('assistant');
|
|
1679
|
-
if (mode === 'explicit') {
|
|
1680
|
-
assert.equal(routing.pin, 'discord:g:cafe', "round two's delivery hasn't run yet");
|
|
1681
|
-
assert.equal(routing.destination, 'chan-alpha', 'though round two set the destination as it ended');
|
|
1682
|
-
}
|
|
1683
|
-
else {
|
|
1684
|
-
assert.equal(routing.hybridHold, false, "round two's delivery hasn't run yet");
|
|
1685
|
-
}
|
|
1686
|
-
release();
|
|
1687
|
-
await within(running, 'the retried turn to settle', 10000);
|
|
1688
|
-
const posts = routed.map((r) => ({ text: r.text.trim(), locus: r.locus }));
|
|
1689
|
-
if (mode === 'explicit') {
|
|
1690
|
-
assert.deepEqual(posts, [
|
|
1691
|
-
{ text: 'zero', locus: 'discord:g:cafe' },
|
|
1692
|
-
{ text: 'first', locus: 'chan-alpha' },
|
|
1693
|
-
{ text: 'carrying on', locus: 'chan-alpha' },
|
|
1694
|
-
], "the replaced stream's posts go first, and the retry's text follows the destination they set");
|
|
1695
|
-
assert.deepEqual(chunks.slice(retryChunks).map((c) => c.channelId), chunks.slice(retryChunks).map(() => 'chan-alpha'), "the retry's stream starts at the destination round two set");
|
|
1696
|
-
}
|
|
1697
|
-
else {
|
|
1698
|
-
assert.deepEqual(posts, [{ text: 'zero', locus: 'discord:g:cafe' }], "the retry's unprefixed text stays private under the hold the late delivery set");
|
|
1699
|
-
const receipt = context(framework).texts.filter((t) => t.startsWith('[delivered]'));
|
|
1700
|
-
assert.equal(receipt.length, 1, `one receipt for the logical turn: ${receipt.join(' | ')}`);
|
|
1701
|
-
assert.match(receipt[0], /discord:g:cafe/, "it reports the replaced stream's late post");
|
|
1702
|
-
assert.match(receipt[0], /1 plain-speech segment\(s\) suppressed/, "and the retry's held segment");
|
|
1703
|
-
}
|
|
1704
|
-
assert.equal(context(framework).bounces, 0, 'nothing bounced');
|
|
1705
|
-
}
|
|
1706
|
-
finally {
|
|
1707
|
-
release();
|
|
1708
|
-
if (running)
|
|
1709
|
-
await running;
|
|
1710
|
-
await framework.stop();
|
|
1711
|
-
}
|
|
1712
|
-
});
|
|
1713
|
-
}
|
|
1714
|
-
});
|
|
1715
|
-
//# sourceMappingURL=explicit-prose-routing.test.js.map
|