事件与输出

各运行时的输出各不相同,agentrun 把它们转成同一种带版本号的事件流,并统一判定成败。本页写事件的字段、subagent 与用量的规则、end.status 的判定,以及 jsonl、text、rich 三种输出格式。各运行时的原始输出怎么映射到这些事件,见运行。

代码位置

文件主要类型与函数本页对应的章节
src/output/event.rsEvent、Body、Start、SandboxKind、NetworkInfo、Prompt、ToolStart、Tool、Text、UsageReport、SubagentStart、SubagentStatus、SubagentEnd、Network、NetworkReason、End(early)、EndStatus、Signal统一事件流、end.status
src/output/usage.rsTokenCounts(context_tokens、total、summary)、Usage(sum)、tokens、cost用量与费用、text 格式里的用量与费用写法
src/output/aggregate.rsAggregator(begin、push、finish、main_calls、tokens)、Record、Translatedtool_start 与 tool 事件的时机、subagent 与 parent、用量合计、未结束调用的补发
src/output/mod.rsOutput(select、add_note)、write_endtext 格式、rich 格式
src/output/text.rsTextFormatter(lines、add_note、subagent_label、subagent_model)text 格式、[note] 行
src/output/rich.rsRich(add_note)、REFRESH_PERIOD、terminal_sizerich 格式

适配器输出的是 output::Record,Aggregator 把它们转成本页的事件:补 parent、给 subagent 编号、合计用量、在 end 之前补发未结束的调用与 subagent。其中 Record::CallStart(一次模型调用开始)与 Record::CallUsage(还没输出 usage 的调用到当时为止的用量)带 parent,不输出事件,运行框架每处理完一行原始输出,用 main_calls 与 tokens 检查轮数与 token 上限。三种输出格式都从同一个事件序列生成。理由:事件是唯一的事实来源,text 与 rich 只做展示。

统一事件流

--format jsonl 时,标准输出每行是一个 JSON 对象,每行写完立即 flush。第一个事件一定是 start,最后一个一定是 end;启动前就失败时只有一个 end。增加字段或增加事件类型时 schema 不变,调用方忽略不认识的 type;删除或改变已有字段的含义时加 1。每个事件的字段按下面类型定义里的顺序输出。

type Event =
  | StartEvent | PromptEvent | ToolStartEvent | ToolEvent | TextEvent
  | UsageEvent | SubagentStartEvent | SubagentEndEvent | NetworkEvent | EndEvent

interface EventBase {
  schema: 1                  // 事件格式的版本号
  type: string
  time: string               // RFC 3339,UTC,精确到毫秒
}

// 运行时进程启动后输出一次
interface StartEvent extends EventBase {
  type: "start"
  runtime: "claude-code" | "codex" | "pi" | "dsh"
  sandbox: "seatbelt" | "bubblewrap" | "codex" | "none"
  network: {
    mode: "none" | "full" | "custom"
    allow: string[]          // --allow-host 的值
    enforced: boolean        // 网络限制是否由沙箱执行
  }
  deny_read: string[]        // agent 不能读的路径:默认列表在前,--deny-read 在后
  model: string | null       // --model 的值,未给时为 null
  cwd: string                // 真实的绝对路径,符号链接已展开
  argv: string[]             // 实际执行的完整命令,不含提示词
  env: string[]              // --env 与 --env-file 设置的变量名,不含值
}

// start 之后输出一次,内容是传给运行时的提示词
interface PromptEvent extends EventBase {
  type: "prompt"
  text: string
  images: string[]           // --image 给出的路径,按给出的顺序;没有图片时为 []
}

// 一次工具调用开始时输出
interface ToolStartEvent extends EventBase {
  type: "tool_start"
  id: string                 // 与这次调用的 tool 事件的 id 相同
  parent: string | null      // 与这次调用的 tool 事件的 parent 相同
  name: string
  summary: string            // 与这次调用的 tool 事件的 summary 相同
}

