# Agents
URL: /docs/building-apps/capabilities/agents
Markdown: /docs/building-apps/capabilities/agents.md
Description: Define LLM agents that belong to your app and call them from your own code.

An app-private agent is an LLM agent set up in an app's YAML. By convention
you call your own agents through `agentRunner.run`, and reach agents you don't
own through the platform `system:summon` action. Each agent states a capability tier,
an allowlist of built-in tools, an allowlist of actions it may call, and, if
you want, a JSON Schema for structured output. The daemon turns the tier into a real model, checks every tool and
action call against the allowlists, and streams the agent's work back to the
caller as a series of `AgentMessage` events.

Use an app-private agent when a task needs open-ended LLM thinking over context
the app gathers — research, triage, summarizing, multi-step planning — rather
than one fixed operation (which should be an action).

## Declaring an agent [#declaring-an-agent]

Agents are set up as YAML files under `src/agents/<name>.yaml` and listed in
`app.yaml`:

```yaml
# app.yaml
formatVersion: 2
id: research-app
agents:
  - agents/planning.yaml
  - agents/coding.yaml
```

A single agent file:

```yaml
name: researcher
description: Researches a given topic and produces a structured summary
tier: medium                   # Capability tier: large | medium | small
reasoningEffort: high          # Optional: low | high | xhigh; defaults to high
permissionMode: acceptEdits    # acceptEdits | bypassPermissions | default
maxTurns: 30                   # Optional: max conversation turns; recommend >= 30
tools:                         # Built-in tool allowlist
  - Read
  - WebSearch
  - WebFetch
actions:                       # Action allowlist; "*" opens everything
  - system:fetch_channel_history
  - research-app:notes_create
allowedSubagents:              # Optional: sub-agents this agent may `summon`
  - assistant:explore
systemPromptPrefix: |
  You are the researcher agent. Read the input topic and context, run
  WebSearch when needed, and finish by calling submit_output.
```

The checks are strict: unknown fields in an agent YAML are rejected at
pack/install. The table lists the commonly used fields; the schema also
accepts `mcpServers`, `networkDiscovery`, `codeBacked`, and the legacy
`model: opus|sonnet|haiku` spelling of `tier`.

### Fields [#fields]

| Field                | Type                                          | Required | Purpose                                                                                                                  |
| -------------------- | --------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `name`               | string                                        | yes      | Local agent name. It must not contain `:`; runtime references use `<app-id>:<name>`.                                     |
| `description`        | string                                        | yes      | Short summary of what the agent does.                                                                                    |
| `tier`               | `large \| medium \| small`                    | yes      | A capability tier that doesn't name a provider. The daemon maps it to a real model for the current provider.             |
| `reasoningEffort`    | `low \| high \| xhigh`                        | no       | How much reasoning the model should use. Defaults to `high`.                                                             |
| `systemPromptPrefix` | string                                        | yes      | The app agent's complete base system prompt.                                                                             |
| `tools`              | string\[]                                     | yes      | Built-in tool allowlist (e.g. `Read`, `Write`, `Edit`, `Grep`, `Glob`, `WebSearch`, `WebFetch`).                         |
| `actions`            | string\[]                                     | no       | Action allowlist; defaults to none. `"*"` opens every action; otherwise list each canonical `<app-id>:<name>` action ID. |
| `permissionMode`     | `acceptEdits \| bypassPermissions \| default` | yes      | File-edit permission mode.                                                                                               |
| `maxTurns`           | number                                        | no       | Cap on conversation turns. Recommended `>= 30`.                                                                          |
| `allowedSubagents`   | string\[]                                     | no       | Canonical `<app-id>:<name>` IDs of sub-agents this agent may `summon`.                                                   |
| `outputSchema`       | JSON Schema                                   | no       | See [Structured output](#structured-output) below.                                                                       |

### systemPromptPrefix [#systempromptprefix]

For an app-owned agent, `systemPromptPrefix` is the complete base prompt. Rome
does not add its core Charter, guardian identity, command-line guidance,
installed-app catalog, or other core-agent instructions. This keeps the
agent's role and behavior under the app developer's control.

Rome still appends contracts that implement features the app explicitly uses,
such as the `submit_output` instructions for `outputSchema` or a handback. A
caller-provided `contextSuffix` is also appended for that session. Put any
platform context or tool-use guidance your agent needs directly in
`systemPromptPrefix`.

### tier [#tier]

`tier` is a capability tier that doesn't name a provider (`large`, `medium`, or
`small`). The daemon turns it into a real model based on the active provider, so
the agent YAML never names a model itself.

