首页
/ Gemini CLI SDK 设计剖析:用代码构建可编程的 AI Agent(GeminiCliAgent、工具、会话与 SessionContext)

Gemini CLI SDK 设计剖析:用代码构建可编程的 AI Agent(GeminiCliAgent、工具、会话与 SessionContext)

2026-09-06 15:15:13作者:余洋婵Anita

Gemini CLI 不仅是一个交互式终端工具,它还在仓库中提供了一个官方 SDK——@google/gemini-cli-sdk,将 Gemini 的 Agent 循环、工具执行与会话管理封装为可编程的 TypeScript API。本文以 SDK 的设计文档 SDK_DESIGN.md 为主体,逐条对照 packages/sdk/src/ 下的真实源码,说明哪些能力已经落地(Agent 会话、系统指令、自定义工具、Skills、SessionContext)、哪些仍处于规划状态(Hooks、Subagents、扩展、ACP),并深入剖析 SDK 与核心包 @google/gemini-cli-core 之间的调用关系,帮助你理解如何在自己的 Node.js 应用中嵌入一个可控、可恢复、可扩展的 Gemini Agent。

一、SDK 定位与实现状态总览

设计文档开宗明义地给出了实现状态声明:核心 Agent 循环(agent loop)、工具执行、会话上下文已经实现,而 Hooks、Skills(文档初稿阶段)、Subagents、ACP 等高级特性当时尚未就绪(见 SDK_DESIGN.md)。

对照当前仓库源码,落地情况如下:

设计文档中的特性 设计文档状态 当前仓库实际状态(据源码)
Simple Example(会话/流式) Implemented 已实现,见 session.ts
System Instructions(静态/动态) Implemented 已实现,动态指令在每轮循环注入
Custom Tools(tool() 助手) Implemented 已实现,见 tool.ts
Custom Hooks Not Implemented 源码中 enableHooks: false,仍未开启
Custom Skills Implemented 已实现,见 skills.ts
Subagents / Extensions / ACP Not Implemented 未见对应实现模块
Approvals / Policies Not Implemented 当前默认策略为直接放行(见下文)

SDK 的公开导出面非常克制,index.ts 仅导出五个模块:agentsessiontoolskillstypes。安装方式为:

npm install @google/gemini-cli-sdk

SDK 与 CLI 的关系可以这样理解:CLI 面向终端用户,SDK 则把同一套核心(认证、工具调度、会话持久化、策略引擎)暴露给开发者,二者共享 @google/gemini-cli-core 这一底座。

二、Simple Example:Agent、Session 与流式输出

设计文档的第一个示例等价于命令行 gemini -p "what does this project do?"——它会加载工作区与用户的全部设置:

import { GeminiCliAgent } from '@google/gemini-cli-sdk';

const simpleAgent = new GeminiCliAgent({
  cwd: '/path/to/some/dir',
});

// 创建一个全新的空会话
const session = simpleAgent.session();

// 也可以按 ID 恢复已有会话
// const session = await simpleAgent.resumeSession('some-session-id');

for await (const chunk of session.sendStream('what does this project do?')) {
  console.log(chunk); // 等价于 JSON 流式块
}

GeminiCliAgent:配置容器 + 会话工厂

agent.ts 可以看到,GeminiCliAgent 本体非常轻量——构造函数只是保存 GeminiCliAgentOptions,真正的重活全部委托给 GeminiCliSession。它提供两个会话入口:

  • session(options?):创建新会话。若未提供 sessionId,则调用核心包的 createSessionId() 生成一个新的 UUID 风格 ID,并返回 new GeminiCliSession(this.options, sessionId, this)
  • resumeSession(sessionId):按 ID 恢复历史会话。实现细节很有代表性:
    1. this.options.cwd || process.cwd() 构造核心包的 Storage(cwd) 并初始化;
    2. 调用 storage.listProjectChatFiles() 列出项目聊天记录文件;
    3. 利用"文件名包含 sessionId 前 8 位"的约定做候选过滤(sessionId.slice(0, 8)),若过滤为空则回退为全量检查;
    4. 逐文件调用 loadConversationRecord(absolutePath),以完整 sessionId 精确匹配;
    5. 找到后将 conversationfilePath 组装成 ResumedSessionData 传给会话构造函数;找不到则抛出 Session with ID ${sessionId} not found

这意味着会话历史是持久化到磁盘的(项目临时目录下的 chats 子目录,错误信息中明确出现了 path.join(storage.getProjectTempDir(), 'chats')),"transcript 保存在内存中同时默认落盘"这一设计文档中的开放问题,从源码结构看答案是肯定的——恢复逻辑正是建立在磁盘持久化之上。

