pi-openai-toolkit 0.8.0 → 0.9.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 +56 -13
- package/README_zh.md +56 -13
- package/extensions/auto-mode.ts +1 -0
- package/package.json +10 -3
- package/src/auto-mode/extension.ts +273 -0
- package/src/auto-mode/policy.ts +77 -0
- package/src/auto-mode/reviewer.ts +239 -0
- package/src/auto-mode/types.ts +58 -0
- package/src/config.ts +103 -16
- package/src/extension-runtime.ts +12 -4
- package/src/image-generation/eligibility.ts +7 -7
- package/src/image-generation/extension.ts +160 -160
- package/src/native-fallback.ts +19 -6
- package/src/types.ts +52 -10
- package/tsconfig.check.json +5 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pi-openai-toolkit
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<strong>OpenAI-focused toolkit for <a href="https://github.com/earendil-works/pi">Pi</a>: Remote Compaction v2, Hosted Web Search
|
|
4
|
+
<strong>OpenAI-focused toolkit for <a href="https://github.com/earendil-works/pi">Pi</a>: Remote Compaction v2, Hosted Web Search, Native Image Generation & Model-Reviewed Auto Mode</strong>
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
`pi-openai-toolkit` is an all-in-one OpenAI enhancement package for **Pi (>= 0.84.x)**. It bundles
|
|
13
|
+
`pi-openai-toolkit` is an all-in-one OpenAI enhancement package for **Pi (>= 0.84.x)**. It bundles four extensions designed for OpenAI Responses endpoints:
|
|
14
14
|
|
|
15
15
|
1. **Remote Responses Compaction v2** (`extensions/compaction.ts`): Server-side opaque context compaction using the `remote_compaction_v2` SSE streaming protocol. Preserves context fidelity without lossy text re-summarization.
|
|
16
16
|
2. **Model-Scoped Web Search** (`extensions/web-search.ts`): Toolkit-owned hosted Web Search for an exact `provider/model-id` allowlist configured with `webSearch.models`, across the supported `openai-responses` and `openai-codex-responses` APIs.
|
|
17
17
|
3. **Native Image Generation** (`extensions/image-generation.ts`): A sequential local tool backed by the hosted Responses `image_generation` tool. It supports text-to-image and explicit local-reference edits while keeping binary image data out of session history.
|
|
18
|
+
4. **Model-Reviewed Auto Mode** (`extensions/auto-mode.ts`): Pi does not prompt before running tools. Auto mode adds a gate on state-changing tool calls and lets a reviewer model you configure approve or deny each one, so the agent keeps working without interrupting you.
|
|
18
19
|
|
|
19
20
|
## Installation
|
|
20
21
|
|
|
@@ -61,6 +62,9 @@ pi --no-extensions -e /absolute/path/to/pi-openai-toolkit/extensions/web-search.
|
|
|
61
62
|
|
|
62
63
|
# Image generation only
|
|
63
64
|
pi --no-extensions -e /absolute/path/to/pi-openai-toolkit/extensions/image-generation.ts
|
|
65
|
+
|
|
66
|
+
# Auto mode only
|
|
67
|
+
pi --no-extensions -e /absolute/path/to/pi-openai-toolkit/extensions/auto-mode.ts
|
|
64
68
|
```
|
|
65
69
|
|
|
66
70
|
> ⚠️ **Conflict Warning**: Do not run `pi-remote-compact`, `@lll9p/pi-better-compaction`, or standalone `pi-openai-web-search` alongside `pi-openai-toolkit`. Duplicate hooks cause race conditions and duplicated system prompts.
|
|
@@ -89,8 +93,11 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
|
|
|
89
93
|
"enabled": true,
|
|
90
94
|
"allowCompactionContinuityBreak": false,
|
|
91
95
|
"remoteCompactModel": "uwoacrimson/gpt-5.6-luna",
|
|
92
|
-
"
|
|
93
|
-
|
|
96
|
+
"nativeFallback": {
|
|
97
|
+
"enabled": true,
|
|
98
|
+
"model": null,
|
|
99
|
+
"thinkingLevel": "off"
|
|
100
|
+
},
|
|
94
101
|
"autoCompaction": {
|
|
95
102
|
"enabled": true,
|
|
96
103
|
"continuation": "inline",
|
|
@@ -114,10 +121,19 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
|
|
|
114
121
|
]
|
|
115
122
|
},
|
|
116
123
|
"imageGeneration": {
|
|
124
|
+
"enabled": false
|
|
125
|
+
},
|
|
126
|
+
"autoMode": {
|
|
117
127
|
"enabled": true,
|
|
118
128
|
"models": [
|
|
119
129
|
"uwoacrimson/gpt-5.6-luna"
|
|
120
|
-
]
|
|
130
|
+
],
|
|
131
|
+
"reviewerModel": "uwoacrimson/gpt-5.6-sol",
|
|
132
|
+
"gate": "side-effect",
|
|
133
|
+
"extraTools": [
|
|
134
|
+
"openai_generate_image"
|
|
135
|
+
],
|
|
136
|
+
"timeoutMs": 30000
|
|
121
137
|
}
|
|
122
138
|
}
|
|
123
139
|
```
|
|
@@ -129,8 +145,9 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
|
|
|
129
145
|
- `enabled` (*boolean*, default: `true`): Enable or disable Remote Compaction.
|
|
130
146
|
- `allowCompactionContinuityBreak` (*boolean*, default: `false`): When `true`, allows restarting a fresh native opaque compaction chain from Pi's current session text if the latest compaction was made by another strategy (e.g. text summary).
|
|
131
147
|
- `remoteCompactModel` (*string | null*, default: `null`): Model formatted as `provider/model-id` used only for the synthetic `remote_compaction_v2` request. Pi's active session model is not switched and resumes normal requests after compaction. The override must resolve to the same effective base URL as the active model; provider, Responses API identifier, and model may differ. `null` preserves the current same-model path.
|
|
132
|
-
- `
|
|
133
|
-
- `
|
|
148
|
+
- `nativeFallback.enabled` (*boolean*, default: `true`): Master switch for toolkit-driven native compaction with an explicit summary model. It never touches the remote chain: `remote_compaction_v2` is still attempted and still wins when it succeeds. When `false`, only the post-failure native step is skipped, so every fallback falls straight through to Pi's own default compaction with the active model.
|
|
149
|
+
- `nativeFallback.model` (*string | null*, default: `null`): Summary model formatted as `provider/model-id` (e.g. `"openai/gpt-4o-mini"`) used **only** when the active model cannot use remote v2 at all (non-Responses API, or an API excluded by `responsesApis`). It is never consulted once a remote request was attempted, and `null` means the active model runs Pi's default compaction path.
|
|
150
|
+
- `nativeFallback.thinkingLevel` (*string*, default: `"off"`): Reasoning / thinking level passed to Pi's native `compact()` whenever this extension supplies the model (`"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`).
|
|
134
151
|
- `autoCompaction.enabled` (*boolean*, default: `true`): Check for compaction after a completed tool-result batch crosses Pi's context threshold.
|
|
135
152
|
- `autoCompaction.continuation` (*`"inline" | "followUp" | "off"`*, default: `"inline"`): `inline` compacts inside the current official Pi 0.84.3 tool loop and refreshes the next provider context; `followUp` uses Pi's public hidden follow-up message path; `off` disables toolkit-triggered automatic compaction.
|
|
136
153
|
- `autoCompaction.unsupportedFallback` (*`"followUp" | "off"`*, default: `"followUp"`): Behavior when the guarded official-host inline adapter is unavailable. It does not change normal remote/native compaction fallback ordering.
|
|
@@ -149,10 +166,22 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
|
|
|
149
166
|
|
|
150
167
|
#### `imageGeneration`
|
|
151
168
|
|
|
152
|
-
- `enabled` (*boolean*, default: `
|
|
153
|
-
- `models` (*string[]*, default: `[]`): Exact `provider/model-id` allowlist. The tool is active only for listed models using `openai-responses` or `openai-codex-responses`. The allowlist is independent from Web Search and compaction.
|
|
169
|
+
- `enabled` (*boolean*, default: `false`): The single opt-in switch for the `openai_generate_image` local tool. It defaults to off because every successful provider request may incur a charge.
|
|
154
170
|
|
|
155
|
-
|
|
171
|
+
There is no model allowlist: the hosted `image_generation` tool is a capability of the Responses API, not of one vendor's model. Setting `enabled: true` exposes the tool to every active model whose API is `openai-responses` or `openai-codex-responses`, and hides it everywhere else. Web Search and compaction keep their own independent model allowlists.
|
|
172
|
+
|
|
173
|
+
The request uses the current model's resolved Responses endpoint, authentication, and headers, while the hosted image tool itself is fixed to `gpt-image-2`; Pi does not switch the active conversation model. A gateway that does not support the hosted image tool still registers the tool and returns a bounded failure at call time; nothing is retried automatically.
|
|
174
|
+
|
|
175
|
+
#### `autoMode`
|
|
176
|
+
|
|
177
|
+
- `enabled` (*boolean*, default: `true`): Master switch. With `true` the mode can still never engage until you allowlist a model and set a reviewer.
|
|
178
|
+
- `models` (*string[]*, default: `[]`): Exact `provider/model-id` allowlist of **active session models** permitted to run in auto mode. An empty list disables auto mode everywhere.
|
|
179
|
+
- `reviewerModel` (*string | null*, default: `null`): The approving model, formatted as `provider/model-id`. It is resolved through Pi's model registry and must exist there; it is used only for one-off review requests and never becomes the active conversation model. Auto mode cannot engage while this is unset.
|
|
180
|
+
- `gate` (*`"side-effect" | "all"`*, default: `"side-effect"`): `side-effect` reviews `bash`, `write`, and `edit`; `all` reviews every tool call, including read-only tools.
|
|
181
|
+
- `extraTools` (*string[]*, default: `[]`): Additional tool names reviewed on top of the `side-effect` default set, for example `openai_generate_image` or an MCP tool name.
|
|
182
|
+
- `timeoutMs` (*integer*, default: `30000`, range `1000`-`120000`): Reviewer deadline. Expiry never becomes approval.
|
|
183
|
+
|
|
184
|
+
Auto mode is opt-in through the empty-by-default `models` allowlist because it authorizes a model to approve writes and shell commands on your behalf. Enable it for a session with `pi --auto` or `/auto on`, and use `/auto all` to review every tool at once. See [Core Mechanics](#core-mechanics) section 5 for the exact decision and fallback rules.
|
|
156
185
|
|
|
157
186
|
---
|
|
158
187
|
|
|
@@ -183,8 +212,8 @@ Content-Type: application/json
|
|
|
183
212
|
- **Fail-Closed Replay Guard**: Replay anchors on the Pi-authored compaction summary item by content and replaces it with the opaque compacted window; the post-compaction context is preserved verbatim, so transient messages injected by other extensions no longer block replay. If the summary anchor cannot be found, the toolkit aborts the provider request, shows an error notification, and always writes a content-free `*-replay-failure.json` artifact even when `logProviderPayloads` is disabled. The sentinel-only payload is never sent to the model.
|
|
184
213
|
- **Graceful Multi-Tier Fallback**:
|
|
185
214
|
1. Compatible Responses endpoints → Remote v2 Opaque Compaction.
|
|
186
|
-
2.
|
|
187
|
-
3.
|
|
215
|
+
2. Remote request attempted but failed on a model that *can* compact remotely → Pi's native `compact()` with `remoteCompactModel`, or the active model when no override is configured;
|
|
216
|
+
3. Model that cannot use remote v2 at all → Pi's native `compact()` with `compaction.nativeFallback.model`, or the active model when unset;
|
|
188
217
|
|
|
189
218
|
### 2. Tool-Loop Auto-Compaction and Continuation
|
|
190
219
|
|
|
@@ -207,7 +236,7 @@ For allowlisted models using `openai-responses` or `openai-codex-responses`, the
|
|
|
207
236
|
|
|
208
237
|
### 4. Native Image Generation and Editing
|
|
209
238
|
|
|
210
|
-
|
|
239
|
+
When `imageGeneration.enabled` is `true` and the active model speaks a Responses API, Pi exposes the sequential `openai_generate_image` tool:
|
|
211
240
|
|
|
212
241
|
- **Isolated request**: Sends one non-streaming, `store: false` Responses request containing only the image prompt and explicitly approved references. It does not forward recent conversation history and does not automatically retry after dispatch, avoiding accidental duplicate charges.
|
|
213
242
|
- **Generation and editing**: Supports text-to-image or editing with 1-5 explicitly supplied local PNG, JPEG, or WebP reference paths. Reference upload always requires interactive confirmation; headless sessions reject reference uploads.
|
|
@@ -218,6 +247,20 @@ For an allowlisted Responses model, Pi exposes the sequential `openai_generate_i
|
|
|
218
247
|
- **Headless behavior**: Text-to-image with the default artifact path remains available without a UI. Reference uploads and external output paths that require approval are rejected in headless sessions.
|
|
219
248
|
- **Billing and verification boundary**: The configured gateway/provider remains the billing authority; the tool does not synthesize Pi usage or cost metadata. Deterministic tests and smoke checks never dispatch image generation, and no live paid probe runs without separate explicit approval.
|
|
220
249
|
|
|
250
|
+
### 5. Model-Reviewed Auto Mode
|
|
251
|
+
|
|
252
|
+
Pi does not prompt before running `bash`, `write`, or `edit`; the only built-in trust prompt concerns project-local settings. This extension adds the approval gate and lets a reviewer model operate it, mirroring Codex's `approval_policy` plus `approvals_reviewer` pairing.
|
|
253
|
+
|
|
254
|
+
- **Engagement is non-blocking**: `pi --auto`, `/auto on`, and `/auto all` only flip in-session state and update the footer status. No provider request happens at engagement time, so turning the mode on can never stall the current turn.
|
|
255
|
+
- **Gate scope**: `side-effect` reviews `bash`, `write`, and `edit`; `extraTools` adds named tools; `/auto all` (or `gate: "all"`) reviews every tool call. Read-only tools pass through untouched in the default scope.
|
|
256
|
+
- **One review per gated call**: The reviewer receives the tool name, its arguments, the most recent user request as intent, and the working directory. Tool arguments are labelled untrusted data, so text inside them cannot instruct the reviewer.
|
|
257
|
+
- **Strict verdict contract**: The reviewer must answer with a single `{"decision":"allow"|"deny","reason":"..."}` object. Anything unparseable, an unknown decision, a provider error, a timeout, or a cancellation is reported as *unavailable* and never treated as approval.
|
|
258
|
+
- **Deny feeds the agent**: A denial blocks the tool and returns the reviewer's reason to the requesting model, which can then choose a different approach instead of stalling.
|
|
259
|
+
- **Human fallback**: When a review is unavailable in an interactive session, the extension asks you directly with the failure reason. In print (`-p`) and JSON mode there is nobody to ask, so the action fails closed and is blocked.
|
|
260
|
+
- **Eligibility is rechecked live**: Switching to a model outside `autoMode.models`, clearing `reviewerModel`, or setting `enabled: false` disengages immediately. Returning to an allowlisted model does not re-engage automatically; you must ask again with `/auto on`.
|
|
261
|
+
- **Bounded audit trail**: Every decision is persisted as a custom session entry (`pi-openai-toolkit.auto-mode-decision.v1`) containing the tool name, call id, decision, bounded reason, reviewer model, and decision source. Custom entries never enter LLM context, and tool arguments are not recorded.
|
|
262
|
+
- **Cost boundary**: Each gated call adds one reviewer completion, so auto mode consumes model tokens. The reviewer runs with prompt caching disabled and no tools, and it cannot execute anything itself.
|
|
263
|
+
|
|
221
264
|
---
|
|
222
265
|
|
|
223
266
|
## Development & Testing
|
package/README_zh.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pi-openai-toolkit
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<strong>专为 <a href="https://github.com/earendil-works/pi">Pi</a> 打造的 OpenAI 增强工具包:远端无损压缩 v2
|
|
4
|
+
<strong>专为 <a href="https://github.com/earendil-works/pi">Pi</a> 打造的 OpenAI 增强工具包:远端无损压缩 v2、原生托管联网搜索、图像生成与模型审批 Auto 模式</strong>
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
`pi-openai-toolkit` 是专为 **Pi (>= 0.84.x)** 打造的 OpenAI
|
|
13
|
+
`pi-openai-toolkit` 是专为 **Pi (>= 0.84.x)** 打造的 OpenAI 增强工具包,集成了四项针对 OpenAI Responses 协议的扩展:
|
|
14
14
|
|
|
15
15
|
1. **远端无损压缩 (Remote Compaction v2)** (`extensions/compaction.ts`): 基于 `remote_compaction_v2` SSE 流式协议实现的服务端密文无损会话压缩,彻底告别会话变长后丢失细节的文本二次摘要。
|
|
16
16
|
2. **按模型启用的联网搜索** (`extensions/web-search.ts`): 通过 `webSearch.models` 精确匹配 `provider/model-id`,由 Toolkit 为符合条件的 `openai-responses` 与 `openai-codex-responses` 会话提供并拥有原生联网搜索。
|
|
17
17
|
3. **原生图像生成** (`extensions/image-generation.ts`): 注册由 Responses 托管 `image_generation` 工具驱动的本地顺序工具,支持文生图和显式本地参考图编辑,同时避免把图像二进制写入会话历史。
|
|
18
|
+
4. **模型审批 Auto 模式** (`extensions/auto-mode.ts`): Pi 默认不会为工具调用弹出审批确认。Auto 模式为改变状态的工具调用加上一道审批门,由你指定的审批模型判定放行或拦截,让 Agent 不必停下来等你确认也能继续推进。
|
|
18
19
|
|
|
19
20
|
## 安装使用
|
|
20
21
|
|
|
@@ -61,6 +62,9 @@ pi --no-extensions -e /本地绝对路径/pi-openai-toolkit/extensions/web-searc
|
|
|
61
62
|
|
|
62
63
|
# 仅加载原生图像生成
|
|
63
64
|
pi --no-extensions -e /本地绝对路径/pi-openai-toolkit/extensions/image-generation.ts
|
|
65
|
+
|
|
66
|
+
# 仅加载 Auto 模式
|
|
67
|
+
pi --no-extensions -e /本地绝对路径/pi-openai-toolkit/extensions/auto-mode.ts
|
|
64
68
|
```
|
|
65
69
|
|
|
66
70
|
> ⚠️ **冲突提示**:请勿同时启用 `pi-remote-compact`、`@lll9p/pi-better-compaction` 或独立的 `pi-openai-web-search`。多重钩子会产生竞态或注入重复的 System Prompt。
|
|
@@ -89,8 +93,11 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
|
|
|
89
93
|
"enabled": true,
|
|
90
94
|
"allowCompactionContinuityBreak": false,
|
|
91
95
|
"remoteCompactModel": "uwoacrimson/gpt-5.6-luna",
|
|
92
|
-
"
|
|
93
|
-
|
|
96
|
+
"nativeFallback": {
|
|
97
|
+
"enabled": true,
|
|
98
|
+
"model": null,
|
|
99
|
+
"thinkingLevel": "off"
|
|
100
|
+
},
|
|
94
101
|
"autoCompaction": {
|
|
95
102
|
"enabled": true,
|
|
96
103
|
"continuation": "inline",
|
|
@@ -114,10 +121,19 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
|
|
|
114
121
|
]
|
|
115
122
|
},
|
|
116
123
|
"imageGeneration": {
|
|
124
|
+
"enabled": false
|
|
125
|
+
},
|
|
126
|
+
"autoMode": {
|
|
117
127
|
"enabled": true,
|
|
118
128
|
"models": [
|
|
119
129
|
"uwoacrimson/gpt-5.6-luna"
|
|
120
|
-
]
|
|
130
|
+
],
|
|
131
|
+
"reviewerModel": "uwoacrimson/gpt-5.6-sol",
|
|
132
|
+
"gate": "side-effect",
|
|
133
|
+
"extraTools": [
|
|
134
|
+
"openai_generate_image"
|
|
135
|
+
],
|
|
136
|
+
"timeoutMs": 30000
|
|
121
137
|
}
|
|
122
138
|
}
|
|
123
139
|
```
|
|
@@ -129,8 +145,9 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
|
|
|
129
145
|
- `enabled` (*boolean*, 默认 `true`): 是否启用远端无损压缩。
|
|
130
146
|
- `allowCompactionContinuityBreak` (*boolean*, 默认 `false`): 若最近一次历史压缩由其他方式(如文本摘要)生成,设为 `true` 允许直接从当前有效文本上下文重建密文压缩链。
|
|
131
147
|
- `remoteCompactModel` (*string | null*, 默认 `null`): 仅用于 synthetic `remote_compaction_v2` 请求的模型,格式为 `"provider/model-id"`。Pi 不会切换当前会话模型,压缩完成后仍由原模型继续正常请求。覆盖模型必须与当前模型使用相同的实际生效 Base URL;Provider、Responses API 标识符和模型本身可以不同。为 `null` 时保持当前模型执行远端压缩的原有行为。
|
|
132
|
-
- `
|
|
133
|
-
- `
|
|
148
|
+
- `nativeFallback.enabled` (*boolean*, 默认 `true`): Toolkit 用显式摘要模型跑原生压缩的总开关。它不影响远端链路:`remote_compaction_v2` 照常被尝试,成功时照常生效。设为 `false` 只跳过失败后的那一步原生压缩,因此所有降级都直接交给 Pi 默认的当前模型压缩。
|
|
149
|
+
- `nativeFallback.model` (*string | null*, 默认 `null`): 摘要模型,格式为 `"provider/model-id"`(例如 `"openai/gpt-4o-mini"`),**仅**用于根本无法走远端 v2 的当前模型(非 Responses API,或 API 被 `responsesApis` 排除)。远端请求已经发出过的情境不会读这个字段;为 `null` 时由当前活动模型走 Pi 默认压缩路径。
|
|
150
|
+
- `nativeFallback.thinkingLevel` (*string*, 默认 `"off"`): 由本扩展指定模型时,传给 Pi 原生 `compact()` 的思考级别。
|
|
134
151
|
- `autoCompaction.enabled` (*boolean*, 默认 `true`): 完整工具调用/结果批次结束并越过 Pi 上下文阈值后,检查是否需要压缩。
|
|
135
152
|
- `autoCompaction.continuation` (*`"inline" | "followUp" | "off"`*, 默认 `"inline"`): `inline` 在官方 Pi 0.84.3 当前工具循环内完成压缩并刷新下一次 Provider 上下文;`followUp` 使用 Pi 公共的隐藏后续消息;`off` 关闭 Toolkit 触发的自动压缩。
|
|
136
153
|
- `autoCompaction.unsupportedFallback` (*`"followUp" | "off"`*, 默认 `"followUp"`): 官方 Host 私有 inline 适配器不可用时的处理方式,不改变原有远端/原生压缩降级顺序。
|
|
@@ -149,10 +166,22 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
|
|
|
149
166
|
|
|
150
167
|
#### `imageGeneration`(图像生成配置)
|
|
151
168
|
|
|
152
|
-
- `enabled` (*boolean*, 默认 `
|
|
153
|
-
- `models` (*string[]*, 默认 `[]`): 精确的 `provider/model-id` 白名单。只有列表中的模型使用 `openai-responses` 或 `openai-codex-responses` 时才会激活工具;它与联网搜索、压缩配置相互独立。
|
|
169
|
+
- `enabled` (*boolean*, 默认 `false`): 本地工具 `openai_generate_image` 的唯一开关。默认关闭是因为每一次成功的 Provider 请求都可能产生费用。
|
|
154
170
|
|
|
155
|
-
|
|
171
|
+
这里没有模型白名单:托管 `image_generation` 是 Responses API 的能力,不属于某个厂商的特定模型。`enabled: true` 后,所有 API 为 `openai-responses` 或 `openai-codex-responses` 的当前模型都会拿到这个工具,其他 API 则完全看不到。联网搜索和压缩仍保留各自的模型白名单。
|
|
172
|
+
|
|
173
|
+
请求复用当前模型解析后的 Responses 端点、认证和请求头,托管图像工具固定使用 `gpt-image-2`;Pi 不会切换当前会话模型。不支持托管图像工具的网关仍会注册该工具,并在调用时返回有界错误;发送后不会自动重试。
|
|
174
|
+
|
|
175
|
+
#### `autoMode`(Auto 模式配置)
|
|
176
|
+
|
|
177
|
+
- `enabled` (*boolean*, 默认 `true`): 总开关。即使为 `true`,在未配置模型白名单和审批模型之前,Auto 模式仍然无法启用。
|
|
178
|
+
- `models` (*string[]*, 默认 `[]`): 允许以 Auto 模式运行的**当前会话模型**精确 `provider/model-id` 白名单。空列表表示任何模型都无法使用 Auto 模式。
|
|
179
|
+
- `reviewerModel` (*string | null*, 默认 `null`): 执行审批的模型,格式为 `"provider/model-id"`。它通过 Pi 的模型注册表解析且必须存在,仅用于一次性审批请求,不会成为当前会话模型。未设置时 Auto 模式无法启用。
|
|
180
|
+
- `gate` (*`"side-effect" | "all"`*, 默认 `"side-effect"`): `side-effect` 只审批 `bash`、`write`、`edit`;`all` 审批包括只读工具在内的全部工具调用。
|
|
181
|
+
- `extraTools` (*string[]*, 默认 `[]`): 在 `side-effect` 默认集之上额外审批的工具名,例如 `openai_generate_image` 或某个 MCP 工具名。
|
|
182
|
+
- `timeoutMs` (*整数*, 默认 `30000`,范围 `1000`–`120000`): 审批超时时间。超时永远不会被当作放行。
|
|
183
|
+
|
|
184
|
+
Auto 模式需要显式启用(`models` 默认为空),因为它相当于允许一个模型代你批准文件写入和命令执行。会话内可用 `pi --auto` 或 `/auto on` 开启,`/auto all` 一次审批全部工具。具体判定与降级规则见「核心工作原理解析」第 5 节。
|
|
156
185
|
|
|
157
186
|
---
|
|
158
187
|
|
|
@@ -183,8 +212,8 @@ Content-Type: application/json
|
|
|
183
212
|
- **重放失败保护(Fail-Closed)**:重放以 Pi 生成的 compaction summary 哨兵为内容锚点,将其替换为密文压缩窗口;压缩后的其余上下文一律原样保留,因此其他扩展注入的临时消息不再阻断重放。若找不到哨兵锚点,插件会中止 provider 请求、弹出错误通知,并且即使 `logProviderPayloads` 关闭也会写入一份不含内容的安全失败记录 `*-replay-failure.json`。只含哨兵占位符的请求体永远不会发送给模型。
|
|
184
213
|
- **三级优雅降级**:
|
|
185
214
|
1. 支持的 Responses 端点 → 触发 Remote v2 密文无损压缩;
|
|
186
|
-
2.
|
|
187
|
-
3.
|
|
215
|
+
2. 本可以远端压缩但网关报错或失败 → 用 `remoteCompactModel` 跑 Pi 原生 `compact()`,未配置覆盖模型则由当前活动模型执行;
|
|
216
|
+
3. 根本无法走远端的模型 → 用 `compaction.nativeFallback.model` 跑 Pi 原生 `compact()`,未配置则由当前活动模型执行;
|
|
188
217
|
|
|
189
218
|
### 2. 工具循环内自动压缩与续跑
|
|
190
219
|
|
|
@@ -207,7 +236,7 @@ Content-Type: application/json
|
|
|
207
236
|
|
|
208
237
|
### 4. 原生图像生成与编辑
|
|
209
238
|
|
|
210
|
-
|
|
239
|
+
当 `imageGeneration.enabled` 为 `true` 且当前模型走 Responses API 时,Pi 会启用顺序工具 `openai_generate_image`:
|
|
211
240
|
|
|
212
241
|
- **隔离请求**:仅发送一次非流式、`store: false` 的 Responses 请求,请求体只包含图像提示词和用户显式批准的参考图,不会附带最近会话历史;发送后不自动重试,避免重复计费。
|
|
213
242
|
- **生成与编辑**:支持文生图,或使用 1–5 个显式指定的本地 PNG、JPEG、WebP 文件编辑。上传参考图前必须经过交互确认;Headless 会话拒绝上传参考图。
|
|
@@ -218,6 +247,20 @@ Content-Type: application/json
|
|
|
218
247
|
- **Headless 行为**:不带参考图并使用默认 Artifact 路径的文生图可在无 UI 环境运行;需要确认的参考图上传和外部输出路径会在 Headless 会话中被拒绝。
|
|
219
248
|
- **计费与验证边界**:实际计费以配置的 Gateway/Provider 为准,工具不会自行构造 Pi usage/cost 元数据。确定性测试和冒烟检查不会发起生图请求;未经单独明确批准,也不会执行真实付费探针。
|
|
220
249
|
|
|
250
|
+
### 5. 模型审批的 Auto 模式
|
|
251
|
+
|
|
252
|
+
Pi 本身在运行 `bash`、`write`、`edit` 前不会询问确认,内置的唯一 trust 提示只针对项目本地配置。本扩展补上这道审批门,并让审批模型来操作它,对应 Codex 的 `approval_policy` 加 `approvals_reviewer` 组合。
|
|
253
|
+
|
|
254
|
+
- **开启动作不阻塞任务流**:`pi --auto`、`/auto on`、`/auto all` 只切换会话内状态并更新底部状态栏,开启时不会发起任何 Provider 请求。
|
|
255
|
+
- **审批范围**:`side-effect` 审批 `bash`、`write`、`edit`;`extraTools` 追加工具名;`/auto all`(或 `gate: "all"`)审批全部工具调用。默认范围下只读工具直接放行。
|
|
256
|
+
- **每个受审调用一次审批**:审批模型收到工具名、完整参数、最近一次用户请求作为意图、以及工作目录。工具参数被标记为不可信数据,其中的文本无法反过来指挥审批模型。
|
|
257
|
+
- **严格结论契约**:审批模型必须只回答一个 `{"decision":"allow"|"deny","reason":"..."}` 对象。无法解析、未知结论、Provider 报错、超时或取消都记为 *unavailable*,绝不视为放行。
|
|
258
|
+
- **拒绝回喂主模型**:deny 会拦截该工具并把理由返回给发起调用的模型,让它改走其他方案而不是卡死。
|
|
259
|
+
- **降级人工确认**:交互会话中审批不可用时,扩展会带着失败原因直接问你;而 `-p` 与 JSON 模式无人可问,因此 fail-closed 直接拦截。
|
|
260
|
+
- **实时重新校验资格**:切到 `autoMode.models` 之外的模型、清空 `reviewerModel` 或设 `enabled: false` 会立即退出 Auto 模式;切回白名单模型不会自动恢复,必须重新 `/auto on`。
|
|
261
|
+
- **有界审计记录**:每次判定都以自定义会话条目(`pi-openai-toolkit.auto-mode-decision.v1`)落盘,包含工具名、调用 id、结论、有界理由、审批模型和判定来源。自定义条目不进入 LLM 上下文,且不记录工具参数。
|
|
262
|
+
- **费用边界**:每个受审调用额外产生一次审批补全,因此 Auto 模式会消耗模型 Token。审批请求禁用提示缓存、不携带任何工具,且自身无法执行操作。
|
|
263
|
+
|
|
221
264
|
---
|
|
222
265
|
|
|
223
266
|
## 测试与验证
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "../src/auto-mode/extension.ts";
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-openai-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "OpenAI-focused Pi toolkit providing remote Responses compaction, native Web Search,
|
|
6
|
+
"description": "OpenAI-focused Pi toolkit providing remote Responses compaction, native Web Search, native image generation, and model-reviewed auto mode.",
|
|
7
7
|
"author": "awoaCrim and contributors",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"compaction",
|
|
26
26
|
"web-search",
|
|
27
27
|
"image-generation",
|
|
28
|
+
"auto-mode",
|
|
28
29
|
"codex",
|
|
29
30
|
"responses"
|
|
30
31
|
],
|
|
@@ -32,13 +33,15 @@
|
|
|
32
33
|
"extensions": [
|
|
33
34
|
"./extensions/compaction.ts",
|
|
34
35
|
"./extensions/web-search.ts",
|
|
35
|
-
"./extensions/image-generation.ts"
|
|
36
|
+
"./extensions/image-generation.ts",
|
|
37
|
+
"./extensions/auto-mode.ts"
|
|
36
38
|
]
|
|
37
39
|
},
|
|
38
40
|
"files": [
|
|
39
41
|
"extensions/compaction.ts",
|
|
40
42
|
"extensions/web-search.ts",
|
|
41
43
|
"extensions/image-generation.ts",
|
|
44
|
+
"extensions/auto-mode.ts",
|
|
42
45
|
"src/compact-client.ts",
|
|
43
46
|
"src/config.ts",
|
|
44
47
|
"src/debug.ts",
|
|
@@ -55,6 +58,10 @@
|
|
|
55
58
|
"src/image-generation/render.ts",
|
|
56
59
|
"src/image-generation/service.ts",
|
|
57
60
|
"src/image-generation/types.ts",
|
|
61
|
+
"src/auto-mode/extension.ts",
|
|
62
|
+
"src/auto-mode/policy.ts",
|
|
63
|
+
"src/auto-mode/reviewer.ts",
|
|
64
|
+
"src/auto-mode/types.ts",
|
|
58
65
|
"src/model-scope.ts",
|
|
59
66
|
"src/native-fallback.ts",
|
|
60
67
|
"src/payload-rewrite.ts",
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { loadToolkitConfig } from "../config";
|
|
3
|
+
import { DEFAULT_AUTO_MODE_CONFIG, type AutoModeConfig } from "../types";
|
|
4
|
+
import {
|
|
5
|
+
applyConfiguredGate,
|
|
6
|
+
createRuntimeState,
|
|
7
|
+
describeGate,
|
|
8
|
+
isAutoModeEligible,
|
|
9
|
+
setGateOverride,
|
|
10
|
+
shouldReviewTool,
|
|
11
|
+
type AutoModeRuntimeState,
|
|
12
|
+
} from "./policy";
|
|
13
|
+
import { requestToolReview } from "./reviewer";
|
|
14
|
+
import {
|
|
15
|
+
AUTO_MODE_COMMAND,
|
|
16
|
+
AUTO_MODE_ENTRY_TYPE,
|
|
17
|
+
AUTO_MODE_FLAG,
|
|
18
|
+
AUTO_MODE_STATUS_KEY,
|
|
19
|
+
boundReviewText,
|
|
20
|
+
MAX_REVIEW_INTENT_CHARS,
|
|
21
|
+
MAX_REVIEW_REASON_CHARS,
|
|
22
|
+
type AutoModeDecisionRecord,
|
|
23
|
+
type ReviewOutcome,
|
|
24
|
+
} from "./types";
|
|
25
|
+
|
|
26
|
+
const registeredApis = new WeakSet<object>();
|
|
27
|
+
|
|
28
|
+
type GateDecision =
|
|
29
|
+
| { kind: "pass" }
|
|
30
|
+
| { kind: "block"; reason: string }
|
|
31
|
+
| { kind: "ask-human"; reason: string };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Auto mode replaces the human approval prompt with a reviewer model. Enabling it
|
|
35
|
+
* is always a synchronous state flip: no provider call happens until a gated tool
|
|
36
|
+
* is about to run, so turning the mode on can never stall the current turn.
|
|
37
|
+
*/
|
|
38
|
+
export function registerAutoModeExtension(
|
|
39
|
+
pi: ExtensionAPI,
|
|
40
|
+
loadConfig: typeof loadToolkitConfig = loadToolkitConfig,
|
|
41
|
+
requestReview: typeof requestToolReview = requestToolReview,
|
|
42
|
+
): void {
|
|
43
|
+
if (registeredApis.has(pi)) return;
|
|
44
|
+
registeredApis.add(pi);
|
|
45
|
+
|
|
46
|
+
const state: AutoModeRuntimeState = createRuntimeState(DEFAULT_AUTO_MODE_CONFIG);
|
|
47
|
+
let lastIntent: string | undefined;
|
|
48
|
+
|
|
49
|
+
function updateStatus(ctx: ExtensionContext): void {
|
|
50
|
+
if (!ctx.hasUI) return;
|
|
51
|
+
ctx.ui.setStatus(
|
|
52
|
+
AUTO_MODE_STATUS_KEY,
|
|
53
|
+
state.engaged ? `auto-review: ${describeGate(state)}` : undefined,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function recordDecision(data: Omit<AutoModeDecisionRecord, "timestamp">): void {
|
|
58
|
+
pi.appendEntry(AUTO_MODE_ENTRY_TYPE, { timestamp: Date.now(), ...data } satisfies AutoModeDecisionRecord);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function engage(ctx: ExtensionContext, config: AutoModeConfig): boolean {
|
|
62
|
+
if (!isAutoModeEligible(ctx.model, config)) {
|
|
63
|
+
if (ctx.hasUI) {
|
|
64
|
+
ctx.ui.notify(
|
|
65
|
+
"Auto mode needs an allowlisted model in autoMode.models and a configured autoMode.reviewerModel.",
|
|
66
|
+
"warning",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
state.engaged = true;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function evaluateOutcome(
|
|
76
|
+
outcome: ReviewOutcome,
|
|
77
|
+
pending: { toolName: string; toolCallId: string; reviewerModelSpec: string },
|
|
78
|
+
ctx: ExtensionContext,
|
|
79
|
+
): GateDecision {
|
|
80
|
+
const reviewerModel = outcome.kind === "unavailable" ? pending.reviewerModelSpec : outcome.reviewerModel;
|
|
81
|
+
|
|
82
|
+
if (outcome.kind === "allow") {
|
|
83
|
+
recordDecision({
|
|
84
|
+
toolName: pending.toolName,
|
|
85
|
+
toolCallId: pending.toolCallId,
|
|
86
|
+
decision: "allow",
|
|
87
|
+
reason: boundReviewText(outcome.reason, MAX_REVIEW_REASON_CHARS),
|
|
88
|
+
reviewerModel,
|
|
89
|
+
source: "reviewer",
|
|
90
|
+
});
|
|
91
|
+
return { kind: "pass" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (outcome.kind === "deny") {
|
|
95
|
+
const reason = boundReviewText(outcome.reason, MAX_REVIEW_REASON_CHARS);
|
|
96
|
+
recordDecision({
|
|
97
|
+
toolName: pending.toolName,
|
|
98
|
+
toolCallId: pending.toolCallId,
|
|
99
|
+
decision: "deny",
|
|
100
|
+
reason,
|
|
101
|
+
reviewerModel,
|
|
102
|
+
source: "reviewer",
|
|
103
|
+
});
|
|
104
|
+
return { kind: "block", reason: `Auto-mode reviewer denied this action: ${reason}` };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const reason = boundReviewText(outcome.reason, MAX_REVIEW_REASON_CHARS);
|
|
108
|
+
|
|
109
|
+
if (ctx.signal?.aborted) {
|
|
110
|
+
recordDecision({
|
|
111
|
+
toolName: pending.toolName,
|
|
112
|
+
toolCallId: pending.toolCallId,
|
|
113
|
+
decision: "unavailable-blocked",
|
|
114
|
+
reason,
|
|
115
|
+
reviewerModel,
|
|
116
|
+
source: "policy",
|
|
117
|
+
});
|
|
118
|
+
return { kind: "block", reason: `Auto-mode review was cancelled, so the action did not run: ${reason}` };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fail closed without an interactive surface: print and JSON modes cannot ask.
|
|
122
|
+
if (!ctx.hasUI) {
|
|
123
|
+
recordDecision({
|
|
124
|
+
toolName: pending.toolName,
|
|
125
|
+
toolCallId: pending.toolCallId,
|
|
126
|
+
decision: "unavailable-blocked",
|
|
127
|
+
reason,
|
|
128
|
+
reviewerModel,
|
|
129
|
+
source: "policy",
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
kind: "block",
|
|
133
|
+
reason: `Auto-mode review is unavailable and no interactive UI can confirm, so the action was blocked: ${reason}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { kind: "ask-human", reason };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
pi.registerFlag(AUTO_MODE_FLAG, {
|
|
141
|
+
description: "Start with auto mode: a reviewer model approves gated tool calls instead of you",
|
|
142
|
+
type: "boolean",
|
|
143
|
+
default: false,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
pi.registerCommand(AUTO_MODE_COMMAND, {
|
|
147
|
+
description: "Model-reviewed tool approval: /auto [on|off|status|all|side]",
|
|
148
|
+
handler: async (args, ctx) => {
|
|
149
|
+
const { config } = loadConfig();
|
|
150
|
+
const auto = config.autoMode;
|
|
151
|
+
applyConfiguredGate(state, auto);
|
|
152
|
+
const sub = args.trim().toLowerCase();
|
|
153
|
+
|
|
154
|
+
switch (sub) {
|
|
155
|
+
case "on":
|
|
156
|
+
if (engage(ctx, auto)) ctx.ui.notify(`Auto mode on. Reviewing: ${describeGate(state)}`, "info");
|
|
157
|
+
break;
|
|
158
|
+
case "off":
|
|
159
|
+
state.engaged = false;
|
|
160
|
+
setGateOverride(state, auto, undefined);
|
|
161
|
+
ctx.ui.notify("Auto mode off.", "info");
|
|
162
|
+
break;
|
|
163
|
+
case "all":
|
|
164
|
+
setGateOverride(state, auto, "all");
|
|
165
|
+
if (engage(ctx, auto)) ctx.ui.notify("Auto mode on. Reviewing all tools.", "info");
|
|
166
|
+
break;
|
|
167
|
+
case "side":
|
|
168
|
+
setGateOverride(state, auto, "side-effect");
|
|
169
|
+
ctx.ui.notify(`Reviewing side-effect tools only: ${describeGate(state)}`, "info");
|
|
170
|
+
break;
|
|
171
|
+
case "":
|
|
172
|
+
case "status":
|
|
173
|
+
ctx.ui.notify(
|
|
174
|
+
state.engaged
|
|
175
|
+
? `Auto mode on. Reviewing: ${describeGate(state)}. Reviewer: ${auto.reviewerModel ?? "unset"}.`
|
|
176
|
+
: `Auto mode off. Eligible: ${isAutoModeEligible(ctx.model, auto) ? "yes" : "no"}.`,
|
|
177
|
+
"info",
|
|
178
|
+
);
|
|
179
|
+
break;
|
|
180
|
+
default:
|
|
181
|
+
ctx.ui.notify("Usage: /auto [on|off|status|all|side]", "warning");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
updateStatus(ctx);
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
pi.on("session_start", (_event, ctx) => {
|
|
189
|
+
const { config } = loadConfig();
|
|
190
|
+
setGateOverride(state, config.autoMode, undefined);
|
|
191
|
+
applyConfiguredGate(state, config.autoMode);
|
|
192
|
+
if (pi.getFlag(AUTO_MODE_FLAG)) engage(ctx, config.autoMode);
|
|
193
|
+
updateStatus(ctx);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
pi.on("model_select", (event, ctx) => {
|
|
197
|
+
const { config } = loadConfig();
|
|
198
|
+
if (state.engaged && !isAutoModeEligible(event.model, config.autoMode)) {
|
|
199
|
+
state.engaged = false;
|
|
200
|
+
if (ctx.hasUI) ctx.ui.notify("Auto mode off: this model is not allowlisted.", "warning");
|
|
201
|
+
}
|
|
202
|
+
updateStatus(ctx);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
206
|
+
const { config } = loadConfig();
|
|
207
|
+
applyConfiguredGate(state, config.autoMode);
|
|
208
|
+
if (typeof event.prompt === "string" && event.prompt.trim().length > 0) {
|
|
209
|
+
lastIntent = boundReviewText(event.prompt, MAX_REVIEW_INTENT_CHARS);
|
|
210
|
+
}
|
|
211
|
+
if (state.engaged && !isAutoModeEligible(ctx.model, config.autoMode)) {
|
|
212
|
+
state.engaged = false;
|
|
213
|
+
}
|
|
214
|
+
updateStatus(ctx);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
218
|
+
if (!state.engaged) return undefined;
|
|
219
|
+
|
|
220
|
+
const { config } = loadConfig();
|
|
221
|
+
const auto = config.autoMode;
|
|
222
|
+
applyConfiguredGate(state, auto);
|
|
223
|
+
|
|
224
|
+
if (!isAutoModeEligible(ctx.model, auto)) {
|
|
225
|
+
state.engaged = false;
|
|
226
|
+
updateStatus(ctx);
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
if (!shouldReviewTool(event.toolName, state)) return undefined;
|
|
230
|
+
|
|
231
|
+
const pending = {
|
|
232
|
+
toolName: event.toolName,
|
|
233
|
+
toolCallId: event.toolCallId,
|
|
234
|
+
reviewerModelSpec: auto.reviewerModel ?? "",
|
|
235
|
+
};
|
|
236
|
+
const outcome = await requestReview({
|
|
237
|
+
registry: ctx.modelRegistry,
|
|
238
|
+
reviewerModelSpec: pending.reviewerModelSpec,
|
|
239
|
+
toolName: event.toolName,
|
|
240
|
+
toolInput: event.input,
|
|
241
|
+
intent: lastIntent,
|
|
242
|
+
cwd: ctx.cwd,
|
|
243
|
+
timeoutMs: auto.timeoutMs,
|
|
244
|
+
signal: ctx.signal,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const decision = evaluateOutcome(outcome, pending, ctx);
|
|
248
|
+
if (decision.kind === "ask-human") {
|
|
249
|
+
const confirmed = await ctx.ui.confirm(
|
|
250
|
+
"Auto-mode review unavailable",
|
|
251
|
+
`${decision.reason}\n\nAllow ${event.toolName} to run?`,
|
|
252
|
+
);
|
|
253
|
+
recordDecision({
|
|
254
|
+
toolName: event.toolName,
|
|
255
|
+
toolCallId: event.toolCallId,
|
|
256
|
+
decision: confirmed ? "unavailable-allowed" : "unavailable-blocked",
|
|
257
|
+
reason: decision.reason,
|
|
258
|
+
reviewerModel: pending.reviewerModelSpec,
|
|
259
|
+
source: "human",
|
|
260
|
+
});
|
|
261
|
+
return confirmed
|
|
262
|
+
? undefined
|
|
263
|
+
: { block: true, reason: "Blocked by user after auto-mode review could not complete." };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
updateStatus(ctx);
|
|
267
|
+
return decision.kind === "block" ? { block: true, reason: decision.reason } : undefined;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export default function autoModeExtension(pi: ExtensionAPI): void {
|
|
272
|
+
registerAutoModeExtension(pi);
|
|
273
|
+
}
|