### reasoningEffort [#reasoningeffort]

`reasoningEffort` controls how much reasoning the selected model should use.
It is independent of `tier`: a `small` or `medium` agent still uses `high`
effort unless you set this field. Valid values are `low`, `high`, and `xhigh`;
omitting the field defaults it to `high`.

### tools and actions allowlists [#tools-and-actions-allowlists]

The `tools` list controls which built-in tools the agent can use. The `actions`
list controls which actions the agent can call through LLM tool-use during a
run. These are the same allowlists the agent session uses to check every tool
call — the agent picks the allowed actions itself as it thinks; you do **not**
set its tool-call order by hand in code.

`actions: ["*"]` gives access to all actions. Be careful with broad
permissions: pairing `permissionMode: bypassPermissions` with tools that reach
the outside world, like `WebFetch`, is a bad idea. Keep `bypassPermissions` for
agents you fully trust, or that are read-only or sandboxed; otherwise use
`acceptEdits`.

### permissionMode [#permissionmode]

Controls how file-edit permissions work for the agent:

* `default` — ask for permission as usual.
* `acceptEdits` — auto-accept file edits.
* `bypassPermissions` — skip permission checks completely. The platform `main`
  orchestrator runs in this mode, but it is a trusted agent with full access.

### maxTurns and allowedSubagents [#maxturns-and-allowedsubagents]

`maxTurns` caps how many conversation turns the agent may take; set it to
`>= 30` for any agent that does real work. `allowedSubagents` uses canonical
agent IDs for the sub-agents this agent may `summon` — for example,
`coding:planning`, `assistant:assistant`, and `assistant:explore`.

## Running an agent [#running-an-agent]

From a backend action, get `agentRunner` off the dependency box and call `run`.
It returns an `AsyncIterable<AgentMessage>` you should read all the way to the
end. The turn runs whether or not you consume the stream — leaving the loop
early just abandons it, so you stop observing and miss the final `result` /
`structured_output` events.

```ts
import {
  createAppLogger,
  type Action,
  type ActionConfig,
  type ActionResult,
  type AgentRunnerInterface,
  type AppActionRuntimeDeps,
} from "@rome-os/app-runtime";

const log = createAppLogger("research-and-summarize");

interface Deps {
  agentRunner: AgentRunnerInterface;
}

export function createAction(
  config: ActionConfig,
  deps: AppActionRuntimeDeps<Deps>,
): Action {
  const { agentRunner } = deps;

  return {
    config,
    inputSchema: {
      type: "object",
      properties: { topic: { type: "string" } },
      required: ["topic"],
    },
    async execute(args): Promise<ActionResult> {
      let finalText = "";
      let sessionId: string | undefined;

      for await (const msg of agentRunner.run({
        agentName: "research-app:researcher",
        prompt: `Research the topic: ${args.topic as string}`,
      })) {
        switch (msg.type) {
          case "turn_start":
            sessionId = msg.sessionId; // capture to resume the session later
            break;
          case "text":
            // streaming/in-turn text — forward to a UI if desired
            break;
          case "result":
            finalText = msg.content;
            break;
          case "error":
            log.error("researcher failed", { error: msg.error });
            return { status: "error", error: msg.error };
        }
      }

      return { status: "ok", data: { summary: finalText } };
    },
  };
}
```

`RunParams` takes `agentName` and `prompt`, plus optional fields:
`channelThreadKey`, `threadContext`, `sessionId` (resume an existing session),
`contextSuffix`, `sharedContext` (extra context shown to the agent), and
`workingDir`.

### The AgentMessage event types [#the-agentmessage-event-types]

Every event in the stream falls into exactly one of three groups. Check
`msg.type`.

| Group     | Types                                                                                 | What they mean                                                     |
| --------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Lifecycle | `turn_start`, `turn_end`, `session_init`                                              | Edge events that describe the stream itself.                       |
| Content   | `thinking`, `text`, `tool_use`, `tool_result`, `structured_output`, `result`, `error` | Lasting blocks made within a turn.                                 |
| Transient | `text_delta`                                                                          | A live preview of a `text` block still being written; never saved. |

