Headless 与 ACP

mcode exec 的参数、输出格式与退出码,无人值守时登录与权限的处理,以及 mcode acp 如何把 ACP 会话映射到运行时、把权限询问转给编辑器。

作者 David更新于 第 31 篇(共 33 篇)

除了交互式 TUI,mcode 还有两个给程序用的入口:mcode exec 跑一次任务就退出,适合脚本与 CI;mcode acp 把自己变成 Agent Client Protocol 服务端,让编辑器来驱动。两者的代码都在 packages/tui 里,分别是 src/headless 与 src/acp。官方分层图把它们与 TUI 并列放在最上层(docs/architecture.md:3):

`TUI / exec / ACP → CliService → local Applications → Session / Turn / Agent services → Pi / model providers / local tools`.

三个入口驱动的是同一个进程内运行时,区别只在创建运行时时传入的表面:headless 或 acp。这两种表面都以 startupExecutionPolicy: 'quarantined' 启动(packages/tui/src/runtime/lifecycle.ts:455),效果是冷启动时不去执行从磁盘恢复的后台作业,交互请求不受影响(packages/local-runtime-v2/src/runtime.ts:831)。

mcode exec:怎么用

参数定义在 packages/tui/src/cli/contract.ts:88:

参数说明
[prompt] 或 --input -提示词来自参数或 stdin,二者只能取一;--input-format json 接受 JSON 字符串或带 prompt 字段的对象
--file <path>附件,可重复,最多 10 个、合计 100 MB
--cwd <path>工作区,缺省为当前目录
--model <provider/model>只对本次运行生效,可带 #variant
--effort <level>本次的推理强度,开跑前按模型声明的档位校验
--session <id>、--continue接着已有会话跑;后者取该工作区最近的会话
--permission <policy>smart(缺省)、full、off
--timeout <duration>、--max-steps <count>如 30s、2m;步数按完成的 assistant 消息计
--output-format <format>text(缺省)、json、stream-json
--output-schema <schema>JSON Schema 文件或内联对象,约束最终答案
-o, --output-last-message <path>成功时把最终答案写进文件
--diagnostics-dir <path>把有界的执行诊断写进一个空目录
--config <path>本进程改用指定的运行时配置文件
--prompt-mode <mode>tui(缺省)、coding、work

子命令 mcode exec review 审查暂存、未暂存与未跟踪的本地改动,不收提示词参数,用的是写死的一句(packages/tui/src/headless/invocation.ts:233):

Please review my uncommitted changes.

几种典型用法:

# 一次性任务,拿机器可读的结果
mcode exec "Run the unit tests and summarize failures" --permission full --timeout 10m --output-format json

# 从 stdin 读提示词,按 JSON Schema 约束最终答案并另存一份
cat task.txt | mcode exec --input - --output-schema schema.json -o answer.json

# 审查未提交的改动,逐事件输出 JSONL
mcode exec review --output-format stream-json

退出码是契约的一部分(packages/tui/src/headless/exit-policy.ts:4):

export const MINIMAX_CODE_EXEC_EXIT_CODES = Object.freeze({
  success: 0,
  invocation: 2,
  config: 3,
  runtime: 4,
  timeout: 6,
  limit: 7,
  internal: 70,
  cancelled: 130,
  brokenPipe: 141,
});

resolveTuiExecInvocation 检出的参数错误是 2;配置有误或需要登录是 3;运行中失败是 4;--timeout 到点是 6,--max-steps 用尽是 7;内部错误是 70;收到 SIGINT、SIGTERM、SIGHUP 是 130;stdout 的管道被关(EPIPE)是 141,这时连 stderr 的诊断也不再写。

一次 exec 走过哪些步骤