// 一次工具调用结束时输出
interface ToolEvent extends EventBase {
  type: "tool"
  id: string                 // 运行时给出的工具调用 id
  parent: string | null      // 主 agent 为 null,subagent 内为 subagent_start.id
  name: string               // 运行时自己的工具名
  summary: string            // 一行,最长 120 个字符,超出时截断并加省略号
  denied: boolean            // 这次调用被运行时的权限机制拒绝
  error: boolean | null      // 这次调用执行出错;被拒绝时为 false,运行时没有报告时为 null
}

// agent 输出一段完整的文字时输出
interface TextEvent extends EventBase {
  type: "text"
  parent: string | null
  text: string
}

// 一次模型调用结束时输出;运行时没报告的字段为 null
interface UsageEvent extends EventBase {
  type: "usage"
  parent: string | null
  model: string | null
  input_tokens: number | null        // 不含缓存命中的部分
  output_tokens: number | null
  cache_read_tokens: number | null
  cache_write_tokens: number | null
  cost_usd: number | null            // 这次调用的费用,单位美元
  context_tokens: number | null      // input_tokens、cache_read_tokens、cache_write_tokens 之和,即送进模型的全部上下文;三项有一项为 null 时为 null
}

// agent 启动一个 subagent 时输出
interface SubagentStartEvent extends EventBase {
  type: "subagent_start"
  id: string                 // 启动它的工具调用 id
  parent: string | null
  number: number             // agentrun 按启动顺序给的编号,从 1 开始
  kind: string               // subagent 的类型,例如 "Explore"
  model: string | null       // 启动时给出的模型
  description: string        // 一行任务描述
}

// subagent 结束时输出
interface SubagentEndEvent extends EventBase {
  type: "subagent_end"
  id: string
  status: "finished" | "failed"
  duration_ms: number
  usage: Usage               // 这个 subagent 及其内部 subagent 的用量
}

// 经过过滤代理的连接,每组 host、port、allowed 第一次出现时输出
interface NetworkEvent extends EventBase {
  type: "network"
  host: string
  port: number
  allowed: boolean
  reason: "not_allowed" | "private_address" | null   // 放行时为 null
}

// 运行结束时输出
interface EndEvent extends EventBase {
  type: "end"
  status: "finished" | "failed" | "interrupted" | "timeout" | "limit" | "rejected"
  exit_code: number | null   // 运行时的退出码,未启动或被信号终止时为 null
  detail: string
  duration_ms: number        // 从 agentrun 开始解析命令行到输出 end
  usage: Usage
  result: string | null      // 主 agent 最后一段文字
  session_id: string | null  // 这次运行保存进 --session-dir 的会话 id,没有保存时为 null
}

interface TokenCounts {
  input_tokens: number | null
  output_tokens: number | null
  cache_read_tokens: number | null
  cache_write_tokens: number | null
  cost_usd: number | null     // 单位美元
}

interface Usage extends TokenCounts {
  by_model: Record<string, TokenCounts>   // 模型未知时键为 "unknown"
}

下面是一次运行的事件流,为了阅读把每行格式化成了多行,实际输出中每个事件占一行。

