Files
smartmate/backend/newAgent/node/execute/state_snapshot.go
LoveLosita 0b0ed3c61a Version: 0.9.47.dev.260427
后端:
1. execute 节点继续拆职责——超大 execute.go 下沉为 node/execute 子包,按决策流、动作路由、上下文锚点、工具执行、状态快照、工具展示与参数解析拆分;顶层 execute.go 收敛为桥接导出,降低单文件编排/业务/模型/工具逻辑混写
2. 节点公共能力继续沉到 shared——抽出 LLM 纠错回灌、完整上下文调试日志、thinking 开关、统一上下文压缩、可见 assistant 文本持久化等 node_* 公共件,减少 execute 独占实现并为其他节点复用铺路
3. speak 文本整理能力独立收口——新增 speak_text 辅助文件,补齐正文归一化的独立承载,继续收缩 execute 主文件体积

前端:
4. NewAgent 时间线接入 business_card 业务卡片协议——schedule_agent.ts 新增 task_query / task_record 卡片载荷类型与 business_card kind;AssistantPanel 增加业务卡片事件存储、时间线恢复、块渲染分支与 BusinessCardRenderer 接入,同时保留 interrupt / status / tool / reasoning 多块并存
5. 新增任务查询卡片与任务记录卡片组件,并补充 DesignDemo 设计预览页与路由,前端可先行验证 business_card 的视觉与交互落点

文档:
6. 新增 newagent business card 前后端对接说明,明确 timeline kind、payload 结构、卡片分类、前后端发射/渲染约束
2026-04-27 17:35:55 +08:00

