断点(M29 勘测定论):后台批次结算后 `subagent.result` 发了、hub 也把它中继成 WS 帧,但**没有任何代码把结果回填父会话** —— `chat_messages` 无行、模型下一轮 看不到,于是「她拍了个子代理出去,但没有下文」。 回填拆成互不冒充的两条面(人格=身份红线:子代理结论绝不写成她的人格发言): - 可观测/历史面(新文件 subagent_backfill.go):独立订阅者(不塞进 hub)订阅 `subagent.result`,写父会话一条 `role=system` 注记(内容以 `[子代理结果]` 开头 并点名 `sub-<id>`),随后走既有 `message_persisted` 扇出 ⇒ 刷新/重连后仍在; - 模型面:同一行经转录播种成为下一回合的**系统注记**(`model.RoleSystem`), 不是「她说过的话」。 落库口径:角色恒为 system(不冒充 user/assistant)、PersonaID 与 TurnID 留空 (注记不属于任何角色、也不属于任何一路回合)。 记忆抽取排除(新文件 subagent_backfill_extract.go):`WithoutSubagentNotes` 装饰 `memory.Extractor`,判据是元数据(role=system + 注记标记,与 loop 的 summaryMarker 同一手法),组合根经 `loopMemoryExtractor` 套上 ⇒ 子代理结论绝不进记忆库;LLM 提取器的异步能力原样保留,压缩摘要(同样是 system)不被误剔。 可观测补齐(M29 §7):`SpawnTool.await` 发布成功补带 source 的 INFO,`b==nil` 与 「无结果」的静默 return 改为 WARN,发布失败 WARN 带上 count/session;回填订阅者 顺手读 `bus.Dropped()`,涨了就记一条 WARN(总线丢事件不再是黑洞)。 测试(均可变异打红):父会话恰一条 system 行且零伪造助手发言;注记/摘要/批次正文 有界且逐代理点名;不可用载荷与落库失败各留 WARN 且不广播;Start 真订阅总线; 结果不进记忆抽取(同步/异步/端到端接缝);组合根真装配(真 bus 发布 → 父会话多 一行)。回归:go build ./... + go vet ./... + go test -count=1 ./... 全绿。 不做:不改 `subagent_result` 帧形状、不加进 frameReachesAll、不新开路由/帧族、 不注入人设、不改 Docs(契约归一建议报塔)。
83 lines
3.2 KiB
Go
83 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration"
|
|
)
|
|
|
|
// 本文件锁住 M32 的接线本身:**组合根真的把回填订阅者挂上了那条总线**。
|
|
//
|
|
// 为什么单独测这一步:这次缺陷的形状就是「所有零件都在、没有一根线」。回填订阅者
|
|
// 写对了、总线也发得对,只要 `wireFrontend` 里少掉那一行调用,主人在界面上看到的
|
|
// 仍然是「没有下文」—— 而且没有任何测试会红。因此这里断言的是**装配后的行为**:
|
|
// 组合根接线 → 真 bus 上发布编排层载荷 → 父会话真的多出一条系统注记行。
|
|
|
|
// waitForBackfillRow 轮询历史,直到父会话出现一条消息或到期。
|
|
func waitForBackfillRow(t *testing.T, st *history.Store, sessionID string) []history.Message {
|
|
t.Helper()
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
rows, err := st.Recent(context.Background(), sessionID, 10)
|
|
if err != nil {
|
|
t.Fatalf("read recent: %v", err)
|
|
}
|
|
if len(rows) > 0 {
|
|
return rows
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
t.Fatal("the parent session never received the backfilled note: the composition root did not wire the subscriber")
|
|
return nil
|
|
}
|
|
|
|
// TestWireSubagentBackfillPersistsThroughBus 是 ④ 的正面证据:装配 → 发布 → 落行。
|
|
func TestWireSubagentBackfillPersistsThroughBus(t *testing.T) {
|
|
b := bus.New()
|
|
t.Cleanup(func() { _ = b.Close() })
|
|
a := &app{bus: b, history: newTestHistory(t), log: testLog()}
|
|
|
|
wireSubagentBackfill(a, nil)
|
|
|
|
payload := &orchestration.SubagentResultEvent{
|
|
SessionID: history.MainSessionID,
|
|
SpawnResult: &orchestration.SpawnResult{
|
|
Status: "done",
|
|
Summary: "子代理汇总: 共 1 个, 1 成功",
|
|
Agents: []orchestration.SpawnAgentResult{{
|
|
ID: "sub-a1", Goal: "调研 X", Role: "cyrene", Status: "done", Summary: "结论:X 可行",
|
|
}},
|
|
},
|
|
}
|
|
if err := b.Publish(context.Background(), orchestration.SubagentResultSubject, payload); err != nil {
|
|
t.Fatalf("publish: %v", err)
|
|
}
|
|
|
|
rows := waitForBackfillRow(t, a.history, history.MainSessionID)
|
|
if len(rows) != 1 {
|
|
t.Fatalf("parent session has %d messages, want exactly the one system note: %+v", len(rows), rows)
|
|
}
|
|
row := rows[0]
|
|
if row.Role != history.RoleSystem {
|
|
t.Fatalf("backfilled row role = %q, want %q (a sub-agent conclusion is not her own words)", row.Role, history.RoleSystem)
|
|
}
|
|
if row.TurnID != "" || row.PersonaID != "" {
|
|
t.Fatalf("backfilled row = %+v, want no turn_id and no persona_id", row)
|
|
}
|
|
}
|
|
|
|
// TestWireSubagentBackfillWithoutHistoryWarns 锁住降级口径:没有历史存储时不装配、
|
|
// 不 panic,而是留一条可见的告警(绝不假装回填成立)。
|
|
func TestWireSubagentBackfillWithoutHistoryWarns(t *testing.T) {
|
|
a := &app{bus: bus.New(), log: testLog()}
|
|
wireSubagentBackfill(a, nil) // 没有 history:只允许告警
|
|
if err := a.bus.Publish(context.Background(), orchestration.SubagentResultSubject,
|
|
&orchestration.SubagentResultEvent{SessionID: history.MainSessionID}); err != nil {
|
|
t.Fatalf("publish on an unwired root: %v", err)
|
|
}
|
|
}
|