# Subagents and messaging

> rlm-spawned child agents, agent-message skill surface, direct agent-to-agent communication, and multi-agent orchestration constraints.

- Repository: PrimeIntellect-ai/prime-agent
- GitHub: https://github.com/PrimeIntellect-ai/prime-agent
- Human docs: https://grok-wiki.com/public/docs/primeintellect-ai-prime-agent-3367c32760b1
- Complete Markdown: https://grok-wiki.com/public/docs/primeintellect-ai-prime-agent-3367c32760b1/llms-full.txt

## Source Files

- `packages/coding-agent/examples/extensions/subagent/agents.ts`
- `packages/coding-agent/examples/extensions/subagent/README.md`
- `packages/coding-agent/src/core/agent-messages.ts`
- `packages/coding-agent/skills/agent-message/SKILL.md`
- `packages/coding-agent/test/suite/regressions/617-subagent-terminal-agent-message.test.ts`
- `packages/coding-agent/test/acp-rlm-subagents.test.ts`

---

---
title: "Subagents and messaging"
description: "rlm-spawned child agents, agent-message skill surface, direct agent-to-agent communication, and multi-agent orchestration constraints."
---

Prime Agent runs multi-agent work through two complementary surfaces: native **RLM child sessions** admitted with `await rlm(...)` from the persistent IPython kernel, and **nuclear-family agent messaging** through the built-in `agent_message` skill and daemon routing. Child answers never return from `rlm()`; they arrive later as attributed `agent_message` turns, terminal notices, or files the parent inspects.

## Architecture

```mermaid
flowchart TB
  subgraph parent["Parent AgentSession"]
    kernel["IPython kernel\nrlm / agent_message"]
    host["Host bridge\nrlm.run · agent_message.*"]
    registry["Parent-scoped child registry"]
    session["AgentSession.runRlmChild"]
  end

  subgraph children["Child runtimes"]
    c1["Child AgentSession\nindependent context + session_dir"]
    c2["Sibling / retained child"]
  end

  subgraph daemon["Daemon supervisor"]
    route["send_message routing\nsender identity + safety limits"]
  end

  kernel -->|"host_request"| host
  host --> session
  session --> registry
  session --> c1
  c1 -->|"agent_message.send parent"| route
  c2 -->|"agent_message.send sibling/child"| route
  route -->|"steer into target context"| session
  route -->|"steer into target context"| c1
```

| Surface | What it is | Result path |
| --- | --- | --- |
| `await rlm(prompt, name=..., model=...)` | Admit a recursive child `AgentSession` | Admission handle only (`rlm_child_id`, `name`, `session_dir`, `model`) |
| `await agent_message.send(...)` | Direct message to parent / sibling / child | Receipt (`delivered` or `queued`); body is a custom `agent_message` in the target session |
| `prime-agent send <agent> <message>` | CLI entry into the same daemon path | Printed delivery status / JSON receipt |
| Extension `subagent` tool | Optional file-defined subprocess agents | Tool result from a separate process (not the native RLM registry) |

<Note>
Native RLM children share the TypeScript agent runtime, providers, skills, tools, and session machinery with the parent. The example extension under `packages/coding-agent/examples/extensions/subagent/` is a separate subprocess workflow for markdown agent profiles; prefer `rlm(...)` for built-in recursive orchestration.
</Note>

## Spawn RLM children

### Admission API

The callable `rlm` object is preloaded in the kernel. These forms are equivalent:

```python
handle = await rlm("Review the authentication flow", name="auth-reviewer")
handle = await rlm.run("Review the authentication flow", name="auth-reviewer")
print(handle.rlm_child_id, handle.name, handle.session_dir, handle.model)
```

Supported kwargs:

| Kwarg | Type | Required | Notes |
| --- | --- | --- | --- |
| `name` | string | no | Unique among siblings under the same parent; max 64 chars after trim |
| `model` | string | no | Exact `provider/id` from `await rlm.find_models(...)`; unavailable models fail spawn (no silent fallback) |

Any other kwarg fails with `Unsupported rlm.run kwargs: ...`.

