English

第 15 章:SDK、Daemon 与远程控制

阅读契约: 用本章比较外部客户端触达方式。跟住 SDK、daemon lifecycle、remote control,以及哪些 runtime contracts 在所有 surface 之间保持共享。

SDK、daemon 与远程控制图:比较 protocol clients、process wrappers、lifecycle management、transports 和共享 runtime semantics
SDK、daemon、in-process caller 和 remote-control bridge 的触达方式不同,但保持同一套协议语义。

源码边界: 本章只有在链接到固定 Codex commit 或本章源码地图的 files、types、functions、tests、schemas、request/event shapes 时,才把说法视为 verified source。像 runtime、owner、projection、contract 这类架构归纳是从可见 anchors 得出的 surrounding contract inference,不是对 OpenAI 服务内部的断言。

第 14 章把 app-server 描述为围绕 shared thread ownership 的契约。本章沿着这份契约看它如何被使用:SDK 隐藏 protocol 细节,daemon 管理本地 server lifecycle,transports 保持 message semantics,remote-control streams 把同一套 runtime model 带出本地 process boundary。

关键区别在于 protocol 和其上的 developer experience。Protocol schema 说明有哪些 messages。SDK 说明 programmer 如何 start session、route responses、consume turn events、answer server requests,而不必成为 protocol engineer。Daemon 说明本地 app-server process 如何被发现、启动、探测、重启和更新。Remote control 说明 backend-mediated client 如何像另一个 connection 一样工作,同时不假装网络是可靠的。

Client Taxonomy

系统中有多种 client shape,而且它们刻意不同。

Client shapePrimary boundary优化目标
Rust app-server clientApp-server protocol内部调用者需要 typed facade,同时保持 protocol semantics。
Python SDKApp-server v2 over standard I/O用 generated model types 和 stream routing 做 programmatic thread/turn control。
TypeScript SDKcodex exec JSON event stream面向简单 process-driven execution,而不是完整 app-server protocol control。
Daemon-managed clientLocal app-server process稳定的 local lifecycle、probing、pid files、locks、restart 和 update behavior。
Remote-control clientBackend-mediated stream通过 reconnect、cursor 和 replay 支持远程 app-server sessions。

这张 taxonomy 避免一句误导性的“SDK 调 app-server”。有些 SDK 是这样,有些刻意包的是另一条 command surface。有些 client in-process 运行,有些通过 daemon-managed socket 连接,有些穿过 backend。架构仍然一致,是因为每个 client 都尊重清晰的边界。

这张图故意是不对称的。Python SDK 和 Rust facade 是 protocol clients。TypeScript SDK 是 process/event-stream client。Daemon 不是面向用户语言的 SDK,而是 lifecycle manager。Remote control 是带有 protocol 后果的 transport bridge。

Schema 是契约来源

App-server protocol types 会生成 schema artifacts,让 clients 可以在不手写复制 definitions 的情况下同意 message shapes。契约演进时这点最重要。Notification 增加一个 field、新增一种 server request variant,或开放一个 experimental method,都应该反映在 generated contracts 和 compatibility filters 中,而不是让 client 在 runtime 才撞上。

Schema 不能消除判断。Server 仍然要决定哪些 features 稳定,哪些 experimental,哪些按 connection gate,哪些 older clients 需要特殊处理。但 generated contracts 会减少 drift 藏身的空间。它把问题变得具体:client 过旧,还是 server 发出了 declared contract 之外的 shape?

Rust Facade

Rust app-server client 最贴近 protocol。它的职责不是在 threads 和 turns 上发明一套新抽象,而是让 Rust code 可靠使用 protocol:发送 requests,等待 responses,消费 notifications,并保留 server requests 的语义。

这种克制很重要。Internal clients 很容易因为在同一个 repository,甚至同一个 process 中,就想绕过 protocol。Facade 保持边界诚实。如果 Rust caller 要 observe thread、start turn 或 answer approval,它应该通过外部 clients 也依赖的同一套概念契约完成。

Python SDK

Python SDK 是更完整的 app-server client。它通过 standard I/O 启动或连接 app-server process,使用 generated model types,并向开发者暴露 session control 方法。它内部最重要的问题是 stream routing。

Standard I/O 给 SDK 一条 incoming stream。Request responses、active turn notifications 和 server requests 都从这条 stream 进来。如果 SDK 的两个部分直接读同一条 stream,就会互相抢消息。因此 SDK 使用一个 reader,再把 messages 路由给等待 request 的 queue、turn stream,或 server-request handler。

pseudocode: SDK stream router

为 process output stream 启动一个 reader

对每条 incoming protocol message:
    如果它完成某个 request id:
        交给该 id 的 waiter
    否则如果它是 turn notification:
        追加到 active thread 或 turn 的 stream
    否则如果它请求 client 做决定:
        交给 server-request handler
    否则:
        报告 unknown 或 unsupported message

这不是 Python 细节,而是 app-server 契约在 client 侧的镜像。Server 承诺 typed messages;SDK 必须保留它们的 ordering 和 destination,不能让并发 user code 从同一条 stream 偷读。

一个 compatibility caveat 需要明确说出:默认 server-request handling 对 automation 很方便,但 approvals 是带 policy 的事件。严肃 client 应该为 command approval、file-change approval、dynamic tool calls 和 elicitation 选择显式 handlers,而不是意外接受本应属于 application 的行为。

TypeScript SDK

TypeScript SDK 处在另一个架构角色。它包装 execution command,并消费 experimental JSON event stream。这让它适合 scriptable execution flows,但它不是完整 app-server protocol client。

