Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 44 additions & 89 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,129 +1,84 @@
# Advisor

<p align="center">
<img src="docs/assets/advisor-overview.png" alt="Advisor Overlay reviewing a Primary Agent run" width="900">
<img src="docs/assets/advisor-overview.png" alt="Advisor reviewing a Primary Agent run in Pi" width="900">
</p>

Advisor is a session-persistent second agent that works alongside the Primary Agent. It asynchronously reviews the Primary Agent's work, gives the user a Second Opinion, and, with user confirmation or during a Watch Run, delivers useful insights to the Primary Agent.
Advisor combines a `btw`-style Second Opinion with an asynchronous watchdog for Pi. Press `Option+/` on macOS to ask a dedicated reviewer whether the Primary Agent's latest answer is sound, or start a Watch Run and let Advisor follow the task in the background.

For the best pairing, use an expressive model that excels at documentation as the Primary Agent, then give Advisor a rigorous review model such as GPT. The two agents keep separate contexts, so Advisor can challenge the Primary Agent instead of echoing it.

## Installation

```bash
# Install from npm
pi install npm:@kkkiio/pi-advisor

# Or install from a local path
pi install ./path/to/pi-advisor
```

After installation, set the Advisor model.
Set the model that will act as Advisor:

```
```text
/advisor:model openai-codex/gpt-5.6-sol
```

## Goals

### Advisor goals

- **Review**: Identify bugs, design issues, workflow problems, and their root causes, then give the user an independent reviewer's Second Opinion. Advisor acts as a reviewer and does not implement features itself.
- **Guide**: Help the Primary Agent escape tunnel vision. Advisor supplies high-value facts, key files, APIs, constraints, and sequencing guidance to get it past obstacles, then applies a reviewer's perspective so it can inspect and correct its work before finishing.

### Product experience goal

- Keep background review quiet, relevant, traceable, and observable without disrupting the Primary Agent's workflow.
You can also install a local checkout with `pi install ./path/to/pi-advisor`.

## Usage

### `/advisor [<message>]`

Ask Advisor.

- **Without an argument**: Open the Advisor Overlay and focus its input box so you can type a message directly.
- **With an argument**: Open the Advisor Overlay and immediately start Ask Advisor with that message.

Message behavior depends on Advisor's current state:

- **Advisor is idle**: Start a new Ask Advisor run. The first Ask after the Primary Agent enters a new user turn automatically includes **Ask Context**: a `<primary-context>` payload whose markdown body uses the same format as Pull Transcript (including tool calls, results, and diffs). Subsequent Asks within the same Primary user turn send a position-only payload without repeating the body.
- **Advisor is running** (during Ask Advisor or Watch Run): Steer the active run with the message. Only the user's input is sent; Ask Context is not included.

Every Ask started while Advisor is idle tells Advisor the current position in the Primary Transcript and whether the Primary Agent is running. When a question requires more history, tool activity, or newer progress, Advisor can Pull the Primary Transcript itself, so the user does not need to copy context manually.

The Primary context sent to Advisor appears in a `Context` block in the Advisor Overlay. Its header shows the display-item count (including tool rows); expanding the block reveals the exact `<primary-context>` payload, which uses the same markdown format as Pull Transcript. When no new Ask Context is available for the current Primary user turn, a position-only `<primary-head>` payload records the current transcript position.

Ask Advisor and Watch Run share the same Advisor, and the Advisor Transcript remains continuous between them. Advisor does not have the `write` or `edit` tools; when changes are needed, it delegates them to the Primary Agent through a Second Opinion or Advice.

### `/advisor:handoff [instructions]`
### Ask whether the Primary Agent is right

Send the latest completed Ask Advisor Second Opinion to the Primary Agent as a user message. When `instructions` are omitted, the default instruction asks the Primary Agent to use the Second Opinion as reference context.
1. Press `Option+/` on macOS (`Alt+/` in Pi's terminal notation).
2. Ask Advisor a question such as: `Was the Primary Agent's last answer correct?`
3. Press `Option+/` again to return to the Primary Agent.

The Primary Agent receives the message immediately when idle. If it is busy, the message is queued as a follow-up.
Advisor automatically receives the latest Primary context and can pull more of the transcript when it needs to inspect earlier messages, tool calls, or diffs.

Handoff message format:
You can also open Advisor and ask in one command:

All three text elements are XML-escaped. When `instructions` are omitted, a default instruction is used.

