Pi Agent 运行时

一句话概括

嵌入式 Pi Agent 运行时是编排每次 LLM 调用的执行引擎 — 它组装 prompt、管理认证 profile、处理重试、触发压缩,并追踪 token 用量。

职责

  • 执行 LLM 调用循环:构建 system prompt → 发送给模型 → 处理响应 → 处理工具调用 → 重复
  • 管理多 profile 认证,在失败时自动轮换(速率限制、认证错误、账单问题)
  • 检测并通过自动压缩和工具结果截断来恢复上下文溢出
  • 跨重试追踪 token 用量,确保上下文大小报告准确(避免累积缓存膨胀)
  • 协调 thinking level 解析和模型不支持时的降级回退

架构图

关键源码文件

文件角色
src/agents/pi-embedded-runner/run.ts (1159 行)主编排器runEmbeddedPiAgent() 入口、重试循环、上下文溢出恢复、认证 profile 轮换、用量累积
src/agents/pi-embedded-runner/run/attempt.ts (~1400 行)单次尝试runEmbeddedAttempt() — 会话初始化、工具设置、system prompt 构建、Pi SDK 调用、hook 执行、压缩处理
src/agents/pi-embedded-runner/system-prompt.ts薄包装层,委托给 buildAgentSystemPrompt()
src/agents/system-prompt.ts (688 行)System prompt 构建:20+ 条件章节、PromptMode 支持、上下文文件注入
src/agents/compaction.ts会话历史压缩:分块、总结、降级策略
src/agents/tool-policy-pipeline.ts (108 行)工具过滤:7 步策略管道(profile → provider → global → agent → group)
src/agents/pi-embedded-runner/compact.tscompactEmbeddedPiSessionDirect() — 溢出触发的显式压缩
src/agents/pi-embedded-runner/tool-result-truncation.ts最后手段:截断会话历史中的超大工具结果
src/agents/pi-embedded-subscribe.ts事件订阅:累积用量、追踪工具元数据、消息工具结果
src/agents/defaults.ts常量:DEFAULT_CONTEXT_TOKENS = 200_000DEFAULT_MODEL = "claude-opus-4-6"
src/agents/pi-settings.ts压缩设置:DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR = 20_000
src/agents/usage.ts跨 provider 的 token 用量归一化
src/agents/session-transcript-repair.ts转录修复:孤儿工具结果、工具调用输入验证、stripToolResultDetails()

数据流

入站

Auto-Reply Pipeline (src/auto-reply/get-reply-run.ts)

  ├── prompt: string(用户消息,已清理 Anthropic 魔术字符串)
  ├── images: any[](如果是视觉模型)
  ├── sessionFile: string(.jsonl 转录文件路径)
  ├── config: OpenClawConfig
  ├── skillsSnapshot: 已加载的 skills 目录
  ├── thinkLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"
  ├── provider + modelId(由模型选择解析)
  └── abortSignal, timeoutMs, 流式回调

内部流程(每次 LLM 调用)

1. 解析工作空间目录(agent 专属或共享)
2. 解析认证 profile(从 profile 存储,带冷却检查)
3. 验证上下文窗口(硬最小值:CONTEXT_WINDOW_HARD_MIN_TOKENS)
4. 构建 system prompt(buildEmbeddedSystemPrompt → buildAgentSystemPrompt)
5. 过滤工具(applyToolPolicyPipeline:7 步级联)
6. 从 JSONL 文件加载会话历史
7. 发送给 Pi Agent SDK(agent.setSystemPrompt + session.run)
8. SDK 在循环中执行工具调用直到 assistant 停止
9. 订阅事件 → 累积用量(UsageAccumulator)
10. 根据结果分类返回或重试

出站

