返回 DeepSeek-Reasonix
goal_driver.go
根目录 / internal / control / goal_driver.go
1 package control
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "strings"
8 "sync"
9 "sync/atomic"
10 "time"
11
12 "reasonix/internal/event"
13 goaldomain "reasonix/internal/goal"
14 "reasonix/internal/session"
15 "reasonix/internal/tool"
16 )
17
18 // waitForGoalTerminal is the CLI/ACP/bot observer. It owns no Activity and
19 // therefore cannot block the driver; one model TurnDone is not completion of
20 // an armed long-running goal.
21 func (c *Controller) waitForGoalTerminal(ctx context.Context) error {
22 if c == nil || !c.sessionEngineEnabled() {
23 return nil
24 }
25 ticker := time.NewTicker(10 * time.Millisecond)
26 defer ticker.Stop()
27 for {
28 view, err := c.goalLifecycleView()
29 if err != nil {
30 return err
31 }
32 if view == nil || view.Phase != goaldomain.PhaseActive || view.Activation != goaldomain.ActivationArmed {
33 return nil
34 }
35 select {
36 case <-ctx.Done():
37 c.Cancel()
38 return ctx.Err()
39 case <-ticker.C:
40 }
41 }
42 }
43
44 // goalRoundReservation fences one proposed automatic turn to the exact idle
45 // runtime and goal version observed before the required durability checkpoint.
46 // It is process-local and is never restored or inherited by a fork.
47 type goalRoundReservation struct {
48 SessionID string
49 RuntimeEpoch string
50 IdleActivityRevision uint64
51 Goal goaldomain.Ref
52 Round uint64
53
54 mu sync.Mutex
55 admitted *goaldomain.View
56 runErr error
57 cancelled bool
58 }
59
60 type goalDriverControl struct {
61 inherited atomic.Bool
62 ctx context.Context
63 cancel context.CancelFunc
64 }
65
66 func (r *goalRoundReservation) setAdmitted(view goaldomain.View) {
67 r.mu.Lock()
68 copy := view
69 r.admitted = &copy
70 r.mu.Unlock()
71 }
72
73 func (r *goalRoundReservation) admittedView() (*goaldomain.View, bool) {
74 r.mu.Lock()
75 defer r.mu.Unlock()
76 if r.admitted == nil {
77 return nil, false
78 }
79 copy := *r.admitted
80 return &copy, true
81 }
82
83 func (r *goalRoundReservation) setResult(err error, cancelled bool) {
84 if r == nil {
85 return
86 }
87 r.mu.Lock()
88 r.runErr, r.cancelled = err, cancelled
89 r.mu.Unlock()
90 }
91
92 func (r *goalRoundReservation) result() (error, bool) {
93 r.mu.Lock()
94 defer r.mu.Unlock()
95 return r.runErr, r.cancelled
96 }
97
98 // kickGoalDriver publishes one level-triggered scheduling check. Duplicate
99 // idle notifications collapse to one worker; a successful worker admits at
100 // most one ordinary top-level turn and the next TurnDone supplies a fresh kick.
101 func (c *Controller) kickGoalDriver() {
102 if c == nil {
103 return
104 }
105 c.mu.Lock()
106 if c.closed {
107 c.mu.Unlock()
108 return
109 }
110 c.goalDriverMu.Lock()
111 if c.goalDriverPending {
112 c.goalDriverMu.Unlock()
113 c.mu.Unlock()
114 return
115 }
116 c.goalDriverPending = true
117 c.goalDriverWG.Add(1)
118 c.goalDriverMu.Unlock()
119 c.mu.Unlock()
120 go func() {
121 defer c.goalDriverWG.Done()
122 started := c.driveOneGoalRound()
123 c.goalDriverMu.Lock()
124 c.goalDriverPending = false
125 finishedBeforeRelease := started && c.goalDriverActive == nil
126 c.goalDriverMu.Unlock()
127 if finishedBeforeRelease {
128 c.kickGoalDriver()
129 }
130 }()
131 }
132
133 func (c *Controller) driveOneGoalRound() bool {
134 view, runtime, snapshot, ok := c.goalRoundEligibility()
135 if !ok {
136 return false
137 }
138 if used, limit, exhausted := c.goalResourceBudget(); exhausted {
139 _, _ = c.applyHostGoalMutation(context.Background(), "resource-budget", func(machine *goaldomain.Machine) (*goaldomain.View, error) {
140 blocked, err := machine.Block(view.Ref(), goaldomain.BlockReason{Code: "resource-budget", Message: fmt.Sprintf("the configured goal token budget was reached (%d/%d tokens)", used, limit)}, true, 0)
141 return &blocked, err
142 })
143 return false
144 }
145 if view.MaxGoalRounds != nil && view.RoundsStarted >= *view.MaxGoalRounds {
146 _, _ = c.applyHostGoalMutation(context.Background(), "round-limit", func(machine *goaldomain.Machine) (*goaldomain.View, error) {
147 blocked, err := machine.Block(view.Ref(), goaldomain.BlockReason{Code: "round-limit", Message: "the configured automatic goal round limit was reached"}, true, 0)
148 return &blocked, err
149 })
150 return false
151 }
152 reservation := &goalRoundReservation{
153 SessionID: snapshot.Ref.SessionID, RuntimeEpoch: snapshot.Epoch,
154 IdleActivityRevision: snapshot.ActivityRevision,
155 Goal: view.Ref(), Round: view.RoundsStarted + 1,
156 }
157 // This is a semantic checkpoint: no downstream model call starts unless all
158 // already accepted events are durable.
159 flushCtx := c.goalDriverControl.ctx
160 if flushCtx == nil {
161 flushCtx = context.Background()
162 }
163 if _, err := runtime.Session().Flush(flushCtx); err != nil {
164 if errors.Is(err, context.Canceled) {
165 return false
166 }
167 c.disarmGoalLifecycle("persistence-error")
168 c.noticeDetail("Goal automatic continuation stopped because session persistence failed.", err.Error())
169 return false
170 }
171 current, currentRuntime, currentSnapshot, ok := c.goalRoundEligibility()
172 if !ok || currentRuntime != runtime || currentSnapshot.Ref.SessionID != reservation.SessionID ||
173 currentSnapshot.Epoch != reservation.RuntimeEpoch || currentSnapshot.ActivityRevision != reservation.IdleActivityRevision ||
174 current.ID != reservation.Goal.ID || current.Revision != reservation.Goal.Revision || current.RoundsStarted+1 != reservation.Round {
175 return false
176 }
177 prompt, err := goaldomain.ContinuationPrompt(*current)
178 if err != nil {
179 return false
180 }
181 c.goalDriverMu.Lock()
182 c.goalDriverActive = reservation
183 c.goalDriverMu.Unlock()
184 result := c.runGuardedGoalRound(reservation, func(ctx context.Context) error {
185 return newTurnOrchestrator(c).runOrchestratedTurn(ctx, orchestratedTurn{
186 input: prompt, raw: prompt, synthetic: true, goalRound: reservation,
187 })
188 })
189 if result != turnStarted {
190 c.goalDriverMu.Lock()
191 if c.goalDriverActive == reservation {
192 c.goalDriverActive = nil
193 }
194 c.goalDriverMu.Unlock()
195 }
196 return result == turnStarted
197 }
198
199 func (c *Controller) recordGoalLifecycleUsage(e event.Event) {
200 if c == nil || !c.sessionEngineEnabled() || e.Usage == nil {
201 return
202 }
203 c.goalDriverMu.Lock()
204 reservation := c.goalDriverActive
205 c.goalDriverMu.Unlock()
206 if reservation == nil {
207 return
208 }
209 if _, admitted := reservation.admittedView(); !admitted {
210 return
211 }
212 c.goalResourceMu.Lock()
213 c.goalTokensUsed += usageTotalTokens(e.Usage)
214 c.goalRequestsUsed += e.Usage.RequestCount
215 c.goalResourceMu.Unlock()
216 }
217
218 func (c *Controller) goalResourceBudget() (used, limit int, exhausted bool) {
219 c.goalResourceMu.Lock()
220 defer c.goalResourceMu.Unlock()
221 return c.goalTokensUsed, c.goalTokenLimit, c.goalTokenLimit > 0 && c.goalTokensUsed >= c.goalTokenLimit
222 }
223
224 func (c *Controller) resetGoalResourceBudget() {
225 c.goalResourceMu.Lock()
226 c.goalTokensUsed = 0
227 c.goalRequestsUsed = 0
228 c.goalTokenLimit = c.goalTokenBudget
229 c.goalBudgetExtensions = 0
230 c.goalResourceMu.Unlock()
231 }
232
233 func (c *Controller) goalRoundEligibility() (*goaldomain.View, *session.Runtime, session.RuntimeSnapshot, bool) {
234 if c == nil || c.PendingPrompt() || c.hasPendingUserWork() {
235 return nil, nil, session.RuntimeSnapshot{}, false
236 }
237 c.mu.Lock()
238 busy := c.bodyActiveLocked() || c.finalizingLocked() || c.rotating || c.cancelRequestedLocked() || c.closed
239 c.mu.Unlock()
240 if busy {
241 return nil, nil, session.RuntimeSnapshot{}, false
242 }
243 view, err := c.goalLifecycleView()
244 if err != nil || view == nil || view.Phase != goaldomain.PhaseActive || view.Activation != goaldomain.ActivationArmed {
245 return nil, nil, session.RuntimeSnapshot{}, false
246 }
247 _, runtime, exclusive := c.v3Binding()
248 if !exclusive || runtime == nil {
249 return nil, nil, session.RuntimeSnapshot{}, false
250 }
251 snapshot := runtime.StateSnapshot()
252 if snapshot.Phase != session.RuntimeIdle {
253 return nil, nil, session.RuntimeSnapshot{}, false
254 }
255 return view, runtime, snapshot, true
256 }
257
258 // commitGoalRoundAdmission persists the turn/start fact and incremented goal
259 // snapshot in the same logical v3 batch. Only after Append is accepted does it
260 // publish the candidate machine and expose goal-round tool authority.
261 func (c *Controller) commitGoalRoundAdmission(reservation *goalRoundReservation) error {
262 if reservation == nil {
263 return errors.New("missing goal round reservation")
264 }
265 c.goalLifecycleMutationMu.Lock()
266 defer c.goalLifecycleMutationMu.Unlock()
267 _, runtime, exclusive := c.v3Binding()
268 if !exclusive || runtime == nil {
269 return session.ErrSessionNotRunning
270 }
271 runtimeSnapshot := runtime.StateSnapshot()
272 if runtimeSnapshot.Phase != session.RuntimeRunning || runtimeSnapshot.Ref.SessionID != reservation.SessionID ||
273 runtimeSnapshot.Epoch != reservation.RuntimeEpoch || runtimeSnapshot.ActivityRevision != reservation.IdleActivityRevision+1 {
274 return &goaldomain.Error{Code: goaldomain.ErrStaleRevision, Message: "goal round runtime reservation is stale"}
275 }
276 c.goalLifecycleMu.RLock()
277 machine, loadErr := c.goalLifecycle, c.goalLifecycleLoadErr
278 c.goalLifecycleMu.RUnlock()
279 if loadErr != nil {
280 return loadErr
281 }
282 if machine == nil {
283 return session.ErrSessionNotRunning
284 }
285 candidate := machine.Clone()
286 view, err := candidate.AdmitRound(reservation.Goal)
287 if err != nil {
288 return err
289 }
290 if view.RoundsStarted != reservation.Round {
291 return &goaldomain.Error{Code: goaldomain.ErrStaleRevision, Message: "goal round number changed before admission"}
292 }
293 payload, err := candidate.Encode()
294 if err != nil {
295 return err
296 }
297 if err := c.emitTurnEventChecked(event.Event{
298 Kind: event.TurnStarted, Status: event.TurnInProgress,
299 DomainKind: "goal/state", DomainPayload: payload,
300 }); err != nil {
301 return err
302 }
303 c.goalLifecycleMu.Lock()
304 if c.goalLifecycle != machine || c.goalLifecycleLoadErr != nil {
305 c.goalLifecycleMu.Unlock()
306 return &goaldomain.Error{Code: goaldomain.ErrStaleRevision, Message: "goal lifecycle changed during round admission"}
307 }
308 c.goalLifecycle = candidate
309 c.goalLifecycleMu.Unlock()
310 reservation.setAdmitted(view)
311 c.refreshRuntimeState(event.Event{})
312 return nil
313 }
314
315 func (c *Controller) finishGoalRoundActivity(reservation *goalRoundReservation) {
316 if reservation == nil {
317 return
318 }
319 c.goalDriverMu.Lock()
320 if c.goalDriverActive == reservation {
321 c.goalDriverActive = nil
322 }
323 c.goalDriverMu.Unlock()
324 if _, admitted := reservation.admittedView(); !admitted {
325 return
326 }
327 runErr, cancelled := reservation.result()
328 if cancelled || errors.Is(runErr, context.Canceled) {
329 current, viewErr := c.goalLifecycleView()
330 if viewErr != nil || current == nil || current.ID != reservation.Goal.ID || current.Phase != goaldomain.PhaseActive {
331 // A terminal goal action accepted before cancellation already owns the
332 // outcome. Do not rewrite complete/blocked into a cancellation pause.
333 return
334 }
335 _, err := c.applyHostGoalMutation(context.Background(), "cancelled-goal-round", func(machine *goaldomain.Machine) (*goaldomain.View, error) {
336 paused, pauseErr := machine.Pause(current.Ref())
337 return &paused, pauseErr
338 })
339 if err != nil {
340 c.disarmGoalLifecycle("cancelled")
341 }
342 return
343 }
344 if runErr != nil {
345 c.disarmGoalLifecycle("model-error")
346 }
347 }
348
349 func (c *Controller) goalAuthorityForRound(reservation *goalRoundReservation) (tool.GoalAuthority, bool) {
350 view, ok := reservation.admittedView()
351 if !ok {
352 return tool.GoalAuthority{}, false
353 }
354 _, runtime, exclusive := c.v3Binding()
355 if !exclusive || runtime == nil {
356 return tool.GoalAuthority{}, false
357 }
358 snapshot := runtime.StateSnapshot()
359 if snapshot.Phase != session.RuntimeRunning || snapshot.Ref.SessionID != reservation.SessionID || snapshot.Epoch != reservation.RuntimeEpoch {
360 return tool.GoalAuthority{}, false
361 }
362 return tool.GoalAuthority{Source: tool.GoalSourceGoalRound, SessionID: reservation.SessionID,
363 RuntimeEpoch: reservation.RuntimeEpoch, ActivityID: snapshot.ActivityRevision,
364 GoalID: view.ID, Revision: view.Revision, Round: view.RoundsStarted}, true
365 }
366
367 func (c *Controller) directHumanGoalAuthority() (tool.GoalAuthority, bool) {
368 _, runtime, exclusive := c.v3Binding()
369 if !exclusive || runtime == nil {
370 return tool.GoalAuthority{}, false
371 }
372 snapshot := runtime.StateSnapshot()
373 if snapshot.Phase != session.RuntimeRunning {
374 return tool.GoalAuthority{}, false
375 }
376 return tool.GoalAuthority{Source: tool.GoalSourceDirectHuman, SessionID: snapshot.Ref.SessionID,
377 RuntimeEpoch: snapshot.Epoch, ActivityID: snapshot.ActivityRevision}, true
378 }
379
380 func (c *Controller) disarmGoalLifecycle(reason string) {
381 c.goalLifecycleMutationMu.Lock()
382 defer c.goalLifecycleMutationMu.Unlock()
383 c.goalLifecycleMu.RLock()
384 machine := c.goalLifecycle
385 c.goalLifecycleMu.RUnlock()
386 if machine != nil {
387 machine.Disarm(strings.TrimSpace(reason))
388 c.refreshRuntimeState(event.Event{})
389 }
390 }
391
392 // applyHostGoalMutation is the serialized UI/command control plane. Idle Goal
393 // state writes use the current session write lease; they never create a fake
394 // control activity.
395 func (c *Controller) applyHostGoalMutation(ctx context.Context, reason string, mutate func(*goaldomain.Machine) (*goaldomain.View, error)) (*goaldomain.View, error) {
396 if c == nil || mutate == nil {
397 return nil, session.ErrSessionNotRunning
398 }
399 c.goalLifecycleMutationMu.Lock()
400 defer c.goalLifecycleMutationMu.Unlock()
401 c.goalLifecycleMu.RLock()
402 machine, loadErr := c.goalLifecycle, c.goalLifecycleLoadErr
403 c.goalLifecycleMu.RUnlock()
404 if loadErr != nil {
405 return nil, loadErr
406 }
407 if machine == nil {
408 return nil, session.ErrSessionNotRunning
409 }
410 candidate := machine.Clone()
411 view, err := mutate(candidate)
412 if err != nil {
413 return nil, err
414 }
415 payload, err := candidate.Encode()
416 if err != nil {
417 return nil, err
418 }
419 _, runtime, exclusive := c.v3Binding()
420 if !exclusive || runtime == nil {
421 return nil, session.ErrSessionNotRunning
422 }
423 snapshot := runtime.StateSnapshot()
424 switch snapshot.Phase {
425 case session.RuntimeIdle, session.RuntimeRunning, session.RuntimeCancelling, session.RuntimeFinalizing:
426 case session.RuntimeRecoveryRequired:
427 return nil, session.ErrRecoveryRequired
428 default:
429 return nil, session.ErrRuntimeBusy
430 }
431 execution := runtime.ExecutionSnapshot()
432 op := fmt.Sprintf("goal-control:%s:%d:%s", reason, execution.Session.EventSequence+1, snapshot.Epoch)
433 if _, err := runtime.Session().Append(context.Background(), session.Batch{OperationID: op, TurnID: execution.Session.Projection.TurnID,
434 Events: []session.Event{{Kind: "goal/state", Payload: payload}}}); err != nil {
435 return nil, err
436 }
437 _, currentRuntime, stillExclusive := c.v3Binding()
438 if !stillExclusive || currentRuntime != runtime {
439 return view, nil
440 }
441 c.goalLifecycleMu.Lock()
442 if c.goalLifecycle == machine && c.goalLifecycleLoadErr == nil {
443 c.goalLifecycle = candidate
444 }
445 c.goalLifecycleMu.Unlock()
446 c.refreshRuntimeState(event.Event{})
447 return view, nil
448 }
449
449 lines GO