333 lines
11 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 newagentexecute
import (
"encoding/json"
"fmt"
"strings"
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
)
func shouldForceFeasibilityNegotiation(
flowState *newagentmodel.CommonState,
registry *newagenttools.ToolRegistry,
toolName string,
) bool {
if flowState == nil || registry == nil {
return false
}
if !flowState.HealthCheckDone || flowState.HealthIsFeasible {
return false
}
if !registry.IsWriteTool(toolName) || !registry.RequiresScheduleState(toolName) {
return false
}
return true
}
func buildInfeasibleNegotiationQuestion(flowState *newagentmodel.CommonState) string {
capacityGap := 0
reasonCode := "capacity_insufficient"
if flowState != nil {
capacityGap = flowState.HealthCapacityGap
if strings.TrimSpace(flowState.HealthReasonCode) != "" {
reasonCode = strings.TrimSpace(flowState.HealthReasonCode)
}
}
return fmt.Sprintf(
"当前计划不可行analyze_health 判断当前约束不可行capacity_gap=%dreason=%s。在继续写操作前请先与用户协商扩展时间窗、放宽约束、缩减范围或预算或接受风险收口。",
capacityGap,
reasonCode,
)
}
func buildInfeasibleBlockedResult(flowState *newagentmodel.CommonState) string {
capacityGap := 0
reasonCode := "capacity_insufficient"
if flowState != nil {
capacityGap = flowState.HealthCapacityGap
if strings.TrimSpace(flowState.HealthReasonCode) != "" {
reasonCode = strings.TrimSpace(flowState.HealthReasonCode)
}
}
return fmt.Sprintf(
"已阻断本次写操作analyze_health 判定当前约束不可行capacity_gap=%dreason=%s。请先与用户协商扩展时间窗 / 放宽约束 / 缩减范围或预算 / 接受风险收口。",
capacityGap,
reasonCode,
)
}
type contextToolsResultEnvelope struct {
Tool string `json:"tool"`
Success bool `json:"success"`
Domain string `json:"domain,omitempty"`
Packs []string `json:"packs,omitempty"`
Mode string `json:"mode,omitempty"`
All bool `json:"all,omitempty"`
}
type analyzeHealthResultEnvelope struct {
Tool string `json:"tool"`
Success bool `json:"success"`
Feasibility *analyzeHealthFeasibilityBrief `json:"feasibility,omitempty"`
Decision *analyzeHealthDecisionBrief `json:"decision,omitempty"`
}
type analyzeHealthFeasibilityBrief struct {
IsFeasible bool `json:"is_feasible"`
CapacityGap int `json:"capacity_gap"`
ReasonCode string `json:"reason_code"`
}
type analyzeHealthDecisionBrief struct {
ShouldContinueOptimize bool `json:"should_continue_optimize"`
PrimaryProblem string `json:"primary_problem,omitempty"`
RecommendedOperation string `json:"recommended_operation,omitempty"`
IsForcedImperfection bool `json:"is_forced_imperfection"`
ImprovementSignal string `json:"improvement_signal,omitempty"`
}
type upsertTaskClassResultEnvelope struct {
Tool string `json:"tool"`
Success bool `json:"success"`
Validation *upsertTaskClassValidationPart `json:"validation,omitempty"`
Error string `json:"error,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
}
type upsertTaskClassValidationPart struct {
OK bool `json:"ok"`
Issues []string `json:"issues"`
}
func updateActiveToolDomainSnapshot(flowState *newagentmodel.CommonState, toolName string, result string) {
if flowState == nil || !newagenttools.IsContextManagementTool(toolName) {
return
}
var envelope contextToolsResultEnvelope
if err := json.Unmarshal([]byte(result), &envelope); err != nil {
return
}
if !envelope.Success {
return
}
switch strings.TrimSpace(toolName) {
case newagenttools.ToolNameContextToolsAdd:
domain := newagenttools.NormalizeToolDomain(envelope.Domain)
if domain == "" {
return
}
nextPacks := newagenttools.ResolveEffectiveToolPacks(domain, envelope.Packs)
mode := strings.ToLower(strings.TrimSpace(envelope.Mode))
if mode == "merge" && newagenttools.NormalizeToolDomain(flowState.ActiveToolDomain) == domain {
merged := make([]string, 0, len(flowState.ActiveToolPacks)+len(nextPacks))
seen := make(map[string]struct{}, len(flowState.ActiveToolPacks)+len(nextPacks))
current := newagenttools.ResolveEffectiveToolPacks(domain, flowState.ActiveToolPacks)
for _, pack := range current {
if _, exists := seen[pack]; exists {
continue
}
seen[pack] = struct{}{}
merged = append(merged, pack)
}
for _, pack := range nextPacks {
if _, exists := seen[pack]; exists {
continue
}
seen[pack] = struct{}{}
merged = append(merged, pack)
}
nextPacks = merged
}
flowState.ActiveToolDomain = domain
flowState.ActiveToolPacks = nextPacks
case newagenttools.ToolNameContextToolsRemove:
if envelope.All {
flowState.ActiveToolDomain = ""
flowState.ActiveToolPacks = nil
return
}
domain := newagenttools.NormalizeToolDomain(envelope.Domain)
if domain == "" {
return
}
currentDomain := newagenttools.NormalizeToolDomain(flowState.ActiveToolDomain)
if currentDomain != domain {
return
}
removedPacks := newagenttools.NormalizeToolPacks(domain, envelope.Packs)
if len(removedPacks) == 0 {
flowState.ActiveToolDomain = ""
flowState.ActiveToolPacks = nil
return
}
currentEffective := newagenttools.ResolveEffectiveToolPacks(domain, flowState.ActiveToolPacks)
if len(currentEffective) == 0 {
flowState.ActiveToolDomain = ""
flowState.ActiveToolPacks = nil
return
}
removedSet := make(map[string]struct{}, len(removedPacks))
for _, pack := range removedPacks {
removedSet[pack] = struct{}{}
}
remaining := make([]string, 0, len(currentEffective))
for _, pack := range currentEffective {
if _, shouldRemove := removedSet[pack]; shouldRemove {
continue
}
remaining = append(remaining, pack)
}
if len(remaining) == 0 {
flowState.ActiveToolDomain = ""
flowState.ActiveToolPacks = nil
return
}
flowState.ActiveToolPacks = remaining
}
}
func updateHealthFeasibilitySnapshot(flowState *newagentmodel.CommonState, toolName string, result string) {
if flowState == nil || !strings.EqualFold(strings.TrimSpace(toolName), toolAnalyzeHealth) {
return
}
flowState.HealthCheckDone = false
flowState.HealthIsFeasible = true
flowState.HealthCapacityGap = 0
flowState.HealthReasonCode = ""
var envelope analyzeHealthResultEnvelope
if err := json.Unmarshal([]byte(result), &envelope); err != nil {
return
}
if !envelope.Success || envelope.Feasibility == nil {
return
}
flowState.HealthCheckDone = true
flowState.HealthIsFeasible = envelope.Feasibility.IsFeasible
flowState.HealthCapacityGap = envelope.Feasibility.CapacityGap
flowState.HealthReasonCode = strings.TrimSpace(envelope.Feasibility.ReasonCode)
}
func updateTaskClassUpsertSnapshot(flowState *newagentmodel.CommonState, toolName string, result string) {
if flowState == nil || !strings.EqualFold(strings.TrimSpace(toolName), "upsert_task_class") {
return
}
flowState.TaskClassUpsertLastTried = true
flowState.TaskClassUpsertLastSuccess = false
flowState.TaskClassUpsertLastIssues = nil
var envelope upsertTaskClassResultEnvelope
if err := json.Unmarshal([]byte(result), &envelope); err != nil {
flowState.TaskClassUpsertConsecutiveFailures++
return
}
success := envelope.Success
issues := make([]string, 0)
if envelope.Validation != nil {
issues = append(issues, parseAnyToStringSlice(any(envelope.Validation.Issues))...)
if !envelope.Validation.OK {
success = false
}
}
if !success && strings.TrimSpace(envelope.Error) != "" && len(issues) == 0 {
issues = append(issues, strings.TrimSpace(envelope.Error))
}
issues = uniqueNonEmptyStrings(issues)
flowState.TaskClassUpsertLastSuccess = success
flowState.TaskClassUpsertLastIssues = issues
if success {
flowState.TaskClassUpsertConsecutiveFailures = 0
return
}
flowState.TaskClassUpsertConsecutiveFailures++
}
func uniqueNonEmptyStrings(values []string) []string {
if len(values) == 0 {
return nil
}
seen := make(map[string]struct{}, len(values))
result := make([]string, 0, len(values))
for _, value := range values {
text := strings.TrimSpace(value)
if text == "" {
continue
}
if _, exists := seen[text]; exists {
continue
}
seen[text] = struct{}{}
result = append(result, text)
}
return result
}
func updateHealthSnapshotV2(flowState *newagentmodel.CommonState, toolName string, result string) {
if flowState == nil || !strings.EqualFold(strings.TrimSpace(toolName), toolAnalyzeHealth) {
return
}
prevSignal := strings.TrimSpace(flowState.HealthImprovementSignal)
flowState.HealthCheckDone = false
flowState.HealthIsFeasible = true
flowState.HealthCapacityGap = 0
flowState.HealthReasonCode = ""
flowState.HealthShouldContinueOptimize = false
flowState.HealthTightnessLevel = ""
flowState.HealthPrimaryProblem = ""
flowState.HealthRecommendedOperation = ""
flowState.HealthIsForcedImperfection = false
flowState.HealthImprovementSignal = ""
var envelope struct {
Success bool `json:"success"`
Feasibility *analyzeHealthFeasibilityBrief `json:"feasibility,omitempty"`
Metrics struct {
Tightness *struct {
TightnessLevel string `json:"tightness_level"`
} `json:"tightness,omitempty"`
} `json:"metrics"`
Decision *analyzeHealthDecisionBrief `json:"decision,omitempty"`
}
if err := json.Unmarshal([]byte(result), &envelope); err != nil {
flowState.HealthStagnationCount = 0
return
}
if !envelope.Success || envelope.Feasibility == nil {
flowState.HealthStagnationCount = 0
return
}
flowState.HealthCheckDone = true
flowState.HealthIsFeasible = envelope.Feasibility.IsFeasible
flowState.HealthCapacityGap = envelope.Feasibility.CapacityGap
flowState.HealthReasonCode = strings.TrimSpace(envelope.Feasibility.ReasonCode)
if envelope.Metrics.Tightness != nil {
flowState.HealthTightnessLevel = strings.TrimSpace(envelope.Metrics.Tightness.TightnessLevel)
}
if envelope.Decision != nil {
flowState.HealthShouldContinueOptimize = envelope.Decision.ShouldContinueOptimize
flowState.HealthPrimaryProblem = strings.TrimSpace(envelope.Decision.PrimaryProblem)
flowState.HealthRecommendedOperation = strings.TrimSpace(envelope.Decision.RecommendedOperation)
flowState.HealthIsForcedImperfection = envelope.Decision.IsForcedImperfection
flowState.HealthImprovementSignal = strings.TrimSpace(envelope.Decision.ImprovementSignal)
}
if signal := strings.TrimSpace(flowState.HealthImprovementSignal); signal != "" && prevSignal != "" && signal == prevSignal {
flowState.HealthStagnationCount++
return
}
flowState.HealthStagnationCount = 0
}