@ian-pascoe/pi-advisor 0.6.0 → 0.8.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `@ian-pascoe/pi-advisor` reviews a Pi agent's completed work and surfaces concise, attributed findings for material problems and worthwhile low-risk cleanup.
4
4
 
5
- Requires Node `>=22.19.0` and Pi `>=0.99.0`.
5
+ Requires Node `>=22.19.0` and Pi `>=1.1.0`.
6
6
 
7
7
  ## Install
8
8
 
@@ -30,13 +30,15 @@ Advisor is **disabled by default**. Configuration precedence is session, trusted
30
30
  /advisor set seedBudgetTokens 40000
31
31
  /advisor set reviewEvery "request"
32
32
  /advisor set maxSessionTokens 150000
33
+ /advisor set maxToolResultChars 2000
34
+ /advisor set anthropicLongCache true
33
35
  ```
34
36
 
35
37
  Argument autocomplete suggests command names, settings keys, and valid trailing scope flags.
36
38
 
37
- In the interactive TUI, `/advisor` alone opens a settings menu built from Pi's native settings list, like `/settings`. A Scope row chooses where edits are written (session, trusted project, or global), and each setting shows its effective value with any non-default source. `enabled`, `includeSubagents`, and `thinkingLevel` cycle the selected scope's own value in place (`inherit → on → off`, or through each thinking level); while that scope has none, the row shows what it inherits, such as `inherit (on · project)`, and a value overridden by a higher-precedence scope is marked. Model, tools, numbers, and the prompt open submenus, and the prompt uses Pi's own editor component. Every edit is validated and applied immediately; a paused Advisor shows its reason and a Resume row. Closing the menu records one status entry listing the changes it applied, or nothing if none were. Without the TUI, `/advisor` records the status entry instead.
39
+ In the interactive TUI, `/advisor` alone opens a settings menu built from Pi's native settings list, like `/settings`. A Scope row chooses where edits are written (session, trusted project, or global), and each setting shows its effective value with any non-default source. `enabled`, `includeSubagents`, `anthropicLongCache`, and `thinkingLevel` cycle the selected scope's own value in place (`inherit → on → off`, or through each thinking level); while that scope has none, the row shows what it inherits, such as `inherit (on · project)`, and a value overridden by a higher-precedence scope is marked. Model, tools, numbers, and the prompt open submenus, and the prompt uses Pi's own editor component. Every edit is validated and applied immediately; a paused Advisor shows its reason and a Resume row. Closing the menu records one status entry listing the changes it applied, or nothing if none were. Without the TUI, `/advisor` records the status entry instead.
38
40
 
39
- `/advisor status` and the other subcommands each record a status entry in the transcript. Collapsed, it shows state, model, backlog, usage and cost, the number of Reviews with their running cost and the last Review's cost, unavailable tools, and the last error; expanded, it lists every setting with its source and each watched Child Agent. A mutating command's entry starts with the changes it applied. While enabled, the footer shows `advisor` when idle, `advisor: reviewing`/`consulting` with any `backlog N`, or `advisor: paused`, plus Child Agents that are reviewing or paused. `advisor_ask` renders its question and a Markdown answer preview, and Child Agent findings carry a `↳ <agentId>` label.
41
+ `/advisor status` and the other subcommands each record a status entry in the transcript. Collapsed, it shows state, model, backlog, usage and cost, the number of Reviews with their running cost and the last Review's cost, unavailable tools, and the last error; expanded, it lists every setting with its source and each watched Child Agent. A mutating command's entry starts with the changes it applied. While enabled, the footer shows `● advisor on` when idle, `● advisor reviewing`/`consulting` with any `backlog N`, or `! advisor paused`, plus Child Agents that are reviewing or paused. Interventions and status entries use Pi's custom-message box with a bold `Advisor` label; an Intervention's severity is a coloured word (`nit`, `concern`, `blocker`), and Child Agent findings add the agent ID. `advisor_ask` renders as Pi's built-in tools do: a lowercase `advisor_ask` header with the question, a Markdown answer collapsed to ten lines with Pi's expand hint, and Pi's `Elapsed`/`Took` footer. Warnings and errors read `Advisor: ...`.
40
42
 
41
43
  Interventions render with their severity and Advisor attribution. Long Nits collapse until expanded; Concerns and Blockers always show in full.
42
44
 
@@ -50,7 +52,7 @@ The tool is absent while Advisor is disabled. A paused Advisor keeps it visible
50
52
 
51
53
  Consultation authorizes analysis and investigation, not implementation or other side effects. The configured Advisor Prompt remains authoritative. Tool Grants still expose each granted tool's full native interface, so exclude mutating tools when a prompt-level boundary is insufficient.
52
54
 
53
- `prompt` opens Pi's native editor and replaces the whole Advisor Prompt. `inherit` removes an override at the selected scope. Invalid keys and values are rejected. Lists, including `allowedTools`, replace the inherited list rather than merge. `catchUpThreshold` accepts any positive safe integer or `"off"`; `reviewTimeoutMs` accepts 1–2,147,483,647 milliseconds (the native timer range); `maxFindingsPerReview` accepts an integer from 1 through 32; `maxNitsPerRequest` accepts any non-negative safe integer, where `0` delivers no Nits; `seedBudgetTokens` and `maxSessionTokens` accept any positive safe integer or `"auto"`; `reviewEvery` accepts `"turn"`, `"request"`, or any positive safe integer number of turns.
55
+ `prompt` opens Pi's native editor and replaces the whole Advisor Prompt. `inherit` removes an override at the selected scope. Invalid keys and values are rejected. Lists, including `allowedTools`, replace the inherited list rather than merge. `catchUpThreshold` accepts any positive safe integer or `"off"`; `reviewTimeoutMs` accepts 1–2,147,483,647 milliseconds (the native timer range); `maxFindingsPerReview` accepts an integer from 1 through 32; `maxNitsPerRequest` accepts any non-negative safe integer, where `0` delivers no Nits; `seedBudgetTokens` and `maxSessionTokens` accept any positive safe integer or `"auto"`; `maxToolResultChars` accepts any positive safe integer; `reviewEvery` accepts `"turn"`, `"request"`, or any positive safe integer number of turns; `anthropicLongCache` accepts `true` or `false`.
54
56
 
55
57
  `advisor_ask` declares MCP-style `annotations` (read-only, non-destructive, idempotent, closed-world) that Pi reports through `pi.getAllTools()` for permission extensions; Pi does not send them to model providers. They describe the consultation itself, not the tools granted to the Advisor. The Advisor Session's internal `advisor_report` tool, which extensions inherited by that session can see, declares non-destructive, closed-world annotations and is not read-only, because it records the Review's findings.
56
58
 
@@ -60,20 +62,24 @@ Consultation authorizes analysis and investigation, not implementation or other
60
62
  | ------------------------------ | -------------------------------------------------------------- |
61
63
  | Enabled | `false` |
62
64
  | Watched sessions | Main only |
65
+ | Thinking level | `high`, independent of the observed agent |
63
66
  | Allowed tools | `read`, `grep`, `find`, `ls` |
64
67
  | Catch-up threshold | `3` |
65
68
  | Review deadline | 120 seconds |
66
69
  | Investigative calls per Review | 8 |
67
70
  | Findings per Review | 4 |
68
71
  | Nits per request | 3 |
69
- | Context Seed budget | `auto` (¼ of the Advisor model's context window, at most 100k) |
72
+ | Context Seed budget | `auto` (¼ of the Advisor model's context window, at most 50k) |
70
73
  | Review Cadence (`reviewEvery`) | `turn` |
71
- | Advisor Session size | `auto` (½ of the Advisor model's context window, at most 200k) |
74
+ | Advisor Session size | `auto` (½ of the Advisor model's context window, at most 100k) |
75
+ | Tool-result text cap | 4,000 characters (`maxToolResultChars`) |
76
+ | OpenAI prompt cache retention | 24 hours on Advisor requests (`openai` provider) |
77
+ | Anthropic 1h prompt cache | `false` (`anthropicLongCache`) |
72
78
  | Automatic Corrective Turns | 1 per request/task |
73
79
 
74
80
  A Tool Grant names tools; it does not sandbox their full native interfaces. Explicitly granting `bash`, for example, permits any command, including mutations. Unavailable names are ignored and reported; status reflects the Advisor Session at that moment, so a granted tool that registers later, such as an MCP tool after its server connects, is reported until it does. Advisor never autogrants missing tools. Granted tools with `codemode` or `deferred` exposure (such as tools from Pi's built-in MCP) are not declared to the Advisor model; they stay callable from granted `codemode` scripts, and a granted `tool_search` can declare them, as in the observed agent. Granting `codemode` lets the Advisor script its other granted tools; it inherits the observed `codemode.mode`, so under `"only"` the Advisor calls `advisor_report` from a script. It also inherits whether scripts get the `models` API: Minimal Subagents Child Agents have none, so neither do their Advisors.
75
81
 
76
- The Advisor inherits each observed agent's model and thinking level independently unless configured otherwise. One root policy can cover current and future Minimal Subagents descendants when enabled.
82
+ The Advisor inherits each observed agent's model unless configured otherwise. An observed model change rebuilds the Advisor Session only when the model is inherited; an Advisor with its own `model` keeps its session. The design notes explain why the inherited case reseeds. Its thinking level is not inherited: unless `thinkingLevel` is set in some layer (session, project, or global), it is `high` whatever the observed agent uses. Review work gains finding quality from `high` reasoning, while reasoning tokens bill at the output price, so following an observed agent at `xhigh` or `max` adds cost for little benefit; a fixed default also means an observed thinking change never discards the Advisor Session or an in-flight Review. Set `thinkingLevel` (for example `low` or `medium` to cut output cost) to override it; explicit values and layer precedence work as for every other setting, and `/advisor inherit thinkingLevel` returns to the default. The `high` default was chosen on this rationale, not from a measured finding-quality evaluation. One root policy can cover current and future Minimal Subagents descendants when enabled.
77
83
 
78
84
  ## Sessions and Context Management
79
85
 
@@ -81,7 +87,9 @@ Each watched agent has a private, persisted native Advisor Session with fresh, s
81
87
 
82
88
  Reviews and Consultations receive Review Evidence: what the observed model saw, not Pi's stored records. Messages keep their role, text, reasoning text, tool calls with arguments, tool-result text with error status, and image attachments; replay signatures, display-only tool `details`, provider metadata, and native IDs are omitted. Each tool call and its result share a short reference. Redacted reasoning and responses that errored, aborted, or hit the output limit are marked. The first Review or Consultation in an Advisor Session carries a Context Seed: the observed system prompt and each observed tool's name and one-line summary (not its full description or schema), plus the current conversation. Later Reviews and Consultations add only new messages.
83
89
 
84
- The current conversation is already bounded by the observed session's native compaction, but can still be very large, so the Context Seed is fitted to `seedBudgetTokens`: Pi's chars/4 estimate of the seed JSON the Advisor receives, plus Pi's per-image estimate. The first Review or Consultation always sees the Observed Setup; the original request; and the newest turn with the request that prompted it. The original request is the first message the user typed into the current context; after compaction it is the compaction summary, which records the earlier goal, plus the first message the user typed after it. Compaction summaries, delivered Advisor findings, other extension messages, and `!` command output are never taken for a request. The rest of the budget holds the newest turns that fit, oldest first. A turn is one message, or an assistant message with all of its tool results, so a tool call is never separated from its result. Text that alone exceeds the remaining budget is shortened with a marker. When messages are omitted or shortened, the seed lists the positions, in the observed context, of the messages it keeps and names the observed session file, so granted tools such as `read` or `grep` can inspect the full messages. The Advisor's own `context_history` covers only its private session. Later Reviews still add only messages newer than the seed; omitted messages are not sent later. `auto`, the default, uses a quarter of the Advisor model's context window (Pi's 128k fallback when a model declares none), at most 100k tokens: the seed is resent with every inference of the first Review and stays in the Advisor Session, so the rest is left for the Advisor Prompt, investigation, and later Reviews, and an `auto` seed always fits within half the `auto` `maxSessionTokens`. Models with windows above 400k tokens get the 100k ceiling rather than a quarter of their window. An explicit budget is capped at the Advisor model's context window.
90
+ Each tool result's text is capped at `maxToolResultChars` (default 4,000 characters), in the Context Seed and in every incremental update: a longer result keeps its first and last half-limit around a marker, `[… N characters omitted from this tool result; the full result is in the observed session file <path>]`, so granted tools such as `read` or `grep` can recover it. The result keeps its short reference, tool name, and error status, so a finding can still cite it. Local sessions showed tool results as about 45% of Review Evidence characters (median 657, 95th percentile about 8k, 99th about 24k), and the Advisor pays for each as a cache write when supplied and as a cache read on every later Review until compaction; a 4k cap saves about 16% of evidence characters (2k, 24%; 8k, 8%). The cap is applied before the seed is fitted to its budget and before the calibrated estimate measures new evidence, so the budget and the [Token calibration](#token-calibration) both describe the capped evidence the Advisor receives. User messages, assistant text, and reasoning are never capped. Capping is a pure function of the message and the setting, so projecting the same messages again gives byte-identical evidence. Changing `maxToolResultChars`, like any configuration change, rebuilds the Advisor Session from a new Context Seed capped at the new value, and findings still pending are retracted. Guardian shares the projection but does not cap tool results.
91
+
92
+ The current conversation is already bounded by the observed session's native compaction, but can still be very large, so the Context Seed is fitted to `seedBudgetTokens`, counted in the Advisor model's reported tokens. Pi's chars/4 estimate of the seed JSON the Advisor receives, plus Pi's per-image estimate, undercounts Review Evidence by about 1.7–1.9× on Claude models, so Advisor scales the estimate by a per-model factor ([Token calibration](#token-calibration)). The first Review or Consultation always sees the Observed Setup; the original request; and the newest turn with the request that prompted it. The original request is the first message the user typed into the current context; after compaction it is the compaction summary, which records the earlier goal, plus the first message the user typed after it. Compaction summaries, delivered Advisor findings, other extension messages, and `!` command output are never taken for a request. The rest of the budget holds the newest turns that fit, oldest first. A turn is one message, or an assistant message with all of its tool results, so a tool call is never separated from its result. Text that alone exceeds the remaining budget is shortened with a marker. When messages are omitted or shortened, the seed lists the positions, in the observed context, of the messages it keeps and names the observed session file, so granted tools such as `read` or `grep` can inspect the full messages. The Advisor's own `context_history` covers only its private session. Later Reviews still add only messages newer than the seed; omitted messages are not sent later. `auto`, the default, uses a quarter of the Advisor model's context window (Pi's 128k fallback when a model declares none), at most 50k tokens: the seed is resent with every inference of the first Review and stays in the Advisor Session, so the rest is left for the Advisor Prompt, investigation, and later Reviews, and an `auto` seed always fits within half the `auto` `maxSessionTokens` in reported tokens. Models with windows above 200k tokens get the 50k ceiling rather than a quarter of their window. The ceiling is half the `auto` Advisor Session cap, and gives up verbatim recall of an older conversation for a cheaper first Review: omitted messages stay reachable through the granted tools. An explicit budget is capped at the Advisor model's context window.
85
93
 
86
94
  Context Management is optional. If loaded and its tools are granted, `context_notes`, `context_history`, and `context_rollover` operate on the Advisor's private context. If any of those three tools is excluded, the Advisor pauses before reviewing. There is no autogrant or hook bypass.
87
95
 
@@ -89,18 +97,34 @@ Context Management is optional. If loaded and its tools are granted, `context_no
89
97
 