```xml
<advisor-handoff>
<original-request>...</original-request>
<second-opinion>...</second-opinion>
<instructions>Use this Second Opinion as supporting context.</instructions>
</advisor-handoff>
```text
/advisor Review the Primary Agent's approach and point out anything it missed.
```

If Advisor is still processing the previous Ask, handoff waits for it to finish before sending the message. If no Ask Advisor Second Opinion has been completed, the user receives a clear notice. Handoff does not clear the Advisor Transcript.

### `/advisor:watch`

Start an asynchronous Watch Run. Advisor follows the Primary Agent's progress and decides when the review is complete. You can cancel it early with `/advisor:watch-off`.

During a Watch Run, Advisor automatically selects a delivery channel based on the intent of its Advice:

- **Hint** (accelerating information): Correct API usage, a better algorithm, and similar guidance are delivered promptly through Steer to reduce wasted effort.
- **Concern** (risk or challenge): Potential bugs, architectural concerns, and similar issues are delivered through Follow-up after the Primary Agent finishes its current work, preserving its flow.

Outside a Watch Run, Advisor does not send Advice on its own. You can still use `/advisor` to ask it to send a specific insight, or `/advisor:handoff` to pass along the latest completed Second Opinion.

### `/advisor:watch-off`
### Watch a task

Cancel the current Watch Run while preserving the Advisor instance and its existing Advisor Transcript.

### `/advisor:new`

Start a fresh Advisor conversation: clear the Advisor Transcript, Ask Context injection history, Second Opinion history, and input draft. If a Watch Run is active, cancel it first. The Overlay opens with its input focused so the new conversation is ready to use.

### `/advisor:model [model]`

Set the model used by Advisor.
```text
/advisor:watch
```

### `/advisor:thinking [level]`
A Watch Run follows the current Primary task asynchronously. Advisor can send timely guidance while work is in progress and queue concerns for the Primary Agent to handle after its current work. The Overlay can stay closed while the review continues.

Set the thinking level used by Advisor.
Stop the Watch Run at any time:

### Advisor Overlay
```text
/advisor:watch-off
```

The Overlay opens automatically after `/advisor`, `/advisor:new`, or `/advisor:watch`. It appears as a top-center panel and shows Advisor's review process in real time as prefixed transcript blocks: user messages, Ask Context, tool calls such as Pull, Hint, and Concern, and Advisor responses. The Overlay header shows Advisor's status and context-window usage, for example `Advisor · thinking · ctx 0.1%/128k`. When content extends beyond the visible area, `↑N ↓M` scroll indicators appear.
### Hand off a Second Opinion

A separate input box at the bottom of the Overlay accepts messages and control commands. A visible Overlay always owns keyboard focus; close it before returning to the Primary input. The Overlay input box supports these six control commands: `/advisor:watch`, `/advisor:watch-off`, `/advisor:handoff`, `/advisor:new`, `/advisor:model`, and `/advisor:thinking`.
After an Ask completes, send its latest Second Opinion to the Primary Agent:

**Overlay controls:**
```text
/advisor:handoff
```

- `Alt+/`: Open the Overlay with focus, or close it and return focus to the Primary input.
- `Esc`: Close the Overlay and return focus to the Primary input.
- Closing the Overlay preserves its transcript, active Watch Run, and input draft.
- Scroll the open Overlay transcript with ↑/↓, PgUp/PgDn, or the mouse wheel.
- Use Pi's `app.tools.expand` keybinding (Ctrl+O by default) to switch every Context and Pull block between a five-visual-line preview and the complete text payload sent to Advisor.
- When work finishes while the Overlay is closed, a notification tells you to press `Alt+/` to view it.
## Commands

After the user interrupts the Primary Agent, Advisor does not automatically wake it.
| Command | What it does |
| --------------------------------- | ---------------------------------------------- |
| `/advisor [message]` | Open Advisor or ask it immediately |
| `/advisor:watch` | Start an asynchronous Watch Run |
| `/advisor:watch-off` | Stop the Watch Run and preserve its transcript |
| `/advisor:handoff [instructions]` | Send the latest Second Opinion to Primary |
| `/advisor:new` | Start a fresh Advisor conversation |
| `/advisor:model [model]` | Show or set the Advisor model |
| `/advisor:thinking [level]` | Show or set the Advisor thinking level |

## Limitations
The Advisor Overlay is a top-center work view with its own input and transcript. `Option+/` opens or closes it, `Esc` returns to Primary, and Pi's `app.tools.expand` keybinding (`Ctrl+O` by default) expands Context and Pull blocks. Closing the Overlay preserves the conversation, input draft, and active Watch Run.

- **No disk persistence for the Advisor Transcript yet**: The Advisor Transcript is kept in memory only for the current session and is not saved when the session closes. This keeps the initial release simple while the disk-persistence design remains undecided.
Ask Advisor and Watch Run share one session-persistent Advisor. It reviews and guides; it does not receive file-writing tools. During a Watch Run, accelerating information is delivered as a Hint, while risks that should not interrupt active work are delivered as a Concern.

## Configuration

The configuration file is located at `~/.pi/agent/advisor.json`:
Advisor settings are stored in `~/.pi/agent/advisor.json`:

```json
{
Expand All @@ -132,8 +87,8 @@ The configuration file is located at `~/.pi/agent/advisor.json`:
}
```

The thinking level can be chosen with `/advisor:thinking` or set with `/advisor:thinking <level>`; when omitted, Advisor uses its built-in default.
The Advisor Transcript currently lives in memory for the Pi session and is not persisted after the session closes.

## Acknowledgments

This project was inspired by pi-btw, oh-my-pi, and pi-omplike-advisor.
Inspired by pi-btw, oh-my-pi, and pi-omplike-advisor.
Binary file modified docs/assets/advisor-overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 12 additions & 5 deletions e2e/fixtures/faux-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function scriptedResponse(
message.role === "user" &&
(contentText(message.content).includes("<primary-context ") ||
contentText(message.content).includes("<primary-head ")),
);
);
if (model.id === advisorModelId && process.env.PI_ADVISOR_TEST_OBSERVATIONS_PATH) {
const question = latestUserMessage ? contentText(latestUserMessage.content) : "";
const latestContextText = latestContextMessage ? contentText(latestContextMessage.content) : "";
Expand Down Expand Up @@ -119,9 +119,7 @@ function scriptedResponse(
}
}
if (script === "readme") {
return fauxAssistantMessage(
fauxText("Implemented request deduplication and eviction of failed in-flight entries."),
);
return fauxAssistantMessage(fauxText("`SessionJournal`: it is an append-only log for one session."));
}
if (script === "visual-natural") {
return fauxAssistantMessage("Primary 已完成 Pull 游标调整,并补充了等待和越界场景。");
Expand Down Expand Up @@ -188,8 +186,17 @@ function scriptedResponse(
return fauxAssistantMessage("E2E_USER_REQUESTED_ADVICE_DONE");
}
if (script === "readme") {
if (!hasToolResult(context, "read")) {
return fauxAssistantMessage(
[
fauxText("I’ll check whether the API is actually bound to one session."),
fauxToolCall("read", { path: "session-manager.ts" }),
],
{ stopReason: "toolUse" },
);
}
return fauxAssistantMessage(
"The direction is sound. One concern: a failed request may delete a newer in-flight entry. Guard eviction by entry identity before committing.",
"`SessionJournal` fits only if each instance stays bound to one append-only log. Since this API can switch files or create sessions, `SessionHandle` describes the mutable binding more honestly.",
);
}
if (script === "visual-overlay-pull-collapse") {
Expand Down
12 changes: 12 additions & 0 deletions e2e/support/tui-pi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ export class TuiPi {
const tmp = await realpath(tmpPath);
const agentDir = join(home, ".pi", "agent");
await writeFile(join(cwd, "README.md"), "# Advisor TUI E2E\n\nE2E_PRIMARY_SENTINEL lives here.\n", "utf8");
await writeFile(
join(cwd, "session-manager.ts"),
[
"export interface SessionManager {",
"\tsetSessionFile(path: string): void;",
"\tnewSession(): void;",
"\tappend(entry: SessionEntry): void;",
"}",
"",
].join("\n"),
"utf8",
);
await writeFile(
join(cwd, "package.json"),
`${JSON.stringify(
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
{
"name": "@kkkiio/pi-advisor",
"version": "0.2.0",
"description": "A pi extension for asynchronous Advisor review with /advisor",
"description": "A btw-style second opinion and watchdog for Pi, using a dedicated review model",
"type": "module",
"license": "Apache-2.0",
"keywords": [
"pi-package",
"pi-extension",
"pi-coding-agent",
"agent",
"code-review",
"second-opinion",
"watchdog",
"tui"
],
"files": [
Expand Down
22 changes: 8 additions & 14 deletions tests/visual/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,34 +20,28 @@ const scenarios: TuiVisualScenario[] = [
{
id: "readme-overview",
title: "Advisor Overview",
description: "A representative Ask Advisor review for the README opening image.",
options: { advisorModelConfigured: true, script: "readme", width: 100, height: 18 },
description: "A representative design review for the README opening image.",
options: { advisorModelConfigured: true, script: "readme", width: 100, height: 28 },
captures: ["whole", "overlay"],
checklist: [
"The Primary task remains visible behind the top-center Advisor Overlay.",
"The Overlay shows the user's review request, Primary Context, Pull, and Second Opinion.",
"The follow-up draft remains readable at README display sizes.",
"The Overlay shows a natural naming question, Primary Context, Pull, independent inspection, and a nuanced Second Opinion.",
"The completed review remains readable at README display sizes without presenting its conclusion as Advice.",
],
async run(pi) {
await pi.submit("I finished the cache refactor. Review the implementation before I commit.");
await pi.submit("SessionManager sounds plural. What should replace it?");
await pi.waitForScreen(
(screen) => screen.includes("Implemented request deduplication"),
(screen) => screen.includes("SessionJournal"),
10_000,
"Primary work before the README Ask Advisor example",
);
await pi.submit("/advisor Review the Primary Agent's cache refactor.");
await pi.submit("/advisor Does that still fit if one instance can switch session files?");
await pi.waitForScreen(
(screen) => screen.includes("Advisor · idle") && screen.includes("Guard eviction by entry identity"),
(screen) => screen.includes("Advisor · idle") && screen.includes("SessionHandle"),
20_000,
"README Ask Advisor example",
);
await pi.waitForMouseReporting(true, 2_000, "focused README Advisor Overlay");
await pi.submit("How should Primary fix it?", []);
await pi.waitForScreen(
(screen) => screen.includes("How should Primary fix it?"),
10_000,
"README Advisor follow-up draft",
);
},
},
{
Expand Down
Loading