{
  "schema": 1,
  "type": "start",
  "time": "2026-10-04T13:00:00.000Z",
  "runtime": "claude-code",
  "sandbox": "bubblewrap",
  "network": {
    "mode": "none",
    "allow": [],
    "enforced": true
  },
  "deny_read": [
    "/home/me/.ssh",
    "/home/me/.aws",
    "..."
  ],
  "model": null,
  "cwd": "/work/repo",
  "argv": [
    "/usr/local/bin/claude",
    "-p",
    "--output-format",
    "stream-json",
    "..."
  ],
  "env": [
    "GH_TOKEN"
  ]
}
{
  "schema": 1,
  "type": "prompt",
  "time": "2026-10-04T13:00:00.850Z",
  "text": "给 parse 模块补测试并修复失败",
  "images": []
}
{
  "schema": 1,
  "type": "subagent_start",
  "time": "2026-10-04T13:00:03.120Z",
  "id": "toolu_01",
  "parent": null,
  "number": 1,
  "kind": "Explore",
  "model": "haiku",
  "description": "查找所有调用 parse_line 的位置"
}
{
  "schema": 1,
  "type": "tool_start",
  "time": "2026-10-04T13:00:03.900Z",
  "id": "toolu_02",
  "parent": "toolu_01",
  "name": "Grep",
  "summary": "Grep: parse_line"
}
{
  "schema": 1,
  "type": "tool",
  "time": "2026-10-04T13:00:04.410Z",
  "id": "toolu_02",
  "parent": "toolu_01",
  "name": "Grep",
  "summary": "Grep: parse_line",
  "denied": false,
  "error": false
}
{
  "schema": 1,
  "type": "usage",
  "time": "2026-10-04T13:00:05.002Z",
  "parent": "toolu_01",
  "model": "claude-haiku-4-5",
  "input_tokens": 3120,
  "output_tokens": 85,
  "cache_read_tokens": 9800,
  "cache_write_tokens": 1200,
  "cost_usd": null,
  "context_tokens": 14120
}
{
  "schema": 1,
  "type": "subagent_end",
  "time": "2026-10-04T13:00:15.530Z",
  "id": "toolu_01",
  "status": "finished",
  "duration_ms": 12400,
  "usage": {
    "input_tokens": 3120,
    "output_tokens": 85,
    "cache_read_tokens": 9800,
    "cache_write_tokens": 1200,
    "cost_usd": null,
    "by_model": {
      "claude-haiku-4-5": {
        "input_tokens": 3120,
        "output_tokens": 85,
        "cache_read_tokens": 9800,
        "cache_write_tokens": 1200,
        "cost_usd": null
      }
    }
  }
}
{
  "schema": 1,
  "type": "tool_start",
  "time": "2026-10-04T13:00:15.900Z",
  "id": "toolu_03",
  "parent": null,
  "name": "Bash",
  "summary": "Bash: cargo test"
}
{
  "schema": 1,
  "type": "tool",
  "time": "2026-10-04T13:00:16.880Z",
  "id": "toolu_03",
  "parent": null,
  "name": "Bash",
  "summary": "Bash: cargo test",
  "denied": false,
  "error": false
}
{
  "schema": 1,
  "type": "text",
  "time": "2026-10-04T13:00:31.100Z",
  "parent": null,
  "text": "测试全部通过。"
}
{
  "schema": 1,
  "type": "end",
  "time": "2026-10-04T13:00:31.250Z",
  "status": "finished",
  "exit_code": 0,
  "detail": "",
  "duration_ms": 31250,
  "usage": {
    "input_tokens": 18234,
    "output_tokens": 912,
    "cache_read_tokens": 42000,
    "cache_write_tokens": 5100,
    "cost_usd": 0.0436916,
    "by_model": {
      "claude-sonnet-5-5": {
        "input_tokens": 15114,
        "output_tokens": 827,
        "cache_read_tokens": 32200,
        "cache_write_tokens": 3900,
        "cost_usd": 0.0384916
      },
      "claude-haiku-4-5": {
        "input_tokens": 3120,
        "output_tokens": 85,
        "cache_read_tokens": 9800,
        "cache_write_tokens": 1200,
        "cost_usd": 0.0052
      }
    }
  },
  "result": "测试全部通过。",
  "session_id": null
}

提示词

prompt 让事件流本身记录这次运行的输入,读事件流的人不需要另外保存提示词。它取自运行时在输出里回显的提示词:claude-code 用 --replay-user-messages 回显,pi 的输出里本来就有用户消息。codex 与 dsh 不回显(codex 0.159.2 与 dsh 0.2.1-rc.2 实测),agentrun 在 start 之后用自己传入的提示词输出。运行时在回显之前就失败时,事件流里没有 prompt。images 列出调用方用 --image 给出的路径,不转成绝对路径,路径里不是 UTF-8 的字节换成 U+FFFD;不写图片内容,也不写 agentrun 在会话临时目录里写的副本路径,见图片。

