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.
A rupu release older than a field also refuses an agent that sets it, so upgrade remote hosts before adding
new keys to agents they run.
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, and
rupu agent validate <name> / --all checks that an
agent file parses, names only tools rupu knows and only skills that are installed.
Frontmatter fields
| Key | Type | Default | Meaning |
|---|---|---|---|
name | string | required | Agent identifier used by rupu run <name> and by workflows. Lowercase, hyphen-separated. |
description | string | none | Human-readable summary shown by rupu agent list. |
provider | string | anthropic | Which LLM provider (account) to use. Canonical names: anthropic, openai, gemini, copilot. |
auth | api-key | sso | resolver chooses (SSO preferred when both exist) | Optional auth-mode hint when a provider has both credentials. |
model | string | config default_model, else claude-sonnet-4-6 | Provider-specific model id. Not validated at parse time; an invalid id fails at runtime. |
tools | array<string> | the default grant (below) | The agent's tool grant: what its model may call. See Tools & permissions. |
maxTurns | integer | 50 | Hard cap on model turns (not a token budget). |
permissionMode | ask | bypass | readonly | ask | Permission model, decided per tool call from the tool's effect. CLI --mode overrides it. |
dispatchableAgents | array<string> | none (no dispatch) | Allowlist of agent names this agent may start with dispatch (kinds subagent and agent, and the legacy dispatch_agent / dispatch_agents_parallel). |
dispatchableWorkflows | array<string> | none | Allowlist of workflow names this agent may start with dispatch {kind: workflow} (and the legacy run_workflow). |
messaging | bool | true | false turns the messaging tools (board.*, msg.send) off for this agent wherever it runs. See Messaging. |
skills | array<string> | none | Skills the model may load with skills.load. See Skills. |
preloadSkills | array<string> | none | Skills whose whole body goes into the system prompt; implies granted. |
effort | string | provider default | Cross-provider reasoning level: auto, minimal, low, medium, high, max. |
thinkingDisplay | summarized | omitted | updates | summarized | Reasoning-display hint; honored by Anthropic adaptive models only. |
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 | object (inline YAML) | none | JSON Schema for Anthropic structured outputs. Only takes effect together with outputFormat: json. |
concerns | object | none | Coverage-concerns block; injects the coverage tools and the catalog into the system prompt. |
findingsProfile | full | summary | full | Findings contract for findings.report. A workflow step's findings_profile or the workflow's defaults.findings_profile overrides it. |
fallbacks | array of {model, provider?} | [recovery].fallbacks from config, else none | Ordered fallback models tried when a reply cannot be used. See Fallbacks. |
maxTokens | integer | discovered output cap | Per-request output-token cap. |
contextWindowTokens | integer | discovered input limit | Input-token limit used for proactive compaction. |
compactAtPercent | integer | 80 | Percentage of the input limit at which compaction triggers; clamped to 10–95. |
anthropicOauthPrefix | bool | provider default | Anthropic SSO only — enables/disables the OAuth system prefix. |
anthropicPromptCache | bool | [providers.<name>] prompt_cache, else on | Anthropic only; false disables prompt caching for this agent. |
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.
Advanced frontmatter
The fields above cover most agents. The ones below tune reasoning, token limits, findings, failure recovery, skills, messaging and provider-specific behavior. All are optional; leave them out and rupu uses the discovered or configured default.
Provider accounts
provider: names an account, not only a vendor. A bare vendor
name (anthropic, openai, …) is its own
account; to keep two identities for one vendor, log in to a named account with
--kind and point the agent at it, e.g.
provider: anthropic-work. auth:
(api-key or sso) then picks between that account’s
credentials when it holds both. See Providers → Accounts.
Reasoning, context & token limits
| Key | Type | Default | Meaning |
|---|---|---|---|
effort | string | provider default | One cross-provider reasoning level: auto, minimal, low, medium, high, max; aliases adaptive (= auto) and xhigh (= max). rupu translates it per provider — Anthropic adaptive models get thinking.type: adaptive plus output_config.effort, pre-4.6 Anthropic models thinking.budget_tokens, Gemini thinkingLevel / thinkingBudget, OpenAI Codex reasoning.effort, OpenAI-compatible endpoints reasoning_effort forwarded verbatim. |
thinkingDisplay | string | summarized | What reasoning the model returns alongside its answer (the thinking.display knob). summarized returns a readable summary captured into the transcript; omitted still runs (and bills) thinking but returns no text; updates returns between-tool-call progress notes and needs a capable model, else degrades to summarized. Honored only by Anthropic adaptive models (4.6+ and the Claude-5 family); elsewhere the run degrades to the model's default display with a one-time warning rather than failing. |
contextWindow | string | default | default or 1m (aliases 1M, one_million). On Anthropic, 1m opts an API-key account into the 1M-token context beta; an SSO account uses a [1m] suffix on model: instead. Neither changes the input limit rupu discovers. |
maxTokens | integer | discovered output cap | Per-request output-token cap. Overrides the cap discovered from the provider’s model list; when neither is known, Anthropic gets 8192 and other providers get no cap. 0 is ignored. Extended thinking draws from this budget. |
contextWindowTokens | integer | discovered input limit | Input-token limit used for proactive context compaction. Overrides the discovered limit; compaction is off only when neither is known. 0 is ignored. |
compactAtPercent | integer | 80 | Share of the input limit at which older turns are summarized before the next turn. Clamped to 10–95. When the output cap is known and shares the input window, compaction also triggers early enough that a full-length reply still fits. |
Limits come, in order, from the agent's pins, a [[providers.<name>.models]]
entry in config.toml, the provider's live model list (cached for 1 hour in
~/.rupu/cache/models/), else unknown. Every run writes a
model_limits notice to its transcript saying what it will use and where each value came from.
Pin maxTokens / contextWindowTokens only to override what rupu
discovers — see Reliability → Model limits discovery.
Coverage & findings
| Key | Type | Default | Meaning |
|---|---|---|---|
concerns | list | none | Coverage-concerns block (include: a catalog, or inline concerns). Injects the coverage tools and prepends the concern catalog to the system prompt. A workflow step’s own concerns: wins. |
findingsProfile | full | summary | full | Contract for findings.report (alias report_finding), whether the tool comes from a concerns: block or an explicit tools: grant. full requires a complete structured report and rejects summary / severity / evidence as separate arguments; summary records the lightweight summary / severity / evidence form and refuses a report. Precedence: workflow step → workflow defaults → agent → full; rupu run --findings-profile overrides it for a standalone run. Children started with dispatch resolve only from their own agent file. |
Fallbacks
fallbacks: is an ordered list of models to try when the agent’s own model
can’t give a usable reply or the provider fails the request (a refusal, repeated truncation, an overloaded
provider). Each entry has a model and an optional provider;
without one, the entry means the provider the run started on. Unknown keys inside an entry are an error. The list
replaces [recovery].fallbacks from config.toml
for this agent (it is not merged), and fallbacks: [] means no fallbacks. A cross-provider
entry sends the whole conversation to that provider, so list one only when the run's data may go there. See
Reliability → Configuring fallbacks.
fallbacks: - model: claude-sonnet-5-5 # same provider as the agent - provider: openai-codex # another provider model: gpt-5.6-cyber
Skills
skills: and preloadSkills: grant the agent
Agent Skills installed under ~/.rupu/skills/
or the project's .rupu/skills/. Installing a skill gives it to no agent: an agent without
these keys has no skills, no skill index and no skills.load tool.
skills: [pdf, code-review] # listed in the prompt; the model loads one when a task matches it preloadSkills: [house-style] # its whole body is in the system prompt from the start
| Entry | Grants |
|---|---|
code-review | the skill of that name in whichever layer has it. If both the global and the project layer have it, the agent fails to load until the entry names a layer. |
global:code-review, project:code-review | that layer's skill |
* | every valid global skill (never a project skill) |
project:* | every valid project skill |
A run binds each name once, so granting the same name from both layers (global:x and
project:x, or * and project:* over a
name in both) is an error. An unknown name, an invalid skill or a malformed entry fails the agent's load with a
did-you-mean, and rupu agent validate reports the same.
A granted skill reaches the run as:
- The index. Each skill in
skills:is listed under## Skillsin the system prompt, after the agent's body and any## Run target, with its description. The model callsskills.loadwith a name to get the skill's instructions, base directory and file list. - Preloaded bodies. Each skill in
preloadSkills:is rendered in full under## Skill: <name>, ahead of the index, and is not listed in it. - Its files.
read_file,grep,globandast_grepmay read under a granted skill's directory even outside the workspace;write_fileandedit_filemay not. A skill's scripts run throughbash, under the run's permission mode.
A skill never widens the agent's tools: grant or its permission mode. A child started with
dispatch gets its own agent file's skills, never its parent's. A session fixes its agent's
skills when it starts (see Sessions → Skills).
Messaging
Every run with a message space is offered board.claim, board.release,
board.post, board.read and msg.send by
default. messaging: false turns them off for this agent wherever it runs: as a standalone run, a
workflow step, a sub-agent, a session's agent or an agentiflow's lead or unit. The run is still in its space, so the
operator and its children reach it, and every child it launches has messaging off too. A file that sets
messaging: false and also names a messaging tool in tools: (by name,
alias or board.* / msg.*) fails to load. See
Messaging.
Anthropic-only fields
| Key | Values | Meaning |
|---|---|---|
anthropicOauthPrefix | true | false | SSO (OAuth) only. Enables or disables Anthropic's OAuth system prefix. |
anthropicPromptCache | true | false | Explicit prompt caching, on by default. false drops the cache_control breakpoints (e.g. for a gateway that rejects them). Overrides [providers.<name>] prompt_cache in either direction. |
anthropicTaskBudget | positive integer | Soft output-token budget, separate from maxTurns. |
anthropicContextManagement | tool_clearing | Server-side pruning of older tool blocks. |
anthropicSpeed | fast | Account-gated fast mode. |
thinking,
budget_tokens, thinkingBudget,
context_management or speed are what rupu
sends to a provider, not frontmatter fields — strict parsing rejects them. Use
effort, thinkingDisplay,
anthropicContextManagement and anthropicSpeed.
Example
--- name: deep-auditor description: Long-running audit agent with tuned limits, a skill and a fallback chain. provider: anthropic-work # a named account auth: sso model: claude-sonnet-4-6 tools: [read_file, grep, glob] permissionMode: readonly effort: high maxTokens: 32000 compactAtPercent: 70 concerns: - include: secrets-in-source findingsProfile: summary skills: [code-review] messaging: false fallbacks: - model: claude-sonnet-5-5 # same account - provider: openai-codex # another provider model: gpt-5.6-cyber anthropicContextManagement: tool_clearing --- Audit the repository for hard-coded secrets and record each one you find.
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
tools: is the agent's tool grant: what its model may call. Each entry is one of:
| Entry | Means |
|---|---|
bash, findings.report, scm.prs.get, … | that tool, by its canonical name |
report_finding, query_findings, … | a legacy name, accepted forever as an alias of its canonical tool (see Tool names) |
findings.*, scm.*, issues.*, board.*, … | every tool in that namespace |
core.* | the core tools: bash, read_file, write_file, edit_file, grep, glob, ast_grep |
* | the whole catalog, except the authority tools board.directive and board.retract |
There are no other wildcards (scm*, coverage_* are errors).
Authority is never implied by *. board.directive
and board.retract let one agent direct the others, so * leaves them
out: an agent gets them only by naming them (or board.*), and an agentiflow lead gets them from
its origin grant.
An unknown name is an error. An agent whose tools: names a tool rupu doesn't
know fails to load — rupu run, a workflow step and a sub-agent dispatch all refuse it — with
a suggestion when a known name is close. rupu agent validate catches it ahead of time.
A tool is offered only where it works. The connector tools (scm.*,
issues.*, github.*, gitlab.*) need
configured SCM connectors, coverage.* a concerns: block,
assets.mark an engagement profile, the board and mailbox tools a message space,
goal.* and budget.status an agentiflow lead, and
workflows.generate a provider to author with. dispatch and
join work in every agent run, and so do the roster tools (agents.list,
agents.get, workflows.list, workflows.get,
catalog.search). A wildcard silently leaves out a tool the run can't serve; a tool named exactly
that the run can't serve is left out too, and the run says so with a tool_unavailable notice in
its transcript.
Omitting tools: gives the default grant: the core tools, the launch pair and
every connector tool (core.*, dispatch, join,
scm.*, issues.*, github.*,
gitlab.*). For reusable agents an explicit tools: is better than the default.
Ambient grants. The run's context adds tools the agent didn't list: a concerns:
block adds the coverage tools and findings.report; an active engagement profile adds
findings.report and assets.mark; a message space adds the default
messaging tools; an agentiflow lead's or unit's origin adds its coordination tools. A run granted
dispatch (by any route, a legacy name included) also gets join.
None of this is hidden: every run's transcript opens with a tool_grant event listing exactly
the tools the model was offered and why, and every call is audited (tool_audit).
tools: is this agent's full grant. A
workflow step’s actions: can only narrow the connector subset of that
grant for one step — it never touches any other tool, and never grants a tool beyond what
tools: already allows. See
Step actions.
Tool names
Tools are named namespace.verb (findings.report,
coverage.mark); the core fs/shell tools stay unqualified. Earlier names remain accepted
forever, in tools: and when a model calls them, as aliases:
| Canonical | Also accepted |
|---|---|
findings.report | report_finding, findings.record |
findings.verify | finding.verify |
findings.query / findings.tag | query_findings / tag_findings |
assets.mark | asset_mark |
coverage.mark / coverage.status / coverage.remaining | coverage_mark / coverage_status / coverage_remaining |
coverage.concerns.search / coverage.concerns.detail | coverage_concerns_search / coverage_concerns_detail |
dispatch | dispatch_agent, dispatch_agents_parallel, run_workflow (see Legacy launch names) |
workflows.generate | generate_workflow |
goal.coverage (agentiflow lead) | coverage.status, inside a lead only |
Structural search with ast_grep
ast_grep matches code by its syntax tree rather than its text, so a
pattern finds every call site or definition however it’s formatted. It runs the
ast-grep binary, which must be
on PATH as ast-grep (brew install ast-grep
or cargo install ast-grep); without it every call returns an “ast-grep not found” error.
It is a read tool: readonly mode allows it. It takes a
pattern, a lang (required: any language ast-grep supports, e.g.
rust, python, typescript,
tsx, javascript, go,
java, c, cpp) and an optional
path inside the workspace.
A pattern is ordinary code in the target language, with metavariables where anything may appear:
| Metavariable | Matches |
|---|---|
$NAME | exactly one syntax node, captured as NAME (upper-case). The same name used twice must match identical code. |
$$$ / $$$NAME | zero or more nodes, such as an argument list or a block body; the named form captures them. |
$_ | one node without capturing it. |
| Pattern | lang | Finds |
|---|---|---|
impl $TRAIT for $TYPE { $$$ } | rust | every trait implementation |
$X.unwrap() | rust | every .unwrap() call, whatever it is called on |
subprocess.run($$$, shell=True, $$$) | python | shell-invoking subprocess calls |
fetch($URL, $$$) | typescript | fetch call sites with any options |
$EL.innerHTML = $VALUE | javascript | direct innerHTML assignments |
The agent sees one path:line:col: match line per hit (1-based, workspace-relative); no
matches is empty output, not an error, while a malformed pattern or a bad path surfaces ast-grep's diagnostic as the
error. Under a concerns: block, each matched file is recorded in the
coverage ledger like a grep hit. Each call also
records up to 200 matches as structured data (file, range, matched text and every metavariable's capture), which the
control plane uses to show a source slice and a syntax-tree preview per match.
Permission modes
permissionMode is ask, bypass or
readonly. Every tool declares one effect, and the mode decides each call from
the effect alone — never from the tool's name, so a new tool is gated correctly the day it lands:
| Effect | Examples |
|---|---|
read | read_file, grep, glob, ast_grep, findings.query, coverage.status, scm.prs.get, goal.status, join |
record | rupu's own bookkeeping: findings.report, findings.verify, findings.tag, assets.mark, coverage.mark, board.post, msg.send |
write | changes the workspace or runs code: bash, write_file, edit_file |
external | acts outside this machine: scm.prs.create, scm.branches.create, issues.comment, issues.create, github.workflows_dispatch |
spawn | starts another run or model call: dispatch (and its aliases), workflows.generate |
| Effect ↓ / Mode → | bypass | ask (interactive) | ask (no operator) | readonly |
|---|---|---|---|---|
| read | allow | allow | allow | allow |
| record | allow | allow | allow | allow |
| write | allow | prompt | allow, with a notice | deny |
| external | allow | prompt | allow, with a notice | deny |
| spawn | allow; child keeps its own mode | allow; child capped at ask | allow; child capped at ask | allow; child capped at readonly |
- A child never has more than its parent. A child of any kind runs at the more restrictive of the parent's mode and its own
permissionMode; a readonly run's children are readonly whatever their frontmatter says, across a process boundary too. readonlystill records findings. It means “no change to your workspace or the outside world”; rupu's own ledgers are bookkeeping, so a readonly reviewer can report, tag and verify findings.askwithout an operator (a workflow step, a session turn, a flow, a detached run) cannot prompt, so it allows write/external calls and the transcript says so once with apermission_mode_degradednotice. An interactiverupu runinaskprompts; a non-TTYrupu runinaskis refused at startup, so pass--mode bypassor--mode readonlythere.- “Allow always” (
aat the prompt) allows that one tool for the rest of the run, not every tool.
ask is the safest default for agents that edit code. Pass --mode readonly
for unattended runs that must not change anything. The CLI --mode flag always overrides the value in the file.
Launching children
An agent can start other runs as tool calls in the middle of its own run, keep working while they run, and fold their results back into its context. 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.
Three tools drive it. The legacy names (dispatch_agent, dispatch_agents_parallel, run_workflow, generate_workflow) are aliases that keep their old behavior and output shapes.
dispatch— start a child and return at once with{handle, kind, name, run_id, codename}; withwait: truethe reply also carries the child'sjoinentry.join— collect the results of handlesdispatchreturned (or every unjoined child when omitted), with atimeout_secs(default 300, at most 3600;0just looks) anduntil: all | any.workflows.generate {description}— write a new workflow under the run'sgenerated/directory, check it, and return{file, name, steps, inputs}. It does not start it: run it withdispatch {kind: "workflow", file}.
dispatch field | Meaning |
|---|---|
kind | subagent (default), agent or workflow |
name | The agent, or for kind workflow the workflow, to run |
file | Kind workflow only, instead of name: a file workflows.generate returned in this run |
prompt | An agent child's task. Required for subagent and agent |
inputs | A workflow's declared inputs. For an agent child, structured inputs appended to its prompt as a JSON block |
wait | true starts the child and waits for it in one call. Default false |
timeout_secs | With wait: how long to wait. Default 300, at most 3600 |
kind | Runs as | Its record |
|---|---|---|
subagent | a task in the parent's process; it dies with the parent | transcript under the parent's run: <RUPU_HOME>/runs/<parent run id>/sub/<sub_id>/transcript.jsonl |
agent | its own rupu run process | its own run and transcript, like any rupu run |
workflow | its own rupu workflow run --plain process | its own workflow run under <RUPU_HOME>/runs/<id>/ |
join returns one entry per child: handle, kind,
name, run_id, codename,
status (pending, running,
done, failed, abandoned or
interrupted) and, once it ended, success, output,
error, cause, tokens and
duration_ms. While a run has children it has not joined, each turn starts with a status block
naming them, so the agent knows what is still out without polling. A workflow child is checked before anything starts:
it parses, has no approval gate and no host: / distribute: step,
every agent its steps run is one the caller may dispatch, and its inputs resolve.
Rules every kind shares
- Allowlist. The agents and workflows a run may start are its
dispatchableAgents/dispatchableWorkflows. Inside an agentiflow the flow's pool applies, intersected with the declared lists when the agent has them. Either way the result is capped by what the parent may launch. Neither declared nor in a flow means none:dispatchis still offered, but a call fails withagent_not_dispatchable/workflow_not_dispatchable. - Depth. A run you start is depth 0 and each child is one deeper. A run at depth 5 can't start a child (
max_dispatch_depth_exceeded). - Permission ceiling. A child runs at the more restrictive of the parent's mode and its own
permissionMode. A sub-agent of anaskparent asks the parent's operator; a process child can't reach that prompt, so under an interactiveaskparent it runsreadonly, and under an unattendedaskparent it runsaskunattended. - Connector narrowing. A workflow step's
actions:narrowing reaches the step's children, and a generated workflow may call a connector tool from anaction:step only when the run that generates or starts it is offered that tool itself. - Concurrency. At most
[dispatch].max_concurrent_children(default 8) children of one run run at once; the rest wait aspending. - Engagement and findings. Children run under the parent's engagement profiles; a child's findings profile comes from its own agent file.
- Usage. A sub-agent's calls are recorded in its parent's usage ledger. A process child keeps its own ledger and copies each call into the root run's ledger as a rollup, which the control plane's usage reports skip, so nothing is counted twice.
--- name: writer tools: [read_file, edit_file, dispatch] # join is implied by dispatch dispatchableAgents: [security-reviewer, perf-reviewer] dispatchableWorkflows: [release-checks] --- Draft the change, then dispatch the reviewers on your diff. Revise against their findings before you finish.
When the parent ends
A run's unjoined children are stopped when the run ends and recorded abandoned, and so is
whatever they left below them. A workflow step that pauses, or a rupu run / rupu workflow run
stopped by SIGTERM or SIGINT, stops them the same way but records each interrupted; when that
run's conversation is continued (rupu workflow resume, rupu run --continue),
the continued run takes on every child under the same handles — ended ones as they ended, the rest continued from their
own records. Every launch is appended to the parent's children ledger, <RUPU_HOME>/runs/<run id>/children.jsonl,
which join, the orphan reaper and the control plane's Children tab read. Each child
gets a codename below its parent's (<parent>><role>#n), and a sub-agent can be continued by its
sub_… id like any run.
Legacy launch names
The tools that predate dispatch keep working, in tools: and when a model calls them:
| Old call | Does | Returns |
|---|---|---|
dispatch_agent {agent, prompt, inputs?} | a subagent, waited for | {ok, agent, output, findings, tokens_used, duration_ms, transcript_path, sub_run_id, codename, error?} |
dispatch_agents_parallel {agents: [{id, agent, prompt, inputs?}], max_parallel?} | one subagent per entry, all checked before any starts, all waited for | {ok, results: {<id>: ...}, all_succeeded} |
run_workflow {workflow, inputs?} | a workflow child, not waited for | {handle, participant} |
generate_workflow {description, inputs?} | workflows.generate, then starts it | {handle, participant, generated_file} |
join {handle} | join on one child | {status, success?, output?}, or {status: "failed", error} |
A model only sees dispatch, so a prompt written for an old name arrives as a
dispatch call in the old shape. 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). On its own 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, when
outputFormat: json is also set, rupu hands it to Anthropic's structured-output mode
(output_config.format = {type: "json_schema", schema: ...}), which returns a
guaranteed schema-conforming object. Without outputFormat: json the schema
has no effect; providers other than Anthropic ignore it.
--- 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 uses the provider's default_model from config,
else claude-sonnet-4-6; for stable project behavior, set model:
per agent. rupu run --model / --provider override the file for one run.
Inspect the models rupu knows with rupu models list and refresh the cached model lists
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 scaffolds the .md, with --describe a model drafts the definition, and the file opens 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, validate them, 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 # check every agent file in both layers (tools:, skills:, syntax) rupu agent validate --all # 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. --skill / --preload-skill add skills
to one run on top of the agent's own, and --no-messaging / --messaging-tool
switch its messaging tools off or narrow them. Every run is recorded as a JSONL transcript you can replay later:
# inspect a recorded run
rupu transcript
Continuing a run
If a run stops before it finishes — a closed terminal, a killed process, a crash — pick it up where it left off instead of starting over. rupu rebuilds the conversation from the run's transcript, drops the turn that was in flight, and tells the agent it was interrupted. The run's command is restored too: its repo target and every flag it was launched with; a flag given on the continue replaces only its own setting. Run it from the same directory as the original run.
# continue an interrupted run rupu run code-reviewer --continue <agent_run_id> # continue any ended run with something new to do rupu run code-reviewer --continue <agent_run_id> "now write the fix" # a run that failed on a provider-side outcome (a refusal, say) can move to another model rupu run code-reviewer --continue <agent_run_id> --model <model> [--provider <provider>]
A run that had already finished prints its recorded answer without calling the model; with a prompt it gets a new turn
after its answer. A run that failed can't be continued bare — give the continue a prompt, or, when it failed on a
provider-side outcome, a --model / --provider. The continued run also
takes on the children the interrupted one launched. rupu run pause / rupu run resume
act on workflow runs only; a standalone agent run has no pause boundary.