Skip to content

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.

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.

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.

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:

FieldDefaultLimits / behavior
enabledtrue when subagents are enabledfalse keeps stateless Agent, without persistence parameters, and removes AgentManage.
root<artifacts.dir>/../subagentsRelative paths resolve like other config paths.
maxPerConversation81–32 live instances.
idleTtlMs86400000 (one day)60000–604800000; expiry is applied on registry access. Running instances do not expire.
maxTurns601–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.

These tools need no extra capability config (coverage: config — they exist by default on the supporting runtime; you gate them):

ToolPurpose
ReadRead a file (text, images, PDFs, notebooks).
WriteCreate or overwrite a file.
EditExact-string replacement in a file.
GlobMatch files by glob pattern.
GrepSearch file contents.
BashRun a command string through a clean non-interactive Bash.
ExecRun one executable directly with an argv array and no shell parsing.
NodeReplEvaluate JavaScript in a run-scoped Node.js REPL.
WebFetchDeterministically decode and locally extract one public URL, with explicit opt-in browser rendering.
WebSearchSearch 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.

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, then SIGKILL after 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({ 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 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.

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 maxInputTokens ceiling (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:

ValueMeaning
trueCompaction fired this run (before the request or mid-run).
falseEnabled but not needed.
nullCompaction 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.

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.

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.

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.

CapabilityCoverageHow
Built-in toolsconfigtools.allowedTools / tools.disallowedTools
Bloat guard (256KB + artifacts)autoBuilt in; artifacts to artifacts.dir
Usage/cost trackingautoRecorded in JSONL artifacts
Context compactionconfig + providerruntime.compaction.*; bridge-driven Pi compaction
Web researchconfig + autotools.web.*; extraction/retry built in
Tool schedulingcoderuntimeOptions.piToolExecutionMode