# 代理与工作流 API

管理代理、工作流、触发器、调度、目标和定时任务的端点。

## Agent 端点

### GET /api/agents

列出所有运行中的 Agent。

**响应** `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

从 TOML 清单创建新 Agent。

**请求体**（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"
}
```

**响应** `201 Created`：

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

### POST /api/agents/bulk

批量创建多个 Agent。

### DELETE /api/agents/bulk

通过 ID 列表批量删除多个 Agent。

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

同时启动多个已停止的 Agent。

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

同时停止多个运行中的 Agent。

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

获取单个 Agent 的详细信息。

**响应** `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;

终止 Agent 并将其从注册表中移除。

**响应** `200 OK`：

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

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

部分更新 Agent 的字段。

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

在运行时更新 Agent 的配置。

**请求体**：

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

**响应** `200 OK`：

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

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

设置 Agent 的运行模式。`Stable` 模式会固定当前模型并冻结技能注册表。`Normal` 模式恢复默认行为。

**请求体**：

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

**响应** `200 OK`：

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

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

更新 Agent 的身份字段（名称、头像、人设）。

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

部分更新 Agent 的配置对象。

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

将现有 Agent 克隆为新的 Agent，使用全新会话。

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

向 Agent 发送消息并接收完整响应。

**请求体**：

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

**可选 `session_id`**：按请求覆盖此消息落到哪个 session。省略时用 agent 当前 session。给了就路由到那个特定 session（必须是该 agent 已存在的 session），让一个 agent 从单个客户端同时服务多条平行 chat，不需要先 `POST /sessions/{id}/switch`。fan-out 后端和多 tab dashboard 用得上。

**响应** `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