GeminiCliSession.sendStream:完整 Agent 循环

sendStream 是 SDK 的心脏,session.ts 展示了完整的 agentic loop:

while (true) {
  // 1. 若指令是动态函数,构造 SessionContext 并刷新系统指令
  if (typeof this.instructions === 'function') {
    const newInstructions = await this.instructions(context);
    this.config.setUserMemory(newInstructions);
    client.updateSystemInstruction();
  }

  // 2. 发送请求,流式接收事件,收集 ToolCallRequest
  const stream = client.sendMessageStream(request, abortSignal, sessionId);
  for await (const event of stream) {
    yield event;
    if (event.type === GeminiEventType.ToolCallRequest) {
      toolCallsToSchedule.push({ ...toolCall, args, isClientInitiated: false, prompt_id: sessionId });
    }
  }

  // 3. 无工具调用则结束循环
  if (toolCallsToSchedule.length === 0) break;

  // 4. 通过核心包的调度器执行工具
  const completedCalls = await scheduleAgentTools(this.config, toolCallsToSchedule, {
    schedulerId: sessionId, toolRegistry: scopedRegistry, signal: abortSignal,
  });

  // 5. 工具响应作为下一轮请求,继续循环
  request = completedCalls.flatMap((call) => call.response.responseParts);
}

几个值得注意的实现要点:

  • 懒初始化sendStream 开头会检查 this.initialized,未初始化时自动调用 initialize(),所以用户无需手动初始化。
  • 动态指令每轮刷新:动态 instructions 函数在每一轮模型请求前都会以完整的 SessionContext 被重新调用,结果经 config.setUserMemory() 写入并通过 client.updateSystemInstruction() 生效——这就是"动态系统指令"的落地方式。
  • 工具上下文绑定:执行工具前,SDK 会克隆工具注册表(originalRegistry.clone()),并覆写 getTool:凡返回 SdkTool 实例时,调用 tool.bindContext(context) 把当前轮的 SessionContext 绑定进去。这保证了工具的 action(params, context) 第二参拿到的是当轮新鲜的上下文(transcript、时间戳),而非构造时的快照。
  • 工具参数兼容:模型返回的工具参数若是 JSON 字符串,会先 JSON.parse 再交给调度器。

会话初始化都做了什么

initialize()session.ts)按序完成:

  1. 认证getAuthTypeFromEnv() || AuthType.COMPUTE_ADC,然后 config.refreshAuth(authType)——即 SDK 沿用核心包的认证体系(环境变量优先,回退到 Cloud ADC)。
  2. 加载 Skills:遍历 skillRefs,对 type === 'dir' 的引用调用核心包的 loadSkillsFromDir(ref.path),加载成功后 skillManager.addSkills(),并重新注册 ActivateSkillTool 使模型可主动激活技能。
  3. 注册自定义工具:每个 SDK 工具包装成 SdkTool(toolDef, messageBus, this.agent, undefined) 后注册进 toolRegistry
  4. 恢复历史:若携带 resumedData,把 conversation.messages 映射为 { role, parts } 形式(type === 'gemini' 映射为 model 角色),调用 client.resumeChat(history, resumedData) 重建对话。

构造函数中还有一组值得关注的默认配置session.ts):

const configParams: ConfigParameters = {
  sessionId: this.sessionId,
  targetDir: cwd,
  cwd,
  debugMode: options.debug ?? false,
  model: options.model || PREVIEW_GEMINI_MODEL_AUTO,  // 默认自动选模型
  userMemory: initialMemory,                            // 静态 instructions 直接作为初始记忆
  enableHooks: false,      // Hooks 未接入
  mcpEnabled: false,       // MCP 未开启
  extensionsEnabled: false, // 扩展未开启
  recordResponses: options.recordResponses,
  fakeResponses: options.fakeResponses,
  skillsSupport: true,
  adminSkillsEnabled: true,
  policyEngineConfig: {
    // TODO: 待审批机制接入后再调整
    defaultDecision: PolicyDecision.ALLOW,
  },
};

这里印证了设计文档中 "Approvals / Policies: Not Implemented" 的状态——SDK 会话当前把策略引擎默认决策设为 ALLOW,源码注释也明确写着"待建立审批接线机制后再重议此默认值"。使用 SDK 时应对此有明确预期:它运行在无交互确认的环境下。

