@oh-my-pi/pi-coding-agent 15.5.7 → 15.5.9
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 +53 -1
- package/dist/types/cli/auth-gateway-cli.d.ts +8 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +10 -10
- package/dist/types/edit/file-snapshot-store.d.ts +9 -6
- package/dist/types/edit/hashline/diff.d.ts +4 -5
- package/dist/types/edit/streaming.d.ts +2 -1
- package/dist/types/eval/py/index.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/vault-protocol.d.ts +93 -0
- package/dist/types/mcp/transports/http.d.ts +9 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +3 -1
- package/dist/types/tools/match-line-format.d.ts +2 -2
- package/dist/types/tools/render-utils.d.ts +3 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/file-mentions.d.ts +2 -0
- package/package.json +8 -8
- package/src/cli/args.ts +2 -0
- package/src/cli/auth-broker-cli.ts +2 -1
- package/src/cli/auth-gateway-cli.ts +210 -9
- package/src/commands/auth-gateway.ts +7 -1
- package/src/config/settings-schema.ts +12 -11
- package/src/edit/file-snapshot-store.ts +9 -6
- package/src/edit/hashline/diff.ts +26 -13
- package/src/edit/hashline/execute.ts +13 -9
- package/src/edit/renderer.ts +9 -9
- package/src/edit/streaming.ts +4 -6
- package/src/eval/py/index.ts +1 -1
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/shared-events.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +2 -0
- package/src/internal-urls/vault-protocol.ts +936 -0
- package/src/main.ts +1 -2
- package/src/mcp/transports/http.ts +29 -2
- package/src/modes/components/tool-execution.ts +6 -4
- package/src/modes/controllers/event-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +10 -2
- package/src/modes/utils/ui-helpers.ts +2 -1
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/read.md +3 -3
- package/src/prompts/tools/search.md +1 -1
- package/src/sdk.ts +26 -1
- package/src/session/agent-session.ts +82 -11
- package/src/system-prompt.ts +2 -0
- package/src/tools/ast-edit.ts +10 -7
- package/src/tools/ast-grep.ts +12 -11
- package/src/tools/eval.ts +28 -3
- package/src/tools/match-line-format.ts +2 -2
- package/src/tools/path-utils.ts +2 -0
- package/src/tools/plan-mode-guard.ts +6 -1
- package/src/tools/read.ts +70 -55
- package/src/tools/render-utils.ts +15 -0
- package/src/tools/search.ts +12 -12
- package/src/tools/write.ts +61 -6
- package/src/utils/file-mentions.ts +11 -5
- package/src/web/search/providers/codex.ts +2 -1
package/src/main.ts
CHANGED
|
@@ -9,7 +9,6 @@ import * as fs from "node:fs/promises";
|
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { createInterface } from "node:readline/promises";
|
|
12
|
-
import { keepaliveWhile } from "@oh-my-pi/pi-agent-core";
|
|
13
12
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
14
13
|
import {
|
|
15
14
|
$env,
|
|
@@ -316,7 +315,7 @@ async function runInteractiveMode(
|
|
|
316
315
|
}
|
|
317
316
|
|
|
318
317
|
while (true) {
|
|
319
|
-
const input = await
|
|
318
|
+
const input = await mode.getUserInput();
|
|
320
319
|
await submitInteractiveInput(mode, session, input);
|
|
321
320
|
}
|
|
322
321
|
}
|
|
@@ -16,8 +16,23 @@ import type {
|
|
|
16
16
|
MCPTransport,
|
|
17
17
|
} from "../../mcp/types";
|
|
18
18
|
import { toJsonRpcError } from "../../mcp/types";
|
|
19
|
-
import { createMCPTimeout, getNeverAbortSignal, resolveMCPTimeoutMs } from "../timeout";
|
|
19
|
+
import { createMCPTimeout, getNeverAbortSignal, isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "../timeout";
|
|
20
20
|
|
|
21
|
+
const HTTP_SSE_CONNECT_TIMEOUT_MS = 1_000;
|
|
22
|
+
/**
|
|
23
|
+
* Best-effort startup deadline for the optional Streamable HTTP GET SSE listener.
|
|
24
|
+
*
|
|
25
|
+
* Returns `0` (disabled) when the operator has explicitly disabled MCP client-side
|
|
26
|
+
* timeouts via `timeout: 0` or `OMP_MCP_TIMEOUT_MS=0`, mirroring the rest of the
|
|
27
|
+
* MCP timeout surface. Otherwise caps the wait at one second and scales below
|
|
28
|
+
* short request timeouts so connect-time never exceeds the request budget.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveSSEConnectTimeoutMs(configTimeout?: number): number {
|
|
31
|
+
const requestTimeout = resolveMCPTimeoutMs(configTimeout);
|
|
32
|
+
if (!isMCPTimeoutEnabled(requestTimeout)) return 0;
|
|
33
|
+
const boundedTimeout = Math.min(HTTP_SSE_CONNECT_TIMEOUT_MS, Math.floor(requestTimeout / 4));
|
|
34
|
+
return Math.max(1, boundedTimeout);
|
|
35
|
+
}
|
|
21
36
|
/**
|
|
22
37
|
* HTTP transport for MCP servers.
|
|
23
38
|
* Uses POST for requests, supports SSE responses.
|
|
@@ -73,6 +88,15 @@ export class HttpTransport implements MCPTransport {
|
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
let response: Response;
|
|
91
|
+
let timedOut = false;
|
|
92
|
+
const startupTimeoutMs = resolveSSEConnectTimeoutMs(this.config.timeout);
|
|
93
|
+
const timeoutId =
|
|
94
|
+
startupTimeoutMs > 0
|
|
95
|
+
? setTimeout(() => {
|
|
96
|
+
timedOut = true;
|
|
97
|
+
this.#sseConnection?.abort();
|
|
98
|
+
}, startupTimeoutMs)
|
|
99
|
+
: null;
|
|
76
100
|
try {
|
|
77
101
|
response = await fetch(this.config.url, {
|
|
78
102
|
method: "GET",
|
|
@@ -81,13 +105,16 @@ export class HttpTransport implements MCPTransport {
|
|
|
81
105
|
});
|
|
82
106
|
} catch (error) {
|
|
83
107
|
this.#sseConnection = null;
|
|
84
|
-
if (error instanceof Error && error.name !== "AbortError") {
|
|
108
|
+
if (error instanceof Error && error.name !== "AbortError" && !timedOut) {
|
|
85
109
|
this.onError?.(error);
|
|
86
110
|
}
|
|
87
111
|
return;
|
|
112
|
+
} finally {
|
|
113
|
+
if (timeoutId !== null) clearTimeout(timeoutId);
|
|
88
114
|
}
|
|
89
115
|
|
|
90
116
|
if (response.status === 405 || !response.ok || !response.body) {
|
|
117
|
+
await response.body?.cancel();
|
|
91
118
|
this.#sseConnection = null;
|
|
92
119
|
return;
|
|
93
120
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SnapshotStore } from "@oh-my-pi/hashline";
|
|
1
2
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
3
|
import {
|
|
3
4
|
Box,
|
|
@@ -105,10 +106,10 @@ function resolveEditModeForTool(toolName: string, tool: AgentTool | undefined):
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
export interface ToolExecutionOptions {
|
|
109
|
+
snapshots?: SnapshotStore;
|
|
108
110
|
showImages?: boolean; // default: true (only used if terminal supports images)
|
|
109
111
|
editFuzzyThreshold?: number;
|
|
110
112
|
editAllowFuzzy?: boolean;
|
|
111
|
-
hashlineAutoDropPureInsertDuplicates?: boolean;
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
export interface ToolExecutionHandle {
|
|
@@ -142,7 +143,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
142
143
|
#showImages: boolean;
|
|
143
144
|
#editFuzzyThreshold: number | undefined;
|
|
144
145
|
#editAllowFuzzy: boolean | undefined;
|
|
145
|
-
#
|
|
146
|
+
#snapshots?: SnapshotStore;
|
|
146
147
|
#isPartial = true;
|
|
147
148
|
#tool?: AgentTool;
|
|
148
149
|
#ui: TUI;
|
|
@@ -189,7 +190,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
189
190
|
this.#showImages = options.showImages ?? true;
|
|
190
191
|
this.#editFuzzyThreshold = options.editFuzzyThreshold;
|
|
191
192
|
this.#editAllowFuzzy = options.editAllowFuzzy;
|
|
192
|
-
this.#
|
|
193
|
+
this.#snapshots = options.snapshots;
|
|
193
194
|
this.#tool = tool;
|
|
194
195
|
this.#ui = ui;
|
|
195
196
|
this.#cwd = cwd;
|
|
@@ -266,12 +267,13 @@ export class ToolExecutionComponent extends Container {
|
|
|
266
267
|
|
|
267
268
|
try {
|
|
268
269
|
const isStreaming = !this.#argsComplete;
|
|
270
|
+
if (editMode === "hashline" && !this.#snapshots) return;
|
|
269
271
|
const previews = await strategy.computeDiffPreview(effectiveArgs, {
|
|
270
272
|
cwd: this.#cwd,
|
|
271
273
|
signal: controller.signal,
|
|
274
|
+
snapshots: this.#snapshots!,
|
|
272
275
|
fuzzyThreshold: this.#editFuzzyThreshold,
|
|
273
276
|
allowFuzzy: this.#editAllowFuzzy,
|
|
274
|
-
hashlineAutoDropPureInsertDuplicates: this.#hashlineAutoDropPureInsertDuplicates,
|
|
275
277
|
isStreaming,
|
|
276
278
|
});
|
|
277
279
|
if (controller.signal.aborted) return;
|
|
@@ -3,6 +3,7 @@ import { calculatePromptTokens } from "@oh-my-pi/pi-agent-core/compaction/compac
|
|
|
3
3
|
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { type Component, Loader, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { settings } from "../../config/settings";
|
|
6
|
+
import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
|
|
6
7
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
7
8
|
import {
|
|
8
9
|
ReadToolGroupComponent,
|
|
@@ -329,10 +330,10 @@ export class EventController {
|
|
|
329
330
|
content.name,
|
|
330
331
|
renderArgs,
|
|
331
332
|
{
|
|
333
|
+
snapshots: getFileSnapshotStore(this.ctx.session),
|
|
332
334
|
showImages: settings.get("terminal.showImages"),
|
|
333
335
|
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
334
336
|
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
335
|
-
hashlineAutoDropPureInsertDuplicates: settings.get("edit.hashlineAutoDropPureInsertDuplicates"),
|
|
336
337
|
},
|
|
337
338
|
tool,
|
|
338
339
|
this.ctx.ui,
|
|
@@ -444,10 +445,10 @@ export class EventController {
|
|
|
444
445
|
event.toolName,
|
|
445
446
|
event.args,
|
|
446
447
|
{
|
|
448
|
+
snapshots: getFileSnapshotStore(this.ctx.session),
|
|
447
449
|
showImages: settings.get("terminal.showImages"),
|
|
448
450
|
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
449
451
|
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
450
|
-
hashlineAutoDropPureInsertDuplicates: settings.get("edit.hashlineAutoDropPureInsertDuplicates"),
|
|
451
452
|
},
|
|
452
453
|
tool,
|
|
453
454
|
this.ctx.ui,
|
|
@@ -598,7 +599,13 @@ export class EventController {
|
|
|
598
599
|
};
|
|
599
600
|
this.ctx.statusContainer.clear();
|
|
600
601
|
const reasonText =
|
|
601
|
-
event.reason === "overflow"
|
|
602
|
+
event.reason === "overflow"
|
|
603
|
+
? "Context overflow detected, "
|
|
604
|
+
: event.reason === "incomplete"
|
|
605
|
+
? "Response incomplete, "
|
|
606
|
+
: event.reason === "idle"
|
|
607
|
+
? "Idle "
|
|
608
|
+
: "";
|
|
602
609
|
const actionLabel = event.action === "handoff" ? "Auto-handoff" : "Auto context-full maintenance";
|
|
603
610
|
this.ctx.autoCompactionLoader = new Loader(
|
|
604
611
|
this.ctx.ui,
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from "node:fs/promises";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type Agent,
|
|
9
|
+
type AgentMessage,
|
|
10
|
+
type AgentToolResult,
|
|
11
|
+
EventLoopKeepalive,
|
|
12
|
+
ThinkingLevel,
|
|
13
|
+
} from "@oh-my-pi/pi-agent-core";
|
|
8
14
|
import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
|
|
9
15
|
import {
|
|
10
16
|
type AssistantMessage,
|
|
@@ -619,7 +625,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
619
625
|
};
|
|
620
626
|
this.#scheduleLoopAutoSubmit();
|
|
621
627
|
this.#scheduleGoalContinuation();
|
|
622
|
-
|
|
628
|
+
|
|
629
|
+
using _ = new EventLoopKeepalive();
|
|
630
|
+
return await promise;
|
|
623
631
|
}
|
|
624
632
|
|
|
625
633
|
#scheduleLoopAutoSubmit(): void {
|
|
@@ -2,6 +2,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
|
2
2
|
import type { AssistantMessage, ImageContent, Message } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { type Component, Spacer, Text, TruncatedText } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { settings } from "../../config/settings";
|
|
5
|
+
import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
|
|
5
6
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
6
7
|
import { BashExecutionComponent } from "../../modes/components/bash-execution";
|
|
7
8
|
import { BranchSummaryMessageComponent } from "../../modes/components/branch-summary-message";
|
|
@@ -377,10 +378,10 @@ export class UiHelpers {
|
|
|
377
378
|
content.name,
|
|
378
379
|
renderArgs,
|
|
379
380
|
{
|
|
381
|
+
snapshots: getFileSnapshotStore(this.ctx.session),
|
|
380
382
|
showImages: settings.get("terminal.showImages"),
|
|
381
383
|
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
382
384
|
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
383
|
-
hashlineAutoDropPureInsertDuplicates: settings.get("edit.hashlineAutoDropPureInsertDuplicates"),
|
|
384
385
|
},
|
|
385
386
|
tool,
|
|
386
387
|
this.ctx.ui,
|
|
@@ -59,6 +59,9 @@ With most FS/bash-like tools, static references to them will automatically resol
|
|
|
59
59
|
- `/<path>`: JSON field extraction
|
|
60
60
|
- `artifact://<id>`: Artifact content
|
|
61
61
|
- `local://<name>.md`: Plan artifacts and shared content with subagents
|
|
62
|
+
{{#if hasObsidian}}
|
|
63
|
+
- `vault://<vault>/<path>`: Obsidian vault content (read/edit). `vault://` lists vaults; `vault://_/…` targets the active vault. File-scoped `?op=outline|backlinks|links|tags|properties|tasks|base|…`; vault-scoped `?op=search&q=…|daily|tasks|orphans|unresolved|bases|…`.
|
|
64
|
+
{{/if}}
|
|
62
65
|
- `mcp://<uri>`: MCP resource
|
|
63
66
|
- `issue://<N>` (or `issue://<owner>/<repo>/<N>`): GitHub issue view; cached on disk so re-reads are free. Bare `issue://` (or `issue://<owner>/<repo>`) lists recent issues; supports `?state=open|closed|all&limit=&author=&label=`.
|
|
64
67
|
- `pr://<N>` (or `pr://<owner>/<repo>/<N>`): GitHub PR view; same cache. Append `?comments=0` to drop the comments section. Bare `pr://` (or `pr://<owner>/<repo>`) lists recent PRs; supports `?state=open|closed|merged|all&limit=&author=&label=`.
|
|
@@ -14,7 +14,7 @@ Performs structural AST-aware rewrites via native ast-grep.
|
|
|
14
14
|
</instruction>
|
|
15
15
|
|
|
16
16
|
<output>
|
|
17
|
-
- Replacement summary, per-file replacement counts, and change diffs as `¶src/foo.ts#
|
|
17
|
+
- Replacement summary, per-file replacement counts, and change diffs as `¶src/foo.ts#0a`, `-12:before`, `+12:after` lines in hashline mode
|
|
18
18
|
- Parse issues when files cannot be processed
|
|
19
19
|
</output>
|
|
20
20
|
|
|
@@ -18,7 +18,7 @@ Performs structural code search using AST matching via native ast-grep.
|
|
|
18
18
|
|
|
19
19
|
<output>
|
|
20
20
|
- Grouped matches with file path, byte range, line/column ranges, metavariable captures
|
|
21
|
-
- Match lines are numbered under a file
|
|
21
|
+
- Match lines are numbered under a file snapshot tag header in hashline mode: `¶src/foo.ts#0a`, `*42:content` for the matched line, ` 43:content` for context
|
|
22
22
|
- Summary counts (`totalMatches`, `filesWithMatches`, `filesSearched`) and parse issues when present
|
|
23
23
|
</output>
|
|
24
24
|
|
|
@@ -8,7 +8,7 @@ Read files, directories, archives, SQLite databases, images, documents, internal
|
|
|
8
8
|
|
|
9
9
|
## Parameters
|
|
10
10
|
|
|
11
|
-
- `path` — required. Local path, internal URI (`skill://`, `agent://`, `artifact://`, `memory://`, `rule://`, `local://`, `mcp://`), or URL. Append `:<sel>` for line ranges, raw mode, or special modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).
|
|
11
|
+
- `path` — required. Local path, internal URI (`skill://`, `agent://`, `artifact://`, `memory://`, `rule://`, `local://`, `vault://`, `mcp://`), or URL. Append `:<sel>` for line ranges, raw mode, or special modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).
|
|
12
12
|
|
|
13
13
|
## Selectors
|
|
14
14
|
|
|
@@ -28,7 +28,7 @@ Append `:<sel>` to `path`. The bare path falls back to the default mode.
|
|
|
28
28
|
|
|
29
29
|
- Reading a directory path returns a depth-limited dirent listing.
|
|
30
30
|
{{#if IS_HL_MODE}}
|
|
31
|
-
- Reading a file with an explicit selector emits a file
|
|
31
|
+
- Reading a file with an explicit selector emits a file snapshot tag header and numbered lines: `¶src/foo.ts#0a` then `41:def alpha():`. Copy the `¶PATH#TAG` header for anchored edits; ops use bare line numbers. NEVER fabricate the tag.
|
|
32
32
|
{{else}}
|
|
33
33
|
{{#if IS_LINE_NUMBER_MODE}}
|
|
34
34
|
- Reading a file with an explicit selector returns lines prefixed with line numbers: `41|def alpha():`.
|
|
@@ -70,7 +70,7 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
|
|
|
70
70
|
|
|
71
71
|
# Internal URIs
|
|
72
72
|
|
|
73
|
-
`skill://<name>`, `agent://<id>`, `artifact://<id>`, `memory://root`, `rule://<name>`, `local://<name>.md`, `mcp://<uri>` resolve transparently and accept the same line selectors as filesystem paths. Use `artifact://<id>` to recover full output that a previous bash/eval/tool result spilled or truncated.
|
|
73
|
+
`skill://<name>`, `agent://<id>`, `artifact://<id>`, `memory://root`, `rule://<name>`, `local://<name>.md`, `vault://<vault>/<path>`, `mcp://<uri>` resolve transparently and accept the same line selectors as filesystem paths. Use `artifact://<id>` to recover full output that a previous bash/eval/tool result spilled or truncated.
|
|
74
74
|
|
|
75
75
|
<critical>
|
|
76
76
|
- You MUST use `read` for every file, directory, archive, and URL inspection. `cat`, `head`, `tail`, `less`, `more`, `ls`, `tar`, `unzip`, `curl`, `wget` are FORBIDDEN — any such bash call is a bug, regardless of how short or convenient it looks.
|
|
@@ -9,7 +9,7 @@ Searches files using powerful regex matching.
|
|
|
9
9
|
|
|
10
10
|
<output>
|
|
11
11
|
{{#if IS_HL_MODE}}
|
|
12
|
-
- Text output emits a file
|
|
12
|
+
- Text output emits a file snapshot tag header per matched file plus numbered lines: `¶src/login.ts#1f`, `*42:if (user.id) {` (match), ` 43:return user;` (context). Copy the header for anchored edits; ops use bare line numbers.
|
|
13
13
|
{{else}}
|
|
14
14
|
{{#if IS_LINE_NUMBER_MODE}}
|
|
15
15
|
- Text output is line-number-prefixed
|
package/src/sdk.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "@oh-my-pi/pi-agent-core";
|
|
11
11
|
import {
|
|
12
12
|
type CredentialDisabledEvent,
|
|
13
|
+
isUsageLimitError,
|
|
13
14
|
type Message,
|
|
14
15
|
type Model,
|
|
15
16
|
type SimpleStreamOptions,
|
|
@@ -23,6 +24,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
23
24
|
import {
|
|
24
25
|
$env,
|
|
25
26
|
$flag,
|
|
27
|
+
extractRetryHint,
|
|
26
28
|
getAgentDbPath,
|
|
27
29
|
getAgentDir,
|
|
28
30
|
getProjectDir,
|
|
@@ -1885,13 +1887,36 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1885
1887
|
...streamOptions,
|
|
1886
1888
|
openrouterVariant: streamOptions?.openrouterVariant ?? openrouterVariant,
|
|
1887
1889
|
onAuthError: async (provider, oldKey, error) => {
|
|
1890
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1891
|
+
// streamSimple invokes this for both 401 auth failures AND
|
|
1892
|
+
// rotatable usage-limit errors (Codex usage_limit_reached,
|
|
1893
|
+
// Anthropic usage_limit_reached, etc.). The two need
|
|
1894
|
+
// different storage actions: a real 401 means the credential
|
|
1895
|
+
// is bad and should be marked suspect; a usage limit just
|
|
1896
|
+
// means this account is parked until reset and should be
|
|
1897
|
+
// temporarily blocked so a sibling can pick the request up.
|
|
1898
|
+
if (isUsageLimitError(message)) {
|
|
1899
|
+
const retryAfterMs = extractRetryHint(undefined, message);
|
|
1900
|
+
const switched = await modelRegistry.authStorage.markUsageLimitReached(provider, agent.sessionId, {
|
|
1901
|
+
retryAfterMs,
|
|
1902
|
+
signal: streamOptions?.signal,
|
|
1903
|
+
});
|
|
1904
|
+
logger.debug("Retrying provider request after usage-limit block", {
|
|
1905
|
+
provider,
|
|
1906
|
+
switched,
|
|
1907
|
+
retryAfterMs,
|
|
1908
|
+
error: message,
|
|
1909
|
+
});
|
|
1910
|
+
if (!switched) return undefined;
|
|
1911
|
+
return modelRegistry.getApiKeyForProvider(provider, agent.sessionId);
|
|
1912
|
+
}
|
|
1888
1913
|
await modelRegistry.authStorage.invalidateCredentialMatching(provider, oldKey, {
|
|
1889
1914
|
signal: streamOptions?.signal,
|
|
1890
1915
|
sessionId: agent.sessionId,
|
|
1891
1916
|
});
|
|
1892
1917
|
logger.debug("Retrying provider request after credential invalidation", {
|
|
1893
1918
|
provider,
|
|
1894
|
-
error:
|
|
1919
|
+
error: message,
|
|
1895
1920
|
});
|
|
1896
1921
|
return modelRegistry.getApiKeyForProvider(provider, agent.sessionId);
|
|
1897
1922
|
},
|
|
@@ -18,6 +18,7 @@ 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 { isPromise } from "node:util/types";
|
|
21
|
+
import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
|
21
22
|
import {
|
|
22
23
|
type AfterToolCallContext,
|
|
23
24
|
type AfterToolCallResult,
|
|
@@ -104,6 +105,8 @@ import { onAppendOnlyModeChanged } from "../config/settings";
|
|
|
104
105
|
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
105
106
|
import { loadCapability } from "../discovery";
|
|
106
107
|
import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
108
|
+
import { getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
109
|
+
import { namespaceSessionId as namespacePythonSessionId } from "../eval/py";
|
|
107
110
|
import {
|
|
108
111
|
disposeKernelSessionsByOwner,
|
|
109
112
|
executePython as executePythonCommand,
|
|
@@ -209,7 +212,11 @@ import { YieldQueue } from "./yield-queue";
|
|
|
209
212
|
/** Session-specific events that extend the core AgentEvent */
|
|
210
213
|
export type AgentSessionEvent =
|
|
211
214
|
| AgentEvent
|
|
212
|
-
| {
|
|
215
|
+
| {
|
|
216
|
+
type: "auto_compaction_start";
|
|
217
|
+
reason: "threshold" | "overflow" | "idle" | "incomplete";
|
|
218
|
+
action: "context-full" | "handoff";
|
|
219
|
+
}
|
|
213
220
|
| {
|
|
214
221
|
type: "auto_compaction_end";
|
|
215
222
|
action: "context-full" | "handoff";
|
|
@@ -738,6 +745,7 @@ export class AgentSession {
|
|
|
738
745
|
readonly sessionManager: SessionManager;
|
|
739
746
|
readonly settings: Settings;
|
|
740
747
|
readonly yieldQueue: YieldQueue;
|
|
748
|
+
fileSnapshotStore?: InMemorySnapshotStore;
|
|
741
749
|
|
|
742
750
|
#powerAssertion: MacOSPowerAssertion | undefined;
|
|
743
751
|
|
|
@@ -4156,6 +4164,7 @@ export class AgentSession {
|
|
|
4156
4164
|
const fileMentionMessages = await generateFileMentionMessages(fileMentions, this.sessionManager.getCwd(), {
|
|
4157
4165
|
autoResizeImages: this.settings.get("images.autoResize"),
|
|
4158
4166
|
useHashLines: resolveFileDisplayMode(this).hashLines,
|
|
4167
|
+
snapshotStore: getFileSnapshotStore(this),
|
|
4159
4168
|
});
|
|
4160
4169
|
messages.push(...fileMentionMessages);
|
|
4161
4170
|
}
|
|
@@ -5662,10 +5671,14 @@ export class AgentSession {
|
|
|
5662
5671
|
* Check if context maintenance or promotion is needed and run it.
|
|
5663
5672
|
* Called after agent_end and before prompt submission.
|
|
5664
5673
|
*
|
|
5665
|
-
*
|
|
5666
|
-
* 1.
|
|
5667
|
-
* 2.
|
|
5668
|
-
* 3.
|
|
5674
|
+
* Four cases (in order):
|
|
5675
|
+
* 1. Input overflow + promotion: promote to larger model, retry without maintenance.
|
|
5676
|
+
* 2. Input overflow + no promotion target: run context maintenance, auto-retry on same model.
|
|
5677
|
+
* 3. Output incomplete (stopReason === "length", e.g. `response.incomplete`): the
|
|
5678
|
+
* model burned its output budget without producing an actionable deliverable
|
|
5679
|
+
* (reasoning-only or truncated). Drop the dead turn, try promotion, otherwise
|
|
5680
|
+
* run compaction/handoff and retry.
|
|
5681
|
+
* 4. Threshold: context over threshold, run context maintenance (no auto-retry).
|
|
5669
5682
|
*
|
|
5670
5683
|
* @param assistantMessage The assistant message to check
|
|
5671
5684
|
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
@@ -5724,10 +5737,49 @@ export class AgentSession {
|
|
|
5724
5737
|
}
|
|
5725
5738
|
return false;
|
|
5726
5739
|
}
|
|
5740
|
+
|
|
5741
|
+
// Case 3: Output-side incomplete — `response.incomplete` from OpenAI Responses
|
|
5742
|
+
// (and Codex) maps to stopReason === "length". The model burned its
|
|
5743
|
+
// `max_output_tokens` budget on reasoning/text and emitted no actionable
|
|
5744
|
+
// deliverable. Same recovery class as overflow: promotion if available,
|
|
5745
|
+
// otherwise compaction/handoff. Unlike overflow, the *input* is fine, so we
|
|
5746
|
+
// allow the handoff strategy to actually run.
|
|
5747
|
+
if (sameModel && !errorIsFromBeforeCompaction && assistantMessage.stopReason === "length") {
|
|
5748
|
+
const messages = this.agent.state.messages;
|
|
5749
|
+
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
5750
|
+
this.agent.replaceMessages(messages.slice(0, -1));
|
|
5751
|
+
}
|
|
5752
|
+
|
|
5753
|
+
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
5754
|
+
if (promoted) {
|
|
5755
|
+
logger.debug("Context promotion triggered by response.incomplete (length stop)", {
|
|
5756
|
+
from: `${assistantMessage.provider}/${assistantMessage.model}`,
|
|
5757
|
+
});
|
|
5758
|
+
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
5759
|
+
return false;
|
|
5760
|
+
}
|
|
5761
|
+
|
|
5762
|
+
const incompleteCompactionSettings = this.settings.getGroup("compaction");
|
|
5763
|
+
if (incompleteCompactionSettings.enabled && incompleteCompactionSettings.strategy !== "off") {
|
|
5764
|
+
logger.debug("Compaction triggered by response.incomplete (length stop, no promotion target)", {
|
|
5765
|
+
model: `${assistantMessage.provider}/${assistantMessage.model}`,
|
|
5766
|
+
strategy: incompleteCompactionSettings.strategy,
|
|
5767
|
+
});
|
|
5768
|
+
await this.#runAutoCompaction("incomplete", true, false, allowDefer);
|
|
5769
|
+
} else {
|
|
5770
|
+
// Neither promotion nor compaction is available — surface the dead-end so
|
|
5771
|
+
// the user understands why the turn yielded with nothing.
|
|
5772
|
+
logger.warn("response.incomplete with no recovery path (promotion + compaction both unavailable)", {
|
|
5773
|
+
model: `${assistantMessage.provider}/${assistantMessage.model}`,
|
|
5774
|
+
});
|
|
5775
|
+
}
|
|
5776
|
+
return false;
|
|
5777
|
+
}
|
|
5778
|
+
|
|
5727
5779
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
5728
5780
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
|
|
5729
5781
|
|
|
5730
|
-
// Case
|
|
5782
|
+
// Case 4: Threshold - turn succeeded but context is getting large
|
|
5731
5783
|
// Skip if this was an error (non-overflow errors don't have usage data)
|
|
5732
5784
|
if (assistantMessage.stopReason === "error") return false;
|
|
5733
5785
|
const pruneResult = await this.#pruneToolOutputs();
|
|
@@ -6450,7 +6502,7 @@ export class AgentSession {
|
|
|
6450
6502
|
* @returns true when a deferred handoff was scheduled. Inline runs always return false.
|
|
6451
6503
|
*/
|
|
6452
6504
|
async #runAutoCompaction(
|
|
6453
|
-
reason: "overflow" | "threshold" | "idle",
|
|
6505
|
+
reason: "overflow" | "threshold" | "idle" | "incomplete",
|
|
6454
6506
|
willRetry: boolean,
|
|
6455
6507
|
deferred = false,
|
|
6456
6508
|
allowDefer = true,
|
|
@@ -6459,10 +6511,14 @@ export class AgentSession {
|
|
|
6459
6511
|
if (compactionSettings.strategy === "off") return false;
|
|
6460
6512
|
if (reason !== "idle" && !compactionSettings.enabled) return false;
|
|
6461
6513
|
const generation = this.#promptGeneration;
|
|
6514
|
+
// "overflow" and "incomplete" force inline execution because they are recovery
|
|
6515
|
+
// paths the caller wants resolved before scheduling the next turn. "idle" is
|
|
6516
|
+
// triggered by the idle loop and does its own scheduling.
|
|
6462
6517
|
if (
|
|
6463
6518
|
!deferred &&
|
|
6464
6519
|
allowDefer &&
|
|
6465
6520
|
reason !== "overflow" &&
|
|
6521
|
+
reason !== "incomplete" &&
|
|
6466
6522
|
reason !== "idle" &&
|
|
6467
6523
|
compactionSettings.strategy === "handoff"
|
|
6468
6524
|
) {
|
|
@@ -6477,6 +6533,9 @@ export class AgentSession {
|
|
|
6477
6533
|
return true;
|
|
6478
6534
|
}
|
|
6479
6535
|
|
|
6536
|
+
// "overflow" forces context-full because the input itself is broken — a handoff
|
|
6537
|
+
// LLM call would hit the same overflow. "incomplete" is an output-side problem,
|
|
6538
|
+
// so a handoff request on the existing context is still viable.
|
|
6480
6539
|
let action: "context-full" | "handoff" =
|
|
6481
6540
|
compactionSettings.strategy === "handoff" && reason !== "overflow" ? "handoff" : "context-full";
|
|
6482
6541
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
@@ -6777,8 +6836,18 @@ export class AgentSession {
|
|
|
6777
6836
|
if (willRetry) {
|
|
6778
6837
|
const messages = this.agent.state.messages;
|
|
6779
6838
|
const lastMsg = messages[messages.length - 1];
|
|
6780
|
-
if (lastMsg?.role === "assistant"
|
|
6781
|
-
|
|
6839
|
+
if (lastMsg?.role === "assistant") {
|
|
6840
|
+
const lastAssistant = lastMsg as AssistantMessage;
|
|
6841
|
+
// Drop the prior turn before retry when it carries no actionable deliverable:
|
|
6842
|
+
// - "error": failure was kept in history but must not re-enter the next turn's prompt.
|
|
6843
|
+
// - reason === "incomplete" && stopReason === "length": truncated output (typically
|
|
6844
|
+
// reasoning-only) — re-running it produces the same dead-end.
|
|
6845
|
+
const shouldDrop =
|
|
6846
|
+
lastAssistant.stopReason === "error" ||
|
|
6847
|
+
(reason === "incomplete" && lastAssistant.stopReason === "length");
|
|
6848
|
+
if (shouldDrop) {
|
|
6849
|
+
this.agent.replaceMessages(messages.slice(0, -1));
|
|
6850
|
+
}
|
|
6782
6851
|
}
|
|
6783
6852
|
|
|
6784
6853
|
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
@@ -6812,7 +6881,9 @@ export class AgentSession {
|
|
|
6812
6881
|
errorMessage:
|
|
6813
6882
|
reason === "overflow"
|
|
6814
6883
|
? `Context overflow recovery failed: ${errorMessage}`
|
|
6815
|
-
:
|
|
6884
|
+
: reason === "incomplete"
|
|
6885
|
+
? `Incomplete response recovery failed: ${errorMessage}`
|
|
6886
|
+
: `Auto-compaction failed: ${errorMessage}`,
|
|
6816
6887
|
});
|
|
6817
6888
|
} finally {
|
|
6818
6889
|
if (this.#autoCompactionAbortController === autoCompactionAbortController) {
|
|
@@ -7521,7 +7592,7 @@ export class AgentSession {
|
|
|
7521
7592
|
});
|
|
7522
7593
|
const result = await executePythonCommand(code, {
|
|
7523
7594
|
cwd,
|
|
7524
|
-
sessionId,
|
|
7595
|
+
sessionId: namespacePythonSessionId(sessionId),
|
|
7525
7596
|
kernelOwnerId: this.#evalKernelOwnerId,
|
|
7526
7597
|
kernelMode: this.settings.get("python.kernelMode"),
|
|
7527
7598
|
onChunk,
|
package/src/system-prompt.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { systemPromptCapability } from "./capability/system-prompt";
|
|
|
11
11
|
import type { SkillsSettings } from "./config/settings";
|
|
12
12
|
import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "./discovery";
|
|
13
13
|
import { loadSkills, type Skill } from "./extensibility/skills";
|
|
14
|
+
import { hasObsidian } from "./internal-urls/vault-protocol";
|
|
14
15
|
import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
|
|
15
16
|
import projectPromptTemplate from "./prompts/system/project-prompt.md" with { type: "text" };
|
|
16
17
|
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
|
|
@@ -569,6 +570,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
569
570
|
mcpDiscoveryServerSummaries,
|
|
570
571
|
eagerTasks,
|
|
571
572
|
secretsEnabled,
|
|
573
|
+
hasObsidian: hasObsidian(),
|
|
572
574
|
};
|
|
573
575
|
const rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
|
|
574
576
|
const systemPrompt = [rendered];
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { formatHashlineHeader } from "@oh-my-pi/hashline";
|
|
3
3
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
4
4
|
import { type AstReplaceChange, type AstReplaceFileChange, astEdit } from "@oh-my-pi/pi-natives";
|
|
5
5
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import * as z from "zod/v4";
|
|
9
|
+
import { getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
10
|
+
import { normalizeToLF } from "../edit/normalize";
|
|
9
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
12
|
import type { Theme } from "../modes/theme/theme";
|
|
11
13
|
import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
|
|
@@ -281,14 +283,15 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
281
283
|
}
|
|
282
284
|
|
|
283
285
|
const useHashLines = resolveFileDisplayMode(this.session).hashLines;
|
|
284
|
-
const hashContexts = new Map<string, {
|
|
286
|
+
const hashContexts = new Map<string, { tag: string }>();
|
|
285
287
|
if (useHashLines) {
|
|
288
|
+
const snapshotStore = getFileSnapshotStore(this.session);
|
|
286
289
|
for (const relativePath of fileList) {
|
|
287
290
|
const absolutePath = path.resolve(this.session.cwd, relativePath);
|
|
288
291
|
try {
|
|
289
|
-
const fullText = await Bun.file(absolutePath).text();
|
|
290
|
-
const
|
|
291
|
-
hashContexts.set(relativePath, {
|
|
292
|
+
const fullText = normalizeToLF(await Bun.file(absolutePath).text());
|
|
293
|
+
const tag = snapshotStore.recordContiguous(absolutePath, 1, fullText.split("\n"), { fullText });
|
|
294
|
+
hashContexts.set(relativePath, { tag });
|
|
292
295
|
} catch {
|
|
293
296
|
// Best-effort: if a file disappears between ast-edit and rendering, emit plain line output.
|
|
294
297
|
}
|
|
@@ -326,7 +329,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
326
329
|
const rendered = renderChangesForFile(relativePath);
|
|
327
330
|
const count = fileReplacementCounts.get(relativePath) ?? 0;
|
|
328
331
|
const hashContext = hashContexts.get(relativePath);
|
|
329
|
-
const hashSuffix = hashContext ? `#${hashContext.
|
|
332
|
+
const hashSuffix = hashContext ? `#${hashContext.tag}` : "";
|
|
330
333
|
return {
|
|
331
334
|
headerSuffix: `${hashSuffix} (${formatCount("replacement", count)})`,
|
|
332
335
|
modelLines: rendered.model,
|
|
@@ -346,7 +349,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
346
349
|
}
|
|
347
350
|
const hashContext = hashContexts.get(relativePath);
|
|
348
351
|
if (hashContext) {
|
|
349
|
-
outputLines.push(formatHashlineHeader(relativePath, hashContext.
|
|
352
|
+
outputLines.push(formatHashlineHeader(relativePath, hashContext.tag));
|
|
350
353
|
}
|
|
351
354
|
outputLines.push(...rendered.model);
|
|
352
355
|
displayLines.push(...rendered.display);
|