入口 packages/tui/src/cli/run-exec-command.ts 解析参数、准备数据目录、以 surface: 'headless' 创建运行时,然后交给 runTuiExec(packages/tui/src/headless/runner.ts:114):

  1. 登录检查:带着 --model 做一次只读的账号状态检查,见下一节。
  2. 定位会话:新建会话时以提示词作标题,超过 80 个字符就截断;--session 与 --continue 要求会话属于同一工作区,归档会话与子 Agent 会话都不能用(runner.ts:841)。
  3. 排查未决交互:会话里若有待答问卷或待批权限,直接失败,提示回 TUI 或 ACP 处理(runner.ts:601)。
  4. 确定模型:--model 必须写成 provider/model 或 provider/model#variant;给了 --effort 才去读模型列表,逐项校验档位,不支持思考或档位不在列表里都在开跑前报错(packages/tui/src/headless/model-selection.ts:55)。
  5. 执行回合:交给 TuiRunCoordinator,策略里总带 requireAnswer: true,跑完没有最终答案记为 EMPTY_RESPONSE;--timeout 由客户端计时器主动停止回合(packages/tui/src/application/run-coordinator.ts:197)。
  6. 盯住回合:ExecRunSupervisor 消费事件流,流断了就按游标重连或回读本回合历史,连续恢复超过 5 次仍无进展才放弃(packages/tui/src/headless/supervisor.ts:40)。
  7. 收尾:校验结构化输出,在 1 秒预算内补齐实际模型与用量(runner.ts:637),关闭运行时,最后才写 stdout 与 -o 文件。

登录检查:模型覆盖只读地参与判断

仓库文档对这一点有专门的一句(docs/architecture.md:22):

Headless model overrides are passed to a read-only account-state check so authentication requirements follow the model selected for that run.

对应的代码在 runner.ts:182:

    const loginModel = input.model ? parseHeadlessModelOverride(input.model) : undefined;
    await requireTuiAgentAccess(
      dependencies.runtime, input.sessionId, undefined, undefined,
      loginModel ? { model: loginModel } : undefined,
    );

模型一路传下去:TUI 适配器把它拼成 provider/model 交给 CliService 的 getAccountStatus(packages/tui/src/runtime/adapters/product-access.ts:56),最终落到宿主的 getProcessLocalAccountStatus。这里只在内存里拷一份配置、把默认模型换成本次指定的,再据此生成账号与模型的诊断快照(packages/local-runtime/src/api/host.ts:1532):

    const effectiveModel = model ?? session?.effectiveModel;
    const config = effectiveModel
      ? { ...baseConfig, defaultModel: effectiveModel }
      : baseConfig;

全局配置与会话模型都没有被改动。拿到状态后,tuiAgentAccessNeedsLogin 只在两种情况下要求登录:状态本身是 needs-login,或者模型走 Token Plan 的托管登录却没有托管令牌(packages/tui/src/application/login-gate.ts:34)。所以 --model 指向自带密钥(BYOK)的提供方时,未登录也能跑,与 docs/tui-capabilities.md 的描述一致。需要登录却没登录时抛 TuiLoginRequiredError,归类为配置错误,退出码 3。

无人值守时的权限

--permission 映射到运行时的权限模式(packages/tui/src/cli/run-exec-command.ts:129):smart 对应 auto,full 对应 bypassPermissions,off 就是 off。交互式的 ask 在解析参数时就被拒绝,错误信息让用户改用 TUI、ACP 或另外三档(invocation.ts:119)。三档的差别见权限核心的模式画像(packages/agent-modules/permission/src/permission-core.ts:523):bypassPermissions 与 off 都不询问,但前者仍是硬闸门,off 则让权限层不再拦截任何执行;auto 允许分类器参与判定,交互方式却仍是“询问”。

headless 运行时在创建时就声明没有权限弹窗、问卷与 elicitation 能力(lifecycle.ts:450)。所以 smart 模式下若判定仍落到“需要询问”,审批环节立刻拒绝,而不是挂起等人(packages/local-runtime/src/api/hosted-agent-permission-capabilities.ts:93),错误信息是:

HOST_CAPABILITY_UNAVAILABLE: this Runtime host cannot prompt for permission.

