运行:命令行与运行时

前半页写调用方怎么运行 agentrun:命令行选项、一次运行的顺序、启动前检查、doctor 与退出码。后半页每个运行时一节:agentrun 用什么参数启动它、它的原始输出怎么映射到统一事件、什么情况算失败。运行时的参数与行为来自实测,写明了实测的版本;运行时升级后要重新核对。原始输出的完整字段见 claude-code 事件流与 pi 事件流两页字段参考。

代码位置

文件主要类型与函数本页对应的章节
src/main.rs入口:安装信号处理,把进程的参数、环境与标准流装进 Caller,调用 run::run,把返回的退出码交给进程命令行界面、退出码
src/cli.rsCli、Parsed、RunArgs、DoctorArgs、ConnectArgs、Runtime、SandboxMode、NetworkMode、Format、prescan_format、default_format、usage_error_detail、MISSING_COMMAND命令行界面
src/run/mod.rsCaller、run、create_tempdir、shell_quote一次运行的顺序、启动前检查、额外传递参数
src/run/execute.rsexecute、read_lines、temp_environment一次运行的顺序、轮数与 token 上限、退出码
src/runtime/mod.rsAdapter 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.rsClaudeCode、MIN_VERSION、SERVICE_HOSTclaude-code
src/runtime/pi.rsPi、MIN_VERSION、Model、parse_model、model_service、session_state、default_model、assistant_modelpi
src/runtime/codex.rsCodex、MIN_VERSION、SERVICE_HOSTS、SERVICE_HOST、sandbox_commandcodex
src/runtime/dsh.rsDsh、MIN_VERSION、KEY_VARIABLE、REMOVED_PREFIX、service_host、saved_session、login_outcomedsh
src/transcript.rsSaved、Transcript、SaveError、store、create_store、locate、find_file、find_file_within、copy_into、copy_dir_into、save保存与继续会话
src/doctor.rsrun、connect、selected、COMMAND_TIMEOUT、USAGE_EXIT_CODEdoctor 检查
src/image.rsImage、ImageKind、read_images、write_copies图片
src/json.rsstring、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_SANDBOXon
--network none|full|custom命令与网页工具能访问哪些主机,见网络none
--allow-host HOST[:PORT]允许访问的主机,可重复,只能与 --network custom 一起给出无
--deny-read PATHagent 不能读的文件或目录,可重复,加在默认列表之后。以 ~ 开头,或者等于或包含工作目录、会话临时目录所在的目录、运行 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 pi --sandbox off --env GH_TOKEN --dry-run --prompt "修复测试" command: /usr/local/bin/pi -p --mode json --no-session ... --tools read,bash,edit,write,grep,find,ls PATH: /usr/local/bin:/usr/bin:/bin set: GH_TOKEN removed: (none) agentrun variables: (none) deny read: /home/me/.ssh, /home/me/.aws, ..., /home/me/.claude/.credentials.json, /home/me/.codex/auth.json

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 在启动前读出每个文件的全部内容,以下情形是用法错误:

运行时拿到的是 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(rejected)退出码 2在沙箱里启动运行时逐行解析运行时输出,写统一事件运行时退出、收到信号或超时:清理进程组,输出 end删除临时目录按退出码表退出
一次运行的顺序。启动前检查失败时不启动运行时。

启动前检查

以下情形在启动运行时之前报错。标准输出只有一个 end 事件,status 为 rejected,detail 写出原因与修正方法;同一个原因另写一行 agentrun: <detail> 到标准错误;agentrun 以退出码 2 结束。格式是 text 或 rich、并且标准输出与标准错误都是终端时,不写标准错误这一行。理由:两路输出都在用户的终端上时,同一句话会出现两次;输出被重定向时,调用方从标准错误或 end 事件都能读到原因。

过滤代理启动失败先于 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 计数时同一条命令可以在这几个运行时之间换。

保存与继续会话

给出 --session-dir DIR 时,agentrun 在运行时退出后把这次运行的会话记录移到 DIR/<运行时名>/,end.session_id 是会话 id。下一次运行给出同一个 --session-dir 与 --resume <会话 id>,运行时接着这段会话继续:会话 id 不变,新内容追加到同一份记录。不给 --session-dir 时四个运行时都不保存会话。--resume 只从 DIR 读,不读用户自己的会话记录;codex、pi 与 dsh 运行期间把记录写在私有目录或会话临时目录里,claude-code 的新会话运行期间写在用户的配置目录里,见下表与配置与账号。

