@prometheus-ai/agent 0.5.4 → 0.5.8
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 +30 -0
- package/dist/cli.js +25110 -0
- package/dist/types/async/index.d.ts +0 -1
- package/dist/types/async/job-manager.d.ts +33 -0
- package/dist/types/autolearn/controller.d.ts +25 -0
- package/dist/types/autolearn/managed-skills.d.ts +45 -0
- package/dist/types/autoresearch/state.d.ts +1 -1
- package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
- package/dist/types/autoresearch/types.d.ts +1 -1
- package/dist/types/capability/context-file.d.ts +0 -13
- package/dist/types/capability/mcp.d.ts +1 -0
- package/dist/types/capability/rule-buckets.d.ts +1 -1
- package/dist/types/capability/rule.d.ts +6 -1
- package/dist/types/capability/types.d.ts +0 -4
- package/dist/types/cli/args.d.ts +23 -3
- package/dist/types/cli/bench-cli.d.ts +78 -0
- package/dist/types/cli/claude-trace-cli.d.ts +7 -0
- package/dist/types/cli/dry-balance-cli.d.ts +16 -2
- package/dist/types/cli/gallery-cli.d.ts +43 -0
- package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
- package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +55 -0
- package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
- package/dist/types/cli/gallery-screenshot.d.ts +35 -0
- package/dist/types/cli/grievances-cli.d.ts +1 -1
- package/dist/types/cli/list-models.d.ts +6 -14
- package/dist/types/cli/models-cli.d.ts +49 -0
- package/dist/types/cli/session-picker.d.ts +1 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/cli/setup-model-picker.d.ts +14 -0
- package/dist/types/cli/startup-cwd.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +13 -40
- package/dist/types/cli/usage-cli.d.ts +81 -0
- package/dist/types/cli-commands.d.ts +12 -0
- package/dist/types/collab/crypto.d.ts +7 -0
- package/dist/types/collab/guest.d.ts +37 -0
- package/dist/types/collab/host.d.ts +29 -0
- package/dist/types/collab/protocol.d.ts +119 -0
- package/dist/types/collab/relay-client.d.ts +22 -0
- package/dist/types/commands/bench.d.ts +29 -0
- package/dist/types/commands/gallery.d.ts +47 -0
- package/dist/types/commands/install.d.ts +1 -1
- package/dist/types/commands/join.d.ts +12 -0
- package/dist/types/commands/launch.d.ts +8 -4
- package/dist/types/commands/models.d.ts +33 -0
- package/dist/types/commands/read.d.ts +1 -1
- package/dist/types/commands/say.d.ts +24 -0
- package/dist/types/commands/token.d.ts +25 -0
- package/dist/types/commands/usage.d.ts +34 -0
- package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
- package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
- package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
- package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
- package/dist/types/commit/analysis/conventional.d.ts +2 -2
- package/dist/types/commit/analysis/summary.d.ts +2 -2
- package/dist/types/commit/changelog/generate.d.ts +3 -3
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/index.d.ts +3 -3
- package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
- package/dist/types/commit/model-selection.d.ts +10 -4
- package/dist/types/commit/shared-llm.d.ts +1 -1
- package/dist/types/config/api-key-resolver.d.ts +43 -0
- package/dist/types/config/append-only-context-mode.d.ts +2 -1
- package/dist/types/config/keybindings.d.ts +12 -7
- package/dist/types/config/model-discovery.d.ts +57 -0
- package/dist/types/config/model-equivalence.d.ts +1 -1
- package/dist/types/config/model-registry.d.ts +86 -222
- package/dist/types/config/model-resolver.d.ts +43 -12
- package/dist/types/config/model-roles.d.ts +29 -0
- package/dist/types/config/models-config-schema.d.ts +536 -43
- package/dist/types/config/models-config.d.ts +391 -0
- package/dist/types/config/settings-schema.d.ts +1202 -324
- package/dist/types/config/settings.d.ts +15 -3
- package/dist/types/dap/config.d.ts +14 -1
- package/dist/types/dap/types.d.ts +10 -0
- package/dist/types/debug/log-viewer.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +1 -1
- package/dist/types/debug/report-bundle.d.ts +3 -0
- package/dist/types/debug/terminal-info.d.ts +0 -1
- package/dist/types/discovery/at-imports.d.ts +15 -0
- package/dist/types/discovery/prometheus-extension-roots.d.ts +7 -7
- package/dist/types/edit/diff.d.ts +3 -2
- package/dist/types/edit/file-snapshot-store.d.ts +18 -0
- package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
- package/dist/types/edit/hashline/params.d.ts +1 -1
- package/dist/types/edit/index.d.ts +0 -1
- package/dist/types/edit/modes/apply-patch.d.ts +1 -1
- package/dist/types/edit/modes/patch.d.ts +1 -1
- package/dist/types/edit/modes/replace.d.ts +1 -1
- package/dist/types/edit/renderer.d.ts +1 -0
- package/dist/types/eval/__tests__/completion-bridge.test.d.ts +1 -0
- package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +1 -0
- package/dist/types/eval/__tests__/js-context-manager.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +7 -2
- package/dist/types/eval/bridge-timeout.d.ts +1 -1
- package/dist/types/eval/completion-bridge.d.ts +25 -0
- package/dist/types/eval/idle-timeout.d.ts +1 -5
- package/dist/types/eval/js/context-manager.d.ts +1 -0
- package/dist/types/eval/js/executor.d.ts +2 -0
- package/dist/types/eval/js/index.d.ts +1 -1
- package/dist/types/eval/js/shared/helpers.d.ts +7 -1
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
- package/dist/types/eval/js/shared/runtime.d.ts +6 -1
- package/dist/types/eval/js/worker-protocol.d.ts +6 -0
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
- package/dist/types/eval/py/executor.d.ts +12 -0
- package/dist/types/eval/py/index.d.ts +1 -1
- package/dist/types/eval/py/kernel.d.ts +6 -1
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exa/index.d.ts +1 -19
- package/dist/types/exa/mcp-client.d.ts +10 -3
- package/dist/types/exa/types.d.ts +0 -83
- package/dist/types/exec/bash-executor.d.ts +7 -0
- package/dist/types/export/custom-share.d.ts +1 -2
- package/dist/types/export/html/index.d.ts +39 -0
- package/dist/types/export/html/template-js.d.ts +2 -0
- package/dist/types/export/share.d.ts +61 -0
- package/dist/types/export/ttsr.d.ts +14 -0
- package/dist/types/extensibility/custom-commands/types.d.ts +9 -4
- package/dist/types/extensibility/custom-tools/loader.d.ts +30 -4
- package/dist/types/extensibility/custom-tools/types.d.ts +16 -8
- package/dist/types/extensibility/extensions/index.d.ts +1 -1
- package/dist/types/extensibility/extensions/loader.d.ts +20 -1
- package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
- package/dist/types/extensibility/extensions/runner.d.ts +5 -2
- package/dist/types/extensibility/extensions/types.d.ts +72 -11
- package/dist/types/extensibility/hooks/index.d.ts +2 -1
- package/dist/types/extensibility/hooks/loader.d.ts +1 -1
- package/dist/types/extensibility/hooks/types.d.ts +11 -5
- package/dist/types/extensibility/{legacy-pi-ai-shim.d.ts → legacy-package-ai-shim.d.ts} +2 -2
- package/dist/types/extensibility/plugins/{legacy-pi-compat.d.ts → legacy-package-compat.d.ts} +20 -3
- package/dist/types/extensibility/plugins/loader.d.ts +11 -0
- package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
- package/dist/types/extensibility/plugins/types.d.ts +2 -2
- package/dist/types/extensibility/shared-events.d.ts +3 -3
- package/dist/types/extensibility/skills.d.ts +10 -0
- package/dist/types/extensibility/slash-commands.d.ts +1 -11
- package/dist/types/goals/guided-setup.d.ts +18 -0
- package/dist/types/goals/state.d.ts +1 -1
- package/dist/types/goals/tools/goal-tool.d.ts +1 -1
- package/dist/types/hindsight/mental-models.d.ts +17 -8
- package/dist/types/hindsight/transcript.d.ts +1 -1
- package/dist/types/index.d.ts +5 -0
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/history-protocol.d.ts +14 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/local-protocol.d.ts +14 -2
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/irc/bus.d.ts +79 -0
- package/dist/types/lib/xai-http.d.ts +1 -1
- package/dist/types/lsp/client.d.ts +10 -0
- package/dist/types/lsp/config.d.ts +2 -2
- package/dist/types/lsp/edits.d.ts +9 -0
- package/dist/types/lsp/format-options.d.ts +32 -0
- package/dist/types/lsp/index.d.ts +2 -7
- package/dist/types/lsp/types.d.ts +13 -1
- package/dist/types/lsp/utils.d.ts +6 -2
- package/dist/types/main.d.ts +23 -8
- package/dist/types/mcp/json-rpc.d.ts +5 -0
- package/dist/types/mcp/manager.d.ts +8 -0
- package/dist/types/mcp/oauth-discovery.d.ts +6 -1
- package/dist/types/mcp/oauth-flow.d.ts +13 -3
- package/dist/types/mcp/startup-events.d.ts +11 -0
- package/dist/types/mcp/tool-bridge.d.ts +2 -0
- package/dist/types/mcp/transports/stdio.d.ts +13 -0
- package/dist/types/mcp/types.d.ts +2 -0
- package/dist/types/memories/index.d.ts +7 -15
- package/dist/types/memories/storage.d.ts +0 -10
- package/dist/types/memory-backend/index.d.ts +3 -1
- package/dist/types/memory-backend/local-backend.d.ts +4 -3
- package/dist/types/memory-backend/resolve.d.ts +2 -2
- package/dist/types/memory-backend/runtime.d.ts +4 -0
- package/dist/types/memory-backend/types.d.ts +67 -2
- package/dist/types/mnemopi/config.d.ts +31 -1
- package/dist/types/mnemopi/state.d.ts +40 -2
- package/dist/types/modes/acp/acp-agent.d.ts +1 -2
- package/dist/types/modes/components/agent-dashboard.d.ts +17 -1
- package/dist/types/modes/components/agent-hub.d.ts +82 -0
- package/dist/types/modes/components/assistant-message.d.ts +5 -12
- package/dist/types/modes/components/bash-execution.d.ts +1 -1
- package/dist/types/modes/components/chat-block.d.ts +64 -0
- package/dist/types/modes/components/collab-prompt-message.d.ts +10 -0
- package/dist/types/modes/components/compaction-summary-message.d.ts +25 -5
- package/dist/types/modes/components/copy-selector.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +49 -2
- package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
- package/dist/types/modes/components/dynamic-border.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
- package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
- package/dist/types/modes/components/footer.d.ts +1 -1
- package/dist/types/modes/components/hook-editor.d.ts +5 -0
- package/dist/types/modes/components/hook-input.d.ts +4 -0
- package/dist/types/modes/components/hook-selector.d.ts +5 -7
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/late-diagnostics-message.d.ts +20 -0
- package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -1
- package/dist/types/modes/components/oauth-selector.d.ts +10 -1
- package/dist/types/modes/components/overlay-box.d.ts +17 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +61 -0
- package/dist/types/modes/components/plan-toc.d.ts +41 -0
- package/dist/types/modes/components/read-tool-group.d.ts +8 -0
- package/dist/types/modes/components/reset-usage-selector.d.ts +12 -0
- package/dist/types/modes/components/segment-track.d.ts +11 -6
- package/dist/types/modes/components/session-selector.d.ts +18 -9
- package/dist/types/modes/components/settings-defs.d.ts +9 -2
- package/dist/types/modes/components/settings-selector.d.ts +17 -4
- package/dist/types/modes/components/snapcompact-shape-preview.d.ts +31 -0
- package/dist/types/modes/components/status-line/component.d.ts +61 -0
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +47 -3
- package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
- package/dist/types/modes/components/tool-execution.d.ts +49 -2
- package/dist/types/modes/components/transcript-container.d.ts +76 -26
- package/dist/types/modes/components/tree-selector.d.ts +2 -2
- package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
- package/dist/types/modes/components/usage-row.d.ts +3 -0
- package/dist/types/modes/components/user-message-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/components/visual-truncate.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +12 -2
- package/dist/types/modes/controllers/command-controller.d.ts +3 -2
- package/dist/types/modes/controllers/event-controller.d.ts +7 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
- package/dist/types/modes/controllers/input-controller.d.ts +25 -3
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +8 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +5 -2
- package/dist/types/modes/controllers/session-focus-controller.d.ts +31 -0
- package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
- package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
- package/dist/types/modes/gradient-highlight.d.ts +9 -4
- package/dist/types/modes/image-references.d.ts +14 -3
- package/dist/types/modes/index.d.ts +8 -7
- package/dist/types/modes/interactive-mode.d.ts +92 -16
- package/dist/types/modes/magic-keywords.d.ts +14 -2
- package/dist/types/modes/markdown-prose.d.ts +1 -1
- package/dist/types/modes/oauth-manual-input.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +48 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +67 -2
- package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +113 -1
- package/dist/types/modes/runtime-init.d.ts +4 -0
- package/dist/types/modes/session-observer-registry.d.ts +9 -0
- package/dist/types/modes/setup-version.d.ts +11 -0
- package/dist/types/modes/setup-wizard/index.d.ts +7 -2
- package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +4 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +11 -2
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +6 -2
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +42 -7
- package/dist/types/modes/types.d.ts +62 -13
- package/dist/types/modes/utils/context-usage.d.ts +6 -1
- package/dist/types/modes/utils/copy-targets.d.ts +21 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +4 -4
- package/dist/types/modes/workflow.d.ts +3 -3
- package/dist/types/plan-mode/approved-plan.d.ts +27 -8
- package/dist/types/plan-mode/plan-protection.d.ts +4 -4
- package/dist/types/registry/agent-lifecycle.d.ts +51 -0
- package/dist/types/registry/agent-registry.d.ts +33 -5
- package/dist/types/sdk.d.ts +46 -4
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +9 -3
- package/dist/types/session/agent-session.d.ts +136 -66
- package/dist/types/session/agent-storage.d.ts +2 -1
- package/dist/types/session/auth-broker-config.d.ts +4 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/codex-auto-reset.d.ts +111 -0
- package/dist/types/session/indexed-session-storage.d.ts +3 -3
- package/dist/types/session/messages.d.ts +26 -15
- package/dist/types/session/session-context.d.ts +39 -0
- package/dist/types/session/session-entries.d.ts +159 -0
- package/dist/types/session/session-history-format.d.ts +12 -0
- package/dist/types/session/session-listing.d.ts +69 -0
- package/dist/types/session/session-loader.d.ts +16 -0
- package/dist/types/session/session-manager.d.ts +107 -440
- package/dist/types/session/session-migrations.d.ts +12 -0
- package/dist/types/session/session-paths.d.ts +25 -0
- package/dist/types/session/session-persistence.d.ts +8 -0
- package/dist/types/session/session-storage.d.ts +11 -7
- package/dist/types/session/snapcompact-inline.d.ts +145 -0
- package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
- package/dist/types/session/streaming-output.d.ts +46 -0
- package/dist/types/session/tool-choice-queue.d.ts +6 -6
- package/dist/types/session/yield-queue.d.ts +10 -1
- package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
- package/dist/types/slash-commands/available-commands.d.ts +34 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +10 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +14 -0
- package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
- package/dist/types/slash-commands/helpers/reset-usage.d.ts +27 -0
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
- package/dist/types/slash-commands/types.d.ts +5 -9
- package/dist/types/ssh/connection-manager.d.ts +8 -0
- package/dist/types/stt/asr-client.d.ts +90 -0
- package/dist/types/stt/asr-protocol.d.ts +97 -0
- package/dist/types/stt/asr-worker.d.ts +2 -0
- package/dist/types/stt/downloader.d.ts +38 -0
- package/dist/types/stt/endpointer.d.ts +59 -0
- package/dist/types/stt/index.d.ts +5 -1
- package/dist/types/stt/models.d.ts +120 -0
- package/dist/types/stt/recorder.d.ts +17 -0
- package/dist/types/stt/stt-controller.d.ts +6 -0
- package/dist/types/stt/transcriber.d.ts +5 -7
- package/dist/types/stt/wav.d.ts +29 -0
- package/dist/types/system-prompt.d.ts +9 -1
- package/dist/types/task/commands.d.ts +1 -1
- package/dist/types/task/discovery.d.ts +1 -2
- package/dist/types/task/executor.d.ts +61 -2
- package/dist/types/task/index.d.ts +37 -6
- package/dist/types/task/output-manager.d.ts +0 -7
- package/dist/types/task/parallel.d.ts +2 -2
- package/dist/types/task/prometheus-command.d.ts +2 -2
- package/dist/types/task/render.d.ts +20 -7
- package/dist/types/task/repair-args.d.ts +8 -7
- package/dist/types/task/types.d.ts +109 -52
- package/dist/types/task/worktree.d.ts +2 -0
- package/dist/types/telemetry-export.d.ts +2 -2
- package/dist/types/thinking.d.ts +4 -0
- package/dist/types/tiny/models.d.ts +1 -1
- package/dist/types/tiny/title-client.d.ts +12 -1
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tools/archive-reader.d.ts +5 -0
- package/dist/types/tools/ask.d.ts +6 -1
- package/dist/types/tools/ast-edit.d.ts +4 -1
- package/dist/types/tools/ast-grep.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +5 -2
- package/dist/types/tools/browser/attach.d.ts +4 -4
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
- package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
- package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
- package/dist/types/tools/browser/registry.d.ts +17 -3
- package/dist/types/tools/browser/render.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
- package/dist/types/tools/browser/tab-worker.d.ts +18 -1
- package/dist/types/tools/browser.d.ts +3 -1
- package/dist/types/tools/checkpoint.d.ts +1 -1
- package/dist/types/tools/conflict-detect.d.ts +16 -0
- package/dist/types/tools/debug.d.ts +1 -1
- package/dist/types/tools/eval-render.d.ts +1 -8
- package/dist/types/tools/eval.d.ts +9 -1
- package/dist/types/tools/fetch.d.ts +17 -8
- package/dist/types/tools/find.d.ts +1 -8
- package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
- package/dist/types/tools/gh.d.ts +4 -1
- package/dist/types/tools/github-cache.d.ts +19 -0
- package/dist/types/tools/grouped-file-output.d.ts +46 -12
- package/dist/types/tools/image-gen.d.ts +1 -1
- package/dist/types/tools/index.d.ts +89 -8
- package/dist/types/tools/inspect-image.d.ts +1 -1
- package/dist/types/tools/irc.d.ts +79 -39
- package/dist/types/tools/job.d.ts +8 -2
- package/dist/types/tools/learn.d.ts +51 -0
- package/dist/types/tools/manage-skill.d.ts +40 -0
- package/dist/types/tools/memory-edit.d.ts +2 -2
- package/dist/types/tools/memory-recall.d.ts +1 -1
- package/dist/types/tools/memory-reflect.d.ts +1 -1
- package/dist/types/tools/memory-render.d.ts +4 -1
- package/dist/types/tools/memory-retain.d.ts +1 -1
- package/dist/types/tools/path-utils.d.ts +17 -5
- package/dist/types/tools/plan-mode-guard.d.ts +18 -9
- package/dist/types/tools/read.d.ts +3 -2
- package/dist/types/tools/render-mermaid.d.ts +1 -1
- package/dist/types/tools/render-utils.d.ts +47 -27
- package/dist/types/tools/renderers.d.ts +10 -2
- package/dist/types/tools/report-tool-issue.d.ts +6 -1
- package/dist/types/tools/resolve.d.ts +1 -1
- package/dist/types/tools/review.d.ts +1 -1
- package/dist/types/tools/search-tool-bm25.d.ts +1 -1
- package/dist/types/tools/search.d.ts +7 -3
- package/dist/types/tools/sqlite-reader.d.ts +4 -0
- package/dist/types/tools/ssh.d.ts +2 -1
- package/dist/types/tools/todo.d.ts +7 -15
- package/dist/types/tools/tool-result.d.ts +2 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/tools/tts.d.ts +26 -1
- package/dist/types/tools/write.d.ts +6 -3
- package/dist/types/tools/yield.d.ts +8 -0
- package/dist/types/tts/downloader.d.ts +20 -0
- package/dist/types/tts/index.d.ts +8 -0
- package/dist/types/tts/models.d.ts +82 -0
- package/dist/types/tts/player.d.ts +32 -0
- package/dist/types/tts/runtime.d.ts +6 -0
- package/dist/types/tts/streaming-player.d.ts +41 -0
- package/dist/types/tts/tts-client.d.ts +93 -0
- package/dist/types/tts/tts-protocol.d.ts +95 -0
- package/dist/types/tts/tts-worker.d.ts +2 -0
- package/dist/types/tts/vocalizer.d.ts +41 -0
- package/dist/types/tts/wav.d.ts +8 -0
- package/dist/types/tui/code-cell.d.ts +0 -2
- package/dist/types/tui/hyperlink.d.ts +13 -7
- package/dist/types/tui/output-block.d.ts +16 -22
- package/dist/types/tui/status-line.d.ts +3 -0
- package/dist/types/utils/block-context.d.ts +35 -0
- package/dist/types/utils/changelog.d.ts +8 -0
- package/dist/types/utils/clipboard.d.ts +4 -3
- package/dist/types/utils/enhanced-paste.d.ts +20 -0
- package/dist/types/utils/file-mentions.d.ts +7 -0
- package/dist/types/utils/git.d.ts +22 -3
- package/dist/types/utils/image-loading.d.ts +30 -1
- package/dist/types/utils/session-color.d.ts +15 -3
- package/dist/types/utils/thinking-display.d.ts +17 -0
- package/dist/types/utils/title-generator.d.ts +3 -2
- package/dist/types/utils/tool-choice.d.ts +8 -0
- package/dist/types/utils/tools-manager.d.ts +2 -1
- package/dist/types/web/kagi.d.ts +2 -2
- package/dist/types/web/parallel.d.ts +3 -0
- package/dist/types/web/scrapers/github.d.ts +22 -0
- package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
- package/dist/types/web/scrapers/types.d.ts +12 -0
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/providers/anthropic.d.ts +2 -1
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/brave.d.ts +2 -1
- package/dist/types/web/search/providers/codex.d.ts +2 -1
- package/dist/types/web/search/providers/exa.d.ts +2 -1
- package/dist/types/web/search/providers/gemini.d.ts +10 -6
- package/dist/types/web/search/providers/jina.d.ts +7 -2
- package/dist/types/web/search/providers/kagi.d.ts +7 -2
- package/dist/types/web/search/providers/kimi.d.ts +7 -2
- package/dist/types/web/search/providers/parallel.d.ts +2 -1
- package/dist/types/web/search/providers/perplexity.d.ts +10 -2
- package/dist/types/web/search/providers/searxng.d.ts +2 -1
- package/dist/types/web/search/providers/synthetic.d.ts +7 -3
- package/dist/types/web/search/providers/tavily.d.ts +2 -1
- package/dist/types/web/search/providers/zai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/examples/extensions/api-demo.ts +2 -2
- package/package.json +41 -15
- package/scripts/bench-guard.ts +71 -0
- package/scripts/build-binary.ts +24 -25
- package/scripts/bundle-dist.ts +97 -0
- package/scripts/generate-share-viewer.ts +34 -0
- package/scripts/prometheus +42 -0
- package/scripts/prometheus.ts +20 -0
- package/src/async/index.ts +0 -1
- package/src/async/job-manager.ts +106 -3
- package/src/auto-thinking/classifier.ts +2 -1
- package/src/autolearn/controller.ts +139 -0
- package/src/autolearn/managed-skills.ts +257 -0
- package/src/autoresearch/dashboard.ts +1 -1
- package/src/autoresearch/prompt-setup.md +6 -6
- package/src/autoresearch/prompt.md +6 -6
- package/src/autoresearch/state.ts +1 -1
- package/src/autoresearch/storage.ts +2 -1
- package/src/autoresearch/tools/init-experiment.ts +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/autoresearch/tools/run-experiment.ts +1 -1
- package/src/autoresearch/tools/update-notes.ts +1 -1
- package/src/autoresearch/types.ts +1 -1
- package/src/capability/context-file.ts +0 -14
- package/src/capability/fs.ts +10 -0
- package/src/capability/index.ts +1 -6
- package/src/capability/mcp.ts +1 -0
- package/src/capability/rule-buckets.ts +4 -2
- package/src/capability/rule.ts +10 -1
- package/src/capability/types.ts +0 -4
- package/src/cli/args.ts +66 -13
- package/src/cli/auth-broker-cli.ts +6 -7
- package/src/cli/auth-gateway-cli.ts +8 -9
- package/src/cli/bench-cli.ts +437 -0
- package/src/cli/claude-trace-cli.ts +28 -50
- package/src/cli/completion-gen.ts +28 -28
- package/src/cli/dry-balance-cli.ts +56 -23
- package/src/cli/gallery-cli.ts +231 -0
- package/src/cli/gallery-fixtures/agentic.ts +407 -0
- package/src/cli/gallery-fixtures/codeintel.ts +187 -0
- package/src/cli/gallery-fixtures/edit.ts +194 -0
- package/src/cli/gallery-fixtures/fs.ts +220 -0
- package/src/cli/gallery-fixtures/index.ts +40 -0
- package/src/cli/gallery-fixtures/interaction.ts +49 -0
- package/src/cli/gallery-fixtures/memory.ts +81 -0
- package/src/cli/gallery-fixtures/misc.ts +250 -0
- package/src/cli/gallery-fixtures/search.ts +213 -0
- package/src/cli/gallery-fixtures/shell.ts +167 -0
- package/src/cli/gallery-fixtures/types.ts +57 -0
- package/src/cli/gallery-fixtures/web.ts +158 -0
- package/src/cli/gallery-screenshot.ts +279 -0
- package/src/cli/grievances-cli.ts +1 -1
- package/src/cli/list-models.ts +16 -174
- package/src/cli/models-cli.ts +429 -0
- package/src/cli/session-picker.ts +2 -1
- package/src/cli/setup-cli.ts +148 -47
- package/src/cli/setup-model-picker.ts +43 -0
- package/src/cli/startup-cwd.ts +68 -0
- package/src/cli/update-cli.ts +144 -272
- package/src/cli/usage-cli.ts +774 -0
- package/src/cli-commands.ts +36 -0
- package/src/cli.ts +141 -32
- package/src/collab/crypto.ts +63 -0
- package/src/collab/guest.ts +451 -0
- package/src/collab/host.ts +565 -0
- package/src/collab/protocol.ts +241 -0
- package/src/collab/relay-client.ts +216 -0
- package/src/commands/bench.ts +42 -0
- package/src/commands/complete.ts +1 -1
- package/src/commands/gallery.ts +52 -0
- package/src/commands/install.ts +1 -1
- package/src/commands/join.ts +39 -0
- package/src/commands/launch.ts +8 -4
- package/src/commands/models.ts +61 -0
- package/src/commands/read.ts +6 -3
- package/src/commands/say.ts +102 -0
- package/src/commands/setup.ts +1 -1
- package/src/commands/token.ts +89 -0
- package/src/commands/usage.ts +43 -0
- package/src/commit/agentic/agent.ts +2 -1
- package/src/commit/agentic/tools/analyze-file.ts +42 -20
- package/src/commit/agentic/tools/git-file-diff.ts +1 -1
- package/src/commit/agentic/tools/git-hunk.ts +1 -1
- package/src/commit/agentic/tools/git-overview.ts +1 -1
- package/src/commit/agentic/tools/propose-changelog.ts +1 -1
- package/src/commit/agentic/tools/propose-commit.ts +1 -1
- package/src/commit/agentic/tools/recent-commits.ts +1 -1
- package/src/commit/agentic/tools/schemas.ts +1 -1
- package/src/commit/agentic/tools/split-commit.ts +9 -2
- package/src/commit/analysis/conventional.ts +2 -2
- package/src/commit/analysis/summary.ts +3 -3
- package/src/commit/changelog/generate.ts +3 -3
- package/src/commit/changelog/index.ts +2 -2
- package/src/commit/map-reduce/index.ts +3 -3
- package/src/commit/map-reduce/map-phase.ts +2 -2
- package/src/commit/map-reduce/reduce-phase.ts +2 -2
- package/src/commit/model-selection.ts +35 -12
- package/src/commit/pipeline.ts +4 -4
- package/src/commit/shared-llm.ts +1 -1
- package/src/config/api-key-resolver.ts +67 -0
- package/src/config/append-only-context-mode.ts +6 -12
- package/src/config/keybindings.ts +9 -4
- package/src/config/mcp-schema.json +4 -0
- package/src/config/model-discovery.ts +574 -0
- package/src/config/model-equivalence.ts +5 -4
- package/src/config/model-registry.ts +659 -1093
- package/src/config/model-resolver.ts +374 -174
- package/src/config/model-roles.ts +88 -0
- package/src/config/models-config-schema.ts +61 -9
- package/src/config/models-config.ts +130 -0
- package/src/config/settings-schema.ts +1441 -387
- package/src/config/settings.ts +261 -69
- package/src/dap/client.ts +138 -53
- package/src/dap/config.ts +41 -2
- package/src/dap/defaults.json +1 -0
- package/src/dap/session.ts +263 -161
- package/src/dap/types.ts +10 -0
- package/src/debug/index.ts +50 -60
- package/src/debug/log-viewer.ts +1 -1
- package/src/debug/protocol-probe.ts +1 -1
- package/src/debug/raw-sse-buffer.ts +7 -4
- package/src/debug/raw-sse.ts +1 -1
- package/src/debug/report-bundle.ts +9 -0
- package/src/debug/terminal-info.ts +0 -3
- package/src/discovery/agents-md.ts +25 -21
- package/src/discovery/agents.ts +9 -15
- package/src/discovery/at-imports.ts +273 -0
- package/src/discovery/builtin-rules/index.ts +4 -0
- package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
- package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
- package/src/discovery/builtin.ts +45 -23
- package/src/discovery/claude-plugins.ts +44 -5
- package/src/discovery/helpers.ts +50 -9
- package/src/discovery/prometheus-extension-roots.ts +10 -10
- package/src/discovery/prometheus-plugins.ts +10 -10
- package/src/edit/diff.ts +191 -4
- package/src/edit/file-snapshot-store.ts +34 -1
- package/src/edit/hashline/block-resolver.ts +20 -1
- package/src/edit/hashline/diff.ts +123 -2
- package/src/edit/hashline/execute.ts +60 -4
- package/src/edit/hashline/filesystem.ts +2 -1
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/edit/hashline/params.ts +1 -1
- package/src/edit/index.ts +47 -18
- package/src/edit/modes/apply-patch.ts +1 -1
- package/src/edit/modes/patch.ts +59 -3
- package/src/edit/modes/replace.ts +58 -24
- package/src/edit/notebook.ts +22 -2
- package/src/edit/renderer.ts +315 -151
- package/src/eval/__tests__/agent-bridge.test.ts +105 -39
- package/src/eval/__tests__/budget-bridge.test.ts +1 -1
- package/src/eval/__tests__/completion-bridge.test.ts +412 -0
- package/src/eval/__tests__/helpers-local-roots.test.ts +58 -0
- package/src/eval/__tests__/js-context-manager.test.ts +241 -0
- package/src/eval/__tests__/llm-bridge.test.ts +6 -4
- package/src/eval/__tests__/shared-executors.test.ts +34 -92
- package/src/eval/agent-bridge.ts +39 -23
- package/src/eval/backend.ts +15 -2
- package/src/eval/bridge-timeout.ts +1 -1
- package/src/eval/completion-bridge.ts +203 -0
- package/src/eval/idle-timeout.ts +3 -10
- package/src/eval/js/context-manager.ts +108 -31
- package/src/eval/js/executor.ts +9 -2
- package/src/eval/js/index.ts +7 -3
- package/src/eval/js/shared/helpers.ts +59 -13
- package/src/eval/js/shared/local-module-loader.ts +2 -2
- package/src/eval/js/shared/prelude.txt +167 -30
- package/src/eval/js/shared/rewrite-imports.ts +58 -34
- package/src/eval/js/shared/runtime.ts +24 -16
- package/src/eval/js/tool-bridge.ts +4 -0
- package/src/eval/js/worker-core.ts +1 -0
- package/src/eval/js/worker-entry.ts +6 -0
- package/src/eval/js/worker-protocol.ts +6 -0
- package/src/eval/llm-bridge.ts +2 -1
- package/src/eval/py/__tests__/prelude.test.ts +19 -0
- package/src/eval/py/executor.ts +70 -26
- package/src/eval/py/index.ts +13 -4
- package/src/eval/py/kernel.ts +48 -9
- package/src/eval/py/prelude.py +73 -24
- package/src/eval/py/runner.py +133 -28
- package/src/eval/py/runtime.ts +38 -1
- package/src/exa/index.ts +1 -26
- package/src/exa/mcp-client.ts +10 -10
- package/src/exa/types.ts +0 -97
- package/src/exec/bash-executor.ts +104 -7
- package/src/export/custom-share.ts +1 -1
- package/src/export/html/index.ts +119 -17
- package/src/export/html/share-loader.js +102 -0
- package/src/export/html/template-js.ts +6 -0
- package/src/export/html/template.css +745 -459
- package/src/export/html/template.css.d.ts +2 -0
- package/src/export/html/template.html +6 -3
- package/src/export/html/template.js +277 -891
- package/src/export/html/tool-views.generated.d.ts +2 -0
- package/src/export/html/tool-views.generated.js +38 -0
- package/src/export/share.ts +269 -0
- package/src/export/ttsr.ts +122 -1
- package/src/extensibility/custom-commands/loader.ts +7 -4
- package/src/extensibility/custom-commands/types.ts +9 -4
- package/src/extensibility/custom-tools/loader.ts +51 -23
- package/src/extensibility/custom-tools/types.ts +16 -8
- package/src/extensibility/extensions/get-commands-handler.ts +2 -1
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/loader.ts +70 -20
- package/src/extensibility/extensions/model-api.ts +41 -0
- package/src/extensibility/extensions/runner.ts +12 -2
- package/src/extensibility/extensions/types.ts +83 -11
- package/src/extensibility/extensions/wrapper.ts +41 -5
- package/src/extensibility/hooks/index.ts +2 -1
- package/src/extensibility/hooks/loader.ts +6 -3
- package/src/extensibility/hooks/types.ts +11 -5
- package/src/extensibility/{legacy-pi-ai-shim.ts → legacy-package-ai-shim.ts} +2 -2
- package/src/extensibility/plugins/doctor.ts +1 -2
- package/src/extensibility/plugins/installer.ts +2 -2
- package/src/extensibility/plugins/{legacy-pi-compat.ts → legacy-package-compat.ts} +165 -77
- package/src/extensibility/plugins/loader.ts +34 -23
- package/src/extensibility/plugins/manager.ts +226 -95
- package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
- package/src/extensibility/plugins/types.ts +3 -3
- package/src/extensibility/shared-events.ts +3 -3
- package/src/extensibility/skills.ts +113 -9
- package/src/extensibility/slash-commands.ts +1 -97
- package/src/goals/guided-setup.ts +133 -0
- package/src/goals/state.ts +1 -1
- package/src/goals/tools/goal-tool.ts +38 -28
- package/src/hindsight/bank.ts +17 -2
- package/src/hindsight/client.ts +27 -2
- package/src/hindsight/mental-models.ts +59 -12
- package/src/hindsight/state.ts +12 -3
- package/src/hindsight/transcript.ts +1 -1
- package/src/index.ts +5 -0
- package/src/internal-urls/artifact-protocol.ts +11 -2
- package/src/internal-urls/docs-index.generated.ts +9 -7
- package/src/internal-urls/history-protocol.ts +113 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +22 -9
- package/src/internal-urls/local-protocol.ts +42 -7
- package/src/internal-urls/memory-protocol.ts +4 -31
- package/src/internal-urls/router.ts +3 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/irc/bus.ts +303 -0
- package/src/lib/xai-http.ts +3 -3
- package/src/lsp/client.ts +245 -104
- package/src/lsp/clients/biome-client.ts +101 -39
- package/src/lsp/clients/lsp-linter-client.ts +2 -10
- package/src/lsp/config.ts +15 -5
- package/src/lsp/defaults.json +6 -0
- package/src/lsp/edits.ts +143 -95
- package/src/lsp/format-options.ts +119 -0
- package/src/lsp/index.ts +233 -93
- package/src/lsp/render.ts +11 -35
- package/src/lsp/types.ts +13 -1
- package/src/lsp/utils.ts +31 -12
- package/src/main.ts +396 -216
- package/src/mcp/config-writer.ts +7 -3
- package/src/mcp/json-rpc.ts +35 -5
- package/src/mcp/manager.ts +31 -16
- package/src/mcp/oauth-discovery.ts +34 -4
- package/src/mcp/oauth-flow.ts +61 -8
- package/src/mcp/render.ts +7 -1
- package/src/mcp/startup-events.ts +21 -0
- package/src/mcp/tool-bridge.ts +2 -0
- package/src/mcp/transports/stdio.ts +224 -4
- package/src/mcp/types.ts +2 -0
- package/src/memories/index.ts +174 -1128
- package/src/memories/storage.ts +2 -41
- package/src/memory-backend/index.ts +14 -1
- package/src/memory-backend/local-backend.ts +18 -3
- package/src/memory-backend/off-backend.ts +9 -0
- package/src/memory-backend/resolve.ts +4 -6
- package/src/memory-backend/runtime.ts +66 -0
- package/src/memory-backend/types.ts +82 -2
- package/src/mnemopi/backend.ts +220 -28
- package/src/mnemopi/config.ts +138 -33
- package/src/mnemopi/state.ts +91 -11
- package/src/modes/acp/acp-agent.ts +149 -142
- package/src/modes/acp/acp-event-mapper.ts +5 -1
- package/src/modes/components/agent-dashboard.ts +17 -11
- package/src/modes/components/agent-hub.ts +1346 -0
- package/src/modes/components/assistant-message.ts +190 -80
- package/src/modes/components/bash-execution.ts +1 -1
- package/src/modes/components/btw-panel.ts +5 -1
- package/src/modes/components/chat-block.ts +111 -0
- package/src/modes/components/collab-prompt-message.ts +30 -0
- package/src/modes/components/compaction-summary-message.ts +168 -33
- package/src/modes/components/copy-selector.ts +2 -45
- package/src/modes/components/custom-editor.test.ts +96 -0
- package/src/modes/components/custom-editor.ts +405 -118
- package/src/modes/components/custom-message.ts +1 -3
- package/src/modes/components/diff.ts +13 -2
- package/src/modes/components/dynamic-border.ts +12 -3
- package/src/modes/components/execution-shared.ts +1 -2
- package/src/modes/components/extensions/extension-dashboard.ts +8 -5
- package/src/modes/components/extensions/extension-list.ts +1 -1
- package/src/modes/components/extensions/inspector-panel.ts +7 -3
- package/src/modes/components/extensions/state-manager.ts +36 -41
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/history-search.ts +1 -1
- package/src/modes/components/hook-editor.ts +8 -0
- package/src/modes/components/hook-input.ts +8 -0
- package/src/modes/components/hook-message.ts +1 -3
- package/src/modes/components/hook-selector.ts +6 -7
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/late-diagnostics-message.ts +60 -0
- package/src/modes/components/login-dialog.ts +1 -1
- package/src/modes/components/logout-account-selector.ts +130 -0
- package/src/modes/components/mcp-add-wizard.ts +14 -1
- package/src/modes/components/model-selector.ts +177 -75
- package/src/modes/components/oauth-selector.ts +102 -16
- package/src/modes/components/overlay-box.ts +108 -0
- package/src/modes/components/plan-review-overlay.ts +845 -0
- package/src/modes/components/plan-toc.ts +138 -0
- package/src/modes/components/plugin-settings.ts +22 -5
- package/src/modes/components/read-tool-group.ts +442 -39
- package/src/modes/components/reset-usage-selector.ts +161 -0
- package/src/modes/components/segment-track.ts +44 -7
- package/src/modes/components/session-selector.ts +97 -37
- package/src/modes/components/settings-defs.ts +28 -6
- package/src/modes/components/settings-selector.ts +541 -93
- package/src/modes/components/skill-message.ts +0 -1
- package/src/modes/components/snapcompact-shape-preview-doc.md +11 -0
- package/src/modes/components/snapcompact-shape-preview.ts +193 -0
- package/src/modes/components/{status-line.ts → status-line/component.ts} +205 -168
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/presets.ts +3 -3
- package/src/modes/components/status-line/segments.ts +26 -7
- package/src/modes/components/status-line/types.ts +40 -9
- package/src/modes/components/tiny-title-download-progress.ts +1 -1
- package/src/modes/components/tips.txt +7 -3
- package/src/modes/components/todo-reminder.ts +0 -2
- package/src/modes/components/tool-execution.ts +236 -103
- package/src/modes/components/transcript-container.ts +724 -99
- package/src/modes/components/tree-selector.ts +19 -4
- package/src/modes/components/ttsr-notification.ts +72 -30
- package/src/modes/components/usage-row.ts +18 -0
- package/src/modes/components/user-message-selector.ts +1 -1
- package/src/modes/components/user-message.ts +28 -12
- package/src/modes/components/visual-truncate.ts +1 -1
- package/src/modes/components/welcome.ts +80 -22
- package/src/modes/controllers/command-controller-shared.ts +7 -6
- package/src/modes/controllers/command-controller.ts +210 -180
- package/src/modes/controllers/event-controller.ts +352 -142
- package/src/modes/controllers/extension-ui-controller.ts +167 -208
- package/src/modes/controllers/input-controller.ts +778 -162
- package/src/modes/controllers/mcp-command-controller.ts +232 -80
- package/src/modes/controllers/selector-controller.ts +284 -145
- package/src/modes/controllers/session-focus-controller.ts +112 -0
- package/src/modes/controllers/ssh-command-controller.ts +2 -2
- package/src/modes/controllers/streaming-reveal.ts +295 -0
- package/src/modes/controllers/tan-command-controller.ts +173 -0
- package/src/modes/controllers/tool-args-reveal.ts +174 -0
- package/src/modes/gradient-highlight.ts +21 -9
- package/src/modes/image-references.ts +33 -7
- package/src/modes/index.ts +8 -25
- package/src/modes/interactive-mode.ts +840 -186
- package/src/modes/magic-keywords.ts +28 -6
- package/src/modes/markdown-prose.ts +1 -1
- package/src/modes/oauth-manual-input.ts +30 -3
- package/src/modes/rpc/rpc-client.ts +186 -3
- package/src/modes/rpc/rpc-mode.ts +318 -24
- package/src/modes/rpc/rpc-subagents.ts +265 -0
- package/src/modes/rpc/rpc-types.ts +111 -2
- package/src/modes/runtime-init.ts +28 -3
- package/src/modes/session-observer-registry.ts +72 -3
- package/src/modes/setup-version.ts +11 -0
- package/src/modes/setup-wizard/index.ts +16 -4
- package/src/modes/setup-wizard/lazy.ts +16 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +25 -7
- package/src/modes/setup-wizard/scenes/providers.ts +45 -12
- package/src/modes/setup-wizard/scenes/sign-in.ts +14 -13
- package/src/modes/setup-wizard/scenes/splash.ts +1 -1
- package/src/modes/setup-wizard/scenes/theme.ts +29 -2
- package/src/modes/setup-wizard/scenes/types.ts +11 -2
- package/src/modes/setup-wizard/scenes/web-search.ts +26 -9
- package/src/modes/setup-wizard/wizard-overlay.ts +40 -3
- package/src/modes/shared.ts +2 -0
- package/src/modes/theme/defaults/dark-poimandres.json +1 -1
- package/src/modes/theme/defaults/light-poimandres.json +1 -1
- package/src/modes/theme/shimmer.ts +20 -9
- package/src/modes/theme/theme-schema.json +1 -1
- package/src/modes/theme/theme.ts +342 -82
- package/src/modes/types.ts +60 -18
- package/src/modes/utils/context-usage.ts +88 -8
- package/src/modes/utils/copy-targets.ts +133 -27
- package/src/modes/utils/hotkeys-markdown.ts +3 -2
- package/src/modes/utils/ui-helpers.ts +191 -110
- package/src/modes/workflow.ts +10 -10
- package/src/plan-mode/approved-plan.ts +66 -43
- package/src/plan-mode/plan-protection.ts +4 -4
- package/src/priority.json +5 -1
- package/src/prompts/agents/designer.md +1 -1
- package/src/prompts/agents/explore.md +3 -3
- package/src/prompts/agents/librarian.md +2 -3
- package/src/prompts/agents/oracle.md +2 -2
- package/src/prompts/agents/plan.md +6 -6
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/task.md +6 -5
- package/src/prompts/bench.md +12 -0
- package/src/prompts/ci-green-request.md +5 -7
- package/src/prompts/goals/goal-budget-limit.md +2 -2
- package/src/prompts/goals/goal-continuation.md +4 -4
- package/src/prompts/goals/goal-mode-active.md +1 -1
- package/src/prompts/goals/guided-goal-interview.md +8 -0
- package/src/prompts/goals/guided-goal-system.md +12 -0
- package/src/prompts/memories/consolidation.md +2 -7
- package/src/prompts/memories/consolidation_system.md +4 -0
- package/src/prompts/memories/identity_review.md +2 -2
- package/src/prompts/memories/read-path.md +11 -10
- package/src/prompts/memories/stage_one_system.md +2 -2
- package/src/prompts/review-custom-request.md +1 -1
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/auto-continue.md +1 -1
- package/src/prompts/system/autolearn-guidance-learn.md +1 -0
- package/src/prompts/system/autolearn-guidance.md +7 -0
- package/src/prompts/system/autolearn-nudge.md +3 -0
- package/src/prompts/system/background-tan-dispatch.md +8 -0
- package/src/prompts/system/btw-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +13 -1
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/eager-task.md +7 -0
- package/src/prompts/system/eager-todo.md +11 -6
- package/src/prompts/system/empty-stop-retry.md +4 -6
- package/src/prompts/system/irc-autoreply.md +6 -0
- package/src/prompts/system/irc-incoming.md +3 -4
- package/src/prompts/system/manual-continue.md +7 -0
- package/src/prompts/system/omfg-user.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +10 -10
- package/src/prompts/system/personalities/default.md +26 -0
- package/src/prompts/system/personalities/friendly.md +17 -0
- package/src/prompts/system/personalities/pragmatic.md +15 -0
- package/src/prompts/system/plan-mode-active.md +70 -77
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +4 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/system/project-prompt.md +2 -2
- package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
- package/src/prompts/system/snapcompact-context-stub.md +1 -0
- package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
- package/src/prompts/system/snapcompact-system-stub.md +1 -0
- package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
- package/src/prompts/system/subagent-system-prompt.md +7 -8
- package/src/prompts/system/system-prompt.md +28 -57
- package/src/prompts/system/tiny-title-system.md +1 -1
- package/src/prompts/system/title-marker-instruction.md +1 -0
- package/src/prompts/system/title-system-marker.md +16 -0
- package/src/prompts/system/title-system.md +16 -3
- package/src/prompts/system/ttsr-tool-reminder.md +1 -1
- package/src/prompts/system/workflow-notice.md +4 -4
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +2 -2
- package/src/prompts/tools/bash.md +16 -8
- package/src/prompts/tools/browser.md +33 -43
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/eval.md +31 -51
- package/src/prompts/tools/find.md +0 -1
- package/src/prompts/tools/github.md +8 -7
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/image-gen.md +1 -1
- package/src/prompts/tools/inspect-image-system.md +1 -1
- package/src/prompts/tools/irc.md +39 -31
- package/src/prompts/tools/job.md +2 -1
- package/src/prompts/tools/learn.md +7 -0
- package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/manage-skill.md +9 -0
- package/src/prompts/tools/memory-edit.md +1 -1
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/read.md +31 -39
- package/src/prompts/tools/recall.md +1 -1
- package/src/prompts/tools/reflect.md +1 -1
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/prompts/tools/replace.md +4 -10
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search-tool-bm25.md +1 -9
- package/src/prompts/tools/search.md +0 -1
- package/src/prompts/tools/ssh.md +0 -4
- package/src/prompts/tools/task-summary.md +5 -16
- package/src/prompts/tools/task.md +47 -31
- package/src/prompts/tools/todo.md +6 -3
- package/src/registry/agent-lifecycle.ts +218 -0
- package/src/registry/agent-registry.ts +46 -5
- package/src/sdk.ts +692 -219
- package/src/secrets/index.ts +8 -1
- package/src/secrets/obfuscator.ts +40 -19
- package/src/session/agent-session.ts +1577 -806
- package/src/session/agent-storage.ts +18 -9
- package/src/session/auth-broker-config.ts +30 -1
- package/src/session/auth-storage.ts +6 -0
- package/src/session/codex-auto-reset.ts +202 -0
- package/src/session/history-storage.ts +3 -2
- package/src/session/indexed-session-storage.ts +7 -10
- package/src/session/messages.ts +59 -95
- package/src/session/session-context.ts +352 -0
- package/src/session/session-dump-format.ts +12 -3
- package/src/session/session-entries.ts +194 -0
- package/src/session/session-history-format.ts +246 -0
- package/src/session/session-listing.ts +588 -0
- package/src/session/session-loader.ts +106 -0
- package/src/session/session-manager.ts +1003 -2920
- package/src/session/session-migrations.ts +78 -0
- package/src/session/session-paths.ts +193 -0
- package/src/session/session-persistence.ts +131 -0
- package/src/session/session-storage.ts +91 -30
- package/src/session/snapcompact-inline.ts +542 -0
- package/src/session/snapcompact-savings-journal.ts +113 -0
- package/src/session/streaming-output.ts +248 -11
- package/src/session/tool-choice-queue.ts +23 -11
- package/src/session/yield-queue.ts +20 -2
- package/src/slash-commands/acp-builtins.ts +25 -1
- package/src/slash-commands/available-commands.ts +105 -0
- package/src/slash-commands/builtin-registry.ts +575 -49
- package/src/slash-commands/helpers/active-oauth-account.ts +44 -0
- package/src/slash-commands/helpers/context-report.ts +28 -1
- package/src/slash-commands/helpers/logout.ts +88 -0
- package/src/slash-commands/helpers/reset-usage.ts +66 -0
- package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
- package/src/slash-commands/helpers/usage-report.ts +38 -3
- package/src/slash-commands/types.ts +5 -9
- package/src/ssh/connection-manager.ts +27 -0
- package/src/ssh/ssh-executor.ts +60 -4
- package/src/stt/asr-client.ts +520 -0
- package/src/stt/asr-protocol.ts +65 -0
- package/src/stt/asr-worker.ts +790 -0
- package/src/stt/downloader.ts +107 -47
- package/src/stt/endpointer.ts +259 -0
- package/src/stt/index.ts +5 -1
- package/src/stt/models.ts +150 -0
- package/src/stt/recorder.ts +254 -67
- package/src/stt/stt-controller.ts +201 -22
- package/src/stt/transcriber.ts +37 -68
- package/src/stt/wav.ts +173 -0
- package/src/system-prompt.ts +52 -10
- package/src/task/agents.ts +3 -4
- package/src/task/commands.ts +3 -2
- package/src/task/discovery.ts +17 -24
- package/src/task/executor.ts +1054 -529
- package/src/task/index.ts +862 -757
- package/src/task/output-manager.ts +0 -11
- package/src/task/parallel.ts +3 -3
- package/src/task/prometheus-command.ts +2 -2
- package/src/task/render.ts +529 -182
- package/src/task/repair-args.ts +21 -9
- package/src/task/types.ts +144 -66
- package/src/task/worktree.ts +64 -56
- package/src/telemetry-export.ts +27 -9
- package/src/thinking.ts +9 -7
- package/src/tiny/models.ts +2 -2
- package/src/tiny/text.ts +5 -1
- package/src/tiny/title-client.ts +72 -20
- package/src/tiny/title-protocol.ts +1 -1
- package/src/tiny/worker.ts +23 -99
- package/src/tool-discovery/tool-index.ts +2 -0
- package/src/tools/archive-reader.ts +94 -2
- package/src/tools/ask.ts +234 -177
- package/src/tools/ast-edit.ts +136 -80
- package/src/tools/ast-grep.ts +41 -45
- package/src/tools/auto-generated-guard.ts +20 -3
- package/src/tools/bash-interactive.ts +28 -8
- package/src/tools/bash.ts +198 -35
- package/src/tools/browser/attach.ts +26 -7
- package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
- package/src/tools/browser/cmux/rpc.ts +156 -0
- package/src/tools/browser/cmux/socket-client.ts +309 -0
- package/src/tools/browser/launch.ts +11 -2
- package/src/tools/browser/readable.ts +19 -2
- package/src/tools/browser/registry.ts +52 -5
- package/src/tools/browser/render.ts +13 -5
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +256 -34
- package/src/tools/browser/tab-worker.ts +259 -91
- package/src/tools/browser.ts +44 -2
- package/src/tools/checkpoint.ts +1 -1
- package/src/tools/conflict-detect.ts +50 -4
- package/src/tools/debug.ts +27 -12
- package/src/tools/eval-render.ts +32 -35
- package/src/tools/eval.ts +26 -12
- package/src/tools/fetch.ts +450 -99
- package/src/tools/find.ts +182 -142
- package/src/tools/gh-cache-invalidation.ts +255 -0
- package/src/tools/gh-renderer.ts +104 -51
- package/src/tools/gh.ts +232 -37
- package/src/tools/github-cache.ts +97 -7
- package/src/tools/grouped-file-output.ts +159 -52
- package/src/tools/image-gen.ts +237 -132
- package/src/tools/index.ts +147 -26
- package/src/tools/inspect-image-renderer.ts +74 -45
- package/src/tools/inspect-image.ts +12 -6
- package/src/tools/irc.ts +626 -173
- package/src/tools/job.ts +106 -29
- package/src/tools/learn.ts +144 -0
- package/src/tools/manage-skill.ts +104 -0
- package/src/tools/memory-edit.ts +4 -4
- package/src/tools/memory-recall.ts +7 -9
- package/src/tools/memory-reflect.ts +5 -9
- package/src/tools/memory-render.ts +23 -6
- package/src/tools/memory-retain.ts +4 -4
- package/src/tools/path-utils.ts +102 -48
- package/src/tools/plan-mode-guard.ts +101 -40
- package/src/tools/read.ts +475 -120
- package/src/tools/render-mermaid.ts +1 -1
- package/src/tools/render-utils.ts +132 -76
- package/src/tools/renderers.ts +12 -1
- package/src/tools/report-tool-issue.ts +14 -6
- package/src/tools/resolve.ts +20 -3
- package/src/tools/review.ts +2 -2
- package/src/tools/search-tool-bm25.ts +37 -24
- package/src/tools/search.ts +233 -115
- package/src/tools/sqlite-reader.ts +26 -17
- package/src/tools/ssh.ts +20 -14
- package/src/tools/todo.ts +197 -191
- package/src/tools/tool-result.ts +8 -0
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/tools/tts.ts +205 -74
- package/src/tools/write.ts +291 -155
- package/src/tools/yield.ts +10 -1
- package/src/tts/downloader.ts +64 -0
- package/src/tts/index.ts +8 -0
- package/src/tts/models.ts +137 -0
- package/src/tts/player.ts +137 -0
- package/src/tts/runtime.ts +21 -0
- package/src/tts/streaming-player.ts +266 -0
- package/src/tts/tts-client.ts +647 -0
- package/src/tts/tts-protocol.ts +60 -0
- package/src/tts/tts-worker.ts +505 -0
- package/src/tts/vocalizer.ts +162 -0
- package/src/tts/wav.ts +58 -0
- package/src/tui/code-cell.ts +2 -7
- package/src/tui/hyperlink.ts +40 -26
- package/src/tui/output-block.ts +60 -108
- package/src/tui/status-line.ts +5 -1
- package/src/utils/block-context.ts +312 -0
- package/src/utils/changelog.ts +27 -1
- package/src/utils/clipboard.ts +91 -22
- package/src/utils/commit-message-generator.ts +8 -3
- package/src/utils/enhanced-paste.ts +230 -0
- package/src/utils/file-mentions.ts +3 -1
- package/src/utils/git.ts +315 -15
- package/src/utils/image-loading.ts +65 -4
- package/src/utils/session-color.ts +83 -9
- package/src/utils/thinking-display.ts +37 -0
- package/src/utils/title-generator.ts +73 -10
- package/src/utils/tool-choice.ts +16 -0
- package/src/utils/tools-manager.ts +19 -1
- package/src/web/kagi.ts +28 -26
- package/src/web/parallel.ts +7 -3
- package/src/web/scrapers/arxiv.ts +1 -1
- package/src/web/scrapers/github.ts +351 -3
- package/src/web/scrapers/go-pkg.ts +1 -1
- package/src/web/scrapers/iacr.ts +1 -1
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/twitter.ts +2 -1
- package/src/web/scrapers/types.ts +87 -8
- package/src/web/scrapers/wikipedia.ts +1 -1
- package/src/web/scrapers/youtube.ts +9 -3
- package/src/web/search/index.ts +15 -2
- package/src/web/search/providers/anthropic.ts +62 -21
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/brave.ts +5 -2
- package/src/web/search/providers/codex.ts +87 -51
- package/src/web/search/providers/exa.ts +101 -10
- package/src/web/search/providers/gemini.ts +49 -24
- package/src/web/search/providers/jina.ts +15 -5
- package/src/web/search/providers/kagi.ts +9 -2
- package/src/web/search/providers/kimi.ts +45 -20
- package/src/web/search/providers/parallel.ts +39 -24
- package/src/web/search/providers/perplexity.ts +226 -63
- package/src/web/search/providers/searxng.ts +19 -3
- package/src/web/search/providers/synthetic.ts +16 -11
- package/src/web/search/providers/tavily.ts +12 -9
- package/src/web/search/providers/zai.ts +22 -9
- package/src/web/search/render.ts +59 -64
- package/src/web/search/types.ts +5 -1
- package/dist/types/discovery/context-files.d.ts +0 -17
- package/dist/types/exa/factory.d.ts +0 -13
- package/dist/types/exa/render.d.ts +0 -19
- package/dist/types/exa/researcher.d.ts +0 -9
- package/dist/types/exa/search.d.ts +0 -9
- package/dist/types/exa/websets.d.ts +0 -9
- package/dist/types/export/html/template.generated.d.ts +0 -1
- package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
- package/dist/types/modes/components/status-line.d.ts +0 -77
- package/dist/types/slash-commands/headless-plan.d.ts +0 -3
- package/dist/types/stt/setup.d.ts +0 -18
- package/scripts/generate-template.ts +0 -33
- package/src/discovery/context-files.ts +0 -49
- package/src/exa/factory.ts +0 -60
- package/src/exa/render.ts +0 -244
- package/src/exa/researcher.ts +0 -36
- package/src/exa/search.ts +0 -47
- package/src/exa/websets.ts +0 -248
- package/src/export/html/template.generated.ts +0 -2
- package/src/modes/components/session-observer-overlay.ts +0 -852
- package/src/slash-commands/headless-plan.ts +0 -142
- package/src/stt/setup.ts +0 -52
- package/src/stt/transcribe.py +0 -70
- /package/dist/types/extensibility/{legacy-pi-coding-agent-shim.d.ts → legacy-package-agent-shim.d.ts} +0 -0
- /package/src/extensibility/{legacy-pi-coding-agent-shim.ts → legacy-package-agent-shim.ts} +0 -0
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
* Modes use this class and add their own I/O layer on top.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import * as crypto from "node:crypto";
|
|
17
16
|
import * as fs from "node:fs";
|
|
18
17
|
import * as os from "node:os";
|
|
19
18
|
import * as path from "node:path";
|
|
@@ -29,6 +28,7 @@ import {
|
|
|
29
28
|
type AgentState,
|
|
30
29
|
type AgentTool,
|
|
31
30
|
AppendOnlyContextManager,
|
|
31
|
+
type AsideMessage,
|
|
32
32
|
resolveTelemetry,
|
|
33
33
|
ThinkingLevel,
|
|
34
34
|
} from "@prometheus-ai/agent-core";
|
|
@@ -49,12 +49,18 @@ import {
|
|
|
49
49
|
generateBranchSummary,
|
|
50
50
|
generateHandoff,
|
|
51
51
|
prepareCompaction,
|
|
52
|
+
resolveThresholdTokens,
|
|
52
53
|
type ShakeConfig,
|
|
53
54
|
type ShakeRegion,
|
|
54
55
|
type SummaryOptions,
|
|
55
56
|
shouldCompact,
|
|
56
57
|
} from "@prometheus-ai/agent-core/compaction";
|
|
57
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
DEFAULT_PRUNE_CONFIG,
|
|
60
|
+
pruneSupersededToolResults,
|
|
61
|
+
pruneToolOutputs,
|
|
62
|
+
readToolSupersedeKey,
|
|
63
|
+
} from "@prometheus-ai/agent-core/compaction/pruning";
|
|
58
64
|
import type { ProtectedToolMatcher } from "@prometheus-ai/agent-core/compaction/tool-protection";
|
|
59
65
|
import type {
|
|
60
66
|
AssistantMessage,
|
|
@@ -65,6 +71,9 @@ import type {
|
|
|
65
71
|
Model,
|
|
66
72
|
ProviderResponseMetadata,
|
|
67
73
|
ProviderSessionState,
|
|
74
|
+
ResetCreditAccountStatus,
|
|
75
|
+
ResetCreditRedeemOutcome,
|
|
76
|
+
ResetCreditTarget,
|
|
68
77
|
ServiceTier,
|
|
69
78
|
SimpleStreamOptions,
|
|
70
79
|
TextContent,
|
|
@@ -76,20 +85,22 @@ import type {
|
|
|
76
85
|
import {
|
|
77
86
|
calculateRateLimitBackoffMs,
|
|
78
87
|
clearAnthropicFastModeFallback,
|
|
88
|
+
deriveClaudeDeviceId,
|
|
79
89
|
Effort,
|
|
80
|
-
getSupportedEfforts,
|
|
81
90
|
isContextOverflow,
|
|
82
91
|
isUsageLimitError,
|
|
83
|
-
modelsAreEqual,
|
|
84
92
|
parseRateLimitReason,
|
|
85
93
|
resolveServiceTier,
|
|
86
94
|
streamSimple,
|
|
87
95
|
} from "@prometheus-ai/ai";
|
|
96
|
+
import { getSupportedEfforts } from "@prometheus-ai/catalog/model-thinking";
|
|
97
|
+
import { modelsAreEqual } from "@prometheus-ai/catalog/models";
|
|
88
98
|
import type { InMemorySnapshotStore } from "@prometheus-ai/hashline";
|
|
89
99
|
import { countTokens, MacOSPowerAssertion } from "@prometheus-ai/natives";
|
|
100
|
+
import * as snapcompact from "@prometheus-ai/snapcompact";
|
|
90
101
|
import {
|
|
91
|
-
APP_DISPLAY_NAME,
|
|
92
102
|
extractRetryHint,
|
|
103
|
+
formatDuration,
|
|
93
104
|
getAgentDbPath,
|
|
94
105
|
getInstallId,
|
|
95
106
|
isBunTestRuntime,
|
|
@@ -105,15 +116,18 @@ import { classifyDifficulty } from "../auto-thinking/classifier";
|
|
|
105
116
|
import { reset as resetCapabilities } from "../capability";
|
|
106
117
|
import type { Rule } from "../capability/rule";
|
|
107
118
|
import { shouldEnableAppendOnlyContext } from "../config/append-only-context-mode";
|
|
108
|
-
import {
|
|
119
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
109
120
|
import {
|
|
110
121
|
extractExplicitThinkingSelector,
|
|
122
|
+
filterAvailableModelsByEnabledPatterns,
|
|
111
123
|
formatModelSelectorValue,
|
|
112
124
|
formatModelString,
|
|
125
|
+
getModelMatchPreferences,
|
|
113
126
|
parseModelString,
|
|
114
127
|
type ResolvedModelRoleValue,
|
|
115
128
|
resolveModelRoleValue,
|
|
116
129
|
} from "../config/model-resolver";
|
|
130
|
+
import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
|
|
117
131
|
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
118
132
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
119
133
|
import { onAppendOnlyModeChanged } from "../config/settings";
|
|
@@ -129,7 +143,6 @@ import {
|
|
|
129
143
|
} from "../eval/py/executor";
|
|
130
144
|
import { defaultEvalSessionId } from "../eval/session-id";
|
|
131
145
|
import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
|
|
132
|
-
import { exportSessionToHtml } from "../export/html";
|
|
133
146
|
import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
|
|
134
147
|
import type { LoadedCustomCommand } from "../extensibility/custom-commands";
|
|
135
148
|
import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
|
|
@@ -152,6 +165,7 @@ import type {
|
|
|
152
165
|
TurnEndEvent,
|
|
153
166
|
TurnStartEvent,
|
|
154
167
|
} from "../extensibility/extensions";
|
|
168
|
+
import { createExtensionModelQuery } from "../extensibility/extensions/model-api";
|
|
155
169
|
import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
|
|
156
170
|
import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
157
171
|
import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
@@ -161,6 +175,7 @@ import { GoalRuntime } from "../goals/runtime";
|
|
|
161
175
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
162
176
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
163
177
|
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
178
|
+
import { IrcBus, type IrcMessage } from "../irc/bus";
|
|
164
179
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
165
180
|
import { getMnemopiSessionState, type MnemopiSessionState, setMnemopiSessionState } from "../mnemopi/state";
|
|
166
181
|
import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
|
|
@@ -172,8 +187,10 @@ import { containsWorkflow, WORKFLOW_NOTICE } from "../modes/workflow";
|
|
|
172
187
|
import { createPlanReadMatcher } from "../plan-mode/plan-protection";
|
|
173
188
|
import type { PlanModeState } from "../plan-mode/state";
|
|
174
189
|
import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
|
|
190
|
+
import eagerTaskPrompt from "../prompts/system/eager-task.md" with { type: "text" };
|
|
175
191
|
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
176
192
|
import emptyStopRetryTemplate from "../prompts/system/empty-stop-retry.md" with { type: "text" };
|
|
193
|
+
import ircAutoReplyTemplate from "../prompts/system/irc-autoreply.md" with { type: "text" };
|
|
177
194
|
import ircIncomingTemplate from "../prompts/system/irc-incoming.md" with { type: "text" };
|
|
178
195
|
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
179
196
|
import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
|
|
@@ -182,8 +199,12 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
|
|
|
182
199
|
};
|
|
183
200
|
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
|
184
201
|
import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" with { type: "text" };
|
|
185
|
-
import {
|
|
186
|
-
|
|
202
|
+
import {
|
|
203
|
+
deobfuscateSessionContext,
|
|
204
|
+
obfuscateProviderContext,
|
|
205
|
+
obfuscateProviderTools,
|
|
206
|
+
type SecretObfuscator,
|
|
207
|
+
} from "../secrets/obfuscator";
|
|
187
208
|
import { invalidateHostMetadata } from "../ssh/connection-manager";
|
|
188
209
|
import {
|
|
189
210
|
AUTO_THINKING,
|
|
@@ -191,6 +212,7 @@ import {
|
|
|
191
212
|
clampAutoThinkingEffort,
|
|
192
213
|
resolveProvisionalAutoLevel,
|
|
193
214
|
resolveThinkingLevelForModel,
|
|
215
|
+
shouldDisableReasoning,
|
|
194
216
|
toReasoningEffort,
|
|
195
217
|
} from "../thinking";
|
|
196
218
|
import { shutdownTinyTitleClient } from "../tiny/title-client";
|
|
@@ -216,29 +238,33 @@ import { parseCommandArgs } from "../utils/command-args";
|
|
|
216
238
|
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
217
239
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
218
240
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
219
|
-
import {
|
|
241
|
+
import { normalizeModelContextImages } from "../utils/image-loading";
|
|
242
|
+
import { buildNamedToolChoice, isToolChoiceActive } from "../utils/tool-choice";
|
|
220
243
|
import type { AuthStorage } from "./auth-storage";
|
|
221
244
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
245
|
+
import {
|
|
246
|
+
type CodexAutoRedeemRedeemDecision,
|
|
247
|
+
defaultCodexAutoRedeemCoordinator,
|
|
248
|
+
evaluateCodexAutoRedeem,
|
|
249
|
+
shouldEvaluateCodexAutoRedeem,
|
|
250
|
+
shouldPromptCodexAutoRedeem,
|
|
251
|
+
} from "./codex-auto-reset";
|
|
222
252
|
import {
|
|
223
253
|
type BashExecutionMessage,
|
|
224
|
-
type CompactionSummaryMessage,
|
|
225
254
|
type CustomMessage,
|
|
226
255
|
convertToLlm,
|
|
227
|
-
type FileMentionMessage,
|
|
228
256
|
type PythonExecutionMessage,
|
|
229
|
-
|
|
257
|
+
readQueueChipText,
|
|
230
258
|
SILENT_ABORT_MARKER,
|
|
259
|
+
SKILL_PROMPT_MESSAGE_TYPE,
|
|
231
260
|
stripImagesFromMessage,
|
|
232
261
|
} from "./messages";
|
|
262
|
+
import type { SessionContext } from "./session-context";
|
|
263
|
+
import { getLatestCompactionEntry, getRestorableSessionModels } from "./session-context";
|
|
233
264
|
import { formatSessionDumpText } from "./session-dump-format";
|
|
234
|
-
import type {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
NewSessionOptions,
|
|
238
|
-
SessionContext,
|
|
239
|
-
SessionManager,
|
|
240
|
-
} from "./session-manager";
|
|
241
|
-
import { EPHEMERAL_MODEL_CHANGE_ROLE, getLatestCompactionEntry, getRestorableSessionModels } from "./session-manager";
|
|
265
|
+
import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions } from "./session-entries";
|
|
266
|
+
import { EPHEMERAL_MODEL_CHANGE_ROLE } from "./session-entries";
|
|
267
|
+
import type { SessionManager } from "./session-manager";
|
|
242
268
|
import type { ShakeMode, ShakeResult } from "./shake-types";
|
|
243
269
|
import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
244
270
|
import { YieldQueue } from "./yield-queue";
|
|
@@ -249,11 +275,11 @@ export type AgentSessionEvent =
|
|
|
249
275
|
| {
|
|
250
276
|
type: "auto_compaction_start";
|
|
251
277
|
reason: "threshold" | "overflow" | "idle" | "incomplete";
|
|
252
|
-
action: "context-full" | "handoff" | "shake";
|
|
278
|
+
action: "context-full" | "handoff" | "shake" | "snapcompact";
|
|
253
279
|
}
|
|
254
280
|
| {
|
|
255
281
|
type: "auto_compaction_end";
|
|
256
|
-
action: "context-full" | "handoff" | "shake";
|
|
282
|
+
action: "context-full" | "handoff" | "shake" | "snapcompact";
|
|
257
283
|
result: CompactionResult | undefined;
|
|
258
284
|
aborted: boolean;
|
|
259
285
|
willRetry: boolean;
|
|
@@ -282,9 +308,38 @@ export type AgentSessionEvent =
|
|
|
282
308
|
|
|
283
309
|
/** Listener function for agent session events */
|
|
284
310
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
311
|
+
export type CommandMetadataChangedListener = () => void | Promise<void>;
|
|
285
312
|
export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime">;
|
|
286
313
|
|
|
287
314
|
const EMPTY_STOP_MAX_RETRIES = 3;
|
|
315
|
+
const RETRY_BACKOFF_MAX_DELAY_MS = 8_000;
|
|
316
|
+
const RETRY_BACKOFF_JITTER_RATIO = 0.25;
|
|
317
|
+
/**
|
|
318
|
+
* Hysteresis band for the post-shake "did we actually create headroom?" check.
|
|
319
|
+
* Shake counts as having resolved threshold pressure only when residual context
|
|
320
|
+
* lands at or below `SHAKE_RECOVERY_BAND × threshold`. Re-checking against the
|
|
321
|
+
* raw threshold lets shake keep reclaiming a trickle of the previous turn's
|
|
322
|
+
* output and land just under the line every turn, sustaining the auto-continue
|
|
323
|
+
* dead loop reported in #2275.
|
|
324
|
+
*/
|
|
325
|
+
const SHAKE_RECOVERY_BAND = 0.8;
|
|
326
|
+
|
|
327
|
+
function calculateRetryBackoffDelayMs(baseDelayMs: number, attempt: number): number {
|
|
328
|
+
const cappedDelayMs = Math.min(Math.max(0, baseDelayMs) * 2 ** Math.max(0, attempt - 1), RETRY_BACKOFF_MAX_DELAY_MS);
|
|
329
|
+
const jitter = 1 - Math.random() * RETRY_BACKOFF_JITTER_RATIO;
|
|
330
|
+
return cappedDelayMs * jitter;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Slack added past a sibling credential's block expiry before retrying, so
|
|
335
|
+
* the next getApiKey lands after the block has actually lapsed.
|
|
336
|
+
*/
|
|
337
|
+
const SIBLING_UNBLOCK_BUFFER_MS = 1_000;
|
|
338
|
+
const NON_WHITESPACE_RE = /\S/;
|
|
339
|
+
|
|
340
|
+
function hasNonWhitespace(value: string): boolean {
|
|
341
|
+
return NON_WHITESPACE_RE.test(value);
|
|
342
|
+
}
|
|
288
343
|
|
|
289
344
|
export interface AsyncJobSnapshot {
|
|
290
345
|
running: AsyncJobSnapshotItem[];
|
|
@@ -302,6 +357,8 @@ export interface AgentSessionConfig {
|
|
|
302
357
|
agent: Agent;
|
|
303
358
|
sessionManager: SessionManager;
|
|
304
359
|
settings: Settings;
|
|
360
|
+
/** Whether the caller explicitly requested yolo/auto-approve behavior for this session. */
|
|
361
|
+
autoApprove?: boolean;
|
|
305
362
|
/** Models to cycle through with Ctrl+P (from --models flag) */
|
|
306
363
|
scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
307
364
|
/** Initial session thinking selector. */
|
|
@@ -382,8 +439,8 @@ export interface AgentSessionConfig {
|
|
|
382
439
|
asyncJobManager?: AsyncJobManager;
|
|
383
440
|
/** Agent identity (registry id like "Main" or "Alice") used for IRC routing. */
|
|
384
441
|
agentId?: string;
|
|
385
|
-
/**
|
|
386
|
-
|
|
442
|
+
/** Whether this session is the top-level agent or a subagent. Defaults to "main". */
|
|
443
|
+
agentKind?: "main" | "sub";
|
|
387
444
|
/**
|
|
388
445
|
* Override the provider-facing session ID for all API requests from this session.
|
|
389
446
|
* When absent, `sessionManager.getSessionId()` is used. Needed when benchmark or
|
|
@@ -473,6 +530,12 @@ export interface SessionStats {
|
|
|
473
530
|
cost: number;
|
|
474
531
|
}
|
|
475
532
|
|
|
533
|
+
export interface FreshSessionResult {
|
|
534
|
+
previousSessionId: string;
|
|
535
|
+
sessionId: string;
|
|
536
|
+
closedProviderSessions: number;
|
|
537
|
+
}
|
|
538
|
+
|
|
476
539
|
/** Internal marker for hook messages queued through the agent loop */
|
|
477
540
|
// ============================================================================
|
|
478
541
|
// Constants
|
|
@@ -496,6 +559,7 @@ interface ActiveRetryFallbackState {
|
|
|
496
559
|
originalSelector: string;
|
|
497
560
|
originalThinkingLevel: ConfiguredThinkingLevel | undefined;
|
|
498
561
|
lastAppliedFallbackThinkingLevel: ConfiguredThinkingLevel | undefined;
|
|
562
|
+
pinned: boolean;
|
|
499
563
|
}
|
|
500
564
|
|
|
501
565
|
function parseRetryFallbackSelector(selector: string): RetryFallbackSelector | undefined {
|
|
@@ -520,24 +584,15 @@ function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): strin
|
|
|
520
584
|
return `${selector.provider}/${selector.id}`;
|
|
521
585
|
}
|
|
522
586
|
|
|
523
|
-
const
|
|
524
|
-
export const ANTHROPIC_TOOL_CALL_BATCH_CAP = 4;
|
|
525
|
-
const CLAUDE_OPUS_4_8_MODEL_ID = /(?:^|[./_-])claude-opus-4[.-]8\b/i;
|
|
526
|
-
|
|
527
|
-
export function resolveToolCallBatchCapForModel(model: Model | undefined): number | undefined {
|
|
528
|
-
if (!model) return undefined;
|
|
529
|
-
return model.provider === "anthropic" && CLAUDE_OPUS_4_8_MODEL_ID.test(model.id)
|
|
530
|
-
? ANTHROPIC_TOOL_CALL_BATCH_CAP
|
|
531
|
-
: undefined;
|
|
532
|
-
}
|
|
587
|
+
const EPHEMERAL_REPLY_MAX_BYTES = 4096;
|
|
533
588
|
|
|
534
589
|
/**
|
|
535
|
-
* Collapse degenerate
|
|
590
|
+
* Collapse degenerate ephemeral replies (/btw, /omfg side-channel turns).
|
|
536
591
|
* Models occasionally loop on a single line (~16 reports of N-times-repeated
|
|
537
592
|
* replies); compress runs longer than 3 down to one instance + `[…N×]`, then
|
|
538
593
|
* cap at 4 KiB so a runaway reply can't flood the channel.
|
|
539
594
|
*/
|
|
540
|
-
function
|
|
595
|
+
function dedupeEphemeralReply(text: string): string {
|
|
541
596
|
if (!text) return text;
|
|
542
597
|
const lines = text.split("\n");
|
|
543
598
|
const out: string[] = [];
|
|
@@ -554,11 +609,11 @@ function dedupeIrcReply(text: string): string {
|
|
|
554
609
|
i = j;
|
|
555
610
|
}
|
|
556
611
|
let result = out.join("\n");
|
|
557
|
-
if (Buffer.byteLength(result, "utf8") >
|
|
612
|
+
if (Buffer.byteLength(result, "utf8") > EPHEMERAL_REPLY_MAX_BYTES) {
|
|
558
613
|
// Trim by characters until we're under the byte budget — handles multi-byte
|
|
559
614
|
// glyphs at the boundary without splitting them.
|
|
560
615
|
const suffix = "\n[…truncated]";
|
|
561
|
-
const budget =
|
|
616
|
+
const budget = EPHEMERAL_REPLY_MAX_BYTES - Buffer.byteLength(suffix, "utf8");
|
|
562
617
|
while (Buffer.byteLength(result, "utf8") > budget) {
|
|
563
618
|
result = result.slice(0, -1);
|
|
564
619
|
}
|
|
@@ -603,14 +658,10 @@ function buildSessionMetadata(
|
|
|
603
658
|
const accountUuid = authStorage?.getOAuthAccountId("anthropic", sessionId);
|
|
604
659
|
if (typeof accountUuid === "string" && accountUuid.length > 0) {
|
|
605
660
|
userId.account_uuid = accountUuid;
|
|
606
|
-
// Claude Code's `device_id` is a stable 64-hex install
|
|
607
|
-
// prometheus's persistent install id
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
userId.device_id = crypto
|
|
611
|
-
.createHash("sha256")
|
|
612
|
-
.update(`prometheus-claude-device-id-v1:${getInstallId()}`)
|
|
613
|
-
.digest("hex");
|
|
661
|
+
// Claude Code's `device_id` is a stable 64-hex account-scoped install
|
|
662
|
+
// identifier. Include both prometheus's persistent install id and the Claude
|
|
663
|
+
// account UUID so two accounts on the same install do not share a device.
|
|
664
|
+
userId.device_id = deriveClaudeDeviceId(getInstallId(), accountUuid);
|
|
614
665
|
}
|
|
615
666
|
}
|
|
616
667
|
return { user_id: JSON.stringify(userId) };
|
|
@@ -811,12 +862,41 @@ function extractPermissionLocations(
|
|
|
811
862
|
// AgentSession Class
|
|
812
863
|
// ============================================================================
|
|
813
864
|
|
|
814
|
-
/**
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
865
|
+
/** Entry returned by {@link AgentSession.clearQueue} / {@link AgentSession.popLastQueuedMessage}. */
|
|
866
|
+
export type RestoredQueuedMessage = { text: string; images?: ImageContent[] };
|
|
867
|
+
|
|
868
|
+
function queuedTextContent(message: AgentMessage): string | undefined {
|
|
869
|
+
if (!("content" in message)) return undefined;
|
|
870
|
+
const content = message.content;
|
|
871
|
+
if (typeof content === "string") return content;
|
|
872
|
+
return content.find((part): part is TextContent => part.type === "text")?.text;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function queuedImageContent(message: AgentMessage): ImageContent[] | undefined {
|
|
876
|
+
if (!("content" in message) || typeof message.content === "string") return undefined;
|
|
877
|
+
const images = message.content.filter(
|
|
878
|
+
(part): part is ImageContent =>
|
|
879
|
+
part.type === "image" && typeof part.data === "string" && typeof part.mimeType === "string",
|
|
880
|
+
);
|
|
881
|
+
return images.length > 0 ? images : undefined;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function isDisplayableQueuedMessage(message: AgentMessage): boolean {
|
|
885
|
+
return !(message.role === "custom" && message.display === false);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function queueChipText(message: AgentMessage): string {
|
|
889
|
+
if (message.role === "custom") {
|
|
890
|
+
return readQueueChipText(message.details) ?? queuedTextContent(message) ?? "";
|
|
891
|
+
}
|
|
892
|
+
const text = queuedTextContent(message) ?? "";
|
|
893
|
+
if (text) return text;
|
|
894
|
+
return queuedImageContent(message) ? "[Image]" : "";
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function toRestoredQueuedMessage(message: AgentMessage): RestoredQueuedMessage {
|
|
898
|
+
return { text: queueChipText(message), images: queuedImageContent(message) };
|
|
899
|
+
}
|
|
820
900
|
|
|
821
901
|
export class AgentSession {
|
|
822
902
|
readonly agent: Agent;
|
|
@@ -824,6 +904,7 @@ export class AgentSession {
|
|
|
824
904
|
readonly settings: Settings;
|
|
825
905
|
readonly yieldQueue: YieldQueue;
|
|
826
906
|
fileSnapshotStore?: InMemorySnapshotStore;
|
|
907
|
+
#autoApprove: boolean;
|
|
827
908
|
|
|
828
909
|
#powerAssertion: MacOSPowerAssertion | undefined;
|
|
829
910
|
|
|
@@ -845,19 +926,12 @@ export class AgentSession {
|
|
|
845
926
|
/** Last (enable, providerId) tuple resolved by `#syncAppendOnlyContext` — used to skip no-op invalidations. */
|
|
846
927
|
#lastAppendOnlyResolution?: { enable: boolean; providerId: string | undefined };
|
|
847
928
|
#eventListeners: AgentSessionEventListener[] = [];
|
|
929
|
+
#commandMetadataChangedListeners: CommandMetadataChangedListener[] = [];
|
|
848
930
|
|
|
849
|
-
/** Tracks pending steering messages for UI display. Removed when delivered.
|
|
850
|
-
* Entry shape: `{ text }` for plain-text steers (user-message dequeue
|
|
851
|
-
* matches by `.text`); `{ text, tag }` for queued custom messages (skill
|
|
852
|
-
* invocations dispatched while streaming) — the custom-role dequeue
|
|
853
|
-
* matches by `.tag` so duplicate-args queued skills cannot collide. */
|
|
854
|
-
#steeringMessages: QueuedDisplayEntry[] = [];
|
|
855
|
-
/** Tracks pending follow-up messages for UI display. Removed when delivered.
|
|
856
|
-
* See `#steeringMessages` for entry shape. */
|
|
857
|
-
#followUpMessages: QueuedDisplayEntry[] = [];
|
|
858
931
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
859
932
|
#pendingNextTurnMessages: CustomMessage[] = [];
|
|
860
933
|
#scheduledHiddenNextTurnGeneration: number | undefined = undefined;
|
|
934
|
+
#queuedMessageDrainScheduled = false;
|
|
861
935
|
#planModeState: PlanModeState | undefined;
|
|
862
936
|
#goalModeState: GoalModeState | undefined;
|
|
863
937
|
#goalRuntime: GoalRuntime;
|
|
@@ -916,14 +990,14 @@ export class AgentSession {
|
|
|
916
990
|
#activeEvalExecutions = new Set<Promise<unknown>>();
|
|
917
991
|
#evalExecutionDisposing = false;
|
|
918
992
|
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
#
|
|
922
|
-
|
|
923
|
-
// Agent identity + registry for IRC relay forwarding to the main session UI.
|
|
993
|
+
// Incoming IRC messages received while a turn was streaming; drained as
|
|
994
|
+
// non-interrupting asides at the next step boundary (see the aside provider).
|
|
995
|
+
#pendingIrcAsides: CustomMessage[] = [];
|
|
996
|
+
// Agent identity (registry id) used for IRC routing and job ownership.
|
|
924
997
|
#agentId: string | undefined;
|
|
925
|
-
#
|
|
998
|
+
#agentKind: "main" | "sub" = "main";
|
|
926
999
|
#providerSessionId: string | undefined;
|
|
1000
|
+
#freshProviderSessionId: string | undefined;
|
|
927
1001
|
#isDisposed = false;
|
|
928
1002
|
// Extension system
|
|
929
1003
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
@@ -1001,10 +1075,6 @@ export class AgentSession {
|
|
|
1001
1075
|
* without producing an aborted message_end). */
|
|
1002
1076
|
#planCompactAbortPending = false;
|
|
1003
1077
|
|
|
1004
|
-
/** Monotonic counter for `enqueueCustomMessageDisplay` tag generation;
|
|
1005
|
-
* combined with `Date.now()` so tags stay unique even across rapid
|
|
1006
|
-
* same-tick enqueues. */
|
|
1007
|
-
#customDisplayTagCounter = 0;
|
|
1008
1078
|
#postPromptTasks = new Set<Promise<unknown>>();
|
|
1009
1079
|
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
1010
1080
|
#postPromptTasksResolve: (() => void) | undefined = undefined;
|
|
@@ -1017,6 +1087,7 @@ export class AgentSession {
|
|
|
1017
1087
|
|
|
1018
1088
|
#streamingEditFileCache = new Map<string, string>();
|
|
1019
1089
|
#promptInFlightCount = 0;
|
|
1090
|
+
#abortInProgress = false;
|
|
1020
1091
|
// Wire-level agent_end emission deferred until #promptInFlightCount drops to 0.
|
|
1021
1092
|
// Internal extension hooks and post-emit work (auto-retry, auto-compaction, todo
|
|
1022
1093
|
// checks in #handleAgentEvent) still fire on the original schedule — only the
|
|
@@ -1048,7 +1119,7 @@ export class AgentSession {
|
|
|
1048
1119
|
if (!idle && !system && !user && !display) return;
|
|
1049
1120
|
try {
|
|
1050
1121
|
this.#powerAssertion = MacOSPowerAssertion.start({
|
|
1051
|
-
reason:
|
|
1122
|
+
reason: "Prometheus agent session",
|
|
1052
1123
|
idle,
|
|
1053
1124
|
system,
|
|
1054
1125
|
user,
|
|
@@ -1082,17 +1153,25 @@ export class AgentSession {
|
|
|
1082
1153
|
if (this.#promptInFlightCount === 0) {
|
|
1083
1154
|
this.#releasePowerAssertion();
|
|
1084
1155
|
this.#flushPendingAgentEnd();
|
|
1156
|
+
this.#drainStrandedQueuedMessages();
|
|
1085
1157
|
}
|
|
1086
1158
|
}
|
|
1087
1159
|
|
|
1160
|
+
/** A steer/follow-up can land after the agent loop's final queue poll, or
|
|
1161
|
+
* after an abort stops an auto-continued queued turn. In both cases the
|
|
1162
|
+
* agent-core queue still owns the message, but no loop is left to poll it.
|
|
1163
|
+
* Runs whenever the session settles; the guard makes it a no-op when the
|
|
1164
|
+
* queue was consumed normally or a new turn already started. */
|
|
1165
|
+
#drainStrandedQueuedMessages(): void {
|
|
1166
|
+
if (this.#abortInProgress) return;
|
|
1167
|
+
this.#scheduleQueuedMessageDrain();
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1088
1170
|
#resetInFlight(): void {
|
|
1089
1171
|
this.#promptInFlightCount = 0;
|
|
1090
1172
|
this.#releasePowerAssertion();
|
|
1091
1173
|
this.#flushPendingAgentEnd();
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
#syncToolCallBatchCap(model: Model | undefined = this.model): void {
|
|
1095
|
-
this.agent.maxToolCallsPerTurn = resolveToolCallBatchCapForModel(model);
|
|
1174
|
+
this.#drainStrandedQueuedMessages();
|
|
1096
1175
|
}
|
|
1097
1176
|
|
|
1098
1177
|
#flushPendingAgentEnd(): void {
|
|
@@ -1106,6 +1185,7 @@ export class AgentSession {
|
|
|
1106
1185
|
this.agent = config.agent;
|
|
1107
1186
|
this.sessionManager = config.sessionManager;
|
|
1108
1187
|
this.settings = config.settings;
|
|
1188
|
+
this.#autoApprove = config.autoApprove === true;
|
|
1109
1189
|
// Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
|
|
1110
1190
|
this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
|
|
1111
1191
|
this.#parentEvalSessionId = config.parentEvalSessionId;
|
|
@@ -1121,6 +1201,7 @@ export class AgentSession {
|
|
|
1121
1201
|
} else {
|
|
1122
1202
|
this.#thinkingLevel = config.thinkingLevel;
|
|
1123
1203
|
}
|
|
1204
|
+
this.#applyThinkingLevelToAgent(this.#thinkingLevel);
|
|
1124
1205
|
this.#promptTemplates = config.promptTemplates ?? [];
|
|
1125
1206
|
this.#slashCommands = config.slashCommands ?? [];
|
|
1126
1207
|
this.#extensionRunner = config.extensionRunner;
|
|
@@ -1163,7 +1244,6 @@ export class AgentSession {
|
|
|
1163
1244
|
this.agent.setRawSseEventInterceptor(this.#onSseEvent);
|
|
1164
1245
|
this.yieldQueue = new YieldQueue({
|
|
1165
1246
|
isStreaming: () => this.isStreaming,
|
|
1166
|
-
injectStreaming: message => this.agent.followUp(message),
|
|
1167
1247
|
injectIdle: async messages => {
|
|
1168
1248
|
const first = messages[0];
|
|
1169
1249
|
if (!first) return;
|
|
@@ -1178,7 +1258,16 @@ export class AgentSession {
|
|
|
1178
1258
|
);
|
|
1179
1259
|
},
|
|
1180
1260
|
});
|
|
1181
|
-
|
|
1261
|
+
// Background-job completions / late diagnostics are pulled into the run at
|
|
1262
|
+
// each step boundary as non-interrupting asides (see Agent.getAsideMessages),
|
|
1263
|
+
// so they reach the model between requests without waiting for a yield.
|
|
1264
|
+
this.agent.setAsideMessageProvider(() => {
|
|
1265
|
+
const pendingIrc = this.#pendingIrcAsides;
|
|
1266
|
+
this.#pendingIrcAsides = [];
|
|
1267
|
+
const thunks: AsideMessage[] = pendingIrc.map(record => () => record);
|
|
1268
|
+
thunks.push(...this.yieldQueue.drainLazy());
|
|
1269
|
+
return thunks;
|
|
1270
|
+
});
|
|
1182
1271
|
this.#convertToLlm = config.convertToLlm ?? convertToLlm;
|
|
1183
1272
|
this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
|
|
1184
1273
|
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
@@ -1209,9 +1298,8 @@ export class AgentSession {
|
|
|
1209
1298
|
this.#ttsrManager = config.ttsrManager;
|
|
1210
1299
|
this.#obfuscator = config.obfuscator;
|
|
1211
1300
|
this.#agentId = config.agentId;
|
|
1212
|
-
this.#
|
|
1301
|
+
this.#agentKind = config.agentKind ?? "main";
|
|
1213
1302
|
this.#providerSessionId = config.providerSessionId;
|
|
1214
|
-
this.#syncToolCallBatchCap();
|
|
1215
1303
|
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
|
1216
1304
|
const event: AgentEvent = {
|
|
1217
1305
|
type: "message_update",
|
|
@@ -1277,9 +1365,22 @@ export class AgentSession {
|
|
|
1277
1365
|
return this.#modelRegistry;
|
|
1278
1366
|
}
|
|
1279
1367
|
|
|
1368
|
+
get asyncJobManager(): AsyncJobManager | undefined {
|
|
1369
|
+
return this.#asyncJobManager;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
getAgentId(): string | undefined {
|
|
1373
|
+
return this.#agentId;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1280
1376
|
/** Advance the tool-choice queue and return the next directive for the upcoming LLM call. */
|
|
1281
1377
|
nextToolChoice(): ToolChoice | undefined {
|
|
1282
|
-
|
|
1378
|
+
const choice = this.#toolChoiceQueue.nextToolChoice();
|
|
1379
|
+
if (isToolChoiceActive(choice, this.agent.state.tools)) {
|
|
1380
|
+
return choice;
|
|
1381
|
+
}
|
|
1382
|
+
this.#toolChoiceQueue.reject("unavailable");
|
|
1383
|
+
return undefined;
|
|
1283
1384
|
}
|
|
1284
1385
|
|
|
1285
1386
|
/**
|
|
@@ -1356,6 +1457,11 @@ export class AgentSession {
|
|
|
1356
1457
|
return this.#ttsrManager;
|
|
1357
1458
|
}
|
|
1358
1459
|
|
|
1460
|
+
/** Secret obfuscator, when secrets are configured; /share redaction reuses it. */
|
|
1461
|
+
get obfuscator(): SecretObfuscator | undefined {
|
|
1462
|
+
return this.#obfuscator;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1359
1465
|
/** Whether a TTSR abort is pending (stream was aborted to inject rules) */
|
|
1360
1466
|
get isTtsrAbortPending(): boolean {
|
|
1361
1467
|
return this.#ttsrAbortPending;
|
|
@@ -1382,28 +1488,6 @@ export class AgentSession {
|
|
|
1382
1488
|
this.#planCompactAbortPending = false;
|
|
1383
1489
|
}
|
|
1384
1490
|
|
|
1385
|
-
/** Register a compact display string for a custom message that the caller is
|
|
1386
|
-
* about to dispatch via `promptCustomMessage` / `sendCustomMessage`.
|
|
1387
|
-
* Returns a stable tag the caller MUST embed in
|
|
1388
|
-
* `CustomMessage.details.__pendingDisplayTag` so the agent-side
|
|
1389
|
-
* `message_start` handler can remove the matching display entry when the
|
|
1390
|
-
* queued message is consumed.
|
|
1391
|
-
*
|
|
1392
|
-
* Does NOT push to the agent's steering/followUp queue — that happens
|
|
1393
|
-
* separately inside `sendCustomMessage`. */
|
|
1394
|
-
enqueueCustomMessageDisplay(text: string, mode: "steer" | "followUp"): string {
|
|
1395
|
-
const tag = `prometheus-cmd-${Date.now()}-${++this.#customDisplayTagCounter}`;
|
|
1396
|
-
const displayText = text.trim();
|
|
1397
|
-
if (!displayText) return tag;
|
|
1398
|
-
const entry: QueuedDisplayEntry = { text: displayText, tag };
|
|
1399
|
-
if (mode === "steer") {
|
|
1400
|
-
this.#steeringMessages.push(entry);
|
|
1401
|
-
} else {
|
|
1402
|
-
this.#followUpMessages.push(entry);
|
|
1403
|
-
}
|
|
1404
|
-
return tag;
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
1491
|
getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
|
|
1408
1492
|
const manager = this.#asyncJobManager;
|
|
1409
1493
|
if (!manager) return null;
|
|
@@ -1525,45 +1609,6 @@ export class AgentSession {
|
|
|
1525
1609
|
|
|
1526
1610
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
1527
1611
|
#handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
1528
|
-
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
1529
|
-
// This ensures the UI sees the updated queue state
|
|
1530
|
-
if (event.type === "message_start" && event.message.role === "user") {
|
|
1531
|
-
const messageText = this.#getUserMessageText(event.message);
|
|
1532
|
-
if (messageText) {
|
|
1533
|
-
// Check steering queue first (match by .text on tagged records)
|
|
1534
|
-
const steeringIndex = this.#steeringMessages.findIndex(e => e.text === messageText);
|
|
1535
|
-
if (steeringIndex !== -1) {
|
|
1536
|
-
this.#steeringMessages.splice(steeringIndex, 1);
|
|
1537
|
-
} else {
|
|
1538
|
-
// Check follow-up queue
|
|
1539
|
-
const followUpIndex = this.#followUpMessages.findIndex(e => e.text === messageText);
|
|
1540
|
-
if (followUpIndex !== -1) {
|
|
1541
|
-
this.#followUpMessages.splice(followUpIndex, 1);
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
// Tag-based dequeue for custom messages (skills queued via promptCustomMessage).
|
|
1548
|
-
// The InputController attached a stable tag via CustomMessage.details when it
|
|
1549
|
-
// registered the display chip; pull it back here to remove the matching entry
|
|
1550
|
-
// from the pending bar atomically with the agent's queue consumption. Match by
|
|
1551
|
-
// tag (not text) — two queued skills with identical args cannot collide.
|
|
1552
|
-
if (event.type === "message_start" && event.message.role === "custom") {
|
|
1553
|
-
const tag = readPendingDisplayTag(event.message.details);
|
|
1554
|
-
if (tag) {
|
|
1555
|
-
const steerIdx = this.#steeringMessages.findIndex(e => e.tag === tag);
|
|
1556
|
-
if (steerIdx !== -1) {
|
|
1557
|
-
this.#steeringMessages.splice(steerIdx, 1);
|
|
1558
|
-
} else {
|
|
1559
|
-
const followUpIdx = this.#followUpMessages.findIndex(e => e.tag === tag);
|
|
1560
|
-
if (followUpIdx !== -1) {
|
|
1561
|
-
this.#followUpMessages.splice(followUpIdx, 1);
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
1612
|
// Plan-mode → compaction transition: stamp `SILENT_ABORT_MARKER` on the
|
|
1568
1613
|
// persisted message BEFORE the obfuscator's display-side copy below.
|
|
1569
1614
|
// Invariant (must hold across refactors): this branch precedes the
|
|
@@ -1666,89 +1711,18 @@ export class AgentSession {
|
|
|
1666
1711
|
}
|
|
1667
1712
|
|
|
1668
1713
|
if (matchContext && "delta" in assistantEvent) {
|
|
1714
|
+
const targetMessageTimestamp = event.message.role === "assistant" ? event.message.timestamp : undefined;
|
|
1669
1715
|
const matches = this.#checkTtsrStream(assistantEvent.delta, matchContext, streamingToolCall);
|
|
1670
|
-
if (matches.length > 0) {
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
this.#addPendingTtsrInjections(matches);
|
|
1681
|
-
|
|
1682
|
-
if (shouldInterrupt) {
|
|
1683
|
-
// Abort the stream immediately — do not gate on extension callbacks
|
|
1684
|
-
this.#ttsrAbortPending = true;
|
|
1685
|
-
this.#ensureTtsrResumePromise();
|
|
1686
|
-
this.agent.abort();
|
|
1687
|
-
// Notify extensions (fire-and-forget, does not block abort)
|
|
1688
|
-
this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
|
|
1689
|
-
// Schedule retry after a short delay
|
|
1690
|
-
const retryToken = ++this.#ttsrRetryToken;
|
|
1691
|
-
const generation = this.#promptGeneration;
|
|
1692
|
-
const targetMessageTimestamp =
|
|
1693
|
-
event.message.role === "assistant" ? event.message.timestamp : undefined;
|
|
1694
|
-
this.#schedulePostPromptTask(
|
|
1695
|
-
async () => {
|
|
1696
|
-
if (this.#ttsrRetryToken !== retryToken) {
|
|
1697
|
-
this.#resolveTtsrResume();
|
|
1698
|
-
return;
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
|
|
1702
|
-
if (
|
|
1703
|
-
!this.#ttsrAbortPending ||
|
|
1704
|
-
this.#promptGeneration !== generation ||
|
|
1705
|
-
targetAssistantIndex === -1
|
|
1706
|
-
) {
|
|
1707
|
-
this.#ttsrAbortPending = false;
|
|
1708
|
-
this.#pendingTtsrInjections = [];
|
|
1709
|
-
this.#perToolTtsrInjections.clear();
|
|
1710
|
-
this.#resolveTtsrResume();
|
|
1711
|
-
return;
|
|
1712
|
-
}
|
|
1713
|
-
this.#ttsrAbortPending = false;
|
|
1714
|
-
this.#perToolTtsrInjections.clear();
|
|
1715
|
-
const ttsrSettings = this.#ttsrManager?.getSettings();
|
|
1716
|
-
if (ttsrSettings?.contextMode === "discard") {
|
|
1717
|
-
// Remove the partial/aborted assistant turn from agent state
|
|
1718
|
-
this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
|
|
1719
|
-
}
|
|
1720
|
-
// Inject TTSR rules as system reminder before retry
|
|
1721
|
-
const injection = this.#getTtsrInjectionContent();
|
|
1722
|
-
if (injection) {
|
|
1723
|
-
const details = { rules: injection.rules.map(rule => rule.name) };
|
|
1724
|
-
this.agent.appendMessage({
|
|
1725
|
-
role: "custom",
|
|
1726
|
-
customType: "ttsr-injection",
|
|
1727
|
-
content: injection.content,
|
|
1728
|
-
display: false,
|
|
1729
|
-
details,
|
|
1730
|
-
attribution: "agent",
|
|
1731
|
-
timestamp: Date.now(),
|
|
1732
|
-
});
|
|
1733
|
-
this.sessionManager.appendCustomMessageEntry(
|
|
1734
|
-
"ttsr-injection",
|
|
1735
|
-
injection.content,
|
|
1736
|
-
false,
|
|
1737
|
-
details,
|
|
1738
|
-
"agent",
|
|
1739
|
-
);
|
|
1740
|
-
this.#markTtsrInjected(details.rules);
|
|
1741
|
-
}
|
|
1742
|
-
try {
|
|
1743
|
-
await this.agent.continue();
|
|
1744
|
-
} catch {
|
|
1745
|
-
this.#resolveTtsrResume();
|
|
1746
|
-
}
|
|
1747
|
-
},
|
|
1748
|
-
{ delayMs: 50 },
|
|
1749
|
-
);
|
|
1750
|
-
return;
|
|
1751
|
-
}
|
|
1716
|
+
if (matches.length > 0 && this.#handleTtsrMatches(matches, matchContext, targetMessageTimestamp)) {
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
// ast-grep `astCondition` rules match against the reconstructed edit/write
|
|
1720
|
+
// snapshot, which only exists for tool argument streams. The native worker
|
|
1721
|
+
// call is async, so this path is awaited and self-throttled by the manager.
|
|
1722
|
+
if (matchContext.source === "tool" && this.#ttsrManager?.hasAstRules()) {
|
|
1723
|
+
const astMatches = await this.#checkTtsrAstStream(matchContext, streamingToolCall);
|
|
1724
|
+
if (astMatches.length > 0 && this.#handleTtsrMatches(astMatches, matchContext, targetMessageTimestamp)) {
|
|
1725
|
+
return;
|
|
1752
1726
|
}
|
|
1753
1727
|
}
|
|
1754
1728
|
}
|
|
@@ -1944,6 +1918,11 @@ export class AgentSession {
|
|
|
1944
1918
|
return;
|
|
1945
1919
|
}
|
|
1946
1920
|
|
|
1921
|
+
// A deliberate abort should settle the current turn, not trigger queued continuations.
|
|
1922
|
+
if (msg.stopReason === "aborted") {
|
|
1923
|
+
this.#resolveRetry();
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1947
1926
|
// Check for retryable errors first (overloaded, rate limit, server errors)
|
|
1948
1927
|
if (this.#isRetryableError(msg)) {
|
|
1949
1928
|
const didRetry = await this.#handleRetryableError(msg);
|
|
@@ -1966,7 +1945,7 @@ export class AgentSession {
|
|
|
1966
1945
|
if (compactionDeferredHandoff) {
|
|
1967
1946
|
return;
|
|
1968
1947
|
}
|
|
1969
|
-
if (msg.stopReason !== "error"
|
|
1948
|
+
if (msg.stopReason !== "error") {
|
|
1970
1949
|
if (this.#enforceRewindBeforeYield()) {
|
|
1971
1950
|
return;
|
|
1972
1951
|
}
|
|
@@ -2062,13 +2041,13 @@ export class AgentSession {
|
|
|
2062
2041
|
onError?: () => void;
|
|
2063
2042
|
}): void {
|
|
2064
2043
|
this.#schedulePostPromptTask(
|
|
2065
|
-
async
|
|
2044
|
+
async signal => {
|
|
2066
2045
|
// Defense in depth: if compaction/handoff slipped onto the post-prompt queue
|
|
2067
2046
|
// alongside us (e.g. via a scheduler we don't own), refuse to start a fresh
|
|
2068
2047
|
// streaming turn — agent.continue() here would race the handoff's session
|
|
2069
2048
|
// reset. The first-class fix is in #checkCompaction/the agent_end handler,
|
|
2070
2049
|
// but this guard catches anything that bypasses that path.
|
|
2071
|
-
if (this.isCompacting || this.isGeneratingHandoff) {
|
|
2050
|
+
if (signal.aborted || this.#isDisposed || this.isCompacting || this.isGeneratingHandoff) {
|
|
2072
2051
|
options?.onSkip?.();
|
|
2073
2052
|
return;
|
|
2074
2053
|
}
|
|
@@ -2076,14 +2055,21 @@ export class AgentSession {
|
|
|
2076
2055
|
options?.onSkip?.();
|
|
2077
2056
|
return;
|
|
2078
2057
|
}
|
|
2058
|
+
this.#beginInFlight();
|
|
2079
2059
|
try {
|
|
2080
2060
|
await this.#maybeRestoreRetryFallbackPrimary();
|
|
2061
|
+
if (signal.aborted || this.#isDisposed) {
|
|
2062
|
+
options?.onSkip?.();
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2081
2065
|
await this.agent.continue();
|
|
2082
2066
|
} catch (error) {
|
|
2083
2067
|
logger.warn("agent.continue failed after scheduling", {
|
|
2084
2068
|
error: error instanceof Error ? error.message : String(error),
|
|
2085
2069
|
});
|
|
2086
2070
|
options?.onError?.();
|
|
2071
|
+
} finally {
|
|
2072
|
+
this.#endInFlight();
|
|
2087
2073
|
}
|
|
2088
2074
|
},
|
|
2089
2075
|
{
|
|
@@ -2139,8 +2125,13 @@ export class AgentSession {
|
|
|
2139
2125
|
* and fire-and-forget `agent.continue()` may still be streaming after
|
|
2140
2126
|
* the TTSR resume gate resolves.
|
|
2141
2127
|
*/
|
|
2142
|
-
async #waitForPostPromptRecovery(): Promise<void> {
|
|
2128
|
+
async #waitForPostPromptRecovery(generation?: number): Promise<void> {
|
|
2143
2129
|
while (true) {
|
|
2130
|
+
// An abort bumps #promptGeneration. When this wait runs on behalf of a
|
|
2131
|
+
// specific prompt turn, stop as soon as that turn has been superseded:
|
|
2132
|
+
// its promise must resolve on the abort, not block on a queued
|
|
2133
|
+
// steer/follow-up that the post-abort drain starts as a fresh turn.
|
|
2134
|
+
if (generation !== undefined && this.#promptGeneration !== generation) return;
|
|
2144
2135
|
if (this.#retryPromise) {
|
|
2145
2136
|
await this.#retryPromise;
|
|
2146
2137
|
continue;
|
|
@@ -2164,6 +2155,12 @@ export class AgentSession {
|
|
|
2164
2155
|
}
|
|
2165
2156
|
}
|
|
2166
2157
|
|
|
2158
|
+
#formatTtsrAbortReason(rules: Rule[]): string {
|
|
2159
|
+
const label = rules.length === 1 ? "rule" : "rules";
|
|
2160
|
+
const ruleNames = rules.map(rule => rule.name).join(", ");
|
|
2161
|
+
return `TTSR matched ${label}: ${ruleNames}`;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2167
2164
|
/** Get TTSR injection payload and clear pending injections. */
|
|
2168
2165
|
#getTtsrInjectionContent(): { content: string; rules: Rule[] } | undefined {
|
|
2169
2166
|
if (this.#pendingTtsrInjections.length === 0) return undefined;
|
|
@@ -2187,13 +2184,20 @@ export class AgentSession {
|
|
|
2187
2184
|
* project, `~`-relative when it lives under home, else the raw path.
|
|
2188
2185
|
*/
|
|
2189
2186
|
#displayRulePath(rulePath: string): string {
|
|
2190
|
-
const cwdRel =
|
|
2187
|
+
const cwdRel =
|
|
2188
|
+
relativePathWithinRoot(this.sessionManager.getCwd(), rulePath) ??
|
|
2189
|
+
this.#displayPathWithinRoot(this.sessionManager.getCwd(), rulePath);
|
|
2191
2190
|
if (cwdRel) return cwdRel;
|
|
2192
2191
|
const homeRel = relativePathWithinRoot(os.homedir(), rulePath);
|
|
2193
2192
|
if (homeRel) return `~/${homeRel}`;
|
|
2194
2193
|
return rulePath;
|
|
2195
2194
|
}
|
|
2196
2195
|
|
|
2196
|
+
#displayPathWithinRoot(root: string, candidate: string): string | null {
|
|
2197
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
2198
|
+
return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : null;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2197
2201
|
#addPendingTtsrInjections(rules: Rule[]): void {
|
|
2198
2202
|
const seen = new Set(this.#pendingTtsrInjections.map(rule => rule.name));
|
|
2199
2203
|
for (const rule of rules) {
|
|
@@ -2407,19 +2411,134 @@ export class AgentSession {
|
|
|
2407
2411
|
if (!manager) {
|
|
2408
2412
|
return [];
|
|
2409
2413
|
}
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
tools.find(t => t.name === toolCall.name) ??
|
|
2414
|
-
tools.find(t => t.customWireName !== undefined && t.customWireName === toolCall.name);
|
|
2415
|
-
const digest = tool?.matcherDigest?.(toolCall.arguments ?? {});
|
|
2416
|
-
if (digest !== undefined) {
|
|
2417
|
-
return manager.checkSnapshot(digest, matchContext);
|
|
2418
|
-
}
|
|
2414
|
+
const digest = this.#resolveTtsrMatcherDigest(toolCall);
|
|
2415
|
+
if (digest !== undefined) {
|
|
2416
|
+
return manager.checkSnapshot(digest, matchContext);
|
|
2419
2417
|
}
|
|
2420
2418
|
return manager.checkDelta(delta, matchContext);
|
|
2421
2419
|
}
|
|
2422
2420
|
|
|
2421
|
+
/** Reconstruct the tool's normalized source snapshot via its `matcherDigest`, if any. */
|
|
2422
|
+
#resolveTtsrMatcherDigest(toolCall: ToolCall | undefined): string | undefined {
|
|
2423
|
+
if (!toolCall) {
|
|
2424
|
+
return undefined;
|
|
2425
|
+
}
|
|
2426
|
+
const tools = this.agent.state.tools;
|
|
2427
|
+
const tool =
|
|
2428
|
+
tools.find(t => t.name === toolCall.name) ??
|
|
2429
|
+
tools.find(t => t.customWireName !== undefined && t.customWireName === toolCall.name);
|
|
2430
|
+
return tool?.matcherDigest?.(toolCall.arguments ?? {});
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
/**
|
|
2434
|
+
* Match ast-grep `astCondition` rules against the reconstructed tool snapshot.
|
|
2435
|
+
*
|
|
2436
|
+
* Only edit/write tool streams expose a `matcherDigest`, which is the real source
|
|
2437
|
+
* the call introduces; AST matching needs that (and a language inferred from the
|
|
2438
|
+
* path argument), so non-digest streams never produce AST matches.
|
|
2439
|
+
*/
|
|
2440
|
+
async #checkTtsrAstStream(matchContext: TtsrMatchContext, toolCall: ToolCall | undefined): Promise<Rule[]> {
|
|
2441
|
+
const manager = this.#ttsrManager;
|
|
2442
|
+
if (!manager) {
|
|
2443
|
+
return [];
|
|
2444
|
+
}
|
|
2445
|
+
const digest = this.#resolveTtsrMatcherDigest(toolCall);
|
|
2446
|
+
if (digest === undefined) {
|
|
2447
|
+
return [];
|
|
2448
|
+
}
|
|
2449
|
+
return manager.checkAstSnapshot(digest, matchContext);
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
/**
|
|
2453
|
+
* Route TTSR matches to either a per-tool injection or a stream-interrupting
|
|
2454
|
+
* retry. Returns true when the stream was aborted and the caller should stop
|
|
2455
|
+
* processing this event.
|
|
2456
|
+
*/
|
|
2457
|
+
#handleTtsrMatches(
|
|
2458
|
+
matches: Rule[],
|
|
2459
|
+
matchContext: TtsrMatchContext,
|
|
2460
|
+
targetMessageTimestamp: number | undefined,
|
|
2461
|
+
): boolean {
|
|
2462
|
+
// Decide first: a non-interrupting tool-source match attaches to the
|
|
2463
|
+
// specific tool call's result instead of driving a loop-wide follow-up.
|
|
2464
|
+
const shouldInterrupt = this.#shouldInterruptForTtsrMatch(matches, matchContext);
|
|
2465
|
+
const perToolId = shouldInterrupt ? undefined : this.#extractTtsrToolCallId(matchContext);
|
|
2466
|
+
if (perToolId) {
|
|
2467
|
+
this.#addPerToolTtsrInjections(perToolId, matches);
|
|
2468
|
+
this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
|
|
2469
|
+
return false;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// Queue rules for injection; mark as injected only after successful enqueue.
|
|
2473
|
+
this.#addPendingTtsrInjections(matches);
|
|
2474
|
+
if (!shouldInterrupt) {
|
|
2475
|
+
return false;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
// Abort the stream immediately — do not gate on extension callbacks
|
|
2479
|
+
this.#ttsrAbortPending = true;
|
|
2480
|
+
this.#ensureTtsrResumePromise();
|
|
2481
|
+
this.agent.abort(this.#formatTtsrAbortReason(matches));
|
|
2482
|
+
// Notify extensions (fire-and-forget, does not block abort)
|
|
2483
|
+
this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
|
|
2484
|
+
// Schedule retry after a short delay
|
|
2485
|
+
const retryToken = ++this.#ttsrRetryToken;
|
|
2486
|
+
const generation = this.#promptGeneration;
|
|
2487
|
+
this.#schedulePostPromptTask(
|
|
2488
|
+
async () => {
|
|
2489
|
+
if (this.#ttsrRetryToken !== retryToken) {
|
|
2490
|
+
this.#resolveTtsrResume();
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
|
|
2495
|
+
if (!this.#ttsrAbortPending || this.#promptGeneration !== generation || targetAssistantIndex === -1) {
|
|
2496
|
+
this.#ttsrAbortPending = false;
|
|
2497
|
+
this.#pendingTtsrInjections = [];
|
|
2498
|
+
this.#perToolTtsrInjections.clear();
|
|
2499
|
+
this.#resolveTtsrResume();
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
this.#ttsrAbortPending = false;
|
|
2503
|
+
this.#perToolTtsrInjections.clear();
|
|
2504
|
+
const ttsrSettings = this.#ttsrManager?.getSettings();
|
|
2505
|
+
if (ttsrSettings?.contextMode === "discard") {
|
|
2506
|
+
// Remove the partial/aborted assistant turn from agent state
|
|
2507
|
+
this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
|
|
2508
|
+
}
|
|
2509
|
+
// Inject TTSR rules as system reminder before retry
|
|
2510
|
+
const injection = this.#getTtsrInjectionContent();
|
|
2511
|
+
if (injection) {
|
|
2512
|
+
const details = { rules: injection.rules.map(rule => rule.name) };
|
|
2513
|
+
this.agent.appendMessage({
|
|
2514
|
+
role: "custom",
|
|
2515
|
+
customType: "ttsr-injection",
|
|
2516
|
+
content: injection.content,
|
|
2517
|
+
display: false,
|
|
2518
|
+
details,
|
|
2519
|
+
attribution: "agent",
|
|
2520
|
+
timestamp: Date.now(),
|
|
2521
|
+
});
|
|
2522
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
2523
|
+
"ttsr-injection",
|
|
2524
|
+
injection.content,
|
|
2525
|
+
false,
|
|
2526
|
+
details,
|
|
2527
|
+
"agent",
|
|
2528
|
+
);
|
|
2529
|
+
this.#markTtsrInjected(details.rules);
|
|
2530
|
+
}
|
|
2531
|
+
try {
|
|
2532
|
+
await this.agent.continue();
|
|
2533
|
+
} catch {
|
|
2534
|
+
this.#resolveTtsrResume();
|
|
2535
|
+
}
|
|
2536
|
+
},
|
|
2537
|
+
{ delayMs: 50 },
|
|
2538
|
+
);
|
|
2539
|
+
return true;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2423
2542
|
/** Extract path-like arguments from tool call payload for TTSR glob matching. */
|
|
2424
2543
|
#extractTtsrFilePathsFromArgs(args: unknown): string[] | undefined {
|
|
2425
2544
|
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
@@ -2474,18 +2593,6 @@ export class AgentSession {
|
|
|
2474
2593
|
|
|
2475
2594
|
return Array.from(candidates);
|
|
2476
2595
|
}
|
|
2477
|
-
/** Extract text content from a message */
|
|
2478
|
-
#getUserMessageText(message: Message): string {
|
|
2479
|
-
if (message.role !== "user") return "";
|
|
2480
|
-
const content = message.content;
|
|
2481
|
-
if (typeof content === "string") return content;
|
|
2482
|
-
const textBlocks = content.filter(c => c.type === "text");
|
|
2483
|
-
const text = textBlocks.map(c => (c as TextContent).text).join("");
|
|
2484
|
-
if (text.length > 0) return text;
|
|
2485
|
-
const hasImages = content.some(c => c.type === "image");
|
|
2486
|
-
return hasImages ? "[Image]" : "";
|
|
2487
|
-
}
|
|
2488
|
-
|
|
2489
2596
|
/** Find the last assistant message in agent state (including aborted ones) */
|
|
2490
2597
|
#findLastAssistantMessage(): AssistantMessage | undefined {
|
|
2491
2598
|
const messages = this.agent.state.messages;
|
|
@@ -2927,6 +3034,27 @@ export class AgentSession {
|
|
|
2927
3034
|
};
|
|
2928
3035
|
}
|
|
2929
3036
|
|
|
3037
|
+
subscribeCommandMetadataChanged(listener: CommandMetadataChangedListener): () => void {
|
|
3038
|
+
this.#commandMetadataChangedListeners.push(listener);
|
|
3039
|
+
return () => {
|
|
3040
|
+
const index = this.#commandMetadataChangedListeners.indexOf(listener);
|
|
3041
|
+
if (index !== -1) {
|
|
3042
|
+
this.#commandMetadataChangedListeners.splice(index, 1);
|
|
3043
|
+
}
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
#notifyCommandMetadataChanged(): void {
|
|
3048
|
+
const listeners = [...this.#commandMetadataChangedListeners];
|
|
3049
|
+
for (const listener of listeners) {
|
|
3050
|
+
try {
|
|
3051
|
+
void listener();
|
|
3052
|
+
} catch (err) {
|
|
3053
|
+
logger.error("Command metadata listener threw", { err });
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
|
|
2930
3058
|
/**
|
|
2931
3059
|
* Temporarily disconnect from agent events.
|
|
2932
3060
|
* User listeners are preserved and will receive events again after resubscribe().
|
|
@@ -2948,19 +3076,23 @@ export class AgentSession {
|
|
|
2948
3076
|
this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
|
|
2949
3077
|
}
|
|
2950
3078
|
|
|
3079
|
+
#activeProviderSessionId(sessionId?: string): string {
|
|
3080
|
+
return this.#freshProviderSessionId ?? this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
|
|
3081
|
+
}
|
|
3082
|
+
|
|
2951
3083
|
/**
|
|
2952
3084
|
* Set agent.sessionId from the session manager and install a dynamic
|
|
2953
3085
|
* metadata resolver so every Anthropic API request carries
|
|
2954
3086
|
* `metadata.user_id` shaped like real Claude Code's `getAPIMetadata` output:
|
|
2955
3087
|
* `{ session_id, account_uuid, device_id }`. `account_uuid` is included only
|
|
2956
3088
|
* when an Anthropic OAuth credential with a known account UUID is loaded;
|
|
2957
|
-
* `device_id` is derived from the persistent prometheus install id
|
|
2958
|
-
* keeps the value in sync with auth-state changes
|
|
2959
|
-
* refresh that surfaces a new account
|
|
2960
|
-
* `#syncAgentSessionId()` on every such event.
|
|
3089
|
+
* `device_id` is derived from both the persistent prometheus install id and that
|
|
3090
|
+
* account UUID. Resolving live keeps the value in sync with auth-state changes
|
|
3091
|
+
* (login/logout, token refresh that surfaces a new account UUID) without
|
|
3092
|
+
* needing to re-call `#syncAgentSessionId()` on every such event.
|
|
2961
3093
|
*/
|
|
2962
3094
|
#syncAgentSessionId(sessionId?: string): void {
|
|
2963
|
-
const sid = this.#
|
|
3095
|
+
const sid = this.#activeProviderSessionId(sessionId);
|
|
2964
3096
|
this.agent.sessionId = sid;
|
|
2965
3097
|
this.agent.setMetadataResolver((provider: string) =>
|
|
2966
3098
|
buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage),
|
|
@@ -2968,14 +3100,14 @@ export class AgentSession {
|
|
|
2968
3100
|
}
|
|
2969
3101
|
|
|
2970
3102
|
#rekeyHindsightMemoryForCurrentSessionId(): void {
|
|
2971
|
-
if (
|
|
3103
|
+
if (this.settings.get("memory.backend") !== "hindsight") return;
|
|
2972
3104
|
const sid = this.agent.sessionId;
|
|
2973
3105
|
if (!sid) return;
|
|
2974
3106
|
this.getHindsightSessionState()?.setSessionId(sid);
|
|
2975
3107
|
}
|
|
2976
3108
|
|
|
2977
3109
|
#rekeyMnemopiMemoryForCurrentSessionId(): void {
|
|
2978
|
-
if (
|
|
3110
|
+
if (this.settings.get("memory.backend") !== "mnemopi") return;
|
|
2979
3111
|
const sid = this.agent.sessionId;
|
|
2980
3112
|
if (!sid) return;
|
|
2981
3113
|
this.getMnemopiSessionState()?.setSessionId(sid);
|
|
@@ -2983,29 +3115,48 @@ export class AgentSession {
|
|
|
2983
3115
|
|
|
2984
3116
|
/** New session file: reset auto-recall / retain-threshold counters for the new transcript. */
|
|
2985
3117
|
#resetHindsightConversationTrackingIfHindsight(): void {
|
|
2986
|
-
if (
|
|
3118
|
+
if (this.settings.get("memory.backend") !== "hindsight") return;
|
|
2987
3119
|
const state = this.getHindsightSessionState();
|
|
2988
3120
|
if (!state || state.aliasOf) return;
|
|
2989
3121
|
state.resetConversationTracking();
|
|
2990
3122
|
}
|
|
2991
3123
|
|
|
2992
3124
|
#resetMnemopiConversationTrackingIfMnemopi(): void {
|
|
2993
|
-
if (
|
|
3125
|
+
if (this.settings.get("memory.backend") !== "mnemopi") return;
|
|
2994
3126
|
const state = this.getMnemopiSessionState();
|
|
2995
3127
|
if (!state || state.aliasOf) return;
|
|
2996
3128
|
state.resetConversationTracking();
|
|
2997
3129
|
}
|
|
2998
3130
|
|
|
3131
|
+
/** True once dispose() has begun; deferred background work (e.g. the deferred
|
|
3132
|
+
* MCP discovery task in sdk.ts) must not touch the session past this point. */
|
|
3133
|
+
get isDisposed(): boolean {
|
|
3134
|
+
return this.#isDisposed;
|
|
3135
|
+
}
|
|
3136
|
+
|
|
2999
3137
|
/**
|
|
3000
|
-
*
|
|
3001
|
-
*
|
|
3138
|
+
* Synchronously mark the session as disposing so new work is rejected
|
|
3139
|
+
* immediately: Python/eval starts throw, queued asides are dropped, and the
|
|
3140
|
+
* aside provider is detached. Idempotent; `dispose()` runs it first.
|
|
3141
|
+
*
|
|
3142
|
+
* Wrappers that await other teardown before delegating to `dispose()` MUST
|
|
3143
|
+
* call this before their first await — otherwise work started in that async
|
|
3144
|
+
* gap slips past the disposal guards.
|
|
3002
3145
|
*/
|
|
3003
|
-
|
|
3146
|
+
beginDispose(): void {
|
|
3004
3147
|
this.#isDisposed = true;
|
|
3005
|
-
this.#
|
|
3148
|
+
this.#pendingIrcAsides = [];
|
|
3006
3149
|
this.yieldQueue.clear();
|
|
3007
|
-
this.agent.
|
|
3150
|
+
this.agent.setAsideMessageProvider(undefined);
|
|
3008
3151
|
this.#evalExecutionDisposing = true;
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
/**
|
|
3155
|
+
* Remove all listeners, flush pending writes, and disconnect from agent.
|
|
3156
|
+
* Call this when completely done with the session.
|
|
3157
|
+
*/
|
|
3158
|
+
async dispose(): Promise<void> {
|
|
3159
|
+
this.beginDispose();
|
|
3009
3160
|
try {
|
|
3010
3161
|
if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
|
|
3011
3162
|
await this.#extensionRunner.emit({ type: "session_shutdown" });
|
|
@@ -3027,8 +3178,9 @@ export class AgentSession {
|
|
|
3027
3178
|
// session's dispose.
|
|
3028
3179
|
this.abortRetry();
|
|
3029
3180
|
this.abortCompaction();
|
|
3181
|
+
const postPromptDrain = this.#cancelPostPromptTasks();
|
|
3030
3182
|
this.agent.abort();
|
|
3031
|
-
await
|
|
3183
|
+
await postPromptDrain;
|
|
3032
3184
|
// Cancel jobs this agent registered so a subagent's teardown doesn't
|
|
3033
3185
|
// leak its background bash/task work into the parent's manager. Only
|
|
3034
3186
|
// the session that owns the manager goes on to dispose it (which itself
|
|
@@ -3065,7 +3217,7 @@ export class AgentSession {
|
|
|
3065
3217
|
this.setHindsightSessionState(undefined);
|
|
3066
3218
|
hindsightState?.dispose();
|
|
3067
3219
|
const mnemopiState = setMnemopiSessionState(this, undefined);
|
|
3068
|
-
mnemopiState?.dispose();
|
|
3220
|
+
await mnemopiState?.dispose();
|
|
3069
3221
|
this.#disconnectFromAgent();
|
|
3070
3222
|
if (this.#unsubscribeAppendOnly) {
|
|
3071
3223
|
this.#unsubscribeAppendOnly();
|
|
@@ -3090,6 +3242,23 @@ export class AgentSession {
|
|
|
3090
3242
|
this.#providerSessionState.clear();
|
|
3091
3243
|
}
|
|
3092
3244
|
|
|
3245
|
+
freshSession(): FreshSessionResult | undefined {
|
|
3246
|
+
if (this.isStreaming) return undefined;
|
|
3247
|
+
const previousSessionId = this.sessionId;
|
|
3248
|
+
const closedProviderSessions = this.#providerSessionState.size;
|
|
3249
|
+
this.#closeAllProviderSessions("fresh session");
|
|
3250
|
+
this.#freshProviderSessionId = Bun.randomUUIDv7();
|
|
3251
|
+
this.#syncAgentSessionId();
|
|
3252
|
+
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
3253
|
+
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
|
3254
|
+
this.agent.appendOnlyContext?.invalidateForModelChange();
|
|
3255
|
+
return {
|
|
3256
|
+
previousSessionId,
|
|
3257
|
+
sessionId: this.sessionId,
|
|
3258
|
+
closedProviderSessions,
|
|
3259
|
+
};
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3093
3262
|
// =========================================================================
|
|
3094
3263
|
// Read-only State Access
|
|
3095
3264
|
// =========================================================================
|
|
@@ -3133,6 +3302,10 @@ export class AgentSession {
|
|
|
3133
3302
|
return this.agent.state.isStreaming || this.#promptInFlightCount > 0;
|
|
3134
3303
|
}
|
|
3135
3304
|
|
|
3305
|
+
get isAborting(): boolean {
|
|
3306
|
+
return this.agent.isAborting;
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3136
3309
|
/** Wait until streaming and deferred recovery work are fully settled. */
|
|
3137
3310
|
async waitForIdle(): Promise<void> {
|
|
3138
3311
|
await this.agent.waitForIdle();
|
|
@@ -3299,6 +3472,17 @@ export class AgentSession {
|
|
|
3299
3472
|
return this.#mcpDiscoveryEnabled;
|
|
3300
3473
|
}
|
|
3301
3474
|
|
|
3475
|
+
/**
|
|
3476
|
+
* Flip MCP discovery on after deferred discovery learns the real tool count.
|
|
3477
|
+
* UI sessions resolve `tools.discoveryMode: "auto"` before MCP servers
|
|
3478
|
+
* connect, so a large MCP toolset discovered later must be able to upgrade
|
|
3479
|
+
* the session from the force-activate path to the discovery path. One-way:
|
|
3480
|
+
* discovery is never downgraded mid-session.
|
|
3481
|
+
*/
|
|
3482
|
+
enableMCPDiscovery(): void {
|
|
3483
|
+
this.#mcpDiscoveryEnabled = true;
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3302
3486
|
getSelectedMCPToolNames(): string[] {
|
|
3303
3487
|
if (!this.#mcpDiscoveryEnabled) {
|
|
3304
3488
|
return this.getActiveToolNames().filter(name => isMCPToolName(name) && this.#toolRegistry.has(name));
|
|
@@ -3430,12 +3614,26 @@ export class AgentSession {
|
|
|
3430
3614
|
* Wrap a tool with a permission-gate proxy when an ACP client is connected.
|
|
3431
3615
|
* Only wraps tools whose name is in PERMISSION_REQUIRED_TOOLS and only when
|
|
3432
3616
|
* the bridge exposes `requestPermission`. No-ops for all other cases.
|
|
3617
|
+
*
|
|
3618
|
+
* When the user has explicitly opted into `yolo` / auto-approve behavior (via
|
|
3619
|
+
* the SDK/CLI `autoApprove` flag or a configured `tools.approvalMode: yolo`),
|
|
3620
|
+
* skips the gate unless the per-tool policy explicitly requires a prompt or
|
|
3621
|
+
* deny. The schema default is also `yolo`, so an explicit configuration or
|
|
3622
|
+
* explicit session flag is required: default-config ACP sessions keep the
|
|
3623
|
+
* client-side permission gate.
|
|
3433
3624
|
*/
|
|
3434
3625
|
#wrapToolForAcpPermission<T extends AgentTool>(tool: T): T {
|
|
3435
3626
|
const bridge = this.#clientBridge;
|
|
3436
3627
|
// Match the capability+method gating pattern used by read/write/bash.
|
|
3437
3628
|
if (!bridge?.capabilities.requestPermission || !bridge.requestPermission) return tool;
|
|
3438
3629
|
if (!PERMISSION_REQUIRED_TOOLS.has(tool.name)) return tool;
|
|
3630
|
+
// Skip the gate only on explicit yolo opt-in; honour per-tool policies
|
|
3631
|
+
// that require a prompt or deny (matching the normal approval wrapper).
|
|
3632
|
+
if (this.#isExplicitAutoApproveMode()) {
|
|
3633
|
+
const userPolicies = (this.settings.get("tools.approval") ?? {}) as Record<string, unknown>;
|
|
3634
|
+
const toolPolicy = userPolicies[tool.name];
|
|
3635
|
+
if (!toolPolicy || toolPolicy === "allow") return tool;
|
|
3636
|
+
}
|
|
3439
3637
|
return new Proxy(tool, {
|
|
3440
3638
|
get: (target, prop) => {
|
|
3441
3639
|
if (prop !== "execute") return Reflect.get(target, prop, target);
|
|
@@ -3523,6 +3721,13 @@ export class AgentSession {
|
|
|
3523
3721
|
}) as T;
|
|
3524
3722
|
}
|
|
3525
3723
|
|
|
3724
|
+
#isExplicitAutoApproveMode(): boolean {
|
|
3725
|
+
return (
|
|
3726
|
+
this.#autoApprove ||
|
|
3727
|
+
(this.settings.isConfigured("tools.approvalMode") && this.settings.get("tools.approvalMode") === "yolo")
|
|
3728
|
+
);
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3526
3731
|
async #applyActiveToolsByName(
|
|
3527
3732
|
toolNames: string[],
|
|
3528
3733
|
options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
|
|
@@ -3671,7 +3876,7 @@ export class AgentSession {
|
|
|
3671
3876
|
}
|
|
3672
3877
|
|
|
3673
3878
|
async #buildSystemPromptForAgentStart(promptText: string): Promise<string[]> {
|
|
3674
|
-
const backend = resolveMemoryBackend(this.settings);
|
|
3879
|
+
const backend = await resolveMemoryBackend(this.settings);
|
|
3675
3880
|
if (!backend.beforeAgentStartPrompt) return this.#baseSystemPrompt;
|
|
3676
3881
|
|
|
3677
3882
|
try {
|
|
@@ -3900,6 +4105,57 @@ export class AgentSession {
|
|
|
3900
4105
|
return deobfuscateSessionContext(this.sessionManager.buildSessionContext(), this.#obfuscator);
|
|
3901
4106
|
}
|
|
3902
4107
|
|
|
4108
|
+
/**
|
|
4109
|
+
* Full-history transcript for TUI display: every path entry in
|
|
4110
|
+
* chronological order with compactions rendered inline at the point they
|
|
4111
|
+
* fired (instead of replacing prior history). Display-only — NEVER feed
|
|
4112
|
+
* the result to `agent.replaceMessages` or a provider.
|
|
4113
|
+
*/
|
|
4114
|
+
buildTranscriptSessionContext(): SessionContext {
|
|
4115
|
+
return deobfuscateSessionContext(this.sessionManager.buildSessionContext({ transcript: true }), this.#obfuscator);
|
|
4116
|
+
}
|
|
4117
|
+
|
|
4118
|
+
#obfuscateForProvider<T>(value: T): T {
|
|
4119
|
+
if (!this.#obfuscator?.hasSecrets()) return value;
|
|
4120
|
+
return this.#obfuscator.obfuscateObject(value);
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
#obfuscateTextForProvider(text: string | undefined): string | undefined {
|
|
4124
|
+
if (!text || !this.#obfuscator?.hasSecrets()) return text;
|
|
4125
|
+
return this.#obfuscator.obfuscate(text);
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
#obfuscatePreparationForProvider(preparation: CompactionPreparation): CompactionPreparation {
|
|
4129
|
+
if (!this.#obfuscator?.hasSecrets()) return preparation;
|
|
4130
|
+
if (!preparation.previousSummary && !preparation.previousPreserveData) return preparation;
|
|
4131
|
+
return {
|
|
4132
|
+
...preparation,
|
|
4133
|
+
previousSummary: preparation.previousSummary
|
|
4134
|
+
? this.#obfuscator.obfuscate(preparation.previousSummary)
|
|
4135
|
+
: preparation.previousSummary,
|
|
4136
|
+
previousPreserveData: preparation.previousPreserveData
|
|
4137
|
+
? this.#obfuscator.obfuscateObject(preparation.previousPreserveData)
|
|
4138
|
+
: preparation.previousPreserveData,
|
|
4139
|
+
};
|
|
4140
|
+
}
|
|
4141
|
+
|
|
4142
|
+
#deobfuscateFromProvider(text: string): string {
|
|
4143
|
+
if (!this.#obfuscator?.hasSecrets()) return text;
|
|
4144
|
+
return this.#obfuscator.deobfuscate(text);
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4147
|
+
#deobfuscatedProviderTextReadyForDelta(text: string): string {
|
|
4148
|
+
const deobfuscated = this.#deobfuscateFromProvider(text);
|
|
4149
|
+
if (!this.#obfuscator?.hasSecrets()) return deobfuscated;
|
|
4150
|
+
const pendingPlaceholderStart = deobfuscated.match(/#[A-Z0-9]{0,4}$/);
|
|
4151
|
+
if (pendingPlaceholderStart?.index === undefined) return deobfuscated;
|
|
4152
|
+
return deobfuscated.slice(0, pendingPlaceholderStart.index);
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4155
|
+
#convertToLlmForSideRequest(messages: AgentMessage[]): Message[] {
|
|
4156
|
+
return this.#obfuscateForProvider(convertToLlm(messages));
|
|
4157
|
+
}
|
|
4158
|
+
|
|
3903
4159
|
/** Convert session messages using the same pre-LLM pipeline as the active session. */
|
|
3904
4160
|
async convertMessagesToLlm(messages: AgentMessage[], signal?: AbortSignal): Promise<Message[]> {
|
|
3905
4161
|
const transformedMessages = await this.#transformContext(messages, signal);
|
|
@@ -3994,7 +4250,7 @@ export class AgentSession {
|
|
|
3994
4250
|
|
|
3995
4251
|
/** Current session ID */
|
|
3996
4252
|
get sessionId(): string {
|
|
3997
|
-
return this.#
|
|
4253
|
+
return this.#activeProviderSessionId();
|
|
3998
4254
|
}
|
|
3999
4255
|
getEvalSessionId(): string | null {
|
|
4000
4256
|
if (this.#parentEvalSessionId !== undefined) return this.#parentEvalSessionId;
|
|
@@ -4137,9 +4393,15 @@ export class AgentSession {
|
|
|
4137
4393
|
return [...this.#customCommands, ...this.#mcpPromptCommands];
|
|
4138
4394
|
}
|
|
4139
4395
|
|
|
4396
|
+
/** MCP prompt commands only, for command-list metadata. */
|
|
4397
|
+
get mcpPromptCommands(): ReadonlyArray<LoadedCustomCommand> {
|
|
4398
|
+
return this.#mcpPromptCommands;
|
|
4399
|
+
}
|
|
4400
|
+
|
|
4140
4401
|
/** Update the MCP prompt commands list. Called when server prompts are (re)loaded. */
|
|
4141
4402
|
setMCPPromptCommands(commands: LoadedCustomCommand[]): void {
|
|
4142
4403
|
this.#mcpPromptCommands = commands;
|
|
4404
|
+
this.#notifyCommandMetadataChanged();
|
|
4143
4405
|
}
|
|
4144
4406
|
|
|
4145
4407
|
// =========================================================================
|
|
@@ -4231,6 +4493,73 @@ export class AgentSession {
|
|
|
4231
4493
|
};
|
|
4232
4494
|
}
|
|
4233
4495
|
|
|
4496
|
+
#normalizeImagesForModel(images: ImageContent[] | undefined): Promise<ImageContent[] | undefined> {
|
|
4497
|
+
return normalizeModelContextImages(images, { model: this.model });
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4500
|
+
async #normalizeMessageContentImages(
|
|
4501
|
+
content: string | (TextContent | ImageContent)[],
|
|
4502
|
+
): Promise<string | (TextContent | ImageContent)[]> {
|
|
4503
|
+
if (typeof content === "string") return content;
|
|
4504
|
+
const images = content.filter((part): part is ImageContent => part.type === "image");
|
|
4505
|
+
if (images.length === 0) return content;
|
|
4506
|
+
const normalizedImages = await this.#normalizeImagesForModel(images);
|
|
4507
|
+
if (!normalizedImages) return content;
|
|
4508
|
+
let imageIndex = 0;
|
|
4509
|
+
return content.map(part => (part.type === "image" ? normalizedImages[imageIndex++]! : part));
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
async #normalizeAgentMessageImages<T extends AgentMessage>(message: T): Promise<T> {
|
|
4513
|
+
if (!("content" in message)) return message;
|
|
4514
|
+
const content = message.content;
|
|
4515
|
+
if (typeof content !== "string" && !Array.isArray(content)) return message;
|
|
4516
|
+
const normalized = await this.#normalizeMessageContentImages(content as string | (TextContent | ImageContent)[]);
|
|
4517
|
+
if (normalized === content) return message;
|
|
4518
|
+
return { ...message, content: normalized } as T;
|
|
4519
|
+
}
|
|
4520
|
+
|
|
4521
|
+
#magicKeywordEnabled(keyword: "orchestrate" | "ultrathink" | "workflow"): boolean {
|
|
4522
|
+
return this.settings.get("magicKeywords.enabled") && this.settings.get(`magicKeywords.${keyword}`);
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4525
|
+
#createMagicKeywordNotices(text: string): CustomMessage[] {
|
|
4526
|
+
const timestamp = Date.now();
|
|
4527
|
+
const turnBudget = parseTurnBudget(text);
|
|
4528
|
+
this.sessionManager.beginTurnBudget(turnBudget?.total ?? null, turnBudget?.hard ?? false);
|
|
4529
|
+
const keywordNotices: CustomMessage[] = [];
|
|
4530
|
+
if (this.#magicKeywordEnabled("ultrathink") && containsUltrathink(text)) {
|
|
4531
|
+
keywordNotices.push({
|
|
4532
|
+
role: "custom",
|
|
4533
|
+
customType: "ultrathink-notice",
|
|
4534
|
+
content: ULTRATHINK_NOTICE,
|
|
4535
|
+
display: false,
|
|
4536
|
+
attribution: "user",
|
|
4537
|
+
timestamp,
|
|
4538
|
+
});
|
|
4539
|
+
}
|
|
4540
|
+
if (this.#magicKeywordEnabled("orchestrate") && containsOrchestrate(text)) {
|
|
4541
|
+
keywordNotices.push({
|
|
4542
|
+
role: "custom",
|
|
4543
|
+
customType: "orchestrate-notice",
|
|
4544
|
+
content: ORCHESTRATE_NOTICE,
|
|
4545
|
+
display: false,
|
|
4546
|
+
attribution: "user",
|
|
4547
|
+
timestamp,
|
|
4548
|
+
});
|
|
4549
|
+
}
|
|
4550
|
+
if (this.#magicKeywordEnabled("workflow") && containsWorkflow(text)) {
|
|
4551
|
+
keywordNotices.push({
|
|
4552
|
+
role: "custom",
|
|
4553
|
+
customType: "workflow-notice",
|
|
4554
|
+
content: WORKFLOW_NOTICE,
|
|
4555
|
+
display: false,
|
|
4556
|
+
attribution: "user",
|
|
4557
|
+
timestamp,
|
|
4558
|
+
});
|
|
4559
|
+
}
|
|
4560
|
+
return keywordNotices;
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4234
4563
|
/**
|
|
4235
4564
|
* Send a prompt to the agent.
|
|
4236
4565
|
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
|
@@ -4240,21 +4569,28 @@ export class AgentSession {
|
|
|
4240
4569
|
* @throws Error if streaming and no streamingBehavior specified
|
|
4241
4570
|
* @throws Error if no model selected or no API key available (when not streaming)
|
|
4242
4571
|
*/
|
|
4243
|
-
|
|
4572
|
+
/**
|
|
4573
|
+
* Returns `false` when the command was fully handled locally (extension or
|
|
4574
|
+
* custom-TS command consumed without calling the LLM). Returns `true` when
|
|
4575
|
+
* the prompt was forwarded to the agent — either directly or queued as a
|
|
4576
|
+
* steer/follow-up. Callers that render a UI or manage turn lifecycle (e.g.
|
|
4577
|
+
* the ACP agent) use this to know whether to expect an `agent_end` event.
|
|
4578
|
+
*/
|
|
4579
|
+
async prompt(text: string, options?: PromptOptions): Promise<boolean> {
|
|
4244
4580
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
4245
4581
|
|
|
4246
4582
|
// Handle extension commands first (execute immediately, even during streaming)
|
|
4247
4583
|
if (expandPromptTemplates && text.startsWith("/")) {
|
|
4248
4584
|
const handled = await this.#tryExecuteExtensionCommand(text);
|
|
4249
4585
|
if (handled) {
|
|
4250
|
-
return;
|
|
4586
|
+
return false;
|
|
4251
4587
|
}
|
|
4252
4588
|
|
|
4253
4589
|
// Try custom commands (TypeScript slash commands)
|
|
4254
4590
|
const customResult = await this.#tryExecuteCustomCommand(text);
|
|
4255
4591
|
if (customResult !== null) {
|
|
4256
4592
|
if (customResult === "") {
|
|
4257
|
-
return;
|
|
4593
|
+
return false;
|
|
4258
4594
|
}
|
|
4259
4595
|
text = customResult;
|
|
4260
4596
|
}
|
|
@@ -4272,42 +4608,7 @@ export class AgentSession {
|
|
|
4272
4608
|
// Magic keywords ("ultrathink", "orchestrate"): append hidden system notices after the
|
|
4273
4609
|
// user's message that steer this turn. User-authored prompts only — synthetic /
|
|
4274
4610
|
// agent-initiated turns never trigger them.
|
|
4275
|
-
const keywordNotices
|
|
4276
|
-
if (!options?.synthetic) {
|
|
4277
|
-
const timestamp = Date.now();
|
|
4278
|
-
const turnBudget = parseTurnBudget(expandedText);
|
|
4279
|
-
this.sessionManager.beginTurnBudget(turnBudget?.total ?? null, turnBudget?.hard ?? false);
|
|
4280
|
-
if (containsUltrathink(expandedText)) {
|
|
4281
|
-
keywordNotices.push({
|
|
4282
|
-
role: "custom",
|
|
4283
|
-
customType: "ultrathink-notice",
|
|
4284
|
-
content: ULTRATHINK_NOTICE,
|
|
4285
|
-
display: false,
|
|
4286
|
-
attribution: "user",
|
|
4287
|
-
timestamp,
|
|
4288
|
-
});
|
|
4289
|
-
}
|
|
4290
|
-
if (containsOrchestrate(expandedText)) {
|
|
4291
|
-
keywordNotices.push({
|
|
4292
|
-
role: "custom",
|
|
4293
|
-
customType: "orchestrate-notice",
|
|
4294
|
-
content: ORCHESTRATE_NOTICE,
|
|
4295
|
-
display: false,
|
|
4296
|
-
attribution: "user",
|
|
4297
|
-
timestamp,
|
|
4298
|
-
});
|
|
4299
|
-
}
|
|
4300
|
-
if (containsWorkflow(expandedText)) {
|
|
4301
|
-
keywordNotices.push({
|
|
4302
|
-
role: "custom",
|
|
4303
|
-
customType: "workflow-notice",
|
|
4304
|
-
content: WORKFLOW_NOTICE,
|
|
4305
|
-
display: false,
|
|
4306
|
-
attribution: "user",
|
|
4307
|
-
timestamp,
|
|
4308
|
-
});
|
|
4309
|
-
}
|
|
4310
|
-
}
|
|
4611
|
+
const keywordNotices = options?.synthetic ? [] : this.#createMagicKeywordNotices(expandedText);
|
|
4311
4612
|
|
|
4312
4613
|
// If streaming, queue via steer() or followUp() based on option
|
|
4313
4614
|
if (this.isStreaming) {
|
|
@@ -4315,25 +4616,28 @@ export class AgentSession {
|
|
|
4315
4616
|
throw new AgentBusyError();
|
|
4316
4617
|
}
|
|
4317
4618
|
if (options.streamingBehavior === "followUp") {
|
|
4318
|
-
await this.#
|
|
4619
|
+
await this.#queueUserMessage(expandedText, options?.images, "followUp");
|
|
4319
4620
|
} else {
|
|
4320
|
-
await this.#
|
|
4621
|
+
await this.#queueUserMessage(expandedText, options?.images, "steer");
|
|
4321
4622
|
}
|
|
4322
4623
|
// Steer/follow-up the keyword notices alongside the queued user message.
|
|
4323
4624
|
for (const notice of keywordNotices) {
|
|
4324
4625
|
await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
|
|
4325
4626
|
}
|
|
4326
|
-
return;
|
|
4627
|
+
return true;
|
|
4327
4628
|
}
|
|
4328
4629
|
|
|
4329
|
-
// Skip eager
|
|
4630
|
+
// Skip eager preludes when the user has already queued a directive
|
|
4330
4631
|
const hasPendingUserDirective = this.#toolChoiceQueue.inspect().includes("user-force");
|
|
4331
4632
|
const eagerTodoPrelude =
|
|
4332
4633
|
!options?.synthetic && !hasPendingUserDirective ? this.#createEagerTodoPrelude(expandedText) : undefined;
|
|
4634
|
+
const eagerTaskPrelude =
|
|
4635
|
+
!options?.synthetic && !hasPendingUserDirective ? this.#createEagerTaskPrelude(expandedText) : undefined;
|
|
4636
|
+
const normalizedImages = await this.#normalizeImagesForModel(options?.images);
|
|
4333
4637
|
|
|
4334
4638
|
const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
|
|
4335
|
-
if (
|
|
4336
|
-
userContent.push(...
|
|
4639
|
+
if (normalizedImages) {
|
|
4640
|
+
userContent.push(...normalizedImages);
|
|
4337
4641
|
}
|
|
4338
4642
|
|
|
4339
4643
|
const promptAttribution = options?.attribution ?? (options?.synthetic ? "agent" : "user");
|
|
@@ -4341,16 +4645,24 @@ export class AgentSession {
|
|
|
4341
4645
|
? { role: "developer" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() }
|
|
4342
4646
|
: { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
|
|
4343
4647
|
|
|
4648
|
+
const preludeMessages: AgentMessage[] = [];
|
|
4344
4649
|
if (eagerTodoPrelude) {
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4650
|
+
if (eagerTodoPrelude.toolChoice) {
|
|
4651
|
+
this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
|
|
4652
|
+
label: "eager-todo",
|
|
4653
|
+
});
|
|
4654
|
+
}
|
|
4655
|
+
preludeMessages.push(eagerTodoPrelude.message);
|
|
4656
|
+
}
|
|
4657
|
+
if (eagerTaskPrelude) {
|
|
4658
|
+
preludeMessages.push(eagerTaskPrelude);
|
|
4348
4659
|
}
|
|
4349
4660
|
|
|
4350
4661
|
try {
|
|
4351
4662
|
await this.#promptWithMessage(message, expandedText, {
|
|
4352
4663
|
...options,
|
|
4353
|
-
|
|
4664
|
+
images: normalizedImages,
|
|
4665
|
+
prependMessages: preludeMessages.length > 0 ? preludeMessages : undefined,
|
|
4354
4666
|
appendMessages: keywordNotices.length > 0 ? keywordNotices : undefined,
|
|
4355
4667
|
});
|
|
4356
4668
|
} finally {
|
|
@@ -4361,11 +4673,12 @@ export class AgentSession {
|
|
|
4361
4673
|
if (!options?.synthetic) {
|
|
4362
4674
|
await this.#enforcePlanModeToolDecision();
|
|
4363
4675
|
}
|
|
4676
|
+
return true;
|
|
4364
4677
|
}
|
|
4365
4678
|
|
|
4366
4679
|
async promptCustomMessage<T = unknown>(
|
|
4367
4680
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
4368
|
-
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice"
|
|
4681
|
+
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice"> & { queueChipText?: string },
|
|
4369
4682
|
): Promise<void> {
|
|
4370
4683
|
const textContent =
|
|
4371
4684
|
typeof message.content === "string"
|
|
@@ -4375,11 +4688,27 @@ export class AgentSession {
|
|
|
4375
4688
|
.map(content => content.text)
|
|
4376
4689
|
.join("");
|
|
4377
4690
|
|
|
4691
|
+
let keywordNotices: CustomMessage[] = [];
|
|
4692
|
+
if (message.customType === SKILL_PROMPT_MESSAGE_TYPE && message.attribution === "user") {
|
|
4693
|
+
const details = message.details;
|
|
4694
|
+
let skillArgs = "";
|
|
4695
|
+
if (details && typeof details === "object" && "args" in details && typeof details.args === "string") {
|
|
4696
|
+
skillArgs = details.args;
|
|
4697
|
+
}
|
|
4698
|
+
keywordNotices = this.#createMagicKeywordNotices(skillArgs);
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4378
4701
|
if (this.isStreaming) {
|
|
4379
4702
|
if (!options?.streamingBehavior) {
|
|
4380
4703
|
throw new AgentBusyError();
|
|
4381
4704
|
}
|
|
4382
|
-
await this.sendCustomMessage(message, {
|
|
4705
|
+
await this.sendCustomMessage(message, {
|
|
4706
|
+
deliverAs: options.streamingBehavior,
|
|
4707
|
+
queueChipText: options.queueChipText,
|
|
4708
|
+
});
|
|
4709
|
+
for (const notice of keywordNotices) {
|
|
4710
|
+
await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
|
|
4711
|
+
}
|
|
4383
4712
|
return;
|
|
4384
4713
|
}
|
|
4385
4714
|
|
|
@@ -4393,7 +4722,10 @@ export class AgentSession {
|
|
|
4393
4722
|
timestamp: Date.now(),
|
|
4394
4723
|
};
|
|
4395
4724
|
|
|
4396
|
-
await this.#promptWithMessage(customMessage, textContent,
|
|
4725
|
+
await this.#promptWithMessage(customMessage, textContent, {
|
|
4726
|
+
...options,
|
|
4727
|
+
appendMessages: keywordNotices.length > 0 ? keywordNotices : undefined,
|
|
4728
|
+
});
|
|
4397
4729
|
}
|
|
4398
4730
|
|
|
4399
4731
|
async #promptWithMessage(
|
|
@@ -4411,7 +4743,7 @@ export class AgentSession {
|
|
|
4411
4743
|
// Flush any pending bash messages before the new prompt
|
|
4412
4744
|
this.#flushPendingBashMessages();
|
|
4413
4745
|
this.#flushPendingPythonMessages();
|
|
4414
|
-
this.#
|
|
4746
|
+
this.#flushPendingIrcAsides();
|
|
4415
4747
|
|
|
4416
4748
|
// Reset todo reminder count on new user prompt
|
|
4417
4749
|
this.#todoReminderCount = 0;
|
|
@@ -4492,7 +4824,9 @@ export class AgentSession {
|
|
|
4492
4824
|
useHashLines: resolveFileDisplayMode(this).hashLines,
|
|
4493
4825
|
snapshotStore: getFileSnapshotStore(this),
|
|
4494
4826
|
});
|
|
4495
|
-
|
|
4827
|
+
for (const fileMentionMessage of fileMentionMessages) {
|
|
4828
|
+
messages.push(await this.#normalizeAgentMessageImages(fileMentionMessage));
|
|
4829
|
+
}
|
|
4496
4830
|
}
|
|
4497
4831
|
|
|
4498
4832
|
const beforeAgentStartSystemPrompt = await this.#buildSystemPromptForAgentStart(expandedText);
|
|
@@ -4508,15 +4842,18 @@ export class AgentSession {
|
|
|
4508
4842
|
const promptAttribution: "user" | "agent" | undefined =
|
|
4509
4843
|
"attribution" in message ? message.attribution : undefined;
|
|
4510
4844
|
for (const msg of result.messages) {
|
|
4511
|
-
messages.push(
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4845
|
+
messages.push(
|
|
4846
|
+
await this.#normalizeAgentMessageImages({
|
|
4847
|
+
role: "custom",
|
|
4848
|
+
customType: msg.customType,
|
|
4849
|
+
content: msg.content,
|
|
4850
|
+
display: msg.display,
|
|
4851
|
+
details: msg.details,
|
|
4852
|
+
attribution:
|
|
4853
|
+
msg.attribution ?? promptAttribution ?? (message.role === "user" ? "user" : "agent"),
|
|
4854
|
+
timestamp: Date.now(),
|
|
4855
|
+
}),
|
|
4856
|
+
);
|
|
4520
4857
|
}
|
|
4521
4858
|
}
|
|
4522
4859
|
|
|
@@ -4553,7 +4890,7 @@ export class AgentSession {
|
|
|
4553
4890
|
const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined;
|
|
4554
4891
|
await this.#promptAgentWithIdleRetry(messages, agentPromptOptions);
|
|
4555
4892
|
if (!options?.skipPostPromptRecoveryWait) {
|
|
4556
|
-
await this.#waitForPostPromptRecovery();
|
|
4893
|
+
await this.#waitForPostPromptRecovery(generation);
|
|
4557
4894
|
}
|
|
4558
4895
|
} finally {
|
|
4559
4896
|
this.#endInFlight();
|
|
@@ -4603,6 +4940,7 @@ export class AgentSession {
|
|
|
4603
4940
|
sessionManager: this.sessionManager,
|
|
4604
4941
|
modelRegistry: this.#modelRegistry,
|
|
4605
4942
|
model: this.model ?? undefined,
|
|
4943
|
+
models: createExtensionModelQuery(this.#modelRegistry, this.settings, () => this.model ?? undefined),
|
|
4606
4944
|
isIdle: () => !this.isStreaming,
|
|
4607
4945
|
abort: () => {
|
|
4608
4946
|
void this.abort();
|
|
@@ -4705,7 +5043,7 @@ export class AgentSession {
|
|
|
4705
5043
|
}
|
|
4706
5044
|
|
|
4707
5045
|
const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
|
|
4708
|
-
await this.#
|
|
5046
|
+
await this.#queueUserMessage(expandedText, images, "steer");
|
|
4709
5047
|
}
|
|
4710
5048
|
|
|
4711
5049
|
/**
|
|
@@ -4717,68 +5055,74 @@ export class AgentSession {
|
|
|
4717
5055
|
}
|
|
4718
5056
|
|
|
4719
5057
|
const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
|
|
4720
|
-
await this.#
|
|
5058
|
+
await this.#queueUserMessage(expandedText, images, "followUp");
|
|
4721
5059
|
}
|
|
4722
5060
|
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
this.#
|
|
5061
|
+
async #queueUserMessage(
|
|
5062
|
+
text: string,
|
|
5063
|
+
images: ImageContent[] | undefined,
|
|
5064
|
+
mode: "steer" | "followUp",
|
|
5065
|
+
): Promise<void> {
|
|
5066
|
+
const normalizedImages = await this.#normalizeImagesForModel(images);
|
|
4729
5067
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
4730
|
-
if (
|
|
4731
|
-
content.push(...
|
|
5068
|
+
if (normalizedImages?.length) {
|
|
5069
|
+
content.push(...normalizedImages);
|
|
5070
|
+
}
|
|
5071
|
+
if (mode === "followUp") {
|
|
5072
|
+
this.agent.followUp({
|
|
5073
|
+
role: "user",
|
|
5074
|
+
content,
|
|
5075
|
+
attribution: "user",
|
|
5076
|
+
timestamp: Date.now(),
|
|
5077
|
+
});
|
|
5078
|
+
} else {
|
|
5079
|
+
this.agent.steer({
|
|
5080
|
+
role: "user",
|
|
5081
|
+
content,
|
|
5082
|
+
steering: true,
|
|
5083
|
+
attribution: "user",
|
|
5084
|
+
timestamp: Date.now(),
|
|
5085
|
+
});
|
|
4732
5086
|
}
|
|
4733
|
-
this
|
|
4734
|
-
role: "user",
|
|
4735
|
-
content,
|
|
4736
|
-
steering: true,
|
|
4737
|
-
attribution: "user",
|
|
4738
|
-
timestamp: Date.now(),
|
|
4739
|
-
});
|
|
5087
|
+
this.#scheduleIdleQueueDrain();
|
|
4740
5088
|
}
|
|
4741
5089
|
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
this.#
|
|
4748
|
-
|
|
4749
|
-
if (images && images.length > 0) {
|
|
4750
|
-
content.push(...images);
|
|
5090
|
+
#scheduleIdleQueueDrain(): void {
|
|
5091
|
+
this.#scheduleQueuedMessageDrain();
|
|
5092
|
+
}
|
|
5093
|
+
|
|
5094
|
+
#scheduleQueuedMessageDrain(): void {
|
|
5095
|
+
if (this.#queuedMessageDrainScheduled || !this.#canAutoContinueForFollowUp() || !this.agent.hasQueuedMessages()) {
|
|
5096
|
+
return;
|
|
4751
5097
|
}
|
|
4752
|
-
this
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
5098
|
+
this.#queuedMessageDrainScheduled = true;
|
|
5099
|
+
this.#scheduleAgentContinue({
|
|
5100
|
+
shouldContinue: () => {
|
|
5101
|
+
this.#queuedMessageDrainScheduled = false;
|
|
5102
|
+
return this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages();
|
|
5103
|
+
},
|
|
5104
|
+
onSkip: () => {
|
|
5105
|
+
this.#queuedMessageDrainScheduled = false;
|
|
5106
|
+
},
|
|
5107
|
+
onError: () => {
|
|
5108
|
+
this.#queuedMessageDrainScheduled = false;
|
|
5109
|
+
},
|
|
4757
5110
|
});
|
|
4758
|
-
// When fully idle AND the session is in a resumable assistant-ended state,
|
|
4759
|
-
// schedule an immediate continue so the queued follow-up is delivered
|
|
4760
|
-
// without waiting for the next user turn. We gate on isStreaming (model
|
|
4761
|
-
// actively producing), isRetrying (auto-retry backoff is sleeping between
|
|
4762
|
-
// attempts, #retryPromise set), and the last message being assistant —
|
|
4763
|
-
// agent.continue() only dequeues follow-ups from an assistant-ended state;
|
|
4764
|
-
// resuming from user/toolResult state runs an extra model call on the
|
|
4765
|
-
// stale prompt before draining the queue.
|
|
4766
|
-
if (this.#canAutoContinueForFollowUp()) {
|
|
4767
|
-
this.#scheduleAgentContinue({
|
|
4768
|
-
shouldContinue: () => this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages(),
|
|
4769
|
-
});
|
|
4770
|
-
}
|
|
4771
5111
|
}
|
|
4772
5112
|
|
|
4773
5113
|
/**
|
|
4774
|
-
* Gate for idle-path
|
|
5114
|
+
* Gate for idle-path queued-message auto-continue. See `#scheduleIdleQueueDrain` for rationale.
|
|
4775
5115
|
*/
|
|
4776
5116
|
#canAutoContinueForFollowUp(): boolean {
|
|
4777
5117
|
if (this.isStreaming) return false;
|
|
4778
5118
|
if (this.isRetrying) return false;
|
|
4779
5119
|
const messages = this.agent.state.messages;
|
|
4780
5120
|
const last = messages[messages.length - 1];
|
|
4781
|
-
|
|
5121
|
+
// A user interrupt during tool execution can leave the transcript ending
|
|
5122
|
+
// with the emitted tool result, not the aborted assistant message. Continuing
|
|
5123
|
+
// from that state is still resumable: Agent.continue() first polls queued
|
|
5124
|
+
// steering before making the next model call.
|
|
5125
|
+
return last?.role === "assistant" || last?.role === "toolResult";
|
|
4782
5126
|
}
|
|
4783
5127
|
|
|
4784
5128
|
queueDeferredMessage(message: CustomMessage): void {
|
|
@@ -4877,71 +5221,87 @@ export class AgentSession {
|
|
|
4877
5221
|
* - Streaming: queue as steer/follow-up or store for next turn
|
|
4878
5222
|
* - Not streaming + triggerTurn: appends to state/session, starts new turn unless the client cannot own it
|
|
4879
5223
|
* - Not streaming + no trigger: appends to state/session, no turn
|
|
5224
|
+
*
|
|
5225
|
+
* @returns true iff this call synchronously started a new turn (awaited
|
|
5226
|
+
* `agent.prompt`); false when the message was queued/appended without a turn.
|
|
4880
5227
|
*/
|
|
4881
5228
|
async sendCustomMessage<T = unknown>(
|
|
4882
5229
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
4883
|
-
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
4884
|
-
): Promise<
|
|
5230
|
+
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn"; queueChipText?: string },
|
|
5231
|
+
): Promise<boolean> {
|
|
5232
|
+
const details =
|
|
5233
|
+
options?.queueChipText && options.deliverAs !== "nextTurn"
|
|
5234
|
+
? ({
|
|
5235
|
+
...((message.details && typeof message.details === "object" ? message.details : {}) as Record<
|
|
5236
|
+
string,
|
|
5237
|
+
unknown
|
|
5238
|
+
>),
|
|
5239
|
+
__queueChipText: options.queueChipText,
|
|
5240
|
+
} as T)
|
|
5241
|
+
: message.details;
|
|
4885
5242
|
const appMessage: CustomMessage<T> = {
|
|
4886
5243
|
role: "custom",
|
|
4887
5244
|
customType: message.customType,
|
|
4888
5245
|
content: message.content,
|
|
4889
5246
|
display: message.display,
|
|
4890
|
-
details
|
|
5247
|
+
details,
|
|
4891
5248
|
attribution: message.attribution ?? "agent",
|
|
4892
5249
|
timestamp: Date.now(),
|
|
4893
5250
|
};
|
|
5251
|
+
const normalizedAppMessage = await this.#normalizeAgentMessageImages(appMessage);
|
|
4894
5252
|
if (this.isStreaming) {
|
|
4895
5253
|
if (options?.deliverAs === "nextTurn") {
|
|
4896
|
-
this.#queueHiddenNextTurnMessage(
|
|
4897
|
-
return;
|
|
5254
|
+
this.#queueHiddenNextTurnMessage(normalizedAppMessage, options?.triggerTurn ?? false);
|
|
5255
|
+
return false;
|
|
4898
5256
|
}
|
|
4899
5257
|
|
|
4900
5258
|
if (options?.deliverAs === "followUp") {
|
|
4901
|
-
this.agent.followUp(
|
|
5259
|
+
this.agent.followUp(normalizedAppMessage);
|
|
4902
5260
|
} else {
|
|
4903
|
-
this.agent.steer(
|
|
5261
|
+
this.agent.steer(normalizedAppMessage);
|
|
4904
5262
|
}
|
|
4905
|
-
|
|
5263
|
+
this.#scheduleIdleQueueDrain();
|
|
5264
|
+
return false;
|
|
4906
5265
|
}
|
|
4907
5266
|
|
|
4908
5267
|
if (options?.deliverAs === "nextTurn") {
|
|
4909
5268
|
if (options?.triggerTurn) {
|
|
4910
5269
|
if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
|
|
4911
|
-
this.#queueHiddenNextTurnMessage(
|
|
4912
|
-
return;
|
|
5270
|
+
this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
|
|
5271
|
+
return false;
|
|
4913
5272
|
}
|
|
4914
|
-
await this.agent.prompt(
|
|
4915
|
-
return;
|
|
5273
|
+
await this.agent.prompt(normalizedAppMessage);
|
|
5274
|
+
return true;
|
|
4916
5275
|
}
|
|
4917
|
-
this.agent.appendMessage(
|
|
5276
|
+
this.agent.appendMessage(normalizedAppMessage);
|
|
4918
5277
|
this.sessionManager.appendCustomMessageEntry(
|
|
4919
|
-
|
|
4920
|
-
|
|
5278
|
+
normalizedAppMessage.customType,
|
|
5279
|
+
normalizedAppMessage.content,
|
|
4921
5280
|
message.display,
|
|
4922
5281
|
message.details,
|
|
4923
5282
|
message.attribution ?? "agent",
|
|
4924
5283
|
);
|
|
4925
|
-
return;
|
|
5284
|
+
return false;
|
|
4926
5285
|
}
|
|
4927
5286
|
|
|
4928
5287
|
if (options?.triggerTurn) {
|
|
4929
5288
|
if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
|
|
4930
|
-
this.#queueHiddenNextTurnMessage(
|
|
4931
|
-
return;
|
|
5289
|
+
this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
|
|
5290
|
+
return false;
|
|
4932
5291
|
}
|
|
4933
|
-
await this.agent.prompt(
|
|
4934
|
-
return;
|
|
5292
|
+
await this.agent.prompt(normalizedAppMessage);
|
|
5293
|
+
return true;
|
|
4935
5294
|
}
|
|
4936
5295
|
|
|
4937
|
-
this.agent.appendMessage(
|
|
5296
|
+
this.agent.appendMessage(normalizedAppMessage);
|
|
4938
5297
|
this.sessionManager.appendCustomMessageEntry(
|
|
4939
|
-
|
|
4940
|
-
|
|
5298
|
+
normalizedAppMessage.customType,
|
|
5299
|
+
normalizedAppMessage.content,
|
|
4941
5300
|
message.display,
|
|
4942
5301
|
message.details,
|
|
4943
5302
|
message.attribution ?? "agent",
|
|
4944
5303
|
);
|
|
5304
|
+
return false;
|
|
4945
5305
|
}
|
|
4946
5306
|
|
|
4947
5307
|
/**
|
|
@@ -4976,11 +5336,11 @@ export class AgentSession {
|
|
|
4976
5336
|
}
|
|
4977
5337
|
|
|
4978
5338
|
if (options?.deliverAs === "followUp") {
|
|
4979
|
-
await this.#
|
|
5339
|
+
await this.#queueUserMessage(text, images, "followUp");
|
|
4980
5340
|
return;
|
|
4981
5341
|
}
|
|
4982
5342
|
if (options?.deliverAs === "steer") {
|
|
4983
|
-
await this.#
|
|
5343
|
+
await this.#queueUserMessage(text, images, "steer");
|
|
4984
5344
|
return;
|
|
4985
5345
|
}
|
|
4986
5346
|
|
|
@@ -4991,55 +5351,37 @@ export class AgentSession {
|
|
|
4991
5351
|
});
|
|
4992
5352
|
}
|
|
4993
5353
|
|
|
4994
|
-
/**
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
clearQueue(): { steering: string[]; followUp: string[] } {
|
|
4999
|
-
const steering = this.#steeringMessages.map(e => e.text);
|
|
5000
|
-
const followUp = this.#followUpMessages.map(e => e.text);
|
|
5001
|
-
this.#steeringMessages = [];
|
|
5002
|
-
this.#followUpMessages = [];
|
|
5354
|
+
/** Clear queued messages and return them (text plus any attached images). */
|
|
5355
|
+
clearQueue(): { steering: RestoredQueuedMessage[]; followUp: RestoredQueuedMessage[] } {
|
|
5356
|
+
const steering = this.agent.peekSteeringQueue().map(toRestoredQueuedMessage);
|
|
5357
|
+
const followUp = this.agent.peekFollowUpQueue().map(toRestoredQueuedMessage);
|
|
5003
5358
|
this.agent.clearAllQueues();
|
|
5004
5359
|
return { steering, followUp };
|
|
5005
5360
|
}
|
|
5006
5361
|
|
|
5007
|
-
/** Number of pending messages (includes steering, follow-up, and next-turn messages) */
|
|
5362
|
+
/** Number of pending displayable messages (includes steering, follow-up, and next-turn messages) */
|
|
5008
5363
|
get queuedMessageCount(): number {
|
|
5009
|
-
return
|
|
5364
|
+
return (
|
|
5365
|
+
this.agent.peekSteeringQueue().filter(isDisplayableQueuedMessage).length +
|
|
5366
|
+
this.agent.peekFollowUpQueue().filter(isDisplayableQueuedMessage).length +
|
|
5367
|
+
this.#pendingNextTurnMessages.length
|
|
5368
|
+
);
|
|
5010
5369
|
}
|
|
5011
5370
|
|
|
5012
|
-
/** Get pending messages (read-only). Returns the public text-only view;
|
|
5013
|
-
* internal `{text, tag?}` records are mapped to `.text` so callers
|
|
5014
|
-
* (`updatePendingMessagesDisplay`, `restoreQueuedMessagesToEditor`) see
|
|
5015
|
-
* the unchanged historical shape. */
|
|
5016
5371
|
getQueuedMessages(): { steering: readonly string[]; followUp: readonly string[] } {
|
|
5017
5372
|
return {
|
|
5018
|
-
steering: this
|
|
5019
|
-
followUp: this
|
|
5373
|
+
steering: this.agent.peekSteeringQueue().filter(isDisplayableQueuedMessage).map(queueChipText),
|
|
5374
|
+
followUp: this.agent.peekFollowUpQueue().filter(isDisplayableQueuedMessage).map(queueChipText),
|
|
5020
5375
|
};
|
|
5021
5376
|
}
|
|
5022
5377
|
|
|
5023
5378
|
/**
|
|
5024
5379
|
* Pop the last queued message (steering first, then follow-up).
|
|
5025
5380
|
* Used by dequeue keybinding to restore messages to editor one at a time.
|
|
5026
|
-
* Returns the popped entry's `.text`; the tag (if any) dies with the
|
|
5027
|
-
* record — no orphan state can outlive the queue entry.
|
|
5028
5381
|
*/
|
|
5029
|
-
popLastQueuedMessage():
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
const entry = this.#steeringMessages.pop();
|
|
5033
|
-
this.agent.popLastSteer();
|
|
5034
|
-
return entry?.text;
|
|
5035
|
-
}
|
|
5036
|
-
// Then from follow-up
|
|
5037
|
-
if (this.#followUpMessages.length > 0) {
|
|
5038
|
-
const entry = this.#followUpMessages.pop();
|
|
5039
|
-
this.agent.popLastFollowUp();
|
|
5040
|
-
return entry?.text;
|
|
5041
|
-
}
|
|
5042
|
-
return undefined;
|
|
5382
|
+
popLastQueuedMessage(): RestoredQueuedMessage | undefined {
|
|
5383
|
+
const message = this.agent.popLastSteer() ?? this.agent.popLastFollowUp();
|
|
5384
|
+
return message ? toRestoredQueuedMessage(message) : undefined;
|
|
5043
5385
|
}
|
|
5044
5386
|
|
|
5045
5387
|
get skillsSettings(): SkillsSettings | undefined {
|
|
@@ -5077,11 +5419,7 @@ export class AgentSession {
|
|
|
5077
5419
|
#cloneTodoPhases(phases: TodoPhase[]): TodoPhase[] {
|
|
5078
5420
|
return phases.map(phase => ({
|
|
5079
5421
|
name: phase.name,
|
|
5080
|
-
tasks: phase.tasks.map(task => {
|
|
5081
|
-
const out: TodoItem = { content: task.content, status: task.status };
|
|
5082
|
-
if (task.notes && task.notes.length > 0) out.notes = [...task.notes];
|
|
5083
|
-
return out;
|
|
5084
|
-
}),
|
|
5422
|
+
tasks: phase.tasks.map(task => ({ content: task.content, status: task.status })),
|
|
5085
5423
|
}));
|
|
5086
5424
|
}
|
|
5087
5425
|
|
|
@@ -5093,30 +5431,44 @@ export class AgentSession {
|
|
|
5093
5431
|
|
|
5094
5432
|
/**
|
|
5095
5433
|
* Abort current operation and wait for agent to become idle.
|
|
5434
|
+
*
|
|
5435
|
+
* `reason` (e.g. `USER_INTERRUPT_LABEL`) rides the agent's `AbortController`
|
|
5436
|
+
* and surfaces verbatim on the aborted assistant message's `errorMessage`, so
|
|
5437
|
+
* the transcript can distinguish a deliberate user interrupt from an opaque
|
|
5438
|
+
* abort. Omit it for internal/lifecycle aborts.
|
|
5096
5439
|
*/
|
|
5097
|
-
async abort(options?: { goalReason?: "interrupted" | "internal" }): Promise<void> {
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
this
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5440
|
+
async abort(options?: { goalReason?: "interrupted" | "internal"; reason?: string }): Promise<void> {
|
|
5441
|
+
// Session switch/compact paths disconnect first; explicit aborts should
|
|
5442
|
+
// leave any queued steer/follow-up visible for the user rather than
|
|
5443
|
+
// auto-starting a fresh turn during cleanup.
|
|
5444
|
+
this.#abortInProgress = true;
|
|
5445
|
+
try {
|
|
5446
|
+
this.abortRetry();
|
|
5447
|
+
this.#promptGeneration++;
|
|
5448
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
5449
|
+
this.abortCompaction();
|
|
5450
|
+
this.abortHandoff();
|
|
5451
|
+
this.abortBash();
|
|
5452
|
+
this.abortEval();
|
|
5453
|
+
const postPromptDrain = this.#cancelPostPromptTasks();
|
|
5454
|
+
this.agent.abort(options?.reason);
|
|
5455
|
+
await postPromptDrain;
|
|
5456
|
+
await this.agent.waitForIdle();
|
|
5457
|
+
await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
|
|
5458
|
+
// Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
|
|
5459
|
+
// block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
|
|
5460
|
+
// a subsequent prompt() can incorrectly observe the session as busy after an abort.
|
|
5461
|
+
this.#resetInFlight();
|
|
5462
|
+
// Safety net: if the agent loop aborted without producing an assistant
|
|
5463
|
+
// message (e.g. failed before the first stream), the in-flight yield was
|
|
5464
|
+
// never resolved or rejected by the normal message_end path. Reject it now
|
|
5465
|
+
// so any requeue callback still fires and the queue stays consistent.
|
|
5466
|
+
if (this.#toolChoiceQueue.hasInFlight) {
|
|
5467
|
+
this.#toolChoiceQueue.reject("aborted");
|
|
5468
|
+
}
|
|
5469
|
+
} finally {
|
|
5470
|
+
this.#abortInProgress = false;
|
|
5471
|
+
this.#drainStrandedQueuedMessages();
|
|
5120
5472
|
}
|
|
5121
5473
|
}
|
|
5122
5474
|
|
|
@@ -5164,13 +5516,12 @@ export class AgentSession {
|
|
|
5164
5516
|
}
|
|
5165
5517
|
await this.sessionManager.newSession(options);
|
|
5166
5518
|
this.setTodoPhases([]);
|
|
5519
|
+
this.#freshProviderSessionId = undefined;
|
|
5167
5520
|
this.#syncAgentSessionId();
|
|
5168
5521
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
5169
5522
|
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
|
5170
5523
|
this.#resetHindsightConversationTrackingIfHindsight();
|
|
5171
5524
|
this.#resetMnemopiConversationTrackingIfMnemopi();
|
|
5172
|
-
this.#steeringMessages = [];
|
|
5173
|
-
this.#followUpMessages = [];
|
|
5174
5525
|
this.#pendingNextTurnMessages = [];
|
|
5175
5526
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
5176
5527
|
|
|
@@ -5261,6 +5612,7 @@ export class AgentSession {
|
|
|
5261
5612
|
}
|
|
5262
5613
|
|
|
5263
5614
|
// Update agent session ID
|
|
5615
|
+
this.#freshProviderSessionId = undefined;
|
|
5264
5616
|
this.#syncAgentSessionId();
|
|
5265
5617
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
5266
5618
|
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
|
@@ -5284,7 +5636,10 @@ export class AgentSession {
|
|
|
5284
5636
|
|
|
5285
5637
|
/**
|
|
5286
5638
|
* Set model directly.
|
|
5287
|
-
* Validates
|
|
5639
|
+
* Validates that a credential source is configured (synchronously, without
|
|
5640
|
+
* refreshing OAuth or running command-backed key programs) and saves to the
|
|
5641
|
+
* active session. Persists settings only when requested. The concrete key is
|
|
5642
|
+
* resolved lazily per request, so switching never blocks the event loop.
|
|
5288
5643
|
* @throws Error if no API key available for the model
|
|
5289
5644
|
*/
|
|
5290
5645
|
async setModel(
|
|
@@ -5293,8 +5648,7 @@ export class AgentSession {
|
|
|
5293
5648
|
options?: { selector?: string; thinkingLevel?: ThinkingLevel; persist?: boolean },
|
|
5294
5649
|
): Promise<void> {
|
|
5295
5650
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
5296
|
-
|
|
5297
|
-
if (!apiKey) {
|
|
5651
|
+
if (!this.#modelRegistry.hasConfiguredAuth(model)) {
|
|
5298
5652
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
5299
5653
|
}
|
|
5300
5654
|
|
|
@@ -5317,7 +5671,9 @@ export class AgentSession {
|
|
|
5317
5671
|
|
|
5318
5672
|
/**
|
|
5319
5673
|
* Set model temporarily (for this session only).
|
|
5320
|
-
* Validates
|
|
5674
|
+
* Validates that a credential source is configured (synchronously, without
|
|
5675
|
+
* refreshing OAuth or running command-backed key programs), saves to session
|
|
5676
|
+
* log but NOT to settings.
|
|
5321
5677
|
* @throws Error if no API key available for the model
|
|
5322
5678
|
*/
|
|
5323
5679
|
async setModelTemporary(
|
|
@@ -5326,8 +5682,7 @@ export class AgentSession {
|
|
|
5326
5682
|
options?: { ephemeral?: boolean },
|
|
5327
5683
|
): Promise<void> {
|
|
5328
5684
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
5329
|
-
|
|
5330
|
-
if (!apiKey) {
|
|
5685
|
+
if (!this.#modelRegistry.hasConfiguredAuth(model)) {
|
|
5331
5686
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
5332
5687
|
}
|
|
5333
5688
|
|
|
@@ -5378,7 +5733,7 @@ export class AgentSession {
|
|
|
5378
5733
|
|
|
5379
5734
|
const currentModel = this.model;
|
|
5380
5735
|
if (!currentModel) return undefined;
|
|
5381
|
-
const matchPreferences =
|
|
5736
|
+
const matchPreferences = getModelMatchPreferences(this.settings);
|
|
5382
5737
|
const models: ResolvedRoleModel[] = [];
|
|
5383
5738
|
|
|
5384
5739
|
for (const role of roleOrder) {
|
|
@@ -5525,16 +5880,25 @@ export class AgentSession {
|
|
|
5525
5880
|
}
|
|
5526
5881
|
|
|
5527
5882
|
/**
|
|
5528
|
-
* Get all available models with valid API keys.
|
|
5883
|
+
* Get all available models with valid API keys, filtered by `enabledModels` when configured.
|
|
5884
|
+
* See {@link filterAvailableModelsByEnabledPatterns} for supported pattern forms and limitations.
|
|
5529
5885
|
*/
|
|
5530
5886
|
getAvailableModels(): Model[] {
|
|
5531
|
-
|
|
5887
|
+
const all = this.#modelRegistry.getAvailable();
|
|
5888
|
+
const patterns = this.settings.get("enabledModels");
|
|
5889
|
+
if (!patterns || patterns.length === 0) return all;
|
|
5890
|
+
return filterAvailableModelsByEnabledPatterns(all, patterns, this.#modelRegistry);
|
|
5532
5891
|
}
|
|
5533
5892
|
|
|
5534
5893
|
// =========================================================================
|
|
5535
5894
|
// Thinking Level Management
|
|
5536
5895
|
// =========================================================================
|
|
5537
5896
|
|
|
5897
|
+
#applyThinkingLevelToAgent(level: ThinkingLevel | undefined): void {
|
|
5898
|
+
this.agent.setThinkingLevel(toReasoningEffort(level));
|
|
5899
|
+
this.agent.setDisableReasoning(shouldDisableReasoning(level));
|
|
5900
|
+
}
|
|
5901
|
+
|
|
5538
5902
|
/**
|
|
5539
5903
|
* Set the thinking level. `auto` enables per-turn classification; the selector
|
|
5540
5904
|
* itself is never written to the session log, but resolved concrete levels are
|
|
@@ -5548,7 +5912,7 @@ export class AgentSession {
|
|
|
5548
5912
|
this.#autoThinking = true;
|
|
5549
5913
|
this.#autoResolvedLevel = undefined;
|
|
5550
5914
|
this.#thinkingLevel = provisional;
|
|
5551
|
-
this
|
|
5915
|
+
this.#applyThinkingLevelToAgent(provisional);
|
|
5552
5916
|
if (persist) {
|
|
5553
5917
|
this.settings.set("defaultThinkingLevel", AUTO_THINKING);
|
|
5554
5918
|
}
|
|
@@ -5564,7 +5928,7 @@ export class AgentSession {
|
|
|
5564
5928
|
const isChanging = effectiveLevel !== this.#thinkingLevel;
|
|
5565
5929
|
|
|
5566
5930
|
this.#thinkingLevel = effectiveLevel;
|
|
5567
|
-
this
|
|
5931
|
+
this.#applyThinkingLevelToAgent(effectiveLevel);
|
|
5568
5932
|
|
|
5569
5933
|
if (isChanging) {
|
|
5570
5934
|
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
|
@@ -5621,7 +5985,7 @@ export class AgentSession {
|
|
|
5621
5985
|
if (!model?.reasoning) return;
|
|
5622
5986
|
|
|
5623
5987
|
let resolved: Effort | undefined;
|
|
5624
|
-
if (containsUltrathink(promptText)) {
|
|
5988
|
+
if (this.#magicKeywordEnabled("ultrathink") && containsUltrathink(promptText)) {
|
|
5625
5989
|
// The user explicitly asked for maximum thinking; bypass the classifier
|
|
5626
5990
|
// and jump straight to the highest auto-supported level for this model.
|
|
5627
5991
|
resolved = clampAutoThinkingEffort(model, Effort.XHigh);
|
|
@@ -5654,7 +6018,7 @@ export class AgentSession {
|
|
|
5654
6018
|
const shouldPersistResolution = this.#autoResolvedLevel !== effort;
|
|
5655
6019
|
this.#autoResolvedLevel = effort;
|
|
5656
6020
|
this.#thinkingLevel = effort;
|
|
5657
|
-
this
|
|
6021
|
+
this.#applyThinkingLevelToAgent(effort);
|
|
5658
6022
|
if (shouldPersistResolution) {
|
|
5659
6023
|
this.sessionManager.appendThinkingLevelChange(effort);
|
|
5660
6024
|
}
|
|
@@ -5710,7 +6074,12 @@ export class AgentSession {
|
|
|
5710
6074
|
// Already on under any scope — keep the user's scoped value.
|
|
5711
6075
|
return;
|
|
5712
6076
|
}
|
|
5713
|
-
|
|
6077
|
+
if (!enabled) {
|
|
6078
|
+
this.setServiceTier(undefined);
|
|
6079
|
+
return;
|
|
6080
|
+
}
|
|
6081
|
+
const scope = this.settings.get("fastModeScope");
|
|
6082
|
+
this.setServiceTier(scope === "openai" ? "openai-only" : scope === "claude" ? "claude-only" : "priority");
|
|
5714
6083
|
}
|
|
5715
6084
|
|
|
5716
6085
|
toggleFastMode(): boolean {
|
|
@@ -5775,7 +6144,45 @@ export class AgentSession {
|
|
|
5775
6144
|
|
|
5776
6145
|
async #pruneToolOutputs(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
|
|
5777
6146
|
const branchEntries = this.sessionManager.getBranch();
|
|
5778
|
-
const result = pruneToolOutputs(
|
|
6147
|
+
const result = pruneToolOutputs(
|
|
6148
|
+
branchEntries,
|
|
6149
|
+
this.#withPlanProtection({
|
|
6150
|
+
...DEFAULT_PRUNE_CONFIG,
|
|
6151
|
+
pruneUseless: this.settings.getGroup("compaction").dropUseless,
|
|
6152
|
+
}),
|
|
6153
|
+
);
|
|
6154
|
+
if (result.prunedCount === 0) {
|
|
6155
|
+
return undefined;
|
|
6156
|
+
}
|
|
6157
|
+
|
|
6158
|
+
await this.sessionManager.rewriteEntries();
|
|
6159
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
6160
|
+
this.agent.replaceMessages(sessionContext.messages);
|
|
6161
|
+
this.#syncTodoPhasesFromBranch();
|
|
6162
|
+
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6163
|
+
return result;
|
|
6164
|
+
}
|
|
6165
|
+
|
|
6166
|
+
/**
|
|
6167
|
+
* Per-turn stale-result pass: prune older `read` results that a newer read
|
|
6168
|
+
* of the same file has made stale, plus results their tool flagged
|
|
6169
|
+
* contextually useless. Cache-aware (only fires when the suffix after a
|
|
6170
|
+
* candidate is small or the session has been idle long enough that the
|
|
6171
|
+
* provider prompt cache is cold), so it is cheap to run every turn. Gated
|
|
6172
|
+
* on the `compaction.supersedeReads` and `compaction.dropUseless` settings.
|
|
6173
|
+
*/
|
|
6174
|
+
async #pruneStaleToolResults(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
|
|
6175
|
+
const { supersedeReads, dropUseless } = this.settings.getGroup("compaction");
|
|
6176
|
+
if (!supersedeReads && !dropUseless) return undefined;
|
|
6177
|
+
const branchEntries = this.sessionManager.getBranch();
|
|
6178
|
+
const result = pruneSupersededToolResults(
|
|
6179
|
+
branchEntries,
|
|
6180
|
+
this.#withPlanProtection({
|
|
6181
|
+
supersedeKey: supersedeReads ? readToolSupersedeKey : undefined,
|
|
6182
|
+
pruneUseless: dropUseless,
|
|
6183
|
+
protectedTools: [...DEFAULT_PRUNE_CONFIG.protectedTools],
|
|
6184
|
+
}),
|
|
6185
|
+
);
|
|
5779
6186
|
if (result.prunedCount === 0) {
|
|
5780
6187
|
return undefined;
|
|
5781
6188
|
}
|
|
@@ -5977,6 +6384,20 @@ export class AgentSession {
|
|
|
5977
6384
|
|
|
5978
6385
|
const compactionPrep = await this.#prepareCompactionFromHooks(preparation, hookCompaction);
|
|
5979
6386
|
|
|
6387
|
+
// Strategy honored on manual /compact too. Custom instructions imply a
|
|
6388
|
+
// directed LLM summary; a text-only model cannot read the frames back —
|
|
6389
|
+
// both take the summarizer path (the latter loudly).
|
|
6390
|
+
const wantsSnapcompact =
|
|
6391
|
+
compactionPrep.kind !== "fromHook" && compactionSettings.strategy === "snapcompact" && !customInstructions;
|
|
6392
|
+
const snapcompactReady = wantsSnapcompact && this.model.input.includes("image");
|
|
6393
|
+
if (wantsSnapcompact && !snapcompactReady) {
|
|
6394
|
+
this.emitNotice(
|
|
6395
|
+
"warning",
|
|
6396
|
+
`snapcompact needs a vision-capable model (${this.model.id} is text-only) — using an LLM summary instead`,
|
|
6397
|
+
"compaction",
|
|
6398
|
+
);
|
|
6399
|
+
}
|
|
6400
|
+
|
|
5980
6401
|
let summary: string;
|
|
5981
6402
|
let shortSummary: string | undefined;
|
|
5982
6403
|
let firstKeptEntryId: string;
|
|
@@ -5990,6 +6411,22 @@ export class AgentSession {
|
|
|
5990
6411
|
tokensBefore = compactionPrep.tokensBefore;
|
|
5991
6412
|
details = compactionPrep.details;
|
|
5992
6413
|
preserveData = compactionPrep.preserveData;
|
|
6414
|
+
} else if (snapcompactReady) {
|
|
6415
|
+
const snapcompactResult = await snapcompact.compact(preparation, {
|
|
6416
|
+
convertToLlm,
|
|
6417
|
+
model: this.model,
|
|
6418
|
+
thinkingLevel: this.thinkingLevel,
|
|
6419
|
+
shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
|
|
6420
|
+
// Providers with hard image caps (OpenRouter: 8) silently drop
|
|
6421
|
+
// frames past the cap — keep the archive within budget.
|
|
6422
|
+
maxFrames: snapcompact.providerFrameBudget(this.model.provider),
|
|
6423
|
+
});
|
|
6424
|
+
summary = snapcompactResult.summary;
|
|
6425
|
+
shortSummary = snapcompactResult.shortSummary;
|
|
6426
|
+
firstKeptEntryId = snapcompactResult.firstKeptEntryId;
|
|
6427
|
+
tokensBefore = snapcompactResult.tokensBefore;
|
|
6428
|
+
details = snapcompactResult.details;
|
|
6429
|
+
preserveData = { ...(compactionPrep.preserveData ?? {}), ...(snapcompactResult.preserveData ?? {}) };
|
|
5993
6430
|
} else {
|
|
5994
6431
|
// Generate compaction result. Only convert known abort-shaped
|
|
5995
6432
|
// rejections (AbortError raised while the abort signal is set,
|
|
@@ -6008,10 +6445,10 @@ export class AgentSession {
|
|
|
6008
6445
|
customInstructions,
|
|
6009
6446
|
compactionAbortController.signal,
|
|
6010
6447
|
{
|
|
6011
|
-
promptOverride: compactionPrep.hookPrompt,
|
|
6012
|
-
extraContext: compactionPrep.hookContext,
|
|
6013
|
-
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
|
6014
|
-
convertToLlm,
|
|
6448
|
+
promptOverride: this.#obfuscateTextForProvider(compactionPrep.hookPrompt),
|
|
6449
|
+
extraContext: this.#obfuscateForProvider(compactionPrep.hookContext),
|
|
6450
|
+
remoteInstructions: this.#obfuscateForProvider(this.#baseSystemPrompt.join("\n\n")),
|
|
6451
|
+
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
6015
6452
|
},
|
|
6016
6453
|
);
|
|
6017
6454
|
summary = result.summary;
|
|
@@ -6097,7 +6534,7 @@ export class AgentSession {
|
|
|
6097
6534
|
messagesToSummarize: AgentMessage[];
|
|
6098
6535
|
turnPrefixMessages: AgentMessage[];
|
|
6099
6536
|
}): Promise<string | undefined> {
|
|
6100
|
-
const backend = resolveMemoryBackend(this.settings);
|
|
6537
|
+
const backend = await resolveMemoryBackend(this.settings);
|
|
6101
6538
|
if (!backend.preCompactionContext) return undefined;
|
|
6102
6539
|
const messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
|
|
6103
6540
|
try {
|
|
@@ -6194,15 +6631,15 @@ export class AgentSession {
|
|
|
6194
6631
|
throw new Error(`No API key for ${model.provider}`);
|
|
6195
6632
|
}
|
|
6196
6633
|
|
|
6197
|
-
const
|
|
6634
|
+
const rawHandoffText = await generateHandoff(
|
|
6198
6635
|
this.agent.state.messages,
|
|
6199
6636
|
model,
|
|
6200
|
-
|
|
6637
|
+
this.#modelRegistry.resolver(model, this.sessionId),
|
|
6201
6638
|
{
|
|
6202
|
-
systemPrompt: this.#baseSystemPrompt,
|
|
6203
|
-
tools: this.agent.state.tools,
|
|
6204
|
-
customInstructions,
|
|
6205
|
-
convertToLlm,
|
|
6639
|
+
systemPrompt: this.#obfuscateForProvider(this.#baseSystemPrompt),
|
|
6640
|
+
tools: obfuscateProviderTools(this.#obfuscator, this.agent.state.tools),
|
|
6641
|
+
customInstructions: this.#obfuscateTextForProvider(customInstructions),
|
|
6642
|
+
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
6206
6643
|
initiatorOverride: "agent",
|
|
6207
6644
|
metadata: this.agent.metadataForProvider(model.provider),
|
|
6208
6645
|
telemetry: resolveTelemetry(this.agent.telemetry, this.sessionId),
|
|
@@ -6214,6 +6651,7 @@ export class AgentSession {
|
|
|
6214
6651
|
},
|
|
6215
6652
|
handoffSignal,
|
|
6216
6653
|
);
|
|
6654
|
+
const handoffText = this.#deobfuscateFromProvider(rawHandoffText);
|
|
6217
6655
|
|
|
6218
6656
|
if (handoffSignal.aborted) {
|
|
6219
6657
|
throw new Error("Handoff cancelled");
|
|
@@ -6228,13 +6666,12 @@ export class AgentSession {
|
|
|
6228
6666
|
this.#cancelOwnAsyncJobs();
|
|
6229
6667
|
await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
|
|
6230
6668
|
this.agent.reset();
|
|
6669
|
+
this.#freshProviderSessionId = undefined;
|
|
6231
6670
|
this.#syncAgentSessionId();
|
|
6232
6671
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
6233
6672
|
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
|
6234
6673
|
this.#resetHindsightConversationTrackingIfHindsight();
|
|
6235
6674
|
this.#resetMnemopiConversationTrackingIfMnemopi();
|
|
6236
|
-
this.#steeringMessages = [];
|
|
6237
|
-
this.#followUpMessages = [];
|
|
6238
6675
|
this.#pendingNextTurnMessages = [];
|
|
6239
6676
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
6240
6677
|
this.#todoReminderCount = 0;
|
|
@@ -6407,7 +6844,10 @@ export class AgentSession {
|
|
|
6407
6844
|
model: `${assistantMessage.provider}/${assistantMessage.model}`,
|
|
6408
6845
|
strategy: incompleteCompactionSettings.strategy,
|
|
6409
6846
|
});
|
|
6410
|
-
await this.#runAutoCompaction("incomplete", true, false, allowDefer, {
|
|
6847
|
+
await this.#runAutoCompaction("incomplete", true, false, allowDefer, {
|
|
6848
|
+
autoContinue,
|
|
6849
|
+
triggerContextTokens: calculateContextTokens(assistantMessage.usage),
|
|
6850
|
+
});
|
|
6411
6851
|
} else {
|
|
6412
6852
|
// Neither promotion nor compaction is available — surface the dead-end so
|
|
6413
6853
|
// the user understands why the turn yielded with nothing.
|
|
@@ -6418,6 +6858,11 @@ export class AgentSession {
|
|
|
6418
6858
|
return false;
|
|
6419
6859
|
}
|
|
6420
6860
|
|
|
6861
|
+
// Stale-result pass runs every turn, before any threshold gating: it is
|
|
6862
|
+
// cheap (bails when no candidate) and independent of the compaction
|
|
6863
|
+
// setting.
|
|
6864
|
+
const supersedeResult = await this.#pruneStaleToolResults();
|
|
6865
|
+
|
|
6421
6866
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6422
6867
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
|
|
6423
6868
|
|
|
@@ -6426,6 +6871,9 @@ export class AgentSession {
|
|
|
6426
6871
|
if (assistantMessage.stopReason === "error") return false;
|
|
6427
6872
|
const pruneResult = await this.#pruneToolOutputs();
|
|
6428
6873
|
let contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
6874
|
+
if (supersedeResult) {
|
|
6875
|
+
contextTokens = Math.max(0, contextTokens - supersedeResult.tokensSaved);
|
|
6876
|
+
}
|
|
6429
6877
|
if (pruneResult) {
|
|
6430
6878
|
contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
6431
6879
|
}
|
|
@@ -6433,7 +6881,10 @@ export class AgentSession {
|
|
|
6433
6881
|
// Try promotion first — if a larger model is available, switch instead of compacting
|
|
6434
6882
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
6435
6883
|
if (!promoted) {
|
|
6436
|
-
return await this.#runAutoCompaction("threshold", false, false, allowDefer, {
|
|
6884
|
+
return await this.#runAutoCompaction("threshold", false, false, allowDefer, {
|
|
6885
|
+
autoContinue,
|
|
6886
|
+
triggerContextTokens: contextTokens,
|
|
6887
|
+
});
|
|
6437
6888
|
}
|
|
6438
6889
|
}
|
|
6439
6890
|
return false;
|
|
@@ -6471,9 +6922,13 @@ export class AgentSession {
|
|
|
6471
6922
|
this.#retryAttempt = 0;
|
|
6472
6923
|
}
|
|
6473
6924
|
this.#resolveRetry();
|
|
6925
|
+
// Tool-use orphans corrupt Anthropic message history (tool_result without
|
|
6926
|
+
// matching tool_use). Always remove them even when the retry cap is hit.
|
|
6927
|
+
if (assistantMessage.stopReason === "toolUse") {
|
|
6928
|
+
this.#removeEmptyStopFromActiveContext(assistantMessage);
|
|
6929
|
+
}
|
|
6474
6930
|
return true;
|
|
6475
6931
|
}
|
|
6476
|
-
|
|
6477
6932
|
this.#removeEmptyStopFromActiveContext(assistantMessage);
|
|
6478
6933
|
this.agent.appendMessage({
|
|
6479
6934
|
role: "developer",
|
|
@@ -6486,12 +6941,27 @@ export class AgentSession {
|
|
|
6486
6941
|
}
|
|
6487
6942
|
|
|
6488
6943
|
#isEmptyAssistantStop(assistantMessage: AssistantMessage): boolean {
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6944
|
+
switch (assistantMessage.stopReason) {
|
|
6945
|
+
case "stop":
|
|
6946
|
+
// Reasoning/thinking-only turns are not actionable: they do not
|
|
6947
|
+
// answer the user and do not give the agent loop a tool call to run.
|
|
6948
|
+
for (const content of assistantMessage.content) {
|
|
6949
|
+
if (content.type === "toolCall") return false;
|
|
6950
|
+
if (content.type === "text" && hasNonWhitespace(content.text)) return false;
|
|
6951
|
+
}
|
|
6952
|
+
return true;
|
|
6953
|
+
case "toolUse":
|
|
6954
|
+
// An orphaned toolUse stop (no tool_use block) corrupts Anthropic history:
|
|
6955
|
+
// a later tool_result has nothing to anchor to. Thinking alone cannot anchor
|
|
6956
|
+
// a tool_result, so it does not rescue a toolUse stop here.
|
|
6957
|
+
for (const content of assistantMessage.content) {
|
|
6958
|
+
if (content.type === "toolCall") return false;
|
|
6959
|
+
if (content.type === "text" && hasNonWhitespace(content.text)) return false;
|
|
6960
|
+
}
|
|
6961
|
+
return true;
|
|
6962
|
+
default:
|
|
6963
|
+
return false;
|
|
6964
|
+
}
|
|
6495
6965
|
}
|
|
6496
6966
|
|
|
6497
6967
|
#emptyStopRetryReminder(): string {
|
|
@@ -6628,10 +7098,21 @@ export class AgentSession {
|
|
|
6628
7098
|
});
|
|
6629
7099
|
}
|
|
6630
7100
|
|
|
6631
|
-
#
|
|
6632
|
-
const
|
|
7101
|
+
#buildEagerPreludeContext(): { toolRefs: Record<string, string>; taskBatch: boolean } {
|
|
7102
|
+
const wireName = (name: string): string => {
|
|
7103
|
+
const tool = this.#toolRegistry.get(name);
|
|
7104
|
+
return typeof tool?.customWireName === "string" ? tool.customWireName : name;
|
|
7105
|
+
};
|
|
7106
|
+
return {
|
|
7107
|
+
toolRefs: { task: wireName("task"), todo: wireName("todo") },
|
|
7108
|
+
taskBatch: this.settings.get("task.batch"),
|
|
7109
|
+
};
|
|
7110
|
+
}
|
|
7111
|
+
|
|
7112
|
+
#createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice?: ToolChoice } | undefined {
|
|
7113
|
+
const mode = this.settings.get("todo.eager");
|
|
6633
7114
|
const todosEnabled = this.settings.get("todo.enabled");
|
|
6634
|
-
if (
|
|
7115
|
+
if (mode === "default" || !todosEnabled) {
|
|
6635
7116
|
return undefined;
|
|
6636
7117
|
}
|
|
6637
7118
|
|
|
@@ -6655,36 +7136,63 @@ export class AgentSession {
|
|
|
6655
7136
|
return undefined;
|
|
6656
7137
|
}
|
|
6657
7138
|
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
7139
|
+
// Must check the active tool set, not just the registry: tool discovery
|
|
7140
|
+
// (tools.discoveryMode === "all") can register `todo` while hiding it from
|
|
7141
|
+
// the exposed tools. Forcing a named tool_choice for an inactive tool makes
|
|
7142
|
+
// the provider reject the request (HTTP 400).
|
|
7143
|
+
if (!this.getActiveToolNames().includes("todo")) {
|
|
7144
|
+
logger.warn("Eager todo enforcement skipped because todo is not active", {
|
|
7145
|
+
activeToolNames: this.getActiveToolNames(),
|
|
6661
7146
|
});
|
|
6662
7147
|
return undefined;
|
|
6663
7148
|
}
|
|
6664
7149
|
|
|
7150
|
+
const message: AgentMessage = {
|
|
7151
|
+
role: "custom",
|
|
7152
|
+
customType: "eager-todo-prelude",
|
|
7153
|
+
content: prompt.render(eagerTodoPrompt, { ...this.#buildEagerPreludeContext(), forced: mode === "always" }),
|
|
7154
|
+
display: false,
|
|
7155
|
+
attribution: "agent",
|
|
7156
|
+
timestamp: Date.now(),
|
|
7157
|
+
};
|
|
7158
|
+
if (mode === "preferred") {
|
|
7159
|
+
return { message };
|
|
7160
|
+
}
|
|
6665
7161
|
const todoToolChoice = buildNamedToolChoice("todo", this.model);
|
|
6666
7162
|
if (!todoToolChoice) {
|
|
6667
|
-
logger.warn(
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
7163
|
+
logger.warn(
|
|
7164
|
+
"Eager todo proceeding with the reminder only because the current model does not support a forced todo tool_choice",
|
|
7165
|
+
{
|
|
7166
|
+
modelApi: this.model?.api,
|
|
7167
|
+
modelId: this.model?.id,
|
|
7168
|
+
},
|
|
7169
|
+
);
|
|
7170
|
+
return { message };
|
|
6672
7171
|
}
|
|
6673
|
-
|
|
6674
|
-
const eagerTodoReminder = prompt.render(eagerTodoPrompt);
|
|
6675
|
-
|
|
6676
7172
|
return {
|
|
6677
|
-
message
|
|
6678
|
-
role: "custom",
|
|
6679
|
-
customType: "eager-todo-prelude",
|
|
6680
|
-
content: eagerTodoReminder,
|
|
6681
|
-
display: false,
|
|
6682
|
-
attribution: "agent",
|
|
6683
|
-
timestamp: Date.now(),
|
|
6684
|
-
},
|
|
7173
|
+
message,
|
|
6685
7174
|
toolChoice: todoToolChoice,
|
|
6686
7175
|
};
|
|
6687
7176
|
}
|
|
7177
|
+
|
|
7178
|
+
#createEagerTaskPrelude(promptText: string): AgentMessage | undefined {
|
|
7179
|
+
if (this.settings.get("task.eager") !== "always") return undefined;
|
|
7180
|
+
if (this.#agentKind === "sub") return undefined;
|
|
7181
|
+
if (this.#planModeState?.enabled) return undefined;
|
|
7182
|
+
if (this.agent.state.messages.some(m => m.role === "user")) return undefined;
|
|
7183
|
+
const trimmed = promptText.trimEnd();
|
|
7184
|
+
if (trimmed.endsWith("?") || trimmed.endsWith("!")) return undefined;
|
|
7185
|
+
if (!this.getActiveToolNames().includes("task")) return undefined;
|
|
7186
|
+
return {
|
|
7187
|
+
role: "custom",
|
|
7188
|
+
customType: "eager-task-prelude",
|
|
7189
|
+
content: prompt.render(eagerTaskPrompt, this.#buildEagerPreludeContext()),
|
|
7190
|
+
display: false,
|
|
7191
|
+
attribution: "agent",
|
|
7192
|
+
timestamp: Date.now(),
|
|
7193
|
+
};
|
|
7194
|
+
}
|
|
7195
|
+
|
|
6688
7196
|
/**
|
|
6689
7197
|
* Check if agent stopped with incomplete todos and prompt to continue.
|
|
6690
7198
|
*/
|
|
@@ -6807,7 +7315,7 @@ export class AgentSession {
|
|
|
6807
7315
|
const candidate = this.#resolveContextPromotionConfiguredTarget(currentModel, availableModels);
|
|
6808
7316
|
if (!candidate) return undefined;
|
|
6809
7317
|
if (modelsAreEqual(candidate, currentModel)) return undefined;
|
|
6810
|
-
if (candidate.contextWindow <= contextWindow) return undefined;
|
|
7318
|
+
if (candidate.contextWindow == null || candidate.contextWindow <= contextWindow) return undefined;
|
|
6811
7319
|
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
6812
7320
|
if (!apiKey) return undefined;
|
|
6813
7321
|
return candidate;
|
|
@@ -6819,7 +7327,6 @@ export class AgentSession {
|
|
|
6819
7327
|
this.#closeProviderSessionsForModelSwitch(currentModel, model);
|
|
6820
7328
|
}
|
|
6821
7329
|
this.agent.setModel(model);
|
|
6822
|
-
this.#syncToolCallBatchCap(model);
|
|
6823
7330
|
|
|
6824
7331
|
// Re-evaluate append-only context mode — provider or setting may have changed
|
|
6825
7332
|
this.#syncAppendOnlyContext(model);
|
|
@@ -6831,6 +7338,22 @@ export class AgentSession {
|
|
|
6831
7338
|
this.#closeProviderSessionsForModelSwitch(currentModel, currentModel);
|
|
6832
7339
|
}
|
|
6833
7340
|
|
|
7341
|
+
#resetCurrentResponsesProviderSession(reason: string): void {
|
|
7342
|
+
const currentModel = this.model;
|
|
7343
|
+
if (currentModel?.api !== "openai-responses" && currentModel?.api !== "openai-codex-responses") {
|
|
7344
|
+
return;
|
|
7345
|
+
}
|
|
7346
|
+
|
|
7347
|
+
this.#closeProviderSessionsForModelSwitch(currentModel, currentModel);
|
|
7348
|
+
this.agent.appendOnlyContext?.invalidateForModelChange();
|
|
7349
|
+
logger.debug("Reset Responses provider session after stale replay error", {
|
|
7350
|
+
provider: currentModel.provider,
|
|
7351
|
+
model: currentModel.id,
|
|
7352
|
+
api: currentModel.api,
|
|
7353
|
+
reason,
|
|
7354
|
+
});
|
|
7355
|
+
}
|
|
7356
|
+
|
|
6834
7357
|
/**
|
|
6835
7358
|
* Re-evaluate append-only context mode, creating or destroying the
|
|
6836
7359
|
* manager as needed. Called on model switch AND setting change.
|
|
@@ -7075,7 +7598,7 @@ export class AgentSession {
|
|
|
7075
7598
|
|
|
7076
7599
|
return resolveModelRoleValue(roleModelStr, availableModels, {
|
|
7077
7600
|
settings: this.settings,
|
|
7078
|
-
matchPreferences:
|
|
7601
|
+
matchPreferences: getModelMatchPreferences(this.settings),
|
|
7079
7602
|
modelRegistry: this.#modelRegistry,
|
|
7080
7603
|
});
|
|
7081
7604
|
}
|
|
@@ -7100,10 +7623,11 @@ export class AgentSession {
|
|
|
7100
7623
|
// as auth fallbacks when the current model has no usable credentials.
|
|
7101
7624
|
addCandidate(currentModel);
|
|
7102
7625
|
for (const role of MODEL_ROLE_IDS) {
|
|
7626
|
+
if (MODEL_ROLES[role]?.hidden) continue;
|
|
7103
7627
|
addCandidate(this.#resolveRoleModelFull(role, availableModels, currentModel).model);
|
|
7104
7628
|
}
|
|
7105
7629
|
|
|
7106
|
-
const sortedByContext = [...availableModels].sort((a, b) => b.contextWindow - a.contextWindow);
|
|
7630
|
+
const sortedByContext = [...availableModels].sort((a, b) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0));
|
|
7107
7631
|
for (const model of sortedByContext) {
|
|
7108
7632
|
if (!seen.has(this.#getModelKey(model))) {
|
|
7109
7633
|
addCandidate(model);
|
|
@@ -7153,17 +7677,24 @@ export class AgentSession {
|
|
|
7153
7677
|
if (!apiKey) continue;
|
|
7154
7678
|
|
|
7155
7679
|
try {
|
|
7156
|
-
return await compact(
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7680
|
+
return await compact(
|
|
7681
|
+
this.#obfuscatePreparationForProvider(preparation),
|
|
7682
|
+
candidate,
|
|
7683
|
+
this.#modelRegistry.resolver(candidate, this.sessionId),
|
|
7684
|
+
this.#obfuscateTextForProvider(customInstructions),
|
|
7685
|
+
signal,
|
|
7686
|
+
{
|
|
7687
|
+
...options,
|
|
7688
|
+
metadata: this.agent.metadataForProvider(candidate.provider),
|
|
7689
|
+
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
7690
|
+
telemetry,
|
|
7691
|
+
// Honor the user's /model thinking selection (incl. `off`) on
|
|
7692
|
+
// the manual `/compact` path. Clamped per-model inside compact()
|
|
7693
|
+
// via resolveCompactionEffort so unsupported-effort models
|
|
7694
|
+
// (xai-oauth/grok-build) don't trip requireSupportedEffort.
|
|
7695
|
+
thinkingLevel: this.thinkingLevel,
|
|
7696
|
+
},
|
|
7697
|
+
);
|
|
7167
7698
|
} catch (error) {
|
|
7168
7699
|
if (!this.#isCompactionAuthFailure(error)) {
|
|
7169
7700
|
throw error;
|
|
@@ -7250,7 +7781,7 @@ export class AgentSession {
|
|
|
7250
7781
|
willRetry: boolean,
|
|
7251
7782
|
deferred = false,
|
|
7252
7783
|
allowDefer = true,
|
|
7253
|
-
options: { autoContinue?: boolean } = {},
|
|
7784
|
+
options: { autoContinue?: boolean; triggerContextTokens?: number } = {},
|
|
7254
7785
|
): Promise<boolean> {
|
|
7255
7786
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
7256
7787
|
if (compactionSettings.strategy === "off") return false;
|
|
@@ -7261,7 +7792,13 @@ export class AgentSession {
|
|
|
7261
7792
|
// reclaims nothing we fall through to the summary-compaction body below so
|
|
7262
7793
|
// the oversized input still gets resolved.
|
|
7263
7794
|
if (compactionSettings.strategy === "shake") {
|
|
7264
|
-
const outcome = await this.#runAutoShake(
|
|
7795
|
+
const outcome = await this.#runAutoShake(
|
|
7796
|
+
reason,
|
|
7797
|
+
willRetry,
|
|
7798
|
+
generation,
|
|
7799
|
+
shouldAutoContinue,
|
|
7800
|
+
options.triggerContextTokens,
|
|
7801
|
+
);
|
|
7265
7802
|
if (outcome !== "fallback") return false;
|
|
7266
7803
|
}
|
|
7267
7804
|
// "overflow" and "incomplete" force inline execution because they are recovery
|
|
@@ -7288,9 +7825,25 @@ export class AgentSession {
|
|
|
7288
7825
|
|
|
7289
7826
|
// "overflow" forces context-full because the input itself is broken — a handoff
|
|
7290
7827
|
// LLM call would hit the same overflow. "incomplete" is an output-side problem,
|
|
7291
|
-
// so a handoff request on the existing context is still viable.
|
|
7292
|
-
|
|
7828
|
+
// so a handoff request on the existing context is still viable. Snapcompact is
|
|
7829
|
+
// safe for every reason (it makes no LLM call at all) but requires a vision
|
|
7830
|
+
// model to be worth anything — fall back to context-full otherwise.
|
|
7831
|
+
let action: "context-full" | "handoff" | "snapcompact" =
|
|
7293
7832
|
compactionSettings.strategy === "handoff" && reason !== "overflow" ? "handoff" : "context-full";
|
|
7833
|
+
if (compactionSettings.strategy === "snapcompact") {
|
|
7834
|
+
if (this.model?.input.includes("image")) {
|
|
7835
|
+
action = "snapcompact";
|
|
7836
|
+
} else {
|
|
7837
|
+
logger.warn("Snapcompact compaction requires a vision-capable model; falling back to context-full", {
|
|
7838
|
+
model: this.model?.id,
|
|
7839
|
+
});
|
|
7840
|
+
this.emitNotice(
|
|
7841
|
+
"warning",
|
|
7842
|
+
`snapcompact needs a vision-capable model (${this.model?.id ?? "unknown"} is text-only) — using an LLM summary instead`,
|
|
7843
|
+
"compaction",
|
|
7844
|
+
);
|
|
7845
|
+
}
|
|
7846
|
+
}
|
|
7294
7847
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
7295
7848
|
// Abort any older auto-compaction before installing this run's controller.
|
|
7296
7849
|
this.#autoCompactionAbortController?.abort();
|
|
@@ -7429,6 +7982,21 @@ export class AgentSession {
|
|
|
7429
7982
|
tokensBefore = compactionPrep.tokensBefore;
|
|
7430
7983
|
details = compactionPrep.details;
|
|
7431
7984
|
preserveData = compactionPrep.preserveData;
|
|
7985
|
+
} else if (action === "snapcompact") {
|
|
7986
|
+
// Local, deterministic: render discarded history onto PNG frames.
|
|
7987
|
+
// No model candidates, no API key, no retry loop.
|
|
7988
|
+
const snapcompactResult = await snapcompact.compact(preparation, {
|
|
7989
|
+
convertToLlm,
|
|
7990
|
+
model: this.model,
|
|
7991
|
+
thinkingLevel: this.thinkingLevel,
|
|
7992
|
+
maxFrames: snapcompact.providerFrameBudget(this.model?.provider),
|
|
7993
|
+
});
|
|
7994
|
+
summary = snapcompactResult.summary;
|
|
7995
|
+
shortSummary = snapcompactResult.shortSummary;
|
|
7996
|
+
firstKeptEntryId = snapcompactResult.firstKeptEntryId;
|
|
7997
|
+
tokensBefore = snapcompactResult.tokensBefore;
|
|
7998
|
+
details = snapcompactResult.details;
|
|
7999
|
+
preserveData = { ...(compactionPrep.preserveData ?? {}), ...(snapcompactResult.preserveData ?? {}) };
|
|
7432
8000
|
} else {
|
|
7433
8001
|
const candidates = this.#getCompactionModelCandidates(availableModels);
|
|
7434
8002
|
const retrySettings = this.settings.getGroup("retry");
|
|
@@ -7443,20 +8011,27 @@ export class AgentSession {
|
|
|
7443
8011
|
let attempt = 0;
|
|
7444
8012
|
while (true) {
|
|
7445
8013
|
try {
|
|
7446
|
-
compactResult = await compact(
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
8014
|
+
compactResult = await compact(
|
|
8015
|
+
this.#obfuscatePreparationForProvider(preparation),
|
|
8016
|
+
candidate,
|
|
8017
|
+
this.#modelRegistry.resolver(candidate, this.sessionId),
|
|
8018
|
+
undefined,
|
|
8019
|
+
autoCompactionSignal,
|
|
8020
|
+
{
|
|
8021
|
+
promptOverride: this.#obfuscateTextForProvider(compactionPrep.hookPrompt),
|
|
8022
|
+
extraContext: this.#obfuscateForProvider(compactionPrep.hookContext),
|
|
8023
|
+
remoteInstructions: this.#obfuscateForProvider(this.#baseSystemPrompt.join("\n\n")),
|
|
8024
|
+
metadata: this.agent.metadataForProvider(candidate.provider),
|
|
8025
|
+
initiatorOverride: "agent",
|
|
8026
|
+
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
8027
|
+
telemetry,
|
|
8028
|
+
// Honor the user's /model thinking selection on the
|
|
8029
|
+
// auto-compaction path — the most-fired compaction
|
|
8030
|
+
// site. Clamped per-model inside compact() via
|
|
8031
|
+
// resolveCompactionEffort.
|
|
8032
|
+
thinkingLevel: this.thinkingLevel,
|
|
8033
|
+
},
|
|
8034
|
+
);
|
|
7460
8035
|
break;
|
|
7461
8036
|
} catch (error) {
|
|
7462
8037
|
if (autoCompactionSignal.aborted) {
|
|
@@ -7661,6 +8236,7 @@ export class AgentSession {
|
|
|
7661
8236
|
willRetry: boolean,
|
|
7662
8237
|
generation: number,
|
|
7663
8238
|
autoContinue: boolean,
|
|
8239
|
+
triggerContextTokens?: number,
|
|
7664
8240
|
): Promise<"handled" | "fallback"> {
|
|
7665
8241
|
const action = "shake";
|
|
7666
8242
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
@@ -7681,16 +8257,52 @@ export class AgentSession {
|
|
|
7681
8257
|
return "handled";
|
|
7682
8258
|
}
|
|
7683
8259
|
const reclaimed = result.toolResultsDropped + result.blocksDropped > 0;
|
|
7684
|
-
//
|
|
7685
|
-
//
|
|
7686
|
-
|
|
8260
|
+
// Detect the dead-loop reported in issues #2119/#2275: the threshold check
|
|
8261
|
+
// fires, shake runs, but residual context is still above the configured
|
|
8262
|
+
// threshold. The next agent_end would re-trigger shake, which has nothing
|
|
8263
|
+
// new to drop on the second pass, so the loop spins until the user kills it.
|
|
8264
|
+
// Same hazard for "incomplete" (the retry would re-hit the length cap) and
|
|
8265
|
+
// for the existing "overflow + nothing reclaimed" case. In every recovery
|
|
8266
|
+
// reason we hand off to the summarization-driven context-full path so the
|
|
8267
|
+
// situation actually resolves; "idle" is exempt because its 60s+ timer
|
|
8268
|
+
// re-checks usage before re-firing and cannot dead-loop on its own.
|
|
8269
|
+
//
|
|
8270
|
+
// #2275: the post-shake check MUST be anchored on the same metric that
|
|
8271
|
+
// triggered compaction. The local estimator (`#estimatePendingPromptTokens`)
|
|
8272
|
+
// undercounts thinking-signature payloads, so on thinking-heavy sessions it
|
|
8273
|
+
// reads well below the provider-reported usage that fired the threshold.
|
|
8274
|
+
// When that estimate slips under the threshold, the fallback never fires
|
|
8275
|
+
// and the auto-continue prompt re-injects every turn. Prefer the trigger's
|
|
8276
|
+
// own `contextTokens` (provider-anchored) when the caller supplies it, and
|
|
8277
|
+
// add hysteresis (80% recovery band) so we don't oscillate at the boundary
|
|
8278
|
+
// while shake keeps reclaiming a trickle of the previous turn's output.
|
|
8279
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
8280
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
8281
|
+
let stillOverThreshold = false;
|
|
8282
|
+
if (contextWindow > 0) {
|
|
8283
|
+
if (typeof triggerContextTokens === "number" && Number.isFinite(triggerContextTokens)) {
|
|
8284
|
+
const correctedTokens = Math.max(0, triggerContextTokens - result.tokensFreed);
|
|
8285
|
+
const thresholdTokens = resolveThresholdTokens(contextWindow, compactionSettings);
|
|
8286
|
+
const recoveryBand = Math.floor(thresholdTokens * SHAKE_RECOVERY_BAND);
|
|
8287
|
+
stillOverThreshold = correctedTokens > recoveryBand;
|
|
8288
|
+
} else {
|
|
8289
|
+
const postShakeTokens = this.#estimatePendingPromptTokens([]);
|
|
8290
|
+
stillOverThreshold = shouldCompact(postShakeTokens, contextWindow, compactionSettings);
|
|
8291
|
+
}
|
|
8292
|
+
}
|
|
8293
|
+
const shouldFallBack = reason !== "idle" && ((reason === "overflow" && !reclaimed) || stillOverThreshold);
|
|
8294
|
+
if (shouldFallBack) {
|
|
8295
|
+
const errorMessage = reclaimed
|
|
8296
|
+
? `Auto-shake reclaimed ~${result.tokensFreed} tokens but context is still above the threshold; falling back to context-full compaction.`
|
|
8297
|
+
: "Auto-shake found nothing eligible to drop; falling back to context-full compaction.";
|
|
7687
8298
|
await this.#emitSessionEvent({
|
|
7688
8299
|
type: "auto_compaction_end",
|
|
7689
8300
|
action,
|
|
7690
8301
|
result: undefined,
|
|
7691
8302
|
aborted: false,
|
|
7692
8303
|
willRetry: false,
|
|
7693
|
-
skipped:
|
|
8304
|
+
skipped: !reclaimed,
|
|
8305
|
+
errorMessage,
|
|
7694
8306
|
});
|
|
7695
8307
|
return "fallback";
|
|
7696
8308
|
}
|
|
@@ -7788,10 +8400,40 @@ export class AgentSession {
|
|
|
7788
8400
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
7789
8401
|
if (isContextOverflow(message, contextWindow)) return false;
|
|
7790
8402
|
|
|
8403
|
+
if (this.#isClassifierRefusal(message)) return true;
|
|
8404
|
+
if (this.#isStaleOpenAIResponsesReplayError(message)) return true;
|
|
8405
|
+
|
|
7791
8406
|
const err = message.errorMessage;
|
|
7792
8407
|
return this.#isTransientErrorMessage(err) || isUsageLimitError(err);
|
|
7793
8408
|
}
|
|
7794
8409
|
|
|
8410
|
+
#isStaleOpenAIResponsesReplayError(message: AssistantMessage): boolean {
|
|
8411
|
+
const currentApi = this.model?.api;
|
|
8412
|
+
if (
|
|
8413
|
+
message.api !== "openai-responses" &&
|
|
8414
|
+
message.api !== "openai-codex-responses" &&
|
|
8415
|
+
currentApi !== "openai-responses" &&
|
|
8416
|
+
currentApi !== "openai-codex-responses"
|
|
8417
|
+
) {
|
|
8418
|
+
return false;
|
|
8419
|
+
}
|
|
8420
|
+
|
|
8421
|
+
const errorMessage = message.errorMessage;
|
|
8422
|
+
if (!errorMessage) return false;
|
|
8423
|
+
|
|
8424
|
+
return (
|
|
8425
|
+
/\bItem with id ['"][^'"]+['"] not found\.?/i.test(errorMessage) ||
|
|
8426
|
+
(/previous[ _]?response/i.test(errorMessage) &&
|
|
8427
|
+
/not[ _]?found|invalid|expired|stale|zero[ _-]?data[ _-]?retention/i.test(errorMessage))
|
|
8428
|
+
);
|
|
8429
|
+
}
|
|
8430
|
+
|
|
8431
|
+
#isClassifierRefusal(message: AssistantMessage): boolean {
|
|
8432
|
+
if (message.stopReason !== "error") return false;
|
|
8433
|
+
const stopType = message.stopDetails?.type;
|
|
8434
|
+
return stopType === "refusal" || stopType === "sensitive";
|
|
8435
|
+
}
|
|
8436
|
+
|
|
7795
8437
|
#isTransientErrorMessage(errorMessage: string): boolean {
|
|
7796
8438
|
return (
|
|
7797
8439
|
this.#isTransientEnvelopeErrorMessage(errorMessage) || this.#isTransientTransportErrorMessage(errorMessage)
|
|
@@ -7806,11 +8448,12 @@ export class AgentSession {
|
|
|
7806
8448
|
#isTransientTransportErrorMessage(errorMessage: string): boolean {
|
|
7807
8449
|
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
|
|
7808
8450
|
// service unavailable, provider-suggested retry, network/connection/socket errors, fetch failed,
|
|
7809
|
-
// terminated, retry delay exceeded, Bun HTTP/2 stream resets
|
|
7810
|
-
// ENHANCE_YOUR_CALM, surfaced verbatim from
|
|
8451
|
+
// gateway upstream failures, terminated, retry delay exceeded, Bun HTTP/2 stream resets
|
|
8452
|
+
// (RST_STREAM / REFUSED_STREAM / ENHANCE_YOUR_CALM, surfaced verbatim from
|
|
8453
|
+
// src/http/h2_client/dispatch.zig)
|
|
7811
8454
|
return (
|
|
7812
8455
|
isUnexpectedSocketCloseMessage(errorMessage) ||
|
|
7813
|
-
/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)/i.test(
|
|
8456
|
+
/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)/i.test(
|
|
7814
8457
|
errorMessage,
|
|
7815
8458
|
)
|
|
7816
8459
|
);
|
|
@@ -7934,6 +8577,7 @@ export class AgentSession {
|
|
|
7934
8577
|
role: string,
|
|
7935
8578
|
selector: RetryFallbackSelector,
|
|
7936
8579
|
currentSelector: string,
|
|
8580
|
+
options?: { pinFallback?: boolean },
|
|
7937
8581
|
): Promise<void> {
|
|
7938
8582
|
const candidate = this.#modelRegistry.find(selector.provider, selector.id);
|
|
7939
8583
|
if (!candidate) {
|
|
@@ -7959,9 +8603,11 @@ export class AgentSession {
|
|
|
7959
8603
|
originalSelector: currentSelector,
|
|
7960
8604
|
originalThinkingLevel: currentThinkingLevel,
|
|
7961
8605
|
lastAppliedFallbackThinkingLevel: nextThinkingLevel,
|
|
8606
|
+
pinned: options?.pinFallback === true,
|
|
7962
8607
|
};
|
|
7963
8608
|
} else {
|
|
7964
8609
|
this.#activeRetryFallback.lastAppliedFallbackThinkingLevel = nextThinkingLevel;
|
|
8610
|
+
this.#activeRetryFallback.pinned = this.#activeRetryFallback.pinned || options?.pinFallback === true;
|
|
7965
8611
|
}
|
|
7966
8612
|
await this.#emitSessionEvent({
|
|
7967
8613
|
type: "retry_fallback_applied",
|
|
@@ -7971,7 +8617,7 @@ export class AgentSession {
|
|
|
7971
8617
|
});
|
|
7972
8618
|
}
|
|
7973
8619
|
|
|
7974
|
-
async #tryRetryModelFallback(currentSelector: string): Promise<boolean> {
|
|
8620
|
+
async #tryRetryModelFallback(currentSelector: string, options?: { pinFallback?: boolean }): Promise<boolean> {
|
|
7975
8621
|
const role = this.#activeRetryFallback?.role ?? this.#resolveRetryFallbackRole(currentSelector);
|
|
7976
8622
|
if (!role) return false;
|
|
7977
8623
|
|
|
@@ -7981,7 +8627,7 @@ export class AgentSession {
|
|
|
7981
8627
|
if (!candidate) continue;
|
|
7982
8628
|
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
7983
8629
|
if (!apiKey) continue;
|
|
7984
|
-
await this.#applyRetryFallbackCandidate(role, selector, currentSelector);
|
|
8630
|
+
await this.#applyRetryFallbackCandidate(role, selector, currentSelector, options);
|
|
7985
8631
|
return true;
|
|
7986
8632
|
}
|
|
7987
8633
|
|
|
@@ -7990,6 +8636,7 @@ export class AgentSession {
|
|
|
7990
8636
|
|
|
7991
8637
|
async #maybeRestoreRetryFallbackPrimary(): Promise<void> {
|
|
7992
8638
|
if (!this.#activeRetryFallback) return;
|
|
8639
|
+
if (this.#activeRetryFallback.pinned) return;
|
|
7993
8640
|
if (this.#getRetryFallbackRevertPolicy() !== "cooldown-expiry") return;
|
|
7994
8641
|
|
|
7995
8642
|
const {
|
|
@@ -8087,6 +8734,7 @@ export class AgentSession {
|
|
|
8087
8734
|
async #handleRetryableError(message: AssistantMessage): Promise<boolean> {
|
|
8088
8735
|
const retrySettings = this.settings.getGroup("retry");
|
|
8089
8736
|
if (!retrySettings.enabled) return false;
|
|
8737
|
+
const classifierRefusal = this.#isClassifierRefusal(message);
|
|
8090
8738
|
|
|
8091
8739
|
const generation = this.#promptGeneration;
|
|
8092
8740
|
this.#retryAttempt++;
|
|
@@ -8113,42 +8761,83 @@ export class AgentSession {
|
|
|
8113
8761
|
}
|
|
8114
8762
|
|
|
8115
8763
|
const errorMessage = message.errorMessage || "Unknown error";
|
|
8764
|
+
const staleOpenAIResponsesReplayError = this.#isStaleOpenAIResponsesReplayError(message);
|
|
8116
8765
|
const parsedRetryAfterMs = this.#parseRetryAfterMsFromError(errorMessage);
|
|
8117
|
-
let delayMs =
|
|
8766
|
+
let delayMs = staleOpenAIResponsesReplayError
|
|
8767
|
+
? 0
|
|
8768
|
+
: calculateRetryBackoffDelayMs(retrySettings.baseDelayMs, this.#retryAttempt);
|
|
8118
8769
|
let switchedCredential = false;
|
|
8119
8770
|
let switchedModel = false;
|
|
8771
|
+
// Set when a usage-limit error pinned the wait to credential
|
|
8772
|
+
// availability — suppresses the generic retry-after bump below.
|
|
8773
|
+
let usageLimitWaitMs: number | undefined;
|
|
8774
|
+
|
|
8775
|
+
if (staleOpenAIResponsesReplayError) {
|
|
8776
|
+
this.#resetCurrentResponsesProviderSession("stale replay error");
|
|
8777
|
+
}
|
|
8120
8778
|
|
|
8121
|
-
if (this.model && isUsageLimitError(errorMessage)) {
|
|
8779
|
+
if (this.model && !staleOpenAIResponsesReplayError && isUsageLimitError(errorMessage)) {
|
|
8122
8780
|
const retryAfterMs = parsedRetryAfterMs ?? calculateRateLimitBackoffMs(parseRateLimitReason(errorMessage));
|
|
8123
|
-
const
|
|
8781
|
+
const outcome = await this.#modelRegistry.authStorage.markUsageLimitReached(
|
|
8124
8782
|
this.model.provider,
|
|
8125
8783
|
this.sessionId,
|
|
8126
8784
|
{
|
|
8127
8785
|
retryAfterMs,
|
|
8128
8786
|
baseUrl: this.model.baseUrl,
|
|
8787
|
+
modelId: this.model.id,
|
|
8129
8788
|
},
|
|
8130
8789
|
);
|
|
8131
|
-
if (switched) {
|
|
8790
|
+
if (outcome.switched) {
|
|
8132
8791
|
switchedCredential = true;
|
|
8133
8792
|
delayMs = 0;
|
|
8134
|
-
} else if (
|
|
8135
|
-
//
|
|
8136
|
-
|
|
8793
|
+
} else if (await this.#maybeAutoRedeemCodexReset()) {
|
|
8794
|
+
// A live usage-limit 429 on the active Codex account, with a banked
|
|
8795
|
+
// reset and the opt-in setting on: spend the reset and retry
|
|
8796
|
+
// immediately instead of waiting out the window. Runs after the
|
|
8797
|
+
// free sibling-switch above and before model fallback below.
|
|
8798
|
+
switchedCredential = true;
|
|
8799
|
+
delayMs = 0;
|
|
8800
|
+
} else {
|
|
8801
|
+
// No sibling credential is usable right now. Wait for whichever
|
|
8802
|
+
// comes first: the provider's retry-after window for the current
|
|
8803
|
+
// account, or the earliest moment a temporarily blocked sibling
|
|
8804
|
+
// frees up (e.g. a 60s post-401 block or a 5-min usage-probe
|
|
8805
|
+
// block) — the next attempt's getApiKey re-ranks and picks it up.
|
|
8806
|
+
// Without this, one short-lived sibling block escalates a
|
|
8807
|
+
// recoverable situation into the provider's multi-hour wait and
|
|
8808
|
+
// trips the fail-fast cap below.
|
|
8809
|
+
usageLimitWaitMs = retryAfterMs;
|
|
8810
|
+
if (outcome.retryAtMs !== undefined) {
|
|
8811
|
+
const siblingWaitMs = Math.max(0, outcome.retryAtMs - Date.now()) + SIBLING_UNBLOCK_BUFFER_MS;
|
|
8812
|
+
if (siblingWaitMs < usageLimitWaitMs) {
|
|
8813
|
+
usageLimitWaitMs = siblingWaitMs;
|
|
8814
|
+
}
|
|
8815
|
+
}
|
|
8816
|
+
if (usageLimitWaitMs > delayMs) {
|
|
8817
|
+
delayMs = usageLimitWaitMs;
|
|
8818
|
+
}
|
|
8137
8819
|
}
|
|
8138
8820
|
}
|
|
8139
8821
|
|
|
8140
8822
|
const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
|
|
8141
|
-
if (!switchedCredential && currentSelector) {
|
|
8823
|
+
if (!staleOpenAIResponsesReplayError && !switchedCredential && currentSelector) {
|
|
8142
8824
|
if (retrySettings.modelFallback) {
|
|
8143
|
-
|
|
8144
|
-
|
|
8825
|
+
if (!classifierRefusal) {
|
|
8826
|
+
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
8827
|
+
}
|
|
8828
|
+
switchedModel = await this.#tryRetryModelFallback(currentSelector, { pinFallback: classifierRefusal });
|
|
8145
8829
|
}
|
|
8146
8830
|
if (switchedModel) {
|
|
8147
8831
|
delayMs = 0;
|
|
8148
|
-
} else if (parsedRetryAfterMs && parsedRetryAfterMs > delayMs) {
|
|
8832
|
+
} else if (usageLimitWaitMs === undefined && parsedRetryAfterMs && parsedRetryAfterMs > delayMs) {
|
|
8149
8833
|
delayMs = parsedRetryAfterMs;
|
|
8150
8834
|
}
|
|
8151
8835
|
}
|
|
8836
|
+
if (classifierRefusal && !switchedModel) {
|
|
8837
|
+
this.#retryAttempt = 0;
|
|
8838
|
+
this.#resolveRetry();
|
|
8839
|
+
return false;
|
|
8840
|
+
}
|
|
8152
8841
|
|
|
8153
8842
|
// Fail-fast cap: if the provider asks us to wait longer than
|
|
8154
8843
|
// retry.maxDelayMs and we have no fallback credential or model to
|
|
@@ -8306,11 +8995,12 @@ export class AgentSession {
|
|
|
8306
8995
|
* @param command The bash command to execute
|
|
8307
8996
|
* @param onChunk Optional streaming callback for output
|
|
8308
8997
|
* @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix)
|
|
8998
|
+
* @param options.useUserShell If true, allow caller to request configured user-shell routing
|
|
8309
8999
|
*/
|
|
8310
9000
|
async executeBash(
|
|
8311
9001
|
command: string,
|
|
8312
9002
|
onChunk?: (chunk: string) => void,
|
|
8313
|
-
options?: { excludeFromContext?: boolean },
|
|
9003
|
+
options?: { excludeFromContext?: boolean; useUserShell?: boolean },
|
|
8314
9004
|
): Promise<BashResult> {
|
|
8315
9005
|
const excludeFromContext = options?.excludeFromContext === true;
|
|
8316
9006
|
const cwd = this.sessionManager.getCwd();
|
|
@@ -8338,6 +9028,7 @@ export class AgentSession {
|
|
|
8338
9028
|
sessionKey: this.sessionId,
|
|
8339
9029
|
timeout: clampTimeout("bash") * 1000,
|
|
8340
9030
|
onMinimizedSave: originalText => this.#saveBashOriginalArtifact(originalText),
|
|
9031
|
+
useUserShell: options?.useUserShell,
|
|
8341
9032
|
});
|
|
8342
9033
|
|
|
8343
9034
|
this.recordBashResult(command, result, options);
|
|
@@ -8463,6 +9154,7 @@ export class AgentSession {
|
|
|
8463
9154
|
sessionId: namespacePythonSessionId(sessionId),
|
|
8464
9155
|
kernelOwnerId: this.#evalKernelOwnerId,
|
|
8465
9156
|
kernelMode: this.settings.get("python.kernelMode"),
|
|
9157
|
+
interpreter: this.settings.get("python.interpreter")?.trim() || undefined,
|
|
8466
9158
|
onChunk,
|
|
8467
9159
|
signal: abortController.signal,
|
|
8468
9160
|
});
|
|
@@ -8589,118 +9281,111 @@ export class AgentSession {
|
|
|
8589
9281
|
}
|
|
8590
9282
|
|
|
8591
9283
|
// =========================================================================
|
|
8592
|
-
//
|
|
9284
|
+
// IRC Delivery
|
|
8593
9285
|
// =========================================================================
|
|
8594
9286
|
|
|
8595
9287
|
/**
|
|
8596
|
-
*
|
|
8597
|
-
*
|
|
9288
|
+
* Deliver an IRC message into this session (recipient side; called by the
|
|
9289
|
+
* IrcBus). Emits the `irc_message` session event for UI cards and injects
|
|
9290
|
+
* the rendered message into the model's context as an `irc:incoming`
|
|
9291
|
+
* custom message:
|
|
9292
|
+
*
|
|
9293
|
+
* - mid-turn → queued on the aside channel and folded in at the next step
|
|
9294
|
+
* boundary (non-interrupting, like async-result deliveries) → "injected";
|
|
9295
|
+
* - idle → starts a real turn with the message so the recipient wakes
|
|
9296
|
+
* → "woken".
|
|
9297
|
+
*
|
|
9298
|
+
* Never blocks on the recipient's turn: the wake turn is fire-and-forget.
|
|
8598
9299
|
*
|
|
8599
|
-
*
|
|
8600
|
-
*
|
|
8601
|
-
*
|
|
8602
|
-
*
|
|
8603
|
-
*
|
|
8604
|
-
*
|
|
9300
|
+
* When the sender expects a reply (`send await:true`) and this session is
|
|
9301
|
+
* mid-turn with async execution disabled, the next step boundary may be
|
|
9302
|
+
* gated on the sender's own batch finishing (blocking task spawns), so a
|
|
9303
|
+
* real reply turn can never happen in time. In that case an ephemeral
|
|
9304
|
+
* side-channel auto-reply is generated from the current context (the old
|
|
9305
|
+
* `respondAsBackground` path) and sent back over the bus on this agent's
|
|
9306
|
+
* behalf.
|
|
8605
9307
|
*/
|
|
8606
|
-
async
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
awaitReply?: boolean;
|
|
8610
|
-
signal?: AbortSignal;
|
|
8611
|
-
}): Promise<{ replyText: string | null }> {
|
|
8612
|
-
const awaitReply = args.awaitReply !== false;
|
|
8613
|
-
const incomingTimestamp = Date.now();
|
|
8614
|
-
const incomingRecord: CustomMessage = {
|
|
8615
|
-
role: "custom",
|
|
8616
|
-
customType: "irc:incoming",
|
|
8617
|
-
content: `[IRC \`${args.from}\` → you]\n\n${args.message}`,
|
|
8618
|
-
display: true,
|
|
8619
|
-
details: { from: args.from, message: args.message },
|
|
8620
|
-
attribution: "agent",
|
|
8621
|
-
timestamp: incomingTimestamp,
|
|
8622
|
-
};
|
|
8623
|
-
void this.#emitSessionEvent({ type: "irc_message", message: incomingRecord });
|
|
8624
|
-
this.#forwardIrcRelayToMain({
|
|
8625
|
-
from: args.from,
|
|
8626
|
-
to: this.#agentId ?? "?",
|
|
8627
|
-
body: args.message,
|
|
8628
|
-
kind: "message",
|
|
8629
|
-
timestamp: incomingTimestamp,
|
|
8630
|
-
});
|
|
8631
|
-
|
|
8632
|
-
this.#queueBackgroundExchangeInjection([incomingRecord]);
|
|
8633
|
-
if (!awaitReply) {
|
|
8634
|
-
return { replyText: null };
|
|
9308
|
+
async deliverIrcMessage(msg: IrcMessage, opts?: { expectsReply?: boolean }): Promise<"injected" | "woken"> {
|
|
9309
|
+
if (this.#isDisposed) {
|
|
9310
|
+
throw new Error("Recipient session is disposed.");
|
|
8635
9311
|
}
|
|
8636
|
-
|
|
8637
|
-
const
|
|
8638
|
-
from: args.from,
|
|
8639
|
-
message: args.message,
|
|
8640
|
-
});
|
|
8641
|
-
const { replyText } = await this.runEphemeralTurn({
|
|
8642
|
-
promptText: incomingPrompt,
|
|
8643
|
-
signal: args.signal,
|
|
8644
|
-
});
|
|
8645
|
-
|
|
8646
|
-
const replyRecord: CustomMessage = {
|
|
9312
|
+
const autoReply = (opts?.expectsReply ?? false) && this.isStreaming && !this.settings.get("async.enabled");
|
|
9313
|
+
const record: CustomMessage = {
|
|
8647
9314
|
role: "custom",
|
|
8648
|
-
customType: "irc:
|
|
8649
|
-
content:
|
|
9315
|
+
customType: "irc:incoming",
|
|
9316
|
+
content: prompt.render(ircIncomingTemplate, {
|
|
9317
|
+
from: msg.from,
|
|
9318
|
+
message: msg.body,
|
|
9319
|
+
replyTo: msg.replyTo ?? "",
|
|
9320
|
+
autoReplied: autoReply,
|
|
9321
|
+
}),
|
|
8650
9322
|
display: true,
|
|
8651
|
-
details: {
|
|
9323
|
+
details: { id: msg.id, from: msg.from, message: msg.body, ...(msg.replyTo ? { replyTo: msg.replyTo } : {}) },
|
|
8652
9324
|
attribution: "agent",
|
|
8653
|
-
timestamp:
|
|
9325
|
+
timestamp: msg.ts,
|
|
8654
9326
|
};
|
|
8655
|
-
void this.#emitSessionEvent({ type: "irc_message", message:
|
|
8656
|
-
this
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
9327
|
+
void this.#emitSessionEvent({ type: "irc_message", message: record });
|
|
9328
|
+
if (this.isStreaming) {
|
|
9329
|
+
this.#pendingIrcAsides.push(record);
|
|
9330
|
+
if (autoReply) void this.#runIrcAutoReply(msg);
|
|
9331
|
+
return "injected";
|
|
9332
|
+
}
|
|
9333
|
+
// Idle: same wake primitive the yield queue uses for async-result
|
|
9334
|
+
// delivery — prompt the agent directly so a real turn runs.
|
|
9335
|
+
this.agent.prompt(record).catch(error => {
|
|
9336
|
+
logger.warn("IRC wake turn failed", { from: msg.from, to: msg.to, error: String(error) });
|
|
8662
9337
|
});
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
return { replyText };
|
|
9338
|
+
return "woken";
|
|
8666
9339
|
}
|
|
8667
9340
|
|
|
8668
9341
|
/**
|
|
8669
|
-
*
|
|
8670
|
-
*
|
|
8671
|
-
*
|
|
8672
|
-
*
|
|
9342
|
+
* Generate and deliver an ephemeral auto-reply to `msg` on this agent's
|
|
9343
|
+
* behalf: a no-tools side-channel turn over the current history (same
|
|
9344
|
+
* pipeline as `/btw`), recorded into this session as an `irc:autoreply`
|
|
9345
|
+
* aside so the model knows what was said for it, and sent back to the
|
|
9346
|
+
* sender as a regular bus message (`replyTo: msg.id`) so their parked
|
|
9347
|
+
* `wait`/`await:true` resolves. Failures only log — the sender then hits
|
|
9348
|
+
* its normal wait timeout.
|
|
8673
9349
|
*/
|
|
8674
|
-
#
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
9350
|
+
async #runIrcAutoReply(msg: IrcMessage): Promise<void> {
|
|
9351
|
+
try {
|
|
9352
|
+
const { replyText } = await this.runEphemeralTurn({
|
|
9353
|
+
promptText: prompt.render(ircAutoReplyTemplate, {
|
|
9354
|
+
from: msg.from,
|
|
9355
|
+
message: msg.body,
|
|
9356
|
+
replyTo: msg.replyTo ?? "",
|
|
9357
|
+
}),
|
|
9358
|
+
});
|
|
9359
|
+
const body = replyText.trim();
|
|
9360
|
+
if (!body || this.#isDisposed) return;
|
|
9361
|
+
const record: CustomMessage = {
|
|
9362
|
+
role: "custom",
|
|
9363
|
+
customType: "irc:autoreply",
|
|
9364
|
+
content: `[IRC you → \`${msg.from}\` (auto)]\n\n${body}`,
|
|
9365
|
+
display: true,
|
|
9366
|
+
details: { to: msg.from, body, replyTo: msg.id },
|
|
9367
|
+
attribution: "agent",
|
|
9368
|
+
timestamp: Date.now(),
|
|
9369
|
+
};
|
|
9370
|
+
void this.#emitSessionEvent({ type: "irc_message", message: record });
|
|
9371
|
+
// Asides drain at the next step boundary; anything left over is
|
|
9372
|
+
// flushed at the start of the next prompt (#flushPendingIrcAsides).
|
|
9373
|
+
this.#pendingIrcAsides.push(record);
|
|
9374
|
+
// `from` must be the id the sender addressed (msg.to) so their
|
|
9375
|
+
// from-filtered waiter matches.
|
|
9376
|
+
const receipt = await IrcBus.global().send({ from: msg.to, to: msg.from, body, replyTo: msg.id });
|
|
9377
|
+
if (receipt.outcome === "failed") {
|
|
9378
|
+
logger.warn("IRC auto-reply delivery failed", { to: msg.from, error: receipt.error });
|
|
9379
|
+
}
|
|
9380
|
+
} catch (error) {
|
|
9381
|
+
logger.warn("IRC auto-reply turn failed", { from: msg.from, error: String(error) });
|
|
9382
|
+
}
|
|
8699
9383
|
}
|
|
8700
9384
|
|
|
8701
9385
|
/**
|
|
8702
9386
|
* Emit an IRC relay observation event on this session for UI rendering only.
|
|
8703
|
-
* Does not persist the record to history.
|
|
9387
|
+
* Does not persist the record to history. Called by the IrcBus to surface
|
|
9388
|
+
* agent↔agent traffic on the main session.
|
|
8704
9389
|
*/
|
|
8705
9390
|
emitIrcRelayObservation(record: CustomMessage): void {
|
|
8706
9391
|
void this.#emitSessionEvent({ type: "irc_message", message: record });
|
|
@@ -8712,14 +9397,13 @@ export class AgentSession {
|
|
|
8712
9397
|
* does not block on, or interfere with, any in-flight main turn. The
|
|
8713
9398
|
* session's history and persisted state are NOT modified by this call.
|
|
8714
9399
|
*
|
|
8715
|
-
* Used by `
|
|
9400
|
+
* Used by `BtwController` (`/btw`) and `OmfgController` (`/omfg`) to share
|
|
8716
9401
|
* the snapshot + stream pipeline. The snapshot includes any in-flight
|
|
8717
9402
|
* streaming assistant text so the model sees the half-finished response
|
|
8718
9403
|
* rather than missing context.
|
|
8719
9404
|
*/
|
|
8720
9405
|
async runEphemeralTurn(args: {
|
|
8721
9406
|
promptText: string;
|
|
8722
|
-
images?: ImageContent[];
|
|
8723
9407
|
onTextDelta?: (delta: string) => void;
|
|
8724
9408
|
signal?: AbortSignal;
|
|
8725
9409
|
dedupeReply?: boolean;
|
|
@@ -8733,7 +9417,7 @@ export class AgentSession {
|
|
|
8733
9417
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
8734
9418
|
}
|
|
8735
9419
|
|
|
8736
|
-
const snapshot = this.#buildEphemeralSnapshot(args.promptText
|
|
9420
|
+
const snapshot = this.#buildEphemeralSnapshot(args.promptText);
|
|
8737
9421
|
const llmMessages = await this.convertMessagesToLlm(snapshot, args.signal);
|
|
8738
9422
|
const context: Context = {
|
|
8739
9423
|
systemPrompt: this.systemPrompt,
|
|
@@ -8756,6 +9440,7 @@ export class AgentSession {
|
|
|
8756
9440
|
promptCacheKey: cacheSessionId,
|
|
8757
9441
|
preferWebsockets: false,
|
|
8758
9442
|
reasoning: toReasoningEffort(this.thinkingLevel),
|
|
9443
|
+
disableReasoning: shouldDisableReasoning(this.thinkingLevel),
|
|
8759
9444
|
hideThinkingSummary: this.agent.hideThinkingSummary,
|
|
8760
9445
|
serviceTier: this.serviceTier,
|
|
8761
9446
|
signal: args.signal,
|
|
@@ -8764,17 +9449,27 @@ export class AgentSession {
|
|
|
8764
9449
|
model.provider,
|
|
8765
9450
|
);
|
|
8766
9451
|
|
|
8767
|
-
let
|
|
9452
|
+
let providerReplyText = "";
|
|
9453
|
+
let emittedReplyText = "";
|
|
8768
9454
|
let assistantMessage: AssistantMessage | undefined;
|
|
8769
|
-
const stream = streamSimple(model, context, options);
|
|
9455
|
+
const stream = streamSimple(model, obfuscateProviderContext(this.#obfuscator, context), options);
|
|
8770
9456
|
for await (const event of stream) {
|
|
8771
9457
|
if (event.type === "text_delta") {
|
|
8772
|
-
|
|
8773
|
-
if (args.onTextDelta)
|
|
9458
|
+
providerReplyText += event.delta;
|
|
9459
|
+
if (args.onTextDelta) {
|
|
9460
|
+
const readyText = this.#deobfuscatedProviderTextReadyForDelta(providerReplyText);
|
|
9461
|
+
if (readyText.length > emittedReplyText.length) {
|
|
9462
|
+
const delta = readyText.slice(emittedReplyText.length);
|
|
9463
|
+
emittedReplyText = readyText;
|
|
9464
|
+
args.onTextDelta(delta);
|
|
9465
|
+
}
|
|
9466
|
+
}
|
|
8774
9467
|
continue;
|
|
8775
9468
|
}
|
|
8776
9469
|
if (event.type === "done") {
|
|
8777
|
-
assistantMessage =
|
|
9470
|
+
assistantMessage = this.#obfuscator?.hasSecrets()
|
|
9471
|
+
? { ...event.message, content: this.#obfuscator.deobfuscateObject(event.message.content) }
|
|
9472
|
+
: event.message;
|
|
8778
9473
|
break;
|
|
8779
9474
|
}
|
|
8780
9475
|
if (event.type === "error") {
|
|
@@ -8785,8 +9480,12 @@ export class AgentSession {
|
|
|
8785
9480
|
if (!assistantMessage) {
|
|
8786
9481
|
throw new Error("Ephemeral turn ended without a final message");
|
|
8787
9482
|
}
|
|
9483
|
+
const replyText = this.#deobfuscateFromProvider(providerReplyText);
|
|
9484
|
+
if (args.onTextDelta && replyText.length > emittedReplyText.length) {
|
|
9485
|
+
args.onTextDelta(replyText.slice(emittedReplyText.length));
|
|
9486
|
+
}
|
|
8788
9487
|
return {
|
|
8789
|
-
replyText: args.dedupeReply === false ? replyText.trim() :
|
|
9488
|
+
replyText: args.dedupeReply === false ? replyText.trim() : dedupeEphemeralReply(replyText.trim()),
|
|
8790
9489
|
assistantMessage,
|
|
8791
9490
|
};
|
|
8792
9491
|
}
|
|
@@ -8797,7 +9496,7 @@ export class AgentSession {
|
|
|
8797
9496
|
* the partial response in context, then appends the prompt as a virtual
|
|
8798
9497
|
* user message.
|
|
8799
9498
|
*/
|
|
8800
|
-
#buildEphemeralSnapshot(promptText: string
|
|
9499
|
+
#buildEphemeralSnapshot(promptText: string): AgentMessage[] {
|
|
8801
9500
|
const messages = [...this.messages];
|
|
8802
9501
|
const streaming = this.agent.state.streamMessage;
|
|
8803
9502
|
if (streaming && streaming.role === "assistant") {
|
|
@@ -8828,59 +9527,30 @@ export class AgentSession {
|
|
|
8828
9527
|
}
|
|
8829
9528
|
}
|
|
8830
9529
|
}
|
|
8831
|
-
const content: (TextContent | ImageContent)[] = [{ type: "text", text: promptText }];
|
|
8832
|
-
if (images && images.length > 0) {
|
|
8833
|
-
content.push(...images);
|
|
8834
|
-
}
|
|
8835
9530
|
messages.push({
|
|
8836
9531
|
role: "user",
|
|
8837
|
-
content,
|
|
9532
|
+
content: [{ type: "text", text: promptText }],
|
|
8838
9533
|
attribution: "agent",
|
|
8839
9534
|
timestamp: Date.now(),
|
|
8840
9535
|
});
|
|
8841
9536
|
return messages;
|
|
8842
9537
|
}
|
|
8843
9538
|
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
this.#
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
this.#scheduledBackgroundExchangeFlush = false;
|
|
8860
|
-
return;
|
|
8861
|
-
}
|
|
8862
|
-
if (this.isStreaming) {
|
|
8863
|
-
setTimeout(attempt, 50);
|
|
8864
|
-
return;
|
|
8865
|
-
}
|
|
8866
|
-
this.#scheduledBackgroundExchangeFlush = false;
|
|
8867
|
-
this.#flushPendingBackgroundExchanges();
|
|
8868
|
-
};
|
|
8869
|
-
setTimeout(attempt, 0);
|
|
8870
|
-
}
|
|
8871
|
-
|
|
8872
|
-
#flushPendingBackgroundExchanges(): void {
|
|
8873
|
-
if (this.#pendingBackgroundExchanges.length === 0) return;
|
|
8874
|
-
const batches = this.#pendingBackgroundExchanges;
|
|
8875
|
-
this.#pendingBackgroundExchanges = [];
|
|
8876
|
-
for (const batch of batches) {
|
|
8877
|
-
for (const msg of batch) {
|
|
8878
|
-
// emitExternalEvent on message_end appends to agent state and dispatches
|
|
8879
|
-
// to all session listeners, which in turn handle TUI rendering and
|
|
8880
|
-
// sessionManager persistence via #handleAgentEvent.
|
|
8881
|
-
this.agent.emitExternalEvent({ type: "message_start", message: msg });
|
|
8882
|
-
this.agent.emitExternalEvent({ type: "message_end", message: msg });
|
|
8883
|
-
}
|
|
9539
|
+
/**
|
|
9540
|
+
* Persist any IRC asides that missed their step-boundary injection (the
|
|
9541
|
+
* message landed after the turn's last aside drain). Called at the start
|
|
9542
|
+
* of the next prompt so the model still sees them.
|
|
9543
|
+
*/
|
|
9544
|
+
#flushPendingIrcAsides(): void {
|
|
9545
|
+
if (this.#pendingIrcAsides.length === 0) return;
|
|
9546
|
+
const records = this.#pendingIrcAsides;
|
|
9547
|
+
this.#pendingIrcAsides = [];
|
|
9548
|
+
for (const record of records) {
|
|
9549
|
+
// emitExternalEvent on message_end appends to agent state and dispatches
|
|
9550
|
+
// to all session listeners, which in turn handle TUI rendering and
|
|
9551
|
+
// sessionManager persistence via #handleAgentEvent.
|
|
9552
|
+
this.agent.emitExternalEvent({ type: "message_start", message: record });
|
|
9553
|
+
this.agent.emitExternalEvent({ type: "message_end", message: record });
|
|
8884
9554
|
}
|
|
8885
9555
|
}
|
|
8886
9556
|
|
|
@@ -8935,8 +9605,8 @@ export class AgentSession {
|
|
|
8935
9605
|
// the existing message objects is sufficient and avoids structured-clone failures for
|
|
8936
9606
|
// extension/custom metadata that is valid to persist but not cloneable.
|
|
8937
9607
|
const previousAgentMessages = [...this.agent.state.messages];
|
|
8938
|
-
const previousSteeringMessages = [...this
|
|
8939
|
-
const previousFollowUpMessages = [...this
|
|
9608
|
+
const previousSteeringMessages = [...this.agent.peekSteeringQueue()];
|
|
9609
|
+
const previousFollowUpMessages = [...this.agent.peekFollowUpQueue()];
|
|
8940
9610
|
const previousPendingNextTurnMessages = [...this.#pendingNextTurnMessages];
|
|
8941
9611
|
const previousScheduledHiddenNextTurnGeneration = this.#scheduledHiddenNextTurnGeneration;
|
|
8942
9612
|
const previousModel = this.model;
|
|
@@ -8948,17 +9618,20 @@ export class AgentSession {
|
|
|
8948
9618
|
const previousTools = [...this.agent.state.tools];
|
|
8949
9619
|
const previousBaseSystemPrompt = this.#baseSystemPrompt;
|
|
8950
9620
|
const previousSystemPrompt = this.agent.state.systemPrompt;
|
|
9621
|
+
const previousFreshProviderSessionId = this.#freshProviderSessionId;
|
|
8951
9622
|
const previousFallbackSelectedMCPToolNames = previousSessionFile
|
|
8952
9623
|
? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
|
|
8953
9624
|
: undefined;
|
|
8954
9625
|
|
|
8955
|
-
this
|
|
8956
|
-
this.#followUpMessages = [];
|
|
9626
|
+
this.agent.clearAllQueues();
|
|
8957
9627
|
this.#pendingNextTurnMessages = [];
|
|
8958
9628
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
8959
9629
|
|
|
8960
9630
|
try {
|
|
8961
9631
|
await this.sessionManager.setSessionFile(sessionPath);
|
|
9632
|
+
if (switchingToDifferentSession) {
|
|
9633
|
+
this.#freshProviderSessionId = undefined;
|
|
9634
|
+
}
|
|
8962
9635
|
this.#syncAgentSessionId();
|
|
8963
9636
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
8964
9637
|
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
|
@@ -9015,7 +9688,6 @@ export class AgentSession {
|
|
|
9015
9688
|
this.#setModelWithProviderSessionReset(match);
|
|
9016
9689
|
} else {
|
|
9017
9690
|
this.agent.setModel(match);
|
|
9018
|
-
this.#syncToolCallBatchCap(match);
|
|
9019
9691
|
}
|
|
9020
9692
|
}
|
|
9021
9693
|
}
|
|
@@ -9045,7 +9717,7 @@ export class AgentSession {
|
|
|
9045
9717
|
this.#autoResolvedLevel = undefined;
|
|
9046
9718
|
this.#thinkingLevel = resolveThinkingLevelForModel(this.model, restoredThinkingLevel);
|
|
9047
9719
|
}
|
|
9048
|
-
this
|
|
9720
|
+
this.#applyThinkingLevelToAgent(this.#thinkingLevel);
|
|
9049
9721
|
this.agent.serviceTier = hasServiceTierEntry
|
|
9050
9722
|
? sessionContext.serviceTier
|
|
9051
9723
|
: configuredServiceTier === "none"
|
|
@@ -9068,6 +9740,7 @@ export class AgentSession {
|
|
|
9068
9740
|
return true;
|
|
9069
9741
|
} catch (error) {
|
|
9070
9742
|
this.sessionManager.restoreState(previousSessionState);
|
|
9743
|
+
this.#freshProviderSessionId = previousFreshProviderSessionId;
|
|
9071
9744
|
this.#syncAgentSessionId(previousSessionState.sessionId);
|
|
9072
9745
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
9073
9746
|
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
|
@@ -9091,20 +9764,16 @@ export class AgentSession {
|
|
|
9091
9764
|
this.#baseSystemPrompt = previousBaseSystemPrompt;
|
|
9092
9765
|
this.agent.setSystemPrompt(previousSystemPrompt);
|
|
9093
9766
|
this.agent.replaceMessages(previousAgentMessages);
|
|
9094
|
-
this
|
|
9095
|
-
this.#followUpMessages = previousFollowUpMessages;
|
|
9767
|
+
this.agent.replaceQueues(previousSteeringMessages, previousFollowUpMessages);
|
|
9096
9768
|
this.#pendingNextTurnMessages = previousPendingNextTurnMessages;
|
|
9097
9769
|
this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
|
|
9098
9770
|
if (previousModel) {
|
|
9099
9771
|
this.agent.setModel(previousModel);
|
|
9100
|
-
this.#syncToolCallBatchCap(previousModel);
|
|
9101
|
-
} else {
|
|
9102
|
-
this.#syncToolCallBatchCap(undefined);
|
|
9103
9772
|
}
|
|
9104
9773
|
this.#thinkingLevel = previousThinkingLevel;
|
|
9105
9774
|
this.#autoThinking = previousAutoThinking;
|
|
9106
9775
|
this.#autoResolvedLevel = previousAutoResolvedLevel;
|
|
9107
|
-
this
|
|
9776
|
+
this.#applyThinkingLevelToAgent(previousThinkingLevel);
|
|
9108
9777
|
this.agent.serviceTier = previousServiceTier;
|
|
9109
9778
|
this.#syncTodoPhasesFromBranch();
|
|
9110
9779
|
this.#reconnectToAgent();
|
|
@@ -9166,6 +9835,7 @@ export class AgentSession {
|
|
|
9166
9835
|
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
9167
9836
|
}
|
|
9168
9837
|
this.#syncTodoPhasesFromBranch();
|
|
9838
|
+
this.#freshProviderSessionId = undefined;
|
|
9169
9839
|
this.#syncAgentSessionId();
|
|
9170
9840
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
9171
9841
|
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
|
@@ -9285,12 +9955,12 @@ export class AgentSession {
|
|
|
9285
9955
|
const branchSummarySettings = this.settings.getGroup("branchSummary");
|
|
9286
9956
|
const result = await generateBranchSummary(entriesToSummarize, {
|
|
9287
9957
|
model,
|
|
9288
|
-
apiKey,
|
|
9958
|
+
apiKey: this.#modelRegistry.resolver(model, this.sessionId),
|
|
9289
9959
|
signal: this.#branchSummaryAbortController.signal,
|
|
9290
|
-
customInstructions: options.customInstructions,
|
|
9960
|
+
customInstructions: this.#obfuscateTextForProvider(options.customInstructions),
|
|
9291
9961
|
reserveTokens: branchSummarySettings.reserveTokens,
|
|
9292
9962
|
metadata: this.agent.metadataForProvider(model.provider),
|
|
9293
|
-
convertToLlm,
|
|
9963
|
+
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
9294
9964
|
telemetry: resolveTelemetry(this.agent.telemetry, this.sessionId),
|
|
9295
9965
|
});
|
|
9296
9966
|
this.#branchSummaryAbortController = undefined;
|
|
@@ -9542,6 +10212,169 @@ export class AgentSession {
|
|
|
9542
10212
|
});
|
|
9543
10213
|
}
|
|
9544
10214
|
|
|
10215
|
+
/**
|
|
10216
|
+
* Redeem one saved Codex rate-limit reset for a specific account, injecting
|
|
10217
|
+
* the provider base URL like {@link AgentSession.fetchUsageReports}. Powers
|
|
10218
|
+
* the `/usage reset` command and auto-redeem. Never throws for business
|
|
10219
|
+
* outcomes — inspect the returned `code`.
|
|
10220
|
+
*/
|
|
10221
|
+
async redeemResetCredit(target: ResetCreditTarget, signal?: AbortSignal): Promise<ResetCreditRedeemOutcome> {
|
|
10222
|
+
return this.#modelRegistry.authStorage.redeemResetCredit({
|
|
10223
|
+
target,
|
|
10224
|
+
baseUrlResolver: provider => this.#modelRegistry.getProviderBaseUrl?.(provider),
|
|
10225
|
+
signal,
|
|
10226
|
+
});
|
|
10227
|
+
}
|
|
10228
|
+
|
|
10229
|
+
/**
|
|
10230
|
+
* List saved Codex rate-limit resets per stored account, fetched live from
|
|
10231
|
+
* the dedicated credits endpoint (bypasses the usage cache). Powers the
|
|
10232
|
+
* `/usage reset` account selector.
|
|
10233
|
+
*/
|
|
10234
|
+
async listResetCredits(signal?: AbortSignal): Promise<ResetCreditAccountStatus[]> {
|
|
10235
|
+
return this.#modelRegistry.authStorage.listResetCredits({
|
|
10236
|
+
sessionId: this.sessionId,
|
|
10237
|
+
baseUrlResolver: provider => this.#modelRegistry.getProviderBaseUrl?.(provider),
|
|
10238
|
+
signal,
|
|
10239
|
+
});
|
|
10240
|
+
}
|
|
10241
|
+
async #confirmCodexAutoRedeem(decision: CodexAutoRedeemRedeemDecision): Promise<boolean> {
|
|
10242
|
+
const runner = this.#extensionRunner;
|
|
10243
|
+
if (!runner?.hasUI()) {
|
|
10244
|
+
this.emitNotice(
|
|
10245
|
+
"warning",
|
|
10246
|
+
"Codex saved reset is eligible, but auto-redeem is unset and no prompt UI is available. Run `/usage reset` or set codexResets.autoRedeem.",
|
|
10247
|
+
"codex-auto-reset",
|
|
10248
|
+
);
|
|
10249
|
+
return false;
|
|
10250
|
+
}
|
|
10251
|
+
|
|
10252
|
+
const who = decision.target.email ?? decision.target.accountId ?? "the active account";
|
|
10253
|
+
const resetLabel = decision.availableCount === 1 ? "reset" : "resets";
|
|
10254
|
+
try {
|
|
10255
|
+
const choice = await runner
|
|
10256
|
+
.getUIContext()
|
|
10257
|
+
.select(
|
|
10258
|
+
`Do you wanna redeem your reset?\n${who} is blocked by the weekly Codex limit for about ${formatDuration(decision.remainingMs)}. Spend 1 of ${decision.availableCount} saved ${resetLabel}?`,
|
|
10259
|
+
[
|
|
10260
|
+
{
|
|
10261
|
+
label: "Yes",
|
|
10262
|
+
description: "Redeem now and remember yes for future eligible Codex weekly blocks.",
|
|
10263
|
+
},
|
|
10264
|
+
{
|
|
10265
|
+
label: "No",
|
|
10266
|
+
description: "Do not auto-redeem saved Codex resets.",
|
|
10267
|
+
},
|
|
10268
|
+
],
|
|
10269
|
+
);
|
|
10270
|
+
if (choice === "Yes") {
|
|
10271
|
+
this.settings.set("codexResets.autoRedeem", "yes");
|
|
10272
|
+
return true;
|
|
10273
|
+
}
|
|
10274
|
+
if (choice === "No") {
|
|
10275
|
+
this.settings.set("codexResets.autoRedeem", "no");
|
|
10276
|
+
}
|
|
10277
|
+
} catch (error) {
|
|
10278
|
+
logger.warn("codex-auto-reset prompt failed", { error: String(error) });
|
|
10279
|
+
}
|
|
10280
|
+
return false;
|
|
10281
|
+
}
|
|
10282
|
+
|
|
10283
|
+
/**
|
|
10284
|
+
* Auto-redeem hook for {@link AgentSession.#handleRetryableError}'s
|
|
10285
|
+
* usage-limit branch. Returns `true` only when a saved Codex reset was
|
|
10286
|
+
* actually spent (so the caller retries immediately). The "unset" mode is
|
|
10287
|
+
* reactive but asks before spending; "yes" skips that prompt, and "no" avoids
|
|
10288
|
+
* the eligibility IO entirely. The decision remains heavily gated — see
|
|
10289
|
+
* `./codex-auto-reset` and the design in `local://autoreset-spec.md`.
|
|
10290
|
+
* Per-account in-flight dedup lets concurrent sessions adopt one redeem
|
|
10291
|
+
* instead of double-spending.
|
|
10292
|
+
*/
|
|
10293
|
+
async #maybeAutoRedeemCodexReset(coordinator = defaultCodexAutoRedeemCoordinator): Promise<boolean> {
|
|
10294
|
+
const cfg = this.settings.getGroup("codexResets");
|
|
10295
|
+
const model = this.model;
|
|
10296
|
+
// Cheap exits before any IO.
|
|
10297
|
+
if (!shouldEvaluateCodexAutoRedeem(cfg.autoRedeem) || !model || model.provider !== "openai-codex") return false;
|
|
10298
|
+
const authStorage = this.#modelRegistry.authStorage;
|
|
10299
|
+
// Capture identity BEFORE awaits: markUsageLimitReached leaves the
|
|
10300
|
+
// usage-limit session credential sticky, so this names the blocked account.
|
|
10301
|
+
const identity = authStorage.getOAuthAccountIdentity("openai-codex", this.sessionId);
|
|
10302
|
+
const accountKey = (identity?.accountId ?? identity?.email)?.trim().toLowerCase();
|
|
10303
|
+
if (!accountKey) return false;
|
|
10304
|
+
const existing = coordinator.inFlightByAccount.get(accountKey);
|
|
10305
|
+
if (existing) return existing;
|
|
10306
|
+
|
|
10307
|
+
const run = (async (): Promise<boolean> => {
|
|
10308
|
+
const reports = await this.fetchUsageReports();
|
|
10309
|
+
const decision = evaluateCodexAutoRedeem({
|
|
10310
|
+
nowMs: Date.now(),
|
|
10311
|
+
provider: model.provider,
|
|
10312
|
+
modelId: model.id,
|
|
10313
|
+
settings: {
|
|
10314
|
+
autoRedeem: true,
|
|
10315
|
+
minBlockedMinutes: Math.max(0, cfg.minBlockedMinutes),
|
|
10316
|
+
keepCredits: Math.max(0, Math.trunc(cfg.keepCredits)),
|
|
10317
|
+
},
|
|
10318
|
+
identity,
|
|
10319
|
+
reports,
|
|
10320
|
+
attemptedBlockKeys: coordinator.attemptedBlockKeys,
|
|
10321
|
+
lastAttemptAtByAccount: coordinator.lastAttemptAtByAccount,
|
|
10322
|
+
});
|
|
10323
|
+
if (!decision.redeem) {
|
|
10324
|
+
logger.debug("codex-auto-reset: skipped", { reason: decision.reason });
|
|
10325
|
+
return false;
|
|
10326
|
+
}
|
|
10327
|
+
if (shouldPromptCodexAutoRedeem(cfg.autoRedeem) && !(await this.#confirmCodexAutoRedeem(decision))) {
|
|
10328
|
+
return false;
|
|
10329
|
+
}
|
|
10330
|
+
// Commit the attempt BEFORE acting so this block can never re-enter.
|
|
10331
|
+
coordinator.attemptedBlockKeys.add(decision.blockKey);
|
|
10332
|
+
coordinator.lastAttemptAtByAccount.set(decision.accountKey, Date.now());
|
|
10333
|
+
const who = decision.target.email ?? decision.target.accountId ?? "the active account";
|
|
10334
|
+
const outcome = await authStorage.redeemResetCredit({
|
|
10335
|
+
target: decision.target,
|
|
10336
|
+
baseUrlResolver: provider => this.#modelRegistry.getProviderBaseUrl?.(provider),
|
|
10337
|
+
// Not tied to the retry abort controller: aborting a consume
|
|
10338
|
+
// mid-flight leaves credit state unknown.
|
|
10339
|
+
signal: AbortSignal.timeout(15_000),
|
|
10340
|
+
});
|
|
10341
|
+
switch (outcome.code) {
|
|
10342
|
+
case "reset": {
|
|
10343
|
+
const left = Math.max(0, decision.availableCount - 1);
|
|
10344
|
+
this.emitNotice(
|
|
10345
|
+
"info",
|
|
10346
|
+
`Auto-redeemed a saved Codex rate-limit reset for ${who} (${left} left); retrying now.`,
|
|
10347
|
+
"codex-auto-reset",
|
|
10348
|
+
);
|
|
10349
|
+
void this.fetchUsageReports();
|
|
10350
|
+
return true;
|
|
10351
|
+
}
|
|
10352
|
+
case "already_redeemed":
|
|
10353
|
+
this.emitNotice(
|
|
10354
|
+
"warning",
|
|
10355
|
+
"A saved Codex reset was already redeemed elsewhere; waiting for the window.",
|
|
10356
|
+
"codex-auto-reset",
|
|
10357
|
+
);
|
|
10358
|
+
return false;
|
|
10359
|
+
case "no_credit":
|
|
10360
|
+
logger.debug("codex-auto-reset: no_credit (snapshot/live mismatch)", { account: accountKey });
|
|
10361
|
+
return false;
|
|
10362
|
+
case "nothing_to_reset":
|
|
10363
|
+
this.emitNotice(
|
|
10364
|
+
"warning",
|
|
10365
|
+
"Codex reset reported nothing to reset; auto-redeem suppressed for this window.",
|
|
10366
|
+
"codex-auto-reset",
|
|
10367
|
+
);
|
|
10368
|
+
return false;
|
|
10369
|
+
default:
|
|
10370
|
+
this.emitNotice("warning", `Codex auto-redeem failed (${outcome.code}).`, "codex-auto-reset");
|
|
10371
|
+
return false;
|
|
10372
|
+
}
|
|
10373
|
+
})().finally(() => coordinator.inFlightByAccount.delete(accountKey));
|
|
10374
|
+
coordinator.inFlightByAccount.set(accountKey, run);
|
|
10375
|
+
return run;
|
|
10376
|
+
}
|
|
10377
|
+
|
|
9545
10378
|
/**
|
|
9546
10379
|
* Estimate context tokens from messages, using the last assistant usage when available.
|
|
9547
10380
|
*/
|
|
@@ -9594,6 +10427,7 @@ export class AgentSession {
|
|
|
9594
10427
|
*/
|
|
9595
10428
|
async exportToHtml(outputPath?: string): Promise<string> {
|
|
9596
10429
|
const themeName = getCurrentThemeName();
|
|
10430
|
+
const { exportSessionToHtml } = await import("../export/html");
|
|
9597
10431
|
return exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName });
|
|
9598
10432
|
}
|
|
9599
10433
|
|
|
@@ -9682,69 +10516,6 @@ export class AgentSession {
|
|
|
9682
10516
|
});
|
|
9683
10517
|
}
|
|
9684
10518
|
|
|
9685
|
-
/**
|
|
9686
|
-
* Format the conversation as compact context for subagents.
|
|
9687
|
-
* Includes only user messages and assistant text responses.
|
|
9688
|
-
* Excludes: system prompt, tool definitions, tool calls/results, thinking blocks.
|
|
9689
|
-
*/
|
|
9690
|
-
formatCompactContext(): string {
|
|
9691
|
-
const lines: string[] = [];
|
|
9692
|
-
lines.push("# Conversation Context");
|
|
9693
|
-
lines.push("");
|
|
9694
|
-
lines.push(
|
|
9695
|
-
"This is a summary of the parent conversation. Read this if you need additional context about what was discussed or decided.",
|
|
9696
|
-
);
|
|
9697
|
-
lines.push("");
|
|
9698
|
-
|
|
9699
|
-
for (const msg of this.messages) {
|
|
9700
|
-
if (msg.role === "user" || msg.role === "developer") {
|
|
9701
|
-
lines.push(msg.role === "developer" ? "## Developer" : "## User");
|
|
9702
|
-
lines.push("");
|
|
9703
|
-
if (typeof msg.content === "string") {
|
|
9704
|
-
lines.push(msg.content);
|
|
9705
|
-
} else {
|
|
9706
|
-
for (const c of msg.content) {
|
|
9707
|
-
if (c.type === "text") {
|
|
9708
|
-
lines.push(c.text);
|
|
9709
|
-
} else if (c.type === "image") {
|
|
9710
|
-
lines.push("[Image attached]");
|
|
9711
|
-
}
|
|
9712
|
-
}
|
|
9713
|
-
}
|
|
9714
|
-
lines.push("");
|
|
9715
|
-
} else if (msg.role === "assistant") {
|
|
9716
|
-
const assistantMsg = msg as AssistantMessage;
|
|
9717
|
-
// Only include text content, skip tool calls and thinking
|
|
9718
|
-
const textParts: string[] = [];
|
|
9719
|
-
for (const c of assistantMsg.content) {
|
|
9720
|
-
if (c.type === "text" && c.text.trim()) {
|
|
9721
|
-
textParts.push(c.text);
|
|
9722
|
-
}
|
|
9723
|
-
}
|
|
9724
|
-
if (textParts.length > 0) {
|
|
9725
|
-
lines.push("## Assistant");
|
|
9726
|
-
lines.push("");
|
|
9727
|
-
lines.push(textParts.join("\n\n"));
|
|
9728
|
-
lines.push("");
|
|
9729
|
-
}
|
|
9730
|
-
} else if (msg.role === "fileMention") {
|
|
9731
|
-
const fileMsg = msg as FileMentionMessage;
|
|
9732
|
-
const paths = fileMsg.files.map(f => f.path).join(", ");
|
|
9733
|
-
lines.push(`[Files referenced: ${paths}]`);
|
|
9734
|
-
lines.push("");
|
|
9735
|
-
} else if (msg.role === "compactionSummary") {
|
|
9736
|
-
const compactMsg = msg as CompactionSummaryMessage;
|
|
9737
|
-
lines.push("## Earlier Context (Summarized)");
|
|
9738
|
-
lines.push("");
|
|
9739
|
-
lines.push(compactMsg.summary);
|
|
9740
|
-
lines.push("");
|
|
9741
|
-
}
|
|
9742
|
-
// Skip: toolResult, bashExecution, pythonExecution, branchSummary, custom, hookMessage
|
|
9743
|
-
}
|
|
9744
|
-
|
|
9745
|
-
return lines.join("\n").trim();
|
|
9746
|
-
}
|
|
9747
|
-
|
|
9748
10519
|
// =========================================================================
|
|
9749
10520
|
// Extension System
|
|
9750
10521
|
// =========================================================================
|