**Lifecycle.** A turn's stream has a clear start and end. `turn_start` is the
first event and carries `turnId`, `sessionId`, and `userPrompt` — read these
here, not from `session_init`. `turn_end` is the last event and carries
`turnId`, `status` (`completed | interrupted | error`), and `durationMs`. Each
stream has exactly one `turn_start`/`turn_end` pair (its own); a sub-agent's
`turn_start`/`turn_end` are never passed into the parent stream, though
sub-agent *content* is, tagged with an `agent` field (`StreamAgentMessage`),
and so is a sub-agent's `session_init`. `session_init`
describes the session (`sessionId`, optional `systemPrompt`, `projectPath`).

**Content.** These are the lasting blocks:

* `thinking` — reasoning content (`content`).
* `text` — narration or answer text (`content`); optional `turnPhase` is
  `commentary` (in-turn narration) or `final` (closing answer).
* `tool_use` — a tool call the model made (`id`, `tool`, `input`).
* `tool_result` — the result of a tool call (`toolUseId`, `tool`, `output`).
* `structured_output` — a checked structured payload (see below).
* `result` — the final block: the agent's answer (`content`), with optional
  `accounting` carrying provider token usage. At most one final block (`result`
  or `error`) per agent per turn.
* `error` — the final failure block (`error`), with optional `accounting`.

**Transient.** `text_delta` (`content`) is a piece-by-piece preview of a `text`
block still being written. The full `text` block still comes after, so readers
that only care about whole blocks (trace, saving, accounting) must ignore it.
Only providers that support partial output send it.

### Reading to the end [#reading-to-the-end]

A typical read: grab `sessionId` from `turn_start` if you want to resume the
session, pass along or collect content blocks, and take the final answer from
the `result` block's `content`. Always loop over the whole stream — leaving the
`for await` loop early doesn't stop the turn; it keeps running underneath while
you no longer see its events, including the final `result`.

## Structured output [#structured-output]

When the agent should return structured JSON instead of free text, set an
`outputSchema` (a JSON Schema) on the agent. The daemon gives the agent a
built-in `submit_output` tool; the agent must call it with a payload that
matches the schema. Submissions that don't match come back to the agent as tool
errors until they do.

```yaml
name: sentinel
description: Triages incoming messages from untrusted senders
tier: small
permissionMode: bypassPermissions
tools: [Read, Glob, Grep]
actions: ["*"]
outputSchema:
  type: object
  required: [decision, reason]
  additionalProperties: false
  properties:
    decision:
      type: string
      enum: [REPLY, ESCALATE, IGNORE]
    reason:
      type: string
systemPromptPrefix: |
  You are a triage agent. For each message decide REPLY / ESCALATE / IGNORE.
  Once decided, call submit_output with your decision and reasoning.
```

### The structured\_output stream event [#the-structured_output-stream-event]

After a `submit_output` payload passes the schema check and is accepted, the
agent session sends a `structured_output` event whose `payload` is the checked
object. Because the check has already passed, readers can trust the payload — it
is not the same as the raw `tool_use` event for the `submit_output` call, which
is only the model's proposal.

```ts
for await (const msg of agentRunner.run({
  agentName: "research-app:sentinel",
  prompt,
})) {
  if (msg.type === "structured_output") {
    const { decision, reason } = msg.payload as {
      decision: "REPLY" | "ESCALATE" | "IGNORE";
      reason: string;
    };
    // `decision` is guaranteed to be one of the enum values
  }
}
```

## Reusing agents [#reusing-agents]

`agentRunner.run` is the right entry point for your **own** app's agents. To
call an agent your app does not own — including another app's agent — go through
the platform `system:summon` action instead of calling `agentRunner.run` yourself:

```ts
const result = await appContext.runAction("system:summon", {
  agentName: "assistant:explore",
  prompt: "Give an overview of the skills and actions relevant to this task.",
  // sessionId?  — optional, to continue an existing session
});
```

### summon vs agentRunner.run [#summon-vs-agentrunnerrun]

| Use                                                                        | When                                                                                                                                                                                                                                                           |
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agentRunner.run({ agentName, prompt, ... })`                              | Running an agent your app declares. Pass its canonical `<your-app-id>:<local-name>` ID. Streams `AgentMessage` events you read directly; use it when you need the live event stream (passing along tokens, reading `structured_output`, grabbing `sessionId`). |
| `appContext.runAction("system:summon", { agentName, prompt, sessionId? })` | Calling any agent you do not own (another app's agent, or a platform agent). `agentName` is canonical. Returns one `ActionResult` — check `status` before reading `data`.                                                                                      |

Don't call `agentRunner.run` on agents that aren't yours; reach them through
`system:summon`. An agent's ability to summon another agent is also limited by its
`allowedSubagents` allowlist.