# anima -- Interface Specification

This document defines the public interfaces of the anima agent runtime.

anima runs LLM agents with lifecycle management. It takes a compiled
PersonaSpec (from larva or hand-written) and a JobSpec (from nervus or
CLI flags), configures a PydanticAI agent, manages the agent loop, and
enforces budget/turn/depth limits.

Canonical data formats are defined by JSON schemas in `contracts/`.
This document references those schemas and describes anima-specific
behavior. **For full field definitions, see the schemas. This document
covers anima-relevant fields and behavioral semantics only.**

---

## A. CLI Interface

All commands support `--json` for machine-readable JSON output on stdout.
CLI exit codes (0-2) are per-command status indicators. Error codes
(300-range) appear in `--json` output. They are separate namespaces.

### Launch (run, spawn)

#### `anima run [OPTIONS] TASK`

Run a one-off (ephemeral) agent task. Blocks until completion or timeout.
Lifecycle is always `ephemeral` -- use `anima spawn` for background agents.
TASK is required (the task string passed to the agent).

| Flag | Type | Description |
| ---- | ---- | ----------- |
| `--persona-file` | path | PersonaSpec JSON file (see Section E) |
| `--job-file` | path | JobSpec JSON file (see Section E). CLI flags override JobSpec fields. |
| `--model` | str | LLM model identifier (overrides PersonaSpec.model) |
| `--llm-base-url` | url | LLM API base URL (e.g. `http://localhost:4000/v1`). If not set, uses provider default from PydanticAI. |
| `--mcp` | str (repeatable) | MCP server: `"command args"` (stdio) or `http[s]://url` (SSE). Disambiguated by URL prefix. |
| `--tools` | str (repeatable) | Tool preset name from config.toml `[tools.<name>]` section. Resolves to MCP server configuration. See Section A.2. |
| `--timeout` | str | Wall clock timeout, e.g. `5m`, `2h` (overrides JobSpec.timeout) |
| `--max-turns` | int | Max agent turns (overrides JobSpec.max_turns) |
| `--max-tokens` | int | Token budget (see Section H.2 for budget precedence) |
| `--context-mode` | str | `isolated` (default) or `shared` |
| `--context-bundle` | path | ContextBundle JSON file (when `--context-mode shared`) |
| `--instance-id` | str | Idempotency key. If agent with this ID already completed, returns cached result. |
| `--memory-provider` | str | Memory provider: `none` (default), `local`, or `mcp`. See Section F. |
| `--completion-policy` | str | `explicit` (default) or `auto` (see Section D.6) |
| `--token-file` | path | CapabilityToken JSON file (see Section E.5, Section H.5). In standalone mode (no nervus), omit this flag. |
| `--sandbox` | flag | Enable sandbox for all MCP stdio servers |
| `--no-sandbox` | flag | Force disable sandbox (overrides config) |
| `--sandbox-allow` | str (repeatable) | Grant capability: `fs:<path>`, `net`, `net:<domain>`. Implies `--sandbox`. |
| `--json` | flag | Machine-readable output |

The following JobSpec fields can only be set via `--job-file` (no CLI flag):
`finalize_grace_budget`, `max_depth`, `side_effect_policy`.

Exit codes: 0 success, 1 error (see Section G for error codes).

#### `anima spawn [OPTIONS] TASK`

Spawn a background agent. Returns immediately with agent ID.

Same flags as `anima run`, plus:

| Flag | Type | Description |
| ---- | ---- | ----------- |
| `--lifecycle` | str | `ephemeral` (default) or `daemon` (see Section H.3) |

Exit codes: 0 spawned, 1 error.

#### A.1. Sandbox Configuration

Anima uses `sandbox-runtime` (srt) for MCP stdio server isolation (not HTTP/SSE servers).

**CLI Flags:**

```
--sandbox                          Enable sandbox for all MCP stdio servers
--no-sandbox                       Force disable (overrides config)
--sandbox-allow fs:<path>          Grant filesystem access to path tree
--sandbox-allow net                Grant all network access
--sandbox-allow net:<domain>       Grant network access to specific domain
```

`--sandbox-allow` applies globally to all sandboxed MCP servers (threat model is agent, not individual tools). `--sandbox-allow` implies `--sandbox`.

**Config Integration:**

```toml
# config.toml
[tools.filesystem]
command = "npx"
args = ["@anthropic/mcp-filesystem", "--root", "/tmp"]
sandbox = true                    # boolean (enable for this preset)
sandbox_allow = ["fs:/tmp"]       # same syntax as CLI

[tools.search]
command = "uvx"
args = ["mcp-web-search"]
sandbox = true
sandbox_allow = ["net"]
```

**State Truth Table:**

| `--sandbox` | `--no-sandbox` | config `sandbox` | Result |
|:-----------:|:--------------:|:-----------------:|:------:|
| — | — | — | off |
| — | — | true | on |
| — | yes | true | **off** (--no-sandbox wins) |
| yes | — | — | on |
| yes | — | false | on (CLI wins) |
| yes | yes | — | **error** (contradictory) |

**Merge Behavior:**

CLI `--sandbox-allow` and config `sandbox_allow` are **unioned** (additive):

```bash
# config has sandbox_allow = ["fs:/tmp"]
anima run --sandbox --sandbox-allow net --tools filesystem "task"
# Effective: fs:/tmp + net
```

**Default Sandbox Policy:**

| Capability | Default |
|------------|---------|
| Filesystem read | Deny sensitive paths (~/.ssh, ~/.aws, ~/.gnupg, ~/.config) |
| Filesystem write | Deny all except /tmp |
| Network outbound | Deny all |
| Process spawning | Deny |

**PersonaSpec.sandbox:**

Reserved field (`str | None`) for future agent-process sandboxing. Does NOT participate in MCP sandbox decisions.

#### A.2. Tool Preset Configuration

Tool presets define reusable MCP server configurations in `config.toml`. Use `--tools <name>` to activate a preset.

**Example:**

```toml
# config.toml
[tools.filesystem]
command = "npx"
args = ["@anthropic/mcp-filesystem", "--root", "/tmp"]
sandbox = true                    # Enable sandbox for this preset
sandbox_allow = ["fs:/tmp"]       # Grant filesystem access

[tools.shell]
command = "uvx"
args = ["mcp-shell-server"]
# sandbox omitted → default false
```

**Resolution:**

When `--tools <name>` is specified:

1. anima reads `[tools.<name>]` from `~/.anima/config.toml` (or `ANIMA_CONFIG` path)
2. Each preset expands to an MCP server configuration:
   - `command`: Executable (required)
   - `args`: Arguments list (optional)
   - `description`: Human-readable description (optional)
   - `sandbox`: Boolean to enable sandbox (optional)
   - `sandbox_allow`: Capabilities list (optional, same syntax as `--sandbox-allow`)

**Multiple presets:**

```bash
anima run --tools filesystem --tools shell 'task'
# Equivalent to:
anima run --mcp "npx @anthropic/mcp-filesystem --root /tmp" \
            --mcp "uvx mcp-shell-server" 'task'
```

**Precedence for sandbox:**

```
--no-sandbox          → off (overrides everything)
--sandbox             → on (CLI override)
config tools.X.sandbox → on for that tool preset
default               → off
```

No PersonaSpec participation in sandbox decisions.

See `config.toml.example` for configuration templates.

### Observe (list, status, inspect, logs, tree)

### Tools (list, init, inspect)

#### `anima tools list [--json]`

List configured tool presets from `~/.anima/config.toml`.

| Flag | Type | Description |
| ---- | ---- | ----------- |
| `--json` | flag | Machine-readable output |

Output columns: `NAME`, `COMMAND`, `DESCRIPTION`, `SANDBOX`.

If no presets configured, displays guidance for adding presets to config.

---

#### `anima tools init [PRESET_NAME] [--json]`

Initialize a tool preset configuration.

