Gemini CLI SDK 设计剖析:用代码构建可编程的 AI Agent(GeminiCliAgent、工具、会话与 SessionContext)
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 仅导出五个模块:agent、session、tool、skills、types。安装方式为:
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 恢复历史会话。实现细节很有代表性:- 以
this.options.cwd || process.cwd()构造核心包的Storage(cwd)并初始化; - 调用
storage.listProjectChatFiles()列出项目聊天记录文件; - 利用"文件名包含 sessionId 前 8 位"的约定做候选过滤(
sessionId.slice(0, 8)),若过滤为空则回退为全量检查; - 逐文件调用
loadConversationRecord(absolutePath),以完整sessionId精确匹配; - 找到后将
conversation与filePath组装成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)按序完成:
- 认证:
getAuthTypeFromEnv() || AuthType.COMPUTE_ADC,然后config.refreshAuth(authType)——即 SDK 沿用核心包的认证体系(环境变量优先,回退到 Cloud ADC)。 - 加载 Skills:遍历
skillRefs,对type === 'dir'的引用调用核心包的loadSkillsFromDir(ref.path),加载成功后skillManager.addSkills(),并重新注册ActivateSkillTool使模型可主动激活技能。 - 注册自定义工具:每个 SDK 工具包装成
SdkTool(toolDef, messageBus, this.agent, undefined)后注册进toolRegistry。 - 恢复历史:若携带
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.json、agent-async-instructions.json、agent-resume-session.json 等用于测试的响应数据文件,agent.integration.test.ts 与 session.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.ts 与 L229-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 提供了三层结构:
tool(definition, action)助手:把声明(name/description/inputSchema/ 可选sendErrorsToModel)与执行函数合并为Tool<T>对象;z直接从 SDK 导出(export { z }),用户无需自行安装 zod。SdkTool extends BaseDeclarativeTool:注册到核心工具注册表。构造函数中调用zodToJsonSchema(definition.inputSchema),把 Zod schema 转成 JSON Schema 供模型理解参数结构——这是"模型在 prompt 中收到工具定义"的实现路径。SdkToolInvocation.execute:执行action(params, context),返回值的处理规则是——字符串原样返回,其他对象JSON.stringify(result, null, 2),同时作为llmContent与returnDisplay回传给模型。
错误处理是一个容易被忽略但很实用的设计(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.ts 与 test-data/skill-dir-success.json、skill-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)与之高度一致,且更严格:transcript 为 readonly Content[](只读对话记录)。每次 sendStream 轮次内,SDK 都会现造 SdkAgentFilesystem 与 SdkAgentShell 并填入上下文,工具与动态指令通过它访问系统资源:
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(合并进默认环境变量)、timeoutSeconds、cwd三个选项;- 一个当前的已知限制:由于底层
ShellExecutionService会把 stderr 并入 stdout,返回结果中stderr字段目前恒为空字符串,output与stdout是同一份合并输出——源码 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 被硬编码为 false(session.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(未实现)
文档规划了 GeminiCliAcpServer:server.start() 启动 stdio ACP 服务器,server.connect({ onMessage }) 返回客户端以发送 session/prompt 等消息。CLI 本体已有成熟的 ACP 实现(见 docs/cli/acp-mode.md 与 packages/cli/src/acp/ 目录),SDK 侧的包装器尚未提供。
Approvals / Policies(未实现)
文档此节仅标注 TODO。从源码可以确认现状:SDK 会话把 policyEngineConfig.defaultDecision 设为 ALLOW,AgentShell 遇到需确认命令时拒绝执行而非发起审批。若你的场景需要写操作与命令执行,建议在 cwd 范围与 AgentFilesystem/AgentShell 的使用面上自行收紧信任边界。
九、实战参考:官方示例与测试依据
仓库内提供了可直接运行的参考示例:
- examples/simple.ts:最基本的 Agent +
sendStream用法; - examples/session-context.ts:演示在工具中消费
SessionContext; - README:安装与流式消费
content事件的快速上手。
验证与测试方面,packages/sdk/src/ 下的 agent.integration.test.ts、session.test.ts、tool.integration.test.ts、skills.integration.test.ts 配合 test-data/ 中的录制响应文件,覆盖了会话恢复、静态/动态/异步指令、工具成功与错误恢复等设计文档中列出的"Validation"检查项——这正是设计文档 Notes 中"用 mock 模型 API 做接近端到端但确定性的验证"这一思路的最终落地形式。
十、小结:从设计文档到可运行 API
回顾 SDK_DESIGN.md 的演进脉络,当前仓库给出的答案是:
GeminiCliAgent作为配置容器与会话工厂,session()/resumeSession()覆盖"新建 + 恢复"两条主线,恢复逻辑建立在核心包Storage的磁盘持久化之上;- Agent 循环由
GeminiCliSession.sendStream完整实现:动态指令每轮刷新 → 流式事件 → 收集工具调用 →scheduleAgentTools执行(工具注册表按轮克隆、上下文热绑定)→ 响应回灌,直至无工具调用为止; SessionContext兑现了设计文档"工具/指令始终拿到会话状态与受控资源"的承诺,AgentFilesystem与AgentShell把路径策略与命令确认机制编织进每一次资源访问;- Skills 已可用(
skillDir+SKILL.md目录),而 Hooks、Subagents、Extensions、ACP、审批策略仍是明确的"未实现"边界,源码中的默认值(enableHooks: false、defaultDecision: ALLOW)是使用者必须知晓的运行前提。
对于希望在 CI 流水线、内部平台或自动化脚本中嵌入 Gemini Agent 能力的开发者,SDK 目前提供的"会话 + 工具 + 技能 + 受控文件/Shell 访问"组合已经构成一个完整可用的最小闭环;而审批与 Hooks 能力,则应等待后续版本或回退到 CLI 自身的对应机制。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00