pi-openai-toolkit 0.16.0 → 0.17.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 +59 -89
- package/README.zh.md +57 -87
- package/docs/internals.md +7 -5
- package/package.json +9 -9
- package/src/compaction-projection.ts +35 -31
- package/src/extension-runtime.ts +16 -20
- package/src/payload-rewrite.ts +34 -64
- package/src/pi-context-hook.ts +117 -116
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-openai-toolkit
|
|
2
2
|
|
|
3
|
-
Add Codex context windows, Responses compaction, hosted tools, and
|
|
3
|
+
Add Codex context windows, Responses compaction, hosted tools, and tool-call review to Pi.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/pi-openai-toolkit)
|
|
6
6
|
[](LICENSE)
|
|
@@ -11,39 +11,37 @@ Add Codex context windows, Responses compaction, hosted tools, and reviewed tool
|
|
|
11
11
|
|
|
12
12
|
| Feature | Use it to |
|
|
13
13
|
| --- | --- |
|
|
14
|
-
| Codex Remote Context |
|
|
15
|
-
| Remote Compaction v2 | Continue
|
|
16
|
-
| Web Search
|
|
17
|
-
| Image generation |
|
|
18
|
-
| Tool-call review |
|
|
14
|
+
| Codex Remote Context | Start new windows with the adapted Codex protocol and retrieve earlier work through `history`. |
|
|
15
|
+
| Remote Compaction v2 | Continue Responses sessions with encrypted server checkpoints. |
|
|
16
|
+
| Web Search | Choose local `pi-web-access`, Responses `web_search`, or experimental CPA `web_run` per model. |
|
|
17
|
+
| Image generation | Generate or edit images with the hosted Responses image tool. |
|
|
18
|
+
| Tool-call review | Have a reviewer model approve selected calls through Toolkit's approval gate. |
|
|
19
19
|
|
|
20
|
-
The package uses Pi's existing
|
|
20
|
+
The package uses Pi's existing models, authentication, and sessions. It does not add a provider or model.
|
|
21
21
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
24
|
-
Requires Pi 0.
|
|
24
|
+
Requires Pi 0.87.0+ and Node.js 22.19.0+.
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
pi install npm:pi-openai-toolkit
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Without a config file, Remote Compaction v2 is enabled for eligible models, Remote Context windows are off, search is unmanaged, image generation is disabled, and Auto Mode is unavailable. These defaults do not verify backend support.
|
|
33
|
-
|
|
34
|
-
The only Toolkit config file is:
|
|
30
|
+
Add `--local` for a project-local installation. Toolkit configuration is global in either case:
|
|
35
31
|
|
|
36
32
|
`~/.pi/agent/extensions/pi-openai-toolkit/config.json`
|
|
37
33
|
|
|
38
|
-
|
|
34
|
+
Create the file and parent directory if needed. Merge the Toolkit examples below into an existing **schema v2** config; do not replace unrelated settings. For an unversioned legacy config, run `/toolkit-config migration-preview` first. See the [migration guide](docs/configuration.md#legacy-compatibility-and-migration).
|
|
35
|
+
|
|
36
|
+
By default, eligible models use Remote Compaction v2. Remote Context windows, image generation, and Auto Mode are off; search is unmanaged. Backend support is still required.
|
|
39
37
|
|
|
40
38
|
## Quick start: enable Remote Context
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
For search, images, or tool-call review only, skip to [Common tasks](#common-tasks).
|
|
43
41
|
|
|
44
42
|
### Use Pi's built-in Codex provider
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
Sign in to Pi's `openai-codex` provider, then add this Toolkit config:
|
|
47
45
|
|
|
48
46
|
```json
|
|
49
47
|
{
|
|
@@ -54,19 +52,24 @@ You must already be signed in to Pi's built-in `openai-codex` provider. Create o
|
|
|
54
52
|
}
|
|
55
53
|
```
|
|
56
54
|
|
|
57
|
-
Start Pi with a model from your
|
|
55
|
+
Start Pi with a model ID from your Codex catalog:
|
|
58
56
|
|
|
59
57
|
```bash
|
|
60
58
|
pi --model openai-codex/<model-id>
|
|
61
59
|
```
|
|
62
60
|
|
|
63
|
-
|
|
61
|
+
Look for `new_context`, `get_context_remaining`, `history`, and `notes`. Their presence confirms activation, not backend compatibility. Earlier windows stay accessible through `history` without being loaded into every request.
|
|
64
62
|
|
|
65
63
|
### Use a compatible gateway
|
|
66
64
|
|
|
67
|
-
|
|
65
|
+
The gateway must support `openai-responses` and the Codex Remote Context protocol. Ordinary chat support is not enough.
|
|
66
|
+
|
|
67
|
+
If your model is already registered in `~/.pi/agent/models.json`, skip the registration example.
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
<details>
|
|
70
|
+
<summary>Register a gateway model in Pi</summary>
|
|
71
|
+
|
|
72
|
+
Merge this into Pi's models file. Replace the provider name, URL, key variable, model ID, and limits with your setup; the numbers are examples.
|
|
70
73
|
|
|
71
74
|
```json
|
|
72
75
|
{
|
|
@@ -88,19 +91,23 @@ If `~/.pi/agent/models.json` already registers a compatible model, skip registra
|
|
|
88
91
|
}
|
|
89
92
|
```
|
|
90
93
|
|
|
91
|
-
Set the
|
|
94
|
+
Set the key in PowerShell:
|
|
92
95
|
|
|
93
96
|
```powershell
|
|
94
97
|
$env:MY_GATEWAY_KEY = "replace-with-your-gateway-key"
|
|
95
98
|
```
|
|
96
99
|
|
|
97
|
-
|
|
100
|
+
Or in a POSIX shell:
|
|
98
101
|
|
|
99
102
|
```bash
|
|
100
103
|
export MY_GATEWAY_KEY="replace-with-your-gateway-key"
|
|
101
104
|
```
|
|
102
105
|
|
|
103
|
-
|
|
106
|
+
Start Pi from the same terminal.
|
|
107
|
+
|
|
108
|
+
</details>
|
|
109
|
+
|
|
110
|
+
Add the exact registered `provider/model-id` to Toolkit's config:
|
|
104
111
|
|
|
105
112
|
```json
|
|
106
113
|
{
|
|
@@ -118,45 +125,23 @@ Use the same terminal to start Pi. In the Toolkit config, make the key exactly m
|
|
|
118
125
|
pi --model my-gateway/gpt-5.6-luna
|
|
119
126
|
```
|
|
120
127
|
|
|
121
|
-
Check for
|
|
122
|
-
|
|
123
|
-
Earlier windows remain retrievable through `history`, but are not all automatically inserted into the current context.
|
|
128
|
+
Check for the same context tools listed above. If they are missing, use `/toolkit-config` and check the notification, model key, API, credentials, and URL.
|
|
124
129
|
|
|
125
130
|
## Common tasks
|
|
126
131
|
|
|
127
|
-
###
|
|
132
|
+
### Save work and switch windows with `/compact`
|
|
128
133
|
|
|
129
|
-
In an active Remote Context session, run `/compact`
|
|
134
|
+
In an active Remote Context session, run `/compact` to save work to notes and enter a new window without a conversation summary. You can append instructions to the command.
|
|
130
135
|
|
|
131
|
-
To
|
|
136
|
+
To use a separate checkpoint model, set `context.remoteCompaction.model` under `defaults` or an exact model override. It must support Remote Context on the same backend/account; leaving it unset uses the current model. Toolkit restores your original model and thinking level before continuing. See [context settings and requirements](docs/configuration.md#context).
|
|
132
137
|
|
|
133
|
-
|
|
134
|
-
{
|
|
135
|
-
"schemaVersion": 2,
|
|
136
|
-
"defaults": {
|
|
137
|
-
"context": {
|
|
138
|
-
"mode": "remote-windows",
|
|
139
|
-
"remoteCompaction": { "model": "my-gateway/gpt-5.6-luna" }
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
The checkpoint model also needs Remote Context support, the same backend/account, enough context capacity, and the permitted context tools. A gateway model needs its own exact transport opt-in. Missing or `null` uses the current model. An engaged Auto Mode gate must remain eligible on the target; otherwise the handoff is refused. Notes or model failures restore the original selection when still owned by Toolkit; your later explicit model selection wins. Reload only recovers selection, without automatically restarting inference.
|
|
146
|
-
|
|
147
|
-
This does not generate a conversation summary. Pi's native compact operation is intentionally cancelled to launch the separate handoff, so an SDK/RPC caller can see a cancellation even while checkpoint duty starts. Pi can also reject an empty or already-compacted session before Toolkit's hook runs. Automatic threshold/overflow handling and internal trim maintenance keep their existing behavior. See the [handoff contract](docs/internals.md#managed-manual-compact).
|
|
148
|
-
|
|
149
|
-
### Continue a session with server-side compaction
|
|
150
|
-
|
|
151
|
-
Use `context.mode: "remote-compaction"` for the Responses compaction path (the default), or `"pi"` to relinquish Toolkit context management. Set `context.remoteCompaction.model` only when a separate model should produce the checkpoint. These fields belong under `defaults` or an exact `models` override.
|
|
152
|
-
|
|
153
|
-
`context.remoteCompaction.inputSource` defaults to `"legacy"`: first compaction uses Pi's current session context, with event preparation as a last resort; recursion uses the raw branch tail. This preserves existing behavior but can diverge from provider-visible context when other extensions rewrite messages.
|
|
138
|
+
### Use server-side compaction
|
|
154
139
|
|
|
155
|
-
|
|
140
|
+
Set `context.mode` to `"remote-compaction"` (the default) for encrypted Responses checkpoints, or `"pi"` to leave context management to Pi. Advanced input and fallback options are in the [configuration reference](docs/configuration.md#context).
|
|
156
141
|
|
|
157
142
|
### Choose a Web Search route
|
|
158
143
|
|
|
159
|
-
|
|
144
|
+
Set a default route and override it for individual models:
|
|
160
145
|
|
|
161
146
|
```json
|
|
162
147
|
{
|
|
@@ -172,16 +157,18 @@ Configure a global default and exact model overrides:
|
|
|
172
157
|
}
|
|
173
158
|
```
|
|
174
159
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
160
|
+
| Route | Behavior |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| `unmanaged` | Leave search to Pi and other extensions. |
|
|
163
|
+
| `local` | Keep the existing local `pi-web-access` tools. |
|
|
164
|
+
| `hosted` | Use Responses `web_search` with source annotations. |
|
|
165
|
+
| `standalone-alpha` | Use experimental `web_run` on a CPA/Codex gateway that supports `/alpha/search`. |
|
|
179
166
|
|
|
180
|
-
|
|
167
|
+
Toolkit does not fall back between routes. See [search configuration](docs/configuration.md#web-search) for backend requirements.
|
|
181
168
|
|
|
182
169
|
### Generate an image
|
|
183
170
|
|
|
184
|
-
|
|
171
|
+
Requires a Responses session and may incur provider charges. Enable it with an image model your provider supports:
|
|
185
172
|
|
|
186
173
|
```json
|
|
187
174
|
{
|
|
@@ -190,19 +177,17 @@ Image generation requires a Responses session and may incur provider charges. En
|
|
|
190
177
|
"imageGeneration": {
|
|
191
178
|
"enabled": true,
|
|
192
179
|
"defaultModel": "gpt-image-2.5",
|
|
193
|
-
"allowedModels": ["gpt-image-2.5"
|
|
180
|
+
"allowedModels": ["gpt-image-2.5"]
|
|
194
181
|
}
|
|
195
182
|
}
|
|
196
183
|
}
|
|
197
184
|
```
|
|
198
185
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
`openai_generate_image` accepts text-to-image requests and edits using explicitly supplied local references. Image policy cannot be overridden per session model.
|
|
186
|
+
Ask Pi to generate an image or edit explicitly supplied local references using `openai_generate_image`. The default model must be in `allowedModels`; image settings are global. See [image configuration](docs/configuration.md#images).
|
|
202
187
|
|
|
203
188
|
### Review tool calls automatically
|
|
204
189
|
|
|
205
|
-
|
|
190
|
+
Enable Auto Mode for a model and choose its reviewer:
|
|
206
191
|
|
|
207
192
|
```json
|
|
208
193
|
{
|
|
@@ -218,38 +203,23 @@ Make Auto Mode available to an exact model and choose its reviewer:
|
|
|
218
203
|
}
|
|
219
204
|
```
|
|
220
205
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
The TUI shows activation, review activity, and a footer status. Compatible Pi tool renderers also show per-call allowed, denied, blocked, or unreviewed states. If that renderer seam is unavailable, Toolkit warns once and retains the footer display. Reviewer, classifier, and circuit-breaker controls are in the [configuration reference](docs/configuration.md).
|
|
206
|
+
Turn it on with `/auto on` or `--auto`, and off with `/auto off`. By default it reviews `bash`, `write`, `edit`, and configured extra tools; `gate: "all"` reviews every call. A reviewer timeout never approves a call. See [Auto Mode settings](docs/configuration.md#auto-mode).
|
|
224
207
|
|
|
225
208
|
## Common configuration
|
|
226
209
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
|
230
|
-
| --- | --- |
|
|
231
|
-
| `
|
|
232
|
-
|
|
|
233
|
-
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
| `defaults.webSearch.route` | `"unmanaged"` | Toolkit search ownership policy. |
|
|
237
|
-
| `defaults.imageGeneration.enabled` | `false` | Global image-generation switch. |
|
|
238
|
-
| `defaults.imageGeneration.defaultModel` | `"gpt-image-2.5"` | Explicit default output model. |
|
|
239
|
-
| `defaults.imageGeneration.allowedModels` | `["gpt-image-2.5"]` | Allowed output models, global only. |
|
|
240
|
-
| `defaults.autoMode.available` | `false` | Permission to engage tool review. |
|
|
241
|
-
| `defaults.autoMode.gate` | `"side-effect"` | Selected side-effect tools or `"all"`. |
|
|
242
|
-
| `defaults.autoMode.timeoutMs` | `30000` | Reviewer timeout in milliseconds. |
|
|
243
|
-
| `diagnostics.level` | `"info"` | `"debug"` enables debug artifacts. |
|
|
244
|
-
| `diagnostics.captureRequests` / `captureResponses` | `false` | Independent request/compact-response capture opt-ins. |
|
|
245
|
-
|
|
246
|
-
Use `/toolkit-config` for effective values and origins, `/toolkit-config validate` for document issues, and `/toolkit-config migration-preview` for a read-only legacy candidate. These commands require a UI and perform no network/auth lookup, tool activation, or file writes. Configuration selection does not verify backend support.
|
|
247
|
-
|
|
248
|
-
Unknown v2 policy keys and malformed values produce visible scoped errors, independent of debug mode. Each public callback or tool execution uses one immutable snapshot across its awaited helpers; later independent operations reread the file. A managed `/compact` handoff retains its initiating context-policy snapshot until selection is restored. Separate Pi events are not a general atomic transaction. See the [complete configuration reference](docs/configuration.md) and [editor schema](config.schema.json).
|
|
210
|
+
Use `defaults` for shared settings and `models["provider/model-id"]` for exact model overrides. Toolkit reads only the global config file.
|
|
211
|
+
|
|
212
|
+
| Command | Purpose |
|
|
213
|
+
| --- | --- |
|
|
214
|
+
| `/toolkit-config` | Show effective settings and their origins. |
|
|
215
|
+
| `/toolkit-config validate` | Check for configuration errors. |
|
|
216
|
+
| `/toolkit-config migration-preview` | Preview a legacy-to-v2 migration without writing files. |
|
|
217
|
+
|
|
218
|
+
These commands require Pi's interactive UI. See the [full configuration reference](docs/configuration.md), [editor schema](config.schema.json), and [implementation details](docs/internals.md).
|
|
249
219
|
|
|
250
220
|
## Development
|
|
251
221
|
|
|
252
|
-
|
|
222
|
+
After installing dependencies in the repository root:
|
|
253
223
|
|
|
254
224
|
```bash
|
|
255
225
|
npm run typecheck
|
package/README.zh.md
CHANGED
|
@@ -11,39 +11,37 @@
|
|
|
11
11
|
|
|
12
12
|
| 功能 | 用途 |
|
|
13
13
|
| --- | --- |
|
|
14
|
-
| Codex 远程上下文 |
|
|
15
|
-
| 远程压缩 v2 |
|
|
16
|
-
|
|
|
17
|
-
| 图像生成 |
|
|
18
|
-
| 工具调用审查 |
|
|
14
|
+
| Codex 远程上下文 | 通过适配的 Codex 协议切换窗口,用 `history` 找回较早的工作内容。 |
|
|
15
|
+
| 远程压缩 v2 | 使用加密的服务端检查点继续 Responses 会话。 |
|
|
16
|
+
| 联网搜索 | 按模型选择本地 `pi-web-access`、Responses `web_search` 或实验性的 CPA `web_run`。 |
|
|
17
|
+
| 图像生成 | 使用 Responses 托管生图工具生成或编辑图片。 |
|
|
18
|
+
| 工具调用审查 | 通过 Toolkit 审批门禁,让审查模型判断指定调用是否可以执行。 |
|
|
19
19
|
|
|
20
|
-
本包沿用 Pi
|
|
20
|
+
本包沿用 Pi 已有的模型、认证和会话,不新增提供商或模型。
|
|
21
21
|
|
|
22
22
|
## 安装
|
|
23
23
|
|
|
24
|
-
需要 Pi 0.
|
|
24
|
+
需要 Pi 0.87.0+ 和 Node.js 22.19.0+。
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
pi install npm:pi-openai-toolkit
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
没有配置文件时,符合条件的模型启用远程压缩 v2,远程上下文窗口关闭,搜索不由 Toolkit 管理,图像生成关闭,自动模式不可用。这些默认值不表示后端能力已经验证。
|
|
33
|
-
|
|
34
|
-
Toolkit 唯一的配置文件是:
|
|
30
|
+
加上 `--local` 可安装到当前项目。无论安装位置如何,Toolkit 都使用全局配置:
|
|
35
31
|
|
|
36
32
|
`~/.pi/agent/extensions/pi-openai-toolkit/config.json`
|
|
37
33
|
|
|
38
|
-
|
|
34
|
+
文件不存在时,创建文件及所需目录。将下方 Toolkit 示例合并到已有的**配置格式 v2** 文件,不要覆盖其他设置。无版本号的旧配置请先运行 `/toolkit-config migration-preview`,详见[迁移指南](docs/configuration.md#legacy-compatibility-and-migration)。
|
|
35
|
+
|
|
36
|
+
默认情况下,符合条件的模型启用远程压缩 v2;远程上下文窗口、生图和自动模式关闭,搜索不由 Toolkit 管理。使用相关功能仍需后端支持。
|
|
39
37
|
|
|
40
38
|
## 快速开始:启用远程上下文
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
如果只需要搜索、生图或工具调用审查,可跳到[常见用法](#常见用法)。
|
|
43
41
|
|
|
44
42
|
### 使用 Pi 内置的 Codex 提供商
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
先登录 Pi 的 `openai-codex` 提供商,再添加以下 Toolkit 配置:
|
|
47
45
|
|
|
48
46
|
```json
|
|
49
47
|
{
|
|
@@ -54,19 +52,24 @@ Toolkit 唯一的配置文件是:
|
|
|
54
52
|
}
|
|
55
53
|
```
|
|
56
54
|
|
|
57
|
-
|
|
55
|
+
使用 Codex 模型目录中的实际模型 ID 启动 Pi:
|
|
58
56
|
|
|
59
57
|
```bash
|
|
60
58
|
pi --model openai-codex/<model-id>
|
|
61
59
|
```
|
|
62
60
|
|
|
63
|
-
|
|
61
|
+
检查是否出现 `new_context`、`get_context_remaining`、`history` 和 `notes`。工具出现表示已激活,不代表后端兼容性已经验证。较早窗口可通过 `history` 检索,不会加载到每次请求中。
|
|
64
62
|
|
|
65
63
|
### 使用兼容网关
|
|
66
64
|
|
|
67
|
-
|
|
65
|
+
网关必须支持 `openai-responses` 和 Codex 远程上下文协议,仅支持普通对话还不够。
|
|
66
|
+
|
|
67
|
+
如果模型已在 `~/.pi/agent/models.json` 注册,可跳过注册示例。
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
<details>
|
|
70
|
+
<summary>在 Pi 中注册网关模型</summary>
|
|
71
|
+
|
|
72
|
+
将以下内容合并到 Pi 的模型文件。按实际情况替换提供商名称、URL、密钥变量、模型 ID 和限制值;其中的数字仅为示例。
|
|
70
73
|
|
|
71
74
|
```json
|
|
72
75
|
{
|
|
@@ -88,19 +91,23 @@ pi --model openai-codex/<model-id>
|
|
|
88
91
|
}
|
|
89
92
|
```
|
|
90
93
|
|
|
91
|
-
|
|
94
|
+
PowerShell 中设置密钥:
|
|
92
95
|
|
|
93
96
|
```powershell
|
|
94
97
|
$env:MY_GATEWAY_KEY = "replace-with-your-gateway-key"
|
|
95
98
|
```
|
|
96
99
|
|
|
97
|
-
POSIX shell
|
|
100
|
+
或使用 POSIX shell:
|
|
98
101
|
|
|
99
102
|
```bash
|
|
100
103
|
export MY_GATEWAY_KEY="replace-with-your-gateway-key"
|
|
101
104
|
```
|
|
102
105
|
|
|
103
|
-
|
|
106
|
+
在同一个终端中启动 Pi。
|
|
107
|
+
|
|
108
|
+
</details>
|
|
109
|
+
|
|
110
|
+
在 Toolkit 配置中填写已注册的精确 `提供商/模型 ID`:
|
|
104
111
|
|
|
105
112
|
```json
|
|
106
113
|
{
|
|
@@ -118,45 +125,23 @@ export MY_GATEWAY_KEY="replace-with-your-gateway-key"
|
|
|
118
125
|
pi --model my-gateway/gpt-5.6-luna
|
|
119
126
|
```
|
|
120
127
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
较早窗口仍可通过 `history` 检索,但不会全部自动加入当前上下文。
|
|
128
|
+
检查是否出现上面列出的上下文工具。如果没有,使用 `/toolkit-config`,并检查通知、模型键、API、认证和 URL。
|
|
124
129
|
|
|
125
130
|
## 常见用法
|
|
126
131
|
|
|
127
|
-
### 用 `/compact`
|
|
132
|
+
### 用 `/compact` 保存工作并切换窗口
|
|
128
133
|
|
|
129
|
-
|
|
134
|
+
远程上下文已激活时,运行 `/compact` 可将工作状态保存到 notes,再进入新窗口,不生成会话摘要。命令后可以附加要求。
|
|
130
135
|
|
|
131
|
-
|
|
136
|
+
如需独立的检查点模型,在 `defaults` 或精确模型覆盖下设置 `context.remoteCompaction.model`。目标模型须在同一后端及账号下支持远程上下文;不设置则使用当前模型。Toolkit 会在继续工作前恢复原模型和思考等级。详见[上下文配置与要求](docs/configuration.md#context)。
|
|
132
137
|
|
|
133
|
-
|
|
134
|
-
{
|
|
135
|
-
"schemaVersion": 2,
|
|
136
|
-
"defaults": {
|
|
137
|
-
"context": {
|
|
138
|
-
"mode": "remote-windows",
|
|
139
|
-
"remoteCompaction": { "model": "my-gateway/gpt-5.6-luna" }
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
检查点模型也必须支持 Remote Context,使用相同后端及账号,有足够的上下文容量,并允许使用上下文工具。网关模型还需要单独配置精确的传输协议选项。未设置或为 `null` 时使用当前模型。若 Auto Mode 已启用,目标模型也必须符合其准入条件,否则交接会被拒绝。notes 或模型操作失败时,只要临时选择仍由 Toolkit 管理,就会恢复原模型;用户后来明确选择的模型优先。重载只恢复模型选择,不会自动重新发起推理。
|
|
146
|
-
|
|
147
|
-
该操作不生成会话摘要。Toolkit 会有意取消 Pi 原生压缩操作,再单独启动检查点交接,因此 SDK/RPC 调用方可能收到取消结果,同时看到检查点任务开始。空会话或刚压缩过的会话也可能在进入 Toolkit 钩子前就被 Pi 拒绝。自动阈值、溢出处理及内部旧窗口清理保持原有行为。详见[交接约定](docs/internals.md#managed-manual-compact)。
|
|
148
|
-
|
|
149
|
-
### 使用服务端压缩继续会话
|
|
150
|
-
|
|
151
|
-
使用 `context.mode: "remote-compaction"` 选择 Responses 压缩路径(默认值),或设为 `"pi"` 交回 Toolkit 的上下文管理权。只有需要独立模型生成检查点时才设置 `context.remoteCompaction.model`。这些字段放在 `defaults` 或精确的 `models` 覆盖下。
|
|
152
|
-
|
|
153
|
-
`context.remoteCompaction.inputSource` 默认为 `"legacy"`:首次压缩使用 Pi 当前会话上下文,最后才回退到事件提供的数据;递归压缩使用原始分支尾部。这保留了既有行为,但当其他扩展改写消息时,可能与提供商实际看到的上下文不同。
|
|
138
|
+
### 使用服务端压缩
|
|
154
139
|
|
|
155
|
-
|
|
140
|
+
将 `context.mode` 设为 `"remote-compaction"`(默认值),使用加密的 Responses 检查点;设为 `"pi"` 则由 Pi 管理上下文。高级输入和回退选项见[配置参考](docs/configuration.md#context)。
|
|
156
141
|
|
|
157
142
|
### 选择联网搜索路由
|
|
158
143
|
|
|
159
|
-
|
|
144
|
+
设置默认路由,并按模型覆盖:
|
|
160
145
|
|
|
161
146
|
```json
|
|
162
147
|
{
|
|
@@ -172,16 +157,18 @@ Remote Context 已激活时,运行 `/compact`,后面可附加检查点要求
|
|
|
172
157
|
}
|
|
173
158
|
```
|
|
174
159
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
160
|
+
| 路由 | 行为 |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| `unmanaged` | 由 Pi 和其他扩展管理搜索。 |
|
|
163
|
+
| `local` | 保留现有本地 `pi-web-access` 工具。 |
|
|
164
|
+
| `hosted` | 使用带来源标注的 Responses `web_search`。 |
|
|
165
|
+
| `standalone-alpha` | 在支持 `/alpha/search` 的 CPA/Codex 网关上使用实验性的 `web_run`。 |
|
|
179
166
|
|
|
180
|
-
|
|
167
|
+
Toolkit 不会在路由之间自动回退。后端要求见[搜索配置](docs/configuration.md#web-search)。
|
|
181
168
|
|
|
182
169
|
### 生成图片
|
|
183
170
|
|
|
184
|
-
|
|
171
|
+
需要 Responses 会话,并可能产生服务商费用。选择服务商支持的生图模型后启用:
|
|
185
172
|
|
|
186
173
|
```json
|
|
187
174
|
{
|
|
@@ -190,19 +177,17 @@ Remote Context 已激活时,运行 `/compact`,后面可附加检查点要求
|
|
|
190
177
|
"imageGeneration": {
|
|
191
178
|
"enabled": true,
|
|
192
179
|
"defaultModel": "gpt-image-2.5",
|
|
193
|
-
"allowedModels": ["gpt-image-2.5"
|
|
180
|
+
"allowedModels": ["gpt-image-2.5"]
|
|
194
181
|
}
|
|
195
182
|
}
|
|
196
183
|
}
|
|
197
184
|
```
|
|
198
185
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
`openai_generate_image` 支持文生图和使用明确传入的本地参考图进行编辑。生图策略不能按当前会话模型覆盖。
|
|
186
|
+
让 Pi 通过 `openai_generate_image` 生成图片,或编辑明确提供的本地参考图。默认模型必须在 `allowedModels` 中;生图设置全局生效。详见[生图配置](docs/configuration.md#images)。
|
|
202
187
|
|
|
203
188
|
### 启用工具调用自动审查
|
|
204
189
|
|
|
205
|
-
|
|
190
|
+
为模型开放自动模式,并选择审查模型:
|
|
206
191
|
|
|
207
192
|
```json
|
|
208
193
|
{
|
|
@@ -218,34 +203,19 @@ Remote Context 已激活时,运行 `/compact`,后面可附加检查点要求
|
|
|
218
203
|
}
|
|
219
204
|
```
|
|
220
205
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
TUI 会显示开启提示、审查活动和底部状态。兼容的 Pi 工具渲染器还会显示每次调用的允许、拒绝、阻止或未审查状态。如果渲染接口不可用,Toolkit 告警一次并保留底部显示。审查器、分类器和熔断器的高级配置见[配置参考](docs/configuration.md)。
|
|
206
|
+
使用 `/auto on` 或 `--auto` 开启,`/auto off` 关闭。默认审查 `bash`、`write`、`edit` 和额外配置的工具;`gate: "all"` 审查所有调用。审查超时不会自动放行。详见[自动模式配置](docs/configuration.md#auto-mode)。
|
|
224
207
|
|
|
225
208
|
## 常用配置
|
|
226
209
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
|
230
|
-
| --- | --- |
|
|
231
|
-
|
|
|
232
|
-
|
|
|
233
|
-
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
| `defaults.webSearch.route` | `"unmanaged"` | Toolkit 搜索管理策略。 |
|
|
237
|
-
| `defaults.imageGeneration.enabled` | `false` | 全局生图开关。 |
|
|
238
|
-
| `defaults.imageGeneration.defaultModel` | `"gpt-image-2.5"` | 明确指定默认输出模型。 |
|
|
239
|
-
| `defaults.imageGeneration.allowedModels` | `["gpt-image-2.5"]` | 允许的输出模型,仅支持全局设置。 |
|
|
240
|
-
| `defaults.autoMode.available` | `false` | 是否允许开启工具审查。 |
|
|
241
|
-
| `defaults.autoMode.gate` | `"side-effect"` | 指定副作用工具,或 `"all"`。 |
|
|
242
|
-
| `defaults.autoMode.timeoutMs` | `30000` | 审查超时,单位为毫秒。 |
|
|
243
|
-
| `diagnostics.level` | `"info"` | `"debug"` 开启调试产物。 |
|
|
244
|
-
| `diagnostics.captureRequests` / `captureResponses` | `false` | 分别开启请求或压缩响应捕获。 |
|
|
245
|
-
|
|
246
|
-
使用 `/toolkit-config` 查看有效值及来源,`/toolkit-config validate` 查看文档问题,`/toolkit-config migration-preview` 预览只读的旧格式迁移候选。这些命令需要 UI,不查询网络或认证,不激活工具,也不写文件。选中配置不表示后端支持已验证。
|
|
247
|
-
|
|
248
|
-
未知 v2 策略键和畸形值会产生可见的分范围错误,不依赖调试模式。每个公开回调或工具执行及其等待的辅助操作使用同一份不可变快照,后续独立操作重新读取文件;托管 `/compact` 交接会保留发起时的上下文策略快照,直到模型恢复。独立 Pi 事件之间不保证通用的原子事务。完整选项见[配置参考](docs/configuration.md)和[编辑器 schema](config.schema.json)。
|
|
210
|
+
`defaults` 设置通用值,`models["provider/model-id"]` 按精确模型覆盖。Toolkit 只读取全局配置文件。
|
|
211
|
+
|
|
212
|
+
| 命令 | 用途 |
|
|
213
|
+
| --- | --- |
|
|
214
|
+
| `/toolkit-config` | 查看生效设置及其来源。 |
|
|
215
|
+
| `/toolkit-config validate` | 检查配置错误。 |
|
|
216
|
+
| `/toolkit-config migration-preview` | 预览旧格式到 v2 的迁移,不写入文件。 |
|
|
217
|
+
|
|
218
|
+
这些命令需要 Pi 交互界面。完整说明见[配置参考](docs/configuration.md)、[编辑器 schema](config.schema.json)和[实现细节](docs/internals.md)。
|
|
249
219
|
|
|
250
220
|
## 开发
|
|
251
221
|
|
package/docs/internals.md
CHANGED
|
@@ -76,7 +76,7 @@ State invariants the lifecycle code must keep honest:
|
|
|
76
76
|
- The gate uses the live session/branch identity from `ctx.sessionManager`; cached manager identity must not hide a valid current-window pair, and old or foreign-session evidence must remain rejected.
|
|
77
77
|
- Every rollover carries the most recent successful checkpoint path in its handoff message. The optional `thread_hint` is supplemental; a hint failure must not erase the local receipt.
|
|
78
78
|
- A successful rollover closes the pre-rollover checkpoint phase. The first turn in the new window reads the receipt once and resumes the active user task; it must not immediately create another checkpoint or call `new_context` as part of that handoff. A later checkpoint is only for a later rollover, and `canRolloverFromCurrentWindow()` enforces it: a window entered by a rollover has to produce a tool result from a tool that is not one of `new_context`, `get_context_remaining`, `history`, or `notes` before it can roll over again.
|
|
79
|
-
- Pi 0.
|
|
79
|
+
- Pi 0.87 separates conversation-only `context` handlers from full-transcript `context_with_system` handlers. Toolkit uses the latter to trim windows and re-anchor the merged prompt/tool state with `preserveSystemHead()`. The output owns its leading system message; Pi does not repair it after this phase.
|
|
80
80
|
- When even that rebuild would lose declared tools, the trim is refused and the full message list is sent: a window that still carries the previous turns is usable, a window with no tools silently degrades into a chat the model can only narrate. Each refusal is reported once per window and lost-tool set through `takeProjectionDiagnostics()`, which writes a `context.projection.tool_loadout_shrink` compaction artifact and warns in the UI.
|
|
81
81
|
- The manager also remembers the head it last anchored. When a boundary compaction retires the transcript entry that declared the tools, the branch itself no longer contains a system message, and the remembered head is re-anchored instead of sending a tool-less window. The cache is request-time only, refreshed by every projection, and cleared by `reset()`.
|
|
82
82
|
- `new_context` has no force escape hatch: a persisted successful notes checkpoint in the current window is always required before rollover. The model must wait for the notes result to be persisted before retrying.
|
|
@@ -98,7 +98,7 @@ State invariants the lifecycle code must keep honest:
|
|
|
98
98
|
2. The matching `session_compact_failed` callback launches duty only after Pi is idle. Public `setModel` changes the session selection, and a visible `sendUserMessage` rebuilds the target's normal prompt/tools. Delivery is observed through `message_start`; return from the fire-and-forget send API is not proof. A delivery timeout restores an abandoned idle selection.
|
|
99
99
|
3. The existing `new_context` gate additionally requires a successful paired notes result after this operation and any later delivered user message. It records the exact target window before queueing a context-only marker, and returns a terminating result. Ordinary tools are blocked during checkpoint duty. Normal `new_context` outside this operation is unchanged.
|
|
100
100
|
4. A terminating result alone does not stop mixed batches or queued messages. At `turn_end`, Toolkit requests abort without awaiting idle; context/request guards also refuse an unsafe continuation. The standard Responses and native Codex transports must not send an already-aborted continuation. Pi can deliver queued user content during that stop; it remains persisted and ordered for the original model.
|
|
101
|
-
5. At `agent_settled`, after Pi has flushed custom messages, the controller verifies the exact durable marker and fresh checkpoint, restores original model/thinking, and sends one visible receipt-reading continuation.
|
|
101
|
+
5. At `agent_settled`, after Pi has flushed custom messages, the controller verifies the exact durable marker and fresh checkpoint, restores original model/thinking, and sends one visible receipt-reading continuation. Pi 0.87 defers that continuation until every settled handler has returned, so later observers still see an idle session. Restoration finishes before the next provider request; marker `message_end` alone is not proof of persistence.
|
|
102
102
|
|
|
103
103
|
Versioned `pi-openai-toolkit:manual-compact` custom entries record operation/session/branch/window identities, exact model keys, original thinking and phase. They contain no credentials, transcript copy or note contents. Reload/navigation reads the current branch and only recovers still-owned selection; it never retries notes, rollover or paid inference. A newer user model/thinking selection ends ownership. Failed restoration stays durable and blocks further provider requests until recovery or an explicit user selection.
|
|
104
104
|
|
|
@@ -118,9 +118,11 @@ The incremental SSE consumer validates the first terminal event and reconciles i
|
|
|
118
118
|
|
|
119
119
|
A v2 response with a missing or empty checkpoint is never stored, and the `nativeFallback` tier is skipped. Pi's own threshold drives the next attempt, which may use `remoteCompactModel` when configured. `remoteCompactModel` must resolve to the same effective base URL as the active model.
|
|
120
120
|
|
|
121
|
-
Pi 0.
|
|
121
|
+
Pi 0.87 makes `SessionManager` the source of model-visible context. An omitted `context.remoteCompaction.inputSource` keeps `"legacy"` behavior: first compaction reads `buildSessionProjection()` (falling back to Pi's preparation if unavailable), and recursion uses the opaque window plus the canonically projected branch tail. Both paths honor persisted context edits. Legacy mode still bypasses request-time extension transforms.
|
|
122
122
|
|
|
123
|
-
Setting `context.remoteCompaction.inputSource: "pi-context-hook"` opts into a narrow runtime bridge around Pi's public `ExtensionRunner.createContext()`. It adds a non-enumerable `ctx.projectContextForCompaction(messages)` method backed by `ExtensionRunner.emitContext()`.
|
|
123
|
+
Setting `context.remoteCompaction.inputSource: "pi-context-hook"` opts into a narrow runtime bridge around Pi's public `ExtensionRunner.createContext()`. It adds a non-enumerable `ctx.projectContextForCompaction(messages)` method backed by `ExtensionRunner.emitContext()`. This runs the ordered conversation-only and full-transcript phases on the canonical projection. If the bridge, session projection, or recursive summary anchor is unavailable, the extension cancels rather than sending unprojected history.
|
|
124
|
+
|
|
125
|
+
Retained-message matching uses the SDK projection and source entry IDs, including omissions and replacements made before compaction. Older retained compaction entries contribute no second summary. A compaction whose kept boundary is its own ID retains no earlier messages. Edits to post-checkpoint input are applied to the live tail; an edit targeting history already sealed in an opaque checkpoint blocks replay and recursive compaction with `checkpoint-context-edited`. The encrypted checkpoint cannot be selectively rewritten. Start a new session or navigate before that checkpoint to rebuild from editable history; the raw log remains unchanged.
|
|
124
126
|
|
|
125
127
|
New checkpoints record `inputProvenance: "pi-context-hook-v1"` or `"legacy-raw-context-v1"`, and replay/recursion reject missing or mode-mismatched markers without searching past the latest compaction. Retained `role: "custom"` messages are optional during replay because they may be changed or removed by context hooks; required user, assistant, and complete tool-call/result content remains ordered and fail-closed.
|
|
126
128
|
|
|
@@ -130,7 +132,7 @@ New checkpoints record `inputProvenance: "pi-context-hook-v1"` or `"legacy-raw-c
|
|
|
130
132
|
|
|
131
133
|
Approval state is scoped to actual user-message delivery. Full structured user content is SHA-256 hashed independently of transcript truncation, and generation-owned classifier completions cannot overwrite newer samples. The bounded transcript selects recent user instructions first and reports omissions. The blocking reviewer and its read-only evidence loop share one deadline that also races uncooperative dependencies; timeout/cancellation is an unavailable review, never approval or a safety denial.
|
|
132
134
|
|
|
133
|
-
Auto Mode owns the review decision, but Pi owns the built-in tool-block component. Pi 0.
|
|
135
|
+
Auto Mode owns the review decision, but Pi owns the built-in tool-block component. Pi 0.87 exports `ToolExecutionComponent` without a public decorator interface, so `src/auto-mode/tool-review-tui.ts` installs a narrow, idempotent compatibility patch on its `render()` method. The patch only reads the component's existing tool-call ID and appends one bounded, single-line status after the normal block output; it never changes tool execution, event ordering, or provider payloads.
|
|
134
136
|
|
|
135
137
|
The extension updates an ephemeral per-call state through the same `tool_call` lifecycle that performs the review:
|
|
136
138
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-openai-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -120,16 +120,16 @@
|
|
|
120
120
|
"typecheck": "tsc -p tsconfig.check.json"
|
|
121
121
|
},
|
|
122
122
|
"peerDependencies": {
|
|
123
|
-
"@earendil-works/pi-agent-core": ">=0.
|
|
124
|
-
"@earendil-works/pi-ai": ">=0.
|
|
125
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
126
|
-
"@earendil-works/pi-tui": ">=0.
|
|
123
|
+
"@earendil-works/pi-agent-core": ">=0.87.0",
|
|
124
|
+
"@earendil-works/pi-ai": ">=0.87.0",
|
|
125
|
+
"@earendil-works/pi-coding-agent": ">=0.87.0",
|
|
126
|
+
"@earendil-works/pi-tui": ">=0.87.0"
|
|
127
127
|
},
|
|
128
128
|
"devDependencies": {
|
|
129
|
-
"@earendil-works/pi-agent-core": "0.
|
|
130
|
-
"@earendil-works/pi-ai": "0.
|
|
131
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
132
|
-
"@earendil-works/pi-tui": "0.
|
|
129
|
+
"@earendil-works/pi-agent-core": "0.87.0",
|
|
130
|
+
"@earendil-works/pi-ai": "0.87.0",
|
|
131
|
+
"@earendil-works/pi-coding-agent": "0.87.0",
|
|
132
|
+
"@earendil-works/pi-tui": "0.87.0",
|
|
133
133
|
"@types/node": "24.12.4",
|
|
134
134
|
"bun": "1.2.23",
|
|
135
135
|
"typescript": "5.9.3"
|
|
@@ -1,31 +1,35 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
NativeCompactionDetails,
|
|
3
|
-
NativeCompactionInputProvenance,
|
|
4
|
-
} from "./types";
|
|
5
|
-
|
|
6
|
-
/** The configured Pi context-hook projector is unavailable or failed. */
|
|
7
|
-
export const COMPACTION_PROJECTION_UNAVAILABLE = "no-supported-projection-seam" as const;
|
|
8
|
-
/** The current session context could not be read before projection. */
|
|
9
|
-
export const COMPACTION_SESSION_CONTEXT_UNAVAILABLE = "session-context-unavailable" as const;
|
|
10
|
-
/** A projected recursive context did not contain the current compaction summary. */
|
|
11
|
-
export const COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE = "projected-compaction-context-unavailable" as const;
|
|
12
|
-
/** The latest opaque checkpoint was created with no or a different input marker. */
|
|
13
|
-
export const COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE = "unverified-compaction-checkpoint" as const;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
| typeof
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
import type {
|
|
2
|
+
NativeCompactionDetails,
|
|
3
|
+
NativeCompactionInputProvenance,
|
|
4
|
+
} from "./types";
|
|
5
|
+
|
|
6
|
+
/** The configured Pi context-hook projector is unavailable or failed. */
|
|
7
|
+
export const COMPACTION_PROJECTION_UNAVAILABLE = "no-supported-projection-seam" as const;
|
|
8
|
+
/** The current session context could not be read before projection. */
|
|
9
|
+
export const COMPACTION_SESSION_CONTEXT_UNAVAILABLE = "session-context-unavailable" as const;
|
|
10
|
+
/** A projected recursive context did not contain the current compaction summary. */
|
|
11
|
+
export const COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE = "projected-compaction-context-unavailable" as const;
|
|
12
|
+
/** The latest opaque checkpoint was created with no or a different input marker. */
|
|
13
|
+
export const COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE = "unverified-compaction-checkpoint" as const;
|
|
14
|
+
|
|
15
|
+
/** Persisted edits changed input already sealed inside an opaque checkpoint. */
|
|
16
|
+
export const COMPACTION_CHECKPOINT_CONTEXT_EDITED = "checkpoint-context-edited" as const;
|
|
17
|
+
|
|
18
|
+
export type UnprojectedCompactionReason =
|
|
19
|
+
| typeof COMPACTION_CHECKPOINT_CONTEXT_EDITED
|
|
20
|
+
| typeof COMPACTION_PROJECTION_UNAVAILABLE
|
|
21
|
+
| typeof COMPACTION_SESSION_CONTEXT_UNAVAILABLE
|
|
22
|
+
| typeof COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE
|
|
23
|
+
| typeof COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A Remote V2 checkpoint is safe for the current replay chain only when its
|
|
27
|
+
* marker matches the configured input source. Structural persistence validation
|
|
28
|
+
* intentionally remains more permissive for old session files.
|
|
29
|
+
*/
|
|
30
|
+
export function hasVerifiedCompactionInputProvenance(
|
|
31
|
+
details: Pick<NativeCompactionDetails, "inputProvenance"> | undefined,
|
|
32
|
+
expected: NativeCompactionInputProvenance,
|
|
33
|
+
): boolean {
|
|
34
|
+
return details?.inputProvenance === expected;
|
|
35
|
+
}
|
package/src/extension-runtime.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type {
|
|
3
3
|
BeforeProviderRequestEvent,
|
|
4
|
-
|
|
4
|
+
ContextWithSystemEvent,
|
|
5
5
|
CompactionResult,
|
|
6
6
|
ExtensionAPI,
|
|
7
7
|
ExtensionContext,
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import { writeDebugArtifact, writeReplayFailureArtifact } from "./debug";
|
|
34
34
|
import {
|
|
35
35
|
COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
|
|
36
|
+
COMPACTION_CHECKPOINT_CONTEXT_EDITED,
|
|
36
37
|
COMPACTION_PROJECTION_UNAVAILABLE,
|
|
37
38
|
COMPACTION_SESSION_CONTEXT_UNAVAILABLE,
|
|
38
39
|
COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
import { runNativeFallbackCompaction } from "./native-fallback";
|
|
51
52
|
import {
|
|
52
53
|
rewriteResponsesPayloadWithNativeReplay,
|
|
54
|
+
hasInvalidatedCompactionContext,
|
|
53
55
|
removeNativeCompactionRetainedMessages,
|
|
54
56
|
serializeLiveTailToResponsesInput,
|
|
55
57
|
} from "./payload-rewrite";
|
|
@@ -260,22 +262,11 @@ function findLatestProjectedCompactionSummaryIndex(
|
|
|
260
262
|
return -1;
|
|
261
263
|
}
|
|
262
264
|
|
|
263
|
-
type SessionManagerWithOptionalContext = ExtensionContext["sessionManager"] & {
|
|
264
|
-
buildSessionContext?: () => { messages?: readonly AgentMessage[] };
|
|
265
|
-
};
|
|
266
|
-
|
|
267
265
|
function readSessionContextMessages(ctx: ExtensionContext): AgentMessage[] | undefined {
|
|
268
|
-
const sessionManager = ctx.sessionManager as SessionManagerWithOptionalContext;
|
|
269
|
-
if (typeof sessionManager.buildSessionContext !== "function") {
|
|
270
|
-
return undefined;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
266
|
try {
|
|
274
|
-
const
|
|
275
|
-
if (!
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
return structuredClone([...sessionContext.messages]);
|
|
267
|
+
const projection = ctx.sessionManager.buildSessionProjection();
|
|
268
|
+
if (!projection || !Array.isArray(projection.messages)) return undefined;
|
|
269
|
+
return structuredClone(projection.messages);
|
|
279
270
|
} catch {
|
|
280
271
|
return undefined;
|
|
281
272
|
}
|
|
@@ -339,6 +330,9 @@ async function runResponsesNativeCompact(
|
|
|
339
330
|
let requestSource: "session-context" | "non-native-session-context" | "latest-native-replay";
|
|
340
331
|
let request: NativeCompactionRequestBody;
|
|
341
332
|
if (latestNativeCompaction.ok) {
|
|
333
|
+
if (hasInvalidatedCompactionContext(branchEntries, latestNativeCompaction.entry.id)) {
|
|
334
|
+
return { outcome: "unprojected-input", reason: COMPACTION_CHECKPOINT_CONTEXT_EDITED };
|
|
335
|
+
}
|
|
342
336
|
const details = latestNativeCompaction.entry.details;
|
|
343
337
|
if (!details) {
|
|
344
338
|
return { outcome: "failed" };
|
|
@@ -658,7 +652,9 @@ async function handleSessionBeforeCompact(
|
|
|
658
652
|
return { cancel: true };
|
|
659
653
|
}
|
|
660
654
|
if (responsesOutcome.outcome === "unprojected-input") {
|
|
661
|
-
const message = responsesOutcome.reason ===
|
|
655
|
+
const message = responsesOutcome.reason === COMPACTION_CHECKPOINT_CONTEXT_EDITED
|
|
656
|
+
? "A context edit changed history sealed in the opaque checkpoint. Start a new session or navigate before that checkpoint; its encrypted content cannot be selectively rewritten."
|
|
657
|
+
: responsesOutcome.reason === COMPACTION_PROJECTION_UNAVAILABLE
|
|
662
658
|
? "Pi's ordered context-hook projection is unavailable; Remote V2 was not sent raw session history."
|
|
663
659
|
: responsesOutcome.reason === COMPACTION_SESSION_CONTEXT_UNAVAILABLE
|
|
664
660
|
? "Pi did not provide the current session context required by its context-hook projection."
|
|
@@ -680,7 +676,7 @@ async function handleSessionBeforeCompact(
|
|
|
680
676
|
);
|
|
681
677
|
notifyWarning(
|
|
682
678
|
ctx,
|
|
683
|
-
`Remote V2 compaction cancelled (${responsesOutcome.reason}); ${message}
|
|
679
|
+
`Remote V2 compaction cancelled (${responsesOutcome.reason}); ${message} The existing checkpoint and session history were left unchanged.`,
|
|
684
680
|
);
|
|
685
681
|
return { cancel: true };
|
|
686
682
|
}
|
|
@@ -850,7 +846,7 @@ async function handleWindowBulk(
|
|
|
850
846
|
}
|
|
851
847
|
|
|
852
848
|
async function handleContextInternal(
|
|
853
|
-
event:
|
|
849
|
+
event: ContextWithSystemEvent,
|
|
854
850
|
ctx: ExtensionContext,
|
|
855
851
|
pi: ExtensionAPI,
|
|
856
852
|
loadConfig: typeof loadToolkitConfig,
|
|
@@ -953,7 +949,7 @@ async function handleContextInternal(
|
|
|
953
949
|
* caught exception for a successful projection.
|
|
954
950
|
*/
|
|
955
951
|
async function handleContext(
|
|
956
|
-
event:
|
|
952
|
+
event: ContextWithSystemEvent,
|
|
957
953
|
ctx: ExtensionContext,
|
|
958
954
|
pi: ExtensionAPI,
|
|
959
955
|
loadConfig: typeof loadToolkitConfig,
|
|
@@ -1308,7 +1304,7 @@ export default function registerCompactionExtension(
|
|
|
1308
1304
|
}
|
|
1309
1305
|
});
|
|
1310
1306
|
|
|
1311
|
-
pi.on("
|
|
1307
|
+
pi.on("context_with_system", (event, ctx) => {
|
|
1312
1308
|
manualCompact.guardRequest(ctx);
|
|
1313
1309
|
return handleContext(event, ctx, pi, dependencies.loadConfig, contextWindows, remoteContextActive);
|
|
1314
1310
|
});
|
package/src/payload-rewrite.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { buildSessionProjection, sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
3
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
4
|
-
import type {
|
|
5
|
-
BranchSummaryEntry,
|
|
6
|
-
CustomMessageEntry,
|
|
7
|
-
SessionEntry,
|
|
8
|
-
SessionMessageEntry,
|
|
9
|
-
} from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
10
5
|
import type { ResponsesCompatibleRequestPayload } from "./runtime";
|
|
11
6
|
import { hasVerifiedCompactionInputProvenance } from "./compaction-projection";
|
|
12
7
|
import { rewritePayloadWithDeferredToolCarryover } from "./deferred-tool-carryover";
|
|
@@ -40,7 +35,8 @@ export type NativeReplayPayloadRewriteFailureReason =
|
|
|
40
35
|
| "unexpected-compaction-after-boundary"
|
|
41
36
|
| "compaction-summary-not-found"
|
|
42
37
|
| "retained-context-mismatch"
|
|
43
|
-
| "unverified-compaction-input"
|
|
38
|
+
| "unverified-compaction-input"
|
|
39
|
+
| "checkpoint-context-edited";
|
|
44
40
|
|
|
45
41
|
export type NativeReplayPayloadRewriteFailure = {
|
|
46
42
|
ok: false;
|
|
@@ -200,46 +196,6 @@ function areEquivalentValues(left: unknown, right: unknown): boolean {
|
|
|
200
196
|
return false;
|
|
201
197
|
}
|
|
202
198
|
|
|
203
|
-
function toBranchSummaryMessage(entry: BranchSummaryEntry): AgentMessage {
|
|
204
|
-
return {
|
|
205
|
-
role: "branchSummary",
|
|
206
|
-
summary: entry.summary,
|
|
207
|
-
fromId: entry.fromId,
|
|
208
|
-
timestamp: new Date(entry.timestamp).getTime(),
|
|
209
|
-
} as AgentMessage;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function toCustomMessage(entry: CustomMessageEntry): AgentMessage {
|
|
213
|
-
return {
|
|
214
|
-
role: "custom",
|
|
215
|
-
customType: entry.customType,
|
|
216
|
-
content: entry.content,
|
|
217
|
-
display: entry.display,
|
|
218
|
-
details: entry.details,
|
|
219
|
-
timestamp: new Date(entry.timestamp).getTime(),
|
|
220
|
-
} as AgentMessage;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function toSessionMessage(entry: SessionMessageEntry): AgentMessage {
|
|
224
|
-
return entry.message;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function toReplayAgentMessage(entry: SessionEntry): AgentMessage | undefined {
|
|
228
|
-
if (entry.type === "message") {
|
|
229
|
-
return toSessionMessage(entry);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (entry.type === "custom_message") {
|
|
233
|
-
return toCustomMessage(entry);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (entry.type === "branch_summary") {
|
|
237
|
-
return toBranchSummaryMessage(entry);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return undefined;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
199
|
function extractUserItemText(item: Record<string, unknown>): unknown {
|
|
244
200
|
const { content } = item;
|
|
245
201
|
if (typeof content === "string") {
|
|
@@ -273,23 +229,29 @@ function findCompactionSummaryIndex(input: readonly unknown[], summaryMarker: st
|
|
|
273
229
|
}
|
|
274
230
|
|
|
275
231
|
function collectReplayMessages(entries: readonly SessionEntry[]): AgentMessage[] {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
for (const entry of entries) {
|
|
279
|
-
const message = toReplayAgentMessage(entry);
|
|
280
|
-
if (message) {
|
|
281
|
-
messages.push(message);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
232
|
+
return buildSessionProjection([...entries]).messages;
|
|
233
|
+
}
|
|
284
234
|
|
|
285
|
-
|
|
235
|
+
/** An opaque checkpoint cannot be selectively edited after it was produced. */
|
|
236
|
+
export function hasInvalidatedCompactionContext(
|
|
237
|
+
entries: readonly SessionEntry[],
|
|
238
|
+
compactionEntryId: string,
|
|
239
|
+
): boolean {
|
|
240
|
+
const boundary = findCompactionBoundaryIndex(entries, compactionEntryId);
|
|
241
|
+
if (boundary === undefined) return false;
|
|
242
|
+
const coveredIds = new Set(entries.slice(0, boundary).map((entry) => entry.id));
|
|
243
|
+
return entries.slice(boundary + 1).some((entry) =>
|
|
244
|
+
entry.type === "context_edit" && coveredIds.has(entry.targetId),
|
|
245
|
+
);
|
|
286
246
|
}
|
|
287
247
|
|
|
288
|
-
function
|
|
248
|
+
function findKeptEntryIndex(
|
|
289
249
|
entries: readonly SessionEntry[],
|
|
290
250
|
entryId: string,
|
|
291
251
|
boundaryIndex: number,
|
|
292
252
|
): number | undefined {
|
|
253
|
+
// Pi 0.87 uses the compaction itself as the retained boundary for retain-none.
|
|
254
|
+
if (entries[boundaryIndex]?.id === entryId) return boundaryIndex;
|
|
293
255
|
const index = entries.findIndex((entry, candidateIndex) => candidateIndex < boundaryIndex && entry.id === entryId);
|
|
294
256
|
return index >= 0 ? index : undefined;
|
|
295
257
|
}
|
|
@@ -319,7 +281,7 @@ export function collectLiveTailMessages(entries: readonly SessionEntry[]): Agent
|
|
|
319
281
|
}
|
|
320
282
|
|
|
321
283
|
/**
|
|
322
|
-
* Remote V2 covers the complete pre-compaction context. Pi
|
|
284
|
+
* Remote V2 covers the complete pre-compaction context. Pi also retains
|
|
323
285
|
* recent messages, so remove those verified copies before provider conversion
|
|
324
286
|
* (where IDs/fields may change across models). Persisted session entries are
|
|
325
287
|
* untouched. Preserve transient additions and all post-compaction messages.
|
|
@@ -335,7 +297,10 @@ export function removeNativeCompactionRetainedMessages(args: {
|
|
|
335
297
|
if (!hasVerifiedCompactionInputProvenance(args.compactionEntry.details, args.expectedInputProvenance)) {
|
|
336
298
|
return { ok: false, reason: "unverified-compaction-input" };
|
|
337
299
|
}
|
|
338
|
-
|
|
300
|
+
if (hasInvalidatedCompactionContext(args.branchEntries, args.compactionEntry.id)) {
|
|
301
|
+
return { ok: false, reason: "checkpoint-context-edited" };
|
|
302
|
+
}
|
|
303
|
+
const firstKept = findKeptEntryIndex(args.branchEntries, args.compactionEntry.firstKeptEntryId, boundary);
|
|
339
304
|
if (firstKept === undefined) return { ok: false, reason: "first-kept-entry-not-found" };
|
|
340
305
|
const summaryMessage = sessionEntryToContextMessages(args.compactionEntry).find(
|
|
341
306
|
(message) => message.role === "compactionSummary",
|
|
@@ -344,9 +309,10 @@ export function removeNativeCompactionRetainedMessages(args: {
|
|
|
344
309
|
const summary = structuredClone(summaryMessage);
|
|
345
310
|
const summaryIndex = args.messages.findIndex((message) => areEquivalentValues(message, summary));
|
|
346
311
|
if (summaryIndex < 0) return { ok: false, reason: "compaction-summary-not-found" };
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
.
|
|
312
|
+
const retainedIds = new Set(args.branchEntries.slice(firstKept, boundary).map((entry) => entry.id));
|
|
313
|
+
const retained = buildSessionProjection([...args.branchEntries]).entries
|
|
314
|
+
.filter((entry) => retainedIds.has(entry.sourceEntry.id))
|
|
315
|
+
.flatMap((entry) => entry.messages)
|
|
350
316
|
.map((message) => structuredClone(message));
|
|
351
317
|
// Custom messages are optional context-hook entries: a hook may filter or
|
|
352
318
|
// rewrite them before provider serialization. An exact custom copy is still
|
|
@@ -418,7 +384,11 @@ function buildNativeReplaySegmentsInternal<TApi extends Api>(args: {
|
|
|
418
384
|
};
|
|
419
385
|
}
|
|
420
386
|
|
|
421
|
-
|
|
387
|
+
if (hasInvalidatedCompactionContext(args.branchEntries, args.compactionEntry.id)) {
|
|
388
|
+
return { ok: false, reason: "checkpoint-context-edited" };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const firstKeptEntryIndex = findKeptEntryIndex(
|
|
422
392
|
args.branchEntries,
|
|
423
393
|
args.compactionEntry.firstKeptEntryId,
|
|
424
394
|
boundaryIndex,
|
package/src/pi-context-hook.ts
CHANGED
|
@@ -1,116 +1,117 @@
|
|
|
1
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import { ExtensionRunner, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
|
|
4
|
-
/** Context property added by the toolkit's host patch. */
|
|
5
|
-
export const PI_CONTEXT_PROJECTOR_FIELD = "projectContextForCompaction" as const;
|
|
6
|
-
const PI_CONTEXT_PROJECTION_REPORTER_FIELD = "__piOpenAiToolkitReportContextProjectionFailure" as const;
|
|
7
|
-
const PATCH_MARKER_FIELD = "__piOpenAiToolkitContextHookPatch" as const;
|
|
8
|
-
|
|
9
|
-
type ProjectionState = {
|
|
10
|
-
failure?: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const activeProjectionByRunner = new WeakMap<ExtensionRunner, ProjectionState>();
|
|
14
|
-
|
|
15
|
-
type PatchedExtensionContext = ExtensionContext & {
|
|
16
|
-
[PI_CONTEXT_PROJECTOR_FIELD]?: PiContextHookProjector;
|
|
17
|
-
[PI_CONTEXT_PROJECTION_REPORTER_FIELD]?: (reason: string) => void;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
type MarkedCreateContext = typeof ExtensionRunner.prototype.createContext & {
|
|
21
|
-
[PATCH_MARKER_FIELD]?: true;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/** The ordered context-hook chain, applied to a cloned compaction context. */
|
|
25
|
-
export type PiContextHookProjector = (
|
|
26
|
-
messages: readonly AgentMessage[],
|
|
27
|
-
) => Promise<readonly AgentMessage[]>;
|
|
28
|
-
|
|
29
|
-
export type PiContextHookPatchResult =
|
|
30
|
-
| { ok: true; installed: boolean; reason?: "already-installed" }
|
|
31
|
-
| { ok: false; reason: "emit-context-unavailable" | "create-context-unavailable" };
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Add a narrow projection method to every ExtensionContext created by Pi.
|
|
35
|
-
*
|
|
36
|
-
* Pi 0.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { ExtensionRunner, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
/** Context property added by the toolkit's host patch. */
|
|
5
|
+
export const PI_CONTEXT_PROJECTOR_FIELD = "projectContextForCompaction" as const;
|
|
6
|
+
const PI_CONTEXT_PROJECTION_REPORTER_FIELD = "__piOpenAiToolkitReportContextProjectionFailure" as const;
|
|
7
|
+
const PATCH_MARKER_FIELD = "__piOpenAiToolkitContextHookPatch" as const;
|
|
8
|
+
|
|
9
|
+
type ProjectionState = {
|
|
10
|
+
failure?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const activeProjectionByRunner = new WeakMap<ExtensionRunner, ProjectionState>();
|
|
14
|
+
|
|
15
|
+
type PatchedExtensionContext = ExtensionContext & {
|
|
16
|
+
[PI_CONTEXT_PROJECTOR_FIELD]?: PiContextHookProjector;
|
|
17
|
+
[PI_CONTEXT_PROJECTION_REPORTER_FIELD]?: (reason: string) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type MarkedCreateContext = typeof ExtensionRunner.prototype.createContext & {
|
|
21
|
+
[PATCH_MARKER_FIELD]?: true;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** The ordered context-hook chain, applied to a cloned compaction context. */
|
|
25
|
+
export type PiContextHookProjector = (
|
|
26
|
+
messages: readonly AgentMessage[],
|
|
27
|
+
) => Promise<readonly AgentMessage[]>;
|
|
28
|
+
|
|
29
|
+
export type PiContextHookPatchResult =
|
|
30
|
+
| { ok: true; installed: boolean; reason?: "already-installed" }
|
|
31
|
+
| { ok: false; reason: "emit-context-unavailable" | "create-context-unavailable" };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Add a narrow projection method to every ExtensionContext created by Pi.
|
|
35
|
+
*
|
|
36
|
+
* Pi 0.87's ordered `ExtensionRunner.emitContext()` runs both `context` and
|
|
37
|
+
* `context_with_system` phases,
|
|
38
|
+
* but the event-level ExtensionContext does not expose the runner. The patch
|
|
39
|
+
* bridges only the method needed by Remote V2; it does not expose the runner
|
|
40
|
+
* or alter Pi's scheduler.
|
|
41
|
+
*/
|
|
42
|
+
export function installPiContextHookPatch(): PiContextHookPatchResult {
|
|
43
|
+
const prototype = ExtensionRunner.prototype;
|
|
44
|
+
if (typeof prototype.emitContext !== "function") {
|
|
45
|
+
return { ok: false, reason: "emit-context-unavailable" };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const original = prototype.createContext;
|
|
49
|
+
if (typeof original !== "function") {
|
|
50
|
+
return { ok: false, reason: "create-context-unavailable" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const markedOriginal = original as MarkedCreateContext;
|
|
54
|
+
if (markedOriginal[PATCH_MARKER_FIELD] === true) {
|
|
55
|
+
return { ok: true, installed: false, reason: "already-installed" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const patched = function patchedCreateContext(this: ExtensionRunner): ExtensionContext {
|
|
59
|
+
const context = original.call(this) as PatchedExtensionContext;
|
|
60
|
+
if (!(PI_CONTEXT_PROJECTOR_FIELD in context)) {
|
|
61
|
+
Object.defineProperty(context, PI_CONTEXT_PROJECTOR_FIELD, {
|
|
62
|
+
configurable: false,
|
|
63
|
+
enumerable: false,
|
|
64
|
+
writable: false,
|
|
65
|
+
value: async (messages: readonly AgentMessage[]) => {
|
|
66
|
+
const state: ProjectionState = {};
|
|
67
|
+
activeProjectionByRunner.set(this, state);
|
|
68
|
+
try {
|
|
69
|
+
const projected = await this.emitContext([...messages]);
|
|
70
|
+
if (state.failure) {
|
|
71
|
+
throw new Error(`context projection failed: ${state.failure}`);
|
|
72
|
+
}
|
|
73
|
+
return projected;
|
|
74
|
+
} finally {
|
|
75
|
+
if (activeProjectionByRunner.get(this) === state) {
|
|
76
|
+
activeProjectionByRunner.delete(this);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (!(PI_CONTEXT_PROJECTION_REPORTER_FIELD in context)) {
|
|
83
|
+
Object.defineProperty(context, PI_CONTEXT_PROJECTION_REPORTER_FIELD, {
|
|
84
|
+
configurable: false,
|
|
85
|
+
enumerable: false,
|
|
86
|
+
writable: false,
|
|
87
|
+
value: (reason: string) => {
|
|
88
|
+
const state = activeProjectionByRunner.get(this);
|
|
89
|
+
if (state) state.failure = reason;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return context;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
Object.defineProperty(patched, PATCH_MARKER_FIELD, {
|
|
97
|
+
configurable: false,
|
|
98
|
+
enumerable: false,
|
|
99
|
+
writable: false,
|
|
100
|
+
value: true,
|
|
101
|
+
});
|
|
102
|
+
prototype.createContext = patched;
|
|
103
|
+
return { ok: true, installed: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Return the patched projection method, if this Pi process supports it. */
|
|
107
|
+
export function getPiContextHookProjector(
|
|
108
|
+
ctx: ExtensionContext,
|
|
109
|
+
): PiContextHookProjector | undefined {
|
|
110
|
+
const candidate = (ctx as PatchedExtensionContext)[PI_CONTEXT_PROJECTOR_FIELD];
|
|
111
|
+
return typeof candidate === "function" ? candidate : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Preserve fail-closed context-hook errors across Pi's emitContext boundary. */
|
|
115
|
+
export function reportPiContextHookFailure(ctx: ExtensionContext, reason: string): void {
|
|
116
|
+
(ctx as PatchedExtensionContext)[PI_CONTEXT_PROJECTION_REPORTER_FIELD]?.(reason);
|
|
117
|
+
}
|