Files
smartmate/backend/services/agent/node/execute/tool_runtime.go
Losita d7184b776b Version: 0.9.75.dev.260505
后端:
1.收口阶段 6 agent 结构迁移,将 newAgent 内核与 agentsvc 编排层迁入 services/agent
- 切换 Agent 启动装配与 HTTP handler 直连 agent sv,移除旧 service agent bridge
- 补齐 Agent 对 memory、task、task-class、schedule 的 RPC 适配与契约字段
- 扩展 schedule、task、task-class RPC/contract 支撑 Agent 查询、写入与 provider 切流
- 更新迁移文档、README 与相关注释,明确 agent 当前切流点和剩余 memory 迁移面
2026-05-05 16:00:57 +08:00

419 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package agentexecute
import (
"context"
"encoding/json"
"fmt"
agentshared "github.com/LoveLosita/smartflow/backend/services/agent/shared"
"log"
"regexp"
"strings"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
agentstream "github.com/LoveLosita/smartflow/backend/services/agent/stream"
agenttools "github.com/LoveLosita/smartflow/backend/services/agent/tools"
"github.com/LoveLosita/smartflow/backend/services/agent/tools/schedule"
"github.com/cloudwego/eino/schema"
"github.com/google/uuid"
)
func appendToolCallResultHistory(
conversationContext *agentmodel.ConversationContext,
toolName string,
args map[string]any,
result agenttools.ToolExecutionResult,
) {
if conversationContext == nil {
return
}
argsJSON := "{}"
if args != nil {
if raw, err := json.Marshal(args); err == nil {
argsJSON = string(raw)
}
}
toolCallID := uuid.NewString()
conversationContext.AppendHistory(&schema.Message{
Role: schema.Assistant,
Content: "",
ToolCalls: []schema.ToolCall{
{
ID: toolCallID,
Type: "function",
Function: schema.FunctionCall{
Name: toolName,
Arguments: argsJSON,
},
},
},
})
conversationContext.AppendHistory(&schema.Message{
Role: schema.Tool,
Content: result.ObservationText,
ToolCallID: toolCallID,
ToolName: toolName,
})
}
func executeToolCall(
ctx context.Context,
flowState *agentmodel.CommonState,
conversationContext *agentmodel.ConversationContext,
toolCall *agentmodel.ToolCallIntent,
emitter *agentstream.ChunkEmitter,
registry *agenttools.ToolRegistry,
scheduleState *schedule.ScheduleState,
writePreview agentmodel.WriteSchedulePreviewFunc,
) error {
if toolCall == nil {
return nil
}
toolName := strings.TrimSpace(toolCall.Name)
if toolName == "" {
return fmt.Errorf("工具调用缺少工具名称")
}
if err := emitter.EmitToolCallStart(
executeStatusBlockID,
executeStageName,
toolName,
buildToolCallStartSummary(toolName, toolCall.Arguments),
buildToolArgumentsPreviewCN(toolCall.Arguments),
false,
); err != nil {
return fmt.Errorf("工具调用开始事件发送失败: %w", err)
}
if registry == nil {
return fmt.Errorf("工具注册表未注入")
}
if scheduleState == nil && registry.RequiresScheduleState(toolName) {
return fmt.Errorf("日程状态未加载,无法执行工具 %q", toolName)
}
if registry.IsToolTemporarilyDisabled(toolName) {
flowState.ConsecutiveCorrections++
if flowState.ConsecutiveCorrections >= maxConsecutiveCorrections {
return fmt.Errorf("连续 %d 次调用临时禁用工具,终止执行: %s",
flowState.ConsecutiveCorrections, toolName)
}
blockedText := buildTemporarilyDisabledToolResult(toolName)
blockedResult := agenttools.BlockedResult(toolName, toolCall.Arguments, blockedText, "tool_temporarily_disabled", blockedText)
emitToolCallResultEvent(emitter, executeStatusBlockID, executeStageName, blockedResult, toolCall.Arguments)
appendToolCallResultHistory(conversationContext, toolName, toolCall.Arguments, blockedResult)
agentshared.AppendLLMCorrectionWithHint(
conversationContext,
"",
fmt.Sprintf("工具 %q 当前暂时禁用。", toolName),
"请改用 move/swap/batch_move/unplace 等排程微调工具继续推进。",
)
return nil
}
if !registry.HasTool(toolName) {
flowState.ConsecutiveCorrections++
if flowState.ConsecutiveCorrections >= maxConsecutiveCorrections {
return fmt.Errorf("连续 %d 次调用未知工具,终止执行: %s可用工具%s。",
flowState.ConsecutiveCorrections, toolName, strings.Join(registry.ToolNames(), "、"))
}
log.Printf("[WARN] execute 工具名不合法 chat=%s round=%d tool=%s consecutive=%d/%d available=%v",
flowState.ConversationID, flowState.RoundUsed, toolName,
flowState.ConsecutiveCorrections, maxConsecutiveCorrections, registry.ToolNames())
agentshared.AppendLLMCorrectionWithHint(
conversationContext,
"",
fmt.Sprintf("你调用的工具 %q 不存在。", toolName),
fmt.Sprintf("可用工具:%s。请检查拼写后重试。", strings.Join(registry.ToolNames(), "、")),
)
return nil
}
if !isToolVisibleForCurrentExecuteMode(flowState, registry, toolName) {
flowState.ConsecutiveCorrections++
if flowState.ConsecutiveCorrections >= maxConsecutiveCorrections {
return fmt.Errorf("连续 %d 次调用未激活工具,终止执行: %sactive_domain=%q active_packs=%v",
flowState.ConsecutiveCorrections,
toolName,
flowState.ActiveToolDomain,
agenttools.ResolveEffectiveToolPacks(flowState.ActiveToolDomain, flowState.ActiveToolPacks))
}
addHint := `请先调用 context_tools_add 激活目标工具域后再继续。`
if flowState != nil && flowState.ActiveOptimizeOnly {
addHint = `当前处于“粗排后主动优化专用模式”,只允许使用 analyze_health、move、swap不要再尝试 query_target_tasks / query_available_slots 等全窗搜索工具。`
} else if domain, pack, ok := agenttools.ResolveToolDomainPack(toolName); ok {
if agenttools.IsFixedToolPack(domain, pack) {
addHint = fmt.Sprintf(`请先调用 context_tools_add参数 domain="%s"。`, domain)
} else {
addHint = fmt.Sprintf(`请先调用 context_tools_add参数 domain="%s", packs=["%s"]。`, domain, pack)
}
}
agentshared.AppendLLMCorrectionWithHint(
conversationContext,
"",
fmt.Sprintf("你调用的工具 %q 当前不在已激活工具域内。", toolName),
addHint,
)
return nil
}
if shouldForceFeasibilityNegotiation(flowState, registry, toolName) {
blockedText := buildInfeasibleBlockedResult(flowState)
blockedResult := agenttools.BlockedResult(toolName, toolCall.Arguments, blockedText, "health_negotiation_required", blockedText)
emitToolCallResultEvent(emitter, executeStatusBlockID, executeStageName, blockedResult, toolCall.Arguments)
appendToolCallResultHistory(conversationContext, toolName, toolCall.Arguments, blockedResult)
return nil
}
beforeDigest := summarizeScheduleStateForDebug(scheduleState)
if !registry.RequiresScheduleState(toolName) {
if toolCall.Arguments == nil {
toolCall.Arguments = make(map[string]any)
}
toolCall.Arguments["_user_id"] = flowState.UserID
}
result := registry.Execute(scheduleState, toolName, toolCall.Arguments)
result = agenttools.EnsureToolResultDefaults(result, toolCall.Arguments)
updateHealthSnapshotV2(flowState, toolName, result.ObservationText)
updateTaskClassUpsertSnapshot(flowState, toolName, result.ObservationText)
updateActiveToolDomainSnapshot(flowState, toolName, result.ObservationText)
afterDigest := summarizeScheduleStateForDebug(scheduleState)
log.Printf(
"[DEBUG] execute tool chat=%s round=%d tool=%s args=%s before=%s after=%s result_preview=%.200s",
flowState.ConversationID,
flowState.RoundUsed,
toolName,
marshalArgsForDebug(toolCall.Arguments),
beforeDigest,
afterDigest,
flattenForLog(result.ObservationText),
)
emitToolCallResultEvent(emitter, executeStatusBlockID, executeStageName, result, toolCall.Arguments)
appendToolCallResultHistory(conversationContext, toolName, toolCall.Arguments, result)
if registry.IsScheduleMutationTool(toolName) {
flowState.HasScheduleWriteOps = true
flowState.HasScheduleChanges = true
}
tryWritePreviewAfterWriteTool(ctx, flowState, scheduleState, registry, toolName, writePreview)
return nil
}
func applyPendingContextHook(flowState *agentmodel.CommonState) {
if flowState == nil || flowState.PendingContextHook == nil {
return
}
hook := flowState.PendingContextHook
domain := agenttools.NormalizeToolDomain(hook.Domain)
if domain == "" {
flowState.PendingContextHook = nil
return
}
flowState.ActiveToolDomain = domain
flowState.ActiveToolPacks = agenttools.ResolveEffectiveToolPacks(domain, hook.Packs)
flowState.PendingContextHook = nil
}
func isToolVisibleForCurrentExecuteMode(
flowState *agentmodel.CommonState,
registry *agenttools.ToolRegistry,
toolName string,
) bool {
if registry == nil {
return false
}
activeDomain := ""
var activePacks []string
if flowState != nil {
activeDomain = flowState.ActiveToolDomain
activePacks = flowState.ActiveToolPacks
}
if !registry.IsToolVisibleInDomain(activeDomain, activePacks, toolName) {
return false
}
if flowState != nil && flowState.ActiveOptimizeOnly && !agenttools.IsToolAllowedInActiveOptimize(toolName) {
return false
}
return true
}
func buildTemporarilyDisabledToolResult(toolName string) string {
return fmt.Sprintf("工具 %q 当前暂时禁用。请改用 move/swap/batch_move/unplace 等排程微调工具。", strings.TrimSpace(toolName))
}
func executePendingTool(
ctx context.Context,
runtimeState *agentmodel.AgentRuntimeState,
conversationContext *agentmodel.ConversationContext,
registry *agenttools.ToolRegistry,
scheduleState *schedule.ScheduleState,
originalState *schedule.ScheduleState,
writePreview agentmodel.WriteSchedulePreviewFunc,
emitter *agentstream.ChunkEmitter,
) error {
pending := runtimeState.PendingConfirmTool
if pending == nil {
return nil
}
var args map[string]any
if err := json.Unmarshal([]byte(pending.ArgsJSON), &args); err != nil {
return fmt.Errorf("解析待确认工具参数失败: %w", err)
}
if err := emitter.EmitToolCallStart(
executeStatusBlockID,
executeStageName,
pending.ToolName,
buildToolCallStartSummary(pending.ToolName, args),
buildToolArgumentsPreviewCN(args),
false,
); err != nil {
return fmt.Errorf("工具调用开始事件发送失败: %w", err)
}
if scheduleState == nil {
return fmt.Errorf("日程状态未加载,无法执行已确认的写工具 %s", pending.ToolName)
}
flowState := runtimeState.EnsureCommonState()
if registry.IsToolTemporarilyDisabled(pending.ToolName) {
blockedText := buildTemporarilyDisabledToolResult(pending.ToolName)
blockedResult := agenttools.BlockedResult(pending.ToolName, args, blockedText, "tool_temporarily_disabled", blockedText)
emitToolCallResultEvent(emitter, executeStatusBlockID, executeStageName, blockedResult, args)
appendToolCallResultHistory(conversationContext, pending.ToolName, args, blockedResult)
runtimeState.PendingConfirmTool = nil
return nil
}
if shouldForceFeasibilityNegotiation(flowState, registry, pending.ToolName) {
blockedText := buildInfeasibleBlockedResult(flowState)
blockedResult := agenttools.BlockedResult(pending.ToolName, args, blockedText, "health_negotiation_required", blockedText)
emitToolCallResultEvent(emitter, executeStatusBlockID, executeStageName, blockedResult, args)
appendToolCallResultHistory(conversationContext, pending.ToolName, args, blockedResult)
runtimeState.PendingConfirmTool = nil
return nil
}
beforeDigest := summarizeScheduleStateForDebug(scheduleState)
if !registry.RequiresScheduleState(pending.ToolName) {
if args == nil {
args = make(map[string]any)
}
args["_user_id"] = flowState.UserID
}
result := registry.Execute(scheduleState, pending.ToolName, args)
result = agenttools.EnsureToolResultDefaults(result, args)
updateHealthSnapshotV2(flowState, pending.ToolName, result.ObservationText)
updateTaskClassUpsertSnapshot(flowState, pending.ToolName, result.ObservationText)
updateActiveToolDomainSnapshot(flowState, pending.ToolName, result.ObservationText)
afterDigest := summarizeScheduleStateForDebug(scheduleState)
log.Printf(
"[DEBUG] execute pending tool chat=%s round=%d tool=%s args=%s before=%s after=%s result_preview=%.200s",
flowState.ConversationID,
flowState.RoundUsed,
pending.ToolName,
marshalArgsForDebug(args),
beforeDigest,
afterDigest,
flattenForLog(result.ObservationText),
)
emitToolCallResultEvent(emitter, executeStatusBlockID, executeStageName, result, args)
appendToolCallResultHistory(conversationContext, pending.ToolName, args, result)
if registry.IsScheduleMutationTool(pending.ToolName) {
flowState.HasScheduleWriteOps = true
flowState.HasScheduleChanges = true
}
tryWritePreviewAfterWriteTool(ctx, flowState, scheduleState, registry, pending.ToolName, writePreview)
runtimeState.PendingConfirmTool = nil
return nil
}
func tryWritePreviewAfterWriteTool(
ctx context.Context,
flowState *agentmodel.CommonState,
scheduleState *schedule.ScheduleState,
registry *agenttools.ToolRegistry,
toolName string,
writePreview agentmodel.WriteSchedulePreviewFunc,
) {
if flowState == nil || scheduleState == nil || registry == nil || writePreview == nil {
return
}
if !registry.IsScheduleMutationTool(toolName) {
return
}
if err := writePreview(ctx, scheduleState, flowState.UserID, flowState.ConversationID, flowState.TaskClassIDs); err != nil {
log.Printf(
"[WARN] execute realtime preview write failed chat=%s tool=%s err=%v",
flowState.ConversationID,
toolName,
err,
)
return
}
log.Printf(
"[DEBUG] execute realtime preview write success chat=%s tool=%s",
flowState.ConversationID,
toolName,
)
}
var listItemRe = regexp.MustCompile(`([^\n])([2-9][\.、]\s)`)
func normalizeSpeak(speak string) string {
speak = strings.TrimSpace(speak)
if speak == "" {
return speak
}
if !strings.Contains(speak, "\n") {
speak = listItemRe.ReplaceAllString(speak, "$1\n$2")
}
return speak + "\n"
}
func buildExecuteNormalizedSpeakTail(streamed, normalized string) string {
streamed = strings.ReplaceAll(streamed, "\r\n", "\n")
normalized = strings.ReplaceAll(normalized, "\r\n", "\n")
if streamed == "" || normalized == "" {
return ""
}
if !strings.HasPrefix(normalized, streamed) {
return ""
}
return normalized[len(streamed):]
}
func emitToolCallResultEvent(
emitter *agentstream.ChunkEmitter,
blockID string,
stage string,
result agenttools.ToolExecutionResult,
args map[string]any,
) {
if emitter == nil {
return
}
result = agenttools.EnsureToolResultDefaults(result, args)
_ = emitter.EmitToolCallResult(
blockID,
stage,
result.Tool,
result.Status,
result.Summary,
result.ArgumentsPreview,
agenttools.ToolArgumentViewToMap(result.ArgumentView),
agenttools.ToolDisplayViewToMap(result.ResultView),
false,
)
}