From 3517cf850cdf864dc6decfbc5c49fc146717a16c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:17:12 +0800 Subject: [PATCH] Add files via upload --- internal/einomcp/mcp_tools.go | 46 ++++++++++++++++++++++++++++++++++- internal/multiagent/runner.go | 36 +++++++++++++++++++++++---- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/internal/einomcp/mcp_tools.go b/internal/einomcp/mcp_tools.go index 5949704a..18d8ce5e 100644 --- a/internal/einomcp/mcp_tools.go +++ b/internal/einomcp/mcp_tools.go @@ -4,10 +4,13 @@ import ( "context" "encoding/json" "fmt" + "strings" "cyberstrike-ai/internal/agent" + "cyberstrike-ai/internal/security" "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" "github.com/cloudwego/eino/schema" "github.com/eino-contrib/jsonschema" ) @@ -15,8 +18,18 @@ import ( // ExecutionRecorder 可选,在 MCP 工具成功返回且带有 execution id 时回调(用于汇总 mcpExecutionIds)。 type ExecutionRecorder func(executionID string) +// ToolErrorPrefix 用于把内部 MCP 执行结果中的 IsError 标记传递到多代理上层。 +// Eino 工具通道目前只支持返回字符串,因此通过前缀标识,随后在多代理 runner 中解析为 success/isError。 +const ToolErrorPrefix = "__CYBERSTRIKE_AI_TOOL_ERROR__\n" + // ToolsFromDefinitions 将单 Agent 使用的 OpenAI 风格工具定义转为 Eino InvokableTool,执行时走 Agent 的 MCP 路径。 -func ToolsFromDefinitions(ag *agent.Agent, holder *ConversationHolder, defs []agent.Tool, rec ExecutionRecorder) ([]tool.BaseTool, error) { +func ToolsFromDefinitions( + ag *agent.Agent, + holder *ConversationHolder, + defs []agent.Tool, + rec ExecutionRecorder, + toolOutputChunk func(toolName, toolCallID, chunk string), +) ([]tool.BaseTool, error) { out := make([]tool.BaseTool, 0, len(defs)) for _, d := range defs { if d.Type != "function" || d.Function.Name == "" { @@ -32,6 +45,7 @@ func ToolsFromDefinitions(ag *agent.Agent, holder *ConversationHolder, defs []ag agent: ag, holder: holder, record: rec, + chunk: toolOutputChunk, }) } return out, nil @@ -68,6 +82,7 @@ type mcpBridgeTool struct { agent *agent.Agent holder *ConversationHolder record ExecutionRecorder + chunk func(toolName, toolCallID, chunk string) } func (m *mcpBridgeTool) Info(ctx context.Context) (*schema.ToolInfo, error) { @@ -86,6 +101,32 @@ func (m *mcpBridgeTool) InvokableRun(ctx context.Context, argumentsInJSON string if args == nil { args = map[string]interface{}{} } + + // Stream tool output (stdout/stderr) to upper layer via security.Executor's callback. + // This enables multi-agent mode to show execution progress on the frontend. + if m.chunk != nil { + toolCallID := compose.GetToolCallID(ctx) + if toolCallID != "" { + if existing, ok := ctx.Value(security.ToolOutputCallbackCtxKey).(security.ToolOutputCallback); ok && existing != nil { + // Chain existing callback (if any) + our progress forwarder. + ctx = context.WithValue(ctx, security.ToolOutputCallbackCtxKey, security.ToolOutputCallback(func(c string) { + existing(c) + if strings.TrimSpace(c) == "" { + return + } + m.chunk(m.name, toolCallID, c) + })) + } else { + ctx = context.WithValue(ctx, security.ToolOutputCallbackCtxKey, security.ToolOutputCallback(func(c string) { + if strings.TrimSpace(c) == "" { + return + } + m.chunk(m.name, toolCallID, c) + })) + } + } + } + conv := m.holder.Get() res, err := m.agent.ExecuteMCPToolForConversation(ctx, conv, m.name, args) if err != nil { @@ -97,5 +138,8 @@ func (m *mcpBridgeTool) InvokableRun(ctx context.Context, argumentsInJSON string if res.ExecutionID != "" && m.record != nil { m.record(res.ExecutionID) } + if res.IsError { + return ToolErrorPrefix + res.Result, nil + } return res.Result, nil } diff --git a/internal/multiagent/runner.go b/internal/multiagent/runner.go index 3104d3b3..9d31afc0 100644 --- a/internal/multiagent/runner.go +++ b/internal/multiagent/runner.go @@ -95,7 +95,23 @@ func RunDeepAgent( } mainDefs := ag.ToolsForRole(roleTools) - mainTools, err := einomcp.ToolsFromDefinitions(ag, holder, mainDefs, recorder) + toolOutputChunk := func(toolName, toolCallID, chunk string) { + // When toolCallId is missing, frontend ignores tool_result_delta. + if progress == nil || toolCallID == "" { + return + } + progress("tool_result_delta", chunk, map[string]interface{}{ + "toolName": toolName, + "toolCallId": toolCallID, + // index/total/iteration are optional for UI; we don't know them in this bridge. + "index": 0, + "total": 0, + "iteration": 0, + "source": "eino", + }) + } + + mainTools, err := einomcp.ToolsFromDefinitions(ag, holder, mainDefs, recorder, toolOutputChunk) if err != nil { return nil, err } @@ -183,7 +199,7 @@ func RunDeepAgent( } subDefs := ag.ToolsForRole(roleTools) - subTools, err := einomcp.ToolsFromDefinitions(ag, holder, subDefs, recorder) + subTools, err := einomcp.ToolsFromDefinitions(ag, holder, subDefs, recorder, toolOutputChunk) if err != nil { return nil, fmt.Errorf("子代理 %q 工具: %w", id, err) } @@ -455,14 +471,24 @@ func RunDeepAgent( if toolName == "" { toolName = mv.ToolName } - preview := msg.Content + + // bridge 工具在 res.IsError=true 时会返回带前缀的内容;这里解析为 success/isError,避免前端误判为成功。 + content := msg.Content + isErr := false + if strings.HasPrefix(content, einomcp.ToolErrorPrefix) { + isErr = true + content = strings.TrimPrefix(content, einomcp.ToolErrorPrefix) + } + + preview := content if len(preview) > 200 { preview = preview[:200] + "..." } data := map[string]interface{}{ "toolName": toolName, - "success": true, - "result": msg.Content, + "success": !isErr, + "isError": isErr, + "result": content, "resultPreview": preview, "conversationId": conversationID, "einoAgent": ev.AgentName,