@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +10 -10
- package/scripts/build-binary.ts +5 -0
- 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/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 +10 -29
- package/src/commands/commit.ts +10 -0
- 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 +44 -3
- package/src/config/model-resolver.ts +1 -4
- package/src/config/settings-schema.ts +82 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/discovery/claude-plugins.ts +19 -7
- package/src/edit/renderer.ts +7 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +2 -2
- package/src/eval/py/executor.ts +5 -0
- package/src/eval/py/runner.py +42 -11
- package/src/eval/py/runtime.ts +1 -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/get-commands-handler.ts +77 -0
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +89 -223
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/plugins/legacy-pi-compat.ts +48 -31
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +9 -0
- 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/hashline/input.ts +2 -1
- package/src/hashline/parser.ts +27 -3
- 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 +11 -12
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +8 -0
- package/src/internal-urls/types.ts +21 -0
- package/src/lsp/config.ts +15 -6
- package/src/lsp/defaults.json +6 -2
- package/src/main.ts +11 -2
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +327 -95
- 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/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +93 -8
- 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 +13 -4
- package/src/modes/controllers/event-controller.ts +36 -7
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +13 -0
- 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 +624 -52
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/host-uris.ts +235 -0
- package/src/modes/rpc/rpc-mode.ts +41 -88
- package/src/modes/rpc/rpc-types.ts +57 -0
- package/src/modes/runtime-init.ts +116 -0
- package/src/modes/theme/defaults/dark-poimandres.json +3 -0
- package/src/modes/theme/defaults/light-poimandres.json +3 -0
- package/src/modes/theme/theme.ts +24 -6
- package/src/modes/types.ts +14 -3
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +10 -3
- package/src/plan-mode/approved-plan.ts +35 -1
- 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/system/plan-mode-active.md +5 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/github.md +4 -4
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +101 -117
- package/src/prompts/tools/read.md +55 -36
- package/src/prompts/tools/resolve.md +6 -5
- package/src/sdk.ts +12 -5
- package/src/session/agent-session.ts +428 -106
- package/src/session/blob-store.ts +36 -3
- 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/builtin-registry.ts +18 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/discovery.ts +5 -2
- package/src/task/executor.ts +19 -8
- package/src/task/index.ts +3 -0
- package/src/task/render.ts +21 -15
- 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-command-fixup.ts +47 -0
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +66 -19
- 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/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +15 -4
- package/src/tools/eval.ts +12 -2
- package/src/tools/find.ts +20 -38
- package/src/tools/gh.ts +44 -10
- package/src/tools/index.ts +22 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/job.ts +16 -7
- package/src/tools/output-meta.ts +202 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +548 -237
- 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/ssh.ts +3 -2
- package/src/tools/write.ts +64 -9
- 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/anthropic.ts +5 -0
- package/src/web/search/providers/exa.ts +3 -0
- package/src/web/search/providers/gemini.ts +40 -95
- package/src/web/search/providers/jina.ts +5 -2
- package/src/web/search/providers/zai.ts +5 -2
- 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 {
|
|
@@ -174,6 +182,8 @@ import {
|
|
|
174
182
|
convertToLlm,
|
|
175
183
|
type FileMentionMessage,
|
|
176
184
|
type PythonExecutionMessage,
|
|
185
|
+
readPendingDisplayTag,
|
|
186
|
+
SILENT_ABORT_MARKER,
|
|
177
187
|
} from "./messages";
|
|
178
188
|
import { formatSessionDumpText } from "./session-dump-format";
|
|
179
189
|
import type {
|
|
@@ -208,7 +218,9 @@ export type AgentSessionEvent =
|
|
|
208
218
|
| { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
|
|
209
219
|
| { type: "todo_auto_clear" }
|
|
210
220
|
| { type: "irc_message"; message: CustomMessage }
|
|
211
|
-
| { 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 };
|
|
212
224
|
|
|
213
225
|
/** Listener function for agent session events */
|
|
214
226
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
@@ -594,6 +606,13 @@ function extractPermissionLocations(args: unknown, cwd: string): { path: string;
|
|
|
594
606
|
// AgentSession Class
|
|
595
607
|
// ============================================================================
|
|
596
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
|
+
|
|
597
616
|
export class AgentSession {
|
|
598
617
|
readonly agent: Agent;
|
|
599
618
|
readonly sessionManager: SessionManager;
|
|
@@ -612,14 +631,22 @@ export class AgentSession {
|
|
|
612
631
|
#unsubscribeAgent?: () => void;
|
|
613
632
|
#eventListeners: AgentSessionEventListener[] = [];
|
|
614
633
|
|
|
615
|
-
/** Tracks pending steering messages for UI display. Removed when delivered.
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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[] = [];
|
|
619
643
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
620
644
|
#pendingNextTurnMessages: CustomMessage[] = [];
|
|
621
645
|
#scheduledHiddenNextTurnGeneration: number | undefined = undefined;
|
|
622
646
|
#planModeState: PlanModeState | undefined;
|
|
647
|
+
#goalModeState: GoalModeState | undefined;
|
|
648
|
+
#goalRuntime: GoalRuntime;
|
|
649
|
+
#goalTurnCounter = 0;
|
|
623
650
|
#planReferenceSent = false;
|
|
624
651
|
#planReferencePath = "local://PLAN.md";
|
|
625
652
|
#clientBridge: ClientBridge | undefined;
|
|
@@ -729,6 +756,19 @@ export class AgentSession {
|
|
|
729
756
|
#ttsrRetryToken = 0;
|
|
730
757
|
#ttsrResumePromise: Promise<void> | undefined = undefined;
|
|
731
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;
|
|
732
772
|
#postPromptTasks = new Set<Promise<void>>();
|
|
733
773
|
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
734
774
|
#postPromptTasksResolve: (() => void) | undefined = undefined;
|
|
@@ -878,6 +918,44 @@ export class AgentSession {
|
|
|
878
918
|
this.agent.providerSessionState = this.#providerSessionState;
|
|
879
919
|
this.#syncAgentSessionId();
|
|
880
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
|
+
});
|
|
881
959
|
|
|
882
960
|
// Always subscribe to agent events for internal handling
|
|
883
961
|
// (session persistence, hooks, auto-compaction, retry logic)
|
|
@@ -925,6 +1003,19 @@ export class AgentSession {
|
|
|
925
1003
|
return this.#toolChoiceQueue.peekInFlightInvoker();
|
|
926
1004
|
}
|
|
927
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
|
+
|
|
928
1019
|
/** Provider-scoped mutable state store for transport/session caches. */
|
|
929
1020
|
get providerSessionState(): Map<string, ProviderSessionState> {
|
|
930
1021
|
return this.#providerSessionState;
|
|
@@ -950,6 +1041,49 @@ export class AgentSession {
|
|
|
950
1041
|
return this.#ttsrAbortPending;
|
|
951
1042
|
}
|
|
952
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
|
+
|
|
953
1087
|
getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
|
|
954
1088
|
const manager = AsyncJobManager.instance();
|
|
955
1089
|
if (!manager) return null;
|
|
@@ -1038,13 +1172,13 @@ export class AgentSession {
|
|
|
1038
1172
|
if (event.type === "message_start" && event.message.role === "user") {
|
|
1039
1173
|
const messageText = this.#getUserMessageText(event.message);
|
|
1040
1174
|
if (messageText) {
|
|
1041
|
-
// Check steering queue first
|
|
1042
|
-
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);
|
|
1043
1177
|
if (steeringIndex !== -1) {
|
|
1044
1178
|
this.#steeringMessages.splice(steeringIndex, 1);
|
|
1045
1179
|
} else {
|
|
1046
1180
|
// Check follow-up queue
|
|
1047
|
-
const followUpIndex = this.#followUpMessages.
|
|
1181
|
+
const followUpIndex = this.#followUpMessages.findIndex(e => e.text === messageText);
|
|
1048
1182
|
if (followUpIndex !== -1) {
|
|
1049
1183
|
this.#followUpMessages.splice(followUpIndex, 1);
|
|
1050
1184
|
}
|
|
@@ -1052,6 +1186,48 @@ export class AgentSession {
|
|
|
1052
1186
|
}
|
|
1053
1187
|
}
|
|
1054
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
|
+
|
|
1055
1231
|
// Deobfuscate assistant message content for display emission — the LLM echoes back
|
|
1056
1232
|
// obfuscated placeholders, but listeners (TUI, extensions, exporters) must see real
|
|
1057
1233
|
// values. The original event.message stays obfuscated so the persistence path below
|
|
@@ -1067,6 +1243,16 @@ export class AgentSession {
|
|
|
1067
1243
|
}
|
|
1068
1244
|
}
|
|
1069
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
|
+
|
|
1070
1256
|
await this.#emitSessionEvent(displayEvent);
|
|
1071
1257
|
|
|
1072
1258
|
if (event.type === "turn_start") {
|
|
@@ -1090,6 +1276,13 @@ export class AgentSession {
|
|
|
1090
1276
|
this.#toolChoiceQueue.resolve();
|
|
1091
1277
|
}
|
|
1092
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
|
+
}
|
|
1093
1286
|
if (event.type === "tool_execution_end" && event.toolName === "yield" && !event.isError) {
|
|
1094
1287
|
this.#lastSuccessfulYieldToolCallId = event.toolCallId;
|
|
1095
1288
|
}
|
|
@@ -1325,6 +1518,15 @@ export class AgentSession {
|
|
|
1325
1518
|
|
|
1326
1519
|
// Check auto-retry and auto-compaction after agent completes
|
|
1327
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
|
+
});
|
|
1328
1530
|
const fallbackAssistant = [...event.messages]
|
|
1329
1531
|
.reverse()
|
|
1330
1532
|
.find((message): message is AssistantMessage => message.role === "assistant");
|
|
@@ -1446,7 +1648,7 @@ export class AgentSession {
|
|
|
1446
1648
|
const scheduled = (async () => {
|
|
1447
1649
|
if (delayMs > 0) {
|
|
1448
1650
|
try {
|
|
1449
|
-
await
|
|
1651
|
+
await scheduler.wait(delayMs, { signal });
|
|
1450
1652
|
} catch {
|
|
1451
1653
|
return;
|
|
1452
1654
|
}
|
|
@@ -1480,7 +1682,10 @@ export class AgentSession {
|
|
|
1480
1682
|
try {
|
|
1481
1683
|
await this.#maybeRestoreRetryFallbackPrimary();
|
|
1482
1684
|
await this.agent.continue();
|
|
1483
|
-
} catch {
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
logger.warn("agent.continue failed after scheduling", {
|
|
1687
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1688
|
+
});
|
|
1484
1689
|
options?.onError?.();
|
|
1485
1690
|
}
|
|
1486
1691
|
},
|
|
@@ -2180,6 +2385,12 @@ export class AgentSession {
|
|
|
2180
2385
|
attempt: event.attempt,
|
|
2181
2386
|
maxAttempts: event.maxAttempts,
|
|
2182
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
|
+
});
|
|
2183
2394
|
}
|
|
2184
2395
|
}
|
|
2185
2396
|
|
|
@@ -2572,7 +2783,7 @@ export class AgentSession {
|
|
|
2572
2783
|
|
|
2573
2784
|
/** Collect built-in tools the model can discover via search_tool_bm25. Restricted to tool
|
|
2574
2785
|
* definitions whose `loadMode === "discoverable"`. This keeps hidden/internal tools
|
|
2575
|
-
* (resolve, yield,
|
|
2786
|
+
* (resolve, yield, report_finding, report_tool_issue) out of the index
|
|
2576
2787
|
* and avoids mislabeling extension/custom default-inactive tools as built-ins. */
|
|
2577
2788
|
#collectDiscoverableBuiltinTools(): DiscoverableTool[] {
|
|
2578
2789
|
const activeNames = new Set(this.getActiveToolNames());
|
|
@@ -3140,6 +3351,18 @@ export class AgentSession {
|
|
|
3140
3351
|
}
|
|
3141
3352
|
}
|
|
3142
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
|
+
|
|
3143
3366
|
markPlanReferenceSent(): void {
|
|
3144
3367
|
this.#planReferenceSent = true;
|
|
3145
3368
|
}
|
|
@@ -3191,6 +3414,21 @@ export class AgentSession {
|
|
|
3191
3414
|
);
|
|
3192
3415
|
}
|
|
3193
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
|
+
|
|
3194
3432
|
resolveRoleModel(role: string): Model | undefined {
|
|
3195
3433
|
return this.#resolveRoleModelFull(role, this.#modelRegistry.getAvailable(), this.model).model;
|
|
3196
3434
|
}
|
|
@@ -3286,7 +3524,6 @@ export class AgentSession {
|
|
|
3286
3524
|
askToolName: "ask",
|
|
3287
3525
|
writeToolName: "write",
|
|
3288
3526
|
editToolName: "edit",
|
|
3289
|
-
exitToolName: "exit_plan_mode",
|
|
3290
3527
|
reentry: state.reentry ?? false,
|
|
3291
3528
|
iterative: state.workflow === "iterative",
|
|
3292
3529
|
});
|
|
@@ -3301,6 +3538,19 @@ export class AgentSession {
|
|
|
3301
3538
|
};
|
|
3302
3539
|
}
|
|
3303
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
|
+
|
|
3304
3554
|
/**
|
|
3305
3555
|
* Send a prompt to the agent.
|
|
3306
3556
|
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
|
@@ -3476,6 +3726,10 @@ export class AgentSession {
|
|
|
3476
3726
|
if (planModeMessage) {
|
|
3477
3727
|
messages.push(planModeMessage);
|
|
3478
3728
|
}
|
|
3729
|
+
const goalModeMessage = this.#buildGoalModeMessage();
|
|
3730
|
+
if (goalModeMessage) {
|
|
3731
|
+
messages.push(goalModeMessage);
|
|
3732
|
+
}
|
|
3479
3733
|
if (options?.prependMessages) {
|
|
3480
3734
|
messages.push(...options.prependMessages);
|
|
3481
3735
|
}
|
|
@@ -3719,7 +3973,7 @@ export class AgentSession {
|
|
|
3719
3973
|
*/
|
|
3720
3974
|
async #queueSteer(text: string, images?: ImageContent[]): Promise<void> {
|
|
3721
3975
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
3722
|
-
this.#steeringMessages.push(displayText);
|
|
3976
|
+
this.#steeringMessages.push({ text: displayText });
|
|
3723
3977
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
3724
3978
|
if (images && images.length > 0) {
|
|
3725
3979
|
content.push(...images);
|
|
@@ -3737,7 +3991,7 @@ export class AgentSession {
|
|
|
3737
3991
|
*/
|
|
3738
3992
|
async #queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
|
|
3739
3993
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
3740
|
-
this.#followUpMessages.push(displayText);
|
|
3994
|
+
this.#followUpMessages.push({ text: displayText });
|
|
3741
3995
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
3742
3996
|
if (images && images.length > 0) {
|
|
3743
3997
|
content.push(...images);
|
|
@@ -3973,8 +4227,8 @@ export class AgentSession {
|
|
|
3973
4227
|
* Useful for restoring to editor when user aborts.
|
|
3974
4228
|
*/
|
|
3975
4229
|
clearQueue(): { steering: string[]; followUp: string[] } {
|
|
3976
|
-
const steering =
|
|
3977
|
-
const followUp =
|
|
4230
|
+
const steering = this.#steeringMessages.map(e => e.text);
|
|
4231
|
+
const followUp = this.#followUpMessages.map(e => e.text);
|
|
3978
4232
|
this.#steeringMessages = [];
|
|
3979
4233
|
this.#followUpMessages = [];
|
|
3980
4234
|
this.agent.clearAllQueues();
|
|
@@ -3986,27 +4240,35 @@ export class AgentSession {
|
|
|
3986
4240
|
return this.#steeringMessages.length + this.#followUpMessages.length + this.#pendingNextTurnMessages.length;
|
|
3987
4241
|
}
|
|
3988
4242
|
|
|
3989
|
-
/** 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. */
|
|
3990
4247
|
getQueuedMessages(): { steering: readonly string[]; followUp: readonly string[] } {
|
|
3991
|
-
return {
|
|
4248
|
+
return {
|
|
4249
|
+
steering: this.#steeringMessages.map(e => e.text),
|
|
4250
|
+
followUp: this.#followUpMessages.map(e => e.text),
|
|
4251
|
+
};
|
|
3992
4252
|
}
|
|
3993
4253
|
|
|
3994
4254
|
/**
|
|
3995
4255
|
* Pop the last queued message (steering first, then follow-up).
|
|
3996
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.
|
|
3997
4259
|
*/
|
|
3998
4260
|
popLastQueuedMessage(): string | undefined {
|
|
3999
4261
|
// Pop from steering first (LIFO)
|
|
4000
4262
|
if (this.#steeringMessages.length > 0) {
|
|
4001
|
-
const
|
|
4263
|
+
const entry = this.#steeringMessages.pop();
|
|
4002
4264
|
this.agent.popLastSteer();
|
|
4003
|
-
return
|
|
4265
|
+
return entry?.text;
|
|
4004
4266
|
}
|
|
4005
4267
|
// Then from follow-up
|
|
4006
4268
|
if (this.#followUpMessages.length > 0) {
|
|
4007
|
-
const
|
|
4269
|
+
const entry = this.#followUpMessages.pop();
|
|
4008
4270
|
this.agent.popLastFollowUp();
|
|
4009
|
-
return
|
|
4271
|
+
return entry?.text;
|
|
4010
4272
|
}
|
|
4011
4273
|
return undefined;
|
|
4012
4274
|
}
|
|
@@ -4120,7 +4382,7 @@ export class AgentSession {
|
|
|
4120
4382
|
/**
|
|
4121
4383
|
* Abort current operation and wait for agent to become idle.
|
|
4122
4384
|
*/
|
|
4123
|
-
async abort(): Promise<void> {
|
|
4385
|
+
async abort(options?: { goalReason?: "interrupted" | "internal" }): Promise<void> {
|
|
4124
4386
|
this.abortRetry();
|
|
4125
4387
|
this.#promptGeneration++;
|
|
4126
4388
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
@@ -4132,6 +4394,7 @@ export class AgentSession {
|
|
|
4132
4394
|
this.agent.abort();
|
|
4133
4395
|
await postPromptDrain;
|
|
4134
4396
|
await this.agent.waitForIdle();
|
|
4397
|
+
await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
|
|
4135
4398
|
// Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
|
|
4136
4399
|
// block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
|
|
4137
4400
|
// a subsequent prompt() can incorrectly observe the session as busy after an abort.
|
|
@@ -4545,6 +4808,7 @@ export class AgentSession {
|
|
|
4545
4808
|
if (persist && effectiveLevel !== undefined && effectiveLevel !== ThinkingLevel.Off) {
|
|
4546
4809
|
this.settings.set("defaultThinkingLevel", effectiveLevel);
|
|
4547
4810
|
}
|
|
4811
|
+
this.#emit({ type: "thinking_level_changed", thinkingLevel: effectiveLevel });
|
|
4548
4812
|
}
|
|
4549
4813
|
}
|
|
4550
4814
|
|
|
@@ -4678,8 +4942,6 @@ export class AgentSession {
|
|
|
4678
4942
|
|
|
4679
4943
|
let hookCompaction: CompactionResult | undefined;
|
|
4680
4944
|
let fromExtension = false;
|
|
4681
|
-
let hookContext: string[] | undefined;
|
|
4682
|
-
let hookPrompt: string | undefined;
|
|
4683
4945
|
let preserveData: Record<string, unknown> | undefined;
|
|
4684
4946
|
|
|
4685
4947
|
if (this.#extensionRunner?.hasHandlers("session_before_compact")) {
|
|
@@ -4701,23 +4963,7 @@ export class AgentSession {
|
|
|
4701
4963
|
}
|
|
4702
4964
|
}
|
|
4703
4965
|
|
|
4704
|
-
|
|
4705
|
-
const compactMessages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
|
|
4706
|
-
const result = (await this.#extensionRunner.emit({
|
|
4707
|
-
type: "session.compacting",
|
|
4708
|
-
sessionId: this.sessionId,
|
|
4709
|
-
messages: compactMessages,
|
|
4710
|
-
})) as { context?: string[]; prompt?: string; preserveData?: Record<string, unknown> } | undefined;
|
|
4711
|
-
|
|
4712
|
-
hookContext = result?.context;
|
|
4713
|
-
hookPrompt = result?.prompt;
|
|
4714
|
-
preserveData = result?.preserveData;
|
|
4715
|
-
}
|
|
4716
|
-
|
|
4717
|
-
const memoryBackendContext = await this.#collectMemoryBackendContext(preparation);
|
|
4718
|
-
if (memoryBackendContext) {
|
|
4719
|
-
hookContext = hookContext ? [...hookContext, memoryBackendContext] : [memoryBackendContext];
|
|
4720
|
-
}
|
|
4966
|
+
const compactionPrep = await this.#prepareCompactionFromHooks(preparation, hookCompaction);
|
|
4721
4967
|
|
|
4722
4968
|
let summary: string;
|
|
4723
4969
|
let shortSummary: string | undefined;
|
|
@@ -4725,14 +4971,13 @@ export class AgentSession {
|
|
|
4725
4971
|
let tokensBefore: number;
|
|
4726
4972
|
let details: unknown;
|
|
4727
4973
|
|
|
4728
|
-
if (
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
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;
|
|
4736
4981
|
} else {
|
|
4737
4982
|
// Generate compaction result. Only convert known abort-shaped
|
|
4738
4983
|
// rejections (AbortError raised while the abort signal is set,
|
|
@@ -4751,8 +4996,8 @@ export class AgentSession {
|
|
|
4751
4996
|
customInstructions,
|
|
4752
4997
|
compactionAbortController.signal,
|
|
4753
4998
|
{
|
|
4754
|
-
promptOverride: hookPrompt,
|
|
4755
|
-
extraContext: hookContext,
|
|
4999
|
+
promptOverride: compactionPrep.hookPrompt,
|
|
5000
|
+
extraContext: compactionPrep.hookContext,
|
|
4756
5001
|
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
|
4757
5002
|
},
|
|
4758
5003
|
);
|
|
@@ -4761,7 +5006,7 @@ export class AgentSession {
|
|
|
4761
5006
|
firstKeptEntryId = result.firstKeptEntryId;
|
|
4762
5007
|
tokensBefore = result.tokensBefore;
|
|
4763
5008
|
details = result.details;
|
|
4764
|
-
preserveData = { ...(preserveData ?? {}), ...(result.preserveData ?? {}) };
|
|
5009
|
+
preserveData = { ...(compactionPrep.preserveData ?? {}), ...(result.preserveData ?? {}) };
|
|
4765
5010
|
} catch (err) {
|
|
4766
5011
|
if (err instanceof CompactionCancelledError) {
|
|
4767
5012
|
throw err;
|
|
@@ -5199,14 +5444,14 @@ export class AgentSession {
|
|
|
5199
5444
|
}
|
|
5200
5445
|
|
|
5201
5446
|
const calledRequiredTool = assistantMessage.content.some(
|
|
5202
|
-
content => content.type === "toolCall" && (content.name === "ask" || content.name === "
|
|
5447
|
+
content => content.type === "toolCall" && (content.name === "ask" || content.name === "resolve"),
|
|
5203
5448
|
);
|
|
5204
5449
|
if (calledRequiredTool) {
|
|
5205
5450
|
return;
|
|
5206
5451
|
}
|
|
5207
|
-
const hasRequiredTools = this.#toolRegistry.has("ask") && this.#toolRegistry.has("
|
|
5452
|
+
const hasRequiredTools = this.#toolRegistry.has("ask") && this.#toolRegistry.has("resolve");
|
|
5208
5453
|
if (!hasRequiredTools) {
|
|
5209
|
-
logger.warn("Plan mode enforcement skipped because ask/
|
|
5454
|
+
logger.warn("Plan mode enforcement skipped because ask/resolve tools are unavailable", {
|
|
5210
5455
|
activeToolNames: this.agent.state.tools.map(tool => tool.name),
|
|
5211
5456
|
});
|
|
5212
5457
|
return;
|
|
@@ -5214,7 +5459,6 @@ export class AgentSession {
|
|
|
5214
5459
|
|
|
5215
5460
|
const reminder = prompt.render(planModeToolDecisionReminderPrompt, {
|
|
5216
5461
|
askToolName: "ask",
|
|
5217
|
-
exitToolName: "exit_plan_mode",
|
|
5218
5462
|
});
|
|
5219
5463
|
|
|
5220
5464
|
await this.prompt(reminder, {
|
|
@@ -5721,6 +5965,64 @@ export class AgentSession {
|
|
|
5721
5965
|
throw this.#buildCompactionAuthError();
|
|
5722
5966
|
}
|
|
5723
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
|
+
|
|
5724
6026
|
/**
|
|
5725
6027
|
* Internal: Run auto-compaction with events.
|
|
5726
6028
|
*/
|
|
@@ -5842,8 +6144,6 @@ export class AgentSession {
|
|
|
5842
6144
|
|
|
5843
6145
|
let hookCompaction: CompactionResult | undefined;
|
|
5844
6146
|
let fromExtension = false;
|
|
5845
|
-
let hookContext: string[] | undefined;
|
|
5846
|
-
let hookPrompt: string | undefined;
|
|
5847
6147
|
let preserveData: Record<string, unknown> | undefined;
|
|
5848
6148
|
|
|
5849
6149
|
if (this.#extensionRunner?.hasHandlers("session_before_compact")) {
|
|
@@ -5872,23 +6172,7 @@ export class AgentSession {
|
|
|
5872
6172
|
}
|
|
5873
6173
|
}
|
|
5874
6174
|
|
|
5875
|
-
|
|
5876
|
-
const compactMessages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
|
|
5877
|
-
const result = (await this.#extensionRunner.emit({
|
|
5878
|
-
type: "session.compacting",
|
|
5879
|
-
sessionId: this.sessionId,
|
|
5880
|
-
messages: compactMessages,
|
|
5881
|
-
})) as { context?: string[]; prompt?: string; preserveData?: Record<string, unknown> } | undefined;
|
|
5882
|
-
|
|
5883
|
-
hookContext = result?.context;
|
|
5884
|
-
hookPrompt = result?.prompt;
|
|
5885
|
-
preserveData = result?.preserveData;
|
|
5886
|
-
}
|
|
5887
|
-
|
|
5888
|
-
const memoryBackendContext = await this.#collectMemoryBackendContext(preparation);
|
|
5889
|
-
if (memoryBackendContext) {
|
|
5890
|
-
hookContext = hookContext ? [...hookContext, memoryBackendContext] : [memoryBackendContext];
|
|
5891
|
-
}
|
|
6175
|
+
const compactionPrep = await this.#prepareCompactionFromHooks(preparation, hookCompaction);
|
|
5892
6176
|
|
|
5893
6177
|
let summary: string;
|
|
5894
6178
|
let shortSummary: string | undefined;
|
|
@@ -5896,14 +6180,13 @@ export class AgentSession {
|
|
|
5896
6180
|
let tokensBefore: number;
|
|
5897
6181
|
let details: unknown;
|
|
5898
6182
|
|
|
5899
|
-
if (
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
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;
|
|
5907
6190
|
} else {
|
|
5908
6191
|
const candidates = this.#getCompactionModelCandidates(availableModels);
|
|
5909
6192
|
const retrySettings = this.settings.getGroup("retry");
|
|
@@ -5918,8 +6201,8 @@ export class AgentSession {
|
|
|
5918
6201
|
while (true) {
|
|
5919
6202
|
try {
|
|
5920
6203
|
compactResult = await compact(preparation, candidate, apiKey, undefined, autoCompactionSignal, {
|
|
5921
|
-
promptOverride: hookPrompt,
|
|
5922
|
-
extraContext: hookContext,
|
|
6204
|
+
promptOverride: compactionPrep.hookPrompt,
|
|
6205
|
+
extraContext: compactionPrep.hookContext,
|
|
5923
6206
|
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
|
5924
6207
|
metadata: this.agent.metadataForProvider(candidate.provider),
|
|
5925
6208
|
initiatorOverride: "agent",
|
|
@@ -5976,7 +6259,7 @@ export class AgentSession {
|
|
|
5976
6259
|
error: message,
|
|
5977
6260
|
model: `${candidate.provider}/${candidate.id}`,
|
|
5978
6261
|
});
|
|
5979
|
-
await
|
|
6262
|
+
await scheduler.wait(delayMs, { signal: autoCompactionSignal });
|
|
5980
6263
|
}
|
|
5981
6264
|
}
|
|
5982
6265
|
|
|
@@ -5997,7 +6280,7 @@ export class AgentSession {
|
|
|
5997
6280
|
firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
5998
6281
|
tokensBefore = compactResult.tokensBefore;
|
|
5999
6282
|
details = compactResult.details;
|
|
6000
|
-
preserveData = { ...(preserveData ?? {}), ...(compactResult.preserveData ?? {}) };
|
|
6283
|
+
preserveData = { ...(compactionPrep.preserveData ?? {}), ...(compactResult.preserveData ?? {}) };
|
|
6001
6284
|
}
|
|
6002
6285
|
|
|
6003
6286
|
if (autoCompactionSignal.aborted) {
|
|
@@ -6148,10 +6431,11 @@ export class AgentSession {
|
|
|
6148
6431
|
|
|
6149
6432
|
#isTransientTransportErrorMessage(errorMessage: string): boolean {
|
|
6150
6433
|
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
|
|
6151
|
-
// 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
|
|
6152
6436
|
return (
|
|
6153
6437
|
isUnexpectedSocketCloseMessage(errorMessage) ||
|
|
6154
|
-
/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(
|
|
6155
6439
|
errorMessage,
|
|
6156
6440
|
)
|
|
6157
6441
|
);
|
|
@@ -6501,7 +6785,7 @@ export class AgentSession {
|
|
|
6501
6785
|
this.#retryAbortController?.abort();
|
|
6502
6786
|
this.#retryAbortController = retryAbortController;
|
|
6503
6787
|
try {
|
|
6504
|
-
await
|
|
6788
|
+
await scheduler.wait(delayMs, { signal: retryAbortController.signal });
|
|
6505
6789
|
} catch {
|
|
6506
6790
|
if (this.#retryAbortController !== retryAbortController) {
|
|
6507
6791
|
return false;
|
|
@@ -7833,21 +8117,11 @@ export class AgentSession {
|
|
|
7833
8117
|
* @returns Text content, or undefined if no assistant message exists
|
|
7834
8118
|
*/
|
|
7835
8119
|
getLastAssistantText(): string | undefined {
|
|
7836
|
-
const lastAssistant = this
|
|
7837
|
-
.slice()
|
|
7838
|
-
.reverse()
|
|
7839
|
-
.find(m => {
|
|
7840
|
-
if (m.role !== "assistant") return false;
|
|
7841
|
-
const msg = m as AssistantMessage;
|
|
7842
|
-
// Skip aborted messages with no content
|
|
7843
|
-
if (msg.stopReason === "aborted" && msg.content.length === 0) return false;
|
|
7844
|
-
return true;
|
|
7845
|
-
});
|
|
7846
|
-
|
|
8120
|
+
const lastAssistant = this.#getLastCopyCandidateAssistantMessage();
|
|
7847
8121
|
if (!lastAssistant) return undefined;
|
|
7848
8122
|
|
|
7849
8123
|
let text = "";
|
|
7850
|
-
for (const content of
|
|
8124
|
+
for (const content of lastAssistant.content) {
|
|
7851
8125
|
if (content.type === "text") {
|
|
7852
8126
|
text += content.text;
|
|
7853
8127
|
}
|
|
@@ -7856,6 +8130,54 @@ export class AgentSession {
|
|
|
7856
8130
|
return text.trim() || undefined;
|
|
7857
8131
|
}
|
|
7858
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
|
+
|
|
7859
8181
|
/**
|
|
7860
8182
|
* Format the entire session as plain text for clipboard export.
|
|
7861
8183
|
* Includes user messages, assistant text, thinking blocks, tool calls, and tool results.
|