pi-openai-toolkit 0.14.7 → 0.14.9
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 +37 -8
- package/README.zh.md +37 -8
- package/docs/internals.md +27 -1
- package/extensions/auto-mode.ts +1 -1
- package/extensions/codex-astra.ts +1 -1
- package/extensions/compaction.ts +1 -1
- package/extensions/image-generation.ts +1 -1
- package/extensions/web-search.ts +1 -1
- package/package.json +6 -2
- package/src/auto-mode/classifier.ts +165 -165
- package/src/auto-mode/extension.ts +129 -20
- package/src/auto-mode/policy.ts +146 -146
- package/src/auto-mode/prompt.ts +279 -279
- package/src/auto-mode/reviewer.ts +325 -325
- package/src/auto-mode/tool-review-tui.ts +211 -0
- package/src/auto-mode/transcript.ts +133 -133
- package/src/auto-mode/types.ts +137 -137
- package/src/codex-astra/effort-planner.ts +175 -175
- package/src/codex-astra/extension.ts +122 -122
- package/src/compaction-projection.ts +31 -0
- package/src/config.ts +100 -1
- package/src/context-management/messages.ts +112 -112
- package/src/context-management/namespace-tools.ts +426 -426
- package/src/context-management/tool-contract.ts +23 -23
- package/src/context-management/tools.ts +291 -279
- package/src/context-management/window-budget.ts +83 -83
- package/src/context-management/window-manager.ts +110 -28
- package/src/context-management/window-request.ts +125 -125
- package/src/extension-runtime.ts +263 -34
- package/src/image-generation/artifacts.ts +237 -237
- package/src/image-generation/client.ts +248 -248
- package/src/image-generation/eligibility.ts +18 -18
- package/src/image-generation/references.ts +194 -194
- package/src/image-generation/render.ts +252 -252
- package/src/model-scope.ts +17 -17
- package/src/payload-rewrite.ts +55 -10
- package/src/pi-context-hook.ts +116 -0
- package/src/provider-headers.ts +23 -23
- 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 +523 -80
- package/src/web-search/payload.ts +295 -96
- package/src/web-search/prompt.ts +59 -20
- package/src/web-search/types.ts +144 -45
- 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
|
|
|
@@ -174,7 +199,7 @@ Allow a model and reviewer in `autoMode`:
|
|
|
174
199
|
}
|
|
175
200
|
```
|
|
176
201
|
|
|
177
|
-
Use `/auto on` in the session. The default `side-effect` gate reviews `bash`, `write`, `edit`, and configured extra tools. Set `gate` to `"all"` when every tool call needs review. A reviewer timeout does not approve a call.
|
|
202
|
+
Use `/auto on` in the session, or start Pi with `--auto`. The TUI shows an activation notice and temporarily changes the working indicator to `Auto mode: reviewing <tool>` while a gated call is being reviewed; the footer keeps the active gate visible. On Pi versions that expose the compatible tool renderer, each tool block in Auto Mode also gets a bottom line: reviewed calls show states such as `allowed by reviewer · low risk · authorization medium` or `denied · <reason>`, while calls outside the configured gate show `not reviewed · outside the configured gate`. If that renderer seam is unavailable, the extension warns once and keeps the footer-only status. The default `side-effect` gate reviews `bash`, `write`, `edit`, and configured extra tools. Set `gate` to `"all"` when every tool call needs review. A reviewer timeout does not approve a call.
|
|
178
203
|
|
|
179
204
|
## Common configuration
|
|
180
205
|
|
|
@@ -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
|
|
|
@@ -174,7 +199,7 @@ pi --model my-gateway/gpt-5.6-luna
|
|
|
174
199
|
}
|
|
175
200
|
```
|
|
176
201
|
|
|
177
|
-
在会话中使用 `/auto on
|
|
202
|
+
在会话中使用 `/auto on`,也可以用 `--auto` 启动 Pi。TUI 会显示自动模式已开启的提示;每次审查受控工具时,工作指示器会临时显示 `Auto mode: reviewing <tool>`,底部状态栏会保留当前审查范围。在支持兼容 tool renderer 的 Pi 版本中,自动模式下每个工具块底部都会显示一行状态:已审查的调用会显示 `allowed by reviewer · low risk · authorization medium` 或 `denied · <reason>`,不在当前审查范围内的调用会显示 `not reviewed · outside the configured gate`。如果当前 Pi 没有这个 renderer seam,扩展会告警一次并继续使用底部状态栏提示。默认的 `side-effect` 审查范围覆盖 `bash`、`write`、`edit` 和额外配置的工具。如果需要审查所有工具调用,将 `gate` 设置为 `"all"`。审查超时不会自动放行调用。
|
|
178
203
|
|
|
179
204
|
## 常用配置
|
|
180
205
|
|
|
@@ -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
|
@@ -9,6 +9,7 @@ Developer-facing reference for [pi-openai-toolkit](../README.md). The README kee
|
|
|
9
9
|
- [Remote Context protocol](#remote-context-protocol)
|
|
10
10
|
- [Rollover lifecycle](#rollover-lifecycle)
|
|
11
11
|
- [Remote Compaction v2 wire contract](#remote-compaction-v2-wire-contract)
|
|
12
|
+
- [Auto Mode TUI review renderer](#auto-mode-tui-review-renderer)
|
|
12
13
|
- [Artifacts and debugging](#artifacts-and-debugging)
|
|
13
14
|
- [Provenance](#provenance)
|
|
14
15
|
|
|
@@ -59,8 +60,10 @@ sequenceDiagram
|
|
|
59
60
|
State invariants the lifecycle code must keep honest:
|
|
60
61
|
|
|
61
62
|
- 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.
|
|
63
|
+
- 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
64
|
- 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.
|
|
65
|
+
- `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.
|
|
66
|
+
- 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
67
|
- 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
68
|
- 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
69
|
- 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 +78,29 @@ A `compaction_trigger` item appended to the live streaming request yields one ou
|
|
|
75
78
|
|
|
76
79
|
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
80
|
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
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.
|
|
84
|
+
|
|
85
|
+
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.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### Auto Mode TUI review renderer
|
|
90
|
+
|
|
91
|
+
Auto Mode owns the review decision, but Pi owns the built-in tool-block component. Pi 0.85.1 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.
|
|
92
|
+
|
|
93
|
+
The extension updates an ephemeral per-call state through the same `tool_call` lifecycle that performs the review:
|
|
94
|
+
|
|
95
|
+
- `skipped` when the tool is outside the configured Auto Mode gate;
|
|
96
|
+
- `reviewing` while the reviewer model is running;
|
|
97
|
+
- `awaiting-user` when an unavailable review reaches interactive confirmation;
|
|
98
|
+
- `allowed`, `denied`, or `blocked` after the final decision.
|
|
99
|
+
|
|
100
|
+
The state is intentionally not persisted to the session. It is bounded to recent calls and cleared at `session_start`, so reloading a session does not invent historical approval claims. The renderer uses Pi's current theme when available, truncates untrusted rationale text, and preserves the original render output when no state exists.
|
|
101
|
+
|
|
102
|
+
This is a version-coupled adapter, not a stable Pi extension contract. It is feature-detected at registration time and marked with a global symbol so extension reloads do not wrap the class repeatedly. If the exported component shape changes or becomes non-writable, the adapter becomes a no-op, emits one warning in TUI mode, and the existing footer/working-message review indicators remain the fallback.
|
|
103
|
+
|
|
78
104
|
---
|
|
79
105
|
|
|
80
106
|
### Artifacts and debugging
|
package/extensions/auto-mode.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default } from "../src/auto-mode/extension.ts";
|
|
1
|
+
export { default } from "../src/auto-mode/extension.ts";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default } from "../src/codex-astra/extension.ts";
|
|
1
|
+
export { default } from "../src/codex-astra/extension.ts";
|
package/extensions/compaction.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default } from "../src/extension-runtime.ts";
|
|
1
|
+
export { default } from "../src/extension-runtime.ts";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default } from "../src/image-generation/extension.ts";
|
|
1
|
+
export { default } from "../src/image-generation/extension.ts";
|
package/extensions/web-search.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default } from "../src/web-search/extension.ts";
|
|
1
|
+
export { default } from "../src/web-search/extension.ts";
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-openai-toolkit",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.9",
|
|
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",
|
|
@@ -72,6 +74,7 @@
|
|
|
72
74
|
"src/image-generation/service.ts",
|
|
73
75
|
"src/image-generation/types.ts",
|
|
74
76
|
"src/auto-mode/extension.ts",
|
|
77
|
+
"src/auto-mode/tool-review-tui.ts",
|
|
75
78
|
"src/auto-mode/policy.ts",
|
|
76
79
|
"src/auto-mode/prompt.ts",
|
|
77
80
|
"src/auto-mode/transcript.ts",
|
|
@@ -92,6 +95,7 @@
|
|
|
92
95
|
"src/serializer.ts",
|
|
93
96
|
"src/supported-environment.ts",
|
|
94
97
|
"src/types.ts",
|
|
98
|
+
"src/web-search/alpha-client.ts",
|
|
95
99
|
"src/web-search/extension.ts",
|
|
96
100
|
"src/web-search/payload.ts",
|
|
97
101
|
"src/web-search/prompt.ts",
|
|
@@ -1,165 +1,165 @@
|
|
|
1
|
-
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
-
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { parseModelSpec } from "../runtime";
|
|
4
|
-
import { CLASSIFIER_SYSTEM_PROMPT } from "./prompt";
|
|
5
|
-
import { boundReviewText, type ClassifierRisk, type FastDecisionReason } from "./types";
|
|
6
|
-
|
|
7
|
-
/** One completed classification of the trajectory, tagged to a tool-call position. */
|
|
8
|
-
export type ScoreRecord = {
|
|
9
|
-
risk: ClassifierRisk;
|
|
10
|
-
/** Tool-call counter value when the sample was taken. */
|
|
11
|
-
scoredAtCall: number;
|
|
12
|
-
/** Fingerprint of the user turns the score was based on. */
|
|
13
|
-
authorizationVersion: string;
|
|
14
|
-
sampledAt: number;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Mutable per-session tracker for the non-blocking pre-scorer.
|
|
19
|
-
*
|
|
20
|
-
* The counters reproduce Codex's staleness rule: the agent keeps running while a
|
|
21
|
-
* classification is in flight, so a score is only trustworthy if it is recent
|
|
22
|
-
* enough and nothing the user said has changed underneath it.
|
|
23
|
-
*/
|
|
24
|
-
export type ScoreTracker = {
|
|
25
|
-
latestCallIndex: number;
|
|
26
|
-
latestScoredIndex: number;
|
|
27
|
-
latestFailedIndex: number;
|
|
28
|
-
score?: ScoreRecord;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export function createScoreTracker(): ScoreTracker {
|
|
32
|
-
return { latestCallIndex: 0, latestScoredIndex: 0, latestFailedIndex: 0 };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Reset everything: a new turn or a disengaged mode invalidates prior scoring. */
|
|
36
|
-
export function resetScoreTracker(tracker: ScoreTracker): void {
|
|
37
|
-
tracker.latestCallIndex = 0;
|
|
38
|
-
tracker.latestScoredIndex = 0;
|
|
39
|
-
tracker.latestFailedIndex = 0;
|
|
40
|
-
tracker.score = undefined;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Fingerprint of the user-authored conversation. Any new or edited user turn makes
|
|
45
|
-
* an existing score stale, because the score's authorization premise changed.
|
|
46
|
-
*/
|
|
47
|
-
export function authorizationVersion(userText: string): string {
|
|
48
|
-
const bounded = boundReviewText(userText, 8_000);
|
|
49
|
-
let hash = 2166136261;
|
|
50
|
-
for (let index = 0; index < bounded.length; index += 1) {
|
|
51
|
-
hash ^= bounded.charCodeAt(index);
|
|
52
|
-
hash = Math.imul(hash, 16777619);
|
|
53
|
-
}
|
|
54
|
-
return `${bounded.length}:${(hash >>> 0).toString(36)}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function recordScoredCall(tracker: ScoreTracker, score: ScoreRecord): void {
|
|
58
|
-
tracker.score = score;
|
|
59
|
-
tracker.latestScoredIndex = Math.max(tracker.latestScoredIndex, score.scoredAtCall);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function recordFailedCall(tracker: ScoreTracker, scoredAtCall: number): void {
|
|
63
|
-
tracker.latestFailedIndex = Math.max(tracker.latestFailedIndex, scoredAtCall);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export type FastDecisionInput = {
|
|
67
|
-
tracker: ScoreTracker;
|
|
68
|
-
/** Tool-call counter value for the call being gated. */
|
|
69
|
-
currentCallIndex: number;
|
|
70
|
-
authorizationVersion: string;
|
|
71
|
-
maxLag: number;
|
|
72
|
-
/** Retries after a denial and widened gates must not take the fast path. */
|
|
73
|
-
requiresSynchronousReview?: boolean;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Decide whether a cached classification can satisfy this gated call without
|
|
78
|
-
* running the blocking reviewer. Anything other than `low_risk` defers to the
|
|
79
|
-
* synchronous review; the classifier never denies an action on its own.
|
|
80
|
-
*/
|
|
81
|
-
export function fastApprovalEligible(input: FastDecisionInput): {
|
|
82
|
-
eligible: boolean;
|
|
83
|
-
reason: FastDecisionReason;
|
|
84
|
-
} {
|
|
85
|
-
const defer = (reason: FastDecisionReason) => ({ eligible: false, reason });
|
|
86
|
-
|
|
87
|
-
if (input.requiresSynchronousReview) return defer("requires_synchronous_review");
|
|
88
|
-
if (input.tracker.latestFailedIndex > input.tracker.latestScoredIndex) return defer("scoring_failure");
|
|
89
|
-
|
|
90
|
-
const score = input.tracker.score;
|
|
91
|
-
if (!score) return defer("missing_score");
|
|
92
|
-
|
|
93
|
-
const lag = input.currentCallIndex - score.scoredAtCall;
|
|
94
|
-
if (lag > input.maxLag) return defer("stale_score");
|
|
95
|
-
if (score.authorizationVersion !== input.authorizationVersion) return defer("authorization_changed");
|
|
96
|
-
if (score.risk === "low") return { eligible: true, reason: "low_risk" };
|
|
97
|
-
return defer("elevated_risk");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export type ClassificationResult =
|
|
101
|
-
| { kind: ClassifierRisk; model: string }
|
|
102
|
-
| { kind: "failed"; reason: string };
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Run one non-blocking classification. Output is a single token; anything else is a
|
|
106
|
-
* failed sample rather than a verdict, so a confused classifier costs a review and
|
|
107
|
-
* never an approval or a denial.
|
|
108
|
-
*/
|
|
109
|
-
export async function classifyTrajectory(params: {
|
|
110
|
-
registry: Pick<ModelRegistry, "find" | "complete">;
|
|
111
|
-
modelSpec: string;
|
|
112
|
-
prompt: string;
|
|
113
|
-
timeoutMs: number;
|
|
114
|
-
signal?: AbortSignal;
|
|
115
|
-
}): Promise<ClassificationResult> {
|
|
116
|
-
const parsed = parseModelSpec(params.modelSpec);
|
|
117
|
-
if (!parsed) {
|
|
118
|
-
return { kind: "failed", reason: `autoMode.classifier.model "${params.modelSpec}" is not provider/model-id.` };
|
|
119
|
-
}
|
|
120
|
-
const model = params.registry.find(parsed.provider, parsed.modelId) as Model<Api> | undefined;
|
|
121
|
-
if (!model) {
|
|
122
|
-
return { kind: "failed", reason: `Classifier model ${params.modelSpec} is not available.` };
|
|
123
|
-
}
|
|
124
|
-
const label = boundReviewText(`${model.provider}/${model.id}`, 256);
|
|
125
|
-
|
|
126
|
-
const controller = new AbortController();
|
|
127
|
-
const abortFromCaller = () => controller.abort();
|
|
128
|
-
if (params.signal?.aborted) controller.abort();
|
|
129
|
-
else params.signal?.addEventListener("abort", abortFromCaller);
|
|
130
|
-
const timer = setTimeout(() => controller.abort(), params.timeoutMs);
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
const response = await params.registry.complete(
|
|
134
|
-
model,
|
|
135
|
-
{
|
|
136
|
-
systemPrompt: CLASSIFIER_SYSTEM_PROMPT,
|
|
137
|
-
messages: [
|
|
138
|
-
{
|
|
139
|
-
role: "user",
|
|
140
|
-
content: [{ type: "text", text: params.prompt }],
|
|
141
|
-
timestamp: Date.now(),
|
|
142
|
-
},
|
|
143
|
-
],
|
|
144
|
-
},
|
|
145
|
-
{ signal: controller.signal, cacheRetention: "none" },
|
|
146
|
-
);
|
|
147
|
-
if (response.stopReason === "error" || response.stopReason === "aborted") {
|
|
148
|
-
return { kind: "failed", reason: response.errorMessage ?? `Classifier stopped: ${response.stopReason}` };
|
|
149
|
-
}
|
|
150
|
-
const text = response.content
|
|
151
|
-
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
|
152
|
-
.map((block) => block.text)
|
|
153
|
-
.join("")
|
|
154
|
-
.trim()
|
|
155
|
-
.toLowerCase();
|
|
156
|
-
if (/^high\b/.test(text)) return { kind: "high", model: label };
|
|
157
|
-
if (/^low\b/.test(text)) return { kind: "low", model: label };
|
|
158
|
-
return { kind: "failed", reason: `Classifier returned unreadable output: ${boundReviewText(text, 80)}` };
|
|
159
|
-
} catch (error) {
|
|
160
|
-
return { kind: "failed", reason: error instanceof Error ? error.message : String(error) };
|
|
161
|
-
} finally {
|
|
162
|
-
clearTimeout(timer);
|
|
163
|
-
params.signal?.removeEventListener("abort", abortFromCaller);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { parseModelSpec } from "../runtime";
|
|
4
|
+
import { CLASSIFIER_SYSTEM_PROMPT } from "./prompt";
|
|
5
|
+
import { boundReviewText, type ClassifierRisk, type FastDecisionReason } from "./types";
|
|
6
|
+
|
|
7
|
+
/** One completed classification of the trajectory, tagged to a tool-call position. */
|
|
8
|
+
export type ScoreRecord = {
|
|
9
|
+
risk: ClassifierRisk;
|
|
10
|
+
/** Tool-call counter value when the sample was taken. */
|
|
11
|
+
scoredAtCall: number;
|
|
12
|
+
/** Fingerprint of the user turns the score was based on. */
|
|
13
|
+
authorizationVersion: string;
|
|
14
|
+
sampledAt: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Mutable per-session tracker for the non-blocking pre-scorer.
|
|
19
|
+
*
|
|
20
|
+
* The counters reproduce Codex's staleness rule: the agent keeps running while a
|
|
21
|
+
* classification is in flight, so a score is only trustworthy if it is recent
|
|
22
|
+
* enough and nothing the user said has changed underneath it.
|
|
23
|
+
*/
|
|
24
|
+
export type ScoreTracker = {
|
|
25
|
+
latestCallIndex: number;
|
|
26
|
+
latestScoredIndex: number;
|
|
27
|
+
latestFailedIndex: number;
|
|
28
|
+
score?: ScoreRecord;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function createScoreTracker(): ScoreTracker {
|
|
32
|
+
return { latestCallIndex: 0, latestScoredIndex: 0, latestFailedIndex: 0 };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Reset everything: a new turn or a disengaged mode invalidates prior scoring. */
|
|
36
|
+
export function resetScoreTracker(tracker: ScoreTracker): void {
|
|
37
|
+
tracker.latestCallIndex = 0;
|
|
38
|
+
tracker.latestScoredIndex = 0;
|
|
39
|
+
tracker.latestFailedIndex = 0;
|
|
40
|
+
tracker.score = undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fingerprint of the user-authored conversation. Any new or edited user turn makes
|
|
45
|
+
* an existing score stale, because the score's authorization premise changed.
|
|
46
|
+
*/
|
|
47
|
+
export function authorizationVersion(userText: string): string {
|
|
48
|
+
const bounded = boundReviewText(userText, 8_000);
|
|
49
|
+
let hash = 2166136261;
|
|
50
|
+
for (let index = 0; index < bounded.length; index += 1) {
|
|
51
|
+
hash ^= bounded.charCodeAt(index);
|
|
52
|
+
hash = Math.imul(hash, 16777619);
|
|
53
|
+
}
|
|
54
|
+
return `${bounded.length}:${(hash >>> 0).toString(36)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function recordScoredCall(tracker: ScoreTracker, score: ScoreRecord): void {
|
|
58
|
+
tracker.score = score;
|
|
59
|
+
tracker.latestScoredIndex = Math.max(tracker.latestScoredIndex, score.scoredAtCall);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function recordFailedCall(tracker: ScoreTracker, scoredAtCall: number): void {
|
|
63
|
+
tracker.latestFailedIndex = Math.max(tracker.latestFailedIndex, scoredAtCall);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type FastDecisionInput = {
|
|
67
|
+
tracker: ScoreTracker;
|
|
68
|
+
/** Tool-call counter value for the call being gated. */
|
|
69
|
+
currentCallIndex: number;
|
|
70
|
+
authorizationVersion: string;
|
|
71
|
+
maxLag: number;
|
|
72
|
+
/** Retries after a denial and widened gates must not take the fast path. */
|
|
73
|
+
requiresSynchronousReview?: boolean;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Decide whether a cached classification can satisfy this gated call without
|
|
78
|
+
* running the blocking reviewer. Anything other than `low_risk` defers to the
|
|
79
|
+
* synchronous review; the classifier never denies an action on its own.
|
|
80
|
+
*/
|
|
81
|
+
export function fastApprovalEligible(input: FastDecisionInput): {
|
|
82
|
+
eligible: boolean;
|
|
83
|
+
reason: FastDecisionReason;
|
|
84
|
+
} {
|
|
85
|
+
const defer = (reason: FastDecisionReason) => ({ eligible: false, reason });
|
|
86
|
+
|
|
87
|
+
if (input.requiresSynchronousReview) return defer("requires_synchronous_review");
|
|
88
|
+
if (input.tracker.latestFailedIndex > input.tracker.latestScoredIndex) return defer("scoring_failure");
|
|
89
|
+
|
|
90
|
+
const score = input.tracker.score;
|
|
91
|
+
if (!score) return defer("missing_score");
|
|
92
|
+
|
|
93
|
+
const lag = input.currentCallIndex - score.scoredAtCall;
|
|
94
|
+
if (lag > input.maxLag) return defer("stale_score");
|
|
95
|
+
if (score.authorizationVersion !== input.authorizationVersion) return defer("authorization_changed");
|
|
96
|
+
if (score.risk === "low") return { eligible: true, reason: "low_risk" };
|
|
97
|
+
return defer("elevated_risk");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type ClassificationResult =
|
|
101
|
+
| { kind: ClassifierRisk; model: string }
|
|
102
|
+
| { kind: "failed"; reason: string };
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Run one non-blocking classification. Output is a single token; anything else is a
|
|
106
|
+
* failed sample rather than a verdict, so a confused classifier costs a review and
|
|
107
|
+
* never an approval or a denial.
|
|
108
|
+
*/
|
|
109
|
+
export async function classifyTrajectory(params: {
|
|
110
|
+
registry: Pick<ModelRegistry, "find" | "complete">;
|
|
111
|
+
modelSpec: string;
|
|
112
|
+
prompt: string;
|
|
113
|
+
timeoutMs: number;
|
|
114
|
+
signal?: AbortSignal;
|
|
115
|
+
}): Promise<ClassificationResult> {
|
|
116
|
+
const parsed = parseModelSpec(params.modelSpec);
|
|
117
|
+
if (!parsed) {
|
|
118
|
+
return { kind: "failed", reason: `autoMode.classifier.model "${params.modelSpec}" is not provider/model-id.` };
|
|
119
|
+
}
|
|
120
|
+
const model = params.registry.find(parsed.provider, parsed.modelId) as Model<Api> | undefined;
|
|
121
|
+
if (!model) {
|
|
122
|
+
return { kind: "failed", reason: `Classifier model ${params.modelSpec} is not available.` };
|
|
123
|
+
}
|
|
124
|
+
const label = boundReviewText(`${model.provider}/${model.id}`, 256);
|
|
125
|
+
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const abortFromCaller = () => controller.abort();
|
|
128
|
+
if (params.signal?.aborted) controller.abort();
|
|
129
|
+
else params.signal?.addEventListener("abort", abortFromCaller);
|
|
130
|
+
const timer = setTimeout(() => controller.abort(), params.timeoutMs);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const response = await params.registry.complete(
|
|
134
|
+
model,
|
|
135
|
+
{
|
|
136
|
+
systemPrompt: CLASSIFIER_SYSTEM_PROMPT,
|
|
137
|
+
messages: [
|
|
138
|
+
{
|
|
139
|
+
role: "user",
|
|
140
|
+
content: [{ type: "text", text: params.prompt }],
|
|
141
|
+
timestamp: Date.now(),
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
{ signal: controller.signal, cacheRetention: "none" },
|
|
146
|
+
);
|
|
147
|
+
if (response.stopReason === "error" || response.stopReason === "aborted") {
|
|
148
|
+
return { kind: "failed", reason: response.errorMessage ?? `Classifier stopped: ${response.stopReason}` };
|
|
149
|
+
}
|
|
150
|
+
const text = response.content
|
|
151
|
+
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
|
152
|
+
.map((block) => block.text)
|
|
153
|
+
.join("")
|
|
154
|
+
.trim()
|
|
155
|
+
.toLowerCase();
|
|
156
|
+
if (/^high\b/.test(text)) return { kind: "high", model: label };
|
|
157
|
+
if (/^low\b/.test(text)) return { kind: "low", model: label };
|
|
158
|
+
return { kind: "failed", reason: `Classifier returned unreadable output: ${boundReviewText(text, 80)}` };
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return { kind: "failed", reason: error instanceof Error ? error.message : String(error) };
|
|
161
|
+
} finally {
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
params.signal?.removeEventListener("abort", abortFromCaller);
|
|
164
|
+
}
|
|
165
|
+
}
|