@coool/pi-adaptive 0.0.0-stage → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 @coool
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,225 @@
1
- # Temporary Holding Version
1
+ # Pi Adaptive
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Not yet published. Release is gated on maintainer approval.
4
+
5
+ Capability-tier routing and bounded recovery for Pi. You choose the available
6
+ models and their tier order; Adaptive keeps provider failures separate from
7
+ capability gaps. Pi still owns the agent, tools, sessions, and model calls.
8
+
9
+ See the [Pi plan](../../docs/plans/pi-plugin.md) for the design and the
10
+ [test strategy](docs/TEST_STRATEGY.md) for the evidence each release requires.
11
+
12
+ ## Install
13
+
14
+ After publication:
15
+
16
+ ```sh
17
+ pi install npm:@coool/pi-adaptive
18
+ ```
19
+
20
+ The package declares Pi `>=1.5.0 <2`. The declared range is not itself a
21
+ compatibility test: the packed minimum (1.5.0) and latest Pi hosts run the
22
+ SDK, JSON, and real-terminal matrix before release, and `/adaptive doctor`
23
+ reports the last host version that matrix recorded. For local development, run
24
+ `pi install ./packages/pi` from the repository root after building.
25
+
26
+ ## How it decides
27
+
28
+ - **Use an explicit tier, not a classifier.** A user pin wins, then the agent's
29
+ `adaptive_tier` request under Auto; otherwise the tier is `standard`. No prompt
30
+ keywords or extra LLM call decide the tier.
31
+ - **Check fit before preference.** Only your configured, authenticated models
32
+ are candidates. Context size, input modalities, declared tool support, health,
33
+ and available capacity evidence constrain selection before tier order.
34
+ - **Keep productive work on its model.** Ordinary tool continuations stay on the
35
+ selected resource. A recovered preferred model becomes eligible at a later
36
+ safe boundary, not just to make an ongoing run cheaper.
37
+ - **Recover horizontally.** Rate limits, quota exhaustion, timeouts, and outages
38
+ use another eligible resource in the same tier, or wait/park. Availability
39
+ never silently borrows a weaker or stronger tier; 429s do not trip the breaker.
40
+ - **Escalate only on capability evidence.** `adaptive_escalate` requires a reason
41
+ and concrete evidence. Recording it switches nothing; a terminal capability
42
+ failure can continue the same task exactly one tier higher, within its budget.
43
+
44
+ ## Models and modes
45
+
46
+ | Picker entry | Meaning |
47
+ | ------------------- | ------------------------------------------------------------------------ |
48
+ | `adaptive/auto` | Use the requested tier, or `standard`; allow evidence-backed escalation. |
49
+ | `adaptive/quick` | Pin the quick tier; recover within it. |
50
+ | `adaptive/standard` | Pin the standard tier; recover within it. |
51
+ | `adaptive/advanced` | Pin the advanced tier; recover within it. |
52
+
53
+ **Observe is the default.** It explains what the same scheduler would choose
54
+ without changing a manually selected model. Choosing an Adaptive entry
55
+ activates routing for that session; choosing any normal model hands control back
56
+ immediately. With observation disabled and a normal model selected, Adaptive is
57
+ off. Off mode does not route or recover work.
58
+
59
+ Pinned tier entries ignore the agent's tier request and do not automatically
60
+ escalate by default. The Preferences dialog can explicitly enable
61
+ `escalateFromPinned`; evidence and task budgets still apply. A concrete model
62
+ pin always wins over automatic routing and recovery. Use Auto for the default
63
+ evidence-backed movement between tiers.
64
+
65
+ The agent tool `adaptive_tier` takes `quick`, `standard`, or `advanced`. Its
66
+ declaration applies from the next user prompt and stays on the session branch
67
+ until replaced. A new user prompt starts a new task at that base tier; it does
68
+ not inherit the last task's escalation. Forking or rewinding follows Pi's branch
69
+ state. `adaptive_escalate` is evidence reporting, not a planning tool.
70
+
71
+ ## Providers, capabilities, and settings
72
+
73
+ Configure credentials and models in Pi first. Adaptive uses Pi's available-model
74
+ registry; it does not add providers, fetch a marketplace, or invent tier
75
+ memberships. Do not run multiple automatic routers in the same session.
76
+
77
+ Settings live under Pi's agent directory, normally
78
+ `~/.pi/agent/adaptive/settings.json`. The shared settings include `mode`
79
+ (`off`, `observe`, `active`), ordered `tiers`, and a concrete-model `pin`
80
+ (`null` means unpinned). For example, replace these placeholders with your
81
+ actual Pi provider/model IDs:
82
+
83
+ ```json
84
+ {
85
+ "mode": "observe",
86
+ "tiers": {
87
+ "quick": [{ "providerID": "your-provider", "id": "your-quick-model" }],
88
+ "standard": [{ "providerID": "your-provider", "id": "your-standard-model" }],
89
+ "advanced": [{ "providerID": "your-provider", "id": "your-advanced-model" }]
90
+ },
91
+ "pin": null
92
+ }
93
+ ```
94
+
95
+ List multiple models in a tier in your preferred order for same-tier recovery.
96
+ Empty tiers do not authorize cross-tier fallback. The interactive tier editor
97
+ and the first-run proposal build tiers from available-model metadata, never
98
+ from prompt classification, and nothing is saved until you confirm.
99
+
100
+ A tier entry can include a Pi-supported thinking level as `variant`, such as
101
+ `"variant": "high"`. Admission checks that exact variant, and dispatch uses it;
102
+ unsupported variants are not silently replaced with a different thinking level.
103
+
104
+ Pi's catalogue does not declare model-level tool capability. Explicit
105
+ declarations live in `adaptive/capabilities.json` beside the settings file:
106
+
107
+ ```json
108
+ {
109
+ "models": [{ "providerID": "your-provider", "id": "your-model", "tools": true }]
110
+ }
111
+ ```
112
+
113
+ Declare support only after verifying that model's tool calls. Missing declarations
114
+ remain unknown; `tools: false` explicitly excludes a model from tool-enabled
115
+ requests. Declarations do not add providers, tier memberships, or credentials.
116
+ Malformed capability files produce field-specific errors rather than guessed
117
+ declarations. Capacity is evidence-driven: unknown capacity is not a promise of
118
+ remaining quota, and not every provider exposes usable capacity readings.
119
+
120
+ ## Status, notes, and commands
121
+
122
+ Adaptive uses one footer item for the current tier and model, recovery,
123
+ task escalation, observation of a manual model, tier setup, or degraded
124
+ operation. Waiting countdowns and parked-work status take priority in that same
125
+ item; parked work also has a card. A manual model pick or session change clears
126
+ stale status, and disabling observation clears its manual-model footer.
127
+
128
+ One-line transcript notes stay out of model context. The note verbosity choices
129
+ are `changes` (default: model changes or problems), `all` (every unit), and
130
+ `quiet` (no transcript notes). The footer and receipts remain useful with
131
+ `quiet`; neither depends on note verbosity.
132
+ Real-terminal frames at 80 columns are in [docs/screenshots](docs/screenshots/README.md):
133
+ [first-run proposal](docs/screenshots/first-run-proposal-80x24.txt),
134
+ [routed note](docs/screenshots/routed-note-80x24.txt),
135
+ [parked card](docs/screenshots/parked-card-80x24.txt), and
136
+ [doctor](docs/screenshots/doctor-80x24.txt). Dialogs are checked at 60, 80, and
137
+ 120 columns.
138
+
139
+ The `/adaptive` command (also `Ctrl+Alt+A`) uses the same dialog flow as OpenCode:
140
+
141
+ | Command | Purpose |
142
+ | --------------------------- | --------------------------------------------------------------------------------------------------------------- |
143
+ | `/adaptive` | Open the main menu. |
144
+ | `/adaptive status` | Show tiers, resource health, and available capacity evidence. |
145
+ | `/adaptive why` | Explain the latest routing receipt. |
146
+ | `/adaptive tiers` | Edit ordered tier membership. |
147
+ | `/adaptive health` | Inspect health and manually mark a resource healthy. |
148
+ | `/adaptive resume` | Act on parked work. |
149
+ | `/adaptive cancel` | Cancel parked work. |
150
+ | `/adaptive observe on\|off` | Enable or disable observation of manual-model sessions. |
151
+ | `/adaptive doctor` | Check host compatibility, credentials, capability declarations, storage, router conflicts, health, and parking. |
152
+
153
+ Typed subcommands complete in Pi's argument menu; the first Enter accepts the
154
+ completion and the second runs it.
155
+
156
+ ## Waiting, parking, and cancellation
157
+
158
+ Parking keeps work in the required tier:
159
+
160
+ - A known reset within two minutes waits in place with a countdown; Esc cancels.
161
+ - Longer waits persist parked work with its session and a shared local index.
162
+ A cross-process claim permits at most one wake for a parked record.
163
+ - No provider request is dispatched while parked. An initial prompt is saved as
164
+ normal Pi conversation history before parking at the routing boundary, so it
165
+ survives closing and reopening Pi. Cancelling its parked work leaves that
166
+ message in history. Later intercepted prompts are kept in session-only payloads
167
+ until resumed. After partial execution, recovery uses one hidden continuation message telling the agent to
168
+ continue from existing session/workspace state, not replay the original prompt
169
+ or redo completed steps. That small continuation **does** enter model context;
170
+ operational notes and receipts do not.
171
+ - A new user message or manual model selection cancels parked work. User aborts
172
+ never trigger failover or restart. A manual choice of another tier is explicit
173
+ user intent, not automatic cross-tier recovery.
174
+ - Reopening a parked interactive session re-arms its wait; already-due work asks
175
+ for confirmation. An owned wake writes a resumed note unless verbosity is
176
+ quiet. Automatic wakes are limited to **three per task**. Terminal
177
+ infrastructure failures get at most **two automatic restarts per task**;
178
+ capability escalation is separately bounded. Permanent auth/configuration
179
+ errors do not enter recovery loops.
180
+
181
+ Headless print/JSON runs never show dialogs. Detached wake timers are not a
182
+ supported headless continuation mechanism: Pi can exit or invalidate the
183
+ extension context after the owned handler returns. Only an awaited, owned
184
+ recovery may continue that process; otherwise the run ends with a clear
185
+ unavailability error. There is no background daemon.
186
+
187
+ ## Privacy
188
+
189
+ Adaptive sends no external telemetry and makes no classifier requests. Normal
190
+ model requests still go through the providers you configured in Pi. Shared
191
+ health, capacity, receipts, and parking metadata stay locally under Pi's agent
192
+ directory; credentials and raw response headers do not belong in those records.
193
+ Receipts are bounded and never injected into prompts. Pi retains its own session
194
+ content, including any input parked before dispatch; the shared parked index
195
+ must contain metadata only, not a second copy of your prompt.
196
+
197
+ ## Small router comparison
198
+
199
+ These are **source-audit facts, not runtime benchmarks**, from the
200
+ [2026-11-08 audit](../../research_notes/Pi%20port%20feasibility/pi_auto_model_audit.md).
201
+ They apply only to the inspected versions; none of those packages was run.
202
+
203
+ | Inspected router | Selection and recovery found in source |
204
+ | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
205
+ | `pi-auto-model` **0.9.2** | Keyword analysis, optional LLM classification (off by default), and utility ranking; its failover target list spans tiers. |
206
+ | `pi-shift-router` **1.8.0** | An LLM judge chooses fast/smart; its failover explicitly stays within the tier. |
207
+ | `@pedro_klein/pi-gateway` **0.6.0** | User-declared tier aliases with same-slot fallback and atomically persisted unhealthy-until times. |
208
+ | Pi Adaptive | Explicit capability tiers without a classifier; same-tier recovery, evidence-backed escalation, and durable parking. |
209
+
210
+ The audit found no durable park-and-auto-resume path in those three inspected
211
+ routers. This does not establish behavior of later versions.
212
+
213
+ ## Development and release evidence
214
+
215
+ From the repository root:
216
+
217
+ ```sh
218
+ npm run check -- --pkg pi
219
+ npm run verify -- --pkg pi
220
+ npm run verify -- --all
221
+ ```
222
+
223
+ See the [test strategy](docs/TEST_STRATEGY.md) for the unit, real-SDK,
224
+ packed-host, and PTY evidence. Publication requires a full, unskipped
225
+ `npm run verify -- --all`; a scoped run alone is not release evidence.
@@ -0,0 +1,92 @@
1
+ import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import "@earendil-works/pi-ai";
3
+ /** OpenCode model reference (`Model.Ref`): provider, model, optional variant. */
4
+ interface ModelRef {
5
+ readonly providerID: string;
6
+ readonly id: string;
7
+ readonly variant?: string | undefined;
8
+ }
9
+ declare const TIERS: readonly ["quick", "standard", "advanced"];
10
+ type TierName = (typeof TIERS)[number];
11
+ /** Operating modes (GOAL manual control). The plugin defaults to `observe`. */
12
+ declare const OPERATING_MODES: readonly ["off", "observe", "active"];
13
+ type OperatingMode = (typeof OPERATING_MODES)[number];
14
+ /** Current settings schema version. */
15
+ declare const SETTINGS_VERSION = 1;
16
+ interface SettingsV1 {
17
+ readonly version: typeof SETTINGS_VERSION;
18
+ /** OFF does nothing; OBSERVE is the safe default after install (ADP-029/032). */
19
+ readonly mode: OperatingMode;
20
+ /** Ordered per-tier resource lists; order is user policy and never reordered (ADP-005). */
21
+ readonly tiers: Readonly<Record<TierName, readonly ModelRef[]>>;
22
+ /**
23
+ * Global explicit user pin. `null` = no pin. A pin names a concrete model
24
+ * and outranks every other routing input (ADP-002).
25
+ */
26
+ readonly pin: ModelRef | null;
27
+ /** User-configured agent → tier hints (role level of the ladder, ADP-002). */
28
+ readonly agentTiers: Readonly<Record<string, TierName>>;
29
+ /** First-install invitation dismissed (ADP-032: shown once, never nagged). */
30
+ readonly inviteDismissed: boolean;
31
+ }
32
+ interface JsonFile {
33
+ /** Current value, or `undefined` when the file does not exist. */
34
+ read(): Promise<unknown>;
35
+ /**
36
+ * Applies `change` under the lock. A `next` of `undefined` leaves the
37
+ * file untouched (read-only decisions still serialize with writers).
38
+ */
39
+ update<R>(change: (current: unknown) => {
40
+ readonly next: unknown;
41
+ readonly result: R;
42
+ }): Promise<R>;
43
+ }
44
+ /** Async core stores must finish their read/modify/write before releasing the lock. */
45
+ interface JsonTransactionFile {
46
+ read(): Promise<unknown>;
47
+ transaction<R>(change: (current: unknown) => Promise<{
48
+ readonly next: unknown;
49
+ readonly result: R;
50
+ }>): Promise<R>;
51
+ }
52
+ interface SharedHealth {
53
+ readonly file: JsonFile;
54
+ readonly clock: () => number;
55
+ /** Probe ids must be unique across every process sharing the file. */
56
+ readonly newId: () => string;
57
+ }
58
+ interface ToolCapability {
59
+ readonly providerID: string;
60
+ readonly id: string;
61
+ readonly tools: boolean;
62
+ }
63
+ interface ParkingSources {
64
+ readonly file: JsonFile;
65
+ readonly clock: () => number;
66
+ readonly timer: (callback: () => void, delay: number) => () => void;
67
+ }
68
+ interface SharedCapacity {
69
+ readonly file: JsonFile;
70
+ readonly clock: () => number;
71
+ }
72
+ /** What only the packaged entry knows: the settings file and the Pi version. */
73
+ interface HostSources {
74
+ readonly settings: JsonFile;
75
+ readonly version: string;
76
+ readonly minimum: string;
77
+ readonly testedUpTo: string | undefined;
78
+ }
79
+ interface AdaptiveSources {
80
+ readonly settings: () => Promise<SettingsV1>;
81
+ readonly capabilities: () => Promise<readonly ToolCapability[]>;
82
+ readonly newId: () => string;
83
+ readonly health: SharedHealth;
84
+ readonly parking: ParkingSources;
85
+ readonly capacity: SharedCapacity;
86
+ readonly receipts: JsonTransactionFile;
87
+ readonly preferences: JsonFile;
88
+ }
89
+ type Host = Pick<ExtensionAPI, "registerVirtualModel" | "getActiveTools" | "registerTool" | "on" | "sendMessage" | "sendUserMessage" | "appendEntry" | "registerCommand" | "registerShortcut" | "setModel" | "registerEntryRenderer">;
90
+ declare function registerAdaptive(pi: Host, sources: AdaptiveSources, host: HostSources): void;
91
+ export default function adaptive(pi: Parameters<typeof registerAdaptive>[0]): void;
92
+ export {};