Main Chain B · Step 09

prepareRequest + buildRequest:从日志折叠请求

agent.ts:501-620。这是「模型可见 ⟺ 已记录」最集中的实现:请求的每个部分——config、system、tools、messages——全部从日志折叠而来,然后冻结成一个不可变对象。0.1.5 起它拆成两个函数:prepareRequest(解析配置 + 绑定 adapter,501 行)与 buildRequest(组装冻结请求,553 行)——因为 system prompt 的投影决策需要先知道 adapter 的能力(systemPromptUpdate)。

11 step:353 buildRequest(...) → agent.ts:501 prepareRequest + :553 buildRequest → (分叉) llm.prepareCall → 10 页 · session.requestHeader → 06 页 → (下一步) 10 llm/stream

示例本次示例:step 1 请求的折叠实况

示例轨迹 09-1 · 首次请求的 buildRequest 输入输出
# 调用(11 页 340 行):
# buildRequest(1, 1, assembly.tools, system, session.deriveMessages(), signal)
# 其中 boundaryMessages = deriveMessages() 的当前投影:
#   [ user/message「帮我列一下当前目录」(seq=2) ]     ← 从日志投影,不是内存对象

# ① 折叠上次配置(510 行):日志里还没有 request/header → persistedHeader=undefined
#    → seedConfig = { provider:'deepseek-official', model:'deepseek-v4-pro',
#                     reasoningEffort:'max' }        ← 来自 AgentOptions
# ② agent/request waterfall(457 行):无监听器改写 → proposedConfig = seedConfig
# ③ prepareCall(468 行):绑定 adapter 注册 + 物化模型默认值
#    → config = { ...proposedConfig, maxTokens: 模型目录的默认 32768 }
#      (真实默认:llm-deepseek 的 DEFAULT_MAX_TOKENS)
# ④ header 落盘(551-562 行):reason:'initial'
#    { "type":"request/header", "data":{ "header":{
#        "config":{...}, "adapterDefaults":{maxTokens:true}, "system":"...", "tools":[...] },
#      "reason":"initial" } }
# ⑤ 最终请求(638 行):markAgentLoopRequest(Object.freeze({...}))
来源:09 页行级解读 + 真实 fixture 的 request/header 行格式(06 页示例轨迹 06-1)
示例轨迹 09-2 · 第二个请求为什么不再记 header
# step 2 的 buildRequest:baseline = 上次的 header(agent.ts:595 requestHeader())
# config/system/tools 都没变 → headerEquals(baseline, header) === true
# → 不落新 request/header 事件(601 行的 else-if 不命中)
# → 日志里 header 历史只有一条 initial——「配置如何演变」的记录是稀疏的
来源:09 页 595-608 行 + 06 页 requestHeader 的增量缓存

§1完整代码与行级解读

