Files
云服务部-叶林立andDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 2a6bf8c929 feat(pi-overflow): 新增统一的新工具结果展示截断扩展
此前截断分散在多层且互相冲突:Pi bash 保留尾部 50KB,RTK 兜底截断又只留
开头 12000 字符(砍掉报错与完整输出路径),pi-chrome 截开头 30K,web_fetch
各 provider 不一致。新扩展统一负责"新结果给模型看多少":
- capture.ts 最先加载暂存原文,index.ts 最后加载(system-prompt 之前)
- 超过 24K 字符时原文落盘 <agentDir>/my-pi/pi-overflow/(700/600、原子写),
  优先取 bash fullOutputPath 完整文件;模型看到 头 + 中段信号行 + 尾 的有界
  视图,行号基于落盘原文、结尾永远可见
- overflow_query:按 id 正则搜索、行区间、jsonPath 只读查询,id 校验防穿越
- 收编:RTK truncate 默认关闭、删除 pi-chrome truncateText、web_fetch 默认
  每页 20000(上限 100000)、web_search max_results 限定 1–20
- pi-compaction 遮蔽带 details.overflow.id 的结果时在占位符提示 overflow_query
- 登记 tools.jsonc / routing / 权限 allow / tool-search 常驻与分组

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-21 20:25:44 -07:00

101 lines
3.8 KiB
TypeScript

import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
export interface OverflowSettings {
enabled: boolean;
thresholdChars: number;
headLines: number;
tailLines: number;
maxSignalLines: number;
signalPattern: string;
signalRegex: RegExp;
maxLineChars: number;
exemptTools: string[];
retentionDays: number;
maxStoreBytes: number;
maxEntryBytes: number;
}
export const DEFAULT_SIGNAL_PATTERN =
"\\b(error|errors|fail|failed|failure|warn|warning|exception|traceback|panic|fatal)\\b";
export const OVERFLOW_DEFAULTS = {
enabled: true,
thresholdChars: 24_000,
headLines: 40,
tailLines: 120,
maxSignalLines: 40,
signalPattern: DEFAULT_SIGNAL_PATTERN,
maxLineChars: 500,
exemptTools: ["read", "edit", "write", "overflow_query", "ask_user_question"],
retentionDays: 7,
maxStoreBytes: 1_073_741_824,
maxEntryBytes: 67_108_864,
} as const;
// Bundle-level `config/` is the single source of truth: the extension reads
// `config/pi-overflow.json` from inside the installed bundle (resolved
// relative to this package-root source file), never `settings.json`.
export const OVERFLOW_CONFIG_PATH = fileURLToPath(
new URL("../config/pi-overflow.json", import.meta.url),
);
function isObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function bool(value: unknown, fallback: boolean): boolean {
return typeof value === "boolean" ? value : fallback;
}
function intInRange(value: unknown, fallback: number, min: number, max: number): number {
return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max
? value
: fallback;
}
function compileSignalPattern(value: unknown): { pattern: string; regex: RegExp } {
const pattern = typeof value === "string" && value.length > 0 ? value : DEFAULT_SIGNAL_PATTERN;
try {
return { pattern, regex: new RegExp(pattern, "i") };
} catch {
return { pattern: DEFAULT_SIGNAL_PATTERN, regex: new RegExp(DEFAULT_SIGNAL_PATTERN, "i") };
}
}
function stringList(value: unknown, fallback: readonly string[]): string[] {
if (!Array.isArray(value)) return [...fallback];
const list = value.filter((item): item is string => typeof item === "string" && item.length > 0);
return [...new Set(list)];
}
/**
* Read `config/pi-overflow.json`. Every field is validated independently and
* falls back to its bundle default when missing or invalid; an unreadable or
* non-object file yields all defaults. Tests inject an explicit path.
*/
export function readOverflowSettings(configPath: string = OVERFLOW_CONFIG_PATH): OverflowSettings {
let parsed: unknown = {};
try {
parsed = JSON.parse(readFileSync(configPath, "utf8"));
} catch {
parsed = {};
}
const config = isObject(parsed) ? parsed : {};
const signal = compileSignalPattern(config.signalPattern);
return {
enabled: bool(config.enabled, OVERFLOW_DEFAULTS.enabled),
thresholdChars: intInRange(config.thresholdChars, OVERFLOW_DEFAULTS.thresholdChars, 1, 10_000_000),
headLines: intInRange(config.headLines, OVERFLOW_DEFAULTS.headLines, 0, 10_000),
tailLines: intInRange(config.tailLines, OVERFLOW_DEFAULTS.tailLines, 0, 10_000),
maxSignalLines: intInRange(config.maxSignalLines, OVERFLOW_DEFAULTS.maxSignalLines, 0, 1_000),
signalPattern: signal.pattern,
signalRegex: signal.regex,
maxLineChars: intInRange(config.maxLineChars, OVERFLOW_DEFAULTS.maxLineChars, 10, 100_000),
exemptTools: stringList(config.exemptTools, OVERFLOW_DEFAULTS.exemptTools),
retentionDays: intInRange(config.retentionDays, OVERFLOW_DEFAULTS.retentionDays, 0, 3_650),
maxStoreBytes: intInRange(config.maxStoreBytes, OVERFLOW_DEFAULTS.maxStoreBytes, 1_024, 1_099_511_627_776),
maxEntryBytes: intInRange(config.maxEntryBytes, OVERFLOW_DEFAULTS.maxEntryBytes, 1_024, 1_099_511_627_776),
};
}