| Argument | Type | Description |
| -------- | ---- | ----------- |
| `PRESET_NAME` | str | Optional name for the preset |

| Flag | Type | Description |
| ---- | ---- | ----------- |
| `--json` | flag | Machine-readable output |

Creates scaffolding for a new tool preset in configuration.

---

#### `anima tools inspect [--agent <id>] [--json]`

Inspect tools for an agent or current configuration.

| Flag | Type | Description |
| ---- | ---- | ----------- |
| `--agent` | str | Agent ID to inspect tools for |
| `--json` | flag | Machine-readable output |

Shows tool configurations and available tools for the specified agent or the current global configuration if no agent specified.

---
#### `anima list [--state <filter>] [--json]`

List all tracked agents.

| Flag | Type | Description |
| ---- | ---- | ----------- |
| `--state` | str | Filter by state: `running`, `completed`, `failed`, `killed`, `suspended`, `awaiting_approval` |
| `--json` | flag | Machine-readable output |

Output columns: `AGENT_ID`, `STATE`, `PERSONA`, `LIFECYCLE`, `UPTIME`, `TOKENS_USED`.

#### `anima status <instance_id> [--json]`

Show details for a single agent. Reads from state.json on disk. Does
not require MCP connection to the agent process. For live interaction,
use `anima talk`.

Output fields: instance_id, state, persona_name, lifecycle, token_usage
(prompt/completion/total), tools_called (list with counts), uptime,
parent_id (if sub-agent), children (list), error (if failed).

#### `anima inspect <instance_id> [--json]`

Show full agent snapshot, including identity, state, hierarchy,
process metadata, budget, token usage, tools called, and timestamps.

#### `anima logs <instance_id> [--tail <int>] [--follow] [--json]`

Read structured event logs (`log.jsonl`) for an agent.

| Flag | Type | Description |
| ---- | ---- | ----------- |
| `--tail` | int | Show only last N events |
| `--follow` | flag | Stream new events (tail -f style) |
| `--json` | flag | Output raw JSON lines |

#### `anima tree [--state <filter>] [--json]`

Show parent/child hierarchy for all tracked agents as a tree view.

### Control (approve, reject, resume, kill)

#### `anima approve <instance_id> [--note <str>] [--json]`

Approve an agent approval request. Agent must be in
`awaiting_approval` state.

#### `anima reject <instance_id> [--reason <str>] [--json]`

Reject an agent approval request. Agent must be in
`awaiting_approval` state.

#### `anima resume <instance_id> [--json]`

Manually resume a suspended agent. Agent must be in `suspended` state.
Clears the suspend condition and transitions to `running`.

#### `anima kill <instance_id> [--json]`

Kill a running/suspended/awaiting-approval agent. Sends interrupt signal;
agent enters grace finalization, then `killed` state.

**Killable states:** `running`, `suspended`, `awaiting_approval`.
**Not killable:** `pending` (no process to kill), `completed`, `failed`, `killed` (already terminal).

### Monitor (monitor, serve, talk)

#### `anima monitor`

TUI dashboard (Rich Live) showing all agents with live updates.
Displays agent table with state, token usage, uptime. Press `q` to quit.

#### `anima serve [--port <int>] [--host <str>] [--json]`

Start anima as a remote node, exposing an SSE MCP server for remote
agent management (see Section C).

| Flag | Type | Description |
| ---- | ---- | ----------- |
| `--port` | int | Listen port (default: 8420) |
| `--host` | str | Bind address (default: 127.0.0.1) |
| `--json` | flag | Machine-readable startup output |

#### `anima talk <instance_id>`

Open an interactive session with a running agent. Connects to the agent's
MCP server via Unix domain socket (see Section B).

Sends messages via `anima.interact()`. Ctrl-C to detach without killing.

Requires agent to be in `running`, `suspended`, or `awaiting_approval` state.

When state is `awaiting_approval`, `anima talk` remains available so a human can
ask clarifying questions before responding with `anima approve` / `anima reject`.

### Cleanup (rm, prune)

#### `anima rm <instance_id> [--force] [--json]`

Delete persisted agent state for one agent.
- Default: only terminal-state agents (`completed`, `failed`, `killed`) are removable.
- `--force`: if non-terminal, performs kill then remove.

#### `anima prune [--state <filter>...] [--older-than <days>] [--dry-run] [--yes] [--json]`

Batch-clean terminal agents, optionally filtered by terminal state
and age threshold.

---

## B. MCP Server Interface (per agent process)

Every spawned agent process exposes an MCP server on a Unix domain
socket at `$ANIMA_STATE/agents/<instance_id>/mcp.sock`. This is how
`anima talk` and `anima kill` communicate with running agents.

(`anima status` and `anima list` read state.json directly and do NOT
use MCP. They work even if the agent process is dead.)

### anima.status()

Returns the agent's current state and metrics (live, from the process).

**Parameters:** none

**Returns:**
```json
{
  "instance_id": "string",
  "state": "pending | running | suspended | awaiting_approval | completed | failed | killed",
  "persona_name": "string",
  "lifecycle": "ephemeral | daemon",
  "token_usage": {
    "prompt_tokens": 0,
    "completion_tokens": 0,
    "total_tokens": 0
  },
  "tools_called": [
    {"name": "string", "count": 0}
  ],
  "uptime_seconds": 0,
  "parent_id": "string | null",
  "current_turn": 0,
  "max_turns": 0
}
```

### anima.interact(message)

Send a message to the agent. The message is injected into the agent's
conversation as a user message and the agent processes one turn.

**Parameters:**

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `message` | string | yes | Message to send to the agent |

**Returns:**
```json
{
  "instance_id": "string",
  "state": "pending"
}
```

### anima.interrupt()

Request the agent to stop. The agent finishes its current tool call
(if any), enters grace finalization (one final `ctl_finish` call with
grace budget), then transitions to `killed` state.

**Parameters:** none

**Returns:**
```json
{
  "acknowledged": true
}
```

### anima.approve(note?)

Approve an agent approval request. Transitions the agent from
`awaiting_approval` to `running` state.

**Parameters:**

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `note` | string | no | Optional note from the approver |

**Returns:**
```json
{
  "instance_id": "string",
  "acknowledged": true
}
```

### anima.reject(reason?)

Reject an agent approval request. Transitions the agent from
`awaiting_approval` back to `running` state, allowing the agent to
re-plan based on the rejection feedback.

**Parameters:**

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `reason` | string | no | Optional reason for rejection |

**Returns:**
```json
{
  "instance_id": "string",
  "acknowledged": true
}
```

### anima.resume()

Manually resume a suspended agent. Clears the suspend condition
and transitions the agent from `suspended` to `running` state.

**Parameters:** none

**Returns:**
```json
{
  "instance_id": "string",
  "acknowledged": true
}
```

### anima.logs(tail?)

Retrieve structured event logs from the agent's `log.jsonl` file.

**Parameters:**

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `tail` | integer | no | Number of most recent events to return |

**Returns:**
```json
{
  "instance_id": "string",
  "entries": [
    {
      "ts": "2026-02-28T10:30:01Z",
      "event": "state_change",
      "from": "pending",
      "to": "running"
    }
  ],
  "count": 1
}
```

### B.1. SSE Streaming Endpoint

When `anima talk` connects to an agent, it first attempts to establish an SSE
(Server-Sent Events) stream for progressive output. If streaming is unavailable
(older agents), it falls back to blocking `anima.interact` responses.

#### GET /stream

Establishes a text/event-stream connection for real-time token output during
interact turns.

**Endpoint**: `GET http://localhost/stream` (same Unix domain socket as MCP)

**Availability**: Up to 8 concurrent streams per agent. Returns 404 if agent doesn't
support streaming, 409 if max subscribers reached (8 active streams).

**Event Types**:

1. **chunk** — Progressive text delta
```json
{
  "turn": 12,
  "seq": 41,
  "chunk": "partial text"
}
```