发送消息并以 Server-Sent Events 流的方式接收响应（参见 [SSE 流式传输](#sse-流式传输)）。

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

作为额外只读订阅者附加到**当前 session** 的 SSE 流——多客户端联合观察时有用，CLI、Tauri 桌面、浏览器多 tab 想跟同一回合 live。

`POST /message/stream` 本身保持 single-consumer（只发起方拿到）；本端点由按 session 的 `SessionStreamHub` 把每个 `StreamEvent` 同时复制给原调用方和任意数量的额外订阅者。订阅者不推进回合，只观察。

**行为：**

- 连接保持到当前回合结束。回合结束时服务端干净关闭。
- 你 attach 时没有正在跑的回合，连接等下一个。
- 单个 agent 可有多个并发 attacher，不会拖慢生产者（broadcast channel；按事件的扇出常数代价）。
- 事件格式跟 `/message/stream` 完全一样，已有解析器无需改动。

```bash
# 第二个客户端联合观察当前回合
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

获取 Agent 当前的对话历史。

**响应** `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

列出 Agent 的所有会话。

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

为 Agent 创建新会话。

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

切换 Agent 使用的会话。

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

通过用户指定的标签查找会话。

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

清除 Agent 的所有对话历史，但不创建新会话。

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

重置 Agent 的会话，清除所有对话历史。

**响应** `200 OK`：

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

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

触发基于 LLM 的会话压缩。Agent 的对话将由 LLM 进行摘要，仅保留最近的消息和生成的摘要。

**响应** `200 OK`：

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

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

取消 Agent 当前的 LLM 运行。中止任何正在进行的生成。

**响应** `200 OK`：

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

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

在运行时切换 Agent 的 LLM 模型。

**请求体**：

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

**响应** `200 OK`：

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

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

获取 Agent 当前的工具列表。

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

在运行时替换 Agent 的工具列表。

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

获取 Agent 当前的技能列表。

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

在运行时替换 Agent 的技能列表。

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

获取附加到指定 Agent 的 MCP 服务器。

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

设置附加到指定 Agent 的 MCP 服务器。

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

获取 Agent 的执行追踪（工具调用、LLM 迭代、耗时等）。

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

获取 Agent 的运行时指标（Token 数量、延迟、错误率）。

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

获取 Agent 进程输出的最近日志行。

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

获取 Agent 从通道接收到的入站消息投递记录。

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

列出 Agent 私有文件工作区中的文件。

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

从 Agent 的工作区下载指定文件。

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

上传或覆盖 Agent 工作区中的文件。

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

从 Agent 的工作区中删除文件。

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

上传文件到 Agent 的工作区（multipart 表单数据）。

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

通过上传 ID 获取之前上传的文件。

### 支持的附件类型

`POST /api/agents/{id}/message`（或 streaming 变体）的 body 引用附件时，后端在调用 LLM 前会把每个 `file_id` 解析为 content block。识别三类：

| 类别 | 检测 | 产出 block |
|---|---|---|
| **图片** | `Content-Type: image/*`（`png`、`jpeg`、`webp`、`gif`） | 内联 base64 图片 block |
| **PDF** | `Content-Type: application/pdf` | 文本 block，前缀 `[Attached PDF: <filename>]` 跟提取的纯文本 |
| **文本 / 代码** | 任何 `text/*` MIME，列出的 `application/*` JSON/XML/YAML/TOML/JS/TS/SQL/GraphQL 类型，**或**浏览器没填 MIME 时识别已知扩展名 | 文本 block，前缀 `[Attached file: <filename>]` 跟 UTF-8 内容 |

识别的文本/代码扩展名包括 `.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` 等等。

**上限：**

- PDF 文本提取和文本文件读取都截断在 200,000 字符。
- 扫描件 / 纯图片 PDF 会带一段简短说明指出无法提取文本，模型仍能看到附件存在。
- 三类都不匹配的文件被跳过并记 warn 日志；LLM 调用照常带上其余附件继续。

**本地小模型注意：** 用户消息含多段文本（比如附件头 + 用户提示）时，runtime 在 session-repair 层把同 role 相邻的文本 block 合并。这个归一化在任何 driver 调用之前发生，所以经 Ollama / vLLM / LM Studio 的 chat-tuned 小模型只看到单段 user 文本加图片，依然会注意到附件。

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

用于实时双向聊天的 WebSocket 连接（参见 [WebSocket 协议](#websocket-协议)）。

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

将 Agent 的所有记忆条目导出为 JSON。

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

从 JSON 导入 Agent 的记忆条目。

### Owner Notice

agent 可以发**结构化的私密旁路消息**给运营者，跟用户可见的回复并行。`notify_owner` 内置 tool 在回合中收集一条或多条 notice；runtime 聚合到 `ReplyEnvelope.owner_notice: Option<String>` 字段。

通知出现在三处：

- **`POST /message`** 响应 body：顶层 `owner_notice` 字符串，跟 `response` 同级。
- **`POST /message/stream` / `GET /attach`** SSE：在最终 `Done` 之前的 `StreamEvent::OwnerNotice { text }` 事件。
- **WhatsApp gateway**：设了 `OWNER_JIDS` 环境变量（逗号分隔的 JID）后，每条 notice 扇出到这组 owner。其他 channel adapter 可以同样接入。

适合 "这个用户问了我没法在公开消息里答的事——升级给运维" 流程，或者任何不该让用户在 chat 里看到的审计轨迹。

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

导出某个 session 的脱敏 trajectory（审计轨迹）。响应打包了该 session 的消息加最小元数据（agent 名、模型、system prompt 指纹、librefang 版本），所有消息都过隐私脱敏器再序列化。用于支持、审计、合规流程，需要分享 session 但又不能泄密时用这个。

**Query 参数：**

- `format=json`（默认）：单 JSON 对象，content-type `application/json`。
- `format=jsonl`：NDJSON — 第一行是元数据头，后续每行一条消息；content-type `application/x-ndjson`。方便 streaming 进 log pipeline 或 `jq` filter。

**响应状态：**

- `200` — trajectory 包。
- `400` — agent 或 session id 不是合法 UUID。
- `404` — agent 或 session 不存在。

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

---

## 工作流端点

### GET /api/workflows

列出所有已注册的工作流。

**响应** `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

创建新的工作流定义。

**请求体**（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"
    }
  ]
}
```

**步骤配置选项：**

| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | string | 步骤名称 |
| `agent_id` | string | Agent UUID（与 `agent_name` 二选一） |
| `agent_name` | string | Agent 名称（与 `agent_id` 二选一） |
| `prompt` | string | 提示词模板，支持 `{&lbrace;input&rbrace;}` 和 `{&lbrace;output_var&rbrace;}` 占位符 |
| `mode` | string | `"sequential"`、`"fan_out"`、`"collect"`、`"conditional"`、`"loop"` |
| `timeout_secs` | integer | 每步超时时间（默认：120） |
| `error_mode` | string | `"fail"`、`"skip"`、`"retry"` |
| `max_retries` | integer | 用于 `"retry"` 错误模式（默认：3） |
| `output_var` | string | 存储输出的变量名，供后续步骤使用 |
| `condition` | string | 用于 `"conditional"` 模式 |
| `max_iterations` | integer | 用于 `"loop"` 模式（默认：5） |
| `until` | string | 用于 `"loop"` 模式：停止条件 |

**响应** `201 Created`：

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

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

获取指定的工作流定义。

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

更新已有的工作流定义。

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

删除工作流定义。

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

执行工作流。

**请求体**：

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

**响应** `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

列出工作流的执行历史。

**响应** `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"
  }
]
```

---

## 触发器端点

### 触发响应路由

触发器触发的响应路由到**该 agent 的主通道** —— agent 在 `[channels]` 下注册的那个通道。之前的行为在调用路径上没有调用方通道时（cron 样的触发、调度器事件）会静默丢弃响应。现在响应总会落到可见的地方：agent 自己拥有的面向用户的表面。

### `task_posted` 触发器的 `assignee_match:self` 过滤

`task_posted` 触发器可以选择只在任务被指派给 agent 自己时触发（按 agent id 匹配）：

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

`assignee_match` 接受：

- `"self"` — 仅当 `task.assignee_id == agent.id` 触发。"我应该处理分配给我的任务" 类 agent 用这个。
- `"any"`（默认） — 旧行为；任何 `task_posted` 事件都触发，不看 assignee。

### GET /api/triggers

列出所有触发器。可按 Agent 过滤。

**查询参数：**
- `agent_id`（可选）：按 Agent UUID 过滤

**响应** `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;

