pi-jar 0.2.1 → 0.2.3
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 +17 -14
- package/docs/EFFICIENCY_PLAN.md +79 -0
- package/docs/INTEGRATIONS.md +22 -6
- package/docs/SUBAGENT-TOOLS.md +1 -1
- package/docs/WORKFLOWS.md +3 -3
- package/extensions/index.ts +84 -17
- package/package.json +1 -1
- package/src/advisor.ts +22 -10
- package/src/ask-tool.ts +2 -7
- package/src/compact-tools.ts +148 -8
- package/src/context-budget.ts +226 -0
- package/src/context-diet.ts +158 -17
- package/src/{democracy.ts → council.ts} +19 -22
- package/src/delegate.ts +99 -54
- package/src/discussion-hub.ts +9 -6
- package/src/discussion.ts +31 -22
- package/src/footer.ts +5 -1
- package/src/goal-loop.ts +11 -22
- package/src/info-panels.ts +6 -0
- package/src/model-roles.ts +26 -2
- package/src/perf.ts +21 -4
- package/src/plan.ts +12 -25
- package/src/profile-ui.ts +21 -6
- package/src/profiles.ts +9 -4
- package/src/quota.ts +71 -49
- package/src/roles-ui.ts +4 -2
- package/src/settings-ui.ts +27 -8
- package/src/settings.ts +21 -1
- package/src/shells.ts +77 -15
- package/src/side-model.ts +14 -0
- package/src/subagent-tools.ts +1 -1
- package/src/suggest.ts +4 -11
- package/src/task-tool.ts +3 -9
- package/src/tool-activation.ts +14 -0
- package/src/usage-view.ts +8 -4
- package/src/usage.ts +4 -2
- package/src/workflow-context.ts +37 -0
package/README.md
CHANGED
|
@@ -19,13 +19,13 @@ The showcase is captured from a real pi-jar terminal session: the torch-style `
|
|
|
19
19
|
| **Plan mode** | Read-only exploration; the agent must write a structured plan file and submit it. You review it in a split view (headings on the left, section on the right) and approve, compact-and-approve, refine or stop. |
|
|
20
20
|
| **Goal mode** | Set an outcome; the agent must break it into tracked tasks and keeps working until they are done, then an **auditor** pass verifies the goal before it can be marked complete. |
|
|
21
21
|
| **Roles** | Named model roles (`default`, `smol`, `slow`, `plan`, `implement`, `advisor`, `moderator`, `scout`, `worker`, `reviewer`, `task`, `commit`, plus your own) with aliases, effort, per-role fallback models and project overrides. |
|
|
22
|
-
| **Advisor** | A second-opinion model: the agent calls `jar_advisor`
|
|
22
|
+
| **Advisor** | A second-opinion model: the agent calls `jar_advisor` when you ask or after two failed attempts, `/advisor [focus]` asks on demand, and opt-in automatic gates consult it when the agent repeats the same tool call or keeps failing. |
|
|
23
23
|
| **Usage & context** | `/usage` shows session cost and tokens per model (advisor and commit calls included) and plan-limit bars with reset times; `/context` draws a grid of what fills the context window, with a per-file, per-skill and per-tool breakdown. |
|
|
24
24
|
| **Commit** | `/jar commit [note]` drafts a message for the staged changes with the `commit` role, lets you edit it, then commits (never pushes). |
|
|
25
25
|
| **Tasks & questions** | A Claude/omp-style `jar_todo` checklist with subtasks and per-task progress that the agent maintains itself, and `jar_ask` structured questions with options, multi-select and free-form answers. |
|
|
26
26
|
| **Change review** | Every file the agent or its editing subagents change is remembered as it was; `/diff` (<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>D</kbd>) shows changed files (each counted once) next to a colored diff, with accept or revert per file or all at once. |
|
|
27
27
|
| **Background shells** | `jar_shell` runs dev servers, watchers and long tests in the background; watch/exit events wake an idle agent or reach it at the next safe turn boundary. Completion status appears immediately in the UI even during model reasoning. Running shells get a footer row; click it (or <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>A</kbd>) to tail or kill them. |
|
|
28
|
-
| **Subagents** | The main agent becomes a moderator over a configurable retained pool (2, 4, 6, 8 or 16; default 4). `jar_delegate` starts `scout`, `fork` or sandboxed `worktree` agents and returns immediately, so the main agent can keep responding to user steering; Pi RPC event-bus messages deliver initial and resumed turn completions without polling. `jar_subagent` peeks, steers, asks BTW questions, pauses, resumes and stops them. Workers can exchange bounded structured Q/A through `jar_discuss`; worktree changes
|
|
28
|
+
| **Subagents** | The main agent becomes a moderator over a configurable retained pool (2, 4, 6, 8 or 16; default 2, or 4 in an opt-in Swarm profile). It delegates only when there are at least two independent chunks that each need about ten or more tool calls. `jar_delegate` starts `scout`, `fork` or sandboxed `worktree` agents and returns immediately, so the main agent can keep responding to user steering; Pi RPC event-bus messages deliver initial and resumed turn completions without polling. `jar_subagent` peeks, steers, asks BTW questions, pauses, resumes and stops them. Workers can exchange bounded structured Q/A through `jar_discuss`; worktree changes reach `/diff` only when the moderator stops a worker. |
|
|
29
29
|
| **Sessions & history** | Recent sessions (with their goal and plan) on the welcome card, one click from resuming; <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>H</kbd> searches earlier prompts; pasted images show as chips under the composer. |
|
|
30
30
|
| **Footer & themes** | Responsive footer (model, effort, session, cwd, context, RAM, cost, quota, goal, roles, branch, live subagents and shells) with unicode, [Nerd Font](https://www.nerdfonts.com) or ascii icons, and seven dark themes. |
|
|
31
31
|
|
|
@@ -73,7 +73,7 @@ Pi only delivers mouse events in its **fullscreen** TUI mode. In regular mode th
|
|
|
73
73
|
| `/jar ask [question]` | Answer a question in a dialog and insert the answer into the editor. |
|
|
74
74
|
| `/jar accent [preset]` · `/jar footer` · `/jar icons [unicode\|nerd\|ascii]` | Switch a loaded accent theme; toggle footer fields; pick the icon set. |
|
|
75
75
|
| `/jar composer on\|off` · `/jar animations on\|off` · `/jar ui on\|off` | Toggle the composer, motion, or all pi-jar UI. |
|
|
76
|
-
| `/jar quota on\|off` | Session-only, read-only quota lookups for supported OAuth providers. Lookups start on session boundaries (startup, prompts, model changes), never from a render: at most one per provider every 5 minutes, and failures back off up to 30 minutes. |
|
|
76
|
+
| `/jar quota on\|off` | Session-only, read-only quota lookups for supported OAuth providers. Lookups start on session boundaries (startup, prompts, model changes) and when `/usage` opens, never from a render: at most one per provider every 5 minutes, and failures back off up to 30 minutes. |
|
|
77
77
|
| `/jar hub` | Open an installed task or subagent manager command. |
|
|
78
78
|
| `/jar demo` · `/jar reset` | Labeled sample roles in the footer / back to live data. |
|
|
79
79
|
|
|
@@ -225,7 +225,7 @@ The advisor is a second model (the `advisor` role; the current model if unassign
|
|
|
225
225
|
|
|
226
226
|
- **`jar_advisor({ question?, draft? })`** — starts a background second opinion and immediately returns a request ID. Use `action: "status"`, `"get"`, bounded `"wait"` (at most 30 seconds), or `"cancel"` with that ID; a pending receipt is **not** reviewed approval. Advice arrives at a safe model boundary or wakes an idle agent, with immediate UI completion feedback. Requests have a 60-second deadline and bounded context; `/advisor` and automatic stuck-work gates are non-blocking too. Get completed advice before consequential decisions. Available in plan mode too.
|
|
227
227
|
- **`/advisor [focus]`** — ask on demand; the answer is added to the conversation (visible) for the agent's next turn.
|
|
228
|
-
- **Gates** — when the agent makes the same tool call
|
|
228
|
+
- **Gates** (opt-in, off by default) — when the agent makes the same tool call four times within its last eight calls, the call is blocked and the advisor reviews in the background. Re-reads, `jar_shell` `output`/`wait`/`peek` polls and test-runner reruns (`npm test`, `node --test`, `pytest`, `go test`, …) never count as a loop. After three failing tool results in a row, the advice is steered into the running turn. At most two automatic consultations per prompt.
|
|
229
229
|
- Settings → Pi toggles the advisor and the gates. Advisor calls are counted in `/usage`. The footer and welcome show it as a working teammate while it thinks.
|
|
230
230
|
|
|
231
231
|
### Role fallback models
|
|
@@ -252,7 +252,7 @@ This is a pi-jar enhancement: [pi-advisor](https://github.com/philipbrembeck/pi-
|
|
|
252
252
|
|
|
253
253
|
`/usage` and `/context` open one tabbed panel (<kbd>Tab</kbd> switches, <kbd>Esc</kbd> closes):
|
|
254
254
|
|
|
255
|
-
- **Usage** — total cost, duration, prompts and responses, tokens (input, output, cache read/write); a per-model breakdown including advisor and commit calls; and, for Anthropic and OpenAI Codex subscriptions, 5-hour and weekly limit bars with reset times (lookups follow `/jar quota on|off`).
|
|
255
|
+
- **Usage** — total cost, duration, prompts and responses, tokens (input, output, cache read/write); a per-model breakdown including advisor and commit calls; and, for Anthropic and OpenAI Codex subscriptions, 5-hour and weekly limit bars with reset times (lookups follow `/jar quota on|off`). A failed lookup shows its reason (for example the provider's HTTP 429 rate limit) and when it retries, instead of loading indefinitely.
|
|
256
256
|
- **Context** — a 10×10 grid (each cell ≈ 1% of the window) beside a legend: system prompt, tools, context files, skills, compaction summary, user and assistant messages, tool results, extension messages, free space and the autocompact buffer. Parts are estimated at ~4 characters per token and scaled to the provider-reported total when one is known; context files, skills and tools are listed individually below.
|
|
257
257
|
|
|
258
258
|
## Tasks, questions and history
|
|
@@ -266,17 +266,19 @@ This is a pi-jar enhancement: [pi-advisor](https://github.com/philipbrembeck/pi-
|
|
|
266
266
|
Use `parent` with `append` to add a genuinely new subtask to active work; `start`, `done`, `open`, `edit`, `list`, `add` and `delete` remain available.
|
|
267
267
|
- **`jar_ask`** — structured questions: numbered options with descriptions, single or multi-select, *Type your own answer* (multi-line, paste-friendly) and *Chat about this* to discuss before choosing.
|
|
268
268
|
- **`/diff`** — review what the agent (and its editing subagents) changed since the first edit to each file: files with `+/−` counts on the left, a full-path header and numbered, colored diff on the right. Changed lines get a colored rail and tinted background, replaced lines highlight the changed words, code is syntax-highlighted for known languages, and collapsed regions are labeled `┄ N unchanged lines ┄`. `/` filters files; `t` switches unified and aligned side-by-side views (unified below 70 columns), `←`/`→` (or `[`/`]`) change the context from 0 lines to the whole file, `w` wraps long lines, and `n`/`p`, `Home`/`End` navigate. Large changes offer summary or explicit windowed review (`v`), with clearly labeled coarse fallback and existing safety limits—not arbitrary-file streaming. See [diff review](docs/DIFF-REVIEW.md). `a` accept (keep, stop tracking), `r` then `y` revert (restore the original, or remove a file it created), `A`/`R` for all. The footer shows `± N files · /diff` while anything is unreviewed; each file counts once no matter who edited it. Only `edit`/`write` changes inside the project are tracked; shell-made changes are not.
|
|
269
|
-
- **Context diet (opt-in)** — `/jar settings` → **Pi** → **Context diet (prior-turn reasoning)**. Saved independently per profile; OFF by default. Removes eligible thinking blocks from completed earlier user turns only in outgoing context, never from stored history. Current reasoning signatures, user requests, tool
|
|
270
|
-
-
|
|
271
|
-
-
|
|
272
|
-
- **`
|
|
269
|
+
- **Context diet (opt-in)** — `/jar settings` → **Pi** → **Context diet (prior-turn reasoning)**. Saved independently per profile; OFF by default. Removes eligible thinking blocks from completed earlier user turns only in outgoing context, never from stored history. In the same completed turn, a `read` result that a later read of the same range or a successful edit/write of the same file superseded becomes a one-line stub; each turn is rewritten only once, when it completes, so later prefix-cache hits are kept. Current reasoning signatures, user requests, tool call/result pairing and workflow state remain intact; ambiguous tool chains are left unchanged. Opaque signature character counts are diagnostics, not token savings.
|
|
270
|
+
- **Context budget** — `/jar settings` → **Pi** → **Context budget** (`off`, `suggest` default, `compact`) and **Context budget limit** (80k–200k, default 120k). When the last call's prompt (input plus cache) passes the limit, the footer's context metric turns amber and `suggest` shows one notice per crossing (`/compact`, or `/new` for the next task). `compact` compacts once at the next safe point: right after a `jar_todo` item completes while work remains (the run stops after that turn, compacts, then continues with a hidden note), or when the run settles; it never cuts a tool chain. The footer shows the average cost per call over the last ten calls next to the total. A prompt after a finished task list in a large context suggests `/new`; a prompt after more than five idle minutes with over 100k context warns that the provider cache has probably expired. No keep-warm requests are sent.
|
|
271
|
+
- **Read hygiene** — a whole-file `read` (no offset/limit) of a file over 50 KB returns its first 400 lines with a note on how to continue. Opt-in **Repeated-read stub** (`/jar settings` → **Pi**): re-reading the same range of an unchanged file returns `[unchanged since read #N (312 lines); pass force: true to re-read]`. Any edit/write of the file, a changed file revision, compaction, branch navigation or a new session clears it.
|
|
272
|
+
- **`jar_shell`** — `start` (optional `name`, `watch` regex, `notify`, task/service `purpose`, `waitMs` up to 30 s), `list`, `peek`, bounded `wait`, `output`, `kill`. Use plain `bash` for finite commands expected to finish in about two minutes; `jar_shell` is for services, watchers and long jobs. `start` with `waitMs` returns the exit code and output tail in one call when the job finishes in time (acknowledged, so no later notification), otherwise the output so far; Esc cancels only the wait. `output` returns only lines new since your last read (with counts of trimmed or skipped lines); `all: true` repeats the trailing window. ANSI-stripped logs have per-job caps and a 4 MiB aggregate budget; surfaced finished jobs compact to a 32 KiB tail. Live jobs retain useful tails and are never killed to reclaim logs. Watch/exit notifications are compact and coalesced; reads acknowledge observed events. Active model responses cannot be interrupted by a completion event: the UI previews status without consuming it, and model delivery waits for a safe boundary. When no useful work remains, use bounded `wait` for the relevant finite jobs rather than waiting for an automatic wake-up. Each running shell gets a footer row and an activity view (`/jar shells`) with `x` kill and `f` follow. All shells stop when the session ends.
|
|
273
|
+
- **`jar_delegate`** — spawns retained, session-scoped subagents up to **Max subagents** in `/jar settings` (2, 4, 6, 8 or 16; default 2). Idle, paused and hibernated agents count toward this pool; over-cap batches are rejected. `/profiles` → **Create Swarm profile** copies the current settings with a pool of 4. `scout` is fresh/read-only, `fork` inherits conversation read-only, and `worktree` forks context into a path-guarded Git worktree. Settled retained agents close their Pi process after 30 seconds of quiet while keeping their private session and workspace; `resume`/`ask` relaunch that same session. Launch returns before startup or the first turn completes, including legacy one-shot `write: true` mode. Turn and stop events carry a summary of at most 400 characters plus changed files; all events pending at one boundary arrive as one `pi-jar.subagent` message, together with the moderator's unread discussion mail. `/roles` warns when the `scout` or `reviewer` role would run on the main model while a cheaper model is available.
|
|
274
|
+
- **`jar_subagent`** — `peek` reports progress, including the workspace path and changed files for recovery records; `steer` redirects a running worker; `resume` relaunches an idle/paused/hibernated agent; `report` returns an agent's latest full report (a synchronous `jar_delegate` result shares a 16k-character report budget across its runs and points here when it truncates). `ask`, `pause` and `stop` return accepted receipts immediately; answers and final handoffs arrive as `pi-jar.subagent` events. `stop` reconciles safe worktree changes; conflicts remain private and can be retried with `stop` after resolving parent drift. `discard` removes an unresolved recovery worktree without applying it. At most four active/recovery worktrees are admitted; resolve or discard one before starting another. Session shutdown reports paths of any preserved worktrees. Controls do not poll the child.
|
|
273
275
|
- **Subagent capabilities** — each task can set `tools` or `inheritTools: true`; known-tool mode filters and child guards remain in force. See [tool capabilities](docs/SUBAGENT-TOOLS.md).
|
|
274
276
|
- **Worktree finalization** — edits do not touch the parent merely because a worker settles or hibernates. On `jar_subagent stop`, pi-jar checks parent drift and reviewability, applies safe changes and adds them to `/diff`; conflicts remain isolated for retry or explicit discard. Failed worktree transcripts are compacted independently of finished history. Process closure is awaited before finalization, and failed multi-file application rolls back rather than leaving a partial result.
|
|
275
|
-
- **`
|
|
276
|
-
- **`jar_discuss`** — session-local, parent-owned discussion broker over authenticated local IPC; no shared paper, lock or polling. `ask`/`answer` return stable IDs without echoing your text. `list` reads only relevant unread questions/answers and advances your cursor; `thread` (or `list` with `questionId`) reads one thread; `pending` shows counts without message bodies. `since: "d0"` rereads relevant mail. At most 128 messages, 1600 characters each and 128 KiB are retained; unread/unanswered questions block new posts instead of being silently lost. Messages do not wake idle agents
|
|
277
|
+
- **`jar_council`** — **exceptional only**, for a persistent super-complex issue with several evidence-backed options after at least two distinct failed approaches. The moderator opens 2–8 options, resumes relevant idle/paused/hibernated fresh-context read-only scouts or spawns new scouts within the pool, and collects one private ballot per voter. A strict majority of invited voters wins; ties, plurality or missing/invalid ballots require user choice. Voting never implements its recommendation. A round has a 10-minute deadline and bounded rationales.
|
|
278
|
+
- **`jar_discuss`** — session-local, parent-owned discussion broker over authenticated local IPC; no shared paper, lock or polling. `ask`/`answer` return stable IDs without echoing your text. `list` reads only relevant unread questions/answers and advances your cursor; `thread` (or `list` with `questionId`) reads one thread; `pending` shows counts without message bodies. `since: "d0"` rereads relevant mail. At most 128 messages, 1600 characters each and 128 KiB are retained; unread/unanswered questions block new posts instead of being silently lost. Messages do not wake idle agents. Unread mail (never your own messages) arrives once, inline, at your next natural turn start, so no `list` call is needed; the moderator's also rides along with subagent events.
|
|
277
279
|
- **Activity view** — one split view (<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>A</kbd>, `/jar activity`, or a click on a footer row) for subagents, background shells and other extensions' teammates: list on the left, live details on the right (auto-follows; scroll up to pause, `f` to follow again). For a subagent the details show its task, its own checklist with subtasks, and a transcript like omp's: each tool call and message is one collapsed line that expands on demand (`Tab` into it, `↑↓`, `Enter`, or click) to show arguments and output, while streaming text shows live. `s` opens a steering input: type a message and press `Enter` to redirect the running subagent. `x` stops just that one. Finished runs stay listed for review.
|
|
278
280
|
- **`/jar history`** — separate, read-only timeline of the active branch (paging, search, expandable details). Pi's native transcript is untouched.
|
|
279
|
-
- Pi's built-in read/shell/edit/write tool cards render compactly: collapsed, each is one padded, state-colored card with its call line and a one-line summary (edit included), and partially streamed arguments never flicker to a fallback; the full output or diff stays one click or <kbd>Ctrl</kbd>+<kbd>O</kbd> away. Every requested frame re-renders the whole transcript, so background sources (subagent streams, shell changes, context and cost updates) share one adaptive repaint budget: at most every 250 ms below 300 branch entries, 500 ms below 800, 1 s below 2,000, and only on visible state changes beyond that; a frame Pi draws on its own satisfies pending updates. User actions and state changes (a subagent or shell starting, settling or failing) still repaint at once. The open activity view repaints live updates every 100 ms, slowing to 500 ms in very large sessions. Context usage is cached: session start, tree navigation, compaction, model changes, renames and the end of a run refresh it at once, while message ends recompute it at most every 750 ms. `/jar perf` shows the counters. The sliding bar some terminals show in the tab while the agent works is Pi's own `OSC 9;4` progress (`terminal.showTerminalProgress` in Pi settings), not pi-jar.
|
|
281
|
+
- Pi's built-in read/shell/edit/write tool cards render compactly: collapsed, each is one padded, state-colored card with its call line and a one-line summary (edit included), and partially streamed arguments never flicker to a fallback; the full output or diff stays one click or <kbd>Ctrl</kbd>+<kbd>O</kbd> away. Every requested frame re-renders the whole transcript, so background sources (subagent streams, shell changes, context and cost updates) share one adaptive repaint budget: at most every 250 ms below 300 branch entries, 500 ms below 800, 1 s below 2,000, and only on visible state changes beyond that; a frame Pi draws on its own satisfies pending updates. User actions and state changes (a subagent or shell starting, settling or failing) still repaint at once. The open activity view repaints live updates every 100 ms, slowing to 500 ms in very large sessions. Context usage is cached: session start, tree navigation, compaction, model changes, renames and the end of a run refresh it at once, while message ends recompute it at most every 750 ms, widening up to 5 s when the measured computation is slow so sampling stays near a 1% duty cycle. `/jar perf` shows the counters. The sliding bar some terminals show in the tab while the agent works is Pi's own `OSC 9;4` progress (`terminal.showTerminalProgress` in Pi settings), not pi-jar.
|
|
280
282
|
|
|
281
283
|
## Settings
|
|
282
284
|
|
|
@@ -284,7 +286,7 @@ This is a pi-jar enhancement: [pi-advisor](https://github.com/philipbrembeck/pi-
|
|
|
284
286
|
|
|
285
287
|
- **Appearance** — accent, motion, rounded composer, Ember mascot, next-prompt suggestions, pi-jar UI, icons (`unicode` default, `nerd` for a [Nerd Font](https://www.nerdfonts.com) terminal like omp's nerd preset, `ascii` for plain labels).
|
|
286
288
|
- **Footer** — field visibility. In fullscreen mode the session name (or `sessions` for an unnamed session) opens the session picker and each subagent/shell row opens the activity view. SoL-Pi's savings status is not repeated in the footer (it already notifies).
|
|
287
|
-
- **Pi** — mouse clicks (Pi fullscreen mode), copy on select, goal auto rounds, **Max subagents** (explicit choice then Save; default
|
|
289
|
+
- **Pi** — mouse clicks (Pi fullscreen mode), copy on select, goal auto rounds, **Max subagents** (explicit choice then Save; default 2, existing saved values are kept), advisor, advisor gates (default off), context diet, context budget and its limit, repeated-read stub.
|
|
288
290
|
|
|
289
291
|
pi-jar preferences are saved in `pi-jar-settings.json` in Pi's agent directory; the Pi tab writes Pi's own settings.
|
|
290
292
|
|
|
@@ -298,6 +300,7 @@ Other extensions can publish teammate roles and quota windows through Pi's publi
|
|
|
298
300
|
pi-jar/
|
|
299
301
|
├── extensions/index.ts extension entry: wiring, welcome, footer, commands
|
|
300
302
|
├── src/
|
|
303
|
+
│ ├── context-budget.ts context budget, per-call cost average, topic and cold-cache hints
|
|
301
304
|
│ ├── flame.ts pixel fire simulation
|
|
302
305
|
│ ├── mascot.ts Ember's moods and sprites
|
|
303
306
|
│ ├── composer.ts rounded composer, ghost text, mouse mapping
|
|
@@ -318,7 +321,7 @@ pi-jar/
|
|
|
318
321
|
│ ├── async-process.ts cancellable async subprocess runner with deadlines and tree cleanup
|
|
319
322
|
│ ├── discussion.ts session-local discussion broker and tool
|
|
320
323
|
│ ├── discussion-hub.ts bounded in-memory discussion mailboxes
|
|
321
|
-
│ ├──
|
|
324
|
+
│ ├── council.ts exceptional scout ballots, strict majority and user tie-break
|
|
322
325
|
│ ├── activity-view.ts subagent/shell details view
|
|
323
326
|
│ ├── perf.ts long-session render budget, context sampling and /jar perf
|
|
324
327
|
│ ├── icons.ts unicode / nerd / ascii glyph sets
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Efficiency Plan
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Triaged against the 0.2.1 codebase and partly implemented in v0.2.2. Most of the original proposal was either already shipped by the long-session work ([LONG_SESSION_PERFORMANCE_PLAN.md](./LONG_SESSION_PERFORMANCE_PLAN.md), 0.1.9–0.2.1) or costs more in complexity and risk than it saves. Only items with a measurable win and no behavior change were taken.
|
|
6
|
+
|
|
7
|
+
## Goal
|
|
8
|
+
|
|
9
|
+
Keep long sessions fast and cheap without losing stored history or changing what the model sees unless the user opted in.
|
|
10
|
+
|
|
11
|
+
## Implemented in 0.2.2
|
|
12
|
+
|
|
13
|
+
### Adaptive context sampling (was item 8)
|
|
14
|
+
|
|
15
|
+
`ContextSampler` (`src/perf.ts`) now widens its mid-turn throttle from the measured cost of `getContextUsage()` instead of a fixed 750 ms: the window is `max(750 ms, 100 × median sample time)`, capped at 5 s. Mid-turn sampling therefore stays near a 1% duty cycle whatever the session size; cheap sessions keep the 750 ms cadence, and lifecycle boundaries (start, tree, compact, model, rename, settle) still refresh immediately. Measured cost is used rather than entry-count tiers because it is the quantity being bounded and needs no extra wiring. `/jar perf` shows the current window.
|
|
16
|
+
|
|
17
|
+
### Profile pin lookup without a copy (part of item 9)
|
|
18
|
+
|
|
19
|
+
`pinnedProfileId` walks the branch backwards in place instead of allocating `[...entries].reverse()`. `conversationStarted` already exits at the first user message, so it needed no change; neither runs per frame, so no incremental cache was added.
|
|
20
|
+
|
|
21
|
+
### Cache-stable workflow prompts (follow-up)
|
|
22
|
+
|
|
23
|
+
Plan and goal prompts used to be pruned to the newest copy on every call. Removing an earlier copy changes the conversation prefix, so each new turn (or goal round) re-sent the whole previous run uncached. `src/workflow-context.ts` now projects them so that every keep/drop decision depends only on earlier messages: identical plan/goal context is sent once, sent reminders and continuations stay in place, and leaving the mode drops them all once. It runs on Pi's message list before provider conversion, so it helps every provider that caches by prefix (Anthropic explicit breakpoints, OpenAI/DeepSeek/Gemini automatic prefix caching, Bedrock). Appending the context at the tail was rejected: pi-ai places the Anthropic cache breakpoint on the last message, so a moving tail would defeat the cache entirely.
|
|
24
|
+
|
|
25
|
+
### Unset side roles are visible (follow-up)
|
|
26
|
+
|
|
27
|
+
`askRole` notifies once per role when an unassigned role (advisor, commit, …) ran on the current main model, with the `/roles set` command to fix it. Behaviour is unchanged.
|
|
28
|
+
|
|
29
|
+
### Bounded synchronous subagent results (follow-up)
|
|
30
|
+
|
|
31
|
+
A synchronous `jar_delegate` result shares a 16k-character report budget across its runs (at least 2k each) instead of up to 12k per run. Truncated reports name `jar_subagent report <key>`, a new read-only action returning the full stored report. Event messages were already capped at 1.2k.
|
|
32
|
+
|
|
33
|
+
### Cheaper subagent subscriber (follow-up)
|
|
34
|
+
|
|
35
|
+
The footer's per-tick registry listener reads `DelegateRegistry.stateSignature()` straight from the map instead of `records()` (a second sweep, a sort and two arrays per 50 ms tick).
|
|
36
|
+
|
|
37
|
+
## Session cost reduction
|
|
38
|
+
|
|
39
|
+
Baseline: a 309-call, $6.34 session (`node scripts/session-cost.mjs 01a101ba-…`): 60% of cost was cache reads, average context 125k, 138 calls above 125k context costing $3.87; 109 reads (12 of one file, 7 of a `/tmp` plan), 65 `jar_shell` calls (33 start, 22 output, 8 wait), 19 subagent events (36 KB). Cost scales with calls × context, so every change cuts calls, context per call, or the fixed prompt.
|
|
40
|
+
|
|
41
|
+
| Change | Where | Effect |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| Context budget (`suggest` by default at 120k; opt-in `compact` at a safe point) and average cost per call in the footer | `src/context-budget.ts` | Long sessions are compacted or split before every call re-sends 200k+ |
|
|
44
|
+
| Topic-switch and cold-cache hints | `src/context-budget.ts` | `/new` for unrelated work; `/compact` before re-sending a large expired context |
|
|
45
|
+
| `bash` for finite commands; `jar_shell start` `waitMs`; incremental `output` | `src/shells.ts` | Short jobs take one call instead of start → output → wait, and repeated reads don't resend old output |
|
|
46
|
+
| 400-line cap on whole reads of files over 50 KB; opt-in repeated-read stub; diet stubs superseded reads | `src/compact-tools.ts`, `src/context-diet.ts` | Smaller tool results and fewer duplicate reads in the context |
|
|
47
|
+
| Pool default 2 (opt-in Swarm profile: 4); delegate only for ≥2 chunks of ≥10 tool calls | `src/settings.ts`, `src/delegate.ts`, `src/profile-ui.ts` | Fewer moderation round-trips |
|
|
48
|
+
| Events: ≤400-char summary + changed files, one message per boundary, discussion mail inlined | `src/delegate.ts`, `src/discussion.ts` | Events shrink from ~1.9 KB to ~0.5 KB; no `list` call to read mail; no echoes of own messages |
|
|
49
|
+
| `/roles` warns when `scout`/`reviewer` run on the main model while a cheaper one is available | `src/model-roles.ts` | Subagent calls move to a cheaper model |
|
|
50
|
+
| Advisor gates off by default; loop threshold 4; re-reads, shell polls and test reruns are never loops | `src/advisor.ts` | No side calls from normal fix loops |
|
|
51
|
+
| Rules moved into tool descriptions; `jar_plan_submit`/`jar_goal` active only in their mode | all tool modules, `src/tool-activation.ts` | Smaller fixed prompt (below) |
|
|
52
|
+
|
|
53
|
+
Fixed prompt that pi-jar contributes, measured from Pi's built system message in RPC mode (same project, same Pi 1.0.4): the `rules` section went from 7,584 to 1,675 characters and `tools` from 1,773 to 1,476; two schemas (~900 characters) are no longer sent outside their mode. Tool descriptions and schemas grew by ~1.1k characters because the guidance moved there. Net: ~5k characters (~1.3k tokens) less on every call. That is modest next to context growth, so the context budget and shell/read changes carry most of the saving.
|
|
54
|
+
|
|
55
|
+
Not done: Pi's compaction thresholds and the user's own settings are left alone; recommendations are in [INTEGRATIONS.md](./INTEGRATIONS.md#keeping-sessions-cheap-recommendations). The ≤ $3 field target needs a real re-run of a similar task with the new defaults.
|
|
56
|
+
|
|
57
|
+
## Already covered — dropped
|
|
58
|
+
|
|
59
|
+
| Item | Why |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| 5. Tier subagent events | Already tiered: tool `onUpdate` ≤ 1 s, footer status only on state change, key/state changes are `transition` and everything else `background` in `RenderScheduler` (250 ms–1 s by session size, transitions-only beyond), Activity view coalesces at 100–500 ms. The 50 ms stream tick only feeds that scheduler. |
|
|
62
|
+
| 6. Footer snapshot | `renderFooterLayout` already memoizes on a key of width/theme/view; repaints between changes are cache hits. A versioned snapshot would need change notifications from every source for a microsecond saving. |
|
|
63
|
+
| 7. Bounded advisor fingerprints | `callKey` already keeps a 200-char prefix plus a SHA-1 of large inputs, so the loop window never pins large payloads. Prefix/suffix-only hashing would let distinct writes collide and trigger false loop alerts. |
|
|
64
|
+
|
|
65
|
+
## Deferred
|
|
66
|
+
|
|
67
|
+
| Item | Why |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| 1. Single Context Projector | The four `context` hooks (child automation filter, diet, plan, goal) are each one `customType` scan that allocates only when something is stale — negligible next to request serialization. Merging them couples independent modules for no measurable gain. Revisit only if `/jar perf` shows context hooks in a profile. |
|
|
70
|
+
| 2. Context Diet v2 (`stripper` model, `DietSnapshot`) | A model-written summary replaces exact history with a lossy one, adds a side call per compression and duplicates Pi's own compaction. Needs its own design (fidelity, resumability, cache-hit impact) before code. |
|
|
71
|
+
| 3. Diet-aware delegation | Depends on item 2. Today `scout` already starts fresh and `fork` is an explicit mode; changing the defaults alters subagent results. |
|
|
72
|
+
| 4. Strict utility roles | Side calls without a role use the current model by design (`askRole`); making them fail or go deterministic breaks advisor and commit messages for users without roles. Addressed instead by the one-time notice above. |
|
|
73
|
+
| 10. Compact workflow history | Rewriting persisted session history is ruled out; provider context is handled by the cache-stable projection above. |
|
|
74
|
+
|
|
75
|
+
## Success criteria
|
|
76
|
+
|
|
77
|
+
- Mid-turn context sampling cost stays bounded (~1% duty cycle) as sessions grow; visible in `/jar perf`.
|
|
78
|
+
- Plan/goal sessions keep prefix-cache hits across turns and rounds on every provider.
|
|
79
|
+
- Stored session history is unchanged.
|
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -39,7 +39,7 @@ ctx.ui.setStatus("pi-jar.quota.openai-codex", JSON.stringify({
|
|
|
39
39
|
- `used` is a percentage from 0 to 100. Either window may be omitted, and each can carry an optional `resetsAt` (epoch milliseconds).
|
|
40
40
|
- `expiresAt` is required and may be at most five minutes ahead. A valid published status always wins.
|
|
41
41
|
- `/jar quota on` (the default each session) lets pi-jar resolve the current Anthropic or OpenAI Codex OAuth credentials through Pi's model registry and make a **read-only** request to the provider's quota endpoint when nothing is published.
|
|
42
|
-
- Requests have a 5 s timeout; successful results are cached for 5 minutes, while consecutive failures back off for 5, 10, 20, then 30 minutes. `/jar quota off` cancels pending requests and clears the cache. Renders only read cached values.
|
|
42
|
+
- Requests have a 5 s timeout; successful results are cached for 5 minutes, while consecutive failures back off for 5, 10, 20, then 30 minutes and `/usage` shows the failure reason until the next retry. `/jar quota off` cancels pending requests and clears the cache. Renders only read cached values.
|
|
43
43
|
- These provider endpoints are not stable public APIs.
|
|
44
44
|
|
|
45
45
|
## Session entries owned by pi-jar
|
|
@@ -58,19 +58,35 @@ Hidden context messages (`pi-jar.plan-context`, `pi-jar.plan-reminder`, `pi-jar.
|
|
|
58
58
|
| --- | --- |
|
|
59
59
|
| `jar_todo` | branch-aware checklist: `todos` full-list writes (`content`, `status` pending/in_progress/completed, `activeForm`), plus `list`, `add`, `start`, `done`, `open`, `edit`, `delete` |
|
|
60
60
|
| `jar_ask` | structured questions answered in the TUI |
|
|
61
|
-
| `jar_plan_submit` | submit a plan file for review
|
|
62
|
-
| `jar_goal` | `get`, `complete` (with evidence, audit phase only), `block
|
|
63
|
-
| `jar_suggest` | one next-prompt suggestion shown as composer ghost text |
|
|
64
|
-
| `jar_shell` | background shells: `start` (`command`, `name`, `watch`, `notify`), `list`, `
|
|
65
|
-
| `jar_delegate` |
|
|
61
|
+
| `jar_plan_submit` | submit a plan file for review; active only in plan mode |
|
|
62
|
+
| `jar_goal` | `get`, `complete` (with evidence, audit phase only), `block`; active only while a goal runs |
|
|
63
|
+
| `jar_suggest` | one next-prompt suggestion shown as composer ghost text; active only when suggestions are enabled |
|
|
64
|
+
| `jar_shell` | background shells: `start` (`command`, `name`, `watch`, `notify`, `purpose`, `waitMs`), `list`, `peek`, `wait`, `output` (new lines; `all`), `kill` |
|
|
65
|
+
| `jar_delegate` | retained subagents within the configured pool (`tasks: [{task, name?, role?, mode?, tools?, inheritTools?}]`) |
|
|
66
|
+
| `jar_subagent` | moderator control: `peek`, `report`, `steer`, `ask`, `pause`, `resume`, `stop`, `discard` |
|
|
67
|
+
| `jar_council` | exceptional scout vote with strict majority; never implements |
|
|
68
|
+
| `jar_discuss` | parent-brokered Q/A between moderator and subagents |
|
|
66
69
|
| `jar_advisor` | second opinion from the `advisor` role (`question?`, `draft?`) |
|
|
67
70
|
|
|
71
|
+
Mode-specific tools are deactivated when their mode is off, so their schema and rules are not sent with every request.
|
|
72
|
+
|
|
68
73
|
`jar_shell` events arrive as a visible `pi-jar.shell` custom message that triggers (or queues) an agent turn; subagent turn ends and `jar_subagent` control completions arrive as one coalesced, visible `pi-jar.subagent` message at the moderator's next turn boundary, or as a wake-up when it is idle. `jar_delegate` runs child processes with `PI_JAR_CHILD=1`, which keeps pi-jar in the child from registering `jar_delegate` again, and publishes each running subagent through the role contract above as `pi-jar.role.delegate-<batch>-<n>`. The advisor publishes `pi-jar.role.advisor` while it is consulting, and its `/advisor` and gate answers arrive as visible `pi-jar.advisor` custom messages.
|
|
69
74
|
|
|
70
75
|
## Commands that overlap other packages
|
|
71
76
|
|
|
72
77
|
pi-jar registers `/usage`, `/context` and `/advisor`. Pi keeps both commands when two extensions register the same name and suffixes the later one (for example `/usage:2`), so remove packages whose commands you no longer need (such as a separate usage, context or advisor extension) to keep the plain names on pi-jar.
|
|
73
78
|
|
|
79
|
+
## Keeping sessions cheap (recommendations)
|
|
80
|
+
|
|
81
|
+
Every model call re-sends the whole context, so cost grows with session length far more than with the work done (see [EFFICIENCY_PLAN.md](./EFFICIENCY_PLAN.md#session-cost-reduction)). pi-jar never edits these files; apply them yourself if they fit:
|
|
82
|
+
|
|
83
|
+
- **Compact earlier.** In `~/.pi/agent/settings.json` set `"compaction": { "reserveTokens": 140000, "keepRecentTokens": 20000 }`. With a 272k-window model this compacts around 130k instead of near the limit. pi-jar's context budget (`/jar settings` → Context budget) suggests `/compact` or `/new` at the same point, or compacts at the next finished task when set to `compact`.
|
|
84
|
+
- **Enable heavy tool packages per project.** A package such as `pi-mono-figma` (~20 tools) sends every schema on every call; enable it only in projects that use it.
|
|
85
|
+
- **Load each MCP server once.** Use codebase-memory either through the lazy `mcp` proxy **or** as direct tools, not both.
|
|
86
|
+
- **Review rarely used packages** (for example `i-have-adhd`, `pi-mono-loop`, `pi-mono-btw`) and enable them per project.
|
|
87
|
+
- **One task per session.** Start `/new` when the next request is unrelated; pi-jar suggests it when a finished checklist is followed by a new prompt in a large context.
|
|
88
|
+
- **Measure.** `node scripts/session-cost.mjs <session.jsonl | id>` (in a pi-jar checkout) reports calls, the cost split, context buckets, repeated reads, tool counts and cache misses for one session.
|
|
89
|
+
|
|
74
90
|
## Working signals and other managers
|
|
75
91
|
|
|
76
92
|
Working wording, icon colors and Ember's moods use only Pi's public agent, turn, UI-prompt and tool-execution events. "Thinking" means active generation, not access to hidden reasoning. Motion stops when animations are off, the run settles, the UI is disabled or the session shuts down.
|
package/docs/SUBAGENT-TOOLS.md
CHANGED
|
@@ -18,7 +18,7 @@ Subagents use Pi's standard `--tools` allowlist. Each `jar_delegate` task can cu
|
|
|
18
18
|
- `worktree` accepts known path-guarded file tools and named web tools. Shell and arbitrary extension tools are rejected. `multi_file_edit` is included in defaults only when active in the parent; each effective edit target, default path and recognized alternate path field is validated, including traversal and existing symlink escapes.
|
|
19
19
|
- Named web capabilities are `web_enable`, `web_search`, `fetch_content`, `get_search_content` and `source_check`. Newly enabled tools must still be in the child's original capability list.
|
|
20
20
|
- These are tool-call restrictions, not an OS sandbox. Extensions execute trusted code; named web tools can access external services, and legacy direct-write mode is not workspace-confined.
|
|
21
|
-
- `jar_delegate`, `jar_subagent`, and `
|
|
21
|
+
- `jar_delegate`, `jar_subagent`, and `jar_council` are always removed from inherited tools and rejected in explicit lists. A child cannot recursively spawn more subagents.
|
|
22
22
|
- Tool capabilities are per task; a batch can mix modes and allowlists. A fork still inherits the parent's conversation context, independently of its tool list.
|
|
23
23
|
|
|
24
24
|
Retained scout/fork/worktree children hibernate after 30 seconds idle: their Pi process exits, but the private child session and worktree remain for `jar_subagent resume` or `ask`. Hibernated records count against Max subagents. Unresolved worktree recovery records do not occupy a pool slot, but at most four active/recovery worktrees are admitted. `jar_subagent stop` retries reconciliation; `discard` explicitly removes an unresolved recovery workspace without applying it. On session shutdown pi-jar reports paths for worktrees it preserves on disk, including still-active worktrees that might contain edits.
|
package/docs/WORKFLOWS.md
CHANGED
|
@@ -68,7 +68,7 @@ State (`pi-jar.plan` entries, v2: `enabled`, `steps`, `text`, `path`, `title`) f
|
|
|
68
68
|
- **Task gate:** with no open task, `write`/`edit` are blocked, and so are shell commands that are not on the read-only allowlist during the implement phase. During the audit, edits are blocked but verification commands are allowed.
|
|
69
69
|
- **Roles per round:** implement rounds run on the `implement` role and the audit round on `advisor` (each only if assigned). The previous model and effort are restored when the run settles.
|
|
70
70
|
- **Completion:** `jar_goal complete` requires the audit phase, no open tasks, and non-empty evidence. It ends the turn, records the evidence, and Ember shows `(★ᴗ★)`.
|
|
71
|
-
- **Context hygiene:**
|
|
71
|
+
- **Context hygiene:** repeated identical goal context is sent once, and goal prompts already sent are never removed from the middle of the prompt while the goal runs, so any provider's prefix cache (Anthropic, OpenAI, Gemini, Bedrock, …) keeps hitting. A completed goal drops them all.
|
|
72
72
|
|
|
73
73
|
Goal events (`pi-jar.goal`, v2: `set`, `status`, `round`) are append-only session entries; v1 `set`/`clear` still replay.
|
|
74
74
|
|
|
@@ -151,9 +151,9 @@ watch matches or process exits → pi-jar.shell message → agent wakes (o
|
|
|
151
151
|
- Progress (current tool, streaming text, tools used, turns, cost) stays in the activity view. Retained `idle` and `paused` agents remain visible in the footer/activity fleet; the newest eight ordinary retired reports remain for review, while unresolved private workspaces remain recoverable until explicit finalization/session cleanup. `s` in the activity view still steers a working subagent and `x` stops the selected agent through the same registry.
|
|
152
152
|
- The deprecated `write: true` mode remains a one-shot shared-workspace compatibility path. New delegated implementation should use `mode: "worktree"`.
|
|
153
153
|
|
|
154
|
-
### Exceptional
|
|
154
|
+
### Exceptional agent council
|
|
155
155
|
|
|
156
|
-
`
|
|
156
|
+
`jar_council` is **not** a default decomposition or decision tool. The moderator must supply `complexity: "super-complex"`, a concrete complexity justification, the persistent issue, at least two distinct failed approaches with evidence, and 2–8 uniquely identified options with tradeoffs. These gates are runtime-validated in addition to prompt guidance; the moderator is responsible for truthful evidence.
|
|
157
157
|
|
|
158
158
|
- Nominate relevant idle/paused **scouts** with `agents`, and set the total electorate with `voters` (default 4, or 2 at a cap of 2; always bounded by the configured cap). Forks, writable workers, busy agents and duplicate nominations are rejected. Reuse preserves context; prefer fresh scouts if discussion history could bias a ballot.
|
|
159
159
|
- Unfilled seats spawn fresh read-only scouts through the normal delegate controller. The same session-wide capacity applies; the moderator must explicitly stop unneeded agents if there is no room. Voting never silently kills existing workers. Scouts spawned for the round are retired once it ends, so they never keep holding pool slots; nominated scouts stay retained.
|
package/extensions/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
5
5
|
import { Key, truncateToWidth, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
6
6
|
import { ACCENT_NAMES, loadedAccents, selectAccent } from "../src/accent.ts";
|
|
7
7
|
import { registerAskTool } from "../src/ask-tool.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { registerCouncil } from "../src/council.ts";
|
|
9
9
|
import type { DelegateController } from "../src/delegate.ts";
|
|
10
10
|
import { ComposerStyle } from "../src/composer.ts";
|
|
11
11
|
import { installCompactBuiltinTools } from "../src/compact-tools.ts";
|
|
@@ -37,6 +37,7 @@ import { manageTasks } from "../src/tasks-ui.ts";
|
|
|
37
37
|
import { registerTaskTool, todoRow } from "../src/task-tool.ts";
|
|
38
38
|
import { TASK_ENTRY, TodoStore, todoProgress, todoTotals } from "../src/tasks.ts";
|
|
39
39
|
import { formatCost, sessionCost } from "../src/usage.ts";
|
|
40
|
+
import { branchCalls, compactForBudget, CONTEXT_CONTINUATION, ContextBudgetGuard, providerCall, resumeAfterBudget } from "../src/context-budget.ts";
|
|
40
41
|
import { WorkingState } from "../src/working.ts";
|
|
41
42
|
import { ChangeTracker } from "../src/changes.ts";
|
|
42
43
|
import { CHILD_ENV, DelegateRegistry, registerDelegate, SUBAGENT_MESSAGE, type DelegateRun } from "../src/delegate.ts";
|
|
@@ -61,8 +62,8 @@ const VERSION = (() => {
|
|
|
61
62
|
})();
|
|
62
63
|
/** Ignore the click Pi may synthesize right after a press we already acted on. */
|
|
63
64
|
const WELCOME_CLICK_DEDUPE_MS = 400;
|
|
64
|
-
/**
|
|
65
|
-
const
|
|
65
|
+
/** Conversation content, unlike startup metadata or extension bookkeeping. */
|
|
66
|
+
const SESSION_TRANSCRIPT_ENTRIES = new Set(["message", "custom_message", "compaction", "branch_summary"]);
|
|
66
67
|
export default function piJar(pi: ExtensionAPI): void {
|
|
67
68
|
const delegatedChild = process.env[CHILD_ENV] === "1";
|
|
68
69
|
const delegatedWorktree = delegatedChild ? process.env[CHILD_WORKTREE_ENV] : undefined;
|
|
@@ -71,7 +72,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
71
72
|
// and handlers are disabled. Remove only parent automation, not task/user context.
|
|
72
73
|
const parentAutomation = new Set([
|
|
73
74
|
"pi-jar.goal-context", "pi-jar.goal-continuation",
|
|
74
|
-
"pi-jar.plan-context", "pi-jar.plan-reminder", "pi-jar.moderator-context", SUBAGENT_MESSAGE, DISCUSSION_NOTICE
|
|
75
|
+
"pi-jar.plan-context", "pi-jar.plan-reminder", "pi-jar.moderator-context", SUBAGENT_MESSAGE, DISCUSSION_NOTICE, CONTEXT_CONTINUATION
|
|
75
76
|
]);
|
|
76
77
|
pi.on("context", (event) => {
|
|
77
78
|
const messages = event.messages.filter((message) => !parentAutomation.has((message as { customType?: string }).customType ?? ""));
|
|
@@ -97,7 +98,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
97
98
|
pi.setActiveTools([...allowed].filter(name => available.has(name)));
|
|
98
99
|
});
|
|
99
100
|
}
|
|
100
|
-
installCompactBuiltinTools(pi);
|
|
101
|
+
installCompactBuiltinTools(pi, { readCache: () => visualSettings.readCache === true });
|
|
101
102
|
let demo = false;
|
|
102
103
|
let visualSettings = defaultVisualSettings();
|
|
103
104
|
let profileStore: ProfileStore | undefined;
|
|
@@ -130,6 +131,15 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
130
131
|
if (footerTui && footerSettings.quota) quotaCache?.refresh(ctx.model?.provider, welcomeStatuses(), Date.now());
|
|
131
132
|
};
|
|
132
133
|
let cost = 0;
|
|
134
|
+
/**
|
|
135
|
+
* Context budget: footer chip, average cost per call, prompt hints and safe-point compaction.
|
|
136
|
+
* Delegated children never run it; the parent session owns the budget.
|
|
137
|
+
*/
|
|
138
|
+
const contextGuard = new ContextBudgetGuard(() => delegatedChild ? { ...visualSettings.contextBudget, action: "off" } : visualSettings.contextBudget);
|
|
139
|
+
/** jar_todo calls in flight → completed count before each ran; a raised count marks a safe point. */
|
|
140
|
+
const todoCalls = new Map<string, number>();
|
|
141
|
+
/** Bumped per session: a compaction finishing after a session change resumes nothing. */
|
|
142
|
+
let budgetGeneration = 0;
|
|
133
143
|
let welcomeInterval: ReturnType<typeof setInterval> | undefined;
|
|
134
144
|
let welcomeFrame = 0;
|
|
135
145
|
let welcomeDismiss = 0;
|
|
@@ -141,6 +151,8 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
141
151
|
let welcomeBranch = (): string | null => null;
|
|
142
152
|
let refreshWelcome: (() => void) | undefined;
|
|
143
153
|
let todos: TodoStore | undefined;
|
|
154
|
+
/** Leaf completion: jar_todo safe points, the stop decision and the topic hint. */
|
|
155
|
+
const todoCounts = () => todoTotals(todos?.all() ?? []);
|
|
144
156
|
let goals: GoalStore | undefined;
|
|
145
157
|
const composer = new ComposerStyle();
|
|
146
158
|
let footerSettings = visualSettings.footer;
|
|
@@ -231,7 +243,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
231
243
|
let subagentSignature = "";
|
|
232
244
|
subagents.subscribe(() => {
|
|
233
245
|
if (!footerTui) return;
|
|
234
|
-
const signature = subagents.
|
|
246
|
+
const signature = subagents.stateSignature();
|
|
235
247
|
repaint("subagents", signature === subagentSignature ? "background" : "transition");
|
|
236
248
|
subagentSignature = signature;
|
|
237
249
|
});
|
|
@@ -310,7 +322,9 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
310
322
|
registerContextDiet(pi, () => visualSettings.contextDiet === true);
|
|
311
323
|
const advisor = registerAdvisor(pi, modelRoles, { enabled: () => !delegatedChild && visualSettings.advisor, gates: () => !delegatedChild && visualSettings.advisorGates, usage: sideUsage });
|
|
312
324
|
registerInfoPanels(pi, { side: sideUsage, quotaEnabled: () => quotaCache?.enabled ?? false,
|
|
313
|
-
quota: (ctx) => quotaCache?.get(ctx.model?.provider, welcomeStatuses(), Date.now())
|
|
325
|
+
quota: (ctx) => quotaCache?.get(ctx.model?.provider, welcomeStatuses(), Date.now()),
|
|
326
|
+
quotaFailure: (ctx) => quotaCache?.failure(ctx.model?.provider, Date.now()),
|
|
327
|
+
refreshQuota: (ctx) => { quotaCache?.refresh(ctx.model?.provider, welcomeStatuses(), Date.now()); } });
|
|
314
328
|
let followingTheme: string | undefined;
|
|
315
329
|
const profileEntries = (ctx: ExtensionContext): readonly unknown[] => {
|
|
316
330
|
try { return ctx.sessionManager.getEntries?.() ?? sessionBranch(ctx); } catch { return sessionBranch(ctx); }
|
|
@@ -358,8 +372,9 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
358
372
|
discussionEnv: (key, name) => discussion?.childEnv(key, name),
|
|
359
373
|
discussionRetire: (key) => discussion?.retire(key),
|
|
360
374
|
discussionUnread: (key) => discussion?.unreadFor(key) ?? 0,
|
|
375
|
+
discussionDigest: () => discussion?.digest(),
|
|
361
376
|
getMaxSubagents: () => visualSettings.maxSubagents, onController: (controller) => { delegateController = controller; } });
|
|
362
|
-
if (!delegatedChild)
|
|
377
|
+
if (!delegatedChild) registerCouncil(pi, () => delegateController, () => visualSettings.maxSubagents);
|
|
363
378
|
const planMode = new PlanMode(pi, () => todos, modelRoles, updateTaskWidget);
|
|
364
379
|
if (!delegatedChild) planMode.register();
|
|
365
380
|
const goalLoop = new GoalLoop(pi, {
|
|
@@ -636,7 +651,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
636
651
|
sessionName: footerSessionName,
|
|
637
652
|
cwd: ctx.cwd, settings: footerSettings, branch: footerData.getGitBranch(),
|
|
638
653
|
context: contextUsage?.label ?? "ctx ?", goal: goalLoop.progress(), chips: footerChips(), activity: footerActivity(now),
|
|
639
|
-
memory, cost: formatCost(cost), quota, roles, extras: live.extras,
|
|
654
|
+
memory, cost: formatCost(cost, contextGuard.perCall), overBudget: contextGuard.over, quota, roles, extras: live.extras,
|
|
640
655
|
demo, animations, frame, motionBudget: ctx.isIdle() ? 2 : 1
|
|
641
656
|
}, width, ctx.ui.theme ?? theme);
|
|
642
657
|
hits = layout.hits;
|
|
@@ -808,7 +823,17 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
808
823
|
const restoreGoal = (ctx: ExtensionContext, branch?: readonly unknown[]) => {
|
|
809
824
|
try { goals?.restore(branch ?? sessionBranch(ctx)); } catch { goals?.restore([]); }
|
|
810
825
|
};
|
|
811
|
-
|
|
826
|
+
/** Awaited by agent_settled (see compactForBudget), whose handler is the last pi-jar settle handler. */
|
|
827
|
+
const settleBudget = async (ctx: ExtensionContext) => {
|
|
828
|
+
const job = contextGuard.settle({ idle: ctx.isIdle(), blocked: planMode.isEnabled() });
|
|
829
|
+
if (!job) return;
|
|
830
|
+
const open = (todos?.all() ?? []).filter((item) => !item.done).map((item) => item.title);
|
|
831
|
+
if (!job.compact) { resumeAfterBudget(pi, ctx, open); return; }
|
|
832
|
+
const generation = budgetGeneration;
|
|
833
|
+
await compactForBudget(pi, ctx, { resume: job.resume, tokens: contextGuard.lastTokens, softTokens: visualSettings.contextBudget.softTokens, open },
|
|
834
|
+
() => generation === budgetGeneration);
|
|
835
|
+
};
|
|
836
|
+
pi.on("session_start", (event, ctx) => {
|
|
812
837
|
sideUsage.clear();
|
|
813
838
|
if (!profileStore) initializeProfiles(ctx);
|
|
814
839
|
composer.setProfile(profileStore?.activeName ?? "Default");
|
|
@@ -859,6 +884,9 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
859
884
|
restoreTodos(ctx, branch);
|
|
860
885
|
restoreGoal(ctx, branch);
|
|
861
886
|
}
|
|
887
|
+
contextGuard.reset(branchCalls(branch, Date.now()));
|
|
888
|
+
todoCalls.clear();
|
|
889
|
+
budgetGeneration++;
|
|
862
890
|
// Quota is enabled per session; no credentials or consent are persisted.
|
|
863
891
|
quotaCache = new QuotaCache(
|
|
864
892
|
(provider: QuotaProvider, signal) => fetchQuota(provider, (id) => ctx.modelRegistry.getProviderAuth(id), signal),
|
|
@@ -890,9 +918,12 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
890
918
|
}
|
|
891
919
|
// Resumed sessions already have a transcript to paint. Rebuilding the animated welcome and
|
|
892
920
|
// scanning other session files competes with that expensive initial render for no real benefit.
|
|
893
|
-
//
|
|
894
|
-
|
|
895
|
-
|
|
921
|
+
// Fresh sessions can already contain model settings and other extensions' custom metadata.
|
|
922
|
+
// Those entries are not a transcript; only conversation content or an explicit resume/fork
|
|
923
|
+
// should suppress the welcome (including resumes of sessions containing only saved state).
|
|
924
|
+
const resumed = event.reason === "resume" || event.reason === "fork" || branch.some((entry) =>
|
|
925
|
+
typeof entry === "object" && entry !== null && "type" in entry
|
|
926
|
+
&& typeof entry.type === "string" && SESSION_TRANSCRIPT_ENTRIES.has(entry.type));
|
|
896
927
|
if (!delegatedChild && !resumed) showWelcome(ctx);
|
|
897
928
|
if (discussion) {
|
|
898
929
|
const current = discussion;
|
|
@@ -920,6 +951,11 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
920
951
|
}
|
|
921
952
|
if (event.source !== "interactive") return;
|
|
922
953
|
dismissWelcome(ctx);
|
|
954
|
+
// Hints read a finished list before this prompt acknowledges (hides) it.
|
|
955
|
+
if (!event.streamingBehavior) {
|
|
956
|
+
const hint = contextGuard.prompt(Date.now(), todoCounts());
|
|
957
|
+
if (hint) ctx.ui.notify(hint, "info");
|
|
958
|
+
}
|
|
923
959
|
if (!todosAcknowledged && todos?.all().every((item) => item.done)) { todosAcknowledged = true; updateTaskWidget(ctx); }
|
|
924
960
|
});
|
|
925
961
|
pi.on("agent_start", (_event, ctx) => {
|
|
@@ -937,20 +973,46 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
937
973
|
if (event.message.role === "assistant") {
|
|
938
974
|
working.reportOutputTokens(event.message.usage?.output ?? 0);
|
|
939
975
|
addMessageCost(event.message);
|
|
976
|
+
// Each provider call moves the per-call average and the budget; a crossing turns the context chip amber.
|
|
977
|
+
const notice = contextGuard.call(providerCall(event.message, Date.now()));
|
|
978
|
+
if (notice) ctx.ui.notify(notice, "warning");
|
|
940
979
|
applyWorking(ctx);
|
|
941
980
|
repaint("cost", "background");
|
|
942
981
|
}
|
|
943
982
|
});
|
|
944
|
-
pi.on("tool_execution_start", (event, ctx) => {
|
|
945
|
-
|
|
983
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
984
|
+
if (event.toolName === "jar_todo") todoCalls.set(event.toolCallId, todoCounts().done);
|
|
985
|
+
working.toolStart(event.toolCallId, event.toolName); applyWorking(ctx);
|
|
986
|
+
});
|
|
987
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
988
|
+
const before = todoCalls.get(event.toolCallId);
|
|
989
|
+
if (before !== undefined) {
|
|
990
|
+
todoCalls.delete(event.toolCallId);
|
|
991
|
+
if (todoCounts().done > before) contextGuard.todoCompleted();
|
|
992
|
+
}
|
|
993
|
+
working.toolEnd(event.toolCallId); applyWorking(ctx);
|
|
994
|
+
});
|
|
946
995
|
pi.on("ui_prompt_start", (_event, ctx) => { working.prompt(true); applyWorking(ctx); });
|
|
947
996
|
pi.on("ui_prompt_end", (_event, ctx) => { working.prompt(false); applyWorking(ctx); });
|
|
948
997
|
// A request may span several tool turns; keep its elapsed time and reported tokens until agent_end.
|
|
949
|
-
pi.on("turn_end", (
|
|
998
|
+
pi.on("turn_end", (event, ctx) => {
|
|
999
|
+
applyWorking(ctx); refreshQuota(ctx);
|
|
1000
|
+
todoCalls.clear();
|
|
1001
|
+
// Safe point: every tool result of this turn is in. Stop before the next request; agent_settled compacts and resumes.
|
|
1002
|
+
const stop = event.message?.role === "assistant" ? event.message.stopReason : undefined;
|
|
1003
|
+
if (contextGuard.turnEnd({ open: () => { const { done, total } = todoCounts(); return total - done; },
|
|
1004
|
+
interrupted: stop === "error" || stop === "aborted" || ctx.signal?.aborted === true || ctx.hasPendingMessages?.() === true,
|
|
1005
|
+
blocked: planMode.isEnabled() })) ctx.abort();
|
|
1006
|
+
});
|
|
950
1007
|
pi.on("agent_end", (_event, ctx) => { working.end(); applyWorking(ctx); sampleFooter(ctx); });
|
|
951
1008
|
pi.on("agent_before_settle", (event) => { if (event.outcome === "error") composer.flash("error"); });
|
|
952
1009
|
// Retries or compaction after agent_end may have added messages; otherwise the agent_end sample stands.
|
|
953
|
-
pi.on("agent_settled", (_event, ctx) => {
|
|
1010
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
1011
|
+
working.end(); applyWorking(ctx); if (contextUsage?.dirty) sampleFooter(ctx); refreshQuota(ctx);
|
|
1012
|
+
if (contextGuard.busy) return settleBudget(ctx);
|
|
1013
|
+
});
|
|
1014
|
+
// Pi's idle cache warming keeps the prompt cache alive; the cold-cache hint counts from its last refresh.
|
|
1015
|
+
pi.on("cache_warming_decision", (event) => { if (event.action === "warm") contextGuard.warmed(Date.now()); });
|
|
954
1016
|
pi.on("session_tree", (_event, ctx) => {
|
|
955
1017
|
let branch: readonly unknown[] = [];
|
|
956
1018
|
try { branch = sessionBranch(ctx); } catch { /* keep empty */ }
|
|
@@ -959,6 +1021,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
959
1021
|
restoreTodos(ctx, branch);
|
|
960
1022
|
restoreGoal(ctx, branch);
|
|
961
1023
|
}
|
|
1024
|
+
contextGuard.reset(branchCalls(branch, Date.now()));
|
|
962
1025
|
updateCost(ctx);
|
|
963
1026
|
composer.refreshSession(ctx);
|
|
964
1027
|
sampleFooter(ctx);
|
|
@@ -968,6 +1031,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
968
1031
|
try { branch = sessionBranch(ctx); } catch { /* keep empty */ }
|
|
969
1032
|
renders?.setEntries(branch.length);
|
|
970
1033
|
if (!delegatedChild) { restoreTodos(ctx, branch); restoreGoal(ctx, branch); }
|
|
1034
|
+
contextGuard.compacted();
|
|
971
1035
|
updateCost(ctx);
|
|
972
1036
|
sampleFooter(ctx);
|
|
973
1037
|
});
|
|
@@ -990,6 +1054,9 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
990
1054
|
renders = undefined;
|
|
991
1055
|
contextUsage?.dispose();
|
|
992
1056
|
contextUsage = undefined;
|
|
1057
|
+
contextGuard.reset();
|
|
1058
|
+
todoCalls.clear();
|
|
1059
|
+
budgetGeneration++;
|
|
993
1060
|
todos = undefined;
|
|
994
1061
|
changes = undefined;
|
|
995
1062
|
changeCount = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-jar",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "A warm, role-aware Pi terminal workspace with switchable profiles, animated UI, plan and goal workflows, tasks, change review, model roles, background shells and subagents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|