Repository navigation
maple-agent: add a scenario checklist and a short performance check - #1099
Closed
AnthonyRonning wants to merge 2 commits into
Closed
AnthonyRonning wants to merge 2 commits into
AnthonyRonning wants to merge 2 commits into
Conversation
AnthonyRonning
added this pull request to stack #1092
October 7, 2026 15:36
AnthonyRonning
marked this pull request as ready for review
October 7, 2026 15:36
AnthonyRonning
force-pushed
the
agent-trim/checklist-perf
branch
from
October 7, 2026 19:54
a768528 to
adb3522
Compare
AnthonyRonning
force-pushed
the
agent-trim/docs-sweep
branch
from
October 7, 2026 19:54
93a235d to
02d81d2
Compare
AnthonyRonning
force-pushed
the
agent-trim/checklist-perf
branch
from
October 7, 2026 20:36
adb3522 to
884dafe
Compare
AnthonyRonning
force-pushed
the
agent-trim/docs-sweep
branch
from
October 7, 2026 20:36
02d81d2 to
76f1e13
Compare
AnthonyRonning
removed this pull request from stack #1092
October 8, 2026 17:06
AnthonyRonning
added this pull request to stack #1107
October 8, 2026 17:06
AnthonyRonning
force-pushed
the
agent-trim/docs-sweep
branch
from
October 8, 2026 17:20
76f1e13 to
d589551
Compare
AnthonyRonning
force-pushed
the
agent-trim/checklist-perf
branch
from
October 8, 2026 17:20
884dafe to
51b777b
Compare
AnthonyRonning
force-pushed
the
agent-trim/docs-sweep
branch
from
October 8, 2026 18:50
d589551 to
2d8c4f2
Compare
AnthonyRonning
force-pushed
the
agent-trim/checklist-perf
branch
from
October 8, 2026 18:50
51b777b to
31ab2e4
Compare
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
force-pushed
the
agent-trim/checklist-perf
branch
from
October 10, 2026 07:38
31ab2e4 to
9663764
Compare
AnthonyRonning
force-pushed
the
agent-trim/docs-sweep
branch
from
October 10, 2026 07:38
2d8c4f2 to
6488691
Compare
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…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 ciin the component Nix shell: green (docs only; Agent CI's change detection skipsdocs/).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.🤖 Generated with Claude Code