Skip to main content

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}