@tylerho/pi-summaries
Generates a compact recap of each agent run and appends it to the session once the run settles.
Package details
Install @tylerho/pi-summaries from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@tylerho/pi-summaries- Package
@tylerho/pi-summaries- Version
0.2.1- Published
- Sep 21, 2026
- Downloads
- 276/mo · 176/wk
- Author
- tylerho
- License
- MIT
- Types
- extension
- Size
- 81.2 KB
- Dependencies
- 1 dependency · 3 peers
Pi manifest JSON
{
"extensions": [
"extensions/summaries"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@tylerho/pi-summaries
Generates a compact recap of each agent run and appends it to the session once the run settles.
Install
pi install npm:@tylerho/pi-summaries
Summaries
After an agent run settles, this extension writes a short recap and one next step, then appends them to the session as a summary-recap entry that the TUI renders as a "✦ Recap" card. Generation runs on its own configured model, held in ~/.pi/agent/summaries/config.private.json, so recap traffic never touches the main agent's provider rotation. /recap changes the model or turns the feature on and off.
How it works
Recaps cover a run, and a run begins at a user turn and ends when pi settles. createRunBoundary() in shared/transcript.ts holds one marker, baselineLeafId. before_agent_start records the current leaf with begin(leafId), and the first call wins because the marker is set with ??=. Recaps wait for a quiet session, so several runs can pile up behind that one marker, and the eventual recap covers all of them. getRunEntries(branch, baselineLeafId) slices the session branch after the baseline leaf. When that leaf is gone, the entries come back empty and the boundary is cleared without a recap.
The recap fires on agent_settled rather than agent_end. pi may auto-retry or queue follow-ups after agent_end, and settling means the run is actually over. The handler requires TUI mode, an active session, and a pending run.
scheduleRecap arms a setTimeout for idleDelayMs() and calls .unref(), so a pending recap never holds the process open. The delay defaults to 180,000 ms, which matches Claude Code's away-summary delay, and the floor is MIN_IDLE_DELAY_MS (30,000 ms). PI_SUMMARY_IDLE_MS overrides it with a positive integer in milliseconds. The timer only decides whether a timer exists. The fire-time checks in writeRecap are the authority: a pending run, config.enabled, sessionActive, ctx.isIdle(), !ctx.hasPendingMessages(), and !anyRunning().
User activity drops an armed recap. The input handler calls onUserActivity() when event.source === "interactive", and user_bash calls it for shell activity. That cancels the timer and aborts in-flight recaps, because a recap landing mid-turn is noise. The boundary survives the abort, so the next quiet stretch recaps the dropped run together with the new one.
Child agents keep the recap deferred. The extension subscribes to shared/agent-activity.ts when it loads, and a notification cancels the timer while any child reports a running count. When the last child settles and the session is idle, the listener re-arms it. A run whose subagent outlives the main agent still gets a recap, written after the child finishes.
summarizeRun looks the configured model up with ctx.modelRegistry.find(provider, model) and throws SummaryError when the model is unavailable. It gets the key and headers from getApiKeyAndHeaders, then calls completeSimple from @earendil-works/pi-ai/compat with SUMMARY_SYSTEM_PROMPT, maxRetries: 1, timeoutMs: 40_000, and an outer Effect.timeout("100 seconds") that gives both attempts room. reasoningOptions passes the reasoning level through unless the level is "off". Thinking tokens count toward DeepSeek's output budget, so a reasoning run gets maxTokens: 8_000 and a run without reasoning gets 1_000.
A parse failure or a length-truncated response from a reasoning run retries once with reasoning off, which needs far fewer tokens. Transport failures do not retry. With reasoning already off, a length stop reports The summary model response hit the token limit and was truncated., and any other parse failure reports that the model did not return valid recap JSON.
parseRecapResponse requires exactly one JSON object with only the keys recap and next. It tries the trimmed response first, then any fenced block, then the brace-delimited slice between the first { and the last }. Each field is stripped of ANSI/OSC sequences and control characters, trimmed, and capped (RECAP_MAX_LENGTH 600, NEXT_MAX_LENGTH 200) with an ellipsis marking the cut. A leading Next: prefix is removed from the next step. An empty field makes the candidate invalid.
The system prompt asks for at most two short sentences of 40 words or fewer, covering what was done and the outcome, plus one actionable next step. buildSummaryPrompt wraps the transcript in <recent_work> tags. The 600 and 200 character caps are a backstop, not the target.
When the model call fails and the failure is not an abort or a shutdown, buildFallbackRecap derives a recap from the transcript: the unique tool names, the number of tool calls, and the final assistant text capped at 700 bytes. The entry is tagged fallback: true and the extension shows a warning notification with the error detail.
serializeRunTranscript renders session entries with role prefixes: USER, ASSISTANT, TOOL CALL <name>, TOOL RESULT <name> (with (error)), USER SHELL (with an exit code), and EXTENSION <customType>. It keeps text and tool-call blocks, so thinking blocks and images never reach the summary model. summary-recap entries are skipped, which keeps the transcript from recursing on its own output. Secrets are redacted twice: redactSecrets covers bearer tokens, API key shapes, and key=value patterns, and sanitizeValue redacts any value whose key name matches a secret pattern. Tool arguments are capped at 2,000 bytes and tool results at 5,000 bytes, each with a notice line. The full transcript is capped at 48,000 bytes and kept as 58 percent head and the rest tail around a middle-omitted marker.
A finished recap goes to pi.appendEntry("summary-recap", recap), which persists it in the session and keeps it out of LLM context. registerEntryRenderer renders it as a RecapCard: a padded box on the customMessageBg background, an accent ✦ Recap title, the recap as Markdown, and a Next: line. Expanded with ctrl+o, the card adds a dim source line with provider/model · reasoning, plus · local fallback for a fallback recap. A missing entry payload renders Recap unavailable in the warning color.
While any recap task is active, ctx.ui.setStatus("summaries", "✦ summarizing run…") sets the muted footer status that expanded-footer.md renders. session_shutdown marks the session inactive, cancels the timer, clears the boundary, unsubscribes from activity changes, aborts in-flight recaps, waits up to SHUTDOWN_WAIT_MS (1,000 ms) for them, and clears the status. sessionActive is ctx.mode === "tui", so the recap path does nothing outside the TUI.
config.enabled gates the feature at both ends. A disabled config never arms a timer, and writeRecap re-checks at fire time, so an already-armed timer becomes a no-op. Disabling through /recap also cancels the armed timer and aborts in-flight recaps. The run boundary survives the toggle, so re-enabling recaps everything since the baseline.
The extension registers one command, one entry renderer, and six event handlers. It registers no tools and no shortcuts.
API
Commands
/recap configures run recaps. It takes no arguments and works in the TUI only, notifying Recap settings are only available in the TUI. in other modes when a UI is present. It loads the current config and opens ui.select("Recap", …) with two items.
Change model…runsopenModelPicker, thenopenReasoningPicker, thensaveSummaryConfig, and notifiesSummary model: <provider>/<model> · <reasoning>. A dismiss at any step returns without a change, and a failed save notifiesCould not save the private summary model config.Disable recapsandEnable recapsflipconfig.enabled, save, and notifyRecaps disabled.orRecaps enabled.. Disabling also callsonUserActivity(), which cancels the armed timer and aborts in-flight recaps. A failed save notifiesCould not save the private summary config.
openModelPicker offers curatedModels(ctx.modelRegistry.getAvailable(), ctx.cwd), the same scope /subagent-model uses, cheapest first. A label reads provider/model and adds · $X/Mtok out when the model reports an output cost. An empty list notifies No configured models are available for run recaps. and returns undefined. openReasoningPicker renders a ThinkingSelectorComponent limited to getSupportedThinkingLevels(model) and preselects the current level when the model supports it.
Events
| Event | Handler |
|---|---|
session_start |
Sets sessionActive = ctx.mode === "tui", records statusContext, cancels any timer, clears the run boundary. |
input |
Calls onUserActivity() when event.source === "interactive". |
user_bash |
Calls onUserActivity(). |
before_agent_start |
TUI only: onUserActivity(), then runBoundary.begin(ctx.sessionManager.getLeafId()). |
agent_settled |
TUI, active session, pending run: scheduleRecap(ctx). |
session_shutdown |
Async. Marks the session inactive, cancels the timer, clears the boundary, unsubscribes from activity changes, aborts in-flight recaps, waits up to 1 s, clears the summaries status. |
Entry renderer
registerEntryRenderer("summary-recap", (entry, { expanded }, theme) => renderRecap(entry.data, expanded, theme)). renderRecap takes RecapEntryData, which is RunRecap (recap, next) plus the config fields provider, model, reasoning, and an optional fallback.
Config and environment
~/.pi/agent/summaries/config.private.json(mode0o600, gitignored) holds{ "enabled", "provider", "model", "reasoning" }, with defaultsenabled: true,deepseek/deepseek-flash/medium.parseSummaryConfigfalls back toDEFAULT_SUMMARY_CONFIGon any missing or invalid field, with one exception: a missingenabledmeans true, because configs written before the toggle predate the field. A mistypedenabledstill falls back to the full defaults. Provider and model are trimmed.REASONING_LEVELSisoff,minimal,low,medium,high,xhigh,max.PI_SUMMARY_IDLE_MSoverrides the quiet delay. A value below 30,000 ms is clamped up, and an invalid value falls back to 180,000 ms.saveSummaryConfigwrites atomically throughwriteJsonAtomic(temp file plus rename) with a 5 second timeout, and accepts an abort signal.- The config used to live inside the extension directory, which broke on a clean package install. That file is ignored now, and a user re-selects the model once with
/recap.
Internal exports
src/config.ts:REASONING_LEVELS,ReasoningLevel,SummaryConfig,DEFAULT_SUMMARY_CONFIG,DEFAULT_IDLE_DELAY_MS,MIN_IDLE_DELAY_MS,idleDelayMs(),PRIVATE_CONFIG_PATH(),parseSummaryConfig(),loadSummaryConfig(),saveSummaryConfig().PRIVATE_CONFIG_PATHis a function resolved at call time fromgetAgentDir(), not a module-load constant, so tests point it at a temp dir withPI_CODING_AGENT_DIR.src/summarizer.ts:RunRecap,parseRecapResponse(),reasoningOptions(),summarizeRun().src/prompt.ts:SUMMARY_SYSTEM_PROMPT,buildSummaryPrompt(transcript).src/ui.ts:RecapEntryData,renderRecap(),openModelPicker(),openReasoningPicker().
Examples
- After any settled run, a recap card appears once the session goes quiet, for example
Updated config and ran focused tests.withNext: Review the diff.Press ctrl+o to see which model wrote it. - To change the recap model, run
/recap, pickChange model…, pick a provider and model from the list, pick a reasoning level, and confirm the notificationSummary model: openrouter/anthropic/claude-sonnet-4 · high. - To pause the feature, run
/recapand pickDisable recaps. Recaps resume from the same run boundary, so the first recap after re-enabling covers everything that accumulated.config.private.jsoncan be edited by hand too, and an invalid value falls back to the defaults. - For faster recaps during interactive work, start pi with
PI_SUMMARY_IDLE_MS=30000, so the recap lands 30 seconds after a run settles instead of 3 minutes. - To debug a missing recap, check the
summariesfooter status during generation, the warning notification when the model failed, and the deferral conditions. Pending user input, pending messages, or a running child agent each defer a recap, and a run with no entries after the baseline is cleared without one.