@oh-my-pi/pi-coding-agent 15.10.11 → 15.11.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/CHANGELOG.md +103 -2
- package/dist/cli.js +5790 -5731
- package/dist/types/async/index.d.ts +0 -1
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
- package/dist/types/cli-commands.d.ts +12 -0
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +85 -34
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
- package/dist/types/eval/py/executor.d.ts +5 -0
- package/dist/types/eval/py/kernel.d.ts +6 -1
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +3 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/history-protocol.d.ts +14 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/irc/bus.d.ts +66 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/runtime.d.ts +4 -0
- package/dist/types/memory-backend/types.d.ts +66 -1
- package/dist/types/modes/components/agent-hub.d.ts +30 -0
- package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
- package/dist/types/modes/components/welcome.d.ts +3 -9
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/index.d.ts +3 -3
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/oauth-manual-input.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
- package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
- package/dist/types/modes/setup-wizard/index.d.ts +5 -1
- package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +5 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +51 -0
- package/dist/types/registry/agent-registry.d.ts +16 -5
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +8 -2
- package/dist/types/session/agent-session.d.ts +49 -32
- package/dist/types/session/messages.d.ts +2 -4
- package/dist/types/session/session-history-format.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +21 -3
- package/dist/types/session/streaming-output.d.ts +46 -0
- package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +1 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +12 -2
- package/dist/types/task/index.d.ts +13 -6
- package/dist/types/task/output-manager.d.ts +0 -7
- package/dist/types/task/repair-args.d.ts +8 -7
- package/dist/types/task/types.d.ts +63 -51
- package/dist/types/thinking.d.ts +4 -0
- package/dist/types/tiny/title-client.d.ts +11 -0
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/find.d.ts +0 -11
- package/dist/types/tools/grouped-file-output.d.ts +0 -49
- package/dist/types/tools/index.d.ts +7 -3
- package/dist/types/tools/irc.d.ts +76 -38
- package/dist/types/tools/job.d.ts +7 -1
- package/dist/types/utils/git.d.ts +15 -2
- package/dist/types/utils/title-generator.d.ts +3 -2
- package/examples/extensions/with-deps/package.json +1 -0
- package/package.json +11 -10
- package/scripts/bundle-dist.ts +28 -19
- package/src/async/index.ts +0 -1
- package/src/auto-thinking/classifier.ts +1 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/gallery-fixtures/agentic.ts +230 -115
- package/src/cli/gallery-fixtures/types.ts +5 -0
- package/src/cli-commands.ts +29 -0
- package/src/cli.ts +28 -15
- package/src/commands/launch.ts +4 -0
- package/src/commit/agentic/tools/analyze-file.ts +38 -19
- package/src/commit/model-selection.ts +3 -2
- package/src/config/api-key-resolver.ts +8 -6
- package/src/config/keybindings.ts +6 -1
- package/src/config/model-registry.ts +97 -30
- package/src/config/model-resolver.ts +60 -0
- package/src/config/settings-schema.ts +99 -55
- package/src/config/settings.ts +68 -3
- package/src/edit/hashline/execute.ts +39 -2
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/eval/__tests__/agent-bridge.test.ts +5 -3
- package/src/eval/agent-bridge.ts +3 -16
- package/src/eval/completion-bridge.ts +1 -0
- package/src/eval/js/shared/prelude.txt +1 -1
- package/src/eval/py/executor.ts +29 -7
- package/src/eval/py/index.ts +6 -1
- package/src/eval/py/kernel.ts +31 -11
- package/src/eval/py/prelude.py +5 -6
- package/src/eval/py/runtime.ts +37 -0
- package/src/exec/bash-executor.ts +82 -3
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +38 -13
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/get-commands-handler.ts +2 -1
- package/src/extensibility/extensions/runner.ts +6 -1
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/shared-events.ts +2 -2
- package/src/hindsight/bank.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/internal-urls/history-protocol.ts +113 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +3 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/irc/bus.ts +292 -0
- package/src/main.ts +26 -66
- package/src/memories/index.ts +2 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/local-backend.ts +9 -0
- package/src/memory-backend/off-backend.ts +9 -0
- package/src/memory-backend/runtime.ts +66 -0
- package/src/memory-backend/types.ts +81 -1
- package/src/mnemopi/backend.ts +151 -4
- package/src/modes/acp/acp-agent.ts +119 -11
- package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
- package/src/modes/components/assistant-message.ts +19 -21
- package/src/modes/components/compaction-summary-message.ts +68 -32
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/footer.ts +3 -1
- package/src/modes/components/status-line/component.ts +118 -34
- package/src/modes/components/tool-execution.ts +31 -1
- package/src/modes/components/ttsr-notification.ts +72 -30
- package/src/modes/components/welcome.ts +9 -33
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +65 -0
- package/src/modes/controllers/extension-ui-controller.ts +8 -8
- package/src/modes/controllers/input-controller.ts +19 -2
- package/src/modes/controllers/mcp-command-controller.ts +38 -3
- package/src/modes/controllers/selector-controller.ts +21 -17
- package/src/modes/index.ts +3 -21
- package/src/modes/interactive-mode.ts +47 -22
- package/src/modes/oauth-manual-input.ts +30 -3
- package/src/modes/rpc/rpc-client.ts +154 -3
- package/src/modes/rpc/rpc-mode.ts +97 -12
- package/src/modes/rpc/rpc-subagents.ts +265 -0
- package/src/modes/rpc/rpc-types.ts +81 -1
- package/src/modes/setup-wizard/index.ts +12 -2
- package/src/modes/setup-wizard/lazy.ts +16 -0
- package/src/modes/theme/theme.ts +18 -5
- package/src/modes/types.ts +5 -5
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +51 -49
- package/src/prompts/system/irc-incoming.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +0 -5
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/irc.md +29 -19
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/task-summary.md +5 -16
- package/src/prompts/tools/task.md +38 -29
- package/src/registry/agent-lifecycle.ts +218 -0
- package/src/registry/agent-registry.ts +16 -5
- package/src/sdk.ts +37 -10
- package/src/secrets/index.ts +8 -1
- package/src/secrets/obfuscator.ts +39 -18
- package/src/session/agent-session.ts +422 -291
- package/src/session/messages.ts +11 -78
- package/src/session/session-history-format.ts +246 -0
- package/src/session/session-manager.ts +59 -5
- package/src/session/streaming-output.ts +226 -10
- package/src/slash-commands/acp-builtins.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/types.ts +1 -1
- package/src/system-prompt.ts +14 -0
- package/src/task/executor.ts +851 -461
- package/src/task/index.ts +721 -796
- package/src/task/output-manager.ts +0 -11
- package/src/task/render.ts +148 -63
- package/src/task/repair-args.ts +21 -9
- package/src/task/types.ts +82 -66
- package/src/thinking.ts +7 -0
- package/src/tiny/title-client.ts +34 -5
- package/src/tiny/title-protocol.ts +1 -1
- package/src/tiny/worker.ts +6 -4
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +61 -10
- package/src/tools/browser/tab-worker.ts +26 -7
- package/src/tools/browser.ts +28 -1
- package/src/tools/find.ts +2 -27
- package/src/tools/grouped-file-output.ts +1 -118
- package/src/tools/image-gen.ts +11 -4
- package/src/tools/index.ts +17 -13
- package/src/tools/inspect-image.ts +1 -0
- package/src/tools/irc.ts +596 -171
- package/src/tools/job.ts +41 -7
- package/src/tools/read.ts +57 -1
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +4 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/git.ts +267 -13
- package/src/utils/title-generator.ts +24 -5
- package/dist/types/async/support.d.ts +0 -2
- package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
- package/dist/types/task/simple-mode.d.ts +0 -8
- package/src/async/support.ts +0 -5
- package/src/task/simple-mode.ts +0 -27
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type AgentState,
|
|
30
30
|
type AgentTool,
|
|
31
31
|
AppendOnlyContextManager,
|
|
32
|
+
type AsideMessage,
|
|
32
33
|
resolveTelemetry,
|
|
33
34
|
ThinkingLevel,
|
|
34
35
|
} from "@oh-my-pi/pi-agent-core";
|
|
@@ -50,12 +51,18 @@ import {
|
|
|
50
51
|
generateBranchSummary,
|
|
51
52
|
generateHandoff,
|
|
52
53
|
prepareCompaction,
|
|
54
|
+
resolveThresholdTokens,
|
|
53
55
|
type ShakeConfig,
|
|
54
56
|
type ShakeRegion,
|
|
55
57
|
type SummaryOptions,
|
|
56
58
|
shouldCompact,
|
|
57
59
|
} from "@oh-my-pi/pi-agent-core/compaction";
|
|
58
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
DEFAULT_PRUNE_CONFIG,
|
|
62
|
+
pruneSupersededToolResults,
|
|
63
|
+
pruneToolOutputs,
|
|
64
|
+
readToolSupersedeKey,
|
|
65
|
+
} from "@oh-my-pi/pi-agent-core/compaction/pruning";
|
|
59
66
|
import type { ProtectedToolMatcher } from "@oh-my-pi/pi-agent-core/compaction/tool-protection";
|
|
60
67
|
import type {
|
|
61
68
|
AssistantMessage,
|
|
@@ -100,6 +107,7 @@ import {
|
|
|
100
107
|
relativePathWithinRoot,
|
|
101
108
|
Snowflake,
|
|
102
109
|
} from "@oh-my-pi/pi-utils";
|
|
110
|
+
import { snapcompactCompact } from "@oh-my-pi/snapcompact";
|
|
103
111
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
104
112
|
import { classifyDifficulty } from "../auto-thinking/classifier";
|
|
105
113
|
import { reset as resetCapabilities } from "../capability";
|
|
@@ -108,6 +116,7 @@ import { shouldEnableAppendOnlyContext } from "../config/append-only-context-mod
|
|
|
108
116
|
import type { ModelRegistry } from "../config/model-registry";
|
|
109
117
|
import {
|
|
110
118
|
extractExplicitThinkingSelector,
|
|
119
|
+
filterAvailableModelsByEnabledPatterns,
|
|
111
120
|
formatModelSelectorValue,
|
|
112
121
|
formatModelString,
|
|
113
122
|
getModelMatchPreferences,
|
|
@@ -162,6 +171,7 @@ import { GoalRuntime } from "../goals/runtime";
|
|
|
162
171
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
163
172
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
164
173
|
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
174
|
+
import type { IrcMessage } from "../irc/bus";
|
|
165
175
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
166
176
|
import { getMnemopiSessionState, type MnemopiSessionState, setMnemopiSessionState } from "../mnemopi/state";
|
|
167
177
|
import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
|
|
@@ -183,8 +193,12 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
|
|
|
183
193
|
};
|
|
184
194
|
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
|
185
195
|
import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" with { type: "text" };
|
|
186
|
-
import {
|
|
187
|
-
|
|
196
|
+
import {
|
|
197
|
+
deobfuscateSessionContext,
|
|
198
|
+
obfuscateProviderContext,
|
|
199
|
+
obfuscateProviderTools,
|
|
200
|
+
type SecretObfuscator,
|
|
201
|
+
} from "../secrets/obfuscator";
|
|
188
202
|
import { invalidateHostMetadata } from "../ssh/connection-manager";
|
|
189
203
|
import {
|
|
190
204
|
AUTO_THINKING,
|
|
@@ -192,6 +206,7 @@ import {
|
|
|
192
206
|
clampAutoThinkingEffort,
|
|
193
207
|
resolveProvisionalAutoLevel,
|
|
194
208
|
resolveThinkingLevelForModel,
|
|
209
|
+
shouldDisableReasoning,
|
|
195
210
|
toReasoningEffort,
|
|
196
211
|
} from "../thinking";
|
|
197
212
|
import { shutdownTinyTitleClient } from "../tiny/title-client";
|
|
@@ -223,10 +238,8 @@ import type { AuthStorage } from "./auth-storage";
|
|
|
223
238
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
224
239
|
import {
|
|
225
240
|
type BashExecutionMessage,
|
|
226
|
-
type CompactionSummaryMessage,
|
|
227
241
|
type CustomMessage,
|
|
228
242
|
convertToLlm,
|
|
229
|
-
type FileMentionMessage,
|
|
230
243
|
type PythonExecutionMessage,
|
|
231
244
|
readPendingDisplayTag,
|
|
232
245
|
SILENT_ABORT_MARKER,
|
|
@@ -252,11 +265,11 @@ export type AgentSessionEvent =
|
|
|
252
265
|
| {
|
|
253
266
|
type: "auto_compaction_start";
|
|
254
267
|
reason: "threshold" | "overflow" | "idle" | "incomplete";
|
|
255
|
-
action: "context-full" | "handoff" | "shake";
|
|
268
|
+
action: "context-full" | "handoff" | "shake" | "snapcompact";
|
|
256
269
|
}
|
|
257
270
|
| {
|
|
258
271
|
type: "auto_compaction_end";
|
|
259
|
-
action: "context-full" | "handoff" | "shake";
|
|
272
|
+
action: "context-full" | "handoff" | "shake" | "snapcompact";
|
|
260
273
|
result: CompactionResult | undefined;
|
|
261
274
|
aborted: boolean;
|
|
262
275
|
willRetry: boolean;
|
|
@@ -290,6 +303,15 @@ export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "la
|
|
|
290
303
|
const EMPTY_STOP_MAX_RETRIES = 3;
|
|
291
304
|
const RETRY_BACKOFF_MAX_DELAY_MS = 8_000;
|
|
292
305
|
const RETRY_BACKOFF_JITTER_RATIO = 0.25;
|
|
306
|
+
/**
|
|
307
|
+
* Hysteresis band for the post-shake "did we actually create headroom?" check.
|
|
308
|
+
* Shake counts as having resolved threshold pressure only when residual context
|
|
309
|
+
* lands at or below `SHAKE_RECOVERY_BAND × threshold`. Re-checking against the
|
|
310
|
+
* raw threshold lets shake keep reclaiming a trickle of the previous turn's
|
|
311
|
+
* output and land just under the line every turn, sustaining the auto-continue
|
|
312
|
+
* dead loop reported in #2275.
|
|
313
|
+
*/
|
|
314
|
+
const SHAKE_RECOVERY_BAND = 0.8;
|
|
293
315
|
|
|
294
316
|
function calculateRetryBackoffDelayMs(baseDelayMs: number, attempt: number): number {
|
|
295
317
|
const cappedDelayMs = Math.min(Math.max(0, baseDelayMs) * 2 ** Math.max(0, attempt - 1), RETRY_BACKOFF_MAX_DELAY_MS);
|
|
@@ -324,6 +346,8 @@ export interface AgentSessionConfig {
|
|
|
324
346
|
agent: Agent;
|
|
325
347
|
sessionManager: SessionManager;
|
|
326
348
|
settings: Settings;
|
|
349
|
+
/** Whether the caller explicitly requested yolo/auto-approve behavior for this session. */
|
|
350
|
+
autoApprove?: boolean;
|
|
327
351
|
/** Models to cycle through with Ctrl+P (from --models flag) */
|
|
328
352
|
scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
329
353
|
/** Initial session thinking selector. */
|
|
@@ -404,8 +428,6 @@ export interface AgentSessionConfig {
|
|
|
404
428
|
asyncJobManager?: AsyncJobManager;
|
|
405
429
|
/** Agent identity (registry id like "Main" or "Alice") used for IRC routing. */
|
|
406
430
|
agentId?: string;
|
|
407
|
-
/** Shared agent registry (for forwarding IRC observations to the main session UI). */
|
|
408
|
-
agentRegistry?: AgentRegistry;
|
|
409
431
|
/**
|
|
410
432
|
* Override the provider-facing session ID for all API requests from this session.
|
|
411
433
|
* When absent, `sessionManager.getSessionId()` is used. Needed when benchmark or
|
|
@@ -548,15 +570,15 @@ function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): strin
|
|
|
548
570
|
return `${selector.provider}/${selector.id}`;
|
|
549
571
|
}
|
|
550
572
|
|
|
551
|
-
const
|
|
573
|
+
const EPHEMERAL_REPLY_MAX_BYTES = 4096;
|
|
552
574
|
|
|
553
575
|
/**
|
|
554
|
-
* Collapse degenerate
|
|
576
|
+
* Collapse degenerate ephemeral replies (/btw, /omfg side-channel turns).
|
|
555
577
|
* Models occasionally loop on a single line (~16 reports of N-times-repeated
|
|
556
578
|
* replies); compress runs longer than 3 down to one instance + `[…N×]`, then
|
|
557
579
|
* cap at 4 KiB so a runaway reply can't flood the channel.
|
|
558
580
|
*/
|
|
559
|
-
function
|
|
581
|
+
function dedupeEphemeralReply(text: string): string {
|
|
560
582
|
if (!text) return text;
|
|
561
583
|
const lines = text.split("\n");
|
|
562
584
|
const out: string[] = [];
|
|
@@ -573,11 +595,11 @@ function dedupeIrcReply(text: string): string {
|
|
|
573
595
|
i = j;
|
|
574
596
|
}
|
|
575
597
|
let result = out.join("\n");
|
|
576
|
-
if (Buffer.byteLength(result, "utf8") >
|
|
598
|
+
if (Buffer.byteLength(result, "utf8") > EPHEMERAL_REPLY_MAX_BYTES) {
|
|
577
599
|
// Trim by characters until we're under the byte budget — handles multi-byte
|
|
578
600
|
// glyphs at the boundary without splitting them.
|
|
579
601
|
const suffix = "\n[…truncated]";
|
|
580
|
-
const budget =
|
|
602
|
+
const budget = EPHEMERAL_REPLY_MAX_BYTES - Buffer.byteLength(suffix, "utf8");
|
|
581
603
|
while (Buffer.byteLength(result, "utf8") > budget) {
|
|
582
604
|
result = result.slice(0, -1);
|
|
583
605
|
}
|
|
@@ -839,6 +861,7 @@ export class AgentSession {
|
|
|
839
861
|
readonly settings: Settings;
|
|
840
862
|
readonly yieldQueue: YieldQueue;
|
|
841
863
|
fileSnapshotStore?: InMemorySnapshotStore;
|
|
864
|
+
#autoApprove: boolean;
|
|
842
865
|
|
|
843
866
|
#powerAssertion: MacOSPowerAssertion | undefined;
|
|
844
867
|
|
|
@@ -931,13 +954,11 @@ export class AgentSession {
|
|
|
931
954
|
#activeEvalExecutions = new Set<Promise<unknown>>();
|
|
932
955
|
#evalExecutionDisposing = false;
|
|
933
956
|
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
#
|
|
937
|
-
|
|
938
|
-
// Agent identity + registry for IRC relay forwarding to the main session UI.
|
|
957
|
+
// Incoming IRC messages received while a turn was streaming; drained as
|
|
958
|
+
// non-interrupting asides at the next step boundary (see the aside provider).
|
|
959
|
+
#pendingIrcAsides: CustomMessage[] = [];
|
|
960
|
+
// Agent identity (registry id) used for IRC routing and job ownership.
|
|
939
961
|
#agentId: string | undefined;
|
|
940
|
-
#agentRegistry: AgentRegistry | undefined;
|
|
941
962
|
#providerSessionId: string | undefined;
|
|
942
963
|
#freshProviderSessionId: string | undefined;
|
|
943
964
|
#isDisposed = false;
|
|
@@ -1118,6 +1139,7 @@ export class AgentSession {
|
|
|
1118
1139
|
this.agent = config.agent;
|
|
1119
1140
|
this.sessionManager = config.sessionManager;
|
|
1120
1141
|
this.settings = config.settings;
|
|
1142
|
+
this.#autoApprove = config.autoApprove === true;
|
|
1121
1143
|
// Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
|
|
1122
1144
|
this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
|
|
1123
1145
|
this.#parentEvalSessionId = config.parentEvalSessionId;
|
|
@@ -1133,6 +1155,7 @@ export class AgentSession {
|
|
|
1133
1155
|
} else {
|
|
1134
1156
|
this.#thinkingLevel = config.thinkingLevel;
|
|
1135
1157
|
}
|
|
1158
|
+
this.#applyThinkingLevelToAgent(this.#thinkingLevel);
|
|
1136
1159
|
this.#promptTemplates = config.promptTemplates ?? [];
|
|
1137
1160
|
this.#slashCommands = config.slashCommands ?? [];
|
|
1138
1161
|
this.#extensionRunner = config.extensionRunner;
|
|
@@ -1192,7 +1215,13 @@ export class AgentSession {
|
|
|
1192
1215
|
// Background-job completions / late diagnostics are pulled into the run at
|
|
1193
1216
|
// each step boundary as non-interrupting asides (see Agent.getAsideMessages),
|
|
1194
1217
|
// so they reach the model between requests without waiting for a yield.
|
|
1195
|
-
this.agent.setAsideMessageProvider(() =>
|
|
1218
|
+
this.agent.setAsideMessageProvider(() => {
|
|
1219
|
+
const pendingIrc = this.#pendingIrcAsides;
|
|
1220
|
+
this.#pendingIrcAsides = [];
|
|
1221
|
+
const thunks: AsideMessage[] = pendingIrc.map(record => () => record);
|
|
1222
|
+
thunks.push(...this.yieldQueue.drainLazy());
|
|
1223
|
+
return thunks;
|
|
1224
|
+
});
|
|
1196
1225
|
this.#convertToLlm = config.convertToLlm ?? convertToLlm;
|
|
1197
1226
|
this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
|
|
1198
1227
|
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
@@ -1223,7 +1252,6 @@ export class AgentSession {
|
|
|
1223
1252
|
this.#ttsrManager = config.ttsrManager;
|
|
1224
1253
|
this.#obfuscator = config.obfuscator;
|
|
1225
1254
|
this.#agentId = config.agentId;
|
|
1226
|
-
this.#agentRegistry = config.agentRegistry;
|
|
1227
1255
|
this.#providerSessionId = config.providerSessionId;
|
|
1228
1256
|
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
|
1229
1257
|
const event: AgentEvent = {
|
|
@@ -3079,15 +3107,28 @@ export class AgentSession {
|
|
|
3079
3107
|
}
|
|
3080
3108
|
|
|
3081
3109
|
/**
|
|
3082
|
-
*
|
|
3083
|
-
*
|
|
3110
|
+
* Synchronously mark the session as disposing so new work is rejected
|
|
3111
|
+
* immediately: Python/eval starts throw, queued asides are dropped, and the
|
|
3112
|
+
* aside provider is detached. Idempotent; `dispose()` runs it first.
|
|
3113
|
+
*
|
|
3114
|
+
* Wrappers that await other teardown before delegating to `dispose()` MUST
|
|
3115
|
+
* call this before their first await — otherwise work started in that async
|
|
3116
|
+
* gap slips past the disposal guards.
|
|
3084
3117
|
*/
|
|
3085
|
-
|
|
3118
|
+
beginDispose(): void {
|
|
3086
3119
|
this.#isDisposed = true;
|
|
3087
|
-
this.#
|
|
3120
|
+
this.#pendingIrcAsides = [];
|
|
3088
3121
|
this.yieldQueue.clear();
|
|
3089
3122
|
this.agent.setAsideMessageProvider(undefined);
|
|
3090
3123
|
this.#evalExecutionDisposing = true;
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
/**
|
|
3127
|
+
* Remove all listeners, flush pending writes, and disconnect from agent.
|
|
3128
|
+
* Call this when completely done with the session.
|
|
3129
|
+
*/
|
|
3130
|
+
async dispose(): Promise<void> {
|
|
3131
|
+
this.beginDispose();
|
|
3091
3132
|
try {
|
|
3092
3133
|
if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
|
|
3093
3134
|
await this.#extensionRunner.emit({ type: "session_shutdown" });
|
|
@@ -3529,12 +3570,26 @@ export class AgentSession {
|
|
|
3529
3570
|
* Wrap a tool with a permission-gate proxy when an ACP client is connected.
|
|
3530
3571
|
* Only wraps tools whose name is in PERMISSION_REQUIRED_TOOLS and only when
|
|
3531
3572
|
* the bridge exposes `requestPermission`. No-ops for all other cases.
|
|
3573
|
+
*
|
|
3574
|
+
* When the user has explicitly opted into `yolo` / auto-approve behavior (via
|
|
3575
|
+
* the SDK/CLI `autoApprove` flag or a configured `tools.approvalMode: yolo`),
|
|
3576
|
+
* skips the gate unless the per-tool policy explicitly requires a prompt or
|
|
3577
|
+
* deny. The schema default is also `yolo`, so an explicit configuration or
|
|
3578
|
+
* explicit session flag is required: default-config ACP sessions keep the
|
|
3579
|
+
* client-side permission gate.
|
|
3532
3580
|
*/
|
|
3533
3581
|
#wrapToolForAcpPermission<T extends AgentTool>(tool: T): T {
|
|
3534
3582
|
const bridge = this.#clientBridge;
|
|
3535
3583
|
// Match the capability+method gating pattern used by read/write/bash.
|
|
3536
3584
|
if (!bridge?.capabilities.requestPermission || !bridge.requestPermission) return tool;
|
|
3537
3585
|
if (!PERMISSION_REQUIRED_TOOLS.has(tool.name)) return tool;
|
|
3586
|
+
// Skip the gate only on explicit yolo opt-in; honour per-tool policies
|
|
3587
|
+
// that require a prompt or deny (matching the normal approval wrapper).
|
|
3588
|
+
if (this.#isExplicitAutoApproveMode()) {
|
|
3589
|
+
const userPolicies = (this.settings.get("tools.approval") ?? {}) as Record<string, unknown>;
|
|
3590
|
+
const toolPolicy = userPolicies[tool.name];
|
|
3591
|
+
if (!toolPolicy || toolPolicy === "allow") return tool;
|
|
3592
|
+
}
|
|
3538
3593
|
return new Proxy(tool, {
|
|
3539
3594
|
get: (target, prop) => {
|
|
3540
3595
|
if (prop !== "execute") return Reflect.get(target, prop, target);
|
|
@@ -3622,6 +3677,13 @@ export class AgentSession {
|
|
|
3622
3677
|
}) as T;
|
|
3623
3678
|
}
|
|
3624
3679
|
|
|
3680
|
+
#isExplicitAutoApproveMode(): boolean {
|
|
3681
|
+
return (
|
|
3682
|
+
this.#autoApprove ||
|
|
3683
|
+
(this.settings.isConfigured("tools.approvalMode") && this.settings.get("tools.approvalMode") === "yolo")
|
|
3684
|
+
);
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3625
3687
|
async #applyActiveToolsByName(
|
|
3626
3688
|
toolNames: string[],
|
|
3627
3689
|
options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
|
|
@@ -3999,6 +4061,57 @@ export class AgentSession {
|
|
|
3999
4061
|
return deobfuscateSessionContext(this.sessionManager.buildSessionContext(), this.#obfuscator);
|
|
4000
4062
|
}
|
|
4001
4063
|
|
|
4064
|
+
/**
|
|
4065
|
+
* Full-history transcript for TUI display: every path entry in
|
|
4066
|
+
* chronological order with compactions rendered inline at the point they
|
|
4067
|
+
* fired (instead of replacing prior history). Display-only — NEVER feed
|
|
4068
|
+
* the result to `agent.replaceMessages` or a provider.
|
|
4069
|
+
*/
|
|
4070
|
+
buildTranscriptSessionContext(): SessionContext {
|
|
4071
|
+
return deobfuscateSessionContext(this.sessionManager.buildSessionContext({ transcript: true }), this.#obfuscator);
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
#obfuscateForProvider<T>(value: T): T {
|
|
4075
|
+
if (!this.#obfuscator?.hasSecrets()) return value;
|
|
4076
|
+
return this.#obfuscator.obfuscateObject(value);
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
#obfuscateTextForProvider(text: string | undefined): string | undefined {
|
|
4080
|
+
if (!text || !this.#obfuscator?.hasSecrets()) return text;
|
|
4081
|
+
return this.#obfuscator.obfuscate(text);
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
#obfuscatePreparationForProvider(preparation: CompactionPreparation): CompactionPreparation {
|
|
4085
|
+
if (!this.#obfuscator?.hasSecrets()) return preparation;
|
|
4086
|
+
if (!preparation.previousSummary && !preparation.previousPreserveData) return preparation;
|
|
4087
|
+
return {
|
|
4088
|
+
...preparation,
|
|
4089
|
+
previousSummary: preparation.previousSummary
|
|
4090
|
+
? this.#obfuscator.obfuscate(preparation.previousSummary)
|
|
4091
|
+
: preparation.previousSummary,
|
|
4092
|
+
previousPreserveData: preparation.previousPreserveData
|
|
4093
|
+
? this.#obfuscator.obfuscateObject(preparation.previousPreserveData)
|
|
4094
|
+
: preparation.previousPreserveData,
|
|
4095
|
+
};
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
#deobfuscateFromProvider(text: string): string {
|
|
4099
|
+
if (!this.#obfuscator?.hasSecrets()) return text;
|
|
4100
|
+
return this.#obfuscator.deobfuscate(text);
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
#deobfuscatedProviderTextReadyForDelta(text: string): string {
|
|
4104
|
+
const deobfuscated = this.#deobfuscateFromProvider(text);
|
|
4105
|
+
if (!this.#obfuscator?.hasSecrets()) return deobfuscated;
|
|
4106
|
+
const pendingPlaceholderStart = deobfuscated.match(/#[A-Z0-9]{0,4}$/);
|
|
4107
|
+
if (pendingPlaceholderStart?.index === undefined) return deobfuscated;
|
|
4108
|
+
return deobfuscated.slice(0, pendingPlaceholderStart.index);
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
#convertToLlmForSideRequest(messages: AgentMessage[]): Message[] {
|
|
4112
|
+
return this.#obfuscateForProvider(convertToLlm(messages));
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4002
4115
|
/** Convert session messages using the same pre-LLM pipeline as the active session. */
|
|
4003
4116
|
async convertMessagesToLlm(messages: AgentMessage[], signal?: AbortSignal): Promise<Message[]> {
|
|
4004
4117
|
const transformedMessages = await this.#transformContext(messages, signal);
|
|
@@ -4398,21 +4511,28 @@ export class AgentSession {
|
|
|
4398
4511
|
* @throws Error if streaming and no streamingBehavior specified
|
|
4399
4512
|
* @throws Error if no model selected or no API key available (when not streaming)
|
|
4400
4513
|
*/
|
|
4401
|
-
|
|
4514
|
+
/**
|
|
4515
|
+
* Returns `false` when the command was fully handled locally (extension or
|
|
4516
|
+
* custom-TS command consumed without calling the LLM). Returns `true` when
|
|
4517
|
+
* the prompt was forwarded to the agent — either directly or queued as a
|
|
4518
|
+
* steer/follow-up. Callers that render a UI or manage turn lifecycle (e.g.
|
|
4519
|
+
* the ACP agent) use this to know whether to expect an `agent_end` event.
|
|
4520
|
+
*/
|
|
4521
|
+
async prompt(text: string, options?: PromptOptions): Promise<boolean> {
|
|
4402
4522
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
4403
4523
|
|
|
4404
4524
|
// Handle extension commands first (execute immediately, even during streaming)
|
|
4405
4525
|
if (expandPromptTemplates && text.startsWith("/")) {
|
|
4406
4526
|
const handled = await this.#tryExecuteExtensionCommand(text);
|
|
4407
4527
|
if (handled) {
|
|
4408
|
-
return;
|
|
4528
|
+
return false;
|
|
4409
4529
|
}
|
|
4410
4530
|
|
|
4411
4531
|
// Try custom commands (TypeScript slash commands)
|
|
4412
4532
|
const customResult = await this.#tryExecuteCustomCommand(text);
|
|
4413
4533
|
if (customResult !== null) {
|
|
4414
4534
|
if (customResult === "") {
|
|
4415
|
-
return;
|
|
4535
|
+
return false;
|
|
4416
4536
|
}
|
|
4417
4537
|
text = customResult;
|
|
4418
4538
|
}
|
|
@@ -4446,7 +4566,7 @@ export class AgentSession {
|
|
|
4446
4566
|
for (const notice of keywordNotices) {
|
|
4447
4567
|
await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
|
|
4448
4568
|
}
|
|
4449
|
-
return;
|
|
4569
|
+
return true;
|
|
4450
4570
|
}
|
|
4451
4571
|
|
|
4452
4572
|
// Skip eager todo prelude when the user has already queued a directive
|
|
@@ -4486,6 +4606,7 @@ export class AgentSession {
|
|
|
4486
4606
|
if (!options?.synthetic) {
|
|
4487
4607
|
await this.#enforcePlanModeToolDecision();
|
|
4488
4608
|
}
|
|
4609
|
+
return true;
|
|
4489
4610
|
}
|
|
4490
4611
|
|
|
4491
4612
|
async promptCustomMessage<T = unknown>(
|
|
@@ -4552,7 +4673,7 @@ export class AgentSession {
|
|
|
4552
4673
|
// Flush any pending bash messages before the new prompt
|
|
4553
4674
|
this.#flushPendingBashMessages();
|
|
4554
4675
|
this.#flushPendingPythonMessages();
|
|
4555
|
-
this.#
|
|
4676
|
+
this.#flushPendingIrcAsides();
|
|
4556
4677
|
|
|
4557
4678
|
// Reset todo reminder count on new user prompt
|
|
4558
4679
|
this.#todoReminderCount = 0;
|
|
@@ -5694,16 +5815,25 @@ export class AgentSession {
|
|
|
5694
5815
|
}
|
|
5695
5816
|
|
|
5696
5817
|
/**
|
|
5697
|
-
* Get all available models with valid API keys.
|
|
5818
|
+
* Get all available models with valid API keys, filtered by `enabledModels` when configured.
|
|
5819
|
+
* See {@link filterAvailableModelsByEnabledPatterns} for supported pattern forms and limitations.
|
|
5698
5820
|
*/
|
|
5699
5821
|
getAvailableModels(): Model[] {
|
|
5700
|
-
|
|
5822
|
+
const all = this.#modelRegistry.getAvailable();
|
|
5823
|
+
const patterns = this.settings.get("enabledModels");
|
|
5824
|
+
if (!patterns || patterns.length === 0) return all;
|
|
5825
|
+
return filterAvailableModelsByEnabledPatterns(all, patterns, this.#modelRegistry);
|
|
5701
5826
|
}
|
|
5702
5827
|
|
|
5703
5828
|
// =========================================================================
|
|
5704
5829
|
// Thinking Level Management
|
|
5705
5830
|
// =========================================================================
|
|
5706
5831
|
|
|
5832
|
+
#applyThinkingLevelToAgent(level: ThinkingLevel | undefined): void {
|
|
5833
|
+
this.agent.setThinkingLevel(toReasoningEffort(level));
|
|
5834
|
+
this.agent.setDisableReasoning(shouldDisableReasoning(level));
|
|
5835
|
+
}
|
|
5836
|
+
|
|
5707
5837
|
/**
|
|
5708
5838
|
* Set the thinking level. `auto` enables per-turn classification; the selector
|
|
5709
5839
|
* itself is never written to the session log, but resolved concrete levels are
|
|
@@ -5717,7 +5847,7 @@ export class AgentSession {
|
|
|
5717
5847
|
this.#autoThinking = true;
|
|
5718
5848
|
this.#autoResolvedLevel = undefined;
|
|
5719
5849
|
this.#thinkingLevel = provisional;
|
|
5720
|
-
this
|
|
5850
|
+
this.#applyThinkingLevelToAgent(provisional);
|
|
5721
5851
|
if (persist) {
|
|
5722
5852
|
this.settings.set("defaultThinkingLevel", AUTO_THINKING);
|
|
5723
5853
|
}
|
|
@@ -5733,7 +5863,7 @@ export class AgentSession {
|
|
|
5733
5863
|
const isChanging = effectiveLevel !== this.#thinkingLevel;
|
|
5734
5864
|
|
|
5735
5865
|
this.#thinkingLevel = effectiveLevel;
|
|
5736
|
-
this
|
|
5866
|
+
this.#applyThinkingLevelToAgent(effectiveLevel);
|
|
5737
5867
|
|
|
5738
5868
|
if (isChanging) {
|
|
5739
5869
|
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
|
@@ -5823,7 +5953,7 @@ export class AgentSession {
|
|
|
5823
5953
|
const shouldPersistResolution = this.#autoResolvedLevel !== effort;
|
|
5824
5954
|
this.#autoResolvedLevel = effort;
|
|
5825
5955
|
this.#thinkingLevel = effort;
|
|
5826
|
-
this
|
|
5956
|
+
this.#applyThinkingLevelToAgent(effort);
|
|
5827
5957
|
if (shouldPersistResolution) {
|
|
5828
5958
|
this.sessionManager.appendThinkingLevelChange(effort);
|
|
5829
5959
|
}
|
|
@@ -5957,6 +6087,35 @@ export class AgentSession {
|
|
|
5957
6087
|
return result;
|
|
5958
6088
|
}
|
|
5959
6089
|
|
|
6090
|
+
/**
|
|
6091
|
+
* Per-turn supersede pass: prune older `read` results that a newer read of
|
|
6092
|
+
* the same file has made stale. Cache-aware (only fires when the suffix
|
|
6093
|
+
* after a candidate is small or the session has been idle long enough that
|
|
6094
|
+
* the provider prompt cache is cold), so it is cheap to run every turn.
|
|
6095
|
+
* Gated on the `compaction.supersedeReads` setting.
|
|
6096
|
+
*/
|
|
6097
|
+
async #pruneSupersededReads(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
|
|
6098
|
+
if (!this.settings.getGroup("compaction").supersedeReads) return undefined;
|
|
6099
|
+
const branchEntries = this.sessionManager.getBranch();
|
|
6100
|
+
const result = pruneSupersededToolResults(
|
|
6101
|
+
branchEntries,
|
|
6102
|
+
this.#withPlanProtection({
|
|
6103
|
+
supersedeKey: readToolSupersedeKey,
|
|
6104
|
+
protectedTools: [...DEFAULT_PRUNE_CONFIG.protectedTools],
|
|
6105
|
+
}),
|
|
6106
|
+
);
|
|
6107
|
+
if (result.prunedCount === 0) {
|
|
6108
|
+
return undefined;
|
|
6109
|
+
}
|
|
6110
|
+
|
|
6111
|
+
await this.sessionManager.rewriteEntries();
|
|
6112
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
6113
|
+
this.agent.replaceMessages(sessionContext.messages);
|
|
6114
|
+
this.#syncTodoPhasesFromBranch();
|
|
6115
|
+
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6116
|
+
return result;
|
|
6117
|
+
}
|
|
6118
|
+
|
|
5960
6119
|
/**
|
|
5961
6120
|
* Strip image content blocks from every message on the current branch and
|
|
5962
6121
|
* persist the rewrite. Walks `SessionManager.getBranch()` in place — both
|
|
@@ -6146,6 +6305,20 @@ export class AgentSession {
|
|
|
6146
6305
|
|
|
6147
6306
|
const compactionPrep = await this.#prepareCompactionFromHooks(preparation, hookCompaction);
|
|
6148
6307
|
|
|
6308
|
+
// Strategy honored on manual /compact too. Custom instructions imply a
|
|
6309
|
+
// directed LLM summary; a text-only model cannot read the frames back —
|
|
6310
|
+
// both take the summarizer path (the latter loudly).
|
|
6311
|
+
const wantsSnapcompact =
|
|
6312
|
+
compactionPrep.kind !== "fromHook" && compactionSettings.strategy === "snapcompact" && !customInstructions;
|
|
6313
|
+
const snapcompactReady = wantsSnapcompact && this.model.input.includes("image");
|
|
6314
|
+
if (wantsSnapcompact && !snapcompactReady) {
|
|
6315
|
+
this.emitNotice(
|
|
6316
|
+
"warning",
|
|
6317
|
+
`snapcompact needs a vision-capable model (${this.model.id} is text-only) — using an LLM summary instead`,
|
|
6318
|
+
"compaction",
|
|
6319
|
+
);
|
|
6320
|
+
}
|
|
6321
|
+
|
|
6149
6322
|
let summary: string;
|
|
6150
6323
|
let shortSummary: string | undefined;
|
|
6151
6324
|
let firstKeptEntryId: string;
|
|
@@ -6159,6 +6332,14 @@ export class AgentSession {
|
|
|
6159
6332
|
tokensBefore = compactionPrep.tokensBefore;
|
|
6160
6333
|
details = compactionPrep.details;
|
|
6161
6334
|
preserveData = compactionPrep.preserveData;
|
|
6335
|
+
} else if (snapcompactReady) {
|
|
6336
|
+
const snapcompactResult = await snapcompactCompact(preparation, { convertToLlm, model: this.model });
|
|
6337
|
+
summary = snapcompactResult.summary;
|
|
6338
|
+
shortSummary = snapcompactResult.shortSummary;
|
|
6339
|
+
firstKeptEntryId = snapcompactResult.firstKeptEntryId;
|
|
6340
|
+
tokensBefore = snapcompactResult.tokensBefore;
|
|
6341
|
+
details = snapcompactResult.details;
|
|
6342
|
+
preserveData = { ...(compactionPrep.preserveData ?? {}), ...(snapcompactResult.preserveData ?? {}) };
|
|
6162
6343
|
} else {
|
|
6163
6344
|
// Generate compaction result. Only convert known abort-shaped
|
|
6164
6345
|
// rejections (AbortError raised while the abort signal is set,
|
|
@@ -6177,10 +6358,10 @@ export class AgentSession {
|
|
|
6177
6358
|
customInstructions,
|
|
6178
6359
|
compactionAbortController.signal,
|
|
6179
6360
|
{
|
|
6180
|
-
promptOverride: compactionPrep.hookPrompt,
|
|
6181
|
-
extraContext: compactionPrep.hookContext,
|
|
6182
|
-
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
|
6183
|
-
convertToLlm,
|
|
6361
|
+
promptOverride: this.#obfuscateTextForProvider(compactionPrep.hookPrompt),
|
|
6362
|
+
extraContext: this.#obfuscateForProvider(compactionPrep.hookContext),
|
|
6363
|
+
remoteInstructions: this.#obfuscateForProvider(this.#baseSystemPrompt.join("\n\n")),
|
|
6364
|
+
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
6184
6365
|
},
|
|
6185
6366
|
);
|
|
6186
6367
|
summary = result.summary;
|
|
@@ -6363,15 +6544,15 @@ export class AgentSession {
|
|
|
6363
6544
|
throw new Error(`No API key for ${model.provider}`);
|
|
6364
6545
|
}
|
|
6365
6546
|
|
|
6366
|
-
const
|
|
6547
|
+
const rawHandoffText = await generateHandoff(
|
|
6367
6548
|
this.agent.state.messages,
|
|
6368
6549
|
model,
|
|
6369
6550
|
apiKey,
|
|
6370
6551
|
{
|
|
6371
|
-
systemPrompt: this.#baseSystemPrompt,
|
|
6372
|
-
tools: this.agent.state.tools,
|
|
6373
|
-
customInstructions,
|
|
6374
|
-
convertToLlm,
|
|
6552
|
+
systemPrompt: this.#obfuscateForProvider(this.#baseSystemPrompt),
|
|
6553
|
+
tools: obfuscateProviderTools(this.#obfuscator, this.agent.state.tools),
|
|
6554
|
+
customInstructions: this.#obfuscateTextForProvider(customInstructions),
|
|
6555
|
+
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
6375
6556
|
initiatorOverride: "agent",
|
|
6376
6557
|
metadata: this.agent.metadataForProvider(model.provider),
|
|
6377
6558
|
telemetry: resolveTelemetry(this.agent.telemetry, this.sessionId),
|
|
@@ -6383,6 +6564,7 @@ export class AgentSession {
|
|
|
6383
6564
|
},
|
|
6384
6565
|
handoffSignal,
|
|
6385
6566
|
);
|
|
6567
|
+
const handoffText = this.#deobfuscateFromProvider(rawHandoffText);
|
|
6386
6568
|
|
|
6387
6569
|
if (handoffSignal.aborted) {
|
|
6388
6570
|
throw new Error("Handoff cancelled");
|
|
@@ -6577,7 +6759,10 @@ export class AgentSession {
|
|
|
6577
6759
|
model: `${assistantMessage.provider}/${assistantMessage.model}`,
|
|
6578
6760
|
strategy: incompleteCompactionSettings.strategy,
|
|
6579
6761
|
});
|
|
6580
|
-
await this.#runAutoCompaction("incomplete", true, false, allowDefer, {
|
|
6762
|
+
await this.#runAutoCompaction("incomplete", true, false, allowDefer, {
|
|
6763
|
+
autoContinue,
|
|
6764
|
+
triggerContextTokens: calculateContextTokens(assistantMessage.usage),
|
|
6765
|
+
});
|
|
6581
6766
|
} else {
|
|
6582
6767
|
// Neither promotion nor compaction is available — surface the dead-end so
|
|
6583
6768
|
// the user understands why the turn yielded with nothing.
|
|
@@ -6588,6 +6773,10 @@ export class AgentSession {
|
|
|
6588
6773
|
return false;
|
|
6589
6774
|
}
|
|
6590
6775
|
|
|
6776
|
+
// Supersede pass runs every turn, before any threshold gating: it is cheap
|
|
6777
|
+
// (bails when no candidate) and independent of the compaction setting.
|
|
6778
|
+
const supersedeResult = await this.#pruneSupersededReads();
|
|
6779
|
+
|
|
6591
6780
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6592
6781
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
|
|
6593
6782
|
|
|
@@ -6596,6 +6785,9 @@ export class AgentSession {
|
|
|
6596
6785
|
if (assistantMessage.stopReason === "error") return false;
|
|
6597
6786
|
const pruneResult = await this.#pruneToolOutputs();
|
|
6598
6787
|
let contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
6788
|
+
if (supersedeResult) {
|
|
6789
|
+
contextTokens = Math.max(0, contextTokens - supersedeResult.tokensSaved);
|
|
6790
|
+
}
|
|
6599
6791
|
if (pruneResult) {
|
|
6600
6792
|
contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
6601
6793
|
}
|
|
@@ -6603,7 +6795,10 @@ export class AgentSession {
|
|
|
6603
6795
|
// Try promotion first — if a larger model is available, switch instead of compacting
|
|
6604
6796
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
6605
6797
|
if (!promoted) {
|
|
6606
|
-
return await this.#runAutoCompaction("threshold", false, false, allowDefer, {
|
|
6798
|
+
return await this.#runAutoCompaction("threshold", false, false, allowDefer, {
|
|
6799
|
+
autoContinue,
|
|
6800
|
+
triggerContextTokens: contextTokens,
|
|
6801
|
+
});
|
|
6607
6802
|
}
|
|
6608
6803
|
}
|
|
6609
6804
|
return false;
|
|
@@ -7344,17 +7539,24 @@ export class AgentSession {
|
|
|
7344
7539
|
if (!apiKey) continue;
|
|
7345
7540
|
|
|
7346
7541
|
try {
|
|
7347
|
-
return await compact(
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7542
|
+
return await compact(
|
|
7543
|
+
this.#obfuscatePreparationForProvider(preparation),
|
|
7544
|
+
candidate,
|
|
7545
|
+
apiKey,
|
|
7546
|
+
this.#obfuscateTextForProvider(customInstructions),
|
|
7547
|
+
signal,
|
|
7548
|
+
{
|
|
7549
|
+
...options,
|
|
7550
|
+
metadata: this.agent.metadataForProvider(candidate.provider),
|
|
7551
|
+
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
7552
|
+
telemetry,
|
|
7553
|
+
// Honor the user's /model thinking selection (incl. `off`) on
|
|
7554
|
+
// the manual `/compact` path. Clamped per-model inside compact()
|
|
7555
|
+
// via resolveCompactionEffort so unsupported-effort models
|
|
7556
|
+
// (xai-oauth/grok-build) don't trip requireSupportedEffort.
|
|
7557
|
+
thinkingLevel: this.thinkingLevel,
|
|
7558
|
+
},
|
|
7559
|
+
);
|
|
7358
7560
|
} catch (error) {
|
|
7359
7561
|
if (!this.#isCompactionAuthFailure(error)) {
|
|
7360
7562
|
throw error;
|
|
@@ -7441,7 +7643,7 @@ export class AgentSession {
|
|
|
7441
7643
|
willRetry: boolean,
|
|
7442
7644
|
deferred = false,
|
|
7443
7645
|
allowDefer = true,
|
|
7444
|
-
options: { autoContinue?: boolean } = {},
|
|
7646
|
+
options: { autoContinue?: boolean; triggerContextTokens?: number } = {},
|
|
7445
7647
|
): Promise<boolean> {
|
|
7446
7648
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
7447
7649
|
if (compactionSettings.strategy === "off") return false;
|
|
@@ -7452,7 +7654,13 @@ export class AgentSession {
|
|
|
7452
7654
|
// reclaims nothing we fall through to the summary-compaction body below so
|
|
7453
7655
|
// the oversized input still gets resolved.
|
|
7454
7656
|
if (compactionSettings.strategy === "shake") {
|
|
7455
|
-
const outcome = await this.#runAutoShake(
|
|
7657
|
+
const outcome = await this.#runAutoShake(
|
|
7658
|
+
reason,
|
|
7659
|
+
willRetry,
|
|
7660
|
+
generation,
|
|
7661
|
+
shouldAutoContinue,
|
|
7662
|
+
options.triggerContextTokens,
|
|
7663
|
+
);
|
|
7456
7664
|
if (outcome !== "fallback") return false;
|
|
7457
7665
|
}
|
|
7458
7666
|
// "overflow" and "incomplete" force inline execution because they are recovery
|
|
@@ -7479,9 +7687,25 @@ export class AgentSession {
|
|
|
7479
7687
|
|
|
7480
7688
|
// "overflow" forces context-full because the input itself is broken — a handoff
|
|
7481
7689
|
// LLM call would hit the same overflow. "incomplete" is an output-side problem,
|
|
7482
|
-
// so a handoff request on the existing context is still viable.
|
|
7483
|
-
|
|
7690
|
+
// so a handoff request on the existing context is still viable. Snapcompact is
|
|
7691
|
+
// safe for every reason (it makes no LLM call at all) but requires a vision
|
|
7692
|
+
// model to be worth anything — fall back to context-full otherwise.
|
|
7693
|
+
let action: "context-full" | "handoff" | "snapcompact" =
|
|
7484
7694
|
compactionSettings.strategy === "handoff" && reason !== "overflow" ? "handoff" : "context-full";
|
|
7695
|
+
if (compactionSettings.strategy === "snapcompact") {
|
|
7696
|
+
if (this.model?.input.includes("image")) {
|
|
7697
|
+
action = "snapcompact";
|
|
7698
|
+
} else {
|
|
7699
|
+
logger.warn("Snapcompact compaction requires a vision-capable model; falling back to context-full", {
|
|
7700
|
+
model: this.model?.id,
|
|
7701
|
+
});
|
|
7702
|
+
this.emitNotice(
|
|
7703
|
+
"warning",
|
|
7704
|
+
`snapcompact needs a vision-capable model (${this.model?.id ?? "unknown"} is text-only) — using an LLM summary instead`,
|
|
7705
|
+
"compaction",
|
|
7706
|
+
);
|
|
7707
|
+
}
|
|
7708
|
+
}
|
|
7485
7709
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
7486
7710
|
// Abort any older auto-compaction before installing this run's controller.
|
|
7487
7711
|
this.#autoCompactionAbortController?.abort();
|
|
@@ -7620,6 +7844,16 @@ export class AgentSession {
|
|
|
7620
7844
|
tokensBefore = compactionPrep.tokensBefore;
|
|
7621
7845
|
details = compactionPrep.details;
|
|
7622
7846
|
preserveData = compactionPrep.preserveData;
|
|
7847
|
+
} else if (action === "snapcompact") {
|
|
7848
|
+
// Local, deterministic: render discarded history onto PNG frames.
|
|
7849
|
+
// No model candidates, no API key, no retry loop.
|
|
7850
|
+
const snapcompactResult = await snapcompactCompact(preparation, { convertToLlm, model: this.model });
|
|
7851
|
+
summary = snapcompactResult.summary;
|
|
7852
|
+
shortSummary = snapcompactResult.shortSummary;
|
|
7853
|
+
firstKeptEntryId = snapcompactResult.firstKeptEntryId;
|
|
7854
|
+
tokensBefore = snapcompactResult.tokensBefore;
|
|
7855
|
+
details = snapcompactResult.details;
|
|
7856
|
+
preserveData = { ...(compactionPrep.preserveData ?? {}), ...(snapcompactResult.preserveData ?? {}) };
|
|
7623
7857
|
} else {
|
|
7624
7858
|
const candidates = this.#getCompactionModelCandidates(availableModels);
|
|
7625
7859
|
const retrySettings = this.settings.getGroup("retry");
|
|
@@ -7634,20 +7868,27 @@ export class AgentSession {
|
|
|
7634
7868
|
let attempt = 0;
|
|
7635
7869
|
while (true) {
|
|
7636
7870
|
try {
|
|
7637
|
-
compactResult = await compact(
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7871
|
+
compactResult = await compact(
|
|
7872
|
+
this.#obfuscatePreparationForProvider(preparation),
|
|
7873
|
+
candidate,
|
|
7874
|
+
apiKey,
|
|
7875
|
+
undefined,
|
|
7876
|
+
autoCompactionSignal,
|
|
7877
|
+
{
|
|
7878
|
+
promptOverride: this.#obfuscateTextForProvider(compactionPrep.hookPrompt),
|
|
7879
|
+
extraContext: this.#obfuscateForProvider(compactionPrep.hookContext),
|
|
7880
|
+
remoteInstructions: this.#obfuscateForProvider(this.#baseSystemPrompt.join("\n\n")),
|
|
7881
|
+
metadata: this.agent.metadataForProvider(candidate.provider),
|
|
7882
|
+
initiatorOverride: "agent",
|
|
7883
|
+
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
7884
|
+
telemetry,
|
|
7885
|
+
// Honor the user's /model thinking selection on the
|
|
7886
|
+
// auto-compaction path — the most-fired compaction
|
|
7887
|
+
// site. Clamped per-model inside compact() via
|
|
7888
|
+
// resolveCompactionEffort.
|
|
7889
|
+
thinkingLevel: this.thinkingLevel,
|
|
7890
|
+
},
|
|
7891
|
+
);
|
|
7651
7892
|
break;
|
|
7652
7893
|
} catch (error) {
|
|
7653
7894
|
if (autoCompactionSignal.aborted) {
|
|
@@ -7852,6 +8093,7 @@ export class AgentSession {
|
|
|
7852
8093
|
willRetry: boolean,
|
|
7853
8094
|
generation: number,
|
|
7854
8095
|
autoContinue: boolean,
|
|
8096
|
+
triggerContextTokens?: number,
|
|
7855
8097
|
): Promise<"handled" | "fallback"> {
|
|
7856
8098
|
const action = "shake";
|
|
7857
8099
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
@@ -7872,8 +8114,8 @@ export class AgentSession {
|
|
|
7872
8114
|
return "handled";
|
|
7873
8115
|
}
|
|
7874
8116
|
const reclaimed = result.toolResultsDropped + result.blocksDropped > 0;
|
|
7875
|
-
// Detect the dead-loop reported in
|
|
7876
|
-
// shake runs, but
|
|
8117
|
+
// Detect the dead-loop reported in issues #2119/#2275: the threshold check
|
|
8118
|
+
// fires, shake runs, but residual context is still above the configured
|
|
7877
8119
|
// threshold. The next agent_end would re-trigger shake, which has nothing
|
|
7878
8120
|
// new to drop on the second pass, so the loop spins until the user kills it.
|
|
7879
8121
|
// Same hazard for "incomplete" (the retry would re-hit the length cap) and
|
|
@@ -7881,10 +8123,30 @@ export class AgentSession {
|
|
|
7881
8123
|
// reason we hand off to the summarization-driven context-full path so the
|
|
7882
8124
|
// situation actually resolves; "idle" is exempt because its 60s+ timer
|
|
7883
8125
|
// re-checks usage before re-firing and cannot dead-loop on its own.
|
|
8126
|
+
//
|
|
8127
|
+
// #2275: the post-shake check MUST be anchored on the same metric that
|
|
8128
|
+
// triggered compaction. The local estimator (`#estimatePendingPromptTokens`)
|
|
8129
|
+
// undercounts thinking-signature payloads, so on thinking-heavy sessions it
|
|
8130
|
+
// reads well below the provider-reported usage that fired the threshold.
|
|
8131
|
+
// When that estimate slips under the threshold, the fallback never fires
|
|
8132
|
+
// and the auto-continue prompt re-injects every turn. Prefer the trigger's
|
|
8133
|
+
// own `contextTokens` (provider-anchored) when the caller supplies it, and
|
|
8134
|
+
// add hysteresis (80% recovery band) so we don't oscillate at the boundary
|
|
8135
|
+
// while shake keeps reclaiming a trickle of the previous turn's output.
|
|
7884
8136
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
7885
8137
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
7886
|
-
|
|
7887
|
-
|
|
8138
|
+
let stillOverThreshold = false;
|
|
8139
|
+
if (contextWindow > 0) {
|
|
8140
|
+
if (typeof triggerContextTokens === "number" && Number.isFinite(triggerContextTokens)) {
|
|
8141
|
+
const correctedTokens = Math.max(0, triggerContextTokens - result.tokensFreed);
|
|
8142
|
+
const thresholdTokens = resolveThresholdTokens(contextWindow, compactionSettings);
|
|
8143
|
+
const recoveryBand = Math.floor(thresholdTokens * SHAKE_RECOVERY_BAND);
|
|
8144
|
+
stillOverThreshold = correctedTokens > recoveryBand;
|
|
8145
|
+
} else {
|
|
8146
|
+
const postShakeTokens = this.#estimatePendingPromptTokens([]);
|
|
8147
|
+
stillOverThreshold = shouldCompact(postShakeTokens, contextWindow, compactionSettings);
|
|
8148
|
+
}
|
|
8149
|
+
}
|
|
7888
8150
|
const shouldFallBack = reason !== "idle" && ((reason === "overflow" && !reclaimed) || stillOverThreshold);
|
|
7889
8151
|
if (shouldFallBack) {
|
|
7890
8152
|
const errorMessage = reclaimed
|
|
@@ -8337,6 +8599,7 @@ export class AgentSession {
|
|
|
8337
8599
|
{
|
|
8338
8600
|
retryAfterMs,
|
|
8339
8601
|
baseUrl: this.model.baseUrl,
|
|
8602
|
+
modelId: this.model.id,
|
|
8340
8603
|
},
|
|
8341
8604
|
);
|
|
8342
8605
|
if (outcome.switched) {
|
|
@@ -8533,11 +8796,12 @@ export class AgentSession {
|
|
|
8533
8796
|
* @param command The bash command to execute
|
|
8534
8797
|
* @param onChunk Optional streaming callback for output
|
|
8535
8798
|
* @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix)
|
|
8799
|
+
* @param options.useUserShell If true, allow caller to request configured user-shell routing
|
|
8536
8800
|
*/
|
|
8537
8801
|
async executeBash(
|
|
8538
8802
|
command: string,
|
|
8539
8803
|
onChunk?: (chunk: string) => void,
|
|
8540
|
-
options?: { excludeFromContext?: boolean },
|
|
8804
|
+
options?: { excludeFromContext?: boolean; useUserShell?: boolean },
|
|
8541
8805
|
): Promise<BashResult> {
|
|
8542
8806
|
const excludeFromContext = options?.excludeFromContext === true;
|
|
8543
8807
|
const cwd = this.sessionManager.getCwd();
|
|
@@ -8565,6 +8829,7 @@ export class AgentSession {
|
|
|
8565
8829
|
sessionKey: this.sessionId,
|
|
8566
8830
|
timeout: clampTimeout("bash") * 1000,
|
|
8567
8831
|
onMinimizedSave: originalText => this.#saveBashOriginalArtifact(originalText),
|
|
8832
|
+
useUserShell: options?.useUserShell,
|
|
8568
8833
|
});
|
|
8569
8834
|
|
|
8570
8835
|
this.recordBashResult(command, result, options);
|
|
@@ -8690,6 +8955,7 @@ export class AgentSession {
|
|
|
8690
8955
|
sessionId: namespacePythonSessionId(sessionId),
|
|
8691
8956
|
kernelOwnerId: this.#evalKernelOwnerId,
|
|
8692
8957
|
kernelMode: this.settings.get("python.kernelMode"),
|
|
8958
|
+
interpreter: this.settings.get("python.interpreter")?.trim() || undefined,
|
|
8693
8959
|
onChunk,
|
|
8694
8960
|
signal: abortController.signal,
|
|
8695
8961
|
});
|
|
@@ -8816,118 +9082,56 @@ export class AgentSession {
|
|
|
8816
9082
|
}
|
|
8817
9083
|
|
|
8818
9084
|
// =========================================================================
|
|
8819
|
-
//
|
|
9085
|
+
// IRC Delivery
|
|
8820
9086
|
// =========================================================================
|
|
8821
9087
|
|
|
8822
9088
|
/**
|
|
8823
|
-
*
|
|
8824
|
-
*
|
|
9089
|
+
* Deliver an IRC message into this session (recipient side; called by the
|
|
9090
|
+
* IrcBus). Emits the `irc_message` session event for UI cards and injects
|
|
9091
|
+
* the rendered message into the model's context as an `irc:incoming`
|
|
9092
|
+
* custom message:
|
|
9093
|
+
*
|
|
9094
|
+
* - mid-turn → queued on the aside channel and folded in at the next step
|
|
9095
|
+
* boundary (non-interrupting, like async-result deliveries) → "injected";
|
|
9096
|
+
* - idle → starts a real turn with the message so the recipient wakes
|
|
9097
|
+
* → "woken".
|
|
8825
9098
|
*
|
|
8826
|
-
*
|
|
8827
|
-
* history immediately so timeouts/abort still preserve delivery. The reply is
|
|
8828
|
-
* computed via a side-channel `streamSimple` call (analogous to `/btw`) so it
|
|
8829
|
-
* never blocks on the recipient's in-flight tool calls. When a reply is
|
|
8830
|
-
* generated, it is queued separately. Injection happens immediately when the
|
|
8831
|
-
* session is idle, otherwise it is deferred until streaming ends.
|
|
9099
|
+
* Never blocks on the recipient's turn: the wake turn is fire-and-forget.
|
|
8832
9100
|
*/
|
|
8833
|
-
async
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
}): Promise<{ replyText: string | null }> {
|
|
8839
|
-
const awaitReply = args.awaitReply !== false;
|
|
8840
|
-
const incomingTimestamp = Date.now();
|
|
8841
|
-
const incomingRecord: CustomMessage = {
|
|
9101
|
+
async deliverIrcMessage(msg: IrcMessage): Promise<"injected" | "woken"> {
|
|
9102
|
+
if (this.#isDisposed) {
|
|
9103
|
+
throw new Error("Recipient session is disposed.");
|
|
9104
|
+
}
|
|
9105
|
+
const record: CustomMessage = {
|
|
8842
9106
|
role: "custom",
|
|
8843
9107
|
customType: "irc:incoming",
|
|
8844
|
-
content:
|
|
9108
|
+
content: prompt.render(ircIncomingTemplate, {
|
|
9109
|
+
from: msg.from,
|
|
9110
|
+
message: msg.body,
|
|
9111
|
+
replyTo: msg.replyTo ?? "",
|
|
9112
|
+
}),
|
|
8845
9113
|
display: true,
|
|
8846
|
-
details: { from:
|
|
9114
|
+
details: { id: msg.id, from: msg.from, message: msg.body, ...(msg.replyTo ? { replyTo: msg.replyTo } : {}) },
|
|
8847
9115
|
attribution: "agent",
|
|
8848
|
-
timestamp:
|
|
9116
|
+
timestamp: msg.ts,
|
|
8849
9117
|
};
|
|
8850
|
-
void this.#emitSessionEvent({ type: "irc_message", message:
|
|
8851
|
-
this
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
body: args.message,
|
|
8855
|
-
kind: "message",
|
|
8856
|
-
timestamp: incomingTimestamp,
|
|
8857
|
-
});
|
|
8858
|
-
|
|
8859
|
-
this.#queueBackgroundExchangeInjection([incomingRecord]);
|
|
8860
|
-
if (!awaitReply) {
|
|
8861
|
-
return { replyText: null };
|
|
9118
|
+
void this.#emitSessionEvent({ type: "irc_message", message: record });
|
|
9119
|
+
if (this.isStreaming) {
|
|
9120
|
+
this.#pendingIrcAsides.push(record);
|
|
9121
|
+
return "injected";
|
|
8862
9122
|
}
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
9123
|
+
// Idle: same wake primitive the yield queue uses for async-result
|
|
9124
|
+
// delivery — prompt the agent directly so a real turn runs.
|
|
9125
|
+
this.agent.prompt(record).catch(error => {
|
|
9126
|
+
logger.warn("IRC wake turn failed", { from: msg.from, to: msg.to, error: String(error) });
|
|
8867
9127
|
});
|
|
8868
|
-
|
|
8869
|
-
promptText: incomingPrompt,
|
|
8870
|
-
signal: args.signal,
|
|
8871
|
-
});
|
|
8872
|
-
|
|
8873
|
-
const replyRecord: CustomMessage = {
|
|
8874
|
-
role: "custom",
|
|
8875
|
-
customType: "irc:autoreply",
|
|
8876
|
-
content: `[IRC you → \`${args.from}\` (auto)]\n\n${replyText}`,
|
|
8877
|
-
display: true,
|
|
8878
|
-
details: { to: args.from, reply: replyText },
|
|
8879
|
-
attribution: "agent",
|
|
8880
|
-
timestamp: Date.now(),
|
|
8881
|
-
};
|
|
8882
|
-
void this.#emitSessionEvent({ type: "irc_message", message: replyRecord });
|
|
8883
|
-
this.#forwardIrcRelayToMain({
|
|
8884
|
-
from: this.#agentId ?? "?",
|
|
8885
|
-
to: args.from,
|
|
8886
|
-
body: replyText,
|
|
8887
|
-
kind: "reply",
|
|
8888
|
-
timestamp: replyRecord.timestamp,
|
|
8889
|
-
});
|
|
8890
|
-
this.#queueBackgroundExchangeInjection([replyRecord]);
|
|
8891
|
-
|
|
8892
|
-
return { replyText };
|
|
8893
|
-
}
|
|
8894
|
-
|
|
8895
|
-
/**
|
|
8896
|
-
* Forward an IRC exchange observation to the main agent's session UI so the
|
|
8897
|
-
* user can see every IRC conversation in the main transcript, even when the
|
|
8898
|
-
* main agent is not a direct participant. The relay record is display-only:
|
|
8899
|
-
* it is NOT injected into the main agent's persisted history.
|
|
8900
|
-
*/
|
|
8901
|
-
#forwardIrcRelayToMain(args: {
|
|
8902
|
-
from: string;
|
|
8903
|
-
to: string;
|
|
8904
|
-
body: string;
|
|
8905
|
-
kind: "message" | "reply";
|
|
8906
|
-
timestamp: number;
|
|
8907
|
-
}): void {
|
|
8908
|
-
const registry = this.#agentRegistry;
|
|
8909
|
-
if (!registry) return;
|
|
8910
|
-
// If this session is the main agent, the local emit already reached the main UI.
|
|
8911
|
-
if (this.#agentId === MAIN_AGENT_ID) return;
|
|
8912
|
-
const mainRef = registry.get(MAIN_AGENT_ID);
|
|
8913
|
-
const mainSession = mainRef?.session;
|
|
8914
|
-
if (!mainSession || mainSession === this) return;
|
|
8915
|
-
const arrow = args.kind === "reply" ? "→ (auto)" : "→";
|
|
8916
|
-
const relayRecord: CustomMessage = {
|
|
8917
|
-
role: "custom",
|
|
8918
|
-
customType: "irc:relay",
|
|
8919
|
-
content: `[IRC \`${args.from}\` ${arrow} \`${args.to}\`]\n\n${args.body}`,
|
|
8920
|
-
display: true,
|
|
8921
|
-
details: { from: args.from, to: args.to, body: args.body, kind: args.kind },
|
|
8922
|
-
attribution: "agent",
|
|
8923
|
-
timestamp: args.timestamp,
|
|
8924
|
-
};
|
|
8925
|
-
mainSession.emitIrcRelayObservation(relayRecord);
|
|
9128
|
+
return "woken";
|
|
8926
9129
|
}
|
|
8927
9130
|
|
|
8928
9131
|
/**
|
|
8929
9132
|
* Emit an IRC relay observation event on this session for UI rendering only.
|
|
8930
|
-
* Does not persist the record to history.
|
|
9133
|
+
* Does not persist the record to history. Called by the IrcBus to surface
|
|
9134
|
+
* agent↔agent traffic on the main session.
|
|
8931
9135
|
*/
|
|
8932
9136
|
emitIrcRelayObservation(record: CustomMessage): void {
|
|
8933
9137
|
void this.#emitSessionEvent({ type: "irc_message", message: record });
|
|
@@ -8939,7 +9143,7 @@ export class AgentSession {
|
|
|
8939
9143
|
* does not block on, or interfere with, any in-flight main turn. The
|
|
8940
9144
|
* session's history and persisted state are NOT modified by this call.
|
|
8941
9145
|
*
|
|
8942
|
-
* Used by `
|
|
9146
|
+
* Used by `BtwController` (`/btw`) and `OmfgController` (`/omfg`) to share
|
|
8943
9147
|
* the snapshot + stream pipeline. The snapshot includes any in-flight
|
|
8944
9148
|
* streaming assistant text so the model sees the half-finished response
|
|
8945
9149
|
* rather than missing context.
|
|
@@ -8982,6 +9186,7 @@ export class AgentSession {
|
|
|
8982
9186
|
promptCacheKey: cacheSessionId,
|
|
8983
9187
|
preferWebsockets: false,
|
|
8984
9188
|
reasoning: toReasoningEffort(this.thinkingLevel),
|
|
9189
|
+
disableReasoning: shouldDisableReasoning(this.thinkingLevel),
|
|
8985
9190
|
hideThinkingSummary: this.agent.hideThinkingSummary,
|
|
8986
9191
|
serviceTier: this.serviceTier,
|
|
8987
9192
|
signal: args.signal,
|
|
@@ -8990,17 +9195,27 @@ export class AgentSession {
|
|
|
8990
9195
|
model.provider,
|
|
8991
9196
|
);
|
|
8992
9197
|
|
|
8993
|
-
let
|
|
9198
|
+
let providerReplyText = "";
|
|
9199
|
+
let emittedReplyText = "";
|
|
8994
9200
|
let assistantMessage: AssistantMessage | undefined;
|
|
8995
|
-
const stream = streamSimple(model, context, options);
|
|
9201
|
+
const stream = streamSimple(model, obfuscateProviderContext(this.#obfuscator, context), options);
|
|
8996
9202
|
for await (const event of stream) {
|
|
8997
9203
|
if (event.type === "text_delta") {
|
|
8998
|
-
|
|
8999
|
-
if (args.onTextDelta)
|
|
9204
|
+
providerReplyText += event.delta;
|
|
9205
|
+
if (args.onTextDelta) {
|
|
9206
|
+
const readyText = this.#deobfuscatedProviderTextReadyForDelta(providerReplyText);
|
|
9207
|
+
if (readyText.length > emittedReplyText.length) {
|
|
9208
|
+
const delta = readyText.slice(emittedReplyText.length);
|
|
9209
|
+
emittedReplyText = readyText;
|
|
9210
|
+
args.onTextDelta(delta);
|
|
9211
|
+
}
|
|
9212
|
+
}
|
|
9000
9213
|
continue;
|
|
9001
9214
|
}
|
|
9002
9215
|
if (event.type === "done") {
|
|
9003
|
-
assistantMessage =
|
|
9216
|
+
assistantMessage = this.#obfuscator?.hasSecrets()
|
|
9217
|
+
? { ...event.message, content: this.#obfuscator.deobfuscateObject(event.message.content) }
|
|
9218
|
+
: event.message;
|
|
9004
9219
|
break;
|
|
9005
9220
|
}
|
|
9006
9221
|
if (event.type === "error") {
|
|
@@ -9011,8 +9226,12 @@ export class AgentSession {
|
|
|
9011
9226
|
if (!assistantMessage) {
|
|
9012
9227
|
throw new Error("Ephemeral turn ended without a final message");
|
|
9013
9228
|
}
|
|
9229
|
+
const replyText = this.#deobfuscateFromProvider(providerReplyText);
|
|
9230
|
+
if (args.onTextDelta && replyText.length > emittedReplyText.length) {
|
|
9231
|
+
args.onTextDelta(replyText.slice(emittedReplyText.length));
|
|
9232
|
+
}
|
|
9014
9233
|
return {
|
|
9015
|
-
replyText: args.dedupeReply === false ? replyText.trim() :
|
|
9234
|
+
replyText: args.dedupeReply === false ? replyText.trim() : dedupeEphemeralReply(replyText.trim()),
|
|
9016
9235
|
assistantMessage,
|
|
9017
9236
|
};
|
|
9018
9237
|
}
|
|
@@ -9063,46 +9282,21 @@ export class AgentSession {
|
|
|
9063
9282
|
return messages;
|
|
9064
9283
|
}
|
|
9065
9284
|
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
this.#
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
this.#scheduledBackgroundExchangeFlush = false;
|
|
9082
|
-
return;
|
|
9083
|
-
}
|
|
9084
|
-
if (this.isStreaming) {
|
|
9085
|
-
setTimeout(attempt, 50);
|
|
9086
|
-
return;
|
|
9087
|
-
}
|
|
9088
|
-
this.#scheduledBackgroundExchangeFlush = false;
|
|
9089
|
-
this.#flushPendingBackgroundExchanges();
|
|
9090
|
-
};
|
|
9091
|
-
setTimeout(attempt, 0);
|
|
9092
|
-
}
|
|
9093
|
-
|
|
9094
|
-
#flushPendingBackgroundExchanges(): void {
|
|
9095
|
-
if (this.#pendingBackgroundExchanges.length === 0) return;
|
|
9096
|
-
const batches = this.#pendingBackgroundExchanges;
|
|
9097
|
-
this.#pendingBackgroundExchanges = [];
|
|
9098
|
-
for (const batch of batches) {
|
|
9099
|
-
for (const msg of batch) {
|
|
9100
|
-
// emitExternalEvent on message_end appends to agent state and dispatches
|
|
9101
|
-
// to all session listeners, which in turn handle TUI rendering and
|
|
9102
|
-
// sessionManager persistence via #handleAgentEvent.
|
|
9103
|
-
this.agent.emitExternalEvent({ type: "message_start", message: msg });
|
|
9104
|
-
this.agent.emitExternalEvent({ type: "message_end", message: msg });
|
|
9105
|
-
}
|
|
9285
|
+
/**
|
|
9286
|
+
* Persist any IRC asides that missed their step-boundary injection (the
|
|
9287
|
+
* message landed after the turn's last aside drain). Called at the start
|
|
9288
|
+
* of the next prompt so the model still sees them.
|
|
9289
|
+
*/
|
|
9290
|
+
#flushPendingIrcAsides(): void {
|
|
9291
|
+
if (this.#pendingIrcAsides.length === 0) return;
|
|
9292
|
+
const records = this.#pendingIrcAsides;
|
|
9293
|
+
this.#pendingIrcAsides = [];
|
|
9294
|
+
for (const record of records) {
|
|
9295
|
+
// emitExternalEvent on message_end appends to agent state and dispatches
|
|
9296
|
+
// to all session listeners, which in turn handle TUI rendering and
|
|
9297
|
+
// sessionManager persistence via #handleAgentEvent.
|
|
9298
|
+
this.agent.emitExternalEvent({ type: "message_start", message: record });
|
|
9299
|
+
this.agent.emitExternalEvent({ type: "message_end", message: record });
|
|
9106
9300
|
}
|
|
9107
9301
|
}
|
|
9108
9302
|
|
|
@@ -9270,7 +9464,7 @@ export class AgentSession {
|
|
|
9270
9464
|
this.#autoResolvedLevel = undefined;
|
|
9271
9465
|
this.#thinkingLevel = resolveThinkingLevelForModel(this.model, restoredThinkingLevel);
|
|
9272
9466
|
}
|
|
9273
|
-
this
|
|
9467
|
+
this.#applyThinkingLevelToAgent(this.#thinkingLevel);
|
|
9274
9468
|
this.agent.serviceTier = hasServiceTierEntry
|
|
9275
9469
|
? sessionContext.serviceTier
|
|
9276
9470
|
: configuredServiceTier === "none"
|
|
@@ -9327,7 +9521,7 @@ export class AgentSession {
|
|
|
9327
9521
|
this.#thinkingLevel = previousThinkingLevel;
|
|
9328
9522
|
this.#autoThinking = previousAutoThinking;
|
|
9329
9523
|
this.#autoResolvedLevel = previousAutoResolvedLevel;
|
|
9330
|
-
this
|
|
9524
|
+
this.#applyThinkingLevelToAgent(previousThinkingLevel);
|
|
9331
9525
|
this.agent.serviceTier = previousServiceTier;
|
|
9332
9526
|
this.#syncTodoPhasesFromBranch();
|
|
9333
9527
|
this.#reconnectToAgent();
|
|
@@ -9511,10 +9705,10 @@ export class AgentSession {
|
|
|
9511
9705
|
model,
|
|
9512
9706
|
apiKey,
|
|
9513
9707
|
signal: this.#branchSummaryAbortController.signal,
|
|
9514
|
-
customInstructions: options.customInstructions,
|
|
9708
|
+
customInstructions: this.#obfuscateTextForProvider(options.customInstructions),
|
|
9515
9709
|
reserveTokens: branchSummarySettings.reserveTokens,
|
|
9516
9710
|
metadata: this.agent.metadataForProvider(model.provider),
|
|
9517
|
-
convertToLlm,
|
|
9711
|
+
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
9518
9712
|
telemetry: resolveTelemetry(this.agent.telemetry, this.sessionId),
|
|
9519
9713
|
});
|
|
9520
9714
|
this.#branchSummaryAbortController = undefined;
|
|
@@ -9907,69 +10101,6 @@ export class AgentSession {
|
|
|
9907
10101
|
});
|
|
9908
10102
|
}
|
|
9909
10103
|
|
|
9910
|
-
/**
|
|
9911
|
-
* Format the conversation as compact context for subagents.
|
|
9912
|
-
* Includes only user messages and assistant text responses.
|
|
9913
|
-
* Excludes: system prompt, tool definitions, tool calls/results, thinking blocks.
|
|
9914
|
-
*/
|
|
9915
|
-
formatCompactContext(): string {
|
|
9916
|
-
const lines: string[] = [];
|
|
9917
|
-
lines.push("# Conversation Context");
|
|
9918
|
-
lines.push("");
|
|
9919
|
-
lines.push(
|
|
9920
|
-
"This is a summary of the parent conversation. Read this if you need additional context about what was discussed or decided.",
|
|
9921
|
-
);
|
|
9922
|
-
lines.push("");
|
|
9923
|
-
|
|
9924
|
-
for (const msg of this.messages) {
|
|
9925
|
-
if (msg.role === "user" || msg.role === "developer") {
|
|
9926
|
-
lines.push(msg.role === "developer" ? "## Developer" : "## User");
|
|
9927
|
-
lines.push("");
|
|
9928
|
-
if (typeof msg.content === "string") {
|
|
9929
|
-
lines.push(msg.content);
|
|
9930
|
-
} else {
|
|
9931
|
-
for (const c of msg.content) {
|
|
9932
|
-
if (c.type === "text") {
|
|
9933
|
-
lines.push(c.text);
|
|
9934
|
-
} else if (c.type === "image") {
|
|
9935
|
-
lines.push("[Image attached]");
|
|
9936
|
-
}
|
|
9937
|
-
}
|
|
9938
|
-
}
|
|
9939
|
-
lines.push("");
|
|
9940
|
-
} else if (msg.role === "assistant") {
|
|
9941
|
-
const assistantMsg = msg as AssistantMessage;
|
|
9942
|
-
// Only include text content, skip tool calls and thinking
|
|
9943
|
-
const textParts: string[] = [];
|
|
9944
|
-
for (const c of assistantMsg.content) {
|
|
9945
|
-
if (c.type === "text" && c.text.trim()) {
|
|
9946
|
-
textParts.push(c.text);
|
|
9947
|
-
}
|
|
9948
|
-
}
|
|
9949
|
-
if (textParts.length > 0) {
|
|
9950
|
-
lines.push("## Assistant");
|
|
9951
|
-
lines.push("");
|
|
9952
|
-
lines.push(textParts.join("\n\n"));
|
|
9953
|
-
lines.push("");
|
|
9954
|
-
}
|
|
9955
|
-
} else if (msg.role === "fileMention") {
|
|
9956
|
-
const fileMsg = msg as FileMentionMessage;
|
|
9957
|
-
const paths = fileMsg.files.map(f => f.path).join(", ");
|
|
9958
|
-
lines.push(`[Files referenced: ${paths}]`);
|
|
9959
|
-
lines.push("");
|
|
9960
|
-
} else if (msg.role === "compactionSummary") {
|
|
9961
|
-
const compactMsg = msg as CompactionSummaryMessage;
|
|
9962
|
-
lines.push("## Earlier Context (Summarized)");
|
|
9963
|
-
lines.push("");
|
|
9964
|
-
lines.push(compactMsg.summary);
|
|
9965
|
-
lines.push("");
|
|
9966
|
-
}
|
|
9967
|
-
// Skip: toolResult, bashExecution, pythonExecution, branchSummary, custom, hookMessage
|
|
9968
|
-
}
|
|
9969
|
-
|
|
9970
|
-
return lines.join("\n").trim();
|
|
9971
|
-
}
|
|
9972
|
-
|
|
9973
10104
|
// =========================================================================
|
|
9974
10105
|
// Extension System
|
|
9975
10106
|
// =========================================================================
|