2. **done** — Turn completed successfully
```json
{
  "turn": 12,
  "seq": 42,
  "done": true,
  "stop_reason": "complete",
  "final_response_chars": 1834,
  "usage": {
    "prompt_tokens": 123,
    "completion_tokens": 456,
    "total_tokens": 579
  }
}
```

Valid `stop_reason` values: `"complete"` (normal completion), `"stopped"` (via `/stop`).

3. **error** — Turn failed
```json
{
  "turn": 12,
  "seq": 42,
  "error": "human readable message",
  "code": "INTERNAL"
}
```

**Sequence Rules**:
- `seq` must be strictly increasing within a turn
- Exactly one terminal event (`done` or `error`) per stream
- `chunk` events may coalesce multiple model tokens (best-effort granularity)

**Stop Mid-Stream Behavior**:

When user types `/stop` during streaming:
1. Talk client calls `anima.stop_turn()` (sets stop flag)
2. Agent detects stop at next node boundary (cooperative cancellation)
3. Agent preserves already-emitted chunks
4. Agent emits terminal `done` event with `stop_reason: "stopped"`
5. Agent continues running and accepts new messages

The agent process continues running after `/stop` — only the current turn is
canceled. User can send new messages or `/quit` to detach.

**Per-Consumer Queue & Drop Policy**:

Each stream consumer has its own bounded queue (maxsize=32 events):
- Non-terminal events (`chunk`) are dropped when queue is full (slow consumer)
- Terminal events (`done`/`error`) are never dropped — always delivered
- This prevents slow consumers from blocking the producer or other consumers

**Backpressure**:

- Producer does not block on full queue — drops non-terminal events silently
- Consumer receives gaps in `seq` when chunks are dropped (best-effort notification)
- Terminal events ensure clean stream termination even under backpressure

**Compatibility (Single-Consumer Legacy)**:

Agent version detection:
- Multi-stream capable agents (current): Support up to 8 concurrent `/stream` connections
- Single-stream agents (older <0.2.0): Return 409 on second connection attempt

Client behavior (`anima talk`):
1. Probe `GET /stream` once per session (first interact turn)
2. If HTTP 404/405 or connection failure: cache `streaming_supported=false`
3. For subsequent turns: use blocking mode if streaming disabled
4. For streaming-enabled: establish SSE connection, consume events until
   terminal (`done`/`error`), then close stream

**409 Conflict Response**:

When a ninth client attempts to connect to `/stream` while 8 streams are already active:
```json
{
  "error": {
    "code": "STREAM_CONFLICT",
    "message": "Maximum subscribers reached (8)"
  }
}
```

**Stream Lifecycle**:
- Open: Client establishes SSE connection at interact start
- Active: Server yields `chunk` events as LLM produces output
- Terminal: Server yields single `done` or `error` event, then closes stream
- Cleanup: Server resets stream consumer count on disconnect

**Cleanup Guarantees**:
- On terminal `done`: consumer count decremented, queue cleared
- On terminal `error`: consumer count decremented, error logged
- On client disconnect: stream count decremented, producer may drop future chunks

**Spec Reference**: Full transport design in `docs/proposals/talk-streaming-transport.md`

**Implementation**: `src/anima/shell/mcp_agent.py` (AgentMCPServer class)
- Per-consumer queue: `asyncio.Queue[StreamEvent]` with maxsize=32
- Max concurrent streams: 8 (enforced at connection time)
- Producer: `_process_interact_message` computes deltas, enqueues events
- Consumer: `_handle_stream_request` SSE generator drains events
- Cleanup: Terminal events decrement consumer count automatically

---

## C. MCP Server Interface (serve mode)

When running `anima serve`, anima exposes an SSE MCP server for remote
agent management. This is the interface nervus (or other orchestrators)
uses to manage agents on remote nodes.

**Serve security posture (normative):**
- Loopback-only (`--host 127.0.0.1`, default) is the safe direct mode for local development.
- Any non-loopback exposure MUST be fronted by an authenticated reverse proxy/gateway with TLS and explicit authorization policy.
- Direct unauthenticated internet exposure of `anima serve` is not supported.

Reference: `docs/proposals/serve-mode-security-decision.md`.

### C.1. Agent Lifecycle Tools

#### anima.spawn(persona_spec, job_spec, task)

Spawn a new agent on this node.

**Parameters:**

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `persona_spec` | object | yes | PersonaSpec (see Section E) |
| `job_spec` | object | yes | JobSpec (see Section E) |
| `task` | string | yes | Task string for the agent |

**Returns:**
```json
{
  "instance_id": "string",
  "agent_id": "string",
  "state": "pending",
  "pid": 12345,
  "_meta": {
    "trace_id": "tr_abc123",
    "instance_id": "string"
  }
}
```

#### anima.list()

List all agents on this node.

**Parameters:** none

**Returns:**
```json
[
  {
    "instance_id": "string",
    "agent_id": "string",
    "state": "string",
    "persona_name": "string",
    "lifecycle": "string",
    "uptime_seconds": 0,
    "token_usage": { "total_tokens": 0 },
    "_meta": {
      "trace_id": "tr_abc123",
      "instance_id": "string"
    }
  }
]
```

#### anima.status(agent_id)

Get status of a specific agent. Uses bridge alive path with dead fallback.

**Parameters:**

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `agent_id` | string | yes | Agent ID (supports prefix matching; resolves to `instance_id`) |

**Bridge Alive Path:**
- If agent's MCP socket exists (`$ANIMA_STATE/agents/<id>/mcp.sock`), calls
  `anima.status` via Unix domain socket
- Returns live state with all metrics from the running process

**Dead Fallback:**
- If socket does not exist (agent dead/missing), falls back to reading
  `state.json` directly from disk
- Returns last known state from persisted file

**Returns (alive or fallback):**
```json
{
  "instance_id": "string",
  "agent_id": "string",
  "state": "pending | running | suspended | awaiting_approval | completed | failed | killed",
  "persona_name": "string",
  "lifecycle": "ephemeral | daemon",
  "token_usage": {
    "prompt_tokens": 0,
    "completion_tokens": 0,
    "total_tokens": 0
  },
  "tools_called": [
    {"name": "string", "count": 0}
  ],
  "uptime_seconds": 0,
  "parent_id": "string | null",
  "current_turn": 0,
  "max_turns": 0,
  "_meta": {
    "trace_id": "tr_abc123",
    "instance_id": "string"
  }
}
```

**Error Envelope (agent not found):**
```json
{
  "error": {
    "code": "AGENT_NOT_FOUND",
    "numeric_code": 300,
    "message": "Agent not found: 'abc123'",
    "details": {
      "instance_id": "abc123"
    }
  },
  "_meta": {
    "trace_id": "tr_abc123",
    "instance_id": "abc123"
  }
}
```

#### anima.interact(agent_id, message)

Send a message to a specific agent.

**Parameters:**

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `agent_id` | string | yes | Agent ID (supports prefix matching; resolves to `instance_id`) |
| `message` | string | yes | Message to send to the agent |

**Bridge Alive Path:**
- Calls `anima.interact` via MCP socket
- Returns agent's response after processing one turn

**Error States:**
- Agent not found → `AGENT_NOT_FOUND` error envelope
- Agent not in interactive state (not running/suspended/awaiting_approval) → `AGENT_NOT_INTERACTIVE` error envelope

**Returns:**
```json
{
  "response": "string",
  "turn_number": 5,
  "_meta": {
    "trace_id": "tr_abc123",
    "instance_id": "string"
  }
}
```

#### anima.collect(agent_id)

Collect result from a terminal-state agent.

**Parameters:**

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `agent_id` | string | yes | Agent ID (supports prefix matching; resolves to `instance_id`) |

