# Agent & Workflow API

Endpoints for managing agents, workflows, triggers, schedules, goals, and cron jobs.

## Agent Endpoints

### GET /api/agents

List all running agents.

**Response** `200 OK`:

```json
[
  {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "name": "hello-world",
    "state": "Running",
    "created_at": "2025-01-15T10:30:00Z",
    "model_provider": "groq",
    "model_name": "llama-3.3-70b-versatile"
  }
]
```

### POST /api/agents

Spawn a new agent from a TOML manifest.

**Request Body** (JSON):

```json
{
  "manifest_toml": "name = \"my-agent\"\nversion = \"0.1.0\"\ndescription = \"Test agent\"\nauthor = \"me\"\nmodule = \"builtin:chat\"\n\n[model]\nprovider = \"groq\"\nmodel = \"llama-3.3-70b-versatile\"\n\n[capabilities]\ntools = [\"file_read\", \"web_fetch\"]\nmemory_read = [\"*\"]\nmemory_write = [\"self.*\"]\n"
}
```

**Response** `201 Created`:

```json
{
  "agent_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "name": "my-agent"
}
```

### POST /api/agents/bulk

Bulk create multiple agents in a single request.

### DELETE /api/agents/bulk

Bulk delete multiple agents by ID list.

### POST /api/agents/bulk/start

Start multiple stopped agents simultaneously.

### POST /api/agents/bulk/stop

Stop multiple running agents simultaneously.

### GET /api/agents/&lbrace;id&rbrace;

Returns detailed information about a single agent.

**Response** `200 OK`:

```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "name": "hello-world",
  "state": "Running",
  "created_at": "2025-01-15T10:30:00Z",
  "session_id": "s1b2c3d4-...",
  "model": {
    "provider": "groq",
    "model": "llama-3.3-70b-versatile"
  },
  "capabilities": {
    "tools": ["file_read", "file_list", "web_fetch"],
    "network": []
  },
  "description": "A friendly greeting agent",
  "tags": []
}
```

### DELETE /api/agents/&lbrace;id&rbrace;

Terminate an agent and remove it from the registry.

**Response** `200 OK`:

```json
{
  "status": "killed",
  "agent_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```

### PATCH /api/agents/&lbrace;id&rbrace;

Partially update an agent's fields.

### PUT /api/agents/&lbrace;id&rbrace;/update

Update an agent's configuration at runtime.

**Request Body**:

```json
{
  "description": "Updated description",
  "system_prompt": "You are a specialized assistant.",
  "tags": ["updated", "v2"]
}
```

**Response** `200 OK`:

```json
{
  "status": "updated",
  "agent_id": "a1b2c3d4-..."
}
```

### PUT /api/agents/&lbrace;id&rbrace;/mode

Set an agent's operating mode. `Stable` mode pins the current model and freezes the skill registry. `Normal` mode restores default behavior.

**Request Body**:

```json
{
  "mode": "Stable"
}
```

**Response** `200 OK`:

```json
{
  "status": "updated",
  "mode": "Stable",
  "agent_id": "a1b2c3d4-..."
}
```

### PATCH /api/agents/&lbrace;id&rbrace;/identity

Update an agent's identity fields (name, avatar, persona).

### PATCH /api/agents/&lbrace;id&rbrace;/config

Partially patch an agent's configuration object.

### POST /api/agents/&lbrace;id&rbrace;/clone

Clone an existing agent into a new one with a fresh session.

### POST /api/agents/&lbrace;id&rbrace;/message

Send a message to an agent and receive the complete response.

**Request Body**:

```json
{
  "message": "What files are in the current directory?",
  "session_id": "optional-session-uuid"
}
```

**Optional `session_id`**: per-request override of which session this message lands in. When omitted, the agent's current session is used. When supplied, the call is routed into that specific session (the session must already exist for the agent), letting one agent serve many parallel chats from a single client without `POST /sessions/{id}/switch` round-trips first. Useful for fan-out backends and multi-tab dashboards.

**Response** `200 OK`:

```json
{
  "response": "Here are the files in the current directory:\n- Cargo.toml\n- README.md\n...",
  "input_tokens": 142,
  "output_tokens": 87,
  "iterations": 1
}
```

### POST /api/agents/&lbrace;id&rbrace;/message/stream

