pi-openai-toolkit 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -17
- package/README_zh.md +11 -17
- package/package.json +12 -10
- package/src/config.ts +0 -69
- package/src/extension-runtime.ts +62 -17
- package/src/payload-rewrite.ts +40 -10
- package/src/remote-v2-client.ts +340 -99
- package/src/request-context-cache.ts +36 -3
- package/src/serializer.ts +4 -12
- package/src/types.ts +0 -19
- package/tsconfig.check.json +0 -1
- package/src/inline-compaction.ts +0 -1264
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
`pi-openai-toolkit` is an all-in-one OpenAI enhancement package for **Pi (>= 0.
|
|
13
|
+
`pi-openai-toolkit` is an all-in-one OpenAI enhancement package for **Pi (>= 0.85.1)**. It bundles five extensions designed for OpenAI Responses endpoints:
|
|
14
14
|
|
|
15
15
|
1. **Remote Responses Compaction v2** (`extensions/compaction.ts`): Server-side opaque context compaction using the `remote_compaction_v2` SSE streaming protocol. Preserves context fidelity without lossy text re-summarization.
|
|
16
16
|
2. **Model-Scoped Web Search** (`extensions/web-search.ts`): Toolkit-owned hosted Web Search for an exact `provider/model-id` allowlist configured with `webSearch.models`, across the supported `openai-responses` and `openai-codex-responses` APIs.
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
23
|
+
Requires **Pi >= 0.85.1** and **Node.js >= 22.19.0**; older Pi versions are not supported. Pi supplies the GPT-6 Astra catalog and OpenAI API Key/Codex subscription login; Toolkit does not implement a separate login flow.
|
|
24
|
+
|
|
23
25
|
### Install the published package globally (recommended)
|
|
24
26
|
|
|
25
27
|
```bash
|
|
@@ -104,11 +106,6 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
|
|
|
104
106
|
"model": null,
|
|
105
107
|
"thinkingLevel": "off"
|
|
106
108
|
},
|
|
107
|
-
"autoCompaction": {
|
|
108
|
-
"enabled": true,
|
|
109
|
-
"continuation": "inline",
|
|
110
|
-
"unsupportedFallback": "followUp"
|
|
111
|
-
},
|
|
112
109
|
"responsesApis": [
|
|
113
110
|
"openai-responses",
|
|
114
111
|
"openai-codex-responses"
|
|
@@ -174,10 +171,6 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
|
|
|
174
171
|
- `nativeFallback.enabled` (*boolean*, default: `true`): Master switch for toolkit-driven native compaction with an explicit summary model. It never touches the remote chain: `remote_compaction_v2` is still attempted and still wins when it succeeds. When `false`, only the post-failure native step is skipped, so every fallback falls straight through to Pi's own default compaction with the active model.
|
|
175
172
|
- `nativeFallback.model` (*string | null*, default: `null`): Summary model formatted as `provider/model-id` (e.g. `"openai/gpt-4o-mini"`) used **only** when the active model cannot use remote v2 at all (non-Responses API, or an API excluded by `responsesApis`). It is never consulted once a remote request was attempted, and `null` means the active model runs Pi's default compaction path.
|
|
176
173
|
- `nativeFallback.thinkingLevel` (*string*, default: `"off"`): Reasoning / thinking level passed to Pi's native `compact()` whenever this extension supplies the model (`"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`).
|
|
177
|
-
- `autoCompaction.enabled` (*boolean*, default: `true`): Check for compaction after a completed tool-result batch crosses Pi's context threshold.
|
|
178
|
-
- `autoCompaction.continuation` (*`"inline" | "followUp" | "off"`*, default: `"inline"`): `inline` compacts inside the current official Pi 0.84.3 tool loop and refreshes the next provider context; `followUp` uses Pi's public hidden follow-up message path; `off` disables toolkit-triggered automatic compaction.
|
|
179
|
-
- `autoCompaction.unsupportedFallback` (*`"followUp" | "off"`*, default: `"followUp"`): Behavior when the guarded official-host inline adapter is unavailable. It does not change normal remote/native compaction fallback ordering.
|
|
180
|
-
- `autoCompaction.reserveTokens` (*non-negative integer*, optional): Override only the automatic trigger reserve. When omitted, the active Pi compaction setting is used. Pi's `keepRecentTokens` remains authoritative for compactability.
|
|
181
174
|
- `responsesApis` (*string[]*): Narrow the supported remote compaction API identifiers (subset of `["openai-responses", "openai-codex-responses"]`).
|
|
182
175
|
- `notifyOnLoad` (*boolean*, default: `false`): Show a notification banner on Pi startup.
|
|
183
176
|
- `debug` (*boolean*, default: `false`): Output diagnostics and write execution artifacts to disk.
|
|
@@ -254,14 +247,15 @@ Content-Type: application/json
|
|
|
254
247
|
2. Remote request attempted but failed on a model that *can* compact remotely → Pi's native `compact()` with `remoteCompactModel`, or the active model when no override is configured;
|
|
255
248
|
3. Model that cannot use remote v2 at all → Pi's native `compact()` with `compaction.nativeFallback.model`, or the active model when unset;
|
|
256
249
|
|
|
257
|
-
### 2.
|
|
250
|
+
### 2. Pi-Native Auto-Compaction and Continuation
|
|
258
251
|
|
|
259
|
-
|
|
252
|
+
**Pi 0.85.1+** owns automatic compaction. After tool results are appended and before the next assistant request, Pi checks its threshold, compacts eligible history and continues in the same run. Toolkit only supplies Remote v2 results and opaque replay through public hooks: no private host patches, extra compaction driver or hidden follow-up messages.
|
|
260
253
|
|
|
261
|
-
-
|
|
262
|
-
-
|
|
263
|
-
-
|
|
264
|
-
-
|
|
254
|
+
- Configure `compaction.enabled`, `compaction.reserveTokens` and `compaction.keepRecentTokens` in Pi's `~/.pi/agent/settings.json` or project `.pi/settings.json`.
|
|
255
|
+
- When Pi disables automatic compaction or has no eligible history, Toolkit does not start another driver. Manual `/compact` still uses the existing remote/native fallback chain.
|
|
256
|
+
- The retired Toolkit `compaction.autoCompaction` setting is ignored as an unknown field; your settings file is not rewritten.
|
|
257
|
+
- Remote v2 covers the complete compact input. A public `context` hook removes verified retained copies before encoding; persisted history is unchanged and new/transient messages survive.
|
|
258
|
+
- Cache parameters follow Pi's actual selection: applicable GPT-5.6+ long-cache requests retain `prompt_cache_options.ttl: "30m"`, while those models retain `mode: "explicit"` when caching is disabled. Toolkit does not force long caching on every request, and a different compactor only receives supported cache fields.
|
|
265
259
|
|
|
266
260
|
### 3. Model-Scoped Web Search
|
|
267
261
|
|
|
@@ -314,7 +308,7 @@ GPT-6 Astra is a Codex-backend (ChatGPT OAuth) model with two wire behaviors tha
|
|
|
314
308
|
- **Stable effort with `configuration_update`**: OpenAI prompt caching compares the request from the first byte, and the request-level `reasoning.effort` sits at the top - changing thinking level mid-conversation normally busts the whole cached prefix. Astra instead accepts a `configuration_update` input item that changes effort from that point onward while the request-level field stays put. This extension implements that for the active conversation: the first request pins the baseline effort, every later change is spliced into the transcript at the position it should take effect (before the user message it applies to, or after the latest tool result inside a tool loop), and the same item is replayed at that position on every following request. The request body keeps the baseline byte-stable, so the cache prefix survives level changes.
|
|
315
309
|
- **History-rewrite safety**: Each spliced item is anchored to a fingerprint of the transcript position it was inserted at. If a later request's history no longer matches - compaction, branch switch, `/clear`, anything rewrote the transcript - the baseline is discarded and the next request re-pins its own effort, rather than placing an update at a stale position.
|
|
316
310
|
- **Compaction isolation**: The compact endpoints reject histories containing `configuration_update` items. This is enforced in two places: the payload rewriter never splices items into a `compaction_trigger`-shaped request, and both compaction clients strip any such item defensively from replayed input, so an Astra session can still be compacted by this package.
|
|
317
|
-
- **Scope**: The rewrite applies only to models listed in `codexAstra.models` on either Responses wire family (Astra is the only SKU known to accept the item; anything else answers HTTP 400 on it - the allowlist is what encodes that knowledge for gateways that proxy it), only when the request actually carries a wire-level effort, and never to `off`/`none`. Everything runs inside a try/catch: any unexpected payload shape or planner failure leaves the request untouched. To avoid double-splicing when both this toolkit and a host that implements the same behavior (Oh My Pi itself) are active, enable this layer on only one side.
|
|
311
|
+
- **Scope**: The rewrite applies only to models listed in `codexAstra.models` on either Responses wire family (Astra is the only SKU known to accept the item; anything else answers HTTP 400 on it - the allowlist is what encodes that knowledge for gateways that proxy it), only when the request actually carries a wire-level effort, and never to `off`/`none`. Gateway pass-through was verified live: a `configuration_update` item placed before the user turn it applies to returned HTTP 200 with a completed response. Everything runs inside a try/catch: any unexpected payload shape or planner failure leaves the request untouched. To avoid double-splicing when both this toolkit and a host that implements the same behavior (Oh My Pi itself) are active, enable this layer on only one side.
|
|
318
312
|
|
|
319
313
|
Catalog note: Pi's bundled `openai-codex` model list does not include `gpt-6-astra` yet. Register it in `~/.pi/agent/models.json` under the built-in provider (custom models merge into the built-in list): `api: "openai-codex-responses"`, `baseUrl: "https://chatgpt.com/backend-api"`, `contextWindow: 272000`, `maxTokens: 128000`, and a `thinkingLevelMap` exposing `low` through `max`. Pi's Codex OAuth login supplies the token; this package reads it through Pi's `ModelRegistry` and never touches the credential store directly.
|
|
320
314
|
|
package/README_zh.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
`pi-openai-toolkit` 是专为 **Pi (>= 0.
|
|
13
|
+
`pi-openai-toolkit` 是专为 **Pi (>= 0.85.1)** 打造的 OpenAI 增强工具包,集成了五项针对 OpenAI Responses 协议的扩展:
|
|
14
14
|
|
|
15
15
|
1. **远端无损压缩 (Remote Compaction v2)** (`extensions/compaction.ts`): 基于 `remote_compaction_v2` SSE 流式协议实现的服务端密文无损会话压缩,彻底告别会话变长后丢失细节的文本二次摘要。
|
|
16
16
|
2. **按模型启用的联网搜索** (`extensions/web-search.ts`): 通过 `webSearch.models` 精确匹配 `provider/model-id`,由 Toolkit 为符合条件的 `openai-responses` 与 `openai-codex-responses` 会话提供并拥有原生联网搜索。
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
## 安装使用
|
|
22
22
|
|
|
23
|
+
需要 **Pi >= 0.85.1** 与 **Node.js >= 22.19.0**;不再支持旧 Pi。GPT-6 Astra 的模型目录、OpenAI API Key 和 Codex 订阅登录由 Pi 提供,Toolkit 不重复实现登录流程。
|
|
24
|
+
|
|
23
25
|
### 全局安装已发布版本(推荐)
|
|
24
26
|
|
|
25
27
|
```bash
|
|
@@ -102,11 +104,6 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
|
|
|
102
104
|
"model": null,
|
|
103
105
|
"thinkingLevel": "off"
|
|
104
106
|
},
|
|
105
|
-
"autoCompaction": {
|
|
106
|
-
"enabled": true,
|
|
107
|
-
"continuation": "inline",
|
|
108
|
-
"unsupportedFallback": "followUp"
|
|
109
|
-
},
|
|
110
107
|
"responsesApis": [
|
|
111
108
|
"openai-responses",
|
|
112
109
|
"openai-codex-responses"
|
|
@@ -172,10 +169,6 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
|
|
|
172
169
|
- `nativeFallback.enabled` (*boolean*, 默认 `true`): Toolkit 用显式摘要模型跑原生压缩的总开关。它不影响远端链路:`remote_compaction_v2` 照常被尝试,成功时照常生效。设为 `false` 只跳过失败后的那一步原生压缩,因此所有降级都直接交给 Pi 默认的当前模型压缩。
|
|
173
170
|
- `nativeFallback.model` (*string | null*, 默认 `null`): 摘要模型,格式为 `"provider/model-id"`(例如 `"openai/gpt-4o-mini"`),**仅**用于根本无法走远端 v2 的当前模型(非 Responses API,或 API 被 `responsesApis` 排除)。远端请求已经发出过的情境不会读这个字段;为 `null` 时由当前活动模型走 Pi 默认压缩路径。
|
|
174
171
|
- `nativeFallback.thinkingLevel` (*string*, 默认 `"off"`): 由本扩展指定模型时,传给 Pi 原生 `compact()` 的思考级别。
|
|
175
|
-
- `autoCompaction.enabled` (*boolean*, 默认 `true`): 完整工具调用/结果批次结束并越过 Pi 上下文阈值后,检查是否需要压缩。
|
|
176
|
-
- `autoCompaction.continuation` (*`"inline" | "followUp" | "off"`*, 默认 `"inline"`): `inline` 在官方 Pi 0.84.3 当前工具循环内完成压缩并刷新下一次 Provider 上下文;`followUp` 使用 Pi 公共的隐藏后续消息;`off` 关闭 Toolkit 触发的自动压缩。
|
|
177
|
-
- `autoCompaction.unsupportedFallback` (*`"followUp" | "off"`*, 默认 `"followUp"`): 官方 Host 私有 inline 适配器不可用时的处理方式,不改变原有远端/原生压缩降级顺序。
|
|
178
|
-
- `autoCompaction.reserveTokens` (*非负整数*, 可选): 仅覆盖自动触发阈值的保留 Token 数。未设置时使用当前 Pi 压缩配置;是否具有可压缩历史仍以 Pi 的 `keepRecentTokens` 为准。
|
|
179
172
|
- `responsesApis` (*string[]*): 允许尝试远端压缩的 API 标识符子集(从 `["openai-responses", "openai-codex-responses"]` 中选定)。
|
|
180
173
|
- `notifyOnLoad` (*boolean*, 默认 `false`): Pi 启动加载时是否弹出气泡提示。
|
|
181
174
|
- `debug` (*boolean*, 默认 `false`): 是否开启调试模式并记录诊断日志。
|
|
@@ -252,14 +245,15 @@ Content-Type: application/json
|
|
|
252
245
|
2. 本可以远端压缩但网关报错或失败 → 用 `remoteCompactModel` 跑 Pi 原生 `compact()`,未配置覆盖模型则由当前活动模型执行;
|
|
253
246
|
3. 根本无法走远端的模型 → 用 `compaction.nativeFallback.model` 跑 Pi 原生 `compact()`,未配置则由当前活动模型执行;
|
|
254
247
|
|
|
255
|
-
### 2.
|
|
248
|
+
### 2. Pi 原生自动压缩与续跑
|
|
256
249
|
|
|
257
|
-
|
|
250
|
+
自动压缩完全由 **Pi 0.85.1+** 管理。Pi 在工具结果写入后、下一次 assistant 请求前检查上下文阈值,符合条件时在同一次运行中压缩并继续。Toolkit 只通过公共 Hook 提供 Remote v2 结果和密文回放,不安装私有补丁、不额外触发压缩,也不发送隐藏续跑消息。
|
|
258
251
|
|
|
259
|
-
-
|
|
260
|
-
-
|
|
261
|
-
-
|
|
262
|
-
-
|
|
252
|
+
- 在 Pi 的 `~/.pi/agent/settings.json` 或项目 `.pi/settings.json` 中配置 `compaction.enabled`、`compaction.reserveTokens` 和 `compaction.keepRecentTokens`。
|
|
253
|
+
- Pi 关闭自动压缩或没有可压缩历史时,Toolkit 不另开一套驱动;手动 `/compact` 仍使用原有远端/原生降级链路。
|
|
254
|
+
- Toolkit 配置中的旧 `compaction.autoCompaction` 已移除;旧键会按未知字段忽略,不会修改你的配置文件。
|
|
255
|
+
- Remote v2 已包含完整压缩输入。公共 `context` Hook 在编码前移除能确认已被 checkpoint 覆盖的近期消息副本;会话存档不变,压缩后的新消息和临时注入内容保留。
|
|
256
|
+
- 缓存参数遵循 Pi 的实际选择:适用的 GPT-5.6+ 长缓存保留 `prompt_cache_options.ttl: "30m"`,这些模型关闭缓存时保留 `mode: "explicit"`,不会对所有请求强制开启长缓存。不同压缩模型只接收其支持的参数。
|
|
263
257
|
|
|
264
258
|
### 3. 按模型启用的联网搜索
|
|
265
259
|
|
|
@@ -312,7 +306,7 @@ GPT-6 Astra 是 Codex 后端(ChatGPT OAuth)上的模型,有两个普通 Op
|
|
|
312
306
|
- **stable effort + `configuration_update`**:OpenAI 提示缓存从请求第一个字节开始比对,而请求级 `reasoning.effort` 就在头部——中途换思考档位通常会打爆整个缓存前缀。Astra 接受一种 `configuration_update` 输入项,从该项位置起改档位而不动请求级字段。本扩展把这套机制接到当前会话:首个请求钉住基线档位,之后的每次变更插入到它应生效的会话位置(作效于哪条用户消息就插在哪条之前,工具循环中途变更插在最近一条工具结果之后),后续请求在同一位置重放该项,请求体开头始终字节不变,缓存前缀存活。
|
|
313
307
|
- **历史重写安全**:每个插入项都锚定在它所在位置前一条内容的指纹上。后续请求的历史对不上(压缩、切分支、`/clear`,任何历史重写),基线直接作废,下一次请求用自己的档位重新钉基线,而不是把更新插到过期位置。
|
|
314
308
|
- **压缩隔离**:compact 端点拒绝含 `configuration_update` 的历史。两层防御:payload 改写器对带 `compaction_trigger` 的请求绝不插入;两个压缩客户端对回放输入里的该类项一律防御性剥离——所以 Astra 会话照常被本包压缩。
|
|
315
|
-
- **适用范围**:改写只作用于两种 Responses 线协议家族内 `codexAstra.models` 白名单中的模型(目前已知只有 astra 接受该 item,其他模型碰它就 400——网关代理同一模型时,白名单就是声明这件事的地方),且只改真正携带线上级 effort 的请求,`off`/`none`
|
|
309
|
+
- **适用范围**:改写只作用于两种 Responses 线协议家族内 `codexAstra.models` 白名单中的模型(目前已知只有 astra 接受该 item,其他模型碰它就 400——网关代理同一模型时,白名单就是声明这件事的地方),且只改真正携带线上级 effort 的请求,`off`/`none` 不动。网关透传已实测:插在生效用户消息之前的 `configuration_update` 返回 HTTP 200 且响应完整。整个钩子在 try/catch 里,任何意外形状或规划失败都原样放行请求。若 host 自身已实现同样行为(例如 Oh My Pi),两边只开一个,避免双重插入。
|
|
316
310
|
|
|
317
311
|
目录说明:Pi 内置的 `openai-codex` 模型列表还没有 `gpt-6-astra`,需要在 `~/.pi/agent/models.json` 内置 provider 下注册(自定义 models 会与内置列表合并):`api: "openai-codex-responses"`、`baseUrl: "https://chatgpt.com/backend-api"`、`contextWindow: 272000`、`maxTokens: 128000`,并用 `thinkingLevelMap` 声明 `low` 到 `max`。登录用 Pi 的 Codex OAuth 流程;本包通过 Pi 的 `ModelRegistry` 读凭据,不碰存储文件。
|
|
318
312
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-openai-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22.19.0"
|
|
8
|
+
},
|
|
6
9
|
"description": "OpenAI-focused Pi toolkit providing remote Responses compaction, native Web Search, native image generation, and model-reviewed auto mode.",
|
|
7
10
|
"author": "awoaCrim and contributors",
|
|
8
11
|
"license": "MIT",
|
|
@@ -48,7 +51,6 @@
|
|
|
48
51
|
"src/debug.ts",
|
|
49
52
|
"src/details-store.ts",
|
|
50
53
|
"src/deferred-tool-carryover.ts",
|
|
51
|
-
"src/inline-compaction.ts",
|
|
52
54
|
"src/extension-runtime.ts",
|
|
53
55
|
"src/image-generation/artifacts.ts",
|
|
54
56
|
"src/image-generation/client.ts",
|
|
@@ -97,16 +99,16 @@
|
|
|
97
99
|
"typecheck": "tsc -p tsconfig.check.json"
|
|
98
100
|
},
|
|
99
101
|
"peerDependencies": {
|
|
100
|
-
"@earendil-works/pi-agent-core": "
|
|
101
|
-
"@earendil-works/pi-ai": "
|
|
102
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
103
|
-
"@earendil-works/pi-tui": ">=0.
|
|
102
|
+
"@earendil-works/pi-agent-core": ">=0.85.1",
|
|
103
|
+
"@earendil-works/pi-ai": ">=0.85.1",
|
|
104
|
+
"@earendil-works/pi-coding-agent": ">=0.85.1",
|
|
105
|
+
"@earendil-works/pi-tui": ">=0.85.1"
|
|
104
106
|
},
|
|
105
107
|
"devDependencies": {
|
|
106
|
-
"@earendil-works/pi-agent-core": "0.
|
|
107
|
-
"@earendil-works/pi-ai": "0.
|
|
108
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
109
|
-
"@earendil-works/pi-tui": "0.
|
|
108
|
+
"@earendil-works/pi-agent-core": "0.85.1",
|
|
109
|
+
"@earendil-works/pi-ai": "0.85.1",
|
|
110
|
+
"@earendil-works/pi-coding-agent": "0.85.1",
|
|
111
|
+
"@earendil-works/pi-tui": "0.85.1",
|
|
110
112
|
"@types/node": "24.12.4",
|
|
111
113
|
"bun": "1.2.23",
|
|
112
114
|
"typescript": "5.9.3"
|
package/src/config.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_COMPACTION_CONFIG,
|
|
7
7
|
DEFAULT_TOOLKIT_CONFIG,
|
|
8
|
-
DEFAULT_AUTO_COMPACTION_CONFIG,
|
|
9
8
|
DEFAULT_AUTO_MODE_CONFIG,
|
|
10
9
|
DEFAULT_IMAGE_GENERATION_CONFIG,
|
|
11
10
|
DEFAULT_CODEX_ASTRA_CONFIG,
|
|
@@ -47,7 +46,6 @@ const COMPACTION_FIELDS = new Set([
|
|
|
47
46
|
"allowCompactionContinuityBreak",
|
|
48
47
|
"remoteCompactModel",
|
|
49
48
|
"nativeFallback",
|
|
50
|
-
"autoCompaction",
|
|
51
49
|
"responsesApis",
|
|
52
50
|
"notifyOnLoad",
|
|
53
51
|
"debug",
|
|
@@ -57,7 +55,6 @@ const COMPACTION_FIELDS = new Set([
|
|
|
57
55
|
"artifactRoot",
|
|
58
56
|
]);
|
|
59
57
|
const NATIVE_FALLBACK_FIELDS = new Set(["enabled", "model", "thinkingLevel"]);
|
|
60
|
-
const AUTO_COMPACTION_FIELDS = new Set(["enabled", "continuation", "unsupportedFallback", "reserveTokens"]);
|
|
61
58
|
const WEB_SEARCH_FIELDS = new Set(["enabled", "models"]);
|
|
62
59
|
const IMAGE_GENERATION_FIELDS = new Set(["enabled"]);
|
|
63
60
|
const CODEX_ASTRA_FIELDS = new Set(["enabled", "models"]);
|
|
@@ -153,37 +150,6 @@ function toThinkingLevel(value: unknown, fieldPath: string, warnings: string[]):
|
|
|
153
150
|
return undefined;
|
|
154
151
|
}
|
|
155
152
|
|
|
156
|
-
function toCompactionContinuation(
|
|
157
|
-
value: unknown,
|
|
158
|
-
fieldPath: string,
|
|
159
|
-
warnings: string[],
|
|
160
|
-
): "inline" | "followUp" | "off" | undefined {
|
|
161
|
-
if (value === undefined) return undefined;
|
|
162
|
-
if (value === "inline" || value === "followUp" || value === "off") return value;
|
|
163
|
-
warnings.push(`Ignoring ${fieldPath}: expected one of inline, followUp, off.`);
|
|
164
|
-
return undefined;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function toUnsupportedFallback(
|
|
168
|
-
value: unknown,
|
|
169
|
-
fieldPath: string,
|
|
170
|
-
warnings: string[],
|
|
171
|
-
): "followUp" | "off" | undefined {
|
|
172
|
-
if (value === undefined) return undefined;
|
|
173
|
-
if (value === "followUp" || value === "off") return value;
|
|
174
|
-
warnings.push(`Ignoring ${fieldPath}: expected one of followUp, off.`);
|
|
175
|
-
return undefined;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function toNonNegativeInteger(value: unknown, fieldPath: string, warnings: string[]): number | undefined {
|
|
179
|
-
if (value === undefined) return undefined;
|
|
180
|
-
if (typeof value === "number" && Number.isInteger(value) && Number.isFinite(value) && value >= 0) {
|
|
181
|
-
return value;
|
|
182
|
-
}
|
|
183
|
-
warnings.push(`Ignoring ${fieldPath}: expected a non-negative integer.`);
|
|
184
|
-
return undefined;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
153
|
function toBoundedInteger(
|
|
188
154
|
value: unknown,
|
|
189
155
|
fieldPath: string,
|
|
@@ -264,7 +230,6 @@ function cloneDefaults(): ToolkitConfig {
|
|
|
264
230
|
compaction: {
|
|
265
231
|
...DEFAULT_COMPACTION_CONFIG,
|
|
266
232
|
nativeFallback: { ...DEFAULT_NATIVE_FALLBACK_CONFIG },
|
|
267
|
-
autoCompaction: { ...DEFAULT_AUTO_COMPACTION_CONFIG },
|
|
268
233
|
responsesApis: [...DEFAULT_COMPACTION_CONFIG.responsesApis],
|
|
269
234
|
},
|
|
270
235
|
webSearch: {
|
|
@@ -305,32 +270,6 @@ function applyNativeFallbackConfig(
|
|
|
305
270
|
resolved.thinkingLevel;
|
|
306
271
|
}
|
|
307
272
|
|
|
308
|
-
function applyAutoCompactionConfig(
|
|
309
|
-
raw: Record<string, unknown>,
|
|
310
|
-
resolved: CompactionConfig["autoCompaction"],
|
|
311
|
-
warnings: string[],
|
|
312
|
-
): void {
|
|
313
|
-
warnUnknownFields(raw, AUTO_COMPACTION_FIELDS, "compaction.autoCompaction", warnings);
|
|
314
|
-
resolved.enabled = toBoolean(raw.enabled, "compaction.autoCompaction.enabled", warnings) ?? resolved.enabled;
|
|
315
|
-
resolved.continuation =
|
|
316
|
-
toCompactionContinuation(raw.continuation, "compaction.autoCompaction.continuation", warnings) ??
|
|
317
|
-
resolved.continuation;
|
|
318
|
-
resolved.unsupportedFallback =
|
|
319
|
-
toUnsupportedFallback(
|
|
320
|
-
raw.unsupportedFallback,
|
|
321
|
-
"compaction.autoCompaction.unsupportedFallback",
|
|
322
|
-
warnings,
|
|
323
|
-
) ?? resolved.unsupportedFallback;
|
|
324
|
-
const reserveTokens = toNonNegativeInteger(
|
|
325
|
-
raw.reserveTokens,
|
|
326
|
-
"compaction.autoCompaction.reserveTokens",
|
|
327
|
-
warnings,
|
|
328
|
-
);
|
|
329
|
-
if (reserveTokens !== undefined) {
|
|
330
|
-
resolved.reserveTokens = reserveTokens;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
273
|
function applyCompactionConfig(
|
|
335
274
|
raw: Record<string, unknown>,
|
|
336
275
|
resolved: CompactionConfig,
|
|
@@ -375,14 +314,6 @@ function applyCompactionConfig(
|
|
|
375
314
|
}
|
|
376
315
|
}
|
|
377
316
|
|
|
378
|
-
if (raw.autoCompaction !== undefined) {
|
|
379
|
-
if (isRecord(raw.autoCompaction)) {
|
|
380
|
-
applyAutoCompactionConfig(raw.autoCompaction, resolved.autoCompaction, warnings);
|
|
381
|
-
} else {
|
|
382
|
-
warnings.push("Ignoring compaction.autoCompaction: expected a JSON object.");
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
317
|
const apis = toSupportedApis(
|
|
387
318
|
raw.responsesApis,
|
|
388
319
|
"compaction.responsesApis",
|
package/src/extension-runtime.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BeforeProviderRequestEvent,
|
|
3
|
+
ContextEvent,
|
|
3
4
|
CompactionResult,
|
|
4
5
|
ExtensionAPI,
|
|
5
6
|
ExtensionContext,
|
|
@@ -11,11 +12,11 @@ import { resolveLatestNativeCompactionEntry } from "./details-store";
|
|
|
11
12
|
import { runNativeFallbackCompaction } from "./native-fallback";
|
|
12
13
|
import {
|
|
13
14
|
rewriteResponsesPayloadWithNativeReplay,
|
|
15
|
+
removeNativeCompactionRetainedMessages,
|
|
14
16
|
serializeLiveTailToResponsesInput,
|
|
15
17
|
} from "./payload-rewrite";
|
|
16
18
|
import { getCompactionRequestExtras, rememberRequestContext } from "./request-context-cache";
|
|
17
19
|
import { executeRemoteV2Compaction } from "./remote-v2-client";
|
|
18
|
-
import { registerInlineCompactionRuntime } from "./inline-compaction";
|
|
19
20
|
import {
|
|
20
21
|
resolveNativeCompactionEnvironment,
|
|
21
22
|
resolveRemoteCompactionExecution,
|
|
@@ -33,6 +34,12 @@ import {
|
|
|
33
34
|
type NativeCompactionRequestMeta,
|
|
34
35
|
} from "./types";
|
|
35
36
|
|
|
37
|
+
type CompactionDependencies = {
|
|
38
|
+
loadConfig: typeof loadToolkitConfig;
|
|
39
|
+
remoteCompact: typeof executeRemoteV2Compaction;
|
|
40
|
+
nativeFallback: typeof runNativeFallbackCompaction;
|
|
41
|
+
};
|
|
42
|
+
|
|
36
43
|
type ResponsesCompactOutcome =
|
|
37
44
|
| { outcome: "success"; compaction: CompactionResult<NativeCompactionDetails> }
|
|
38
45
|
| { outcome: "aborted" }
|
|
@@ -98,6 +105,7 @@ async function runResponsesNativeCompact(
|
|
|
98
105
|
ctx: ExtensionContext,
|
|
99
106
|
config: CompactionConfig,
|
|
100
107
|
execution: RemoteCompactionExecution,
|
|
108
|
+
remoteCompact: typeof executeRemoteV2Compaction,
|
|
101
109
|
): Promise<ResponsesCompactOutcome> {
|
|
102
110
|
const { consumer, compactor } = execution;
|
|
103
111
|
const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
|
|
@@ -179,12 +187,12 @@ async function runResponsesNativeCompact(
|
|
|
179
187
|
model: consumer.model,
|
|
180
188
|
baseUrl: consumer.baseUrl,
|
|
181
189
|
sessionId: getSessionId(ctx),
|
|
182
|
-
});
|
|
190
|
+
}, compactor.currentModel);
|
|
183
191
|
if (extras) {
|
|
184
192
|
request = { ...request, ...extras };
|
|
185
193
|
}
|
|
186
194
|
|
|
187
|
-
const compactResult = await
|
|
195
|
+
const compactResult = await remoteCompact({
|
|
188
196
|
runtime: compactor,
|
|
189
197
|
request,
|
|
190
198
|
signal: event.signal,
|
|
@@ -287,8 +295,12 @@ async function runResponsesNativeCompact(
|
|
|
287
295
|
return { outcome: "success", compaction };
|
|
288
296
|
}
|
|
289
297
|
|
|
290
|
-
async function handleSessionBeforeCompact(
|
|
291
|
-
|
|
298
|
+
async function handleSessionBeforeCompact(
|
|
299
|
+
event: SessionBeforeCompactEvent,
|
|
300
|
+
ctx: ExtensionContext,
|
|
301
|
+
dependencies: CompactionDependencies,
|
|
302
|
+
) {
|
|
303
|
+
const { config: toolkitConfig } = dependencies.loadConfig();
|
|
292
304
|
const config = toolkitConfig.compaction;
|
|
293
305
|
if (!config.enabled) {
|
|
294
306
|
return undefined;
|
|
@@ -327,7 +339,7 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
327
339
|
);
|
|
328
340
|
if (resolution.ok) {
|
|
329
341
|
remoteAttempted = true;
|
|
330
|
-
const responsesOutcome = await runResponsesNativeCompact(event, ctx, config, resolution.execution);
|
|
342
|
+
const responsesOutcome = await runResponsesNativeCompact(event, ctx, config, resolution.execution, dependencies.remoteCompact);
|
|
331
343
|
if (responsesOutcome.outcome === "success") {
|
|
332
344
|
return { compaction: responsesOutcome.compaction };
|
|
333
345
|
}
|
|
@@ -362,7 +374,7 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
362
374
|
// Branch 2: run pi's native compaction method. A failed remote request is compacted by the
|
|
363
375
|
// remote producer itself; a model that cannot use remote v2 at all uses nativeFallback.model.
|
|
364
376
|
const fallbackModelSpec = remoteAttempted ? config.remoteCompactModel : config.nativeFallback.model;
|
|
365
|
-
const fallback = await
|
|
377
|
+
const fallback = await dependencies.nativeFallback({ ctx, event, config, modelSpec: fallbackModelSpec });
|
|
366
378
|
if (fallback.ok) {
|
|
367
379
|
if (ctx.hasUI) {
|
|
368
380
|
ctx.ui.notify(
|
|
@@ -414,8 +426,35 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
414
426
|
return undefined;
|
|
415
427
|
}
|
|
416
428
|
|
|
417
|
-
async function
|
|
418
|
-
const { config:
|
|
429
|
+
async function handleContext(event: ContextEvent, ctx: ExtensionContext, loadConfig: typeof loadToolkitConfig) {
|
|
430
|
+
const { config: { compaction: config } } = loadConfig();
|
|
431
|
+
if (!config.enabled) return undefined;
|
|
432
|
+
// Resolve the effective authenticated endpoint before dropping any history.
|
|
433
|
+
// A checkpoint for a different endpoint must keep Pi's original context.
|
|
434
|
+
const resolution = await resolveNativeCompactionEnvironment(ctx, {
|
|
435
|
+
enabled: config.enabled,
|
|
436
|
+
responsesApis: config.responsesApis,
|
|
437
|
+
});
|
|
438
|
+
if (!resolution.ok) return undefined;
|
|
439
|
+
const branchEntries = ctx.sessionManager.getBranch();
|
|
440
|
+
const latest = resolveLatestNativeCompactionEntry(branchEntries, { baseUrl: resolution.runtime.baseUrl });
|
|
441
|
+
if (!latest.ok) return undefined;
|
|
442
|
+
const result = removeNativeCompactionRetainedMessages({ messages: event.messages, branchEntries, compactionEntry: latest.entry });
|
|
443
|
+
if (!result.ok) {
|
|
444
|
+
writeReplayFailureArtifact({ reason: result.reason, compactionEntryId: latest.entry.id }, config, ctx);
|
|
445
|
+
if (ctx.hasUI) ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: replay failed (${result.reason}); request aborted`, "error");
|
|
446
|
+
ctx.abort();
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
return result.messages === event.messages ? undefined : { messages: result.messages };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function handleBeforeProviderRequest(
|
|
453
|
+
event: BeforeProviderRequestEvent,
|
|
454
|
+
ctx: ExtensionContext,
|
|
455
|
+
loadConfig: typeof loadToolkitConfig,
|
|
456
|
+
) {
|
|
457
|
+
const { config: toolkitConfig } = loadConfig();
|
|
419
458
|
const config = toolkitConfig.compaction;
|
|
420
459
|
if (!config.enabled) {
|
|
421
460
|
return undefined;
|
|
@@ -570,11 +609,18 @@ async function handleBeforeProviderRequest(event: BeforeProviderRequestEvent, ct
|
|
|
570
609
|
return rewrite.rewrittenPayload;
|
|
571
610
|
}
|
|
572
611
|
|
|
573
|
-
export default function (
|
|
574
|
-
|
|
575
|
-
|
|
612
|
+
export default function registerCompactionExtension(
|
|
613
|
+
pi: ExtensionAPI,
|
|
614
|
+
overrides: Partial<CompactionDependencies> = {},
|
|
615
|
+
) {
|
|
616
|
+
const dependencies: CompactionDependencies = {
|
|
617
|
+
loadConfig: loadToolkitConfig,
|
|
618
|
+
remoteCompact: executeRemoteV2Compaction,
|
|
619
|
+
nativeFallback: runNativeFallbackCompaction,
|
|
620
|
+
...overrides,
|
|
621
|
+
};
|
|
576
622
|
pi.on("session_start", (_event, ctx) => {
|
|
577
|
-
const { config: toolkitConfig, source, warnings } =
|
|
623
|
+
const { config: toolkitConfig, source, warnings } = dependencies.loadConfig();
|
|
578
624
|
const config = toolkitConfig.compaction;
|
|
579
625
|
if (!config.enabled) return;
|
|
580
626
|
|
|
@@ -604,8 +650,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
604
650
|
}
|
|
605
651
|
});
|
|
606
652
|
|
|
607
|
-
pi.on("
|
|
608
|
-
pi.on("
|
|
609
|
-
pi.on("
|
|
610
|
-
pi.on("session_shutdown", (_event, ctx) => inlineCompaction.dispose(ctx));
|
|
653
|
+
pi.on("context", (event, ctx) => handleContext(event, ctx, dependencies.loadConfig));
|
|
654
|
+
pi.on("session_before_compact", (event, ctx) => handleSessionBeforeCompact(event, ctx, dependencies));
|
|
655
|
+
pi.on("before_provider_request", (event, ctx) => handleBeforeProviderRequest(event, ctx, dependencies.loadConfig));
|
|
611
656
|
}
|
package/src/payload-rewrite.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
3
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
4
|
import type {
|
|
@@ -33,7 +34,8 @@ export type NativeReplayPayloadRewriteFailureReason =
|
|
|
33
34
|
| "first-kept-entry-not-found"
|
|
34
35
|
| "invalid-compacted-window"
|
|
35
36
|
| "unexpected-compaction-after-boundary"
|
|
36
|
-
| "compaction-summary-not-found"
|
|
37
|
+
| "compaction-summary-not-found"
|
|
38
|
+
| "retained-context-mismatch";
|
|
37
39
|
|
|
38
40
|
export type NativeReplayPayloadRewriteFailure = {
|
|
39
41
|
ok: false;
|
|
@@ -311,6 +313,41 @@ export function collectLiveTailMessages(entries: readonly SessionEntry[]): Agent
|
|
|
311
313
|
return collectReplayMessages(entries);
|
|
312
314
|
}
|
|
313
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Remote V2 covers the complete pre-compaction context. Pi 0.85.1 also retains
|
|
318
|
+
* recent messages, so remove those verified copies before provider conversion
|
|
319
|
+
* (where IDs/fields may change across models). Persisted session entries are
|
|
320
|
+
* untouched. Preserve transient additions and all post-compaction messages.
|
|
321
|
+
*/
|
|
322
|
+
export function removeNativeCompactionRetainedMessages(args: {
|
|
323
|
+
messages: AgentMessage[];
|
|
324
|
+
branchEntries: readonly SessionEntry[];
|
|
325
|
+
compactionEntry: NativeCompactionEntry;
|
|
326
|
+
}): { ok: true; messages: AgentMessage[] } | NativeReplayPayloadRewriteFailure {
|
|
327
|
+
const boundary = findCompactionBoundaryIndex(args.branchEntries, args.compactionEntry.id);
|
|
328
|
+
if (boundary === undefined) return { ok: false, reason: "compaction-boundary-not-found" };
|
|
329
|
+
const firstKept = findEntryIndexByIdBeforeBoundary(args.branchEntries, args.compactionEntry.firstKeptEntryId, boundary);
|
|
330
|
+
if (firstKept === undefined) return { ok: false, reason: "first-kept-entry-not-found" };
|
|
331
|
+
const summary = sessionEntryToContextMessages(args.compactionEntry)[0];
|
|
332
|
+
const summaryIndex = args.messages.findIndex((message) => areEquivalentValues(message, summary));
|
|
333
|
+
if (summaryIndex < 0) return { ok: false, reason: "compaction-summary-not-found" };
|
|
334
|
+
const retained = args.branchEntries.slice(firstKept, boundary).flatMap(sessionEntryToContextMessages);
|
|
335
|
+
const removed = new Set<number>();
|
|
336
|
+
let cursor = summaryIndex + 1;
|
|
337
|
+
for (const expected of retained) {
|
|
338
|
+
const index = args.messages.findIndex((message, index) => index >= cursor && areEquivalentValues(message, expected));
|
|
339
|
+
if (index >= 0) {
|
|
340
|
+
removed.add(index);
|
|
341
|
+
cursor = index + 1;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Already removed by another context handler: leave it alone. Partial
|
|
345
|
+
// matches are ambiguous; never remove half a call/result batch.
|
|
346
|
+
if (removed.size === 0) return { ok: true, messages: args.messages };
|
|
347
|
+
if (removed.size !== retained.length) return { ok: false, reason: "retained-context-mismatch" };
|
|
348
|
+
return { ok: true, messages: args.messages.filter((_, index) => !removed.has(index)) };
|
|
349
|
+
}
|
|
350
|
+
|
|
314
351
|
export function serializeLiveTailToResponsesInput<TApi extends Api>(args: {
|
|
315
352
|
model: Model<TApi>;
|
|
316
353
|
entries: readonly SessionEntry[];
|
|
@@ -370,15 +407,8 @@ function buildNativeReplaySegmentsInternal<TApi extends Api>(args: {
|
|
|
370
407
|
};
|
|
371
408
|
}
|
|
372
409
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
// Pi-authored compaction summary item. Pi 0.84.3 renders the context after a
|
|
377
|
-
// compaction as [leading prompts][summary item][all post-compaction messages]:
|
|
378
|
-
// pre-compaction messages never appear in the payload (entry.retainedTail is
|
|
379
|
-
// never persisted by appendCompaction), so the summary item itself is the only
|
|
380
|
-
// anchor and the only thing that gets replaced. Everything else is preserved
|
|
381
|
-
// verbatim, so transient messages injected by other extensions stay intact.
|
|
410
|
+
// The context hook has removed Pi's verified retained copies. Replace only
|
|
411
|
+
// the summary anchor here; preserve provider fields and transient/post items.
|
|
382
412
|
const summaryIndex = findCompactionSummaryIndex(args.payload.input, args.compactionEntry.summary);
|
|
383
413
|
if (summaryIndex < 0) {
|
|
384
414
|
return {
|