@neruok/pi-advisor 0.1.0 → 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/README.md +18 -18
- package/docs/pi-advisor.md +135 -3
- package/lib/protocol.ts +4 -10
- package/lib/render.ts +2 -2
- package/lib/schemas.ts +5 -5
- package/lib/session.ts +5 -7
- package/lib/settings.ts +7 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -164,7 +164,8 @@ Continue through `advisor`:
|
|
|
164
164
|
|
|
165
165
|
`advisor_sessions({})` lists identifiers and metadata, not transcripts. Finish with `advisor_close({"session":"adv_<returned-id>"})`.
|
|
166
166
|
|
|
167
|
-
Each entry includes known cumulative usage, `totalUsageComplete`,
|
|
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.
|
|
168
169
|
`contextUsage` contains `tokens`, `contextWindow`, and `percent`, based on committed history only.
|
|
169
170
|
A pending request marks cumulative usage incomplete until its outcome is known. Estimates do not guarantee another exchange will fit.
|
|
170
171
|
|
|
@@ -216,17 +217,22 @@ Consultations are ephemeral. The extension stores no separate consultation files
|
|
|
216
217
|
|
|
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.
|
|
218
219
|
|
|
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.
|
|
220
223
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
-
|
|
224
|
-
- The pinned advisor model's `contextWindow`, from Pi's model registry. There is no fixed history-byte cap.
|
|
224
|
+
The remaining consultation limits are:
|
|
225
|
+
|
|
226
|
+
- The pinned advisor model's `contextWindow`, from Pi's model registry.
|
|
225
227
|
- A configured deadline per call, with a 300000 ms default.
|
|
226
228
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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.
|
|
230
236
|
|
|
231
237
|
Context measurement follows Pi: use the latest positive assistant usage plus Pi's estimates for later messages.
|
|
232
238
|
With no positive usage, estimate the fixed system prompt and messages with Pi's public `estimateTokens` helper.
|
|
@@ -238,14 +244,8 @@ Requests declare no tools and omit `toolChoice` for every provider. Pi controls
|
|
|
238
244
|
|
|
239
245
|
Limits fail closed. The extension never evicts or silently summarizes history.
|
|
240
246
|
Limit failures retain `error.code = "limit-exceeded"` and add `error.limit = {resource, maximum, actual}`.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
| Resource | Recovery |
|
|
244
|
-
| --- | --- |
|
|
245
|
-
| `input-bytes` | Send a shorter message. |
|
|
246
|
-
| `reply-bytes` | Request a shorter answer. |
|
|
247
|
-
| `context-tokens` or `turns` | Start a new consultation with your own explicit summary. |
|
|
248
|
-
| `sessions` | Close idle consultations. |
|
|
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
249
|
|
|
250
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
251
|
A zero usage value with that flag does not mean zero cost. Remote work can continue after local cancellation.
|
|
@@ -295,7 +295,7 @@ Verified against Pi 1.0.4 on Node 24, on Linux. Offline tests verify determinist
|
|
|
295
295
|
|
|
296
296
|
## Release checklist
|
|
297
297
|
|
|
298
|
-
The
|
|
298
|
+
The release version is `@neruok/pi-advisor@0.1.1`, licensed under MIT. The manifest selects public access on the npm registry.
|
|
299
299
|
|
|
300
300
|
1. Confirm the release version and regenerate `package-lock.json` after manifest changes with `npm install --package-lock-only --ignore-scripts`.
|
|
301
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.
|
package/docs/pi-advisor.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- generated by workspace-docs. source: pi-advisor revision
|
|
1
|
+
<!-- generated by workspace-docs. source: pi-advisor revision 41. do not edit; author with workspace-docs checkout. -->
|
|
2
2
|
|
|
3
3
|
# Pi advisor: isolated consultation contract and design
|
|
4
4
|
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
- [Short provider caching verification](#cache-verification)
|
|
27
27
|
- [Private continuation-state verification](#opaque-replay-verification)
|
|
28
28
|
- [Model context and display verification](#context-display-verification)
|
|
29
|
+
- [Fixed consultation cap removal](#fixed-cap-removal-verification)
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
## Purpose and scope {#scope}
|
|
@@ -46,7 +47,10 @@ Consultations remain available across parent turns and compaction. Clear them on
|
|
|
46
47
|
|
|
47
48
|
The extension writes no consultation files. Pi can still persist tool arguments and results in its parent transcript. The configured provider receives explicit consultation messages. Ephemeral means no separate extension persistence, not guaranteed erasure by Pi or the provider.
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
REQ-31 removes the fixed consultation, exchange, message-byte, reply-byte, and output-token caps at the user's request.
|
|
51
|
+
REQ-29 retains the pinned model context window. REQ-26 retains the configurable deadline, with a 300000 ms default.
|
|
52
|
+
Pi controls the model output allowance. Settings-file and presentation bounds remain separate from consultation capacity.
|
|
53
|
+
These controls do not provide a monetary guarantee.
|
|
50
54
|
|
|
51
55
|
Settings use the host agent directory, not a hard-coded home path. Project settings require current Pi project trust. No default model is selected. No live provider calls are part of build verification.
|
|
52
56
|
|
|
@@ -82,7 +86,7 @@ Each advisor call MUST make at most one model request. Provider retries MUST be
|
|
|
82
86
|
Only a validated reply MUST commit its user/reply pair. Failed creation MUST leave no consultation. Failed continuation MUST preserve previous exchanges.
|
|
83
87
|
The extension MUST combine caller cancellation with the deadline defined by REQ-26, measured from call entry. Cancellation MUST prevent a later commit.
|
|
84
88
|
Timeout and cancellation MUST return without waiting indefinitely for a provider that ignores abort. Late completions MUST NOT change state.
|
|
85
|
-
Concurrent calls on one consultation MUST reject the second with busy. Different consultations MAY run concurrently
|
|
89
|
+
Concurrent calls on one consultation MUST reject the second with busy. Different consultations MAY run concurrently. REQ-31 removes the active consultation cap.
|
|
86
90
|
Failures MUST use fixed safe error codes and messages. Raw provider exceptions MUST NOT appear in output.
|
|
87
91
|
Results MUST report usage available at return, including rejected completed replies. Usage from a late unobserved completion MAY be unavailable.
|
|
88
92
|
|
|
@@ -537,8 +541,85 @@ Existing eight-row advice previews, expanded full advice, safe failures, current
|
|
|
537
541
|
README MUST explain visible questions, model-sized estimated context, remaining limits, cumulative totals, per-call CH, and reported cost.
|
|
538
542
|
|
|
539
543
|
|
|
544
|
+
<a id="REQ-31"></a>
|
|
545
|
+
|
|
546
|
+
**REQ-31** Remove fixed consultation caps
|
|
547
|
+
|
|
548
|
+
Advisor MUST NOT cap active consultations, committed exchanges, message bytes, or reply bytes with fixed extension limits.
|
|
549
|
+
Advisor MUST omit maxTokens from every completion options object. Pi MUST control the model output allowance.
|
|
550
|
+
Before dispatch, advisor MUST compare estimated pending context tokens with the pinned model contextWindow, without a fixed output reserve.
|
|
551
|
+
After reply validation, advisor MUST compare estimated committed context tokens with that same window.
|
|
552
|
+
Equality MUST pass both checks. Overflow MUST return limit-exceeded with resource context-tokens and commit no exchange.
|
|
553
|
+
The fixed safe context error MUST report maximum contextWindow and actual estimated tokens.
|
|
554
|
+
Completed rejected replies MUST retain their known usage. A failed continuation MUST retain its previous context and replay.
|
|
555
|
+
|
|
556
|
+
Success and discovery schemas MUST permit exchange counts above 24. Discovery MUST permit more than eight entries.
|
|
557
|
+
Discovery MUST omit turnsRemaining. Session renderers MUST show committed exchange counts and all listed consultations, without a fixed eight-entry cutoff.
|
|
558
|
+
Public limit schemas MUST accept only context-tokens as a resource. Removed resource names MUST fail schema validation.
|
|
559
|
+
The README MUST describe the removed caps, Pi output defaults, retained context checks, and unchanged deadline.
|
|
560
|
+
|
|
561
|
+
The configurable timeout, caller cancellation, busy protection, no retries, strict inputs, reply validation, and isolation MUST remain unchanged.
|
|
562
|
+
Usage snapshots, opaque replay privacy, close, lifecycle reset, and ephemeral retention MUST remain unchanged.
|
|
563
|
+
The 16384-byte settings-file bound, 80-code-point labels, picker row bounds, and eight-row advice previews MUST remain unchanged.
|
|
564
|
+
|
|
565
|
+
This requirement supersedes REQ-4's fixed consultation, exchange, input-byte, reply-byte, and output-token bounds.
|
|
566
|
+
It supersedes REQ-11's removed limit resources and REQ-13's turnsRemaining field.
|
|
567
|
+
It supersedes REQ-29's fixed output reserve and preservation of the removed caps.
|
|
568
|
+
Earlier requirements and acceptance criteria MUST NOT restore these caps through a preservation clause.
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
- #REQ-31 supersedes: [#REQ-4](#REQ-4)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
- #REQ-31 supersedes: [#REQ-11](#REQ-11)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
- #REQ-31 supersedes: [#REQ-13](#REQ-13)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
- #REQ-31 supersedes: [#REQ-29](#REQ-29)
|
|
581
|
+
|
|
582
|
+
|
|
540
583
|
## Acceptance criteria {#acceptance}
|
|
541
584
|
|
|
585
|
+
<a id="AC-31"></a>
|
|
586
|
+
|
|
587
|
+
**AC-31** Calls beyond former caps
|
|
588
|
+
|
|
589
|
+
Changed checks MUST fail against the capped baseline before implementation.
|
|
590
|
+
Send multibyte messages and valid replies above 16384 UTF-8 bytes within the model window.
|
|
591
|
+
PASS when the exact message reaches completion, the exact reply commits, and continuation retains both without truncation.
|
|
592
|
+
Create at least nine consultations, including pending creation. Continue one consultation through at least 25 successful exchanges.
|
|
593
|
+
PASS when all calls succeed, identifiers remain distinct, and prior consultations remain available without eviction.
|
|
594
|
+
Inspect options across provider selections. PASS when maxTokens is absent while reasoning, short caching, and zero retries remain.
|
|
595
|
+
Use the installed Responses adapter with synthetic authentication and mock fetch.
|
|
596
|
+
PASS when a model allowance above 4096 reaches the payload instead of the former extension cap.
|
|
597
|
+
Test pending context at the exact model window and one token above it.
|
|
598
|
+
PASS when equality dispatches without a fixed output reserve and overflow returns exact context diagnostics without dispatch.
|
|
599
|
+
Validate successful results and discovery above the former count caps.
|
|
600
|
+
PASS when turnsRemaining is absent, strict unknown fields remain rejected, and removed limit resource names fail validation.
|
|
601
|
+
Render at least nine entries in both views. PASS when every identifier appears, committed counts replace remaining counts, and lines fit.
|
|
602
|
+
Inspect the README. PASS when it describes the removed caps and retained context, timeout, and provider-cost caveats.
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
<a id="AC-32"></a>
|
|
606
|
+
|
|
607
|
+
**AC-32** Preserved safeguards after cap removal
|
|
608
|
+
|
|
609
|
+
Preserved checks MUST pass before and after implementation.
|
|
610
|
+
Inject committed-context overflow and a malformed completion during continuation.
|
|
611
|
+
PASS when previous context and private replay remain unchanged, completed rejected usage remains counted, and no retry occurs.
|
|
612
|
+
Cancel pending creation and settle its reply afterward. PASS when no late exchange commits or discarded consultation reappears.
|
|
613
|
+
Retain existing deadline, busy, close, lifecycle, settings-file bounds, strict validation, privacy, and eight-row advice-preview checks.
|
|
614
|
+
Use mock catalogs, temporary settings, synthetic replay, and mock fetch only. No paid provider call is part of verification.
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
- #REQ-31 verified-by: [#AC-31](#AC-31)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
- #REQ-31 verified-by: [#AC-32](#AC-32)
|
|
621
|
+
|
|
622
|
+
|
|
542
623
|
<a id="AC-23"></a>
|
|
543
624
|
|
|
544
625
|
**AC-23** SDK timeout and terminal error regression coverage
|
|
@@ -1421,3 +1502,54 @@ The nine new-coder profile checks and both profile core TypeScript commands pass
|
|
|
1421
1502
|
Verification used temporary settings, mock catalogs, synthetic replay, and mock provider responses only.
|
|
1422
1503
|
No new paid request, live settings change, commit, push, deployment, or live terminal check occurred.
|
|
1423
1504
|
These results do not prove exact provider tokenization, universal compatibility, or live UI appearance. Reload is required to load the changed extension.
|
|
1505
|
+
|
|
1506
|
+
## Fixed consultation cap removal {#fixed-cap-removal-verification}
|
|
1507
|
+
|
|
1508
|
+
|
|
1509
|
+
The user authorized removal of the active consultation, exchange, message-byte, reply-byte, and output-token caps.
|
|
1510
|
+
The user retained the model context limit and configurable timeout. REQ-31, AC-31, and AC-32 define this change.
|
|
1511
|
+
The inspected baseline was commit 69c64a3 and stored document revision 38. TypeScript and all 137 baseline tests passed.
|
|
1512
|
+
|
|
1513
|
+
Changed behavior: eleven AC-31 checks failed before implementation for the removed limits, schemas, renderer cutoff, or README.
|
|
1514
|
+
Ten failures came from the first focused run. The separate reply-byte check also failed before implementation.
|
|
1515
|
+
Preserved behavior: the AC-32 rollback, private replay, rejected usage, and cancelled late-work check passed before and after.
|
|
1516
|
+
All twelve focused checks passed after implementation.
|
|
1517
|
+
|
|
1518
|
+
| Criterion | Checks in tests/removed-limits.test.mjs | Before | After |
|
|
1519
|
+
| --- | --- | --- | --- |
|
|
1520
|
+
| AC-31 | Multibyte messages and replies above 16 KiB, including a short-input reply case | fail | pass |
|
|
1521
|
+
| AC-31 | Nine active consultations and nine pending creations, without eviction | fail | pass |
|
|
1522
|
+
| AC-31 | Twenty-five exchanges with the complete consultation prefix | fail | pass |
|
|
1523
|
+
| AC-31 | Provider matrix without maxTokens and installed Responses model output allowance | fail | pass |
|
|
1524
|
+
| AC-31 | Exact pending-context equality and one-token overflow without dispatch | fail | pass |
|
|
1525
|
+
| AC-31 | Strict schemas above count caps, no turnsRemaining, and only context-tokens | fail | pass |
|
|
1526
|
+
| AC-31 | Every discovery entry in both views at widths 1, 8, 30, 80, and 160 | fail | pass |
|
|
1527
|
+
| AC-31 | README removed caps and retained controls | fail | pass |
|
|
1528
|
+
| AC-32 | Context overflow, malformed replies, private replay, accounting, and cancelled late work | pass | pass |
|
|
1529
|
+
|
|
1530
|
+
Advisor now omits maxTokens. Installed Pi AI 1.0.4 uses model defaults and its own context clamp.
|
|
1531
|
+
A synthetic-authentication, mock-fetch Responses test used the installed gpt-4.1 output allowance of 32768 tokens.
|
|
1532
|
+
This is payload evidence, not proof of live provider acceptance or reasoning quality.
|
|
1533
|
+
Advisor compares pending context directly with the pinned model window. It also checks committed context before each commit.
|
|
1534
|
+
No fixed output reserve remains in advisor. Pi can still apply its own reserve or provider output limits.
|
|
1535
|
+
|
|
1536
|
+
The first full run identified 21 obsolete expectations for removed caps, reserve tokens, or turnsRemaining.
|
|
1537
|
+
Those expectations now trace to AC-31. Context overflow, deadlines, privacy, accounting, replay, and cancellation assertions remain.
|
|
1538
|
+
A renderer recovery-text check required whitespace-aware matching because the longer context error wraps onto two lines.
|
|
1539
|
+
The change did not suppress that diagnostic or relax the required recovery text.
|
|
1540
|
+
|
|
1541
|
+
npm run verify passed TypeScript and all 149 tests. npm run packcheck passed package loading and archive checks.
|
|
1542
|
+
The nine new-coder profile scripts and both profile core TypeScript checks passed. Project git diff --check passed.
|
|
1543
|
+
Verification used mock catalogs, synthetic replies, mock fetch, controlled clocks, and temporary settings only.
|
|
1544
|
+
No paid provider call, live settings change, live terminal check, commit, push, or deployment ran.
|
|
1545
|
+
Reload is required before the running extension uses this change.
|
|
1546
|
+
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
After reload, the user separately authorized one live consultation and one continuation with the configured advisor model.
|
|
1550
|
+
Both calls succeeded on xai/grok-4.7. The same consultation reached two exchanges with the same model selection.
|
|
1551
|
+
Both usage-completeness flags were true on each call. The reported total was 3595 tokens and 0.00471 dollars.
|
|
1552
|
+
Reported reasoning totaled 251 tokens. The pinned context window was 500000 tokens.
|
|
1553
|
+
The idle consultation closed successfully. Discovery then returned no active consultations. No retry occurred.
|
|
1554
|
+
This confirms one live two-call flow, not live behavior beyond the removed caps or independently verified provider billing.
|
|
1555
|
+
The user then authorized a version bump, commit, push, and npm publication.
|
package/lib/protocol.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AssistantMessage, Usage } from '@earendil-works/pi-ai';
|
|
2
2
|
import type { ContextUsage } from './context.ts';
|
|
3
3
|
|
|
4
|
-
export const LIMITS = Object.freeze({
|
|
4
|
+
export const LIMITS = Object.freeze({ timeoutMs: 300000 });
|
|
5
5
|
export const MAX_TIMEOUT_MS = 2147483647;
|
|
6
6
|
export const REASONING_LEVELS = ['default', 'off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'] as const;
|
|
7
7
|
export type Reasoning = typeof REASONING_LEVELS[number];
|
|
@@ -11,7 +11,7 @@ const ERRORS: Record<ErrorCode, string> = {
|
|
|
11
11
|
'invalid-argument': 'Use a nonblank message and an existing session identifier, if supplied.',
|
|
12
12
|
'not-found': 'Consultation not found. It may have closed or its parent session may have changed.',
|
|
13
13
|
busy: 'Consultation has a pending request. Wait for it before sending or closing.',
|
|
14
|
-
'limit-exceeded': '
|
|
14
|
+
'limit-exceeded': 'Advisor context limit exceeded. Start a new consultation with an explicit summary.',
|
|
15
15
|
'not-configured': 'No advisor model configured. Use /advisor model <provider> <model>.',
|
|
16
16
|
'invalid-config': 'Advisor settings must be strict JSON with an optional model selection and integer timeoutMs.',
|
|
17
17
|
'settings-unavailable': 'Cannot safely read or save advisor settings. Inspect settings and locks before retrying.',
|
|
@@ -22,14 +22,10 @@ const ERRORS: Record<ErrorCode, string> = {
|
|
|
22
22
|
cancelled: 'Advisor request cancelled. No exchange was committed.',
|
|
23
23
|
timeout: 'Advisor request deadline exceeded. No exchange was committed.'
|
|
24
24
|
};
|
|
25
|
-
export type LimitResource = '
|
|
25
|
+
export type LimitResource = 'context-tokens';
|
|
26
26
|
export type LimitDetails = { resource: LimitResource; maximum: number; actual: number };
|
|
27
27
|
const LIMIT_MESSAGES: Record<LimitResource, string> = {
|
|
28
|
-
'
|
|
29
|
-
'reply-bytes': 'Reply byte limit exceeded. Request a shorter answer.',
|
|
30
|
-
'context-tokens': 'Advisor context limit exceeded. Start a new consultation with an explicit summary.',
|
|
31
|
-
turns: 'Exchange limit exceeded. Start a new consultation with an explicit summary.',
|
|
32
|
-
sessions: 'Active consultation limit exceeded. Close idle consultations before starting another.'
|
|
28
|
+
'context-tokens': 'Advisor context limit exceeded. Start a new consultation with an explicit summary.'
|
|
33
29
|
};
|
|
34
30
|
const REPLY_MESSAGES = {
|
|
35
31
|
completion: 'Advisor completion did not stop normally.',
|
|
@@ -120,7 +116,6 @@ export function parseInput(value: unknown): { message: string; session?: string;
|
|
|
120
116
|
const input = object(value, ['message', 'session', 'diagnostics'], 'invalid-argument');
|
|
121
117
|
if (input.diagnostics !== undefined && typeof input.diagnostics !== 'boolean') throw new AdvisorError('invalid-argument');
|
|
122
118
|
if (typeof input.message !== 'string' || !input.message.trim() || (input.session !== undefined && (typeof input.session !== 'string' || !input.session.trim()))) throw new AdvisorError('invalid-argument');
|
|
123
|
-
checkLimit('input-bytes', LIMITS.messageBytes, Buffer.byteLength(input.message));
|
|
124
119
|
return { message: input.message, ...(input.session === undefined ? {} : { session: input.session as string }), ...(input.diagnostics === undefined ? {} : { diagnostics: input.diagnostics as boolean }) };
|
|
125
120
|
}
|
|
126
121
|
export function replyText(message: AssistantMessage): string {
|
|
@@ -131,7 +126,6 @@ export function replyText(message: AssistantMessage): string {
|
|
|
131
126
|
throw new AdvisorError('invalid-response', undefined, 'content');
|
|
132
127
|
}
|
|
133
128
|
const text = message.content.filter(block => block.type === 'text').map(block => block.text).join('\n');
|
|
134
|
-
checkLimit('reply-bytes', LIMITS.replyBytes, Buffer.byteLength(text));
|
|
135
129
|
if (!text.trim()) throw new AdvisorError('invalid-response', undefined, 'text');
|
|
136
130
|
return text;
|
|
137
131
|
}
|
package/lib/render.ts
CHANGED
|
@@ -45,7 +45,7 @@ function header(data: Advice): string {
|
|
|
45
45
|
}
|
|
46
46
|
function sessionLines(entry: Metadata, expanded: boolean): string[] {
|
|
47
47
|
return [
|
|
48
|
-
`${label(entry.session)}${entry.busy ? ' • busy' : ''} • ${entry.
|
|
48
|
+
`${label(entry.session)}${entry.busy ? ' • busy' : ''} • ${entry.turns} exchanges`,
|
|
49
49
|
...(expanded ? [label(entry.label), `${label(entry.model?.provider)}/${label(entry.model?.model)} • ${contextLine(entry.contextUsage)} advisor context`, usageLine(entry.totalUsage, entry.totalUsageComplete)] : [])
|
|
50
50
|
];
|
|
51
51
|
}
|
|
@@ -92,7 +92,7 @@ function resultLines(kind: Kind, data: Data | undefined, options: ToolRenderResu
|
|
|
92
92
|
return lines;
|
|
93
93
|
}
|
|
94
94
|
if (kind === 'advisor_sessions' && 'sessions' in data) {
|
|
95
|
-
return [...styled('muted', `${data.sessions.length} active advisor consultations`), ...data.sessions.
|
|
95
|
+
return [...styled('muted', `${data.sessions.length} active advisor consultations`), ...data.sessions.flatMap(entry => sessionLines(entry, options.expanded).flatMap(line => styled('toolOutput', line)))];
|
|
96
96
|
}
|
|
97
97
|
if (kind === 'advisor_close' && 'session' in data) return styled('muted', `Advisor consultation closed: ${label(data.session)}. Parent transcript remains.`);
|
|
98
98
|
return styled('muted', 'Advisor result details unavailable.');
|
package/lib/schemas.ts
CHANGED
|
@@ -7,7 +7,7 @@ export const UsageSchema = Type.Object({ input: Type.Number(), output: Type.Numb
|
|
|
7
7
|
const totals = { totalUsage: UsageSchema, totalUsageComplete: Type.Boolean() };
|
|
8
8
|
export const ContextUsageSchema = Type.Object({ tokens: Type.Number({ minimum: 0 }), contextWindow: Type.Integer({ minimum: 1 }), percent: Type.Number({ minimum: 0 }) }, object);
|
|
9
9
|
const LimitSchema = Type.Object({
|
|
10
|
-
resource: Type.
|
|
10
|
+
resource: Type.Literal('context-tokens'),
|
|
11
11
|
maximum: Type.Integer({ minimum: 0 }), actual: Type.Integer({ minimum: 0 })
|
|
12
12
|
}, object);
|
|
13
13
|
export const DiagnosticsSchema = Type.Object({
|
|
@@ -27,10 +27,10 @@ export const FailureSchema = Type.Union([Type.Object(failure, object), Type.Obje
|
|
|
27
27
|
export const InputSchema = Type.Object({ message: text, session: Type.Optional(text), diagnostics: Type.Optional(Type.Boolean({ description: 'Opt in for safe failure phase, category and selected model metadata. No raw provider errors. Does not retry.' })) }, object);
|
|
28
28
|
export const CloseInputSchema = Type.Object({ session: text }, object);
|
|
29
29
|
export const EmptyInputSchema = Type.Object({}, object);
|
|
30
|
-
export const AdviceSchema = Type.Union([Type.Object({ ok: Type.Literal(true), advisory: Type.Literal(true), session: text, response: text, turns: Type.Integer({ minimum: 1
|
|
30
|
+
export const AdviceSchema = Type.Union([Type.Object({ ok: Type.Literal(true), advisory: Type.Literal(true), session: text, response: text, turns: Type.Integer({ minimum: 1 }), model: ModelSchema, usage: UsageSchema, usageComplete: Type.Boolean(), contextUsage: ContextUsageSchema, ...totals }, object), FailureSchema]);
|
|
31
31
|
export const ListSchema = Type.Union([Type.Object({ ok: Type.Literal(true), sessions: Type.Array(Type.Object({
|
|
32
|
-
session: text, label: text, turns: Type.Integer({ minimum: 0
|
|
33
|
-
...totals,
|
|
32
|
+
session: text, label: text, turns: Type.Integer({ minimum: 0 }), model: ModelSchema, busy: Type.Boolean(),
|
|
33
|
+
...totals,
|
|
34
34
|
historyBytes: Type.Integer({ minimum: 0 }), contextUsage: ContextUsageSchema
|
|
35
|
-
}, object)
|
|
35
|
+
}, object)) }, object), FailureSchema]);
|
|
36
36
|
export const CloseSchema = Type.Union([Type.Object({ ok: Type.Literal(true), session: text }, object), FailureSchema]);
|
package/lib/session.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type Dependencies = {
|
|
|
15
15
|
progress?: (phase: Phase) => void;
|
|
16
16
|
};
|
|
17
17
|
export type Advice = { ok: true; advisory: true; session: string; response: string; turns: number; model: Selection; usage: Usage; usageComplete: boolean; contextUsage: ContextUsage } & UsageTotals;
|
|
18
|
-
export type Metadata = { session: string; label: string; turns: number; model: Selection; busy: boolean;
|
|
18
|
+
export type Metadata = { session: string; label: string; turns: number; model: Selection; busy: boolean; historyBytes: number; contextUsage: ContextUsage } & UsageTotals;
|
|
19
19
|
const size = (history: Exchange[]): number => Buffer.byteLength(JSON.stringify(history.map(({ role, text, replay }) => ({ role, text, ...(replay ? { replay } : {}) }))));
|
|
20
20
|
const retainedContext = (session: Session): ContextUsage => contextUsage(context(session.history, session.model!).messages, session.contextWindow!);
|
|
21
21
|
const totals = (session: Session, pending = Boolean(session.pending)): UsageTotals => ({ totalUsage: structuredClone(session.usage), totalUsageComplete: session.totalUsageComplete && !pending });
|
|
@@ -53,7 +53,7 @@ export class Consultations {
|
|
|
53
53
|
list(): Metadata[] {
|
|
54
54
|
return [...this.sessions.values()].filter((s): s is Session & { model: Selection } => Boolean(s.model)).map(s => ({
|
|
55
55
|
session: s.id, label: s.label, turns: s.history.length / 2, model: { ...s.model }, busy: Boolean(s.pending),
|
|
56
|
-
...totals(s),
|
|
56
|
+
...totals(s),
|
|
57
57
|
historyBytes: size(s.history), contextUsage: retainedContext(s)
|
|
58
58
|
}));
|
|
59
59
|
}
|
|
@@ -103,7 +103,6 @@ export class Consultations {
|
|
|
103
103
|
timeoutMs = session.timeoutMs ?? this.limits.timeoutMs;
|
|
104
104
|
armDeadline();
|
|
105
105
|
const pending: Exchange[] = [...session.history, { role: 'user', text: input.message }];
|
|
106
|
-
checkLimit('turns', this.limits.turns, session.history.length / 2 + 1);
|
|
107
106
|
phase = 'preparation';
|
|
108
107
|
const selected = session.model ?? await interruptible(() => {
|
|
109
108
|
progress(deps, 'preparing');
|
|
@@ -122,13 +121,13 @@ export class Consultations {
|
|
|
122
121
|
const model = { ...selected };
|
|
123
122
|
const window = session.contextWindow ?? deps.getContextWindow?.(model);
|
|
124
123
|
if (!Number.isSafeInteger(window) || window < 1) throw new AdvisorError('model-unavailable');
|
|
125
|
-
// Pin before awaiting model work.
|
|
124
|
+
// Pin before awaiting model work. The map identity guards against lifecycle invalidation.
|
|
126
125
|
session.model = model;
|
|
127
126
|
session.contextWindow = window;
|
|
128
127
|
session.selectionSource = selectionSource;
|
|
129
128
|
session.timeoutMs = timeoutMs;
|
|
130
129
|
diagnosticModel = { ...model };
|
|
131
|
-
checkLimit('context-tokens', window, contextUsage(context(pending, model).messages, window).tokens
|
|
130
|
+
checkLimit('context-tokens', window, contextUsage(context(pending, model).messages, window).tokens);
|
|
132
131
|
phase = 'completion';
|
|
133
132
|
const reply = await interruptible(() => {
|
|
134
133
|
progress(deps, 'waiting');
|
|
@@ -137,7 +136,7 @@ export class Consultations {
|
|
|
137
136
|
usageComplete = false;
|
|
138
137
|
return deps.complete(model, context(pending, model), {
|
|
139
138
|
signal: controller!.signal, timeoutMs: Math.max(1, Math.floor(timeoutMs - (performance.now() - started))), maxRetries: 0,
|
|
140
|
-
|
|
139
|
+
cacheRetention: 'short', sessionId: session!.id,
|
|
141
140
|
...(model.reasoning && model.reasoning !== 'default' && model.reasoning !== 'off' ? { reasoning: model.reasoning } : {})
|
|
142
141
|
});
|
|
143
142
|
}, controller.signal);
|
|
@@ -187,7 +186,6 @@ export class Consultations {
|
|
|
187
186
|
if (existing.pending) throw new AdvisorError('busy');
|
|
188
187
|
return existing;
|
|
189
188
|
}
|
|
190
|
-
checkLimit('sessions', this.limits.sessions, this.sessions.size + 1);
|
|
191
189
|
const session: Session = { id: 'adv_' + randomUUID(), label: Array.from(input.message.trim().replace(/\s+/gu, ' ')).slice(0, 80).join(''), history: [], usage: zeroUsage(), totalUsageComplete: true };
|
|
192
190
|
this.sessions.set(session.id, session);
|
|
193
191
|
return session;
|
package/lib/settings.ts
CHANGED
|
@@ -3,7 +3,9 @@ import { lstat, open, mkdir, rename, link, unlink, type FileHandle } from 'node:
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { withFileMutationQueue } from '@earendil-works/pi-coding-agent';
|
|
6
|
-
import { AdvisorError,
|
|
6
|
+
import { AdvisorError, object, parseSelection, parseTimeoutMs, type Selection } from './protocol.ts';
|
|
7
|
+
|
|
8
|
+
const MAX_SETTINGS_BYTES = 16384;
|
|
7
9
|
|
|
8
10
|
export type Settings = { model?: Selection; timeoutMs?: number };
|
|
9
11
|
export type Scope = 'global' | 'project';
|
|
@@ -32,15 +34,15 @@ async function readLayer(path: string): Promise<{ settings: Settings; raw?: Buff
|
|
|
32
34
|
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
33
35
|
const opened = await handle.stat();
|
|
34
36
|
if (!opened.isFile()) throw new AdvisorError('settings-unavailable');
|
|
35
|
-
if (opened.size >
|
|
36
|
-
const buffer = Buffer.alloc(
|
|
37
|
+
if (opened.size > MAX_SETTINGS_BYTES) throw new AdvisorError('invalid-config');
|
|
38
|
+
const buffer = Buffer.alloc(MAX_SETTINGS_BYTES + 1);
|
|
37
39
|
let length = 0;
|
|
38
40
|
while (length < buffer.length) {
|
|
39
41
|
const { bytesRead } = await handle.read(buffer, length, buffer.length - length, length);
|
|
40
42
|
if (!bytesRead) break;
|
|
41
43
|
length += bytesRead;
|
|
42
44
|
}
|
|
43
|
-
if (length >
|
|
45
|
+
if (length > MAX_SETTINGS_BYTES) throw new AdvisorError('invalid-config');
|
|
44
46
|
const raw = buffer.subarray(0, length);
|
|
45
47
|
try { return { settings: parseSettings(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(raw))), raw }; }
|
|
46
48
|
catch { throw new AdvisorError('invalid-config'); }
|
|
@@ -73,7 +75,7 @@ export async function saveSettings(paths: SettingsPaths, scope: Scope, value: un
|
|
|
73
75
|
// Preserve the independent field from the locked checkpoint, not a prior command read.
|
|
74
76
|
const next = { ...(preserve ? { [preserve]: before.settings[preserve] } : {}), ...settings };
|
|
75
77
|
const text = JSON.stringify(next, null, 2) + '\n';
|
|
76
|
-
if (Buffer.byteLength(text) >
|
|
78
|
+
if (Buffer.byteLength(text) > MAX_SETTINGS_BYTES) throw new AdvisorError('invalid-config');
|
|
77
79
|
const output = await open(temp, 'wx', 0o600); ownedTemp = true;
|
|
78
80
|
try { await output.writeFile(text, 'utf8'); await output.sync(); } finally { await output.close(); }
|
|
79
81
|
const current = await readLayer(path);
|