pi-openai-toolkit 0.14.6 → 0.14.8
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 +36 -7
- package/README.zh.md +36 -7
- package/docs/internals.md +9 -1
- package/package.json +5 -2
- package/src/compaction-projection.ts +31 -0
- package/src/config.ts +100 -1
- package/src/context-management/messages.ts +1 -1
- package/src/context-management/tools.ts +24 -39
- package/src/context-management/window-manager.ts +110 -28
- package/src/extension-runtime.ts +263 -34
- package/src/payload-rewrite.ts +55 -10
- package/src/pi-context-hook.ts +116 -0
- package/src/request-context-cache.ts +16 -1
- package/src/runtime.ts +52 -0
- package/src/types.ts +46 -0
- package/src/web-search/alpha-client.ts +875 -0
- package/src/web-search/extension.ts +469 -26
- package/src/web-search/payload.ts +209 -10
- package/src/web-search/prompt.ts +44 -5
- package/src/web-search/types.ts +107 -8
- package/tsconfig.check.json +2 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Add Codex context windows, Responses compaction, hosted tools, and reviewed tool
|
|
|
13
13
|
| --- | --- |
|
|
14
14
|
| Codex Remote Context | Start a new context window and retrieve earlier windows with `history`. |
|
|
15
15
|
| Remote Compaction v2 | Continue an eligible Responses session with an encrypted server checkpoint. |
|
|
16
|
-
|
|
|
16
|
+
| Web Search routes | Choose local `pi-web-access`, hosted Responses `web_search`, or CPA standalone `web.run` per exact model route. |
|
|
17
17
|
| Image generation | Generate images or edit explicitly supplied local reference images. |
|
|
18
18
|
| Tool-call review | Ask a reviewer model whether selected tool calls may run. |
|
|
19
19
|
|
|
@@ -31,7 +31,7 @@ pi install npm:pi-openai-toolkit
|
|
|
31
31
|
|
|
32
32
|
Use `--local` to install it in the current project.
|
|
33
33
|
|
|
34
|
-
Installing the package alone does not enable every feature. With no extension config, compaction is enabled but Remote Context is off,
|
|
34
|
+
Installing the package alone does not enable every feature. With no extension config, compaction is enabled but Remote Context is off, Web Search has no toolkit-selected route, image generation is disabled, and Auto Mode has no allowed models or reviewer.
|
|
35
35
|
|
|
36
36
|
The extension config file is:
|
|
37
37
|
|
|
@@ -130,19 +130,44 @@ Earlier windows remain available through `history`, but they are not all automat
|
|
|
130
130
|
|
|
131
131
|
Leave Remote Context off when you want the Responses compaction path instead. Remote Compaction v2 stores and replays an encrypted checkpoint for eligible Responses models. Set `compaction.remoteCompactModel` only when the compaction request should use a separate model.
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
When `compaction.remoteV2ContextSource` is omitted, Remote V2 keeps the original `"legacy"` input chain: first compaction uses Pi's current session context (or the supplied preparation as a last resort), and recursive compaction uses the raw branch tail. This preserves existing behavior, but can diverge from provider-visible context when other extensions rewrite messages.
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
Set `compaction.remoteV2ContextSource` to `"pi-context-hook"` to opt into the Pi 0.85.1 runtime bridge. The toolkit then uses the same ordered `context` hook chain as the live provider request. If Pi cannot expose that public hook path, Remote V2 cancels instead of sending an unprojected history. Checkpoint replay is accepted only when its source marker matches the current setting. Switching between the two modes requires a new Remote V2 checkpoint; disable Remote V2 or use Pi's native compaction when custom-only state must survive inside the opaque checkpoint.
|
|
136
|
+
|
|
137
|
+
### Choose a Web Search route
|
|
138
|
+
|
|
139
|
+
Web Search has three mutually exclusive routes. Configure one global default and, when needed, exact `provider/model-id` overrides:
|
|
136
140
|
|
|
137
141
|
```json
|
|
138
142
|
{
|
|
139
143
|
"webSearch": {
|
|
140
|
-
"
|
|
144
|
+
"enabled": true,
|
|
145
|
+
"defaultRoute": "hosted",
|
|
146
|
+
"routes": {
|
|
147
|
+
"uwoacrimson/gpt-6-astra": "standalone-alpha",
|
|
148
|
+
"my-gateway/gpt-5.6-luna": "local"
|
|
149
|
+
}
|
|
141
150
|
}
|
|
142
151
|
}
|
|
143
152
|
```
|
|
144
153
|
|
|
145
|
-
|
|
154
|
+
Route selection is exact and deterministic: `routes[provider/model-id]` wins over `defaultRoute`; `defaultRoute` wins over the legacy `models` list. There is no wildcard, fuzzy model matching, capability guessing, fallback, or retry between routes. Invalid route values/keys are ignored with warnings rather than guessed, and overlapping new/legacy fields produce migration warnings. `enabled: false` releases toolkit ownership and disables all three toolkit-selected paths.
|
|
155
|
+
|
|
156
|
+
- **`local`** keeps the already-installed `pi-web-access` `web_search` tool. The toolkit does not activate it when it was not active, and it does not add a hosted provider tool or `web.run`.
|
|
157
|
+
- **`hosted`** removes the local function named `web_search` and injects the native Responses `{ "type": "web_search" }` tool plus source annotations. The legacy configuration remains valid:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"webSearch": {
|
|
162
|
+
"models": ["my-gateway/gpt-5.6-luna"]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
With no new route fields, only models in this exact list and the existing Responses-family APIs (`openai-responses` or `openai-codex-responses`) use hosted search.
|
|
168
|
+
- **`standalone-alpha`** exposes one sequential `web.run` tool and sends one isolated `POST` request to the provider-relative `/alpha/search` endpoint. A base URL such as `https://gateway.example/v1` therefore receives `https://gateway.example/v1/alpha/search`, not a Responses endpoint. Supported command families are `search_query`, `image_query`, `open`, `click`, `find`, `screenshot`, `finance`, `weather`, `sports`, and `time`; `response_length` controls the requested result size.
|
|
169
|
+
|
|
170
|
+
The standalone route is an experimental CPA/Codex gateway protocol, not a stable public OpenAI Responses endpoint. The gateway/provider must expose `/alpha/search`, enable its `alpha-search` capability, and support standalone web search (Codex providers commonly expose this as `supports_standalone_web_search = true`). The request reuses Pi's current model, authentication, provider headers, session identity, and Codex/gateway affinity; it does not change the active model. The MVP sends the bounded command envelope rather than the full conversation transcript, relies on provider session/reference handling for `ref_id` follow-ups, makes at most one request per tool call, and fails closed on invalid configuration, unavailable routes, cancellation, timeout, non-2xx, malformed, or oversized responses.
|
|
146
171
|
|
|
147
172
|
### Generate an image
|
|
148
173
|
|
|
@@ -186,8 +211,12 @@ The config file is `~/.pi/agent/extensions/pi-openai-toolkit/config.json`. Unkno
|
|
|
186
211
|
| `compaction.contextManagement` | `"off"` | Enables Codex Remote Context when set to `"remote"`. |
|
|
187
212
|
| `compaction.gatewayContextModels` | `[]` | Gateway models allowed to use Remote Context. |
|
|
188
213
|
| `compaction.remoteCompactModel` | unset | Optional model used only for a v2 compaction request. |
|
|
214
|
+
| `compaction.remoteV2ContextSource` | `"legacy"` | Preserve the original raw session/branch input path. Set `"pi-context-hook"` to opt into Pi's ordered context-hook projection. Checkpoints are mode-specific. |
|
|
189
215
|
| `compaction.contextReminderThresholdPercent` | `5` | Remaining budget percentage for the once-per-window reminder. `0` disables the reminder and exhausted-window fallback. |
|
|
190
|
-
| `webSearch.
|
|
216
|
+
| `webSearch.enabled` | `true` | Total switch for the toolkit's Web Search route selection. `false` selects no toolkit route. |
|
|
217
|
+
| `webSearch.defaultRoute` | unset | Default route: `local`, `hosted`, or `standalone-alpha`. Unset preserves legacy behavior. |
|
|
218
|
+
| `webSearch.routes` | unset | Exact `provider/model-id` to route overrides. Exact entries take precedence over `defaultRoute`. |
|
|
219
|
+
| `webSearch.models` | `[]` | Legacy exact allowlist; without the new route fields, listed Responses-family models receive hosted Web Search. |
|
|
191
220
|
| `imageGeneration.enabled` | `false` | Enables `openai_generate_image`. |
|
|
192
221
|
| `imageGeneration.models` | `["gpt-image-2.5"]` | Bare image-generation model IDs; the first entry is the default. |
|
|
193
222
|
| `autoMode.models` | `[]` | Models allowed to use Auto Mode. |
|
package/README.zh.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
| --- | --- |
|
|
14
14
|
| Codex 远程上下文 | 切换到新上下文窗口,并通过 `history` 按需检索较早窗口。 |
|
|
15
15
|
| 远程压缩 v2 | 使用服务端返回的加密检查点继续符合条件的 Responses 会话。 |
|
|
16
|
-
|
|
|
16
|
+
| 联网搜索路由 | 按精确模型路由选择本地 `pi-web-access`、Responses 托管 `web_search` 或 CPA standalone `web.run`。 |
|
|
17
17
|
| 图像生成 | 生成图片,或根据明确传入的本地参考图片进行编辑。 |
|
|
18
18
|
| 工具调用审查 | 在指定范围的工具调用执行前,由审查模型判断是否允许执行。 |
|
|
19
19
|
|
|
@@ -31,7 +31,7 @@ pi install npm:pi-openai-toolkit
|
|
|
31
31
|
|
|
32
32
|
在当前项目中安装时,在命令后加上 `--local`。
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
只安装扩展不会自动启用所有功能。没有扩展配置时,压缩模块处于开启状态,但远程上下文关闭,联网搜索没有工具包选择的路由,图像生成关闭,自动模式也没有允许的模型和审查模型。
|
|
35
35
|
|
|
36
36
|
扩展配置文件位于:
|
|
37
37
|
|
|
@@ -130,19 +130,44 @@ pi --model my-gateway/gpt-5.6-luna
|
|
|
130
130
|
|
|
131
131
|
如果希望使用 Responses 压缩路径,就保持远程上下文关闭。远程压缩 v2 会为符合条件的 Responses 模型保存并回放加密检查点。只有在压缩请求需要使用其他模型时,才设置 `compaction.remoteCompactModel`。
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
不配置 `compaction.remoteV2ContextSource` 时,Remote V2 保持原来的 `"legacy"` 输入链路:首次压缩使用 Pi 当前的 session context(最后才回退到事件提供的 preparation),递归压缩使用原始 branch tail。这会保留既有行为,但如果其他扩展改写消息,可能与 provider 实际看到的上下文不同。
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
将 `compaction.remoteV2ContextSource` 设为 `"pi-context-hook"` 可主动启用 Pi 0.85.1 runtime bridge,让 Remote V2 压缩使用与实时 provider 请求相同的有序 `context` hook 链。如果 Pi 无法提供这条公开 hook 路径,Remote V2 会取消压缩,而不会发送未投影的历史。只有检查点来源标记与当前设置一致时,才会继续回放。在两种模式之间切换后,必须重新创建 Remote V2 检查点;如果自定义状态必须保留在不透明检查点中,请关闭 Remote V2,或使用 Pi 原生压缩。
|
|
136
|
+
|
|
137
|
+
### 选择联网搜索路由
|
|
138
|
+
|
|
139
|
+
联网搜索有三条互斥路由。可以设置一个全局默认路由,也可以按精确的 `provider/model-id` 覆盖:
|
|
136
140
|
|
|
137
141
|
```json
|
|
138
142
|
{
|
|
139
143
|
"webSearch": {
|
|
140
|
-
"
|
|
144
|
+
"enabled": true,
|
|
145
|
+
"defaultRoute": "hosted",
|
|
146
|
+
"routes": {
|
|
147
|
+
"uwoacrimson/gpt-6-astra": "standalone-alpha",
|
|
148
|
+
"my-gateway/gpt-5.6-luna": "local"
|
|
149
|
+
}
|
|
141
150
|
}
|
|
142
151
|
}
|
|
143
152
|
```
|
|
144
153
|
|
|
145
|
-
|
|
154
|
+
路由选择是精确且确定的:`routes[provider/model-id]` 优先于 `defaultRoute`,`defaultRoute` 优先于旧版 `models` 列表。不支持通配符、模糊模型匹配、按模型名称猜能力、路由间 fallback 或重试。无效的路由值或模型键会告警后忽略,不会猜测路由;新旧字段重叠时会给出迁移告警。`enabled: false` 会释放工具包的工具所有权,并关闭工具包选择的全部三条路径。
|
|
155
|
+
|
|
156
|
+
- **`local`** 保留已经安装的 `pi-web-access` `web_search` 工具。如果它原本没有激活,工具包不会替你激活;同时不会添加托管 provider 工具或 `web.run`。
|
|
157
|
+
- **`hosted`** 移除名为 `web_search` 的本地 function,并注入原生 Responses `{ "type": "web_search" }` 工具和来源标注。旧版配置仍然有效:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"webSearch": {
|
|
162
|
+
"models": ["my-gateway/gpt-5.6-luna"]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
没有配置新路由字段时,只有精确命中该列表、且 API 属于现有 Responses 系列(`openai-responses` 或 `openai-codex-responses`)的模型会使用托管搜索。
|
|
168
|
+
- **`standalone-alpha`** 暴露一个 sequential 的 `web.run` 工具,并向 provider 相对的 `/alpha/search` 端点发送一次隔离的 `POST` 请求。例如基础地址是 `https://gateway.example/v1` 时,请求地址是 `https://gateway.example/v1/alpha/search`,不会变成 Responses 端点。支持的命令族包括 `search_query`、`image_query`、`open`、`click`、`find`、`screenshot`、`finance`、`weather`、`sports` 和 `time`;`response_length` 用于调整请求的结果长度。
|
|
169
|
+
|
|
170
|
+
standalone 路由属于实验性的 CPA/Codex 网关协议,不是稳定的公开 OpenAI Responses 端点。网关/provider 必须提供 `/alpha/search`、启用 `alpha-search` 能力,并支持 standalone web search(Codex provider 通常以 `supports_standalone_web_search = true` 表示)。请求会复用 Pi 当前模型、认证、provider headers、会话标识及 Codex/gateway affinity,不会切换当前模型。MVP 发送受边界限制的命令 envelope,而不是完整会话 transcript;`ref_id` 后续操作依赖 provider 的会话/引用处理;每次工具调用最多发送一次请求;配置无效、路由不可用、取消、超时、非 2xx、响应畸形或超限时都会 fail closed。
|
|
146
171
|
|
|
147
172
|
### 生成图片
|
|
148
173
|
|
|
@@ -186,8 +211,12 @@ pi --model my-gateway/gpt-5.6-luna
|
|
|
186
211
|
| `compaction.contextManagement` | `"off"` | 设置为 `"remote"` 后启用 Codex 远程上下文。 |
|
|
187
212
|
| `compaction.gatewayContextModels` | `[]` | 允许使用远程上下文的网关模型。 |
|
|
188
213
|
| `compaction.remoteCompactModel` | 未设置 | 仅用于 v2 压缩请求的可选模型。 |
|
|
214
|
+
| `compaction.remoteV2ContextSource` | `"legacy"` | 保留原来的原始 session/branch 输入路径。设置为 `"pi-context-hook"` 后才启用 Pi 的有序 context hook projection。检查点与模式绑定。 |
|
|
189
215
|
| `compaction.contextReminderThresholdPercent` | `5` | 每个窗口触发一次提醒的剩余预算百分比。设置为 `0` 会关闭提醒和窗口耗尽兜底。 |
|
|
190
|
-
| `webSearch.
|
|
216
|
+
| `webSearch.enabled` | `true` | 工具包联网搜索路由的总开关。设为 `false` 时不选择任何工具包路由。 |
|
|
217
|
+
| `webSearch.defaultRoute` | 未设置 | 默认路由:`local`、`hosted` 或 `standalone-alpha`。未设置时保留旧版行为。 |
|
|
218
|
+
| `webSearch.routes` | 未设置 | 精确的 `provider/model-id` 路由覆盖。精确条目优先于 `defaultRoute`。 |
|
|
219
|
+
| `webSearch.models` | `[]` | 旧版精确白名单;没有新路由字段时,列表中的 Responses 系列模型使用托管联网搜索。 |
|
|
191
220
|
| `imageGeneration.enabled` | `false` | 启用 `openai_generate_image`。 |
|
|
192
221
|
| `imageGeneration.models` | `["gpt-image-2.5"]` | 裸生图模型 ID;列表第一项是默认模型。 |
|
|
193
222
|
| `autoMode.models` | `[]` | 允许使用自动模式的模型。 |
|
package/docs/internals.md
CHANGED
|
@@ -59,8 +59,10 @@ sequenceDiagram
|
|
|
59
59
|
State invariants the lifecycle code must keep honest:
|
|
60
60
|
|
|
61
61
|
- The checkpoint gate verifies a successful `notes` `append_to_file` or `write_file` against the persisted session branch after the latest window boundary, so it survives restarts and forks. A response with `ok: false` or `success: false` is not successful, even when HTTP returns 200.
|
|
62
|
+
- 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.
|
|
62
63
|
- 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.
|
|
63
|
-
- `new_context` has no force escape hatch: a successful notes checkpoint is always required before rollover.
|
|
64
|
+
- `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.
|
|
65
|
+
- After a marker is accepted for sending, the manager keeps a session/window-anchored pending guard until that exact target marker is persisted or the session changes. A duplicate `new_context` during this persistence gap returns `started: false` and sends no second marker; projected/in-memory messages do not retire the guard.
|
|
64
66
|
- Budget checks are skipped until the current window has produced its own assistant usage. Acting on the previous window's usage anchor would burn the once-per-window reminder on a false alarm.
|
|
65
67
|
- The scheduled trim is consumed exactly once, synchronously, by the first compaction attempt whose boundary window ID matches. Every other compaction path, including a threshold without a scheduled rollover, manual `/compact`, and overflow, is cancelled.
|
|
66
68
|
- A window boundary is persisted as a `codex-context-window` custom message. `session_start` replays boundaries from the branch and rebuilds identity after forks.
|
|
@@ -75,6 +77,12 @@ A `compaction_trigger` item appended to the live streaming request yields one ou
|
|
|
75
77
|
|
|
76
78
|
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.
|
|
77
79
|
|
|
80
|
+
Pi 0.85.1's `session_before_compact` event supplies preparation and branch data, while the direct Remote V2 client can bypass the provider `context` hook chain. To preserve compatibility, an omitted `compaction.remoteV2ContextSource` keeps the original `"legacy"` behavior: first compaction uses `buildSessionContext()` and falls back to `SessionBeforeCompactEvent.preparation`, while recursion uses the opaque window plus the raw branch tail.
|
|
81
|
+
|
|
82
|
+
Setting `compaction.remoteV2ContextSource: "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()`. In this opt-in mode, Remote V2 first reads `buildSessionContext()` and then runs the ordered projection. If the bridge, session context, or recursive summary anchor is unavailable, the extension cancels rather than sending an unprojected history. The legacy mode is an intentional compatibility trade-off and does not claim parity with provider-visible context hooks.
|
|
83
|
+
|
|
84
|
+
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.
|
|
85
|
+
|
|
78
86
|
---
|
|
79
87
|
|
|
80
88
|
### Artifacts and debugging
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-openai-toolkit",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.19.0"
|
|
8
8
|
},
|
|
9
|
-
"description": "OpenAI toolkit for Pi: Codex Remote Context windows, remote compaction v2,
|
|
9
|
+
"description": "OpenAI toolkit for Pi: Codex Remote Context windows, remote compaction v2, routed Web Search, image gen, auto mode.",
|
|
10
10
|
"author": "awoaCrim and contributors",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"repository": {
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
"extensions/image-generation.ts",
|
|
48
48
|
"extensions/auto-mode.ts",
|
|
49
49
|
"src/compact-client.ts",
|
|
50
|
+
"src/compaction-projection.ts",
|
|
51
|
+
"src/pi-context-hook.ts",
|
|
50
52
|
"src/config.ts",
|
|
51
53
|
"src/debug.ts",
|
|
52
54
|
"src/details-store.ts",
|
|
@@ -92,6 +94,7 @@
|
|
|
92
94
|
"src/serializer.ts",
|
|
93
95
|
"src/supported-environment.ts",
|
|
94
96
|
"src/types.ts",
|
|
97
|
+
"src/web-search/alpha-client.ts",
|
|
95
98
|
"src/web-search/extension.ts",
|
|
96
99
|
"src/web-search/payload.ts",
|
|
97
100
|
"src/web-search/prompt.ts",
|
|
@@ -0,0 +1,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
|
+
export type UnprojectedCompactionReason =
|
|
16
|
+
| typeof COMPACTION_PROJECTION_UNAVAILABLE
|
|
17
|
+
| typeof COMPACTION_SESSION_CONTEXT_UNAVAILABLE
|
|
18
|
+
| typeof COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE
|
|
19
|
+
| typeof COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A Remote V2 checkpoint is safe for the current replay chain only when its
|
|
23
|
+
* marker matches the configured input source. Structural persistence validation
|
|
24
|
+
* intentionally remains more permissive for old session files.
|
|
25
|
+
*/
|
|
26
|
+
export function hasVerifiedCompactionInputProvenance(
|
|
27
|
+
details: Pick<NativeCompactionDetails, "inputProvenance"> | undefined,
|
|
28
|
+
expected: NativeCompactionInputProvenance,
|
|
29
|
+
): boolean {
|
|
30
|
+
return details?.inputProvenance === expected;
|
|
31
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -31,10 +31,12 @@ import {
|
|
|
31
31
|
type CompactionConfig,
|
|
32
32
|
type ContextManagementMode,
|
|
33
33
|
type ImageGenerationConfig,
|
|
34
|
+
type RemoteV2ContextSource,
|
|
34
35
|
type LoadedToolkitConfig,
|
|
35
36
|
type NativeFallbackConfig,
|
|
36
37
|
type ToolkitConfig,
|
|
37
38
|
type WebSearchConfig,
|
|
39
|
+
type WebSearchRoute,
|
|
38
40
|
} from "./types";
|
|
39
41
|
import { MAX_IMAGE_MODEL_ID_CHARS } from "./image-generation/types";
|
|
40
42
|
|
|
@@ -47,6 +49,7 @@ const COMPACTION_FIELDS = new Set([
|
|
|
47
49
|
"contextManagement",
|
|
48
50
|
"allowCompactionContinuityBreak",
|
|
49
51
|
"remoteCompactModel",
|
|
52
|
+
"remoteV2ContextSource",
|
|
50
53
|
"nativeFallback",
|
|
51
54
|
"responsesApis",
|
|
52
55
|
"gatewayContextModels",
|
|
@@ -59,7 +62,7 @@ const COMPACTION_FIELDS = new Set([
|
|
|
59
62
|
"artifactRoot",
|
|
60
63
|
]);
|
|
61
64
|
const NATIVE_FALLBACK_FIELDS = new Set(["enabled", "model", "thinkingLevel"]);
|
|
62
|
-
const WEB_SEARCH_FIELDS = new Set(["enabled", "models"]);
|
|
65
|
+
const WEB_SEARCH_FIELDS = new Set(["enabled", "models", "defaultRoute", "routes"]);
|
|
63
66
|
const IMAGE_GENERATION_FIELDS = new Set(["enabled", "models"]);
|
|
64
67
|
const AUTO_MODE_FIELDS = new Set([
|
|
65
68
|
"enabled",
|
|
@@ -148,6 +151,20 @@ function toContextManagementMode(
|
|
|
148
151
|
return undefined;
|
|
149
152
|
}
|
|
150
153
|
|
|
154
|
+
function toRemoteV2ContextSource(
|
|
155
|
+
value: unknown,
|
|
156
|
+
fieldPath: string,
|
|
157
|
+
warnings: string[],
|
|
158
|
+
): RemoteV2ContextSource | undefined {
|
|
159
|
+
if (value === undefined) return undefined;
|
|
160
|
+
if (typeof value === "string") {
|
|
161
|
+
const normalized = value.trim();
|
|
162
|
+
if (normalized === "pi-context-hook" || normalized === "legacy") return normalized;
|
|
163
|
+
}
|
|
164
|
+
warnings.push(`Ignoring ${fieldPath}: expected one of pi-context-hook, legacy.`);
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
151
168
|
function toModelSpec(value: unknown, fieldPath: string, warnings: string[]): string | null | undefined {
|
|
152
169
|
if (value === undefined) return undefined;
|
|
153
170
|
if (value === null) return null;
|
|
@@ -231,6 +248,53 @@ function toImageGenerationModels(value: unknown, fieldPath: string, warnings: st
|
|
|
231
248
|
return models;
|
|
232
249
|
}
|
|
233
250
|
|
|
251
|
+
function toWebSearchRoute(
|
|
252
|
+
value: unknown,
|
|
253
|
+
fieldPath: string,
|
|
254
|
+
warnings: string[],
|
|
255
|
+
): WebSearchRoute | undefined {
|
|
256
|
+
if (value === undefined) return undefined;
|
|
257
|
+
if (value === "local" || value === "hosted" || value === "standalone-alpha") {
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
warnings.push(`Ignoring ${fieldPath}: expected one of local, hosted, standalone-alpha.`);
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isExactWebSearchModelKey(value: string): boolean {
|
|
265
|
+
const trimmed = value.trim();
|
|
266
|
+
const separatorIndex = trimmed.indexOf("/");
|
|
267
|
+
if (separatorIndex <= 0 || separatorIndex >= trimmed.length - 1) return false;
|
|
268
|
+
if (/[\s*?\[\]{}]/.test(trimmed)) return false;
|
|
269
|
+
const provider = trimmed.slice(0, separatorIndex);
|
|
270
|
+
const modelId = trimmed.slice(separatorIndex + 1);
|
|
271
|
+
return provider.length > 0 && modelId.length > 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function toWebSearchRoutes(
|
|
275
|
+
value: unknown,
|
|
276
|
+
fieldPath: string,
|
|
277
|
+
warnings: string[],
|
|
278
|
+
): Record<string, WebSearchRoute> | undefined {
|
|
279
|
+
if (value === undefined) return undefined;
|
|
280
|
+
if (!isRecord(value)) {
|
|
281
|
+
warnings.push(`Ignoring ${fieldPath}: expected a JSON object.`);
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const routes: Record<string, WebSearchRoute> = {};
|
|
286
|
+
for (const [rawKey, rawRoute] of Object.entries(value)) {
|
|
287
|
+
const key = rawKey.trim();
|
|
288
|
+
if (!isExactWebSearchModelKey(key)) {
|
|
289
|
+
warnings.push(`Ignoring ${fieldPath}.${rawKey}: expected an exact "provider/model-id" key.`);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const route = toWebSearchRoute(rawRoute, `${fieldPath}.${rawKey}`, warnings);
|
|
293
|
+
if (route !== undefined) routes[key] = route;
|
|
294
|
+
}
|
|
295
|
+
return routes;
|
|
296
|
+
}
|
|
297
|
+
|
|
234
298
|
function toAutoModeGate(value: unknown, fieldPath: string, warnings: string[]): AutoModeGate | undefined {
|
|
235
299
|
if (value === undefined) return undefined;
|
|
236
300
|
if (value === "side-effect" || value === "all") return value;
|
|
@@ -264,6 +328,12 @@ function cloneDefaults(): ToolkitConfig {
|
|
|
264
328
|
webSearch: {
|
|
265
329
|
...DEFAULT_WEB_SEARCH_CONFIG,
|
|
266
330
|
models: [...DEFAULT_WEB_SEARCH_CONFIG.models],
|
|
331
|
+
...(DEFAULT_WEB_SEARCH_CONFIG.defaultRoute
|
|
332
|
+
? { defaultRoute: DEFAULT_WEB_SEARCH_CONFIG.defaultRoute }
|
|
333
|
+
: {}),
|
|
334
|
+
...(DEFAULT_WEB_SEARCH_CONFIG.routes
|
|
335
|
+
? { routes: { ...DEFAULT_WEB_SEARCH_CONFIG.routes } }
|
|
336
|
+
: {}),
|
|
267
337
|
},
|
|
268
338
|
imageGeneration: {
|
|
269
339
|
...DEFAULT_IMAGE_GENERATION_CONFIG,
|
|
@@ -347,6 +417,10 @@ function applyCompactionConfig(
|
|
|
347
417
|
resolved.remoteCompactModel = remoteCompactModelSpec === null ? undefined : remoteCompactModelSpec;
|
|
348
418
|
}
|
|
349
419
|
|
|
420
|
+
resolved.remoteV2ContextSource =
|
|
421
|
+
toRemoteV2ContextSource(raw.remoteV2ContextSource, "compaction.remoteV2ContextSource", warnings) ??
|
|
422
|
+
resolved.remoteV2ContextSource;
|
|
423
|
+
|
|
350
424
|
if (raw.nativeFallback !== undefined) {
|
|
351
425
|
if (isRecord(raw.nativeFallback)) {
|
|
352
426
|
applyNativeFallbackConfig(raw.nativeFallback, resolved.nativeFallback, warnings);
|
|
@@ -393,6 +467,31 @@ function applyWebSearchConfig(
|
|
|
393
467
|
if (models !== undefined) {
|
|
394
468
|
resolved.models = models;
|
|
395
469
|
}
|
|
470
|
+
|
|
471
|
+
const defaultRoute = toWebSearchRoute(raw.defaultRoute, "webSearch.defaultRoute", warnings);
|
|
472
|
+
if (defaultRoute !== undefined) {
|
|
473
|
+
resolved.defaultRoute = defaultRoute;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const routes = toWebSearchRoutes(raw.routes, "webSearch.routes", warnings);
|
|
477
|
+
if (routes !== undefined) {
|
|
478
|
+
resolved.routes = routes;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (defaultRoute !== undefined && resolved.models.length > 0) {
|
|
482
|
+
warnings.push(
|
|
483
|
+
"webSearch.defaultRoute overrides legacy webSearch.models for models without an exact webSearch.routes entry.",
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
if (routes !== undefined) {
|
|
487
|
+
for (const key of Object.keys(routes)) {
|
|
488
|
+
if (resolved.models.includes(key)) {
|
|
489
|
+
warnings.push(
|
|
490
|
+
`webSearch.routes.${key} overrides the legacy webSearch.models entry for the same exact model.`,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
396
495
|
}
|
|
397
496
|
|
|
398
497
|
function applyImageGenerationConfig(
|
|
@@ -35,7 +35,7 @@ export const CONTEXT_WINDOW_COMPACTION_SUMMARY =
|
|
|
35
35
|
"[Pi Codex context-window boundary; no conversation summary was generated.]";
|
|
36
36
|
|
|
37
37
|
const CONTEXT_WINDOW_GUIDANCE = `<context_window_guidance>
|
|
38
|
-
Checkpoint the active request, known history IDs, decisions, progress, learnings and next steps in notes before new_context; no summary carries over. After rollover, read the checkpoint receipt note first when present; use a thread hint only as supplemental guidance. Use history only for a missing detail.
|
|
38
|
+
Checkpoint the active request, known history IDs, decisions, progress, learnings and next steps in notes before new_context, and wait for that notes result to be persisted; only a persisted successful result in the current window unlocks the rollover, and no summary carries over. If new_context reports that a rollover is already scheduled, do not call it again in the same window. After rollover, read the checkpoint receipt note first when present; use a thread hint only as supplemental guidance. Use history only for a missing detail.
|
|
39
39
|
</context_window_guidance>`;
|
|
40
40
|
|
|
41
41
|
export function renderContextWindowMessage(
|
|
@@ -61,8 +61,10 @@ export interface NewContextDetails { started: boolean; }
|
|
|
61
61
|
export const NEW_CONTEXT_PARAMETERS = Type.Object({}, { additionalProperties: false });
|
|
62
62
|
|
|
63
63
|
export const NEW_CONTEXT_CHECKPOINT_REQUIRED_MESSAGE =
|
|
64
|
-
"new_context refused: no successful notes checkpoint in
|
|
65
|
-
+ "
|
|
64
|
+
"new_context refused: no persisted successful notes checkpoint in the current context window. "
|
|
65
|
+
+ "Write the active request, decisions, progress and next steps with notes append_to_file or write_file, "
|
|
66
|
+
+ "wait until that notes result is persisted, then retry new_context in a later tool turn. "
|
|
67
|
+
+ "Do not pass force arguments and do not repeat new_context while a rollover is already scheduled.";
|
|
66
68
|
export interface ContextRemainingDetails {
|
|
67
69
|
remainingTokens?: number;
|
|
68
70
|
windowId?: string;
|
|
@@ -88,13 +90,23 @@ export function createContextManagementTools(
|
|
|
88
90
|
const newContext: ToolDefinition<typeof NEW_CONTEXT_PARAMETERS, NewContextDetails> = {
|
|
89
91
|
name: "new_context",
|
|
90
92
|
label: "new_context",
|
|
91
|
-
description: "Start a new remote Codex context window without generating a conversation summary. Requires a successful notes checkpoint in the current window.",
|
|
93
|
+
description: "Start a new remote Codex context window without generating a conversation summary. Requires a persisted successful notes checkpoint in the current window.",
|
|
92
94
|
parameters: NEW_CONTEXT_PARAMETERS,
|
|
93
95
|
promptSnippet: "Start a new remote Codex context window without summarizing history.",
|
|
94
|
-
promptGuidelines: [
|
|
96
|
+
promptGuidelines: [
|
|
97
|
+
"Checkpoint active work in notes before calling new_context; no conversation summary carries over. Only a persisted successful notes append/write in the current window unlocks the rollover.",
|
|
98
|
+
"Wait for the notes tool result before calling new_context; an in-flight or failed write is not a checkpoint. If new_context reports that a rollover is already scheduled, do not call it again in the same window.",
|
|
99
|
+
],
|
|
95
100
|
executionMode: "sequential",
|
|
96
101
|
async execute(_id, _params, signal, _update, ctx) {
|
|
97
102
|
await assertActive(ctx);
|
|
103
|
+
manager.synchronize(ctx);
|
|
104
|
+
if (manager.hasPendingRollover(ctx)) {
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: "text", text: "A new context window is already scheduled." }],
|
|
107
|
+
details: { started: false },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
98
110
|
if (!manager.hasNotesCheckpointSinceBoundary(ctx)) {
|
|
99
111
|
throw new Error(NEW_CONTEXT_CHECKPOINT_REQUIRED_MESSAGE);
|
|
100
112
|
}
|
|
@@ -148,7 +160,7 @@ export function createContextManagementTools(
|
|
|
148
160
|
parameters: NOTES_PARAMETERS,
|
|
149
161
|
promptSnippet: "Read and checkpoint remote Codex notes across context windows.",
|
|
150
162
|
promptGuidelines: [
|
|
151
|
-
"Before calling new_context, checkpoint the current turn's active work (unfinished tasks, decisions, open questions, references) into notes with append_to_file or write_file so it survives the window change.",
|
|
163
|
+
"Before calling new_context, checkpoint the current turn's active work (unfinished tasks, decisions, open questions, references) into notes with append_to_file or write_file so it survives the window change; wait for that result to be persisted before calling new_context.",
|
|
152
164
|
"When a large task spans multiple context windows, keep a running note per line of work and read it at the start of each new window; append new state instead of replacing it unless the note is stale.",
|
|
153
165
|
"Prefer read_file or search_contents for looking things up; reserve write_file for explicit rewrite/clear and append_to_file for incremental state.",
|
|
154
166
|
"Keep note text concise and self-contained: it may be read later without the rest of the conversation, so include identifiers and verbatim key decisions, not hearsay summaries.",
|
|
@@ -170,12 +182,9 @@ export class ContextManagementToolController {
|
|
|
170
182
|
parameters: unknown;
|
|
171
183
|
promptGuidelines?: string[];
|
|
172
184
|
}>();
|
|
173
|
-
private readonly ownedNames = new Set<string>();
|
|
174
|
-
private readonly baselineNames = new Set<string>();
|
|
175
185
|
private registered = false;
|
|
176
186
|
private registrationChecked = false;
|
|
177
187
|
private registrationValid = false;
|
|
178
|
-
private baselineCaptured = false;
|
|
179
188
|
|
|
180
189
|
constructor(private readonly pi: ExtensionAPI) {}
|
|
181
190
|
|
|
@@ -233,44 +242,22 @@ export class ContextManagementToolController {
|
|
|
233
242
|
} catch {
|
|
234
243
|
this.registrationValid = false;
|
|
235
244
|
}
|
|
236
|
-
if (!this.registrationValid) this.ownedNames.clear();
|
|
237
245
|
return this.registrationValid;
|
|
238
246
|
}
|
|
239
247
|
|
|
240
|
-
private captureBaseline(): boolean {
|
|
241
|
-
if (this.baselineCaptured) return true;
|
|
242
|
-
try {
|
|
243
|
-
const api = this.pi as ExtensionAPI & { getActiveTools?: () => string[] };
|
|
244
|
-
if (typeof api.getActiveTools !== "function") return false;
|
|
245
|
-
this.baselineNames.clear();
|
|
246
|
-
for (const name of api.getActiveTools()) this.baselineNames.add(name);
|
|
247
|
-
this.baselineCaptured = true;
|
|
248
|
-
return true;
|
|
249
|
-
} catch {
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
248
|
sync(active: boolean): boolean {
|
|
255
|
-
if (!this.verifyRegistration()
|
|
249
|
+
if (!this.verifyRegistration()) return false;
|
|
256
250
|
const api = this.pi as ExtensionAPI & { getActiveTools?: () => string[]; setActiveTools?: (names: string[]) => void };
|
|
257
251
|
if (typeof api.getActiveTools !== "function" || typeof api.setActiveTools !== "function") return false;
|
|
258
252
|
try {
|
|
259
253
|
const current = api.getActiveTools();
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
if (next.length !== current.length) api.setActiveTools(next);
|
|
269
|
-
return true;
|
|
270
|
-
}
|
|
271
|
-
const next = current.filter((name) => !this.ownedNames.has(name));
|
|
254
|
+
// Pi may activate a newly registered tool before the first sync. Once
|
|
255
|
+
// registration is verified, every registered name belongs to this
|
|
256
|
+
// controller: active models get all four, inactive models get none.
|
|
257
|
+
const next = active
|
|
258
|
+
? [...current, ...[...this.registeredNames].filter((name) => !current.includes(name))]
|
|
259
|
+
: current.filter((name) => !this.registeredNames.has(name));
|
|
272
260
|
if (next.length !== current.length) api.setActiveTools(next);
|
|
273
|
-
this.ownedNames.clear();
|
|
274
261
|
return true;
|
|
275
262
|
} catch {
|
|
276
263
|
return false;
|
|
@@ -279,8 +266,6 @@ export class ContextManagementToolController {
|
|
|
279
266
|
|
|
280
267
|
reset(): void {
|
|
281
268
|
this.sync(false);
|
|
282
|
-
this.baselineNames.clear();
|
|
283
|
-
this.baselineCaptured = false;
|
|
284
269
|
}
|
|
285
270
|
get isRegistered(): boolean { return this.registrationValid; }
|
|
286
271
|
}
|