运行时输出的非 JSON 行和未识别的事件不进入统一事件流,只写入 --raw 文件,不报错。运行时的标准错误原样转到 agentrun 的标准错误。

tool 事件在调用结束时输出

tool 在收到这次调用的结果时输出,因为调用是否被拒绝、是否出错要在调用之后才知道。运行时结束时还没有结果的调用,在 end 之前补发。被拒绝的调用不改变 end.status:运行时会把拒绝结果交还模型,由模型决定下一步,是否算失败由调用方根据 denied 判断。不提供「被拒绝即停止」的选项。

调用开始时先输出 tool_start,调用方据此显示正在执行的工具。同一次调用的 tool_start 与 tool 的 id、parent、name、summary 相同,parent 在调用开始时按已经启动的 subagent 确定,结束时沿用。每个 tool_start 之后都有一个同 id 的 tool,包括在 end 之前补发的。运行时只在调用结束时才报告这次调用的,tool_start 与 tool 一起输出。

denied 只表示运行时权限机制的拒绝,例如 claude-code 的 auto 模式分类器拒绝一条命令。被沙箱拦下的命令表现为命令出错,denied 为 false,error 为 true。pi 没有权限机制,dsh 在 agentrun 下关闭了它自己的审批(见dsh 一节),两者的 denied 恒为 false。

error 表示这次调用执行了但出错,例如命令的退出码不为 0、要读的文件不存在。取值来自运行时自己的报告,每个运行时用哪个字段见运行页各运行时的输出映射。被拒绝的调用没有执行,error 为 false。理由:claude-code 拒绝一次调用时,交还模型的结果也标为出错,只看 error 会把拒绝算成执行出错。运行时没有报告时 error 为 null,在 end 之前补发的 tool 也为 null。出错的调用同样不改变 end.status,理由与被拒绝的调用相同。

subagent 与 parent

主 agent 的事件 parent 为 null。subagent 内部的 tool_start、tool、text、usage、subagent_start 事件,parent 是启动它的 subagent_start 的 id;subagent 再启动 subagent 时按同样的规则嵌套。启动 subagent 的那次工具调用只输出 subagent_start 与 subagent_end,不输出 tool_start 与 tool。理由:调用方据此知道 subagent 何时开始与结束,不需要认识每个运行时的工具名。

主 agent 与多个 subagent 可以同时运行,它们的事件在流里交错出现。id 是运行时给出的工具调用 id,较长,用于关联事件;number 用于显示。

运行结束时还没结束的 subagent,在 end 之前补发 status 为 failed 的 subagent_end,duration_ms 记到运行结束时。subagent 失败不改变 end.status,理由是主 agent 可能已经处理了它的失败并完成任务。

subagent 与主 agent 在同一个运行时进程里运行,受同样的沙箱与权限限制。claude-code、codex 与 dsh 默认允许启动 subagent,给出 --no-subagents 时关闭;pi 没有 subagent。codex 的输出里没有 subagent 的启动事件与 subagent 内部的事件,所以 codex 的事件流里没有 subagent_start 与 subagent_end,subagent 做的事不出现在事件流里(codex 0.159.2 实测)。dsh 只输出主会话的事件,启动 subagent 的调用按普通工具调用输出 tool_start 与 tool,同样没有 subagent_start 与 subagent_end。

用量

用量属于一次模型调用,一次调用会产生多个 tool 与 text 事件,所以用量单独成一个 usage 事件,不附在其他事件上。

