Skip to content

maple-agent: add a scenario checklist and a short performance check - #1099

Closed
AnthonyRonning wants to merge 2 commits into
agent-trim/docs-sweepfrom
agent-trim/checklist-perf
Closed

AnthonyRonning wants to merge 2 commits into
agent-trim/docs-sweepfrom
agent-trim/checklist-perf

Conversation

@AnthonyRonning

@AnthonyRonning AnthonyRonning commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Layer 9 of 9 in the Maple Agent approvals-and-subagents removal stack (base: agent-trim/docs-sweep, PR #1098). Docs only.

What changes

  • docs/scenario-checklist.md: one scenario per user-facing feature (tasks, composer, slash commands and skills, tools, external agents, computer use, MCP, ACP and command line, side models, the rest), each with how to run it and what passing looks like. Scenarios the runtime tests cover name the covering test. A results table records each run.
  • docs/performance-check.md: a quick release-build sanity check, not a gate. It records the time to an open window, memory after a long streamed answer, and whether streaming stays smooth, compared on the same machine before and after a runtime change.

Validation

  • just ci in the component Nix shell: green (docs only; Agent CI's change detection skips docs/).
  • The checklist ran on a release build of this stack (93a235d2): 15 scenarios passed on that build, 13 more passed earlier the same day on debug builds of the stack, and 10 are not run yet (listed in the results row with the reason). No scenario failed.
  • The performance check on that build: the window opened in 729 ms, memory was 187 MB after a 1500-word streamed answer, and streaming was smooth.

🤖 Generated with Claude Code

@AnthonyRonning
AnthonyRonning added this pull request to stack #1092 October 7, 2026 15:36
@AnthonyRonning
AnthonyRonning marked this pull request as ready for review October 7, 2026 15:36
@AnthonyRonning
AnthonyRonning force-pushed the agent-trim/checklist-perf branch from a768528 to adb3522 Compare October 7, 2026 19:54
@AnthonyRonning AnthonyRonning changed the title maple-agent: add the phase 0 scenario checklist and a performance check maple-agent: add a scenario checklist and a short performance check Oct 7, 2026
@AnthonyRonning
AnthonyRonning force-pushed the agent-trim/checklist-perf branch from adb3522 to 884dafe Compare October 7, 2026 20:36
@AnthonyRonning
AnthonyRonning removed this pull request from stack #1092 October 8, 2026 17:06
@AnthonyRonning
AnthonyRonning added this pull request to stack #1107 October 8, 2026 17:06
@AnthonyRonning
AnthonyRonning force-pushed the agent-trim/checklist-perf branch from 884dafe to 51b777b Compare October 8, 2026 17:20
@AnthonyRonning
AnthonyRonning force-pushed the agent-trim/checklist-perf branch from 51b777b to 31ab2e4 Compare October 8, 2026 18:50
AnthonyRonning and others added 2 commits October 10, 2026 07:32
Two documents and one script for the trimmed build. The scenario
checklist has one scenario per feature that carries over to the next
runtime, marks the ones the runtime tests already cover, and keeps a
results table; it ran on this build and runs again on the new runtime
before that merges. The performance check is a smoke and sanity check,
not a gate: cold start to an interactive window from the app's own
startup markers, resident memory at idle and after a long task, and a
by-hand note on whether a long streamed answer stays smooth. The
numbers recorded here come from a release build on the machine named
in the document and are to be repeated on the same machine before the
swap merges.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The performance check now asks for three things on a release build: the
time to an open window, memory after a long streamed answer, and whether
streaming stays smooth. The script and the extra startup and idle numbers
go. The scenario checklist drops its planning-stage name, cites the build
it ran on, and no longer claims that a background-completion test covers
Ctrl/Cmd+Enter steering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d
@AnthonyRonning
AnthonyRonning force-pushed the agent-trim/checklist-perf branch from 31ab2e4 to 9663764 Compare October 10, 2026 07:38
@AnthonyRonning
AnthonyRonning removed this pull request from stack #1107 October 10, 2026 09:01
AnthonyRonning added a commit that referenced this pull request Oct 11, 2026
Simplify the existing runtime before replacing Goose with the Pi crates:
remove approval and mode surfaces, Goose subagents, local usage tables,
and standalone CuaDriver; update the scenario and performance guidance.

Consolidates PRs #1090, #1091, #1093, #1094, #1095, #1096, #1097,
#1098 and #1099. The resulting tree is identical to 9663764.

Co-authored-by: Anthony Ronning <101225832+AnthonyRonning@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
AnthonyRonning added a commit that referenced this pull request Oct 13, 2026
Simplify the existing runtime before replacing Goose with the Pi crates:
remove approval and mode surfaces, Goose subagents, local usage tables,
and standalone CuaDriver; update the scenario and performance guidance.

Consolidates PRs #1090, #1091, #1093, #1094, #1095, #1096, #1097,
#1098 and #1099. The resulting tree is identical to 9663764.

Co-authored-by: Claude <noreply@anthropic.com>
AnthonyRonning added a commit that referenced this pull request Oct 13, 2026
…1104)

* maple-agent: consolidate phase 0 runtime simplification

Simplify the existing runtime before replacing Goose with the Pi crates:
remove approval and mode surfaces, Goose subagents, local usage tables,
and standalone CuaDriver; update the scenario and performance guidance.

Consolidates PRs #1090, #1091, #1093, #1094, #1095, #1096, #1097,
#1098 and #1099. The resulting tree is identical to 9663764acbffc155cfd867d2455201c9738f68bf.

Co-authored-by: Anthony Ronning <101225832+AnthonyRonning@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>

* pi-ai: model messages, streaming events and an OpenAI-compatible provider

The first of three crates that rebuild Pi's agent harness in Rust, after
Pi's pi-ai package. It has no Maple dependencies.

- One message model for every provider. System messages carry the
  prompt as named sections and the tool declarations, so replaying the
  transcript yields the current prompt and tools.
- A provider streams an assistant response as events that always end in
  Done or Error: request failures are messages, not errors. A builder
  emits the events in protocol order; consumers apply deltas.
- An API registry dispatches on the model's API, and a scripted faux
  provider stands in for models in tests.
- The OpenAI-compatible Chat Completions provider over a host-supplied
  HTTP transport, with transcript repair, reasoning effort, usage and
  cost.
- Shared helpers: streaming JSON repair for cut-off tool arguments,
  tool-argument validation with lenient coercion, context-overflow and
  retry classification, and token estimates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-agent-core: the agent loop, tools, hooks and message queues

The second crate, after Pi's pi-agent-core package. No Maple
dependencies.

- The loop streams a response, runs its tool calls and asks again until
  the model stops. Tool calls run in parallel by default (ends reported
  as they finish, results appended in call order) or one at a time when
  any tool asks. Calls from a length-truncated response never run.
- Steering messages arrive after the current turn's tool calls;
  follow-ups once the agent would stop.
- Applications plug in through hooks: message conversion, per-request
  context transforms and credentials, tool-call gates that fail closed,
  result rewrites, message finalization, and turn and request
  preparation (where compaction can run mid-run).
- The transcript type is the application's own: anything implementing
  AgentMessage, with a conversion to what the model sees.
- Agent owns the transcript, tools and queues and offers prompt, steer,
  follow-up, continue, abort, reset and awaited listeners.

Tests cover the event order, tool batches, gates and rewrites, queues,
turn hooks, continuation and cancellation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: sessions, compaction, extensions and the agent session

The third crate, after the core of Pi's coding agent. No Maple
dependencies, and no tools of its own: hosts register theirs.

- A session is an append-only tree of entries. Moving the leaf starts a
  branch; context edits, labels and branch summaries never rewrite
  history. Storage sits behind SessionStore (JSONL here) and is only
  written once there is a conversation.
- Compaction summarizes the older part of a branch near the context
  limit, never separating a tool result from its call and summarizing a
  split turn's start separately. Moving in the tree can summarize the
  branch being left.
- Resources from folders the host names: AGENTS.md context files,
  skills and prompt templates.
- The system prompt is built from named sections with the host's app
  name, and later changes are sent as section patches.
- The extension API: typed events with Pi's semantics (gates fail
  closed, rewrites chain, observer failures are isolated), tools,
  commands and model providers, and a context to act on the session.