$ agentrun pi --session-dir ~/agent-sessions --format text --prompt "Remember the word PELICAN. Reply only OK." [main] prompt Remember the word PELICAN. Reply only OK. [main] text OK [end] finished 1.8s in 2.2k out 2 cached 93% $0.0001 session 01a152a2-6878-70d1-8e7f-3877dfb11836 $ agentrun pi --session-dir ~/agent-sessions --resume 01a152a2-6878-70d1-8e7f-3877dfb11836 --format text --prompt "What word did I ask you to remember? One word." [main] prompt What word did I ask you to remember? One word. [main] text PELICAN [end] finished 2.0s in 2.2k out 5 cached 92% $0.0001 session 01a152a2-6878-70d1-8e7f-3877dfb11836
运行时新会话继续会话
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 实测)
dshdsh 把记录写在私有 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)

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},被信号中断时输出已完成的项。沙箱检查失败时,说明里写明可能的原因与修正方法。

$ agentrun doctor claude-code pi [ok] claude-code executable /opt/node22/bin/claude 2.1.303 [ok] claude-code login CLAUDE_CODE_OAUTH_TOKEN [fail] claude-code sandbox bwrap cannot start: Creating new namespace failed: Operation not permitted. In a docker container use --sandbox off. On Ubuntu 23.10 or later, allow bwrap to create user namespaces with an AppArmor profile: https://yhnocoder.github.io/agentrun/pages/isolation.html#apparmor [skip] claude-code network --network none [ok] pi executable /opt/node22/bin/pi 1.2.0 [ok] pi login deepseek/deepseek-flash (from pi settings) [fail] pi sandbox bwrap cannot start: Creating new namespace failed: Operation not permitted. In a docker container use --sandbox off. On Ubuntu 23.10 or later, allow bwrap to create user namespaces with an AppArmor profile: https://yhnocoder.github.io/agentrun/pages/isolation.html#apparmor [skip] pi network --network none $ agentrun doctor claude-code pi --sandbox off --network full [ok] claude-code executable /opt/node22/bin/claude 2.1.303 [ok] claude-code login CLAUDE_CODE_OAUTH_TOKEN [skip] claude-code sandbox --sandbox off [skip] claude-code network sandbox not running [ok] pi executable /opt/node22/bin/pi 1.2.0 [ok] pi login deepseek/deepseek-flash (from pi settings) [skip] pi sandbox --sandbox off [skip] pi network sandbox not running $ agentrun doctor claude-code pi --network custom --allow-host api.github.com [ok] claude-code executable /opt/node22/bin/claude 2.1.303 [ok] claude-code login oauth_token [ok] claude-code sandbox bubblewrap: wrote inside, blocked outside [ok] claude-code network api.github.com:443 reachable, doctor-check.invalid:443 refused (not_allowed) [ok] pi executable /opt/node22/bin/pi 1.2.0 [ok] pi login deepseek/deepseek-flash (from pi settings) [ok] pi sandbox bubblewrap: wrote inside, blocked outside, pi started [ok] pi network api.github.com:443 reachable, doctor-check.invalid:443 refused (not_allowed)

前两条命令在没有用户命名空间权限的 docker 容器里运行,第三条在 Cloud Managed 容器里运行。下面是 dsh 在 Cloud Managed 容器里的结果,dsh 的路径改成了短路径:

$ agentrun doctor dsh --network full [ok] dsh executable /usr/local/bin/dsh 0.2.1 [ok] dsh login DEEPSEEK_API_KEY [ok] dsh sandbox bubblewrap: wrote inside, blocked outside, dsh started [ok] dsh network api.deepseek.com:443 reachable, 169.254.169.254:80 refused (private_address)

退出码

退出码运行时子命令doctor
0finished所有被检查的项都是 ok 或 skip
1failed有 fail 的检查项
2rejected:启动前失败用法错误、--env-file 读取失败或凭据写入失败
3timeout不出现
4limit:超过 --max-turns 或 --max-tokens不出现
130interrupted(SIGINT)被中断
143interrupted(SIGTERM)被终止

调用方只看退出码就能区分「环境未配置好」(2)和「agent 没有完成任务」(1)。退出码为 2 时,调用方不改环境直接重试不会成功。状态的判定顺序见end.status。

运行时版本

