返回 DeepSeek-Reasonix
plan_decision.go
根目录 / internal / control / plan_decision.go
1 package control
2
3 import (
4 "encoding/json"
5 "fmt"
6 "strings"
7
8 "reasonix/internal/event"
9 "reasonix/internal/sessioninbox"
10 )
11
12 // ResolvePlanDecision answers the Plan card without collapsing revise and exit.
13 func (c *Controller) ResolvePlanDecision(id string, action PlanDecisionAction) error {
14 return c.ResolvePlanDecisionWithFeedback(id, action, "")
15 }
16
17 // ResolvePlanDecisionWithFeedback durably stages revision guidance before the
18 // approval is removed. A persistence failure leaves the card pending for retry.
19 func (c *Controller) ResolvePlanDecisionWithFeedback(id string, action PlanDecisionAction, feedback string) error {
20 defer c.refreshRuntimeState(event.Event{})
21 return c.resolvePlanDecisionWithFeedbackLocked(id, action, feedback)
22 }
23
24 func (c *Controller) resolvePlanDecisionWithFeedbackLocked(id string, action PlanDecisionAction, feedback string) error {
25 if c == nil {
26 return fmt.Errorf("controller is nil")
27 }
28 id = strings.TrimSpace(id)
29 if id == "" {
30 return fmt.Errorf("empty plan approval id")
31 }
32 switch action {
33 case PlanDecisionStartExecution, PlanDecisionRevisePlan, PlanDecisionExitPlan:
34 default:
35 return fmt.Errorf("unknown plan decision %q", action)
36 }
37 feedback = strings.TrimSpace(feedback)
38 planPayload, err := json.Marshal(map[string]any{"requestId": id, "decision": action, "feedback": feedback})
39 if err != nil {
40 return err
41 }
42 var staged sessioninbox.InboxReceipt
43 rollback := func() {
44 if staged.ItemID != "" && !staged.Idempotent {
45 _ = c.DeleteInboxItem(staged.ItemID)
46 }
47 }
48 pending, ok, err := c.approval.resolveToolAfter(id, planApprovalTool, func(p pendingApproval) error {
49 if action == PlanDecisionRevisePlan && feedback != "" {
50 var enqueueErr error
51 staged, enqueueErr = c.EnqueueInbox(InboxRequest{Intent: sessioninbox.IntentFollowup, Display: feedback, Raw: feedback, Submit: feedback, Source: "plan_revision", Idempotency: "plan-revision:" + id})
52 if enqueueErr != nil {
53 return fmt.Errorf("queue plan revision: %w", enqueueErr)
54 }
55 }
56 state := PromptRejected
57 if action == PlanDecisionStartExecution {
58 state = PromptAnswered
59 }
60 if emitErr := c.emitTurnEventChecked(event.Event{
61 Kind: event.PromptAnswered, ItemID: id, InteractionState: string(state), Status: event.TurnInProgress,
62 DomainKind: "plan/state", DomainPayload: planPayload,
63 }); emitErr != nil {
64 rollback()
65 return emitErr
66 }
67 return nil
68 })
69 if err != nil {
70 return err
71 }
72 if !ok || pending.reply == nil {
73 rollback()
74 return nil
75 }
76 terminal := PromptRejected
77 if action == PlanDecisionStartExecution {
78 terminal = PromptAnswered
79 }
80 c.promptOwner.MarkIDTerminal(id, terminal)
81 pending.kind = "plan"
82 c.recordDecisionReceipt(pending, string(action))
83 pending.reply <- approvalReply{allow: action == PlanDecisionStartExecution}
84 return nil
85 }
86
86 lines GO