mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-09-30 13:09:40 +02:00
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>
This commit is contained in:
1 parent
569513f375
commit
bf1b23f14b
2 files changed
+157
-10
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package multiagent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
@@ -119,6 +120,89 @@ func TestEinoRunResultBuilderFinalFallsBackToPlainAssistantTrace(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEinoRunResultBuilderPrefersExitFinalOverAssistantIntro(t *testing.T) {
|
||||
intro := "本轮渗透收束完成。交付终审报告:"
|
||||
report := "## 终审报告\n目标 alvin-whn.top 已完成 getshell 验证。"
|
||||
asst := schema.AssistantMessage(intro, []schema.ToolCall{{
|
||||
ID: "exit-1",
|
||||
Type: "function",
|
||||
Function: schema.FunctionCall{
|
||||
Name: "exit",
|
||||
Arguments: `{"final_result":` + mustJSONString(report) + `}`,
|
||||
},
|
||||
}})
|
||||
runMessages := newEinoRunMessageAccumulator(nil)
|
||||
runMessages.Append(schema.UserMessage("继续"))
|
||||
runMessages.Append(asst)
|
||||
runMessages.Append(toolExitMsg(report, "exit-1"))
|
||||
|
||||
assistantOutput := newEinoAssistantOutputAccumulator("supervisor")
|
||||
assistantOutput.RecordMainAssistant("cyberstrike-supervisor", intro)
|
||||
|
||||
got := newEinoRunResultBuilder(einoRunResultBuilderConfig{
|
||||
OrchMode: "supervisor",
|
||||
EmptyHint: "empty",
|
||||
RunMessages: runMessages,
|
||||
AssistantOutput: assistantOutput,
|
||||
}).BuildFinal()
|
||||
|
||||
want := intro + "\n\n" + report
|
||||
if got.Response != want {
|
||||
t.Fatalf("response = %q, want %q", got.Response, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEinoRunResultBuilderPrefersExitFinalFromArgsWhenToolContentMissing(t *testing.T) {
|
||||
intro := "交付终审报告:"
|
||||
report := "full report body"
|
||||
asst := schema.AssistantMessage(intro, []schema.ToolCall{{
|
||||
ID: "exit-1",
|
||||
Type: "function",
|
||||
Function: schema.FunctionCall{
|
||||
Name: "exit",
|
||||
Arguments: `{"final_result":` + mustJSONString(report) + `}`,
|
||||
},
|
||||
}})
|
||||
runMessages := newEinoRunMessageAccumulator(nil)
|
||||
runMessages.Append(asst)
|
||||
runMessages.Append(toolExitMsg("", "exit-1"))
|
||||
|
||||
assistantOutput := newEinoAssistantOutputAccumulator("supervisor")
|
||||
assistantOutput.RecordMainAssistant("cyberstrike-supervisor", intro)
|
||||
|
||||
got := newEinoRunResultBuilder(einoRunResultBuilderConfig{
|
||||
OrchMode: "supervisor",
|
||||
EmptyHint: "empty",
|
||||
RunMessages: runMessages,
|
||||
AssistantOutput: assistantOutput,
|
||||
}).BuildFinal()
|
||||
|
||||
want := intro + "\n\n" + report
|
||||
if got.Response != want {
|
||||
t.Fatalf("response = %q, want %q", got.Response, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEinoMergeAssistantIntroWithExitFinal(t *testing.T) {
|
||||
if got := einoMergeAssistantIntroWithExitFinal("", "final"); got != "final" {
|
||||
t.Fatalf("empty intro: %q", got)
|
||||
}
|
||||
if got := einoMergeAssistantIntroWithExitFinal("same", "same"); got != "same" {
|
||||
t.Fatalf("identical: %q", got)
|
||||
}
|
||||
if got := einoMergeAssistantIntroWithExitFinal("intro", "intro\n\nbody"); got != "intro\n\nbody" {
|
||||
t.Fatalf("contained intro: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func mustJSONString(s string) string {
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func toolExitMsg(content, callID string) *schema.Message {
|
||||
m := schema.ToolMessage(content, callID)
|
||||
m.ToolName = "exit"
|
||||
|
||||
@@ -104,7 +104,11 @@ func buildEinoRunResultFromAccumulated(
|
||||
cleaned = UnwrapPlanExecuteUserText(cleaned)
|
||||
}
|
||||
}
|
||||
if 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" {
|
||||
@@ -149,15 +153,10 @@ func markModelFacingTraceForPersistence(msgs []adk.Message) []adk.Message {
|
||||
return out
|
||||
}
|
||||
|
||||
// einoExtractFallbackAssistantFromMsgs 在「主通道未产出助手正文」时,从 Eino ADK
|
||||
// 原生消息轨迹中回填用户可见回复。这里保持克制:只采纳倒序最近的可交付终态,
|
||||
// 避免把工具调用前的过渡语或子任务过程误升为最终回复。
|
||||
//
|
||||
// 可交付终态:
|
||||
// - exit 工具输出;
|
||||
// - assistant 调用 exit 时 arguments.final_result;
|
||||
// - 没有后续普通工具结果截断的纯 assistant 正文。
|
||||
func einoExtractFallbackAssistantFromMsgs(msgs []adk.Message) string {
|
||||
// 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 {
|
||||
@@ -170,12 +169,76 @@ func einoExtractFallbackAssistantFromMsgs(msgs []adk.Message) string {
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user