@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.1
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 +78 -0
- package/README.md +1 -1
- package/dist/cli.js +4685 -4614
- package/dist/types/async/job-manager.d.ts +37 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +41 -0
- package/dist/types/commands/bench.d.ts +15 -3
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/models-config-schema.d.ts +18 -6
- package/dist/types/config/models-config.d.ts +14 -5
- package/dist/types/config/provider-globals.d.ts +2 -2
- package/dist/types/config/service-tier.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +374 -47
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/dap/client.d.ts +20 -0
- package/dist/types/exec/bash-executor.d.ts +38 -0
- package/dist/types/exec/direnv.d.ts +33 -0
- package/dist/types/export/html/args.d.ts +15 -0
- package/dist/types/export/html/index.d.ts +11 -26
- package/dist/types/export/html/web-palette.d.ts +55 -116
- package/dist/types/extensibility/extensions/loader.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
- package/dist/types/live/attestation.d.ts +2 -0
- package/dist/types/live/controller.d.ts +48 -0
- package/dist/types/live/protocol.d.ts +85 -0
- package/dist/types/live/protocol.test.d.ts +1 -0
- package/dist/types/live/transport.d.ts +35 -0
- package/dist/types/live/visualizer.d.ts +30 -0
- package/dist/types/lsp/client.d.ts +16 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/memory-backend/tool-names.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/custom-message.d.ts +1 -1
- package/dist/types/modes/components/login-dialog.d.ts +2 -2
- package/dist/types/modes/components/message-frame.d.ts +8 -4
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/session-account-selector.d.ts +11 -0
- package/dist/types/modes/components/settings-defs.d.ts +7 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +6 -1
- package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -2
- package/dist/types/plan-mode/plan-files.d.ts +10 -0
- package/dist/types/registry/agent-lifecycle.d.ts +21 -9
- package/dist/types/registry/agent-registry.d.ts +11 -4
- package/dist/types/sdk.d.ts +15 -1
- package/dist/types/secrets/index.d.ts +15 -2
- package/dist/types/secrets/obfuscator.d.ts +113 -15
- package/dist/types/session/acp-permission-gate.d.ts +19 -0
- package/dist/types/session/agent-session-events.d.ts +84 -0
- package/dist/types/session/agent-session-types.d.ts +321 -0
- package/dist/types/session/agent-session.d.ts +135 -753
- package/dist/types/session/async-job-delivery.d.ts +28 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/bash-runner.d.ts +75 -0
- package/dist/types/session/blob-store.d.ts +11 -1
- package/dist/types/session/blob-store.test.d.ts +1 -0
- package/dist/types/session/checkpoint-entries.d.ts +28 -0
- package/dist/types/session/eval-runner.d.ts +51 -0
- package/dist/types/session/irc-bridge.d.ts +46 -0
- package/dist/types/session/messages.d.ts +24 -0
- package/dist/types/session/model-controls.d.ts +180 -0
- package/dist/types/session/prewalk.d.ts +46 -0
- package/dist/types/session/queued-messages.d.ts +22 -0
- package/dist/types/session/redis-session-storage.d.ts +1 -0
- package/dist/types/session/retry-fallback-chains.d.ts +74 -0
- package/dist/types/session/role-models.d.ts +15 -0
- package/dist/types/session/session-advisors.d.ts +228 -0
- package/dist/types/session/session-entries.d.ts +8 -0
- package/dist/types/session/session-handoff.d.ts +70 -0
- package/dist/types/session/session-history-format.d.ts +6 -1
- package/dist/types/session/session-maintenance.d.ts +246 -0
- package/dist/types/session/session-manager.d.ts +41 -0
- package/dist/types/session/session-memory.d.ts +56 -0
- package/dist/types/session/session-provider-boundary.d.ts +62 -0
- package/dist/types/session/session-stats.d.ts +48 -0
- package/dist/types/session/session-tools.d.ts +158 -0
- package/dist/types/session/session-workspace.d.ts +32 -0
- package/dist/types/session/stream-guards.d.ts +44 -0
- package/dist/types/session/streaming-output.d.ts +8 -0
- package/dist/types/session/todo-tracker.d.ts +56 -0
- package/dist/types/session/ttsr-coordinator.d.ts +51 -0
- package/dist/types/session/turn-recovery.d.ts +225 -0
- package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
- package/dist/types/stt/index.d.ts +0 -2
- package/dist/types/stt/stt-controller.d.ts +7 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +4 -1
- package/dist/types/task/output-manager.d.ts +3 -1
- package/dist/types/tiny/title-client.d.ts +10 -0
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/computer/protocol.d.ts +43 -0
- package/dist/types/tools/computer/supervisor.d.ts +32 -0
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +15 -0
- package/dist/types/tools/computer-renderer.d.ts +22 -0
- package/dist/types/tools/computer.d.ts +71 -0
- package/dist/types/tools/context.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +21 -0
- package/dist/types/tools/image-gen.d.ts +4 -4
- package/dist/types/tools/image-providers.d.ts +38 -0
- package/dist/types/tools/index.d.ts +7 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/todo.d.ts +10 -4
- package/dist/types/tools/xdev.d.ts +5 -1
- package/dist/types/tts/streaming-player.d.ts +10 -29
- package/dist/types/tts/vocalizer.d.ts +5 -0
- package/dist/types/utils/jj.d.ts +29 -0
- package/dist/types/utils/lang-from-path.d.ts +1 -0
- package/dist/types/utils/title-generator.d.ts +30 -0
- package/dist/types/utils/tools-manager.d.ts +1 -2
- package/dist/types/vibe/runtime.d.ts +36 -8
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +15 -5
- package/dist/types/web/search/types.d.ts +94 -0
- package/package.json +12 -12
- package/scripts/generate-share-viewer.ts +4 -6
- package/src/advisor/__tests__/advisor.test.ts +643 -0
- package/src/advisor/runtime.ts +144 -43
- package/src/async/job-manager.ts +90 -2
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-gateway-cli.ts +18 -3
- package/src/cli/bench-cli.ts +395 -29
- package/src/cli/flag-tables.ts +3 -0
- package/src/cli/gc-cli.ts +2 -2
- package/src/cli/setup-cli.ts +2 -14
- package/src/cli/usage-cli.ts +10 -1
- package/src/cli.ts +8 -0
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/model-registry.ts +16 -2
- package/src/config/models-config-schema.ts +15 -4
- package/src/config/provider-globals.ts +9 -9
- package/src/config/service-tier.ts +26 -1
- package/src/config/settings-schema.ts +270 -51
- package/src/config/settings.ts +81 -2
- package/src/dap/client.ts +37 -4
- package/src/edit/index.ts +49 -12
- package/src/eval/executor-base.ts +1 -0
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/py/executor.ts +103 -33
- package/src/exec/bash-executor.ts +90 -6
- package/src/exec/direnv.ts +145 -0
- package/src/export/html/args.ts +20 -0
- package/src/export/html/index.ts +45 -47
- package/src/export/html/template.css +19 -1
- package/src/export/html/template.html +6 -0
- package/src/export/html/template.js +21 -0
- package/src/export/html/tool-views.generated.js +31 -31
- package/src/export/html/web-palette.ts +116 -132
- package/src/export/share.ts +249 -49
- package/src/extensibility/extensions/loader.ts +29 -1
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +33 -2
- package/src/extensibility/extensions/wrapper.ts +68 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
- package/src/hindsight/state.ts +64 -6
- package/src/internal-urls/local-protocol.ts +2 -1
- package/src/internal-urls/memory-protocol.ts +2 -2
- package/src/internal-urls/skill-protocol.ts +2 -2
- package/src/internal-urls/ssh-protocol.ts +2 -1
- package/src/internal-urls/vault-protocol.ts +2 -1
- package/src/live/attestation.ts +91 -0
- package/src/live/controller.ts +517 -0
- package/src/live/prompts/agent-final-message.md +3 -0
- package/src/live/prompts/live-instructions.md +23 -0
- package/src/live/protocol.test.ts +140 -0
- package/src/live/protocol.ts +233 -0
- package/src/live/transport.ts +422 -0
- package/src/live/visualizer.ts +214 -0
- package/src/lsp/client.ts +29 -9
- package/src/lsp/index.ts +19 -4
- package/src/main.ts +8 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- package/src/memories/index.ts +40 -20
- package/src/memory-backend/tool-names.ts +2 -0
- package/src/mnemopi/backend.ts +24 -7
- package/src/modes/acp/acp-agent.ts +56 -10
- package/src/modes/acp/acp-event-mapper.ts +8 -1
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/assistant-message.ts +7 -2
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/message-frame.ts +14 -8
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/session-account-selector.ts +62 -0
- package/src/modes/components/settings-defs.ts +23 -1
- package/src/modes/components/settings-selector.ts +169 -1
- package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
- package/src/modes/components/status-line/component.ts +123 -2
- package/src/modes/components/tool-execution.ts +28 -112
- package/src/modes/controllers/command-controller.ts +56 -15
- package/src/modes/controllers/event-controller.ts +111 -16
- package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -4
- package/src/modes/controllers/input-controller.ts +47 -39
- package/src/modes/controllers/live-command-controller.ts +255 -0
- package/src/modes/controllers/mcp-command-controller.ts +10 -1
- package/src/modes/controllers/omfg-controller.ts +44 -40
- package/src/modes/controllers/selector-controller.ts +80 -10
- package/src/modes/controllers/session-focus-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +176 -38
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/context-usage.ts +23 -7
- package/src/modes/utils/ui-helpers.ts +10 -5
- package/src/plan-mode/plan-files.ts +40 -0
- package/src/prompts/bench/cache-prefix-chunk.md +1 -0
- package/src/prompts/bench/cache-prefix.md +3 -0
- package/src/prompts/bench/cache-suffix.md +1 -0
- package/src/prompts/system/computer-safety.md +14 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/project-prompt.md +8 -1
- package/src/prompts/system/subagent-async-pending.md +6 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/workflow-notice.md +48 -40
- package/src/prompts/system/xdev-mount-notice.md +4 -0
- package/src/prompts/tools/computer.md +26 -0
- package/src/prompts/tools/todo.md +3 -1
- package/src/registry/agent-lifecycle.ts +116 -46
- package/src/registry/agent-registry.ts +35 -8
- package/src/registry/persisted-agents.ts +26 -2
- package/src/sdk.ts +215 -136
- package/src/secrets/index.ts +127 -4
- package/src/secrets/obfuscator.ts +2328 -105
- package/src/session/acp-permission-gate.ts +165 -0
- package/src/session/agent-session-error-log.test.ts +1 -1
- package/src/session/agent-session-events.ts +66 -0
- package/src/session/agent-session-types.ts +343 -0
- package/src/session/agent-session.ts +1940 -12218
- package/src/session/artifacts.ts +6 -5
- package/src/session/async-job-delivery.ts +74 -0
- package/src/session/auth-storage.ts +1 -0
- package/src/session/bash-runner.ts +326 -0
- package/src/session/blob-store.test.ts +56 -0
- package/src/session/blob-store.ts +18 -2
- package/src/session/checkpoint-entries.ts +81 -0
- package/src/session/eval-runner.ts +217 -0
- package/src/session/indexed-session-storage.ts +9 -2
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/messages.ts +266 -1
- package/src/session/model-controls.ts +714 -0
- package/src/session/prewalk.ts +252 -0
- package/src/session/queued-messages.ts +93 -0
- package/src/session/redis-session-storage.ts +51 -11
- package/src/session/retry-fallback-chains.ts +455 -0
- package/src/session/role-models.ts +85 -0
- package/src/session/session-advisors.ts +1679 -0
- package/src/session/session-context.ts +6 -3
- package/src/session/session-entries.ts +8 -0
- package/src/session/session-handoff.ts +308 -0
- package/src/session/session-history-format.ts +20 -9
- package/src/session/session-loader.ts +0 -46
- package/src/session/session-maintenance.ts +2983 -0
- package/src/session/session-manager.ts +390 -30
- package/src/session/session-memory.ts +222 -0
- package/src/session/session-provider-boundary.ts +307 -0
- package/src/session/session-stats.ts +293 -0
- package/src/session/session-tools.ts +980 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/streaming-output.ts +52 -5
- package/src/session/todo-tracker.ts +380 -0
- package/src/session/ttsr-coordinator.ts +496 -0
- package/src/session/turn-recovery.ts +1629 -0
- package/src/slash-commands/builtin-registry.ts +257 -20
- package/src/slash-commands/helpers/session-pin.ts +44 -0
- package/src/slash-commands/helpers/usage-report.ts +22 -3
- package/src/stt/downloader.ts +0 -2
- package/src/stt/index.ts +0 -2
- package/src/stt/stt-controller.ts +57 -146
- package/src/system-prompt.ts +45 -17
- package/src/task/executor.ts +285 -70
- package/src/task/index.ts +98 -105
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +1 -0
- package/src/tiny/title-client.ts +22 -0
- package/src/tools/approval.ts +55 -10
- package/src/tools/bash-interactive.ts +116 -86
- package/src/tools/bash-skill-urls.ts +28 -2
- package/src/tools/bash.ts +339 -123
- package/src/tools/browser/launch.ts +9 -1
- package/src/tools/browser/tab-supervisor.ts +8 -5
- package/src/tools/browser.ts +94 -47
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/computer/protocol.ts +28 -0
- package/src/tools/computer/supervisor.ts +258 -0
- package/src/tools/computer/worker-entry.ts +25 -0
- package/src/tools/computer/worker.ts +135 -0
- package/src/tools/computer-renderer.ts +108 -0
- package/src/tools/computer.ts +433 -0
- package/src/tools/context.ts +2 -0
- package/src/tools/default-renderer.ts +139 -0
- package/src/tools/essential-tools.ts +1 -0
- package/src/tools/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +14 -0
- package/src/tools/path-utils.ts +49 -2
- package/src/tools/read.ts +156 -88
- package/src/tools/renderers.ts +9 -1
- package/src/tools/todo.ts +101 -11
- package/src/tools/vibe.ts +1 -2
- package/src/tools/write.ts +51 -1
- package/src/tools/xdev.ts +121 -29
- package/src/tts/streaming-player.ts +82 -283
- package/src/tts/vocalizer.ts +18 -1
- package/src/utils/clipboard.ts +1 -30
- package/src/utils/jj.ts +125 -4
- package/src/utils/lang-from-path.ts +7 -0
- package/src/utils/mac-file-urls.applescript +37 -0
- package/src/utils/title-generator.ts +137 -1
- package/src/utils/tool-choice.ts +14 -0
- package/src/utils/tools-manager.ts +1 -19
- package/src/vibe/runtime.ts +852 -72
- package/src/web/search/index.ts +3 -6
- package/src/web/search/provider.ts +31 -16
- package/src/web/search/providers/xai.ts +7 -1
- package/src/web/search/types.ts +3 -0
- package/dist/types/stt/recorder.d.ts +0 -30
- package/dist/types/stt/transcriber.d.ts +0 -14
- package/dist/types/stt/wav.d.ts +0 -29
- package/dist/types/tts/player.d.ts +0 -32
- package/src/stt/recorder.ts +0 -551
- package/src/stt/transcriber.ts +0 -60
- package/src/stt/wav.ts +0 -173
- package/src/tts/player.ts +0 -137
|
@@ -0,0 +1,1679 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Agent,
|
|
3
|
+
type AgentMessage,
|
|
4
|
+
type AgentTool,
|
|
5
|
+
AppendOnlyContextManager,
|
|
6
|
+
type CompactionSummaryMessage,
|
|
7
|
+
countTokens,
|
|
8
|
+
resolveTelemetry,
|
|
9
|
+
type StreamFn,
|
|
10
|
+
ThinkingLevel,
|
|
11
|
+
} from "@oh-my-pi/pi-agent-core";
|
|
12
|
+
import {
|
|
13
|
+
type CompactionResult,
|
|
14
|
+
calculateContextTokens,
|
|
15
|
+
compact,
|
|
16
|
+
compactionContextTokens,
|
|
17
|
+
createCompactionSummaryMessage,
|
|
18
|
+
estimateTokens,
|
|
19
|
+
prepareCompaction,
|
|
20
|
+
type SessionMessageEntry,
|
|
21
|
+
shouldCompact,
|
|
22
|
+
} from "@oh-my-pi/pi-agent-core/compaction";
|
|
23
|
+
import type {
|
|
24
|
+
AssistantMessage,
|
|
25
|
+
CodexCompactionContext,
|
|
26
|
+
Context,
|
|
27
|
+
Message,
|
|
28
|
+
Model,
|
|
29
|
+
ProviderSessionState,
|
|
30
|
+
ServiceTier,
|
|
31
|
+
SimpleStreamOptions,
|
|
32
|
+
} from "@oh-my-pi/pi-ai";
|
|
33
|
+
import { isUsageLimitOutcome, resolveModelServiceTier } from "@oh-my-pi/pi-ai";
|
|
34
|
+
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
35
|
+
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
36
|
+
import { extractHttpStatusFromError, extractRetryHint, logger } from "@oh-my-pi/pi-utils";
|
|
37
|
+
import {
|
|
38
|
+
ADVISOR_DEFAULT_TOOL_NAMES,
|
|
39
|
+
AdviseTool,
|
|
40
|
+
type AdvisorAgent,
|
|
41
|
+
type AdvisorConfig,
|
|
42
|
+
AdvisorEmissionGuard,
|
|
43
|
+
type AdvisorMessageDetails,
|
|
44
|
+
type AdvisorNote,
|
|
45
|
+
AdvisorOutputQuarantinedError,
|
|
46
|
+
AdvisorRuntime,
|
|
47
|
+
type AdvisorRuntimeStatus,
|
|
48
|
+
type AdvisorSeverity,
|
|
49
|
+
AdvisorTranscriptRecorder,
|
|
50
|
+
advisorTranscriptFilename,
|
|
51
|
+
annotateForStaleness,
|
|
52
|
+
buildAdvisorQuarantineSourceText,
|
|
53
|
+
formatAdvisorBatchContent,
|
|
54
|
+
getOrCreateAdvisorProviderSessionId,
|
|
55
|
+
isAdvisorInterruptImmuneTurnActive,
|
|
56
|
+
isInterruptingSeverity,
|
|
57
|
+
quarantineAdvisorUnsafeOutput,
|
|
58
|
+
resolveAdvisorDeliveryChannel,
|
|
59
|
+
slugifyAdvisorName,
|
|
60
|
+
} from "../advisor";
|
|
61
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
62
|
+
import {
|
|
63
|
+
formatModelString,
|
|
64
|
+
formatModelStringWithRouting,
|
|
65
|
+
resolveAdvisorRoleSelection,
|
|
66
|
+
resolveModelOverride,
|
|
67
|
+
} from "../config/model-resolver";
|
|
68
|
+
import { MODEL_ROLES } from "../config/model-roles";
|
|
69
|
+
import { serviceTierForAllFamilies, serviceTierSettingToTier } from "../config/service-tier";
|
|
70
|
+
import type { Settings } from "../config/settings";
|
|
71
|
+
import { CursorExecHandlers } from "../cursor";
|
|
72
|
+
import { estimateToolSchemaTokens } from "../modes/utils/context-usage";
|
|
73
|
+
import type { PlanModeState } from "../plan-mode/state";
|
|
74
|
+
import advisorSystemPrompt from "../prompts/advisor/system.md" with { type: "text" };
|
|
75
|
+
import type { SecretObfuscator } from "../secrets/obfuscator";
|
|
76
|
+
import {
|
|
77
|
+
concreteThinkingLevel,
|
|
78
|
+
resolveThinkingLevelForModel,
|
|
79
|
+
shouldDisableReasoning,
|
|
80
|
+
toReasoningEffort,
|
|
81
|
+
} from "../thinking";
|
|
82
|
+
import type { AgentSessionEvent } from "./agent-session-events";
|
|
83
|
+
import type { ClientBridge } from "./client-bridge";
|
|
84
|
+
import type { CustomMessage, CustomMessagePayload } from "./messages";
|
|
85
|
+
import { isAdvisorCard, isTerminalTextAssistantAnswer } from "./queued-messages";
|
|
86
|
+
import {
|
|
87
|
+
formatRetryFallbackSelector,
|
|
88
|
+
getRetryFallbackRevertPolicy,
|
|
89
|
+
parseRetryFallbackSelector,
|
|
90
|
+
type RetryFallbackSelector,
|
|
91
|
+
} from "./retry-fallback-chains";
|
|
92
|
+
import { formatSessionDumpText } from "./session-dump-format";
|
|
93
|
+
import type { CompactionEntry, SessionEntry } from "./session-entries";
|
|
94
|
+
import { formatSessionHistoryMarkdown } from "./session-history-format";
|
|
95
|
+
import type { SessionManager } from "./session-manager";
|
|
96
|
+
import type { YieldQueue } from "./yield-queue";
|
|
97
|
+
|
|
98
|
+
/** Advisor statistics for the advisor status command. */
|
|
99
|
+
export interface AdvisorStats {
|
|
100
|
+
configured: boolean;
|
|
101
|
+
active: boolean;
|
|
102
|
+
model?: Model;
|
|
103
|
+
contextWindow: number;
|
|
104
|
+
contextTokens: number;
|
|
105
|
+
tokens: {
|
|
106
|
+
input: number;
|
|
107
|
+
output: number;
|
|
108
|
+
reasoning: number;
|
|
109
|
+
cacheRead: number;
|
|
110
|
+
cacheWrite: number;
|
|
111
|
+
total: number;
|
|
112
|
+
};
|
|
113
|
+
cost: number;
|
|
114
|
+
messages: {
|
|
115
|
+
user: number;
|
|
116
|
+
assistant: number;
|
|
117
|
+
total: number;
|
|
118
|
+
};
|
|
119
|
+
/** Per-advisor breakdown for every configured advisor. */
|
|
120
|
+
advisors: PerAdvisorStat[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** One advisor's status, token usage, cost, and message counts. */
|
|
124
|
+
export interface PerAdvisorStat {
|
|
125
|
+
name: string;
|
|
126
|
+
status: AdvisorRuntimeStatus;
|
|
127
|
+
model?: Model;
|
|
128
|
+
contextWindow: number;
|
|
129
|
+
contextTokens: number;
|
|
130
|
+
tokens: AdvisorStats["tokens"];
|
|
131
|
+
cost: number;
|
|
132
|
+
messages: AdvisorStats["messages"];
|
|
133
|
+
sessionId?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface AdvisorRetryFallbackState {
|
|
137
|
+
role: string;
|
|
138
|
+
originalSelector: string;
|
|
139
|
+
originalThinkingLevel: ThinkingLevel;
|
|
140
|
+
lastAppliedThinkingLevel: ThinkingLevel;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface ActiveAdvisor {
|
|
144
|
+
name: string;
|
|
145
|
+
slug: string;
|
|
146
|
+
agent: Agent;
|
|
147
|
+
runtime: AdvisorRuntime;
|
|
148
|
+
adviseTool: AdviseTool;
|
|
149
|
+
emissionGuard: AdvisorEmissionGuard;
|
|
150
|
+
recorder: AdvisorTranscriptRecorder;
|
|
151
|
+
recorderClosed: Promise<void>;
|
|
152
|
+
agentUnsubscribe?: () => void;
|
|
153
|
+
model: Model;
|
|
154
|
+
thinkingLevel: ThinkingLevel;
|
|
155
|
+
providerSessionId: string | undefined;
|
|
156
|
+
retryFallback?: AdvisorRetryFallbackState;
|
|
157
|
+
retryFallbackPendingSuccess: boolean;
|
|
158
|
+
signature: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface AdvisorCompactionSummaryMessage extends CompactionSummaryMessage {
|
|
162
|
+
firstKeptEntryId?: string;
|
|
163
|
+
advisorUsageAnchorStartIndex?: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface AdvisorRuntimeDescriptor {
|
|
167
|
+
config: AdvisorConfig;
|
|
168
|
+
name: string;
|
|
169
|
+
slug: string;
|
|
170
|
+
model: Model;
|
|
171
|
+
thinkingLevel: ThinkingLevel;
|
|
172
|
+
signature: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Inputs that configure the advisor roster owned by a session. */
|
|
176
|
+
export interface SessionAdvisorsOptions {
|
|
177
|
+
enabled: boolean;
|
|
178
|
+
tools?: AgentTool[];
|
|
179
|
+
watchdogPrompt?: string;
|
|
180
|
+
sharedInstructions?: string;
|
|
181
|
+
contextPrompt?: string;
|
|
182
|
+
configs?: AdvisorConfig[];
|
|
183
|
+
streamFn?: StreamFn;
|
|
184
|
+
transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Options accepted when an advisor injects a primary-session message. */
|
|
188
|
+
export interface AdvisorMessageDeliveryOptions {
|
|
189
|
+
triggerTurn?: boolean;
|
|
190
|
+
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
191
|
+
queueChipText?: string;
|
|
192
|
+
acceptTerminalEmptyStop?: boolean;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Session capabilities borrowed by the advisor controller. */
|
|
196
|
+
export interface SessionAdvisorsHost {
|
|
197
|
+
agent: Agent;
|
|
198
|
+
sessionManager: SessionManager;
|
|
199
|
+
settings: Settings;
|
|
200
|
+
modelRegistry: ModelRegistry;
|
|
201
|
+
yieldQueue: YieldQueue;
|
|
202
|
+
obfuscator: SecretObfuscator | undefined;
|
|
203
|
+
providerSessionState: Map<string, ProviderSessionState>;
|
|
204
|
+
preferWebsockets: boolean | undefined;
|
|
205
|
+
onPayload: SimpleStreamOptions["onPayload"] | undefined;
|
|
206
|
+
onResponse: SimpleStreamOptions["onResponse"] | undefined;
|
|
207
|
+
onSseEvent: SimpleStreamOptions["onSseEvent"] | undefined;
|
|
208
|
+
agentKind(): "main" | "sub";
|
|
209
|
+
isDisposed(): boolean;
|
|
210
|
+
abortInProgress(): boolean;
|
|
211
|
+
allowAgentInitiatedTurns(): boolean;
|
|
212
|
+
planModeState(): PlanModeState | undefined;
|
|
213
|
+
clientBridge(): ClientBridge | undefined;
|
|
214
|
+
emitSessionEvent(event: AgentSessionEvent): Promise<void>;
|
|
215
|
+
emitNotice(level: "info" | "warning" | "error", message: string, source?: string): void;
|
|
216
|
+
sendCustomMessage(message: CustomMessagePayload, options?: AdvisorMessageDeliveryOptions): Promise<boolean>;
|
|
217
|
+
extractQueuedAdvisorCards(): CustomMessage[];
|
|
218
|
+
dropPendingAdvisorCards(): void;
|
|
219
|
+
preserveAdvisorCard(card: CustomMessage): void;
|
|
220
|
+
hasPendingNextTurnMessages(): boolean;
|
|
221
|
+
convertToLlmForSideRequest(messages: AgentMessage[]): Message[];
|
|
222
|
+
effectiveServiceTier(model: Model): ServiceTier | undefined;
|
|
223
|
+
resolveContextPromotionTarget(currentModel: Model, contextWindow: number): Promise<Model | undefined>;
|
|
224
|
+
resolveCompactionModelCandidates(preferredModel: Model | null | undefined, availableModels: Model[]): Model[];
|
|
225
|
+
resolveRetryFallbackRole(currentSelector: string, currentModel?: Model | null): string | undefined;
|
|
226
|
+
findRetryFallbackCandidates(
|
|
227
|
+
role: string,
|
|
228
|
+
currentSelector: string,
|
|
229
|
+
currentModel?: Model | null,
|
|
230
|
+
): RetryFallbackSelector[];
|
|
231
|
+
isRetryFallbackSelectorSuppressed(selector: RetryFallbackSelector): boolean;
|
|
232
|
+
noteRetryFallbackCooldown(currentSelector: string, retryAfterMs: number | undefined, errorMessage: string): void;
|
|
233
|
+
createCodexCompactionContext(options: {
|
|
234
|
+
trigger: CodexCompactionContext["trigger"];
|
|
235
|
+
reason: CodexCompactionContext["reason"];
|
|
236
|
+
phase: CodexCompactionContext["phase"];
|
|
237
|
+
}): CodexCompactionContext;
|
|
238
|
+
sessionId(): string;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Owns advisor runtimes, delivery policy, context maintenance, and status reporting. */
|
|
242
|
+
export class SessionAdvisors {
|
|
243
|
+
readonly #host: SessionAdvisorsHost;
|
|
244
|
+
#advisorEnabled: boolean;
|
|
245
|
+
#advisorTools: AgentTool[] | undefined;
|
|
246
|
+
#advisorWatchdogPrompt: string | undefined;
|
|
247
|
+
#advisorSharedInstructions: string | undefined;
|
|
248
|
+
#advisorContextPrompt: string | undefined;
|
|
249
|
+
#advisorStreamFn: StreamFn | undefined;
|
|
250
|
+
#transformProviderContext: ((context: Context, model: Model) => Context | Promise<Context>) | undefined;
|
|
251
|
+
#advisors: ActiveAdvisor[] = [];
|
|
252
|
+
#advisorConfigs: AdvisorConfig[] | undefined;
|
|
253
|
+
#advisorStatuses = new Map<string, { name: string; status: AdvisorRuntimeStatus }>();
|
|
254
|
+
#advisorProviderSessionIds = new Map<string, string>();
|
|
255
|
+
#advisorRecorderClosed: Promise<void> = Promise.resolve();
|
|
256
|
+
#advisorAutoResumeSuppressed = false;
|
|
257
|
+
#preserveAdvisorAdvice = false;
|
|
258
|
+
#advisorPrimaryTurnsCompleted = 0;
|
|
259
|
+
#advisorInterruptImmuneTurnStart: number | undefined;
|
|
260
|
+
#pendingAdvisorCardEvents = new Set<Promise<void>>();
|
|
261
|
+
#advisorYieldQueueUnsubscribe: (() => void) | undefined;
|
|
262
|
+
|
|
263
|
+
constructor(host: SessionAdvisorsHost, options: SessionAdvisorsOptions) {
|
|
264
|
+
this.#host = host;
|
|
265
|
+
this.#advisorEnabled = options.enabled;
|
|
266
|
+
this.#advisorTools = options.tools;
|
|
267
|
+
this.#advisorWatchdogPrompt = options.watchdogPrompt;
|
|
268
|
+
this.#advisorSharedInstructions = options.sharedInstructions;
|
|
269
|
+
this.#advisorContextPrompt = options.contextPrompt;
|
|
270
|
+
this.#advisorConfigs = options.configs;
|
|
271
|
+
this.#advisorStreamFn = options.streamFn;
|
|
272
|
+
this.#transformProviderContext = options.transformProviderContext;
|
|
273
|
+
if (this.#advisorEnabled) this.#buildAdvisorRuntime();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Delivers one completed primary turn to every live advisor. */
|
|
277
|
+
async onPrimaryTurnEnd(
|
|
278
|
+
messages: AgentMessage[],
|
|
279
|
+
willContinue: boolean | undefined,
|
|
280
|
+
signal?: AbortSignal,
|
|
281
|
+
): Promise<void> {
|
|
282
|
+
this.#advisorPrimaryTurnsCompleted++;
|
|
283
|
+
for (const advisor of this.#advisors) {
|
|
284
|
+
if (advisor.runtime.disposed) continue;
|
|
285
|
+
try {
|
|
286
|
+
advisor.runtime.onTurnEnd(messages, { willContinue });
|
|
287
|
+
} catch (error) {
|
|
288
|
+
logger.warn("advisor onTurnEnd threw; delta dropped", { advisor: advisor.name, err: String(error) });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const syncBacklog = this.#host.settings.get("advisor.syncBacklog");
|
|
292
|
+
if (this.#advisors.length === 0 || syncBacklog === "off") return;
|
|
293
|
+
const threshold = Number.parseInt(syncBacklog, 10);
|
|
294
|
+
await Promise.all(this.#advisors.map(advisor => advisor.runtime.waitForCatchup(30_000, threshold, signal)));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Rebuilds live advisors when role assignments alter their resolved runtime inputs. */
|
|
298
|
+
onModelRolesChanged(): void {
|
|
299
|
+
if (!this.#advisorEnabled || this.#host.isDisposed()) return;
|
|
300
|
+
if (this.#advisors.length > 0 && !this.#advisorRuntimeMatchesCurrentConfig()) this.#stopAdvisorRuntime();
|
|
301
|
+
this.#buildAdvisorRuntime(true);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Starts configured advisor runtimes when they are eligible. */
|
|
305
|
+
buildRuntime(seedToCurrent = false): boolean {
|
|
306
|
+
return this.#buildAdvisorRuntime(seedToCurrent);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Stops every advisor runtime and starts recorder shutdown. */
|
|
310
|
+
stopRuntime(): void {
|
|
311
|
+
this.#stopAdvisorRuntime();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Detaches and drains recorder feeds before transcript artifacts are removed. */
|
|
315
|
+
async detachAndCloseRecorders(): Promise<void> {
|
|
316
|
+
const closes: Promise<void>[] = [];
|
|
317
|
+
for (const advisor of this.#advisors) {
|
|
318
|
+
advisor.agentUnsubscribe?.();
|
|
319
|
+
advisor.agentUnsubscribe = undefined;
|
|
320
|
+
advisor.recorderClosed = advisor.recorder.close();
|
|
321
|
+
closes.push(advisor.recorderClosed);
|
|
322
|
+
}
|
|
323
|
+
await Promise.all(closes);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Re-primes advisor transcript views across a conversation boundary. */
|
|
327
|
+
resetSessionState(): void {
|
|
328
|
+
this.#resetAdvisorSessionState();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Re-primes advisor transcript views after an in-conversation history rewrite. */
|
|
332
|
+
resetAllRuntimes(): void {
|
|
333
|
+
this.#resetAllAdvisorRuntimes();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Whether live runtimes still match the resolved advisor configuration. */
|
|
337
|
+
runtimeMatchesCurrentConfig(): boolean {
|
|
338
|
+
return this.#advisorRuntimeMatchesCurrentConfig();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Whether concern/blocker delivery is inside the post-interrupt immunity window. */
|
|
342
|
+
isInterruptImmuneTurnActive(): boolean {
|
|
343
|
+
return this.#isAdvisorInterruptImmuneTurnActive();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Latest aggregate recorder-close barrier. */
|
|
347
|
+
recorderClosed(): Promise<void> {
|
|
348
|
+
return this.#advisorRecorderClosed;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Whether a user interrupt currently suppresses advisor-driven auto-resume. */
|
|
352
|
+
get autoResumeSuppressed(): boolean {
|
|
353
|
+
return this.#advisorAutoResumeSuppressed;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
set autoResumeSuppressed(value: boolean) {
|
|
357
|
+
this.#advisorAutoResumeSuppressed = value;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Tracks persistence of a visible advisor card emitted outside the primary loop. */
|
|
361
|
+
trackCardEvent(processing: Promise<void>): void {
|
|
362
|
+
this.#pendingAdvisorCardEvents.add(processing);
|
|
363
|
+
void processing.finally(() => this.#pendingAdvisorCardEvents.delete(processing)).catch(() => {});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Waits for all advisor-card persistence handlers currently in flight. */
|
|
367
|
+
async waitForPendingCardEvents(): Promise<void> {
|
|
368
|
+
await Promise.allSettled([...this.#pendingAdvisorCardEvents]);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Advisor runtime lifecycle
|
|
372
|
+
// -------------------------------------------------------------------------
|
|
373
|
+
#advisorImmuneTurnLimit(): number {
|
|
374
|
+
const immuneTurns = this.#host.settings.get("advisor.immuneTurns") as number;
|
|
375
|
+
if (!Number.isFinite(immuneTurns) || immuneTurns <= 0) return 0;
|
|
376
|
+
return Math.trunc(immuneTurns);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#isAdvisorInterruptImmuneTurnActive(): boolean {
|
|
380
|
+
return isAdvisorInterruptImmuneTurnActive({
|
|
381
|
+
completedTurns: this.#advisorPrimaryTurnsCompleted,
|
|
382
|
+
immuneTurnStart: this.#advisorInterruptImmuneTurnStart,
|
|
383
|
+
immuneTurns: this.#advisorImmuneTurnLimit(),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// The next primary turn number starts the immune-turn window. While the
|
|
388
|
+
// interrupting steer is still in flight, completedTurns is lower than this
|
|
389
|
+
// start, so duplicate concern/blocker advice is also downgraded.
|
|
390
|
+
#recordAdvisorInterruptDelivered(): void {
|
|
391
|
+
this.#advisorInterruptImmuneTurnStart = this.#advisorPrimaryTurnsCompleted + 1;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Re-prime the advisor across a conversation boundary: `/new`, `/branch`,
|
|
396
|
+
* `/btw`, `/tree`, and session switch/resume. Beyond {@link AdvisorRuntime.reset}
|
|
397
|
+
* (which only re-primes the advisor's transcript view and is also fired by
|
|
398
|
+
* within-conversation rewrites like compaction/shake/rewind), this clears the
|
|
399
|
+
* session-level interrupt latches so the prior conversation's cooldown cannot
|
|
400
|
+
* leak into the new one: the post-interrupt immune-turn window
|
|
401
|
+
* (`#advisorPrimaryTurnsCompleted`, `#advisorInterruptImmuneTurnStart`) and the
|
|
402
|
+
* user-interrupt auto-resume suppression flag. It also drops advisor deliveries
|
|
403
|
+
* still queued against the prior conversation — pending asides in the yield
|
|
404
|
+
* queue (advisor entries use `skipIdleFlush`, so they linger until the next
|
|
405
|
+
* `drainLazy` rather than self-flushing), interrupting cards parked in the
|
|
406
|
+
* agent steer/follow-up queues, and preserved cards deferred to the next turn —
|
|
407
|
+
* so none of them inject into the new conversation.
|
|
408
|
+
*/
|
|
409
|
+
#resetAdvisorSessionState(): void {
|
|
410
|
+
// Mute the recorder across the re-prime: AdvisorRuntime.reset() aborts the advisor
|
|
411
|
+
// loop, and that abort can emit an `aborted` message_end we must not attribute to
|
|
412
|
+
// either session's transcript. Detach, reset, then re-attach the live agent's feed.
|
|
413
|
+
for (const a of this.#advisors) {
|
|
414
|
+
a.agentUnsubscribe?.();
|
|
415
|
+
a.agentUnsubscribe = undefined;
|
|
416
|
+
a.runtime.reset();
|
|
417
|
+
a.adviseTool.resetDeliveredNotes();
|
|
418
|
+
a.emissionGuard.reset();
|
|
419
|
+
this.#attachAdvisorRecorderFeed(a);
|
|
420
|
+
}
|
|
421
|
+
this.#advisorPrimaryTurnsCompleted = 0;
|
|
422
|
+
this.#advisorInterruptImmuneTurnStart = undefined;
|
|
423
|
+
this.#advisorAutoResumeSuppressed = false;
|
|
424
|
+
this.#host.yieldQueue.clear("advisor");
|
|
425
|
+
this.#host.extractQueuedAdvisorCards();
|
|
426
|
+
this.#host.dropPendingAdvisorCards();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
#resolveAdvisorRuntimeDescriptors(emitWarnings: boolean): AdvisorRuntimeDescriptor[] {
|
|
430
|
+
const legacy = !this.#advisorConfigs?.length;
|
|
431
|
+
const roster: AdvisorConfig[] = legacy ? [{ name: "default" }] : this.#advisorConfigs!;
|
|
432
|
+
const descriptors: AdvisorRuntimeDescriptor[] = [];
|
|
433
|
+
const usedSlugs = new Set<string>();
|
|
434
|
+
for (const config of roster) {
|
|
435
|
+
let slug = legacy ? "" : slugifyAdvisorName(config.name);
|
|
436
|
+
if (slug) {
|
|
437
|
+
let candidate = slug;
|
|
438
|
+
let n = 2;
|
|
439
|
+
while (usedSlugs.has(candidate)) candidate = `${slug}-${n++}`;
|
|
440
|
+
slug = candidate;
|
|
441
|
+
usedSlugs.add(slug);
|
|
442
|
+
}
|
|
443
|
+
// Per-advisor toggle: skip disabled advisors but keep them in the
|
|
444
|
+
// status map so they show `○` rather than disappearing.
|
|
445
|
+
if (config.enabled === false) {
|
|
446
|
+
this.#advisorStatuses.set(slug, { name: config.name, status: "paused" });
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Resolve the advisor's model: an explicit `model` override wins; else the
|
|
451
|
+
// `advisor` role chain. A model that fails to resolve skips just this advisor.
|
|
452
|
+
let model: Model | undefined;
|
|
453
|
+
let thinkingLevel: ThinkingLevel | undefined;
|
|
454
|
+
if (config.model) {
|
|
455
|
+
const resolved = resolveModelOverride([config.model], this.#host.modelRegistry, this.#host.settings);
|
|
456
|
+
model = resolved.model;
|
|
457
|
+
thinkingLevel = concreteThinkingLevel(resolved.thinkingLevel);
|
|
458
|
+
if (!model) {
|
|
459
|
+
this.#advisorStatuses.set(slug, { name: config.name, status: "no_model" });
|
|
460
|
+
if (emitWarnings) {
|
|
461
|
+
this.#host.emitNotice(
|
|
462
|
+
"warning",
|
|
463
|
+
`Advisor "${config.name}": no model matched "${config.model}"`,
|
|
464
|
+
"advisor",
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
const sel = resolveAdvisorRoleSelection(this.#host.settings, this.#host.modelRegistry.getAvailable());
|
|
471
|
+
if (!sel) {
|
|
472
|
+
this.#advisorStatuses.set(slug, { name: config.name, status: "no_model" });
|
|
473
|
+
if (emitWarnings) {
|
|
474
|
+
logger.debug("advisor enabled but no model assigned to the 'advisor' role; advisor inactive", {
|
|
475
|
+
advisor: config.name,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
model = sel.model;
|
|
481
|
+
thinkingLevel = concreteThinkingLevel(sel.thinkingLevel);
|
|
482
|
+
}
|
|
483
|
+
// Clamp the effort against the resolved model. Historically we defaulted
|
|
484
|
+
// to `ThinkingLevel.Medium` unconditionally, which threw at first stream
|
|
485
|
+
// on reasoning models that expose no controllable effort surface
|
|
486
|
+
// (e.g. `devin-agent`: Cascade routes by sibling model id, not a wire
|
|
487
|
+
// param; `getSupportedEfforts` returns `[]`). `resolveThinkingLevelForModel`
|
|
488
|
+
// preserves an explicit `off`, clamps a concrete effort into the model's
|
|
489
|
+
// supported range, and returns `undefined` for reasoning models without
|
|
490
|
+
// controllable efforts — for that case we forward `Inherit` so no effort
|
|
491
|
+
// is sent and reasoning stays enabled (matching the `auto`-path fix for
|
|
492
|
+
// Devin models via `clampAutoThinkingEffort`). See #4579.
|
|
493
|
+
const requestedLevel = thinkingLevel ?? ThinkingLevel.Medium;
|
|
494
|
+
const resolvedLevel = resolveThinkingLevelForModel(model, requestedLevel);
|
|
495
|
+
const advisorThinkingLevel: ThinkingLevel = resolvedLevel ?? ThinkingLevel.Inherit;
|
|
496
|
+
// Record the status entry now (in roster order) so the Map's insertion
|
|
497
|
+
// order matches the configured roster even when earlier advisors were
|
|
498
|
+
// skipped as paused/no_model. The build loop overwrites this to "running"
|
|
499
|
+
// without changing insertion order.
|
|
500
|
+
this.#advisorStatuses.set(slug, { name: config.name, status: "running" });
|
|
501
|
+
descriptors.push({
|
|
502
|
+
config,
|
|
503
|
+
name: config.name,
|
|
504
|
+
slug,
|
|
505
|
+
model,
|
|
506
|
+
thinkingLevel: advisorThinkingLevel,
|
|
507
|
+
signature: this.#advisorRuntimeSignature(config, slug, model, advisorThinkingLevel),
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
return descriptors;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
#advisorRuntimeSignature(config: AdvisorConfig, slug: string, model: Model, thinkingLevel: ThinkingLevel): string {
|
|
514
|
+
const tools = config.tools?.length ? config.tools.join("\u001e") : "";
|
|
515
|
+
const instructions = config.instructions?.trim() ?? "";
|
|
516
|
+
return [config.name, slug, formatModelStringWithRouting(model), thinkingLevel, tools, instructions].join(
|
|
517
|
+
"\u001f",
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
#advisorRuntimeMatchesCurrentConfig(): boolean {
|
|
522
|
+
const descriptors = this.#resolveAdvisorRuntimeDescriptors(false);
|
|
523
|
+
if (descriptors.length !== this.#advisors.length) return false;
|
|
524
|
+
for (let i = 0; i < descriptors.length; i++) {
|
|
525
|
+
if (descriptors[i].signature !== this.#advisors[i].signature) return false;
|
|
526
|
+
}
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
#buildAdvisorRuntime(seedToCurrent = false): boolean {
|
|
531
|
+
if (this.#host.isDisposed()) return false;
|
|
532
|
+
if (this.#advisors.length > 0) return true;
|
|
533
|
+
if (!this.#advisorEnabled) return false;
|
|
534
|
+
if (this.#host.agentKind() !== "main" && !this.#host.settings.get("advisor.subagents")) return false;
|
|
535
|
+
|
|
536
|
+
// Rebuild the status map from scratch so removed/renamed advisors don't
|
|
537
|
+
// leave stale entries. #resolveAdvisorRuntimeDescriptors populates every
|
|
538
|
+
// entry (`paused`/`no_model`/`running`) in roster order; the build loop
|
|
539
|
+
// below confirms `running` for successfully built advisors.
|
|
540
|
+
this.#advisorStatuses.clear();
|
|
541
|
+
const descriptors = this.#resolveAdvisorRuntimeDescriptors(true);
|
|
542
|
+
|
|
543
|
+
// Advisor service tier (`tier.advisor`): "none" (default) runs the advisor
|
|
544
|
+
// on standard processing; "inherit" tracks the session's live per-family
|
|
545
|
+
// tiers per request (like the main agent, including /fast toggles); a
|
|
546
|
+
// concrete value is broadcast across families and applied to the advisor
|
|
547
|
+
// model's family. One value for all advisors.
|
|
548
|
+
const advisorTierSetting = this.#host.settings.get("tier.advisor");
|
|
549
|
+
const advisorTierMap =
|
|
550
|
+
advisorTierSetting === "inherit"
|
|
551
|
+
? undefined
|
|
552
|
+
: serviceTierForAllFamilies(serviceTierSettingToTier(advisorTierSetting));
|
|
553
|
+
const advisorServiceTierResolver = (model: Model): ServiceTier | undefined =>
|
|
554
|
+
advisorTierSetting === "inherit"
|
|
555
|
+
? this.#host.effectiveServiceTier(model)
|
|
556
|
+
: resolveModelServiceTier(advisorTierMap, model);
|
|
557
|
+
|
|
558
|
+
for (const descriptor of descriptors) {
|
|
559
|
+
const {
|
|
560
|
+
config,
|
|
561
|
+
slug,
|
|
562
|
+
model: advisorModel,
|
|
563
|
+
name: advisorName,
|
|
564
|
+
thinkingLevel: advisorThinkingLevel,
|
|
565
|
+
signature,
|
|
566
|
+
} = descriptor;
|
|
567
|
+
|
|
568
|
+
const emissionGuard = new AdvisorEmissionGuard();
|
|
569
|
+
const adviseTool = new AdviseTool((note, severity) => this.#routeAdvice(advisorRef, note, severity));
|
|
570
|
+
|
|
571
|
+
// `#advisorWatchdogPrompt` already carries WATCHDOG.md + YAML shared
|
|
572
|
+
// instructions; `config.instructions` adds this advisor's specialization.
|
|
573
|
+
const systemPrompt = [advisorSystemPrompt];
|
|
574
|
+
if (this.#advisorContextPrompt) systemPrompt.push(this.#advisorContextPrompt);
|
|
575
|
+
if (this.#advisorWatchdogPrompt) systemPrompt.push(this.#advisorWatchdogPrompt);
|
|
576
|
+
if (this.#advisorSharedInstructions) systemPrompt.push(this.#advisorSharedInstructions);
|
|
577
|
+
if (config.instructions?.trim()) systemPrompt.push(config.instructions.trim());
|
|
578
|
+
|
|
579
|
+
const names = config.tools === undefined ? ADVISOR_DEFAULT_TOOL_NAMES : new Set(config.tools);
|
|
580
|
+
const tools = (this.#advisorTools ?? []).filter(t => names.has(t.name));
|
|
581
|
+
const advisorLoopTools: AgentTool<any>[] = [adviseTool, ...tools];
|
|
582
|
+
const advisorToolMap = new Map<string, AgentTool<any>>();
|
|
583
|
+
const availableAdvisorToolNames = new Set<string>();
|
|
584
|
+
for (const tool of advisorLoopTools) {
|
|
585
|
+
availableAdvisorToolNames.add(tool.name);
|
|
586
|
+
advisorToolMap.set(tool.name, tool);
|
|
587
|
+
if (tool.customWireName !== undefined) {
|
|
588
|
+
availableAdvisorToolNames.add(tool.customWireName);
|
|
589
|
+
advisorToolMap.set(tool.customWireName, tool);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
let quarantinedAdvisorOutput: string | undefined;
|
|
593
|
+
let currentAdvisorInput = "";
|
|
594
|
+
|
|
595
|
+
const primaryProviderSessionId = this.#host.sessionId();
|
|
596
|
+
const advisorSessionLabel = slug
|
|
597
|
+
? `${primaryProviderSessionId}-advisor-${slug}`
|
|
598
|
+
: `${primaryProviderSessionId}-advisor`;
|
|
599
|
+
const advisorProviderSessionId = getOrCreateAdvisorProviderSessionId(
|
|
600
|
+
this.#advisorProviderSessionIds,
|
|
601
|
+
primaryProviderSessionId,
|
|
602
|
+
slug,
|
|
603
|
+
);
|
|
604
|
+
const appendOnlyContext = new AppendOnlyContextManager();
|
|
605
|
+
|
|
606
|
+
// Thread the primary's telemetry into the advisor loop so the advisor
|
|
607
|
+
// model's GenAI spans + usage/cost hooks fire stamped with the local advisor
|
|
608
|
+
// identity. `conversationId` is cleared so provider telemetry falls back to
|
|
609
|
+
// the UUIDv7 provider session id, not the local `-advisor` label.
|
|
610
|
+
const advisorTelemetry = this.#host.agent.telemetry
|
|
611
|
+
? {
|
|
612
|
+
...this.#host.agent.telemetry,
|
|
613
|
+
agent: {
|
|
614
|
+
id: advisorSessionLabel,
|
|
615
|
+
name: slug ? `${MODEL_ROLES.advisor.name}: ${advisorName}` : MODEL_ROLES.advisor.name,
|
|
616
|
+
description: formatModelString(advisorModel),
|
|
617
|
+
},
|
|
618
|
+
conversationId: undefined,
|
|
619
|
+
}
|
|
620
|
+
: undefined;
|
|
621
|
+
// Mirror the SDK's provider-shaping options (streamFn/onPayload/...,
|
|
622
|
+
// providerSessionState, promptCacheKey, transformProviderContext) so each
|
|
623
|
+
// advisor's requests cache, route, and obfuscate like the main turn.
|
|
624
|
+
// `promptCacheKey` preserves an explicitly pinned provider cache key
|
|
625
|
+
// unchanged so tan/shared-session advisor calls read the exact shard the
|
|
626
|
+
// parent turn populated. Otherwise the advisor uses its provider UUIDv7 so
|
|
627
|
+
// Codex request identity remains UUID-shaped while local labels keep the
|
|
628
|
+
// `-advisor` suffix.
|
|
629
|
+
const advisorPromptCacheKey = this.#host.agent.promptCacheKey ?? advisorProviderSessionId;
|
|
630
|
+
// On the Cursor provider every tool runs server-side and is dispatched
|
|
631
|
+
// back through `cursorExecHandlers`; without this bridge the advisor's
|
|
632
|
+
// own tools (including the MCP `advise` tool) return `toolNotFound` and
|
|
633
|
+
// no advice is ever routed (issue #5680). Mirrors the primary agent's
|
|
634
|
+
// bridge (`sdk.ts`), scoped to this advisor's granted tool set.
|
|
635
|
+
// Cursor's native `delete` frame removes files directly, bypassing the
|
|
636
|
+
// tool map, so gate it on the advisor actually holding a file-mutating
|
|
637
|
+
// tool. A default read-only advisor (advise/read/grep/glob) never gets
|
|
638
|
+
// to delete workspace files it was never granted (issue #5680 review).
|
|
639
|
+
const advisorCanMutateFiles = advisorToolMap.has("write") || advisorToolMap.has("edit");
|
|
640
|
+
if (advisorCanMutateFiles) availableAdvisorToolNames.add("delete");
|
|
641
|
+
const advisorCursorExecHandlers = new CursorExecHandlers({
|
|
642
|
+
cwd: this.#host.sessionManager.getCwd(),
|
|
643
|
+
getCwd: () => this.#host.sessionManager.getCwd(),
|
|
644
|
+
tools: advisorToolMap,
|
|
645
|
+
allowNativeDelete: advisorCanMutateFiles,
|
|
646
|
+
});
|
|
647
|
+
const advisorAgent = new Agent({
|
|
648
|
+
initialState: {
|
|
649
|
+
systemPrompt,
|
|
650
|
+
model: advisorModel,
|
|
651
|
+
thinkingLevel: toReasoningEffort(advisorThinkingLevel),
|
|
652
|
+
tools: advisorLoopTools,
|
|
653
|
+
},
|
|
654
|
+
appendOnlyContext,
|
|
655
|
+
sessionId: advisorProviderSessionId,
|
|
656
|
+
promptCacheKey: advisorPromptCacheKey,
|
|
657
|
+
providerSessionState: this.#host.providerSessionState,
|
|
658
|
+
cursorExecHandlers: advisorCursorExecHandlers,
|
|
659
|
+
cwdResolver: () => this.#host.sessionManager.getCwd(),
|
|
660
|
+
preferWebsockets: this.#host.preferWebsockets,
|
|
661
|
+
getApiKey: requestModel => this.#host.modelRegistry.resolver(requestModel, advisorProviderSessionId),
|
|
662
|
+
streamFn: this.#advisorStreamFn,
|
|
663
|
+
onPayload: this.#host.onPayload,
|
|
664
|
+
onResponse: this.#host.onResponse,
|
|
665
|
+
onSseEvent: this.#host.onSseEvent,
|
|
666
|
+
transformProviderContext: this.#transformProviderContext,
|
|
667
|
+
intentTracing: false,
|
|
668
|
+
transformAssistantMessage: message => {
|
|
669
|
+
quarantinedAdvisorOutput = quarantineAdvisorUnsafeOutput(
|
|
670
|
+
message,
|
|
671
|
+
availableAdvisorToolNames,
|
|
672
|
+
buildAdvisorQuarantineSourceText(currentAdvisorInput, advisorAgent.state.messages),
|
|
673
|
+
);
|
|
674
|
+
},
|
|
675
|
+
telemetry: advisorTelemetry,
|
|
676
|
+
serviceTier: undefined,
|
|
677
|
+
serviceTierResolver: advisorServiceTierResolver,
|
|
678
|
+
});
|
|
679
|
+
advisorAgent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
680
|
+
|
|
681
|
+
const advisorAgentFacade: AdvisorAgent = {
|
|
682
|
+
prompt: async input => {
|
|
683
|
+
let quarantined: string | undefined;
|
|
684
|
+
try {
|
|
685
|
+
quarantinedAdvisorOutput = undefined;
|
|
686
|
+
currentAdvisorInput = input;
|
|
687
|
+
await advisorAgent.prompt(input);
|
|
688
|
+
quarantined = quarantinedAdvisorOutput;
|
|
689
|
+
} finally {
|
|
690
|
+
quarantinedAdvisorOutput = undefined;
|
|
691
|
+
currentAdvisorInput = "";
|
|
692
|
+
}
|
|
693
|
+
if (quarantined) throw new AdvisorOutputQuarantinedError(quarantined);
|
|
694
|
+
},
|
|
695
|
+
abort: reason => advisorAgent.abort(reason),
|
|
696
|
+
reset: () => {
|
|
697
|
+
advisorAgent.reset();
|
|
698
|
+
appendOnlyContext.log.clear();
|
|
699
|
+
},
|
|
700
|
+
rollbackTo: count => {
|
|
701
|
+
// Drop the failed user batch + synthetic assistant-error turn
|
|
702
|
+
// `Agent.#runLoop` appended for a turn ending in `stopReason: "error"`.
|
|
703
|
+
const messages = advisorAgent.state.messages;
|
|
704
|
+
if (count < messages.length) {
|
|
705
|
+
messages.length = count;
|
|
706
|
+
}
|
|
707
|
+
appendOnlyContext.resetSyncCursor();
|
|
708
|
+
advisorAgent.state.error = undefined;
|
|
709
|
+
},
|
|
710
|
+
state: advisorAgent.state,
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
// Persist this advisor's turns to `<session>/__advisor[.<slug>].jsonl`
|
|
714
|
+
// (resolved lazily so it follows session switches) for stats attribution
|
|
715
|
+
// and Agent Hub observability, without registering it as a peer.
|
|
716
|
+
const recorder = new AdvisorTranscriptRecorder(
|
|
717
|
+
() => this.#host.sessionManager.getSessionFile(),
|
|
718
|
+
() => this.#host.sessionManager.getCwd(),
|
|
719
|
+
advisorTranscriptFilename(slug),
|
|
720
|
+
// On the advisor on→off→on toggle, wait for the prior recorders' closes
|
|
721
|
+
// so two SessionManagers never hold the same file at once.
|
|
722
|
+
this.#advisorRecorderClosed,
|
|
723
|
+
);
|
|
724
|
+
const runtime = new AdvisorRuntime(advisorAgentFacade, {
|
|
725
|
+
snapshotMessages: () => this.#host.agent.state.messages,
|
|
726
|
+
enqueueAdvice: (note, severity) => this.#routeAdvice(advisorRef, note, severity),
|
|
727
|
+
maintainContext: incomingTokens => this.#maintainAdvisorContext(advisorRef, incomingTokens),
|
|
728
|
+
obfuscator: this.#host.obfuscator,
|
|
729
|
+
beginAdvisorUpdate: () => advisorRef.emissionGuard.beginUpdate(),
|
|
730
|
+
onTurnError: (error, failedMessages) => this.#recoverAdvisorTurn(advisorRef, error, failedMessages),
|
|
731
|
+
onTurnSuccess: async () => {
|
|
732
|
+
const fallback = advisorRef.retryFallback;
|
|
733
|
+
if (!advisorRef.retryFallbackPendingSuccess || !fallback) return;
|
|
734
|
+
advisorRef.retryFallbackPendingSuccess = false;
|
|
735
|
+
await this.#host.emitSessionEvent({
|
|
736
|
+
type: "retry_fallback_succeeded",
|
|
737
|
+
model: formatRetryFallbackSelector(advisorRef.agent.state.model, advisorRef.thinkingLevel),
|
|
738
|
+
role: fallback.role,
|
|
739
|
+
});
|
|
740
|
+
},
|
|
741
|
+
notifyFailure: error => {
|
|
742
|
+
this.#advisorStatuses.set(slug, { name: advisorName, status: "error" });
|
|
743
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
744
|
+
this.#host.emitNotice(
|
|
745
|
+
"warning",
|
|
746
|
+
`Advisor${slug ? ` "${advisorName}"` : ""} unavailable for ${formatModelString(advisorAgent.state.model)}: ${message}`,
|
|
747
|
+
"advisor",
|
|
748
|
+
);
|
|
749
|
+
},
|
|
750
|
+
notifyQuotaExhausted: () => {
|
|
751
|
+
this.#advisorStatuses.set(slug, { name: advisorName, status: "quota_exhausted" });
|
|
752
|
+
this.#host.emitNotice(
|
|
753
|
+
"warning",
|
|
754
|
+
`Advisor "${advisorName}" quota exhausted — pausing until reset.`,
|
|
755
|
+
"advisor",
|
|
756
|
+
);
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
const advisorRef: ActiveAdvisor = {
|
|
761
|
+
name: advisorName,
|
|
762
|
+
slug,
|
|
763
|
+
agent: advisorAgent,
|
|
764
|
+
runtime,
|
|
765
|
+
adviseTool,
|
|
766
|
+
emissionGuard,
|
|
767
|
+
recorder,
|
|
768
|
+
recorderClosed: Promise.resolve(),
|
|
769
|
+
model: advisorModel,
|
|
770
|
+
thinkingLevel: advisorThinkingLevel,
|
|
771
|
+
providerSessionId: advisorProviderSessionId,
|
|
772
|
+
retryFallbackPendingSuccess: false,
|
|
773
|
+
signature,
|
|
774
|
+
};
|
|
775
|
+
this.#attachAdvisorRecorderFeed(advisorRef);
|
|
776
|
+
if (seedToCurrent) runtime.seedTo(this.#host.agent.state.messages.length);
|
|
777
|
+
this.#advisorStatuses.set(slug, { name: advisorName, status: "running" });
|
|
778
|
+
this.#advisors.push(advisorRef);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// One shared non-blocking aside channel for all advisors; the build callback
|
|
782
|
+
// aggregates every advisor's queued nits into one card (each entry already
|
|
783
|
+
// carries its own `advisor` name).
|
|
784
|
+
if (this.#advisors.length > 0 && !this.#advisorYieldQueueUnsubscribe) {
|
|
785
|
+
this.#advisorYieldQueueUnsubscribe = this.#host.yieldQueue.register<AdvisorNote>("advisor", {
|
|
786
|
+
build: entries =>
|
|
787
|
+
entries.length === 0
|
|
788
|
+
? null
|
|
789
|
+
: ({
|
|
790
|
+
role: "custom",
|
|
791
|
+
customType: "advisor",
|
|
792
|
+
display: true,
|
|
793
|
+
attribution: "agent",
|
|
794
|
+
timestamp: Date.now(),
|
|
795
|
+
content: formatAdvisorBatchContent(entries),
|
|
796
|
+
details: { notes: entries } satisfies AdvisorMessageDetails,
|
|
797
|
+
} satisfies CustomMessage),
|
|
798
|
+
skipIdleFlush: true,
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return this.#advisors.length > 0;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Route one accepted advice note from `advisor` to the primary. Concern and
|
|
807
|
+
* blocker interrupt the running agent through the steering channel; once the
|
|
808
|
+
* loop has yielded, `triggerTurn` resumes it. After a terminal text answer with
|
|
809
|
+
* no queued work, a concern is preserved as a visible advisor card, while a
|
|
810
|
+
* blocker wakes the primary to acknowledge work it handed off incorrectly.
|
|
811
|
+
* After a deliberate user interrupt auto-resume is suppressed while idle/unwinding
|
|
812
|
+
* (the note becomes a preserved card re-entering on resume); a live-streaming turn is
|
|
813
|
+
* steered in directly. A plain nit always rides the non-interrupting YieldQueue
|
|
814
|
+
* aside. Suppression by the per-advisor emission guard drops the note silently —
|
|
815
|
+
* the model still saw `Recorded.`, so it isn't tempted to rephrase the same note
|
|
816
|
+
* past the dedupe.
|
|
817
|
+
*/
|
|
818
|
+
#hasTerminalTextAnswerWithoutQueuedWork(): boolean {
|
|
819
|
+
if (this.#host.agent.hasQueuedMessages() || this.#host.hasPendingNextTurnMessages()) return false;
|
|
820
|
+
const messages = this.#host.agent.state.messages;
|
|
821
|
+
let tail = messages.length - 1;
|
|
822
|
+
while (tail >= 0 && isAdvisorCard(messages[tail])) tail--;
|
|
823
|
+
return isTerminalTextAssistantAnswer(messages[tail]);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
#routeAdvice(advisor: ActiveAdvisor, note: string, severity?: AdvisorSeverity): void {
|
|
827
|
+
if (!advisor.emissionGuard.accept(note)) {
|
|
828
|
+
logger.debug("advisor advice suppressed by emission guard", { severity, advisor: advisor.name });
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
// When newer primary turns already arrived while the advisor model was
|
|
832
|
+
// processing this batch, the advice was generated without seeing them.
|
|
833
|
+
// Append a lightweight staleness caveat so the primary can weigh recency.
|
|
834
|
+
const deliveredNote = annotateForStaleness(note, advisor.runtime.hasFreshBacklog);
|
|
835
|
+
// The implicit single ("default") advisor stamps no source name, so its
|
|
836
|
+
// agent-facing `<advisory>` bytes stay identical to the pre-multi-advisor path.
|
|
837
|
+
const source = advisor.slug ? advisor.name : undefined;
|
|
838
|
+
const interrupting = isInterruptingSeverity(severity);
|
|
839
|
+
const channel = resolveAdvisorDeliveryChannel({
|
|
840
|
+
severity,
|
|
841
|
+
autoResumeSuppressed: this.#advisorAutoResumeSuppressed,
|
|
842
|
+
preserveOnly: this.#preserveAdvisorAdvice,
|
|
843
|
+
// Key on the live agent-core loop, not session `isStreaming` (which also
|
|
844
|
+
// counts `#promptInFlightCount` during post-turn unwind). Only a running
|
|
845
|
+
// loop consumes a steer at its next boundary.
|
|
846
|
+
streaming: this.#host.agent.state.isStreaming,
|
|
847
|
+
aborting: this.#host.abortInProgress(),
|
|
848
|
+
terminalAnswerNoQueuedWork: this.#hasTerminalTextAnswerWithoutQueuedWork(),
|
|
849
|
+
interruptImmuneTurnActive: interrupting && this.#isAdvisorInterruptImmuneTurnActive(),
|
|
850
|
+
});
|
|
851
|
+
if (channel === "aside") {
|
|
852
|
+
this.#host.yieldQueue.enqueue("advisor", { note: deliveredNote, severity, advisor: source });
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const notes: AdvisorNote[] = [{ note: deliveredNote, severity, advisor: source }];
|
|
856
|
+
const content = formatAdvisorBatchContent(notes);
|
|
857
|
+
const details = { notes } satisfies AdvisorMessageDetails;
|
|
858
|
+
if (channel === "preserve") {
|
|
859
|
+
this.#host.preserveAdvisorCard({
|
|
860
|
+
role: "custom",
|
|
861
|
+
customType: "advisor",
|
|
862
|
+
content,
|
|
863
|
+
display: true,
|
|
864
|
+
attribution: "agent",
|
|
865
|
+
details,
|
|
866
|
+
timestamp: Date.now(),
|
|
867
|
+
});
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
// A steered interrupting note only continues the run when the session can
|
|
871
|
+
// actually start (or is already running) a turn. Two idle cases cannot, so
|
|
872
|
+
// `sendCustomMessage({ triggerTurn: true })` would silently bury the card in
|
|
873
|
+
// `#pendingNextTurnMessages` until the next user prompt — strictly worse than
|
|
874
|
+
// the visible preserved card. Preserve instead:
|
|
875
|
+
// - Plan mode: only user-driven turns converge on ask/resolve.
|
|
876
|
+
// - ACP bridges with `deferAgentInitiatedTurns`: the client cannot show an
|
|
877
|
+
// agent-initiated turn as busy, so idle triggers are refused (#5628 review).
|
|
878
|
+
const cannotAutoTrigger =
|
|
879
|
+
!this.#host.agent.state.isStreaming &&
|
|
880
|
+
this.#host.clientBridge()?.deferAgentInitiatedTurns === true &&
|
|
881
|
+
!this.#host.allowAgentInitiatedTurns();
|
|
882
|
+
if (this.#host.planModeState()?.enabled || cannotAutoTrigger) {
|
|
883
|
+
this.#host.preserveAdvisorCard({
|
|
884
|
+
role: "custom",
|
|
885
|
+
customType: "advisor",
|
|
886
|
+
content,
|
|
887
|
+
display: true,
|
|
888
|
+
attribution: "agent",
|
|
889
|
+
details,
|
|
890
|
+
timestamp: Date.now(),
|
|
891
|
+
});
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
// Arm the post-interrupt immune window only now that a turn is actually
|
|
895
|
+
// being steered/triggered. A merely preserved card never interrupts, so
|
|
896
|
+
// arming earlier would downgrade the next `advisor.immuneTurns` worth of
|
|
897
|
+
// real concerns/blockers to skip-idle-flush asides (#5628 review).
|
|
898
|
+
this.#recordAdvisorInterruptDelivered();
|
|
899
|
+
void this.#host
|
|
900
|
+
.sendCustomMessage(
|
|
901
|
+
{ customType: "advisor", content, display: true, attribution: "agent", details },
|
|
902
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
903
|
+
)
|
|
904
|
+
.catch(err => logger.debug("advisor delivery failed", { err: String(err) }));
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/** Re-prime every advisor's transcript view (compaction/shake/rewind) without the
|
|
908
|
+
* session-level latch reset {@link #resetAdvisorSessionState} performs. */
|
|
909
|
+
#resetAllAdvisorRuntimes(): void {
|
|
910
|
+
for (const a of this.#advisors) a.runtime.reset();
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
#stopAdvisorRuntime(): void {
|
|
914
|
+
// Detach each recorder feed BEFORE aborting its advisor agent: dispose() aborts
|
|
915
|
+
// the loop, and an abort emits a final `message_end` we must not enqueue against
|
|
916
|
+
// a closing recorder (it would reopen and resurrect an already-released file).
|
|
917
|
+
const closes: Promise<void>[] = [];
|
|
918
|
+
for (const a of this.#advisors) {
|
|
919
|
+
a.agentUnsubscribe?.();
|
|
920
|
+
a.agentUnsubscribe = undefined;
|
|
921
|
+
a.runtime.dispose();
|
|
922
|
+
// Capture each close so dispose()/`/drop` can await the queued open+append+close —
|
|
923
|
+
// the last advisor turn would otherwise be lost on a fast process exit.
|
|
924
|
+
a.recorderClosed = a.recorder.close();
|
|
925
|
+
closes.push(a.recorderClosed);
|
|
926
|
+
}
|
|
927
|
+
this.#advisorRecorderClosed = Promise.all(closes).then(() => {});
|
|
928
|
+
this.#advisors = [];
|
|
929
|
+
this.#advisorYieldQueueUnsubscribe?.();
|
|
930
|
+
this.#advisorYieldQueueUnsubscribe = undefined;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/** Subscribe the advisor agent's finalized messages into the transcript recorder.
|
|
934
|
+
* Idempotent-by-replacement: callers detach the prior feed first. Kept separate
|
|
935
|
+
* so the re-prime path can mute the feed across an abort-driven reset. */
|
|
936
|
+
#attachAdvisorRecorderFeed(advisor: ActiveAdvisor): void {
|
|
937
|
+
advisor.agentUnsubscribe = advisor.agent.subscribe(event => {
|
|
938
|
+
if (event.type === "message_end") advisor.recorder.record(event.message);
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** Switch one advisor model while preserving its context and effort invariants. */
|
|
943
|
+
#setAdvisorModel(advisor: ActiveAdvisor, model: Model, requestedThinkingLevel: ThinkingLevel): ThinkingLevel {
|
|
944
|
+
const resolvedThinkingLevel = resolveThinkingLevelForModel(model, requestedThinkingLevel);
|
|
945
|
+
const nextThinkingLevel = resolvedThinkingLevel ?? ThinkingLevel.Inherit;
|
|
946
|
+
advisor.agent.setModel(model);
|
|
947
|
+
advisor.agent.setThinkingLevel(toReasoningEffort(nextThinkingLevel));
|
|
948
|
+
advisor.agent.setDisableReasoning(shouldDisableReasoning(nextThinkingLevel));
|
|
949
|
+
advisor.agent.appendOnlyContext?.invalidateForModelChange();
|
|
950
|
+
advisor.model = model;
|
|
951
|
+
advisor.thinkingLevel = nextThinkingLevel;
|
|
952
|
+
return nextThinkingLevel;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/** Restore an advisor's configured primary once its fallback cooldown expires. */
|
|
956
|
+
async #maybeRestoreAdvisorRetryFallbackPrimary(advisor: ActiveAdvisor): Promise<void> {
|
|
957
|
+
const fallback = advisor.retryFallback;
|
|
958
|
+
if (!fallback || getRetryFallbackRevertPolicy(this.#host.settings) !== "cooldown-expiry") return;
|
|
959
|
+
|
|
960
|
+
const originalSelector = parseRetryFallbackSelector(fallback.originalSelector, this.#host.modelRegistry);
|
|
961
|
+
if (!originalSelector) {
|
|
962
|
+
advisor.retryFallback = undefined;
|
|
963
|
+
advisor.retryFallbackPendingSuccess = false;
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const currentSelector = formatRetryFallbackSelector(advisor.agent.state.model, advisor.thinkingLevel);
|
|
967
|
+
if (currentSelector === originalSelector.raw) {
|
|
968
|
+
if (!this.#host.isRetryFallbackSelectorSuppressed(originalSelector)) {
|
|
969
|
+
advisor.retryFallback = undefined;
|
|
970
|
+
advisor.retryFallbackPendingSuccess = false;
|
|
971
|
+
}
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (this.#host.isRetryFallbackSelectorSuppressed(originalSelector)) return;
|
|
975
|
+
|
|
976
|
+
const resolvedPrimary = resolveModelOverride(
|
|
977
|
+
[originalSelector.raw],
|
|
978
|
+
this.#host.modelRegistry,
|
|
979
|
+
this.#host.settings,
|
|
980
|
+
);
|
|
981
|
+
const primaryModel =
|
|
982
|
+
resolvedPrimary.model ?? this.#host.modelRegistry.find(originalSelector.provider, originalSelector.id);
|
|
983
|
+
if (!primaryModel) return;
|
|
984
|
+
const apiKey = await this.#host.modelRegistry.getApiKey(primaryModel, advisor.providerSessionId);
|
|
985
|
+
if (!apiKey) return;
|
|
986
|
+
|
|
987
|
+
const thinkingToApply =
|
|
988
|
+
advisor.thinkingLevel === fallback.lastAppliedThinkingLevel
|
|
989
|
+
? fallback.originalThinkingLevel
|
|
990
|
+
: advisor.thinkingLevel;
|
|
991
|
+
this.#setAdvisorModel(advisor, primaryModel, thinkingToApply);
|
|
992
|
+
this.#host.settings.getStorage()?.recordModelUsage(formatModelStringWithRouting(primaryModel));
|
|
993
|
+
advisor.retryFallback = undefined;
|
|
994
|
+
advisor.retryFallbackPendingSuccess = false;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Apply the advisor's configured provider-failure fallback chain after
|
|
999
|
+
* same-provider credential rotation has no usable sibling.
|
|
1000
|
+
*/
|
|
1001
|
+
async #recoverAdvisorTurn(
|
|
1002
|
+
advisor: ActiveAdvisor,
|
|
1003
|
+
error: unknown,
|
|
1004
|
+
failedMessages: readonly AgentMessage[],
|
|
1005
|
+
): Promise<boolean> {
|
|
1006
|
+
if (error instanceof AdvisorOutputQuarantinedError) return false;
|
|
1007
|
+
|
|
1008
|
+
const failedMessage = failedMessages.findLast(
|
|
1009
|
+
(message): message is AssistantMessage => message.role === "assistant",
|
|
1010
|
+
);
|
|
1011
|
+
if (failedMessage?.stopReason !== "error") {
|
|
1012
|
+
// Stream setup can reject before any assistant turn is recorded (e.g.
|
|
1013
|
+
// an HTTP 429 thrown from prompt()); classify the raw error so a
|
|
1014
|
+
// structural usage limit still marks the exhausted credential.
|
|
1015
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1016
|
+
if (!AIError.isUsageLimit(error) && !isUsageLimitOutcome(extractHttpStatusFromError(error), message)) {
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
const currentModel = advisor.agent.state.model;
|
|
1020
|
+
const outcome = await this.#host.modelRegistry.authStorage.markUsageLimitReached(
|
|
1021
|
+
currentModel.provider,
|
|
1022
|
+
advisor.providerSessionId,
|
|
1023
|
+
{
|
|
1024
|
+
retryAfterMs: extractRetryHint(undefined, message),
|
|
1025
|
+
baseUrl: currentModel.baseUrl,
|
|
1026
|
+
modelId: currentModel.id,
|
|
1027
|
+
},
|
|
1028
|
+
);
|
|
1029
|
+
return outcome.switched;
|
|
1030
|
+
}
|
|
1031
|
+
if (failedMessage.content.some(block => block.type === "toolCall")) return false;
|
|
1032
|
+
|
|
1033
|
+
const currentModel = advisor.agent.state.model;
|
|
1034
|
+
const message = failedMessage.errorMessage ?? (error instanceof Error ? error.message : String(error));
|
|
1035
|
+
const errorId = AIError.classifyMessage({
|
|
1036
|
+
api: currentModel.api,
|
|
1037
|
+
errorId: failedMessage.errorId,
|
|
1038
|
+
errorMessage: message,
|
|
1039
|
+
errorStatus: failedMessage.errorStatus,
|
|
1040
|
+
});
|
|
1041
|
+
if (AIError.is(errorId, AIError.Flag.Abort) || AIError.is(errorId, AIError.Flag.UserInterrupt)) return false;
|
|
1042
|
+
if (AIError.isContextOverflow(failedMessage, currentModel.contextWindow ?? 0)) return false;
|
|
1043
|
+
|
|
1044
|
+
const currentSelector = formatRetryFallbackSelector(currentModel, advisor.thinkingLevel);
|
|
1045
|
+
|
|
1046
|
+
const retryAfterMs = extractRetryHint(undefined, message);
|
|
1047
|
+
if (
|
|
1048
|
+
AIError.is(errorId, AIError.Flag.UsageLimit) ||
|
|
1049
|
+
isUsageLimitOutcome(extractHttpStatusFromError(error), message)
|
|
1050
|
+
) {
|
|
1051
|
+
const outcome = await this.#host.modelRegistry.authStorage.markUsageLimitReached(
|
|
1052
|
+
currentModel.provider,
|
|
1053
|
+
advisor.providerSessionId,
|
|
1054
|
+
{
|
|
1055
|
+
retryAfterMs,
|
|
1056
|
+
baseUrl: currentModel.baseUrl,
|
|
1057
|
+
modelId: currentModel.id,
|
|
1058
|
+
},
|
|
1059
|
+
);
|
|
1060
|
+
if (outcome.switched) return true;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const retrySettings = this.#host.settings.getGroup("retry");
|
|
1064
|
+
if (!retrySettings.enabled || !retrySettings.modelFallback) return false;
|
|
1065
|
+
const role = advisor.retryFallback?.role ?? this.#host.resolveRetryFallbackRole(currentSelector, currentModel);
|
|
1066
|
+
if (!role || this.#host.findRetryFallbackCandidates(role, currentSelector, currentModel).length === 0)
|
|
1067
|
+
return false;
|
|
1068
|
+
|
|
1069
|
+
this.#host.noteRetryFallbackCooldown(currentSelector, retryAfterMs, message);
|
|
1070
|
+
for (const selector of this.#host.findRetryFallbackCandidates(role, currentSelector, currentModel)) {
|
|
1071
|
+
if (this.#host.isRetryFallbackSelectorSuppressed(selector)) continue;
|
|
1072
|
+
const resolved = resolveModelOverride([selector.raw], this.#host.modelRegistry, this.#host.settings);
|
|
1073
|
+
const candidate = resolved.model ?? this.#host.modelRegistry.find(selector.provider, selector.id);
|
|
1074
|
+
if (!candidate || modelsAreEqual(candidate, currentModel)) continue;
|
|
1075
|
+
const apiKey = await this.#host.modelRegistry.getApiKey(candidate, advisor.providerSessionId);
|
|
1076
|
+
if (!apiKey) continue;
|
|
1077
|
+
|
|
1078
|
+
const originalThinkingLevel = advisor.thinkingLevel;
|
|
1079
|
+
const requestedThinkingLevel = selector.thinkingLevel ?? originalThinkingLevel;
|
|
1080
|
+
const nextThinkingLevel = this.#setAdvisorModel(advisor, candidate, requestedThinkingLevel);
|
|
1081
|
+
if (advisor.retryFallback) {
|
|
1082
|
+
advisor.retryFallback.lastAppliedThinkingLevel = nextThinkingLevel;
|
|
1083
|
+
} else {
|
|
1084
|
+
advisor.retryFallback = {
|
|
1085
|
+
role,
|
|
1086
|
+
originalSelector: currentSelector,
|
|
1087
|
+
originalThinkingLevel,
|
|
1088
|
+
lastAppliedThinkingLevel: nextThinkingLevel,
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
advisor.retryFallbackPendingSuccess = true;
|
|
1092
|
+
this.#host.settings.getStorage()?.recordModelUsage(formatModelStringWithRouting(candidate));
|
|
1093
|
+
await this.#host.emitSessionEvent({
|
|
1094
|
+
type: "retry_fallback_applied",
|
|
1095
|
+
from: currentSelector,
|
|
1096
|
+
to: selector.raw,
|
|
1097
|
+
role,
|
|
1098
|
+
});
|
|
1099
|
+
return true;
|
|
1100
|
+
}
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
async #promoteAdvisorContextModel(advisor: ActiveAdvisor, currentModel: Model): Promise<boolean> {
|
|
1105
|
+
const promotionSettings = this.#host.settings.getGroup("contextPromotion");
|
|
1106
|
+
if (!promotionSettings.enabled) return false;
|
|
1107
|
+
const contextWindow = currentModel.contextWindow ?? 0;
|
|
1108
|
+
if (contextWindow <= 0) return false;
|
|
1109
|
+
const targetModel = await this.#host.resolveContextPromotionTarget(currentModel, contextWindow);
|
|
1110
|
+
if (!targetModel) return false;
|
|
1111
|
+
|
|
1112
|
+
// Preserve this advisor's own thinking level (a configured `model:...:high`
|
|
1113
|
+
// keeps its suffix across a promotion); only the model changes.
|
|
1114
|
+
const advisorThinkingLevel = advisor.thinkingLevel;
|
|
1115
|
+
try {
|
|
1116
|
+
this.#setAdvisorModel(advisor, targetModel, advisorThinkingLevel);
|
|
1117
|
+
logger.debug("Advisor context promotion switched model on overflow", {
|
|
1118
|
+
advisor: advisor.name,
|
|
1119
|
+
from: `${currentModel.provider}/${currentModel.id}`,
|
|
1120
|
+
to: `${targetModel.provider}/${targetModel.id}`,
|
|
1121
|
+
});
|
|
1122
|
+
return true;
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
logger.warn("Advisor context promotion failed", {
|
|
1125
|
+
advisor: advisor.name,
|
|
1126
|
+
from: `${currentModel.provider}/${currentModel.id}`,
|
|
1127
|
+
to: `${targetModel.provider}/${targetModel.id}`,
|
|
1128
|
+
error: String(error),
|
|
1129
|
+
});
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async #maintainAdvisorContext(advisor: ActiveAdvisor, incomingTokens: number): Promise<boolean> {
|
|
1135
|
+
await this.#maybeRestoreAdvisorRetryFallbackPrimary(advisor);
|
|
1136
|
+
const agent = advisor.agent;
|
|
1137
|
+
|
|
1138
|
+
const compactionSettings = this.#host.settings.getGroup("compaction");
|
|
1139
|
+
if (compactionSettings.strategy === "off") return false;
|
|
1140
|
+
if (!compactionSettings.enabled) return false;
|
|
1141
|
+
|
|
1142
|
+
const advisorModel = agent.state.model;
|
|
1143
|
+
const contextWindow = advisorModel.contextWindow ?? 0;
|
|
1144
|
+
if (contextWindow <= 0) return false;
|
|
1145
|
+
|
|
1146
|
+
const messages = agent.state.messages;
|
|
1147
|
+
const estimateOptions = { excludeEncryptedReasoning: true } as const;
|
|
1148
|
+
let storedConversationTokens = 0;
|
|
1149
|
+
for (const message of messages) {
|
|
1150
|
+
storedConversationTokens += estimateTokens(message, estimateOptions);
|
|
1151
|
+
}
|
|
1152
|
+
// Provider usage (including cache reads and generated output) is the
|
|
1153
|
+
// trustworthy anchor for accumulated context. Add only the trailing incoming
|
|
1154
|
+
// delta to that arm. Floor it by a full local estimate — fixed advisor system
|
|
1155
|
+
// prompt, tool schemas, stored messages, and incoming delta — so provider
|
|
1156
|
+
// under-reporting or payload transforms cannot suppress maintenance.
|
|
1157
|
+
const providerContextTokens = this.#estimateAdvisorContextTokens(messages) + incomingTokens;
|
|
1158
|
+
const localContextTokens =
|
|
1159
|
+
countTokens(agent.state.systemPrompt) +
|
|
1160
|
+
estimateToolSchemaTokens(agent.state.tools) +
|
|
1161
|
+
storedConversationTokens +
|
|
1162
|
+
incomingTokens;
|
|
1163
|
+
const contextTokens = compactionContextTokens(providerContextTokens, localContextTokens);
|
|
1164
|
+
|
|
1165
|
+
if (!shouldCompact(contextTokens, contextWindow, compactionSettings)) {
|
|
1166
|
+
return false;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// 1. Try promotion first
|
|
1170
|
+
if (await this.#promoteAdvisorContextModel(advisor, advisorModel)) {
|
|
1171
|
+
// Promotion succeeded, check if new model has enough space
|
|
1172
|
+
const newModel = agent.state.model;
|
|
1173
|
+
const newWindow = newModel.contextWindow ?? 0;
|
|
1174
|
+
if (newWindow > 0) {
|
|
1175
|
+
const stillNeedsCompaction = shouldCompact(contextTokens, newWindow, compactionSettings);
|
|
1176
|
+
if (!stillNeedsCompaction) return false;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// 2. Run compaction on advisor messages
|
|
1181
|
+
const pathEntries: SessionEntry[] = messages.map((message, i) => {
|
|
1182
|
+
const id = `msg-${i}`;
|
|
1183
|
+
const parentId = i > 0 ? `msg-${i - 1}` : null;
|
|
1184
|
+
const timestamp = String(message.timestamp || Date.now());
|
|
1185
|
+
|
|
1186
|
+
if (message.role === "compactionSummary") {
|
|
1187
|
+
const advisorSummary = message as AdvisorCompactionSummaryMessage;
|
|
1188
|
+
return {
|
|
1189
|
+
type: "compaction",
|
|
1190
|
+
id,
|
|
1191
|
+
parentId,
|
|
1192
|
+
timestamp,
|
|
1193
|
+
summary: message.summary,
|
|
1194
|
+
shortSummary: message.shortSummary,
|
|
1195
|
+
firstKeptEntryId: advisorSummary.firstKeptEntryId || `msg-${i + 1}`,
|
|
1196
|
+
tokensBefore: message.tokensBefore,
|
|
1197
|
+
} satisfies CompactionEntry;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
return {
|
|
1201
|
+
type: "message",
|
|
1202
|
+
id,
|
|
1203
|
+
parentId,
|
|
1204
|
+
timestamp,
|
|
1205
|
+
message,
|
|
1206
|
+
} satisfies SessionMessageEntry;
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
const availableModels = this.#host.modelRegistry.getAvailable();
|
|
1210
|
+
const candidates = this.#host.resolveCompactionModelCandidates(advisorModel, availableModels);
|
|
1211
|
+
if (candidates.length === 0) {
|
|
1212
|
+
// No compaction candidates, fallback to re-prime
|
|
1213
|
+
return true;
|
|
1214
|
+
}
|
|
1215
|
+
const advisorProviderSessionId = getOrCreateAdvisorProviderSessionId(
|
|
1216
|
+
this.#advisorProviderSessionIds,
|
|
1217
|
+
this.#host.sessionId(),
|
|
1218
|
+
advisor.slug,
|
|
1219
|
+
);
|
|
1220
|
+
const preparation = prepareCompaction(pathEntries, compactionSettings, advisorModel);
|
|
1221
|
+
if (!preparation) {
|
|
1222
|
+
// Cannot prepare compaction, fallback to re-prime
|
|
1223
|
+
return true;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const advisorCompactionThinkingLevel: ThinkingLevel | undefined = agent.state.disableReasoning
|
|
1227
|
+
? ThinkingLevel.Off
|
|
1228
|
+
: agent.state.thinkingLevel;
|
|
1229
|
+
|
|
1230
|
+
// Advisor state is in-memory-only, so snapcompact's frame archive has no
|
|
1231
|
+
// stable SessionEntry preserveData slot to carry across future advisor
|
|
1232
|
+
// maintenance runs. Use an LLM summary even when the primary session is
|
|
1233
|
+
// configured for snapcompact.
|
|
1234
|
+
|
|
1235
|
+
let compactResult: CompactionResult | undefined;
|
|
1236
|
+
let lastError: unknown;
|
|
1237
|
+
// Instrument the advisor's overflow-compaction one-shot like the primary
|
|
1238
|
+
// compaction path so the advisor model's maintenance call also emits spans.
|
|
1239
|
+
const telemetry = resolveTelemetry(agent.telemetry, advisorProviderSessionId);
|
|
1240
|
+
|
|
1241
|
+
const codexCompaction = this.#host.createCodexCompactionContext({
|
|
1242
|
+
trigger: "auto",
|
|
1243
|
+
reason: "context_limit",
|
|
1244
|
+
phase: "pre_turn",
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
for (const candidate of candidates) {
|
|
1248
|
+
const apiKey = await this.#host.modelRegistry.getApiKey(candidate, advisorProviderSessionId);
|
|
1249
|
+
if (!apiKey) continue;
|
|
1250
|
+
|
|
1251
|
+
try {
|
|
1252
|
+
compactResult = await compact(
|
|
1253
|
+
preparation,
|
|
1254
|
+
candidate,
|
|
1255
|
+
this.#host.modelRegistry.resolver(candidate, advisorProviderSessionId),
|
|
1256
|
+
undefined,
|
|
1257
|
+
undefined,
|
|
1258
|
+
{
|
|
1259
|
+
thinkingLevel: advisorCompactionThinkingLevel,
|
|
1260
|
+
convertToLlm: messages => this.#host.convertToLlmForSideRequest(messages),
|
|
1261
|
+
telemetry,
|
|
1262
|
+
tools: agent.state.tools,
|
|
1263
|
+
sessionId: advisorProviderSessionId,
|
|
1264
|
+
promptCacheKey: advisorProviderSessionId,
|
|
1265
|
+
providerSessionState: this.#host.providerSessionState,
|
|
1266
|
+
codexCompaction,
|
|
1267
|
+
},
|
|
1268
|
+
);
|
|
1269
|
+
break;
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
lastError = error;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (!compactResult) {
|
|
1276
|
+
logger.warn("Advisor compaction failed, falling back to re-prime", { error: String(lastError) });
|
|
1277
|
+
return true;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const summary = compactResult.summary;
|
|
1281
|
+
const shortSummary = compactResult.shortSummary;
|
|
1282
|
+
const firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
1283
|
+
const tokensBefore = compactResult.tokensBefore;
|
|
1284
|
+
|
|
1285
|
+
// The retained messages still carry provider usage from before this
|
|
1286
|
+
// compaction. Record their exact array boundary on the in-memory summary so
|
|
1287
|
+
// only assistants appended afterward can become the next usage anchor.
|
|
1288
|
+
const advisorUsageAnchorStartIndex = preparation.recentMessages.length + 1;
|
|
1289
|
+
const summaryMessage = {
|
|
1290
|
+
...createCompactionSummaryMessage(summary, tokensBefore, new Date().toISOString(), shortSummary),
|
|
1291
|
+
firstKeptEntryId,
|
|
1292
|
+
advisorUsageAnchorStartIndex,
|
|
1293
|
+
} satisfies AdvisorCompactionSummaryMessage;
|
|
1294
|
+
|
|
1295
|
+
agent.replaceMessages([summaryMessage, ...preparation.recentMessages]);
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Prevent advisor notes from starting hidden primary turns while a headless
|
|
1300
|
+
* caller prints and drains the final primary response.
|
|
1301
|
+
*/
|
|
1302
|
+
prepareForHeadlessAdvisorDrain(): void {
|
|
1303
|
+
this.#preserveAdvisorAdvice = true;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
async #waitForPendingAdvisorCardEvents(timeoutMs: number): Promise<boolean> {
|
|
1307
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
1308
|
+
while (this.#pendingAdvisorCardEvents.size > 0) {
|
|
1309
|
+
const remainingMs = deadline - Date.now();
|
|
1310
|
+
if (remainingMs <= 0) return false;
|
|
1311
|
+
const settled = Promise.allSettled([...this.#pendingAdvisorCardEvents]).then(() => true as const);
|
|
1312
|
+
const { promise: timedOut, resolve } = Promise.withResolvers<false>();
|
|
1313
|
+
const timer = setTimeout(() => resolve(false), remainingMs);
|
|
1314
|
+
try {
|
|
1315
|
+
if (!(await Promise.race([settled, timedOut]))) return false;
|
|
1316
|
+
} finally {
|
|
1317
|
+
clearTimeout(timer);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
return true;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Wait for active advisor reviews and their emitted card events before a
|
|
1325
|
+
* headless caller disposes the session. Returns `false` and logs work disposal
|
|
1326
|
+
* will abandon when the shared deadline expires or an advisor fails.
|
|
1327
|
+
*/
|
|
1328
|
+
async waitForAdvisorCatchup(timeoutMs: number): Promise<boolean> {
|
|
1329
|
+
const deadline = Date.now() + timeoutMs;
|
|
1330
|
+
const results = await Promise.all(this.#advisors.map(advisor => advisor.runtime.waitForCatchup(timeoutMs, 1)));
|
|
1331
|
+
const cardEventsCaughtUp = await this.#waitForPendingAdvisorCardEvents(Math.max(0, deadline - Date.now()));
|
|
1332
|
+
const abandoned = this.#advisors.filter(
|
|
1333
|
+
(advisor, index) => results[index] === false && advisor.runtime.backlog > 0,
|
|
1334
|
+
);
|
|
1335
|
+
if (abandoned.length > 0 || !cardEventsCaughtUp) {
|
|
1336
|
+
logger.warn("advisor shutdown drain incomplete; disposal will abandon reviews or cards", {
|
|
1337
|
+
timeoutMs,
|
|
1338
|
+
advisors: abandoned.map(advisor => ({ name: advisor.name, backlog: advisor.runtime.backlog })),
|
|
1339
|
+
pendingAdvisorCards: this.#pendingAdvisorCardEvents.size,
|
|
1340
|
+
});
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
return true;
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Enable or disable the advisor for this session. The setting is overridden for the session,
|
|
1347
|
+
* and the runtime is started or stopped to match.
|
|
1348
|
+
*
|
|
1349
|
+
* @returns true when the advisor is actively running after the call.
|
|
1350
|
+
*/
|
|
1351
|
+
setAdvisorEnabled(enabled: boolean): boolean {
|
|
1352
|
+
this.#advisorEnabled = enabled;
|
|
1353
|
+
if (enabled) {
|
|
1354
|
+
if (this.#advisors.length > 0 && !this.#advisorRuntimeMatchesCurrentConfig()) this.#stopAdvisorRuntime();
|
|
1355
|
+
return this.#buildAdvisorRuntime(true);
|
|
1356
|
+
}
|
|
1357
|
+
this.#stopAdvisorRuntime();
|
|
1358
|
+
return false;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Toggle the advisor setting and start/stop the runtime accordingly.
|
|
1363
|
+
*
|
|
1364
|
+
* @returns true when the advisor is actively running after the call.
|
|
1365
|
+
*/
|
|
1366
|
+
toggleAdvisorEnabled(): boolean {
|
|
1367
|
+
return this.setAdvisorEnabled(!this.#advisorEnabled);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Replace the live advisor roster from an edited `WATCHDOG.yml` (the `/advisor
|
|
1372
|
+
* configure` save path). Swaps the configs + shared baseline, then rebuilds the
|
|
1373
|
+
* runtimes in place so the change applies without a restart. When the advisor is
|
|
1374
|
+
* disabled the new configs are simply stored for the next enable.
|
|
1375
|
+
*
|
|
1376
|
+
* @returns the number of advisors active after the rebuild.
|
|
1377
|
+
*/
|
|
1378
|
+
applyAdvisorConfigs(advisors: AdvisorConfig[], sharedInstructions: string | undefined): number {
|
|
1379
|
+
this.#advisorConfigs = advisors;
|
|
1380
|
+
this.#advisorSharedInstructions = sharedInstructions;
|
|
1381
|
+
if (!this.#advisorEnabled) return 0;
|
|
1382
|
+
this.#stopAdvisorRuntime();
|
|
1383
|
+
this.#buildAdvisorRuntime(true);
|
|
1384
|
+
return this.#advisors.length;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Whether the advisor setting is enabled for this session.
|
|
1389
|
+
*/
|
|
1390
|
+
isAdvisorEnabled(): boolean {
|
|
1391
|
+
return this.#advisorEnabled;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* Whether a live advisor agent is attached to this session. True only when
|
|
1396
|
+
* `advisor.enabled` is set AND a model resolved for the `advisor` role AND
|
|
1397
|
+
* the advisor applies to this agent kind — i.e. the actual runtime exists,
|
|
1398
|
+
* not merely the setting. Drives the status-line badge and `/dump advisor`.
|
|
1399
|
+
*/
|
|
1400
|
+
isAdvisorActive(): boolean {
|
|
1401
|
+
return this.#advisors.length > 0;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* The names of the tools available to advisors this session (the pool a
|
|
1406
|
+
* `/advisor configure` editor lists). The advisor is a full agent, so this is the
|
|
1407
|
+
* full built tool set; a tool whose optional factory returns null (e.g. lsp with
|
|
1408
|
+
* no servers) is absent.
|
|
1409
|
+
*/
|
|
1410
|
+
getAdvisorAvailableToolNames(): string[] {
|
|
1411
|
+
return (this.#advisorTools ?? []).map(tool => tool.name);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* The live advisor `Agent`, or `undefined` when no advisor runtime is
|
|
1416
|
+
* attached. Surfaced for diagnostics (`/dump advisor` already serializes
|
|
1417
|
+
* its transcript via {@link formatAdvisorHistoryAsText}) and so callers can
|
|
1418
|
+
* verify the advisor inherits the session's provider-shaping options
|
|
1419
|
+
* (`streamFn`, `promptCacheKey`, `providerSessionState`, ...).
|
|
1420
|
+
*/
|
|
1421
|
+
getAdvisorAgent(): Agent | undefined {
|
|
1422
|
+
return this.#advisors[0]?.agent;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* Lightweight advisor status for the status line: returns just the configured
|
|
1427
|
+
* flag and per-advisor name/status without computing token/cost breakdowns.
|
|
1428
|
+
* Avoids re-tokenizing the advisor transcript on every render frame.
|
|
1429
|
+
*/
|
|
1430
|
+
getAdvisorStatusOverview(): { configured: boolean; advisors: { name: string; status: AdvisorRuntimeStatus }[] } {
|
|
1431
|
+
// Override stale map entries with live runtime status: failureNotified/quotaExhausted
|
|
1432
|
+
// clear on reset() but #advisorStatuses lags until the next build.
|
|
1433
|
+
const liveStatusBySlug = new Map<string, AdvisorRuntimeStatus>();
|
|
1434
|
+
for (const a of this.#advisors) {
|
|
1435
|
+
liveStatusBySlug.set(
|
|
1436
|
+
a.slug,
|
|
1437
|
+
a.runtime.quotaExhausted ? "quota_exhausted" : a.runtime.failureNotified ? "error" : "running",
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
const advisors = [...this.#advisorStatuses.entries()].map(([slug, { name, status }]) => ({
|
|
1441
|
+
name,
|
|
1442
|
+
status: liveStatusBySlug.get(slug) ?? status,
|
|
1443
|
+
}));
|
|
1444
|
+
return { configured: this.#advisorEnabled, advisors };
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Return structured advisor stats for the status command and TUI panel.
|
|
1448
|
+
*/
|
|
1449
|
+
getAdvisorStats(): AdvisorStats {
|
|
1450
|
+
const configured = this.#advisorEnabled;
|
|
1451
|
+
const liveAdvisors = this.#advisors.map(a => this.#computeAdvisorStat(a));
|
|
1452
|
+
// Build the complete roster from #advisorStatuses, which already has the
|
|
1453
|
+
// correct de-duped slugs as keys. Live advisors (from #advisors) carry full
|
|
1454
|
+
// token/cost data; disabled/no-model/quota-exhausted advisors appear as
|
|
1455
|
+
// skeleton entries with just name + status so the status line renders a dot.
|
|
1456
|
+
const liveStatBySlug = new Map(this.#advisors.map((a, i) => [a.slug, liveAdvisors[i]]));
|
|
1457
|
+
const roster: PerAdvisorStat[] = [];
|
|
1458
|
+
for (const [slug, entry] of this.#advisorStatuses) {
|
|
1459
|
+
const live = liveStatBySlug.get(slug);
|
|
1460
|
+
if (live) {
|
|
1461
|
+
roster.push(live);
|
|
1462
|
+
} else {
|
|
1463
|
+
roster.push({
|
|
1464
|
+
name: entry.name,
|
|
1465
|
+
status: entry.status,
|
|
1466
|
+
contextWindow: 0,
|
|
1467
|
+
contextTokens: 0,
|
|
1468
|
+
tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
1469
|
+
cost: 0,
|
|
1470
|
+
messages: { user: 0, assistant: 0, total: 0 },
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
const active = liveAdvisors.length > 0;
|
|
1475
|
+
if (liveAdvisors.length === 0) {
|
|
1476
|
+
return {
|
|
1477
|
+
configured,
|
|
1478
|
+
active,
|
|
1479
|
+
contextWindow: 0,
|
|
1480
|
+
contextTokens: 0,
|
|
1481
|
+
tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
1482
|
+
cost: 0,
|
|
1483
|
+
messages: { user: 0, assistant: 0, total: 0 },
|
|
1484
|
+
advisors: roster,
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
const tokens = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
1488
|
+
const messages = { user: 0, assistant: 0, total: 0 };
|
|
1489
|
+
let cost = 0;
|
|
1490
|
+
let contextTokens = 0;
|
|
1491
|
+
for (const a of liveAdvisors) {
|
|
1492
|
+
tokens.input += a.tokens.input;
|
|
1493
|
+
tokens.output += a.tokens.output;
|
|
1494
|
+
tokens.reasoning += a.tokens.reasoning;
|
|
1495
|
+
tokens.cacheRead += a.tokens.cacheRead;
|
|
1496
|
+
tokens.cacheWrite += a.tokens.cacheWrite;
|
|
1497
|
+
tokens.total += a.tokens.total;
|
|
1498
|
+
messages.user += a.messages.user;
|
|
1499
|
+
messages.assistant += a.messages.assistant;
|
|
1500
|
+
messages.total += a.messages.total;
|
|
1501
|
+
cost += a.cost;
|
|
1502
|
+
contextTokens += a.contextTokens;
|
|
1503
|
+
}
|
|
1504
|
+
// Single-advisor displays read the top-level model/window directly; surface the
|
|
1505
|
+
// first advisor's so the legacy status line stays byte-identical.
|
|
1506
|
+
return {
|
|
1507
|
+
configured,
|
|
1508
|
+
active,
|
|
1509
|
+
model: liveAdvisors[0].model,
|
|
1510
|
+
contextWindow: liveAdvisors[0].contextWindow,
|
|
1511
|
+
contextTokens,
|
|
1512
|
+
tokens,
|
|
1513
|
+
cost,
|
|
1514
|
+
messages,
|
|
1515
|
+
advisors: roster,
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/** Compute one advisor's stats slice (tokens, cost, context, message counts). */
|
|
1520
|
+
#computeAdvisorStat(advisor: ActiveAdvisor): PerAdvisorStat {
|
|
1521
|
+
const model = advisor.agent.state.model;
|
|
1522
|
+
const messages = advisor.agent.state.messages;
|
|
1523
|
+
const contextTokens = this.#estimateAdvisorContextTokens(messages);
|
|
1524
|
+
let input = 0;
|
|
1525
|
+
let output = 0;
|
|
1526
|
+
let reasoning = 0;
|
|
1527
|
+
let cacheRead = 0;
|
|
1528
|
+
let cacheWrite = 0;
|
|
1529
|
+
let totalTokens = 0;
|
|
1530
|
+
let cost = 0;
|
|
1531
|
+
let user = 0;
|
|
1532
|
+
let assistant = 0;
|
|
1533
|
+
for (const message of messages) {
|
|
1534
|
+
if (message.role === "user") user++;
|
|
1535
|
+
if (message.role === "assistant") {
|
|
1536
|
+
assistant++;
|
|
1537
|
+
const assistantMsg = message as AssistantMessage;
|
|
1538
|
+
input += assistantMsg.usage.input;
|
|
1539
|
+
output += assistantMsg.usage.output;
|
|
1540
|
+
reasoning += assistantMsg.usage.reasoningTokens ?? 0;
|
|
1541
|
+
cacheRead += assistantMsg.usage.cacheRead;
|
|
1542
|
+
cacheWrite += assistantMsg.usage.cacheWrite;
|
|
1543
|
+
totalTokens += assistantMsg.usage.totalTokens;
|
|
1544
|
+
cost += assistantMsg.usage.cost.total;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
return {
|
|
1548
|
+
name: advisor.name,
|
|
1549
|
+
status: advisor.runtime.quotaExhausted
|
|
1550
|
+
? "quota_exhausted"
|
|
1551
|
+
: advisor.runtime.failureNotified
|
|
1552
|
+
? "error"
|
|
1553
|
+
: "running",
|
|
1554
|
+
model,
|
|
1555
|
+
contextWindow: model.contextWindow ?? 0,
|
|
1556
|
+
contextTokens,
|
|
1557
|
+
tokens: { input, output, reasoning, cacheRead, cacheWrite, total: totalTokens },
|
|
1558
|
+
cost,
|
|
1559
|
+
messages: { user, assistant, total: messages.length },
|
|
1560
|
+
sessionId: advisor.agent.sessionId,
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/**
|
|
1565
|
+
* Format a concise advisor status line for ACP/text output.
|
|
1566
|
+
*/
|
|
1567
|
+
formatAdvisorStatus(): string {
|
|
1568
|
+
const stats = this.getAdvisorStats();
|
|
1569
|
+
if (!stats.active && stats.advisors.length === 0) {
|
|
1570
|
+
return stats.configured
|
|
1571
|
+
? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
|
|
1572
|
+
: "Advisor is disabled.";
|
|
1573
|
+
}
|
|
1574
|
+
if (stats.advisors.length <= 1) {
|
|
1575
|
+
const s = stats.advisors[0];
|
|
1576
|
+
if (s && s.status === "no_model") {
|
|
1577
|
+
return stats.configured
|
|
1578
|
+
? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
|
|
1579
|
+
: "Advisor is disabled.";
|
|
1580
|
+
}
|
|
1581
|
+
const contextLine =
|
|
1582
|
+
s.contextWindow > 0
|
|
1583
|
+
? `Context: ${s.contextTokens.toLocaleString()} / ${s.contextWindow.toLocaleString()} tokens (${Math.round((s.contextTokens / s.contextWindow) * 100)}%)`
|
|
1584
|
+
: `Context: ${s.contextTokens.toLocaleString()} tokens`;
|
|
1585
|
+
const spendParts = [`${s.tokens.input.toLocaleString()} input`, `${s.tokens.output.toLocaleString()} output`];
|
|
1586
|
+
if (s.tokens.cacheRead > 0) spendParts.push(`${s.tokens.cacheRead.toLocaleString()} cache read`);
|
|
1587
|
+
if (s.tokens.cacheWrite > 0) spendParts.push(`${s.tokens.cacheWrite.toLocaleString()} cache write`);
|
|
1588
|
+
const spendLine = `Spend: ${spendParts.join(", ")}, $${s.cost.toFixed(4)}`;
|
|
1589
|
+
if (!s.model || s.status !== "running") return `Advisor "${s.name}" is ${s.status.replace("_", " ")}.`;
|
|
1590
|
+
return `Advisor is enabled (${s.model.provider}/${s.model.id}). ${contextLine}. ${spendLine}.`;
|
|
1591
|
+
}
|
|
1592
|
+
const lines = [`Advisors enabled (${stats.advisors.length}):`];
|
|
1593
|
+
for (const s of stats.advisors) {
|
|
1594
|
+
const ctx =
|
|
1595
|
+
s.contextWindow > 0
|
|
1596
|
+
? `${s.contextTokens.toLocaleString()} / ${s.contextWindow.toLocaleString()} (${Math.round((s.contextTokens / s.contextWindow) * 100)}%)`
|
|
1597
|
+
: `${s.contextTokens.toLocaleString()}`;
|
|
1598
|
+
lines.push(
|
|
1599
|
+
` • ${s.name}${s.model && s.status === "running" ? ` (${s.model.provider}/${s.model.id})` : ` [${s.status}]`} — context ${ctx} tokens, $${s.cost.toFixed(4)}`,
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
lines.push(
|
|
1603
|
+
`Totals: ${stats.tokens.input.toLocaleString()} input, ${stats.tokens.output.toLocaleString()} output, $${stats.cost.toFixed(4)}.`,
|
|
1604
|
+
);
|
|
1605
|
+
return lines.join("\n");
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* Estimate the advisor's current context tokens. A successful provider usage
|
|
1610
|
+
* after the latest advisor compaction is ground truth for the prompt plus its
|
|
1611
|
+
* generated output; only messages after that anchor are estimated. Usage from
|
|
1612
|
+
* retained pre-compaction messages is stale and must not immediately retrigger
|
|
1613
|
+
* maintenance on the newly compacted context.
|
|
1614
|
+
*/
|
|
1615
|
+
#estimateAdvisorContextTokens(messages: AgentMessage[]): number {
|
|
1616
|
+
let usageAnchorStartIndex = 0;
|
|
1617
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1618
|
+
const message = messages[i];
|
|
1619
|
+
if (message.role !== "compactionSummary") continue;
|
|
1620
|
+
const advisorSummary = message as AdvisorCompactionSummaryMessage;
|
|
1621
|
+
// Advisor summaries created before this runtime-only boundary existed have
|
|
1622
|
+
// no trustworthy way to distinguish retained from newly appended messages.
|
|
1623
|
+
// Conservatively ignore every current assistant until the next compaction.
|
|
1624
|
+
usageAnchorStartIndex = advisorSummary.advisorUsageAnchorStartIndex ?? messages.length;
|
|
1625
|
+
break;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
let lastUsageIndex: number | undefined;
|
|
1629
|
+
let lastUsage: AssistantMessage["usage"] | undefined;
|
|
1630
|
+
for (let i = messages.length - 1; i >= usageAnchorStartIndex; i--) {
|
|
1631
|
+
const message = messages[i];
|
|
1632
|
+
if (message.role !== "assistant") continue;
|
|
1633
|
+
const assistant = message as AssistantMessage;
|
|
1634
|
+
if (assistant.stopReason !== "aborted" && assistant.stopReason !== "error" && assistant.usage) {
|
|
1635
|
+
lastUsage = assistant.usage;
|
|
1636
|
+
lastUsageIndex = i;
|
|
1637
|
+
break;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
const estimateOptions = { excludeEncryptedReasoning: true } as const;
|
|
1642
|
+
if (!lastUsage || lastUsageIndex === undefined) {
|
|
1643
|
+
let estimated = 0;
|
|
1644
|
+
for (const message of messages) {
|
|
1645
|
+
estimated += estimateTokens(message, estimateOptions);
|
|
1646
|
+
}
|
|
1647
|
+
return estimated;
|
|
1648
|
+
}
|
|
1649
|
+
let trailingTokens = 0;
|
|
1650
|
+
for (let i = lastUsageIndex + 1; i < messages.length; i++) {
|
|
1651
|
+
trailingTokens += estimateTokens(messages[i], estimateOptions);
|
|
1652
|
+
}
|
|
1653
|
+
return calculateContextTokens(lastUsage) + trailingTokens;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* Format the advisor agent's own transcript (its system prompt, config,
|
|
1658
|
+
* tools, and the markdown deltas it received plus its thinking/advise/read
|
|
1659
|
+
* calls) as plain text — the advisor-side equivalent of
|
|
1660
|
+
* {@link formatSessionAsText}. Returns null when no advisor is active.
|
|
1661
|
+
*/
|
|
1662
|
+
formatAdvisorHistoryAsText(options?: { compact?: boolean }): string | null {
|
|
1663
|
+
if (this.#advisors.length === 0) return null;
|
|
1664
|
+
const dump = (a: ActiveAdvisor): string =>
|
|
1665
|
+
options?.compact
|
|
1666
|
+
? formatSessionHistoryMarkdown(a.agent.state.messages)
|
|
1667
|
+
: formatSessionDumpText({
|
|
1668
|
+
messages: a.agent.state.messages,
|
|
1669
|
+
systemPrompt: a.agent.state.systemPrompt,
|
|
1670
|
+
model: a.agent.state.model,
|
|
1671
|
+
thinkingLevel: a.agent.state.thinkingLevel,
|
|
1672
|
+
tools: a.agent.state.tools,
|
|
1673
|
+
});
|
|
1674
|
+
if (this.#advisors.length === 1) return dump(this.#advisors[0]);
|
|
1675
|
+
return this.#advisors
|
|
1676
|
+
.map(a => `### Advisor: ${a.name} (${a.agent.state.model.provider}/${a.agent.state.model.id})\n\n${dump(a)}`)
|
|
1677
|
+
.join("\n\n");
|
|
1678
|
+
}
|
|
1679
|
+
}
|