M12 · SOURCE-GROUNDED TUTORIAL
Gemini CLI从源码学会它怎么工作 工具调度、PolicyEngine、扩展热装卸和新图式上下文系统都很有研究价值;默认 sandbox 仍关闭。 我们不把 README 当结论,而是沿主循环、工具、上下文、权限、扩展、协作和状态一路读到实现。
TypeScript · Scheduler + Context Pipeline Apache-2.0 bef6119500b0 28 个结论 · 67 处引用
这门课怎么读
先建立直觉,再沿一条任务链钻进代码 参考教程的做法不是把 API 名称罗列出来,而是从一个小白能理解的问题开始,先解释“为什么需要这个机制”,再用概念对比、执行链路和固定提交的源码回答“它究竟怎么做”。本页把 Gemini CLI 的 28 个源码结论重新编排成十节课;每个结论都保留证据等级、文件路径、行号和可点击源码。
你会得到 一张可复述的架构地图 一次完整任务的链路追踪 能迁移到自研 Harness 的设计判断
你不会得到 把 README 功能当成已验证事实 把 prompt 约束说成 OS 沙箱 把一次双模型调用夸成多 Agent 平台
M00 · MAP
先看全景:这个 Agent 的控制面在哪里 下面的图不是产品宣传图,而是把固定提交里最关键的入口、循环、模型、工具、安全、状态和协作节点放在一张地图上。
核心机制 递归 sendMessageStream;100 turn 上限;loop 恢复一次
上下文 Legacy 摘要 + 新图/流水线/GC/蒸馏/真实 token 校准
适用建设 Google 生态、大型扩展平台、上下文工程研究
M00.5 · TRACE
跟踪一个任务:从输入到交付 把下面九步当成你读源码时的“地图坐标”。每到一个节点,都能回到后面的章节查具体实现。
01 提交请求并配对 IDE 任务进入
↓
02 上下文/溢出/loop 预检 把结果交给下一层
↓
03 锁定模型与工具 把结果交给下一层
↓
04 ContentGenerator 流式响应 把结果交给下一层
↓
05 Turn 只解析 tool calls 把结果交给下一层
↓
06 Scheduler + PolicyEngine 把结果交给下一层
↓
07 OS sandbox 执行并回合法 response 把结果交给下一层
↓
08 checkpoint / GC / compact 把结果交给下一层
↓
09 继续递归或终止 交付/续跑
读图提醒 箭头只表示控制面之间的关系,不代表每个实现都同步、串行或拥有 OS 隔离;真正的边界要以对应章节的源码摘录和 caveat 为准。
M01 · ORIENTATION
先把 Agent 看成一台会交付的机器 如果只看 README,你知道它能做什么;钻进源码后,我们要知道它为什么能做、什么时候会停、失败后谁负责收拾。
先用一个生活比喻 把 Agent 想成一间带传送带的工作室:入口收任务,主循环决定下一步,模型负责提出动作,工具负责动手,状态账本负责让下一班人接着干。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
读源码时先问 本课的判断方式 这一层有没有独立证据? 没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。 谁拥有最终控制权? 区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念 固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。
小练习 1 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M02 · LOOP
主循环:模型为什么会继续动 一次模型调用为什么会变成十几步?循环靠什么继续,靠什么停止?
先用一个生活比喻 像一个会看回执的快递员:模型先写行动单,工具返回回执,主循环把回执放回桌面,再让模型决定下一张行动单。
这套实现先回答了什么? 一次用户请求可以连续让模型说、用工具、再说;但最多转 100 圈,避免无尽自言自语。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 主 Harness 用递归 sendMessageStream 驱动多 turn,硬上限为 100 packages/core/src/core/client.ts:79一次用户请求可以连续让模型说、用工具、再说;但最多转 100 圈,避免无尽自言自语。 每轮先做上下文、溢出、IDE 配对和 loop 检测,再锁定模型与工具 packages/core/src/core/client.ts:614开口前先整理历史、确认装得下、保证工具回执不被编辑器消息插队,然后才选本轮模型和工具箱。 循环检测能先恢复一次,再判定硬循环 packages/core/src/core/client.ts:744第一次怀疑绕圈会给模型一次纠偏机会,第二次还绕就停。
01 L1 · fact · gemini-loop-001
主 Harness 用递归 sendMessageStream 驱动多 turn,硬上限为 100
先看源码事实 GeminiClient 将单次 turn 交给 processTurn;若 next-speaker 判定模型应继续,或 AfterAgent hook 返回继续原因,就递归调用 sendMessageStream 并递减 boundedTurns,入口始终 clamp 到 MAX_TURNS=100。
翻译成白话 一次用户请求可以连续让模型说、用工具、再说;但最多转 100 圈,避免无尽自言自语。
为什么这对自研重要 控制流直观,递归路径共享 prompt_id 和 hook state,需要严格做 activeCalls 记账。
固定提交源码摘录 复制
79 const MAX_TURNS = 100;
80
81 type BeforeAgentHookReturn =
82 | {
83 type: GeminiEventType.AgentExecutionStopped;
84 value: { reason: string; systemMessage?: string };
85 }
86 | {
87 type: GeminiEventType.AgentExecutionBlocked;
88 value: { reason: string; systemMessage?: string };
89 }
90 | { additionalContext: string | undefined }
91 | undefined;
92
93 export class GeminiClient {
94 private chat?: GeminiChat;
95 private sessionTurnCount = 0;
96
97 private readonly loopDetector: LoopDetectionService;
98 private readonly compressionService: ChatCompressionService;
99 private readonly agentHistoryProvider: AgentHistoryProvider;
100 private readonly toolOutputMaskingService: ToolOutputMaskingService;
101 private contextManager?: ContextManager;
102 private lastPromptId: string;
103 private currentSequenceModel: string | null = null;
104 private lastSentIdeContext: IdeContext | undefined;
105 private forceFullIdeContext = true;
106
107 /**
108 * At any point in this conversation, was compression triggered without
109 * being forced and did it fail?
110 */
111 private hasFailedCompressionAttempt = false;
为什么相信这条结论?查看 3 处证据
02 L1 · fact · gemini-loop-002
每轮先做上下文、溢出、IDE 配对和 loop 检测,再锁定模型与工具
先看源码事实 processTurn 先运行 context management/压缩与工具输出 masking,估算请求 token;为保持 functionCall→functionResponse 紧邻,会延迟 IDE context;随后 loop detector、model router/availability 和 model-dependent tool declarations 才进入 Turn.run。
翻译成白话 开口前先整理历史、确认装得下、保证工具回执不被编辑器消息插队,然后才选本轮模型和工具箱。
为什么这对自研重要 上下文与模型选择顺序清楚,工具描述可随模型变化且同一 sequence 保持模型粘性。
固定提交源码摘录 复制
614 private async *processTurn(
615 request: PartListUnion,
616 signal: AbortSignal,
617 prompt_id: string,
618 boundedTurns: number,
619 displayContent?: PartListUnion,
620 ): AsyncGenerator<ServerGeminiStreamEvent, Turn> {
621 // Re-initialize turn (it was empty before if in loop, or new instance)
622 let turn = new Turn(this.getChat(), prompt_id);
623
624 this.sessionTurnCount++;
625 if (
626 this.config.getMaxSessionTurns() > 0 &&
627 this.sessionTurnCount > this.config.getMaxSessionTurns()
628 ) {
629 yield { type: GeminiEventType.MaxSessionTurns };
630 return turn;
631 }
632
633 if (!boundedTurns) {
634 return turn;
635 }
636
637 // Check for context window overflow
638 const modelForLimitCheck = this._getActiveModelForCurrentTurn();
… 66 lines omitted; exact range 614–715 …
705 this.getContentGeneratorOrFail(),
706 modelForLimitCheck,
707 );
708
709 if (estimatedRequestTokenCount > remainingTokenCount) {
710 yield {
711 type: GeminiEventType.ContextWindowWillOverflow,
712 value: { estimatedRequestTokenCount, remainingTokenCount },
713 };
714 return turn;
715 }
为什么相信这条结论?查看 2 处证据
03 L1 · fact · gemini-loop-003
循环检测能先恢复一次,再判定硬循环
先看源码事实 turnStarted 和每个流事件都送入 LoopDetectionService;count=1 进入 _recoverFromLoop,count>1 发 LoopDetected 并终止,剩余 turn 不足时转 MaxSessionTurns。
翻译成白话 第一次怀疑绕圈会给模型一次纠偏机会,第二次还绕就停。
为什么这对自研重要 比仅靠 turn 上限更早止损,并保留一次自恢复空间。
固定提交源码摘录 复制
744 // Re-initialize turn with fresh history
745 turn = new Turn(this.getChat(), prompt_id);
746
747 const loopResult = await this.loopDetector.turnStarted(signal);
748 if (loopResult.count > 1) {
749 yield { type: GeminiEventType.LoopDetected };
750 return turn;
751 } else if (loopResult.count === 1) {
752 if (boundedTurns <= 1) {
753 yield { type: GeminiEventType.MaxSessionTurns };
754 return turn;
755 }
756 return yield* this._recoverFromLoop(
757 loopResult,
758 signal,
759 prompt_id,
760 boundedTurns,
761 displayContent,
762 );
763 }
为什么相信这条结论?查看 2 处证据
小练习 2 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M03 · MODEL
模型调用:流式输出如何变成可执行步骤 模型输出的文字、思考、工具调用和错误,经过哪些转换才进入 Agent 状态?
先用一个生活比喻 模型像电话另一端的同事:你听到的不是一整段录音,而是一串实时片段;Harness 要边听边拼装,还要能在电话断线时留下可恢复的记录。
这套实现先回答了什么? 上层只认一套生成接口,底下可换个人 Google 登录、API key、企业 Vertex 或网关。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 统一 ContentGenerator 契约覆盖流式、非流式、计数与 embedding packages/core/src/core/contentGenerator.ts:35上层只认一套生成接口,底下可换个人 Google 登录、API key、企业 Vertex 或网关。 个人/ADC 走 Code Assist,API key/Vertex/Gateway 走 Google GenAI SDK packages/core/src/core/contentGenerator.ts:285登录方式不仅换凭证,也可能换后端客户端;企业 Vertex 还能指定共享/专用路由。 连接阶段与中途流错误分开重试,中途流最多四次尝试 packages/core/src/core/geminiChat.ts:517连不上和连上后半路断掉是两类事故,分别计数;不会因为全局重试设得很大就反复重播半截响应。
04 L2 · fact · gemini-provider-001
统一 ContentGenerator 契约覆盖流式、非流式、计数与 embedding
先看源码事实 ContentGenerator 抽象 generateContent、generateContentStream、countTokens 和 embedContent;AuthType 覆盖 Google OAuth、Gemini API key、Vertex AI、ADC 与 Gateway。
翻译成白话 上层只认一套生成接口,底下可换个人 Google 登录、API key、企业 Vertex 或网关。
为什么这对自研重要 Provider 切换不改主循环,但 thought signature 兼容性仍需在切换认证后处理。
固定提交源码摘录 复制
35
36 /**
37 * Interface abstracting the core functionalities for generating content and counting tokens.
38 */
39 export interface ContentGenerator {
40 generateContent(
41 request: GenerateContentParameters,
42 userPromptId: string,
43 role: LlmRole,
44 ): Promise<GenerateContentResponse>;
45
46 generateContentStream(
47 request: GenerateContentParameters,
48 userPromptId: string,
49 role: LlmRole,
50 ): Promise<AsyncGenerator<GenerateContentResponse>>;
51
52 countTokens(request: CountTokensParameters): Promise<CountTokensResponse>;
53
54 embedContent(request: EmbedContentParameters): Promise<EmbedContentResponse>;
55
56 userTier?: UserTierId;
57
58 userTierName?: string;
59
60 paidTier?: GeminiUserTier;
61 }
62
63 export enum AuthType {
64 LOGIN_WITH_GOOGLE = 'oauth-personal',
65 USE_GEMINI = 'gemini-api-key',
66 USE_VERTEX_AI = 'vertex-ai',
67 LEGACY_CLOUD_SHELL = 'cloud-shell',
68 COMPUTE_ADC = 'compute-default-credentials',
69 GATEWAY = 'gateway',
70 }
为什么相信这条结论?查看 2 处证据
05 L1 · fact · gemini-provider-002
个人/ADC 走 Code Assist,API key/Vertex/Gateway 走 Google GenAI SDK
先看源码事实 createContentGenerator 对 LOGIN_WITH_GOOGLE/COMPUTE_ADC 创建 CodeAssistServer 并加模型映射;Gemini/Vertex/Gateway 构造 GoogleGenAI,支持自定义 base URL、headers、proxy 和 Vertex routing headers。
翻译成白话 登录方式不仅换凭证,也可能换后端客户端;企业 Vertex 还能指定共享/专用路由。
为什么这对自研重要 同一主循环有多条传输实现,认证切换测试必须覆盖历史 thoughtSignature 清理。
固定提交源码摘录 复制
285 if (
286 apiKeyAuthMechanism === 'bearer' &&
287 (config.authType === AuthType.USE_GEMINI ||
288 config.authType === AuthType.USE_VERTEX_AI) &&
289 config.apiKey
290 ) {
291 baseHeaders['Authorization'] = `Bearer ${config.apiKey}`;
292 }
293 if (
294 config.authType === AuthType.LOGIN_WITH_GOOGLE ||
295 config.authType === AuthType.COMPUTE_ADC
296 ) {
297 const httpOptions = { headers: baseHeaders };
298 return new LoggingContentGenerator(
299 new ModelMappingContentGenerator(
300 await createCodeAssistContentGenerator(
301 httpOptions,
302 config.authType,
303 gcConfig,
304 sessionId,
305 ),
306 CCPA_AI_MODEL_MAPPINGS,
307 ),
308 gcConfig,
309 );
310 }
为什么相信这条结论?查看 2 处证据
06 L1 · fact · gemini-provider-003
连接阶段与中途流错误分开重试,中途流最多四次尝试
先看源码事实 GeminiChat 的 streamWithRetries 把连接阶段错误交给底层 retryWithBackoff;流迭代中的可重试网络/内容错误使用独立指数退避,并限制到 MID_STREAM_RETRY_OPTIONS 的四次总尝试。
翻译成白话 连不上和连上后半路断掉是两类事故,分别计数;不会因为全局重试设得很大就反复重播半截响应。
为什么这对自研重要 降低 mid-stream 重复输出/工具调用风险。
固定提交源码摘录 复制
517 const streamWithRetries = async function* (
518 this: GeminiChat,
519 ): AsyncGenerator<StreamEvent, void, void> {
520 try {
521 const maxAttempts = this.context.config.getMaxAttempts();
522
523 for (let attempt = 0; attempt < maxAttempts; attempt++) {
524 let isConnectionPhase = true;
525 try {
526 if (attempt > 0) {
527 yield { type: StreamEventType.RETRY };
528 }
529
530 // If this is a retry, update the key with the new context.
531 const currentConfigKey =
532 attempt > 0
533 ? { ...modelConfigKey, isRetry: true }
534 : modelConfigKey;
535
536 isConnectionPhase = true;
537 const stream = await this.makeApiCallAndProcessStream(
538 currentConfigKey,
539 requestHistory,
540 prompt_id,
541 signal,
… 26 lines omitted; exact range 517–578 …
568 value: error.syntheticResponse,
569 };
570 }
571 return; // Stop the generator
572 }
573
574 if (isConnectionPhase) {
575 // Connection phase errors have already been retried by retryWithBackoff.
576 // If they bubble up here, they are exhausted or fatal.
577 throw error;
578 }
为什么相信这条结论?查看 2 处证据
小练习 3 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M05 · CONTEXT
上下文:有限窗口怎样装下长任务 当对话、工具输出、计划和记忆越来越多,系统怎样决定留下什么、折叠什么、放到哪里?
先用一个生活比喻 上下文不是聊天记录,而是一张会整理的工作台:常用零件放桌面,旧材料装进档案盒,必要时只留下索引卡。
这套实现先回答了什么? 箱子装到一半就提前整理,最近三成原文留下,旧七成写成摘要;切口只选完整对话边界。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 Legacy 压缩默认在 50% 窗口触发,并保留最近约 30% packages/core/src/context/chatCompressionService.ts:37箱子装到一半就提前整理,最近三成原文留下,旧七成写成摘要;切口只选完整对话边界。 旧工具输出采用反向预算,超额内容落临时文件并只留尾部 packages/core/src/context/chatCompressionService.ts:124最新日志全文留在桌面,旧日志搬进档案室,只在上下文里留末尾和取件地址。 摘要膨胀会触发熔断,随后只做内容截断 packages/core/src/core/client.ts:107如果摘要反而比原文胖,就不再每轮花钱重写摘要,改用更轻的裁剪。 新 ContextManager 是图与流水线系统,带 preview late-bind、压力屏障、GC/蒸馏和结构校验 packages/core/src/context/contextManager.ts:26新系统不再把历史当一长串消息,而是当可追溯的节点图;当前问题先在草稿区处理,确认后才影响长期账本。
09 L1 · fact · gemini-context-001
Legacy 压缩默认在 50% 窗口触发,并保留最近约 30%
先看源码事实 ChatCompressionService 默认阈值为模型 token limit 的 0.5,压缩切点按序列化字符量寻找安全 user turn,目标保留最近 30%,且不会拆断 function call/response 边界。
翻译成白话 箱子装到一半就提前整理,最近三成原文留下,旧七成写成摘要;切口只选完整对话边界。
为什么这对自研重要 为长回复和工具调用预留较大余量,代价是较早产生摘要成本。
固定提交源码摘录 复制
37 /**
38 * Default threshold for compression token count as a fraction of the model's
39 * token limit. If the chat history exceeds this threshold, it will be compressed.
40 */
41 const DEFAULT_COMPRESSION_TOKEN_THRESHOLD = 0.5;
42
43 /**
44 * The fraction of the latest chat history to keep. A value of 0.3
45 * means that only the last 30% of the chat history will be kept after compression.
46 */
47 const COMPRESSION_PRESERVE_THRESHOLD = 0.3;
48
49 /**
50 * The budget for function response tokens in the preserved history.
51 */
52 const COMPRESSION_FUNCTION_RESPONSE_TOKEN_BUDGET = 50_000;
为什么相信这条结论?查看 2 处证据
10 L1 · fact · gemini-context-002
旧工具输出采用反向预算,超额内容落临时文件并只留尾部
先看源码事实 压缩前从最新向最旧累计 function response token,优先保留近端;超过 50k 工具响应预算后,将较老大输出保存到文件并替换为最后 30 行和路径提示。
翻译成白话 最新日志全文留在桌面,旧日志搬进档案室,只在上下文里留末尾和取件地址。
为什么这对自研重要 既省 token 又保留可恢复原文,比不可逆截断更适合调试。
固定提交源码摘录 复制
124 /**
125 * Processes the chat history to ensure function responses don't exceed a specific token budget.
126 *
127 * This function implements a "Reverse Token Budget" strategy:
128 * 1. It iterates through the history from the most recent turn to the oldest.
129 * 2. It keeps a running tally of tokens used by function responses.
130 * 3. Recent tool outputs are preserved in full to maintain high-fidelity context for the current turn.
131 * 4. Once the budget (COMPRESSION_FUNCTION_RESPONSE_TOKEN_BUDGET) is exceeded, any older large
132 * tool responses are truncated to their last 30 lines and saved to a temporary file.
133 *
134 * This ensures that compression effectively reduces context size even when recent turns
135 * contain massive tool outputs (like large grep results or logs).
136 */
137 async function truncateHistoryToBudget(
138 history: readonly Content[],
139 config: Config,
140 ): Promise<Content[]> {
141 let functionResponseTokenCounter = 0;
142 const truncatedHistory: Content[] = [];
为什么相信这条结论?查看 2 处证据
11 L1 · fact · gemini-context-003
摘要膨胀会触发熔断,随后只做内容截断
先看源码事实 tryCompressChat 记录 COMPRESSION_FAILED_INFLATED_TOKEN_COUNT;非强制压缩一旦失败即保持 hasFailedCompressionAttempt,后续服务可返回 CONTENT_TRUNCATED 并直接更新 history,不重建 chat。
翻译成白话 如果摘要反而比原文胖,就不再每轮花钱重写摘要,改用更轻的裁剪。
为什么这对自研重要 避免压缩风暴,但本会话之后的语义保真会更多依赖工具输出 masking。
固定提交源码摘录 复制
107 /**
108 * At any point in this conversation, was compression triggered without
109 * being forced and did it fail?
110 */
111 private hasFailedCompressionAttempt = false;
112
113 constructor(private readonly context: AgentLoopContext) {
114 this.loopDetector = new LoopDetectionService(this.config);
115 this.compressionService = new ChatCompressionService();
116 this.agentHistoryProvider = new AgentHistoryProvider(
117 this.config.agentHistoryProviderConfig,
118 this.config,
119 );
120 this.toolOutputMaskingService = new ToolOutputMaskingService();
为什么相信这条结论?查看 2 处证据
12 L1 · fact · gemini-context-004
新 ContextManager 是图与流水线系统,带 preview late-bind、压力屏障、GC/蒸馏和结构校验
先看源码事实 ContextManager 从 durable AgentChatHistory 同步 pristine graph,对 pending request 建 ephemeral preview,等待 pipeline/hot-start barrier,执行 triggers,再带保护节点渲染、harden 与 invariant check;相同 node hash 可复用 render cache。
翻译成白话 新系统不再把历史当一长串消息,而是当可追溯的节点图;当前问题先在草稿区处理,确认后才影响长期账本。
为什么这对自研重要 能做精细蒸馏和增量管理,但复杂度显著高于 legacy summary,必须明确 feature gate 与回退路径。
固定提交源码摘录 复制
26 export class ContextManager {
27 // Master state containing the pristine graph and current active graph.
28 private buffer: ContextWorkingBufferImpl =
29 ContextWorkingBufferImpl.initialize([]);
30
31 private readonly eventBus: ContextEventBus;
32 private readonly orchestrator: PipelineOrchestrator;
33
34 // Track what IDs have been evaluated for triggers to prevent redundant processing
35 private readonly evaluatedNodeIds = new Set<string>();
36
37 // Hysteresis tracking to prevent utility call churn
38 private lastTriggeredDeficit = 0;
39 private lastTriggeredNormalizeDeficit = 0;
40
41 // Cache for Anomaly 3 (Redundant Renders)
42 private lastRenderCache?: {
43 nodesHash: string;
44 result: {
45 history: HistoryTurn[];
46 apiHistory: Content[];
47 pendingApiHistory: Content[];
48 didApplyManagement: boolean;
49 baseUnits: number;
50 processedNodes: readonly ConcreteNode[];
… 27 lines omitted; exact range 26–88 …
78 );
79 return;
80 }
81
82 this.buffer = this.buffer.applyProcessorResult(
83 event.processorId,
84 event.targets,
85 event.returnedNodes,
86 );
87 });
88 }
为什么相信这条结论?查看 3 处证据
13 L1 · fact · gemini-context-005
新上下文系统用真实 API token 反校准本地估算
先看源码事实 processTurn 把 ContextManager 渲染得到的 baseUnits 带入请求;Finished 事件若包含 promptTokenCount,则通过 eventBus 发 actualTokens 与 promptBaseUnits ground truth。
翻译成白话 先用本地尺子估,再用模型 API 的过磅结果校准尺子。
为什么这对自研重要 长期预算判断可适应多模态与不同模型 tokenizer 偏差。
固定提交源码摘录 复制
640 let currentBaseUnits = 0;
641 let apiHistoryOverride: Content[] | undefined = undefined;
642
643 if (this.config.getContextManagementConfig().enabled) {
644 if (this.contextManager) {
645 const rawPendingRequest = createUserContent(request);
646 const pendingRequest = {
647 id: randomUUID(),
648 content: rawPendingRequest,
649 };
650 const {
651 history: newHistory,
652 apiHistory,
653 pendingApiHistory,
654 baseUnits,
655 } = await this.contextManager.renderHistory(
656 pendingRequest,
657 undefined,
658 signal,
659 );
660
661 currentBaseUnits = baseUnits;
662
663 // Use the PROCESSED pending content if available (e.g. if cleaned or distilled)
664 const finalPendingContent =
… 3 lines omitted; exact range 640–678 …
668
669 // Late-bind the prompt: Append the active request to the managed history
670 // only for the purpose of the upcoming API call.
671 apiHistoryOverride = [...apiHistory, finalPendingContent];
672
673 this.getChat().setHistory(newHistory);
674
675 // Use the original request for display/recording,
676 // but the processed one for the API and durable history.
677 displayContent = rawPendingRequest.parts || [];
678 request = finalPendingContent.parts || [];
为什么相信这条结论?查看 2 处证据
小练习 5 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M06 · SECURITY
权限与沙箱:能做什么,在哪里做 审批按钮、规则引擎、容器和操作系统沙箱分别解决什么问题?为什么“问过用户”不等于“隔离了风险”?
先用一个生活比喻 审批像门卫问你有没有预约,沙箱像把访客关在指定房间;前者决定是否放行,后者限制放行后能摸到什么。
这套实现先回答了什么? 政策可以精确到“哪个子 Agent 在非交互模式调用哪个 MCP 的哪个参数”,不只是允许/禁止 Bash。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 PolicyEngine 按优先级匹配工具、参数、MCP 身份、annotations、模式、交互状态和 subagent packages/core/src/policy/policy-engine.ts:49政策可以精确到“哪个子 Agent 在非交互模式调用哪个 MCP 的哪个参数”,不只是允许/禁止 Bash。 非交互默认拒绝,交互默认询问;危险命令强制 ASK,YOLO 例外 packages/core/src/policy/policy-engine.ts:253没人看屏幕时不赌;有人在时先问。只有明确 YOLO 才允许危险命令绕过这层强制提问。 Sandbox 默认不开启,但即使关闭仍净化环境变量 packages/core/src/services/sandboxManagerFactory.ts:19防护罩不是默认扣上的;没扣罩子时至少会先从进程环境里清理不该传给子进程的东西。 三平台使用真实 OS 隔离,并保护治理文件与 .env 类秘密 packages/core/src/services/sandboxManager.ts:194开罩后不是靠模型自觉:Linux、macOS、Windows 各用系统级限制器,仓库规则和秘密文件另加保护。
14 L1 · fact · gemini-policy-001
PolicyEngine 按优先级匹配工具、参数、MCP 身份、annotations、模式、交互状态和 subagent
先看源码事实 ruleMatches 对稳定序列化参数、MCP 全限定名、tool annotations、approval mode、interactive/non-interactive 与 subagent 做组合匹配;规则/checker/hook checker 都按 priority 降序。
翻译成白话 政策可以精确到“哪个子 Agent 在非交互模式调用哪个 MCP 的哪个参数”,不只是允许/禁止 Bash。
为什么这对自研重要 企业控制力强,但规则冲突需要良好解释器和测试。
固定提交源码摘录 复制
49 function isWildcardPattern(name: string): boolean {
50 return name === '*' || name.includes('*');
51 }
52
53 /**
54 * Checks if a tool call matches a wildcard pattern.
55 * Supports global (*) and the explicit MCP (*mcp_serverName_**) format.
56 */
57 function matchesWildcard(
58 pattern: string,
59 toolName: string,
60 serverName: string | undefined,
61 ): boolean {
62 if (pattern === '*') {
63 return true;
64 }
65
66 if (pattern === `${MCP_TOOL_PREFIX}*`) {
67 return serverName !== undefined;
68 }
69
70 if (pattern.startsWith(MCP_TOOL_PREFIX) && pattern.endsWith('_*')) {
71 const expectedServerName = pattern.slice(MCP_TOOL_PREFIX.length, -2);
72 // 1. Must be an MCP tool call (has serverName)
73 // 2. Server name must match
… 111 lines omitted; exact range 49–195 …
185 // Check interactive if specified
186 if ('interactive' in rule && rule.interactive !== undefined) {
187 if (rule.interactive && nonInteractive) {
188 return false;
189 }
190 if (!rule.interactive && !nonInteractive) {
191 return false;
192 }
193 }
194
195 return true;
为什么相信这条结论?查看 2 处证据
15 L1 · fact · gemini-policy-002
非交互默认拒绝,交互默认询问;危险命令强制 ASK,YOLO 例外
先看源码事实 PolicyEngine 未配置 defaultDecision 时,nonInteractive 为 DENY、交互为 ASK_USER;shell heuristic 将危险命令改为 ASK_USER,YOLO 保留原决定,已知安全命令可把 ASK 降为 ALLOW。
翻译成白话 没人看屏幕时不赌;有人在时先问。只有明确 YOLO 才允许危险命令绕过这层强制提问。
为什么这对自研重要 无头执行失败关闭,YOLO 是明确的高风险模式。
固定提交源码摘录 复制
253 this.nonInteractive = config.nonInteractive ?? false;
254 this.defaultDecision =
255 config.defaultDecision ??
256 (this.nonInteractive ? PolicyDecision.DENY : PolicyDecision.ASK_USER);
257 this.disableAlwaysAllow = config.disableAlwaysAllow ?? false;
258 this.checkerRunner = checkerRunner;
259 this.approvalMode = config.approvalMode ?? ApprovalMode.DEFAULT;
260 this.sandboxManager = config.sandboxManager ?? new NoopSandboxManager();
为什么相信这条结论?查看 2 处证据
16 L1 · fact · gemini-sandbox-001
Sandbox 默认不开启,但即使关闭仍净化环境变量
先看源码事实 factory 只有 sandbox.enabled 才按平台选择 Linux/Mac/Windows manager,否则用 NoopSandboxManager;Noop 不隔离进程,但会按安全配置 sanitize environment 后原样运行。
翻译成白话 防护罩不是默认扣上的;没扣罩子时至少会先从进程环境里清理不该传给子进程的东西。
为什么这对自研重要 必须评价为“可选强隔离”,不能写成“默认在沙箱中执行”。
固定提交源码摘录 复制
19 /**
20 * Creates a sandbox manager based on the provided settings.
21 */
22 export function createSandboxManager(
23 sandbox: SandboxConfig | undefined,
24 options: GlobalSandboxOptions,
25 approvalMode?: string,
26 ): SandboxManager {
27 if (!options.modeConfig && options.policyManager && approvalMode) {
28 options.modeConfig = options.policyManager.getModeConfig(approvalMode);
29 }
30
31 if (sandbox?.enabled) {
32 if (os.platform() === 'win32') {
33 return new WindowsSandboxManager(options);
34 } else if (os.platform() === 'linux') {
35 return new LinuxSandboxManager(options);
36 } else if (os.platform() === 'darwin') {
37 return new MacOsSandboxManager(options);
38 }
39 return new LocalSandboxManager(options);
40 }
41
42 return new NoopSandboxManager(options);
43 }
为什么相信这条结论?查看 2 处证据
17 L1 · fact · gemini-sandbox-002
三平台使用真实 OS 隔离,并保护治理文件与 .env 类秘密
先看源码事实 Linux 使用 bubblewrap 并生成 seccomp BPF 禁 ptrace;macOS 用 sandbox-exec/Seatbelt profile;Windows 用 restricted token、Job Object、Low Integrity helper。共同模型保护 .git/.gitignore/.geminiignore,隐藏 .env/.env.*。
翻译成白话 开罩后不是靠模型自觉:Linux、macOS、Windows 各用系统级限制器,仓库规则和秘密文件另加保护。
为什么这对自研重要 跨平台安全面完整,但每个后端差异大,需各自做 denial 与路径逃逸回归。
固定提交源码摘录 复制
194 /**
195 * Files that represent the governance or "constitution" of the repository
196 * and should be write-protected in any sandbox.
197 */
198 export const GOVERNANCE_FILES = [
199 { path: '.gitignore', isDirectory: false },
200 { path: '.geminiignore', isDirectory: false },
201 { path: '.git', isDirectory: true },
202 ] as const;
203
204 /**
205 * Files that contain sensitive secrets or credentials and should be
206 * completely hidden (deny read/write) in any sandbox.
207 */
208 export const SECRET_FILES = [
209 { pattern: '.env' },
210 { pattern: '.env.*' },
211 ] as const;
212
213 /**
214 * Checks if a given file name matches any of the secret file patterns.
215 */
216 export function isSecretFile(fileName: string): boolean {
217 return SECRET_FILES.some((s) => {
218 if (s.pattern.endsWith('*')) {
219 const prefix = s.pattern.slice(0, -1);
220 return fileName.startsWith(prefix);
221 }
222 return fileName === s.pattern;
223 });
为什么相信这条结论?查看 3 处证据
18 L1 · risk · gemini-sandbox-003
默认 sandbox mode 的 default 是可写 workspace,plan 才只读
先看源码事实 SandboxPolicyManager 内建 fallback 中 plan 为 readonly/network off,default 和 accepting_edits 为 writable/network off;YOLO 明确打开网络、写权限和 yolo 标志。
翻译成白话 即使开启沙箱,普通默认模式也允许改工作区;“开沙箱”不等于“只读”。
为什么这对自研重要 产品 UI 必须同时展示 sandbox enabled 与当前 mode,避免用户产生错误安全感。
固定提交源码摘录 复制
49 private static get DEFAULT_CONFIG(): SandboxTomlSchemaType {
50 if (!SandboxPolicyManager._DEFAULT_CONFIG) {
51 const __filename = fileURLToPath(import.meta.url);
52 const __dirname = path.dirname(__filename);
53 const defaultPath = path.join(
54 __dirname,
55 'policies',
56 'sandbox-default.toml',
57 );
58 try {
59 const content = fs.readFileSync(defaultPath, 'utf8');
60 if (typeof content !== 'string') {
61 SandboxPolicyManager._DEFAULT_CONFIG = {
62 modes: {
63 plan: {
64 network: false,
65 readonly: true,
66 approvedTools: [],
67 allowOverrides: true,
68 },
69 default: {
70 network: false,
71 readonly: false,
72 approvedTools: [],
73 allowOverrides: true,
… 10 lines omitted; exact range 49–94 …
84 return SandboxPolicyManager._DEFAULT_CONFIG;
85 }
86 SandboxPolicyManager._DEFAULT_CONFIG = SandboxTomlSchema.parse(
87 toml.parse(content),
88 );
89 } catch (e) {
90 debugLogger.error(`Failed to parse default sandbox policy: ${e}`);
91 throw new Error(`Failed to parse default sandbox policy: ${e}`);
92 }
93 }
94 return SandboxPolicyManager._DEFAULT_CONFIG;
为什么相信这条结论?查看 2 处证据
小练习 6 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M07 · ECOSYSTEM
指令、MCP、Skills 与插件:能力如何接进来 一条系统指令、一个 Skill、一个 MCP server 和一个插件,分别在什么时候进入上下文和执行路径?
先用一个生活比喻 这像给工作室接设备:说明书不是设备,设备也不等于电源;成熟 Harness 会分别治理发现、信任、加载、调用和卸载。
这套实现先回答了什么? 本地子进程和远程服务都能接;服务器换工具会热刷新,远程登录不是自动偷偷弹出,必须配置允许 OAuth。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 MCP 支持 stdio、Streamable HTTP、SSE fallback、OAuth、动态目录刷新和 progress packages/core/src/tools/mcp-client.ts:188本地子进程和远程服务都能接;服务器换工具会热刷新,远程登录不是自动偷偷弹出,必须配置允许 OAuth。 扩展是能力包:MCP、policy/checker、context、commands、hooks、agents 与 skills 可成组热装卸 packages/core/src/utils/extensionLoader.ts:31扩展不是单个脚本,而是一箱可协同变化的工具、规则、记忆、钩子和 Agent。 GEMINI.md/Memory 分 global、user-project、extension、project,并支持受信目录 JIT 加载 packages/core/src/utils/memoryDiscovery.ts:383常驻规章分层保存,走进更深目录时才加载当地规则;不受信目录不会因为读一个文件就注入它的说明。 Skills 有明确覆盖顺序,workspace skills 受 folder trust 保护 packages/core/src/skills/skillManager.ts:17越靠近项目的技能优先级越高,但项目没被信任前不会让它改 Agent 的做事方式。
19 L1 · fact · gemini-mcp-001
MCP 支持 stdio、Streamable HTTP、SSE fallback、OAuth、动态目录刷新和 progress
先看源码事实 McpClient 连接后发现 tools/prompts/resources 并注册,监听 listChanged;网络 transport 首选 HTTP,失败可回退 SSE,401 仅在显式 oauth.enabled 时发现/认证并重试;调用有 timeout、abort 与 progress token 路由。
翻译成白话 本地子进程和远程服务都能接;服务器换工具会热刷新,远程登录不是自动偷偷弹出,必须配置允许 OAuth。
为什么这对自研重要 连接器成熟,且认证意图边界清楚;热刷新仍需依赖 registry sort 与 policy validation。
固定提交源码摘录 复制
188 async connect(): Promise<void> {
189 if (this.status !== MCPServerStatus.DISCONNECTED) {
190 throw new Error(
191 `Can only connect when the client is disconnected, current state is ${this.status}`,
192 );
193 }
194 this.updateStatus(MCPServerStatus.CONNECTING);
195 try {
196 this.client = await connectToMcpServer(
197 this.clientVersion,
198 this.serverName,
199 this.serverConfig,
200 this.debugMode,
201 this.workspaceContext,
202 this.cliConfig,
203 );
204
205 this.registerNotificationHandlers();
206
207 const originalOnError = this.client.onerror;
208 this.client.onerror = (error) => {
209 if (this.status !== MCPServerStatus.CONNECTED) {
210 return;
211 }
212 if (originalOnError) originalOnError(error);
… 69 lines omitted; exact range 188–292 …
282 }
283
284 /**
285 * Disconnects from the MCP server.
286 */
287 async disconnect(): Promise<void> {
288 if (this.status !== MCPServerStatus.CONNECTED) {
289 return;
290 }
291 for (const registries of this.registeredRegistries) {
292 registries.toolRegistry.removeMcpToolsByServer(this.serverName);
为什么相信这条结论?查看 4 处证据
20 L1 · fact · gemini-extension-001
扩展是能力包:MCP、policy/checker、context、commands、hooks、agents 与 skills 可成组热装卸
先看源码事实 ExtensionLoader 启动扩展时连接 MCP、刷新工具、注册 rules/checkers;批次完成后统一 refresh memory/system prompt/hooks/agent registry/skills。停止时按 source 移除政策并做同样刷新。
翻译成白话 扩展不是单个脚本,而是一箱可协同变化的工具、规则、记忆、钩子和 Agent。
为什么这对自研重要 扩展卸载能回收治理状态;批量刷新减少 prompt cache 抖动。
固定提交源码摘录 复制
31 /**
32 * Fully initializes all active extensions.
33 *
34 * Called within `Config.initialize`, which must already have an
35 * McpClientManager, PromptRegistry, and GeminiChat set up.
36 */
37 async start(config: Config): Promise<void> {
38 this.isStarting = true;
39 try {
40 if (!this.config) {
41 this.config = config;
42 } else {
43 throw new Error('Already started, you may only call `start` once.');
44 }
45 await Promise.all(
46 this.getExtensions()
47 .filter((e) => e.isActive)
48 .map(this.startExtension.bind(this)),
49 );
50 } finally {
51 this.isStarting = false;
52 }
53 }
54
55 /**
… 44 lines omitted; exact range 31–110 …
100 this.startCompletedCount++;
101 this.eventEmitter?.emit('extensionsStarting', {
102 total: this.startingCount,
103 completed: this.startCompletedCount,
104 });
105 if (this.startingCount === this.startCompletedCount) {
106 this.startingCount = 0;
107 this.startCompletedCount = 0;
108 }
109 await this.maybeRefreshMemories();
110 }
为什么相信这条结论?查看 3 处证据
21 L1 · fact · gemini-memory-001
GEMINI.md/Memory 分 global、user-project、extension、project,并支持受信目录 JIT 加载
先看源码事实 memory discovery 分类别收集并串联;环境 memory 从 trusted root 向上到 git root;工具触达子目录时,只有目标处于 trusted root 才 JIT 搜索并按 inode/device 去重。
翻译成白话 常驻规章分层保存,走进更深目录时才加载当地规则;不受信目录不会因为读一个文件就注入它的说明。
为什么这对自研重要 兼顾大仓库按需上下文与 prompt injection 边界。
固定提交源码摘录 复制
383 export function getExtensionMemoryPaths(
384 extensionLoader: ExtensionLoader,
385 ): string[] {
386 const extensionPaths = extensionLoader
387 .getExtensions()
388 .filter((ext) => ext.isActive)
389 .flatMap((ext) => ext.contextFiles)
390 .map((p) => toAbsolutePath(p));
391
392 // Deduplicate case-insensitively (so macOS/Windows don't keep two casings of
393 // the same file) while preserving the first encountered casing for display.
394 const seenKeys = new Set<string>();
395 const unique: string[] = [];
396 for (const p of extensionPaths) {
397 const key = normalizePath(p);
398 if (seenKeys.has(key)) continue;
399 seenKeys.add(key);
400 unique.push(p);
401 }
402 return unique.sort();
403 }
404
405 export async function getEnvironmentMemoryPaths(
406 trustedRoots: string[],
407 boundaryMarkers: readonly string[] = ['.git'],
… 36 lines omitted; exact range 383–454 …
444 pList
445 .map((p) => contentsMap.get(p))
446 .filter((c): c is GeminiFileContent => !!c),
447 );
448
449 return {
450 global: getConcatenated(paths.global),
451 extension: getConcatenated(paths.extension),
452 project: getConcatenated(paths.project),
453 userProjectMemory: getConcatenated(paths.userProjectMemory ?? []),
454 };
为什么相信这条结论?查看 3 处证据
22 L1 · fact · gemini-skills-001
Skills 有明确覆盖顺序,workspace skills 受 folder trust 保护
先看源码事实 SkillManager 依次加载 builtin、extension、user、.agents alias、workspace,后者同名覆盖前者;未信任 workspace 时直接跳过项目 skills,并可由管理员全局禁用或按名禁用。
翻译成白话 越靠近项目的技能优先级越高,但项目没被信任前不会让它改 Agent 的做事方式。
为什么这对自研重要 skills 被正确视为可执行影响,而非普通文档。
固定提交源码摘录 复制
17 export class SkillManager {
18 private skills: SkillDefinition[] = [];
19 private activeSkillNames: Set<string> = new Set();
20 private adminSkillsEnabled = true;
21
22 /**
23 * Clears all discovered skills.
24 */
25 clearSkills(): void {
26 this.skills = [];
27 }
28
29 /**
30 * Resets session-scoped state (active skill names).
31 */
32 reset(): void {
33 this.activeSkillNames.clear();
34 }
35
36 /**
37 * Sets administrative settings for skills.
38 */
39 setAdminSettings(enabled: boolean): void {
40 this.adminSkillsEnabled = enabled;
41 }
… 47 lines omitted; exact range 17–99 …
89 const projectSkills = await loadSkillsFromDir(
90 storage.getProjectSkillsDir(),
91 );
92 this.addSkillsWithPrecedence(projectSkills);
93
94 // 4.1 Workspace agent skills alias (.agents/skills)
95 const projectAgentSkills = await loadSkillsFromDir(
96 storage.getProjectAgentSkillsDir(),
97 );
98 this.addSkillsWithPrecedence(projectAgentSkills);
99 }
为什么相信这条结论?查看 2 处证据
23 L1 · fact · gemini-hooks-001
Before/AfterAgent hooks 可停止、阻断、注入上下文或要求清空后继续
先看源码事实 BeforeAgent 对每个 prompt_id 去重并可 stop/block/additionalContext;AfterAgent 只在最外层且没有 pending tools 时运行,可 stop、block、clearContext,并将 continue reason 作为新请求递归运行。
翻译成白话 钩子既能在开工前加背景/拦截,也能在收工时验收,不合格可清空现场后让 Agent 按理由重做。
为什么这对自研重要 适合策略与质量门,但递归 continuation 要有 turn 上限和 hook state 防重。
固定提交源码摘录 复制
153 // Hook state to deduplicate BeforeAgent calls and track response for
154 // AfterAgent
155 private hookStateMap = new Map<
156 string,
157 {
158 hasFiredBeforeAgent: boolean;
159 cumulativeResponse: string;
160 activeCalls: number;
161 originalRequest: PartListUnion;
162 }
163 >();
164
165 private async fireBeforeAgentHookSafe(
166 request: PartListUnion,
167 prompt_id: string,
168 ): Promise<BeforeAgentHookReturn> {
169 let hookState = this.hookStateMap.get(prompt_id);
170 if (!hookState) {
171 hookState = {
172 hasFiredBeforeAgent: false,
173 cumulativeResponse: '',
174 activeCalls: 0,
175 originalRequest: request,
176 };
177 this.hookStateMap.set(prompt_id, hookState);
… 64 lines omitted; exact range 153–252 …
242 const finalRequest = hookState.originalRequest || currentRequest;
243
244 const hookOutput = await this.config
245 .getHookSystem()
246 ?.fireAfterAgentEvent(
247 partToString(finalRequest),
248 finalResponseText,
249 stopHookActive,
250 );
251
252 return hookOutput;
为什么相信这条结论?查看 2 处证据
小练习 7 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M08 · COLLABORATION
子 Agent:把一个大任务拆成可治理的协作 什么时候是普通工具调用,什么时候才算子 Agent?子 Agent 的上下文、预算、取消和结果怎样回到父 Agent?
先用一个生活比喻 不是把同事叫来聊天就叫协作;真正的协作要有工单、权限、截止时间、交付物和回收机制。
这套实现先回答了什么? 无论子 Agent 在本机还是远程,上层看到的都是一条带编号、能续看的事件流。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 子 Agent 统一为可订阅、可重放、可中止的 AgentProtocol packages/core/src/agent/agent-session.ts:14无论子 Agent 在本机还是远程,上层看到的都是一条带编号、能续看的事件流。 Local 子 Agent 支持后台执行和取消,但同一 protocol 实例不允许并发 stream packages/core/src/agents/local-subagent-protocol.ts:69一个子 Agent 会话一次只接一单;主线程能先拿到 streamId,不必等它做完,但不能同时塞第二单。
24 L1 · fact · gemini-agent-001
子 Agent 统一为可订阅、可重放、可中止的 AgentProtocol
先看源码事实 AgentSession 包装 send/subscribe/abort/events,并把一次 stream 限定在 agent_start→agent_end;它先订阅再回放历史,处理 setup 期间 early events,支持按 eventId 或 streamId 重接。
翻译成白话 无论子 Agent 在本机还是远程,上层看到的都是一条带编号、能续看的事件流。
为什么这对自研重要 UI、SDK 与 A2A server 可共享协议,断线恢复不必理解每种 executor。
固定提交源码摘录 复制
14 /**
15 * AgentSession is a wrapper around AgentProtocol that provides a more
16 * convenient API for consuming agent activity as an AsyncIterable.
17 */
18 export class AgentSession implements AgentProtocol {
19 private _protocol: AgentProtocol;
20
21 constructor(protocol: AgentProtocol) {
22 this._protocol = protocol;
23 }
24
25 async send(payload: AgentSend): Promise<{ streamId: string | null }> {
26 return this._protocol.send(payload);
27 }
28
29 subscribe(callback: (event: AgentEvent) => void): Unsubscribe {
30 return this._protocol.subscribe(callback);
31 }
32
33 async abort(): Promise<void> {
34 return this._protocol.abort();
35 }
36
37 get events(): readonly AgentEvent[] {
38 return this._protocol.events;
… 20 lines omitted; exact range 14–69 …
59 * Returns an AsyncIterable that yields events from the agent session,
60 * optionally replaying events from history or reattaching to an existing stream.
61 *
62 * @param options Options for replaying or reattaching to the event stream.
63 */
64 async *stream(
65 options: {
66 eventId?: string;
67 streamId?: string;
68 } = {},
69 ): AsyncIterable<AgentEvent> {
为什么相信这条结论?查看 3 处证据
25 L1 · limitation · gemini-agent-002
Local 子 Agent 支持后台执行和取消,但同一 protocol 实例不允许并发 stream
先看源码事实 LocalSubagentProtocol 在 send(message) 时若已有 activeStreamId 直接报错;它用 setTimeout 后台启动,独立 AbortController,取消时丢弃 partial output 并返回空 aborted result。
翻译成白话 一个子 Agent 会话一次只接一单;主线程能先拿到 streamId,不必等它做完,但不能同时塞第二单。
为什么这对自研重要 并发应通过多个 Agent 实例而非重入同一实例,取消的部分成果不会自动保留。
固定提交源码摘录 复制
69 class LocalSubagentProtocol implements AgentProtocol {
70 private _events: AgentEvent[] = [];
71 private _subscribers = new Set<(event: AgentEvent) => void>();
72 private _streamId: string = randomUUID();
73 private _eventCounter = 0;
74 private _agentStartEmitted = false;
75 private _agentEndEmitted = false;
76 private _activeStreamId: string | undefined;
77 private _abortController = new AbortController();
78
79 // Result promise wiring — re-created per stream in _beginNewStream()
80 private _resultResolve!: (output: OutputObject) => void;
81 private _resultReject!: (err: unknown) => void;
82 private _resultPromise: Promise<OutputObject> | undefined;
83
84 // Buffered config from send({update})
85 private _bufferedConfig: Record<string, unknown> = {};
86
87 constructor(
88 private readonly definition: LocalAgentDefinition,
89 private readonly context: AgentLoopContext,
90 // Required for API parity across protocol constructors (local, remote, legacy)
91 _messageBus: MessageBus,
92 private readonly _rawActivityCallback?: (
93 activity: SubagentActivityEvent,
94 ) => void,
95 ) {}
为什么相信这条结论?查看 3 处证据
小练习 8 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M09 · STATE
会话、持久化与观测:让一次运行变成可追溯事实 如果进程崩了、用户刷新了、任务跑了一夜,系统凭什么恢复并解释“刚才究竟发生了什么”?
先用一个生活比喻 内存像白板,数据库像目录,append-only journal 像监控录像;可靠 Harness 不只保存最后答案,还保存每次转弯。
这套实现先回答了什么? 对话文件像事件日志:可以写“回到某一步”、只改元数据,也能偶尔写一张完整快照;一行坏了不拖垮整份会话。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 会话记录是增量 JSONL,支持 rewind、metadata patch 和完整 checkpoint packages/core/src/services/chatRecordingService.ts:150对话文件像事件日志:可以写“回到某一步”、只改元数据,也能偶尔写一张完整快照;一行坏了不拖垮整份会话。 OpenTelemetry 可导出到 GCP、OTLP HTTP/gRPC、文件或控制台 packages/core/src/telemetry/sdk.ts:240既能接企业观测平台,也能只落本地文件;模型调用之外还能看到进程内存和事件循环卡顿。 大型 TypeScript monorepo,测试面广,Apache-2.0 LICENSE:1这是一套带 CLI、core、SDK、ACP/A2A、策略和平台沙箱的系统,不是单文件 demo。
26 L1 · fact · gemini-persistence-001
会话记录是增量 JSONL,支持 rewind、metadata patch 和完整 checkpoint
先看源码事实 ChatRecordingService 逐行解析;$rewindTo 删除目标及以后消息,$set 可增量更新 metadata 或用 messages 数组重建 checkpoint,坏行被隔离忽略,缺关键 metadata 时回退 legacy parser。
翻译成白话 对话文件像事件日志:可以写“回到某一步”、只改元数据,也能偶尔写一张完整快照;一行坏了不拖垮整份会话。
为什么这对自研重要 支持 resume/rewind 与格式迁移,需持续测试 checkpoint 和增量事件的一致性。
固定提交源码摘录 复制
150 try {
151 const fileStream = fs.createReadStream(filePath);
152 const rl = readline.createInterface({
153 input: fileStream,
154 crlfDelay: Infinity,
155 });
156
157 let metadata: Partial<ConversationRecord> = {};
158 const messagesMap = new Map<string, MessageRecord>();
159 const messageIds: string[] = [];
160 const messageKinds = new Map<
161 string,
162 { isUser: boolean; isResumable: boolean }
163 >();
164 let isTrackingMemoryScratchpadFreshness = false;
165 let memoryScratchpadIsStale = false;
166 let firstUserMessageStr: string | undefined;
167
168 for await (const line of rl) {
169 if (!line.trim()) continue;
170 try {
171 const record = JSON.parse(line) as unknown;
172 if (isRewindRecord(record)) {
173 if (isTrackingMemoryScratchpadFreshness) {
174 memoryScratchpadIsStale = true;
… 18 lines omitted; exact range 150–203 …
193 if (found) idsToDelete.push(id);
194 }
195 if (found) {
196 for (const id of idsToDelete) {
197 messagesMap.delete(id);
198 }
199 } else {
200 messagesMap.clear();
201 }
202 }
203 } else if (isMessageRecord(record)) {
为什么相信这条结论?查看 3 处证据
27 L1 · fact · gemini-observe-001
OpenTelemetry 可导出到 GCP、OTLP HTTP/gRPC、文件或控制台
先看源码事实 telemetry SDK 同时构造 trace/log/metric exporter,支持 GCP 直出、OTLP HTTP/grpc+gzip、单文件和 console;NodeSDK 加载 HTTP instrumentation,并可周期监控内存和 event loop。
翻译成白话 既能接企业观测平台,也能只落本地文件;模型调用之外还能看到进程内存和事件循环卡顿。
为什么这对自研重要 生产可观测性完整,但凭据、提示内容与遥测开关需纳入隐私治理。
固定提交源码摘录 复制
240 const otlpEndpoint = config.getTelemetryOtlpEndpoint();
241 const otlpProtocol = config.getTelemetryOtlpProtocol();
242 const telemetryTarget = config.getTelemetryTarget();
243 const useCollector = config.getTelemetryUseCollector();
244
245 const parsedEndpoint = parseOtlpEndpoint(otlpEndpoint, otlpProtocol);
246 const telemetryOutfile = config.getTelemetryOutfile();
247 const useOtlp = !!parsedEndpoint && !telemetryOutfile;
248
249 const gcpProjectId =
250 process.env['OTLP_GOOGLE_CLOUD_PROJECT'] ||
251 process.env['GOOGLE_CLOUD_PROJECT'];
252 const useDirectGcpExport =
253 telemetryTarget === TelemetryTarget.GCP && !useCollector;
254
255 let spanExporter:
256 | OTLPTraceExporter
257 | OTLPTraceExporterHttp
258 | GcpTraceExporter
259 | FileSpanExporter
260 | ConsoleSpanExporter;
261 let logExporter:
262 | OTLPLogExporter
263 | OTLPLogExporterHttp
264 | GcpLogExporter
… 43 lines omitted; exact range 240–318 …
308 url: parsedEndpoint,
309 compression: CompressionAlgorithm.GZIP,
310 });
311 metricReader = new PeriodicExportingMetricReader({
312 exporter: new OTLPMetricExporter({
313 url: parsedEndpoint,
314 compression: CompressionAlgorithm.GZIP,
315 }),
316 exportIntervalMillis: 10000,
317 });
318 }
为什么相信这条结论?查看 2 处证据
28 L3 · fact · gemini-maturity-001
大型 TypeScript monorepo,测试面广,Apache-2.0
先看源码事实 固定提交约 2,144 个 TypeScript 文件,机械统计 892 个 *.test.ts/tsx;根许可证是 Apache License 2.0。
翻译成白话 这是一套带 CLI、core、SDK、ACP/A2A、策略和平台沙箱的系统,不是单文件 demo。
为什么这对自研重要 架构可借鉴性高,但新旧上下文路径和多前端会扩大回归矩阵。
边界与风险 文件数是固定 checkout 的机械统计,不等于测试覆盖率。
固定提交源码摘录 复制
1
2 Apache License
3 Version 2.0, January 2004
4 http://www.apache.org/licenses/
5
6 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
8 1. Definitions.
9
10 "License" shall mean the terms and conditions for use, reproduction,
11 and distribution as defined by Sections 1 through 9 of this document.
12
13 "Licensor" shall mean the copyright owner or entity authorized by
14 the copyright owner that is granting the License.
15
16 "Legal Entity" shall mean the union of the acting entity and all
17 other entities that control, are controlled by, or are under common
18 control with that entity. For the purposes of this definition,
19 "control" means (i) the power, direct or indirect, to cause the
20 direction or management of such entity, whether by contract or
21 otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 outstanding shares, or (iii) beneficial ownership of such entity.
23
24 "You" (or "Your") shall mean an individual or Legal Entity
25 exercising permissions granted by this License.
26
27 "Source" form shall mean the preferred form for making modifications,
28 including but not limited to software source code, documentation
为什么相信这条结论?查看 3 处证据
小练习 9 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M10 · ENGINEERING
测试、恢复与工程取舍:把漂亮机制变成可靠产品 哪些行为有测试证明?哪些只是配置或 prompt 约定?当安全、性能、可恢复性冲突时,源码选择了什么?
先用一个生活比喻 这像验收一座桥:图纸说明结构,测试证明承重,故障演练证明断电后还能不能让人安全回来。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
读源码时先问 本课的判断方式 这一层有没有独立证据? 没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。 谁拥有最终控制权? 区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念 固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。
小练习 10 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M11 · PRACTICE
把读懂变成会判断 下面的练习不要求你先写一个完整 Agent,而是训练你检查设计边界:事实是什么、推断是什么、如果换成自研产品要补哪一层。
Q1 主 Harness 用递归 sendMessageStream 驱动多 turn,硬上限为 100 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 控制流直观,递归路径共享 prompt_id 和 hook state,需要严格做 activeCalls 记账。
证据:packages/core/src/core/client.ts:79 Q2 每轮先做上下文、溢出、IDE 配对和 loop 检测,再锁定模型与工具 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 上下文与模型选择顺序清楚,工具描述可随模型变化且同一 sequence 保持模型粘性。
证据:packages/core/src/core/client.ts:614 Q3 循环检测能先恢复一次,再判定硬循环 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 比仅靠 turn 上限更早止损,并保留一次自恢复空间。
证据:packages/core/src/core/client.ts:744 Q4 统一 ContentGenerator 契约覆盖流式、非流式、计数与 embedding 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 Provider 切换不改主循环,但 thought signature 兼容性仍需在切换认证后处理。
证据:packages/core/src/core/contentGenerator.ts:35 Q5 个人/ADC 走 Code Assist,API key/Vertex/Gateway 走 Google GenAI SDK 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 同一主循环有多条传输实现,认证切换测试必须覆盖历史 thoughtSignature 清理。
证据:packages/core/src/core/contentGenerator.ts:285
APPENDIX · SOURCE INDEX
本课读过的实现文件 文件索引帮助你在课程外继续追踪调用链;每一条路径来自固定提交的证据账本。
01 packages/core/src/core/client.tsL79–111, L875–907, L910–1060, L614–715, L717–807, L744–763, L810–855, L107–120, L1196–1248, L640–678, L829–838, L153–252, L930–1034 02 packages/core/src/core/contentGenerator.tsL35–70, L72–110, L285–310, L312–410 03 packages/core/src/core/geminiChat.tsL517–578, L580–648 04 packages/core/src/context/chatCompressionService.tsL37–52, L54–100, L124–142, L144–215 05 packages/core/src/context/contextManager.tsL26–88, L90–195, L197–275 06 packages/core/src/core/turn.tsL236–320 07 packages/core/src/agents/agent-scheduler.tsL42–92 08 packages/core/src/scheduler/tool-executor.tsL250–297, L299–368 09 packages/core/src/policy/policy-engine.tsL49–195, L198–260, L253–260, L284–341 10 packages/core/src/services/sandboxManagerFactory.tsL19–43 11 packages/core/src/services/sandboxManager.tsL285–333, L194–223 12 packages/core/src/sandbox/linux/LinuxSandboxManager.tsL48–114 13 packages/core/src/sandbox/windows/WindowsSandboxManager.tsL56–72 14 packages/core/src/policy/sandboxPolicyManager.tsL49–94, L140–158 15 packages/core/src/tools/mcp-client.tsL188–292, L390–430, L1829–1916, L1927–2059 16 packages/core/src/utils/extensionLoader.tsL31–110, L113–132, L172–225 17 packages/core/src/utils/memoryDiscovery.tsL383–454, L512–572, L578–639 18 packages/core/src/skills/skillManager.tsL17–99, L124–188 19 packages/core/src/agent/agent-session.tsL14–69, L70–165, L166–223 20 packages/core/src/agents/local-subagent-protocol.tsL69–95, L112–162, L183–215 21 packages/core/src/services/chatRecordingService.tsL150–203, L203–300, L300–350 22 packages/core/src/telemetry/sdk.tsL240–318, L319–375 23 LICENSEL1–28 24 packages/core/src/context/contextManager.test.tsL1–40 25 packages/core/src/services/sandboxManager.integration.test.tsL1–40