Harness · Coding Agent Book12 / Gemini CLI
研究总览
M12 · SOURCE-GROUNDED TUTORIAL

Gemini CLI
从源码学会它怎么工作

工具调度、PolicyEngine、扩展热装卸和新图式上下文系统都很有研究价值;默认 sandbox 仍关闭。 我们不把 README 当结论,而是沿主循环、工具、上下文、权限、扩展、协作和状态一路读到实现。

TypeScript · Scheduler + Context PipelineApache-2.0bef6119500b028 个结论 · 67 处引用
这门课怎么读

先建立直觉,再沿一条任务链钻进代码

参考教程的做法不是把 API 名称罗列出来,而是从一个小白能理解的问题开始,先解释“为什么需要这个机制”,再用概念对比、执行链路和固定提交的源码回答“它究竟怎么做”。本页把 Gemini CLI 的 28 个源码结论重新编排成十节课;每个结论都保留证据等级、文件路径、行号和可点击源码。

你会得到一张可复述的架构地图一次完整任务的链路追踪能迁移到自研 Harness 的设计判断
你不会得到把 README 功能当成已验证事实把 prompt 约束说成 OS 沙箱把一次双模型调用夸成多 Agent 平台
M00 · MAP

先看全景:这个 Agent 的控制面在哪里

下面的图不是产品宣传图,而是把固定提交里最关键的入口、循环、模型、工具、安全、状态和协作节点放在一张地图上。

架构图全屏打开 ↗
一轮执行链路全屏打开 ↗
核心机制

递归 sendMessageStream;100 turn 上限;loop 恢复一次

上下文

Legacy 摘要 + 新图/流水线/GC/蒸馏/真实 token 校准

适用建设

Google 生态、大型扩展平台、上下文工程研究

M00.5 · TRACE

跟踪一个任务:从输入到交付

把下面九步当成你读源码时的“地图坐标”。每到一个节点,都能回到后面的章节查具体实现。

读图提醒

箭头只表示控制面之间的关系,不代表每个实现都同步、串行或拥有 OS 隔离;真正的边界要以对应章节的源码摘录和 caveat 为准。

M01 · ORIENTATION

先把 Agent 看成一台会交付的机器

如果只看 README,你知道它能做什么;钻进源码后,我们要知道它为什么能做、什么时候会停、失败后谁负责收拾。

先用一个生活比喻

把 Agent 想成一间带传送带的工作室:入口收任务,主循环决定下一步,模型负责提出动作,工具负责动手,状态账本负责让下一班人接着干。

本节阅读法先问问题读事实看代码做迁移判断
读源码时先问本课的判断方式
这一层有没有独立证据?没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。
谁拥有最终控制权?区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念

固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。

小练习 1

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M02 · LOOP

主循环:模型为什么会继续动

一次模型调用为什么会变成十几步?循环靠什么继续,靠什么停止?

先用一个生活比喻

像一个会看回执的快递员:模型先写行动单,工具返回回执,主循环把回执放回桌面,再让模型决定下一张行动单。

这套实现先回答了什么?

一次用户请求可以连续让模型说、用工具、再说;但最多转 100 圈,避免无尽自言自语。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
主 Harness 用递归 sendMessageStream 驱动多 turn,硬上限为 100packages/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 契约覆盖流式、非流式、计数与 embeddingpackages/core/src/core/contentGenerator.ts:35上层只认一套生成接口,底下可换个人 Google 登录、API key、企业 Vertex 或网关。
个人/ADC 走 Code Assist,API key/Vertex/Gateway 走 Google GenAI SDKpackages/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

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M04 · TOOLS

工具系统:Agent 的手脚怎样被注册和调度

模型看见的工具说明,和真正执行工具的代码,是不是同一个东西?并发、编辑和失败结果怎么处理?

先用一个生活比喻

工具系统像机场:模型提交登机牌,注册表确认航班,权限闸机检查证件,调度器决定跑道,最后才允许真正起飞。

这套实现先回答了什么?

