apex-code 0.0.1-alpha.0 → 0.0.1-alpha.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/CHANGELOG.md +24 -0
- package/README.md +66 -681
- package/dist/cli/args.d.ts +5 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +45 -6
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/auth-command.d.ts.map +1 -1
- package/dist/cli/auth-command.js +3 -3
- package/dist/cli/auth-command.js.map +1 -1
- package/dist/cli/cost-command.d.ts +19 -0
- package/dist/cli/cost-command.d.ts.map +1 -0
- package/dist/cli/cost-command.js +107 -0
- package/dist/cli/cost-command.js.map +1 -0
- package/dist/cli/startup-ui.d.ts +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +1 -2
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +45 -16
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -7
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +2 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +9 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +30 -4
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +46 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +137 -19
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +6 -6
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +1 -1
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/context/deferred-schemas.d.ts +92 -0
- package/dist/core/context/deferred-schemas.d.ts.map +1 -0
- package/dist/core/context/deferred-schemas.js +74 -0
- package/dist/core/context/deferred-schemas.js.map +1 -0
- package/dist/core/context/eviction.d.ts +47 -0
- package/dist/core/context/eviction.d.ts.map +1 -0
- package/dist/core/context/eviction.js +83 -0
- package/dist/core/context/eviction.js.map +1 -0
- package/dist/core/context/pipeline.d.ts +90 -0
- package/dist/core/context/pipeline.d.ts.map +1 -0
- package/dist/core/context/pipeline.js +109 -0
- package/dist/core/context/pipeline.js.map +1 -0
- package/dist/core/credential-failover.d.ts +30 -0
- package/dist/core/credential-failover.d.ts.map +1 -0
- package/dist/core/credential-failover.js +46 -0
- package/dist/core/credential-failover.js.map +1 -0
- package/dist/core/credential-pool.d.ts +60 -0
- package/dist/core/credential-pool.d.ts.map +1 -0
- package/dist/core/credential-pool.js +84 -0
- package/dist/core/credential-pool.js.map +1 -0
- package/dist/core/delegation/agents.d.ts +15 -0
- package/dist/core/delegation/agents.d.ts.map +1 -0
- package/dist/core/delegation/agents.js +55 -0
- package/dist/core/delegation/agents.js.map +1 -0
- package/dist/core/delegation/ceiling.d.ts +29 -0
- package/dist/core/delegation/ceiling.d.ts.map +1 -0
- package/dist/core/delegation/ceiling.js +34 -0
- package/dist/core/delegation/ceiling.js.map +1 -0
- package/dist/core/delegation/runtime.d.ts +94 -0
- package/dist/core/delegation/runtime.d.ts.map +1 -0
- package/dist/core/delegation/runtime.js +117 -0
- package/dist/core/delegation/runtime.js.map +1 -0
- package/dist/core/durable-state/daemon.d.ts +37 -0
- package/dist/core/durable-state/daemon.d.ts.map +1 -0
- package/dist/core/durable-state/daemon.js +62 -0
- package/dist/core/durable-state/daemon.js.map +1 -0
- package/dist/core/durable-state/provenance.d.ts +7 -0
- package/dist/core/durable-state/provenance.d.ts.map +1 -0
- package/dist/core/durable-state/provenance.js +24 -0
- package/dist/core/durable-state/provenance.js.map +1 -0
- package/dist/core/durable-state/sqlite.d.ts +84 -0
- package/dist/core/durable-state/sqlite.d.ts.map +1 -0
- package/dist/core/durable-state/sqlite.js +331 -0
- package/dist/core/durable-state/sqlite.js.map +1 -0
- package/dist/core/environment.d.ts +12 -0
- package/dist/core/environment.d.ts.map +1 -0
- package/dist/core/environment.js +69 -0
- package/dist/core/environment.js.map +1 -0
- package/dist/core/evidence-policy.d.ts +17 -0
- package/dist/core/evidence-policy.d.ts.map +1 -0
- package/dist/core/evidence-policy.js +25 -0
- package/dist/core/evidence-policy.js.map +1 -0
- package/dist/core/evidence.d.ts +16 -0
- package/dist/core/evidence.d.ts.map +1 -0
- package/dist/core/evidence.js +51 -0
- package/dist/core/evidence.js.map +1 -0
- package/dist/core/experimental.d.ts.map +1 -1
- package/dist/core/experimental.js +2 -1
- package/dist/core/experimental.js.map +1 -1
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/model-config.d.ts +8 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +21 -5
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-resolver.d.ts +20 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +59 -0
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/model-runtime.d.ts +73 -0
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +225 -8
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/observability/aggregate.d.ts +21 -0
- package/dist/core/observability/aggregate.d.ts.map +1 -0
- package/dist/core/observability/aggregate.js +36 -0
- package/dist/core/observability/aggregate.js.map +1 -0
- package/dist/core/observability/otlp.d.ts +66 -0
- package/dist/core/observability/otlp.d.ts.map +1 -0
- package/dist/core/observability/otlp.js +95 -0
- package/dist/core/observability/otlp.js.map +1 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +3 -2
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/permissions/gate.d.ts +35 -0
- package/dist/core/permissions/gate.d.ts.map +1 -0
- package/dist/core/permissions/gate.js +72 -0
- package/dist/core/permissions/gate.js.map +1 -0
- package/dist/core/permissions/modes.d.ts +18 -0
- package/dist/core/permissions/modes.d.ts.map +1 -0
- package/dist/core/permissions/modes.js +57 -0
- package/dist/core/permissions/modes.js.map +1 -0
- package/dist/core/permissions/responder.d.ts +30 -0
- package/dist/core/permissions/responder.d.ts.map +1 -0
- package/dist/core/permissions/responder.js +32 -0
- package/dist/core/permissions/responder.js.map +1 -0
- package/dist/core/permissions/rules.d.ts +34 -0
- package/dist/core/permissions/rules.d.ts.map +1 -0
- package/dist/core/permissions/rules.js +74 -0
- package/dist/core/permissions/rules.js.map +1 -0
- package/dist/core/permissions/startup.d.ts +33 -0
- package/dist/core/permissions/startup.d.ts.map +1 -0
- package/dist/core/permissions/startup.js +55 -0
- package/dist/core/permissions/startup.js.map +1 -0
- package/dist/core/permissions/store.d.ts +114 -0
- package/dist/core/permissions/store.d.ts.map +1 -0
- package/dist/core/permissions/store.js +229 -0
- package/dist/core/permissions/store.js.map +1 -0
- package/dist/core/project-trust.d.ts.map +1 -1
- package/dist/core/project-trust.js +1 -1
- package/dist/core/project-trust.js.map +1 -1
- package/dist/core/provider-attribution.d.ts.map +1 -1
- package/dist/core/provider-attribution.js +6 -7
- package/dist/core/provider-attribution.js.map +1 -1
- package/dist/core/remote-catalog-provider.d.ts +2 -2
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +20 -16
- package/dist/core/remote-catalog-provider.js.map +1 -1
- package/dist/core/sandbox/child-entry.d.ts +2 -0
- package/dist/core/sandbox/child-entry.d.ts.map +1 -0
- package/dist/core/sandbox/child-entry.js +11 -0
- package/dist/core/sandbox/child-entry.js.map +1 -0
- package/dist/core/sandbox/cli-launch.d.ts +27 -0
- package/dist/core/sandbox/cli-launch.d.ts.map +1 -0
- package/dist/core/sandbox/cli-launch.js +133 -0
- package/dist/core/sandbox/cli-launch.js.map +1 -0
- package/dist/core/sandbox/cli-supervisor.d.ts +25 -0
- package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -0
- package/dist/core/sandbox/cli-supervisor.js +57 -0
- package/dist/core/sandbox/cli-supervisor.js.map +1 -0
- package/dist/core/sandbox/linux-backend.d.ts +18 -0
- package/dist/core/sandbox/linux-backend.d.ts.map +1 -0
- package/dist/core/sandbox/linux-backend.js +186 -0
- package/dist/core/sandbox/linux-backend.js.map +1 -0
- package/dist/core/sandbox/macos-backend.d.ts +18 -0
- package/dist/core/sandbox/macos-backend.d.ts.map +1 -0
- package/dist/core/sandbox/macos-backend.js +195 -0
- package/dist/core/sandbox/macos-backend.js.map +1 -0
- package/dist/core/sandbox/network-proxy.d.ts +19 -0
- package/dist/core/sandbox/network-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/network-proxy.js +79 -0
- package/dist/core/sandbox/network-proxy.js.map +1 -0
- package/dist/core/sandbox/policy.d.ts +32 -0
- package/dist/core/sandbox/policy.d.ts.map +1 -0
- package/dist/core/sandbox/policy.js +38 -0
- package/dist/core/sandbox/policy.js.map +1 -0
- package/dist/core/sandbox/supervisor.d.ts +31 -0
- package/dist/core/sandbox/supervisor.d.ts.map +1 -0
- package/dist/core/sandbox/supervisor.js +18 -0
- package/dist/core/sandbox/supervisor.js.map +1 -0
- package/dist/core/sandbox/violations.d.ts +23 -0
- package/dist/core/sandbox/violations.d.ts.map +1 -0
- package/dist/core/sandbox/violations.js +30 -0
- package/dist/core/sandbox/violations.js.map +1 -0
- package/dist/core/sdk.d.ts +20 -36
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +129 -2
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +41 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +57 -0
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-share.d.ts +16 -0
- package/dist/core/session-share.d.ts.map +1 -0
- package/dist/core/session-share.js +22 -0
- package/dist/core/session-share.js.map +1 -0
- package/dist/core/settings-manager.d.ts +38 -9
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +39 -24
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +5 -5
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/timings.d.ts +1 -1
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js +3 -2
- package/dist/core/timings.js.map +1 -1
- package/dist/core/tools/ask-user.d.ts +25 -0
- package/dist/core/tools/ask-user.d.ts.map +1 -0
- package/dist/core/tools/ask-user.js +49 -0
- package/dist/core/tools/ask-user.js.map +1 -0
- package/dist/core/tools/bash-command-segments.d.ts +22 -0
- package/dist/core/tools/bash-command-segments.d.ts.map +1 -0
- package/dist/core/tools/bash-command-segments.js +128 -0
- package/dist/core/tools/bash-command-segments.js.map +1 -0
- package/dist/core/tools/bash.d.ts +26 -3
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +105 -16
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/contract.d.ts +156 -0
- package/dist/core/tools/contract.d.ts.map +1 -0
- package/dist/core/tools/contract.js +48 -0
- package/dist/core/tools/contract.js.map +1 -0
- package/dist/core/tools/delegate.d.ts +38 -0
- package/dist/core/tools/delegate.d.ts.map +1 -0
- package/dist/core/tools/delegate.js +66 -0
- package/dist/core/tools/delegate.js.map +1 -0
- package/dist/core/tools/edit.d.ts +2 -2
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +24 -0
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts +2 -2
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +12 -0
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts +2 -2
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +12 -0
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +29 -4
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +118 -2
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts +2 -2
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +12 -0
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +1 -1
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/core/tools/path-permission.d.ts +18 -0
- package/dist/core/tools/path-permission.d.ts.map +1 -0
- package/dist/core/tools/path-permission.js +28 -0
- package/dist/core/tools/path-permission.js.map +1 -0
- package/dist/core/tools/plan-present.d.ts +27 -0
- package/dist/core/tools/plan-present.d.ts.map +1 -0
- package/dist/core/tools/plan-present.js +55 -0
- package/dist/core/tools/plan-present.js.map +1 -0
- package/dist/core/tools/read.d.ts +2 -2
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +12 -0
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/test.d.ts +34 -0
- package/dist/core/tools/test.d.ts.map +1 -0
- package/dist/core/tools/test.js +75 -0
- package/dist/core/tools/test.js.map +1 -0
- package/dist/core/tools/todo-write.d.ts +31 -0
- package/dist/core/tools/todo-write.d.ts.map +1 -0
- package/dist/core/tools/todo-write.js +51 -0
- package/dist/core/tools/todo-write.js.map +1 -0
- package/dist/core/tools/tool-definition-wrapper.d.ts +13 -2
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.js +11 -1
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
- package/dist/core/tools/tool-schema.d.ts +22 -0
- package/dist/core/tools/tool-schema.d.ts.map +1 -0
- package/dist/core/tools/tool-schema.js +45 -0
- package/dist/core/tools/tool-schema.js.map +1 -0
- package/dist/core/tools/web-fetch.d.ts +26 -0
- package/dist/core/tools/web-fetch.d.ts.map +1 -0
- package/dist/core/tools/web-fetch.js +62 -0
- package/dist/core/tools/web-fetch.js.map +1 -0
- package/dist/core/tools/web-search.d.ts +26 -0
- package/dist/core/tools/web-search.d.ts.map +1 -0
- package/dist/core/tools/web-search.js +53 -0
- package/dist/core/tools/web-search.js.map +1 -0
- package/dist/core/tools/write.d.ts +6 -2
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +32 -1
- package/dist/core/tools/write.js.map +1 -1
- package/dist/core/usage-performance-store.d.ts +56 -0
- package/dist/core/usage-performance-store.d.ts.map +1 -0
- package/dist/core/usage-performance-store.js +85 -0
- package/dist/core/usage-performance-store.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +48 -6
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +2 -2
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +9 -6
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts +1 -4
- package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.js +11 -39
- package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +8 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +49 -20
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -2
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +6 -6
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +2 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/external-editor.d.ts.map +1 -1
- package/dist/modes/interactive/external-editor.js +58 -9
- package/dist/modes/interactive/external-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +2 -3
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +92 -104
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme-schema.json +2 -2
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +15 -10
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/rpc-entry.d.ts.map +1 -1
- package/dist/rpc-entry.js +2 -1
- package/dist/rpc-entry.js.map +1 -1
- package/dist/server/create-harness.d.ts +1 -1
- package/dist/server/create-harness.d.ts.map +1 -1
- package/dist/server/create-harness.js +6 -5
- package/dist/server/create-harness.js.map +1 -1
- package/dist/testing/replay/metrics.d.ts +24 -0
- package/dist/testing/replay/metrics.d.ts.map +1 -0
- package/dist/testing/replay/metrics.js +35 -0
- package/dist/testing/replay/metrics.js.map +1 -0
- package/dist/testing/replay/recorded-provider.d.ts +10 -0
- package/dist/testing/replay/recorded-provider.d.ts.map +1 -0
- package/dist/testing/replay/recorded-provider.js +108 -0
- package/dist/testing/replay/recorded-provider.js.map +1 -0
- package/dist/testing/replay/runner.d.ts +56 -0
- package/dist/testing/replay/runner.d.ts.map +1 -0
- package/dist/testing/replay/runner.js +406 -0
- package/dist/testing/replay/runner.js.map +1 -0
- package/dist/utils/apex-code-user-agent.d.ts +2 -0
- package/dist/utils/apex-code-user-agent.d.ts.map +1 -0
- package/dist/utils/apex-code-user-agent.js +5 -0
- package/dist/utils/apex-code-user-agent.js.map +1 -0
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +1 -1
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +32 -0
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +198 -100
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts +5 -5
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +23 -14
- package/dist/utils/version-check.js.map +1 -1
- package/dist/utils/windows-self-update.d.ts.map +1 -1
- package/dist/utils/windows-self-update.js +1 -1
- package/dist/utils/windows-self-update.js.map +1 -1
- package/docs/compaction.md +21 -21
- package/docs/containerization.md +24 -21
- package/docs/custom-provider.md +9 -9
- package/docs/development.md +11 -11
- package/docs/environment-variables.md +36 -77
- package/docs/extensions.md +47 -47
- package/docs/index.md +12 -24
- package/docs/json.md +6 -6
- package/docs/keybindings.md +3 -3
- package/docs/llama-cpp.md +7 -7
- package/docs/models.md +11 -11
- package/docs/packages.md +46 -42
- package/docs/prompt-templates.md +3 -3
- package/docs/providers.md +10 -10
- package/docs/quickstart.md +49 -32
- package/docs/rpc.md +11 -11
- package/docs/sdk.md +4 -4
- package/docs/security.md +18 -18
- package/docs/session-format.md +9 -9
- package/docs/sessions.md +13 -13
- package/docs/settings.md +10 -14
- package/docs/shell-aliases.md +1 -1
- package/docs/skills.md +8 -8
- package/docs/terminal-setup.md +12 -12
- package/docs/termux.md +4 -4
- package/docs/themes.md +8 -8
- package/docs/tmux.md +4 -4
- package/docs/tui.md +3 -3
- package/docs/usage.md +54 -51
- package/docs/windows.md +1 -1
- package/npm-shrinkwrap.json +5 -5
- package/package.json +2 -3
- package/dist/core/telemetry.d.ts +0 -3
- package/dist/core/telemetry.d.ts.map +0 -1
- package/dist/core/telemetry.js +0 -9
- package/dist/core/telemetry.js.map +0 -1
- package/dist/utils/pi-user-agent.d.ts +0 -2
- package/dist/utils/pi-user-agent.d.ts.map +0 -1
- package/dist/utils/pi-user-agent.js +0 -5
- package/dist/utils/pi-user-agent.js.map +0 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-result eviction — Phase 3 task 3.1.
|
|
3
|
+
*
|
|
4
|
+
* Implements the eviction algorithm settled in `docs/architecture/contracts.md` § 2
|
|
5
|
+
* ("Context pipeline order — settled"): a pure, prefix-oldest, contiguous-run
|
|
6
|
+
* transform over the message list. It runs *before* compaction in the pipeline
|
|
7
|
+
* order (deferred-schema resolution → eviction → compaction), but this module knows
|
|
8
|
+
* nothing about that ordering or about `transformContext` — wiring is task 3.3.
|
|
9
|
+
*
|
|
10
|
+
* Pure by construction: no clock, no randomness, no I/O, no reads of the live tool
|
|
11
|
+
* registry. Every fact this function needs about a tool comes through the injected
|
|
12
|
+
* `contractLookup`, and every fact about "how big is this" comes from the message
|
|
13
|
+
* content already in hand.
|
|
14
|
+
*/
|
|
15
|
+
import type { AgentMessage } from "apex-code-agent-core";
|
|
16
|
+
import type { ToolContract } from "../tools/contract.ts";
|
|
17
|
+
/**
|
|
18
|
+
* What `evictToolResults` needs to know about a tool's contract to decide whether
|
|
19
|
+
* (and how) to evict one of its results. Only the `context` axis of `ToolContract`
|
|
20
|
+
* matters here — callers may pass a full contract, or (as tests do) a hand-built
|
|
21
|
+
* stand-in with just `context` filled in.
|
|
22
|
+
*/
|
|
23
|
+
export type ContractLookup = (toolName: string) => Pick<ToolContract, "context"> | undefined;
|
|
24
|
+
/** Marker substituted for an evicted result when its contract declares none. */
|
|
25
|
+
export declare const DEFAULT_EVICTION_MARKER = "[Tool result evicted to save context. Re-run the tool to see it again.]";
|
|
26
|
+
/**
|
|
27
|
+
* Replace the oldest contiguous run of recoverable tool results with markers, until
|
|
28
|
+
* the estimated total token cost of *all* tool-result messages in the list drops to
|
|
29
|
+
* or below `budget` (chars/4, the same heuristic `estimateTokens` uses elsewhere in
|
|
30
|
+
* this codebase — see `core/compaction/compaction.ts`). "Contiguous run from the
|
|
31
|
+
* oldest end" is enforced by walking messages in order and stopping the walk for
|
|
32
|
+
* good — not just skipping — the moment a tool result is encountered whose contract
|
|
33
|
+
* says it is not recoverable (or has no contract at all, matching the `UNCLASSIFIED`
|
|
34
|
+
* default of `resultRecoverable: false`): later eligible results are never reached,
|
|
35
|
+
* so no hole ever opens in the middle of the transcript. Non-tool-result messages
|
|
36
|
+
* (assistant/user turns) are passed through untouched and do not interrupt or count
|
|
37
|
+
* toward the walk.
|
|
38
|
+
*
|
|
39
|
+
* Deterministic and idempotent: a message whose content already exactly equals its
|
|
40
|
+
* tool's marker is recognized as already evicted and left alone, so re-running this
|
|
41
|
+
* function over its own output (or over a budget that is already satisfied) is a
|
|
42
|
+
* no-op. Never mutates `messages` or any message object — always returns a new
|
|
43
|
+
* array; unevicted messages keep their original object identity (unmutated sharing
|
|
44
|
+
* is not mutation), evicted ones are new objects with only `content` replaced.
|
|
45
|
+
*/
|
|
46
|
+
export declare function evictToolResults(messages: readonly AgentMessage[], contractLookup: ContractLookup, budget: number): AgentMessage[];
|
|
47
|
+
//# sourceMappingURL=eviction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eviction.d.ts","sourceRoot":"","sources":["../../../src/core/context/eviction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,GAAG,SAAS,CAAC;AAE7F,gFAAgF;AAChF,eAAO,MAAM,uBAAuB,4EAA4E,CAAC;AAiBjH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAC/B,QAAQ,EAAE,SAAS,YAAY,EAAE,EACjC,cAAc,EAAE,cAAc,EAC9B,MAAM,EAAE,MAAM,GACZ,YAAY,EAAE,CAsChB","sourcesContent":["/**\n * Tool-result eviction — Phase 3 task 3.1.\n *\n * Implements the eviction algorithm settled in `docs/architecture/contracts.md` § 2\n * (\"Context pipeline order — settled\"): a pure, prefix-oldest, contiguous-run\n * transform over the message list. It runs *before* compaction in the pipeline\n * order (deferred-schema resolution → eviction → compaction), but this module knows\n * nothing about that ordering or about `transformContext` — wiring is task 3.3.\n *\n * Pure by construction: no clock, no randomness, no I/O, no reads of the live tool\n * registry. Every fact this function needs about a tool comes through the injected\n * `contractLookup`, and every fact about \"how big is this\" comes from the message\n * content already in hand.\n */\n\nimport type { TextContent } from \"@earendil-works/pi-ai\";\nimport type { AgentMessage } from \"apex-code-agent-core\";\nimport { estimateTokens } from \"../compaction/compaction.ts\";\nimport type { ToolContract } from \"../tools/contract.ts\";\n\n/**\n * What `evictToolResults` needs to know about a tool's contract to decide whether\n * (and how) to evict one of its results. Only the `context` axis of `ToolContract`\n * matters here — callers may pass a full contract, or (as tests do) a hand-built\n * stand-in with just `context` filled in.\n */\nexport type ContractLookup = (toolName: string) => Pick<ToolContract, \"context\"> | undefined;\n\n/** Marker substituted for an evicted result when its contract declares none. */\nexport const DEFAULT_EVICTION_MARKER = \"[Tool result evicted to save context. Re-run the tool to see it again.]\";\n\nfunction isToolResultMessage(message: AgentMessage): message is Extract<AgentMessage, { role: \"toolResult\" }> {\n\treturn message.role === \"toolResult\";\n}\n\n/** True when `content` is already exactly the single-text-block marker form. */\nfunction isAlreadyEvicted(content: readonly unknown[], marker: string): boolean {\n\treturn (\n\t\tcontent.length === 1 &&\n\t\ttypeof content[0] === \"object\" &&\n\t\tcontent[0] !== null &&\n\t\t(content[0] as { type?: unknown }).type === \"text\" &&\n\t\t(content[0] as { text?: unknown }).text === marker\n\t);\n}\n\n/**\n * Replace the oldest contiguous run of recoverable tool results with markers, until\n * the estimated total token cost of *all* tool-result messages in the list drops to\n * or below `budget` (chars/4, the same heuristic `estimateTokens` uses elsewhere in\n * this codebase — see `core/compaction/compaction.ts`). \"Contiguous run from the\n * oldest end\" is enforced by walking messages in order and stopping the walk for\n * good — not just skipping — the moment a tool result is encountered whose contract\n * says it is not recoverable (or has no contract at all, matching the `UNCLASSIFIED`\n * default of `resultRecoverable: false`): later eligible results are never reached,\n * so no hole ever opens in the middle of the transcript. Non-tool-result messages\n * (assistant/user turns) are passed through untouched and do not interrupt or count\n * toward the walk.\n *\n * Deterministic and idempotent: a message whose content already exactly equals its\n * tool's marker is recognized as already evicted and left alone, so re-running this\n * function over its own output (or over a budget that is already satisfied) is a\n * no-op. Never mutates `messages` or any message object — always returns a new\n * array; unevicted messages keep their original object identity (unmutated sharing\n * is not mutation), evicted ones are new objects with only `content` replaced.\n */\nexport function evictToolResults(\n\tmessages: readonly AgentMessage[],\n\tcontractLookup: ContractLookup,\n\tbudget: number,\n): AgentMessage[] {\n\tlet remaining = 0;\n\tfor (const message of messages) {\n\t\tif (isToolResultMessage(message)) remaining += estimateTokens(message);\n\t}\n\n\tconst result: AgentMessage[] = [];\n\tlet stopped = false;\n\n\tfor (const message of messages) {\n\t\tif (stopped || !isToolResultMessage(message) || remaining <= budget) {\n\t\t\tresult.push(message);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst contract = contractLookup(message.toolName);\n\t\tconst recoverable = contract?.context.resultRecoverable === true;\n\t\tif (!recoverable) {\n\t\t\tstopped = true;\n\t\t\tresult.push(message);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst marker = contract.context.evictionMarker ?? DEFAULT_EVICTION_MARKER;\n\t\tif (isAlreadyEvicted(message.content, marker)) {\n\t\t\tresult.push(message);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst before = estimateTokens(message);\n\t\tconst markerContent: TextContent[] = [{ type: \"text\", text: marker }];\n\t\tconst evicted = { ...message, content: markerContent };\n\t\tconst after = estimateTokens(evicted);\n\t\tremaining = remaining - before + after;\n\t\tresult.push(evicted);\n\t}\n\n\treturn result;\n}\n"]}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-result eviction — Phase 3 task 3.1.
|
|
3
|
+
*
|
|
4
|
+
* Implements the eviction algorithm settled in `docs/architecture/contracts.md` § 2
|
|
5
|
+
* ("Context pipeline order — settled"): a pure, prefix-oldest, contiguous-run
|
|
6
|
+
* transform over the message list. It runs *before* compaction in the pipeline
|
|
7
|
+
* order (deferred-schema resolution → eviction → compaction), but this module knows
|
|
8
|
+
* nothing about that ordering or about `transformContext` — wiring is task 3.3.
|
|
9
|
+
*
|
|
10
|
+
* Pure by construction: no clock, no randomness, no I/O, no reads of the live tool
|
|
11
|
+
* registry. Every fact this function needs about a tool comes through the injected
|
|
12
|
+
* `contractLookup`, and every fact about "how big is this" comes from the message
|
|
13
|
+
* content already in hand.
|
|
14
|
+
*/
|
|
15
|
+
import { estimateTokens } from "../compaction/compaction.js";
|
|
16
|
+
/** Marker substituted for an evicted result when its contract declares none. */
|
|
17
|
+
export const DEFAULT_EVICTION_MARKER = "[Tool result evicted to save context. Re-run the tool to see it again.]";
|
|
18
|
+
function isToolResultMessage(message) {
|
|
19
|
+
return message.role === "toolResult";
|
|
20
|
+
}
|
|
21
|
+
/** True when `content` is already exactly the single-text-block marker form. */
|
|
22
|
+
function isAlreadyEvicted(content, marker) {
|
|
23
|
+
return (content.length === 1 &&
|
|
24
|
+
typeof content[0] === "object" &&
|
|
25
|
+
content[0] !== null &&
|
|
26
|
+
content[0].type === "text" &&
|
|
27
|
+
content[0].text === marker);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Replace the oldest contiguous run of recoverable tool results with markers, until
|
|
31
|
+
* the estimated total token cost of *all* tool-result messages in the list drops to
|
|
32
|
+
* or below `budget` (chars/4, the same heuristic `estimateTokens` uses elsewhere in
|
|
33
|
+
* this codebase — see `core/compaction/compaction.ts`). "Contiguous run from the
|
|
34
|
+
* oldest end" is enforced by walking messages in order and stopping the walk for
|
|
35
|
+
* good — not just skipping — the moment a tool result is encountered whose contract
|
|
36
|
+
* says it is not recoverable (or has no contract at all, matching the `UNCLASSIFIED`
|
|
37
|
+
* default of `resultRecoverable: false`): later eligible results are never reached,
|
|
38
|
+
* so no hole ever opens in the middle of the transcript. Non-tool-result messages
|
|
39
|
+
* (assistant/user turns) are passed through untouched and do not interrupt or count
|
|
40
|
+
* toward the walk.
|
|
41
|
+
*
|
|
42
|
+
* Deterministic and idempotent: a message whose content already exactly equals its
|
|
43
|
+
* tool's marker is recognized as already evicted and left alone, so re-running this
|
|
44
|
+
* function over its own output (or over a budget that is already satisfied) is a
|
|
45
|
+
* no-op. Never mutates `messages` or any message object — always returns a new
|
|
46
|
+
* array; unevicted messages keep their original object identity (unmutated sharing
|
|
47
|
+
* is not mutation), evicted ones are new objects with only `content` replaced.
|
|
48
|
+
*/
|
|
49
|
+
export function evictToolResults(messages, contractLookup, budget) {
|
|
50
|
+
let remaining = 0;
|
|
51
|
+
for (const message of messages) {
|
|
52
|
+
if (isToolResultMessage(message))
|
|
53
|
+
remaining += estimateTokens(message);
|
|
54
|
+
}
|
|
55
|
+
const result = [];
|
|
56
|
+
let stopped = false;
|
|
57
|
+
for (const message of messages) {
|
|
58
|
+
if (stopped || !isToolResultMessage(message) || remaining <= budget) {
|
|
59
|
+
result.push(message);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const contract = contractLookup(message.toolName);
|
|
63
|
+
const recoverable = contract?.context.resultRecoverable === true;
|
|
64
|
+
if (!recoverable) {
|
|
65
|
+
stopped = true;
|
|
66
|
+
result.push(message);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const marker = contract.context.evictionMarker ?? DEFAULT_EVICTION_MARKER;
|
|
70
|
+
if (isAlreadyEvicted(message.content, marker)) {
|
|
71
|
+
result.push(message);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const before = estimateTokens(message);
|
|
75
|
+
const markerContent = [{ type: "text", text: marker }];
|
|
76
|
+
const evicted = { ...message, content: markerContent };
|
|
77
|
+
const after = estimateTokens(evicted);
|
|
78
|
+
remaining = remaining - before + after;
|
|
79
|
+
result.push(evicted);
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=eviction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eviction.js","sourceRoot":"","sources":["../../../src/core/context/eviction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAW7D,gFAAgF;AAChF,MAAM,CAAC,MAAM,uBAAuB,GAAG,yEAAyE,CAAC;AAEjH,SAAS,mBAAmB,CAAC,OAAqB,EAA4D;IAC7G,OAAO,OAAO,CAAC,IAAI,KAAK,YAAY,CAAC;AAAA,CACrC;AAED,gFAAgF;AAChF,SAAS,gBAAgB,CAAC,OAA2B,EAAE,MAAc,EAAW;IAC/E,OAAO,CACN,OAAO,CAAC,MAAM,KAAK,CAAC;QACpB,OAAO,OAAO,CAAC,CAAC,CAAC,KAAK,QAAQ;QAC9B,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI;QAClB,OAAO,CAAC,CAAC,CAAwB,CAAC,IAAI,KAAK,MAAM;QACjD,OAAO,CAAC,CAAC,CAAwB,CAAC,IAAI,KAAK,MAAM,CAClD,CAAC;AAAA,CACF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,gBAAgB,CAC/B,QAAiC,EACjC,cAA8B,EAC9B,MAAc,EACG;IACjB,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAChC,IAAI,mBAAmB,CAAC,OAAO,CAAC;YAAE,SAAS,IAAI,cAAc,CAAC,OAAO,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,MAAM,GAAmB,EAAE,CAAC;IAClC,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAChC,IAAI,OAAO,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,IAAI,SAAS,IAAI,MAAM,EAAE,CAAC;YACrE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACrB,SAAS;QACV,CAAC;QAED,MAAM,QAAQ,GAAG,cAAc,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAClD,MAAM,WAAW,GAAG,QAAQ,EAAE,OAAO,CAAC,iBAAiB,KAAK,IAAI,CAAC;QACjE,IAAI,CAAC,WAAW,EAAE,CAAC;YAClB,OAAO,GAAG,IAAI,CAAC;YACf,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACrB,SAAS;QACV,CAAC;QAED,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,cAAc,IAAI,uBAAuB,CAAC;QAC1E,IAAI,gBAAgB,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC;YAC/C,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACrB,SAAS;QACV,CAAC;QAED,MAAM,MAAM,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;QACvC,MAAM,aAAa,GAAkB,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QACtE,MAAM,OAAO,GAAG,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC;QACvD,MAAM,KAAK,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;QACtC,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,KAAK,CAAC;QACvC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACtB,CAAC;IAED,OAAO,MAAM,CAAC;AAAA,CACd","sourcesContent":["/**\n * Tool-result eviction — Phase 3 task 3.1.\n *\n * Implements the eviction algorithm settled in `docs/architecture/contracts.md` § 2\n * (\"Context pipeline order — settled\"): a pure, prefix-oldest, contiguous-run\n * transform over the message list. It runs *before* compaction in the pipeline\n * order (deferred-schema resolution → eviction → compaction), but this module knows\n * nothing about that ordering or about `transformContext` — wiring is task 3.3.\n *\n * Pure by construction: no clock, no randomness, no I/O, no reads of the live tool\n * registry. Every fact this function needs about a tool comes through the injected\n * `contractLookup`, and every fact about \"how big is this\" comes from the message\n * content already in hand.\n */\n\nimport type { TextContent } from \"@earendil-works/pi-ai\";\nimport type { AgentMessage } from \"apex-code-agent-core\";\nimport { estimateTokens } from \"../compaction/compaction.ts\";\nimport type { ToolContract } from \"../tools/contract.ts\";\n\n/**\n * What `evictToolResults` needs to know about a tool's contract to decide whether\n * (and how) to evict one of its results. Only the `context` axis of `ToolContract`\n * matters here — callers may pass a full contract, or (as tests do) a hand-built\n * stand-in with just `context` filled in.\n */\nexport type ContractLookup = (toolName: string) => Pick<ToolContract, \"context\"> | undefined;\n\n/** Marker substituted for an evicted result when its contract declares none. */\nexport const DEFAULT_EVICTION_MARKER = \"[Tool result evicted to save context. Re-run the tool to see it again.]\";\n\nfunction isToolResultMessage(message: AgentMessage): message is Extract<AgentMessage, { role: \"toolResult\" }> {\n\treturn message.role === \"toolResult\";\n}\n\n/** True when `content` is already exactly the single-text-block marker form. */\nfunction isAlreadyEvicted(content: readonly unknown[], marker: string): boolean {\n\treturn (\n\t\tcontent.length === 1 &&\n\t\ttypeof content[0] === \"object\" &&\n\t\tcontent[0] !== null &&\n\t\t(content[0] as { type?: unknown }).type === \"text\" &&\n\t\t(content[0] as { text?: unknown }).text === marker\n\t);\n}\n\n/**\n * Replace the oldest contiguous run of recoverable tool results with markers, until\n * the estimated total token cost of *all* tool-result messages in the list drops to\n * or below `budget` (chars/4, the same heuristic `estimateTokens` uses elsewhere in\n * this codebase — see `core/compaction/compaction.ts`). \"Contiguous run from the\n * oldest end\" is enforced by walking messages in order and stopping the walk for\n * good — not just skipping — the moment a tool result is encountered whose contract\n * says it is not recoverable (or has no contract at all, matching the `UNCLASSIFIED`\n * default of `resultRecoverable: false`): later eligible results are never reached,\n * so no hole ever opens in the middle of the transcript. Non-tool-result messages\n * (assistant/user turns) are passed through untouched and do not interrupt or count\n * toward the walk.\n *\n * Deterministic and idempotent: a message whose content already exactly equals its\n * tool's marker is recognized as already evicted and left alone, so re-running this\n * function over its own output (or over a budget that is already satisfied) is a\n * no-op. Never mutates `messages` or any message object — always returns a new\n * array; unevicted messages keep their original object identity (unmutated sharing\n * is not mutation), evicted ones are new objects with only `content` replaced.\n */\nexport function evictToolResults(\n\tmessages: readonly AgentMessage[],\n\tcontractLookup: ContractLookup,\n\tbudget: number,\n): AgentMessage[] {\n\tlet remaining = 0;\n\tfor (const message of messages) {\n\t\tif (isToolResultMessage(message)) remaining += estimateTokens(message);\n\t}\n\n\tconst result: AgentMessage[] = [];\n\tlet stopped = false;\n\n\tfor (const message of messages) {\n\t\tif (stopped || !isToolResultMessage(message) || remaining <= budget) {\n\t\t\tresult.push(message);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst contract = contractLookup(message.toolName);\n\t\tconst recoverable = contract?.context.resultRecoverable === true;\n\t\tif (!recoverable) {\n\t\t\tstopped = true;\n\t\t\tresult.push(message);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst marker = contract.context.evictionMarker ?? DEFAULT_EVICTION_MARKER;\n\t\tif (isAlreadyEvicted(message.content, marker)) {\n\t\t\tresult.push(message);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst before = estimateTokens(message);\n\t\tconst markerContent: TextContent[] = [{ type: \"text\", text: marker }];\n\t\tconst evicted = { ...message, content: markerContent };\n\t\tconst after = estimateTokens(evicted);\n\t\tremaining = remaining - before + after;\n\t\tresult.push(evicted);\n\t}\n\n\treturn result;\n}\n"]}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context pipeline wiring — shared by `AgentSession` (production) and the offline
|
|
3
|
+
* replay harness (`src/testing/replay/runner.ts`), so there is exactly one
|
|
4
|
+
* implementation of "how deferred-schema resolution and eviction attach to an
|
|
5
|
+
* `Agent`" rather than two independently maintained copies.
|
|
6
|
+
*
|
|
7
|
+
* Implements the ordering settled in `docs/architecture/contracts.md` § 2:
|
|
8
|
+
* deferred-schema resolution → tool-result eviction → compaction. Compaction runs
|
|
9
|
+
* reactively from its own existing seam (`_checkCompaction` in `agent-session.ts`)
|
|
10
|
+
* and is untouched here; this module only adds the two stages that run *ahead of*
|
|
11
|
+
* every LLM request, so that reactive check is reached less often because the
|
|
12
|
+
* request it is reacting to is smaller.
|
|
13
|
+
*
|
|
14
|
+
* The two stages land in different seams because they operate on different halves
|
|
15
|
+
* of the outbound request. Eviction is a pure function of `AgentMessage[]`, which is
|
|
16
|
+
* exactly what `transformContext` sees, immediately before `convertToLlm` — so it is
|
|
17
|
+
* wired there. Deferred-schema resolution operates on the *tool list*, not messages;
|
|
18
|
+
* `transformContext`'s signature has no tools parameter, and `AgentContext.tools` is
|
|
19
|
+
* a snapshot taken once per prompt rather than rebuilt per request. The seam that
|
|
20
|
+
* actually sees the assembled outbound tool list on every request is
|
|
21
|
+
* `streamFunction` — it receives the full `Context`, tools included, immediately
|
|
22
|
+
* before the provider call — so the projection is applied there instead. Because the
|
|
23
|
+
* two stages act on disjoint fields of the request with no data dependency between
|
|
24
|
+
* them, this does not change the observable pipeline order.
|
|
25
|
+
*/
|
|
26
|
+
import type { Agent, StreamFn } from "apex-code-agent-core";
|
|
27
|
+
import { type ContractLookup } from "./eviction.ts";
|
|
28
|
+
/**
|
|
29
|
+
* Eviction-budget formula shared by every caller of `installContextPipeline`.
|
|
30
|
+
* Compaction fires once total context tokens exceed `contextWindow - reserveTokens`
|
|
31
|
+
* (`shouldCompact` in `core/compaction/compaction.ts`). Budgeting eviction — counted
|
|
32
|
+
* purely against tool-result tokens, per `evictToolResults`'s own contract — at half
|
|
33
|
+
* of that headroom leaves the other half free for system prompt and conversational
|
|
34
|
+
* growth, so eviction (cheap, structure-preserving) gets a chance to shrink the
|
|
35
|
+
* transcript before the expensive compaction stage is ever reached, matching
|
|
36
|
+
* contracts.md § 2's "why this order" (eviction before compaction, so compaction is
|
|
37
|
+
* reached later and less often).
|
|
38
|
+
*
|
|
39
|
+
* `Math.max(0, ...)` mirrors `shouldCompact`'s own behavior when `contextWindow` is
|
|
40
|
+
* unknown (0): evict aggressively rather than not at all, rather than inventing a
|
|
41
|
+
* different, unproven fallback.
|
|
42
|
+
*
|
|
43
|
+
* Production (`AgentSession`) calls this with the real model's `contextWindow` and
|
|
44
|
+
* the user's configured `reserveTokens`. The offline replay harness deliberately
|
|
45
|
+
* does *not* call this with the fixture model's `contextWindow` — see
|
|
46
|
+
* `REPLAY_EVICTION_BUDGET` in `src/testing/replay/runner.ts` for why a dedicated
|
|
47
|
+
* constant is used there instead.
|
|
48
|
+
*/
|
|
49
|
+
export declare function evictionBudget(contextWindow: number, reserveTokens: number): number;
|
|
50
|
+
/**
|
|
51
|
+
* Adapter between a caller's tool registry and `announceToolsByName`, which
|
|
52
|
+
* operates on a minimal `DeferrableTool` shape that has no notion of a tool
|
|
53
|
+
* registry. Only `parameters` is ever replaced; every other field of the outbound
|
|
54
|
+
* tool (name, description, and on `AgentTool` specifically, `execute`, `label`,
|
|
55
|
+
* etc.) passes through unchanged via the object spread.
|
|
56
|
+
*
|
|
57
|
+
* A tool with no entry in `contractLookup` (a foreign/unclassified tool) is treated
|
|
58
|
+
* as `deferSchema: false` here — the safe default on this axis is to keep
|
|
59
|
+
* describing it fully, the mirror image of eviction's "no contract => don't evict"
|
|
60
|
+
* default: when unsure, don't withhold information the model needs.
|
|
61
|
+
*/
|
|
62
|
+
export declare function projectToolSchemas<T extends {
|
|
63
|
+
name: string;
|
|
64
|
+
description: string;
|
|
65
|
+
parameters: unknown;
|
|
66
|
+
}>(tools: readonly T[], contractLookup: ContractLookup, loadedSchemaNames?: ReadonlySet<string>): T[];
|
|
67
|
+
/** Returns true for the built-in streamSimple function, including its context-pipeline wrapper. */
|
|
68
|
+
export declare function isDefaultStreamFunction(streamFunction: StreamFn): boolean;
|
|
69
|
+
export interface ContextPipelineOptions {
|
|
70
|
+
/** Resolves a tool's contract; shared by both eviction and schema projection. */
|
|
71
|
+
contractLookup: ContractLookup;
|
|
72
|
+
/** Names whose real schemas have been loaded by the model in this session. */
|
|
73
|
+
loadedSchemaNames?: ReadonlySet<string>;
|
|
74
|
+
/**
|
|
75
|
+
* Eviction budget as a thunk, not a plain number: `AgentSession`'s budget
|
|
76
|
+
* depends on `this.model` and `this.settingsManager`, both of which can change
|
|
77
|
+
* over the session's lifetime (model switch, settings edit), and is never
|
|
78
|
+
* cached. Evaluating it fresh on every request is what keeps eviction correct
|
|
79
|
+
* after either changes mid-session.
|
|
80
|
+
*/
|
|
81
|
+
evictionBudget: () => number;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Installs the settled context pipeline (deferred-schema resolution → eviction)
|
|
85
|
+
* onto `agent`, chaining onto whatever `transformContext` / `streamFunction` the
|
|
86
|
+
* caller already installed rather than replacing it — the same pattern
|
|
87
|
+
* `AgentSession._installAgentNextTurnRefresh` uses for `prepareNextTurnWithContext`.
|
|
88
|
+
*/
|
|
89
|
+
export declare function installContextPipeline(agent: Agent, options: ContextPipelineOptions): void;
|
|
90
|
+
//# sourceMappingURL=pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../../src/core/context/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAG5D,OAAO,EAAE,KAAK,cAAc,EAAoB,MAAM,eAAe,CAAC;AAEtE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,EACtG,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,cAAc,EAAE,cAAc,EAC9B,iBAAiB,GAAE,WAAW,CAAC,MAAM,CAAa,GAChD,CAAC,EAAE,CAcL;AAID,mGAAmG;AACnG,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,QAAQ,GAAG,OAAO,CAEzE;AAED,MAAM,WAAW,sBAAsB;IACtC,iFAAiF;IACjF,cAAc,EAAE,cAAc,CAAC;IAC/B,8EAA8E;IAC9E,iBAAiB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;;OAMG;IACH,cAAc,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,sBAAsB,GAAG,IAAI,CA0B1F","sourcesContent":["/**\n * Context pipeline wiring — shared by `AgentSession` (production) and the offline\n * replay harness (`src/testing/replay/runner.ts`), so there is exactly one\n * implementation of \"how deferred-schema resolution and eviction attach to an\n * `Agent`\" rather than two independently maintained copies.\n *\n * Implements the ordering settled in `docs/architecture/contracts.md` § 2:\n * deferred-schema resolution → tool-result eviction → compaction. Compaction runs\n * reactively from its own existing seam (`_checkCompaction` in `agent-session.ts`)\n * and is untouched here; this module only adds the two stages that run *ahead of*\n * every LLM request, so that reactive check is reached less often because the\n * request it is reacting to is smaller.\n *\n * The two stages land in different seams because they operate on different halves\n * of the outbound request. Eviction is a pure function of `AgentMessage[]`, which is\n * exactly what `transformContext` sees, immediately before `convertToLlm` — so it is\n * wired there. Deferred-schema resolution operates on the *tool list*, not messages;\n * `transformContext`'s signature has no tools parameter, and `AgentContext.tools` is\n * a snapshot taken once per prompt rather than rebuilt per request. The seam that\n * actually sees the assembled outbound tool list on every request is\n * `streamFunction` — it receives the full `Context`, tools included, immediately\n * before the provider call — so the projection is applied there instead. Because the\n * two stages act on disjoint fields of the request with no data dependency between\n * them, this does not change the observable pipeline order.\n */\n\nimport { streamSimple } from \"@earendil-works/pi-ai/compat\";\nimport type { Agent, StreamFn } from \"apex-code-agent-core\";\nimport { resolveToolContext } from \"../tools/contract.ts\";\nimport { announceToolsByName, type DeferrableTool } from \"./deferred-schemas.ts\";\nimport { type ContractLookup, evictToolResults } from \"./eviction.ts\";\n\n/**\n * Eviction-budget formula shared by every caller of `installContextPipeline`.\n * Compaction fires once total context tokens exceed `contextWindow - reserveTokens`\n * (`shouldCompact` in `core/compaction/compaction.ts`). Budgeting eviction — counted\n * purely against tool-result tokens, per `evictToolResults`'s own contract — at half\n * of that headroom leaves the other half free for system prompt and conversational\n * growth, so eviction (cheap, structure-preserving) gets a chance to shrink the\n * transcript before the expensive compaction stage is ever reached, matching\n * contracts.md § 2's \"why this order\" (eviction before compaction, so compaction is\n * reached later and less often).\n *\n * `Math.max(0, ...)` mirrors `shouldCompact`'s own behavior when `contextWindow` is\n * unknown (0): evict aggressively rather than not at all, rather than inventing a\n * different, unproven fallback.\n *\n * Production (`AgentSession`) calls this with the real model's `contextWindow` and\n * the user's configured `reserveTokens`. The offline replay harness deliberately\n * does *not* call this with the fixture model's `contextWindow` — see\n * `REPLAY_EVICTION_BUDGET` in `src/testing/replay/runner.ts` for why a dedicated\n * constant is used there instead.\n */\nexport function evictionBudget(contextWindow: number, reserveTokens: number): number {\n\treturn Math.max(0, Math.floor((contextWindow - reserveTokens) / 2));\n}\n\n/**\n * Adapter between a caller's tool registry and `announceToolsByName`, which\n * operates on a minimal `DeferrableTool` shape that has no notion of a tool\n * registry. Only `parameters` is ever replaced; every other field of the outbound\n * tool (name, description, and on `AgentTool` specifically, `execute`, `label`,\n * etc.) passes through unchanged via the object spread.\n *\n * A tool with no entry in `contractLookup` (a foreign/unclassified tool) is treated\n * as `deferSchema: false` here — the safe default on this axis is to keep\n * describing it fully, the mirror image of eviction's \"no contract => don't evict\"\n * default: when unsure, don't withhold information the model needs.\n */\nexport function projectToolSchemas<T extends { name: string; description: string; parameters: unknown }>(\n\ttools: readonly T[],\n\tcontractLookup: ContractLookup,\n\tloadedSchemaNames: ReadonlySet<string> = new Set(),\n): T[] {\n\tconst deferrable: DeferrableTool[] = tools.map((tool) => ({\n\t\tname: tool.name,\n\t\tdescription: tool.description,\n\t\tparameters: tool.parameters,\n\t\tcontract: {\n\t\t\tcontext: {\n\t\t\t\tdeferSchema:\n\t\t\t\t\t!loadedSchemaNames.has(tool.name) && resolveToolContext(contractLookup, tool.name).context.deferSchema,\n\t\t\t},\n\t\t},\n\t}));\n\tconst announced = announceToolsByName(deferrable);\n\treturn tools.map((tool, index) => ({ ...tool, parameters: announced[index].parameters }));\n}\n\nconst defaultStreamFunctions = new WeakSet<StreamFn>();\n\n/** Returns true for the built-in streamSimple function, including its context-pipeline wrapper. */\nexport function isDefaultStreamFunction(streamFunction: StreamFn): boolean {\n\treturn streamFunction === streamSimple || defaultStreamFunctions.has(streamFunction);\n}\n\nexport interface ContextPipelineOptions {\n\t/** Resolves a tool's contract; shared by both eviction and schema projection. */\n\tcontractLookup: ContractLookup;\n\t/** Names whose real schemas have been loaded by the model in this session. */\n\tloadedSchemaNames?: ReadonlySet<string>;\n\t/**\n\t * Eviction budget as a thunk, not a plain number: `AgentSession`'s budget\n\t * depends on `this.model` and `this.settingsManager`, both of which can change\n\t * over the session's lifetime (model switch, settings edit), and is never\n\t * cached. Evaluating it fresh on every request is what keeps eviction correct\n\t * after either changes mid-session.\n\t */\n\tevictionBudget: () => number;\n}\n\n/**\n * Installs the settled context pipeline (deferred-schema resolution → eviction)\n * onto `agent`, chaining onto whatever `transformContext` / `streamFunction` the\n * caller already installed rather than replacing it — the same pattern\n * `AgentSession._installAgentNextTurnRefresh` uses for `prepareNextTurnWithContext`.\n */\nexport function installContextPipeline(agent: Agent, options: ContextPipelineOptions): void {\n\tconst { contractLookup, evictionBudget: getBudget, loadedSchemaNames = new Set<string>() } = options;\n\n\tconst previousTransformContext = agent.transformContext;\n\tagent.transformContext = async (messages, signal) => {\n\t\tconst afterPrevious = previousTransformContext ? await previousTransformContext(messages, signal) : messages;\n\t\treturn evictToolResults(afterPrevious, contractLookup, getBudget());\n\t};\n\n\tconst previousStreamFunction = agent.streamFunction;\n\tconst wrappedStreamFunction = (\n\t\tmodel: Parameters<typeof previousStreamFunction>[0],\n\t\tcontext: Parameters<typeof previousStreamFunction>[1],\n\t\tstreamOptions: Parameters<typeof previousStreamFunction>[2],\n\t) => {\n\t\tif (!context.tools || context.tools.length === 0) {\n\t\t\treturn previousStreamFunction(model, context, streamOptions);\n\t\t}\n\t\treturn previousStreamFunction(\n\t\t\tmodel,\n\t\t\t{ ...context, tools: projectToolSchemas(context.tools, contractLookup, loadedSchemaNames) },\n\t\t\tstreamOptions,\n\t\t);\n\t};\n\tif (isDefaultStreamFunction(previousStreamFunction)) defaultStreamFunctions.add(wrappedStreamFunction);\n\tagent.streamFunction = wrappedStreamFunction;\n}\n"]}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context pipeline wiring — shared by `AgentSession` (production) and the offline
|
|
3
|
+
* replay harness (`src/testing/replay/runner.ts`), so there is exactly one
|
|
4
|
+
* implementation of "how deferred-schema resolution and eviction attach to an
|
|
5
|
+
* `Agent`" rather than two independently maintained copies.
|
|
6
|
+
*
|
|
7
|
+
* Implements the ordering settled in `docs/architecture/contracts.md` § 2:
|
|
8
|
+
* deferred-schema resolution → tool-result eviction → compaction. Compaction runs
|
|
9
|
+
* reactively from its own existing seam (`_checkCompaction` in `agent-session.ts`)
|
|
10
|
+
* and is untouched here; this module only adds the two stages that run *ahead of*
|
|
11
|
+
* every LLM request, so that reactive check is reached less often because the
|
|
12
|
+
* request it is reacting to is smaller.
|
|
13
|
+
*
|
|
14
|
+
* The two stages land in different seams because they operate on different halves
|
|
15
|
+
* of the outbound request. Eviction is a pure function of `AgentMessage[]`, which is
|
|
16
|
+
* exactly what `transformContext` sees, immediately before `convertToLlm` — so it is
|
|
17
|
+
* wired there. Deferred-schema resolution operates on the *tool list*, not messages;
|
|
18
|
+
* `transformContext`'s signature has no tools parameter, and `AgentContext.tools` is
|
|
19
|
+
* a snapshot taken once per prompt rather than rebuilt per request. The seam that
|
|
20
|
+
* actually sees the assembled outbound tool list on every request is
|
|
21
|
+
* `streamFunction` — it receives the full `Context`, tools included, immediately
|
|
22
|
+
* before the provider call — so the projection is applied there instead. Because the
|
|
23
|
+
* two stages act on disjoint fields of the request with no data dependency between
|
|
24
|
+
* them, this does not change the observable pipeline order.
|
|
25
|
+
*/
|
|
26
|
+
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
27
|
+
import { resolveToolContext } from "../tools/contract.js";
|
|
28
|
+
import { announceToolsByName } from "./deferred-schemas.js";
|
|
29
|
+
import { evictToolResults } from "./eviction.js";
|
|
30
|
+
/**
|
|
31
|
+
* Eviction-budget formula shared by every caller of `installContextPipeline`.
|
|
32
|
+
* Compaction fires once total context tokens exceed `contextWindow - reserveTokens`
|
|
33
|
+
* (`shouldCompact` in `core/compaction/compaction.ts`). Budgeting eviction — counted
|
|
34
|
+
* purely against tool-result tokens, per `evictToolResults`'s own contract — at half
|
|
35
|
+
* of that headroom leaves the other half free for system prompt and conversational
|
|
36
|
+
* growth, so eviction (cheap, structure-preserving) gets a chance to shrink the
|
|
37
|
+
* transcript before the expensive compaction stage is ever reached, matching
|
|
38
|
+
* contracts.md § 2's "why this order" (eviction before compaction, so compaction is
|
|
39
|
+
* reached later and less often).
|
|
40
|
+
*
|
|
41
|
+
* `Math.max(0, ...)` mirrors `shouldCompact`'s own behavior when `contextWindow` is
|
|
42
|
+
* unknown (0): evict aggressively rather than not at all, rather than inventing a
|
|
43
|
+
* different, unproven fallback.
|
|
44
|
+
*
|
|
45
|
+
* Production (`AgentSession`) calls this with the real model's `contextWindow` and
|
|
46
|
+
* the user's configured `reserveTokens`. The offline replay harness deliberately
|
|
47
|
+
* does *not* call this with the fixture model's `contextWindow` — see
|
|
48
|
+
* `REPLAY_EVICTION_BUDGET` in `src/testing/replay/runner.ts` for why a dedicated
|
|
49
|
+
* constant is used there instead.
|
|
50
|
+
*/
|
|
51
|
+
export function evictionBudget(contextWindow, reserveTokens) {
|
|
52
|
+
return Math.max(0, Math.floor((contextWindow - reserveTokens) / 2));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Adapter between a caller's tool registry and `announceToolsByName`, which
|
|
56
|
+
* operates on a minimal `DeferrableTool` shape that has no notion of a tool
|
|
57
|
+
* registry. Only `parameters` is ever replaced; every other field of the outbound
|
|
58
|
+
* tool (name, description, and on `AgentTool` specifically, `execute`, `label`,
|
|
59
|
+
* etc.) passes through unchanged via the object spread.
|
|
60
|
+
*
|
|
61
|
+
* A tool with no entry in `contractLookup` (a foreign/unclassified tool) is treated
|
|
62
|
+
* as `deferSchema: false` here — the safe default on this axis is to keep
|
|
63
|
+
* describing it fully, the mirror image of eviction's "no contract => don't evict"
|
|
64
|
+
* default: when unsure, don't withhold information the model needs.
|
|
65
|
+
*/
|
|
66
|
+
export function projectToolSchemas(tools, contractLookup, loadedSchemaNames = new Set()) {
|
|
67
|
+
const deferrable = tools.map((tool) => ({
|
|
68
|
+
name: tool.name,
|
|
69
|
+
description: tool.description,
|
|
70
|
+
parameters: tool.parameters,
|
|
71
|
+
contract: {
|
|
72
|
+
context: {
|
|
73
|
+
deferSchema: !loadedSchemaNames.has(tool.name) && resolveToolContext(contractLookup, tool.name).context.deferSchema,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
}));
|
|
77
|
+
const announced = announceToolsByName(deferrable);
|
|
78
|
+
return tools.map((tool, index) => ({ ...tool, parameters: announced[index].parameters }));
|
|
79
|
+
}
|
|
80
|
+
const defaultStreamFunctions = new WeakSet();
|
|
81
|
+
/** Returns true for the built-in streamSimple function, including its context-pipeline wrapper. */
|
|
82
|
+
export function isDefaultStreamFunction(streamFunction) {
|
|
83
|
+
return streamFunction === streamSimple || defaultStreamFunctions.has(streamFunction);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Installs the settled context pipeline (deferred-schema resolution → eviction)
|
|
87
|
+
* onto `agent`, chaining onto whatever `transformContext` / `streamFunction` the
|
|
88
|
+
* caller already installed rather than replacing it — the same pattern
|
|
89
|
+
* `AgentSession._installAgentNextTurnRefresh` uses for `prepareNextTurnWithContext`.
|
|
90
|
+
*/
|
|
91
|
+
export function installContextPipeline(agent, options) {
|
|
92
|
+
const { contractLookup, evictionBudget: getBudget, loadedSchemaNames = new Set() } = options;
|
|
93
|
+
const previousTransformContext = agent.transformContext;
|
|
94
|
+
agent.transformContext = async (messages, signal) => {
|
|
95
|
+
const afterPrevious = previousTransformContext ? await previousTransformContext(messages, signal) : messages;
|
|
96
|
+
return evictToolResults(afterPrevious, contractLookup, getBudget());
|
|
97
|
+
};
|
|
98
|
+
const previousStreamFunction = agent.streamFunction;
|
|
99
|
+
const wrappedStreamFunction = (model, context, streamOptions) => {
|
|
100
|
+
if (!context.tools || context.tools.length === 0) {
|
|
101
|
+
return previousStreamFunction(model, context, streamOptions);
|
|
102
|
+
}
|
|
103
|
+
return previousStreamFunction(model, { ...context, tools: projectToolSchemas(context.tools, contractLookup, loadedSchemaNames) }, streamOptions);
|
|
104
|
+
};
|
|
105
|
+
if (isDefaultStreamFunction(previousStreamFunction))
|
|
106
|
+
defaultStreamFunctions.add(wrappedStreamFunction);
|
|
107
|
+
agent.streamFunction = wrappedStreamFunction;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=pipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../../../src/core/context/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAE5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAuB,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAuB,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,cAAc,CAAC,aAAqB,EAAE,aAAqB,EAAU;IACpF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,aAAa,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAAA,CACpE;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CACjC,KAAmB,EACnB,cAA8B,EAC9B,iBAAiB,GAAwB,IAAI,GAAG,EAAE,EAC5C;IACN,MAAM,UAAU,GAAqB,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACzD,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,QAAQ,EAAE;YACT,OAAO,EAAE;gBACR,WAAW,EACV,CAAC,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,kBAAkB,CAAC,cAAc,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,WAAW;aACvG;SACD;KACD,CAAC,CAAC,CAAC;IACJ,MAAM,SAAS,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;IAClD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;AAAA,CAC1F;AAED,MAAM,sBAAsB,GAAG,IAAI,OAAO,EAAY,CAAC;AAEvD,mGAAmG;AACnG,MAAM,UAAU,uBAAuB,CAAC,cAAwB,EAAW;IAC1E,OAAO,cAAc,KAAK,YAAY,IAAI,sBAAsB,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;AAAA,CACrF;AAiBD;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAY,EAAE,OAA+B,EAAQ;IAC3F,MAAM,EAAE,cAAc,EAAE,cAAc,EAAE,SAAS,EAAE,iBAAiB,GAAG,IAAI,GAAG,EAAU,EAAE,GAAG,OAAO,CAAC;IAErG,MAAM,wBAAwB,GAAG,KAAK,CAAC,gBAAgB,CAAC;IACxD,KAAK,CAAC,gBAAgB,GAAG,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC;QACpD,MAAM,aAAa,GAAG,wBAAwB,CAAC,CAAC,CAAC,MAAM,wBAAwB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QAC7G,OAAO,gBAAgB,CAAC,aAAa,EAAE,cAAc,EAAE,SAAS,EAAE,CAAC,CAAC;IAAA,CACpE,CAAC;IAEF,MAAM,sBAAsB,GAAG,KAAK,CAAC,cAAc,CAAC;IACpD,MAAM,qBAAqB,GAAG,CAC7B,KAAmD,EACnD,OAAqD,EACrD,aAA2D,EAC1D,EAAE,CAAC;QACJ,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAClD,OAAO,sBAAsB,CAAC,KAAK,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC;QAC9D,CAAC;QACD,OAAO,sBAAsB,CAC5B,KAAK,EACL,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,kBAAkB,CAAC,OAAO,CAAC,KAAK,EAAE,cAAc,EAAE,iBAAiB,CAAC,EAAE,EAC3F,aAAa,CACb,CAAC;IAAA,CACF,CAAC;IACF,IAAI,uBAAuB,CAAC,sBAAsB,CAAC;QAAE,sBAAsB,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;IACvG,KAAK,CAAC,cAAc,GAAG,qBAAqB,CAAC;AAAA,CAC7C","sourcesContent":["/**\n * Context pipeline wiring — shared by `AgentSession` (production) and the offline\n * replay harness (`src/testing/replay/runner.ts`), so there is exactly one\n * implementation of \"how deferred-schema resolution and eviction attach to an\n * `Agent`\" rather than two independently maintained copies.\n *\n * Implements the ordering settled in `docs/architecture/contracts.md` § 2:\n * deferred-schema resolution → tool-result eviction → compaction. Compaction runs\n * reactively from its own existing seam (`_checkCompaction` in `agent-session.ts`)\n * and is untouched here; this module only adds the two stages that run *ahead of*\n * every LLM request, so that reactive check is reached less often because the\n * request it is reacting to is smaller.\n *\n * The two stages land in different seams because they operate on different halves\n * of the outbound request. Eviction is a pure function of `AgentMessage[]`, which is\n * exactly what `transformContext` sees, immediately before `convertToLlm` — so it is\n * wired there. Deferred-schema resolution operates on the *tool list*, not messages;\n * `transformContext`'s signature has no tools parameter, and `AgentContext.tools` is\n * a snapshot taken once per prompt rather than rebuilt per request. The seam that\n * actually sees the assembled outbound tool list on every request is\n * `streamFunction` — it receives the full `Context`, tools included, immediately\n * before the provider call — so the projection is applied there instead. Because the\n * two stages act on disjoint fields of the request with no data dependency between\n * them, this does not change the observable pipeline order.\n */\n\nimport { streamSimple } from \"@earendil-works/pi-ai/compat\";\nimport type { Agent, StreamFn } from \"apex-code-agent-core\";\nimport { resolveToolContext } from \"../tools/contract.ts\";\nimport { announceToolsByName, type DeferrableTool } from \"./deferred-schemas.ts\";\nimport { type ContractLookup, evictToolResults } from \"./eviction.ts\";\n\n/**\n * Eviction-budget formula shared by every caller of `installContextPipeline`.\n * Compaction fires once total context tokens exceed `contextWindow - reserveTokens`\n * (`shouldCompact` in `core/compaction/compaction.ts`). Budgeting eviction — counted\n * purely against tool-result tokens, per `evictToolResults`'s own contract — at half\n * of that headroom leaves the other half free for system prompt and conversational\n * growth, so eviction (cheap, structure-preserving) gets a chance to shrink the\n * transcript before the expensive compaction stage is ever reached, matching\n * contracts.md § 2's \"why this order\" (eviction before compaction, so compaction is\n * reached later and less often).\n *\n * `Math.max(0, ...)` mirrors `shouldCompact`'s own behavior when `contextWindow` is\n * unknown (0): evict aggressively rather than not at all, rather than inventing a\n * different, unproven fallback.\n *\n * Production (`AgentSession`) calls this with the real model's `contextWindow` and\n * the user's configured `reserveTokens`. The offline replay harness deliberately\n * does *not* call this with the fixture model's `contextWindow` — see\n * `REPLAY_EVICTION_BUDGET` in `src/testing/replay/runner.ts` for why a dedicated\n * constant is used there instead.\n */\nexport function evictionBudget(contextWindow: number, reserveTokens: number): number {\n\treturn Math.max(0, Math.floor((contextWindow - reserveTokens) / 2));\n}\n\n/**\n * Adapter between a caller's tool registry and `announceToolsByName`, which\n * operates on a minimal `DeferrableTool` shape that has no notion of a tool\n * registry. Only `parameters` is ever replaced; every other field of the outbound\n * tool (name, description, and on `AgentTool` specifically, `execute`, `label`,\n * etc.) passes through unchanged via the object spread.\n *\n * A tool with no entry in `contractLookup` (a foreign/unclassified tool) is treated\n * as `deferSchema: false` here — the safe default on this axis is to keep\n * describing it fully, the mirror image of eviction's \"no contract => don't evict\"\n * default: when unsure, don't withhold information the model needs.\n */\nexport function projectToolSchemas<T extends { name: string; description: string; parameters: unknown }>(\n\ttools: readonly T[],\n\tcontractLookup: ContractLookup,\n\tloadedSchemaNames: ReadonlySet<string> = new Set(),\n): T[] {\n\tconst deferrable: DeferrableTool[] = tools.map((tool) => ({\n\t\tname: tool.name,\n\t\tdescription: tool.description,\n\t\tparameters: tool.parameters,\n\t\tcontract: {\n\t\t\tcontext: {\n\t\t\t\tdeferSchema:\n\t\t\t\t\t!loadedSchemaNames.has(tool.name) && resolveToolContext(contractLookup, tool.name).context.deferSchema,\n\t\t\t},\n\t\t},\n\t}));\n\tconst announced = announceToolsByName(deferrable);\n\treturn tools.map((tool, index) => ({ ...tool, parameters: announced[index].parameters }));\n}\n\nconst defaultStreamFunctions = new WeakSet<StreamFn>();\n\n/** Returns true for the built-in streamSimple function, including its context-pipeline wrapper. */\nexport function isDefaultStreamFunction(streamFunction: StreamFn): boolean {\n\treturn streamFunction === streamSimple || defaultStreamFunctions.has(streamFunction);\n}\n\nexport interface ContextPipelineOptions {\n\t/** Resolves a tool's contract; shared by both eviction and schema projection. */\n\tcontractLookup: ContractLookup;\n\t/** Names whose real schemas have been loaded by the model in this session. */\n\tloadedSchemaNames?: ReadonlySet<string>;\n\t/**\n\t * Eviction budget as a thunk, not a plain number: `AgentSession`'s budget\n\t * depends on `this.model` and `this.settingsManager`, both of which can change\n\t * over the session's lifetime (model switch, settings edit), and is never\n\t * cached. Evaluating it fresh on every request is what keeps eviction correct\n\t * after either changes mid-session.\n\t */\n\tevictionBudget: () => number;\n}\n\n/**\n * Installs the settled context pipeline (deferred-schema resolution → eviction)\n * onto `agent`, chaining onto whatever `transformContext` / `streamFunction` the\n * caller already installed rather than replacing it — the same pattern\n * `AgentSession._installAgentNextTurnRefresh` uses for `prepareNextTurnWithContext`.\n */\nexport function installContextPipeline(agent: Agent, options: ContextPipelineOptions): void {\n\tconst { contractLookup, evictionBudget: getBudget, loadedSchemaNames = new Set<string>() } = options;\n\n\tconst previousTransformContext = agent.transformContext;\n\tagent.transformContext = async (messages, signal) => {\n\t\tconst afterPrevious = previousTransformContext ? await previousTransformContext(messages, signal) : messages;\n\t\treturn evictToolResults(afterPrevious, contractLookup, getBudget());\n\t};\n\n\tconst previousStreamFunction = agent.streamFunction;\n\tconst wrappedStreamFunction = (\n\t\tmodel: Parameters<typeof previousStreamFunction>[0],\n\t\tcontext: Parameters<typeof previousStreamFunction>[1],\n\t\tstreamOptions: Parameters<typeof previousStreamFunction>[2],\n\t) => {\n\t\tif (!context.tools || context.tools.length === 0) {\n\t\t\treturn previousStreamFunction(model, context, streamOptions);\n\t\t}\n\t\treturn previousStreamFunction(\n\t\t\tmodel,\n\t\t\t{ ...context, tools: projectToolSchemas(context.tools, contractLookup, loadedSchemaNames) },\n\t\t\tstreamOptions,\n\t\t);\n\t};\n\tif (isDefaultStreamFunction(previousStreamFunction)) defaultStreamFunctions.add(wrappedStreamFunction);\n\tagent.streamFunction = wrappedStreamFunction;\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for bounded, pre-completion credential failover around
|
|
3
|
+
* ModelRuntime.streamSimple(). Classification and stream buffering/replay only;
|
|
4
|
+
* pool selection state lives in CredentialPool, request orchestration in ModelRuntime.
|
|
5
|
+
*/
|
|
6
|
+
import type { AssistantMessage, AssistantMessageEvent } from "@earendil-works/pi-ai";
|
|
7
|
+
import type { CredentialFailureKind } from "./credential-pool.ts";
|
|
8
|
+
/**
|
|
9
|
+
* Classifies a failed AssistantMessage into a rotation-eligible CredentialFailureKind,
|
|
10
|
+
* or undefined when the failure should not trigger credential rotation (success,
|
|
11
|
+
* abort, or a non-retryable provider error such as quota exhaustion).
|
|
12
|
+
*/
|
|
13
|
+
export declare function classifyCredentialFailure(message: AssistantMessage): CredentialFailureKind | undefined;
|
|
14
|
+
/** An AsyncIterable<AssistantMessageEvent> paired with its eventual AssistantMessage, matching the shape `lazyStream` forwards. */
|
|
15
|
+
export interface ResultStream extends AsyncIterable<AssistantMessageEvent> {
|
|
16
|
+
result(): Promise<AssistantMessage>;
|
|
17
|
+
}
|
|
18
|
+
export interface DrainedAttempt {
|
|
19
|
+
events: readonly AssistantMessageEvent[];
|
|
20
|
+
message: AssistantMessage;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Fully consumes one attempt's stream before any event reaches the caller. This is
|
|
24
|
+
* what makes a failed attempt invisible to the caller: nothing is forwarded until the
|
|
25
|
+
* attempt's outcome (and therefore its retry eligibility) is known.
|
|
26
|
+
*/
|
|
27
|
+
export declare function drainAttempt(stream: ResultStream): Promise<DrainedAttempt>;
|
|
28
|
+
/** Replays a fully-drained attempt's events as a fresh stream terminating in its known message. */
|
|
29
|
+
export declare function replayAttempt({ events, message }: DrainedAttempt): ResultStream;
|
|
30
|
+
//# sourceMappingURL=credential-failover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-failover.d.ts","sourceRoot":"","sources":["../../src/core/credential-failover.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAErF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAMlE;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,GAAG,qBAAqB,GAAG,SAAS,CAOtG;AAED,mIAAmI;AACnI,MAAM,WAAW,YAAa,SAAQ,aAAa,CAAC,qBAAqB,CAAC;IACzE,MAAM,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,cAAc;IAC9B,MAAM,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACzC,OAAO,EAAE,gBAAgB,CAAC;CAC1B;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC,CAIhF;AAED,mGAAmG;AACnG,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,cAAc,GAAG,YAAY,CAO/E","sourcesContent":["/**\n * Pure helpers for bounded, pre-completion credential failover around\n * ModelRuntime.streamSimple(). Classification and stream buffering/replay only;\n * pool selection state lives in CredentialPool, request orchestration in ModelRuntime.\n */\n\nimport type { AssistantMessage, AssistantMessageEvent } from \"@earendil-works/pi-ai\";\nimport { isRetryableAssistantError } from \"@earendil-works/pi-ai\";\nimport type { CredentialFailureKind } from \"./credential-pool.ts\";\n\nconst RATE_LIMIT_PATTERN = /\\b429\\b|rate.?limit|too many requests/i;\nconst BLOCKED_CREDENTIAL_PATTERN =\n\t/\\b401\\b|\\b403\\b|unauthorized|forbidden|invalid.?api.?key|permission denied|expired/i;\n\n/**\n * Classifies a failed AssistantMessage into a rotation-eligible CredentialFailureKind,\n * or undefined when the failure should not trigger credential rotation (success,\n * abort, or a non-retryable provider error such as quota exhaustion).\n */\nexport function classifyCredentialFailure(message: AssistantMessage): CredentialFailureKind | undefined {\n\tif (message.stopReason !== \"error\" || !message.errorMessage) return undefined;\n\tconst text = message.errorMessage;\n\tif (RATE_LIMIT_PATTERN.test(text)) return \"rate_limited\";\n\tif (BLOCKED_CREDENTIAL_PATTERN.test(text)) return \"blocked\";\n\tif (isRetryableAssistantError(message)) return \"temporary\";\n\treturn undefined;\n}\n\n/** An AsyncIterable<AssistantMessageEvent> paired with its eventual AssistantMessage, matching the shape `lazyStream` forwards. */\nexport interface ResultStream extends AsyncIterable<AssistantMessageEvent> {\n\tresult(): Promise<AssistantMessage>;\n}\n\nexport interface DrainedAttempt {\n\tevents: readonly AssistantMessageEvent[];\n\tmessage: AssistantMessage;\n}\n\n/**\n * Fully consumes one attempt's stream before any event reaches the caller. This is\n * what makes a failed attempt invisible to the caller: nothing is forwarded until the\n * attempt's outcome (and therefore its retry eligibility) is known.\n */\nexport async function drainAttempt(stream: ResultStream): Promise<DrainedAttempt> {\n\tconst events: AssistantMessageEvent[] = [];\n\tfor await (const event of stream) events.push(event);\n\treturn { events, message: await stream.result() };\n}\n\n/** Replays a fully-drained attempt's events as a fresh stream terminating in its known message. */\nexport function replayAttempt({ events, message }: DrainedAttempt): ResultStream {\n\treturn {\n\t\tasync *[Symbol.asyncIterator]() {\n\t\t\tyield* events;\n\t\t},\n\t\tresult: () => Promise.resolve(message),\n\t};\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for bounded, pre-completion credential failover around
|
|
3
|
+
* ModelRuntime.streamSimple(). Classification and stream buffering/replay only;
|
|
4
|
+
* pool selection state lives in CredentialPool, request orchestration in ModelRuntime.
|
|
5
|
+
*/
|
|
6
|
+
import { isRetryableAssistantError } from "@earendil-works/pi-ai";
|
|
7
|
+
const RATE_LIMIT_PATTERN = /\b429\b|rate.?limit|too many requests/i;
|
|
8
|
+
const BLOCKED_CREDENTIAL_PATTERN = /\b401\b|\b403\b|unauthorized|forbidden|invalid.?api.?key|permission denied|expired/i;
|
|
9
|
+
/**
|
|
10
|
+
* Classifies a failed AssistantMessage into a rotation-eligible CredentialFailureKind,
|
|
11
|
+
* or undefined when the failure should not trigger credential rotation (success,
|
|
12
|
+
* abort, or a non-retryable provider error such as quota exhaustion).
|
|
13
|
+
*/
|
|
14
|
+
export function classifyCredentialFailure(message) {
|
|
15
|
+
if (message.stopReason !== "error" || !message.errorMessage)
|
|
16
|
+
return undefined;
|
|
17
|
+
const text = message.errorMessage;
|
|
18
|
+
if (RATE_LIMIT_PATTERN.test(text))
|
|
19
|
+
return "rate_limited";
|
|
20
|
+
if (BLOCKED_CREDENTIAL_PATTERN.test(text))
|
|
21
|
+
return "blocked";
|
|
22
|
+
if (isRetryableAssistantError(message))
|
|
23
|
+
return "temporary";
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Fully consumes one attempt's stream before any event reaches the caller. This is
|
|
28
|
+
* what makes a failed attempt invisible to the caller: nothing is forwarded until the
|
|
29
|
+
* attempt's outcome (and therefore its retry eligibility) is known.
|
|
30
|
+
*/
|
|
31
|
+
export async function drainAttempt(stream) {
|
|
32
|
+
const events = [];
|
|
33
|
+
for await (const event of stream)
|
|
34
|
+
events.push(event);
|
|
35
|
+
return { events, message: await stream.result() };
|
|
36
|
+
}
|
|
37
|
+
/** Replays a fully-drained attempt's events as a fresh stream terminating in its known message. */
|
|
38
|
+
export function replayAttempt({ events, message }) {
|
|
39
|
+
return {
|
|
40
|
+
async *[Symbol.asyncIterator]() {
|
|
41
|
+
yield* events;
|
|
42
|
+
},
|
|
43
|
+
result: () => Promise.resolve(message),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=credential-failover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-failover.js","sourceRoot":"","sources":["../../src/core/credential-failover.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAGlE,MAAM,kBAAkB,GAAG,wCAAwC,CAAC;AACpE,MAAM,0BAA0B,GAC/B,qFAAqF,CAAC;AAEvF;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAyB,EAAqC;IACvG,IAAI,OAAO,CAAC,UAAU,KAAK,OAAO,IAAI,CAAC,OAAO,CAAC,YAAY;QAAE,OAAO,SAAS,CAAC;IAC9E,MAAM,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC;IAClC,IAAI,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,cAAc,CAAC;IACzD,IAAI,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,yBAAyB,CAAC,OAAO,CAAC;QAAE,OAAO,WAAW,CAAC;IAC3D,OAAO,SAAS,CAAC;AAAA,CACjB;AAYD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAoB,EAA2B;IACjF,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM;QAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrD,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;AAAA,CAClD;AAED,mGAAmG;AACnG,MAAM,UAAU,aAAa,CAAC,EAAE,MAAM,EAAE,OAAO,EAAkB,EAAgB;IAChF,OAAO;QACN,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG;YAC/B,KAAK,CAAC,CAAC,MAAM,CAAC;QAAA,CACd;QACD,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC;KACtC,CAAC;AAAA,CACF","sourcesContent":["/**\n * Pure helpers for bounded, pre-completion credential failover around\n * ModelRuntime.streamSimple(). Classification and stream buffering/replay only;\n * pool selection state lives in CredentialPool, request orchestration in ModelRuntime.\n */\n\nimport type { AssistantMessage, AssistantMessageEvent } from \"@earendil-works/pi-ai\";\nimport { isRetryableAssistantError } from \"@earendil-works/pi-ai\";\nimport type { CredentialFailureKind } from \"./credential-pool.ts\";\n\nconst RATE_LIMIT_PATTERN = /\\b429\\b|rate.?limit|too many requests/i;\nconst BLOCKED_CREDENTIAL_PATTERN =\n\t/\\b401\\b|\\b403\\b|unauthorized|forbidden|invalid.?api.?key|permission denied|expired/i;\n\n/**\n * Classifies a failed AssistantMessage into a rotation-eligible CredentialFailureKind,\n * or undefined when the failure should not trigger credential rotation (success,\n * abort, or a non-retryable provider error such as quota exhaustion).\n */\nexport function classifyCredentialFailure(message: AssistantMessage): CredentialFailureKind | undefined {\n\tif (message.stopReason !== \"error\" || !message.errorMessage) return undefined;\n\tconst text = message.errorMessage;\n\tif (RATE_LIMIT_PATTERN.test(text)) return \"rate_limited\";\n\tif (BLOCKED_CREDENTIAL_PATTERN.test(text)) return \"blocked\";\n\tif (isRetryableAssistantError(message)) return \"temporary\";\n\treturn undefined;\n}\n\n/** An AsyncIterable<AssistantMessageEvent> paired with its eventual AssistantMessage, matching the shape `lazyStream` forwards. */\nexport interface ResultStream extends AsyncIterable<AssistantMessageEvent> {\n\tresult(): Promise<AssistantMessage>;\n}\n\nexport interface DrainedAttempt {\n\tevents: readonly AssistantMessageEvent[];\n\tmessage: AssistantMessage;\n}\n\n/**\n * Fully consumes one attempt's stream before any event reaches the caller. This is\n * what makes a failed attempt invisible to the caller: nothing is forwarded until the\n * attempt's outcome (and therefore its retry eligibility) is known.\n */\nexport async function drainAttempt(stream: ResultStream): Promise<DrainedAttempt> {\n\tconst events: AssistantMessageEvent[] = [];\n\tfor await (const event of stream) events.push(event);\n\treturn { events, message: await stream.result() };\n}\n\n/** Replays a fully-drained attempt's events as a fresh stream terminating in its known message. */\nexport function replayAttempt({ events, message }: DrainedAttempt): ResultStream {\n\treturn {\n\t\tasync *[Symbol.asyncIterator]() {\n\t\t\tyield* events;\n\t\t},\n\t\tresult: () => Promise.resolve(message),\n\t};\n}\n"]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Non-secret credential selection state for a single process. */
|
|
2
|
+
export type CredentialIdentity = string & {
|
|
3
|
+
readonly __brand: "CredentialIdentity";
|
|
4
|
+
};
|
|
5
|
+
export declare function createCredentialIdentity(value: string): CredentialIdentity;
|
|
6
|
+
export interface CredentialPoolEntry {
|
|
7
|
+
identity: CredentialIdentity;
|
|
8
|
+
providerId: string;
|
|
9
|
+
}
|
|
10
|
+
export type CredentialFailureKind = "rate_limited" | "blocked" | "temporary";
|
|
11
|
+
export interface CredentialPoolOptions {
|
|
12
|
+
entries: readonly CredentialPoolEntry[];
|
|
13
|
+
now?: () => number;
|
|
14
|
+
cooldownMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface CredentialPoolSelection {
|
|
17
|
+
identity: CredentialIdentity;
|
|
18
|
+
providerId: string;
|
|
19
|
+
}
|
|
20
|
+
export interface CredentialPoolSnapshotEntry {
|
|
21
|
+
identity: CredentialIdentity;
|
|
22
|
+
providerId: string;
|
|
23
|
+
blockedUntil: number | undefined;
|
|
24
|
+
}
|
|
25
|
+
export interface CredentialRefreshLease {
|
|
26
|
+
identity: CredentialIdentity;
|
|
27
|
+
owner: string;
|
|
28
|
+
expiresAt: number;
|
|
29
|
+
}
|
|
30
|
+
export declare class CredentialPool {
|
|
31
|
+
private readonly entries;
|
|
32
|
+
private readonly now;
|
|
33
|
+
private readonly cooldownMs;
|
|
34
|
+
private readonly blockedUntilByIdentity;
|
|
35
|
+
private readonly refreshLeaseByIdentity;
|
|
36
|
+
private nextIndex;
|
|
37
|
+
constructor({ entries, now, cooldownMs }: CredentialPoolOptions);
|
|
38
|
+
select({ providerId, attempted }: {
|
|
39
|
+
providerId: string;
|
|
40
|
+
attempted: ReadonlySet<CredentialIdentity>;
|
|
41
|
+
}): CredentialPoolSelection | undefined;
|
|
42
|
+
recordFailure({ identity, kind }: {
|
|
43
|
+
identity: CredentialIdentity;
|
|
44
|
+
kind: CredentialFailureKind;
|
|
45
|
+
}): void;
|
|
46
|
+
snapshot(): CredentialPoolSnapshotEntry[];
|
|
47
|
+
private activeLease;
|
|
48
|
+
/** Grant a refresh lease for an identity, unless another owner already holds an unexpired one. */
|
|
49
|
+
acquireRefreshLease({ identity, owner, durationMs }: {
|
|
50
|
+
identity: CredentialIdentity;
|
|
51
|
+
owner: string;
|
|
52
|
+
durationMs: number;
|
|
53
|
+
}): CredentialRefreshLease | undefined;
|
|
54
|
+
/** Release a held refresh lease. Returns false if the caller is not the current owner. */
|
|
55
|
+
releaseRefreshLease({ identity, owner }: {
|
|
56
|
+
identity: CredentialIdentity;
|
|
57
|
+
owner: string;
|
|
58
|
+
}): boolean;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=credential-pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-pool.d.ts","sourceRoot":"","sources":["../../src/core/credential-pool.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAElE,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAA;CAAE,CAAC;AAErF,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,CAG1E;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,qBAAqB,GAAG,cAAc,GAAG,SAAS,GAAG,WAAW,CAAC;AAE7E,MAAM,WAAW,qBAAqB;IACrC,OAAO,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACxC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IACvC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,2BAA2B;IAC3C,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,cAAc;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAyC;IAChF,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAyD;IAChG,OAAO,CAAC,SAAS,CAAK;IAEtB,YAAY,EAAE,OAAO,EAAE,GAAc,EAAE,UAAmB,EAAE,EAAE,qBAAqB,EAWlF;IAED,MAAM,CAAC,EACN,UAAU,EACV,SAAS,EACT,EAAE;QACF,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,WAAW,CAAC,kBAAkB,CAAC,CAAC;KAC3C,GAAG,uBAAuB,GAAG,SAAS,CAkBtC;IAED,aAAa,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;QAAE,QAAQ,EAAE,kBAAkB,CAAC;QAAC,IAAI,EAAE,qBAAqB,CAAA;KAAE,GAAG,IAAI,CAIrG;IAED,QAAQ,IAAI,2BAA2B,EAAE,CAMxC;IAED,OAAO,CAAC,WAAW;IAUnB,kGAAkG;IAClG,mBAAmB,CAAC,EACnB,QAAQ,EACR,KAAK,EACL,UAAU,EACV,EAAE;QACF,QAAQ,EAAE,kBAAkB,CAAC;QAC7B,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;KACnB,GAAG,sBAAsB,GAAG,SAAS,CAMrC;IAED,0FAA0F;IAC1F,mBAAmB,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;QAAE,QAAQ,EAAE,kBAAkB,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAKjG;CACD","sourcesContent":["/** Non-secret credential selection state for a single process. */\n\nexport type CredentialIdentity = string & { readonly __brand: \"CredentialIdentity\" };\n\nexport function createCredentialIdentity(value: string): CredentialIdentity {\n\tif (!value.trim()) throw new Error(\"Credential identity must not be empty\");\n\treturn value as CredentialIdentity;\n}\n\nexport interface CredentialPoolEntry {\n\tidentity: CredentialIdentity;\n\tproviderId: string;\n}\n\nexport type CredentialFailureKind = \"rate_limited\" | \"blocked\" | \"temporary\";\n\nexport interface CredentialPoolOptions {\n\tentries: readonly CredentialPoolEntry[];\n\tnow?: () => number;\n\tcooldownMs?: number;\n}\n\nexport interface CredentialPoolSelection {\n\tidentity: CredentialIdentity;\n\tproviderId: string;\n}\n\nexport interface CredentialPoolSnapshotEntry {\n\tidentity: CredentialIdentity;\n\tproviderId: string;\n\tblockedUntil: number | undefined;\n}\n\nexport interface CredentialRefreshLease {\n\tidentity: CredentialIdentity;\n\towner: string;\n\texpiresAt: number;\n}\n\nexport class CredentialPool {\n\tprivate readonly entries: readonly CredentialPoolEntry[];\n\tprivate readonly now: () => number;\n\tprivate readonly cooldownMs: number;\n\tprivate readonly blockedUntilByIdentity = new Map<CredentialIdentity, number>();\n\tprivate readonly refreshLeaseByIdentity = new Map<CredentialIdentity, CredentialRefreshLease>();\n\tprivate nextIndex = 0;\n\n\tconstructor({ entries, now = Date.now, cooldownMs = 60_000 }: CredentialPoolOptions) {\n\t\tconst seen = new Set<CredentialIdentity>();\n\t\tfor (const entry of entries) {\n\t\t\tif (seen.has(entry.identity)) {\n\t\t\t\tthrow new Error(`Duplicate credential identity \"${entry.identity}\"`);\n\t\t\t}\n\t\t\tseen.add(entry.identity);\n\t\t}\n\t\tthis.entries = entries;\n\t\tthis.now = now;\n\t\tthis.cooldownMs = cooldownMs;\n\t}\n\n\tselect({\n\t\tproviderId,\n\t\tattempted,\n\t}: {\n\t\tproviderId: string;\n\t\tattempted: ReadonlySet<CredentialIdentity>;\n\t}): CredentialPoolSelection | undefined {\n\t\tif (this.entries.length === 0) return undefined;\n\t\tconst now = this.now();\n\t\tfor (let offset = 0; offset < this.entries.length; offset++) {\n\t\t\tconst index = (this.nextIndex + offset) % this.entries.length;\n\t\t\tconst entry = this.entries[index];\n\t\t\tconst blockedUntil = this.blockedUntilByIdentity.get(entry.identity);\n\t\t\tif (\n\t\t\t\tentry.providerId !== providerId ||\n\t\t\t\tattempted.has(entry.identity) ||\n\t\t\t\t(blockedUntil !== undefined && blockedUntil > now)\n\t\t\t) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.nextIndex = (index + 1) % this.entries.length;\n\t\t\treturn entry;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\trecordFailure({ identity, kind }: { identity: CredentialIdentity; kind: CredentialFailureKind }): void {\n\t\tif (kind === \"rate_limited\" || kind === \"blocked\") {\n\t\t\tthis.blockedUntilByIdentity.set(identity, this.now() + this.cooldownMs);\n\t\t}\n\t}\n\n\tsnapshot(): CredentialPoolSnapshotEntry[] {\n\t\treturn this.entries.map((entry) => ({\n\t\t\tidentity: entry.identity,\n\t\t\tproviderId: entry.providerId,\n\t\t\tblockedUntil: this.blockedUntilByIdentity.get(entry.identity),\n\t\t}));\n\t}\n\n\tprivate activeLease(identity: CredentialIdentity): CredentialRefreshLease | undefined {\n\t\tconst lease = this.refreshLeaseByIdentity.get(identity);\n\t\tif (!lease) return undefined;\n\t\tif (lease.expiresAt <= this.now()) {\n\t\t\tthis.refreshLeaseByIdentity.delete(identity);\n\t\t\treturn undefined;\n\t\t}\n\t\treturn lease;\n\t}\n\n\t/** Grant a refresh lease for an identity, unless another owner already holds an unexpired one. */\n\tacquireRefreshLease({\n\t\tidentity,\n\t\towner,\n\t\tdurationMs,\n\t}: {\n\t\tidentity: CredentialIdentity;\n\t\towner: string;\n\t\tdurationMs: number;\n\t}): CredentialRefreshLease | undefined {\n\t\tconst existing = this.activeLease(identity);\n\t\tif (existing && existing.owner !== owner) return undefined;\n\t\tconst lease: CredentialRefreshLease = { identity, owner, expiresAt: this.now() + durationMs };\n\t\tthis.refreshLeaseByIdentity.set(identity, lease);\n\t\treturn lease;\n\t}\n\n\t/** Release a held refresh lease. Returns false if the caller is not the current owner. */\n\treleaseRefreshLease({ identity, owner }: { identity: CredentialIdentity; owner: string }): boolean {\n\t\tconst existing = this.activeLease(identity);\n\t\tif (!existing || existing.owner !== owner) return false;\n\t\tthis.refreshLeaseByIdentity.delete(identity);\n\t\treturn true;\n\t}\n}\n"]}
|