Agents
One agent answers by default. This page is about naming more of them, and about what changes when you do.
A named agent is a description, a model, and the tools it is allowed to see. Nothing else about nanoinfra changes: a deployment that names no agents behaves exactly as it did before, and every surface described here disappears when the roster is empty.
Quick Jump
| Need | Section |
|---|---|
| Name a second agent | Naming an agent |
| Let one agent ask another | The roster |
| Understand what a manager does | The manager plans |
| Ask for a specific agent | Asking for an agent |
| Know who is accountable for a delegated action | Who signs it |
| See who answered a turn | Attribution |
| Read a manager's plan in the thread | The plan |
| Narrow what a peer may reach | The ceiling |
| Give an automation a narrower agent | Automations |
Naming an agent
In ~/.nanoinfra/config.json:
{
"agents": {
"named": {
"sre-prod": {
"description": "Hands-on checks on production hosts",
"modelPreset": "kimi-general",
"toolGroups": ["servers"],
"skills": ["servers"],
"addendum": "Prefer read-only checks. Never restart a service without asking."
}
}
}
}
Every field is optional except the name. An omitted field inherits from agents.defaults, which is
what makes a two-line agent meaningful.
description— what this agent is for. It reaches the model when another agent is deciding whom to ask, and it reaches you in the picker. Write it for both readers.modelPreset— which model this agent answers with. Names amodelPresetsentry.toolGroups— the tool groups this agent may use. Naming groups narrows; it never widens. Tools that belong to no group — reading a file, running a command — stay governed bytools.fileandtools.execas they always were.skills,connectors,mcpServers— same reading: naming narrows.
Each of those four lists holds three states, and the third is the one worth knowing:
| In config | Means |
|---|---|
| the field is absent | no ceiling — this agent reaches everything the deployment has, and gains what you add later |
[] | declared, and empty — this agent reaches none of them |
["servers"] | only what is named |
An empty list used to mean all of them, and that made two useful agents impossible to write down.
One is a coordinator: an agent that may reach no grouped surface, and therefore has to ask a peer
instead of running the command itself. The other is any agent on a deployment with a dozen MCP
servers installed, where loading all of them costs a schema set apiece in the first message of
every conversation. [] is how you say none.
addendum— appended after the platform's own prompt sections. It specialises the agent and cannot replace the tool contract or the safety notes. That is structural, not a convention.
There is deliberately no agent type whose capability list means no restrictions. An agent's tool set is declared, not accumulated.
A name has to survive being a token — letters, digits, -, _, ., and no spaces — because an
agent is addressed as @agent:<name>. Config refuses a name you could not type.
The roster is the authorization
An agent may ask another only if config says so:
{
"agents": {
"named": {
"sre-prod": { "description": "Hands-on checks on production hosts" },
"db-oncall": { "description": "Postgres and Valkey, read-only by default" },
"manager": {
"description": "Plans work and asks the right specialist",
"delegates": ["sre-prod", "db-oncall"]
}
}
}
}
Membership in delegates is the grant. It lives in config because that is where authority
lives — not in who can reach whom over the network, and not in what a tool call asks for. The
executor re-reads the roster when the delegation actually runs, so removing an agent from a roster
stops it being reachable on the next turn rather than on the next restart.
A roster that names an agent you never configured is refused when config loads, with the name in the message. So is an agent that lists itself.
Delegation is one level deep. A delegated turn cannot delegate again; it reports what it found and the agent that asked decides the next step. That is not a simplification for later — with chains, "a delegated turn never holds more authority than the turn that spawned it" needs a transitive check at every hop, and a reader of the audit log has to reconstruct the path to learn who authorised what. With one level, the check is local and the record is two names.
The manager plans; it does not re-route
Naming an agent does not reach that agent. The request goes to the manager, and the manager makes a plan. The plan may delegate to the agent you named, to a peer that actually covers the target, to several peers at once, or to nobody — the manager may simply answer.
Two tools carry this:
| Tool | What it does |
|---|---|
list_delegates | Who this agent may ask, and what each is for. Filterable, so a problem — "postgres", "firewall" — finds the agent for it. |
delegate_to_agent | Hands one task to one peer and waits for its answer. |
A peer sees the task, not the conversation. The manager is responsible for including what the peer needs; passing the transcript would hand a hands-on agent a conversation it was never part of. If a task is missing something, the peer is expected to say what is missing rather than guess.
Asking for an agent
Type @agent: in the composer and pick one. The token stays in your message as text, and that is
deliberate: it is a preference the manager reads, not an invocation. The manager may still
delegate elsewhere, or answer itself, and it can tell you why.
It reads the way asking a colleague directly does: they hear you, and they still check with their manager before acting.
The composer also offers an agent for the conversation. Choosing one is what decides who
answers your turns; the @agent: token inside a message is a hint to whoever is answering. When
the deployment names agents, the model selector goes away — once the model belongs to the agent,
choosing a model here would be choosing something that is no longer the turn's to decide.
The plan in the thread
A manager turn that delegates shows its plan as one object, not as a row of unrelated tool calls: one row per delegation, each naming the peer that ran it and what that peer's own turn cost.
- Partial failure is visible. When one peer answers and another fails, the plan says which was which. A row never reads as done when its peer returned an error.
- The plan's total is the peers' cost, not the manager's. A delegated turn is its own turn, so
folding a peer's tokens into the manager's usage line would print one turn's cost twice. When
only some peers reported a cost, the total says so —
1 of 2 reported— rather than presenting a partial sum as a whole one. - A reload shows the same plan. The live frame and the persisted transcript are one body.
One round of delegations is one card. A turn that delegates, edits a file, then delegates again shows two — two rounds, two decisions.
The ceiling
Two limits apply to a delegated turn, and they are different questions:
- What the peer declared. Its
toolGroupsdecide which grouped surfaces it may reach. - What the turn that asked could reach. A manager that can only read cannot reach a remote mutation by asking a peer to do it. This is computed from the asking turn's own registry — what it can actually call, which is not always what config would suggest — and it is enforced where the gate decides rather than in the tool that asks.
The same applies one level further down: a capped turn hands its ceiling to anything it spawns, so a subagent cannot be the way around it.
Who signs a delegated action
When a peer does something that needs approval, three parties are involved: the human who asked, the agent that delegated, and the peer that acts. The record names all three, in that order:
alberto -> manager -> sre-prod
- The peer runs as itself, not as the agent that delegated to it. An approval that appeared to come from the manager while a peer acted would be a silent misattribution.
- The originating human is the actor when a person is waiting on the turn. An approval prompt says which agent will act, so you are never approving a command without knowing who runs it.
- When nobody is waiting — a manager started by an automation — there is no human in the chain,
and a standing grant is the only thing that can authorise the peer's
action. The chain then reads
manager -> sre-prod, and a reader cannot mistake it for something a person approved.
The rule underneath all three: a delegated turn never holds more authority than the turn that spawned it.
Every turn says who answered it
An assistant turn records the agent that produced it, and shows it beside the turn's cost. This matters because switching agents mid-conversation is allowed: without a per-turn record, a reader would be inferring from the model or the tools, which is a guess that is sometimes wrong and cannot be corrected afterwards.
A turn answered by the default agent records nothing and shows nothing — which is every turn in a deployment that names no agents.
Automations name an agent too
An automation may name the agent that runs it, and the agent is a ceiling: the job runs with that agent's tools and its addendum, and the job's own skill selection may only narrow what the agent already has. Asking for a skill the agent does not have is refused with a reason rather than silently widened.
This is the point of a narrow agent. A nightly package check wants one host and one command; an agent scoped to exactly that is a far smaller blast radius than the default agent running the same prompt.
What this does not do
- No cross-workspace delegation. The authority invariant does not survive it, and the rule is easy to write now and hard to add once somebody depends on a shared specialist.
- No per-agent instances or idle timeouts. Concurrency here is a session lock and the subagent limit, both in one process.
- Subagents are not peers. A subagent is a child of the same agent with the same tools and no separate identity. A delegate is a different agent with its own bindings.