返回 DeepSeek-Reasonix
decision_runtime_api.go
根目录 / desktop / decision_runtime_api.go
1 package main
2
3 import (
4 "fmt"
5 "reasonix/internal/agent"
6 "reasonix/internal/control"
7 "reasonix/internal/event"
8 )
9
10 // PromptAnswerView is the Wails-safe union for all interactive cards.
11 type PromptAnswerView struct {
12 Questions []QuestionAnswer `json:"questions,omitempty"`
13 Allow bool `json:"allow,omitempty"`
14 Session bool `json:"session,omitempty"`
15 Persist bool `json:"persist,omitempty"`
16 Action string `json:"action,omitempty"`
17 Feedback string `json:"feedback,omitempty"`
18 Content map[string]any `json:"content,omitempty"`
19 Generation uint64 `json:"generation,omitempty"`
20 PermissionRevision uint64 `json:"permissionRevision,omitempty"`
21 }
22
23 type PromptIdentityView struct {
24 PromptID string `json:"promptId"`
25 TurnID string `json:"turnId"`
26 RuntimeEpoch string `json:"runtimeEpoch,omitempty"`
27 Kind string `json:"kind"`
28 }
29
30 type InteractionTargetView struct {
31 TabID string `json:"tabId"`
32 HostID string `json:"hostId"`
33 SessionID string `json:"sessionId"`
34 SessionGeneration uint64 `json:"sessionGeneration"`
35 PromptID string `json:"promptId"`
36 TurnID string `json:"turnId"`
37 RuntimeEpoch string `json:"runtimeEpoch"`
38 Kind string `json:"kind"`
39 }
40
41 type RemotePromptTarget = InteractionTargetView
42
43 func (a *App) PendingPromptIdentitiesForTab(tabID string) ([]PromptIdentityView, error) {
44 tab, ctrl := a.tabAndCtrlByID(tabID)
45 if ctrl == nil {
46 return []PromptIdentityView{}, a.workspaceNotReadyErr(tab)
47 }
48 reader, ok := ctrl.(interface {
49 PendingPromptIdentities() []control.PromptIdentity
50 })
51 if !ok {
52 return []PromptIdentityView{}, fmt.Errorf("prompt identity replay is unavailable")
53 }
54 identities := reader.PendingPromptIdentities()
55 views := make([]PromptIdentityView, 0, len(identities))
56 for _, identity := range identities {
57 views = append(views, PromptIdentityView{PromptID: identity.PromptID, TurnID: identity.TurnID, RuntimeEpoch: identity.RuntimeEpoch, Kind: string(identity.Kind)})
58 }
59 return views, nil
60 }
61
62 // ReplayPendingPromptIdentitiesForTab returns the authoritative identity list
63 // without re-emitting cards. The existing ReplayPendingPromptsForTab method
64 // remains the event replay compatibility surface.
65 func (a *App) ReplayPendingPromptIdentitiesForTab(tabID string) ([]PromptIdentityView, error) {
66 return a.PendingPromptIdentitiesForTab(tabID)
67 }
68
69 // ResolvePromptForTab is the canonical exact-identity decision endpoint.
70 func (a *App) ResolvePromptForTab(tabID, promptID, turnID, runtimeEpoch, kind string, answer PromptAnswerView) error {
71 tab, ctrl := a.tabAndCtrlByID(tabID)
72 if ctrl == nil {
73 return a.workspaceNotReadyErr(tab)
74 }
75 questions := make([]event.AskAnswer, len(answer.Questions))
76 for i, q := range answer.Questions {
77 questions[i] = event.AskAnswer{QuestionID: q.QuestionID, Selected: q.Selected}
78 }
79 resolver, ok := ctrl.(interface {
80 ResolvePromptExact(control.PromptIdentity, control.PromptAnswer) error
81 })
82 if !ok {
83 return fmt.Errorf("this runtime cannot resolve exact prompts")
84 }
85 return resolver.ResolvePromptExact(
86 control.PromptIdentity{PromptID: promptID, TurnID: turnID, RuntimeEpoch: runtimeEpoch, Kind: control.PromptKind(kind)},
87 control.PromptAnswer{Questions: questions, Allow: answer.Allow, Session: answer.Session, Persist: answer.Persist, Action: answer.Action, Feedback: answer.Feedback, Content: answer.Content, Generation: answer.Generation, PermissionRevision: answer.PermissionRevision},
88 )
89 }
90
91 // ResolvePromptForSession fixes the controller and session binding before
92 // dispatch. A reusable tab cannot redirect an old card to its new session.
93 func (a *App) ResolvePromptForSession(target InteractionTargetView, answer PromptAnswerView) error {
94 // Generation zero is a valid initial binding. Equality with the live tab,
95 // together with exact session/turn/runtime identity, fences stale answers.
96 if target.TabID == "" || target.SessionID == "" ||
97 target.PromptID == "" || target.TurnID == "" || target.Kind == "" {
98 return fmt.Errorf("exact prompt and session identity is required")
99 }
100 if target.HostID != "" && target.HostID != localDesktopHostID {
101 return fmt.Errorf("prompt host binding is stale")
102 }
103 a.mu.RLock()
104 tab := a.tabByIDLocked(target.TabID)
105 if tab == nil || tab.SessionID != target.SessionID || tab.SessionGeneration != target.SessionGeneration {
106 a.mu.RUnlock()
107 return fmt.Errorf("prompt session binding is stale")
108 }
109 ctrl := tab.Ctrl
110 a.mu.RUnlock()
111 if ctrl == nil {
112 return fmt.Errorf("prompt runtime is unavailable")
113 }
114 resolver, ok := ctrl.(interface {
115 ResolvePromptExact(control.PromptIdentity, control.PromptAnswer) error
116 })
117 if !ok {
118 return fmt.Errorf("this runtime cannot resolve exact prompts")
119 }
120 questions := make([]event.AskAnswer, len(answer.Questions))
121 for i, q := range answer.Questions {
122 questions[i] = event.AskAnswer{QuestionID: q.QuestionID, Selected: q.Selected}
123 }
124 return resolver.ResolvePromptExact(
125 control.PromptIdentity{PromptID: target.PromptID, TurnID: target.TurnID, RuntimeEpoch: target.RuntimeEpoch, Kind: control.PromptKind(target.Kind)},
126 control.PromptAnswer{Questions: questions, Allow: answer.Allow, Session: answer.Session, Persist: answer.Persist, Action: answer.Action, Feedback: answer.Feedback, Content: answer.Content, Generation: answer.Generation, PermissionRevision: answer.PermissionRevision},
127 )
128 }
129
130 // ApproveTabForTurn is the exact-identity approval entry point for new
131 // frontends. The legacy ApproveTab method remains available to old clients.
132 func (a *App) ApproveTabForTurn(tabID, turnID, runtimeEpoch, id string, allow, session, persist bool) error {
133 ctrl, err := a.validatePromptIdentity(tabID, turnID, runtimeEpoch)
134 if err != nil {
135 return err
136 }
137 ctrl.Approve(id, allow, session, persist)
138 return nil
139 }
140
141 func (a *App) ResolvePlanDecisionTabForTurn(tabID, turnID, runtimeEpoch, id, action string) error {
142 ctrl, err := a.validatePromptIdentity(tabID, turnID, runtimeEpoch)
143 if err != nil {
144 return err
145 }
146 return ctrl.ResolvePlanDecision(id, control.PlanDecisionAction(action))
147 }
148
149 func (a *App) ResolveRecoveryTabForTurn(tabID, turnID, runtimeEpoch, id, action, feedback string) error {
150 ctrl, err := a.validatePromptIdentity(tabID, turnID, runtimeEpoch)
151 if err != nil {
152 return err
153 }
154 return ctrl.ResolveRecovery(id, agent.RecoveryAction(action), feedback)
155 }
156
157 func (a *App) AnswerMCPInteractionForTurn(tabID, turnID, runtimeEpoch, id, action string, content map[string]any) error {
158 ctrl, err := a.validatePromptIdentity(tabID, turnID, runtimeEpoch)
159 if err != nil {
160 return err
161 }
162 resolver, ok := ctrl.(interface {
163 AnswerMCPInteractionChecked(string, string, map[string]any) error
164 })
165 if !ok {
166 return errMCPPromptUnsupported
167 }
168 return resolver.AnswerMCPInteractionChecked(id, action, content)
169 }
170
170 lines GO