- AgentSession ties it together: prompts through commands, input
  handlers and expansion, persistence of every message, retry with
  backoff that keeps failed attempts out of the context, and automatic
  compaction on overflow and near the limit.

Maple's runtime does not use these crates yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: harden the OpenAI stream, overflow checks and argument coercion

Fixes from a review of the provider code:

- A body that ends with neither [DONE] nor a finish reason is a
  retryable error instead of a finished reply, so tool calls cut off
  mid-arguments never run. A JSON error body sent with a success
  status is reported the same way, with the provider's message.
- Tool-call fragments without an index are told apart by id, and an
  id or name that arrives after the first fragment is filled in.
- Reasoning that a server sends in two fields is kept once.
- A 429 that mentions tokens is a rate limit, not a context overflow.
- Integer text keeps every digit when coerced, and a null the schema
  allows (anyOf with null, nullable, enum) is kept.
- A thinking level the model does not accept is clamped before it is
  sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-agent-core: keep queues and cancellation right around runs

- continue_run takes the run before draining the queues, so a refused
  continue no longer loses queued messages.
- A run whose future is dropped (a timeout, a panic) cancels its token,
  so the provider and tool work it started stops.
- The running flag changes under the run lock, so a run that is ending
  cannot mark the next one idle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: fix sessions, compaction and the agent session

Fixes from a review of the session code:

- The session is busy from a prompt's start until it settles, retries
  and compactions included. Another prompt is queued when it asks to
  be, otherwise refused, and compaction, tree moves and forks wait.
  A dropped prompt or compaction future no longer leaves it busy.
- A user message an extension sends goes through the prompt path, and
  a custom message that starts a run brings the system prompt up to
  date first. Failures are reported as extension errors.
- Queued extension messages are not listed as queued prompts.
- The starting thinking level is recorded, and a model an extension
  provides is restored on resume.
- A retry the run leaves unfinished ends there instead of carrying
  into the next prompt. A reply cut off by a full context is
  compacted and asked again. An aborted run is not compacted.
- Prompt options an extension makes invalid are refused and reported
  instead of breaking every later prompt.
- Compaction no longer treats a turn after a state change as split,
  context size ignores usage from before the latest compaction, and
  branch summaries see the branch as the context did (edits applied,
  no request when there is nothing to summarize).
- Forks keep what a compaction kept from a label. A failed write is
  written again with the next entry, entries are written in one call,
  a line cut off by a crash no longer swallows the next one, and
  loading skips lines that are not valid UTF-8.
- Resources: an empty frontmatter block parses, plain skill files are
  named after themselves, template name collisions are reported, and
  an oversized ${@:N} start selects nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: refresh each turn's settings and let stop cancel summaries

Three differences from Pi's session, found while comparing review
findings with Pi 1.0.4:

- The session's next-turn hook passes the current model, thinking
  level, active tools and prompt patch to the loop, as Pi's session
  does. Changes an extension or the host makes during a run now reach
  the run's next request; the loop declares a changed tool set.
- Branch summarization registers its cancellation, so stopping the
  session stops it. The move then ends as cancelled and the leaf
  stays where it was.
- Project skills and prompt templates override the user's, matching
  Pi's resource precedence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: send a model its earlier reasoning, as Pi does

The Chat Completions provider dropped earlier thinking when it built a
request, so a model lost its own reasoning between turns and across tool
calls. Pi sends it back, and so does today's Maple build.

- The field a response streamed its reasoning in (reasoning_content,
  reasoning or reasoning_text) becomes the thinking block's signature.
- A request sends a model's own earlier reasoning back in that field.
  Reasoning without a known field is not sent.
- Another model's reasoning goes back as plain text, before its answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: Pi's built-in tools, read, bash, edit, write and powershell

A session now creates Pi's built-in tools for its folder and gives the
model read, bash, edit and write, as Pi's does; powershell stays
registered for Windows. Hosts configure them through ToolsOptions, pick
the active ones, and replace one by registering a tool with its name.
They follow Pi's tools in names, schemas, limits, messages and prompt
text, written the Rust way:

- read: text a page at a time (2000 lines or 50KB, with continuation
  notes), images recognized by their bytes, BMP converted to PNG, and
  images resized to fit inline limits. macOS screenshot names are found
  when the typed name is not.
- bash and powershell: output streams to the interface at most every
  100ms; the model gets the last 2000 lines or 50KB, and the full output
  goes to a private file it can read. No default timeout. Stop, a
  timeout or a dropped call ends the command's whole process group (its
  process tree on Windows); a finished command may leave a background
  job running. A spawn hook adjusts the command, folder and environment;
  the session's id, file, model and thinking level are exposed as
  <APP>_* variables.
