@dreb/coding-agent 2.71.0 → 2.73.0
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/README.md +3 -3
- package/dist/core/agent-session.d.ts +37 -9
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +226 -149
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/thinking.d.ts +2 -0
- package/dist/core/thinking.d.ts.map +1 -1
- package/dist/core/thinking.js +20 -14
- package/dist/core/thinking.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +77 -6
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +312 -64
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +6 -8
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +9 -5
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +6 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +9 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +7 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +11 -2
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/agent-models.md +2 -2
- package/docs/dashboard.md +46 -3
- package/docs/json.md +2 -2
- package/docs/models.md +1 -1
- package/docs/providers.md +26 -0
- package/docs/rpc.md +25 -6
- package/docs/settings.md +1 -1
- package/package.json +1 -1
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { copyFileSync, existsSync, mkdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { tmpdir } from "node:os";
|
|
17
17
|
import { basename, dirname, join, resolve } from "node:path";
|
|
18
|
-
import { isContextOverflow, isOutputLimitExhaustion, modelsAreEqual, resetApiProviders,
|
|
18
|
+
import { clampThinkingLevel, getAvailableThinkingLevels, isContextOverflow, isOutputLimitExhaustion, modelsAreEqual, resetApiProviders, } from "@dreb/ai";
|
|
19
19
|
import { getDocsPath, getSubagentSessionsDir } from "../config.js";
|
|
20
20
|
import { theme } from "../modes/interactive/theme/theme.js";
|
|
21
21
|
import { sleep } from "../utils/sleep.js";
|
|
@@ -41,7 +41,7 @@ import { DEFAULT_BG_PARENT_TURN_LIMIT, DEFAULT_MAX_CONCURRENT_SUBAGENTS, } from
|
|
|
41
41
|
import { createSyntheticSourceInfo } from "./source-info.js";
|
|
42
42
|
import { buildSystemPrompt } from "./system-prompt.js";
|
|
43
43
|
import { resolveThinkingDisplay } from "./thinking.js";
|
|
44
|
-
import { createAllToolDefinitions, createSubagentConcurrencyGate, discoverAgentTypes, getRunningBackgroundAgents, } from "./tools/index.js";
|
|
44
|
+
import { createAllToolDefinitions, createSubagentConcurrencyGate, discoverAgentTypes, formatAgentOutputBlock, getRunningBackgroundAgents, } from "./tools/index.js";
|
|
45
45
|
import { expandSkillContent } from "./tools/skill.js";
|
|
46
46
|
import { createToolDefinitionFromAgentTool, wrapToolDefinition } from "./tools/tool-definition-wrapper.js";
|
|
47
47
|
// ============================================================================
|
|
@@ -67,12 +67,6 @@ export function parseSkillBlock(text) {
|
|
|
67
67
|
// ============================================================================
|
|
68
68
|
// Constants
|
|
69
69
|
// ============================================================================
|
|
70
|
-
/** Standard thinking levels */
|
|
71
|
-
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
72
|
-
/** Thinking levels including xhigh (for supported models). */
|
|
73
|
-
const THINKING_LEVELS_WITH_XHIGH = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
74
|
-
/** Complete ordered scale, including the native max tier for supported models. */
|
|
75
|
-
const THINKING_LEVELS_WITH_MAX = [...THINKING_LEVELS_WITH_XHIGH, "max"];
|
|
76
70
|
const MIN_PERFORMANCE_DURATION_MS = 10;
|
|
77
71
|
// ============================================================================
|
|
78
72
|
// AgentSession Class
|
|
@@ -90,6 +84,11 @@ export class AgentSession {
|
|
|
90
84
|
_unsubscribeAgent;
|
|
91
85
|
_eventListeners = [];
|
|
92
86
|
_agentEventQueue = Promise.resolve();
|
|
87
|
+
/**
|
|
88
|
+
* Agent events queued on `_agentEventQueue` that haven't finished processing yet.
|
|
89
|
+
* Persistence happens there, so the session log lags agent state until this is 0.
|
|
90
|
+
*/
|
|
91
|
+
_agentEventsInFlight = 0;
|
|
93
92
|
/** Tracks pending steering messages for UI display. Removed when delivered. */
|
|
94
93
|
_steeringMessages = [];
|
|
95
94
|
/** Tracks pending follow-up messages for UI display. Removed when delivered. */
|
|
@@ -114,6 +113,14 @@ export class AgentSession {
|
|
|
114
113
|
// Bash execution state
|
|
115
114
|
_bashAbortController = undefined;
|
|
116
115
|
_pendingBashMessages = [];
|
|
116
|
+
/**
|
|
117
|
+
* Background-agent cancellation notices that arrived while the parent was busy (see
|
|
118
|
+
* `_canDeliverCancellationNotices`). Appending them then could land a user message
|
|
119
|
+
* between an assistant tool call and its result, or ahead of messages not yet persisted.
|
|
120
|
+
* They are held until the parent is idle again, or are persisted to their session before
|
|
121
|
+
* it is replaced.
|
|
122
|
+
*/
|
|
123
|
+
_pendingCancellationNotices = [];
|
|
117
124
|
// Extension system
|
|
118
125
|
_extensionRunner = undefined;
|
|
119
126
|
_turnIndex = 0;
|
|
@@ -652,11 +659,19 @@ export class AgentSession {
|
|
|
652
659
|
if (cancelled) {
|
|
653
660
|
parts.push("This agent was cancelled by the user.");
|
|
654
661
|
}
|
|
655
|
-
|
|
662
|
+
const failed = !cancelled && (result.exitCode !== 0 || !!result.errorMessage);
|
|
663
|
+
if (failed) {
|
|
656
664
|
parts.push(`Error: ${result.errorMessage || "unknown"}`);
|
|
657
665
|
}
|
|
666
|
+
if (result.notes && result.notes.length > 0) {
|
|
667
|
+
parts.push(...result.notes);
|
|
668
|
+
}
|
|
658
669
|
if (result.output) {
|
|
659
|
-
|
|
670
|
+
// The agent's own words are delimited from the system lines around them. A chain's
|
|
671
|
+
// output is already a per-step report with each step's output delimited
|
|
672
|
+
// (formatSingleResult), so it is not wrapped again.
|
|
673
|
+
const isChain = result.steps !== undefined;
|
|
674
|
+
parts.push(isChain ? result.output : formatAgentOutputBlock(result.output, cancelled || failed));
|
|
660
675
|
}
|
|
661
676
|
if (result.sessionFile) {
|
|
662
677
|
parts.push(`Session log: ${result.sessionFile}`);
|
|
@@ -680,13 +695,15 @@ export class AgentSession {
|
|
|
680
695
|
timestamp: Date.now(),
|
|
681
696
|
};
|
|
682
697
|
if (cancelled) {
|
|
683
|
-
// Cancelled by user (Esc) — add to
|
|
684
|
-
// but do NOT trigger a response
|
|
698
|
+
// Cancelled by the user (single-agent stop, Esc, or the parent stop) — add to
|
|
699
|
+
// context and render in chat, but do NOT trigger a response: the user chose to
|
|
700
|
+
// stop this work, and waking the parent could relaunch it behind their back.
|
|
701
|
+
// While the parent is busy (streaming, still persisting the run, retrying, or
|
|
702
|
+
// compacting) the notice is deferred, so it can never split an assistant tool call
|
|
703
|
+
// from its result.
|
|
704
|
+
this._pendingCancellationNotices.push(message);
|
|
685
705
|
try {
|
|
686
|
-
this.
|
|
687
|
-
this.sessionManager.appendMessage(message);
|
|
688
|
-
this._emit({ type: "message_start", message });
|
|
689
|
-
this._emit({ type: "message_end", message });
|
|
706
|
+
this._flushPendingCancellationNotices();
|
|
690
707
|
}
|
|
691
708
|
catch (err) {
|
|
692
709
|
log.warn(`[subagent] Failed to deliver cancellation message for agent ${agentId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -702,6 +719,8 @@ export class AgentSession {
|
|
|
702
719
|
this.agent.steer(message);
|
|
703
720
|
}
|
|
704
721
|
else {
|
|
722
|
+
// Earlier deferred cancellation notices precede this completion in context.
|
|
723
|
+
this._flushPendingCancellationNotices();
|
|
705
724
|
// Fallback: if streaming started between the isStreaming check and this call, deliver as follow-up
|
|
706
725
|
this.agent.prompt(message).catch((promptErr) => {
|
|
707
726
|
log.warn(`[subagent] prompt() failed for background agent ${agentId}: ${promptErr instanceof Error ? promptErr.message : String(promptErr)}`);
|
|
@@ -720,7 +739,8 @@ export class AgentSession {
|
|
|
720
739
|
type: "background_agent_end",
|
|
721
740
|
agentId,
|
|
722
741
|
agentType: result.agent,
|
|
723
|
-
success: result.exitCode === 0,
|
|
742
|
+
success: !cancelled && result.exitCode === 0,
|
|
743
|
+
cancelled,
|
|
724
744
|
model: result.model,
|
|
725
745
|
thinking: result.thinking,
|
|
726
746
|
steps: result.steps,
|
|
@@ -750,6 +770,7 @@ export class AgentSession {
|
|
|
750
770
|
// _processAgentEvent, slow earlier queued events can delay agent_end processing
|
|
751
771
|
// and waitForRetry() can miss the in-flight retry.
|
|
752
772
|
this._createRetryPromiseForAgentEnd(event);
|
|
773
|
+
this._agentEventsInFlight++;
|
|
753
774
|
this._agentEventQueue = this._agentEventQueue.then(() => this._processAgentEvent(event), () => {
|
|
754
775
|
// Prior event failed — already warned by the .catch() below.
|
|
755
776
|
// Swallow the old rejection and continue with the current event.
|
|
@@ -853,101 +874,119 @@ export class AgentSession {
|
|
|
853
874
|
return undefined;
|
|
854
875
|
}
|
|
855
876
|
async _processAgentEvent(event) {
|
|
856
|
-
//
|
|
857
|
-
//
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
if (
|
|
862
|
-
|
|
863
|
-
const
|
|
864
|
-
if (
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
877
|
+
// The finally runs inline: an extra async wrapper here would add microtask hops to every
|
|
878
|
+
// queued event and delay event delivery relative to prompt() resolution.
|
|
879
|
+
try {
|
|
880
|
+
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
881
|
+
// This ensures the UI sees the updated queue state
|
|
882
|
+
if (event.type === "message_start" && event.message.role === "user") {
|
|
883
|
+
this._overflowRecoveryAttempted = false;
|
|
884
|
+
const messageText = this._getUserMessageText(event.message);
|
|
885
|
+
if (messageText) {
|
|
886
|
+
// Check steering queue first
|
|
887
|
+
const steeringIndex = this._steeringMessages.findIndex((message) => message.text === messageText);
|
|
888
|
+
if (steeringIndex !== -1) {
|
|
889
|
+
this._steeringMessages.splice(steeringIndex, 1);
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
// Check follow-up queue
|
|
893
|
+
const followUpIndex = this._followUpMessages.findIndex((message) => message.text === messageText);
|
|
894
|
+
if (followUpIndex !== -1) {
|
|
895
|
+
this._followUpMessages.splice(followUpIndex, 1);
|
|
896
|
+
}
|
|
872
897
|
}
|
|
873
898
|
}
|
|
874
899
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
this.sessionManager.appendCustomMessageEntry(event.message.customType, event.message.content, event.message.display, event.message.details);
|
|
897
|
-
}
|
|
898
|
-
else if (event.message.role === "user" ||
|
|
899
|
-
event.message.role === "assistant" ||
|
|
900
|
-
event.message.role === "toolResult") {
|
|
901
|
-
// Regular LLM message - persist as SessionMessageEntry
|
|
902
|
-
this.sessionManager.appendMessage(event.message);
|
|
903
|
-
}
|
|
904
|
-
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
905
|
-
// Track assistant message for auto-compaction (checked on agent_end)
|
|
906
|
-
if (event.message.role === "assistant") {
|
|
907
|
-
this._lastAssistantMessage = event.message;
|
|
908
|
-
const assistantMsg = event.message;
|
|
909
|
-
if (assistantMsg.stopReason !== "error") {
|
|
910
|
-
this._overflowRecoveryAttempted = false;
|
|
900
|
+
// Emit to extensions first
|
|
901
|
+
await this._emitExtensionEvent(event);
|
|
902
|
+
// Notify all listeners
|
|
903
|
+
this._emit(event);
|
|
904
|
+
// Keep a small, mode-independent rolling context for the headless arbiter.
|
|
905
|
+
if (event.type === "message_end") {
|
|
906
|
+
this._dispatchArbiter.onMessageEnd(event.message);
|
|
907
|
+
}
|
|
908
|
+
else if (event.type === "tool_execution_end") {
|
|
909
|
+
this._dispatchArbiter.onToolEnd({
|
|
910
|
+
toolName: event.toolName,
|
|
911
|
+
isError: event.isError,
|
|
912
|
+
result: event.result,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
// Handle session persistence
|
|
916
|
+
if (event.type === "message_end") {
|
|
917
|
+
// Check if this is a custom message from extensions
|
|
918
|
+
if (event.message.role === "custom") {
|
|
919
|
+
// Persist as CustomMessageEntry
|
|
920
|
+
this.sessionManager.appendCustomMessageEntry(event.message.customType, event.message.content, event.message.display, event.message.details);
|
|
911
921
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
success: true,
|
|
918
|
-
attempt: this._retryAttempt,
|
|
919
|
-
});
|
|
920
|
-
this._retryAttempt = 0;
|
|
922
|
+
else if (event.message.role === "user" ||
|
|
923
|
+
event.message.role === "assistant" ||
|
|
924
|
+
event.message.role === "toolResult") {
|
|
925
|
+
// Regular LLM message - persist as SessionMessageEntry
|
|
926
|
+
this.sessionManager.appendMessage(event.message);
|
|
921
927
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
928
|
+
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
929
|
+
// Track assistant message for auto-compaction (checked on agent_end)
|
|
930
|
+
if (event.message.role === "assistant") {
|
|
931
|
+
this._lastAssistantMessage = event.message;
|
|
932
|
+
const assistantMsg = event.message;
|
|
933
|
+
if (assistantMsg.stopReason !== "error") {
|
|
934
|
+
this._overflowRecoveryAttempted = false;
|
|
935
|
+
}
|
|
936
|
+
// Reset retry counter immediately on successful assistant response
|
|
937
|
+
// This prevents accumulation across multiple LLM calls within a turn
|
|
938
|
+
if (assistantMsg.stopReason !== "error" && this._retryAttempt > 0) {
|
|
939
|
+
this._emit({
|
|
940
|
+
type: "auto_retry_end",
|
|
941
|
+
success: true,
|
|
942
|
+
attempt: this._retryAttempt,
|
|
943
|
+
});
|
|
944
|
+
this._retryAttempt = 0;
|
|
945
|
+
}
|
|
946
|
+
const durationMs = assistantMsg.durationMs ?? 0;
|
|
947
|
+
if (assistantMsg.stopReason !== "error" &&
|
|
948
|
+
assistantMsg.stopReason !== "aborted" &&
|
|
949
|
+
assistantMsg.usage.output > 0 &&
|
|
950
|
+
durationMs >= MIN_PERFORMANCE_DURATION_MS) {
|
|
951
|
+
this.performanceTracker.record({
|
|
952
|
+
timestamp: new Date().toISOString(),
|
|
953
|
+
sessionId: this.sessionId,
|
|
954
|
+
provider: assistantMsg.provider,
|
|
955
|
+
modelId: assistantMsg.model,
|
|
956
|
+
outputTokens: assistantMsg.usage.output,
|
|
957
|
+
durationMs,
|
|
958
|
+
tps: (assistantMsg.usage.output * 1000) / durationMs,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
936
961
|
}
|
|
937
962
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
963
|
+
// Check auto-retry and auto-compaction after agent completes
|
|
964
|
+
if (event.type === "agent_end") {
|
|
965
|
+
if (this._lastAssistantMessage) {
|
|
966
|
+
const msg = this._lastAssistantMessage;
|
|
967
|
+
this._lastAssistantMessage = undefined;
|
|
968
|
+
// Check for retryable errors first (overloaded, rate limit, server errors)
|
|
969
|
+
if (this._isRetryableError(msg)) {
|
|
970
|
+
const didRetry = await this._handleRetryableError(msg);
|
|
971
|
+
// Retry was initiated, don't proceed to compaction. Deferred cancellation
|
|
972
|
+
// notices stay held while the retry is pending (isRetrying) and are delivered
|
|
973
|
+
// after the retried run's agent_end.
|
|
974
|
+
if (didRetry)
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
this._resolveRetry();
|
|
978
|
+
await this._checkCompaction(msg);
|
|
979
|
+
}
|
|
980
|
+
// Deferred cancellation notices are delivered in the finally below once this
|
|
981
|
+
// agent_end has finished processing (after any retry decision or compaction above).
|
|
948
982
|
}
|
|
949
|
-
|
|
950
|
-
|
|
983
|
+
}
|
|
984
|
+
finally {
|
|
985
|
+
this._agentEventsInFlight--;
|
|
986
|
+
// This event is handled and persisted. If the parent is now idle, deliver cancellation
|
|
987
|
+
// notices deferred while it was busy. After a run's final agent_end, that places them
|
|
988
|
+
// after everything the run persisted.
|
|
989
|
+
this._flushPendingCancellationNotices();
|
|
951
990
|
}
|
|
952
991
|
}
|
|
953
992
|
/** Resolve the pending retry promise */
|
|
@@ -1410,8 +1449,9 @@ export class AgentSession {
|
|
|
1410
1449
|
}
|
|
1411
1450
|
return;
|
|
1412
1451
|
}
|
|
1413
|
-
// Flush any pending bash messages before the new prompt
|
|
1452
|
+
// Flush any pending bash messages and cancellation notices before the new prompt
|
|
1414
1453
|
this._flushPendingBashMessages();
|
|
1454
|
+
this._flushPendingCancellationNotices();
|
|
1415
1455
|
// Validate model
|
|
1416
1456
|
if (!this.model) {
|
|
1417
1457
|
throw new Error("No model selected.\n\n" +
|
|
@@ -1775,6 +1815,7 @@ export class AgentSession {
|
|
|
1775
1815
|
}
|
|
1776
1816
|
this._disconnectFromAgent();
|
|
1777
1817
|
await this.abort();
|
|
1818
|
+
await this._persistPendingCancellationNoticesBeforeSessionChange();
|
|
1778
1819
|
this.agent.reset();
|
|
1779
1820
|
this._dispatchArbiter.clearContext();
|
|
1780
1821
|
this.sessionManager.newSession({ parentSession: options?.parentSession, cwd: this.cwd });
|
|
@@ -1949,14 +1990,16 @@ export class AgentSession {
|
|
|
1949
1990
|
* Saves to session and settings only if the level actually changes.
|
|
1950
1991
|
*/
|
|
1951
1992
|
setThinkingLevel(level) {
|
|
1952
|
-
const
|
|
1953
|
-
const effectiveLevel = availableLevels.includes(level) ? level : this._clampThinkingLevel(level, availableLevels);
|
|
1993
|
+
const effectiveLevel = clampThinkingLevel(this.model, level);
|
|
1954
1994
|
// Only persist if actually changing
|
|
1955
1995
|
const isChanging = effectiveLevel !== this.agent.state.thinkingLevel;
|
|
1956
1996
|
this.agent.setThinkingLevel(effectiveLevel);
|
|
1957
1997
|
if (isChanging) {
|
|
1958
1998
|
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
|
1959
|
-
|
|
1999
|
+
// A capability clamp (non-reasoning model, `off` on an always-thinking model,
|
|
2000
|
+
// unsupported xhigh/max) must not overwrite the saved preference, so the next
|
|
2001
|
+
// capable model can honor it again.
|
|
2002
|
+
if (this.supportsThinking() && effectiveLevel === level) {
|
|
1960
2003
|
this.settingsManager.setDefaultThinkingLevel(effectiveLevel);
|
|
1961
2004
|
}
|
|
1962
2005
|
}
|
|
@@ -1976,25 +2019,12 @@ export class AgentSession {
|
|
|
1976
2019
|
return nextLevel;
|
|
1977
2020
|
}
|
|
1978
2021
|
/**
|
|
1979
|
-
* Get available thinking levels for current model.
|
|
1980
|
-
*
|
|
2022
|
+
* Get available thinking levels for current model, in ascending order.
|
|
2023
|
+
* `off` is omitted for models whose thinking cannot be turned off (Claude Opus 5.5+),
|
|
2024
|
+
* and `xhigh` / `max` are included independently per model capability.
|
|
1981
2025
|
*/
|
|
1982
2026
|
getAvailableThinkingLevels() {
|
|
1983
|
-
|
|
1984
|
-
return ["off"];
|
|
1985
|
-
if (this.supportsMaxThinking())
|
|
1986
|
-
return THINKING_LEVELS_WITH_MAX;
|
|
1987
|
-
return this.supportsXhighThinking() ? THINKING_LEVELS_WITH_XHIGH : THINKING_LEVELS;
|
|
1988
|
-
}
|
|
1989
|
-
/**
|
|
1990
|
-
* Check if current model supports xhigh thinking level.
|
|
1991
|
-
*/
|
|
1992
|
-
supportsXhighThinking() {
|
|
1993
|
-
return this.model ? supportsXhigh(this.model) : false;
|
|
1994
|
-
}
|
|
1995
|
-
/** Check if the current model supports the native max thinking level. */
|
|
1996
|
-
supportsMaxThinking() {
|
|
1997
|
-
return this.model ? supportsMax(this.model) : false;
|
|
2027
|
+
return getAvailableThinkingLevels(this.model);
|
|
1998
2028
|
}
|
|
1999
2029
|
/**
|
|
2000
2030
|
* Check if current model supports thinking/reasoning.
|
|
@@ -2006,29 +2036,17 @@ export class AgentSession {
|
|
|
2006
2036
|
if (explicitLevel !== undefined) {
|
|
2007
2037
|
return explicitLevel;
|
|
2008
2038
|
}
|
|
2039
|
+
const savedLevel = this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;
|
|
2009
2040
|
if (!this.supportsThinking()) {
|
|
2010
|
-
return
|
|
2041
|
+
return savedLevel;
|
|
2011
2042
|
}
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
const requestedIndex = ordered.indexOf(level);
|
|
2018
|
-
if (requestedIndex === -1) {
|
|
2019
|
-
return availableLevels[0] ?? "off";
|
|
2020
|
-
}
|
|
2021
|
-
for (let i = requestedIndex; i < ordered.length; i++) {
|
|
2022
|
-
const candidate = ordered[i];
|
|
2023
|
-
if (available.has(candidate))
|
|
2024
|
-
return candidate;
|
|
2025
|
-
}
|
|
2026
|
-
for (let i = requestedIndex - 1; i >= 0; i--) {
|
|
2027
|
-
const candidate = ordered[i];
|
|
2028
|
-
if (available.has(candidate))
|
|
2029
|
-
return candidate;
|
|
2043
|
+
// When the current level is only the capability clamp of the saved preference
|
|
2044
|
+
// (e.g. saved `off` shown as `minimal` on Opus 5.5), carry the saved preference
|
|
2045
|
+
// to the next model instead of the clamped value.
|
|
2046
|
+
if (clampThinkingLevel(this.model, savedLevel) === this.thinkingLevel) {
|
|
2047
|
+
return savedLevel;
|
|
2030
2048
|
}
|
|
2031
|
-
return
|
|
2049
|
+
return this.thinkingLevel;
|
|
2032
2050
|
}
|
|
2033
2051
|
// =========================================================================
|
|
2034
2052
|
// Queue Mode Management
|
|
@@ -2148,6 +2166,9 @@ export class AgentSession {
|
|
|
2148
2166
|
finally {
|
|
2149
2167
|
this._compactionAbortController = undefined;
|
|
2150
2168
|
this._reconnectToAgent();
|
|
2169
|
+
// Cancellation notices that arrived while compacting were held back; deliver them
|
|
2170
|
+
// now that the compacted context is in place.
|
|
2171
|
+
this._flushPendingCancellationNotices();
|
|
2151
2172
|
}
|
|
2152
2173
|
}
|
|
2153
2174
|
/**
|
|
@@ -3146,6 +3167,60 @@ export class AgentSession {
|
|
|
3146
3167
|
get hasPendingBashMessages() {
|
|
3147
3168
|
return this._pendingBashMessages.length > 0;
|
|
3148
3169
|
}
|
|
3170
|
+
/**
|
|
3171
|
+
* Whether deferred cancellation notices can be appended now without landing inside
|
|
3172
|
+
* work that is still changing the transcript. Not while the agent streams, while
|
|
3173
|
+
* earlier agent events are still being processed or persisted (the session log would
|
|
3174
|
+
* get the notice ahead of them), while an auto-retry is pending (its backoff or the
|
|
3175
|
+
* retried run), or while compaction or branch summarization runs.
|
|
3176
|
+
*/
|
|
3177
|
+
_canDeliverCancellationNotices() {
|
|
3178
|
+
return !this.agent.state.isStreaming && this._agentEventsInFlight === 0 && !this.isRetrying && !this.isCompacting;
|
|
3179
|
+
}
|
|
3180
|
+
/**
|
|
3181
|
+
* Append deferred background-agent cancellation notices to agent state and the session
|
|
3182
|
+
* log, in arrival order, and render them, if the parent is idle (see
|
|
3183
|
+
* `_canDeliverCancellationNotices`). Otherwise they stay queued. They are retried after
|
|
3184
|
+
* each processed agent event (`_processAgentEvent`), before the next prompt, and after manual compaction or
|
|
3185
|
+
* branch navigation. Never triggers a response.
|
|
3186
|
+
*/
|
|
3187
|
+
_flushPendingCancellationNotices() {
|
|
3188
|
+
if (this._pendingCancellationNotices.length === 0 || !this._canDeliverCancellationNotices())
|
|
3189
|
+
return;
|
|
3190
|
+
this._appendPendingCancellationNotices();
|
|
3191
|
+
}
|
|
3192
|
+
_appendPendingCancellationNotices() {
|
|
3193
|
+
while (this._pendingCancellationNotices.length > 0) {
|
|
3194
|
+
const message = this._pendingCancellationNotices.shift();
|
|
3195
|
+
this.agent.appendMessage(message);
|
|
3196
|
+
this.sessionManager.appendMessage(message);
|
|
3197
|
+
this._emit({ type: "message_start", message });
|
|
3198
|
+
this._emit({ type: "message_end", message });
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
/**
|
|
3202
|
+
* Persist cancellation notices still deferred for the current session before it is replaced
|
|
3203
|
+
* (new session, resume/switch, fork). The run they wait on will never end in this
|
|
3204
|
+
* session, so they are written now instead of being dropped. First every event that session
|
|
3205
|
+
* already produced is processed, so the notices land after the last message it persisted.
|
|
3206
|
+
*
|
|
3207
|
+
* Callers that abort the run first (new session, switch) get a normal append to agent state
|
|
3208
|
+
* and the log. A fork keeps the run going, so if it is still streaming the notices are only
|
|
3209
|
+
* written to the outgoing session's log. Live context is rebuilt from the fork, and must
|
|
3210
|
+
* not get a user message between a tool call and its result.
|
|
3211
|
+
*/
|
|
3212
|
+
async _persistPendingCancellationNoticesBeforeSessionChange() {
|
|
3213
|
+
if (this._pendingCancellationNotices.length === 0)
|
|
3214
|
+
return;
|
|
3215
|
+
await this._agentEventQueue.catch(() => undefined);
|
|
3216
|
+
if (this.agent.state.isStreaming) {
|
|
3217
|
+
for (const message of this._pendingCancellationNotices.splice(0)) {
|
|
3218
|
+
this.sessionManager.appendMessage(message);
|
|
3219
|
+
}
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
3222
|
+
this._appendPendingCancellationNotices();
|
|
3223
|
+
}
|
|
3149
3224
|
/**
|
|
3150
3225
|
* Flush pending bash messages to agent state and session.
|
|
3151
3226
|
* Called after agent turn completes to maintain proper message ordering.
|
|
@@ -3196,6 +3271,7 @@ export class AgentSession {
|
|
|
3196
3271
|
}
|
|
3197
3272
|
this._disconnectFromAgent();
|
|
3198
3273
|
await this.abort();
|
|
3274
|
+
await this._persistPendingCancellationNoticesBeforeSessionChange();
|
|
3199
3275
|
this._steeringMessages = [];
|
|
3200
3276
|
this._followUpMessages = [];
|
|
3201
3277
|
this._pendingNextTurnMessages = [];
|
|
@@ -3229,10 +3305,7 @@ export class AgentSession {
|
|
|
3229
3305
|
this.setThinkingLevel(sessionContext.thinkingLevel);
|
|
3230
3306
|
}
|
|
3231
3307
|
else {
|
|
3232
|
-
const
|
|
3233
|
-
const effectiveLevel = availableLevels.includes(defaultThinkingLevel)
|
|
3234
|
-
? defaultThinkingLevel
|
|
3235
|
-
: this._clampThinkingLevel(defaultThinkingLevel, availableLevels);
|
|
3308
|
+
const effectiveLevel = clampThinkingLevel(this.model, defaultThinkingLevel);
|
|
3236
3309
|
this.agent.setThinkingLevel(effectiveLevel);
|
|
3237
3310
|
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
|
3238
3311
|
}
|
|
@@ -3329,6 +3402,8 @@ export class AgentSession {
|
|
|
3329
3402
|
}
|
|
3330
3403
|
skipConversationRestore = result?.skipConversationRestore ?? false;
|
|
3331
3404
|
}
|
|
3405
|
+
// Cancellation notices belong to the outgoing session: persist them there.
|
|
3406
|
+
await this._persistPendingCancellationNoticesBeforeSessionChange();
|
|
3332
3407
|
// Clear pending messages (bound to old session state)
|
|
3333
3408
|
this._pendingNextTurnMessages = [];
|
|
3334
3409
|
branch(previousSessionFile);
|
|
@@ -3540,6 +3615,8 @@ export class AgentSession {
|
|
|
3540
3615
|
}
|
|
3541
3616
|
finally {
|
|
3542
3617
|
this._branchSummaryAbortController = undefined;
|
|
3618
|
+
// Deliver cancellation notices held back while the branch summary was generated.
|
|
3619
|
+
this._flushPendingCancellationNotices();
|
|
3543
3620
|
}
|
|
3544
3621
|
}
|
|
3545
3622
|
/**
|