| 1 | package agent |
| 2 | |
| 3 | import ( |
| 4 | "time" |
| 5 | |
| 6 | "reasonix/internal/event" |
| 7 | ) |
| 8 | |
| 9 | type phaseClock struct { |
| 10 | last event.TurnPhaseName |
| 11 | at time.Time |
| 12 | } |
| 13 | |
| 14 | // emitTurnPhase publishes a content-free host phase for the active turn. |
| 15 | func (a *Agent) emitTurnPhase(phase event.TurnPhaseName) { |
| 16 | if a == nil || a.svc.sink == nil || phase == "" { |
| 17 | return |
| 18 | } |
| 19 | now := time.Now() |
| 20 | a.recordOpenPhase(now) |
| 21 | a.turn.phase = phaseClock{last: phase, at: now} |
| 22 | a.svc.sink.Emit(event.Event{Kind: event.TurnPhase, PhaseName: phase, Text: string(phase)}) |
| 23 | } |
| 24 | |
| 25 | // recordOpenPhase bills the phase that is ending. A span is accounted for only |
| 26 | // when something closes it, so a phase nobody follows records nothing. |
| 27 | func (a *Agent) recordOpenPhase(now time.Time) { |
| 28 | if a.turn.phase.at.IsZero() || a.capabilityAudit == nil { |
| 29 | return |
| 30 | } |
| 31 | a.capabilityAudit.RecordPhaseMs(phaseAuditName(a.turn.phase.last), now.Sub(a.turn.phase.at).Milliseconds()) |
| 32 | } |
| 33 | |
| 34 | // closeTurnPhase bills the open phase without opening another one, for the end |
| 35 | // of a turn: beginRunTurn resets a.turn, so an unclosed tail span is lost. |
| 36 | func (a *Agent) closeTurnPhase() { |
| 37 | if a == nil { |
| 38 | return |
| 39 | } |
| 40 | a.recordOpenPhase(time.Now()) |
| 41 | a.turn.phase = phaseClock{} |
| 42 | } |
| 43 | |
| 44 | func phaseAuditName(phase event.TurnPhaseName) string { |
| 45 | switch phase { |
| 46 | case event.TurnPhaseWorking: |
| 47 | return "provider" |
| 48 | case event.TurnPhaseChecking, event.TurnPhaseVerifying: |
| 49 | return "tool" |
| 50 | case event.TurnPhaseReviewing: |
| 51 | return "review" |
| 52 | default: |
| 53 | return "" |
| 54 | } |
| 55 | } |
| 56 |