- edit: several disjoint replacements per call, matched against the
  original file, exactly or else loosely (trailing spaces, typographic
  quotes, dashes and spaces, Unicode compatibility forms), keeping line
  endings, the byte order mark and untouched lines. The result carries a
  display diff and a unified patch. Older argument shapes are accepted.
- write: creates folders as needed. Edits and writes to one file run one
  at a time.

Each tool takes swappable operations, as Pi's do. Settings gain Pi's
shellPath, shellCommandPrefix and images.autoResize. The system prompt
again says to use the shell for file operations when no file tools are
active, and loads skills through read, else bash, as Pi's does.

New dependencies: similar (diffs); image, base64, unicode-normalization
and libc were already in the lockfile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: Pi's grep, find and ls tools

Sessions now register Pi's grep, find and ls, off until a host or the
user turns them on, as in Pi. They follow Pi's tools in names, schemas,
limits, notices and prompt text, and search natively where Pi runs
ripgrep and fd, by the same rules:

- grep: Rust regular expressions (ripgrep's syntax), literal and
  case-insensitive search, a glob filter like ripgrep's --glob, context
  lines, 100 matches or 50KB, and lines cut at 500 characters. Hidden
  files are searched; a file's search stops at a NUL byte, as ripgrep's
  does for binary files.
- find: a pattern without / matches names, and one with / matches paths
  from the search folder at any depth; a pattern without capitals
  ignores case, as fd's does. Folders end with /. 1000 results or 50KB.
  node_modules is left out, as Pi asks of custom operations.
- ls: entries sorted regardless of case, dotfiles included and folders
  marked with /, 500 entries or 50KB.

grep and find skip .git and respect .gitignore, .ignore and their own
ignore file (.rgignore, .fdignore). As Pi has fd do, .gitignore applies
outside a repository too, and inside one a nested repository is not
matched against its parent's rules. Results come in path order. Each
tool takes swappable operations, as Pi's do.

New dependencies: ignore and globset, both already in the lockfile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: never retry an exhausted quota, as Pi does not

A failed response that names an account limit is no longer retried,
even when it comes as a 429: an exhausted quota, budget or balance,
billing, or a usage limit that resets after hours. Pi checks these
before its transient patterns, and so does is_retryable_error now.

The transient patterns gain the ones Pi 1.1 added: WebSocket closes,
HTTP/2 streams that got no response or were cancelled, retry-delay
caps, gRPC ResourceExhausted and gateway buffer limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: project trust, SYSTEM.md and APPEND_SYSTEM.md, and .agents skills

Three of Pi's resource features:

- Project trust. A folder's own resources (its project folder's skills,
  prompts, SYSTEM.md, APPEND_SYSTEM.md and settings, and .agents/skills)
  can carry instructions for the model, so they load only for a trusted
  project. resolve_project_trusted decides as Pi does: an override, then
  no project resources means trusted, then the decision remembered for
  the folder or one above it, then the defaultProjectTrust setting,
  which asks through the extension interface and remembers the answer.
  Decisions live in trust.json in the agent folder, behind a file lock.
  Only the user's settings can set defaultProjectTrust.
- SYSTEM.md replaces the default preamble, tools and rules, and
  APPEND_SYSTEM.md is added after the prompt's text: the project's for a
  trusted project, else the user's. A host's own prompt or appended text
  wins over them.
- Skills also load from .agents/skills, in the working folder and each
  one above it up to the repository root, and from ~/.agents/skills.

Context files load whether or not the project is trusted, as in Pi.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: strict tool schemas, after Pi's constrained sampling

A tool can ask for its arguments to be sampled to fit its schema, in a
provider's strict mode, preferring it or requiring it, as Pi's
constrainedSampling does. Providers opt in with supports_strict_mode,
off by default since being OpenAI-compatible does not imply it.

In strict mode the Chat Completions provider sends strict: true and the
schema rewritten as strict mode wants it: every property required,
optional ones made nullable, objects closed. Argument validation
already drops the nulls again. A schema strict mode cannot express
($ref, unions of objects, open objects and the like) is sent as usual
for a tool that prefers strictness, and fails the request for one that
requires it.

As in Pi, read, bash, powershell, edit and write prefer strict mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: the user's own shell commands, as Pi's ! and !!

AgentSession::execute_bash runs a command for the user in the session's
folder, with the settings' shell and command prefix, or through the
host's operations, and adds it to the conversation as a bashExecution
message. The model reads it as Pi words it: the command, its output in
a code block, and the exit code, cancellation or full-output file when
there is one. With exclude_from_context (!!command) it is kept in the
session but the model never sees it.

Output is cleaned as it streams (no colour codes, control characters or
carriage returns), reported in BashExecutionUpdate events, and kept to
its last 2000 lines or 50KB, with the whole of a long output saved to a
file. A command that ends during a run is added when the run ends, so
it never comes between a tool call and its result. abort_bash stops
running commands, and shutdown does too. record_bash_result adds a
result for hosts and extensions that run commands themselves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: make images that tools return fit, as Pi does

Images a tool hands back now go through the same processing as read's
before they enter the conversation: resized to fit inline limits,
unsupported formats converted, with Pi's note on the original size.
Tools that make images themselves (extensions, screenshots, MCP
servers) return whatever they have, and an image a provider rejects
fails every later request, not just one. It runs after the extensions'
tool_result handlers, so the images they add or replace fit too, and
follows images.autoResize. An image that cannot be processed is kept as
the tool gave it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: session stats, as Pi's

AgentSession::session_stats counts a session's user and assistant
messages, tool calls and results over every entry and branch, and adds
up the tokens and cost of every response, tools' own usage and
summaries included, with the current context usage, as Pi's
getSessionStats does. The prompt, which this port keeps in the
transcript, is not counted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: Stop ends a command whose background job keeps printing

After the shell exits, the tool keeps collecting output for as long as
some arrives within 100 ms, as Pi's does: a background job can hold the
output open. Pi's Stop and timeout still end the call then, and kill
what is left of the command's process group. The port stopped watching
both once the shell exited, so a job that kept printing, such as a dev
server started with `&`, held the call open, and neither Stop nor the
timeout could end it.

Stop and the timeout now end the call during that wait too, with the
group. Only the group is signalled, never the reaped shell's pid: the
group keeps the shell's id while any member lives.

