@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
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export * from './types/index.js';
|
|
3
3
|
|
|
4
4
|
// Core classes
|
|
5
|
-
export { AgentFramework, BudgetPreflightError, ResumeBlockedError } from './framework.js';
|
|
5
|
+
export { AgentFramework, BudgetPreflightError, ProviderWaitNotSleptError, ResumeBlockedError } from './framework.js';
|
|
6
6
|
export type { RuntimeSettingsPreview, HostModeStatus } from './framework.js';
|
|
7
7
|
export { Agent } from './agent.js';
|
|
8
8
|
export type { StartStreamResult } from './agent.js';
|
|
@@ -17,7 +17,7 @@ export * from './modules/index.js';
|
|
|
17
17
|
|
|
18
18
|
// API
|
|
19
19
|
export { ApiServer } from './api/server.js';
|
|
20
|
-
export { McpServer } from './api/mcp-server.js';
|
|
20
|
+
export { McpServer } from './api/mcp-server-core.js';
|
|
21
21
|
export type {
|
|
22
22
|
ApiServerConfig,
|
|
23
23
|
ApiMessage,
|
|
@@ -37,8 +37,10 @@ import type { McplServerRegistry } from './server-registry.js';
|
|
|
37
37
|
import type { FeatureSetManager } from './feature-set-manager.js';
|
|
38
38
|
import type { ToolDefinition, ToolResult, ProcessEvent } from '../types/index.js';
|
|
39
39
|
import { expandCoreTags } from './tags.js';
|
|
40
|
-
import { EmptyContentError, validateCoalescedContent } from './push-coalescer.js';
|
|
40
|
+
import { coalesceRejectionReason, EmptyContentError, validateCoalescedContent } from './push-coalescer.js';
|
|
41
41
|
import { isVisiblyEmptyContent } from './visible-content.js';
|
|
42
|
+
import { logDetail, logValue } from './log-value.js';
|
|
43
|
+
import { RefusalLines } from './refusal-lines.js';
|
|
42
44
|
import { CapabilityGrant } from './capability-grant.js';
|
|
43
45
|
|
|
44
46
|
// ============================================================================
|
|
@@ -249,7 +251,9 @@ interface McplChannelIncomingEvent {
|
|
|
249
251
|
function convertBlock(block: McplContentBlock): ContentBlock {
|
|
250
252
|
switch (block.type) {
|
|
251
253
|
case 'text':
|
|
252
|
-
|
|
254
|
+
// Text that isn't a string is no text, as the request sanitizer and
|
|
255
|
+
// the visible-content check already read it.
|
|
256
|
+
return { type: 'text', text: typeof block.text === 'string' ? block.text : '' };
|
|
253
257
|
|
|
254
258
|
case 'image':
|
|
255
259
|
if (isInlineContradiction(block)) {
|
|
@@ -257,24 +261,24 @@ function convertBlock(block: McplContentBlock): ContentBlock {
|
|
|
257
261
|
// closed, withhold the data (checked BEFORE the data branch).
|
|
258
262
|
return { type: 'text', text: INLINE_WITHHELD_TEXT };
|
|
259
263
|
}
|
|
260
|
-
if (block.uri && block.disposition) {
|
|
264
|
+
if (isFilledString(block.uri) && block.disposition) {
|
|
261
265
|
// RFC-005 uri-form media with a disposition claim: stub, do not
|
|
262
266
|
// hand the URI to the provider or inline it.
|
|
263
267
|
return { type: 'text', text: referenceStubOrNull(block, 'attachment on channel message') ?? '[reference]' };
|
|
264
268
|
}
|
|
265
|
-
if (block.data && block.mimeType) {
|
|
269
|
+
if (isFilledString(block.data) && isFilledString(block.mimeType)) {
|
|
266
270
|
return {
|
|
267
271
|
type: 'image',
|
|
268
272
|
source: { type: 'base64', data: block.data, mediaType: block.mimeType },
|
|
269
273
|
} as ContentBlock;
|
|
270
274
|
}
|
|
271
|
-
if (block.uri) {
|
|
275
|
+
if (isFilledString(block.uri)) {
|
|
272
276
|
return {
|
|
273
277
|
type: 'image',
|
|
274
278
|
source: { type: 'url', url: block.uri },
|
|
275
279
|
} as ContentBlock;
|
|
276
280
|
}
|
|
277
|
-
return { type: 'text', text: '[Image: no data]' };
|
|
281
|
+
return { type: 'text', text: isFilledString(block.data) ? '[Image: no media type]' : '[Image: no data]' };
|
|
278
282
|
|
|
279
283
|
case 'audio':
|
|
280
284
|
if (isInlineContradiction(block)) {
|
|
@@ -282,31 +286,43 @@ function convertBlock(block: McplContentBlock): ContentBlock {
|
|
|
282
286
|
// closed, withhold the data (checked BEFORE the data branch).
|
|
283
287
|
return { type: 'text', text: INLINE_WITHHELD_TEXT };
|
|
284
288
|
}
|
|
285
|
-
if (block.uri && block.disposition) {
|
|
289
|
+
if (isFilledString(block.uri) && block.disposition) {
|
|
286
290
|
// RFC-005 uri-form media with a disposition claim: stub, do not
|
|
287
291
|
// hand the URI to the provider or inline it.
|
|
288
292
|
return { type: 'text', text: referenceStubOrNull(block, 'attachment on channel message') ?? '[reference]' };
|
|
289
293
|
}
|
|
290
|
-
if (block.data && block.mimeType) {
|
|
294
|
+
if (isFilledString(block.data) && isFilledString(block.mimeType)) {
|
|
291
295
|
return {
|
|
292
296
|
type: 'audio',
|
|
293
297
|
source: { type: 'base64', data: block.data, mediaType: block.mimeType },
|
|
294
298
|
} as ContentBlock;
|
|
295
299
|
}
|
|
296
|
-
return { type: 'text', text: '[Audio: no data]' };
|
|
300
|
+
return { type: 'text', text: isFilledString(block.data) ? '[Audio: no media type]' : '[Audio: no data]' };
|
|
297
301
|
|
|
298
302
|
case 'resource':
|
|
299
303
|
// RFC-005: reference blocks become bounded stubs — never raw URIs
|
|
300
304
|
// (a signed URL is a bearer credential that looks like a location).
|
|
301
305
|
return { type: 'text', text: referenceStubOrNull(block, 'attachment on channel message') ?? '[reference]' };
|
|
302
306
|
|
|
303
|
-
default:
|
|
307
|
+
default: {
|
|
304
308
|
// Unknown wire block types previously fell off the exhaustive switch
|
|
305
|
-
// and propagated `undefined` into ContentBlock[]. Fail visibly.
|
|
306
|
-
|
|
309
|
+
// and propagated `undefined` into ContentBlock[]. Fail visibly. A type
|
|
310
|
+
// that isn't a string is no type, and is never converted to one: an
|
|
311
|
+
// object can throw when it is.
|
|
312
|
+
const type: unknown = (block as { type?: unknown }).type;
|
|
313
|
+
return { type: 'text', text: `[unrecognized content block: ${typeof type === 'string' ? type : 'untyped'}]` };
|
|
314
|
+
}
|
|
307
315
|
}
|
|
308
316
|
}
|
|
309
317
|
|
|
318
|
+
/** A string with something in it: the only data, mimeType or uri a block's
|
|
319
|
+
* source is built from, and the only uri that makes it a reference. Anything
|
|
320
|
+
* else a sender puts there leaves the block without it, so conversion never
|
|
321
|
+
* throws and never copies a non-string. */
|
|
322
|
+
function isFilledString(value: unknown): value is string {
|
|
323
|
+
return typeof value === 'string' && value !== '';
|
|
324
|
+
}
|
|
325
|
+
|
|
310
326
|
// ============================================================================
|
|
311
327
|
// Channel Tool Definitions
|
|
312
328
|
// ============================================================================
|
|
@@ -512,17 +528,19 @@ interface ChannelRegistryOptions {
|
|
|
512
528
|
*/
|
|
513
529
|
homeChannelResolver?: (agentName: string) => string | undefined;
|
|
514
530
|
/**
|
|
515
|
-
* Resolve
|
|
516
|
-
*
|
|
531
|
+
* Resolve an agent's channel for its CURRENT inference turn, by agent name:
|
|
532
|
+
* the turn's locus once it has one, else the channel that triggered the
|
|
533
|
+
* turn. This is the fix for single-TRUNK agents (the only mode
|
|
517
534
|
* connectome-host runs — it never exposes conversation forks). A trunk agent
|
|
518
535
|
* has no fork home, so without this its plain-text speech falls back to the
|
|
519
536
|
* process-global `defaultPublishChannel`, which tracks the most-recent inbound
|
|
520
537
|
* across ALL channels and misroutes a reply to whichever channel last spoke
|
|
521
|
-
* under concurrency (item-3 redux). The framework
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
* global fallback
|
|
525
|
-
* wins), BEFORE
|
|
538
|
+
* under concurrency (item-3 redux). The framework keeps both on the live
|
|
539
|
+
* turn's routing state and exposes them here. For a heartbeat / no-trigger
|
|
540
|
+
* turn it resolves to undefined until the turn's locus is resolved, so that
|
|
541
|
+
* resolution takes the global fallback, which the turn then keeps.
|
|
542
|
+
* Consulted AFTER `homeChannelResolver` (a fork's home always wins), BEFORE
|
|
543
|
+
* `defaultPublishChannel`.
|
|
526
544
|
*/
|
|
527
545
|
activeChannelResolver?: (agentName: string) => string | undefined;
|
|
528
546
|
}
|
|
@@ -577,6 +595,10 @@ export class ChannelRegistry {
|
|
|
577
595
|
/** Registered channels, keyed by `{serverId}:{channelId}`. */
|
|
578
596
|
private channels = new Map<string, ChannelEntry>();
|
|
579
597
|
|
|
598
|
+
/** Holds back repeats of the line for a message refused because its
|
|
599
|
+
* channel isn't registered (RefusalLines). */
|
|
600
|
+
private unknownChannelLines = new RefusalLines();
|
|
601
|
+
|
|
580
602
|
/**
|
|
581
603
|
* Most-recently-known label per channelId, replayed from
|
|
582
604
|
* `CHANNEL_LABEL_HISTORY_LOG_ID` (last sighting wins) and kept current by
|
|
@@ -848,6 +870,45 @@ export class ChannelRegistry {
|
|
|
848
870
|
});
|
|
849
871
|
}
|
|
850
872
|
|
|
873
|
+
/**
|
|
874
|
+
* A coalesced message the host refused. Until now the host said nothing, so
|
|
875
|
+
* a dropped message (agent-framework#266: every Discord message with a
|
|
876
|
+
* screenshot) left no trace where operators look. The sender gets the
|
|
877
|
+
* per-message result this lane gives: `coalesce_invalid` for a malformed
|
|
878
|
+
* item, or the thrown error's message for anything else. `answered` is the
|
|
879
|
+
* code that result stands for (-32602 for `coalesce_invalid`), and the
|
|
880
|
+
* reason follows it (coalesceRejectionReason): `coalesce-failed` when the
|
|
881
|
+
* host itself failed, `coalesce-invalid` when the sender's item was
|
|
882
|
+
* refused, with the failing `field` and the error's message as `detail`.
|
|
883
|
+
* The line prints the ids the sender sent, whatever their type; the trace
|
|
884
|
+
* carries them when they are strings.
|
|
885
|
+
*/
|
|
886
|
+
private traceCoalesceRejection(
|
|
887
|
+
serverId: string,
|
|
888
|
+
message: { channelId?: unknown; messageId?: unknown } | null | undefined,
|
|
889
|
+
error: { field?: unknown; message: string },
|
|
890
|
+
answered: number,
|
|
891
|
+
): void {
|
|
892
|
+
const reason = coalesceRejectionReason(answered);
|
|
893
|
+
const field = typeof error.field === 'string' && error.field ? error.field : undefined;
|
|
894
|
+
const channelId = typeof message?.channelId === 'string' ? message.channelId : '';
|
|
895
|
+
const messageId = typeof message?.messageId === 'string' && message.messageId ? message.messageId : undefined;
|
|
896
|
+
console.error(
|
|
897
|
+
`[channel-incoming-rejected] server=${serverId} channel=${logValue(message?.channelId ?? '')} ` +
|
|
898
|
+
`messageId=${logValue(message?.messageId ?? '')} ` +
|
|
899
|
+
`reason=${reason}${field ? ` field=${logValue(field)}` : ''}: ${logDetail(error.message)}`,
|
|
900
|
+
);
|
|
901
|
+
this.emitTraceFn({
|
|
902
|
+
type: 'mcpl:channel-incoming-rejected',
|
|
903
|
+
serverId,
|
|
904
|
+
channelId,
|
|
905
|
+
...(messageId ? { messageId } : {}),
|
|
906
|
+
reason,
|
|
907
|
+
...(field ? { field } : {}),
|
|
908
|
+
detail: error.message,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
|
|
851
912
|
/**
|
|
852
913
|
* Handle `channels/incoming` from a server.
|
|
853
914
|
*
|
|
@@ -864,10 +925,20 @@ export class ChannelRegistry {
|
|
|
864
925
|
for (const message of params.messages) {
|
|
865
926
|
if (!message || typeof message.channelId !== 'string' || !message.channelId
|
|
866
927
|
|| typeof message.messageId !== 'string' || !message.messageId) {
|
|
928
|
+
// The same gate as admission below: with no coalescer wired, a
|
|
929
|
+
// message carrying `coalesce` is handled uncoalesced, and refused as
|
|
930
|
+
// any uncoalesced message without an identity is.
|
|
931
|
+
const coalescedItem = message?.coalesce !== undefined && !!this.handleCoalescedIncoming;
|
|
932
|
+
if (coalescedItem) {
|
|
933
|
+
this.traceCoalesceRejection(serverId, message, {
|
|
934
|
+
field: typeof message.channelId !== 'string' || !message.channelId ? 'channelId' : 'messageId',
|
|
935
|
+
message: 'a coalesced message needs a channelId and a messageId',
|
|
936
|
+
}, -32602);
|
|
937
|
+
}
|
|
867
938
|
results.push({
|
|
868
939
|
messageId: typeof message?.messageId === 'string' ? message.messageId : '',
|
|
869
940
|
accepted: false,
|
|
870
|
-
reason:
|
|
941
|
+
reason: coalescedItem ? 'coalesce_invalid' : 'invalid channel/message identity',
|
|
871
942
|
});
|
|
872
943
|
continue;
|
|
873
944
|
}
|
|
@@ -899,11 +970,24 @@ export class ChannelRegistry {
|
|
|
899
970
|
// routable entry rather than an open self-attested one.
|
|
900
971
|
const incomingKey = `${serverId}:${message.channelId}`;
|
|
901
972
|
if (!this.channels.has(incomingKey)) {
|
|
973
|
+
// Loud, coalesced or not, like every other refusal of a message, but
|
|
974
|
+
// at most once a minute per channel (RefusalLines): a server that
|
|
975
|
+
// never registered a busy channel would otherwise print a line for
|
|
976
|
+
// every message in it. Every refusal is traced.
|
|
977
|
+
const held = this.unknownChannelLines.admit(serverId, message.channelId);
|
|
978
|
+
if (held !== null) {
|
|
979
|
+
console.error(
|
|
980
|
+
`[channel-incoming-rejected] server=${serverId} channel=${logValue(message.channelId)} ` +
|
|
981
|
+
`messageId=${logValue(message.messageId)} reason=unknown-channel ` +
|
|
982
|
+
`(not registered, §14.5${held ? `; ${held} more since the last line` : ''})`,
|
|
983
|
+
);
|
|
984
|
+
}
|
|
902
985
|
this.emitTraceFn({
|
|
903
986
|
type: 'mcpl:channel-incoming-rejected',
|
|
904
987
|
serverId,
|
|
905
988
|
channelId: message.channelId,
|
|
906
|
-
|
|
989
|
+
messageId: message.messageId,
|
|
990
|
+
reason: 'unknown-channel',
|
|
907
991
|
});
|
|
908
992
|
results.push({
|
|
909
993
|
messageId: message.messageId,
|
|
@@ -921,7 +1005,7 @@ export class ChannelRegistry {
|
|
|
921
1005
|
// shows the model nothing would wake it with no visible cause.
|
|
922
1006
|
const emptyAllowed = coalesced && (message.coalesce as { retract?: unknown } | null)?.retract === true;
|
|
923
1007
|
const rejectEmpty = () => {
|
|
924
|
-
console.error(`[channel-incoming-rejected] server=${serverId} channel=${message.channelId} messageId=${message.messageId} reason=empty-content`);
|
|
1008
|
+
console.error(`[channel-incoming-rejected] server=${serverId} channel=${logValue(message.channelId)} messageId=${logValue(message.messageId)} reason=empty-content`);
|
|
925
1009
|
this.emitTraceFn({
|
|
926
1010
|
type: 'mcpl:channel-incoming-rejected',
|
|
927
1011
|
serverId,
|
|
@@ -935,10 +1019,14 @@ export class ChannelRegistry {
|
|
|
935
1019
|
// RFC-006 §13: malformed content on a coalesced item is that item's
|
|
936
1020
|
// failure, not the batch's — check the shape before converting.
|
|
937
1021
|
try {
|
|
938
|
-
validateCoalescedContent(message.content,
|
|
1022
|
+
validateCoalescedContent(message.content, { allowEmpty: emptyAllowed });
|
|
939
1023
|
} catch (error) {
|
|
940
1024
|
if (error instanceof EmptyContentError) rejectEmpty();
|
|
941
|
-
else
|
|
1025
|
+
else {
|
|
1026
|
+
// Answered coalesce_invalid whatever the error's code: -32602.
|
|
1027
|
+
this.traceCoalesceRejection(serverId, message, error as Error & { field?: string }, -32602);
|
|
1028
|
+
results.push({ messageId: message.messageId, accepted: false, reason: 'coalesce_invalid' });
|
|
1029
|
+
}
|
|
942
1030
|
continue;
|
|
943
1031
|
}
|
|
944
1032
|
}
|
|
@@ -970,8 +1058,11 @@ export class ChannelRegistry {
|
|
|
970
1058
|
// Determine whether to trigger inference
|
|
971
1059
|
let triggerInference = true;
|
|
972
1060
|
if (this.shouldTriggerInference) {
|
|
973
|
-
|
|
974
|
-
|
|
1061
|
+
// The text the agent's context gets, as the push lane's filter reads
|
|
1062
|
+
// it: the converted text blocks, a placeholder or reference stub
|
|
1063
|
+
// among them, and a text block's non-string text as empty text.
|
|
1064
|
+
const textContent = convertedContent
|
|
1065
|
+
.filter((b): b is ContentBlock & { type: 'text' } => b.type === 'text')
|
|
975
1066
|
.map((b) => b.text)
|
|
976
1067
|
.join('\n');
|
|
977
1068
|
triggerInference = this.shouldTriggerInference(
|
|
@@ -1014,7 +1105,10 @@ export class ChannelRegistry {
|
|
|
1014
1105
|
if (result.accepted) markAccepted();
|
|
1015
1106
|
results.push(result);
|
|
1016
1107
|
} catch (error) {
|
|
1017
|
-
const err = error as Error & { code?: number };
|
|
1108
|
+
const err = error as Error & { code?: number; field?: string };
|
|
1109
|
+
// -32602 is answered coalesce_invalid; anything else as its message,
|
|
1110
|
+
// a plain Error standing for an internal error (-32603).
|
|
1111
|
+
this.traceCoalesceRejection(serverId, message, err, typeof err.code === 'number' ? err.code : -32603);
|
|
1018
1112
|
results.push({
|
|
1019
1113
|
messageId: message.messageId,
|
|
1020
1114
|
accepted: false,
|
|
@@ -1361,8 +1455,9 @@ export class ChannelRegistry {
|
|
|
1361
1455
|
|
|
1362
1456
|
// Resolve the outbound locus the SAME way routeSpeech does, so the agent is
|
|
1363
1457
|
// told the channel its speech will actually land in: a conversation fork's
|
|
1364
|
-
// home channel, else this turn's
|
|
1365
|
-
//
|
|
1458
|
+
// home channel, else this turn's channel (single trunk agent — item-3
|
|
1459
|
+
// redux: its locus, else its triggering channel), else the global default
|
|
1460
|
+
// (heartbeats). Without this, a fork
|
|
1366
1461
|
// was advertised the global locus but published somewhere else; and a trunk
|
|
1367
1462
|
// agent was told the wrong channel under concurrency.
|
|
1368
1463
|
const home = agentName ? this.homeChannelResolver?.(agentName) : undefined;
|
|
@@ -3018,7 +3113,7 @@ export class ChannelRegistry {
|
|
|
3018
3113
|
try {
|
|
3019
3114
|
await this.openChannelNow(entry, 'opened-by-delivery');
|
|
3020
3115
|
console.error(
|
|
3021
|
-
`[routeSpeech] ${conversationId}: locus ${channelId} was closed — opened by delivery (speech implies engagement)`,
|
|
3116
|
+
`[routeSpeech] ${conversationId}: locus ${logValue(channelId)} was closed — opened by delivery (speech implies engagement)`,
|
|
3022
3117
|
);
|
|
3023
3118
|
this.emitTraceFn({
|
|
3024
3119
|
type: 'mcpl:channel-opened-by-send',
|
|
@@ -3071,7 +3166,7 @@ export class ChannelRegistry {
|
|
|
3071
3166
|
return fail(channelId, `server "${entry.serverId}" reported delivered:false for "${channelId}"`);
|
|
3072
3167
|
}
|
|
3073
3168
|
|
|
3074
|
-
console.error(`[routeSpeech] ${conversationId}: routed ${text.length} chars -> ${channelId} (server=${entry.serverId}, delivered=${delivered})`);
|
|
3169
|
+
console.error(`[routeSpeech] ${conversationId}: routed ${text.length} chars -> ${logValue(channelId)} (server=${entry.serverId}, delivered=${delivered})`);
|
|
3075
3170
|
this.emitTraceFn({
|
|
3076
3171
|
type: 'mcpl:speech-routed',
|
|
3077
3172
|
conversationId,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
UNKNOWN_FEATURE_SET,
|
|
20
20
|
} from './errors.js';
|
|
21
21
|
import { isKnownCapabilityPath } from './capability-grant.js';
|
|
22
|
+
import { logValue } from './log-value.js';
|
|
22
23
|
|
|
23
24
|
// ============================================================================
|
|
24
25
|
// Error Class
|
|
@@ -222,7 +223,7 @@ export class FeatureSetManager {
|
|
|
222
223
|
): void => {
|
|
223
224
|
const selectedByConfig = state.enabled.delete(name);
|
|
224
225
|
const note = explicitSelection && selectedByConfig ? ' — overrides enabledFeatureSets' : '';
|
|
225
|
-
console.error(`[mcpl] ${serverId}/${name} disabled: ${detail}${note}`);
|
|
226
|
+
console.error(`[mcpl] ${serverId}/${logValue(name)} disabled: ${detail}${note}`);
|
|
226
227
|
this.emitTrace?.({
|
|
227
228
|
type: 'mcpl:feature-set-disabled',
|
|
228
229
|
serverId,
|
|
@@ -240,14 +241,14 @@ export class FeatureSetManager {
|
|
|
240
241
|
}
|
|
241
242
|
const unknown = uses.filter((u) => typeof u !== 'string' || !isKnownCapabilityPath(u));
|
|
242
243
|
if (unknown.length > 0) {
|
|
243
|
-
disable(name, 'invalid_uses', `invalid_uses (§6.4 — unrecognized: ${unknown.join(', ')})`, {
|
|
244
|
+
disable(name, 'invalid_uses', `invalid_uses (§6.4 — unrecognized: ${unknown.map(logValue).join(', ')})`, {
|
|
244
245
|
unrecognized: unknown.map((u) => (typeof u === 'string' ? u : JSON.stringify(u))),
|
|
245
246
|
});
|
|
246
247
|
continue;
|
|
247
248
|
}
|
|
248
249
|
const missing = (uses as string[]).filter((u) => !grant.has(u));
|
|
249
250
|
if (missing.length > 0) {
|
|
250
|
-
disable(name, 'missing_capabilities', `missing capabilities ${missing.join(', ')} (§6.4)`, { missing });
|
|
251
|
+
disable(name, 'missing_capabilities', `missing capabilities ${missing.map(logValue).join(', ')} (§6.4)`, { missing });
|
|
251
252
|
}
|
|
252
253
|
}
|
|
253
254
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A value a connected server chose, as one field of a host log line.
|
|
3
|
+
*
|
|
4
|
+
* The rejection lines (`[channel-incoming-rejected]`, `[push-event-rejected]`)
|
|
5
|
+
* and the other host lines that name a server's own identifiers are read a
|
|
6
|
+
* line at a time, by eye and with grep. Printed raw, an identifier carrying a
|
|
7
|
+
* line break prints a second line that reads as another host record, about
|
|
8
|
+
* another server; one carrying a space can pose as another field of the same
|
|
9
|
+
* line; and one carrying `=` can hold a lookalike field (`reason=…`) that a
|
|
10
|
+
* search for that field finds before the real one.
|
|
11
|
+
*
|
|
12
|
+
* A plain token (visible characters only: no space, quote, backslash or `=`)
|
|
13
|
+
* prints as it is, so ordinary ids read and grep as they always have.
|
|
14
|
+
* Anything else prints as a JSON string with every `=` and every invisible
|
|
15
|
+
* character escaped: controls, format and bidi characters, line and paragraph
|
|
16
|
+
* separators, every space (the plain one included), default-ignorable
|
|
17
|
+
* characters (Hangul fillers, the combining grapheme joiner, variation
|
|
18
|
+
* selectors) and the blank Braille pattern. So it stays one field on one line,
|
|
19
|
+
* a reader that splits on whitespace keeps it whole, no field holds a raw
|
|
20
|
+
* `=` (so every `=` before a line's free-text tail, `logDetail`, belongs to
|
|
21
|
+
* a real field), and `JSON.parse` of a quoted value that isn't cut gives
|
|
22
|
+
* back exactly what was sent. Escaping rather than stripping keeps two
|
|
23
|
+
* identifiers that differ in invisible characters within the units a line
|
|
24
|
+
* prints from printing alike; canonically equivalent or lookalike
|
|
25
|
+
* identifiers still can.
|
|
26
|
+
*
|
|
27
|
+
* A value longer than 256 UTF-16 units is cut, so one value can't make a line
|
|
28
|
+
* long: it prints quoted, as its first 256 units (255 when the 256th is a
|
|
29
|
+
* high surrogate, so a pair is never split), each at most six characters once
|
|
30
|
+
* escaped, followed by `…(N)`, N being its full length. `JSON.parse` of the
|
|
31
|
+
* quoted part gives back those first units. A cut value always opens with a
|
|
32
|
+
* quote, and its marker follows the closing quote. A plain token never holds
|
|
33
|
+
* a quote, and a quoted value that isn't cut ends at its closing quote, so
|
|
34
|
+
* only a value that was cut prints as one.
|
|
35
|
+
*
|
|
36
|
+
* A value that isn't a string prints as its JSON text, quoted as any text is
|
|
37
|
+
* that isn't a plain token: the number 5 prints as `5`, and `{"toString":1}`
|
|
38
|
+
* as `"{\"toString\":1}"`. So the number 5 and the string "5" print alike, as
|
|
39
|
+
* they name the same message. A number JSON writes as `null` (NaN, Infinity)
|
|
40
|
+
* prints as `String()` of it. Converting a value never throws: one JSON can't
|
|
41
|
+
* write prints as `String()` of it, or else as its object tag, and one none
|
|
42
|
+
* of those can read (a revoked Proxy) as `[unprintable]`.
|
|
43
|
+
*/
|
|
44
|
+
export function logValue(value: unknown): string {
|
|
45
|
+
return printed(textOf(value), FIELD);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The host's own message, as the free text that ends a log line after its
|
|
50
|
+
* fields: `reason=coalesce-invalid field=payload.content: "content must be an
|
|
51
|
+
* array"`. It prints as `logValue` prints a field, cut the same way, except
|
|
52
|
+
* that a plain space and `=` stay raw, for reading. That is safe because the
|
|
53
|
+
* text is the host's (an error the host raised), not an id a sender chose,
|
|
54
|
+
* and every real field of the line comes before it.
|
|
55
|
+
*/
|
|
56
|
+
export function logDetail(value: unknown): string {
|
|
57
|
+
return printed(textOf(value), DETAIL);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** How a form prints: the text that stands as it is, and what it escapes. */
|
|
61
|
+
interface Form {
|
|
62
|
+
plain: RegExp;
|
|
63
|
+
escaped: RegExp;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const FIELD: Form = {
|
|
67
|
+
plain: /^[^"\\=\p{C}\p{Z}\p{Default_Ignorable_Code_Point}\u2800]+$/u,
|
|
68
|
+
escaped: /[=\p{C}\p{Z}\p{Default_Ignorable_Code_Point}\u2800]/gu,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const DETAIL: Form = {
|
|
72
|
+
plain: /^[^"\\\p{C}\p{Z}\p{Default_Ignorable_Code_Point}\u2800]+$/u,
|
|
73
|
+
escaped: /(?! )[\p{C}\p{Z}\p{Default_Ignorable_Code_Point}\u2800]/gu,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** The most of a value's text a line prints, in UTF-16 units. */
|
|
77
|
+
const MAX_PRINTED = 256;
|
|
78
|
+
|
|
79
|
+
function printed(text: string, form: Form): string {
|
|
80
|
+
if (text.length <= MAX_PRINTED) return form.plain.test(text) ? text : quoted(text, form);
|
|
81
|
+
let end = MAX_PRINTED;
|
|
82
|
+
const last = text.charCodeAt(end - 1);
|
|
83
|
+
if (last >= 0xd800 && last <= 0xdbff) end--;
|
|
84
|
+
return `${quoted(text.slice(0, end), form)}…(${text.length})`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function quoted(text: string, form: Form): string {
|
|
88
|
+
// JSON escapes C0 controls, quotes and backslashes and leaves the rest of
|
|
89
|
+
// the escaped set raw: escape those here, per UTF-16 unit, as JSON does.
|
|
90
|
+
return JSON.stringify(text).replace(form.escaped, escapeUnits);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function textOf(value: unknown): string {
|
|
94
|
+
if (typeof value === 'string') return value;
|
|
95
|
+
// JSON writes a non-finite number as null, which reads as a missing value.
|
|
96
|
+
if (typeof value === 'number' && !Number.isFinite(value)) return String(value);
|
|
97
|
+
const readers = [
|
|
98
|
+
() => JSON.stringify(value) ?? String(value),
|
|
99
|
+
() => String(value),
|
|
100
|
+
() => Object.prototype.toString.call(value),
|
|
101
|
+
];
|
|
102
|
+
for (const read of readers) {
|
|
103
|
+
try {
|
|
104
|
+
return read();
|
|
105
|
+
} catch {
|
|
106
|
+
// the next reader
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return '[unprintable]';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function escapeUnits(chars: string): string {
|
|
113
|
+
let out = '';
|
|
114
|
+
for (let i = 0; i < chars.length; i++) out += `\\u${chars.charCodeAt(i).toString(16).padStart(4, '0')}`;
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
*
|
|
22
22
|
* Three modes, mirroring proseRouting:
|
|
23
23
|
* - explicit: parse `>>` prefixes. A fresh turn's stream starts suppressed;
|
|
24
|
-
*
|
|
25
|
-
* each round's end returns the stream to it (retarget).
|
|
26
|
-
* - hybrid: parse `>>>` envelopes, with the
|
|
24
|
+
* a later stream of the turn starts at the turn's destination,
|
|
25
|
+
* and each round's end returns the stream to it (retarget).
|
|
26
|
+
* - hybrid: parse `>>>` envelopes, with the turn's locus as the
|
|
27
27
|
* fallback. An envelope with no target, or `>>>skip_reply`,
|
|
28
28
|
* holds back what follows until a later `>>>` names a target.
|
|
29
29
|
* - locus: NO prefix parsing (locus mode's `>>` lines are plain text);
|
|
30
|
-
* everything streams to the
|
|
30
|
+
* everything streams to the turn's locus.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
export interface RoutedDelta {
|
|
@@ -52,9 +52,9 @@ export class ProseStreamRouter {
|
|
|
52
52
|
private opts: {
|
|
53
53
|
/** explicit parses `>>`; hybrid parses `>>>` while retaining locus fallback. */
|
|
54
54
|
mode: 'explicit' | 'hybrid' | 'locus';
|
|
55
|
-
/** Initial destination: the
|
|
55
|
+
/** Initial destination: the turn's locus (locus and hybrid mode); in
|
|
56
56
|
* explicit mode, null on a fresh turn, or the turn's destination on a
|
|
57
|
-
* stream
|
|
57
|
+
* later stream of the turn. */
|
|
58
58
|
initialTarget: string | null;
|
|
59
59
|
/** Resolve a `>>spec` to a channelId, or null (unresolved/ambiguous). */
|
|
60
60
|
resolve: (spec: string) => string | null;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* bookkeeping and is unit-tested without a framework.
|
|
19
19
|
*/
|
|
20
20
|
import type { McplContentBlock, PushEventResult } from './types.js';
|
|
21
|
+
import { classifyBlock, isInlineContradiction } from './references.js';
|
|
21
22
|
import { isVisiblyEmptyContent } from './visible-content.js';
|
|
22
23
|
|
|
23
24
|
export const PUSH_COALESCING_SUPPORT = {
|
|
@@ -37,6 +38,18 @@ export class CoalesceError extends Error {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* How the host names a coalesce rejection, from the code the wire answered
|
|
43
|
+
* with. `coalesce-failed` is the host's own failure: -32000 (coalescing
|
|
44
|
+
* unavailable, the host stopping, or an acceptance it couldn't make durable)
|
|
45
|
+
* and -32603 (an internal error). `coalesce-invalid` is every other answer,
|
|
46
|
+
* the sender's: -32602 for a malformed item, -32017 or -32023 for a channel
|
|
47
|
+
* that isn't permitted or isn't registered.
|
|
48
|
+
*/
|
|
49
|
+
export function coalesceRejectionReason(answered: number): 'coalesce-failed' | 'coalesce-invalid' {
|
|
50
|
+
return answered === -32000 || answered === -32603 ? 'coalesce-failed' : 'coalesce-invalid';
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
/** Content with nothing model-visible (see visible-content.ts): -32602. */
|
|
41
54
|
export class EmptyContentError extends CoalesceError {
|
|
42
55
|
readonly reason = 'empty-content';
|
|
@@ -212,7 +225,6 @@ export interface CoalescerOptions {
|
|
|
212
225
|
recoveryBackoffMs?: number;
|
|
213
226
|
maxSubjects?: number;
|
|
214
227
|
maxNotices?: number;
|
|
215
|
-
maxContentBytes?: number;
|
|
216
228
|
maxDataBytes?: number;
|
|
217
229
|
}
|
|
218
230
|
|
|
@@ -232,6 +244,32 @@ function mergeIdentity(prior: CoalescedOccurrence['identity'], next: CoalescedOc
|
|
|
232
244
|
/**
|
|
233
245
|
* Validate wire content before it is stored, rendered or converted.
|
|
234
246
|
*
|
|
247
|
+
* Its shape and its visible emptiness, nothing else. An incoming coalesced
|
|
248
|
+
* occurrence reaches context through the converters an uncoalesced message
|
|
249
|
+
* goes through: a plain occurrence the moment it is admitted (the coalescer
|
|
250
|
+
* only remembers where it landed), a deferred one when its render settles or
|
|
251
|
+
* falls back to it. Its content must be an array of objects. A block the
|
|
252
|
+
* converters can't render (a video, an unknown type, an image with neither
|
|
253
|
+
* uri nor data) becomes a placeholder, as it does uncoalesced, and so does a
|
|
254
|
+
* malformed one, since the converters take any object without throwing: a
|
|
255
|
+
* type, text, data, mimeType or uri that isn't a string counts as absent, a
|
|
256
|
+
* text block's as no text. The uncoalesced lane doesn't check the array:
|
|
257
|
+
* there a member that isn't an object becomes the untyped placeholder, and a
|
|
258
|
+
* null member or content that isn't an array throws in conversion
|
|
259
|
+
* (agent-framework#297 brings that lane to this rule).
|
|
260
|
+
* Nor does the uncoalesced path set a byte budget on content:
|
|
261
|
+
* an inline image is admitted here as it is there (agent-framework#266: a
|
|
262
|
+
* 1 MiB serialized-content budget here dropped every coalesced message
|
|
263
|
+
* carrying a screenshot). A deferred occurrence's content is held, until then,
|
|
264
|
+
* in the coalescer's state and its persisted snapshot and receipts.
|
|
265
|
+
*
|
|
266
|
+
* A render result, which the host delivers in the occurrence's place, gets
|
|
267
|
+
* these checks, as RFC-006 §5.2 gives it every check a pushed payload gets,
|
|
268
|
+
* and one more (`checkBlockShapes`): each block must be one the converters
|
|
269
|
+
* show as itself or as a reference stub, not as a placeholder. A malformed
|
|
270
|
+
* render falls back to the occurrence's own content; a pushed payload has
|
|
271
|
+
* nothing to fall back to, and gets its placeholders.
|
|
272
|
+
*
|
|
235
273
|
* Visibly-empty content is refused unless `allowEmpty`: an admitted
|
|
236
274
|
* occurrence with nothing to show would wake the model with no visible cause.
|
|
237
275
|
* Empty is legitimate only where the RFC gives it a meaning — a retraction
|
|
@@ -240,22 +278,38 @@ function mergeIdentity(prior: CoalescedOccurrence['identity'], next: CoalescedOc
|
|
|
240
278
|
*/
|
|
241
279
|
export function validateCoalescedContent(
|
|
242
280
|
content: unknown,
|
|
243
|
-
|
|
244
|
-
options: { allowEmpty?: boolean } = {},
|
|
281
|
+
options: { allowEmpty?: boolean; checkBlockShapes?: boolean } = {},
|
|
245
282
|
): asserts content is McplContentBlock[] {
|
|
246
283
|
if (!Array.isArray(content)) throw new CoalesceError('payload.content', 'content must be an array');
|
|
247
284
|
for (const b of content as Array<Record<string, unknown>>) {
|
|
248
285
|
if (!b || typeof b !== 'object' || Array.isArray(b)) throw new CoalesceError('payload.content', 'invalid content block');
|
|
249
|
-
if (
|
|
250
|
-
if (b.type === 'resource' && typeof b.uri === 'string') continue;
|
|
251
|
-
if ((b.type === 'image' || b.type === 'audio')
|
|
252
|
-
&& (typeof b.uri === 'string' || (typeof b.data === 'string' && typeof b.mimeType === 'string'))) continue;
|
|
253
|
-
throw new CoalesceError('payload.content', 'invalid content block');
|
|
286
|
+
if (options.checkBlockShapes && !showsAsItself(b)) throw new CoalesceError('payload.content', 'invalid content block');
|
|
254
287
|
}
|
|
255
|
-
if (Buffer.byteLength(JSON.stringify(content)) > maxBytes) throw new CoalesceError('payload.content', 'content exceeds host byte limit');
|
|
256
288
|
if (!options.allowEmpty && isVisiblyEmptyContent(content)) throw new EmptyContentError();
|
|
257
289
|
}
|
|
258
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Whether the converters (`convertBlock` in push-handler.ts and
|
|
293
|
+
* channel-registry.ts) show a block as itself, its text or its image or
|
|
294
|
+
* audio source, or as a reference stub, and not as a placeholder: an
|
|
295
|
+
* unrecognized block, a no-data or no-media-type one, or withheld inline
|
|
296
|
+
* data. A text block whose text isn't a string, which they show as no text,
|
|
297
|
+
* is refused too. This follows their branches in their order.
|
|
298
|
+
*/
|
|
299
|
+
function showsAsItself(b: Record<string, unknown>): boolean {
|
|
300
|
+
if (b.type === 'text') return typeof b.text === 'string';
|
|
301
|
+
if (b.type === 'resource') return classifyBlock(b).kind === 'reference';
|
|
302
|
+
if (b.type !== 'image' && b.type !== 'audio') return false;
|
|
303
|
+
if (isInlineContradiction(b)) return false;
|
|
304
|
+
if (isFilledString(b.uri) && b.disposition) return classifyBlock(b).kind === 'reference';
|
|
305
|
+
if (isFilledString(b.data) && isFilledString(b.mimeType)) return true;
|
|
306
|
+
return b.type === 'image' && isFilledString(b.uri);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function isFilledString(value: unknown): value is string {
|
|
310
|
+
return typeof value === 'string' && value !== '';
|
|
311
|
+
}
|
|
312
|
+
|
|
259
313
|
/** Validate the `coalesce` member of either lane (§13). */
|
|
260
314
|
export function validateCoalesceMember(
|
|
261
315
|
c: unknown,
|
|
@@ -700,14 +754,24 @@ export class PushCoalescer<E = unknown> {
|
|
|
700
754
|
try {
|
|
701
755
|
const result = await this.host.render(occurrence, params);
|
|
702
756
|
if (rendering.cancelled) { this.host.audit({ kind: 'late-render', subject, eventId: occurrence.eventId, discarded: true }); }
|
|
703
|
-
// §5.2: an empty result is legitimate ("nothing happened")
|
|
704
|
-
|
|
757
|
+
// §5.2: an empty result is legitimate ("nothing happened"); each
|
|
758
|
+
// block must be well formed.
|
|
759
|
+
else validateCoalescedContent(result?.content, { allowEmpty: true, checkBlockShapes: true });
|
|
705
760
|
if (rendering.cancelled) throw new CancelledRender();
|
|
706
761
|
content = result.content;
|
|
707
762
|
timestamp = typeof result.timestamp === 'string' ? result.timestamp : new Date().toISOString();
|
|
708
763
|
source = 'render';
|
|
709
764
|
} catch (error) {
|
|
710
|
-
if (!(error instanceof CancelledRender))
|
|
765
|
+
if (!(error instanceof CancelledRender)) {
|
|
766
|
+
// The occurrence's own content is delivered instead; the host names
|
|
767
|
+
// the failure where operators look (a console line, not only this
|
|
768
|
+
// audit record).
|
|
769
|
+
this.host.audit({
|
|
770
|
+
kind: 'render-failed', subject, serverId: occurrence.serverId, eventId: occurrence.eventId,
|
|
771
|
+
...(error instanceof CoalesceError ? { field: error.field } : {}),
|
|
772
|
+
error: String(error),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
711
775
|
} finally {
|
|
712
776
|
const left = (this.rendersInFlight.get(occurrence.serverId) ?? 1) - 1;
|
|
713
777
|
if (left > 0) this.rendersInFlight.set(occurrence.serverId, left); else this.rendersInFlight.delete(occurrence.serverId);
|