模型流负责开任务单,调度器负责审批、排队、执行和回执;两者不是揉在一个 switch 里。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
Turn 只解析模型流,工具执行交给独立 event-driven Schedulerpackages/core/src/core/turn.ts:236模型流负责开任务单,调度器负责审批、排队、执行和回执;两者不是揉在一个 switch 里。
超大工具结果在调度阶段落盘,取消也返回合法 functionResponsepackages/core/src/scheduler/tool-executor.ts:250工具被叫停也必须交一张正式回执;已经产生的大输出不会硬塞回上下文。
07
L1 · fact · gemini-tools-001

Turn 只解析模型流,工具执行交给独立 event-driven Scheduler

先看源码事实

Turn 从 generateContentStream 收集 text、thought、functionCall、usage 和 finish reason,将 functionCall 放进 pendingToolCalls;主/子 Agent 再以各自 registry、message bus、sandbox manager 创建 Scheduler 批量执行。

翻译成白话

模型流负责开任务单,调度器负责审批、排队、执行和回执;两者不是揉在一个 switch 里。

为什么这对自研重要

主 Agent 和子 Agent 能复用同一工具治理链,同时各自限定工具集。

固定提交源码摘录
  236    | ServerGeminiModelInfoEvent
  237    | ServerGeminiAgentExecutionStoppedEvent
  238    | ServerGeminiAgentExecutionBlockedEvent;
  239  
  240  // A turn manages the agentic loop turn within the server context.
  241  export class Turn {
  242    private callCounter = 0;
  243  
  244    readonly pendingToolCalls: ToolCallRequestInfo[] = [];
  245    private debugResponses: GenerateContentResponse[] = [];
  246    private pendingCitations = new Set<string>();
  247    private cachedResponseText: string | undefined = undefined;
  248    finishReason: FinishReason | undefined = undefined;
  249    private hasLoggedRagTrace = false;
  250  
  251    constructor(
  252      private readonly chat: GeminiChat,
  253      private readonly prompt_id: string,
  254    ) {}
  255  
  256    // The run method yields simpler events suitable for server logic
  257    async *run(
  258      modelConfigKey: ModelConfigKey,
  259      req: PartListUnion,
  260      signal: AbortSignal,
      … 49 lines omitted; exact range 236–320 …
  310          const resp = streamEvent.value;
  311          if (!resp) continue; // Skip if there's no response body
  312  
  313          // Log RAG trace if enabled (only once per turn to avoid log bloat on streams)
  314          if (
  315            !this.hasLoggedRagTrace &&
  316            this.chat.context.config.getLogRagSnippets?.()
  317          ) {
  318            let ragStatus: string | undefined;
  319            let snippets: RagSnippet[] | undefined;
  320  
为什么相信这条结论?查看 2 处证据
08
L1 · fact · gemini-tools-002

超大工具结果在调度阶段落盘,取消也返回合法 functionResponse

先看源码事实

ToolExecutor 对超过阈值的文本写入项目临时目录并替换为截断文本;取消时若已有部分输出仍先截断/保存,再构造带 error 的 functionResponse,保持 Gemini 协议配对。

翻译成白话

工具被叫停也必须交一张正式回执;已经产生的大输出不会硬塞回上下文。

为什么这对自研重要

失败/取消不会破坏下一轮历史,且原始输出可从文件追查。

固定提交源码摘录
  250        content.length === 1 &&
  251        'tool' in call &&
  252        call.tool instanceof DiscoveredMCPTool
  253      ) {
  254        const firstPart = content[0];
  255        if (typeof firstPart === 'object' && typeof firstPart.text === 'string') {
  256          const textContent = firstPart.text;
  257          const threshold = this.config.getTruncateToolOutputThreshold();
  258  
  259          if (threshold > 0 && textContent.length > threshold) {
  260            const originalContentLength = textContent.length;
  261            const { outputFile: savedPath } = await saveTruncatedToolOutput(
  262              textContent,
  263              toolName,
  264              callId,
  265              this.config.storage.getProjectTempDir(),
  266              this.context.promptId,
  267            );
  268            outputFile = savedPath;
  269            const truncatedText = formatTruncatedToolOutput(
  270              textContent,
  271              outputFile,
  272              threshold,
  273            );
  274  
      … 12 lines omitted; exact range 250–297 …
  287                threshold,
  288              }),
  289            );
  290  
  291            return { truncatedContent, outputFile };
  292          }
  293        }
  294      }
  295  
  296      return { truncatedContent: content, outputFile };
  297    }