Found in a security review of the stack. Master's shell tool capped the
wait at 500 ms instead; Pi keeps reading because a fixed deadline lost
output (earendil-works/pi#5303).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi crates: transports get the cancellation; a host names a session

- pi-ai: HttpTransport::post receives the request's cancellation, as Pi
  hands its fetch an abort signal. A transport with work that must
  finish, such as a credential refresh, sees the cancellation and winds
  down on its own.
- pi-coding-agent: SessionManager::create_with_id and new_id, as Pi's
  newSession takes an id. A host that lists a session before its first
  write opens it again under the same id.

Both are Pi's; phase 1 had not ported them. They were first written in
the next phase's pull request, where Maple needed them, and moved here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: replace a session's resources between prompts

AgentSession::set_resources takes the context files, skills and prompt
templates found again, as Pi's reload does with what its resource
loader finds. SYSTEM.md and APPEND_SYSTEM.md apply where the host gave
no prompt or appended text of its own, as when the session was built.
The next prompt brings the transcript's prompt up to date with the
sections that changed, so a host can look again at each run without
sending the whole prompt again.

Pi's; phase 1 had not ported it. It was first written in the next
phase's pull request, where Maple needed it, and moved here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: steering and follow-ups carry images, as Pi's do

AgentSession::steer and follow_up take images with the text, as Pi's
steer and followUp do, so a message sent into a running turn can show
the model a picture. The queue still lists the texts.

Pi's; phase 1 had not ported it. It was first written in the next
phase's pull request, where Maple needed it, and moved here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: Pi's truncate_middle

truncate_middle keeps the first and last halves of a byte limit and
marks the characters removed between them, as Pi's truncateMiddle does
for long MCP results in Codex's format. Cuts fall on character
boundaries.

write_output_file saves data to a new private output file, as the
tools save full output, for results a tool hands to the model as a
file: the whole of a cut MCP result, or a binary resource.

Pi's; phase 1 had not ported it. It was first written in the next
phase's pull request, where Maple needed it, and moved here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: register tools after a session starts

A tool can be offered from the next prompt on, as Pi's registerTool
can be called while Pi runs, for tools found later, such as an MCP
server's once it connects. The host registers one on its
AgentSession; an extension registers one through its context, which
it keeps from loading, so it holds no reference to the session.

A tool with a registered tool's name replaces it and keeps whether it
is active. The next prompt declares the change to the model.
ExtensionContext::tool_names lists the registered tools, as Pi's
getAllTools does, so an extension can name its own without taking
another's name.

Pi's; phase 1 had not ported it. It was first written in the next
phase's pull request, where Maple needed it, and moved here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: a host starts a turn with a custom message

AgentSession::send_custom_message starts a turn with a custom message
and waits for it, as Pi's sendCustomMessage does with triggerTurn, for
a host that hands the model something the user did not type, such as a
background agent's result. A message not to display reaches the model
as the user's turn but stays out of what the user sees. During a
prompt it is queued as steering instead.

Pi's; phase 1 had not ported it. It was first written in the next
phase's pull request, where Maple needed it, and moved here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: clamp thinking levels up first, and send off's value

Model::clamp_thinking_level moves a level the model does not accept to
the nearest accepted level above it, and below it only when there is
none above, as Pi's clampThinkingLevel does: a model that rejects
minimal now gets low instead of off. thinking_levels leaves out off
when the model's map sends it to None, as getSupportedThinkingLevels
does, so clamping off on such a model gives its lowest level.

With reasoning off, a reasoning model's Chat Completions request now
carries the value the map gives off, such as "none", as Pi's provider
sends reasoning_effort for it. An unlisted off still sends nothing.

Drift: Pi already did both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: a reply without a known finish reason fails, as Pi's does

[DONE] without a finish reason now ends the reply with the error a
stream gets when it stops before [DONE], since Pi's provider fails any
stream that ends without one. Finish reasons map as Pi's
mapStopReason maps them: stop and end are a stop, length a length,
tool_calls and function_call a tool use, whatever the content holds,
and any other reason is an error that names it, such as "Provider
finish_reason: network_error". The retry check reads that one as
transient and the overflow check reads z.ai's
model_context_window_exceeded as an overflow, as Pi's do. The port
had turned unknown reasons into a stop or a tool use, so the tool
calls of such a reply could run. content_filter reads as Pi's too.

A failed response keeps the usage the provider reported before it
failed, as Pi's error keeps its output's usage.
AssistantMessageBuilder::set_usage records it as it arrives.

Drift: Pi already did all of this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: error messages keep the whole error, as Pi's do

An error response's message is composed as Pi composes it from the
OpenAI SDK's error: the status and the provider's error object as
JSON, such as `429: {"message":"Rate limit reached"}`, the status and
the body when it is not JSON, or "400 status code (no body)" when
there is none. OpenRouter's metadata.raw, the upstream provider's own
error, follows on its own line when the message does not show it
already, for errors in the stream as well. The port kept only
error.message, so an overflow that OpenRouter wraps as "Provider
returned error" was retried instead of compacted.

A streamed chunk whose error field is null, false or empty no longer
fails the reply, since the SDK only fails on an error that is set.

The error object keeps the key order the server sent, as Pi's
JSON.stringify keeps it, so pi-ai turns on serde_json's
preserve_order, which Maple's workspace already has on.

Drift: Pi already did this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: send the transcript as Pi's Chat Completions provider does

- The system prompt has the developer role only for a reasoning model
  whose endpoint takes that role, as in Pi.
- A model without image input finds a placeholder where each run of
  images was, "(image omitted: model does not support images)" in a
  user message and "(tool image omitted: ...)" in a tool result, as
  Pi's transformMessages puts them. The port told it "(see attached
  image)" for an image it never sent, or dropped the image without a
  word. A tool result without output reads "(no tool output)", as in
  Pi.
- A user message goes out as a list of content parts without empty
  text, as Pi sends one, and is left out when no part remains. A
  reply's blank text blocks are left out and the others join without
  a separator, as in Pi. The port joined both with newlines, so a
  reply split into blocks came back with breaks the model never wrote.

Drift: Pi already did all three.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: classify overflow and transient errors as Pi does

- From Cerebras, a 400 or 413 without a body is an overflow, as Pi's
  isContextOverflow reads it; such a response now reads "400 status
  code (no body)".
- A length stop without output is an overflow once its input reaches
  99% of the window rather than all of it, as in Pi, since a server
  that cuts the input to fit can leave it a little short.
- is_recoverable_length, after Pi's isRecoverableLength, tells a
  length stop that ended below the output the caller wanted. Pi's
  session compacts and retries such a reply once.
- server_busy and "servers are currently busy" are retried, as in
  Pi 1.1.0.
- The overflow texts and their exclusions are Pi's. Four texts asked
  for less than Pi's, which want the sizes in them, and the
  exclusions took throttling, availability and rate limits anywhere,
  so a LiteLLM overflow inside a 503 ServiceUnavailableError was
  retried instead of compacted. A 429 still never counts as an
  overflow, as this port fixed.

