mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Attribute command and plugin analytics to the invoking model (#45445)
## Why Command execution and plugin measurement events lack model and reasoning-effort labels. Attribution needs to reflect the step that invoked the command, even when model settings change before a background process finishes. ## What changed - Add `model_slug` and `reasoning_effort` to command execution and plugin measurement analytics. - Capture model context from resolved step settings and carry it through execution, approval, Guardian review, and plugin metrics collection. - Preserve the first command-start model context when subsequent start notifications arrive. - Keep the carried context out of serialized protocol items and generated schemas. ## Testing Extend analytics tests to cover model switches before invocation and during background execution, default reasoning effort, Guardian-denied commands, and repeated start notifications retaining the original model context. GitOrigin-RevId: af90e1c0d39bab625f2e89786085b61a9b96c0ce
This commit is contained in:
@@ -869,6 +869,8 @@ fn plugin_measurements(rows: Vec<PluginMeasurementRow>) -> PluginMeasurementsInp
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "item-1".to_string(),
|
||||
originator: "codex_cli_rs".to_string(),
|
||||
model_slug: None,
|
||||
reasoning_effort: None,
|
||||
plugin_id: "sample@openai-curated".to_string(),
|
||||
execution_id: "execution-1".to_string(),
|
||||
operation: "security_scan".to_string(),
|
||||
@@ -946,6 +948,7 @@ fn sample_command_execution_item_with_id(
|
||||
duration_ms: Option<i64>,
|
||||
) -> ThreadItem {
|
||||
ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: id.to_string(),
|
||||
plugin_id: None,
|
||||
script_path: None,
|
||||
@@ -1790,6 +1793,8 @@ fn command_execution_event_serializes_expected_shape() {
|
||||
let event = TrackEventRequest::CommandExecution(CodexCommandExecutionEventRequest {
|
||||
event_type: "codex_command_execution_event",
|
||||
event_params: CodexCommandExecutionEventParams {
|
||||
model_slug: None,
|
||||
reasoning_effort: None,
|
||||
base: CodexToolItemEventBase {
|
||||
thread_id: "thread-1".to_string(),
|
||||
session_id: "session-thread-1".to_string(),
|
||||
@@ -1848,6 +1853,8 @@ fn command_execution_event_serializes_expected_shape() {
|
||||
json!({
|
||||
"event_type": "codex_command_execution_event",
|
||||
"event_params": {
|
||||
"model_slug": null,
|
||||
"reasoning_effort": null,
|
||||
"thread_id": "thread-1",
|
||||
"session_id": "session-thread-1",
|
||||
"turn_id": "turn-1",
|
||||
@@ -2781,23 +2788,35 @@ async fn item_lifecycle_notifications_publish_command_execution_event() {
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at_ms: 1_000,
|
||||
item: sample_command_execution_item(
|
||||
CommandExecutionStatus::InProgress,
|
||||
/*exit_code*/ None,
|
||||
/*duration_ms*/ None,
|
||||
),
|
||||
},
|
||||
))),
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
for model in ["invoking-model", "later-model"] {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at_ms: 1_000,
|
||||
item: {
|
||||
let mut item = sample_command_execution_item(
|
||||
CommandExecutionStatus::InProgress,
|
||||
/*exit_code*/ None,
|
||||
/*duration_ms*/ None,
|
||||
);
|
||||
if let ThreadItem::CommandExecution { model_context, .. } = &mut item {
|
||||
*model_context =
|
||||
Some(codex_protocol::items::ModelInvocationContext {
|
||||
model_slug: model.to_string(),
|
||||
reasoning_effort: Some("max".to_string()),
|
||||
});
|
||||
}
|
||||
item
|
||||
},
|
||||
},
|
||||
))),
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
assert!(
|
||||
events.is_empty(),
|
||||
"tool item event should emit on completion"
|
||||
@@ -2844,6 +2863,8 @@ async fn item_lifecycle_notifications_publish_command_execution_event() {
|
||||
|
||||
let payload = serde_json::to_value(&events).expect("serialize events");
|
||||
assert_eq!(payload.as_array().expect("events array").len(), 1);
|
||||
assert_eq!(payload[0]["event_params"]["model_slug"], "invoking-model");
|
||||
assert_eq!(payload[0]["event_params"]["reasoning_effort"], "max");
|
||||
assert_eq!(payload[0]["event_type"], "codex_command_execution_event");
|
||||
assert_eq!(payload[0]["event_params"]["thread_id"], "thread-1");
|
||||
assert_eq!(payload[0]["event_params"]["session_id"], "session-thread-1");
|
||||
@@ -2899,7 +2920,7 @@ async fn plugin_measurement_batch_emits_directly_and_filters_invalid_rows() {
|
||||
for index in 0..9 {
|
||||
too_many_dimensions.insert(format!("dimension_{index}"), "allowed".to_string());
|
||||
}
|
||||
let measurements = plugin_measurements(vec![
|
||||
let mut measurements = plugin_measurements(vec![
|
||||
PluginMeasurementRow {
|
||||
measurement_name: "finding_count".to_string(),
|
||||
number_value: 3.0,
|
||||
@@ -2921,6 +2942,8 @@ async fn plugin_measurement_batch_emits_directly_and_filters_invalid_rows() {
|
||||
dimensions: BTreeMap::new(),
|
||||
},
|
||||
]);
|
||||
measurements.model_slug = Some("invoking-model".to_string());
|
||||
measurements.reasoning_effort = Some("max".to_string());
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::PluginMeasurements(measurements)),
|
||||
@@ -2942,6 +2965,8 @@ async fn plugin_measurement_batch_emits_directly_and_filters_invalid_rows() {
|
||||
"operation": "security_scan",
|
||||
"measurement_name": "finding_count",
|
||||
"originator": "codex_cli_rs",
|
||||
"model_slug": "invoking-model",
|
||||
"reasoning_effort": "max",
|
||||
"number_value": 3.0,
|
||||
"dimensions": {"severity": "high"},
|
||||
},
|
||||
@@ -2957,6 +2982,8 @@ async fn plugin_measurement_batch_emits_directly_and_filters_invalid_rows() {
|
||||
"operation": "security_scan",
|
||||
"measurement_name": "files_scanned",
|
||||
"originator": "codex_cli_rs",
|
||||
"model_slug": "invoking-model",
|
||||
"reasoning_effort": "max",
|
||||
"number_value": 17.0,
|
||||
"dimensions": null,
|
||||
},
|
||||
|
||||
@@ -420,6 +420,8 @@ async fn api_key_auth_sends_only_plugin_events_to_codex_backend() {
|
||||
TrackEventRequest::PluginMeasurement(CodexPluginMeasurementEventRequest {
|
||||
event_type: "codex_plugin_measurement_event",
|
||||
event_params: CodexPluginMeasurementEventParams {
|
||||
model_slug: None,
|
||||
reasoning_effort: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "item-1".to_string(),
|
||||
@@ -712,6 +714,8 @@ fn track_plugin_measurements_rejects_unbounded_inputs_before_queueing() {
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "item-1".to_string(),
|
||||
originator: "codex_cli_rs".to_string(),
|
||||
model_slug: None,
|
||||
reasoning_effort: None,
|
||||
plugin_id: "sample@openai-curated".to_string(),
|
||||
execution_id: "execution-1".to_string(),
|
||||
operation: "security_scan".to_string(),
|
||||
|
||||
@@ -778,6 +778,8 @@ pub(crate) enum WebSearchActionKind {
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexCommandExecutionEventParams {
|
||||
pub(crate) model_slug: Option<String>,
|
||||
pub(crate) reasoning_effort: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub(crate) base: CodexToolItemEventBase,
|
||||
pub(crate) plugin_id: Option<String>,
|
||||
@@ -803,6 +805,8 @@ pub(crate) struct CodexPluginMeasurementEventParams {
|
||||
pub(crate) turn_id: String,
|
||||
pub(crate) item_id: String,
|
||||
pub(crate) originator: String,
|
||||
pub(crate) model_slug: Option<String>,
|
||||
pub(crate) reasoning_effort: Option<String>,
|
||||
pub(crate) plugin_id: String,
|
||||
pub(crate) execution_id: String,
|
||||
pub(crate) operation: String,
|
||||
|
||||
@@ -604,6 +604,8 @@ pub struct PluginMeasurementsInput {
|
||||
pub turn_id: String,
|
||||
pub item_id: String,
|
||||
pub originator: String,
|
||||
pub model_slug: Option<String>,
|
||||
pub reasoning_effort: Option<String>,
|
||||
pub plugin_id: String,
|
||||
pub execution_id: String,
|
||||
pub operation: String,
|
||||
|
||||
@@ -159,6 +159,7 @@ use codex_login::default_client::originator;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::items::ModelInvocationContext;
|
||||
use codex_protocol::items::is_safe_plugin_relative_path;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
@@ -203,7 +204,7 @@ pub(crate) struct AnalyticsReducer {
|
||||
turns: HashMap<String, TurnState>,
|
||||
connections: HashMap<u64, ConnectionState>,
|
||||
threads: HashMap<String, ThreadAnalyticsState>,
|
||||
tool_items_started_at_ms: HashMap<ToolItemKey, u64>,
|
||||
tool_items_started_at_ms: HashMap<ToolItemKey, (u64, Option<ModelInvocationContext>)>,
|
||||
tool_response_states: HashMap<(String, String), ToolResponseState>,
|
||||
code_mode_cells: HashMap<String, HashMap<String, CodeModeCellState>>,
|
||||
pending_reviews: HashMap<RequestId, PendingReviewState>,
|
||||
@@ -1891,14 +1892,18 @@ impl AnalyticsReducer {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.tool_items_started_at_ms.insert(
|
||||
ToolItemKey {
|
||||
let model_context = match ¬ification.item {
|
||||
ThreadItem::CommandExecution { model_context, .. } => model_context.clone(),
|
||||
_ => None,
|
||||
};
|
||||
self.tool_items_started_at_ms
|
||||
.entry(ToolItemKey {
|
||||
thread_id: notification.thread_id,
|
||||
turn_id: notification.turn_id,
|
||||
item_id: item_id.to_string(),
|
||||
},
|
||||
started_at_ms,
|
||||
);
|
||||
})
|
||||
.and_modify(|(timestamp, _)| *timestamp = started_at_ms)
|
||||
.or_insert((started_at_ms, model_context));
|
||||
}
|
||||
ServerNotification::ItemCompleted(notification) => {
|
||||
if matches!(notification.item, ThreadItem::SubAgentActivity { .. }) {
|
||||
@@ -1931,7 +1936,9 @@ impl AnalyticsReducer {
|
||||
turn_id: notification.turn_id.clone(),
|
||||
item_id: item_id.to_string(),
|
||||
};
|
||||
let Some(started_at_ms) = self.tool_items_started_at_ms.remove(&key) else {
|
||||
let Some((started_at_ms, model_context)) =
|
||||
self.tool_items_started_at_ms.remove(&key)
|
||||
else {
|
||||
tracing::warn!(
|
||||
thread_id = %notification.thread_id,
|
||||
turn_id = %notification.turn_id,
|
||||
@@ -1953,6 +1960,7 @@ impl AnalyticsReducer {
|
||||
thread_id: ¬ification.thread_id,
|
||||
turn_id: ¬ification.turn_id,
|
||||
item: ¬ification.item,
|
||||
model_context: model_context.as_ref(),
|
||||
started_at_ms,
|
||||
completed_at_ms,
|
||||
connection_state,
|
||||
@@ -2063,6 +2071,8 @@ impl AnalyticsReducer {
|
||||
turn_id,
|
||||
item_id,
|
||||
originator,
|
||||
model_slug,
|
||||
reasoning_effort,
|
||||
plugin_id,
|
||||
execution_id,
|
||||
operation,
|
||||
@@ -2079,6 +2089,8 @@ impl AnalyticsReducer {
|
||||
turn_id: turn_id.clone(),
|
||||
item_id: item_id.clone(),
|
||||
originator: originator.clone(),
|
||||
model_slug: model_slug.clone(),
|
||||
reasoning_effort: reasoning_effort.clone(),
|
||||
plugin_id: plugin_id.clone(),
|
||||
execution_id: execution_id.clone(),
|
||||
operation: operation.clone(),
|
||||
@@ -2598,6 +2610,7 @@ struct ToolItemEventInput<'a> {
|
||||
thread_id: &'a str,
|
||||
turn_id: &'a str,
|
||||
item: &'a ThreadItem,
|
||||
model_context: Option<&'a ModelInvocationContext>,
|
||||
started_at_ms: u64,
|
||||
completed_at_ms: u64,
|
||||
connection_state: &'a ConnectionState,
|
||||
@@ -2611,6 +2624,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
model_context,
|
||||
started_at_ms,
|
||||
completed_at_ms,
|
||||
connection_state,
|
||||
@@ -2655,6 +2669,9 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
||||
CodexCommandExecutionEventRequest {
|
||||
event_type: "codex_command_execution_event",
|
||||
event_params: CodexCommandExecutionEventParams {
|
||||
model_slug: model_context.map(|context| context.model_slug.clone()),
|
||||
reasoning_effort: model_context
|
||||
.and_then(|context| context.reasoning_effort.clone()),
|
||||
base,
|
||||
plugin_id: plugin_id.clone(),
|
||||
script_path: safe_plugin_relative_script_path(
|
||||
|
||||
@@ -97,6 +97,7 @@ pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> Th
|
||||
let presentation =
|
||||
CommandExecutionPresentation::from_raw(&payload.command, &payload.parsed_cmd, &payload.cwd);
|
||||
ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: payload.call_id.clone(),
|
||||
plugin_id: payload.plugin_id.clone(),
|
||||
script_path: payload.script_path.clone(),
|
||||
@@ -123,6 +124,7 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread
|
||||
CommandExecutionPresentation::from_raw(&payload.command, &payload.parsed_cmd, &payload.cwd);
|
||||
|
||||
ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: payload.call_id.clone(),
|
||||
plugin_id: payload.plugin_id.clone(),
|
||||
script_path: payload.script_path.clone(),
|
||||
@@ -199,6 +201,7 @@ pub fn build_item_from_guardian_event(
|
||||
}];
|
||||
Some(ThreadItem::CommandExecution {
|
||||
id: id.clone(),
|
||||
model_context: assessment.model_context.clone(),
|
||||
plugin_id: assessment.plugin_id.clone(),
|
||||
script_path: assessment.script_path.clone(),
|
||||
command,
|
||||
@@ -237,6 +240,7 @@ pub fn build_item_from_guardian_event(
|
||||
};
|
||||
Some(ThreadItem::CommandExecution {
|
||||
id: id.clone(),
|
||||
model_context: assessment.model_context.clone(),
|
||||
plugin_id: assessment.plugin_id.clone(),
|
||||
script_path: assessment.script_path.clone(),
|
||||
command,
|
||||
|
||||
@@ -131,6 +131,7 @@ fn guardian_stdin_reviews_preserve_parent_command_history() {
|
||||
|
||||
let assessment = GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "review-stdin".into(),
|
||||
target_item_id: Some("terminal-command".into()),
|
||||
plugin_id: None,
|
||||
|
||||
@@ -2255,6 +2255,7 @@ mod tests {
|
||||
.to_string(),
|
||||
}];
|
||||
let command_item = CoreTurnItem::CommandExecution(CoreCommandExecutionItem {
|
||||
model_context: None,
|
||||
id: "exec-1".to_string(),
|
||||
plugin_id: Some("sample@openai-curated".to_string()),
|
||||
script_path: Some("scripts/run.py".to_string()),
|
||||
@@ -2340,6 +2341,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
build_turns_from_rollout_items(&items[..2])[0].items,
|
||||
vec![ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "exec-1".to_string(),
|
||||
plugin_id: Some("sample@openai-curated".to_string()),
|
||||
script_path: Some("scripts/run.py".to_string()),
|
||||
@@ -2365,6 +2367,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
turns[0].items,
|
||||
vec![ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "exec-1".to_string(),
|
||||
plugin_id: Some("sample@openai-curated".to_string()),
|
||||
script_path: Some("scripts/run.py".to_string()),
|
||||
@@ -3051,6 +3054,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
turns[0].items[2],
|
||||
ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "exec-1".into(),
|
||||
plugin_id: None,
|
||||
script_path: None,
|
||||
@@ -3321,6 +3325,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
turns[0].items[1],
|
||||
ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "exec-declined".into(),
|
||||
plugin_id: None,
|
||||
script_path: None,
|
||||
@@ -3372,6 +3377,7 @@ mod tests {
|
||||
}),
|
||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "review-guardian-exec".into(),
|
||||
target_item_id: Some("guardian-exec".into()),
|
||||
plugin_id: Some("sample@openai-curated".into()),
|
||||
@@ -3394,6 +3400,7 @@ mod tests {
|
||||
}),
|
||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "review-guardian-exec".into(),
|
||||
target_item_id: Some("guardian-exec".into()),
|
||||
plugin_id: Some("sample@openai-curated".into()),
|
||||
@@ -3428,6 +3435,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
turns[0].items[1],
|
||||
ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "guardian-exec".into(),
|
||||
plugin_id: Some("sample@openai-curated".into()),
|
||||
script_path: Some("scripts/run.py".into()),
|
||||
@@ -3467,6 +3475,7 @@ mod tests {
|
||||
}),
|
||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "review-guardian-execve".into(),
|
||||
target_item_id: Some("guardian-execve".into()),
|
||||
plugin_id: Some("sample@openai-curated".into()),
|
||||
@@ -3500,6 +3509,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
turns[0].items[1],
|
||||
ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "guardian-execve".into(),
|
||||
plugin_id: Some("sample@openai-curated".into()),
|
||||
script_path: Some("scripts/run.py".into()),
|
||||
@@ -3701,6 +3711,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
turns[0].items[1],
|
||||
ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "exec-late".into(),
|
||||
plugin_id: None,
|
||||
script_path: None,
|
||||
|
||||
@@ -287,6 +287,10 @@ pub enum ThreadItem {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
CommandExecution {
|
||||
#[serde(skip)]
|
||||
#[schemars(skip)]
|
||||
#[ts(skip)]
|
||||
model_context: Option<codex_protocol::items::ModelInvocationContext>,
|
||||
id: String,
|
||||
/// Trusted first-party plugin id when this command resolves to one plugin script.
|
||||
#[serde(default)]
|
||||
@@ -910,6 +914,7 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
);
|
||||
ThreadItem::CommandExecution {
|
||||
id: command.id,
|
||||
model_context: command.model_context,
|
||||
plugin_id: command.plugin_id,
|
||||
script_path: command.script_path,
|
||||
command: presentation.command,
|
||||
|
||||
@@ -3210,6 +3210,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
);
|
||||
|
||||
let command_item = TurnItem::CommandExecution(CommandExecutionItem {
|
||||
model_context: None,
|
||||
id: "exec-1".to_string(),
|
||||
plugin_id: Some("sample@openai-curated".to_string()),
|
||||
script_path: Some("scripts/run.py".to_string()),
|
||||
@@ -3242,6 +3243,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
assert_eq!(
|
||||
ThreadItem::from(command_item),
|
||||
ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "exec-1".to_string(),
|
||||
plugin_id: Some("sample@openai-curated".to_string()),
|
||||
script_path: Some("scripts/run.py".to_string()),
|
||||
|
||||
@@ -92,6 +92,7 @@ use codex_core::ThreadManager;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool;
|
||||
use codex_protocol::items::ModelInvocationContext;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
@@ -129,6 +130,7 @@ enum CommandExecutionApprovalPresentation {
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct CommandExecutionCompletionItem {
|
||||
model_context: Option<ModelInvocationContext>,
|
||||
plugin_id: Option<String>,
|
||||
script_path: Option<String>,
|
||||
command: String,
|
||||
@@ -294,6 +296,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
) {
|
||||
Some(ThreadItem::CommandExecution {
|
||||
id,
|
||||
model_context,
|
||||
plugin_id,
|
||||
script_path,
|
||||
command,
|
||||
@@ -303,6 +306,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
}) => Some((
|
||||
id,
|
||||
CommandExecutionCompletionItem {
|
||||
model_context,
|
||||
plugin_id,
|
||||
script_path,
|
||||
command,
|
||||
@@ -324,6 +328,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
&conversation_id,
|
||||
assessment_turn_id.clone(),
|
||||
target_item_id.clone(),
|
||||
completion_item.model_context.clone(),
|
||||
completion_item.plugin_id.clone(),
|
||||
completion_item.script_path.clone(),
|
||||
completion_item.command.clone(),
|
||||
@@ -669,6 +674,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.map(CommandExecutionApprovalDecision::from)
|
||||
.collect::<Vec<_>>();
|
||||
let ExecApprovalRequestEvent {
|
||||
model_context,
|
||||
kind,
|
||||
call_id,
|
||||
plugin_id,
|
||||
@@ -723,6 +729,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
CommandExecutionApprovalPresentation::Network(network_approval_context)
|
||||
} else {
|
||||
let completion_item = CommandExecutionCompletionItem {
|
||||
model_context,
|
||||
plugin_id,
|
||||
script_path,
|
||||
command: command_presentation.command,
|
||||
@@ -751,6 +758,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
&conversation_id,
|
||||
event_turn_id.clone(),
|
||||
call_id.clone(),
|
||||
completion_item.model_context.clone(),
|
||||
completion_item.plugin_id.clone(),
|
||||
completion_item.script_path.clone(),
|
||||
completion_item.command.clone(),
|
||||
@@ -1389,6 +1397,7 @@ async fn start_command_execution_item(
|
||||
conversation_id: &ThreadId,
|
||||
turn_id: String,
|
||||
item_id: String,
|
||||
model_context: Option<ModelInvocationContext>,
|
||||
plugin_id: Option<String>,
|
||||
script_path: Option<String>,
|
||||
command: String,
|
||||
@@ -1412,6 +1421,7 @@ async fn start_command_execution_item(
|
||||
started_at_ms: now_unix_timestamp_ms(),
|
||||
item: ThreadItem::CommandExecution {
|
||||
id: item_id,
|
||||
model_context,
|
||||
plugin_id,
|
||||
script_path,
|
||||
command,
|
||||
@@ -1456,6 +1466,7 @@ async fn complete_command_execution_item(
|
||||
|
||||
let item = ThreadItem::CommandExecution {
|
||||
id: item_id,
|
||||
model_context: completion_item.model_context,
|
||||
plugin_id: completion_item.plugin_id,
|
||||
script_path: completion_item.script_path,
|
||||
command: completion_item.command,
|
||||
@@ -2183,6 +2194,7 @@ mod tests {
|
||||
|
||||
fn command_execution_completion_item(command: &str) -> CommandExecutionCompletionItem {
|
||||
CommandExecutionCompletionItem {
|
||||
model_context: None,
|
||||
plugin_id: Some("sample@openai-curated".to_string()),
|
||||
script_path: Some("scripts/run.py".to_string()),
|
||||
command: command.to_string(),
|
||||
@@ -2217,6 +2229,7 @@ mod tests {
|
||||
};
|
||||
GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: format!("review-{id}"),
|
||||
target_item_id: Some(id.to_string()),
|
||||
plugin_id: Some("sample@openai-curated".to_string()),
|
||||
@@ -2285,6 +2298,7 @@ mod tests {
|
||||
"turn-from-event",
|
||||
&GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "review-1".to_string(),
|
||||
target_item_id: Some("item-1".to_string()),
|
||||
plugin_id: None,
|
||||
@@ -2334,6 +2348,7 @@ mod tests {
|
||||
"turn-from-event",
|
||||
&GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "review-2".to_string(),
|
||||
target_item_id: Some("item-2".to_string()),
|
||||
plugin_id: None,
|
||||
@@ -2391,6 +2406,7 @@ mod tests {
|
||||
"turn-from-event",
|
||||
&GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "review-3".to_string(),
|
||||
target_item_id: None,
|
||||
plugin_id: None,
|
||||
@@ -2446,6 +2462,7 @@ mod tests {
|
||||
&conversation_id,
|
||||
"turn-1".to_string(),
|
||||
"cmd-1".to_string(),
|
||||
completion_item.model_context.clone(),
|
||||
completion_item.plugin_id.clone(),
|
||||
completion_item.script_path.clone(),
|
||||
completion_item.command.clone(),
|
||||
@@ -2466,6 +2483,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
payload.item,
|
||||
ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "cmd-1".to_string(),
|
||||
plugin_id: completion_item.plugin_id.clone(),
|
||||
script_path: completion_item.script_path.clone(),
|
||||
@@ -2488,6 +2506,7 @@ mod tests {
|
||||
&conversation_id,
|
||||
"turn-1".to_string(),
|
||||
"cmd-1".to_string(),
|
||||
completion_item.model_context.clone(),
|
||||
completion_item.plugin_id.clone(),
|
||||
completion_item.script_path.clone(),
|
||||
completion_item.command.clone(),
|
||||
@@ -2524,6 +2543,7 @@ mod tests {
|
||||
&conversation_id,
|
||||
"turn-1".to_string(),
|
||||
"cmd-1".to_string(),
|
||||
completion_item.model_context.clone(),
|
||||
completion_item.plugin_id.clone(),
|
||||
completion_item.script_path.clone(),
|
||||
completion_item.command.clone(),
|
||||
|
||||
@@ -14,11 +14,15 @@ use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SandboxPolicy;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
use codex_app_server_protocol::ThreadReadResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnSettingsUpdateParams;
|
||||
use codex_app_server_protocol::TurnSettingsUpdateResponse;
|
||||
use codex_app_server_protocol::TurnSettingsUpdateStatus;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
@@ -29,7 +33,10 @@ use codex_core::config::ConfigBuilder;
|
||||
use codex_core_plugins::loader::curated_plugin_cache_version;
|
||||
use codex_core_plugins::store::PluginStore;
|
||||
use codex_features::Feature;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::skip_if_remote;
|
||||
@@ -135,6 +142,7 @@ async fn guardian_review_turns_and_tools_reach_analytics() -> Result<()> {
|
||||
text: "Review three commands".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
effort: Some(ReasoningEffort::High),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
@@ -166,6 +174,19 @@ async fn guardian_review_turns_and_tools_reach_analytics() -> Result<()> {
|
||||
timeout(READ_TIMEOUT, app_server.shutdown_gracefully()).await??;
|
||||
let events = captured_analytics_events(&server).await;
|
||||
assert!(!serde_json::to_string(&events)?.contains(PRIVATE));
|
||||
let parent_command = events
|
||||
.iter()
|
||||
.find(|event| {
|
||||
event["event_type"] == "codex_command_execution_event"
|
||||
&& event["event_params"]["thread_id"] == thread.id
|
||||
&& event["event_params"]["item_id"] == "parent-first"
|
||||
})
|
||||
.expect("denied parent command analytics");
|
||||
let params = &parent_command["event_params"];
|
||||
assert_eq!(
|
||||
json!([params["model_slug"], params["reasoning_effort"]]),
|
||||
json!(["mock-model", "high"])
|
||||
);
|
||||
let children = events
|
||||
.iter()
|
||||
.filter(|event| event["event_params"]["subagent_source"] == "guardian")
|
||||
@@ -692,6 +713,7 @@ operations:
|
||||
&script_path,
|
||||
r#"test -n "$CODEX_PLUGIN_METRICS_OUTPUT"
|
||||
sleep "${1:-0.3}"
|
||||
while [ -n "${2:-}" ] && [ ! -f "$2" ]; do sleep 0.01; done
|
||||
printf '%s' '{"version":1,"measurements":[{"name":"findings","value":3,"dimensions":{"severity":"high"}},{"name":"files_scanned","value":17}]}' > "$CODEX_PLUGIN_METRICS_OUTPUT"
|
||||
"#,
|
||||
)?;
|
||||
@@ -724,12 +746,14 @@ async fn assert_plugin_measurement_analytics(remote: bool, background: bool) ->
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let script_path = write_curated_metrics_plugin(codex_home.path())?.canonicalize()?;
|
||||
let release_path = codex_home.path().join("release-command");
|
||||
let mut command = vec![
|
||||
"/bin/sh".to_string(),
|
||||
script_path.to_string_lossy().into_owned(),
|
||||
];
|
||||
if background {
|
||||
command.push("1.0".to_string());
|
||||
command.push(release_path.to_string_lossy().into_owned());
|
||||
}
|
||||
let call_id = "curated-plugin-metrics";
|
||||
let arguments = serde_json::to_string(&json!({
|
||||
@@ -742,8 +766,32 @@ async fn assert_plugin_measurement_analytics(remote: bool, background: bool) ->
|
||||
responses::ev_completed("resp-1"),
|
||||
]);
|
||||
let final_response = create_final_assistant_message_sse_response("done")?;
|
||||
let server =
|
||||
create_mock_responses_server_sequence(vec![command_response, final_response]).await;
|
||||
let pause_response = |id: &str| {
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created(id),
|
||||
responses::ev_function_call(
|
||||
id,
|
||||
"request_user_input",
|
||||
&json!({
|
||||
"questions": [{
|
||||
"id": "continue", "header": "Continue", "question": "Continue?",
|
||||
"options": [
|
||||
{"label": "Yes", "description": "Continue the turn."},
|
||||
{"label": "No", "description": "Stop the turn."}
|
||||
]
|
||||
}]
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
responses::ev_completed(id),
|
||||
])
|
||||
};
|
||||
let mut response_sequence = vec![pause_response("before-command"), command_response];
|
||||
if background {
|
||||
response_sequence.push(pause_response("after-launch"));
|
||||
}
|
||||
response_sequence.push(final_response);
|
||||
let server = create_mock_responses_server_sequence(response_sequence).await;
|
||||
|
||||
let analytics_server = responses::start_mock_server().await;
|
||||
write_mock_responses_config_toml_with_chatgpt_base_url(
|
||||
@@ -753,11 +801,40 @@ async fn assert_plugin_measurement_analytics(remote: bool, background: bool) ->
|
||||
)?;
|
||||
let config_path = codex_home.path().join("config.toml");
|
||||
let config = std::fs::read_to_string(&config_path)?;
|
||||
let model = bundled_models_response()?
|
||||
.models
|
||||
.into_iter()
|
||||
.find(|model| model.slug == "gpt-5.4")
|
||||
.expect("bundled gpt-5.4 model");
|
||||
let models = [
|
||||
("initial-model", ReasoningEffort::Low),
|
||||
("invoking-model", ReasoningEffort::High),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(slug, effort)| {
|
||||
let mut model = model.clone();
|
||||
model.slug = slug.to_string();
|
||||
model.default_reasoning_level = Some(effort);
|
||||
model
|
||||
})
|
||||
.collect();
|
||||
let catalog_path = codex_home.path().join("measurement-models.json");
|
||||
std::fs::write(
|
||||
&catalog_path,
|
||||
serde_json::to_vec(&ModelsResponse { models })?,
|
||||
)?;
|
||||
let catalog_config = format!(
|
||||
"model_catalog_json = {}",
|
||||
serde_json::to_string(&catalog_path)?
|
||||
);
|
||||
std::fs::write(
|
||||
config_path,
|
||||
format!(
|
||||
r#"{config}
|
||||
r#"{catalog_config}
|
||||
{config}
|
||||
[features]
|
||||
step_model_switching = true
|
||||
default_mode_request_user_input = true
|
||||
plugins = true
|
||||
remote_plugin = false
|
||||
unified_exec = true
|
||||
@@ -787,7 +864,7 @@ enabled = true
|
||||
timeout(Duration::from_secs(10), mcp.initialize()).await??;
|
||||
let thread_request = mcp
|
||||
.send_thread_start_request_with_auto_env(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
model: Some("initial-model".to_string()),
|
||||
service_name: Some("codex_work_desktop".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -813,11 +890,43 @@ enabled = true
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
let turn_response = timeout(
|
||||
Duration::from_secs(10),
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_request)),
|
||||
)
|
||||
.await??;
|
||||
let turn_response: TurnStartResponse = to_response(turn_response)?;
|
||||
for model in if background {
|
||||
vec!["invoking-model", "initial-model"]
|
||||
} else {
|
||||
vec!["invoking-model"]
|
||||
} {
|
||||
let request = timeout(
|
||||
Duration::from_secs(10),
|
||||
mcp.read_stream_until_request_message(),
|
||||
)
|
||||
.await??;
|
||||
let ServerRequest::ToolRequestUserInput { request_id, .. } = request else {
|
||||
anyhow::bail!("expected request_user_input, received {request:?}");
|
||||
};
|
||||
let response: TurnSettingsUpdateResponse = mcp
|
||||
.request(|request_id| ClientRequest::TurnSettingsUpdate {
|
||||
request_id,
|
||||
params: TurnSettingsUpdateParams {
|
||||
thread_id: thread_id.clone(),
|
||||
turn_id: turn_response.turn.id.clone(),
|
||||
model: Some(model.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(response.status, TurnSettingsUpdateStatus::Applied);
|
||||
mcp.send_response(
|
||||
request_id,
|
||||
json!({"answers": {"continue": {"answers": ["Yes"]}}}),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let completed_turn = timeout(
|
||||
Duration::from_secs(10),
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
@@ -830,17 +939,7 @@ enabled = true
|
||||
.expect("completed turn id");
|
||||
|
||||
if background {
|
||||
let model_request_bodies = server
|
||||
.received_requests()
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|request| request.url.path().ends_with("/responses"))
|
||||
.map(|request| serde_json::from_slice::<Value>(&request.body))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let request_text = serde_json::to_string(&model_request_bodies)?;
|
||||
assert!(request_text.contains("Process running with session ID "));
|
||||
assert!(!request_text.contains("Process exited with code 0"));
|
||||
std::fs::write(release_path, "release")?;
|
||||
}
|
||||
|
||||
for measurement_name in ["findings", "files_scanned"] {
|
||||
@@ -859,12 +958,16 @@ enabled = true
|
||||
.await?;
|
||||
assert_eq!(
|
||||
json!({
|
||||
"model_slug": command_event["event_params"]["model_slug"],
|
||||
"reasoning_effort": command_event["event_params"]["reasoning_effort"],
|
||||
"plugin_id": command_event["event_params"]["plugin_id"],
|
||||
"script_path": command_event["event_params"]["script_path"],
|
||||
"item_id": command_event["event_params"]["item_id"],
|
||||
"exit_code": command_event["event_params"]["exit_code"],
|
||||
}),
|
||||
json!({
|
||||
"model_slug": "invoking-model",
|
||||
"reasoning_effort": "high",
|
||||
"plugin_id": METRICS_PLUGIN_ID,
|
||||
"script_path": "scripts/run.sh",
|
||||
"item_id": call_id,
|
||||
@@ -952,10 +1055,12 @@ enabled = true
|
||||
"execution_id",
|
||||
"item_id",
|
||||
"measurement_name",
|
||||
"model_slug",
|
||||
"number_value",
|
||||
"operation",
|
||||
"originator",
|
||||
"plugin_id",
|
||||
"reasoning_effort",
|
||||
"thread_id",
|
||||
"turn_id",
|
||||
]
|
||||
@@ -963,6 +1068,8 @@ enabled = true
|
||||
assert_eq!(event_params["thread_id"], thread_id);
|
||||
assert_eq!(event_params["turn_id"], turn_id);
|
||||
assert_eq!(event_params["originator"], "codex_work_desktop");
|
||||
assert_eq!(event_params["model_slug"], "invoking-model");
|
||||
assert_eq!(event_params["reasoning_effort"], "high");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -20,6 +20,7 @@ mod review_session;
|
||||
mod reviewer_config;
|
||||
mod runtime;
|
||||
|
||||
use codex_protocol::items::ModelInvocationContext;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
@@ -82,6 +83,7 @@ pub(crate) struct GuardianReviewContext {
|
||||
pub(crate) parent_response_id: Option<String>,
|
||||
turn: Arc<TurnContext>,
|
||||
environments: TurnEnvironmentSnapshot,
|
||||
// Model and reasoning inputs are carried for the follow-up Guardian and V2 migrations.
|
||||
pub(crate) model_info: Arc<ModelInfo>,
|
||||
pub(crate) reasoning_effort: Option<ReasoningEffort>,
|
||||
pub(crate) reasoning_summary: ReasoningSummary,
|
||||
@@ -91,6 +93,17 @@ pub(crate) struct GuardianReviewContext {
|
||||
}
|
||||
|
||||
impl GuardianReviewContext {
|
||||
pub(crate) fn model_context(&self) -> ModelInvocationContext {
|
||||
ModelInvocationContext {
|
||||
model_slug: self.model_info.slug.clone(),
|
||||
reasoning_effort: self
|
||||
.reasoning_effort
|
||||
.as_ref()
|
||||
.or(self.model_info.default_reasoning_level.as_ref())
|
||||
.map(ToString::to_string),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_resolved_settings(
|
||||
turn: Arc<TurnContext>,
|
||||
settings: &ResolvedStepSettings,
|
||||
|
||||
@@ -44,6 +44,7 @@ impl ReviewHost for super::super::runtime::ReviewRuntime {
|
||||
Ok(request) => request.clone(),
|
||||
Err(decision) => return Err(decision),
|
||||
};
|
||||
let model_context = context.model_context();
|
||||
let turn = Arc::clone(context.turn());
|
||||
let GuardianReviewOptions {
|
||||
plugin_attribution_override,
|
||||
@@ -101,6 +102,7 @@ impl ReviewHost for super::super::runtime::ReviewRuntime {
|
||||
reviewed_action: guardian_reviewed_action(&request),
|
||||
action: guardian_assessment_action(&request),
|
||||
review_reason,
|
||||
model_context,
|
||||
});
|
||||
session
|
||||
.send_event(
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::tools::sandboxing::ToolCtx;
|
||||
use codex_analytics::PluginMeasurementsInput;
|
||||
use codex_core_plugins::PluginMetricsSidecar;
|
||||
use codex_exec_server::Environment;
|
||||
use codex_protocol::items::ModelInvocationContext;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
/// Creates a metrics sidecar for one plugin command.
|
||||
@@ -34,6 +35,7 @@ pub(crate) async fn finish_and_track_measurements(
|
||||
exit_code: i32,
|
||||
session: &Session,
|
||||
turn: &TurnContext,
|
||||
model_context: &ModelInvocationContext,
|
||||
item_id: &str,
|
||||
) {
|
||||
let Some(metrics_sidecar) = metrics_sidecar else {
|
||||
@@ -50,6 +52,8 @@ pub(crate) async fn finish_and_track_measurements(
|
||||
turn_id: turn.sub_id.clone(),
|
||||
item_id: item_id.to_string(),
|
||||
originator: turn.originator.clone(),
|
||||
model_slug: Some(model_context.model_slug.clone()),
|
||||
reasoning_effort: model_context.reasoning_effort.clone(),
|
||||
plugin_id: batch.plugin_id,
|
||||
execution_id: batch.execution_id,
|
||||
operation: batch.operation,
|
||||
|
||||
@@ -52,6 +52,8 @@ async fn command_approval_holds_an_elicitation_until_response() {
|
||||
.request_command_approval(
|
||||
turn_context.as_ref(),
|
||||
ExecApprovalKind::Command,
|
||||
crate::guardian::GuardianReviewContext::from(turn_context.clone())
|
||||
.model_context(),
|
||||
"call-1".to_string(),
|
||||
/*approval_id*/ None,
|
||||
/*environment_id*/ None,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::context::GuardianContextMode;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
@@ -23,6 +22,7 @@ use crate::config::ManagedFeatures;
|
||||
use crate::config::resolve_tool_suggest_config_from_layer_stack;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::DeveloperInstructions;
|
||||
use crate::context::GuardianContextMode;
|
||||
use crate::context::GuardianPolicy;
|
||||
use crate::context::ManagedDeveloperInstructions;
|
||||
use crate::context::ModelSwitchInstructions;
|
||||
@@ -107,6 +107,7 @@ use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::items::EnteredReviewModeItem;
|
||||
use codex_protocol::items::ModelInvocationContext;
|
||||
use codex_protocol::items::SubAgentActivityItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::items::UserMessageItem;
|
||||
@@ -2790,6 +2791,7 @@ impl Session {
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
kind: ExecApprovalKind,
|
||||
model_context: ModelInvocationContext,
|
||||
call_id: String,
|
||||
approval_id: Option<String>,
|
||||
environment_id: Option<String>,
|
||||
@@ -2853,6 +2855,7 @@ impl Session {
|
||||
.map(PluginCommandAttribution::serialized_fields)
|
||||
.unzip();
|
||||
let event = EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
|
||||
model_context: Some(model_context),
|
||||
kind,
|
||||
call_id,
|
||||
plugin_id,
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
|
||||
use codex_exec_server::ResolvedSelectedCapabilityRoot;
|
||||
use codex_mcp::McpBinding;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_protocol::items::ModelInvocationContext;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
|
||||
/// Request-scoped state that may change between model sampling requests.
|
||||
@@ -43,4 +44,14 @@ impl StepContext {
|
||||
item.summary = self.settings.reasoning_summary;
|
||||
item
|
||||
}
|
||||
|
||||
pub(crate) fn model_context(&self) -> ModelInvocationContext {
|
||||
ModelInvocationContext {
|
||||
model_slug: self.settings.model_info.slug.clone(),
|
||||
reasoning_effort: self
|
||||
.settings
|
||||
.effective_reasoning_effort()
|
||||
.map(|effort| effort.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
.emit_turn_item_started(
|
||||
turn_context.as_ref(),
|
||||
&TurnItem::CommandExecution(CommandExecutionItem {
|
||||
model_context: None,
|
||||
id: call_id.clone(),
|
||||
plugin_id: None,
|
||||
script_path: None,
|
||||
@@ -271,6 +272,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
.emit_turn_item_completed(
|
||||
turn_context.as_ref(),
|
||||
TurnItem::CommandExecution(CommandExecutionItem {
|
||||
model_context: None,
|
||||
id: call_id,
|
||||
plugin_id: None,
|
||||
script_path: None,
|
||||
@@ -296,6 +298,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
.emit_turn_item_completed(
|
||||
turn_context.as_ref(),
|
||||
TurnItem::CommandExecution(CommandExecutionItem {
|
||||
model_context: None,
|
||||
id: call_id.clone(),
|
||||
plugin_id: None,
|
||||
script_path: None,
|
||||
@@ -341,6 +344,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
.emit_turn_item_completed(
|
||||
turn_context.as_ref(),
|
||||
TurnItem::CommandExecution(CommandExecutionItem {
|
||||
model_context: None,
|
||||
id: call_id,
|
||||
plugin_id: None,
|
||||
script_path: None,
|
||||
|
||||
@@ -701,6 +701,7 @@ impl Session {
|
||||
self.request_command_approval(
|
||||
ctx.review_context.turn(),
|
||||
ExecApprovalKind::Command,
|
||||
ctx.review_context.model_context(),
|
||||
ctx.call_id.clone(),
|
||||
/*approval_id*/ None,
|
||||
Some(environment_id.clone()),
|
||||
@@ -730,6 +731,7 @@ impl Session {
|
||||
self.request_command_approval(
|
||||
ctx.review_context.turn(),
|
||||
ExecApprovalKind::WriteStdin,
|
||||
ctx.review_context.model_context(),
|
||||
id.clone(),
|
||||
Some(approval_id.clone()),
|
||||
Some(environment_id.clone()),
|
||||
@@ -761,6 +763,7 @@ impl Session {
|
||||
self.request_command_approval(
|
||||
ctx.review_context.turn(),
|
||||
ExecApprovalKind::Command,
|
||||
ctx.review_context.model_context(),
|
||||
ctx.call_id.clone(),
|
||||
Some(approval_id.clone()),
|
||||
Some(environment_id.clone()),
|
||||
@@ -833,6 +836,7 @@ impl Session {
|
||||
self.request_command_approval(
|
||||
ctx.review_context.turn(),
|
||||
ExecApprovalKind::Command,
|
||||
ctx.review_context.model_context(),
|
||||
ctx.call_id.clone(),
|
||||
/*approval_id*/ None,
|
||||
Some(environment_id.clone()),
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::items::CommandExecutionItem;
|
||||
use codex_protocol::items::CommandExecutionStatus;
|
||||
use codex_protocol::items::FileChangeItem;
|
||||
use codex_protocol::items::ModelInvocationContext;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
@@ -43,6 +44,7 @@ pub(super) fn truncate_rejection_message(message: &str) -> String {
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct ToolEventCtx<'a> {
|
||||
pub model_context: Option<&'a ModelInvocationContext>,
|
||||
pub session: &'a Session,
|
||||
pub turn: &'a TurnContext,
|
||||
/// Model captured by the step that issued this call, including delayed completion events.
|
||||
@@ -60,6 +62,7 @@ impl<'a> ToolEventCtx<'a> {
|
||||
turn_diff_tracker: Option<&'a SharedTurnDiffTracker>,
|
||||
) -> Self {
|
||||
Self {
|
||||
model_context: None,
|
||||
session,
|
||||
turn,
|
||||
model_info,
|
||||
@@ -168,6 +171,7 @@ async fn emit_exec_command_begin(ctx: ToolEventCtx<'_>, exec_input: &ExecCommand
|
||||
ctx.turn,
|
||||
&TurnItem::CommandExecution(CommandExecutionItem {
|
||||
id: ctx.call_id.to_string(),
|
||||
model_context: ctx.model_context.cloned(),
|
||||
plugin_id,
|
||||
script_path,
|
||||
process_id: exec_input.process_id.map(str::to_owned),
|
||||
@@ -581,6 +585,7 @@ async fn emit_exec_end(
|
||||
ctx.turn,
|
||||
TurnItem::CommandExecution(CommandExecutionItem {
|
||||
id: ctx.call_id.to_string(),
|
||||
model_context: ctx.model_context.cloned(),
|
||||
plugin_id,
|
||||
script_path,
|
||||
process_id: exec_input.process_id.map(str::to_owned),
|
||||
|
||||
@@ -178,6 +178,7 @@ pub(crate) fn spawn_exit_watcher(
|
||||
let session_ref = Arc::clone(&context.session);
|
||||
let turn_ref = Arc::clone(&context.step_context.turn);
|
||||
let model_info = Arc::clone(&context.step_context.settings.model_info);
|
||||
let model_context = context.step_context.model_context();
|
||||
let call_id = context.call_id.clone();
|
||||
let exit_token = process.cancellation_token();
|
||||
let output_drained = process.output_drained_notify();
|
||||
@@ -223,6 +224,7 @@ pub(crate) fn spawn_exit_watcher(
|
||||
exit_code,
|
||||
&session_ref,
|
||||
&turn_ref,
|
||||
&model_context,
|
||||
&call_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -276,11 +276,13 @@ struct InitialExecCommandGuard {
|
||||
|
||||
impl InitialExecCommandGuard {
|
||||
async fn finish_plugin_metrics(&mut self, context: &UnifiedExecContext, exit_code: i32) {
|
||||
let model_context = context.step_context.model_context();
|
||||
finish_and_track_measurements(
|
||||
self.metrics_sidecar.take(),
|
||||
exit_code,
|
||||
&context.session,
|
||||
&context.step_context.turn,
|
||||
&model_context,
|
||||
&context.call_id,
|
||||
)
|
||||
.await;
|
||||
@@ -530,13 +532,15 @@ impl UnifiedExecProcessManager {
|
||||
});
|
||||
|
||||
let transcript = Arc::new(tokio::sync::Mutex::new(HeadTailBuffer::default()));
|
||||
let event_ctx = ToolEventCtx::new(
|
||||
let model_context = context.step_context.model_context();
|
||||
let mut event_ctx = ToolEventCtx::new(
|
||||
context.session.as_ref(),
|
||||
context.step_context.turn.as_ref(),
|
||||
&context.step_context.settings.model_info,
|
||||
&context.call_id,
|
||||
/*turn_diff_tracker*/ None,
|
||||
);
|
||||
event_ctx.model_context = Some(&model_context);
|
||||
let plugin_attribution = if request.turn_environment.environment.is_remote() {
|
||||
let file_system = request.turn_environment.environment.get_filesystem();
|
||||
context
|
||||
@@ -727,6 +731,7 @@ impl UnifiedExecProcessManager {
|
||||
exit_code.unwrap_or(-1),
|
||||
&context.session,
|
||||
&context.step_context.turn,
|
||||
&model_context,
|
||||
&context.call_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -76,6 +76,7 @@ async fn first_request_item_types_roles_and_content_annotations() -> Result<()>
|
||||
.submit(Op::ApproveGuardianDeniedAction {
|
||||
event: GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "guardian-review".to_string(),
|
||||
target_item_id: None,
|
||||
plugin_id: None,
|
||||
|
||||
@@ -169,6 +169,7 @@ fn turn_started_emits_turn_started_event() {
|
||||
fn command_execution_started_and_completed_translate_to_thread_events() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
let command_item = ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "cmd-1".to_string(),
|
||||
command: "ls".to_string(),
|
||||
cwd: test_path_buf("/tmp/project").abs().into(),
|
||||
@@ -211,6 +212,7 @@ fn command_execution_started_and_completed_translate_to_thread_events() {
|
||||
let completed = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "cmd-1".to_string(),
|
||||
command: "ls".to_string(),
|
||||
cwd: test_path_buf("/tmp/project").abs().into(),
|
||||
@@ -1348,6 +1350,7 @@ fn turn_completion_reconciles_started_items_from_turn_items() {
|
||||
let started =
|
||||
processor.collect_thread_events(ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
item: ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "cmd-1".to_string(),
|
||||
command: "ls".to_string(),
|
||||
cwd: test_path_buf("/tmp/project").abs().into(),
|
||||
@@ -1390,6 +1393,7 @@ fn turn_completion_reconciles_started_items_from_turn_items() {
|
||||
id: "turn-1".to_string(),
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "cmd-1".to_string(),
|
||||
command: "ls".to_string(),
|
||||
cwd: test_path_buf("/tmp/project").abs().into(),
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_extension_api::ExtensionData;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_protocol::approvals::GuardianAssessmentAction;
|
||||
use codex_protocol::approvals::GuardianReviewReason;
|
||||
use codex_protocol::items::ModelInvocationContext;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::protocol::GuardianAssessmentDecisionSource;
|
||||
use codex_protocol::protocol::GuardianAssessmentEvent;
|
||||
@@ -36,6 +37,7 @@ pub struct ReviewMetadata {
|
||||
pub reviewed_action: GuardianReviewedAction,
|
||||
pub action: GuardianAssessmentAction,
|
||||
pub review_reason: GuardianReviewReason,
|
||||
pub model_context: ModelInvocationContext,
|
||||
}
|
||||
|
||||
pub struct ReviewReport {
|
||||
@@ -58,6 +60,7 @@ impl ReviewReport {
|
||||
);
|
||||
let started = GuardianAssessmentEvent {
|
||||
review_reason: Some(metadata.review_reason),
|
||||
model_context: Some(metadata.model_context),
|
||||
id: metadata.review_id,
|
||||
target_item_id: metadata.target_item_id,
|
||||
plugin_id: metadata.plugin_id,
|
||||
|
||||
@@ -208,6 +208,10 @@ pub struct GuardianAssessmentEvent {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub review_reason: Option<GuardianReviewReason>,
|
||||
#[serde(skip)]
|
||||
#[schemars(skip)]
|
||||
#[ts(skip)]
|
||||
pub model_context: Option<crate::items::ModelInvocationContext>,
|
||||
/// Stable identifier for this guardian review lifecycle.
|
||||
pub id: String,
|
||||
/// Thread item being reviewed, when the review maps to a concrete item.
|
||||
@@ -264,6 +268,10 @@ pub enum ExecApprovalKind {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct ExecApprovalRequestEvent {
|
||||
#[serde(skip)]
|
||||
#[schemars(skip)]
|
||||
#[ts(skip)]
|
||||
pub model_context: Option<crate::items::ModelInvocationContext>,
|
||||
/// Missing on older events, which retain command approval semantics.
|
||||
#[serde(default)]
|
||||
pub kind: ExecApprovalKind,
|
||||
|
||||
@@ -232,8 +232,19 @@ pub fn is_safe_plugin_relative_path(path: &str) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// Immutable model labels carried within command lifecycle events for analytics.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ModelInvocationContext {
|
||||
pub model_slug: String,
|
||||
pub reasoning_effort: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
|
||||
pub struct CommandExecutionItem {
|
||||
#[serde(skip)]
|
||||
#[schemars(skip)]
|
||||
#[ts(skip)]
|
||||
pub model_context: Option<ModelInvocationContext>,
|
||||
pub id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
|
||||
@@ -5516,6 +5516,7 @@ mod tests {
|
||||
turn_id: "turn-1".into(),
|
||||
started_at_ms: 10,
|
||||
item: TurnItem::CommandExecution(CommandExecutionItem {
|
||||
model_context: None,
|
||||
id: "exec-1".into(),
|
||||
plugin_id: Some("sample@openai-curated".into()),
|
||||
script_path: Some("scripts/run.py".into()),
|
||||
@@ -5542,6 +5543,7 @@ mod tests {
|
||||
started_at_ms: Some(10),
|
||||
completed_at_ms: 20,
|
||||
item: TurnItem::CommandExecution(CommandExecutionItem {
|
||||
model_context: None,
|
||||
id: "exec-1".into(),
|
||||
plugin_id: Some("sample@openai-curated".into()),
|
||||
script_path: Some("scripts/run.py".into()),
|
||||
|
||||
@@ -213,6 +213,7 @@ pub(super) fn completed_item(
|
||||
)),
|
||||
EventMsg::ExecCommandEnd(event) => Some((
|
||||
TurnItem::CommandExecution(CommandExecutionItem {
|
||||
model_context: None,
|
||||
id: event.call_id.clone(),
|
||||
plugin_id: event.plugin_id.clone(),
|
||||
script_path: event.script_path.clone(),
|
||||
|
||||
@@ -10,6 +10,7 @@ fn agent_status_uses_bounded_buffered_activity() {
|
||||
store.push_notification(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "command-1".to_string(),
|
||||
command: "cargo test -p codex-tui".to_string(),
|
||||
cwd: AbsolutePathBuf::try_from("/workspace")
|
||||
|
||||
@@ -95,6 +95,7 @@ mod tests {
|
||||
fn denied_event(id: usize) -> GuardianAssessmentEvent {
|
||||
GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: format!("review-{id}"),
|
||||
target_item_id: None,
|
||||
plugin_id: None,
|
||||
|
||||
@@ -194,6 +194,7 @@ mod tests {
|
||||
|
||||
fn command_execution(call_id: &str) -> ThreadItem {
|
||||
ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: call_id.to_string(),
|
||||
command: "true".to_string(),
|
||||
cwd: AbsolutePathBuf::current_dir().expect("current dir").into(),
|
||||
|
||||
@@ -96,6 +96,7 @@ impl ChatWidget {
|
||||
|
||||
self.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id,
|
||||
target_item_id: None,
|
||||
plugin_id: None,
|
||||
|
||||
@@ -1080,6 +1080,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at_ms: 0,
|
||||
item: AppServerThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "cmd-1".to_string(),
|
||||
command: command.clone(),
|
||||
cwd: test_path_buf("/tmp").abs().into(),
|
||||
@@ -1104,6 +1105,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
item: AppServerThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "cmd-1".to_string(),
|
||||
command,
|
||||
cwd: test_path_buf("/tmp").abs().into(),
|
||||
|
||||
@@ -164,6 +164,7 @@ async fn replayed_commands_preserve_individual_output_and_failure_status() {
|
||||
let cwd = chat.config.cwd.clone();
|
||||
let replayed_command =
|
||||
|id: &str, output: &str, source: ExecCommandSource| AppServerThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: id.to_string(),
|
||||
command: format!("printf {output}"),
|
||||
cwd: cwd.clone().into(),
|
||||
@@ -612,6 +613,7 @@ async fn exec_end_without_begin_uses_event_command() {
|
||||
handle_exec_end(
|
||||
&mut chat,
|
||||
AppServerThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "call-orphan".to_string(),
|
||||
command: codex_shell_command::parse_command::shlex_join(&command),
|
||||
cwd: cwd.into(),
|
||||
|
||||
@@ -4,6 +4,7 @@ use pretty_assertions::assert_eq;
|
||||
fn auto_review_denial_event() -> GuardianAssessmentEvent {
|
||||
GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "auto-review-recent-1".into(),
|
||||
target_item_id: Some("target-auto-review-recent-1".into()),
|
||||
plugin_id: None,
|
||||
@@ -33,6 +34,7 @@ fn guardian_command_event(
|
||||
let terminal = status != GuardianAssessmentStatus::InProgress;
|
||||
GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: id.to_string(),
|
||||
target_item_id: Some(format!("{id}-target")),
|
||||
plugin_id: None,
|
||||
@@ -247,6 +249,7 @@ async fn guardian_denied_exec_renders_warning_and_denied_request() {
|
||||
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "guardian-1".into(),
|
||||
target_item_id: Some("guardian-target-1".into()),
|
||||
plugin_id: None,
|
||||
@@ -264,6 +267,7 @@ async fn guardian_denied_exec_renders_warning_and_denied_request() {
|
||||
chat.on_warning("Automatic approval review denied (risk: high): The planned action would transmit the full contents of a workspace source file (`core/src/codex.rs`) to `https://example.com`, which is an external and untrusted endpoint.");
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "guardian-1".into(),
|
||||
target_item_id: Some("guardian-target-1".into()),
|
||||
plugin_id: None,
|
||||
@@ -318,6 +322,7 @@ async fn guardian_approved_exec_is_hidden_from_history() {
|
||||
);
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "thread:child-thread:guardian-1".into(),
|
||||
target_item_id: Some("guardian-approved-target".into()),
|
||||
plugin_id: None,
|
||||
@@ -376,6 +381,7 @@ async fn guardian_approved_request_permissions_clears_status_without_history() {
|
||||
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "guardian-request-permissions".into(),
|
||||
target_item_id: None,
|
||||
plugin_id: None,
|
||||
@@ -403,6 +409,7 @@ async fn guardian_approved_request_permissions_clears_status_without_history() {
|
||||
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "guardian-request-permissions".into(),
|
||||
target_item_id: None,
|
||||
plugin_id: None,
|
||||
@@ -456,6 +463,7 @@ async fn guardian_timed_out_exec_renders_warning_and_timed_out_request() {
|
||||
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "guardian-1".into(),
|
||||
target_item_id: Some("guardian-target-1".into()),
|
||||
plugin_id: None,
|
||||
@@ -473,6 +481,7 @@ async fn guardian_timed_out_exec_renders_warning_and_timed_out_request() {
|
||||
chat.on_warning("Automatic approval review timed out while evaluating the requested approval.");
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "guardian-1".into(),
|
||||
target_item_id: Some("guardian-target-1".into()),
|
||||
plugin_id: None,
|
||||
|
||||
@@ -883,6 +883,7 @@ pub(super) fn begin_exec_with_source(
|
||||
.map(|parsed| AppServerCommandAction::from_core_with_cwd(parsed, &chat.config.cwd))
|
||||
.collect();
|
||||
let item = AppServerThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: call_id.to_string(),
|
||||
command: codex_shell_command::parse_command::shlex_join(&command),
|
||||
cwd: chat.config.cwd.clone().into(),
|
||||
@@ -908,6 +909,7 @@ pub(super) fn begin_unified_exec_startup(
|
||||
) -> AppServerThreadItem {
|
||||
let command = vec!["bash".to_string(), "-lc".to_string(), raw_cmd.to_string()];
|
||||
let item = AppServerThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: call_id.to_string(),
|
||||
command: codex_shell_command::parse_command::shlex_join(&command),
|
||||
cwd: chat.config.cwd.clone().into(),
|
||||
@@ -1142,6 +1144,7 @@ pub(super) fn end_exec(
|
||||
handle_exec_end(
|
||||
chat,
|
||||
AppServerThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id,
|
||||
command,
|
||||
cwd,
|
||||
|
||||
@@ -535,6 +535,7 @@ async fn mcp_startup_complete_preserves_review_status() {
|
||||
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
review_reason: None,
|
||||
model_context: None,
|
||||
id: "guardian-1".to_string(),
|
||||
target_item_id: Some("guardian-target-1".to_string()),
|
||||
plugin_id: None,
|
||||
|
||||
@@ -5668,6 +5668,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
|
||||
handle_exec_begin(
|
||||
&mut chat,
|
||||
AppServerThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "c1".into(),
|
||||
command: codex_shell_command::parse_command::shlex_join(&command),
|
||||
cwd: cwd.clone().into(),
|
||||
@@ -5685,6 +5686,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
|
||||
handle_exec_end(
|
||||
&mut chat,
|
||||
AppServerThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: "c1".into(),
|
||||
command: codex_shell_command::parse_command::shlex_join(&command),
|
||||
cwd: cwd.into(),
|
||||
|
||||
Reference in New Issue
Block a user