end.usage 取运行时自己报告的整次运行总量;运行时没有总量时(pi、codex、dsh)取全部 usage 事件的合计。claude-code 的总量里有它内部的一笔 haiku 调用(auto 模式的分类器),这笔调用没有对应的 usage 事件,所以 end.usage 可能大于 usage 事件之和。理由:与实际计费一致。subagent_end.usage 是 parent 链上属于这个 subagent 的 usage 事件之和,只用于展示,已经包含在 end.usage 里。

end.usage 的字段为 input_tokens、output_tokens、cache_read_tokens、cache_write_tokens、cost_usd,另有 by_model 按模型给出同样的五个字段,模型未知时键为 unknown。合计只加非 null 的值,所有来源都没报告的字段为 null;一次运行没有任何用量时五个总量为 null,by_model 为 {}。

费用

cost_usd 是运行时自己报告的费用,单位美元,agentrun 不按单价计算。各运行时报告的范围不同:

运行时usage 事件end.usagesubagent_end.usage
claude-codenull,逐次调用的用量里没有费用总量取最后一个 result 的 total_cost_usd,by_model 取 modelUsage 各模型的 costUSD。总量用运行时自己的合计,与 token 字段不同,不保证等于 by_model 之和;样本里两者相等null
pi这次调用的 usage.cost.totalusage 事件之和没有 subagent
codexnullnull没有这个事件
dshnullnull没有这个事件

end.status

按表中从上到下的顺序取第一个成立的状态。理由:超时、到达上限或被中断时,运行时可能已经输出了失败事件(例如 claude-code 收到 SIGINT 后的 result 为错误),这时调用方需要知道的是超时、上限或中断。

status条件agentrun 退出码
rejected启动前失败,情形见启动前检查2
interruptedagentrun 收到 SIGINT 或 SIGTERM,以第一个信号为准130 或 143
timeout超过 --timeout3
limit超过 --max-turns 或 --max-tokens(由 agentrun 自己计数),且运行时没有满足 finished 的条件,见轮数与 token 上限4
failed运行时的失败条件成立,见运行。各运行时共有的条件:退出码非 0;正常退出却没有输出结果事件;运行中发现沙箱启动失败1
finished以上都不成立0

text 格式

--format text 每个事件以一行 [agent 标签] 事件类型 内容 开头,除 text 与 end 外都只占这一行。主 agent 的标签是 main,subagent 的标签是小写的类型加编号,例如 explore#1、general-purpose#2。没有 subagent 时也带 [main]。理由:主 agent 与 subagent 的事件交错出现,每行都带标签才能分辨来源,也能用 grep '^\[explore#1\]' 取出一个 subagent 的全部事件(text 的续行不带标签,取不到)。

一个 agent 显示的模型取它最近一次 usage 的 model;还没有 usage 时取 subagent_start 的 model,主 agent 取 start 的 model;都没有时不显示。

$ agentrun claude-code --format text --prompt "给 parse 模块补测试并修复失败" [main] prompt 给 parse 模块补测试并修复失败 [main] tool Read: src/parse.rs [main] agent start explore#1 (haiku): 查找所有调用 parse_line 的位置 [main] agent start general-purpose#2: 为 parse_line 写边界测试 [explore#1] tool Grep: parse_line [general-purpose#2] tool Read: tests/parse.rs [main] tool Bash: cat ~/.ssh/id_rsa (denied) [explore#1] text 共 3 处调用,都在 src/cli.rs。 [explore#1] agent finished 12.4s claude-haiku-4-5 in 3.1k out 85 [general-purpose#2] tool Edit: tests/parse.rs [general-purpose#2] agent finished 20.8s claude-sonnet-5-5 in 9.4k out 0.5k [main] tool Bash: cargo test (error) [main] tool Edit: src/parse.rs [main] tool Bash: cargo test [main] text 修复了 parse_line 对空行的处理,测试全部通过。 [end] finished 31.3s in 65.3k out 0.9k cached 64% $0.0437

rich 格式

--format rich 在终端里显示动态界面,分三个区域。标准输出不是终端时,rich 按 text 输出。