万一回合还是进入了等待用户决定的 decision-blocked 状态,监督器报 INTERACTION_NOT_AVAILABLE 收场(supervisor.ts:211);以 awaiting-user-continuation 结束的回合也同样改记为失败(packages/tui/src/headless/contract.ts:148)。

输出契约

  • text:只在成功时往 stdout 写最终答案,失败原因一律写 stderr(packages/tui/src/headless/output.ts:61)。
  • json:结束时输出一行 ExecResult。
  • stream-json:逐行输出事件,依次是 exec.started、session.started 或 session.resumed、turn.started,中间是 item.started、item.updated、item.completed(条目分 agent_message、reasoning、tool_call 三类),最后 turn.completed 或 turn.failed,再以携带完整结果的 exec.completed 收尾;每个事件带 schemaVersion、递增的 sequence 与时间戳(packages/tui/src/headless/events.ts:27)。

ExecResult 的形状(headless/contract.ts:37):

export interface ExecResult {
  readonly schemaVersion: 1;
  readonly type: 'exec.result';
  readonly runId: string;
  readonly sessionId: string;
  readonly turnId: string;
  readonly status: ExecResultStatus;
  readonly output?: unknown;
  readonly error?: ExecError;
  readonly model?: ExecModelIdentity;
  readonly usage?: ExecTokenUsage;
  readonly usageSource?: ExecUsageSource;
  readonly usageIncomplete?: boolean;
  readonly durationMs: number;
}

用了 --output-schema 时,最终答案先按 JSON 解析,再用 Ajv 校验;空答案、非 JSON、不合 schema 都把状态改成失败,错误码 STRUCTURED_OUTPUT_INVALID(headless/contract.ts:160)。-o 的文件先写临时文件再改名,只在成功时写(output.ts:73)。--diagnostics-dir 必须是空目录,事件记录上限 4 MiB、答案 256 KiB,形似凭据的字段一律打码(packages/tui/src/headless/diagnostics.ts:18)。参数名、事件名与 Codex 的非交互模式多有相同,可以对照着读。

mcode acp:让编辑器来驱动

mcode acp 以 stdio 提供服务(packages/tui/src/cli/program.ts:107),编辑器把它当子进程启动,在 stdin、stdout 上交换换行分隔的 JSON-RPC 消息(packages/tui/src/acp/stdio.ts:17)。实现基于 @agentclientprotocol/sdk 1.3.0(packages/tui/package.json:23)。为了不污染协议流,进程里的 console 输出全部改走 stderr(packages/tui/src/cli/run-acp-command.ts:86);一个进程只接受一个客户端连接(packages/tui/src/acp/agent.ts:318)。