Default names are generated as readable selectors (`subagent-<prompt-slug>-<id-suffix>`) when `name` is omitted. Reserved selectors such as `all` / `*` / `broadcast` are rejected for names because they collide with messaging policy.

### What admission returns

```python
# RLMSpawnHandle — admission only, never the child answer
{
  "rlm_child_id": "sub-…",
  "name": "auth-reviewer",
  "session_dir": "…/artifacts/…/sub-…",
  "model": "provider/model-id",
}
```

Spawn is **fire-and-forget at the call site**: the host creates the child runtime and runs the prompt in detached work. End the parent turn instead of awaiting completion.

```python
api = await rlm("Review the public API", name="api-reviewer")
tests = await rlm("Review test coverage", name="test-reviewer")
# parent turn ends; replies arrive later via agent_message or files
```

### Lifecycle and registry

```python
children = await rlm.list_subagents()
for child in children:
    print(child.session_name, child.status, child.active_session_id, child.rlm_child_id)

await rlm.delete_subagent(children[0])  # id, active session id, session id, or unique name
```

| Status | Meaning |
| --- | --- |
| `running` | Child task still in flight |
| `completed` | Initial task finished; daemon-backed children can remain addressable for follow-ups |
| `error` | Startup or run failed |

Registry scope is **parent-session-local**. It survives compaction, kernel restart, and parent restore. An unrelated new root session does not inherit children. Deletion cancels/closes the runtime and tombstones the registry entry; it does **not** erase transcript or artifact files.

### Inheritance and depth

| Property | Behavior |
| --- | --- |
| Model | Child inherits parent model unless `model=` selects an authenticated executable model |
| Tools / skills / retry / resource loader | Reused from parent runtime configuration |
| Depth | Child receives `RLM_DEPTH + 1`; spawn blocked when `RLM_DEPTH >= RLM_MAX_DEPTH` |
| Default max depth | `1` (root may spawn children; children may not recurse further unless raised) |

Depth resolution order: chat-persisted value → configured/inherited option → global settings `rlmMaxDepth` → env `RLM_MAX_DEPTH` → default `1`.

```text
RLM recursion depth limit reached (RLM_DEPTH=1, RLM_MAX_DEPTH=1)
```

### Child reply doctrine

When `agent_message` is installed, child system doctrine tells the model:

- Task prompts are labeled `[task from parent]`.
- When an answer is required, reply with `await agent_message.send(message, receiver_role="parent")`.
- Not every task needs a reply; cleanup and idle after sending.

If a child finishes without any parent reply, the parent still learns via an attributed agent message when messaging is available, or a fallback `rlm_child_terminal_notice` injection (`completed without sending a reply`). Silent drop is not allowed.

## Agent messaging surface

### Built-in skill

Skill package: `packages/coding-agent/skills/agent-message/`  
Import name: `agent_message`  
Host requests: `agent_message.list_agents`, `agent_message.send`

```python
roster = await agent_message.list_agents()
# roster["current"] → name, id, depth
# roster["entries"] → relationship, name, id, depth, status (+ repliedSinceTask for children)

receipt = await agent_message.send(
    "Please inspect the latest result.",
    receiver_role="child",
    receiver_name="api-reviewer",
)
print(receipt["deliveryStatus"], receipt.get("deliveredAt") or receipt.get("queuedAt"))
```

### Send contract

| Argument | Constraint |
| --- | --- |
| `message` | Non-empty string after trim |
| `receiver_role` | `"parent"` \| `"sibling"` \| `"child"` |
| `receiver_name` | **Omitted** for parent; **required** for sibling and child (name or id) |
| Broadcast | `await agent_message.send("all", "status update")` → family roster only |

Positional session-id targets are rejected. Sender identity is daemon-derived; Python cannot supply a spoofable `from` field.

### Receipt shape

| Field | Values / meaning |
| --- | --- |
| `id` | `agentmsg_<uuid>` |
| `source` | `agent_message` |
| `deliveryStatus` | `delivered` (idle target context accepted) or `queued` (steer accepted while busy) |
| `deliveredAt` / `queuedAt` | ISO timestamps |
| `deliveryMode` | Skill path always steers; receipt records `steer` |
| Broadcast result | `{ "receipts": [ receipt \| { target, error } ] }` — one failure does not cancel others |