Drift, but for is_recoverable_length, a Pi helper phase 1 had not
ported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi crates: fit the output cap to the window with Pi's estimate

- pi-ai: a Chat Completions request lowers max_tokens so the
  estimated input and the output stay inside the context window, with
  4,096 tokens to spare and at least one token, as Pi's
  clampMaxTokensToContext does for every simple request. The port
  always asked for the model's max_tokens, which servers that count
  input and output against one window reject for a long context.
- pi-ai: the estimate is Pi's: 3.5 characters per token, tools
  counted as the JSON they are declared in, and a context size made of
  the last usage a response reported and estimates after it, unless an
  earlier message is newer than that response. Pi went from 4 to 3.5
  characters in 1.1.0 so that fewer requests fail at the limit.
- pi-coding-agent: compaction gets its own estimate, Pi's compaction
  one at four characters per token, so its numbers do not move with
  the request's. It counts as Pi's does: a shell command by its text
  and output, a summary by its text, and a system message by its
  prompt, sections and added tools.

The fitting is a Pi feature phase 1 had not ported; the estimate is
drift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: key OpenAI's prompt cache and route by the session id

A request to api.openai.com carries the session id as
prompt_cache_key, cut to OpenAI's 64 characters, as Pi's Chat
Completions provider sends it, so a session's prompts share a cache.
With ModelCompat::send_session_affinity_headers, Pi's
sendSessionAffinityHeaders, the id also goes out in the headers that
session_affinity_format names: session_id, x-client-request-id and
x-session-affinity for openai, the last two for openai-nosession, and
x-session-id for openrouter. A request's own headers still win.
StreamOptions::session_id was carried but never read.

Pi's cacheRetention and OpenRouter's Anthropic-style cache_control are
left out, so requests go as Pi sends them with its default, short
retention. Pi turns the headers on for OpenRouter by itself; here a
host sets them, as it sets the other compat flags.

A Pi feature phase 1 had not ported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: read each server's cache usage, as Pi does

Cache hits come from prompt_tokens_details.cached_tokens, or else
DeepSeek's prompt_cache_hit_tokens or Kimi's top-level cached_tokens,
and OpenRouter's prompt_tokens_details.cache_write_tokens count as
cache writes, as Pi's parseChunkUsage reads them. Hits and writes are
taken out of the prompt's tokens, and the total adds them back.
Moonshot's usage inside the choice is read when the chunk has none, as
in Pi. The port read only cached_tokens, so these servers' hits were
priced as fresh input and OpenRouter's writes not at all.

Drift: Pi already did this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: repair broken strings in tool-call arguments, as Pi does

parse_streaming_json escapes raw control characters inside strings and
doubles a backslash that starts no valid escape, as Pi's repairJson
does, when the arguments do not parse as they are. Closing truncated
JSON is then tried on the text as it came and on the repaired text, in
Pi's order. Models write tabs, line breaks and Windows paths into
arguments this way, and the port turned such arguments into {}.

Drift: Pi already did this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: coerce tool arguments through unions, as Pi does

Coercion goes through every allOf schema, and to the first anyOf or
oneOf branch that accepts the coerced value unless the value already
fits a branch, as Pi's coerceWithJsonSchema does. "5" for anyOf
[integer, null], as MCP servers and pydantic declare optional
parameters, now becomes 5 instead of failing. Tuple items are coerced
by their own schemas, and objects and arrays are entered when their
schema is typed as one, as in Pi. Pi's other primitive rules apply
too: null becomes a number's 0, a boolean's false or a string's "",
and "", 0 and false become null for a null type.

An optional null is dropped when its schema, checked on its own,
rejects null, as Pi's normalizeOptionalNulls checks it, inside arrays
as well. A reference keeps its null, since it cannot be checked alone.
The port had kept the null of an OpenAPI-style nullable property,
which the validator then rejected.

Numeric strings still keep every digit, as this port decided.

Drift: Pi already did this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: apply_event follows a tool call's arguments as they stream

apply_event keeps the argument JSON received so far in a streaming
call's new ToolCall::partial_json, and on every fragment sets the
call's arguments to what that JSON parses to, as Pi's partial message
carries them and as Pi's streamProxy rebuilds them from deltas. When
the call ends, the final call replaces it without the text. Events
still carry deltas; a listener that rebuilds the message no longer
sees {} until the call is complete.

partial_json is never serialized. A ToolCall written out field by
field sets it to None.

A Pi feature phase 1 had not ported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: read server-sent events as Pi's OpenAI client does

The Chat Completions stream is read as the OpenAI SDK, which Pi's
provider streams through, reads it: lines end at \n, \r\n or \r, the
data lines of an event join until a blank line, and an event the
stream leaves open at its end still counts. The port read each data
line on its own.

An event whose data is not JSON now fails the reply with the SDK's
"Error reading response: malformed server-sent event JSON.", where
the port skipped it and could finish a reply with text missing. An
error event fails with its error, or with the event itself when it
has no error field.

A stream that ends without a finish reason says "Stream ended
without finish_reason", Pi's text. Lines that are not events, such as
a JSON error body, still follow that text, as before.

Drift: Pi already did this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: a broken settings file no longer stops the load

Settings::load leaves out a settings file that cannot be read or
parsed and returns it with its error, so the other file still
applies, as Pi's SettingsManager records the error and carries on.
The host shows the returned errors, as Pi's hosts show the ones
drainErrors hands them. A leading byte order mark is skipped and an
empty file has no settings, as in Pi.

A value of the wrong type leaves out its file the same way. Pi does
not check types when it loads; these settings are typed, so such a
file cannot be kept in part.

Drift: Pi already loads settings this way. Here a file saved with a
byte order mark, as some Windows editors save it, failed the load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: a prompt template path can name one file

An extra prompt template path that names a Markdown file loads that
file as a template, as Pi's loadPromptTemplates does with a path that
is a .md file. A folder is read as before. Another kind of file is
passed over, as in Pi.

Drift: Pi already takes a file there, from the settings, the command
line or an extension. Here a file path loaded nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: hold back a colour code split between output chunks

A user shell command's output is cleaned chunk by chunk, so a colour
code cut by a chunk's end was cleaned in halves and left pieces of
itself, such as "1m", in the text. An escape sequence that a chunk
ends in the middle of now waits for the next chunk, as Pi's bash
executor holds it back with splitIncompleteAnsiSuffix. As in Pi, only
the last 256 characters are searched, so a longer sequence is cleaned
as it is, and what is still held back when the command ends is
cleaned then and streamed too. The pattern is Pi's, written without
the lookahead Rust's regex crate does not have.

