mirror of
https://github.com/openai/codex.git
synced 2026-05-01 03:42:05 +03:00
[codex-analytics] denormalize thread metadata onto turn events
This commit is contained in:
@@ -8,7 +8,6 @@ use crate::events::CodexPluginUsedEventRequest;
|
||||
use crate::events::CodexRuntimeMetadata;
|
||||
use crate::events::CodexTurnEventRequest;
|
||||
use crate::events::CodexTurnSteerEventRequest;
|
||||
use crate::events::ThreadInitializationMode;
|
||||
use crate::events::ThreadInitializedEvent;
|
||||
use crate::events::ThreadInitializedEventParams;
|
||||
use crate::events::TrackEventRequest;
|
||||
@@ -30,6 +29,7 @@ use crate::facts::PluginUsedInput;
|
||||
use crate::facts::SkillInvocation;
|
||||
use crate::facts::SkillInvokedInput;
|
||||
use crate::facts::SubAgentThreadStartedInput;
|
||||
use crate::facts::ThreadInitializationMode;
|
||||
use crate::facts::TrackEventsContext;
|
||||
use crate::facts::TurnResolvedConfigFact;
|
||||
use crate::facts::TurnStatus;
|
||||
@@ -73,6 +73,7 @@ use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -234,6 +235,9 @@ fn sample_turn_resolved_config(turn_id: &str) -> TurnResolvedConfigFact {
|
||||
thread_id: "thread-2".to_string(),
|
||||
num_input_images: 1,
|
||||
submission_type: Some(TurnSubmissionType::Default),
|
||||
ephemeral: false,
|
||||
session_source: SessionSource::Exec,
|
||||
initialization_mode: ThreadInitializationMode::New,
|
||||
model: "gpt-5".to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
@@ -252,8 +256,8 @@ fn sample_turn_resolved_config(turn_id: &str) -> TurnResolvedConfigFact {
|
||||
fn sample_app_server_client_metadata() -> CodexAppServerClientMetadata {
|
||||
CodexAppServerClientMetadata {
|
||||
product_client_id: "codex-tui".to_string(),
|
||||
client_name: Some("codex-tui".to_string()),
|
||||
client_version: Some("1.0.0".to_string()),
|
||||
client_name: "codex-tui".to_string(),
|
||||
client_version: "1.0.0".to_string(),
|
||||
rpc_transport: AppServerRpcTransport::Stdio,
|
||||
experimental_api_enabled: None,
|
||||
}
|
||||
@@ -535,8 +539,8 @@ fn thread_initialized_event_serializes_expected_shape() {
|
||||
thread_id: "thread-0".to_string(),
|
||||
app_server_client: CodexAppServerClientMetadata {
|
||||
product_client_id: DEFAULT_ORIGINATOR.to_string(),
|
||||
client_name: Some("codex-tui".to_string()),
|
||||
client_version: Some("1.0.0".to_string()),
|
||||
client_name: "codex-tui".to_string(),
|
||||
client_version: "1.0.0".to_string(),
|
||||
rpc_transport: AppServerRpcTransport::Stdio,
|
||||
experimental_api_enabled: Some(true),
|
||||
},
|
||||
@@ -1073,8 +1077,14 @@ fn turn_event_serializes_expected_shape() {
|
||||
event_params: crate::events::CodexTurnEventParams {
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
product_client_id: "codex-tui".to_string(),
|
||||
app_server_client: sample_app_server_client_metadata(),
|
||||
runtime: sample_runtime_metadata(),
|
||||
submission_type: Some(TurnSubmissionType::Default),
|
||||
ephemeral: false,
|
||||
thread_source: Some("user".to_string()),
|
||||
initialization_mode: ThreadInitializationMode::New,
|
||||
subagent_source: None,
|
||||
parent_thread_id: None,
|
||||
model: Some("gpt-5".to_string()),
|
||||
model_provider: "openai".to_string(),
|
||||
sandbox_policy: Some("read_only"),
|
||||
@@ -1111,51 +1121,97 @@ fn turn_event_serializes_expected_shape() {
|
||||
}));
|
||||
|
||||
let payload = serde_json::to_value(&event).expect("serialize 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,
|
||||
payload["event_params"]["app_server_client"],
|
||||
json!({
|
||||
"event_type": "codex_turn_event",
|
||||
"event_params": {
|
||||
"thread_id": "thread-2",
|
||||
"turn_id": "turn-2",
|
||||
"product_client_id": "codex-tui",
|
||||
"submission_type": "default",
|
||||
"model": "gpt-5",
|
||||
"model_provider": "openai",
|
||||
"sandbox_policy": "read_only",
|
||||
"reasoning_effort": "high",
|
||||
"reasoning_summary": "detailed",
|
||||
"service_tier": "flex",
|
||||
"approval_policy": "on-request",
|
||||
"approvals_reviewer": "guardian_subagent",
|
||||
"sandbox_network_access": true,
|
||||
"collaboration_mode": "plan",
|
||||
"personality": "pragmatic",
|
||||
"num_input_images": 2,
|
||||
"is_first_turn": true,
|
||||
"status": "completed",
|
||||
"turn_error": null,
|
||||
"steer_count": 0,
|
||||
"total_tool_call_count": null,
|
||||
"shell_command_count": null,
|
||||
"file_change_count": null,
|
||||
"mcp_tool_call_count": null,
|
||||
"dynamic_tool_call_count": null,
|
||||
"subagent_tool_call_count": null,
|
||||
"web_search_count": null,
|
||||
"image_generation_count": null,
|
||||
"input_tokens": null,
|
||||
"cached_input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"reasoning_output_tokens": null,
|
||||
"total_tokens": null,
|
||||
"duration_ms": 1234,
|
||||
"started_at": 455,
|
||||
"completed_at": 456
|
||||
}
|
||||
"product_client_id": "codex-tui",
|
||||
"client_name": "codex-tui",
|
||||
"client_version": "1.0.0",
|
||||
"rpc_transport": "stdio",
|
||||
"experimental_api_enabled": null,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
payload["event_params"]["runtime"],
|
||||
json!({
|
||||
"codex_rs_version": "0.1.0",
|
||||
"runtime_os": "macos",
|
||||
"runtime_os_version": "15.3.1",
|
||||
"runtime_arch": "aarch64",
|
||||
})
|
||||
);
|
||||
assert!(payload["event_params"].get("product_client_id").is_none());
|
||||
assert_eq!(payload["event_params"]["submission_type"], json!("default"));
|
||||
assert_eq!(payload["event_params"]["ephemeral"], json!(false));
|
||||
assert_eq!(payload["event_params"]["thread_source"], json!("user"));
|
||||
assert_eq!(payload["event_params"]["initialization_mode"], json!("new"));
|
||||
assert_eq!(payload["event_params"]["subagent_source"], json!(null));
|
||||
assert_eq!(payload["event_params"]["parent_thread_id"], json!(null));
|
||||
assert_eq!(payload["event_params"]["model"], json!("gpt-5"));
|
||||
assert_eq!(payload["event_params"]["model_provider"], json!("openai"));
|
||||
assert_eq!(
|
||||
payload["event_params"]["sandbox_policy"],
|
||||
json!("read_only")
|
||||
);
|
||||
assert_eq!(payload["event_params"]["reasoning_effort"], json!("high"));
|
||||
assert_eq!(
|
||||
payload["event_params"]["reasoning_summary"],
|
||||
json!("detailed")
|
||||
);
|
||||
assert_eq!(payload["event_params"]["service_tier"], json!("flex"));
|
||||
assert_eq!(
|
||||
payload["event_params"]["approval_policy"],
|
||||
json!("on-request")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["event_params"]["approvals_reviewer"],
|
||||
json!("guardian_subagent")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["event_params"]["sandbox_network_access"],
|
||||
json!(true)
|
||||
);
|
||||
assert_eq!(payload["event_params"]["collaboration_mode"], json!("plan"));
|
||||
assert_eq!(payload["event_params"]["personality"], json!("pragmatic"));
|
||||
assert_eq!(payload["event_params"]["num_input_images"], json!(2));
|
||||
assert_eq!(payload["event_params"]["is_first_turn"], json!(true));
|
||||
assert_eq!(payload["event_params"]["status"], json!("completed"));
|
||||
assert_eq!(payload["event_params"]["turn_error"], json!(null));
|
||||
assert_eq!(payload["event_params"]["steer_count"], json!(0));
|
||||
assert_eq!(
|
||||
payload["event_params"]["total_tool_call_count"],
|
||||
json!(null)
|
||||
);
|
||||
assert_eq!(payload["event_params"]["shell_command_count"], json!(null));
|
||||
assert_eq!(payload["event_params"]["file_change_count"], json!(null));
|
||||
assert_eq!(payload["event_params"]["mcp_tool_call_count"], json!(null));
|
||||
assert_eq!(
|
||||
payload["event_params"]["dynamic_tool_call_count"],
|
||||
json!(null)
|
||||
);
|
||||
assert_eq!(
|
||||
payload["event_params"]["subagent_tool_call_count"],
|
||||
json!(null)
|
||||
);
|
||||
assert_eq!(payload["event_params"]["web_search_count"], json!(null));
|
||||
assert_eq!(
|
||||
payload["event_params"]["image_generation_count"],
|
||||
json!(null)
|
||||
);
|
||||
assert_eq!(payload["event_params"]["input_tokens"], json!(null));
|
||||
assert_eq!(payload["event_params"]["cached_input_tokens"], json!(null));
|
||||
assert_eq!(payload["event_params"]["output_tokens"], json!(null));
|
||||
assert_eq!(
|
||||
payload["event_params"]["reasoning_output_tokens"],
|
||||
json!(null)
|
||||
);
|
||||
assert_eq!(payload["event_params"]["total_tokens"], json!(null));
|
||||
assert_eq!(payload["event_params"]["duration_ms"], json!(1234));
|
||||
assert_eq!(payload["event_params"]["started_at"], json!(455));
|
||||
assert_eq!(payload["event_params"]["completed_at"], json!(456));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1464,9 +1520,30 @@ async fn turn_lifecycle_emits_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")
|
||||
payload["event_params"]["app_server_client"],
|
||||
json!({
|
||||
"product_client_id": "codex-tui",
|
||||
"client_name": "codex-tui",
|
||||
"client_version": "1.0.0",
|
||||
"rpc_transport": "stdio",
|
||||
"experimental_api_enabled": null,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
payload["event_params"]["runtime"],
|
||||
json!({
|
||||
"codex_rs_version": "0.1.0",
|
||||
"runtime_os": "macos",
|
||||
"runtime_os_version": "15.3.1",
|
||||
"runtime_arch": "aarch64",
|
||||
})
|
||||
);
|
||||
assert!(payload["event_params"].get("product_client_id").is_none());
|
||||
assert_eq!(payload["event_params"]["ephemeral"], json!(false));
|
||||
assert_eq!(payload["event_params"]["thread_source"], json!("user"));
|
||||
assert_eq!(payload["event_params"]["initialization_mode"], json!("new"));
|
||||
assert_eq!(payload["event_params"]["subagent_source"], json!(null));
|
||||
assert_eq!(payload["event_params"]["parent_thread_id"], json!(null));
|
||||
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));
|
||||
@@ -1664,6 +1741,73 @@ async fn queued_submission_type_emits_queued_turn_event() {
|
||||
assert_eq!(payload["event_params"]["submission_type"], json!("queued"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_event_includes_subagent_thread_metadata() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut out = Vec::new();
|
||||
|
||||
ingest_turn_prerequisites(
|
||||
&mut reducer,
|
||||
&mut out,
|
||||
/*include_initialize*/ true,
|
||||
/*include_resolved_config*/ false,
|
||||
/*include_started*/ true,
|
||||
/*include_token_usage*/ false,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut resolved_config = sample_turn_resolved_config("turn-2");
|
||||
resolved_config.ephemeral = true;
|
||||
let parent_thread_id =
|
||||
codex_protocol::ThreadId::from_string("11111111-1111-1111-1111-111111111111")
|
||||
.expect("valid thread id");
|
||||
resolved_config.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: Some("worker".to_string()),
|
||||
agent_role: None,
|
||||
});
|
||||
resolved_config.initialization_mode = ThreadInitializationMode::Forked;
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::TurnResolvedConfig(Box::new(
|
||||
resolved_config,
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(sample_turn_completed_notification(
|
||||
"thread-2",
|
||||
"turn-2",
|
||||
AppServerTurnStatus::Completed,
|
||||
/*codex_error_info*/ None,
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(out.len(), 1);
|
||||
let payload = serde_json::to_value(&out[0]).expect("serialize turn event");
|
||||
assert_eq!(payload["event_params"]["ephemeral"], json!(true));
|
||||
assert_eq!(payload["event_params"]["thread_source"], json!("subagent"));
|
||||
assert_eq!(
|
||||
payload["event_params"]["initialization_mode"],
|
||||
json!("forked")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["event_params"]["subagent_source"],
|
||||
json!("thread_spawn")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["event_params"]["parent_thread_id"],
|
||||
json!("11111111-1111-1111-1111-111111111111")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_does_not_emit_without_required_prerequisites() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
@@ -1689,12 +1833,7 @@ async fn turn_does_not_emit_without_required_prerequisites() {
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(out.len(), 1);
|
||||
let payload = serde_json::to_value(&out[0]).expect("serialize turn event");
|
||||
assert_eq!(
|
||||
payload["event_params"]["product_client_id"],
|
||||
json!(originator().value)
|
||||
);
|
||||
assert!(out.is_empty());
|
||||
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut out = Vec::new();
|
||||
|
||||
Reference in New Issue
Block a user