后端: 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 结构、卡片分类、前后端发射/渲染约束
333 lines
11 KiB
Go
333 lines
11 KiB
Go
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=%d,reason=%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=%d,reason=%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
|
||
}
|