Send a message and receive the response as a Server-Sent Events stream (see [SSE Streaming](#sse-streaming)).

### GET /api/agents/&lbrace;id&rbrace;/attach

Attach as an additional read-only subscriber to the **current session's** SSE stream — useful for multi-client co-watching when multiple surfaces (CLI, Tauri desktop, browser tabs) need to follow the same turn live.

`POST /message/stream` itself remains single-consumer (the originating caller); this endpoint is fed by a per-session `SessionStreamHub` that tees every produced `StreamEvent` to the original caller plus any number of attached subscribers. Subscribers do not advance the turn; they only observe.

**Behaviour:**

- The connection stays open for the lifetime of the current turn. When the turn ends, the server closes the stream cleanly.
- If no turn is in flight when you attach, the connection waits for the next one.
- A single agent can have many concurrent attachers without slowing the producer (broadcast channel; fan-out is constant-cost per event).
- The event format is identical to `/message/stream`, so existing parsers work unchanged.

```bash
# Co-watch the current turn from a second client
curl -N 'http://127.0.0.1:4545/api/agents/$AGENT/attach' \
  -H 'Authorization: Bearer $LIBREFANG_API_KEY'
```

### GET /api/agents/&lbrace;id&rbrace;/session

Returns the agent's current conversation history.

**Response** `200 OK`:

```json
{
  "session_id": "s1b2c3d4-...",
  "agent_id": "a1b2c3d4-...",
  "message_count": 4,
  "context_window_tokens": 1250,
  "messages": [
    {
      "role": "User",
      "content": "Hello"
    },
    {
      "role": "Assistant",
      "content": "Hello! How can I help you?"
    }
  ]
}
```

### GET /api/agents/&lbrace;id&rbrace;/sessions

List all sessions for an agent.

### POST /api/agents/&lbrace;id&rbrace;/sessions

Create a new session for an agent.

### POST /api/agents/&lbrace;id&rbrace;/sessions/&lbrace;session_id&rbrace;/switch

Switch the agent to use a different session by ID.

### GET /api/agents/&lbrace;id&rbrace;/sessions/by-label/&lbrace;label&rbrace;

Find a session by its user-assigned label.

### DELETE /api/agents/&lbrace;id&rbrace;/history

Clear all conversation history for an agent without creating a new session.

### POST /api/agents/&lbrace;id&rbrace;/session/reset

Reset an agent's session, clearing all conversation history.

**Response** `200 OK`:

```json
{
  "status": "reset",
  "agent_id": "a1b2c3d4-...",
  "new_session_id": "s5e6f7g8-..."
}
```

### POST /api/agents/&lbrace;id&rbrace;/session/compact

Trigger LLM-based session compaction. The agent's conversation is summarized by an LLM, keeping only the most recent messages plus a generated summary.

**Response** `200 OK`:

```json
{
  "status": "compacted",
  "message": "Session compacted: 80 messages summarized, 20 kept"
}
```

### POST /api/agents/&lbrace;id&rbrace;/stop

Cancel the agent's current LLM run. Aborts any in-progress generation.

**Response** `200 OK`:

```json
{
  "status": "stopped",
  "message": "Agent run cancelled"
}
```

### PUT /api/agents/&lbrace;id&rbrace;/model

Switch an agent's LLM model at runtime.

**Request Body**:

```json
{
  "model": "claude-sonnet-4-20250514"
}
```

**Response** `200 OK`:

```json
{
  "status": "updated",
  "model": "claude-sonnet-4-20250514"
}
```

### GET /api/agents/&lbrace;id&rbrace;/tools

Get an agent's current tool list.

### PUT /api/agents/&lbrace;id&rbrace;/tools

Replace an agent's tool list at runtime.

### GET /api/agents/&lbrace;id&rbrace;/skills

Get an agent's current skill list.

### PUT /api/agents/&lbrace;id&rbrace;/skills

Replace an agent's skill list at runtime.

### GET /api/agents/&lbrace;id&rbrace;/mcp_servers

Get the MCP servers attached to a specific agent.

### PUT /api/agents/&lbrace;id&rbrace;/mcp_servers

Set the MCP servers attached to a specific agent.

### GET /api/agents/&lbrace;id&rbrace;/traces

Get execution traces for an agent (tool calls, LLM iterations, timings).

### GET /api/agents/&lbrace;id&rbrace;/metrics

Get runtime metrics for an agent (token counts, latency, error rates).

### GET /api/agents/&lbrace;id&rbrace;/logs

Get recent log lines emitted by an agent's process.

### GET /api/agents/&lbrace;id&rbrace;/deliveries

Get inbound message deliveries received by an agent from channels.

### GET /api/agents/&lbrace;id&rbrace;/files

List files stored in an agent's private file workspace.

### GET /api/agents/&lbrace;id&rbrace;/files/&lbrace;filename&rbrace;

Download a specific file from an agent's workspace.

### PUT /api/agents/&lbrace;id&rbrace;/files/&lbrace;filename&rbrace;

Upload or overwrite a file in an agent's workspace.

### DELETE /api/agents/&lbrace;id&rbrace;/files/&lbrace;filename&rbrace;

Delete a file from an agent's workspace.

### POST /api/agents/&lbrace;id&rbrace;/upload

Upload a file to an agent's workspace (multipart form data).

### GET /api/uploads/&lbrace;file_id&rbrace;

Retrieve a previously uploaded file by its upload ID.

### Supported Attachment Types

When attachments are referenced from a `POST /api/agents/{id}/message` (or the streaming variant) body, the backend resolves each `file_id` to a content block before invoking the LLM. Three content classes are recognized:

| Class | Detection | Block produced |
|---|---|---|
| **Image** | `Content-Type: image/*` (`png`, `jpeg`, `webp`, `gif`) | Inline base64 image block |
| **PDF** | `Content-Type: application/pdf` | Text block prefixed with `[Attached PDF: <filename>]` containing extracted plain text |
| **Text / code** | Any `text/*` MIME, the listed `application/*` JSON/XML/YAML/TOML/JS/TS/SQL/GraphQL types, **or** a recognized file extension when the browser left the MIME blank | Text block prefixed with `[Attached file: <filename>]` containing the UTF-8 contents |

Recognized text/code extensions include `.txt`, `.md`, `.markdown`, `.rst`, `.csv`, `.tsv`, `.log`, `.json`, `.yaml`, `.yml`, `.toml`, `.xml`, `.ini`, `.conf`, `.cfg`, `.env`, `.html`, `.css`, `.js`, `.ts`, `.tsx`, `.jsx`, `.vue`, `.svelte`, `.py`, `.rs`, `.go`, `.java`, `.kt`, `.swift`, `.scala`, `.c`, `.cpp`, `.h`, `.rb`, `.php`, `.lua`, `.r`, `.sh`, `.bash`, `.sql`, `.graphql`, `.proto`, `.ipynb`, `Dockerfile`, `Makefile`, and others.

**Limits:**

- PDF text extraction and text-file reads are both truncated at 200,000 characters.
- Scanned/image-only PDFs surface as a short note explaining that no text was extractable, so the model still sees the attachment exists.
- Files that match none of the three classes are skipped with a warn log; the LLM call proceeds with the remaining attachments.

**Note for local models:** when a user message contains multiple text segments (e.g. attachment header + user prompt), the runtime coalesces adjacent same-role text blocks at the session-repair layer. This normalization happens before any driver call, so small chat-tuned models served via Ollama / vLLM / LM Studio see a single user text run plus images and continue to attend to the attachment correctly.

### GET /api/agents/&lbrace;id&rbrace;/ws

WebSocket connection for real-time bidirectional chat (see [WebSocket Protocol](#websocket-protocol)).

### GET /api/agents/&lbrace;id&rbrace;/memory/export

Export all memory entries for an agent as JSON.

### POST /api/agents/&lbrace;id&rbrace;/memory/import

Import memory entries for an agent from JSON.

### Owner Notices

Agents can send the operator a **structured private side-channel message** in addition to the user-facing reply. The `notify_owner` built-in tool collects one or more notices during the turn; the runtime aggregates them onto a `ReplyEnvelope.owner_notice: Option<String>` field.

The notice surfaces in three places:

- **`POST /message`** response body: a top-level `owner_notice` string alongside `response`.
- **`POST /message/stream` / `GET /attach`** SSE: a `StreamEvent::OwnerNotice { text }` event right before the final `Done`.
- **WhatsApp gateway**: when the `OWNER_JIDS` env var is set (comma-separated JIDs), each notice is fanned out to that owner set. Other channel adapters can opt in similarly.

Use it for "this user asked something I can't answer in public — escalate to ops" flows or any audit trail the user shouldn't see in the chat.

### GET /api/agents/&lbrace;id&rbrace;/sessions/&lbrace;session_id&rbrace;/trajectory

Export a redacted trajectory (audit trail) for a single session. The response bundles the session messages plus minimal metadata (agent name, model, system-prompt fingerprint, librefang version) and runs every message through the privacy redactor before serialisation. Intended for support, audit, and compliance flows where you need to share a session without leaking secrets.

**Query parameters:**

- `format=json` (default): single JSON object, content-type `application/json`.
- `format=jsonl`: NDJSON — first line is the metadata header, every subsequent line is one message; content-type `application/x-ndjson`. Useful for streaming into log pipelines or `jq` filters.

**Response statuses:**

- `200` — trajectory bundle.
- `400` — agent or session id failed UUID parsing.
- `404` — agent or session does not exist.

```bash
curl 'http://127.0.0.1:4545/api/agents/$AGENT/sessions/$SESSION/trajectory?format=jsonl' \
  -H 'Authorization: Bearer $LIBREFANG_API_KEY' > session.jsonl
```

---

## Workflow Endpoints

### GET /api/workflows

List all registered workflows.

**Response** `200 OK`:

```json
[
  {
    "id": "w1b2c3d4-...",
    "name": "code-review-pipeline",
    "description": "Automated code review workflow",
    "steps": 3,
    "created_at": "2025-01-15T10:30:00Z"
  }
]
```

### POST /api/workflows

Create a new workflow definition.

**Request Body** (JSON):

```json
{
  "name": "code-review-pipeline",
  "description": "Review code changes with multiple agents",
  "steps": [
    {
      "name": "analyze",
      "agent_name": "coder",
      "prompt": "Analyze this code for potential issues: {&lbrace;input&rbrace;}",
      "mode": "sequential",
      "timeout_secs": 120,
      "error_mode": "fail",
      "output_var": "analysis"
    }
  ]
}
```

**Step configuration options:**

| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Step name |
| `agent_id` | string | Agent UUID (use either this or `agent_name`) |
| `agent_name` | string | Agent name (use either this or `agent_id`) |
| `prompt` | string | Prompt template with `{&lbrace;input&rbrace;}` and `{&lbrace;output_var&rbrace;}` placeholders |
| `mode` | string | `"sequential"`, `"fan_out"`, `"collect"`, `"conditional"`, `"loop"` |
| `timeout_secs` | integer | Timeout per step (default: 120) |
| `error_mode` | string | `"fail"`, `"skip"`, `"retry"` |
| `max_retries` | integer | For `"retry"` error mode (default: 3) |
| `output_var` | string | Variable name to store output for later steps |
| `condition` | string | For `"conditional"` mode |
| `max_iterations` | integer | For `"loop"` mode (default: 5) |
| `until` | string | For `"loop"` mode: stop condition |

**Response** `201 Created`:

```json
{
  "workflow_id": "w1b2c3d4-..."
}
```

### GET /api/workflows/&lbrace;id&rbrace;

Get a specific workflow definition.

### PUT /api/workflows/&lbrace;id&rbrace;

Update an existing workflow definition.

### DELETE /api/workflows/&lbrace;id&rbrace;

Delete a workflow definition.

### POST /api/workflows/&lbrace;id&rbrace;/run

Execute a workflow.

**Request Body**:

```json
{
  "input": "Review this pull request: ..."
}
```

**Response** `200 OK`:

```json
{
  "run_id": "r1b2c3d4-...",
  "output": "Code review summary:\n- No critical issues found\n...",
  "status": "completed"
}
```

### GET /api/workflows/&lbrace;id&rbrace;/runs

List execution history for a workflow.

**Response** `200 OK`:

```json
[
  {
    "id": "r1b2c3d4-...",
    "workflow_name": "code-review-pipeline",
    "state": "Completed",
    "steps_completed": 3,
    "started_at": "2025-01-15T10:30:00Z",
    "completed_at": "2025-01-15T10:32:15Z"
  }
]
```

---

## Trigger Endpoints

### Trigger response routing

Trigger-fired responses route to the **agent's home channel** — the channel the agent is registered against under `[channels]`. Earlier behaviour silently dropped the response when no caller channel was on the call path (cron-style fires, scheduler events). Now the response always lands somewhere visible: the user-facing surface the agent owns.

### `assignee_match:self` filter on `task_posted` triggers

A `task_posted` trigger can opt to fire only when the task is assigned to the agent itself (matching by agent id):

```json
{
  "pattern": {
    "task_posted": {
      "assignee_match": "self"
    }
  }
}
```

`assignee_match` accepts:

- `"self"` — fire only when `task.assignee_id == agent.id`. Use this for "I should work on tasks assigned to me" agents.
- `"any"` (default) — legacy behaviour; fires on every `task_posted` event regardless of assignee.

### GET /api/triggers

List all triggers. Optionally filter by agent.

**Query Parameters:**
- `agent_id` (optional): Filter by agent UUID

**Response** `200 OK`:

```json
[
  {
    "id": "t1b2c3d4-...",
    "agent_id": "a1b2c3d4-...",
    "pattern": {"lifecycle": {}},
    "prompt_template": "Event: {&lbrace;event&rbrace;}",
    "enabled": true,
    "fire_count": 5,
    "max_fires": 0,
    "created_at": "2025-01-15T10:30:00Z",
    "cooldown_secs": 60,
    "session_mode": "persistent",
    "target_agent_id": null
  }
]
```

### GET /api/triggers/&lbrace;id&rbrace;

Retrieve a single trigger by ID.

**Response** `200 OK`: same shape as a single item from the list above.

**Response** `404 Not Found` if the trigger does not exist.

### POST /api/triggers

Create a new event trigger.

**Request Body**:

```json
{
  "agent_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "pattern": {
    "agent_spawned": {
      "name_pattern": "*"
    }
  },
  "prompt_template": "A new agent was spawned: {&lbrace;event&rbrace;}. Review its capabilities.",
  "max_fires": 0,
  "cooldown_secs": 60,
  "session_mode": "persistent",
  "target_agent_id": "b2c3d4e5-..."
}
```

All fields except `agent_id`, `pattern`, and `prompt_template` are optional.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `agent_id` | UUID | — | Owner agent. The trigger is registered under this agent. |
| `pattern` | object | — | Event pattern that activates the trigger (see pattern types below). |
| `prompt_template` | string | — | Message sent to the agent when the event fires. Supports `{&lbrace;event&rbrace;}` placeholder. |
| `max_fires` | integer | `0` | Maximum times this trigger may fire. `0` = unlimited. |
| `cooldown_secs` | integer | engine default | Minimum seconds between fires. Omit to use the engine default. |
| `session_mode` | `"persistent"` \| `"new"` | agent default | Session continuity for each fire. Omit to inherit from the agent manifest. |
| `target_agent_id` | UUID | `null` | Route the fired message to a different agent instead of the owner. |

**Supported pattern types:**

| Pattern | Description |
|---------|-------------|
| `{"lifecycle": {}}` | All lifecycle events |
| `{"agent_spawned": {"name_pattern": "*"}}` | Agent spawn events |
| `{"agent_terminated": {}}` | Agent termination events |
| `{"all": {}}` | All events |

**Response** `201 Created`:

```json
{
  "trigger_id": "t1b2c3d4-...",
  "agent_id": "a1b2c3d4-..."
}
```

### PATCH /api/triggers/&lbrace;id&rbrace;

Partially update a trigger. All fields are optional — only provided fields are changed.

Pass `null` for `cooldown_secs`, `session_mode`, or `target_agent_id` to clear the override and revert to the default. Omitting a field leaves it unchanged.

**Request Body**:

```json
{
  "prompt_template": "Updated: {&lbrace;event&rbrace;}",
  "enabled": true,
  "max_fires": 10,
  "cooldown_secs": 120,
  "session_mode": "new",
  "target_agent_id": "b2c3d4e5-..."
}
```

**Response** `200 OK`: the full updated trigger object (same shape as `GET /api/triggers/&lbrace;id&rbrace;`).

**Response** `404 Not Found` if the trigger does not exist.

### DELETE /api/triggers/&lbrace;id&rbrace;

Remove a trigger.

**Response** `200 OK`:

```json
{
  "status": "removed",
  "trigger_id": "t1b2c3d4-..."
}
```

---

## Schedule Endpoints

Time-based agent activation using cron expressions or interval syntax.

### GET /api/schedules

List all configured schedules.

### POST /api/schedules

Create a new schedule.

**Request Body**:

```json
{
  "agent_id": "a1b2c3d4-...",
  "cron": "0 9 * * 1-5",
  "prompt": "Good morning! Summarize overnight events.",
  "enabled": true
}
```

### GET /api/schedules/&lbrace;id&rbrace;

Get a specific schedule.

### PUT /api/schedules/&lbrace;id&rbrace;

Update a schedule.

### DELETE /api/schedules/&lbrace;id&rbrace;

Delete a schedule.

### POST /api/schedules/&lbrace;id&rbrace;/run

Trigger a schedule to run immediately (ignores its cron expression for this one invocation).

---

## Goals Endpoints

Hierarchical goal tracking system for multi-step agent objectives.

### GET /api/goals

List all goals.

### POST /api/goals

Create a new goal.

**Request Body**:

```json
{
  "title": "Migrate database to PostgreSQL",
  "description": "Complete migration with zero downtime",
  "agent_id": "a1b2c3d4-...",
  "parent_id": null,
  "due_at": "2025-02-01T00:00:00Z"
}
```

### GET /api/goals/&lbrace;id&rbrace;

Get a specific goal.

### PUT /api/goals/&lbrace;id&rbrace;

Update a goal (title, status, description, due date).

### DELETE /api/goals/&lbrace;id&rbrace;

Delete a goal and all its children.

### GET /api/goals/&lbrace;id&rbrace;/children

List sub-goals for a given parent goal.

---

## Cron Endpoints

Schedule recurring jobs using standard cron expression syntax.

### GET /api/cron/jobs

List all cron jobs.

### POST /api/cron/jobs

Create a new cron job.

**Request Body**:

```json
{
  "name": "daily-digest",
  "schedule": "0 8 * * *",
  "agent_id": "a1b2c3d4-...",
  "prompt": "Prepare the daily digest of overnight activity.",
  "enabled": true
}
```

### GET /api/cron/jobs/&lbrace;id&rbrace;

Get details for a specific cron job.

### PUT /api/cron/jobs/&lbrace;id&rbrace;

Update a cron job (schedule, prompt, agent, etc.).

### DELETE /api/cron/jobs/&lbrace;id&rbrace;

Delete a cron job.

### PUT /api/cron/jobs/&lbrace;id&rbrace;/enable

Toggle a cron job's enabled state.

**Request Body**:

```json
{
  "enabled": false
}
```

### GET /api/cron/jobs/&lbrace;id&rbrace;/status

Get the last run result and next scheduled time for a cron job.

---

## Interrupting a Running Agent Turn

**Source:** `librefang-runtime/src/interrupt.rs`

A `SessionInterrupt` is an `Arc<AtomicBool>` signal that the agent loop checks at every
tool-call boundary. Setting the flag causes the loop to exit cleanly after the current
tool call completes — it does not forcibly kill threads or cancel in-flight network
requests.

**Cascade into `agent_send` subagents**: a parent `/stop` now propagates to every subagent the parent spawned via the `agent_send` tool. The cascade walks the parent → subagent edges built when each `agent_send` call ran, so a single `/stop` against the user-facing agent stops the entire fan-out tree it kicked off — no orphaned subagent loops continuing after the user gave up. Fully recursive: subagents that themselves called `agent_send` propagate further.

### Interrupt Endpoint

```bash
POST /api/agents/{agent_id}/sessions/{session_id}/interrupt
```

No request body is required. The response is `204 No Content` when the interrupt signal
was accepted.

```bash
curl -X POST \
  "http://127.0.0.1:4545/api/agents/${AGENT_ID}/sessions/${SESSION_ID}/interrupt"
```

### Behavior

| Situation | What happens |
|---|---|
| Agent is between tool calls | Loop exits at the next boundary check. The partial response up to that point is recorded. |
| Agent is mid-LLM-call | The LLM call completes normally, then the loop checks the flag and exits before executing the next tool. |
| Agent is mid-tool-execution | The tool runs to completion, then the loop exits. No tool call is abandoned mid-flight. |
| Agent has already finished | The interrupt flag is ignored; no error is returned. |
| No active turn | `204` is still returned; the flag is set and will take effect on the next turn. |

### Automatic Clearing

The interrupt flag is automatically cleared when:
- A new incoming message starts a fresh agent turn
- The session is reset

You do not need to send a "resume" or "clear" request after the interrupted turn ends.

### Use Cases

- **User-initiated stop**: A user presses a "Stop" button in a UI. The frontend calls the
  interrupt endpoint and the agent stops after the current tool call.
- **Watchdog timeout**: An external monitor detects a long-running turn and interrupts
  it to avoid token budget exhaustion.
- **Graceful shutdown**: Before restarting the daemon, interrupt all active sessions so
  they record a clean stop rather than an abrupt disconnect.

