@oh-my-pi/pi-coding-agent 13.19.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 +266 -1
- package/package.json +86 -20
- 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 +91 -0
- 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 +83 -125
- 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 -5
- package/src/commit/agentic/index.ts +3 -4
- package/src/commit/agentic/tools/analyze-file.ts +3 -3
- 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/map-reduce/map-phase.ts +4 -4
- package/src/commit/map-reduce/reduce-phase.ts +4 -4
- package/src/commit/pipeline.ts +3 -4
- package/src/config/prompt-templates.ts +44 -226
- package/src/config/resolve-config-value.ts +4 -2
- package/src/config/settings-schema.ts +54 -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 +2 -2
- package/src/extensibility/custom-commands/bundled/review/index.ts +2 -2
- package/src/extensibility/custom-commands/loader.ts +1 -2
- package/src/extensibility/custom-tools/loader.ts +34 -11
- package/src/extensibility/extensions/loader.ts +9 -4
- package/src/extensibility/extensions/runner.ts +24 -1
- package/src/extensibility/extensions/types.ts +1 -1
- package/src/extensibility/hooks/loader.ts +5 -6
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/extensibility/plugins/doctor.ts +2 -1
- package/src/extensibility/slash-commands.ts +3 -7
- package/src/index.ts +2 -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 +102 -46
- 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/read-tool-group.ts +6 -12
- package/src/modes/components/settings-defs.ts +5 -0
- 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 +3 -2
- package/src/modes/controllers/input-controller.ts +12 -8
- package/src/modes/index.ts +20 -2
- package/src/modes/interactive-mode.ts +94 -37
- 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/theme/theme.ts +80 -8
- package/src/modes/types.ts +2 -2
- package/src/prompts/system/system-prompt.md +2 -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 +190 -154
- package/src/secrets/obfuscator.ts +1 -1
- package/src/session/agent-session.ts +306 -256
- 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-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 +21 -13
- package/src/task/agents.ts +5 -6
- package/src/task/commands.ts +2 -5
- package/src/task/executor.ts +4 -4
- package/src/task/index.ts +3 -4
- package/src/task/template.ts +2 -2
- package/src/task/worktree.ts +4 -4
- 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 +2 -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 +10 -11
- 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 +198 -67
- 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/file-display-mode.ts +6 -5
- package/src/utils/file-mentions.ts +8 -7
- package/src/utils/git.ts +4 -4
- 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/autoresearch/command-initialize.md +0 -34
- 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/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";
|
|
@@ -62,8 +61,9 @@ import {
|
|
|
62
61
|
type ResolvedModelRoleValue,
|
|
63
62
|
resolveModelRoleValue,
|
|
64
63
|
} from "../config/model-resolver";
|
|
65
|
-
import { expandPromptTemplate, type PromptTemplate
|
|
64
|
+
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
66
65
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
66
|
+
import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
67
67
|
import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
|
|
68
68
|
import { exportSessionToHtml } from "../export/html";
|
|
69
69
|
import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
|
|
@@ -104,7 +104,6 @@ import {
|
|
|
104
104
|
selectDiscoverableMCPToolNamesByServer,
|
|
105
105
|
} from "../mcp/discoverable-tool-metadata";
|
|
106
106
|
import { getCurrentThemeName, theme } from "../modes/theme/theme";
|
|
107
|
-
import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../patch";
|
|
108
107
|
import type { PlanModeState } from "../plan-mode/state";
|
|
109
108
|
import autoHandoffThresholdFocusPrompt from "../prompts/system/auto-handoff-threshold-focus.md" with { type: "text" };
|
|
110
109
|
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
@@ -117,11 +116,13 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
|
|
|
117
116
|
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
|
118
117
|
import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
|
|
119
118
|
import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
|
119
|
+
import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
120
120
|
import type { CheckpointState } from "../tools/checkpoint";
|
|
121
121
|
import { outputMeta } from "../tools/output-meta";
|
|
122
122
|
import { resolveToCwd } from "../tools/path-utils";
|
|
123
|
-
import
|
|
123
|
+
import { isAutoQaEnabled } from "../tools/report-tool-issue";
|
|
124
124
|
import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
|
|
125
|
+
import { ToolError } from "../tools/tool-errors";
|
|
125
126
|
import { clampTimeout } from "../tools/tool-timeouts";
|
|
126
127
|
import { parseCommandArgs } from "../utils/command-args";
|
|
127
128
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
@@ -141,16 +142,13 @@ import {
|
|
|
141
142
|
import { DEFAULT_PRUNE_CONFIG, pruneToolOutputs } from "./compaction/pruning";
|
|
142
143
|
import {
|
|
143
144
|
type BashExecutionMessage,
|
|
144
|
-
type BranchSummaryMessage,
|
|
145
|
-
bashExecutionToText,
|
|
146
145
|
type CompactionSummaryMessage,
|
|
147
146
|
type CustomMessage,
|
|
148
147
|
convertToLlm,
|
|
149
148
|
type FileMentionMessage,
|
|
150
|
-
type HookMessage,
|
|
151
149
|
type PythonExecutionMessage,
|
|
152
|
-
pythonExecutionToText,
|
|
153
150
|
} from "./messages";
|
|
151
|
+
import { formatSessionDumpText } from "./session-dump-format";
|
|
154
152
|
import type {
|
|
155
153
|
BranchSummaryEntry,
|
|
156
154
|
CompactionEntry,
|
|
@@ -159,6 +157,7 @@ import type {
|
|
|
159
157
|
SessionManager,
|
|
160
158
|
} from "./session-manager";
|
|
161
159
|
import { getLatestCompactionEntry } from "./session-manager";
|
|
160
|
+
import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
162
161
|
|
|
163
162
|
/** Session-specific events that extend the core AgentEvent */
|
|
164
163
|
export type AgentSessionEvent =
|
|
@@ -244,8 +243,6 @@ export interface AgentSessionConfig {
|
|
|
244
243
|
ttsrManager?: TtsrManager;
|
|
245
244
|
/** Secret obfuscator for deobfuscating streaming edit content */
|
|
246
245
|
obfuscator?: SecretObfuscator;
|
|
247
|
-
/** Pending action store for preview/apply workflows */
|
|
248
|
-
pendingActionStore?: PendingActionStore;
|
|
249
246
|
/** Shared native search DB for grep/glob/fuzzyFind-backed workflows. */
|
|
250
247
|
searchDb?: SearchDb;
|
|
251
248
|
}
|
|
@@ -321,7 +318,7 @@ interface HandoffOptions {
|
|
|
321
318
|
|
|
322
319
|
/** Standard thinking levels */
|
|
323
320
|
|
|
324
|
-
const AUTO_HANDOFF_THRESHOLD_FOCUS =
|
|
321
|
+
const AUTO_HANDOFF_THRESHOLD_FOCUS = prompt.render(autoHandoffThresholdFocusPrompt);
|
|
325
322
|
|
|
326
323
|
type RetryFallbackChains = Record<string, string[]>;
|
|
327
324
|
|
|
@@ -400,6 +397,9 @@ export class AgentSession {
|
|
|
400
397
|
readonly sessionManager: SessionManager;
|
|
401
398
|
readonly settings: Settings;
|
|
402
399
|
readonly searchDb: SearchDb | undefined;
|
|
400
|
+
|
|
401
|
+
#powerAssertion: MacOSPowerAssertion | undefined;
|
|
402
|
+
|
|
403
403
|
readonly configWarnings: string[] = [];
|
|
404
404
|
|
|
405
405
|
#asyncJobManager: AsyncJobManager | undefined = undefined;
|
|
@@ -410,7 +410,6 @@ export class AgentSession {
|
|
|
410
410
|
|
|
411
411
|
// Event subscription state
|
|
412
412
|
#unsubscribeAgent?: () => void;
|
|
413
|
-
#unsubscribePendingActionPush?: () => void;
|
|
414
413
|
#eventListeners: AgentSessionEventListener[] = [];
|
|
415
414
|
|
|
416
415
|
/** Tracks pending steering messages for UI display. Removed when delivered. */
|
|
@@ -445,7 +444,7 @@ export class AgentSession {
|
|
|
445
444
|
#todoReminderCount = 0;
|
|
446
445
|
#todoPhases: TodoPhase[] = [];
|
|
447
446
|
#todoClearTimers = new Map<string, Timer>();
|
|
448
|
-
#
|
|
447
|
+
#toolChoiceQueue = new ToolChoiceQueue();
|
|
449
448
|
|
|
450
449
|
// Bash execution state
|
|
451
450
|
#bashAbortController: AbortController | undefined = undefined;
|
|
@@ -483,6 +482,7 @@ export class AgentSession {
|
|
|
483
482
|
#discoverableMCPTools = new Map<string, DiscoverableMCPTool>();
|
|
484
483
|
#discoverableMCPSearchIndex: DiscoverableMCPSearchIndex | null = null;
|
|
485
484
|
#selectedMCPToolNames = new Set<string>();
|
|
485
|
+
#rpcHostToolNames = new Set<string>();
|
|
486
486
|
#defaultSelectedMCPServerNames = new Set<string>();
|
|
487
487
|
#defaultSelectedMCPToolNames = new Set<string>();
|
|
488
488
|
#sessionDefaultSelectedMCPToolNames = new Map<string, string[]>();
|
|
@@ -502,20 +502,49 @@ export class AgentSession {
|
|
|
502
502
|
|
|
503
503
|
#streamingEditAbortTriggered = false;
|
|
504
504
|
#streamingEditCheckedLineCounts = new Map<string, number>();
|
|
505
|
+
|
|
506
|
+
#streamingEditPrecheckedToolCallIds = new Set<string>();
|
|
507
|
+
|
|
505
508
|
#streamingEditFileCache = new Map<string, string>();
|
|
506
509
|
#promptInFlightCount = 0;
|
|
507
510
|
#obfuscator: SecretObfuscator | undefined;
|
|
508
|
-
#pendingActionStore: PendingActionStore | undefined;
|
|
509
511
|
#checkpointState: CheckpointState | undefined = undefined;
|
|
510
512
|
#pendingRewindReport: string | undefined = undefined;
|
|
511
513
|
#promptGeneration = 0;
|
|
512
514
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
513
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
|
+
|
|
514
540
|
constructor(config: AgentSessionConfig) {
|
|
541
|
+
setNativeKillTree(killTree);
|
|
542
|
+
|
|
515
543
|
this.agent = config.agent;
|
|
516
544
|
this.sessionManager = config.sessionManager;
|
|
517
545
|
this.settings = config.settings;
|
|
518
546
|
this.searchDb = config.searchDb;
|
|
547
|
+
this.#startPowerAssertion();
|
|
519
548
|
this.#asyncJobManager = config.asyncJobManager;
|
|
520
549
|
this.#scopedModels = config.scopedModels ?? [];
|
|
521
550
|
this.#thinkingLevel = config.thinkingLevel;
|
|
@@ -557,24 +586,16 @@ export class AgentSession {
|
|
|
557
586
|
);
|
|
558
587
|
this.#ttsrManager = config.ttsrManager;
|
|
559
588
|
this.#obfuscator = config.obfuscator;
|
|
560
|
-
this.agent.
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
this.agent.steer({
|
|
569
|
-
role: "custom",
|
|
570
|
-
customType: "resolve-reminder",
|
|
571
|
-
content: reminderText,
|
|
572
|
-
display: false,
|
|
573
|
-
details: { toolName: action.sourceToolName },
|
|
574
|
-
attribution: "agent",
|
|
575
|
-
timestamp: Date.now(),
|
|
576
|
-
});
|
|
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);
|
|
577
597
|
});
|
|
598
|
+
this.agent.providerSessionState = this.#providerSessionState;
|
|
578
599
|
this.#syncTodoPhasesFromBranch();
|
|
579
600
|
|
|
580
601
|
// Always subscribe to agent events for internal handling
|
|
@@ -587,10 +608,40 @@ export class AgentSession {
|
|
|
587
608
|
return this.#modelRegistry;
|
|
588
609
|
}
|
|
589
610
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
this.#
|
|
593
|
-
|
|
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();
|
|
594
645
|
}
|
|
595
646
|
|
|
596
647
|
/** Provider-scoped mutable state store for transport/session caches. */
|
|
@@ -696,6 +747,17 @@ export class AgentSession {
|
|
|
696
747
|
if (event.type === "turn_end" && this.#ttsrManager) {
|
|
697
748
|
this.#ttsrManager.incrementMessageCount();
|
|
698
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
|
+
}
|
|
699
761
|
if (event.type === "turn_end" && this.#pendingRewindReport) {
|
|
700
762
|
const report = this.#pendingRewindReport;
|
|
701
763
|
this.#pendingRewindReport = undefined;
|
|
@@ -794,8 +856,13 @@ export class AgentSession {
|
|
|
794
856
|
}
|
|
795
857
|
}
|
|
796
858
|
|
|
797
|
-
if (
|
|
798
|
-
|
|
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);
|
|
799
866
|
}
|
|
800
867
|
|
|
801
868
|
if (
|
|
@@ -1113,7 +1180,7 @@ export class AgentSession {
|
|
|
1113
1180
|
if (this.#pendingTtsrInjections.length === 0) return undefined;
|
|
1114
1181
|
const rules = this.#pendingTtsrInjections;
|
|
1115
1182
|
const content = rules
|
|
1116
|
-
.map(r =>
|
|
1183
|
+
.map(r => prompt.render(ttsrInterruptTemplate, { name: r.name, path: r.path, content: r.content }))
|
|
1117
1184
|
.join("\n\n");
|
|
1118
1185
|
this.#pendingTtsrInjections = [];
|
|
1119
1186
|
return { content, rules };
|
|
@@ -1326,31 +1393,101 @@ export class AgentSession {
|
|
|
1326
1393
|
#resetStreamingEditState(): void {
|
|
1327
1394
|
this.#streamingEditAbortTriggered = false;
|
|
1328
1395
|
this.#streamingEditCheckedLineCounts.clear();
|
|
1396
|
+
this.#streamingEditPrecheckedToolCallIds.clear();
|
|
1329
1397
|
this.#streamingEditFileCache.clear();
|
|
1330
1398
|
}
|
|
1331
1399
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
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;
|
|
1340
1414
|
const messageContent = event.message.content;
|
|
1341
|
-
if (!Array.isArray(messageContent) || contentIndex >= messageContent.length)
|
|
1415
|
+
if (!Array.isArray(messageContent) || contentIndex < 0 || contentIndex >= messageContent.length) {
|
|
1416
|
+
return undefined;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1342
1419
|
const toolCall = messageContent[contentIndex] as ToolCall;
|
|
1343
|
-
if (toolCall.name !== "edit") return;
|
|
1420
|
+
if (toolCall.name !== "edit") return undefined;
|
|
1344
1421
|
|
|
1345
1422
|
const args = toolCall.arguments;
|
|
1346
|
-
if (!args || typeof args !== "object" || Array.isArray(args)) return;
|
|
1347
|
-
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;
|
|
1348
1425
|
|
|
1349
1426
|
const path = typeof args.path === "string" ? args.path : undefined;
|
|
1350
|
-
if (!path) return;
|
|
1427
|
+
if (!path) return undefined;
|
|
1351
1428
|
|
|
1352
|
-
|
|
1353
|
-
|
|
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);
|
|
1354
1491
|
}
|
|
1355
1492
|
|
|
1356
1493
|
#ensureFileCache(resolvedPath: string): void {
|
|
@@ -1375,24 +1512,15 @@ export class AgentSession {
|
|
|
1375
1512
|
if (!this.settings.get("edit.streamingAbort")) return;
|
|
1376
1513
|
if (this.#streamingEditAbortTriggered) return;
|
|
1377
1514
|
if (event.type !== "message_update") return;
|
|
1515
|
+
|
|
1378
1516
|
const assistantEvent = event.assistantMessageEvent;
|
|
1379
1517
|
if (assistantEvent.type !== "toolcall_end" && assistantEvent.type !== "toolcall_delta") return;
|
|
1380
|
-
if (event.message.role !== "assistant") return;
|
|
1381
1518
|
|
|
1382
|
-
const
|
|
1383
|
-
|
|
1384
|
-
if (!Array.isArray(messageContent) || contentIndex >= messageContent.length) return;
|
|
1385
|
-
const toolCall = messageContent[contentIndex] as ToolCall;
|
|
1386
|
-
if (toolCall.name !== "edit" || !toolCall.id) return;
|
|
1387
|
-
|
|
1388
|
-
const args = toolCall.arguments;
|
|
1389
|
-
if (!args || typeof args !== "object" || Array.isArray(args)) return;
|
|
1390
|
-
if ("old_text" in args || "new_text" in args) return;
|
|
1519
|
+
const streamingEdit = this.#getStreamingEditToolCall(event);
|
|
1520
|
+
if (!streamingEdit?.toolCall.id) return;
|
|
1391
1521
|
|
|
1392
|
-
const
|
|
1393
|
-
|
|
1394
|
-
const op = typeof args.op === "string" ? args.op : undefined;
|
|
1395
|
-
if (!path || !diff) return;
|
|
1522
|
+
const { toolCall, path, resolvedPath, diff, op, rename } = streamingEdit;
|
|
1523
|
+
if (!diff) return;
|
|
1396
1524
|
if (op && op !== "update") return;
|
|
1397
1525
|
|
|
1398
1526
|
if (!diff.includes("\n")) return;
|
|
@@ -1415,13 +1543,10 @@ export class AgentSession {
|
|
|
1415
1543
|
if (lastChecked !== undefined && lineCount <= lastChecked) return;
|
|
1416
1544
|
this.#streamingEditCheckedLineCounts.set(toolCall.id, lineCount);
|
|
1417
1545
|
|
|
1418
|
-
const rename = typeof args.rename === "string" ? args.rename : undefined;
|
|
1419
|
-
|
|
1420
1546
|
const removedLines = lines
|
|
1421
1547
|
.filter(line => line.startsWith("-") && !line.startsWith("--- "))
|
|
1422
1548
|
.map(line => line.slice(1));
|
|
1423
1549
|
if (removedLines.length > 0) {
|
|
1424
|
-
const resolvedPath = resolveToCwd(path, this.sessionManager.getCwd());
|
|
1425
1550
|
let cachedContent = this.#streamingEditFileCache.get(resolvedPath);
|
|
1426
1551
|
if (cachedContent === undefined) {
|
|
1427
1552
|
this.#ensureFileCache(resolvedPath);
|
|
@@ -1511,7 +1636,6 @@ export class AgentSession {
|
|
|
1511
1636
|
if (!this.#extensionRunner) return;
|
|
1512
1637
|
if (event.type === "agent_start") {
|
|
1513
1638
|
this.#turnIndex = 0;
|
|
1514
|
-
this.#nextToolChoiceOverride = undefined;
|
|
1515
1639
|
await this.#extensionRunner.emit({ type: "agent_start" });
|
|
1516
1640
|
} else if (event.type === "agent_end") {
|
|
1517
1641
|
await this.#extensionRunner.emit({ type: "agent_end", messages: event.messages });
|
|
@@ -1677,10 +1801,9 @@ export class AgentSession {
|
|
|
1677
1801
|
if (drained === false && deliveryState) {
|
|
1678
1802
|
logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
|
|
1679
1803
|
}
|
|
1804
|
+
this.#stopPowerAssertion();
|
|
1680
1805
|
await this.sessionManager.close();
|
|
1681
1806
|
this.#closeAllProviderSessions("dispose");
|
|
1682
|
-
this.#unsubscribePendingActionPush?.();
|
|
1683
|
-
this.#unsubscribePendingActionPush = undefined;
|
|
1684
1807
|
this.#disconnectFromAgent();
|
|
1685
1808
|
this.#eventListeners = [];
|
|
1686
1809
|
}
|
|
@@ -1893,6 +2016,14 @@ export class AgentSession {
|
|
|
1893
2016
|
validToolNames.push(name);
|
|
1894
2017
|
}
|
|
1895
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
|
+
}
|
|
1896
2027
|
if (this.#mcpDiscoveryEnabled) {
|
|
1897
2028
|
this.#selectedMCPToolNames = new Set(
|
|
1898
2029
|
validToolNames.filter(
|
|
@@ -1999,6 +2130,49 @@ export class AgentSession {
|
|
|
1999
2130
|
await this.#applyActiveToolsByName(nextActive, { previousSelectedMCPToolNames });
|
|
2000
2131
|
}
|
|
2001
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
|
+
|
|
2002
2176
|
/** Whether auto-compaction is currently running */
|
|
2003
2177
|
get isCompacting(): boolean {
|
|
2004
2178
|
return this.#autoCompactionAbortController !== undefined || this.#compactionAbortController !== undefined;
|
|
@@ -2183,7 +2357,7 @@ export class AgentSession {
|
|
|
2183
2357
|
throw error;
|
|
2184
2358
|
}
|
|
2185
2359
|
|
|
2186
|
-
const content =
|
|
2360
|
+
const content = prompt.render(planModeReferencePrompt, {
|
|
2187
2361
|
planFilePath,
|
|
2188
2362
|
planContent,
|
|
2189
2363
|
});
|
|
@@ -2220,7 +2394,7 @@ export class AgentSession {
|
|
|
2220
2394
|
: sessionPlanUrl;
|
|
2221
2395
|
|
|
2222
2396
|
const planExists = fs.existsSync(resolvedPlanPath);
|
|
2223
|
-
const content =
|
|
2397
|
+
const content = prompt.render(planModeActivePrompt, {
|
|
2224
2398
|
planFilePath: displayPlanPath,
|
|
2225
2399
|
planExists,
|
|
2226
2400
|
askToolName: "ask",
|
|
@@ -2292,7 +2466,10 @@ export class AgentSession {
|
|
|
2292
2466
|
return;
|
|
2293
2467
|
}
|
|
2294
2468
|
|
|
2295
|
-
|
|
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;
|
|
2296
2473
|
|
|
2297
2474
|
const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
|
|
2298
2475
|
if (options?.images) {
|
|
@@ -2305,7 +2482,9 @@ export class AgentSession {
|
|
|
2305
2482
|
: { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
|
|
2306
2483
|
|
|
2307
2484
|
if (eagerTodoPrelude) {
|
|
2308
|
-
this.#
|
|
2485
|
+
this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
|
|
2486
|
+
label: "eager-todo",
|
|
2487
|
+
});
|
|
2309
2488
|
}
|
|
2310
2489
|
|
|
2311
2490
|
try {
|
|
@@ -2314,9 +2493,9 @@ export class AgentSession {
|
|
|
2314
2493
|
prependMessages: eagerTodoPrelude ? [eagerTodoPrelude.message] : undefined,
|
|
2315
2494
|
});
|
|
2316
2495
|
} finally {
|
|
2317
|
-
if
|
|
2318
|
-
|
|
2319
|
-
|
|
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");
|
|
2320
2499
|
}
|
|
2321
2500
|
if (!options?.synthetic) {
|
|
2322
2501
|
await this.#enforcePlanModeToolDecision();
|
|
@@ -2680,6 +2859,10 @@ export class AgentSession {
|
|
|
2680
2859
|
});
|
|
2681
2860
|
}
|
|
2682
2861
|
|
|
2862
|
+
queueDeferredMessage(message: CustomMessage): void {
|
|
2863
|
+
this.#queueHiddenNextTurnMessage(message, true);
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2683
2866
|
#queueHiddenNextTurnMessage(message: CustomMessage, triggerTurn: boolean): void {
|
|
2684
2867
|
this.#pendingNextTurnMessages.push(message);
|
|
2685
2868
|
if (!triggerTurn) return;
|
|
@@ -3036,6 +3219,13 @@ export class AgentSession {
|
|
|
3036
3219
|
// block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
|
|
3037
3220
|
// a subsequent prompt() can incorrectly observe the session as busy after an abort.
|
|
3038
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
|
+
}
|
|
3039
3229
|
}
|
|
3040
3230
|
|
|
3041
3231
|
/**
|
|
@@ -3742,7 +3932,7 @@ export class AgentSession {
|
|
|
3742
3932
|
}
|
|
3743
3933
|
|
|
3744
3934
|
// Build the handoff prompt
|
|
3745
|
-
const handoffPrompt =
|
|
3935
|
+
const handoffPrompt = prompt.render(handoffDocumentPrompt, {
|
|
3746
3936
|
additionalFocus: customInstructions,
|
|
3747
3937
|
});
|
|
3748
3938
|
|
|
@@ -4010,7 +4200,7 @@ export class AgentSession {
|
|
|
4010
4200
|
return;
|
|
4011
4201
|
}
|
|
4012
4202
|
|
|
4013
|
-
const reminder =
|
|
4203
|
+
const reminder = prompt.render(planModeToolDecisionReminderPrompt, {
|
|
4014
4204
|
askToolName: "ask",
|
|
4015
4205
|
exitToolName: "exit_plan_mode",
|
|
4016
4206
|
});
|
|
@@ -4022,7 +4212,7 @@ export class AgentSession {
|
|
|
4022
4212
|
});
|
|
4023
4213
|
}
|
|
4024
4214
|
|
|
4025
|
-
#createEagerTodoPrelude(): { message: AgentMessage; toolChoice: ToolChoice } | undefined {
|
|
4215
|
+
#createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice: ToolChoice } | undefined {
|
|
4026
4216
|
const eagerTodosEnabled = this.settings.get("todo.eager");
|
|
4027
4217
|
const todosEnabled = this.settings.get("todo.enabled");
|
|
4028
4218
|
if (!eagerTodosEnabled || !todosEnabled) {
|
|
@@ -4036,6 +4226,11 @@ export class AgentSession {
|
|
|
4036
4226
|
return undefined;
|
|
4037
4227
|
}
|
|
4038
4228
|
|
|
4229
|
+
const trimmedPromptText = promptText.trimEnd();
|
|
4230
|
+
if (trimmedPromptText.endsWith("?") || trimmedPromptText.endsWith("!")) {
|
|
4231
|
+
return undefined;
|
|
4232
|
+
}
|
|
4233
|
+
|
|
4039
4234
|
if (!this.#toolRegistry.has("todo_write")) {
|
|
4040
4235
|
logger.warn("Eager todo enforcement skipped because todo_write is unavailable", {
|
|
4041
4236
|
activeToolNames: this.agent.state.tools.map(tool => tool.name),
|
|
@@ -4052,7 +4247,7 @@ export class AgentSession {
|
|
|
4052
4247
|
return undefined;
|
|
4053
4248
|
}
|
|
4054
4249
|
|
|
4055
|
-
const eagerTodoReminder =
|
|
4250
|
+
const eagerTodoReminder = prompt.render(eagerTodoPrompt);
|
|
4056
4251
|
|
|
4057
4252
|
return {
|
|
4058
4253
|
message: {
|
|
@@ -4070,6 +4265,13 @@ export class AgentSession {
|
|
|
4070
4265
|
* Check if agent stopped with incomplete todos and prompt to continue.
|
|
4071
4266
|
*/
|
|
4072
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
|
+
|
|
4073
4275
|
const remindersEnabled = this.settings.get("todo.reminders");
|
|
4074
4276
|
const todosEnabled = this.settings.get("todo.enabled");
|
|
4075
4277
|
if (!remindersEnabled || !todosEnabled) {
|
|
@@ -4868,6 +5070,17 @@ export class AgentSession {
|
|
|
4868
5070
|
}
|
|
4869
5071
|
|
|
4870
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 {
|
|
4871
5084
|
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
|
|
4872
5085
|
// service unavailable, network/connection errors, fetch failed, terminated, retry delay exceeded
|
|
4873
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(
|
|
@@ -6201,176 +6414,13 @@ export class AgentSession {
|
|
|
6201
6414
|
* Includes user messages, assistant text, thinking blocks, tool calls, and tool results.
|
|
6202
6415
|
*/
|
|
6203
6416
|
formatSessionAsText(): string {
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
6212
|
-
parts.push(`${indent}<parameter name="${key}">${text}</parameter>`);
|
|
6213
|
-
}
|
|
6214
|
-
return parts.join("\n");
|
|
6215
|
-
}
|
|
6216
|
-
|
|
6217
|
-
// Include system prompt at the beginning
|
|
6218
|
-
const systemPrompt = this.agent.state.systemPrompt;
|
|
6219
|
-
if (systemPrompt) {
|
|
6220
|
-
lines.push("## System Prompt\n");
|
|
6221
|
-
lines.push(systemPrompt);
|
|
6222
|
-
lines.push("\n");
|
|
6223
|
-
}
|
|
6224
|
-
|
|
6225
|
-
// Include model and thinking level
|
|
6226
|
-
const model = this.agent.state.model;
|
|
6227
|
-
const thinkingLevel = this.#thinkingLevel;
|
|
6228
|
-
lines.push("## Configuration\n");
|
|
6229
|
-
lines.push(`Model: ${model ? `${model.provider}/${model.id}` : "(not selected)"}`);
|
|
6230
|
-
lines.push(`Thinking Level: ${thinkingLevel}`);
|
|
6231
|
-
lines.push("\n");
|
|
6232
|
-
|
|
6233
|
-
// Include available tools
|
|
6234
|
-
const tools = this.agent.state.tools;
|
|
6235
|
-
|
|
6236
|
-
// Recursively strip all fields starting with 'TypeBox.' from an object
|
|
6237
|
-
function stripTypeBoxFields(obj: any): any {
|
|
6238
|
-
if (Array.isArray(obj)) {
|
|
6239
|
-
return obj.map(stripTypeBoxFields);
|
|
6240
|
-
}
|
|
6241
|
-
if (obj && typeof obj === "object") {
|
|
6242
|
-
const result: Record<string, any> = {};
|
|
6243
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
6244
|
-
if (!k.startsWith("TypeBox.")) {
|
|
6245
|
-
result[k] = stripTypeBoxFields(v);
|
|
6246
|
-
}
|
|
6247
|
-
}
|
|
6248
|
-
return result;
|
|
6249
|
-
}
|
|
6250
|
-
return obj;
|
|
6251
|
-
}
|
|
6252
|
-
|
|
6253
|
-
if (tools.length > 0) {
|
|
6254
|
-
lines.push("## Available Tools\n");
|
|
6255
|
-
for (const tool of tools) {
|
|
6256
|
-
lines.push(`<tool name="${tool.name}">`);
|
|
6257
|
-
lines.push(tool.description);
|
|
6258
|
-
const parametersClean = stripTypeBoxFields(tool.parameters);
|
|
6259
|
-
lines.push(`\nParameters:\n${formatArgsAsXml(parametersClean as Record<string, unknown>)}`);
|
|
6260
|
-
lines.push("<" + "/tool>\n");
|
|
6261
|
-
}
|
|
6262
|
-
lines.push("\n");
|
|
6263
|
-
}
|
|
6264
|
-
|
|
6265
|
-
for (const msg of this.messages) {
|
|
6266
|
-
if (msg.role === "user" || msg.role === "developer") {
|
|
6267
|
-
lines.push(msg.role === "developer" ? "## Developer\n" : "## User\n");
|
|
6268
|
-
if (typeof msg.content === "string") {
|
|
6269
|
-
lines.push(msg.content);
|
|
6270
|
-
} else {
|
|
6271
|
-
for (const c of msg.content) {
|
|
6272
|
-
if (c.type === "text") {
|
|
6273
|
-
lines.push(c.text);
|
|
6274
|
-
} else if (c.type === "image") {
|
|
6275
|
-
lines.push("[Image]");
|
|
6276
|
-
}
|
|
6277
|
-
}
|
|
6278
|
-
}
|
|
6279
|
-
lines.push("\n");
|
|
6280
|
-
} else if (msg.role === "assistant") {
|
|
6281
|
-
const assistantMsg = msg as AssistantMessage;
|
|
6282
|
-
lines.push("## Assistant\n");
|
|
6283
|
-
|
|
6284
|
-
for (const c of assistantMsg.content) {
|
|
6285
|
-
if (c.type === "text") {
|
|
6286
|
-
lines.push(c.text);
|
|
6287
|
-
} else if (c.type === "thinking") {
|
|
6288
|
-
lines.push("<thinking>");
|
|
6289
|
-
lines.push(c.thinking);
|
|
6290
|
-
lines.push("</thinking>\n");
|
|
6291
|
-
} else if (c.type === "toolCall") {
|
|
6292
|
-
lines.push(`<invoke name="${c.name}">`);
|
|
6293
|
-
if (c.arguments && typeof c.arguments === "object") {
|
|
6294
|
-
lines.push(formatArgsAsXml(c.arguments as Record<string, unknown>));
|
|
6295
|
-
}
|
|
6296
|
-
lines.push("<" + "/invoke>\n");
|
|
6297
|
-
}
|
|
6298
|
-
}
|
|
6299
|
-
lines.push("");
|
|
6300
|
-
} else if (msg.role === "toolResult") {
|
|
6301
|
-
lines.push(`### Tool Result: ${msg.toolName}`);
|
|
6302
|
-
if (msg.isError) {
|
|
6303
|
-
lines.push("(error)");
|
|
6304
|
-
}
|
|
6305
|
-
for (const c of msg.content) {
|
|
6306
|
-
if (c.type === "text") {
|
|
6307
|
-
lines.push("```");
|
|
6308
|
-
lines.push(c.text);
|
|
6309
|
-
lines.push("```");
|
|
6310
|
-
} else if (c.type === "image") {
|
|
6311
|
-
lines.push("[Image output]");
|
|
6312
|
-
}
|
|
6313
|
-
}
|
|
6314
|
-
lines.push("");
|
|
6315
|
-
} else if (msg.role === "bashExecution") {
|
|
6316
|
-
const bashMsg = msg as BashExecutionMessage;
|
|
6317
|
-
if (!bashMsg.excludeFromContext) {
|
|
6318
|
-
lines.push("## Bash Execution\n");
|
|
6319
|
-
lines.push(bashExecutionToText(bashMsg));
|
|
6320
|
-
lines.push("\n");
|
|
6321
|
-
}
|
|
6322
|
-
} else if (msg.role === "pythonExecution") {
|
|
6323
|
-
const pythonMsg = msg as PythonExecutionMessage;
|
|
6324
|
-
if (!pythonMsg.excludeFromContext) {
|
|
6325
|
-
lines.push("## Python Execution\n");
|
|
6326
|
-
lines.push(pythonExecutionToText(pythonMsg));
|
|
6327
|
-
lines.push("\n");
|
|
6328
|
-
}
|
|
6329
|
-
} else if (msg.role === "custom" || msg.role === "hookMessage") {
|
|
6330
|
-
const customMsg = msg as CustomMessage | HookMessage;
|
|
6331
|
-
lines.push(`## ${customMsg.customType}\n`);
|
|
6332
|
-
if (typeof customMsg.content === "string") {
|
|
6333
|
-
lines.push(customMsg.content);
|
|
6334
|
-
} else {
|
|
6335
|
-
for (const c of customMsg.content) {
|
|
6336
|
-
if (c.type === "text") {
|
|
6337
|
-
lines.push(c.text);
|
|
6338
|
-
} else if (c.type === "image") {
|
|
6339
|
-
lines.push("[Image]");
|
|
6340
|
-
}
|
|
6341
|
-
}
|
|
6342
|
-
}
|
|
6343
|
-
lines.push("\n");
|
|
6344
|
-
} else if (msg.role === "branchSummary") {
|
|
6345
|
-
const branchMsg = msg as BranchSummaryMessage;
|
|
6346
|
-
lines.push("## Branch Summary\n");
|
|
6347
|
-
lines.push(`(from branch: ${branchMsg.fromId})\n`);
|
|
6348
|
-
lines.push(branchMsg.summary);
|
|
6349
|
-
lines.push("\n");
|
|
6350
|
-
} else if (msg.role === "compactionSummary") {
|
|
6351
|
-
const compactMsg = msg as CompactionSummaryMessage;
|
|
6352
|
-
lines.push("## Compaction Summary\n");
|
|
6353
|
-
lines.push(`(${compactMsg.tokensBefore} tokens before compaction)\n`);
|
|
6354
|
-
lines.push(compactMsg.summary);
|
|
6355
|
-
lines.push("\n");
|
|
6356
|
-
} else if (msg.role === "fileMention") {
|
|
6357
|
-
const fileMsg = msg as FileMentionMessage;
|
|
6358
|
-
lines.push("## File Mention\n");
|
|
6359
|
-
for (const file of fileMsg.files) {
|
|
6360
|
-
lines.push(`<file path="${file.path}">`);
|
|
6361
|
-
if (file.content) {
|
|
6362
|
-
lines.push(file.content);
|
|
6363
|
-
}
|
|
6364
|
-
if (file.image) {
|
|
6365
|
-
lines.push("[Image attached]");
|
|
6366
|
-
}
|
|
6367
|
-
lines.push("</file>\n");
|
|
6368
|
-
}
|
|
6369
|
-
lines.push("\n");
|
|
6370
|
-
}
|
|
6371
|
-
}
|
|
6372
|
-
|
|
6373
|
-
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
|
+
});
|
|
6374
6424
|
}
|
|
6375
6425
|
|
|
6376
6426
|
/**
|