Drift: Pi already cleans the output this way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: compaction summaries think at the session's level

A compaction's summary requests, for the history and for a split
turn's start, carry the session's current thinking level, as Pi's
compaction sends it with each summary request. As in Pi's
createSummarizationOptions, the level goes only to a model that
reasons and only when it is not off. The session's level is clamped
to the model when it is set, and the provider clamps it again for the
request, as Pi's does.

Branch summaries still ask for no thinking. Pi's session hands the
level to generateBranchSummary along with the credentials, but that
function never reads it.

Drift: Pi already did this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: write branch summaries as Pi writes them

A branch summary now reads as Pi's generateBranchSummary writes it:

- It starts with Pi's preamble, which says the user explored another
  branch before returning, and ends with the files the branch read
  and changed. The entry keeps those lists in its details.
- The files come from the tool calls of the entries taken, and from
  every earlier branch summary on the branch that is not an
  extension's, as Pi's prepareBranchEntries collects them.
- Tool results are left out of the text, as Pi leaves them out: the
  calls say what was done.
- Entries are taken newest first while they fit the budget, which is
  the window, or Pi's 128k when the model does not say, less the
  reserve. An earlier compaction or branch summary that does not fit
  is still taken while less than 90% of the budget is used, and a
  budget of nothing left means no limit, as in Pi. The newest entry
  is no longer taken when it does not fit by itself, and the budget
  no longer has a floor of its own.
- The summary may run to 4096 tokens, as Pi's does, or the model's
  limit when lower.

As before, edited and omitted content stays out, which Pi does not
do, and a branch with nothing to summarize makes no request and keeps
an empty summary where Pi stores "No content to summarize".

Drift: Pi already did this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: estimate the context after edits and compactions

estimate_projected_context_tokens reads a branch's context size as
Pi's estimateProjectedContextTokens does: the last reported usage and
estimates for what came after it, unless a context edit or a
compaction came later on the branch. That usage measured a context
the edit or compaction has changed, so the projection is estimated
instead, with the prompt and its tools counted once. The session uses
it, as Pi's does, for the context usage it reports, for the
compaction checks after a run and before each next turn, and for a
compaction's tokens_before.

CompactionResult gains estimated_tokens_after, Pi's
estimatedTokensAfter: the estimate of the context the session goes
on with once it has recorded a compaction, its own or an extension's.

Pi reports the context usage as unknown after a compaction until a
response measures it again; this still reports the estimate.

The estimate was drift: usage from before a later edit was trusted,
for example during a retry, whose failed attempt an edit omits.
estimated_tokens_after is a Pi feature phase 1 had not ported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-agent-core: queue modes are spelled as Pi spells them

QueueMode is written and read as "all" and "one-at-a-time", as Pi's
QueueMode spells it, instead of "one_at_a_time". A settings file
written for Pi, whose steeringMode or followUpMode is
"one-at-a-time", now loads; until now that file was left out with an
error. Nothing else in the crates stores the mode.

Drift: Pi already spells it so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: a compaction update asks in Pi's update format

When a compaction updates the summary of an earlier one, its request
now reads as Pi's UPDATE_SUMMARIZATION_PROMPT: with the rule to
preserve exact file paths, function names and error messages, and
with Pi's update format, which asks to keep the earlier goals,
decisions and context and to move finished work to Done. The port
asked in the format of a first summary, which invites rewriting what
the earlier summary kept.

The two history prompts are now whole, as Pi's are, so they no
longer share a format. Every summary prompt now matches Pi's text.

Drift: Pi already did this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: an extension's compaction passes on no file lists

A compaction carries the read and modified files of the compaction
before it into its own lists only when that one is the session's
own, as Pi's compaction skips the details of a compaction an
extension wrote. An extension's details are its own data, in a shape
it chooses, and may not be file lists at all.

Drift: Pi already did this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: an empty argument hint is no hint

A prompt template whose argument-hint is empty has no hint, as Pi
keeps an argument hint only when it has text, so a host does not
show an empty hint next to the command.

Drift: Pi already did this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: search prompt template folders as Pi does

Prompt template folders are searched by the rules Pi's resource
discovery follows:

- The project's and the user's prompts folders offer their own files,
  as Pi's auto-discovery takes them. A folder named as an extra path,
  as a settings entry names one, is searched with its subfolders, as
  Pi collects a settings folder's files.
- Names starting with "." and node_modules are passed over.
- What a folder's .gitignore, .ignore or .fdignore leaves out is
  passed over, its rules made relative to the folder searched and
  matched regardless of case, as Pi's ignore matcher reads them. A
  parent folder's ignore files have no say.

A folder that links back into itself is searched once; Pi's search
recurses until the stack runs out and keeps what it found, which a
Rust program cannot do.

Drift: Pi already searched them so. The port read each folder one
level deep and took every Markdown file in it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: thinking levels as Pi sets them

A session starts at Pi's default level, medium, when neither its record
nor the settings name one; it started at off. As in Pi, a session with
a conversation keeps the level it recorded, a new one records the model
and level it starts with, and an older one without a recorded level
gets the level it now has.

Switching models applies the settings' default level again, or else
the current one, clamped to the new model, as Pi's setModel does; it
kept the current level. A level is recorded and announced only when it
changes, and a model switch is announced to extensions only when the
model really changes. Hosts hear of a new level through
ThinkingLevelChanged, Pi's thinking_level_changed. Without a model,
any level stands, as in Pi.

Drift: Pi already did all of this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: before_agent_start changes last for one run

Prompt options a before_agent_start handler returns now hold for the
run that prompt starts, every request of it included, and the next
prompt goes back to the session's own, as in Pi, where they are the
run's system prompt options. They were kept for every later prompt.
As in Pi, a handler that lists other tools changes which tools are
active, beyond the run, and the run's tool texts win over the tools'
own while it lasts.

Drift: Pi already did this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: Pi's two context events

The context event now has Pi's semantics: a handler sees the
conversation without its system messages, and the session puts the
prompt and tools back in front of what it returns, so it cannot drop
them. Ours behaved like Pi's context_with_system, which is now its own
event, ContextWithSystem: it runs after every context handler, sees
the whole transcript and is sent as returned.

As in Pi, a forced system prompt from before_agent_start is applied
after the handlers, so they see the transcript's own prompt.

Drift: Pi already had both events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: manual compaction stops a run first

