@oh-my-pi/pi-coding-agent 13.11.0 → 13.12.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 +86 -0
- package/package.json +7 -7
- package/src/capability/rule.ts +4 -0
- package/src/cli/commands/init-xdg.ts +27 -0
- package/src/cli/config-cli.ts +8 -3
- package/src/cli/shell-cli.ts +1 -1
- package/src/commands/config.ts +1 -1
- package/src/config/model-registry.ts +160 -26
- package/src/config/model-resolver.ts +84 -21
- package/src/config/settings-schema.ts +812 -647
- package/src/discovery/helpers.ts +11 -2
- package/src/exa/index.ts +1 -11
- package/src/exa/search.ts +1 -122
- package/src/exec/bash-executor.ts +62 -25
- package/src/extensibility/custom-tools/types.ts +2 -3
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/extensibility/hooks/types.ts +2 -0
- package/src/index.ts +6 -6
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/lsp/config.ts +1 -0
- package/src/lsp/defaults.json +3 -3
- package/src/memories/index.ts +20 -7
- package/src/memories/storage.ts +46 -32
- package/src/modes/components/agent-dashboard.ts +23 -35
- package/src/modes/components/assistant-message.ts +25 -2
- package/src/modes/components/btw-panel.ts +104 -0
- package/src/modes/components/settings-defs.ts +5 -1
- package/src/modes/components/settings-selector.ts +6 -6
- package/src/modes/controllers/btw-controller.ts +193 -0
- package/src/modes/controllers/command-controller.ts +3 -1
- package/src/modes/controllers/event-controller.ts +4 -0
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +10 -1
- package/src/modes/controllers/selector-controller.ts +18 -17
- package/src/modes/interactive-mode.ts +22 -0
- package/src/modes/prompt-action-autocomplete.ts +17 -3
- package/src/modes/rpc/rpc-client.ts +30 -19
- package/src/modes/theme/theme.ts +28 -36
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/ui-helpers.ts +3 -0
- package/src/patch/hashline.ts +120 -16
- package/src/prompts/system/btw-user.md +8 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/code-search.md +45 -0
- package/src/prompts/tools/hashline.md +3 -0
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +36 -40
- package/src/session/agent-session.ts +65 -37
- package/src/session/blob-store.ts +32 -0
- package/src/session/compaction/compaction.ts +27 -6
- package/src/session/history-storage.ts +2 -2
- package/src/session/session-manager.ts +116 -44
- package/src/session/streaming-output.ts +17 -54
- package/src/slash-commands/builtin-registry.ts +11 -0
- package/src/system-prompt.ts +4 -17
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +1 -1
- package/src/task/index.ts +9 -8
- package/src/tools/browser.ts +11 -0
- package/src/tools/exit-plan-mode.ts +6 -0
- package/src/tools/fetch.ts +1 -1
- package/src/tools/output-meta.ts +104 -9
- package/src/tools/read.ts +13 -26
- package/src/utils/title-generator.ts +70 -92
- package/src/utils/tools-manager.ts +1 -1
- package/src/web/scrapers/index.ts +7 -7
- package/src/web/scrapers/utils.ts +1 -0
- package/src/web/search/code-search.ts +385 -0
- package/src/web/search/index.ts +25 -280
- package/src/web/search/provider.ts +1 -1
- package/src/web/search/types.ts +28 -0
- package/src/exa/company.ts +0 -26
- package/src/exa/linkedin.ts +0 -26
|
@@ -36,16 +36,14 @@ export interface OutputSinkOptions {
|
|
|
36
36
|
|
|
37
37
|
export interface TruncationResult {
|
|
38
38
|
content: string;
|
|
39
|
-
truncated
|
|
40
|
-
truncatedBy
|
|
39
|
+
truncated?: boolean;
|
|
40
|
+
truncatedBy?: "lines" | "bytes";
|
|
41
41
|
totalLines: number;
|
|
42
42
|
totalBytes: number;
|
|
43
|
-
outputLines
|
|
44
|
-
outputBytes
|
|
45
|
-
lastLinePartial
|
|
46
|
-
firstLineExceedsLimit
|
|
47
|
-
maxLines: number;
|
|
48
|
-
maxBytes: number;
|
|
43
|
+
outputLines?: number;
|
|
44
|
+
outputBytes?: number;
|
|
45
|
+
lastLinePartial?: boolean;
|
|
46
|
+
firstLineExceedsLimit?: boolean;
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
export interface TruncationOptions {
|
|
@@ -206,26 +204,10 @@ export function truncateLine(
|
|
|
206
204
|
// =============================================================================
|
|
207
205
|
|
|
208
206
|
/** Shared helper to build a no-truncation result. */
|
|
209
|
-
function noTruncResult(
|
|
210
|
-
content
|
|
211
|
-
|
|
212
|
-
totalBytes
|
|
213
|
-
maxLines: number,
|
|
214
|
-
maxBytes: number,
|
|
215
|
-
): TruncationResult {
|
|
216
|
-
return {
|
|
217
|
-
content,
|
|
218
|
-
truncated: false,
|
|
219
|
-
truncatedBy: null,
|
|
220
|
-
totalLines,
|
|
221
|
-
totalBytes,
|
|
222
|
-
outputLines: totalLines,
|
|
223
|
-
outputBytes: totalBytes,
|
|
224
|
-
lastLinePartial: false,
|
|
225
|
-
firstLineExceedsLimit: false,
|
|
226
|
-
maxLines,
|
|
227
|
-
maxBytes,
|
|
228
|
-
};
|
|
207
|
+
export function noTruncResult(content: string, totalLines?: number, totalBytes?: number): TruncationResult {
|
|
208
|
+
if (totalLines == null) totalLines = countNewlines(content) + 1;
|
|
209
|
+
if (totalBytes == null) totalBytes = Buffer.byteLength(content, "utf-8");
|
|
210
|
+
return { content, totalLines, totalBytes };
|
|
229
211
|
}
|
|
230
212
|
|
|
231
213
|
/**
|
|
@@ -244,7 +226,7 @@ export function truncateHead(content: string, options: TruncationOptions = {}):
|
|
|
244
226
|
const totalLines = countNewlines(content) + 1;
|
|
245
227
|
|
|
246
228
|
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
247
|
-
return noTruncResult(content, totalLines, totalBytes
|
|
229
|
+
return noTruncResult(content, totalLines, totalBytes);
|
|
248
230
|
}
|
|
249
231
|
|
|
250
232
|
let includedLines = 0;
|
|
@@ -283,8 +265,6 @@ export function truncateHead(content: string, options: TruncationOptions = {}):
|
|
|
283
265
|
outputBytes: 0,
|
|
284
266
|
lastLinePartial: false,
|
|
285
267
|
firstLineExceedsLimit: true,
|
|
286
|
-
maxLines,
|
|
287
|
-
maxBytes,
|
|
288
268
|
};
|
|
289
269
|
}
|
|
290
270
|
break;
|
|
@@ -307,8 +287,6 @@ export function truncateHead(content: string, options: TruncationOptions = {}):
|
|
|
307
287
|
outputBytes: 0,
|
|
308
288
|
lastLinePartial: false,
|
|
309
289
|
firstLineExceedsLimit: true,
|
|
310
|
-
maxLines,
|
|
311
|
-
maxBytes,
|
|
312
290
|
};
|
|
313
291
|
}
|
|
314
292
|
break;
|
|
@@ -335,8 +313,6 @@ export function truncateHead(content: string, options: TruncationOptions = {}):
|
|
|
335
313
|
outputBytes: bytesUsed,
|
|
336
314
|
lastLinePartial: false,
|
|
337
315
|
firstLineExceedsLimit: false,
|
|
338
|
-
maxLines,
|
|
339
|
-
maxBytes,
|
|
340
316
|
};
|
|
341
317
|
}
|
|
342
318
|
|
|
@@ -354,7 +330,7 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
|
|
|
354
330
|
const totalLines = countNewlines(content) + 1;
|
|
355
331
|
|
|
356
332
|
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
357
|
-
return noTruncResult(content, totalLines, totalBytes
|
|
333
|
+
return noTruncResult(content, totalLines, totalBytes);
|
|
358
334
|
}
|
|
359
335
|
|
|
360
336
|
let includedLines = 0;
|
|
@@ -396,8 +372,6 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
|
|
|
396
372
|
outputBytes: tail.bytes,
|
|
397
373
|
lastLinePartial: true,
|
|
398
374
|
firstLineExceedsLimit: false,
|
|
399
|
-
maxLines,
|
|
400
|
-
maxBytes,
|
|
401
375
|
};
|
|
402
376
|
}
|
|
403
377
|
break;
|
|
@@ -420,8 +394,6 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
|
|
|
420
394
|
outputBytes: tail.bytes,
|
|
421
395
|
lastLinePartial: true,
|
|
422
396
|
firstLineExceedsLimit: false,
|
|
423
|
-
maxLines,
|
|
424
|
-
maxBytes,
|
|
425
397
|
};
|
|
426
398
|
}
|
|
427
399
|
break;
|
|
@@ -447,8 +419,6 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
|
|
|
447
419
|
outputBytes: bytesUsed,
|
|
448
420
|
lastLinePartial: false,
|
|
449
421
|
firstLineExceedsLimit: false,
|
|
450
|
-
maxLines,
|
|
451
|
-
maxBytes,
|
|
452
422
|
};
|
|
453
423
|
}
|
|
454
424
|
|
|
@@ -693,7 +663,7 @@ export function formatTailTruncationNotice(
|
|
|
693
663
|
if (!truncation.truncated) return "";
|
|
694
664
|
|
|
695
665
|
const { fullOutputPath, originalContent, suffix = "" } = options;
|
|
696
|
-
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
|
666
|
+
const startLine = truncation.totalLines - (truncation.outputLines ?? truncation.totalLines) + 1;
|
|
697
667
|
const endLine = truncation.totalLines;
|
|
698
668
|
const fullOutputPart = fullOutputPath ? `. Full output: ${fullOutputPath}` : "";
|
|
699
669
|
|
|
@@ -705,11 +675,9 @@ export function formatTailTruncationNotice(
|
|
|
705
675
|
const lastLine = lastNl === -1 ? originalContent : originalContent.substring(lastNl + 1);
|
|
706
676
|
lastLineSizePart = ` (line is ${formatBytes(Buffer.byteLength(lastLine, "utf-8"))})`;
|
|
707
677
|
}
|
|
708
|
-
notice = `[Showing last ${formatBytes(truncation.outputBytes)} of line ${endLine}${lastLineSizePart}${fullOutputPart}${suffix}]`;
|
|
709
|
-
} else if (truncation.truncatedBy === "lines") {
|
|
710
|
-
notice = `[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}${fullOutputPart}${suffix}]`;
|
|
678
|
+
notice = `[Showing last ${formatBytes(truncation.outputBytes ?? truncation.totalBytes)} of line ${endLine}${lastLineSizePart}${fullOutputPart}${suffix}]`;
|
|
711
679
|
} else {
|
|
712
|
-
notice = `[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}
|
|
680
|
+
notice = `[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}${fullOutputPart}${suffix}]`;
|
|
713
681
|
}
|
|
714
682
|
|
|
715
683
|
return `\n\n${notice}`;
|
|
@@ -727,13 +695,8 @@ export function formatHeadTruncationNotice(
|
|
|
727
695
|
|
|
728
696
|
const startLineDisplay = options.startLine ?? 1;
|
|
729
697
|
const totalFileLines = options.totalFileLines ?? truncation.totalLines;
|
|
730
|
-
const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
|
|
698
|
+
const endLineDisplay = startLineDisplay + (truncation.outputLines ?? truncation.totalLines) - 1;
|
|
731
699
|
const nextOffset = endLineDisplay + 1;
|
|
732
|
-
|
|
733
|
-
const notice =
|
|
734
|
-
truncation.truncatedBy === "lines"
|
|
735
|
-
? `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`
|
|
736
|
-
: `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatBytes(truncation.maxBytes)} limit). Use offset=${nextOffset} to continue]`;
|
|
737
|
-
|
|
700
|
+
const notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;
|
|
738
701
|
return `\n\n${notice}`;
|
|
739
702
|
}
|
|
@@ -465,6 +465,17 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
465
465
|
runtime.ctx.editor.setText("");
|
|
466
466
|
},
|
|
467
467
|
},
|
|
468
|
+
{
|
|
469
|
+
name: "btw",
|
|
470
|
+
description: "Ask an ephemeral side question using the current session context",
|
|
471
|
+
inlineHint: "<question>",
|
|
472
|
+
allowArgs: true,
|
|
473
|
+
handle: async (command, runtime) => {
|
|
474
|
+
const question = command.text.slice(`/${command.name}`.length).trim();
|
|
475
|
+
runtime.ctx.editor.setText("");
|
|
476
|
+
await runtime.ctx.handleBtwCommand(question);
|
|
477
|
+
},
|
|
478
|
+
},
|
|
468
479
|
{
|
|
469
480
|
name: "background",
|
|
470
481
|
aliases: ["bg"],
|
package/src/system-prompt.ts
CHANGED
|
@@ -447,22 +447,9 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
447
447
|
skills = prepResult.value.skills;
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
const
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
month: "2-digit",
|
|
454
|
-
day: "2-digit",
|
|
455
|
-
});
|
|
456
|
-
const dateTime = now.toLocaleString("en-US", {
|
|
457
|
-
weekday: "long",
|
|
458
|
-
year: "numeric",
|
|
459
|
-
month: "long",
|
|
460
|
-
day: "numeric",
|
|
461
|
-
hour: "2-digit",
|
|
462
|
-
minute: "2-digit",
|
|
463
|
-
second: "2-digit",
|
|
464
|
-
timeZoneName: "short",
|
|
465
|
-
});
|
|
450
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
451
|
+
const dateTime = date;
|
|
452
|
+
const promptCwd = resolvedCwd.replace(/\\/g, "/");
|
|
466
453
|
|
|
467
454
|
// Build tool metadata for system prompt rendering
|
|
468
455
|
// Priority: explicit list > tools map > defaults
|
|
@@ -504,7 +491,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
504
491
|
rules: rules ?? [],
|
|
505
492
|
date,
|
|
506
493
|
dateTime,
|
|
507
|
-
cwd:
|
|
494
|
+
cwd: promptCwd,
|
|
508
495
|
intentTracing: !!intentField,
|
|
509
496
|
intentField: intentField ?? "",
|
|
510
497
|
eagerTasks,
|
package/src/task/agents.ts
CHANGED
|
@@ -53,7 +53,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
|
|
53
53
|
name: "task",
|
|
54
54
|
description: "General-purpose subagent with full capabilities for delegated multi-step tasks",
|
|
55
55
|
spawns: "*",
|
|
56
|
-
model: "
|
|
56
|
+
model: "pi/task",
|
|
57
57
|
thinkingLevel: Effort.Medium,
|
|
58
58
|
},
|
|
59
59
|
template: taskMd,
|
package/src/task/executor.ts
CHANGED
|
@@ -1243,7 +1243,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1243
1243
|
exitCode,
|
|
1244
1244
|
output: truncatedOutput,
|
|
1245
1245
|
stderr,
|
|
1246
|
-
truncated,
|
|
1246
|
+
truncated: Boolean(truncated),
|
|
1247
1247
|
durationMs: Date.now() - startTime,
|
|
1248
1248
|
tokens: progress.tokens,
|
|
1249
1249
|
modelOverride,
|
package/src/task/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ import type { Usage } from "@oh-my-pi/pi-ai";
|
|
|
20
20
|
import { $env, Snowflake } from "@oh-my-pi/pi-utils";
|
|
21
21
|
import { $ } from "bun";
|
|
22
22
|
import type { ToolSession } from "..";
|
|
23
|
-
import {
|
|
23
|
+
import { resolveAgentModelPatterns } from "../config/model-resolver";
|
|
24
24
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
25
25
|
import type { Theme } from "../modes/theme/theme";
|
|
26
26
|
import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
|
|
@@ -507,14 +507,15 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
507
507
|
: agent;
|
|
508
508
|
|
|
509
509
|
// Apply per-agent model override from settings (highest priority)
|
|
510
|
-
const agentModelOverrides = this.session.settings.get("task.agentModelOverrides")
|
|
510
|
+
const agentModelOverrides = this.session.settings.get("task.agentModelOverrides");
|
|
511
511
|
const settingsModelOverride = agentModelOverrides[agentName];
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
this.session.getActiveModelString?.()
|
|
517
|
-
this.session.getModelString?.()
|
|
512
|
+
const modelOverride = resolveAgentModelPatterns({
|
|
513
|
+
settingsOverride: settingsModelOverride,
|
|
514
|
+
agentModel: effectiveAgent.model,
|
|
515
|
+
settings: this.session.settings,
|
|
516
|
+
activeModelPattern: this.session.getActiveModelString?.(),
|
|
517
|
+
fallbackModelPattern: this.session.getModelString?.(),
|
|
518
|
+
});
|
|
518
519
|
const thinkingLevelOverride = effectiveAgent.thinkingLevel;
|
|
519
520
|
|
|
520
521
|
// Output schema priority: agent frontmatter > params > inherited from parent session
|
package/src/tools/browser.ts
CHANGED
|
@@ -526,6 +526,17 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
526
526
|
const proxy = process.env.PUPPETEER_PROXY;
|
|
527
527
|
if (proxy) {
|
|
528
528
|
launchArgs.push(`--proxy-server=${proxy}`);
|
|
529
|
+
// Chrome (since v72) bypasses proxies for localhost by default. When PUPPETEER_PROXY_BYPASS_LOOPBACK
|
|
530
|
+
// is true, add <-loopback> so traffic to localhost reaches the proxy (e.g. for mitmdump/auth capture).
|
|
531
|
+
const bypassLoopback = process.env.PUPPETEER_PROXY_BYPASS_LOOPBACK?.toLowerCase();
|
|
532
|
+
if (
|
|
533
|
+
bypassLoopback === "true" ||
|
|
534
|
+
bypassLoopback === "1" ||
|
|
535
|
+
bypassLoopback === "yes" ||
|
|
536
|
+
bypassLoopback === "on"
|
|
537
|
+
) {
|
|
538
|
+
launchArgs.push("--proxy-bypass-list=<-loopback>");
|
|
539
|
+
}
|
|
529
540
|
}
|
|
530
541
|
const ignoreCert = process.env.PUPPETEER_PROXY_IGNORE_CERT_ERRORS?.toLowerCase();
|
|
531
542
|
if (ignoreCert === "true" || ignoreCert === "1" || ignoreCert === "yes" || ignoreCert === "on") {
|
|
@@ -77,6 +77,12 @@ export class ExitPlanModeTool implements AgentTool<typeof exitPlanModeSchema, Ex
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
if (!planExists) {
|
|
81
|
+
throw new ToolError(
|
|
82
|
+
`Plan file not found at ${state.planFilePath}. Write the finalized plan to ${state.planFilePath} before calling exit_plan_mode.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
80
86
|
return {
|
|
81
87
|
content: [{ type: "text", text: "Plan ready for approval." }],
|
|
82
88
|
details: {
|
package/src/tools/fetch.ts
CHANGED
|
@@ -1159,7 +1159,7 @@ export class FetchTool implements AgentTool<typeof fetchSchema, FetchToolDetails
|
|
|
1159
1159
|
finalUrl: result.finalUrl,
|
|
1160
1160
|
contentType: result.contentType,
|
|
1161
1161
|
method: result.method,
|
|
1162
|
-
truncated: result.truncated || needsArtifact,
|
|
1162
|
+
truncated: Boolean(result.truncated || needsArtifact),
|
|
1163
1163
|
notes: result.notes,
|
|
1164
1164
|
};
|
|
1165
1165
|
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
15
15
|
import { formatGroupedDiagnosticMessages } from "../lsp/utils";
|
|
16
16
|
import type { Theme } from "../modes/theme/theme";
|
|
17
|
-
import type
|
|
17
|
+
import { type OutputSummary, type TruncationResult, truncateTail } from "../session/streaming-output";
|
|
18
18
|
import { formatBytes, wrapBrackets } from "./render-utils";
|
|
19
19
|
import { renderError } from "./tool-errors";
|
|
20
20
|
|
|
@@ -117,26 +117,28 @@ export class OutputMetaBuilder {
|
|
|
117
117
|
if (!result.truncated) return this;
|
|
118
118
|
|
|
119
119
|
const { direction, startLine = 1, totalFileLines, artifactId } = options;
|
|
120
|
+
const outputLines = result.outputLines ?? result.totalLines;
|
|
121
|
+
const outputBytes = result.outputBytes ?? result.totalBytes;
|
|
122
|
+
const truncatedBy: "lines" | "bytes" = result.truncatedBy === "lines" ? "lines" : "bytes";
|
|
120
123
|
|
|
121
124
|
let shownStart: number;
|
|
122
125
|
let shownEnd: number;
|
|
123
126
|
|
|
124
127
|
if (direction === "tail") {
|
|
125
|
-
shownStart = result.totalLines -
|
|
128
|
+
shownStart = result.totalLines - outputLines + 1;
|
|
126
129
|
shownEnd = result.totalLines;
|
|
127
130
|
} else {
|
|
128
131
|
shownStart = startLine;
|
|
129
|
-
shownEnd = startLine +
|
|
132
|
+
shownEnd = startLine + outputLines - 1;
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
this.#meta.truncation = {
|
|
133
136
|
direction,
|
|
134
|
-
truncatedBy
|
|
137
|
+
truncatedBy,
|
|
135
138
|
totalLines: totalFileLines ?? result.totalLines,
|
|
136
139
|
totalBytes: result.totalBytes,
|
|
137
|
-
outputLines
|
|
138
|
-
outputBytes
|
|
139
|
-
maxBytes: result.maxBytes,
|
|
140
|
+
outputLines,
|
|
141
|
+
outputBytes,
|
|
140
142
|
shownRange: { start: shownStart, end: shownEnd },
|
|
141
143
|
artifactId,
|
|
142
144
|
nextOffset: direction === "head" ? shownEnd + 1 : undefined,
|
|
@@ -315,7 +317,7 @@ export function outputMeta(): OutputMetaBuilder {
|
|
|
315
317
|
// =============================================================================
|
|
316
318
|
|
|
317
319
|
export function formatFullOutputReference(artifactId: string): string {
|
|
318
|
-
return `
|
|
320
|
+
return `Read artifact://${artifactId} for full output`;
|
|
319
321
|
}
|
|
320
322
|
|
|
321
323
|
export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
|
|
@@ -432,6 +434,95 @@ function appendOutputNotice(
|
|
|
432
434
|
|
|
433
435
|
const kUnwrappedExecute = Symbol("OutputMeta.UnwrappedExecute");
|
|
434
436
|
|
|
437
|
+
// =============================================================================
|
|
438
|
+
// Centralized artifact spill for large tool results
|
|
439
|
+
// =============================================================================
|
|
440
|
+
|
|
441
|
+
/** Text content above this byte threshold gets saved to an artifact. */
|
|
442
|
+
const RESULT_ARTIFACT_THRESHOLD = 50 * 1024; // 50KB
|
|
443
|
+
|
|
444
|
+
/** When spilling, keep this many bytes of tail in the result sent to the LLM. */
|
|
445
|
+
const RESULT_ARTIFACT_TAIL_BYTES = 20 * 1024; // 20KB
|
|
446
|
+
|
|
447
|
+
/** When spilling, keep at most this many lines of tail. */
|
|
448
|
+
const RESULT_ARTIFACT_TAIL_LINES = 500;
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* If the tool result text exceeds RESULT_ARTIFACT_THRESHOLD, save the full
|
|
452
|
+
* output as a session artifact and replace the content with a tail-truncated
|
|
453
|
+
* version plus an artifact reference. Skips when the tool already saved its
|
|
454
|
+
* own artifact (e.g. bash/python via OutputSink).
|
|
455
|
+
*/
|
|
456
|
+
async function spillLargeResultToArtifact(
|
|
457
|
+
result: AgentToolResult,
|
|
458
|
+
toolName: string,
|
|
459
|
+
context: AgentToolContext | undefined,
|
|
460
|
+
): Promise<AgentToolResult> {
|
|
461
|
+
const sessionManager = context?.sessionManager;
|
|
462
|
+
if (!sessionManager) return result;
|
|
463
|
+
|
|
464
|
+
// Skip if tool already saved an artifact
|
|
465
|
+
const existingMeta = (result.details as { meta?: OutputMeta } | undefined)?.meta;
|
|
466
|
+
if (existingMeta?.truncation?.artifactId) return result;
|
|
467
|
+
|
|
468
|
+
// Measure total text content
|
|
469
|
+
const textParts: string[] = [];
|
|
470
|
+
for (const block of result.content) {
|
|
471
|
+
if (block.type === "text" && block.text) {
|
|
472
|
+
textParts.push(block.text);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (textParts.length === 0) return result;
|
|
476
|
+
|
|
477
|
+
const fullText = textParts.length === 1 ? textParts[0] : textParts.join("\n");
|
|
478
|
+
const totalBytes = Buffer.byteLength(fullText, "utf-8");
|
|
479
|
+
if (totalBytes <= RESULT_ARTIFACT_THRESHOLD) return result;
|
|
480
|
+
|
|
481
|
+
// Save full output as artifact
|
|
482
|
+
const artifactId = await sessionManager.saveArtifact(fullText, toolName);
|
|
483
|
+
if (!artifactId) return result;
|
|
484
|
+
|
|
485
|
+
// Truncate to tail
|
|
486
|
+
const truncated = truncateTail(fullText, {
|
|
487
|
+
maxBytes: RESULT_ARTIFACT_TAIL_BYTES,
|
|
488
|
+
maxLines: RESULT_ARTIFACT_TAIL_LINES,
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// Replace text blocks with single tail-truncated block, keep images
|
|
492
|
+
const newContent: (TextContent | ImageContent)[] = [];
|
|
493
|
+
for (const block of result.content) {
|
|
494
|
+
if (block.type !== "text") {
|
|
495
|
+
newContent.push(block);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
newContent.push({ type: "text", text: truncated.content });
|
|
499
|
+
|
|
500
|
+
// Build truncation meta
|
|
501
|
+
const outputLines = truncated.outputLines ?? truncated.totalLines;
|
|
502
|
+
const outputBytes = truncated.outputBytes ?? truncated.totalBytes;
|
|
503
|
+
const shownStart = truncated.totalLines - outputLines + 1;
|
|
504
|
+
const truncationMeta: TruncationMeta = {
|
|
505
|
+
direction: "tail",
|
|
506
|
+
truncatedBy: truncated.truncatedBy ?? "bytes",
|
|
507
|
+
totalLines: truncated.totalLines,
|
|
508
|
+
totalBytes: truncated.totalBytes,
|
|
509
|
+
outputLines,
|
|
510
|
+
outputBytes,
|
|
511
|
+
maxBytes: RESULT_ARTIFACT_TAIL_BYTES,
|
|
512
|
+
shownRange: { start: shownStart, end: truncated.totalLines },
|
|
513
|
+
artifactId,
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const newMeta: OutputMeta = { ...(existingMeta ?? {}), truncation: truncationMeta };
|
|
517
|
+
const newDetails = { ...(result.details ?? {}), meta: newMeta };
|
|
518
|
+
|
|
519
|
+
return { ...result, content: newContent, details: newDetails };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// =============================================================================
|
|
523
|
+
// Tool wrapper
|
|
524
|
+
// =============================================================================
|
|
525
|
+
|
|
435
526
|
async function wrappedExecute(
|
|
436
527
|
this: AgentTool & { [kUnwrappedExecute]: AgentToolExecFn },
|
|
437
528
|
toolCallId: string,
|
|
@@ -443,8 +534,12 @@ async function wrappedExecute(
|
|
|
443
534
|
const originalExecute = this[kUnwrappedExecute];
|
|
444
535
|
|
|
445
536
|
try {
|
|
537
|
+
let result = await originalExecute.call(this, toolCallId, params, signal, onUpdate, context);
|
|
538
|
+
|
|
539
|
+
// Spill large results to artifact, truncate to tail
|
|
540
|
+
result = await spillLargeResultToArtifact(result, this.name, context);
|
|
541
|
+
|
|
446
542
|
// Append notices from meta
|
|
447
|
-
const result = await originalExecute.call(this, toolCallId, params, signal, onUpdate, context);
|
|
448
543
|
const meta = (result.details as { meta?: OutputMeta } | undefined)?.meta;
|
|
449
544
|
if (meta) {
|
|
450
545
|
return {
|
package/src/tools/read.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { ToolSession } from "../sdk";
|
|
|
16
16
|
import {
|
|
17
17
|
DEFAULT_MAX_BYTES,
|
|
18
18
|
DEFAULT_MAX_LINES,
|
|
19
|
+
noTruncResult,
|
|
19
20
|
type TruncationResult,
|
|
20
21
|
truncateHead,
|
|
21
22
|
truncateHeadBytes,
|
|
@@ -592,15 +593,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
592
593
|
const truncation: TruncationResult = {
|
|
593
594
|
content: selectedContent,
|
|
594
595
|
truncated: wasTruncated,
|
|
595
|
-
truncatedBy: stoppedByByteLimit ? "bytes" : wasTruncated ? "lines" :
|
|
596
|
+
truncatedBy: stoppedByByteLimit ? "bytes" : wasTruncated ? "lines" : undefined,
|
|
596
597
|
totalLines: totalSelectedLines,
|
|
597
598
|
totalBytes: totalSelectedBytes,
|
|
598
599
|
outputLines: collectedLines.length,
|
|
599
600
|
outputBytes: collectedBytes,
|
|
600
601
|
lastLinePartial: false,
|
|
601
602
|
firstLineExceedsLimit,
|
|
602
|
-
maxLines: DEFAULT_MAX_LINES,
|
|
603
|
-
maxBytes: DEFAULT_MAX_BYTES,
|
|
604
603
|
};
|
|
605
604
|
|
|
606
605
|
const shouldAddHashLines = displayMode.hashLines;
|
|
@@ -687,8 +686,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
687
686
|
async #handleInternalUrl(url: string, offset?: number, limit?: number): Promise<AgentToolResult<ReadToolDetails>> {
|
|
688
687
|
const internalRouter = this.session.internalRouter!;
|
|
689
688
|
|
|
690
|
-
const displayMode = resolveFileDisplayMode(this.session);
|
|
691
|
-
|
|
692
689
|
// Check if URL has query extraction (agent:// only)
|
|
693
690
|
let parsed: URL;
|
|
694
691
|
try {
|
|
@@ -716,7 +713,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
716
713
|
return toolResult(details).text(resource.content).sourceInternal(url).done();
|
|
717
714
|
}
|
|
718
715
|
|
|
719
|
-
// Apply pagination similar to file reading
|
|
716
|
+
// Apply pagination similar to file reading.
|
|
720
717
|
const allLines = resource.content.split("\n");
|
|
721
718
|
const totalLines = allLines.length;
|
|
722
719
|
|
|
@@ -733,9 +730,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
733
730
|
.done();
|
|
734
731
|
}
|
|
735
732
|
|
|
733
|
+
const ignoreLimits = scheme === "skill";
|
|
736
734
|
let selectedContent: string;
|
|
737
735
|
let userLimitedLines: number | undefined;
|
|
738
|
-
if (limit !== undefined) {
|
|
736
|
+
if (limit !== undefined && !ignoreLimits) {
|
|
739
737
|
const endLine = Math.min(startLine + limit, allLines.length);
|
|
740
738
|
selectedContent = allLines.slice(startLine, endLine).join("\n");
|
|
741
739
|
userLimitedLines = endLine - startLine;
|
|
@@ -743,14 +741,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
743
741
|
selectedContent = allLines.slice(startLine).join("\n");
|
|
744
742
|
}
|
|
745
743
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const shouldAddHashLines = displayMode.hashLines;
|
|
750
|
-
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
751
|
-
const formatText = (text: string, startNum: number): string => {
|
|
752
|
-
return formatTextWithMode(text, startNum, shouldAddHashLines, shouldAddLineNumbers);
|
|
753
|
-
};
|
|
744
|
+
const truncation: TruncationResult = ignoreLimits
|
|
745
|
+
? noTruncResult(selectedContent)
|
|
746
|
+
: truncateHead(selectedContent);
|
|
754
747
|
|
|
755
748
|
let outputText: string;
|
|
756
749
|
let truncationInfo:
|
|
@@ -762,13 +755,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
762
755
|
const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
|
|
763
756
|
const snippet = truncateHeadBytes(firstLine, DEFAULT_MAX_BYTES);
|
|
764
757
|
|
|
765
|
-
|
|
766
|
-
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
767
|
-
firstLineBytes,
|
|
768
|
-
)}, exceeds ${formatBytes(DEFAULT_MAX_BYTES)} limit. Hashline output requires full lines; cannot compute hashes for a truncated preview.]`;
|
|
769
|
-
} else {
|
|
770
|
-
outputText = formatText(snippet.text, startLineDisplay);
|
|
771
|
-
}
|
|
758
|
+
outputText = snippet.text;
|
|
772
759
|
if (snippet.text.length === 0) {
|
|
773
760
|
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
774
761
|
firstLineBytes,
|
|
@@ -780,7 +767,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
780
767
|
options: { direction: "head", startLine: startLineDisplay, totalFileLines: totalLines },
|
|
781
768
|
};
|
|
782
769
|
} else if (truncation.truncated) {
|
|
783
|
-
outputText =
|
|
770
|
+
outputText = truncation.content;
|
|
784
771
|
details.truncation = truncation;
|
|
785
772
|
truncationInfo = {
|
|
786
773
|
result: truncation,
|
|
@@ -790,11 +777,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
790
777
|
const remaining = allLines.length - (startLine + userLimitedLines);
|
|
791
778
|
const nextOffset = startLine + userLimitedLines + 1;
|
|
792
779
|
|
|
793
|
-
outputText =
|
|
780
|
+
outputText = truncation.content;
|
|
794
781
|
outputText += `\n\n[${remaining} more lines in resource. Use offset=${nextOffset} to continue]`;
|
|
795
782
|
details.truncation = truncation;
|
|
796
783
|
} else {
|
|
797
|
-
outputText =
|
|
784
|
+
outputText = truncation.content;
|
|
798
785
|
}
|
|
799
786
|
|
|
800
787
|
const resultBuilder = toolResult(details).text(outputText).sourceInternal(url);
|
|
@@ -924,7 +911,7 @@ export const readToolRenderer = {
|
|
|
924
911
|
}
|
|
925
912
|
if (truncation) {
|
|
926
913
|
if (fallback?.firstLineExceedsLimit) {
|
|
927
|
-
let warning = `First line exceeds ${formatBytes(fallback.
|
|
914
|
+
let warning = `First line exceeds ${formatBytes(fallback.outputBytes ?? fallback.totalBytes)} limit`;
|
|
928
915
|
if (truncation.artifactId) {
|
|
929
916
|
warning += `. ${formatFullOutputReference(truncation.artifactId)}`;
|
|
930
917
|
}
|