| 1 | package main |
| 2 | |
| 3 | // phaseTrace is the per-task privacy-safe latency trace: only counts and |
| 4 | // milliseconds, no prompts, arguments, or outputs. It stitches the harness |
| 5 | // clock (total, solved, tokens) to the trajectory digest's phase buckets. |
| 6 | type phaseTrace struct { |
| 7 | TotalMs int64 `json:"total_ms"` |
| 8 | TTFTMs int64 `json:"ttft_ms,omitempty"` |
| 9 | TimeToFirstTool int64 `json:"time_to_first_tool_ms,omitempty"` |
| 10 | |
| 11 | Planner phaseModel `json:"planner"` |
| 12 | Executor phaseModel `json:"executor"` |
| 13 | Tool phaseTool `json:"tool"` |
| 14 | // Recovery counts extra requests forced by provider flakiness (stream and |
| 15 | // header retries, missing-reasoning replays, empty-final retries); Ms is |
| 16 | // the model-gap time of the rounds those events tainted. |
| 17 | Recovery phaseModel `json:"recovery"` |
| 18 | |
| 19 | CompactionMs int64 `json:"compaction_ms,omitempty"` |
| 20 | // NoProgressSignals counts progress-guard escalations (each threshold |
| 21 | // fires once), not raw zero-evidence-gain rounds. |
| 22 | NoProgressSignals int `json:"no_progress_signals,omitempty"` |
| 23 | // Rounds is the outcome ledger: total classified rounds, how many bought |
| 24 | // progress, the wasted gap total, and the per-outcome composition. |
| 25 | Rounds phaseRounds `json:"rounds"` |
| 26 | |
| 27 | PromptTokens int `json:"prompt_tokens"` |
| 28 | CompletionTokens int `json:"completion_tokens"` |
| 29 | Solved bool `json:"solved"` |
| 30 | } |
| 31 | |
| 32 | type phaseModel struct { |
| 33 | Requests int `json:"requests"` |
| 34 | Ms int64 `json:"ms"` |
| 35 | } |
| 36 | |
| 37 | type phaseTool struct { |
| 38 | Calls int `json:"calls"` |
| 39 | QueueMs int64 `json:"queue_ms"` |
| 40 | CriticalPathMs int64 `json:"critical_path_ms"` |
| 41 | } |
| 42 | |
| 43 | type phaseRounds struct { |
| 44 | Total int `json:"total"` |
| 45 | Useful int `json:"useful"` |
| 46 | WastedMs int64 `json:"wasted_ms"` |
| 47 | Outcomes map[string]int `json:"outcomes,omitempty"` |
| 48 | MsByKind map[string]int64 `json:"ms_by_outcome,omitempty"` |
| 49 | } |
| 50 | |
| 51 | func classifiedRounds(outcomes map[string]int) int { |
| 52 | total := 0 |
| 53 | for _, n := range outcomes { |
| 54 | total += n |
| 55 | } |
| 56 | return total |
| 57 | } |
| 58 | |
| 59 | // buildPhaseTrace derives the trace from a graded result; nil without a |
| 60 | // recorded trajectory (the digest carries every phase input). |
| 61 | func buildPhaseTrace(r result) *phaseTrace { |
| 62 | t := r.Trajectory |
| 63 | if t == nil { |
| 64 | return nil |
| 65 | } |
| 66 | return &phaseTrace{ |
| 67 | TotalMs: r.WallMs, |
| 68 | TTFTMs: t.TTFTMs, |
| 69 | TimeToFirstTool: t.FirstToolMs, |
| 70 | Planner: phaseModel{Requests: t.PlannerRequests, Ms: t.PlannerStreamMs}, |
| 71 | Executor: phaseModel{Requests: t.ExecutorRequests, Ms: t.ModelStreamMs}, |
| 72 | Tool: phaseTool{ |
| 73 | Calls: t.TopLevelCalls, |
| 74 | QueueMs: t.ToolQueueMs, |
| 75 | CriticalPathMs: t.toolWall(), |
| 76 | }, |
| 77 | Recovery: phaseModel{ |
| 78 | Requests: t.StreamRetries + t.HeaderRetries + t.ReasoningReplays + t.EmptyFinalRetries, |
| 79 | Ms: t.RecoveryGapMs, |
| 80 | }, |
| 81 | CompactionMs: t.CompactionMs, |
| 82 | NoProgressSignals: t.NoProgressSignals, |
| 83 | Rounds: phaseRounds{ |
| 84 | Total: classifiedRounds(t.RoundOutcomes), |
| 85 | Useful: t.UsefulRounds, |
| 86 | WastedMs: t.WastedGapMs, |
| 87 | Outcomes: t.RoundOutcomes, |
| 88 | MsByKind: t.RoundOutcomeMs, |
| 89 | }, |
| 90 | PromptTokens: r.PromptTokens, |
| 91 | CompletionTokens: r.CompletionTokens, |
| 92 | Solved: r.Passed, |
| 93 | } |
| 94 | } |
| 95 |