openai4s 命令都放在这里:daemon 生命周期(serve、status、stop、url)、本地一次性任务执行(run)、首次模型配置(init)、科学计算环境创建(setup)以及与之并列、把环境当事务处理的 env generation、几个支持面(doctor、diagnostics、verify-package)、工作流 benchmark、只读会话分享(share)与替它对外承接流量的公网 relay、批处理任务提交(cluster)、team 模式的账号管理(user),还有可选的 Jupyter 适配器命令。-V/--version 回答包版本号。
CLI 只负责组合,不负责编排。openai4s run 用 ../agent/ 搭出本地的外层循环,只有当某个回合真的路由到代码 Cell 时,常驻内核才会启动。openai4s serve 把活交给 HTTP/WebSocket server。setup 和 status 这类命令都跑在 Agent 回合之外。
同一棵 argparse 树下其实有三类命令,而失败意味着什么,取决于它属于哪一类。run、init、setup、env、doctor、diagnostics、verify-package、benchmark、user 和 Jupyter 命令都在当前进程里做完 —— user 直接写 store,因为需要初始化或停用账号的运维者,往往正是 daemon 起不来的那位。serve 自己变成 daemon;share 和 cluster 则是 REST 客户端,要求 daemon 已经在跑,因此 daemon 不在或凭据缺失时,它们在触及功能本身之前就先失败;cluster 面向 daemon 是刻意为之 —— 一次提交必须送达真正会处理它的 reconciler,第二个进程绕过 daemon 直接写 workload 行,正是两个 reconciler 对同一个 job 各执一词的由来。
| 文件 | 职责 |
|---|---|
__init__.py |
重新导出 main,包本身就是 CLI 入口。 |
main.py |
一棵 argparse 树和它的 handler。在本进程内跑完的:run、init、setup、env(plan/apply/list/rollback/recover)、doctor、diagnostics、verify-package、benchmark、user(add/list/disable/reset-password,直接操作 store;密码走 --password-stdin 或生成后只打印一次,停用账号还会连带清掉它存下的 LLM key),以及 jupyter 的 describe/export/install。与 daemon 打交道的:serve(--detached 在 Linux/macOS 含 WSL2 上转入后台)、status、stop、url、share 的八个子命令,以及 cluster(submit/list/cancel/logs/profiles,走 daemon 的 orchestration 路由);relay serve / relay gen-token 跑的是架在 VPS 上的公网 share relay,不是本机的东西。daemon 的 pidfile 与 statefile 也归它管;建环境时也是它在调 conda —— setup --profile standard 建日常用的 Python 与 R 这一对,full 建全部四个,--only <name> 只建一个。已有的环境除非加 --update,否则不动它;更新绝不会 prune 掉你自己装的包;而且更新时传的是发现逻辑真正找到的那个 prefix,而不是 spec 里的 name: —— 只给名字的话 conda 会拿它去解析自己的 root prefix,结果是在 Agent 根本不会用到的地方另建一个环境,还报成功。 |
run 在进程内跑完,动作路由与完成判定用的是与本地 Agent facade 相同的一套 Engine 规则。run --auto 是 Auto Mode:越界动作交给 Guardian 处理而不是直接失败关闭,结果也要先经过复核,run 才报告终态 —— CI 得能分清「跑完且验证过」和「跑完但没人检查」。复核读的是这次运行记录下来的内容(--auto 会记录它的 cell:代码、输出、报错,外加原生工具账本);缺失的证据记为遗漏,绝不当作已验证。这只覆盖根运行自己的 cell:委派子代理的 cell 记在子代理自己的 frame 下,既不收集也不声明为遗漏,与 Web 路径一致。这不是完全放权;Guardian 真正生效的面是一份只读允许名单,且绑定在经过校验的动作摘要上。run --mode reusable_pipeline|codebase_change 只有在测试证据背后有一张 Host 授权的 host.bash 回执时才算完成;无头运行里没人能批准那条 shell 命令,所以 --allow-test-command CMD(可重复)只为本次运行预先授权这一条精确命令字符串;既没有它、也没有常设规则或无人值守策略时,运行会在第一次调用模型之前直接拒绝。那张回执是内核 worker 自己报告的、这条字符串在 Cell 环境(PATH、cwd、进程内状态)里运行后的退出码:它能抓住没跑过、只是打印出来或者跑失败了的测试,但并不能证明 Cell 没有蓄意伪造测试运行器。run 只有在任务真正完成(stop_reason 为 submitted)时才以 0 退出。以其他任何方式结束的 run —— max_turns、no_progress、cancelled,或 CLI 从没见过的停止原因 —— 一律以 3 退出,--json 照样打印完整结果,停止原因仍然读得到。2 仍表示拒绝执行(用法错误,比如任务为空 empty_task、--allow-test-command 无效 invalid_allow_test_command;显式代码模式下没有任何途径能授权它的测试命令 code_mode_test_runner_unauthorized;标准环境未就绪;数据库比当前版本新 future_schema;较旧数据库的升级迁移失败并已回滚 migration_failed,错误行会给出保留的备份位置 —— --json 会在 stdout 打印错误和 code,两种数据库拒绝仍会在 stderr 打印那一行 error:),1 表示带 traceback 的未处理异常。加 --auto 时退出码同样只看 stop_reason,复核结论在 auto_mode.terminal 里。这张表也写在 openai4s run --help 中。以前任何终态都返回 0,于是检查 $? 的包装脚本会把打转或撞上轮数上限的 run 当成成功,而 Action Ledger 记下的却是失败。run 打开的 frame 由这次 run 自己收尾:完成记 done,取消或 Ctrl-C 记 cancelled,其余情况(包括抛异常)记 failed,并把 provider 的 token 总数写到这一行上。frame 已经建好、却在第一次调用模型之前就被拒绝的 run(显式代码模式下没有任何途径能授权它的测试命令)同样记 failed。以前它永远停在 processing、token 为 NULL,同一数据目录上的 daemon 查到的也就是这个状态。委派子 Agent 的 frame 仍归委派 runner 管。serve 的绑定地址必须始终取自 Config,默认也必须留在 loopback 上,不要写死。要把 daemon 暴露到本机之外,应该交给可信的反向代理或 SSH tunnel。access token 现在是默认必需的,loopback 上同样如此:Gateway 会把它写进数据目录下一个仅属主可读的文件,跨重启复用,并要求除 /health 和 /api/v1/auth/status 之外的每条路径都带上它 —— 一个客户端总不能靠一份它无权读取的响应来得知自己需要 token。这道门没有豁免:OPENAI4S_REQUIRE_TOKEN=0 只在 loopback 上被承认了恰好一个 minor release(D1),自 0.3.0 起被忽略,因此 daemon 启动过后 token 文件总是存在。这个 token 只是最后一道薄薄的防线,不能把它当成可以放心暴露端口的理由。serve、url 打印给人看的 URL 会带上 ?token=…,否则 SPA 在能给出任何入口之前就先收到 401。凡是不交到人手上的用法,取的都是同一个 URL 的不带 token 版本:凭据不该出现在日志行或窗口标题里。status 就属于这种用法 —— 它是健康检查,输出会进 CI 和支持日志 —— 所以它只打印不带 token 的地址,并提示用 openai4s url 获取登录 URL;以前它会把完整 token 打印出来。OPENAI4S_TOKEN。请求路径由 contract.API_ROOT 拼出来,调用方自带前缀会直接报错而不是给出一个 404 —— 把 /api/ 写死,正是当初每个 share 子命令都只收到 daemon 那句「API 是带版本的」404、而没有任何一个真正走到路由的原因。serve 先绑定端口,再打印横幅,这样端口冲突表现为一次失败的启动,而不是「启动成功之后的崩溃」。stop 只宣称它验证过的事:它轮询等待进程真正退出,--timeout(默认 30 秒)用完时 daemon 仍在收尾,就以退出码 2 报错、pidfile 原样留下 —— 过早清掉状态文件,正是当初一个还活着、还占着端口的 daemon 被 status 判成「没在跑」、又被下一个 serve 一头撞上的原因;stop --force 在同样的轮询之后升级为 SIGKILL。超过 5 秒的 SIGTERM 宽限期后,它会向 stderr 打印一行 shutting down… 并继续轮询:正在收尾首个 kernel 的 bootstrap 或多个活 kernel 的 daemon 需要 8-9 秒,在 5 秒处放弃,会把一次自己就能完成的关闭误报成失败。setup 是就地改一个环境;env 把环境当事务处理 —— 一个 generation 从 apply 锁下暂存的 spec 构建出来,靠真正启动解释器来验证(路径上有个文件并不等于有个环境),验证通过之后才把指针指过去。env recover 报告重启后需要知道的东西,包括某次 apply 是否仍持有锁。doctor 用 0 正常 / 1 可用但降级 / 2 检查失败作答,并且不需要 daemon —— 会让人想跑它的场景,多半正是 daemon 起不来;数据目录本身就是坏的那一项时,它也退回到朴素的 Config 而不是直接抛异常。benchmark 在工作流数为零时判失败,而不是对着零个工作流报一次干净的通过。