Skip to main content

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. Every surface described here disappears when the roster is empty.

Quick Jump​

NeedSection
Name a second agentNaming an agent
Let one agent ask anotherThe roster
Understand what a manager doesThe manager plans
Ask for a specific agentAsking for an agent
Know which of four sources decidesWhich agent answers
Bind one agent to a whole channelA channel names an agent
Know who is accountable for a delegated actionWho signs it
See who answered a turnAttribution
Read a manager's plan in the threadThe plan
Narrow what a peer may reachThe ceiling
Give an automation a narrower agentAutomations

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 a modelPresets entry.
  • 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 by tools.file and tools.exec as 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 configMeans
the field is absentno 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. 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. Authority is 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. 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. A reader of the audit log then 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 answer itself instead.

Two tools carry this:

ToolWhat it does
list_delegatesWho this agent may ask, and what each is for. Filterable, so a problem — "postgres", "firewall" — finds the agent for it.
delegate_to_agentHands 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.

Which agent answers​

Four things can decide, and they are decided in this order. The first one that resolves wins.

OrderSourceSet by
1The conversation's agentthe composer's picker, or an automation binding
2@agent:<name> in the messagea person, mid-sentence
3channels.<name>.agentan operator, per channel
4agents.defaultsnothing. It is what answers when the three above say nothing

@agent: sits above the channel binding on purpose. A person who names an agent expects that agent. A channel default that swallowed the mention would make a narrow agent look broken rather than narrow.

A name nobody configured is not a step down this list. A client that sends an agent name for source 1 gets agents.defaults when that name is absent from the roster. It does not fall to the mention or to the channel binding. A client that invents a name reaches the agent that grants least, which is the safe end rather than the next-most-specific one.

This does not reach the composer, whose picker only offers names from the roster. It reaches a Python SDK caller, and a roster edited while a settings form was open.

A channel names an agent too​

Every channel except the WebUI may bind one agent. Then every message arriving there is answered by that agent:

{
"channels": {
"telegram": { "token": "...", "agent": "sre" },
"email": { "agent": "triage" }
}
}

Set it in Settings → Channels, in the Answering agent section of the channel's panel. See Channels for the field.

Two refusals, both when config loads rather than when a message arrives:

  • A name absent from agents.named refuses to load, and the message names it. An operator who mistypes learns it from the config, not from a channel that answers as the deployment default for a week.
  • channels.websocket.agent refuses to load. The WebUI chooses per message in the composer, so a channel-wide default there would answer every turn where you chose nothing.

The plan in the thread​

A manager turn that delegates shows its plan as one object, not as a row of unrelated tool calls. The plan holds one row per delegation. Each row names 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:

  1. What the peer declared. Its toolGroups decide which grouped surfaces it may reach.
  2. 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. That registry is what the turn can actually call, which is not always what config would suggest. The gate enforces the limit where it 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. 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. That 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. 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. See Which agent runs it for the blast-radius argument, and for what happens when a named agent is removed from config.

What this does not do​

  • No cross-workspace delegation. The authority invariant does not survive it. 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.