Table of contents
Open Table of contents
三层架构
Tau 的三个包各有一个最值得先抓住的协调中心:
tau_ai Provider adapters
tau_agent run_agent_loop + AgentHarness
tau_coding CodingSession
tau_ai 实现模型供应商的 adapter。OpenAI-compatible、Anthropic、Google 和 Mistral 返回的字段、流式格式、错误信息各不相同,这一层负责吸收这些差异。provider 实现内部会先产生私有的 ProviderEvent,再通过 canonicalize_provider_stream() 转成公共的 AssistantMessageEvent。认证、请求重试和 HTTP 错误也在这一层处理。
tau_agent 是可复用的 agent core。它定义 AgentMessage、AssistantMessageEvent、AgentEvent、ModelProvider 和 AgentTool,并实现 run_agent_loop 与 AgentHarness。这一层知道怎样与模型交互、执行工具和维护内存中的 transcript,但不知道终端界面、skill 目录或者项目配置。
tau_coding 是应用层。核心协调者是 CodingSession。它组合 AgentHarness、coding tools、持久会话、项目资源、extensions 和 context policy,再由 CLI、print renderer 与 Textual TUI 调用。
完整的调用关系可以先记成:
Provider adapter
↓
run_agent_loop
↓
AgentHarness
↓
CodingSession
↓
CLI / TUI / renderer
Tau 明确受 Pi 启发,对齐 Pi 的分层与事件协议,然后在 Python、Pydantic 和 Textual 上重新实现。
消息与事件
阅读这套代码最容易混淆的是 message 和 event。message 表示已经发生、可以保存在 transcript 中的事实,event 则描述这些事实如何产生。
AgentMessage 最常见的三个成员是:
UserMessage
AssistantMessage
ToolResultMessage
AssistantMessage.content 是一组有顺序的 block:
TextContent
ThinkingContent
ToolCall
因此一条回复可以先有 thinking,再输出文字,然后请求工具。最终消息还包含 provider、model、usage、stop reason,以及 provider replay 所需的 metadata。
AssistantMessageEvent 描述一条 assistant message 怎样逐步生成:
start
text_start / text_delta / text_end
thinking_start / thinking_delta / thinking_end
toolcall_start / toolcall_delta / toolcall_end
done / error
流式事件中的 partial 是当前消息的快照。到 done 或 error 时,事件携带的完整 AssistantMessage 才是最终结果。
AgentEvent 又高一层,它描述整个 agent run:
agent_start / agent_end
turn_start / turn_end
message_start / message_update / message_end
tool_execution_start / update / end
run_agent_loop 中的 _assistant_events() 连接这两层事件。provider 的文字增量会被包装成:
MessageUpdateEvent(
message=event.partial,
assistant_message_event=event,
)
因此 Textual TUI、print renderer、JSON renderer 和 extension 只需要消费 AgentEvent,不必认识具体厂商的 stream chunk。
事件流沿着三层向上移动:
provider SDK / HTTP chunks
↓
tau_ai 私有 ProviderEvent
↓ canonicalize_provider_stream()
AssistantMessageEvent
↓ run_agent_loop._assistant_events()
AgentEvent
↓
CodingSession / UI
最终消息则进入另一条路径:
AssistantMessageEvent.done
↓
AssistantMessage
↓
AgentHarness.messages
↓
CodingSession 持久化
简单说,event 描述过程,message 保存结果。
run_agent_loop:执行一次 run
run_agent_loop 是 Tau 最小的执行引擎。它接收 provider、model、system prompt、已有 messages、tools 和本轮 prompts,返回异步的 AgentEvent 流。
它不持有跨调用的长期状态。messages、取消信号以及 steering 和 follow-up 的读取函数都由调用者传入。不过它也不是没有副作用:它会请求 provider、执行工具,并直接扩展传入的 messages。
一次没有工具调用的运行大致产生:
agent_start
turn_start
message_start(user)
message_end(user)
message_start(assistant)
message_update(...)
message_end(assistant)
turn_end
agent_end
如果最终的 AssistantMessage 包含 ToolCall,loop 会查找同名 AgentTool,执行它,再构造 ToolResultMessage:
AssistantMessage(tool call)
↓
tool_execution_start / update / end
↓
ToolResultMessage
↓
再次请求 provider
工具结果必须进入 transcript。模型只有在下一次请求中看到结果,才能继续判断下一步。coding agent 与普通聊天程序的主要差别,就落在这个“调用工具、写回结果、继续请求”的循环上。
源码中有两层循环。去掉错误处理和事件发送后,可以简化为:
while True:
while has_more_tools or pending:
assistant = stream_model_response()
execute_tool_calls(assistant.tool_calls)
pending = drain_steering_messages()
follow_ups = drain_follow_up_messages()
if not follow_ups:
break
pending = follow_ups
内层处理连续的模型与工具调用,也在每个 turn 结束后读取 steering message。外层只在 agent 原本准备退出时读取 follow-up。
源码把它称为 “pure agent loop”。这里的 pure 指它不包含 CLI、Textual、session 路径、skill discovery 或其他 coding 应用策略,并不是说它没有 I/O 或状态变化。
AgentHarness:管理多次 run
直接调用 run_agent_loop 时,调用者需要保存 transcript、创建 cancellation token,并提供 steering 与 follow-up 队列。AgentHarness 把这些状态放进一个可复用对象:
_messages
_running
_current_signal
_steering_queue
_follow_up_queue
_listeners
两者的分工可以概括为:
run_agent_loop 执行一次 agent run
AgentHarness 保存跨 run 状态并控制 run
Harness 规定同一时间只能有一个 loop 在运行。重叠调用 prompt() 或 continue_() 会抛出错误;运行期间的新输入只能进入 steering 或 follow-up 队列。
五个入口可以按生命周期分成三组:
| 操作 | 方法 | 作用 |
|---|---|---|
| 启动 run | prompt() / continue_() | 用新消息启动,或从现有 transcript 继续 |
| 干预 run | steer() / follow_up() | 调整当前工作,或安排完成后的输入 |
| 停止 run | cancel() | 通过 cancellation token 请求停止 |
prompt() 和 continue_()
prompt() 是正常入口。它把字符串包装成 UserMessage,检查当前没有其他 run,修复上次中断留下的不完整 tool call,然后调用 _run():
def prompt(self, content: str) -> AsyncIterator[AgentEvent]:
return self.prompt_message(UserMessage(content=content))
_run() 创建新的 cancellation token,再把 Harness 保存的 provider、model、system prompt、tools、messages 和队列读取函数交给 run_agent_loop。事件既会通知 subscriber,也会继续 yield 给调用者。
continue_() 走同一条路径,但不会添加 UserMessage:
harness.prompt("继续") # transcript 中多一条用户消息
harness.continue_() # 直接从现有 transcript 继续
因此 continue_() 适合恢复已有上下文,也被 CodingSession 用于 context overflow 后的自动重试。
Harness 只维护内存 transcript。它不决定消息应写到哪个 JSONL 文件,也不负责项目级 session 索引。
steer() 和 follow_up()
steer() 与 follow_up() 都不会启动第二个 loop。它们只是把 UserMessage 放入不同队列,区别在于 loop 何时读取。
| 方法 | 注入时机 | 用途 |
|---|---|---|
steer() | 当前 assistant turn 和工具批次结束后 | 调整正在进行的工作 |
follow_up() | agent 原本准备停止时 | 安排当前工作完成后的下一件事 |
如果 provider 正在输出,steer() 不会截断当前 stream。Tau 会等当前回复和对应工具调用结束,再把 steering message 写入 transcript,并开始下一次 provider 请求。
follow_up() 更晚。只有当前 assistant 不再请求工具、steering queue 也已经为空时,loop 才读取它。follow-up 仍属于同一次 run,中间不会再次发出 agent_start。
cancel()
cancel() 只设置当前 cancellation token:
def cancel(self) -> None:
if self._current_signal is not None:
self._current_signal.cancel()
它不会强制终止 asyncio task。provider adapter 和工具需要主动观察 token,所以这是 cooperative cancellation。前端仍要消费 event stream,直到 generator 正常结束。
如果取消发生在 tool call 已生成、tool result 尚未写回之间,Harness 会补上一条错误结果:
Tool call interrupted by user
许多 provider 要求每个 tool call 后都有对应结果。这条修复让 transcript 保持可重放,下一次 prompt() 或 continue_() 才不会违反消息协议。
tau_coding:把 Harness 变成 coding agent
CodingSession 是 tau_coding 的组合中心。它不重新实现 agent loop,而是把 AgentHarness 放进 coding 应用所需的工作目录、工具、项目资源、持久会话、extensions 和 context policy 中。
CodingSession
├── AgentHarness
├── coding tools
├── session storage 与项目索引
├── AGENTS.md / skills / prompt templates
├── extensions
└── context accounting / compaction
CLI、print renderer 和 Textual TUI 都围绕这个对象工作。它们负责交互与呈现,CodingSession 负责一次 coding session 的应用语义。
CodingSession 的调用链
一次 CodingSession.prompt() 依次完成:
- 运行 extension input hooks。
- 展开 skill 或 prompt template。
- 在调用模型前检查是否需要自动 compaction。
- 调用
AgentHarness.prompt_message()并转发AgentEvent。 - 在
MessageEndEvent到达时把新消息写入 session storage。 - context overflow 时压缩旧消息,再调用
continue_()重试。 - 压缩和重试都结束后发出
agent_settled。
agent_end 只表示底层 loop 结束。agent_settled 才表示包括应用级恢复逻辑在内的整个 coding session 已经停止。
内置工具
Tau 默认注册四个 AgentTool,相对路径都以 session 的 cwd 为基准。
| 工具 | 具体实现 |
|---|---|
read | 读取 UTF-8 文本,支持 offset、limit 和截断;图片以 base64 metadata 返回。 |
write | 创建父目录并覆盖文件;write 和 edit 对同一路径共用进程内异步锁。 |
edit | 执行 exact text replacement;先验证唯一性与区间,再一次写回,并保留换行风格和 UTF-8 BOM。 |
bash | 合并 stdout 与 stderr,支持 timeout 和 cancellation;POSIX 下可以终止整个进程组。 |
cwd 只是工作目录,不是 sandbox。工具仍继承 Tau 进程的文件、进程和网络权限。
Session 管理
通用 session primitives 位于 tau_agent.session。这里定义 SessionEntry、SessionStorage、JsonlSessionStorage、树路径查找和 SessionState.from_entries(),它们不依赖 coding UI。
CodingSession 使用这些 primitives 实现应用策略:在消息结束时追加 entry,维护 active leaf,在切换分支或 compaction 后重放状态,并把结果同步回 AgentHarness.messages。
session 正文是 append-only JSONL。消息、model 变化、thinking level、compaction、branch summary 和当前 leaf 都是独立 entry,通过 parent_id 组成树。
tau_coding.SessionManager 维护另一份项目级索引,保存 session id、JSONL 路径、cwd、model、provider 和标题,用于列表与恢复。
因此需要区分三种状态:
| 状态 | 内容 |
|---|---|
| JSONL entries | 完整历史,包括已经离开的分支 |
| active branch | 当前 leaf 对应的 root-to-leaf 路径 |
AgentHarness.messages | 当前准备发送给模型的 transcript |
项目资源
CodingSession.load() 发现项目资源并构造 system prompt。三类 Markdown 资源进入模型的方式不同:
| 资源 | 进入模型的方式 | 用途 |
|---|---|---|
AGENTS.md | 写入 system prompt,每轮生效 | 项目约定与长期指令 |
| skill | 先暴露名称、描述和路径,调用时再展开完整 SKILL.md | 按需加载的工作方法 |
| prompt template | 注册为 slash command,调用时替换参数 | 复用常用 prompt |
Tau 会发现用户级 ~/.tau、~/.agents,也会发现项目内 .tau、.agents,以及从项目根目录到当前 cwd 沿途的 AGENTS.md。同名 skill 或 template 以项目资源优先。
Context accounting 和 compaction
CodingSession 使用近似值估算 context,不调用厂商 tokenizer。默认按四个字符约一个 token 计算,再为 message 和 tool 加固定 overhead。
估算范围包括 system prompt、当前 messages、thinking、tool call 参数,以及工具的 description 和 input schema。结果用于界面展示和自动 compaction 判断。
默认阈值是:
context window - 16,384 tokens
触发后,CodingSession 会:
- 选择较早的消息,并尽量保留最近约 20,000 tokens。
- 使用当前 provider 生成结构化 summary。
- 向 session storage 追加
CompactionEntry,记录 summary 和replaces_entry_ids。 - 重放 session 时,用一条
Previous conversation summary替换被压缩的旧消息。 - 调用
AgentHarness.replace_messages()更新内存 transcript。
旧消息不会从 JSONL 删除。compaction 改变的是模型下一次请求看到的 context,不是 session 的完整历史。
从这条调用链回看,Tau 的分层很清楚:provider adapter 隔离模型差异,run_agent_loop 执行 agent 循环,AgentHarness 管理可复用的运行状态,CodingSession 再把它们组合成完整的 coding agent。