> ## 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](/docs/miniapp/authors/agent#mcp)：Mini App 的 Node 服务作为 MCP 服务器，向 Agent 暴露工具 | 已发布 | 按 Agent 现有的工具权限 |
| Mini App → 输入框 | [message.append](/docs/miniapp/authors/client-api#append)：把页面内容追加到输入框 | 已发布 | 宿主确认框，用户自己发送 |
| Mini App → Agent 会话 | [Agent API](/docs/miniapp/authors/agent#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](/docs/miniapp/authors/client-api#append)。如果要让 Agent 修改表格、卡片等业务对象，给它提供 MCP 工具，并在请求和工具里用稳定的业务 ID。输入框里的文字用户可以改、可以删，不能当作请求和回合之间的关联 ID。

## Agent API

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

### 开启

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

* 不写 `agentAccess` 就是禁止。唯一合法值是 `"session.run"`，其他取值发布时以 `MINIAPP_AGENT_ACCESS_INVALID` 拒绝。
* `agentConcurrency` 可选，取 1 到 32，默认 4：这个 Mini App 同时运行的回合数。超出的调用按顺序排队，队列最多是并发数的 4 倍。每个会话同一时间只有一个回合。
* 授权跟着进程生效：改了 `agentAccess` 并重新发布，服务会重启。

### 会话属于谁

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

这些会话不出现在用户的会话列表里。每个 Mini App 只能续用自己创建的会话，续用时 agent 和目录不变；`run` 和 `answer` 可以传 `model` 切换模型，必须是用户在 `models()` 里能看到的模型。这些会话和聊天之间的完整关系见 [Mini App 与会话](/docs/miniapp/sessions#agent-sessions)。

### 方法

| 方法 | 页面 | Node | 作用 |
| - | - | - | - |
| `run({ text, sessionId?, model?, agent? })` | ✓ | ✓ | 运行一个回合，结束时返回 `{ sessionId, turnId, status, text?, error?, pendingQuestion? }`；`status` 为 `finished`、`error` 或 `aborted` |
| `reply({ sessionId, requestId, decision })` | ✓ | ✓ | 处理工具审批，`decision` 为 `allowOnce` 或 `deny` |
| `answer(...)` | ✓ | ✓ | 回答暂停中的问卷并观察续跑的回合。页面用 `questionId` 指定问卷，Node 用 `requestId` |
| `cancel(requestId)` | ✓ | — | 取消页面发起的调用；Node 用 `signal` 取消 |
| `open(sessionId)` | ✓ | — | 在宿主聊天侧栏打开这个会话，让用户在那里交互 |
| `models()` / `agents()` | ✓ | ✓ | 列出可用模型和用户配置的 agent |
| `create({ agent? })` | ✓ | ✓ | 只创建会话、不运行，返回 `{ sessionId }` |

### 审批和问卷

![谁能回答审批](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` 里调用。服务重启会取消进行中的回合。

### 事件

| 事件 | 含义 |
| - | - |
| `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` |

不认识的事件类型直接忽略，以后可能还会增加。

## 设计建议

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