EmbeddedPiRunResult

  ├── payloads: Array<{ text, isError?, channel?, ... }>
  ├── meta:
  │     ├── durationMs
  │     ├── agentMeta: { sessionId, provider, model }
  │     ├── systemPromptReport(token 计数)
  │     └── error?: { kind, message }
  ├── usage?: { input, output, cacheRead, cacheWrite, total }
  ├── messagingToolResults?: any[]
  └── autoCompactionCount: number

Token 关键机制

1. 用量累积策略(run.ts:83-179)

UsageAccumulator 跨重试追踪 token,有一个关键细节:

问题:每次工具调用往返报告 cacheRead ≈ 当前上下文大小。
      累加 N 次往返得到 N × 上下文大小(膨胀了)。

解决方案:将"最后一次"缓存字段与累积总量分开追踪。
          - output:累积(总生成文本)
          - input/cacheRead/cacheWrite:仅来自最后一次 API 调用
          - total:lastPromptTokens + 累积 output

这在 GitHub issue #13698 中有记录。没有此修复,一个 5 次工具调用的轮次会报告 ~1M tokens 而非 ~200k。

2. 上下文溢出恢复(run.ts:666-846)

三层恢复策略:

第一层:自动压缩(最多 3 次尝试)
  ├── 如果 SDK 本次尝试已压缩 → 直接重试
  └── 如果未压缩 → 调用 compactEmbeddedPiSessionDirect() → 重试

第二层:工具结果截断(仅一次)
  ├── 检查 sessionLikelyHasOversizedToolResults()
  └── 如果发现 → truncateOversizedToolResultsInSession() → 重试

第三层:放弃
  └── 返回错误:"Context overflow: prompt too large for the model"

关键约束:overflowCompactionAttempts 跨层永不重置(防止无界压缩循环,参考:OC-65)。

3. 压缩内部实现(compaction.ts)

常量:
  BASE_CHUNK_RATIO = 0.4        (默认块 = 上下文的 40%)
  MIN_CHUNK_RATIO = 0.15        (最小块 = 15%)
  SAFETY_MARGIN = 1.2           (20% 缓冲,因为 estimateTokens() 不精确)
  SUMMARIZATION_OVERHEAD_TOKENS = 4096

压缩算法:
  1. 剥离 toolResult.details(安全:不可信/冗长的 payload)
  2. 按 token 份额分割消息(默认 2 部分)
  3. 对每个块:通过 LLM 生成摘要(重试 3 次,reasoning: "high")
  4. 如果多个摘要:用"合并这些部分摘要"指令合并
  5. 降级:如果完整总结失败,排除超大消息(>50% 上下文)
  6. 最终降级:"上下文包含 N 条消息,摘要因大小限制不可用"

自适应块比率:
  如果平均消息 > 上下文窗口的 10% → 减小块比率
  减量 = min(avgRatio × 2, BASE - MIN)

4. 压缩设置(pi-settings.ts)

DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR = 20,000

解析顺序:
  1. agents.defaults.compaction.reserveTokens(来自配置)
  2. agents.defaults.compaction.reserveTokensFloor(底线保证)
  3. DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR(20,000)

触发条件:history_tokens > contextWindow - reserveTokens
  200k 上下文 + 20k 预留 → 压缩在 ~180k 历史时触发

5. 工具策略管道(tool-policy-pipeline.ts)

每次 LLM 调用前过滤可用工具的 7 步级联:

步骤 1:tools.profile(per-profile 白名单)
步骤 2:tools.byProvider.profile(provider 特定 profile)
步骤 3:tools.allow(全局白名单)
步骤 4:tools.byProvider.allow(provider 特定全局)
步骤 5:agents.<id>.tools.allow(agent 特定)
步骤 6:agents.<id>.tools.byProvider.allow(agent + provider)
步骤 7:group tools.allow(群组级别)

每步:
  - 剥离仅插件白名单(对未知条目发出警告)
  - 展开插件工具组
  - 应用 filterToolsByPolicy()

更少工具 = 更少 JSON schema 在 API 调用中 = 更低的每次调用固定 token 成本。

