Files
Agent-System/internal/server/task_conflict_cards.go
T
weichen 76ddf4dd35 feat(ctx): surface dialogue conflict confirmation cards to triggering user
Capture trigger_user HITL expectations from dialogue adjudication (previously
dropped at the bizdev call site), project them as chat confirmation cards via
taskrun OnTerminal (exactly-once), arm the session PendingExpectation, and
handle confirm_context_fact hits through the supplement-info blackboard path.
Decline (纠正) guides the user to state the correct fact.
2026-09-25 21:01:37 +00:00

75 lines
2.8 KiB
Go

package server
import (
"context"
"fmt"
"strings"
"time"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/chat"
"chat-agent-system/internal/coordinator"
"chat-agent-system/internal/taskrun"
)
// projectContextConflictCards surfaces dialogue-context conflict confirmation
// cards (HITL trigger_user route) to the user who triggered the run:
// 1. a durable assistant message in the run's chat session (history APIs);
// 2. the first card armed as PendingExpectation on the live session so a
// follow-up「确认/纠正」hits the expectation branch.
//
// The adjudication ledger (context_conflict_adjudications) and the workspace
// CONFIRMATIONS.md remain the authoritative record; this projection is UX.
func (s *Server) projectContextConflictCards(ctx context.Context, record *taskrun.RunRecord, result *bizdev.TaskRunResult) {
if s == nil || record == nil || result == nil || len(result.PendingContextConflicts) == 0 {
return
}
if strings.TrimSpace(record.SessionID) == "" {
return
}
content := renderContextConflictCardMessage(result.PendingContextConflicts)
if cm := s.ctxMgr(); cm != nil {
meta := map[string]any{
"run_id": record.RunID,
"project_id": record.ProjectID,
"client_turn_id": record.ClientTurnID,
"ctx_conflict_card": true,
}
_, _ = cm.RecordUserMessageWithID(ctx, record.SessionID, 0, "assistant", content, "CTX_CONFLICT_CARD", "EXPECTATION_HIT", meta)
}
s.sessionsMu.RLock()
sess := s.sessions[record.SessionID]
s.sessionsMu.RUnlock()
if sess == nil {
return
}
if exp := bizdev.ContextConflictExpectation(result.PendingContextConflicts[0]); exp != nil && sess.UserCtx != nil {
_ = coordinator.ApplyContextUpdate(sess.UserCtx, coordinator.ContextUpdate{
ExpectedVersion: sess.UserCtx.Version,
SetPendingExpectation: true,
PendingExpectation: exp,
})
}
sess.AppendMessage(chat.Message{
ID: fmt.Sprintf("msg_%d_cc", time.Now().UnixNano()),
Role: "assistant",
Content: content,
Timestamp: time.Now(),
})
}
// renderContextConflictCardMessage builds the chat-visible confirmation card
// text. Reply「确认」accepts the recommended winning value;「纠正」clears the
// card and the user states the correct fact, which SUPPLEMENT_INFO absorbs.
func renderContextConflictCardMessage(cards []bizdev.ContextConflictCard) string {
var sb strings.Builder
sb.WriteString("⚠️ 检测到项目对话与现有事实存在说法冲突,需要您确认:\n\n")
for i, c := range cards {
sb.WriteString(fmt.Sprintf("%d. %s\n 推荐采信:%s = %s\n 依据:%s\n", i+1, c.Prompt, c.FactKey, c.WinningValue, c.Snippet))
}
sb.WriteString("\n回复「确认」采信推荐值(将同步项目黑板);回复「纠正」后直接告诉我正确的事实。")
return sb.String()
}