mirror of
https://github.com/openai/codex.git
synced 2026-05-23 20:44:50 +00:00
## Why Review telemetry should describe reviews as first-class events, not only as counters denormalized onto terminal tool-item events. That lets us analyze guardian and user reviews consistently across command execution, file changes, permissions, and network access, while still preserving the terminal item summaries that existing tool analytics need. To make those review events accurate, analytics also needs the observed completion time for each review and enough command metadata to distinguish `shell` from `unified_exec` reviews. ## What changed - emit generic `codex_review_event` rows for completed user and guardian reviews, with review subjects, reviewer, trigger, terminal status, resolution, and observed duration - reduce approval request / response / abort facts into review events for command execution, file change, and permissions flows - keep denormalized review counts, final approval outcome, and permission-request flags on terminal tool-item events for item-associated reviews - plumb review completion timing so user-review responses and aborts use app-server-observed completion times, while guardian analytics reuse the same terminal timestamps emitted on guardian assessment events - carry command approval `source` through the protocol and app-server layers so review analytics can distinguish `shell` from `unified_exec` - add analytics coverage for user-review emission, guardian-review emission, permission reviews that should not denormalize onto tool items, item-summary isolation across threads, and the serialized review-event shape ## Verification - `cargo test -p codex-analytics` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18748). * __->__ #18748 * #21434 * #18747 * #17090 * #17089 * #20514
1077 lines
36 KiB
Rust
1077 lines
36 KiB
Rust
use std::time::Instant;
|
|
|
|
use crate::facts::AcceptedLineFingerprint;
|
|
use crate::facts::AppInvocation;
|
|
use crate::facts::CodexCompactionEvent;
|
|
use crate::facts::CompactionImplementation;
|
|
use crate::facts::CompactionPhase;
|
|
use crate::facts::CompactionReason;
|
|
use crate::facts::CompactionStatus;
|
|
use crate::facts::CompactionStrategy;
|
|
use crate::facts::CompactionTrigger;
|
|
use crate::facts::HookRunFact;
|
|
use crate::facts::InvocationType;
|
|
use crate::facts::PluginState;
|
|
use crate::facts::SubAgentThreadStartedInput;
|
|
use crate::facts::ThreadInitializationMode;
|
|
use crate::facts::TrackEventsContext;
|
|
use crate::facts::TurnStatus;
|
|
use crate::facts::TurnSteerRejectionReason;
|
|
use crate::facts::TurnSteerResult;
|
|
use crate::facts::TurnSubmissionType;
|
|
use crate::now_unix_millis;
|
|
use codex_app_server_protocol::CodexErrorInfo;
|
|
use codex_app_server_protocol::CommandExecutionSource;
|
|
use codex_login::default_client::originator;
|
|
use codex_plugin::PluginTelemetryMetadata;
|
|
use codex_protocol::approvals::NetworkApprovalProtocol;
|
|
use codex_protocol::models::AdditionalPermissionProfile;
|
|
use codex_protocol::models::SandboxPermissions;
|
|
use codex_protocol::protocol::GuardianAssessmentOutcome;
|
|
use codex_protocol::protocol::GuardianCommandSource;
|
|
use codex_protocol::protocol::GuardianRiskLevel;
|
|
use codex_protocol::protocol::GuardianUserAuthorization;
|
|
use codex_protocol::protocol::HookEventName;
|
|
use codex_protocol::protocol::HookRunStatus;
|
|
use codex_protocol::protocol::HookSource;
|
|
use codex_protocol::protocol::SubAgentSource;
|
|
use codex_protocol::protocol::ThreadSource;
|
|
use codex_protocol::protocol::TokenUsage;
|
|
use serde::Serialize;
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum AppServerRpcTransport {
|
|
Stdio,
|
|
Websocket,
|
|
InProcess,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct TrackEventsRequest {
|
|
pub(crate) events: Vec<TrackEventRequest>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(untagged)]
|
|
pub(crate) enum TrackEventRequest {
|
|
SkillInvocation(SkillInvocationEventRequest),
|
|
ThreadInitialized(ThreadInitializedEvent),
|
|
GuardianReview(Box<GuardianReviewEventRequest>),
|
|
AppMentioned(CodexAppMentionedEventRequest),
|
|
AppUsed(CodexAppUsedEventRequest),
|
|
HookRun(CodexHookRunEventRequest),
|
|
Compaction(Box<CodexCompactionEventRequest>),
|
|
TurnEvent(Box<CodexTurnEventRequest>),
|
|
TurnSteer(CodexTurnSteerEventRequest),
|
|
CommandExecution(CodexCommandExecutionEventRequest),
|
|
FileChange(CodexFileChangeEventRequest),
|
|
McpToolCall(CodexMcpToolCallEventRequest),
|
|
DynamicToolCall(CodexDynamicToolCallEventRequest),
|
|
CollabAgentToolCall(CodexCollabAgentToolCallEventRequest),
|
|
WebSearch(CodexWebSearchEventRequest),
|
|
ImageGeneration(CodexImageGenerationEventRequest),
|
|
AcceptedLineFingerprints(Box<CodexAcceptedLineFingerprintsEventRequest>),
|
|
#[allow(dead_code)]
|
|
ReviewEvent(CodexReviewEventRequest),
|
|
PluginUsed(CodexPluginUsedEventRequest),
|
|
PluginInstalled(CodexPluginEventRequest),
|
|
PluginUninstalled(CodexPluginEventRequest),
|
|
PluginEnabled(CodexPluginEventRequest),
|
|
PluginDisabled(CodexPluginEventRequest),
|
|
}
|
|
|
|
impl TrackEventRequest {
|
|
pub(crate) fn should_send_in_isolated_request(&self) -> bool {
|
|
matches!(self, Self::AcceptedLineFingerprints(_))
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexAcceptedLineFingerprintsEventParams {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) turn_id: String,
|
|
pub(crate) thread_id: String,
|
|
pub(crate) product_surface: Option<String>,
|
|
pub(crate) model_slug: Option<String>,
|
|
pub(crate) completed_at: u64,
|
|
pub(crate) repo_hash: Option<String>,
|
|
pub(crate) accepted_added_lines: u64,
|
|
pub(crate) accepted_deleted_lines: u64,
|
|
pub(crate) line_fingerprints: Vec<AcceptedLineFingerprint>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexAcceptedLineFingerprintsEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexAcceptedLineFingerprintsEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct SkillInvocationEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) skill_id: String,
|
|
pub(crate) skill_name: String,
|
|
pub(crate) event_params: SkillInvocationEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct SkillInvocationEventParams {
|
|
pub(crate) product_client_id: Option<String>,
|
|
pub(crate) skill_scope: Option<String>,
|
|
pub(crate) plugin_id: Option<String>,
|
|
pub(crate) repo_url: Option<String>,
|
|
pub(crate) thread_id: Option<String>,
|
|
pub(crate) turn_id: Option<String>,
|
|
pub(crate) invoke_type: Option<InvocationType>,
|
|
pub(crate) model_slug: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Serialize)]
|
|
pub(crate) struct CodexAppServerClientMetadata {
|
|
pub(crate) product_client_id: String,
|
|
pub(crate) client_name: Option<String>,
|
|
pub(crate) client_version: Option<String>,
|
|
pub(crate) rpc_transport: AppServerRpcTransport,
|
|
pub(crate) experimental_api_enabled: Option<bool>,
|
|
}
|
|
|
|
#[derive(Clone, Serialize)]
|
|
pub(crate) struct CodexRuntimeMetadata {
|
|
pub(crate) codex_rs_version: String,
|
|
pub(crate) runtime_os: String,
|
|
pub(crate) runtime_os_version: String,
|
|
pub(crate) runtime_arch: String,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct ThreadInitializedEventParams {
|
|
pub(crate) thread_id: String,
|
|
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
|
pub(crate) runtime: CodexRuntimeMetadata,
|
|
pub(crate) model: String,
|
|
pub(crate) ephemeral: bool,
|
|
pub(crate) thread_source: Option<ThreadSource>,
|
|
pub(crate) initialization_mode: ThreadInitializationMode,
|
|
pub(crate) subagent_source: Option<String>,
|
|
pub(crate) parent_thread_id: Option<String>,
|
|
pub(crate) created_at: u64,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct ThreadInitializedEvent {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: ThreadInitializedEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct GuardianReviewEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: GuardianReviewEventPayload,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum GuardianReviewDecision {
|
|
Approved,
|
|
Denied,
|
|
Aborted,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum GuardianReviewTerminalStatus {
|
|
Approved,
|
|
Denied,
|
|
Aborted,
|
|
TimedOut,
|
|
FailedClosed,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum GuardianReviewFailureReason {
|
|
Timeout,
|
|
Cancelled,
|
|
PromptBuildError,
|
|
SessionError,
|
|
ParseError,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum GuardianReviewSessionKind {
|
|
TrunkNew,
|
|
TrunkReused,
|
|
EphemeralForked,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum GuardianApprovalRequestSource {
|
|
/// Approval requested directly by the main Codex turn.
|
|
MainTurn,
|
|
/// Approval requested by a delegated subagent and routed through the parent
|
|
/// session for guardian review.
|
|
DelegatedSubagent,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum GuardianReviewedAction {
|
|
Shell {
|
|
sandbox_permissions: SandboxPermissions,
|
|
additional_permissions: Option<AdditionalPermissionProfile>,
|
|
},
|
|
UnifiedExec {
|
|
sandbox_permissions: SandboxPermissions,
|
|
additional_permissions: Option<AdditionalPermissionProfile>,
|
|
tty: bool,
|
|
},
|
|
Execve {
|
|
source: GuardianCommandSource,
|
|
program: String,
|
|
additional_permissions: Option<AdditionalPermissionProfile>,
|
|
},
|
|
ApplyPatch {},
|
|
NetworkAccess {
|
|
protocol: NetworkApprovalProtocol,
|
|
port: u16,
|
|
},
|
|
McpToolCall {
|
|
server: String,
|
|
tool_name: String,
|
|
connector_id: Option<String>,
|
|
connector_name: Option<String>,
|
|
tool_title: Option<String>,
|
|
},
|
|
RequestPermissions {},
|
|
}
|
|
|
|
#[derive(Clone, Serialize)]
|
|
pub struct GuardianReviewEventParams {
|
|
pub thread_id: String,
|
|
pub turn_id: String,
|
|
pub review_id: String,
|
|
pub target_item_id: Option<String>,
|
|
pub approval_request_source: GuardianApprovalRequestSource,
|
|
pub reviewed_action: GuardianReviewedAction,
|
|
pub reviewed_action_truncated: bool,
|
|
pub decision: GuardianReviewDecision,
|
|
pub terminal_status: GuardianReviewTerminalStatus,
|
|
pub failure_reason: Option<GuardianReviewFailureReason>,
|
|
pub risk_level: Option<GuardianRiskLevel>,
|
|
pub user_authorization: Option<GuardianUserAuthorization>,
|
|
pub outcome: Option<GuardianAssessmentOutcome>,
|
|
pub guardian_thread_id: Option<String>,
|
|
pub guardian_session_kind: Option<GuardianReviewSessionKind>,
|
|
pub guardian_model: Option<String>,
|
|
pub guardian_reasoning_effort: Option<String>,
|
|
pub had_prior_review_context: Option<bool>,
|
|
pub review_timeout_ms: u64,
|
|
pub tool_call_count: Option<u64>,
|
|
pub time_to_first_token_ms: Option<u64>,
|
|
pub completion_latency_ms: Option<u64>,
|
|
pub started_at: u64,
|
|
pub completed_at: Option<u64>,
|
|
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 struct GuardianReviewTrackContext {
|
|
thread_id: String,
|
|
turn_id: String,
|
|
review_id: String,
|
|
target_item_id: Option<String>,
|
|
approval_request_source: GuardianApprovalRequestSource,
|
|
reviewed_action: GuardianReviewedAction,
|
|
review_timeout_ms: u64,
|
|
pub started_at_ms: u64,
|
|
started_instant: Instant,
|
|
}
|
|
|
|
impl GuardianReviewTrackContext {
|
|
pub fn new(
|
|
thread_id: String,
|
|
turn_id: String,
|
|
review_id: String,
|
|
target_item_id: Option<String>,
|
|
approval_request_source: GuardianApprovalRequestSource,
|
|
reviewed_action: GuardianReviewedAction,
|
|
review_timeout_ms: u64,
|
|
) -> Self {
|
|
Self {
|
|
thread_id,
|
|
turn_id,
|
|
review_id,
|
|
target_item_id,
|
|
approval_request_source,
|
|
reviewed_action,
|
|
review_timeout_ms,
|
|
started_at_ms: now_unix_millis(),
|
|
started_instant: Instant::now(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn event_params(
|
|
&self,
|
|
result: GuardianReviewAnalyticsResult,
|
|
completed_at_ms: u64,
|
|
) -> GuardianReviewEventParams {
|
|
GuardianReviewEventParams {
|
|
thread_id: self.thread_id.clone(),
|
|
turn_id: self.turn_id.clone(),
|
|
review_id: self.review_id.clone(),
|
|
target_item_id: self.target_item_id.clone(),
|
|
approval_request_source: self.approval_request_source,
|
|
reviewed_action: self.reviewed_action.clone(),
|
|
reviewed_action_truncated: result.reviewed_action_truncated,
|
|
decision: result.decision,
|
|
terminal_status: result.terminal_status,
|
|
failure_reason: result.failure_reason,
|
|
risk_level: result.risk_level,
|
|
user_authorization: result.user_authorization,
|
|
outcome: result.outcome,
|
|
guardian_thread_id: result.guardian_thread_id,
|
|
guardian_session_kind: result.guardian_session_kind,
|
|
guardian_model: result.guardian_model,
|
|
guardian_reasoning_effort: result.guardian_reasoning_effort,
|
|
had_prior_review_context: result.had_prior_review_context,
|
|
review_timeout_ms: self.review_timeout_ms,
|
|
// TODO(rhan-oai): plumb nested Guardian review session tool-call counts.
|
|
tool_call_count: None,
|
|
time_to_first_token_ms: result.time_to_first_token_ms,
|
|
completion_latency_ms: Some(self.started_instant.elapsed().as_millis() as u64),
|
|
started_at: self.started_at_ms / 1_000,
|
|
completed_at: Some(completed_at_ms / 1_000),
|
|
input_tokens: result.token_usage.as_ref().map(|usage| usage.input_tokens),
|
|
cached_input_tokens: result
|
|
.token_usage
|
|
.as_ref()
|
|
.map(|usage| usage.cached_input_tokens),
|
|
output_tokens: result.token_usage.as_ref().map(|usage| usage.output_tokens),
|
|
reasoning_output_tokens: result
|
|
.token_usage
|
|
.as_ref()
|
|
.map(|usage| usage.reasoning_output_tokens),
|
|
total_tokens: result.token_usage.as_ref().map(|usage| usage.total_tokens),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct GuardianReviewAnalyticsResult {
|
|
pub decision: GuardianReviewDecision,
|
|
pub terminal_status: GuardianReviewTerminalStatus,
|
|
pub failure_reason: Option<GuardianReviewFailureReason>,
|
|
pub risk_level: Option<GuardianRiskLevel>,
|
|
pub user_authorization: Option<GuardianUserAuthorization>,
|
|
pub outcome: Option<GuardianAssessmentOutcome>,
|
|
pub guardian_thread_id: Option<String>,
|
|
pub guardian_session_kind: Option<GuardianReviewSessionKind>,
|
|
pub guardian_model: Option<String>,
|
|
pub guardian_reasoning_effort: Option<String>,
|
|
pub had_prior_review_context: Option<bool>,
|
|
pub reviewed_action_truncated: bool,
|
|
pub token_usage: Option<TokenUsage>,
|
|
pub time_to_first_token_ms: Option<u64>,
|
|
}
|
|
|
|
impl GuardianReviewAnalyticsResult {
|
|
pub fn without_session() -> Self {
|
|
Self {
|
|
decision: GuardianReviewDecision::Denied,
|
|
terminal_status: GuardianReviewTerminalStatus::FailedClosed,
|
|
failure_reason: None,
|
|
risk_level: None,
|
|
user_authorization: None,
|
|
outcome: None,
|
|
guardian_thread_id: None,
|
|
guardian_session_kind: None,
|
|
guardian_model: None,
|
|
guardian_reasoning_effort: None,
|
|
had_prior_review_context: None,
|
|
reviewed_action_truncated: false,
|
|
token_usage: None,
|
|
time_to_first_token_ms: None,
|
|
}
|
|
}
|
|
|
|
pub fn from_session(
|
|
guardian_thread_id: String,
|
|
guardian_session_kind: GuardianReviewSessionKind,
|
|
guardian_model: String,
|
|
guardian_reasoning_effort: Option<String>,
|
|
had_prior_review_context: bool,
|
|
) -> Self {
|
|
Self {
|
|
guardian_thread_id: Some(guardian_thread_id),
|
|
guardian_session_kind: Some(guardian_session_kind),
|
|
guardian_model: Some(guardian_model),
|
|
guardian_reasoning_effort,
|
|
had_prior_review_context: Some(had_prior_review_context),
|
|
..Self::without_session()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct GuardianReviewEventPayload {
|
|
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
|
pub(crate) runtime: CodexRuntimeMetadata,
|
|
#[serde(flatten)]
|
|
pub(crate) guardian_review: GuardianReviewEventParams,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub(crate) enum FinalApprovalOutcome {
|
|
Unknown,
|
|
NotNeeded,
|
|
ConfigAllowed,
|
|
PolicyForbidden,
|
|
GuardianApproved,
|
|
GuardianDenied,
|
|
GuardianAborted,
|
|
UserApproved,
|
|
UserApprovedForSession,
|
|
UserDenied,
|
|
UserAborted,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub(crate) enum ToolItemTerminalStatus {
|
|
Completed,
|
|
Failed,
|
|
Rejected,
|
|
Interrupted,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub(crate) enum ToolItemFailureKind {
|
|
ToolError,
|
|
ApprovalDenied,
|
|
ApprovalAborted,
|
|
SandboxDenied,
|
|
PolicyForbidden,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexToolItemEventBase {
|
|
pub(crate) thread_id: String,
|
|
pub(crate) turn_id: String,
|
|
/// App-server ThreadItem.id. For tool-originated items this generally
|
|
/// corresponds to the originating core call_id.
|
|
pub(crate) item_id: String,
|
|
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
|
pub(crate) runtime: CodexRuntimeMetadata,
|
|
pub(crate) thread_source: Option<ThreadSource>,
|
|
pub(crate) subagent_source: Option<String>,
|
|
pub(crate) parent_thread_id: Option<String>,
|
|
pub(crate) tool_name: String,
|
|
pub(crate) started_at_ms: u64,
|
|
pub(crate) completed_at_ms: u64,
|
|
// Observed item lifecycle duration. This may undercount end-to-end execution
|
|
// for tools where app-server only sees part of the upstream flow.
|
|
pub(crate) duration_ms: Option<u64>,
|
|
pub(crate) execution_duration_ms: Option<u64>,
|
|
pub(crate) review_count: u64,
|
|
pub(crate) guardian_review_count: u64,
|
|
pub(crate) user_review_count: u64,
|
|
pub(crate) final_approval_outcome: FinalApprovalOutcome,
|
|
pub(crate) terminal_status: ToolItemTerminalStatus,
|
|
pub(crate) failure_kind: Option<ToolItemFailureKind>,
|
|
pub(crate) requested_additional_permissions: bool,
|
|
pub(crate) requested_network_access: bool,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub(crate) enum ReviewSubjectKind {
|
|
CommandExecution,
|
|
FileChange,
|
|
McpToolCall,
|
|
Permissions,
|
|
NetworkAccess,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub(crate) enum Reviewer {
|
|
Guardian,
|
|
User,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub(crate) enum ReviewTrigger {
|
|
Initial,
|
|
SandboxDenial,
|
|
NetworkPolicyDenial,
|
|
ExecveIntercept,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub(crate) enum ReviewStatus {
|
|
Approved,
|
|
Denied,
|
|
Aborted,
|
|
TimedOut,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub(crate) enum ReviewResolution {
|
|
None,
|
|
SessionApproval,
|
|
ExecPolicyAmendment,
|
|
NetworkPolicyAmendment,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexReviewEventParams {
|
|
pub(crate) thread_id: String,
|
|
pub(crate) turn_id: String,
|
|
pub(crate) item_id: Option<String>,
|
|
pub(crate) review_id: String,
|
|
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
|
pub(crate) runtime: CodexRuntimeMetadata,
|
|
pub(crate) thread_source: Option<ThreadSource>,
|
|
pub(crate) subagent_source: Option<String>,
|
|
pub(crate) parent_thread_id: Option<String>,
|
|
pub(crate) subject_kind: ReviewSubjectKind,
|
|
pub(crate) subject_name: String,
|
|
pub(crate) reviewer: Reviewer,
|
|
pub(crate) trigger: ReviewTrigger,
|
|
pub(crate) status: ReviewStatus,
|
|
pub(crate) resolution: ReviewResolution,
|
|
pub(crate) started_at_ms: u64,
|
|
pub(crate) completed_at_ms: u64,
|
|
pub(crate) duration_ms: Option<u64>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexReviewEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexReviewEventParams,
|
|
}
|
|
#[allow(dead_code)]
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub(crate) enum WebSearchActionKind {
|
|
Search,
|
|
OpenPage,
|
|
FindInPage,
|
|
Other,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexCommandExecutionEventParams {
|
|
#[serde(flatten)]
|
|
pub(crate) base: CodexToolItemEventBase,
|
|
pub(crate) command_execution_source: CommandExecutionSource,
|
|
pub(crate) exit_code: Option<i32>,
|
|
pub(crate) command_total_action_count: u64,
|
|
pub(crate) command_read_action_count: u64,
|
|
pub(crate) command_list_files_action_count: u64,
|
|
pub(crate) command_search_action_count: u64,
|
|
pub(crate) command_unknown_action_count: u64,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexCommandExecutionEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexCommandExecutionEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexFileChangeEventParams {
|
|
#[serde(flatten)]
|
|
pub(crate) base: CodexToolItemEventBase,
|
|
pub(crate) file_change_count: u64,
|
|
pub(crate) file_add_count: u64,
|
|
pub(crate) file_update_count: u64,
|
|
pub(crate) file_delete_count: u64,
|
|
pub(crate) file_move_count: u64,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexFileChangeEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexFileChangeEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexMcpToolCallEventParams {
|
|
#[serde(flatten)]
|
|
pub(crate) base: CodexToolItemEventBase,
|
|
pub(crate) mcp_server_name: String,
|
|
pub(crate) mcp_tool_name: String,
|
|
pub(crate) mcp_error_present: bool,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexMcpToolCallEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexMcpToolCallEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexDynamicToolCallEventParams {
|
|
#[serde(flatten)]
|
|
pub(crate) base: CodexToolItemEventBase,
|
|
pub(crate) dynamic_tool_name: String,
|
|
pub(crate) success: Option<bool>,
|
|
pub(crate) output_content_item_count: Option<u64>,
|
|
pub(crate) output_text_item_count: Option<u64>,
|
|
pub(crate) output_image_item_count: Option<u64>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexDynamicToolCallEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexDynamicToolCallEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexCollabAgentToolCallEventParams {
|
|
#[serde(flatten)]
|
|
pub(crate) base: CodexToolItemEventBase,
|
|
pub(crate) sender_thread_id: String,
|
|
pub(crate) receiver_thread_count: u64,
|
|
pub(crate) receiver_thread_ids: Option<Vec<String>>,
|
|
pub(crate) requested_model: Option<String>,
|
|
pub(crate) requested_reasoning_effort: Option<String>,
|
|
pub(crate) agent_state_count: Option<u64>,
|
|
pub(crate) completed_agent_count: Option<u64>,
|
|
pub(crate) failed_agent_count: Option<u64>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexCollabAgentToolCallEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexCollabAgentToolCallEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexWebSearchEventParams {
|
|
#[serde(flatten)]
|
|
pub(crate) base: CodexToolItemEventBase,
|
|
pub(crate) web_search_action: Option<WebSearchActionKind>,
|
|
pub(crate) query_present: bool,
|
|
pub(crate) query_count: Option<u64>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexWebSearchEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexWebSearchEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexImageGenerationEventParams {
|
|
#[serde(flatten)]
|
|
pub(crate) base: CodexToolItemEventBase,
|
|
pub(crate) revised_prompt_present: bool,
|
|
pub(crate) saved_path_present: bool,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexImageGenerationEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexImageGenerationEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexAppMetadata {
|
|
pub(crate) connector_id: Option<String>,
|
|
pub(crate) thread_id: Option<String>,
|
|
pub(crate) turn_id: Option<String>,
|
|
pub(crate) app_name: Option<String>,
|
|
pub(crate) product_client_id: Option<String>,
|
|
pub(crate) invoke_type: Option<InvocationType>,
|
|
pub(crate) model_slug: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexAppMentionedEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexAppMetadata,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexAppUsedEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexAppMetadata,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexHookRunMetadata {
|
|
pub(crate) thread_id: Option<String>,
|
|
pub(crate) turn_id: Option<String>,
|
|
pub(crate) model_slug: Option<String>,
|
|
pub(crate) hook_name: Option<String>,
|
|
pub(crate) hook_source: Option<&'static str>,
|
|
pub(crate) status: Option<HookRunStatus>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexHookRunEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexHookRunMetadata,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexCompactionEventParams {
|
|
pub(crate) thread_id: String,
|
|
pub(crate) turn_id: String,
|
|
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
|
pub(crate) runtime: CodexRuntimeMetadata,
|
|
pub(crate) thread_source: Option<ThreadSource>,
|
|
pub(crate) subagent_source: Option<String>,
|
|
pub(crate) parent_thread_id: Option<String>,
|
|
pub(crate) trigger: CompactionTrigger,
|
|
pub(crate) reason: CompactionReason,
|
|
pub(crate) implementation: CompactionImplementation,
|
|
pub(crate) phase: CompactionPhase,
|
|
pub(crate) strategy: CompactionStrategy,
|
|
pub(crate) status: CompactionStatus,
|
|
pub(crate) error: Option<String>,
|
|
pub(crate) active_context_tokens_before: i64,
|
|
pub(crate) active_context_tokens_after: i64,
|
|
pub(crate) started_at: u64,
|
|
pub(crate) completed_at: u64,
|
|
pub(crate) duration_ms: Option<u64>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexCompactionEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexCompactionEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexTurnEventParams {
|
|
pub(crate) thread_id: String,
|
|
pub(crate) turn_id: String,
|
|
// TODO(rhan-oai): Populate once queued/default submission type is plumbed from
|
|
// the turn/start callsites instead of always being reported as None.
|
|
pub(crate) submission_type: Option<TurnSubmissionType>,
|
|
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
|
pub(crate) runtime: CodexRuntimeMetadata,
|
|
pub(crate) ephemeral: bool,
|
|
pub(crate) thread_source: Option<ThreadSource>,
|
|
pub(crate) initialization_mode: ThreadInitializationMode,
|
|
pub(crate) subagent_source: Option<String>,
|
|
pub(crate) parent_thread_id: Option<String>,
|
|
pub(crate) model: Option<String>,
|
|
pub(crate) model_provider: String,
|
|
pub(crate) sandbox_policy: Option<&'static str>,
|
|
pub(crate) reasoning_effort: Option<String>,
|
|
pub(crate) reasoning_summary: Option<String>,
|
|
pub(crate) service_tier: String,
|
|
pub(crate) approval_policy: String,
|
|
pub(crate) approvals_reviewer: String,
|
|
pub(crate) sandbox_network_access: bool,
|
|
pub(crate) collaboration_mode: Option<&'static str>,
|
|
pub(crate) personality: Option<String>,
|
|
pub(crate) num_input_images: usize,
|
|
pub(crate) is_first_turn: bool,
|
|
pub(crate) status: Option<TurnStatus>,
|
|
pub(crate) turn_error: Option<CodexErrorInfo>,
|
|
pub(crate) steer_count: Option<usize>,
|
|
pub(crate) total_tool_call_count: Option<usize>,
|
|
pub(crate) shell_command_count: Option<usize>,
|
|
pub(crate) file_change_count: Option<usize>,
|
|
pub(crate) mcp_tool_call_count: Option<usize>,
|
|
pub(crate) dynamic_tool_call_count: Option<usize>,
|
|
pub(crate) subagent_tool_call_count: Option<usize>,
|
|
pub(crate) web_search_count: Option<usize>,
|
|
pub(crate) image_generation_count: Option<usize>,
|
|
pub(crate) input_tokens: Option<i64>,
|
|
pub(crate) cached_input_tokens: Option<i64>,
|
|
pub(crate) output_tokens: Option<i64>,
|
|
pub(crate) reasoning_output_tokens: Option<i64>,
|
|
pub(crate) total_tokens: Option<i64>,
|
|
pub(crate) duration_ms: Option<u64>,
|
|
pub(crate) started_at: Option<u64>,
|
|
pub(crate) completed_at: Option<u64>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexTurnEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexTurnEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexTurnSteerEventParams {
|
|
pub(crate) thread_id: String,
|
|
pub(crate) expected_turn_id: Option<String>,
|
|
pub(crate) accepted_turn_id: Option<String>,
|
|
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
|
pub(crate) runtime: CodexRuntimeMetadata,
|
|
pub(crate) thread_source: Option<ThreadSource>,
|
|
pub(crate) subagent_source: Option<String>,
|
|
pub(crate) parent_thread_id: Option<String>,
|
|
pub(crate) num_input_images: usize,
|
|
pub(crate) result: TurnSteerResult,
|
|
pub(crate) rejection_reason: Option<TurnSteerRejectionReason>,
|
|
pub(crate) created_at: u64,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexTurnSteerEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexTurnSteerEventParams,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexPluginMetadata {
|
|
pub(crate) plugin_id: Option<String>,
|
|
pub(crate) plugin_name: Option<String>,
|
|
pub(crate) marketplace_name: Option<String>,
|
|
pub(crate) has_skills: Option<bool>,
|
|
pub(crate) mcp_server_count: Option<usize>,
|
|
pub(crate) connector_ids: Option<Vec<String>>,
|
|
pub(crate) product_client_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexPluginUsedMetadata {
|
|
#[serde(flatten)]
|
|
pub(crate) plugin: CodexPluginMetadata,
|
|
pub(crate) thread_id: Option<String>,
|
|
pub(crate) turn_id: Option<String>,
|
|
pub(crate) model_slug: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexPluginEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexPluginMetadata,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub(crate) struct CodexPluginUsedEventRequest {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) event_params: CodexPluginUsedMetadata,
|
|
}
|
|
|
|
pub(crate) fn plugin_state_event_type(state: PluginState) -> &'static str {
|
|
match state {
|
|
PluginState::Installed => "codex_plugin_installed",
|
|
PluginState::Uninstalled => "codex_plugin_uninstalled",
|
|
PluginState::Enabled => "codex_plugin_enabled",
|
|
PluginState::Disabled => "codex_plugin_disabled",
|
|
}
|
|
}
|
|
|
|
pub(crate) fn codex_app_metadata(
|
|
tracking: &TrackEventsContext,
|
|
app: AppInvocation,
|
|
) -> CodexAppMetadata {
|
|
CodexAppMetadata {
|
|
connector_id: app.connector_id,
|
|
thread_id: Some(tracking.thread_id.clone()),
|
|
turn_id: Some(tracking.turn_id.clone()),
|
|
app_name: app.app_name,
|
|
product_client_id: Some(originator().value),
|
|
invoke_type: app.invocation_type,
|
|
model_slug: Some(tracking.model_slug.clone()),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPluginMetadata {
|
|
let PluginTelemetryMetadata {
|
|
plugin_id,
|
|
remote_plugin_id,
|
|
capability_summary,
|
|
} = plugin;
|
|
let event_plugin_id = remote_plugin_id.unwrap_or_else(|| plugin_id.as_key());
|
|
CodexPluginMetadata {
|
|
plugin_id: Some(event_plugin_id),
|
|
plugin_name: Some(plugin_id.plugin_name),
|
|
marketplace_name: Some(plugin_id.marketplace_name),
|
|
has_skills: capability_summary
|
|
.as_ref()
|
|
.map(|summary| summary.has_skills),
|
|
mcp_server_count: capability_summary
|
|
.as_ref()
|
|
.map(|summary| summary.mcp_server_names.len()),
|
|
connector_ids: capability_summary.map(|summary| {
|
|
summary
|
|
.app_connector_ids
|
|
.into_iter()
|
|
.map(|connector_id| connector_id.0)
|
|
.collect()
|
|
}),
|
|
product_client_id: Some(originator().value),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn codex_compaction_event_params(
|
|
input: CodexCompactionEvent,
|
|
app_server_client: CodexAppServerClientMetadata,
|
|
runtime: CodexRuntimeMetadata,
|
|
thread_source: Option<ThreadSource>,
|
|
subagent_source: Option<String>,
|
|
parent_thread_id: Option<String>,
|
|
) -> CodexCompactionEventParams {
|
|
CodexCompactionEventParams {
|
|
thread_id: input.thread_id,
|
|
turn_id: input.turn_id,
|
|
app_server_client,
|
|
runtime,
|
|
thread_source,
|
|
subagent_source,
|
|
parent_thread_id,
|
|
trigger: input.trigger,
|
|
reason: input.reason,
|
|
implementation: input.implementation,
|
|
phase: input.phase,
|
|
strategy: input.strategy,
|
|
status: input.status,
|
|
error: input.error,
|
|
active_context_tokens_before: input.active_context_tokens_before,
|
|
active_context_tokens_after: input.active_context_tokens_after,
|
|
started_at: input.started_at,
|
|
completed_at: input.completed_at,
|
|
duration_ms: input.duration_ms,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn codex_plugin_used_metadata(
|
|
tracking: &TrackEventsContext,
|
|
plugin: PluginTelemetryMetadata,
|
|
) -> CodexPluginUsedMetadata {
|
|
CodexPluginUsedMetadata {
|
|
plugin: codex_plugin_metadata(plugin),
|
|
thread_id: Some(tracking.thread_id.clone()),
|
|
turn_id: Some(tracking.turn_id.clone()),
|
|
model_slug: Some(tracking.model_slug.clone()),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn codex_hook_run_metadata(
|
|
tracking: &TrackEventsContext,
|
|
hook: HookRunFact,
|
|
) -> CodexHookRunMetadata {
|
|
CodexHookRunMetadata {
|
|
thread_id: Some(tracking.thread_id.clone()),
|
|
turn_id: Some(tracking.turn_id.clone()),
|
|
model_slug: Some(tracking.model_slug.clone()),
|
|
hook_name: Some(analytics_hook_event_name(hook.event_name).to_owned()),
|
|
hook_source: Some(analytics_hook_source(hook.hook_source)),
|
|
status: Some(analytics_hook_status(hook.status)),
|
|
}
|
|
}
|
|
|
|
fn analytics_hook_event_name(event_name: HookEventName) -> &'static str {
|
|
match event_name {
|
|
HookEventName::PreToolUse => "PreToolUse",
|
|
HookEventName::PermissionRequest => "PermissionRequest",
|
|
HookEventName::PostToolUse => "PostToolUse",
|
|
HookEventName::PreCompact => "PreCompact",
|
|
HookEventName::PostCompact => "PostCompact",
|
|
HookEventName::SessionStart => "SessionStart",
|
|
HookEventName::UserPromptSubmit => "UserPromptSubmit",
|
|
HookEventName::Stop => "Stop",
|
|
}
|
|
}
|
|
|
|
fn analytics_hook_source(source: HookSource) -> &'static str {
|
|
match source {
|
|
HookSource::System => "system",
|
|
HookSource::User => "user",
|
|
HookSource::Project => "project",
|
|
HookSource::Mdm => "mdm",
|
|
HookSource::SessionFlags => "session_flags",
|
|
HookSource::Plugin => "plugin",
|
|
HookSource::CloudRequirements => "cloud_requirements",
|
|
HookSource::LegacyManagedConfigFile => "legacy_managed_config_file",
|
|
HookSource::LegacyManagedConfigMdm => "legacy_managed_config_mdm",
|
|
HookSource::Unknown => "unknown",
|
|
}
|
|
}
|
|
|
|
pub(crate) fn current_runtime_metadata() -> CodexRuntimeMetadata {
|
|
let os_info = os_info::get();
|
|
CodexRuntimeMetadata {
|
|
codex_rs_version: env!("CARGO_PKG_VERSION").to_string(),
|
|
runtime_os: std::env::consts::OS.to_string(),
|
|
runtime_os_version: os_info.version().to_string(),
|
|
runtime_arch: std::env::consts::ARCH.to_string(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn subagent_thread_started_event_request(
|
|
input: SubAgentThreadStartedInput,
|
|
) -> ThreadInitializedEvent {
|
|
let event_params = ThreadInitializedEventParams {
|
|
thread_id: input.thread_id,
|
|
app_server_client: CodexAppServerClientMetadata {
|
|
product_client_id: input.product_client_id,
|
|
client_name: Some(input.client_name),
|
|
client_version: Some(input.client_version),
|
|
rpc_transport: AppServerRpcTransport::InProcess,
|
|
experimental_api_enabled: None,
|
|
},
|
|
runtime: current_runtime_metadata(),
|
|
model: input.model,
|
|
ephemeral: input.ephemeral,
|
|
thread_source: Some(ThreadSource::Subagent),
|
|
initialization_mode: ThreadInitializationMode::New,
|
|
subagent_source: Some(subagent_source_name(&input.subagent_source)),
|
|
parent_thread_id: input
|
|
.parent_thread_id
|
|
.or_else(|| subagent_parent_thread_id(&input.subagent_source)),
|
|
created_at: input.created_at,
|
|
};
|
|
ThreadInitializedEvent {
|
|
event_type: "codex_thread_initialized",
|
|
event_params,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn subagent_source_name(subagent_source: &SubAgentSource) -> String {
|
|
match subagent_source {
|
|
SubAgentSource::Review => "review".to_string(),
|
|
SubAgentSource::Compact => "compact".to_string(),
|
|
SubAgentSource::ThreadSpawn { .. } => "thread_spawn".to_string(),
|
|
SubAgentSource::MemoryConsolidation => "memory_consolidation".to_string(),
|
|
SubAgentSource::Other(other) => other.clone(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn subagent_parent_thread_id(subagent_source: &SubAgentSource) -> Option<String> {
|
|
match subagent_source {
|
|
SubAgentSource::ThreadSpawn {
|
|
parent_thread_id, ..
|
|
} => Some(parent_thread_id.to_string()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn analytics_hook_status(status: HookRunStatus) -> HookRunStatus {
|
|
match status {
|
|
// Running is unexpected here and normalized defensively.
|
|
HookRunStatus::Running => HookRunStatus::Failed,
|
|
other => other,
|
|
}
|
|
}
|