源码解析 · 科研 Agent · commit 85e9fa0 · 品味深挖

JSON 编排
Code-as-Action 做科学

OpenAI4S 用火山方舟 / 豆包 ¥9.9 复刻 Claude Science 体验。全文不止拆链路——更反推内核协议、假 provenance 过敏与十条搭建品味。配套从零构建教程含思维链与内核品味实验室。

23
10
品味选择
255
Python 模块
0
核心第三方依赖

分析对象PKU-YuanGroup/OpenAI4S(Open AI for Scientist)
基线 commit85e9fa0Feat/retrosynthesis workflow triage #7085e9fa062c354b2ea9a40a668f03669a5452e56d
许可:MIT
产品一句话:用火山方舟 / 豆包 ¥9.9 套餐复刻 Claude Science 体验的开源混合式科研智能体——原生 JSON 工具做编排与权限控制平面,持久 Python/R 内核做科学执行平面;核心零第三方依赖(pure stdlib)。
读者对象:已经读过本系列 Claude Code / Open Design / Codex / OpenManus 至少一份分析的产品经理与资深工程师;希望把「科研 Agent」从口号落到可核对源码的人。
配套教程从零构建OpenAI4S-开发全流程教程.md · HTML
本地基线路径参考项目/OpenAI4S @ 85e9fa0


Part 1

读前约定

约定 说明
引用格式 重大主张尽量落到 文件:行号 或至少 文件 + 符号名,便于回源码核对
术语 控制平面 = provider-native JSON Tool;科学平面 = Python/R Cell;Host = 内核内 host 单例背后的编排信封
完成信号 非科学回合 → Engine 自有 finalize_response;科学 Python Cell → 唯一内部完成信号 host.submit_output(...)
HTML 配套站:OpenAI4S-解析.html(由 build-html.py 生成)
Part 3

第 1 章 一句话定位:JSON 控制平面 + Python/R 科学平面

1.1 它不是又一个 ReAct 编程助手

OpenAI4S(Open AI for Scientist)把自己钉在一个很窄、也很硬的产品位上:

原生 JSON Tool Call 负责编排与权限;持久 Python/R Code-as-Action 内核负责科学执行。

官方中文 README 的口号是「💸 9.9 元豆包 API 复刻 Claude Science」(README_zh.md:7-10)。工程上它不是「Claude Code 的科研皮肤」,也不是「再写一个带 shell 工具的 ReAct」。它刻意拆成两个永不在同一步竞争的动作通道docs/architecture.md:3-20):

平面 动作单元 适合干什么 不适合干什么
JSON 控制平面 一个有序原生工具批次,或单独的 FinalizeAction 权限、元数据、外部服务、工作流控制、会话分叉/回退 十万行 DataFrame 分析、仿真循环、图绘制流水线
Python/R 科学平面 恰好一个完整 fenced Cell 计算、探索、分析、仿真、长时任务;Python 可中途同步 host.* RPC 把「读文件 / 搜网页 / 改权限」拆成十几次 tool_use 往返

README 里那张对照表写得很直白(README_zh.md:52-68):同样「找匹配文件 → 排序 → 读 CSV → 画图」的活,ReAct 可能要 ~14 次往返;OpenAI4S 压成一个代码 cell,大对象留在内核内存,上下文里只剩一句 "<DataFrame 100000×20>"

1.2 产品经理视角:它在卖什么体验

卖点 工程落点
¥9.9 豆包 / 火山方舟 llm/capabilities.py:206-221 内置 ark provider,wire="openai",默认 doubao-seed-2.0-pro
Claude Science 级科研工作台 静态 WebUI(无打包)+ Notebook 投影 + 版本化 Artifacts + 审批卡
开源、可自托管 MIT;daemon 默认绑 127.0.0.1:8760
核心不拖科学栈 pyproject.toml:9 dependencies = [];numpy/pandas 走 optional science extra

🧠 一句话:别家在优化「怎么把 bash / 编辑器工具调得更聪明」;OpenAI4S 在优化「怎么让模型写完一段真的科学代码,并在内核里把状态留下」。