initialize 的回答说明了能力边界(agent.ts:399):

    return {
      protocolVersion: acp.PROTOCOL_VERSION,
      agentCapabilities: {
        loadSession: true,
        mcpCapabilities: {
          http: true,
          sse: true,
        },
        promptCapabilities: {
          image: false,
          audio: false,
          embeddedContext: false,
        },
        sessionCapabilities: {
          list: {},
          fork: {},
          resume: {},
          close: {},
        },
      },

提示词只收文本与 resource_link 两种内容块,后者被改写成一行写明资源标题与 URI 的文本(agent.ts:2098)。客户端声明支持终端认证时,还会得到一个参数为 ['login'] 的认证方法;acp 下正好挂着一个与顶层 mcode login 相同的 login 子命令(program.ts:115)。新建、加载、恢复、列出会话前都先查登录状态,未登录时返回 authRequired 错误,提示先运行 mcode login(agent.ts:2087)。

协议之上还有几层 MiniMax 自己的东西:

  • 斜杠命令:/help、/model、/status、/mcp、/compact 等 10 个命令由适配器就地处理,不经模型,结果作为一条 agent_message_chunk 发回(packages/tui/src/acp/commands.ts:7);/new 只提示在客户端里新开一个会话。
  • 模式与配置项:模式只有 default 与 plan,切换在下一个提示时生效;配置项有权限模式、模型、思考强度三个(packages/tui/src/acp/control-state.ts:12)。其中权限模式可选 default、auto、bypassPermissions,并标注 'minimax-code/scope': 'process',改的是整个进程,不只当前会话(control-state.ts:151)。
  • 扩展方法:mcode/session/steer、mcode/session/queue/list 等队列方法,以及目标与委派相关的一组方法,列表通过 initialize 返回的 _meta 告知客户端(packages/tui/src/acp/extensions.ts:17)。

会话映射

下文 session/new 这类名字是 ACP 协议规定的方法名,代码里通过 SDK 的 acp.methods 常量引用。ACP 的会话 ID 就是运行时的会话 ID:session/new 以请求里的 cwd 创建运行时会话,客户端带来的 mcpServers 注册为这个会话专属的 MCP 服务器(见 MCP),然后直接返回运行时的 ID(agent.ts:519)。其余请求的对应关系:

ACP 请求做什么
session/load挂接已有会话并按页回放全部历史,每页 100 条
session/resume挂接已有会话,不回放
session/list列出含归档在内的会话,跳过子 Agent 会话
session/fork在运行时里分叉会话
session/prompt跑一个回合;成功返回 end_turn,取消返回 cancelled,步数超限返回 max_turn_requests,其余作为内部错误抛出(agent.ts:1166)
session/cancel中止进行中的提示;没有进行中的提示时,留一个一次性标记,拦下紧随其后到达的那一个
session/close断开挂接,中止进行中的提示,清掉会话级 MCP

挂接已有会话时,cwd 必须与会话记录的工作区一致,子 Agent 会话不能直接打开,additionalDirectories 不支持(agent.ts:1654)。子 Agent 在运行时里另开会话,适配器用 runtimeParentSessionIds 记下父子关系;子会话里冒出的权限询问沿着父链找到挂接中的那个 ACP 会话,再转给编辑器(agent.ts:1533)。

权限询问怎样转给编辑器

ACP 表面不是 headless,运行时保有权限弹窗能力:需要询问时,它发出 permission.ask 事件并等待回复。适配器在 runTuiAcpInteractions 里订阅运行时事件(packages/tui/src/acp/interactions.ts:68),把询问翻译成 session/request_permission:

图表加载中…

给编辑器的选项是 allow-once、deny,运行时支持时再加一个 allow-always(interactions.ts:898)。处理逻辑处处按拒绝兜底(interactions.ts:560):

  let decision: 'allowOnce' | 'allowAlways' | 'deny' = 'deny';
  try {
    const response = await options.connection.client.request(
      acp.methods.client.session.requestPermission,
      permissionRequest(resolved.acpSessionId, requestId, event.request),
      {
        cancellationSignal: AbortSignal.any([
          options.connection.signal,
          resolved.attachmentSignal,
          schedulerSignal,
        ]),
      },
    );
    if (response.outcome.outcome === 'selected') {
      decision =
        response.outcome.optionId === 'allow-always' && event.request.allowAlwaysSupported
          ? 'allowAlways'
          : response.outcome.optionId === 'allow-once'
            ? 'allowOnce'
            : 'deny';
    }
  } catch {
    decision = 'deny';
  }

请求出错、被取消、客户端没选任何项,或者回复时会话已被别的挂接取代,结果都是 deny。Agent 向用户提的问卷走另一条路:客户端声明支持表单式 elicitation 时发 elicitation/create;否则退而借 session/request_permission 的选项形式只问第一步,而且第一步必须是不许自由填写的单选题,其余步骤记为跳过;条件不满足就撤销问卷(interactions.ts:607)。工具调用的 kind 按工具名里的关键词粗分成 read、edit、execute 等几类(packages/tui/src/acp/updates.ts:124)。

对照 OpenCode 的 ACP 实现:那边先起一个真正的 HTTP 服务端,再用 SDK 连回自己;这里的适配器直接拿着 TUI 用的那个进程内运行时适配器(run-acp-command.ts:67),与 TUI、mcode exec 地位相同。

下一篇:遥测、更新与诊断——使用统计的开关、mcode update 的更新流程与诊断上报。

本页目录