packages/core/agent-loop/src/agent.tsprepareRequest + buildRequest 全文529-646
529  private async prepareRequest(turn: number, step: number, signal: AbortSignal): Promise<{ config; preparedCall? }> {   // 拆分 1解析配置 + 绑定 adapter
538    const persistedHeader = session.requestHeader()
539    const persistedConfig = persistedHeader?.config
541    const persistedReasoningEffort = persistedConfig?.provider === route.provider
543      && persistedHeader?.adapterDefaults?.reasoningEffort !== true
546    const reasoningEffort = this.options.reasoningEffort ?? persistedReasoningEffort
547    const maxTokens = this.options.maxTokens
548    const seedConfig = deepFreeze(structuredClone(
558    const proposedConfig = await this.dispatch.waterfall(
559      'agent/request', { turn, step, signal },
562    signal.throwIfAborted()
563    if (!proposedConfig.provider || !proposedConfig.model) {
564      throw new Error(`agent "${this.id}" has no provider/model: ...`)
565    }
566    let config: LlmCallConfig
567    let preparedCall: PreparedLlmCall | undefined
568    try {
569      preparedCall = await this.loopCtx.llm.prepareCall(proposedConfig, signal)
570      config = preparedCall.config
571    } catch (error: unknown) {
573      if (!(error instanceof LlmError) || error.code !== 'NO_ADAPTER') throw error
574      config = proposedConfig
575    }
576    signal.throwIfAborted()
577    return { config, ...preparedCall === undefined ? {} : { preparedCall } }   // 交给调用方step 用它投影 system
578  }
581  private buildRequest(config: LlmCallConfig, preparedCall: PreparedLlmCall | undefined,
585    startsRequestSeries: boolean, signal: AbortSignal): GenerateOptions {   // 拆分 2
588    const { session } = this
589    const surfaceGeneration = session.surface.contentGeneration   // 0.1.6:contentGeneration含插件改写
590    const header = canonicalHeader({
591      config,
592      ...preparedCall === undefined ? {} : { adapterDefaults: preparedCall.adapterDefaults },
593      ...tools.length > 0 ? { tools } : {},     // 注意:没有 system 字段
594    })
595    const baseline = this.session.requestHeader()
596    const startsSeries = startsRequestSeries
597      || this.requestSurfaceGeneration !== surfaceGeneration
598    if (!this.requestHeaderLogged) {
599      this.session.append('request/header', { header, reason: baseline === undefined ? 'initial' : 'resume' })
600      this.requestHeaderLogged = true
601    } else if (baseline === undefined || !headerEquals(baseline, header)) {
612    const contextWindow = preparedCall?.context?.contextWindow
613    const systemPromptUpdate = preparedCall?.systemPromptUpdate
614    const requestContext: RequestContext = {
615      provider: config.provider,
616      model: config.model,
617      ...contextWindow === undefined ? {} : { contextWindow },
618      ...systemPromptUpdate === undefined ? {} : { systemPromptUpdate },
619    }
620    const previousContext = session.requestContext()
621    if (previousContext?.provider !== requestContext.provider
622      || previousContext.model !== requestContext.model
623      || previousContext.contextWindow !== requestContext.contextWindow
624      || previousContext.systemPromptUpdate !== requestContext.systemPromptUpdate) {
625      session.append('request/context', requestContext)
626    }
627    signal.throwIfAborted()
630    deepFreeze(header)
631    const boundaryMessages = session.deriveMessages()   // 现在在此 derive
632    for (const message of boundaryMessages) {
633      if (this.frozenMessages.has(message)) continue
634      deepFreeze(message)
635      this.frozenMessages.add(message)
636    }
637    Object.freeze(boundaryMessages)
638    const request = markAgentLoopRequest(Object.freeze({
644    }))
645    return request     // 不再返回 preparedCall(它由 prepareRequest 单独返回)
646  }
501-549

prepareRequest(0.1.5 新增):只做「解析请求配置 + 绑定 adapter」——从日志折叠上次 header(510-518)、agent/request waterfall(530-532)、prepareCall 冻结绑定(541)、NO_ADAPTER 宽容(545-546)。返回 { config, preparedCall } 交给调用方。**它不碰日志、不组装请求**——纯解析。

553-569

buildRequest 现在是同步纯函数:接收已解析的 config/preparedCall/tools,折叠 canonicalHeader(562-566)。注意 header 里没有 system 字段(agent.ts:565)——0.1.5 起 system prompt 不在 header 里(它在 surface 上,由 step 的 systemPrompt.project() 落盘为 system/message)。

570-582

header 落盘三路(initial/resume · change 可附 startsSeries · series),与 0.1.2 引入的 series 机制一致。

584-598

request/context 新增 systemPromptUpdate 维度:记录该路由的 prompt 更新能力('in-history' 或缺席)。能力变化也会触发落盘(agent.ts:596)。这是 0.1.5 的新元数据——step 的 systemPrompt.project() 用它决定「能否在历史中间追加 prompt」。

601-609

boundaryMessages 现在在这里 derive(agent.ts:603 session.deriveMessages())——不再由 step 传入。这正是「模型可见 ⟺ 已记录」的强化:请求的 messages 完全由日志派生,连参数都不给外部传的机会。

610-617

组装冻结请求(markAgentLoopRequest(Object.freeze({...})))并返回。不再返回 preparedCall——它由 prepareRequest 单独返回,两者在 step 里汇合(11 页 362/379 行)。

§40.1.3-alpha.2 新增:request/header 的 series 维度

这一版为 request/header 引入「消息系列」概念。背景:模型对话可以包含多个显式区分的消息系列(例如 compaction 之后的新系列、跨模型代际的系列切换)——若 header 完全没变,日志里就没有「这里开始了一个新系列」的持久标记,下游投影/续跑无法定位系列边界。

  • 两个触发源(agent.ts:568-569):① preStep 的 enter 决定带 startsRequestSeries: true(08 页 PreparedStep,插件显式宣告);② surface 的 replaceGeneration 变了(compaction 的 replace 重写过 surface)——step 在每次迭代时快照 surfaceGeneration(11 页),buildRequest 拿它和 requestSurfaceGeneration(上一个已建请求的代数)比较。
  • 落盘三条路(agent.ts:570-581):见上表——initial/resume 只落一次;change 可附 startsSeries;series 是「header 未变但系列开始」的专门 reason。
  • 类型层:RequestHeaderReason = 'initial' | 'resume' | 'change' | 'series'(session/src/types.ts:261),request/header 数据新增可选 startsSeries?: true(types.ts:369)。

§2requestProposal:adapter 默认值的剥离

packages/core/agent-loop/src/agent.ts剥离 adapter 派生值62-69
62/** Remove adapter-derived values before plugins propose the next request config. */
63function requestProposal(header: EpochHeader): LlmCallConfig {
64  if (header.adapterDefaults === undefined) return header.config
65  const proposal = { ...header.config }
66  if (header.adapterDefaults.reasoningEffort === true) delete proposal.reasoningEffort
67  if (header.adapterDefaults.maxTokens === true) delete proposal.maxTokens
68  return proposal
69}
62-69

上一次请求的 config 里可能混着 adapter 物化出来的默认值(调用方没指定,adapter 按模型目录补上的)。如果把它们原样作为下一次提案的种子,切换模型后这些值会「粘」在新模型上。所以:凡是 adapterDefaults 标记过的字段,从提案里删掉,让下一个 adapter 重新物化自己的默认值。这就是 agent.ts:513-518「effort 只在同模型时恢复」的姊妹逻辑。

§3prepareCall:一次性派发 + 代际绑定

prepareCall 在 packages/llm/llm/src/index.ts:921(10 页会看到 LlmRuntime 全貌)。这里看它的全文(llm/src/index.ts:921-965)——尤其 systemPromptUpdate(947 行,0.1.5 新增)与 stream 闭包的两道守卫:

packages/llm/llm/src/index.tsprepareCall 与 stream 闭包921-965
921  async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall> {
922    const registration = this.registration(config.provider)
923    const adapterCall = await registration.adapter.prepareCall(config.provider, config.model, signal)
924    const modelInfo = this.normalizeModelInfo(registration, config.model, adapterCall.model)
925    const resolved = this.resolveCallWithInfo(config, modelInfo)
926    const resolvedConfig = deepFreeze(structuredClone(resolved.config))
927    const context = resolved.context === undefined ? undefined : deepFreeze(structuredClone(resolved.context))
930    const adapterDefaults = deepFreeze<LlmCallConfigAdapterDefaults>({标注哪些是 adapter 补的
931      ...config.reasoningEffort === undefined && resolvedConfig.reasoningEffort !== undefined
932        ? { reasoningEffort: true }
933        : {},
934      ...config.maxTokens === undefined && resolvedConfig.maxTokens !== undefined
935        ? { maxTokens: true }
936        : {},
937    })
938    let dispatched = false
939    return Object.freeze({
940      config: resolvedConfig,
941      retryPolicy: registration.retryPolicy,
942      adapterDefaults,
943      ...context === undefined ? {} : { context },
944      ...modelInfo.inputModalities === undefined
945        ? {}
946        : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
947      ...modelInfo.systemPromptUpdate === undefined ? {} : { systemPromptUpdate: ... },0.1.5 新增能力位
948      stream: (options: GenerateOptions): AsyncIterable<StreamChunk> => {
949        if (dispatched) {
950          throw new LlmError('a prepared LLM call can only be dispatched once', 'INVALID_PREPARED_CALL')
951        }
952        if (!callConfigEquals(options, resolvedConfig)) {
953          throw new LlmError(
954            'prepared LLM call config changed before adapter dispatch',
955            'INVALID_PREPARED_CALL',
956          )
957        }
958        dispatched = true
959        return this.streamWithRegistration(options, {
960          registration, config: resolvedConfig, modelInfo,
961          dispatch: options => adapterCall.stream(options),
962        })
963      },
964    })
965  }
933-953

两个守卫:① dispatched——一个 prepared call 只能派发一次;② callConfigEquals——派发时的请求 config 必须与 prepare 时解析出的 config 完全一致。违者 INVALID_PREPARED_CALL。0.1.5 起返回对象新增 systemPromptUpdate(942 行)——路由声明它能否读 in-history 的 system prompt,供 11 页 step 的投影决策使用。

934-942

返回对象整体 freeze;resolvedConfig 在 928 行已 deepFreeze + structuredClone。还携带 inputModalities 与 systemPromptUpdate(942 行,0.1.5 新增)(该 adapter 代际捕获的精确模型模态,同样冻结)——prepare 之后的一切都是不可变的。

916-923

0.1.3 机制:代际绑定。prepareCall 现在先调 registration.adapter.prepareCall(provider, model, signal)(LlmAdapter.prepareCall,llm/src/index.ts:266,动态 adapter 可 override)拿到与「最终 stream 调用」同一代际的精确模型元数据(PreparedAdapterCall),再 resolve 配置——设置变更发生在 prepare 与 dispatch 之间时,不可能把一代的能力与另一代的 endpoint 拼起来。

为什么需要这个机制?配置树可以热重组(04 页)。一次 prepare 把能力结果(contextWindow、adapterDefaults、retryPolicy、inputModalities)绑定到那个时刻的 adapter 注册与模型代际。如果不守卫,HMR 后可能把 A adapter 的能力结果和 B adapter 的请求拼在一起。prepared call 要么原样用掉,要么明确失败,绝不静默换 adapter。