**Returns:**
```json
{
  "instance_id": "string",
  "result_ref": "string | null",
  "status": "completed | failed | killed",
  "output": "string | null",
  "error": "string | null",
  "_meta": {
    "trace_id": "tr_abc123",
    "instance_id": "string"
  }
}
```

**Error Envelope (not terminal):**
```json
{
  "error": {
    "code": "AGENT_NOT_TERMINAL",
    "numeric_code": 315,
    "message": "Agent not in terminal state (state: running)",
    "details": {
      "instance_id": "abc123",
      "current_state": "running"
    }
  }
}
```

#### anima.kill(agent_id)

Kill a running agent.

**Parameters:**

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `agent_id` | string | yes | Agent ID (supports prefix matching; resolves to `instance_id`) |

**Behavior:**
- Transitions agent to `killed` state via `kill_agent_tree`
- If agent is alive, attempts to notify via MCP `anima.interrupt` (best-effort)
- If agent already terminal, returns error

**Returns:**
```json
{
  "acknowledged": true,
  "_meta": {
    "trace_id": "tr_abc123",
    "instance_id": "string"
  }
}
```

**Error Envelope (already terminal):**
```json
{
  "error": {
    "code": "AGENT_TERMINATED",
    "numeric_code": 313,
    "message": "Already terminal: completed",
    "details": {
      "instance_id": "abc123",
      "current_state": "completed"
    }
  },
  "acknowledged": false,
  "_meta": {...}
}
```

### C.2. Log Retrieval Tool

#### anima.logs(agent_id, tail?)

Retrieve structured event logs from the agent. Uses bridge alive path with dead fallback.

**Parameters:**

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `agent_id` | string | yes | Agent ID (supports prefix matching; resolves to `instance_id`) |
| `tail` | integer | no | Number of most recent events to return |

**Bridge Alive Path:**
- If agent's MCP socket exists, calls `anima.logs` via Unix domain socket
- Returns live log entries from the running process

**Dead Fallback:**
- If socket does not exist, reads `log.jsonl` directly from disk
- Applies `tail` parameter to the file (returns last N lines)

**Returns:**
```json
{
  "instance_id": "string",
  "entries": [
    {
      "ts": "2026-02-28T10:30:01Z",
      "event": "state_change",
      "from": "pending",
      "to": "running"
    }
  ],
  "count": 1,
  "_meta": {
    "trace_id": "tr_abc123",
    "instance_id": "string"
  }
}
```

**Error Envelope (agent not found):**
```json
{
  "error": {
    "code": "AGENT_NOT_FOUND",
    "numeric_code": 300,
    "message": "Agent not found: 'abc123'",
    "details": {
      "instance_id": "abc123"
    }
  },
  "instance_id": "abc123",
  "entries": [],
  "count": 0,
  "_meta": {...}
}
```

### C.3. Approval Control Tools

#### anima.approve(agent_id, note?)

Approve an agent approval request. Agent must be in `awaiting_approval` state.
Uses bridge alive path with dead fallback.

**Parameters:**

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `agent_id` | string | yes | Agent ID (supports prefix matching; resolves to `instance_id`) |
| `note` | string | no | Optional note from the approver |

**Bridge Alive Path:**
- If agent's MCP socket exists, calls `anima.approve` via Unix domain socket
- Notifies running agent to resume execution
- If the bridge call fails while socket is alive/reachable, returns
  `MCP_CONNECTION_ERROR` (numeric 323) and does **not** apply dead fallback
  state mutation

**Dead Fallback:**
- If socket does not exist, writes approval response to state file
- Transitions agent state directly from `awaiting_approval` to `running`

**Returns:**
```json
{
  "instance_id": "string",
  "acknowledged": true,
  "_meta": {
    "trace_id": "tr_abc123",
    "instance_id": "string"
  }
}
```

**Error Envelope (not awaiting approval):**
```json
{
  "error": {
    "code": "INVALID_STATE",
    "numeric_code": 400,
    "message": "Agent is not awaiting approval (state: running)",
    "details": {
      "instance_id": "abc123",
      "state": "running"
    }
  },
  "instance_id": "abc123",
  "acknowledged": false,
  "_meta": {...}
}
```

#### anima.reject(agent_id, reason?)

Reject an agent approval request. Agent must be in `awaiting_approval` state.
Uses bridge alive path with dead fallback.

**Parameters:**

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `agent_id` | string | yes | Agent ID (supports prefix matching; resolves to `instance_id`) |
| `reason` | string | no | Optional reason for rejection |

**Bridge Alive Path:**
- If agent's MCP socket exists, calls `anima.reject` via Unix domain socket
- Notifies running agent of rejection
- If the bridge call fails while socket is alive/reachable, returns
  `MCP_CONNECTION_ERROR` (numeric 323) and does **not** apply dead fallback
  state mutation

**Dead Fallback:**
- If socket does not exist, writes rejection response to state file
- Transitions agent state directly from `awaiting_approval` to `running`

**Returns:**
```json
{
  "instance_id": "string",
  "acknowledged": true,
  "_meta": {
    "trace_id": "tr_abc123",
    "instance_id": "string"
  }
}
```

**Error Envelope (not awaiting approval):**
```json
{
  "error": {
    "code": "INVALID_STATE",
    "numeric_code": 400,
    "message": "Agent is not awaiting approval (state: running)",
    "details": {
      "instance_id": "abc123",
      "state": "running"
    }
  },
  "instance_id": "abc123",
  "acknowledged": false,
  "_meta": {...}
}
```

### C.4. Resume Control Tool

#### anima.resume(agent_id)

Manually resume a suspended agent. Agent must be in `suspended` state.
Uses bridge alive path with dead fallback.

**Parameters:**

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `agent_id` | string | yes | Agent ID (supports prefix matching; resolves to `instance_id`) |

**Bridge Alive Path:**
- If agent's MCP socket exists, calls `anima.resume` via Unix domain socket
- Clears suspend condition and resumes agent loop
- If the bridge call fails while socket is alive/reachable, returns
  `MCP_CONNECTION_ERROR` (numeric 323) and does **not** apply dead fallback
  transition

**Dead Fallback:**
- If socket does not exist, transitions agent state directly from `suspended` to `running`

**Returns:**
```json
{
  "instance_id": "string",
  "acknowledged": true,
  "_meta": {
    "trace_id": "tr_abc123",
    "instance_id": "string"
  }
}
```

**Error Envelope (not suspended):**
```json
{
  "error": {
    "code": "INVALID_STATE",
    "numeric_code": 400,
    "message": "Agent is not suspended (state: running)",
    "details": {
      "instance_id": "abc123",
      "state": "running"
    }
  },
  "instance_id": "abc123",
  "acknowledged": false,
  "_meta": {...}
}
```

### C.5. Bridge Alive Path and Dead Fallback Semantics

All serve-mode tools that query or control running agents follow a consistent
bridge/fallback pattern:

**Bridge Alive Path:**
1. Check if agent's MCP socket exists at `$ANIMA_STATE/agents/<instance_id>/mcp.sock`
2. If socket exists, call the corresponding tool via Unix domain socket
3. Return live data from the running agent process
4. For mutating tools (`anima.approve`, `anima.reject`, `anima.resume`),
   bridge call failure returns an MCP connection error and does not fall
   through to dead fallback mutation

**Dead Fallback:**
1. If socket does not exist (agent dead/crashed/missing), fall back to direct state file access
2. Read from `state.json` or `log.jsonl` directly
3. Return last known persisted state

**Tools with Bridge/Fallback:**
- `anima.status` — Live metrics from MCP, state.json fallback
- `anima.logs` — Live log stream via MCP, log.jsonl fallback
- `anima.approve` — Notify via MCP, direct state transition fallback
- `anima.reject` — Notify via MCP, direct state transition fallback
- `anima.resume` — Notify via MCP, direct state transition fallback
- `anima.approve/reject/resume` fallback is only for socket-missing dead path
  (never for live bridge failure)
