Files
CyberStrikeAI/internal/multiagent/eino_run_result_builder.go
T
Ed1s0nZandCursor bf1b23f14b fix: prefer exit final_result over supervisor intro text
When supervisor exits with a short assistant intro, surface exit.final_result in the final response so the audit report is not dropped.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-18 11:32:59 +08:00

293 lines
8.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package multiagent
import (
"encoding/json"
"strings"
"cyberstrike-ai/internal/agent"
"cyberstrike-ai/internal/einomcp"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/schema"
)
type einoRunResultBuilderConfig struct {
OrchMode string
EmptyHint string
RunMessages *einoRunMessageAccumulator
AssistantOutput *einoAssistantOutputAccumulator
SnapshotMCPIDs func() []string
ModelFacingTrace func() []adk.Message
}
type einoRunResultBuilder struct {
cfg einoRunResultBuilderConfig
}
func newEinoRunResultBuilder(cfg einoRunResultBuilderConfig) *einoRunResultBuilder {
return &einoRunResultBuilder{cfg: cfg}
}
func (b *einoRunResultBuilder) BuildPartial(runErr error) (*RunResult, error) {
if b == nil || b.cfg.RunMessages == nil || !b.cfg.RunMessages.HasNewMessages() {
return nil, runErr
}
return b.build(true), runErr
}
func (b *einoRunResultBuilder) BuildFinal() *RunResult {
if b == nil {
return &RunResult{}
}
return b.build(false)
}
func (b *einoRunResultBuilder) build(partial bool) *RunResult {
var runMsgs []adk.Message
if b.cfg.RunMessages != nil {
runMsgs = b.cfg.RunMessages.NewMessages()
}
var lastAssistant string
var lastPlanExecuteExecutor string
if b.cfg.AssistantOutput != nil {
lastAssistant = b.cfg.AssistantOutput.LastAssistant()
lastPlanExecuteExecutor = b.cfg.AssistantOutput.LastPlanExecuteExecutor()
}
var modelFacing []adk.Message
if b.cfg.ModelFacingTrace != nil {
modelFacing = b.cfg.ModelFacingTrace()
}
var ids []string
if b.cfg.SnapshotMCPIDs != nil {
ids = b.cfg.SnapshotMCPIDs()
}
return buildEinoRunResultFromAccumulated(
b.cfg.OrchMode,
runMsgs,
modelFacing,
lastAssistant,
lastPlanExecuteExecutor,
b.cfg.EmptyHint,
ids,
partial,
)
}
func einoPartialRunLastOutputHint() string {
return "[执行未正常结束(用户停止、超时或异常)。续跑时请基于上文已产生的工具与结果继续,勿重复已完成步骤。]\n" +
"[Run ended abnormally; continue from the trace above without repeating completed steps.]"
}
func buildEinoRunResultFromAccumulated(
orchMode string,
runAccumulatedMsgs []adk.Message,
persistMsgs []adk.Message,
lastAssistant string,
lastPlanExecuteExecutor string,
emptyHint string,
mcpIDs []string,
partial bool,
) *RunResult {
traceForJSON := persistMsgs
traceJSON := ""
if len(traceForJSON) > 0 {
traceForJSON = markModelFacingTraceForPersistence(traceForJSON)
if histJSON, err := json.Marshal(traceForJSON); err == nil {
traceJSON = string(histJSON)
}
}
cleaned := strings.TrimSpace(lastAssistant)
if orchMode == "plan_execute" {
if e := strings.TrimSpace(lastPlanExecuteExecutor); e != "" {
cleaned = e
} else {
cleaned = UnwrapPlanExecuteUserText(cleaned)
}
}
// exit.final_result 是正式交付物:即使助手正文已有过渡语(如「交付终审报告:」),
// 也必须优先采用 exit 内容,避免 supervisor 等模式只展示空壳开场白。
if exitFinal := strings.TrimSpace(einoExtractExitDeliverableFromMsgs(runAccumulatedMsgs)); exitFinal != "" {
cleaned = einoMergeAssistantIntroWithExitFinal(cleaned, exitFinal)
} else if cleaned == "" {
if fb := strings.TrimSpace(einoExtractFallbackAssistantFromMsgs(runAccumulatedMsgs)); fb != "" {
cleaned = fb
if orchMode == "plan_execute" {
cleaned = UnwrapPlanExecuteUserText(cleaned)
}
}
}
cleaned = dedupeRepeatedParagraphs(cleaned, 80)
cleaned = dedupeParagraphsByLineFingerprint(cleaned, 100)
const maxResponseRunes = 100000
if rs := []rune(cleaned); len(rs) > maxResponseRunes {
cleaned = string(rs[:maxResponseRunes]) + "\n\n... (response truncated / 响应已截断)"
}
lastOut := cleaned
resp := cleaned
if partial && cleaned == "" {
lastOut = einoPartialRunLastOutputHint()
resp = emptyHint
}
out := &RunResult{
Response: resp,
MCPExecutionIDs: mcpIDs,
LastAgentTraceInput: traceJSON,
LastAgentTraceOutput: lastOut,
}
if !partial && out.Response == "" {
out.Response = emptyHint
out.LastAgentTraceOutput = out.Response
}
return out
}
func markModelFacingTraceForPersistence(msgs []adk.Message) []adk.Message {
out := cloneADKMessagesForTrace(msgs)
if len(out) == 0 || out[0] == nil {
return out
}
if out[0].Extra == nil {
out[0].Extra = make(map[string]any, 1)
}
out[0].Extra[agent.ModelFacingTraceVersionKey] = 1
return out
}
// einoExtractExitDeliverableFromMsgs 从轨迹中提取当前轮次的 exit 正式交付物
//(工具输出或 assistant 调用 exit 时的 arguments.final_result)。
// 若更靠近末尾出现了非 exit 的工具结果,则认为 exit 不是终态交付。
func einoExtractExitDeliverableFromMsgs(msgs []adk.Message) string {
for i := len(msgs) - 1; i >= 0; i-- {
m := msgs[i]
if m == nil {
continue
}
switch m.Role {
case schema.Tool:
if strings.EqualFold(strings.TrimSpace(m.ToolName), adk.ToolInfoExit.Name) {
content := strings.TrimSpace(m.Content)
if content != "" && !strings.HasPrefix(content, einomcp.ToolErrorPrefix) {
return content
}
// exit 工具输出为空时,继续向前从 assistant 参数回填。
continue
}
return ""
case schema.Assistant:
if s := einoExtractExitFinalFromAssistantToolCalls(m); s != "" {
return s
}
if einoAssistantHasNonExitToolCall(m) {
return ""
}
}
}
return ""
}
func einoAssistantHasNonExitToolCall(msg *schema.Message) bool {
if msg == nil {
return false
}
for _, tc := range msg.ToolCalls {
if !strings.EqualFold(strings.TrimSpace(tc.Function.Name), adk.ToolInfoExit.Name) {
return true
}
}
return false
}
// einoMergeAssistantIntroWithExitFinal 合并助手过渡语与 exit 交付正文。
// exit 内容优先;若助手正文只是开场白且未被 exit 文本包含,则前置保留。
func einoMergeAssistantIntroWithExitFinal(assistant, exitFinal string) string {
assistant = strings.TrimSpace(assistant)
exitFinal = strings.TrimSpace(exitFinal)
if exitFinal == "" {
return assistant
}
if assistant == "" || assistant == exitFinal {
return exitFinal
}
if strings.Contains(exitFinal, assistant) {
return exitFinal
}
if strings.Contains(assistant, exitFinal) {
return assistant
}
return assistant + "\n\n" + exitFinal
}
// einoExtractFallbackAssistantFromMsgs 在「主通道未产出助手正文」时,从 Eino ADK
// 原生消息轨迹中回填用户可见回复。这里保持克制:只采纳倒序最近的可交付终态,
// 避免把工具调用前的过渡语或子任务过程误升为最终回复。
//
// 可交付终态:
// - exit 工具输出;
// - assistant 调用 exit 时 arguments.final_result;
// - 没有后续普通工具结果截断的纯 assistant 正文。
func einoExtractFallbackAssistantFromMsgs(msgs []adk.Message) string {
if s := einoExtractExitDeliverableFromMsgs(msgs); s != "" {
return s
}
for i := len(msgs) - 1; i >= 0; i-- {
m := msgs[i]
if m == nil {
continue
}
switch m.Role {
case schema.Tool:
// 最近一条是普通工具结果:说明助手尚未给出最终正文,勿回退到更早过程语。
return ""
case schema.Assistant:
if len(m.ToolCalls) == 0 {
if content := strings.TrimSpace(m.Content); content != "" {
return content
}
}
}
}
return ""
}
func einoExtractExitFinalFromAssistantToolCalls(msg *schema.Message) string {
if msg == nil || len(msg.ToolCalls) == 0 {
return ""
}
for i := len(msg.ToolCalls) - 1; i >= 0; i-- {
tc := msg.ToolCalls[i]
if !strings.EqualFold(strings.TrimSpace(tc.Function.Name), adk.ToolInfoExit.Name) {
continue
}
if s := einoParseExitFinalResultArguments(tc.Function.Arguments); s != "" {
return s
}
}
return ""
}
func einoParseExitFinalResultArguments(arguments string) string {
arguments = strings.TrimSpace(arguments)
if arguments == "" {
return ""
}
var wrap struct {
FinalResult json.RawMessage `json:"final_result"`
}
if err := json.Unmarshal([]byte(arguments), &wrap); err != nil || len(wrap.FinalResult) == 0 {
return ""
}
var s string
if err := json.Unmarshal(wrap.FinalResult, &s); err == nil {
return strings.TrimSpace(s)
}
var anyVal interface{}
if err := json.Unmarshal(wrap.FinalResult, &anyVal); err != nil {
return ""
}
b, err := json.Marshal(anyVal)
if err != nil {
return ""
}
return strings.TrimSpace(string(b))
}