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