三、GeminiCliAgentOptions:完整配置项说明

types.ts 中的 GeminiCliAgentOptions 是 Agent 的全部构造配置:

字段 类型 默认值 说明
instructions string | ((ctx) => string | Promise<string>) 必填(类型上) 系统指令;静态字符串或接收 SessionContext 的动态函数
tools Array<Tool<any>> [] tool() 助手定义的工具列表
skills SkillReference[] [] 通过 skillDir() 引用的技能目录
model string PREVIEW_GEMINI_MODEL_AUTO 指定 Gemini 模型,缺省自动选择
cwd string process.cwd() 工作目录,决定设置加载与文件策略的根
debug boolean false 调试模式,输出详细日志
recordResponses string 记录模型响应到指定文件,用于调试/回放
fakeResponses string 从指定文件加载伪造响应,用于确定性测试

其中 recordResponses / fakeResponses 两个选项直接回应了设计文档 Notes 一节"需要一种强大的方式 mock 底层模型 API,使测试更接近端到端但仍可确定"的诉求:录制真实响应、再回放伪造响应。仓库的 test-data/ 目录下即存放着 agent-static-instructions.jsonagent-async-instructions.jsonagent-resume-session.json 等用于测试的响应数据文件,agent.integration.test.tssession.test.ts 就基于这套机制验证会话恢复与指令行为。

四、System Instructions:静态字符串与动态函数

设计文档说明系统指令支持两种形态,且静态字符串会出现在模型调用中"GEMINI.md 内容通常出现的位置":

import { GeminiCliAgent } from '@google/gemini-cli-sdk';

const agent = new GeminiCliAgent({
  // 形态一:静态字符串
  instructions: 'This is a static string instruction',
  // 形态二:动态函数,可访问 SessionContext
  // instructions: (ctx) =>
  //   `The current time is ${new Date().toISOString()} in session ${ctx.sessionId}.`,
});

源码中两种形态的处理路径不同(session.tsL229-L244):

  • 静态字符串:在构造函数中直接作为 userMemory 写入 ConfigParameters,一次性生效;
  • 动态函数:每轮请求前以 SessionContext 调用,返回值经 setUserMemory + updateSystemInstruction 热更新到系统指令中。

types.ts 还附带了明确的安全警告:使用动态函数时,务必对来自会话上下文的输入做净化(去除换行符、],转义 <>),以防止提示注入——因为动态指令的返回值将直接拼进系统提示。

五、Custom Tools:tool() 助手、Zod 校验与错误语义

设计文档的自定义工具示例:

import { GeminiCliAgent, tool, z } from '@google/gemini-cli-sdk';

const addTool = tool({
  name: 'add',
  description: 'add two numbers',
  inputSchema: z.object({
    a: z.number().describe('first number to add'),
    b: z.number().describe('second number to add'),
  }),
}, (params: { a: number; b: number }) => ({ result: params.a + params.b }));

const toolAgent = new GeminiCliAgent({
  instructions: 'Assistant',
  tools: [addTool],
});

const session = toolAgent.session();
for await (const chunk of session.sendStream('what is 23 + 79?')) {
  console.log(chunk);
}

(设计文档中的原始示例存在语法瑕疵,如 ({a, b}) => ({result: a + b}),toolAgent.send(...);当前 SDK 并无 agent.send() 快捷方法,统一入口是 session().sendStream(),上文示例已按实际 API 修正。)

实现层面,tool.ts 提供了三层结构:

  1. tool(definition, action) 助手:把声明(name / description / inputSchema / 可选 sendErrorsToModel)与执行函数合并为 Tool<T> 对象;z 直接从 SDK 导出(export { z }),用户无需自行安装 zod。
  2. SdkTool extends BaseDeclarativeTool:注册到核心工具注册表。构造函数中调用 zodToJsonSchema(definition.inputSchema),把 Zod schema 转成 JSON Schema 供模型理解参数结构——这是"模型在 prompt 中收到工具定义"的实现路径。
  3. SdkToolInvocation.execute:执行 action(params, context),返回值的处理规则是——字符串原样返回,其他对象 JSON.stringify(result, null, 2),同时作为 llmContentreturnDisplay 回传给模型。

错误处理是一个容易被忽略但很实用的设计(tool.ts):

  • 工具抛错且开启 sendErrorsToModel 且不是 ModelVisibleError 时,异常直接向上抛出(对模型不可见);
  • sendErrorsToModel: true 或抛出专门的 ModelVisibleError,错误信息以 Error: ${message} 的形式回传给模型,使其能感知失败原因、自行重试或调整策略。
