运行:命令行与运行时
前半页写调用方怎么运行 agentrun:命令行选项、一次运行的顺序、启动前检查、doctor 与退出码。后半页每个运行时一节:agentrun 用什么参数启动它、它的原始输出怎么映射到统一事件、什么情况算失败。运行时的参数与行为来自实测,写明了实测的版本;运行时升级后要重新核对。原始输出的完整字段见 claude-code 事件流与 pi 事件流两页字段参考。
代码位置
| 文件 | 主要类型与函数 | 本页对应的章节 |
|---|---|---|
src/main.rs | 入口:安装信号处理,把进程的参数、环境与标准流装进 Caller,调用 run::run,把返回的退出码交给进程 | 命令行界面、退出码 |
src/cli.rs | Cli、Parsed、RunArgs、DoctorArgs、ConnectArgs、Runtime、SandboxMode、NetworkMode、Format、prescan_format、default_format、usage_error_detail、MISSING_COMMAND | 命令行界面 |
src/run/mod.rs | Caller、run、create_tempdir、shell_quote | 一次运行的顺序、启动前检查、额外传递参数 |
src/run/execute.rs | execute、read_lines、temp_environment | 一次运行的顺序、轮数与 token 上限、退出码 |
src/runtime/mod.rs | Adapter trait(runtime、check_args、launch、echoes_prompt、translate、after_exit、failure、transcript)、Invocation(new)、Launch、Failure、PrivateDir、builtin、reject_limits、DETAIL_MAX_CHARS、detail_head、detail_tail、Version(find)、min_version、model_service_host、service_hosts、proxy_needed、starts_inside_wrapper、sandbox_state、display_path、contains | 各运行时一节的共同接口 |
src/runtime/claudecode.rs | ClaudeCode、MIN_VERSION、SERVICE_HOST | claude-code |
src/runtime/pi.rs | Pi、MIN_VERSION、Model、parse_model、model_service、session_state、default_model、assistant_model | pi |
src/runtime/codex.rs | Codex、MIN_VERSION、SERVICE_HOSTS、SERVICE_HOST、sandbox_command | codex |
src/runtime/dsh.rs | Dsh、MIN_VERSION、KEY_VARIABLE、REMOVED_PREFIX、service_host、saved_session、login_outcome | dsh |
src/transcript.rs | Saved、Transcript、SaveError、store、create_store、locate、find_file、find_file_within、copy_into、copy_dir_into、save | 保存与继续会话 |
src/doctor.rs | run、connect、selected、COMMAND_TIMEOUT、USAGE_EXIT_CODE | doctor 检查 |
src/image.rs | Image、ImageKind、read_images、write_copies | 图片 |
src/json.rs | string、optional_string、joined_text、first_line | 各运行时的输出映射 |
每个运行时一个 Adapter 实现。check_args 在启动前检查只对某个运行时有要求的参数,例如 pi 的 --model 格式与 codex 不支持的 --max-turns、--max-tokens 与它挡不住的 --deny-read 路径。launch 返回 Launch(argv、标准输入内容、环境变量,以及记录 agentrun 用哪种沙箱包裹了命令的 wrapped 与适配器创建的私有目录 private_dirs)。运行框架按 wrapped 是否为 bubblewrap 决定第一个信号发给谁(见进程、信号与超时),过滤代理放行的模型服务主机取自 runtime::service_hosts,与 doctor 用同一个函数。translate 把运行时的一行原始输出转成零个或多个 Record,failure 在运行时退出后按本页各节的「失败判定」返回 Failure:Message 给出 detail,Unexplained 表示没有说明,由运行框架先看包裹命令是否启动失败,再取标准错误。适配器给出的说明为空字符串时按 Unexplained 处理。transcript 在运行时退出后返回这次运行留下的会话记录:会话 id 与要移进 --session-dir 的文件和目录;没有给出 --session-dir 或运行时没有留下记录时返回 None,见保存与继续会话。运行框架 src/run/ 不认识任何运行时的字段,理由:运行框架的流程对每个运行时相同;运行时之间的差别写在适配器里,以及 src/runtime/mod.rs 里与 doctor 共用、按 Runtime 分支的函数(min_version、service_hosts、proxy_needed、sandbox_state、default_denied_reads 等),另有 Session::assemble、sandbox::check、credential::write_session_credential 三个按 Runtime 分支的函数,运行框架只把 Runtime 传给它们。
按运行时分支一律用列出全部运行时的 match,不用 ==、!= 或带 _ 的分支;要遍历所有运行时时用 Runtime::value_variants(),不另写运行时列表。理由:新增一个运行时时,编译器会列出每个要补上的地方。
命令行界面
核心子命令有 agentrun claude-code、agentrun codex、agentrun pi、agentrun dsh。四个运行时子命令接受同一组选项。
agentrun claude-code --prompt "给 parse 模块补测试并修复失败"
agentrun pi --model deepseek/deepseek-flash --network custom --allow-host pypi.org --prompt-file task.md
agentrun codex --sandbox off --format jsonl --timeout 600 < task.md
| 选项 | 含义 | 默认 |
|---|---|---|
--cwd DIR | 会话的工作目录,必须存在。agentrun 把它解析为真实路径(展开符号链接),运行时、沙箱、claude-code 的放行规则与 start 事件都用这个路径,理由:bwrap 不能把目录挂载到符号链接上,claude-code 按真实路径匹配放行规则。沙箱运行时,它与会话临时目录是 agent 仅有的可写目录 | 当前目录 |
--prompt TEXT | 提示词。以 - 开头的值也是提示词,例如以 Markdown 列表开头的任务。--prompt 后面的参数是 agentrun 运行子命令的长选项(例如 --dry-run,或带 =值 的写法)或 -- 时是用法错误,detail 为 --prompt is followed by the option --dry-run; to use it as the prompt, write --prompt=--dry-run;要用这样的提示词时写成 --prompt=--dry-run。理由:脚本里没加引号的空变量会让 --prompt 吞掉后面的选项,运行照常开始 | 无 |
--prompt-file FILE | 从文件读提示词。与 --prompt 同时给出是用法错误 | 无 |
--image FILE | 与提示词一起交给模型的图片,可重复,见图片。dsh 不支持,给出时是用法错误 | 无 |
| 标准输入 | 前两项都没给时从标准输入读提示词。标准输入是终端时不等待,直接报用法错误;提示词为空也是用法错误 | |
--model ID | 模型,原样传给运行时。pi 必须写成 服务商/模型,并检查 pi 实际使用的模型是否一致,见pi 一节;dsh 写进补丁文件,见dsh 一节 | 不传,用运行时自己的默认配置 |
--effort LEVEL | 推理强度,传给运行时对应的参数:claude-code 的 --effort、codex 的 model_reasoning_effort、pi 的 --thinking、dsh 补丁里的 reasoningEffort。不检查取值 | 不传 |
--max-turns N | 主 agent 最多调用模型 N 次,N 为正整数。主 agent 开始第 N+1 次调用时终止整个进程组,结果是 limit,见轮数与 token 上限。codex 给出时是用法错误 | 不限 |
--max-tokens N | 全部模型调用(含 subagent)的 token 数之和超过 N 时终止整个进程组,结果是 limit,N 为正整数,见轮数与 token 上限。codex 给出时是用法错误 | 不限 |
--timeout SECONDS | 从运行时启动开始计时,到时终止整个进程组,SECONDS 为正整数,见进程、信号与超时 | 不限 |
--sandbox on|relax|off | 沙箱做不到时怎么办,见沙箱模式。高于环境变量 AGENTRUN_SANDBOX | on |
--network none|full|custom | 命令与网页工具能访问哪些主机,见网络 | none |
--allow-host HOST[:PORT] | 允许访问的主机,可重复,只能与 --network custom 一起给出 | 无 |
--deny-read PATH | agent 不能读的文件或目录,可重复,加在默认列表之后。以 ~ 开头,或者等于或包含工作目录、会话临时目录所在的目录、运行 pi 时 pi 的状态目录时是用法错误,运行 codex 时路径含 * ? [ ] { } 也是用法错误,见读取限制 | 无 |
--no-default-deny-read | 不用默认的不可读列表,只限制 --deny-read 给出的路径 | 使用默认列表 |
--path DIR | 放在会话 PATH 最前面的目录,可重复,必须存在 | 无 |
--env KEY[=VALUE] | 给会话设置一个环境变量,可重复。只写 KEY 时取调用方环境里的同名变量,见会话环境 | 无 |
--env-file FILE | 从文件读取环境变量,可重复,按给出的顺序加载 | 无 |
--no-subagents | 不允许 agent 启动 subagent 与 workflow。pi 没有 subagent,给出时不起作用 | 允许 |
--session-dir DIR | 把这次运行的会话记录保存到 DIR/<运行时名>/,end 事件的 session_id 给出会话 id,见保存与继续会话 | 不保存 |
--resume ID | 继续 --session-dir 里保存的会话 ID。只给 --resume 不给 --session-dir 是用法错误 | 新会话 |
--format rich|text|jsonl | 标准输出的格式,见事件与输出 | 标准输出是终端时 rich,否则 jsonl |
--raw FILE | 把运行时的原始标准输出逐行写入这个文件,包括无法解析的行 | 不写 |
--debug | 在标准错误输出实际执行的命令、会话 PATH、被删除与被设置的环境变量名、凭据写入情况、沙箱检查结果、临时目录的路径与 codex 私有目录的路径(codex home: );保留本次运行的临时目录与 codex 私有目录(dsh 的私有 DSH_HOME 在临时目录里,一起保留);没有给 --raw 时,原始输出写入临时目录里的 raw.jsonl | 不开 |
--dry-run | 完成命令行解析与启动前检查,把将执行的命令、会话 PATH 与变量名打印到标准输出后以 0 退出。不创建临时目录,不写凭据,不输出事件 | 不开 |
agentrun <运行时> --help 把选项分成 Prompt、Model and limits、Sandbox, network and reads、Session environment、Saved sessions、Output 六组,逐项说明 --sandbox、--network 与 --format 的取值,末尾写出退出码与使用指南的链接;-h 给出每项一行的简表。分组名是 src/cli.rs 里的常量。
任何输出都只写变量名,不写变量的值,PATH 除外。四个运行时都从标准输入读提示词,所以提示词不出现在 start 事件的 argv 里,也不出现在 --dry-run 与 --debug 打印的命令里。
agentrun 自己写出的文字都用英文:detail、标准错误里的说明、text 与 rich 格式里的固定词、[note] 提示与 doctor 的输出。运行时与模型给出的内容(提示词、模型的文字、运行时的错误信息)原样输出。理由:命令行解析使用现成的库,它的错误信息是英文,全部输出用同一种语言。
启动前检查失败时,写到标准错误的一行是 agentrun: 加 detail,见启动前检查。--dry-run 写到标准输出、--debug 写到标准错误,每项一行,格式是 项目名: 值,--debug 的每行前面另加 [debug] 。命令的每个参数按 shell 规则加引号。
agentrun variables 一行列出调用方环境里以 AGENTRUN_ 开头的变量名,它们只给 agentrun 自己用,不进入会话,见会话环境。deny read 一行列出 agent 不能读的路径,与 start 事件的 deny_read 相同,见读取限制。
另外有 agentrun doctor。在 后文 详细描述。
额外传递参数给运行时
-- 之后的所有参数原样传给运行时,用于工具没有提供选项的运行时功能。工具生成的参数在前,-- 之后的参数接在后面。多数运行时遇到同一个选项出现两次时以后一次为准,所以调用方的参数可以覆盖工具的设置。
可以覆盖的设置包括权限与沙箱设置。工具不阻止,只在 start 事件的 argv 里如实记录,这时安全保证由调用方负责。理由:用户需要为没有预料到的用途保留完整的控制。
调用方的参数可能让运行时输出工具不认识的事件,例如给 claude-code 传 --include-partial-messages 后,会多出逐字输出的 stream_event。这类事件不进入统一事件流,只写入 --raw 文件。
图片
--image FILE 把一个图片文件与提示词放在同一条用户消息里交给模型,可以重复,图片按给出的顺序排列,数量不限。agentrun 在启动前读出每个文件的全部内容,以下情形是用法错误:
- 文件不存在或读不了,
detail为cannot read image <路径>: <系统错误>。 - 路径(跟随符号链接之后)不是普通文件,例如目录、FIFO、设备文件,
detail为image <路径> is not a regular file。文件以非阻塞方式打开,再检查类型。理由:FIFO 会让打开一直阻塞,设备文件的大小是 0、读取没有尽头,这两种都发生在启动前检查里,--timeout管不到。 - 文件超过 20 MiB(20971520 字节),
detail为image <路径> is larger than the limit of 20971520 bytes。读取最多读到上限加 1 字节,按实际读到的长度判断。 - 文件不是 PNG、JPEG、GIF 或 WebP,
detail为image <路径> is not a png, jpeg, gif or webp file (animated png and jpeg-ls are not supported)。类型按文件内容判断,不看扩展名,规则按 pi 1.2.0 识别图片的规则写:PNG 的第一个块必须是长度 13 的 IHDR,IDAT 之前出现 acTL 块(动画 PNG)时不接受;JPEG 第 4 个字节是 0xF7(JPEG-LS)时不接受。pi 只在文件的前 4100 字节里找 acTL,agentrun 检查整个文件,所以 acTL 在 4100 字节之后的动画 PNG 被 agentrun 拒绝,pi 却会接受。理由:pi 不认作图片的文件会被当成文本读进提示词,不报错,模型看不到图片。
运行时拿到的是 agentrun 读出的内容,不读调用方给的路径。claude-code 的图片直接写进标准输入;pi 与 codex 的图片先写成会话临时目录里的 image-N.扩展名(N 从 1 开始,扩展名按类型取 png、jpg、gif、webp,权限 0600),再把这个路径传给运行时。理由:沙箱打开时 agent 一定能读会话临时目录,调用方的图片可能在别的目录里。--dry-run 不写这些文件,命令里的路径写成 <tempdir>/image-1.png。各运行时的传法见 claude-code、pi、codex 三节。dsh 的 headless 模式只接受文字,给出 --image 时启动前按用法错误拒绝,detail 为 --image is not supported by dsh: its headless mode takes text only。
三个支持图片的运行时都没有更严的文件大小限制:claude-code 与 pi 把过大的图片缩小后交给模型(实测 48 MB、4000×4000 像素的 PNG),codex 读本地图片时不检查大小、超过 2048 像素时缩小(codex 源码 codex-rs/utils/image)。20 MiB 是 agentrun 自己的上限,理由:图片在启动前整个读进内存,claude-code 的图片还要按 base64 写进标准输入的一行 JSON,体积再增加三分之一。
prompt 事件的 images 按顺序列出调用方写的路径,不转成绝对路径,不写图片内容,见事件与输出。
一次运行的顺序
启动前检查
以下情形在启动运行时之前报错。标准输出只有一个 end 事件,status 为 rejected,detail 写出原因与修正方法;同一个原因另写一行 agentrun: <detail> 到标准错误;agentrun 以退出码 2 结束。格式是 text 或 rich、并且标准输出与标准错误都是终端时,不写标准错误这一行。理由:两路输出都在用户的终端上时,同一句话会出现两次;输出被重定向时,调用方从标准错误或 end 事件都能读到原因。
- 用法错误:未知子命令或选项、值不合法、codex 给出
--max-turns或--max-tokens、提示词来源冲突或为空、--prompt-file读取失败、--image读取失败、类型不对或超过上限、--cwd或--path不存在、--raw文件无法创建、只写 KEY 的--env在调用方环境里找不到、--allow-host格式不对、--deny-read以~开头、等于或包含运行需要的目录、运行 codex 时含* ? [ ] { }、环境变量AGENTRUN_SANDBOX的值不是on、relax、off之一。 --env-file读取失败或格式错误。- 在会话 PATH 里找不到运行时。
- 凭据写入失败。
--session-dir的目录创建失败,或--resume给出的会话在--session-dir里找不到。--sandbox on时沙箱不可用,或--network要求的限制无法执行,见沙箱与网络。- 过滤代理启动失败;pi 的服务商不在模型服务主机表里且用
--network none;dsh 的DEEPSEEK_BASE_URL读不出主机且用--network none。 - 会话临时目录创建失败;适配器准备启动参数失败,包括 pi 的状态目录创建失败或 Seatbelt 配置文件写不出、codex 的登录文件不存在或私有目录创建失败、dsh 的会话环境里没有
DEEPSEEK_API_KEY或私有目录与补丁文件写不出。
过滤代理启动失败先于 codex 登录文件与私有目录的检查:两者同时成立时,detail 是过滤代理那一条。
命令行解析失败时还不知道 --format 的值,所以先单独扫描一遍参数找 --format,找不到或值不合法时按默认格式输出这个 end。
命令行解析的错误说明由 clap 生成。usage_error_detail 去掉其中的 error: 前缀、Usage: 与 For more information 两部分,把剩下的各行连成一行:前一行以冒号结尾时用空格连接,冒号之后紧接着的缩进各行是一个列表,列表项之间用 , ,其余用 ; 。例如 the following required arguments were not provided: <HOST>, <PORT> 与 unexpected argument '--promt' found; tip: a similar argument exists: '--prompt'; tip: to pass '--promt' as a value, use '-- --promt'。完全没有参数时不解析命令行,把顶层帮助写到标准错误,以退出码 2 结束,不输出 end。有参数但没有子命令时(例如只有 --),detail 是常量 MISSING_COMMAND:no command given: use claude-code, codex, pi, dsh or doctor; see agentrun --help。理由:clap 自己的说明会列出只给 doctor 内部使用的 doctor-connect。--max-turns、--max-tokens 与 --timeout 的值不是正整数时,说明是 invalid value '0' for '--timeout <SECONDS>': expected a positive integer。
run::run 解析命令行后,把运行时子命令交给运行框架,把 doctor 与 doctor-connect 分别交给 doctor::run 与 doctor::connect。运行时子命令被拒绝时的 end 只由 run::run 输出。
轮数与 token 上限
--max-turns 与 --max-tokens 由 agentrun 自己计数,不传给运行时,claude-code、pi 与 dsh 接受同样的选项。理由:运行时自带的上限只有 claude-code 有,由 agentrun 计数时同一条命令可以在这几个运行时之间换。
- 一轮是主 agent 的一次模型调用。适配器在一次调用开始时输出
Record::CallStart:claude-code 是某个parent_tool_use_id下出现新的message.id,pi 是 assistant 的message_start,dsh 是phase为step_start的status。Aggregator只数 parent 为 null 的调用,parent 指向没见过的 subagent 时按主 agent 计,与其他事件的 parent 规则相同。主 agent 开始第 N+1 次调用时终止。claude-code 可能在这次调用的输出还没结束时就开始执行它请求的工具,所以这些工具可能已经开始执行;沙箱外的 Bash 命令用 setsid 启动,不在进程组里,由运行时退出后的进程树清理结束。理由:与 claude-code 自带的--max-turns计数相同;不按usage事件计数,因为 claude-code 的usage要等下一次调用开始才输出,按它计数时 claude-code 比 pi 多一轮。 - token 数是全部
usage事件(含 subagent)的input_tokens、output_tokens、cache_read_tokens、cache_write_tokens之和,null 按 0 计。还没有输出usage事件的调用,按适配器输出的Record::CallUsage计入:claude-code 的usage要等下一次调用开始才输出,而每条 assistant 消息都带着这次调用到当时为止的用量,适配器每读到一条就输出一次,同一个 parent 的usage到达时取代它。运行框架每处理完一行原始输出检查一次,和超过 N 时终止,运行时退出后读到的剩余输出也照常检查;end.usage里没有对应usage事件的那笔 haiku 调用(见事件与输出的用量一节)不计入。dsh 不输出 subagent 的事件,输出里只有主会话的用量,所以 dsh 的--max-tokens只计主 agent 的 token,需要严格限制 dsh 的 token 数时同时给出--no-subagents。dsh 的end.usage是usage事件之和,与上限计数的范围相同,同样不含 subagent。pi 与 dsh 只在一次调用结束时报告这次调用的用量,最后一次调用的用量全部计入,所以一次很长的调用会让最终的 token 数明显超过 N(dsh 实测:上限 8000,结束时 11059)。 - 终止的方式与
--timeout相同:向进程组发 SIGTERM,5 秒后 SIGKILL,见进程、信号与超时。超过上限时,运行时满足finished的条件,结果是finished(例如最后一次调用使和超过 N,但运行时已经给出结果并以 0 退出);不满足时end.status为limit,退出码 4。是否发过 SIGTERM 不影响判定,理由:运行时可能在退出码已经定为 0、进程还没有结束时收到信号,agentrun 无法区分这种情况与还在运行的运行时,按是否发过信号判定时结果会随时机变化。这个判定依赖一个条件:运行时在完成之前被 SIGTERM 停止时,适配器不能把这次运行判为finished,靠非 0 的退出码或缺少完成事件都可以。claude-code(>= 2.1.303)与 pi(>= 1.0.2)收到 SIGTERM 时都以 143 退出,Linux 与 macOS 上包裹与不包裹相同。dsh 收到 SIGTERM 时以 0 退出。在一轮中间被停止时它不再输出turn_end,按 dsh 一节失败判定的第 4 条不满足finished;在一轮刚结束时被停止时,它照常输出completed的turn_end,随后输出error事件session …: flush on a closed handle,按第 1 条同样不满足finished(0.2.1-rc.2 实测,前一种 3 次、后一种 9 次,结果都是limit)。适配器只看事件,dsh 在收到 SIGTERM 前已经输出turn_end并且之后没有error事件时,结果是finished。所以被 agentrun 停止的运行一定是limit,即使运行时当时已经输出了完整的回答,这时end.result照常取已经收到的内容。不满足这个条件的运行时要拒绝这两个选项:codex 在 Linux 上收到 SIGTERM 时以 0 退出,这是 codex 不接受这两个选项的理由之一。detail写出是哪个上限、上限值与实际值,例如--max-turns 3 reached: the main agent started turn 4、--max-tokens 50000 exceeded: 51234 tokens。两个上限在同一行原始输出上同时超过时,detail写--max-turns。 - 超时、上限与信号中先触发的一个决定怎样终止,之后的上限不再生效;上限触发后的宽限期内收到信号,结果是
interrupted。适配器要求终止运行时(例如 pi 的模型不一致)的那一行不检查上限。 - codex 给出任一选项都是用法错误(
rejected,退出码 2),由reject_limits检查。codex 只在运行结束时输出一次用量(turn.completed),也没有调用开始的事件(codex 0.159.2 实测),agentrun 无法在运行中停止它。理由:停不住 agent 的上限会让调用方以为有保护,明确拒绝不会。
保存与继续会话
给出 --session-dir DIR 时,agentrun 在运行时退出后把这次运行的会话记录移到 DIR/<运行时名>/,end.session_id 是会话 id。下一次运行给出同一个 --session-dir 与 --resume <会话 id>,运行时接着这段会话继续:会话 id 不变,新内容追加到同一份记录。不给 --session-dir 时四个运行时都不保存会话。--resume 只从 DIR 读,不读用户自己的会话记录;codex、pi 与 dsh 运行期间把记录写在私有目录或会话临时目录里,claude-code 的新会话运行期间写在用户的配置目录里,见下表与配置与账号。
DIR与DIR/<运行时名>/不存在时在启动前创建,新建的目录权限为 0700;DIR是相对路径时相对 agentrun 自己的当前目录。目录在所有启动前检查通过之后才创建,--dry-run与被拒绝的运行不创建目录。DIR要放在工作目录与会话临时目录之外:沙箱里的 agent 能写这两处,DIR在里面时 agent 能改写已保存的会话。沙箱运行、DIR等于或在工作目录之内时(与--deny-read相同的包含判断,见读取限制;判断在创建DIR之后进行,所以DIR总是跟随符号链接展开后比较,第一次使用时也一样),运行照常进行,text 与 rich 格式在start的位置输出一行[note] --session-dir DIR is inside the working directory. The agent can change the saved sessions;jsonl 格式没有这一行。不拒绝运行,理由:调用方可能有意把会话放在工作目录里,例如沙箱之外另有隔离时。会话临时目录每次新建、名字随机,DIR不会在它里面,所以不检查。沙箱没有运行时不输出这一行,沙箱关闭的[note]已经说明 agentrun 不限制写入。- 记录的文件名沿用运行时自己的文件名。
--resume ID在DIR/<运行时名>/里找文件名以ID.jsonl结尾、且 ID 前面是文件名开头、_或-的文件;dsh 的记录是一个目录,找名为session-ID的目录。ID 必须是完整的会话 id,四个运行时的会话 id 都是 UUID(8-4-4-4-12位十六进制),不是这个形式时启动前按rejected拒绝,detail为--resume "<ID>" is not a session id。理由:只要求文件名以 ID 结尾时,UUID 的最后一段也能匹配,end.session_id与传给 codex 的 id 就不是真实的会话 id。只在这次运行时自己的子目录里找,用另一个运行时继续某个运行时保存的 id 同样找不到。找不到时启动前按rejected拒绝,detail为no saved <运行时名> session <ID> in <目录>;这个 id 在DIR里另一个运行时的子目录里找得到时,后面再加; it was saved by <运行时名>, continue it with agentrun <运行时名>,按 claude-code、codex、pi、dsh 的顺序取第一个找到的。 - 保存在运行时退出、进程组清理之后,
end输出之前进行。failed、interrupted、timeout与limit的运行只要留下了记录也照样保存;运行时没有留下记录时session_id为 null。移动失败时标准错误多一行agentrun: cannot save the session: <原因>. The session is kept in <路径>,session_id为 null,status不变。保存先把新记录放到目标旁边的.<文件名>.agentrun-saving(同一个文件系统时改名,不在同一个文件系统时复制),完整放好之后才替换已保存的旧记录:文件直接改名覆盖,目录先把旧目录改名为.<目录名>.agentrun-replaced,新目录改名到位后再删除旧目录。放置或替换失败时已保存的旧记录不变,放了一半的副本被删除,这一行末尾列出还没有保存的记录现在所在的路径,记录改名到暂存位置后替换失败时改回原处,会话临时目录与 codex 的私有目录在结束时保留,不删除。保存在两次改名之间中断时会留下.<目录名>.agentrun-replaced,之后的--resume查找与保存在目标不存在时把它改回原名,目标存在时删除它。理由:继续会话后旧记录与新记录同名,先删旧记录再复制时,磁盘满或复制出错会同时丢掉两份。 - 继续时 claude-code、codex 与 pi 的
--cwd可以与上一次不同,运行时在新的工作目录里工作(三个运行时实测);dsh 的--cwd必须与第一次相同,见下表。--model等选项每次照常传给运行时。 - 不支持同时继续同一个会话。两个运行同时
--resume同一个 id 时,claude-code 的两份新内容交错写进同一个文件;codex、pi 与 dsh 后结束的那次覆盖先结束的那次。
| 运行时 | 新会话 | 继续会话 |
|---|---|---|
| claude-code | 不加 --no-session-persistence,仍传新生成的 --session-id。claude-code 把记录写在 <配置目录>/projects/<由 cwd 转换的目录名>/<id>.jsonl,subagent 的记录在同级的 <id>/;配置目录是会话环境里的 CLAUDE_CONFIG_DIR,未设时为 ~/.claude。运行结束后 agentrun 在 projects/ 的各个子目录里按文件名找 <id>.jsonl(只读目录列表,不读文件内容),把它与 <id>/ 移到 DIR/claude-code/,<由 cwd 转换的目录名> 变空时删除。目录里还有别的内容时保留。会话环境里既没有 CLAUDE_CONFIG_DIR 也没有 HOME 时仍加 --no-session-persistence,不保存,session_id 为 null | 用 --resume <DIR/claude-code/<id>.jsonl> 代替 --session-id。claude-code 直接在这个文件末尾追加,subagent 的记录写在它旁边的 <id>/,不写配置目录(2.1.288 与 2.1.303 实测,2.1.303 在 macOS 上用钥匙串登录同样如此) |
| codex | 记录在私有目录里的 sessions/YYYY/MM/DD/rollout-<时间>-<id>.jsonl,id 取 thread.started 的 thread_id。运行结束后移到 DIR/codex/。subagent 的记录不保存 | 启动前把保存的文件复制进私有目录的 sessions/,argv 在最后的 - 之前加 resume <id>。codex 在 sessions/ 下按 id 找到文件并在末尾追加。运行结束后 agentrun 在 sessions/ 下取文件名含这个 id、修改时间最新的文件,移回 DIR/codex/ 覆盖原文件(0.159.2 与 0.163.0 用模拟的模型服务实测,0.160.1 在 macOS 上用真实模型实测) |
| pi | --no-session 换成 --session-dir <会话临时目录>/pi-session,id 取输出里 session 行的 id。运行结束后把记录移到 DIR/pi/ | 启动前把保存的文件复制进 <会话临时目录>/pi-session/,传 --session <复制后的路径>,运行结束后移回 DIR/pi/ 覆盖原文件。不用 pi 的 --session-id:它只在与第一次相同的 cwd 下找得到会话,换一个 cwd 会新建一个 id 相同的会话(1.2.0 实测) |
| dsh | dsh 把记录写在私有 DSH_HOME 的 sessions/<由 cwd 转换的目录名>/session-<id>/,目录里是 session.v4.jsonl.zstd 与锁文件;id 取 session 事件的 sessionId 去掉 session- 前缀。运行结束后 agentrun 在 sessions/ 的各个子目录里找 session-<id>/,把整个目录移到 DIR/dsh/ | 启动前把 DIR/dsh/session-<id>/ 复制到 DSH_HOME/sessions/<由 cwd 转换的目录名>/ 下,在 --json 之后传 --session-id session-<id>,运行结束后移回 DIR/dsh/ 覆盖原目录。--cwd 必须与第一次的真实路径相同:dsh 既按目录名找会话,又比较记录开头保存的 cwd,不同时报会话记录损坏,这次运行为 failed(0.2.1-rc.2 实测)。目录名由 cwd 的真实路径按 dsh 的规则计算(project_key,移植自 dsh 的 dsh-session-persistence-jsonl) |
- claude-code 的新会话写在用户的配置目录里,运行结束后才移走。理由:claude-code 不能把新会话直接写到别的位置(
--resume指向空文件、不存在的文件或只有元数据行没有对话的文件时都报No conversation found,2.1.303 实测);改CLAUDE_CONFIG_DIR会改变 claude-code 读登录信息的位置:Linux 上登录文件在配置目录里,macOS 上钥匙串里的登录条目名随配置目录改变(未实测)。代价是运行期间这份记录出现在用户自己的claude --resume列表里,agentrun 被 SIGKILL 时它留在那里。 - pi 与 dsh 的记录先写在会话临时目录里,运行结束后再移到
DIR。前提是DIR在工作目录与会话临时目录之外。理由:pi 与 dsh 整个进程在沙箱里,让它直接写DIR就要把DIR放进沙箱的可写范围,agent 就能改写其他会话的记录。claude-code 与 codex 的记录由运行时进程自己写,不经过 agentrun 的沙箱,agent 的工具写不到DIR。 - 复制会话目录时只复制普通文件与目录,跳过符号链接和其他类型的条目;claude-code 的
<id>/是符号链接时不保存它;dsh 的session-<id>/与它所在的<由 cwd 转换的目录名>/是符号链接时不保存,DIR/dsh/session-<id>是符号链接时--resume报找不到会话。理由:dsh 的记录目录在沙箱里可写,agent 能在里面放指向沙箱外文件的链接,agentrun 在沙箱外复制时如果跟随链接,就会把 agent 读不到的文件复制进会话记录,下一次运行时 agent 就能读到。
doctor 检查
agentrun doctor [运行时...] [--model ID] [--sandbox MODE] [--network MODE] [--allow-host HOST]... [--path DIR]... [--env KEY[=VALUE]]... [--env-file FILE]... [--json] [--debug]
按当前环境检查运行时能否使用。不写运行时时检查全部四个,顺序固定为 claude-code、codex、pi、dsh。各选项的含义与运行时子命令相同,保证检查的条件与实际运行一致;--model 只由 pi 与 dsh 的检查使用:pi 的登录检查把它与实际使用的模型比较,dsh 的登录检查用它调用一次模型;claude-code 与 codex 的登录检查不调用模型,不检查模型名是否存在;--path DIR 把目录加到会话 PATH 的前面,可以重复,目录不存在时按用法错误处理。开始检查前,doctor 对每个要检查的运行时调用适配器的 check_args,参数不符合要求时按用法错误处理,例如 pi 的 --model 格式。不写运行时时四个运行时都要通过这项检查,所以 doctor --model sonnet 会因为 pi 要求 provider/model 格式以退出码 2 结束,只检查 claude-code 时要写成 doctor claude-code --model sonnet。
| 检查项 | 通过条件 |
|---|---|
可执行文件(executable) | 在会话 PATH 里找到,并且版本不低于运行时版本一节的最低版本。找到后运行 <运行时> --version(codex 用私有 CODEX_HOME),先在标准输出、再在标准错误里找第一个 X.Y.Z 形式的版本号,v 前缀与 - 之后的预发布后缀不参与比较。说明里写出路径与版本,例如 /opt/node22/bin/claude 2.1.303;低于最低版本时为 fail,说明写成 <路径> 2.1.287, requires >= 2.1.288。命令启动失败、超时或退出码不为 0 时为 fail,退出码不为 0 时说明写成 <路径>: --version exited with code 1: <一行输出>,这行输出依次取标准错误里第一个形如 TypeError: 的错误行、标准错误的第一行、标准输出的第一行,都没有时只写 exited with code 1,被信号终止时写 terminated by a signal;不在输出里找版本号:npm 安装的运行时是 node 脚本,node 启动失败时标准错误的最后一行是 node 自己的版本,例如 Node.js v22.22.0。退出码为 0 但找不到版本号时为 ok,说明写成 <路径>, version not recognized: <输出的第一行>,因为解析不出版本不能说明运行时不能用。版本过低时其余三项照常检查 |
登录(login) | 运行时能用当前环境登录。先按凭据一节写入环境变量给出的凭据。claude-code 运行 claude auth status,读它输出的 JSON 里的 loggedIn;设了 CLAUDE_CODE_OAUTH_TOKEN 时说明写这个变量名,否则写 authMethod。codex 先检查登录文件存在,再用私有 CODEX_HOME 运行 codex login status,退出码 0 为通过。pi 用 --mode json 启动一次,提示词与运行时一样写入标准输入,读到 assistant 的 message_start 事件就终止它,不等模型回复;事件里的 provider 与 model 是 pi 实际选中的服务商与模型,给了 --model 时与它比较,否则与 pi 设置里的默认模型比较,不一致时失败。pi 对不存在的模型名只在 stderr 上警告并照用,这个检查把该警告判为失败。dsh 先检查会话环境里有 DEEPSEEK_API_KEY,没有时不启动 dsh,说明与运行时子命令的启动前检查相同;然后用与实际运行相同的参数、私有 DSH_HOME 与补丁启动一次,--effort 固定为 off,提示词写入标准输入,读到第一个带 usage 的 step_end 就终止它并通过,说明写 DEEPSEEK_API_KEY,给了 --model 时加上括号里的模型名;先读到 kind 为 error 的 turn_end 或 error 事件时失败,说明是 code: message,例如 AUTH: ...、INVALID_REQUEST: ...。dsh 在模型回复完一步之前没有成功的信号,所以这项检查要等一次模型请求(实测约 3 秒)。dsh 的 TMPDIR、TMP、TEMP 指向 doctor 临时目录里的 dsh-login/,理由:dsh 被终止时来不及删除它在临时目录里建的 dsh-subprocess-* 目录,这些目录随 doctor 的临时目录一起删除 |
沙箱(sandbox) | 在一个临时目录里,用每个运行时实际使用的沙箱执行一条写文件的命令,确认命令执行了、目录内的文件写出、目录外写不进去,并且读不到目录外一个加进不可读列表的文件(失败说明为 a read of a denied path succeeded)。claude-code 的沙箱由它自己在进程内启动,无法单独调用,所以用 agentrun 给 pi 用的包裹代替:两者在 Linux 上都用 bwrap 与 socat,在 macOS 上都用 Seatbelt,检查的是同一组程序能否在当前环境启动。pi 与 dsh 用同一个包裹,并额外在包裹下运行一次 pi --version 或 dsh --version,确认运行时与它依赖的 node 能在沙箱里启动,说明里加 pi started 或 dsh started。codex 用 codex sandbox 执行同一条命令,文件系统与网络配置同实际运行。检查结束后删除该目录,给了 --debug 时保留并打印路径,保留前删除目录里指向登录文件的链接。--sandbox relax 下沙箱不可用时这一项为 skip,off 时不检查 |
网络(network) | 只在 --network full|custom 时检查,且沙箱检查没有运行时为 skip。启动过滤代理,在该运行时实际使用的沙箱里对一个允许的主机发 CONNECT 请求确认能连上,再对一个不允许的主机确认被拒绝。custom 时允许的主机是第一条 --allow-host(通配规则取 www. 加域名),不允许的主机是 doctor-check.invalid:443,要求拒绝原因为 not_allowed。full 时允许的主机是该运行时的模型服务主机(claude-code 为 api.anthropic.com,codex 为 chatgpt.com,pi 为 --model 的服务商,没给时为 pi 设置里的 defaultProvider,与实际运行时相同,取它在模型服务主机表里的主机,表里没有时用 api.anthropic.com;dsh 为 DEEPSEEK_BASE_URL 的主机,没设时为 api.deepseek.com),不允许的主机是 169.254.169.254:80,要求拒绝原因为 private_address。两次连接都由 agentrun 自己完成:在沙箱里运行 agentrun doctor-connect HOST PORT,它按环境里的代理变量经过滤代理连接,退出码 0 表示连上。codex 的网络由它自己的代理控制:full 时不启动过滤代理,不拦截私有地址,只检查允许的一半;custom 时 codex 的代理先按放行列表判断,放行的请求转给过滤代理;不允许的主机由 codex 的代理直接拒绝,过滤代理看不到这个请求,这时要求 codex 的代理回复 403 且带响应头 x-proxy-error: blocked-by-allowlist,过滤代理看到了请求时与其他运行时一样要求记录 not_allowed。macOS 上 codex 的 custom 检查失败且主机解析到 198.18.0.0/15 时,说明里指出这是 fake-ip,需要关掉本机代理软件的 fake-ip 模式 |
每项结果为 ok、fail 或 skip。每个外部命令最多等 30 秒,超时按 fail 记并写明;doctor-connect 连接代理或目标最多等 10 秒。可执行文件没找到时,登录为 skip;沙箱与网络对 claude-code、pi 与 dsh 仍然检查,因为它们用的是 agentrun 自己的包裹与 doctor-connect,pi 与 dsh 的沙箱检查这时不启动运行时,说明里加 pi not started (executable not found) 或 dsh not started (executable not found);对 codex 两项都为 fail,说明为 executable not found,因为它的沙箱与代理就是 codex 本身。默认输出每项一行,四列对齐;--json 输出一个数组,每项是 {runtime, check, status, detail},被信号中断时输出已完成的项。沙箱检查失败时,说明里写明可能的原因与修正方法。
前两条命令在没有用户命名空间权限的 docker 容器里运行,第三条在 Cloud Managed 容器里运行。下面是 dsh 在 Cloud Managed 容器里的结果,dsh 的路径改成了短路径:
退出码
| 退出码 | 运行时子命令 | doctor |
|---|---|---|
| 0 | finished | 所有被检查的项都是 ok 或 skip |
| 1 | failed | 有 fail 的检查项 |
| 2 | rejected:启动前失败 | 用法错误、--env-file 读取失败或凭据写入失败 |
| 3 | timeout | 不出现 |
| 4 | limit:超过 --max-turns 或 --max-tokens | 不出现 |
| 130 | interrupted(SIGINT) | 被中断 |
| 143 | interrupted(SIGTERM) | 被终止 |
调用方只看退出码就能区分「环境未配置好」(2)和「agent 没有完成任务」(1)。退出码为 2 时,调用方不改环境直接重试不会成功。状态的判定顺序见end.status。
运行时版本
下面是 agentrun 要求的最低版本,agentrun doctor 的 executable 一项按它检查,见doctor 检查。最低版本是各运行时模块里的常量 MIN_VERSION,改这个列表时同时改常量与快速开始的表,单元测试会比较三处是否一致。运行时子命令启动前不检查版本,start 事件也不带版本:运行开始前拿到版本要多启动一次运行时。
- claude-code: >= 2.1.288
- pi: >= 1.0.1,npm 包
@earendil-works/pi-coding-agent - codex: >= 0.159.2,权限配置与
--ignore-user-config需要这个版本 - dsh: >= 0.2.1-rc.2,npm 包
@deepseek-ai/dsh。npm 的默认标签latest指向更早的 0.2.0-rc.2,要写明版本安装。dsh 要求 Node^22.19.0 || >=24.0.0
claude-code
参数
<claude 的绝对路径> -p --output-format stream-json --verbose
--input-format stream-json --replay-user-messages
--permission-mode dontAsk|auto
--setting-sources ""
--strict-mcp-config
[--no-session-persistence]
--session-id <新生成的 UUID v4> | --resume <保存的会话文件>
--tools Read,Edit,Write,Glob,Grep,Bash,Task
--allowedTools <放行列表>
[--settings <权限与沙箱设置>]
[--model ID] [--effort LEVEL]
<-- 之后的调用方参数>
--setting-sources ""不加载用户、项目与本地设置,工作目录里的CLAUDE.md不进入模型上下文(实测)。因此沙箱设置只来自--settings。- 会话环境设置
CLAUDE_CODE_DISABLE_AUTO_MEMORY=1,关闭 auto-memory。理由:--setting-sources ""不关闭它,claude-code 仍把用户的~/.claude/projects/<项目>/memory/MEMORY.md放进模型上下文;设置这个变量后,请求里不再有这个文件的内容与 memory 的说明(claude 2.1.288 与 2.1.303 云端实测)。这个变量在--env之后设置,调用方不能用--env改回。 --strict-mcp-config不加载任何 MCP 服务器。- 新会话的
--session-id每次新生成。理由:在 Claude Code 会话里运行 agentrun 时,claude -p子进程会继承父会话的 id,把记录写进父会话,只加--no-session-persistence不能避免。没有给出--session-dir时另加--no-session-persistence;给出时不加,继续会话时用--resume代替--session-id,见保存与继续会话。 --tools限定可用的内置工具,去掉网页工具与技能等。--allowedTools放行列出的工具,不经权限判断。Bash 不在放行列表里:沙箱打开时由autoAllowBashIfSandboxed自动放行,沙箱没有运行时由 auto 模式逐条判断。只用--tools而不放行时,auto 模式会拒绝 Write,且结果仍报告成功(实测)。- 沙箱打开时权限模式是
dontAsk,放行列表是Read,Glob,Grep,Task加上Edit(//<cwd>/**)、Write(//<cwd>/**)、Edit(//<会话临时目录>/**)、Write(//<会话临时目录>/**);沙箱没有运行时权限模式是auto,放行列表是Read,Edit,Write,Glob,Grep,Task。理由见沙箱与网络。规则里的绝对路径以//开头,只写一个/时规则不匹配(实测);路径里的\ * ? [ ] { }前面加反斜杠,否则工作目录名里的[1]这类字符被当作通配符,Write 写不进工作目录(claude 2.1.303 云端实测)。 - 启动 subagent 的工具在参数与
system/init的工具列表里叫Task,在输出的tool_use里叫Agent,两个名字都要识别。--no-subagents从--tools与--allowedTools里去掉Task,生效与否看 init 的工具列表里有没有Task。 --network full时两个列表都加上WebFetch与WebSearch;custom时--tools加WebFetch,--allowedTools每个主机加一条WebFetch(domain:HOST)。- 提示词从标准输入传入,不在命令行里:写入一行 JSON
{"type":"user","message":{"role":"user","content":"<提示词>"}},然后关闭标准输入。--replay-user-messages让 claude-code 把这条消息回显到输出里,agentrun 据此输出prompt事件。不加这两个选项时,输出里没有提示词。实测关闭标准输入后进程正常结束(退出码 0);多行提示词与约 200 KB 的提示词,回显与原文逐字相同;收到 SIGINT 时,回显在中断前已经输出。 - 给了
--image时,content换成数组:每张图片一个 image 元素,按给出的顺序排列,最后是提示词的 text 元素。回显里的 text 元素就是提示词原文。
claude-code 2.1.303 实测:四种类型都能让模型读出图中的文字;48 MB、4000×4000 像素的 PNG 由 claude-code 缩小后交给模型,不报错。{"type":"user","message":{"role":"user","content":[ {"type":"image","source":{"type":"base64","media_type":"image/png","data":"<base64>"}}, {"type":"text","text":"<提示词>"} ]}}
--settings 在沙箱打开、或者不可读列表不为空时传。permissions 只在不可读列表不为空时写,autoMemoryDirectory 与 sandbox 只在沙箱打开时写:
{
"permissions": {
"deny": ["Read(//<路径>)", "Read(//<路径>/**)", ...]
},
"autoMemoryDirectory": "<会话临时目录>/claude-memory",
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"autoAllowBashIfSandboxed": true,
"allowUnsandboxedCommands": false,
"filesystem": {
"allowWrite": ["<cwd 的绝对路径>", "<会话临时目录>"],
"denyRead": ["<路径>", ...]
},
"network": {
"allowedDomains": []
}
}
}
autoMemoryDirectory 把 auto-memory 目录改到会话临时目录里。理由:claude-code 对当前项目 memory 目录里的 .md 文件内置放行 Write 与 Edit,不看 auto-memory 是否关闭,也不受 dontAsk 与放行规则限制;不改目录时,沙箱打开的运行仍能写用户的 MEMORY.md,内容会进入用户之后的 claude 会话。改到会话临时目录后,写用户的 memory 目录被拒绝(claude 2.1.288 与 2.1.303 云端实测)。沙箱没有运行时 Write 本来就能写任何位置,所以只在沙箱设置里改。
allowWrite 与空的 allowedDomains 生效:Bash 能写 allowWrite 列出的目录,不能写 /tmp 的其他位置,不能联网(云端实测)。failIfUnavailable 不能发现沙箱启动失败:PATH 里的 bwrap 启动失败时,claude-code 不报错,每条 Bash 命令以退出码 1 失败,整次运行仍报告成功(云端实测)。所以沙箱能否使用只靠 agentrun 的启动前检查判断,这一项保留作为额外的保护。--network full|custom 时 network 改为 {"allowedDomains":[],"httpProxyPort":<代理端口>,"socksProxyPort":<代理端口>},两个端口都是过滤代理的 TCP 端口(过滤代理按第一个字节区分 HTTP 与 SOCKS5);--dry-run 的命令里端口写成 <proxy port>。deny 与 denyRead 的路径来自读取限制,路径是符号链接时链接与目标都写;不可读列表为空时没有 denyRead。
输出映射
stream-json 每行一个 JSON 对象。用到的对象与字段:
| 原始事件 | 用到的字段 | 转成 |
|---|---|---|
system,subtype init | permissionMode、model、tools | 不输出;用于检查 --no-subagents |
assistant | message.id、message.model、message.content[]、message.usage、parent_tool_use_id | tool_use 块转成 tool_start 并记为未结束的调用(Agent 或 Task 转成 subagent_start);text 块转成 text;thinking 块忽略;同一个 parent_tool_use_id 下出现新的 message.id 时记为一次模型调用开始(CallStart),每条消息的 message.usage 输出为这次调用到当时为止的用量(CallUsage),见轮数与 token 上限 |
system,subtype permission_denied | tool_use_id、tool_name、decision_reason | 把对应的调用标为被拒绝;它出现在对应的 tool_result 之前,那个 tool_result 的 is_error 为 true |
user,isReplay 为 true | message.content | prompt,只取第一条。它在 system/init 之后、第一个 assistant 之前出现,parent_tool_use_id 为 null。之后出现的 isReplay 消息是后台 subagent 交回的结果(内容以 <agent-message 开头),只写入 --raw |
user,其他 | tool_result 块的 tool_use_id、is_error、content | 对应的调用结束,输出 tool;error 取 is_error。调用成功时 tool_result 没有 is_error 字段,这时 error 为 false。is_error 为 true、content 以 <tool_use_error> 开头并且含 denied by your permission settings 或 Read deny rule in your permission settings 时也标为被拒绝:Read 被拒绝规则挡住、Write 写被拒绝读取的路径时只返回这条工具错误,没有 permission_denied(claude 2.1.303 云端与 macOS 实测) |
system,subtype task_notification | tool_use_id、status、usage.duration_ms | subagent_end;status 为 completed 时 finished,其他值 failed |
result | is_error、subtype、result、modelUsage、total_cost_usd | end 的 result、失败判定与 usage |
其他,例如 stream_event、rate_limit_event、system/status、task_started、task_progress | 只写入 --raw |
parent取所在 assistant 消息的parent_tool_use_id;它不是任何subagent_start的 id 时按 null 处理。- subagent:
kind取 input 的subagent_type,缺省为general-purpose;model取 input 的model;description取 input 的description第一行。subagent 默认在后台运行,启动后立即有一个status为async_launched的 tool_result,这个结果不输出事件。前台运行的 subagent 没有task_notification,在收到它的 tool_result 时输出subagent_end,is_error为 true 时 failed。subagent 交回结果用的内部工具SubagentHandback不输出tool。 - usage:claude-code 把一次模型调用拆成多条 assistant 消息(thinking、tool_use、text 各一条),它们的
message.id相同,每条都带 usage。同一个 id 只输出一个usage,取最后一条的值,time取输出时的时间。合并按parent_tool_use_id分别进行:主 agent 与每个 subagent 各保存一条待输出的用量,某个 parent 出现新的message.id时输出它自己的那条;输出subagent_end之前与运行时结束时输出全部待输出的用量。理由:多个 subagent 并行时,它们的 assistant 消息交错出现,只按 id 的先后判断会把一次调用拆成多个usage。字段:input_tokens、output_tokens、cache_read_input_tokens、cache_creation_input_tokens;这里没有费用,cost_usd为 null。model 为<synthetic>的消息不输出usage,它的文字照常输出。 - 一次运行可能有多个 result:subagent 在后台运行时,主 agent 每收到一个 subagent 的结果就多一轮,每轮一个 result,
modelUsage是到当时为止的累计值。所有用到 result 的地方都取运行时退出前的最后一个。end.usage由它的modelUsage按模型换算,costUSD对应各模型的cost_usd,总量的cost_usd取total_cost_usd。
| 工具 | summary |
|---|---|
| Bash | Bash: 加 command 的第一行 |
| Read、Edit、Write | 工具名加 file_path;在 --cwd 之内时写相对路径,等于 --cwd 时写 . |
| Glob、Grep | 工具名加 pattern |
| WebFetch | 工具名加 url |
| 其他 | 只写工具名 |
失败判定
以下任一成立即为 failed,detail 按顺序取第一项的信息:
- 最后一个 result 的
is_error为 true:取它的result文字。例如无效模型(同时退出码 1、标准错误有[claude-code:unrecognized_model])、无效凭据(退出码 1)。 - 退出码非 0:取标准错误的最后 500 个字符。例如未知选项,没有任何事件。
- 退出码 0 但没有 result:
runtime exited with code 0 without a result event。
收到 SIGINT 时 claude-code 约 0.4 秒退出,退出码 0,最后的 result 为 is_error: true、subtype error_during_execution;SIGTERM 时退出码 143、没有 result。两种情况都由 agentrun 记为 interrupted。
auto 模式的分类器会拒绝部分命令。实测稳定被拒绝的是读取凭据的命令,例如 cat ~/.ssh/id_rsa;用 curl 把文件外发会触发模型的安全拦截,整次运行以 API Error 失败。
pi
参数
<pi 的绝对路径> -p --mode json
--no-session | --session-dir <临时目录>/pi-session | --session <临时目录>/pi-session/<文件名>
--no-extensions --no-skills --no-prompt-templates --no-themes --no-context-files
--no-approve --offline
--tools read,bash,edit,write,grep,find,ls
[--model 服务商/模型] [--thinking LEVEL]
<-- 之后的调用方参数> [-- @<图片副本>...]
--no-context-files不读AGENTS.md与CLAUDE.md;--no-approve不信任工作目录里的项目级配置,因为工作目录 agent 可写,项目级配置能在 pi 进程内执行代码;--offline启动时不联网下载资源。- 提示词原文写入标准输入后关闭,不放在命令行里。理由:pi 把以
@开头的位置参数当成文件路径,写在--之后也一样,文件不存在时报File not found退出,文件存在时把文件内容当成提示词。pi 去掉标准输入内容首尾的空白(pi 1.0.1 与 1.2.0 相同),所以模型收到的提示词与prompt事件都没有首尾的空白与空行,中间的换行与缩进不变;claude-code 与 codex 收到的是原文。调用方参数里的位置参数由 pi 当成消息:第一个直接接在提示词后面,中间不加分隔,之后的每个各算一轮新的消息,以@开头的被当成文件。所以调用方参数里只写选项。 - 给了
--image时,命令最后加上--与每个图片副本的@<路径>。pi 按文件内容识别出图片,把它作为图片元素放进用户消息,并在提示词之后为每个文件加一段<file name="<路径>"></file>;按模型的限制缩小图片时,还在最后追加一行尺寸说明(pi 1.2.0 实测,48 MB、4000×4000 像素的 PNG 被缩小到 2000×2000)。 --effort传成--thinking。--no-subagents不起作用,也不报错。- 沙箱打开时,整条命令前面加上包裹命令,见沙箱与网络。Linux:
macOS 是bwrap --ro-bind / / --bind <cwd> <cwd> --bind <临时目录> <临时目录> --bind <pi 状态目录> <pi 状态目录> --ro-bind <状态目录里已有的条目> <同一路径>... --bind <auth.json 的链接目标> <同一路径> --tmpfs <不可读的目录> --remount-ro <同一路径>... --ro-bind /dev/null <不可读的文件>... --dev /dev --proc /proc --die-with-parent --unshare-net -- /bin/sh -c '"$0" "TCP-LISTEN:$1,bind=127.0.0.1,fork,reuseaddr" "UNIX-CONNECT:$2" 2>/dev/null & shift 2; exec "$@"' <socat> <代理端口> <临时目录>/proxy.sock <pi 的命令>/usr/bin/sandbox-exec -f <临时目录>/seatbelt.sb <pi 的命令>。状态目录的三组参数见沙箱与网络:--ro-bind对目录里除五个可写条目之外的每个已有条目各一组,auth.json不是符号链接时没有最后一组--bind;算不出状态目录(PI_CODING_AGENT_DIR与HOME都没有)时这三组都没有,--dry-run下目录不存在时也没有。不可读的路径见读取限制,不存在的路径没有对应的参数。Linux 的sh先在后台启动socat,把沙箱里127.0.0.1上与过滤代理相同的端口号转发到会话临时目录里的 Unix socket,再用exec换成 pi,进程组与退出码不变;pi 退出后进程组收到 SIGKILL,socat 一起结束。socat 的标准错误指向/dev/null:沙箱里的进程中途关闭连接时 socat 会打印一行 read 错误,这些行会混进 pi 的标准错误输出;转发本身不会启动失败,因为命名空间里只有回环接口,socket 文件在启动 pi 之前已经存在。 - 环境里不另设变量。pi 按
PI_CODING_AGENT_DIR、未设时~/.pi/agent使用用户的状态目录,见pi 使用用户的状态目录。
模型检查
--model 必须写成 服务商/模型,可以带 :推理强度 后缀;没有 / 或任一侧为空时是用法错误。理由:pi 对模型名做模糊匹配,能匹配上的未知模型名会被换成别的服务商的模型,退出码仍为 0。例如环境里有 AWS_ 变量时,deepseek/deepseek-v4-flash(1.0.1 已改名为 deepseek-flash)被换成了 Amazon Bedrock 上的模型。
给了 --model 时,agentrun 在第一个 assistant 的 message_start(此时还没有请求模型)比较它的 provider 与 model,区分大小写,不做别名处理。不一致时向进程组发 SIGKILL,判为 failed,detail 为 pi uses <provider>/<model>, which does not match --model <值>。
没给 --model 时不传,pi 用它 settings.json 里的默认模型。设置里的模型不存在、或者没有设置文件时,pi 同样会不提示地改用环境里其他可用的服务商。这种情况由 doctor 的登录检查发现:它报告 pi 实际选中的服务商与模型,与设置不一致时失败。
输出映射
--mode json 每行一个事件,只用以下几种,其余(session、message_update、turn_end、agent_end 等)只写入 --raw。不用 turn_end 与 agent_end,因为它们重复 message_end 的内容,会重复计算用量。
| 原始事件 | 转成 |
|---|---|
tool_execution_start | tool_start:id 为 toolCallId,name 为 toolName,parent 为 null,summary 由 args 计算 |
tool_execution_end | tool:id 为 toolCallId,name 为 toolName,parent 为 null,denied 为 false,error 取 isError,没有这个字段时为 null。args 不在这个事件里,从对应的 tool_execution_start 取。同一条消息里的多个调用并行执行,结束顺序可能与开始顺序不同 |
assistant 的 message_end | text:全部 text 元素按顺序用换行连接,不输出 thinking;之后输出 usage:model 为 provider/model,input、output、cacheRead、cacheWrite 对应四个字段(input 不含缓存命中,实测),cost.total 对应 cost_usd;reasoning 与 cost 的其他分项不进统一事件 |
user 的 message_end | prompt:全部 text 元素按顺序用换行连接。给了 --image 时,文字以去掉首尾空白的提示词开头、后面是 pi 加的文件段与尺寸说明,这时只取去掉首尾空白的提示词;开头不是它时原样输出 |
assistant 的 message_start | 不输出;记为一次模型调用开始,见轮数与 token 上限;第一个用于模型检查 |
summary:bash 写 command 的第一行;read、write、edit、ls 写 path(ls 没有 path 时写 .),path 是 --cwd 之内的绝对路径时写相对路径,等于 --cwd 时写 .,与 claude-code 的 Read、Edit、Write 相同(display_path);grep、find 写 pattern;其他只写工具名。end.result 是最后一个 text。
失败判定
以下任一成立即为 failed,detail 按顺序取第一项的信息:
- 模型检查不通过。
- 沙箱运行,且标准错误里有包裹命令开头的行(Linux 是
bwrap:,macOS 是sandbox-exec:)、退出码非 0:包裹命令启动失败,detail为sandbox failed to start:加该行去掉这个前缀后的内容。 - 退出码非 0:取标准错误的最后 500 个字符。
- 最后一条 assistant 消息的
stopReason为error或aborted:取errorMessage的前 500 个字符,没有时写stopReason: 值。 - 退出码 0 但没有 assistant 的
message_end:pi produced no model reply。
| 实测情况 | 退出码 | 输出 |
|---|---|---|
| 正常完成 | 0 | 最后的 message_end 的 stopReason 为 stop |
| 模型调用失败(无效 key、连不上服务) | 0 | stopReason 为 error,errorMessage 例如 401: {...}、Connection error. |
| 没有所选服务商的 key | 1 | 只有 session 事件;标准错误 No API key found |
| 模型不存在、未知选项 | 1 | 没有事件;标准错误 Error: ... |
| SIGINT(包裹与不包裹相同) | 130 | 最后没有 message_end 与 agent_end |
| SIGTERM(pi 1.2.0 Linux 与 1.0.2 macOS,包裹与不包裹相同) | 143 | 最后没有 message_end 与 agent_end |
codex
参数
<codex 的绝对路径> exec --json --skip-git-repo-check
--ignore-user-config --ignore-rules -C <cwd>
[--image <图片副本>]...
-c default_permissions=agentrun
-c 'permissions.agentrun.filesystem={":root"="read", ":workspace_roots"={"."="write"}, "<会话临时目录>"="write", "<不可读的路径>"="deny", ...}'
-c permissions.agentrun.network.enabled=false
-c approval_policy="never" -c web_search="disabled" -c project_doc_max_bytes=0
-c skills.include_instructions=false -c skills.bundled.enabled=false -c allow_login_shell=false
--disable apps --disable plugins --disable remote_plugin --disable plugin_sharing
--disable tool_suggest --disable hooks --disable skill_mcp_dependency_install
--disable image_generation --disable goals --disable memories --disable shell_snapshot
--disable computer_use --disable browser_use --disable browser_use_external
--disable in_app_browser --disable daemon_auto_start
[-m ID] [-c model_reasoning_effort="LEVEL"]
<-- 之后的调用方参数> [resume <会话 id>] -
- 环境里
CODEX_HOME指向私有目录,见私有配置目录;它不出现在 argv、start.env与set:行里。最后的-表示从标准输入读提示词,提示词原文写入标准输入后关闭。 - 沙箱打开时用名为
agentrun的自定义权限配置,会话临时目录写真实路径(macOS 上/var指向/private/var)。filesystem表最后给不可读列表里每个存在的路径加一项"<真实路径>"="deny",见读取限制。权限参数的三行按沙箱与--network取值如下,web_search只在full下为"live":
| 沙箱 | --network | 权限参数 |
|---|---|---|
| 运行 | none | 上面的三行:default_permissions=agentrun、permissions.agentrun.filesystem={...}、permissions.agentrun.network.enabled=false |
| 运行 | full | 前两行同上,第三行为 -c permissions.agentrun.network.enabled=true |
| 运行 | custom | 前两行同上,第三行换成 --enable network_proxy -c 'permissions.agentrun.network={enabled=true, enable_socks5=false, enable_socks5_udp=false, allow_upstream_proxy=true, domains={"<主机1>"="allow", ...}}'。domains 写每条 --allow-host 的主机部分,不写端口,按给出的顺序去重,通配写成 *.example.com(codex 只匹配子域名,与 --allow-host 相同,codex 0.159.2 实测);端口限制由过滤代理执行。不写 allow_local_binding,理由见网络 |
| 不运行 | 任意 | -c default_permissions=":danger-full-access" |
- 写进
-c值的字符串按 TOML 基本字符串写:外加双引号,\写成\\,"写成\",控制字符写成\uXXXX。用于会话临时目录路径、--effort的值与主机名。 --dry-run时会话临时目录写成<tempdir>,私有目录写成<tempdir>-codex(只在环境里,不在 argv 里)。- 登录文件的检查与私有目录的创建由
Codex::launch做。用户登录文件($CODEX_HOME/auth.json,未设时为~/.codex/auth.json)不存在、也没有给出AGENTRUN_CODEX_AUTH时启动前报错,--dry-run也检查,detail为codex login file <路径> not found. Run codex login, or pass its content in AGENTRUN_CODEX_AUTH。私有目录创建失败的detail为cannot create the codex home directory <路径>: <系统错误>。 - 不加
--ephemeral:加了以后 subagent 无法启动(报no rollout found for thread id,codex 0.159.2 实测)。会话记录写进私有目录,随它一起删除;给出--session-dir时先移到DIR/codex/,见保存与继续会话。resume子命令没有自己的-C,写在它前面的-C、-c、--disable、-m照常生效(0.159.2 与 0.163.0 实测)。 --disable列表关闭会给模型增加工具或在 codex 进程里执行用户代码的功能。shell_snapshot与allow_login_shell=false让命令不读用户的 shell 配置。其中computer_use、browser_use、daemon_auto_start等在 exec 下没有可见效果,仍然关闭。功能名不存在时 codex 报错退出,所以这份列表要随 codex 版本核对。--no-subagents加-c agents.enabled=false --disable multi_agent --disable multi_agent_v2。只关multi_agent不够,模型目录里的设置优先(codex 0.159.2 源码)。--max-turns与--max-tokens是用法错误,见轮数与 token 上限。--deny-read的真实路径含* ? [ ] { }时是用法错误,见读取限制。- 给了
--image时,每个图片副本一组--image <路径>,紧跟在-C <cwd>之后(codex 0.160.1 实测 PNG、JPEG、GIF、WebP 与多张图片)。codex 的--image接受一个或多个值,并把逗号当作值之间的分隔符(codex 源码codex-rs/utils/cli/src/shared_options.rs),所以它后面必须紧跟另一个选项,否则最后的-会被当成图片;路径里有逗号时 codex 会把它拆成两个文件,这时启动前报错,detail为cannot give images to codex: the path <路径> contains ',', which codex reads as a list separator。副本路径在会话临时目录里,只有$TMPDIR含逗号时才会出现这个错误。
输出映射
| 原始事件 | 转成 |
|---|---|
| 没有对应的原始事件 | prompt:agentrun 在 start 之后用自己传入的提示词输出。codex 的输出里没有提示词(codex 0.159.2 实测) |
item.started,command_execution | tool_start:name 为 command_execution,summary 为 shell: 加命令第一行 |
item.started,file_change | tool_start:name 为 file_change,summary 为 patch: 加全部路径,空格分隔 |
item.started,mcp_tool_call | tool_start:name 为 mcp_tool_call,summary 为 mcp: server.tool |
item.completed,command_execution、file_change、mcp_tool_call | tool,id 为 item 的 id。这个 id 之前没有 item.started 时,先按上面三行由这个 item 输出 tool_start,再输出 tool。error:exit_code 不为 0 或 status 为 failed 时为 true;status 为 completed,或者没有 status 而 exit_code 为 0 时为 false;其余情况为 null,例如 status 为 declined。只有 command_execution 有 exit_code |
item.completed,agent_message | text |
turn.completed | usage:input_tokens 减去 cached_input_tokens 为输入,cached_input_tokens 对应 cache_read_tokens,cache_write_input_tokens 对应 cache_write_tokens,output_tokens 为输出。input_tokens 含缓存部分按 OpenAI 用量的含义处理,样本里 cached_input_tokens 小于 input_tokens。事件里没有模型名,model 为 --model 的值,没给时为 null;codex 不报告费用,cost_usd 为 null |
其他(collab_tool_call、reasoning、todo_list、web_search,以及其余类型的 item.started 等) | 只写入 --raw |
denied 恒为 false。codex 的输出里没有 subagent 的启动事件,也没有 subagent 自己的命令与文件改动,只有主 agent 等待 subagent 的 collab_tool_call(tool 为 wait,codex 0.159.2 实测)。所以 codex 不输出 subagent_start 与 subagent_end,parent 全部为 null。
失败判定
- Linux 上沙箱运行时,有
item.completed里command_execution的aggregated_output以bwrap:开头:detail 为sandbox failed to start:加这一行去掉bwrap:的部分,有多条时取第一条。启动前检查已经覆盖已知的失败情况,这一条是附加信号:codex 的沙箱启动失败时仍以退出码 0 结束并输出turn.completed,只有命令的输出里有bwrap的报错;模型改用file_change写文件时连这条报错也没有(codex 0.160.0 实测)。 turn.failed:取最后一个的error.message前 500 个字符。- 没有
turn.completed时的error事件:取最后一个的message前 500 个字符。codex 重试连接时也输出error,重试成功后照常完成,所以有turn.completed时不看error。 - 退出码非 0:取标准错误的最后 500 个字符。
- 退出码 0 但没有
turn.completed:codex produced no turn.completed。
| 实测情况(未注明的为 codex 0.159.2,macOS) | 退出码 | 输出 |
|---|---|---|
| 正常完成 | 0 | 以 turn.completed 结束 |
| 模型不存在 | 1 | error 与 turn.failed,消息为服务端的 400 错误 |
| 无效凭据 | 1 | 约 10 个 error(重试)后 turn.failed,消息含 401 Unauthorized |
| 未知选项 | 2 | 没有事件;标准错误 error: unexpected argument ... |
| SIGINT | 1 | 最后是 item.completed,没有 turn.completed |
| SIGTERM | 143 | 同上 |
| SIGTERM(codex 0.160.0,Linux) | 0 | 同上 |
| 沙箱启动失败(codex 0.160.0,本机 Linux 未放行 AppArmor 与 docker 默认权限) | 0 | 以 turn.completed 结束,没有 error 与 turn.failed;失败的 command_execution 的 aggregated_output 是 bwrap: 开头的报错 |
dsh
参数
<dsh 的绝对路径> headless --patch <临时目录>/dsh-patch.yml --json
[--session-id session-<会话 id>]
<-- 之后的调用方参数> -
headless是 dsh 的非交互 profile,--json让它每行输出一个事件,最后的-表示从标准输入读任务;提示词原文写入标准输入后关闭。--patch是 dsh 启动器的选项,必须写在--json之前,写在后面时 dsh 报unknown option '--patch'。提示词不放在命令行里,理由:以-开头的位置参数被 dsh 当成选项,dsh 也不接受--(0.2.1-rc.2 实测)。调用方参数写在--json之后、-之前,只写 headless 的选项。- 环境里
DSH_HOME指向会话临时目录里的dsh-home/,DSH_AGENTS_HOME指向空目录dsh-agents/,两个目录在启动前创建,权限 0700;另设DSH_PERMISSION_MODE=danger-full-access与DSH_TELEMETRY_DISABLED=1。这四个变量在--env之后设置,调用方不能用--env改回。调用方环境里以DSH_开头的变量启动前删除,见会话环境;私有目录关掉了哪些配置来源,见配置与账号。 DSH_PERMISSION_MODE=danger-full-access关掉 dsh 自带的沙箱与审批,写限制只由 agentrun 的包裹执行;沙箱没有运行时同样这样设置,与 codex 在沙箱没有运行时用:danger-full-access一致(用户定)。理由:dsh 自带的沙箱只限制写文件,不限制网络;默认的workspace-write下 bash 命令的/tmp在命令结束后消失,需要审批的操作在 headless 里没有人回答,直接失败;macOS 上 dsh 在 agentrun 的 Seatbelt 里用workspace-write时每条 bash 命令都报no sandbox backend is usable on this host(0.2.1-rc.2 在云端与 macOS 实测)。- 会话环境里必须有非空的
DEEPSEEK_API_KEY,没有时启动前拒绝,--dry-run也检查,detail为DEEPSEEK_API_KEY is not set in the session environment. Export it, or pass it with --env DEEPSEEK_API_KEY=<key>。理由:会话环境没有这个变量时,dsh 读工作目录里.env的 key,agent 能写工作目录,下一次运行的账号就由 agent 决定;私有DSH_HOME里没有用户的~/.dsh/.env(用户定)。DEEPSEEK_BASE_URL照常进入会话,dsh 用它访问兼容的模型服务。 - 补丁文件
dsh-patch.yml写在会话临时目录里,权限 0600,内容是一个 JSON 数组,dsh 按 YAML 读它。每一项按id修改 dsh 的一个配置条目。始终关闭的七个条目:
给了条目 关闭的内容 agent-instructions读 AGENTS.md、CLAUDE.md等指令文件skill-filesystem读用户与项目的技能目录 session-log-deepseek每次模型请求在请求体里附带会话事件( dsh_session_log)plugin-package-inventory-deepseek请求体里附带已安装插件的清单 session-telemetry-otelOTel 遥测导出 session-title-llm额外请求一次模型生成会话标题。这次请求的用量不出现在输出里 tool-webweb_search与web_fetch两个工具(用户定)。理由:web_search由模型服务执行,请求只到api.deepseek.com,过滤代理无法按主机限制它,--network none也拦不住--no-subagents时另外关闭tool-subagent、tool-subagent-fork、tool-workflow、tool-subagent-control、tool-subagent-list-agents,模型的工具列表里随之没有 subagent、workflow 与给 subagent 发消息的工具(实测)。给了--model ID时加{"id":"agent-default-model","config":{"provider":"deepseek-official","model":"ID"}},模型名不检查,不存在的模型在第一次请求时报INVALID_REQUEST;没给时用 dsh 的默认模型。给了--effort LEVEL时加{"id":"llm-deepseek","config":{"reasoningEffort":"LEVEL"}},dsh 接受off、low、high、max,其他值在联网前报UNSUPPORTED_REASONING_EFFORT。 - 补丁按
id匹配条目,dsh 把某个条目改名或删除后,这一项既不报错也不生效。升级 dsh 后用dsh headless --patch <补丁文件> --dump-config核对合成后的配置,它不发请求。 --image是用法错误,detail见图片。- 沙箱打开时,整条命令前面加上包裹命令,与 pi 相同,只是没有 pi 状态目录的三组参数:Linux 是
bwrap加socat,macOS 是sandbox-exec与会话临时目录里的seatbelt.sb。dsh-home/与dsh-agents/在会话临时目录里,所以 dsh 能写它们。被bwrap包裹时第一个信号直接发给 dsh,与 pi 相同,见进程、信号与超时。
这段输出取自 Cloud Managed 容器,工作目录、dsh 的路径与 PATH 改成了短路径。agentrun 设置的四个 DSH_ 变量不出现在 set: 一行与 start 事件的 env 字段里。
输出映射
--json 每行一个事件。字符串字段超过 8 KiB 时被 dsh 截断,final 不截断。
| 原始事件 | 转成 |
|---|---|
| 没有对应的原始事件 | prompt:dsh 不回显提示词,agentrun 在 start 之后用自己传入的提示词输出 |
session | 不输出;sessionId 去掉 session- 前缀是会话 id,见保存与继续会话 |
tool_call | tool_start:id 为 callId,name 为 tool,parent 为 null,summary 由 input 计算 |
tool_result | tool:id 为 callId,denied 为 false;status 为 error 时 error 为 true;为 completed 时,bash 与 pwsh 的 error 按 result 末尾的退出标记判定,[exit code: N] 的 N 不为 0 或有 [killed by signal: X] 时为 true,没有标记或 N 为 0 时为 false,result 不是字符串时为 null,其他工具为 false;status 为其他值时为 null。理由:dsh 在命令退出码不为 0 时也报 completed,claude-code 与 pi 对同样的命令报告出错,dsh 只在 result 末尾写出退出码,没有退出码时表示命令正常退出(0.2.1-rc.2 的 @deepseek-ai/dsh-shell) |
text | text,空字符串不输出。dsh 输出的是一段完整的文字,不是增量 |
status,phase 为 step_end 且带 usage | usage:inputTokens、outputTokens、cacheReadTokens、cacheWriteTokens 对应四个字段(inputTokens 不含缓存命中,实测)。事件里没有模型名,model 为 --model 的值,没给时为 null;dsh 不报告费用,cost_usd 为 null。模型请求失败的那一步没有 usage,不输出 |
status,phase 为 turn_end | 不输出;reason 用于失败判定 |
final | end.result。失败与中断时 final 是空字符串,这时不作为结果 |
error | 不输出;用于失败判定 |
status,phase 为 step_start | 不输出;作为主 agent 一次模型调用的开始,用于 --max-turns,见轮数与 token 上限 |
其他(thinking,phase 为 turn_start 的 status) | 只写入 --raw |
summary:bash 写 command 的第一行;read、write、edit、read_image 写 file_path,在 --cwd 之内时写相对路径,等于 --cwd 时写 .,与 claude-code、pi 相同;glob、grep 写 pattern;其他只写工具名。
dsh 只输出主会话的事件。subagent 里的工具调用不出现,主会话启动 subagent 的调用照常输出 tool_start 与 tool,所以 dsh 不输出 subagent_start 与 subagent_end,parent 全部为 null。
这段输出取自 Cloud Managed 容器,工作目录改成了短路径。dsh 不报告费用,[end] 行没有金额。
失败判定
以下任一成立即为 failed,detail 按顺序取第一项的信息:
- 有
error事件:取它的message前 500 个字符。dsh 启动阶段的错误只有这个事件,例如未知选项与空任务。 turn_end的reason.kind为error:取reason.error的code: message前 500 个字符,没有message时只写code。kind是completed以外的其他值(max-tokens、aborted、interrupted)时写turn_end reason: 值。- 退出码非 0:与 pi 相同,先看包裹命令是否启动失败,再取标准错误的最后 500 个字符。
- 退出码 0 但没有
turn_end:dsh produced no turn_end。
| 实测情况(dsh 0.2.1-rc.2,Cloud Managed 容器) | 退出码 | 输出 |
|---|---|---|
| 正常完成 | 0 | turn_end 的 kind 为 completed,final 是最后的回答 |
| 无效 key | 1 | turn_end 的 kind 为 error,code 为 AUTH,status 401;标准错误 dsh: AUTH: ...;final 为空 |
| 模型不存在 | 1 | 同上,code 为 INVALID_REQUEST,status 400,message 列出可用的模型 |
| 连不上模型服务 | 1 | 同上,code 为 TRANSPORT |
| 未知选项、空任务 | 1 | 只有 error 事件 |
工作目录的 .env 里有 dsh 拒绝读取的变量名(DSH_ 开头、DEEPSEEK_BASE_URL、代理变量、PATH 等) | 1 | 没有事件,标准错误是 node 的异常栈 |
| SIGINT(包裹与不包裹相同) | 130 | 没有 turn_end,final 为空 |
| SIGTERM(包裹与不包裹相同) | 0 | 同上 |
SIGTERM 后退出码为 0,所以中断只按 agentrun 自己收到的信号判定,见进程、信号与超时;超过上限被停止的运行因为没有 turn_end,按上面的第 4 条不满足 finished,结果是 limit,见轮数与 token 上限。dsh 在新的进程组里启动命令,这些命令由 agentrun 的进程树清理结束(云端实测 SIGINT 与 SIGTERM 后没有留下进程)。本机 Linux 上用户的 systemd 可用时,dsh 改用 systemd-run --user --scope 启动命令(dsh 源码),这种情况没有实测。