获取单个触发器详情。

**响应** `200 OK`：与列表中的单条记录格式相同。

**响应** `404 Not Found`：触发器不存在时返回。

### POST /api/triggers

创建新的事件触发器。

**请求体**：

```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-..."
}
```

除 `agent_id`、`pattern` 和 `prompt_template` 外，其余字段均为可选。

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `agent_id` | UUID | — | 触发器归属的 Agent。 |
| `pattern` | object | — | 激活触发器的事件模式（见下方模式类型）。 |
| `prompt_template` | string | — | 事件触发时发送给 Agent 的提示，支持 `{&lbrace;event&rbrace;}` 占位符。 |
| `max_fires` | integer | `0` | 最多触发次数，`0` 表示无限制。 |
| `cooldown_secs` | integer | 引擎默认值 | 两次触发之间的最短间隔（秒）。省略则使用引擎默认值。 |
| `session_mode` | `"persistent"` \| `"new"` | Agent 默认值 | 每次触发的会话连续性。省略则继承 Agent 配置。 |
| `target_agent_id` | UUID | `null` | 将触发消息路由到指定 Agent，而非触发器所有者。 |

**支持的模式类型：**

| 模式 | 说明 |
|------|------|
| `{"lifecycle": {}}` | 所有生命周期事件 |
| `{"agent_spawned": {"name_pattern": "*"}}` | Agent 创建事件 |
| `{"agent_terminated": {}}` | Agent 终止事件 |
| `{"all": {}}` | 所有事件 |