import { tool, z, ModelVisibleError } from '@google/gemini-cli-sdk';

const risky = tool(
  {
    name: 'query_api',
    description: 'Query the external API',
    inputSchema: z.object({ q: z.string() }),
    sendErrorsToModel: true, // 让模型看到失败原因
  },
  async ({ q }) => {
    // 或抛出 new ModelVisibleError('API rate limited'),同样对模型可见
    return { data: q };
  },
);

六、Custom Skills:目录结构与 skillDir 引用

设计文档规定技能以目录形式组织:

skill-dir/
  SKILL.md  (元数据与指令)
  tools/    (可选的工具目录)
    my-tool.js

用法(文档示例按当前 API 修正了包名):

import { GeminiCliAgent, skillDir } from '@google/gemini-cli-sdk';

const agent = new GeminiCliAgent({
  instructions: 'You are a helpful assistant.',
  skills: [
    skillDir('./my-skill'),          // 单个技能目录
    skillDir('./skills-collection'),  // 技能根目录:加载其下所有子目录技能
  ],
});

实现极简而清晰(skills.ts):SkillReference 目前只有一种类型 { type: 'dir'; path: string }skillDir(path) 只是工厂函数。真正的加载发生在 initialize() 中:SDK 调用核心包的 loadSkillsFromDir() 解析目录内的 SKILL.md(仓库内 test-data/skills/ 下有真实的测试技能文件),再注册 ActivateSkillTool 让模型在需要时激活对应技能。skills.integration.test.tstest-data/skill-dir-success.jsonskill-root-success.json 分别验证了"单个目录"与"技能根"两种加载路径的响应内容。

七、SessionContext:工具的第二参数与沙箱化访问

这是设计文档 "Implementation Guidance" 一节的核心,也是 SDK 区别于"裸调模型 API"的关键。文档给出的接口草案:

export interface SessionContext {
  sessionId: string;
  transcript: Message[];
  cwd: string;
  timestamp: string;

  fs: AgentFilesystem;   // 遵守策略/沙箱的文件系统
  shell: AgentShell;     // 遵守策略的命令执行
  agent: GeminiCliAgent;
  session: GeminiCliSession;
}

当前落地版(types.ts)与之高度一致,且更严格:transcriptreadonly Content[](只读对话记录)。每次 sendStream 轮次内,SDK 都会现造 SdkAgentFilesystemSdkAgentShell 并填入上下文,工具与动态指令通过它访问系统资源:

AgentFilesystem:读返回 null,写抛异常

SdkAgentFilesystem 在每次操作前先调用核心包的 config.validatePathAccess(path, 'read' | 'write')

  • readFile:路径校验失败或文件不存在时返回 null(把"禁止访问"与"文件不存在"合并为同一信号);
  • writeFile:路径校验失败时抛出错误,校验通过才真正 fs.writeFile

类型文档(types.ts)同时提醒:实现必须防路径穿越(..、null 字节),推荐用 resolveToRealPath 一类的健壮函数——从源码结构看,这部分职责目前委托给了核心 Config.validatePathAccess

AgentShell:无交互确认即拒绝

SdkAgentShell 展示了 SDK 模式下策略执行的实际行为,调用链为:

SdkAgentShell.exec(command, options)
  → new ShellTool(config, messageBus).build({ command, dir_path: cwd })
  → invocation.shouldConfirmExecute(signal)   // 需要交互确认?
      → 是:抛出 "requires confirmation but no interactive session is available"
  → ShellExecutionService.execute(command, cwd, ..., shouldUseNodePty: false, ...)

要点:

  • 需要确认的命令会被拒绝:SDK 是无头的,无法弹出交互确认框,因此 shouldConfirmExecute 为真时直接抛错并返回 exitCode: 1 加错误对象;
  • AgentShellOptions 支持 env(合并进默认环境变量)、timeoutSecondscwd 三个选项;
  • 一个当前的已知限制:由于底层 ShellExecutionService 会把 stderr 并入 stdout,返回结果中 stderr 字段目前恒为空字符串,outputstdout 是同一份合并输出——源码 JSDoc 已明确标注了这一行为;
  • 返回结构 AgentShellResult{ exitCode: number | null, output, stdout, stderr, error? }

八、尚未落地:Hooks、Subagents、Extensions、ACP、审批策略

设计文档还规划了五个尚未实现的特性,读源码可确认它们当前的边界:

Custom Hooks(未实现)

