数据截至 (上游 commit 6fb43a175495)
聊天缓冲区主循环
这一章讲整个插件的骨架:用户按回车后,一条消息怎么变成 HTTP 请求、LLM 的流式回话怎么实时写回屏幕、以及工具调用怎么让对话「自己转下去」。
1. 核心直觉:对话是一个 buffer,但还有一份「影子消息表」
CodeCompanion 把对话渲染成一个可编辑的 Markdown buffer(你看到的 ## Me / ## CodeCompanion)。但发给 LLM 的不是 buffer 文本本身,而是一份结构化的影子消息表 self.messages(角色 + 内容 + 元数据)。
为什么要两份?因为:
- buffer 是给人看的——可编辑、可滚动、带语法高亮。
self.messages是给LLM 看的——带role、隐藏的系统提示、工具调用的tool_calls、token 估算等 buffer 里看不见的字段。
submit 时,插件会重新解析 buffer 里用户那段新文本(parser.messages,见 chat/init.lua:1326),把它并进 self.messages,再发出去。两者通过这次解析对齐。
2. 主线:submit → 流式 → done
按回车
│
▼
Chat:submit ──┬─ 解析 buffer 新文本 → 并入 self.messages
├─ 拼 payload { messages, tools(schemas) }
└─ 按 adapter.type 分流
│
┌───────────┴───────────┐
▼ ▼
_submit_http _submit_acp
(HTTP 流式) (子进程 JSON-RPC)
│
▼ 每个 chunk
process_chunk ── adapter.parse_chat ──▶ 实时 add_buf_message 写回屏幕
│ (文本) 或攒进 tools(工具调用)
▼ 流结束
Chat:done ──┬─ 有文本 → 落成一条 assistant 消息
├─ 有工具 → Tools:execute(见 02 章),跑完自动 submit 下一轮
└─ 都没有 → ready_for_input,等用户
2.1 submit:把 buffer 拼成请求
Chat:submit(chat/init.lua:1305)先用一个守卫挡住并发请求(if self.current_request then return),然后区分两种触发:
- 用户手动提交:解析 buffer 里
header_line之后的新文本,做上下文展开(check_context)、图片检查、replace_user_inputs等,再把用户消息add_message进历史。 - 自动提交(
opts.auto_submit,工具跑完后触发):跳过解析,只注入排队的 btw 消息。
关键是 payload 的拼装(chat/init.lua:1393):
local payload = {
messages = self.adapter:map_roles(shallow_messages),
session_id = self.session_id,
tools = (not vim.tbl_isempty(self.tool_registry.schemas)
and { self.tool_registry.schemas } or {}),
}
注意 map_roles——同一份消息,不同适配器对「assistant」「user」的叫法不同,适配器在这里把内部角色名翻译成各家的叫法。工具 schemas 来自 tool_registry(见 02 章),只有挂了工具才非空。
一个易错点:提交前会浅拷贝每条消息(
chat/init.lua:1384),这样map_roles改role字段时不会污染self.messages原表。注释明确点了这个原因。
2.2 process_chunk:流式回话怎么实时上屏
_submit_http(chat/init.lua:1203)里定义了 process_chunk,它是流式的心脏。每收到一个 chunk:
- 先让适配器抽 token 数(
parse_tokens)。 - 调
adapter.parse_chat(chat/init.lua:1224)把这家 LLM 的原始 chunk 解析成统一结构{ status, output = { role, content, reasoning, ... } }。 - 如果
output.content有内容,立刻add_buf_message写进 buffer——这就是你看到文字一个个蹦出来的原因(chat/init.lua:1257)。 reasoning(思考过程)、meta(如 compaction 压缩信号)分别攒到各自的累加器里。
这段演示「流式累加」的核心想法(# 示意,非源码):
-- 累加器:文本和工具调用分开攒
local output, reasoning, tools = {}, {}, {}
local function on_chunk(raw)
local result = adapter.parse_chat(raw, tools) -- 各家格式 → 统一结构
if result.output.content then
table.insert(output, result.output.content) -- 攒最终文本
add_buf_message(result.output.content) -- 同时实时上屏
end
-- 工具调用由 parse_chat 直接塞进 tools 表,不在这里处理
end
注意 tools 是传进 parse_chat 的引用——适配器把流式拼接的工具调用片段直接攒进这张表,等流结束一次性交给 done。
2.3 done:收尾与「自动转下一圈」
Chat:done(chat/init.lua:1441)是回合的终点,做三件事,顺序很重要:
- 先落文本消息:把累加的
output拼成一条 assistant 消息进历史。 - 再处理工具(
chat/init.lua:1510):如果has_tools,先把工具调用作为一条隐藏的 assistant 消息存好,然后return self.tools:execute(self, tools)——把控制权交给工具系统。 - 否则就绪:没工具、没排队消息,就
checkpoint存档,准 备接收用户下一句。
「自动转下一圈」是 agent 能多轮自主工作的关键:工具跑完后,工具系统会触发一次 auto_submit(见 tools/init.lua:236 的 CodeCompanionToolsFinished 处理),把工具输出作为新消息再发给 LLM,LLM 看到结果后决定下一步——形成「LLM 调工具 → 看结果 → 再调工具」的循环,直到它不再调工具为止。
3. 巧妙之处
「stopped 后不标记 sent」防丢消息。 label_sent_items(chat/init.lua:1446)给已发送的用户消息打 sent 标记,避免对「替我们存状态的适配器」重复发送。但如果用户中途按 stop(opts.status == "stopped"),代码故意不打这个标记(chat/init.lua:1505)——因为无法确定 LLM 到底收到了多少,留着以便重发。这是一个很细的正确性考量。
孤儿工具调用补全。 如果用户在工具调用还没返回结果时就停掉请求,消息历史里会留下「有 tool_call 却没有对应 tool_result」的孤儿,多数 LLM 的 API 会因此报错。_complete_orphaned_tool_calls(chat/init.lua:1096,在 done 的 stopped 分支调用)会补一个占位结果,保证历史合法。
4. 边界与局限
- 单飞请求:同一个聊天 buffer 同时只能有一个在途请求(
current_request守卫,chat/init.lua:1306)。 - buffer 被关:done 回来时若 buffer 已失效会直接返回(
chat/init.lua:1446),自动化测试里常见。 - 这个文件超过 2000 行,是全库最大的单文件——
Chat类承担了 buffer 管理、上下文、提交、工具桥接等过多职责。
5. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 提交入口 | lua/codecompanion/interactions/chat/init.lua | Chat:submit |
| HTTP 流式 | lua/codecompanion/interactions/chat/init.lua | Chat:_submit_http、process_chunk(局部) |
| 回合收尾 | lua/codecompanion/interactions/chat/init.lua | Chat:done、Chat:tools_done |
| 工具输出回写 | lua/codecompanion/interactions/chat/init.lua | Chat:add_tool_output |
| 孤儿补全 | lua/codecompanion/interactions/chat/init.lua | Chat:_complete_orphaned_tool_calls |
| 底层发请求 | lua/codecompanion/http.lua | Client:send、Client:request |