@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.1
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 +123 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- package/scripts/build-binary.ts +5 -0
- package/scripts/format-prompts.ts +1 -1
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +11 -29
- package/src/commands/acp.ts +24 -0
- package/src/commands/launch.ts +6 -4
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +13 -2
- package/src/config/model-resolver.ts +31 -4
- package/src/config/settings-schema.ts +102 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/edit/index.ts +22 -1
- package/src/edit/modes/patch.ts +10 -0
- package/src/edit/modes/replace.ts +3 -0
- package/src/edit/renderer.ts +17 -1
- package/src/eval/js/context-manager.ts +1 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +122 -50
- package/src/eval/js/shared/runtime.ts +31 -4
- package/src/eval/js/tool-bridge.ts +43 -21
- package/src/eval/py/executor.ts +5 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/runner.ts +55 -2
- package/src/extensibility/extensions/types.ts +98 -221
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +42 -1
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +9 -10
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +577 -0
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +6 -3
- package/src/internal-urls/types.ts +22 -1
- package/src/main.ts +24 -11
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +412 -71
- package/src/modes/acp/acp-client-bridge.ts +152 -0
- package/src/modes/acp/acp-event-mapper.ts +180 -15
- package/src/modes/acp/terminal-auth.ts +37 -0
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/read-tool-group.ts +29 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +55 -4
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +27 -10
- package/src/modes/controllers/event-controller.ts +60 -18
- package/src/modes/controllers/extension-ui-controller.ts +8 -2
- package/src/modes/controllers/input-controller.ts +85 -39
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +675 -39
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/rpc-mode.ts +30 -88
- package/src/modes/runtime-init.ts +115 -0
- package/src/modes/theme/defaults/dark-poimandres.json +2 -0
- package/src/modes/theme/defaults/light-poimandres.json +2 -0
- package/src/modes/theme/theme.ts +18 -6
- package/src/modes/types.ts +20 -5
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +25 -6
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/agents/designer.md +5 -5
- package/src/prompts/agents/explore.md +7 -7
- package/src/prompts/agents/init.md +9 -9
- package/src/prompts/agents/librarian.md +14 -14
- package/src/prompts/agents/plan.md +4 -4
- package/src/prompts/agents/reviewer.md +5 -5
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/commands/orchestrate.md +2 -2
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/memories/consolidation.md +2 -2
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +17 -17
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +2 -2
- package/src/prompts/system/eager-todo.md +6 -6
- package/src/prompts/system/handoff-document.md +1 -1
- package/src/prompts/system/plan-mode-active.md +25 -24
- package/src/prompts/system/plan-mode-approved.md +4 -4
- package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
- package/src/prompts/system/project-prompt.md +4 -4
- package/src/prompts/system/subagent-system-prompt.md +7 -7
- package/src/prompts/system/subagent-yield-reminder.md +4 -4
- package/src/prompts/system/system-prompt.md +72 -71
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/tools/apply-patch.md +1 -1
- package/src/prompts/tools/ast-edit.md +3 -3
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/checkpoint.md +3 -3
- package/src/prompts/tools/find.md +3 -3
- package/src/prompts/tools/github.md +2 -5
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +104 -116
- package/src/prompts/tools/image-gen.md +3 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +6 -6
- package/src/prompts/tools/read.md +8 -7
- package/src/prompts/tools/replace.md +5 -5
- package/src/prompts/tools/resolve.md +6 -5
- package/src/prompts/tools/retain.md +1 -1
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search.md +2 -2
- package/src/prompts/tools/ssh.md +2 -2
- package/src/prompts/tools/task.md +12 -6
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +3 -3
- package/src/sdk.ts +81 -17
- package/src/session/agent-session.ts +656 -125
- package/src/session/blob-store.ts +36 -3
- package/src/session/client-bridge.ts +81 -0
- package/src/session/compaction/errors.ts +31 -0
- package/src/session/compaction/index.ts +1 -0
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/acp-builtins.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +717 -116
- package/src/slash-commands/helpers/context-report.ts +39 -0
- package/src/slash-commands/helpers/format.ts +23 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +532 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/ssh.ts +193 -0
- package/src/slash-commands/helpers/todo.ts +279 -0
- package/src/slash-commands/helpers/usage-report.ts +91 -0
- package/src/slash-commands/types.ts +126 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/executor.ts +27 -10
- package/src/task/index.ts +20 -1
- package/src/task/render.ts +27 -18
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +203 -6
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +21 -10
- package/src/tools/eval.ts +3 -1
- package/src/tools/fetch.ts +15 -4
- package/src/tools/find.ts +39 -39
- package/src/tools/gh-renderer.ts +0 -12
- package/src/tools/gh.ts +689 -182
- package/src/tools/github-cache.ts +548 -0
- package/src/tools/index.ts +25 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/output-meta.ts +176 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +605 -239
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/write.ts +67 -10
- package/src/tui/code-cell.ts +70 -2
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/gemini.ts +35 -95
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import * as crypto from "node:crypto";
|
|
17
17
|
import * as fs from "node:fs";
|
|
18
18
|
import * as path from "node:path";
|
|
19
|
-
|
|
19
|
+
import { scheduler } from "node:timers/promises";
|
|
20
20
|
import {
|
|
21
21
|
type Agent,
|
|
22
22
|
AgentBusyError,
|
|
@@ -47,14 +47,20 @@ import {
|
|
|
47
47
|
calculateRateLimitBackoffMs,
|
|
48
48
|
getSupportedEfforts,
|
|
49
49
|
isContextOverflow,
|
|
50
|
-
isUnexpectedSocketCloseMessage,
|
|
51
50
|
isUsageLimitError,
|
|
52
51
|
modelsAreEqual,
|
|
53
52
|
parseRateLimitReason,
|
|
54
53
|
streamSimple,
|
|
55
54
|
} from "@oh-my-pi/pi-ai";
|
|
56
55
|
import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
|
|
57
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
getAgentDbPath,
|
|
58
|
+
isEnoent,
|
|
59
|
+
isUnexpectedSocketCloseMessage,
|
|
60
|
+
logger,
|
|
61
|
+
prompt,
|
|
62
|
+
Snowflake,
|
|
63
|
+
} from "@oh-my-pi/pi-utils";
|
|
58
64
|
import { type AsyncJob, AsyncJobManager } from "../async";
|
|
59
65
|
import type { Rule } from "../capability/rule";
|
|
60
66
|
import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
|
|
@@ -104,6 +110,8 @@ import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
|
104
110
|
import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
105
111
|
import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
106
112
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
113
|
+
import { GoalRuntime } from "../goals/runtime";
|
|
114
|
+
import type { Goal, GoalModeState } from "../goals/state";
|
|
107
115
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
108
116
|
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
109
117
|
import {
|
|
@@ -143,7 +151,7 @@ import { outputMeta } from "../tools/output-meta";
|
|
|
143
151
|
import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
|
144
152
|
import { isAutoQaEnabled } from "../tools/report-tool-issue";
|
|
145
153
|
import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
|
|
146
|
-
import { ToolError } from "../tools/tool-errors";
|
|
154
|
+
import { ToolAbortError, ToolError } from "../tools/tool-errors";
|
|
147
155
|
import { clampTimeout } from "../tools/tool-timeouts";
|
|
148
156
|
import { parseCommandArgs } from "../utils/command-args";
|
|
149
157
|
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
@@ -151,7 +159,9 @@ import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
|
151
159
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
152
160
|
import { buildNamedToolChoice } from "../utils/tool-choice";
|
|
153
161
|
import type { AuthStorage } from "./auth-storage";
|
|
162
|
+
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
154
163
|
import {
|
|
164
|
+
CompactionCancelledError,
|
|
155
165
|
type CompactionPreparation,
|
|
156
166
|
type CompactionResult,
|
|
157
167
|
calculateContextTokens,
|
|
@@ -172,6 +182,8 @@ import {
|
|
|
172
182
|
convertToLlm,
|
|
173
183
|
type FileMentionMessage,
|
|
174
184
|
type PythonExecutionMessage,
|
|
185
|
+
readPendingDisplayTag,
|
|
186
|
+
SILENT_ABORT_MARKER,
|
|
175
187
|
} from "./messages";
|
|
176
188
|
import { formatSessionDumpText } from "./session-dump-format";
|
|
177
189
|
import type {
|
|
@@ -206,7 +218,9 @@ export type AgentSessionEvent =
|
|
|
206
218
|
| { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
|
|
207
219
|
| { type: "todo_auto_clear" }
|
|
208
220
|
| { type: "irc_message"; message: CustomMessage }
|
|
209
|
-
| { type: "notice"; level: "info" | "warning" | "error"; message: string; source?: string }
|
|
221
|
+
| { type: "notice"; level: "info" | "warning" | "error"; message: string; source?: string }
|
|
222
|
+
| { type: "thinking_level_changed"; thinkingLevel: ThinkingLevel | undefined }
|
|
223
|
+
| { type: "goal_updated"; goal: Goal | null; state?: GoalModeState };
|
|
210
224
|
|
|
211
225
|
/** Listener function for agent session events */
|
|
212
226
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
@@ -498,10 +512,107 @@ const noOpUIContext: ExtensionUIContext = {
|
|
|
498
512
|
setToolsExpanded: () => {},
|
|
499
513
|
};
|
|
500
514
|
|
|
515
|
+
// ============================================================================
|
|
516
|
+
// ACP Permission Gate
|
|
517
|
+
// ============================================================================
|
|
518
|
+
|
|
519
|
+
/** Tools that require user permission before execution when an ACP client is connected. */
|
|
520
|
+
const PERMISSION_REQUIRED_TOOLS = new Set(["bash", "edit", "write", "ast_edit", "delete", "move"]);
|
|
521
|
+
|
|
522
|
+
/** Permission options presented to the client on each gated tool call. */
|
|
523
|
+
const PERMISSION_OPTIONS: ClientBridgePermissionOption[] = [
|
|
524
|
+
{ optionId: "allow_once", name: "Allow once", kind: "allow_once" },
|
|
525
|
+
{ optionId: "allow_always", name: "Always allow", kind: "allow_always" },
|
|
526
|
+
{ optionId: "reject_once", name: "Reject", kind: "reject_once" },
|
|
527
|
+
{ optionId: "reject_always", name: "Always reject", kind: "reject_always" },
|
|
528
|
+
];
|
|
529
|
+
|
|
530
|
+
const PERMISSION_OPTIONS_BY_ID = new Map(PERMISSION_OPTIONS.map(option => [option.optionId, option]));
|
|
531
|
+
|
|
532
|
+
function derivePermissionTitle(toolName: string, args: unknown): string {
|
|
533
|
+
const a = args && typeof args === "object" ? (args as Record<string, unknown>) : {};
|
|
534
|
+
if (toolName === "bash") {
|
|
535
|
+
const cmd = typeof a.command === "string" ? a.command.slice(0, 80) : undefined;
|
|
536
|
+
if (cmd) return cmd;
|
|
537
|
+
} else if (toolName === "edit" || toolName === "write" || toolName === "delete") {
|
|
538
|
+
const p = typeof a.path === "string" ? a.path : undefined;
|
|
539
|
+
if (p) {
|
|
540
|
+
const verb = toolName === "edit" ? "Edit" : toolName === "write" ? "Write" : "Delete";
|
|
541
|
+
return `${verb} ${p}`;
|
|
542
|
+
}
|
|
543
|
+
} else if (toolName === "move") {
|
|
544
|
+
const from =
|
|
545
|
+
typeof a.oldPath === "string"
|
|
546
|
+
? a.oldPath
|
|
547
|
+
: typeof a.path === "string"
|
|
548
|
+
? a.path
|
|
549
|
+
: typeof a.from === "string"
|
|
550
|
+
? a.from
|
|
551
|
+
: undefined;
|
|
552
|
+
const to =
|
|
553
|
+
typeof a.newPath === "string"
|
|
554
|
+
? a.newPath
|
|
555
|
+
: typeof a.to === "string"
|
|
556
|
+
? a.to
|
|
557
|
+
: typeof a.destination === "string"
|
|
558
|
+
? a.destination
|
|
559
|
+
: undefined;
|
|
560
|
+
if (from && to) return `Move ${from} to ${to}`;
|
|
561
|
+
if (from) return `Move ${from}`;
|
|
562
|
+
} else if (toolName === "ast_edit") {
|
|
563
|
+
const paths = Array.isArray(a.paths)
|
|
564
|
+
? (a.paths as unknown[]).filter(x => typeof x === "string").join(", ")
|
|
565
|
+
: undefined;
|
|
566
|
+
if (paths) return `AST edit ${paths}`;
|
|
567
|
+
}
|
|
568
|
+
return toolName;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function extractPermissionLocations(args: unknown, cwd: string): { path: string; line?: number }[] {
|
|
572
|
+
if (!args || typeof args !== "object") return [];
|
|
573
|
+
const a = args as Record<string, unknown>;
|
|
574
|
+
const out: { path: string; line?: number }[] = [];
|
|
575
|
+
const pushPath = (value: unknown) => {
|
|
576
|
+
if (typeof value !== "string" || value.length === 0) return;
|
|
577
|
+
// ACP locations carry file paths that the editor host will open or focus;
|
|
578
|
+
// they must be absolute or the client cannot resolve them. Resolve raw
|
|
579
|
+
// tool args (often cwd-relative) against the session cwd before sending.
|
|
580
|
+
let resolved: string;
|
|
581
|
+
try {
|
|
582
|
+
resolved = resolveToCwd(value, cwd);
|
|
583
|
+
} catch {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (out.some(location => location.path === resolved)) return;
|
|
587
|
+
out.push({ path: resolved });
|
|
588
|
+
};
|
|
589
|
+
pushPath(a.path);
|
|
590
|
+
pushPath(a.file);
|
|
591
|
+
if (Array.isArray(a.paths)) {
|
|
592
|
+
for (const p of a.paths) {
|
|
593
|
+
pushPath(p);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
pushPath(a.oldPath);
|
|
597
|
+
pushPath(a.newPath);
|
|
598
|
+
pushPath(a.from);
|
|
599
|
+
pushPath(a.to);
|
|
600
|
+
pushPath(a.source);
|
|
601
|
+
pushPath(a.destination);
|
|
602
|
+
return out;
|
|
603
|
+
}
|
|
604
|
+
|
|
501
605
|
// ============================================================================
|
|
502
606
|
// AgentSession Class
|
|
503
607
|
// ============================================================================
|
|
504
608
|
|
|
609
|
+
/** Internal record stored in the steering/followUp display queues. The optional
|
|
610
|
+
* `tag` is set only by `enqueueCustomMessageDisplay` (used for skill-prompt
|
|
611
|
+
* custom messages queued during streaming) and is matched by the custom-role
|
|
612
|
+
* `message_start` dequeue branch; user-message pushes leave it undefined and
|
|
613
|
+
* rely on the existing text-equality match. */
|
|
614
|
+
type QueuedDisplayEntry = { text: string; tag?: string };
|
|
615
|
+
|
|
505
616
|
export class AgentSession {
|
|
506
617
|
readonly agent: Agent;
|
|
507
618
|
readonly sessionManager: SessionManager;
|
|
@@ -520,16 +631,27 @@ export class AgentSession {
|
|
|
520
631
|
#unsubscribeAgent?: () => void;
|
|
521
632
|
#eventListeners: AgentSessionEventListener[] = [];
|
|
522
633
|
|
|
523
|
-
/** Tracks pending steering messages for UI display. Removed when delivered.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
634
|
+
/** Tracks pending steering messages for UI display. Removed when delivered.
|
|
635
|
+
* Entry shape: `{ text }` for plain-text steers (user-message dequeue
|
|
636
|
+
* matches by `.text`); `{ text, tag }` for queued custom messages (skill
|
|
637
|
+
* invocations dispatched while streaming) — the custom-role dequeue
|
|
638
|
+
* matches by `.tag` so duplicate-args queued skills cannot collide. */
|
|
639
|
+
#steeringMessages: QueuedDisplayEntry[] = [];
|
|
640
|
+
/** Tracks pending follow-up messages for UI display. Removed when delivered.
|
|
641
|
+
* See `#steeringMessages` for entry shape. */
|
|
642
|
+
#followUpMessages: QueuedDisplayEntry[] = [];
|
|
527
643
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
528
644
|
#pendingNextTurnMessages: CustomMessage[] = [];
|
|
529
645
|
#scheduledHiddenNextTurnGeneration: number | undefined = undefined;
|
|
530
646
|
#planModeState: PlanModeState | undefined;
|
|
647
|
+
#goalModeState: GoalModeState | undefined;
|
|
648
|
+
#goalRuntime: GoalRuntime;
|
|
649
|
+
#goalTurnCounter = 0;
|
|
531
650
|
#planReferenceSent = false;
|
|
532
651
|
#planReferencePath = "local://PLAN.md";
|
|
652
|
+
#clientBridge: ClientBridge | undefined;
|
|
653
|
+
/** Per-session memory of allow_always / reject_always decisions for gated tools. */
|
|
654
|
+
#acpPermissionDecisions: Map<string, "allow_always" | "reject_always"> = new Map();
|
|
533
655
|
|
|
534
656
|
// Compaction state
|
|
535
657
|
#compactionAbortController: AbortController | undefined = undefined;
|
|
@@ -634,6 +756,19 @@ export class AgentSession {
|
|
|
634
756
|
#ttsrRetryToken = 0;
|
|
635
757
|
#ttsrResumePromise: Promise<void> | undefined = undefined;
|
|
636
758
|
#ttsrResumeResolve: (() => void) | undefined = undefined;
|
|
759
|
+
|
|
760
|
+
/** One-shot flag set in InteractiveMode.#approvePlan(compactBeforeExecute=true)
|
|
761
|
+
* before the plan-mode → compaction transition. Consumed inside
|
|
762
|
+
* #handleAgentEvent for the matching `message_end` + `stopReason: "aborted"`;
|
|
763
|
+
* cleared unconditionally by the caller's `finally` so it cannot leak into
|
|
764
|
+
* later unrelated aborts (e.g. when compaction returns cancelled/failed
|
|
765
|
+
* without producing an aborted message_end). */
|
|
766
|
+
#planCompactAbortPending = false;
|
|
767
|
+
|
|
768
|
+
/** Monotonic counter for `enqueueCustomMessageDisplay` tag generation;
|
|
769
|
+
* combined with `Date.now()` so tags stay unique even across rapid
|
|
770
|
+
* same-tick enqueues. */
|
|
771
|
+
#customDisplayTagCounter = 0;
|
|
637
772
|
#postPromptTasks = new Set<Promise<void>>();
|
|
638
773
|
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
639
774
|
#postPromptTasksResolve: (() => void) | undefined = undefined;
|
|
@@ -783,6 +918,44 @@ export class AgentSession {
|
|
|
783
918
|
this.agent.providerSessionState = this.#providerSessionState;
|
|
784
919
|
this.#syncAgentSessionId();
|
|
785
920
|
this.#syncTodoPhasesFromBranch();
|
|
921
|
+
this.#goalRuntime = new GoalRuntime({
|
|
922
|
+
getState: () => this.#goalModeState,
|
|
923
|
+
setState: state => {
|
|
924
|
+
this.#goalModeState = state;
|
|
925
|
+
},
|
|
926
|
+
getCurrentUsage: () => {
|
|
927
|
+
const usage = this.getSessionStats().tokens;
|
|
928
|
+
return {
|
|
929
|
+
input: usage.input,
|
|
930
|
+
output: usage.output,
|
|
931
|
+
cacheRead: usage.cacheRead,
|
|
932
|
+
cacheWrite: usage.cacheWrite,
|
|
933
|
+
};
|
|
934
|
+
},
|
|
935
|
+
emit: event => {
|
|
936
|
+
if (event.type === "goal_updated") {
|
|
937
|
+
return this.#emitSessionEvent({ type: "goal_updated", goal: event.goal, state: event.state });
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
persist: (mode, state) => {
|
|
941
|
+
if (mode === "none") {
|
|
942
|
+
this.sessionManager.appendModeChange("none");
|
|
943
|
+
} else if (state) {
|
|
944
|
+
this.sessionManager.appendModeChange(mode, { goal: state.goal });
|
|
945
|
+
}
|
|
946
|
+
},
|
|
947
|
+
sendHiddenMessage: async message => {
|
|
948
|
+
await this.sendCustomMessage(
|
|
949
|
+
{
|
|
950
|
+
customType: message.customType,
|
|
951
|
+
content: message.content,
|
|
952
|
+
display: false,
|
|
953
|
+
attribution: "agent",
|
|
954
|
+
},
|
|
955
|
+
{ deliverAs: message.deliverAs },
|
|
956
|
+
);
|
|
957
|
+
},
|
|
958
|
+
});
|
|
786
959
|
|
|
787
960
|
// Always subscribe to agent events for internal handling
|
|
788
961
|
// (session persistence, hooks, auto-compaction, retry logic)
|
|
@@ -830,6 +1003,19 @@ export class AgentSession {
|
|
|
830
1003
|
return this.#toolChoiceQueue.peekInFlightInvoker();
|
|
831
1004
|
}
|
|
832
1005
|
|
|
1006
|
+
/** Standing (long-lived) handler the `resolve` tool falls back to when no
|
|
1007
|
+
* queue invoker is in flight. Used by plan mode so the agent can submit
|
|
1008
|
+
* approval via `resolve` without forcing the tool choice every turn. */
|
|
1009
|
+
#standingResolveHandler: ((input: unknown) => Promise<unknown> | unknown) | undefined;
|
|
1010
|
+
|
|
1011
|
+
peekStandingResolveHandler(): ((input: unknown) => Promise<unknown> | unknown) | undefined {
|
|
1012
|
+
return this.#standingResolveHandler;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
setStandingResolveHandler(handler: ((input: unknown) => Promise<unknown> | unknown) | null): void {
|
|
1016
|
+
this.#standingResolveHandler = handler ?? undefined;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
833
1019
|
/** Provider-scoped mutable state store for transport/session caches. */
|
|
834
1020
|
get providerSessionState(): Map<string, ProviderSessionState> {
|
|
835
1021
|
return this.#providerSessionState;
|
|
@@ -855,6 +1041,49 @@ export class AgentSession {
|
|
|
855
1041
|
return this.#ttsrAbortPending;
|
|
856
1042
|
}
|
|
857
1043
|
|
|
1044
|
+
/** Whether the plan-mode → compaction transition's expected internal abort is
|
|
1045
|
+
* pending. Consumed by `#handleAgentEvent` to stamp `SILENT_ABORT_MARKER`
|
|
1046
|
+
* on the next aborted assistant message_end; cleared unconditionally by
|
|
1047
|
+
* `InteractiveMode.#approvePlan`'s `finally` block. */
|
|
1048
|
+
get isPlanCompactAbortPending(): boolean {
|
|
1049
|
+
return this.#planCompactAbortPending;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/** Arm the silent-abort marker for the next aborted assistant message_end.
|
|
1053
|
+
* Caller MUST clear via `clearPlanCompactAbortPending()` in a `finally`
|
|
1054
|
+
* to guarantee no leak. */
|
|
1055
|
+
markPlanCompactAbortPending(): void {
|
|
1056
|
+
this.#planCompactAbortPending = true;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/** Unconditionally clear the silent-abort flag. Idempotent: safe when the
|
|
1060
|
+
* flag was never set OR was already consumed by `#handleAgentEvent`. */
|
|
1061
|
+
clearPlanCompactAbortPending(): void {
|
|
1062
|
+
this.#planCompactAbortPending = false;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/** Register a compact display string for a custom message that the caller is
|
|
1066
|
+
* about to dispatch via `promptCustomMessage` / `sendCustomMessage`.
|
|
1067
|
+
* Returns a stable tag the caller MUST embed in
|
|
1068
|
+
* `CustomMessage.details.__pendingDisplayTag` so the agent-side
|
|
1069
|
+
* `message_start` handler can remove the matching display entry when the
|
|
1070
|
+
* queued message is consumed.
|
|
1071
|
+
*
|
|
1072
|
+
* Does NOT push to the agent's steering/followUp queue — that happens
|
|
1073
|
+
* separately inside `sendCustomMessage`. */
|
|
1074
|
+
enqueueCustomMessageDisplay(text: string, mode: "steer" | "followUp"): string {
|
|
1075
|
+
const tag = `omp-cmd-${Date.now()}-${++this.#customDisplayTagCounter}`;
|
|
1076
|
+
const displayText = text.trim();
|
|
1077
|
+
if (!displayText) return tag;
|
|
1078
|
+
const entry: QueuedDisplayEntry = { text: displayText, tag };
|
|
1079
|
+
if (mode === "steer") {
|
|
1080
|
+
this.#steeringMessages.push(entry);
|
|
1081
|
+
} else {
|
|
1082
|
+
this.#followUpMessages.push(entry);
|
|
1083
|
+
}
|
|
1084
|
+
return tag;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
858
1087
|
getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
|
|
859
1088
|
const manager = AsyncJobManager.instance();
|
|
860
1089
|
if (!manager) return null;
|
|
@@ -943,13 +1172,13 @@ export class AgentSession {
|
|
|
943
1172
|
if (event.type === "message_start" && event.message.role === "user") {
|
|
944
1173
|
const messageText = this.#getUserMessageText(event.message);
|
|
945
1174
|
if (messageText) {
|
|
946
|
-
// Check steering queue first
|
|
947
|
-
const steeringIndex = this.#steeringMessages.
|
|
1175
|
+
// Check steering queue first (match by .text on tagged records)
|
|
1176
|
+
const steeringIndex = this.#steeringMessages.findIndex(e => e.text === messageText);
|
|
948
1177
|
if (steeringIndex !== -1) {
|
|
949
1178
|
this.#steeringMessages.splice(steeringIndex, 1);
|
|
950
1179
|
} else {
|
|
951
1180
|
// Check follow-up queue
|
|
952
|
-
const followUpIndex = this.#followUpMessages.
|
|
1181
|
+
const followUpIndex = this.#followUpMessages.findIndex(e => e.text === messageText);
|
|
953
1182
|
if (followUpIndex !== -1) {
|
|
954
1183
|
this.#followUpMessages.splice(followUpIndex, 1);
|
|
955
1184
|
}
|
|
@@ -957,6 +1186,48 @@ export class AgentSession {
|
|
|
957
1186
|
}
|
|
958
1187
|
}
|
|
959
1188
|
|
|
1189
|
+
// Tag-based dequeue for custom messages (skills queued via promptCustomMessage).
|
|
1190
|
+
// The InputController attached a stable tag via CustomMessage.details when it
|
|
1191
|
+
// registered the display chip; pull it back here to remove the matching entry
|
|
1192
|
+
// from the pending bar atomically with the agent's queue consumption. Match by
|
|
1193
|
+
// tag (not text) — two queued skills with identical args cannot collide.
|
|
1194
|
+
if (event.type === "message_start" && event.message.role === "custom") {
|
|
1195
|
+
const tag = readPendingDisplayTag(event.message.details);
|
|
1196
|
+
if (tag) {
|
|
1197
|
+
const steerIdx = this.#steeringMessages.findIndex(e => e.tag === tag);
|
|
1198
|
+
if (steerIdx !== -1) {
|
|
1199
|
+
this.#steeringMessages.splice(steerIdx, 1);
|
|
1200
|
+
} else {
|
|
1201
|
+
const followUpIdx = this.#followUpMessages.findIndex(e => e.tag === tag);
|
|
1202
|
+
if (followUpIdx !== -1) {
|
|
1203
|
+
this.#followUpMessages.splice(followUpIdx, 1);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Plan-mode → compaction transition: stamp `SILENT_ABORT_MARKER` on the
|
|
1210
|
+
// persisted message BEFORE the obfuscator's display-side copy below.
|
|
1211
|
+
// Invariant (must hold across refactors): this branch precedes the
|
|
1212
|
+
// `let displayEvent = event; ... displayEvent = { ...event, message: { ...message, content: deobfuscated } }`
|
|
1213
|
+
// block. After stamping, both `displayEvent.message` (via the spread)
|
|
1214
|
+
// and `event.message` (in-place mutation, used by SessionManager
|
|
1215
|
+
// persistence) carry the marker, guaranteeing streaming render and
|
|
1216
|
+
// history replay branch identically. The one-shot flag is consumed
|
|
1217
|
+
// here, scoped strictly to this aborted message_end; the caller's
|
|
1218
|
+
// `finally` (in `InteractiveMode.#approvePlan`) clears it again on
|
|
1219
|
+
// every terminal compaction outcome (`ok` / `cancelled` / `failed` /
|
|
1220
|
+
// throw) so a leaked flag cannot silence a later unrelated abort.
|
|
1221
|
+
if (
|
|
1222
|
+
event.type === "message_end" &&
|
|
1223
|
+
event.message.role === "assistant" &&
|
|
1224
|
+
event.message.stopReason === "aborted" &&
|
|
1225
|
+
this.#planCompactAbortPending
|
|
1226
|
+
) {
|
|
1227
|
+
(event.message as AssistantMessage).errorMessage = SILENT_ABORT_MARKER;
|
|
1228
|
+
this.#planCompactAbortPending = false;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
960
1231
|
// Deobfuscate assistant message content for display emission — the LLM echoes back
|
|
961
1232
|
// obfuscated placeholders, but listeners (TUI, extensions, exporters) must see real
|
|
962
1233
|
// values. The original event.message stays obfuscated so the persistence path below
|
|
@@ -972,6 +1243,16 @@ export class AgentSession {
|
|
|
972
1243
|
}
|
|
973
1244
|
}
|
|
974
1245
|
|
|
1246
|
+
if (event.type === "turn_start") {
|
|
1247
|
+
const usage = this.getSessionStats().tokens;
|
|
1248
|
+
this.#goalRuntime.onTurnStart(`turn-${++this.#goalTurnCounter}`, {
|
|
1249
|
+
input: usage.input,
|
|
1250
|
+
output: usage.output,
|
|
1251
|
+
cacheRead: usage.cacheRead,
|
|
1252
|
+
cacheWrite: usage.cacheWrite,
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
|
|
975
1256
|
await this.#emitSessionEvent(displayEvent);
|
|
976
1257
|
|
|
977
1258
|
if (event.type === "turn_start") {
|
|
@@ -995,6 +1276,13 @@ export class AgentSession {
|
|
|
995
1276
|
this.#toolChoiceQueue.resolve();
|
|
996
1277
|
}
|
|
997
1278
|
}
|
|
1279
|
+
if (event.type === "tool_execution_end") {
|
|
1280
|
+
if (event.toolName === "goal") {
|
|
1281
|
+
await this.#goalRuntime.onGoalToolCompleted();
|
|
1282
|
+
} else {
|
|
1283
|
+
await this.#goalRuntime.onToolCompleted(event.toolName);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
998
1286
|
if (event.type === "tool_execution_end" && event.toolName === "yield" && !event.isError) {
|
|
999
1287
|
this.#lastSuccessfulYieldToolCallId = event.toolCallId;
|
|
1000
1288
|
}
|
|
@@ -1230,6 +1518,15 @@ export class AgentSession {
|
|
|
1230
1518
|
|
|
1231
1519
|
// Check auto-retry and auto-compaction after agent completes
|
|
1232
1520
|
if (event.type === "agent_end") {
|
|
1521
|
+
const usage = this.getSessionStats().tokens;
|
|
1522
|
+
await this.#goalRuntime.onAgentEnd({
|
|
1523
|
+
currentUsage: {
|
|
1524
|
+
input: usage.input,
|
|
1525
|
+
output: usage.output,
|
|
1526
|
+
cacheRead: usage.cacheRead,
|
|
1527
|
+
cacheWrite: usage.cacheWrite,
|
|
1528
|
+
},
|
|
1529
|
+
});
|
|
1233
1530
|
const fallbackAssistant = [...event.messages]
|
|
1234
1531
|
.reverse()
|
|
1235
1532
|
.find((message): message is AssistantMessage => message.role === "assistant");
|
|
@@ -1351,7 +1648,7 @@ export class AgentSession {
|
|
|
1351
1648
|
const scheduled = (async () => {
|
|
1352
1649
|
if (delayMs > 0) {
|
|
1353
1650
|
try {
|
|
1354
|
-
await
|
|
1651
|
+
await scheduler.wait(delayMs, { signal });
|
|
1355
1652
|
} catch {
|
|
1356
1653
|
return;
|
|
1357
1654
|
}
|
|
@@ -1385,7 +1682,10 @@ export class AgentSession {
|
|
|
1385
1682
|
try {
|
|
1386
1683
|
await this.#maybeRestoreRetryFallbackPrimary();
|
|
1387
1684
|
await this.agent.continue();
|
|
1388
|
-
} catch {
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
logger.warn("agent.continue failed after scheduling", {
|
|
1687
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1688
|
+
});
|
|
1389
1689
|
options?.onError?.();
|
|
1390
1690
|
}
|
|
1391
1691
|
},
|
|
@@ -2085,6 +2385,12 @@ export class AgentSession {
|
|
|
2085
2385
|
attempt: event.attempt,
|
|
2086
2386
|
maxAttempts: event.maxAttempts,
|
|
2087
2387
|
});
|
|
2388
|
+
} else if (event.type === "goal_updated") {
|
|
2389
|
+
await this.#extensionRunner.emit({
|
|
2390
|
+
type: "goal_updated",
|
|
2391
|
+
goal: event.goal,
|
|
2392
|
+
state: event.state,
|
|
2393
|
+
});
|
|
2088
2394
|
}
|
|
2089
2395
|
}
|
|
2090
2396
|
|
|
@@ -2477,7 +2783,7 @@ export class AgentSession {
|
|
|
2477
2783
|
|
|
2478
2784
|
/** Collect built-in tools the model can discover via search_tool_bm25. Restricted to tool
|
|
2479
2785
|
* definitions whose `loadMode === "discoverable"`. This keeps hidden/internal tools
|
|
2480
|
-
* (resolve, yield,
|
|
2786
|
+
* (resolve, yield, report_finding, report_tool_issue) out of the index
|
|
2481
2787
|
* and avoids mislabeling extension/custom default-inactive tools as built-ins. */
|
|
2482
2788
|
#collectDiscoverableBuiltinTools(): DiscoverableTool[] {
|
|
2483
2789
|
const activeNames = new Set(this.getActiveToolNames());
|
|
@@ -2547,6 +2853,85 @@ export class AgentSession {
|
|
|
2547
2853
|
return [...new Set(activated)];
|
|
2548
2854
|
}
|
|
2549
2855
|
|
|
2856
|
+
/**
|
|
2857
|
+
* Wrap a tool with a permission-gate proxy when an ACP client is connected.
|
|
2858
|
+
* Only wraps tools whose name is in PERMISSION_REQUIRED_TOOLS and only when
|
|
2859
|
+
* the bridge exposes `requestPermission`. No-ops for all other cases.
|
|
2860
|
+
*/
|
|
2861
|
+
#wrapToolForAcpPermission<T extends AgentTool>(tool: T): T {
|
|
2862
|
+
const bridge = this.#clientBridge;
|
|
2863
|
+
// Match the capability+method gating pattern used by read/write/bash.
|
|
2864
|
+
if (!bridge?.capabilities.requestPermission || !bridge.requestPermission) return tool;
|
|
2865
|
+
if (!PERMISSION_REQUIRED_TOOLS.has(tool.name)) return tool;
|
|
2866
|
+
return new Proxy(tool, {
|
|
2867
|
+
get: (target, prop, receiver) => {
|
|
2868
|
+
if (prop !== "execute") return Reflect.get(target, prop, receiver);
|
|
2869
|
+
return async (
|
|
2870
|
+
toolCallId: string,
|
|
2871
|
+
args: unknown,
|
|
2872
|
+
signal: AbortSignal | undefined,
|
|
2873
|
+
onUpdate: never,
|
|
2874
|
+
ctx: never,
|
|
2875
|
+
) => {
|
|
2876
|
+
// Short-circuit on persisted decisions.
|
|
2877
|
+
const persisted = this.#acpPermissionDecisions.get(target.name);
|
|
2878
|
+
if (persisted === "allow_always") {
|
|
2879
|
+
return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
|
|
2880
|
+
}
|
|
2881
|
+
if (persisted === "reject_always") {
|
|
2882
|
+
throw new ToolError(`Tool call rejected by user (preference)`);
|
|
2883
|
+
}
|
|
2884
|
+
if (signal?.aborted) {
|
|
2885
|
+
throw new ToolAbortError("Permission request cancelled");
|
|
2886
|
+
}
|
|
2887
|
+
type PermissionRaceResult =
|
|
2888
|
+
| { kind: "permission"; outcome: ClientBridgePermissionOutcome }
|
|
2889
|
+
| { kind: "aborted" };
|
|
2890
|
+
const { promise: abortPromise, resolve: resolveAbort } = Promise.withResolvers<PermissionRaceResult>();
|
|
2891
|
+
const onAbort = () => resolveAbort({ kind: "aborted" });
|
|
2892
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2893
|
+
let raced: PermissionRaceResult;
|
|
2894
|
+
try {
|
|
2895
|
+
const permissionPromise = bridge.requestPermission!(
|
|
2896
|
+
{
|
|
2897
|
+
toolCallId,
|
|
2898
|
+
toolName: target.name,
|
|
2899
|
+
title: derivePermissionTitle(target.name, args),
|
|
2900
|
+
rawInput: args,
|
|
2901
|
+
locations: extractPermissionLocations(args, this.sessionManager.getCwd()),
|
|
2902
|
+
},
|
|
2903
|
+
PERMISSION_OPTIONS,
|
|
2904
|
+
signal,
|
|
2905
|
+
).then(outcome => ({ kind: "permission" as const, outcome }));
|
|
2906
|
+
raced = await Promise.race([permissionPromise, abortPromise]);
|
|
2907
|
+
} finally {
|
|
2908
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2909
|
+
}
|
|
2910
|
+
if (raced.kind === "aborted" || signal?.aborted) {
|
|
2911
|
+
throw new ToolAbortError("Permission request cancelled");
|
|
2912
|
+
}
|
|
2913
|
+
const outcome = raced.outcome;
|
|
2914
|
+
if (outcome.outcome === "cancelled") {
|
|
2915
|
+
throw new ToolAbortError("Permission request cancelled");
|
|
2916
|
+
}
|
|
2917
|
+
const selectedOption = PERMISSION_OPTIONS_BY_ID.get(outcome.optionId);
|
|
2918
|
+
if (!selectedOption) {
|
|
2919
|
+
throw new ToolError(`Tool permission response used unknown option ID: ${outcome.optionId}`);
|
|
2920
|
+
}
|
|
2921
|
+
if (selectedOption.kind === "allow_always") {
|
|
2922
|
+
this.#acpPermissionDecisions.set(target.name, "allow_always");
|
|
2923
|
+
} else if (selectedOption.kind === "reject_always") {
|
|
2924
|
+
this.#acpPermissionDecisions.set(target.name, "reject_always");
|
|
2925
|
+
}
|
|
2926
|
+
if (selectedOption.kind === "reject_once" || selectedOption.kind === "reject_always") {
|
|
2927
|
+
throw new ToolError(`Tool call rejected by user (${target.name})`);
|
|
2928
|
+
}
|
|
2929
|
+
return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
|
|
2930
|
+
};
|
|
2931
|
+
},
|
|
2932
|
+
}) as T;
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2550
2935
|
async #applyActiveToolsByName(
|
|
2551
2936
|
toolNames: string[],
|
|
2552
2937
|
options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
|
|
@@ -2558,7 +2943,7 @@ export class AgentSession {
|
|
|
2558
2943
|
for (const name of toolNames) {
|
|
2559
2944
|
const tool = this.#toolRegistry.get(name);
|
|
2560
2945
|
if (tool) {
|
|
2561
|
-
tools.push(tool);
|
|
2946
|
+
tools.push(this.#wrapToolForAcpPermission(tool));
|
|
2562
2947
|
validToolNames.push(name);
|
|
2563
2948
|
}
|
|
2564
2949
|
}
|
|
@@ -2566,7 +2951,7 @@ export class AgentSession {
|
|
|
2566
2951
|
if (isAutoQaEnabled(this.settings) && !validToolNames.includes("report_tool_issue")) {
|
|
2567
2952
|
const qaTool = this.#toolRegistry.get("report_tool_issue");
|
|
2568
2953
|
if (qaTool) {
|
|
2569
|
-
tools.push(qaTool);
|
|
2954
|
+
tools.push(this.#wrapToolForAcpPermission(qaTool));
|
|
2570
2955
|
validToolNames.push("report_tool_issue");
|
|
2571
2956
|
}
|
|
2572
2957
|
}
|
|
@@ -2966,6 +3351,18 @@ export class AgentSession {
|
|
|
2966
3351
|
}
|
|
2967
3352
|
}
|
|
2968
3353
|
|
|
3354
|
+
getGoalModeState(): GoalModeState | undefined {
|
|
3355
|
+
return this.#goalModeState;
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
setGoalModeState(state: GoalModeState | undefined): void {
|
|
3359
|
+
this.#goalModeState = state;
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
get goalRuntime(): GoalRuntime {
|
|
3363
|
+
return this.#goalRuntime;
|
|
3364
|
+
}
|
|
3365
|
+
|
|
2969
3366
|
markPlanReferenceSent(): void {
|
|
2970
3367
|
this.#planReferenceSent = true;
|
|
2971
3368
|
}
|
|
@@ -2974,6 +3371,21 @@ export class AgentSession {
|
|
|
2974
3371
|
this.#planReferencePath = path;
|
|
2975
3372
|
}
|
|
2976
3373
|
|
|
3374
|
+
get clientBridge(): ClientBridge | undefined {
|
|
3375
|
+
return this.#clientBridge;
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
setClientBridge(bridge: ClientBridge | undefined): void {
|
|
3379
|
+
this.#clientBridge = bridge;
|
|
3380
|
+
this.#acpPermissionDecisions.clear();
|
|
3381
|
+
const activeToolNames = this.getActiveToolNames();
|
|
3382
|
+
const activeTools = activeToolNames
|
|
3383
|
+
.map(name => this.#toolRegistry.get(name))
|
|
3384
|
+
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
3385
|
+
.map(tool => this.#wrapToolForAcpPermission(tool));
|
|
3386
|
+
this.agent.setTools(activeTools);
|
|
3387
|
+
}
|
|
3388
|
+
|
|
2977
3389
|
getCheckpointState(): CheckpointState | undefined {
|
|
2978
3390
|
return this.#checkpointState;
|
|
2979
3391
|
}
|
|
@@ -3002,6 +3414,21 @@ export class AgentSession {
|
|
|
3002
3414
|
);
|
|
3003
3415
|
}
|
|
3004
3416
|
|
|
3417
|
+
async sendGoalModeContext(options?: { deliverAs?: "steer" | "followUp" | "nextTurn" }): Promise<void> {
|
|
3418
|
+
const message = this.#buildGoalModeMessage();
|
|
3419
|
+
if (!message) return;
|
|
3420
|
+
await this.sendCustomMessage(
|
|
3421
|
+
{
|
|
3422
|
+
customType: message.customType,
|
|
3423
|
+
content: message.content,
|
|
3424
|
+
display: message.display,
|
|
3425
|
+
details: message.details,
|
|
3426
|
+
attribution: message.attribution,
|
|
3427
|
+
},
|
|
3428
|
+
options ? { deliverAs: options.deliverAs } : undefined,
|
|
3429
|
+
);
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3005
3432
|
resolveRoleModel(role: string): Model | undefined {
|
|
3006
3433
|
return this.#resolveRoleModelFull(role, this.#modelRegistry.getAvailable(), this.model).model;
|
|
3007
3434
|
}
|
|
@@ -3097,7 +3524,6 @@ export class AgentSession {
|
|
|
3097
3524
|
askToolName: "ask",
|
|
3098
3525
|
writeToolName: "write",
|
|
3099
3526
|
editToolName: "edit",
|
|
3100
|
-
exitToolName: "exit_plan_mode",
|
|
3101
3527
|
reentry: state.reentry ?? false,
|
|
3102
3528
|
iterative: state.workflow === "iterative",
|
|
3103
3529
|
});
|
|
@@ -3112,6 +3538,19 @@ export class AgentSession {
|
|
|
3112
3538
|
};
|
|
3113
3539
|
}
|
|
3114
3540
|
|
|
3541
|
+
#buildGoalModeMessage(): CustomMessage | null {
|
|
3542
|
+
const content = this.#goalRuntime.buildActivePrompt();
|
|
3543
|
+
if (!content) return null;
|
|
3544
|
+
return {
|
|
3545
|
+
role: "custom",
|
|
3546
|
+
customType: "goal-mode-context",
|
|
3547
|
+
content,
|
|
3548
|
+
display: false,
|
|
3549
|
+
attribution: "agent",
|
|
3550
|
+
timestamp: Date.now(),
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3115
3554
|
/**
|
|
3116
3555
|
* Send a prompt to the agent.
|
|
3117
3556
|
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
|
@@ -3287,6 +3726,10 @@ export class AgentSession {
|
|
|
3287
3726
|
if (planModeMessage) {
|
|
3288
3727
|
messages.push(planModeMessage);
|
|
3289
3728
|
}
|
|
3729
|
+
const goalModeMessage = this.#buildGoalModeMessage();
|
|
3730
|
+
if (goalModeMessage) {
|
|
3731
|
+
messages.push(goalModeMessage);
|
|
3732
|
+
}
|
|
3290
3733
|
if (options?.prependMessages) {
|
|
3291
3734
|
messages.push(...options.prependMessages);
|
|
3292
3735
|
}
|
|
@@ -3530,7 +3973,7 @@ export class AgentSession {
|
|
|
3530
3973
|
*/
|
|
3531
3974
|
async #queueSteer(text: string, images?: ImageContent[]): Promise<void> {
|
|
3532
3975
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
3533
|
-
this.#steeringMessages.push(displayText);
|
|
3976
|
+
this.#steeringMessages.push({ text: displayText });
|
|
3534
3977
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
3535
3978
|
if (images && images.length > 0) {
|
|
3536
3979
|
content.push(...images);
|
|
@@ -3548,7 +3991,7 @@ export class AgentSession {
|
|
|
3548
3991
|
*/
|
|
3549
3992
|
async #queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
|
|
3550
3993
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
3551
|
-
this.#followUpMessages.push(displayText);
|
|
3994
|
+
this.#followUpMessages.push({ text: displayText });
|
|
3552
3995
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
3553
3996
|
if (images && images.length > 0) {
|
|
3554
3997
|
content.push(...images);
|
|
@@ -3784,8 +4227,8 @@ export class AgentSession {
|
|
|
3784
4227
|
* Useful for restoring to editor when user aborts.
|
|
3785
4228
|
*/
|
|
3786
4229
|
clearQueue(): { steering: string[]; followUp: string[] } {
|
|
3787
|
-
const steering =
|
|
3788
|
-
const followUp =
|
|
4230
|
+
const steering = this.#steeringMessages.map(e => e.text);
|
|
4231
|
+
const followUp = this.#followUpMessages.map(e => e.text);
|
|
3789
4232
|
this.#steeringMessages = [];
|
|
3790
4233
|
this.#followUpMessages = [];
|
|
3791
4234
|
this.agent.clearAllQueues();
|
|
@@ -3797,27 +4240,35 @@ export class AgentSession {
|
|
|
3797
4240
|
return this.#steeringMessages.length + this.#followUpMessages.length + this.#pendingNextTurnMessages.length;
|
|
3798
4241
|
}
|
|
3799
4242
|
|
|
3800
|
-
/** Get pending messages (read-only)
|
|
4243
|
+
/** Get pending messages (read-only). Returns the public text-only view;
|
|
4244
|
+
* internal `{text, tag?}` records are mapped to `.text` so callers
|
|
4245
|
+
* (`updatePendingMessagesDisplay`, `restoreQueuedMessagesToEditor`) see
|
|
4246
|
+
* the unchanged historical shape. */
|
|
3801
4247
|
getQueuedMessages(): { steering: readonly string[]; followUp: readonly string[] } {
|
|
3802
|
-
return {
|
|
4248
|
+
return {
|
|
4249
|
+
steering: this.#steeringMessages.map(e => e.text),
|
|
4250
|
+
followUp: this.#followUpMessages.map(e => e.text),
|
|
4251
|
+
};
|
|
3803
4252
|
}
|
|
3804
4253
|
|
|
3805
4254
|
/**
|
|
3806
4255
|
* Pop the last queued message (steering first, then follow-up).
|
|
3807
4256
|
* Used by dequeue keybinding to restore messages to editor one at a time.
|
|
4257
|
+
* Returns the popped entry's `.text`; the tag (if any) dies with the
|
|
4258
|
+
* record — no orphan state can outlive the queue entry.
|
|
3808
4259
|
*/
|
|
3809
4260
|
popLastQueuedMessage(): string | undefined {
|
|
3810
4261
|
// Pop from steering first (LIFO)
|
|
3811
4262
|
if (this.#steeringMessages.length > 0) {
|
|
3812
|
-
const
|
|
4263
|
+
const entry = this.#steeringMessages.pop();
|
|
3813
4264
|
this.agent.popLastSteer();
|
|
3814
|
-
return
|
|
4265
|
+
return entry?.text;
|
|
3815
4266
|
}
|
|
3816
4267
|
// Then from follow-up
|
|
3817
4268
|
if (this.#followUpMessages.length > 0) {
|
|
3818
|
-
const
|
|
4269
|
+
const entry = this.#followUpMessages.pop();
|
|
3819
4270
|
this.agent.popLastFollowUp();
|
|
3820
|
-
return
|
|
4271
|
+
return entry?.text;
|
|
3821
4272
|
}
|
|
3822
4273
|
return undefined;
|
|
3823
4274
|
}
|
|
@@ -3931,7 +4382,7 @@ export class AgentSession {
|
|
|
3931
4382
|
/**
|
|
3932
4383
|
* Abort current operation and wait for agent to become idle.
|
|
3933
4384
|
*/
|
|
3934
|
-
async abort(): Promise<void> {
|
|
4385
|
+
async abort(options?: { goalReason?: "interrupted" | "internal" }): Promise<void> {
|
|
3935
4386
|
this.abortRetry();
|
|
3936
4387
|
this.#promptGeneration++;
|
|
3937
4388
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
@@ -3943,6 +4394,7 @@ export class AgentSession {
|
|
|
3943
4394
|
this.agent.abort();
|
|
3944
4395
|
await postPromptDrain;
|
|
3945
4396
|
await this.agent.waitForIdle();
|
|
4397
|
+
await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
|
|
3946
4398
|
// Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
|
|
3947
4399
|
// block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
|
|
3948
4400
|
// a subsequent prompt() can incorrectly observe the session as busy after an abort.
|
|
@@ -4356,6 +4808,7 @@ export class AgentSession {
|
|
|
4356
4808
|
if (persist && effectiveLevel !== undefined && effectiveLevel !== ThinkingLevel.Off) {
|
|
4357
4809
|
this.settings.set("defaultThinkingLevel", effectiveLevel);
|
|
4358
4810
|
}
|
|
4811
|
+
this.#emit({ type: "thinking_level_changed", thinkingLevel: effectiveLevel });
|
|
4359
4812
|
}
|
|
4360
4813
|
}
|
|
4361
4814
|
|
|
@@ -4489,8 +4942,6 @@ export class AgentSession {
|
|
|
4489
4942
|
|
|
4490
4943
|
let hookCompaction: CompactionResult | undefined;
|
|
4491
4944
|
let fromExtension = false;
|
|
4492
|
-
let hookContext: string[] | undefined;
|
|
4493
|
-
let hookPrompt: string | undefined;
|
|
4494
4945
|
let preserveData: Record<string, unknown> | undefined;
|
|
4495
4946
|
|
|
4496
4947
|
if (this.#extensionRunner?.hasHandlers("session_before_compact")) {
|
|
@@ -4503,7 +4954,7 @@ export class AgentSession {
|
|
|
4503
4954
|
})) as SessionBeforeCompactResult | undefined;
|
|
4504
4955
|
|
|
4505
4956
|
if (result?.cancel) {
|
|
4506
|
-
throw new
|
|
4957
|
+
throw new CompactionCancelledError();
|
|
4507
4958
|
}
|
|
4508
4959
|
|
|
4509
4960
|
if (result?.compaction) {
|
|
@@ -4512,23 +4963,7 @@ export class AgentSession {
|
|
|
4512
4963
|
}
|
|
4513
4964
|
}
|
|
4514
4965
|
|
|
4515
|
-
|
|
4516
|
-
const compactMessages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
|
|
4517
|
-
const result = (await this.#extensionRunner.emit({
|
|
4518
|
-
type: "session.compacting",
|
|
4519
|
-
sessionId: this.sessionId,
|
|
4520
|
-
messages: compactMessages,
|
|
4521
|
-
})) as { context?: string[]; prompt?: string; preserveData?: Record<string, unknown> } | undefined;
|
|
4522
|
-
|
|
4523
|
-
hookContext = result?.context;
|
|
4524
|
-
hookPrompt = result?.prompt;
|
|
4525
|
-
preserveData = result?.preserveData;
|
|
4526
|
-
}
|
|
4527
|
-
|
|
4528
|
-
const memoryBackendContext = await this.#collectMemoryBackendContext(preparation);
|
|
4529
|
-
if (memoryBackendContext) {
|
|
4530
|
-
hookContext = hookContext ? [...hookContext, memoryBackendContext] : [memoryBackendContext];
|
|
4531
|
-
}
|
|
4966
|
+
const compactionPrep = await this.#prepareCompactionFromHooks(preparation, hookCompaction);
|
|
4532
4967
|
|
|
4533
4968
|
let summary: string;
|
|
4534
4969
|
let shortSummary: string | undefined;
|
|
@@ -4536,36 +4971,55 @@ export class AgentSession {
|
|
|
4536
4971
|
let tokensBefore: number;
|
|
4537
4972
|
let details: unknown;
|
|
4538
4973
|
|
|
4539
|
-
if (
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
preserveData ??= hookCompaction.preserveData;
|
|
4974
|
+
if (compactionPrep.kind === "fromHook") {
|
|
4975
|
+
summary = compactionPrep.summary;
|
|
4976
|
+
shortSummary = compactionPrep.shortSummary;
|
|
4977
|
+
firstKeptEntryId = compactionPrep.firstKeptEntryId;
|
|
4978
|
+
tokensBefore = compactionPrep.tokensBefore;
|
|
4979
|
+
details = compactionPrep.details;
|
|
4980
|
+
preserveData = compactionPrep.preserveData;
|
|
4547
4981
|
} else {
|
|
4548
|
-
// Generate compaction result
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4982
|
+
// Generate compaction result. Only convert known abort-shaped
|
|
4983
|
+
// rejections (AbortError raised while the abort signal is set,
|
|
4984
|
+
// or an already-typed sentinel) into `CompactionCancelledError`
|
|
4985
|
+
// so downstream callers can discriminate cancel from generic
|
|
4986
|
+
// failure via `instanceof` without inspecting message strings.
|
|
4987
|
+
// Real compaction bugs (network, server, parsing, etc.) keep
|
|
4988
|
+
// their original shape — they must not be silently relabeled
|
|
4989
|
+
// as cancellations even if the signal happens to be aborted
|
|
4990
|
+
// for an unrelated reason. Assignments live inside the try
|
|
4991
|
+
// block because every catch path throws — the post-try reads
|
|
4992
|
+
// of the result-derived locals are reachable only on success.
|
|
4993
|
+
try {
|
|
4994
|
+
const result = await this.#compactWithFallbackModel(
|
|
4995
|
+
preparation,
|
|
4996
|
+
customInstructions,
|
|
4997
|
+
compactionAbortController.signal,
|
|
4998
|
+
{
|
|
4999
|
+
promptOverride: compactionPrep.hookPrompt,
|
|
5000
|
+
extraContext: compactionPrep.hookContext,
|
|
5001
|
+
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
|
5002
|
+
},
|
|
5003
|
+
);
|
|
5004
|
+
summary = result.summary;
|
|
5005
|
+
shortSummary = result.shortSummary;
|
|
5006
|
+
firstKeptEntryId = result.firstKeptEntryId;
|
|
5007
|
+
tokensBefore = result.tokensBefore;
|
|
5008
|
+
details = result.details;
|
|
5009
|
+
preserveData = { ...(compactionPrep.preserveData ?? {}), ...(result.preserveData ?? {}) };
|
|
5010
|
+
} catch (err) {
|
|
5011
|
+
if (err instanceof CompactionCancelledError) {
|
|
5012
|
+
throw err;
|
|
5013
|
+
}
|
|
5014
|
+
if (compactionAbortController.signal.aborted && err instanceof Error && err.name === "AbortError") {
|
|
5015
|
+
throw new CompactionCancelledError();
|
|
5016
|
+
}
|
|
5017
|
+
throw err;
|
|
5018
|
+
}
|
|
4565
5019
|
}
|
|
4566
5020
|
|
|
4567
5021
|
if (compactionAbortController.signal.aborted) {
|
|
4568
|
-
throw new
|
|
5022
|
+
throw new CompactionCancelledError();
|
|
4569
5023
|
}
|
|
4570
5024
|
|
|
4571
5025
|
this.sessionManager.appendCompaction(
|
|
@@ -4990,14 +5444,14 @@ export class AgentSession {
|
|
|
4990
5444
|
}
|
|
4991
5445
|
|
|
4992
5446
|
const calledRequiredTool = assistantMessage.content.some(
|
|
4993
|
-
content => content.type === "toolCall" && (content.name === "ask" || content.name === "
|
|
5447
|
+
content => content.type === "toolCall" && (content.name === "ask" || content.name === "resolve"),
|
|
4994
5448
|
);
|
|
4995
5449
|
if (calledRequiredTool) {
|
|
4996
5450
|
return;
|
|
4997
5451
|
}
|
|
4998
|
-
const hasRequiredTools = this.#toolRegistry.has("ask") && this.#toolRegistry.has("
|
|
5452
|
+
const hasRequiredTools = this.#toolRegistry.has("ask") && this.#toolRegistry.has("resolve");
|
|
4999
5453
|
if (!hasRequiredTools) {
|
|
5000
|
-
logger.warn("Plan mode enforcement skipped because ask/
|
|
5454
|
+
logger.warn("Plan mode enforcement skipped because ask/resolve tools are unavailable", {
|
|
5001
5455
|
activeToolNames: this.agent.state.tools.map(tool => tool.name),
|
|
5002
5456
|
});
|
|
5003
5457
|
return;
|
|
@@ -5005,7 +5459,6 @@ export class AgentSession {
|
|
|
5005
5459
|
|
|
5006
5460
|
const reminder = prompt.render(planModeToolDecisionReminderPrompt, {
|
|
5007
5461
|
askToolName: "ask",
|
|
5008
|
-
exitToolName: "exit_plan_mode",
|
|
5009
5462
|
});
|
|
5010
5463
|
|
|
5011
5464
|
await this.prompt(reminder, {
|
|
@@ -5512,6 +5965,64 @@ export class AgentSession {
|
|
|
5512
5965
|
throw this.#buildCompactionAuthError();
|
|
5513
5966
|
}
|
|
5514
5967
|
|
|
5968
|
+
async #prepareCompactionFromHooks(
|
|
5969
|
+
preparation: CompactionPreparation,
|
|
5970
|
+
hookCompaction: CompactionResult | undefined,
|
|
5971
|
+
): Promise<
|
|
5972
|
+
| {
|
|
5973
|
+
kind: "fromHook";
|
|
5974
|
+
summary: string;
|
|
5975
|
+
shortSummary: string | undefined;
|
|
5976
|
+
firstKeptEntryId: string;
|
|
5977
|
+
tokensBefore: number;
|
|
5978
|
+
details: unknown;
|
|
5979
|
+
preserveData: Record<string, unknown> | undefined;
|
|
5980
|
+
}
|
|
5981
|
+
| {
|
|
5982
|
+
kind: "needsLlm";
|
|
5983
|
+
hookContext: string[] | undefined;
|
|
5984
|
+
hookPrompt: string | undefined;
|
|
5985
|
+
preserveData: Record<string, unknown> | undefined;
|
|
5986
|
+
}
|
|
5987
|
+
> {
|
|
5988
|
+
let hookContext: string[] | undefined;
|
|
5989
|
+
let hookPrompt: string | undefined;
|
|
5990
|
+
let preserveData: Record<string, unknown> | undefined;
|
|
5991
|
+
|
|
5992
|
+
if (!hookCompaction && this.#extensionRunner?.hasHandlers("session.compacting")) {
|
|
5993
|
+
const compactMessages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
|
|
5994
|
+
const result = (await this.#extensionRunner.emit({
|
|
5995
|
+
type: "session.compacting",
|
|
5996
|
+
sessionId: this.sessionId,
|
|
5997
|
+
messages: compactMessages,
|
|
5998
|
+
})) as { context?: string[]; prompt?: string; preserveData?: Record<string, unknown> } | undefined;
|
|
5999
|
+
|
|
6000
|
+
hookContext = result?.context;
|
|
6001
|
+
hookPrompt = result?.prompt;
|
|
6002
|
+
preserveData = result?.preserveData;
|
|
6003
|
+
}
|
|
6004
|
+
|
|
6005
|
+
const memoryBackendContext = await this.#collectMemoryBackendContext(preparation);
|
|
6006
|
+
if (memoryBackendContext) {
|
|
6007
|
+
hookContext = hookContext ? [...hookContext, memoryBackendContext] : [memoryBackendContext];
|
|
6008
|
+
}
|
|
6009
|
+
|
|
6010
|
+
if (hookCompaction) {
|
|
6011
|
+
preserveData ??= hookCompaction.preserveData;
|
|
6012
|
+
return {
|
|
6013
|
+
kind: "fromHook",
|
|
6014
|
+
summary: hookCompaction.summary,
|
|
6015
|
+
shortSummary: hookCompaction.shortSummary,
|
|
6016
|
+
firstKeptEntryId: hookCompaction.firstKeptEntryId,
|
|
6017
|
+
tokensBefore: hookCompaction.tokensBefore,
|
|
6018
|
+
details: hookCompaction.details,
|
|
6019
|
+
preserveData,
|
|
6020
|
+
};
|
|
6021
|
+
}
|
|
6022
|
+
|
|
6023
|
+
return { kind: "needsLlm", hookContext, hookPrompt, preserveData };
|
|
6024
|
+
}
|
|
6025
|
+
|
|
5515
6026
|
/**
|
|
5516
6027
|
* Internal: Run auto-compaction with events.
|
|
5517
6028
|
*/
|
|
@@ -5633,8 +6144,6 @@ export class AgentSession {
|
|
|
5633
6144
|
|
|
5634
6145
|
let hookCompaction: CompactionResult | undefined;
|
|
5635
6146
|
let fromExtension = false;
|
|
5636
|
-
let hookContext: string[] | undefined;
|
|
5637
|
-
let hookPrompt: string | undefined;
|
|
5638
6147
|
let preserveData: Record<string, unknown> | undefined;
|
|
5639
6148
|
|
|
5640
6149
|
if (this.#extensionRunner?.hasHandlers("session_before_compact")) {
|
|
@@ -5663,23 +6172,7 @@ export class AgentSession {
|
|
|
5663
6172
|
}
|
|
5664
6173
|
}
|
|
5665
6174
|
|
|
5666
|
-
|
|
5667
|
-
const compactMessages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
|
|
5668
|
-
const result = (await this.#extensionRunner.emit({
|
|
5669
|
-
type: "session.compacting",
|
|
5670
|
-
sessionId: this.sessionId,
|
|
5671
|
-
messages: compactMessages,
|
|
5672
|
-
})) as { context?: string[]; prompt?: string; preserveData?: Record<string, unknown> } | undefined;
|
|
5673
|
-
|
|
5674
|
-
hookContext = result?.context;
|
|
5675
|
-
hookPrompt = result?.prompt;
|
|
5676
|
-
preserveData = result?.preserveData;
|
|
5677
|
-
}
|
|
5678
|
-
|
|
5679
|
-
const memoryBackendContext = await this.#collectMemoryBackendContext(preparation);
|
|
5680
|
-
if (memoryBackendContext) {
|
|
5681
|
-
hookContext = hookContext ? [...hookContext, memoryBackendContext] : [memoryBackendContext];
|
|
5682
|
-
}
|
|
6175
|
+
const compactionPrep = await this.#prepareCompactionFromHooks(preparation, hookCompaction);
|
|
5683
6176
|
|
|
5684
6177
|
let summary: string;
|
|
5685
6178
|
let shortSummary: string | undefined;
|
|
@@ -5687,14 +6180,13 @@ export class AgentSession {
|
|
|
5687
6180
|
let tokensBefore: number;
|
|
5688
6181
|
let details: unknown;
|
|
5689
6182
|
|
|
5690
|
-
if (
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
preserveData ??= hookCompaction.preserveData;
|
|
6183
|
+
if (compactionPrep.kind === "fromHook") {
|
|
6184
|
+
summary = compactionPrep.summary;
|
|
6185
|
+
shortSummary = compactionPrep.shortSummary;
|
|
6186
|
+
firstKeptEntryId = compactionPrep.firstKeptEntryId;
|
|
6187
|
+
tokensBefore = compactionPrep.tokensBefore;
|
|
6188
|
+
details = compactionPrep.details;
|
|
6189
|
+
preserveData = compactionPrep.preserveData;
|
|
5698
6190
|
} else {
|
|
5699
6191
|
const candidates = this.#getCompactionModelCandidates(availableModels);
|
|
5700
6192
|
const retrySettings = this.settings.getGroup("retry");
|
|
@@ -5709,8 +6201,8 @@ export class AgentSession {
|
|
|
5709
6201
|
while (true) {
|
|
5710
6202
|
try {
|
|
5711
6203
|
compactResult = await compact(preparation, candidate, apiKey, undefined, autoCompactionSignal, {
|
|
5712
|
-
promptOverride: hookPrompt,
|
|
5713
|
-
extraContext: hookContext,
|
|
6204
|
+
promptOverride: compactionPrep.hookPrompt,
|
|
6205
|
+
extraContext: compactionPrep.hookContext,
|
|
5714
6206
|
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
|
5715
6207
|
metadata: this.agent.metadataForProvider(candidate.provider),
|
|
5716
6208
|
initiatorOverride: "agent",
|
|
@@ -5767,7 +6259,7 @@ export class AgentSession {
|
|
|
5767
6259
|
error: message,
|
|
5768
6260
|
model: `${candidate.provider}/${candidate.id}`,
|
|
5769
6261
|
});
|
|
5770
|
-
await
|
|
6262
|
+
await scheduler.wait(delayMs, { signal: autoCompactionSignal });
|
|
5771
6263
|
}
|
|
5772
6264
|
}
|
|
5773
6265
|
|
|
@@ -5788,7 +6280,7 @@ export class AgentSession {
|
|
|
5788
6280
|
firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
5789
6281
|
tokensBefore = compactResult.tokensBefore;
|
|
5790
6282
|
details = compactResult.details;
|
|
5791
|
-
preserveData = { ...(preserveData ?? {}), ...(compactResult.preserveData ?? {}) };
|
|
6283
|
+
preserveData = { ...(compactionPrep.preserveData ?? {}), ...(compactResult.preserveData ?? {}) };
|
|
5792
6284
|
}
|
|
5793
6285
|
|
|
5794
6286
|
if (autoCompactionSignal.aborted) {
|
|
@@ -5939,10 +6431,11 @@ export class AgentSession {
|
|
|
5939
6431
|
|
|
5940
6432
|
#isTransientTransportErrorMessage(errorMessage: string): boolean {
|
|
5941
6433
|
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
|
|
5942
|
-
// service unavailable, network/connection/socket errors, fetch failed,
|
|
6434
|
+
// service unavailable, provider-suggested retry, network/connection/socket errors, fetch failed,
|
|
6435
|
+
// terminated, retry delay exceeded
|
|
5943
6436
|
return (
|
|
5944
6437
|
isUnexpectedSocketCloseMessage(errorMessage) ||
|
|
5945
|
-
/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response/i.test(
|
|
6438
|
+
/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response/i.test(
|
|
5946
6439
|
errorMessage,
|
|
5947
6440
|
)
|
|
5948
6441
|
);
|
|
@@ -6292,7 +6785,7 @@ export class AgentSession {
|
|
|
6292
6785
|
this.#retryAbortController?.abort();
|
|
6293
6786
|
this.#retryAbortController = retryAbortController;
|
|
6294
6787
|
try {
|
|
6295
|
-
await
|
|
6788
|
+
await scheduler.wait(delayMs, { signal: retryAbortController.signal });
|
|
6296
6789
|
} catch {
|
|
6297
6790
|
if (this.#retryAbortController !== retryAbortController) {
|
|
6298
6791
|
return false;
|
|
@@ -7624,21 +8117,11 @@ export class AgentSession {
|
|
|
7624
8117
|
* @returns Text content, or undefined if no assistant message exists
|
|
7625
8118
|
*/
|
|
7626
8119
|
getLastAssistantText(): string | undefined {
|
|
7627
|
-
const lastAssistant = this
|
|
7628
|
-
.slice()
|
|
7629
|
-
.reverse()
|
|
7630
|
-
.find(m => {
|
|
7631
|
-
if (m.role !== "assistant") return false;
|
|
7632
|
-
const msg = m as AssistantMessage;
|
|
7633
|
-
// Skip aborted messages with no content
|
|
7634
|
-
if (msg.stopReason === "aborted" && msg.content.length === 0) return false;
|
|
7635
|
-
return true;
|
|
7636
|
-
});
|
|
7637
|
-
|
|
8120
|
+
const lastAssistant = this.#getLastCopyCandidateAssistantMessage();
|
|
7638
8121
|
if (!lastAssistant) return undefined;
|
|
7639
8122
|
|
|
7640
8123
|
let text = "";
|
|
7641
|
-
for (const content of
|
|
8124
|
+
for (const content of lastAssistant.content) {
|
|
7642
8125
|
if (content.type === "text") {
|
|
7643
8126
|
text += content.text;
|
|
7644
8127
|
}
|
|
@@ -7647,6 +8130,54 @@ export class AgentSession {
|
|
|
7647
8130
|
return text.trim() || undefined;
|
|
7648
8131
|
}
|
|
7649
8132
|
|
|
8133
|
+
hasCopyCandidateAssistantMessage(): boolean {
|
|
8134
|
+
return this.#getLastCopyCandidateAssistantMessage() !== undefined;
|
|
8135
|
+
}
|
|
8136
|
+
|
|
8137
|
+
#getLastCopyCandidateAssistantMessage(): AssistantMessage | undefined {
|
|
8138
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
8139
|
+
const message = this.messages[i];
|
|
8140
|
+
if (message.role !== "assistant") continue;
|
|
8141
|
+
|
|
8142
|
+
const assistantMessage = message as AssistantMessage;
|
|
8143
|
+
// Skip aborted messages with no content
|
|
8144
|
+
if (assistantMessage.stopReason === "aborted" && assistantMessage.content.length === 0) continue;
|
|
8145
|
+
|
|
8146
|
+
return assistantMessage;
|
|
8147
|
+
}
|
|
8148
|
+
|
|
8149
|
+
return undefined;
|
|
8150
|
+
}
|
|
8151
|
+
/**
|
|
8152
|
+
* Get text content of the most recent visible handoff message.
|
|
8153
|
+
* Fresh handoff sessions store the handoff context as a custom message, not
|
|
8154
|
+
* an assistant message, so callers that copy the "last" message can use this
|
|
8155
|
+
* as a fallback before the new session has an assistant response.
|
|
8156
|
+
*/
|
|
8157
|
+
getLastVisibleHandoffText(): string | undefined {
|
|
8158
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
8159
|
+
const message = this.messages[i];
|
|
8160
|
+
if (message.role !== "custom") continue;
|
|
8161
|
+
|
|
8162
|
+
const customMessage = message as CustomMessage;
|
|
8163
|
+
if (customMessage.customType !== "handoff" || !customMessage.display) continue;
|
|
8164
|
+
|
|
8165
|
+
if (typeof customMessage.content === "string") {
|
|
8166
|
+
return customMessage.content.trim() || undefined;
|
|
8167
|
+
}
|
|
8168
|
+
|
|
8169
|
+
let text = "";
|
|
8170
|
+
for (const content of customMessage.content) {
|
|
8171
|
+
if (content.type === "text") {
|
|
8172
|
+
text += content.text;
|
|
8173
|
+
}
|
|
8174
|
+
}
|
|
8175
|
+
return text.trim() || undefined;
|
|
8176
|
+
}
|
|
8177
|
+
|
|
8178
|
+
return undefined;
|
|
8179
|
+
}
|
|
8180
|
+
|
|
7650
8181
|
/**
|
|
7651
8182
|
* Format the entire session as plain text for clipboard export.
|
|
7652
8183
|
* Includes user messages, assistant text, thinking blocks, tool calls, and tool results.
|