Agents
An agent is a single AI worker — one Markdown file whose YAML frontmatter configures it and whose body becomes its system prompt.
What is an agent
An agent is the smallest unit of work in rupu: a narrow, single-purpose AI worker. Each agent lives in one Markdown file. The file has two parts — a block of YAML frontmatter that tells rupu how to run the agent (which provider, which model, which tools, what it is allowed to do), and a Markdown body that is sent verbatim to the model as its system prompt.
Good agents are narrow, explicit, and testable. A well-scoped agent answers six questions clearly: what job it owns, what tools it may use, what sequence it should follow, what counts as done, what it must not do, and what shape the final answer should take. When several specialists, review boundaries, or fix loops are involved, reach for a workflow instead of one giant prompt.
The agent file
An agent is a Markdown file with YAML frontmatter. The frontmatter (between the opening and closing
--- lines) tells rupu how to run the agent; everything after the closing
--- is the system prompt. rupu parses the frontmatter with
strict unknown-field rejection — a misspelled key fails fast rather than being silently ignored.
Agent files resolve from two locations:
~/.rupu/agents/<name>.md— global agents (personal utilities).<project>/.rupu/agents/<name>.md— project-local agents (checked into the repo).
Project-local agents shadow global agents by name:. Shadowing
is all-or-nothing — rupu does not merge frontmatter or prompts. rupu agent list
shows whether each agent comes from project or global scope.
Frontmatter fields
| Key | Type | Default | Meaning |
|---|---|---|---|
name | string | required | Agent identifier used by rupu run <name> and by workflows. |
description | string | none | Human-readable summary shown by rupu agent list. |
provider | string | anthropic | Which LLM provider to use: anthropic, openai, gemini, or copilot. |
auth | api-key | sso | resolver chooses | Optional auth-mode hint when a provider has both credentials. |
model | string | config default, else claude-sonnet-4-6 | Provider-specific model id. Not validated at parse time. |
tools | array<string> | built-in + discovered MCP tools | The agent's tool allowlist. Strongly recommended to declare explicitly. |
dispatchableAgents | array<string> | none | Allowlist of child agent names this agent may spawn via dispatch_agent / dispatch_agents_parallel. A dispatch call fails if its target isn’t listed. |
maxTurns | integer | 50 | Hard cap on model turns (not a token budget). |
permissionMode | ask | bypass | readonly | ask | Permission model for shell and write effects. CLI --mode overrides it. |
effort | string | provider default | Cross-provider reasoning level: auto, minimal, low, medium, high, max. |
contextWindow | string | model default | Cross-provider context tier: default or 1m. |
outputFormat | text | json | free-form text | Hint for structured output; describe the JSON shape in the prompt too. |
outputSchema | JSON Schema | none | Inline JSON Schema for a guaranteed schema-conforming result via the provider’s structured-output mode (Anthropic). Absent ⇒ prompt-driven outputFormat only. |
anthropicOauthPrefix | bool | provider default | Anthropic SSO only — enables/disables the OAuth system prefix. |
anthropicTaskBudget | integer | none | Anthropic-only soft output budget, separate from maxTurns. |
anthropicContextManagement | string | none | Anthropic-only server-side pruning of older tool blocks (tool_clearing). |
anthropicSpeed | string | none | Anthropic-only account-gated fast mode (fast). |
anthropic* fields unless
you specifically need them. For checked-in repo agents, set provider,
model, tools, and permissionMode
explicitly so behavior does not drift with a mutable global default.
A complete example
This is the code-reviewer agent that ships as a rupu template — a read-only
reviewer that can inspect a local file, a diff, or a pull request and return concise findings.
--- name: code-reviewer description: Read a file, diff, or PR and return concise textual findings. provider: anthropic model: claude-sonnet-4-6 tools: [read_file, grep, glob, scm.prs.get, scm.prs.diff] maxTurns: 12 permissionMode: readonly --- You are a code reviewer. If the prompt contains a PR reference, use the SCM tools to inspect the PR metadata and diff. If the prompt names local files or includes a diff, review that directly. Focus on: - correctness bugs - missing edge-case handling - unclear or brittle structure - missing tests where behavior changed Return either: - `no issues` - or a short bulleted list ordered by severity Do not edit code.
Note the shape: the frontmatter pins a provider and model, declares the smallest useful tool set, and
runs readonly so a reviewer can never mutate the thing it is reviewing.
The body gives the agent a role, a process, and an explicit output contract.
Tools & permissions
The tools: field is the agent's tool allowlist. If you omit it, the agent gets
the full built-in surface plus, when SCM / issue connectors are configured, any discovered MCP tools. For
reusable repo agents, an explicit list is far better than the implicit wide-open default.
The built-in tools are:
bash— run shell commandsread_file·write_file·edit_file— file I/Ogrep·glob— search and match
MCP-backed tool names are also valid — for example scm.prs.get,
scm.prs.diff, scm.prs.create,
issues.get, issues.comment. The allowlist supports
exact matches (scm.prs.get), prefix wildcards (scm.*),
and a global wildcard (*).
tools: gates the tool calls an agent may
make. It is not the same as a workflow's actions:, which gates the
action protocol emitted from agent output inside a workflow.
Permission modes
permissionMode controls what an agent may do without confirmation:
| Mode | Meaning |
|---|---|
ask | Prompt before shell and write effects. The safest default for agents that edit code. |
bypass | Execute allowed tools without confirmation. |
readonly | Allow reads, deny writes. |
readonly blocks the built-in writes (write_file,
edit_file, destructive shell work via bash) and MCP
write tools such as scm.prs.create, issues.comment,
issues.create, and scm.branches.create. Because
ask cannot proceed in a non-interactive context, pass
--mode bypass or --mode readonly explicitly there. The
CLI --mode flag always overrides the value in the file.
Sub-agent dispatch
An agent can invoke other agents as tool calls in the middle of its own run, read their results, and fold them back into its context before continuing. This is composition at the agent layer — distinct from a workflow, which composes agents at the step layer. A writer agent can dispatch a reviewer mid-draft, revise against the findings, and carry on, without the workflow author fanning out at the YAML level.
Two built-in tools drive it:
dispatch_agent— run one child agent synchronously. The child runs to completion in its own context; the parent receives the child's final text plus token and duration accounting.dispatch_agents_parallel— fan out several children concurrently and get back a map keyed by id, each with its own output. Reach for this when N specialist reviews can run independently.
Dispatch is gated by a per-agent allowlist. The dispatchableAgents: frontmatter
field names exactly which children this agent may spawn; a dispatch call fails at invocation if its target
is not in the list (empty or absent means the agent can dispatch no one). The dispatch tools themselves
live in the built-in tool surface, so they are available when tools: is omitted
— but for a repo agent with an explicit allowlist, list them alongside
dispatchableAgents:.
--- name: writer tools: [read_file, edit_file, dispatch_agent, dispatch_agents_parallel] dispatchableAgents: [security-reviewer, perf-reviewer] --- Draft the change, then dispatch the reviewers on your diff. Revise against their findings before you finish.
Each child runs as its own agent: its own permission mode (a child is never widened by its parent), its own
JSONL transcript written under the parent run with a parent→child link, and its own token budget.
Sub-agent dispatch works in a bare rupu run as well as inside workflows. See
MCP & tools for the tool surface and
Workflows for step-layer composition.
Structured outputs
By default an agent returns free-form text. Two frontmatter fields tighten that when a downstream step or tool needs a predictable shape.
outputFormat: json is a hint — it asks the model to return
JSON (describe the shape in the prompt too). It is prompt-driven: nothing structurally guarantees the
result parses or matches a particular schema.
outputSchema: goes further. Declare a JSON Schema inline in the frontmatter and
rupu hands it to the provider's structured-output mode (Anthropic's
output_config.format with type: json_schema), which
returns a guaranteed schema-conforming object. Anthropic mandates a real schema for this
mode, so omitting outputSchema: keeps the prompt-driven-only behavior; other
providers currently ignore the schema.
--- name: security-reviewer outputFormat: json outputSchema: type: object properties: findings: type: array items: type: object properties: severity: { type: string, enum: [low, medium, high, critical] } title: { type: string } body: { type: string } required: [severity, title] additionalProperties: false required: [findings] additionalProperties: false --- Audit the diff and return your findings.
With a guaranteed shape, downstream workflow steps can rely on those fields in their template expressions instead of defensively parsing free text.
Providers & models
rupu supports four LLM providers, each with two authentication modes (API key or SSO):
| Provider | API key | SSO | Notes |
|---|---|---|---|
anthropic | yes | browser callback | Console API key or Claude.ai SSO. The most exercised provider. |
openai | yes | browser callback | Platform API key or ChatGPT SSO. |
gemini | yes | browser callback | API key via Google AI Studio; SSO via Vertex / CLI. |
copilot | yes | device code | API-key path uses a GitHub PAT; requires paid Copilot. |
An agent picks its provider with provider: and its model with
model:. Model ids are provider-specific and are not validated
at parse time — an invalid id fails at runtime when the provider is called. When an agent omits
model:, rupu falls back to the provider's
default_model from ~/.rupu/config.toml; there is no
global default, so the agent must either set model: or have one resolvable in config.
rupu run resolves a model id through three sources, in order: custom entries
under [[providers.<name>.models]] in config, the live cache
(~/.rupu/cache/models/<provider>.json, TTL 1h), then a baked-in list for
providers with limited listing endpoints. Inspect what is available with
rupu models list and refresh the caches with
rupu models refresh.
Authenticate a provider before running an agent against it:
# API key (or pipe the secret on stdin) rupu auth login --provider anthropic --mode api-key --key <secret> # SSO rupu auth login --provider anthropic --mode sso
Generate one from a description
Don’t want to hand-write the frontmatter? Describe the agent in plain language and let rupu draft it for you. create generates the .md with a model (running a validate → repair loop so the result actually parses) and opens it in your editor to refine.
# scaffold + AI-draft an agent from a one-line brief rupu agent create security-reviewer \ --describe "A read-only reviewer that audits a diff for OWASP Top 10 issues and returns JSON findings" # or scaffold an empty agent and edit it yourself (omit --describe) rupu agent create my-agent
The same generate-from-a-description flow is available in the control plane’s authoring UI.
Running an agent
List and inspect the agents rupu can see, then run one:
# list every resolvable agent (project + global scope) rupu agent list # show one agent's resolved frontmatter and prompt rupu agent show code-reviewer # run an agent with a prompt rupu run code-reviewer --prompt "Review the staged diff for correctness bugs"
Use --mode to override the file's permissionMode at
run time — for example --mode bypass or --mode readonly
in a non-interactive context. Every run is recorded as a JSONL transcript you can replay later:
# inspect a recorded run
rupu transcript