@oh-my-pi/pi-coding-agent 13.18.0 → 14.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +316 -1
- package/package.json +86 -24
- package/scripts/format-prompts.ts +2 -2
- package/src/autoresearch/apply-contract-to-state.ts +24 -0
- package/src/autoresearch/contract.ts +0 -44
- package/src/autoresearch/dashboard.ts +1 -2
- package/src/autoresearch/git.ts +116 -30
- package/src/autoresearch/helpers.ts +49 -0
- package/src/autoresearch/index.ts +28 -187
- package/src/autoresearch/prompt.md +26 -9
- package/src/autoresearch/state.ts +0 -6
- package/src/autoresearch/tools/init-experiment.ts +202 -117
- package/src/autoresearch/tools/log-experiment.ts +123 -178
- package/src/autoresearch/tools/run-experiment.ts +48 -10
- package/src/autoresearch/types.ts +2 -2
- package/src/capability/index.ts +4 -2
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/grep-cli.ts +8 -8
- package/src/cli/grievances-cli.ts +78 -0
- package/src/cli/read-cli.ts +67 -0
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/update-cli.ts +3 -3
- package/src/cli.ts +2 -0
- package/src/commands/grep.ts +6 -1
- package/src/commands/grievances.ts +20 -0
- package/src/commands/read.ts +33 -0
- package/src/commit/agentic/agent.ts +5 -8
- package/src/commit/agentic/index.ts +22 -26
- package/src/commit/agentic/tools/analyze-file.ts +3 -3
- package/src/commit/agentic/tools/git-file-diff.ts +3 -6
- package/src/commit/agentic/tools/git-hunk.ts +3 -3
- package/src/commit/agentic/tools/git-overview.ts +6 -9
- package/src/commit/agentic/tools/index.ts +6 -8
- package/src/commit/agentic/tools/propose-commit.ts +4 -7
- package/src/commit/agentic/tools/recent-commits.ts +3 -3
- package/src/commit/agentic/tools/split-commit.ts +4 -4
- package/src/commit/agentic/validation.ts +1 -1
- package/src/commit/analysis/conventional.ts +4 -4
- package/src/commit/analysis/summary.ts +3 -3
- package/src/commit/changelog/generate.ts +4 -4
- package/src/commit/changelog/index.ts +5 -9
- package/src/commit/map-reduce/map-phase.ts +4 -4
- package/src/commit/map-reduce/reduce-phase.ts +4 -4
- package/src/commit/pipeline.ts +13 -16
- package/src/config/keybindings.ts +7 -6
- package/src/config/prompt-templates.ts +44 -226
- package/src/config/resolve-config-value.ts +4 -2
- package/src/config/settings-schema.ts +98 -2
- package/src/config/settings.ts +25 -26
- package/src/dap/client.ts +674 -0
- package/src/dap/config.ts +150 -0
- package/src/dap/defaults.json +211 -0
- package/src/dap/index.ts +4 -0
- package/src/dap/session.ts +1255 -0
- package/src/dap/types.ts +600 -0
- package/src/debug/log-viewer.ts +3 -2
- package/src/discovery/builtin.ts +1 -2
- package/src/discovery/codex.ts +2 -2
- package/src/discovery/github.ts +2 -1
- package/src/discovery/helpers.ts +2 -2
- package/src/discovery/opencode.ts +2 -2
- package/src/edit/diff.ts +818 -0
- package/src/edit/index.ts +309 -0
- package/src/edit/line-hash.ts +67 -0
- package/src/edit/modes/chunk.ts +454 -0
- package/src/{patch → edit/modes}/hashline.ts +741 -361
- package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
- package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
- package/src/{patch → edit}/normalize.ts +97 -76
- package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
- package/src/exec/bash-executor.ts +4 -2
- package/src/exec/idle-timeout-watchdog.ts +126 -0
- package/src/exec/non-interactive-env.ts +5 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
- package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
- package/src/extensibility/custom-commands/loader.ts +1 -2
- package/src/extensibility/custom-tools/loader.ts +34 -11
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/loader.ts +9 -4
- package/src/extensibility/extensions/runner.ts +24 -1
- package/src/extensibility/extensions/types.ts +4 -2
- package/src/extensibility/hooks/loader.ts +5 -6
- package/src/extensibility/hooks/types.ts +2 -2
- package/src/extensibility/plugins/doctor.ts +2 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
- package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
- package/src/extensibility/slash-commands.ts +3 -7
- package/src/index.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/ipy/executor.ts +58 -17
- package/src/ipy/gateway-coordinator.ts +6 -4
- package/src/ipy/kernel.ts +45 -22
- package/src/ipy/runtime.ts +2 -2
- package/src/lsp/client.ts +7 -4
- package/src/lsp/clients/lsp-linter-client.ts +4 -4
- package/src/lsp/config.ts +2 -2
- package/src/lsp/defaults.json +688 -154
- package/src/lsp/index.ts +234 -45
- package/src/lsp/lspmux.ts +2 -2
- package/src/lsp/startup-events.ts +13 -0
- package/src/lsp/types.ts +12 -1
- package/src/lsp/utils.ts +8 -1
- package/src/main.ts +125 -47
- package/src/memories/index.ts +4 -5
- package/src/modes/acp/acp-agent.ts +563 -163
- package/src/modes/acp/acp-event-mapper.ts +9 -1
- package/src/modes/acp/acp-mode.ts +4 -2
- package/src/modes/components/agent-dashboard.ts +3 -4
- package/src/modes/components/diff.ts +6 -7
- package/src/modes/components/footer.ts +9 -29
- package/src/modes/components/hook-editor.ts +3 -3
- package/src/modes/components/hook-selector.ts +6 -1
- package/src/modes/components/read-tool-group.ts +6 -12
- package/src/modes/components/session-observer-overlay.ts +472 -0
- package/src/modes/components/settings-defs.ts +24 -0
- package/src/modes/components/status-line.ts +15 -61
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/btw-controller.ts +2 -2
- package/src/modes/controllers/command-controller.ts +4 -2
- package/src/modes/controllers/event-controller.ts +59 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +15 -8
- package/src/modes/controllers/selector-controller.ts +26 -0
- package/src/modes/index.ts +20 -2
- package/src/modes/interactive-mode.ts +278 -69
- package/src/modes/rpc/host-tools.ts +186 -0
- package/src/modes/rpc/rpc-client.ts +178 -13
- package/src/modes/rpc/rpc-mode.ts +73 -3
- package/src/modes/rpc/rpc-types.ts +53 -1
- package/src/modes/session-observer-registry.ts +146 -0
- package/src/modes/shared.ts +0 -42
- package/src/modes/theme/theme.ts +80 -8
- package/src/modes/types.ts +4 -2
- package/src/modes/utils/keybinding-matchers.ts +9 -0
- package/src/prompts/system/custom-system-prompt.md +5 -0
- package/src/prompts/system/system-prompt.md +8 -1
- package/src/prompts/tools/chunk-edit.md +219 -0
- package/src/prompts/tools/debug.md +43 -0
- package/src/prompts/tools/grep.md +3 -0
- package/src/prompts/tools/lsp.md +5 -5
- package/src/prompts/tools/read-chunk.md +17 -0
- package/src/prompts/tools/read.md +19 -5
- package/src/sdk.ts +216 -165
- package/src/secrets/index.ts +1 -1
- package/src/secrets/obfuscator.ts +25 -17
- package/src/session/agent-session.ts +381 -286
- package/src/session/agent-storage.ts +12 -12
- package/src/session/compaction/branch-summarization.ts +3 -3
- package/src/session/compaction/compaction.ts +5 -6
- package/src/session/compaction/utils.ts +3 -3
- package/src/session/history-storage.ts +62 -19
- package/src/session/messages.ts +3 -3
- package/src/session/session-dump-format.ts +203 -0
- package/src/session/session-manager.ts +15 -5
- package/src/session/session-storage.ts +4 -2
- package/src/session/streaming-output.ts +1 -1
- package/src/session/tool-choice-queue.ts +213 -0
- package/src/slash-commands/builtin-registry.ts +56 -8
- package/src/ssh/connection-manager.ts +2 -2
- package/src/ssh/sshfs-mount.ts +5 -5
- package/src/stt/downloader.ts +4 -4
- package/src/stt/recorder.ts +4 -4
- package/src/stt/transcriber.ts +2 -2
- package/src/system-prompt.ts +25 -13
- package/src/task/agents.ts +5 -6
- package/src/task/commands.ts +2 -5
- package/src/task/executor.ts +32 -4
- package/src/task/index.ts +91 -82
- package/src/task/template.ts +2 -2
- package/src/task/types.ts +25 -0
- package/src/task/worktree.ts +131 -149
- package/src/tools/ask.ts +2 -3
- package/src/tools/ast-edit.ts +7 -7
- package/src/tools/ast-grep.ts +7 -7
- package/src/tools/auto-generated-guard.ts +36 -41
- package/src/tools/await-tool.ts +2 -2
- package/src/tools/bash.ts +5 -23
- package/src/tools/browser.ts +4 -5
- package/src/tools/calculator.ts +2 -3
- package/src/tools/cancel-job.ts +2 -2
- package/src/tools/checkpoint.ts +3 -3
- package/src/tools/debug.ts +1007 -0
- package/src/tools/exit-plan-mode.ts +3 -3
- package/src/tools/fetch.ts +67 -3
- package/src/tools/find.ts +4 -5
- package/src/tools/fs-cache-invalidation.ts +5 -0
- package/src/tools/gemini-image.ts +13 -5
- package/src/tools/gh.ts +130 -308
- package/src/tools/grep.ts +57 -9
- package/src/tools/index.ts +44 -22
- package/src/tools/inspect-image.ts +4 -4
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/python.ts +19 -6
- package/src/tools/read.ts +211 -146
- package/src/tools/render-mermaid.ts +2 -3
- package/src/tools/render-utils.ts +20 -6
- package/src/tools/renderers.ts +3 -1
- package/src/tools/report-tool-issue.ts +80 -0
- package/src/tools/resolve.ts +70 -39
- package/src/tools/search-tool-bm25.ts +2 -2
- package/src/tools/ssh.ts +2 -2
- package/src/tools/todo-write.ts +2 -2
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/tools/write.ts +5 -6
- package/src/tui/tree-list.ts +3 -1
- package/src/utils/clipboard.ts +80 -0
- package/src/utils/commit-message-generator.ts +2 -3
- package/src/utils/edit-mode.ts +49 -0
- package/src/utils/external-editor.ts +11 -5
- package/src/utils/file-display-mode.ts +6 -5
- package/src/utils/file-mentions.ts +8 -7
- package/src/utils/git.ts +1400 -0
- package/src/utils/image-loading.ts +98 -0
- package/src/utils/title-generator.ts +2 -3
- package/src/utils/tools-manager.ts +6 -6
- package/src/web/scrapers/choosealicense.ts +1 -1
- package/src/web/search/index.ts +3 -3
- package/src/web/search/render.ts +6 -4
- package/src/autoresearch/command-initialize.md +0 -34
- package/src/commit/git/errors.ts +0 -9
- package/src/commit/git/index.ts +0 -210
- package/src/commit/git/operations.ts +0 -54
- package/src/patch/diff.ts +0 -433
- package/src/patch/index.ts +0 -888
- package/src/patch/parser.ts +0 -532
- package/src/patch/types.ts +0 -292
- package/src/prompts/agents/oracle.md +0 -77
- package/src/tools/gh-cli.ts +0 -125
- package/src/tools/pending-action.ts +0 -49
- package/src/utils/child-process.ts +0 -88
- package/src/utils/frontmatter.ts +0 -117
- package/src/utils/image-input.ts +0 -274
- package/src/utils/mime.ts +0 -53
- package/src/utils/prompt-format.ts +0 -170
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
type AgentMessage,
|
|
24
24
|
type AgentState,
|
|
25
25
|
type AgentTool,
|
|
26
|
-
INTENT_FIELD,
|
|
27
26
|
ThinkingLevel,
|
|
28
27
|
} from "@oh-my-pi/pi-agent-core";
|
|
29
28
|
import type {
|
|
@@ -50,8 +49,8 @@ import {
|
|
|
50
49
|
modelsAreEqual,
|
|
51
50
|
parseRateLimitReason,
|
|
52
51
|
} from "@oh-my-pi/pi-ai";
|
|
53
|
-
import type
|
|
54
|
-
import { abortableSleep, getAgentDbPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
52
|
+
import { killTree, MacOSPowerAssertion, type SearchDb } from "@oh-my-pi/pi-natives";
|
|
53
|
+
import { abortableSleep, getAgentDbPath, isEnoent, logger, prompt, setNativeKillTree } from "@oh-my-pi/pi-utils";
|
|
55
54
|
import type { AsyncJob, AsyncJobManager } from "../async";
|
|
56
55
|
import type { Rule } from "../capability/rule";
|
|
57
56
|
import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
|
|
@@ -59,10 +58,12 @@ import {
|
|
|
59
58
|
extractExplicitThinkingSelector,
|
|
60
59
|
formatModelString,
|
|
61
60
|
parseModelString,
|
|
61
|
+
type ResolvedModelRoleValue,
|
|
62
62
|
resolveModelRoleValue,
|
|
63
63
|
} from "../config/model-resolver";
|
|
64
|
-
import { expandPromptTemplate, type PromptTemplate
|
|
64
|
+
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
65
65
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
66
|
+
import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
66
67
|
import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
|
|
67
68
|
import { exportSessionToHtml } from "../export/html";
|
|
68
69
|
import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
|
|
@@ -103,7 +104,6 @@ import {
|
|
|
103
104
|
selectDiscoverableMCPToolNamesByServer,
|
|
104
105
|
} from "../mcp/discoverable-tool-metadata";
|
|
105
106
|
import { getCurrentThemeName, theme } from "../modes/theme/theme";
|
|
106
|
-
import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../patch";
|
|
107
107
|
import type { PlanModeState } from "../plan-mode/state";
|
|
108
108
|
import autoHandoffThresholdFocusPrompt from "../prompts/system/auto-handoff-threshold-focus.md" with { type: "text" };
|
|
109
109
|
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
@@ -114,13 +114,15 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
|
|
|
114
114
|
type: "text",
|
|
115
115
|
};
|
|
116
116
|
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
|
117
|
-
import type
|
|
117
|
+
import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
|
|
118
118
|
import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
|
119
|
+
import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
119
120
|
import type { CheckpointState } from "../tools/checkpoint";
|
|
120
121
|
import { outputMeta } from "../tools/output-meta";
|
|
121
122
|
import { resolveToCwd } from "../tools/path-utils";
|
|
122
|
-
import
|
|
123
|
+
import { isAutoQaEnabled } from "../tools/report-tool-issue";
|
|
123
124
|
import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
|
|
125
|
+
import { ToolError } from "../tools/tool-errors";
|
|
124
126
|
import { clampTimeout } from "../tools/tool-timeouts";
|
|
125
127
|
import { parseCommandArgs } from "../utils/command-args";
|
|
126
128
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
@@ -140,16 +142,13 @@ import {
|
|
|
140
142
|
import { DEFAULT_PRUNE_CONFIG, pruneToolOutputs } from "./compaction/pruning";
|
|
141
143
|
import {
|
|
142
144
|
type BashExecutionMessage,
|
|
143
|
-
type BranchSummaryMessage,
|
|
144
|
-
bashExecutionToText,
|
|
145
145
|
type CompactionSummaryMessage,
|
|
146
146
|
type CustomMessage,
|
|
147
147
|
convertToLlm,
|
|
148
148
|
type FileMentionMessage,
|
|
149
|
-
type HookMessage,
|
|
150
149
|
type PythonExecutionMessage,
|
|
151
|
-
pythonExecutionToText,
|
|
152
150
|
} from "./messages";
|
|
151
|
+
import { formatSessionDumpText } from "./session-dump-format";
|
|
153
152
|
import type {
|
|
154
153
|
BranchSummaryEntry,
|
|
155
154
|
CompactionEntry,
|
|
@@ -158,11 +157,12 @@ import type {
|
|
|
158
157
|
SessionManager,
|
|
159
158
|
} from "./session-manager";
|
|
160
159
|
import { getLatestCompactionEntry } from "./session-manager";
|
|
160
|
+
import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
161
161
|
|
|
162
162
|
/** Session-specific events that extend the core AgentEvent */
|
|
163
163
|
export type AgentSessionEvent =
|
|
164
164
|
| AgentEvent
|
|
165
|
-
| { type: "auto_compaction_start"; reason: "threshold" | "overflow"; action: "context-full" | "handoff" }
|
|
165
|
+
| { type: "auto_compaction_start"; reason: "threshold" | "overflow" | "idle"; action: "context-full" | "handoff" }
|
|
166
166
|
| {
|
|
167
167
|
type: "auto_compaction_end";
|
|
168
168
|
action: "context-full" | "handoff";
|
|
@@ -243,8 +243,6 @@ export interface AgentSessionConfig {
|
|
|
243
243
|
ttsrManager?: TtsrManager;
|
|
244
244
|
/** Secret obfuscator for deobfuscating streaming edit content */
|
|
245
245
|
obfuscator?: SecretObfuscator;
|
|
246
|
-
/** Pending action store for preview/apply workflows */
|
|
247
|
-
pendingActionStore?: PendingActionStore;
|
|
248
246
|
/** Shared native search DB for grep/glob/fuzzyFind-backed workflows. */
|
|
249
247
|
searchDb?: SearchDb;
|
|
250
248
|
}
|
|
@@ -320,7 +318,7 @@ interface HandoffOptions {
|
|
|
320
318
|
|
|
321
319
|
/** Standard thinking levels */
|
|
322
320
|
|
|
323
|
-
const AUTO_HANDOFF_THRESHOLD_FOCUS =
|
|
321
|
+
const AUTO_HANDOFF_THRESHOLD_FOCUS = prompt.render(autoHandoffThresholdFocusPrompt);
|
|
324
322
|
|
|
325
323
|
type RetryFallbackChains = Record<string, string[]>;
|
|
326
324
|
|
|
@@ -399,6 +397,9 @@ export class AgentSession {
|
|
|
399
397
|
readonly sessionManager: SessionManager;
|
|
400
398
|
readonly settings: Settings;
|
|
401
399
|
readonly searchDb: SearchDb | undefined;
|
|
400
|
+
|
|
401
|
+
#powerAssertion: MacOSPowerAssertion | undefined;
|
|
402
|
+
|
|
402
403
|
readonly configWarnings: string[] = [];
|
|
403
404
|
|
|
404
405
|
#asyncJobManager: AsyncJobManager | undefined = undefined;
|
|
@@ -409,7 +410,6 @@ export class AgentSession {
|
|
|
409
410
|
|
|
410
411
|
// Event subscription state
|
|
411
412
|
#unsubscribeAgent?: () => void;
|
|
412
|
-
#unsubscribePendingActionPush?: () => void;
|
|
413
413
|
#eventListeners: AgentSessionEventListener[] = [];
|
|
414
414
|
|
|
415
415
|
/** Tracks pending steering messages for UI display. Removed when delivered. */
|
|
@@ -444,7 +444,7 @@ export class AgentSession {
|
|
|
444
444
|
#todoReminderCount = 0;
|
|
445
445
|
#todoPhases: TodoPhase[] = [];
|
|
446
446
|
#todoClearTimers = new Map<string, Timer>();
|
|
447
|
-
#
|
|
447
|
+
#toolChoiceQueue = new ToolChoiceQueue();
|
|
448
448
|
|
|
449
449
|
// Bash execution state
|
|
450
450
|
#bashAbortController: AbortController | undefined = undefined;
|
|
@@ -482,6 +482,7 @@ export class AgentSession {
|
|
|
482
482
|
#discoverableMCPTools = new Map<string, DiscoverableMCPTool>();
|
|
483
483
|
#discoverableMCPSearchIndex: DiscoverableMCPSearchIndex | null = null;
|
|
484
484
|
#selectedMCPToolNames = new Set<string>();
|
|
485
|
+
#rpcHostToolNames = new Set<string>();
|
|
485
486
|
#defaultSelectedMCPServerNames = new Set<string>();
|
|
486
487
|
#defaultSelectedMCPToolNames = new Set<string>();
|
|
487
488
|
#sessionDefaultSelectedMCPToolNames = new Map<string, string[]>();
|
|
@@ -501,20 +502,49 @@ export class AgentSession {
|
|
|
501
502
|
|
|
502
503
|
#streamingEditAbortTriggered = false;
|
|
503
504
|
#streamingEditCheckedLineCounts = new Map<string, number>();
|
|
505
|
+
|
|
506
|
+
#streamingEditPrecheckedToolCallIds = new Set<string>();
|
|
507
|
+
|
|
504
508
|
#streamingEditFileCache = new Map<string, string>();
|
|
505
509
|
#promptInFlightCount = 0;
|
|
506
510
|
#obfuscator: SecretObfuscator | undefined;
|
|
507
|
-
#pendingActionStore: PendingActionStore | undefined;
|
|
508
511
|
#checkpointState: CheckpointState | undefined = undefined;
|
|
509
512
|
#pendingRewindReport: string | undefined = undefined;
|
|
510
513
|
#promptGeneration = 0;
|
|
511
514
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
512
515
|
|
|
516
|
+
#startPowerAssertion(): void {
|
|
517
|
+
if (process.platform !== "darwin") {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
this.#powerAssertion = MacOSPowerAssertion.start({ reason: "Oh My Pi agent session" });
|
|
522
|
+
} catch (error) {
|
|
523
|
+
logger.warn("Failed to acquire macOS power assertion", { error: String(error) });
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
#stopPowerAssertion(): void {
|
|
528
|
+
const assertion = this.#powerAssertion;
|
|
529
|
+
this.#powerAssertion = undefined;
|
|
530
|
+
if (!assertion) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
assertion.stop();
|
|
535
|
+
} catch (error) {
|
|
536
|
+
logger.warn("Failed to release macOS power assertion", { error: String(error) });
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
513
540
|
constructor(config: AgentSessionConfig) {
|
|
541
|
+
setNativeKillTree(killTree);
|
|
542
|
+
|
|
514
543
|
this.agent = config.agent;
|
|
515
544
|
this.sessionManager = config.sessionManager;
|
|
516
545
|
this.settings = config.settings;
|
|
517
546
|
this.searchDb = config.searchDb;
|
|
547
|
+
this.#startPowerAssertion();
|
|
518
548
|
this.#asyncJobManager = config.asyncJobManager;
|
|
519
549
|
this.#scopedModels = config.scopedModels ?? [];
|
|
520
550
|
this.#thinkingLevel = config.thinkingLevel;
|
|
@@ -539,7 +569,7 @@ export class AgentSession {
|
|
|
539
569
|
this.#defaultSelectedMCPServerNames = new Set(config.defaultSelectedMCPServerNames ?? []);
|
|
540
570
|
this.#defaultSelectedMCPToolNames = new Set(config.defaultSelectedMCPToolNames ?? []);
|
|
541
571
|
this.#pruneSelectedMCPToolNames();
|
|
542
|
-
const persistedSelectedMCPToolNames = this.
|
|
572
|
+
const persistedSelectedMCPToolNames = this.buildDisplaySessionContext().selectedMCPToolNames;
|
|
543
573
|
const currentSelectedMCPToolNames = this.getSelectedMCPToolNames();
|
|
544
574
|
const persistInitialMCPToolSelection =
|
|
545
575
|
config.persistInitialMCPToolSelection ?? this.sessionManager.getBranch().length === 0;
|
|
@@ -556,24 +586,16 @@ export class AgentSession {
|
|
|
556
586
|
);
|
|
557
587
|
this.#ttsrManager = config.ttsrManager;
|
|
558
588
|
this.#obfuscator = config.obfuscator;
|
|
559
|
-
this.agent.
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
this.agent.steer({
|
|
568
|
-
role: "custom",
|
|
569
|
-
customType: "resolve-reminder",
|
|
570
|
-
content: reminderText,
|
|
571
|
-
display: false,
|
|
572
|
-
details: { toolName: action.sourceToolName },
|
|
573
|
-
attribution: "agent",
|
|
574
|
-
timestamp: Date.now(),
|
|
575
|
-
});
|
|
589
|
+
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
|
590
|
+
const event: AgentEvent = {
|
|
591
|
+
type: "message_update",
|
|
592
|
+
message,
|
|
593
|
+
assistantMessageEvent,
|
|
594
|
+
};
|
|
595
|
+
this.#preCacheStreamingEditFile(event);
|
|
596
|
+
this.#maybeAbortStreamingEdit(event);
|
|
576
597
|
});
|
|
598
|
+
this.agent.providerSessionState = this.#providerSessionState;
|
|
577
599
|
this.#syncTodoPhasesFromBranch();
|
|
578
600
|
|
|
579
601
|
// Always subscribe to agent events for internal handling
|
|
@@ -586,10 +608,40 @@ export class AgentSession {
|
|
|
586
608
|
return this.#modelRegistry;
|
|
587
609
|
}
|
|
588
610
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
this.#
|
|
592
|
-
|
|
611
|
+
/** Advance the tool-choice queue and return the next directive for the upcoming LLM call. */
|
|
612
|
+
nextToolChoice(): ToolChoice | undefined {
|
|
613
|
+
return this.#toolChoiceQueue.nextToolChoice();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Force the next model call to target a specific active tool, then terminate
|
|
618
|
+
* the agent loop. Pushes a two-step sequence [forced, "none"] so the model
|
|
619
|
+
* calls exactly the forced tool once and then cannot call another.
|
|
620
|
+
*/
|
|
621
|
+
setForcedToolChoice(toolName: string): void {
|
|
622
|
+
if (!this.getActiveToolNames().includes(toolName)) {
|
|
623
|
+
throw new Error(`Tool "${toolName}" is not currently active.`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const forced = buildNamedToolChoice(toolName, this.model);
|
|
627
|
+
if (!forced || typeof forced === "string") {
|
|
628
|
+
throw new Error("Current model does not support forcing a specific tool.");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
this.#toolChoiceQueue.pushSequence([forced, "none"], {
|
|
632
|
+
label: "user-force",
|
|
633
|
+
onRejected: () => "requeue",
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** The tool-choice queue: forces forthcoming tool invocations and carries handlers. */
|
|
638
|
+
get toolChoiceQueue(): ToolChoiceQueue {
|
|
639
|
+
return this.#toolChoiceQueue;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** Peek the in-flight directive's invocation handler for use by the resolve tool. */
|
|
643
|
+
peekQueueInvoker(): ((input: unknown) => Promise<unknown> | unknown) | undefined {
|
|
644
|
+
return this.#toolChoiceQueue.peekInFlightInvoker();
|
|
593
645
|
}
|
|
594
646
|
|
|
595
647
|
/** Provider-scoped mutable state store for transport/session caches. */
|
|
@@ -668,7 +720,22 @@ export class AgentSession {
|
|
|
668
720
|
}
|
|
669
721
|
}
|
|
670
722
|
|
|
671
|
-
|
|
723
|
+
// Deobfuscate assistant message content for display emission — the LLM echoes back
|
|
724
|
+
// obfuscated placeholders, but listeners (TUI, extensions, exporters) must see real
|
|
725
|
+
// values. The original event.message stays obfuscated so the persistence path below
|
|
726
|
+
// writes `#HASH#` tokens to the session file; convertToLlm re-obfuscates outbound
|
|
727
|
+
// traffic on the next turn. Walks text, thinking, and toolCall arguments/intent.
|
|
728
|
+
let displayEvent: AgentEvent = event;
|
|
729
|
+
const obfuscator = this.#obfuscator;
|
|
730
|
+
if (obfuscator && event.type === "message_end" && event.message.role === "assistant") {
|
|
731
|
+
const message = event.message;
|
|
732
|
+
const deobfuscatedContent = obfuscator.deobfuscateObject(message.content);
|
|
733
|
+
if (deobfuscatedContent !== message.content) {
|
|
734
|
+
displayEvent = { ...event, message: { ...message, content: deobfuscatedContent } };
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
await this.#emitSessionEvent(displayEvent);
|
|
672
739
|
|
|
673
740
|
if (event.type === "turn_start") {
|
|
674
741
|
this.#resetStreamingEditState();
|
|
@@ -680,6 +747,17 @@ export class AgentSession {
|
|
|
680
747
|
if (event.type === "turn_end" && this.#ttsrManager) {
|
|
681
748
|
this.#ttsrManager.incrementMessageCount();
|
|
682
749
|
}
|
|
750
|
+
// Finalize the tool-choice queue's in-flight yield after tools have executed.
|
|
751
|
+
// This must happen at turn_end (not message_end) because onInvoked handlers
|
|
752
|
+
// run during tool execution, which happens between message_end and turn_end.
|
|
753
|
+
if (event.type === "turn_end" && this.#toolChoiceQueue.hasInFlight) {
|
|
754
|
+
const msg = event.message as AssistantMessage;
|
|
755
|
+
if (msg.stopReason === "aborted" || msg.stopReason === "error") {
|
|
756
|
+
this.#toolChoiceQueue.reject(msg.stopReason === "error" ? "error" : "aborted");
|
|
757
|
+
} else {
|
|
758
|
+
this.#toolChoiceQueue.resolve();
|
|
759
|
+
}
|
|
760
|
+
}
|
|
683
761
|
if (event.type === "turn_end" && this.#pendingRewindReport) {
|
|
684
762
|
const report = this.#pendingRewindReport;
|
|
685
763
|
this.#pendingRewindReport = undefined;
|
|
@@ -778,8 +856,13 @@ export class AgentSession {
|
|
|
778
856
|
}
|
|
779
857
|
}
|
|
780
858
|
|
|
781
|
-
if (
|
|
782
|
-
|
|
859
|
+
if (
|
|
860
|
+
event.type === "message_update" &&
|
|
861
|
+
(event.assistantMessageEvent.type === "toolcall_start" ||
|
|
862
|
+
event.assistantMessageEvent.type === "toolcall_delta" ||
|
|
863
|
+
event.assistantMessageEvent.type === "toolcall_end")
|
|
864
|
+
) {
|
|
865
|
+
void this.#preCacheStreamingEditFile(event);
|
|
783
866
|
}
|
|
784
867
|
|
|
785
868
|
if (
|
|
@@ -1097,7 +1180,7 @@ export class AgentSession {
|
|
|
1097
1180
|
if (this.#pendingTtsrInjections.length === 0) return undefined;
|
|
1098
1181
|
const rules = this.#pendingTtsrInjections;
|
|
1099
1182
|
const content = rules
|
|
1100
|
-
.map(r =>
|
|
1183
|
+
.map(r => prompt.render(ttsrInterruptTemplate, { name: r.name, path: r.path, content: r.content }))
|
|
1101
1184
|
.join("\n\n");
|
|
1102
1185
|
this.#pendingTtsrInjections = [];
|
|
1103
1186
|
return { content, rules };
|
|
@@ -1310,31 +1393,101 @@ export class AgentSession {
|
|
|
1310
1393
|
#resetStreamingEditState(): void {
|
|
1311
1394
|
this.#streamingEditAbortTriggered = false;
|
|
1312
1395
|
this.#streamingEditCheckedLineCounts.clear();
|
|
1396
|
+
this.#streamingEditPrecheckedToolCallIds.clear();
|
|
1313
1397
|
this.#streamingEditFileCache.clear();
|
|
1314
1398
|
}
|
|
1315
1399
|
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1400
|
+
#getStreamingEditToolCall(event: AgentEvent):
|
|
1401
|
+
| {
|
|
1402
|
+
toolCall: ToolCall;
|
|
1403
|
+
path: string;
|
|
1404
|
+
resolvedPath: string;
|
|
1405
|
+
diff?: string;
|
|
1406
|
+
op?: string;
|
|
1407
|
+
rename?: string;
|
|
1408
|
+
}
|
|
1409
|
+
| undefined {
|
|
1410
|
+
if (event.type !== "message_update") return undefined;
|
|
1411
|
+
if (event.message.role !== "assistant") return undefined;
|
|
1412
|
+
|
|
1413
|
+
const contentIndex = event.assistantMessageEvent.contentIndex ?? 0;
|
|
1324
1414
|
const messageContent = event.message.content;
|
|
1325
|
-
if (!Array.isArray(messageContent) || contentIndex >= messageContent.length)
|
|
1415
|
+
if (!Array.isArray(messageContent) || contentIndex < 0 || contentIndex >= messageContent.length) {
|
|
1416
|
+
return undefined;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1326
1419
|
const toolCall = messageContent[contentIndex] as ToolCall;
|
|
1327
|
-
if (toolCall.name !== "edit") return;
|
|
1420
|
+
if (toolCall.name !== "edit") return undefined;
|
|
1328
1421
|
|
|
1329
1422
|
const args = toolCall.arguments;
|
|
1330
|
-
if (!args || typeof args !== "object" || Array.isArray(args)) return;
|
|
1331
|
-
if ("old_text" in args || "new_text" in args) return;
|
|
1423
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return undefined;
|
|
1424
|
+
if ("old_text" in args || "new_text" in args) return undefined;
|
|
1332
1425
|
|
|
1333
1426
|
const path = typeof args.path === "string" ? args.path : undefined;
|
|
1334
|
-
if (!path) return;
|
|
1427
|
+
if (!path) return undefined;
|
|
1335
1428
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1429
|
+
return {
|
|
1430
|
+
toolCall,
|
|
1431
|
+
path,
|
|
1432
|
+
resolvedPath: resolveToCwd(path, this.sessionManager.getCwd()),
|
|
1433
|
+
diff: typeof args.diff === "string" ? args.diff : undefined,
|
|
1434
|
+
op: typeof args.op === "string" ? args.op : undefined,
|
|
1435
|
+
rename: typeof args.rename === "string" ? args.rename : undefined,
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
#lastStreamingEditToolCallId: string | undefined;
|
|
1440
|
+
#abortStreamingEditForAutoGeneratedPath(toolCall: ToolCall, path: string, resolvedPath: string): void {
|
|
1441
|
+
if (this.#lastStreamingEditToolCallId === toolCall.id) return;
|
|
1442
|
+
this.#lastStreamingEditToolCallId = toolCall.id;
|
|
1443
|
+
void assertEditableFile(resolvedPath, path).catch(err => {
|
|
1444
|
+
// peekFile and other I/O can reject with ENOENT, etc. Only ToolError means
|
|
1445
|
+
// auto-generated detection; other failures are left for the edit tool.
|
|
1446
|
+
if (!(err instanceof ToolError)) return;
|
|
1447
|
+
if (this.#lastStreamingEditToolCallId !== toolCall.id) return;
|
|
1448
|
+
|
|
1449
|
+
if (!this.#streamingEditAbortTriggered) {
|
|
1450
|
+
this.#streamingEditAbortTriggered = true;
|
|
1451
|
+
logger.warn("Streaming edit aborted due to auto-generated file guard", {
|
|
1452
|
+
toolCallId: toolCall.id,
|
|
1453
|
+
path,
|
|
1454
|
+
});
|
|
1455
|
+
this.agent.abort();
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
#preCacheStreamingEditFile(event: AgentEvent): void {
|
|
1461
|
+
if (!this.settings.get("edit.streamingAbort")) return;
|
|
1462
|
+
if (this.#streamingEditAbortTriggered) return;
|
|
1463
|
+
if (event.type !== "message_update") return;
|
|
1464
|
+
|
|
1465
|
+
const assistantEvent = event.assistantMessageEvent;
|
|
1466
|
+
if (
|
|
1467
|
+
assistantEvent.type !== "toolcall_start" &&
|
|
1468
|
+
assistantEvent.type !== "toolcall_delta" &&
|
|
1469
|
+
assistantEvent.type !== "toolcall_end"
|
|
1470
|
+
) {
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
const streamingEdit = this.#getStreamingEditToolCall(event);
|
|
1475
|
+
if (!streamingEdit) return;
|
|
1476
|
+
|
|
1477
|
+
const shouldCheckAutoGenerated =
|
|
1478
|
+
!streamingEdit.toolCall.id || !this.#streamingEditPrecheckedToolCallIds.has(streamingEdit.toolCall.id);
|
|
1479
|
+
if (shouldCheckAutoGenerated) {
|
|
1480
|
+
if (streamingEdit.toolCall.id) {
|
|
1481
|
+
this.#streamingEditPrecheckedToolCallIds.add(streamingEdit.toolCall.id);
|
|
1482
|
+
}
|
|
1483
|
+
this.#abortStreamingEditForAutoGeneratedPath(
|
|
1484
|
+
streamingEdit.toolCall,
|
|
1485
|
+
streamingEdit.path,
|
|
1486
|
+
streamingEdit.resolvedPath,
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
this.#ensureFileCache(streamingEdit.resolvedPath);
|
|
1338
1491
|
}
|
|
1339
1492
|
|
|
1340
1493
|
#ensureFileCache(resolvedPath: string): void {
|
|
@@ -1359,24 +1512,15 @@ export class AgentSession {
|
|
|
1359
1512
|
if (!this.settings.get("edit.streamingAbort")) return;
|
|
1360
1513
|
if (this.#streamingEditAbortTriggered) return;
|
|
1361
1514
|
if (event.type !== "message_update") return;
|
|
1515
|
+
|
|
1362
1516
|
const assistantEvent = event.assistantMessageEvent;
|
|
1363
1517
|
if (assistantEvent.type !== "toolcall_end" && assistantEvent.type !== "toolcall_delta") return;
|
|
1364
|
-
if (event.message.role !== "assistant") return;
|
|
1365
|
-
|
|
1366
|
-
const contentIndex = assistantEvent.contentIndex;
|
|
1367
|
-
const messageContent = event.message.content;
|
|
1368
|
-
if (!Array.isArray(messageContent) || contentIndex >= messageContent.length) return;
|
|
1369
|
-
const toolCall = messageContent[contentIndex] as ToolCall;
|
|
1370
|
-
if (toolCall.name !== "edit" || !toolCall.id) return;
|
|
1371
1518
|
|
|
1372
|
-
const
|
|
1373
|
-
if (!
|
|
1374
|
-
if ("old_text" in args || "new_text" in args) return;
|
|
1519
|
+
const streamingEdit = this.#getStreamingEditToolCall(event);
|
|
1520
|
+
if (!streamingEdit?.toolCall.id) return;
|
|
1375
1521
|
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1378
|
-
const op = typeof args.op === "string" ? args.op : undefined;
|
|
1379
|
-
if (!path || !diff) return;
|
|
1522
|
+
const { toolCall, path, resolvedPath, diff, op, rename } = streamingEdit;
|
|
1523
|
+
if (!diff) return;
|
|
1380
1524
|
if (op && op !== "update") return;
|
|
1381
1525
|
|
|
1382
1526
|
if (!diff.includes("\n")) return;
|
|
@@ -1399,13 +1543,10 @@ export class AgentSession {
|
|
|
1399
1543
|
if (lastChecked !== undefined && lineCount <= lastChecked) return;
|
|
1400
1544
|
this.#streamingEditCheckedLineCounts.set(toolCall.id, lineCount);
|
|
1401
1545
|
|
|
1402
|
-
const rename = typeof args.rename === "string" ? args.rename : undefined;
|
|
1403
|
-
|
|
1404
1546
|
const removedLines = lines
|
|
1405
1547
|
.filter(line => line.startsWith("-") && !line.startsWith("--- "))
|
|
1406
1548
|
.map(line => line.slice(1));
|
|
1407
1549
|
if (removedLines.length > 0) {
|
|
1408
|
-
const resolvedPath = resolveToCwd(path, this.sessionManager.getCwd());
|
|
1409
1550
|
let cachedContent = this.#streamingEditFileCache.get(resolvedPath);
|
|
1410
1551
|
if (cachedContent === undefined) {
|
|
1411
1552
|
this.#ensureFileCache(resolvedPath);
|
|
@@ -1495,7 +1636,6 @@ export class AgentSession {
|
|
|
1495
1636
|
if (!this.#extensionRunner) return;
|
|
1496
1637
|
if (event.type === "agent_start") {
|
|
1497
1638
|
this.#turnIndex = 0;
|
|
1498
|
-
this.#nextToolChoiceOverride = undefined;
|
|
1499
1639
|
await this.#extensionRunner.emit({ type: "agent_start" });
|
|
1500
1640
|
} else if (event.type === "agent_end") {
|
|
1501
1641
|
await this.#extensionRunner.emit({ type: "agent_end", messages: event.messages });
|
|
@@ -1661,10 +1801,9 @@ export class AgentSession {
|
|
|
1661
1801
|
if (drained === false && deliveryState) {
|
|
1662
1802
|
logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
|
|
1663
1803
|
}
|
|
1804
|
+
this.#stopPowerAssertion();
|
|
1664
1805
|
await this.sessionManager.close();
|
|
1665
1806
|
this.#closeAllProviderSessions("dispose");
|
|
1666
|
-
this.#unsubscribePendingActionPush?.();
|
|
1667
|
-
this.#unsubscribePendingActionPush = undefined;
|
|
1668
1807
|
this.#disconnectFromAgent();
|
|
1669
1808
|
this.#eventListeners = [];
|
|
1670
1809
|
}
|
|
@@ -1877,6 +2016,14 @@ export class AgentSession {
|
|
|
1877
2016
|
validToolNames.push(name);
|
|
1878
2017
|
}
|
|
1879
2018
|
}
|
|
2019
|
+
// Auto-QA tool must survive any runtime tool-set mutation.
|
|
2020
|
+
if (isAutoQaEnabled(this.settings) && !validToolNames.includes("report_tool_issue")) {
|
|
2021
|
+
const qaTool = this.#toolRegistry.get("report_tool_issue");
|
|
2022
|
+
if (qaTool) {
|
|
2023
|
+
tools.push(qaTool);
|
|
2024
|
+
validToolNames.push("report_tool_issue");
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
1880
2027
|
if (this.#mcpDiscoveryEnabled) {
|
|
1881
2028
|
this.#selectedMCPToolNames = new Set(
|
|
1882
2029
|
validToolNames.filter(
|
|
@@ -1968,7 +2115,7 @@ export class AgentSession {
|
|
|
1968
2115
|
|
|
1969
2116
|
this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
|
|
1970
2117
|
this.#pruneSelectedMCPToolNames();
|
|
1971
|
-
if (!this.
|
|
2118
|
+
if (!this.buildDisplaySessionContext().hasPersistedMCPToolSelection) {
|
|
1972
2119
|
this.#selectedMCPToolNames = new Set([
|
|
1973
2120
|
...this.#selectedMCPToolNames,
|
|
1974
2121
|
...this.#getConfiguredDefaultSelectedMCPToolNames(),
|
|
@@ -1983,6 +2130,49 @@ export class AgentSession {
|
|
|
1983
2130
|
await this.#applyActiveToolsByName(nextActive, { previousSelectedMCPToolNames });
|
|
1984
2131
|
}
|
|
1985
2132
|
|
|
2133
|
+
/**
|
|
2134
|
+
* Replace RPC host-owned tools and refresh the active tool set before the next model call.
|
|
2135
|
+
*/
|
|
2136
|
+
async refreshRpcHostTools(rpcTools: AgentTool[]): Promise<void> {
|
|
2137
|
+
const nextToolNames = rpcTools.map(tool => tool.name);
|
|
2138
|
+
const uniqueToolNames = new Set(nextToolNames);
|
|
2139
|
+
if (uniqueToolNames.size !== nextToolNames.length) {
|
|
2140
|
+
throw new Error("RPC host tool names must be unique");
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
for (const name of uniqueToolNames) {
|
|
2144
|
+
if (this.#toolRegistry.has(name) && !this.#rpcHostToolNames.has(name)) {
|
|
2145
|
+
throw new Error(`RPC host tool "${name}" conflicts with an existing tool`);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
const previousRpcHostToolNames = new Set(this.#rpcHostToolNames);
|
|
2150
|
+
const previousActiveToolNames = this.getActiveToolNames();
|
|
2151
|
+
for (const name of previousRpcHostToolNames) {
|
|
2152
|
+
this.#toolRegistry.delete(name);
|
|
2153
|
+
}
|
|
2154
|
+
this.#rpcHostToolNames.clear();
|
|
2155
|
+
|
|
2156
|
+
for (const tool of rpcTools) {
|
|
2157
|
+
const finalTool = (
|
|
2158
|
+
this.#extensionRunner ? new ExtensionToolWrapper(tool, this.#extensionRunner) : tool
|
|
2159
|
+
) as AgentTool;
|
|
2160
|
+
this.#toolRegistry.set(finalTool.name, finalTool);
|
|
2161
|
+
this.#rpcHostToolNames.add(finalTool.name);
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
const activeNonRpcToolNames = previousActiveToolNames.filter(name => !previousRpcHostToolNames.has(name));
|
|
2165
|
+
const preservedRpcToolNames = previousActiveToolNames.filter(
|
|
2166
|
+
name => previousRpcHostToolNames.has(name) && this.#rpcHostToolNames.has(name),
|
|
2167
|
+
);
|
|
2168
|
+
const autoActivatedRpcToolNames = rpcTools
|
|
2169
|
+
.filter(tool => !tool.hidden && !previousRpcHostToolNames.has(tool.name))
|
|
2170
|
+
.map(tool => tool.name);
|
|
2171
|
+
await this.#applyActiveToolsByName(
|
|
2172
|
+
Array.from(new Set([...activeNonRpcToolNames, ...preservedRpcToolNames, ...autoActivatedRpcToolNames])),
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
|
|
1986
2176
|
/** Whether auto-compaction is currently running */
|
|
1987
2177
|
get isCompacting(): boolean {
|
|
1988
2178
|
return this.#autoCompactionAbortController !== undefined || this.#compactionAbortController !== undefined;
|
|
@@ -1993,6 +2183,10 @@ export class AgentSession {
|
|
|
1993
2183
|
return this.agent.state.messages;
|
|
1994
2184
|
}
|
|
1995
2185
|
|
|
2186
|
+
buildDisplaySessionContext(): SessionContext {
|
|
2187
|
+
return deobfuscateSessionContext(this.sessionManager.buildSessionContext(), this.#obfuscator);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
1996
2190
|
/** Convert session messages using the same pre-LLM pipeline as the active session. */
|
|
1997
2191
|
async convertMessagesToLlm(messages: AgentMessage[], signal?: AbortSignal): Promise<Message[]> {
|
|
1998
2192
|
const transformedMessages = await this.#transformContext(messages, signal);
|
|
@@ -2103,7 +2297,16 @@ export class AgentSession {
|
|
|
2103
2297
|
}
|
|
2104
2298
|
|
|
2105
2299
|
resolveRoleModel(role: string): Model | undefined {
|
|
2106
|
-
return this.#
|
|
2300
|
+
return this.#resolveRoleModelFull(role, this.#modelRegistry.getAvailable(), this.model).model;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
/**
|
|
2304
|
+
* Resolve a role to its model AND thinking level.
|
|
2305
|
+
* Unlike resolveRoleModel(), this preserves the thinking level suffix
|
|
2306
|
+
* from role configuration (e.g., "anthropic/claude-sonnet-4-5:xhigh").
|
|
2307
|
+
*/
|
|
2308
|
+
resolveRoleModelWithThinking(role: string): ResolvedModelRoleValue {
|
|
2309
|
+
return this.#resolveRoleModelFull(role, this.#modelRegistry.getAvailable(), this.model);
|
|
2107
2310
|
}
|
|
2108
2311
|
|
|
2109
2312
|
get promptTemplates(): ReadonlyArray<PromptTemplate> {
|
|
@@ -2154,7 +2357,7 @@ export class AgentSession {
|
|
|
2154
2357
|
throw error;
|
|
2155
2358
|
}
|
|
2156
2359
|
|
|
2157
|
-
const content =
|
|
2360
|
+
const content = prompt.render(planModeReferencePrompt, {
|
|
2158
2361
|
planFilePath,
|
|
2159
2362
|
planContent,
|
|
2160
2363
|
});
|
|
@@ -2191,7 +2394,7 @@ export class AgentSession {
|
|
|
2191
2394
|
: sessionPlanUrl;
|
|
2192
2395
|
|
|
2193
2396
|
const planExists = fs.existsSync(resolvedPlanPath);
|
|
2194
|
-
const content =
|
|
2397
|
+
const content = prompt.render(planModeActivePrompt, {
|
|
2195
2398
|
planFilePath: displayPlanPath,
|
|
2196
2399
|
planExists,
|
|
2197
2400
|
askToolName: "ask",
|
|
@@ -2263,7 +2466,10 @@ export class AgentSession {
|
|
|
2263
2466
|
return;
|
|
2264
2467
|
}
|
|
2265
2468
|
|
|
2266
|
-
|
|
2469
|
+
// Skip eager todo prelude when the user has already queued a directive
|
|
2470
|
+
const hasPendingUserDirective = this.#toolChoiceQueue.inspect().includes("user-force");
|
|
2471
|
+
const eagerTodoPrelude =
|
|
2472
|
+
!options?.synthetic && !hasPendingUserDirective ? this.#createEagerTodoPrelude(expandedText) : undefined;
|
|
2267
2473
|
|
|
2268
2474
|
const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
|
|
2269
2475
|
if (options?.images) {
|
|
@@ -2276,7 +2482,9 @@ export class AgentSession {
|
|
|
2276
2482
|
: { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
|
|
2277
2483
|
|
|
2278
2484
|
if (eagerTodoPrelude) {
|
|
2279
|
-
this.#
|
|
2485
|
+
this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
|
|
2486
|
+
label: "eager-todo",
|
|
2487
|
+
});
|
|
2280
2488
|
}
|
|
2281
2489
|
|
|
2282
2490
|
try {
|
|
@@ -2285,9 +2493,9 @@ export class AgentSession {
|
|
|
2285
2493
|
prependMessages: eagerTodoPrelude ? [eagerTodoPrelude.message] : undefined,
|
|
2286
2494
|
});
|
|
2287
2495
|
} finally {
|
|
2288
|
-
if
|
|
2289
|
-
|
|
2290
|
-
|
|
2496
|
+
// Clean up residual eager-todo directive if the prompt never consumed it
|
|
2497
|
+
// (e.g., compaction aborted, validation failed).
|
|
2498
|
+
this.#toolChoiceQueue.removeByLabel("eager-todo");
|
|
2291
2499
|
}
|
|
2292
2500
|
if (!options?.synthetic) {
|
|
2293
2501
|
await this.#enforcePlanModeToolDecision();
|
|
@@ -2651,6 +2859,10 @@ export class AgentSession {
|
|
|
2651
2859
|
});
|
|
2652
2860
|
}
|
|
2653
2861
|
|
|
2862
|
+
queueDeferredMessage(message: CustomMessage): void {
|
|
2863
|
+
this.#queueHiddenNextTurnMessage(message, true);
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2654
2866
|
#queueHiddenNextTurnMessage(message: CustomMessage, triggerTurn: boolean): void {
|
|
2655
2867
|
this.#pendingNextTurnMessages.push(message);
|
|
2656
2868
|
if (!triggerTurn) return;
|
|
@@ -3007,6 +3219,13 @@ export class AgentSession {
|
|
|
3007
3219
|
// block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
|
|
3008
3220
|
// a subsequent prompt() can incorrectly observe the session as busy after an abort.
|
|
3009
3221
|
this.#promptInFlightCount = 0;
|
|
3222
|
+
// Safety net: if the agent loop aborted without producing an assistant
|
|
3223
|
+
// message (e.g. failed before the first stream), the in-flight yield was
|
|
3224
|
+
// never resolved or rejected by the normal message_end path. Reject it now
|
|
3225
|
+
// so any requeue callback still fires and the queue stays consistent.
|
|
3226
|
+
if (this.#toolChoiceQueue.hasInFlight) {
|
|
3227
|
+
this.#toolChoiceQueue.reject("aborted");
|
|
3228
|
+
}
|
|
3010
3229
|
}
|
|
3011
3230
|
|
|
3012
3231
|
/**
|
|
@@ -3182,7 +3401,7 @@ export class AgentSession {
|
|
|
3182
3401
|
* Validates API key, saves to session log but NOT to settings.
|
|
3183
3402
|
* @throws Error if no API key available for the model
|
|
3184
3403
|
*/
|
|
3185
|
-
async setModelTemporary(model: Model): Promise<void> {
|
|
3404
|
+
async setModelTemporary(model: Model, thinkingLevel?: ThinkingLevel): Promise<void> {
|
|
3186
3405
|
const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
|
|
3187
3406
|
if (!apiKey) {
|
|
3188
3407
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
@@ -3193,8 +3412,8 @@ export class AgentSession {
|
|
|
3193
3412
|
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "temporary");
|
|
3194
3413
|
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
3195
3414
|
|
|
3196
|
-
//
|
|
3197
|
-
this.setThinkingLevel(this.thinkingLevel);
|
|
3415
|
+
// Apply explicit thinking level, or re-clamp current level to new model's capabilities
|
|
3416
|
+
this.setThinkingLevel(thinkingLevel ?? this.thinkingLevel);
|
|
3198
3417
|
}
|
|
3199
3418
|
|
|
3200
3419
|
/**
|
|
@@ -3267,13 +3486,12 @@ export class AgentSession {
|
|
|
3267
3486
|
const next = roleModels[nextIndex];
|
|
3268
3487
|
|
|
3269
3488
|
if (options?.temporary) {
|
|
3270
|
-
await this.setModelTemporary(next.model);
|
|
3489
|
+
await this.setModelTemporary(next.model, next.explicitThinkingLevel ? next.thinkingLevel : undefined);
|
|
3271
3490
|
} else {
|
|
3272
3491
|
await this.setModel(next.model, next.role);
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
this.setThinkingLevel(next.thinkingLevel);
|
|
3492
|
+
if (next.explicitThinkingLevel && next.thinkingLevel !== undefined) {
|
|
3493
|
+
this.setThinkingLevel(next.thinkingLevel);
|
|
3494
|
+
}
|
|
3277
3495
|
}
|
|
3278
3496
|
|
|
3279
3497
|
return { model: next.model, thinkingLevel: this.thinkingLevel, role: next.role };
|
|
@@ -3473,7 +3691,7 @@ export class AgentSession {
|
|
|
3473
3691
|
}
|
|
3474
3692
|
|
|
3475
3693
|
await this.sessionManager.rewriteEntries();
|
|
3476
|
-
const sessionContext = this.
|
|
3694
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
3477
3695
|
this.agent.replaceMessages(sessionContext.messages);
|
|
3478
3696
|
this.#syncTodoPhasesFromBranch();
|
|
3479
3697
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
@@ -3599,7 +3817,7 @@ export class AgentSession {
|
|
|
3599
3817
|
preserveData,
|
|
3600
3818
|
);
|
|
3601
3819
|
const newEntries = this.sessionManager.getEntries();
|
|
3602
|
-
const sessionContext = this.
|
|
3820
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
3603
3821
|
this.agent.replaceMessages(sessionContext.messages);
|
|
3604
3822
|
this.#syncTodoPhasesFromBranch();
|
|
3605
3823
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
@@ -3646,6 +3864,12 @@ export class AgentSession {
|
|
|
3646
3864
|
this.#handoffAbortController?.abort();
|
|
3647
3865
|
}
|
|
3648
3866
|
|
|
3867
|
+
/** Trigger idle compaction through the auto-compaction flow (with UI events). */
|
|
3868
|
+
async runIdleCompaction(): Promise<void> {
|
|
3869
|
+
if (this.isStreaming || this.isCompacting) return;
|
|
3870
|
+
await this.#runAutoCompaction("idle", false, true);
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3649
3873
|
/**
|
|
3650
3874
|
* Cancel in-progress branch summarization.
|
|
3651
3875
|
*/
|
|
@@ -3708,7 +3932,7 @@ export class AgentSession {
|
|
|
3708
3932
|
}
|
|
3709
3933
|
|
|
3710
3934
|
// Build the handoff prompt
|
|
3711
|
-
const handoffPrompt =
|
|
3935
|
+
const handoffPrompt = prompt.render(handoffDocumentPrompt, {
|
|
3712
3936
|
additionalFocus: customInstructions,
|
|
3713
3937
|
});
|
|
3714
3938
|
|
|
@@ -3814,7 +4038,7 @@ export class AgentSession {
|
|
|
3814
4038
|
}
|
|
3815
4039
|
|
|
3816
4040
|
// Rebuild agent messages from session
|
|
3817
|
-
const sessionContext = this.
|
|
4041
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
3818
4042
|
this.agent.replaceMessages(sessionContext.messages);
|
|
3819
4043
|
this.#syncTodoPhasesFromBranch();
|
|
3820
4044
|
|
|
@@ -3976,7 +4200,7 @@ export class AgentSession {
|
|
|
3976
4200
|
return;
|
|
3977
4201
|
}
|
|
3978
4202
|
|
|
3979
|
-
const reminder =
|
|
4203
|
+
const reminder = prompt.render(planModeToolDecisionReminderPrompt, {
|
|
3980
4204
|
askToolName: "ask",
|
|
3981
4205
|
exitToolName: "exit_plan_mode",
|
|
3982
4206
|
});
|
|
@@ -3988,7 +4212,7 @@ export class AgentSession {
|
|
|
3988
4212
|
});
|
|
3989
4213
|
}
|
|
3990
4214
|
|
|
3991
|
-
#createEagerTodoPrelude(): { message: AgentMessage; toolChoice: ToolChoice } | undefined {
|
|
4215
|
+
#createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice: ToolChoice } | undefined {
|
|
3992
4216
|
const eagerTodosEnabled = this.settings.get("todo.eager");
|
|
3993
4217
|
const todosEnabled = this.settings.get("todo.enabled");
|
|
3994
4218
|
if (!eagerTodosEnabled || !todosEnabled) {
|
|
@@ -4002,6 +4226,11 @@ export class AgentSession {
|
|
|
4002
4226
|
return undefined;
|
|
4003
4227
|
}
|
|
4004
4228
|
|
|
4229
|
+
const trimmedPromptText = promptText.trimEnd();
|
|
4230
|
+
if (trimmedPromptText.endsWith("?") || trimmedPromptText.endsWith("!")) {
|
|
4231
|
+
return undefined;
|
|
4232
|
+
}
|
|
4233
|
+
|
|
4005
4234
|
if (!this.#toolRegistry.has("todo_write")) {
|
|
4006
4235
|
logger.warn("Eager todo enforcement skipped because todo_write is unavailable", {
|
|
4007
4236
|
activeToolNames: this.agent.state.tools.map(tool => tool.name),
|
|
@@ -4018,7 +4247,7 @@ export class AgentSession {
|
|
|
4018
4247
|
return undefined;
|
|
4019
4248
|
}
|
|
4020
4249
|
|
|
4021
|
-
const eagerTodoReminder =
|
|
4250
|
+
const eagerTodoReminder = prompt.render(eagerTodoPrompt);
|
|
4022
4251
|
|
|
4023
4252
|
return {
|
|
4024
4253
|
message: {
|
|
@@ -4036,6 +4265,13 @@ export class AgentSession {
|
|
|
4036
4265
|
* Check if agent stopped with incomplete todos and prompt to continue.
|
|
4037
4266
|
*/
|
|
4038
4267
|
async #checkTodoCompletion(): Promise<void> {
|
|
4268
|
+
// Skip todo reminders when the most recent turn was driven by an explicit user force —
|
|
4269
|
+
// the user wanted exactly that tool, not a follow-up nag about incomplete todos.
|
|
4270
|
+
const lastServedLabel = this.#toolChoiceQueue.consumeLastServedLabel();
|
|
4271
|
+
if (lastServedLabel === "user-force") {
|
|
4272
|
+
return;
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4039
4275
|
const remindersEnabled = this.settings.get("todo.reminders");
|
|
4040
4276
|
const todosEnabled = this.settings.get("todo.enabled");
|
|
4041
4277
|
if (!remindersEnabled || !todosEnabled) {
|
|
@@ -4364,19 +4600,25 @@ export class AgentSession {
|
|
|
4364
4600
|
return availableModels.find(m => m.provider === currentModel.provider && m.id === configuredTarget);
|
|
4365
4601
|
}
|
|
4366
4602
|
|
|
4367
|
-
#
|
|
4603
|
+
#resolveRoleModelFull(
|
|
4604
|
+
role: string,
|
|
4605
|
+
availableModels: Model[],
|
|
4606
|
+
currentModel: Model | undefined,
|
|
4607
|
+
): ResolvedModelRoleValue {
|
|
4368
4608
|
const roleModelStr =
|
|
4369
4609
|
role === "default"
|
|
4370
4610
|
? (this.settings.getModelRole("default") ??
|
|
4371
4611
|
(currentModel ? `${currentModel.provider}/${currentModel.id}` : undefined))
|
|
4372
4612
|
: this.settings.getModelRole(role);
|
|
4373
4613
|
|
|
4374
|
-
if (!roleModelStr)
|
|
4614
|
+
if (!roleModelStr) {
|
|
4615
|
+
return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
|
|
4616
|
+
}
|
|
4375
4617
|
|
|
4376
4618
|
return resolveModelRoleValue(roleModelStr, availableModels, {
|
|
4377
4619
|
settings: this.settings,
|
|
4378
4620
|
matchPreferences: { usageOrder: this.settings.getStorage()?.getModelUsageOrder() },
|
|
4379
|
-
})
|
|
4621
|
+
});
|
|
4380
4622
|
}
|
|
4381
4623
|
|
|
4382
4624
|
#getCompactionModelCandidates(availableModels: Model[]): Model[] {
|
|
@@ -4393,7 +4635,7 @@ export class AgentSession {
|
|
|
4393
4635
|
|
|
4394
4636
|
const currentModel = this.model;
|
|
4395
4637
|
for (const role of MODEL_ROLE_IDS) {
|
|
4396
|
-
addCandidate(this.#
|
|
4638
|
+
addCandidate(this.#resolveRoleModelFull(role, availableModels, currentModel).model);
|
|
4397
4639
|
}
|
|
4398
4640
|
|
|
4399
4641
|
const sortedByContext = [...availableModels].sort((a, b) => b.contextWindow - a.contextWindow);
|
|
@@ -4410,11 +4652,16 @@ export class AgentSession {
|
|
|
4410
4652
|
/**
|
|
4411
4653
|
* Internal: Run auto-compaction with events.
|
|
4412
4654
|
*/
|
|
4413
|
-
async #runAutoCompaction(
|
|
4655
|
+
async #runAutoCompaction(
|
|
4656
|
+
reason: "overflow" | "threshold" | "idle",
|
|
4657
|
+
willRetry: boolean,
|
|
4658
|
+
deferred = false,
|
|
4659
|
+
): Promise<void> {
|
|
4414
4660
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
4415
|
-
if (
|
|
4661
|
+
if (compactionSettings.strategy === "off") return;
|
|
4662
|
+
if (reason !== "idle" && !compactionSettings.enabled) return;
|
|
4416
4663
|
const generation = this.#promptGeneration;
|
|
4417
|
-
if (!deferred && reason !== "overflow" && compactionSettings.strategy === "handoff") {
|
|
4664
|
+
if (!deferred && reason !== "overflow" && reason !== "idle" && compactionSettings.strategy === "handoff") {
|
|
4418
4665
|
this.#schedulePostPromptTask(
|
|
4419
4666
|
async signal => {
|
|
4420
4667
|
await Promise.resolve();
|
|
@@ -4689,7 +4936,7 @@ export class AgentSession {
|
|
|
4689
4936
|
preserveData,
|
|
4690
4937
|
);
|
|
4691
4938
|
const newEntries = this.sessionManager.getEntries();
|
|
4692
|
-
const sessionContext = this.
|
|
4939
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
4693
4940
|
this.agent.replaceMessages(sessionContext.messages);
|
|
4694
4941
|
this.#syncTodoPhasesFromBranch();
|
|
4695
4942
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
@@ -4717,7 +4964,7 @@ export class AgentSession {
|
|
|
4717
4964
|
};
|
|
4718
4965
|
await this.#emitSessionEvent({ type: "auto_compaction_end", action, result, aborted: false, willRetry });
|
|
4719
4966
|
|
|
4720
|
-
if (!willRetry && compactionSettings.autoContinue !== false) {
|
|
4967
|
+
if (!willRetry && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
4721
4968
|
const continuePrompt = async () => {
|
|
4722
4969
|
await this.#promptWithMessage(
|
|
4723
4970
|
{
|
|
@@ -4823,6 +5070,17 @@ export class AgentSession {
|
|
|
4823
5070
|
}
|
|
4824
5071
|
|
|
4825
5072
|
#isTransientErrorMessage(errorMessage: string): boolean {
|
|
5073
|
+
return (
|
|
5074
|
+
this.#isTransientEnvelopeErrorMessage(errorMessage) || this.#isTransientTransportErrorMessage(errorMessage)
|
|
5075
|
+
);
|
|
5076
|
+
}
|
|
5077
|
+
|
|
5078
|
+
#isTransientEnvelopeErrorMessage(errorMessage: string): boolean {
|
|
5079
|
+
// Match Anthropic stream-envelope failures that indicate a broken stream before any content starts.
|
|
5080
|
+
return /anthropic stream envelope error:/i.test(errorMessage) && /before message_start/i.test(errorMessage);
|
|
5081
|
+
}
|
|
5082
|
+
|
|
5083
|
+
#isTransientTransportErrorMessage(errorMessage: string): boolean {
|
|
4826
5084
|
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
|
|
4827
5085
|
// service unavailable, network/connection errors, fetch failed, terminated, retry delay exceeded
|
|
4828
5086
|
return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall/i.test(
|
|
@@ -5516,7 +5774,7 @@ export class AgentSession {
|
|
|
5516
5774
|
// Flush pending writes before switching so restore snapshots reflect committed state.
|
|
5517
5775
|
await this.sessionManager.flush();
|
|
5518
5776
|
const previousSessionState = this.sessionManager.captureState();
|
|
5519
|
-
const previousSessionContext = this.
|
|
5777
|
+
const previousSessionContext = this.buildDisplaySessionContext();
|
|
5520
5778
|
// switchSession replaces these arrays wholesale during load/rollback, so retaining
|
|
5521
5779
|
// the existing message objects is sufficient and avoids structured-clone failures for
|
|
5522
5780
|
// extension/custom metadata that is valid to persist but not cloneable.
|
|
@@ -5545,7 +5803,7 @@ export class AgentSession {
|
|
|
5545
5803
|
await this.sessionManager.setSessionFile(sessionPath);
|
|
5546
5804
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
5547
5805
|
|
|
5548
|
-
const sessionContext = this.
|
|
5806
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
5549
5807
|
const didReloadConversationChange =
|
|
5550
5808
|
!switchingToDifferentSession &&
|
|
5551
5809
|
this.#didSessionMessagesChange(previousSessionContext.messages, sessionContext.messages);
|
|
@@ -5711,7 +5969,7 @@ export class AgentSession {
|
|
|
5711
5969
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
5712
5970
|
|
|
5713
5971
|
// Reload messages from entries (works for both file and in-memory mode)
|
|
5714
|
-
const sessionContext = this.
|
|
5972
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
5715
5973
|
|
|
5716
5974
|
await this.#restoreMCPSelectionsForSessionContext(sessionContext);
|
|
5717
5975
|
|
|
@@ -5882,7 +6140,7 @@ export class AgentSession {
|
|
|
5882
6140
|
}
|
|
5883
6141
|
|
|
5884
6142
|
// Update agent state
|
|
5885
|
-
const sessionContext = this.
|
|
6143
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
5886
6144
|
await this.#restoreMCPSelectionsForSessionContext(sessionContext);
|
|
5887
6145
|
this.agent.replaceMessages(sessionContext.messages);
|
|
5888
6146
|
this.#syncTodoPhasesFromBranch();
|
|
@@ -6156,176 +6414,13 @@ export class AgentSession {
|
|
|
6156
6414
|
* Includes user messages, assistant text, thinking blocks, tool calls, and tool results.
|
|
6157
6415
|
*/
|
|
6158
6416
|
formatSessionAsText(): string {
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
6167
|
-
parts.push(`${indent}<parameter name="${key}">${text}</parameter>`);
|
|
6168
|
-
}
|
|
6169
|
-
return parts.join("\n");
|
|
6170
|
-
}
|
|
6171
|
-
|
|
6172
|
-
// Include system prompt at the beginning
|
|
6173
|
-
const systemPrompt = this.agent.state.systemPrompt;
|
|
6174
|
-
if (systemPrompt) {
|
|
6175
|
-
lines.push("## System Prompt\n");
|
|
6176
|
-
lines.push(systemPrompt);
|
|
6177
|
-
lines.push("\n");
|
|
6178
|
-
}
|
|
6179
|
-
|
|
6180
|
-
// Include model and thinking level
|
|
6181
|
-
const model = this.agent.state.model;
|
|
6182
|
-
const thinkingLevel = this.#thinkingLevel;
|
|
6183
|
-
lines.push("## Configuration\n");
|
|
6184
|
-
lines.push(`Model: ${model ? `${model.provider}/${model.id}` : "(not selected)"}`);
|
|
6185
|
-
lines.push(`Thinking Level: ${thinkingLevel}`);
|
|
6186
|
-
lines.push("\n");
|
|
6187
|
-
|
|
6188
|
-
// Include available tools
|
|
6189
|
-
const tools = this.agent.state.tools;
|
|
6190
|
-
|
|
6191
|
-
// Recursively strip all fields starting with 'TypeBox.' from an object
|
|
6192
|
-
function stripTypeBoxFields(obj: any): any {
|
|
6193
|
-
if (Array.isArray(obj)) {
|
|
6194
|
-
return obj.map(stripTypeBoxFields);
|
|
6195
|
-
}
|
|
6196
|
-
if (obj && typeof obj === "object") {
|
|
6197
|
-
const result: Record<string, any> = {};
|
|
6198
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
6199
|
-
if (!k.startsWith("TypeBox.")) {
|
|
6200
|
-
result[k] = stripTypeBoxFields(v);
|
|
6201
|
-
}
|
|
6202
|
-
}
|
|
6203
|
-
return result;
|
|
6204
|
-
}
|
|
6205
|
-
return obj;
|
|
6206
|
-
}
|
|
6207
|
-
|
|
6208
|
-
if (tools.length > 0) {
|
|
6209
|
-
lines.push("## Available Tools\n");
|
|
6210
|
-
for (const tool of tools) {
|
|
6211
|
-
lines.push(`<tool name="${tool.name}">`);
|
|
6212
|
-
lines.push(tool.description);
|
|
6213
|
-
const parametersClean = stripTypeBoxFields(tool.parameters);
|
|
6214
|
-
lines.push(`\nParameters:\n${formatArgsAsXml(parametersClean as Record<string, unknown>)}`);
|
|
6215
|
-
lines.push("<" + "/tool>\n");
|
|
6216
|
-
}
|
|
6217
|
-
lines.push("\n");
|
|
6218
|
-
}
|
|
6219
|
-
|
|
6220
|
-
for (const msg of this.messages) {
|
|
6221
|
-
if (msg.role === "user" || msg.role === "developer") {
|
|
6222
|
-
lines.push(msg.role === "developer" ? "## Developer\n" : "## User\n");
|
|
6223
|
-
if (typeof msg.content === "string") {
|
|
6224
|
-
lines.push(msg.content);
|
|
6225
|
-
} else {
|
|
6226
|
-
for (const c of msg.content) {
|
|
6227
|
-
if (c.type === "text") {
|
|
6228
|
-
lines.push(c.text);
|
|
6229
|
-
} else if (c.type === "image") {
|
|
6230
|
-
lines.push("[Image]");
|
|
6231
|
-
}
|
|
6232
|
-
}
|
|
6233
|
-
}
|
|
6234
|
-
lines.push("\n");
|
|
6235
|
-
} else if (msg.role === "assistant") {
|
|
6236
|
-
const assistantMsg = msg as AssistantMessage;
|
|
6237
|
-
lines.push("## Assistant\n");
|
|
6238
|
-
|
|
6239
|
-
for (const c of assistantMsg.content) {
|
|
6240
|
-
if (c.type === "text") {
|
|
6241
|
-
lines.push(c.text);
|
|
6242
|
-
} else if (c.type === "thinking") {
|
|
6243
|
-
lines.push("<thinking>");
|
|
6244
|
-
lines.push(c.thinking);
|
|
6245
|
-
lines.push("</thinking>\n");
|
|
6246
|
-
} else if (c.type === "toolCall") {
|
|
6247
|
-
lines.push(`<invoke name="${c.name}">`);
|
|
6248
|
-
if (c.arguments && typeof c.arguments === "object") {
|
|
6249
|
-
lines.push(formatArgsAsXml(c.arguments as Record<string, unknown>));
|
|
6250
|
-
}
|
|
6251
|
-
lines.push("<" + "/invoke>\n");
|
|
6252
|
-
}
|
|
6253
|
-
}
|
|
6254
|
-
lines.push("");
|
|
6255
|
-
} else if (msg.role === "toolResult") {
|
|
6256
|
-
lines.push(`### Tool Result: ${msg.toolName}`);
|
|
6257
|
-
if (msg.isError) {
|
|
6258
|
-
lines.push("(error)");
|
|
6259
|
-
}
|
|
6260
|
-
for (const c of msg.content) {
|
|
6261
|
-
if (c.type === "text") {
|
|
6262
|
-
lines.push("```");
|
|
6263
|
-
lines.push(c.text);
|
|
6264
|
-
lines.push("```");
|
|
6265
|
-
} else if (c.type === "image") {
|
|
6266
|
-
lines.push("[Image output]");
|
|
6267
|
-
}
|
|
6268
|
-
}
|
|
6269
|
-
lines.push("");
|
|
6270
|
-
} else if (msg.role === "bashExecution") {
|
|
6271
|
-
const bashMsg = msg as BashExecutionMessage;
|
|
6272
|
-
if (!bashMsg.excludeFromContext) {
|
|
6273
|
-
lines.push("## Bash Execution\n");
|
|
6274
|
-
lines.push(bashExecutionToText(bashMsg));
|
|
6275
|
-
lines.push("\n");
|
|
6276
|
-
}
|
|
6277
|
-
} else if (msg.role === "pythonExecution") {
|
|
6278
|
-
const pythonMsg = msg as PythonExecutionMessage;
|
|
6279
|
-
if (!pythonMsg.excludeFromContext) {
|
|
6280
|
-
lines.push("## Python Execution\n");
|
|
6281
|
-
lines.push(pythonExecutionToText(pythonMsg));
|
|
6282
|
-
lines.push("\n");
|
|
6283
|
-
}
|
|
6284
|
-
} else if (msg.role === "custom" || msg.role === "hookMessage") {
|
|
6285
|
-
const customMsg = msg as CustomMessage | HookMessage;
|
|
6286
|
-
lines.push(`## ${customMsg.customType}\n`);
|
|
6287
|
-
if (typeof customMsg.content === "string") {
|
|
6288
|
-
lines.push(customMsg.content);
|
|
6289
|
-
} else {
|
|
6290
|
-
for (const c of customMsg.content) {
|
|
6291
|
-
if (c.type === "text") {
|
|
6292
|
-
lines.push(c.text);
|
|
6293
|
-
} else if (c.type === "image") {
|
|
6294
|
-
lines.push("[Image]");
|
|
6295
|
-
}
|
|
6296
|
-
}
|
|
6297
|
-
}
|
|
6298
|
-
lines.push("\n");
|
|
6299
|
-
} else if (msg.role === "branchSummary") {
|
|
6300
|
-
const branchMsg = msg as BranchSummaryMessage;
|
|
6301
|
-
lines.push("## Branch Summary\n");
|
|
6302
|
-
lines.push(`(from branch: ${branchMsg.fromId})\n`);
|
|
6303
|
-
lines.push(branchMsg.summary);
|
|
6304
|
-
lines.push("\n");
|
|
6305
|
-
} else if (msg.role === "compactionSummary") {
|
|
6306
|
-
const compactMsg = msg as CompactionSummaryMessage;
|
|
6307
|
-
lines.push("## Compaction Summary\n");
|
|
6308
|
-
lines.push(`(${compactMsg.tokensBefore} tokens before compaction)\n`);
|
|
6309
|
-
lines.push(compactMsg.summary);
|
|
6310
|
-
lines.push("\n");
|
|
6311
|
-
} else if (msg.role === "fileMention") {
|
|
6312
|
-
const fileMsg = msg as FileMentionMessage;
|
|
6313
|
-
lines.push("## File Mention\n");
|
|
6314
|
-
for (const file of fileMsg.files) {
|
|
6315
|
-
lines.push(`<file path="${file.path}">`);
|
|
6316
|
-
if (file.content) {
|
|
6317
|
-
lines.push(file.content);
|
|
6318
|
-
}
|
|
6319
|
-
if (file.image) {
|
|
6320
|
-
lines.push("[Image attached]");
|
|
6321
|
-
}
|
|
6322
|
-
lines.push("</file>\n");
|
|
6323
|
-
}
|
|
6324
|
-
lines.push("\n");
|
|
6325
|
-
}
|
|
6326
|
-
}
|
|
6327
|
-
|
|
6328
|
-
return lines.join("\n").trim();
|
|
6417
|
+
return formatSessionDumpText({
|
|
6418
|
+
messages: this.messages,
|
|
6419
|
+
systemPrompt: this.agent.state.systemPrompt,
|
|
6420
|
+
model: this.agent.state.model,
|
|
6421
|
+
thinkingLevel: this.#thinkingLevel,
|
|
6422
|
+
tools: this.agent.state.tools,
|
|
6423
|
+
});
|
|
6329
6424
|
}
|
|
6330
6425
|
|
|
6331
6426
|
/**
|