后端: 1. 工具执行结果协议升级为结构化 ToolExecutionResult——execute/tool_runtime、ToolRegistry、stream extra 与 timeline 持久化统一改为透传 observation_text / summary / argument_view / result_view,不再只回写纯文本结果;context_tools、upsert_task_class 与旧 schedule/web 工具通过兼容包装接入新协议 2. 日程写工具注册继续收口——place / move / swap / batch_move / unplace / queue_apply_head_move 从 registry 内联实现下沉为独立 handler,降低注册表内参数解析与业务逻辑混写 3. 工具结果展示基础能力补齐——新增 execution_result / schedule_operation_handlers 公共件,为日程操作结果、参数本地化展示、blocked/failed/done 状态统一建模 前端: 4. AssistantPanel 接入结构化工具卡片渲染——新增 ToolCardRenderer,tool_call / tool_result 支持 argument_view / result_view 展示;schedule_completed 恢复为时间线内的占位卡片块,避免排程卡片脱离原消息顺序 5. 时间线类型与渲染收敛——schedule_agent.ts 补齐 ToolView 协议,AssistantPanel 改为按块渲染 tool / schedule_card / business_card,并移除旧 demo/prototype 路由与页面,收束正式面板代码路径 仓库: 6. AGENTS.md 新增协作约束——禁止擅自回滚、覆盖或删除用户/其他代理产生的工作区改动
419 lines
14 KiB
Go
419 lines
14 KiB
Go
package newagentexecute
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
newagentshared "github.com/LoveLosita/smartflow/backend/newAgent/shared"
|
||
"log"
|
||
"regexp"
|
||
"strings"
|
||
|
||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||
newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream"
|
||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||
"github.com/cloudwego/eino/schema"
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
func appendToolCallResultHistory(
|
||
conversationContext *newagentmodel.ConversationContext,
|
||
toolName string,
|
||
args map[string]any,
|
||
result newagenttools.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 *newagentmodel.CommonState,
|
||
conversationContext *newagentmodel.ConversationContext,
|
||
toolCall *newagentmodel.ToolCallIntent,
|
||
emitter *newagentstream.ChunkEmitter,
|
||
registry *newagenttools.ToolRegistry,
|
||
scheduleState *schedule.ScheduleState,
|
||
writePreview newagentmodel.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 := newagenttools.BlockedResult(toolName, toolCall.Arguments, blockedText, "tool_temporarily_disabled", blockedText)
|
||
emitToolCallResultEvent(emitter, executeStatusBlockID, executeStageName, blockedResult, toolCall.Arguments)
|
||
appendToolCallResultHistory(conversationContext, toolName, toolCall.Arguments, blockedResult)
|
||
newagentshared.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())
|
||
newagentshared.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 次调用未激活工具,终止执行: %s(active_domain=%q active_packs=%v)",
|
||
flowState.ConsecutiveCorrections,
|
||
toolName,
|
||
flowState.ActiveToolDomain,
|
||
newagenttools.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 := newagenttools.ResolveToolDomainPack(toolName); ok {
|
||
if newagenttools.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)
|
||
}
|
||
}
|
||
|
||
newagentshared.AppendLLMCorrectionWithHint(
|
||
conversationContext,
|
||
"",
|
||
fmt.Sprintf("你调用的工具 %q 当前不在已激活工具域内。", toolName),
|
||
addHint,
|
||
)
|
||
return nil
|
||
}
|
||
|
||
if shouldForceFeasibilityNegotiation(flowState, registry, toolName) {
|
||
blockedText := buildInfeasibleBlockedResult(flowState)
|
||
blockedResult := newagenttools.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 = newagenttools.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 *newagentmodel.CommonState) {
|
||
if flowState == nil || flowState.PendingContextHook == nil {
|
||
return
|
||
}
|
||
hook := flowState.PendingContextHook
|
||
domain := newagenttools.NormalizeToolDomain(hook.Domain)
|
||
if domain == "" {
|
||
flowState.PendingContextHook = nil
|
||
return
|
||
}
|
||
flowState.ActiveToolDomain = domain
|
||
flowState.ActiveToolPacks = newagenttools.ResolveEffectiveToolPacks(domain, hook.Packs)
|
||
flowState.PendingContextHook = nil
|
||
}
|
||
|
||
func isToolVisibleForCurrentExecuteMode(
|
||
flowState *newagentmodel.CommonState,
|
||
registry *newagenttools.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 && !newagenttools.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 *newagentmodel.AgentRuntimeState,
|
||
conversationContext *newagentmodel.ConversationContext,
|
||
registry *newagenttools.ToolRegistry,
|
||
scheduleState *schedule.ScheduleState,
|
||
originalState *schedule.ScheduleState,
|
||
writePreview newagentmodel.WriteSchedulePreviewFunc,
|
||
emitter *newagentstream.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 := newagenttools.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 := newagenttools.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 = newagenttools.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 *newagentmodel.CommonState,
|
||
scheduleState *schedule.ScheduleState,
|
||
registry *newagenttools.ToolRegistry,
|
||
toolName string,
|
||
writePreview newagentmodel.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 *newagentstream.ChunkEmitter,
|
||
blockID string,
|
||
stage string,
|
||
result newagenttools.ToolExecutionResult,
|
||
args map[string]any,
|
||
) {
|
||
if emitter == nil {
|
||
return
|
||
}
|
||
result = newagenttools.EnsureToolResultDefaults(result, args)
|
||
_ = emitter.EmitToolCallResult(
|
||
blockID,
|
||
stage,
|
||
result.Tool,
|
||
result.Status,
|
||
result.Summary,
|
||
result.ArgumentsPreview,
|
||
newagenttools.ToolArgumentViewToMap(result.ArgumentView),
|
||
newagenttools.ToolDisplayViewToMap(result.ResultView),
|
||
false,
|
||
)
|
||
}
|