mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 06:45:59 +00:00
feat(condense): tune default pruning policy
This commit is contained in:
@@ -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 优先利用中文、站点和日期筛选能力。
|
||||
- 搜索 key 默认保存在 `${XDG_CONFIG_HOME:-$HOME/.config}/my-pi/search.env`,文件必须为 `600` 且不得提交;`search_config.sh` 只输出配置状态,禁止回显 key。
|
||||
- `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 全局默认值;新会话继承全局值,已有分支记录优先,配置缺失或无效时回退为关闭。
|
||||
- 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 序列。
|
||||
|
||||
@@ -361,7 +361,9 @@ macOS 通知中心。Kitty 位于 tmux 内时需要 `set -g allow-passthrough al
|
||||
|
||||
### 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`。
|
||||
|
||||
|
||||
@@ -3,12 +3,25 @@ import { dirname, join } from "node:path";
|
||||
|
||||
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> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/** Enable pi-condense only when the user has not already chosen an enabled state. */
|
||||
export function ensureCondenseEnabledDefault(agentDir: string): CondenseDefaultResult {
|
||||
/** Fill missing pi-condense settings with bundle defaults while preserving every explicit user choice. */
|
||||
export function ensureCondenseDefaults(agentDir: string): CondenseDefaultResult {
|
||||
const targetPath = join(agentDir, "settings.json");
|
||||
let settings: Record<string, unknown> = {};
|
||||
|
||||
@@ -22,9 +35,12 @@ export function ensureCondenseEnabledDefault(agentDir: string): CondenseDefaultR
|
||||
|
||||
const existing = settings.contextPrune;
|
||||
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 });
|
||||
const temporaryPath = `${targetPath}.my-pi.tmp`;
|
||||
writeFileSync(temporaryPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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. */
|
||||
export default async function bundledCondenseExtension(pi: ExtensionAPI): Promise<void> {
|
||||
const result = ensureCondenseEnabledDefault(getAgentDir());
|
||||
const result = ensureCondenseDefaults(getAgentDir());
|
||||
if (result === "skipped-invalid") {
|
||||
console.warn("my-pi: skipped the pi-condense default because settings.json or contextPrune is invalid");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,20 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
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> {
|
||||
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) => {
|
||||
assert.equal(ensureCondenseEnabledDefault(agentDir), "updated");
|
||||
assert.equal(ensureCondenseDefaults(agentDir), "updated");
|
||||
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) => {
|
||||
const path = join(agentDir, "settings.json");
|
||||
await writeFile(path, JSON.stringify({ theme: "dark", contextPrune: { minBatchChars: 4000 } }), "utf8");
|
||||
assert.equal(ensureCondenseEnabledDefault(agentDir), "updated");
|
||||
await writeFile(
|
||||
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")), {
|
||||
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) => {
|
||||
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");
|
||||
assert.equal(ensureCondenseEnabledDefault(agentDir), "unchanged");
|
||||
assert.equal(ensureCondenseDefaults(agentDir), "unchanged");
|
||||
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) => {
|
||||
const path = join(agentDir, "settings.json");
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user