Files
Agent-System/internal/coordinator/engine.go
T
weichen b820e0070d fix(taskrun): 刷新后恢复进行中任务并纠正进度问询
进行中的 run 提前写下消息锚点,并按会话列出活跃任务;进度疑问句改为查询真实状态,交付物已落盘且不是传输失败时不再被空结果否决。
2026-09-28 06:09:42 +00:00

444 lines
15 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 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.claim | docgov.submit_review | docgov.review_approve | docgov.approver_approve | docgov.reject | docgov.comment_add | docgov.comment_resolve | docgov.reopen_request | docgov.reopen_approve | workflow.run | taskrun.resume | taskrun.restart
DocID string
WorkflowKey string
RunID string
WorkDir string
Reason string
Comment string
Rollback vocabulary.DocumentState
TaskKey string
Role string
Section string
RequestID int64
}
// 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
docResolver DocResolver
pendingSupps PendingSupplementReader
reopenLister ReopenLister
sessionRuns SessionRunReader
// OnSupplement is optional; composition typically wires supplementimpact.TriggerAsync.
// Nil-safe: unset hooks are skipped.
OnSupplement func(ctx context.Context, projectID, userID int64, sessionID, summary, markdown string)
pendingSupplementCard *intent.InteractiveCardPayload
}
// 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
}
// SessionRunSnapshot 是进度问询可读的任务运行投影,不含工作目录绝对路径。
type SessionRunSnapshot struct {
RunID string
TaskKey string
Status string
WorkspaceID string
ErrorMessage string
UpdatedAt time.Time
}
// SessionRunReader 按会话读取最近一次任务运行。未注入时进度问询保持模板答复。
type SessionRunReader interface {
LatestSessionRun(ctx context.Context, sessionID string) (*SessionRunSnapshot, error)
}
func (e *CoordinatorEngine) SetSessionRunReader(reader SessionRunReader) {
if e != nil {
e.sessionRuns = reader
}
}
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
}
}
// SetDocResolver wires the controlled-document resolution gate.
func (e *CoordinatorEngine) SetDocResolver(r DocResolver) {
if e != nil {
e.docResolver = r
}
}
// SetPendingSupplementReader wires locked-period pending supplement queries.
func (e *CoordinatorEngine) SetPendingSupplementReader(r PendingSupplementReader) {
if e != nil {
e.pendingSupps = r
}
}
// SetReopenLister wires pending reopen listing for approve dialogue.
func (e *CoordinatorEngine) SetReopenLister(r ReopenLister) {
if e != nil {
e.reopenLister = r
}
}
// SetOnSupplement wires the optional post-blackboard supplement impact hook.
func (e *CoordinatorEngine) SetOnSupplement(fn func(ctx context.Context, projectID, userID int64, sessionID, summary, markdown string)) {
if e != nil {
e.OnSupplement = fn
}
}
// HasDocResolver reports whether a document resolution gate is already wired.
func (e *CoordinatorEngine) HasDocResolver() bool {
return e != nil && e.docResolver != nil
}
func (e *CoordinatorEngine) SetPendingSupplementCard(card *intent.InteractiveCardPayload) {
if e == nil {
return
}
e.pendingSupplementCard = card
}
func (e *CoordinatorEngine) TakePendingSupplementCard() *intent.InteractiveCardPayload {
if e == nil {
return nil
}
c := e.pendingSupplementCard
e.pendingSupplementCard = nil
return c
}
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,
}
}
if userCtx.AttachmentOnlyTurn && len(userCtx.PendingTurnAttachments) > 0 {
return e.handleUploadIntake(ctx, userCtx)
}
if top.FilledSlots["upload_action"] == uploadActionReviseGuidance {
return handleUploadReviseGuidance(top, userCtx), nil
}
// 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.IntentDocGovCommentAdd:
coordRes, coordErr = e.handleDocGovCommentAdd(ctx, top, userCtx)
case intent.IntentDocGovCommentResolve:
coordRes, coordErr = e.handleDocGovCommentResolve(ctx, top, userCtx)
case intent.IntentDocGovReopenRequest:
coordRes, coordErr = e.handleDocGovReopenRequest(ctx, top, userCtx)
case intent.IntentDocGovReopenApprove:
coordRes, coordErr = e.handleDocGovReopenApprove(ctx, top, userCtx)
case intent.IntentQueryPendingSupplements:
coordRes, coordErr = e.handleQueryPendingSupplements(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.IntentConfirmAction:
coordRes, coordErr = e.handleConfirmActionHit(top, userCtx, recRes.Branch)
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:
coordRes = e.genericAcknowledgement(top, userCtx, recRes.Branch)
}
if coordErr == nil && coordRes != nil {
e.enrichAdvisorBriefWithCases(ctx, coordRes.AdvisorBrief, userCtx, top.FilledSlots)
}
return coordRes, coordErr
}