90
98
  Each Review is a new prompt in the same private Advisor Session, so its input is the whole Advisor Session so far: mostly prompt-cache reads, but billed on every Review. Two settings bound that cost.
91
99
 
92
- `reviewEvery` sets the Review Cadence: when Reviews run. Whatever the cadence, a Review covers every turn not yet reviewed, and only messages the Advisor has not yet seen are added. When those messages would exceed `seedBudgetTokens`, as a coarse cadence can gather or, under any cadence, a single very large turn, Advisor instead rebuilds the Advisor Session from a Context Seed within that budget. Like any rebuild, this withdraws earlier Concerns and Blockers that were queued to steer the observed agent but not yet consumed; findings already in its context stay.
100
+ `reviewEvery` sets the Review Cadence: when Reviews run. Whatever the cadence, a Review covers every turn not yet reviewed, and only messages the Advisor has not yet seen are added. When those messages would exceed `seedBudgetTokens` (in reported tokens, scaled by the same factor), as a coarse cadence can gather or, under any cadence, a single very large turn, Advisor instead rebuilds the Advisor Session from a Context Seed within that budget. Like any rebuild, this withdraws earlier Concerns and Blockers that were queued to steer the observed agent but not yet consumed; findings already in its context stay.
93
101
 
94
- - `"turn"`, the default, reviews after every turn (one model response and its tool batch). Findings arrive soonest, and Concerns and Blockers can steer the agent while it still runs; cost grows with the number of turns.
102
+ - `"turn"`, the default, reviews after every turn (one model response and its tool batch) that is not read-only. Findings arrive soonest, and Concerns and Blockers can steer the agent while it still runs; cost grows with the number of turns.
95
103
  - `N` reviews after every `N` turns and again when the request completes, for any turns left over.
