@oh-my-pi/pi-coding-agent 14.5.8 → 14.5.10
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 +56 -0
- package/package.json +7 -15
- package/scripts/build-binary.ts +1 -1
- package/src/cli/update-cli.ts +25 -1
- package/src/config/model-registry.ts +21 -19
- package/src/config/settings-schema.ts +14 -19
- package/src/discovery/claude-plugins.ts +28 -3
- package/src/edit/modes/atom.lark +7 -5
- package/src/edit/modes/atom.ts +510 -73
- package/src/edit/modes/hashline.ts +172 -91
- package/src/extensibility/extensions/runner.ts +34 -1
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/lsp/client.ts +27 -35
- package/src/lsp/index.ts +2 -4
- package/src/lsp/render.ts +0 -3
- package/src/lsp/types.ts +1 -4
- package/src/lsp/utils.ts +18 -14
- package/src/memories/index.ts +5 -0
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/controllers/command-controller.ts +17 -0
- package/src/modes/controllers/input-controller.ts +7 -1
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/interactive-mode.ts +57 -26
- package/src/modes/theme/theme.ts +10 -1
- package/src/modes/types.ts +5 -3
- package/src/modes/utils/context-usage.ts +294 -0
- package/src/modes/utils/ui-helpers.ts +19 -6
- package/src/prompts/system/auto-continue.md +1 -0
- package/src/prompts/tools/atom.md +99 -44
- package/src/prompts/tools/exit-plan-mode.md +5 -39
- package/src/prompts/tools/github.md +3 -3
- package/src/prompts/tools/lsp.md +2 -3
- package/src/prompts/tools/{run-command.md → recipe.md} +1 -1
- package/src/prompts/tools/task.md +34 -147
- package/src/prompts/tools/todo-write.md +22 -64
- package/src/sdk.ts +13 -2
- package/src/session/agent-session.ts +175 -79
- package/src/session/compaction/compaction.ts +35 -22
- package/src/session/session-dump-format.ts +1 -0
- package/src/session/session-manager.ts +19 -2
- package/src/slash-commands/builtin-registry.ts +12 -5
- package/src/tools/bash.ts +9 -4
- package/src/tools/debug.ts +57 -70
- package/src/tools/gh.ts +267 -119
- package/src/tools/index.ts +7 -7
- package/src/tools/{run-command → recipe}/index.ts +19 -19
- package/src/tools/recipe/render.ts +19 -0
- package/src/tools/{run-command → recipe}/runner.ts +28 -7
- package/src/tools/{run-command → recipe}/runners/pkg.ts +23 -53
- package/src/tools/renderers.ts +2 -2
- package/src/utils/git.ts +61 -2
- package/src/web/search/providers/searxng.ts +71 -13
- package/src/tools/run-command/render.ts +0 -18
- /package/src/tools/{run-command → recipe}/runners/cargo.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/index.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/just.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/make.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/task.ts +0 -0
|
@@ -1,69 +1,33 @@
|
|
|
1
|
-
Manages a phased task list
|
|
2
|
-
The next pending task is auto-promoted to `in_progress` after
|
|
1
|
+
Manages a phased task list. Pass `ops`: a flat array of operations.
|
|
2
|
+
The next pending task is auto-promoted to `in_progress` after each completion.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
## Shape
|
|
4
|
+
## Operations
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```ts
|
|
10
|
-
{
|
|
11
|
-
ops: [
|
|
12
|
-
{ op: "replace", phases: [...] },
|
|
13
|
-
{ op: "start", task: "task-3" },
|
|
14
|
-
{ op: "done", phase: "Implementation" },
|
|
15
|
-
{ op: "rm" },
|
|
16
|
-
{ op: "drop", task: "task-9" },
|
|
17
|
-
{ op: "append", phase: "Implementation", items: [{ id: "task-10", label: "Run tests" }] },
|
|
18
|
-
],
|
|
19
|
-
}
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Operation fields
|
|
23
|
-
|
|
24
|
-
|Field|Type|When to use|
|
|
6
|
+
|`op`|Required fields|Effect|
|
|
25
7
|
|---|---|---|
|
|
26
|
-
|`
|
|
27
|
-
|`task`|
|
|
28
|
-
|`
|
|
29
|
-
|`
|
|
30
|
-
|`
|
|
31
|
-
|`
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- `
|
|
36
|
-
- `
|
|
37
|
-
- `drop`: marks one task, one phase, or all tasks abandoned
|
|
38
|
-
- `append`: appends `items` to `phase`; creates the phase if missing
|
|
39
|
-
- `replace`: replaces the full todo list
|
|
40
|
-
- `note`: append `text` as a new note attached to `task`. Notes are append-only context the user added; they only render to you when the task is `in_progress`. Other tasks display only a `+N` marker. Use this when you want to leave a follow-up reminder for yourself when you reach a later task.
|
|
41
|
-
|
|
42
|
-
If `done`, `rm`, or `drop` omits both `task` and `phase`, it applies to all tasks.
|
|
43
|
-
|
|
44
|
-
## Task Anatomy
|
|
45
|
-
- `label`: Short label (5-10 words). What is being done, not how.
|
|
46
|
-
- `replace` task `content` should stay short and specific.
|
|
47
|
-
|
|
48
|
-
## Phase Anatomy
|
|
49
|
-
- `name`: Short, human-readable noun phrase (1-3 words). Capitalize naturally.
|
|
50
|
-
- Always prefix with a roman-numeral ordinal (`I.`, `II.`, `III.`, `IV.`, …) to convey ordering — e.g. `I. Foundation`, `II. Auth`, `III. Routing`. Single-phase plans use `I.` too.
|
|
51
|
-
- You **MUST NOT** use snake_case, `Phase1_*`, arabic numerals (`1.`), or letter prefixes (`A.`) — they render as ugly identifiers.
|
|
8
|
+
|`replace`|`phases`|Replace the full list (initial setup, full restructure)|
|
|
9
|
+
|`start`|`task`|Set task to `in_progress`|
|
|
10
|
+
|`done`|`task` or `phase` (or neither = all)|Mark completed|
|
|
11
|
+
|`drop`|`task` or `phase` (or neither = all)|Mark abandoned|
|
|
12
|
+
|`rm`|`task` or `phase` (or neither = all)|Remove|
|
|
13
|
+
|`append`|`phase`, `items: {id, label}[]`|Append tasks; creates phase if missing|
|
|
14
|
+
|`note`|`task`, `text`|Append a note to `task.notes`. Only use to leave reminders for future-you.|
|
|
15
|
+
|
|
16
|
+
## Anatomy
|
|
17
|
+
- **Task `label`**: 5–10 words, what is being done, not how.
|
|
18
|
+
- **Phase `name`**: short noun phrase prefixed with a roman numeral — `I. Foundation`, `II. Auth`, `III. Verification`. Single-phase plans still use `I.`. Never use snake_case, arabic numerals, or letter prefixes.
|
|
52
19
|
|
|
53
20
|
## Rules
|
|
54
21
|
- Mark tasks done immediately after finishing — never defer.
|
|
55
|
-
- Complete phases in order
|
|
56
|
-
- On blockers, append a new task to the active phase.
|
|
22
|
+
- Complete phases in order.
|
|
23
|
+
- On blockers, `append` a new task to the active phase.
|
|
57
24
|
- Keep ids stable once introduced.
|
|
58
|
-
</protocol>
|
|
59
25
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
4. New instructions arrive mid-task — capture before proceeding
|
|
66
|
-
</conditions>
|
|
26
|
+
## When to create a list
|
|
27
|
+
- Task requires 3+ distinct steps
|
|
28
|
+
- User explicitly requests one
|
|
29
|
+
- User provides a set of tasks to complete
|
|
30
|
+
- New instructions arrive mid-task — capture before proceeding
|
|
67
31
|
|
|
68
32
|
<examples>
|
|
69
33
|
# Initial setup (multi-phase)
|
|
@@ -81,9 +45,3 @@ Create a todo list when:
|
|
|
81
45
|
# Append tasks to a phase
|
|
82
46
|
`{"ops":[{"op":"append","phase":"II. Auth","items":[{"id":"task-8","label":"Handle retries"},{"id":"task-9","label":"Run tests"}]}]}`
|
|
83
47
|
</examples>
|
|
84
|
-
|
|
85
|
-
<avoid>
|
|
86
|
-
- Single-step tasks — act directly
|
|
87
|
-
- Conversational or informational requests
|
|
88
|
-
- Tasks completable in under 3 trivial steps
|
|
89
|
-
</avoid>
|
package/src/sdk.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
INTENT_FIELD,
|
|
7
7
|
type ThinkingLevel,
|
|
8
8
|
} from "@oh-my-pi/pi-agent-core";
|
|
9
|
-
import type { Message, Model } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import type { Message, Model, SimpleStreamOptions } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import {
|
|
11
11
|
getOpenAICodexTransportDetails,
|
|
12
12
|
prewarmOpenAICodexResponses,
|
|
@@ -793,7 +793,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
793
793
|
thinkingLevel = defaultRoleSpec.thinkingLevel;
|
|
794
794
|
}
|
|
795
795
|
|
|
796
|
-
//
|
|
796
|
+
// Prefer the selected model's configured defaultLevel, otherwise fall back
|
|
797
|
+
// to the global settings default.
|
|
798
|
+
if (thinkingLevel === undefined && model?.thinking?.defaultLevel !== undefined) {
|
|
799
|
+
thinkingLevel = model.thinking.defaultLevel;
|
|
800
|
+
}
|
|
797
801
|
if (thinkingLevel === undefined) {
|
|
798
802
|
thinkingLevel = settings.get("defaultThinkingLevel");
|
|
799
803
|
}
|
|
@@ -1498,6 +1502,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1498
1502
|
return await extensionRunner.emitBeforeProviderRequest(payload);
|
|
1499
1503
|
}
|
|
1500
1504
|
: undefined;
|
|
1505
|
+
const onResponse: SimpleStreamOptions["onResponse"] | undefined = extensionRunner
|
|
1506
|
+
? async (response, model) => {
|
|
1507
|
+
await extensionRunner.emitAfterProviderResponse(response, model);
|
|
1508
|
+
}
|
|
1509
|
+
: undefined;
|
|
1501
1510
|
|
|
1502
1511
|
const setToolUIContext = (uiContext: ExtensionUIContext, hasUI: boolean) => {
|
|
1503
1512
|
toolContextStore.setUIContext(uiContext, hasUI);
|
|
@@ -1527,6 +1536,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1527
1536
|
},
|
|
1528
1537
|
convertToLlm: convertToLlmFinal,
|
|
1529
1538
|
onPayload,
|
|
1539
|
+
onResponse,
|
|
1530
1540
|
sessionId: providerSessionId,
|
|
1531
1541
|
transformContext,
|
|
1532
1542
|
steeringMode: settings.get("steeringMode") ?? "one-at-a-time",
|
|
@@ -1599,6 +1609,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1599
1609
|
toolRegistry,
|
|
1600
1610
|
transformContext,
|
|
1601
1611
|
onPayload,
|
|
1612
|
+
onResponse,
|
|
1602
1613
|
convertToLlm: convertToLlmFinal,
|
|
1603
1614
|
rebuildSystemPrompt,
|
|
1604
1615
|
mcpDiscoveryEnabled,
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
calculateRateLimitBackoffMs,
|
|
47
47
|
getSupportedEfforts,
|
|
48
48
|
isContextOverflow,
|
|
49
|
+
isUnexpectedSocketCloseMessage,
|
|
49
50
|
isUsageLimitError,
|
|
50
51
|
modelsAreEqual,
|
|
51
52
|
parseRateLimitReason,
|
|
@@ -104,7 +105,7 @@ import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
|
104
105
|
import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
105
106
|
import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
106
107
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
107
|
-
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
108
|
+
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
108
109
|
import {
|
|
109
110
|
disposeKernelSessionsByOwner,
|
|
110
111
|
executePython as executePythonCommand,
|
|
@@ -120,6 +121,7 @@ import {
|
|
|
120
121
|
} from "../mcp/discoverable-tool-metadata";
|
|
121
122
|
import { getCurrentThemeName, theme } from "../modes/theme/theme";
|
|
122
123
|
import type { PlanModeState } from "../plan-mode/state";
|
|
124
|
+
import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
|
|
123
125
|
import autoHandoffThresholdFocusPrompt from "../prompts/system/auto-handoff-threshold-focus.md" with { type: "text" };
|
|
124
126
|
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
125
127
|
import handoffDocumentPrompt from "../prompts/system/handoff-document.md" with { type: "text" };
|
|
@@ -244,6 +246,8 @@ export interface AgentSessionConfig {
|
|
|
244
246
|
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
245
247
|
/** Provider payload hook used by the active session request path */
|
|
246
248
|
onPayload?: SimpleStreamOptions["onPayload"];
|
|
249
|
+
/** Provider response hook used by the active session request path */
|
|
250
|
+
onResponse?: SimpleStreamOptions["onResponse"];
|
|
247
251
|
/** Current session message-to-LLM conversion pipeline */
|
|
248
252
|
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
249
253
|
/** System prompt builder that can consider tool availability */
|
|
@@ -469,7 +473,7 @@ export class AgentSession {
|
|
|
469
473
|
#toolChoiceQueue = new ToolChoiceQueue();
|
|
470
474
|
|
|
471
475
|
// Bash execution state
|
|
472
|
-
#
|
|
476
|
+
#bashAbortControllers = new Set<AbortController>();
|
|
473
477
|
#pendingBashMessages: BashExecutionMessage[] = [];
|
|
474
478
|
|
|
475
479
|
// Python execution state
|
|
@@ -507,6 +511,7 @@ export class AgentSession {
|
|
|
507
511
|
#toolRegistry: Map<string, AgentTool>;
|
|
508
512
|
#transformContext: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
509
513
|
#onPayload: SimpleStreamOptions["onPayload"] | undefined;
|
|
514
|
+
#onResponse: SimpleStreamOptions["onResponse"] | undefined;
|
|
510
515
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
511
516
|
#rebuildSystemPrompt: ((toolNames: string[], tools: Map<string, AgentTool>) => Promise<string>) | undefined;
|
|
512
517
|
#baseSystemPrompt: string;
|
|
@@ -526,8 +531,7 @@ export class AgentSession {
|
|
|
526
531
|
#ttsrRetryToken = 0;
|
|
527
532
|
#ttsrResumePromise: Promise<void> | undefined = undefined;
|
|
528
533
|
#ttsrResumeResolve: (() => void) | undefined = undefined;
|
|
529
|
-
#
|
|
530
|
-
#postPromptTaskIds = new Set<number>();
|
|
534
|
+
#postPromptTasks = new Set<Promise<void>>();
|
|
531
535
|
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
532
536
|
#postPromptTasksResolve: (() => void) | undefined = undefined;
|
|
533
537
|
#postPromptTasksAbortController = new AbortController();
|
|
@@ -593,6 +597,7 @@ export class AgentSession {
|
|
|
593
597
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
594
598
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
595
599
|
this.#onPayload = config.onPayload;
|
|
600
|
+
this.#onResponse = config.onResponse;
|
|
596
601
|
this.#convertToLlm = config.convertToLlm ?? convertToLlm;
|
|
597
602
|
this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
|
|
598
603
|
this.#baseSystemPrompt = this.agent.state.systemPrompt;
|
|
@@ -1144,14 +1149,13 @@ export class AgentSession {
|
|
|
1144
1149
|
}
|
|
1145
1150
|
|
|
1146
1151
|
#trackPostPromptTask(task: Promise<void>): void {
|
|
1147
|
-
|
|
1148
|
-
this.#postPromptTaskIds.add(taskId);
|
|
1152
|
+
this.#postPromptTasks.add(task);
|
|
1149
1153
|
this.#ensurePostPromptTasksPromise();
|
|
1150
1154
|
void task
|
|
1151
1155
|
.catch(() => {})
|
|
1152
1156
|
.finally(() => {
|
|
1153
|
-
this.#
|
|
1154
|
-
if (this.#
|
|
1157
|
+
this.#postPromptTasks.delete(task);
|
|
1158
|
+
if (this.#postPromptTasks.size === 0) {
|
|
1155
1159
|
this.#resolvePostPromptTasks();
|
|
1156
1160
|
}
|
|
1157
1161
|
});
|
|
@@ -1217,11 +1221,11 @@ export class AgentSession {
|
|
|
1217
1221
|
await this.#promptWithMessage(
|
|
1218
1222
|
{
|
|
1219
1223
|
role: "developer",
|
|
1220
|
-
content: [{ type: "text", text:
|
|
1224
|
+
content: [{ type: "text", text: autoContinuePrompt }],
|
|
1221
1225
|
attribution: "agent",
|
|
1222
1226
|
timestamp: Date.now(),
|
|
1223
1227
|
},
|
|
1224
|
-
|
|
1228
|
+
autoContinuePrompt,
|
|
1225
1229
|
{ skipPostPromptRecoveryWait: true },
|
|
1226
1230
|
);
|
|
1227
1231
|
};
|
|
@@ -1235,11 +1239,21 @@ export class AgentSession {
|
|
|
1235
1239
|
);
|
|
1236
1240
|
}
|
|
1237
1241
|
|
|
1238
|
-
#cancelPostPromptTasks(): void {
|
|
1242
|
+
async #cancelPostPromptTasks(): Promise<void> {
|
|
1239
1243
|
this.#postPromptTasksAbortController.abort();
|
|
1240
1244
|
this.#postPromptTasksAbortController = new AbortController();
|
|
1241
|
-
this.#
|
|
1242
|
-
|
|
1245
|
+
this.#resolveTtsrResume();
|
|
1246
|
+
|
|
1247
|
+
const pendingTasks = Array.from(this.#postPromptTasks);
|
|
1248
|
+
if (pendingTasks.length === 0) {
|
|
1249
|
+
this.#resolvePostPromptTasks();
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
await Promise.allSettled(pendingTasks);
|
|
1254
|
+
if (this.#postPromptTasks.size === 0) {
|
|
1255
|
+
this.#resolvePostPromptTasks();
|
|
1256
|
+
}
|
|
1243
1257
|
}
|
|
1244
1258
|
/**
|
|
1245
1259
|
* Wait for retry, TTSR resume, and any background continuation to settle.
|
|
@@ -1523,10 +1537,19 @@ export class AgentSession {
|
|
|
1523
1537
|
const path = typeof args.path === "string" ? args.path : undefined;
|
|
1524
1538
|
if (!path) return undefined;
|
|
1525
1539
|
|
|
1540
|
+
// `local://` URLs (e.g. local://PLAN.md for plan-mode) resolve to a real
|
|
1541
|
+
// on-disk artifacts path; pre-caching works as long as we ask the
|
|
1542
|
+
// local-protocol handler. Other internal-scheme URLs (agent://, skill://,
|
|
1543
|
+
// rule://, mcp://, artifact://) have no stable filesystem representation;
|
|
1544
|
+
// skip pre-cache entirely for those — the edit tool itself will reject
|
|
1545
|
+
// them through its normal dispatch path.
|
|
1546
|
+
const resolvedPath = this.#resolveSessionFsPath(path);
|
|
1547
|
+
if (resolvedPath === undefined) return undefined;
|
|
1548
|
+
|
|
1526
1549
|
return {
|
|
1527
1550
|
toolCall,
|
|
1528
1551
|
path,
|
|
1529
|
-
resolvedPath
|
|
1552
|
+
resolvedPath,
|
|
1530
1553
|
diff: typeof args.diff === "string" ? args.diff : undefined,
|
|
1531
1554
|
op: typeof args.op === "string" ? args.op : undefined,
|
|
1532
1555
|
rename: typeof args.rename === "string" ? args.rename : undefined,
|
|
@@ -1600,11 +1623,47 @@ export class AgentSession {
|
|
|
1600
1623
|
}
|
|
1601
1624
|
|
|
1602
1625
|
/** Invalidate cache for a file after an edit completes to prevent stale data */
|
|
1603
|
-
#invalidateFileCacheForPath(
|
|
1604
|
-
const resolvedPath =
|
|
1626
|
+
#invalidateFileCacheForPath(filePath: string): void {
|
|
1627
|
+
const resolvedPath = this.#resolveSessionFsPath(filePath);
|
|
1628
|
+
if (resolvedPath === undefined) return;
|
|
1605
1629
|
this.#streamingEditFileCache.delete(resolvedPath);
|
|
1606
1630
|
}
|
|
1607
1631
|
|
|
1632
|
+
/**
|
|
1633
|
+
* Resolve a path supplied to a tool to a real filesystem path.
|
|
1634
|
+
*
|
|
1635
|
+
* - `local://` URLs route through the local-protocol handler so they map
|
|
1636
|
+
* onto the session's on-disk artifacts directory; pre-caching, ENOENT
|
|
1637
|
+
* handling, and post-edit invalidation all work normally.
|
|
1638
|
+
* - Other internal-scheme URLs (agent://, skill://, rule://, mcp://,
|
|
1639
|
+
* artifact://) have no stable filesystem path; this returns `undefined`
|
|
1640
|
+
* so callers skip filesystem-only operations.
|
|
1641
|
+
* - Cwd-relative and absolute paths resolve via `resolveToCwd`.
|
|
1642
|
+
*/
|
|
1643
|
+
#resolveSessionFsPath(filePath: string): string | undefined {
|
|
1644
|
+
const normalized = normalizeLocalScheme(filePath);
|
|
1645
|
+
if (normalized.startsWith("local:")) {
|
|
1646
|
+
return resolveLocalUrlToPath(normalized, this.#localProtocolOptions());
|
|
1647
|
+
}
|
|
1648
|
+
if (
|
|
1649
|
+
normalized.startsWith("agent://") ||
|
|
1650
|
+
normalized.startsWith("skill://") ||
|
|
1651
|
+
normalized.startsWith("rule://") ||
|
|
1652
|
+
normalized.startsWith("mcp://") ||
|
|
1653
|
+
normalized.startsWith("artifact://")
|
|
1654
|
+
) {
|
|
1655
|
+
return undefined;
|
|
1656
|
+
}
|
|
1657
|
+
return resolveToCwd(normalized, this.sessionManager.getCwd());
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
#localProtocolOptions(): LocalProtocolOptions {
|
|
1661
|
+
return {
|
|
1662
|
+
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
1663
|
+
getSessionId: () => this.sessionManager.getSessionId(),
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1608
1667
|
#maybeAbortStreamingEdit(event: AgentEvent): void {
|
|
1609
1668
|
if (!this.settings.get("edit.streamingAbort")) return;
|
|
1610
1669
|
if (this.#streamingEditAbortTriggered) return;
|
|
@@ -1892,7 +1951,7 @@ export class AgentSession {
|
|
|
1892
1951
|
} catch (error) {
|
|
1893
1952
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
1894
1953
|
}
|
|
1895
|
-
this.#cancelPostPromptTasks();
|
|
1954
|
+
await this.#cancelPostPromptTasks();
|
|
1896
1955
|
this.#clearTodoClearTimers();
|
|
1897
1956
|
const drained = await this.#asyncJobManager?.dispose({ timeoutMs: 3_000 });
|
|
1898
1957
|
const deliveryState = this.#asyncJobManager?.getDeliveryState();
|
|
@@ -2318,21 +2377,39 @@ export class AgentSession {
|
|
|
2318
2377
|
|
|
2319
2378
|
/** Apply session-level stream hooks to a direct side request. */
|
|
2320
2379
|
prepareSimpleStreamOptions(options: SimpleStreamOptions): SimpleStreamOptions {
|
|
2321
|
-
if (!this.#onPayload) return options;
|
|
2322
|
-
if (!options.onPayload) {
|
|
2323
|
-
return { ...options, onPayload: this.#onPayload };
|
|
2324
|
-
}
|
|
2325
2380
|
const sessionOnPayload = this.#onPayload;
|
|
2326
|
-
const
|
|
2327
|
-
return
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2381
|
+
const sessionOnResponse = this.#onResponse;
|
|
2382
|
+
if (!sessionOnPayload && !sessionOnResponse) return options;
|
|
2383
|
+
|
|
2384
|
+
const preparedOptions: SimpleStreamOptions = { ...options };
|
|
2385
|
+
|
|
2386
|
+
if (sessionOnPayload) {
|
|
2387
|
+
if (!options.onPayload) {
|
|
2388
|
+
preparedOptions.onPayload = sessionOnPayload;
|
|
2389
|
+
} else {
|
|
2390
|
+
const requestOnPayload = options.onPayload;
|
|
2391
|
+
preparedOptions.onPayload = async (payload, model) => {
|
|
2392
|
+
const sessionPayload = await sessionOnPayload(payload, model);
|
|
2393
|
+
const sessionResolvedPayload = sessionPayload ?? payload;
|
|
2394
|
+
const requestPayload = await requestOnPayload(sessionResolvedPayload, model);
|
|
2395
|
+
return requestPayload ?? sessionResolvedPayload;
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
if (sessionOnResponse) {
|
|
2401
|
+
if (!options.onResponse) {
|
|
2402
|
+
preparedOptions.onResponse = sessionOnResponse;
|
|
2403
|
+
} else {
|
|
2404
|
+
const requestOnResponse = options.onResponse;
|
|
2405
|
+
preparedOptions.onResponse = async (response, model) => {
|
|
2406
|
+
await sessionOnResponse(response, model);
|
|
2407
|
+
await requestOnResponse(response, model);
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
return preparedOptions;
|
|
2336
2413
|
}
|
|
2337
2414
|
|
|
2338
2415
|
/** Current steering mode */
|
|
@@ -2466,10 +2543,7 @@ export class AgentSession {
|
|
|
2466
2543
|
if (this.#planReferenceSent) return null;
|
|
2467
2544
|
|
|
2468
2545
|
const planFilePath = this.#planReferencePath;
|
|
2469
|
-
const resolvedPlanPath = resolveLocalUrlToPath(planFilePath,
|
|
2470
|
-
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
2471
|
-
getSessionId: () => this.sessionManager.getSessionId(),
|
|
2472
|
-
});
|
|
2546
|
+
const resolvedPlanPath = resolveLocalUrlToPath(planFilePath, this.#localProtocolOptions());
|
|
2473
2547
|
let planContent: string;
|
|
2474
2548
|
try {
|
|
2475
2549
|
planContent = await Bun.file(resolvedPlanPath).text();
|
|
@@ -2502,15 +2576,9 @@ export class AgentSession {
|
|
|
2502
2576
|
if (!state?.enabled) return null;
|
|
2503
2577
|
const sessionPlanUrl = "local://PLAN.md";
|
|
2504
2578
|
const resolvedPlanPath = state.planFilePath.startsWith("local:")
|
|
2505
|
-
? resolveLocalUrlToPath(normalizeLocalScheme(state.planFilePath),
|
|
2506
|
-
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
2507
|
-
getSessionId: () => this.sessionManager.getSessionId(),
|
|
2508
|
-
})
|
|
2579
|
+
? resolveLocalUrlToPath(normalizeLocalScheme(state.planFilePath), this.#localProtocolOptions())
|
|
2509
2580
|
: resolveToCwd(state.planFilePath, this.sessionManager.getCwd());
|
|
2510
|
-
const resolvedSessionPlan = resolveLocalUrlToPath(sessionPlanUrl,
|
|
2511
|
-
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
2512
|
-
getSessionId: () => this.sessionManager.getSessionId(),
|
|
2513
|
-
});
|
|
2581
|
+
const resolvedSessionPlan = resolveLocalUrlToPath(sessionPlanUrl, this.#localProtocolOptions());
|
|
2514
2582
|
const displayPlanPath =
|
|
2515
2583
|
state.planFilePath.startsWith("local:") || resolvedPlanPath !== resolvedSessionPlan
|
|
2516
2584
|
? state.planFilePath
|
|
@@ -3358,9 +3426,13 @@ export class AgentSession {
|
|
|
3358
3426
|
this.abortRetry();
|
|
3359
3427
|
this.#promptGeneration++;
|
|
3360
3428
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
3361
|
-
this
|
|
3362
|
-
this
|
|
3429
|
+
this.abortCompaction();
|
|
3430
|
+
this.abortHandoff();
|
|
3431
|
+
this.abortBash();
|
|
3432
|
+
this.abortPython();
|
|
3433
|
+
const postPromptDrain = this.#cancelPostPromptTasks();
|
|
3363
3434
|
this.agent.abort();
|
|
3435
|
+
await postPromptDrain;
|
|
3364
3436
|
await this.agent.waitForIdle();
|
|
3365
3437
|
// Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
|
|
3366
3438
|
// block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
|
|
@@ -3555,8 +3627,9 @@ export class AgentSession {
|
|
|
3555
3627
|
);
|
|
3556
3628
|
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
3557
3629
|
|
|
3558
|
-
// Re-apply
|
|
3559
|
-
|
|
3630
|
+
// Re-apply thinking for the newly selected model. Prefer the model's
|
|
3631
|
+
// configured defaultLevel; otherwise preserve the current level.
|
|
3632
|
+
this.setThinkingLevel(model.thinking?.defaultLevel ?? this.thinkingLevel);
|
|
3560
3633
|
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
3561
3634
|
}
|
|
3562
3635
|
|
|
@@ -3577,8 +3650,9 @@ export class AgentSession {
|
|
|
3577
3650
|
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "temporary");
|
|
3578
3651
|
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
3579
3652
|
|
|
3580
|
-
// Apply explicit thinking level
|
|
3581
|
-
|
|
3653
|
+
// Apply explicit thinking level if given; otherwise prefer the model's
|
|
3654
|
+
// configured defaultLevel; otherwise re-clamp the current level.
|
|
3655
|
+
this.setThinkingLevel(thinkingLevel ?? model.thinking?.defaultLevel ?? this.thinkingLevel);
|
|
3582
3656
|
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
3583
3657
|
}
|
|
3584
3658
|
|
|
@@ -3876,9 +3950,13 @@ export class AgentSession {
|
|
|
3876
3950
|
* @param options Optional callbacks for completion/error handling
|
|
3877
3951
|
*/
|
|
3878
3952
|
async compact(customInstructions?: string, options?: CompactOptions): Promise<CompactionResult> {
|
|
3953
|
+
if (this.#compactionAbortController) {
|
|
3954
|
+
throw new Error("Compaction already in progress");
|
|
3955
|
+
}
|
|
3879
3956
|
this.#disconnectFromAgent();
|
|
3880
3957
|
await this.abort();
|
|
3881
|
-
|
|
3958
|
+
const compactionAbortController = new AbortController();
|
|
3959
|
+
this.#compactionAbortController = compactionAbortController;
|
|
3882
3960
|
|
|
3883
3961
|
try {
|
|
3884
3962
|
if (!this.model) {
|
|
@@ -3916,7 +3994,7 @@ export class AgentSession {
|
|
|
3916
3994
|
preparation,
|
|
3917
3995
|
branchEntries: pathEntries,
|
|
3918
3996
|
customInstructions,
|
|
3919
|
-
signal:
|
|
3997
|
+
signal: compactionAbortController.signal,
|
|
3920
3998
|
})) as SessionBeforeCompactResult | undefined;
|
|
3921
3999
|
|
|
3922
4000
|
if (result?.cancel) {
|
|
@@ -3963,7 +4041,7 @@ export class AgentSession {
|
|
|
3963
4041
|
compactionModel,
|
|
3964
4042
|
apiKey,
|
|
3965
4043
|
customInstructions,
|
|
3966
|
-
|
|
4044
|
+
compactionAbortController.signal,
|
|
3967
4045
|
{ promptOverride: hookPrompt, extraContext: hookContext, remoteInstructions: this.#baseSystemPrompt },
|
|
3968
4046
|
);
|
|
3969
4047
|
summary = result.summary;
|
|
@@ -3974,7 +4052,7 @@ export class AgentSession {
|
|
|
3974
4052
|
preserveData = { ...(preserveData ?? {}), ...(result.preserveData ?? {}) };
|
|
3975
4053
|
}
|
|
3976
4054
|
|
|
3977
|
-
if (
|
|
4055
|
+
if (compactionAbortController.signal.aborted) {
|
|
3978
4056
|
throw new Error("Compaction cancelled");
|
|
3979
4057
|
}
|
|
3980
4058
|
|
|
@@ -4021,7 +4099,9 @@ export class AgentSession {
|
|
|
4021
4099
|
options?.onError?.(err);
|
|
4022
4100
|
throw error;
|
|
4023
4101
|
} finally {
|
|
4024
|
-
this.#compactionAbortController
|
|
4102
|
+
if (this.#compactionAbortController === compactionAbortController) {
|
|
4103
|
+
this.#compactionAbortController = undefined;
|
|
4104
|
+
}
|
|
4025
4105
|
this.#reconnectToAgent();
|
|
4026
4106
|
}
|
|
4027
4107
|
}
|
|
@@ -5263,9 +5343,12 @@ export class AgentSession {
|
|
|
5263
5343
|
|
|
5264
5344
|
#isTransientTransportErrorMessage(errorMessage: string): boolean {
|
|
5265
5345
|
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
|
|
5266
|
-
// service unavailable, network/connection errors, fetch failed, terminated, retry delay exceeded
|
|
5267
|
-
return
|
|
5268
|
-
errorMessage
|
|
5346
|
+
// service unavailable, network/connection/socket errors, fetch failed, terminated, retry delay exceeded
|
|
5347
|
+
return (
|
|
5348
|
+
isUnexpectedSocketCloseMessage(errorMessage) ||
|
|
5349
|
+
/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/i.test(
|
|
5350
|
+
errorMessage,
|
|
5351
|
+
)
|
|
5269
5352
|
);
|
|
5270
5353
|
}
|
|
5271
5354
|
|
|
@@ -5608,15 +5691,16 @@ export class AgentSession {
|
|
|
5608
5691
|
this.agent.replaceMessages(messages.slice(0, -1));
|
|
5609
5692
|
}
|
|
5610
5693
|
|
|
5611
|
-
// Wait with exponential backoff (abortable)
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
}
|
|
5616
|
-
this.#retryAbortController = new AbortController();
|
|
5694
|
+
// Wait with exponential backoff (abortable).
|
|
5695
|
+
const retryAbortController = new AbortController();
|
|
5696
|
+
this.#retryAbortController?.abort();
|
|
5697
|
+
this.#retryAbortController = retryAbortController;
|
|
5617
5698
|
try {
|
|
5618
|
-
await abortableSleep(delayMs,
|
|
5699
|
+
await abortableSleep(delayMs, retryAbortController.signal);
|
|
5619
5700
|
} catch {
|
|
5701
|
+
if (this.#retryAbortController !== retryAbortController) {
|
|
5702
|
+
return false;
|
|
5703
|
+
}
|
|
5620
5704
|
// Aborted during sleep - emit end event so UI can clean up
|
|
5621
5705
|
const attempt = this.#retryAttempt;
|
|
5622
5706
|
this.#retryAttempt = 0;
|
|
@@ -5630,7 +5714,9 @@ export class AgentSession {
|
|
|
5630
5714
|
this.#resolveRetry();
|
|
5631
5715
|
return false;
|
|
5632
5716
|
}
|
|
5633
|
-
this.#retryAbortController
|
|
5717
|
+
if (this.#retryAbortController === retryAbortController) {
|
|
5718
|
+
this.#retryAbortController = undefined;
|
|
5719
|
+
}
|
|
5634
5720
|
|
|
5635
5721
|
// Retry via continue() outside the agent_end event callback chain.
|
|
5636
5722
|
this.#scheduleAgentContinue({ delayMs: 1, generation });
|
|
@@ -5722,12 +5808,13 @@ export class AgentSession {
|
|
|
5722
5808
|
}
|
|
5723
5809
|
}
|
|
5724
5810
|
|
|
5725
|
-
|
|
5811
|
+
const abortController = new AbortController();
|
|
5812
|
+
this.#bashAbortControllers.add(abortController);
|
|
5726
5813
|
|
|
5727
5814
|
try {
|
|
5728
5815
|
const result = await executeBashCommand(command, {
|
|
5729
5816
|
onChunk,
|
|
5730
|
-
signal:
|
|
5817
|
+
signal: abortController.signal,
|
|
5731
5818
|
sessionKey: this.sessionId,
|
|
5732
5819
|
timeout: clampTimeout("bash") * 1000,
|
|
5733
5820
|
onMinimizedSave: originalText => this.#saveBashOriginalArtifact(originalText),
|
|
@@ -5736,7 +5823,7 @@ export class AgentSession {
|
|
|
5736
5823
|
this.recordBashResult(command, result, options);
|
|
5737
5824
|
return result;
|
|
5738
5825
|
} finally {
|
|
5739
|
-
this.#
|
|
5826
|
+
this.#bashAbortControllers.delete(abortController);
|
|
5740
5827
|
}
|
|
5741
5828
|
}
|
|
5742
5829
|
|
|
@@ -5775,12 +5862,14 @@ export class AgentSession {
|
|
|
5775
5862
|
* Cancel running bash command.
|
|
5776
5863
|
*/
|
|
5777
5864
|
abortBash(): void {
|
|
5778
|
-
this.#
|
|
5865
|
+
for (const abortController of this.#bashAbortControllers) {
|
|
5866
|
+
abortController.abort();
|
|
5867
|
+
}
|
|
5779
5868
|
}
|
|
5780
5869
|
|
|
5781
5870
|
/** Whether a bash command is currently running */
|
|
5782
5871
|
get isBashRunning(): boolean {
|
|
5783
|
-
return this.#
|
|
5872
|
+
return this.#bashAbortControllers.size > 0;
|
|
5784
5873
|
}
|
|
5785
5874
|
|
|
5786
5875
|
/** Whether there are pending bash messages waiting to be flushed */
|
|
@@ -6518,6 +6607,8 @@ export class AgentSession {
|
|
|
6518
6607
|
cancelled: boolean;
|
|
6519
6608
|
aborted?: boolean;
|
|
6520
6609
|
summaryEntry?: BranchSummaryEntry;
|
|
6610
|
+
/** Raw session context built during navigation — pass to renderInitialMessages to skip a second O(N) walk. */
|
|
6611
|
+
sessionContext?: SessionContext;
|
|
6521
6612
|
}> {
|
|
6522
6613
|
const oldLeafId = this.sessionManager.getLeafId();
|
|
6523
6614
|
|
|
@@ -6647,15 +6738,20 @@ export class AgentSession {
|
|
|
6647
6738
|
this.sessionManager.branch(newLeafId);
|
|
6648
6739
|
}
|
|
6649
6740
|
|
|
6650
|
-
// Update agent state
|
|
6651
|
-
const
|
|
6652
|
-
|
|
6653
|
-
this
|
|
6741
|
+
// Update agent state — build display context to populate agent messages.
|
|
6742
|
+
const stateContext = this.sessionManager.buildSessionContext();
|
|
6743
|
+
const displayContext = deobfuscateSessionContext(stateContext, this.#obfuscator);
|
|
6744
|
+
await this.#restoreMCPSelectionsForSessionContext(displayContext);
|
|
6745
|
+
this.agent.replaceMessages(displayContext.messages);
|
|
6654
6746
|
this.#syncTodoPhasesFromBranch();
|
|
6655
6747
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6656
6748
|
|
|
6657
|
-
|
|
6658
|
-
|
|
6749
|
+
this.#branchSummaryAbortController = undefined;
|
|
6750
|
+
|
|
6751
|
+
// Emit session_tree event; only handlers can mutate session entries, so skip
|
|
6752
|
+
// the emit and the context rebuild when no handlers are registered (mirrors
|
|
6753
|
+
// the session_before_tree guard above).
|
|
6754
|
+
if (this.#extensionRunner?.hasHandlers("session_tree")) {
|
|
6659
6755
|
await this.#extensionRunner.emit({
|
|
6660
6756
|
type: "session_tree",
|
|
6661
6757
|
newLeafId: this.sessionManager.getLeafId(),
|
|
@@ -6663,10 +6759,10 @@ export class AgentSession {
|
|
|
6663
6759
|
summaryEntry,
|
|
6664
6760
|
fromExtension: summaryText ? fromExtension : undefined,
|
|
6665
6761
|
});
|
|
6762
|
+
const rawContext = this.sessionManager.buildSessionContext();
|
|
6763
|
+
return { editorText, cancelled: false, summaryEntry, sessionContext: rawContext };
|
|
6666
6764
|
}
|
|
6667
|
-
|
|
6668
|
-
this.#branchSummaryAbortController = undefined;
|
|
6669
|
-
return { editorText, cancelled: false, summaryEntry };
|
|
6765
|
+
return { editorText, cancelled: false, summaryEntry, sessionContext: stateContext };
|
|
6670
6766
|
}
|
|
6671
6767
|
|
|
6672
6768
|
/**
|