package gateway import ( "fmt" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration" ) // 本文件拥有「后台批次结果写成什么一行注记」这一件事(subagent_backfill.go 的正文 // 构造)。它单独成篇的理由与 history_write.go 拆自 history.go 相同:**措辞与上限** // 是可以在评审里被逐字审的东西,与总线/落库的生命周期无关。 // // 三条硬口径: // // 1. **必须点名 `sub-`**:归属可追溯(design/05 §3 的可审计访问也依赖它)。 // 读者与模型都要能分辨「这句是哪个子代理说的」,而不是她说的。 // 2. **绝不写成人格发言**:没有「我说」「我让 XX 去做」,也没有人设口吻 —— 注记是 // 一条**系统事实**,作者是 system(project 红线:人格 = 身份)。 // 3. **有界**:整条注记 ≤ maxSubagentNoteRunes(单代理正文 ≤ maxSubagentNoteBodyRunes), // 使一条失控的子代理无法把父会话上下文淹掉(与 maxSpawnTextRunes 同一理由)。 const ( // subagentNoteMarker 标记「这是一条子代理结果注记」。它是**结构性标记**:同一个 // 包里的 `WithoutSubagentNotes`(subagent_backfill_extract.go)按它把注记挡在 // 记忆抽取之外,两处共用这一个常量,因此判据不会漂移。取法照抄 loop 的 // `summaryMarker`(压缩摘要的系统消息同一手法)。 subagentNoteMarker = "[子代理结果]" // maxSubagentNoteRunes 是整条注记的上限。批次上限是 8 个子代理 // (orchestration 的 maxSpawnBatch),逐个正文 400 rune 时整条仍会被收到这里。 maxSubagentNoteRunes = 2000 // maxSubagentNoteBodyRunes 是单个子代理结论的上限:够读出结论,不够塞进整篇。 maxSubagentNoteBodyRunes = 400 // maxSubagentNoteGoalRunes 是注记里回显的目标上限(目标只用来认出是哪个任务)。 maxSubagentNoteGoalRunes = 120 // maxSubagentNoteSummaryRunes 是批次汇总额的上限。 maxSubagentNoteSummaryRunes = 400 // subagentNoteEmptyBody 是「该子代理什么都没写」时的诚实占位,绝不空着让读者 // 以为注记被截断了。 subagentNoteEmptyBody = "(无结论正文)" ) // subagentNote 把一条后台批次结果渲染成父会话的注记正文。单代理批次写成一行 // (最常见、也最可读);多代理批次写汇总行 + 逐代理一行。 func subagentNote(ev *orchestration.SubagentResultEvent) string { if ev == nil || ev.SpawnResult == nil { return subagentNoteMarker } var b strings.Builder b.WriteString(subagentNoteMarker) b.WriteString(" ") if len(ev.Agents) == 1 { b.WriteString(subagentNoteSingleLine(ev.Agents[0])) return clampNote(b.String(), maxSubagentNoteRunes) } b.WriteString(subagentNoteBatchLine(ev)) for _, a := range ev.Agents { b.WriteString("\n- ") b.WriteString(subagentNoteAgentLine(a)) } return clampNote(b.String(), maxSubagentNoteRunes) } // subagentNoteSingleLine 渲染单代理批次的正文行: // `子代理 sub-x(role/mode)已完成(目标:…):结论`。 func subagentNoteSingleLine(a orchestration.SpawnAgentResult) string { var b strings.Builder b.WriteString("子代理 ") b.WriteString(subagentAgentName(a.ID)) if bind := subagentAgentBinding(a); bind != "" { b.WriteString("(") b.WriteString(bind) b.WriteString(")") } b.WriteString(" ") b.WriteString(subagentStatusWord(a.Status)) if goal := strings.TrimSpace(a.Goal); goal != "" { fmt.Fprintf(&b, "(目标:%s)", clampNote(goal, maxSubagentNoteGoalRunes)) } b.WriteString(":") b.WriteString(subagentAgentBody(a)) return b.String() } // subagentNoteBatchLine 渲染批次汇总行:`2 个子代理批次已完成:<汇总>`。 func subagentNoteBatchLine(ev *orchestration.SubagentResultEvent) string { var b strings.Builder fmt.Fprintf(&b, "%d 个子代理批次%s", len(ev.Agents), subagentStatusWord(ev.Status)) if summary := strings.TrimSpace(ev.Summary); summary != "" { b.WriteString(":") b.WriteString(clampNote(summary, maxSubagentNoteSummaryRunes)) } return b.String() } // subagentNoteAgentLine 渲染批次里的一行:`sub-x(role/mode)已完成:结论`。 func subagentNoteAgentLine(a orchestration.SpawnAgentResult) string { var b strings.Builder b.WriteString(subagentAgentName(a.ID)) if bind := subagentAgentBinding(a); bind != "" { b.WriteString("(") b.WriteString(bind) b.WriteString(")") } b.WriteString(" ") b.WriteString(subagentStatusWord(a.Status)) b.WriteString(":") b.WriteString(subagentAgentBody(a)) return b.String() } // subagentAgentName 返回注记里点名的子代理标识。id 为空时退化为 `sub-?`(绝不留空, // 否则「谁说的」就丢了)。 func subagentAgentName(id string) string { if id = strings.TrimSpace(id); id != "" { return id } return "sub-?" } // subagentAgentBinding 返回该子代理绑定的角色/模式标签(只作归属,不冒充发言人)。 func subagentAgentBinding(a orchestration.SpawnAgentResult) string { parts := make([]string, 0, 2) if role := strings.TrimSpace(a.Role); role != "" { parts = append(parts, role) } if mode := strings.TrimSpace(a.Mode); mode != "" { parts = append(parts, mode) } return strings.Join(parts, "/") } // subagentAgentBody 取该子代理可读的结论:优先执行摘要,其次结果正文;两者都没有 // 时给一个诚实的占位。 func subagentAgentBody(a orchestration.SpawnAgentResult) string { for _, s := range []string{a.Summary, a.Text} { if s = strings.TrimSpace(s); s != "" { return clampNote(collapseBlankLines(s), maxSubagentNoteBodyRunes) } } return subagentNoteEmptyBody } // subagentStatusWord 把编排层的生命周期状态翻成注记里可读的中文。未知状态原样保留 // (绝不把它当成「完成」—— 那正是只看状态会误判的地方)。 func subagentStatusWord(status string) string { switch status { case "done": return "已完成" case "running": return "仍在运行" case "failed": return "失败" case "aborted": return "已中止" case "": return "已结束" default: return status } } // collapseBlankLines 把连续空行收成一行空行,使注记在气泡里不会拉出一大片空白 // (子代理正文常带 markdown 段落间距)。 func collapseBlankLines(s string) string { var b strings.Builder blank := 0 for _, line := range strings.Split(s, "\n") { if strings.TrimSpace(line) == "" { blank++ if blank > 1 { continue } } else { blank = 0 } if b.Len() > 0 { b.WriteString("\n") } b.WriteString(strings.TrimRight(line, " \t\r")) } return strings.TrimSpace(b.String()) } // clampNote 把 s 收到 n 个 rune 以内(超出时以省略号收尾)。n 非正时返回空串。 func clampNote(s string, n int) string { if n <= 0 { return "" } r := []rune(s) if len(r) <= n { return s } return string(r[:n-1]) + "…" }