package coordinator import ( "context" "fmt" "time" "chat-agent-system/internal/agent" "chat-agent-system/internal/app/stageflow" "chat-agent-system/internal/contracts/vocabulary" "chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/intent" "chat-agent-system/internal/kb" "chat-agent-system/internal/projectinstance" "chat-agent-system/internal/squad" "chat-agent-system/internal/stream" ) // CoordinationStatus 协调裁决状态 type CoordinationStatus string const ( StatusAccepted CoordinationStatus = "ACCEPTED" StatusRejected CoordinationStatus = "REJECTED" StatusClarificationRequired CoordinationStatus = "CLARIFICATION_REQUIRED" ) // PendingAction 声明对话层需执行的真实动作(由 Session 持有服务后执行)。 type PendingAction struct { Kind string // docgov.review_approve | docgov.approver_approve | docgov.reject | taskrun.resume | taskrun.restart DocID string RunID string WorkDir string Reason string Comment string Rollback vocabulary.DocumentState TaskKey string } // CoordinationResult 协调层给对话层的回执 type CoordinationResult struct { Status CoordinationStatus `json:"status"` Intent intent.IntentType `json:"intent"` Branch intent.ResolutionBranch `json:"branch,omitempty"` RejectReason string `json:"reject_reason,omitempty"` ClarificationPrompt string `json:"clarification_prompt,omitempty"` NaturalReply string `json:"natural_reply"` AdvisorBrief *squad.AdvisorBriefing `json:"advisor_brief,omitempty"` ArtifactPath string `json:"artifact_path,omitempty"` UpdatedContext *UserProjectContext `json:"updated_context"` InteractiveCard *intent.InteractiveCardPayload `json:"interactive_card,omitempty"` NextExpectation *intent.PendingExpectation `json:"next_expectation,omitempty"` ClearExpectation bool `json:"clear_expectation,omitempty"` ReplySource string `json:"reply_source,omitempty"` // pi-agent | template PendingAction *PendingAction `json:"-"` // Task projection for clickable document entry (sync TRIGGER_TASK / adhoc). TaskStatus string `json:"task_status,omitempty"` DocID string `json:"doc_id,omitempty"` ArtifactTitle string `json:"artifact_title,omitempty"` TaskKey string `json:"task_key,omitempty"` PendingAgentCandidateID int64 `json:"pending_agent_candidate_id,omitempty"` PendingChangeReportID int64 `json:"pending_change_report_id,omitempty"` PendingRiskLevel string `json:"pending_risk_level,omitempty"` } // CoordinatorEngine L2 协调引擎 type CoordinatorEngine struct { stageTasks StageTaskRunner instSvc *projectinstance.Service assembler *ContextAssembler dialogueBackend agent.Backend realtimeSearcher RealtimeSearcher decideAssist DecideAssist dataDir string ctxMgr *ctxmgr.ContextManager kbClient *kb.Client impact ImpactActions projectAccess ProjectAccess } // StageTaskRunner is the application owner for ExecuteTask / ExecuteStage. type StageTaskRunner interface { ExecuteTask(ctx context.Context, req stageflow.TaskRunRequest) (*stageflow.TaskRunResult, error) ExecuteStage(ctx context.Context, req stageflow.StageRunRequest) (*stageflow.StageRunResult, error) } func (e *CoordinatorEngine) SetStageTaskRunner(r StageTaskRunner) { if e != nil { e.stageTasks = r } } func (e *CoordinatorEngine) tasks() StageTaskRunner { if e == nil { return nil } return e.stageTasks } func (e *CoordinatorEngine) SetContextServices(ctxMgr *ctxmgr.ContextManager, kbClient *kb.Client) { if e != nil { e.ctxMgr = ctxMgr e.kbClient = kbClient } } // SetInstanceService 注入项目工作流实例服务(对话推进与实例门禁合流)。 func (e *CoordinatorEngine) SetInstanceService(svc *projectinstance.Service) { if e != nil { e.instSvc = svc } } // SetRealtimeSearcher 注入对话路径的实时检索(天气/新闻等);nil 时跳过强制检索。 func (e *CoordinatorEngine) SetRealtimeSearcher(s RealtimeSearcher) { if e != nil { e.realtimeSearcher = s } } // SetImpactActions wires read-only impact analysis and confirmed revision proposals. func (e *CoordinatorEngine) SetImpactActions(a ImpactActions) { if e != nil { e.impact = a } } func NewCoordinatorEngine(runner StageTaskRunner) *CoordinatorEngine { return &CoordinatorEngine{ stageTasks: runner, assembler: NewContextAssembler(), } } // NewCoordinatorEngineWithDialogue 注入对话 LLM 后端(Phase A:CASUAL / AMBIGUOUS) func NewCoordinatorEngineWithDialogue(runner StageTaskRunner, dialogueBackend agent.Backend, dataDir string) *CoordinatorEngine { return &CoordinatorEngine{ stageTasks: runner, assembler: NewContextAssembler(), dialogueBackend: dialogueBackend, dataDir: dataDir, } } // ProcessIntent 校验候选意图合法性并驱动调度与响应 func (e *CoordinatorEngine) ProcessIntent( ctx context.Context, recRes *intent.IntentRecognitionResult, userCtx *UserProjectContext, ) (*CoordinationResult, error) { if userCtx == nil { userCtx = &UserProjectContext{ User: UserInfo{ID: "guest", Name: "用户", Role: "engineer", ProjectRole: "composer"}, BlackboardFacts: make(map[string]string), } } // 交互卡片 TTL 轮次消退与生命周期清理 if userCtx.InteractiveCard != nil { _ = ApplyContextUpdate(userCtx, ContextUpdate{ExpectedVersion: userCtx.Version, TickInteractive: true}) } if userCtx.PendingExpectation != nil && !userCtx.PendingExpectation.ExpiresAt.IsZero() && time.Now().After(userCtx.PendingExpectation.ExpiresAt) { userCtx.applyPending(nil) } top := recRes.TopIntent if top == nil { top = &intent.CandidateIntent{ Intent: intent.IntentCasualChat, Confidence: 1.0, } } // Phase 5:RankedFrames 写/读冲突二次门禁 if intent.RankedFrameConflictRequiresClarification(recRes) { msg := "检测到查询与写操作意图接近,为避免误执行,请明确您是要查询状态,还是要执行变更操作?" return &CoordinationResult{ Status: StatusClarificationRequired, Intent: top.Intent, Branch: recRes.Branch, ClarificationPrompt: msg, NaturalReply: msg, UpdatedContext: userCtx, }, nil } // Phase 5:有限多意图计划(query+query / expectation_plus_new) if recRes.IntentPlan != nil && intent.CurrentFeatureFlags().MultiIntentEnabled { if planRes, planErr := e.executeIntentPlan(ctx, recRes, userCtx); planRes != nil { return planRes, planErr } } // Phase 5:V2 灰度路由(intent_v2_decision 开启且档位允许时对齐 V2 帧) if effective := intent.ResolveEffectiveIntent(recRes); effective != top.Intent { top = &intent.CandidateIntent{ Intent: effective, Confidence: top.Confidence, Branch: top.Branch, FilledSlots: top.FilledSlots, MissingSlots: top.MissingSlots, Reasoning: top.Reasoning + " (v2_decision)", ClarificationQuestion: top.ClarificationQuestion, MatchedOption: top.MatchedOption, PatchSlots: top.PatchSlots, IsDecline: top.IsDecline, } recRes.TopIntent = top } streamer := stream.FromContext(ctx) if streamer != nil { streamer.EmitStatus(string(top.Intent), string(recRes.Branch), string(userCtx.Project.CurrentStage), fmt.Sprintf("意图判定: %s (置信度: %.0f%%)", top.Intent, top.Confidence*100), top.FilledSlots) } logRouteDecision(ctx, recRes, RouteIntent(top.Intent, TaskProfile{})) // 1. 若缺少核心槽位且需定向澄清(优先于模糊咨询,避免缺槽追问被参谋兜底截断) if recRes.RequiresClarifying && top.ClarificationQuestion != "" { return &CoordinationResult{ Status: StatusClarificationRequired, Intent: top.Intent, Branch: recRes.Branch, ClarificationPrompt: top.ClarificationQuestion, NaturalReply: top.ClarificationQuestion, UpdatedContext: userCtx, }, nil } // 2. 若为分支 3 (模糊/分忧引导),触发参谋主动分析与建议(Phase A:优先真实 LLM) if recRes.Branch == intent.BranchAmbiguousFallback { res, err := e.handleAmbiguousConsult(ctx, top, userCtx, recRes.RawInput) if err == nil && res != nil { e.enrichAdvisorBriefWithCases(ctx, res.AdvisorBrief, userCtx, top.FilledSlots) } return res, err } // 3. 根据候选意图执行分支 var coordRes *CoordinationResult var coordErr error switch top.Intent { case intent.IntentDeclineExpectation: coordRes, coordErr = e.handleDeclineExpectation(userCtx) case intent.IntentQueryProgress: coordRes, coordErr = e.handleQueryProgress(ctx, top, userCtx, recRes.RawInput) case intent.IntentSupplementInfo: coordRes, coordErr = e.handleSupplementInfo(top, userCtx) case intent.IntentAdvanceStage: coordRes, coordErr = e.handleAdvanceStage(ctx, top, userCtx) case intent.IntentTriggerTask: coordRes, coordErr = e.handleTriggerTask(ctx, top, userCtx) case intent.IntentTriggerAdhocTask: coordRes, coordErr = e.handleTriggerAdhocTask(ctx, top, userCtx, recRes.RawInput) case intent.IntentCasualChat: coordRes, coordErr = e.handleCasualChat(ctx, userCtx, recRes.RawInput) case intent.IntentSecurityForbidden: coordRes, coordErr = e.handleSecurityForbidden(userCtx) case intent.IntentDocGovReviewReject, intent.IntentDocGovFinalReject: coordRes, coordErr = e.handleDocGovReject(ctx, top, userCtx) case intent.IntentQueryRejectReason: coordRes, coordErr = e.handleQueryRejectReason(ctx, top, userCtx) case intent.IntentDocGovReviewApprove, intent.IntentDocGovFinalApprove: coordRes, coordErr = e.handleDocGovApprove(ctx, top, userCtx) case intent.IntentDocGovClaim: coordRes, coordErr = e.handleDocGovClaim(ctx, top, userCtx) case intent.IntentDocGovSubmitReview: coordRes, coordErr = e.handleDocGovSubmitReview(ctx, top, userCtx) case intent.IntentDocGovProposalDecide: coordRes, coordErr = e.handleDocGovProposalDecide(ctx, top, userCtx) case intent.IntentQueryRegulations: coordRes, coordErr = e.handleQueryRegulations(ctx, top, userCtx) case intent.IntentQuerySimilarCases: coordRes, coordErr = e.handleQuerySimilarCases(ctx, top, userCtx) case intent.IntentQueryUserRights: coordRes, coordErr = e.handleQueryUserRights(ctx, top, userCtx) case intent.IntentSwitchWorkflow: coordRes, coordErr = e.handleSwitchWorkflow(ctx, top, userCtx) case intent.IntentTriggerWorkflow: coordRes, coordErr = e.handleTriggerWorkflow(ctx, top, userCtx) case intent.IntentQueryWorkflowStatus: coordRes, coordErr = e.handleQueryWorkflowStatus(ctx, top, userCtx) case intent.IntentQueryDocStatus: coordRes, coordErr = e.handleQueryDocStatus(ctx, top, userCtx) case intent.IntentHelpCapabilities: coordRes, coordErr = e.handleHelpCapabilities(ctx, top, userCtx) case intent.IntentUnsupportedAction: coordRes, coordErr = e.handleUnsupportedAction(ctx, top, userCtx) case intent.IntentRunStatusQuery: coordRes, coordErr = e.handleRunStatusQuery(ctx, top, userCtx) case intent.IntentRunResume: coordRes, coordErr = e.handleRunResume(ctx, top, userCtx) case intent.IntentRunRestart: coordRes, coordErr = e.handleRunRestart(ctx, top, userCtx) case intent.IntentImpactAnalyze: coordRes, coordErr = e.handleImpactAnalyze(ctx, top, userCtx, recRes.RawInput) case intent.IntentRevisePropose: coordRes, coordErr = e.handleRevisePropose(ctx, top, userCtx, recRes.RawInput) default: nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name) coordRes = &CoordinationResult{ Status: StatusAccepted, Intent: top.Intent, Branch: recRes.Branch, NaturalReply: "我已收到您的指示。针对当前项目,请问需要我为您查询进展、补充技术事实,还是推进方案论证?随时吩咐我。", UpdatedContext: userCtx, NextExpectation: nextExp, } } if coordErr == nil && coordRes != nil { e.enrichAdvisorBriefWithCases(ctx, coordRes.AdvisorBrief, userCtx, top.FilledSlots) } return coordRes, coordErr }