6. System prompt 构建(system-prompt.ts)

三种模式控制 prompt 大小:

模式包含章节预估 tokens
"full"全部 20+ 章节~3,000-5,000
"minimal"Tooling、Safety、Skills、Workspace、Runtime + 子集~1,500
"none"单行身份声明~15

"full" 模式中 token 较重的章节:

  • Skills 目录(如果加载了 skills):~1,000-5,000 tokens
  • 上下文文件(SOUL.md、RULES.md):0-5,000+ tokens,无截断
  • Messaging 章节:~200-500 tokens(详细路由 + message 工具说明)
  • 工具摘要:~500-800 tokens(24 个核心工具 × 约 30 tokens)

与其他模块的关系

  • 依赖

    • auto-reply/ — 调用 runEmbeddedPiAgent() 作为执行引擎
    • config/ — 读取 OpenClawConfig 获取所有设置
    • skills/ — 接收 skills 快照用于 prompt 注入
    • memory/ — 通过 Pi SDK hooks 注入记忆上下文
    • @mariozechner/pi-agent-core — 实际的 LLM 交互 SDK
    • @mariozechner/pi-coding-agentestimateTokens()generateSummary()
  • 被依赖

    • auto-reply/get-reply-run.ts — 主要调用方
    • cron/service/timer.ts — 心跳调用同一运行时
    • 任何子 agent 生成都经过此运行时

重试逻辑汇总

错误类型恢复动作最大尝试次数
上下文溢出自动压缩 → 工具截断 → 放弃3 次压缩 + 1 次截断
认证失败轮换认证 profile所有 profile 耗尽
速率限制轮换认证 profile所有 profile 耗尽
账单错误格式化错误消息,轮换或失败所有 profile 耗尽
Thinking 不支持降级 thinking level所有更低级别已尝试
超时轮换认证 profile(不冷却)所有 profile 耗尽
角色排序返回用户友好错误不重试
图片过大返回用户友好错误不重试
所有重试耗尽返回"请求失败"错误MAX_RUN_LOOP_ITERATIONS(24-160)

Token 优化影响

此模块是 token 消耗的中央控制点

机制Token 影响在此可控?
System prompt 大小2,000-5,000/次(固定)是 — PromptMode 选择
工具定义2,000-4,000/次(固定)是 — 工具策略管道
会话历史40,000-150,000/次(增长)是 — 压缩阈值
上下文溢出恢复防止浪费满上下文调用是 — 重试逻辑
用量报告准确性防止膨胀报告是 — UsageAccumulator

我的认知盲区

  • runEmbeddedAttempt() 中 Pi SDK 会话生命周期的确切流程 — session.run() 内部在 SDK 中,不在 OpenClaw 源码中
  • compact.tscompactEmbeddedPiSessionDirect()compaction.ts 函数的区别 — 需要阅读桥接代码
  • 插件 hooks(before_agent_startbefore_model_resolve)— 它们能多大程度修改运行时行为和 token 预算
  • streamParams 以及流式传输如何影响 token 计数
  • Pi SDK 的 estimateTokens() 是否使用 tiktoken 还是 chars/4 启发式 — 精度影响压缩触发时机
  • tool-result-truncation.ts — 精确的截断策略和大小阈值

相关贡献

  • 暂无

变更频率

  • run.ts:高 — 错误处理和重试逻辑随着新边缘情况的发现而频繁变更(如 OC-65 压缩循环、#13698 用量膨胀)
  • system-prompt.ts:中 — 随着功能发布添加新章节(reactions、sandbox、model aliases)
  • compaction.ts:中 — 压缩策略随模型和上下文窗口变化而演进
  • tool-policy-pipeline.ts:低 — 7 步级联是稳定的;变更通常在策略定义中,而非管道本身
  • defaults.ts:低 — 常量很少变更(200k 上下文与 Claude 模型限制挂钩)