- `anima.interact` — MCP only (requires live agent)
- `anima.kill` — Direct kill with best-effort MCP notification

**Error Envelope Format:**

All error responses follow the format defined in Section G:

```json
{
  "error": {
    "code": "ERROR_CODE",
    "numeric_code": 300,
    "message": "Human-readable message",
    "details": {
      "instance_id": "string",
      ...
    }
  }
}
```

Common serve-mode error codes:
- `AGENT_NOT_FOUND` (300) — Agent state not found
- `AGENT_NOT_INTERACTIVE` (314) — Agent not in interactive state
- `AGENT_NOT_TERMINAL` (315) — Agent not in terminal state for collect
- `AGENT_TERMINATED` (313) — Agent already in terminal state
- `INVALID_STATE` (400) — Operation not valid for current agent state
- `INTERNAL` (10) — Unexpected internal error

### C.6. Serve Stream Route

Serve mode also exposes an HTTP/SSE stream bridge route for progressive
interact output:

#### GET /agent/{agent_id}/stream

- Path parameter: `agent_id` (supports prefix matching, same resolution behavior as serve tools)
- Behavior: proxies stream events from the target agent's MCP stream endpoint
- Transport: `text/event-stream`

This route is part of the serve-mode control surface. For event framing,
terminal semantics, and stream behavior details, see Section B.1.

---

## D. Built-in ctl_* Tools

These tools are registered into every agent's tool set. The agent
calls them like any other MCP tool.

### D.1. ctl_finish(result_ref?)

Agent marks its task as complete.

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `result_ref` | string | no | Reference to the result artifact |

**Behavior:** State → `completed`. Result_ref stored for `anima collect`.
Agent loop exits. For daemon agents, see Section H.3.

### D.2. ctl_fail(error)

Agent marks its task as failed.

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `error` | string | yes | Error description |

**Behavior:** State → `failed`. Error recorded. Agent loop exits.

### D.3. ctl_request_approval(reason)

Agent pauses and requests human approval.

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `reason` | string | yes | Why approval is needed |

**Behavior:** State → `awaiting_approval`. In standalone mode, anima
prints the request to the terminal (if interactive) or waits for
`anima approve <id>` / `anima reject <id>`. Agent loop blocks until
approval or rejection.

The built-in implementation is a blocking call: `ctl_request_approval`
does not return until the agent leaves `awaiting_approval`.

**Returns:**
```json
{
  "approved": true,
  "note": "string | null"
}
```

Payload schema is strict: only `approved` and `note` are returned by
the built-in tool (`src/anima/shell/ctl.py`).

- `anima approve <id> --note <text>` → `{ "approved": true, "note": <text|null> }`
- `anima reject <id> --reason <text>` → `{ "approved": false, "note": <reason> }`
- If approval state is interrupted (for example, kill), the call resolves
  with `approved: false` and `note: null`.

### D.4. ctl_spawn_subagent(persona_ref, task)

Spawn a child agent. Tracked in the parent tree.

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `persona_ref` | string | no | **Standalone:** relative file path to PersonaSpec (e.g., `personas/helper.json`). If omitted/null, child inherits parent persona. **Nervus:** spec_id resolved via registry (not yet implemented). |
| `task` | string | yes | Task string for the child |

**Positional ordering contract:** `persona_ref` first, `task` second.

**Omission contract:** `persona_ref` may be omitted or null; omission means
"inherit parent persona".

To avoid positional ambiguity when omitting `persona_ref`, callers SHOULD pass
`task` by keyword (for example, `ctl_spawn_subagent(task="...")`).

**Behavior:** Anima spawns a sub-agent. The child inherits all MCP
server connections from the parent. The child's `_meta.trace_id` is
the same as the parent's (for end-to-end tracing). The child gets its
own `instance_id` (unique per instance).

Max_depth enforcement: child depth = parent depth + 1. Rejected with
`MAX_DEPTH_EXCEEDED` if it exceeds the parent JobSpec's `max_depth`.

can_spawn enforcement: if `can_spawn` is `false` (or absent), spawn is
rejected. If `can_spawn` is `true`, any persona_ref is allowed. If
`can_spawn` is a list of strings, persona_ref must match one of the
listed names (compared against the PersonaSpec `name` field of the
target persona).

**persona_ref semantics (standalone mode):**
- Treated as a **relative file path** from the current working directory
- Security: Rejects absolute paths and path traversal (`..`)
- Loads PersonaSpec JSON from the resolved path
- No registry lookup or spec_id resolution in standalone mode

**persona_ref semantics (nervus integration):**
- **Not yet implemented.** When nervus integration is complete:
- Will support spec_id resolution via registry lookup
- Will maintain backward compatibility with file paths

**Returns:**
- Success: string message `"Spawned subagent: <instance_id>"`
- Failure: string message prefixed with `"Error:"`

### D.5. ctl_switch_persona(persona_ref)

Switch the agent's persona mid-session. The agent is rebuilt with the new
persona while preserving conversation history.

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `persona_ref` | string | yes | PersonaSpec file path for the new persona |

**Behavior:**
- The switch is applied at the next turn boundary (cooperative, not immediate)
- The agent is rebuilt via `AgentFactory.create()` with the new persona
- Conversation history (`message_history`) is preserved across the switch
- Turn counters, token accounting, and MCP server state are preserved
- Ongoing tool calls in the current turn are not interrupted; the switch
  applies to the next turn
- Only one pending persona switch may exist at a time; subsequent requests
  are rejected until the pending switch is applied

**State changes:**
- `AgentStateData.persona_name` is updated to the new persona's name
- `AgentStateData.persona_ref` is updated to the new persona reference
- `AgentStateData.spec_id` and `AgentStateData.spec_digest` are updated
  if the new persona provides them

**Returns:**
- Success: `"Persona switched to: <persona_name>. Will take effect at next turn."`
- Failure: string message prefixed with `"Error:"`

### D.6. ctl_suspend_until(resume_after?)

Agent suspends itself.

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `resume_after` | string | no | Duration string, e.g. `5m`, `1h`, `30s`. If omitted, suspends indefinitely until `anima resume`. |

**Behavior:** State → `suspended`. Agent loop pauses. Anima resumes
the agent when:
- The `resume_after` duration expires (if provided), OR
- `anima resume <id>` is called manually, OR
- An external event matches (with nervus integration, not standalone)

**Standalone mode:** Only timer-based and manual suspension are supported.
Event-based suspension requires nervus.

### D.7. Auto-Completion Policy

When `completion_policy` is `auto` (JobSpec or `--completion-policy` flag):

- The agent loop auto-completes when the LLM produces a final text
  response with no tool calls for two consecutive turns.
- The agent still MAY call `ctl_finish` explicitly to set `result_ref`.
- If the agent never calls `ctl_finish`, the final LLM response text
  is stored as the result output.
- `auto` is a convenience for simple tasks; `explicit` (default) is
  required for multi-step workflows.

### Combined Mode

When anima is connected to an external orchestrator (nervus) that
provides its own `ctl_*` tools via MCP:

- **`ctl_finish`**, **`ctl_fail`**, **`ctl_suspend_until`** -- always
- **`ctl_request_approval`** -- provided by nervus. Routes approvals
- **`ctl_spawn_subagent`** -- provided by nervus. Enables global
- **`ctl_switch_persona`** -- provided by nervus. Enables persona
  management at the orchestrator level

tool list from each connected MCP endpoint. If `ctl_request_approval`,
`ctl_spawn_subagent`, or `ctl_switch_persona` already exists, anima does
not register its built-in version. The external version takes precedence.
Detection is per-tool (partial overlap is valid). anima performs no
signature validation on external ctl tools -- compatibility is the external
provider's responsibility.

---

## E. Input Formats

All data formats are defined by JSON schemas in `contracts/`. This
section describes anima-relevant fields and behavior. **See schemas
for the complete field list.**

### PersonaSpec