这不是缺陷,而是产品选择。Process-oriented SDK 可能更容易安装,更适合 short-lived jobs,也不太耦合 app-server lifecycle。代价是它不暴露第 14 章描述的完整 thread-sharing、server-request、replay 和 daemon-managed contract。需要这些语义的代码应该使用 protocol client;只需要“运行这个任务并读取结构化 events”的代码,command wrapper 可能更合适。

Daemon Lifecycle

Daemon 把 app-server startup 变成操作系统层面的关注点。它管理 pid files、operation locks、probing、bootstrap、restart、remote-control enrollment 和 update loops。这些词看起来普通,直到某个 client 在 server starting、restarting、stale,或已被另一个 process 拥有时尝试连接。

Daemon 的职责是让 local server 可发现且稳定,同时不允许两个 supervisors 破坏同一份状态。Operation locks 防止 lifecycle actions 重叠。Probing 区分“server healthy”和“pid file exists”。Restart behavior 给 clients 走出 stale state 的路径。Update behavior 让产品升级 server,而不要求每个 SDK 都发明自己的 process manager。

Daemon 不只是 convenience wrapper。它让 long-lived local contract 对 short-lived tools 和 SDK processes 变得实际可用。

Transport Choices

存在多种 transports,是因为不同 clients 需要不同 process 和 deployment shape。

Transport何时有用架构关注点
Standard I/OClient 把 server 作为 child process 启动。一个 reader 必须路由所有 inbound messages。
Local socket with WebSocket-style framingDaemon 或 local client 连接已有 server。Framing 与 disconnect behavior 必须符合 protocol 预期。
Local WebSocketBrowser-like 或 network-capable local clients 需要 framed messages。Origin、lifecycle 和 backpressure 变得可见。
In-process transportTests 或 internal callers 需要同一契约但不想跨 process。不能绕过 protocol semantics。
Remote-control streamBackend 把 remote client 连接到 local app-server。Reconnect、cursor ack、buffering 和 identity 都是正确性的一部分。

Transport layer 的目标,是让这些选项在边界之上尽量无聊。一个 request 不应该因为来自 socket 而不是 standard I/O,就获得不同语义。可以不同的是 latency、lifecycle、authentication、framing 和 failure behavior。

Remote Control

Remote control 通过 backend-mediated stream 延伸 app-server contract。本地侧用 backend identity 和 installation metadata enrollment。Remote streams 被映射到 app-server connection ids。Messages 会 chunk,cursors 会 ack,outbound messages 在合适时被 buffer,reconnect 可以 replay remote client 尚未 ack 的数据。

网络会让隐藏假设暴露出来。本地 socket 常常可以说“断了就断了”;remote control 不行。它需要显式 cursor 和 replay behavior,因为 remote client 可能在本地 runtime 持续产生 notifications 的同时经历网络失败。

Compatibility Costs

App-server protocol 有版本化概念,但演进仍然有成本。有些 v2 behavior 仍锚定在 v1 initialization ideas 上。有些 experimental support 在 runtime 检查,并在 generated contracts 中过滤。有些 older clients 需要 response 或 notification workarounds。有些 threads 会被 capability levels 不同的 connections 共享。

这些成本对活协议来说很正常。教训不是“避免兼容性”,而是“把兼容性放在边界”。Request processor 中的 client-version workaround 比散落到 core runtime state 的 workaround 更容易删除。Connection 上的 feature gate 比 SDK method 中的 silent failure 更容易推理。Schema drift check 也比 remote client 收到 unknown server request 后的 bug report 更便宜。

Failure Modes

SDKs 和 daemons 会带来自己的 failure surface。

FailureTypical symptomBoundary response
Process launch failureSDK 无法启动 server。在任何 request 被假定 accepted 前报告 startup failure。
Multiple stdout readersResponses 消失或到达错误 waiter。使用单 reader,再内部路由。
Stale pid fileClient 以为 server 存在但无法连接。Probe health,而不是信任 pid state。
Experimental mismatchClient 调用自己不支持的 method。按 capability gate,并返回 protocol error。
Remote reconnect gap网络丢失后 client 漏掉 notifications。使用 cursor ack 并 replay buffered outbound messages。

目标不是隐藏所有失败,而是让失败落在正确边界,使 caller 能决定 retry、restart、询问用户或 abort。

Trace Ledger

问题第 15 章答案
用户请求现在在哪里?它可能在 SDK call、daemon-managed server connection、execution wrapper 或 remote-control stream 中。
什么数据结构携带它?Generated schema models、SDK queues、process streams、daemon probes、socket frames、remote chunks、cursors 和 app-server connection ids。
谁拥有下一步决策?SDK router、daemon lifecycle manager、transport adapter、remote bridge,或 app-server connection gate。
这里可能怎么失败?Launch failure、stale daemon state、stream routing race、unsupported client capability、transport backpressure、lost remote cursor,或 replay mismatch。

应用到实践

  1. 区分 protocol 与 ergonomics。 让 schemas 定义契约,让 SDKs 定义安全好用的使用方式。
  2. 每条 stream 只设一个 reader。 内部路由 messages,不要让并发代码竞争 protocol bytes。
  3. 显式监督 lifecycle。 把 pid files、probes、locks、restarts 和 updates 视为 client reliability 的一部分。
  4. 跨 transport 保持语义。 Transport 可以机制不同,但 protocol message 的含义不能不同。
  5. 把兼容性放在边界。 在 connection 和 SDK boundary 管理 experimental 与 version-sensitive behavior。

收束

SDKs、daemons 和 remote control 让 app-server 契约进入真实程序。它们也说明:有了 message types,并不代表 protocol 已经可用。只有 lifecycle、routing、transport 和 compatibility 被像 runtime 一样认真工程化,protocol 才真正可用。第 16 章转向这份契约最可见的 client:terminal UI。