@oh-my-pi/pi-coding-agent 15.3.2 → 15.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/dist/types/cli/file-processor.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +45 -3
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +2 -0
- package/dist/types/edit/file-read-cache.d.ts +15 -4
- package/dist/types/edit/index.d.ts +3 -8
- package/dist/types/edit/renderer.d.ts +1 -2
- package/dist/types/eval/__tests__/shared-executors.test.d.ts +1 -0
- package/dist/types/eval/js/shared/local-module-loader.d.ts +16 -0
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +4 -0
- package/dist/types/eval/js/shared/runtime.d.ts +14 -8
- package/dist/types/eval/py/executor.d.ts +1 -2
- package/dist/types/eval/py/kernel.d.ts +6 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -5
- package/dist/types/eval/session-id.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +1 -3
- package/dist/types/hashline/anchors.d.ts +15 -9
- package/dist/types/hashline/constants.d.ts +0 -2
- package/dist/types/hashline/diff.d.ts +1 -2
- package/dist/types/hashline/executor.d.ts +52 -0
- package/dist/types/hashline/hash.d.ts +44 -93
- package/dist/types/hashline/index.d.ts +2 -1
- package/dist/types/hashline/input.d.ts +2 -9
- package/dist/types/hashline/recovery.d.ts +3 -9
- package/dist/types/hashline/tokenizer.d.ts +91 -0
- package/dist/types/hashline/types.d.ts +5 -7
- package/dist/types/modes/components/extensions/types.d.ts +0 -4
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +11 -15
- package/dist/types/session/agent-storage.d.ts +11 -10
- package/dist/types/slash-commands/acp-builtins.d.ts +3 -3
- package/dist/types/slash-commands/types.d.ts +0 -5
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/tool-discovery/tool-index.d.ts +0 -50
- package/dist/types/tools/index.d.ts +2 -8
- package/dist/types/tools/match-line-format.d.ts +4 -4
- package/dist/types/tools/output-schema-validator.d.ts +64 -0
- package/dist/types/tools/review.d.ts +13 -0
- package/dist/types/tools/search-tool-bm25.d.ts +1 -1
- package/dist/types/tools/search.d.ts +4 -3
- package/dist/types/utils/edit-mode.d.ts +1 -1
- package/dist/types/web/kagi.d.ts +4 -2
- package/dist/types/web/parallel.d.ts +4 -3
- package/dist/types/web/scrapers/types.d.ts +2 -1
- package/dist/types/web/search/index.d.ts +12 -4
- package/dist/types/web/search/provider.d.ts +2 -1
- package/dist/types/web/search/providers/anthropic.d.ts +9 -4
- package/dist/types/web/search/providers/base.d.ts +34 -2
- package/dist/types/web/search/providers/brave.d.ts +8 -1
- package/dist/types/web/search/providers/codex.d.ts +13 -9
- package/dist/types/web/search/providers/exa.d.ts +10 -1
- package/dist/types/web/search/providers/gemini.d.ts +20 -23
- package/dist/types/web/search/providers/jina.d.ts +2 -1
- package/dist/types/web/search/providers/kagi.d.ts +4 -1
- package/dist/types/web/search/providers/kimi.d.ts +10 -1
- package/dist/types/web/search/providers/parallel.d.ts +3 -2
- package/dist/types/web/search/providers/perplexity.d.ts +5 -2
- package/dist/types/web/search/providers/searxng.d.ts +2 -1
- package/dist/types/web/search/providers/synthetic.d.ts +5 -8
- package/dist/types/web/search/providers/tavily.d.ts +11 -4
- package/dist/types/web/search/providers/utils.d.ts +8 -6
- package/dist/types/web/search/providers/zai.d.ts +12 -3
- package/package.json +7 -7
- package/src/cli/file-processor.ts +12 -2
- package/src/cli.ts +0 -8
- package/src/commands/commit.ts +8 -8
- package/src/config/prompt-templates.ts +6 -6
- package/src/config/settings-schema.ts +47 -3
- package/src/config/settings.ts +5 -5
- package/src/debug/raw-sse.ts +68 -3
- package/src/edit/file-read-cache.ts +68 -25
- package/src/edit/index.ts +6 -37
- package/src/edit/renderer.ts +9 -47
- package/src/edit/streaming.ts +43 -56
- package/src/eval/__tests__/shared-executors.test.ts +520 -0
- package/src/eval/js/context-manager.ts +64 -53
- package/src/eval/js/shared/local-module-loader.ts +265 -0
- package/src/eval/js/shared/prelude.txt +4 -0
- package/src/eval/js/shared/rewrite-imports.ts +85 -0
- package/src/eval/js/shared/runtime.ts +129 -86
- package/src/eval/js/worker-core.ts +23 -38
- package/src/eval/py/executor.ts +155 -84
- package/src/eval/py/kernel.ts +10 -1
- package/src/eval/py/prelude.py +22 -24
- package/src/eval/py/runner.py +203 -85
- package/src/eval/py/tool-bridge.ts +17 -10
- package/src/eval/session-id.ts +8 -0
- package/src/exec/bash-executor.ts +27 -16
- package/src/extensibility/extensions/runner.ts +0 -1
- package/src/extensibility/extensions/types.ts +1 -3
- package/src/hashline/anchors.ts +56 -65
- package/src/hashline/apply.ts +29 -31
- package/src/hashline/constants.ts +0 -3
- package/src/hashline/diff-preview.ts +4 -5
- package/src/hashline/diff.ts +30 -4
- package/src/hashline/execute.ts +91 -26
- package/src/hashline/executor.ts +239 -0
- package/src/hashline/grammar.lark +12 -10
- package/src/hashline/hash.ts +69 -114
- package/src/hashline/index.ts +2 -1
- package/src/hashline/input.ts +48 -41
- package/src/hashline/prefixes.ts +21 -11
- package/src/hashline/recovery.ts +63 -71
- package/src/hashline/stream.ts +2 -2
- package/src/hashline/tokenizer.ts +467 -0
- package/src/hashline/types.ts +6 -8
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/modes/components/extensions/types.ts +0 -5
- package/src/modes/components/session-observer-overlay.ts +11 -2
- package/src/modes/components/tree-selector.ts +10 -2
- package/src/modes/controllers/command-controller.ts +1 -3
- package/src/modes/controllers/extension-ui-controller.ts +10 -11
- package/src/modes/controllers/selector-controller.ts +5 -5
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/ui-helpers.ts +4 -0
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/eval.md +1 -1
- package/src/prompts/tools/hashline.md +73 -94
- package/src/prompts/tools/read.md +4 -4
- package/src/prompts/tools/search.md +3 -3
- package/src/sdk.ts +17 -23
- package/src/session/agent-session.ts +59 -66
- package/src/session/agent-storage.ts +13 -14
- package/src/slash-commands/acp-builtins.ts +3 -3
- package/src/slash-commands/types.ts +0 -6
- package/src/task/executor.ts +26 -57
- package/src/task/index.ts +8 -4
- package/src/tool-discovery/tool-index.ts +0 -134
- package/src/tools/ast-edit.ts +36 -13
- package/src/tools/ast-grep.ts +45 -4
- package/src/tools/browser/tab-worker.ts +3 -2
- package/src/tools/eval.ts +2 -1
- package/src/tools/fetch.ts +23 -14
- package/src/tools/index.ts +2 -8
- package/src/tools/irc.ts +59 -5
- package/src/tools/match-line-format.ts +5 -7
- package/src/tools/output-schema-validator.ts +132 -0
- package/src/tools/read.ts +142 -31
- package/src/tools/review.ts +23 -0
- package/src/tools/search-tool-bm25.ts +3 -30
- package/src/tools/search.ts +48 -16
- package/src/tools/write.ts +3 -3
- package/src/tools/yield.ts +32 -41
- package/src/utils/edit-mode.ts +1 -2
- package/src/utils/file-mentions.ts +2 -2
- package/src/web/kagi.ts +15 -6
- package/src/web/parallel.ts +9 -6
- package/src/web/scrapers/types.ts +7 -1
- package/src/web/scrapers/youtube.ts +13 -7
- package/src/web/search/index.ts +37 -11
- package/src/web/search/provider.ts +5 -3
- package/src/web/search/providers/anthropic.ts +30 -21
- package/src/web/search/providers/base.ts +35 -2
- package/src/web/search/providers/brave.ts +4 -4
- package/src/web/search/providers/codex.ts +118 -89
- package/src/web/search/providers/exa.ts +3 -2
- package/src/web/search/providers/gemini.ts +58 -155
- package/src/web/search/providers/jina.ts +4 -4
- package/src/web/search/providers/kagi.ts +17 -11
- package/src/web/search/providers/kimi.ts +29 -13
- package/src/web/search/providers/parallel.ts +171 -23
- package/src/web/search/providers/perplexity.ts +38 -37
- package/src/web/search/providers/searxng.ts +3 -1
- package/src/web/search/providers/synthetic.ts +16 -19
- package/src/web/search/providers/tavily.ts +23 -18
- package/src/web/search/providers/utils.ts +11 -17
- package/src/web/search/providers/zai.ts +16 -8
- package/dist/types/hashline/parser.d.ts +0 -7
- package/dist/types/mcp/discoverable-tool-metadata.d.ts +0 -7
- package/dist/types/tools/vim.d.ts +0 -58
- package/dist/types/vim/buffer.d.ts +0 -41
- package/dist/types/vim/commands.d.ts +0 -6
- package/dist/types/vim/engine.d.ts +0 -47
- package/dist/types/vim/parser.d.ts +0 -3
- package/dist/types/vim/render.d.ts +0 -25
- package/dist/types/vim/types.d.ts +0 -182
- package/src/hashline/parser.ts +0 -246
- package/src/mcp/discoverable-tool-metadata.ts +0 -24
- package/src/prompts/tools/vim.md +0 -98
- package/src/tools/vim.ts +0 -949
- package/src/vim/buffer.ts +0 -309
- package/src/vim/commands.ts +0 -382
- package/src/vim/engine.ts +0 -2409
- package/src/vim/parser.ts +0 -134
- package/src/vim/render.ts +0 -252
- package/src/vim/types.ts +0 -197
package/src/tools/irc.ts
CHANGED
|
@@ -25,6 +25,7 @@ import ircDescription from "../prompts/tools/irc.md" with { type: "text" };
|
|
|
25
25
|
import type { AgentRef, AgentRegistry } from "../registry/agent-registry";
|
|
26
26
|
import type { ToolSession } from ".";
|
|
27
27
|
|
|
28
|
+
const DEFAULT_IRC_TIMEOUT_MS = 120_000;
|
|
28
29
|
const ircSchema = z.object({
|
|
29
30
|
op: z.enum(["send", "list"]).describe("irc operation"),
|
|
30
31
|
to: z.string().optional().describe('recipient agent id or "all"'),
|
|
@@ -159,6 +160,7 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
159
160
|
|
|
160
161
|
const awaitReply = params.awaitReply ?? !isBroadcast;
|
|
161
162
|
|
|
163
|
+
const timeoutMs = normalizeIrcTimeoutMs(this.session.settings.get("irc.timeoutMs"));
|
|
162
164
|
const delivered: string[] = [];
|
|
163
165
|
const replies: IrcReply[] = [];
|
|
164
166
|
const failed: Array<{ id: string; error: string }> = [];
|
|
@@ -174,12 +176,18 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
174
176
|
return;
|
|
175
177
|
}
|
|
176
178
|
try {
|
|
177
|
-
const result = await
|
|
178
|
-
|
|
179
|
-
message,
|
|
180
|
-
awaitReply,
|
|
179
|
+
const result = await runIrcDispatchWithTimeout(
|
|
180
|
+
timeoutMs,
|
|
181
181
|
signal,
|
|
182
|
-
|
|
182
|
+
timeoutSignal =>
|
|
183
|
+
targetSession.respondAsBackground({
|
|
184
|
+
from: senderId,
|
|
185
|
+
message,
|
|
186
|
+
awaitReply,
|
|
187
|
+
signal: timeoutSignal,
|
|
188
|
+
}),
|
|
189
|
+
target.id,
|
|
190
|
+
);
|
|
183
191
|
delivered.push(target.id);
|
|
184
192
|
if (awaitReply && result.replyText) {
|
|
185
193
|
replies.push({ from: target.id, text: result.replyText });
|
|
@@ -237,3 +245,49 @@ function errorResult(text: string, details: IrcDetails): AgentToolResult<IrcDeta
|
|
|
237
245
|
details,
|
|
238
246
|
};
|
|
239
247
|
}
|
|
248
|
+
|
|
249
|
+
function normalizeIrcTimeoutMs(value: number): number {
|
|
250
|
+
if (!Number.isFinite(value) || value === 0) return value === 0 ? 0 : DEFAULT_IRC_TIMEOUT_MS;
|
|
251
|
+
return Math.max(1, Math.trunc(value));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function runIrcDispatchWithTimeout<T>(
|
|
255
|
+
timeoutMs: number,
|
|
256
|
+
parentSignal: AbortSignal | undefined,
|
|
257
|
+
run: (signal?: AbortSignal) => Promise<T>,
|
|
258
|
+
targetId: string,
|
|
259
|
+
): Promise<T> {
|
|
260
|
+
if (timeoutMs <= 0) {
|
|
261
|
+
return await run(parentSignal);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const controller = new AbortController();
|
|
265
|
+
const timeoutError = new Error(`IRC timed out waiting for ${targetId} after ${timeoutMs} ms`);
|
|
266
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
267
|
+
let parentAbortListener: (() => void) | undefined;
|
|
268
|
+
|
|
269
|
+
const timeoutDeferred = Promise.withResolvers<never>();
|
|
270
|
+
if (parentSignal) {
|
|
271
|
+
if (parentSignal.aborted) {
|
|
272
|
+
throw parentSignal.reason instanceof Error ? parentSignal.reason : new Error("IRC aborted");
|
|
273
|
+
}
|
|
274
|
+
parentAbortListener = () => {
|
|
275
|
+
controller.abort(parentSignal.reason);
|
|
276
|
+
timeoutDeferred.reject(parentSignal.reason instanceof Error ? parentSignal.reason : new Error("IRC aborted"));
|
|
277
|
+
};
|
|
278
|
+
parentSignal.addEventListener("abort", parentAbortListener, { once: true });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
timeout = setTimeout(() => {
|
|
282
|
+
controller.abort(timeoutError);
|
|
283
|
+
timeoutDeferred.reject(timeoutError);
|
|
284
|
+
}, timeoutMs);
|
|
285
|
+
timeout.unref?.();
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
return await Promise.race([run(controller.signal), timeoutDeferred.promise]);
|
|
289
|
+
} finally {
|
|
290
|
+
if (timeout) clearTimeout(timeout);
|
|
291
|
+
if (parentSignal && parentAbortListener) parentSignal.removeEventListener("abort", parentAbortListener);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { computeLineHash } from "../hashline/hash";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Format a single line of match output for grep/ast-grep style results.
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Matched lines are prefixed with `*`; context lines are prefixed with a single
|
|
5
|
+
* space so line numbers align in column. In hashline mode the line uses the
|
|
6
|
+
* editable `LINE:content` shape under a file-hash header; in plain mode it keeps
|
|
7
|
+
* the legacy `LINE|content` display-only shape. Line numbers are never padded.
|
|
10
8
|
*/
|
|
11
9
|
export function formatMatchLine(
|
|
12
10
|
lineNumber: number,
|
|
@@ -16,7 +14,7 @@ export function formatMatchLine(
|
|
|
16
14
|
): string {
|
|
17
15
|
const marker = isMatch ? "*" : " ";
|
|
18
16
|
if (options.useHashLines) {
|
|
19
|
-
return `${marker}${lineNumber}
|
|
17
|
+
return `${marker}${lineNumber}:${line}`;
|
|
20
18
|
}
|
|
21
19
|
return `${marker}${lineNumber}|${line}`;
|
|
22
20
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared output-schema validation for subagent yield + executor finalization.
|
|
3
|
+
*
|
|
4
|
+
* Both the in-process `yield` tool (subagent side) and the executor's post-mortem
|
|
5
|
+
* finalize path (parent side) need to validate yield payloads against the agent's
|
|
6
|
+
* declared output schema. This module is the single source of truth for that
|
|
7
|
+
* pipeline — keeping the two callsites in lockstep so a schema accepted in-tool
|
|
8
|
+
* cannot be rejected post-mortem (or vice versa).
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
isValidJsonSchema,
|
|
12
|
+
type JsonSchemaValidationIssue,
|
|
13
|
+
type JsonSchemaValidationResult,
|
|
14
|
+
validateJsonSchemaValue,
|
|
15
|
+
} from "@oh-my-pi/pi-ai/utils/schema";
|
|
16
|
+
import { jtdToJsonSchema, normalizeSchema } from "./jtd-to-json-schema";
|
|
17
|
+
|
|
18
|
+
/** A validator bound to a specific output schema. */
|
|
19
|
+
export interface OutputValidator {
|
|
20
|
+
/** Run JSON Schema validation; returns the raw `success`/`issues` shape so callers may inspect every failure. */
|
|
21
|
+
validate(value: unknown): JsonSchemaValidationResult;
|
|
22
|
+
/** Top-level required property names. Empty if the schema has no `required` array at root. */
|
|
23
|
+
readonly requiredFields: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BuildOutputValidatorResult {
|
|
27
|
+
/** Present when the schema produced a usable validator (i.e. constraining schemas). Absent for missing/unconstrained schemas. */
|
|
28
|
+
validator?: OutputValidator;
|
|
29
|
+
/** Raw JSON Schema produced by `jtdToJsonSchema`. Available alongside the validator so callers can derive related artifacts (strict-mode probe, dereference, hint text). */
|
|
30
|
+
jsonSchema?: Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Normalized schema (post-`normalizeSchema`). Surfaced so callers can distinguish
|
|
33
|
+
* "no schema provided" (`undefined`) from "intentionally unconstrained" (`true`)
|
|
34
|
+
* when both produce no validator.
|
|
35
|
+
*/
|
|
36
|
+
normalized?: unknown;
|
|
37
|
+
/** Set when the schema cannot be used. Callers should treat this as a "no validation" case (loose acceptance) and surface the message in diagnostics. */
|
|
38
|
+
error?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build the canonical validator for a JTD-or-JSON-Schema output declaration.
|
|
43
|
+
*
|
|
44
|
+
* Returns:
|
|
45
|
+
* - `{ validator, jsonSchema, normalized }` for constraining schemas — both callers use this path.
|
|
46
|
+
* - `{ normalized: true }` for an intentionally unconstrained schema (the JSON Schema literal `true`).
|
|
47
|
+
* No validator, but distinguishable from "no schema provided".
|
|
48
|
+
* - `{}` for an absent schema (`undefined`).
|
|
49
|
+
* - `{ error, normalized? }` when the schema cannot be honored (invalid syntax, `false`, malformed JTD).
|
|
50
|
+
*/
|
|
51
|
+
export function buildOutputValidator(schema: unknown): BuildOutputValidatorResult {
|
|
52
|
+
const { normalized, error: normalizeError } = normalizeSchema(schema);
|
|
53
|
+
if (normalizeError) return { error: normalizeError, normalized };
|
|
54
|
+
if (normalized === undefined) return {};
|
|
55
|
+
if (normalized === false) return { error: "boolean false schema rejects all outputs", normalized };
|
|
56
|
+
if (normalized === true) return { normalized };
|
|
57
|
+
|
|
58
|
+
const jsonSchema = jtdToJsonSchema(normalized);
|
|
59
|
+
if (jsonSchema === undefined) return { normalized };
|
|
60
|
+
if (jsonSchema === false) return { error: "boolean false schema rejects all outputs", normalized };
|
|
61
|
+
if (jsonSchema === true) return { normalized };
|
|
62
|
+
if (typeof jsonSchema !== "object" || Array.isArray(jsonSchema)) {
|
|
63
|
+
return { error: "invalid JSON schema", normalized };
|
|
64
|
+
}
|
|
65
|
+
if (!isValidJsonSchema(jsonSchema)) return { error: "invalid JSON schema", normalized };
|
|
66
|
+
|
|
67
|
+
const jsonSchemaRecord = jsonSchema as Record<string, unknown>;
|
|
68
|
+
const required = extractRequiredFields(jsonSchemaRecord);
|
|
69
|
+
return {
|
|
70
|
+
normalized,
|
|
71
|
+
jsonSchema: jsonSchemaRecord,
|
|
72
|
+
validator: {
|
|
73
|
+
requiredFields: required,
|
|
74
|
+
validate: value => validateJsonSchemaValue(jsonSchemaRecord, value),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Produce the executor's headline+missing-required summary from a failed validation. */
|
|
80
|
+
export function summarizeValidationFailure(
|
|
81
|
+
result: JsonSchemaValidationResult,
|
|
82
|
+
value: unknown,
|
|
83
|
+
requiredFields: readonly string[],
|
|
84
|
+
): { message: string; missingRequired: string[] } {
|
|
85
|
+
if (result.success) return { message: "", missingRequired: [] };
|
|
86
|
+
const missing = computeMissingRequired(requiredFields, value);
|
|
87
|
+
const message = formatValidationIssueHeadline(result.issues[0]) ?? "schema validation failed";
|
|
88
|
+
return { message, missingRequired: missing };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function extractRequiredFields(jsonSchema: unknown): string[] {
|
|
92
|
+
if (!jsonSchema || typeof jsonSchema !== "object") return [];
|
|
93
|
+
const required = (jsonSchema as { required?: unknown }).required;
|
|
94
|
+
return Array.isArray(required) ? required.filter((k): k is string => typeof k === "string") : [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function computeMissingRequired(required: readonly string[], value: unknown): string[] {
|
|
98
|
+
if (required.length === 0) return [];
|
|
99
|
+
if (value === null || value === undefined) return [...required];
|
|
100
|
+
if (typeof value !== "object" || Array.isArray(value)) return [];
|
|
101
|
+
const record = value as Record<string, unknown>;
|
|
102
|
+
return required.filter(key => !(key in record) || record[key] === undefined);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Format a single validation issue as `path.with.dots: message`.
|
|
107
|
+
*
|
|
108
|
+
* Used by the executor's post-mortem `schema_violation` headline — one line, dot-separated path,
|
|
109
|
+
* since the executor's error format already lists missing-required fields separately.
|
|
110
|
+
*/
|
|
111
|
+
export function formatValidationIssueHeadline(issue: JsonSchemaValidationIssue | undefined): string | undefined {
|
|
112
|
+
if (!issue) return undefined;
|
|
113
|
+
const path = issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)";
|
|
114
|
+
return `${path}: ${issue.message}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Format every validation issue as `path/with/slashes: message; ...`.
|
|
119
|
+
*
|
|
120
|
+
* Used by the yield tool's model-facing retry feedback — the model gets every problem at once so it
|
|
121
|
+
* can fix the entire output in one retry instead of iterating issue-by-issue. The slash separator
|
|
122
|
+
* mirrors JSON Pointer convention and disambiguates against fields whose names contain dots.
|
|
123
|
+
*/
|
|
124
|
+
export function formatAllValidationIssues(issues: ReadonlyArray<JsonSchemaValidationIssue> | undefined): string {
|
|
125
|
+
if (!issues || issues.length === 0) return "Unknown schema validation error.";
|
|
126
|
+
return issues
|
|
127
|
+
.map(issue => {
|
|
128
|
+
const path = issue.path.length === 0 ? "" : `${issue.path.map(seg => String(seg)).join("/")}: `;
|
|
129
|
+
return `${path}${issue.message}`;
|
|
130
|
+
})
|
|
131
|
+
.join("; ");
|
|
132
|
+
}
|
package/src/tools/read.ts
CHANGED
|
@@ -9,9 +9,10 @@ import { Text } from "@oh-my-pi/pi-tui";
|
|
|
9
9
|
import { getRemoteDir, logger, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import * as z from "zod/v4";
|
|
11
11
|
import { getFileReadCache } from "../edit/file-read-cache";
|
|
12
|
+
import { normalizeToLF } from "../edit/normalize";
|
|
12
13
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
13
14
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
14
|
-
import {
|
|
15
|
+
import { computeFileHash, formatHashlineHeader, formatNumberedLine, formatNumberedLines } from "../hashline/hash";
|
|
15
16
|
import { InternalUrlRouter } from "../internal-urls";
|
|
16
17
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
17
18
|
import type { InternalUrl } from "../internal-urls/types";
|
|
@@ -113,13 +114,50 @@ function prependLineNumbers(text: string, startNum: number): string {
|
|
|
113
114
|
return textLines.map((line, i) => `${startNum + i}|${line}`).join("\n");
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
interface HashlineHeaderContext {
|
|
118
|
+
header: string;
|
|
119
|
+
fileHash: string;
|
|
120
|
+
fullText: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildHashlineHeaderContext(displayPath: string, fullText: string): HashlineHeaderContext {
|
|
124
|
+
const normalized = normalizeToLF(fullText);
|
|
125
|
+
const fileHash = computeFileHash(normalized);
|
|
126
|
+
return {
|
|
127
|
+
header: formatHashlineHeader(displayPath, fileHash),
|
|
128
|
+
fileHash,
|
|
129
|
+
fullText: normalized,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function readHashlineHeaderContext(absolutePath: string, cwd: string): Promise<HashlineHeaderContext> {
|
|
134
|
+
const fullText = await Bun.file(absolutePath).text();
|
|
135
|
+
return buildHashlineHeaderContext(formatPathRelativeToCwd(absolutePath, cwd), fullText);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function prependHashlineHeader(text: string, context: HashlineHeaderContext | undefined): string {
|
|
139
|
+
return context ? `${context.header}\n${text}` : text;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function recordHashlineSnapshot(
|
|
143
|
+
session: ToolSession,
|
|
144
|
+
absolutePath: string | undefined,
|
|
145
|
+
context: HashlineHeaderContext | undefined,
|
|
146
|
+
): void {
|
|
147
|
+
if (!context || !absolutePath || !path.isAbsolute(absolutePath)) return;
|
|
148
|
+
getFileReadCache(session).recordContiguous(absolutePath, 1, context.fullText.split("\n"), {
|
|
149
|
+
fullText: context.fullText,
|
|
150
|
+
fileHash: context.fileHash,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
116
154
|
function formatTextWithMode(
|
|
117
155
|
text: string,
|
|
118
156
|
startNum: number,
|
|
119
157
|
shouldAddHashLines: boolean,
|
|
120
158
|
shouldAddLineNumbers: boolean,
|
|
121
159
|
): string {
|
|
122
|
-
if (shouldAddHashLines) return
|
|
160
|
+
if (shouldAddHashLines) return formatNumberedLines(text, startNum);
|
|
123
161
|
if (shouldAddLineNumbers) return prependLineNumbers(text, startNum);
|
|
124
162
|
return text;
|
|
125
163
|
}
|
|
@@ -150,7 +188,7 @@ function formatSingleLine(
|
|
|
150
188
|
shouldAddHashLines: boolean,
|
|
151
189
|
shouldAddLineNumbers: boolean,
|
|
152
190
|
): string {
|
|
153
|
-
if (shouldAddHashLines) return
|
|
191
|
+
if (shouldAddHashLines) return formatNumberedLine(line, text);
|
|
154
192
|
if (shouldAddLineNumbers) return `${line}|${text}`;
|
|
155
193
|
return text;
|
|
156
194
|
}
|
|
@@ -165,9 +203,7 @@ function formatMergedBraceLine(
|
|
|
165
203
|
): { model: string; display: string } {
|
|
166
204
|
const merged = `${headText.trimEnd()} .. ${tailText.trim()}`;
|
|
167
205
|
if (shouldAddHashLines) {
|
|
168
|
-
|
|
169
|
-
const end = formatLineHash(endLine, tailText);
|
|
170
|
-
return { model: `${start}-${end}${HL_BODY_SEP}${merged}`, display: merged };
|
|
206
|
+
return { model: `${startLine}-${endLine}:${merged}`, display: merged };
|
|
171
207
|
}
|
|
172
208
|
if (shouldAddLineNumbers) {
|
|
173
209
|
return { model: `${startLine}-${endLine}|${merged}`, display: merged };
|
|
@@ -180,17 +216,38 @@ function countTextLines(text: string): number {
|
|
|
180
216
|
return text.split("\n").length;
|
|
181
217
|
}
|
|
182
218
|
|
|
219
|
+
/** Inclusive line range describing one elided span in a structural summary. */
|
|
220
|
+
interface ElidedRange {
|
|
221
|
+
start: number;
|
|
222
|
+
end: number;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Sample ranges shown in the footer to demonstrate the multi-range syntax. */
|
|
226
|
+
const FOOTER_RANGE_SAMPLES = 2;
|
|
227
|
+
|
|
183
228
|
/**
|
|
184
229
|
* Footer appended to summarized reads telling the model how to recover the
|
|
185
230
|
* elided body. Without this hint, agents either ignore the `...`/`{ .. }`
|
|
186
|
-
* markers or burn a turn guessing the right selector (see issue #1046).
|
|
231
|
+
* markers or burn a turn guessing the right selector (see issue #1046). The
|
|
232
|
+
* footer demonstrates the multi-range selector syntax with concrete sample
|
|
233
|
+
* ranges drawn from the actual elision so the model re-reads only what it
|
|
234
|
+
* needs instead of falling back to `:raw` or whole-file reads.
|
|
187
235
|
*/
|
|
188
|
-
function formatSummaryElisionFooter(
|
|
189
|
-
|
|
190
|
-
|
|
236
|
+
function formatSummaryElisionFooter(
|
|
237
|
+
readPath: string,
|
|
238
|
+
elidedRanges: ReadonlyArray<ElidedRange>,
|
|
239
|
+
elidedLines: number,
|
|
240
|
+
): string {
|
|
241
|
+
if (elidedRanges.length === 0) return "";
|
|
191
242
|
const lineWord = elidedLines === 1 ? "line" : "lines";
|
|
192
|
-
const
|
|
193
|
-
|
|
243
|
+
const sampleCount = Math.min(elidedRanges.length, FOOTER_RANGE_SAMPLES);
|
|
244
|
+
const selector = elidedRanges
|
|
245
|
+
.slice(0, sampleCount)
|
|
246
|
+
.map(r => `${r.start}-${r.end}`)
|
|
247
|
+
.join(",");
|
|
248
|
+
const example = `${readPath}:${selector}`;
|
|
249
|
+
const tail = elidedRanges.length > sampleCount ? `, e.g. ${example}` : ` with ${example}`;
|
|
250
|
+
return `[${elidedLines} ${lineWord} elided; re-read needed ranges${tail}]`;
|
|
194
251
|
}
|
|
195
252
|
const READ_CHUNK_SIZE = 8 * 1024;
|
|
196
253
|
|
|
@@ -844,9 +901,18 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
844
901
|
|
|
845
902
|
const shouldAddHashLines = displayMode.hashLines;
|
|
846
903
|
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
904
|
+
const hashContext =
|
|
905
|
+
shouldAddHashLines && options.sourcePath
|
|
906
|
+
? buildHashlineHeaderContext(formatPathRelativeToCwd(options.sourcePath, this.session.cwd), text)
|
|
907
|
+
: undefined;
|
|
908
|
+
recordHashlineSnapshot(this.session, options.sourcePath, hashContext);
|
|
909
|
+
let emittedHashlineHeader = false;
|
|
847
910
|
const formatText = (content: string, startNum: number): string => {
|
|
848
911
|
details.displayContent = { text: content, startLine: startNum };
|
|
849
|
-
|
|
912
|
+
const formatted = formatTextWithMode(content, startNum, shouldAddHashLines, shouldAddLineNumbers);
|
|
913
|
+
if (!hashContext || emittedHashlineHeader) return formatted;
|
|
914
|
+
emittedHashlineHeader = true;
|
|
915
|
+
return prependHashlineHeader(formatted, hashContext);
|
|
850
916
|
};
|
|
851
917
|
|
|
852
918
|
let outputText: string;
|
|
@@ -862,7 +928,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
862
928
|
if (shouldAddHashLines) {
|
|
863
929
|
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
864
930
|
firstLineBytes,
|
|
865
|
-
)}, exceeds ${formatBytes(DEFAULT_MAX_BYTES)} limit. Hashline output requires full lines; cannot
|
|
931
|
+
)}, exceeds ${formatBytes(DEFAULT_MAX_BYTES)} limit. Hashline output requires full lines; cannot emit an editable numbered preview for a truncated line.]`;
|
|
866
932
|
} else {
|
|
867
933
|
outputText = formatText(snippet.text, startLineDisplay);
|
|
868
934
|
}
|
|
@@ -928,6 +994,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
928
994
|
const totalLines = allLines.length;
|
|
929
995
|
const shouldAddHashLines = displayMode.hashLines;
|
|
930
996
|
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
997
|
+
const hashContext =
|
|
998
|
+
shouldAddHashLines && options.sourcePath
|
|
999
|
+
? buildHashlineHeaderContext(formatPathRelativeToCwd(options.sourcePath, this.session.cwd), text)
|
|
1000
|
+
: undefined;
|
|
1001
|
+
recordHashlineSnapshot(this.session, options.sourcePath, hashContext);
|
|
1002
|
+
let emittedHashlineHeader = false;
|
|
931
1003
|
|
|
932
1004
|
const resultBuilder = toolResult(details);
|
|
933
1005
|
if (options.sourcePath) resultBuilder.sourcePath(options.sourcePath);
|
|
@@ -943,7 +1015,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
943
1015
|
}
|
|
944
1016
|
const effectiveEnd = Math.min(range.endLine ?? totalLines, totalLines);
|
|
945
1017
|
const sliced = allLines.slice(range.startLine - 1, effectiveEnd).join("\n");
|
|
946
|
-
|
|
1018
|
+
const formatted = formatTextWithMode(sliced, range.startLine, shouldAddHashLines, shouldAddLineNumbers);
|
|
1019
|
+
parts.push(hashContext && !emittedHashlineHeader ? prependHashlineHeader(formatted, hashContext) : formatted);
|
|
1020
|
+
if (hashContext) emittedHashlineHeader = true;
|
|
947
1021
|
}
|
|
948
1022
|
|
|
949
1023
|
const outputText = parts.length > 0 ? parts.join("\n\n…\n\n") : "";
|
|
@@ -1002,6 +1076,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1002
1076
|
|
|
1003
1077
|
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1004
1078
|
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1079
|
+
const hashContext = shouldAddHashLines
|
|
1080
|
+
? await readHashlineHeaderContext(absolutePath, this.session.cwd)
|
|
1081
|
+
: undefined;
|
|
1082
|
+
recordHashlineSnapshot(this.session, absolutePath, hashContext);
|
|
1083
|
+
let emittedHashlineHeader = false;
|
|
1005
1084
|
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
1006
1085
|
|
|
1007
1086
|
const blocks: string[] = [];
|
|
@@ -1042,11 +1121,18 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1042
1121
|
}
|
|
1043
1122
|
|
|
1044
1123
|
if (collectedLines.length > 0) {
|
|
1045
|
-
getFileReadCache(this.session).recordContiguous(
|
|
1124
|
+
getFileReadCache(this.session).recordContiguous(
|
|
1125
|
+
absolutePath,
|
|
1126
|
+
range.startLine,
|
|
1127
|
+
collectedLines,
|
|
1128
|
+
hashContext ? { fullText: hashContext.fullText, fileHash: hashContext.fileHash } : {},
|
|
1129
|
+
);
|
|
1046
1130
|
}
|
|
1047
1131
|
|
|
1048
1132
|
const blockText = collectedLines.join("\n");
|
|
1049
|
-
|
|
1133
|
+
const formatted = formatTextWithMode(blockText, range.startLine, shouldAddHashLines, shouldAddLineNumbers);
|
|
1134
|
+
blocks.push(hashContext && !emittedHashlineHeader ? prependHashlineHeader(formatted, hashContext) : formatted);
|
|
1135
|
+
if (hashContext) emittedHashlineHeader = true;
|
|
1050
1136
|
}
|
|
1051
1137
|
|
|
1052
1138
|
let outputText = blocks.join("\n\n…\n\n");
|
|
@@ -1335,7 +1421,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1335
1421
|
#renderSummary(summary: SummaryResult): {
|
|
1336
1422
|
text: string;
|
|
1337
1423
|
displayText: string;
|
|
1338
|
-
|
|
1424
|
+
elidedRanges: ElidedRange[];
|
|
1339
1425
|
elidedLines: number;
|
|
1340
1426
|
} {
|
|
1341
1427
|
const displayMode = resolveFileDisplayMode(this.session);
|
|
@@ -1396,13 +1482,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1396
1482
|
|
|
1397
1483
|
const modelParts: string[] = [];
|
|
1398
1484
|
const displayParts: string[] = [];
|
|
1399
|
-
|
|
1485
|
+
const elidedRanges: ElidedRange[] = [];
|
|
1400
1486
|
let elidedLines = 0;
|
|
1401
1487
|
for (const unit of units) {
|
|
1402
1488
|
if (unit.kind === "elided") {
|
|
1403
1489
|
modelParts.push("...");
|
|
1404
1490
|
displayParts.push("...");
|
|
1405
|
-
|
|
1491
|
+
elidedRanges.push({ start: unit.startLine, end: unit.endLine });
|
|
1406
1492
|
elidedLines += unit.endLine - unit.startLine + 1;
|
|
1407
1493
|
continue;
|
|
1408
1494
|
}
|
|
@@ -1417,7 +1503,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1417
1503
|
);
|
|
1418
1504
|
modelParts.push(formatted.model);
|
|
1419
1505
|
displayParts.push(formatted.display);
|
|
1420
|
-
|
|
1506
|
+
// Suggest the full brace range so re-reading shows both braces
|
|
1507
|
+
// plus the elided body in one shot.
|
|
1508
|
+
elidedRanges.push({ start: unit.startLine, end: unit.endLine });
|
|
1421
1509
|
// Merged brace pair encloses (start+1)..(end-1) as elided.
|
|
1422
1510
|
elidedLines += Math.max(0, unit.endLine - unit.startLine - 1);
|
|
1423
1511
|
continue;
|
|
@@ -1426,7 +1514,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1426
1514
|
displayParts.push(unit.text);
|
|
1427
1515
|
}
|
|
1428
1516
|
|
|
1429
|
-
return { text: modelParts.join("\n"), displayText: displayParts.join("\n"),
|
|
1517
|
+
return { text: modelParts.join("\n"), displayText: displayParts.join("\n"), elidedRanges, elidedLines };
|
|
1430
1518
|
}
|
|
1431
1519
|
|
|
1432
1520
|
async execute(
|
|
@@ -1674,15 +1762,20 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1674
1762
|
const renderedSummary = this.#renderSummary(summary);
|
|
1675
1763
|
const footer = formatSummaryElisionFooter(
|
|
1676
1764
|
localReadPath,
|
|
1677
|
-
renderedSummary.
|
|
1765
|
+
renderedSummary.elidedRanges,
|
|
1678
1766
|
renderedSummary.elidedLines,
|
|
1679
1767
|
);
|
|
1680
|
-
const
|
|
1768
|
+
const summaryHashContext = displayMode.hashLines
|
|
1769
|
+
? await readHashlineHeaderContext(absolutePath, this.session.cwd)
|
|
1770
|
+
: undefined;
|
|
1771
|
+
recordHashlineSnapshot(this.session, absolutePath, summaryHashContext);
|
|
1772
|
+
const bodyText = footer ? `${renderedSummary.text}\n\n${footer}` : renderedSummary.text;
|
|
1773
|
+
const modelText = prependHashlineHeader(bodyText, summaryHashContext);
|
|
1681
1774
|
details = {
|
|
1682
1775
|
displayContent: { text: renderedSummary.displayText, startLine: 1 },
|
|
1683
1776
|
summary: {
|
|
1684
1777
|
lines: countTextLines(renderedSummary.text),
|
|
1685
|
-
elidedSpans: renderedSummary.
|
|
1778
|
+
elidedSpans: renderedSummary.elidedRanges.length,
|
|
1686
1779
|
elidedLines: renderedSummary.elidedLines,
|
|
1687
1780
|
},
|
|
1688
1781
|
};
|
|
@@ -1820,16 +1913,29 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1820
1913
|
firstLineExceedsLimit,
|
|
1821
1914
|
};
|
|
1822
1915
|
|
|
1916
|
+
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1917
|
+
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1918
|
+
const hashContext = shouldAddHashLines
|
|
1919
|
+
? await readHashlineHeaderContext(absolutePath, this.session.cwd)
|
|
1920
|
+
: undefined;
|
|
1921
|
+
|
|
1823
1922
|
if (collectedLines.length > 0 && !firstLineExceedsLimit) {
|
|
1824
|
-
getFileReadCache(this.session).recordContiguous(
|
|
1923
|
+
getFileReadCache(this.session).recordContiguous(
|
|
1924
|
+
absolutePath,
|
|
1925
|
+
startLineDisplay,
|
|
1926
|
+
collectedLines,
|
|
1927
|
+
hashContext ? { fullText: hashContext.fullText, fileHash: hashContext.fileHash } : {},
|
|
1928
|
+
);
|
|
1825
1929
|
}
|
|
1826
1930
|
|
|
1827
|
-
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1828
|
-
const shouldAddLineNumbers = rawSelector ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1829
1931
|
let capturedDisplayContent: { text: string; startLine: number } | undefined;
|
|
1932
|
+
let emittedHashlineHeader = false;
|
|
1830
1933
|
const formatText = (text: string, startNum: number): string => {
|
|
1831
1934
|
capturedDisplayContent = { text, startLine: startNum };
|
|
1832
|
-
|
|
1935
|
+
const formatted = formatTextWithMode(text, startNum, shouldAddHashLines, shouldAddLineNumbers);
|
|
1936
|
+
if (!hashContext || emittedHashlineHeader) return formatted;
|
|
1937
|
+
emittedHashlineHeader = true;
|
|
1938
|
+
return prependHashlineHeader(formatted, hashContext);
|
|
1833
1939
|
};
|
|
1834
1940
|
|
|
1835
1941
|
let outputText: string;
|
|
@@ -1841,7 +1947,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1841
1947
|
if (shouldAddHashLines) {
|
|
1842
1948
|
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
1843
1949
|
firstLineBytes,
|
|
1844
|
-
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Hashline output requires full lines; cannot
|
|
1950
|
+
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Hashline output requires full lines; cannot emit an editable numbered preview for a truncated line.]`;
|
|
1845
1951
|
} else {
|
|
1846
1952
|
outputText = formatText(snippet.text, startLineDisplay);
|
|
1847
1953
|
}
|
|
@@ -1964,7 +2070,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1964
2070
|
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1965
2071
|
|
|
1966
2072
|
const rawText = region.lines.join("\n");
|
|
1967
|
-
const
|
|
2073
|
+
const hashContext = shouldAddHashLines
|
|
2074
|
+
? await readHashlineHeaderContext(entry.absolutePath, this.session.cwd)
|
|
2075
|
+
: undefined;
|
|
2076
|
+
recordHashlineSnapshot(this.session, entry.absolutePath, hashContext);
|
|
2077
|
+
const formattedBody = formatTextWithMode(rawText, region.startLine, shouldAddHashLines, shouldAddLineNumbers);
|
|
2078
|
+
const formattedText = prependHashlineHeader(formattedBody, hashContext);
|
|
1968
2079
|
|
|
1969
2080
|
const details: ReadToolDetails = {
|
|
1970
2081
|
resolvedPath: entry.absolutePath,
|
package/src/tools/review.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { isRecord } from "@oh-my-pi/pi-utils";
|
|
|
15
15
|
import * as z from "zod/v4";
|
|
16
16
|
import type { Theme, ThemeColor } from "../modes/theme/theme";
|
|
17
17
|
import { subprocessToolRegistry } from "../task/subprocess-tool-registry";
|
|
18
|
+
import type { ReviewFinding } from "../task/types";
|
|
18
19
|
export type FindingPriority = "P0" | "P1" | "P2" | "P3";
|
|
19
20
|
|
|
20
21
|
export interface FindingPriorityInfo {
|
|
@@ -186,6 +187,28 @@ export interface SubmitReviewDetails {
|
|
|
186
187
|
|
|
187
188
|
// Re-export types for external use
|
|
188
189
|
export type { ReportFindingDetails };
|
|
190
|
+
/**
|
|
191
|
+
* Coerce a tool-side `ReportFindingDetails` into the cross-boundary
|
|
192
|
+
* `ReviewFinding` shape consumed by the reviewer agent's JTD output schema.
|
|
193
|
+
*
|
|
194
|
+
* The `report_finding` tool exposes `priority` as a string enum (`"P0".."P3"`)
|
|
195
|
+
* for ergonomics, but the bundled reviewer schema (and every custom review
|
|
196
|
+
* agent that mirrors it) declares `priority: number`. Without this coercion
|
|
197
|
+
* the auto-populated `findings[]` fails JTD validation and every review run
|
|
198
|
+
* that surfaces a finding is rejected with `findings.0.priority: expected
|
|
199
|
+
* number, received string`.
|
|
200
|
+
*/
|
|
201
|
+
export function toReviewFinding(details: ReportFindingDetails): ReviewFinding {
|
|
202
|
+
return {
|
|
203
|
+
title: details.title,
|
|
204
|
+
body: details.body,
|
|
205
|
+
priority: getPriorityInfo(details.priority).ord,
|
|
206
|
+
confidence: details.confidence,
|
|
207
|
+
file_path: details.file_path,
|
|
208
|
+
line_start: details.line_start,
|
|
209
|
+
line_end: details.line_end,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
189
212
|
|
|
190
213
|
// Register report_finding handler
|
|
191
214
|
subprocessToolRegistry.register<ReportFindingDetails>("report_finding", {
|
|
@@ -19,12 +19,6 @@ import type { ToolSession } from ".";
|
|
|
19
19
|
import { formatCount, replaceTabs, TRUNCATE_LENGTHS } from "./render-utils";
|
|
20
20
|
import { ToolError } from "./tool-errors";
|
|
21
21
|
|
|
22
|
-
// Re-export legacy MCP types for back-compat (tests and external callers may reference them)
|
|
23
|
-
export type {
|
|
24
|
-
DiscoverableMCPSearchIndex,
|
|
25
|
-
DiscoverableMCPTool,
|
|
26
|
-
} from "../mcp/discoverable-tool-metadata";
|
|
27
|
-
|
|
28
22
|
const DEFAULT_LIMIT = 8;
|
|
29
23
|
const TOOL_DISCOVERY_TITLE = "Tool Discovery";
|
|
30
24
|
const COLLAPSED_MATCH_LIMIT = 5;
|
|
@@ -81,21 +75,7 @@ function buildSearchToolBm25Content(details: SearchToolBm25Details): string {
|
|
|
81
75
|
/** Get discoverable tools for description rendering. Falls back to empty array on error. */
|
|
82
76
|
function getDiscoverableToolsForDescription(session: ToolSession): DiscoverableTool[] {
|
|
83
77
|
try {
|
|
84
|
-
|
|
85
|
-
if (session.getDiscoverableTools) {
|
|
86
|
-
return session.getDiscoverableTools();
|
|
87
|
-
}
|
|
88
|
-
// Legacy MCP path — adapt DiscoverableMCPTool (with `description`) → DiscoverableTool.
|
|
89
|
-
const legacy = session.getDiscoverableMCPTools?.() ?? [];
|
|
90
|
-
return legacy.map(t => ({
|
|
91
|
-
name: t.name,
|
|
92
|
-
label: t.label,
|
|
93
|
-
summary: t.description,
|
|
94
|
-
source: "mcp" as const,
|
|
95
|
-
serverName: t.serverName,
|
|
96
|
-
mcpToolName: t.mcpToolName,
|
|
97
|
-
schemaKeys: t.schemaKeys,
|
|
98
|
-
}));
|
|
78
|
+
return session.getDiscoverableTools?.() ?? [];
|
|
99
79
|
} catch {
|
|
100
80
|
return [];
|
|
101
81
|
}
|
|
@@ -103,15 +83,8 @@ function getDiscoverableToolsForDescription(session: ToolSession): DiscoverableT
|
|
|
103
83
|
|
|
104
84
|
function getDiscoverableToolSearchIndexForExecution(session: ToolSession): DiscoverableToolSearchIndex {
|
|
105
85
|
try {
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
const cached = session.getDiscoverableToolSearchIndex();
|
|
109
|
-
if (cached) return cached;
|
|
110
|
-
}
|
|
111
|
-
// Legacy MCP: use cached MCP index. Its documents expose `tool.description` as well as
|
|
112
|
-
// `tool.summary`, so it is structurally compatible with DiscoverableToolSearchIndex.
|
|
113
|
-
const mcpCached = session.getDiscoverableMCPSearchIndex?.();
|
|
114
|
-
if (mcpCached) return mcpCached as unknown as DiscoverableToolSearchIndex;
|
|
86
|
+
const cached = session.getDiscoverableToolSearchIndex?.();
|
|
87
|
+
if (cached) return cached;
|
|
115
88
|
} catch {}
|
|
116
89
|
return buildDiscoverableToolSearchIndex(getDiscoverableToolsForDescription(session));
|
|
117
90
|
}
|