Agents

An agent is data: a system prompt, a model, a set of tools. It is declared in Markdown with frontmatter, following pi’s own convention, and it holds no state. Bringing one to life is spawn()’s job, and that produces a subagent.

---
name: reviewer
description: Reviews code and returns actionable remarks
tools: read, grep, find, ls
model: anthropic/claude-sonnet-5
lifetime: workflow
openInHerdr: true
---

You review the code produced and return at most 5 remarks…

The Markdown body is the system prompt, used verbatim.

Fields

Field

Required

Meaning

name

yes

How every caller refers to the agent.

description

yes

One line on what it is for. Also what route and orchestrate read to decide who does the work.

tools

no

Allowed tools. Absent means read-only: read, grep, find, ls.

skills

no

Skills it may load, by name. Absent means none - see below.

concurrency

no

How many subagents it runs at once when it delegates. Only read for an agent that names subagent.

model

no

A pattern such as anthropic/claude-sonnet-5. A caller’s model argument beats it; absent everywhere means pi’s default - see below.

lifetime

no

Default lifetime, for spawn and inside every workflow. A lifetime passed to either wins. A flow does not read it: a node’s memory: decides.

openInHerdr

no

Default for “give this agent its own herdr split”. See Display.

A file missing name or description is ignored silently. That is pi’s behaviour and it is kept.

description is not decoration. A vague one produces vague routing, and no parser can repair that.

Where they are loaded from

import { findAgent, loadAgents } from "@ai-for-dev/combo";

const agents = loadAgents();                       // ~/.pi/agent/agents/ only
const both = loadAgents({ scope: "both" });        // plus .pi/agents/
const reviewer = findAgent(agents, "reviewer");    // throws on an unknown name
  • "user" (the default) reads ~/.pi/agent/agents/.

  • "project" reads .pi/agents/ of the current repository.

  • "both" reads the two.

There is a third source, off by default: the agents shipped with this package (scout, coder, reviewer, planner, router, synthesiser, interviewer, auditor, committer, explorer, member, debater). Pass builtin: true to include them - which the pi extension always does, because otherwise its commands only work inside a repository where someone has already copied the definitions by hand.

Precedence runs from the least specific to the most: shipped, then yours, then the repository’s. Whoever is closer to the work wins the name, so writing your own scout.md replaces ours without having to remove anything.

loadAgents({ scope: "both", builtin: true });

It is off by default for scripts on purpose: asking for “the user’s agents” must not hand you ours as well.

Project agents are never loaded by default, and that is a security boundary rather than a preference: .pi/agents/ is repository-controlled content, so its instructions are third-party instructions. Asking for them is one word; getting them by surprise is not acceptable.

findAgent throws on an unknown name, deliberately: a typo in a workflow should fail immediately, not three steps later as a failed Result. When no agent is found at all, the error says so in terms of scope, because that is nearly always the cause.

Agents are rediscovered on every call, so editing a .md file is enough to reload it.

Seeing what you have

/agents
project · /repo/.pi/agents
  scout        Locates the code relevant to a question and reports where it lives
user · /home/you/.pi/agent/agents
  (none)
builtin · /repo/agents
  coder        Implements a change, and applies review remarks across iterations
  reviewer     Reviews code and returns at most five actionable remarks

A project agent needs scope "project" or "both" from the subagent tool. /run and /step load all three.

Grouped by where each definition came from, most specific first, because that is half of what is being asked: an agent nobody can find is usually one whose scope is not the one being loaded. A source that turned up nothing still names the directory it looked in, which is where yours would go. A name defined twice appears once, under the source that won it.

Tools, and why a prompt is not a boundary

A subagent that must not write must not have write and edit. Asking it nicely in the system prompt does not work. This is not a hypothetical: an example in this repository once gave its coder the full toolset and merely asked it to change nothing. It edited src/usage.ts anyway, twice, in a plain demo run.

---
name: scout
description: Locates the code relevant to a question
tools: read, grep, find, ls
---

Read-only is the recommended default for anything that explores. The allowlist is genuinely enforced by pi. A weak model will still emit calls to tools it does not have; those fail, and the model may retry them in a loop, which is an argument for loop’s maxIterations and for timeoutMs, not for widening the allowlist.

Tools combo brings

Some names in tools: are not pi’s. verdict is combo’s, and it is not one to write there: a flow node written with verdict: adds it to its agent’s tools:, the way a typed node adds submit, so the reviewer declares its decision as a call rather than as a word in its prose. See Ledgers and verdicts. Nothing else offers it, not a /step, not the tool’s loop, not a TypeScript loop, so the shipped reviewer and auditor name only pi’s tools and decide in prose there: LGTM and APPROVED, the words a loop’s until is given.

subagent is combo’s too, and an agent that names it can split its task across children of its own. Two levels deep by default, and the roster it may reach is the caller’s to pass.

board is the third, and a swarm hands it to every member whose tools: names it: post what you found, read what the others posted, take a key before writing to it. Who is posting is in the closure rather than in the parameters, so a member cannot post as another one. A member that does not name board still runs, and cannot reach the others, which makes that run a fan-out.

How wide it splits is the agent’s own, declared as concurrency: in its frontmatter:

---
name: explorer
description: Answers a question by splitting the reading across scouts
tools: read, grep, find, ls, subagent
concurrency: 3
---

