跳到正文
Wang Jianhua
返回

Tau:Python 版的 Pi Agent

更新于:

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 队列。

五个入口可以按生命周期分成三组:

操作方法作用
启动 runprompt() / continue_()用新消息启动,或从现有 transcript 继续
干预 runsteer() / follow_up()调整当前工作,或安排完成后的输入
停止 runcancel()通过 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() 依次完成:

  1. 运行 extension input hooks。
  2. 展开 skill 或 prompt template。
  3. 在调用模型前检查是否需要自动 compaction。
  4. 调用 AgentHarness.prompt_message() 并转发 AgentEvent。
  5. 在 MessageEndEvent 到达时把新消息写入 session storage。
  6. context overflow 时压缩旧消息,再调用 continue_() 重试。
  7. 压缩和重试都结束后发出 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 会:

  1. 选择较早的消息,并尽量保留最近约 20,000 tokens。
  2. 使用当前 provider 生成结构化 summary。
  3. 向 session storage 追加 CompactionEntry,记录 summary 和 replaces_entry_ids。
  4. 重放 session 时,用一条 Previous conversation summary 替换被压缩的旧消息。
  5. 调用 AgentHarness.replace_messages() 更新内存 transcript。

旧消息不会从 JSONL 删除。compaction 改变的是模型下一次请求看到的 context,不是 session 的完整历史。

从这条调用链回看,Tau 的分层很清楚:provider adapter 隔离模型差异,run_agent_loop 执行 agent 循环,AgentHarness 管理可复用的运行状态,CodingSession 再把它们组合成完整的 coding agent。

参考资料



下一篇
Agent 系统实现:Loop、工具、上下文与多 Agent