Repository navigation
Agent core in Rust after Pi: pi-ai, pi-agent-core, pi-coding-agent - #1104
Merged
Merged
Conversation
Contributor
Author
|
The description now has a Compared with Pi section. Every review finding was checked against Pi 1.0.4, and the shared ones again against Pi 1.1.0 (
The same commit makes a project's skills and templates override the user's, as in Pi. Decision for now: nothing is reported upstream, and the shared behaviors and quirks stay as they are. Which of them to change will be decided once the Rust version works end to end. Generated by Claude Code |
AnthonyRonning
added this pull request to stack #1107
October 8, 2026 17:06
AnthonyRonning
force-pushed
the
agent-trim/checklist-perf
branch
from
October 8, 2026 17:20
884dafe to
51b777b
Compare
AnthonyRonning
force-pushed
the
claude/kind-turing-gzv8nl
branch
2 times, most recently
from
October 8, 2026 18:50
2052e83 to
d03a8e2
Compare
AnthonyRonning
force-pushed
the
agent-trim/checklist-perf
branch
from
October 8, 2026 18:50
51b777b to
31ab2e4
Compare
AnthonyRonning
pushed a commit
that referenced
this pull request
Oct 9, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d
AnthonyRonning
pushed a commit
that referenced
this pull request
Oct 9, 2026
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
claude/kind-turing-gzv8nl
branch
3 times, most recently
from
October 10, 2026 09:01
d95c54c to
ec74687
Compare
AnthonyRonning
removed this pull request from stack #1107
October 10, 2026 09:01
AnthonyRonning
changed the base branch from
agent-trim/checklist-perf
to
agent-pi/phase-0
October 10, 2026 09:01
AnthonyRonning
added this pull request to stack #1125
October 10, 2026 09:01
AnthonyRonning
force-pushed
the
claude/kind-turing-gzv8nl
branch
from
October 10, 2026 21:32
fe8c5ff to
df7bfbd
Compare
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>
…ider 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
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
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
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
- 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
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
…maries 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
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
…ershell 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
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
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
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
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
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
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
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
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 earendil-works/pi#1382. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d
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
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
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
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
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
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'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
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: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d
In a linked worktree inside the main checkout (git worktree add .worktrees/feat, say), the main checkout's AGENTS.md is an ancestor of the worktree, so both it and the worktree's own copy loaded: the same repository's context twice. As Pi's findShadowedContextFile does, the main checkout's file with the name of the worktree's own is now left out. A worktree beside the main checkout, a bare layout and a submodule are left alone, as in Pi, and so is a worktree without a context file of its own, which still inherits the main checkout's. 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: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d
compaction.modelOverrides gives a model, by provider/model-id, its own reserveTokens and keepRecentTokens, over the others, as Pi's getCompactionSettings resolves them. The session applies them where Pi does: automatic compaction after a reply and before the next turn, and every compaction's cut. Branch summaries keep the plain reserve, as they stand in for Pi's branchSummary settings. images.blockImages sends the model no images: in user messages and tool results each becomes "Image reading is disabled.", one note for images in a row, as Pi's session converts them. The setting is read for each request, and the session keeps the images. 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
resources_discover is asked after session_start, as in Pi: an extension answers with skill and prompt template files or folders of its own, and they join the session's resources after the others, as Pi's extendResourcesFromExtensions adds them. A name taken already keeps its skill or template and the newcomer is reported as a collision. Resources the host replaces keep what was discovered. Pi's theme paths have no counterpart here. user_bash is asked about a shell command the user runs, before it runs: AgentSession::user_bash returns the first answer, operations to run it with or the extension's own result, as Pi's emitUserBash does for its hosts. A failing handler is reported and its error returned, so the host does not run the command. 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
- parse_skill_block reads back the block an expanded /skill: command puts in a user message, as Pi's parseSkillBlock: the skill's name, location and text, and what the user wrote after it. - AgentSession::export_to_jsonl writes the current branch to a session file of its own, as Pi's exportToJsonl: a header with the session's id and folder, then the branch's entries, each the parent of the next. Without a path it is session-<time>.jsonl. A relative path is relative to the session's folder, where Pi takes its process's; a library's host decides its process's folder for other reasons. - An extension command can offer completions for its arguments, as Pi's getArgumentCompletions, through register_command_completions; a host asks with command_argument_completions. - ExtensionContext::register_provider and unregister_provider take effect at once, as Pi's registerProvider and unregisterProvider do once extensions are loaded. 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
ExtensionUi gains the parts of Pi's ExtensionUIContext that are not its terminal's: - notify takes a level: info, warning or error; - confirm, select and input take Pi's dialog options: a cancellation and a timeout, after which the host closes the dialog; - editor, a multi-line editor with prefilled text; - set_status, set_working_message, set_widget (lines above or below the editor) and set_title; - paste_to_editor, set_editor_text and editor_text, for the prompt being written. Each does nothing by default, so a host implements what it shows. Left out with Pi's terminal interface: raw terminal input, the working indicator's frames, custom components, footers and headers, editor components, autocomplete providers and themes. 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
When the host names a settings file (AgentSessionOptions:: settings_file), what the session's setters change is saved to it as Pi's settings manager saves the user's settings: turning automatic compaction or retries on or off, the steering and follow-up modes, and, through set_thinking_level_as_default (Pi's setThinkingLevel with persist), the default thinking level. Only the changed settings are written; the rest of the file stays, and it is written indented by two spaces, as Pi writes it. A file that does not parse is left alone, as Pi leaves it, and a failed save is reported as a persistence error. The settings gain Pi's defaultProvider and defaultModel, and set_model_as_default (Pi's setModel with persist) switches the model and saves both in one write. Pi always saves, since its settings always come from files; without a file here, changes stay in the session, as before. Pi locks the file across processes while it writes; this writes it through a temporary file instead. 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: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d
A fork's header named the session it came from by id. Pi's createBranchedSession names the parent's session file, and nothing when the parent was kept in memory; forks now do the same, so a session file reads as Pi's does. The labels a fork carries over also keep the time they were set, as Pi keeps it (labelTimestampsById); they were stamped with the time of the fork. Drift from Pi, found while porting Pi's session runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d
create_agent_session sets a session up as Pi's createAgentSession
does. Without a session, it starts a new one kept in Pi's folder for
the project (default_session_dir: sessions/--<cwd>-- in the agent's
folder, the file named <time>_<id>.jsonl). Without settings, it loads
the user's settings.json from the agent's folder, overlaid with the
project's when the project is trusted, and saves what the session
changes back to the user's file. Without resources, it loads them for
the working directory.
The model is chosen in Pi's order: the one asked for; else, for a
session with a conversation, the one it recorded while its provider
has credentials ("Could not restore model p/id. Using q/id"
otherwise); else find_initial_model, the settings' defaultProvider and
defaultModel while that provider has credentials, or else the first
model whose provider has. The thinking level follows Pi too: the one
asked for, else the recorded one, else the model's own level from the
settings, else the default, clamped to the model. The result carries
Pi's modelFallbackMessage and the settings files that did not load.
The model is chosen after extensions register their providers, so a
session restores a model an extension provides; Pi looks it up first
and misses it, which is one of the Pi bugs this PR lists as fixed.
AgentSession::new keeps its own rule (the recorded model, or else the
host's). Sessions also take the start event extensions hear
(session_start, Pi's sessionStartEvent), with Pi's reasons: startup,
reload, new, resume and fork. A reload discovers resources again, as
in Pi.
The supporting pieces: SessionManager::create_in and open_file (Pi's
create and open, including cwdOverride), ApiKeySource and
ModelRegistry::has_configured_auth, and available_models.
Departures from Pi: the host names the agent's folder and the
project folder's name (Pi defaults to ~/.pi/agent and .pi); the
project is trusted only when the host says so, where Pi's SDK trusts
it unless told otherwise; models come from the host's registry, so
Pi's preferred model per known provider is not ported and the first
available one is taken; "No models available." has no login help,
which is the host's to give; and tools are chosen as
AgentSessionOptions takes them, since Pi's tool lists and defaultTools
are not ported.
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: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d
AgentSessionRuntime holds the session a host is on and replaces it as Pi's runtime does: new_session (kept next to the current session's file, or in memory with it, with Pi's parentSession, setup and withSession), switch_session (a stored file, with cwdOverride), fork (before a user message, giving its text back, or at an entry, into a new file next to the current one) and import_from_jsonl (copied into the session's folder unless it is there, a name taken getting -1, -2 and so on). dispose shuts the session down. create_agent_session_runtime builds the first session with the host's RuntimeFactory, Pi's CreateAgentSessionRuntimeFactory, which builds each later one. Each replacement goes as Pi's: extensions can cancel it with the new session_before_switch event (new or resume) or session_before_fork; the session left is stopped, so its last turn is recorded, and hears session_shutdown, which now carries Pi's reason (quit, reload, new, resume or fork) and the file that replaces it; the next session hears session_start with its reason and the file before. A stored session whose folder is gone is refused with MissingSessionCwd, as Pi's MissingSessionCwdError, so the host can continue elsewhere; a missing import is ImportFileNotFound. set_rebind_session hands the host each new session. Commands get Pi's newSession and switchSession, and fork as Pi's takes its options: a session the runtime owns is replaced through it. Without a runtime, newSession and switchSession do nothing, as in Pi, and fork goes on in the same session as before, now kept in a new file next to the old one when there is one. Departures from Pi: a cancelled replacement fails with Cancelled, as tree moves do here, where Pi returns cancelled; one started while another is under way fails as busy; a replaced session's commands no longer act on the runtime, where Pi's stale contexts throw, and Pi's setBeforeSessionInvalidate is not ported; a fork copies the tree in memory instead of reading the file again, which gives the same session; extensions hear session_start before the runtime takes the session, so a session_start handler cannot replace it; setup is not async; the factory reports the settings errors of the sessions it builds, where Pi's runtime keeps their diagnostics; and project trust is the host's, so Pi's projectTrustContext is not taken. 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: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d
Extensions can add session entries where a run pauses, as Pi's boundary drafts let them: at turn_end, once a turn's reply and tool results are in the session, and at the new agent_before_settle, once a run's retries and compactions are done and before it settles. A handler answers with drafts (extension state, an extension message, a context edit, or a compaction of its own, which without a first kept entry keeps nothing before it) and whether one more request should follow even when nothing else would make one. Each handler sees what the ones before it asked for and a preview of the context it would make (Pi's BoundaryContextPreview). Drafts that would not apply, such as an edit of an entry the branch does not hold, are reported as "Invalid boundary entries" and none are added; a request with nothing to answer is refused and reported, in Pi's words. turn_end carries the turn's index in the run, its reply and tool results and their entry ids, and how the turn ended; drafts added there are in the next request. Entries added before settling are in the session, and a run asked to go on continues from it, as in Pi. Departures from Pi: turn_end is an event of its own here, while the agent's TurnEnd still reaches extensions with the other agent events; the entry ids are found by the message in the branch, where Pi keeps a map from each message; and `continue` is continue_run, as continue is a Rust keyword. A Pi feature phase 1 had not ported; it was listed under not implementing for turn_end and to decide for agent_before_settle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d
AnthonyRonning
force-pushed
the
agent-pi/phase-0
branch
from
October 11, 2026 08:48
afddf99 to
5782e61
Compare
AnthonyRonning
force-pushed
the
claude/kind-turing-gzv8nl
branch
from
October 11, 2026 08:48
2a33432 to
76309fc
Compare
AnthonyRonning
marked this pull request as ready for review
October 13, 2026 00:02
AnthonyRonning
removed this pull request from stack #1125
October 13, 2026 00:04
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.
Stacked on #1124 (phase 0; base
agent-pi/phase-0). Do not merge: nothing in the stack merges until the end result is accepted.What this is
A Rust rebuild of the Pi agent harness as three crates under
apps/maple-agent/crates/. They keep Pi's structure and design, written as idiomatic Rust. This is not a transliteration. There is no byte-for-byte conformance with Pi's TypeScript, and Pi's TypeScript tests were not translated. The tests are Rust unit and integration tests of the same kinds Pi has, run against a scripted provider.The crates have no Maple dependencies. Maple's runtime moves onto them in the next stacked PR, #1109.
Size: about 22,800 lines of code and 14,600 lines of tests (405 tests).
The crates
pi-ai(after Pi'spi-ai)DoneorError, so request failures are messages, not errors.FauxProviderfor tests.max_tokensto the context window;x-should-retrysays, after the waitretry-after-msorretry-afterasks for or a growing backoff. A server asking for longer than the limit fails the request at once.classify): choice, score and yes/no questions about a JSON state and images, answered with probabilities, through a registry by API.system_onespeaks Maple's System One protocol (POST /systemone), sent as Pi's classifier requests are, with a timeout per attempt and two retries. A scriptedFauxClassifieris for tests.apply_eventfollows a tool call's arguments as they stream.pi-agent-core(after Pi'spi-agent-core)all,one-at-a-time);terminate.AgentHooks: message conversion, context transforms, per-request keys, tool gates (a failing gate blocks the call), result rewrites, message finalization.AgentMessage).Agentcovers prompt (with images, as Pi'sprompt(text, images)), steer, follow-up, continue, abort and reset, with awaited listeners, and gives the running turn's cancellation, as Pi'ssignaldoes. Each queue can be cleared alone. A run whose loop fails still ends as runs end, with an empty reply carrying the error.pi-coding-agent(after the core of Pi's coding agent)SessionStore. JSONL here; an encrypted SQLite store can replace it later.newSessiontakes one.sessions/--<cwd>--/<time>_<id>.jsonlin the agent's folder), and a fork names its parent's file. The current branch can be exported as a session file of its own, as Pi'sexportToJsonl.AGENTS.mdcontext files, skills (also from.agents/skills, in the project up to its repository root and in the home folder) and prompt templates; a project's skills and templates override the user's. A template path can name one file, and template folders are searched as Pi's discovery searches them. The skills and templates the settings list resolve against their own file's folder and win over the default folders, as in Pi.SYSTEM.mdreplaces the default prompt andAPPEND_SYSTEM.mdadds to it. Skill folders follow their ignore files, and in a linked worktree inside the main checkout, the worktree's own context file replaces the main checkout's, as in Pi. The project folder name comes from the host (.maple).modelThinkingLevels) win over the default level. Pi's per-model compaction settings,images.blockImages, the default model, and the provider's timeout and retries (retry.provider,httpIdleTimeoutMs) are read as Pi reads them. What a session changes is saved to a settings file the host names, as Pi saves the user's.trust.json. Context files always load.contexthides system messages and keeps the prompt, as Pi's does;context_with_systemsees the whole transcript.session_before_fork,session_before_switch,session_info_changed,session_compact_failed,agent_settled,resources_discover,user_bashand the provider's request, header, response and stream events, and compaction and tree events carry Pi's payloads (the reason, whether the reply is asked again, the branch, the tree preparation and the extension's overrides). Session start and shutdown say why, as Pi's do.turn_endand the newagent_before_settle, handlers can add entries (Pi's boundary drafts: extension state and messages, context edits, compactions) and ask for one more request, with a preview of the context they would make.core/tools): a session creates them for its folder and gives the modelread,bash,editandwrite. As in Pi,grep,findandlsare registered but off, andpowershellstays registered for Windows. Hosts configure them, choose the active ones, and replace one by registering a tool with its name.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 found when the typed name is not.bashandpowershell: 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 or a timeout ends the command's whole process group (its process tree on Windows). A background job that keeps printing after the shell exits keeps the call reading, as in Pi, until Stop or the timeout ends the call, and on Unix the group with it. A finished command may leave a background job running. A spawn hook adjusts the command, folder and environment.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). Line endings, the byte order mark and untouched lines are kept. The result carries a display diff and a unified patch.write: creates folders as needed. Edits and writes to one file run one at a time.grep,findandls: by the rules Pi's ripgrep and fd follow. Hidden files are included;.gitignore,.ignoreand each tool's own ignore file are respected;.gitis skipped.greptakes Rust regular expressions (ripgrep's syntax) or literal text, a glob filter and context lines. It stops at 100 matches or 50KB and cuts lines at 500 characters.findmatches names, or paths when the pattern has a/. A pattern without capitals ignores case, as in fd.lssorts entries regardless of case and marks folders with/.truncate_middleandwrite_output_filecut a long result in the middle and save the whole of it to a private file, for a host's own tools, such as MCP ones.AgentSession:before_agent_starthandler returns hold for that run;_checkCompactiondecides it: a retryable error is retried first; an overflow, or a reply cut short below the model's output limit, is compacted and asked again once; a reply over the window is compacted and kept; a context near its limit is compacted, mid-run for long tool loops too; and the same check runs before a prompt, so a stopped run or a switch to a smaller model compacts first;!command, and!!commandkept out of the context) join the conversation, after the run when one is going; a colour code split between output chunks is held back;all_toolsandcommandsdescribe the tools and slash commands as Pi'sgetAllToolsandgetCommandsdo;reloaddoes with what its resource loader finds; the next prompt sends only the prompt sections that changed;registerToolcan be called while Pi runs;sendCustomMessagedoes withtriggerTurn;create_agent_sessionsets a session up in one call, as Pi'screateAgentSession: settings and resources from the agent's folder and the trusted project, a new session file unless the host opens one, and the model and thinking level chosen in Pi's order, with Pi's message when a recorded model cannot be restored.AgentSessionRuntimeholds the session a host is on and replaces it as Pi's runtime does: a new session, a stored one, a fork, or an imported file, each with Pi's events.Where it departs from Pi, and why
Decided with the repository owner: each adaptation that Pi's TypeScript does not have is to be reviewed before the stack merges; until then they stay.
apply_event, which also follows a tool call's arguments as they stream.ApiKeySource.MAPLE_SESSION_ID,maple-bash-….log) instead ofPI_*andpi-bash-…, since nothing model-facing says "pi".imagecrate instead of Photon (WebAssembly), with Pi's limits; diffs come fromsimilarinstead of jsdiff, so hunk headers can differ slightly.grepandfindsearch natively with ripgrep's own libraries (ignore,globset,regex) instead of runningrgandfd, which Pi downloads when they are missing. Results come in path order; ripgrep and fd search in parallel, in no fixed order.grepapplies.gitignoreoutside a repository too, as Pi hasfddo (Pi's ripgrep applies it only inside one). It skips.git, and passes over unreadable files instead of failing the whole search.findalways leaves outnode_modules, as Pi asks of custom operations.structuredContentfor programmatic callers; that belongs with codemode (below).reloadreads settings, resources and extensions again itself. Here the host finds the resources and hands them toset_resources; settings and extensions are not reloaded.create_agent_sessionnames the user's); Pi always saves, as its settings always come from files. Pi locks the file across processes while it writes; this writes through a temporary file instead.AgentSessionwith the same extensions, so they getSessionStart(fork) with noSessionShutdownbefore it; commands' new and switch do nothing then, as in Pi. The runtime replaces the session as Pi's does.turn_endis an event of its own here, while the agent'sTurnEndstill reaches extensions with the other agent events; a boundary handler'scontinueiscontinue_run, ascontinueis a Rust keyword; the turn's entry ids are found by its messages in the branch, where Pi keeps a map from each message.ExtensionContext::compactreturns a handle to await instead of takingonCompleteandonErrorcallbacks.is_project_trustedis what the host states inAgentSessionOptions.ModelSelecthas no source, since only the host sets models here; Pi also reports cycling and restoring.set_session_nameawaits the extensions' handlers, where Pi fires them off.Cancelled. Pi returnscancelled, withabortedfor a stop.cacheRetention(not ported).~/.pi/agentand.pi. The project is trusted only when the host says so; Pi's SDK trusts it unless told otherwise.AgentSessionOptionstakes them, since Pi's tool lists anddefaultToolsare not ported.Cancelled, as tree moves do here, where Pi returnscancelled; one started while another is under way fails as busy. A replaced session's commands no longer act on the runtime, where Pi's stale contexts throw.session_startbefore the runtime takes the session, so asession_starthandler cannot replace it. A new session'ssetupis not async. The factory reports the settings errors of the sessions it builds, where Pi's runtime keeps their diagnostics. Pi'sprojectTrustContextandsetBeforeSessionInvalidateare not taken.500in the text, so now that whole error bodies are in messages it would retry a 400 that mentions "15000 tokens". Rust's transport texts ("error sending request", "broken pipe") and a few looser phrases ("high demand", "at capacity") are retried too.supportsFinishReason: falseis not ported: every stream that ends without a finish reason fails, as with Pi's default.metadata.rawthat is not text is not added.reasoningis absent when a server does not report it; Pi reports 0.Value.Convertpass, and reads tuples only from list-formitems, as Pi's own coercion does.env,metadataand websocket request options are left out, as they serve providers and transports these crates do not have. Pi also gives its HTTP clienthttpIdleTimeoutMsas the time a response may stall; here that is the transport's.FauxClassifierhas no counterpart in Pi.!,+and-override entries and globs in the settings'skillsandpromptsare not ported; they belong to its package system.ignorepackage and the Rustignorecrate.branchSummarysettings are not implemented. Prompt-state messages on the branch left do not count against the budget; Pi counts them, though it never writes them out.fromExtension; Pi's storefromHook.Not included yet
Decided with the repository owner:
project_trustextension event (only the host and the user decide trust); grammar-constrained tools (OpenAI Lark and regex tools, which only Pi's codemode uses); session listing (hosts keep their own index, as Maple does); and Pi's smaller settings (branchSummary,enableSkillCommands,defaultToolsandthinkingBudgets).rmcpcrate, with only Pi's thin conventions on top (mcp__<server>__<tool>names, an output limit, server instructions in the prompt). Pi's own MCP client, exposure modes, resource tools andtool_searchcan follow later if wanted. Where it lands, its differences from Pi's MCP get written down for later comparison.executeTool, the cache warmer, the event bus andpi.exec) is left out with it.rg/fddownloader; reading Pi CLI session files (format migrations);streamProxy,replayand the deferred-handle helpers.reasoning_detailsandcache_control, andcacheRetention; provider detection from the name or URL; cost tiers, input limits, the response's model and durations; image generation; and the TypeSafe, OpenAI Decisions and llama.cpp classifiers.AgentSession::newfor now.Reviews
After the PR opened, four independent reviewers checked the crates for real defects, and an outside review of the first round of fixes found two more. I verified every finding against the code. Each was fixed with a regression test, except the behaviors Pi shares on purpose (listed below). The fixes are in commits 4 to 7. Commit 8 fixes a gap found while planning phase 2: the provider did not send a model its earlier reasoning, which Pi does. Commits 9 to 16 add what the first version left out of Pi's built-ins: its tools, the quota rule, project trust with
SYSTEM.md,APPEND_SYSTEM.mdand.agentsskills, strict tool schemas, user shell commands, image fitting for every tool result, and session stats. Commit 17 fixes drift that a security review of the stack found: once the shell had exited, Stop and the timeout no longer reached a command whose background job kept printing, as Pi's do. Commits 18 to 23 are Pi features phase 1 had not ported: a transport's cancellation and a host-chosen session id, resources replaced between prompts, images in steering and follow-ups,truncate_middleandwrite_output_file, tools registered after the session starts, and a custom message that starts a turn. They were first written in #1109, where Maple needed them, and moved here on 2026-10-10 so that Pi's features live in this layer.Commits 24 to 60 come from a parity review on 2026-10-10. Two read-only reviewers compared Pi 1.1.0 with these crates, for behavior that differs without being recorded here and for features of Pi's ported packages that the crates still lacked. The repository owner asked for the differences to be matched or recorded and for the missing features marked for porting to be ported; what it left to decide was settled on 2026-10-11 (below and under "Not porting" above). Every claim was checked against Pi's code before anything changed, and each commit message says whether it fixes drift (Pi already behaved so) or adds a Pi feature. Commits 24 to 35 are
pi-ai; 36 to 46 are compaction, settings and resources, with 42 forpi-agent-core's queue modes; 47 to 60 are the session and the extension API. Commits 61 and 62 follow the owner's answers on two questions the review left: the skill and template paths the settings list now resolve and rank as Pi's, and Pi's levels for particular models are ported, as the place for Maple's own recommendations per model.Commits 63 to 77 port what the review left to decide, as the repository owner asked on 2026-10-11, except what they chose not to port (above): 63 for
pi-agent-core(a failed run still ends; prompts with images; clearing one queue); 64 and 66 forpi-ai(request options and the retries before a response; classifiers and System One, after Maple's API); 65 and 67 to 73 for the session (Pi's provider settings on its requests, ignore files in skill folders, worktree context files, per-model compaction and blocked images, theresources_discoveranduser_bashevents, Pi's small helpers, the rest of the extension interface, and saving settings); 75 and 76 for Pi's one-call setup and session runtime; and 77 for the boundary drafts. Commit 74 fixes drift found while porting the runtime: a fork named its parent by id and re-stamped its labels.Compared with Pi
Every finding was also checked against Pi 1.0.4, and the shared ones again against Pi 1.1.0 (
earendil-works/piat6fb2e78, 2026-10-08). Paths and line numbers below are in that repository at 1.1.0. Issue states are from Pi's public tracker on the same day.Nothing here is being reported upstream for now. Once the Rust version works end to end, we decide which of the shared behaviors and quirks to change.
Bugs Pi has too (fixed here)
These look like real bugs in Pi.
/too many tokens/i(packages/ai/src/utils/overflow.ts:61), and the throttling exclusions only match Bedrock's message prefix (:76-80).packages/coding-agent/src/core/agent-session.ts:3711).packages/coding-agent/src/core/compaction/compaction.ts:858-864).getMessageFromEntryhas no case for context edits (packages/coding-agent/src/core/compaction/branch-summarization.ts:156).sendCustomMessagecalls_runAgentPromptdirectly (packages/coding-agent/src/core/agent-session.ts:2314-2319).sendCustomMessage({ triggerTurn: true })sends the first request without the system prompt earendil-works/pi#10639.packages/coding-agent/src/core/session-manager.ts:1191-1195)..mdskills without anameare named after their folder, so two of them in one skills folder collide and one is dropped.packages/coding-agent/src/core/skills.ts:311,321. Pi does report the clash.createAgentSession()directly.packages/coding-agent/src/core/sdk.ts:219-230) before the runner registers extension providers.AgentSession::newand increate_agent_session.Shared on purpose (kept as Pi does)
packages/agent/src/agent-loop.ts:575,641;packages/ai/src/api/transform-messages.ts:175).before_agent_startand the next-turn messages, as Pi documents. Ours does bring the prompt up to date first. Related open issue: Prompt text contributed in before_agent_start is dropped on runs without a user prompt, re-billing the whole prompt earendil-works/pi#10267.clear_queuedrops queued extension messages without returning them, as Pi does. Open upstream: clearQueue() silently destroys extension-delivered custom messages without returning them。 earendil-works/pi#9886.packages/ai/src/utils/validation.ts:82). Ours keeps every digit.Small Pi quirks noticed (not filed)
auto_retry_endwithsuccess: true(packages/coding-agent/src/core/agent-session.ts:1176-1178).packages/coding-agent/src/core/compaction/branch-summarization.ts:318).packages/ai/src/api/openai-completions.ts:500-504).EEXIST(packages/coding-agent/src/core/session-manager.ts:1177). Compare the closed SessionManager._persist() throws EEXIST when file is pre-written by _rewriteFile(), masking real agent errors earendil-works/pi#5844.generateBranchSummary, which never reads it, so branch summaries are written without thinking. Ours sends none either.Our own drift (fixed here; Pi already gets these right)
continuedrained the queues before it refused a busy agent.[DONE]without a finish reason, and unknown finish reasons, finished the reply, so its tool calls could run; errors kept onlyerror.message, so an overflow that OpenRouter wraps was retried instead of compacted; every model got the developer role, a model without image input was told "(see attached image)", and reply blocks came back joined with newlines; looser overflow texts and exclusions retried a LiteLLM overflow inside a 503; estimates used 4 characters per token; DeepSeek, Kimi, OpenRouter and Moonshot cache usage was not read; raw control characters and stray backslashes turned arguments into{}; coercion did not go through unions; malformed and multi-line events were skipped, anderrorevents ignored;before_agent_startoptions outlived their run; thecontextevent saw system messages; manual compaction during a run was refused; steering skipped input handlers and expansion; prompt images went out unprocessed; a tree move left a user message as the leaf and kept the current tools; a reply from another model, or from before the latest compaction, could trigger compaction, and the threshold was not checked after a failed reply; the context usage after a compaction was an estimate; summary requests retried when retries were off, with delays capped at a fixed minute;one_at_a_time; the skill and template paths the settings list resolved against the working folder, lost name clashes to the default folders, and a project's list replaced the user's.Rust only
Dropped futures (a timed-out prompt or compaction, a dropped tool call) and races between threads. JavaScript cannot have either.
Testing
cargo fmt --all -- --check: passes.cargo clippy -p pi-ai -p pi-agent-core -p pi-coding-agent --all-targets -- -D warnings: clean.pi-aiis also clippy-clean with--no-default-features.cargo test -p pi-ai -p pi-agent-core -p pi-coding-agent: 405 tests pass. The built-in tools' tests follow Pi's tool tests (paging, images, exact and loose edits, line endings, truncation and full-output files, timeouts, exit codes and signals, flag-like search patterns, glob errors, hidden and ignored files). They add what stopping or dropping a command reaches (also once the shell has exited while a background job keeps printing), the ripgrep and fd rules above (nested repositories, globs, case, limits), and sessions that runbashandwriteend to end. The newer built-ins have their own: strict schema conversion and refusal, quota errors, the trust store and its resolution order, what an untrusted project keeps, user commands (cleaned output,!!, abort, a command ending mid-run), oversized tool images, and stats. The features moved from Maple Agent on the Pi crates: the runtime, then every feature, then no Goose #1109 have theirs: the middle cut and output files, replaced resources, images in steering, tools registered later, and a custom message's turn.x-should-retry,retry-after-ms,retry-afterseconds and dates, a refused long wait, a stop during the wait, timeouts and the response hooks; the provider settings and their old names; classifier questions on the wire, answers, usage, retries, timeouts and errors, and refused images; ignore files in skill folders; worktree context files; per-model compaction and blocked images; both new events; skill blocks, exports and completions; dialogs and the editor; saved settings; fork parents and label times; session files, model and thinking choice, credentials, trust and settings errors in the one-call setup; new, stored, forked and imported sessions, cancellation, commands, missing folders and stale sessions in the runtime; and drafts, previews, refused continuations and invalid entries at both boundaries.pi-aiturns on serde_json'spreserve_order, so error objects keep the server's key order as Pi'sJSON.stringifydoes; the full workspace already had it on.Cargo.lockgains the three path packages andsimilar. Every other external crate they use (ignore,globsetandimageamong them) was already locked, at the same versions; a locked resolution of the full workspace, GUI app included, accepts the lockfile unchanged. The commits from 24 on change no dependencies.🤖 Generated with Claude Code
https://claude.ai/code/session_01STEaSNTNUiiNNLXcKidE2d