pi-openai-toolkit 0.15.1 → 0.16.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 +24 -2
- package/README.zh.md +24 -2
- package/docs/configuration.md +9 -3
- package/docs/internals.md +25 -2
- package/package.json +3 -1
- package/src/auto-mode/classifier.ts +161 -165
- package/src/auto-mode/extension.ts +691 -661
- package/src/auto-mode/model-selection-guard.ts +52 -0
- package/src/auto-mode/reviewer.ts +325 -335
- package/src/auto-mode/transcript.ts +135 -133
- package/src/context-management/manual-compact.ts +433 -0
- package/src/context-management/tools.ts +11 -2
- package/src/context-management/window-manager.ts +35 -9
- package/src/extension-runtime.ts +49 -11
- package/src/remote-v2-client.ts +108 -33
package/README.md
CHANGED
|
@@ -124,6 +124,28 @@ Earlier windows remain retrievable through `history`, but are not all automatica
|
|
|
124
124
|
|
|
125
125
|
## Common tasks
|
|
126
126
|
|
|
127
|
+
### Checkpoint a managed session with `/compact`
|
|
128
|
+
|
|
129
|
+
In an active Remote Context session, run `/compact` (optionally followed by checkpoint instructions) to save work in notes and enter one new window in the same Pi session. Toolkit temporarily selects `context.remoteCompaction.model`, waits for a new successful persisted notes checkpoint and window marker, then restores the original model and thinking level before the first new-window request. That model is instructed to read the checkpoint receipt before continuing.
|
|
130
|
+
|
|
131
|
+
To choose a checkpoint model, merge this into your v2 configuration and replace the model reference with an existing Pi model:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"schemaVersion": 2,
|
|
136
|
+
"defaults": {
|
|
137
|
+
"context": {
|
|
138
|
+
"mode": "remote-windows",
|
|
139
|
+
"remoteCompaction": { "model": "my-gateway/gpt-5.6-luna" }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The checkpoint model also needs Remote Context support, the same backend/account, enough context capacity, and the permitted context tools. A gateway model needs its own exact transport opt-in. Missing or `null` uses the current model. An engaged Auto Mode gate must remain eligible on the target; otherwise the handoff is refused. Notes or model failures restore the original selection when still owned by Toolkit; your later explicit model selection wins. Reload only recovers selection, without automatically restarting inference.
|
|
146
|
+
|
|
147
|
+
This does not generate a conversation summary. Pi's native compact operation is intentionally cancelled to launch the separate handoff, so an SDK/RPC caller can see a cancellation even while checkpoint duty starts. Pi can also reject an empty or already-compacted session before Toolkit's hook runs. Automatic threshold/overflow handling and internal trim maintenance keep their existing behavior. See the [handoff contract](docs/internals.md#managed-manual-compact).
|
|
148
|
+
|
|
127
149
|
### Continue a session with server-side compaction
|
|
128
150
|
|
|
129
151
|
Use `context.mode: "remote-compaction"` for the Responses compaction path (the default), or `"pi"` to relinquish Toolkit context management. Set `context.remoteCompaction.model` only when a separate model should produce the checkpoint. These fields belong under `defaults` or an exact `models` override.
|
|
@@ -208,7 +230,7 @@ The global file uses built-ins, then `defaults`, then exact `models["provider/mo
|
|
|
208
230
|
| --- | --- | --- |
|
|
209
231
|
| `defaults.context.mode` | `"remote-compaction"` | `"pi"`, `"remote-compaction"`, or `"remote-windows"`. |
|
|
210
232
|
| `models[exact].compatibility.transport` | `"standard"` | Exact gateway opt-in with `"codex-gateway"`. |
|
|
211
|
-
| `defaults.context.remoteCompaction.model` | `null` |
|
|
233
|
+
| `defaults.context.remoteCompaction.model` | `null` | Checkpoint producer, or manual checkpoint-duty model in `remote-windows`. |
|
|
212
234
|
| `defaults.context.remoteCompaction.inputSource` | `"legacy"` | Source-specific checkpoint input policy. |
|
|
213
235
|
| `defaults.context.remoteWindows.reminderThresholdPercent` | `5` | `0` disables reminders and exhausted-window fallback. |
|
|
214
236
|
| `defaults.webSearch.route` | `"unmanaged"` | Toolkit search ownership policy. |
|
|
@@ -223,7 +245,7 @@ The global file uses built-ins, then `defaults`, then exact `models["provider/mo
|
|
|
223
245
|
|
|
224
246
|
Use `/toolkit-config` for effective values and origins, `/toolkit-config validate` for document issues, and `/toolkit-config migration-preview` for a read-only legacy candidate. These commands require a UI and perform no network/auth lookup, tool activation, or file writes. Configuration selection does not verify backend support.
|
|
225
247
|
|
|
226
|
-
Unknown v2 policy keys and malformed values produce visible scoped errors, independent of debug mode. Each public callback or tool execution uses one immutable snapshot across its awaited helpers; later operations reread the file. Separate Pi events are not
|
|
248
|
+
Unknown v2 policy keys and malformed values produce visible scoped errors, independent of debug mode. Each public callback or tool execution uses one immutable snapshot across its awaited helpers; later independent operations reread the file. A managed `/compact` handoff retains its initiating context-policy snapshot until selection is restored. Separate Pi events are not a general atomic transaction. See the [complete configuration reference](docs/configuration.md) and [editor schema](config.schema.json).
|
|
227
249
|
|
|
228
250
|
## Development
|
|
229
251
|
|
package/README.zh.md
CHANGED
|
@@ -124,6 +124,28 @@ pi --model my-gateway/gpt-5.6-luna
|
|
|
124
124
|
|
|
125
125
|
## 常见用法
|
|
126
126
|
|
|
127
|
+
### 用 `/compact` 为托管会话保存检查点
|
|
128
|
+
|
|
129
|
+
Remote Context 已激活时,运行 `/compact`,后面可附加检查点要求,即可把工作状态保存到 notes,并在同一 Pi 会话中进入一个新窗口。Toolkit 临时选择 `context.remoteCompaction.model`,等待新的 notes 检查点和窗口标记成功持久化,再在新窗口首次请求前恢复原模型及思考等级,并要求原模型先读取检查点回执再继续工作。
|
|
130
|
+
|
|
131
|
+
要指定检查点模型,将以下内容合并到 v2 配置,并把模型引用替换为 Pi 中已有的模型:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"schemaVersion": 2,
|
|
136
|
+
"defaults": {
|
|
137
|
+
"context": {
|
|
138
|
+
"mode": "remote-windows",
|
|
139
|
+
"remoteCompaction": { "model": "my-gateway/gpt-5.6-luna" }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
检查点模型也必须支持 Remote Context,使用相同后端及账号,有足够的上下文容量,并允许使用上下文工具。网关模型还需要单独配置精确的传输协议选项。未设置或为 `null` 时使用当前模型。若 Auto Mode 已启用,目标模型也必须符合其准入条件,否则交接会被拒绝。notes 或模型操作失败时,只要临时选择仍由 Toolkit 管理,就会恢复原模型;用户后来明确选择的模型优先。重载只恢复模型选择,不会自动重新发起推理。
|
|
146
|
+
|
|
147
|
+
该操作不生成会话摘要。Toolkit 会有意取消 Pi 原生压缩操作,再单独启动检查点交接,因此 SDK/RPC 调用方可能收到取消结果,同时看到检查点任务开始。空会话或刚压缩过的会话也可能在进入 Toolkit 钩子前就被 Pi 拒绝。自动阈值、溢出处理及内部旧窗口清理保持原有行为。详见[交接约定](docs/internals.md#managed-manual-compact)。
|
|
148
|
+
|
|
127
149
|
### 使用服务端压缩继续会话
|
|
128
150
|
|
|
129
151
|
使用 `context.mode: "remote-compaction"` 选择 Responses 压缩路径(默认值),或设为 `"pi"` 交回 Toolkit 的上下文管理权。只有需要独立模型生成检查点时才设置 `context.remoteCompaction.model`。这些字段放在 `defaults` 或精确的 `models` 覆盖下。
|
|
@@ -208,7 +230,7 @@ TUI 会显示开启提示、审查活动和底部状态。兼容的 Pi 工具渲
|
|
|
208
230
|
| --- | --- | --- |
|
|
209
231
|
| `defaults.context.mode` | `"remote-compaction"` | `"pi"`、`"remote-compaction"` 或 `"remote-windows"`。 |
|
|
210
232
|
| `models[exact].compatibility.transport` | `"standard"` | 用 `"codex-gateway"` 显式选择网关协议。 |
|
|
211
|
-
| `defaults.context.remoteCompaction.model` | `null` |
|
|
233
|
+
| `defaults.context.remoteCompaction.model` | `null` | 检查点生成模型,或 `remote-windows` 下手动检查点任务使用的模型。 |
|
|
212
234
|
| `defaults.context.remoteCompaction.inputSource` | `"legacy"` | 与检查点来源绑定的输入策略。 |
|
|
213
235
|
| `defaults.context.remoteWindows.reminderThresholdPercent` | `5` | `0` 关闭提醒和窗口耗尽兜底。 |
|
|
214
236
|
| `defaults.webSearch.route` | `"unmanaged"` | Toolkit 搜索管理策略。 |
|
|
@@ -223,7 +245,7 @@ TUI 会显示开启提示、审查活动和底部状态。兼容的 Pi 工具渲
|
|
|
223
245
|
|
|
224
246
|
使用 `/toolkit-config` 查看有效值及来源,`/toolkit-config validate` 查看文档问题,`/toolkit-config migration-preview` 预览只读的旧格式迁移候选。这些命令需要 UI,不查询网络或认证,不激活工具,也不写文件。选中配置不表示后端支持已验证。
|
|
225
247
|
|
|
226
|
-
未知 v2
|
|
248
|
+
未知 v2 策略键和畸形值会产生可见的分范围错误,不依赖调试模式。每个公开回调或工具执行及其等待的辅助操作使用同一份不可变快照,后续独立操作重新读取文件;托管 `/compact` 交接会保留发起时的上下文策略快照,直到模型恢复。独立 Pi 事件之间不保证通用的原子事务。完整选项见[配置参考](docs/configuration.md)和[编辑器 schema](config.schema.json)。
|
|
227
249
|
|
|
228
250
|
## 开发
|
|
229
251
|
|
package/docs/configuration.md
CHANGED
|
@@ -57,7 +57,7 @@ Place `context` under `defaults` or an exact `models` entry.
|
|
|
57
57
|
| Relative field | Default | Contract |
|
|
58
58
|
| --- | --- | --- |
|
|
59
59
|
| `mode` | `"remote-compaction"` | `"pi"` relinquishes Toolkit context management; `"remote-compaction"` uses the eligible Responses checkpoint path; `"remote-windows"` selects Codex windows where supported. |
|
|
60
|
-
| `remoteCompaction.model` | `null` | Exact producer reference; `null` uses the active model.
|
|
60
|
+
| `remoteCompaction.model` | `null` | Exact producer reference; `null` uses the active model. In `remote-compaction`, it selects the synthetic checkpoint producer on the same effective base URL. In active `remote-windows`, manual `/compact` temporarily selects it for notes checkpoint duty on the same backend/account. |
|
|
61
61
|
| `remoteCompaction.inputSource` | `"legacy"` | `"legacy"` preserves session/raw-branch input; `"pi-context-hook"` opts into Pi's ordered context projection. Checkpoint provenance must match. |
|
|
62
62
|
| `remoteCompaction.allowContinuityBreak` | `false` | Allow restarting from Pi context after a foreign compaction entry. It does not make malformed opaque checkpoints replayable. |
|
|
63
63
|
| `remoteCompaction.apis` | `["openai-responses", "openai-codex-responses"]` | May narrow this set; `[]` permits no remote-compaction API. Unsupported entries are errors. |
|
|
@@ -67,7 +67,13 @@ Place `context` under `defaults` or an exact `models` entry.
|
|
|
67
67
|
| `remoteWindows.leaveManagedMode` | `"warn"` | `"compact"` also requests a close-out when retired history exceeds 80% of the next model's window and compaction can finish. Print mode remains warning-only; turn-end close-out requires a queued trim. |
|
|
68
68
|
| `remoteWindows.reminderThresholdPercent` | `5` | Integer 0-100. `0` disables both the once-per-window reminder and exhausted-window fallback. |
|
|
69
69
|
|
|
70
|
-
Native Codex eligibility derives from the actual Pi provider/API. Gateways additionally need the exact transport opt-in below. `remote-windows` retains the compaction chain for models outside window capability. A configured window model whose activation fails does not silently receive a summary fallback.
|
|
70
|
+
Native Codex eligibility derives from the actual Pi provider/API. Gateways additionally need the exact transport opt-in below. `remote-windows` retains the compaction chain for models outside window capability. A configured window model whose activation fails does not silently receive a summary fallback. Normal `new_context` keeps its persisted-notes, duplicate and cooldown gates. Synthetic Remote V2 producer/consumer identity and checkpoint provenance are unchanged. See [Toolkit internals](internals.md).
|
|
71
|
+
|
|
72
|
+
In active `remote-windows`, `/compact [instructions]` starts checkpoint duty rather than producing a summary. The selected model must authenticate to the same native backend/account or gateway credential/affinity domain, fit the projected window plus its output reserve, and retain the required permitted context tools. Invalid or unsuitable explicit targets are refused without switching to a fallback model. If Auto Mode is engaged, the target must also be eligible for its gate. The entire handoff uses the initiating context-policy snapshot; no config or global Pi defaults are rewritten.
|
|
73
|
+
|
|
74
|
+
Only a new persisted successful notes write/append after the handoff and the latest delivered user message authorizes its rollover. After the exact target marker is durable, Toolkit restores the original model/thinking before new-window inference and asks it to read the receipt first. Failure or cancellation restores an owned selection; a later user selection takes priority. Failed restoration blocks further handoff requests until selection/auth is repaired. Reload can recover selection, but does not automatically restart inference.
|
|
75
|
+
|
|
76
|
+
The SDK native compact call reports its intentional cancellation; the visible handoff is a separate agent operation. Pi's pre-hook “nothing to compact” and “already compacted” outcomes remain. Automatic threshold/overflow compaction and internal pending-trim maintenance do not initiate checkpoint duty. `nativeFallback.model` keeps its existing summary-model meaning.
|
|
71
77
|
|
|
72
78
|
---
|
|
73
79
|
|
|
@@ -124,7 +130,7 @@ Place `autoMode` under `defaults` or an exact model. `available` permits engagem
|
|
|
124
130
|
| `circuitBreaker.recentDenials` | `10` | Integer 0-100; `0` disables this limit. |
|
|
125
131
|
| `circuitBreaker.windowSize` | `50` | Integer 1-200, recent-verdict window size. |
|
|
126
132
|
|
|
127
|
-
Reviewer and classifier references are not active-session overrides. Classifier scores, denial history, and human decisions never become config entries.
|
|
133
|
+
Reviewer and classifier references are not active-session overrides. Classifier scores, denial history, and human decisions never become config entries. The reviewer and its read-only evidence calls share one cancellable deadline, including dependencies that ignore abort. Denial history resets on actual delivery of a user message, including identical queued messages, not on enqueue or an ordinary tool continuation. Pre-scores are bound to a full structured authorization fingerprint; the bounded review transcript prioritizes recent user instructions and reports omissions.
|
|
128
134
|
|
|
129
135
|
---
|
|
130
136
|
|
package/docs/internals.md
CHANGED
|
@@ -9,6 +9,7 @@ Developer-facing reference for [pi-openai-toolkit](../README.md). The README kee
|
|
|
9
9
|
- [Configuration resolution](#configuration-resolution)
|
|
10
10
|
- [Remote Context protocol](#remote-context-protocol)
|
|
11
11
|
- [Rollover lifecycle](#rollover-lifecycle)
|
|
12
|
+
- [Managed manual compact](#managed-manual-compact)
|
|
12
13
|
- [Remote Compaction v2 wire contract](#remote-compaction-v2-wire-contract)
|
|
13
14
|
- [Auto Mode TUI review renderer](#auto-mode-tui-review-renderer)
|
|
14
15
|
- [Artifacts and debugging](#artifacts-and-debugging)
|
|
@@ -20,7 +21,7 @@ Developer-facing reference for [pi-openai-toolkit](../README.md). The README kee
|
|
|
20
21
|
|
|
21
22
|
The single global document is loaded through [src/config.ts](../src/config.ts). [src/config/legacy.ts](../src/config/legacy.ts) owns unversioned compatibility; [src/config/v2.ts](../src/config/v2.ts) validates v2 scopes, resolves exact policy, and records leaf origins. [src/config/policy.ts](../src/config/policy.ts) defines effective values independently of persisted session data. See the [configuration reference](configuration.md) for supported fields and migration limits.
|
|
22
23
|
|
|
23
|
-
Feature entrypoints resolve once per public operation and pass that immutable snapshot through awaited helpers. A separately named compaction producer is resolved from the same decoded document for its own identity. Subsequent callbacks reload;
|
|
24
|
+
Feature entrypoints resolve once per public operation and pass that immutable snapshot through awaited helpers. A separately named compaction producer is resolved from the same decoded document for its own identity. Subsequent independent callbacks reload; a managed manual handoff retains its initiating context-policy snapshot until selection is restored. There is no general cross-event provider transaction, project lookup, singleton cache, or config state writer. Internal legacy-shaped engine adapters preserve compaction/search algorithms without exposing v2 parsing to consumers. Cross-feature gateway policy belongs to the resolver, not the context enable switch.
|
|
24
25
|
|
|
25
26
|
Selected invalid policy blocks dependent operations; unrelated model errors remain reportable without replacing the selected model. An engaged auto gate stays blocking when its policy becomes invalid. Image choices are checked before auth, uploads, or paid requests. The human `/toolkit-config` command inspects effective values, validation, or a migration candidate without action APIs or network access. Migration is preview-only; source bytes and unknown/dormant content stay untouched.
|
|
26
27
|
|
|
@@ -81,7 +82,7 @@ State invariants the lifecycle code must keep honest:
|
|
|
81
82
|
- `new_context` has no force escape hatch: a persisted successful notes checkpoint in the current window is always required before rollover. The model must wait for the notes result to be persisted before retrying.
|
|
82
83
|
- 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.
|
|
83
84
|
- 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.
|
|
84
|
-
-
|
|
85
|
+
- Preparing a boundary never consumes its scheduled trim. The manager reconciles the latest persisted compaction and branch state, so only a durable matching boundary consumes it. Missing/late callbacks, aborted preparation and navigation across branches sharing a marker remain retryable. Automatic threshold/overflow and toolkit-owned manual maintenance only trim a matching scheduled window; a user manual `/compact` follows the managed handoff below.
|
|
85
86
|
- A window boundary is persisted as a `codex-context-window` custom message. `session_start` replays boundaries from the branch and rebuilds identity after forks.
|
|
86
87
|
- Retiring windows is a projection-level act: the session branch keeps every retired window until a compaction consumes the scheduled trim. `evaluateWindowBulk()` measures that gap and compares it with the model about to receive it, `decideBulkCliffAction()` maps the report onto the `leaveManagedMode` policy (`warn` default, `compact`), and `decideBulkCloseOut()` only allows the automatic compaction where a compaction can actually finish: never in print mode (the process exits with the turn and would kill the summary), and at turn end only while a rollover trim is queued, because the window policy cancels every other compaction. Each window and model is surfaced once, and every decision writes a `window-bulk.<action>` compaction artifact with its trigger.
|
|
87
88
|
- The close-out runs from `model_select` and `agent_settled`, never from `before_agent_start`: `ExtensionContext.compact()` aborts the running turn, and a hook inside turn processing is exactly the busy lane it must not fight.
|
|
@@ -89,12 +90,32 @@ State invariants the lifecycle code must keep honest:
|
|
|
89
90
|
|
|
90
91
|
---
|
|
91
92
|
|
|
93
|
+
### Managed manual compact
|
|
94
|
+
|
|
95
|
+
[src/context-management/manual-compact.ts](../src/context-management/manual-compact.ts) owns manual checkpoint duty. It uses public Pi lifecycle and selection APIs; it does not replace the built-in command or patch the scheduler. [src/context-management/window-manager.ts](../src/context-management/window-manager.ts) remains the sole owner of notes pairing, window identity, projection and trim.
|
|
96
|
+
|
|
97
|
+
1. An active managed `session_before_compact` with reason `manual` validates the source/target policies from one snapshot, actual permitted tools, remaining capacity, backend/account scope and approval eligibility. It records the original model/thinking and cancels the native compact attempt. Internal window-bulk maintenance has a separate in-process owner even though Pi also labels it `manual`.
|
|
98
|
+
2. The matching `session_compact_failed` callback launches duty only after Pi is idle. Public `setModel` changes the session selection, and a visible `sendUserMessage` rebuilds the target's normal prompt/tools. Delivery is observed through `message_start`; return from the fire-and-forget send API is not proof. A delivery timeout restores an abandoned idle selection.
|
|
99
|
+
3. The existing `new_context` gate additionally requires a successful paired notes result after this operation and any later delivered user message. It records the exact target window before queueing a context-only marker, and returns a terminating result. Ordinary tools are blocked during checkpoint duty. Normal `new_context` outside this operation is unchanged.
|
|
100
|
+
4. A terminating result alone does not stop mixed batches or queued messages. At `turn_end`, Toolkit requests abort without awaiting idle; context/request guards also refuse an unsafe continuation. The standard Responses and native Codex transports must not send an already-aborted continuation. Pi can deliver queued user content during that stop; it remains persisted and ordered for the original model.
|
|
101
|
+
5. At `agent_settled`, after Pi has flushed custom messages, the controller verifies the exact durable marker and fresh checkpoint, restores original model/thinking, and sends one visible receipt-reading continuation. Restoring inside marker `message_end` would be too early for persistence and too late for a model snapshot already captured by `prepareNextTurn`.
|
|
102
|
+
|
|
103
|
+
Versioned `pi-openai-toolkit:manual-compact` custom entries record operation/session/branch/window identities, exact model keys, original thinking and phase. They contain no credentials, transcript copy or note contents. Reload/navigation reads the current branch and only recovers still-owned selection; it never retries notes, rollover or paid inference. A newer user model/thinking selection ends ownership. Failed restoration stays durable and blocks further provider requests until recovery or an explicit user selection.
|
|
104
|
+
|
|
105
|
+
An engaged Auto Mode gate grants a session/operation-scoped selection lease through [src/auto-mode/model-selection-guard.ts](../src/auto-mode/model-selection-guard.ts). An ineligible target is refused. While switching, a later eligibility change cannot silently disengage the gate: calls remain blocked. The lease neither enables Auto Mode nor widens its allowlist.
|
|
106
|
+
|
|
107
|
+
The underlying native compact promise still rejects its intentional cancellation. Empty/already-compacted sessions can fail before the extension hook. This is a separate handoff operation, not a fabricated successful native compaction. An observed user cancellation before the owned checkpoint stop restores selection without automatically resuming. Tests use the official Pi runtime, disk-backed sessions, isolated HOME/settings and synthetic Responses/native Codex traffic in [test/pi-managed-compact.test.ts](../test/pi-managed-compact.test.ts); they assert every request's model/thinking, marker/note order, retained tool head and queue preservation.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
92
111
|
### Remote Compaction v2 wire contract
|
|
93
112
|
|
|
94
113
|
v2 is what an uncovered session gets. Any model that Remote Context declines, such as a gateway without an exact transport opt-in, can still compact through v2 when its API appears in `context.remoteCompaction.apis`. The two strategies never compete for one session.
|
|
95
114
|
|
|
96
115
|
A `compaction_trigger` item appended to the live streaming request yields one output item of `type: "compaction"` with non-empty `encrypted_content`, stored in `CompactionEntry.details.compactedWindow`. On later requests the opaque checkpoint is replayed ahead of live turns, with no text summary. Replay fails closed: if the summary anchor cannot be located, the request is aborted with a notification and a content-free failure artifact. The sentinel-only payload is never sent.
|
|
97
116
|
|
|
117
|
+
The incremental SSE consumer validates the first terminal event and reconciles its checkpoint with prior item evidence. A valid completed frame ends the operation even if the HTTP body stays open; reader cancellation is best-effort cleanup and is not awaited. Incomplete frames at EOF, conflicting checkpoint evidence and terminal errors cannot become successful checkpoints.
|
|
118
|
+
|
|
98
119
|
A v2 response with a missing or empty checkpoint is never stored, and the `nativeFallback` tier is skipped. Pi's own threshold drives the next attempt, which may use `remoteCompactModel` when configured. `remoteCompactModel` must resolve to the same effective base URL as the active model.
|
|
99
120
|
|
|
100
121
|
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 `context.remoteCompaction.inputSource` 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.
|
|
@@ -107,6 +128,8 @@ New checkpoints record `inputProvenance: "pi-context-hook-v1"` or `"legacy-raw-c
|
|
|
107
128
|
|
|
108
129
|
### Auto Mode TUI review renderer
|
|
109
130
|
|
|
131
|
+
Approval state is scoped to actual user-message delivery. Full structured user content is SHA-256 hashed independently of transcript truncation, and generation-owned classifier completions cannot overwrite newer samples. The bounded transcript selects recent user instructions first and reports omissions. The blocking reviewer and its read-only evidence loop share one deadline that also races uncooperative dependencies; timeout/cancellation is an unavailable review, never approval or a safety denial.
|
|
132
|
+
|
|
110
133
|
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.
|
|
111
134
|
|
|
112
135
|
The extension updates an ephemeral per-call state through the same `tool_call` lifecycle that performs the review:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-openai-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"src/context-management/window-request.ts",
|
|
64
64
|
"src/context-management/window-bulk.ts",
|
|
65
65
|
"src/context-management/window-manager.ts",
|
|
66
|
+
"src/context-management/manual-compact.ts",
|
|
66
67
|
"src/context-management/codex-provider.ts",
|
|
67
68
|
"src/context-management/history-notes.ts",
|
|
68
69
|
"src/context-management/tools.ts",
|
|
@@ -80,6 +81,7 @@
|
|
|
80
81
|
"src/auto-mode/extension.ts",
|
|
81
82
|
"src/auto-mode/tool-review-tui.ts",
|
|
82
83
|
"src/auto-mode/policy.ts",
|
|
84
|
+
"src/auto-mode/model-selection-guard.ts",
|
|
83
85
|
"src/auto-mode/prompt.ts",
|
|
84
86
|
"src/auto-mode/transcript.ts",
|
|
85
87
|
"src/auto-mode/reviewer.ts",
|
|
@@ -1,165 +1,161 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
tracker.
|
|
39
|
-
tracker.
|
|
40
|
-
tracker.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function
|
|
58
|
-
tracker.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
modelSpec
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
clearTimeout(timer);
|
|
163
|
-
params.signal?.removeEventListener("abort", abortFromCaller);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { parseModelSpec } from "../runtime";
|
|
5
|
+
import { CLASSIFIER_SYSTEM_PROMPT } from "./prompt";
|
|
6
|
+
import { boundReviewText, type ClassifierRisk, type FastDecisionReason } from "./types";
|
|
7
|
+
|
|
8
|
+
/** One completed classification of the trajectory, tagged to a tool-call position. */
|
|
9
|
+
export type ScoreRecord = {
|
|
10
|
+
risk: ClassifierRisk;
|
|
11
|
+
/** Tool-call counter value when the sample was taken. */
|
|
12
|
+
scoredAtCall: number;
|
|
13
|
+
/** Fingerprint of the user turns the score was based on. */
|
|
14
|
+
authorizationVersion: string;
|
|
15
|
+
sampledAt: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Mutable per-session tracker for the non-blocking pre-scorer.
|
|
20
|
+
*
|
|
21
|
+
* The counters reproduce Codex's staleness rule: the agent keeps running while a
|
|
22
|
+
* classification is in flight, so a score is only trustworthy if it is recent
|
|
23
|
+
* enough and nothing the user said has changed underneath it.
|
|
24
|
+
*/
|
|
25
|
+
export type ScoreTracker = {
|
|
26
|
+
latestCallIndex: number;
|
|
27
|
+
latestScoredIndex: number;
|
|
28
|
+
latestFailedIndex: number;
|
|
29
|
+
score?: ScoreRecord;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function createScoreTracker(): ScoreTracker {
|
|
33
|
+
return { latestCallIndex: 0, latestScoredIndex: 0, latestFailedIndex: 0 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Reset everything: a new turn or a disengaged mode invalidates prior scoring. */
|
|
37
|
+
export function resetScoreTracker(tracker: ScoreTracker): void {
|
|
38
|
+
tracker.latestCallIndex = 0;
|
|
39
|
+
tracker.latestScoredIndex = 0;
|
|
40
|
+
tracker.latestFailedIndex = 0;
|
|
41
|
+
tracker.score = undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fingerprint of the user-authored conversation. Any new or edited user turn makes
|
|
46
|
+
* an existing score stale, because the score's authorization premise changed.
|
|
47
|
+
*/
|
|
48
|
+
export function authorizationVersion(userText: string): string {
|
|
49
|
+
return createHash("sha256").update(userText).digest("hex");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function recordScoredCall(tracker: ScoreTracker, score: ScoreRecord): void {
|
|
53
|
+
tracker.score = score;
|
|
54
|
+
tracker.latestScoredIndex = Math.max(tracker.latestScoredIndex, score.scoredAtCall);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function recordFailedCall(tracker: ScoreTracker, scoredAtCall: number): void {
|
|
58
|
+
tracker.latestFailedIndex = Math.max(tracker.latestFailedIndex, scoredAtCall);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type FastDecisionInput = {
|
|
62
|
+
tracker: ScoreTracker;
|
|
63
|
+
/** Tool-call counter value for the call being gated. */
|
|
64
|
+
currentCallIndex: number;
|
|
65
|
+
authorizationVersion: string;
|
|
66
|
+
maxLag: number;
|
|
67
|
+
/** Retries after a denial and widened gates must not take the fast path. */
|
|
68
|
+
requiresSynchronousReview?: boolean;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Decide whether a cached classification can satisfy this gated call without
|
|
73
|
+
* running the blocking reviewer. Anything other than `low_risk` defers to the
|
|
74
|
+
* synchronous review; the classifier never denies an action on its own.
|
|
75
|
+
*/
|
|
76
|
+
export function fastApprovalEligible(input: FastDecisionInput): {
|
|
77
|
+
eligible: boolean;
|
|
78
|
+
reason: FastDecisionReason;
|
|
79
|
+
} {
|
|
80
|
+
const defer = (reason: FastDecisionReason) => ({ eligible: false, reason });
|
|
81
|
+
|
|
82
|
+
if (input.requiresSynchronousReview) return defer("requires_synchronous_review");
|
|
83
|
+
if (input.tracker.latestFailedIndex > input.tracker.latestScoredIndex) return defer("scoring_failure");
|
|
84
|
+
|
|
85
|
+
const score = input.tracker.score;
|
|
86
|
+
if (!score) return defer("missing_score");
|
|
87
|
+
|
|
88
|
+
const lag = input.currentCallIndex - score.scoredAtCall;
|
|
89
|
+
if (lag < 0 || lag > input.maxLag) return defer("stale_score");
|
|
90
|
+
if (score.authorizationVersion !== input.authorizationVersion) return defer("authorization_changed");
|
|
91
|
+
if (score.risk === "low") return { eligible: true, reason: "low_risk" };
|
|
92
|
+
return defer("elevated_risk");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type ClassificationResult =
|
|
96
|
+
| { kind: ClassifierRisk; model: string }
|
|
97
|
+
| { kind: "failed"; reason: string };
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Run one non-blocking classification. Output is a single token; anything else is a
|
|
101
|
+
* failed sample rather than a verdict, so a confused classifier costs a review and
|
|
102
|
+
* never an approval or a denial.
|
|
103
|
+
*/
|
|
104
|
+
export async function classifyTrajectory(params: {
|
|
105
|
+
registry: Pick<ModelRegistry, "find" | "complete">;
|
|
106
|
+
modelSpec: string;
|
|
107
|
+
prompt: string;
|
|
108
|
+
timeoutMs: number;
|
|
109
|
+
signal?: AbortSignal;
|
|
110
|
+
}): Promise<ClassificationResult> {
|
|
111
|
+
const parsed = parseModelSpec(params.modelSpec);
|
|
112
|
+
if (!parsed) {
|
|
113
|
+
return { kind: "failed", reason: `autoMode.classifier.model "${params.modelSpec}" is not provider/model-id.` };
|
|
114
|
+
}
|
|
115
|
+
const model = params.registry.find(parsed.provider, parsed.modelId) as Model<Api> | undefined;
|
|
116
|
+
if (!model) {
|
|
117
|
+
return { kind: "failed", reason: `Classifier model ${params.modelSpec} is not available.` };
|
|
118
|
+
}
|
|
119
|
+
const label = boundReviewText(`${model.provider}/${model.id}`, 256);
|
|
120
|
+
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
const abortFromCaller = () => controller.abort();
|
|
123
|
+
if (params.signal?.aborted) controller.abort();
|
|
124
|
+
else params.signal?.addEventListener("abort", abortFromCaller);
|
|
125
|
+
const timer = setTimeout(() => controller.abort(), params.timeoutMs);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const response = await params.registry.complete(
|
|
129
|
+
model,
|
|
130
|
+
{
|
|
131
|
+
systemPrompt: CLASSIFIER_SYSTEM_PROMPT,
|
|
132
|
+
messages: [
|
|
133
|
+
{
|
|
134
|
+
role: "user",
|
|
135
|
+
content: [{ type: "text", text: params.prompt }],
|
|
136
|
+
timestamp: Date.now(),
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
{ signal: controller.signal, cacheRetention: "none" },
|
|
141
|
+
);
|
|
142
|
+
if (controller.signal.aborted) return { kind: "failed", reason: "Classifier was cancelled or timed out." };
|
|
143
|
+
if (response.stopReason === "error" || response.stopReason === "aborted") {
|
|
144
|
+
return { kind: "failed", reason: response.errorMessage ?? `Classifier stopped: ${response.stopReason}` };
|
|
145
|
+
}
|
|
146
|
+
const text = response.content
|
|
147
|
+
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
|
148
|
+
.map((block) => block.text)
|
|
149
|
+
.join("")
|
|
150
|
+
.trim()
|
|
151
|
+
.toLowerCase();
|
|
152
|
+
if (/^high\b/.test(text)) return { kind: "high", model: label };
|
|
153
|
+
if (/^low\b/.test(text)) return { kind: "low", model: label };
|
|
154
|
+
return { kind: "failed", reason: `Classifier returned unreadable output: ${boundReviewText(text, 80)}` };
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return { kind: "failed", reason: error instanceof Error ? error.message : String(error) };
|
|
157
|
+
} finally {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
params.signal?.removeEventListener("abort", abortFromCaller);
|
|
160
|
+
}
|
|
161
|
+
}
|