@neruok/pi-advisor 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cody Wentz
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,311 @@
1
- # Temporary Holding Version
1
+ # Pi advisor
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
+ A standalone Pi extension for isolated, multi-turn consultation. The main agent explains a problem. The advisor challenges assumptions and recommends evidence or next steps.
4
+
5
+ The advisor receives only the fixed advisor prompt and explicit consultation messages. It has no tools, code access, parent conversation, or workspace instructions. It does not use pi-magic8ball.
6
+
7
+ ## Try it
8
+
9
+ From this directory, load it for one invocation:
10
+
11
+ ```sh
12
+ pi -e ./advisor.ts
13
+ ```
14
+
15
+ No profile change is required.
16
+ The unscoped npm package `pi-advisor` belongs to a different project; do not install it to get this extension.
17
+
18
+ Install this package from npm:
19
+
20
+ ```sh
21
+ pi install npm:@neruok/pi-advisor
22
+ ```
23
+
24
+ Or install this checkout persistently:
25
+
26
+ ```sh
27
+ pi install /absolute/path/to/pi-advisor
28
+ ```
29
+
30
+ Local installs load the checkout in place. Restart Pi or use `/reload` after changes.
31
+
32
+ Choose a physical chat model already configured in Pi:
33
+
34
+ ```text
35
+ /advisor model <provider> <model-id>
36
+ /advisor show
37
+ ```
38
+
39
+ A bare `/advisor` opens an inline searchable picker in TUI mode, matching `/model` and pi-magic8ball. Type to filter by provider, identifier, or display name. Arrow keys navigate. Enter selects. Escape or Ctrl+C cancels without saving.
40
+
41
+ The picker marks the configured advisor model and shows at most ten model rows. RPC retains the standard selection dialog. Without UI, commands emit a custom transcript message and do not start a model turn. JSON mode exposes that message as an event.
42
+
43
+ Saves default to `<agent-dir>/advisor.json`. Use `--project` for `<cwd>/.pi/advisor.json`. Project reads and saves require current Pi project trust. A project selection replaces the whole global model pair.
44
+
45
+ ```json
46
+ {
47
+ "model": { "provider": "your-provider", "model": "your-model-id" }
48
+ }
49
+ ```
50
+
51
+ Use normal Pi authentication. No model fallback occurs. Configuration changes affect new consultations only.
52
+
53
+ ## Reasoning effort
54
+
55
+ ```text
56
+ /advisor reasoning
57
+ /advisor reasoning high
58
+ /advisor --project reasoning medium
59
+ ```
60
+
61
+ The query shows the effective model, configured effort, and supported choices without a provider request or settings write.
62
+ Set effort with `default`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. Only advertised model levels are accepted. Unsupported levels return `unsupported-reasoning`; the extension does not clamp them or select another model.
63
+
64
+ Settings store effort inside the complete model selection:
65
+
66
+ ```json
67
+ {
68
+ "model": { "provider": "your-provider", "model": "your-model-id", "reasoning": "high" }
69
+ }
70
+ ```
71
+
72
+ Omitted effort and `default` keep legacy provider behavior, without inheriting the parent thinking level. As in pi-magic8ball, `off` omits the SDK reasoning option and requires advertised off support. Other explicit levels reach each request unchanged. Provider adapters can map those levels to model-specific budgets. Higher reasoning can increase cost and latency; it is not a monetary cap or a guarantee of provider behavior.
73
+
74
+ The model and reasoning are pinned for each consultation. Existing sessions do not change when you edit settings. Explicit effort appears in result and discovery model metadata. Expand a reply to see its effort label. Plaintext thinking remains excluded from advice and history. Private opaque continuation state is described below.
75
+
76
+ Global writes use the global model, not a project override. Trusted project writes copy the effective complete selection when necessary. A project selection replaces the whole global selection, including effort. Selecting a model through the command or picker resets effort to legacy behavior; set effort afterward.
77
+
78
+ ## Request timeout
79
+
80
+ The default deadline is five minutes (300000 milliseconds) per call, including preparation.
81
+ Inspect or change it with the user-only command:
82
+
83
+ ```text
84
+ /advisor timeout
85
+ /advisor timeout 600000
86
+ /advisor --project timeout 900000
87
+ /advisor --project timeout default
88
+ ```
89
+
90
+ `/advisor timeout` and `/advisor show` report effective milliseconds and their source without a provider call.
91
+ `timeout default` removes that scope's explicit timeout. A project then inherits the global timeout, or the five-minute default.
92
+ Global reset restores the five-minute default unless a trusted project has its own timeout.
93
+
94
+ Settings store an optional root `timeoutMs`:
95
+
96
+ ```json
97
+ {
98
+ "model": { "provider": "your-provider", "model": "your-model-id" },
99
+ "timeoutMs": 600000
100
+ }
101
+ ```
102
+
103
+ Accepted values are integers from 1 through 2147483647 milliseconds, inclusive, within Node's timer range.
104
+ Zero, null, strings, fractions, and out-of-range values fail validation. Command values use decimal integer text.
105
+ Timeout-only settings are allowed, but a model must still be configured before consultation.
106
+
107
+ Timeout precedence is independent of model selection: trusted project `timeoutMs`, global `timeoutMs`, then the default.
108
+ A project model without a timeout inherits the global timeout. A timeout-only project retains the global model.
109
+ Untrusted project settings remain ignored. Model, picker, and reasoning commands preserve the save scope's explicit timeout.
110
+
111
+ Each consultation pins its timeout with the model and effort. Settings changes affect new consultations only.
112
+ Every continuation gets a fresh call budget of that pinned duration. Preparation consumes part of the same budget, not an extra period.
113
+ Before settings resolve, preparation uses the five-minute default. A resolved timeout replaces that budget from the original call-entry time.
114
+ If that budget has already elapsed, the request stops before generation.
115
+ Caller cancellation remains active. Deadlines do not enable retries or guarantee remote cancellation or zero billing.
116
+
117
+ ## Command autocomplete
118
+
119
+ The registered `/advisor` argument autocomplete suggests subcommands, scope flags, available physical chat providers, fuzzy model identifiers/names, supported reasoning levels, and `timeout default`. One scope flag can appear before, between, or after arguments. Reasoning suggestions target the save scope: global by default, or the effective model for a trusted project.
120
+
121
+ Selecting a suggestion inserts text and does not save settings or run a model. Submit the command to apply it. Dynamic choices use current catalog and settings snapshots without remote catalog refreshes.
122
+
123
+ Pi 1.0.4 has a Tab-routing limitation after command-name completion: accepting `/advisor ` with Tab can make another Tab miss argument completion. Type an argument prefix, such as `rea`, and wait for its completion menu, then accept the suggestion. This extension does not replace or patch the host editor.
124
+
125
+ ## Consultation tools
126
+
127
+ Start:
128
+
129
+ ```json
130
+ { "message": "Objective: fix the timeout. My understanding: workers wait for a queue signal. Evidence: the queue contains work, but no worker wakes. Constraint: preserve shutdown behavior. Uncertainty: signal loss or lock contention? What should I test?" }
131
+ ```
132
+
133
+ `advisor` returns a session identifier, free-form response, committed exchange count, pinned model, per-call usage, and cumulative usage.
134
+ The extension constructs the structured result envelope. It does not require the model to produce JSON, markers, or a status label, and it does not interpret the advice.
135
+
136
+ `usageComplete` describes this call's reported usage. `totalUsageComplete` describes the known consultation total.
137
+ These flags describe local reporting, not verified provider billing. A completed rejected reply still reports its usage.
138
+ An unobserved completion makes cumulative usage incomplete for the rest of that consultation, even after later successes.
139
+ Failures include cumulative usage when the requested consultation still exists. The host receives per-call usage only.
140
+
141
+ The collapsed TUI result shows replies of at most eight wrapped advice rows in full. Longer replies show the first eight advice rows and an expansion hint. Expand for full advice, model, and cumulative usage details. The cutoff uses the current terminal width, with no character-count cap. The advisory label and reported usage remain visible in both views.
142
+ The call view shows the main agent’s message in full, including line breaks, with terminal controls neutralized.
143
+ The provider still receives the original message. Hidden reasoning and private replay never appear in that view.
144
+
145
+ Both result views show a Pi-like consultation usage line, for example:
146
+
147
+ ```text
148
+ ↑3.0k ↓500 R10k W2.0k CH60.0% $0.038 51.5%/20k
149
+ ```
150
+
151
+ Input, output, cache reads/writes, and reported cost are cumulative for this consultation, including completed rejected calls.
152
+ CH uses the latest call: cache reads divided by uncached input plus cache reads plus cache writes.
153
+ Unknown prompt usage shows `CH?`. Incomplete cumulative reporting has an explicit warning.
154
+ Context uses the advisor's own estimate and model window, not the parent's totals. No subscription or auto-compaction labels appear.
155
+
156
+ Responses stay in the existing tool result. No duplicate chat messages or new model requests are added when you expand.
157
+ Pending updates show preparation or waiting only. They never show partial advice, thinking, or the explicit input.
158
+
159
+ Continue through `advisor`:
160
+
161
+ ```json
162
+ { "session": "adv_<returned-id>", "message": "I traced the signal. It fires before the worker begins waiting. How can I distinguish a lost wakeup from a stale predicate?" }
163
+ ```
164
+
165
+ `advisor_sessions({})` lists identifiers and metadata, not transcripts. Finish with `advisor_close({"session":"adv_<returned-id>"})`.
166
+
167
+ Each entry includes committed exchange count, known cumulative usage, `totalUsageComplete`, informational `historyBytes`, and `contextUsage`.
168
+ Both discovery views show every listed consultation and its committed exchange count.
169
+ `contextUsage` contains `tokens`, `contextWindow`, and `percent`, based on committed history only.
170
+ A pending request marks cumulative usage incomplete until its outcome is known. Estimates do not guarantee another exchange will fit.
171
+
172
+ Success results and session metadata have no status classification. Advice never automatically closes a consultation or executes an action.
173
+
174
+ The main agent must gather requested evidence and verify factual claims. Advice is not evidence or authorization. Ask the user when an action requires approval.
175
+
176
+ ## Opt-in failure diagnostics
177
+
178
+ For a requested call, add `"diagnostics": true` to the `advisor` input. Omission or `false` keeps the existing failure envelope. This option does not start another call or enable retries. Success results are unchanged.
179
+
180
+ Failures add `error.diagnostics` with a fixed `phase` and `category`. Phases are `validation`, `preparation`, `completion`, `response-validation`, and `commit`. When known, diagnostics include the effective or pinned `model` (including reasoning) and `selectionSource`: `global`, `project`, or `unknown`. Continuations retain the original selection source, even after settings change. If preparation fails before settings resolve, model metadata is absent.
181
+
182
+ Categories distinguish `local-error`, `provider-error` (terminal error response), `provider-aborted` (terminal abort response), and `advisor-error` (an extension-defined failure). Thrown completion errors can report `authentication`, `provider-rejection`, `rate-limit`, or `transport` from recognized own data fields: HTTP `status` 401/403, 400/404/409/413/422, 429, or 408/500/502/503/504 respectively; transport `code` values ECONNRESET, ECONNREFUSED, ETIMEDOUT, ENOTFOUND, and EAI_AGAIN also map to `transport`. HTTP status takes precedence. Other completion exceptions report `unknown`.
183
+
184
+ Terminal SDK errors can report `category: "sdk-error"` and `code: "sdk-invalid-timeout"`. This identifies the fixed timeout validation error. The renderer shows: "SDK timeout must be a positive integer." Requests use an integer remaining timeout without extending the local deadline.
185
+
186
+ The installed Responses adapter converts exceptions into terminal error text. Diagnostics read only its fixed SDK wrapper prefix, such as `xai API error (401): `. A matching wrapper adds `httpStatus` and the corresponding HTTP category. For xAI HTTP 400, an exact known rejection can add `code: "tool-choice-without-tools"`. This identifies a `tool_choice` setting without tool definitions. Unmapped statuses retain `provider-error`. Arbitrary error text, malformed wrappers, and unknown formats remain unclassified. Terminal aborts remain `provider-aborted`.
187
+
188
+ These are diagnostic hints, not proof of a remote root cause. A terminal `provider-error` can also represent an unrecognized local SDK failure. The extension compares only fixed known rejection details. It does not expose the provider body. Credentials, headers, raw errors, causes, arbitrary payloads, rejected advice, and thinking remain excluded. Diagnostic fields appear in the tool result and can persist in the parent transcript. Zero reported usage still does not establish zero billing. Stop after a failed live call; enabling diagnostics is not permission to retry.
189
+
190
+ ## Provider prompt caching
191
+
192
+ Every request uses `cacheRetention: 'short'` through Pi's provider adapter, with a stable identifier for that consultation.
193
+ The fixed advisor prompt and validated exchanges form a reusable prefix. Continue the same session for the same issue.
194
+ Different consultations use different identifiers and do not share local history.
195
+
196
+ Validated replies can retain opaque replay state in process memory, private to the consultation:
197
+
198
+ - Responses APIs (OpenAI, Codex, Azure, and compatible providers such as xAI): encrypted reasoning items and text item IDs/phases. Plaintext reasoning and summaries are removed.
199
+ - Google Generative AI and Vertex: thought signatures on their original text/thinking blocks, with thinking text removed.
200
+ - Anthropic and Bedrock: opaque redacted thinking only. Ordinary signed plaintext thinking is not retained or replayed; caching or continuity that requires it remains unsupported.
201
+
202
+ Original text-block boundaries and API identity are preserved for these formats. Malformed or unsupported metadata is omitted.
203
+ Replay never appears in tool results, session listings, renderers, or separate files. Requests remain stateless: advisor does not enable server-side conversation storage or use `previous_response_id`.
204
+ Private replay commits only with a valid reply. Provider-reported context usage includes reasoning when the provider reports it.
205
+ Ciphertext size is not a model-token count and does not set the context limit.
206
+
207
+ Cache hits depend on the provider, model, prompt size, and time between requests. Short caching does not guarantee savings.
208
+ Pi controls cache payload construction and the provider controls cache lifetime. Advisor does not request extended retention.
209
+ The explicit short option takes precedence over the SDK's `PI_CACHE_RETENTION` environment default.
210
+
211
+ Closing a consultation does not delete provider caches or the parent transcript. It removes only the extension's local consultation state.
212
+ Provider retention policies still apply. Do not include secrets.
213
+
214
+ ## Limits and retention
215
+
216
+ Consultations are ephemeral. The extension stores no separate consultation files and does not restore history after restart or reload.
217
+
218
+ Session replacement, fork, tree navigation, shutdown, or reload clears consultations. Parent compaction retains them. Pi can still persist tool arguments and results in its parent transcript. Closing a consultation does not erase that transcript. The provider receives the supplied messages and applies its own retention policy. Do not include secrets.
219
+
220
+ Advisor has no fixed consultation count, exchange count, message-byte, or reply-byte caps.
221
+ There is no fixed history-byte cap. More consultations and longer exchanges can use more process memory and increase provider cost.
222
+ Close consultations when you no longer need them.
223
+
224
+ The remaining consultation limits are:
225
+
226
+ - The pinned advisor model's `contextWindow`, from Pi's model registry.
227
+ - A configured deadline per call, with a 300000 ms default.
228
+
229
+ Advisor omits `maxTokens` and lets Pi control the model output allowance.
230
+ Pi applies model defaults and provider-specific context handling. Removing the extension cap does not remove provider output limits.
231
+ Before dispatch, estimated pending context must fit within the model window, with no fixed output reserve.
232
+ After validation, the full pair must also fit before commit. Equality passes both checks. Overflow returns `context-tokens`.
233
+ No automatic compaction, trimming, or summarization occurs.
234
+
235
+ The settings-file limit remains 16384 bytes. Label lengths, model-picker rows, and eight-row advice previews remain unchanged.
236
+
237
+ Context measurement follows Pi: use the latest positive assistant usage plus Pi's estimates for later messages.
238
+ With no positive usage, estimate the fixed system prompt and messages with Pi's public `estimateTokens` helper.
239
+ Usage includes cached input and output, including reasoning when reported. It is not cumulative consultation usage or ciphertext length.
240
+ The model window is pinned at creation. Missing or invalid window metadata fails before generation.
241
+ These are estimates, not exact tokenization or a provider acceptance guarantee. Provider calls may incur charges, including replies rejected by validation. No monetary cap is enforced.
242
+
243
+ Requests declare no tools and omit `toolChoice` for every provider. Pi controls provider payload construction without an advisor tool-choice override. Tool-call replies still fail validation.
244
+
245
+ Limits fail closed. The extension never evicts or silently summarizes history.
246
+ Limit failures retain `error.code = "limit-exceeded"` and add `error.limit = {resource, maximum, actual}`.
247
+ `context-tokens` is the only limit resource. Measurements use estimated context tokens. Bounds are inclusive.
248
+ To recover from context overflow, start a new consultation with your own explicit summary.
249
+
250
+ One request may run per consultation. Overlapping requests and busy close return `busy`. Cancellation and provider failure leave previous exchanges intact. The extension makes no automatic retries. A provider that ignores cancellation can complete later, but its reply cannot change consultation state. Late usage may be unavailable. If a completion attempt produces no observed response, `usageComplete` is false.
251
+ A zero usage value with that flag does not mean zero cost. Remote work can continue after local cancellation.
252
+ An unavailable model during a completion attempt can also produce incomplete usage. The extension does not infer remote billing from an exception.
253
+
254
+ Replies must contain nonblank text and finish normally. Empty replies, incomplete completions, malformed content, or tool calls fail with `invalid-response`.
255
+ The fixed error message identifies abnormal completion, malformed content, or absent text. Plaintext thinking is removed; only the allowlisted private opaque state above can survive. Public text blocks are joined with a newline without trimming or parsing their contents. Marker-looking strings are ordinary text.
256
+ It does not expose rejected advice, thinking, provider field values, or raw provider messages. A failed exchange never enters history.
257
+ Keep the rejection message when reporting a live failure. Stop rather than retry or relax validation. Earlier generic errors cannot reveal the exact violation after the reply is discarded.
258
+
259
+ Settings locks coordinate participating writers. An unrelated writer can still race between the final byte comparison and replacement. Parent-directory races and provider code remain outside an operating-system sandbox.
260
+
261
+ ## Opt-in live verification
262
+
263
+ Offline checks remain provider-free. Use this manual procedure only when a user requests a live compatibility check.
264
+ It checks one consultation and one continuation, not reasoning quality or universal provider support.
265
+
266
+ 1. Get separate spending authorization for a designated physical model and at most two requests.
267
+ 2. Disclose that the extension has no local monetary cap. Use a provider-side budget if a hard spending cap is required.
268
+ 3. Record the provider/model and Pi and Node versions. Use normal authentication. Do not copy credentials into the report.
269
+ 4. Use `/advisor show` to check the effective model. Select the designated model only with authorization for that settings change.
270
+ 5. Ask the main agent to call `advisor` with nonsecret text: "Recommend one offline check for a pure integer addition function."
271
+ 6. Check success, `advisory: true`, the free-form response, returned session identifier, model, `usage`, `usageComplete`, and `totalUsageComplete`.
272
+ 7. Continue that session: "I have not run the proposed check. Suggest one boundary case for that function."
273
+ 8. Check that the model remains pinned, turns equals two, and reported cumulative usage includes both calls.
274
+ 9. Close the idle session with `advisor_close`. Confirm that `advisor_sessions` no longer lists it.
275
+ 10. Report versions, provider/model, outcomes, reported usage, completeness, and whether the session closed.
276
+
277
+ Use no automatic retries. Stop after any failure, including an invalid reply. Close any existing idle consultation before reporting.
278
+ Do not spend a replacement request on a failed call. Do not try to induce malformed replies with paid requests.
279
+ If cleanup reports busy, inspect the pending state before another action. Do not claim that local abort stops remote billing.
280
+ Never describe an offline pass as live provider interoperability.
281
+
282
+ ## Development
283
+
284
+ ```sh
285
+ npm ci --ignore-scripts
286
+ npm run verify
287
+ npm run packcheck
288
+ ```
289
+
290
+ `packcheck` creates a temporary npm archive, checks its file list, extracts it, and loads the extracted package through Pi's resource loader. It requires `tar` on PATH and makes no model requests. Tests and release scripts are not shipped.
291
+
292
+ Tests use Node's test runner, mock model responses, and private temporary settings directories. TypeScript checks the extension and core. Pi and TypeBox remain host-provided runtime peers.
293
+
294
+ Verified against Pi 1.0.4 on Node 24, on Linux. Offline tests verify deterministic boundaries and package loading, not reasoning quality or live provider compatibility. Paid provider tests require separate authorization.
295
+
296
+ ## Release checklist
297
+
298
+ The release version is `@neruok/pi-advisor@0.1.1`, licensed under MIT. The manifest selects public access on the npm registry.
299
+
300
+ 1. Confirm the release version and regenerate `package-lock.json` after manifest changes with `npm install --package-lock-only --ignore-scripts`.
301
+ 2. Run `npm ci --ignore-scripts`, `npm run verify`, `npm run packcheck`, and `npm publish --dry-run`. The `prepublishOnly` hook runs verification and the archive check again.
302
+ 3. Inspect the archive file list and registry name/version availability. Check that the publishing account can publish to the `@neruok` scope without sharing credentials. A dry-run does not establish account permissions.
303
+ 4. Obtain explicit authorization before committing, tagging, pushing, or running `npm publish`. No automatic release workflow is configured.
304
+
305
+ The checked archive contains the TypeScript entry point, library modules, README, generated specification, package manifest, and MIT license. Pi supplies the runtime peers; no compilation step is required.
306
+
307
+ ## License
308
+
309
+ MIT. See [LICENSE](LICENSE).
310
+
311
+ The specification is generated at `docs/pi-advisor.md` from document `pi-advisor` in the maintainer workspace store. Author through checkout, preview, import, and compile. Do not edit the generated file.
package/advisor.ts ADDED
@@ -0,0 +1,75 @@
1
+ import type { JsonValue, Usage } from '@earendil-works/pi-ai';
2
+ import { getAgentDir, type ExtensionAPI, type ExtensionContext, type ExtensionToolContext } from '@earendil-works/pi-coding-agent';
3
+ import { Consultations, type Dependencies, type Metadata } from './lib/session.ts';
4
+ import { AdvisorError, LIMITS, failure, object, type Failure } from './lib/protocol.ts';
5
+ import { MAIN_GUIDANCE } from './lib/prompt.ts';
6
+ import { AdviceSchema, InputSchema, EmptyInputSchema, CloseInputSchema, ListSchema, CloseSchema } from './lib/schemas.ts';
7
+ import { loadSettings, settingsPaths } from './lib/settings.ts';
8
+ import { registerSettingsCommand, resolveModel } from './lib/settings-command.ts';
9
+ import { toolRenderers } from './lib/render.ts';
10
+
11
+ function dependencies(ctx: ExtensionToolContext, progress?: Dependencies['progress']): Dependencies {
12
+ return {
13
+ progress,
14
+ async prepare(reportSelection, reportTimeout) {
15
+ const loaded = await loadSettings(settingsPaths(ctx.cwd, getAgentDir()), ctx.isProjectTrusted());
16
+ const pair = loaded.settings.model;
17
+ if (pair) reportSelection?.(pair, loaded.source ?? 'unknown');
18
+ reportTimeout?.(loaded.settings.timeoutMs ?? LIMITS.timeoutMs);
19
+ if (!pair) throw new AdvisorError('not-configured');
20
+ resolveModel(ctx, pair, true);
21
+ return pair;
22
+ },
23
+ getContextWindow(pair) { return resolveModel(ctx, pair, true).contextWindow; },
24
+ async complete(pair, context, options) {
25
+ const model = resolveModel(ctx, pair, true);
26
+ return ctx.modelRegistry.streamSimple(model, context, options).result();
27
+ }
28
+ };
29
+ }
30
+ function result<T extends { ok: boolean; usage?: Usage }>(data: T) {
31
+ return { content: [{ type: 'text' as const, text: JSON.stringify(data) }], details: data, structuredContent: data as unknown as JsonValue, isError: !data.ok, ...(data.usage ? { usage: data.usage } : {}) };
32
+ }
33
+ export default function advisor(pi: ExtensionAPI): void {
34
+ const consultations = new Consultations();
35
+ const setCompletionContext = registerSettingsCommand(pi);
36
+ const reset = async (_event: unknown, ctx: ExtensionContext) => { consultations.clear(); setCompletionContext(ctx); };
37
+ pi.on('session_start', reset);
38
+ pi.on('session_tree', reset);
39
+ pi.on('session_shutdown', async () => { consultations.clear(); setCompletionContext(); });
40
+ pi.registerTool({
41
+ name: 'advisor', label: 'Advisor', exposure: 'model-only',
42
+ description: 'Consult an isolated, tool-free advisor. Explain your problem, evidence and uncertainty. Omit session to start, or supply it to continue. Advice is not evidence or authorization. No automatic workspace or parent context access. Explicit host-configured model required. Provider calls may incur charges.',
43
+ promptGuidelines: MAIN_GUIDANCE, parameters: InputSchema, outputSchema: AdviceSchema,
44
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
45
+ ...toolRenderers('advisor'),
46
+ async execute(_id, args, signal, update, ctx) {
47
+ const deps = dependencies(ctx, phase => update?.({ content: [{ type: 'text', text: `Advisor: ${phase}` }], details: { phase } }));
48
+ return result(await consultations.send(args, deps, signal ?? ctx.signal));
49
+ }
50
+ });
51
+ pi.registerTool({
52
+ name: 'advisor_sessions', label: 'Advisor sessions', exposure: 'model-only',
53
+ description: 'List active ephemeral advisor sessions and metadata, without full transcripts. Recover the identifier for the same issue.',
54
+ parameters: EmptyInputSchema, outputSchema: ListSchema,
55
+ ...toolRenderers('advisor_sessions'),
56
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
57
+ async execute(_id, args) {
58
+ let data: { ok: true; sessions: Metadata[] } | Failure;
59
+ try { object(args, [], 'invalid-argument'); data = { ok: true, sessions: consultations.list() }; }
60
+ catch (error) { data = failure(error); }
61
+ return result(data);
62
+ }
63
+ });
64
+ pi.registerTool({
65
+ name: 'advisor_close', label: 'Close advisor session', exposure: 'model-only',
66
+ description: 'Close an idle advisor consultation and release its history. Busy or unknown sessions return errors. Closing does not erase the parent Pi transcript.',
67
+ parameters: CloseInputSchema, outputSchema: CloseSchema,
68
+ ...toolRenderers('advisor_close'),
69
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
70
+ async execute(_id, args) {
71
+ try { object(args, ['session'], 'invalid-argument'); if (typeof args.session !== 'string' || !args.session.trim()) throw new AdvisorError('invalid-argument'); return result(consultations.close(args.session)); }
72
+ catch (error) { return result(failure(error)); }
73
+ }
74
+ });
75
+ }