`send` does **not** block until a queued message is processed. Do not delete a child immediately after send; wait until observation shows idle and the context is no longer needed.

### Safety limits (daemon)

| Limit | Default |
| --- | --- |
| Max message characters | `16384` |
| Max pending messages per target session | `20` |
| Rate limit capacity | `3` tokens |
| Rate limit refill | `1` token / `1000` ms |

Daemon admin surfaces:

```bash
prime-agent daemon agent-messages status
prime-agent daemon agent-messages pause
prime-agent daemon agent-messages resume
prime-agent daemon agent-messages clear <session>
```

RPC equivalents: `agent_messages_status`, `agent_messages_pause`, `agent_messages_resume`, `agent_messages_clear`, plus `send_message`.

### Nuclear-family reach

Messaging is limited to **parent, siblings, and direct children**. Roots at depth 0 are siblings of each other. Grandchildren and cousins are out of reach:

```text
Agent reach is limited to parent, siblings, and children
```

Relay through an intermediate child for deeper trees. Session names are unique per sibling group (same parent + depth); conflicts raise:

```text
Agent name "api-reviewer" is unavailable: an agent of that name already exists at depth N under this parent
```

### Delivered prompt shape

Target sessions receive a custom message (`customType: "agent_message"`) whose text content looks like:

```text
[from child:api-reviewer]
Agent-to-agent message received.
Source: agent_message
From: api-reviewer, active …, session …
To: …, active …, session …
Message id: agentmsg_…

Please inspect the latest result.
```

## CLI and external routing

```bash
prime-agent send <agent> "Please verify the latest migration"
prime-agent send --from <source-agent> <target-agent> --message "…"
```

From a parent that already holds child handles:

```python
children = await rlm.list_subagents()
child = next(c for c in children if c.session_name == "api-reviewer")
await agent_message.send(
    "Continue with the updated diff",
    receiver_role="child",
    receiver_name=child.session_name,
)
```

Sending to an idle completed daemon-backed subagent starts an ordinary follow-up turn in that same child session/context. The child remains available only until the parent session closes (unless deleted earlier).

## ACP surface for RLM children

In ACP mode, RLM subagent lifecycle is streamed to the client as namespaced metadata under the Prime Agent meta namespace (`subagents` entries with statuses such as `running` and `done`). Fire-and-forget children still surface without requiring a concurrent user prompt turn when the ACP session is subscribed for the session lifetime.

## Optional extension: file-defined subprocess agents

The example extension at `packages/coding-agent/examples/extensions/subagent/` is **not** the native RLM path. It:

- Discovers markdown agents from `~/.prime/agent/agents/*.md` (user) and optionally `.prime/agent/agents/*.md` (project)
- Spawns a separate Prime Agent process per invocation with isolated context
- Supports single, parallel (max 8 tasks, 4 concurrent), and chain modes

Agent definition frontmatter:

```markdown
---
name: scout
description: Fast codebase recon
tools: bash
model: claude-haiku-4-5
---

System prompt body…
```

| Scope | Load path | Default |
| --- | --- | --- |
| `user` | `~/.prime/agent/agents` | Default (safe) |
| `project` | nearest `.prime/agent/agents` | Off unless `agentScope: "project"` or `"both"` |
| `both` | project overrides same-name user agents | Only for trusted repos |

Project-local agents are repo-controlled prompts that can instruct tools including shell/IPython. Interactive confirmation applies before project agents unless disabled.

## Multi-agent orchestration constraints

| Constraint | Rule |
| --- | --- |
| Result channel | Never treat `rlm()` return as the answer; use `agent_message` or files |
| Reach | Parent / sibling / child only; relay for deeper graphs |
| Naming | Sibling names unique under parent; reserved `all`/`broadcast`/`*` blocked |
| Depth | Default max depth `1`; raise via settings/env/chat before nested recursion |
| Broadcast | Family roster only; not a global daemon broadcast |
| Identity | Daemon-derived sender; cannot spoof `from` from Python |
| Queues | Busy targets get steered/queued messages; capacity and rate limited |
| Child deletion | Wait for idle after follow-ups; deletion removes messaging reach, not disk artifacts |
| Usage | Child token/cost folds into the parent assistant turn that spawned it |
| Trust | Kernel and workers use OS permissions of the client; not a security sandbox |

