资讯动态

OpencodeServeClient 设计解析:Onyx Craft 基于 `opencode serve` 传输层的沙箱 Agent 客户端

发布时间:2026/9/10 19:18:43 来源:尧图企业网站定制
OpencodeServeClient 设计解析Onyx Craft 基于opencode serve传输层的沙箱 Agent 客户端【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswerOpencodeServeClient是 Onyx Craft 中驱动沙箱内 Agent 回合的唯一传输层客户端它以进程内 Python 客户端的形式封装单个 pod 中的opencode serve实例将 opencode 的/eventSSE 事件流翻译成 Craft 前端可消费的 SandboxEventACP 协议事件并内置断线补洞、取消/中止、权限自动应答与成本观测能力。本文基于 设计文档 展开结合 serve_client.py 与 event_bus.py 的真实实现讲清它的公开接口、线程模型、事件翻译规则、gap-fill 重连算法、取消路径、认证配置与测试策略让读者既能读懂设计意图也能对照源码理解落地细节。背景与定位为什么要一个opencode serve客户端该文档是opencode serve迁移方案的配套设计迁移方案本身记录于迁移计划后续 ACP 层清理见 drop-acp-layer.md。迁移的根本动机是修复ACP terminator-drop bug旧的 ACP 执行路径会在某些竞态下丢失终止信号导致回合永远卡住。OpencodeServeClient作为 Phase-1 交付物在SandboxManager.send_message之后替换掉ACPExecClient/DockerACPExecClient但对外契约保持不变——send_message仍然返回一个产出 ACP 事件的生成器因此session/manager.py、scheduled_tasks/executor.py、SSE 编码层与数据包日志等所有调用方都无需改动。从部署演进看迁移后的架构中opencode serve是 pod 内长驻进程由入口点 supervisor 管理OpencodeServeClient是一个客户端对应一个 opencode HTTP 目标每次调用在SandboxManager.send_message内部创建、随调用结束销毁。更完整的部署视角可参考 docker-opencode-serve.md实际部署中的坑可参考 deploy-gotchas.md 与 event-stream-pitfalls.md。范围界定迁移计划覆盖、本文不展开pod spec、入口点 supervisor、Dockerfile 改动BuildSession.opencode_session_id持久化列KubernetesSandboxManager.send_message/DockerSandboxManager.send_message的接线Phase-2/3/4/5 的滚动上线机制。公开接口一个只讲 HTTP的薄客户端设计文档中OpencodeServeClient的核心方法是class OpencodeServeClient: def __init__( self, base_url: str, # http://10.0.0.42:4096 password: str | None, # None 表示开发环境集群内必填 *, client_info: dict[str, Any] | None None, timeouts: ClientTimeouts | None None, ) - None: ... # 会话生命周期 def health_check(self) - bool: ... # GET /doc200 即 True def ensure_session(self, opencode_session_id, *, directory, titleNone) - str: ... def delete_session(self, opencode_session_id, *, directory) - bool: ... # 承重方法发送提示词并流式返回事件 def send_message(self, opencode_session_id, message, *, timeout...) \ - Generator[ACPEvent, None, None]: ... # 生成器外部的取消入口 def abort(self, opencode_session_id) - None: ... # 供测试断言 gap-fill 的重连辅助 def list_messages(self, opencode_session_id) - list[Message]: ...ClientTimeouts是三个命名超时的数据类超时字段默认值设计实现中的环境变量与默认值connect_timeout5sOPENCODE_SERVE_CONNECT_TIMEOUT默认5.0request_timeout30sOPENCODE_SERVE_REQUEST_TIMEOUT默认30.0event_read_timeout60sOPENCODE_SERVE_EVENT_READ_TIMEOUT默认60.0注意实现中的默认值从 configs.py 拉取部署方可通过环境变量调优而无需改动客户端代码。event_read_timeout是/eventSSE 流的空闲超时——超过该时长没有字节到达读取器就触发重连。方法语义细节ensure_session幂等、可从任意 API 副本安全调用。若传入opencode_session_id先GET /session/{id}校验存在性200 直接复用404 则回退创建否则POST /session新建。一个关键实现细节是directory参数opencode-serve 通过每个路由上的?directory查询参数Instance.provide中间件按目录隔离会话存储body 里的目录字段会被静默忽略。不带directory会把会话创建到服务器启动目录/workspace破坏按会话的文件系统隔离。delete_session尽力而为失败返回 False 但不影响 Onyx 侧会话删除。send_message内部流程见下文对外契约是产出PromptResponse或Error作为回合终结事件。abortPOST /session/{id}/abort可安全地与在途的send_message生成器并发——opencode 将入站 abort 视为会话状态翻转生成器在/event上看到终结信号后产出合成的Error。内部架构为什么是读者线程 队列而不是 asynciosend_message是一个同步 Python 生成器调用方sandbox manager → session manager在单线程上同步迭代它但 opencode 的/event是推送式流后台读取器不可避免。设计文档给出的模型是每次调用一个守护读者线程┌─────────────────────────┐ ┌─────────────────────┐ │ caller thread │ │ /event reader │ │ for ev in send_msg(): │ ── ACPEvent ──│ thread (daemon) │ │ yield ev │ via Queue │ - httpx.stream │ │ │ │ - parse SSE │ │ │ │ - translate │ │ │ │ enqueue │ └─────────────────────────┘ └─────────────────────┘每次send_message调用对应一个queue.Queue[ACPEvent | _ReaderError | _ReaderEnded]读者线程在send_message内启动、在退出时销毁成功、出错或GeneratorExit不越过单次调用存活读者入队前按sessionID做事件关联——/event是实例级的同一流上混合多个会话的事件关键防挂起机制读者在 SSE 连接关闭或看到终结符并干净退出时向队列放入哨兵_ReaderEnded(reason)调用方线程每次出队都检查该哨兵因此读者线程死亡永远不会让调用方无限期挂起。这正是此前数据包丢失调查中 Bug A 的修复——在设计层面落地且永不作为回归目标。事件流读者线程内部┌─────────────────────────────┐ GET /event ─── SSE chunks ──► │ buffer until \n\n │ └──────────────┬──────────────┘ │ one event ▼ ┌──────────────────────────────┐ │ json.loads(data line) │ └──────────────┬───────────────┘ │ evt.properties.info.sessionID ── filter ──┐ │ │ ▼ ▼ ┌──────────────────────────────┐ drop │ translate (see below) │ └──────────────┬──────────────┘ │ ▼ ┌──────────────────────────────┐ │ queue.put(ACPEvent) │ └──────────────────────────────┘为什么不用 asyncio现有SandboxManager.send_message契约是同步生成器调用方FastAPI 同步端点、定时任务 worker都是同步的。把 asyncio 引入这条路径意味着要改造所有调用方为一个客户端不值得。httpx.stream 守护线程正是现有 ACP 客户端已经使用的模式。实现演进从 per-call 读者线程到共享PodEventBus设计文档中每次调用一个读者线程的方案在最终实现中升级为每 pod 一个长期存活的共享事件总线event_bus.pyPodEventBus维护一条GET /eventSSE 订阅通过subscribe(session_id)扇出到每个会话的订阅队列Queue(maxsize500)满时计数丢弃并告警。send_message内部改为self._event_bus.subscribe(opencode_session_id)→ 等待stream_ready→POST prompt_async→_consume_from_bus排空订阅队列并翻译事件。这带来几个设计文档没有的收益跨调用复用连接同一 pod 内多个并发回合共享一条 SSE 流不再为每个回合建立新连接统一重连总线以 1s→2s→4s…上限 30s指数退避重连连续失败 20 次后自关闭并向订阅者投递BUS_CLOSED_SENTINEL子代理subagent路由总线维护_child_to_parent/_parent_to_children映射把session.created记录的父子关系用于将后代会话事件向上游转发前端即可在父会话流中看到子代理事件401 自愈/event收到 401 时通过reload_auth重新拉取凭据处理 peer pod 轮换密码的场景。事件翻译opencode /event→ SandboxEventACP 事件翻译核心是一个纯函数无 I/O、无self因此极易单测def translate_opencode_event( raw: dict[str, Any], session_id: str, state: _TurnState, ) - Iterable[ACPEvent]: Translate one opencode /event payload into 0..N ACPEvents. Returns an iterable because a single opencode event can imply two ACP events (e.g. a message.updated with time.completed set both finalizes streaming AND emits PromptResponse). Pure — call it from tests with hand-rolled dicts.返回迭代器而非单事件是因为一条 opencode 事件可能蕴含多条 ACP 事件例如最终的message.updated既要冲刷流又要产出PromptResponse。实现中该函数位于 serve_client.py签名进一步演化新增可选的fetch_messageREST 消息水合回调解决delta 先于 message.updated 到达的竞态以及parent_resolver/children_resolver/fetch_message_by_session子代理会话路由。映射表函数的事实来源opencode 事件类型过滤条件产出server.connected总是无——仅设置流就绪标记session.created匹配 session_id无session.next.agent.switched匹配无session.next.model.switched匹配无message.part.delta匹配目标 part 角色assistant类型textAgentMessageChunk(contentTextContent(textdelta))message.part.delta匹配目标 part 角色assistant类型reasoningAgentThoughtChunk(contentTextContent(textdelta))message.part.updated匹配类型toolstatuspendingpart.id首次出现ToolCallStart(...)message.part.updated匹配类型tool后续出现ToolCallProgress(... statusrunning\|completed)message.part.updated类型text无token 流已由 delta 发出message.updated匹配roleassistanttime.completed 非空冲刷缓冲事件然后PromptResponse(stopReason...)session.idle匹配兜底终结符若尚未产出PromptResponse立即产出session.status匹配statusidle兜底终结符同上session.error匹配Error(code..., message...)permission.asked匹配自动允许POST /session/.../permissions/{id}body{response: once}不向消费者产出任何事件以 WARN 记录权限与模式并上报指标opencode_unexpected_permission_askpermission.replied匹配无仅信息性server.heartbeat总是无或作为SSEKeepalive透传到上层session.diff、session.updated终结后匹配无其他—DEBUG 日志忽略兜底终结符是对 ACP terminator-drop bug 在 serve 层的纵深防御Phase 0 的经验数据显示三个终结信号都会可靠触发代码对先到者产出PromptResponse并忽略其余。重要实现差异真正的回合终结信号实现代码serve_client.py对终结语义做了修正message.updated不是回合终结符——opencode 在每一个 step的 assistant 消息上都会发出带time.completed的message.updated工具调用 step、文本 step 等真正的 end-of-turn 信号是session.status: idle/session.idle。因此message.updated在实现中只负责登记 assistant 消息 id、记录last_finish、产出上下文用量包ContextUsagePacket以及当消息携带 error 时产出终结事件。终结统一由_emit_terminator完成它保证每回合至多产出一次PromptResponse/Error并正确区分stopReasonopencode 的stop映射为 schema 的end_turnmax_tokens/refusal/cancelled透传。工具调用 content 合成翻译器逻辑前端 parsePacket.ts 从content[].typediff读 diff 数据、从content[].typecontent读文件内容。但 opencode serve 在工具 part 上不产出content数组——只有state.input/state.output/state.metadata。翻译器因此合成content数组使前端保持零改动。字段名映射以测试报告锁定edit工具state.status到达completedcontent [ { type: diff, path: state.input[filePath], oldText: state.input[oldString], newText: state.input[newString], } ]read工具state.status到达completedcontent [ { type: content, content: { type: text, text: state.output, # opencode 返回带行号字符串 }, } ] # 前端的 extractFileContent 通过 /^\d\| /gm 正则剥掉行号——开箱即用。bash与task工具无需合成content前端直接读rawOutput.output见下方字段映射的raw_output行。raw_input/raw_output字段名映射保证前端现有getRawInput/getRawOutput助手零改动ACP 字段Opencode 来源翻译器动作raw_inputstate.input已是 camelCase与前端的filePath/oldString等回退链一致原样透传raw_outputstate.output纯字符串或对象字符串则包裹为{output: state.output}dict 则原样透传工具名 → ACPtitle和kind的推导实现中用两个小查表_TOOL_KIND/_TOOL_TITLE完成serve_client.py与前端的NAME_MAP/TOOL_KIND_MAP对齐覆盖bash、read、write、edit、patch、apply_patch、glob、grep、list、task、todowrite、webfetch、websearch及 opencode 1.15.x 新增的lsp、skill、question、invalid未知工具回退为kindother、title Running tool。opencode 的工具状态值pending/running/completed/error/cancelled通过_TOOL_STATUS_MAP归一为 Onyx schema 的pending/in_progress/completed/failed。为什么需要 per-turn 状态对象三件事需要跨事件状态由_TurnStateserve_client.py承载ToolCallStart是part.id 首次出现——opencode 会对同一工具 part 发出多条message.part.updated随state.status变迁。用seen_tool_calls: set[str]记录已见过的工具 part id 即可。幂等终结符——一旦产出PromptResponse后续任何兜底终结信号都是 no-op。一个布尔terminator_yielded即可。按文本 part 的累计器用于补洞——local_text: dict[str, str]记录partID → 已产出的累计文本供message.part.updated上的 gap-fill 对账使用。实现中的_TurnState比设计文档更丰富还包含assistant_message_ids/user_message_ids角色缓存避免对每条 delta 重复发 REST 水合请求、part_typespart 类型缓存因为 delta 事件本身不带类型只有 part id、task_child_by_calltask 工具 callID → 子会话映射、child_states每个子代理会话独立状态等。凡是不需要跨事件关联的信息一律不进状态。重连与补洞Gap-fill/event断开后不丢一个事件/event不支持Last-Event-IDopencode 上游 issue #25657纯 TCP 重试会丢失断开窗口内的全部事件。原始方案想用GET /session/:id/message做快照但经验测试证明快照中的part.text在流式期间是空的只有回合终结后才填充——对回合中途恢复毫无用处。可靠的对账点是message.part.updated事件本身对每个文本 partopencode 在实时流上至少发出两条——part 创建时空text和 part 终结时text 完整累计内容其间还有工具边界触发的中间更新每条都携带累计的part.text。如果断开窗口内错过了 delta该 part 的下一条message.part.updated就能通过比较累计长度找回缺失内容。算法读者线程内维护local_text: dict[str, str]partID → 已累计文本。每次message.part.deltafield textlocal_text[partID] delta产出AgentMessageChunk(textdelta)每次message.part.updatedtype textexpected properties.part.textlocal local_text.get(partID, )若len(expected) len(local)错过了 delta——补发AgentMessageChunk(textexpected[len(local):])然后local_text[partID] expected若expected localno-op稳态情形若len(expected) len(local)告警并保留local除非 opencode 回退否则不应发生按数据完整性问题处理实现将该逻辑抽象为共享函数_reconcile_part_textserve_client.py文本 part 与 reasoning part 各自封装为_reconcile_text_part/_reconcile_reasoning_part分别产出AgentMessageChunk/AgentThoughtChunk。注意实现中 delta 与对账共用local_text累计器——reasoning 与 text 的 partID 全局唯一可共用同一字典而无碰撞。httpx.stream抛错/连接在服务端未关闭时结束不要用GET /session/{id}/message做快照——回合中途它帮不上忙退避重连/event1s、2s、4s最多 3 次等待server.connected在途 part 的下一条message.part.updated会自动通过上面的对账补洞——无需专门的补洞模式。边界情况一回合恰好在断开窗口内完成。新流上不会再有本回合事件。重连后静默超过MAX_GAP_WAIT_SECONDS10回退到GET /session/{id}/message终结后它已被完整填充找到 assistant 消息把尚未拿到的文本合成一条AgentMessageChunk再从快照的info.finish产出PromptResponse(stopReason...)。实现在_post_disconnect_snapshot与list_messages中体现serve_client.py。边界情况二重连本身失败。3 次尝试后向队列推入_ReaderError(event stream lost)并退出调用方线程循环捕获哨兵后产出Error。gap-fill 逻辑按设计拆为两个纯函数便于单测_reconcile_text_part()per-event 钩子与_post_disconnect_snapshot()罕见的终结后回退。取消路径三种触发一个机制三种不同触发统一走POST /session/{id}/abort调用方关闭 SSE 流 →send_message内产生GeneratorExit用try/except GeneratorExit:包裹主yield先 abort 再 re-raise。外部/cancelAPI 端点本次迁移新增直接调用client.abort(session_id)。在途的send_message生成器看到session.status变化后产出Error或在 opencode 1.15.7 的行为下只是合成的兜底终结符——需在 Phase-2 测试中验证。生成器内墙钟超时同一代码路径——abort然后产出Error(code-1)返回。旧 ACP 路径依赖GeneratorExit传播进cancel()调用这个职责位于 sandbox-manager 层现在它移入send_message内部。集中化后定时任务不再需要自己的GeneratorExit管道——直接调用abort即可。实现中send_message的取消语义进一步细化GeneratorExit时仅在已成功 POST 过 prompt才发 abort未发提示词则无需中止_consume_from_bus还支持可选的should_interrupt回调约 1 秒轮询一次让调用方能确定性结束回合——先 abort 再自行产出PromptResponse(stopReasoncancelled)而不是等待可能永不到来的session.idle避免被中断且无事件的回合钉住槽位直到超时。认证与配置API 服务端所需环境变量OPENCODE_SERVE_PORT默认4096——与沙箱 Dockerfile 的 EXPOSE 指令对应OPENCODE_SERVER_PASSWORD_SOURCE——每 pod 密码的读取方式二选一每个 pod 一个 Kubernetes Secret与现有ONYX_PAT模式一致由集群级 Secret sandbox-id 确定性派生更省事pod 环境本身就是 secret 存储安全边界相同。客户端构造函数接收password——由 sandbox manager 负责获取密码OpencodeServeClient不关心密码来源。HTTP 细节以 configs.py 与 serve_client.py 的实现为准Authorization: Basic ${base64(username:password)}。设计文档称 username 默认onyx但实现已修正opencode 的 serve 实现在仅设置OPENCODE_SERVER_PASSWORD时把用户名硬编码为opencode任何其他值都会得到 401已对 opencode 1.15.7 验证。因此OPENCODE_SERVER_USERNAME opencode/event上Accept: text/event-stream其余端点Accept: application/jsonPOST/PATCH 使用Content-Type: application/json。实现中还有两层健壮性细节值得注意冷 pod 重试_http_with_cold_pod_retry——沙箱 pod 已 K8s-Ready 但 opencode-serve 尚未绑定 4096 端口时ConnectError总是可重试TCP 拒绝证明服务端从未见到请求重试不会产生重复副作用而RemoteProtocolError仅在调用方声明idempotentTrue时才重试服务端可能已处理请求对非幂等 POST 重试会制造孤立会话——这正是该传输层要避免的 bug401 密码自愈_request——收到 401 时通过reload_password回调重新拉取密码并重建 httpx 客户端peer pod 轮换密码导致本地缓存失效的场景。错误表面化两层错误来源客户端如何暴露POST /session/{id}/prompt_async返回非 2xx终结符之前产出Error(codehttp_status, messagebody[:200])读者线程关闭/event上的session.error事件产出Error(code-2, messageevent.properties.message)若事件同时携带info.time.completed视为终结符读者线程崩溃httpx 异常、JSON 解析失败等通过_ReaderError哨兵合成Error(code-3, messageevent stream error: {e})墙钟超时Error(code-1, messageTimeout waiting for response)调用方发起的 abort不产出——GeneratorExit在POST /abort后传播所有错误事件还会把 opencode 的requestID若事件中存在追加进消息用于与opencode serve日志交叉关联。实现中的错误码改为语义常量serve_client.pyTURN_ERROR_CODE_SESSION会话错误、TURN_ERROR_CODE_TIMEOUT超时、TURN_ERROR_CODE_TRANSPORT传输层。超时进一步细分inactivity 超时timeout随回合活动续期产出ActivityTimeoutError绝对墙钟超时absolute_timeout产出Error(TURN_ERROR_CODE_TIMEOUT, Turn exceeded maximum duration)。被中止的消息MessageAbortedError被正确映射为PromptResponse(stopReasoncancelled)这样消费者不会把用户打断当成回合失败。测试策略外部依赖单元测试针对真实opencode servesubprocess 跑在临时目录位于backend/tests/external_dependency_unit/craft/test_serve_client_basic.py——ensure_session、同一会话上连续三次提示词断言事件有序且每回合恰好一个PromptResponsetest_serve_client_terminator_backstops.py——跑一个回合用注入的代理删掉message.updated断言客户端仍能通过session.idle终结且只产出一个PromptResponsePhase 0 显示该竞态在 serve 上罕见但兜底是承重设计必须测test_serve_client_reconnect.py——回合中途切断/event代理验证重连 补洞产出的最终累计器与未切断的运行一致test_serve_client_abort.py——发提示词后 100ms 中止验证生成器产出Error(-1)或GeneratorExit传播视取消路径而定且同一会话的下一次提示词干净启动test_serve_client_tool_call.py——驱动 bash 工具提示词断言每个工具 part 恰好一个ToolCallStart且ToolCallProgress状态循环到completed。纯函数单元测试backend/tests/unit/test_translate_opencode_event.py——罐装 dict 进、ACPEvents 出断言完整映射表包含message.part.delta与message.part.updated的区别防止未来贡献者回归test_gap_fill_diff.py——罐装快照 罐装已发出事件断言合成事件与实时流产出一致。单元测试是承重的线缆契约锁定外部依赖单元测试是防止 opencode 升级改变行为的集成安全网。仓库中另有针对 401 密码自愈的单元测试 test_serve_client_401_reload.py。关键设计决策2026-05-22 定案1. 权限流——Path A内部自动处理线缆格式冻结OpencodeServeClient在内部处理permission.asked不上抛给前端也不向消费者产出RequestPermissionRequest。生产环境中 Onyx 生成的opencode.json已对所用每个工具类别固定*: allow见 opencode_config.py 的build_opencode_config权限询问理论上不应发生一旦发生说明 opencode 引入了尚未配置的新权限类别——按配置漂移 bug处理。行为默认响应自动允许POST /session/.../permissions/{id}body{response: once}与现 ACP 路径行为一致遥测上以 WARN 记录权限类型与模式并递增 ERROR 级指标内部方法_auto_respond_permission不属公开 API。实现进一步区分了connect_app权限对应 no-op 的connect_app工具它走 connect-card 流程——把待决请求存入缓存并向前端公告由决策端点带外应答消费循环的超时回退在用户未决定时干净拒绝_handle_connect_app_permission与_reject_expired_connect_app_permissions。Path B真实用户审批 UI是产品功能而非迁移需求推迟。2.OPENCODE_SERVER_PASSWORD来源——每 pod K8s Secret每个沙箱 pod 获得一个含新生成密码的独立 Secret以OPENCODE_SERVER_PASSWORD环境变量挂载到sandbox容器。sandbox manager 在provision()期间与现有ONYX_PATSecret 一起生成密码并创建 Secret。不用集群级派生密码的理由横向移动——若沙箱内 agent 能窃取集群级 secret就能知道所有沙箱的密码per-pod 隔离把爆炸半径限制在一个沙箱。且ONYX_PAT已是 per-pod Secret 供给复用该模式保持 K8s manager 对称运维开销约 10 行kubernetes.client.V1Secret创建代码pod 删除级联清理 Secret。3. 多副本并发——不加锁客户端处理 409真实并发路径罕见双标签页用户定时任务 vs 用户。opencode 的session.status: busy状态强烈暗示其prompt_async按会话串行化——要么排队第二个提示词要么以 409 拒绝。客户端把prompt_async的非 2xx 视为软信号409 Conflict会话忙→ 等待/event流上的下一个session.idle最多 30s然后重试一次prompt_async再失败则暴露为Error。其他非 2xx → 直接产出Error(codestatus, messagebody)并结束。配套计数器指标opencode_serve_busy_retries用于观察该路径在生产的触发频率若频繁触发再升级为 Redis 锁——在经验信号出现之前不提前引入复杂度。4. Token 用量/成本捕获——新的LLMFlow.OPENCODE_TURNspan终结符message.updated载荷携带成本观测所需的全部数据cost: 0.00107985, tokens: {total: ..., input: ..., output: ..., reasoning: ..., cache: {read: ..., write: ...}}, modelID: gpt-4o-mini, providerID: openai实现步骤给 flows.py 的LLMFlow枚举加OPENCODE_TURN回合开始时经traced_llm_call(flowLLMFlow.OPENCODE_TURN, model…, provider…)开启 generation spanmodel/provider 来自opencode.json配置或由首个session.next.model.switched事件填充终结时设置 span 属性cost、tokens.input、tokens.output、tokens.total、tokens.reasoning、tokens.cache.read、tokens.cache.write并关闭。不做 per-token 延迟的 span 字段——底层 LLM 调用是 opencode 发起的不是我们聚合成本/token 即我们拥有的可观测性。这是并行工作项不阻塞客户端在ACP_TRANSPORTserve标志后落地但必须在生产翻转标志前落地否则过渡期会丢失成本遥测。实现中该能力以ContextUsagePacket的形式体现在翻译层message.updated携带的info.tokens含 cache.read/write与info.cost被聚合为ContextUsagePacket(used_tokens..., cost...)直接产出。从设计到代码文档骨架与实现的对应设计文档给出的代码骨架展示了核心结构serve_client.py 的__init__与其一致# backend/onyx/server/features/build/sandbox/opencode/serve_client.py class OpencodeServeClient: def __init__( self, base_url, password, *, event_bus, client_infoNone, timeoutsNone ): self._base_url base_url.rstrip(/) self._auth httpx.BasicAuth(onyx, password) if password else None self._timeouts timeouts or ClientTimeouts() # Unary-only client. request_timeout bounds GET/POST against /session, # /prompt_async, /abort, etc. The long-lived /event SSE stream lives on # the shared per-pod PodEventBus, which owns its own httpx.stream with # event_read_timeout — that way the buss per-frame idle timeout is # not capped by this clients unary read timeout. self._http httpx.Client( base_urlself._base_url, authself._auth, timeouthttpx.Timeout( connectself._timeouts.connect_timeout, readself._timeouts.request_timeout, writeself._timeouts.request_timeout, poolself._timeouts.connect_timeout, ), )设计与实现的差异点上文已逐一展开用户名从onyx修正为opencodeper-call 读者线程演进为共享PodEventBusmessage.updated从主终结符修正为每 step 信息源终结交由session.idle/session.status兜底超时从单一值拆为 inactivity 超时 绝对墙钟超时新增 401 自愈与冷 pod 重试。骨架中_reader_loop与_consume_until_terminator共同实现死读者快速失败_consume_until_terminator中每次q.get(timeout1.0)都检查_ReaderEnded哨兵若在终结符前看到它则合成Error并返回——这一杜绝 15 分钟挂起的结构性修复在实现中由PodEventBus的BUS_CLOSED_SENTINEL_consume_from_bus的显式检查延续并因unsubscribe的确定性清理而更加健壮。至此OpencodeServeClient的设计与实现闭环对外是一个契约不变的同步生成器客户端对内以共享事件总线 纯函数翻译器 累计对账补洞 统一中止机制解决了 ACP 传输层的终止符丢失、断线丢包与跨会话事件路由问题。剩余未决问题何时翻转传输标志、何时删除 ACP 代码已不属于客户端库本身详见 drop-acp-layer.md。【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价