@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 { openTransport, type McplTransport, type TransportCloseInfo } from './tr
|
|
|
15
15
|
import { maskNegotiatedCapabilities } from './capability-mask.js';
|
|
16
16
|
import { CapabilityGrant, expandAdvertisementShorthand } from './capability-grant.js';
|
|
17
17
|
import { CAPABILITY_DISABLED } from './errors.js';
|
|
18
|
+
import { logDetail, logValue } from './log-value.js';
|
|
18
19
|
|
|
19
20
|
import type {
|
|
20
21
|
McplServerConfig,
|
|
@@ -48,10 +49,52 @@ import type {
|
|
|
48
49
|
import { McplMethod } from './types.js';
|
|
49
50
|
import type { ToolLifecycleParams, ToolObserveRule } from './tool-lifecycle.js';
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Payload of the connection `'close'` event (third argument; the first two
|
|
54
|
+
* stay `(code, signal)` for existing listeners). Lets a host see WHY the
|
|
55
|
+
* link went down — in particular a WebSocket close code such as 4001 from
|
|
56
|
+
* a server that refused or expired the credential — so it can refresh what
|
|
57
|
+
* `accessProvider` will hand the next dial instead of replaying it.
|
|
58
|
+
*/
|
|
59
|
+
export interface McplConnectionCloseInfo {
|
|
60
|
+
/** Transport kind that closed; null when the connection never had one
|
|
61
|
+
* (a disconnected stub torn down before its first reconnect). */
|
|
62
|
+
transport: McplTransport['kind'] | null;
|
|
63
|
+
/** WebSocket close code (e.g. 4001) or stdio child exit code; null when unknown. */
|
|
64
|
+
code: number | null;
|
|
65
|
+
/** Signal that ended a stdio child; null otherwise. */
|
|
66
|
+
signal: NodeJS.Signals | null;
|
|
67
|
+
/** WebSocket close reason, or a short description of the cause. */
|
|
68
|
+
reason?: string;
|
|
69
|
+
/** True when `reason` is the peer's own text, a WebSocket server's close
|
|
70
|
+
* reason, rather than the transport's or the host's words. */
|
|
71
|
+
reasonFromPeer?: boolean;
|
|
72
|
+
/** True when the host closed the link (`close()` / `reconnectAfterFailure()`),
|
|
73
|
+
* false when the transport dropped unexpectedly. */
|
|
74
|
+
byHost: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
51
77
|
/** Timeout for the initialize handshake in milliseconds.
|
|
52
78
|
* Spring Boot + JDA servers can take 5-10s to boot, so 30s is safe. */
|
|
53
79
|
const INITIALIZE_TIMEOUT_MS = 30_000;
|
|
54
80
|
|
|
81
|
+
/** A failed connect's `stderrTail`, the last of the server's stderr that its
|
|
82
|
+
* handshake's error keeps, when there is one. */
|
|
83
|
+
export function stderrTailOf(error: unknown): string | undefined {
|
|
84
|
+
const tail = (error as { stderrTail?: unknown } | null | undefined)?.stderrTail;
|
|
85
|
+
return typeof tail === 'string' && tail !== '' ? tail : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** For the host's own log line about a failed connect: the server's stderr
|
|
89
|
+
* that the error keeps as `stderrTail`, one marked line per line of it,
|
|
90
|
+
* each through log-value, since it's the server's text. '' when there's
|
|
91
|
+
* none. */
|
|
92
|
+
export function withStderr(error: unknown): string {
|
|
93
|
+
const tail = stderrTailOf(error);
|
|
94
|
+
if (tail === undefined) return '';
|
|
95
|
+
return '. The last of its stderr:' + tail.split('\n').map((line) => `\n stderr: ${logDetail(line)}`).join('');
|
|
96
|
+
}
|
|
97
|
+
|
|
55
98
|
/** Default per-request timeout in milliseconds (see McplServerConfig.requestTimeoutMs).
|
|
56
99
|
* A live-but-wedged server (accepts requests, never answers, never closes its
|
|
57
100
|
* transport) would otherwise freeze the awaiting agent turn forever — no
|
|
@@ -93,9 +136,14 @@ interface RequestOptions {
|
|
|
93
136
|
* - `'channels-changed'` — Server sent `channels/changed`
|
|
94
137
|
* - `'channels-incoming'` — Server sent `channels/incoming`
|
|
95
138
|
* - `'error'` — Connection-level error
|
|
96
|
-
* - `'close'` — Connection closed
|
|
97
|
-
* - `'connect-failed'` — Initial connect failed; background retry scheduled
|
|
98
|
-
*
|
|
139
|
+
* - `'close'` — Connection closed: `(code, signal, info: McplConnectionCloseInfo)`
|
|
140
|
+
* - `'connect-failed'` — Initial connect failed; background retry scheduled:
|
|
141
|
+
* `{ error, stderrTail?, attempt }`
|
|
142
|
+
* - `'reconnect-failed'` — A background reconnect attempt failed; next retry scheduled:
|
|
143
|
+
* `{ error, stderrTail?, attempt }`. `stderrTail` is the last of the
|
|
144
|
+
* server's stderr when its handshake failed by its transport closing, a
|
|
145
|
+
* transport error or its timeout and it had written any; `error` never
|
|
146
|
+
* carries it.
|
|
99
147
|
* - `'reconnect'` — Background reconnect succeeded
|
|
100
148
|
*/
|
|
101
149
|
export class McplServerConnection extends EventEmitter {
|
|
@@ -209,6 +257,9 @@ export class McplServerConnection extends EventEmitter {
|
|
|
209
257
|
* observable even though its original promise has already been rejected. */
|
|
210
258
|
private orphanedRequests = new Map<string | number, { method: string; renderParams?: Record<string, unknown> }>();
|
|
211
259
|
private closed = false;
|
|
260
|
+
/** Set by `reconnectAfterFailure()` before it closes a live transport, so
|
|
261
|
+
* the transport close handler reports that teardown as `byHost: true`. */
|
|
262
|
+
private hostCloseRequested = false;
|
|
212
263
|
private closePromise: Promise<void> | null = null;
|
|
213
264
|
private closeIncomplete = false;
|
|
214
265
|
private reconnectPromise: Promise<void> | null = null;
|
|
@@ -378,7 +429,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
378
429
|
if (responder?.respondError) {
|
|
379
430
|
responder.respondError(CAPABILITY_DISABLED, `${name} rejected: capability "${requiredCap}" ${phase}`, { capability: requiredCap });
|
|
380
431
|
} else {
|
|
381
|
-
console.error(`[mcpl] ${this.id}: discarded ${name} — "${requiredCap}" ${phase}`);
|
|
432
|
+
console.error(`[mcpl] ${this.id}: discarded ${logValue(name)} — "${requiredCap}" ${phase}`);
|
|
382
433
|
}
|
|
383
434
|
return true;
|
|
384
435
|
}
|
|
@@ -506,6 +557,15 @@ export class McplServerConnection extends EventEmitter {
|
|
|
506
557
|
hostCapabilities: McplHostCapabilities,
|
|
507
558
|
): Promise<{ transport: McplTransport; capabilities: McplCapabilities | null; droppedCapabilities: ReadonlySet<string>; mcpToolsAdvertised: boolean }> {
|
|
508
559
|
const transport = await openTransport(config);
|
|
560
|
+
// Once the handshake stops listening for 'error', this logs what the
|
|
561
|
+
// transport reports, until the handshake hands the transport on, or for
|
|
562
|
+
// good if the handshake fails. EventEmitter throws an 'error' that has no
|
|
563
|
+
// listener, and from an I/O callback nothing catches it. A malformed
|
|
564
|
+
// WebSocket frame that arrives with the initialize response, or while a
|
|
565
|
+
// failed handshake's close() runs, is such an error.
|
|
566
|
+
const logAfterSettled = (err: Error) => {
|
|
567
|
+
console.error(`[mcpl] ${config.id}: transport error after the handshake settled: ${err.message}`);
|
|
568
|
+
};
|
|
509
569
|
|
|
510
570
|
try {
|
|
511
571
|
const initId = 0; // use id=0 for the handshake
|
|
@@ -523,10 +583,20 @@ export class McplServerConnection extends EventEmitter {
|
|
|
523
583
|
// Await the initialize response, racing an early transport close/error and
|
|
524
584
|
// a timeout. All three paths clean up their listeners.
|
|
525
585
|
const { mcpl: rawCapabilities, mcpToolsAdvertised } = await new Promise<{ mcpl: McplCapabilities | null; mcpToolsAdvertised: boolean }>((resolve, reject) => {
|
|
586
|
+
// When the transport fails, the end of the server's stderr often says
|
|
587
|
+
// why, such as a missing token or a crash at startup. It goes on the
|
|
588
|
+
// error as `stderrTail`, never into the message: an error's message
|
|
589
|
+
// travels to traces and tool results, and a server's stderr is its
|
|
590
|
+
// own text, which can hold a credential.
|
|
591
|
+
const failed = (failure: string): Error & { stderrTail?: string } => {
|
|
592
|
+
const tail = transport.stderrTail.trimEnd();
|
|
593
|
+
return Object.assign(new Error(failure), tail ? { stderrTail: tail } : {});
|
|
594
|
+
};
|
|
526
595
|
const cleanup = () => {
|
|
527
596
|
transport.off('line', onLine);
|
|
528
597
|
transport.off('close', onClose);
|
|
529
598
|
transport.off('error', onError);
|
|
599
|
+
transport.on('error', logAfterSettled);
|
|
530
600
|
clearTimeout(timer);
|
|
531
601
|
};
|
|
532
602
|
const onLine = (line: string) => {
|
|
@@ -555,15 +625,15 @@ export class McplServerConnection extends EventEmitter {
|
|
|
555
625
|
};
|
|
556
626
|
const onClose = (info: TransportCloseInfo) => {
|
|
557
627
|
cleanup();
|
|
558
|
-
reject(
|
|
628
|
+
reject(failed(`MCPL server "${config.id}" transport closed before handshake completed (${info.reason ?? 'unknown'})`));
|
|
559
629
|
};
|
|
560
630
|
const onError = (err: Error) => {
|
|
561
631
|
cleanup();
|
|
562
|
-
reject(
|
|
632
|
+
reject(failed(`MCPL server "${config.id}" transport error during handshake: ${err.message}`));
|
|
563
633
|
};
|
|
564
634
|
const timer = setTimeout(() => {
|
|
565
635
|
cleanup();
|
|
566
|
-
reject(
|
|
636
|
+
reject(failed(`MCPL server "${config.id}" initialize handshake timed out`));
|
|
567
637
|
}, INITIALIZE_TIMEOUT_MS);
|
|
568
638
|
|
|
569
639
|
transport.on('line', onLine);
|
|
@@ -585,9 +655,13 @@ export class McplServerConnection extends EventEmitter {
|
|
|
585
655
|
// had never been advertised (hooks, streaming, queries all read this).
|
|
586
656
|
const { capabilities: scoped, dropped } = maskNegotiatedCapabilities(capabilities, config);
|
|
587
657
|
if (dropped.length > 0) {
|
|
588
|
-
console.error(`MCPL server "${config.id}" capabilities masked by host config: ${dropped.join(', ')}`);
|
|
658
|
+
console.error(`MCPL server "${config.id}" capabilities masked by host config: ${dropped.map(logValue).join(', ')}`);
|
|
589
659
|
}
|
|
590
660
|
|
|
661
|
+
// The caller wires the transport, or keeps it for close() with a
|
|
662
|
+
// listener of its own, in the continuation of this return, before any
|
|
663
|
+
// I/O callback can emit.
|
|
664
|
+
transport.off('error', logAfterSettled);
|
|
591
665
|
return { transport, capabilities: scoped, droppedCapabilities: new Set(dropped), mcpToolsAdvertised };
|
|
592
666
|
} catch (err) {
|
|
593
667
|
// Never leak the child / socket if the handshake fails.
|
|
@@ -614,12 +688,13 @@ export class McplServerConnection extends EventEmitter {
|
|
|
614
688
|
}
|
|
615
689
|
|
|
616
690
|
// Non-blocking start: create a disconnected stub that will reconnect in background
|
|
617
|
-
console.error(`MCPL server "${config.id}" initial connect failed, will retry
|
|
691
|
+
console.error(`MCPL server "${config.id}" initial connect failed, will retry: ${(error as Error).message}${withStderr(error)}`);
|
|
618
692
|
const stub = McplServerConnection.createDisconnectedStub(config, hostCapabilities);
|
|
619
693
|
// Surface the initial failure. The event is buffered by the emit()
|
|
620
694
|
// override until the host wires listeners and calls ready(), so it is
|
|
621
695
|
// not lost in the window before wireMcplEvents runs.
|
|
622
|
-
|
|
696
|
+
const stderrTail = stderrTailOf(error);
|
|
697
|
+
stub.emit('connect-failed', { error: (error as Error).message, ...(stderrTail ? { stderrTail } : {}), attempt: 0 });
|
|
623
698
|
return stub;
|
|
624
699
|
}
|
|
625
700
|
}
|
|
@@ -864,7 +939,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
864
939
|
if (this.reconnectPromise) await this.reconnectPromise;
|
|
865
940
|
if (this.transport) await this.transport.close();
|
|
866
941
|
this.closeIncomplete = false;
|
|
867
|
-
this.emit('close');
|
|
942
|
+
this.emit('close', null, null, this.closeInfoByHost());
|
|
868
943
|
});
|
|
869
944
|
this.closePromise = attempt;
|
|
870
945
|
void attempt.catch(() => {
|
|
@@ -886,14 +961,26 @@ export class McplServerConnection extends EventEmitter {
|
|
|
886
961
|
return;
|
|
887
962
|
}
|
|
888
963
|
if (this.transport) {
|
|
964
|
+
this.hostCloseRequested = true;
|
|
889
965
|
await this.transport.close();
|
|
890
966
|
return;
|
|
891
967
|
}
|
|
892
968
|
this.closed = true;
|
|
893
|
-
this.emit('close');
|
|
969
|
+
this.emit('close', null, null, this.closeInfoByHost());
|
|
894
970
|
this.scheduleReconnect();
|
|
895
971
|
}
|
|
896
972
|
|
|
973
|
+
/** 'close' payload for a host-initiated teardown (no transport close code). */
|
|
974
|
+
private closeInfoByHost(): McplConnectionCloseInfo {
|
|
975
|
+
return {
|
|
976
|
+
transport: this.transport?.kind ?? null,
|
|
977
|
+
code: null,
|
|
978
|
+
signal: null,
|
|
979
|
+
reason: 'closed by host',
|
|
980
|
+
byHost: true,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
897
984
|
/**
|
|
898
985
|
* Schedule a background reconnection attempt.
|
|
899
986
|
*
|
|
@@ -952,6 +1039,13 @@ export class McplServerConnection extends EventEmitter {
|
|
|
952
1039
|
// The prior transport was disconnected before this retry began. Keep
|
|
953
1040
|
// the fresh, unadmitted transport owned by close(), including on a
|
|
954
1041
|
// failed transport cleanup, instead of reviving a stopped connection.
|
|
1042
|
+
// It isn't wired, and the handshake has removed its own listeners, so
|
|
1043
|
+
// an error it reports from here on, such as a malformed frame from a
|
|
1044
|
+
// WebSocket server while close() closes it, is logged here rather than
|
|
1045
|
+
// thrown where nothing catches it.
|
|
1046
|
+
transport.on('error', (err: Error) => {
|
|
1047
|
+
console.error(`[mcpl] ${this.id}: a transport kept for close() reported an error: ${err.message}`);
|
|
1048
|
+
});
|
|
955
1049
|
this.transport = transport;
|
|
956
1050
|
return;
|
|
957
1051
|
}
|
|
@@ -976,9 +1070,10 @@ export class McplServerConnection extends EventEmitter {
|
|
|
976
1070
|
this.reconnectAttempts = 0;
|
|
977
1071
|
this.emit('reconnect', { attempts: attempt });
|
|
978
1072
|
} catch (error) {
|
|
979
|
-
console.error(`MCPL server "${this.id}" reconnect failed
|
|
1073
|
+
console.error(`MCPL server "${this.id}" reconnect failed: ${(error as Error).message}${withStderr(error)}`);
|
|
980
1074
|
this.reconnectAttempts = attempt + 1;
|
|
981
|
-
|
|
1075
|
+
const stderrTail = stderrTailOf(error);
|
|
1076
|
+
this.emit('reconnect-failed', { error: (error as Error).message, ...(stderrTail ? { stderrTail } : {}), attempt });
|
|
982
1077
|
this.scheduleReconnect();
|
|
983
1078
|
}
|
|
984
1079
|
}
|
|
@@ -1285,6 +1380,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1285
1380
|
* and — when enabled — schedules a background reconnect.
|
|
1286
1381
|
*/
|
|
1287
1382
|
private setupLifecycle(transport: McplTransport): void {
|
|
1383
|
+
this.hostCloseRequested = false;
|
|
1288
1384
|
transport.on('error', (err: Error) => {
|
|
1289
1385
|
this.emit('error', new Error(`MCPL server "${this.id}" transport error: ${err.message}`));
|
|
1290
1386
|
});
|
|
@@ -1306,7 +1402,21 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1306
1402
|
this.pendingRequests.clear();
|
|
1307
1403
|
this.orphanedRequests.clear();
|
|
1308
1404
|
|
|
1309
|
-
|
|
1405
|
+
// Forward the transport's close info so listeners can tell a server
|
|
1406
|
+
// that refused the credential (WebSocket 4001) from a crash, and act
|
|
1407
|
+
// before the reconnect below redials. A teardown the host asked for
|
|
1408
|
+
// via reconnectAfterFailure() arrives here too; report it as such.
|
|
1409
|
+
const byHost = this.hostCloseRequested;
|
|
1410
|
+
this.hostCloseRequested = false;
|
|
1411
|
+
const closeInfo: McplConnectionCloseInfo = {
|
|
1412
|
+
transport: transport.kind,
|
|
1413
|
+
code: info.code ?? null,
|
|
1414
|
+
signal: info.signal ?? null,
|
|
1415
|
+
reason: info.reason ?? (byHost ? 'closed by host' : undefined),
|
|
1416
|
+
...(info.reasonFromPeer ? { reasonFromPeer: true } : {}),
|
|
1417
|
+
byHost,
|
|
1418
|
+
};
|
|
1419
|
+
this.emit('close', closeInfo.code, closeInfo.signal, closeInfo);
|
|
1310
1420
|
|
|
1311
1421
|
// Schedule reconnect if enabled (unexpected disconnect triggers auto-reconnect)
|
|
1312
1422
|
if (this.reconnectEnabled) {
|
package/src/mcpl/tool-classes.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { globMatch } from './tool-glob.js';
|
|
24
|
+
import { logValue } from './log-value.js';
|
|
24
25
|
|
|
25
26
|
export const TOOL_CLASSES = [
|
|
26
27
|
'comms',
|
|
@@ -116,7 +117,7 @@ export function parseDeclaredClasses(meta: unknown, toolName = '(unnamed)'): Too
|
|
|
116
117
|
const raw = (meta as Record<string, unknown>)[TOOL_CLASS_META_KEY];
|
|
117
118
|
if (raw === undefined) return undefined;
|
|
118
119
|
if (!Array.isArray(raw)) {
|
|
119
|
-
console.error(`[mcpl] ${toolName}: _meta["${TOOL_CLASS_META_KEY}"] is not an array — treated as unclassed`);
|
|
120
|
+
console.error(`[mcpl] ${logValue(toolName)}: _meta["${TOOL_CLASS_META_KEY}"] is not an array — treated as unclassed`);
|
|
120
121
|
return undefined;
|
|
121
122
|
}
|
|
122
123
|
const out: ToolClass[] = [];
|
|
@@ -124,7 +125,7 @@ export function parseDeclaredClasses(meta: unknown, toolName = '(unnamed)'): Too
|
|
|
124
125
|
if (isToolClass(value)) {
|
|
125
126
|
if (!out.includes(value)) out.push(value);
|
|
126
127
|
} else {
|
|
127
|
-
console.error(`[mcpl] ${toolName}: unknown tool class ${
|
|
128
|
+
console.error(`[mcpl] ${logValue(toolName)}: unknown tool class ${logValue(value)} ignored (RFC-008 §3)`);
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
return out.length > 0 ? out : undefined;
|
package/src/mcpl/transport.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { spawn, type ChildProcess } from 'node:child_process';
|
|
20
20
|
import { createInterface, type Interface as ReadlineInterface } from 'node:readline';
|
|
21
21
|
import { EventEmitter } from 'node:events';
|
|
22
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
22
23
|
import WebSocket from 'ws';
|
|
23
24
|
|
|
24
25
|
import type { McplServerConfig } from './types.js';
|
|
@@ -29,6 +30,9 @@ export interface TransportCloseInfo {
|
|
|
29
30
|
code?: number | null;
|
|
30
31
|
signal?: NodeJS.Signals | null;
|
|
31
32
|
reason?: string;
|
|
33
|
+
/** True when `reason` is the peer's own text, a WebSocket server's close
|
|
34
|
+
* reason, rather than the transport's words. */
|
|
35
|
+
reasonFromPeer?: boolean;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
/**
|
|
@@ -71,6 +75,16 @@ export abstract class McplTransport extends EventEmitter {
|
|
|
71
75
|
return this._closed;
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The last of what the server wrote to its diagnostic stream: at most its
|
|
80
|
+
* last {@link STDERR_TAIL_CHARS} UTF-16 code units, never opening inside a
|
|
81
|
+
* surrogate pair, after a '…' when earlier ones were dropped. A stdio
|
|
82
|
+
* child's stderr, and '' for a transport that has none.
|
|
83
|
+
*/
|
|
84
|
+
get stderrTail(): string {
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
|
|
74
88
|
/** Deliver an inbound line, buffering it if nobody is listening yet. */
|
|
75
89
|
protected pushLine(line: string): void {
|
|
76
90
|
if (this.listenerCount('line') > 0) this.emit('line', line);
|
|
@@ -167,8 +181,19 @@ export function buildChildEnv(
|
|
|
167
181
|
return merged;
|
|
168
182
|
}
|
|
169
183
|
|
|
184
|
+
/** The most of a stdio child's stderr a transport keeps for {@link McplTransport.stderrTail}. */
|
|
185
|
+
export const STDERR_TAIL_CHARS = 2_048;
|
|
186
|
+
|
|
170
187
|
export class StdioTransport extends McplTransport {
|
|
171
188
|
readonly kind = 'stdio' as const;
|
|
189
|
+
/** The first error on the child's stdin or stdout, named in the close reason. */
|
|
190
|
+
private pipeError: Error | undefined;
|
|
191
|
+
private stderrEnd = '';
|
|
192
|
+
private stderrCut = false;
|
|
193
|
+
|
|
194
|
+
override get stderrTail(): string {
|
|
195
|
+
return (this.stderrCut ? '…' : '') + this.stderrEnd;
|
|
196
|
+
}
|
|
172
197
|
|
|
173
198
|
private constructor(
|
|
174
199
|
private readonly child: ChildProcess,
|
|
@@ -178,26 +203,70 @@ export class StdioTransport extends McplTransport {
|
|
|
178
203
|
|
|
179
204
|
this.rl.on('line', (line: string) => this.pushLine(line));
|
|
180
205
|
|
|
181
|
-
// Stitch stderr chunks into whole lines (a chunk can split a line
|
|
206
|
+
// Stitch stderr chunks into whole lines (a chunk can split a line, and a
|
|
207
|
+
// character's bytes), and keep the stream's last characters for
|
|
208
|
+
// stderrTail.
|
|
182
209
|
let carry = '';
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
210
|
+
const decoder = new StringDecoder('utf8');
|
|
211
|
+
const received = (text: string) => {
|
|
212
|
+
const end = this.stderrEnd + text;
|
|
213
|
+
this.stderrCut ||= end.length > STDERR_TAIL_CHARS;
|
|
214
|
+
let kept = end.slice(-STDERR_TAIL_CHARS);
|
|
215
|
+
// A cut between a surrogate pair's halves drops the low half too.
|
|
216
|
+
if (/^[\uDC00-\uDFFF]/.test(kept) && end.length > STDERR_TAIL_CHARS) kept = kept.slice(1);
|
|
217
|
+
this.stderrEnd = kept;
|
|
218
|
+
const lines = (carry + text).split('\n');
|
|
186
219
|
carry = lines.pop() ?? '';
|
|
187
220
|
for (const l of lines) if (l.length > 0) this.pushStderr(l);
|
|
221
|
+
};
|
|
222
|
+
this.child.stderr?.on('data', (chunk: Buffer) => received(decoder.write(chunk)));
|
|
223
|
+
// A last line with no newline is complete once the stream ends.
|
|
224
|
+
this.child.stderr?.on('end', () => {
|
|
225
|
+
received(decoder.end());
|
|
226
|
+
if (carry.length > 0) this.pushStderr(carry);
|
|
188
227
|
});
|
|
189
228
|
|
|
190
229
|
this.child.on('error', (err) => this.emit('error', err));
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
230
|
+
// The child's stdin and stdout carry the protocol, so after an error on
|
|
231
|
+
// either, a write's EPIPE or a read error, this transport can't reach the
|
|
232
|
+
// child even if it's still running. The handler sends a running child
|
|
233
|
+
// SIGTERM, and the exit's close names the error, so the connection handles
|
|
234
|
+
// it as any other exit. The close is the report: no 'error' is emitted,
|
|
235
|
+
// nothing is thrown. Nothing here escalates to SIGKILL, so a child that
|
|
236
|
+
// ignores SIGTERM keeps running, unreported. A child that stops reading
|
|
237
|
+
// without closing its stdin raises no error: the writes are buffered, and
|
|
238
|
+
// only request timeouts bound it (McplServerConfig.requestTimeoutMs).
|
|
239
|
+
const endOnPipeError = (err: Error) => {
|
|
240
|
+
this.pipeError ??= err;
|
|
241
|
+
if (this.child.exitCode === null && this.child.signalCode === null) this.child.kill();
|
|
242
|
+
};
|
|
243
|
+
this.child.stdin?.on('error', endOnPipeError);
|
|
244
|
+
// On the stream itself, since close() closes readline, which takes its own
|
|
245
|
+
// listener off the stream. While readline is open, its listener, which
|
|
246
|
+
// came first, re-emits the error on the interface, where an empty
|
|
247
|
+
// listener absorbs that copy.
|
|
248
|
+
this.child.stdout?.on('error', endOnPipeError);
|
|
249
|
+
this.rl.on('error', () => { /* readline's copy of a stdout error, handled above */ });
|
|
250
|
+
// stderr carries diagnostics only: a read error on it is reported if
|
|
251
|
+
// anyone listens and never thrown. It ends nothing once the connection
|
|
252
|
+
// is wired; during a handshake the listener is the handshake's, which
|
|
253
|
+
// fails it.
|
|
254
|
+
this.child.stderr?.on('error', (err) => {
|
|
255
|
+
if (this.listenerCount('error') > 0) this.emit('error', err);
|
|
256
|
+
});
|
|
257
|
+
this.child.on('exit', (code, signal) => this.markClosed({
|
|
258
|
+
code,
|
|
259
|
+
signal,
|
|
260
|
+
reason: this.pipeError ? `child process exited, and its pipe failed with ${this.pipeError.message}` : 'child process exited',
|
|
261
|
+
}));
|
|
194
262
|
}
|
|
195
263
|
|
|
196
264
|
/**
|
|
197
|
-
* Spawn `config.command` and
|
|
198
|
-
* command is missing (a config bug) or the
|
|
265
|
+
* Spawn `config.command` and resolve with a ready stdio transport once the
|
|
266
|
+
* child has spawned. Rejects if the command is missing (a config bug) or the
|
|
267
|
+
* process fails to spawn.
|
|
199
268
|
*/
|
|
200
|
-
static spawn(config: McplServerConfig): StdioTransport {
|
|
269
|
+
static async spawn(config: McplServerConfig): Promise<StdioTransport> {
|
|
201
270
|
if (!config.command) {
|
|
202
271
|
throw new Error(
|
|
203
272
|
`MCPL server "${config.id}": stdio transport requires "command" (none provided)`,
|
|
@@ -207,6 +276,22 @@ export class StdioTransport extends McplTransport {
|
|
|
207
276
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
208
277
|
env: buildChildEnv(config),
|
|
209
278
|
});
|
|
279
|
+
// Node reports a failed spawn (a command that doesn't exist, say) as an
|
|
280
|
+
// 'error' on a tick after spawn() returns, and no 'exit' follows. It is
|
|
281
|
+
// settled here, where it is listened for. A caller that listens only once
|
|
282
|
+
// this resolves, as the handshake does, would be too late when reached
|
|
283
|
+
// from a timer callback, as a reconnect is: Node runs that tick before the
|
|
284
|
+
// caller's promise continuation, and throws an 'error' nothing listens for.
|
|
285
|
+
await new Promise<void>((resolve, reject) => {
|
|
286
|
+
const failed = (err: Error) => {
|
|
287
|
+
reject(new Error(`MCPL server "${config.id}" could not be started: ${err.message}`, { cause: err }));
|
|
288
|
+
};
|
|
289
|
+
child.once('error', failed);
|
|
290
|
+
child.once('spawn', () => {
|
|
291
|
+
child.off('error', failed);
|
|
292
|
+
resolve();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
210
295
|
const rl = createInterface({ input: child.stdout! });
|
|
211
296
|
return new StdioTransport(child, rl);
|
|
212
297
|
}
|
|
@@ -223,7 +308,12 @@ export class StdioTransport extends McplTransport {
|
|
|
223
308
|
this.child.kill();
|
|
224
309
|
}
|
|
225
310
|
await new Promise<void>((resolve) => {
|
|
226
|
-
|
|
311
|
+
// Once Node has seen a child exit, it has its exitCode set, or its
|
|
312
|
+
// signalCode when a signal ended it, and its 'exit' event has fired. A
|
|
313
|
+
// signal that didn't come through kill() (an OOM kill, a crash) leaves
|
|
314
|
+
// `killed` false. A child that has exited but isn't reaped yet still
|
|
315
|
+
// takes the kill() above, which sets `killed`.
|
|
316
|
+
if (!this.child || this.child.exitCode !== null || this.child.signalCode !== null || this.child.killed) {
|
|
227
317
|
resolve();
|
|
228
318
|
} else {
|
|
229
319
|
this.child.once('exit', () => resolve());
|
|
@@ -256,9 +346,10 @@ export class WebSocketTransport extends McplTransport {
|
|
|
256
346
|
});
|
|
257
347
|
|
|
258
348
|
this.ws.on('error', (err: Error) => this.emit('error', err));
|
|
259
|
-
this.ws.on('close', (code: number, reason: Buffer) =>
|
|
260
|
-
|
|
261
|
-
|
|
349
|
+
this.ws.on('close', (code: number, reason: Buffer) => {
|
|
350
|
+
const text = reason?.toString('utf8');
|
|
351
|
+
this.markClosed(text ? { code, reason: text, reasonFromPeer: true } : { code, reason: 'websocket closed' });
|
|
352
|
+
});
|
|
262
353
|
}
|
|
263
354
|
|
|
264
355
|
/**
|
package/src/mcpl/types.ts
CHANGED
|
@@ -374,7 +374,11 @@ export interface McplServerConfig {
|
|
|
374
374
|
* accepts a request and never responds would otherwise freeze the awaiting
|
|
375
375
|
* agent turn forever; with the timeout the pending request rejects with a
|
|
376
376
|
* descriptive error, which the framework surfaces as a normal isError
|
|
377
|
-
* tool_result.
|
|
377
|
+
* tool_result. Request timeouts are the only bound for a stdio server that
|
|
378
|
+
* stops reading its stdin without closing it: the host's writes are
|
|
379
|
+
* buffered, no error is raised, and the connection stays up. This is the
|
|
380
|
+
* timeout of every request that names none; the handshake's initialize has
|
|
381
|
+
* its own. Set 0 to disable. Default: 60000 (60 seconds).
|
|
378
382
|
*/
|
|
379
383
|
requestTimeoutMs?: number;
|
|
380
384
|
|
package/src/module-registry.ts
CHANGED
|
@@ -26,6 +26,12 @@ import type {
|
|
|
26
26
|
import type { Agent } from './agent.js';
|
|
27
27
|
import { DEFAULT_SHUTDOWN_TIMEOUT_MS, validateShutdownTimeout, waitForShutdown } from './shutdown.js';
|
|
28
28
|
|
|
29
|
+
/** How the registry queues a module's event: its push names the module. A
|
|
30
|
+
* `ProcessQueue` serves too, ignoring the name. */
|
|
31
|
+
export interface ModuleQueue {
|
|
32
|
+
push(event: ProcessEvent, moduleName?: string): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
const MODULE_STATE_PREFIX = 'modules/';
|
|
30
36
|
|
|
31
37
|
/**
|
|
@@ -78,7 +84,7 @@ export class ModuleRegistry {
|
|
|
78
84
|
private moduleContexts: Map<string, ModuleContextImpl> = new Map();
|
|
79
85
|
private speechHandlers: SpeechHandler[] = [];
|
|
80
86
|
private store: JsStore;
|
|
81
|
-
private queue:
|
|
87
|
+
private queue: ModuleQueue;
|
|
82
88
|
private getAgents: () => Agent[];
|
|
83
89
|
private addMessageFn: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
|
|
84
90
|
private editMessageFn: (id: MessageId, content: ContentBlock[]) => void;
|
|
@@ -86,7 +92,7 @@ export class ModuleRegistry {
|
|
|
86
92
|
private getMessageFn: (id: MessageId) => StoredMessage | null;
|
|
87
93
|
private queryMessagesFn: (filter: MessageQuery) => MessageQueryResult;
|
|
88
94
|
|
|
89
|
-
private pushEventFn: (event: ProcessEvent) => void;
|
|
95
|
+
private pushEventFn: (event: ProcessEvent, moduleName?: string) => void;
|
|
90
96
|
private onTraceFn: (listener: TraceEventListener) => () => void;
|
|
91
97
|
private callToolFn: (call: ToolCall) => Promise<ToolResult>;
|
|
92
98
|
/** Framework ops-alert channel, surfaced to modules via
|
|
@@ -101,7 +107,7 @@ export class ModuleRegistry {
|
|
|
101
107
|
|
|
102
108
|
constructor(
|
|
103
109
|
store: JsStore,
|
|
104
|
-
queue:
|
|
110
|
+
queue: ModuleQueue,
|
|
105
111
|
options: {
|
|
106
112
|
getAgents: () => Agent[];
|
|
107
113
|
addMessage: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
|
|
@@ -109,7 +115,8 @@ export class ModuleRegistry {
|
|
|
109
115
|
removeMessage: (id: MessageId) => void;
|
|
110
116
|
getMessage: (id: MessageId) => StoredMessage | null;
|
|
111
117
|
queryMessages: (filter: MessageQuery) => MessageQueryResult;
|
|
112
|
-
|
|
118
|
+
/** Called with the pushing module's name. */
|
|
119
|
+
pushEvent: (event: ProcessEvent, moduleName?: string) => void;
|
|
113
120
|
onTrace: (listener: TraceEventListener) => () => void;
|
|
114
121
|
callTool: (call: ToolCall) => Promise<ToolResult>;
|
|
115
122
|
notifyOps?: (kind: string, agentName: string, message: string, data?: Record<string, unknown>) => void;
|
|
@@ -537,7 +544,7 @@ class ModuleContextImpl implements ModuleContext {
|
|
|
537
544
|
private removeMessageFn: (id: MessageId) => void;
|
|
538
545
|
private getMessageFn: (id: MessageId) => StoredMessage | null;
|
|
539
546
|
private queryMessagesFn: (filter: MessageQuery) => MessageQueryResult;
|
|
540
|
-
private pushEventFn: (event: ProcessEvent) => void;
|
|
547
|
+
private pushEventFn: (event: ProcessEvent, moduleName?: string) => void;
|
|
541
548
|
private onTraceFn: (listener: TraceEventListener) => () => void;
|
|
542
549
|
private callToolFn: (call: ToolCall) => Promise<ToolResult>;
|
|
543
550
|
|
|
@@ -548,7 +555,7 @@ class ModuleContextImpl implements ModuleContext {
|
|
|
548
555
|
moduleName: string,
|
|
549
556
|
store: JsStore,
|
|
550
557
|
stateId: string,
|
|
551
|
-
queue:
|
|
558
|
+
queue: ModuleQueue,
|
|
552
559
|
registry: ModuleRegistry,
|
|
553
560
|
isRestart: boolean,
|
|
554
561
|
getAgents: () => Agent[],
|
|
@@ -557,14 +564,15 @@ class ModuleContextImpl implements ModuleContext {
|
|
|
557
564
|
removeMessage: (id: MessageId) => void,
|
|
558
565
|
getMessage: (id: MessageId) => StoredMessage | null,
|
|
559
566
|
queryMessages: (filter: MessageQuery) => MessageQueryResult,
|
|
560
|
-
pushEvent: (event: ProcessEvent) => void,
|
|
567
|
+
pushEvent: (event: ProcessEvent, moduleName?: string) => void,
|
|
561
568
|
onTrace: (listener: TraceEventListener) => () => void,
|
|
562
569
|
callTool: (call: ToolCall) => Promise<ToolResult>
|
|
563
570
|
) {
|
|
564
571
|
this.moduleName = moduleName;
|
|
565
572
|
this.store = store;
|
|
566
573
|
this.stateId = stateId;
|
|
567
|
-
this
|
|
574
|
+
// The deprecated handle: a push names this module, as pushEvent() does.
|
|
575
|
+
this.queue = { push: (event) => queue.push(event, moduleName) };
|
|
568
576
|
this.registry = registry;
|
|
569
577
|
this.isRestart = isRestart;
|
|
570
578
|
this.getAgentsFn = getAgents;
|
|
@@ -747,7 +755,7 @@ class ModuleContextImpl implements ModuleContext {
|
|
|
747
755
|
}
|
|
748
756
|
|
|
749
757
|
pushEvent(event: ProcessEvent): void {
|
|
750
|
-
this.pushEventFn(event);
|
|
758
|
+
this.pushEventFn(event, this.moduleName);
|
|
751
759
|
}
|
|
752
760
|
|
|
753
761
|
private persistExternalIdMap(): void {
|
|
@@ -67,9 +67,11 @@ const TYPING_REFRESH_MS = 8_000;
|
|
|
67
67
|
// stuck on forever.
|
|
68
68
|
const TYPING_MAX_MS = 5 * 60_000;
|
|
69
69
|
|
|
70
|
-
// Trace event types that
|
|
71
|
-
// typing keepalive.
|
|
70
|
+
// Trace event types that end an agent's turn (onTrace's contract) — any of
|
|
71
|
+
// these stops the typing keepalive. A turn's tool-call cycles stay inside its
|
|
72
|
+
// stream, so they come after them. inference:completed is the normal end.
|
|
72
73
|
const TURN_TERMINAL_TRACES = new Set([
|
|
74
|
+
'inference:completed',
|
|
73
75
|
'inference:turn_ended',
|
|
74
76
|
'inference:aborted',
|
|
75
77
|
'inference:failed',
|