Schema: `contracts/persona_spec.schema.json`

Passed via `--persona-file`. Defines agent capabilities and identity.
Typically compiled by larva from YAML, but can be hand-written.

```json
{
  "spec_version": "0.1.0",
  "name": "code-reviewer",
  "system_prompt": "You are a senior code reviewer...",
  "model": "claude-sonnet-4-20250514",
  "model_params": { "temperature": 0.3 },
  "tools_profile": "read-only",
  "can_spawn": ["coder", "researcher"],
  "spec_id": "code-reviewer@a1b2c3d4",
  "spec_digest": "sha256:e3b0c442..."
}
```

**anima-relevant fields:**

| Field | Type | Description |
| ----- | ---- | ----------- |
| `spec_version` | `"0.1.0"` | anima rejects unknown versions. |
| `name` | string | Displayed in `anima list`, `anima status`. |
| `system_prompt` | string \| null | Passed to PydanticAI as the system prompt. `null` or omitted = no system prompt sent. |
| `model` | string | LLM model identifier. Overridable via `--model`. |
| `tools_profile` | string \| null | Standalone mode: comma-separated tool preset names resolved from `config.toml` `[tools.*]` sections (e.g., `"filesystem,shell"`). Combined mode (with CapabilityToken): passed through to tela unchanged. |
| `sandbox` | `str \| None` | Reserved for future agent-process sandboxing. Does NOT participate in MCP sandbox decisions. |
| `model_params` | object \| null | Provider-specific model parameters (e.g. temperature, top_p). Passed through to LLM call. |
| `can_spawn` | `boolean \| list[str]` | Spawn permission. `false` (default) = forbidden; `true` = any persona; `["name", ...]` = only listed personas. |

**anima-ignored fields:** `node` (used by nervus).

**Removed fields:** `base` (inheritance resolved by larva before delivery to anima; anima never receives this field).

**Runtime Switchability (Planned):**

> **Status:** Persona hot-swap is planned but not yet implemented. Persona is
> currently fixed at agent start.

When implemented, agents will be able to switch personas at runtime using
`ctl_switch_persona`. The switch would preserve conversation history while
rebuilding the agent with the new persona's configuration (model, system
prompt, tools, etc.). See Section D.5 for the planned API.

### JobSpec

Schema: `contracts/job_spec.schema.json`

Passed via `--job-file`. Defines runtime execution strategy. Generated
by nervus on dispatch, or constructed from CLI flags for manual runs.

```json
{
  "job_version": "0.1.0",
  "persona_ref": "code-reviewer@a1b2c3d4",
  "instance_id": "inst_abc123",
  "timeout": "10m",
  "max_turns": 50,
  "max_tokens": 100000,
  "lifecycle": "ephemeral",
  "context_mode": "isolated",
  "completion_policy": "explicit",
  "side_effect_policy": "allow",
  "finalize_grace_budget": 500,
  "max_depth": 3,
  "context_bundle_ref": "bundle_xyz"
}
```

**anima-relevant fields:**

| Field | Type | Description |
| ----- | ---- | ----------- |
| `job_version` | `"0.1.0"` | anima rejects unknown versions. |
| `persona_ref` | string | **Standalone:** file path to PersonaSpec. **Nervus:** spec_id resolved via registry (not yet implemented). |
| `instance_id` | string | Unique per-dispatch. For idempotency. |
| `timeout` | string | Wall clock timeout, e.g. `10m`, `2h`. |
| `max_turns` | integer | Max agent loop iterations. |
| `max_tokens` | integer | Max total tokens. See Section H.2 for precedence. |
| `lifecycle` | `"ephemeral" \| "daemon"` | Default: `ephemeral`. See Section H.3. |
| `context_mode` | `"isolated" \| "shared"` | Default: `isolated`. |
| `completion_policy` | `"explicit" \| "auto"` | Default: `explicit`. See Section D.6. |
| `side_effect_policy` | `"allow" \| "approval_required" \| "read_only"` | Default: `allow`. See below. |
| `finalize_grace_budget` | integer | Tokens for final `ctl_finish` after budget exhaustion. Default: 500. |
| `max_depth` | integer | Max sub-agent nesting depth. Default: 3. |
| `context_bundle_ref` | string | Reference to a ContextBundle file. |

**Note:** `task` is NOT a JobSpec field. It is a CLI positional argument
or a parameter to `anima.spawn()`.

**side_effect_policy enforcement:**

- `allow` -- all tool calls permitted.
- `approval_required` -- anima intercepts tool calls and calls
  `ctl_request_approval` before executing any tool not in a read-only
  allowlist. The allowlist is tool-name-based: tools whose names start
  with `read`, `get`, `list`, `search`, `find`, `show`, or `status`
  are considered read-only. All others require approval.
- `read_only` -- only read-only tools (per the allowlist above) are
  permitted. Other tool calls are rejected with an error message to
  the agent.

In standalone mode (no tela), side_effect_policy is a best-effort
heuristic based on tool names. With tela, the tool profile provides
authoritative tool metadata.

### CLI Flags → JobSpec Mapping

When no `--job-file` is provided, anima constructs a JobSpec from CLI flags:

| CLI Flag | JobSpec Field | Notes |
| -------- | ------------- | ----- |
| `--timeout` | `timeout` | |
| `--max-turns` | `max_turns` | |
| `--max-tokens` | `max_tokens` | See Section H.2 for budget precedence |
| `--context-mode` | `context_mode` | |
| `--context-bundle` | `context_bundle_ref` | File path |
| `--instance-id` | `instance_id` | Auto-generated (UUID) if not provided |
| `--completion-policy` | `completion_policy` | |
| `--lifecycle` | `lifecycle` | `anima spawn` only |

When `--job-file` IS provided, CLI flags override the corresponding
JobSpec fields (CLI takes precedence).

### ContextBundle

Schema: `contracts/context_bundle.schema.json`

Used when `context_mode` is `shared`. Provides parent context to child agents.

**Runtime Enforcement:**

context_bundle_ref is only loaded when **both** conditions are true:
1. `context_bundle_ref is not None`
2. `context_mode.value == "shared"`

This check is performed in AgentRunner._run_agent_loop (lines 605-609 of
src/anima/shell/agent.py) as defense-in-depth. Schema validation also
rejects `context_mode='isolated'` with `context_bundle_ref` set. The
double-check ensures:
- Schema catches invalid combinations at construction time
- Runtime check prevents bundle loading even if schema is bypassed

```json
{
  "bundle_id": "bundle_xyz",
  "inherited_from": "inst_parent123",
  "raw_tail": [
    {
      "role": "user",
      "speaker_id": "human",
      "content": "Review the authentication module."
    },
    {
      "role": "assistant",
      "speaker_id": "inst_agent456",
      "content": "I'll review the auth module now."
    },
    {
      "role": "tool",
      "speaker_id": "inst_agent456",
      "content": "{\"file\": \"src/auth.py\", \"lines\": 142}"
    }
  ],
  "summary_refs": [
    {
      "summary_id": "sum_abc",
      "model_id": "claude-sonnet-4-20250514",
      "prompt_digest": "sha256:aabb...",
      "input_digest": "sha256:ccdd...",
      "created_at": "2026-02-28T10:30:00Z"
    }
  ],
  "artifact_refs": ["plan_v2.json", "diff_auth.patch"]
}
```

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `bundle_id` | string | yes | Unique identifier for this context bundle. |
| `inherited_from` | string | no | Parent agent's instance_id. |
| `raw_tail` | array | no | Recent conversation turns. Each item: `role` (`user \| assistant \| tool`), `speaker_id`, `content`. |
| `summary_refs` | array | no | Pre-computed summaries. Each item: `summary_id`, `model_id`, `created_at`, optional `prompt_digest`, `input_digest`. |
| `artifact_refs` | array of strings | no | References to structured artifacts (plans, diffs, decisions). |
### CapabilityToken

