package main // 本文件把**子代理结果回填**接进组合根(M32;design/05 §2/§5/§9)。 // // 为什么必须在组合根装配,而不是在网关 `New` 里顺手订阅: // // - 回填要写的是**父会话的聊天历史**,而网关 New 拿到的 History 与组合根 // `as.history` 是同一个 `*history.Store`,但它此刻还没被 hub 的广播出口包上 —— // 落库之后必须用**同一个** `message_persisted` 扇出(`Server.BroadcastPersisted`) // 才能让在线端与刷新/重连的端看到同一行; // - 与 hub 的分工:hub 只把主题归一成 `subagent_result` 帧(投递),回填订阅者只把 // 结果写成父会话的一行系统注记(落库)。两者订阅**同一个主题**、互不相识, // 因此以后改帧不会牵连落库,改落库也不会牵连帧。 // // 订阅用的 ctx 是 `context.Background()`:组合根这一步没有任何长生命周期 ctx 可用 // (`newApp` 的 ctx 只用于构造期的库操作),而进程关停时 `app.close` 会 // `a.bus.Close()` —— 它取消所有订阅的处理器 ctx,因此这里不需要第二个停止开关。 // 这与 hub(`Server.Start` 里自建 ctx 并在 `Close` 取消)是同一取舍。 import ( "context" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/gateway" ) // backfillWireLogSource 是这条接线自己的日志来源:订阅成功/失败要能按来源定位, // 而不是混在 `main` 或 `gateway` 的裸日志里(AGENTS §10 的日志来源铁律)。 const backfillWireLogSource = "cmd/cyrene/wire_subagent_backfill" // wireSubagentBackfill 把「后台子代理结果 → 父会话系统注记」的订阅者接到组合根的 // 进程内总线上(与调度器、网关 hub **同一个** bus 实例)。 // // 三条降级口径(都**必须可见**,绝不静默): // // - 没有 bus 或没有历史存储:这条回填此刻做不了(未接持久化的构建); // - 订阅失败:总线已关闭或主题非法 —— 那意味着后台结果永远不会回填; // - 广播出口缺失(网关为 nil):仍然落库(历史是唯一事实来源),只是不推帧。 func wireSubagentBackfill(a *app, gw *gateway.Server) { log := logger.WithSource(a.log, backfillWireLogSource) if a.bus == nil || a.history == nil { log.Warn("subagent backfill not wired: needs both a bus and a history store", "bus", a.bus != nil, "history", a.history != nil) return } backfill, err := gateway.NewSubagentBackfill(gateway.SubagentBackfillDeps{ Bus: a.bus, History: a.history, Broadcast: backfillSink(gw), Log: a.log, }) if err != nil { log.Warn("subagent backfill not wired", "err", err) return } if err := backfill.Start(context.Background()); err != nil { log.Warn("subagent backfill subscribe failed; background results will not reach the parent session", "err", err) return } log.Info("subagent backfill wired", "subject", "subagent.result", "session_role", history.RoleSystem) } // backfillSink 返回回填的下行出口。网关为 nil 时返回**真 nil 接口**(带类型的 nil // 指针会让订阅者里的 `!= nil` 判断失效,落库之后去调一个空接收者)。 func backfillSink(gw *gateway.Server) gateway.SubagentNoteSink { if gw == nil { return nil } return gw } // 编译期锁住这条链路的两端:网关仍是回填的下行出口,历史存储仍是它的落库面。 // 任何一边改签名,这里先红,而不是等到运行期回填静默缺席。 var ( _ gateway.SubagentNoteSink = (*gateway.Server)(nil) _ gateway.SubagentNoteStore = (*history.Store)(nil) )