feat(condense): tune default pruning policy

This commit is contained in:
云服务部-叶林立
2026-08-28 12:14:19 +08:00
parent 2dcec0207c
commit 0c80a8b537
5 changed files with 81 additions and 21 deletions
+1 -1
View File
@@ -90,7 +90,7 @@
- Exa MCP 使用托管 Streamable HTTP 端点和 `eager` lifecycle`EXA_API_KEY` 通过 `x-api-key` 请求头发送,不得放入 URL、仓库文件或日志。所有在线搜索工具统一采用“来源名 + 原始语义工具名”的命名形式,例如 `tavily_web_search``exa_web_search``keenable_search`。Exa 高级搜索必须显式约束结果数量和文本长度;Tavily 输出默认控制 `max_results` 且非必要不请求 raw content;Keenable 优先利用中文、站点和日期筛选能力。 - Exa MCP 使用托管 Streamable HTTP 端点和 `eager` lifecycle`EXA_API_KEY` 通过 `x-api-key` 请求头发送,不得放入 URL、仓库文件或日志。所有在线搜索工具统一采用“来源名 + 原始语义工具名”的命名形式,例如 `tavily_web_search``exa_web_search``keenable_search`。Exa 高级搜索必须显式约束结果数量和文本长度;Tavily 输出默认控制 `max_results` 且非必要不请求 raw content;Keenable 优先利用中文、站点和日期筛选能力。
- 搜索 key 默认保存在 `${XDG_CONFIG_HOME:-$HOME/.config}/my-pi/search.env`,文件必须为 `600` 且不得提交;`search_config.sh` 只输出配置状态,禁止回显 key。 - 搜索 key 默认保存在 `${XDG_CONFIG_HOME:-$HOME/.config}/my-pi/search.env`,文件必须为 `600` 且不得提交;`search_config.sh` 只输出配置状态,禁止回显 key。
- `pi-context-view` 只观察上下文占用,不参与压缩策略。 - `pi-context-view` 只观察上下文占用,不参与压缩策略。
- `pi-condense` 负责压缩已经进入会话的历史工具结果,与 Context Mode 的输入隔离职责互补;`extensions/condense.ts` `settings.json` 尚无 `contextPrune.enabled` 时写入 `true`,必须保留用户显式设置的 `false`,配置无效时不得覆盖原文件。其他参数沿用上游默认,包括 `agent-message` 触发模式和 skill 路径保护。 - `pi-condense` 负责压缩已经进入会话的历史工具结果,与 Context Mode 的输入隔离职责互补;`extensions/condense.ts` `settings.json` `contextPrune` 补入缺失的组合包默认字段,必须保留全部用户显式值(包括 `enabled: false`,配置无效时不得覆盖原文件,配置完整时不得重复改写。组合包默认开启,使用 `agent-message` 触发、`turn` 批处理、73.5% 总预算与 4% 单轮增量触发,摘要模型和 thinking 均为 `default`,空闲/总超时为 90 秒/5 分钟,`quietOversizedSkips``false`;未显式部署的参数继续沿用上游默认,包括 skill 路径保护。
- Codex fast mode 只为符合条件的 `openai-codex-responses` 请求设置 priority service tier。`/fast-mode on|off` 同时更新当前分支记录与 Pi agent 目录中的 owner-only 全局默认值;新会话继承全局值,已有分支记录优先,配置缺失或无效时回退为关闭。 - Codex fast mode 只为符合条件的 `openai-codex-responses` 请求设置 priority service tier。`/fast-mode on|off` 同时更新当前分支记录与 Pi agent 目录中的 owner-only 全局默认值;新会话继承全局值,已有分支记录优先,配置缺失或无效时回退为关闭。
- Kitty 通知只在 TUI 模式的 `agent_settled` 后发送;自动重试、自动 compaction 和 follow-up 期间不得提前通知或重置总耗时。默认 `o=always``a=focus`,每个 Pi Session 使用独立稳定 ID,标题和正文必须 Base64 编码。 - Kitty 通知只在 TUI 模式的 `agent_settled` 后发送;自动重试、自动 compaction 和 follow-up 期间不得提前通知或重置总耗时。默认 `o=always``a=focus`,每个 Pi Session 使用独立稳定 ID,标题和正文必须 Base64 编码。
- `/notify on|off` 只覆盖当前 Session;持久默认来自 `PI_NOTIFY_*` 环境变量。`PI_NOTIFY_MESSAGE_SOURCE=none` 必须继续提供不泄露回复正文的隐私模式,非 TUI 模式不得写入 OSC 序列。 - `/notify on|off` 只覆盖当前 Session;持久默认来自 `PI_NOTIFY_*` 环境变量。`PI_NOTIFY_MESSAGE_SOURCE=none` 必须继续提供不泄露回复正文的隐私模式,非 TUI 模式不得写入 OSC 序列。
+3 -1
View File
@@ -361,7 +361,9 @@ macOS 通知中心。Kitty 位于 tmux 内时需要 `set -g allow-passthrough al
### Pi Condense ### Pi Condense
组合包首次加载时会 `contextPrune.enabled` 尚未配置的情况下写入 `true`,因此 `pi-condense` 默认开启;用户通过 `/pruner off` 写入的显式 `false` 会被保留,后续启动或升级不会重新覆盖。它使用上游默认的 `agent-message` 触发模式:同一用户任务中的多轮工具调用会在最终文本回复后统一触发整理,默认仍按每个 assistant turn 分别生成摘要;后续上下文只保留摘要和 `t1``t2` 等引用。需要原文时,模型可调用 `context_tree_query({ toolCallIds: ["t1"] })` 恢复 组合包加载时会只为 `contextPrune` 中缺失的字段补入默认策略,任何用户显式值(包括 `/pruner off` 写入的 `enabled: false`)均优先且不会被覆盖。默认使用 `agent-message` 触发模式和 `turn` 批处理;正常上下文达到窗口的 73.5% 时中途整理,单轮增长达到窗口的 4% 时提前整理。对于 272K 上下文模型,两者约为 200K 和 11K tokens;其他模型按各自窗口比例计算。摘要使用当前默认模型和默认 thinking,连续 90 秒无输出才判定空闲超时,每次最长运行 5 分钟,并保留未压缩或摘要过大等跳过提示
同一用户任务中的多轮工具调用通常在最终文本回复后触发整理,预算触发器则可在长任务中提前处理;后续上下文只保留摘要和 `t1``t2` 等引用。需要原文时,模型可调用 `context_tree_query({ toolCallIds: ["t1"] })` 恢复。
常用命令:`/pruner status` 查看状态,`/pruner settings` 调整配置,`/pruner now` 立即处理待总结批次,`/pruner tree` 浏览已归档调用,`/pruner stats` 查看累计成本和节省,`/pruner off` 关闭。配置保存在 Pi agent 目录 `settings.json``contextPrune` 字段。建议保留默认的 skill 路径保护,并把必须逐字复用输出的工具加入 `protectedTools` 常用命令:`/pruner status` 查看状态,`/pruner settings` 调整配置,`/pruner now` 立即处理待总结批次,`/pruner tree` 浏览已归档调用,`/pruner stats` 查看累计成本和节省,`/pruner off` 关闭。配置保存在 Pi agent 目录 `settings.json``contextPrune` 字段。建议保留默认的 skill 路径保护,并把必须逐字复用输出的工具加入 `protectedTools`
+20 -4
View File
@@ -3,12 +3,25 @@ import { dirname, join } from "node:path";
export type CondenseDefaultResult = "updated" | "unchanged" | "skipped-invalid"; export type CondenseDefaultResult = "updated" | "unchanged" | "skipped-invalid";
const CONDENSE_BUNDLE_DEFAULTS = {
enabled: true,
pruneOn: "agent-message",
batchingMode: "turn",
autoBudgetThreshold: 0.735,
budgetTurnDelta: 0.04,
summarizerModel: "default",
summarizerThinking: "default",
summarizerIdleTimeoutMs: 90_000,
summarizerMaxTimeoutMs: 300_000,
quietOversizedSkips: false,
} as const;
function isObject(value: unknown): value is Record<string, unknown> { function isObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value); return value !== null && typeof value === "object" && !Array.isArray(value);
} }
/** Enable pi-condense only when the user has not already chosen an enabled state. */ /** Fill missing pi-condense settings with bundle defaults while preserving every explicit user choice. */
export function ensureCondenseEnabledDefault(agentDir: string): CondenseDefaultResult { export function ensureCondenseDefaults(agentDir: string): CondenseDefaultResult {
const targetPath = join(agentDir, "settings.json"); const targetPath = join(agentDir, "settings.json");
let settings: Record<string, unknown> = {}; let settings: Record<string, unknown> = {};
@@ -22,9 +35,12 @@ export function ensureCondenseEnabledDefault(agentDir: string): CondenseDefaultR
const existing = settings.contextPrune; const existing = settings.contextPrune;
if (existing !== undefined && !isObject(existing)) return "skipped-invalid"; if (existing !== undefined && !isObject(existing)) return "skipped-invalid";
if (isObject(existing) && Object.hasOwn(existing, "enabled")) return "unchanged";
settings.contextPrune = { ...(existing ?? {}), enabled: true }; const contextPrune = existing ?? {};
const alreadyComplete = Object.keys(CONDENSE_BUNDLE_DEFAULTS).every((key) => Object.hasOwn(contextPrune, key));
if (alreadyComplete) return "unchanged";
settings.contextPrune = { ...CONDENSE_BUNDLE_DEFAULTS, ...contextPrune };
mkdirSync(dirname(targetPath), { recursive: true }); mkdirSync(dirname(targetPath), { recursive: true });
const temporaryPath = `${targetPath}.my-pi.tmp`; const temporaryPath = `${targetPath}.my-pi.tmp`;
writeFileSync(temporaryPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8"); writeFileSync(temporaryPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
+2 -2
View File
@@ -1,9 +1,9 @@
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { ensureCondenseEnabledDefault } from "./condense-config.ts"; import { ensureCondenseDefaults } from "./condense-config.ts";
/** Apply the bundle default before pi-condense reads settings on session_start. */ /** Apply the bundle default before pi-condense reads settings on session_start. */
export default async function bundledCondenseExtension(pi: ExtensionAPI): Promise<void> { export default async function bundledCondenseExtension(pi: ExtensionAPI): Promise<void> {
const result = ensureCondenseEnabledDefault(getAgentDir()); const result = ensureCondenseDefaults(getAgentDir());
if (result === "skipped-invalid") { if (result === "skipped-invalid") {
console.warn("my-pi: skipped the pi-condense default because settings.json or contextPrune is invalid"); console.warn("my-pi: skipped the pi-condense default because settings.json or contextPrune is invalid");
} }
+55 -13
View File
@@ -4,7 +4,20 @@ import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import test from "node:test"; import test from "node:test";
import { ensureCondenseEnabledDefault } from "../extensions/condense-config.ts"; import { ensureCondenseDefaults } from "../extensions/condense-config.ts";
const EXPECTED_DEFAULTS = {
enabled: true,
pruneOn: "agent-message",
batchingMode: "turn",
autoBudgetThreshold: 0.735,
budgetTurnDelta: 0.04,
summarizerModel: "default",
summarizerThinking: "default",
summarizerIdleTimeoutMs: 90_000,
summarizerMaxTimeoutMs: 300_000,
quietOversizedSkips: false,
};
async function withAgentDir(run: (agentDir: string) => Promise<void>): Promise<void> { async function withAgentDir(run: (agentDir: string) => Promise<void>): Promise<void> {
const agentDir = await mkdtemp(join(tmpdir(), "my-pi-condense-")); const agentDir = await mkdtemp(join(tmpdir(), "my-pi-condense-"));
@@ -15,42 +28,71 @@ async function withAgentDir(run: (agentDir: string) => Promise<void>): Promise<v
} }
} }
test("pi-condense defaults to enabled when contextPrune is absent", async () => { test("pi-condense writes the complete bundle defaults when contextPrune is absent", async () => {
await withAgentDir(async (agentDir) => { await withAgentDir(async (agentDir) => {
assert.equal(ensureCondenseEnabledDefault(agentDir), "updated"); assert.equal(ensureCondenseDefaults(agentDir), "updated");
assert.deepEqual(JSON.parse(await readFile(join(agentDir, "settings.json"), "utf8")), { assert.deepEqual(JSON.parse(await readFile(join(agentDir, "settings.json"), "utf8")), {
contextPrune: { enabled: true }, contextPrune: EXPECTED_DEFAULTS,
}); });
}); });
}); });
test("pi-condense default preserves existing settings and contextPrune fields", async () => { test("pi-condense fills only missing defaults and preserves unrelated settings", async () => {
await withAgentDir(async (agentDir) => { await withAgentDir(async (agentDir) => {
const path = join(agentDir, "settings.json"); const path = join(agentDir, "settings.json");
await writeFile(path, JSON.stringify({ theme: "dark", contextPrune: { minBatchChars: 4000 } }), "utf8"); await writeFile(
assert.equal(ensureCondenseEnabledDefault(agentDir), "updated"); path,
JSON.stringify({ theme: "dark", contextPrune: { minBatchChars: 4000, budgetTurnDelta: 0.03 } }),
"utf8",
);
assert.equal(ensureCondenseDefaults(agentDir), "updated");
assert.deepEqual(JSON.parse(await readFile(path, "utf8")), { assert.deepEqual(JSON.parse(await readFile(path, "utf8")), {
theme: "dark", theme: "dark",
contextPrune: { minBatchChars: 4000, enabled: true }, contextPrune: { ...EXPECTED_DEFAULTS, minBatchChars: 4000, budgetTurnDelta: 0.03 },
}); });
}); });
}); });
test("pi-condense default respects an explicit enabled choice", async () => { test("pi-condense preserves an explicit disabled choice while filling other defaults", async () => {
await withAgentDir(async (agentDir) => { await withAgentDir(async (agentDir) => {
const path = join(agentDir, "settings.json"); const path = join(agentDir, "settings.json");
const original = `${JSON.stringify({ contextPrune: { enabled: false } }, null, 2)}\n`; await writeFile(path, JSON.stringify({ contextPrune: { enabled: false } }), "utf8");
assert.equal(ensureCondenseDefaults(agentDir), "updated");
assert.deepEqual(JSON.parse(await readFile(path, "utf8")), {
contextPrune: { ...EXPECTED_DEFAULTS, enabled: false },
});
});
});
test("pi-condense does not rewrite a complete explicit configuration", async () => {
await withAgentDir(async (agentDir) => {
const path = join(agentDir, "settings.json");
const original = `${JSON.stringify(
{ contextPrune: { ...EXPECTED_DEFAULTS, enabled: false, quietOversizedSkips: true } },
null,
2,
)}\n`;
await writeFile(path, original, "utf8"); await writeFile(path, original, "utf8");
assert.equal(ensureCondenseEnabledDefault(agentDir), "unchanged"); assert.equal(ensureCondenseDefaults(agentDir), "unchanged");
assert.equal(await readFile(path, "utf8"), original); assert.equal(await readFile(path, "utf8"), original);
}); });
}); });
test("pi-condense default does not overwrite malformed settings", async () => { test("pi-condense defaults do not overwrite malformed settings", async () => {
await withAgentDir(async (agentDir) => { await withAgentDir(async (agentDir) => {
const path = join(agentDir, "settings.json"); const path = join(agentDir, "settings.json");
await writeFile(path, "{not-json", "utf8"); await writeFile(path, "{not-json", "utf8");
assert.equal(ensureCondenseEnabledDefault(agentDir), "skipped-invalid"); assert.equal(ensureCondenseDefaults(agentDir), "skipped-invalid");
assert.equal(await readFile(path, "utf8"), "{not-json"); assert.equal(await readFile(path, "utf8"), "{not-json");
}); });
}); });
test("pi-condense defaults reject a non-object contextPrune value", async () => {
await withAgentDir(async (agentDir) => {
const path = join(agentDir, "settings.json");
const original = `${JSON.stringify({ contextPrune: false }, null, 2)}\n`;
await writeFile(path, original, "utf8");
assert.equal(ensureCondenseDefaults(agentDir), "skipped-invalid");
assert.equal(await readFile(path, "utf8"), original);
});
});