AgentSession::compact stops whatever the session is doing, a run, a
retry, another compaction or a branch summary, waits for it to settle
and then compacts, as Pi's compact does. It refused with Busy. As in
Pi, the stopped run is not continued afterwards.

Drift: Pi already did this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: settings, tools and commands as Pi gives them

A session's automatic compaction and retries can be turned on and off,
and its steering and follow-up modes changed, as Pi's
setAutoCompactionEnabled, setAutoRetryEnabled, setSteeringMode and
setFollowUpMode do. They change the session's settings; unlike Pi's,
they do not write the settings files, which stay the host's.

all_tools describes every registered tool as Pi's getAllTools does:
its description, argument schema, guidelines, whether it is active
and which extension registered it. It replaces tool_names, which gave
the names only.

commands lists what a slash runs as Pi's getCommands does: extension
commands, then prompt templates, then skills as skill:<name>. It gave
extension commands only. Extensions get all_tools, commands and the
settings, as Pi's getAllTools, getCommands and getSettings.

Pi features phase 1 had not ported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: steering goes the way of a prompt, as in Pi

AgentSession::steer and follow_up now run the input handlers, expand
skills and prompt templates, and refuse an extension command, which
cannot wait in the queue, as Pi's steer and followUp do. They queued
the text as it was. They are async now, and say whether the input was
queued or an input handler took it.

The input event says how the input waits for the run in progress, as
Pi's streamingBehavior does, and an extension's user message carries
images and always takes the prompt path, during a run too, as Pi's
sendUserMessage does. During a run it skipped the input handlers.

Drift and a missing Pi feature (images in sendUserMessage).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: session events as Pi reports them

The end of an agent run is now its own session event, AgentEnd, which
says whether a retry follows, as Pi's agent_end does with willRetry.
Settled says whether the run was stopped, and extensions hear of it
first as AgentSettled, Pi's agent_settled.

Entries outside the conversation's flow are reported as Pi reports
them, as EntryAppended: an extension's own entry, and the context
edit that takes a failed attempt out of the context. Naming a session
is reported to the host and to extensions, as Pi's
session_info_changed, and hosts can name the session and read its
name, as with Pi's setSessionName and getSessionName.

Pi features phase 1 had not ported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: forks as Pi makes them

AgentSession::fork takes Pi's position. Before a user message, the new
session holds the path up to that message and the message's text comes
back to be edited; that is Pi's default. At an entry, it holds the
path up to and including it, as ours always did. A fork before the
first message starts an empty session.

As in Pi, extensions hear of a fork first as session_before_fork and
can cancel it, and a run in progress is stopped first. The forked
session's SessionStart names the file it came from, and a prompt's
before_agent_start event carries its images, as Pi's do.

Ours forks in place: the session and its extensions go on, so they get
a SessionStart without a shutdown before it, where Pi starts a new
runtime and shuts the old one down.

Pi features phase 1 had not ported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: what extensions can do, as in Pi

Every handler's context gains what Pi's ExtensionContext has and ours
lacked: compact, which runs without the handler waiting, stopping what
runs first, with a handle to await for the result; signal, the running
turn's cancellation; isProjectTrusted, which the host states in the
session's options; and the model registry.

Commands get their own context, as Pi's ExtensionCommandContext: the
same context plus what only a command the user started can wait for,
with no run waiting on it: waitForIdle, navigateTree, fork and the
system prompt options. pi-agent-core's Agent gives the running turn's
cancellation, as Pi's agent.signal does.

Pi features phase 1 had not ported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: images attached to a prompt are made to fit

A prompt's images are converted and resized as the read tool's are,
after the before_agent_start handlers, as Pi's prompt does. The notes
on what was done follow the prompt's text, and an image that cannot be
used is left out, with the note saying so. They were sent as they
came, so an image a provider rejects failed every later request.

Steering and follow-ups keep their images as they are, as in Pi.

Drift: Pi already did this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: moves in the tree as Pi makes them

AgentSession::navigate_tree takes Pi's options and answers as Pi's
navigateTree does. Moving to a user or custom message stands before
it, and its text comes back to be edited, so sending again starts a
branch there; before, the message stayed the leaf and sending put two
user messages in a row. Moving to the first message stands before
everything. After a move, the tools the transcript declares there are
the active ones; ours kept the current set.

A label goes on the summary, or on the target when there is none.
Custom instructions can replace the default summary instructions.
Extensions get Pi's tree preparation and the stop signal in
session_before_tree, and can change the instructions and the label as
well as supply the summary, which is used only when the user wants
one. session_tree carries the summary entry, as Pi's does.

Drift (where the leaf goes, the tools) and Pi features phase 1 had not
ported (labels, replaced instructions, the preparation).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: compaction checks and reports as Pi's do

After a run, the session decides as Pi's _checkCompaction does. A
retryable error is retried first, never an overflow. An overflow, or a
reply cut short below the model's output limit, is compacted and asked
again once; a second time, the failure is reported, in Pi's words. A
complete reply over the window is compacted and kept, and a context
near its limit is compacted, after a failed reply too, from the
estimate. A reply from another model, or from before the latest
compaction, is left alone, and a silent overflow is read from usage
only while no edit since has changed the context. Before a prompt,
the same check runs on the last reply, a stopped one included, as in
Pi, so a stopped run or a switch to a smaller model can compact first.

Compaction reports as Pi's does: CompactionEnd says whether it was
stopped and whether the reply is asked again, failures read
"Compaction failed: ...", "Auto-compaction failed: ..." or "Context
overflow recovery failed: ...", and an automatic compaction with
nothing to compact reports nothing. Extensions hear of a failure as
session_compact_failed, and session_before_compact and
session_compact carry the reason, whether the reply is asked again,
and, before, the branch and the stop signal.

A reply's place in the branch says whether it came before the latest
compaction; Pi compares timestamps, which a fast reply can share with
the compaction. The timestamps decide only for a reply the branch does
not hold.

Drift (the checks, the order) and Pi features phase 1 had not ported
(the cut-short trigger, the check before a prompt, the events).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: context usage is unknown after a compaction

As Pi's getContextUsage reports it, the context size is unknown after
a compaction until a reply measures the context again: the usage kept
from before the compaction measured the old context. ContextUsage's
tokens and percent are options now, and a model without a context
window has no usage. Ours went on reporting an estimate.

Drift: Pi already did this, since badlogic/pi-mono#1382.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: summary requests report their retries