下面是 agentrun 要求的最低版本,agentrun doctor 的 executable 一项按它检查,见doctor 检查。最低版本是各运行时模块里的常量 MIN_VERSION,改这个列表时同时改常量与快速开始的表,单元测试会比较三处是否一致。运行时子命令启动前不检查版本,start 事件也不带版本:运行开始前拿到版本要多启动一次运行时。

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]
  <-- 之后的调用方参数>

--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 initpermissionMode、model、tools不输出;用于检查 --no-subagents
assistantmessage.id、message.model、message.content[]、message.usage、parent_tool_use_idtool_use 块转成 tool_start 并记为未结束的调用(Agent 或 Task 转成 subagent_start);text 块转成 text;thinking 块忽略;同一个 parent_tool_use_id 下出现新的 message.id 时记为一次模型调用开始(CallStart),每条消息的 message.usage 输出为这次调用到当时为止的用量(CallUsage),见轮数与 token 上限
system,subtype permission_deniedtool_use_id、tool_name、decision_reason把对应的调用标为被拒绝;它出现在对应的 tool_result 之前,那个 tool_result 的 is_error 为 true
user,isReplay 为 truemessage.contentprompt,只取第一条。它在 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_notificationtool_use_id、status、usage.duration_mssubagent_end;status 为 completed 时 finished,其他值 failed
resultis_error、subtype、result、modelUsage、total_cost_usdend 的 result、失败判定与 usage
其他,例如 stream_event、rate_limit_event、system/status、task_started、task_progress只写入 --raw
工具summary
BashBash: 加 command 的第一行
Read、Edit、Write工具名加 file_path;在 --cwd 之内时写相对路径,等于 --cwd 时写 .
Glob、Grep工具名加 pattern
WebFetch工具名加 url
其他只写工具名

失败判定

以下任一成立即为 failed,detail 按顺序取第一项的信息:

  1. 最后一个 result 的 is_error 为 true:取它的 result 文字。例如无效模型(同时退出码 1、标准错误有 [claude-code:unrecognized_model])、无效凭据(退出码 1)。
  2. 退出码非 0:取标准错误的最后 500 个字符。例如未知选项,没有任何事件。
  3. 退出码 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]
  <-- 之后的调用方参数> [-- @<图片副本>...]

模型检查

--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_starttool_start:id 为 toolCallId,name 为 toolName,parent 为 null,summary 由 args 计算
tool_execution_endtool:id 为 toolCallId,name 为 toolName,parent 为 null,denied 为 false,error 取 isError,没有这个字段时为 null。args 不在这个事件里,从对应的 tool_execution_start 取。同一条消息里的多个调用并行执行,结束顺序可能与开始顺序不同
assistant 的 message_endtext:全部 text 元素按顺序用换行连接,不输出 thinking;之后输出 usage:model 为 provider/model,input、output、cacheRead、cacheWrite 对应四个字段(input 不含缓存命中,实测),cost.total 对应 cost_usd;reasoning 与 cost 的其他分项不进统一事件
user 的 message_endprompt:全部 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 按顺序取第一项的信息:

  1. 模型检查不通过。
  2. 沙箱运行,且标准错误里有包裹命令开头的行(Linux 是 bwrap: ,macOS 是 sandbox-exec: )、退出码非 0:包裹命令启动失败,detail 为 sandbox failed to start: 加该行去掉这个前缀后的内容。
  3. 退出码非 0:取标准错误的最后 500 个字符。
  4. 最后一条 assistant 消息的 stopReason 为 error 或 aborted:取 errorMessage 的前 500 个字符,没有时写 stopReason: 值。
  5. 退出码 0 但没有 assistant 的 message_end:pi produced no model reply。
实测情况退出码输出
正常完成0最后的 message_end 的 stopReason 为 stop
模型调用失败(无效 key、连不上服务)0stopReason 为 error,errorMessage 例如 401: {...}、Connection error.
没有所选服务商的 key1只有 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>] -
沙箱--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"

输出映射

原始事件转成
没有对应的原始事件prompt:agentrun 在 start 之后用自己传入的提示词输出。codex 的输出里没有提示词(codex 0.159.2 实测)
item.started,command_executiontool_start:name 为 command_execution,summary 为 shell: 加命令第一行
item.started,file_changetool_start:name 为 file_change,summary 为 patch: 加全部路径,空格分隔
item.started,mcp_tool_calltool_start:name 为 mcp_tool_call,summary 为 mcp: server.tool
item.completed,command_execution、file_change、mcp_tool_calltool,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_messagetext
turn.completedusage: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。

