Built-in tools & auto-guards
This page covers mono-agent’s managed built-ins (Read, Write, Edit, Glob, Grep, Bash, Exec, NodeRepl, WebFetch, WebSearch, Agent) and the runtime guards that protect each turn: loss-aware process execution, the tool-output bloat guard, per-run usage/cost tracking, bridge-driven Pi context compaction, and WebFetch’s in-tool retry. It also notes which behaviors you configure versus which run automatically.
Configured local peers (PeerAgent)
Section titled “Configured local peers (PeerAgent)”PeerAgent calls another running local mono-agent by a named ACP thread. It is
not an Agent profile: the peer keeps its own tools, model, workspace and
credentials. Configure peers.finance.sourceId and allow PeerAgent in
tools.allowedTools; only running compatible peers appear in its description.
send waits for a bounded untrusted answer, or background:true returns a
started process-job receipt when an exact-origin wake is available. stop
requests ACP cancellation. ACP-served turns can only send in the foreground.
Named sessions survive restarts without replaying interrupted prompts; a lost
session starts fresh only on the next explicit send. Signed generations reject
replays and the source chain rejects cycles before a nested foreground call can
deadlock its origin. A peer’s AskUser now returns an untrusted bounded ACP
form to this caller: answer with its current questionId and form field IDs,
or decline; answer from your own evidence or ask your own user. The bridge
rejects invalid/sensitive forms, and a peer request is not owner approval. See
ACP bridge for the handoff and trust boundary.
Subagents (Agent)
Section titled “Subagents (Agent)”Agent lets the main agent hand a self-contained task to a helper that works
independently and reports back. It exists only on the pi runtime, and only when
subagents.enabled is true and Agent appears in tools.allowedTools —
mono-agent validate warns when one half is configured without the other.
{ "tools": { "allowedTools": ["Read", "Glob", "Grep", "Agent", "AgentManage"] }, "subagents": { "enabled": true, "maxConcurrent": 5, "models": [{ "name": "fable", "model": "anthropic:claude-fable-5-1" }, "openai-codex:gpt-6-astra"], "definitions": [ { "name": "researcher", "description": "Reads code and docs to answer a factual question. Read-only.", "prompt": "You are a codebase researcher. Answer with file:line citations. Never modify files.", "allowedTools": ["Read", "Glob", "Grep", "WebFetch"], "maxTurns": 25 } ] }}Each definition needs exactly one of prompt or promptPath. The Agent tool
takes {prompt, name?, description?, model?, effort?}; with no name it runs a
read-only general-purpose researcher. model is offered only when the operator
configures a non-empty subagents.models allow-list. Entries can be full model
reference strings or {name?, model} objects. An omitted name uses the canonical
reference; explicit aliases must be lowercase kebab-case, 1–40 characters, unique,
and distinct from profile names and general-purpose. Duplicate model references
are rejected. Unknown call-time choices fail with the allowed names.
Model and effort each resolve independently: call-time override → profile pin →
parent effective value → base/runtime default. Inheritance follows the parent’s
current turn, including per-conversation model choices and effort overrides.
Both options work with configured profiles, authored helpers, and general-purpose,
even when authoring is disabled. The parent’s fallback chain does not implicitly
add choices to subagents.models.
For example, Agent({name: "researcher", prompt: "Find the relevant callers", model: "fable", effort: "high"}) keeps the researcher’s prompt and tool policy
while changing its route. Pinned or overridden values appear in the result header
and structured requested details; executed details reflect the successful route
when the child reports it, including fallback differences.
Subagents built at call time. A pre-declared profile means editing config and
restarting for every new specialization, so the agent can also author one on the
spot: passing systemPrompt (plus a kebab-case name, and optionally tools,
model, and effort) builds a one-off subagent for that call instead of selecting a
profile. Only tools requires systemPrompt; configured profiles retain their
operator-defined tool policy. A name that collides with a configured profile is
rejected so the activity log stays unambiguous.
What an authored subagent may reach is an operator decision, not the model’s. Requested tools are intersected with a ceiling, and anything dropped is reported back in the result so the model stops asking for it:
{ "subagents": { "enabled": true, "inline": { "allowedTools": ["Read", "Glob", "Grep", "Edit", "Bash"] } }}The same ceiling clamps the built-in general-purpose profile to the intersection
of its read-only defaults; configured profiles retain their explicit contracts,
and a ceiling with no read-only tools rejects general-purpose instead of widening
it. Omitting inline.allowedTools caps authored subagents and general-purpose at
the parent agent’s own built-ins (its tools.allowedTools minus
tools.disallowedTools), so a helper never reaches further than the agent that
built it. Omitting tools on the call gives a read-only helper. Set
inline.enabled to false to allow only pre-declared profiles; the Agent
tool then takes exactly {prompt, name?, description?} with name restricted
to the configured profiles.
What comes back. The main agent receives the subagent’s final answer plus a
compact log — one line per tool call with a short argument summary, ok/error,
and duration — capped at roughly 24 KB. It does not receive raw tool output. A
subagent that fails, times out, or returns nothing still reports its activity
log, since that log is usually the most useful part of a failed delegation.
When the answer exceeds 12,000 characters, the log is elided, or the result hits
its byte cap, the complete result is written to the run’s tool-output artifact
directory through the same sink other oversized tool payloads use, and the
retained text names that file directly under its header
([result truncated; full result saved to: …]) so the main agent can Read it.
Without a configured artifact sink the text says the full result was not saved.
What operators see. Foreground subagent tool calls stream live to the web console
console as its own entry, named <profile>▸<tool> and bracketed by the
subagent’s own start/finish rows. The subagent’s thinking and prose stay
internal — only its final answer reaches the parent, through the tool result.
Detached persistent children instead publish bounded, redacted tool progress and
a terminal report in the web Background jobs card, with an agent icon and a
scrollable body; the parent Activity keeps the launch and terminal job rows.
Limits. maxConcurrent (default 5) is an upper bound on simultaneous
subagents; the provider may schedule fewer. Agent calls can overlap even when
stateful tools are offered, but an invoked stateful, MCP, or unknown tool is
an exclusive barrier: earlier calls settle first and later calls wait.
Overlapping children share the workspace, so give parallel children disjoint file
ownership rather than allowing competing writes to the same paths. maxPerTurn (default 20) bounds the total per parent turn and is the real
runaway guard, since a delegation loop can spend budget serially without ever
hitting the concurrency cap. Each subagent gets maxTurns (default 100) and
timeoutMs (default 5 minutes), and its timeout starts only once it actually
begins, not while queued.
All children get a foreground Bash/Exec timeout_ms ceiling bounded by
subagents.commandTimeoutMs (positive integer milliseconds, default 30 minutes).
Detached persistent children (Agent / AgentManage with background: true)
clamp it to their process job’s remaining runtime at child-run setup. Foreground
children clamp it to their remaining turn time minus a settlement reserve (10%
for short turns, at most 15 seconds). Tool descriptions and the child envelope
show the effective ceiling; the child timer and parent abort can stop commands
sooner, as can the detached job deadline. Interactive parent turns and NodeRepl
keep their 120-second caps. Child-owned background commands remain unsupported.
Provider outages. subagents.providerRetryBudgetMs (integer 0–180000 ms,
default 120000) gives each child assistant request a shared pre-/post-generation
outage window on Anthropic, OpenAI Responses and Codex. Zero restores legacy;
inherited providers.piNative.piMaxRetries: 0 disables retries entirely. Parent
turns, other adapters and Bedrock’s AWS policy stay unchanged. Throttling keeps
short header-aware handling, cancellation interrupts sleeps, and no tool is
replayed. The window is not a hard request timeout; unknown-outcome resends can
duplicate billing. Exhaustion retains bounded request-attributed phase/count/
time/cause diagnostics. See provider retry timing and ownership.
Guardrails. A subagent is read-only unless its profile enumerates more (or,
for one built at call time, unless its tools request survives the ceiling), and
it never receives Agent, AgentManage, AskUser, or any channel-send tool — it cannot
message the user or spawn subagents of its own. It inherits the parent’s sandbox
and cannot widen it, gets no MCP servers unless its profile names them, and runs
with no provider session of its own. Without a call-time override or profile pin,
it inherits the parent’s effective model and effort. A child using the base model
uses the shared fallback/retry runtime; a different resolved model goes through
the host’s runtimeForModel callback so the shared router cannot replace it with
the base model.
Skills. A subagent inherits the parent’s skill index and the ReadSkill tool
whenever the agent runs with context.skillDisclosure: "index" and a
context.skillsRoot — the index is appended to the profile prompt, and the child
pulls any body it needs on demand. It never receives inlined skill bodies:
selectedSkills and skillMaxBytes are full-disclosure concepts and do not carry
over. Under full disclosure a child gets no index, matching its parent. Opt a
profile out with "disallowedTools": ["ReadSkill"], which withholds both the tool
and the index. A profile pinned to a model whose runtime lacks skill support
is skipped automatically rather than failing the run, since a
non-empty skill list makes skill support a routing requirement.
Persistent subagents
Section titled “Persistent subagents”With subagents enabled, persistence is available when effective tool policy
allows both Agent and AgentManage (explicitly or through "*", with denies
applied). When only Agent is allowed, it keeps its stateless schema and the
Session envelope omits instance guidance. Agent({name: "researcher", prompt: "Review the design", persist: true, id: "reviewer"}) creates an instance and runs its first turn.
AgentManage({id: "reviewer", message: "Now check this revision"}) resumes its own
engine-turn durable session. The parent transcript is never seeded into the child.
The selected model, effort, prompt, and profile are retained for that instance.
The tool copy states that a child is stateless by default and that persist
is for a child the parent will actually continue: a persistent instance holds
its transcript and one live instance slot until it is closed, and background
(which requires persist) is reserved for sustained work that outlives a reply.
Current global tool denies still apply on every turn. Only MCP server names are
retained; their configuration is resolved from the current catalog. A removed
server or unavailable retained route causes an error rather than using stale
configuration or silently changing the selected model.
AgentManage({id, message, model, effort}) runs that continuation on a
different route. The child keeps its durable session and full prior context; the
new route is persisted on the instance, so later continuations inherit it
without repeating it. model takes the same configured choices as Agent and
is offered only when the host configured any; effort takes the usual levels.
Both compose with background: true, close: true and ack, and both are
rejected — starting no turn and writing nothing — with stop, steer,
inspect and close-only calls, and for an unknown model name or effort level.
A turn that fails keeps the new route, because the next continuation should
inherit what was asked for. Any ordinary failed child turn can continue the same
instance only after native recovery certifies retained continuity, actual
ownership settles, and the parent inspects and acknowledges the evidence.
Detached recovery also requires background: true. Re-check partial effects;
there is no automatic replay. Lost/unknown continuity stays close/create only.
The ordinary-message recovery shortcut remains exclusive to certified timeouts.
Child runs forward providers.piNative.piMaxRetries, maxRetryDelayMs and
transport (as piTransport), including explicit zero, without inheriting parent
controllers or session authority. Per-call model choices remain independent.
IDs are conversation-scoped lowercase kebab-case, 1–40 characters. If omitted,
an id such as researcher-1 is generated. Results include the id, turn count,
and status; the parent’s Session envelope lists live instances on every turn.
Use AgentManage({id: "reviewer", close: true}) when done, or combine a final
message with close: true. While a detached turn runs,
AgentManage({id, steer: "<text>"}) offers text into that in-progress turn and
returns an honest receipt (applied / pending / not_applied / unsupported);
it starts no turn, and a foreground child can be reached by neither steer nor
stop, because it blocks the parent’s own turn. See
Background process jobs for the receipt
table and the in-process, restart-truthful mailbox. A combined call closes only after a successful
message; busy, cancelled, timed-out, or failed turns keep the instance live.
Close-only calls do not spend the parent call budget.
Persistent children automatically receive AskParent({question, options?}),
unless global or current profile tool policy denies AskParent. It is absent
from parent and stateless runs. The question must be non-empty and at most 2,000
characters; optional choices contain 2–5 non-empty strings of at most 200
characters, deduplicated after trimming (at least two distinct choices remain).
The tool durably saves the question before ending the child turn. It never
blocks waiting for a reply, calls the parent directly, or contacts the user.
AskUser and channel send tools remain unavailable to children.
The enclosing Agent or AgentManage result is successful with
details.subagent.status: "awaiting_reply" and structured question details.
Reply with ordinary AgentManage({id, message}) into the same child transcript.
The Session envelope also includes a bounded, quoted pending question for
restart or compaction recovery. Failed replies retain the question; a successful
answer clears it; another AskParent replaces it. Close-only retires it.
A message combined with close: true stays open if the child asks again.
Configure subagents.instances:
| Field | Default | Limits / behavior |
|---|---|---|
enabled | true when subagents are enabled | false keeps stateless Agent, without persistence parameters, and removes AgentManage. |
root | <artifacts.dir>/../subagents | Relative paths resolve like other config paths. |
maxPerConversation | 8 | 1–32 live instances. |
idleTtlMs | 86400000 (one day) | 60000–604800000; expiry is applied on registry access. Running instances do not expire. |
maxTurns | 60 | 1–500 total child turns per instance; close and create another when exhausted. This differs from the per-run subagents.maxTurns model-turn cap. |
The registry and session JSONL transcripts survive restarts. An interrupted instance
returns to awaiting_reply when it has a pending question, otherwise idle; it does not rerun automatically. Closed and expired
records remain for up to 24 hours, bounded to 64 terminal records per
conversation and a 16 MiB registry. Terminal records are pruned before a write
would exceed that byte ceiling; an oversized live registry is rejected. Their
provider sessions are retired best-effort. restart --clear-sessions removes
the configured instance root and reports its registry and session-file counts.
One instance can run only one turn at a time; concurrent continuation or close
requests fail with a busy error. Continuations share Agent’s parent-turn call,
concurrency, output, and timeout limits. If a runner ignores cancellation, its
instance stays busy until that runner actually settles. If a retry or fallback
answers outside the retained durable session, the tool reports
session_continuity_lost rather than claiming the turn was retained. Close that
instance and create another with the context it needs.
There is no cross-conversation reuse. Persistent children can run detached
with background: true through the host-owned ProcessJobs lane; see
detached persistent children. Persistence grants no additional authority. Bare runtime
hosts without a conversation registry retain stateless Agent and reject
persist/id if passed directly.
Built-in tools
Section titled “Built-in tools”These tools need no extra capability config (coverage: config — they exist by default on the supporting runtime; you gate them):
| Tool | Purpose |
|---|---|
Read | Read a file (text, images, PDFs, notebooks). |
Write | Create or overwrite a file. |
Edit | Exact-string replacement in a file. |
Glob | Match files by glob pattern. |
Grep | Search file contents. |
Bash | Run a command string through a clean non-interactive Bash. |
Exec | Run one executable directly with an argv array and no shell parsing. |
NodeRepl | Evaluate JavaScript in a run-scoped Node.js REPL. |
WebFetch | Deterministically decode and locally extract one public URL, with explicit opt-in browser rendering. |
WebSearch | Search via explicit Ollama or SearXNG, ChatGPT-subscription Codex app-server, or keyless public fallbacks. |
These are gated by tools.allowedTools / tools.disallowedTools. Deny always wins, and listing the same tool in both is rejected at validation time. Mono-agent-managed built-ins are provided by the Pi runtime’s managed tool seam on every route. See Tool Policy for the full allow/deny semantics, plus MCP tools and the sandbox for process and network confinement.
{ "tools": { "allowedTools": ["Read", "Glob", "Grep", "WebFetch", "WebSearch"], "disallowedTools": ["Bash", "Write", "Edit"] }}Configure tools.allowedTools and tools.disallowedTools as JSON arrays.
These are the normalized policy semantics. The Pi tool layer enforces them: allowedTools (with the "*" allow-all sentinel) selects the built-in tool set and skill reads, and disallowedTools is the deny-wins filter applied to that final set. MCP tools are gated by the same tool-output, sandbox, and approval machinery but are not re-filtered through the built-in allow/deny list. See Tool policy.
Exec and Bash
Section titled “Exec and Bash”Use Exec({ executable, args, workdir?, timeout_ms?, max_output_chars?, background? }) for
ordinary commands. It calls the executable directly, so every argument stays
literal: no shell expansion, redirection, command substitution, pipelines, or
quoting ambiguity. Use Bash only for commands that genuinely require shell
syntax.
Bash launches /bin/bash --noprofile --norc -c, pins BASH_ENV and ENV to
/dev/null, and removes inherited Bash functions/startup-option variables.
This avoids interactive aliases, user profiles, exported functions, and shell
startup hooks changing an agent call. Its public timeout_ms is exact
milliseconds.
The old timeout field remains temporarily compatible—values up to 600 retain
the historical seconds interpretation and larger values mean milliseconds—but
every use emits a deprecation warning.
Both tools share one loss-aware process runner. It:
- spawns a detached process group without an extra shell;
- enforces abort and timeout with
SIGTERM, thenSIGKILLafter a one-second grace period; - bounds stdout and stderr together at 8 MiB before applying the smaller model-facing output cap;
- preserves partial stdout/stderr on non-zero exit, signal, abort, timeout, and overflow;
- returns sandbox preparation and cleanup failures as structured tool errors;
- records structured status, exit code/signal, duration, byte count, timeout, and truncation metadata without copying the command or argv into timing telemetry.
background is absent from both schemas unless an enabled engine-turn host
injects an available process-job controller for the exact request. With no
controller, schemas and foreground behavior are unchanged. With one,
background: true hands the already sandbox-prepared command to the host and
returns an opaque job id without waiting for completion. The host preserves the
same command/shell semantics, owns cleanup after the inherited POSIX process
group exits, and wakes the originating Slack, Telegram, or web conversation
through a normal tool-capable turn. Commands that daemonize into another group
or session are unsupported. Usage guidance travels with the capability: when the
controller is present the schema line, the start result, and the system prompt’s
session block all tell the agent when to background a command and that it must
wait to be woken rather than poll. See Background process jobs
for configuration, limits, supported origins, recovery, and operator access.
These are macOS-facing tools. Prefer portable commands or feature-detect flags
instead of assuming GNU variants of sed, date, stat, xargs, and similar
utilities.
NodeRepl
Section titled “NodeRepl”NodeRepl({ code }) uses Node’s built-in node:repl default evaluator. Mono-agent lazily starts one child REPL for a run and reuses it for later NodeRepl calls in that run. Variables, the module cache, _, and _error therefore persist between calls; the child is destroyed when the run ends, so the next run starts clean. The evaluator supports multiline JavaScript, top-level await, Node built-ins, console output, and require() of packages already installed for the workspace.
This is code execution, with the same filesystem, process, and network authority as Exec and Bash. The child goes through the same sandbox preparation seam and configured SRT policy. With no active sandbox it runs on the host; with native SRT it receives the configured roots, deny-write rules, and network policy. A fixed 120-second evaluation timeout, abort, child exit, or hard output overflow kills the child and resets its state before a later call. Normal results use the existing tool-output cap.
The host and REPL child communicate through random-token, length-prefixed JSON frames on ordinary stdin/stdout. Console output is captured separately from protocol frames. This works through sandbox wrappers that forward standard pipes and avoids relying on Node’s special IPC file descriptor.
NodeRepl is intentionally small: it has no session ids, persistent history, reset command, terminal emulation, or package installer. Use Bash for shell commands and install dependencies before the run. REPL dot commands such as .save and .load are not a supported tool interface.
Tool-output bloat guard (auto)
Section titled “Tool-output bloat guard (auto)”Tool results are truncated at a 256KB budget so a single oversized result cannot blow up the context window or the model’s reasoning. When a text-only result exceeds the budget, the guard keeps a UTF-8-safe 60/40 head/tail sample inside a new balanced untrusted-content frame. A host notice says that the omitted middle may contain more content, so the retained tail is not mistaken for the source ending. Image, binary, and mixed payloads retain the summary-only fallback.
Before rewriting the result, the guard offers every original block to the configured app’s per-run artifact sink. Successful files land as owner-private files under artifacts.dir/tool-output/<runId>/; only returned paths appear in the summary. A missing or failed sink is reported as persistence unavailable and never fails the tool call. These files contain raw, untrusted payloads: keep the artifact directory access-controlled. The configured app’s hourly artifact sweep owns each run directory under the existing artifacts.retention age/count/dry-run policy. A directory associated with a running or uncertain summary, or modified within the last sweep interval, is kept; an aged orphan needs no tool-history record to be removed. Tool-history retention still owns only lifecycle rows and tombstones, not these files.
Images get a separate, larger budget than text so vision payloads are not clipped at the text limit.
The per-tool caps that fire well before that budget — max_output_chars on Bash, Exec, NodeRepl, Read and WebFetch, and the line/char limits on Grep and Glob listings — spill through the same per-run sink. When a result is trimmed, the full output is written to artifacts.dir/tool-output/<runId>/ and the retained text ends with Full output saved to: <path> so the agent can Read it or narrow its request. Without a sink the notice omits that line rather than naming a file that was never written.
This guard is always on (coverage: auto). You do not enable it; you only choose where artifacts are written:
{ "artifacts": { "dir": ".mono-agent/artifacts" }}Configure artifacts.dir in JSON.
Usage & cost tracking (auto)
Section titled “Usage & cost tracking (auto)”Each run collects per-turn usage, cost, and cache metrics as events for its JSONL artifact. Pi catalog estimates delegate to Pi’s native cost calculation, including request-wide pricing tiers and cache-write rates. Before persistence, non-numeric values under sensitive-looking object keys are redacted; numeric values under matched keys are retained; retained free text is scanned for a closed set of high-confidence credential shapes. The recorder applies a 4,096-byte default cap per string, writes an empty start snapshot, schedules best-effort running checkpoints after 25 new events or five seconds from the first uncheckpointed event, and queues the terminal snapshot after any scheduled checkpoint. It replaces the events and summary files separately rather than appending or fsyncing a journal, so a crash can preserve the last successful prefix while losing the unscheduled or failed-write tail. This is automatic (coverage: auto) — it rides on the same artifacts.dir and needs no separate flag. See Artifacts & traces for the complete write-boundary and stale-reconciliation contract.
Related per-turn timing also lands in the JSONL: a provider_bridge_latency event separates provider/tool/IO time from harness overhead, and per-tool tool_timing events carry execution_ms. See Artifacts & traces and the CLI reference for reading these.
Context compaction (Pi bridge-driven, configurable)
Section titled “Context compaction (Pi bridge-driven, configurable)”Pi’s native checkpoint and overflow compaction is disabled by mono-agent so only one guarded policy runs. Summary preparation preserves bounded tool-result heads and tails and distinguishes confirmed built-in file changes from failed attempts. Empty, malformed, aborted or output-truncated summaries retain the original context. Thresholds, tail size and output budgets are unchanged.
context_compaction.accounting records operation duration, policy, comparable
transcript/full-request estimates, tail estimate, summary text token estimates,
appended file-metadata bytes and omission counts. Each summary request has its own
ID, ordinal, status, duration and provider usage/cost when available, including
requests whose summaries are rejected. Unknown values are null; estimates use
tokenCountsExact: false. Accounting excludes prose, paths, arguments and raw
cache keys. The recorder preserves boolean exactness and null summary/tail
estimates through closed, typed redaction exceptions; strings under these field
names remain redacted. With providers.piNative.promptCacheDiagnostics: true, assistant
payload and usage events share request IDs; the prompt-cache summary script shows
compaction boundaries and separates assistant and summary cost. Fingerprint
changes do not prove cache misses, and delta/unsupported prefix comparisons remain
unknown. Summary requests keep Pi’s existing disabled-cache setting.
Compaction is delegated to the active provider bridge rather than hand-rolled in the runtime. On the engine-turn adapter, the bridge owns the decision in all three cases:
- Proactively — before a turn when the request reaches the proactive
trigger: near the running model’s context window, or the absolute
maxInputTokensceiling (default 200,000 tokens) on large-window models. - Mid-run — between completed model/tool rounds of a single turn, so a long tool-using run that crosses the trigger while it is still working compacts then instead of drifting until the turn ends. The check runs at Pi’s own durable checkpoints, which are only reached once a whole tool batch has finished, and the compaction is applied inside the same run: it never starts a second agent run, appends a user message, consumes queued steering input, or splits a tool call from its result. The summary itself is a separate paid provider request. Guards keep it cheap — at most one attempt in flight, at most one evaluation per completed round, a re-check of the trigger before a summary is requested, no summary at all when the retained recent messages already hold nearly all of the context, and required fresh assistant progress plus meaningful growth before any further attempt.
- Reactively — if a turn still overflows, it compacts and re-prompts once only after the rebuilt context preview proves a positive reduction. A non-reducing compaction is cancelled before persistence and is not sent back to the same model unchanged.
The window auto-tracks whichever model is actually serving the request. Numeric
provider overflow limits become learned ceilings; a generic overflow temporarily
lowers the process-local ceiling to 90% of the failed request estimate. If the
provider metadata is persistently wrong, set runtime.compaction.contextWindowOverride.
The runtime still cannot make a malformed model definition, one individually
oversized prompt, or a provider failure compactable; unrecovered overflow remains
an explicit context_limit and advances through configured fallbacks.
Every run reports context_compaction_applied:
| Value | Meaning |
|---|---|
true | Compaction fired this run (before the request or mid-run). |
false | Enabled but not needed. |
null | Compaction disabled (or the bridge does not support it). |
Pi diagnostics also report the full proactive request estimate and fixed
overhead components on every check, plus context_compaction_reactive_attempted,
context_compaction_tokens_after, and context_compaction_reduced. Mid-run
activity is reported separately as context_compaction_midrun_armed,
context_compaction_midrun_attempts and context_compaction_midrun_applied. If the
request still exceeds the primary model’s window, the run is classified as
context_limit; the fallback router may then try the next configured model.
This is automatic and configurable on the engine-turn adapter. Defaults resolve against the effective context window W: trigger ratio 0.90, safety headroom
clamp(floor(W × 0.10), 16000, 48000), absolute ceiling maxInputTokens
200000, retained context
clamp(floor(W × 0.10), 4000, 20000), summary output
clamp(floor(W × 0.04), 2000, 12000), and minimum proactive savings
clamp(floor(W × 0.10), 4000, 20000). Configure overrides in the agent’s
runtime.compaction config; there are no compaction environment variables.
See Sessions & concurrency for how sessions persist, and Fallback & failover for window changes across the fallback chain.
Absolute compaction ceiling
Section titled “Absolute compaction ceiling”The proactive trigger (before a turn and at mid-run checkpoints) is
min(floor(W × triggerRatio), W − safetyHeadroom, maxInputTokens). Windows up to
about 222,000 tokens already trigger at or below 200,000, so the default changes
nothing for them. On larger windows the context is compacted at the ceiling
rather than near 90% of the window. Without a ceiling, a multi-day conversation
on a 1M-window model grew past 500,000 tokens without compacting, and every
request carried that whole prefix. A 1M-window conversation now compacts at
about 200,000 tokens. It drains to the summary (at most summaryMaxTokens), the
retained tail (keepRecentTokens) and the fixed request overhead, using the same
guards as every other proactive compaction.
The ceiling is a soft trigger only. It never refuses a request, and it never
changes the usable window, the output reserve, contextWindowOverride or reactive
overflow handling. It is never reported as context_limit, so it cannot cause a
fallback to a larger-window model. If a compaction cannot get under the ceiling
because savings are too small, nothing can be cut or the summary fails, the turn
proceeds as before. The minimum-savings, failure-cooldown and mid-run growth
guards then limit retries. When the stable fixed overhead (system prompt, tool
schemas and current turn) plus summaryMaxTokens + keepRecentTokens + minSavingsTokens would not fit under the ceiling, the trigger for that run is
raised to that sum. It is never raised above the window-derived trigger. Without
this, every turn would arm again and pay for a summary that could not save
enough.
The trade-off is deliberate: large-window conversations compact more often. Each
compaction replaces the cached prompt prefix, so the next request rebuilds the
provider prompt cache, and the summary is a separate paid request. Request
shapes, reasoning and signed-thinking handling are unchanged. Set a larger value
(for example "maxInputTokens": 400000) for one agent to compact later, or
"maxInputTokens": 0 to turn the ceiling off and keep only the ratio and
safety-headroom trigger. Explicit values other than 0 must be 32,000–10,000,000.
Each proactive check reports context_compaction_trigger_tokens,
context_compaction_trigger_source (ratio, reserve, max_input_tokens, or
max_input_tokens_floor when the overhead floor raised the ceiling),
context_compaction_max_input_tokens (0 when disabled) and, when raised,
context_compaction_trigger_floor_tokens.
Web research and WebFetch retry
Section titled “Web research and WebFetch retry”WebSearch supports explicit Ollama Web Search, a loopback SearXNG companion, structured
ChatGPT-subscription Codex app-server search, and deterministic keyless
fallbacks. Named modes never fall back; auto tries explicitly configured
Ollama → configured SearXNG → Codex → keyless. It enforces a configurable hard
budget of four actual provider requests per logical run by default, advances
immediately past a provider cooldown, and tells the model to fetch returned
URLs instead of sleeping or retrying. WebFetch performs
local Defuddle/Readability/Turndown extraction for HTML,
plus JSON, feed, PDF, and text handling. Optional agent-browser rendering is
off by default and is a config-level capability ceiling.
WebFetch retries transient network failures and HTTP 408/425/429/5xx in-tool
with bounded backoff and Retry-After handling. This keeps the model from
burning reasoning rounds re-issuing a fetch that failed for a momentary network
reason. Explicit render: "always" is browser-first; automatic rendering is
limited to sparse successful HTML. Browser failures and access challenges are
reported honestly and never used to bypass authentication, CAPTCHA, Cloudflare,
robots/access controls, or site policy. Automatic rendering is never attempted
for HTTP failures or non-HTML content.
See Local-first web research for the backend, extraction, isolation, and config contract.
Tool scheduling (code-only)
Section titled “Tool scheduling (code-only)”Pi defaults to safe parallelism. Invoked independent read-only tools and Agent
calls may overlap even if stateful tools are merely offered. An invoked Write,
Edit, Bash, Exec, NodeRepl, Monitor, AgentManage, AskParent,
StructuredOutput, MCP, or unknown tool is exclusive: it waits for earlier
admitted calls, then blocks later calls until its result settles. A background
process job or detached Agent releases admission on its started receipt, not
when the detached work finishes; separate job and child limits still apply.
Calls waiting behind the gate already have Pi’s durable effect_pending intent
and emitted tool_execution_start: a visible “running” state and its duration
include time waiting for admission, not just execution time. If interrupted
before admission, non-replay-safe calls recover as interrupted, not re-executed. A host can also force every tool to run sequentially:
const runtimeOptions = { piToolExecutionMode: "sequential", // default: "safe-parallel"};There is no config-file or CLI key for this (coverage: code). The retired
piToolParallelismMode key is rejected. Migrate one-at-a-time to
piToolExecutionMode: "sequential" and all to "safe-parallel" (the default).
Tool scheduling is independent from Pi’s one-at-a-time user steering/follow-up
queue.
See Programmatic composition for where
runtimeOptions is supplied.
Coverage at a glance
Section titled “Coverage at a glance”| Capability | Coverage | How |
|---|---|---|
| Built-in tools | config | tools.allowedTools / tools.disallowedTools |
| Bloat guard (256KB + artifacts) | auto | Built in; artifacts to artifacts.dir |
| Usage/cost tracking | auto | Recorded in JSONL artifacts |
| Context compaction | config + provider | runtime.compaction.*; bridge-driven Pi compaction |
| Web research | config + auto | tools.web.*; extraction/retry built in |
| Tool scheduling | code | runtimeOptions.piToolExecutionMode |