mixdog 0.9.62 → 0.9.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/package.json +9 -2
- package/scripts/code-graph-description-contract.mjs +3 -3
- package/scripts/verify-release-assets.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +11 -14
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +1 -0
- package/src/runtime/agent/orchestrator/config.mjs +19 -124
- package/src/runtime/agent/orchestrator/context/collect.mjs +6 -6
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +2 -2
- package/src/runtime/agent/orchestrator/internal-agents.mjs +2 -2
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +1 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +333 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -277
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +196 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +460 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +14 -593
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -280
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +357 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +216 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +2 -188
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +18 -26
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +260 -0
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +72 -0
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +163 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/store.mjs +9 -561
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/bash-policy-scan.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +2 -154
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/cwd-utils.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-locks.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-open.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +519 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +5 -449
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +212 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +1 -7
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +9 -36
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +429 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -395
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/mutation-planner.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/next-call-utils.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +72 -9
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +2 -444
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +505 -0
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +1 -1
- package/src/runtime/channels/lib/backend-dispatch.mjs +20 -5
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +1 -1
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +4 -4
- package/src/runtime/channels/tool-defs.mjs +2 -3
- package/src/runtime/memory/index.mjs +1 -5
- package/src/runtime/memory/lib/core-memory-candidates.mjs +357 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +7 -357
- package/src/runtime/memory/lib/cycle-signatures.mjs +1 -1
- package/src/runtime/memory/lib/embedding-model-config.mjs +1 -1
- package/src/runtime/memory/lib/embedding-provider.mjs +1 -5
- package/src/runtime/memory/lib/embedding-worker.mjs +1 -5
- package/src/runtime/memory/lib/ko-morph.mjs +2 -2
- package/src/runtime/memory/lib/memory-config-flags.mjs +2 -5
- package/src/runtime/memory/lib/memory-cycle-requests.mjs +3 -7
- package/src/runtime/memory/lib/memory-cycle1.mjs +4 -8
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +2 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -5
- package/src/runtime/memory/lib/memory-embed.mjs +2 -6
- package/src/runtime/memory/lib/memory-log.mjs +8 -0
- package/src/runtime/memory/lib/memory-ops-policy.mjs +4 -8
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +1 -1
- package/src/runtime/memory/lib/memory-recall-store.mjs +2 -193
- package/src/runtime/memory/lib/memory-score.mjs +1 -1
- package/src/runtime/memory/lib/memory-text-utils.mjs +2 -2
- package/src/runtime/memory/lib/memory.mjs +2 -6
- package/src/runtime/memory/lib/pg/adapter.mjs +2 -6
- package/src/runtime/memory/lib/pg/process.mjs +1 -5
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -5
- package/src/runtime/memory/lib/recall-format.mjs +2 -2
- package/src/runtime/memory/lib/recall-scoring.mjs +195 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -5
- package/src/runtime/memory/lib/trace-store.mjs +2 -6
- package/src/runtime/search/lib/http-fetch.mjs +6 -6
- package/src/runtime/search/lib/web-tools.mjs +2 -2
- package/src/runtime/shared/buffered-appender.mjs +1 -1
- package/src/runtime/shared/child-guardian.mjs +1 -1
- package/src/runtime/shared/child-spawn-gate.mjs +1 -1
- package/src/runtime/shared/config.mjs +4 -4
- package/src/runtime/shared/launcher-control.mjs +9 -10
- package/src/runtime/shared/llm/index.mjs +5 -20
- package/src/runtime/shared/markdown-frontmatter.mjs +1 -1
- package/src/runtime/shared/memory-snapshot.mjs +4 -4
- package/src/runtime/shared/pristine-execution.mjs +1 -1
- package/src/runtime/shared/process-lifecycle.mjs +0 -1
- package/src/runtime/shared/process-shutdown.mjs +1 -1
- package/src/runtime/shared/resource-admission.mjs +2 -2
- package/src/runtime/shared/schedule-session-run.mjs +1 -1
- package/src/runtime/shared/schedules-db.mjs +1 -1
- package/src/runtime/shared/service-discovery.mjs +2 -2
- package/src/runtime/shared/staged-update.mjs +6 -6
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/runtime/shared/tool-primitives.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +2 -2
- package/src/runtime/shared/update-checker.mjs +1 -1
- package/src/runtime/shared/user-cwd.mjs +3 -3
- package/src/runtime/shared/user-data-guard.mjs +4 -4
- package/src/runtime/shared/workspace-router.mjs +5 -5
- package/src/session-runtime/config-helpers.mjs +2 -3
- package/src/session-runtime/config-lifecycle.mjs +1 -1
- package/src/session-runtime/hitch-profile.mjs +1 -1
- package/src/session-runtime/lifecycle-api.mjs +6 -4
- package/src/session-runtime/model-recency.mjs +5 -5
- package/src/session-runtime/plugin-mcp.mjs +1 -1
- package/src/session-runtime/provider-request-snapshot.mjs +338 -0
- package/src/session-runtime/session-text.mjs +4 -4
- package/src/session-runtime/tool-catalog-schema.mjs +197 -0
- package/src/session-runtime/tool-catalog.mjs +6 -483
- package/src/session-runtime/tool-defs.mjs +1 -1
- package/src/standalone/agent-task-status.mjs +1 -1
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/channel-admin.mjs +4 -4
- package/src/standalone/explore-tool.mjs +3 -3
- package/src/standalone/hook-bus/config.mjs +1 -1
- package/src/standalone/hook-bus/constants.mjs +1 -1
- package/src/standalone/hook-bus/rules.mjs +1 -1
- package/src/standalone/projects.mjs +1 -1
- package/src/standalone/provider-admin.mjs +2 -2
- package/src/tui/app/app-format.mjs +2 -2
- package/src/tui/app/clipboard.mjs +3 -3
- package/src/tui/app/input-parsers.mjs +1 -1
- package/src/tui/app/text-layout.mjs +1 -1
- package/src/tui/app/transcript-row-estimate.mjs +360 -0
- package/src/tui/app/transcript-window.mjs +5 -347
- package/src/tui/app/use-mouse-input.mjs +1 -1
- package/src/tui/app/use-prompt-handlers.mjs +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +9 -9
- package/src/tui/components/tool-execution/text-format.mjs +2 -2
- package/src/tui/components/tool-output-format.mjs +7 -7
- package/src/tui/display-width.mjs +4 -4
- package/src/tui/dist/index.mjs +63 -97
- package/src/tui/engine/agent-envelope.mjs +5 -5
- package/src/tui/engine/notification-plan.mjs +1 -1
- package/src/tui/engine/queue-helpers.mjs +3 -3
- package/src/tui/engine/render-timing.mjs +4 -5
- package/src/tui/engine/session-api-ext.mjs +3 -3
- package/src/tui/engine/tool-result-status.mjs +5 -5
- package/src/tui/engine/tool-result-text.mjs +1 -1
- package/src/tui/engine/transcript-spill.mjs +585 -0
- package/src/tui/engine/tui-steering-persist.mjs +1 -1
- package/src/tui/engine.mjs +2 -474
- package/src/tui/lib/voice-setup.mjs +1 -1
- package/src/tui/markdown/format-token.mjs +1 -1
- package/src/tui/markdown/render-ansi.mjs +1 -1
- package/src/tui/markdown/table-layout.mjs +4 -4
- package/src/tui/paste-attachments.mjs +1 -1
- package/src/tui/prompt-history-store.mjs +3 -3
- package/src/tui/statusline-ansi-bridge.mjs +2 -2
- package/src/tui/theme.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +2 -2
- package/src/ui/ansi.mjs +0 -9
- package/src/ui/statusline-format.mjs +2 -4
- package/scripts/_bench-cwc.json +0 -20
- package/scripts/_jitter-fuzz.jsx +0 -42
- package/scripts/_jitter-fuzz2.jsx +0 -30
- package/scripts/_jitter-probe.jsx +0 -35
- package/scripts/_jp2.jsx +0 -16
- package/scripts/_smoke_wd.mjs +0 -7
- package/scripts/abort-recovery-test.mjs +0 -175
- package/scripts/agent-dispatch-abort-compose-test.mjs +0 -31
- package/scripts/agent-live-arg-guard-smoke.mjs +0 -20
- package/scripts/agent-live-path-suffix-smoke.mjs +0 -34
- package/scripts/agent-live-toolcall-args-smoke.mjs +0 -45
- package/scripts/agent-loop-policy-test.mjs +0 -37
- package/scripts/agent-model-liveness-test.mjs +0 -763
- package/scripts/agent-parallel-smoke.mjs +0 -435
- package/scripts/agent-route-batch-test.mjs +0 -40
- package/scripts/agent-tag-reuse-smoke.mjs +0 -441
- package/scripts/agent-terminal-reap-test.mjs +0 -252
- package/scripts/agent-trace-io-test.mjs +0 -133
- package/scripts/ansi-color-capability-test.mjs +0 -90
- package/scripts/anthropic-admission-retry-integration-test.mjs +0 -119
- package/scripts/anthropic-maxtokens-test.mjs +0 -119
- package/scripts/anthropic-oauth-refresh-race-test.mjs +0 -397
- package/scripts/anthropic-transport-policy-test.mjs +0 -726
- package/scripts/apply-patch-edit-smoke.mjs +0 -71
- package/scripts/arg-guard-test.mjs +0 -93
- package/scripts/async-notify-settlement-test.mjs +0 -99
- package/scripts/atomic-lock-tryonce-test.mjs +0 -125
- package/scripts/background-task-meta-smoke.mjs +0 -38
- package/scripts/bench-run.mjs +0 -508
- package/scripts/boot-smoke.mjs +0 -137
- package/scripts/build-runtime-windows.ps1 +0 -242
- package/scripts/channel-daemon-smoke.mjs +0 -1103
- package/scripts/code-graph-aggregate-cwd-test.mjs +0 -158
- package/scripts/code-graph-disk-hit-test.mjs +0 -290
- package/scripts/code-graph-dispatch-test.mjs +0 -96
- package/scripts/code-graph-root-federation-test.mjs +0 -273
- package/scripts/compact-active-turn-test.mjs +0 -68
- package/scripts/compact-file-reattach-test.mjs +0 -88
- package/scripts/compact-pressure-test.mjs +0 -788
- package/scripts/compact-prior-context-flatten-test.mjs +0 -252
- package/scripts/compact-recall-digest-test.mjs +0 -57
- package/scripts/compact-smoke.mjs +0 -1017
- package/scripts/compact-trigger-migration-smoke.mjs +0 -261
- package/scripts/compacted-placeholder-scrub-test.mjs +0 -63
- package/scripts/context-mcp-metering-test.mjs +0 -1406
- package/scripts/debounced-skills-async-save-test.mjs +0 -57
- package/scripts/deferred-tool-loading-test.mjs +0 -250
- package/scripts/desktop-session-bridge-test.mjs +0 -1013
- package/scripts/dispatch-persist-recovery-test.mjs +0 -141
- package/scripts/embedding-worker-exit-test.mjs +0 -76
- package/scripts/execution-completion-dedup-test.mjs +0 -205
- package/scripts/execution-pending-resume-kick-test.mjs +0 -151
- package/scripts/execution-resume-esc-integration-test.mjs +0 -176
- package/scripts/explore-bench-tmp.mjs +0 -36
- package/scripts/explore-bench.mjs +0 -248
- package/scripts/explore-prompt-policy-test.mjs +0 -256
- package/scripts/explore-timeout-cancel-test.mjs +0 -345
- package/scripts/find-fuzzy-hidden-test.mjs +0 -267
- package/scripts/forwarder-rebind-tail-test.mjs +0 -67
- package/scripts/freevar-smoke.mjs +0 -98
- package/scripts/gemini-provider-test.mjs +0 -1448
- package/scripts/generate-oc-icons.mjs +0 -11
- package/scripts/grok-oauth-refresh-race-test.mjs +0 -273
- package/scripts/headless-pristine-execution-test.mjs +0 -614
- package/scripts/hook-bus-test.mjs +0 -552
- package/scripts/ingest-pure-conversation-smoke.mjs +0 -175
- package/scripts/internal-comms-bench-test.mjs +0 -226
- package/scripts/internal-comms-bench.mjs +0 -853
- package/scripts/internal-comms-smoke.mjs +0 -277
- package/scripts/internal-tools-normalization-test.mjs +0 -62
- package/scripts/interrupted-turn-history-test.mjs +0 -399
- package/scripts/lead-workflow-smoke.mjs +0 -592
- package/scripts/lifecycle-api-test.mjs +0 -137
- package/scripts/live-share-test.mjs +0 -148
- package/scripts/live-worker-smoke.mjs +0 -333
- package/scripts/log-writer-guard-smoke.mjs +0 -131
- package/scripts/maintenance-default-routes-test.mjs +0 -164
- package/scripts/max-output-recovery-persist-test.mjs +0 -81
- package/scripts/max-output-recovery-test.mjs +0 -368
- package/scripts/mcp-client-normalization-test.mjs +0 -45
- package/scripts/mcp-grace-deferred-test.mjs +0 -225
- package/scripts/memory-core-input-test.mjs +0 -167
- package/scripts/memory-cycle-routing-test.mjs +0 -111
- package/scripts/memory-meta-concurrency-test.mjs +0 -20
- package/scripts/memory-pg-recovery-test.mjs +0 -59
- package/scripts/memory-rule-contract-test.mjs +0 -93
- package/scripts/model-list-sanitize-test.mjs +0 -37
- package/scripts/mouse-tracking-restore-smoke.mjs +0 -107
- package/scripts/native-edit-wire-test.mjs +0 -152
- package/scripts/notify-completion-mirror-test.mjs +0 -73
- package/scripts/openai-oauth-refresh-race-test.mjs +0 -120
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +0 -954
- package/scripts/openai-ws-early-settle-test.mjs +0 -216
- package/scripts/output-style-bench.mjs +0 -292
- package/scripts/output-style-smoke.mjs +0 -145
- package/scripts/parent-abort-link-test.mjs +0 -90
- package/scripts/patch-binary-cache-test.mjs +0 -181
- package/scripts/path-suffix-test.mjs +0 -57
- package/scripts/pending-completion-drop-test.mjs +0 -239
- package/scripts/pending-messages-lock-nonblocking-test.mjs +0 -62
- package/scripts/pretool-ask-runtime-test.mjs +0 -54
- package/scripts/process-lifecycle-test.mjs +0 -461
- package/scripts/prompt-immediate-render-test.mjs +0 -89
- package/scripts/prompt-input-parity-test.mjs +0 -145
- package/scripts/provider-admission-scheduler-test.mjs +0 -681
- package/scripts/provider-contract-test.mjs +0 -632
- package/scripts/provider-stream-stall-test.mjs +0 -276
- package/scripts/provider-toolcall-test.mjs +0 -3063
- package/scripts/reactive-compact-persist-smoke.mjs +0 -203
- package/scripts/recall-bench.mjs +0 -359
- package/scripts/resource-admission-test.mjs +0 -792
- package/scripts/result-classification-test.mjs +0 -75
- package/scripts/rg-runner-test.mjs +0 -240
- package/scripts/routing-corpus-test.mjs +0 -349
- package/scripts/sanitize-tool-pairs-test.mjs +0 -260
- package/scripts/session-bench-cache-break-test.mjs +0 -102
- package/scripts/session-bench.mjs +0 -1708
- package/scripts/session-context-bench.mjs +0 -344
- package/scripts/session-heartbeat-lifecycle-test.mjs +0 -68
- package/scripts/session-ingest-compaction-smoke.mjs +0 -279
- package/scripts/session-ingest-smoke.mjs +0 -177
- package/scripts/session-orphan-sweep-test.mjs +0 -109
- package/scripts/set-effort-config-test.mjs +0 -41
- package/scripts/shell-failure-diagnostics-test.mjs +0 -280
- package/scripts/shell-hardening-test.mjs +0 -209
- package/scripts/shell-jobs-windows-hide-test.mjs +0 -164
- package/scripts/ship-mode-test.mjs +0 -98
- package/scripts/smoke-loop-failure-summary-test.mjs +0 -38
- package/scripts/smoke-loop-failure-summary.mjs +0 -16
- package/scripts/smoke-loop-report.mjs +0 -221
- package/scripts/smoke-loop.mjs +0 -202
- package/scripts/smoke-runtime-negative.ps1 +0 -106
- package/scripts/statusline-quota-hysteresis-test.mjs +0 -62
- package/scripts/steering-drain-buckets-test.mjs +0 -467
- package/scripts/steering-fold-provenance-test.mjs +0 -71
- package/scripts/streaming-tail-window-test.mjs +0 -175
- package/scripts/submit-commandbusy-race-test.mjs +0 -257
- package/scripts/task-bench.mjs +0 -205
- package/scripts/terminal-bench-isolation-guards-test.mjs +0 -102
- package/scripts/termio-input-smoke.mjs +0 -199
- package/scripts/tool-result-hook-test.mjs +0 -48
- package/scripts/tool-smoke.mjs +0 -2453
- package/scripts/tool-tui-presentation-test.mjs +0 -286
- package/scripts/toolcall-args-test.mjs +0 -239
- package/scripts/tui-ambiguous-width-test.mjs +0 -113
- package/scripts/tui-background-failure-smoke.mjs +0 -73
- package/scripts/tui-perf-run.ps1 +0 -26
- package/scripts/tui-render-smoke.mjs +0 -90
- package/scripts/tui-runtime-load-bench-entry.jsx +0 -608
- package/scripts/tui-runtime-load-bench.mjs +0 -50
- package/scripts/tui-store-frame-batch-test.mjs +0 -99
- package/scripts/tui-transcript-jitter-harness-entry.jsx +0 -383
- package/scripts/tui-transcript-perf-test.mjs +0 -934
- package/scripts/usage-metrics-epoch-smoke.mjs +0 -114
- package/scripts/verify-release-assets-test.mjs +0 -281
- package/scripts/web-fetch-routing-test.mjs +0 -158
- package/scripts/webhook-smoke.mjs +0 -201
- package/scripts/windows-hide-spawn-options-test.mjs +0 -19
- package/scripts/worker-notify-rejection-test.mjs +0 -51
- package/scripts/write-backpressure-test.mjs +0 -147
|
@@ -1,1406 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
createContextStatus,
|
|
6
|
-
requestSerializedToolsForContext,
|
|
7
|
-
} from '../src/session-runtime/context-status.mjs';
|
|
8
|
-
import { splitToolStatusCounts } from '../src/session-runtime/session-turn-api.mjs';
|
|
9
|
-
import {
|
|
10
|
-
estimateToolSchemaBreakdown,
|
|
11
|
-
snapshotProviderRequestTools,
|
|
12
|
-
toolKind,
|
|
13
|
-
} from '../src/session-runtime/tool-catalog.mjs';
|
|
14
|
-
import {
|
|
15
|
-
invalidateProviderRequestToolsScope,
|
|
16
|
-
providerNativeToolPrefixCount,
|
|
17
|
-
runWithProviderRequestToolsScope,
|
|
18
|
-
scopedProviderRequestTools,
|
|
19
|
-
} from '../src/session-runtime/provider-request-tools.mjs';
|
|
20
|
-
import { _buildRequestBodyForCacheSmoke as buildAnthropicRequestBody } from '../src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs';
|
|
21
|
-
import { _test as anthropicApiKeyTest } from '../src/runtime/agent/orchestrator/providers/anthropic.mjs';
|
|
22
|
-
import {
|
|
23
|
-
estimateMessagesTokens,
|
|
24
|
-
estimateRequestReserveTokens,
|
|
25
|
-
estimateToolSchemaTokens,
|
|
26
|
-
toolSchemaSignature,
|
|
27
|
-
} from '../src/runtime/agent/orchestrator/session/context-utils.mjs';
|
|
28
|
-
import { agentLoop } from '../src/runtime/agent/orchestrator/session/agent-loop.mjs';
|
|
29
|
-
import {
|
|
30
|
-
recordProviderContextBaseline,
|
|
31
|
-
resolveCompactionPressureTokens,
|
|
32
|
-
resolveWorkerCompactPolicy,
|
|
33
|
-
} from '../src/runtime/agent/orchestrator/session/loop/compact-policy.mjs';
|
|
34
|
-
|
|
35
|
-
const schema = (description) => ({
|
|
36
|
-
type: 'object',
|
|
37
|
-
properties: { value: { type: 'string', description } },
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('context meters only provider-visible Anthropic deferred schemas', () => {
|
|
41
|
-
const read = { name: 'read', description: 'Read a file', inputSchema: schema('path') };
|
|
42
|
-
const mcp = { name: 'mcp__demo__lookup', description: 'MCP lookup', inputSchema: schema('mcp payload '.repeat(100)) };
|
|
43
|
-
const skill = { name: 'skill_hidden', description: 'Undiscovered skill', inputSchema: schema('skill payload '.repeat(100)) };
|
|
44
|
-
const control = {
|
|
45
|
-
name: 'hidden_control',
|
|
46
|
-
description: 'Undiscovered control',
|
|
47
|
-
annotations: { agentHidden: true },
|
|
48
|
-
inputSchema: schema('control payload '.repeat(100)),
|
|
49
|
-
};
|
|
50
|
-
const session = {
|
|
51
|
-
id: 'deferred-mcp-context',
|
|
52
|
-
provider: 'anthropic',
|
|
53
|
-
contextWindow: 100_000,
|
|
54
|
-
messages: [{ role: 'user', content: 'hello' }],
|
|
55
|
-
tools: [read],
|
|
56
|
-
deferredNativeTools: true,
|
|
57
|
-
deferredToolCatalog: [read, mcp, skill, control],
|
|
58
|
-
compaction: {},
|
|
59
|
-
};
|
|
60
|
-
const route = { provider: 'openai', model: 'test' };
|
|
61
|
-
const { contextStatus } = createContextStatus({
|
|
62
|
-
getSession: () => session,
|
|
63
|
-
getRoute: () => route,
|
|
64
|
-
getCurrentCwd: () => '',
|
|
65
|
-
getMode: () => 'default',
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
assert.deepEqual(requestSerializedToolsForContext(session, 'openai'), [read]);
|
|
69
|
-
assert.deepEqual(requestSerializedToolsForContext(session, 'anthropic'), [read]);
|
|
70
|
-
const before = contextStatus();
|
|
71
|
-
assert.equal(before.request.toolSchemaBreakdown.mcp, undefined);
|
|
72
|
-
assert.equal(before.request.toolSchemaBreakdown.skills, undefined);
|
|
73
|
-
assert.equal(before.request.toolSchemaBreakdown.control, undefined);
|
|
74
|
-
|
|
75
|
-
mcp.inputSchema.properties.value.description += ' mutated payload '.repeat(100);
|
|
76
|
-
session.deferredToolCatalog.push({
|
|
77
|
-
name: 'mcp__demo__second',
|
|
78
|
-
description: 'Second MCP tool',
|
|
79
|
-
inputSchema: schema('second payload '.repeat(100)),
|
|
80
|
-
});
|
|
81
|
-
const undiscoveredChanged = contextStatus();
|
|
82
|
-
assert.equal(undiscoveredChanged, before, 'undiscovered catalog changes must not churn status/baselines');
|
|
83
|
-
|
|
84
|
-
session.deferredDiscoveredTools = [mcp.name];
|
|
85
|
-
const discovered = contextStatus();
|
|
86
|
-
assert.notEqual(discovered, before);
|
|
87
|
-
assert.equal(discovered.request.toolSchemaBreakdown.mcp.count, 1);
|
|
88
|
-
assert.equal(discovered.request.toolSchemaBreakdown.skills, undefined);
|
|
89
|
-
assert.equal(discovered.request.toolSchemaBreakdown.control, undefined);
|
|
90
|
-
assert.ok(discovered.request.toolSchemaTokens > before.request.toolSchemaTokens);
|
|
91
|
-
assert.ok(discovered.request.reserveTokens > before.request.reserveTokens);
|
|
92
|
-
assert.ok(discovered.usedTokens > before.usedTokens);
|
|
93
|
-
|
|
94
|
-
mcp.inputSchema.properties.value.description += ' discovered mutation '.repeat(200);
|
|
95
|
-
const mutated = contextStatus();
|
|
96
|
-
assert.notEqual(mutated, discovered);
|
|
97
|
-
assert.equal(mutated.request.toolSchemaBreakdown.mcp.count, 1);
|
|
98
|
-
assert.ok(mutated.request.toolSchemaTokens > discovered.request.toolSchemaTokens);
|
|
99
|
-
assert.ok(mutated.request.reserveTokens > discovered.request.reserveTokens);
|
|
100
|
-
assert.ok(mutated.usedTokens > discovered.usedTokens);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('provider baseline ignores undiscovered schemas and invalidates after visible discovery/mutation', () => {
|
|
104
|
-
const read = { name: 'read', inputSchema: schema('path') };
|
|
105
|
-
const mcp = { name: 'mcp__demo__lookup', inputSchema: schema('initial') };
|
|
106
|
-
const session = {
|
|
107
|
-
provider: 'anthropic',
|
|
108
|
-
model: 'claude-sonnet-4-6',
|
|
109
|
-
contextWindow: 100_000,
|
|
110
|
-
messages: [{ role: 'user', content: 'hello' }],
|
|
111
|
-
tools: [read],
|
|
112
|
-
deferredNativeTools: true,
|
|
113
|
-
deferredToolCatalog: [read, mcp],
|
|
114
|
-
compaction: {},
|
|
115
|
-
};
|
|
116
|
-
const pressure = (requestTools) => resolveCompactionPressureTokens(
|
|
117
|
-
estimateMessagesTokens(session.messages),
|
|
118
|
-
resolveWorkerCompactPolicy(session, requestTools),
|
|
119
|
-
{ messages: session.messages, sessionRef: session },
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
const baseTools = requestSerializedToolsForContext(session, session.provider);
|
|
123
|
-
recordProviderContextBaseline(
|
|
124
|
-
session,
|
|
125
|
-
session.messages,
|
|
126
|
-
{ inputTokens: 80_000 },
|
|
127
|
-
{ sendTools: baseTools },
|
|
128
|
-
);
|
|
129
|
-
mcp.inputSchema.properties.value.description = 'undiscovered mutation '.repeat(200);
|
|
130
|
-
assert.equal(
|
|
131
|
-
pressure(requestSerializedToolsForContext(session, session.provider)),
|
|
132
|
-
80_000,
|
|
133
|
-
'an undiscovered schema mutation must retain the provider baseline',
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
session.deferredDiscoveredTools = [mcp.name];
|
|
137
|
-
const discoveredTools = requestSerializedToolsForContext(session, session.provider);
|
|
138
|
-
assert.notEqual(
|
|
139
|
-
pressure(discoveredTools),
|
|
140
|
-
80_000,
|
|
141
|
-
'discovery must invalidate a baseline recorded against the base surface',
|
|
142
|
-
);
|
|
143
|
-
recordProviderContextBaseline(
|
|
144
|
-
session,
|
|
145
|
-
session.messages,
|
|
146
|
-
{ inputTokens: 80_000 },
|
|
147
|
-
{ sendTools: discoveredTools },
|
|
148
|
-
);
|
|
149
|
-
assert.equal(pressure(discoveredTools), 80_000);
|
|
150
|
-
|
|
151
|
-
mcp.inputSchema.properties.value.description += ' visible mutation '.repeat(200);
|
|
152
|
-
assert.notEqual(
|
|
153
|
-
pressure(requestSerializedToolsForContext(session, session.provider)),
|
|
154
|
-
80_000,
|
|
155
|
-
'a discovered in-place schema mutation must invalidate the provider baseline',
|
|
156
|
-
);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test('agent-loop repairs history before one coherent pressure/send/baseline tool snapshot', async () => {
|
|
160
|
-
const read = { name: 'read', description: 'Read', inputSchema: schema('path') };
|
|
161
|
-
const mcp = { name: 'mcp__demo__lookup', description: 'Lookup', inputSchema: schema('mcp') };
|
|
162
|
-
const messages = [
|
|
163
|
-
{ role: 'user', content: 'hello' },
|
|
164
|
-
{
|
|
165
|
-
role: 'tool',
|
|
166
|
-
toolCallId: 'orphan-load',
|
|
167
|
-
content: 'Loaded',
|
|
168
|
-
nativeToolSearch: {
|
|
169
|
-
provider: 'anthropic-oauth',
|
|
170
|
-
toolReferences: [mcp.name],
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
|
-
];
|
|
174
|
-
const session = {
|
|
175
|
-
id: 'snapshot-repair',
|
|
176
|
-
provider: 'anthropic-oauth',
|
|
177
|
-
model: 'claude-sonnet-4-6',
|
|
178
|
-
contextWindow: 100_000,
|
|
179
|
-
messages,
|
|
180
|
-
tools: [read],
|
|
181
|
-
deferredNativeTools: true,
|
|
182
|
-
deferredToolCatalog: [read, mcp],
|
|
183
|
-
compaction: {},
|
|
184
|
-
};
|
|
185
|
-
let body;
|
|
186
|
-
let apiKeyTools;
|
|
187
|
-
let retryBody;
|
|
188
|
-
let retryApiKeyTools;
|
|
189
|
-
const provider = {
|
|
190
|
-
name: session.provider,
|
|
191
|
-
async send(outgoing, model, tools, opts) {
|
|
192
|
-
assert.equal(opts.providerToolSnapshotAuthoritative, true);
|
|
193
|
-
const late = {
|
|
194
|
-
name: 'mcp__demo__late',
|
|
195
|
-
description: 'Connected after snapshot',
|
|
196
|
-
inputSchema: schema('late'),
|
|
197
|
-
};
|
|
198
|
-
session.deferredToolCatalog.push(late);
|
|
199
|
-
session.deferredDiscoveredTools = [late.name];
|
|
200
|
-
await Promise.resolve();
|
|
201
|
-
body = buildAnthropicRequestBody(outgoing, model, tools, opts);
|
|
202
|
-
apiKeyTools = anthropicApiKeyTest.requestAnthropicTools(tools, outgoing, opts);
|
|
203
|
-
session.deferredToolCatalog.push({
|
|
204
|
-
name: 'mcp__demo__retry_late',
|
|
205
|
-
inputSchema: schema('retry late'),
|
|
206
|
-
});
|
|
207
|
-
session.deferredDiscoveredTools.push('mcp__demo__retry_late');
|
|
208
|
-
await Promise.resolve();
|
|
209
|
-
retryBody = buildAnthropicRequestBody(outgoing, model, tools, opts);
|
|
210
|
-
retryApiKeyTools = anthropicApiKeyTest.requestAnthropicTools(tools, outgoing, opts);
|
|
211
|
-
return { content: 'done', usage: { inputTokens: 100, outputTokens: 1 } };
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
const result = await agentLoop(provider, messages, session.model, session.tools, null, '', {
|
|
215
|
-
session,
|
|
216
|
-
sessionId: session.id,
|
|
217
|
-
onUsageDelta(delta) {
|
|
218
|
-
if (delta.source !== 'provider_send') return;
|
|
219
|
-
recordProviderContextBaseline(session, messages, {
|
|
220
|
-
inputTokens: delta.deltaInput,
|
|
221
|
-
outputTokens: delta.deltaOutput,
|
|
222
|
-
}, { boundary: 'request', sendTools: delta.sendTools });
|
|
223
|
-
},
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
assert.deepEqual(body.tools.map((tool) => tool.name), [read.name]);
|
|
227
|
-
assert.deepEqual(apiKeyTools.map((tool) => tool.name), [read.name]);
|
|
228
|
-
assert.deepEqual(retryBody.tools, body.tools, 'OAuth retry must reuse authoritative provider bytes');
|
|
229
|
-
assert.deepEqual(retryApiKeyTools, apiKeyTools, 'API-key retry must reuse authoritative provider bytes');
|
|
230
|
-
assert.equal(messages.some((message) => message.toolCallId === 'orphan-load'), false);
|
|
231
|
-
assert.equal(Object.isFrozen(result.lastSendTools), true);
|
|
232
|
-
assert.equal(Object.isFrozen(result.lastSendTools[0].inputSchema), true);
|
|
233
|
-
assert.equal(session.compaction.requestReserveTokens, estimateRequestReserveTokens(result.lastSendTools));
|
|
234
|
-
assert.equal(session.contextPressureBaselineToolSignature, toolSchemaSignature(result.lastSendTools));
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
test('direct Anthropic adapter callers retain unresolved deferred resolution', () => {
|
|
238
|
-
const read = { name: 'read', description: 'Read', inputSchema: schema('path') };
|
|
239
|
-
const mcp = { name: 'mcp__demo__lookup', description: 'Lookup', inputSchema: schema('mcp') };
|
|
240
|
-
const messages = [{ role: 'user', content: 'hello' }];
|
|
241
|
-
const session = {
|
|
242
|
-
provider: 'anthropic-oauth',
|
|
243
|
-
deferredNativeTools: true,
|
|
244
|
-
deferredDiscoveredTools: [mcp.name],
|
|
245
|
-
deferredToolCatalog: [read, mcp],
|
|
246
|
-
};
|
|
247
|
-
const native = { type: 'web_search_20250305', name: 'web_search', max_uses: 2 };
|
|
248
|
-
const opts = { session, nativeTools: [native] };
|
|
249
|
-
const oauthBody = buildAnthropicRequestBody(messages, 'claude-sonnet-4-6', [read], opts);
|
|
250
|
-
const apiKeyTools = anthropicApiKeyTest.requestAnthropicTools([read], messages, opts);
|
|
251
|
-
assert.deepEqual(oauthBody.tools.map((tool) => tool.name), [native.name, read.name, mcp.name]);
|
|
252
|
-
assert.deepEqual(apiKeyTools.map((tool) => tool.name), [native.name, read.name, mcp.name]);
|
|
253
|
-
assert.equal(oauthBody.tools[0], native);
|
|
254
|
-
assert.equal(apiKeyTools[0], native);
|
|
255
|
-
assert.equal(oauthBody.tools.at(-1).defer_loading, true);
|
|
256
|
-
assert.equal(apiKeyTools.at(-1).defer_loading, true);
|
|
257
|
-
|
|
258
|
-
const nativeOnlySnapshot = snapshotProviderRequestTools({
|
|
259
|
-
provider: session.provider,
|
|
260
|
-
tools: [],
|
|
261
|
-
nativeTools: [native],
|
|
262
|
-
messages,
|
|
263
|
-
session,
|
|
264
|
-
});
|
|
265
|
-
assert.deepEqual(nativeOnlySnapshot.map((tool) => tool.name), [native.name]);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
test('provider tool snapshots match JSON normalization and preserve stable provider bytes', () => {
|
|
269
|
-
let accessorDescription = 'snapshotted accessor';
|
|
270
|
-
const inherited = { inheritedMutation: 'before' };
|
|
271
|
-
const properties = Object.create(inherited);
|
|
272
|
-
Object.defineProperty(properties, 'dynamic', {
|
|
273
|
-
enumerable: true,
|
|
274
|
-
get() {
|
|
275
|
-
return { type: 'string', description: accessorDescription };
|
|
276
|
-
},
|
|
277
|
-
});
|
|
278
|
-
Object.defineProperty(properties, '__proto__', {
|
|
279
|
-
enumerable: true,
|
|
280
|
-
configurable: true,
|
|
281
|
-
writable: true,
|
|
282
|
-
value: { type: 'string', description: 'own prototype key' },
|
|
283
|
-
});
|
|
284
|
-
const inputSchema = Object.create({ inheritedSchemaField: 'not provider-visible' });
|
|
285
|
-
inputSchema.type = 'object';
|
|
286
|
-
inputSchema.properties = properties;
|
|
287
|
-
inputSchema.optionalUndefined = undefined;
|
|
288
|
-
inputSchema.optionalFunction = () => {};
|
|
289
|
-
inputSchema.optionalSymbol = Symbol('omit');
|
|
290
|
-
inputSchema.arrayValues = [
|
|
291
|
-
undefined,
|
|
292
|
-
() => {},
|
|
293
|
-
Symbol('array-null'),
|
|
294
|
-
Number.NaN,
|
|
295
|
-
Number.POSITIVE_INFINITY,
|
|
296
|
-
Number.NEGATIVE_INFINITY,
|
|
297
|
-
null,
|
|
298
|
-
'[Circular]',
|
|
299
|
-
];
|
|
300
|
-
inputSchema.nullValue = null;
|
|
301
|
-
const tool = Object.assign(Object.create({ inheritedToolField: 'hidden' }), {
|
|
302
|
-
name: 'read',
|
|
303
|
-
description: 'Read',
|
|
304
|
-
inputSchema,
|
|
305
|
-
});
|
|
306
|
-
const snapshot = snapshotProviderRequestTools({
|
|
307
|
-
provider: 'anthropic-oauth',
|
|
308
|
-
tools: [tool],
|
|
309
|
-
messages: [{ role: 'user', content: 'hello' }],
|
|
310
|
-
session: { provider: 'anthropic-oauth' },
|
|
311
|
-
});
|
|
312
|
-
const signature = toolSchemaSignature(snapshot);
|
|
313
|
-
const reserve = estimateRequestReserveTokens(snapshot);
|
|
314
|
-
|
|
315
|
-
accessorDescription = 'mutated accessor';
|
|
316
|
-
inherited.inheritedMutation = 'after';
|
|
317
|
-
properties.__proto__.description = 'mutated own prototype key';
|
|
318
|
-
inputSchema.type = 'array';
|
|
319
|
-
|
|
320
|
-
assert.equal(Object.getPrototypeOf(snapshot[0]), Object.prototype);
|
|
321
|
-
assert.equal(Object.getPrototypeOf(snapshot[0].inputSchema), Object.prototype);
|
|
322
|
-
assert.equal(snapshot[0].inputSchema.type, 'object');
|
|
323
|
-
assert.equal(snapshot[0].inputSchema.properties.dynamic.description, 'snapshotted accessor');
|
|
324
|
-
assert.equal(snapshot[0].inputSchema.properties.__proto__.description, 'own prototype key');
|
|
325
|
-
assert.equal(Object.hasOwn(snapshot[0].inputSchema.properties, '__proto__'), true);
|
|
326
|
-
assert.equal(Object.hasOwn(snapshot[0].inputSchema, 'optionalUndefined'), false);
|
|
327
|
-
assert.equal(Object.hasOwn(snapshot[0].inputSchema, 'optionalFunction'), false);
|
|
328
|
-
assert.equal(Object.hasOwn(snapshot[0].inputSchema, 'optionalSymbol'), false);
|
|
329
|
-
assert.deepEqual(snapshot[0].inputSchema.arrayValues, [
|
|
330
|
-
null, null, null, null, null, null, null, '[Circular]',
|
|
331
|
-
]);
|
|
332
|
-
assert.equal(snapshot[0].inputSchema.nullValue, null);
|
|
333
|
-
assert.equal('inheritedMutation' in snapshot[0].inputSchema.properties, false);
|
|
334
|
-
assert.equal('inheritedSchemaField' in snapshot[0].inputSchema, false);
|
|
335
|
-
assert.equal('inheritedToolField' in snapshot[0], false);
|
|
336
|
-
assert.equal(toolSchemaSignature(snapshot), signature);
|
|
337
|
-
assert.equal(estimateRequestReserveTokens(snapshot), reserve);
|
|
338
|
-
|
|
339
|
-
const opts = {
|
|
340
|
-
providerToolSnapshotAuthoritative: true,
|
|
341
|
-
session: {
|
|
342
|
-
provider: 'anthropic-oauth',
|
|
343
|
-
deferredNativeTools: true,
|
|
344
|
-
deferredDiscoveredTools: ['mcp__should_not_appear'],
|
|
345
|
-
deferredToolCatalog: [{ name: 'mcp__should_not_appear', inputSchema: schema('late') }],
|
|
346
|
-
},
|
|
347
|
-
};
|
|
348
|
-
const messages = [{ role: 'user', content: 'hello' }];
|
|
349
|
-
const oauthBody = buildAnthropicRequestBody(messages, 'claude-sonnet-4-6', snapshot, opts);
|
|
350
|
-
const apiKeyTools = anthropicApiKeyTest.requestAnthropicTools(snapshot, messages, opts);
|
|
351
|
-
for (const wireTool of [oauthBody.tools[0], apiKeyTools[0]]) {
|
|
352
|
-
assert.equal(wireTool.input_schema.type, 'object');
|
|
353
|
-
assert.equal(wireTool.input_schema.properties.dynamic.description, 'snapshotted accessor');
|
|
354
|
-
assert.equal(wireTool.input_schema.properties.__proto__.description, 'own prototype key');
|
|
355
|
-
assert.equal(Object.hasOwn(wireTool.input_schema.properties, '__proto__'), true);
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
test('provider tool snapshot captures JSON array length once', () => {
|
|
360
|
-
const snapshotArray = (values) => snapshotProviderRequestTools({
|
|
361
|
-
provider: 'anthropic-oauth',
|
|
362
|
-
tools: [{ name: 'array_tool', inputSchema: { type: 'object', values } }],
|
|
363
|
-
messages: [],
|
|
364
|
-
session: { provider: 'anthropic-oauth' },
|
|
365
|
-
})[0].inputSchema.values;
|
|
366
|
-
|
|
367
|
-
const extending = [null];
|
|
368
|
-
Object.defineProperty(extending, 0, {
|
|
369
|
-
enumerable: true,
|
|
370
|
-
get() {
|
|
371
|
-
extending.push('late');
|
|
372
|
-
return 'first';
|
|
373
|
-
},
|
|
374
|
-
});
|
|
375
|
-
assert.deepEqual(snapshotArray(extending), ['first']);
|
|
376
|
-
|
|
377
|
-
const shrinking = ['first', 'second', 'third'];
|
|
378
|
-
Object.defineProperty(shrinking, 0, {
|
|
379
|
-
enumerable: true,
|
|
380
|
-
get() {
|
|
381
|
-
shrinking.length = 1;
|
|
382
|
-
return 'first';
|
|
383
|
-
},
|
|
384
|
-
});
|
|
385
|
-
assert.deepEqual(snapshotArray(shrinking), ['first', null, null]);
|
|
386
|
-
|
|
387
|
-
const sparse = new Array(4);
|
|
388
|
-
sparse[2] = 'present';
|
|
389
|
-
assert.deepEqual(snapshotArray(sparse), [null, null, 'present', null]);
|
|
390
|
-
|
|
391
|
-
const getterFailure = new Error('array getter failed');
|
|
392
|
-
const throwing = new Array(2);
|
|
393
|
-
Object.defineProperty(throwing, 0, {
|
|
394
|
-
enumerable: true,
|
|
395
|
-
get() { throw getterFailure; },
|
|
396
|
-
});
|
|
397
|
-
assert.throws(() => snapshotArray(throwing), (error) => error === getterFailure);
|
|
398
|
-
|
|
399
|
-
let conversions = 0;
|
|
400
|
-
const fractional = new Proxy([], {
|
|
401
|
-
get(target, key, receiver) {
|
|
402
|
-
if (key === 'length') {
|
|
403
|
-
return {
|
|
404
|
-
valueOf() {
|
|
405
|
-
conversions += 1;
|
|
406
|
-
return 2.9;
|
|
407
|
-
},
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
if (key === '0') return 'zero';
|
|
411
|
-
if (key === '1') return 'one';
|
|
412
|
-
return Reflect.get(target, key, receiver);
|
|
413
|
-
},
|
|
414
|
-
});
|
|
415
|
-
assert.deepEqual(snapshotArray(fractional), ['zero', 'one']);
|
|
416
|
-
assert.equal(conversions, 1);
|
|
417
|
-
|
|
418
|
-
let nullExoticConversions = 0;
|
|
419
|
-
const nullExotic = new Proxy([], {
|
|
420
|
-
get(target, key, receiver) {
|
|
421
|
-
if (key === 'length') {
|
|
422
|
-
return {
|
|
423
|
-
[Symbol.toPrimitive]: null,
|
|
424
|
-
valueOf() {
|
|
425
|
-
nullExoticConversions += 1;
|
|
426
|
-
return 1.9;
|
|
427
|
-
},
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
if (key === '0') return 'ordinary';
|
|
431
|
-
return Reflect.get(target, key, receiver);
|
|
432
|
-
},
|
|
433
|
-
});
|
|
434
|
-
assert.deepEqual(snapshotArray(nullExotic), ['ordinary']);
|
|
435
|
-
assert.equal(nullExoticConversions, 1);
|
|
436
|
-
|
|
437
|
-
const cappedFraction = new Proxy([], {
|
|
438
|
-
get(target, key, receiver) {
|
|
439
|
-
if (key === 'length') return 1_000_000.1;
|
|
440
|
-
return Reflect.get(target, key, receiver);
|
|
441
|
-
},
|
|
442
|
-
});
|
|
443
|
-
assert.equal(snapshotArray(cappedFraction).length, 1_000_000);
|
|
444
|
-
|
|
445
|
-
for (const [rawLength, expected] of [
|
|
446
|
-
[-1, []],
|
|
447
|
-
[Number.NaN, []],
|
|
448
|
-
[0.9, []],
|
|
449
|
-
]) {
|
|
450
|
-
const values = new Proxy([], {
|
|
451
|
-
get(target, key, receiver) {
|
|
452
|
-
return key === 'length' ? rawLength : Reflect.get(target, key, receiver);
|
|
453
|
-
},
|
|
454
|
-
});
|
|
455
|
-
assert.deepEqual(snapshotArray(values), expected);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
for (const rawLength of [
|
|
459
|
-
1n,
|
|
460
|
-
Symbol('length'),
|
|
461
|
-
{ valueOf: () => 1n },
|
|
462
|
-
{ [Symbol.toPrimitive]: () => Symbol('length') },
|
|
463
|
-
]) {
|
|
464
|
-
const values = new Proxy([], {
|
|
465
|
-
get(target, key, receiver) {
|
|
466
|
-
return key === 'length' ? rawLength : Reflect.get(target, key, receiver);
|
|
467
|
-
},
|
|
468
|
-
});
|
|
469
|
-
assert.throws(
|
|
470
|
-
() => snapshotArray(values),
|
|
471
|
-
(error) => error instanceof TypeError
|
|
472
|
-
&& error.message === 'provider tool snapshot: invalid array length',
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
for (const rawLength of [Number.POSITIVE_INFINITY, 1_000_001, Number.MAX_SAFE_INTEGER]) {
|
|
476
|
-
const values = new Proxy([], {
|
|
477
|
-
get(target, key, receiver) {
|
|
478
|
-
return key === 'length' ? rawLength : Reflect.get(target, key, receiver);
|
|
479
|
-
},
|
|
480
|
-
});
|
|
481
|
-
assert.throws(
|
|
482
|
-
() => snapshotArray(values),
|
|
483
|
-
(error) => error instanceof RangeError
|
|
484
|
-
&& error.message === 'provider tool snapshot: array length exceeds safe limit 1000000',
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
test('request tool scopes isolate parallel and nested same-session gauges and restore on errors', async () => {
|
|
490
|
-
const session = {
|
|
491
|
-
id: 'scope-isolation',
|
|
492
|
-
provider: 'anthropic-oauth',
|
|
493
|
-
messages: [{ role: 'user', content: 'fallback' }],
|
|
494
|
-
tools: [{ name: 'fallback', inputSchema: schema('fallback') }],
|
|
495
|
-
};
|
|
496
|
-
const messagesA = [{ role: 'user', content: 'a' }];
|
|
497
|
-
const messagesB = [{ role: 'user', content: 'b' }];
|
|
498
|
-
const messagesNested = [{ role: 'user', content: 'nested' }];
|
|
499
|
-
const snapshotA = snapshotProviderRequestTools({
|
|
500
|
-
provider: session.provider,
|
|
501
|
-
nativeTools: [{ type: 'native_a', name: 'native_a', input_schema: { ambiguous: true } }],
|
|
502
|
-
tools: [{ name: 'a', inputSchema: schema('a') }],
|
|
503
|
-
messages: messagesA,
|
|
504
|
-
session,
|
|
505
|
-
});
|
|
506
|
-
const snapshotB = snapshotProviderRequestTools({
|
|
507
|
-
provider: session.provider,
|
|
508
|
-
tools: [{ name: 'b', inputSchema: schema('b') }],
|
|
509
|
-
messages: messagesB,
|
|
510
|
-
session,
|
|
511
|
-
});
|
|
512
|
-
const nestedSnapshot = snapshotProviderRequestTools({
|
|
513
|
-
provider: session.provider,
|
|
514
|
-
tools: [{ name: 'nested', inputSchema: schema('nested') }],
|
|
515
|
-
messages: messagesNested,
|
|
516
|
-
session,
|
|
517
|
-
});
|
|
518
|
-
const scopeFor = (messages, requestTools) => ({
|
|
519
|
-
session,
|
|
520
|
-
provider: session.provider,
|
|
521
|
-
messages,
|
|
522
|
-
requestTools,
|
|
523
|
-
nativePrefixCount: providerNativeToolPrefixCount(requestTools),
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
const [seenA, seenB] = await Promise.all([
|
|
527
|
-
runWithProviderRequestToolsScope(scopeFor(messagesA, snapshotA), async () => {
|
|
528
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
529
|
-
assert.equal(
|
|
530
|
-
requestSerializedToolsForContext(session, session.provider, messagesA),
|
|
531
|
-
snapshotA,
|
|
532
|
-
);
|
|
533
|
-
assert.equal(scopedProviderRequestTools(session, 'anthropic', messagesA), null);
|
|
534
|
-
await runWithProviderRequestToolsScope(scopeFor(messagesNested, nestedSnapshot), async () => {
|
|
535
|
-
await Promise.resolve();
|
|
536
|
-
assert.equal(
|
|
537
|
-
requestSerializedToolsForContext(session, session.provider, messagesNested),
|
|
538
|
-
nestedSnapshot,
|
|
539
|
-
);
|
|
540
|
-
});
|
|
541
|
-
assert.equal(
|
|
542
|
-
requestSerializedToolsForContext(session, session.provider, messagesA),
|
|
543
|
-
snapshotA,
|
|
544
|
-
);
|
|
545
|
-
return requestSerializedToolsForContext(session, session.provider, messagesA);
|
|
546
|
-
}),
|
|
547
|
-
runWithProviderRequestToolsScope(scopeFor(messagesB, snapshotB), async () => {
|
|
548
|
-
await Promise.resolve();
|
|
549
|
-
return requestSerializedToolsForContext(session, session.provider, messagesB);
|
|
550
|
-
}),
|
|
551
|
-
]);
|
|
552
|
-
assert.equal(seenA, snapshotA);
|
|
553
|
-
assert.equal(seenB, snapshotB);
|
|
554
|
-
|
|
555
|
-
let delayedErrorLookup;
|
|
556
|
-
let resolveDelayedError;
|
|
557
|
-
const delayedError = new Promise((resolve) => {
|
|
558
|
-
resolveDelayedError = resolve;
|
|
559
|
-
});
|
|
560
|
-
await assert.rejects(
|
|
561
|
-
runWithProviderRequestToolsScope(scopeFor(messagesA, snapshotA), async () => {
|
|
562
|
-
assert.equal(
|
|
563
|
-
requestSerializedToolsForContext(session, session.provider, messagesA),
|
|
564
|
-
snapshotA,
|
|
565
|
-
);
|
|
566
|
-
setTimeout(() => {
|
|
567
|
-
delayedErrorLookup = scopedProviderRequestTools(session, session.provider, messagesA);
|
|
568
|
-
resolveDelayedError();
|
|
569
|
-
}, 10);
|
|
570
|
-
throw new Error('scope failure');
|
|
571
|
-
}),
|
|
572
|
-
/scope failure/,
|
|
573
|
-
);
|
|
574
|
-
await delayedError;
|
|
575
|
-
assert.equal(delayedErrorLookup, null);
|
|
576
|
-
assert.equal(scopedProviderRequestTools(session, session.provider, messagesA), null);
|
|
577
|
-
|
|
578
|
-
let delayedOldLookup;
|
|
579
|
-
const delayedOld = new Promise((resolve) => {
|
|
580
|
-
runWithProviderRequestToolsScope(scopeFor(messagesA, snapshotA), () => {
|
|
581
|
-
setTimeout(() => {
|
|
582
|
-
delayedOldLookup = scopedProviderRequestTools(session, session.provider, messagesA);
|
|
583
|
-
resolve();
|
|
584
|
-
}, 15);
|
|
585
|
-
});
|
|
586
|
-
});
|
|
587
|
-
await runWithProviderRequestToolsScope(scopeFor(messagesA, snapshotB), async () => {
|
|
588
|
-
assert.equal(
|
|
589
|
-
scopedProviderRequestTools(session, session.provider, messagesA)?.requestTools,
|
|
590
|
-
snapshotB,
|
|
591
|
-
);
|
|
592
|
-
await delayedOld;
|
|
593
|
-
assert.equal(delayedOldLookup, null);
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
await runWithProviderRequestToolsScope(scopeFor(messagesA, snapshotA), async () => {
|
|
597
|
-
const outerScope = scopedProviderRequestTools(session, session.provider, messagesA);
|
|
598
|
-
await runWithProviderRequestToolsScope(scopeFor(messagesNested, nestedSnapshot), async () => {
|
|
599
|
-
invalidateProviderRequestToolsScope(outerScope);
|
|
600
|
-
assert.equal(
|
|
601
|
-
scopedProviderRequestTools(session, session.provider, messagesNested)?.requestTools,
|
|
602
|
-
nestedSnapshot,
|
|
603
|
-
);
|
|
604
|
-
});
|
|
605
|
-
assert.equal(scopedProviderRequestTools(session, session.provider, messagesA), null);
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
const explicitNative = [{ type: 'fallback_native', name: 'fallback_native' }];
|
|
609
|
-
const fallback = requestSerializedToolsForContext(
|
|
610
|
-
session,
|
|
611
|
-
session.provider,
|
|
612
|
-
session.messages,
|
|
613
|
-
{ nativeTools: explicitNative },
|
|
614
|
-
);
|
|
615
|
-
assert.deepEqual(fallback.map((tool) => tool.name), ['fallback_native', 'fallback']);
|
|
616
|
-
assert.equal(providerNativeToolPrefixCount(fallback), 1);
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
test('selected deferred snapshots force one pre-freeze deferLoading value', () => {
|
|
620
|
-
const snapshotSelected = (tool) => snapshotProviderRequestTools({
|
|
621
|
-
provider: 'anthropic-oauth',
|
|
622
|
-
tools: [{ name: 'read', inputSchema: schema('path') }],
|
|
623
|
-
messages: [],
|
|
624
|
-
session: {
|
|
625
|
-
provider: 'anthropic-oauth',
|
|
626
|
-
deferredNativeTools: true,
|
|
627
|
-
deferredDiscoveredTools: ['mcp__demo__selected'],
|
|
628
|
-
deferredToolCatalog: [tool],
|
|
629
|
-
},
|
|
630
|
-
});
|
|
631
|
-
for (const initial of [true, false]) {
|
|
632
|
-
const tool = { name: 'mcp__demo__selected', inputSchema: schema(String(initial)) };
|
|
633
|
-
Object.defineProperty(tool, 'deferLoading', {
|
|
634
|
-
value: initial,
|
|
635
|
-
enumerable: true,
|
|
636
|
-
configurable: false,
|
|
637
|
-
});
|
|
638
|
-
const selected = snapshotSelected(tool).at(-1);
|
|
639
|
-
assert.equal(selected.deferLoading, true);
|
|
640
|
-
assert.equal(Object.isFrozen(selected), true);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
let getterReads = 0;
|
|
644
|
-
const accessor = { name: 'mcp__demo__selected', inputSchema: schema('getter') };
|
|
645
|
-
Object.defineProperty(accessor, 'deferLoading', {
|
|
646
|
-
enumerable: true,
|
|
647
|
-
configurable: false,
|
|
648
|
-
get() {
|
|
649
|
-
getterReads += 1;
|
|
650
|
-
if (getterReads > 1) throw new Error('deferLoading read twice');
|
|
651
|
-
return false;
|
|
652
|
-
},
|
|
653
|
-
});
|
|
654
|
-
const selected = snapshotSelected(accessor).at(-1);
|
|
655
|
-
assert.equal(getterReads, 1);
|
|
656
|
-
assert.equal(selected.deferLoading, true);
|
|
657
|
-
|
|
658
|
-
let calls = 0;
|
|
659
|
-
const callable = {
|
|
660
|
-
name: 'mcp__demo__selected',
|
|
661
|
-
toJSON() {
|
|
662
|
-
calls += 1;
|
|
663
|
-
const properties = {};
|
|
664
|
-
Object.defineProperty(properties, '__proto__', {
|
|
665
|
-
value: { type: 'string' },
|
|
666
|
-
enumerable: true,
|
|
667
|
-
});
|
|
668
|
-
return {
|
|
669
|
-
name: 'mcp__demo__selected',
|
|
670
|
-
inputSchema: {
|
|
671
|
-
type: 'object',
|
|
672
|
-
properties,
|
|
673
|
-
},
|
|
674
|
-
deferLoading: false,
|
|
675
|
-
};
|
|
676
|
-
},
|
|
677
|
-
};
|
|
678
|
-
const callableSelected = snapshotSelected(callable).at(-1);
|
|
679
|
-
assert.equal(calls, 1);
|
|
680
|
-
assert.equal(callableSelected.deferLoading, true);
|
|
681
|
-
assert.equal(Object.hasOwn(callableSelected.inputSchema.properties, '__proto__'), true);
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
test('authoritative snapshot includes native tools in gauge, pressure, baseline, and both Anthropic bodies', async () => {
|
|
685
|
-
let schemaReads = 0;
|
|
686
|
-
let schemaToJsonReads = 0;
|
|
687
|
-
let schemaToJsonCalls = 0;
|
|
688
|
-
const read = { name: 'read' };
|
|
689
|
-
Object.defineProperty(read, 'inputSchema', {
|
|
690
|
-
enumerable: true,
|
|
691
|
-
get() {
|
|
692
|
-
schemaReads += 1;
|
|
693
|
-
if (schemaReads > 1) throw new Error('request schema observed twice');
|
|
694
|
-
const inputSchema = {};
|
|
695
|
-
Object.defineProperty(inputSchema, 'toJSON', {
|
|
696
|
-
enumerable: true,
|
|
697
|
-
get() {
|
|
698
|
-
schemaToJsonReads += 1;
|
|
699
|
-
if (schemaToJsonReads > 1) throw new Error('request schema toJSON observed twice');
|
|
700
|
-
return function toJSON() {
|
|
701
|
-
schemaToJsonCalls += 1;
|
|
702
|
-
if (schemaToJsonCalls > 1) throw new Error('request schema toJSON called twice');
|
|
703
|
-
return schema('single gauge/send snapshot');
|
|
704
|
-
};
|
|
705
|
-
},
|
|
706
|
-
});
|
|
707
|
-
return inputSchema;
|
|
708
|
-
},
|
|
709
|
-
});
|
|
710
|
-
const native = {
|
|
711
|
-
type: 'web_search_20250305',
|
|
712
|
-
name: 'web_search',
|
|
713
|
-
max_uses: 3,
|
|
714
|
-
};
|
|
715
|
-
const ambiguousNative = {
|
|
716
|
-
type: 'ambiguous_native',
|
|
717
|
-
name: 'read',
|
|
718
|
-
input_schema: { nativeAlias: 'input_schema' },
|
|
719
|
-
inputSchema: { nativeAlias: 'inputSchema' },
|
|
720
|
-
parameters: { nativeAlias: 'parameters' },
|
|
721
|
-
schema: { nativeAlias: 'schema' },
|
|
722
|
-
};
|
|
723
|
-
const session = {
|
|
724
|
-
id: 'native-authoritative-snapshot',
|
|
725
|
-
owner: 'agent',
|
|
726
|
-
provider: 'anthropic-oauth',
|
|
727
|
-
model: 'claude-sonnet-4-6',
|
|
728
|
-
contextWindow: 100_000,
|
|
729
|
-
messages: [{ role: 'user', content: 'hello' }],
|
|
730
|
-
tools: [read],
|
|
731
|
-
compaction: {},
|
|
732
|
-
};
|
|
733
|
-
const { contextStatus } = createContextStatus({
|
|
734
|
-
getSession: () => session,
|
|
735
|
-
getRoute: () => ({ provider: session.provider, model: session.model }),
|
|
736
|
-
getCurrentCwd: () => '',
|
|
737
|
-
getMode: () => 'default',
|
|
738
|
-
getNativeTools() {
|
|
739
|
-
throw new Error('scoped gauge must not read live native tools');
|
|
740
|
-
},
|
|
741
|
-
});
|
|
742
|
-
let firstBody;
|
|
743
|
-
let retryBody;
|
|
744
|
-
let apiKeyTools;
|
|
745
|
-
let gauge;
|
|
746
|
-
let baselineGauge;
|
|
747
|
-
const provider = {
|
|
748
|
-
name: session.provider,
|
|
749
|
-
async send(messages, model, tools, opts) {
|
|
750
|
-
gauge = contextStatus();
|
|
751
|
-
assert.equal(schemaReads, 1);
|
|
752
|
-
assert.equal(schemaToJsonReads, 1);
|
|
753
|
-
assert.equal(schemaToJsonCalls, 1);
|
|
754
|
-
assert.equal(opts.providerNativeToolPrefixCount, 2);
|
|
755
|
-
const expectedSignature = toolSchemaSignature(tools);
|
|
756
|
-
const expectedReserve = estimateRequestReserveTokens(tools);
|
|
757
|
-
native.max_uses = 99;
|
|
758
|
-
ambiguousNative.input_schema.nativeAlias = 'mutated';
|
|
759
|
-
opts.nativeTools.push({ type: 'code_execution_20250825', name: 'late_native' });
|
|
760
|
-
opts.providerNativeToolPrefixCount = 0;
|
|
761
|
-
firstBody = buildAnthropicRequestBody(messages, model, tools, opts);
|
|
762
|
-
apiKeyTools = anthropicApiKeyTest.requestAnthropicTools(tools, messages, opts);
|
|
763
|
-
retryBody = buildAnthropicRequestBody(messages, model, tools, opts);
|
|
764
|
-
assert.equal(toolSchemaSignature(tools), expectedSignature);
|
|
765
|
-
assert.equal(estimateRequestReserveTokens(tools), expectedReserve);
|
|
766
|
-
return { content: 'done', usage: { inputTokens: 100, outputTokens: 1 } };
|
|
767
|
-
},
|
|
768
|
-
};
|
|
769
|
-
const result = await agentLoop(provider, session.messages, session.model, session.tools, null, '', {
|
|
770
|
-
session,
|
|
771
|
-
sessionId: session.id,
|
|
772
|
-
nativeTools: [native, ambiguousNative],
|
|
773
|
-
onUsageDelta(delta) {
|
|
774
|
-
if (delta.source !== 'provider_send') return;
|
|
775
|
-
baselineGauge = contextStatus();
|
|
776
|
-
recordProviderContextBaseline(session, session.messages, {
|
|
777
|
-
inputTokens: delta.deltaInput,
|
|
778
|
-
outputTokens: delta.deltaOutput,
|
|
779
|
-
}, { boundary: 'request', sendTools: delta.sendTools });
|
|
780
|
-
},
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
assert.equal(schemaReads, 1);
|
|
784
|
-
assert.equal(schemaToJsonReads, 1);
|
|
785
|
-
assert.equal(schemaToJsonCalls, 1);
|
|
786
|
-
assert.deepEqual(result.lastSendTools.map((tool) => tool.name), ['web_search', 'read', 'read']);
|
|
787
|
-
assert.equal(providerNativeToolPrefixCount(result.lastSendTools), 2);
|
|
788
|
-
assert.deepEqual(firstBody.tools, retryBody.tools);
|
|
789
|
-
assert.deepEqual(firstBody.tools, apiKeyTools);
|
|
790
|
-
assert.equal(firstBody.tools[0].max_uses, 3);
|
|
791
|
-
assert.deepEqual(firstBody.tools[1], {
|
|
792
|
-
type: 'ambiguous_native',
|
|
793
|
-
name: 'read',
|
|
794
|
-
input_schema: { nativeAlias: 'input_schema' },
|
|
795
|
-
inputSchema: { nativeAlias: 'inputSchema' },
|
|
796
|
-
parameters: { nativeAlias: 'parameters' },
|
|
797
|
-
schema: { nativeAlias: 'schema' },
|
|
798
|
-
});
|
|
799
|
-
assert.equal(firstBody.tools.some((tool) => tool.name === 'late_native'), false);
|
|
800
|
-
assert.equal(gauge.request.reserveTokens, estimateRequestReserveTokens(result.lastSendTools));
|
|
801
|
-
assert.equal(baselineGauge.request.reserveTokens, gauge.request.reserveTokens);
|
|
802
|
-
const nativeWebOnly = snapshotProviderRequestTools({
|
|
803
|
-
provider: session.provider,
|
|
804
|
-
nativeTools: [result.lastSendTools[0]],
|
|
805
|
-
tools: [],
|
|
806
|
-
messages: session.messages,
|
|
807
|
-
session,
|
|
808
|
-
});
|
|
809
|
-
const nativeAmbiguousOnly = snapshotProviderRequestTools({
|
|
810
|
-
provider: session.provider,
|
|
811
|
-
nativeTools: [result.lastSendTools[1]],
|
|
812
|
-
tools: [],
|
|
813
|
-
messages: session.messages,
|
|
814
|
-
session,
|
|
815
|
-
});
|
|
816
|
-
const functionOnly = snapshotProviderRequestTools({
|
|
817
|
-
provider: session.provider,
|
|
818
|
-
tools: [result.lastSendTools[2]],
|
|
819
|
-
messages: session.messages,
|
|
820
|
-
session,
|
|
821
|
-
});
|
|
822
|
-
assert.equal(
|
|
823
|
-
gauge.request.toolSchemaBreakdown.other.tokens,
|
|
824
|
-
estimateToolSchemaTokens(nativeWebOnly),
|
|
825
|
-
);
|
|
826
|
-
assert.equal(
|
|
827
|
-
gauge.request.toolSchemaBreakdown.code.tokens,
|
|
828
|
-
estimateToolSchemaTokens(nativeAmbiguousOnly) + estimateToolSchemaTokens(functionOnly),
|
|
829
|
-
);
|
|
830
|
-
assert.deepEqual(
|
|
831
|
-
estimateToolSchemaBreakdown(result.lastSendTools),
|
|
832
|
-
gauge.request.toolSchemaBreakdown,
|
|
833
|
-
);
|
|
834
|
-
assert.equal(session.compaction.requestReserveTokens, estimateRequestReserveTokens(result.lastSendTools));
|
|
835
|
-
assert.equal(session.contextPressureBaselineToolSignature, toolSchemaSignature(result.lastSendTools));
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
test('provider tool snapshots reject BigInt, cycles, and throwing accessors without wire substitutes', () => {
|
|
839
|
-
const snapshotForSchema = (inputSchema) => snapshotProviderRequestTools({
|
|
840
|
-
provider: 'anthropic-oauth',
|
|
841
|
-
tools: [{ name: 'read', inputSchema }],
|
|
842
|
-
messages: [],
|
|
843
|
-
session: { provider: 'anthropic-oauth' },
|
|
844
|
-
});
|
|
845
|
-
|
|
846
|
-
assert.throws(
|
|
847
|
-
() => snapshotForSchema({ type: 'object', invalid: 1n }),
|
|
848
|
-
(error) => error instanceof TypeError
|
|
849
|
-
&& error.message === 'provider tool snapshot: BigInt is not JSON-serializable',
|
|
850
|
-
);
|
|
851
|
-
assert.throws(
|
|
852
|
-
() => snapshotForSchema({ type: 'object', invalid: Object(1n) }),
|
|
853
|
-
(error) => error instanceof TypeError
|
|
854
|
-
&& error.message === 'provider tool snapshot: BigInt is not JSON-serializable',
|
|
855
|
-
);
|
|
856
|
-
|
|
857
|
-
const cyclic = { type: 'object' };
|
|
858
|
-
cyclic.self = cyclic;
|
|
859
|
-
assert.throws(
|
|
860
|
-
() => snapshotForSchema(cyclic),
|
|
861
|
-
(error) => error instanceof TypeError
|
|
862
|
-
&& error.message === 'provider tool snapshot: cyclic value is not JSON-serializable',
|
|
863
|
-
);
|
|
864
|
-
|
|
865
|
-
const accessorFailure = new Error('schema accessor failed');
|
|
866
|
-
const throwing = { type: 'object' };
|
|
867
|
-
Object.defineProperty(throwing, 'properties', {
|
|
868
|
-
enumerable: true,
|
|
869
|
-
get() { throw accessorFailure; },
|
|
870
|
-
});
|
|
871
|
-
assert.throws(
|
|
872
|
-
() => snapshotForSchema(throwing),
|
|
873
|
-
(error) => error === accessorFailure,
|
|
874
|
-
);
|
|
875
|
-
|
|
876
|
-
const legitimate = snapshotForSchema({
|
|
877
|
-
type: 'object',
|
|
878
|
-
properties: {
|
|
879
|
-
literal: { type: 'string', description: '[Circular]' },
|
|
880
|
-
nullable: { type: 'string', default: null },
|
|
881
|
-
},
|
|
882
|
-
});
|
|
883
|
-
assert.equal(legitimate[0].inputSchema.properties.literal.description, '[Circular]');
|
|
884
|
-
assert.equal(legitimate[0].inputSchema.properties.nullable.default, null);
|
|
885
|
-
assert.doesNotThrow(() => toolSchemaSignature(legitimate));
|
|
886
|
-
assert.doesNotThrow(() => estimateRequestReserveTokens(legitimate));
|
|
887
|
-
});
|
|
888
|
-
|
|
889
|
-
test('provider tool snapshot evaluates active and selected catalog names exactly once', () => {
|
|
890
|
-
let activeNameReads = 0;
|
|
891
|
-
const active = {
|
|
892
|
-
description: 'Read',
|
|
893
|
-
inputSchema: schema('active'),
|
|
894
|
-
};
|
|
895
|
-
Object.defineProperty(active, 'name', {
|
|
896
|
-
enumerable: true,
|
|
897
|
-
get() {
|
|
898
|
-
activeNameReads += 1;
|
|
899
|
-
if (activeNameReads > 1) throw new Error('active name read twice');
|
|
900
|
-
return 'read';
|
|
901
|
-
},
|
|
902
|
-
});
|
|
903
|
-
|
|
904
|
-
let selectedNameReads = 0;
|
|
905
|
-
let selectedSchemaReads = 0;
|
|
906
|
-
const selected = { description: 'Selected MCP' };
|
|
907
|
-
Object.defineProperty(selected, 'name', {
|
|
908
|
-
enumerable: true,
|
|
909
|
-
get() {
|
|
910
|
-
selectedNameReads += 1;
|
|
911
|
-
if (selectedNameReads > 1) throw new Error('selected name read twice');
|
|
912
|
-
return 'mcp__demo__selected';
|
|
913
|
-
},
|
|
914
|
-
});
|
|
915
|
-
Object.defineProperty(selected, 'inputSchema', {
|
|
916
|
-
enumerable: true,
|
|
917
|
-
get() {
|
|
918
|
-
selectedSchemaReads += 1;
|
|
919
|
-
return schema('selected');
|
|
920
|
-
},
|
|
921
|
-
});
|
|
922
|
-
const undiscovered = { name: 'mcp__demo__undiscovered' };
|
|
923
|
-
Object.defineProperty(undiscovered, 'inputSchema', {
|
|
924
|
-
enumerable: true,
|
|
925
|
-
get() { throw new Error('undiscovered schema must stay inert'); },
|
|
926
|
-
});
|
|
927
|
-
const duplicate = { name: 'mcp__demo__selected' };
|
|
928
|
-
Object.defineProperty(duplicate, 'inputSchema', {
|
|
929
|
-
enumerable: true,
|
|
930
|
-
get() { throw new Error('duplicate schema must stay inert'); },
|
|
931
|
-
});
|
|
932
|
-
const session = {
|
|
933
|
-
provider: 'anthropic-oauth',
|
|
934
|
-
deferredNativeTools: true,
|
|
935
|
-
deferredDiscoveredTools: ['mcp__demo__selected'],
|
|
936
|
-
// The active object is intentionally also in the catalog; identity skip
|
|
937
|
-
// must prevent a second active-name observation.
|
|
938
|
-
deferredToolCatalog: [active, selected, undiscovered, duplicate],
|
|
939
|
-
};
|
|
940
|
-
const messages = [{ role: 'user', content: 'hello' }];
|
|
941
|
-
const snapshot = snapshotProviderRequestTools({
|
|
942
|
-
provider: session.provider,
|
|
943
|
-
tools: [active],
|
|
944
|
-
messages,
|
|
945
|
-
session,
|
|
946
|
-
});
|
|
947
|
-
|
|
948
|
-
assert.equal(activeNameReads, 1);
|
|
949
|
-
assert.equal(selectedNameReads, 1);
|
|
950
|
-
assert.equal(selectedSchemaReads, 1);
|
|
951
|
-
assert.deepEqual(snapshot.map((tool) => tool.name), ['read', 'mcp__demo__selected']);
|
|
952
|
-
assert.equal(new Set(snapshot.map((tool) => tool.name)).size, snapshot.length);
|
|
953
|
-
const signature = toolSchemaSignature(snapshot);
|
|
954
|
-
const reserve = estimateRequestReserveTokens(snapshot);
|
|
955
|
-
const opts = { session, providerToolSnapshotAuthoritative: true };
|
|
956
|
-
const oauthBody = buildAnthropicRequestBody(messages, 'claude-sonnet-4-6', snapshot, opts);
|
|
957
|
-
const apiKeyTools = anthropicApiKeyTest.requestAnthropicTools(snapshot, messages, opts);
|
|
958
|
-
assert.deepEqual(oauthBody.tools.map((tool) => tool.name), snapshot.map((tool) => tool.name));
|
|
959
|
-
assert.deepEqual(apiKeyTools.map((tool) => tool.name), snapshot.map((tool) => tool.name));
|
|
960
|
-
assert.equal(toolSchemaSignature(snapshot), signature);
|
|
961
|
-
assert.equal(estimateRequestReserveTokens(snapshot), reserve);
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
test('selected deferred tool identity is stable before pressure, baseline, or provider effects', async () => {
|
|
965
|
-
const read = { name: 'read', inputSchema: schema('path') };
|
|
966
|
-
const selectedTool = (toJSON) => ({
|
|
967
|
-
name: 'mcp__demo__selected',
|
|
968
|
-
toJSON,
|
|
969
|
-
});
|
|
970
|
-
const snapshotSelected = (tool) => snapshotProviderRequestTools({
|
|
971
|
-
provider: 'anthropic-oauth',
|
|
972
|
-
tools: [read],
|
|
973
|
-
messages: [{ role: 'user', content: 'hello' }],
|
|
974
|
-
session: {
|
|
975
|
-
provider: 'anthropic-oauth',
|
|
976
|
-
deferredNativeTools: true,
|
|
977
|
-
deferredDiscoveredTools: ['mcp__demo__selected'],
|
|
978
|
-
deferredToolCatalog: [read, tool],
|
|
979
|
-
},
|
|
980
|
-
});
|
|
981
|
-
const identityError = (error) => error instanceof TypeError
|
|
982
|
-
&& error.message === 'provider tool snapshot: selected tool identity mismatch for "mcp__demo__selected"';
|
|
983
|
-
|
|
984
|
-
for (const tool of [
|
|
985
|
-
selectedTool(() => ({ name: 'mcp__demo__other', inputSchema: schema('changed identity') })),
|
|
986
|
-
selectedTool(() => ({ inputSchema: schema('missing identity') })),
|
|
987
|
-
selectedTool(() => ({ name: 42, inputSchema: schema('non-string identity') })),
|
|
988
|
-
]) {
|
|
989
|
-
assert.throws(() => snapshotSelected(tool), identityError);
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
const activeByNormalizedName = snapshotProviderRequestTools({
|
|
993
|
-
provider: 'anthropic-oauth',
|
|
994
|
-
tools: [
|
|
995
|
-
{
|
|
996
|
-
name: 'pre_normalized_name',
|
|
997
|
-
toJSON: () => ({ name: 'normalized_name', inputSchema: schema('first') }),
|
|
998
|
-
},
|
|
999
|
-
{ name: 'normalized_name', inputSchema: schema('duplicate') },
|
|
1000
|
-
],
|
|
1001
|
-
messages: [],
|
|
1002
|
-
session: { provider: 'anthropic-oauth' },
|
|
1003
|
-
});
|
|
1004
|
-
assert.deepEqual(activeByNormalizedName.map((tool) => tool.name), ['normalized_name']);
|
|
1005
|
-
|
|
1006
|
-
let providerCalls = 0;
|
|
1007
|
-
const session = {
|
|
1008
|
-
id: 'selected-identity-mismatch',
|
|
1009
|
-
owner: 'agent',
|
|
1010
|
-
provider: 'anthropic-oauth',
|
|
1011
|
-
model: 'claude-sonnet-4-6',
|
|
1012
|
-
contextWindow: 100_000,
|
|
1013
|
-
messages: [{ role: 'user', content: 'hello' }],
|
|
1014
|
-
tools: [read],
|
|
1015
|
-
deferredNativeTools: true,
|
|
1016
|
-
deferredDiscoveredTools: ['mcp__demo__selected'],
|
|
1017
|
-
deferredToolCatalog: [
|
|
1018
|
-
read,
|
|
1019
|
-
selectedTool(() => ({ name: 'mcp__demo__other', inputSchema: schema('agent mismatch') })),
|
|
1020
|
-
],
|
|
1021
|
-
compaction: {},
|
|
1022
|
-
};
|
|
1023
|
-
const provider = {
|
|
1024
|
-
name: session.provider,
|
|
1025
|
-
async send() {
|
|
1026
|
-
providerCalls += 1;
|
|
1027
|
-
return { content: 'must not send', usage: { inputTokens: 1, outputTokens: 1 } };
|
|
1028
|
-
},
|
|
1029
|
-
};
|
|
1030
|
-
await assert.rejects(
|
|
1031
|
-
agentLoop(provider, session.messages, session.model, session.tools, null, '', {
|
|
1032
|
-
session,
|
|
1033
|
-
sessionId: session.id,
|
|
1034
|
-
}),
|
|
1035
|
-
identityError,
|
|
1036
|
-
);
|
|
1037
|
-
assert.equal(providerCalls, 0);
|
|
1038
|
-
assert.deepEqual(session.compaction, {});
|
|
1039
|
-
assert.equal(Object.hasOwn(session, 'contextPressureBaselineToolSignature'), false);
|
|
1040
|
-
assert.equal(Object.hasOwn(session, 'contextPressureBaseline'), false);
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
test('provider tool snapshot applies valid toJSON once and rejects invalid toJSON results', () => {
|
|
1044
|
-
let topLevelCalls = 0;
|
|
1045
|
-
let nestedCalls = 0;
|
|
1046
|
-
let nestedDescription = 'nested snapshot';
|
|
1047
|
-
const nestedSchema = {
|
|
1048
|
-
toJSON() {
|
|
1049
|
-
nestedCalls += 1;
|
|
1050
|
-
return {
|
|
1051
|
-
type: 'object',
|
|
1052
|
-
properties: { value: { type: 'string', description: nestedDescription } },
|
|
1053
|
-
};
|
|
1054
|
-
},
|
|
1055
|
-
};
|
|
1056
|
-
const topLevelTool = {
|
|
1057
|
-
toJSON() {
|
|
1058
|
-
topLevelCalls += 1;
|
|
1059
|
-
return {
|
|
1060
|
-
name: 'read',
|
|
1061
|
-
description: 'top-level snapshot',
|
|
1062
|
-
inputSchema: nestedSchema,
|
|
1063
|
-
};
|
|
1064
|
-
},
|
|
1065
|
-
};
|
|
1066
|
-
const valid = snapshotProviderRequestTools({
|
|
1067
|
-
provider: 'anthropic-oauth',
|
|
1068
|
-
tools: [topLevelTool],
|
|
1069
|
-
messages: [],
|
|
1070
|
-
session: { provider: 'anthropic-oauth' },
|
|
1071
|
-
});
|
|
1072
|
-
nestedDescription = 'mutated after snapshot';
|
|
1073
|
-
assert.equal(topLevelCalls, 1);
|
|
1074
|
-
assert.equal(nestedCalls, 1);
|
|
1075
|
-
assert.equal(valid[0].inputSchema.properties.value.description, 'nested snapshot');
|
|
1076
|
-
assert.equal(Object.isFrozen(valid[0].inputSchema), true);
|
|
1077
|
-
const validSignature = toolSchemaSignature(valid);
|
|
1078
|
-
const validReserve = estimateRequestReserveTokens(valid);
|
|
1079
|
-
const opts = { providerToolSnapshotAuthoritative: true, session: { provider: 'anthropic-oauth' } };
|
|
1080
|
-
const messages = [{ role: 'user', content: 'hello' }];
|
|
1081
|
-
const oauthBody = buildAnthropicRequestBody(messages, 'claude-sonnet-4-6', valid, opts);
|
|
1082
|
-
const apiKeyTools = anthropicApiKeyTest.requestAnthropicTools(valid, messages, opts);
|
|
1083
|
-
assert.equal(oauthBody.tools[0].input_schema.properties.value.description, 'nested snapshot');
|
|
1084
|
-
assert.equal(apiKeyTools[0].input_schema.properties.value.description, 'nested snapshot');
|
|
1085
|
-
assert.equal(toolSchemaSignature(valid), validSignature);
|
|
1086
|
-
assert.equal(estimateRequestReserveTokens(valid), validReserve);
|
|
1087
|
-
|
|
1088
|
-
const snapshotTool = (tool) => snapshotProviderRequestTools({
|
|
1089
|
-
provider: 'anthropic-oauth',
|
|
1090
|
-
tools: [tool],
|
|
1091
|
-
messages: [],
|
|
1092
|
-
session: { provider: 'anthropic-oauth' },
|
|
1093
|
-
});
|
|
1094
|
-
let ownNonFunctionReads = 0;
|
|
1095
|
-
const ownNonFunction = { name: 'own_non_function', inputSchema: schema('own') };
|
|
1096
|
-
Object.defineProperty(ownNonFunction, 'toJSON', {
|
|
1097
|
-
enumerable: true,
|
|
1098
|
-
get() {
|
|
1099
|
-
ownNonFunctionReads += 1;
|
|
1100
|
-
if (ownNonFunctionReads > 1) throw new Error('own non-function toJSON read twice');
|
|
1101
|
-
return 'metadata';
|
|
1102
|
-
},
|
|
1103
|
-
});
|
|
1104
|
-
const ownNonFunctionSnapshot = snapshotTool(ownNonFunction);
|
|
1105
|
-
assert.equal(ownNonFunctionReads, 1);
|
|
1106
|
-
assert.equal(ownNonFunctionSnapshot[0].toJSON, 'metadata');
|
|
1107
|
-
assert.deepEqual(Object.keys(ownNonFunctionSnapshot[0]), ['name', 'inputSchema', 'toJSON']);
|
|
1108
|
-
|
|
1109
|
-
let inheritedNonFunctionReads = 0;
|
|
1110
|
-
const inheritedPrototype = {};
|
|
1111
|
-
Object.defineProperty(inheritedPrototype, 'toJSON', {
|
|
1112
|
-
get() {
|
|
1113
|
-
inheritedNonFunctionReads += 1;
|
|
1114
|
-
if (inheritedNonFunctionReads > 1) throw new Error('inherited non-function toJSON read twice');
|
|
1115
|
-
return 'inherited metadata';
|
|
1116
|
-
},
|
|
1117
|
-
});
|
|
1118
|
-
const inheritedNonFunction = Object.assign(Object.create(inheritedPrototype), {
|
|
1119
|
-
name: 'inherited_non_function',
|
|
1120
|
-
inputSchema: schema('inherited'),
|
|
1121
|
-
});
|
|
1122
|
-
const inheritedNonFunctionSnapshot = snapshotTool(inheritedNonFunction);
|
|
1123
|
-
assert.equal(inheritedNonFunctionReads, 1);
|
|
1124
|
-
assert.equal(Object.hasOwn(inheritedNonFunctionSnapshot[0], 'toJSON'), false);
|
|
1125
|
-
|
|
1126
|
-
let callableReads = 0;
|
|
1127
|
-
let callableCalls = 0;
|
|
1128
|
-
const callableAccessor = {};
|
|
1129
|
-
Object.defineProperty(callableAccessor, 'toJSON', {
|
|
1130
|
-
enumerable: true,
|
|
1131
|
-
get() {
|
|
1132
|
-
callableReads += 1;
|
|
1133
|
-
if (callableReads > 1) throw new Error('callable toJSON read twice');
|
|
1134
|
-
return function toJSON() {
|
|
1135
|
-
callableCalls += 1;
|
|
1136
|
-
return { name: 'callable_accessor', inputSchema: schema('callable') };
|
|
1137
|
-
};
|
|
1138
|
-
},
|
|
1139
|
-
});
|
|
1140
|
-
const callableSnapshot = snapshotTool(callableAccessor);
|
|
1141
|
-
assert.equal(callableReads, 1);
|
|
1142
|
-
assert.equal(callableCalls, 1);
|
|
1143
|
-
assert.equal(callableSnapshot[0].name, 'callable_accessor');
|
|
1144
|
-
|
|
1145
|
-
for (const tool of [
|
|
1146
|
-
{ toJSON: () => 1n },
|
|
1147
|
-
{ toJSON: () => ({ name: 'read', inputSchema: { toJSON: () => ({ invalid: 1n }) } }) },
|
|
1148
|
-
]) {
|
|
1149
|
-
assert.throws(
|
|
1150
|
-
() => snapshotTool(tool),
|
|
1151
|
-
(error) => error instanceof TypeError
|
|
1152
|
-
&& error.message === 'provider tool snapshot: BigInt is not JSON-serializable',
|
|
1153
|
-
);
|
|
1154
|
-
}
|
|
1155
|
-
const cyclicOutput = { name: 'read' };
|
|
1156
|
-
cyclicOutput.self = cyclicOutput;
|
|
1157
|
-
assert.throws(
|
|
1158
|
-
() => snapshotTool({ toJSON: () => cyclicOutput }),
|
|
1159
|
-
(error) => error instanceof TypeError
|
|
1160
|
-
&& error.message === 'provider tool snapshot: cyclic value is not JSON-serializable',
|
|
1161
|
-
);
|
|
1162
|
-
});
|
|
1163
|
-
|
|
1164
|
-
test('toJSON source-reference cycles fail once before pressure, baseline, or provider effects', async () => {
|
|
1165
|
-
const cycleError = (error) => error instanceof TypeError
|
|
1166
|
-
&& error.message === 'provider tool snapshot: cyclic value is not JSON-serializable';
|
|
1167
|
-
const sourceCycle = (replacementFor) => {
|
|
1168
|
-
let getterReads = 0;
|
|
1169
|
-
let calls = 0;
|
|
1170
|
-
const source = {};
|
|
1171
|
-
Object.defineProperty(source, 'toJSON', {
|
|
1172
|
-
enumerable: true,
|
|
1173
|
-
get() {
|
|
1174
|
-
getterReads += 1;
|
|
1175
|
-
if (getterReads > 1) throw new Error('cyclic toJSON getter read twice');
|
|
1176
|
-
return function toJSON() {
|
|
1177
|
-
calls += 1;
|
|
1178
|
-
if (calls > 1) throw new Error('cyclic toJSON called twice');
|
|
1179
|
-
return replacementFor(source);
|
|
1180
|
-
};
|
|
1181
|
-
},
|
|
1182
|
-
});
|
|
1183
|
-
return {
|
|
1184
|
-
source,
|
|
1185
|
-
counts: () => ({ getterReads, calls }),
|
|
1186
|
-
};
|
|
1187
|
-
};
|
|
1188
|
-
const snapshot = (tool) => snapshotProviderRequestTools({
|
|
1189
|
-
provider: 'anthropic-oauth',
|
|
1190
|
-
tools: [tool],
|
|
1191
|
-
messages: [],
|
|
1192
|
-
session: { provider: 'anthropic-oauth' },
|
|
1193
|
-
});
|
|
1194
|
-
|
|
1195
|
-
for (const replacementFor of [
|
|
1196
|
-
(source) => source,
|
|
1197
|
-
(source) => ({ name: 'read', source }),
|
|
1198
|
-
(source) => ({ name: 'read', inputSchema: { nested: { source } } }),
|
|
1199
|
-
(source) => ({ name: 'read', inputSchema: { values: [source] } }),
|
|
1200
|
-
]) {
|
|
1201
|
-
const cyclic = sourceCycle(replacementFor);
|
|
1202
|
-
assert.throws(() => snapshot(cyclic.source), cycleError);
|
|
1203
|
-
assert.deepEqual(cyclic.counts(), { getterReads: 1, calls: 1 });
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
let deep = { type: 'string', description: 'finite leaf' };
|
|
1207
|
-
for (let index = 0; index < 64; index += 1) deep = { nested: deep };
|
|
1208
|
-
const valid = sourceCycle(() => ({
|
|
1209
|
-
name: 'deep_valid',
|
|
1210
|
-
inputSchema: { type: 'object', properties: { value: deep } },
|
|
1211
|
-
}));
|
|
1212
|
-
const validSnapshot = snapshot(valid.source);
|
|
1213
|
-
assert.deepEqual(valid.counts(), { getterReads: 1, calls: 1 });
|
|
1214
|
-
let leaf = validSnapshot[0].inputSchema.properties.value;
|
|
1215
|
-
for (let index = 0; index < 64; index += 1) leaf = leaf.nested;
|
|
1216
|
-
assert.equal(leaf.description, 'finite leaf');
|
|
1217
|
-
const signature = toolSchemaSignature(validSnapshot);
|
|
1218
|
-
const reserve = estimateRequestReserveTokens(validSnapshot);
|
|
1219
|
-
const messages = [{ role: 'user', content: 'hello' }];
|
|
1220
|
-
const opts = {
|
|
1221
|
-
providerToolSnapshotAuthoritative: true,
|
|
1222
|
-
session: { provider: 'anthropic-oauth' },
|
|
1223
|
-
};
|
|
1224
|
-
const oauthBody = buildAnthropicRequestBody(messages, 'claude-sonnet-4-6', validSnapshot, opts);
|
|
1225
|
-
const apiKeyTools = anthropicApiKeyTest.requestAnthropicTools(validSnapshot, messages, opts);
|
|
1226
|
-
assert.equal(oauthBody.tools[0].name, 'deep_valid');
|
|
1227
|
-
assert.equal(apiKeyTools[0].name, 'deep_valid');
|
|
1228
|
-
assert.equal(toolSchemaSignature(validSnapshot), signature);
|
|
1229
|
-
assert.equal(estimateRequestReserveTokens(validSnapshot), reserve);
|
|
1230
|
-
|
|
1231
|
-
const cyclic = sourceCycle((source) => ({
|
|
1232
|
-
name: 'read',
|
|
1233
|
-
inputSchema: { nested: { source } },
|
|
1234
|
-
}));
|
|
1235
|
-
let providerCalls = 0;
|
|
1236
|
-
const session = {
|
|
1237
|
-
id: 'to-json-source-cycle',
|
|
1238
|
-
owner: 'agent',
|
|
1239
|
-
provider: 'anthropic-oauth',
|
|
1240
|
-
model: 'claude-sonnet-4-6',
|
|
1241
|
-
contextWindow: 100_000,
|
|
1242
|
-
messages: [{ role: 'user', content: 'hello' }],
|
|
1243
|
-
tools: [cyclic.source],
|
|
1244
|
-
compaction: {},
|
|
1245
|
-
};
|
|
1246
|
-
const provider = {
|
|
1247
|
-
name: session.provider,
|
|
1248
|
-
async send() {
|
|
1249
|
-
providerCalls += 1;
|
|
1250
|
-
return { content: 'must not send', usage: { inputTokens: 1, outputTokens: 1 } };
|
|
1251
|
-
},
|
|
1252
|
-
};
|
|
1253
|
-
await assert.rejects(
|
|
1254
|
-
agentLoop(provider, session.messages, session.model, session.tools, null, '', {
|
|
1255
|
-
session,
|
|
1256
|
-
sessionId: session.id,
|
|
1257
|
-
}),
|
|
1258
|
-
cycleError,
|
|
1259
|
-
);
|
|
1260
|
-
assert.deepEqual(cyclic.counts(), { getterReads: 1, calls: 1 });
|
|
1261
|
-
assert.equal(providerCalls, 0);
|
|
1262
|
-
assert.deepEqual(session.compaction, {});
|
|
1263
|
-
assert.equal(Object.hasOwn(session, 'contextPressureBaselineToolSignature'), false);
|
|
1264
|
-
assert.equal(Object.hasOwn(session, 'contextPressureBaseline'), false);
|
|
1265
|
-
});
|
|
1266
|
-
|
|
1267
|
-
test('agent-loop re-snapshots after reactive compaction without pressure/send signature drift', async () => {
|
|
1268
|
-
const read = { name: 'read', description: 'Read', inputSchema: schema('path') };
|
|
1269
|
-
const mcp = { name: 'mcp__demo__lookup', description: 'Lookup', inputSchema: schema('before snapshot') };
|
|
1270
|
-
const messages = [
|
|
1271
|
-
{ role: 'user', content: `old request ${'x'.repeat(30_000)}` },
|
|
1272
|
-
{ role: 'assistant', content: `old answer ${'y'.repeat(10_000)}` },
|
|
1273
|
-
{ role: 'user', content: 'current request' },
|
|
1274
|
-
];
|
|
1275
|
-
const session = {
|
|
1276
|
-
id: 'snapshot-reactive-compact',
|
|
1277
|
-
owner: 'agent',
|
|
1278
|
-
provider: 'anthropic-oauth',
|
|
1279
|
-
model: 'claude-sonnet-4-6',
|
|
1280
|
-
contextWindow: 100_000,
|
|
1281
|
-
messages,
|
|
1282
|
-
tools: [read],
|
|
1283
|
-
deferredNativeTools: true,
|
|
1284
|
-
deferredDiscoveredTools: [mcp.name],
|
|
1285
|
-
deferredToolCatalog: [read, mcp],
|
|
1286
|
-
compaction: {
|
|
1287
|
-
type: 'semantic',
|
|
1288
|
-
compactType: 'semantic',
|
|
1289
|
-
tailTurns: 1,
|
|
1290
|
-
keepTokens: 100,
|
|
1291
|
-
preserveRecentTokens: 100,
|
|
1292
|
-
},
|
|
1293
|
-
};
|
|
1294
|
-
const requestBodies = [];
|
|
1295
|
-
let actualAttempts = 0;
|
|
1296
|
-
let compactCalls = 0;
|
|
1297
|
-
let compactMutationDone = false;
|
|
1298
|
-
let preCompactGauge;
|
|
1299
|
-
let postCompactTools;
|
|
1300
|
-
let postCompactScoped;
|
|
1301
|
-
let delayedPreCompactLookup;
|
|
1302
|
-
let resolveDelayedPreCompact;
|
|
1303
|
-
const delayedPreCompact = new Promise((resolve) => {
|
|
1304
|
-
resolveDelayedPreCompact = resolve;
|
|
1305
|
-
});
|
|
1306
|
-
const { contextStatus } = createContextStatus({
|
|
1307
|
-
getSession: () => session,
|
|
1308
|
-
getRoute: () => ({ provider: session.provider, model: session.model }),
|
|
1309
|
-
getCurrentCwd: () => '',
|
|
1310
|
-
getMode: () => 'default',
|
|
1311
|
-
getNativeTools: () => [],
|
|
1312
|
-
});
|
|
1313
|
-
const provider = {
|
|
1314
|
-
name: session.provider,
|
|
1315
|
-
async send(outgoing, model, tools, opts) {
|
|
1316
|
-
if (String(opts?.sessionId || '').endsWith(':compact')) {
|
|
1317
|
-
compactCalls += 1;
|
|
1318
|
-
return { content: 'Compacted prior context.', usage: { inputTokens: 10, outputTokens: 2 } };
|
|
1319
|
-
}
|
|
1320
|
-
assert.equal(opts.providerToolSnapshotAuthoritative, true);
|
|
1321
|
-
actualAttempts += 1;
|
|
1322
|
-
if (actualAttempts === 1) {
|
|
1323
|
-
const snapshottedDescription = tools.find((tool) => tool.name === mcp.name)
|
|
1324
|
-
.inputSchema.properties.value.description;
|
|
1325
|
-
mcp.inputSchema.properties.value.description = 'mutated after first snapshot';
|
|
1326
|
-
await Promise.resolve();
|
|
1327
|
-
const body = buildAnthropicRequestBody(outgoing, model, tools, opts);
|
|
1328
|
-
requestBodies.push(body);
|
|
1329
|
-
assert.equal(
|
|
1330
|
-
body.tools.find((tool) => tool.name === mcp.name).input_schema.properties.value.description,
|
|
1331
|
-
snapshottedDescription,
|
|
1332
|
-
'post-snapshot catalog mutation must not alter provider bytes',
|
|
1333
|
-
);
|
|
1334
|
-
throw new Error('context length exceeded');
|
|
1335
|
-
}
|
|
1336
|
-
const body = buildAnthropicRequestBody(outgoing, model, tools, opts);
|
|
1337
|
-
requestBodies.push(body);
|
|
1338
|
-
return { content: 'done', usage: { inputTokens: 200, outputTokens: 2 } };
|
|
1339
|
-
},
|
|
1340
|
-
};
|
|
1341
|
-
const usageSnapshots = [];
|
|
1342
|
-
const result = await agentLoop(provider, messages, session.model, session.tools, null, '', {
|
|
1343
|
-
session,
|
|
1344
|
-
sessionId: session.id,
|
|
1345
|
-
async preCompactHook() {
|
|
1346
|
-
preCompactGauge = contextStatus();
|
|
1347
|
-
setTimeout(() => {
|
|
1348
|
-
delayedPreCompactLookup = scopedProviderRequestTools(session, session.provider, messages);
|
|
1349
|
-
resolveDelayedPreCompact();
|
|
1350
|
-
}, 30);
|
|
1351
|
-
await Promise.resolve();
|
|
1352
|
-
mcp.inputSchema.properties.value.description = 'mutated during compaction';
|
|
1353
|
-
compactMutationDone = true;
|
|
1354
|
-
},
|
|
1355
|
-
async postCompactHook() {
|
|
1356
|
-
if (!compactMutationDone || postCompactTools) return;
|
|
1357
|
-
postCompactScoped = scopedProviderRequestTools(session, session.provider, messages);
|
|
1358
|
-
postCompactTools = requestSerializedToolsForContext(session, session.provider, messages);
|
|
1359
|
-
contextStatus();
|
|
1360
|
-
await Promise.resolve();
|
|
1361
|
-
},
|
|
1362
|
-
onUsageDelta(delta) {
|
|
1363
|
-
if (delta.source !== 'provider_send') return;
|
|
1364
|
-
usageSnapshots.push(delta.sendTools);
|
|
1365
|
-
recordProviderContextBaseline(session, messages, {
|
|
1366
|
-
inputTokens: delta.deltaInput,
|
|
1367
|
-
outputTokens: delta.deltaOutput,
|
|
1368
|
-
}, { boundary: 'request', sendTools: delta.sendTools });
|
|
1369
|
-
},
|
|
1370
|
-
});
|
|
1371
|
-
|
|
1372
|
-
assert.equal(compactCalls, 1);
|
|
1373
|
-
assert.equal(preCompactGauge.request.toolSchemaBreakdown.mcp.count, 1);
|
|
1374
|
-
assert.equal(postCompactScoped, null);
|
|
1375
|
-
assert.equal(
|
|
1376
|
-
postCompactTools.find((tool) => tool.name === mcp.name).inputSchema.properties.value.description,
|
|
1377
|
-
'mutated during compaction',
|
|
1378
|
-
);
|
|
1379
|
-
await delayedPreCompact;
|
|
1380
|
-
assert.equal(delayedPreCompactLookup, null);
|
|
1381
|
-
assert.equal(compactMutationDone, true);
|
|
1382
|
-
assert.equal(actualAttempts, 2);
|
|
1383
|
-
const finalBodyTool = requestBodies.at(-1).tools.find((tool) => tool.name === mcp.name);
|
|
1384
|
-
assert.equal(finalBodyTool.input_schema.properties.value.description, 'mutated during compaction');
|
|
1385
|
-
assert.equal(
|
|
1386
|
-
session.compaction.requestReserveTokens,
|
|
1387
|
-
estimateRequestReserveTokens(result.lastSendTools),
|
|
1388
|
-
'post-compaction pressure must use the exact final send snapshot',
|
|
1389
|
-
);
|
|
1390
|
-
assert.equal(toolSchemaSignature(usageSnapshots.at(-1)), toolSchemaSignature(result.lastSendTools));
|
|
1391
|
-
assert.equal(session.contextPressureBaselineToolSignature, toolSchemaSignature(result.lastSendTools));
|
|
1392
|
-
});
|
|
1393
|
-
|
|
1394
|
-
test('tool status counts exclude MCP and skills while exposing MCP tool count', () => {
|
|
1395
|
-
const rows = [
|
|
1396
|
-
{ name: 'read', kind: toolKind({ name: 'read' }), active: true },
|
|
1397
|
-
{ name: 'mcp__demo__lookup', kind: toolKind({ name: 'mcp__demo__lookup' }), active: false },
|
|
1398
|
-
{ name: 'Skill', kind: toolKind({ name: 'Skill' }), active: true },
|
|
1399
|
-
];
|
|
1400
|
-
assert.deepEqual(splitToolStatusCounts(rows), {
|
|
1401
|
-
count: 1,
|
|
1402
|
-
activeCount: 1,
|
|
1403
|
-
mcpToolCount: 1,
|
|
1404
|
-
activeMcpToolCount: 0,
|
|
1405
|
-
});
|
|
1406
|
-
});
|