@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
|
@@ -4,8 +4,8 @@ import { mkdtempSync, rmSync } from 'node:fs';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { JsStore } from '@animalabs/chronicle';
|
|
7
|
-
import { AutobiographicalStrategy } from '@animalabs/context-manager';
|
|
8
|
-
import { AgentFramework, BudgetPreflightError } from '../src/index.js';
|
|
7
|
+
import { AutobiographicalStrategy, ContextManager } from '@animalabs/context-manager';
|
|
8
|
+
import { Agent, AgentFramework, BudgetPreflightError } from '../src/index.js';
|
|
9
9
|
const membrane = {};
|
|
10
10
|
function strategy() {
|
|
11
11
|
return new AutobiographicalStrategy({
|
|
@@ -42,6 +42,7 @@ describe('agent runtime settings', () => {
|
|
|
42
42
|
});
|
|
43
43
|
assert.deepEqual(framework.getAgentRuntimeSettings('agent'), {
|
|
44
44
|
contextBudgetTokens: 100_000,
|
|
45
|
+
effectiveContextBudgetTokens: 100_000,
|
|
45
46
|
tailTokens: 30_000,
|
|
46
47
|
transitionPaceTokens: 8_000,
|
|
47
48
|
sameRoundThinkTextPolicy: 'public',
|
|
@@ -55,6 +56,7 @@ describe('agent runtime settings', () => {
|
|
|
55
56
|
sameRoundThinkTextPolicy: 'private',
|
|
56
57
|
}), {
|
|
57
58
|
contextBudgetTokens: 60_000,
|
|
59
|
+
effectiveContextBudgetTokens: 100_000,
|
|
58
60
|
tailTokens: 20_000,
|
|
59
61
|
transitionPaceTokens: 4_000,
|
|
60
62
|
sameRoundThinkTextPolicy: 'private',
|
|
@@ -64,6 +66,7 @@ describe('agent runtime settings', () => {
|
|
|
64
66
|
assert.equal(framework.cancelAgentRuntimeSettingsTransition('agent').transition, 'stable');
|
|
65
67
|
assert.deepEqual(framework.resetAgentRuntimeSettings('agent'), {
|
|
66
68
|
contextBudgetTokens: 100_000,
|
|
69
|
+
effectiveContextBudgetTokens: 100_000,
|
|
67
70
|
tailTokens: 30_000,
|
|
68
71
|
transitionPaceTokens: 8_000,
|
|
69
72
|
sameRoundThinkTextPolicy: 'public',
|
|
@@ -103,8 +106,11 @@ describe('agent runtime settings', () => {
|
|
|
103
106
|
await framework.stop();
|
|
104
107
|
try {
|
|
105
108
|
framework = await AgentFramework.create(config());
|
|
109
|
+
// The descent was still running at the stop, so it resumes paced from the
|
|
110
|
+
// recipe's budget.
|
|
106
111
|
assert.deepEqual(framework.getAgentRuntimeSettings('agent'), {
|
|
107
112
|
contextBudgetTokens: 70_000,
|
|
113
|
+
effectiveContextBudgetTokens: 100_000,
|
|
108
114
|
tailTokens: 18_000,
|
|
109
115
|
transitionPaceTokens: 5_000,
|
|
110
116
|
sameRoundThinkTextPolicy: 'private',
|
|
@@ -117,6 +123,1132 @@ describe('agent runtime settings', () => {
|
|
|
117
123
|
rmSync(dir, { recursive: true, force: true });
|
|
118
124
|
}
|
|
119
125
|
});
|
|
126
|
+
// context-manager #162: a lower budget the agent had reached comes back reached.
|
|
127
|
+
describe('a reached budget across a restart', () => {
|
|
128
|
+
const persisted = (framework) => framework.getStore().getStateJson('framework/state')
|
|
129
|
+
?.agentRuntimeSettings?.agent;
|
|
130
|
+
const writeRecord = (framework, record) => {
|
|
131
|
+
const store = framework.getStore();
|
|
132
|
+
const state = (store.getStateJson('framework/state') ?? {});
|
|
133
|
+
store.setStateJson('framework/state', { ...state, agentRuntimeSettings: { ...state.agentRuntimeSettings, agent: record } });
|
|
134
|
+
};
|
|
135
|
+
const setup = () => {
|
|
136
|
+
const dir = mkdtempSync(join(tmpdir(), 'agent-settings-reached-'));
|
|
137
|
+
const config = () => ({
|
|
138
|
+
storePath: join(dir, 'store'),
|
|
139
|
+
membrane,
|
|
140
|
+
agents: [{
|
|
141
|
+
name: 'agent', model: 'test-model', systemPrompt: 'test',
|
|
142
|
+
strategy: strategy(), contextBudgetTokens: 100_000, maxTokens: 10_000,
|
|
143
|
+
}],
|
|
144
|
+
modules: [],
|
|
145
|
+
});
|
|
146
|
+
return { dir, config };
|
|
147
|
+
};
|
|
148
|
+
const budgets = (framework) => {
|
|
149
|
+
const s = framework.getAgentRuntimeSettings('agent');
|
|
150
|
+
return { target: s.contextBudgetTokens, effective: s.effectiveContextBudgetTokens, transition: s.transition };
|
|
151
|
+
};
|
|
152
|
+
const compile = (framework) => framework.getAgent('agent').compileContext();
|
|
153
|
+
/** Run `fn` with ContextManager.prototype[method] replaced. */
|
|
154
|
+
async function withPatched(method, impl, fn) {
|
|
155
|
+
const proto = ContextManager.prototype;
|
|
156
|
+
const original = proto[method];
|
|
157
|
+
proto[method] = impl;
|
|
158
|
+
try {
|
|
159
|
+
return await fn();
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
proto[method] = original;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const quietErrors = () => {
|
|
166
|
+
const lines = [];
|
|
167
|
+
const original = console.error;
|
|
168
|
+
console.error = (...args) => { lines.push(args.map(String).join(' ')); };
|
|
169
|
+
return { lines, restore: () => { console.error = original; } };
|
|
170
|
+
};
|
|
171
|
+
/** Each test's framework, stopped and removed afterwards. */
|
|
172
|
+
async function run(fn) {
|
|
173
|
+
const { dir, config } = setup();
|
|
174
|
+
let framework = await AgentFramework.create(config());
|
|
175
|
+
try {
|
|
176
|
+
await fn({
|
|
177
|
+
config,
|
|
178
|
+
framework: () => framework,
|
|
179
|
+
restart: async () => { await framework.stop(); framework = await AgentFramework.create(config()); return framework; },
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
await framework.stop();
|
|
184
|
+
rmSync(dir, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
it('a descent that settled comes back settled, and the first compile plans at it', async () => {
|
|
188
|
+
await run(async ({ framework, restart }) => {
|
|
189
|
+
framework().updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 });
|
|
190
|
+
assert.deepEqual(budgets(framework()), { target: 60_000, effective: 100_000, transition: 'converging' });
|
|
191
|
+
assert.deepEqual(persisted(framework()), { contextBudgetTokens: 60_000 }, 'not reached yet');
|
|
192
|
+
// The store is empty, so the first compile's frontier fits and the descent settles.
|
|
193
|
+
await compile(framework());
|
|
194
|
+
assert.deepEqual(budgets(framework()), { target: 60_000, effective: 60_000, transition: 'stable' });
|
|
195
|
+
assert.deepEqual(persisted(framework()), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 }, 'the settle is recorded');
|
|
196
|
+
const restarted = await restart();
|
|
197
|
+
assert.deepEqual(budgets(restarted), { target: 60_000, effective: 60_000, transition: 'stable' });
|
|
198
|
+
// The record's proof stands until a compile here says otherwise, so a
|
|
199
|
+
// persisting call before any compile keeps it.
|
|
200
|
+
restarted.updateAgentRuntimeSettings('agent', { tailTokens: 20_000 });
|
|
201
|
+
assert.equal(persisted(restarted)?.contextBudgetReachedTokens, 60_000);
|
|
202
|
+
const planned = [];
|
|
203
|
+
const cm = restarted.getAgent('agent').getContextManager();
|
|
204
|
+
const original = cm.compile.bind(cm);
|
|
205
|
+
cm.compile = (budget, ...rest) => { planned.push(budget?.maxTokens ?? -1); return original(budget, ...rest); };
|
|
206
|
+
await compile(restarted);
|
|
207
|
+
assert.deepEqual(planned, [60_000], 'the first compile after the restart plans at the reached budget');
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
it('a restored descent that settles after the restart is recorded then', async () => {
|
|
211
|
+
await run(async ({ framework, restart }) => {
|
|
212
|
+
framework().updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 });
|
|
213
|
+
const restarted = await restart();
|
|
214
|
+
assert.deepEqual(budgets(restarted), { target: 60_000, effective: 100_000, transition: 'converging' });
|
|
215
|
+
await compile(restarted);
|
|
216
|
+
assert.deepEqual(persisted(restarted), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 });
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
it('an immediate decrease is recorded once a compile fits at it, not before', async () => {
|
|
220
|
+
await run(async ({ framework, restart }) => {
|
|
221
|
+
framework().updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
222
|
+
assert.deepEqual(persisted(framework()), { contextBudgetTokens: 60_000 }, 'nothing has compiled at it yet');
|
|
223
|
+
await compile(framework());
|
|
224
|
+
assert.deepEqual(persisted(framework()), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 });
|
|
225
|
+
assert.deepEqual(budgets(await restart()), { target: 60_000, effective: 60_000, transition: 'stable' });
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
it('an immediate decrease that never compiled comes back paced', async () => {
|
|
229
|
+
await run(async ({ framework, restart }) => {
|
|
230
|
+
framework().updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
231
|
+
assert.deepEqual(budgets(await restart()), { target: 60_000, effective: 100_000, transition: 'converging' });
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
it('a compile refused at the reached budget withdraws the record, and the restart paces', async () => {
|
|
235
|
+
await run(async ({ framework, restart }) => {
|
|
236
|
+
framework().updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
237
|
+
await compile(framework());
|
|
238
|
+
const refusal = Object.assign(new Error('over budget'), { name: 'OverBudgetError' });
|
|
239
|
+
await withPatched('compile', async () => { throw refusal; }, async () => {
|
|
240
|
+
await assert.rejects(() => compile(framework()), /over budget/);
|
|
241
|
+
});
|
|
242
|
+
assert.deepEqual(persisted(framework()), { contextBudgetTokens: 60_000 });
|
|
243
|
+
assert.deepEqual(budgets(await restart()), { target: 60_000, effective: 100_000, transition: 'converging' });
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
it('a reached budget the preflight says no longer fits restores paced, with a line saying why', async () => {
|
|
247
|
+
await run(async ({ framework, restart }) => {
|
|
248
|
+
framework().updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
249
|
+
await compile(framework());
|
|
250
|
+
const log = quietErrors();
|
|
251
|
+
try {
|
|
252
|
+
// A restart with a new maxTokens or strategy configuration raised the folded floor.
|
|
253
|
+
const restarted = await withPatched('previewContext', (budget) => ({
|
|
254
|
+
finalTokens: 70_000, budgetTokens: budget.maxTokens, fits: 70_000 <= budget.maxTokens,
|
|
255
|
+
}), restart);
|
|
256
|
+
assert.deepEqual(budgets(restarted), { target: 60_000, effective: 100_000, transition: 'converging' });
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
log.restore();
|
|
260
|
+
}
|
|
261
|
+
// One line, written once the restore is made, saying why and what it made.
|
|
262
|
+
assert.deepEqual(log.lines.filter((line) => line.includes('[agent-settings]')), [
|
|
263
|
+
'[agent-settings] agent: its record says a compile fit at contextBudgetTokens=60000, but the folded floor renders ' +
|
|
264
|
+
'70000 tokens against it now; restored it paced instead: contextBudgetTokens is 60000, and compiles plan at 100000 (converging)',
|
|
265
|
+
]);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
it('a reached budget the preflight says fits but that fails to apply at once restores paced, with a line naming that step', async () => {
|
|
269
|
+
await run(async ({ framework, restart }) => {
|
|
270
|
+
framework().updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
271
|
+
await compile(framework());
|
|
272
|
+
const proto = Agent.prototype;
|
|
273
|
+
const original = proto.restoreRuntimeSettings;
|
|
274
|
+
proto.restoreRuntimeSettings = function (settings, opts) {
|
|
275
|
+
if (opts?.provenBudget !== undefined)
|
|
276
|
+
throw new Error('hot settings refused');
|
|
277
|
+
return original.call(this, settings, opts);
|
|
278
|
+
};
|
|
279
|
+
const log = quietErrors();
|
|
280
|
+
let restarted;
|
|
281
|
+
try {
|
|
282
|
+
restarted = await restart();
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
proto.restoreRuntimeSettings = original;
|
|
286
|
+
log.restore();
|
|
287
|
+
}
|
|
288
|
+
assert.deepEqual(budgets(restarted), { target: 60_000, effective: 100_000, transition: 'converging' });
|
|
289
|
+
assert.ok(log.lines.some((line) => line.includes('applying it at once failed (hot settings refused), though the preflight says it fits now')), log.lines.join('\n'));
|
|
290
|
+
assert.ok(!log.lines.some((line) => line.includes("can't be checked")), log.lines.join('\n'));
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
it('a record without the field, or with one for another budget, restores paced', async () => {
|
|
294
|
+
await run(async ({ framework, restart }) => {
|
|
295
|
+
// An earlier release's record; then one an older release carried
|
|
296
|
+
// forward beside a budget it changed.
|
|
297
|
+
writeRecord(framework(), { contextBudgetTokens: 60_000 });
|
|
298
|
+
assert.equal(budgets(await restart()).transition, 'converging');
|
|
299
|
+
writeRecord(framework(), { contextBudgetTokens: 70_000, contextBudgetReachedTokens: 60_000 });
|
|
300
|
+
assert.deepEqual(budgets(await restart()), { target: 70_000, effective: 100_000, transition: 'converging' });
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
it('an invalid field is ignored with a bounded line, and a record holding only it still boots', async () => {
|
|
304
|
+
await run(async ({ framework, restart }) => {
|
|
305
|
+
const log = quietErrors();
|
|
306
|
+
try {
|
|
307
|
+
for (const invalid of ['yes'.repeat(100), [60_000], { budget: 60_000 }, null]) {
|
|
308
|
+
writeRecord(framework(), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: invalid });
|
|
309
|
+
assert.equal(budgets(await restart()).transition, 'converging', JSON.stringify(invalid));
|
|
310
|
+
}
|
|
311
|
+
writeRecord(framework(), { contextBudgetReachedTokens: -5 });
|
|
312
|
+
assert.deepEqual(budgets(await restart()), { target: 100_000, effective: 100_000, transition: 'stable' }, 'the recipe budget');
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
log.restore();
|
|
316
|
+
}
|
|
317
|
+
// The line names the kind of value, never its content.
|
|
318
|
+
const lines = log.lines.filter((l) => l.includes('ignoring an invalid persisted contextBudgetReachedTokens'));
|
|
319
|
+
const line = (kind) => `[agent-settings] ignoring an invalid persisted contextBudgetReachedTokens for agent "agent": ${kind}`;
|
|
320
|
+
assert.deepEqual(lines, [
|
|
321
|
+
line('a string'),
|
|
322
|
+
line('an array'),
|
|
323
|
+
line('an object'),
|
|
324
|
+
line('null'),
|
|
325
|
+
line('a number that isn\'t a positive safe integer'),
|
|
326
|
+
], log.lines.join('\n'));
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
it('a malformed persisted budget, tail or pace is ignored with a line naming its kind, and the rest restores', async () => {
|
|
330
|
+
await run(async ({ framework, restart }) => {
|
|
331
|
+
writeRecord(framework(), { contextBudgetTokens: 'x'.repeat(100), tailTokens: -1, transitionPaceTokens: 4_000 });
|
|
332
|
+
const log = quietErrors();
|
|
333
|
+
let restarted;
|
|
334
|
+
try {
|
|
335
|
+
restarted = await restart();
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
log.restore();
|
|
339
|
+
}
|
|
340
|
+
const { contextBudgetTokens, effectiveContextBudgetTokens, tailTokens, transitionPaceTokens } = restarted.getAgentRuntimeSettings('agent');
|
|
341
|
+
assert.deepEqual({ contextBudgetTokens, effectiveContextBudgetTokens, tailTokens, transitionPaceTokens }, { contextBudgetTokens: 100_000, effectiveContextBudgetTokens: 100_000, tailTokens: 30_000, transitionPaceTokens: 4_000 });
|
|
342
|
+
assert.deepEqual(log.lines.filter((l) => l.includes('[agent-settings]')), [
|
|
343
|
+
'[agent-settings] ignoring an invalid persisted contextBudgetTokens for agent "agent": a string',
|
|
344
|
+
'[agent-settings] ignoring an invalid persisted tailTokens for agent "agent": a number that isn\'t a non-negative safe integer',
|
|
345
|
+
]);
|
|
346
|
+
restarted.updateAgentRuntimeSettings('agent', { tailTokens: 20_000 });
|
|
347
|
+
assert.deepEqual(persisted(restarted), { transitionPaceTokens: 4_000, tailTokens: 20_000 }, 'a later write leaves them out');
|
|
348
|
+
});
|
|
349
|
+
await run(async ({ framework, restart }) => {
|
|
350
|
+
writeRecord(framework(), { transitionPaceTokens: 0, tailTokens: 20_000 });
|
|
351
|
+
const log = quietErrors();
|
|
352
|
+
let restarted;
|
|
353
|
+
try {
|
|
354
|
+
restarted = await restart();
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
log.restore();
|
|
358
|
+
}
|
|
359
|
+
assert.equal(restarted.getAgentRuntimeSettings('agent').tailTokens, 20_000);
|
|
360
|
+
assert.deepEqual(log.lines.filter((l) => l.includes('[agent-settings]')), [
|
|
361
|
+
'[agent-settings] ignoring an invalid persisted transitionPaceTokens for agent "agent": a number that isn\'t a positive safe integer',
|
|
362
|
+
]);
|
|
363
|
+
restarted.updateAgentRuntimeSettings('agent', { sameRoundThinkTextPolicy: 'private' });
|
|
364
|
+
assert.deepEqual(persisted(restarted), { tailTokens: 20_000, sameRoundThinkTextPolicy: 'private' }, 'a later write leaves the pace out');
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
it('a patch cannot set the field', async () => {
|
|
368
|
+
await run(async ({ framework }) => {
|
|
369
|
+
framework().updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 });
|
|
370
|
+
assert.deepEqual(framework().getAgent('agent').getRuntimeSettingsOverrides(), { contextBudgetTokens: 60_000 });
|
|
371
|
+
assert.deepEqual(persisted(framework()), { contextBudgetTokens: 60_000 });
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
it('every persisting call keeps the field true to the live state', async () => {
|
|
375
|
+
await run(async ({ framework }) => {
|
|
376
|
+
const f = framework();
|
|
377
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
378
|
+
await compile(f);
|
|
379
|
+
f.updateAgentRuntimeSettings('agent', { tailTokens: 20_000 });
|
|
380
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000, tailTokens: 20_000, contextBudgetReachedTokens: 60_000 }, 'another setting keeps it');
|
|
381
|
+
f.cancelAgentRuntimeSettingsTransition('agent');
|
|
382
|
+
assert.equal(persisted(f)?.contextBudgetReachedTokens, 60_000, 'a cancel with no descent keeps it');
|
|
383
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 50_000 });
|
|
384
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 50_000, tailTokens: 20_000 }, 'a new descent has reached nothing');
|
|
385
|
+
f.cancelAgentRuntimeSettingsTransition('agent');
|
|
386
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000, tailTokens: 20_000, contextBudgetReachedTokens: 60_000 }, 'cancelled back to the reached budget');
|
|
387
|
+
f.resetAgentRuntimeSettings('agent', ['contextBudgetTokens']);
|
|
388
|
+
assert.deepEqual(persisted(f), { tailTokens: 20_000 }, 'reset to the recipe');
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
it('a settle marks only the persisted budget it reached', async () => {
|
|
392
|
+
await run(async ({ framework }) => {
|
|
393
|
+
framework().updateAgentRuntimeSettings('agent', { contextBudgetTokens: 80_000 });
|
|
394
|
+
framework().updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 }, { persist: false });
|
|
395
|
+
await compile(framework());
|
|
396
|
+
assert.deepEqual(budgets(framework()), { target: 60_000, effective: 60_000, transition: 'stable' });
|
|
397
|
+
assert.deepEqual(persisted(framework()), { contextBudgetTokens: 80_000 }, "the record's 80k was never compiled at");
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
it('a descent applied without persisting settles without writing anything', async () => {
|
|
401
|
+
await run(async ({ framework, restart }) => {
|
|
402
|
+
framework().updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 }, { persist: false });
|
|
403
|
+
const log = quietErrors();
|
|
404
|
+
try {
|
|
405
|
+
await compile(framework());
|
|
406
|
+
}
|
|
407
|
+
finally {
|
|
408
|
+
log.restore();
|
|
409
|
+
}
|
|
410
|
+
assert.deepEqual(log.lines.filter((l) => l.includes('[agent-settings]')), [], 'no record is no error');
|
|
411
|
+
assert.deepEqual(budgets(framework()), { target: 60_000, effective: 60_000, transition: 'stable' });
|
|
412
|
+
assert.equal(persisted(framework()), undefined, 'nothing written for an ephemeral descent');
|
|
413
|
+
assert.deepEqual(budgets(await restart()), { target: 100_000, effective: 100_000, transition: 'stable' }, 'the recipe budget');
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
it('a failed write of the field is logged, and the compile stands', async () => {
|
|
417
|
+
await run(async ({ framework }) => {
|
|
418
|
+
framework().updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
419
|
+
const store = framework().getStore();
|
|
420
|
+
const original = store.setStateJson.bind(store);
|
|
421
|
+
store.setStateJson = () => { throw new Error('disk full'); };
|
|
422
|
+
const log = quietErrors();
|
|
423
|
+
try {
|
|
424
|
+
await compile(framework());
|
|
425
|
+
}
|
|
426
|
+
finally {
|
|
427
|
+
store.setStateJson = original;
|
|
428
|
+
log.restore();
|
|
429
|
+
}
|
|
430
|
+
assert.ok(log.lines.some((l) => l.includes("couldn't record agent's compile at budget 60000")), log.lines.join('\n'));
|
|
431
|
+
assert.deepEqual(persisted(framework()), { contextBudgetTokens: 60_000 });
|
|
432
|
+
// The next compile at that budget records it, once writes work again.
|
|
433
|
+
await compile(framework());
|
|
434
|
+
assert.deepEqual(persisted(framework()), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 });
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
it('a budget changed while a compile runs is not proven by that compile', async () => {
|
|
438
|
+
await run(async ({ framework }) => {
|
|
439
|
+
const f = framework();
|
|
440
|
+
const original = ContextManager.prototype.compile;
|
|
441
|
+
// The compile was asked for at the recipe's 100k; the budget drops to
|
|
442
|
+
// 60k at once while it runs.
|
|
443
|
+
await withPatched('compile', async function (...args) {
|
|
444
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
445
|
+
return original.apply(this, args);
|
|
446
|
+
}, () => compile(f));
|
|
447
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000 }, 'the 100k compile proves nothing about 60k');
|
|
448
|
+
assert.equal(f.getAgent('agent').getProvenContextBudgetTokens(), undefined, 'and no proof stands for the live budget: that compile planned at 100k');
|
|
449
|
+
await compile(f);
|
|
450
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 });
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
it('a compile refused after the budget changed under it withdraws its own budget\'s marker, and leaves the new budget\'s proof', async () => {
|
|
454
|
+
await run(async ({ framework }) => {
|
|
455
|
+
const f = framework();
|
|
456
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
457
|
+
await compile(f);
|
|
458
|
+
assert.equal(persisted(f)?.contextBudgetReachedTokens, 60_000);
|
|
459
|
+
const original = ContextManager.prototype.compile;
|
|
460
|
+
const refusal = Object.assign(new Error('over budget'), { name: 'OverBudgetError' });
|
|
461
|
+
let release;
|
|
462
|
+
const held = new Promise((resolve) => { release = resolve; });
|
|
463
|
+
let calls = 0;
|
|
464
|
+
// The first compile plans at 60k and is refused only after the budget
|
|
465
|
+
// went to 80k, unpersisted, and a second compile fit there.
|
|
466
|
+
await withPatched('compile', async function (...args) {
|
|
467
|
+
if (++calls === 1) {
|
|
468
|
+
await held;
|
|
469
|
+
throw refusal;
|
|
470
|
+
}
|
|
471
|
+
return original.apply(this, args);
|
|
472
|
+
}, async () => {
|
|
473
|
+
const first = compile(f);
|
|
474
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 80_000 }, { persist: false });
|
|
475
|
+
await compile(f);
|
|
476
|
+
release();
|
|
477
|
+
await assert.rejects(first, /over budget/);
|
|
478
|
+
});
|
|
479
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000 }, 'the record still holds 60k, now without its marker');
|
|
480
|
+
assert.equal(f.getAgent('agent').getProvenContextBudgetTokens(), 80_000, 'the refusal at 60k says nothing about 80k');
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
/**
|
|
484
|
+
* Two compiles at 60k, the first held until `between` has run: `firstFits`
|
|
485
|
+
* says how the first ends, and the second, started after it, ends the
|
|
486
|
+
* other way. Each selects at its start, as the context manager does (a fit
|
|
487
|
+
* calls the real compile then, a refusal is decided then); only the
|
|
488
|
+
* first's outcome is held.
|
|
489
|
+
*/
|
|
490
|
+
async function twoCompilesAt60k(f, firstFits, between) {
|
|
491
|
+
const original = ContextManager.prototype.compile;
|
|
492
|
+
const refusal = Object.assign(new Error('over budget'), { name: 'OverBudgetError' });
|
|
493
|
+
let release;
|
|
494
|
+
const held = new Promise((resolve) => { release = resolve; });
|
|
495
|
+
let calls = 0;
|
|
496
|
+
await withPatched('compile', async function (...args) {
|
|
497
|
+
const isFirst = ++calls === 1;
|
|
498
|
+
const outcome = isFirst !== firstFits ? Promise.reject(refusal) : original.apply(this, args);
|
|
499
|
+
outcome.catch(() => { }); // returned below
|
|
500
|
+
if (isFirst)
|
|
501
|
+
await held;
|
|
502
|
+
return outcome;
|
|
503
|
+
}, async () => {
|
|
504
|
+
const first = compile(f);
|
|
505
|
+
first.catch(() => { }); // asserted below; if between() fails, its refusal is not left unhandled
|
|
506
|
+
try {
|
|
507
|
+
await between();
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
release();
|
|
511
|
+
}
|
|
512
|
+
if (firstFits)
|
|
513
|
+
await first;
|
|
514
|
+
else
|
|
515
|
+
await assert.rejects(first, /over budget/);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
it('of two compiles at one budget, the one that started later decides, whichever finishes last', async () => {
|
|
519
|
+
// A refusal planned before a fit at the same budget, with nothing changed between them.
|
|
520
|
+
await run(async ({ framework }) => {
|
|
521
|
+
const f = framework();
|
|
522
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
523
|
+
await twoCompilesAt60k(f, false, async () => {
|
|
524
|
+
await compile(f);
|
|
525
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 }, 'the second compile fit at 60k');
|
|
526
|
+
});
|
|
527
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 }, 'the later-started fit stands');
|
|
528
|
+
assert.equal(f.getAgent('agent').getProvenContextBudgetTokens(), 60_000);
|
|
529
|
+
});
|
|
530
|
+
// A refusal planned before the budget went to 80k and came back to 60k,
|
|
531
|
+
// where a second compile fit; then, in a second run, the budget moves to
|
|
532
|
+
// 70k unpersisted before the refusal arrives.
|
|
533
|
+
for (const moveAway of [false, true]) {
|
|
534
|
+
await run(async ({ framework }) => {
|
|
535
|
+
const f = framework();
|
|
536
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
537
|
+
await twoCompilesAt60k(f, false, async () => {
|
|
538
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 80_000 });
|
|
539
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
540
|
+
await compile(f);
|
|
541
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 }, 'the second compile fit at 60k');
|
|
542
|
+
if (moveAway)
|
|
543
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 70_000 }, { persist: false });
|
|
544
|
+
});
|
|
545
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 }, 'the later-started fit stands');
|
|
546
|
+
assert.equal(f.getAgent('agent').getProvenContextBudgetTokens(), moveAway ? undefined : 60_000);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
// A fit planned before a refusal at the same budget.
|
|
550
|
+
await run(async ({ framework }) => {
|
|
551
|
+
const f = framework();
|
|
552
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
553
|
+
await compile(f);
|
|
554
|
+
await twoCompilesAt60k(f, true, async () => {
|
|
555
|
+
await assert.rejects(compile(f), /over budget/);
|
|
556
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000 }, 'the second compile\'s refusal withdrew the marker');
|
|
557
|
+
});
|
|
558
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000 }, 'the later-started refusal stands');
|
|
559
|
+
assert.equal(f.getAgent('agent').getProvenContextBudgetTokens(), undefined);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
it("a later-started compile whose fit isn't credited still outranks an earlier-started refusal at its budget", async () => {
|
|
563
|
+
await run(async ({ framework }) => {
|
|
564
|
+
const f = framework();
|
|
565
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
566
|
+
await compile(f);
|
|
567
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 });
|
|
568
|
+
const original = ContextManager.prototype.compile;
|
|
569
|
+
const refusal = Object.assign(new Error('over budget'), { name: 'OverBudgetError' });
|
|
570
|
+
let releaseFirst;
|
|
571
|
+
let releaseSecond;
|
|
572
|
+
const held = [
|
|
573
|
+
new Promise((resolve) => { releaseFirst = resolve; }),
|
|
574
|
+
new Promise((resolve) => { releaseSecond = resolve; }),
|
|
575
|
+
];
|
|
576
|
+
let calls = 0;
|
|
577
|
+
await withPatched('compile', async function (...args) {
|
|
578
|
+
const n = calls++;
|
|
579
|
+
// Each selects at its start; only its outcome is held.
|
|
580
|
+
const outcome = n === 0 ? Promise.reject(refusal) : original.apply(this, args);
|
|
581
|
+
outcome.catch(() => { }); // returned below
|
|
582
|
+
await held[n];
|
|
583
|
+
return outcome;
|
|
584
|
+
}, async () => {
|
|
585
|
+
// Both plan at 60k. The second fits while the budget moves to 70k
|
|
586
|
+
// unpersisted, so nothing credits it; the first is refused after it.
|
|
587
|
+
const first = compile(f);
|
|
588
|
+
first.catch(() => { }); // asserted below
|
|
589
|
+
try {
|
|
590
|
+
const second = compile(f);
|
|
591
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 70_000 }, { persist: false });
|
|
592
|
+
releaseSecond();
|
|
593
|
+
await second;
|
|
594
|
+
}
|
|
595
|
+
finally {
|
|
596
|
+
releaseFirst();
|
|
597
|
+
releaseSecond();
|
|
598
|
+
}
|
|
599
|
+
await assert.rejects(first, /over budget/);
|
|
600
|
+
});
|
|
601
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 }, 'the earlier-started refusal is ignored');
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
it("a settle's credit outranks an earlier-started refusal at its target", async () => {
|
|
605
|
+
await run(async ({ framework }) => {
|
|
606
|
+
const f = framework();
|
|
607
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
608
|
+
const original = ContextManager.prototype.compile;
|
|
609
|
+
const refusal = Object.assign(new Error('over budget'), { name: 'OverBudgetError' });
|
|
610
|
+
let release;
|
|
611
|
+
const held = new Promise((resolve) => { release = resolve; });
|
|
612
|
+
let calls = 0;
|
|
613
|
+
await withPatched('compile', async function (...args) {
|
|
614
|
+
if (++calls === 1) {
|
|
615
|
+
await held;
|
|
616
|
+
throw refusal;
|
|
617
|
+
}
|
|
618
|
+
return original.apply(this, args);
|
|
619
|
+
}, async () => {
|
|
620
|
+
// The first plans at 60k and is refused only after the budget went to
|
|
621
|
+
// 80k and paced back down, and a compile at 80k settled that descent.
|
|
622
|
+
const first = compile(f);
|
|
623
|
+
first.catch(() => { }); // asserted below
|
|
624
|
+
try {
|
|
625
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 80_000 });
|
|
626
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 });
|
|
627
|
+
assert.equal(budgets(f).transition, 'converging');
|
|
628
|
+
await compile(f);
|
|
629
|
+
assert.deepEqual(budgets(f), { target: 60_000, effective: 60_000, transition: 'stable' });
|
|
630
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 }, 'the settle credited 60k');
|
|
631
|
+
}
|
|
632
|
+
finally {
|
|
633
|
+
release();
|
|
634
|
+
}
|
|
635
|
+
await assert.rejects(first, /over budget/);
|
|
636
|
+
});
|
|
637
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 }, 'the settle outranks the earlier-started refusal');
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
it('a settle credits its target only from the latest compile to end at the budget it planned at', async () => {
|
|
641
|
+
await run(async ({ framework }) => {
|
|
642
|
+
const f = framework();
|
|
643
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 });
|
|
644
|
+
assert.equal(budgets(f).transition, 'converging');
|
|
645
|
+
const original = ContextManager.prototype.compile;
|
|
646
|
+
const refusal = Object.assign(new Error('over budget'), { name: 'OverBudgetError' });
|
|
647
|
+
let release;
|
|
648
|
+
const held = new Promise((resolve) => { release = resolve; });
|
|
649
|
+
let calls = 0;
|
|
650
|
+
await withPatched('compile', async function (...args) {
|
|
651
|
+
if (++calls > 1)
|
|
652
|
+
throw refusal;
|
|
653
|
+
// The first selects at its start; only its outcome is held.
|
|
654
|
+
const outcome = original.apply(this, args);
|
|
655
|
+
await held;
|
|
656
|
+
return outcome;
|
|
657
|
+
}, async () => {
|
|
658
|
+
// Both plan at 100k. The second is refused there first; the first
|
|
659
|
+
// then fits and settles the descent to 60k.
|
|
660
|
+
const first = compile(f);
|
|
661
|
+
try {
|
|
662
|
+
await assert.rejects(compile(f), /over budget/);
|
|
663
|
+
}
|
|
664
|
+
finally {
|
|
665
|
+
release();
|
|
666
|
+
}
|
|
667
|
+
await first;
|
|
668
|
+
});
|
|
669
|
+
assert.deepEqual(budgets(f), { target: 60_000, effective: 60_000, transition: 'stable' }, 'the first compile settled the descent');
|
|
670
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000 }, 'and credits nothing: a later-started compile was refused at 100k');
|
|
671
|
+
assert.equal(f.getAgent('agent').getProvenContextBudgetTokens(), undefined);
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
it('the start order keeps nothing once no compile runs, through descents and compiles at once', async () => {
|
|
675
|
+
await run(async ({ framework }) => {
|
|
676
|
+
const f = framework();
|
|
677
|
+
// What it keeps is visible only inside the agent.
|
|
678
|
+
const kept = () => [...f.getAgent('agent').latestEndedStart.keys()];
|
|
679
|
+
// Descents to three budgets, each settled by a compile.
|
|
680
|
+
for (const budget of [90_000, 75_000, 60_000]) {
|
|
681
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: budget });
|
|
682
|
+
await compile(f);
|
|
683
|
+
assert.deepEqual(budgets(f), { target: budget, effective: budget, transition: 'stable' });
|
|
684
|
+
assert.deepEqual(kept(), [], `after the descent to ${budget}`);
|
|
685
|
+
}
|
|
686
|
+
// Two at once: the later-started one's place at 60k is kept while the earlier one runs.
|
|
687
|
+
await twoCompilesAt60k(f, false, async () => {
|
|
688
|
+
await compile(f);
|
|
689
|
+
assert.deepEqual(kept(), [60_000], 'kept while an earlier-started compile runs');
|
|
690
|
+
});
|
|
691
|
+
assert.deepEqual(kept(), [], 'and dropped once it ends');
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
it('of two compiles in flight across a settle, the one that finishes later proves nothing it planned before it', async () => {
|
|
695
|
+
await run(async ({ framework }) => {
|
|
696
|
+
const f = framework();
|
|
697
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 });
|
|
698
|
+
const original = ContextManager.prototype.compile;
|
|
699
|
+
let release;
|
|
700
|
+
const held = new Promise((resolve) => { release = resolve; });
|
|
701
|
+
let calls = 0;
|
|
702
|
+
// Both plan at the live 100k; the first is held until the second has
|
|
703
|
+
// finished and settled the descent.
|
|
704
|
+
await withPatched('compile', async function (...args) {
|
|
705
|
+
const n = ++calls;
|
|
706
|
+
const result = await original.apply(this, args);
|
|
707
|
+
if (n === 1)
|
|
708
|
+
await held;
|
|
709
|
+
return result;
|
|
710
|
+
}, async () => {
|
|
711
|
+
const first = compile(f);
|
|
712
|
+
await compile(f);
|
|
713
|
+
assert.deepEqual(budgets(f), { target: 60_000, effective: 60_000, transition: 'stable' });
|
|
714
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 }, 'the second settled it');
|
|
715
|
+
release();
|
|
716
|
+
await first;
|
|
717
|
+
});
|
|
718
|
+
assert.equal(f.getAgent('agent').getProvenContextBudgetTokens(), 60_000, "the first's 100k plan doesn't replace the proof");
|
|
719
|
+
f.updateAgentRuntimeSettings('agent', { tailTokens: 20_000 });
|
|
720
|
+
assert.equal(persisted(f)?.contextBudgetReachedTokens, 60_000, 'so a persisting call keeps the marker');
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
it('a proof stands only until the live budget changes: back at a proven budget, nothing is marked before a compile fits', async () => {
|
|
724
|
+
await run(async ({ framework }) => {
|
|
725
|
+
const f = framework();
|
|
726
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
727
|
+
await compile(f);
|
|
728
|
+
assert.equal(persisted(f)?.contextBudgetReachedTokens, 60_000);
|
|
729
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 80_000 });
|
|
730
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
731
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000 }, 'back at once: no compile has fit at it since');
|
|
732
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 80_000 });
|
|
733
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 });
|
|
734
|
+
assert.equal(budgets(f).transition, 'converging');
|
|
735
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000 }, 'back paced: the descent has reached nothing');
|
|
736
|
+
await compile(f);
|
|
737
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 }, 'its settle marks it');
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
it('a descent re-armed to the same target while a compile runs is not credited to that compile', async () => {
|
|
741
|
+
await run(async ({ framework }) => {
|
|
742
|
+
const f = framework();
|
|
743
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 });
|
|
744
|
+
const original = ContextManager.prototype.compile;
|
|
745
|
+
// The compile plans at the live 100k; meanwhile the budget is raised
|
|
746
|
+
// at once and lowered back, paced, to the same 60k.
|
|
747
|
+
await withPatched('compile', async function (...args) {
|
|
748
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 120_000 });
|
|
749
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 });
|
|
750
|
+
return original.apply(this, args);
|
|
751
|
+
}, () => compile(f));
|
|
752
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000 }, 'the compile planned at 100k under another descent');
|
|
753
|
+
});
|
|
754
|
+
// The same with the live budget left alone: the descent is cancelled and
|
|
755
|
+
// armed again, to the same 60k, while the compile runs.
|
|
756
|
+
await run(async ({ framework }) => {
|
|
757
|
+
const f = framework();
|
|
758
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 });
|
|
759
|
+
const original = ContextManager.prototype.compile;
|
|
760
|
+
await withPatched('compile', async function (...args) {
|
|
761
|
+
f.cancelAgentRuntimeSettingsTransition('agent');
|
|
762
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 });
|
|
763
|
+
return original.apply(this, args);
|
|
764
|
+
}, () => compile(f));
|
|
765
|
+
assert.equal(persisted(f)?.contextBudgetReachedTokens, undefined, 'a descent armed again is another descent');
|
|
766
|
+
});
|
|
767
|
+
// And armed again directly, to the same 60k, with nothing in between.
|
|
768
|
+
await run(async ({ framework }) => {
|
|
769
|
+
const f = framework();
|
|
770
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 });
|
|
771
|
+
const original = ContextManager.prototype.compile;
|
|
772
|
+
await withPatched('compile', async function (...args) {
|
|
773
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 });
|
|
774
|
+
return original.apply(this, args);
|
|
775
|
+
}, () => compile(f));
|
|
776
|
+
assert.deepEqual(budgets(f), { target: 60_000, effective: 60_000, transition: 'stable' }, 'the re-armed descent settled');
|
|
777
|
+
assert.equal(persisted(f)?.contextBudgetReachedTokens, undefined, 'a descent armed again to the same target is another descent');
|
|
778
|
+
});
|
|
779
|
+
// And armed again by a reset to the recipe's budget, below the live one.
|
|
780
|
+
await run(async ({ framework }) => {
|
|
781
|
+
const f = framework();
|
|
782
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 120_000 });
|
|
783
|
+
f.resetAgentRuntimeSettings('agent', ['contextBudgetTokens']);
|
|
784
|
+
assert.deepEqual(budgets(f), { target: 100_000, effective: 120_000, transition: 'converging' });
|
|
785
|
+
const original = ContextManager.prototype.compile;
|
|
786
|
+
await withPatched('compile', async function (...args) {
|
|
787
|
+
f.resetAgentRuntimeSettings('agent', ['contextBudgetTokens']);
|
|
788
|
+
return original.apply(this, args);
|
|
789
|
+
}, () => compile(f));
|
|
790
|
+
assert.deepEqual(budgets(f), { target: 100_000, effective: 100_000, transition: 'stable' }, 'the re-armed descent settled');
|
|
791
|
+
// Setting the budget it now runs at keeps any proof the settle gave it.
|
|
792
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 100_000, immediate: true });
|
|
793
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 100_000 }, 'a descent armed again by a reset is another descent');
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
it('a compile without a budget through compileWithInjections proves and records it too', async () => {
|
|
797
|
+
await run(async ({ framework }) => {
|
|
798
|
+
const f = framework();
|
|
799
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
800
|
+
await f.getAgent('agent').compileWithInjections();
|
|
801
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 });
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
it('a record that cannot be read is logged, and told again at the next compile', async () => {
|
|
805
|
+
await run(async ({ framework }) => {
|
|
806
|
+
const f = framework();
|
|
807
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
808
|
+
const store = f.getStore();
|
|
809
|
+
const original = store.getStateJson.bind(store);
|
|
810
|
+
store.getStateJson = () => { throw new Error('store unreadable'); };
|
|
811
|
+
const log = quietErrors();
|
|
812
|
+
try {
|
|
813
|
+
await compile(f);
|
|
814
|
+
}
|
|
815
|
+
finally {
|
|
816
|
+
store.getStateJson = original;
|
|
817
|
+
log.restore();
|
|
818
|
+
}
|
|
819
|
+
assert.ok(log.lines.some((l) => l.includes("couldn't record agent's compile at budget 60000")), log.lines.join('\n'));
|
|
820
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000 });
|
|
821
|
+
await compile(f);
|
|
822
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 });
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
it("a record the boot can't read is logged, and the agent boots at the recipe's", async () => {
|
|
826
|
+
await run(async ({ framework, restart }) => {
|
|
827
|
+
framework().updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
828
|
+
// Only the boot's read of the record fails.
|
|
829
|
+
const proto = JsStore.prototype;
|
|
830
|
+
const original = proto.getStateJson;
|
|
831
|
+
proto.getStateJson = function (...args) {
|
|
832
|
+
if (new Error().stack?.includes('readAgentRuntimeSettings'))
|
|
833
|
+
throw new Error('store unreadable');
|
|
834
|
+
return original.apply(this, args);
|
|
835
|
+
};
|
|
836
|
+
const log = quietErrors();
|
|
837
|
+
let restarted;
|
|
838
|
+
try {
|
|
839
|
+
restarted = await restart();
|
|
840
|
+
}
|
|
841
|
+
finally {
|
|
842
|
+
proto.getStateJson = original;
|
|
843
|
+
log.restore();
|
|
844
|
+
}
|
|
845
|
+
assert.deepEqual(budgets(restarted), { target: 100_000, effective: 100_000, transition: 'stable' });
|
|
846
|
+
assert.ok(log.lines.some((l) => l.includes("agent: its persisted settings can't be read (store unreadable); it boots at the recipe's")), log.lines.join('\n'));
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
it('after a withdrawal that failed and a persisting write without the marker, the next fit writes it again', async () => {
|
|
850
|
+
await run(async ({ framework }) => {
|
|
851
|
+
const f = framework();
|
|
852
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
853
|
+
await compile(f);
|
|
854
|
+
assert.equal(persisted(f)?.contextBudgetReachedTokens, 60_000);
|
|
855
|
+
const store = f.getStore();
|
|
856
|
+
const write = store.setStateJson.bind(store);
|
|
857
|
+
store.setStateJson = () => { throw new Error('disk full'); };
|
|
858
|
+
const refusal = Object.assign(new Error('over budget'), { name: 'OverBudgetError' });
|
|
859
|
+
const log = quietErrors();
|
|
860
|
+
try {
|
|
861
|
+
await withPatched('compile', async () => { throw refusal; }, async () => {
|
|
862
|
+
await assert.rejects(() => compile(f), /over budget/);
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
finally {
|
|
866
|
+
store.setStateJson = write;
|
|
867
|
+
log.restore();
|
|
868
|
+
}
|
|
869
|
+
f.updateAgentRuntimeSettings('agent', { tailTokens: 20_000 });
|
|
870
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000, tailTokens: 20_000 }, 'the refusal stands in the record now');
|
|
871
|
+
await compile(f);
|
|
872
|
+
assert.equal(persisted(f)?.contextBudgetReachedTokens, 60_000, 'the next fit is told, and written');
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
/** Apply `budget` at once and compile, so it's proven, then restart: the boot applies it at once. */
|
|
876
|
+
async function provenAtBoot(h, budget) {
|
|
877
|
+
h.framework().updateAgentRuntimeSettings('agent', { contextBudgetTokens: budget, immediate: true });
|
|
878
|
+
await compile(h.framework());
|
|
879
|
+
const restarted = await h.restart();
|
|
880
|
+
assert.deepEqual(budgets(restarted), { target: budget, effective: budget, transition: 'stable' }, 'applied at once at boot');
|
|
881
|
+
return restarted;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* A compile whose first plan the context manager refuses, after `meanwhile`
|
|
885
|
+
* ran while that plan was under way: the budget each plan was asked for,
|
|
886
|
+
* the record as the second began, the log, and what it threw.
|
|
887
|
+
*/
|
|
888
|
+
async function refusedFirst(f, meanwhile) {
|
|
889
|
+
const original = ContextManager.prototype.compile;
|
|
890
|
+
const planned = [];
|
|
891
|
+
let recordAtRetry;
|
|
892
|
+
const refusal = Object.assign(new Error('over budget'), { name: 'OverBudgetError' });
|
|
893
|
+
const log = quietErrors();
|
|
894
|
+
let thrown;
|
|
895
|
+
try {
|
|
896
|
+
await withPatched('compile', async function (budget, ...rest) {
|
|
897
|
+
planned.push(budget?.maxTokens ?? -1);
|
|
898
|
+
if (planned.length === 1) {
|
|
899
|
+
meanwhile?.();
|
|
900
|
+
throw refusal;
|
|
901
|
+
}
|
|
902
|
+
recordAtRetry = { ...persisted(f) };
|
|
903
|
+
return original.apply(this, [budget, ...rest]);
|
|
904
|
+
}, () => compile(f));
|
|
905
|
+
}
|
|
906
|
+
catch (error) {
|
|
907
|
+
thrown = error;
|
|
908
|
+
}
|
|
909
|
+
finally {
|
|
910
|
+
log.restore();
|
|
911
|
+
}
|
|
912
|
+
return { planned, recordAtRetry, lines: log.lines, thrown };
|
|
913
|
+
}
|
|
914
|
+
it('a budget applied at once at boot whose first compile is refused is restored paced, and the compile goes on', async () => {
|
|
915
|
+
await run(async (h) => {
|
|
916
|
+
const restarted = await provenAtBoot(h, 60_000);
|
|
917
|
+
const { planned, recordAtRetry, lines, thrown } = await refusedFirst(restarted);
|
|
918
|
+
assert.equal(thrown, undefined);
|
|
919
|
+
assert.deepEqual(planned, [60_000, 100_000], 'refused at the boot budget, then compiled from the recipe\'s');
|
|
920
|
+
assert.deepEqual(recordAtRetry, { contextBudgetTokens: 60_000 }, 'the refusal withdrew the marker before the compile went on');
|
|
921
|
+
assert.ok(lines.some((l) => l.includes('restored it as a paced descent from the recipe\'s budget')), lines.join('\n'));
|
|
922
|
+
assert.equal(budgets(restarted).target, 60_000, 'the descent to it is armed (or settled)');
|
|
923
|
+
assert.equal(persisted(restarted)?.contextBudgetTokens, 60_000);
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
it('a dry run under a budget the boot applied at once proves nothing: the fallback stays armed, whether the preview fits or is refused', async () => {
|
|
927
|
+
await run(async (h) => {
|
|
928
|
+
const restarted = await provenAtBoot(h, 60_000);
|
|
929
|
+
const recorded = persisted(restarted);
|
|
930
|
+
// A preview that fits notes nothing and keeps the fallback.
|
|
931
|
+
await restarted.previewActivation('agent');
|
|
932
|
+
// A preview refused at the boot budget restores nothing from a preview.
|
|
933
|
+
const refusal = Object.assign(new Error('over budget'), { name: 'OverBudgetError' });
|
|
934
|
+
const log = quietErrors();
|
|
935
|
+
try {
|
|
936
|
+
await withPatched('compile', async () => { throw refusal; }, async () => {
|
|
937
|
+
await assert.rejects(restarted.previewActivation('agent'), /over budget/);
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
finally {
|
|
941
|
+
log.restore();
|
|
942
|
+
}
|
|
943
|
+
assert.ok(!log.lines.some((l) => l.includes('the context manager refused')), log.lines.join('\n'));
|
|
944
|
+
assert.deepEqual(budgets(restarted), { target: 60_000, effective: 60_000, transition: 'stable' }, 'still the boot budget, applied at once');
|
|
945
|
+
assert.deepEqual(persisted(restarted), recorded, 'neither preview wrote the record');
|
|
946
|
+
// The fallback is still armed: the first sent compile refused at the
|
|
947
|
+
// boot budget restores it paced, and goes on.
|
|
948
|
+
const { planned, lines, thrown } = await refusedFirst(restarted);
|
|
949
|
+
assert.equal(thrown, undefined);
|
|
950
|
+
assert.deepEqual(planned, [60_000, 100_000]);
|
|
951
|
+
assert.ok(lines.some((l) => l.includes('restored it as a paced descent from the recipe\'s budget')), lines.join('\n'));
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
it("the fallback runs once: if the recipe's budget is refused too, its compile reports that refusal", { timeout: 20_000 }, async () => {
|
|
955
|
+
// Restored paced, and kept set where the strategy can't prepare a smaller window.
|
|
956
|
+
for (const canPace of [true, false]) {
|
|
957
|
+
await run(async (h) => {
|
|
958
|
+
const restarted = await provenAtBoot(h, 60_000);
|
|
959
|
+
if (!canPace) {
|
|
960
|
+
const cm = restarted.getAgent('agent').getContextManager();
|
|
961
|
+
const hot = cm.updateHotContextSettings.bind(cm);
|
|
962
|
+
cm.updateHotContextSettings = (update) => {
|
|
963
|
+
if (typeof update.preparedWindowTokens === 'number')
|
|
964
|
+
throw new Error('no smaller window here');
|
|
965
|
+
return hot(update);
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
const refusal = Object.assign(new Error('over budget'), { name: 'OverBudgetError' });
|
|
969
|
+
const planned = [];
|
|
970
|
+
const log = quietErrors();
|
|
971
|
+
try {
|
|
972
|
+
await withPatched('compile', async function (budget) {
|
|
973
|
+
planned.push(budget?.maxTokens ?? -1);
|
|
974
|
+
throw refusal;
|
|
975
|
+
}, async () => {
|
|
976
|
+
await assert.rejects(compile(restarted), /over budget/);
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
finally {
|
|
980
|
+
log.restore();
|
|
981
|
+
}
|
|
982
|
+
assert.deepEqual(planned, [60_000, 100_000], `one fallback, then the refusal at the recipe's budget (canPace ${canPace})`);
|
|
983
|
+
assert.equal(log.lines.filter((l) => l.includes('the context manager refused')).length, 1, `one fallback line (canPace ${canPace})`);
|
|
984
|
+
assert.equal(budgets(restarted).target, 60_000, 'the boot budget stays set');
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
it('a budget at or above the recipe\'s, applied at once at boot, has no paced restore to fall back to: its refusal is the compile\'s', async () => {
|
|
989
|
+
await run(async (h) => {
|
|
990
|
+
const restarted = await provenAtBoot(h, 120_000);
|
|
991
|
+
const { planned, lines, thrown } = await refusedFirst(restarted);
|
|
992
|
+
assert.equal(thrown?.message, 'over budget');
|
|
993
|
+
assert.deepEqual(planned, [120_000], 'no second compile');
|
|
994
|
+
assert.ok(!lines.some((l) => l.includes('paced')), lines.join('\n'));
|
|
995
|
+
assert.deepEqual(budgets(restarted), { target: 120_000, effective: 120_000, transition: 'stable' });
|
|
996
|
+
assert.deepEqual(persisted(restarted), { contextBudgetTokens: 120_000 }, 'the refusal withdrew the marker');
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
it("a refused budget the boot applied at once that can't be restored paced: the agent compiles at the recipe's, keeping the budget set", async () => {
|
|
1000
|
+
await run(async (h) => {
|
|
1001
|
+
const restarted = await provenAtBoot(h, 60_000);
|
|
1002
|
+
// The strategy can't prepare a smaller window now.
|
|
1003
|
+
const cm = restarted.getAgent('agent').getContextManager();
|
|
1004
|
+
const hot = cm.updateHotContextSettings.bind(cm);
|
|
1005
|
+
cm.updateHotContextSettings = (update) => {
|
|
1006
|
+
if (typeof update.preparedWindowTokens === 'number')
|
|
1007
|
+
throw new Error('no smaller window here');
|
|
1008
|
+
return hot(update);
|
|
1009
|
+
};
|
|
1010
|
+
const { planned, lines, thrown } = await refusedFirst(restarted);
|
|
1011
|
+
assert.equal(thrown, undefined);
|
|
1012
|
+
assert.deepEqual(planned, [60_000, 100_000]);
|
|
1013
|
+
assert.ok(lines.some((l) => l.includes("couldn't restore it paced (no smaller window here), so it compiles at the recipe's budget, with contextBudgetTokens=60000 kept set")), lines.join('\n'));
|
|
1014
|
+
assert.deepEqual(budgets(restarted), { target: 60_000, effective: 100_000, transition: 'stable' }, 'set to 60k, live at the recipe\'s');
|
|
1015
|
+
restarted.updateAgentRuntimeSettings('agent', { tailTokens: 20_000 });
|
|
1016
|
+
assert.deepEqual(persisted(restarted), { contextBudgetTokens: 60_000, tailTokens: 20_000 }, 'a later write keeps the budget');
|
|
1017
|
+
});
|
|
1018
|
+
});
|
|
1019
|
+
it('after the boot, a tail change keeps the fallback, and any write of the budget ends it', async () => {
|
|
1020
|
+
await run(async (h) => {
|
|
1021
|
+
h.framework().updateAgentRuntimeSettings('agent', { tailTokens: 15_000 });
|
|
1022
|
+
const restarted = await provenAtBoot(h, 60_000);
|
|
1023
|
+
restarted.updateAgentRuntimeSettings('agent', { tailTokens: 20_000 });
|
|
1024
|
+
const { planned, thrown } = await refusedFirst(restarted);
|
|
1025
|
+
assert.equal(thrown, undefined);
|
|
1026
|
+
assert.deepEqual(planned, [60_000, 100_000], 'restored paced');
|
|
1027
|
+
assert.equal(restarted.getAgentRuntimeSettings('agent').tailTokens, 20_000, 'the fallback re-applies only the budget, not the boot\'s tail');
|
|
1028
|
+
});
|
|
1029
|
+
// Each write of the budget, before the first compile or while it runs,
|
|
1030
|
+
// is the operator's decision: the refusal is that compile's own.
|
|
1031
|
+
const writes = [
|
|
1032
|
+
['a paced decrease', (f) => { f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 50_000 }); }, false, 60_000,
|
|
1033
|
+
{ target: 50_000, effective: 60_000, transition: 'converging' }],
|
|
1034
|
+
['the same budget at once', (f) => { f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true }); }, false, 60_000,
|
|
1035
|
+
{ target: 60_000, effective: 60_000, transition: 'stable' }],
|
|
1036
|
+
['the same budget, unpersisted', (f) => { f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 }, { persist: false }); }, false, 60_000,
|
|
1037
|
+
{ target: 60_000, effective: 60_000, transition: 'stable' }],
|
|
1038
|
+
['a reset of the budget', (f) => { f.resetAgentRuntimeSettings('agent', ['contextBudgetTokens']); }, false, 100_000,
|
|
1039
|
+
{ target: 100_000, effective: 100_000, transition: 'stable' }],
|
|
1040
|
+
['an increase while the first compile runs', (f) => { f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 80_000 }); }, true, 60_000,
|
|
1041
|
+
{ target: 80_000, effective: 80_000, transition: 'stable' }],
|
|
1042
|
+
];
|
|
1043
|
+
for (const [what, write, meanwhile, refusedAt, after] of writes) {
|
|
1044
|
+
await run(async (h) => {
|
|
1045
|
+
const restarted = await provenAtBoot(h, 60_000);
|
|
1046
|
+
if (!meanwhile)
|
|
1047
|
+
write(restarted);
|
|
1048
|
+
const { planned, thrown } = await refusedFirst(restarted, meanwhile ? () => write(restarted) : undefined);
|
|
1049
|
+
assert.equal(thrown?.message, 'over budget', what);
|
|
1050
|
+
assert.deepEqual(planned, [refusedAt], `${what}: no fallback`);
|
|
1051
|
+
assert.deepEqual(budgets(restarted), after, what);
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
// And a compile that fits at it ends the fallback too.
|
|
1055
|
+
await run(async (h) => {
|
|
1056
|
+
const restarted = await provenAtBoot(h, 60_000);
|
|
1057
|
+
await compile(restarted);
|
|
1058
|
+
const { planned, thrown } = await refusedFirst(restarted);
|
|
1059
|
+
assert.equal(thrown?.message, 'over budget');
|
|
1060
|
+
assert.deepEqual(planned, [60_000], 'once a compile has fit at it, a refusal is the compile\'s own');
|
|
1061
|
+
});
|
|
1062
|
+
});
|
|
1063
|
+
/** A writer at the usual maxTokens leaves `record`; the agent then boots with maxTokens raised to 70k, above a persisted 60k. */
|
|
1064
|
+
async function bootRaised(record, fn) {
|
|
1065
|
+
const { dir, config } = setup();
|
|
1066
|
+
const raised = () => ({ ...config(), agents: config().agents.map((a) => ({ ...a, maxTokens: 70_000 })) });
|
|
1067
|
+
try {
|
|
1068
|
+
const writer = await AgentFramework.create(config());
|
|
1069
|
+
writeRecord(writer, record);
|
|
1070
|
+
await writer.stop();
|
|
1071
|
+
const log = quietErrors();
|
|
1072
|
+
let booted;
|
|
1073
|
+
try {
|
|
1074
|
+
booted = await AgentFramework.create(raised());
|
|
1075
|
+
}
|
|
1076
|
+
finally {
|
|
1077
|
+
log.restore();
|
|
1078
|
+
}
|
|
1079
|
+
try {
|
|
1080
|
+
await fn(booted, log.lines);
|
|
1081
|
+
}
|
|
1082
|
+
finally {
|
|
1083
|
+
await booted.stop();
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
finally {
|
|
1087
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
it("a persisted budget the agent can't take now (maxTokens raised above it) stays set but unapplied, and it runs at the recipe's, with a line", async () => {
|
|
1091
|
+
const refused = 'contextBudgetTokens must be a safe integer greater than max response tokens (70000)';
|
|
1092
|
+
const kept = `contextBudgetTokens=60000 (${refused}) can't be applied now, so it stays set but unapplied: ` +
|
|
1093
|
+
'contextBudgetTokens is 60000, and compiles plan at 100000';
|
|
1094
|
+
const cases = [
|
|
1095
|
+
// One line, which keeps why a reached budget wasn't applied at once.
|
|
1096
|
+
[{ contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 },
|
|
1097
|
+
`[agent-settings] agent: its record says a compile fit at contextBudgetTokens=60000, but whether it fits now can't be checked (${refused}); ${kept}`],
|
|
1098
|
+
[{ contextBudgetTokens: 60_000 }, `[agent-settings] agent: ${kept}`],
|
|
1099
|
+
];
|
|
1100
|
+
for (const [record, line] of cases) {
|
|
1101
|
+
await bootRaised(record, (booted, lines) => {
|
|
1102
|
+
assert.deepEqual(budgets(booted), { target: 60_000, effective: 100_000, transition: 'stable' }, JSON.stringify(record));
|
|
1103
|
+
assert.deepEqual(lines.filter((l) => l.includes('[agent-settings]')), [line], JSON.stringify(record));
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
it('a reached budget applies at once on its own, though a tail beside it can\'t be applied', async () => {
|
|
1108
|
+
await run(async ({ framework, restart }) => {
|
|
1109
|
+
framework().updateAgentRuntimeSettings('agent', { tailTokens: 20_000 });
|
|
1110
|
+
framework().updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
1111
|
+
await compile(framework());
|
|
1112
|
+
assert.deepEqual(persisted(framework()), { tailTokens: 20_000, contextBudgetTokens: 60_000, contextBudgetReachedTokens: 60_000 });
|
|
1113
|
+
// The strategy refuses a live tail now; it takes a budget as before.
|
|
1114
|
+
const proto = ContextManager.prototype;
|
|
1115
|
+
const original = proto.updateHotContextSettings;
|
|
1116
|
+
proto.updateHotContextSettings = function (update) {
|
|
1117
|
+
if (update.tailTokens !== undefined)
|
|
1118
|
+
throw new Error('no live tail here');
|
|
1119
|
+
return original.call(this, update);
|
|
1120
|
+
};
|
|
1121
|
+
const log = quietErrors();
|
|
1122
|
+
let restarted;
|
|
1123
|
+
try {
|
|
1124
|
+
restarted = await restart();
|
|
1125
|
+
}
|
|
1126
|
+
finally {
|
|
1127
|
+
proto.updateHotContextSettings = original;
|
|
1128
|
+
log.restore();
|
|
1129
|
+
}
|
|
1130
|
+
assert.deepEqual(budgets(restarted), { target: 60_000, effective: 60_000, transition: 'stable' }, 'the budget applied at once');
|
|
1131
|
+
assert.deepEqual(log.lines.filter((l) => l.includes('[agent-settings]')), [
|
|
1132
|
+
"[agent-settings] agent: tailTokens=20000 (no live tail here) can't be applied now, so it stays set but unapplied: " +
|
|
1133
|
+
'contextBudgetTokens is 60000, and compiles plan at 60000',
|
|
1134
|
+
]);
|
|
1135
|
+
});
|
|
1136
|
+
});
|
|
1137
|
+
it("on a strategy without live settings, a tail and a lower budget stay set but unapplied, a later write keeps them, and a reset clears the tail", async () => {
|
|
1138
|
+
const dir = mkdtempSync(join(tmpdir(), 'agent-settings-no-hot-'));
|
|
1139
|
+
// The context manager's default strategy has no live settings.
|
|
1140
|
+
const config = () => ({
|
|
1141
|
+
storePath: join(dir, 'store'), membrane,
|
|
1142
|
+
agents: [{ name: 'agent', model: 'test-model', systemPrompt: 'test', contextBudgetTokens: 100_000, maxTokens: 10_000 }],
|
|
1143
|
+
modules: [],
|
|
1144
|
+
});
|
|
1145
|
+
let framework = await AgentFramework.create(config());
|
|
1146
|
+
try {
|
|
1147
|
+
writeRecord(framework, { contextBudgetTokens: 60_000, tailTokens: 20_000 });
|
|
1148
|
+
await framework.stop();
|
|
1149
|
+
const log = quietErrors();
|
|
1150
|
+
try {
|
|
1151
|
+
framework = await AgentFramework.create(config());
|
|
1152
|
+
}
|
|
1153
|
+
finally {
|
|
1154
|
+
log.restore();
|
|
1155
|
+
}
|
|
1156
|
+
assert.deepEqual(framework.getAgentRuntimeSettings('agent'), {
|
|
1157
|
+
contextBudgetTokens: 60_000,
|
|
1158
|
+
effectiveContextBudgetTokens: 100_000,
|
|
1159
|
+
sameRoundThinkTextPolicy: 'public',
|
|
1160
|
+
sameRoundThinkTextPolicySource: 'compatibility_default',
|
|
1161
|
+
transition: 'stable',
|
|
1162
|
+
});
|
|
1163
|
+
assert.deepEqual(log.lines.filter((l) => l.includes('[agent-settings]')), [
|
|
1164
|
+
'[agent-settings] agent: tailTokens=20000 (The active context strategy does not support live tail/transition settings), ' +
|
|
1165
|
+
'contextBudgetTokens=60000 (The active context strategy cannot prepare a smaller window live) ' +
|
|
1166
|
+
"can't be applied now, so they stay set but unapplied: contextBudgetTokens is 60000, and compiles plan at 100000",
|
|
1167
|
+
]);
|
|
1168
|
+
framework.updateAgentRuntimeSettings('agent', { sameRoundThinkTextPolicy: 'private' });
|
|
1169
|
+
assert.deepEqual(persisted(framework), { contextBudgetTokens: 60_000, tailTokens: 20_000, sameRoundThinkTextPolicy: 'private' });
|
|
1170
|
+
framework.resetAgentRuntimeSettings('agent', ['tailTokens']);
|
|
1171
|
+
assert.deepEqual(persisted(framework), { contextBudgetTokens: 60_000, sameRoundThinkTextPolicy: 'private' }, 'the reset clears the kept tail');
|
|
1172
|
+
}
|
|
1173
|
+
finally {
|
|
1174
|
+
await framework.stop();
|
|
1175
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
it("each persisted setting is restored on its own: a private policy governs and a tail applies when the budget can't, and a later write keeps the budget", async () => {
|
|
1179
|
+
await bootRaised({ contextBudgetTokens: 60_000, tailTokens: 20_000, sameRoundThinkTextPolicy: 'private' }, (booted) => {
|
|
1180
|
+
const { contextBudgetTokens, effectiveContextBudgetTokens, tailTokens, sameRoundThinkTextPolicy, sameRoundThinkTextPolicySource } = booted.getAgentRuntimeSettings('agent');
|
|
1181
|
+
assert.deepEqual({ contextBudgetTokens, effectiveContextBudgetTokens, tailTokens, sameRoundThinkTextPolicy, sameRoundThinkTextPolicySource }, { contextBudgetTokens: 60_000, effectiveContextBudgetTokens: 100_000, tailTokens: 20_000, sameRoundThinkTextPolicy: 'private', sameRoundThinkTextPolicySource: 'runtime_override' });
|
|
1182
|
+
// The snapshot each request takes, which routes its same-round text and describes its think tool.
|
|
1183
|
+
const request = booted
|
|
1184
|
+
.captureInferenceToolSnapshot(booted.getAgent('agent'));
|
|
1185
|
+
assert.equal(request.sameRoundThinkTextPolicy, 'private');
|
|
1186
|
+
booted.updateAgentRuntimeSettings('agent', { tailTokens: 25_000 });
|
|
1187
|
+
assert.deepEqual(persisted(booted), { contextBudgetTokens: 60_000, tailTokens: 25_000, sameRoundThinkTextPolicy: 'private' });
|
|
1188
|
+
});
|
|
1189
|
+
});
|
|
1190
|
+
it("a patch's preview plans at the reach its next compile plans at", async () => {
|
|
1191
|
+
// The reach of every kv-stable plan, a preview's or a compile's, as the
|
|
1192
|
+
// strategy hands it to the planner.
|
|
1193
|
+
const proto = AutobiographicalStrategy.prototype;
|
|
1194
|
+
const original = proto.buildPicker;
|
|
1195
|
+
let reaches = [];
|
|
1196
|
+
proto.buildPicker = function (...args) {
|
|
1197
|
+
const picker = original.apply(this, args);
|
|
1198
|
+
reaches.push(this._lastKvStable?.opts?.reachTokens);
|
|
1199
|
+
return picker;
|
|
1200
|
+
};
|
|
1201
|
+
// Preview `patch` after `before`, then apply it and compile: the reach both planned at.
|
|
1202
|
+
const both = async (before, patch) => {
|
|
1203
|
+
let reach;
|
|
1204
|
+
await run(async ({ framework }) => {
|
|
1205
|
+
const f = framework();
|
|
1206
|
+
before(f);
|
|
1207
|
+
reaches = [];
|
|
1208
|
+
f.previewAgentRuntimeSettings('agent', patch);
|
|
1209
|
+
const previewed = reaches;
|
|
1210
|
+
reaches = [];
|
|
1211
|
+
f.updateAgentRuntimeSettings('agent', patch, { persist: false });
|
|
1212
|
+
reaches = [];
|
|
1213
|
+
await compile(f);
|
|
1214
|
+
assert.ok(previewed.length > 0 && reaches.length > 0, `${JSON.stringify(patch)}: planned ${JSON.stringify({ previewed, reaches })}`);
|
|
1215
|
+
assert.deepEqual([...new Set(previewed)], [...new Set(reaches)], JSON.stringify(patch));
|
|
1216
|
+
reach = reaches[0];
|
|
1217
|
+
});
|
|
1218
|
+
return reach;
|
|
1219
|
+
};
|
|
1220
|
+
const none = () => { };
|
|
1221
|
+
try {
|
|
1222
|
+
// The recipe's reach is 8k; no descent is under way unless `before` arms one.
|
|
1223
|
+
assert.equal(await both(none, { contextBudgetTokens: 60_000, transitionPaceTokens: 4_000, immediate: true }), 8_000, 'an immediate change: the configured reach');
|
|
1224
|
+
assert.equal(await both(none, { contextBudgetTokens: 60_000, transitionPaceTokens: 4_000 }), 4_000, 'a paced decrease: its pace');
|
|
1225
|
+
assert.equal(await both(none, { transitionPaceTokens: 4_000 }), 8_000, 'a pace with no descent: the configured reach');
|
|
1226
|
+
const descending = (f) => { f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 }); };
|
|
1227
|
+
assert.equal(await both(descending, { transitionPaceTokens: 4_000 }), 4_000, 'a pace during a descent: that pace');
|
|
1228
|
+
const paced = (f) => { f.updateAgentRuntimeSettings('agent', { transitionPaceTokens: 4_000 }); };
|
|
1229
|
+
assert.equal(await both(paced, { contextBudgetTokens: 60_000 }), 4_000, 'a paced decrease without a pace: the live one');
|
|
1230
|
+
}
|
|
1231
|
+
finally {
|
|
1232
|
+
proto.buildPicker = original;
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
it('a descent ended by an immediate change to its own target is not credited to the compile that ran meanwhile', async () => {
|
|
1236
|
+
await run(async ({ framework }) => {
|
|
1237
|
+
const f = framework();
|
|
1238
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000 });
|
|
1239
|
+
assert.equal(budgets(f).transition, 'converging');
|
|
1240
|
+
const original = ContextManager.prototype.compile;
|
|
1241
|
+
// The compile plans at the live 100k; the same 60k is applied at once
|
|
1242
|
+
// while it runs, ending the descent without a settle.
|
|
1243
|
+
await withPatched('compile', async function (...args) {
|
|
1244
|
+
f.updateAgentRuntimeSettings('agent', { contextBudgetTokens: 60_000, immediate: true });
|
|
1245
|
+
return original.apply(this, args);
|
|
1246
|
+
}, () => compile(f));
|
|
1247
|
+
assert.deepEqual(budgets(f), { target: 60_000, effective: 60_000, transition: 'stable' });
|
|
1248
|
+
assert.deepEqual(persisted(f), { contextBudgetTokens: 60_000 }, 'nothing settled, and the compile planned at 100k');
|
|
1249
|
+
});
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
120
1252
|
it('reset-all skips tail controls unsupported by the active strategy', async () => {
|
|
121
1253
|
const dir = mkdtempSync(join(tmpdir(), 'agent-settings-basic-'));
|
|
122
1254
|
const framework = await AgentFramework.create({
|
|
@@ -128,6 +1260,7 @@ describe('agent runtime settings', () => {
|
|
|
128
1260
|
try {
|
|
129
1261
|
assert.deepEqual(framework.resetAgentRuntimeSettings('agent'), {
|
|
130
1262
|
contextBudgetTokens: 100_000,
|
|
1263
|
+
effectiveContextBudgetTokens: 100_000,
|
|
131
1264
|
sameRoundThinkTextPolicy: 'public',
|
|
132
1265
|
sameRoundThinkTextPolicySource: 'compatibility_default',
|
|
133
1266
|
transition: 'stable',
|
|
@@ -154,18 +1287,21 @@ describe('agent runtime settings', () => {
|
|
|
154
1287
|
try {
|
|
155
1288
|
assert.deepEqual(framework.getAgentRuntimeSettings('agent'), {
|
|
156
1289
|
contextBudgetTokens: 100_000,
|
|
1290
|
+
effectiveContextBudgetTokens: 100_000,
|
|
157
1291
|
sameRoundThinkTextPolicy: 'private',
|
|
158
1292
|
sameRoundThinkTextPolicySource: 'recipe',
|
|
159
1293
|
transition: 'stable',
|
|
160
1294
|
});
|
|
161
1295
|
assert.deepEqual(framework.updateAgentRuntimeSettings('agent', { sameRoundThinkTextPolicy: 'public' }), {
|
|
162
1296
|
contextBudgetTokens: 100_000,
|
|
1297
|
+
effectiveContextBudgetTokens: 100_000,
|
|
163
1298
|
sameRoundThinkTextPolicy: 'public',
|
|
164
1299
|
sameRoundThinkTextPolicySource: 'runtime_override',
|
|
165
1300
|
transition: 'stable',
|
|
166
1301
|
});
|
|
167
1302
|
assert.deepEqual(framework.resetAgentRuntimeSettings('agent', ['sameRoundThinkTextPolicy']), {
|
|
168
1303
|
contextBudgetTokens: 100_000,
|
|
1304
|
+
effectiveContextBudgetTokens: 100_000,
|
|
169
1305
|
sameRoundThinkTextPolicy: 'private',
|
|
170
1306
|
sameRoundThinkTextPolicySource: 'recipe',
|
|
171
1307
|
transition: 'stable',
|
|
@@ -407,7 +1543,7 @@ it('previewAgentRuntimeSettings: paced patches report effective=live plus an adv
|
|
|
407
1543
|
assert.equal(current.effective?.budgetTokens, 550_000);
|
|
408
1544
|
});
|
|
409
1545
|
});
|
|
410
|
-
it('preflight: boot restore is never preflighted
|
|
1546
|
+
it('preflight: a boot restore without a matching reached budget is never preflighted, so an infeasible persisted budget cannot brick startup', async () => {
|
|
411
1547
|
const dir = mkdtempSync(join(tmpdir(), 'agent-settings-preflight-boot-'));
|
|
412
1548
|
const storePath = join(dir, 'store');
|
|
413
1549
|
const config = () => ({
|