## Common workflows

### Parallel research, fan-in later

```python
a = await rlm("Find auth entry points", name="auth-scout")
b = await rlm("Find provider config paths", name="provider-scout")
# end turn — collect agent_message replies or files children write
```

### Explicit parent follow-up

```python
children = await rlm.list_subagents()
await agent_message.send(
    "Re-check after the new regression landed.",
    receiver_role="child",
    receiver_name="auth-scout",
)
# keep child until idle before delete
# await rlm.delete_subagent("auth-scout")
```

### Child answer path

```python
await agent_message.send(
    "Findings: …",
    receiver_role="parent",
)
```

### Family broadcast status

```python
result = await agent_message.send("all", "stand down — root is compacting")
for item in result["receipts"]:
    print(item)
```

## Failure modes

| Symptom | Likely cause | Action |
| --- | --- | --- |
| `RLM recursion depth limit reached` | Depth at max | Raise `rlmMaxDepth` / `RLM_MAX_DEPTH` if nested spawn is intentional |
| `Requested subagent model "…" is unavailable…` | Bad or unauthenticated selector | `await rlm.find_models(query)` and use an exact returned selector, or omit `model` |
| `Agent name "…" is unavailable…` | Sibling name collision | Rename, delete old child, or pick another name |
| `Broadcast agent messaging is not supported` | Target was `*` / `all` / `broadcast` as a direct target without the `send("all", msg)` form | Use role addressing or `send("all", message)` |
| `Agent reach is limited to parent, siblings, and children` | Target outside nuclear family | Relay through intermediate child |
| `Target session has too many pending messages` | Queue at `20` unfinished | Wait for target idle; avoid message storms |
| Rate-limit reject | Sender bucket exhausted | Back off ~1s per refill token |
| `agent messaging is not available in this session` | No controller (e.g. non-daemon / skill filtered) | Use daemon-backed session; ensure skill not filtered out |
| Child finished, parent saw nothing | Delivery failed | Parent still gets fallback terminal notice; check messaging controller health |
| Extension `Unknown agent: "…"` | Markdown agent not discovered | Install under user agents path or enable project scope deliberately |

## Verification signals

| Check | Expected |
| --- | --- |
| Spawn | Immediate handle with non-empty `rlm_child_id` and `name` |
| List | New entry appears in `rlm.list_subagents()` with `status` `running` then `completed`/`error` |
| Reply | Parent transcript shows custom `agent_message` with `[from child:…]` (or parent relationship label) |
| Terminal silence | Parent still receives completion notice text containing `completed without sending a reply` |
| ACP | Client `session/update` meta includes subagent lifecycle entries |
| CLI send | `Sent to <name>` or `Queued for <name>` (or JSON receipt with `deliveryStatus`) |

## Related pages

<CardGroup>
  <Card title="RLM control plane" href="/rlm-control-plane">
    Persistent IPython control tool, prompt-as-variable context, and rlm(...) admission model.
  </Card>
  <Card title="Long-running tasks" href="/long-running-tasks">
    Goals, compaction, heartbeats, autonomous mode, and retained subagents across disconnects.
  </Card>
  <Card title="Built-in skills reference" href="/builtin-skills">
    Catalog entry for agent-message and related orchestration skills.
  </Card>
  <Card title="Sessions and runtime" href="/sessions-runtime">
    Session lifecycle, queueing, tree navigation, and session-scoped vs durable state.
  </Card>
  <Card title="Extensions and custom tools" href="/extensions">
    Extension registration and the sample subagent tool patterns.
  </Card>
  <Card title="Run daemon-backed sessions" href="/daemon-sessions">
    Detach/reattach, resume selectors, and worker recovery for multi-agent work.
  </Card>
</CardGroup>