$ agentrun claude-code --prompt "给 parse 模块补测试并修复失败" [main] prompt 给 parse 模块补测试并修复失败 1 [main] tool Read: src/parse.rs [main] agent start explore#1 (haiku): 查找所有调用 parse_line 的位置 [main] agent start general-purpose#2: 为 parse_line 写边界测试 [main] agent start explore#3 (haiku): 确认测试框架与运行方式 [explore#1] tool Grep: parse_line [general-purpose#2] tool Read: tests/parse.rs [explore#3] agent finished 6.2s claude-haiku-4-5 in 1.8k out 40 ─────────────────────────────────────────────────────────────────────────── subagent 2 running 2 ⠙ explore#1 claude-haiku-4-5 查找所有调用 parse_line 的位置 Read: src/cli.rs 0:11 ctx 14.1k ⠹ general-purpose#2 claude-sonnet-5-5 为 parse_line 写边界测试 Edit: tests/parse.rs 0:09 ctx 22.4k ─────────────────────────────────────────────────────────────────────────── ⠸ main claude-sonnet-5-5 Bash: cargo check 0:24 in 54.2k cached 78% ctx 61.3k 3
  1. 滚动区。提示词、已完成的工具调用、文字与 subagent 的启动和结束按 text 格式逐行输出,终端的滚动缓冲保留全部历史。最近 3 段文字(常量 RECENT_TEXTS)按 text 格式显示多行,它们和其后的行先画在面板上方、每次随面板重画;出现第 4 段文字时,最早的一段只留第一行,和它后面直到下一段文字之前的行一起写入滚动区。这几段加上面板高于终端时,也按同样的方式从最早的一段开始折叠,理由:重画只能擦除屏幕上可见的行。运行结束时剩下的几段完整写入滚动区,再输出 [result] 与 end 行。制表符换成 4 个空格,理由:计算行数时要知道每行占几列。
  2. subagent 列表。出现过 subagent 之后才显示,每个运行中的 subagent 一行:动画、标签、模型、描述、它正在执行的工具、已运行时间、最近一次 usage 的上下文占用。按启动顺序排列。subagent 结束后从列表移除,结束行进入滚动区。没有运行中的 subagent 时显示「0 running」。终端宽度不够时截断描述。标签与模型取自 TextFormatter 记下的值,与 text 格式显示的相同。
  3. 状态栏。动画、main 标签与主 agent 的模型、主 agent 正在执行的工具、已用时间、累计用量(到目前为止全部 usage 之和,包括 subagent,只写 in 与 cached,不写 out)、主 agent 的上下文占用。状态栏比终端宽度少用 3 列,宽度不够时先截断工具、再截断整行,理由:按 Ctrl-C 时终端在光标处回显 ^C,光标在状态栏末尾,状态栏占满一行时回显会折到下一行,结束时擦除少擦一行,面板的分隔线留在滚动区里。运行时没报告的项不显示。结束时状态栏换成 end 行,subagent 列表清除。

「正在执行的工具」从事件流取:rich 记下已经收到 tool_start、还没收到同 id 的 tool 的调用,按 parent 分到各个 agent,每个 agent 显示其中最近开始的一个。

面板每 100 毫秒重画一次,滚动区每输出一行也立即重画;动画每 100 毫秒换一帧。面板第一次出现时隐藏光标,结束时恢复。

rich 显示期间,如果标准错误也是终端,运行时写到标准错误的内容与 agentrun 自己写到标准错误的行(例如 [debug] 行)按行插入滚动区,不加标签与颜色,不完整的最后一行在运行结束时输出。理由:两路输出落在同一个终端上,直接写出会打在面板中间。标准错误不是终端时照常直接写出。

rich 只用暗色(SGR 2)显示滚动区的 agent 标签与面板的分隔线。环境变量 NO_COLOR 存在且不为空时不输出 SGR 序列,其他控制序列(移动光标、清屏、隐藏光标)照常输出。

已知限制: