AI工具配置 6

用 ACP 把 Grok Build 接到 IDE 和其他应用

Grok Build 日常入口是全屏 TUI:不带参数执行 grok,打开能点鼠标的终端界面。脚本里发一条提示就结束,用 -p。要把同一套编码 agent 嵌进 IDE,或接到自己写的工具里,走第三条入口:Agent Client Protocol(ACP)。

对应命令是 grok agent stdio。它把 Grok Build 跑成一个 ACP agent,宿主应用在 stdin/stdout 上用 JSON-RPC 驱它,不必再包一层 TUI。

三种入口里 ACP 站在哪

三条路用的是同一套 agent,差在谁来交互。

TUI 给人盯着终端用。无头 -p 给脚本和机器人:发一条,跑完退出,stdout 留给你解析。ACP 给的是应用嵌入:编辑器、内部工具、自己写的宿主把 Grok Build 当 ACP agent 来驱,不再另开一次终端会话。

直接打 grok-4.6 的 xAI API 是另一条路。你已经有自己的 agent loop、IDE 插件或编码工具时,可以不经过 Grok Build 进程。ACP 做的是把进程本身接到应用里:会话、工具调用、落盘仍由 Grok Build 负责。

启动与鉴权

拉起 agent:

bash
grok agent stdio

进程在 stdin/stdout 上讲 JSON-RPC。跑示例之前,本机需要已经登录过,或者环境里有 XAI_API_KEY

客户端先发 initialize。返回值里有 authMethods,每项带一个 id。示例按这个顺序挑一种:环境变量里已经有 XAI_API_KEY,并且方法列表含 xai.api_key,就用 API Key;否则用本机缓存的 cached_token。两种都对不上,就停下来,提示先 grok login,或设置 XAI_API_KEY

javascript
const authMethods = new Set((init.authMethods ?? []).map(method => method.id));
const methodId =
  process.env.XAI_API_KEY && authMethods.has("xai.api_key")
    ? "xai.api_key"
    : authMethods.has("cached_token")
      ? "cached_token"
      : null;

if (!methodId) {
  throw new Error("Run `grok login` first, or set XAI_API_KEY.");
}

await request("authenticate", { methodId, _meta: { headless: true } });

两边都有的时候,示例会走 xai.api_key,不会仅仅因为本机登录过就忽略环境变量里的 Key。authenticate 里的 _meta.headless: true 跟着示例写即可。

一次最小会话回路

鉴权之后按这个顺序发请求:initializeauthenticatesession/newsession/prompt。每条都是 JSON-RPC 2.0,一行一个 JSON,写进 stdin。

initialize 要带协议版本和客户端能力。示例里的版本是 1,并声明可以读写文本文件、使用终端:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": 1,
    "clientCapabilities": {
      "fs": { "readTextFile": true, "writeTextFile": true },
      "terminal": true
    }
  }
}

session/new 给出工作目录。返回值里的 sessionId 后面每一步都要用:

javascript
const { sessionId } = await request("session/new", {
  cwd: process.cwd(),
  mcpServers: [],
});

然后把用户话放进 session/promptprompt 是数组,文本块的形状是 { type: "text", text: "..." }

javascript
const prompt = await request("session/prompt", {
  sessionId,
  prompt: [{ type: "text", text: "Say hello in one short sentence." }],
});

session/prompt 的返回值是完成元数据,示例会读 stopReason。助手正文不在这个 result 里,而是作为通知推过来。

通知的方法名是 session/update。当 update.sessionUpdate 等于 agent_message_chunk 时,片段在 update.content.text。客户端按行读 stdout,把这些 chunk 拼起来:

javascript
if (message.method === "session/update") {
  const update = message.params?.update;
  if (update?.sessionUpdate === "agent_message_chunk" && update.content?.text) {
    text += update.content.text;
  }
  return;
}

stdout 上请求应答和 session/update 会混在一起。示例先看 message.method === "session/update",命中就当通知处理,再拿 message.id 去对还没完成的请求。宿主要两边都收。

下面这份示例拉起进程、走完最小回路。超时、以及 session/prompt 返回后再等文本长度连续两次不变,属于示例客户端自己的等待逻辑,不是协议字段。

javascript
import { spawn } from "node:child_process";
import readline from "node:readline";
import process from "node:process";

const proc = spawn("grok", ["agent", "stdio"], { stdio: ["pipe", "pipe", "pipe"] });
const rl = readline.createInterface({ input: proc.stdout });
const pending = new Map();
let nextId = 1;
let text = "";

proc.stderr.on("data", chunk => process.stderr.write(chunk));

rl.on("line", line => {
  const message = JSON.parse(line);

  if (message.method === "session/update") {
    const update = message.params?.update;
    if (update?.sessionUpdate === "agent_message_chunk" && update.content?.text) {
      text += update.content.text;
    }
    return;
  }

  const pendingRequest = pending.get(message.id);
  if (!pendingRequest) return;

  pending.delete(message.id);
  if (message.error) {
    pendingRequest.reject(new Error(message.error.message ?? JSON.stringify(message.error)));
  } else {
    pendingRequest.resolve(message.result ?? {});
  }
});

function request(method, params, timeoutMs = 30000) {
  const id = nextId++;

  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      pending.delete(id);
      reject(new Error(`${method} timed out`));
    }, timeoutMs);

    pending.set(id, {
      resolve(result) {
        clearTimeout(timer);
        resolve(result);
      },
      reject(error) {
        clearTimeout(timer);
        reject(error);
      },
    });

    proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
  });
}

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

try {
  const init = await request("initialize", {
    protocolVersion: 1,
    clientCapabilities: {
      fs: { readTextFile: true, writeTextFile: true },
      terminal: true,
    },
  });

  const authMethods = new Set((init.authMethods ?? []).map(method => method.id));
  const methodId =
    process.env.XAI_API_KEY && authMethods.has("xai.api_key")
      ? "xai.api_key"
      : authMethods.has("cached_token")
        ? "cached_token"
        : null;

  if (!methodId) {
    throw new Error("Run `grok login` first, or set XAI_API_KEY.");
  }

  await request("authenticate", { methodId, _meta: { headless: true } });

  const { sessionId } = await request("session/new", {
    cwd: process.cwd(),
    mcpServers: [],
  });

  const prompt = await request("session/prompt", {
    sessionId,
    prompt: [{ type: "text", text: "Say hello in one short sentence." }],
  });

  let lastLength = -1;
  let stableChecks = 0;
  while (stableChecks < 2) {
    await sleep(150);
    if (text.length === lastLength) {
      stableChecks += 1;
    } else {
      lastLength = text.length;
      stableChecks = 0;
    }
  }

  console.log(text.trim() || `No text returned (stopReason=${prompt.stopReason})`);
} finally {
  rl.close();
  proc.kill();
}

会话与 TUI 是同一套

Grok Build 会自动把每次对话写到磁盘:提示、回复、工具调用、文件快照,按工作目录归档在 ~/.grok/sessions/。TUI、无头、ACP 用的是同一套落盘。

session/new 返回的 sessionId 就是这条会话的 ID。关掉宿主之后,可以在终端里续:

bash
grok --resume <session-id>
grok --resume
grok -c

带 ID 的 --resume 恢复指定会话。不带 ID 的 --resume 恢复当前目录最近一次;-c 是同一件事的简写。TUI 里用 /resume 打开当前工作区的近期会话列表,欢迎屏也会列出这些会话。

无头侧若要从 JSON 里取出 ID 再续跑,和 ACP 拿到的是同一类标识:

bash
grok -p "Start the refactor" --output-format json | jq -r '.sessionId'

IDE 里开过的对话,只要还拿着那个 sessionId、还在同一个工作目录,回到终端 grok --resume 就能接着聊。

感谢阅读,如果这篇文章对你有帮助,欢迎继续浏览同栏目内容。

返回 AI工具配置