Add files via upload

This commit is contained in:
公明
2026-03-23 22:17:12 +08:00
committed by GitHub
parent c25d7bb495
commit 3517cf850c
2 changed files with 76 additions and 6 deletions
+45 -1
View File
@@ -4,10 +4,13 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"cyberstrike-ai/internal/agent" "cyberstrike-ai/internal/agent"
"cyberstrike-ai/internal/security"
"github.com/cloudwego/eino/components/tool" "github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema" "github.com/cloudwego/eino/schema"
"github.com/eino-contrib/jsonschema" "github.com/eino-contrib/jsonschema"
) )
@@ -15,8 +18,18 @@ import (
// ExecutionRecorder 可选,在 MCP 工具成功返回且带有 execution id 时回调(用于汇总 mcpExecutionIds)。 // ExecutionRecorder 可选,在 MCP 工具成功返回且带有 execution id 时回调(用于汇总 mcpExecutionIds)。
type ExecutionRecorder func(executionID string) 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 路径。 // 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)) out := make([]tool.BaseTool, 0, len(defs))
for _, d := range defs { for _, d := range defs {
if d.Type != "function" || d.Function.Name == "" { if d.Type != "function" || d.Function.Name == "" {
@@ -32,6 +45,7 @@ func ToolsFromDefinitions(ag *agent.Agent, holder *ConversationHolder, defs []ag
agent: ag, agent: ag,
holder: holder, holder: holder,
record: rec, record: rec,
chunk: toolOutputChunk,
}) })
} }
return out, nil return out, nil
@@ -68,6 +82,7 @@ type mcpBridgeTool struct {
agent *agent.Agent agent *agent.Agent
holder *ConversationHolder holder *ConversationHolder
record ExecutionRecorder record ExecutionRecorder
chunk func(toolName, toolCallID, chunk string)
} }
func (m *mcpBridgeTool) Info(ctx context.Context) (*schema.ToolInfo, error) { 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 { if args == nil {
args = map[string]interface{}{} 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() conv := m.holder.Get()
res, err := m.agent.ExecuteMCPToolForConversation(ctx, conv, m.name, args) res, err := m.agent.ExecuteMCPToolForConversation(ctx, conv, m.name, args)
if err != nil { if err != nil {
@@ -97,5 +138,8 @@ func (m *mcpBridgeTool) InvokableRun(ctx context.Context, argumentsInJSON string
if res.ExecutionID != "" && m.record != nil { if res.ExecutionID != "" && m.record != nil {
m.record(res.ExecutionID) m.record(res.ExecutionID)
} }
if res.IsError {
return ToolErrorPrefix + res.Result, nil
}
return res.Result, nil return res.Result, nil
} }
+31 -5
View File
@@ -95,7 +95,23 @@ func RunDeepAgent(
} }
mainDefs := ag.ToolsForRole(roleTools) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -183,7 +199,7 @@ func RunDeepAgent(
} }
subDefs := ag.ToolsForRole(roleTools) subDefs := ag.ToolsForRole(roleTools)
subTools, err := einomcp.ToolsFromDefinitions(ag, holder, subDefs, recorder) subTools, err := einomcp.ToolsFromDefinitions(ag, holder, subDefs, recorder, toolOutputChunk)
if err != nil { if err != nil {
return nil, fmt.Errorf("子代理 %q 工具: %w", id, err) return nil, fmt.Errorf("子代理 %q 工具: %w", id, err)
} }
@@ -455,14 +471,24 @@ func RunDeepAgent(
if toolName == "" { if toolName == "" {
toolName = mv.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 { if len(preview) > 200 {
preview = preview[:200] + "..." preview = preview[:200] + "..."
} }
data := map[string]interface{}{ data := map[string]interface{}{
"toolName": toolName, "toolName": toolName,
"success": true, "success": !isErr,
"result": msg.Content, "isError": isErr,
"result": content,
"resultPreview": preview, "resultPreview": preview,
"conversationId": conversationID, "conversationId": conversationID,
"einoAgent": ev.AgentName, "einoAgent": ev.AgentName,