pi-verdict 0.9.0 → 0.9.1
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 +6 -6
- package/README.zh-CN.md +6 -6
- package/extensions/jev-adapter.ts +124 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -109,7 +109,7 @@ pi-verdict runs on both [pi](https://github.com/badlogic/pi-mono) and [oh-my-pi]
|
|
|
109
109
|
- `denyPaths` are plain paths you declare **protected** — touches trigger a terminal ask you adjudicate (non-interactive → deny); the classifier never learns the paths themselves, only that they exist. `grep`/`find`/`ls` compare their whole **search scope**: an omitted `path` (pi's default: the current directory) or a parent directory of a declared path triggers the ask as well. A fresh install pre-fills a **starter list** (`~/.ssh/`, `~/.gnupg`, `~/.mc`, shell rc/profile files), active from the first session after the initial run (any config change applies to new sessions) — a pre-filled *user declaration*, not a built-in floor: edit or empty it freely, add your own (`~/Documents/private`, …) alongside; existing configs are never rewritten
|
|
110
110
|
- `builtinDenyFloor: false` turns off the built-in danger/path floor (your risk; the self-protection layer below always stays on)
|
|
111
111
|
- `classifierModel` pins the classifier model, e.g. `"zai/glm-5.3-flash:low"` (thinking suffix supported; default: session model with thinking off)
|
|
112
|
-
- `classifierModel: "typesafe/jev-latest"` opts into the bundled **jev decisions adapter** — gray-zone verdicts via TypeSafe's jev
|
|
112
|
+
- `classifierModel: "typesafe/jev-latest"` opts into the bundled **jev decisions adapter** — gray-zone verdicts via TypeSafe's jev (OpenRouter by default, or TypeSafe's official API directly with `PI_VERDICT_JEV_TRANSPORT=typesafe`); experimental, see [ADR-0003](docs/adr/0003-jev-decisions-adapter.md)
|
|
113
113
|
- `audit: true` records every **gray-zone adjudication** (the full transcript sent to the classifier, its raw response, the parsed verdict) as JSONL under `~/.pi/agent/verdicts/<sessionId>.jsonl` — one file per session, the 20 most recent kept. Local-only and full-fidelity (protected-path plaintext may appear — it never leaves your machine; [ADR-0002](docs/adr/0002-deny-paths-deterministic-ask.md) boundary note); the agent can neither read nor write the directory. `/automode` shows the audit state and path while on
|
|
114
114
|
- `notifyAllows: true` notifies on every **classifier allow** (reason + action line — e.g. jev's probability breakdown); default `false` keeps passes silent. Mechanical passes (your own allow rules, protected-path confirms) never notify; shadow-cache annotations stay debug-only; with both switches on the notification appears once
|
|
115
115
|
|
|
@@ -118,17 +118,17 @@ No built-in allowlist — every "always allow" claim is yours ([why](docs/config
|
|
|
118
118
|
### Jev decisions backend (experimental — [ADR-0003](docs/adr/0003-jev-decisions-adapter.md))
|
|
119
119
|
|
|
120
120
|
1. Install a version that ships the adapter (v0.8+): `pi install npm:pi-verdict`
|
|
121
|
-
2.
|
|
122
|
-
- run `/login openrouter` inside pi
|
|
123
|
-
-
|
|
121
|
+
2. Pick a transport (both serve the same decisions wire contract):
|
|
122
|
+
- **OpenRouter (default)**: run `/login openrouter` inside pi, or `export OPENROUTER_API_KEY=sk-or-v1...` in your shell
|
|
123
|
+
- **TypeSafe direct (official v1 API)**: grab a self-service key at console.typesafe.ai, then `export TYPESAFE_API_KEY=apikey_...` and `export PI_VERDICT_JEV_TRANSPORT=typesafe`
|
|
124
124
|
3. Point the classifier at jev (applies to new sessions)
|
|
125
125
|
- persistent: edit `~/.pi/agent/config/pi-verdict.json` outside pi and set `{ "classifierModel": "typesafe/jev-latest" }`
|
|
126
126
|
- or try it once: `PI_AUTO_MODE_MODEL=typesafe/jev-latest pi`
|
|
127
127
|
|
|
128
128
|
**Limits**:
|
|
129
|
-
- **
|
|
129
|
+
- **Transports**: OpenRouter decisions (default) or TypeSafe direct — on the TypeSafe transport per-call cost shows $0 (its API does not report it)
|
|
130
130
|
- **Hosts**: pi only. On omp the setting warns and falls back to the session model; and it must never be selected as the session model (no text generation — selecting it warns)
|
|
131
|
-
- **Escape hatch**: `PI_VERDICT_JEV_URL` overrides the
|
|
131
|
+
- **Escape hatch**: `PI_VERDICT_JEV_URL` overrides the active transport's endpoint (OpenRouter's is an alpha API)
|
|
132
132
|
|
|
133
133
|
### Self-protection (the gate guards itself — [ADR-0001](docs/adr/0001-self-protection-layer.md))
|
|
134
134
|
|
package/README.zh-CN.md
CHANGED
|
@@ -111,7 +111,7 @@ pi-verdict 同时支持 [pi](https://github.com/badlogic/pi-mono) 与 [oh-my-pi]
|
|
|
111
111
|
- `denyPaths` 是你声明**受保护**的普通路径列表:触碰触发**终局 ask** 由你裁决(非交互降级 deny);分类器只被告知路径**存在**,路径明文永不出本机。`grep`/`find`/`ls` 按**整个搜索范围**比较:省略 `path`(pi 默认:当前目录)或传入位于声明路径之上的父目录,同样触发 ask。全新安装会预填一份**入门列表**(`~/.ssh/`、`~/.gnupg`、`~/.mc`、shell rc/profile 文件),自初次运行后的第一个会话起生效(一切配置变更均自新会话生效)——它是预填的*用户声明*而非内置 floor:可随意增删清空,也可与自己的路径(`~/Documents/private`、……)并列;既有配置永不被改写
|
|
112
112
|
- `builtinDenyFloor: false` 整体关闭内置危险/路径拦截(风险自担;下方自保护层永远开启)
|
|
113
113
|
- `classifierModel` 指定分类器模型,如 `"zai/glm-5.3-flash:low"`(支持思考后缀;缺省 = 会话模型且显式关思考)
|
|
114
|
-
- `classifierModel: "typesafe/jev-latest"` 启用随包的 **jev 决策适配器**——灰区裁决经
|
|
114
|
+
- `classifierModel: "typesafe/jev-latest"` 启用随包的 **jev 决策适配器**——灰区裁决经 TypeSafe jev 完成(默认 OpenRouter,或 `PI_VERDICT_JEV_TRANSPORT=typesafe` 直连官方 API);实验性质,详见 [ADR-0003](docs/adr/0003-jev-decisions-adapter.md)
|
|
115
115
|
- `audit: true` 把每次**灰区裁决**(发给分类器的完整转录、其原始响应、解析出的裁决)以 JSONL 记录到 `~/.pi/agent/verdicts/<sessionId>.jsonl`——按会话一分文件,保留最近 20 个。仅存本机且全保真(受保护路径明文可能出现——永不出本机;[ADR-0002](docs/adr/0002-deny-paths-deterministic-ask.md) 边界注);agent 对该目录读写双拒。开启时 `/automode` 会显示审计状态与路径
|
|
116
116
|
- `notifyAllows: true` 对每次 **classifier 放行**发通知(reason + action 行——如 jev 的概率分解);默认 `false` 保持放行静默。机械放行(你自己的 allow 规则、protected-path 确认)永不通知;shadow 标注仍属 debug;两开关同开时通知只出现一次
|
|
117
117
|
|
|
@@ -120,17 +120,17 @@ pi-verdict 同时支持 [pi](https://github.com/badlogic/pi-mono) 与 [oh-my-pi]
|
|
|
120
120
|
### Jev 决策后端(实验性——[ADR-0003](docs/adr/0003-jev-decisions-adapter.md))
|
|
121
121
|
|
|
122
122
|
1. 安装含适配器的版本( v0.8 及以上): `pi install npm:pi-verdict`
|
|
123
|
-
2.
|
|
124
|
-
- pi 内执行 `/login openrouter`
|
|
125
|
-
-
|
|
123
|
+
2. 选一条 transport(两条走同一 decisions wire 契约):
|
|
124
|
+
- **OpenRouter(默认)**: pi 内执行 `/login openrouter`,或 shell 里 `export OPENROUTER_API_KEY=sk-or-v1...`
|
|
125
|
+
- **TypeSafe 直连(官方 v1 API)**: 在 console.typesafe.ai 自助发 key,然后 `export TYPESAFE_API_KEY=apikey_...` 并 `export PI_VERDICT_JEV_TRANSPORT=typesafe`
|
|
126
126
|
3. 把分类器指到 jev(新会话生效)
|
|
127
127
|
- 持久:在 pi 之外编辑 `~/.pi/agent/config/pi-verdict.json` 并设置 `{ "classifierModel": "typesafe/jev-latest" }`
|
|
128
128
|
- 或者临时试一把:`PI_AUTO_MODE_MODEL=typesafe/jev-latest pi`
|
|
129
129
|
|
|
130
130
|
**限制**:
|
|
131
|
-
- **
|
|
131
|
+
- **Transport**: OpenRouter decisions(默认)或 TypeSafe 直连——TypeSafe 侧单次成本显示 $0(其 API 不返回 cost)
|
|
132
132
|
- **宿主**:仅支持pi。omp 上该设置会警告并回退会话模型。也绝不能选作会话主模型(不生成文本,选中即警告)
|
|
133
|
-
- **逃生口**:`PI_VERDICT_JEV_URL`
|
|
133
|
+
- **逃生口**:`PI_VERDICT_JEV_URL` 可覆盖当前 transport 的端点(OpenRouter 侧为 alpha 接口)
|
|
134
134
|
|
|
135
135
|
### 自保护(门禁守护自身——[ADR-0001](docs/adr/0001-self-protection-layer.md))
|
|
136
136
|
|
|
@@ -2,18 +2,29 @@
|
|
|
2
2
|
* pi-verdict jev adapter (ADR-0003) — exposes TypeSafe's jev decisions model
|
|
3
3
|
* as a pi provider (`typesafe/jev-latest`) so `classifierModel` can name it.
|
|
4
4
|
*
|
|
5
|
-
* jev is not an LLM:
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* jev is not an LLM: its decisions API takes `{state, questions}` and returns
|
|
6
|
+
* typed answers, which is why the model cannot ride pi's chat-completions
|
|
7
|
+
* providers. Two transports (PI_VERDICT_JEV_TRANSPORT, default `openrouter`),
|
|
8
|
+
* whose wire contracts are isomorphic except for the model slug
|
|
9
|
+
* (live-verified 2026-09-19: same `{state, questions}` body; answers carry
|
|
10
|
+
* choice/probabilities/confidence; usage snake_case, TypeSafe's own API omits
|
|
11
|
+
* `cost` and mapUsage defaults it to 0):
|
|
12
|
+
* - `openrouter`: POST /api/alpha/decisions, model `~typesafe/jev-latest`,
|
|
13
|
+
* credentials reuse pi's OpenRouter login with OPENROUTER_API_KEY fallback
|
|
14
|
+
* (no second credential channel);
|
|
15
|
+
* - `typesafe`: POST api.typesafe.ai/v1/systemone, model `jev-latest` —
|
|
16
|
+
* TypeSafe's official v1 API. pi has no typesafe login, so TYPESAFE_API_KEY
|
|
17
|
+
* is this transport's only source, still resolved through the provider
|
|
18
|
+
* auth pipeline rather than a bare fetch (ADR-0003 amendment).
|
|
10
19
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
20
|
+
* This adapter translates the classifier's completion call into one `choice`
|
|
21
|
+
* question and synthesizes the `<verdict>…</verdict>` contract text from the
|
|
22
|
+
* typed answer. The transport is pinned at provider creation (env is
|
|
23
|
+
* process-constant), so provider metadata, auth, and request routing always
|
|
24
|
+
* agree. Because `hasConfiguredAuth` reads a sync snapshot built
|
|
25
|
+
* before any extension event fires, the provider is re-registered on
|
|
26
|
+
* `session_start` to re-run the availability check with the stashed
|
|
27
|
+
* resolver (see ADR-0003).
|
|
17
28
|
*
|
|
18
29
|
* Known limitations (ADR-0003): the classifier system prompt — including the
|
|
19
30
|
* denyPaths existence hint — does not reach jev; jev treats state as data and
|
|
@@ -36,11 +47,63 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
36
47
|
|
|
37
48
|
export const PROVIDER_ID = "typesafe";
|
|
38
49
|
export const MODEL_ID = "jev-latest";
|
|
39
|
-
/** Wire slug OpenRouter resolves to the newest jev snapshot. */
|
|
40
|
-
export const WIRE_MODEL = "~typesafe/jev-latest";
|
|
41
50
|
export const API_ID = "jev-decisions";
|
|
42
|
-
|
|
43
|
-
export const
|
|
51
|
+
|
|
52
|
+
export const TRANSPORTS = ["openrouter", "typesafe"] as const;
|
|
53
|
+
export type Transport = (typeof TRANSPORTS)[number];
|
|
54
|
+
|
|
55
|
+
/** Everything that differs between transports, in one place: the decisions
|
|
56
|
+
* endpoint, the model slug it expects (OpenRouter wants the `~latest` alias;
|
|
57
|
+
* TypeSafe's own API wants the bare slug), the provider/auth display names,
|
|
58
|
+
* the credential sources, and the missing-key error hint. PI_VERDICT_JEV_URL
|
|
59
|
+
* overrides either endpoint. */
|
|
60
|
+
export interface TransportConfig {
|
|
61
|
+
/** Decisions endpoint (PI_VERDICT_JEV_URL overrides). */
|
|
62
|
+
url: string;
|
|
63
|
+
/** Model slug this endpoint expects. */
|
|
64
|
+
wireModel: string;
|
|
65
|
+
providerName: string;
|
|
66
|
+
authName: string;
|
|
67
|
+
/** Env var carrying the API key. */
|
|
68
|
+
keyEnv: "OPENROUTER_API_KEY" | "TYPESAFE_API_KEY";
|
|
69
|
+
/** Pi provider-auth id when a pi login exists to reuse; absent = env-only. */
|
|
70
|
+
loginProvider?: "openrouter";
|
|
71
|
+
/** Completes "no API key resolved (…)". */
|
|
72
|
+
keyHint: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const TRANSPORT_DEFAULTS: Record<Transport, TransportConfig> = {
|
|
76
|
+
openrouter: {
|
|
77
|
+
url: "https://openrouter.ai/api/alpha/decisions",
|
|
78
|
+
wireModel: "~typesafe/jev-latest",
|
|
79
|
+
providerName: "TypeSafe (jev via OpenRouter)",
|
|
80
|
+
authName: "OpenRouter credentials (reused for jev)",
|
|
81
|
+
keyEnv: "OPENROUTER_API_KEY",
|
|
82
|
+
loginProvider: "openrouter",
|
|
83
|
+
keyHint: "openrouter login or OPENROUTER_API_KEY",
|
|
84
|
+
},
|
|
85
|
+
typesafe: {
|
|
86
|
+
url: "https://api.typesafe.ai/v1/systemone",
|
|
87
|
+
wireModel: "jev-latest",
|
|
88
|
+
providerName: "TypeSafe (jev direct)",
|
|
89
|
+
authName: "TYPESAFE_API_KEY",
|
|
90
|
+
keyEnv: "TYPESAFE_API_KEY",
|
|
91
|
+
keyHint: "TYPESAFE_API_KEY",
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Unknown or unset values fall back to `openrouter` (the historical default). */
|
|
96
|
+
export function activeTransport(): Transport {
|
|
97
|
+
return process.env.PI_VERDICT_JEV_TRANSPORT?.trim().toLowerCase() === "typesafe" ? "typesafe" : "openrouter";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function decisionsUrl(transport: Transport = activeTransport()): string {
|
|
101
|
+
return process.env.PI_VERDICT_JEV_URL?.trim() || TRANSPORT_DEFAULTS[transport].url;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function wireModel(transport: Transport = activeTransport()): string {
|
|
105
|
+
return TRANSPORT_DEFAULTS[transport].wireModel;
|
|
106
|
+
}
|
|
44
107
|
|
|
45
108
|
const VERDICTS = ["allow", "ask", "deny"] as const;
|
|
46
109
|
type Verdict = (typeof VERDICTS)[number];
|
|
@@ -84,8 +147,8 @@ export function extractState(context: { messages: unknown[] }): string {
|
|
|
84
147
|
return state;
|
|
85
148
|
}
|
|
86
149
|
|
|
87
|
-
export function buildDecisionsBody(state: string,
|
|
88
|
-
return { model
|
|
150
|
+
export function buildDecisionsBody(state: string, model: string = wireModel()): Record<string, unknown> {
|
|
151
|
+
return { model, state, questions: VERDICT_QUESTIONS };
|
|
89
152
|
}
|
|
90
153
|
|
|
91
154
|
interface DecisionAnswer {
|
|
@@ -130,7 +193,7 @@ function mapUsage(u: unknown): AssistantMessage["usage"] {
|
|
|
130
193
|
};
|
|
131
194
|
}
|
|
132
195
|
|
|
133
|
-
function streamDecisions(model: Model<string>, context: Context, options: StreamOptions | SimpleStreamOptions | undefined, fetcher: typeof fetch): AssistantMessageEventStream {
|
|
196
|
+
function streamDecisions(transport: Transport, model: Model<string>, context: Context, options: StreamOptions | SimpleStreamOptions | undefined, fetcher: typeof fetch): AssistantMessageEventStream {
|
|
134
197
|
const stream = createAssistantMessageEventStream();
|
|
135
198
|
void (async () => {
|
|
136
199
|
const output: AssistantMessage = {
|
|
@@ -146,11 +209,11 @@ function streamDecisions(model: Model<string>, context: Context, options: Stream
|
|
|
146
209
|
try {
|
|
147
210
|
stream.push({ type: "start", partial: output });
|
|
148
211
|
const apiKey = options?.apiKey;
|
|
149
|
-
if (!apiKey) throw new Error(
|
|
150
|
-
const response = await fetcher(
|
|
212
|
+
if (!apiKey) throw new Error(`jev adapter: no API key resolved (${TRANSPORT_DEFAULTS[transport].keyHint})`);
|
|
213
|
+
const response = await fetcher(decisionsUrl(transport), {
|
|
151
214
|
method: "POST",
|
|
152
215
|
headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
|
|
153
|
-
body: JSON.stringify(buildDecisionsBody(extractState(context))),
|
|
216
|
+
body: JSON.stringify(buildDecisionsBody(extractState(context), wireModel(transport))),
|
|
154
217
|
signal: options?.signal,
|
|
155
218
|
});
|
|
156
219
|
const text = await response.text();
|
|
@@ -181,50 +244,62 @@ function streamDecisions(model: Model<string>, context: Context, options: Stream
|
|
|
181
244
|
return stream;
|
|
182
245
|
}
|
|
183
246
|
|
|
184
|
-
/** Input $0.042/MTok, output free (research/typesafe-jev-classifiermodel.md
|
|
185
|
-
*
|
|
247
|
+
/** Input $0.042/MTok, output free (research/typesafe-jev-classifiermodel.md).
|
|
248
|
+
* OpenRouter settles per-call cost in usage; TypeSafe's own API omits it and
|
|
249
|
+
* mapUsage defaults it to 0. Context ceiling is undocumented upstream;
|
|
186
250
|
* 30k matches the classifier transcript budget with margin. */
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
251
|
+
function jevModel(transport: Transport): Model<typeof API_ID> {
|
|
252
|
+
return {
|
|
253
|
+
id: MODEL_ID,
|
|
254
|
+
name: "Jev (latest, decisions)",
|
|
255
|
+
api: API_ID,
|
|
256
|
+
provider: PROVIDER_ID,
|
|
257
|
+
baseUrl: decisionsUrl(transport),
|
|
258
|
+
reasoning: false,
|
|
259
|
+
input: ["text"],
|
|
260
|
+
cost: { input: 0.042, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
261
|
+
contextWindow: 30_000,
|
|
262
|
+
maxTokens: 512,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
199
265
|
|
|
200
266
|
type OpenRouterKeyResolver = () => Promise<string | undefined>;
|
|
201
267
|
|
|
202
268
|
export function createJevProvider(openRouterKey: OpenRouterKeyResolver | undefined, fetcher: typeof fetch = fetch): Provider {
|
|
269
|
+
// Transport is pinned at creation: env is constant for the process
|
|
270
|
+
// lifetime, and pinning keeps provider metadata, auth, and request
|
|
271
|
+
// routing in agreement (no half-switched state).
|
|
272
|
+
const transport = activeTransport();
|
|
273
|
+
const config = TRANSPORT_DEFAULTS[transport];
|
|
203
274
|
return createProvider({
|
|
204
275
|
id: PROVIDER_ID,
|
|
205
|
-
name:
|
|
206
|
-
baseUrl:
|
|
276
|
+
name: config.providerName,
|
|
277
|
+
baseUrl: decisionsUrl(transport),
|
|
207
278
|
auth: {
|
|
208
|
-
// Ambient-only (no login):
|
|
209
|
-
// login or the env fallback
|
|
279
|
+
// Ambient-only (no login): the openrouter transport reuses pi's
|
|
280
|
+
// OpenRouter login or the env fallback; the typesafe transport has
|
|
281
|
+
// no pi credential store (pi has no typesafe provider) and reads
|
|
282
|
+
// TYPESAFE_API_KEY only. Neither path opens a second channel.
|
|
210
283
|
apiKey: {
|
|
211
|
-
name:
|
|
284
|
+
name: config.authName,
|
|
212
285
|
resolve: async () => {
|
|
213
286
|
let key: string | undefined;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
287
|
+
if (config.loginProvider) {
|
|
288
|
+
try {
|
|
289
|
+
key = await openRouterKey?.();
|
|
290
|
+
} catch {
|
|
291
|
+
/* getProviderAuth may reject on auth-store errors; env still applies */
|
|
292
|
+
}
|
|
218
293
|
}
|
|
219
|
-
key ||= process.env.
|
|
220
|
-
return key ? { auth: { apiKey: key }, source:
|
|
294
|
+
key ||= process.env[config.keyEnv]?.trim();
|
|
295
|
+
return key ? { auth: { apiKey: key }, source: transport } : undefined;
|
|
221
296
|
},
|
|
222
297
|
},
|
|
223
298
|
},
|
|
224
|
-
models: [
|
|
299
|
+
models: [jevModel(transport)],
|
|
225
300
|
api: {
|
|
226
|
-
stream: (m, c, o) => streamDecisions(m, c, o, fetcher),
|
|
227
|
-
streamSimple: (m, c, o) => streamDecisions(m, c, o, fetcher),
|
|
301
|
+
stream: (m, c, o) => streamDecisions(transport, m, c, o, fetcher),
|
|
302
|
+
streamSimple: (m, c, o) => streamDecisions(transport, m, c, o, fetcher),
|
|
228
303
|
},
|
|
229
304
|
});
|
|
230
305
|
}
|