@oh-my-pi/pi-coding-agent 17.0.7 → 17.0.9
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 +91 -0
- package/dist/cli.js +5025 -4930
- package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +28 -1
- package/dist/types/config/model-resolver.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +50 -3
- package/dist/types/dap/client.d.ts +19 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +1 -1
- package/dist/types/extensibility/typebox.d.ts +4 -0
- package/dist/types/hindsight/client.d.ts +10 -0
- package/dist/types/hindsight/config.d.ts +8 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +6 -2
- package/dist/types/mcp/render.d.ts +2 -2
- package/dist/types/mcp/tool-bridge.d.ts +16 -3
- package/dist/types/mcp/types.d.ts +6 -0
- package/dist/types/mnemopi/state.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +8 -0
- package/dist/types/modes/components/settings-defs.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +25 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +8 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
- package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/types.d.ts +14 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +12 -8
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-error-log.test.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +86 -3
- package/dist/types/session/session-loader.d.ts +6 -4
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/prewalk.d.ts +3 -0
- package/dist/types/task/types.d.ts +14 -0
- package/dist/types/tools/ask.d.ts +10 -0
- package/dist/types/tools/hub/jobs.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/report-tool-issue.d.ts +24 -9
- package/dist/types/tools/tool-timeouts.d.ts +8 -2
- package/dist/types/utils/changelog.d.ts +4 -6
- package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +13 -13
- package/scripts/legacy-pi-virtual-module.ts +1 -1
- package/src/cli/grievances-cli.ts +2 -2
- package/src/cli/models-cli.ts +37 -17
- package/src/cli/update-cli.ts +14 -1
- package/src/cli/usage-cli.ts +1 -1
- package/src/config/__tests__/model-registry.test.ts +147 -0
- package/src/config/model-registry.ts +87 -24
- package/src/config/model-resolver.ts +56 -32
- package/src/config/settings-schema.ts +46 -3
- package/src/config/settings.ts +68 -5
- package/src/dap/client.ts +86 -7
- package/src/edit/diff.ts +4 -4
- package/src/eval/py/prelude.py +5 -5
- package/src/export/html/template.js +23 -9
- package/src/extensibility/extensions/runner.ts +9 -4
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
- package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +851 -310
- package/src/extensibility/typebox.ts +22 -9
- package/src/hindsight/client.test.ts +42 -0
- package/src/hindsight/client.ts +40 -3
- package/src/hindsight/config.ts +18 -0
- package/src/internal-urls/registry-helpers.ts +40 -0
- package/src/launch/broker.ts +31 -5
- package/src/lsp/client.ts +23 -0
- package/src/lsp/clients/biome-client.ts +3 -4
- package/src/lsp/index.ts +26 -8
- package/src/main.ts +16 -5
- package/src/mcp/manager.ts +39 -8
- package/src/mcp/render.ts +94 -35
- package/src/mcp/tool-bridge.ts +116 -11
- package/src/mcp/types.ts +7 -0
- package/src/mnemopi/state.ts +70 -3
- package/src/modes/components/agent-dashboard.ts +6 -2
- package/src/modes/components/agent-hub.ts +26 -9
- package/src/modes/components/chat-transcript-builder.ts +13 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/oauth-selector.ts +24 -7
- package/src/modes/components/plan-review-overlay.ts +20 -4
- package/src/modes/components/settings-defs.ts +5 -2
- package/src/modes/components/settings-selector.ts +9 -5
- package/src/modes/components/tool-execution.test.ts +63 -2
- package/src/modes/components/user-message.ts +100 -1
- package/src/modes/controllers/command-controller.ts +36 -12
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +18 -0
- package/src/modes/controllers/input-controller.ts +47 -12
- package/src/modes/controllers/mcp-command-controller.ts +70 -40
- package/src/modes/controllers/selector-controller.ts +82 -6
- package/src/modes/interactive-mode.ts +39 -4
- package/src/modes/rpc/rpc-client.ts +94 -3
- package/src/modes/rpc/rpc-frame.ts +164 -4
- package/src/modes/rpc/rpc-messages.ts +127 -0
- package/src/modes/rpc/rpc-mode.ts +79 -7
- package/src/modes/rpc/rpc-types.ts +34 -2
- package/src/modes/setup-wizard/scenes/model.ts +5 -7
- package/src/modes/setup-wizard/scenes/providers.ts +3 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
- package/src/modes/setup-wizard/scenes/theme.ts +13 -3
- package/src/modes/setup-wizard/scenes/types.ts +9 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +6 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +40 -20
- package/src/prompts/goals/guided-goal-system.md +24 -3
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/task.md +12 -2
- package/src/sdk.ts +179 -51
- package/src/session/agent-session-error-log.test.ts +59 -0
- package/src/session/agent-session.ts +351 -46
- package/src/session/session-context.test.ts +224 -1
- package/src/session/session-context.ts +41 -2
- package/src/session/session-loader.ts +10 -5
- package/src/slash-commands/helpers/usage-report.ts +3 -1
- package/src/system-prompt.ts +1 -2
- package/src/task/executor.ts +44 -27
- package/src/task/index.ts +63 -8
- package/src/task/prewalk.ts +6 -0
- package/src/task/structured-subagent.ts +3 -1
- package/src/task/types.ts +16 -0
- package/src/task/worktree.ts +25 -11
- package/src/tools/ask.ts +15 -0
- package/src/tools/bash.ts +14 -6
- package/src/tools/browser.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval.ts +4 -5
- package/src/tools/fetch.ts +1 -1
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +67 -8
- package/src/tools/hub/launch.ts +7 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/report-tool-issue.ts +79 -28
- package/src/tools/tool-timeouts.ts +10 -3
- package/src/tools/write.ts +31 -0
- package/src/utils/changelog.ts +54 -58
- package/src/utils/git.ts +57 -49
- package/src/web/search/providers/firecrawl.ts +46 -13
- package/src/web/search/types.ts +5 -1
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
type AgentMessage,
|
|
31
31
|
type AgentState,
|
|
32
32
|
type AgentTool,
|
|
33
|
+
type AgentToolCall,
|
|
34
|
+
type AgentToolContext,
|
|
33
35
|
type AgentToolResult,
|
|
34
36
|
type AgentTurnEndContext,
|
|
35
37
|
AppendOnlyContextManager,
|
|
@@ -37,6 +39,7 @@ import {
|
|
|
37
39
|
type CompactionSummaryMessage,
|
|
38
40
|
countTokens,
|
|
39
41
|
createToolScopedAbortReason,
|
|
42
|
+
isSyntheticToolResultMessage,
|
|
40
43
|
resolveTelemetry,
|
|
41
44
|
type StreamFn,
|
|
42
45
|
TERMINAL_TOOL_RESULT_ABORT_REASON,
|
|
@@ -105,6 +108,7 @@ import type {
|
|
|
105
108
|
TextContent,
|
|
106
109
|
ToolCall,
|
|
107
110
|
ToolChoice,
|
|
111
|
+
ToolResultMessage,
|
|
108
112
|
Usage,
|
|
109
113
|
UsageReport,
|
|
110
114
|
} from "@oh-my-pi/pi-ai";
|
|
@@ -238,6 +242,7 @@ import type {
|
|
|
238
242
|
TurnEndEvent,
|
|
239
243
|
TurnStartEvent,
|
|
240
244
|
} from "../extensibility/extensions";
|
|
245
|
+
import { emitSessionShutdownEvent } from "../extensibility/extensions";
|
|
241
246
|
import { ManagedTimers } from "../extensibility/extensions/managed-timers";
|
|
242
247
|
import { createExtensionModelQuery } from "../extensibility/extensions/model-api";
|
|
243
248
|
import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
|
|
@@ -302,6 +307,7 @@ import { AgentRegistry } from "../registry/agent-registry";
|
|
|
302
307
|
import {
|
|
303
308
|
deobfuscateAssistantContent,
|
|
304
309
|
deobfuscateSessionContext,
|
|
310
|
+
deobfuscateToolArguments,
|
|
305
311
|
obfuscateMessages,
|
|
306
312
|
obfuscateProviderContext,
|
|
307
313
|
type SecretObfuscator,
|
|
@@ -320,6 +326,7 @@ import {
|
|
|
320
326
|
} from "../thinking";
|
|
321
327
|
import { formatTitleConversationContext, type TitleConversationTurn } from "../tiny/message-preproc";
|
|
322
328
|
import { shutdownTinyTitleClient } from "../tiny/title-client";
|
|
329
|
+
import { type AskToolDetails, type AskToolInput, recoverAskQuestions } from "../tools/ask";
|
|
323
330
|
import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
324
331
|
import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
|
|
325
332
|
import { isMCPToolName, normalizeToolNames } from "../tools/builtin-names";
|
|
@@ -872,6 +879,16 @@ export interface PlanYolo {
|
|
|
872
879
|
// Types
|
|
873
880
|
// ============================================================================
|
|
874
881
|
|
|
882
|
+
/** Identifies a retry fallback chain already entered during startup model resolution. */
|
|
883
|
+
export interface InitialRetryFallbackState {
|
|
884
|
+
/** Role whose configured primary was unavailable. */
|
|
885
|
+
role: string;
|
|
886
|
+
/** Configured primary selector retained for restoration when it becomes available. */
|
|
887
|
+
originalSelector: string;
|
|
888
|
+
/** Thinking selector configured for the unavailable primary. */
|
|
889
|
+
originalThinkingLevel: ConfiguredThinkingLevel | undefined;
|
|
890
|
+
}
|
|
891
|
+
|
|
875
892
|
export interface AgentSessionConfig {
|
|
876
893
|
agent: Agent;
|
|
877
894
|
sessionManager: SessionManager;
|
|
@@ -882,6 +899,8 @@ export interface AgentSessionConfig {
|
|
|
882
899
|
scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
883
900
|
/** Initial session thinking selector. */
|
|
884
901
|
thinkingLevel?: ConfiguredThinkingLevel;
|
|
902
|
+
/** Retry chain ownership when startup selected one of its fallback entries. */
|
|
903
|
+
initialRetryFallback?: InitialRetryFallbackState;
|
|
885
904
|
/** Prewalk from the starting model to a fast/cheap target at the first edit/write once the todo list exists. */
|
|
886
905
|
prewalk?: Prewalk;
|
|
887
906
|
/** Force read-only plan mode at start, auto-approve on the model's first
|
|
@@ -1682,6 +1701,25 @@ function isAdvisorCard(message: AgentMessage): message is CustomMessage {
|
|
|
1682
1701
|
return message.role === "custom" && message.customType === "advisor";
|
|
1683
1702
|
}
|
|
1684
1703
|
|
|
1704
|
+
/**
|
|
1705
|
+
* Emit a warn-level log for a turn that ended in a provider error so recurring
|
|
1706
|
+
* stream failures are diagnosable from the main log alone. The `agent_end`
|
|
1707
|
+
* routing trace is debug-only and omits the error fields; without this a
|
|
1708
|
+
* session dying on provider errors leaves only `stopReason:"error"` debug lines
|
|
1709
|
+
* and the real cause lives solely in the session transcript (issue #6177).
|
|
1710
|
+
* No-op for any non-error stop reason.
|
|
1711
|
+
*/
|
|
1712
|
+
export function logProviderTurnError(msg: AssistantMessage): void {
|
|
1713
|
+
if (msg.stopReason !== "error") return;
|
|
1714
|
+
logger.warn("agent turn ended with provider error", {
|
|
1715
|
+
provider: msg.provider,
|
|
1716
|
+
model: msg.model,
|
|
1717
|
+
errorMessage: msg.errorMessage,
|
|
1718
|
+
errorStatus: msg.errorStatus,
|
|
1719
|
+
errorId: msg.errorId,
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1685
1723
|
function isTerminalTextAssistantAnswer(message: AgentMessage | undefined): message is AssistantMessage {
|
|
1686
1724
|
if (message?.role !== "assistant" || message.stopReason !== "stop") return false;
|
|
1687
1725
|
let hasText = false;
|
|
@@ -1843,6 +1881,22 @@ function titleConversationTurnFromMessage(message: AgentMessage): TitleConversat
|
|
|
1843
1881
|
return { role: message.role, ...(text ? { text } : {}), ...(thinking ? { thinking } : {}) };
|
|
1844
1882
|
}
|
|
1845
1883
|
|
|
1884
|
+
function syntheticToolResultTailStart(messages: readonly AgentMessage[]): number {
|
|
1885
|
+
let index = messages.length;
|
|
1886
|
+
while (index > 0 && isSyntheticToolResultMessage(messages[index - 1])) {
|
|
1887
|
+
index--;
|
|
1888
|
+
}
|
|
1889
|
+
return index;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function retryableAssistantTurnEnd(messages: readonly AgentMessage[]): number | undefined {
|
|
1893
|
+
const turnEnd = syntheticToolResultTailStart(messages);
|
|
1894
|
+
const message = messages[turnEnd - 1];
|
|
1895
|
+
if (message?.role !== "assistant") return undefined;
|
|
1896
|
+
if (message.stopReason !== "error" && message.stopReason !== "aborted") return undefined;
|
|
1897
|
+
return turnEnd;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1846
1900
|
export class AgentSession {
|
|
1847
1901
|
readonly agent: Agent;
|
|
1848
1902
|
readonly sessionManager: SessionManager;
|
|
@@ -2698,6 +2752,13 @@ export class AgentSession {
|
|
|
2698
2752
|
} else {
|
|
2699
2753
|
this.#thinkingLevel = config.thinkingLevel;
|
|
2700
2754
|
}
|
|
2755
|
+
if (config.initialRetryFallback) {
|
|
2756
|
+
this.#activeRetryFallback = {
|
|
2757
|
+
...config.initialRetryFallback,
|
|
2758
|
+
lastAppliedFallbackThinkingLevel: this.configuredThinkingLevel(),
|
|
2759
|
+
pinned: false,
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2701
2762
|
if (config.prewalk) {
|
|
2702
2763
|
this.#prewalk = config.prewalk;
|
|
2703
2764
|
}
|
|
@@ -4828,9 +4889,10 @@ export class AgentSession {
|
|
|
4828
4889
|
if (toolName === "edit" && editedPath) {
|
|
4829
4890
|
this.#invalidateFileCacheForPath(editedPath);
|
|
4830
4891
|
}
|
|
4892
|
+
// TodoTool commits its state during execute. Replaying the result after
|
|
4893
|
+
// awaited event fan-out can overwrite a newer call from the same batch.
|
|
4831
4894
|
const phases = details?.phases;
|
|
4832
4895
|
if (toolName === "todo" && !isError && details && Array.isArray(phases) && phases.every(isTodoPhase)) {
|
|
4833
|
-
this.setTodoPhases(phases);
|
|
4834
4896
|
if (this.#isTodoInitResult(details, toolCallId)) {
|
|
4835
4897
|
this.#scheduleReplanTitleRefresh();
|
|
4836
4898
|
}
|
|
@@ -4930,6 +4992,12 @@ export class AgentSession {
|
|
|
4930
4992
|
};
|
|
4931
4993
|
maintenanceRoute("entered");
|
|
4932
4994
|
|
|
4995
|
+
// Surface provider stream failures in the main log. The routing trace
|
|
4996
|
+
// above is debug-only and drops the error fields, so a session dying
|
|
4997
|
+
// repeatedly on provider errors otherwise leaves no actionable trace
|
|
4998
|
+
// outside the session transcript (issue #6177).
|
|
4999
|
+
logProviderTurnError(msg);
|
|
5000
|
+
|
|
4933
5001
|
// Invalidate GitHub Copilot credentials on auth failure so stale tokens
|
|
4934
5002
|
// aren't reused on the next request
|
|
4935
5003
|
if (
|
|
@@ -5081,6 +5149,15 @@ export class AgentSession {
|
|
|
5081
5149
|
if (this.#isClassifierRefusal(msg)) {
|
|
5082
5150
|
this.#prunedTerminalRefusal = msg;
|
|
5083
5151
|
this.#removeAssistantMessageFromActiveContext(msg);
|
|
5152
|
+
} else if (!AIError.isContextOverflow(msg, this.model?.contextWindow ?? 0)) {
|
|
5153
|
+
// No retry, fallback, or compaction continuation fired: this errored
|
|
5154
|
+
// turn ends the run. #persistSessionMessageIfMissing dropped it as an
|
|
5155
|
+
// empty error turn, so record it here — otherwise the JSONL stops at
|
|
5156
|
+
// the last tool result and the provider's errorMessage is lost (#6249).
|
|
5157
|
+
// Idempotent and a no-op for non-empty turns. Content-less overflow
|
|
5158
|
+
// rejections stay live-UI only per the auto-compaction progress guard:
|
|
5159
|
+
// persisting one would replay an empty assistant turn on reload.
|
|
5160
|
+
await this.#persistTerminalEmptyErrorTurn(msg);
|
|
5084
5161
|
}
|
|
5085
5162
|
this.#resolveRetry();
|
|
5086
5163
|
|
|
@@ -6399,6 +6476,10 @@ export class AgentSession {
|
|
|
6399
6476
|
messages: AgentMessage[],
|
|
6400
6477
|
lastAssistantMessage = this.getLastAssistantMessage(),
|
|
6401
6478
|
): Promise<boolean> {
|
|
6479
|
+
if (this.#abortInProgress || this.#isDisposed) {
|
|
6480
|
+
this.#resetSessionStopContinuationState();
|
|
6481
|
+
return false;
|
|
6482
|
+
}
|
|
6402
6483
|
if (this.#agentKind === "sub" || !this.#extensionRunner?.hasHandlers("session_stop")) {
|
|
6403
6484
|
return false;
|
|
6404
6485
|
}
|
|
@@ -6882,15 +6963,12 @@ export class AgentSession {
|
|
|
6882
6963
|
this.#cancelExitRecorder?.();
|
|
6883
6964
|
this.#cancelExitRecorder = undefined;
|
|
6884
6965
|
try {
|
|
6885
|
-
|
|
6886
|
-
await this.#extensionRunner.emit({ type: "session_shutdown" });
|
|
6887
|
-
}
|
|
6966
|
+
await emitSessionShutdownEvent(this.#extensionRunner);
|
|
6888
6967
|
} catch (error) {
|
|
6889
6968
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
6890
6969
|
}
|
|
6891
6970
|
|
|
6892
|
-
// Stop extension timers before aborting deferred work they could enqueue.
|
|
6893
|
-
this.#extensionRunner?.clearManagedTimers?.();
|
|
6971
|
+
// Stop fallback extension timers before aborting deferred work they could enqueue.
|
|
6894
6972
|
this.#fallbackExtensionTimers?.clearAll();
|
|
6895
6973
|
this.abortRetry();
|
|
6896
6974
|
this.abortCompaction();
|
|
@@ -6991,6 +7069,12 @@ export class AgentSession {
|
|
|
6991
7069
|
return this.agent.state.model;
|
|
6992
7070
|
}
|
|
6993
7071
|
|
|
7072
|
+
/** Resolved selector while retry routing is using a fallback model. */
|
|
7073
|
+
get retryFallbackModel(): string | undefined {
|
|
7074
|
+
const model = this.model;
|
|
7075
|
+
return this.#activeRetryFallback && model ? formatRetryFallbackSelector(model, this.thinkingLevel) : undefined;
|
|
7076
|
+
}
|
|
7077
|
+
|
|
6994
7078
|
/** Effective thinking level applied to the agent (the resolved level when `auto`). */
|
|
6995
7079
|
get thinkingLevel(): ThinkingLevel | undefined {
|
|
6996
7080
|
return this.#thinkingLevel;
|
|
@@ -8889,12 +8973,16 @@ export class AgentSession {
|
|
|
8889
8973
|
);
|
|
8890
8974
|
}
|
|
8891
8975
|
|
|
8892
|
-
//
|
|
8893
|
-
//
|
|
8894
|
-
//
|
|
8895
|
-
//
|
|
8976
|
+
// Recover a previously failed/incomplete assistant turn before sending.
|
|
8977
|
+
// Successful historical turns take the cheaper pre-prompt threshold path
|
|
8978
|
+
// below; re-running the full post-turn check on resume can synchronously
|
|
8979
|
+
// rewrite/re-render old context before the new prompt starts.
|
|
8896
8980
|
const lastAssistant = this.#findLastAssistantMessage();
|
|
8897
|
-
if (
|
|
8981
|
+
if (
|
|
8982
|
+
lastAssistant &&
|
|
8983
|
+
!options?.skipCompactionCheck &&
|
|
8984
|
+
(lastAssistant.stopReason === "error" || lastAssistant.stopReason === "length")
|
|
8985
|
+
) {
|
|
8898
8986
|
await this.#checkCompaction(lastAssistant, false, false, false);
|
|
8899
8987
|
}
|
|
8900
8988
|
|
|
@@ -11988,7 +12076,17 @@ export class AgentSession {
|
|
|
11988
12076
|
this.#pendingRecoveredRetryErrors = [];
|
|
11989
12077
|
}
|
|
11990
12078
|
|
|
11991
|
-
|
|
12079
|
+
/**
|
|
12080
|
+
* Durably record a terminal empty error turn (`stopReason: "error"` with no
|
|
12081
|
+
* substantive content) that `#persistSessionMessageIfMissing` skipped, so the
|
|
12082
|
+
* session JSONL keeps a record of why the run stopped instead of ending at the
|
|
12083
|
+
* last tool result. A no-op for non-empty/non-error turns and idempotent via
|
|
12084
|
+
* the already-persisted guard; the turn is dropped from active context by the
|
|
12085
|
+
* caller (or `isProviderRefusalMessage`/`isEmptyErrorTurn` filters) so it is
|
|
12086
|
+
* never replayed on the wire. Used by the retry-lifecycle dead-ends and the
|
|
12087
|
+
* non-retry terminal error tail.
|
|
12088
|
+
*/
|
|
12089
|
+
async #persistTerminalEmptyErrorTurn(message: AssistantMessage): Promise<void> {
|
|
11992
12090
|
await this.#waitForSessionMessagePersistence(message);
|
|
11993
12091
|
if (!isEmptyErrorTurn(message)) return;
|
|
11994
12092
|
if (this.#sessionMessageAlreadyPersisted(message)) return;
|
|
@@ -12030,7 +12128,7 @@ export class AgentSession {
|
|
|
12030
12128
|
id: number,
|
|
12031
12129
|
options: { switchedCredential: boolean; switchedModel: boolean; delayMs: number },
|
|
12032
12130
|
): Promise<void> {
|
|
12033
|
-
await this.#
|
|
12131
|
+
await this.#persistTerminalEmptyErrorTurn(message);
|
|
12034
12132
|
const persistenceKey = sessionMessagePersistenceKey(message);
|
|
12035
12133
|
if (!persistenceKey) return;
|
|
12036
12134
|
let branchEntry: SessionEntry | undefined;
|
|
@@ -14810,15 +14908,34 @@ export class AgentSession {
|
|
|
14810
14908
|
return message.content.some(block => block.type === "toolCall");
|
|
14811
14909
|
}
|
|
14812
14910
|
|
|
14911
|
+
/**
|
|
14912
|
+
* OpenRouter can repeatedly close Gemini streams at the reasoning-to-payload
|
|
14913
|
+
* transition. One retry covers a transient edge failure; the normal ten-retry
|
|
14914
|
+
* budget would otherwise re-run the same expensive reasoning cycle unchanged.
|
|
14915
|
+
*/
|
|
14916
|
+
#isOpenRouterThinkingStreamClose(message: AssistantMessage): boolean {
|
|
14917
|
+
return (
|
|
14918
|
+
message.provider === "openrouter" &&
|
|
14919
|
+
/server_error:\s*stream closed with reason:\s*error/i.test(message.errorMessage ?? "") &&
|
|
14920
|
+
message.content.some(block => block.type === "thinking" && block.thinking.trim().length > 0)
|
|
14921
|
+
);
|
|
14922
|
+
}
|
|
14923
|
+
|
|
14813
14924
|
#isClassifierRefusal(message: AssistantMessage): boolean {
|
|
14814
14925
|
if (message.stopReason !== "error") return false;
|
|
14815
14926
|
const stopType = message.stopDetails?.type;
|
|
14816
14927
|
return stopType === "refusal" || stopType === "sensitive";
|
|
14817
14928
|
}
|
|
14818
14929
|
|
|
14819
|
-
/**
|
|
14820
|
-
|
|
14821
|
-
|
|
14930
|
+
/**
|
|
14931
|
+
* True when `provider` has registered models or is configured for dynamic
|
|
14932
|
+
* discovery. Discovery-only providers (e.g. a models.yml provider with
|
|
14933
|
+
* `discovery:` and no static models) can hold zero models until the online
|
|
14934
|
+
* refresh completes, so a models-only check would misreport them as
|
|
14935
|
+
* unknown during session construction.
|
|
14936
|
+
*/
|
|
14937
|
+
#isKnownProvider(provider: string): boolean {
|
|
14938
|
+
return this.#modelRegistry.hasProvider(provider);
|
|
14822
14939
|
}
|
|
14823
14940
|
|
|
14824
14941
|
#getRetryFallbackChains(): RetryFallbackChains {
|
|
@@ -14851,8 +14968,8 @@ export class AgentSession {
|
|
|
14851
14968
|
const keyKind = isRetryFallbackModelKey(key) ? "model" : "role";
|
|
14852
14969
|
if (keyKind === "model") {
|
|
14853
14970
|
if (isRetryFallbackWildcardKey(key)) {
|
|
14854
|
-
const { provider } = parseRetryFallbackWildcard(key, p => this.#
|
|
14855
|
-
if (!this.#
|
|
14971
|
+
const { provider } = parseRetryFallbackWildcard(key, p => this.#isKnownProvider(p));
|
|
14972
|
+
if (!this.#isKnownProvider(provider)) {
|
|
14856
14973
|
const msg = `retry.fallbackChains wildcard key references unknown provider: ${key}`;
|
|
14857
14974
|
logger.warn(msg);
|
|
14858
14975
|
this.configWarnings.push(msg);
|
|
@@ -14884,8 +15001,8 @@ export class AgentSession {
|
|
|
14884
15001
|
continue;
|
|
14885
15002
|
}
|
|
14886
15003
|
if (isRetryFallbackWildcardKey(selectorStr)) {
|
|
14887
|
-
const { provider } = parseRetryFallbackWildcard(selectorStr, p => this.#
|
|
14888
|
-
if (!this.#
|
|
15004
|
+
const { provider } = parseRetryFallbackWildcard(selectorStr, p => this.#isKnownProvider(p));
|
|
15005
|
+
if (!this.#isKnownProvider(provider)) {
|
|
14889
15006
|
const msg = `Fallback chain for ${keyKind} '${key}' references unknown provider: ${selectorStr}`;
|
|
14890
15007
|
logger.warn(msg);
|
|
14891
15008
|
this.configWarnings.push(msg);
|
|
@@ -14985,7 +15102,7 @@ export class AgentSession {
|
|
|
14985
15102
|
let wildcardPrefixLength = -1;
|
|
14986
15103
|
for (const key in chains) {
|
|
14987
15104
|
if (!isRetryFallbackWildcardKey(key) || !Array.isArray(chains[key])) continue;
|
|
14988
|
-
const { provider, idPrefix } = parseRetryFallbackWildcard(key, p => this.#
|
|
15105
|
+
const { provider, idPrefix } = parseRetryFallbackWildcard(key, p => this.#isKnownProvider(p));
|
|
14989
15106
|
if (provider !== parsedCurrent.provider) continue;
|
|
14990
15107
|
if (idPrefix !== undefined && !parsedCurrent.id.startsWith(`${idPrefix}/`)) continue;
|
|
14991
15108
|
const prefixLength = idPrefix === undefined ? 0 : idPrefix.length;
|
|
@@ -15025,7 +15142,7 @@ export class AgentSession {
|
|
|
15025
15142
|
): RetryFallbackSelector | undefined {
|
|
15026
15143
|
if (isRetryFallbackWildcardKey(entry)) {
|
|
15027
15144
|
if (!current) return undefined;
|
|
15028
|
-
const { provider, idPrefix } = parseRetryFallbackWildcard(entry, p => this.#
|
|
15145
|
+
const { provider, idPrefix } = parseRetryFallbackWildcard(entry, p => this.#isKnownProvider(p));
|
|
15029
15146
|
const bareId = current.id.slice(current.id.lastIndexOf("/") + 1);
|
|
15030
15147
|
let id: string;
|
|
15031
15148
|
if (idPrefix !== undefined) {
|
|
@@ -15408,7 +15525,10 @@ export class AgentSession {
|
|
|
15408
15525
|
// (every rotation sets switchedCredential and skips it), so without
|
|
15409
15526
|
// this last resort a provider-wide usage cap never fails over to the
|
|
15410
15527
|
// configured chain.
|
|
15411
|
-
const
|
|
15528
|
+
const maxRetries = this.#isOpenRouterThinkingStreamClose(message)
|
|
15529
|
+
? Math.min(retrySettings.maxRetries, 1)
|
|
15530
|
+
: retrySettings.maxRetries;
|
|
15531
|
+
const retryBudgetExhausted = this.#retryAttempt > maxRetries;
|
|
15412
15532
|
|
|
15413
15533
|
const errorMessage = message.errorMessage || "Unknown error";
|
|
15414
15534
|
const id = this.#classifyRetryMessage(message);
|
|
@@ -15501,7 +15621,7 @@ export class AgentSession {
|
|
|
15501
15621
|
}
|
|
15502
15622
|
if (retryBudgetExhausted) {
|
|
15503
15623
|
if (!switchedModel) {
|
|
15504
|
-
await this.#
|
|
15624
|
+
await this.#persistTerminalEmptyErrorTurn(message);
|
|
15505
15625
|
// Max retries exceeded and no fallback model to switch to: emit
|
|
15506
15626
|
// final failure and reset.
|
|
15507
15627
|
await this.#emitSessionEvent({
|
|
@@ -15548,7 +15668,7 @@ export class AgentSession {
|
|
|
15548
15668
|
// can act on it.
|
|
15549
15669
|
const maxDelayMs = retrySettings.maxDelayMs;
|
|
15550
15670
|
if (maxDelayMs > 0 && delayMs > maxDelayMs && !switchedCredential && !switchedModel) {
|
|
15551
|
-
await this.#
|
|
15671
|
+
await this.#persistTerminalEmptyErrorTurn(message);
|
|
15552
15672
|
const attempt = this.#retryAttempt;
|
|
15553
15673
|
this.#retryAttempt = 0;
|
|
15554
15674
|
await this.#emitSessionEvent({
|
|
@@ -15567,7 +15687,7 @@ export class AgentSession {
|
|
|
15567
15687
|
await this.#emitSessionEvent({
|
|
15568
15688
|
type: "auto_retry_start",
|
|
15569
15689
|
attempt: this.#retryAttempt,
|
|
15570
|
-
maxAttempts:
|
|
15690
|
+
maxAttempts: maxRetries,
|
|
15571
15691
|
delayMs,
|
|
15572
15692
|
errorMessage,
|
|
15573
15693
|
errorId: message.errorId,
|
|
@@ -15692,21 +15812,40 @@ export class AgentSession {
|
|
|
15692
15812
|
}
|
|
15693
15813
|
/**
|
|
15694
15814
|
* Manually retry the last failed assistant turn.
|
|
15695
|
-
* Removes the error message from agent state
|
|
15815
|
+
* Removes the error message from active agent state when present and
|
|
15816
|
+
* re-attempts with a fresh retry budget.
|
|
15817
|
+
*
|
|
15818
|
+
* A stream that stalls or aborts mid-tool-call ends the turn with
|
|
15819
|
+
* `stopReason: "error" | "aborted"` and then appends one synthetic
|
|
15820
|
+
* {@link isSyntheticToolResultMessage tool_result} per emitted tool call to
|
|
15821
|
+
* preserve the provider's tool_use/tool_result pairing (see
|
|
15822
|
+
* `createAbortedToolResult` in `agent-loop.ts`). Those placeholders trail the
|
|
15823
|
+
* failed assistant turn, so the retry lookback walks back over them before
|
|
15824
|
+
* checking the assistant message; it strips both the placeholders and the
|
|
15825
|
+
* failed turn before re-attempting.
|
|
15826
|
+
*
|
|
15827
|
+
* A restored session deliberately omits failed assistant turns from provider
|
|
15828
|
+
* context. In that case, the persisted display transcript remains the source
|
|
15829
|
+
* of truth for whether the current branch has a retryable failed tail.
|
|
15830
|
+
*
|
|
15696
15831
|
* @returns true if retry was initiated, false if no failed turn to retry or agent is busy
|
|
15697
15832
|
*/
|
|
15698
15833
|
async retry(): Promise<boolean> {
|
|
15699
15834
|
if (this.isStreaming || this.isCompacting || this.isRetrying) return false;
|
|
15700
15835
|
|
|
15701
15836
|
const messages = this.agent.state.messages;
|
|
15702
|
-
const
|
|
15703
|
-
if (
|
|
15704
|
-
|
|
15705
|
-
|
|
15706
|
-
|
|
15707
|
-
|
|
15708
|
-
|
|
15709
|
-
|
|
15837
|
+
const activeTurnEnd = retryableAssistantTurnEnd(messages);
|
|
15838
|
+
if (activeTurnEnd !== undefined) {
|
|
15839
|
+
// Remove the failed/aborted assistant message plus its synthetic tool
|
|
15840
|
+
// results (same as auto-retry does before re-attempting).
|
|
15841
|
+
this.agent.replaceMessages(messages.slice(0, activeTurnEnd - 1));
|
|
15842
|
+
} else {
|
|
15843
|
+
// A restored session already dropped the failed assistant turn (and its
|
|
15844
|
+
// paired synthetic tool results) from provider context, so the persisted
|
|
15845
|
+
// display transcript is the source of truth for a retryable failed tail.
|
|
15846
|
+
const transcriptMessages = this.sessionManager.buildSessionContext({ transcript: true }).messages;
|
|
15847
|
+
if (retryableAssistantTurnEnd(transcriptMessages) === undefined) return false;
|
|
15848
|
+
}
|
|
15710
15849
|
|
|
15711
15850
|
// Reset retry budget for a fresh attempt
|
|
15712
15851
|
this.#retryAttempt = 0;
|
|
@@ -15941,7 +16080,7 @@ export class AgentSession {
|
|
|
15941
16080
|
signal: abortController.signal,
|
|
15942
16081
|
sessionKey: target.sessionId,
|
|
15943
16082
|
cwd,
|
|
15944
|
-
timeout: clampTimeout("bash") * 1000,
|
|
16083
|
+
timeout: clampTimeout("bash", undefined, this.settings.get("tools.maxTimeout")) * 1000,
|
|
15945
16084
|
onMinimizedSave: originalText => this.#saveBashOriginalArtifact(target, originalText),
|
|
15946
16085
|
useUserShell: options?.useUserShell,
|
|
15947
16086
|
});
|
|
@@ -17023,7 +17162,30 @@ export class AgentSession {
|
|
|
17023
17162
|
*/
|
|
17024
17163
|
async navigateTree(
|
|
17025
17164
|
targetId: string,
|
|
17026
|
-
options: {
|
|
17165
|
+
options: {
|
|
17166
|
+
summarize?: boolean;
|
|
17167
|
+
customInstructions?: string;
|
|
17168
|
+
/**
|
|
17169
|
+
* Opts into the two-phase `ask` toolResult re-answer protocol
|
|
17170
|
+
* (issue #5642): set only by the interactive `/tree` selector, which
|
|
17171
|
+
* knows how to re-open the picker on `reopenAsk` and complete the
|
|
17172
|
+
* navigation with `reanswerAskResult`. Every other public caller
|
|
17173
|
+
* (extensions, hooks, ACP, session-extension actions) leaves this
|
|
17174
|
+
* unset and gets the pre-#5642 plain leaf move onto `ask`
|
|
17175
|
+
* toolResults instead — they have no picker to re-open and would
|
|
17176
|
+
* otherwise report a successful no-op navigation (roboomp review on
|
|
17177
|
+
* #5895).
|
|
17178
|
+
*/
|
|
17179
|
+
allowAskReopen?: boolean;
|
|
17180
|
+
/**
|
|
17181
|
+
* Completes an in-progress `ask` re-answer (issue #5642): the caller
|
|
17182
|
+
* already received `reopenAsk` from a prior call on the same
|
|
17183
|
+
* `targetId`, re-opened the picker, and is handing back the fresh
|
|
17184
|
+
* answer. Branches a new toolResult sibling instead of landing on
|
|
17185
|
+
* the original one.
|
|
17186
|
+
*/
|
|
17187
|
+
reanswerAskResult?: AgentToolResult<AskToolDetails>;
|
|
17188
|
+
} = {},
|
|
17027
17189
|
): Promise<{
|
|
17028
17190
|
editorText?: string;
|
|
17029
17191
|
cancelled: boolean;
|
|
@@ -17031,12 +17193,36 @@ export class AgentSession {
|
|
|
17031
17193
|
summaryEntry?: BranchSummaryEntry;
|
|
17032
17194
|
/** Raw session context built during navigation — pass to renderInitialMessages to skip a second O(N) walk. */
|
|
17033
17195
|
sessionContext?: SessionContext;
|
|
17196
|
+
/**
|
|
17197
|
+
* Set when `targetId` is an `ask` toolResult, `options.allowAskReopen`
|
|
17198
|
+
* was set, and `options.reanswerAskResult` was not supplied: nothing was
|
|
17199
|
+
* mutated. The caller must re-open the ask picker with these
|
|
17200
|
+
* `questions`, then call `navigateTree(targetId, { ...options,
|
|
17201
|
+
* reanswerAskResult })` with the produced result to actually branch
|
|
17202
|
+
* (issue #5642).
|
|
17203
|
+
*/
|
|
17204
|
+
reopenAsk?: { toolCallId: string; questions: AskToolInput["questions"] };
|
|
17034
17205
|
}> {
|
|
17035
17206
|
await this.#flushPendingBashMessages();
|
|
17036
17207
|
const oldLeafId = this.sessionManager.getLeafId();
|
|
17037
17208
|
|
|
17038
|
-
|
|
17039
|
-
if (
|
|
17209
|
+
const targetEntry = this.sessionManager.getEntry(targetId);
|
|
17210
|
+
if (!targetEntry) {
|
|
17211
|
+
throw new Error(`Entry ${targetId} not found`);
|
|
17212
|
+
}
|
|
17213
|
+
const targetIsAskResult =
|
|
17214
|
+
targetEntry.type === "message" &&
|
|
17215
|
+
targetEntry.message.role === "toolResult" &&
|
|
17216
|
+
targetEntry.message.toolName === "ask";
|
|
17217
|
+
|
|
17218
|
+
// No-op if already at target — except mid-flight through the `ask`
|
|
17219
|
+
// re-answer protocol (issue #5642): a probe or completion call can
|
|
17220
|
+
// legitimately target the *current* leaf (e.g. the user interrupted
|
|
17221
|
+
// right after answering `ask`, before a follow-up assistant message
|
|
17222
|
+
// landed, or another caller navigated straight onto the ask result),
|
|
17223
|
+
// and must still return `reopenAsk` / branch the new answer instead of
|
|
17224
|
+
// silently reporting a no-op (chatgpt-codex review on #5895).
|
|
17225
|
+
if (targetId === oldLeafId && !(options.allowAskReopen && targetIsAskResult)) {
|
|
17040
17226
|
return { cancelled: false };
|
|
17041
17227
|
}
|
|
17042
17228
|
|
|
@@ -17045,16 +17231,47 @@ export class AgentSession {
|
|
|
17045
17231
|
throw new Error("No model available for summarization");
|
|
17046
17232
|
}
|
|
17047
17233
|
|
|
17048
|
-
|
|
17049
|
-
|
|
17050
|
-
|
|
17051
|
-
|
|
17052
|
-
|
|
17053
|
-
//
|
|
17234
|
+
// `ask` toolResult, first pass: hand control back to the caller to
|
|
17235
|
+
// re-open the picker instead of landing on the stale answer in place.
|
|
17236
|
+
// Nothing is mutated here — see the `reanswerAskResult` branch below for
|
|
17237
|
+
// the actual sibling-branch construction once the caller has an answer.
|
|
17238
|
+
// Gated on `allowAskReopen` — callers that don't understand `reopenAsk`
|
|
17239
|
+
// fall straight through to the plain leaf move below instead of
|
|
17240
|
+
// reporting a successful no-op (roboomp review on #5895).
|
|
17241
|
+
if (
|
|
17242
|
+
options.allowAskReopen &&
|
|
17243
|
+
!options.reanswerAskResult &&
|
|
17244
|
+
targetEntry.type === "message" &&
|
|
17245
|
+
targetEntry.message.role === "toolResult" &&
|
|
17246
|
+
targetEntry.message.toolName === "ask"
|
|
17247
|
+
) {
|
|
17248
|
+
const toolCallId = targetEntry.message.toolCallId;
|
|
17249
|
+
const questions = this.#recoverAskReanswerQuestions(targetEntry.parentId, toolCallId);
|
|
17250
|
+
if (questions) {
|
|
17251
|
+
return { cancelled: false, reopenAsk: { toolCallId, questions } };
|
|
17252
|
+
}
|
|
17253
|
+
// Original arguments couldn't be recovered (corrupted/legacy session
|
|
17254
|
+
// data) — fall through to a plain leaf move so navigation still works.
|
|
17255
|
+
}
|
|
17256
|
+
|
|
17257
|
+
// Collect entries to summarize (from old leaf to common ancestor). For an
|
|
17258
|
+
// `ask` re-answer completion, the branch point is `targetEntry.parentId`
|
|
17259
|
+
// (the new sibling toolResult lands there, not on `targetId`) — anchor
|
|
17260
|
+
// the collection there too, or the old answer entry is neither on the
|
|
17261
|
+
// new branch nor included in the summary (chatgpt-codex review on
|
|
17262
|
+
// #5895).
|
|
17263
|
+
const summaryAnchorId =
|
|
17264
|
+
options.reanswerAskResult !== undefined &&
|
|
17265
|
+
targetEntry.type === "message" &&
|
|
17266
|
+
targetEntry.message.role === "toolResult" &&
|
|
17267
|
+
targetEntry.message.toolName === "ask" &&
|
|
17268
|
+
targetEntry.parentId !== null
|
|
17269
|
+
? targetEntry.parentId
|
|
17270
|
+
: targetId;
|
|
17054
17271
|
const { entries: entriesToSummarize, commonAncestorId } = collectEntriesForBranchSummary(
|
|
17055
17272
|
this.sessionManager,
|
|
17056
17273
|
oldLeafId,
|
|
17057
|
-
|
|
17274
|
+
summaryAnchorId,
|
|
17058
17275
|
);
|
|
17059
17276
|
|
|
17060
17277
|
// Prepare event data
|
|
@@ -17150,6 +17367,27 @@ export class AgentSession {
|
|
|
17150
17367
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
17151
17368
|
.map(c => c.text)
|
|
17152
17369
|
.join("");
|
|
17370
|
+
} else if (
|
|
17371
|
+
targetEntry.type === "message" &&
|
|
17372
|
+
targetEntry.message.role === "toolResult" &&
|
|
17373
|
+
targetEntry.message.toolName === "ask" &&
|
|
17374
|
+
options.reanswerAskResult
|
|
17375
|
+
) {
|
|
17376
|
+
// `ask` toolResult, second pass: the caller re-opened the picker and
|
|
17377
|
+
// is handing back a fresh answer. Branch a *new* sibling toolResult
|
|
17378
|
+
// off the same `ask` toolCall instead of reusing `targetId` — the
|
|
17379
|
+
// original answer's branch stays reachable (issue #5642).
|
|
17380
|
+
const reanswer = options.reanswerAskResult;
|
|
17381
|
+
const toolResultMessage: ToolResultMessage = {
|
|
17382
|
+
role: "toolResult",
|
|
17383
|
+
toolCallId: targetEntry.message.toolCallId,
|
|
17384
|
+
toolName: "ask",
|
|
17385
|
+
content: reanswer.content,
|
|
17386
|
+
details: reanswer.details,
|
|
17387
|
+
isError: reanswer.isError === true,
|
|
17388
|
+
timestamp: Date.now(),
|
|
17389
|
+
};
|
|
17390
|
+
newLeafId = this.sessionManager.appendMessageToBranch(toolResultMessage, targetEntry.parentId);
|
|
17153
17391
|
} else {
|
|
17154
17392
|
// Non-user message (or a user-invoked skill-prompt injection): land the
|
|
17155
17393
|
// leaf on the selected node so it stays on the active branch. Skill
|
|
@@ -17212,6 +17450,73 @@ export class AgentSession {
|
|
|
17212
17450
|
return { editorText, cancelled: false, summaryEntry, sessionContext: stateContext };
|
|
17213
17451
|
}
|
|
17214
17452
|
|
|
17453
|
+
/**
|
|
17454
|
+
* Look up the `ask` toolCall's persisted `arguments` and validate them
|
|
17455
|
+
* back into `questions`, for `/tree` `ask` re-answer (issue #5642). Walks
|
|
17456
|
+
* up from the toolResult's parent past any interleaved ancestor entries
|
|
17457
|
+
* — sibling toolResults from other tool calls in the same turn (`ask`
|
|
17458
|
+
* runs `exclusive`, which only serializes *execution*, not persistence
|
|
17459
|
+
* order — roboomp review on #5895), and bookkeeping entries such as the
|
|
17460
|
+
* `tool_execution_start` custom entry `#recordToolExecutionStart()`
|
|
17461
|
+
* appends before every toolResult in real persisted sessions (chatgpt-codex
|
|
17462
|
+
* review on #5895) — until it finds the assistant entry that actually
|
|
17463
|
+
* emitted `toolCallId`. Stops at a `user` message (turn boundary) or a
|
|
17464
|
+
* dead end. Returns `undefined` when no ancestor entry holds a matching
|
|
17465
|
+
* `ask` toolCall, or the arguments can't be resolved — the caller falls
|
|
17466
|
+
* back to a plain leaf move rather than opening a picker with bad data.
|
|
17467
|
+
*/
|
|
17468
|
+
#recoverAskReanswerQuestions(parentId: string | null, toolCallId: string): AskToolInput["questions"] | undefined {
|
|
17469
|
+
let current = parentId;
|
|
17470
|
+
while (current !== null) {
|
|
17471
|
+
const entry = this.sessionManager.getEntry(current);
|
|
17472
|
+
if (!entry) return undefined;
|
|
17473
|
+
if (entry.type === "message") {
|
|
17474
|
+
if (entry.message.role === "assistant") {
|
|
17475
|
+
const toolCall = entry.message.content.find(
|
|
17476
|
+
(block): block is AgentToolCall => block.type === "toolCall" && block.id === toolCallId,
|
|
17477
|
+
);
|
|
17478
|
+
if (!toolCall) return undefined;
|
|
17479
|
+
if (toolCall.name !== "ask") return undefined;
|
|
17480
|
+
const args = this.#obfuscator?.hasSecrets()
|
|
17481
|
+
? deobfuscateToolArguments(this.#obfuscator, toolCall.arguments)
|
|
17482
|
+
: toolCall.arguments;
|
|
17483
|
+
return recoverAskQuestions(args);
|
|
17484
|
+
}
|
|
17485
|
+
if (entry.message.role === "user") return undefined;
|
|
17486
|
+
}
|
|
17487
|
+
current = entry.parentId;
|
|
17488
|
+
}
|
|
17489
|
+
return undefined;
|
|
17490
|
+
}
|
|
17491
|
+
|
|
17492
|
+
/**
|
|
17493
|
+
* Build a standalone `AgentToolContext` for running `AskTool.execute()`
|
|
17494
|
+
* outside a normal agent turn, for `/tree` `ask` re-answer (issue #5642).
|
|
17495
|
+
* `SelectorController` has no reachable `ToolContextStore` (that store is
|
|
17496
|
+
* built inside `sdk.ts` and never threaded through to mode controllers),
|
|
17497
|
+
* so this mirrors `refreshMCPTools()`'s `getCustomToolContext` factory
|
|
17498
|
+
* with real session state instead of a `{ ... } as unknown as
|
|
17499
|
+
* AgentToolContext` cast that could silently compile with an incomplete
|
|
17500
|
+
* context (roboomp review on #5895) — every `CustomToolContext` field is
|
|
17501
|
+
* backed by live session state, so a future required field fails to
|
|
17502
|
+
* compile here instead of surfacing as `undefined` at runtime.
|
|
17503
|
+
*/
|
|
17504
|
+
buildAskReanswerContext(uiContext: ExtensionUIContext): AgentToolContext {
|
|
17505
|
+
return {
|
|
17506
|
+
sessionManager: this.sessionManager,
|
|
17507
|
+
modelRegistry: this.#modelRegistry,
|
|
17508
|
+
model: this.model,
|
|
17509
|
+
isIdle: () => !this.isStreaming,
|
|
17510
|
+
hasQueuedMessages: () => this.queuedMessageCount > 0,
|
|
17511
|
+
abort: () => {
|
|
17512
|
+
this.agent.abort();
|
|
17513
|
+
},
|
|
17514
|
+
settings: this.settings,
|
|
17515
|
+
ui: uiContext,
|
|
17516
|
+
hasUI: true,
|
|
17517
|
+
};
|
|
17518
|
+
}
|
|
17519
|
+
|
|
17215
17520
|
/**
|
|
17216
17521
|
* Get all user messages from session for branch selector.
|
|
17217
17522
|
*/
|