96
104
  - `"request"` reviews once when the request completes: the agent's run has ended, including its steering and follow-up messages, and Pi will not retry it automatically. This is the cheapest cadence, but findings arrive only after the agent has finished, so a Concern waits for the next continuation and a Blocker can only prompt a Corrective Turn.
97
105
 
98
- Under `N` or `"request"`, a turn whose tool call failed still starts a Review at once. A Catch-up Wait waits only for a running Review: turns waiting for the next cadence point count toward the backlog but never start a Catch-up Wait themselves, and the wait ends when that Review finishes. Headless final drains and Minimal Subagents task completion review whatever turns remain, waiting up to one Review deadline (`reviewTimeoutMs`, 120 seconds by default) rather than the 30-second Catch-up Wait ceiling, so a Review covering a whole request can finish. A Review already running when the observed session compacts still finishes and delivers its findings; the next Review rebuilds the Advisor Session from the compacted context.
106
+ A read-only turn is one whose tool calls are all read-only: Pi's built-in `read`, `grep`, `find`, and `ls`, or a tool whose definition carries `readOnlyHint: true`, such as LSP queries, web search, `subagent_wait`, and `terminal_wait`. `bash` is never read-only, even for a command that only reads, and neither is a turn that also calls any other tool, a tool that is not registered, or that makes no tool calls. Under `"turn"`, a read-only turn joins the Review Backlog without starting a Review. The next turn that is not read-only, a turn with a failed tool call, or request completion reviews the backlog, so the Review still sees the exploration. This saves the Reviews of pure exploration, which rarely yield a finding alone. The trade-off is latency: a Concern about a misdirected search or a wrong file read arrives with the Review after the exploration ends, not one turn after it. `N` and `"request"` already batch turns and count read-only turns like any other.
107
+
108
+ Under `N` or `"request"`, a turn whose tool call failed still starts a Review at once, as it does under `"turn"`. A Catch-up Wait waits only for a running Review: turns waiting for their Review, whether for the next cadence point or because they only read, count toward the backlog but never start a Catch-up Wait themselves, and the wait ends when that Review finishes. Headless final drains and Minimal Subagents task completion review whatever turns remain, waiting up to one Review deadline (`reviewTimeoutMs`, 120 seconds by default) rather than the 30-second Catch-up Wait ceiling, so a Review covering a whole request can finish. A Review already running when the observed session compacts still finishes and delivers its findings; the next Review rebuilds the Advisor Session from the compacted context.
99
109
 