为什么相信这条结论?查看 2 处证据
小练习 4

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

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、模式、交互状态和 subagentpackages/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、动态目录刷新和 progresspackages/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 统一为可订阅、可重放、可中止的 AgentProtocolpackages/core/src/agent/agent-session.ts:14无论子 Agent 在本机还是远程,上层看到的都是一条带编号、能续看的事件流。
Local 子 Agent 支持后台执行和取消,但同一 protocol 实例不允许并发 streampackages/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 和完整 checkpointpackages/core/src/services/chatRecordingService.ts:150对话文件像事件日志:可以写“回到某一步”、只改元数据,也能偶尔写一张完整快照;一行坏了不拖垮整份会话。
OpenTelemetry 可导出到 GCP、OTLP HTTP/gRPC、文件或控制台packages/core/src/telemetry/sdk.ts:240既能接企业观测平台,也能只落本地文件;模型调用之外还能看到进程内存和事件循环卡顿。
大型 TypeScript monorepo,测试面广,Apache-2.0LICENSE: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

本课读过的实现文件

文件索引帮助你在课程外继续追踪调用链;每一条路径来自固定提交的证据账本。

  1. 01packages/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
  2. 02packages/core/src/core/contentGenerator.tsL35–70, L72–110, L285–310, L312–410
  3. 03packages/core/src/core/geminiChat.tsL517–578, L580–648
  4. 04packages/core/src/context/chatCompressionService.tsL37–52, L54–100, L124–142, L144–215
  5. 05packages/core/src/context/contextManager.tsL26–88, L90–195, L197–275
  6. 06packages/core/src/core/turn.tsL236–320
  7. 07packages/core/src/agents/agent-scheduler.tsL42–92
  8. 08packages/core/src/scheduler/tool-executor.tsL250–297, L299–368
  9. 09packages/core/src/policy/policy-engine.tsL49–195, L198–260, L253–260, L284–341
  10. 10packages/core/src/services/sandboxManagerFactory.tsL19–43
  11. 11packages/core/src/services/sandboxManager.tsL285–333, L194–223
  12. 12packages/core/src/sandbox/linux/LinuxSandboxManager.tsL48–114
  13. 13packages/core/src/sandbox/windows/WindowsSandboxManager.tsL56–72
  14. 14packages/core/src/policy/sandboxPolicyManager.tsL49–94, L140–158
  15. 15packages/core/src/tools/mcp-client.tsL188–292, L390–430, L1829–1916, L1927–2059
  16. 16packages/core/src/utils/extensionLoader.tsL31–110, L113–132, L172–225
  17. 17packages/core/src/utils/memoryDiscovery.tsL383–454, L512–572, L578–639
  18. 18packages/core/src/skills/skillManager.tsL17–99, L124–188
  19. 19packages/core/src/agent/agent-session.tsL14–69, L70–165, L166–223
  20. 20packages/core/src/agents/local-subagent-protocol.tsL69–95, L112–162, L183–215
  21. 21packages/core/src/services/chatRecordingService.tsL150–203, L203–300, L300–350
  22. 22packages/core/src/telemetry/sdk.tsL240–318, L319–375
  23. 23LICENSEL1–28
  24. 24packages/core/src/context/contextManager.test.tsL1–40
  25. 25packages/core/src/services/sandboxManager.integration.test.tsL1–40
下一步

从课程回到报告,做一次反向核验

教程负责让你读懂,报告负责让你查证。打开报告页,任选一个章节,尝试只靠源码摘录复述它的边界。

查看 Gemini CLI 报告 ↗