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.
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"] }, "subagents": { "enabled": true, "maxConcurrent": 5, "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?}; with no name it runs a read-only
general-purpose researcher on the parent’s model.
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
and effort) builds a one-off subagent for that call instead of selecting a
profile. tools and effort apply only alongside systemPrompt — a configured
profile brings its own — and 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.
What operators see. Every subagent tool call streams live to the TUI and web
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.
Limits. maxConcurrent (default 5) is an upper bound on simultaneous
subagents; the provider may schedule fewer. In particular, Pi 0.85 exposes only
a global tool-execution mode, so any offered stateful/mutating or MCP tool makes
the whole harness sequential, including an otherwise parallel batch of Agent
calls. 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.
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, 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. Omitting a profile’s model inherits the
parent’s configured route, so subagents get the fallback chain and same-model
retries too; naming a model routes that profile through it instead.
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.
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"] }}Env equivalents: MONO_AGENT_ALLOWED_TOOLS, MONO_AGENT_DISALLOWED_TOOLS (comma-separated tool names).
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 Pi-native 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.
This guard is always on (coverage: auto). You do not enable it; you only choose where artifacts are written:
{ "artifacts": { "dir": ".mono-agent/artifacts" }}Env: MONO_AGENT_ARTIFACT_DIR.
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, and Phoenix & backfill to export them as spans.
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 pi-native bridge, the bridge drives AgentHarness.compact():
- Proactively — before a turn when the running model is near its context window.
- 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. |
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. 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 Pi-native bridge. Defaults resolve against the effective context window W: trigger ratio 0.70, safety headroom
clamp(floor(W × 0.25), 16000, 96000), 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 under
runtime.compaction (or the matching MONO_AGENT_COMPACTION_* variables).
See Sessions & concurrency for how sessions persist, and Fallback & failover for window changes across the fallback chain.
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. With Pi 0.85, independent read-only tools may
overlap only when the offered tool set contains no sequential tool. If Write,
Edit, Bash, Exec, NodeRepl, any MCP tool, or another stateful/mutating
built-in is available, the harness serializes the whole batch because upstream
no longer exposes mixed per-tool scheduling. 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 deprecated
piToolParallelismMode alias maps one-at-a-time to sequential and all to
safe-parallel, with a runtime warning. 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 |