A summary request that fails for a moment is tried again, and the
host hears of it as Pi's summarization retry events report it: the
retry scheduled, with its attempt, delay and error; the request going
out again, for a compaction with its reason or for a branch summary;
and the end of the retries, however they end. As in Pi's
retryAssistantCall, summaries do not retry when retries are off, and
the delays grow to the settings' longest one, which was fixed at a
minute.

Pi features phase 1 had not ported, and drift (the policy).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: settings resource paths resolve and rank as in Pi

The skill and prompt template paths a settings file lists resolve as
Pi resolves them: `~` is the home folder, and a relative path is
relative to the folder of the settings file that lists it. They were
resolved against the working folder. Both files' lists apply, the
project's first, as Pi's resource loader takes them; a project's list
replaced the user's.

What the settings list comes before the default folders, so a listed
template or skill wins a name clash over the folders', as in Pi. Paths
a host adds still come after the folders, as Pi's additional paths do.
Resources::load takes the settings for their lists, and load_skills
and load_prompt_templates take the listed paths ahead of the host's.

Decided with the repository owner: follow Pi in both.

Drift: Pi already did this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: thinking levels for particular models, as Pi's

Settings::model_thinking_levels is Pi's modelThinkingLevels: a level
per model, by provider/model-id. As in Pi, a new session starts at its
model's own level before the settings' default, and a switch to a
model with its own level applies it. A session restored with a
conversation keeps the level it recorded. This is where a host's
recommendations per model go.

A Pi feature phase 1 had not ported; it was listed as not implemented
yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-agent-core: a failed run still ends, and prompts take images

As in Pi's Agent, a run whose loop fails still ends as runs end: with
an empty reply carrying the error (aborted when the run was stopped),
then message_start, message_end, turn_end and agent_end, and the
prompt returns normally. In Rust only a panic in a hook, a listener
or the stream function fails the loop, and it ended the run without
those events. What the run left behind is then cancelled.

prompt_with_images is Pi's prompt(text, images): the images follow
the text in one user message. clear_steering_queue and
clear_follow_up_queue each clear one queue, as Pi's do; like
clear_queues, they return what they removed.

Pi features phase 1 had not ported; they were listed to decide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: request options and retries, as Pi's requests take them

StreamOptions gains Pi's request options for the Chat Completions
provider:
- tool_choice, auto or none;
- sampling_params, merged into the body after the named fields, over
  the model's own and those for the request's thinking level;
- timeout_ms, how long a response may take to start: ten minutes when
  unset, as in the OpenAI SDK;
- transform_headers, Pi's transformHeaders, which rewrites the
  request's own headers before they are sent;
- on_response and on_provider_stream_event, which see the response's
  status and headers, and each streamed event before it is read.
A request header may now be None, which removes it, and a model's own
headers go under the request's.

max_retries and max_retry_delay_ms are Pi's retryProviderRequest. A
request without a response, a timeout, a conflict, a rate limit or a
server error is sent again, or as x-should-retry says, after the wait
retry-after-ms or retry-after asks for or else a growing backoff. A
server asking for longer than the limit (a minute unless set) fails
the request at once, and the session's retries take over. Unset,
nothing is retried, as in Pi. HttpResponse carries the response
headers for this.

Pi features phase 1 had not ported; they were listed to decide. Left
out: the env, metadata and websocket options, which serve providers
or transports these crates do not have.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: session requests carry Pi's provider settings

A session's own requests now go out as Pi's session sends them
(buildRequestOptions), with options read as each request goes:
- the session's id;
- how long the response may take to start: retry.provider.timeoutMs,
  or else httpIdleTimeoutMs, five minutes unless set, and zero or
  "disabled" for no limit;
- retry.provider.maxRetries and maxRetryDelayMs, for a provider's
  retries before a response starts;
- hooks for the extension events before_provider_headers (rewrite the
  request's headers; None removes one), after_provider_response and
  provider_stream_event, next to before_provider_request.
Compaction and summaries still go to the models directly, as in Pi.

The retry settings take Pi's current names: the session's longest
delay is maxAgentDelayMs, and a file's older maxDelayMs moves to
retry.provider.maxRetryDelayMs unless the file sets that too, as Pi
migrates it. maxDelayMs had been read as the session's delay.

Pi features phase 1 had not ported, and drift (the names). Pi also
gives its HTTP client httpIdleTimeoutMs as the time a response may
stall; here that stays the transport's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-ai: classifiers and System One, after Pi's classify()

Pi's classifier API: a ClassifierModel; a ClassifierContext with a
JSON state, images and named questions (choice, score and yes/no);
and answers with their probabilities, in a ClassifierResult that ends
in error or aborted as a failed reply does. ClassifyFn is Pi's
ProviderClassifier, and ClassifierRegistry dispatches by API as Pi's
classify() does, refusing images for a model without image input.
The session's ModelRegistry holds classifier models and APIs and
classifies with the provider's key; extensions register classifiers,
as with Pi's registerProvider, and reach them through their context's
models().

system_one speaks System One's protocol (POST {base_url}/systemone),
sent as Pi's postClassifierRequest sends it: the payload and response
hooks, a timeout per attempt, two retries unless set, and Pi's error
messages. A yes/no question is a noul question on the wire.

The protocol is Maple's version, so these differ from Pi:
- images go as data URLs and the request's temperature is sent; Pi's
  typesafe-system-one sends neither;
- answers keep the temperature and option mass Maple's server
  reports, and scores keep their probabilities and legend;
- the result keeps the reply's id and its upstream request count;
- the state is any JSON, instructions and criteria are JSON values,
  and a yes/no question's criteria are optional, as Maple's API takes
  them; Pi takes an object and text;
- cached tokens are read out of the input as cache reads, as Chat
  Completions usage is;
- an API key is optional, since the host's transport may
  authenticate, as for Chat Completions.
FauxClassifier is a scripted classifier for tests; Pi has none.

Pi's TypeSafe, Cloudflare, llama.cpp and OpenAI Decisions APIs are
left out, as the repository owner decided.

A Pi feature phase 1 had not ported. The differences above follow
Maple's System One API, at the repository owner's request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d

* pi-coding-agent: skill folders follow their ignore files, as in Pi

A skills folder's search now leaves out what the .gitignore, .ignore
and .fdignore files of each folder searched name, as Pi's skill scan
does and as the template folders already did. A SKILL.md they leave
out does not make its folder a skill, and the folder's subfolders are
still searched, as in Pi.

A folder linked from inside itself is searched once, as for template
folders. Pi recurses until its stack runs out and keeps what it found;
in Rust that would end the process.

A Pi feature phase 1 had not ported; it was listed to decide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: htt…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants