feat: add pi-condense with default enablement

This commit is contained in:
云服务部-叶林立
2026-08-19 15:06:40 +08:00
parent 410c50a3e5
commit 6ce932df60
7 changed files with 134 additions and 0 deletions
+2
View File
@@ -29,6 +29,7 @@
- `pi-lsp@0.1.7`:提供声明式 LSP 接入;组合包内置 `typescript-language-server@5.3.0` + `typescript@6.0.3`,并配置机器级 `kotlin-lsp --stdio``jdtls`
- `pi-hermes-memory@0.9.6`:提供持久记忆、会话搜索、后台学习和 secret scanning,默认使用 policy-only 模式。
- `@ogulcancelik/pi-codex-compaction@0.1.3`:为 `openai-codex` 提供原生远程 compaction,默认阈值为 90%。
- `pi-condense@2.9.1`:总结已完成的工具调用批次,以短 stub 替换历史原始输出,并通过 `context_tree_query` 按需恢复;组合包在用户尚未配置 `contextPrune.enabled` 时默认开启。
- 根包还固定安装 `@tavily/pi-extension@0.1.2``@keenable/pi-search@0.1.2``pi-hashline-edit@0.8.3``typescript-language-server@5.3.0``typescript@6.0.3``pi-context-view``@firstpick/pi-extension-codex-fast-mode``@gotgenes/pi-permission-system` 改为指向仓库内 `pi-permission-system/` 的本地 `file:` 依赖。
- `install.sh`:先安装根组合包,再交互检查 Kitty/Solarized Dark、Oh My Zsh/Powerlevel10k/Zsh 插件、CodeGraph、Kotlin LSP、Java 21+ 和 JDT LS;缺失项目只在用户明确选择 `Y` 后安装或配置。Powerlevel10k 默认配置来自仓库内置的 `config/p10k.zsh`(当前 Rainbow/ASCII 单行紧凑主题)。
- `update.sh`:先通过 `pi update` 升级根组合包,再只升级当前已安装的终端环境和机器级依赖;未安装项直接跳过。升级前先查询并比较本地与远端版本,只有版本不同时才下载或替换;Powerlevel10k 配置优先从 `pi update` 后的已安装组合包读取,并与 `.zshrc` 受管块一起按内容比较后增量同步。
@@ -51,6 +52,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 路径保护。
- Codex fast mode 只为符合条件的 `openai-codex-responses` 请求设置 priority service tier,由 `/fast-mode` 在会话内控制。
- 权限策略默认允许常规工具,允许 FFF 工具;拒绝 Bash 直搜和敏感凭据路径;Git 非只读操作、包管理、外部目录、文件/系统/网络高风险操作与普通 MCP 调用先由 `pi-permission-system` 判为 `ask`
- `pi-permission-auto-review` 不是独立 `tool_call` gate,而是 `pi-permission-system` authorizer chain 中名为 `auto-review` 的链路;只复核权限基线产生的 `ask`,不会重复处理已 `allow` 或已 `deny` 的请求。
+9
View File
@@ -13,6 +13,7 @@
- `pi-lsp@0.1.7`:为 TypeScript/JavaScript、Kotlin 和 Java 提供 LSP 诊断、跳转与符号工具;TypeScript 后端由组合包内置。
- `pi-hermes-memory@0.9.6`:提供持久记忆、会话搜索和 secret scanning。
- `@ogulcancelik/pi-codex-compaction@0.1.3`:为 `openai-codex` 提供原生远程压缩。
- `pi-condense@2.9.1`:把已完成的工具调用批次总结为可恢复的短摘要,并通过 `context_tree_query` 按需取回原始输出;组合包首次加载时默认开启。
- 本仓库维护的 `pi-permission-auto-review`:作为 `pi-permission-system` authorizer,使用 Codex Guardian 风格策略自动复核 `ask` 请求。
- 本仓库维护的 `pi-permission-system`:从 `@gotgenes/pi-permission-system@26.2.1` 源码导入,负责工具、路径、MCP、硬拒绝和兜底权限基线。
- `extensions/tool-routing.ts`:保留 Pi 默认系统提示词,并按当前激活工具追加简短的工具与搜索路由规则;提供 `/dump-system-prompt` 导出当前有效提示词。
@@ -256,6 +257,14 @@ Hermes Memory 使用上游默认的 `policy-only` 模式:不把完整记忆直
达到 90% 上下文占用时触发,并写入 Pi 原生 compaction boundary。失败时保持原历史且不静默
回退到文本摘要;checkpoint 与创建它的 Codex model 绑定。
### Pi Condense
组合包首次加载时会在 `contextPrune.enabled` 尚未配置的情况下写入 `true`,因此 `pi-condense` 默认开启;用户通过 `/pruner off` 写入的显式 `false` 会被保留,后续启动或升级不会重新覆盖。它使用上游默认的 `agent-message` 触发模式:同一用户任务中的多轮工具调用会在最终文本回复后统一触发整理,默认仍按每个 assistant turn 分别生成摘要;后续上下文只保留摘要和 `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`
它与 Context Mode 的职责不同:Context Mode 避免大型原始输出进入当前模型上下文,Pi Condense 则压缩已经进入会话、且已完成使用的历史工具结果。它也不替代 Codex 原生远程 compaction;不希望自动整理历史时可运行 `/pruner off`
### Codex fast mode
该扩展只在 `openai-codex` provider 的 Responses 请求上加入
+33
View File
@@ -0,0 +1,33 @@
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
export type CondenseDefaultResult = "updated" | "unchanged" | "skipped-invalid";
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 {
const targetPath = join(agentDir, "settings.json");
let settings: Record<string, unknown> = {};
try {
const parsed: unknown = JSON.parse(readFileSync(targetPath, "utf8"));
if (!isObject(parsed)) return "skipped-invalid";
settings = parsed;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") return "skipped-invalid";
}
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 };
mkdirSync(dirname(targetPath), { recursive: true });
const temporaryPath = `${targetPath}.my-pi.tmp`;
writeFileSync(temporaryPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
renameSync(temporaryPath, targetPath);
return "updated";
}
+12
View File
@@ -0,0 +1,12 @@
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { ensureCondenseEnabledDefault } 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());
if (result === "skipped-invalid") {
console.warn("my-pi: skipped the pi-condense default because settings.json or contextPrune is invalid");
}
const { default: condenseExtension } = await import("../node_modules/pi-condense/index.ts");
condenseExtension(pi);
}
+20
View File
@@ -15,6 +15,7 @@
"@ogulcancelik/pi-codex-compaction": "0.1.3",
"@tavily/pi-extension": "0.1.2",
"context-mode": "1.0.169",
"pi-condense": "2.9.1",
"pi-context-view": "0.4.2",
"pi-hashline-edit": "0.8.3",
"pi-hermes-memory": "0.9.6",
@@ -5624,6 +5625,25 @@
"dev": true,
"license": "MIT"
},
"node_modules/pi-condense": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/pi-condense/-/pi-condense-2.9.1.tgz",
"integrity": "sha512-FDwd0RuncXTeUHmfUiKBxCnSjmgf4JOiZ83ABsxiw6IYPqIfmWWmouFVopDnfcL/36UOfyv3lP5ZV30pj2ffZA==",
"license": "MIT",
"engines": {
"node": ">=22.19.0"
},
"funding": {
"type": "buymeacoffee",
"url": "https://buymeacoffee.com/jjurasszek"
},
"peerDependencies": {
"@earendil-works/pi-ai": "*",
"@earendil-works/pi-coding-agent": "*",
"@earendil-works/pi-tui": "*",
"@sinclair/typebox": "*"
}
},
"node_modules/pi-context-view": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/pi-context-view/-/pi-context-view-0.4.2.tgz",
+2
View File
@@ -43,6 +43,7 @@
"./node_modules/@firstpick/pi-extension-codex-fast-mode/index.ts",
"./node_modules/pi-hermes-memory/src/index.ts",
"./node_modules/@ogulcancelik/pi-codex-compaction/index.ts",
"./extensions/condense.ts",
"./pi-permission-auto-review/index.ts",
"./extensions/permission-system.ts",
"./extensions/tool-routing.ts"
@@ -60,6 +61,7 @@
"@ogulcancelik/pi-codex-compaction": "0.1.3",
"@tavily/pi-extension": "0.1.2",
"context-mode": "1.0.169",
"pi-condense": "2.9.1",
"pi-context-view": "0.4.2",
"pi-hashline-edit": "0.8.3",
"pi-hermes-memory": "0.9.6",
+56
View File
@@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { ensureCondenseEnabledDefault } from "../extensions/condense-config.ts";
async function withAgentDir(run: (agentDir: string) => Promise<void>): Promise<void> {
const agentDir = await mkdtemp(join(tmpdir(), "my-pi-condense-"));
try {
await run(agentDir);
} finally {
await rm(agentDir, { recursive: true, force: true });
}
}
test("pi-condense defaults to enabled when contextPrune is absent", async () => {
await withAgentDir(async (agentDir) => {
assert.equal(ensureCondenseEnabledDefault(agentDir), "updated");
assert.deepEqual(JSON.parse(await readFile(join(agentDir, "settings.json"), "utf8")), {
contextPrune: { enabled: true },
});
});
});
test("pi-condense default preserves existing settings and contextPrune fields", 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");
assert.deepEqual(JSON.parse(await readFile(path, "utf8")), {
theme: "dark",
contextPrune: { minBatchChars: 4000, enabled: true },
});
});
});
test("pi-condense default respects an explicit enabled choice", async () => {
await withAgentDir(async (agentDir) => {
const path = join(agentDir, "settings.json");
const original = `${JSON.stringify({ contextPrune: { enabled: false } }, null, 2)}\n`;
await writeFile(path, original, "utf8");
assert.equal(ensureCondenseEnabledDefault(agentDir), "unchanged");
assert.equal(await readFile(path, "utf8"), original);
});
});
test("pi-condense default does 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(await readFile(path, "utf8"), "{not-json");
});
});