文档展示了目标形态——以 AfterTool 事件为例,在 write_file 后自动格式化 .ts 文件,并通过 ctx.fs 遵守沙箱:

const myHook = hook(
  { event: 'AfterTool', name: 'reformat', matcher: 'write_file' },
  (hook, ctx) => {
    const filePath = hook.toolInput.path;
    if (!filePath.endsWith('.ts')) return; // 返回 void 即 no-op
    const reformatted = await reformat(await ctx.fs.read(filePath));
    await ctx.fs.write(filePath, reformatted);
    return { hookSpecificOutput: { additionalContext: `Reformatted file ${filePath}...` } };
  },
);

文档还提出 hook.runAsCommand() 的"用户态命令式 hook"构想(解析 stdin、按语义设置退出码)。当前代码中 ConfigParameters.enableHooks 被硬编码为 falsesession.ts),且 "Next Steps" 一节把"为 GeminiCliAgentOptions 增加 hooks 选项、强类型映射事件名到请求/响应类型"列为后续任务——即该能力尚未接入 SDK。CLI 侧的 Hooks 体系(参见 docs/hooks/index.md)是独立于 SDK 的,本文不作展开。

Subagents(未实现)

文档构想了 subagent({ name, description, instructions | agent }) 两种形态(简单提示词 Agent 或直接挂一个完整 GeminiCliAgent),并留下开放问题"子 Agent 是否继承同一 session id"。当前 GeminiCliAgentOptions 中无 subagents 字段。

Extensions(未实现)

文档称其为"最重要的特性"——把 instructions、tools、skills、hooks、subagents 模块化封装为一个 extension() 对象。当前源码中 extensionsEnabled: false,无对应实现。

ACP Mode(未实现)

文档规划了 GeminiCliAcpServerserver.start() 启动 stdio ACP 服务器,server.connect({ onMessage }) 返回客户端以发送 session/prompt 等消息。CLI 本体已有成熟的 ACP 实现(见 docs/cli/acp-mode.mdpackages/cli/src/acp/ 目录),SDK 侧的包装器尚未提供。

Approvals / Policies(未实现)

文档此节仅标注 TODO。从源码可以确认现状:SDK 会话把 policyEngineConfig.defaultDecision 设为 ALLOWAgentShell 遇到需确认命令时拒绝执行而非发起审批。若你的场景需要写操作与命令执行,建议在 cwd 范围与 AgentFilesystem/AgentShell 的使用面上自行收紧信任边界。

九、实战参考:官方示例与测试依据

仓库内提供了可直接运行的参考示例:

验证与测试方面,packages/sdk/src/ 下的 agent.integration.test.tssession.test.tstool.integration.test.tsskills.integration.test.ts 配合 test-data/ 中的录制响应文件,覆盖了会话恢复、静态/动态/异步指令、工具成功与错误恢复等设计文档中列出的"Validation"检查项——这正是设计文档 Notes 中"用 mock 模型 API 做接近端到端但确定性的验证"这一思路的最终落地形式。

十、小结:从设计文档到可运行 API

回顾 SDK_DESIGN.md 的演进脉络,当前仓库给出的答案是:

  1. GeminiCliAgent 作为配置容器与会话工厂,session() / resumeSession() 覆盖"新建 + 恢复"两条主线,恢复逻辑建立在核心包 Storage 的磁盘持久化之上;
  2. Agent 循环GeminiCliSession.sendStream 完整实现:动态指令每轮刷新 → 流式事件 → 收集工具调用 → scheduleAgentTools 执行(工具注册表按轮克隆、上下文热绑定)→ 响应回灌,直至无工具调用为止;
  3. SessionContext 兑现了设计文档"工具/指令始终拿到会话状态与受控资源"的承诺,AgentFilesystemAgentShell 把路径策略与命令确认机制编织进每一次资源访问;
  4. Skills 已可用skillDir + SKILL.md 目录),而 Hooks、Subagents、Extensions、ACP、审批策略仍是明确的"未实现"边界,源码中的默认值(enableHooks: falsedefaultDecision: ALLOW)是使用者必须知晓的运行前提。

对于希望在 CI 流水线、内部平台或自动化脚本中嵌入 Gemini Agent 能力的开发者,SDK 目前提供的"会话 + 工具 + 技能 + 受控文件/Shell 访问"组合已经构成一个完整可用的最小闭环;而审批与 Hooks 能力,则应等待后续版本或回退到 CLI 自身的对应机制。

登录后查看全文
热门项目推荐
相关项目推荐