失败判定

  1. 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 实测)。
  2. turn.failed:取最后一个的 error.message 前 500 个字符。
  3. 没有 turn.completed 时的 error 事件:取最后一个的 message 前 500 个字符。codex 重试连接时也输出 error,重试成功后照常完成,所以有 turn.completed 时不看 error。
  4. 退出码非 0:取标准错误的最后 500 个字符。
  5. 退出码 0 但没有 turn.completed:codex produced no turn.completed。
实测情况(未注明的为 codex 0.159.2,macOS)退出码输出
正常完成0以 turn.completed 结束
模型不存在1error 与 turn.failed,消息为服务端的 400 错误
无效凭据1约 10 个 error(重试)后 turn.failed,消息含 401 Unauthorized
未知选项2没有事件;标准错误 error: unexpected argument ...
SIGINT1最后是 item.completed,没有 turn.completed
SIGTERM143同上
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>]
  <-- 之后的调用方参数> -
$ agentrun dsh --dry-run --prompt "修复测试" command: /usr/bin/bwrap --ro-bind / / --bind /home/me/hello /home/me/hello --bind '<tempdir>' '<tempdir>' --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 "$@"' /usr/bin/socat '<proxy port>' '<tempdir>/proxy.sock' /usr/local/bin/dsh headless --patch '<tempdir>/dsh-patch.yml' --json - PATH: /usr/local/bin:/usr/bin:/bin set: (none) removed: (none) agentrun variables: (none)

这段输出取自 Cloud Managed 容器,工作目录、dsh 的路径与 PATH 改成了短路径。agentrun 设置的四个 DSH_ 变量不出现在 set: 一行与 start 事件的 env 字段里。

输出映射

--json 每行一个事件。字符串字段超过 8 KiB 时被 dsh 截断,final 不截断。

原始事件转成
没有对应的原始事件prompt:dsh 不回显提示词,agentrun 在 start 之后用自己传入的提示词输出
session不输出;sessionId 去掉 session- 前缀是会话 id,见保存与继续会话
tool_calltool_start:id 为 callId,name 为 tool,parent 为 null,summary 由 input 计算
tool_resulttool: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)
texttext,空字符串不输出。dsh 输出的是一段完整的文字,不是增量
status,phase 为 step_end 且带 usageusage:inputTokens、outputTokens、cacheReadTokens、cacheWriteTokens 对应四个字段(inputTokens 不含缓存命中,实测)。事件里没有模型名,model 为 --model 的值,没给时为 null;dsh 不报告费用,cost_usd 为 null。模型请求失败的那一步没有 usage,不输出
status,phase 为 turn_end不输出;reason 用于失败判定
finalend.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。

$ agentrun dsh --format text --prompt "给 greet.py 写一个测试并运行" [main] prompt 给 greet.py 写一个测试并运行 [main] text I'll look at the file first, then write and run a test. [main] tool bash: ls -la /home/me/hello [main] tool glob: **/greet*.py [main] tool read: greet.py [main] tool bash: cd /home/me/hello && python3 --version && python3 -c "import pytest; print(pytest.__version__)" [main] text pytest isn't installed, so I'll write the test with the standard-library `unittest` module. [main] tool write: test_greet.py [main] tool bash: cd /home/me/hello && python3 -m unittest -v [main] text 完成。 [end] finished 12.9s in 35.4k out 0.8k cached 96%

这段输出取自 Cloud Managed 容器,工作目录改成了短路径。dsh 不报告费用,[end] 行没有金额。

失败判定

以下任一成立即为 failed,detail 按顺序取第一项的信息:

  1. 有 error 事件:取它的 message 前 500 个字符。dsh 启动阶段的错误只有这个事件,例如未知选项与空任务。
  2. turn_end 的 reason.kind 为 error:取 reason.error 的 code: message 前 500 个字符,没有 message 时只写 code。kind 是 completed 以外的其他值(max-tokens、aborted、interrupted)时写 turn_end reason: 值。
  3. 退出码非 0:与 pi 相同,先看包裹命令是否启动失败,再取标准错误的最后 500 个字符。
  4. 退出码 0 但没有 turn_end:dsh produced no turn_end。
实测情况(dsh 0.2.1-rc.2,Cloud Managed 容器)退出码输出
正常完成0turn_end 的 kind 为 completed,final 是最后的回答
无效 key1turn_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 源码),这种情况没有实测。