vct_core/models/copilot.rs
1//! Serde models for the GitHub Copilot CLI `events.jsonl` session format.
2//!
3//! Current Copilot CLI writes `~/.copilot/session-state/<sessionId>/events.jsonl`
4//! where every line is a single event carrying its own `type` discriminator.
5//! The analyzer walks these events in order and pulls session metadata from
6//! `session.start`, model switches from `session.model_change`, tool calls
7//! from the `tool.execution_start` / `tool.execution_complete` pair, and
8//! authoritative per-model token usage from `session.shutdown.modelMetrics`.
9//!
10//! Earlier Copilot CLI releases wrote a single pretty-printed JSON object
11//! under `~/.copilot/history-session-state/<sessionId>.json` with no token
12//! accounting at all; that layout is no longer supported.
13
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use std::collections::BTreeMap;
17
18/// Single line of the Copilot `events.jsonl` stream.
19///
20/// `data` intentionally stays as raw [`Value`] — every event type has a
21/// different payload, so the analyzer branches on `event_type` and then
22/// deserialises `data` into the concrete shape on demand.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct CopilotEvent {
26 /// Event discriminator (e.g. `session.start`, `tool.execution_complete`).
27 #[serde(rename = "type", default)]
28 pub event_type: String,
29 /// Raw event payload, deserialized on demand based on `event_type`.
30 #[serde(default)]
31 pub data: Value,
32 /// Event identifier.
33 #[serde(default)]
34 pub id: String,
35 /// ISO-8601 timestamp string for the event.
36 #[serde(default)]
37 pub timestamp: String,
38 /// Identifier of the parent event, when the event is nested.
39 #[serde(default)]
40 pub parent_id: Option<String>,
41}
42
43/// `session.start` payload — session-scoped identifiers and workspace context.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct CopilotSessionStartData {
47 /// Session identifier.
48 #[serde(default)]
49 pub session_id: String,
50 /// Copilot CLI version that produced the session.
51 #[serde(default)]
52 pub copilot_version: String,
53 /// ISO-8601 session start time.
54 #[serde(default)]
55 pub start_time: String,
56 /// Workspace context, when the session ran inside a project.
57 #[serde(default)]
58 pub context: Option<CopilotSessionContext>,
59}
60
61/// Workspace context recorded at the start of a Copilot CLI session.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct CopilotSessionContext {
65 /// Working directory the session ran in.
66 #[serde(default)]
67 pub cwd: String,
68 /// Absolute path of the git repository root, when inside a repo.
69 #[serde(default)]
70 pub git_root: String,
71 /// Current git branch name.
72 #[serde(default)]
73 pub branch: String,
74 /// Commit hash checked out at session start.
75 #[serde(default)]
76 pub head_commit: String,
77 /// E.g. `"Mai0313/VibeCodingTracker"` — empty when the session did not
78 /// run inside a git repo.
79 #[serde(default)]
80 pub repository: String,
81 /// E.g. `"github"`; used together with `repository_host` to reconstruct
82 /// the remote URL when the CLI omits the full URL.
83 #[serde(default)]
84 pub host_type: String,
85 /// E.g. `"github.com"`.
86 #[serde(default)]
87 pub repository_host: String,
88}
89
90/// `session.model_change` payload — each session may switch between models
91/// at any point, so the analyzer uses the most recent one when attributing
92/// streaming `assistant.message` tokens that arrive before `session.shutdown`.
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct CopilotModelChangeData {
96 /// Model the session switched to.
97 #[serde(default)]
98 pub new_model: String,
99}
100
101/// `session.shutdown` payload — authoritative per-model token usage.
102///
103/// The map key is the model name (e.g. `"claude-sonnet-4.6"`). Copilot CLI
104/// writes this event on graceful shutdown after totalling up every API
105/// interaction in the session, which is the only place that carries *input*
106/// tokens (individual `assistant.message` events only carry `outputTokens`).
107#[derive(Debug, Clone, Serialize, Deserialize)]
108#[serde(rename_all = "camelCase")]
109pub struct CopilotShutdownData {
110 /// Per-model token metrics, keyed by model name.
111 #[serde(default)]
112 pub model_metrics: BTreeMap<String, CopilotModelMetric>,
113 /// Model that was active when the session shut down.
114 #[serde(default)]
115 pub current_model: String,
116}
117
118/// Per-model block inside [`CopilotShutdownData::model_metrics`].
119#[derive(Debug, Clone, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct CopilotModelMetric {
122 /// Token counts for the model, when present.
123 #[serde(default)]
124 pub usage: Option<CopilotModelUsage>,
125}
126
127/// Token counts captured by Copilot CLI at session shutdown.
128///
129/// Field names mirror the camelCase keys the CLI writes to disk — the
130/// usage_processor normalises these into the Claude-style field names
131/// (`input_tokens`, `cache_read_input_tokens`, …) before storing them in
132/// `conversation_usage`.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct CopilotModelUsage {
136 /// Input (prompt) tokens.
137 #[serde(default)]
138 pub input_tokens: i64,
139 /// Output (completion) tokens.
140 #[serde(default)]
141 pub output_tokens: i64,
142 /// Tokens served from the prompt cache.
143 #[serde(default)]
144 pub cache_read_tokens: i64,
145 /// Tokens written into the prompt cache.
146 #[serde(default)]
147 pub cache_write_tokens: i64,
148 /// Reasoning tokens billed separately by the model.
149 #[serde(default)]
150 pub reasoning_tokens: i64,
151}
152
153/// `tool.execution_start` payload — paired with `tool.execution_complete`
154/// via `tool_call_id`.
155#[derive(Debug, Clone, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct CopilotToolStartData {
158 /// Correlation id paired with the matching `tool.execution_complete`.
159 #[serde(default)]
160 pub tool_call_id: String,
161 /// Name of the invoked tool (e.g. `view`, `create`).
162 #[serde(default)]
163 pub tool_name: String,
164 /// Raw tool arguments.
165 #[serde(default)]
166 pub arguments: Value,
167}
168
169/// `tool.execution_complete` payload — carries the tool's actual output
170/// (file contents on `view`, creation confirmation on `create`, …) under
171/// `result`, plus the model that invoked the tool.
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(rename_all = "camelCase")]
174pub struct CopilotToolCompleteData {
175 /// Correlation id matching the originating `tool.execution_start`.
176 #[serde(default)]
177 pub tool_call_id: String,
178 /// Whether the tool call succeeded.
179 #[serde(default)]
180 pub success: bool,
181 /// Tool output (file contents, confirmation, …).
182 #[serde(default)]
183 pub result: Value,
184 /// Model that invoked the tool.
185 #[serde(default)]
186 pub model: String,
187}