**响应** `201 Created`：

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

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

部分更新触发器配置。所有字段均为可选，仅更新提供的字段。

将 `cooldown_secs`、`session_mode` 或 `target_agent_id` 设为 `null` 可清除覆盖值并恢复默认。省略字段则保持不变。

**请求体**：

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

**响应** `200 OK`：返回完整的更新后触发器对象（与 `GET /api/triggers/&lbrace;id&rbrace;` 格式相同）。

**响应** `404 Not Found`：触发器不存在时返回。

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

删除触发器。

**响应** `200 OK`：

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

---

## 调度端点

基于时间的 Agent 激活，使用 cron 表达式或间隔语法。

### GET /api/schedules

列出所有已配置的调度。

### POST /api/schedules

创建新的调度。

**请求体**：

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

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

获取指定调度。

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

更新调度。

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

删除调度。

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

立即触发调度执行（本次调用忽略其 cron 表达式）。

---

## 目标端点

用于多步骤 Agent 目标的层级化目标追踪系统。

### GET /api/goals

列出所有目标。

### POST /api/goals

创建新目标。

**请求体**：

```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;

获取指定目标。

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

更新目标（标题、状态、描述、截止日期）。

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

删除目标及其所有子目标。

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

列出指定父目标的子目标。

---

## 定时任务端点

使用标准 cron 表达式语法调度周期性任务。

### GET /api/cron/jobs

列出所有定时任务。

### POST /api/cron/jobs

创建新的定时任务。

**请求体**：

```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;

获取指定定时任务的详情。

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

更新定时任务（调度表达式、提示词、Agent 等）。

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

删除定时任务。

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

切换定时任务的启用状态。

**请求体**：

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

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

获取定时任务的上次执行结果和下次预定时间。

---

## 中断正在运行的 Agent 轮次

**源码：** `librefang-runtime/src/interrupt.rs`

`SessionInterrupt` 是一个 `Arc<AtomicBool>` 信号，agent 循环在每个工具调用边界处检查它。设置该标志后，循环在当前工具调用完成后干净退出——不会强制终止线程或取消正在进行的网络请求。

**级联到 `agent_send` 子 agent**：父 agent 的 `/stop` 现在会传播到它通过 `agent_send` tool 派出的每个子 agent。级联沿着 `agent_send` 调用时建立的父→子边走，所以对面向用户的 agent 单次 `/stop` 会停掉它启动的整个 fan-out 树 — 用户放弃后不会有孤儿子 agent 循环继续。完全递归：子 agent 自己又调了 `agent_send` 的话继续向下传。

### 中断端点

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

无需请求体。信号被接受时响应 `204 No Content`。

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

### 行为

| 场景 | 发生的事情 |
|---|---|
| Agent 在工具调用之间 | 循环在下次边界检查时退出，记录截止当前的部分响应。 |
| Agent 正在进行 LLM 调用 | LLM 调用正常完成，然后循环检查标志并在执行下一个工具前退出。 |
| Agent 正在执行工具 | 工具运行至完成，然后循环退出。不会在工具执行途中中止。 |
| Agent 已经完成 | 中断标志被忽略，不返回错误。 |
| 没有活跃轮次 | 仍返回 `204`；标志被设置，将在下次轮次时生效。 |

### 自动清除

中断标志在以下情况下自动清除：
- 新的传入消息开始新的 agent 轮次
- 会话被重置

轮次中断结束后无需发送"恢复"或"清除"请求。

### 使用场景

- **用户主动停止**：用户在界面中点击"停止"按钮，前端调用中断端点，agent 在当前工具调用后停止。
- **看门狗超时**：外部监控检测到长时间运行的轮次，中断它以避免 token 预算耗尽。
- **优雅关闭**：重启守护进程前中断所有活跃会话，使其记录干净的停止状态而非突然断连。

