Harness 设计

Pi 有两个相关概念:低层 Agent/agentLoop,以及更高层的 harness/session runtime。packages/agent/src/harness/agent-harness.ts 提供了通用 harness;packages/coding-agent/src/core/agent-session.ts 则是 coding-agent 产品层的 harness 实现。

本章涉及的具体类型、hook 与 save point 行号见源码索引

为什么需要 Harness

低层 loop 只会执行消息和工具,但真实产品还需要:

  • 加载系统提示、skills、prompt templates、项目说明。
  • 选择模型、thinking level、transport、请求 header。
  • 保存消息、模型切换、工具切换、session name 等状态。
  • 提供扩展 hook。
  • 管理 steering/follow-up/nextTurn 队列。
  • 在 provider 请求前后插入逻辑。
  • 做 compaction、branch summary、retry。

Harness 就是这些“运行环境语义”的承载层。

AgentHarness 的关键设计

源码:packages/agent/src/harness/agent-harness.ts

AgentHarness 维护一份 turn state:

interface AgentHarnessTurnState {
  messages: AgentMessage[];
  resources: AgentHarnessResources;
  streamOptions: AgentHarnessStreamOptions;
  sessionId: string;
  systemPrompt: string;
  model: Model<any>;
  thinkingLevel: ThinkingLevel;
  tools: TTool[];
  activeTools: TTool[];
}

每个 turn 开始时都会创建快照。好处是:

  • 扩展或应用可以在 turn 间修改模型、工具、thinking level。
  • 当前 provider 请求使用稳定配置,避免运行中被外部突变影响。
  • prepareNextTurn 后重新读取 session context 和配置,支持工具调用后状态变化。

Hook 边界

AgentHarness.createLoopConfig() 把 harness hook 映射到底层 loop:

Loop hook Harness 事件 用途
transformContext context 修改即将发给模型的消息
beforeToolCall tool_call 阻止或审计工具调用
afterToolCall tool_result 改写工具结果
prepareNextTurn flush session writes + create turn state 工具结果后刷新上下文/模型
getSteeringMessages steer queue 工作中插入消息
getFollowUpMessages follow-up queue 工作结束后续问

这个映射让低层 loop 保持通用,又允许上层接入复杂策略。

Session 写入策略

AgentHarness 有一个 pendingSessionWrites 队列。它不会让扩展随意直接改低层 context,而是把写操作排队,在 save point 时 flush:

  • message
  • model_change
  • thinking_level_change
  • active_tools_change
  • custom
  • custom_message
  • label
  • session_info
  • leaf

保存点主要在 turn_endagent_end。这让 session 的持久化顺序和 agent 事件顺序一致,也避免 hook 中途写入破坏当前 turn 的上下文。

Provider 请求扩展点

createStreamFn() 在 provider 请求前会:

  1. getApiKeyAndHeaders 获取认证信息。
  2. 合并 stream options 与 auth headers。
  3. 触发 before_provider_request,允许 patch headers、metadata、timeout、transport、cache retention。
  4. 调用 streamSimple
  5. 在 response 后发出 after_provider_response

这让诸如自定义 provider header、审计日志、代理、缓存 affinity、实验参数等能力可以在 harness 层完成,而不用侵入 provider 实现。

Coding Agent 的 Harness 化

AgentSession 是面向 coding agent 的具体 harness。它比通用 AgentHarness 更贴近产品:

  • prompt 前处理扩展命令、input hook、skill/template 展开。
  • 管理内置工具和扩展工具。
  • message_end 写入 SessionManager
  • AgentEvent 翻译成 extension event。
  • 自动重试和上下文压缩。
  • 提供 sendCustomMessage()sendUserMessage()steer()followUp() 等 API。

换句话说,AgentSessionagent-core 的通用能力绑定到 Pi 的 CLI/SDK/RPC 语义上。

错误处理

通用 harness 会把不同子系统错误归类成稳定 code:

  • session
  • compaction
  • branch_summary
  • hook
  • auth
  • busy
  • invalid_state
  • invalid_argument

这比直接抛原始异常更适合 SDK/RPC,因为调用方可以按 code 做 UI 提示或恢复策略。

设计启发

  • Harness 是 agent runtime 与产品体验之间的隔离层。
  • 每个 turn 使用配置快照,能避免异步突变造成难以复现的问题。
  • Hook 应该有明确的合并规则、短路规则和错误策略。
  • 会话写入最好经过统一队列和 save point,而不是在任意 hook 中直接落盘。