@oh-my-pi/pi-coding-agent 15.3.1 → 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 +119 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +1 -1
- 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/task/types.d.ts +8 -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/auth-gateway-cli.ts +71 -2
- package/src/cli/file-processor.ts +12 -2
- package/src/cli.ts +0 -8
- package/src/commands/auth-gateway.ts +2 -0
- 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/extensibility/plugins/marketplace/manager.ts +20 -1
- 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 +9 -8
- package/src/lsp/config.ts +87 -22
- 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 +21 -24
- 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 +55 -57
- package/src/task/index.ts +8 -4
- package/src/task/render.ts +53 -1
- package/src/task/types.ts +8 -0
- 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/jtd-to-json-schema.ts +5 -1
- package/src/tools/match-line-format.ts +5 -7
- package/src/tools/output-schema-validator.ts +132 -0
- package/src/tools/read.ts +142 -63
- 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 -212
- 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/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
|
}
|
package/src/tools/search.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
|
9
9
|
import * as z from "zod/v4";
|
|
10
10
|
import { getFileReadCache } from "../edit/file-read-cache";
|
|
11
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
12
|
+
import { computeFileHash, formatHashlineHeader } from "../hashline/hash";
|
|
12
13
|
import type { Theme } from "../modes/theme/theme";
|
|
13
14
|
import searchDescription from "../prompts/tools/search.md" with { type: "text" };
|
|
14
15
|
import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead } from "../session/streaming-output";
|
|
@@ -38,13 +39,13 @@ import {
|
|
|
38
39
|
import { ToolError } from "./tool-errors";
|
|
39
40
|
import { toolResult } from "./tool-result";
|
|
40
41
|
|
|
42
|
+
const searchPathEntrySchema = z.string().describe("file, directory, glob, or internal URL to search");
|
|
41
43
|
const searchSchema = z
|
|
42
44
|
.object({
|
|
43
45
|
pattern: z.string().describe("regex pattern"),
|
|
44
46
|
paths: z
|
|
45
|
-
.
|
|
46
|
-
.
|
|
47
|
-
.describe("files, directories, globs, or internal URLs to search"),
|
|
47
|
+
.union([searchPathEntrySchema, z.array(searchPathEntrySchema).min(1)])
|
|
48
|
+
.describe("file, directory, glob, internal URL, or array of those to search"),
|
|
48
49
|
i: z.boolean().optional().describe("case-insensitive search"),
|
|
49
50
|
gitignore: z.boolean().optional().describe("respect gitignore"),
|
|
50
51
|
skip: z
|
|
@@ -55,6 +56,9 @@ const searchSchema = z
|
|
|
55
56
|
.strict();
|
|
56
57
|
|
|
57
58
|
export type SearchToolInput = z.infer<typeof searchSchema>;
|
|
59
|
+
export function toPathList(input: string | string[] | undefined): string[] {
|
|
60
|
+
return typeof input === "string" ? [input] : (input ?? []);
|
|
61
|
+
}
|
|
58
62
|
|
|
59
63
|
/** Maximum number of distinct files surfaced in a single response. The
|
|
60
64
|
* agent paginates further pages via `skip`. */
|
|
@@ -236,7 +240,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
236
240
|
_onUpdate?: AgentToolUpdateCallback<SearchToolDetails>,
|
|
237
241
|
_toolContext?: AgentToolContext,
|
|
238
242
|
): Promise<AgentToolResult<SearchToolDetails>> {
|
|
239
|
-
const { pattern, paths, i, gitignore, skip } = params;
|
|
243
|
+
const { pattern, paths: rawPaths, i, gitignore, skip } = params;
|
|
240
244
|
|
|
241
245
|
return untilAborted(signal, async () => {
|
|
242
246
|
const normalizedPattern = pattern.trim();
|
|
@@ -248,6 +252,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
248
252
|
if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) {
|
|
249
253
|
throw new ToolError("Skip must be a non-negative number");
|
|
250
254
|
}
|
|
255
|
+
const paths = toPathList(rawPaths);
|
|
251
256
|
for (const entry of paths) {
|
|
252
257
|
if (containsTopLevelComma(entry)) {
|
|
253
258
|
throw new ToolError('paths is an array — pass ["a", "b"] not ["a,b"]');
|
|
@@ -303,7 +308,6 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
303
308
|
}
|
|
304
309
|
const { globFilter } = scope;
|
|
305
310
|
const baseDisplayMode = resolveFileDisplayMode(this.session);
|
|
306
|
-
const immutableDisplayMode = resolveFileDisplayMode(this.session, { immutable: true });
|
|
307
311
|
|
|
308
312
|
const effectiveOutputMode = GrepOutputMode.Content;
|
|
309
313
|
// Multi-scope = more than one file may match. We fetch up to
|
|
@@ -485,14 +489,27 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
485
489
|
matchesByFile.get(relativePath)!.push(match);
|
|
486
490
|
}
|
|
487
491
|
const displayLines: string[] = [];
|
|
492
|
+
const hashContexts = new Map<string, { absolutePath: string; fileHash: string }>();
|
|
493
|
+
if (baseDisplayMode.hashLines) {
|
|
494
|
+
for (const relativePath of fileList) {
|
|
495
|
+
if (archiveDisplaySet.has(relativePath)) continue;
|
|
496
|
+
const absoluteFilePath = path.resolve(this.session.cwd, relativePath);
|
|
497
|
+
if (immutableSourcePaths.has(absoluteFilePath)) continue;
|
|
498
|
+
try {
|
|
499
|
+
const fullText = await Bun.file(absoluteFilePath).text();
|
|
500
|
+
const fileHash = computeFileHash(fullText);
|
|
501
|
+
hashContexts.set(relativePath, { absolutePath: absoluteFilePath, fileHash });
|
|
502
|
+
} catch {
|
|
503
|
+
// Best-effort: if the file disappeared between grep and render, fall back to plain line output.
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
488
507
|
const renderMatchesForFile = (relativePath: string): { model: string[]; display: string[] } => {
|
|
489
508
|
const modelOut: string[] = [];
|
|
490
509
|
const displayOut: string[] = [];
|
|
491
510
|
const fileMatches = matchesByFile.get(relativePath) ?? [];
|
|
492
|
-
const
|
|
493
|
-
const useHashLines =
|
|
494
|
-
? immutableDisplayMode.hashLines
|
|
495
|
-
: baseDisplayMode.hashLines;
|
|
511
|
+
const hashContext = hashContexts.get(relativePath);
|
|
512
|
+
const useHashLines = hashContext !== undefined;
|
|
496
513
|
const lineNumberWidth = fileMatches.reduce((width, match) => {
|
|
497
514
|
let nextWidth = Math.max(width, String(match.lineNumber).length);
|
|
498
515
|
for (const ctx of match.contextBefore ?? []) {
|
|
@@ -533,17 +550,21 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
533
550
|
}
|
|
534
551
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
535
552
|
}
|
|
536
|
-
if (cacheEntries.length > 0 &&
|
|
537
|
-
getFileReadCache(this.session).recordSparse(
|
|
553
|
+
if (cacheEntries.length > 0 && hashContext) {
|
|
554
|
+
getFileReadCache(this.session).recordSparse(hashContext.absolutePath, cacheEntries, {
|
|
555
|
+
fileHash: hashContext.fileHash,
|
|
556
|
+
});
|
|
538
557
|
}
|
|
539
558
|
return { model: modelOut, display: displayOut };
|
|
540
559
|
};
|
|
541
560
|
if (isDirectory) {
|
|
542
561
|
const grouped = formatGroupedFiles(fileList, relativePath => {
|
|
543
562
|
const rendered = renderMatchesForFile(relativePath);
|
|
563
|
+
const hashContext = hashContexts.get(relativePath);
|
|
544
564
|
return {
|
|
545
565
|
modelLines: rendered.model,
|
|
546
566
|
displayLines: rendered.display,
|
|
567
|
+
headerSuffix: hashContext ? `#${hashContext.fileHash}` : "",
|
|
547
568
|
skip: rendered.model.length === 0,
|
|
548
569
|
};
|
|
549
570
|
});
|
|
@@ -552,6 +573,15 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
552
573
|
} else {
|
|
553
574
|
for (const relativePath of fileList) {
|
|
554
575
|
const rendered = renderMatchesForFile(relativePath);
|
|
576
|
+
if (rendered.model.length === 0) continue;
|
|
577
|
+
if (outputLines.length > 0) {
|
|
578
|
+
outputLines.push("");
|
|
579
|
+
displayLines.push("");
|
|
580
|
+
}
|
|
581
|
+
const hashContext = hashContexts.get(relativePath);
|
|
582
|
+
if (hashContext) {
|
|
583
|
+
outputLines.push(formatHashlineHeader(relativePath, hashContext.fileHash));
|
|
584
|
+
}
|
|
555
585
|
outputLines.push(...rendered.model);
|
|
556
586
|
displayLines.push(...rendered.display);
|
|
557
587
|
}
|
|
@@ -607,7 +637,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
607
637
|
|
|
608
638
|
interface SearchRenderArgs {
|
|
609
639
|
pattern: string;
|
|
610
|
-
paths?: string[];
|
|
640
|
+
paths?: string | string[];
|
|
611
641
|
i?: boolean;
|
|
612
642
|
gitignore?: boolean;
|
|
613
643
|
skip?: number;
|
|
@@ -618,8 +648,9 @@ const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
|
618
648
|
export const searchToolRenderer = {
|
|
619
649
|
inline: true,
|
|
620
650
|
renderCall(args: SearchRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
651
|
+
const paths = toPathList(args.paths);
|
|
621
652
|
const meta: string[] = [];
|
|
622
|
-
if (
|
|
653
|
+
if (paths.length) meta.push(`in ${paths.join(", ")}`);
|
|
623
654
|
if (args.i) meta.push("case:insensitive");
|
|
624
655
|
if (args.gitignore === false) meta.push("gitignore:false");
|
|
625
656
|
if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
|
|
@@ -745,11 +776,12 @@ export const searchToolRenderer = {
|
|
|
745
776
|
let contextDir = searchBase ?? "";
|
|
746
777
|
return group.map(line => {
|
|
747
778
|
if (line.startsWith("## ")) {
|
|
748
|
-
// Strip optional ` (suffix)`
|
|
779
|
+
// Strip optional ` (suffix)` and `#hash` before resolving.
|
|
749
780
|
const fileName = line
|
|
750
781
|
.slice(3)
|
|
751
782
|
.trimEnd()
|
|
752
|
-
.replace(/\s+\([^)]*\)\s*$/, "")
|
|
783
|
+
.replace(/\s+\([^)]*\)\s*$/, "")
|
|
784
|
+
.replace(/#[0-9a-f]+$/, "");
|
|
753
785
|
const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
|
|
754
786
|
const styled = uiTheme.fg("dim", line);
|
|
755
787
|
return absPath ? fileHyperlink(absPath, styled) : styled;
|
|
@@ -760,7 +792,7 @@ export const searchToolRenderer = {
|
|
|
760
792
|
.trimEnd()
|
|
761
793
|
.replace(/\s+\([^)]*\)\s*$/, "");
|
|
762
794
|
const isDirectory = raw.endsWith("/");
|
|
763
|
-
const name = raw.replace(/\/$/, "");
|
|
795
|
+
const name = isDirectory ? raw.replace(/\/$/, "") : raw.replace(/#[0-9a-f]+$/, "");
|
|
764
796
|
if (isDirectory) {
|
|
765
797
|
if (searchBase) {
|
|
766
798
|
contextDir = name === "." ? searchBase : path.join(searchBase, name);
|
package/src/tools/write.ts
CHANGED
|
@@ -74,8 +74,8 @@ export interface WriteToolDetails {
|
|
|
74
74
|
/**
|
|
75
75
|
* Strip hashline display prefixes from write content.
|
|
76
76
|
*
|
|
77
|
-
* Only active when hashline edit mode is enabled — the model sees `
|
|
78
|
-
* prefixes in read output and sometimes copies them into write content.
|
|
77
|
+
* Only active when hashline edit mode is enabled — the model sees `¶PATH#HASH`
|
|
78
|
+
* headers plus `LINE:` prefixes in read output and sometimes copies them into write content.
|
|
79
79
|
*/
|
|
80
80
|
function stripWriteContent(session: ToolSession, content: string): { text: string; stripped: boolean } {
|
|
81
81
|
if (!resolveFileDisplayMode(session).hashLines) {
|
|
@@ -658,7 +658,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
658
658
|
context?: AgentToolContext,
|
|
659
659
|
): Promise<AgentToolResult<WriteToolDetails>> {
|
|
660
660
|
return untilAborted(signal, async () => {
|
|
661
|
-
// Strip hashline display prefixes (LINE
|
|
661
|
+
// Strip hashline display prefixes (¶PATH#HASH + LINE:) if the model copied them from read output
|
|
662
662
|
const { text: cleanContent, stripped } = stripWriteContent(this.session, content);
|
|
663
663
|
const internalRouter = InternalUrlRouter.instance();
|
|
664
664
|
if (internalRouter.canHandle(path)) {
|
package/src/tools/yield.ts
CHANGED
|
@@ -8,15 +8,13 @@ import type { TSchema } from "@oh-my-pi/pi-ai/types";
|
|
|
8
8
|
import {
|
|
9
9
|
dereferenceJsonSchema,
|
|
10
10
|
isValidJsonSchema,
|
|
11
|
-
type JsonSchemaValidationIssue,
|
|
12
11
|
type JsonSchemaValidationResult,
|
|
13
12
|
sanitizeSchemaForStrictMode,
|
|
14
13
|
tryEnforceStrictSchema,
|
|
15
|
-
validateJsonSchemaValue,
|
|
16
14
|
} from "@oh-my-pi/pi-ai/utils/schema";
|
|
17
15
|
import { subprocessToolRegistry } from "../task/subprocess-tool-registry";
|
|
18
16
|
import type { ToolSession } from ".";
|
|
19
|
-
import {
|
|
17
|
+
import { buildOutputValidator, formatAllValidationIssues } from "./output-schema-validator";
|
|
20
18
|
|
|
21
19
|
export interface YieldDetails {
|
|
22
20
|
data: unknown;
|
|
@@ -34,16 +32,6 @@ function formatSchema(schema: unknown): string {
|
|
|
34
32
|
}
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
function formatJsonSchemaIssues(issues: ReadonlyArray<JsonSchemaValidationIssue> | undefined): string {
|
|
38
|
-
if (!issues || issues.length === 0) return "Unknown schema validation error.";
|
|
39
|
-
return issues
|
|
40
|
-
.map(issue => {
|
|
41
|
-
const path = issue.path.length === 0 ? "" : `${issue.path.map(seg => String(seg)).join("/")}: `;
|
|
42
|
-
return `${path}${issue.message}`;
|
|
43
|
-
})
|
|
44
|
-
.join("; ");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
35
|
function looseRecordSchema(description: string): Record<string, unknown> {
|
|
48
36
|
return {
|
|
49
37
|
type: "object",
|
|
@@ -100,6 +88,15 @@ function wrapYieldParameters(dataSchema: Record<string, unknown>): Record<string
|
|
|
100
88
|
};
|
|
101
89
|
}
|
|
102
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Max consecutive schema-validation failures before the yield tool overrides validation
|
|
93
|
+
* and lets non-conforming data through. The override is a safety net for schemas the
|
|
94
|
+
* JTD→JSON-Schema converter cannot fully express; it should not be reached during normal
|
|
95
|
+
* model retries. Three matches the existing "3 reminders" pattern elsewhere in the agent
|
|
96
|
+
* runtime.
|
|
97
|
+
*/
|
|
98
|
+
const MAX_SCHEMA_RETRIES = 3;
|
|
99
|
+
|
|
103
100
|
export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
104
101
|
readonly name = "yield";
|
|
105
102
|
readonly label = "Submit Result";
|
|
@@ -120,21 +117,14 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
120
117
|
let parameters: TSchema;
|
|
121
118
|
|
|
122
119
|
try {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (normalizedSchema !== undefined && normalizedSchema !== false && !schemaError) {
|
|
133
|
-
if (!isValidJsonSchema(normalizedSchema)) {
|
|
134
|
-
schemaError = "invalid JSON schema";
|
|
135
|
-
} else {
|
|
136
|
-
validate = value => validateJsonSchemaValue(normalizedSchema, value);
|
|
137
|
-
}
|
|
120
|
+
const {
|
|
121
|
+
validator,
|
|
122
|
+
jsonSchema: normalizedSchema,
|
|
123
|
+
normalized,
|
|
124
|
+
error: schemaError,
|
|
125
|
+
} = buildOutputValidator(session.outputSchema);
|
|
126
|
+
if (validator) {
|
|
127
|
+
validate = value => validator.validate(value);
|
|
138
128
|
}
|
|
139
129
|
|
|
140
130
|
const schemaHint = formatSchema(normalizedSchema ?? session.outputSchema);
|
|
@@ -142,21 +132,15 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
142
132
|
? `Structured JSON output (output schema invalid; accepting unconstrained object): ${schemaError}`
|
|
143
133
|
: `Structured output matching the schema:\n${schemaHint}`;
|
|
144
134
|
let sanitizedSchema: Record<string, unknown> | undefined;
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
normalizedSchema != null &&
|
|
148
|
-
typeof normalizedSchema === "object" &&
|
|
149
|
-
!Array.isArray(normalizedSchema)
|
|
150
|
-
) {
|
|
151
|
-
const normalizedRecord = normalizedSchema as Record<string, unknown>;
|
|
152
|
-
const strictProbe = tryEnforceStrictSchema(normalizedRecord);
|
|
135
|
+
if (!schemaError && normalizedSchema !== undefined) {
|
|
136
|
+
const strictProbe = tryEnforceStrictSchema(normalizedSchema);
|
|
153
137
|
if (strictProbe.strict) {
|
|
154
|
-
sanitizedSchema = sanitizeSchemaForStrictMode(
|
|
138
|
+
sanitizedSchema = sanitizeSchemaForStrictMode(normalizedSchema);
|
|
155
139
|
} else {
|
|
156
|
-
sanitizedSchema =
|
|
140
|
+
sanitizedSchema = normalizedSchema;
|
|
157
141
|
this.strict = false;
|
|
158
142
|
}
|
|
159
|
-
} else if (!schemaError &&
|
|
143
|
+
} else if (!schemaError && normalized === true) {
|
|
160
144
|
sanitizedSchema = {};
|
|
161
145
|
this.strict = false;
|
|
162
146
|
}
|
|
@@ -229,8 +213,15 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
229
213
|
const parsed = this.#validate(data);
|
|
230
214
|
if (!parsed.success) {
|
|
231
215
|
this.#schemaValidationFailures++;
|
|
232
|
-
if (this.#schemaValidationFailures <=
|
|
233
|
-
|
|
216
|
+
if (this.#schemaValidationFailures <= MAX_SCHEMA_RETRIES) {
|
|
217
|
+
const remaining = MAX_SCHEMA_RETRIES - this.#schemaValidationFailures;
|
|
218
|
+
const retryHint =
|
|
219
|
+
remaining > 0
|
|
220
|
+
? ` Call yield again with the corrected shape — ${remaining} retry attempt(s) remain before the schema constraint is dropped.`
|
|
221
|
+
: " Call yield again with the corrected shape — this is the final retry before the schema constraint is dropped.";
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Output does not match schema: ${formatAllValidationIssues(parsed.issues)}.${retryHint}`,
|
|
224
|
+
);
|
|
234
225
|
}
|
|
235
226
|
schemaValidationOverridden = true;
|
|
236
227
|
}
|
package/src/utils/edit-mode.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
2
2
|
|
|
3
|
-
export type EditMode = "replace" | "patch" | "hashline" | "
|
|
3
|
+
export type EditMode = "replace" | "patch" | "hashline" | "apply_patch";
|
|
4
4
|
|
|
5
5
|
export const DEFAULT_EDIT_MODE: EditMode = "hashline";
|
|
6
6
|
|
|
@@ -9,7 +9,6 @@ const EDIT_MODE_IDS = {
|
|
|
9
9
|
hashline: "hashline",
|
|
10
10
|
patch: "patch",
|
|
11
11
|
replace: "replace",
|
|
12
|
-
vim: "vim",
|
|
13
12
|
} as const satisfies Record<string, EditMode>;
|
|
14
13
|
|
|
15
14
|
export const EDIT_MODES = Object.keys(EDIT_MODE_IDS) as EditMode[];
|
|
@@ -12,7 +12,7 @@ import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
|
12
12
|
import { glob } from "@oh-my-pi/pi-natives";
|
|
13
13
|
import { fuzzyMatch } from "@oh-my-pi/pi-tui";
|
|
14
14
|
import { formatAge, formatBytes, readImageMetadata } from "@oh-my-pi/pi-utils";
|
|
15
|
-
import {
|
|
15
|
+
import { computeFileHash, formatHashlineHeader, formatNumberedLines } from "../hashline/hash";
|
|
16
16
|
import type { FileMentionMessage } from "../session/messages";
|
|
17
17
|
import {
|
|
18
18
|
DEFAULT_MAX_BYTES,
|
|
@@ -356,7 +356,7 @@ export async function generateFileMentionMessages(
|
|
|
356
356
|
const content = await Bun.file(absolutePath).text();
|
|
357
357
|
let { output, lineCount } = buildTextOutput(content);
|
|
358
358
|
if (options?.useHashLines) {
|
|
359
|
-
output =
|
|
359
|
+
output = `${formatHashlineHeader(resolvedPath, computeFileHash(content))}\n${formatNumberedLines(output)}`;
|
|
360
360
|
}
|
|
361
361
|
files.push({ path: resolvedPath, content: output, lineCount });
|
|
362
362
|
} catch {
|
package/src/web/kagi.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { withHardTimeout } from "./search/providers/utils";
|
|
3
3
|
|
|
4
4
|
const KAGI_SEARCH_URL = "https://kagi.com/api/v0/search";
|
|
5
5
|
|
|
@@ -97,6 +97,7 @@ function parseKagiErrorResponse(statusCode: number, responseText: string): KagiA
|
|
|
97
97
|
|
|
98
98
|
export interface KagiSearchOptions {
|
|
99
99
|
limit?: number;
|
|
100
|
+
sessionId?: string;
|
|
100
101
|
signal?: AbortSignal;
|
|
101
102
|
}
|
|
102
103
|
|
|
@@ -113,8 +114,12 @@ export interface KagiSearchResult {
|
|
|
113
114
|
relatedQuestions: string[];
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
export async function findKagiApiKey(
|
|
117
|
-
|
|
117
|
+
export async function findKagiApiKey(
|
|
118
|
+
authStorage: AuthStorage,
|
|
119
|
+
sessionId?: string,
|
|
120
|
+
signal?: AbortSignal,
|
|
121
|
+
): Promise<string | null> {
|
|
122
|
+
return (await authStorage.getApiKey("kagi", sessionId, { signal })) ?? null;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
function getAuthHeaders(apiKey: string): Record<string, string> {
|
|
@@ -124,8 +129,12 @@ function getAuthHeaders(apiKey: string): Record<string, string> {
|
|
|
124
129
|
};
|
|
125
130
|
}
|
|
126
131
|
|
|
127
|
-
export async function searchWithKagi(
|
|
128
|
-
|
|
132
|
+
export async function searchWithKagi(
|
|
133
|
+
query: string,
|
|
134
|
+
options: KagiSearchOptions = {},
|
|
135
|
+
authStorage: AuthStorage,
|
|
136
|
+
): Promise<KagiSearchResult> {
|
|
137
|
+
const apiKey = await findKagiApiKey(authStorage, options.sessionId, options.signal);
|
|
129
138
|
if (!apiKey) {
|
|
130
139
|
throw new KagiApiError("Kagi credentials not found. Set KAGI_API_KEY or login with 'omp /login kagi'.");
|
|
131
140
|
}
|
package/src/web/parallel.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import type { AgentStorage } from "../session/agent-storage";
|
|
2
3
|
import { findCredential, withHardTimeout } from "./search/providers/utils";
|
|
3
4
|
|
|
4
5
|
const PARALLEL_API_URL = "https://api.parallel.ai";
|
|
@@ -73,8 +74,8 @@ export class ParallelApiError extends Error {
|
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
export
|
|
77
|
-
return findCredential(getEnvApiKey("parallel"), "parallel");
|
|
77
|
+
export function findParallelApiKey(storage: AgentStorage | null | undefined): string | null {
|
|
78
|
+
return findCredential(storage, getEnvApiKey("parallel"), "parallel");
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
export function getParallelExtractContent(document: ParallelExtractDocument): string {
|
|
@@ -284,9 +285,10 @@ function parseExtractPayload(payload: unknown): ParallelExtractResult {
|
|
|
284
285
|
export async function searchWithParallel(
|
|
285
286
|
objective: string,
|
|
286
287
|
queries: string[],
|
|
287
|
-
options: ParallelSearchOptions
|
|
288
|
+
options: ParallelSearchOptions,
|
|
289
|
+
storage: AgentStorage | null | undefined,
|
|
288
290
|
): Promise<ParallelSearchResult> {
|
|
289
|
-
const apiKey =
|
|
291
|
+
const apiKey = findParallelApiKey(storage);
|
|
290
292
|
if (!apiKey) {
|
|
291
293
|
throw new ParallelApiError(
|
|
292
294
|
"Parallel credentials not found. Set PARALLEL_API_KEY or login with 'omp /login parallel'.",
|
|
@@ -316,9 +318,10 @@ export async function searchWithParallel(
|
|
|
316
318
|
|
|
317
319
|
export async function extractWithParallel(
|
|
318
320
|
urls: string[],
|
|
319
|
-
options: ParallelExtractOptions
|
|
321
|
+
options: ParallelExtractOptions,
|
|
322
|
+
storage: AgentStorage | null | undefined,
|
|
320
323
|
): Promise<ParallelExtractResult> {
|
|
321
|
-
const apiKey =
|
|
324
|
+
const apiKey = findParallelApiKey(storage);
|
|
322
325
|
if (!apiKey) {
|
|
323
326
|
throw new ParallelApiError(
|
|
324
327
|
"Parallel credentials not found. Set PARALLEL_API_KEY or login with 'omp /login parallel'.",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { ptree } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import type TurndownService from "turndown";
|
|
6
6
|
|
|
7
|
+
import type { AgentStorage } from "../../session/agent-storage";
|
|
7
8
|
import { ToolAbortError } from "../../tools/tool-errors";
|
|
8
9
|
|
|
9
10
|
export { formatNumber } from "@oh-my-pi/pi-utils";
|
|
@@ -19,7 +20,12 @@ export interface RenderResult {
|
|
|
19
20
|
notes: string[];
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
export type SpecialHandler = (
|
|
23
|
+
export type SpecialHandler = (
|
|
24
|
+
url: string,
|
|
25
|
+
timeout: number,
|
|
26
|
+
signal?: AbortSignal,
|
|
27
|
+
storage?: AgentStorage | null,
|
|
28
|
+
) => Promise<RenderResult | null>;
|
|
23
29
|
|
|
24
30
|
export const MAX_OUTPUT_CHARS = 500_000;
|
|
25
31
|
export const MAX_BYTES = 50 * 1024 * 1024;
|
|
@@ -3,6 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { ptree, Snowflake } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { settings } from "../../config/settings";
|
|
6
|
+
import type { AgentStorage } from "../../session/agent-storage";
|
|
6
7
|
import { throwIfAborted } from "../../tools/tool-errors";
|
|
7
8
|
import { ensureTool } from "../../utils/tools-manager";
|
|
8
9
|
import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../parallel";
|
|
@@ -101,6 +102,7 @@ export const handleYouTube: SpecialHandler = async (
|
|
|
101
102
|
url: string,
|
|
102
103
|
timeout: number,
|
|
103
104
|
userSignal?: AbortSignal,
|
|
105
|
+
storage?: AgentStorage | null,
|
|
104
106
|
): Promise<RenderResult | null> => {
|
|
105
107
|
throwIfAborted(userSignal);
|
|
106
108
|
const yt = parseYouTubeUrl(url);
|
|
@@ -112,14 +114,18 @@ export const handleYouTube: SpecialHandler = async (
|
|
|
112
114
|
const videoUrl = `https://www.youtube.com/watch?v=${yt.videoId}`;
|
|
113
115
|
|
|
114
116
|
// Prefer Parallel extract when credentials are available
|
|
115
|
-
if (settings.get("providers.parallelFetch") &&
|
|
117
|
+
if (settings.get("providers.parallelFetch") && findParallelApiKey(storage)) {
|
|
116
118
|
try {
|
|
117
|
-
const parallelResult = await extractWithParallel(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
const parallelResult = await extractWithParallel(
|
|
120
|
+
[videoUrl],
|
|
121
|
+
{
|
|
122
|
+
objective: "Extract the main content of this YouTube video page",
|
|
123
|
+
excerpts: true,
|
|
124
|
+
fullContent: false,
|
|
125
|
+
signal,
|
|
126
|
+
},
|
|
127
|
+
storage,
|
|
128
|
+
);
|
|
123
129
|
const firstDocument = parallelResult.results[0];
|
|
124
130
|
if (firstDocument) {
|
|
125
131
|
const content = getParallelExtractContent(firstDocument);
|