feat(hitl): 增量2a —— board 可序列化(compose checkpoint 的状态载体)

compose checkpoint 把图执行态(含 GenLocalState 的 board)序列化进 store,但
Eino 序列化器只认导出字段,而 board 字段全未导出 → 直接持久化会落成空、resume
丢全部状态。Eino 对同时实现 json.Marshaler+Unmarshaler 的类型改走自定义 JSON
(internal/serialization checkMarshaler),故给 *board 实现一对 JSON 方法映射到
导出 DTO,无需把 board 字段全导出(牵连几十处调用点)。

- board_serde.go: *board 的 MarshalJSON/UnmarshalJSON ↔ boardSnapshot;丢弃
  fatalErr(transient error,中断点必为 nil);schema.RegisterName[*board] 注册
  类型名供 checkpoint 的 State(any) 还原。
- 测试: 快照往返无损 + 编译期断言实现 json.Marshaler/Unmarshaler + fatalErr
  不被持久化。

零行为变更(纯新增)。go test ./... 全绿。
下一步 2b:审批节点 compose.Interrupt + 编译挂 checkpoint store + orchestrator
识别 InterruptInfo 置 waiting 并释放 goroutine。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-29 12:38:23 +08:00
parent f3bf42c432
commit 5a1a994cc2
2 changed files with 139 additions and 0 deletions
@@ -0,0 +1,75 @@
package eino
import (
"encoding/json"
"github.com/cloudwego/eino/schema"
)
// board 的字段全是未导出的,而 Eino 的 checkpoint 序列化器只认导出字段——若直接让 compose
// 持久化 board,会落成空、resume 丢全部执行态。Eino 对「同时实现 json.Marshaler+Unmarshaler」
// 的类型改走自定义 JSON(见 internal/serialization checkMarshaler),故给 *board 实现一对 JSON
// 方法、映射到导出 DTO,即可把整个黑板按需持久化,而无需把 board 字段全导出(牵连几十处调用点)。
//
// 不持久化 fatalErrerrortransient——中断点处必为 nil):审批中断时图尚未失败,
// 它是单次 Invoke 内的致命错标志,跨 resume 无意义。rejected 同理在中断点为 false
// 但仍带上以求快照完整、零歧义。
type boardSnapshot struct {
UID string `json:"uid,omitempty"`
SID string `json:"sid,omitempty"`
Query string `json:"query,omitempty"`
Profile string `json:"profile,omitempty"`
History []*schema.Message `json:"history,omitempty"`
KB string `json:"kb,omitempty"`
Refs []string `json:"refs,omitempty"`
ToolOut []string `json:"tool_out,omitempty"`
Sections []reportSection `json:"sections,omitempty"`
Answer string `json:"answer,omitempty"`
AgentOut []string `json:"agent_out,omitempty"`
Rejected bool `json:"rejected,omitempty"`
}
// MarshalJSON 把黑板序列化为快照(Eino checkpoint 经此持久化整图执行态)。
func (b *board) MarshalJSON() ([]byte, error) {
return json.Marshal(boardSnapshot{
UID: b.uid,
SID: b.sid,
Query: b.query,
Profile: b.profile,
History: b.history,
KB: b.kb,
Refs: b.refs,
ToolOut: b.toolOut,
Sections: b.sections,
Answer: b.answer,
AgentOut: b.agentOut,
Rejected: b.rejected,
})
}
// UnmarshalJSON 从快照还原黑板(resume 时 Eino 据此重建中断前的执行态)。
func (b *board) UnmarshalJSON(data []byte) error {
var s boardSnapshot
if err := json.Unmarshal(data, &s); err != nil {
return err
}
b.uid = s.UID
b.sid = s.SID
b.query = s.Query
b.profile = s.Profile
b.history = s.History
b.kb = s.KB
b.refs = s.Refs
b.toolOut = s.ToolOut
b.sections = s.Sections
b.answer = s.Answer
b.agentOut = s.AgentOut
b.rejected = s.Rejected
return nil
}
// 注册 *board 的序列化类型名:Eino checkpoint 的 State 是 any,持久化时记类型名、
// resume 时据名还原具体类型再走 UnmarshalJSON。名字一经上线不可改(旧 checkpoint 据此解码)。
func init() {
schema.RegisterName[*board]("sundynix_board")
}