Files
Agent-System/internal/coordinator/confirm_gate.go
T
weichenandCursor 5ba9bbbe11 feat(coordinator): smart confirm gate before TRIGGER_TASK execution
Ask via PendingExpectation for weak-write verbs, mid confidence, or
stage-mismatched doc tasks instead of running blindly.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-21 08:27:49 +00:00

204 lines
6.6 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"
"strings"
"time"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/maturity"
"chat-agent-system/internal/projectinstance"
)
// taskDisplayTitle 用户可见的交付物标题(确认话术用)。
func taskDisplayTitle(taskKey string) string {
switch strings.TrimSpace(taskKey) {
case "intake", "bd_intake":
return "用户初始需求收集表"
case "user_requirements", "bd_user_requirements":
return "用户需求表"
case "research", "bd_research":
return "技术与市场调研报告"
case "feasibility_reg", "bd_feasibility_reg":
return "法规与注册论证报告"
case "feasibility_dev", "bd_feasibility_dev":
return "设计开发可行性评估"
case "prelim_design", "bd_prelim_design":
return "系统初步设计方案"
case "cdo_quote", "bd_cdo_quote":
return "CDO报价单"
default:
if taskKey == "" {
return "目标交付物"
}
return taskKey
}
}
// confirmGate 智能确认门禁:弱写动词 / 阶段不匹配 / 中等置信时先下发 PendingExpectation,不盲目执行。
// 返回非 nil 时 handleTriggerTask 应直接返回该结果。
func (e *CoordinatorEngine) confirmGate(
ctx context.Context,
cand *intent.CandidateIntent,
userCtx *UserProjectContext,
taskKey string,
) *CoordinationResult {
if cand == nil || userCtx == nil {
return nil
}
if cand.FilledSlots[intent.SlotWorkflowScopeMismatch] == "true" {
return nil
}
needsConfirm := cand.FilledSlots[intent.SlotNeedsConfirmation] == "true"
midConfidence := cand.Confidence >= 0.6 && cand.Confidence <= 0.85
stageMismatch := false
if e != nil && e.instSvc != nil && e.instSvc.Available() {
projectID := parseDecimalID(userCtx.Project.ID)
userID := parseDecimalID(userCtx.User.ID)
if projectID > 0 && userID > 0 && taskKey != "" {
isCreator := userCtx.Project.CreatorUserID > 0 && userID == userCtx.Project.CreatorUserID
d := e.instSvc.CheckDocTaskAccess(ctx, projectID, userID, taskKey, projectinstance.DocAccessActionRun, isCreator)
if !d.Allowed && d.Code == projectinstance.AccessCodeNotCurrentStage {
stageMismatch = true
}
}
}
if !needsConfirm && !stageMismatch && !midConfidence {
return nil
}
title := taskDisplayTitle(taskKey)
exp := buildTriggerConfirmExpectation(taskKey, title, userCtx, stageMismatch)
userCtx.applyPending(exp)
prompt := formatTriggerConfirmPrompt(title, needsConfirm, stageMismatch, midConfidence, userCtx)
return &CoordinationResult{
Status: StatusClarificationRequired,
Intent: cand.Intent,
Branch: cand.Branch,
ClarificationPrompt: prompt,
NaturalReply: prompt,
UpdatedContext: userCtx,
NextExpectation: exp,
}
}
func buildTriggerConfirmExpectation(
taskKey, title string,
userCtx *UserProjectContext,
stageMismatch bool,
) *intent.PendingExpectation {
optRun := intent.ExpectationOption{
OptionID: "1",
Title: fmt.Sprintf("开始整理并编写《%s》", title),
Description: "确认后启动正式文档任务,写入受控交付物",
Intent: intent.IntentTriggerTask,
Slots: map[string]string{
intent.SlotTargetTask: taskKey,
},
}
// 阶段不匹配时:默认改为当前阶段摸底表,避免再次撞门禁。
if stageMismatch {
stage := userCtx.Project.CurrentStage
if stage == "" || stage == maturity.StageV01_Triage {
optRun = intent.ExpectationOption{
OptionID: "1",
Title: "改为编制当前阶段《用户初始需求收集表》",
Description: "当前仍在 V0.1 摸底期,先完成初始需求收集更合适",
Intent: intent.IntentTriggerTask,
Slots: map[string]string{
intent.SlotTargetTask: "intake",
},
}
}
}
optTalk := intent.ExpectationOption{
OptionID: "2",
Title: "先在对话中梳理,暂不启动文档任务",
Description: "继续对话细化需求要点,稍后再决定是否生成正式文档",
Intent: intent.IntentCasualChat,
}
options := []intent.ExpectationOption{optRun, optTalk}
if stageMismatch {
options = append(options, intent.ExpectationOption{
OptionID: "3",
Title: "先推进到下一成熟度阶段",
Description: "确认阶段准入后再编制后续文档",
Intent: intent.IntentAdvanceStage,
Slots: map[string]string{
intent.SlotTargetStage: string(nextStageAfter(userCtx.Project.CurrentStage)),
},
})
}
return &intent.PendingExpectation{
ExpectationID: fmt.Sprintf("confirm-trigger-%d", time.Now().UnixNano()),
ExpectedIntent: intent.IntentTriggerTask,
TargetAction: "confirm_trigger_task",
Prompt: fmt.Sprintf("是否开始编写《%s》?", title),
AffirmativeWord: "好",
DeclineWord: "先不",
ExpiresAt: time.Now().Add(30 * time.Minute),
ContextSnippet: fmt.Sprintf("候选任务:%s(%s)", title, taskKey),
DefaultOption: &optRun,
Options: options,
AllowAffirmative: true,
CreatedAt: time.Now(),
}
}
func nextStageAfter(stage maturity.MaturityStage) maturity.MaturityStage {
switch stage {
case maturity.StageV01_Triage, "":
return maturity.StageV05_Feasibility
case maturity.StageV05_Feasibility:
return maturity.StageV10_Quoting
default:
return maturity.StageV05_Feasibility
}
}
func formatTriggerConfirmPrompt(
title string,
needsConfirm, stageMismatch, midConfidence bool,
userCtx *UserProjectContext,
) string {
var b strings.Builder
switch {
case stageMismatch:
stageLabel := string(userCtx.Project.CurrentStage)
if stageLabel == "" {
stageLabel = "初期"
}
b.WriteString(fmt.Sprintf(
"您提到的《%s》不在当前活动阶段(%s)。为避免启动后失败,请先确认下一步:\n\n",
title, stageLabel,
))
b.WriteString("1. 改为编制当前阶段《用户初始需求收集表》(推荐,回复「好」或「1」)\n")
b.WriteString("2. 先在对话中梳理,暂不启动文档任务\n")
b.WriteString("3. 先推进到下一成熟度阶段\n")
case needsConfirm:
b.WriteString(fmt.Sprintf(
"我理解您希望先整理需求。更稳妥的做法是开始整理并编写《%s》。请确认:\n\n",
title,
))
b.WriteString(fmt.Sprintf("1. 开始整理并编写《%s》(推荐,回复「好」或「1」)\n", title))
b.WriteString("2. 先在对话中梳理,暂不启动文档任务\n")
default: // midConfidence
b.WriteString(fmt.Sprintf(
"我倾向于为您启动《%s》的编制任务,但表述略有歧义。请确认:\n\n",
title,
))
b.WriteString(fmt.Sprintf("1. 确认启动《%s》(回复「好」或「1」)\n", title))
b.WriteString("2. 先在对话中说明,暂不启动\n")
}
_ = midConfidence
return b.String()
}