Schema: `contracts/capability_token.schema.json`

Issued by nervus, passed to anima via `--token-file`, presented to tela
at MCP connection time. In standalone mode (no nervus), no token is
present. See Section H.5 for the full transport chain.

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `token_id` | string (`tok_` prefix) | yes | Unique token identifier. |
| `tools_profile` | string | yes | tela profile this token grants. |
| `persona_ref` | string | no | PersonaSpec spec_id this token is bound to. |
| `instance_id` | string | no | Agent instance this token is bound to. |
| `budget` | integer | no | Token budget ceiling for tela enforcement. |
| `max_depth` | integer | no | Max sub-agent depth this token permits. |
| `issued_at` | string (format: datetime) | yes | Token issuance time. |
| `expires_at` | string (format: datetime) | yes | Token expiration time. |
| `signature` | string | yes | HMAC-SHA256 signature. |

### _meta

Schema: `contracts/meta.schema.json`

Injected by anima into every outgoing MCP tool call's arguments.
Stripped by tela before forwarding to downstream servers.

```json
{
  "trace_id": "tr_abc123",
  "instance_id": "inst_abc123",
  "spec_id": "code-reviewer@a1b2c3d4",
  "spec_digest": "sha256:e3b0c442...",
  "event_id": "evt_xyz",
  "idempotency_key": "idem_456"
}
```

**Field presence rules** (the schema has no required fields; anima
defines when each is present):

| Field | When present |
| ----- | ------------ |
| `trace_id` | **Always.** Generated by anima at job start. Propagated to sub-agents. |
| `instance_id` | **Always.** From JobSpec. |
| `spec_id` | **Always (if PersonaSpec has it).** From PersonaSpec. |
| `spec_digest` | **Always (if PersonaSpec has it).** From PersonaSpec. |
| `event_id` | **Only when dispatched by nervus.** From JobSpec context. |
| `idempotency_key` | **Only when tela is involved.** Generated per tool call for dedup. |

---

## F. State Directory

Location: `$ANIMA_STATE` environment variable, default `~/.anima/`.

```
$ANIMA_STATE/
  agents/
    <instance_id>/
      state.json          # Current state, metrics, config
      log.jsonl           # Structured event log
      result.json         # Final result (on completion/failure)
      mcp.sock            # Unix domain socket for per-agent MCP server
  logs/
    anima.log             # Global anima log (structured JSON lines)
```

### agents/<instance_id>/state.json

```json
{
  "instance_id": "inst_abc123",
  "state": "running",
  "persona_name": "code-reviewer",
  "persona_ref": "code-reviewer@a1b2c3d4",
  "spec_id": "code-reviewer@a1b2c3d4",
  "spec_digest": "sha256:e3b0c442...",
  "lifecycle": "ephemeral",
  "parent_id": null,
  "children": [],
  "depth": 0,
  "pid": 12345,
  "mcp_socket": "$ANIMA_STATE/agents/inst_abc123/mcp.sock",
  "created_at": "2026-02-28T10:30:00Z",
  "updated_at": "2026-02-28T10:31:00Z",
  "token_usage": {
    "prompt_tokens": 5000,
    "completion_tokens": 1200,
    "total_tokens": 6200
  },
  "tools_called": [
    {"name": "read_file", "count": 3},
    {"name": "ctl_finish", "count": 0}
  ],
  "current_turn": 5,
  "max_turns": 50,
  "budget_total": 100000,
  "grace_budget": 500,
  "error": null,
  "result_ref": null
}
```

**Persona Runtime Fields:**

The following fields track the agent's current persona:

| Field | Description |
|-------|-------------|
| `persona_name` | Display name of the current persona |
| `persona_ref` | Persona reference (`<persona_id>@<spec_digest>`) |
| `spec_id` | Persona spec ID from PersonaSpec |
| `spec_digest` | Content-addressable hash of the persona spec |

**Note:** These fields are fixed at agent start. Persona hot-swap is
planned but not yet implemented (see Section D.5). When implemented,
these fields would be updated via `ctl_switch_persona` while
preserving conversation history.

**PID Liveness Checking:**

The `pid` field is used for process liveness checking. When an agent is in the
`running` state, commands like `anima ls`, `anima status`, and `anima talk`
check if the process (via `os.kill(pid, 0)`) is still alive:

- **Live process**: Agent continues operating normally
- **Dead process**: Agent is "stale" — its state file says `running` but the
  process has terminated unexpectedly (crash, OOM, SIGKILL, etc.)

**Auto-Reap Semantics:**

When a stale agent is detected, it is automatically transitioned from `running`
to `failed` with the error message:
`"Stale agent state: process terminated unexpectedly"`

This auto-reap behavior is transparent to users — commands that query agent
state automatically reconcile stale agents before returning results. The agent's
`log.jsonl` records an `agent_reaped` event with `{"reason": "stale_state", "pid": <pid>}`.
### agents/<instance_id>/log.jsonl

Each line is a JSON object with a structured event:

```json
{"ts": "2026-02-28T10:30:01Z", "event": "state_change", "from": "pending", "to": "running"}
{"ts": "2026-02-28T10:30:02Z", "event": "turn", "turn": 1, "tokens": {"prompt": 1500, "completion": 300}}
{"ts": "2026-02-28T10:30:03Z", "event": "tool_call", "tool": "read_file", "args": {"path": "src/main.py"}}
{"ts": "2026-02-28T10:30:04Z", "event": "tool_result", "tool": "read_file", "ok": true}
{"ts": "2026-02-28T10:30:10Z", "event": "state_change", "from": "running", "to": "completed"}
```

### Memory Provider

> **Note:** Memory provider support has been removed. The following section describes the legacy interface for reference only. All memory operations return `not_configured`.

**Legacy provider selection** (precedence high → low):

1. `--memory-provider` CLI flag
2. `ANIMA_MEMORY_PROVIDER` environment variable
3. `memory.provider` in `config.toml`
4. Default: `none`

**Legacy providers:**

| Provider | Description |
| -------- | ----------- |
| `none` | No memory. Agent loop works normally. Memory operations return `not_configured`. |
| `local` | Filesystem-backed (previously under `$ANIMA_STATE/scratchpads/`). Must be explicitly selected. |
| `mcp` | Remote provider via MCP tools. Used in composed opifex deployments. |

**Legacy operations:**
- `read(ref, file)` — read a file
- `write(ref, file, content)` — write content
- `list(ref)` — list files
- `delete(ref, file)` — delete a file

---

## G. Error Codes

Error codes align with `contracts/errors.yaml` (canonical source for
error descriptions). anima uses the 300-range.

| Code | Numeric | Description |
| ---- | ------- | ----------- |
| `AGENT_NOT_FOUND` | 300 | Agent ID not found |
| `AGENT_ALREADY_EXISTS` | 301 | Agent with this instance_id already running |
| `BUDGET_EXHAUSTED` | 310 | Token budget exceeded |
| `MAX_TURNS_REACHED` | 311 | Max turns safety valve triggered |
| `MAX_DEPTH_EXCEEDED` | 312 | Sub-agent nesting depth exceeded |
| `AGENT_TERMINATED` | 313 | Agent was killed or interrupted |
| `AGENT_NOT_INTERACTIVE` | 314 | Agent not in interactive state for talk command |
| `AGENT_NOT_TERMINAL` | 315 | Agent not in terminal state for collect operation |
| `UNSUPPORTED_MODEL` | 320 | Model not available via LLM provider |
| `MCP_SOCKET_NOT_FOUND` | 321 | Agent MCP socket not found |
| `MCP_CONNECTION_REFUSED` | 322 | Agent MCP socket refused connection |
| `MCP_CONNECTION_ERROR` | 323 | MCP connection error (transport layer failure) |
| `TOOL_BLOCKED` | 330 | Tool call blocked by sandbox policy |
| `APPROVAL_REQUIRED` | 331 | Operation requires human approval |

