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 迁移面
This commit is contained in:
Losita
2026-05-05 16:00:57 +08:00
parent e1819c5653
commit d7184b776b
174 changed files with 2189 additions and 1236 deletions

View File

@@ -0,0 +1,418 @@
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,
)
}