Add files via upload

This commit is contained in:
公明
2026-04-28 11:40:09 +08:00
committed by GitHub
parent b53cae3a02
commit 62a0d8cb71
14 changed files with 567 additions and 456 deletions
+9 -9
View File
@@ -145,7 +145,7 @@ func (b *Builder) BuildChainFromConversation(ctx context.Context, conversationID
} }
// 1. 优先尝试从数据库获取保存的最后一轮ReAct输入和输出 // 1. 优先尝试从数据库获取保存的最后一轮ReAct输入和输出
reactInputJSON, modelOutput, err := b.db.GetReActData(conversationID) reactInputJSON, modelOutput, err := b.db.GetAgentTrace(conversationID)
if err != nil { if err != nil {
b.logger.Warn("获取保存的ReAct数据失败,将使用消息历史构建", zap.Error(err)) b.logger.Warn("获取保存的ReAct数据失败,将使用消息历史构建", zap.Error(err))
// 继续使用原来的逻辑 // 继续使用原来的逻辑
@@ -170,7 +170,7 @@ func (b *Builder) BuildChainFromConversation(ctx context.Context, conversationID
messageCount = len(tempMessages) messageCount = len(tempMessages)
} }
dataSource = "database_last_react_input" dataSource = "database_last_agent_trace"
b.logger.Info("使用保存的ReAct数据构建攻击链", b.logger.Info("使用保存的ReAct数据构建攻击链",
zap.String("conversationId", conversationID), zap.String("conversationId", conversationID),
zap.String("dataSource", dataSource), zap.String("dataSource", dataSource),
@@ -183,7 +183,7 @@ func (b *Builder) BuildChainFromConversation(ctx context.Context, conversationID
// userInput = b.extractUserInputFromReActInput(reactInputJSON) // userInput = b.extractUserInputFromReActInput(reactInputJSON)
// 将JSON格式的messages转换为可读格式 // 将JSON格式的messages转换为可读格式
reactInputFinal = b.formatReActInputFromJSON(reactInputJSON) reactInputFinal = b.formatAgentTraceInputFromJSON(reactInputJSON)
} else { } else {
// 2. 如果没有保存的ReAct数据,从对话消息构建 // 2. 如果没有保存的ReAct数据,从对话消息构建
dataSource = "messages_table" dataSource = "messages_table"
@@ -201,7 +201,7 @@ func (b *Builder) BuildChainFromConversation(ctx context.Context, conversationID
} }
// 提取最后一轮ReAct的输入(历史消息+当前用户输入) // 提取最后一轮ReAct的输入(历史消息+当前用户输入)
reactInputFinal = b.buildReActInput(messages) reactInputFinal = b.buildAgentTraceInput(messages)
// 提取大模型最后的输出(最后一条assistant消息) // 提取大模型最后的输出(最后一条assistant消息)
for i := len(messages) - 1; i >= 0; i-- { for i := len(messages) - 1; i >= 0; i-- {
@@ -212,7 +212,7 @@ func (b *Builder) BuildChainFromConversation(ctx context.Context, conversationID
} }
} }
// 多代理:保存的 last_react_input 可能仅为首轮用户消息,不含工具轨迹;补充最后一轮助手的过程详情(与单代理「最后一轮 ReAct」对齐) // 多代理:保存的轨迹列可能仅为首轮用户消息,不含工具轨迹;补充最后一轮助手的过程详情(与单代理完整轨迹对齐)
hasMCPOnAssistant := false hasMCPOnAssistant := false
var lastAssistantID string var lastAssistantID string
for i := len(messages) - 1; i >= 0; i-- { for i := len(messages) - 1; i >= 0; i-- {
@@ -366,8 +366,8 @@ func (b *Builder) formatProcessDetailsForAttackChain(details []database.ProcessD
return strings.TrimSpace(sb.String()) return strings.TrimSpace(sb.String())
} }
// buildReActInput 构建最后一轮ReAct的输入(历史消息+当前用户输入) // buildAgentTraceInput 构建最后一轮ReAct的输入(历史消息+当前用户输入)
func (b *Builder) buildReActInput(messages []database.Message) string { func (b *Builder) buildAgentTraceInput(messages []database.Message) string {
var builder strings.Builder var builder strings.Builder
for _, msg := range messages { for _, msg := range messages {
builder.WriteString(fmt.Sprintf("[%s]: %s\n\n", msg.Role, msg.Content)) builder.WriteString(fmt.Sprintf("[%s]: %s\n\n", msg.Role, msg.Content))
@@ -396,8 +396,8 @@ func (b *Builder) buildReActInput(messages []database.Message) string {
// return "" // return ""
// } // }
// formatReActInputFromJSON 将JSON格式的messages数组转换为可读的字符串格式 // formatAgentTraceInputFromJSON 将JSON格式的messages数组转换为可读的字符串格式
func (b *Builder) formatReActInputFromJSON(reactInputJSON string) string { func (b *Builder) formatAgentTraceInputFromJSON(reactInputJSON string) string {
var messages []map[string]interface{} var messages []map[string]interface{}
if err := json.Unmarshal([]byte(reactInputJSON), &messages); err != nil { if err := json.Unmarshal([]byte(reactInputJSON), &messages); err != nil {
b.logger.Warn("解析ReAct输入JSON失败", zap.Error(err)) b.logger.Warn("解析ReAct输入JSON失败", zap.Error(err))
-1
View File
@@ -165,4 +165,3 @@ func (db *DB) DeleteAttackChain(conversationID string) error {
return nil return nil
} }
+11 -10
View File
@@ -421,20 +421,21 @@ func (db *DB) DeleteConversation(id string) error {
return nil return nil
} }
// SaveReActData 保存最后一轮ReAct的输入和输出 // SaveAgentTrace 保存最后一轮代理消息轨迹与助手输出摘要。
func (db *DB) SaveReActData(conversationID, reactInput, reactOutput string) error { // SQLite 列名仍为 last_react_input / last_react_output,与历史库表兼容;语义上为「全模式代理轨迹」,非仅 ReAct。
func (db *DB) SaveAgentTrace(conversationID, traceInputJSON, assistantOutput string) error {
_, err := db.Exec( _, err := db.Exec(
"UPDATE conversations SET last_react_input = ?, last_react_output = ?, updated_at = ? WHERE id = ?", "UPDATE conversations SET last_react_input = ?, last_react_output = ?, updated_at = ? WHERE id = ?",
reactInput, reactOutput, time.Now(), conversationID, traceInputJSON, assistantOutput, time.Now(), conversationID,
) )
if err != nil { if err != nil {
return fmt.Errorf("保存ReAct数据失败: %w", err) return fmt.Errorf("保存代理轨迹失败: %w", err)
} }
return nil return nil
} }
// GetReActData 获取最后一轮ReAct的输入和输出 // GetAgentTrace 读取 conversations 中保存的代理轨迹(列名 last_react_*)。
func (db *DB) GetReActData(conversationID string) (reactInput, reactOutput string, err error) { func (db *DB) GetAgentTrace(conversationID string) (traceInputJSON, assistantOutput string, err error) {
var input, output sql.NullString var input, output sql.NullString
err = db.QueryRow( err = db.QueryRow(
"SELECT last_react_input, last_react_output FROM conversations WHERE id = ?", "SELECT last_react_input, last_react_output FROM conversations WHERE id = ?",
@@ -444,17 +445,17 @@ func (db *DB) GetReActData(conversationID string) (reactInput, reactOutput strin
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return "", "", fmt.Errorf("对话不存在") return "", "", fmt.Errorf("对话不存在")
} }
return "", "", fmt.Errorf("获取ReAct数据失败: %w", err) return "", "", fmt.Errorf("获取代理轨迹失败: %w", err)
} }
if input.Valid { if input.Valid {
reactInput = input.String traceInputJSON = input.String
} }
if output.Valid { if output.Valid {
reactOutput = output.String assistantOutput = output.String
} }
return reactInput, reactOutput, nil return traceInputJSON, assistantOutput, nil
} }
// ConversationHasToolProcessDetails 对话是否存在已落库的工具调用/结果(用于多代理等场景下 MCP execution id 未汇总时的攻击链判定)。 // ConversationHasToolProcessDetails 对话是否存在已落库的工具调用/结果(用于多代理等场景下 MCP execution id 未汇总时的攻击链判定)。
+1 -1
View File
@@ -52,7 +52,7 @@ func NewDB(dbPath string, logger *zap.Logger) (*DB, error) {
// initTables 初始化数据库表 // initTables 初始化数据库表
func (db *DB) initTables() error { func (db *DB) initTables() error {
// 创建对话表 // 创建对话表last_react_input / last_react_output 存「代理消息轨迹」JSON 与助手摘要,列名保留以兼容已有库)
createConversationsTable := ` createConversationsTable := `
CREATE TABLE IF NOT EXISTS conversations ( CREATE TABLE IF NOT EXISTS conversations (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
+1 -2
View File
@@ -12,7 +12,7 @@ import (
// Vulnerability 漏洞 // Vulnerability 漏洞
type Vulnerability struct { type Vulnerability struct {
ID string `json:"id"` ID string `json:"id"`
ConversationID string `json:"conversation_id"` ConversationID string `json:"conversation_id"`
ConversationTag string `json:"conversation_tag,omitempty"` ConversationTag string `json:"conversation_tag,omitempty"`
TaskTag string `json:"task_tag,omitempty"` TaskTag string `json:"task_tag,omitempty"`
TaskID string `json:"task_id,omitempty"` TaskID string `json:"task_id,omitempty"`
@@ -367,4 +367,3 @@ func (db *DB) GetVulnerabilityFilterOptions() (map[string][]string, error) {
"task_tags": taskTags, "task_tags": taskTags,
}, nil }, nil
} }
+10 -10
View File
@@ -55,14 +55,14 @@ func New(level, output string) *Logger {
} }
func (l *Logger) Fatal(msg string, fields ...interface{}) { func (l *Logger) Fatal(msg string, fields ...interface{}) {
zapFields := make([]zap.Field, 0, len(fields)) zapFields := make([]zap.Field, 0, len(fields))
for _, f := range fields { for _, f := range fields {
switch v := f.(type) { switch v := f.(type) {
case error: case error:
zapFields = append(zapFields, zap.Error(v)) zapFields = append(zapFields, zap.Error(v))
default: default:
zapFields = append(zapFields, zap.Any("field", v)) zapFields = append(zapFields, zap.Any("field", v))
} }
} }
l.Logger.Fatal(msg, zapFields...) l.Logger.Fatal(msg, zapFields...)
} }
+10 -10
View File
@@ -62,7 +62,7 @@ func TestExternalMCPManager_RemoveConfig(t *testing.T) {
manager := NewExternalMCPManager(logger) manager := NewExternalMCPManager(logger)
cfg := config.ExternalMCPServerConfig{ cfg := config.ExternalMCPServerConfig{
Command: "python3", Command: "python3",
ExternalMCPEnable: false, ExternalMCPEnable: false,
} }
@@ -86,17 +86,17 @@ func TestExternalMCPManager_GetStats(t *testing.T) {
// 添加多个配置 // 添加多个配置
manager.AddOrUpdateConfig("enabled1", config.ExternalMCPServerConfig{ manager.AddOrUpdateConfig("enabled1", config.ExternalMCPServerConfig{
Command: "python3", Command: "python3",
ExternalMCPEnable: true, ExternalMCPEnable: true,
}) })
manager.AddOrUpdateConfig("enabled2", config.ExternalMCPServerConfig{ manager.AddOrUpdateConfig("enabled2", config.ExternalMCPServerConfig{
URL: "http://127.0.0.1:8081/mcp", URL: "http://127.0.0.1:8081/mcp",
ExternalMCPEnable: true, ExternalMCPEnable: true,
}) })
manager.AddOrUpdateConfig("disabled1", config.ExternalMCPServerConfig{ manager.AddOrUpdateConfig("disabled1", config.ExternalMCPServerConfig{
Command: "python3", Command: "python3",
ExternalMCPEnable: false, ExternalMCPEnable: false,
}) })
@@ -122,11 +122,11 @@ func TestExternalMCPManager_LoadConfigs(t *testing.T) {
externalMCPConfig := config.ExternalMCPConfig{ externalMCPConfig := config.ExternalMCPConfig{
Servers: map[string]config.ExternalMCPServerConfig{ Servers: map[string]config.ExternalMCPServerConfig{
"loaded1": { "loaded1": {
Command: "python3", Command: "python3",
ExternalMCPEnable: true, ExternalMCPEnable: true,
}, },
"loaded2": { "loaded2": {
URL: "http://127.0.0.1:8081/mcp", URL: "http://127.0.0.1:8081/mcp",
ExternalMCPEnable: false, ExternalMCPEnable: false,
}, },
}, },
@@ -153,9 +153,9 @@ func TestLazySDKClient_InitializeFails(t *testing.T) {
logger := zap.NewNop() logger := zap.NewNop()
// 使用不存在的 HTTP 地址,Initialize 应失败 // 使用不存在的 HTTP 地址,Initialize 应失败
cfg := config.ExternalMCPServerConfig{ cfg := config.ExternalMCPServerConfig{
Type: "http", Type: "http",
URL: "http://127.0.0.1:19999/nonexistent", URL: "http://127.0.0.1:19999/nonexistent",
Timeout: 2, Timeout: 2,
} }
c := newLazySDKClient(cfg, logger) c := newLazySDKClient(cfg, logger)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -176,7 +176,7 @@ func TestExternalMCPManager_StartStopClient(t *testing.T) {
// 添加一个禁用的配置 // 添加一个禁用的配置
cfg := config.ExternalMCPServerConfig{ cfg := config.ExternalMCPServerConfig{
Command: "python3", Command: "python3",
ExternalMCPEnable: false, ExternalMCPEnable: false,
} }
+508 -394
View File
@@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
@@ -109,11 +110,11 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
} }
}() }()
var lastRunMsgs []adk.Message
var lastAssistant string var lastAssistant string
var lastPlanExecuteExecutor string var lastPlanExecuteExecutor string
msgs := append([]adk.Message(nil), baseMsgs...) msgs := append([]adk.Message(nil), baseMsgs...)
runAccumulatedMsgs := append([]adk.Message(nil), msgs...) runAccumulatedMsgs := append([]adk.Message(nil), msgs...)
baseAccumulatedCount := len(runAccumulatedMsgs)
emptyHint := strings.TrimSpace(args.EmptyResponseMessage) emptyHint := strings.TrimSpace(args.EmptyResponseMessage)
if emptyHint == "" { if emptyHint == "" {
@@ -132,125 +133,150 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
pendingByID := make(map[string]toolCallPendingInfo) pendingByID := make(map[string]toolCallPendingInfo)
pendingQueueByAgent := make(map[string][]string) pendingQueueByAgent := make(map[string][]string)
markPending := func(tc toolCallPendingInfo) { markPending := func(tc toolCallPendingInfo) {
if tc.ToolCallID == "" { if tc.ToolCallID == "" {
return return
}
pendingByID[tc.ToolCallID] = tc
pendingQueueByAgent[tc.EinoAgent] = append(pendingQueueByAgent[tc.EinoAgent], tc.ToolCallID)
} }
popNextPendingForAgent := func(agentName string) (toolCallPendingInfo, bool) { pendingByID[tc.ToolCallID] = tc
q := pendingQueueByAgent[agentName] pendingQueueByAgent[tc.EinoAgent] = append(pendingQueueByAgent[tc.EinoAgent], tc.ToolCallID)
for len(q) > 0 { }
id := q[0] popNextPendingForAgent := func(agentName string) (toolCallPendingInfo, bool) {
q = q[1:] q := pendingQueueByAgent[agentName]
pendingQueueByAgent[agentName] = q for len(q) > 0 {
if tc, ok := pendingByID[id]; ok { id := q[0]
delete(pendingByID, id) q = q[1:]
return tc, true pendingQueueByAgent[agentName] = q
} if tc, ok := pendingByID[id]; ok {
delete(pendingByID, id)
return tc, true
} }
return toolCallPendingInfo{}, false
} }
removePendingByID := func(toolCallID string) { return toolCallPendingInfo{}, false
if toolCallID == "" { }
return removePendingByID := func(toolCallID string) {
} if toolCallID == "" {
delete(pendingByID, toolCallID) return
} }
flushAllPendingAsFailed := func(err error) { delete(pendingByID, toolCallID)
if progress == nil { }
pendingByID = make(map[string]toolCallPendingInfo) flushAllPendingAsFailed := func(err error) {
pendingQueueByAgent = make(map[string][]string) if progress == nil {
return
}
msg := ""
if err != nil {
msg = err.Error()
}
for _, tc := range pendingByID {
toolName := tc.ToolName
if strings.TrimSpace(toolName) == "" {
toolName = "unknown"
}
progress("tool_result", fmt.Sprintf("工具结果 (%s)", toolName), map[string]interface{}{
"toolName": toolName,
"success": false,
"isError": true,
"result": msg,
"resultPreview": msg,
"toolCallId": tc.ToolCallID,
"conversationId": conversationID,
"einoAgent": tc.EinoAgent,
"einoRole": tc.EinoRole,
"source": "eino",
})
}
pendingByID = make(map[string]toolCallPendingInfo) pendingByID = make(map[string]toolCallPendingInfo)
pendingQueueByAgent = make(map[string][]string) pendingQueueByAgent = make(map[string][]string)
return
} }
msg := ""
if err != nil {
msg = err.Error()
}
for _, tc := range pendingByID {
toolName := tc.ToolName
if strings.TrimSpace(toolName) == "" {
toolName = "unknown"
}
progress("tool_result", fmt.Sprintf("工具结果 (%s)", toolName), map[string]interface{}{
"toolName": toolName,
"success": false,
"isError": true,
"result": msg,
"resultPreview": msg,
"toolCallId": tc.ToolCallID,
"conversationId": conversationID,
"einoAgent": tc.EinoAgent,
"einoRole": tc.EinoRole,
"source": "eino",
})
}
pendingByID = make(map[string]toolCallPendingInfo)
pendingQueueByAgent = make(map[string][]string)
}
runnerCfg := adk.RunnerConfig{ runnerCfg := adk.RunnerConfig{
Agent: da, Agent: da,
EnableStreaming: true, EnableStreaming: true,
} }
if cp := strings.TrimSpace(args.CheckpointDir); cp != "" { var cpStore *fileCheckPointStore
cpDir := filepath.Join(cp, sanitizeEinoPathSegment(conversationID)) var checkPointID string
st, stErr := newFileCheckPointStore(cpDir) if cp := strings.TrimSpace(args.CheckpointDir); cp != "" {
if stErr != nil { cpDir := filepath.Join(cp, sanitizeEinoPathSegment(conversationID))
if logger != nil { st, stErr := newFileCheckPointStore(cpDir)
logger.Warn("eino checkpoint store disabled", zap.String("dir", cpDir), zap.Error(stErr)) if stErr != nil {
} if logger != nil {
} else { logger.Warn("eino checkpoint store disabled", zap.String("dir", cpDir), zap.Error(stErr))
runnerCfg.CheckPointStore = st }
if logger != nil { } else {
logger.Info("eino runner: checkpoint store enabled", zap.String("dir", cpDir)) cpStore = st
} checkPointID = buildEinoCheckpointID(orchMode)
runnerCfg.CheckPointStore = st
if logger != nil {
logger.Info("eino runner: checkpoint store enabled",
zap.String("dir", cpDir),
zap.String("checkPointID", checkPointID))
} }
} }
}
runner := adk.NewRunner(ctx, runnerCfg) runner := adk.NewRunner(ctx, runnerCfg)
iter := runner.Run(ctx, msgs) var iter *adk.AsyncIterator[*adk.AgentEvent]
handleRunErr := func(runErr error) error { if cpStore != nil && checkPointID != "" {
if runErr == nil { if _, existed, getErr := cpStore.Get(ctx, checkPointID); getErr != nil {
return nil if logger != nil {
logger.Warn("eino checkpoint preflight get failed", zap.String("checkPointID", checkPointID), zap.Error(getErr))
} }
if errors.Is(runErr, context.DeadlineExceeded) { } else if existed {
flushAllPendingAsFailed(runErr) if progress != nil {
if progress != nil { progress("progress", "检测到断点,正在从中断节点恢复执行...", map[string]interface{}{
progress("error", runErr.Error(), map[string]interface{}{ "conversationId": conversationID,
"conversationId": conversationID, "source": "eino",
"source": "eino", "orchestration": orchMode,
"errorKind": "timeout", "checkPointID": checkPointID,
}) })
}
if logger != nil {
logger.Info("eino runner: resume from checkpoint", zap.String("checkPointID", checkPointID))
}
resumeIter, resumeErr := runner.Resume(ctx, checkPointID)
if resumeErr == nil {
iter = resumeIter
} else {
if logger != nil {
logger.Warn("eino runner: resume failed, fallback to fresh run",
zap.String("checkPointID", checkPointID),
zap.Error(resumeErr))
} }
return runErr
}
// context.Canceled 是唯一应当直接终止编排的错误(用户关闭页面、主动停止等)。
if errors.Is(runErr, context.Canceled) {
flushAllPendingAsFailed(runErr)
if progress != nil { if progress != nil {
progress("error", runErr.Error(), map[string]interface{}{ progress("progress", "断点恢复失败,已回退为全新执行。", map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
})
}
return runErr
}
if isEinoIterationLimitError(runErr) {
flushAllPendingAsFailed(runErr)
if progress != nil {
progress("iteration_limit_reached", runErr.Error(), map[string]interface{}{
"conversationId": conversationID, "conversationId": conversationID,
"source": "eino", "source": "eino",
"orchestration": orchMode, "orchestration": orchMode,
}) "checkPointID": checkPointID,
progress("error", runErr.Error(), map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
"errorKind": "iteration_limit",
}) })
} }
return runErr
} }
}
}
if iter == nil {
if checkPointID != "" {
iter = runner.Run(ctx, msgs, adk.WithCheckPointID(checkPointID))
} else {
iter = runner.Run(ctx, msgs)
}
}
handleRunErr := func(runErr error) error {
if runErr == nil {
return nil
}
if errors.Is(runErr, context.DeadlineExceeded) {
flushAllPendingAsFailed(runErr)
if progress != nil {
progress("error", runErr.Error(), map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
"errorKind": "timeout",
})
}
return runErr
}
// context.Canceled 是唯一应当直接终止编排的错误(用户关闭页面、主动停止等)。
if errors.Is(runErr, context.Canceled) {
flushAllPendingAsFailed(runErr) flushAllPendingAsFailed(runErr)
if progress != nil { if progress != nil {
progress("error", runErr.Error(), map[string]interface{}{ progress("error", runErr.Error(), map[string]interface{}{
@@ -260,249 +286,190 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
} }
return runErr return runErr
} }
if isEinoIterationLimitError(runErr) {
for { flushAllPendingAsFailed(runErr)
// 检测 context 取消(用户关闭浏览器、请求超时等),flush pending 工具状态避免 UI 卡在 "执行中"。 if progress != nil {
select { progress("iteration_limit_reached", runErr.Error(), map[string]interface{}{
case <-ctx.Done():
flushAllPendingAsFailed(ctx.Err())
if progress != nil {
progress("error", "Request cancelled / 请求已取消", map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
})
}
return nil, ctx.Err()
default:
}
ev, ok := iter.Next()
if !ok {
if len(pendingByID) > 0 {
orphanCount := len(pendingByID)
flushAllPendingAsFailed(errors.New("pending tool call missing result before run completion"))
if progress != nil {
progress("eino_pending_orphaned", "pending tool calls were force-closed at run end", map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
"orchestration": orchMode,
"pendingCount": orphanCount,
})
}
}
lastRunMsgs = runAccumulatedMsgs
break
}
if ev == nil {
continue
}
if ev.Err != nil {
if retErr := handleRunErr(ev.Err); retErr != nil {
return nil, retErr
}
}
if ev.AgentName != "" && progress != nil {
iterEinoAgent := orchestratorName
if orchMode == "plan_execute" {
if a := strings.TrimSpace(ev.AgentName); a != "" {
iterEinoAgent = a
}
}
if streamsMainAssistant(ev.AgentName) {
if einoMainRound == 0 {
einoMainRound = 1
progress("iteration", "", map[string]interface{}{
"iteration": 1,
"einoScope": "main",
"einoRole": "orchestrator",
"einoAgent": iterEinoAgent,
"orchestration": orchMode,
"conversationId": conversationID,
"source": "eino",
})
} else if einoLastAgent != "" && !streamsMainAssistant(einoLastAgent) {
einoMainRound++
progress("iteration", "", map[string]interface{}{
"iteration": einoMainRound,
"einoScope": "main",
"einoRole": "orchestrator",
"einoAgent": iterEinoAgent,
"orchestration": orchMode,
"conversationId": conversationID,
"source": "eino",
})
}
}
einoLastAgent = ev.AgentName
progress("progress", fmt.Sprintf("[Eino] %s", ev.AgentName), map[string]interface{}{
"conversationId": conversationID, "conversationId": conversationID,
"einoAgent": ev.AgentName, "source": "eino",
"einoRole": einoRoleTag(ev.AgentName),
"orchestration": orchMode, "orchestration": orchMode,
}) })
progress("error", runErr.Error(), map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
"errorKind": "iteration_limit",
})
} }
if ev.Output == nil || ev.Output.MessageOutput == nil { return runErr
continue }
} flushAllPendingAsFailed(runErr)
mv := ev.Output.MessageOutput if progress != nil {
progress("error", runErr.Error(), map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
})
}
return runErr
}
if mv.IsStreaming && mv.MessageStream != nil { takePartial := func(runErr error) (*RunResult, error) {
streamHeaderSent := false if len(runAccumulatedMsgs) <= baseAccumulatedCount {
var reasoningStreamID string return nil, runErr
var toolStreamFragments []schema.ToolCall }
var subAssistantBuf strings.Builder ids := snapshotMCPIDs()
var subReplyStreamID string return buildEinoRunResultFromAccumulated(
var mainAssistantBuf strings.Builder orchMode, runAccumulatedMsgs, lastAssistant, lastPlanExecuteExecutor, emptyHint, ids, true,
var streamRecvErr error ), runErr
for { }
chunk, rerr := mv.MessageStream.Recv()
if rerr != nil {
if errors.Is(rerr, io.EOF) {
break
}
if logger != nil {
logger.Warn("eino stream recv error, flushing incomplete stream",
zap.Error(rerr),
zap.String("agent", ev.AgentName),
zap.Int("toolFragments", len(toolStreamFragments)))
}
streamRecvErr = rerr
break
}
if chunk == nil {
continue
}
if progress != nil && strings.TrimSpace(chunk.ReasoningContent) != "" {
if reasoningStreamID == "" {
reasoningStreamID = fmt.Sprintf("eino-reasoning-%s-%d", conversationID, atomic.AddInt64(&reasoningStreamSeq, 1))
progress("thinking_stream_start", " ", map[string]interface{}{
"streamId": reasoningStreamID,
"source": "eino",
"einoAgent": ev.AgentName,
"einoRole": einoRoleTag(ev.AgentName),
"orchestration": orchMode,
})
}
progress("thinking_stream_delta", chunk.ReasoningContent, map[string]interface{}{
"streamId": reasoningStreamID,
})
}
if chunk.Content != "" {
if progress != nil && streamsMainAssistant(ev.AgentName) {
if !streamHeaderSent {
progress("response_start", "", map[string]interface{}{
"conversationId": conversationID,
"mcpExecutionIds": snapshotMCPIDs(),
"messageGeneratedBy": "eino:" + ev.AgentName,
"einoRole": "orchestrator",
"einoAgent": ev.AgentName,
"orchestration": orchMode,
})
streamHeaderSent = true
}
progress("response_delta", chunk.Content, map[string]interface{}{
"conversationId": conversationID,
"mcpExecutionIds": snapshotMCPIDs(),
"einoRole": "orchestrator",
"einoAgent": ev.AgentName,
"orchestration": orchMode,
})
mainAssistantBuf.WriteString(chunk.Content)
} else if !streamsMainAssistant(ev.AgentName) {
if progress != nil {
if subReplyStreamID == "" {
subReplyStreamID = fmt.Sprintf("eino-sub-reply-%s-%d", conversationID, atomic.AddInt64(&einoSubReplyStreamSeq, 1))
progress("eino_agent_reply_stream_start", "", map[string]interface{}{
"streamId": subReplyStreamID,
"einoAgent": ev.AgentName,
"einoRole": "sub",
"conversationId": conversationID,
"source": "eino",
})
}
progress("eino_agent_reply_stream_delta", chunk.Content, map[string]interface{}{
"streamId": subReplyStreamID,
"conversationId": conversationID,
})
}
subAssistantBuf.WriteString(chunk.Content)
}
}
if len(chunk.ToolCalls) > 0 {
toolStreamFragments = append(toolStreamFragments, chunk.ToolCalls...)
}
}
if streamsMainAssistant(ev.AgentName) {
if s := strings.TrimSpace(mainAssistantBuf.String()); s != "" {
lastAssistant = s
runAccumulatedMsgs = append(runAccumulatedMsgs, schema.AssistantMessage(s, nil))
if orchMode == "plan_execute" && strings.EqualFold(strings.TrimSpace(ev.AgentName), "executor") {
lastPlanExecuteExecutor = UnwrapPlanExecuteUserText(s)
}
}
}
if subAssistantBuf.Len() > 0 && progress != nil {
if s := strings.TrimSpace(subAssistantBuf.String()); s != "" {
if subReplyStreamID != "" {
progress("eino_agent_reply_stream_end", s, map[string]interface{}{
"streamId": subReplyStreamID,
"einoAgent": ev.AgentName,
"einoRole": "sub",
"conversationId": conversationID,
"source": "eino",
})
} else {
progress("eino_agent_reply", s, map[string]interface{}{
"conversationId": conversationID,
"einoAgent": ev.AgentName,
"einoRole": "sub",
"source": "eino",
})
}
}
}
var lastToolChunk *schema.Message
if merged := mergeStreamingToolCallFragments(toolStreamFragments); len(merged) > 0 {
lastToolChunk = &schema.Message{ToolCalls: merged}
}
tryEmitToolCallsOnce(lastToolChunk, ev.AgentName, orchestratorName, conversationID, progress, toolEmitSeen, subAgentToolStep, markPending)
if streamRecvErr != nil {
if progress != nil {
progress("eino_stream_error", streamRecvErr.Error(), map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
"einoAgent": ev.AgentName,
"einoRole": einoRoleTag(ev.AgentName),
})
}
if retErr := handleRunErr(streamRecvErr); retErr != nil {
return nil, retErr
}
}
continue
}
msg, gerr := mv.GetMessage() for {
if gerr != nil || msg == nil { // 检测 context 取消(用户关闭浏览器、请求超时等),flush pending 工具状态避免 UI 卡在 "执行中"。
continue select {
case <-ctx.Done():
flushAllPendingAsFailed(ctx.Err())
if progress != nil {
progress("error", "Request cancelled / 请求已取消", map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
})
} }
runAccumulatedMsgs = append(runAccumulatedMsgs, msg) return takePartial(ctx.Err())
tryEmitToolCallsOnce(mergeMessageToolCalls(msg), ev.AgentName, orchestratorName, conversationID, progress, toolEmitSeen, subAgentToolStep, markPending) default:
}
if mv.Role == schema.Assistant { ev, ok := iter.Next()
if progress != nil && strings.TrimSpace(msg.ReasoningContent) != "" { if !ok {
progress("thinking", strings.TrimSpace(msg.ReasoningContent), map[string]interface{}{ // iter 结束并不总是“正常完成”:
// 当取消/超时发生在 iter.Next() 阻塞期间时,可能直接返回 !ok。
// 此时必须保留 checkpoint,避免后续恢复时被误判为“无断点”而全量重跑。
if ctxErr := ctx.Err(); ctxErr != nil {
flushAllPendingAsFailed(ctxErr)
if progress != nil {
progress("error", ctxErr.Error(), map[string]interface{}{
"conversationId": conversationID, "conversationId": conversationID,
"source": "eino", "source": "eino",
"einoAgent": ev.AgentName,
"einoRole": einoRoleTag(ev.AgentName),
"orchestration": orchMode,
}) })
} }
body := strings.TrimSpace(msg.Content) return takePartial(ctxErr)
if body != "" { }
if streamsMainAssistant(ev.AgentName) { if len(pendingByID) > 0 {
if progress != nil { orphanCount := len(pendingByID)
flushAllPendingAsFailed(errors.New("pending tool call missing result before run completion"))
if progress != nil {
progress("eino_pending_orphaned", "pending tool calls were force-closed at run end", map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
"orchestration": orchMode,
"pendingCount": orphanCount,
})
}
}
if cpStore != nil && checkPointID != "" {
if p, pErr := cpStore.path(checkPointID); pErr == nil {
if rmErr := os.Remove(p); rmErr != nil && !os.IsNotExist(rmErr) && logger != nil {
logger.Warn("eino checkpoint cleanup failed", zap.String("path", p), zap.Error(rmErr))
}
}
}
break
}
if ev == nil {
continue
}
if ev.Err != nil {
if retErr := handleRunErr(ev.Err); retErr != nil {
return takePartial(retErr)
}
}
if ev.AgentName != "" && progress != nil {
iterEinoAgent := orchestratorName
if orchMode == "plan_execute" {
if a := strings.TrimSpace(ev.AgentName); a != "" {
iterEinoAgent = a
}
}
if streamsMainAssistant(ev.AgentName) {
if einoMainRound == 0 {
einoMainRound = 1
progress("iteration", "", map[string]interface{}{
"iteration": 1,
"einoScope": "main",
"einoRole": "orchestrator",
"einoAgent": iterEinoAgent,
"orchestration": orchMode,
"conversationId": conversationID,
"source": "eino",
})
} else if einoLastAgent != "" && !streamsMainAssistant(einoLastAgent) {
einoMainRound++
progress("iteration", "", map[string]interface{}{
"iteration": einoMainRound,
"einoScope": "main",
"einoRole": "orchestrator",
"einoAgent": iterEinoAgent,
"orchestration": orchMode,
"conversationId": conversationID,
"source": "eino",
})
}
}
einoLastAgent = ev.AgentName
progress("progress", fmt.Sprintf("[Eino] %s", ev.AgentName), map[string]interface{}{
"conversationId": conversationID,
"einoAgent": ev.AgentName,
"einoRole": einoRoleTag(ev.AgentName),
"orchestration": orchMode,
})
}
if ev.Output == nil || ev.Output.MessageOutput == nil {
continue
}
mv := ev.Output.MessageOutput
if mv.IsStreaming && mv.MessageStream != nil {
streamHeaderSent := false
var reasoningStreamID string
var toolStreamFragments []schema.ToolCall
var subAssistantBuf strings.Builder
var subReplyStreamID string
var mainAssistantBuf strings.Builder
var streamRecvErr error
for {
chunk, rerr := mv.MessageStream.Recv()
if rerr != nil {
if errors.Is(rerr, io.EOF) {
break
}
if logger != nil {
logger.Warn("eino stream recv error, flushing incomplete stream",
zap.Error(rerr),
zap.String("agent", ev.AgentName),
zap.Int("toolFragments", len(toolStreamFragments)))
}
streamRecvErr = rerr
break
}
if chunk == nil {
continue
}
if progress != nil && strings.TrimSpace(chunk.ReasoningContent) != "" {
if reasoningStreamID == "" {
reasoningStreamID = fmt.Sprintf("eino-reasoning-%s-%d", conversationID, atomic.AddInt64(&reasoningStreamSeq, 1))
progress("thinking_stream_start", " ", map[string]interface{}{
"streamId": reasoningStreamID,
"source": "eino",
"einoAgent": ev.AgentName,
"einoRole": einoRoleTag(ev.AgentName),
"orchestration": orchMode,
})
}
progress("thinking_stream_delta", chunk.ReasoningContent, map[string]interface{}{
"streamId": reasoningStreamID,
})
}
if chunk.Content != "" {
if progress != nil && streamsMainAssistant(ev.AgentName) {
if !streamHeaderSent {
progress("response_start", "", map[string]interface{}{ progress("response_start", "", map[string]interface{}{
"conversationId": conversationID, "conversationId": conversationID,
"mcpExecutionIds": snapshotMCPIDs(), "mcpExecutionIds": snapshotMCPIDs(),
@@ -511,20 +478,61 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
"einoAgent": ev.AgentName, "einoAgent": ev.AgentName,
"orchestration": orchMode, "orchestration": orchMode,
}) })
progress("response_delta", body, map[string]interface{}{ streamHeaderSent = true
"conversationId": conversationID, }
"mcpExecutionIds": snapshotMCPIDs(), progress("response_delta", chunk.Content, map[string]interface{}{
"einoRole": "orchestrator", "conversationId": conversationID,
"einoAgent": ev.AgentName, "mcpExecutionIds": snapshotMCPIDs(),
"orchestration": orchMode, "einoRole": "orchestrator",
"einoAgent": ev.AgentName,
"orchestration": orchMode,
})
mainAssistantBuf.WriteString(chunk.Content)
} else if !streamsMainAssistant(ev.AgentName) {
if progress != nil {
if subReplyStreamID == "" {
subReplyStreamID = fmt.Sprintf("eino-sub-reply-%s-%d", conversationID, atomic.AddInt64(&einoSubReplyStreamSeq, 1))
progress("eino_agent_reply_stream_start", "", map[string]interface{}{
"streamId": subReplyStreamID,
"einoAgent": ev.AgentName,
"einoRole": "sub",
"conversationId": conversationID,
"source": "eino",
})
}
progress("eino_agent_reply_stream_delta", chunk.Content, map[string]interface{}{
"streamId": subReplyStreamID,
"conversationId": conversationID,
}) })
} }
lastAssistant = body subAssistantBuf.WriteString(chunk.Content)
if orchMode == "plan_execute" && strings.EqualFold(strings.TrimSpace(ev.AgentName), "executor") { }
lastPlanExecuteExecutor = UnwrapPlanExecuteUserText(body) }
} if len(chunk.ToolCalls) > 0 {
} else if progress != nil { toolStreamFragments = append(toolStreamFragments, chunk.ToolCalls...)
progress("eino_agent_reply", body, map[string]interface{}{ }
}
if streamsMainAssistant(ev.AgentName) {
if s := strings.TrimSpace(mainAssistantBuf.String()); s != "" {
lastAssistant = s
runAccumulatedMsgs = append(runAccumulatedMsgs, schema.AssistantMessage(s, nil))
if orchMode == "plan_execute" && strings.EqualFold(strings.TrimSpace(ev.AgentName), "executor") {
lastPlanExecuteExecutor = UnwrapPlanExecuteUserText(s)
}
}
}
if subAssistantBuf.Len() > 0 && progress != nil {
if s := strings.TrimSpace(subAssistantBuf.String()); s != "" {
if subReplyStreamID != "" {
progress("eino_agent_reply_stream_end", s, map[string]interface{}{
"streamId": subReplyStreamID,
"einoAgent": ev.AgentName,
"einoRole": "sub",
"conversationId": conversationID,
"source": "eino",
})
} else {
progress("eino_agent_reply", s, map[string]interface{}{
"conversationId": conversationID, "conversationId": conversationID,
"einoAgent": ev.AgentName, "einoAgent": ev.AgentName,
"einoRole": "sub", "einoRole": "sub",
@@ -533,65 +541,157 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
} }
} }
} }
var lastToolChunk *schema.Message
if mv.Role == schema.Tool && progress != nil { if merged := mergeStreamingToolCallFragments(toolStreamFragments); len(merged) > 0 {
toolName := msg.ToolName lastToolChunk = &schema.Message{ToolCalls: merged}
if toolName == "" { }
toolName = mv.ToolName tryEmitToolCallsOnce(lastToolChunk, ev.AgentName, orchestratorName, conversationID, progress, toolEmitSeen, subAgentToolStep, markPending)
if streamRecvErr != nil {
if progress != nil {
progress("eino_stream_error", streamRecvErr.Error(), map[string]interface{}{
"conversationId": conversationID,
"source": "eino",
"einoAgent": ev.AgentName,
"einoRole": einoRoleTag(ev.AgentName),
})
} }
if retErr := handleRunErr(streamRecvErr); retErr != nil {
content := msg.Content return takePartial(retErr)
isErr := false
if strings.HasPrefix(content, einomcp.ToolErrorPrefix) {
isErr = true
content = strings.TrimPrefix(content, einomcp.ToolErrorPrefix)
} }
}
continue
}
preview := content msg, gerr := mv.GetMessage()
if len(preview) > 200 { if gerr != nil || msg == nil {
preview = preview[:200] + "..." continue
} }
data := map[string]interface{}{ runAccumulatedMsgs = append(runAccumulatedMsgs, msg)
"toolName": toolName, tryEmitToolCallsOnce(mergeMessageToolCalls(msg), ev.AgentName, orchestratorName, conversationID, progress, toolEmitSeen, subAgentToolStep, markPending)
"success": !isErr,
"isError": isErr, if mv.Role == schema.Assistant {
"result": content, if progress != nil && strings.TrimSpace(msg.ReasoningContent) != "" {
"resultPreview": preview, progress("thinking", strings.TrimSpace(msg.ReasoningContent), map[string]interface{}{
"conversationId": conversationID, "conversationId": conversationID,
"source": "eino",
"einoAgent": ev.AgentName, "einoAgent": ev.AgentName,
"einoRole": einoRoleTag(ev.AgentName), "einoRole": einoRoleTag(ev.AgentName),
"source": "eino", "orchestration": orchMode,
} })
toolCallID := strings.TrimSpace(msg.ToolCallID) }
if toolCallID == "" { body := strings.TrimSpace(msg.Content)
if inferred, ok := popNextPendingForAgent(ev.AgentName); ok { if body != "" {
toolCallID = inferred.ToolCallID if streamsMainAssistant(ev.AgentName) {
} else if inferred, ok := popNextPendingForAgent(orchestratorName); ok { if progress != nil {
toolCallID = inferred.ToolCallID progress("response_start", "", map[string]interface{}{
} else if inferred, ok := popNextPendingForAgent(""); ok { "conversationId": conversationID,
toolCallID = inferred.ToolCallID "mcpExecutionIds": snapshotMCPIDs(),
} else { "messageGeneratedBy": "eino:" + ev.AgentName,
for id := range pendingByID { "einoRole": "orchestrator",
toolCallID = id "einoAgent": ev.AgentName,
delete(pendingByID, id) "orchestration": orchMode,
break })
} progress("response_delta", body, map[string]interface{}{
"conversationId": conversationID,
"mcpExecutionIds": snapshotMCPIDs(),
"einoRole": "orchestrator",
"einoAgent": ev.AgentName,
"orchestration": orchMode,
})
} }
} else { lastAssistant = body
removePendingByID(toolCallID) if orchMode == "plan_execute" && strings.EqualFold(strings.TrimSpace(ev.AgentName), "executor") {
lastPlanExecuteExecutor = UnwrapPlanExecuteUserText(body)
}
} else if progress != nil {
progress("eino_agent_reply", body, map[string]interface{}{
"conversationId": conversationID,
"einoAgent": ev.AgentName,
"einoRole": "sub",
"source": "eino",
})
} }
if toolCallID != "" {
data["toolCallId"] = toolCallID
}
progress("tool_result", fmt.Sprintf("工具结果 (%s)", toolName), data)
} }
} }
if mv.Role == schema.Tool && progress != nil {
toolName := msg.ToolName
if toolName == "" {
toolName = mv.ToolName
}
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": !isErr,
"isError": isErr,
"result": content,
"resultPreview": preview,
"conversationId": conversationID,
"einoAgent": ev.AgentName,
"einoRole": einoRoleTag(ev.AgentName),
"source": "eino",
}
toolCallID := strings.TrimSpace(msg.ToolCallID)
if toolCallID == "" {
if inferred, ok := popNextPendingForAgent(ev.AgentName); ok {
toolCallID = inferred.ToolCallID
} else if inferred, ok := popNextPendingForAgent(orchestratorName); ok {
toolCallID = inferred.ToolCallID
} else if inferred, ok := popNextPendingForAgent(""); ok {
toolCallID = inferred.ToolCallID
} else {
for id := range pendingByID {
toolCallID = id
delete(pendingByID, id)
break
}
}
} else {
removePendingByID(toolCallID)
}
if toolCallID != "" {
data["toolCallId"] = toolCallID
}
progress("tool_result", fmt.Sprintf("工具结果 (%s)", toolName), data)
}
}
mcpIDsMu.Lock() mcpIDsMu.Lock()
ids := append([]string(nil), *mcpIDs...) ids := append([]string(nil), *mcpIDs...)
mcpIDsMu.Unlock() mcpIDsMu.Unlock()
histJSON, _ := json.Marshal(lastRunMsgs) out := buildEinoRunResultFromAccumulated(
orchMode, runAccumulatedMsgs, lastAssistant, lastPlanExecuteExecutor, emptyHint, ids, false,
)
return out, nil
}
func einoPartialRunLastOutputHint() string {
return "[执行未正常结束(用户停止、超时或异常)。续跑时请基于上文已产生的工具与结果继续,勿重复已完成步骤。]\n" +
"[Run ended abnormally; continue from the trace above without repeating completed steps.]"
}
func buildEinoRunResultFromAccumulated(
orchMode string,
runAccumulatedMsgs []adk.Message,
lastAssistant string,
lastPlanExecuteExecutor string,
emptyHint string,
mcpIDs []string,
partial bool,
) *RunResult {
histJSON, _ := json.Marshal(runAccumulatedMsgs)
cleaned := strings.TrimSpace(lastAssistant) cleaned := strings.TrimSpace(lastAssistant)
if orchMode == "plan_execute" { if orchMode == "plan_execute" {
if e := strings.TrimSpace(lastPlanExecuteExecutor); e != "" { if e := strings.TrimSpace(lastPlanExecuteExecutor); e != "" {
@@ -607,15 +707,29 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
if rs := []rune(cleaned); len(rs) > maxResponseRunes { if rs := []rune(cleaned); len(rs) > maxResponseRunes {
cleaned = string(rs[:maxResponseRunes]) + "\n\n... (response truncated / 响应已截断)" cleaned = string(rs[:maxResponseRunes]) + "\n\n... (response truncated / 响应已截断)"
} }
lastOut := cleaned
resp := cleaned
if partial && cleaned == "" {
lastOut = einoPartialRunLastOutputHint()
resp = emptyHint
}
out := &RunResult{ out := &RunResult{
Response: cleaned, Response: resp,
MCPExecutionIDs: ids, MCPExecutionIDs: mcpIDs,
LastReActInput: string(histJSON), LastAgentTraceInput: string(histJSON),
LastReActOutput: cleaned, LastAgentTraceOutput: lastOut,
} }
if out.Response == "" { if !partial && out.Response == "" {
out.Response = emptyHint out.Response = emptyHint
out.LastReActOutput = out.Response out.LastAgentTraceOutput = out.Response
} }
return out, nil return out
}
func buildEinoCheckpointID(orchMode string) string {
mode := sanitizeEinoPathSegment(strings.TrimSpace(orchMode))
if mode == "" {
mode = "default"
}
return "runner-" + mode
} }
-1
View File
@@ -59,4 +59,3 @@ func (m *noNestedTaskMiddleware) WrapInvokableToolCall(
return endpoint(ctx2, argumentsInJSON, opts...) return endpoint(ctx2, argumentsInJSON, opts...)
}, nil }, nil
} }
@@ -13,7 +13,7 @@ import (
const ( const (
planExecuteMaxStepResultRunes = 12000 planExecuteMaxStepResultRunes = 12000
planExecuteKeepLastSteps = 16 planExecuteKeepLastSteps = 16
) )
func truncateRunesWithSuffix(s string, maxRunes int, suffix string) string { func truncateRunesWithSuffix(s string, maxRunes int, suffix string) string {
+4 -4
View File
@@ -30,10 +30,10 @@ import (
// RunResult 与单 Agent 循环结果字段对齐,便于复用存储与 SSE 收尾逻辑。 // RunResult 与单 Agent 循环结果字段对齐,便于复用存储与 SSE 收尾逻辑。
type RunResult struct { type RunResult struct {
Response string Response string
MCPExecutionIDs []string MCPExecutionIDs []string
LastReActInput string LastAgentTraceInput string // 已序列化的消息带(JSON):原生循环或 Eino 均写入,供续跑/攻击链等恢复上下文
LastReActOutput string LastAgentTraceOutput string // 本轮助手侧对外展示文本(摘要或最终回复)
} }
// toolCallPendingInfo tracks a tool_call emitted to the UI so we can later // toolCallPendingInfo tracks a tool_call emitted to the UI so we can later
-1
View File
@@ -162,4 +162,3 @@ func truncateRunes(s string, max int) string {
} }
return string(r[:max]) + "…" return string(r[:max]) + "…"
} }
+6 -6
View File
@@ -49,12 +49,12 @@ func ParseSkillMD(raw []byte) (*SkillManifest, string, error) {
} }
type skillFrontMatterExport struct { type skillFrontMatterExport struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Description string `yaml:"description"` Description string `yaml:"description"`
License string `yaml:"license,omitempty"` License string `yaml:"license,omitempty"`
Compatibility string `yaml:"compatibility,omitempty"` Compatibility string `yaml:"compatibility,omitempty"`
Metadata map[string]any `yaml:"metadata,omitempty"` Metadata map[string]any `yaml:"metadata,omitempty"`
AllowedTools string `yaml:"allowed-tools,omitempty"` AllowedTools string `yaml:"allowed-tools,omitempty"`
} }
// BuildSkillMD serializes SKILL.md per agentskills.io. // BuildSkillMD serializes SKILL.md per agentskills.io.
+4 -4
View File
@@ -9,10 +9,10 @@ import (
) )
const ( const (
maxPackageFiles = 4000 maxPackageFiles = 4000
maxPackageDepth = 24 maxPackageDepth = 24
maxScriptsDepth = 24 maxScriptsDepth = 24
defaultMaxRead = 10 << 20 defaultMaxRead = 10 << 20
) )
// SafeRelPath resolves rel inside root (no ..). // SafeRelPath resolves rel inside root (no ..).