Hive 终端工具退出码权威指南:从 POSIX 约定到语义化退出状态
【免费下载链接】hiveMulti-Agent Harness for Production AI项目地址: https://gitcode.com/gh_mirrors/hive48/hive
导读
本文是 Hive 项目中terminal-tools工具集的退出码速查与深度解析。它面向所有调用terminal_exec、terminal_job_logs等终端工具的 Agent 与开发者,系统讲解 POSIX 退出码约定、信号导致的负退出码、以及 Hive 特有的「语义化退出状态」(semantic_status)机制——后者正是避免把grep无匹配、diff有差异这类"正常退出 1"误判为错误的关键。读完本文,你将掌握完整退出码解读体系、exit_code为null时的三类场景,以及如何通过源码与测试证据验证这些行为。
本文内容主体源自 references/exit_codes.md,它是 terminal-tools-foundations 技能 的配套参考文档。
一、POSIX 退出码约定:先建立基准心智模型
任何进程退出时都会向父进程传递一个 0~255 范围内的整数状态码。POSIX 约定是 Agent 解读一切退出结果的起点,Hive 的terminal-tools完全遵循这一约定:
| Code | 含义 |
|---|---|
| 0 | 成功(Success) |
| 1 | 一般错误 / 兜底错误(General error / catchall) |
| 2 | Shell 内建命令误用、语法错误(Misuse of shell builtins, syntax error) |
| 126 | 命令已找到但不可执行(Command found but not executable) |
| 127 | 命令未找到(Command not found) |
| 128 | exit的参数无效(Invalid argument toexit) |
| 128 + N | 被信号 N 杀死(Killed by signal N) |
| 130 | 被 SIGINT 杀死(Ctrl-C) |
| 137 | 被 SIGKILL 杀死 |
| 143 | 被 SIGTERM 杀死 |
| 255 | 退出状态超出范围(Exit status out of range) |
核心规则:0永远表示成功;非零值表示某种失败或特殊信息;126/127专门用于启动失败;而128 + N这一组是 shell 层面对"进程被信号终止"的编码方式(信号编号 + 128)。
二、负退出码:envelope 中的信号化编码
在 Hive 的标准 envelope 中,exit_code还有一套与 shell 约定不同的编码——当exit_code < 0时,进程是被信号杀死的,此时abs(exit_code)就是信号编号:
When
exit_code < 0in the envelope, the process was killed by a signal:abs(exit_code)is the signal number (subprocess uses negative codes for signaled exits, separate from the128 + Nshell convention).
这源于 Pythonsubprocess的行为:进程被信号终止时,returncode是负数信号号(例如被 SIGKILL 杀死返回-9),而不是 shell 的128 + 9 = 137。两种编码并存但语义清晰:
- 负退出码(
-N):出现在 Hive envelope 的exit_code字段,来自subprocess原生返回,abs(exit_code)即信号编号; - 128 + N:出现在你直接运行 bash 并让 shell 报告
$?时,是 shell 层面对信号终止的再编码。
源码中,exec.py在构建 envelope 时显式传递signaled=(exit_code is not None and exit_code < 0)(见 exec.py),而 JobManager 在后台作业结束时用更严格的判定:
record.signaled = rc < 0 or (rc != 0 and abs(rc) in _SIGNAL_NUMBERS)(见 jobs/manager.py)——即只要返回码为负,或非零且绝对值恰好落在已知信号编号集合内(SIGINT、SIGTERM、SIGKILL、SIGHUP、SIGUSR1、SIGUSR2 等,见同文件_SIGNAL_NUMBERS定义),就判定为信号化退出。这一信息最终被semantic_exit.classify()转换为("signal", "Killed by signal (exit {exit_code})"),即 envelope 中的semantic_status: "signal"。
三、语义化退出:什么时候 exit 1 完全不是错误
这是整个文档中最关键的实战知识点。许多常见命令把退出码 1 用作"正常的信息性结果",而非错误。如果 Agent 只读原始exit_code,就会把grep没匹配到、diff文件有差异这类完全正常的结果误判为失败。
terminal-tools将这些语义编码在semantic_status字段中,Agent 应优先读取semantic_status:
| 命令 | 退出码 0 | 退出码 1 | 退出码 ≥2 |
|---|---|---|---|
grep/rg/ripgrep | 找到匹配(matches found) | 无匹配(正常,不是错误) | 错误 |
find | 成功 | 部分目录不可读(正常,信息性) | 错误 |
diff | 文件相同 | 文件不同(正常,信息性) | 错误 |
test/[ | 条件为真 | 条件为假(正常,信息性) | 错误 |
对不在该表中的任何命令,默认约定仍然成立:0 = 正常,非零 = 错误。
这套语义表的实现位于 tools/src/terminal_tools/common/semantic_exit.py,核心数据结构_SEMANTICS精确对应文档表格:
_SEMANTICS: dict[str, dict[int, tuple[SemanticStatus, str | None]]] = { "grep": {0: ("ok", None), 1: ("ok", "No matches found")}, "rg": {0: ("ok", None), 1: ("ok", "No matches found")}, "ripgrep": {0: ("ok", None), 1: ("ok", "No matches found")}, "find": {0: ("ok", None), 1: ("ok", "Some directories were inaccessible")}, "diff": {0: ("ok", None), 1: ("ok", "Files differ")}, "test": {0: ("ok", None), 1: ("ok", "Condition is false")}, "[": {0: ("ok", None), 1: ("ok", "Condition is false")}, }classify()函数的判定优先级依次为:超时(timed_out→"error")→ 信号化(signaled→"signal")→ 退出码为None("ok",对应 auto-backgrounded 仍在运行)→ 查表命中 → 兜底默认语义。表内命令的已知退出码之外的取值(如grep的 2、3…)一律按"error"处理,保证不会误把真正的失败放行。
值得一提的实现细节:_base_command会从 argv 或命令字符串中提取基础命令名(剥离/usr/bin/之类的前缀),并且对管道链只考察最后一个命令(因为 shell 传播的是管道末尾的退出码)。对shell=True的字符串,这种"取最后一段"的解析是显式标注的启发式,官方注释指出它"仅供标注语义、不涉及安全边界"(见 semantic_exit.py)。
测试验证:exit 1 + semantic_status ok
仓库测试 test_terminal_tools_exec.py 直接固化了这一行为:
def test_grep_no_matches_is_ok_not_error(exec_tool, tmp_path): f = tmp_path / "haystack.txt" f.write_text("apples\nbananas\n") result = exec_tool(command=f"grep zzz {f}") assert result["exit_code"] == 1 assert result["semantic_status"] == "ok" assert "No matches found" in (result["semantic_message"] or "")同文件的test_diff_files_differ_is_ok_not_error亦验证diff两文件不同时exit_code == 1且semantic_status == "ok"、semantic_message含"differ"。这两个用例是理解"何时读semantic_status而非裸exit_code"的最佳实证。
实操规则
- 规则一:永远先检查
semantic_status。它只有三档:"ok"/"signal"/"error"。 - 规则二:仅当你确实需要精确数值时(例如区分
make的 1 与 2)才回退到exit_code。 - 规则三:看到
semantic_status: "ok"且semantic_message为"No matches found"/"Files differ"时,不要恐慌——这是命令在正常履行职责。
四、exit_code 为 null:三种必须区分的场景
envelope 中的exit_code并非总是整数,文档明确列出null的三种情形:
auto_backgrounded: true——进程仍在运行,已被移交到后台作业,持有job_id。此时应改用terminal_job_logs轮询(支持since_offset增量读取、wait_until_exit阻塞等待,见 jobs/tools.py),而不是把null当作失败。- Pre-spawn 错误(命令未找到、exec 失败)——此时 envelope 的
error字段会给出具体原因。实现上对应 exec.py 中的_err_envelope():捕获FileNotFoundError返回"command not found: ...",捕获其他OSError返回"spawn failed: ...",并置semantic_status: "error"、exit_code: null。测试test_terminal_tools_exec.py也断言了此场景:semantic_status == "error"或error字段存在、semantic_message含"not found"。 timed_out: true且进程拒绝退出——极为罕见,此时"内核才有答案"(如僵尸进程或不可中断的 D 状态),不要指望从退出码获得信息。
注意classify()对exit_code is None且未超时、未被信号化的情形返回("ok", "Still running")——这正是 auto-backgrounded 场景的语义化表达(见 semantic_exit.py)。
五、常见信号导致的退出速查表
文档给出两套信号编码的对照,是排查"进程为何非零退出"的高频查表:
| 信号 | 编号 | Subprocess 退出码 | Shell 退出码 | 含义 |
|---|---|---|---|---|
| SIGHUP | 1 | -1 | 129 | 终端挂断(Terminal hangup) |
| SIGINT | 2 | -2 | 130 | 中断(Ctrl-C) |
| SIGQUIT | 3 | -3 | 131 | 退出(Ctrl-\) |
| SIGKILL | 9 | -9 | 137 | 强制杀死(不可捕获) |
| SIGTERM | 15 | -15 | 143 | 礼貌终止 |
| SIGSEGV | 11 | -11 | 139 | 段错误 |
| SIGABRT | 6 | -6 | 134 | 中止(断言失败等) |
读表要点:
- Subprocess 退出码(负值)= Hive envelope 中
exit_code的取值,abs()即信号号; - Shell 退出码(128 + N)= 你在交互式 bash 中执行
echo $?得到的值; - 同一个信号在两套体系中数值不同,解读前先确认数据来源。
在 Hive 的作业工具中,信号操作被封装为具名动作:terminal_job_control支持signal_term(SIGTERM)、signal_kill(SIGKILL)、signal_int(SIGINT)、signal_hup(SIGHUP)、signal_usr1、signal_usr2,文档建议按"先signal_int优雅中断 → 数秒后signal_term→ 最后signal_kill"的顺序逐级升级(见 jobs/tools.py)。
六、退出码在标准 envelope 中的完整位置
退出码不是孤立字段,它与semantic_status、semantic_message、warning等共同构成terminal_exec的标准返回结构(完整 envelope 定义见 terminal-tools-foundations SKILL):
{ "exit_code": 0, // null 时见上文第四节 "semantic_status": "ok", // "ok" | "signal" | "error" — 优先读它 "semantic_message": null, // 如 grep 无匹配时的 "No matches found" "warning": null, // 如 rm -rf 的 "may force-remove files" "auto_backgrounded": false, // true 时 exit_code 为 null,转 job_id 轮询 "job_id": null, "timed_out": false, "shell_kind": "bash" // "bash" | "powershell" | "cmd" | "direct" }envelope 的组装逻辑集中在 common/truncation.py:build_exec_envelope()先做输出截断(默认max_output_kb = 256,溢出时把完整字节存到output_handle),再调用classify()得出semantic_status/semantic_message,最后通过get_warning()附加破坏性命令警告。退出码解读、输出截断、破坏性警告三者是一套整体机制,semantic_status是其中承载"退出码语义"的一等公民。
七、实战总结:Agent 解读退出码的四步流程
结合文档与源码,推荐所有调用终端工具的 Agent 遵循以下流程:
- 先看
semantic_status:"ok"直接继续;"signal"查信号表判断是被谁杀的(常见为signal_int/signal_term/signal_kill);"error"再往下看。 exit_code为null:检查auto_backgrounded/job_id(转为轮询)、error字段(pre-spawn 失败)、timed_out(内核级异常)。exit_code非零但semantic_status为"ok":这是grep/rg/find/diff/test的信息性退出,读取semantic_message了解具体含义。exit_code为负:abs()即信号编号,对照上表(如-9= SIGKILL、-15= SIGTERM)还原真相。
这套约定让"终端工具返回退出码"从一串难懂的整数,变成了 Agent 可直接执行的决策信号——这也是 terminal-tools-foundations 将"读懂semantic_status而非裸exit_code"列为必读技能的根本原因:跳过它,就会把grep无匹配误判为错误、把正常退出的后台任务当成丢失,产生"工具返回空输出"式的误报与恐慌。
【免费下载链接】hiveMulti-Agent Harness for Production AI项目地址: https://gitcode.com/gh_mirrors/hive48/hive
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考