100
- `maxSessionTokens` bounds the Advisor Session itself. After a Review, when the Advisor Session's context exceeds the cap, Advisor compacts it with Pi's native compaction (`compact()`, as `/compact` does), which summarizes older Advisor history with the Advisor model and keeps Pi's `compaction.keepRecentTokens` of recent history; a cap below `keepRecentTokens` acts as `keepRecentTokens`. The summary is asked to keep the observed request, findings already reported, open concerns, and what the Advisor verified. Observed evidence continues incrementally afterwards, and delivered and deferred findings are kept by Advisor itself. Compaction is one extra Advisor model call that re-reads the history it summarizes without cache hits, counted in that Review's cost. It runs after the Review's findings are delivered, under its own deadline of `reviewTimeoutMs`. If it fails or times out, for example because an inherited extension cancels it, Advisor does not pause: it discards the Advisor Session, and the next Review starts a new one from a Context Seed. Pi declining because the whole Advisor Session is recent history it keeps is not a failure. `auto` takes half the Advisor model's context window (Pi's 128k fallback when a model declares none), at most 200k tokens: room for an `auto` Context Seed plus as much again for later Reviews, so a full seed alone never forces compaction, and far below Pi's own threshold (the window less `compaction.reserveTokens`), where each Review would re-read almost a full window. The 200k ceiling exists because the per-Review cost follows the Advisor Session's size, not the model's window: with a 1M-token window, half the window is about 500k tokens, where an Advisor Session was measured costing at least $0.12 for each Review that found nothing, and $0.20–0.34 for typical ones, from cache reads alone, more than the observed agent cost over the same period. At 200k, the same prices bound that floor at about 40% of it (about $0.05). A lower cap makes each Review cheaper at the price of more frequent compaction; a higher one, up to the window, compacts less often. When Context Management is loaded in the Advisor Session, its Rollover manages that context instead, and `maxSessionTokens` is not applied.
110
+ `maxSessionTokens` bounds the Advisor Session itself. After a Review, when the Advisor Session's context exceeds the cap, Advisor compacts it with Pi's native compaction (`compact()`, as `/compact` does), which summarizes older Advisor history with the Advisor model and keeps Pi's `compaction.keepRecentTokens` of recent history; a cap below `keepRecentTokens` acts as `keepRecentTokens`. The summary is asked to keep the observed request, findings already reported, open concerns, and what the Advisor verified. Observed evidence continues incrementally afterwards, and delivered and deferred findings are kept by Advisor itself. Compaction is one extra Advisor model call that re-reads the history it summarizes without cache hits, counted in that Review's cost. It runs after the Review's findings are delivered, under its own deadline of `reviewTimeoutMs`. If it fails or times out, for example because an inherited extension cancels it, Advisor does not pause: it discards the Advisor Session, and the next Review starts a new one from a Context Seed. Pi declining because the whole Advisor Session is recent history it keeps is not a failure. `auto` takes half the Advisor model's context window (Pi's 128k fallback when a model declares none), at most 100k tokens: room for an `auto` Context Seed plus as much again for later Reviews, so a full seed alone never forces compaction (seed budgets count reported tokens, so this holds on models whose tokens outnumber Pi's estimate), and far below Pi's own threshold (the window less `compaction.reserveTokens`), where each Review would re-read almost a full window. The 100k ceiling exists because the per-Review cost follows the Advisor Session's size, not the model's window: with a 1M-token window, half the window is about 500k tokens, where an Advisor Session was measured costing at least $0.12 for each Review that found nothing, and $0.20–0.34 for typical ones, from cache reads alone, more than the observed agent cost over the same period; cache reads were the largest Advisor cost (44% of $33.72 across 338 measured responses). Ceilings count reported tokens, which the Token calibration below makes possible: against Pi's estimate alone, 100k would have meant about 175k real tokens for a seed. The ceiling trades cost against recall. Compaction is a separate, uncached call that summarizes each Advisor token about once, so its cost per Review barely depends on the cap, while the read cost of every Review grows with the session. With Pi's default `compaction.keepRecentTokens` of 20k, about 5k of new evidence per Review, cache reads at 0.1× the input price, and a summary costing about 15k input tokens, the modeled cost per Review is about 11k input-token equivalents at 200k, 6–7k at 80–100k, and lowest near 60k. The cost of 100k is more frequent compaction, and older evidence reaching the Advisor only as a summary instead of verbatim; the granted tools can still read the full observed session. A higher cap, up to the window, compacts less often and recalls more, at a higher cost for every Review. When Context Management is loaded in the Advisor Session, its Rollover manages that context instead, and `maxSessionTokens` is not applied.
101
111
 
102
112
  `/advisor status` shows the cost of the last Review, including any compaction it ran, and the running total of Reviews since the session started or Advisor was reloaded, from the native usage the Advisor Session records. Reviews that failed or were cancelled after prompting the Advisor count, since they were billed; Consultations do not. Its `tokens` and `cost` instead cover the current Advisor Session, including Consultations, and restart when that session is rebuilt. Unknown cost, such as for a model without prices, is shown as unknown; a Review that reported no usage on a priced model costs $0. Expanded, status also shows each Child Agent's Review cost.
103
113
 
114
+ ### Prompt cache retention
115
+
116
+ A provider's prompt cache lasts about five minutes by default, so a Review after a longer idle gap re-reads the whole Advisor Session uncached. Advisor asks for longer retention on its own requests only, as a per-request stream option on the Advisor Session's agent; it never sets the process-wide `PI_CACHE_RETENTION`, and the observed agent's requests are byte-for-byte unchanged.
117
+
118
+ - **OpenAI** (the `openai` provider): Advisor requests send `prompt_cache_retention: "24h"` (`prompt_cache_options.ttl` on models with explicit cache modes). It costs nothing extra, so it is always on.
119
+ - **Anthropic** (the `anthropic` provider): `anthropicLongCache` (default `false`) asks for the 1h cache TTL on Advisor requests. A 1h cache write costs 2× the input price instead of 1.25×, so enable it only when Reviews are routinely more than five minutes apart. With `false`, Advisor requests use the provider default, so a process-wide `PI_CACHE_RETENTION=long` still applies to them.
120
+ - **OpenAI Codex** (`openai-codex`) and other providers keep their default: Pi's Codex adapter sends no retention field, and ChatGPT sign-in rejects the OpenAI one.
121
+
122
+ Pi's own cache warming (`cacheWarming`) reads the retention of the request it replays, so it follows these lifetimes.
123
+
124
+ ### Token calibration
125
+
126
+ Pi's chars/4 estimate undercounts Review Evidence, which is JSON-heavy: on Claude models real tokens run about 1.7–1.9× the estimate for a large seed, so a seed fitted to an estimated 100k reached about 175k real tokens and pushed the Advisor Session past `maxSessionTokens` at once. Advisor therefore measures `seedBudgetTokens`, and whether new evidence fits it, in the Advisor model's reported tokens. It fits the seed by the estimate scaled by a per-model factor. The factor starts at 2 and follows the Advisor's own Reviews and Consultations: each prompt that added at least 2,000 estimated tokens compares the input tokens its first response reported, less the Advisor Session's size before the prompt, with Pi's estimate of the prompt. Before a session's first response Pi counts only its messages, so Advisor adds Pi's estimate of the system prompt and active tool definitions to that size; afterwards the previous response's reported usage already includes them. The factor is that ratio rounded up to a quarter, changing only when the ratio moves clearly past it, so a steady model keeps its evidence window, and with it the prompt cache, stable. The same helper, `@ian-pascoe/pi-utils/token-calibration`, calibrates Guardian's evidence budget. Advisor keeps its factors in memory for the life of the observer, not in the session, so after a reload the first seed uses the fallback of 2 again; a smaller fallback would overshoot dense models, and a larger one only shrinks a seed until the first Review reports usage.
127
+
104
128
  ## Scheduling and safety
105
129
 
106
130
  Reviews combine completed observed turns (one model response plus its tool batches), as often as `reviewEvery` allows, and use bounded catch-up waits of at most 30 seconds. Each Review has its own deadline, investigative-call budget, and configurable finding limit. One terminating report returns findings in severity order. Formatting-equivalent findings retain their highest severity within a Review; later escalation from Nit to Concern to Blocker remains deliverable. Distinct Concerns are delivered together when eligible or retained together for re-evaluation during the three-turn cooldown.
@@ -109,7 +133,7 @@ Reviews combine completed observed turns (one model response plus its tool batch
109
133
 
110
134
  Each finding must name a concrete defect in work the observed agent has already done and cite its evidence: the short `ref` that Review Evidence gives each tool call and its result, or a short verbatim quote. Advice about what to do, test, or say next belongs in a consultation (`advisor_ask`), not in an Intervention. The default Advisor Prompt asks for this, and `advisor_report` requires an `evidence` object with `refs`, a `quote`, or both on every finding, so a replaced Advisor Prompt keeps the requirement. Advisor drops a finding that cites no evidence, or cites a `ref` it never supplied to that Advisor Session, and tells the Advisor which findings it dropped; quotes are not checked against the evidence. A report in the older single-finding form cites no evidence, so its finding is dropped too. An `advisor_report` call that does not match the report schema, such as a finding without `evidence`, is rejected with the reason so the Advisor can correct it; after two rejected calls the Review ends without findings rather than retrying until its deadline, and Advisor is not paused. Delivered Interventions keep their evidence in the journaled message details; the observed agent sees only the severity and message.
111
135
 
112
- A Review judges the turns completed when it started. If the observed agent completes more turns before the Review's findings are delivered, those findings are superseded: they may already be fixed or explained, so none of them is delivered. Instead, the next Review receives them alongside the newer turns, re-validates them, and reports again only those that still apply; this adds no model call. Under `reviewEvery: "turn"` that Review is already due. Under `N` or `"request"` it waits for the next cadence point, at the latest the Review when the request completes, which normally has no newer turns to supersede it (unless a new request has already started). Findings are withheld this way at most once. If the agent also outruns a Review that was given withheld findings, all of that Review's Concerns and Blockers are delivered anyway, including ones it reports for the first time, since it judged them against newer turns, and all its Nits are dropped. This is a deliberate starvation guard: it is applied per Review, so it holds even when the re-validating Review rewords a finding or cites newer references. A withheld Concern that the next Review defers for the Concern cooldown keeps that mark. So findings are never held back for more than one extra Review, however fast turns arrive. Superseded findings still waiting when Advisor is disabled, reconfigured, resumed after a pause, or moved to another branch or session are dropped with the rest of its invalidated review state, and so are those whose re-validating Review does not finish within a headless or Child Agent final drain.
136
+ A Review judges the turns completed when it started. If the observed agent completes more turns before the Review's findings are delivered, those findings are superseded: they may already be fixed or explained, so none of them is delivered. Instead, the next Review receives them alongside the newer turns, re-validates them, and reports again only those that still apply; this adds no model call. Under `reviewEvery: "turn"` that Review is due at once, even if the turns since were read-only: withheld findings make the backlog due, so a finding about an edit is not held until the next edit or the request's end. Under `N` or `"request"` it waits for the next cadence point, at the latest the Review when the request completes, which normally has no newer turns to supersede it (unless a new request has already started). Findings are withheld this way at most once. If the agent also outruns a Review that was given withheld findings, all of that Review's Concerns and Blockers are delivered anyway, including ones it reports for the first time, since it judged them against newer turns, and all its Nits are dropped. This is a deliberate starvation guard: it is applied per Review, so it holds even when the re-validating Review rewords a finding or cites newer references. A withheld Concern that the next Review defers for the Concern cooldown keeps that mark. So findings are never held back for more than one extra Review, however fast turns arrive. Superseded findings still waiting when Advisor is disabled, reconfigured, resumed after a pause, or moved to another branch or session are dropped with the rest of its invalidated review state, and so are those whose re-validating Review does not finish within a headless or Child Agent final drain.
113
137
 
114
138
  `maxNitsPerRequest` (default 3) limits the Nits delivered per request; further Nits are dropped. Concerns and Blockers are never capped. A request runs from a user prompt (or a Minimal Subagents task) through its completion, including the Review at its completion and any Corrective Turn; the count restarts with the next request. Superseded Nits are kept for re-validation only while the request has room for them. `/advisor status` shows how many findings await re-validation and how many were dropped since the session started or Advisor was reloaded: Nits over the cap, findings with missing or unknown evidence, Nits from a re-validating Review that was superseded again, and Reviews ended by invalid reports. The Nit count follows the selected branch, so it survives a configuration change within a request.
115
139
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-advisor",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "description": "Background review and attributed corrective advice for Pi agent sessions.",
6
6
  "keywords": [
@@ -32,13 +32,13 @@
32
32
  "provenance": true
33
33
  },
34
34
  "dependencies": {
35
- "@ian-pascoe/pi-utils": "^0.3.1"
35
+ "@ian-pascoe/pi-utils": "^0.6.0"
36
36
  },
37
37
  "peerDependencies": {
38
- "@earendil-works/pi-agent-core": ">=0.99.0",
39
- "@earendil-works/pi-ai": ">=0.99.0",
40
- "@earendil-works/pi-coding-agent": ">=0.99.0",
41
- "@earendil-works/pi-tui": ">=0.99.0",
38
+ "@earendil-works/pi-agent-core": ">=1.1.0",
39
+ "@earendil-works/pi-ai": ">=1.1.0",
40
+ "@earendil-works/pi-coding-agent": ">=1.1.0",
41
+ "@earendil-works/pi-tui": ">=1.1.0",
42
42
  "typebox": "*"
43
43
  },
44
44
  "engines": {
@@ -19,7 +19,7 @@ Use this sequence for a live Advisor problem:
19
19
  8. For stale, duplicated, or unsafe advice, check branch/session identity and configuration-generation changes. Advisor Sessions are separate persisted native sessions with fresh extension resources. Unsupported opaque inline/custom resources pause rather than being reused. Nits use non-interrupting context delivery and never start Corrective Turns. Running Concerns and Blockers use Pi's native steering boundary; Blockers after normal interactive completion use only tracked, budgeted Corrective Turns. Deliberately interrupted, aborted, uncertain, and headless-completed work is preserved without restart. Child correction must finish inside Minimal's owned operation.
20
20
  - If a finding never arrived, check `/advisor status` for findings awaiting re-validation: a Superseded Finding is withheld when the agent completed more turns before delivery, and the next Review reports it again only if it still applies. Findings are withheld at most once: if that Review is superseded too, its Concerns and Blockers are delivered and its Nits dropped. Status also counts Nits dropped over `maxNitsPerRequest`, findings dropped for citing no evidence or an unknown Tool-Call Reference (the Advisor Session's `advisor_report` result names them), Nits from a superseded re-validating Review, and Reviews ended after two invalid `advisor_report` calls; frequent invalid reports suggest an Advisor model that cannot follow the report schema.
21
21
  - If findings are directives rather than defects, or lack evidence, check whether a replaced Advisor Prompt dropped the default's evidence and completed-work wording; `advisor_report` still requires evidence on every finding.
22
- 9. If a finding seems to miss tool output or schemas, remember that Review Evidence carries tool-result text but not display-only `details`, and the Context Seed's Observed Setup lists observed tools by name and one-line summary only. Grant read tools or use `advisor_ask` when the Advisor must inspect more than the observed model received. If an early finding misses older history, check `seedBudgetTokens`: the Context Seed keeps the Observed Setup, the original request, the newest turn, and the newest turns that fit, lists the observed messages it keeps, and names the observed session file for the rest.
22
+ 9. If a finding seems to miss tool output or schemas, remember that Review Evidence carries tool-result text but not display-only `details`, and caps each tool result's text to its head and tail at `maxToolResultChars` (default 4,000 characters; the marker names the observed session file, where the full result lives), and the Context Seed's Observed Setup lists observed tools by name and one-line summary only. Grant read tools or use `advisor_ask` when the Advisor must inspect more than the observed model received. If an early finding misses older history, check `seedBudgetTokens`: the Context Seed keeps the Observed Setup, the original request, the newest turn, and the newest turns that fit, lists the observed messages it keeps, and names the observed session file for the rest.
23
23
  10. If Advisor costs too much, compare the last Review's cost and the running total in `/advisor status` with the observed agent's cost. Each Review re-reads the whole Advisor Session: lower `maxSessionTokens` so Pi's native compaction keeps it smaller, or use a coarser `reviewEvery`, which delays findings. A compaction that fails or times out does not pause Advisor; the next Review instead starts a new Advisor Session from a Context Seed (`Current context seed.` in its prompt). Context Management's Rollover replaces this compaction where it is loaded.
24
24
  11. On shutdown, allow only the bounded headless final drain, which waits up to one `reviewTimeoutMs` Review deadline. Verify that no hidden corrective turn starts after root completion and that native Advisor resources are aborted before disposal.
25
25
 
@@ -0,0 +1,62 @@
1
+ import { estimateTokens, type AgentSession } from "@earendil-works/pi-coding-agent";
2
+ import type { TokenSample } from "@ian-pascoe/pi-utils/token-calibration";
3
+
4
+ /**
5
+ * Factor assumed for an Advisor model before any of its Reviews reports usage. Review Evidence
6
+ * tokenizes about 1.7–1.9× denser than Pi's chars/4 estimate on Claude models, so this errs
7
+ * toward a smaller seed rather than one that overshoots `seedBudgetTokens`.
8
+ */
9
+ export const advisorFallbackTokenFactor = 2;
10
+
11
+ /** The key a model's calibrated factor is kept under. */
12
+ export function calibrationKey(model: { provider: string; id: string } | undefined): string {
13
+ return model ? `${model.provider}/${model.id}` : "unknown";
14
+ }
15
+
16
+ /** Pi's chars/4 estimate of the system prompt and active tool definitions a provider receives. */
17
+ function setupEstimate(session: AgentSession): number {
18
+ const active = new Set(session.getActiveToolNames());
19
+ const tools = session
20
+ .getAllTools()
21
+ .filter(({ name }) => active.has(name))
22
+ .map(({ name, description, parameters }) => ({ name, description, parameters }));
23
+ return Math.ceil(JSON.stringify([session.systemPrompt, tools]).length / 4);
24
+ }
25
+
26
+ /**
27
+ * The Advisor Session's size in tokens before a prompt, as the provider counts it, or null when
28
+ * Pi reports none (such as just after compaction). Pi's `getContextUsage()` counts the system
29
+ * prompt and tool definitions only through the last response's reported usage; a session that has
30
+ * not yet recorded its system message, and so has no usage, reports just its messages, so their
31
+ * Pi estimate is added. `totalTokens` includes the previous response's output tokens, which the
32
+ * next request resends as input, so that is the right baseline.
33
+ */
34
+ export function contextBeforePrompt(session: AgentSession): number | null {
35
+ const tokens = session.getContextUsage()?.tokens;
36
+ if (tokens === null || tokens === undefined) return null;
37
+ const hasSystem = session.messages.some((message) => message.role === "system");
38
+ return hasSystem ? tokens : tokens + setupEstimate(session);
39
+ }
40
+
41
+ /**
42
+ * What one prompt taught about its model: Pi's chars/4 estimate of the prompt that carried the
43
+ * evidence, and the input tokens it added to the Advisor Session, which is the first response's
44
+ * input less `contextBefore` (see `contextBeforePrompt`, which includes the cached system prompt
45
+ * and tools). With no size known, a response without usage, or an error, nothing is learned.
46
+ */
47
+ export function promptSample(
48
+ messages: AgentSession["messages"],
49
+ before: number,
50
+ contextBefore: number | null | undefined,
51
+ ): TokenSample | undefined {
52
+ if (contextBefore === null || contextBefore === undefined) return;
53
+ const added = messages.slice(before);
54
+ const prompt = added.find((message) => message.role === "user");
55
+ const response = added.find((message) => message.role === "assistant");
56
+ if (!prompt || response?.role !== "assistant") return;
57
+ if (response.stopReason === "error" || response.stopReason === "aborted") return;
58
+ const { input, cacheRead, cacheWrite } = response.usage;
59
+ const reported = input + cacheRead + cacheWrite - contextBefore;
60
+ if (!(reported > 0)) return;
61
+ return { estimated: estimateTokens(prompt), reported };
62
+ }
@@ -1,64 +1,43 @@
1
+ import {
2
+ completeSettingsCommandArguments,
3
+ parseSettingsCommand,
4
+ type SettingsCommand,
5
+ } from "@ian-pascoe/pi-utils/settings-command";
1
6
  import type { AutocompleteItem } from "@earendil-works/pi-tui";
2
7
  import {
3
8
  advisorOptionKey,
4
9
  advisorOptionKeys,
5
10
  parseAdvisorOptions,
6
11
  type AdvisorOptions,
12
+ type AdvisorSettingScope,
7
13
  } from "./advisor-settings.js";
8
14
 
9
15
  /** Native command completion values replace the entire argument prefix. */
10
16
  export function completeAdvisorCommandArguments(prefix: string): AutocompleteItem[] {
11
- const candidates = ["on", "off", "status", "prompt", "inherit", "set"];
12
- const keyPrefix = /^((?:set|inherit)\s+)\S*$/.exec(prefix)?.[1];
13
- if (keyPrefix) candidates.push(...advisorOptionKeys.map((key) => `${keyPrefix}${key}`));
14
- const scopePrefix = /^(.*\s+)(--\S*)?$/s.exec(prefix)?.[1];
15
- if (scopePrefix) {
16
- try {
17
- const command = parseAdvisorCommand(scopePrefix);
18
- if ("scope" in command && command.scope === "session")
19
- candidates.push(`${scopePrefix}--global`, `${scopePrefix}--project`);
20
- } catch {
21
- // Incomplete commands and JSON values cannot accept a scope yet.
22
- }
23
- }
24
- return candidates
25
- .filter((value) => value.startsWith(prefix))
26
- .map((value) => ({ value, label: value }));
17
+ return completeSettingsCommandArguments(prefix, {
18
+ words: ["on", "off", "status", "prompt", "inherit", "set"],
19
+ optionKeys: advisorOptionKeys,
20
+ parse: parseAdvisorCommand,
21
+ });
27
22
  }
28
23
 
29
24
  const usage =
30
25
  "Usage: /advisor [on|off|status|prompt|inherit [key]|set <key> <JSON>] [--global|--project]; /advisor alone opens settings";
31
26
 
27
+ /** One parsed `/advisor` command. */
28
+ export type AdvisorCommand = SettingsCommand<
29
+ AdvisorOptions,
30
+ keyof AdvisorOptions,
31
+ { action: "prompt"; scope: AdvisorSettingScope }
32
+ >;
33
+
32
34
  /** Parse one configuration change; validated patches cannot invent option keys. */
33
- export function parseAdvisorCommand(input: string) {
34
- const flag = /\s+--(global|project)$/.exec(input.trim());
35
- const scope =
36
- flag?.[1] === "global"
37
- ? ("global" as const)
38
- : flag?.[1] === "project"
39
- ? ("project" as const)
40
- : ("session" as const);
41
- const text = flag ? input.trim().slice(0, flag.index) : input.trim();
42
- if (text === "" || text === "status") {
43
- if (flag) throw new Error(usage);
44
- return text === "" ? { action: "menu" as const } : { action: "status" as const };
45
- }
46
- if (text === "prompt") return { action: "prompt" as const, scope };
47
- if (text === "on" || text === "off") {
48
- return {
49
- action: "set" as const,
50
- scope,
51
- key: "enabled" as const,
52
- patch: { enabled: text === "on" },
53
- };
54
- }
55
- const inherit = /^inherit(?:\s+(\S+))?$/.exec(text);
56
- if (inherit) {
57
- return { action: "inherit" as const, scope, key: advisorOptionKey(inherit[1] ?? "enabled") };
58
- }
59
- const set = /^set\s+(\S+)\s+([\s\S]+)$/.exec(text);
60
- if (!set?.[1] || !set[2]) throw new Error(usage);
61
- const key = advisorOptionKey(set[1]);
62
- const patch: AdvisorOptions = parseAdvisorOptions({ [key]: JSON.parse(set[2]) }, scope);
63
- return { action: "set" as const, scope, key, patch };
35
+ export function parseAdvisorCommand(input: string): AdvisorCommand {
36
+ return parseSettingsCommand(input, {
37
+ usage,
38
+ optionKey: advisorOptionKey,
39
+ parseOptions: parseAdvisorOptions,
40
+ toggle: (enabled) => ({ key: "enabled", patch: { enabled } }),
41
+ parseExtra: (text, scope) => (text === "prompt" ? { action: "prompt", scope } : undefined),
42
+ });
64
43
  }