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.

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.allowedToolsmono-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.

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"]
}
}

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.

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 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({ 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.

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.

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:

ValueMeaning
trueCompaction fired this 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. 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.

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. 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.

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