That number belongs in the file rather than at the call site because it follows from how the agent was told to think: an explorer asked for two to four tasks wants three of them in flight, and saying so once beats saying it everywhere it is used. A count that is not a positive whole number is ignored, since concurrency: 0 would mean an agent that delegates to nobody. See Design decisions; agents/explorer.md is the one shipped agent that asks for it.

The allowlist covers these exactly as it covers pi’s own, which is what keeps the rule readable: what an agent can do is in its file. A name nothing offers is not free, though: the tool is absent, and a model whose prompt speaks of it calls it anyway and is refused, turn after turn. Measured against ilaas/gemma-4-31b, a reviewer naming verdict in a plain /step called it three times, each answered Tool verdict not found, before its prose.

Skills

An agent can name skills, and only the ones it names:

---
name: scout
description: Locates the code relevant to a question
tools: read, grep, find, ls
skills: diffing, humanising
---

A name is looked up in three places, nearest first:

  1. agents/scout/skills/ - beside the definition. agents/scout.md and the skills it needs travel together, so a clone of the repository resolves the same names.

  2. .pi/skills/ in the repository, found by walking up from the working directory.

  3. ~/.pi/agent/skills/ - yours.

The first two are how a skill stays reproducible; the third is a convenience that depends on the machine. A skill an agent cannot work without belongs in the first.

The name is the one SKILL.md gives in its frontmatter, not the name of its directory: skills/assertion-smell/SKILL.md saying name: assertion-smells is the skill assertion-smells.

Nothing is loaded eagerly: pi puts a name, a description and a path in the system prompt, and the model opens SKILL.md itself. That last part is why an agent declaring a skill needs read in its tools: - without it pi drops the whole section - and why a skill whose frontmatter sets disable-model-invocation is refused, since pi keeps that one out of the prompt and the agent would never see it. Both fail at spawn, as does a name that matches nothing. That error lists the skills it found, offers the nearest name when one is close, says so when a directory carries the name but its SKILL.md names another, and ends with the directories it looked in:

Agent "scout" declares unknown skill(s) assertion-smell.
.../agents/scout/skills/assertion-smell/SKILL.md is named "assertion-smells":
a skill goes by the name in its SKILL.md, not its directory's, so declare
"assertion-smells" or change that name. Skills found: assertion-smells.
Looked in: .../agents/scout/skills, .../.pi/skills, ~/.pi/agent/skills.

What a subagent inherits

Nothing from your environment. The system prompt goes through the library’s own resource loader: no extensions, no context files, no project trust, and no skill the definition did not name. A subagent sees what its own file asks for, which is what makes a run reproducible.

Two lines are appended to it, and neither is inherited context. The first is where it is: the ground every tool call stands on, without which a model guesses. A real run showed the cost - a scout called ls /Users/loic/gouarin/…, the user’s name with a dot turned into a slash, got “no such path” and gave up without ever trying a relative one.

The second is which language to answer in: the one the work is written in, not the one these instructions are. Definitions here are English, so a French question used to come back in English, translated by nobody’s decision. The rule points at the material rather than at the prompt around it, which is what makes it hold when a workflow wraps a French request in English scaffolding - a review, a plan, an audit - and it names no language itself, because an example in a standing instruction is read as the target. Measured on a small open-weight model: an English task answers English and a French one answers French; /step scout and the whole explore flow answer in French, and a reviewer handed a French goal answers in French while the router still answers an agent name and the planner still answers JSON.

The same rule closes every turn, in one sentence, because the English a subagent reads is not only in its prompt. A combinator frames the work - which round this is, what is new on the board, what is still free to take - and that framing arrives in the same message as the task, which weighs far more than anything standing behind it. Measured on ilaas/gpt-oss-120b, a French question put to a swarm of two over two rounds, counting the board posts that came back in French: 5 of 19 with the standing rule alone, 62 of 89 with the closing line as well. ask() adds it, so no combinator has to remember to, and what the event stream reports stays the task the caller wrote.

What a word must not do is move. A model writing French writes PRÊT for READY and RAS for LGTM, which is a loop that never ends and a review nobody can parse. The instruction exempts by shape rather than by list - a word you were told to answer with, a JSON key, a name, an identifier, a path, anything quoted from code - so it also covers the sentinels of a workflow you write yourself.

The model: yours to pin, not ours

An agent that declares no model: runs on whatever the caller passed, and on pi’s own settings when nobody passed anything. None of the agents shipped here declares one, on purpose: a package that pinned a model would override the settings you already made and fail outright if you hold no key for that provider.

The cost of that is invisible unless it is said out loud: a run with no --model runs on your ~/.pi/agent/settings.json, whatever is in it that day. Fine for using the thing; useless for measuring it. So anything whose numbers will be compared names its model - experiment({ models }), --model on /run, /step, /swarm and /interview, --model in the examples’ argv. An agent you write for your own machine is welcome to declare one; it is only what ships that must not.

Agents shipped here

agents/ holds the demo definitions used by the examples and by the shipped flows: scout, coder, reviewer, planner, router, synthesiser, interviewer, auditor, committer, explorer, member, debater. They are symlinked into .pi/agents/ so the extension can find them - with an explicit scope, like anyone else’s. None of them pins a model, for the reason above.

Reference

  • agent - Agent, loadAgents, findAgent, parseAgent.

  • skills - resolveSkills, skillDirs.

  • language - answerInTheirLanguage, the standing instruction.