资讯动态

happy 项目 Session Protocol 落地指南:Codex/Claude 统一会话消息格式的实现与客户端规范化

发布时间:2026/9/20 9:51:23 来源:尧图企业网站定制
happy 项目 Session Protocol 落地指南Codex/Claude 统一会话消息格式的实现与客户端规范化【免费下载链接】happyMobile and Web client for Codex and Claude Code, with realtime voice, encryption and fully featured项目地址: https://gitcode.com/gh_mirrors/happy20/happy本篇技术指南围绕 docs/plans/session-protocol-impl.md 这一实施计划展开系统讲解 happyCodex 与 Claude Code 的移动端/Web 客户端如何将 CLI 侧混合的output、codex、acp三种消息格式统一为 docs/session-protocol.md 定义的扁平事件流Session Protocol并在happy-app中完成解析、归一化与渲染。读完本文你将掌握会话协议信封与 9 类事件的结构设计、CLI 端 Codex 消息到协议事件的映射与回合追踪机制、App 端NormalizedMessage归一化映射的全部规则以及整套实现所配套的测试与验证策略。背景为什么需要一套统一的会话消息协议在 Session Protocol 落地之前happy 的消息链路同时混用三种格式output早期会话输出格式codexCodex CLI 原始消息包装格式acp基于 Agent Communication Protocol 思想的统一消息格式message/reasoning/tool-call/tool-result。三套格式并存意味着客户端要为每种格式各写一套解析与渲染分支维护成本高且格式间语义不一致例如工具调用在codex里是嵌套结构在acp里是扁平数组。Session Protocol 的定位是用单一、扁平的加密事件流取代上述混合格式。旧会话继续使用历史格式渲染向后兼容新会话则全部走新协议。与 ACP 的本质区别docs/session-protocol.md开篇用一整节对比了真正的 ACPAgent-to-Agent 的 REST 互操作标准与本协议。结论很明确本协议解决的是在移动端/Web 客户端渲染端到端加密的 agent 聊天会话这一完全不同的问题。关注点ACPhappy Session Protocol目的Agent 之间互操作REST加密 agent 会话在移动/Web 端的渲染传输REST SSEWebSocket 上的加密载荷消息模型Message { role, parts[] } MIME 类型以t判别的扁平事件流内容类型MIME 类型text/plain、image/png显式事件类型text、service、file等文件content_url或带 MIME 的 base64先上传、按ref引用图片同文件MIME partfile事件携带可选图片元数据width、height、thumbhash工具调用parts 上的 TrajectoryMetadata一等公民tool-call-start/tool-call-end生命周期7 种运行状态、11 种 SSE 事件turn-start/turn-end agentstart/stop事件身份run 上的 UUID、message 上的 created_at每条消息idcuid2time毫秒不直接采用 ACP 的四个核心理由加密ACP 假定明文 REST而 happy 的载荷是端到端加密的工具调用对用户可见ACP 把工具建模为调试元数据而 happy 需要渲染 spinner、描述与权限对话框图片即时渲染ACP 没有 thumbhash 与尺寸信息happy 的file事件可携带图片元数据以支撑占位布局简单总共 9 类事件客户端用一个switch(ev.t)即可实现完整协议。同时协议也从 ACP 借鉴了三样东西信封上的roleuser/agent、按引用传递内容content_url→ref、生命周期事件与内容事件分离。协议核心信封Envelope与事件流信封结构每个经过加密的消息载荷都是一个信封承载事件主体ev以及消息身份、归属信息{ id: cuid2, time: 1739347200000, role: user | agent, turn: cuid2, subagent: cuid2, ev: { t: ..., ... } }字段类型说明idcuid2全局唯一消息标识timenumberUnix 时间戳毫秒roleuser|agent事件生产者turncuid2?由turn-start建立的回合 id所有 agent 消息必须携带无turn的 agent 消息会被忽略subagentcuid2?可选。子 agent 产生的消息所携带的子 agent 标识必须是适配器生成的 cuid2禁止使用 Claude/Codex 等 provider 原生 idevobject事件体由ev.t判别类型该 schema 在共享包 packages/happy-wire/src/sessionProtocol.ts 中以 Zod 形式落地sessionEnvelopeSchema第 109 行起。值得注意的校验细节事件体sessionEventSchema使用 ZoddiscriminatedUnion(t, [...])判别联合第 95 行起9 类事件类型安全信封上的superRefine强制service、start、stop事件必须role: agent第 133-148 行从 schema 层杜绝角色误用subagent字段用isCuid()refine 校验必须是合法 cuid2信封还扩展了三个可选字段claudeUuidClaude 会话 JSONL 的uuid用于精确回滚点、codexItemIdCodex app-server item id用于 fork/duplicate 的精确回滚点、usage模型用量供客户端更新上下文仪表而无需渲染独立行。9 类事件事件ev.t用途关键字段text展示给用户的文本支持 markdowntext、thinking?service仅 agent 的服务性文本如重连中...按原样展示texttool-call-startagent 开始调用工具call、name、title、description、argstool-call-end工具调用结束按call匹配callfile文件附件须先上传服务器ref、name、size、image?turn-startagent 开始处理建立回合信封上的turnturn-endagent 处理结束关闭回合status:completed|failed|cancelledstart子 agent 生命周期开始标记title?stop子 agent 生命周期结束标记—各事件 JSON 示例取自docs/session-protocol.md// text带思考标记 { t: text, text: Hello, how can I help?, thinking: true } // tool-call-start { t: tool-call-start, call: tc_abc, name: grep, title: Searching for handleClick, description: Searching for handleClick in **src/** directory, args: { pattern: handleClick, path: src/ } } // file图片可带元数据 { t: file, ref: upload_def, name: report.pdf, size: 524288 }其中tool-call-start的title/description支持内联 markdowncode、粗体、斜体、[链接]file事件可携带image.width/image.height/image.thumbhashBase64 编码的 ThumbHash用于附件占位即时布局。设计规则docs/session-protocol.md归纳了七条设计规则理解它们有助于后续映射逻辑的阅读扁平流无嵌套工具边界只是流中的标记先上传文件先上传服务器再按ref引用每条消息都有身份信封上的idcuid2time毫秒9 类事件任何客户端只需一个switch(ev.t)Provider 无关协议中不泄漏任何 agent 后端实现命名一致全部kebab-case内联 markdowntitle/description支持代码、粗体、斜体与链接。示例事件流一个典型回合用户提问 → 回合开始 → 文本 → 工具调用 → 回合结束← { id: a1, time: 1000, role: user, ev: { t: text, text: Find TODOs } } ← { id: a2, time: 1001, role: agent, turn: t2, ev: { t: turn-start } } ← { id: a3, time: 1002, role: agent, turn: t2, ev: { t: text, text: Searching... } } ← { id: a4, time: 1003, role: agent, turn: t2, ev: { t: tool-call-start, call: tc1, name: grep, title: Searching for TODO, description: Searching for TODO in project root, args: { pattern: TODO } } } ← { id: a5, time: 1004, role: agent, turn: t2, ev: { t: tool-call-end, call: tc1 } } ← { id: a6, time: 1005, role: agent, turn: t2, ev: { t: text, text: Found 3 TODOs. } } ← { id: a7, time: 1006, role: agent, turn: t2, ev: { t: turn-end, status: completed } }注意a2的turn-start建立了turn: t2其后所有 agent 消息包括turn-end都携带该turn值——这是 App 端实现回合归组turn grouping的依据。端到端消息流改造实施计划文档清晰刻画了改造前后的消息流。改造前Codex → ApprunCodex.ts从 Codex CLI 接收 MCP 消息调用session.sendCodexMessage()包装为{ role: agent, content: { type: codex, data: body } }加密后经 WebSocket 发送App 解密后由typesRaw.ts的normalizeRawMessage()转为NormalizedMessagereducer.ts处理NormalizedMessage[]生成 UI 用的Message[]。改造后CLI仅 Codex不再调用sendCodexMessage()改用新的sendSessionProtocolMessage()按docs/session-protocol.md发出 7 种事件实施计划所述最终协议文档扩充为 9 类ApptypesRaw.ts的rawAgentRecordSchema判别联合新增type: session分支normalizeRawMessage()将 session-protocol 事件转换为NormalizedMessageReducer基本无改动本就能正确处理NormalizedMessage只需具备回合追踪turn tracking感知。关键分工是格式转换发生在 CLI 层客户端负责把扁平事件流重新归一化成分组结构子 agent 嵌套、回合归组。CLI 侧实现从sendCodexMessage到 session-protocol 事件发送方法sendSessionProtocolMessagepackages/happy-cli/src/api/apiSession.ts 中旧方法sendCodexMessage第 784 行将整个 Codex 消息体原样包装为type: codex新方法sendSessionProtocolMessage(envelope)第 810 行则走统一的信封队列private enqueueSessionProtocolEnvelope(envelope: SessionEnvelope, invalidate: boolean true) { const content { role: session, content: envelope, meta: { sentFrom: cli } }; this.enqueueMessage(content, invalidate); } sendSessionProtocolMessage(envelope: SessionEnvelope) { // 统一入队加密与发送沿用与 sendCodexMessage 相同的 socket 通道 this.enqueueSessionProtocolEnvelope(envelope); }外层线格式docs/plans/session-protocol-impl.md的 Envelope structure 小节即{ role: agent, content: { type: session, // 新判别符 data: { // session-protocol 信封 id: cuid2..., time: 1739347200000, role: agent, turn: turn-id, invoke: parent-call-id, // 仅子 agent 使用 ev: { t: text, text: Hello } } }, meta: { sentFrom: cli } }需要说明的是计划文档中的invoke字段在最终落地的协议中被重命名为subagent见 packages/happy-wire/src/sessionProtocol.ts 的信封 schema语义不变标识子 agent 归属供客户端通过既有 sidechain/tracer 逻辑建立父子嵌套。回合追踪状态机packages/happy-cli/src/codex/runCodex.ts 维护currentTurnId: string | null第 352 行将 Codex 的任务生命周期映射为协议回合task_started → 发出 turn-start设置 currentTurnId agent_message → 发出 textturn currentTurnId agent_reasoning → 发出 textthinking: trueturn currentTurnId exec_command_begin → 发出 tool-call-startturn currentTurnId exec_command_end → 发出 tool-call-endturn currentTurnId task_complete → 发出 turn-end清空 currentTurnId turn_aborted → 发出 turn-endstatus: cancelled清空 currentTurnIdCodex 消息到信封的映射映射逻辑集中在 packages/happy-cli/src/codex/utils/sessionProtocolMapper.ts。runCodex.ts不再直接构造信封而是调用mapCodexProcessorMessageToSessionEnvelopes(message, { currentTurnId })第 564/571/789 行拿到{ currentTurnId, envelopes }后逐条sendSessionProtocolMessage(envelope)发送。Codex/MCP 消息映射为协议事件说明task_startedturn-start建立回合task_complete/turn_abortedturn-endstatus取completed/cancelledagent_messagetext带turn字段agent_reasoning/agent_reasoning_deltatextthinking: true内部推理默认不展示exec_command_begin/exec_approval_requesttool-call-startcall取 MCP 的call_idname为CodexBashtitle为命令摘要description为完整命令args为输入参数exec_command_endtool-call-end按call匹配patch_apply_begintool-call-startname: CodexPatchdiff 类工具patch_apply_endtool-call-end—token_count跳过无协议等价物或按旧格式透传以保持兼容task_started/task_complete/turn_aborted生命周期sendSessionEvent属于生命周期而非消息子 agent 的确定性标识sessionProtocolMapper.ts处理子 agent 的一个细节值得注意provider 原生子 agent id如 Codex 的 task id禁止直接作为协议subagent值协议规范明确要求适配器生成 cuid2。实现采用确定性哈希映射第 73-92 行function deterministicSessionSubagentId(providerSubagent: string): string { const digest createHash(sha256) .update(codex-subagent:${providerSubagent}) .digest(hex); return c${digest.slice(0, 23)}; // 前缀 c 23 位十六进制凑足 24 位 cuid2 形态 }映射表providerSubagentToSessionSubagent保证同一 provider 子 agent 在会话内始终映射到同一个协议 id同时maybeEmitSubagentStart/maybeEmitSubagentStop第 94-120 行负责在子 agent 首条消息前补发start事件、结束后补发stop事件并维护startedSubagents/activeSubagents集合防止重复发射。关于 ReasoningProcessor 的注意点实施计划明确记录了一个现状文档中 ⚠️ 标记ReasoningProcessor与DiffProcessor仍产出旧有的内部结构legacy internal shapesCodex 路径由sessionProtocolMapper.ts在发送前将这些输出二次映射为 session-protocol 信封。这意味着协议信封的产生被收敛在 mapper 一个模块内processor 无需感知协议细节接口更稳定。App 侧实现typesRaw.ts的 session 分支Schema 扩展packages/happy-app/sources/sync/typesRaw.ts 中第 149 行起定义sessionEnvelopeSchema与共享包一致的 envelope 判别联合事件体rawAgentRecordSchema判别联合新增分支type: session第 363 行形如z.object({ type: z.literal(session), data: sessionEnvelopeSchema })第 465-502 行还有一段兼容性兜底当收到role: session的载荷但content.type不是session时会尝试把看起来像信封的对象重新包装成{ type: session, data }保证较老 CLI 版本发出的载荷也能被识别。归一化入口normalizeRawMessage()第 852 行先经rawRecordSchema.safeParse校验失败则告警并返回null实现非法/畸形 session 事件优雅降级随后按role分流role: user→ 直接按用户消息返回role: session→ 调用normalizeSessionEnvelope()role: agent且content.type session→ 同样进入normalizeSessionEnvelope()第 1123 行。事件 →NormalizedMessage映射表normalizeSessionEnvelope()第 601 行起是整套归一化的核心映射规则与计划文档的表格一致协议事件NormalizedMessagerolecontent 类型实现要点text无 thinkingagenttextuuid 信封idparentUUIDsubagent或nulltextthinking: trueagentthinking内部推理独立渲染tool-call-startagenttool-callidev.callname/input/description/title一一对应tool-call-endagenttool-resulttool_use_idev.callis_error取自ev.isErrorturn-start——直接返回null不产生可见消息turn-endevent{ type: ready }触发 ready 处理fileagenttool-calltool-result成对合成显示用工具卡片start/stop——子 agent 生命周期标记不渲染为聊天内容serviceagenttext空文本 usage的纯用量事件不渲染行计划文档中的invoke字段对应实现中的subagent第 624-625 行parentUUID envelope.subagentisSidechain parentUUID ! null从而让子 agent 消息通过既有 sidechain/tracer 逻辑自然嵌套在父工具调用之下。turn字段的处理也完全遵循计划agent 消息缺失turn直接返回null第 618 行有豁免纯用量service事件与user-message-accepted/rejected回执可以在无turn时通过turn值透传到NormalizedMessage.turn供 reducer 做回合归组。file 事件的成对技巧file事件归一化有一个值得专门讲解的实现细节第 794-847 行文件上传在事件发出时已完毕协议里没有独立的文件完成信号。为让 reducer 的 Phase 2/Phase 3 同时看到工具调用的两个半场、使气泡从转圈中直接翻转为已完成归一化在同一条消息内同时产出tool-callname: fileinput含ref/name/size/图片元数据与配对的tool-resulttool_use_id相同否则附件气泡会永远显示 spinner。description按图片/普通文件生成不同文案如Attached image: xxx.png (800x600)。Reducer 与回合生命周期packages/happy-app/sources/sync/reducer 目录下改动保持最小化turn-end→{ type: ready }事件通过既有代码路径触发hasReadyEvent true无需新增逻辑turn-start已在归一化层返回null天然不会产生可见消息子 agent 消息subagent→parentUUID走既有 sidechain/tracer 逻辑完成嵌套回合 id 透传到Message.turn供分组与回合被打断等 UI 场景使用。测试 packages/happy-app/sources/sync/reducer/pendingUserMessages.spec.ts 专门覆盖了回合 id 在 agent text/tool/event 行上的传递以及新旧回合交替时的消息排序如old turn tail → USER → new turnpackages/happy-app/sources/sync/reducer/messageToEvent.ts 处理event类消息ready等向AgentEvent的转换。测试与验证策略实施计划将测试纪律列为硬性要求文档中三次 CRITICAL 强调每个任务必须包含新增/更新测试、所有测试通过后才能进入下一任务、范围变化时同步更新计划文档。测试采用 Vitest与源码同目录共置.test.ts后缀。任务测试覆盖点关键文件Task 1 类型与 schemaZod 校验合法事件通过、非法事件被拒createEnvelope生成正确packages/happy-wire/src/sessionProtocol.ts、packages/happy-cli/src/sessionProtocol/types.test.tsTask 2 发送方法信封包装、加密调用packages/happy-cli/src/api/apiSession.test.tsTask 3 Codex 转换mock session验证各 MCP 消息发出的正确事件类型packages/happy-cli/src/codex/runCodex.ts、packages/happy-cli/src/codex/utils/sessionProtocolMapper.tsTask 4 ReasoningProcessor更新后回调输出格式tool-call-start/end、thinking文本packages/happy-cli/src/codex/__tests__/sessionProtocolMapper.test.tsTask 5 App 解析每类事件归一化结果、畸形输入优雅返回nullpackages/happy-app/sources/sync/typesRaw.tsTask 6 Reducerturn 生命周期事件流转、子 agent 消息在父工具调用下正确嵌套packages/happy-app/sources/sync/reducer/pendingUserMessages.spec.ts等Task 7 验收全量测试套件yarn test typecheckhappy-cli、happy-app 两个包Task 8 文档规格偏差回写docs/session-protocol.md、转换路径内联注释docs/session-protocol.md实施中的已知取舍与注意点计划文档用 ⚠️ 记录了三个实施过程中的关键决策理解它们有助于阅读代码无 lint 脚本packages/happy-cli与packages/happy-app未定义lint脚本验证手段是全量测试套件 两个包的yarn typecheckuuid取信封id而非turnApp 归一化content.type session时消息身份uuid使用信封id保证消息身份唯一subagent原计划中的invoke负责 sidechain 链路关联turn仅用于归组——三者职责分离避免turn重复导致身份冲突processor 保持 legacy 形状ReasoningProcessor/DiffProcessor仍输出旧内部结构由sessionProtocolMapper.ts在发送前统一映射为协议信封协议转换逻辑收敛于单一模块。此外向后兼容是贯穿始终的硬约束旧会话output/codex/acp格式必须继续正常解析与渲染App 端通过判别联合同时支持新旧格式。apiSession.ts中 Claude 路径的mapClaudeLogMessageToSessionEnvelopes第 769 行与closeClaudeSessionTurn第 778 行说明该协议不止服务 Codex——Claude 日志同样被映射为协议信封进一步印证了统一协议的定位相关背景见 docs/session-protocol-claude.md。结语手工验证清单计划文档在 Post-Completion 一节给出落地后的手工验证清单可作为集成验收的最终检查项用真实 Codex 会话验证 App 中消息显示正确验证既有 Claude Code 会话legacy 格式仍正常显示用协议格式验证 abort/resume 流程用协议格式验证权限permission流程。整体来看Session Protocol 的落地是一次典型的协议先行 分层改造实践CLI 端以sessionProtocolMapper收敛所有格式转换App 端以normalizeSessionEnvelope收敛所有协议解析两者之间只通过一套被 Zod 严格校验的扁平信封通信从而让移动/Web 客户端可以用一个switch(ev.t)渲染任意 provider 的 agent 会话。【免费下载链接】happyMobile and Web client for Codex and Claude Code, with realtime voice, encryption and fully featured项目地址: https://gitcode.com/gh_mirrors/happy20/happy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

免费获取报价