@animalabs/agent-framework 0.22.0 → 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 +12 -5
- package/dist/src/agent.d.ts +192 -19
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +407 -66
- 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 +84 -23
- 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 +682 -103
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3202 -718
- 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 +4 -4
- 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/index.d.ts +2 -0
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js.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 +2 -1
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +20 -9
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +29 -6
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +57 -16
- 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 +46 -3
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +98 -15
- 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 +9 -3
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +3 -2
- 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 +85 -16
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +56 -11
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +288 -43
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +31 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +63 -12
- 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 +41 -0
- 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 +216 -0
- package/dist/src/provider-waits.d.ts.map +1 -0
- package/dist/src/provider-waits.js +482 -0
- package/dist/src/provider-waits.js.map +1 -0
- 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/refusal-reactions.d.ts +14 -12
- package/dist/src/refusal-reactions.d.ts.map +1 -1
- package/dist/src/refusal-reactions.js +14 -12
- package/dist/src/refusal-reactions.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 +30 -13
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +71 -25
- 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/events.d.ts +12 -0
- package/dist/src/types/events.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 +20 -8
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +75 -13
- 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 +107 -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 -44
- package/dist/test/explicit-prose-routing.test.js.map +1 -1
- package/dist/test/framework.test.js +160 -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 +11 -4
- 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 +220 -14
- package/dist/test/inference-exhaustion-rewind.test.js.map +1 -1
- package/dist/test/inference-failure-bounds.test.d.ts +2 -0
- package/dist/test/inference-failure-bounds.test.d.ts.map +1 -0
- package/dist/test/inference-failure-bounds.test.js +1202 -0
- package/dist/test/inference-failure-bounds.test.js.map +1 -0
- package/dist/test/kv-unified-open-flight-retry.test.js.map +1 -1
- package/dist/test/late-events-after-stop.test.d.ts +2 -0
- package/dist/test/late-events-after-stop.test.d.ts.map +1 -0
- package/dist/test/late-events-after-stop.test.js +122 -0
- package/dist/test/late-events-after-stop.test.js.map +1 -0
- 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 +141 -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 +78 -16
- package/dist/test/mcpl-coalescing.test.js.map +1 -1
- package/dist/test/mcpl-config-diagnostics.test.js +6 -3
- 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 +2 -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 +232 -4
- package/dist/test/mcpl-reference-fetcher.test.js.map +1 -1
- package/dist/test/mcpl-reference-fields.test.js +69 -2
- 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 +104 -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/ops-alert.test.js +2 -1
- package/dist/test/ops-alert.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.d.ts +2 -0
- package/dist/test/placed-reaction-baseline.test.d.ts.map +1 -0
- package/dist/test/placed-reaction-baseline.test.js +312 -0
- package/dist/test/placed-reaction-baseline.test.js.map +1 -0
- 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 +5 -1
- package/dist/test/provider-acceleration-cooldown.test.js.map +1 -1
- package/dist/test/provider-waits.test.d.ts +2 -0
- package/dist/test/provider-waits.test.d.ts.map +1 -0
- package/dist/test/provider-waits.test.js +2259 -0
- package/dist/test/provider-waits.test.js.map +1 -0
- 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.d.ts +2 -0
- package/dist/test/reaction-baseline-platform.test.d.ts.map +1 -0
- package/dist/test/reaction-baseline-platform.test.js +124 -0
- package/dist/test/reaction-baseline-platform.test.js.map +1 -0
- package/dist/test/record-journal.test.js.map +1 -1
- package/dist/test/refusal-reaction-baseline.test.js +10 -3
- package/dist/test/refusal-reaction-baseline.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/save-recent-image.test.js +138 -2
- package/dist/test/save-recent-image.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.d.ts +2 -0
- package/dist/test/surgery-marks-receipt.test.d.ts.map +1 -0
- package/dist/test/surgery-marks-receipt.test.js +540 -0
- package/dist/test/surgery-marks-receipt.test.js.map +1 -0
- package/dist/test/tool-batch-hook.test.js.map +1 -1
- package/dist/test/tool-result-guard.test.js +352 -16
- 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 +1022 -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 +415 -67
- package/src/api/mcp-server-core.ts +681 -0
- package/src/api/mcp-server.ts +16 -685
- package/src/api/server.ts +87 -26
- package/src/api/types.ts +9 -1
- package/src/framework.ts +3389 -782
- package/src/index.ts +9 -4
- package/src/mcpl/channel-registry.ts +126 -31
- package/src/mcpl/feature-set-manager.ts +4 -3
- package/src/mcpl/index.ts +2 -0
- 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/reference-fetcher.ts +18 -8
- package/src/mcpl/references.ts +57 -13
- package/src/mcpl/refusal-lines.ts +59 -0
- package/src/mcpl/server-connection.ts +125 -15
- package/src/mcpl/tool-classes.ts +3 -2
- package/src/mcpl/transport.ts +105 -14
- package/src/mcpl/types.ts +5 -1
- package/src/module-registry.ts +17 -9
- package/src/modules/discord/index.ts +4 -2
- package/src/modules/workspace/disk-agreement.ts +74 -18
- package/src/modules/workspace/index.ts +85 -15
- package/src/modules/workspace/observe.ts +295 -43
- package/src/modules/workspace/reconcile.ts +90 -12
- package/src/modules/workspace/types.ts +26 -1
- package/src/operator-log.ts +30 -0
- package/src/provider-waits.ts +523 -0
- package/src/recovery/offline-branch.ts +32 -28
- package/src/refusal-reactions.ts +14 -12
- package/src/stored-removal.ts +126 -0
- package/src/tool-result-guard.ts +72 -24
- package/src/types/agent.ts +42 -6
- package/src/types/events.ts +12 -0
- package/src/types/framework.ts +10 -0
- package/src/types/module.ts +20 -8
- package/src/types/trace.ts +76 -14
|
@@ -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']) {
|
|
@@ -509,8 +515,6 @@ describe('explicit prose routing', () => {
|
|
|
509
515
|
const framework = await createFramework(mode);
|
|
510
516
|
const routed = stubRegistry(framework);
|
|
511
517
|
const skip = answerSkipReply(framework);
|
|
512
|
-
// Count turns before feedTurn subscribes: a listener that unsubscribes
|
|
513
|
-
// during an event hides that event from the listener after it.
|
|
514
518
|
let turns = 0;
|
|
515
519
|
framework.onTrace((e) => {
|
|
516
520
|
if (e.type === 'inference:started')
|
|
@@ -615,11 +619,11 @@ describe('explicit prose routing', () => {
|
|
|
615
619
|
// its queued delivery runs; the turn goes on in a re-inference, which
|
|
616
620
|
// starts a new stream with a fresh routing state while the earlier
|
|
617
621
|
// stream's logical turn is still open.
|
|
618
|
-
const earlier = {
|
|
622
|
+
const earlier = routingState({ continuation: when === 'already set', locus: 'world:commons' });
|
|
619
623
|
internals.turnRouting.set('assistant', earlier);
|
|
620
|
-
internals.openLogical.set('assistant', earlier
|
|
624
|
+
internals.openLogical.set('assistant', earlier);
|
|
621
625
|
if (when === 'set late') {
|
|
622
|
-
internals.proseSpeechChains.set('assistant', held.then(() => { earlier.
|
|
626
|
+
internals.proseSpeechChains.set('assistant', held.then(() => { earlier.continuation = true; }));
|
|
623
627
|
}
|
|
624
628
|
internals.pendingRequests.push({ agentName: 'assistant', reason: 'tool_results_ready', source: 'framework', timestamp: Date.now() });
|
|
625
629
|
const running = framework.runUntilIdle();
|
|
@@ -628,7 +632,7 @@ describe('explicit prose routing', () => {
|
|
|
628
632
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
629
633
|
}
|
|
630
634
|
assert.equal(turns, 1, 'the re-inference has started');
|
|
631
|
-
assert.equal(earlier.
|
|
635
|
+
assert.equal(earlier.continuation, false, 'the earlier delivery has not run as the re-inference starts');
|
|
632
636
|
release();
|
|
633
637
|
}
|
|
634
638
|
await within(running, 'the re-inference to settle');
|
|
@@ -638,7 +642,7 @@ describe('explicit prose routing', () => {
|
|
|
638
642
|
}
|
|
639
643
|
assert.deepEqual(routed.map((r) => r.text.trim()), ['carrying on']);
|
|
640
644
|
assert.equal(turns, 2, 'the re-inference completes, then the continuation it carried starts');
|
|
641
|
-
assert.equal(earlier.
|
|
645
|
+
assert.equal(earlier.continuation, false, "the check took the earlier stream's flag");
|
|
642
646
|
}
|
|
643
647
|
finally {
|
|
644
648
|
release();
|
|
@@ -661,18 +665,19 @@ describe('explicit prose routing', () => {
|
|
|
661
665
|
try {
|
|
662
666
|
// The earlier stream left its tool results pending, its logical turn
|
|
663
667
|
// still open; its delivery lands only after the re-inference has
|
|
664
|
-
// begun, into the logical turn the two share.
|
|
665
|
-
|
|
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' });
|
|
666
671
|
internals.turnRouting.set('assistant', earlier);
|
|
667
|
-
internals.openLogical.set('assistant', earlier
|
|
668
|
-
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'); }));
|
|
669
674
|
internals.pendingRequests.push({ agentName: 'assistant', reason: 'tool_results_ready', source: 'framework', timestamp: Date.now() });
|
|
670
675
|
const running = framework.runUntilIdle();
|
|
671
676
|
for (const end = Date.now() + 5000; turns < 1 && Date.now() < end;) {
|
|
672
677
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
673
678
|
}
|
|
674
679
|
assert.equal(turns, 1, 'the re-inference has started');
|
|
675
|
-
assert.deepEqual(earlier.
|
|
680
|
+
assert.deepEqual(earlier.deliveries, [], 'the earlier delivery has not landed yet');
|
|
676
681
|
release();
|
|
677
682
|
await within(running, 'the re-inference to settle');
|
|
678
683
|
assert.deepEqual(routed.map((r) => r.text.trim()), ['carrying on']);
|
|
@@ -780,7 +785,7 @@ describe('explicit prose routing', () => {
|
|
|
780
785
|
stubRegistry(framework);
|
|
781
786
|
const agent = framework.getAgent('assistant');
|
|
782
787
|
const internals = framework;
|
|
783
|
-
const origin = internals.routingOf('assistant')
|
|
788
|
+
const origin = internals.routingOf('assistant');
|
|
784
789
|
origin.continuation = true;
|
|
785
790
|
origin.deliveries.push('chan-alpha');
|
|
786
791
|
// The turn that ran the tools is open, as its fresh start left it.
|
|
@@ -809,8 +814,8 @@ describe('explicit prose routing', () => {
|
|
|
809
814
|
it("a settle closes the open logical turn only when it is the settling stream's own, and stop() clears open turns", async () => {
|
|
810
815
|
const framework = await createFramework('hybrid');
|
|
811
816
|
const internals = framework;
|
|
812
|
-
const open = { continuation: true
|
|
813
|
-
const stale =
|
|
817
|
+
const open = routingState({ continuation: true });
|
|
818
|
+
const stale = routingState();
|
|
814
819
|
internals.openLogical.set('assistant', open);
|
|
815
820
|
// A settle from a stream a newer one replaced passes its own logical
|
|
816
821
|
// turn, or none; neither closes the open one.
|
|
@@ -820,10 +825,12 @@ describe('explicit prose routing', () => {
|
|
|
820
825
|
internals.settleAgent('assistant', { stopReason: 'completed', speech: '' }, open);
|
|
821
826
|
assert.equal(internals.openLogical.has('assistant'), false, "the turn's own settle closes it");
|
|
822
827
|
internals.openLogical.set('assistant', open);
|
|
828
|
+
internals.turnRouting.set('assistant', open);
|
|
823
829
|
await framework.stop();
|
|
824
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');
|
|
825
832
|
});
|
|
826
|
-
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 () => {
|
|
827
834
|
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'quiet' }]));
|
|
828
835
|
const framework = await createFramework('hybrid');
|
|
829
836
|
stubRegistry(framework);
|
|
@@ -834,16 +841,23 @@ describe('explicit prose routing', () => {
|
|
|
834
841
|
});
|
|
835
842
|
const internals = framework;
|
|
836
843
|
try {
|
|
837
|
-
|
|
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();
|
|
838
850
|
internals.openLogical.set('assistant', open);
|
|
839
851
|
internals.pendingRequests.push({ agentName: 'assistant', reason: 'heartbeat', source: 'framework', timestamp: Date.now(), suppressProse: true });
|
|
840
852
|
// Bounded: a silent start that took the open turn's ' !' would start a
|
|
841
853
|
// continuation with no response queued, and this would wait for good.
|
|
842
854
|
await within(framework.runUntilIdle(), 'the silent turn to settle', 3000);
|
|
843
855
|
assert.equal(turns, 1);
|
|
844
|
-
|
|
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');
|
|
845
859
|
assert.equal(internals.openLogical.get('assistant'), open, 'and neither took nor closed the open one');
|
|
846
|
-
assert.
|
|
860
|
+
assert.deepEqual(open, decided(), "everything the open turn decided is untouched, its ' !' included");
|
|
847
861
|
// The next start that routes prose continues it, and its ' !' runs.
|
|
848
862
|
// (feedTurn counts starts from here: the prose turn is 1, the
|
|
849
863
|
// continuation 2.)
|
|
@@ -861,6 +875,32 @@ describe('explicit prose routing', () => {
|
|
|
861
875
|
await framework.stop();
|
|
862
876
|
}
|
|
863
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
|
+
});
|
|
864
904
|
for (const mode of ['explicit', 'hybrid']) {
|
|
865
905
|
it(`${mode}: the wake that consumes unstreamed tool results continues the turn that scheduled them`, async () => {
|
|
866
906
|
const p = mode === 'explicit' ? '>>' : '>>>';
|
|
@@ -878,9 +918,9 @@ describe('explicit prose routing', () => {
|
|
|
878
918
|
// The scheduling turn asked for a continuation and left its tool
|
|
879
919
|
// results pending; the scheduler then starts the agent with another
|
|
880
920
|
// wake, which consumes them.
|
|
881
|
-
const scheduling = {
|
|
882
|
-
internals.openLogical.set('assistant', scheduling
|
|
883
|
-
internals.turnRouting.set('assistant',
|
|
921
|
+
const scheduling = routingState({ continuation: true });
|
|
922
|
+
internals.openLogical.set('assistant', scheduling);
|
|
923
|
+
internals.turnRouting.set('assistant', routingState());
|
|
884
924
|
trigger(framework);
|
|
885
925
|
await framework.runUntilIdle();
|
|
886
926
|
for (const end = Date.now() + 5000; turns < 2 && Date.now() < end;) {
|
|
@@ -888,7 +928,7 @@ describe('explicit prose routing', () => {
|
|
|
888
928
|
await framework.runUntilIdle();
|
|
889
929
|
}
|
|
890
930
|
assert.equal(turns, 2, "the consuming turn took the scheduling turn's ' !'");
|
|
891
|
-
assert.equal(scheduling.
|
|
931
|
+
assert.equal(scheduling.continuation, false);
|
|
892
932
|
assert.equal(internals.openLogical.has('assistant'), false, 'and the turns it ran have settled');
|
|
893
933
|
}
|
|
894
934
|
finally {
|
|
@@ -924,7 +964,7 @@ describe('explicit prose routing', () => {
|
|
|
924
964
|
await within(reached, 'the queued delivery to reach routing');
|
|
925
965
|
const chain = internals.proseSpeechChains.get('assistant');
|
|
926
966
|
const routing = internals.turnRouting.get('assistant');
|
|
927
|
-
assert.equal(routing.
|
|
967
|
+
assert.equal(routing.continuation, true, "the held delivery's ' !' is on its turn's object");
|
|
928
968
|
assert.equal(internals.pendingRequests.length, 0);
|
|
929
969
|
if (boundary === 'stop')
|
|
930
970
|
await framework.stop();
|
|
@@ -934,7 +974,7 @@ describe('explicit prose routing', () => {
|
|
|
934
974
|
await within(chain, 'the queued delivery');
|
|
935
975
|
await new Promise((resolve) => setImmediate(resolve));
|
|
936
976
|
assert.equal(internals.pendingRequests.filter((r) => r.reason === 'prose-continuation').length, 0, 'the late check adds no request');
|
|
937
|
-
assert.equal(routing.
|
|
977
|
+
assert.equal(routing.continuation, false, 'it took the flag and dropped it');
|
|
938
978
|
}
|
|
939
979
|
finally {
|
|
940
980
|
release();
|
|
@@ -983,11 +1023,11 @@ describe('explicit prose routing', () => {
|
|
|
983
1023
|
// so the round's delivery can run once the next turn has begun.
|
|
984
1024
|
await internals.deliverProse(agent, '>>#alpha ! kept private', { keepPrivate: true });
|
|
985
1025
|
assert.equal(routed.length, 0, 'nothing is delivered');
|
|
986
|
-
assert.equal(internals.turnRouting.get('assistant')?.
|
|
1026
|
+
assert.equal(internals.turnRouting.get('assistant')?.continuation ?? false, false, 'and no continuation is asked for');
|
|
987
1027
|
// The same envelope outside a private round asks for one.
|
|
988
1028
|
await internals.deliverProse(agent, '>>#alpha ! sent');
|
|
989
1029
|
assert.equal(routed.length, 1);
|
|
990
|
-
assert.equal(internals.turnRouting.get('assistant')?.
|
|
1030
|
+
assert.equal(internals.turnRouting.get('assistant')?.continuation, true);
|
|
991
1031
|
await framework.stop();
|
|
992
1032
|
});
|
|
993
1033
|
it('a skip_reply round that sets the retained text aside with >>skip_reply {{unsent}} clears it', async () => {
|
|
@@ -1067,8 +1107,7 @@ describe('explicit prose routing', () => {
|
|
|
1067
1107
|
try {
|
|
1068
1108
|
for (const segments of rounds) {
|
|
1069
1109
|
for (const start of [null, 'chan-alpha']) {
|
|
1070
|
-
internals.turnRouting.set('assistant', { pin: start ?? undefined
|
|
1071
|
-
logical: { continuation: false, deliveries: [], suppressed: 0 } });
|
|
1110
|
+
internals.turnRouting.set('assistant', routingState({ pin: start ?? undefined }));
|
|
1072
1111
|
for (const segment of segments)
|
|
1073
1112
|
await internals.deliverProse(agent, segment);
|
|
1074
1113
|
const admitted = segments.flatMap((segment) => internals.admitProse(segment));
|
|
@@ -1091,9 +1130,8 @@ describe('explicit prose routing', () => {
|
|
|
1091
1130
|
try {
|
|
1092
1131
|
for (const text of cases) {
|
|
1093
1132
|
routed.length = 0;
|
|
1094
|
-
const queued =
|
|
1095
|
-
|
|
1096
|
-
const live = { ...queued, logical: { continuation: false, deliveries: [], suppressed: 0 } };
|
|
1133
|
+
const queued = routingState();
|
|
1134
|
+
const live = routingState();
|
|
1097
1135
|
gateway.turnRouting.set('assistant', live);
|
|
1098
1136
|
await gateway.deliverHybridProse(agent, text, 'chan-locus', true, queued);
|
|
1099
1137
|
assert.equal(queued.hybridHold, true, 'the supplied turn receives the hold');
|
|
@@ -1117,17 +1155,16 @@ describe('explicit prose routing', () => {
|
|
|
1117
1155
|
try {
|
|
1118
1156
|
for (const prefix of ['>>>', '>>> ', '>>>\t']) {
|
|
1119
1157
|
routed.length = 0;
|
|
1120
|
-
const queued =
|
|
1121
|
-
|
|
1122
|
-
const live = { ...queued, logical: { continuation: false, deliveries: [], suppressed: 0 } };
|
|
1158
|
+
const queued = routingState();
|
|
1159
|
+
const live = routingState();
|
|
1123
1160
|
gateway.turnRouting.set('assistant', live);
|
|
1124
1161
|
await gateway.deliverHybridProse(agent, '>>>#alpha first', 'chan-locus', true, queued);
|
|
1125
1162
|
await gateway.deliverHybridProse(agent, prefix, 'chan-locus', true, queued);
|
|
1126
1163
|
await gateway.deliverHybridProse(agent, 'secret', 'chan-locus', true, queued);
|
|
1127
|
-
assert.deepEqual(queued.
|
|
1128
|
-
assert.equal(queued.
|
|
1129
|
-
assert.deepEqual(live.
|
|
1130
|
-
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');
|
|
1131
1168
|
const router = new ProseStreamRouter({ mode: 'hybrid', initialTarget: 'chan-locus', resolve });
|
|
1132
1169
|
const streamed = [
|
|
1133
1170
|
...router.feed('>>>#alpha first'), ...router.endSegment(),
|
|
@@ -1392,5 +1429,287 @@ describe('explicit prose routing', () => {
|
|
|
1392
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');
|
|
1393
1430
|
await framework.stop();
|
|
1394
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
|
+
}
|
|
1395
1714
|
});
|
|
1396
1715
|
//# sourceMappingURL=explicit-prose-routing.test.js.map
|