# pi-jev-compaction

[English](./README.md)

Pi Coding Agent 扩展：用 TypeSafe **Jev** 的保留 / 丢弃 / 截断决策，替换（或增强）Pi 默认的 LLM 摘要 compaction。

对 tool call 与 result 打分，丢弃或截断过时内容，用户 / 助手文本原样保留。失败时软回退到 Pi 内置 compaction。

算法与提问格式改编自 [fast-jev-compaction](https://github.com/tamaratran/fast-jev-compaction)（MIT）。该仓库的 Claude Code 插件可以**整表替换**消息列表；Pi 做不到这一点，见下方 [Approach](#approach--limitations-vs-claude-code)。

## Install（安装）

每个使用者自备 TypeSafe 密钥。不要把个人 `TYPESAFE_API_KEY` 写进仓库、设置文件或发给同事。

```bash
# 发布到 npm 之后
pi install npm:pi-jev-compaction

# 固定到本版本（或使用 @latest 获取最新版本）
pi install npm:pi-jev-compaction@1.0.0

# 或从本仓库本地路径（无需发布）
git clone <this-repo>
cd pi-jev-compaction
npm install
pi install /absolute/path/to/pi-jev-compaction

# 仅本次会话试用
pi -e /absolute/path/to/pi-jev-compaction/src/extension.ts
```

项目级 npm 安装（写入 `.pi/settings.json`，便于团队共享**扩展本身**，而不是密钥）：

```bash
pi install -l npm:pi-jev-compaction
```

Pi 会把不带前缀的包名当作本地路径；从 npm registry 安装时始终使用 `npm:` 前缀。

然后每位同事在自己的环境里导出自己的 key：

```bash
export TYPESAFE_API_KEY="sk-..."   # https://console.typesafe.ai/settings/keys
```

重启 Pi 或 `/reload`。之后 `/compact` 以及阈值 / overflow 自动 compaction 都会先走 `session_before_compact`。

会话开始时扩展会 **toast 一次**（内存标记，不写盘；`/reload` 不再刷）：

- 已检测到 `TYPESAFE_API_KEY`：`pi-jev-compaction: ready…`
- 缺失：给出申请地址、`export TYPESAFE_API_KEY`、可选阈值文件，并提示 `/jev-setup` 与 `/jev-status`

## Key setup（密钥）

| 谁 | 怎么做 |
| --- | --- |
| 你自己 | 在 shell profile 或会话环境里设置 `TYPESAFE_API_KEY`。 |
| 同事 / 开源贡献者 | **各自申请** TypeSafe key。本扩展不会、也不应该内置或转发别人的 key。 |
| CI | 用 runner secret 注入 `TYPESAFE_API_KEY`。没有 key 时扩展会静默回退到 Pi 内置摘要。 |

**禁止**把 key 写进 `~/.config/pi-jev-compaction/config.json`。该文件只接受阈值，解析与 `/jev-setup` 都会丢掉任何 `apiKey` 字段。

### Commands（命令）

| Command | 作用 |
| --- | --- |
| `/jev-status` | 是否检测到 key（只显示 detected/missing，**永不打印密钥**）、config 路径、生效选项、本次会话上次 compact 结果 |
| `/jev-setup` | 引导申请 key + `export TYPESAFE_API_KEY`；把阈值写入 `config.json`。交互确认，或 `/jev-setup defaults`，或粘贴 JSON（会剥掉 apiKey） |

```text
/jev-status
/jev-setup
/jev-setup defaults
/jev-setup {"keepThreshold":0.6,"minReductionRatio":0.3}
```

## Options（选项）

默认值对齐 Claude 插件。

| Option | Default | 说明 |
| --- | ---: | --- |
| `keepThreshold` | `0.5` | Jev 保留 call / result 的最低概率 |
| `preserveRecentMessages` | `6` | 最新 N 条 Jev 消息永不改动（第一条也始终保留） |
| `minReductionRatio` | `0.25` | 字符减少比例低于此值则回退 Pi 默认摘要 |
| `maxStateTokens` | `25000` | 发给 Jev 的 state 估计上限 |
| `maxRequestTokens` | `30000` | 单次请求（state + 问题）估计上限 |
| `truncateHeadChars` | `300` | 被截断的 tool result 保留的头部字符数 |
| `model` | `jev-latest` | Jev 模型 |
| `baseUrl` | `https://api.typesafe.ai/v1/systemone` | System One 端点 |

可选配置文件（**仅阈值**）：

`~/.config/pi-jev-compaction/config.json`（或 `$XDG_CONFIG_HOME/pi-jev-compaction/config.json`）

```json
{
  "keepThreshold": 0.5,
  "preserveRecentMessages": 6,
  "minReductionRatio": 0.25,
  "maxStateTokens": 25000,
  "maxRequestTokens": 30000,
  "truncateHeadChars": 300,
  "model": "jev-latest"
}
```

环境变量覆盖：`PI_JEV_MODEL`、`PI_JEV_BASE_URL`、`PI_JEV_KEEP_THRESHOLD`、`PI_JEV_PRESERVE_RECENT`、`PI_JEV_MIN_REDUCTION`、`PI_JEV_MAX_STATE_TOKENS`、`PI_JEV_MAX_REQUEST_TOKENS`、`PI_JEV_TRUNCATE_HEAD`、`PI_JEV_GOAL`。

## Fallback & toasts（回退与提示）

扩展在以下情况**不**返回 `compaction`，Pi 继续走内置 LLM 摘要。每条路径都有对应 toast（含 `manual` / `threshold` / `overflow`）：

| 路径 | Toast |
| --- | --- |
| 无 key | 回退 + 如何配置（`/jev-setup`、申请地址） |
| Jev 网络 / 鉴权 / 畸形响应 | 原因 + 回退 |
| 历史塞不进 state 预算 | 原因 + 回退 |
| `reductionRatio < minReductionRatio` | “not worth it” + 回退 |
| 很短 / 没有可打分的 tool call | 跳过 Jev，说明后回退 |
| `AbortSignal` 中止 | `session_before_compact` 静默；`session_compact_failed` 短提示 |
| 成功 | kept / truncated / dropped、大约减少百分比、耗时 |

`session_compact` 会再确认保存的是 Jev 逐字摘要还是 Pi 内置摘要。

## Approach & limitations vs Claude Code

Pi 的 compaction 是 **summary + cut-point**，不是 Claude Code 那种整表替换。

已核对 `@earendil-works/pi-coding-agent@0.85.1`：

- 事件：`session_before_compact`，载荷为 `SessionBeforeCompactEvent`（`preparation`、`branchEntries`、`reason`: `manual` \| `threshold` \| `overflow`、`signal`）。
- 处理器可返回 `SessionBeforeCompactResult`：`{ cancel?: boolean; compaction?: CompactionResult }`。
- `CompactionResult`：`{ summary, firstKeptEntryId, tokensBefore, estimatedTokensAfter?, usage?, details? }`。
- `SessionManager` 是 **append-only**：`ctx.sessionManager` 只读，**不能**改写或删除历史条目。没有等价于 Claude `session.compact → { messages }` 的 API。

因此本扩展的做法是：

1. 把 `messagesToSummarize` + `turnPrefixMessages` + 从 `firstKeptEntryId` 起的 kept 消息转成 Jev `Message[]`（外加可选的 `previousSummary`）。
2. 用同一套 `compact()`：给每个未 pinned 的 tool call 问两个 `noul`（保留 call / 保留完整 result）。
3. 只把 **compactable 区域**里被 prune / truncate 后的内容编成一份**逐字结构化 summary**（用户 / 助手文本原样保留；丢掉的 tool call 不出现；截断的 result 留 head + 说明）。
4. 返回 `{ summary, firstKeptEntryId: preparation.firstKeptEntryId, tokensBefore, details }`。Pi 仍会把 cut-point 之后的条目原样发给模型。

做不到的事（相对 Claude 插件）：

- 无法就地改写「将保留」区域内的 tool result。那些消息落在 `firstKeptEntryId` 之后，会按原文进入下一轮上下文。`preserveRecentMessages` 会把它们 pin 住，Jev 也不会去截它们。
- 无法把 prune 后的旧消息重新插回 session 树；它们只能活在 `summary` 字符串里。
- 因此这是「无损于文本、有损于 tool 体积」的 **summary 编码**，不是 Claude 那种对象级 transcript 替换。

## Library API

```ts
import {
  compactPiSession,
  compact,
  type JevAsker,
} from "pi-jev-compaction";

const outcome = await compactPiSession({
  messagesToSummarize,
  turnPrefixMessages,
  keptMessages,
  firstKeptEntryId: preparation.firstKeptEntryId,
  tokensBefore: preparation.tokensBefore,
  asker, // fake JevAsker in tests; omit to use TYPESAFE_API_KEY
});
if (outcome.ok) {
  return { compaction: outcome.compaction };
}
// else: let Pi compact
```

`compact(messages, asker, options)` 与上游 `fast-jev-compaction` 相同。测试应注入 `JevAsker`，不要打网络。

## Development

```bash
npm install
npm test
npm run typecheck
npm run build
```

单元测试使用假 `JevAsker`，不访问 TypeSafe。

## License

MIT. Jev 核心见 `src/jev/` 与 `NOTICE`（改编自 fast-jev-compaction）。
