From b788bc6dab0ef07da3fa84b761e22ffc6c9103b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Sun, 10 May 2026 02:01:28 +0800 Subject: [PATCH] Add files via upload --- internal/multiagent/eino_adk_run_loop.go | 78 +++++++++++++++++++ .../multiagent/eino_exit_fallback_test.go | 62 +++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 internal/multiagent/eino_exit_fallback_test.go diff --git a/internal/multiagent/eino_adk_run_loop.go b/internal/multiagent/eino_adk_run_loop.go index 96328557..10e46cc8 100644 --- a/internal/multiagent/eino_adk_run_loop.go +++ b/internal/multiagent/eino_adk_run_loop.go @@ -859,6 +859,11 @@ func buildEinoRunResultFromAccumulated( cleaned = UnwrapPlanExecuteUserText(cleaned) } } + if cleaned == "" { + if fb := strings.TrimSpace(einoExtractFallbackAssistantFromMsgs(runAccumulatedMsgs)); fb != "" { + cleaned = fb + } + } cleaned = dedupeRepeatedParagraphs(cleaned, 80) cleaned = dedupeParagraphsByLineFingerprint(cleaned, 100) // 防止超长响应导致 JSON 序列化慢或 OOM(多代理拼接大量工具输出时可能触发)。 @@ -885,6 +890,79 @@ func buildEinoRunResultFromAccumulated( return out } +// einoExtractFallbackAssistantFromMsgs 在「主通道未产出助手正文」时,从 Eino ADK 轨迹中回填用户可见回复。 +// 典型场景:监督者仅调用 exit(final_result 落在 Tool 消息中),或工具结果已写入历史但 lastAssistant 未更新。 +// +// 优先级:最后一次 exit 工具输出 → 最后一条含 exit 的助手 tool_calls 参数中的 final_result。 +func einoExtractFallbackAssistantFromMsgs(msgs []adk.Message) string { + for i := len(msgs) - 1; i >= 0; i-- { + m := msgs[i] + if m == nil || m.Role != schema.Tool { + continue + } + if !strings.EqualFold(strings.TrimSpace(m.ToolName), adk.ToolInfoExit.Name) { + continue + } + content := strings.TrimSpace(m.Content) + if content == "" || strings.HasPrefix(content, einomcp.ToolErrorPrefix) { + continue + } + return content + } + for i := len(msgs) - 1; i >= 0; i-- { + m := msgs[i] + if m == nil || m.Role != schema.Assistant { + continue + } + if s := einoExtractExitFinalFromAssistantToolCalls(m); s != "" { + return s + } + } + 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)) +} + func buildEinoCheckpointID(orchMode string) string { mode := sanitizeEinoPathSegment(strings.TrimSpace(orchMode)) if mode == "" { diff --git a/internal/multiagent/eino_exit_fallback_test.go b/internal/multiagent/eino_exit_fallback_test.go new file mode 100644 index 00000000..57bba91d --- /dev/null +++ b/internal/multiagent/eino_exit_fallback_test.go @@ -0,0 +1,62 @@ +package multiagent + +import ( + "testing" + + "github.com/cloudwego/eino/schema" +) + +func TestEinoExtractFallbackAssistantFromMsgs_exitToolMessage(t *testing.T) { + u := schema.UserMessage("hi") + tm := schema.ToolMessage("answer for user", "call-exit-1") + tm.ToolName = "exit" + if got := einoExtractFallbackAssistantFromMsgs([]*schema.Message{u, tm}); got != "answer for user" { + t.Fatalf("got %q", got) + } +} + +func TestEinoExtractFallbackAssistantFromMsgs_lastExitWins(t *testing.T) { + msgs := []*schema.Message{ + schema.UserMessage("hi"), + toolExitMsg("first", "c1"), + toolExitMsg("second", "c2"), + } + if got := einoExtractFallbackAssistantFromMsgs(msgs); got != "second" { + t.Fatalf("got %q", got) + } +} + +func TestEinoExtractFallbackAssistantFromMsgs_fromAssistantToolCalls(t *testing.T) { + m := schema.AssistantMessage("", []schema.ToolCall{{ + ID: "x", + Type: "function", + Function: schema.FunctionCall{ + Name: "exit", + Arguments: `{"final_result":"from args"}`, + }, + }}) + if got := einoExtractFallbackAssistantFromMsgs([]*schema.Message{m}); got != "from args" { + t.Fatalf("got %q", got) + } +} + +func TestEinoExtractFallbackAssistantFromMsgs_prefersToolOverEarlierAssistant(t *testing.T) { + asst := schema.AssistantMessage("", []schema.ToolCall{{ + ID: "x", + Type: "function", + Function: schema.FunctionCall{ + Name: "exit", + Arguments: `{"final_result":"from args"}`, + }, + }}) + tool := toolExitMsg("from tool", "c1") + if got := einoExtractFallbackAssistantFromMsgs([]*schema.Message{asst, tool}); got != "from tool" { + t.Fatalf("got %q", got) + } +} + +func toolExitMsg(content, callID string) *schema.Message { + m := schema.ToolMessage(content, callID) + m.ToolName = "exit" + return m +}