返回 DeepSeek-Reasonix
status.go
根目录 / internal / acp / status.go
1 package acp
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "math"
8 "runtime"
9 "strings"
10 "sync"
11
12 "reasonix/internal/agent"
13 "reasonix/internal/control"
14 "reasonix/internal/event"
15 "reasonix/internal/provider"
16 "reasonix/internal/secrets"
17 )
18
19 const (
20 reasonixStatusSchemaVersion = 1
21 sessionStatusMethod = "_reasonix.io/session/status"
22 sessionStatusUpdateMethod = "_reasonix.io/session/status_update"
23 )
24
25 // ReasonixSchemaCapability advertises one versioned vendor extension in
26 // agentCapabilities._meta. The method name is the map key so clients can fail
27 // closed before opening a session.
28 type ReasonixSchemaCapability struct {
29 SchemaVersion int `json:"schemaVersion"`
30 }
31
32 // SessionStatusParams addresses one live ACP session.
33 type SessionStatusParams struct {
34 SessionID string `json:"sessionId"`
35 }
36
37 // SessionSandboxState is the effective sandbox, after all CLI hard overrides
38 // have been applied. It deliberately reports no credentials or environment.
39 type SessionSandboxState struct {
40 Mode string `json:"mode"`
41 Engine string `json:"engine"`
42 Available bool `json:"available"`
43 WorkspaceRoot string `json:"workspaceRoot"`
44 WriteRoots []string `json:"writeRoots"`
45 NetworkEnabled bool `json:"networkEnabled"`
46 }
47
48 // SessionRuntimeState is supplied by the composition root because only it can
49 // truthfully report config-derived sandbox and planner state.
50 type SessionRuntimeState struct {
51 PlannerMode string `json:"plannerMode"`
52 Sandbox SessionSandboxState `json:"sandbox"`
53 }
54
55 // SessionRuntimeStateParams identifies the exact controller configuration whose
56 // effective policy is being reported. Model/profile switches rebuild the
57 // controller, so status must be recomputed from the same resolved inputs.
58 type SessionRuntimeStateParams struct {
59 Cwd string
60 Model string
61 RuntimeProfile string
62 }
63
64 // SessionRuntimeStateProvider exposes effective process/session policy without
65 // coupling the ACP adapter to Reasonix configuration internals.
66 type SessionRuntimeStateProvider interface {
67 SessionRuntimeState(ctx context.Context, p SessionRuntimeStateParams) (SessionRuntimeState, error)
68 }
69
70 type ReasonixStatusGoal struct {
71 Status string `json:"status"`
72 Objective string `json:"objective,omitempty"`
73 // Runtime is the optional Goal budget/runtime summary; absent for old
74 // hosts or when no goal is active.
75 Runtime *ReasonixGoalRuntime `json:"runtime,omitempty"`
76 }
77
78 type ReasonixGoalRuntime struct {
79 TurnsUsed int `json:"turnsUsed"`
80 TurnsLimit int `json:"turnsLimit"`
81 TokensUsed int `json:"tokensUsed"`
82 TokensLimit int `json:"tokensLimit"` // Deprecated: always 0; retained for protocol compatibility.
83 NoProgressTurns int `json:"noProgressTurns"`
84 NoProgressLimit int `json:"noProgressLimit"`
85 LastReason string `json:"lastReason,omitempty"`
86 StopCause string `json:"stopCause,omitempty"`
87 BudgetExtensions int `json:"budgetExtensions"`
88 }
89
90 type ReasonixTurnOutcome struct {
91 Kind string `json:"kind"`
92 Reason string `json:"reason,omitempty"`
93 }
94
95 type ReasonixFinalReadiness struct {
96 ReadyForReview bool `json:"readyForReview"`
97 Summary string `json:"summary"`
98 Risks []string `json:"risks"`
99 }
100
101 type ReasonixUsage struct {
102 PromptTokens int `json:"promptTokens"`
103 CompletionTokens int `json:"completionTokens"`
104 ReasoningTokens int `json:"reasoningTokens"`
105 CacheHitTokens int `json:"cacheHitTokens"`
106 CacheMissTokens int `json:"cacheMissTokens"`
107 Estimated bool `json:"estimated,omitempty"`
108 CacheHitRatio *float64 `json:"cacheHitRatio"`
109 EstimatedCost *float64 `json:"estimatedCost"`
110 Currency *string `json:"currency"`
111 UsageSource string `json:"usageSource"`
112 }
113
114 type ReasonixStatusUsage struct {
115 Turn ReasonixUsage `json:"turn"`
116 Cumulative ReasonixUsage `json:"cumulative"`
117 }
118
119 // ReasonixSessionStatus is the stable schemaVersion=1 recovery snapshot.
120 // Reasoning text and unbounded terminal output are intentionally absent.
121 type ReasonixSessionStatus struct {
122 SchemaVersion int `json:"schemaVersion"`
123 Sequence uint64 `json:"sequence"`
124 SessionID string `json:"sessionId"`
125 State string `json:"state"`
126 Model string `json:"model"`
127 Effort string `json:"effort"`
128 Mode string `json:"mode"`
129 WorkMode string `json:"workMode"`
130 PlannerMode string `json:"plannerMode"`
131 Goal ReasonixStatusGoal `json:"goal"`
132 Phase string `json:"phase"`
133 TurnOutcome ReasonixTurnOutcome `json:"turnOutcome"`
134 FinalReadiness ReasonixFinalReadiness `json:"finalReadiness"`
135 Sandbox SessionSandboxState `json:"sandbox"`
136 Usage ReasonixStatusUsage `json:"usage"`
137 }
138
139 type ReasonixStatusUpdate struct {
140 SchemaVersion int `json:"schemaVersion"`
141 Sequence uint64 `json:"sequence"`
142 SessionID string `json:"sessionId"`
143 Event string `json:"event"`
144 Status ReasonixSessionStatus `json:"status"`
145 }
146
147 type usageAccumulator struct {
148 promptTokens int
149 completionTokens int
150 reasoningTokens int
151 cacheHitTokens int
152 cacheMissTokens int
153 estimated bool
154 events int
155 pricedEvents int
156 estimatedCost float64
157 currency string
158 source string
159 }
160
161 func (a *usageAccumulator) add(u *provider.Usage, pricing *provider.Pricing, source string) {
162 if u == nil {
163 return
164 }
165 a.promptTokens += u.PromptTokens
166 a.completionTokens += u.CompletionTokens
167 a.reasoningTokens += u.ReasoningTokens
168 a.cacheHitTokens += u.CacheHitTokens
169 a.cacheMissTokens += u.CacheMissTokens
170 a.estimated = a.estimated || u.Estimated
171 a.events++
172 source = strings.TrimSpace(source)
173 if source == "" {
174 source = event.UsageSourceExecutor
175 }
176 if a.source == "" {
177 a.source = source
178 } else if a.source != source {
179 a.source = "mixed"
180 }
181 if pricing != nil {
182 currency := strings.TrimSpace(pricing.Currency)
183 if currency == "" {
184 currency = pricing.Symbol()
185 }
186 if a.pricedEvents == 0 {
187 a.currency = currency
188 } else if a.currency != currency {
189 a.currency = ""
190 }
191 a.estimatedCost += pricing.Cost(u)
192 a.pricedEvents++
193 }
194 }
195
196 func (a usageAccumulator) wire() ReasonixUsage {
197 usage := ReasonixUsage{
198 PromptTokens: a.promptTokens,
199 CompletionTokens: a.completionTokens,
200 ReasoningTokens: a.reasoningTokens,
201 CacheHitTokens: a.cacheHitTokens,
202 CacheMissTokens: a.cacheMissTokens,
203 Estimated: a.estimated,
204 UsageSource: a.source,
205 }
206 if usage.UsageSource == "" {
207 usage.UsageSource = event.UsageSourceExecutor
208 }
209 if total := a.cacheHitTokens + a.cacheMissTokens; total > 0 {
210 ratio := float64(a.cacheHitTokens) / float64(total)
211 usage.CacheHitRatio = &ratio
212 }
213 if a.events > 0 && a.pricedEvents == a.events && a.currency != "" && !math.IsNaN(a.estimatedCost) && !math.IsInf(a.estimatedCost, 0) {
214 cost := a.estimatedCost
215 currency := a.currency
216 usage.EstimatedCost = &cost
217 usage.Currency = &currency
218 }
219 return usage
220 }
221
222 type statusTelemetry struct {
223 mu sync.Mutex
224 sequence uint64
225 state string
226 phase string
227 turnOutcome ReasonixTurnOutcome
228 finalReadiness ReasonixFinalReadiness
229 turnUsage usageAccumulator
230 cumulative usageAccumulator
231 goalOverride string
232 }
233
234 func newStatusTelemetry() *statusTelemetry {
235 return &statusTelemetry{
236 state: "idle",
237 phase: "idle",
238 turnOutcome: ReasonixTurnOutcome{Kind: "none"},
239 finalReadiness: ReasonixFinalReadiness{
240 Risks: []string{},
241 },
242 }
243 }
244
245 func (t *statusTelemetry) mutate(fn func(*statusTelemetry)) uint64 {
246 t.mu.Lock()
247 defer t.mu.Unlock()
248 fn(t)
249 t.sequence++
250 return t.sequence
251 }
252
253 func (t *statusTelemetry) beginTurn() {
254 t.mutate(func(t *statusTelemetry) {
255 t.state = "running"
256 t.phase = "starting"
257 t.turnOutcome = ReasonixTurnOutcome{Kind: "none"}
258 t.finalReadiness = ReasonixFinalReadiness{Risks: []string{}}
259 t.turnUsage = usageAccumulator{}
260 t.goalOverride = ""
261 })
262 }
263
264 func (t *statusTelemetry) onEvent(e event.Event) (string, bool) {
265 switch e.Kind {
266 case event.Phase:
267 t.mutate(func(t *statusTelemetry) {
268 t.phase = normalizeStatusPhase(e)
269 })
270 return "phase", true
271 case event.Usage:
272 t.mutate(func(t *statusTelemetry) {
273 t.turnUsage.add(e.Usage, e.Pricing, e.UsageSource)
274 t.cumulative.add(e.Usage, e.Pricing, e.UsageSource)
275 })
276 return "usage", true
277 case event.ApprovalRequest:
278 t.mutate(func(t *statusTelemetry) { t.phase = "waiting_permission" })
279 return "phase", true
280 case event.AskRequest:
281 t.mutate(func(t *statusTelemetry) { t.phase = "waiting_input" })
282 return "phase", true
283 case event.ToolDispatch:
284 t.mutate(func(t *statusTelemetry) { t.phase = "implementing" })
285 case event.Notice:
286 if e.Code == event.NoticeCodeFinalReadiness {
287 t.mutate(func(t *statusTelemetry) { t.phase = "checking_readiness" })
288 return "phase", true
289 }
290 }
291 return "", false
292 }
293
294 func (t *statusTelemetry) finishTurn(runErr error, cancelled bool, goalStatus, summary string) string {
295 eventName := "completion"
296 t.mutate(func(t *statusTelemetry) {
297 t.state = "idle"
298 t.finalReadiness.Summary = clipStatusText(summary, 16_384)
299 t.finalReadiness.Risks = []string{}
300 t.goalOverride = ""
301 switch {
302 case cancelled:
303 t.phase = "cancelled"
304 t.turnOutcome = ReasonixTurnOutcome{Kind: "cancelled"}
305 t.goalOverride = "cancelled"
306 eventName = "completion"
307 case runErr == nil && goalStatus == control.GoalStatusComplete:
308 t.phase = "review_ready"
309 t.turnOutcome = ReasonixTurnOutcome{Kind: "completed"}
310 t.finalReadiness.ReadyForReview = true
311 case runErr == nil && (goalStatus == "" || goalStatus == control.GoalStatusStopped):
312 t.phase = "completed"
313 t.turnOutcome = ReasonixTurnOutcome{Kind: "completed"}
314 t.finalReadiness.ReadyForReview = true
315 case goalStatus == control.GoalStatusBlocked:
316 t.phase = "paused"
317 t.turnOutcome = ReasonixTurnOutcome{Kind: "paused", Reason: "goal blocked"}
318 eventName = "pause"
319 default:
320 var readinessErr *agent.FinalReadinessError
321 var recoveryPause *agent.RecoveryPauseError
322 switch {
323 case errors.As(runErr, &readinessErr):
324 t.phase = "readiness_paused"
325 t.turnOutcome = ReasonixTurnOutcome{Kind: "paused", Reason: clipStatusText(readinessErr.Error(), 2_048)}
326 t.finalReadiness.Risks = redactStatusTexts(readinessErr.Missing, 2_048)
327 eventName = "pause"
328 case errors.As(runErr, &recoveryPause):
329 t.phase = "recovery_paused"
330 t.turnOutcome = ReasonixTurnOutcome{Kind: "paused", Reason: clipStatusText(recoveryPause.Error(), 2_048)}
331 eventName = "pause"
332 case runErr != nil:
333 t.phase = "error"
334 t.turnOutcome = ReasonixTurnOutcome{Kind: "error", Reason: clipStatusText(runErr.Error(), 2_048)}
335 t.goalOverride = "failed"
336 eventName = "error"
337 default:
338 t.phase = "paused"
339 t.turnOutcome = ReasonixTurnOutcome{Kind: "paused", Reason: "goal is not complete"}
340 eventName = "pause"
341 }
342 }
343 })
344 return eventName
345 }
346
347 type statusTelemetrySnapshot struct {
348 sequence uint64
349 state string
350 phase string
351 turnOutcome ReasonixTurnOutcome
352 finalReadiness ReasonixFinalReadiness
353 turnUsage ReasonixUsage
354 cumulative ReasonixUsage
355 goalOverride string
356 }
357
358 type persistedUsageAccumulator struct {
359 PromptTokens int `json:"promptTokens"`
360 CompletionTokens int `json:"completionTokens"`
361 ReasoningTokens int `json:"reasoningTokens"`
362 CacheHitTokens int `json:"cacheHitTokens"`
363 CacheMissTokens int `json:"cacheMissTokens"`
364 Estimated bool `json:"estimated,omitempty"`
365 Events int `json:"events"`
366 PricedEvents int `json:"pricedEvents"`
367 EstimatedCost float64 `json:"estimatedCost"`
368 Currency string `json:"currency,omitempty"`
369 Source string `json:"source,omitempty"`
370 }
371
372 type persistedStatusTelemetry struct {
373 Sequence uint64 `json:"sequence"`
374 State string `json:"state"`
375 Phase string `json:"phase"`
376 TurnOutcome ReasonixTurnOutcome `json:"turnOutcome"`
377 FinalReadiness ReasonixFinalReadiness `json:"finalReadiness"`
378 TurnUsage persistedUsageAccumulator `json:"turnUsage"`
379 Cumulative persistedUsageAccumulator `json:"cumulative"`
380 GoalOverride string `json:"goalOverride,omitempty"`
381 }
382
383 func persistUsage(a usageAccumulator) persistedUsageAccumulator {
384 return persistedUsageAccumulator{
385 PromptTokens: a.promptTokens, CompletionTokens: a.completionTokens,
386 ReasoningTokens: a.reasoningTokens, CacheHitTokens: a.cacheHitTokens,
387 CacheMissTokens: a.cacheMissTokens, Estimated: a.estimated, Events: a.events,
388 PricedEvents: a.pricedEvents, EstimatedCost: a.estimatedCost,
389 Currency: a.currency, Source: a.source,
390 }
391 }
392
393 func restoreUsage(a persistedUsageAccumulator) usageAccumulator {
394 return usageAccumulator{
395 promptTokens: a.PromptTokens, completionTokens: a.CompletionTokens,
396 reasoningTokens: a.ReasoningTokens, cacheHitTokens: a.CacheHitTokens,
397 cacheMissTokens: a.CacheMissTokens, estimated: a.Estimated, events: a.Events,
398 pricedEvents: a.PricedEvents, estimatedCost: a.EstimatedCost,
399 currency: a.Currency, source: a.Source,
400 }
401 }
402
403 func (t *statusTelemetry) persisted() *persistedStatusTelemetry {
404 if t == nil {
405 return nil
406 }
407 t.mu.Lock()
408 defer t.mu.Unlock()
409 return &persistedStatusTelemetry{
410 Sequence: t.sequence, State: t.state, Phase: t.phase,
411 TurnOutcome: t.turnOutcome,
412 FinalReadiness: ReasonixFinalReadiness{
413 ReadyForReview: t.finalReadiness.ReadyForReview,
414 Summary: clipStatusText(t.finalReadiness.Summary, 16_384),
415 Risks: redactStatusTexts(t.finalReadiness.Risks, 2_048),
416 },
417 TurnUsage: persistUsage(t.turnUsage), Cumulative: persistUsage(t.cumulative),
418 GoalOverride: t.goalOverride,
419 }
420 }
421
422 func restoreStatusTelemetry(saved *persistedStatusTelemetry) *statusTelemetry {
423 t := newStatusTelemetry()
424 if saved == nil {
425 return t
426 }
427 interrupted := saved.State == "running"
428 t.sequence = saved.Sequence
429 // A restored process never owns the turn that wrote a running snapshot.
430 // Publish a new, terminal recovery state instead of leaving supervisors
431 // waiting on work that no longer exists in this runtime.
432 t.state = "idle"
433 t.phase = normalizePersistedStatusPhase(saved.Phase)
434 t.turnOutcome = saved.TurnOutcome
435 if t.turnOutcome.Kind == "" {
436 t.turnOutcome.Kind = "none"
437 }
438 t.finalReadiness = ReasonixFinalReadiness{
439 ReadyForReview: saved.FinalReadiness.ReadyForReview,
440 Summary: clipStatusText(saved.FinalReadiness.Summary, 16_384),
441 Risks: redactStatusTexts(saved.FinalReadiness.Risks, 2_048),
442 }
443 t.turnUsage = restoreUsage(saved.TurnUsage)
444 t.cumulative = restoreUsage(saved.Cumulative)
445 t.goalOverride = saved.GoalOverride
446 if interrupted {
447 t.sequence++
448 t.phase = "recovery_paused"
449 t.turnOutcome = ReasonixTurnOutcome{Kind: "paused", Reason: "previous turn interrupted"}
450 t.finalReadiness.ReadyForReview = false
451 }
452 return t
453 }
454
455 func (t *statusTelemetry) snapshot() statusTelemetrySnapshot {
456 t.mu.Lock()
457 defer t.mu.Unlock()
458 return statusTelemetrySnapshot{
459 sequence: t.sequence,
460 state: t.state,
461 phase: t.phase,
462 turnOutcome: ReasonixTurnOutcome{
463 Kind: t.turnOutcome.Kind, Reason: clipStatusText(t.turnOutcome.Reason, 2_048),
464 },
465 finalReadiness: ReasonixFinalReadiness{
466 ReadyForReview: t.finalReadiness.ReadyForReview,
467 Summary: clipStatusText(t.finalReadiness.Summary, 16_384),
468 Risks: redactStatusTexts(t.finalReadiness.Risks, 2_048),
469 },
470 turnUsage: t.turnUsage.wire(),
471 cumulative: t.cumulative.wire(),
472 goalOverride: t.goalOverride,
473 }
474 }
475
476 func defaultSessionRuntimeState(cwd string) SessionRuntimeState {
477 engine := "bubblewrap"
478 if runtime.GOOS == "darwin" {
479 engine = "seatbelt"
480 }
481 return SessionRuntimeState{
482 PlannerMode: "on",
483 Sandbox: SessionSandboxState{
484 Mode: "enforce",
485 Engine: engine,
486 Available: true,
487 WorkspaceRoot: cwd,
488 WriteRoots: []string{cwd},
489 },
490 }
491 }
492
493 func normalizeStatusEffort(value *string) string {
494 if value == nil || strings.TrimSpace(*value) == "" {
495 return "auto"
496 }
497 return strings.TrimSpace(*value)
498 }
499
500 func normalizeGoalStatus(value string) string {
501 switch value {
502 case control.GoalStatusRunning, control.GoalStatusComplete, control.GoalStatusBlocked:
503 return value
504 default:
505 return "none"
506 }
507 }
508
509 func clipStatusText(value string, limit int) string {
510 value = strings.TrimSpace(secrets.Redact(value))
511 if len(value) <= limit {
512 return value
513 }
514 return value[:limit]
515 }
516
517 func redactStatusTexts(values []string, limit int) []string {
518 out := make([]string, 0, len(values))
519 for _, value := range values {
520 out = append(out, clipStatusText(value, limit))
521 }
522 return out
523 }
524
525 func normalizeStatusPhase(e event.Event) string {
526 switch strings.TrimSpace(e.Source) {
527 case event.UsageSourcePlanner:
528 return "planning"
529 case event.UsageSourceExecutor:
530 return "implementing"
531 }
532 lower := strings.ToLower(strings.TrimSpace(e.Text))
533 switch {
534 case strings.Contains(lower, "planning"):
535 return "planning"
536 case strings.Contains(lower, "executing"), strings.Contains(lower, "implementing"):
537 return "implementing"
538 default:
539 return "working"
540 }
541 }
542
543 func normalizePersistedStatusPhase(value string) string {
544 value = strings.TrimSpace(value)
545 switch value {
546 case "idle", "starting", "planning", "working", "implementing",
547 "waiting_permission", "waiting_input", "checking_readiness",
548 "cancelled", "review_ready", "completed", "paused",
549 "readiness_paused", "recovery_paused", "error":
550 return value
551 default:
552 return normalizeStatusPhase(event.Event{Kind: event.Phase, Text: value})
553 }
554 }
555
556 func (s *service) sessionRuntimeState(ctx context.Context, p SessionRuntimeStateParams) (SessionRuntimeState, error) {
557 if provider, ok := s.factory.(SessionRuntimeStateProvider); ok {
558 state, err := provider.SessionRuntimeState(ctx, p)
559 if err != nil {
560 return SessionRuntimeState{}, err
561 }
562 if strings.EqualFold(strings.TrimSpace(p.RuntimeProfile), "economy") {
563 state.PlannerMode = "off"
564 }
565 if strings.TrimSpace(state.PlannerMode) == "" {
566 state.PlannerMode = "on"
567 }
568 if state.Sandbox.WriteRoots == nil {
569 state.Sandbox.WriteRoots = []string{}
570 }
571 return state, nil
572 }
573 state := defaultSessionRuntimeState(p.Cwd)
574 if strings.EqualFold(strings.TrimSpace(p.RuntimeProfile), "economy") {
575 state.PlannerMode = "off"
576 }
577 return state, nil
578 }
579
580 func (s *service) bindStatusEvents(sess *acpSession) {
581 if sess == nil || sess.sink == nil {
582 return
583 }
584 if sess.status == nil {
585 sess.status = newStatusTelemetry()
586 }
587 sess.sink.bindStatus(func(e event.Event) {
588 eventName, publish := sess.status.onEvent(e)
589 if publish {
590 s.publishStatus(sess, eventName)
591 }
592 })
593 }
594
595 func (s *service) sessionStatus(_ context.Context, raw json.RawMessage) (any, error) {
596 var p SessionStatusParams
597 if err := json.Unmarshal(raw, &p); err != nil {
598 return nil, &RPCError{Code: ErrInvalidParams, Message: sessionStatusMethod + ": " + err.Error()}
599 }
600 sess := s.session(p.SessionID)
601 if sess == nil {
602 return nil, &RPCError{Code: ErrInvalidParams, Message: sessionStatusMethod + ": unknown session " + p.SessionID}
603 }
604 return sess.statusSnapshot(), nil
605 }
606
607 func (s *service) publishStatus(sess *acpSession, eventName string) {
608 if sess == nil {
609 return
610 }
611 status := sess.statusSnapshot()
612 _ = s.conn.Notify(sessionStatusUpdateMethod, ReasonixStatusUpdate{
613 SchemaVersion: reasonixStatusSchemaVersion,
614 Sequence: status.Sequence,
615 SessionID: status.SessionID,
616 Event: eventName,
617 Status: status,
618 })
619 }
620
621 func (s *acpSession) statusSnapshot() ReasonixSessionStatus {
622 s.mu.Lock()
623 id := s.id
624 ctrl := s.ctrl
625 model := s.model
626 effort := cloneStringPtr(s.effortOverride)
627 workMode := s.runtimeProfile
628 mode := s.modeID
629 runtimeState := s.runtimeState
630 telemetry := s.status
631 s.mu.Unlock()
632
633 if telemetry == nil {
634 telemetry = newStatusTelemetry()
635 }
636 t := telemetry.snapshot()
637 goalStatus := "none"
638 goalObjective := ""
639 var goalRuntime *ReasonixGoalRuntime
640 if ctrl != nil {
641 goalStatus = normalizeGoalStatus(ctrl.GoalStatus())
642 goalObjective = clipStatusText(ctrl.Goal(), 16_384)
643 if strings.TrimSpace(goalObjective) != "" {
644 rt := ctrl.GoalRuntime()
645 goalRuntime = &ReasonixGoalRuntime{
646 TurnsUsed: rt.TurnsUsed,
647 TurnsLimit: rt.TurnsLimit,
648 TokensUsed: rt.TokensUsed,
649 TokensLimit: rt.TokensLimit,
650 NoProgressTurns: rt.NoProgressTurns,
651 NoProgressLimit: rt.NoProgressLimit,
652 LastReason: rt.LastReason,
653 StopCause: rt.StopCause,
654 BudgetExtensions: rt.BudgetExtensions,
655 }
656 }
657 }
658 if t.goalOverride != "" {
659 goalStatus = t.goalOverride
660 }
661 mode = normalizeACPCollaborationMode(mode)
662 workMode = strings.ToLower(strings.TrimSpace(workMode))
663 switch workMode {
664 case "economy", "delivery":
665 default:
666 workMode = "balanced"
667 }
668 if runtimeState.PlannerMode != "off" {
669 runtimeState.PlannerMode = "on"
670 }
671 if runtimeState.Sandbox.WriteRoots == nil {
672 runtimeState.Sandbox.WriteRoots = []string{}
673 }
674 phase := strings.TrimSpace(t.phase)
675 if phase == "" {
676 phase = "idle"
677 }
678 state := t.state
679 if state != "running" {
680 state = "idle"
681 }
682 return ReasonixSessionStatus{
683 SchemaVersion: reasonixStatusSchemaVersion,
684 Sequence: t.sequence,
685 SessionID: id,
686 State: state,
687 Model: strings.TrimSpace(model),
688 Effort: normalizeStatusEffort(effort),
689 Mode: mode,
690 WorkMode: workMode,
691 PlannerMode: runtimeState.PlannerMode,
692 Goal: ReasonixStatusGoal{
693 Status: goalStatus,
694 Objective: goalObjective,
695 Runtime: goalRuntime,
696 },
697 Phase: phase,
698 TurnOutcome: t.turnOutcome,
699 FinalReadiness: t.finalReadiness,
700 Sandbox: runtimeState.Sandbox,
701 Usage: ReasonixStatusUsage{
702 Turn: t.turnUsage,
703 Cumulative: t.cumulative,
704 },
705 }
706 }
707
708 func finalAssistantSummary(ctrl acpController) string {
709 if ctrl == nil {
710 return ""
711 }
712 history := ctrl.History()
713 for i := len(history) - 1; i >= 0; i-- {
714 if history[i].Role == provider.RoleAssistant && strings.TrimSpace(history[i].Content) != "" {
715 return history[i].Content
716 }
717 }
718 return ""
719 }
720
720 lines GO