> ## Documentation Index
> Fetch the complete documentation index at: https://agent.minimaxi.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 与 Agent 双向协作

> Agent 可以调用 Mini App 的工具，Mini App 也可以把工作交给 Agent。关键节点上始终由人确认。

## 三条通道

| 方向 | 机制 | 谁来确认 |
| - | - | - |
| Agent → Mini App | [托管 MCP](https://agent.minimaxi.com/docs/miniapp/authors/agent.md#mcp)：Mini App 的 Node 服务作为 MCP 服务器，向 Agent 暴露工具 | 按 Agent 现有的工具权限 |
| Mini App → 输入框 | [message.append](https://agent.minimaxi.com/docs/miniapp/authors/client-api.md#append)：把页面内容追加到输入框 | 宿主确认框，用户自己发送 |
| Mini App → Agent 会话 | [Agent API](https://agent.minimaxi.com/docs/miniapp/authors/agent.md#agent-api)：从页面或 Node 发起 Agent 回合 | 工具审批和问卷由 Mini App 渲染，或交给宿主侧栏 |

## 让 Agent 调用 Mini App：托管 MCP

每个 MCP 服务器只能选一种生命周期：

* **普通插件 MCP：**独立运行（比如 stdio），不依赖 Mini App 的 Node 服务。不要把它写进 `mcpEndpoints`。
* **Mini App 托管 MCP：**由 Mini App 的 Node 服务实现，只在服务运行时可用。适合和页面共享同一份业务状态。

托管 MCP 先在插件的 `*.mcp.json` 里声明一个 Streamable HTTP 服务器，`url` 用这个固定的占位地址，发布后由宿主改写：

```json servers.mcp.json theme={null}
{
  "schemaVersion": 1,
  "mcpServers": {
    "board-summary": {
      "type": "streamable-http",
      "url": "http://127.0.0.1:9/mcp/board-summary",
      "headers": {},
      "description": "汇总当前看板的数据。",
      "timeout": 30000
    }
  }
}
```

再在 `miniapp.json` 里把它挂到 Node 服务的路径上，并在 `plugin.json` 的 `mcpServers` 里引用 `servers.mcp.json`：

```json miniapp/miniapp.json theme={null}
{
  "mcpEndpoints": [
    { "server": "board-summary", "path": "/mcp/board-summary" }
  ]
}
```

* Node 入口必须在 `start` 返回前，在 `context.listen` 的对应路径上提供 Streamable HTTP MCP。宿主只改写这个服务器的地址，工具发现、排序、调用和错误仍由插件 MCP 运行时负责。
* 用户发送一条引用了该 Mini App 的消息时，宿主会在请求模型之前启动服务、连好 MCP、确认工具可用。必需的托管 MCP 不可用时，这次请求直接失败，不会让模型在缺少工具时继续执行。
* `tools/list` 应只返回工具定义，不在这里导入数据、扫描文档或调用远程服务。业务初始化放到真正需要它的工具调用里。
* MCP 处理器故障不会影响页面，但在修复并重新发布之前，Agent 无法使用这些工具。

## 交给输入框

适合「整理好上下文，由用户决定是否发送」的场景，用法见 [message.append](https://agent.minimaxi.com/docs/miniapp/authors/client-api.md#append)。如果要让 Agent 修改表格、卡片等业务对象，给它提供 MCP 工具，并在请求和工具里用稳定的业务 ID。用户可以修改或删除输入框里的文字，因此不能把它当作请求和回合之间的关联 ID。

## Agent API

Mini App 可以从页面（`window.mcode.miniapp.agent`）或 Node（`context.agent`）直接发起 Agent 回合。Agent 拥有完整能力：文件、Shell、Skill 和 MCP，包括这个 Mini App 自己的 MCP。

完整签名、模型与问卷类型、会话字段和事件结构见 [Agent API 参考](https://agent.minimaxi.com/docs/miniapp/reference/agent-api.md)，错误处理见[错误码说明](https://agent.minimaxi.com/docs/miniapp/reference/errors.md#agent)。

### 开启

```json miniapp/miniapp.json theme={null}
{
  "agentAccess": ["session.run"],
  "agentConcurrency": 4
}
```

* 不写 `agentAccess` 就是禁止。可声明 `"session.run"`（运行回合）和 `"session.read"`（读取、观察用户会话），两者互不包含，也可以同时声明。
* `agentConcurrency` 可选，用于限制这个 Mini App 同时运行的回合数，取 1 到 32，默认 4。超出的调用按顺序排队，队列最多是并发数的 4 倍。每个会话同一时间只有一个回合。
* 授权以进程为单位生效：改了 `agentAccess` 并重新发布，服务会重启。
* 用户可以在插件详情里分别关闭运行和读取权限，不用重启。关闭运行权限会中止本 Mini App 的在途回合；关闭读取权限会结束相应的观察订阅。接口对象仍在，调用方需要处理权限错误。

### 会话属于谁

| | 页面发起 | Node 发起 |
| - | - | - |
| agent | 打开这个 Mini App 的聊天所用的 agent | 主 agent；可用 `agents()` 查看并在创建时指定 |
| 工作目录 | 该聊天的项目目录；没有项目目录时，用 Mini App 自己的 Agent 目录 | Mini App 自己的 Agent 目录 |
| 模型 | 继承创建它的聊天的模型和思考强度 | 运行时默认模型 |

这些会话不出现在用户的会话列表里。每个 Mini App 只能续用自己创建的会话，续用时 agent 和目录不变；另行声明 `session.read` 后可以读取和观察用户会话，但不能续跑它们。`run` 和 `answer` 可以传 `model` 切换模型，必须是用户在 `models()` 里能看到的模型。这些会话与聊天的关系见 [Mini App 与会话](https://agent.minimaxi.com/docs/miniapp/sessions.md#agent-sessions)。

### 方法

| 方法 | 页面 | Node | 作用 |
| - | - | - | - |
| `run({ text, sessionId?, model?, agent?, whenBusy? })` | ✓ | ✓ | 运行一个回合，返回 `{ sessionId, turnId?, model?, status, text?, error?, pendingQuestion? }`；`status` 为 `finished`、`error`、`aborted`，或忙时跟进的 `steered` |
| `reply({ sessionId, requestId, decision })` | ✓ | ✓ | 处理工具审批，`decision` 为 `allowOnce` 或 `deny` |
| `answer(...)` | ✓ | ✓ | 回答暂停中的问卷并观察续跑的回合。页面用 `questionId` 指定问卷，Node 用 `requestId` |
| `cancel(requestId)` | ✓ | — | 取消页面发起的调用；Node 用 `signal` 取消 |
| `open(sessionId)` | ✓ | — | 在宿主聊天侧栏打开这个会话，让用户在那里交互 |
| `models()` / `agents()` | ✓ | ✓ | 列出可用模型和用户配置的 agent |
| `create({ agent? })` | ✓ | ✓ | 只创建会话、不运行，返回 `{ sessionId }` |
| `sessions()` / `session(sessionId)` | ✓ | ✓ | 列出最近用户会话或读取一个会话的详情；需要 `session.read` |
| `observe({ all: true })` / `observe({ sessionId })` | ✓ | ✓ | 持续观察用户会话；通过第二个参数的 `onEvent` 接收事件，需要 `session.read` |

### 会话忙时跟进

续用会话时可以传 `whenBusy`，必须同时给出 `sessionId`：

```javascript theme={null}
const result = await agent.run({ sessionId, text, whenBusy: 'queue' });
```

* `queue`：当前回合结束后，消息作为独立回合运行，返回该回合的结果。
* `steer`：把消息交给正在运行的回合，调整它的工作。消息被读入后返回 `status: 'steered'`，没有独立的 `text`；回复由原回合的调用接收。若原回合在读入前已经结束，消息会作为下一回合运行。
* 会话空闲时，两种方式都直接开始新回合。不传 `whenBusy` 而会话正忙，页面返回 `busy`，Node 返回 `BUSY`。
* 用户停止会话后，已有队列可能暂停，后续跟进会等待用户恢复。排队期间仍占运行名额；需要放弃时取消调用。已经交给运行中回合的 steer 消息不能撤回。

### 读取和观察用户会话

```json miniapp/miniapp.json theme={null}
{
  "agentAccess": ["session.read"]
}
```

`session.read` 可以读取用户会话及其最终回复，应只在产品确实需要这些内容时声明。它不授予运行回合的权限。

* `sessions()` 返回 `{ sessions }`，最多 50 条最近更新的用户会话；不列出 Mini App、定时任务和子 Agent 会话。
* `session(sessionId)` 返回会话状态、项目目录、模型及可用的最后一条助手回复。页面不传 ID 的 `session()` 读取打开它的聊天；页面不属于聊天时返回 `undefined`。Node 没有当前聊天，必须显式传 ID。
* `observe({ all: true }, { onEvent })` 接收所有用户会话的 `turn.started`、最终回复 `message.committed` 和 `turn.finished`；指定 `sessionId` 则观察该会话的进度。审批和问卷不会交给观察者。
* 观察不是一次运行，不占运行名额；每个 Mini App 最多 4 个订阅。页面用传入的 `requestId` 配合 `cancel(requestId)` 结束订阅；Node 用第二个参数的 `signal`。关闭页面、服务结束或读取权限被关闭也会结束相应订阅。
* 读取、观察本 Mini App 自己的会话时，`session.run` 也可授权；不能读取其他 Mini App 的会话或子 Agent 会话。

调用前可用 `capabilities?.has('agent.observe')` 等检查宿主是否支持接口；这不代表 manifest 已声明或用户仍允许该权限。

### 审批和问卷

![谁能回答审批](https://mintcdn.com/agent-cn/cVBevhAJXWhxEBzo/images/miniapp/approvals-zh.svg?fit=max\&auto=format\&n=cVBevhAJXWhxEBzo\&q=85\&s=4359d322389555e05b0405ac6c12e4dc)

宿主先确认有人能回答，再让审批等待；没人能回答就直接拒绝。

* `permission.requested` → `reply`：工具调用等待批准。只有「允许一次」和「拒绝」，没有「始终允许」，Mini App 不会写入用户的全局权限规则。
* `question.requested` → `answer`：回合停在问卷上（`ask_user`、功能开启或 Plan 审阅）。等暂停的 `run` 返回之后再调用 `answer`。
* 审批只在有人能回答时等待：发起调用时传了 `onEvent`，或者页面用 `open` 把会话显示在侧栏里。否则直接拒绝，并告诉模型没人能批准、不用这个工具继续。
* 等待没有超时限制，调用通过取消、进程退出或重新发布释放。
* Agent 委派给子会话时，子会话的审批也会通过 `onEvent` 到达，带着子会话自己的 `sessionId`。不要按根回合过滤事件，否则调用会一直等下去。

### 页面示例

```javascript theme={null}
const agent = window.mcode?.miniapp?.agent;
if (agent) {
  const result = await agent.run(
    { text: '按当前筛选条件总结本周的风险项，并列出需要我确认的事项。' },
    {
      onEvent(event) {
        if (event.type === 'text.delta') output.textContent += event.text;
        if (event.type === 'permission.requested') {
          // 在页面里渲染审批卡片，用户决定后调用：
          // agent.reply({ sessionId: event.sessionId, requestId: event.requestId, decision: 'allowOnce' })
          renderApproval(event);
        }
      },
    },
  );
  if (result.pendingQuestion) renderQuestionnaire(result.pendingQuestion.request);
  openInChatButton.onclick = () => agent.open(result.sessionId);
}
```

### Node 示例

```typescript theme={null}
import type {
  MiniAppAgentClient,
  MiniAppAgentQuestionAnswer,
  MiniAppAgentQuestionnaire,
  MiniAppAgentRunEvent,
} from './miniapp-api.js';

// 你自己的页面往返，比如用 SSE 推给页面，再由页面 POST 回来
declare function askUser(request: MiniAppAgentQuestionnaire): Promise<MiniAppAgentQuestionAnswer[]>;
declare function confirmTool(toolName: string, reason: string): Promise<boolean>;

async function runWithQuestions(agent: MiniAppAgentClient, text: string) {
  const onEvent = (event: MiniAppAgentRunEvent) => {
    if (event.type !== 'permission.requested') return;
    // 回合在等这个决定；即使是第一次运行，事件里也带着会话 ID
    void confirmTool(event.toolName, event.reason)
      .then((allowed) =>
        agent.reply({
          sessionId: event.sessionId,
          requestId: event.requestId,
          decision: allowed ? 'allowOnce' : 'deny',
        }),
      )
      .catch(() => {}); // REQUEST_NOT_FOUND：这次运行已经被取消
  };
  let result = await agent.run({ text }, { onEvent });
  // 等 run 返回后再回答：在此之前它占着这个会话唯一的运行名额
  while (result.pendingQuestion) {
    const answers = await askUser(result.pendingQuestion.request);
    result = await agent.answer(
      { sessionId: result.sessionId, requestId: result.pendingQuestion.requestId, answers },
      { onEvent },
    );
  }
  return result;
}
```

在请求处理里调用 `context.agent`，不要在 `start` 里调用。服务重启会取消进行中的回合。

### 事件

下表概述常用事件；完整字段、忙时跟进事件，以及运行和观察事件的区别见 [Agent 事件参考](https://agent.minimaxi.com/docs/miniapp/reference/agent-api.md#events)。

| 事件 | 含义 |
| - | - |
| `run.queued` | 运行名额已满，正在排队；`position` 是队列中的位置 |
| `turn.started` | 回合开始，带实际使用的 `model` |
| `text.delta` | 流式输出的下一段回复 |
| `message.committed` | 一条完整的助手消息；一个回合可以有多条 |
| `tool.started` / `tool.finished` | 调用了某个工具 / 工具返回，`ok` 为 `false` 表示失败 |
| `permission.requested` | 工具等待审批，带 `requestId`、`reason` 和可选的 `toolInput` |
| `question.requested` | 回合暂停在问卷上，在 `turn.finished` 之前发出 |
| `turn.finished` | 回合的最后一个事件，带和结果相同的 `status`、`text`、`error`；以 `steered` 结束的跟进调用不单独发此事件 |

忽略不认识的事件类型，后续可能会增加新类型。

## 设计建议

* 把任务、数据范围、需要的交互、成功标准和停止条件都写进 `text`。页面上的标签、表单状态和宿主聊天的上下文不会自动带进去。
* `deny` 只拒绝这一次工具请求，不代表这个文件以后都不能访问。需要跨调用生效的限制，应通过宿主权限或 Hook 规则实施。
* 根回合 `finished` 不代表后台子任务都完成了。需要子任务结果时，在业务请求里使用前台委派。
* `AGENT_OUTCOME_UNKNOWN` 表示回合可能已经执行过。先核对会话状态，不要自动重试。