Common codes (shared across all opifex tools):

| Code | Numeric | Description |
| ---- | ------- | ----------- |
| `INVALID_INPUT` | 1 | Malformed input or missing required field |
| `NOT_FOUND` | 2 | Requested resource not found |
| `TIMEOUT` | 3 | Operation timed out |
| `INTERNAL` | 10 | Unexpected internal error |

### Error Response Format (--json)

```json
{
  "error": {
    "code": "AGENT_NOT_FOUND",
    "numeric_code": 300,
    "message": "Agent not found: 'abc123'",
    "details": {
      "instance_id": "abc123"
    }
  }
}
```

---

## H. Agent Lifecycle

### H.1. State Machine

```
             ┌─────────────┐
             │   pending   │
             └──────┬──────┘
                    │ start
                    ▼
              ┌──────────────┐  ctl_suspend_until   ┌─────────────┐
              │   running    │◄─────────────────────│  suspended  │
              │              │─────────────────────► │             │
              └───┬────┬─────┘  resume / timer      └─────────────┘
                  │    │
    ctl_request_  │    │ ctl_finish / ctl_fail / budget / turns / timeout
    approval      │    │
                 ▼    ▼
  ┌──────────────────┐  ┌───────────┐  ┌───────────┐
  │ awaiting_approval│  │ completed │  │  failed   │◄───────┐
  └────────┬─────────┘  └───────────┘  └───────────┘        │
           │ approve / reject                           │    │
           ▼                                             │    │
    ┌──────────────┐ (if approved → running)            │    │
    │   running    │ (if rejected → running)            │    │
    └──────────────┘                                    │    │
                                                        │    │
   Stale Detection:                                      │    │
   ┌─────────────────────────────────────────────────────┘    │
   │  anima ls/status/talk detects dead PID                │    │
   │  running ──► failed (stale_state error) ─────────────────┘
   └─────────────────────────────────────────────────────────────

   Kill Transitions:
   ┌─────────────────────────────────────────────────────────────
   │  running ──► killed
   │  suspended ──► killed
   │  awaiting_approval ──► killed
   │  
   │  Note: pending is NOT killable (no process to kill).
   │  Terminal states (completed/failed/killed) cannot transition.
   └─────────────────────────────────────────────────────────────
```
### Valid State Transitions

| From | To | Trigger |
| ---- | -- | ------- |
| `pending` | `running` | Agent process starts |
| `running` | `completed` | `ctl_finish` called |
| `running` | `failed` | `ctl_fail`, budget exhausted (after grace), max_turns (after grace), timeout, process crash |
| `running` | `suspended` | `ctl_suspend_until` called |
| `running` | `awaiting_approval` | `ctl_request_approval` called |
| `running` | `failed` | **Stale detection**: PID is dead (auto-reap via `anima ls/status/talk`) |
| `suspended` | `running` | Timer expires, `anima resume`, external event (with nervus) |
| `awaiting_approval` | `running` | `anima approve` |
| `awaiting_approval` | `running` | `anima reject` |
| `running` | `killed` | `anima kill` / `anima.interrupt()` |
| `suspended` | `killed` | `anima kill` / `anima.interrupt()` |
| `awaiting_approval` | `killed` | `anima kill` / `anima.interrupt()` |

Terminal states: `completed`, `failed`, `killed`.

**Stale Agent Transitions:**

When an agent in `running` state has a dead process (PID check fails), the
following commands automatically transition it to `failed`:

- `anima list` — Reaps stale agents before displaying, shows updated state
- `anima status <id>` — Reaps before displaying status details
- `anima talk <id>` — Reaps before attaching to agent, exits with error message

The auto-reap is atomic: state is transitioned from `running` to `failed` with
error `"Stale agent state: process terminated unexpectedly"`, and an
`agent_reaped` event is logged with `{"reason": "stale_state", "pid": <pid>}`.
### H.2. Budget Precedence

Multiple sources can set a token budget. Precedence (highest wins):

1. `--max-tokens` CLI flag (if provided)
2. `JobSpec.max_tokens` (if provided)
3. No budget (unlimited)

The **effective budget** is the value from the highest-precedence
source that is set. There is no min/max merging -- the most specific
override wins. `finalize_grace_budget` always comes from JobSpec
(default: 500).

### H.3. Daemon Lifecycle

When `lifecycle` is `daemon`:

- The agent runs continuously after its initial task completes.
- **`ctl_finish` is not valid for daemons.** If a daemon calls
  `ctl_finish`, anima logs a warning and ignores the call. The daemon
  continues running.
- Daemons are stopped only by `anima kill` or process-level signals.
- `anima collect` on a running daemon returns the agent's current state
  (not a result). Use `--wait` to block until the daemon is killed.
- Daemons can receive new messages via `anima talk` / `anima.interact()`.
  Each message starts a new turn cycle.

### H.4. Grace Finalization

When budget is exhausted or max_turns is reached:

1. anima injects a system message: "Budget exhausted. You have
   {grace_budget} tokens remaining. Call ctl_finish now."
2. Agent gets exactly `finalize_grace_budget` tokens for one final turn.
3. If the agent calls `ctl_finish` → state = `completed`.
4. If the agent does not call `ctl_finish` (or exceeds grace budget)
   → state = `failed` with error `BUDGET_EXHAUSTED` or `MAX_TURNS_REACHED`.

**Timeout during grace:** If the wall clock timeout fires while the
agent is in grace finalization, the agent is immediately transitioned
to `failed` (no additional grace). Timeout is a hard stop.

**Timeout without grace:** When timeout fires during normal execution
(not budget/turn exhaustion), the agent is immediately transitioned to
`failed` with error `TIMEOUT`. No grace finalization is attempted for
timeout -- it is a hard wall-clock limit.

### H.5. Token Transport

CapabilityToken flows: nervus → anima → tela.

```
nervus                              anima                              tela
  |                                   |                                 |
  |-- sign token                      |                                 |
  |-- write token.json                |                                 |
  |-- spawn: anima run                |                                 |
  |     --token-file token.json       |                                 |
  |     --persona-file p.json ...     |                                 |
  |                                   |                                 |
  |                                   |-- read --token-file             |
  |                                   |-- MCP initialize() ----------->|
  |                                   |   clientInfo.capability_token   |
  |                                   |   = { token_id, tools_profile, |
  |                                   |       signature, ... }         |
  |                                   |                                 |
  |                                   |                      validate --|
  |                                   |                      HMAC +    --|
  |                                   |                      expiry    --|
  |                                   |                      bind      --|
  |                                   |                      profile   --|
  |                                   |                                 |
  |                                   |<-- tools/list (filtered) ------|
  |                                   |                                 |
  |                                   |-- tools/call (with _meta) ---->|
  |                                   |   (_meta has trace_id etc.     |
  |                                   |    but NOT the token)          |
```

**Step by step:**

1. **nervus signs** a CapabilityToken (HMAC-SHA256 with shared secret).
2. **nervus passes** the token to anima via `--token-file <path>`.
   The file contains the full CapabilityToken JSON.
3. **anima reads** the token file at startup.
4. **anima presents** the token to tela in the MCP `initialize` request,
   inside `clientInfo.capability_token`. This is a one-time handshake.
5. **tela validates** the HMAC signature and expiry. On success, tela
   binds the connection to the profile named in `token.tools_profile`.
6. **Subsequent tool calls** use `_meta` for tracing (trace_id,
   instance_id, etc.) but do NOT carry the token. The token is
   per-connection, not per-call.

**Standalone mode** (no nervus, no tela): `--token-file` is omitted.
anima connects to MCP servers directly without token authentication.

**Sub-agent token inheritance:** When anima spawns a sub-agent in
standalone mode, the child inherits the parent's MCP connections
(including any authenticated tela connection). The child does not
need its own token. In combined mode (nervus), nervus signs a
separate token for each child agent.
