pi-pignon 0.1.1 → 0.1.2
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 +100 -411
- package/docs/CONFIGURATION.md +234 -0
- package/docs/DESIGN.md +49 -0
- package/docs/PLAN-command-output.md +161 -0
- package/docs/PLAN-deciders.md +327 -0
- package/docs/assets/pignon.svg +14 -0
- package/package.json +2 -1
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Plan (pignon): pluggable deciders (local Laya + remote Jev) and a user-defined routing table
|
|
2
|
+
|
|
3
|
+
Status: proposal · 2026-09-23
|
|
4
|
+
|
|
5
|
+
## Goals
|
|
6
|
+
|
|
7
|
+
1. **Remote decider.** Route with TypeSafe's Jev (hosted, via `@typesafe-ai/sdk`) as well
|
|
8
|
+
as the local Laya worker. Jev is used as a *fallback*: when Laya is unavailable, errors,
|
|
9
|
+
or answers below a confidence floor.
|
|
10
|
+
2. **User-owned routing table.** Move the model map and the difficulty tiers out of
|
|
11
|
+
`types.ts` into config. Users name their own models and write their own ordered list of
|
|
12
|
+
difficulty tiers (2..N, with the criterion text sent to the decider).
|
|
13
|
+
3. **Publishable.** Easy setup, a schema-checked config, docs, tests, and modules small
|
|
14
|
+
enough to read one at a time.
|
|
15
|
+
|
|
16
|
+
Non-goals (for now): fine-tuning Laya, per-project config, and asking questions other than
|
|
17
|
+
tier and form.
|
|
18
|
+
|
|
19
|
+
## What stays the same
|
|
20
|
+
|
|
21
|
+
- The routing policy (`policy.ts`) keeps its algorithm: confidence gates, cooldown, payback
|
|
22
|
+
and cache guard, fail-open.
|
|
23
|
+
- Pi wiring: modes (shadow/live/off), manual pin, decision cards, `/pignon-stats` (renamed from `/laya*`, kept as aliases for one release).
|
|
24
|
+
- The Laya worker protocol and the Python worker. Only the TS client moves.
|
|
25
|
+
- Prompt privacy on the local path (the hash and length are logged, never the text).
|
|
26
|
+
|
|
27
|
+
## Facts that constrain the design (from the SDK v0.6.0 `.d.ts`)
|
|
28
|
+
|
|
29
|
+
| Fact | Consequence |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `TypeSafeClient({ apiKey, baseURL, defaultModel, timeout, retry, logger, logLevel, fetch })` | `fetch` can be injected, so tests need no network. `baseURL: "https://openrouter.ai/api"` + an OpenRouter key also works. |
|
|
32
|
+
| Env fallbacks: `TYPESAFE_API_KEY`, `TYPESAFE_BASE_URL`, `TYPESAFE_DEFAULT_MODEL` | Nothing to configure for the common case. |
|
|
33
|
+
| `timeout` applies **per attempt**, default 2 retries, no total budget | Set `retry.maxRetries: 0` (or 1) and bound the whole call with our own `AbortSignal.timeout`. |
|
|
34
|
+
| Default logger is `console`, and `debug` logs request **bodies** | Pass our own logger (into the `/pignon log` ring buffer), cap `logLevel` at `warn`, so nothing draws over the TUI and prompts never reach logs. |
|
|
35
|
+
| `ChoiceResponse` = `{ choice, confidence, probabilities }`, **no `type` field** | The shared parser must not require `type === "choice"` (the local worker's `choiceOf` does today). |
|
|
36
|
+
| `usage` has tokens; cost comes back at runtime but is not typed | Record `costUsd` when present and show it in stats. |
|
|
37
|
+
| 70–500 ms latency, 32k-token state limit, input-billed | Separate timeout per decider (Jev ≈ 1500 ms). Keep the 4 000-char prompt cap for both deciders. |
|
|
38
|
+
| Error classes: `AuthenticationError`, `RateLimitError`, `APITimeoutError`, `APIUserAbortError`… | Map them to short status texts ("jev: bad API key", "jev: rate limited"). |
|
|
39
|
+
|
|
40
|
+
## Architecture
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
┌──────────────── extension.ts (Pi wiring only) ────────────────┐
|
|
44
|
+
prompt ───► │ router.ts: routePrompt() │
|
|
45
|
+
│ deciders/* ──► RoutingDecision ──► policy.decide() ──► setModel
|
|
46
|
+
└───────────────────────────────────────────────────────────────┘
|
|
47
|
+
|
|
48
|
+
deciders/
|
|
49
|
+
types.ts Decider interface, DecisionQuestions, RawAnswers
|
|
50
|
+
questions.ts build the tier/form questions from config (single source)
|
|
51
|
+
parse.ts RawAnswers → RoutingDecision (shared, tolerant)
|
|
52
|
+
laya-local.ts LayaWorker (today's laya-worker.ts) implementing Decider
|
|
53
|
+
jev.ts TypeSafeClient adapter implementing Decider
|
|
54
|
+
fallback.ts FallbackDecider: tries deciders in order, escalating on low confidence
|
|
55
|
+
create.ts factory: config → Decider
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### The `Decider` seam
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
export interface Decider {
|
|
62
|
+
readonly id: string; // "laya-local" | "jev" | "fallback(laya-local→jev)"
|
|
63
|
+
readonly label: string; // shown on the spinner and card, e.g. "jev-1.13.0"
|
|
64
|
+
readonly isReady: boolean; // local: model warm; remote: key present
|
|
65
|
+
readonly remote: boolean; // prompt leaves the machine → shown on the card
|
|
66
|
+
warmup(signal?: AbortSignal): Promise<void>;
|
|
67
|
+
decide(q: DecisionQuestions, text: string, signal: AbortSignal): Promise<DeciderResult>;
|
|
68
|
+
stop(): void;
|
|
69
|
+
readonly recentLogs: readonly string[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface DeciderResult {
|
|
73
|
+
answers: RawAnswers; // { reasoning_demand: {choice, confidence}, needs_exploration: … }
|
|
74
|
+
deciderId: string; // which decider actually answered
|
|
75
|
+
model: string; // checkpoint / jev model version
|
|
76
|
+
latencyMs: number;
|
|
77
|
+
costUsd?: number;
|
|
78
|
+
attempts: Attempt[]; // one per decider tried (for the card and stats)
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`parse.ts` turns `answers` into `RoutingDecision` against the **configured** tier ids, so the
|
|
83
|
+
policy never sees decider-specific shapes.
|
|
84
|
+
|
|
85
|
+
### Fallback semantics (`FallbackDecider`)
|
|
86
|
+
|
|
87
|
+
Config order is the try order. For each decider:
|
|
88
|
+
|
|
89
|
+
1. Skip it if it is not ready (e.g. Laya still loading) → record `skipped: not ready`.
|
|
90
|
+
2. Call it with its own timeout, under the caller's signal.
|
|
91
|
+
3. Accept the answer if `tierConfidence >= escalateBelow` **or** it is the last decider.
|
|
92
|
+
4. Otherwise escalate: record the attempt and try the next one.
|
|
93
|
+
|
|
94
|
+
If every decider fails, rethrow → the policy fails open (as today). When the last decider
|
|
95
|
+
answers with *lower* confidence than an earlier one, keep the most confident answer. An
|
|
96
|
+
overall `budgetMs` (default 3000) caps total latency, so a slow Laya plus a slow Jev cannot
|
|
97
|
+
stall a prompt.
|
|
98
|
+
|
|
99
|
+
Defaults: `escalateBelow: 0.75`, and escalate on error and on not-ready.
|
|
100
|
+
|
|
101
|
+
## Configuration (v2)
|
|
102
|
+
|
|
103
|
+
One JSON file, as today (`~/.pi/agent/pignon.json` or `$PIGNON_CONFIG`; the old `laya-router.json` is read as a fallback with a rename hint). Every
|
|
104
|
+
key is optional; defaults reproduce today's behavior exactly.
|
|
105
|
+
|
|
106
|
+
```jsonc
|
|
107
|
+
{
|
|
108
|
+
"$schema": "https://unpkg.com/pignon/schema/config.schema.json",
|
|
109
|
+
"version": 2,
|
|
110
|
+
|
|
111
|
+
// 1. Deciders, in fallback order
|
|
112
|
+
"deciders": [
|
|
113
|
+
{ "type": "laya-local", "timeoutMs": 2500 }, // optional: model, python, workerDir…
|
|
114
|
+
{ "type": "jev", "timeoutMs": 1500, "model": "jev-1.13.0" } // apiKey from TYPESAFE_API_KEY
|
|
115
|
+
],
|
|
116
|
+
"strategy": { "mode": "sequential", "escalateBelow": 0.75 }, // see "Decision strategies"
|
|
117
|
+
|
|
118
|
+
// 2. Model aliases: define once, reference by name
|
|
119
|
+
"models": {
|
|
120
|
+
"flash": { "provider": "openrouter", "modelId": "deepseek/deepseek-v4-flash-0731", "thinking": "off" },
|
|
121
|
+
"flash-41": { "provider": "openrouter", "modelId": "deepseek/deepseek-v4.1-flash", "thinking": "low" },
|
|
122
|
+
"glm": { "provider": "openrouter", "modelId": "z-ai/glm-5.3", "thinking": "high" },
|
|
123
|
+
"hy4": { "provider": "openrouter", "modelId": "tencent/hy4-preview", "thinking": "low" }
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
// 3. Difficulty tiers, easiest first. The criterion is what the decider reads.
|
|
127
|
+
"tiers": [
|
|
128
|
+
{ "id": "trivial", "criterion": "Mechanical edit, rename, formatting, or a single factual lookup",
|
|
129
|
+
"model": "flash", "explorationAllowed": false },
|
|
130
|
+
{ "id": "standard", "criterion": "Localized change across a few files with clear intent",
|
|
131
|
+
"model": "flash-41" },
|
|
132
|
+
{ "id": "hard", "criterion": "Multi-step investigation, debugging with unclear cause, or cross-cutting design",
|
|
133
|
+
"direct": "glm", "exploration": "hy4" }
|
|
134
|
+
],
|
|
135
|
+
|
|
136
|
+
// Optional wording overrides; bump questionsVersion whenever you edit criteria.
|
|
137
|
+
"questions": { "version": "q1", "tierInstructions": "…", "explorationInstructions": "…" },
|
|
138
|
+
|
|
139
|
+
"thresholds": { "minConfidenceDowngrade": 0.85 }
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Rules:
|
|
144
|
+
|
|
145
|
+
- A tier gives either `model` (both forms) or `direct` + `exploration`. A value can be an
|
|
146
|
+
alias or an inline `{ provider, modelId, thinking }`.
|
|
147
|
+
- `explorationAllowed: false` generalizes today's hard-coded "exploration forbids trivial"
|
|
148
|
+
rule: an exploration task at that tier goes up to the next tier that allows it.
|
|
149
|
+
- Need 2..8 tiers with unique ids. Unknown alias → error naming the tier.
|
|
150
|
+
- `version` missing + the old `tiers: { hard: { direct: … } }` object shape → migrate in
|
|
151
|
+
memory and notify once ("config uses v1 format; run `/pignon config migrate`").
|
|
152
|
+
- Invalid config: same contract as today. Report it on session start, fall back per
|
|
153
|
+
section, never block loading.
|
|
154
|
+
- **Secrets never go in the file.** `jev.apiKeyEnv` (default `TYPESAFE_API_KEY`) names the
|
|
155
|
+
variable. A literal `apiKey` is rejected with a message.
|
|
156
|
+
|
|
157
|
+
### Validation
|
|
158
|
+
|
|
159
|
+
Use TypeBox (see "Validation with TypeBox" below). The schema module is the single source
|
|
160
|
+
for the TS types, the runtime checks, and `schema/config.schema.json`.
|
|
161
|
+
|
|
162
|
+
### Presets and onboarding
|
|
163
|
+
|
|
164
|
+
- `presets/openrouter.json` (today's table), `presets/anthropic.json`, `presets/openai.json`.
|
|
165
|
+
Config can say `"extends": "openrouter"` and override only what differs.
|
|
166
|
+
- `/pignon init [preset]` writes a starter config (it refuses to overwrite an existing file),
|
|
167
|
+
lists the models from `ctx.modelRegistry` that resolve, and flags those that do not.
|
|
168
|
+
- `/pignon doctor` checks each decider (worker venv present? Apple Silicon? key set? one test
|
|
169
|
+
`decide` round-trip) and each table model (in registry? auth?), with one ✓/✗ line per check.
|
|
170
|
+
- `/pignon config` shows the resolved table (tier × form → model · thinking) and its source.
|
|
171
|
+
|
|
172
|
+
## Code changes, file by file
|
|
173
|
+
|
|
174
|
+
| File | Change |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `src/types.ts` | Drop `Tier` union, `DEFAULT_TIERS`, `TIER_ORDER`; keep protocol + policy types. `Profile.tier: TierId` (string). Add `DeciderAttempt`, extend `RouterLogEntry` with `decider`, `escalated`, `attempts`, `costUsd`, `questionsVersion`. |
|
|
177
|
+
| `src/config/schema.ts` | TypeBox schema, v1→v2 migration. |
|
|
178
|
+
| `src/config/load.ts` | Today's `loadConfig` (file read, error collection) on top of the schema; resolves `extends` and aliases into a `ResolvedConfig` whose `table` is an ordered array. |
|
|
179
|
+
| `src/config/defaults.ts` | Default models, tiers, and deciders (`[laya-local]` so behavior does not change on upgrade). |
|
|
180
|
+
| `src/deciders/*` | See the architecture section. `laya-worker.ts` moves to `deciders/laya-local.ts` with its public API unchanged, plus a thin `Decider` adapter. |
|
|
181
|
+
| `src/policy.ts` | Tier order comes from `config.table` (index = rank). The trivial/exploration rule reads `explorationAllowed`. `profileFromModel` takes the table. |
|
|
182
|
+
| `src/router.ts` | New: `routePrompt` + `buildLogEntry` pulled out of `extension.ts` (Pi-free except for a small `RouterHost` interface: `findModel`, `setModel`, `setThinking`, `contextTokens`), so it can be tested without Pi mocks. |
|
|
183
|
+
| `src/extension.ts` | Wiring only: build the decider from config, register hooks and commands. Spinner/status text use `decider.label`. |
|
|
184
|
+
| `src/ui.ts` | Card head shows the decider that answered (`laya` / `jev ☁`), escalation (`laya 0.62 → jev 0.91`), and cost. Stats grid rows come from the config tiers. |
|
|
185
|
+
| `package.json` | `@typesafe-ai/sdk` and `typebox` in `dependencies`; `files`, `license`, `repository`, `keywords: ["pi-package", "pi-extension"]`; drop `private`. |
|
|
186
|
+
|
|
187
|
+
The old `~/.pi/agent/extensions/jev-router` becomes redundant: its behavior equals
|
|
188
|
+
`deciders: [{ "type": "jev" }]`. Remove it after migration so two routers don't both fire.
|
|
189
|
+
|
|
190
|
+
## Tests
|
|
191
|
+
|
|
192
|
+
| Suite | Covers |
|
|
193
|
+
|---|---|
|
|
194
|
+
| `deciders/contract.test.ts` | One shared suite run against every `Decider` (Laya via a fake spawn, Jev via an injected `fetch`): ready/not-ready, abort, timeout, malformed answers, `stop()`. |
|
|
195
|
+
| `deciders/jev.test.ts` | Request body (questions from config, 4 000-char cap, model pin); SDK errors → short messages; `maxRetries` 0; no console output; cost parsing; missing key → `isReady=false`, never throws at construction. |
|
|
196
|
+
| `deciders/fallback.test.ts` | Escalation on low confidence, error, and not-ready; keeps the best answer; overall budget; caller abort stops the chain; `attempts` recorded. |
|
|
197
|
+
| `config/*.test.ts` | Schema accept/reject tables, alias resolution, `extends`, v1 migration, literal-apiKey rejection, per-section fallback, JSON Schema file up to date. |
|
|
198
|
+
| `policy.test.ts` | Existing 38 cases kept, parameterized over a 2-tier and a 4-tier table; `explorationAllowed`. |
|
|
199
|
+
| `router.test.ts` | End-to-end through `RouterHost` fakes (moves most of `extension-routing.test.ts`). |
|
|
200
|
+
| `extension.test.ts` | Commands incl. `init`, `doctor`, `config`. |
|
|
201
|
+
| `live.test.ts` | Skipped unless `TYPESAFE_API_KEY` is set (`npm run test:live`): one real Jev call checks the response shape still matches the parser. |
|
|
202
|
+
|
|
203
|
+
`npm run check` = typecheck + unit + worker tests + schema freshness. No network in `check`.
|
|
204
|
+
|
|
205
|
+
## Docs
|
|
206
|
+
|
|
207
|
+
- `README.md`, restructured: 60-second quickstart (Jev only: set the key, `/pignon init`,
|
|
208
|
+
`/pignon live`), then "Add the local Laya model (Apple Silicon)", then Configuration
|
|
209
|
+
reference, Commands, How routing decides, Privacy, Troubleshooting (`/pignon doctor`).
|
|
210
|
+
- **Privacy section:** local Laya keeps prompts on the machine. Jev sends the first 4 000
|
|
211
|
+
characters of each routed prompt to TypeSafe (or OpenRouter). The card marks remote
|
|
212
|
+
decisions with ☁.
|
|
213
|
+
- `docs/configuration.md`: every key, generated tables from the schema descriptions.
|
|
214
|
+
- `docs/writing-tiers.md`: how to write criteria, why to bump `questions.version`, and how
|
|
215
|
+
to calibrate thresholds from `/laya-stats` in shadow mode.
|
|
216
|
+
- `CHANGELOG.md`, `LICENSE`, and `examples/*.json`.
|
|
217
|
+
|
|
218
|
+
## Phases (each ends green on `npm run check`)
|
|
219
|
+
|
|
220
|
+
1. ✅ **Seam, no behavior change.** `git init`; add the `Decider` interface; wrap `LayaWorker`;
|
|
221
|
+
move parsing to `parse.ts`; extract `router.ts`. Existing tests keep passing.
|
|
222
|
+
2. ✅ **Configurable table.** TypeBox schema, aliases, ordered tiers, questions built from config,
|
|
223
|
+
v1 migration, dynamic tiers in policy/UI/stats.
|
|
224
|
+
3. ✅ **Jev decider.** SDK adapter, error mapping, logger/retry hardening, contract + unit tests.
|
|
225
|
+
4. ✅ **Strategies.** sequential + parallel, budget, `/pignon-stats compare|export`, attempts on card and log, stats
|
|
226
|
+
per decider (escalation rate, cost).
|
|
227
|
+
5. ✅ **Onboarding.** Presets + `extends`, `/pignon init|doctor|config`, JSON Schema generation.
|
|
228
|
+
6. ✅ **Publish.** ~~PyPI `pignon-laya` + uvx launcher~~ (dropped, see below) + protocol check, docs, package metadata, live test,
|
|
229
|
+
`npm pack` dry-run, and a test install into a clean `~/.pi` via `pi install`/symlink. Retire `jev-router`.
|
|
230
|
+
Released as git tag `v0.1.0` (`pi install git:github.com/siiick/pignon@v0.1.0`); npm is deferred, the package
|
|
231
|
+
is ready for it (only the Pi package gallery needs npm).
|
|
232
|
+
7. ✅ **laya-serve.** `laya-serve` decider over the Jev client (no key, local when on loopback), detected by
|
|
233
|
+
`/pignon init`, documented as the way to run Laya locally; `laya-local` becomes experimental.
|
|
234
|
+
|
|
235
|
+
## Decision (2026-09-23): laya-serve instead of publishing pignon-laya
|
|
236
|
+
|
|
237
|
+
The official `laya` package already ships `laya-serve`, a server speaking Jev's
|
|
238
|
+
`POST /v1/systemone`. Benchmarked through pignon's own deciders on 12 prompts × 5 rounds:
|
|
239
|
+
same tier and exploration answers on 12/12 (probabilities equal to ~0.001), p50 75 ms
|
|
240
|
+
against 61 ms for the MLX worker, 2–3 s restart (18 s on the very first run), 712 MB
|
|
241
|
+
installed against 258 MB. A stopped server refuses connections within milliseconds, so
|
|
242
|
+
prompts are never held. Publishing our own package would duplicate it for ~14 ms, so
|
|
243
|
+
`pignon-laya` stays in the repository, unpublished (PyPI's `Private :: Do Not Upload`
|
|
244
|
+
classifier), and the uvx launcher is removed. A `serve` command upstream in laya-mlx
|
|
245
|
+
would bring MLX speed to everyone; to propose there.
|
|
246
|
+
|
|
247
|
+
## Decisions (2026-09-23)
|
|
248
|
+
|
|
249
|
+
1. **Default deciders.** Laya if its runtime can start, else Jev if a key is set, else a
|
|
250
|
+
status that says "no decider, run `/pignon doctor`". Resolved once per session, and
|
|
251
|
+
`/pignon config` shows which one was picked.
|
|
252
|
+
2. **Name: `pignon`** (the sprocket on a bike cassette: the router changes sprockets between tiers). npm `pignon`, PyPI `pignon-laya`, commands `/pignon` and `/pignon-stats`, config `~/.pi/agent/pignon.json`, env prefix `PIGNON_` (`LAYA_*` stays for the worker itself).
|
|
253
|
+
3. **Validation: TypeBox**, not zod (see below).
|
|
254
|
+
4. **Both strategies**, `sequential` and `parallel`, with a comparison view for benchmarking.
|
|
255
|
+
|
|
256
|
+
## Validation with TypeBox (replaces the zod section)
|
|
257
|
+
|
|
258
|
+
TypeBox 1.x (`typebox` on npm, 1.3.x; Pi itself depends on 1.3.27) builds schemas that
|
|
259
|
+
*are* JSON Schema objects:
|
|
260
|
+
|
|
261
|
+
- Types: `Static<typeof ConfigSchema>`.
|
|
262
|
+
- Checking: `Compile(ConfigSchema)` from `typebox/compile`. Its `.Errors(value)` gives
|
|
263
|
+
instance paths for the notify message.
|
|
264
|
+
- JSON Schema: `npm run schema` writes `JSON.stringify(ConfigSchema, null, 2)` to
|
|
265
|
+
`schema/config.schema.json`. No converter needed. A test fails if the file is stale.
|
|
266
|
+
- Add `typebox` as our own `dependency` (range `^1.3.27`). It is not hoisted from Pi.
|
|
267
|
+
|
|
268
|
+
## Decision strategies
|
|
269
|
+
|
|
270
|
+
```jsonc
|
|
271
|
+
"deciders": [ { "type": "laya-local" }, { "type": "jev" } ],
|
|
272
|
+
"strategy": {
|
|
273
|
+
"mode": "sequential", // or "parallel"
|
|
274
|
+
"escalateBelow": 0.75, // sequential: try the next decider below this confidence
|
|
275
|
+
"pick": "most-confident", // parallel: "most-confident" | "first" (list order wins when it answers)
|
|
276
|
+
"budgetMs": 3000 // both modes: total wall time
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
- **sequential** (default): the fallback behavior above. Cheapest; Jev is only called
|
|
281
|
+
when needed.
|
|
282
|
+
- **parallel**: every ready decider runs at once under `budgetMs`. The router then uses
|
|
283
|
+
one answer, chosen by `pick`. With `pick: "first"`, Laya stays authoritative and Jev
|
|
284
|
+
only runs alongside it for comparison: this is the benchmarking setup. Note that every
|
|
285
|
+
routed prompt then costs a Jev call and sends the prompt out.
|
|
286
|
+
- Every attempt is logged (`decider`, `model`, tier, confidences, latency, cost, error), so
|
|
287
|
+
the data is the same in both modes.
|
|
288
|
+
- `/pignon-stats compare` shows, over the session's parallel decisions, the tier agreement rate
|
|
289
|
+
and a Laya × Jev confusion matrix, the form agreement rate, mean confidence per decider,
|
|
290
|
+
p50/p95 latency, and total Jev cost. `/pignon-stats export` writes the attempts as JSONL
|
|
291
|
+
(hashes, not prompts) for offline analysis or future fine-tuning labels.
|
|
292
|
+
|
|
293
|
+
## Distributing the optional Laya runtime
|
|
294
|
+
|
|
295
|
+
Laya needs Python, `laya-mlx` (MLX), and Apple Silicon. Most users of a published
|
|
296
|
+
extension will have none of these. So the runtime must be **opt-in**, **outside the npm
|
|
297
|
+
package**, and it **must survive extension updates**.
|
|
298
|
+
|
|
299
|
+
| Option | Verdict |
|
|
300
|
+
|---|---|
|
|
301
|
+
| Ship `worker/` in the npm package; user runs `uv sync` inside `node_modules/…` | ✗ The venv is hidden, is wiped on every update, and the absolute paths break (already an issue today with `rsync`). |
|
|
302
|
+
| Prebuilt binary (PyInstaller) as an optional npm dependency | ✗ MLX + Metal make it large, it needs code signing and notarization, and it rebuilds on every laya-mlx release. |
|
|
303
|
+
| Docker | ✗ No Metal GPU in containers on macOS. |
|
|
304
|
+
| **Separate PyPI package, launched with `uvx`** | ✓ Recommended. |
|
|
305
|
+
|
|
306
|
+
**Recommended design:**
|
|
307
|
+
|
|
308
|
+
- Publish `worker/` to PyPI as `pignon-laya` (`[project.scripts] pignon-laya = "laya_worker:main"`,
|
|
309
|
+
dependency `laya-mlx`). It is a separate release artifact with its own version, and it
|
|
310
|
+
lives in the same repo.
|
|
311
|
+
- The npm package does **not** include `worker/` (`files` whitelist).
|
|
312
|
+
- Launch order in the `laya-local` decider:
|
|
313
|
+
1. `command` in config (dev: `["uv", "run", "--project", "./worker", "pignon-laya"]`);
|
|
314
|
+
2. `pignon-laya` on `PATH` (for `uv tool install pignon-laya` or `pipx`);
|
|
315
|
+
3. `uvx --from pignon-laya==<compatible range> pignon-laya`. It installs nothing up
|
|
316
|
+
front, uv caches the environment outside the extension, and it survives npm updates;
|
|
317
|
+
4. none of these → `isReady = false` with a clear reason; the strategy skips it.
|
|
318
|
+
- **Protocol handshake:** the `ready` line already carries `PROTOCOL_VERSION` (0.3.0).
|
|
319
|
+
The extension declares the major version it accepts and refuses a mismatch with an
|
|
320
|
+
"upgrade with `uv tool upgrade pignon-laya`" message instead of misparsing.
|
|
321
|
+
- **Platform gate:** on anything other than `darwin`/`arm64`, the decider reports
|
|
322
|
+
"unsupported platform" without trying to spawn anything.
|
|
323
|
+
- **Onboarding:** `/pignon laya install` runs `uv tool install pignon-laya` and then a warmup
|
|
324
|
+
(the first checkpoint download is about 850 MB; progress goes to the log widget). It
|
|
325
|
+
asks first and needs `uv` (doctor links to the uv installer).
|
|
326
|
+
- **Worker tests** stay in the Python package. `npm run check` still runs them in the repo.
|
|
327
|
+
- The existing env allowlist, the model pinning, and the stderr capture are unchanged.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0.5 0.5 99 99" width="100" height="100" role="img" aria-label="pignon: a bicycle sprocket">
|
|
2
|
+
<title>pignon</title>
|
|
3
|
+
<defs>
|
|
4
|
+
<linearGradient id="steel" x1="0" y1="0" x2="1" y2="1">
|
|
5
|
+
<stop offset="0" stop-color="#d3d9e0"/>
|
|
6
|
+
<stop offset="0.5" stop-color="#96a0ab"/>
|
|
7
|
+
<stop offset="1" stop-color="#5a6570"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
</defs>
|
|
10
|
+
<path fill="url(#steel)" fill-rule="evenodd" stroke="#46505a" stroke-width="0.8" stroke-linejoin="round"
|
|
11
|
+
d="M47.90,2.55 A2.1,2.1 0 0 1 52.10,2.55 L53.29,8.99 A4.4,4.4 0 0 0 61.75,10.57 L65.18,4.99 A2.1,2.1 0 0 1 69.10,6.51 L67.88,12.95 A4.4,4.4 0 0 0 75.20,17.48 L80.42,13.52 A2.1,2.1 0 0 1 83.52,16.35 L80.06,21.91 A4.4,4.4 0 0 0 85.24,28.78 L91.54,26.97 A2.1,2.1 0 0 1 93.41,30.73 L88.18,34.66 A4.4,4.4 0 0 0 90.53,42.94 L97.06,43.53 A2.1,2.1 0 0 1 97.44,47.71 L91.14,49.49 A4.4,4.4 0 0 0 90.34,58.06 L96.22,60.97 A2.1,2.1 0 0 1 95.07,65.01 L88.54,64.38 A4.4,4.4 0 0 0 84.71,72.09 L89.13,76.92 A2.1,2.1 0 0 1 86.60,80.27 L80.74,77.34 A4.4,4.4 0 0 0 74.38,83.14 L76.77,89.24 A2.1,2.1 0 0 1 73.20,91.45 L68.79,86.60 A4.4,4.4 0 0 0 60.77,89.71 L60.78,96.26 A2.1,2.1 0 0 1 56.66,97.03 L54.30,90.91 A4.4,4.4 0 0 0 45.70,90.91 L43.34,97.03 A2.1,2.1 0 0 1 39.22,96.26 L39.23,89.71 A4.4,4.4 0 0 0 31.21,86.60 L26.80,91.45 A2.1,2.1 0 0 1 23.23,89.24 L25.62,83.14 A4.4,4.4 0 0 0 19.26,77.34 L13.40,80.27 A2.1,2.1 0 0 1 10.87,76.92 L15.29,72.09 A4.4,4.4 0 0 0 11.46,64.38 L4.93,65.01 A2.1,2.1 0 0 1 3.78,60.97 L9.66,58.06 A4.4,4.4 0 0 0 8.86,49.49 L2.56,47.71 A2.1,2.1 0 0 1 2.94,43.53 L9.47,42.94 A4.4,4.4 0 0 0 11.82,34.66 L6.59,30.73 A2.1,2.1 0 0 1 8.46,26.97 L14.76,28.78 A4.4,4.4 0 0 0 19.94,21.91 L16.48,16.35 A2.1,2.1 0 0 1 19.58,13.52 L24.80,17.48 A4.4,4.4 0 0 0 32.12,12.95 L30.90,6.51 A2.1,2.1 0 0 1 34.82,4.99 L38.25,10.57 A4.4,4.4 0 0 0 46.71,8.99 Z M72.37,26.43 A32.5,32.5 0 0 1 81.96,55.92 L68.47,51.13 A18.5,18.5 0 0 0 64.28,38.23 Z M79.33,63.99 A32.5,32.5 0 0 1 54.24,82.22 L54.63,67.91 A18.5,18.5 0 0 0 65.60,59.94 Z M45.76,82.22 A32.5,32.5 0 0 1 20.67,63.99 L34.40,59.94 A18.5,18.5 0 0 0 45.37,67.91 Z M18.04,55.92 A32.5,32.5 0 0 1 27.63,26.43 L35.72,38.23 A18.5,18.5 0 0 0 31.53,51.13 Z M34.49,21.44 A32.5,32.5 0 0 1 65.51,21.44 L56.78,32.79 A18.5,18.5 0 0 0 43.22,32.79 Z M48.61,38.28 A11.8,11.8 0 0 1 51.39,38.28 L51.35,40.09 A10.0,10.0 0 0 1 53.78,40.74 L54.66,39.16 A11.8,11.8 0 0 1 57.06,40.55 L56.12,42.09 A10.0,10.0 0 0 1 57.91,43.88 L59.45,42.94 A11.8,11.8 0 0 1 60.84,45.34 L59.26,46.22 A10.0,10.0 0 0 1 59.91,48.65 L61.72,48.61 A11.8,11.8 0 0 1 61.72,51.39 L59.91,51.35 A10.0,10.0 0 0 1 59.26,53.78 L60.84,54.66 A11.8,11.8 0 0 1 59.45,57.06 L57.91,56.12 A10.0,10.0 0 0 1 56.12,57.91 L57.06,59.45 A11.8,11.8 0 0 1 54.66,60.84 L53.78,59.26 A10.0,10.0 0 0 1 51.35,59.91 L51.39,61.72 A11.8,11.8 0 0 1 48.61,61.72 L48.65,59.91 A10.0,10.0 0 0 1 46.22,59.26 L45.34,60.84 A11.8,11.8 0 0 1 42.94,59.45 L43.88,57.91 A10.0,10.0 0 0 1 42.09,56.12 L40.55,57.06 A11.8,11.8 0 0 1 39.16,54.66 L40.74,53.78 A10.0,10.0 0 0 1 40.09,51.35 L38.28,51.39 A11.8,11.8 0 0 1 38.28,48.61 L40.09,48.65 A10.0,10.0 0 0 1 40.74,46.22 L39.16,45.34 A11.8,11.8 0 0 1 40.55,42.94 L42.09,43.88 A10.0,10.0 0 0 1 43.88,42.09 L42.94,40.55 A11.8,11.8 0 0 1 45.34,39.16 L46.22,40.74 A10.0,10.0 0 0 1 48.65,40.09 Z"/>
|
|
12
|
+
<circle cx="50" cy="50" r="33.20" fill="none" stroke="#46505a" stroke-opacity="0.4" stroke-width="0.6"/>
|
|
13
|
+
<circle cx="50" cy="50" r="15.2" fill="none" stroke="#46505a" stroke-opacity="0.4" stroke-width="0.6"/>
|
|
14
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pignon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Pi coding agent extension that shifts to the right LLM for each prompt, using a local (Laya) or remote (Jev) decision model to judge task difficulty",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"src",
|
|
28
28
|
"schema",
|
|
29
29
|
"examples",
|
|
30
|
+
"docs",
|
|
30
31
|
"CHANGELOG.md"
|
|
31
32
|
],
|
|
32
33
|
"engines": {
|