mirror of
https://github.com/openai/codex.git
synced 2026-10-26 18:38:47 +00:00
rebase
This commit is contained in:
Generated
+1
@@ -1400,6 +1400,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-analytics",
|
||||
"codex-app-server-protocol",
|
||||
"codex-arg0",
|
||||
"codex-backend-client",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ClientResponse;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
@@ -29,6 +30,8 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use std::time::SystemTime;
|
||||
use std::time::UNIX_EPOCH;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -39,8 +42,10 @@ pub struct TrackEventsContext {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CodexTurnEvent {
|
||||
pub struct TurnResolvedConfigFact {
|
||||
pub turn_id: String,
|
||||
pub submission_type: Option<TurnSubmissionType>,
|
||||
pub model: String,
|
||||
pub model_provider: String,
|
||||
pub sandbox_policy: SandboxPolicy,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
@@ -51,27 +56,7 @@ pub struct CodexTurnEvent {
|
||||
pub sandbox_network_access: bool,
|
||||
pub collaboration_mode: ModeKind,
|
||||
pub personality: Option<Personality>,
|
||||
pub num_input_images: usize,
|
||||
pub is_first_turn: bool,
|
||||
pub status: Option<TurnStatus>,
|
||||
pub turn_error: Option<CodexErrorInfo>,
|
||||
pub steer_count: Option<usize>,
|
||||
pub total_tool_call_count: Option<usize>,
|
||||
pub shell_command_count: Option<usize>,
|
||||
pub file_change_count: Option<usize>,
|
||||
pub mcp_tool_call_count: Option<usize>,
|
||||
pub dynamic_tool_call_count: Option<usize>,
|
||||
pub subagent_tool_call_count: Option<usize>,
|
||||
pub web_search_count: Option<usize>,
|
||||
pub image_generation_count: Option<usize>,
|
||||
pub input_tokens: Option<i64>,
|
||||
pub cached_input_tokens: Option<i64>,
|
||||
pub output_tokens: Option<i64>,
|
||||
pub reasoning_output_tokens: Option<i64>,
|
||||
pub total_tokens: Option<i64>,
|
||||
pub duration_ms: Option<u64>,
|
||||
pub started_at: Option<u64>,
|
||||
pub completed_at: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
@@ -90,13 +75,14 @@ pub enum TurnStatus {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ThreadInitializedInput {
|
||||
struct ThreadInitializedInput {
|
||||
pub connection_id: u64,
|
||||
pub thread_id: String,
|
||||
pub model: String,
|
||||
pub ephemeral: bool,
|
||||
pub session_source: SessionSource,
|
||||
pub initialization_mode: InitializationMode,
|
||||
pub created_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
@@ -177,24 +163,21 @@ pub enum AnalyticsFact {
|
||||
Request {
|
||||
connection_id: u64,
|
||||
request_id: RequestId,
|
||||
request: ClientRequest,
|
||||
request: Box<ClientRequest>,
|
||||
},
|
||||
Notification {
|
||||
Response {
|
||||
connection_id: u64,
|
||||
notification: ServerNotification,
|
||||
response: Box<ClientResponse>,
|
||||
},
|
||||
Notification(Box<ServerNotification>),
|
||||
// Facts that do not naturally exist on the app-server protocol surface, or
|
||||
// would require non-trivial protocol reshaping on this branch.
|
||||
Custom(CustomAnalyticsFact),
|
||||
}
|
||||
|
||||
pub enum CustomAnalyticsFact {
|
||||
// This remains custom on this branch because app-server-protocol does not
|
||||
// yet expose a generic client response enum we can reduce over directly.
|
||||
ThreadInitialized(ThreadInitializedInput),
|
||||
TurnStarted(Box<TurnStartedInput>),
|
||||
TurnResolvedConfig(Box<TurnResolvedConfigFact>),
|
||||
TurnSteer(TurnSteerInput),
|
||||
TurnCompleted(TurnCompletedInput),
|
||||
SkillInvoked(SkillInvokedInput),
|
||||
AppMentioned(AppMentionedInput),
|
||||
AppUsed(AppUsedInput),
|
||||
@@ -202,20 +185,11 @@ pub enum CustomAnalyticsFact {
|
||||
PluginStateChanged(PluginStateChangedInput),
|
||||
}
|
||||
|
||||
pub struct TurnStartedInput {
|
||||
pub tracking: TrackEventsContext,
|
||||
pub turn_event: CodexTurnEvent,
|
||||
}
|
||||
|
||||
pub struct TurnSteerInput {
|
||||
pub tracking: TrackEventsContext,
|
||||
pub turn_steer: CodexTurnSteerEvent,
|
||||
}
|
||||
|
||||
pub struct TurnCompletedInput {
|
||||
pub turn_id: String,
|
||||
}
|
||||
|
||||
pub struct SkillInvokedInput {
|
||||
pub tracking: TrackEventsContext,
|
||||
pub invocations: Vec<SkillInvocation>,
|
||||
@@ -251,16 +225,42 @@ pub enum PluginState {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AnalyticsReducer {
|
||||
requests: HashMap<(u64, RequestId), RequestState>,
|
||||
turns: HashMap<String, TurnState>,
|
||||
clients: HashMap<u64, ClientState>,
|
||||
connections: HashMap<u64, ConnectionState>,
|
||||
}
|
||||
|
||||
struct ClientState {
|
||||
struct ConnectionState {
|
||||
product_client_id: String,
|
||||
client_name: Option<String>,
|
||||
client_version: Option<String>,
|
||||
experimental_api_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
enum RequestState {
|
||||
TurnStart(PendingTurnStartState),
|
||||
}
|
||||
|
||||
struct PendingTurnStartState {
|
||||
thread_id: String,
|
||||
num_input_images: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CompletedTurnState {
|
||||
status: Option<TurnStatus>,
|
||||
turn_error: Option<CodexErrorInfo>,
|
||||
completed_at_secs: u64,
|
||||
duration_ms: Option<u64>,
|
||||
}
|
||||
|
||||
struct TurnState {
|
||||
started_input: TurnStartedInput,
|
||||
connection_id: Option<u64>,
|
||||
thread_id: Option<String>,
|
||||
num_input_images: Option<usize>,
|
||||
resolved_config: Option<TurnResolvedConfigFact>,
|
||||
started_at_ms: Option<u64>,
|
||||
completed: Option<CompletedTurnState>,
|
||||
steer_count: usize,
|
||||
}
|
||||
|
||||
@@ -367,10 +367,12 @@ impl AnalyticsEventsClient {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn track_thread_initialized(&self, input: ThreadInitializedInput) {
|
||||
self.record_fact(AnalyticsFact::Custom(
|
||||
CustomAnalyticsFact::ThreadInitialized(input),
|
||||
));
|
||||
pub fn track_request(&self, connection_id: u64, request_id: RequestId, request: ClientRequest) {
|
||||
self.record_fact(AnalyticsFact::Request {
|
||||
connection_id,
|
||||
request_id,
|
||||
request: Box::new(request),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn track_app_mentioned(&self, tracking: TrackEventsContext, mentions: Vec<AppInvocation>) {
|
||||
@@ -400,13 +402,10 @@ impl AnalyticsEventsClient {
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn track_turn_started(&self, tracking: TrackEventsContext, turn_event: CodexTurnEvent) {
|
||||
self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::TurnStarted(
|
||||
Box::new(TurnStartedInput {
|
||||
tracking,
|
||||
turn_event,
|
||||
}),
|
||||
)));
|
||||
pub fn track_turn_resolved_config(&self, fact: TurnResolvedConfigFact) {
|
||||
self.record_fact(AnalyticsFact::Custom(
|
||||
CustomAnalyticsFact::TurnResolvedConfig(Box::new(fact)),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn track_turn_steer(&self, tracking: TrackEventsContext, turn_steer: CodexTurnSteerEvent) {
|
||||
@@ -418,12 +417,6 @@ impl AnalyticsEventsClient {
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn track_turn_completed(&self, turn_id: String) {
|
||||
self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::TurnCompleted(
|
||||
TurnCompletedInput { turn_id },
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn track_plugin_installed(&self, plugin: PluginTelemetryMetadata) {
|
||||
self.record_fact(AnalyticsFact::Custom(
|
||||
CustomAnalyticsFact::PluginStateChanged(PluginStateChangedInput {
|
||||
@@ -466,6 +459,17 @@ impl AnalyticsEventsClient {
|
||||
}
|
||||
self.queue.try_send(input);
|
||||
}
|
||||
|
||||
pub fn track_response(&self, connection_id: u64, response: ClientResponse) {
|
||||
self.record_fact(AnalyticsFact::Response {
|
||||
connection_id,
|
||||
response: Box::new(response),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn track_notification(&self, notification: ServerNotification) {
|
||||
self.record_fact(AnalyticsFact::Notification(Box::new(notification)));
|
||||
}
|
||||
}
|
||||
|
||||
const ANALYTICS_EVENTS_QUEUE_SIZE: usize = 256;
|
||||
@@ -481,7 +485,7 @@ struct TrackEventsRequest {
|
||||
#[serde(untagged)]
|
||||
enum TrackEventRequest {
|
||||
SkillInvocation(SkillInvocationEventRequest),
|
||||
CodexThreadInitialized(CodexThreadInitializedEvent),
|
||||
ThreadInitialized(ThreadInitializedEvent),
|
||||
AppMentioned(CodexAppMentionedEventRequest),
|
||||
AppUsed(CodexAppUsedEventRequest),
|
||||
TurnEvent(Box<CodexTurnEventRequest>),
|
||||
@@ -512,9 +516,12 @@ struct SkillInvocationEventParams {
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CodexThreadInitializedEventParams {
|
||||
struct ThreadInitializedEventParams {
|
||||
thread_id: String,
|
||||
product_client_id: String,
|
||||
client_name: Option<String>,
|
||||
client_version: Option<String>,
|
||||
experimental_api_enabled: Option<bool>,
|
||||
model: String,
|
||||
ephemeral: bool,
|
||||
session_source: Option<&'static str>,
|
||||
@@ -525,9 +532,9 @@ struct CodexThreadInitializedEventParams {
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CodexThreadInitializedEvent {
|
||||
struct ThreadInitializedEvent {
|
||||
event_type: &'static str,
|
||||
event_params: CodexThreadInitializedEventParams,
|
||||
event_params: ThreadInitializedEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -656,30 +663,31 @@ impl AnalyticsReducer {
|
||||
connection_id,
|
||||
params,
|
||||
} => {
|
||||
self.ingest_initialize(connection_id, params);
|
||||
self.ingest_initialize(connection_id, params, out);
|
||||
}
|
||||
AnalyticsFact::Request {
|
||||
connection_id: _connection_id,
|
||||
request_id: _request_id,
|
||||
request: _request,
|
||||
} => {}
|
||||
AnalyticsFact::Notification {
|
||||
connection_id: _connection_id,
|
||||
notification: _notification,
|
||||
} => {}
|
||||
connection_id,
|
||||
request_id,
|
||||
request,
|
||||
} => {
|
||||
self.ingest_request(connection_id, request_id, *request);
|
||||
}
|
||||
AnalyticsFact::Response {
|
||||
connection_id,
|
||||
response,
|
||||
} => {
|
||||
self.ingest_response(connection_id, *response, out);
|
||||
}
|
||||
AnalyticsFact::Notification(notification) => {
|
||||
self.ingest_notification(*notification, out);
|
||||
}
|
||||
AnalyticsFact::Custom(input) => match input {
|
||||
CustomAnalyticsFact::ThreadInitialized(input) => {
|
||||
self.ingest_thread_initialized(input, out);
|
||||
}
|
||||
CustomAnalyticsFact::TurnStarted(input) => {
|
||||
self.ingest_turn_started(*input);
|
||||
CustomAnalyticsFact::TurnResolvedConfig(input) => {
|
||||
self.ingest_turn_resolved_config(*input, out);
|
||||
}
|
||||
CustomAnalyticsFact::TurnSteer(input) => {
|
||||
self.ingest_turn_steer(input, out);
|
||||
}
|
||||
CustomAnalyticsFact::TurnCompleted(input) => {
|
||||
self.ingest_turn_completed(input, out);
|
||||
}
|
||||
CustomAnalyticsFact::SkillInvoked(input) => {
|
||||
self.ingest_skill_invoked(input, out).await;
|
||||
}
|
||||
@@ -699,13 +707,36 @@ impl AnalyticsReducer {
|
||||
}
|
||||
}
|
||||
|
||||
fn ingest_initialize(&mut self, connection_id: u64, params: InitializeParams) {
|
||||
self.clients.insert(
|
||||
fn ingest_initialize(
|
||||
&mut self,
|
||||
connection_id: u64,
|
||||
params: InitializeParams,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
self.connections.insert(
|
||||
connection_id,
|
||||
ClientState {
|
||||
product_client_id: params.client_info.name,
|
||||
ConnectionState {
|
||||
product_client_id: params.client_info.name.clone(),
|
||||
client_name: Some(params.client_info.name),
|
||||
client_version: Some(params.client_info.version),
|
||||
experimental_api_enabled: params
|
||||
.capabilities
|
||||
.map(|capabilities| capabilities.experimental_api),
|
||||
},
|
||||
);
|
||||
let ready_turn_ids = self
|
||||
.turns
|
||||
.iter()
|
||||
.filter_map(|(turn_id, state)| {
|
||||
(state.connection_id == Some(connection_id)
|
||||
&& state.resolved_config.is_some()
|
||||
&& state.completed.is_some())
|
||||
.then_some(turn_id.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for turn_id in ready_turn_ids {
|
||||
self.maybe_emit_turn_event(&turn_id, out);
|
||||
}
|
||||
}
|
||||
|
||||
fn ingest_thread_initialized(
|
||||
@@ -713,47 +744,232 @@ impl AnalyticsReducer {
|
||||
input: ThreadInitializedInput,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
let Some(client_state) = self.clients.get(&input.connection_id) else {
|
||||
let Some(connection_state) = self.connections.get(&input.connection_id) else {
|
||||
return;
|
||||
};
|
||||
out.push(TrackEventRequest::CodexThreadInitialized(
|
||||
codex_thread_initialized_event_request(client_state.product_client_id.clone(), input),
|
||||
out.push(TrackEventRequest::ThreadInitialized(
|
||||
thread_initialized_event_request(connection_state, input),
|
||||
));
|
||||
}
|
||||
|
||||
fn ingest_turn_started(&mut self, input: TurnStartedInput) {
|
||||
self.turns.insert(
|
||||
input.tracking.turn_id.clone(),
|
||||
TurnState {
|
||||
started_input: input,
|
||||
steer_count: 0,
|
||||
},
|
||||
fn ingest_request(
|
||||
&mut self,
|
||||
connection_id: u64,
|
||||
request_id: RequestId,
|
||||
request: ClientRequest,
|
||||
) {
|
||||
let ClientRequest::TurnStart { params, .. } = request else {
|
||||
return;
|
||||
};
|
||||
self.requests.insert(
|
||||
(connection_id, request_id),
|
||||
RequestState::TurnStart(PendingTurnStartState {
|
||||
thread_id: params.thread_id,
|
||||
num_input_images: params
|
||||
.input
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
matches!(
|
||||
item,
|
||||
codex_app_server_protocol::UserInput::Image { .. }
|
||||
| codex_app_server_protocol::UserInput::LocalImage { .. }
|
||||
)
|
||||
})
|
||||
.count(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn ingest_turn_completed(
|
||||
fn ingest_turn_resolved_config(
|
||||
&mut self,
|
||||
input: TurnCompletedInput,
|
||||
input: TurnResolvedConfigFact,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
let Some(TurnState {
|
||||
started_input,
|
||||
steer_count,
|
||||
}) = self.turns.remove(&input.turn_id)
|
||||
else {
|
||||
let turn_id = input.turn_id.clone();
|
||||
let turn_state = self.turns.entry(turn_id.clone()).or_insert(TurnState {
|
||||
connection_id: None,
|
||||
thread_id: None,
|
||||
num_input_images: None,
|
||||
resolved_config: None,
|
||||
started_at_ms: None,
|
||||
completed: None,
|
||||
steer_count: 0,
|
||||
});
|
||||
turn_state.resolved_config = Some(input);
|
||||
self.maybe_emit_turn_event(&turn_id, out);
|
||||
}
|
||||
|
||||
fn ingest_response(
|
||||
&mut self,
|
||||
connection_id: u64,
|
||||
response: ClientResponse,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
match response {
|
||||
ClientResponse::ThreadStart { response, .. } => {
|
||||
let thread = response.thread;
|
||||
self.ingest_thread_initialized(
|
||||
ThreadInitializedInput {
|
||||
connection_id,
|
||||
thread_id: thread.id,
|
||||
model: response.model,
|
||||
ephemeral: thread.ephemeral,
|
||||
session_source: thread.source.into(),
|
||||
initialization_mode: InitializationMode::New,
|
||||
created_at: u64::try_from(thread.created_at).unwrap_or_default(),
|
||||
},
|
||||
out,
|
||||
);
|
||||
}
|
||||
ClientResponse::ThreadResume { response, .. } => {
|
||||
let thread = response.thread;
|
||||
self.ingest_thread_initialized(
|
||||
ThreadInitializedInput {
|
||||
connection_id,
|
||||
thread_id: thread.id,
|
||||
model: response.model,
|
||||
ephemeral: thread.ephemeral,
|
||||
session_source: thread.source.into(),
|
||||
initialization_mode: InitializationMode::Resumed,
|
||||
created_at: u64::try_from(thread.created_at).unwrap_or_default(),
|
||||
},
|
||||
out,
|
||||
);
|
||||
}
|
||||
ClientResponse::ThreadFork { response, .. } => {
|
||||
let thread = response.thread;
|
||||
self.ingest_thread_initialized(
|
||||
ThreadInitializedInput {
|
||||
connection_id,
|
||||
thread_id: thread.id,
|
||||
model: response.model,
|
||||
ephemeral: thread.ephemeral,
|
||||
session_source: thread.source.into(),
|
||||
initialization_mode: InitializationMode::Forked,
|
||||
created_at: u64::try_from(thread.created_at).unwrap_or_default(),
|
||||
},
|
||||
out,
|
||||
);
|
||||
}
|
||||
ClientResponse::TurnStart {
|
||||
request_id,
|
||||
response,
|
||||
} => {
|
||||
let turn_id = response.turn.id;
|
||||
let Some(RequestState::TurnStart(pending_request)) =
|
||||
self.requests.remove(&(connection_id, request_id))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let turn_state = self.turns.entry(turn_id.clone()).or_insert(TurnState {
|
||||
connection_id: None,
|
||||
thread_id: None,
|
||||
num_input_images: None,
|
||||
resolved_config: None,
|
||||
started_at_ms: None,
|
||||
completed: None,
|
||||
steer_count: 0,
|
||||
});
|
||||
turn_state.connection_id = Some(connection_id);
|
||||
turn_state.thread_id = Some(pending_request.thread_id);
|
||||
turn_state.num_input_images = Some(pending_request.num_input_images);
|
||||
self.maybe_emit_turn_event(&turn_id, out);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn ingest_notification(
|
||||
&mut self,
|
||||
notification: ServerNotification,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
match notification {
|
||||
ServerNotification::TurnStarted(notification) => {
|
||||
let turn_state = self.turns.entry(notification.turn.id).or_insert(TurnState {
|
||||
connection_id: None,
|
||||
thread_id: None,
|
||||
num_input_images: None,
|
||||
resolved_config: None,
|
||||
started_at_ms: None,
|
||||
completed: None,
|
||||
steer_count: 0,
|
||||
});
|
||||
turn_state.started_at_ms = Some(now_unix_timestamp_millis());
|
||||
}
|
||||
ServerNotification::TurnCompleted(notification) => {
|
||||
let turn_state =
|
||||
self.turns
|
||||
.entry(notification.turn.id.clone())
|
||||
.or_insert(TurnState {
|
||||
connection_id: None,
|
||||
thread_id: None,
|
||||
num_input_images: None,
|
||||
resolved_config: None,
|
||||
started_at_ms: None,
|
||||
completed: None,
|
||||
steer_count: 0,
|
||||
});
|
||||
let completed_at_secs = now_unix_timestamp_secs();
|
||||
let completed_at_ms = completed_at_secs.saturating_mul(1000);
|
||||
turn_state.completed = Some(CompletedTurnState {
|
||||
status: analytics_turn_status(notification.turn.status),
|
||||
turn_error: notification.turn.error.and_then(|error| {
|
||||
error
|
||||
.codex_error_info
|
||||
.map(app_server_codex_error_info_to_core)
|
||||
}),
|
||||
completed_at_secs,
|
||||
duration_ms: turn_state
|
||||
.started_at_ms
|
||||
.map(|started_at_ms| completed_at_ms.saturating_sub(started_at_ms)),
|
||||
});
|
||||
let turn_id = notification.turn.id;
|
||||
self.maybe_emit_turn_event(&turn_id, out);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_emit_turn_event(&mut self, turn_id: &str, out: &mut Vec<TrackEventRequest>) {
|
||||
let Some(turn_state) = self.turns.get(turn_id) else {
|
||||
return;
|
||||
};
|
||||
let TurnStartedInput {
|
||||
tracking,
|
||||
mut turn_event,
|
||||
} = started_input;
|
||||
turn_event.steer_count = Some(steer_count);
|
||||
let Some(connection_id) = turn_state.connection_id else {
|
||||
return;
|
||||
};
|
||||
let Some(connection_state) = self.connections.get(&connection_id) else {
|
||||
return;
|
||||
};
|
||||
let Some(thread_id) = turn_state.thread_id.clone() else {
|
||||
return;
|
||||
};
|
||||
let Some(num_input_images) = turn_state.num_input_images else {
|
||||
return;
|
||||
};
|
||||
let Some(resolved_config) = turn_state.resolved_config.clone() else {
|
||||
return;
|
||||
};
|
||||
let Some(completed) = turn_state.completed.clone() else {
|
||||
return;
|
||||
};
|
||||
let steer_count = turn_state.steer_count;
|
||||
out.push(TrackEventRequest::TurnEvent(Box::new(
|
||||
CodexTurnEventRequest {
|
||||
event_type: "codex_turn_event",
|
||||
event_params: codex_turn_event_params(&tracking, turn_event),
|
||||
event_params: codex_turn_event_params(
|
||||
connection_state.product_client_id.clone(),
|
||||
thread_id,
|
||||
turn_id.to_string(),
|
||||
num_input_images,
|
||||
resolved_config,
|
||||
completed,
|
||||
steer_count,
|
||||
turn_state.started_at_ms.map(|value| value / 1000),
|
||||
),
|
||||
},
|
||||
)));
|
||||
self.turns.remove(turn_id);
|
||||
}
|
||||
|
||||
fn ingest_turn_steer(&mut self, input: TurnSteerInput, out: &mut Vec<TrackEventRequest>) {
|
||||
@@ -889,49 +1105,70 @@ fn codex_app_metadata(tracking: &TrackEventsContext, app: AppInvocation) -> Code
|
||||
}
|
||||
|
||||
fn codex_turn_event_params(
|
||||
tracking: &TrackEventsContext,
|
||||
turn_event: CodexTurnEvent,
|
||||
product_client_id: String,
|
||||
thread_id: String,
|
||||
turn_id: String,
|
||||
num_input_images: usize,
|
||||
resolved_config: TurnResolvedConfigFact,
|
||||
completed: CompletedTurnState,
|
||||
steer_count: usize,
|
||||
started_at: Option<u64>,
|
||||
) -> CodexTurnEventParams {
|
||||
let TurnResolvedConfigFact {
|
||||
turn_id: _resolved_turn_id,
|
||||
submission_type,
|
||||
model,
|
||||
model_provider,
|
||||
sandbox_policy,
|
||||
reasoning_effort,
|
||||
reasoning_summary,
|
||||
service_tier,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox_network_access,
|
||||
collaboration_mode,
|
||||
personality,
|
||||
is_first_turn,
|
||||
} = resolved_config;
|
||||
CodexTurnEventParams {
|
||||
thread_id: tracking.thread_id.clone(),
|
||||
turn_id: tracking.turn_id.clone(),
|
||||
product_client_id: Some(originator().value),
|
||||
submission_type: turn_event.submission_type,
|
||||
model: Some(tracking.model_slug.clone()),
|
||||
model_provider: turn_event.model_provider,
|
||||
sandbox_policy: Some(sandbox_policy_mode(&turn_event.sandbox_policy)),
|
||||
reasoning_effort: turn_event.reasoning_effort.map(|value| value.to_string()),
|
||||
reasoning_summary: reasoning_summary_mode(turn_event.reasoning_summary),
|
||||
service_tier: turn_event
|
||||
.service_tier
|
||||
thread_id,
|
||||
turn_id,
|
||||
product_client_id: Some(product_client_id),
|
||||
submission_type,
|
||||
model: Some(model),
|
||||
model_provider,
|
||||
sandbox_policy: Some(sandbox_policy_mode(&sandbox_policy)),
|
||||
reasoning_effort: reasoning_effort.map(|value| value.to_string()),
|
||||
reasoning_summary: reasoning_summary_mode(reasoning_summary),
|
||||
service_tier: service_tier
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "default".to_string()),
|
||||
approval_policy: turn_event.approval_policy.to_string(),
|
||||
approvals_reviewer: turn_event.approvals_reviewer.to_string(),
|
||||
sandbox_network_access: turn_event.sandbox_network_access,
|
||||
collaboration_mode: Some(collaboration_mode_mode(turn_event.collaboration_mode)),
|
||||
personality: personality_mode(turn_event.personality),
|
||||
num_input_images: turn_event.num_input_images,
|
||||
is_first_turn: turn_event.is_first_turn,
|
||||
status: turn_event.status,
|
||||
turn_error: turn_event.turn_error,
|
||||
steer_count: turn_event.steer_count,
|
||||
total_tool_call_count: turn_event.total_tool_call_count,
|
||||
shell_command_count: turn_event.shell_command_count,
|
||||
file_change_count: turn_event.file_change_count,
|
||||
mcp_tool_call_count: turn_event.mcp_tool_call_count,
|
||||
dynamic_tool_call_count: turn_event.dynamic_tool_call_count,
|
||||
subagent_tool_call_count: turn_event.subagent_tool_call_count,
|
||||
web_search_count: turn_event.web_search_count,
|
||||
image_generation_count: turn_event.image_generation_count,
|
||||
input_tokens: turn_event.input_tokens,
|
||||
cached_input_tokens: turn_event.cached_input_tokens,
|
||||
output_tokens: turn_event.output_tokens,
|
||||
reasoning_output_tokens: turn_event.reasoning_output_tokens,
|
||||
total_tokens: turn_event.total_tokens,
|
||||
duration_ms: turn_event.duration_ms,
|
||||
started_at: turn_event.started_at,
|
||||
completed_at: turn_event.completed_at,
|
||||
approval_policy: approval_policy.to_string(),
|
||||
approvals_reviewer: approvals_reviewer.to_string(),
|
||||
sandbox_network_access,
|
||||
collaboration_mode: Some(collaboration_mode_mode(collaboration_mode)),
|
||||
personality: personality_mode(personality),
|
||||
num_input_images,
|
||||
is_first_turn,
|
||||
status: completed.status,
|
||||
turn_error: completed.turn_error,
|
||||
steer_count: Some(steer_count),
|
||||
total_tool_call_count: None,
|
||||
shell_command_count: None,
|
||||
file_change_count: None,
|
||||
mcp_tool_call_count: None,
|
||||
dynamic_tool_call_count: None,
|
||||
subagent_tool_call_count: None,
|
||||
web_search_count: None,
|
||||
image_generation_count: None,
|
||||
input_tokens: None,
|
||||
cached_input_tokens: None,
|
||||
output_tokens: None,
|
||||
reasoning_output_tokens: None,
|
||||
total_tokens: None,
|
||||
duration_ms: completed.duration_ms,
|
||||
started_at,
|
||||
completed_at: Some(completed.completed_at_secs),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -981,30 +1218,33 @@ fn personality_mode(personality: Option<Personality>) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn codex_thread_initialized_event_request(
|
||||
product_client_id: String,
|
||||
fn thread_initialized_event_request(
|
||||
connection_state: &ConnectionState,
|
||||
input: ThreadInitializedInput,
|
||||
) -> CodexThreadInitializedEvent {
|
||||
CodexThreadInitializedEvent {
|
||||
) -> ThreadInitializedEvent {
|
||||
ThreadInitializedEvent {
|
||||
event_type: "codex_thread_initialized",
|
||||
event_params: codex_thread_initialized_event_params(product_client_id, input),
|
||||
event_params: thread_initialized_event_params(connection_state, input),
|
||||
}
|
||||
}
|
||||
|
||||
fn codex_thread_initialized_event_params(
|
||||
product_client_id: String,
|
||||
fn thread_initialized_event_params(
|
||||
connection_state: &ConnectionState,
|
||||
input: ThreadInitializedInput,
|
||||
) -> CodexThreadInitializedEventParams {
|
||||
CodexThreadInitializedEventParams {
|
||||
) -> ThreadInitializedEventParams {
|
||||
ThreadInitializedEventParams {
|
||||
thread_id: input.thread_id,
|
||||
product_client_id,
|
||||
product_client_id: connection_state.product_client_id.clone(),
|
||||
client_name: connection_state.client_name.clone(),
|
||||
client_version: connection_state.client_version.clone(),
|
||||
experimental_api_enabled: connection_state.experimental_api_enabled,
|
||||
model: input.model,
|
||||
ephemeral: input.ephemeral,
|
||||
session_source: session_source_name(&input.session_source),
|
||||
initialization_mode: input.initialization_mode,
|
||||
subagent_source: None,
|
||||
parent_thread_id: None,
|
||||
created_at: now_unix_timestamp_secs(),
|
||||
created_at: input.created_at,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1054,12 +1294,78 @@ fn session_source_name(session_source: &SessionSource) -> Option<&'static str> {
|
||||
}
|
||||
|
||||
fn now_unix_timestamp_secs() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
fn now_unix_timestamp_millis() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u64
|
||||
}
|
||||
|
||||
fn analytics_turn_status(status: codex_app_server_protocol::TurnStatus) -> Option<TurnStatus> {
|
||||
match status {
|
||||
codex_app_server_protocol::TurnStatus::Completed => Some(TurnStatus::Completed),
|
||||
codex_app_server_protocol::TurnStatus::Failed => Some(TurnStatus::Failed),
|
||||
codex_app_server_protocol::TurnStatus::Interrupted => Some(TurnStatus::Interrupted),
|
||||
codex_app_server_protocol::TurnStatus::InProgress => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn app_server_codex_error_info_to_core(
|
||||
info: codex_app_server_protocol::CodexErrorInfo,
|
||||
) -> CodexErrorInfo {
|
||||
match info {
|
||||
codex_app_server_protocol::CodexErrorInfo::ContextWindowExceeded => {
|
||||
CodexErrorInfo::ContextWindowExceeded
|
||||
}
|
||||
codex_app_server_protocol::CodexErrorInfo::UsageLimitExceeded => {
|
||||
CodexErrorInfo::UsageLimitExceeded
|
||||
}
|
||||
codex_app_server_protocol::CodexErrorInfo::ServerOverloaded => {
|
||||
CodexErrorInfo::ServerOverloaded
|
||||
}
|
||||
codex_app_server_protocol::CodexErrorInfo::HttpConnectionFailed { http_status_code } => {
|
||||
CodexErrorInfo::HttpConnectionFailed { http_status_code }
|
||||
}
|
||||
codex_app_server_protocol::CodexErrorInfo::ResponseStreamConnectionFailed {
|
||||
http_status_code,
|
||||
} => CodexErrorInfo::ResponseStreamConnectionFailed { http_status_code },
|
||||
codex_app_server_protocol::CodexErrorInfo::InternalServerError => {
|
||||
CodexErrorInfo::InternalServerError
|
||||
}
|
||||
codex_app_server_protocol::CodexErrorInfo::Unauthorized => CodexErrorInfo::Unauthorized,
|
||||
codex_app_server_protocol::CodexErrorInfo::BadRequest => CodexErrorInfo::BadRequest,
|
||||
codex_app_server_protocol::CodexErrorInfo::ThreadRollbackFailed => {
|
||||
CodexErrorInfo::ThreadRollbackFailed
|
||||
}
|
||||
codex_app_server_protocol::CodexErrorInfo::SandboxError => CodexErrorInfo::SandboxError,
|
||||
codex_app_server_protocol::CodexErrorInfo::ResponseStreamDisconnected {
|
||||
http_status_code,
|
||||
} => CodexErrorInfo::ResponseStreamDisconnected { http_status_code },
|
||||
codex_app_server_protocol::CodexErrorInfo::ResponseTooManyFailedAttempts {
|
||||
http_status_code,
|
||||
} => CodexErrorInfo::ResponseTooManyFailedAttempts { http_status_code },
|
||||
codex_app_server_protocol::CodexErrorInfo::ActiveTurnNotSteerable { turn_kind } => {
|
||||
CodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: match turn_kind {
|
||||
codex_app_server_protocol::NonSteerableTurnKind::Review => {
|
||||
codex_protocol::protocol::NonSteerableTurnKind::Review
|
||||
}
|
||||
codex_app_server_protocol::NonSteerableTurnKind::Compact => {
|
||||
codex_protocol::protocol::NonSteerableTurnKind::Compact
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
codex_app_server_protocol::CodexErrorInfo::Other => CodexErrorInfo::Other,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_track_events(
|
||||
auth_manager: &AuthManager,
|
||||
base_url: &str,
|
||||
|
||||
@@ -6,29 +6,49 @@ use super::CodexAppMentionedEventRequest;
|
||||
use super::CodexAppUsedEventRequest;
|
||||
use super::CodexPluginEventRequest;
|
||||
use super::CodexPluginUsedEventRequest;
|
||||
use super::CodexTurnEvent;
|
||||
use super::CodexTurnEventRequest;
|
||||
use super::CodexTurnSteerEvent;
|
||||
use super::CodexTurnSteerEventRequest;
|
||||
use super::CompletedTurnState;
|
||||
use super::CustomAnalyticsFact;
|
||||
use super::InitializationMode;
|
||||
use super::InvocationType;
|
||||
use super::ThreadInitializedInput;
|
||||
use super::TrackEventRequest;
|
||||
use super::TrackEventsContext;
|
||||
use super::TurnCompletedInput;
|
||||
use super::TurnStartedInput;
|
||||
use super::TurnResolvedConfigFact;
|
||||
use super::TurnStatus;
|
||||
use super::TurnSteerInput;
|
||||
use super::TurnSteerRejectionReason;
|
||||
use super::TurnSteerResult;
|
||||
use super::codex_app_metadata;
|
||||
use super::codex_plugin_metadata;
|
||||
use super::codex_plugin_used_metadata;
|
||||
use super::codex_thread_initialized_event_request;
|
||||
use super::codex_turn_event_params;
|
||||
use super::codex_turn_steer_event_params;
|
||||
use super::normalize_path_for_skill_id;
|
||||
use super::thread_initialized_event_request;
|
||||
use codex_app_server_protocol::ApprovalsReviewer as AppServerApprovalsReviewer;
|
||||
use codex_app_server_protocol::AskForApproval as AppServerAskForApproval;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ClientResponse;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SandboxPolicy as AppServerSandboxPolicy;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::SessionSource as AppServerSessionSource;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadResumeResponse;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadStatus as AppServerThreadStatus;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnError as AppServerTurnError;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartedNotification;
|
||||
use codex_app_server_protocol::TurnStatus as AppServerTurnStatus;
|
||||
use codex_app_server_protocol::UserInput as AppServerUserInput;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_plugin::AppConnectorId;
|
||||
use codex_plugin::PluginCapabilitySummary;
|
||||
@@ -51,6 +71,215 @@ use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
fn sample_thread(thread_id: &str, ephemeral: bool) -> Thread {
|
||||
Thread {
|
||||
id: thread_id.to_string(),
|
||||
preview: "first prompt".to_string(),
|
||||
ephemeral,
|
||||
model_provider: "openai".to_string(),
|
||||
created_at: 1,
|
||||
updated_at: 2,
|
||||
status: AppServerThreadStatus::Idle,
|
||||
path: None,
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: AppServerSessionSource::Exec,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
git_info: None,
|
||||
name: None,
|
||||
turns: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_thread_start_response(thread_id: &str, ephemeral: bool, model: &str) -> ClientResponse {
|
||||
ClientResponse::ThreadStart {
|
||||
request_id: RequestId::Integer(1),
|
||||
response: ThreadStartResponse {
|
||||
thread: sample_thread(thread_id, ephemeral),
|
||||
model: model.to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
approval_policy: AppServerAskForApproval::OnFailure,
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
reasoning_effort: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_thread_resume_response(thread_id: &str, ephemeral: bool, model: &str) -> ClientResponse {
|
||||
ClientResponse::ThreadResume {
|
||||
request_id: RequestId::Integer(2),
|
||||
response: ThreadResumeResponse {
|
||||
thread: sample_thread(thread_id, ephemeral),
|
||||
model: model.to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
approval_policy: AppServerAskForApproval::OnFailure,
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
reasoning_effort: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_turn_start_request(thread_id: &str, request_id: i64) -> ClientRequest {
|
||||
ClientRequest::TurnStart {
|
||||
request_id: RequestId::Integer(request_id),
|
||||
params: TurnStartParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
input: vec![
|
||||
AppServerUserInput::Text {
|
||||
text: "hello".to_string(),
|
||||
text_elements: vec![],
|
||||
},
|
||||
AppServerUserInput::Image {
|
||||
url: "https://example.com/a.png".to_string(),
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_turn_start_response(turn_id: &str, request_id: i64) -> ClientResponse {
|
||||
ClientResponse::TurnStart {
|
||||
request_id: RequestId::Integer(request_id),
|
||||
response: codex_app_server_protocol::TurnStartResponse {
|
||||
turn: Turn {
|
||||
id: turn_id.to_string(),
|
||||
items: vec![],
|
||||
status: AppServerTurnStatus::InProgress,
|
||||
error: None,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_turn_started_notification(thread_id: &str, turn_id: &str) -> ServerNotification {
|
||||
ServerNotification::TurnStarted(TurnStartedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn: Turn {
|
||||
id: turn_id.to_string(),
|
||||
items: vec![],
|
||||
status: AppServerTurnStatus::InProgress,
|
||||
error: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn sample_turn_completed_notification(
|
||||
thread_id: &str,
|
||||
turn_id: &str,
|
||||
status: AppServerTurnStatus,
|
||||
codex_error_info: Option<codex_app_server_protocol::CodexErrorInfo>,
|
||||
) -> ServerNotification {
|
||||
ServerNotification::TurnCompleted(TurnCompletedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn: Turn {
|
||||
id: turn_id.to_string(),
|
||||
items: vec![],
|
||||
status,
|
||||
error: codex_error_info.map(|codex_error_info| AppServerTurnError {
|
||||
message: "turn failed".to_string(),
|
||||
codex_error_info: Some(codex_error_info),
|
||||
additional_details: None,
|
||||
}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn sample_turn_resolved_config(turn_id: &str) -> TurnResolvedConfigFact {
|
||||
TurnResolvedConfigFact {
|
||||
turn_id: turn_id.to_string(),
|
||||
submission_type: None,
|
||||
model: "gpt-5".to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
reasoning_effort: None,
|
||||
reasoning_summary: None,
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::GuardianSubagent,
|
||||
sandbox_network_access: true,
|
||||
collaboration_mode: ModeKind::Plan,
|
||||
personality: None,
|
||||
is_first_turn: true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn ingest_turn_prerequisites(
|
||||
reducer: &mut AnalyticsReducer,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
include_initialize: bool,
|
||||
include_resolved_config: bool,
|
||||
include_started: bool,
|
||||
) {
|
||||
if include_initialize {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Initialize {
|
||||
connection_id: 7,
|
||||
params: InitializeParams {
|
||||
client_info: ClientInfo {
|
||||
name: "codex-tui".to_string(),
|
||||
title: None,
|
||||
version: "1.0.0".to_string(),
|
||||
},
|
||||
capabilities: None,
|
||||
},
|
||||
},
|
||||
out,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Request {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(3),
|
||||
request: Box::new(sample_turn_start_request("thread-2", 3)),
|
||||
},
|
||||
out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Response {
|
||||
connection_id: 7,
|
||||
response: Box::new(sample_turn_start_response("turn-2", 3)),
|
||||
},
|
||||
out,
|
||||
)
|
||||
.await;
|
||||
|
||||
if include_resolved_config {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::TurnResolvedConfig(Box::new(
|
||||
sample_turn_resolved_config("turn-2"),
|
||||
))),
|
||||
out,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if include_started {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(sample_turn_started_notification(
|
||||
"thread-2", "turn-2",
|
||||
))),
|
||||
out,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_absolute_path(path: &PathBuf) -> String {
|
||||
std::fs::canonicalize(path)
|
||||
.unwrap_or_else(|_| path.to_path_buf())
|
||||
@@ -215,17 +444,17 @@ fn app_used_dedupe_is_keyed_by_turn_and_connector() {
|
||||
|
||||
#[test]
|
||||
fn turn_event_serializes_expected_shape() {
|
||||
let tracking = TrackEventsContext {
|
||||
model_slug: "gpt-5".to_string(),
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
};
|
||||
let event = TrackEventRequest::TurnEvent(Box::new(CodexTurnEventRequest {
|
||||
event_type: "codex_turn_event",
|
||||
event_params: codex_turn_event_params(
|
||||
&tracking,
|
||||
CodexTurnEvent {
|
||||
"codex-tui".to_string(),
|
||||
"thread-2".to_string(),
|
||||
"turn-2".to_string(),
|
||||
2,
|
||||
TurnResolvedConfigFact {
|
||||
turn_id: "turn-2".to_string(),
|
||||
submission_type: None,
|
||||
model: "gpt-5".to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
reasoning_effort: Some(ReasoningEffort::High),
|
||||
@@ -236,28 +465,16 @@ fn turn_event_serializes_expected_shape() {
|
||||
sandbox_network_access: true,
|
||||
collaboration_mode: ModeKind::Plan,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
num_input_images: 2,
|
||||
is_first_turn: true,
|
||||
status: None,
|
||||
turn_error: None,
|
||||
steer_count: None,
|
||||
total_tool_call_count: None,
|
||||
shell_command_count: None,
|
||||
file_change_count: None,
|
||||
mcp_tool_call_count: None,
|
||||
dynamic_tool_call_count: None,
|
||||
subagent_tool_call_count: None,
|
||||
web_search_count: None,
|
||||
image_generation_count: None,
|
||||
input_tokens: None,
|
||||
cached_input_tokens: None,
|
||||
output_tokens: None,
|
||||
reasoning_output_tokens: None,
|
||||
total_tokens: None,
|
||||
duration_ms: None,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
},
|
||||
CompletedTurnState {
|
||||
status: Some(TurnStatus::Completed),
|
||||
turn_error: None,
|
||||
completed_at_secs: 456,
|
||||
duration_ms: Some(1234),
|
||||
},
|
||||
0,
|
||||
Some(455),
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -270,7 +487,7 @@ fn turn_event_serializes_expected_shape() {
|
||||
"event_params": {
|
||||
"thread_id": "thread-2",
|
||||
"turn_id": "turn-2",
|
||||
"product_client_id": originator().value,
|
||||
"product_client_id": "codex-tui",
|
||||
"submission_type": null,
|
||||
"model": "gpt-5",
|
||||
"model_provider": "openai",
|
||||
@@ -285,9 +502,9 @@ fn turn_event_serializes_expected_shape() {
|
||||
"personality": "pragmatic",
|
||||
"num_input_images": 2,
|
||||
"is_first_turn": true,
|
||||
"status": null,
|
||||
"status": "completed",
|
||||
"turn_error": null,
|
||||
"steer_count": null,
|
||||
"steer_count": 0,
|
||||
"total_tool_call_count": null,
|
||||
"shell_command_count": null,
|
||||
"file_change_count": null,
|
||||
@@ -301,9 +518,9 @@ fn turn_event_serializes_expected_shape() {
|
||||
"output_tokens": null,
|
||||
"reasoning_output_tokens": null,
|
||||
"total_tokens": null,
|
||||
"duration_ms": null,
|
||||
"started_at": null,
|
||||
"completed_at": null
|
||||
"duration_ms": 1234,
|
||||
"started_at": 455,
|
||||
"completed_at": 456
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -394,56 +611,23 @@ fn rejected_turn_steer_event_serializes_expected_shape() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_started_then_completed_emits_turn_event() {
|
||||
let tracking = TrackEventsContext {
|
||||
model_slug: "gpt-5".to_string(),
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
};
|
||||
async fn turn_lifecycle_emits_turn_event() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut out = Vec::new();
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::TurnStarted(Box::new(
|
||||
TurnStartedInput {
|
||||
tracking: tracking.clone(),
|
||||
turn_event: CodexTurnEvent {
|
||||
submission_type: None,
|
||||
model_provider: "openai".to_string(),
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
reasoning_effort: Some(ReasoningEffort::High),
|
||||
reasoning_summary: Some(ReasoningSummary::Detailed),
|
||||
service_tier: Some(ServiceTier::Flex),
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::GuardianSubagent,
|
||||
sandbox_network_access: true,
|
||||
collaboration_mode: ModeKind::Plan,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
num_input_images: 2,
|
||||
is_first_turn: true,
|
||||
status: None,
|
||||
turn_error: None,
|
||||
steer_count: None,
|
||||
total_tool_call_count: None,
|
||||
shell_command_count: None,
|
||||
file_change_count: None,
|
||||
mcp_tool_call_count: None,
|
||||
dynamic_tool_call_count: None,
|
||||
subagent_tool_call_count: None,
|
||||
web_search_count: None,
|
||||
image_generation_count: None,
|
||||
input_tokens: None,
|
||||
cached_input_tokens: None,
|
||||
output_tokens: None,
|
||||
reasoning_output_tokens: None,
|
||||
total_tokens: None,
|
||||
duration_ms: None,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
AnalyticsFact::Initialize {
|
||||
connection_id: 7,
|
||||
params: InitializeParams {
|
||||
client_info: ClientInfo {
|
||||
name: "codex-tui".to_string(),
|
||||
title: None,
|
||||
version: "1.0.0".to_string(),
|
||||
},
|
||||
capabilities: None,
|
||||
},
|
||||
))),
|
||||
},
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
@@ -452,9 +636,62 @@ async fn turn_started_then_completed_emits_turn_event() {
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::TurnCompleted(TurnCompletedInput {
|
||||
turn_id: tracking.turn_id.clone(),
|
||||
})),
|
||||
AnalyticsFact::Request {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(3),
|
||||
request: Box::new(sample_turn_start_request("thread-2", 3)),
|
||||
},
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Response {
|
||||
connection_id: 7,
|
||||
response: Box::new(sample_turn_start_response("turn-2", 3)),
|
||||
},
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::TurnResolvedConfig(Box::new(
|
||||
TurnResolvedConfigFact {
|
||||
turn_id: "turn-2".to_string(),
|
||||
submission_type: None,
|
||||
model: "gpt-5".to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
reasoning_effort: Some(ReasoningEffort::High),
|
||||
reasoning_summary: Some(ReasoningSummary::Detailed),
|
||||
service_tier: Some(ServiceTier::Flex),
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::GuardianSubagent,
|
||||
sandbox_network_access: true,
|
||||
collaboration_mode: ModeKind::Plan,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
is_first_turn: true,
|
||||
},
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(sample_turn_started_notification(
|
||||
"thread-2", "turn-2",
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(sample_turn_completed_notification(
|
||||
"thread-2",
|
||||
"turn-2",
|
||||
AppServerTurnStatus::Completed,
|
||||
None,
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
@@ -464,71 +701,35 @@ async fn turn_started_then_completed_emits_turn_event() {
|
||||
assert_eq!(payload["event_type"], json!("codex_turn_event"));
|
||||
assert_eq!(payload["event_params"]["thread_id"], json!("thread-2"));
|
||||
assert_eq!(payload["event_params"]["turn_id"], json!("turn-2"));
|
||||
assert_eq!(
|
||||
payload["event_params"]["product_client_id"],
|
||||
json!("codex-tui")
|
||||
);
|
||||
assert_eq!(payload["event_params"]["num_input_images"], json!(1));
|
||||
assert_eq!(payload["event_params"]["status"], json!("completed"));
|
||||
assert_eq!(payload["event_params"]["steer_count"], json!(0));
|
||||
assert!(payload["event_params"]["started_at"].as_u64().is_some());
|
||||
assert!(payload["event_params"]["completed_at"].as_u64().is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn accepted_steers_increment_turn_steer_count() {
|
||||
let tracking = TrackEventsContext {
|
||||
model_slug: "gpt-5".to_string(),
|
||||
thread_id: "thread-4".to_string(),
|
||||
turn_id: "turn-4".to_string(),
|
||||
};
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut out = Vec::new();
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::TurnStarted(Box::new(
|
||||
TurnStartedInput {
|
||||
tracking: tracking.clone(),
|
||||
turn_event: CodexTurnEvent {
|
||||
submission_type: None,
|
||||
model_provider: "openai".to_string(),
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
reasoning_effort: None,
|
||||
reasoning_summary: None,
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::GuardianSubagent,
|
||||
sandbox_network_access: true,
|
||||
collaboration_mode: ModeKind::Default,
|
||||
personality: None,
|
||||
num_input_images: 0,
|
||||
is_first_turn: false,
|
||||
status: None,
|
||||
turn_error: None,
|
||||
steer_count: None,
|
||||
total_tool_call_count: None,
|
||||
shell_command_count: None,
|
||||
file_change_count: None,
|
||||
mcp_tool_call_count: None,
|
||||
dynamic_tool_call_count: None,
|
||||
subagent_tool_call_count: None,
|
||||
web_search_count: None,
|
||||
image_generation_count: None,
|
||||
input_tokens: None,
|
||||
cached_input_tokens: None,
|
||||
output_tokens: None,
|
||||
reasoning_output_tokens: None,
|
||||
total_tokens: None,
|
||||
duration_ms: None,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
},
|
||||
},
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
ingest_turn_prerequisites(&mut reducer, &mut out, true, true, true).await;
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer(super::TurnSteerInput {
|
||||
tracking: tracking.clone(),
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer(TurnSteerInput {
|
||||
tracking: TrackEventsContext {
|
||||
model_slug: "gpt-5".to_string(),
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
},
|
||||
turn_steer: CodexTurnSteerEvent {
|
||||
expected_turn_id: Some("turn-4".to_string()),
|
||||
accepted_turn_id: Some("turn-4".to_string()),
|
||||
expected_turn_id: Some("turn-2".to_string()),
|
||||
accepted_turn_id: Some("turn-2".to_string()),
|
||||
num_input_images: 0,
|
||||
result: TurnSteerResult::Accepted,
|
||||
rejection_reason: None,
|
||||
@@ -541,8 +742,12 @@ async fn accepted_steers_increment_turn_steer_count() {
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer(super::TurnSteerInput {
|
||||
tracking: tracking.clone(),
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer(TurnSteerInput {
|
||||
tracking: TrackEventsContext {
|
||||
model_slug: "gpt-5".to_string(),
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
},
|
||||
turn_steer: CodexTurnSteerEvent {
|
||||
expected_turn_id: None,
|
||||
accepted_turn_id: None,
|
||||
@@ -558,11 +763,15 @@ async fn accepted_steers_increment_turn_steer_count() {
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer(super::TurnSteerInput {
|
||||
tracking: tracking.clone(),
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer(TurnSteerInput {
|
||||
tracking: TrackEventsContext {
|
||||
model_slug: "gpt-5".to_string(),
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
},
|
||||
turn_steer: CodexTurnSteerEvent {
|
||||
expected_turn_id: Some("turn-4".to_string()),
|
||||
accepted_turn_id: Some("turn-4".to_string()),
|
||||
expected_turn_id: Some("turn-2".to_string()),
|
||||
accepted_turn_id: Some("turn-2".to_string()),
|
||||
num_input_images: 1,
|
||||
result: TurnSteerResult::Accepted,
|
||||
rejection_reason: None,
|
||||
@@ -575,9 +784,12 @@ async fn accepted_steers_increment_turn_steer_count() {
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::TurnCompleted(TurnCompletedInput {
|
||||
turn_id: tracking.turn_id.clone(),
|
||||
})),
|
||||
AnalyticsFact::Notification(Box::new(sample_turn_completed_notification(
|
||||
"thread-2",
|
||||
"turn-2",
|
||||
AppServerTurnStatus::Completed,
|
||||
None,
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
@@ -587,14 +799,119 @@ async fn accepted_steers_increment_turn_steer_count() {
|
||||
.find(|event| matches!(event, TrackEventRequest::TurnEvent(_)))
|
||||
.expect("turn event should be emitted");
|
||||
let payload = serde_json::to_value(turn_event).expect("serialize turn event");
|
||||
|
||||
assert_eq!(payload["event_params"]["steer_count"], json!(2));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_does_not_emit_without_required_prerequisites() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut out = Vec::new();
|
||||
|
||||
ingest_turn_prerequisites(&mut reducer, &mut out, false, true, false).await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(sample_turn_completed_notification(
|
||||
"thread-2",
|
||||
"turn-2",
|
||||
AppServerTurnStatus::Completed,
|
||||
None,
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
assert!(out.is_empty());
|
||||
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut out = Vec::new();
|
||||
|
||||
ingest_turn_prerequisites(&mut reducer, &mut out, true, false, false).await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(sample_turn_completed_notification(
|
||||
"thread-2",
|
||||
"turn-2",
|
||||
AppServerTurnStatus::Completed,
|
||||
None,
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
assert!(out.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_completed_without_started_notification_emits_null_started_at() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut out = Vec::new();
|
||||
|
||||
ingest_turn_prerequisites(&mut reducer, &mut out, true, true, false).await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(sample_turn_completed_notification(
|
||||
"thread-2",
|
||||
"turn-2",
|
||||
AppServerTurnStatus::Completed,
|
||||
None,
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
|
||||
let payload = serde_json::to_value(&out[0]).expect("serialize turn event");
|
||||
assert_eq!(payload["event_params"]["started_at"], json!(null));
|
||||
assert_eq!(payload["event_params"]["duration_ms"], json!(null));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_completed_maps_completion_variants() {
|
||||
for (status, codex_error_info, expected_status, expected_turn_error) in [
|
||||
(
|
||||
AppServerTurnStatus::Failed,
|
||||
Some(codex_app_server_protocol::CodexErrorInfo::BadRequest),
|
||||
json!("failed"),
|
||||
Some(json!("bad_request")),
|
||||
),
|
||||
(
|
||||
AppServerTurnStatus::Interrupted,
|
||||
None,
|
||||
json!("interrupted"),
|
||||
None,
|
||||
),
|
||||
] {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut out = Vec::new();
|
||||
|
||||
ingest_turn_prerequisites(&mut reducer, &mut out, true, true, false).await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(sample_turn_completed_notification(
|
||||
"thread-2",
|
||||
"turn-2",
|
||||
status,
|
||||
codex_error_info,
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
|
||||
let payload = serde_json::to_value(&out[0]).expect("serialize turn event");
|
||||
assert_eq!(payload["event_params"]["status"], expected_status);
|
||||
assert_eq!(
|
||||
payload["event_params"]["turn_error"],
|
||||
expected_turn_error.unwrap_or(json!(null))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_initialized_event_serializes_expected_shape() {
|
||||
let event = TrackEventRequest::CodexThreadInitialized(codex_thread_initialized_event_request(
|
||||
"codex-tui".to_string(),
|
||||
let event = TrackEventRequest::ThreadInitialized(thread_initialized_event_request(
|
||||
&super::ConnectionState {
|
||||
product_client_id: "codex-tui".to_string(),
|
||||
client_name: Some("codex-tui".to_string()),
|
||||
client_version: Some("1.0.0".to_string()),
|
||||
experimental_api_enabled: Some(true),
|
||||
},
|
||||
ThreadInitializedInput {
|
||||
connection_id: 1,
|
||||
thread_id: "thread-0".to_string(),
|
||||
@@ -602,6 +919,7 @@ fn thread_initialized_event_serializes_expected_shape() {
|
||||
ephemeral: true,
|
||||
session_source: SessionSource::Exec,
|
||||
initialization_mode: InitializationMode::New,
|
||||
created_at: 1,
|
||||
},
|
||||
));
|
||||
|
||||
@@ -614,17 +932,19 @@ fn thread_initialized_event_serializes_expected_shape() {
|
||||
"event_params": {
|
||||
"thread_id": "thread-0",
|
||||
"product_client_id": "codex-tui",
|
||||
"client_name": "codex-tui",
|
||||
"client_version": "1.0.0",
|
||||
"experimental_api_enabled": true,
|
||||
"model": "gpt-5",
|
||||
"ephemeral": true,
|
||||
"session_source": "user",
|
||||
"initialization_mode": "new",
|
||||
"subagent_source": null,
|
||||
"parent_thread_id": null,
|
||||
"created_at": payload["event_params"]["created_at"]
|
||||
"created_at": 1
|
||||
}
|
||||
})
|
||||
);
|
||||
assert!(payload["event_params"]["created_at"].as_u64().is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -634,16 +954,14 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::ThreadInitialized(
|
||||
ThreadInitializedInput {
|
||||
connection_id: 7,
|
||||
thread_id: "thread-no-client".to_string(),
|
||||
model: "gpt-5".to_string(),
|
||||
ephemeral: false,
|
||||
session_source: SessionSource::Exec,
|
||||
initialization_mode: InitializationMode::New,
|
||||
},
|
||||
)),
|
||||
AnalyticsFact::Response {
|
||||
connection_id: 7,
|
||||
response: Box::new(sample_thread_start_response(
|
||||
"thread-no-client",
|
||||
false,
|
||||
"gpt-5",
|
||||
)),
|
||||
},
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
@@ -669,16 +987,10 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::ThreadInitialized(
|
||||
ThreadInitializedInput {
|
||||
connection_id: 7,
|
||||
thread_id: "thread-1".to_string(),
|
||||
model: "gpt-5".to_string(),
|
||||
ephemeral: true,
|
||||
session_source: SessionSource::Exec,
|
||||
initialization_mode: InitializationMode::Resumed,
|
||||
},
|
||||
)),
|
||||
AnalyticsFact::Response {
|
||||
connection_id: 7,
|
||||
response: Box::new(sample_thread_resume_response("thread-1", true, "gpt-5")),
|
||||
},
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -6,7 +6,6 @@ pub use analytics_client::AnalyticsReducer;
|
||||
pub use analytics_client::AppInvocation;
|
||||
pub use analytics_client::AppMentionedInput;
|
||||
pub use analytics_client::AppUsedInput;
|
||||
pub use analytics_client::CodexTurnEvent;
|
||||
pub use analytics_client::CodexTurnSteerEvent;
|
||||
pub use analytics_client::CustomAnalyticsFact;
|
||||
pub use analytics_client::InitializationMode;
|
||||
@@ -16,10 +15,8 @@ pub use analytics_client::PluginStateChangedInput;
|
||||
pub use analytics_client::PluginUsedInput;
|
||||
pub use analytics_client::SkillInvocation;
|
||||
pub use analytics_client::SkillInvokedInput;
|
||||
pub use analytics_client::ThreadInitializedInput;
|
||||
pub use analytics_client::TrackEventsContext;
|
||||
pub use analytics_client::TurnCompletedInput;
|
||||
pub use analytics_client::TurnStartedInput;
|
||||
pub use analytics_client::TurnResolvedConfigFact;
|
||||
pub use analytics_client::TurnStatus;
|
||||
pub use analytics_client::TurnSteerRejectionReason;
|
||||
pub use analytics_client::TurnSteerResult;
|
||||
|
||||
@@ -30,6 +30,7 @@ axum = { workspace = true, default-features = false, features = [
|
||||
"ws",
|
||||
] }
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-analytics = { workspace = true }
|
||||
codex-cloud-requirements = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::thread_state::ThreadState;
|
||||
use crate::thread_state::TurnSummary;
|
||||
use crate::thread_status::ThreadWatchActiveGuard;
|
||||
use crate::thread_status::ThreadWatchManager;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_app_server_protocol::AccountRateLimitsUpdatedNotification;
|
||||
use codex_app_server_protocol::AdditionalPermissionProfile as V2AdditionalPermissionProfile;
|
||||
use codex_app_server_protocol::AgentMessageDeltaNotification;
|
||||
@@ -257,6 +258,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
conversation_id: ThreadId,
|
||||
conversation: Arc<CodexThread>,
|
||||
thread_manager: Arc<ThreadManager>,
|
||||
analytics_events_client: AnalyticsEventsClient,
|
||||
outgoing: ThreadScopedOutgoingMessageSender,
|
||||
thread_state: Arc<tokio::sync::Mutex<ThreadState>>,
|
||||
thread_watch_manager: ThreadWatchManager,
|
||||
@@ -289,6 +291,8 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn,
|
||||
};
|
||||
analytics_events_client
|
||||
.track_notification(ServerNotification::TurnStarted(notification.clone()));
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::TurnStarted(notification))
|
||||
.await;
|
||||
@@ -301,7 +305,14 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
thread_watch_manager
|
||||
.note_turn_completed(&conversation_id.to_string(), turn_failed)
|
||||
.await;
|
||||
handle_turn_complete(conversation_id, event_turn_id, &outgoing, &thread_state).await;
|
||||
handle_turn_complete(
|
||||
conversation_id,
|
||||
event_turn_id,
|
||||
Some(&analytics_events_client),
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
EventMsg::SkillsUpdateAvailable => {
|
||||
if let ApiVersion::V2 = api_version {
|
||||
@@ -1946,6 +1957,7 @@ async fn emit_turn_completed_with_status(
|
||||
event_turn_id: String,
|
||||
status: TurnStatus,
|
||||
error: Option<TurnError>,
|
||||
analytics_events_client: Option<&AnalyticsEventsClient>,
|
||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||
) {
|
||||
let notification = TurnCompletedNotification {
|
||||
@@ -1957,6 +1969,10 @@ async fn emit_turn_completed_with_status(
|
||||
status,
|
||||
},
|
||||
};
|
||||
if let Some(analytics_events_client) = analytics_events_client {
|
||||
analytics_events_client
|
||||
.track_notification(ServerNotification::TurnCompleted(notification.clone()));
|
||||
}
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::TurnCompleted(notification))
|
||||
.await;
|
||||
@@ -2100,6 +2116,7 @@ async fn find_and_remove_turn_summary(
|
||||
async fn handle_turn_complete(
|
||||
conversation_id: ThreadId,
|
||||
event_turn_id: String,
|
||||
analytics_events_client: Option<&AnalyticsEventsClient>,
|
||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||
thread_state: &Arc<Mutex<ThreadState>>,
|
||||
) {
|
||||
@@ -2110,7 +2127,15 @@ async fn handle_turn_complete(
|
||||
None => (TurnStatus::Completed, None),
|
||||
};
|
||||
|
||||
emit_turn_completed_with_status(conversation_id, event_turn_id, status, error, outgoing).await;
|
||||
emit_turn_completed_with_status(
|
||||
conversation_id,
|
||||
event_turn_id,
|
||||
status,
|
||||
error,
|
||||
analytics_events_client,
|
||||
outgoing,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn handle_turn_interrupted(
|
||||
@@ -2126,6 +2151,7 @@ async fn handle_turn_interrupted(
|
||||
event_turn_id,
|
||||
TurnStatus::Interrupted,
|
||||
/*error*/ None,
|
||||
None,
|
||||
outgoing,
|
||||
)
|
||||
.await;
|
||||
@@ -3280,6 +3306,7 @@ mod tests {
|
||||
handle_turn_complete(
|
||||
conversation_id,
|
||||
event_turn_id.clone(),
|
||||
None,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -3368,6 +3395,7 @@ mod tests {
|
||||
handle_turn_complete(
|
||||
conversation_id,
|
||||
event_turn_id.clone(),
|
||||
None,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -3628,7 +3656,14 @@ mod tests {
|
||||
&thread_state,
|
||||
)
|
||||
.await;
|
||||
handle_turn_complete(conversation_a, a_turn1.clone(), &outgoing, &thread_state).await;
|
||||
handle_turn_complete(
|
||||
conversation_a,
|
||||
a_turn1.clone(),
|
||||
None,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Turn 1 on conversation B
|
||||
let b_turn1 = "b_turn1".to_string();
|
||||
@@ -3642,11 +3677,25 @@ mod tests {
|
||||
&thread_state,
|
||||
)
|
||||
.await;
|
||||
handle_turn_complete(conversation_b, b_turn1.clone(), &outgoing, &thread_state).await;
|
||||
handle_turn_complete(
|
||||
conversation_b,
|
||||
b_turn1.clone(),
|
||||
None,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Turn 2 on conversation A
|
||||
let a_turn2 = "a_turn2".to_string();
|
||||
handle_turn_complete(conversation_a, a_turn2.clone(), &outgoing, &thread_state).await;
|
||||
handle_turn_complete(
|
||||
conversation_a,
|
||||
a_turn2.clone(),
|
||||
None,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Verify: A turn 1
|
||||
let msg = recv_broadcast_message(&mut rx).await?;
|
||||
|
||||
@@ -32,6 +32,7 @@ use codex_app_server_protocol::CancelLoginAccountParams;
|
||||
use codex_app_server_protocol::CancelLoginAccountResponse;
|
||||
use codex_app_server_protocol::CancelLoginAccountStatus;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ClientResponse;
|
||||
use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
|
||||
use codex_app_server_protocol::CollaborationModeListParams;
|
||||
use codex_app_server_protocol::CollaborationModeListResponse;
|
||||
@@ -185,13 +186,11 @@ use codex_core::CodexAuth;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::Cursor as RolloutCursor;
|
||||
use codex_core::ForkSnapshot;
|
||||
use codex_core::InitializationMode;
|
||||
use codex_core::NewThread;
|
||||
use codex_core::RolloutRecorder;
|
||||
use codex_core::SessionMeta;
|
||||
use codex_core::SteerInputError;
|
||||
use codex_core::ThreadConfigSnapshot;
|
||||
use codex_core::ThreadInitializedInput;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::ThreadSortKey as CoreThreadSortKey;
|
||||
use codex_core::auth::AuthMode as CoreAuthMode;
|
||||
@@ -2170,12 +2169,13 @@ impl CodexMessageProcessor {
|
||||
};
|
||||
listener_task_context
|
||||
.analytics_events_client
|
||||
.track_thread_initialized(thread_initialize_input(
|
||||
request_id.connection_id,
|
||||
&thread,
|
||||
response.model.clone(),
|
||||
InitializationMode::New,
|
||||
));
|
||||
.track_response(
|
||||
request_id.connection_id.0,
|
||||
ClientResponse::ThreadStart {
|
||||
request_id: request_id.request_id.clone(),
|
||||
response: response.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
listener_task_context
|
||||
.outgoing
|
||||
@@ -3654,13 +3654,13 @@ impl CodexMessageProcessor {
|
||||
sandbox: session_configured.sandbox_policy.into(),
|
||||
reasoning_effort: session_configured.reasoning_effort,
|
||||
};
|
||||
self.analytics_events_client
|
||||
.track_thread_initialized(thread_initialize_input(
|
||||
request_id.connection_id,
|
||||
&response.thread,
|
||||
response.model.clone(),
|
||||
InitializationMode::Resumed,
|
||||
));
|
||||
self.analytics_events_client.track_response(
|
||||
request_id.connection_id.0,
|
||||
ClientResponse::ThreadResume {
|
||||
request_id: request_id.request_id.clone(),
|
||||
response: response.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
}
|
||||
@@ -4268,13 +4268,13 @@ impl CodexMessageProcessor {
|
||||
sandbox: session_configured.sandbox_policy.into(),
|
||||
reasoning_effort: session_configured.reasoning_effort,
|
||||
};
|
||||
self.analytics_events_client
|
||||
.track_thread_initialized(thread_initialize_input(
|
||||
request_id.connection_id,
|
||||
&thread,
|
||||
response.model.clone(),
|
||||
InitializationMode::Forked,
|
||||
));
|
||||
self.analytics_events_client.track_response(
|
||||
request_id.connection_id.0,
|
||||
ClientResponse::ThreadFork {
|
||||
request_id: request_id.request_id.clone(),
|
||||
response: response.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
|
||||
@@ -6179,6 +6179,16 @@ impl CodexMessageProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
let analytics_turn_start_params = params.clone();
|
||||
self.analytics_events_client.track_request(
|
||||
request_id.connection_id.0,
|
||||
request_id.request_id.clone(),
|
||||
ClientRequest::TurnStart {
|
||||
request_id: request_id.request_id.clone(),
|
||||
params: analytics_turn_start_params,
|
||||
},
|
||||
);
|
||||
|
||||
let collaboration_modes_config = CollaborationModesConfig {
|
||||
default_mode_request_user_input: thread.enabled(Feature::DefaultModeRequestUserInput),
|
||||
};
|
||||
@@ -6254,6 +6264,13 @@ impl CodexMessageProcessor {
|
||||
};
|
||||
|
||||
let response = TurnStartResponse { turn };
|
||||
self.analytics_events_client.track_response(
|
||||
request_id.connection_id.0,
|
||||
ClientResponse::TurnStart {
|
||||
request_id: request_id.request_id.clone(),
|
||||
response: response.clone(),
|
||||
},
|
||||
);
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -6976,7 +6993,7 @@ impl CodexMessageProcessor {
|
||||
outgoing,
|
||||
thread_manager,
|
||||
thread_state_manager,
|
||||
analytics_events_client: _,
|
||||
analytics_events_client,
|
||||
thread_watch_manager,
|
||||
fallback_model_provider,
|
||||
codex_home,
|
||||
@@ -7023,6 +7040,7 @@ impl CodexMessageProcessor {
|
||||
conversation_id,
|
||||
conversation.clone(),
|
||||
thread_manager.clone(),
|
||||
analytics_events_client.clone(),
|
||||
thread_outgoing,
|
||||
thread_state.clone(),
|
||||
thread_watch_manager.clone(),
|
||||
@@ -7894,22 +7912,6 @@ fn cloud_requirements_load_error(err: &std::io::Error) -> Option<&CloudRequireme
|
||||
None
|
||||
}
|
||||
|
||||
fn thread_initialize_input(
|
||||
connection_id: ConnectionId,
|
||||
thread: &Thread,
|
||||
model: String,
|
||||
initialization_mode: InitializationMode,
|
||||
) -> ThreadInitializedInput {
|
||||
ThreadInitializedInput {
|
||||
connection_id: connection_id.0,
|
||||
thread_id: thread.id.clone(),
|
||||
model,
|
||||
ephemeral: thread.ephemeral,
|
||||
session_source: thread.source.clone().into(),
|
||||
initialization_mode,
|
||||
}
|
||||
}
|
||||
|
||||
fn config_load_error(err: &std::io::Error) -> JSONRPCErrorError {
|
||||
let data = cloud_requirements_load_error(err).map(|cloud_error| {
|
||||
let mut data = serde_json::json!({
|
||||
|
||||
@@ -78,3 +78,31 @@ model_provider = "{model_provider_id}"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn write_mock_responses_config_toml_with_chatgpt_base_url(
|
||||
codex_home: &Path,
|
||||
server_uri: &str,
|
||||
chatgpt_base_url: &str,
|
||||
) -> std::io::Result<()> {
|
||||
let config_toml = codex_home.join("config.toml");
|
||||
std::fs::write(
|
||||
config_toml,
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "read-only"
|
||||
chatgpt_base_url = "{chatgpt_base_url}"
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "{server_uri}/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ pub use auth_fixtures::encode_id_token;
|
||||
pub use auth_fixtures::write_chatgpt_auth;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
pub use config::write_mock_responses_config_toml;
|
||||
pub use config::write_mock_responses_config_toml_with_chatgpt_base_url;
|
||||
pub use core_test_support::format_with_current_shell;
|
||||
pub use core_test_support::format_with_current_shell_display;
|
||||
pub use core_test_support::format_with_current_shell_display_non_login;
|
||||
|
||||
@@ -120,6 +120,41 @@ pub(crate) async fn wait_for_analytics_payload(
|
||||
serde_json::from_slice(&body).map_err(|err| anyhow::anyhow!("invalid analytics payload: {err}"))
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_for_analytics_event(
|
||||
server: &MockServer,
|
||||
read_timeout: Duration,
|
||||
event_type: &str,
|
||||
) -> Result<Value> {
|
||||
timeout(read_timeout, async {
|
||||
loop {
|
||||
let Some(requests) = server.received_requests().await else {
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
continue;
|
||||
};
|
||||
for request in &requests {
|
||||
if request.method != "POST"
|
||||
|| request.url.path() != "/codex/analytics-events/events"
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let payload: Value = serde_json::from_slice(&request.body)
|
||||
.map_err(|err| anyhow::anyhow!("invalid analytics payload: {err}"))?;
|
||||
let Some(events) = payload["events"].as_array() else {
|
||||
continue;
|
||||
};
|
||||
if let Some(event) = events
|
||||
.iter()
|
||||
.find(|event| event["event_type"] == event_type)
|
||||
{
|
||||
return Ok::<Value, anyhow::Error>(event.clone());
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
pub(crate) fn thread_initialized_event(payload: &Value) -> Result<&Value> {
|
||||
let events = payload["events"]
|
||||
.as_array()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::DEFAULT_CLIENT_NAME;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::create_apply_patch_sse_response;
|
||||
use app_test_support::create_exec_command_sse_response;
|
||||
@@ -9,6 +10,7 @@ use app_test_support::create_mock_responses_server_sequence_unchecked;
|
||||
use app_test_support::create_shell_command_sse_response;
|
||||
use app_test_support::format_with_current_shell_display;
|
||||
use app_test_support::to_response;
|
||||
use app_test_support::write_mock_responses_config_toml_with_chatgpt_base_url;
|
||||
use codex_app_server::INPUT_TOO_LARGE_ERROR_CODE;
|
||||
use codex_app_server::INVALID_PARAMS_ERROR_CODE;
|
||||
use codex_app_server_protocol::ByteRange;
|
||||
@@ -64,6 +66,9 @@ use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use super::analytics::enable_analytics_capture;
|
||||
use super::analytics::wait_for_analytics_event;
|
||||
|
||||
#[cfg(windows)]
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(25);
|
||||
#[cfg(not(windows))]
|
||||
@@ -238,6 +243,75 @@ async fn turn_start_emits_user_message_item_with_text_elements() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_tracks_turn_event_analytics() -> Result<()> {
|
||||
let responses = vec![create_final_assistant_message_sse_response("Done")?];
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
write_mock_responses_config_toml_with_chatgpt_base_url(
|
||||
codex_home.path(),
|
||||
&server.uri(),
|
||||
&server.uri(),
|
||||
)?;
|
||||
enable_analytics_capture(&server, codex_home.path()).await?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![V2UserInput::Image {
|
||||
url: "https://example.com/a.png".to_string(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let event = wait_for_analytics_event(&server, DEFAULT_READ_TIMEOUT, "codex_turn_event").await?;
|
||||
assert_eq!(event["event_params"]["thread_id"], thread.id);
|
||||
assert_eq!(event["event_params"]["turn_id"], turn.id);
|
||||
assert_eq!(
|
||||
event["event_params"]["product_client_id"],
|
||||
DEFAULT_CLIENT_NAME
|
||||
);
|
||||
assert_eq!(event["event_params"]["model"], "mock-model");
|
||||
assert_eq!(event["event_params"]["model_provider"], "mock_provider");
|
||||
assert_eq!(event["event_params"]["sandbox_policy"], "read_only");
|
||||
assert_eq!(event["event_params"]["num_input_images"], 1);
|
||||
assert_eq!(event["event_params"]["status"], "completed");
|
||||
assert!(event["event_params"]["started_at"].as_u64().is_some());
|
||||
assert!(event["event_params"]["completed_at"].as_u64().is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_accepts_text_at_limit_with_mention_item() -> Result<()> {
|
||||
let responses = vec![create_final_assistant_message_sse_response("Done")?];
|
||||
|
||||
@@ -363,10 +363,10 @@ use crate::util::backoff;
|
||||
use crate::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_analytics::AppInvocation;
|
||||
use codex_analytics::CodexTurnEvent;
|
||||
use codex_analytics::CodexTurnSteerEvent;
|
||||
use codex_analytics::InvocationType;
|
||||
use codex_analytics::TrackEventsContext;
|
||||
use codex_analytics::TurnResolvedConfigFact;
|
||||
use codex_analytics::TurnSteerRejectionReason;
|
||||
use codex_analytics::TurnSteerResult;
|
||||
use codex_analytics::build_track_events_context;
|
||||
@@ -5936,10 +5936,12 @@ pub(crate) async fn run_turn(
|
||||
let mut state = sess.state.lock().await;
|
||||
state.take_next_turn_is_first()
|
||||
};
|
||||
sess.services.analytics_events_client.track_turn_started(
|
||||
tracking.clone(),
|
||||
CodexTurnEvent {
|
||||
sess.services
|
||||
.analytics_events_client
|
||||
.track_turn_resolved_config(TurnResolvedConfigFact {
|
||||
turn_id: tracking.turn_id.clone(),
|
||||
submission_type: None,
|
||||
model: turn_context.model_info.slug.clone(),
|
||||
model_provider: turn_context.config.model_provider_id.clone(),
|
||||
sandbox_policy: turn_context.sandbox_policy.get().clone(),
|
||||
reasoning_effort: turn_context.reasoning_effort,
|
||||
@@ -5950,34 +5952,8 @@ pub(crate) async fn run_turn(
|
||||
sandbox_network_access: turn_context.network_sandbox_policy.is_enabled(),
|
||||
collaboration_mode: turn_context.collaboration_mode.mode,
|
||||
personality: turn_context.personality,
|
||||
num_input_images: input
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
matches!(item, UserInput::Image { .. } | UserInput::LocalImage { .. })
|
||||
})
|
||||
.count(),
|
||||
is_first_turn,
|
||||
status: None,
|
||||
turn_error: None,
|
||||
steer_count: None,
|
||||
total_tool_call_count: None,
|
||||
shell_command_count: None,
|
||||
file_change_count: None,
|
||||
mcp_tool_call_count: None,
|
||||
dynamic_tool_call_count: None,
|
||||
subagent_tool_call_count: None,
|
||||
web_search_count: None,
|
||||
image_generation_count: None,
|
||||
input_tokens: None,
|
||||
cached_input_tokens: None,
|
||||
output_tokens: None,
|
||||
reasoning_output_tokens: None,
|
||||
total_tokens: None,
|
||||
duration_ms: None,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let skills_outcome = Some(turn_context.turn_skills.outcome.as_ref());
|
||||
@@ -6261,13 +6237,6 @@ pub(crate) async fn run_turn(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !input.is_empty() {
|
||||
sess.services
|
||||
.analytics_events_client
|
||||
.track_turn_completed(tracking.turn_id.clone());
|
||||
}
|
||||
|
||||
last_agent_message
|
||||
}
|
||||
|
||||
|
||||
@@ -136,8 +136,6 @@ pub use auth::CodexAuth;
|
||||
pub use codex_analytics::AnalyticsEventsClient;
|
||||
pub use codex_analytics::AnalyticsFact;
|
||||
pub use codex_analytics::CustomAnalyticsFact;
|
||||
pub use codex_analytics::InitializationMode;
|
||||
pub use codex_analytics::ThreadInitializedInput;
|
||||
mod default_client_forwarding;
|
||||
|
||||
/// Default Codex HTTP client headers and reqwest construction.
|
||||
|
||||
Reference in New Issue
Block a user