@oh-my-pi/pi-coding-agent 16.2.12 → 16.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +115 -7
- package/dist/cli.js +5996 -5945
- package/dist/types/advisor/config.d.ts +4 -2
- package/dist/types/collab/host.d.ts +16 -0
- package/dist/types/collab/protocol.d.ts +9 -3
- package/dist/types/commit/model-selection.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +12 -10
- package/dist/types/config/models-config-schema.d.ts +12 -0
- package/dist/types/config/models-config.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +28 -5
- package/dist/types/edit/modes/patch.d.ts +11 -0
- package/dist/types/eval/agent-bridge.d.ts +5 -1
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +86 -2
- package/dist/types/extensibility/skills.d.ts +2 -1
- package/dist/types/mnemopi/state.d.ts +12 -0
- package/dist/types/modes/components/agent-hub.d.ts +9 -5
- package/dist/types/modes/components/status-line/component.test.d.ts +1 -0
- package/dist/types/modes/components/usage-row.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +6 -0
- package/dist/types/modes/controllers/streaming-reveal.d.ts +17 -1
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +30 -3
- package/dist/types/modes/interactive-mode.d.ts +12 -7
- package/dist/types/modes/rpc/rpc-client.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +64 -1
- package/dist/types/modes/session-teardown.d.ts +63 -0
- package/dist/types/modes/session-teardown.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +9 -4
- package/dist/types/modes/types.d.ts +2 -3
- package/dist/types/modes/utils/copy-targets.d.ts +2 -0
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +1 -0
- package/dist/types/sdk.d.ts +9 -0
- package/dist/types/session/agent-session.d.ts +46 -13
- package/dist/types/session/exit-diagnostics.d.ts +48 -0
- package/dist/types/session/session-loader.d.ts +5 -0
- package/dist/types/task/executor.d.ts +12 -5
- package/dist/types/task/parallel.d.ts +1 -0
- package/dist/types/task/render.test.d.ts +1 -0
- package/dist/types/thinking.d.ts +2 -0
- package/dist/types/tools/checkpoint.d.ts +8 -0
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +11 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/irc.d.ts +1 -0
- package/dist/types/tools/output-meta.d.ts +7 -0
- package/dist/types/tools/output-schema-validator.d.ts +7 -4
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/render-utils.d.ts +4 -1
- package/dist/types/utils/git.d.ts +47 -2
- package/dist/types/web/search/provider.d.ts +7 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/config.ts +4 -2
- package/src/cli/bench-cli.ts +7 -2
- package/src/collab/guest.ts +94 -5
- package/src/collab/host.ts +80 -3
- package/src/collab/protocol.ts +9 -2
- package/src/commit/model-selection.ts +8 -2
- package/src/config/keybindings.ts +3 -1
- package/src/config/model-discovery.ts +66 -2
- package/src/config/model-registry.ts +7 -3
- package/src/config/model-resolver.ts +51 -23
- package/src/config/models-config-schema.ts +13 -0
- package/src/config/settings-schema.ts +44 -14
- package/src/edit/hashline/diff.ts +13 -2
- package/src/edit/index.ts +58 -8
- package/src/edit/modes/patch.ts +53 -18
- package/src/edit/streaming.ts +7 -6
- package/src/eval/__tests__/agent-bridge.test.ts +57 -1
- package/src/eval/agent-bridge.ts +15 -5
- package/src/exec/bash-executor.ts +7 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +534 -16
- package/src/extensibility/skills.ts +29 -6
- package/src/goals/guided-setup.ts +4 -3
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/client.ts +11 -14
- package/src/main.ts +38 -10
- package/src/mnemopi/state.ts +43 -7
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/agent-hub.ts +10 -2
- package/src/modes/components/agent-transcript-viewer.ts +39 -7
- package/src/modes/components/assistant-message.ts +16 -10
- package/src/modes/components/chat-transcript-builder.ts +11 -1
- package/src/modes/components/custom-editor.test.ts +20 -0
- package/src/modes/components/hook-selector.ts +44 -25
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/status-line/component.test.ts +44 -0
- package/src/modes/components/status-line/component.ts +9 -1
- package/src/modes/components/status-line/segments.ts +3 -3
- package/src/modes/components/usage-row.ts +16 -2
- package/src/modes/controllers/command-controller.ts +6 -21
- package/src/modes/controllers/event-controller.ts +11 -9
- package/src/modes/controllers/extension-ui-controller.ts +84 -3
- package/src/modes/controllers/input-controller.ts +16 -13
- package/src/modes/controllers/selector-controller.ts +3 -8
- package/src/modes/controllers/streaming-reveal.ts +75 -53
- package/src/modes/controllers/tool-args-reveal.ts +340 -10
- package/src/modes/interactive-mode.ts +122 -79
- package/src/modes/rpc/rpc-client.ts +21 -0
- package/src/modes/rpc/rpc-mode.ts +197 -46
- package/src/modes/session-teardown.test.ts +219 -0
- package/src/modes/session-teardown.ts +82 -0
- package/src/modes/setup-wizard/scenes/theme.ts +2 -2
- package/src/modes/skill-command.ts +7 -20
- package/src/modes/theme/theme.ts +29 -15
- package/src/modes/types.ts +2 -3
- package/src/modes/utils/copy-targets.ts +12 -0
- package/src/modes/utils/ui-helpers.ts +19 -2
- package/src/plan-mode/approved-plan-prompt.test.ts +36 -0
- package/src/prompts/advisor/system.md +1 -1
- package/src/prompts/agents/tester.md +6 -2
- package/src/prompts/skills/autoload.md +8 -0
- package/src/prompts/skills/user-invocation.md +11 -0
- package/src/prompts/steering/parent-irc.md +5 -0
- package/src/prompts/system/irc-incoming.md +2 -0
- package/src/prompts/system/mid-run-todo-nudge.md +3 -0
- package/src/prompts/system/plan-mode-approved.md +7 -10
- package/src/prompts/system/plan-mode-compact-instructions.md +2 -1
- package/src/prompts/system/plan-mode-reference.md +3 -4
- package/src/prompts/system/rewind-report.md +6 -0
- package/src/prompts/system/subagent-system-prompt.md +3 -0
- package/src/prompts/tools/irc.md +2 -1
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/rewind.md +3 -2
- package/src/prompts/tools/ssh.md +1 -0
- package/src/prompts/tools/task.md +4 -0
- package/src/sdk.ts +18 -4
- package/src/session/agent-session.ts +660 -114
- package/src/session/exit-diagnostics.ts +202 -0
- package/src/session/session-context.ts +25 -13
- package/src/session/session-loader.ts +58 -25
- package/src/session/session-manager.ts +0 -1
- package/src/session/session-persistence.ts +30 -4
- package/src/session/settings-stream-fn.ts +14 -0
- package/src/slash-commands/builtin-registry.ts +35 -3
- package/src/system-prompt.ts +15 -1
- package/src/task/executor.ts +31 -8
- package/src/task/index.ts +31 -9
- package/src/task/isolation-runner.ts +18 -2
- package/src/task/parallel.ts +7 -2
- package/src/task/render.test.ts +121 -0
- package/src/task/render.ts +48 -2
- package/src/task/worktree.ts +12 -13
- package/src/task/yield-assembly.ts +8 -5
- package/src/thinking.ts +5 -0
- package/src/tools/ask.ts +188 -9
- package/src/tools/ast-edit.ts +7 -0
- package/src/tools/ast-grep.ts +11 -0
- package/src/tools/bash.ts +6 -30
- package/src/tools/checkpoint.ts +15 -1
- package/src/tools/eval-render.ts +15 -0
- package/src/tools/fetch.ts +82 -18
- package/src/tools/gh.ts +1 -1
- package/src/tools/grep.ts +45 -27
- package/src/tools/index.ts +3 -1
- package/src/tools/irc.ts +24 -9
- package/src/tools/output-meta.ts +50 -0
- package/src/tools/output-schema-validator.ts +152 -15
- package/src/tools/path-utils.ts +55 -10
- package/src/tools/read.ts +1 -1
- package/src/tools/render-utils.ts +5 -3
- package/src/tools/ssh.ts +8 -1
- package/src/tools/yield.ts +41 -1
- package/src/utils/commit-message-generator.ts +2 -2
- package/src/utils/git.ts +271 -29
- package/src/utils/thinking-display.ts +13 -0
- package/src/web/search/index.ts +9 -28
- package/src/web/search/provider.ts +26 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/types.ts +5 -1
package/src/tools/ast-edit.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { Ellipsis, fileHyperlink, framedBlock, renderStatusLine, truncateToWidth
|
|
|
16
16
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
17
17
|
import type { ToolSession } from ".";
|
|
18
18
|
import { truncateForPrompt } from "./approval";
|
|
19
|
+
import { parseReadUrlTarget } from "./fetch";
|
|
19
20
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
20
21
|
import { classifyGroupedLines, formatGroupedFiles, groupLineIndicesByBlank } from "./grouped-file-output";
|
|
21
22
|
import type { OutputMeta } from "./output-meta";
|
|
@@ -284,6 +285,12 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
284
285
|
signal,
|
|
285
286
|
localProtocolOptions: this.session.localProtocolOptions,
|
|
286
287
|
skills: this.session.skills,
|
|
288
|
+
resolveExternalUrl: async rawPath => {
|
|
289
|
+
if (!parseReadUrlTarget(rawPath)) return undefined;
|
|
290
|
+
throw new ToolError(
|
|
291
|
+
`Cannot rewrite external URL: ${rawPath}. Use \`read\` or \`search\` to inspect fetched web content; ast_edit only applies to local files.`,
|
|
292
|
+
);
|
|
293
|
+
},
|
|
287
294
|
});
|
|
288
295
|
const { searchPath: resolvedSearchPath, scopePath, isDirectory, multiTargets, globFilter } = scope;
|
|
289
296
|
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -14,6 +14,7 @@ import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text
|
|
|
14
14
|
import { Ellipsis, fileHyperlink, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
15
15
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
16
16
|
import type { ToolSession } from ".";
|
|
17
|
+
import { materializeReadUrlToFile, parseReadUrlTarget } from "./fetch";
|
|
17
18
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
18
19
|
import { classifyGroupedLines, formatGroupedFiles, groupLineIndicesByBlank } from "./grouped-file-output";
|
|
19
20
|
import { formatMatchLine } from "./match-line-format";
|
|
@@ -208,6 +209,16 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
208
209
|
signal,
|
|
209
210
|
localProtocolOptions: this.session.localProtocolOptions,
|
|
210
211
|
skills: this.session.skills,
|
|
212
|
+
resolveExternalUrl: async rawPath => {
|
|
213
|
+
const target = parseReadUrlTarget(rawPath);
|
|
214
|
+
if (!target) return undefined;
|
|
215
|
+
const materialized = await materializeReadUrlToFile(
|
|
216
|
+
this.session,
|
|
217
|
+
{ path: target.path, raw: target.raw },
|
|
218
|
+
signal,
|
|
219
|
+
);
|
|
220
|
+
return { sourcePath: materialized.path, immutable: true };
|
|
221
|
+
},
|
|
211
222
|
});
|
|
212
223
|
const { searchPath: resolvedSearchPath, scopePath, isDirectory, multiTargets, globFilter } = scope;
|
|
213
224
|
|
package/src/tools/bash.ts
CHANGED
|
@@ -29,7 +29,12 @@ import { checkBashInterception } from "./bash-interceptor";
|
|
|
29
29
|
import { canUseInteractiveBashPty } from "./bash-pty-selection";
|
|
30
30
|
import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
|
|
31
31
|
import { invalidateGithubCacheForBashCommand } from "./gh-cache-invalidation";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
formatStyledTruncationWarning,
|
|
34
|
+
type OutputMeta,
|
|
35
|
+
stripOutputNotice,
|
|
36
|
+
stripRawOutputArtifactNotice,
|
|
37
|
+
} from "./output-meta";
|
|
33
38
|
import { resolveToCwd } from "./path-utils";
|
|
34
39
|
import { capPreviewLines, formatToolWorkingDirectory, previewWindowRows, replaceTabs } from "./render-utils";
|
|
35
40
|
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
@@ -292,35 +297,6 @@ function formatExitCodeNotice(exitCode: number): string {
|
|
|
292
297
|
return `Command exited with code ${exitCode}`;
|
|
293
298
|
}
|
|
294
299
|
|
|
295
|
-
const RAW_OUTPUT_ARTIFACT_PREFIX = "[raw output: artifact://";
|
|
296
|
-
const RAW_OUTPUT_ARTIFACT_SUFFIX = "]";
|
|
297
|
-
|
|
298
|
-
function stripRawOutputArtifactNotice(text: string): { text: string; artifactId?: string } {
|
|
299
|
-
const trimmed = text.trimEnd();
|
|
300
|
-
const lineStart = trimmed.lastIndexOf("\n");
|
|
301
|
-
const candidateStart = lineStart === -1 ? 0 : lineStart + 1;
|
|
302
|
-
if (
|
|
303
|
-
!trimmed.startsWith(RAW_OUTPUT_ARTIFACT_PREFIX, candidateStart) ||
|
|
304
|
-
!trimmed.endsWith(RAW_OUTPUT_ARTIFACT_SUFFIX)
|
|
305
|
-
) {
|
|
306
|
-
return { text };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const idStart = candidateStart + RAW_OUTPUT_ARTIFACT_PREFIX.length;
|
|
310
|
-
const idEnd = trimmed.length - RAW_OUTPUT_ARTIFACT_SUFFIX.length;
|
|
311
|
-
if (idStart === idEnd) return { text };
|
|
312
|
-
for (let i = idStart; i < idEnd; i++) {
|
|
313
|
-
const code = trimmed.charCodeAt(i);
|
|
314
|
-
if (code < 48 || code > 57) return { text };
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const artifactId = trimmed.slice(idStart, idEnd);
|
|
318
|
-
return {
|
|
319
|
-
text: trimmed.slice(0, lineStart === -1 ? 0 : lineStart).trimEnd(),
|
|
320
|
-
artifactId,
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
300
|
/**
|
|
325
301
|
* Strip the trailing occurrence of `notice` (plus a single surrounding newline
|
|
326
302
|
* on each side) so the TUI can echo the value via a styled footer label
|
package/src/tools/checkpoint.ts
CHANGED
|
@@ -17,6 +17,15 @@ export interface CheckpointState {
|
|
|
17
17
|
startedAt: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export interface CompletedRewindState {
|
|
21
|
+
/** Report retained after a successful rewind. */
|
|
22
|
+
report: string;
|
|
23
|
+
/** Timestamp for the checkpoint that was rewound. */
|
|
24
|
+
startedAt: string;
|
|
25
|
+
/** Timestamp when the rewind completed. */
|
|
26
|
+
rewoundAt: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
const checkpointSchema = type({
|
|
21
30
|
goal: type("string").describe("investigation goal"),
|
|
22
31
|
});
|
|
@@ -123,7 +132,12 @@ export class RewindTool implements AgentTool<typeof rewindSchema, RewindToolDeta
|
|
|
123
132
|
throw new ToolError("Checkpoint not available in subagents.");
|
|
124
133
|
}
|
|
125
134
|
if (!this.session.getCheckpointState?.()) {
|
|
126
|
-
|
|
135
|
+
if (this.session.getLastCompletedRewind?.()) {
|
|
136
|
+
throw new ToolError(
|
|
137
|
+
"Checkpoint already completed; continue from the retained rewind report instead of calling rewind again.",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
throw new ToolError("No active checkpoint. Create a checkpoint before calling rewind.");
|
|
127
141
|
}
|
|
128
142
|
const report = params.report.trim();
|
|
129
143
|
if (report.length === 0) {
|
package/src/tools/eval-render.ts
CHANGED
|
@@ -775,4 +775,19 @@ export const evalToolRenderer = {
|
|
|
775
775
|
// below the first cell. Expanded output is top-anchored enough for the
|
|
776
776
|
// transcript to commit its settled prefix.
|
|
777
777
|
provisionalPendingPreview: "collapsed",
|
|
778
|
+
// Partial-result chrome is NOT byte-stable: `renderAgentProgressEvents`
|
|
779
|
+
// inserts/removes each subagent's current-tool line as it starts/stops a
|
|
780
|
+
// tool, and ticks status icon/stats/duration on already-rendered rows,
|
|
781
|
+
// while `options.isPartial` holds for the whole eval() cell (agent
|
|
782
|
+
// progress ticks never carry an `async` completed/failed state, so
|
|
783
|
+
// `event-controller.ts` keeps `isPartial: true` throughout). If this
|
|
784
|
+
// block were commit-stable during that churn, `deriveLiveCommitState`'s
|
|
785
|
+
// stable-prefix ratchet would promote agent rows that keep mutating (a
|
|
786
|
+
// "slow ticker", see transcript-container.ts) into native scrollback,
|
|
787
|
+
// and the tui resync's "duplication, never loss" contract would
|
|
788
|
+
// repeatedly re-show the frame tail under a heavy concurrent
|
|
789
|
+
// `agent()`/`parallel()` fan-out — the overlapping/duplicated tree rows
|
|
790
|
+
// seen when many subagents run at once. Once the cell settles
|
|
791
|
+
// (`isPartial === false`) the block is commit-stable again.
|
|
792
|
+
provisionalPartialResult: true,
|
|
778
793
|
};
|
package/src/tools/fetch.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { finalizeOutput, loadPage, looksLikeHtml, MAX_BYTES, MAX_OUTPUT_CHARS }
|
|
|
27
27
|
import { convertWithMarkit, fetchBinary } from "../web/scrapers/utils";
|
|
28
28
|
import { applyListLimit } from "./list-limit";
|
|
29
29
|
import { formatStyledArtifactReference, type OutputMeta } from "./output-meta";
|
|
30
|
-
import { type LineRange, parseLineRanges } from "./path-utils";
|
|
30
|
+
import { isReadableUrlPath, type LineRange, parseLineRanges } from "./path-utils";
|
|
31
31
|
import { formatBytes, formatExpandHint, getDomain, replaceTabs } from "./render-utils";
|
|
32
32
|
import { listTables, looksLikeSqlite, renderTableList } from "./sqlite-reader";
|
|
33
33
|
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
@@ -145,10 +145,6 @@ function normalizeUrl(url: string): string {
|
|
|
145
145
|
return url;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
export function isReadableUrlPath(value: string): boolean {
|
|
149
|
-
return /^https?:\/\/?/i.test(value) || /^www\./i.test(value);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
148
|
// URL line selectors mirror the file form: `:50`, `:50-100`, `:50+150`, `:5-10,20-30`, `:raw`,
|
|
153
149
|
// or `:raw:N-M` / `:N-M:raw` to combine raw mode with a range. If a URL would otherwise look
|
|
154
150
|
// like `host:port`, add a trailing slash before the selector (e.g. `https://example.com/:80`
|
|
@@ -1559,9 +1555,12 @@ export interface ReadUrlToolDetails {
|
|
|
1559
1555
|
|
|
1560
1556
|
interface ReadUrlCacheEntry {
|
|
1561
1557
|
artifactId?: string;
|
|
1558
|
+
artifactPath?: string;
|
|
1559
|
+
contentPath?: string;
|
|
1562
1560
|
details: ReadUrlToolDetails;
|
|
1563
1561
|
image?: FetchImagePayload;
|
|
1564
1562
|
output: string;
|
|
1563
|
+
content: string;
|
|
1565
1564
|
}
|
|
1566
1565
|
|
|
1567
1566
|
const READ_URL_CACHE_MAX_ENTRIES = 100;
|
|
@@ -1572,20 +1571,24 @@ function getReadUrlCacheKey(session: ToolSession, requestedUrl: string, raw: boo
|
|
|
1572
1571
|
return `${scope}::${raw ? "raw" : "rendered"}::${normalizeUrl(requestedUrl)}`;
|
|
1573
1572
|
}
|
|
1574
1573
|
|
|
1575
|
-
async function
|
|
1574
|
+
async function findArtifactPath(session: ToolSession, artifactId: string): Promise<string | null> {
|
|
1576
1575
|
const artifactsDir = session.getArtifactsDir?.();
|
|
1577
1576
|
if (!artifactsDir) return null;
|
|
1578
1577
|
|
|
1579
1578
|
try {
|
|
1580
1579
|
const files = await fs.readdir(artifactsDir);
|
|
1581
1580
|
const match = files.find(file => file.startsWith(`${artifactId}.`));
|
|
1582
|
-
|
|
1583
|
-
return await Bun.file(path.join(artifactsDir, match)).text();
|
|
1581
|
+
return match ? path.join(artifactsDir, match) : null;
|
|
1584
1582
|
} catch {
|
|
1585
1583
|
return null;
|
|
1586
1584
|
}
|
|
1587
1585
|
}
|
|
1588
1586
|
|
|
1587
|
+
async function readArtifactOutput(session: ToolSession, artifactId: string): Promise<string | null> {
|
|
1588
|
+
const artifactPath = await findArtifactPath(session, artifactId);
|
|
1589
|
+
return artifactPath ? await Bun.file(artifactPath).text() : null;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1589
1592
|
async function materializeReadUrlCacheEntry(
|
|
1590
1593
|
session: ToolSession,
|
|
1591
1594
|
entry: ReadUrlCacheEntry,
|
|
@@ -1600,17 +1603,58 @@ async function materializeReadUrlCacheEntry(
|
|
|
1600
1603
|
return entry.output.length > 0 ? entry : null;
|
|
1601
1604
|
}
|
|
1602
1605
|
|
|
1603
|
-
async function persistReadUrlArtifact(
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1606
|
+
async function persistReadUrlArtifact(
|
|
1607
|
+
session: ToolSession,
|
|
1608
|
+
output: string,
|
|
1609
|
+
): Promise<{ id?: string; path?: string } | undefined> {
|
|
1610
|
+
const artifact = await session.allocateOutputArtifact?.("read");
|
|
1611
|
+
if (!artifact?.path) return undefined;
|
|
1612
|
+
await Bun.write(artifact.path, output);
|
|
1613
|
+
return artifact;
|
|
1608
1614
|
}
|
|
1609
1615
|
|
|
1610
1616
|
async function ensureReadUrlCacheArtifact(session: ToolSession, entry: ReadUrlCacheEntry): Promise<ReadUrlCacheEntry> {
|
|
1611
|
-
if (entry.artifactId) return entry;
|
|
1612
|
-
|
|
1613
|
-
|
|
1617
|
+
if (entry.artifactId && entry.artifactPath) return entry;
|
|
1618
|
+
if (entry.artifactId) {
|
|
1619
|
+
const artifactPath = await findArtifactPath(session, entry.artifactId);
|
|
1620
|
+
if (artifactPath) return { ...entry, artifactPath };
|
|
1621
|
+
}
|
|
1622
|
+
const artifact = await persistReadUrlArtifact(session, entry.output);
|
|
1623
|
+
return artifact?.id ? { ...entry, artifactId: artifact.id, artifactPath: artifact.path } : entry;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
function readUrlContentExtension(finalUrl: string): string {
|
|
1627
|
+
try {
|
|
1628
|
+
const ext = getFilenameExtensionHint(new URL(finalUrl).pathname);
|
|
1629
|
+
return ext && /^\.[a-z0-9][a-z0-9+.-]{0,15}$/i.test(ext) ? ext : ".txt";
|
|
1630
|
+
} catch {
|
|
1631
|
+
return ".txt";
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
async function ensureReadUrlContentFile(
|
|
1636
|
+
session: ToolSession,
|
|
1637
|
+
entry: ReadUrlCacheEntry,
|
|
1638
|
+
raw: boolean,
|
|
1639
|
+
): Promise<ReadUrlCacheEntry> {
|
|
1640
|
+
if (entry.contentPath) {
|
|
1641
|
+
try {
|
|
1642
|
+
await Bun.file(entry.contentPath).stat();
|
|
1643
|
+
return entry;
|
|
1644
|
+
} catch {
|
|
1645
|
+
// Recreate below when the cached scratch file was removed.
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
const root = session.getArtifactsDir?.();
|
|
1649
|
+
if (!root) {
|
|
1650
|
+
throw new ToolError("Cannot search URL output because this session cannot materialize read artifacts.");
|
|
1651
|
+
}
|
|
1652
|
+
const dir = path.join(root, "url-search");
|
|
1653
|
+
await fs.mkdir(dir, { recursive: true });
|
|
1654
|
+
const hash = Bun.hash(`${raw ? "raw" : "rendered"}:${entry.details.finalUrl}`).toString(36);
|
|
1655
|
+
const contentPath = path.join(dir, `${hash}${readUrlContentExtension(entry.details.finalUrl)}`);
|
|
1656
|
+
await Bun.write(contentPath, entry.content);
|
|
1657
|
+
return { ...entry, contentPath };
|
|
1614
1658
|
}
|
|
1615
1659
|
|
|
1616
1660
|
function cacheReadUrlEntry(session: ToolSession, requestedUrl: string, raw: boolean, entry: ReadUrlCacheEntry): void {
|
|
@@ -1644,10 +1688,11 @@ async function buildReadUrlCacheEntry(
|
|
|
1644
1688
|
webpExclusionForModel(session.getActiveModel?.()),
|
|
1645
1689
|
);
|
|
1646
1690
|
const output = buildUrlReadOutput(result, result.content);
|
|
1647
|
-
const
|
|
1691
|
+
const artifact = options?.ensureArtifact ? await persistReadUrlArtifact(session, output) : undefined;
|
|
1648
1692
|
|
|
1649
1693
|
return {
|
|
1650
|
-
artifactId,
|
|
1694
|
+
artifactId: artifact?.id,
|
|
1695
|
+
artifactPath: artifact?.path,
|
|
1651
1696
|
details: {
|
|
1652
1697
|
kind: "url",
|
|
1653
1698
|
url: result.url,
|
|
@@ -1659,6 +1704,7 @@ async function buildReadUrlCacheEntry(
|
|
|
1659
1704
|
},
|
|
1660
1705
|
image: result.image,
|
|
1661
1706
|
output,
|
|
1707
|
+
content: result.content,
|
|
1662
1708
|
};
|
|
1663
1709
|
}
|
|
1664
1710
|
|
|
@@ -1686,6 +1732,24 @@ export async function loadReadUrlCacheEntry(
|
|
|
1686
1732
|
return fresh;
|
|
1687
1733
|
}
|
|
1688
1734
|
|
|
1735
|
+
/** Materialize rendered URL body text to a local file for tools that require filesystem paths. */
|
|
1736
|
+
export async function materializeReadUrlToFile(
|
|
1737
|
+
session: ToolSession,
|
|
1738
|
+
params: { path: string; raw?: boolean },
|
|
1739
|
+
signal?: AbortSignal,
|
|
1740
|
+
): Promise<{ path: string; details: ReadUrlToolDetails }> {
|
|
1741
|
+
if (!session.settings.get("fetch.enabled")) {
|
|
1742
|
+
throw new ToolError("URL reads are disabled by settings.");
|
|
1743
|
+
}
|
|
1744
|
+
const cacheEntry = await loadReadUrlCacheEntry(session, params, signal, { preferCached: true });
|
|
1745
|
+
const materialized = await ensureReadUrlContentFile(session, cacheEntry, params.raw ?? false);
|
|
1746
|
+
cacheReadUrlEntry(session, params.path, params.raw ?? false, materialized);
|
|
1747
|
+
if (!materialized.contentPath) {
|
|
1748
|
+
throw new ToolError("Cannot search URL output because this session cannot materialize read artifacts.");
|
|
1749
|
+
}
|
|
1750
|
+
return { path: materialized.contentPath, details: materialized.details };
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1689
1753
|
function buildUrlReadOutput(result: FetchRenderResult, content: string): string {
|
|
1690
1754
|
let output = "";
|
|
1691
1755
|
output += `URL: ${result.finalUrl}\n`;
|
package/src/tools/gh.ts
CHANGED
package/src/tools/grep.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
37
37
|
import { type ArchiveReader, type ExtractedArchiveFile, openArchive, parseArchivePathCandidates } from "../utils/zip";
|
|
38
38
|
import type { ToolSession } from ".";
|
|
39
|
+
import { materializeReadUrlToFile, parseReadUrlTarget } from "./fetch";
|
|
39
40
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
40
41
|
import { classifyGroupedLines, formatGroupedFiles, groupLineIndicesByBlank } from "./grouped-file-output";
|
|
41
42
|
import { formatMatchLine } from "./match-line-format";
|
|
@@ -901,6 +902,18 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
901
902
|
const rawEntries = await expandDelimitedPathEntries(effectivePaths, this.session.cwd);
|
|
902
903
|
const pathSpecs = parsePathSpecs(rawEntries);
|
|
903
904
|
const paths = pathSpecs.map(spec => spec.clean);
|
|
905
|
+
const materializedExternalPaths = new Map<string, string>();
|
|
906
|
+
const materializeExternalUrlForSearch = async (rawPath: string) => {
|
|
907
|
+
const target = parseReadUrlTarget(rawPath);
|
|
908
|
+
if (!target) return undefined;
|
|
909
|
+
const materialized = await materializeReadUrlToFile(
|
|
910
|
+
this.session,
|
|
911
|
+
{ path: target.path, raw: target.raw },
|
|
912
|
+
signal,
|
|
913
|
+
);
|
|
914
|
+
materializedExternalPaths.set(rawPath, materialized.path);
|
|
915
|
+
return { sourcePath: materialized.path, immutable: true };
|
|
916
|
+
};
|
|
904
917
|
const {
|
|
905
918
|
resolvedPaths,
|
|
906
919
|
displayMap: archiveDisplayMap,
|
|
@@ -921,34 +934,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
921
934
|
});
|
|
922
935
|
const searchablePaths = internalResolution.paths;
|
|
923
936
|
const { virtualResources, virtualPathSet, virtualInputIndexes } = internalResolution;
|
|
924
|
-
// Build the per-file line-range filter (keyed by absolute path) now that
|
|
925
|
-
// archive entries have been materialized to scratch files. Plain entries
|
|
926
|
-
// resolve through `resolveReadPath`; archive entries are keyed by the
|
|
927
|
-
// scratch path that grep will actually report against.
|
|
928
937
|
const rangesByAbsPath = new Map<string, LineRange[]>();
|
|
929
|
-
for (let idx = 0; idx < pathSpecs.length; idx++) {
|
|
930
|
-
const spec = pathSpecs[idx];
|
|
931
|
-
if (!spec.ranges) continue;
|
|
932
|
-
if (virtualInputIndexes.has(idx)) continue;
|
|
933
|
-
const resolved = internalResolution.resolvedPathsByInput[idx];
|
|
934
|
-
if (!resolved) continue;
|
|
935
|
-
if (resolved === spec.clean && !archiveDisplayMap.has(resolved)) {
|
|
936
|
-
// Non-archive entry; ensure the cleaned path resolves to a regular file.
|
|
937
|
-
const absKey = path.resolve(resolveReadPath(resolved, this.session.cwd));
|
|
938
|
-
const stats = await stat(absKey).catch(() => null);
|
|
939
|
-
if (!stats) {
|
|
940
|
-
throw new ToolError(`Path not found for line-range selector: ${spec.original}`);
|
|
941
|
-
}
|
|
942
|
-
if (!stats.isFile()) {
|
|
943
|
-
throw new ToolError(`Line-range selector requires a single file: ${spec.original} is a directory`);
|
|
944
|
-
}
|
|
945
|
-
mergeRangesInto(rangesByAbsPath, absKey, spec.ranges);
|
|
946
|
-
} else {
|
|
947
|
-
// Archive entry — `resolveArchiveSearchPaths` substituted a scratch path.
|
|
948
|
-
const absKey = path.resolve(resolved);
|
|
949
|
-
mergeRangesInto(rangesByAbsPath, absKey, spec.ranges);
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
938
|
|
|
953
939
|
if (
|
|
954
940
|
archiveUnreadable.length > 0 &&
|
|
@@ -991,6 +977,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
991
977
|
signal,
|
|
992
978
|
localProtocolOptions: this.session.localProtocolOptions,
|
|
993
979
|
skills: this.session.skills,
|
|
980
|
+
resolveExternalUrl: materializeExternalUrlForSearch,
|
|
994
981
|
});
|
|
995
982
|
searchPath = scope.searchPath;
|
|
996
983
|
isDirectory = scope.isDirectory;
|
|
@@ -1001,6 +988,37 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1001
988
|
for (const immutablePath of scope.immutableSourcePaths) {
|
|
1002
989
|
immutableSourcePaths.add(immutablePath);
|
|
1003
990
|
}
|
|
991
|
+
// Build the per-file line-range filter after URL materialization has run:
|
|
992
|
+
// archive entries are keyed by scratch path, URL entries by read-cache
|
|
993
|
+
// content path, and ordinary files by their resolved filesystem path.
|
|
994
|
+
for (let idx = 0; idx < pathSpecs.length; idx++) {
|
|
995
|
+
const spec = pathSpecs[idx];
|
|
996
|
+
if (!spec.ranges) continue;
|
|
997
|
+
if (virtualInputIndexes.has(idx)) continue;
|
|
998
|
+
const resolved = internalResolution.resolvedPathsByInput[idx];
|
|
999
|
+
if (!resolved) continue;
|
|
1000
|
+
const materializedExternalPath = materializedExternalPaths.get(spec.clean);
|
|
1001
|
+
if (materializedExternalPath) {
|
|
1002
|
+
mergeRangesInto(rangesByAbsPath, path.resolve(materializedExternalPath), spec.ranges);
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
if (resolved === spec.clean && !archiveDisplayMap.has(resolved)) {
|
|
1006
|
+
// Non-archive entry; ensure the cleaned path resolves to a regular file.
|
|
1007
|
+
const absKey = path.resolve(resolveReadPath(resolved, this.session.cwd));
|
|
1008
|
+
const stats = await stat(absKey).catch(() => null);
|
|
1009
|
+
if (!stats) {
|
|
1010
|
+
throw new ToolError(`Path not found for line-range selector: ${spec.original}`);
|
|
1011
|
+
}
|
|
1012
|
+
if (!stats.isFile()) {
|
|
1013
|
+
throw new ToolError(
|
|
1014
|
+
`Line-range selector requires a single file: ${spec.original} is a directory`,
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
mergeRangesInto(rangesByAbsPath, absKey, spec.ranges);
|
|
1018
|
+
} else {
|
|
1019
|
+
mergeRangesInto(rangesByAbsPath, path.resolve(resolved), spec.ranges);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1004
1022
|
// When the only input was an archive selector, surface that selector instead
|
|
1005
1023
|
// of the temp scratch path the resolver substituted in.
|
|
1006
1024
|
const physicalScopePath =
|
package/src/tools/index.ts
CHANGED
|
@@ -40,7 +40,7 @@ import { AstGrepTool } from "./ast-grep";
|
|
|
40
40
|
import { BashTool } from "./bash";
|
|
41
41
|
import { BrowserTool } from "./browser";
|
|
42
42
|
import { type BuiltinToolName, normalizeToolNames } from "./builtin-names";
|
|
43
|
-
import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
|
|
43
|
+
import { type CheckpointState, CheckpointTool, type CompletedRewindState, RewindTool } from "./checkpoint";
|
|
44
44
|
import { DebugTool } from "./debug";
|
|
45
45
|
import { EvalTool } from "./eval";
|
|
46
46
|
import { resolveEvalBackends } from "./eval-backends";
|
|
@@ -332,6 +332,8 @@ export interface ToolSession {
|
|
|
332
332
|
getCheckpointState?: () => CheckpointState | undefined;
|
|
333
333
|
/** Set or clear active checkpoint state. */
|
|
334
334
|
setCheckpointState?: (state: CheckpointState | null) => void;
|
|
335
|
+
/** Get the most recent completed rewind, if this session just rewound a checkpoint. */
|
|
336
|
+
getLastCompletedRewind?: () => CompletedRewindState | undefined;
|
|
335
337
|
|
|
336
338
|
/** Per-session snapshot store of file contents as last shown to the model
|
|
337
339
|
* by `read`/`search`. Used by hashline anchor-stale recovery to
|
package/src/tools/irc.ts
CHANGED
|
@@ -97,6 +97,7 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
97
97
|
readonly description: string;
|
|
98
98
|
readonly parameters = ircSchema;
|
|
99
99
|
readonly strict = true;
|
|
100
|
+
readonly interruptible = true;
|
|
100
101
|
|
|
101
102
|
readonly examples: readonly ToolExample<typeof ircSchema.infer>[] = [
|
|
102
103
|
{
|
|
@@ -317,16 +318,30 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
317
318
|
lines.push("");
|
|
318
319
|
if (delivered.length > 0) {
|
|
319
320
|
const reply = await waiting;
|
|
320
|
-
if (reply.error)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
321
|
+
if (reply.error) {
|
|
322
|
+
// The send already succeeded; if the wait was interrupted by our
|
|
323
|
+
// caller signal (steering / IRC), preserve the delivery receipt so
|
|
324
|
+
// the agent loop keeps this tool as "sent" instead of marking it
|
|
325
|
+
// skipped, which would prompt a duplicate resend on the next turn.
|
|
326
|
+
if (signal?.aborted) {
|
|
327
|
+
lines.push(
|
|
328
|
+
`Send delivered but the reply wait was interrupted before ${to} answered. ` +
|
|
329
|
+
"Check `inbox` or `wait` again after handling the interrupt.",
|
|
330
|
+
);
|
|
331
|
+
} else {
|
|
332
|
+
throw reply.error;
|
|
333
|
+
}
|
|
325
334
|
} else {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
335
|
+
waited = reply.message;
|
|
336
|
+
if (waited) {
|
|
337
|
+
lines.push(`Reply from ${waited.from}:`);
|
|
338
|
+
lines.push(waited.body);
|
|
339
|
+
} else {
|
|
340
|
+
lines.push(
|
|
341
|
+
`No reply from ${to} within ${formatDuration(timeoutMs)}. ` +
|
|
342
|
+
"They may answer later — check `inbox` or `wait` again.",
|
|
343
|
+
);
|
|
344
|
+
}
|
|
330
345
|
}
|
|
331
346
|
} else {
|
|
332
347
|
awaitAbort?.abort(awaitCancelled);
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -385,6 +385,56 @@ export function formatFullOutputReference(artifactId: string): string {
|
|
|
385
385
|
return `Read artifact://${artifactId} for full output`;
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
const RAW_OUTPUT_ARTIFACT_PREFIX = "[raw output: artifact://";
|
|
389
|
+
const RAW_OUTPUT_ARTIFACT_SUFFIX = "]";
|
|
390
|
+
|
|
391
|
+
/** Remove the trailing bash raw-output artifact footer while preserving its artifact id. */
|
|
392
|
+
export function stripRawOutputArtifactNotice(text: string): { text: string; artifactId?: string } {
|
|
393
|
+
const trimmed = text.trimEnd();
|
|
394
|
+
const lineStart = trimmed.lastIndexOf("\n");
|
|
395
|
+
const candidateStart = lineStart === -1 ? 0 : lineStart + 1;
|
|
396
|
+
if (
|
|
397
|
+
!trimmed.startsWith(RAW_OUTPUT_ARTIFACT_PREFIX, candidateStart) ||
|
|
398
|
+
!trimmed.endsWith(RAW_OUTPUT_ARTIFACT_SUFFIX)
|
|
399
|
+
) {
|
|
400
|
+
return { text };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const idStart = candidateStart + RAW_OUTPUT_ARTIFACT_PREFIX.length;
|
|
404
|
+
const idEnd = trimmed.length - RAW_OUTPUT_ARTIFACT_SUFFIX.length;
|
|
405
|
+
if (idStart === idEnd) return { text };
|
|
406
|
+
for (let i = idStart; i < idEnd; i++) {
|
|
407
|
+
const code = trimmed.charCodeAt(i);
|
|
408
|
+
if (code < 48 || code > 57) return { text };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const artifactId = trimmed.slice(idStart, idEnd);
|
|
412
|
+
return {
|
|
413
|
+
text: trimmed.slice(0, lineStart === -1 ? 0 : lineStart).trimEnd(),
|
|
414
|
+
artifactId,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function isGeneratedOutputNoticeLine(line: string): boolean {
|
|
419
|
+
if (!line.startsWith("[") || !line.endsWith("]")) return false;
|
|
420
|
+
const body = line.slice(1, -1);
|
|
421
|
+
return (
|
|
422
|
+
body.startsWith("Showing ") ||
|
|
423
|
+
/^\d+ matches limit reached\. Use limit=\d+ for more/u.test(body) ||
|
|
424
|
+
/^\d+ results limit reached\. Use limit=\d+ for more/u.test(body) ||
|
|
425
|
+
body.startsWith("Some lines truncated to ")
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Remove a trailing generated output notice when metadata is unavailable. */
|
|
430
|
+
export function stripGeneratedOutputNotice(text: string): string {
|
|
431
|
+
const trimmed = text.trimEnd();
|
|
432
|
+
const lineStart = trimmed.lastIndexOf("\n");
|
|
433
|
+
const candidateStart = lineStart === -1 ? 0 : lineStart + 1;
|
|
434
|
+
if (!isGeneratedOutputNoticeLine(trimmed.slice(candidateStart))) return text;
|
|
435
|
+
return trimmed.slice(0, lineStart === -1 ? 0 : lineStart).trimEnd();
|
|
436
|
+
}
|
|
437
|
+
|
|
388
438
|
export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
|
|
389
439
|
let notice: string;
|
|
390
440
|
|