flowchart LR
    U["科学家 / 用户提示"] --> CP["① JSON 控制平面
permissions · metadata · web · MCP · remote"] U --> SP["② Python/R 科学平面
persistent kernel · host RPC · artifacts"] CP -->|"route 铁律:native > finalize > one cell"| ENG["AgentEngine 外循环"] SP --> ENG ENG --> DONE["完成信号
finalize_response 或 host.submit_output"]

Part 4

第 2 章 仓库地图与技术栈

2.1 数字化的项目形状(基线 85e9fa0)

指标 量级(约)
包内 Python openai4s/255.py,约 114k LOC
测试 tests/286test_*.py(默认离线门禁)
WebUI server/webui/app.js9.7k 行;无 bundler
Gateway server/gateway.py12.9k 行(HTTP + 手写 WS 组合门面)
内置 Skills 34skills/*/SKILL.md
控制工具 TOOL_TYPES 70Tool 子类(tools/registry.py:118-189
核心依赖 0pyproject.toml:9
OpenAI4S/                          # commit 85e9fa0 · MIT
├── openai4s/                      # 产品主体(stdlib)
│   ├── agent/                     # 外循环:engine · actions · finalize · ledger · loop
│   ├── kernel/                    # 科学平面:manager · worker · lazy · r_kernel · sandbox 对接
│   ├── host/ + host_dispatch.py   # Host 能力服务 + 共享编排信封
│   ├── sdk/host.py                # 注入到 Python 内核的 host 单例门面
│   ├── tools/                     # 70 个 native Tool 子类 + registry
│   ├── llm/                       # urllib 传输 + openai/responses/anthropic/gemini 线
│   ├── server/                    # gateway · agent_run · branching · webui/
│   ├── security/                  # sandbox · permissions · biosecurity · injection
│   ├── skills_loader/             # SKILL.md 发现与渐进披露
│   ├── store.py + storage/        # 单连接 SQLite + 仓储
│   ├── compute/                   # BYOC 主机侧
│   ├── egress.py                  # 出站域名 allowlist
│   └── …
├── openai4s_compute_provider/     # 远端 GPU 上跑的 stdlib 沙箱 SDK
├── openai4s_worker_runtime/       # worker 运行时附属包
├── skills/                        # 34 份代码食谱(非 JSON tool schema)
├── envs/                          # conda 环境配方
├── workflows/                     # 科学工作流 benchmark 清单
├── harness/                       # 脚本化场景 / golden(在生产 import 图之外)
├── docs/                          # architecture · security · compute · webapp…
├── pyproject.toml                 # dependencies=[] ;science/chemistry extras
├── setup.sh / start.sh
└── tests/                         # 离线默认;external/network/live_llm 需显式 -m

2.2 技术栈选择:为什么是「纯标准库」

选型 源码锚点
包管理 / 运行 uv + setuptools pyproject.toml./setup.sh./start.sh
Agent 核心 纯 Python stdlib agent/engine.py 无第三方 import
LLM 客户端 urllib.request llm/transport.py:27-28
HTTP 服务 http.server server/gateway.py 模块头注释
WebSocket 手写帧编解码 gateway.py 顶部:GET /api/v1/ws
前端 静态 app.js / index.html / style.css 无 build step(CLAUDE.md:100
持久化 SQLite 单连接 store.py
可选科学栈 uv sync --extra science pyproject.toml:36-41(numpy/pandas/matplotlib/sklearn)
可选化学 --extra chemistry(RDKit) pyproject.toml:45-47

硬约束写在 CLAUDE.md:90(核查修正 2026-08-04:原稿写 :11,但所引 "Never add a hard third-party import to the core" 这句真实位置是 :90;:11 是相关但不同的一句 "Core is zero-dependency by design")与 pyproject.toml:9Never add a hard third-party import to the core. 科学库可以出现在 agent cells 里(内核继承 venv),但引擎本身必须 try/except ImportError 守护每一次 in-tree 使用。

2.3 两个入口,同一台引擎

flowchart TB
    subgraph CLI["CLI 路径"]
        RUN["openai4s run '…'"] --> AGENT["agent/loop.py · Agent.run"]
        AGENT --> LK["LazyKernel"]
        AGENT --> LAE["LocalActionExecutor"]
    end
    subgraph WEB["Web 路径"]
        SERVE["openai4s serve / start.sh"] --> GW["server/gateway.py"]
        GW --> SR["SessionRunner._loop"]
        SR --> WAE["WebActionExecutor"]
        SR --> WES["WebEventSink"]
    end
    AGENT --> ENG["AgentEngine.run while"]
    SR --> ENG
    ENG --> RA["route_action"]

CLI 用 Agent.runagent/loop.py:352-447)组装 LazyKernel + LocalActionExecutor + ChatModel;Web 用 SessionRunner._loopgateway.py:6337-6447)组装同一 AgentEngine,只是把执行器与事件投影换成 Web 版本。这是全文最重要的架构对称性:一个引擎,两层薄适配器actions.py:3-6 自称 CoreCoder-style)。


Part 5

第 3 章 与 Claude Science / CodeAct / ReAct 的关系

3.1 三组对照

范式 典型形态 OpenAI4S 怎么站队
ReAct Thought → Action(tool) → Observation,工具原子、无中途回调 控制平面借用其「结构化工具」;科学平面拒绝把分析拆成原子 tool
CodeAct 模型主要产出可执行代码 科学平面就是 Code-as-Action;但外循环仍保留 JSON 控制通道
Claude Science(闭源) 科研工作台 + 强模型 + 闭源运行时 产品体验对标;实现上用开源双平面 + ¥9.9 Ark 替代

3.2 关键主张:为什么「没有注册 shell 工具」

tools/registry.py:115-117:192 写死:

  • 故意 NO shell tool
  • _FORBIDDEN_CONTROL_NAMES = frozenset({"bash", "submit_output"})
  • register_tool 遇到它们直接 ValueError:211)。

Shell 只能发生在 Python 内核里:host.bash(...)sdk/host.py:854-865)。这不是风格偏好,而是平面分离的承重墙——一旦 shell 变成 JSON tool,模型就会退回 ReAct 式「一步一命令」,十万行分析再次被拆碎。

3.3 与「纯 tool_use」的关键差分:中途 Host RPC

docs/architecture.md:56

This inner RPC loop does not exist in a tool_use architecture — there, actions are atomic and never call back into the host mid-execution.

Python Cell 执行中,host.llm / host.delegate / host.compute 走独立于 stdout 的 host_call → host_ack → host_response 通道;Cell 阻塞、Host 服务、Cell 恢复。这是 Code-as-Action 相对 ReAct 的结构性优势,不是提示词技巧。

sequenceDiagram
    participant M as Model
    participant E as AgentEngine
    participant K as Python Kernel
    participant H as HostDispatcher
    M->>E: assistant reply (fenced python)
    E->>K: execute one CodeCell
    K->>H: host_call(llm / compute / …)
    H-->>K: host_response
    K->>H: host.submit_output(...)
    H-->>E: completion signal
    E-->>M: stop_reason=submitted

Part II · 双平面混合引擎

Part 6

第 4 章 AgentEngine 外循环

4.1 状态机本体只有 ~143 行

AgentEngineagent/engine.py:34-143)是 provider-neutral 的外循环。它 import 具体 kernel、dispatcher、store、server——那些都是 ports,由 CLI / Web 适配器注入(docs/architecture.md:58-59)。

核心 run while(engine.py:60-108)可以读成:

state ← RunState(messages, max_turns)
emit RunStarted
while turn < max_turns:
    if cancelled → finish("cancelled")
    if completion.completion() → finish("submitted")   # 例如已有 submit_output
    prepare context → model.complete(stream deltas)
    append assistant message
    action ← route_action(content, tool_calls)
    outcome ← executor.execute(action, reply, state)
    append history_messages; turn++
    if outcome.stop_reason → finish(that)
    if outcome.completion or completion port → finish("submitted")
finish("max_turns")

默认 max_turns=32engine.py:47);Web 会话常从 cfg.max_turns 取(gateway 侧默认常见为 12,explore 可抬高,gateway.py:6349-6351)。

4.2 端口化的好处

Port 作用 默认实现
ModelPort 调模型 CLI/Web 的 ChatModel
ActionExecutor 执行 route 出的动作 LocalActionExecutor / WebActionExecutor
ContextPolicy 压缩上下文 CompactionPolicy / PassthroughContext
EventSink 投影事件 Transcript / WebEventSink / Null
CancellationPort Stop NeverCancelled / EventCancellation
CompletionPort 侦测 submit_output CompletionSignal(dispatcher.last_output)

🧠 产品含义:测试可以在不启动 daemon、不 spawn 内核的情况下,单独验证「取消 / 最大回合 / finalize / 路由优先级」。这是工程成熟度信号,不是过度抽象。

stateDiagram-v2
    [*] --> Running: RunStarted
    Running --> ModelCall: TurnStarted
    ModelCall --> Routing: ReplyReceived
    Routing --> Executing: ActionRouted
    Executing --> Running: OutcomeProduced (continue)
    Executing --> Submitted: stop_reason / completion
    Running --> Cancelled: cancellation
    Running --> MaxTurns: turn == max_turns
    Submitted --> [*]
    Cancelled --> [*]
    MaxTurns --> [*]

Part 7

第 5 章 route_action 路由铁律

5.1 单通道决策

route_actionagent/actions.py:165-185)是双循环共用的唯一动作决策点:

# 伪代码忠实于 actions.py:176-185
calls = normalize(tool_calls)
if len(calls) == 1 and calls[0].name == "finalize_response":
    return FinalizeAction(calls[0])
if calls:
    return NativeToolBatch(calls)          # 任意原生调用压过代码
return extract_action(content)             # 第一个完整 python/r fence

铁律可以背成三句:

  1. 有结构化原生调用 → 绝不跑代码(控制平面不能和科学平面抢同一回合);
  2. 唯一且名为 finalize_response → FinalizeAction(混在其他 tool 里不算完成);
  3. 否则 → 文档顺序上第一个完整的 python/r Cell(一步一格;未闭合 fence 不可执行)。

FINALIZE_RESPONSE_NAME 字面量放在 routing 边界(actions.py:40-43),刻意不 import registry——避免 actions 模块沾上工具注册副作用。

5.2 动作类型代数

类型 定义位置 含义
CodeCell actions.py:46-51 language ∈ {python,r} + code
NativeToolCall :55-71 无损保留 raw_arguments / parse_error / provider_meta
NativeToolBatch :74-78 有序元组
FinalizeAction :81-90 Engine 自有终态声明
Action :93 以上三者的并集

extract_action:146-162)只认:

  • Python:info ∈ {"", "python", "py"}(裸 ``` 默认 python);
  • R:必须显式 ```r

5.3 隐藏「纯完成 Cell」

is_completion_only_cellactions.py:96-143)用 AST 判断:若 Python Cell 整段只是 host.submit_output(...) 且参数里没有嵌套计算,则 Web Notebook 隐藏该格——完成是真实 RPC,但不是科学分析,不应污染只读 Notebook(docs/architecture.md / CLAUDE.md 用户可见完成投影约定)。

flowchart TD
    R["Model reply"] --> Q1{"有 native tool_calls?"}
    Q1 -->|否| EX["extract_action → 至多一个 Cell"]
    Q1 -->|是| Q2{"恰好 1 个且名=finalize_response?"}
    Q2 -->|是| F["FinalizeAction"]
    Q2 -->|否| B["NativeToolBatch(整批)"]
    EX --> C["CodeCell | None"]
    B --> NOTE["代码 fence 即使存在也被忽略"]

Part 8

第 6 章 finalize_response vs host.submit_output

这是整份分析里最容易被「工具列表截图」误导的一章。

6.1 finalize_response:Engine 自有,不是 Tool 注册项

agent/finalize.py:1-12 开宗明义:

finalize_response is deliberately not a control-plane Tool and is never registered in openai4s.tools.registry.

它做什么:

机制 位置 行为
封闭 schema finalize.py:35-83 要求 summary + completion_bullets;可选 findings/metrics/artifacts…
提供给模型的 spec finalize_response_tool_spec() :92-111 metadata-only ToolSpec
拼进工具目录 with_finalize_response() :114-123 追加 spec;若目录里已有同名 → 抛错(防止插件冒充终态)
路由 route_action 仅当它是唯一 native call 时变成 FinalizeAction

科学 Python Cell 不能用它替代内部完成:description 明文写了 does not replace host.submit_output:106-107)。

6.2 host.submit_output:Cell 内唯一完成信号

sdk/host.py:803-827

host.submit_output(output, completion_bullets, output_schema=None)
→ HostDispatcher CompletionService
→ dispatcher.last_output
→ AgentEngine CompletionPort 侦测到 → stop_reason="submitted"

约束:completion_bullets 必须 1–4 条「已完成动作」短语;可选 output_schema 校验失败则 soft-fail {"error":...} 让模型重试。

6.3 对照表(务必记住)

finalize_response host.submit_output
谁拥有 Engine(finalize.py Kernel 内 Host RPC
是否在 TOOL_TYPES (且禁止注册)
何时用 对话 / 纯工具回合收尾 科学 Python Cell 收尾
能否与其它 tool 同回合 不能(必须 sole) N/A(在代码里调用)
R Cell 不从 R 内发出 R Cell 内完成信号
普通 prose / 最大回合 不是完成 不是完成
flowchart LR
    subgraph NonSci["非科学回合"]
        T["Native tools…"] --> F["sole finalize_response"]
        F --> CR["CompletionRecord"]
    end
    subgraph Sci["科学 Python Cell"]
        CELL["```python …"] --> SO["host.submit_output"]
        SO --> CR2["CompletionRecord 同源结构"]
    end
    CR --> UI["Gateway 投影:output + bullets + artifact delta"]
    CR2 --> UI

Part 9

第 7 章 Action Ledger 与 Store SQLite 概览

7.1 Ledger:先记账,再执行

agent/ledger.py 的模块文档(:1-12)说明设计目标:

  • storage 仓储不懂 agent 语义;
  • RuntimeActionLedger 把 typed AgentEngine 事件翻译成不可变 groups/events;
  • Native 声明与结果原子归约;崩溃时用规范错误/取消结果补齐半开 batch,绝不把半开 tool batch 回灌给 LLM。

CLI 在 Agent.run 里构造 ledger(loop.py:405-409, 463+);Web 在 SessionRunner._loopaction_ledger 传入 WebEventSinkgateway.py:6343-6377)。

引擎是 ledger-firstdocs/architecture.md:59-64):打开 append-only action group → 执行 → 以 tool result / Cell milestone 关闭 → 终态追加而非从 UI transcript 反推。

7.2 Store:一进程一连接的真相源

store.py 持有唯一 SQLite 连接与 schema/migration。基线可见的核心表包括(节选,store.py:114+storage/):

职责
frames 会话 / turn 帧(树状深度、模型、token)
messages 聊天消息投影
execution_log 执行日志
artifacts / artifact_versions 版本化产物
lineage_edges 数据血缘
host_call_log Host RPC 审计
permission_rules / permission_requests 持久审批
plans 计划模式
annotations / annotation_admissions 图像批注与客户端 admission
compute_jobs / compute_job_events 远程算力作业
session_branches / session_checkpoints 分支与检查点(storage/snapshots.py
kernel_generations 内核世代(storage/kernels.py
recovery_journal 恢复日志
settings / connectors / memories / shares 配置、连接器、记忆、分享

Agent 侧通过 host.query 只读暴露 SQL(CLAUDE.md / architecture),写路径一律走服务与仓储。

erDiagram
    frames ||--o{ messages : contains
    frames ||--o{ artifacts : produces
    artifacts ||--o{ artifact_versions : versions
    artifact_versions ||--o{ lineage_edges : lineage
    frames ||--o{ permission_requests : asks
    frames ||--o{ session_checkpoints : checkpoints
    session_branches ||--o{ session_checkpoints : head
    frames ||--o{ kernel_generations : generations
    frames ||--o{ compute_jobs : remote

Part III · Host · Kernel · Tools

Part 10

第 8 章 host singleton 与 host_dispatch 编排信封

8.1 内核里的 host

sdk/host.py(约 1177 行)是注入 Python worker 的兼容门面。架构文档列出的能力面(docs/architecture.md:77-88)包括:

  • 网络:web_search / web_fetch / web_download
  • 文件系统:workspace-jailed 的 read/write/edit/grep/glob/list
  • 模型与子代理:llm / delegate / collect
  • 科学 API:science.*
  • 远程算力:compute.* / fold
  • 产物:save_artifact / artifacts / view_image
  • 技能 / 环境 / MCP / 只读 SQL
  • 完成submit_output
  • Shellbash(仅内核内)

8.2 HostDispatcher:共享编排信封,不是上帝类实现桶

HostDispatcherhost_dispatch.py:646+)注释写清定位:

Backs control tools and worker host.* RPC. One instance per session.

__call__(method, args):996)是统一入口。能力实现拆到 openai4s/host/* 服务:files、llm、completion、data、delegation、remote_science、progress、skills、mcp、endpoints、credentials…(模块 import 区 :26-51)。

软失败契约:handler 可返回单键 {"error": msg};worker 转成 RuntimeError。未捕获异常由 manager 同样压成 error 字典上线。

控制平面的 native Tool execute() 与内核 host.* 共用同一 dispatcher——权限、egress、注入筛查、activity step、审计日志共享(docs/architecture.md:142-147)。

flowchart TB
    subgraph Surfaces["调用表面"]
        NT["Native Tool.execute"]
        HC["kernel host.* RPC"]
    end
    NT --> HD["HostDispatcher.__call__"]
    HC --> HD
    HD --> PERM["permissions / approval"]
    HD --> AUD["audit / replay / injection"]
    HD --> SVC["host/* services"]
    SVC --> STORE[(SQLite Store)]
    SVC --> LLM["llm.chat"]
    SVC --> NET["webtools + egress"]

Part 11

第 9 章 LazyKernel、沙箱与环境 allowlist

9.1 LazyKernel:工具回合不白白 spawn

LazyKernelkernel/lazy.py:15-22):

Create a worker only when code first needs an interpreter. Control-tool and structured-finalization turns can carry this object without spawning a process.

Agent.run 组装方式(loop.py:385-389):factory + skill bootstrap + foreground publish。属性 spawned / generation / execute / shutdown 都围绕「线程安全的一次性所有权」。

R 内核是兄弟通道:r_kernel.py + r_worker.R,同一 manager 协议;通过 fd3/fd4 走帧,避免 print 污染协议(CLAUDE.md)。

9.2 OS 沙箱:Seatbelt / bubblewrap

security/sandbox.py 头注释(:7-8):

  • macOS → sandbox-exec(Seatbelt)
  • Linux → bwrap(bubblewrap)

模式:OPENAI4S_KERNEL_SANDBOX=auto|enforce|offauto 自检失败则可见降级enforce 失败即关闭。写入限制在 workspace/private temp;默认拒绝对外原始网络(architecture / CLAUDE)。

9.3 子进程环境 allowlist

spawn 时 worker 环境从严格 allowlist 重建,而不是 os.environ.copy()——防止 daemon 的 provider/API/cloud secrets 与 loader 注入变量泄漏进 Python/R 及其子进程(docs/architecture.md:111-113kernel/environment.py)。

9.4 host.bash 的一击令牌

CLAUDE.md 强调:host.bash 仍在内核本地执行,但子进程启动需要绑定 command hash、cwd、active worker generation、challenge 的一次性 Host token;Host 授权/审计,永不在 daemon 进程里执行 shell

flowchart LR
    CELL["Python Cell"] --> BASH["host.bash(cmd)"]
    BASH --> TOK["one-shot Host token"]
    TOK --> AUTH["BashAuthorizationService"]
    AUTH -->|ok| SUB["subprocess inside worker"]
    AUTH -->|deny| ERR["soft error"]
    SUB --> SANDBOX["Seatbelt / bwrap wrapper"]

9.5 内核协议的硬纪律(读 worker 注释才能懂的品味)

kernel/worker.py 文件头不是装饰性 docstring,而是一份协议设计说明书。把它当成「开发者在怕什么」来读:

机制 怕什么 做法
dup2 换轨 C 扩展 / 杂讯 print 污染协议线 真协议 stdin/stdout 挪到高位不可继承 fd,并发布在 sys._openai4s_protocol_*dup2(2,1) 让 fd1 别名到 stderr(worker.py:86-119
两把锁 帧交错 / 读到别人的 response _PROTOCOL_WRITE_LOCK(写帧)+ _HOST_CALL_LOCK(整段 host_call 事务)(worker.py:12-15
15MB host_call 上限 一次 RPC 拖垮 daemon _HOST_CALL_WIRE_CAP = 15_000_000
stdout 分块 64KB print("x"*2e8) 一次变 200MB JSON 行 _MAX_CHUNK_CHARS = 64_000worker.py:47-51
帧字节上限按 UTF-8 最坏情形推导 CJK / emoji 把「字符上限」撑破「字节上限」 _JSON_WORST_BYTES_PER_CHAR = 12(代理对);注释明确写:六字节推演被测试打脸(worker.py:52-77
SIGINT 纪律 用户 raise KeyboardInterrupt 与真信号混淆 one-shot handler + _in_user_code + _sigint_delivered
crash recovery 读阻塞中 fd 身份漂了 每次读前后核对 (st_dev, st_ino);预算一次 os.dup(reserve) 重建
flowchart LR
    subgraph Worker["worker.py 进程"]
      CODE["用户 Cell / C 扩展"] -->|fd1 已 alias| STDERR["stderr 捕获"]
      HOST["host.* SDK"] -->|高位 fd| PROTO["protocol JSON lines"]
      HOST --> LOCK["_HOST_CALL_LOCK"]
      LOCK --> CALL["host_call 帧"]
    end
    subgraph Manager["manager.py"]
      CALL --> READ["单 reader 循环"]
      READ --> DISP["HostDispatcher"]
      DISP --> RESP["host_response"]
      RESP --> LOCK
    end

品味观察:他们不怕把协议写成「难读」——他们怕的是静默错乱。宁可注释写满「为什么这个数字是 12 不是 6」,也不愿意让一条 CJK 输出把 error_lineno/usage 整帧丢掉。

9.6 Artifact 环境指纹:绑定 kernel generation,不是 daemon

CLAUDE.md 写得很重的一句话(大意):

Artifact 的环境 provenance 来自产生文件的那个内核 generation,不是 daemon 进程的零参数冻结。曾经把 R Cell 产物盖上 Python 包列表——错误的 provenance 比没有更糟,因为它会被相信

这是整份代码库反复出现的认识论:

坏味道 OpenAI4S 的纠正
静默成功 / 假绿 harness:声明应失败的场景若成功则判失败
假 provenance 记「为何缺失」而不是借 daemon 的包列表
stub 形状进契约 stubbed_backend marker 暂停 schema recorder
审批重启后重放参数 只记决议,要求 Continue/replan

9.7 R 通道的「同协议、不同完成语义」

R 复用同一 Kernel manager 与帧合同,但:

  • 协议走 fd3/fd4,杂讯进 stderr(shell 重定向等价于 Python 的 dup2);
  • 没有 mid-cell host RPC,也没有 Cell 内 submit_output
  • 完成叙事仍偏 Python finalize / 后续 Python cell。

品味:不是「两个并列完整运行时」,而是「一个科学平面主通道 + 一个统计/绘图兄弟通道」。产品诚实标注差异,而不是假装对称。


Part 12

第 10 章 tools/ 类目录与只读并行波次

10.1 TOOL_TYPES:70 个具名 Tool 子类

tools/registry.py:118-189 是唯一组合根。按域粗分:

代表工具
文件 ListDirectory / ReadTextFile / WriteFile / Glob / ContentSearch / Edit
环境 EnvList / EnvUse / EnvCreate
Web WebSearch / WebDownload / WebFetch
科学库 ScienceListDatabases / ScienceSearch(背后归一化 UniProt/PDB/…)
Skills Search / Load / Status / History / Rollback
Artifacts List / Metadata / Versions / Save / Restore
会话控制 Query / Frames / Lineage / Todos / Plan / Review / Checkpoint / Fork / RevertPreview / Permissions
委托 Delegate / ListChildren / Collect / Stop / SendMessage
后台执行 Submit / List / Peek / Interrupt
MCP ListServers/Tools/Resources/Prompts + Call
网络放行 RequestNetworkAccess
远程算力 RemoteGPU / Register / Submit / Status / Result / Cancel / Close
动态工具 Define / List / Promote / Versions / Activate / Rollback

没有bashsubmit_outputfinalize_response

10.2 只读并行波次

agent/control.py

  • tool_parallel_policy:152+)识别只读调度;
  • _execute_read_only_waves:167+)按资源键冲突分波;
  • 第一个 mutating / unknown 调用是屏障,之后串行;
  • 结果按 provider 原始顺序写回历史(docs/architecture.md:149-153)。

这让「连读多个文件 / 多库检索」降延迟,又不破坏 canonical tool group 顺序。

10.3 科学库不膨胀 tool 数量

只有 science_list_dbs + science_search 两个控制工具;连接器服务在背后归一化多个公共数据库,并附带 provenance envelope(时间戳 + 请求 + SHA-256)(architecture 后半)。Cell 内对应 host.science.*


Part 13

第 11 章 llm/:ark \| openai \| anthropic \| gemini over urllib

11.1 四条线,一个传输

SUPPORTED_WIRES = frozenset({"openai", "responses", "anthropic", "gemini"})llm/capabilities.py:24)。

传输层 llm/transport.py 纯 stdlib:urllib.request + 有界重试(仅可安全重放的状态码;尊重 Retry-After;可取消;总预算上限)。

Provider 适配文件:

文件 线
llm/providers/openai.py OpenAI Chat 兼容(ark 走这条 wire
llm/providers/responses.py OpenAI Responses
llm/providers/anthropic.py Anthropic Messages
llm/providers/gemini.py Gemini generateContent

11.2 ark / 豆包是一等公民

capabilities.py:206-221

ark:
  wire=openai
  base=https://ark.cn-beijing.volces.com/api/plan/v3
  default_model=doubao-seed-2.0-pro
  context_window=262_144
  tool_calling=True, parallel_tool_calls=True, vision=True, streaming=True

llm/catalog.py 预置多条 Ark 模型(doubao-seed 系列、glm、kimi、deepseek、minimax…)。这是「¥9.9 复刻」的工程底座,不是营销文案空转。

11.3 配置分层

CLAUDE.md:每个 api_key / base_url / model 解析为
per-provider 变量 → OPENAI4S_LLM_* → provider default
Daemon 可无 key 启动,随后在 UI Customize → Models 或 .env 配置。

register_providerllm/registry.py:47-60)只允许挂到已有 wire——不能动态加载任意传输代码。

flowchart TB
    CFG["Config / UI model profile"] --> RES["llm/resolve"]
    RES --> CAP["ProviderCapabilities"]
    CAP --> WIRE{"wire"}
    WIRE -->|openai| OA["providers/openai.py"]
    WIRE -->|responses| RP["providers/responses.py"]
    WIRE -->|anthropic| AN["providers/anthropic.py"]
    WIRE -->|gemini| GE["providers/gemini.py"]
    OA --> TR["transport.post_json · urllib"]
    RP --> TR
    AN --> TR
    GE --> TR

Part IV · Web 工作台

Part 14

第 12 章 serve → gateway.SessionRunner

12.1 进程与绑定

openai4s servestart.sh 薄封装)拉起单例 daemon(pidfile);默认 OPENAI4S_HOST=127.0.0.1OPENAI4S_PORT=8760。文档明确:信任网络上不要 0.0.0.0 裸奔,用 SSH tunnel。

gateway.py 是 stdlib HTTP/WebSocket 组合适配器(CLAUDE.md:对外科手术式修改,禁止整文件重写)。

12.2 SessionRunner._loop:Web 回合的组装现场

SessionRunner._loopgateway.py:6337-6447)做的事:

  1. max_turns(explore 可抬高);
  2. 构造 WebEventSink(emit, rid, assistant_visible, add_usage, …)
  3. 非 plan 模式:从 dispatcher 取 tool_catalog,用 with_finalize_response(...) 拼终态 spec;
  4. AgentEngine(ChatModel(...), WebActionExecutor(...), CompactionPolicy, …)
  5. WebActionExecutor 注入:execute_cell_execute_and_log、native wrapper → _invoke_control_with_artifacts、plan/explore 钩子。

这与 CLI Agent.run 对称,只是事件沉到 WebSocket,Cell 执行走会话 FIFO coordinator。

12.3 POST /frames/{id}/message

路由匹配(gateway.py:9654+):

  • 解析 input_data.request(兼容顶层 request);
  • 处理 annotation_ids + 客户端生成的 annotation_reservation_id
  • wait:false 时返回 202 + execution_id,turn 在后台跑(fire-and-forget MessageJob)。

前端契约见下一章时序。


Part 15

第 13 章 WebSocket `/api/v1/ws` 事件面

13.1 通道职责

gateway.py 文件头(:11 附近):

WebSocket GET /api/v1/wsview_session / pingtext_reset / text_chunk / …)

WSHub:650+)维护:

  • 订阅连接集合;
  • 每帧 live-turn buffer(断线重连可 replay);
  • 单调 seq + 进程 epoch(防 daemon 重启后假「你已追上」)。

13.2 高频事件类型(产品可见)

事件 含义
text_reset / text_chunk 流式助手正文
artifact_created / artifact_ref_problems 产物面板(新产物 / 引用问题)
permission_resolved 人工审批结果
kernel_status / plan_progress / execution_queue / branch_activation_state 内核状态 / 计划进度 / 执行队列 / 分支激活

⚠️ 核查修正(2026-08-04):原表把 cell / artifacts / permissions 列为 WS 事件 type,这是错误的cell 是 live-buffer 的 scope 值(gateway.py:1057),artifacts / permissions 是投影字典的字段名,三者都不是 WS 事件 type。真实的事件 type 是上面三行所列(gateway.py 源码枚举)。 | frame_update | 帧级状态 | | step / step_update | Host 活动时间线 |

turn-scoped 类型集合见 _TURN_SCOPED_TYPES(约 :954):text_resettext_chunkframe_update


Part 16

第 14 章 一次用户提示词的完整路径(最重要)

这是工作台的「主血管」。下列步骤均可在基线源码核对。

14.1 前端 send()openai4s/server/webui/app.js:5574+

关键顺序(注释写明了为什么必须这样排):

  1. 处理 plan / explore / /skillname 指令改写;
  2. 若无会话 → POST /frames 建帧并 sub(id)
  3. 乐观插入 user bubble;
  4. 若有图像批注:用 CSPRNG 生成 admissionId = "resv-" + hex rememberAdmission,再发请求(:5640-5656);
  5. sub(S.currentId) 必须在 POST 之前:5663-5669)——否则首回合 text_chunk 在订阅集合外被丢掉;
  6. POST /frames/{id}/message,body:
{
  "input_data": { "request": "<payload>" },
  "plan": false,
  "explore": false,
  "annotation_ids": [...],
  "annotation_reservation_id": "resv-...",
  "wait": false
}

app.js:5676

  1. 用 202 的 execution_id 关联气泡;按 annotations 字段 reconcile admission。

14.2 服务端接球 → SessionRunner → AgentEngine

sequenceDiagram
    autonumber
    participant UI as app.js send()
    participant API as gateway HTTP
    participant Hub as WSHub
    participant SR as SessionRunner
    participant ENG as AgentEngine
    participant EX as WebActionExecutor
    participant K as LazyKernel/Worker
    participant HD as HostDispatcher

    UI->>UI: sub(frameId) 先订阅
    UI->>API: POST /frames/{id}/message wait:false
    API-->>UI: 202 execution_id
    API->>SR: 后台 MessageJob / run turn
    SR->>ENG: _loop 组装 Engine+Executor+Sink
    loop 每回合
        ENG->>ENG: model.complete → route_action
        alt NativeToolBatch
            ENG->>EX: 控制工具(可只读并行波)
            EX->>HD: 同源 HostDispatcher
            HD-->>Hub: step / permissions / artifacts…
        else FinalizeAction
            ENG->>ENG: CompletionRecord → stop
        else CodeCell
            ENG->>K: 执行一格
            K->>HD: host_call mid-cell
            HD-->>K: host_response
            K->>HD: host.submit_output?
        end
        ENG-->>Hub: text_chunk / cell / frame_update
        Hub-->>UI: WS 推送(含 seq)
    end
    ENG-->>SR: EngineResult
    SR-->>Hub: 终态投影(bullets + artifact delta)

14.3 为什么 wait:false + 先 sub() 是产品级细节

阻塞 POST 会让浏览器像「卡死的表单」;202 + WS 才像科研工作台。但异步带来经典竞态:线程已开始 broadcast(text_chunk),而客户端还没进 conn.subssend() 里那两行注释(:5663-5669)不是废话,是线上事故的墓碑。

同理,admissionId 必须客户端先生成再发出——因为机制要覆盖的恰恰是「永远收不到 202」的情况;服务端铸造的 id 在那种情形下对浏览器毫无用处(:5640-5646,服务端回声在 gateway.py:9680-9685)。


Part 17

第 15 章 Notebook 只读默认、Artifacts、Branch/Revert

15.1 Notebook 默认只读

config.py:364-368

read-only Notebook by default; set OPENAI4S_NOTEBOOK_REPL=1 to re-enable the in-Notebook developer REPL.

notebook_repl 默认 Falsekernel_routes.py 多处 if not runner.cfg.notebook_repl 直接拒绝交互式写入路径。产品语义:Notebook 是 Agent 执行的只读投影,不是第二个 Jupyter 主循环(Jupyter KernelSpec bridge 也是可选旁路,不带 tool batch / finalize,见 CLAUDE.md)。

15.2 Artifacts 版本

  • 表:artifacts + artifact_versions
  • Cell 写文件 / host.save_artifact / 声明 writes_files=True 的 native tool(Web 边界包装)都会生成版本;
  • 环境 provenance 绑到 kernel generation,避免 R 产物盖上 Python 包列表(CLAUDE.md 那句 wrong rather than absent);
  • 完成投影会带上「本回合实际产生的 artifact-version delta」。

15.3 Branch / Revert

server/session_branching.py

  • checkpoint 不可变;
  • fork 物化隔离 workspace(:116+);
  • preview_revert / revert_and_continue:先记当前检查点,再安全恢复,追加 revert,不改写历史(:9-10, :240+);
  • 从 cell fork 仅当该 cell 带 cursor checkpoint,否则 409(CLAUDE.md)——没有检查点无法重建状态。
gitGraph
    commit id: "checkpoint A"
    commit id: "checkpoint B"
    branch experiment
    checkout experiment
    commit id: "fork workspace"
    commit id: "new cells"
    checkout main
    commit id: "continue"
    commit id: "revert→B (append-only)"

Part V · Skills · Compute · Security

Part 18

第 16 章 Skills:34 份代码食谱,非 JSON schema

16.1 模型

skills_loader/loader.py:1-8

  1. Discovery — 扫 skills/<name>/SKILL.md(+ 可选 kernel.py);
  2. Progressive disclosure — 系统提示只列 name + 一行 summary;全文经 host.search_skills / load_skill 拉取;
  3. Sidecar gatekernel.py 先 compile-check。

基线 find skills -name SKILL.md = 34。目录覆盖:蛋白折叠 / 对接 / 单细胞 / 文献 / 图表 / 远程 compute provider 食谱 / retrosynthesis_planning(本基线 PR 主题)等。

16.2 与 Tool 的本质区别

Native Tool Skill
形态 Python Tool 子类 + JSON schema Markdown 食谱 + 可选 sidecar
注册 TOOL_TYPES 文件系统发现
何时进上下文 schema 进 tool catalogue 默认仅索引;按需加载全文
失败模式 schema / permission 模型没 load → 技能从不跑(UI 用 /skill 指令硬注入,app.js:5590-5601

bundled 只读;用户技能在 <data_dir>/user-skills;重名时 bundled 胜出。


Part 19

第 17 章 BYOC 与 `openai4s_compute_provider`

17.1 分工

跑在哪 职责
openai4s/compute/ 本机 daemon 注册表、作业编排、与 Host 对接
openai4s_compute_provider/ 远端机器 stdlib-only 沙箱 SDK;oneshot/repl
skills/remote-compute-* 技能树 具体 provider shim(如 nvidia;注意 remote-compute-ssh 只有 SKILL.md + README,无 provider.py,是纯食谱而非 shim,不宜作 shim 例证

__main__.py 强调两阶段 secret scrub:在 import provider.py 之前先做通用 scrub,resident prologue 再按 provider 声明的 secret_env_prefixes 二次擦除;凭证从 stdin/fd-3 读入,不进环境变量

17.2 控制平面入口

Registry 中的 Remote* Tool(Submit/Status/Result/Cancel/Close…)与 host.compute.* / host.fold 共用审批与审计。host.fold(单序列折叠类)走严格 no-fabrication 策略(CLAUDE.md)。

寻址形态产品文档常见 ssh: / byoc: 风格端点——本机只编排,重计算在你自己的 GPU 上。

flowchart LR
    AGENT["Agent / Cell"] --> HOST["host.compute / Remote* tools"]
    HOST --> MGR["openai4s/compute manager"]
    MGR -->|SSH/BYOC| REM["remote GPU"]
    REM --> PROV["openai4s_compute_provider __main__"]
    PROV --> SHIM["skills/.../provider.py"]

Part 20

第 18 章 权限、审批持久化、biosecurity、egress

18.1 多层防御(独立层,不互相替代)

模块 要点
OS sandbox security/sandbox.py Seatbelt/bwrap;auto/enforce/off
环境 allowlist kernel/environment.py 防 secret 泄漏进 worker
权限 / 审批 openai4s/permissions.py(771 行,opencode 式 allow/deny/ask gate;注意 security/permissions.py 是另一个文件,仅管数据目录文件权限位,与审批无关)+ storage/permissions.py Store 表 持久规则;无人值守默认 deny

⚠️ 核查修正(2026-08-04):原稿写 security/permissions.py,属张冠李戴。审批 broker 是顶层 openai4s/permissions.py;同名的 security/permissions.py(116 行)管的是 SQLite 明文凭证默认 0644 的收紧,并非审批逻辑。 | 代码门 | security/classifier.py + loop _pre_exec_gate | 拒跑不安全 Cell | | 生物安全 | security/biosecurity.py | trajectory screener;BLOCK 停 Cell | | 注入筛查 | security/injection.py | 不可信输出 | | 出站 | egress.py | OPENAI4S_EGRESS=allowlist 时后缀域名白名单 | | bash 令牌 | host/bash.py | generation 绑定 one-shot |

18.2 审批重启语义(容易做错的产品细节)

docs/architecture.md:121-128

  • 持久化的是决策,不是 Python 调用栈;
  • 进程内可恢复 exact blocked gate;
  • daemon 重启后:从 SQLite 露出请求;记录无参 permission_resolution ledger marker;声明旧操作未执行;要求显式重新继续;
  • restart-only once 授权 15 分钟过期,精确匹配 conversation/tool/target,原子消费。

测试默认姿态(CLAUDE.md):OPENAI4S_UNATTENDED_APPROVAL=deny

18.3 egress 的诚实边界

egress.py:9-19 自称是 host-tool 边界上的 best-effort fence(web_* 与静态 bash 检查);默认 mode off。打开 allowlist 后,科学数据库 / 包索引 / 数据仓库可达,其它域需 request_network_access 经审批放宽。它不是 OS sandbox 的替代品。

⚠️ 核查修正(2026-08-04)egress.py:9-10 的 docstring 原文写「openai4s does not ship an OS-level sandbox (Seatbelt/bubblewrap)」,security/sandbox.py(863 行、真实实现 Seatbelt/bwrap)直接冲突——该 docstring 已过期。原稿把改写后的话仍归因给 egress.py:9-19,读者按图索骥会读到相反表述。此处应显式标注 egress docstring 陈旧,而非替作者打补丁。


Part VI · 内核深挖与开发者品味

本章是全文重心之一。OpenAI4S 的作者群(北大—元空联合实验室开源)在公开叙事上对标 Claude Science,但源码里真正的签名不是「又一个 Agent while」,而是:对「假真相」的过敏、对依赖边界的偏执、以及对门面文件的外科手术纪律。下面先把内核协议读透,再反推十条搭建选择。


Part 21

第 19 章 内核协议:dup2、帧上限与「错误比没有更糟」

19.1 技术链路(从 Cell 到 Host 再回来)

sequenceDiagram
    participant M as 模型
    participant Eng as AgentEngine
    participant Ex as Web/Local Executor
    participant Km as Kernel.manager
    participant W as worker.py
    participant H as HostDispatcher
    participant Store as SQLite Store

    M->>Eng: assistant + tool_calls 或 ```python
    Eng->>Ex: route_action → CodeCell
    Ex->>Km: execute(code)
    Km->>W: {"type":"execute",...}
    Note over W: stdout→捕获;协议在高位 fd
    W->>Km: host_call(method,args)  [持锁]
    Km->>H: dispatcher(method,args)
    H->>Store: audit / permission / ledger
    H-->>Km: data | {"error":...}
    Km->>W: host_response
    W-->>Km: result(stdout, usage, error_lineno)
    Km-->>Ex: observation
    Ex-->>Eng: history + maybe CompletionSignal

关键不变量(CLAUDE.md + manager.py / worker.py):

  1. 单 reader:只有 Kernel 读协议出站;supervisor/watchdog 禁偷读帧。
  2. 单飞行 host_call:整段 RPC 一把锁;并发会交错。
  3. generation 单调:respawn 必 bump;旧 bash token / lease 失效。
  4. 软失败契约:Host 可返回 {"error": msg},worker 转 RuntimeError——Cell 看见异常而不是 daemon 崩。
  5. 不要在 worker 里 autoclose matplotlib:gateway 要先 savefig 捕 Artifact(CLAUDE.md 明确禁令)。

19.2 为什么这套内核「品味很重」

大多数 Agent 把「跑代码」做成:

tool Bash → subprocess → 文本结果 → 塞回上下文

OpenAI4S 把「跑代码」做成:

持久 REPL + 协议分家 + 中途同步 Host RPC + 环境指纹 + 账本

前者优化的是「模型会不会用工具」;后者优化的是「科学计算会不会在一周后仍可辩护」。品味差在问题定义,不在循环语法。

19.3 搭建思维链(反推:他们先信什么)

按依赖顺序还原一条开发者内心独白:

  1. 若科学状态不能持久,Code-as-Action 就是笑话 → 先做 persistent kernel,而不是先堆 70 个 Tool。
  2. 若 print 能砸协议,一切可观测性都假 → 先做 dup2 换轨,再谈 notebook。

    核查修正(2026-08-04):原稿写「dup2 / fd3·fd4」混淆了两套机制。Python worker.pyos.dup() 得到内核分配的匿名高位 fd + dup2(2,1) 让 fd1 别名到 stderr,全文件无 fd3/fd4 常量fd3/fd4R 通道专有r_kernel.py:12-13「protocol OUT rides fd 3 / protocol IN rides fd 4」)。应改为「Python 走匿名高位 fd,R 走固定 fd3/fd4」。

  3. 若 mid-cell 不能回调 Host,科研循环次数会爆炸 → 内环 RPC 与外环 tool_use 必须并存。
  4. 若控制与科学抢同一回合,审计与计算会互相污染route_action 铁律。
  5. 若 UI 气泡是真相源,重启必说谎 → Ledger-first;气泡是投影(completions.py)。
  6. 若 provenance 可以「猜」 → 宁愿记缺失原因。
  7. 若核心引入重依赖 → 边界糊掉、审计变难、¥9.9 叙事也站不住 → stdlib hard constraint。
  8. 若门面文件被整页重写 → 兼容/路由/传输契约会静默蒸发 → 外科修改约定写进 CLAUDE.md / PR template。
flowchart TB
    P1["① 科学要持久状态"] --> P2["② 协议必须抗污染"]
    P2 --> P3["③ 需要 mid-cell Host RPC"]
    P3 --> P4["④ 控制/科学分平面"]
    P4 --> P5["⑤ 账本先于 UI"]
    P5 --> P6["⑥ 假真相不可接受"]
    P6 --> P7["⑦ 核心零依赖"]
    P7 --> P8["⑧ 巨石门面只许外科改"]

Part 22

第 20 章 十条开发选择:反推搭建思维与品味

体例对齐本系列 CodexMonitor「品味十章」:每条选择给主张 → 源码证据 → 放弃了什么 → 品味标签

选择一:对标体验,不抄实现 ——「Claude Science 开源复现」是产品叙事,不是 fork

主张:用公开架构思想(Code-as-Action、持久内核、host RPC、安全层)独立复现,MIT 开源;模型可走方舟 ¥9.9。
证据:README Acknowledgement;docs/architecture.md 双平面图;llm/capabilities.pyark 一等公民。
放弃:逐行兼容闭源、绑定单一前沿模型。
品味:垂直领域的开放替代,用价格与可审计性换封闭飞轮。

选择二:stdlib 是硬约束,不是风格偏好

主张:engine / urllib LLM / http.server+手写 WS 零第三方;科学库只能 try/except ImportError
证据:pyproject.toml dependencies = [];CLAUDE.md「Never add a hard third-party import」。
放弃:FastAPI、websockets 库、httpx、ORM。
品味:依赖纪律 = 信任边界纪律。少一个包,就少一条供应链与审计盲区。

选择三:双平面不竞争 —— ReAct「万物皆 tool」被显式拒绝

主张:每回合至多一个动作通道;native tools 优先;sole finalize_response 另案。
证据:actions.py:route_action;禁止注册 bash/submit_outputregistry.py:_FORBIDDEN_CONTROL_NAMES)。
放弃:把 shell、submit、科学计算都塞进 JSON tool 表的「统一感」。
品味:统一感让位于可审计性。科学计算需要语言;编排需要 schema。

选择四:完成信号是产品契约,不是模型习惯

主张:对话/工具完成 ≠ 科学完成;UI 完成是投影。
证据:agent/finalize.pyhost.submit_outputserver/completions.py;纯 submit Cell 不进 Notebook 展示。
放弃:「模型说完再见就算完」。
品味:把「完了」从自然语言降级为结构化事实——科研产品必须能回答「你到底提交了什么指标」。

选择五:内核协议按「最坏字符」设计,不按 ASCII 幻想

主张:上限从测量推导;CJK/emoji 代理对要把字节预算拉到 12×。
证据:worker.py 关于 _MAX_FRAME_BYTES 的长注释与测试驱动修正。
放弃:拍脑袋常量、默契「输出不会太大」。
品味:注释里写失败史——这是实验室工程文化,不是教程工程文化。

选择六:假绿比红更可耻

主张:应失败的场景若成功,benchmark/harness 判失败;stub 不得污染 response schema 契约。
证据:CLAUDE.md harness/workflows 段;stubbed_backend marker;capture_response_schemas.py --check
放弃:「无异常 = 通过」的偷懒评分。
品味:把拒绝能力当成产品一半——科学 Agent 的价值经常是「敢说不」。

选择七:巨石门面允许存在,但禁止整页重写

主张:gateway.py / host_dispatch.py / store.py / app.js / worker.py / manager.py / sdk/host.py(核查修正 2026-08-04:CLAUDE.md:98 列 7 个门面文件,原稿漏了 sdk/host.py)是兼容与路由合同的堆叠;新算法进 service/repo/tool。
证据:CLAUDE.md「Edit the compatibility/composition facades surgically」;PR template 确认项。
放弃:清爽的「理想分层一次到位」。
品味:承认历史债务,用纪律管理,而不是用大爆炸重构制造静默回归

选择八:Skills 是食谱,不是插件 SDK 幻想

主张:SKILL.md + 可选 kernel.py;渐进披露;bundled 赢名字冲突;用户技能不能冒充信任。
证据:skills_loader/loader.py;34 个 bundled skills;Customize CRUD 进 user-skills
放弃:把每个科学流程做成 JSON tool(爆炸)或动态 import 任意代码当一等公民。
品味:扩展面放在「代码食谱」,信任面仍收在 Host 与沙箱。

选择九:安全是叠层,默认姿态偏偏执

主张:loopback、env allowlist、Seatbelt/bwrap、审批(无头 deny)、biosecurity、injection screen、egress(默认 off)彼此独立。
证据:docs/security.mdOPENAI4S_UNATTENDED_APPROVAL=deny 测试默认;审批重启不重放参数。
放弃:单层「沙箱万能」叙事;无头自动放行的 DX 快感。
品味:每层只承诺自己能承诺的;egress 自称 best-effort 反而是诚实。

选择十:Web 是零构建工作台,不是前端事业部

主张:webui/ 静态直出 working tree;编辑即热;没有 bundler。
证据:server/webui/app.js 近万行仍手写;CLAUDE.md「no build step」。
放弃:React/Vite 工程美学与组件生态。
品味:产品重心在运行时与证据,不在前端栈——用一门语言(Python)+ 静态 JS 压运维面,服务「科学家本机一键起」。

一句话总结品味

用纯标准库守住可审计边界,用双平面拆开编排与计算,用内核协议对抗静默错乱,用账本与 provenance 对抗假真相——工程上偏执保守,产品上对科研完成定义进取,架构上宁可巨石门面加外科纪律,也不用大重构换虚假干净。


Part VII · 横向对比与总结

Part 23

第 21 章 对比表

维度 Claude Code Open Design OpenAI Codex OpenManus / Suna OpenAI4S Claude Science(闭源)
产品位 终端编程 Agent 设计 Agent 宿主(不自研循环) 云+本地编程 Agent 通用任务 / 浏览器 Agent 科研 Code-as-Action 闭源科研工作台
主循环 自研 tool 循环 委托外部 CLI 自研 自研 ReAct 变体 双平面 AgentEngine 闭源
工具哲学 Shell/编辑器一等公民 文件系统技能喂给 CLI 沙箱+工具 工具多多益善 无 shell Tool;bash 仅内核 未知
科学内核 无持久科研内核 持久 Python/R + mid-cell host RPC 有(闭源)
完成信号 会话/任务约定 CLI 退出/产物文件 任务完成事件 常靠最终消息 finalize ⊕ submit_output 未知
前端 TUI Next.js 重前端 多端 Web 零构建静态工作台 专有 UI
核心依赖 Node 生态 大 monorepo Rust/TS 等 Python 重依赖 stdlib only 云服务
扩展 MCP/hooks/skills 26 CLI + 内容库 MCP/插件 工具/浏览器 SKILL.md 食谱 + BYOC 封闭
对标价格叙事 Pro 订阅 自托管+自备 CLI Plus/API 自备 key ¥9.9 Ark/Doubao 高价闭源
quadrantChart
    title Agent 产品地图(示意)
    x-axis 通用任务 --> 垂直领域
    y-axis 委托他人循环 --> 自研循环
    quadrant-1 垂直自研
    quadrant-2 通用自研
    quadrant-3 通用宿主
    quadrant-4 垂直宿主
    Claude-Code: [0.25, 0.8]
    Codex: [0.3, 0.75]
    OpenManus: [0.35, 0.7]
    Open-Design: [0.55, 0.2]
    OpenAI4S: [0.85, 0.78]
    Claude-Science: [0.9, 0.85]

Part 24

第 22 章 五个独特设计决策与诚实边界

22.1 五个独特决策(浓缩;展开见第 20 章)

  1. 双平面不竞争route_action 铁律把控制与科学拆开;这是对 ReAct「万物皆 tool」的显式拒绝。
  2. 完成信号双轨且互不冒充finalize_response 永不进 registry;submit_output / bash 禁止注册为控制工具。
  3. 中途 Host RPC — tool_use 架构做不到的「Cell 内同步回调」。
  4. Ledger-first + 投影分离 — UI transcript 不是真相;Action Ledger / Store 才是。
  5. 纯标准库核心 — 用依赖纪律换可审计边界与「科学栈可选」。

22.2 诚实边界

边界 说明
不是 Claude Science 的逐行克隆 体验对标;模型质量、实验室集成、闭源数据飞轮不可等同
Notebook 默认只读 需要 REPL 请显式开 OPENAI4S_NOTEBOOK_REPL
egress 默认 off 打开 allowlist 才是强约束;且仍是应用层 fence
R 无 Cell 内完成 R 是分析通道,完成叙事仍偏 Python/finalize
Gateway / app.js 巨石 组合门面巨大;约定是外科修改,重构成本高
离线测试 ≠ 全部门禁 harness、response schema capture、browser smoke、Linux CI 分支都是独立闸

22.3 何时选它

  • 你要的是科研分析 Agent,不是又一个编程助手;
  • 你接受「模型写代码,内核持状态」;
  • 你希望核心可审计、少依赖、可挂廉价国内模型;
  • 你需要 BYOC 把重计算放到自己的 GPU。

何时不选:纯软件工程仓库遍历、需要重型浏览器操作员、或要「零代码只点工具」的运营 Agent——那些更像 Claude Code / Manus 赛道。


Part 25

第 23 章 源码导览索引

23.1 按阅读顺序(建议 1 个下午)

顺序 路径 看什么
1 docs/architecture.md 官方双循环叙事
2 agent/actions.py route_action 铁律
3 agent/engine.py AgentEngine.run while
4 agent/finalize.py 为何 finalize 不是 Tool
5 agent/loop.py CLI Agent.run 组装
6 server/gateway.pySessionRunner._loop Web 组装
7 server/webui/app.jssend() 202 + 先 sub + admission
8 host_dispatch.py + sdk/host.py 编排信封与内核门面
9 kernel/lazy.py + manager.py + worker.py 科学平面协议(先读 worker 文件头注释
9b 本文第 19–20 章 内核品味与十条选择
10 tools/registry.py 70 tools 与禁止项
11 llm/capabilities.py + transport.py ark 与 urllib
12 skills_loader/loader.py + skills/*/SKILL.md 食谱模型
13 security/sandbox.py + openai4s/egress.py(核查修正:在顶层,非 security/) + openai4s/permissions.py(审批,非 security/permissions.py 安全层
14 store.py / storage/snapshots.py 持久化与分支
15 openai4s_compute_provider/ BYOC 远端 SDK

23.2 按符号速查

符号 文件
AgentEngine.run openai4s/agent/engine.py
route_action / FinalizeAction / CodeCell openai4s/agent/actions.py
with_finalize_response openai4s/agent/finalize.py
RuntimeActionLedger openai4s/agent/ledger.py
Agent.run / LazyKernel 组装 openai4s/agent/loop.py
_execute_read_only_waves openai4s/agent/control.py
HostDispatcher openai4s/host_dispatch.py
host.submit_output / host.bash openai4s/sdk/host.py
TOOL_TYPES openai4s/tools/registry.py
SessionRunner._loop openai4s/server/gateway.py
send / admissionId openai4s/server/webui/app.js
WebActionExecutor / WebEventSink openai4s/server/agent_run.py
SessionBranchingService openai4s/server/session_branching.py
SUPPORTED_WIRES / ark openai4s/llm/capabilities.py
SkillLoader openai4s/skills_loader/loader.py

23.3 本仓库内相关文档

  • 系列对比与目录:总目录.md项目分析/ 下各项目源码分析
  • 配套教程:从零构建OpenAI4S-开发全流程教程.md / 从零构建OpenAI4S-开发全流程教程.html
  • 上游:https://github.com/PKU-YuanGroup/OpenAI4S

Part 26

结语

OpenAI4S 的源码魅力不在「又实现了一个 Agent while 循环」,而在它把循环劈成两半还不肯让它们抢回合,并且在内核协议层对「静默错乱 / 假 provenance」表现出近乎过敏的工程品味:JSON 控制平面负责可审计的编排与权限,Python/R 科学平面负责把真实计算留在持久内核里,并用 mid-cell host RPC 补上 tool_use 缺失的那一截。finalize_responsehost.submit_output 的双轨完成、TOOL_TYPES 对 shell 的显式缺席、以及 Web 上 sub()-before-POST / 客户端 admissionId 这类「小到像 bugfix、大到像产品契约」的细节,共同构成了它相对 Claude Code / Open Design / Manus 系的差异化。

基线停留在 85e9fa0。对照本文时请以该 commit 为准;主线继续演进时,优先核对 agent/actions.pyagent/engine.pytools/registry.pyserver/webui/app.js::send 四条承重墙是否仍成立。


文档生成说明:面向本系列「产品经理 + 资深工程师」笔法;主张尽量回源。配套 HTML:OpenAI4S-解析.html