mirror of
https://github.com/openai/codex.git
synced 2026-05-19 02:33:10 +00:00
Based on work from Vincent K - https://github.com/openai/codex/pull/19060 <img width="1836" height="642" alt="CleanShot 2026-04-29 at 20 47 40@2x" src="https://github.com/user-attachments/assets/b647bb89-65fe-40c8-80b0-7a6b7c984634" /> ## Why Compaction rewrites the conversation context that future model turns receive, but hooks currently have no deterministic lifecycle point around that rewrite. This adds compact lifecycle hooks so users can audit manual and automatic compaction, surface hook messages in the UI, and run post-compaction follow-up without overloading tool or prompt hooks. ## What Changed - Added `PreCompact` and `PostCompact` hook events across hook config, discovery, dispatch, generated schemas, app-server notifications, analytics, and TUI hook rendering. - Added trigger matching for compact hooks with the documented `manual` and `auto` matcher values. - Wired `PreCompact` before both local and remote compaction, and `PostCompact` after successful local or remote compaction. - Kept compact hook command input to lifecycle metadata: session id, Codex turn id, transcript path, cwd, hook event name, model, and trigger. - Made compact stdout handling consistent with other hooks: plain stdout is ignored as debug output, while malformed JSON-looking stdout is reported as failed hook output. - Added integration coverage for compact hook dispatch, trigger matching, post-compact execution, and the audited behavior that `decision:"block"` does not block compaction. ## Out of Scope - Hook-specific compaction blocking is not implemented; `decision:"block"` and exit-code-2 blocking semantics are intentionally unsupported for `PreCompact`. - Custom compaction instructions are not exposed to compact hooks in this PR. - Compact summaries, summary character counts, and summary previews are not exposed to compact hooks in this PR. ## Verification - `cargo test -p codex-hooks` - `cargo test -p codex-core manual_pre_compact_block_decision_does_not_block_compaction` - `cargo test -p codex-app-server hooks_list` - `cargo test -p codex-core config_schema_matches_fixture` - `cargo test -p codex-tui hooks_browser` ## Docs The developer documentation for Codex hooks should be updated alongside this feature to document `PreCompact` and `PostCompact`, the `manual`/`auto` matcher values, and the compact hook payload fields. --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
971 lines
33 KiB
Rust
971 lines
33 KiB
Rust
use std::time::Instant;
|
|
|
|
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_seconds;
|
|
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),
|
|
PluginUsed(CodexPluginUsedEventRequest),
|
|
PluginInstalled(CodexPluginEventRequest),
|
|
PluginUninstalled(CodexPluginEventRequest),
|
|
PluginEnabled(CodexPluginEventRequest),
|
|
PluginDisabled(CodexPluginEventRequest),
|
|
}
|
|
|
|
#[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,
|
|
started_at: 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: now_unix_seconds(),
|
|
started_instant: Instant::now(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn event_params(
|
|
&self,
|
|
result: GuardianReviewAnalyticsResult,
|
|
) -> 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,
|
|
completed_at: Some(now_unix_seconds()),
|
|
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 ToolItemFinalApprovalOutcome {
|
|
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<&'static str>,
|
|
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: ToolItemFinalApprovalOutcome,
|
|
pub(crate) terminal_status: ToolItemTerminalStatus,
|
|
pub(crate) failure_kind: Option<ToolItemFailureKind>,
|
|
pub(crate) requested_additional_permissions: bool,
|
|
pub(crate) requested_network_access: bool,
|
|
}
|
|
|
|
#[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>,
|
|
// TODO(rhan-oai): Populate these once tool-call accounting is emitted from
|
|
// core; the schema is reserved but these fields are currently always None.
|
|
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,
|
|
}
|
|
}
|