query.ts用户消息主线解析
query.ts用户消息主线解析
1. 这份文档回答什么问题
这份文档只回答一个问题:
一条用户消息进入 src/query.ts 之后,如何一步步变成一次完整的 assistant turn。
这里不按文件结构讲,而按“用户消息生命周期”讲。
2. 用户消息进入点
在 query.ts 里,用户消息不是在这里被创建的,而是已经作为历史消息存在于 QueryParams.messages 里。
入口层次是:
query(params)yield* queryLoop(params, consumedCommandUuids)
所以对阅读者来说,真正重要的是 queryLoop()。
3. 第一步:从历史里取出本轮可见消息视图
queryLoop() 每轮都会从 state.messages 出发,先构造:
messagesForQuery
这一步不是简单复制,而是“为本轮模型请求投影一个可见上下文”。
它会经历:
- compact boundary 之后的消息切片
- tool result budget
- snip compact
- microcompact
- context collapse
- autocompact
因此,一条用户消息进入模型之前,已经先和历史上下文一起经过了一轮治理。
4. 第二步:拼接 system prompt 和上下文
完成 messagesForQuery 之后,query.ts 还会组装:
fullSystemPromptuserContextsystemContext
真正发模型时,不是裸发 messagesForQuery,而是:
prependUserContext(messagesForQuery, userContext)systemPrompt: fullSystemPrompt
这说明用户消息最终进入的是一份“加工后的 API 输入”,而不是本地原始 transcript。
5. 第三步:开始模型流式输出
核心调用在:
for await (const message of deps.callModel(...))
这时 query.ts 会一边接流,一边做三类工作:
- 收集
assistantMessages - 识别
tool_use - 识别那些“先别往外抛、后面可能能恢复”的错误
这一步是整条链路的真正运行时核心。
6. 第四步:分叉判断是否触发了工具
如果 assistant 输出里出现了 tool_use:
- 把它加入
toolUseBlocks - 设置
needsFollowUp = true
这是用户消息生命周期里最关键的分叉点:
- 没有
tool_use:尝试结束 - 有
tool_use:进入工具执行,再继续下一轮
7. 第五步:如果没有工具,先别急着结束
即便模型看起来“答完了”,query.ts 仍然不会立刻结束。
它还会继续检查:
- prompt-too-long 恢复
- media-size 恢复
- max_output_tokens 恢复
- stop hooks
- token budget continuation
也就是说:
模型结束,不等于系统结束。
系统还要判断:
- 要不要压缩后重试
- 要不要插一条 meta user message 续写
- 要不要因为 stop hook 或预算策略再跑一轮
8. 第六步:如果有工具,执行工具并把结果回填
当 needsFollowUp === true 时,流程进入工具阶段。
这里有两条执行路径:
StreamingToolExecutor.getRemainingResults()runTools(...)
最终都产出统一的 toolUpdates 流。
每个 update 可能带两种东西:
messagenewContext
于是这条用户消息会继续派生出:
tool_result- attachment
- 甚至新的 runtime 上下文
9. 第七步:工具结束后再补附件
工具执行完,并不代表这一轮立即结束。
query.ts 还会继续注入:
- queued command attachments
- memory attachments
- skill attachments
所以一条用户消息触发出来的下一轮输入,不只是:
- 用户消息
- assistant 消息
- tool_result
还包括系统补充进来的辅助上下文。
10. 第八步:构造下一轮 state
如果要继续,query.ts 最后会构造新的 State:
messages = [...messagesForQuery, ...assistantMessages, ...toolResults]- 更新
toolUseContext turnCount + 1- 重置若干恢复标志
transition = { reason: 'next_turn' }
然后回到 while (true) 顶部。
这说明 query.ts 表面上是一个 while 循环,实际上更接近:
一个围绕 transcript 反复重建 state 的状态机。
11. 一句话总结
如果把用户消息在 query.ts 里的生命周期压成一句话:
用户消息先进入上下文治理,再参与模型采样;如果触发工具,就把工具结果和附件重新塞回消息流,重建下一轮状态;如果没有工具,也仍要经过恢复和续轮判断,最后才真正结束。