initial draft

This commit is contained in:
Roy Han
2026-03-16 12:28:14 -07:00
parent ec75ad7230
commit 40030331d3
18 changed files with 409 additions and 10 deletions
+3
View File
@@ -391,6 +391,7 @@ async fn spawn_agent_can_fork_parent_thread_history() {
namespace: None,
arguments: "{}".to_string(),
call_id: parent_spawn_call_id.clone(),
metadata: None,
};
parent_thread
.codex
@@ -474,6 +475,7 @@ async fn spawn_agent_fork_injects_output_for_parent_spawn_call() {
namespace: None,
arguments: "{}".to_string(),
call_id: parent_spawn_call_id.clone(),
metadata: None,
};
parent_thread
.codex
@@ -550,6 +552,7 @@ async fn spawn_agent_fork_flushes_parent_rollout_before_loading_history() {
namespace: None,
arguments: "{}".to_string(),
call_id: parent_spawn_call_id.clone(),
metadata: None,
};
parent_thread
.codex
+2
View File
@@ -133,6 +133,7 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies()
namespace: None,
arguments: "{\"old\":true}".to_string(),
call_id: "call_old".to_string(),
metadata: None,
}],
&turn_context,
)
@@ -161,6 +162,7 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies()
env: None,
user: None,
}),
metadata: None,
}],
&turn_context,
)
+1
View File
@@ -79,6 +79,7 @@ fn reserialize_shell_outputs(items: &mut [ResponseItem]) {
call_id,
name,
input: _,
..
} => {
if name == "apply_patch" {
shell_call_ids.insert(call_id.clone());
+4
View File
@@ -147,6 +147,7 @@ fn reserializes_shell_outputs_for_function_and_custom_tool_calls() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-1".to_string(),
metadata: None,
},
ResponseItem::FunctionCallOutput {
call_id: "call-1".to_string(),
@@ -158,6 +159,7 @@ fn reserializes_shell_outputs_for_function_and_custom_tool_calls() {
call_id: "call-2".to_string(),
name: "apply_patch".to_string(),
input: "*** Begin Patch".to_string(),
metadata: None,
},
ResponseItem::CustomToolCallOutput {
call_id: "call-2".to_string(),
@@ -176,6 +178,7 @@ fn reserializes_shell_outputs_for_function_and_custom_tool_calls() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-1".to_string(),
metadata: None,
},
ResponseItem::FunctionCallOutput {
call_id: "call-1".to_string(),
@@ -187,6 +190,7 @@ fn reserializes_shell_outputs_for_function_and_custom_tool_calls() {
call_id: "call-2".to_string(),
name: "apply_patch".to_string(),
input: "*** Begin Patch".to_string(),
metadata: None,
},
ResponseItem::CustomToolCallOutput {
call_id: "call-2".to_string(),
+159
View File
@@ -321,6 +321,8 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::DeveloperInstructions;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::models::ResponseItemMetadata;
use codex_protocol::models::ReviewDecisionMetadata;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::InitialHistory;
@@ -1010,6 +1012,92 @@ fn stamp_user_message_type_on_input_item(item: &mut ResponseInputItem, kind: Use
*metadata = Some(metadata_value);
}
fn review_decision_to_metadata(decision: &ReviewDecision) -> ReviewDecisionMetadata {
match decision {
ReviewDecision::Approved => ReviewDecisionMetadata::Approved,
ReviewDecision::ApprovedExecpolicyAmendment { .. } => {
ReviewDecisionMetadata::ApprovedWithAmendment
}
ReviewDecision::ApprovedForSession => ReviewDecisionMetadata::ApprovedForSession,
ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment,
} => match network_policy_amendment.action {
codex_protocol::protocol::NetworkPolicyRuleAction::Allow => {
ReviewDecisionMetadata::ApprovedWithNetworkPolicyAllow
}
codex_protocol::protocol::NetworkPolicyRuleAction::Deny => {
ReviewDecisionMetadata::DeniedWithNetworkPolicyDeny
}
},
ReviewDecision::Denied => ReviewDecisionMetadata::Denied,
ReviewDecision::Abort => ReviewDecisionMetadata::Abort,
}
}
fn response_item_tool_call_id(item: &ResponseItem) -> Option<&str> {
match item {
ResponseItem::LocalShellCall {
call_id: Some(call_id),
..
} => Some(call_id),
ResponseItem::FunctionCall { call_id, .. } => Some(call_id),
ResponseItem::CustomToolCall { call_id, .. } => Some(call_id),
_ => None,
}
}
fn stamp_tool_metadata_on_response_item(
item: ResponseItem,
metadata: ResponseItemMetadata,
) -> ResponseItem {
match item {
ResponseItem::LocalShellCall {
id,
call_id,
status,
action,
..
} => ResponseItem::LocalShellCall {
id,
call_id,
status,
action,
metadata: Some(metadata),
},
ResponseItem::FunctionCall {
id,
name,
namespace,
arguments,
call_id,
..
} => ResponseItem::FunctionCall {
id,
name,
namespace,
arguments,
call_id,
metadata: Some(metadata),
},
ResponseItem::CustomToolCall {
id,
status,
call_id,
name,
input,
..
} => ResponseItem::CustomToolCall {
id,
status,
call_id,
name,
input,
metadata: Some(metadata),
},
other => other,
}
}
#[derive(Clone)]
pub(crate) struct SessionConfiguration {
/// Provider identifier ("openai", "openrouter", ...).
@@ -2904,6 +2992,10 @@ impl Session {
match active.as_mut() {
Some(at) => {
let mut ts = at.turn_state.lock().await;
ts.insert_pending_approval_call_id(
effective_approval_id.clone(),
call_id.clone(),
);
ts.insert_pending_approval(effective_approval_id.clone(), tx_approve)
}
None => None,
@@ -2969,6 +3061,7 @@ impl Session {
match active.as_mut() {
Some(at) => {
let mut ts = at.turn_state.lock().await;
ts.insert_pending_approval_call_id(approval_id.clone(), call_id.clone());
ts.insert_pending_approval(approval_id.clone(), tx_approve)
}
None => None,
@@ -3254,7 +3347,20 @@ impl Session {
}
}
pub async fn record_approval_outcome(&self, approval_id: &str, decision: &ReviewDecision) {
let mut active = self.active_turn.lock().await;
let Some(at) = active.as_mut() else {
return;
};
let mut ts = at.turn_state.lock().await;
let call_id = ts
.remove_pending_approval_call_id(approval_id)
.unwrap_or_else(|| approval_id.to_string());
ts.record_approval_outcome(call_id, review_decision_to_metadata(decision));
}
pub async fn notify_approval(&self, approval_id: &str, decision: ReviewDecision) {
self.record_approval_outcome(approval_id, &decision).await;
let entry = {
let mut active = self.active_turn.lock().await;
match active.as_mut() {
@@ -3275,6 +3381,53 @@ impl Session {
}
}
async fn stamp_tool_approval_metadata(
&self,
_turn_context: &TurnContext,
response_item: ResponseItem,
) -> ResponseItem {
if !self.enabled(Feature::ItemMetadata) {
return response_item;
}
let Some(call_id) = response_item_tool_call_id(&response_item) else {
return response_item;
};
let (outcome, has_pending_approval) = {
let active = self.active_turn.lock().await;
let Some(at) = active.as_ref() else {
return response_item;
};
let ts = at.turn_state.lock().await;
let outcome = ts.approval_outcome_for_call_id(call_id);
let has_pending_approval = ts.has_pending_approval_for_call_id(call_id);
(outcome, has_pending_approval)
};
let mut metadata = match &response_item {
ResponseItem::LocalShellCall { metadata, .. }
| ResponseItem::FunctionCall { metadata, .. }
| ResponseItem::CustomToolCall { metadata, .. } => metadata.clone().unwrap_or_default(),
_ => return response_item,
};
match outcome {
Some(review_decision) => {
metadata.is_tool_call_escalated = Some(true);
metadata.review_decision = Some(review_decision);
}
None if !has_pending_approval => {
metadata.is_tool_call_escalated = Some(false);
metadata.review_decision = None;
}
None => {
return response_item;
}
}
stamp_tool_metadata_on_response_item(response_item, metadata)
}
pub async fn resolve_elicitation(
&self,
server_name: String,
@@ -3769,6 +3922,10 @@ impl Session {
turn_context: &TurnContext,
response_item: ResponseItem,
) {
let response_item = self
.stamp_tool_approval_metadata(turn_context, response_item)
.await;
// Add to conversation history and persist response item to rollout.
self.record_conversation_items(turn_context, std::slice::from_ref(&response_item))
.await;
@@ -4731,6 +4888,8 @@ mod handlers {
}
match decision {
ReviewDecision::Abort => {
sess.record_approval_outcome(&approval_id, &ReviewDecision::Abort)
.await;
sess.interrupt_task().await;
}
other => sess.notify_approval(&approval_id, other).await,
@@ -77,6 +77,7 @@ async fn forward_events_cancelled_while_send_blocked_shuts_down_delegate() {
call_id: "call-1".to_string(),
name: "tool".to_string(),
input: "{}".to_string(),
metadata: None,
},
}),
})
+132
View File
@@ -3920,6 +3920,137 @@ async fn task_finish_emits_prompt_queued_metadata_for_injected_user_input_when_f
));
}
#[test]
fn review_decision_metadata_mapping_is_stable() {
assert_eq!(
review_decision_to_metadata(&ReviewDecision::Approved),
codex_protocol::models::ReviewDecisionMetadata::Approved
);
assert_eq!(
review_decision_to_metadata(&ReviewDecision::Denied),
codex_protocol::models::ReviewDecisionMetadata::Denied
);
assert_eq!(
review_decision_to_metadata(&ReviewDecision::Abort),
codex_protocol::models::ReviewDecisionMetadata::Abort
);
assert_eq!(
review_decision_to_metadata(&ReviewDecision::ApprovedForSession),
codex_protocol::models::ReviewDecisionMetadata::ApprovedForSession
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn tool_call_metadata_stamps_escalated_review_decision_when_feature_enabled() {
let (mut sess, tc, rx) = make_session_and_context_with_rx().await;
Arc::get_mut(&mut sess)
.expect("session should be uniquely owned in this test")
.features
.enable(crate::features::Feature::ItemMetadata)
.expect("feature flag should be enabled for this test");
sess.spawn_task(
Arc::clone(&tc),
vec![UserInput::Text {
text: "start".to_string(),
text_elements: Vec::new(),
}],
NeverEndingTask {
kind: TaskKind::Regular,
listen_to_cancellation_token: false,
},
)
.await;
while rx.try_recv().is_ok() {}
sess.record_approval_outcome("call-1", &ReviewDecision::Denied)
.await;
sess.record_response_item_and_emit_turn_item(
tc.as_ref(),
ResponseItem::FunctionCall {
id: None,
name: "shell".to_string(),
namespace: None,
arguments: "{}".to_string(),
call_id: "call-1".to_string(),
metadata: None,
},
)
.await;
let event = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv())
.await
.expect("expected raw response item event")
.expect("channel open");
assert!(matches!(
event.msg,
EventMsg::RawResponseItem(ref ev)
if matches!(
&ev.item,
ResponseItem::FunctionCall {
metadata: Some(metadata),
..
} if metadata.is_tool_call_escalated == Some(true)
&& metadata.review_decision
== Some(codex_protocol::models::ReviewDecisionMetadata::Denied)
)
));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn tool_call_metadata_stamps_non_escalated_false_when_feature_enabled() {
let (mut sess, tc, rx) = make_session_and_context_with_rx().await;
Arc::get_mut(&mut sess)
.expect("session should be uniquely owned in this test")
.features
.enable(crate::features::Feature::ItemMetadata)
.expect("feature flag should be enabled for this test");
sess.spawn_task(
Arc::clone(&tc),
vec![UserInput::Text {
text: "start".to_string(),
text_elements: Vec::new(),
}],
NeverEndingTask {
kind: TaskKind::Regular,
listen_to_cancellation_token: false,
},
)
.await;
while rx.try_recv().is_ok() {}
sess.record_response_item_and_emit_turn_item(
tc.as_ref(),
ResponseItem::FunctionCall {
id: None,
name: "shell".to_string(),
namespace: None,
arguments: "{}".to_string(),
call_id: "call-2".to_string(),
metadata: None,
},
)
.await;
let event = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv())
.await
.expect("expected raw response item event")
.expect("channel open");
assert!(matches!(
event.msg,
EventMsg::RawResponseItem(ref ev)
if matches!(
&ev.item,
ResponseItem::FunctionCall {
metadata: Some(metadata),
..
} if metadata.is_tool_call_escalated == Some(false)
&& metadata.review_decision.is_none()
)
));
}
#[tokio::test]
async fn steer_input_requires_active_turn() {
let (sess, _tc, _rx) = make_session_and_context_with_rx().await;
@@ -4119,6 +4250,7 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
call_id: "call-1".to_string(),
name: "shell".to_string(),
input: "{}".to_string(),
metadata: None,
};
let call = ToolRouter::build_tool_call(session.as_ref(), item.clone())
@@ -281,6 +281,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-1".to_string(),
metadata: None,
},
ResponseItem::FunctionCallOutput {
call_id: "call-1".to_string(),
@@ -300,6 +301,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
call_id: "tool-1".to_string(),
name: "js_repl".to_string(),
input: "view_image".to_string(),
metadata: None,
},
ResponseItem::CustomToolCallOutput {
call_id: "tool-1".to_string(),
@@ -344,6 +346,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-1".to_string(),
metadata: None,
},
ResponseItem::FunctionCallOutput {
call_id: "call-1".to_string(),
@@ -363,6 +366,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
call_id: "tool-1".to_string(),
name: "js_repl".to_string(),
input: "view_image".to_string(),
metadata: None,
},
ResponseItem::CustomToolCallOutput {
call_id: "tool-1".to_string(),
@@ -536,6 +540,7 @@ fn remove_first_item_removes_matching_output_for_function_call() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-1".to_string(),
metadata: None,
},
ResponseItem::FunctionCallOutput {
call_id: "call-1".to_string(),
@@ -560,6 +565,7 @@ fn remove_first_item_removes_matching_call_for_output() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-2".to_string(),
metadata: None,
},
];
let mut h = create_history_with_items(items);
@@ -577,6 +583,7 @@ fn remove_last_item_removes_matching_call_for_output() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-delete-last".to_string(),
metadata: None,
},
ResponseItem::FunctionCallOutput {
call_id: "call-delete-last".to_string(),
@@ -661,6 +668,7 @@ fn remove_first_item_handles_local_shell_pair() {
env: None,
user: None,
}),
metadata: None,
},
ResponseItem::FunctionCallOutput {
call_id: "call-3".to_string(),
@@ -817,6 +825,7 @@ fn remove_first_item_handles_custom_tool_pair() {
call_id: "tool-1".to_string(),
name: "my_tool".to_string(),
input: "{}".to_string(),
metadata: None,
},
ResponseItem::CustomToolCallOutput {
call_id: "tool-1".to_string(),
@@ -842,6 +851,7 @@ fn normalization_retains_local_shell_outputs() {
env: None,
user: None,
}),
metadata: None,
},
ResponseItem::FunctionCallOutput {
call_id: "shell-1".to_string(),
@@ -1052,6 +1062,7 @@ fn normalize_adds_missing_output_for_function_call() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-x".to_string(),
metadata: None,
}];
let mut h = create_history_with_items(items);
@@ -1066,6 +1077,7 @@ fn normalize_adds_missing_output_for_function_call() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-x".to_string(),
metadata: None,
},
ResponseItem::FunctionCallOutput {
call_id: "call-x".to_string(),
@@ -1084,6 +1096,7 @@ fn normalize_adds_missing_output_for_custom_tool_call() {
call_id: "tool-x".to_string(),
name: "custom".to_string(),
input: "{}".to_string(),
metadata: None,
}];
let mut h = create_history_with_items(items);
@@ -1098,6 +1111,7 @@ fn normalize_adds_missing_output_for_custom_tool_call() {
call_id: "tool-x".to_string(),
name: "custom".to_string(),
input: "{}".to_string(),
metadata: None,
},
ResponseItem::CustomToolCallOutput {
call_id: "tool-x".to_string(),
@@ -1121,6 +1135,7 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id() {
env: None,
user: None,
}),
metadata: None,
}];
let mut h = create_history_with_items(items);
@@ -1140,6 +1155,7 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id() {
env: None,
user: None,
}),
metadata: None,
},
ResponseItem::FunctionCallOutput {
call_id: "shell-1".to_string(),
@@ -1188,6 +1204,7 @@ fn normalize_mixed_inserts_and_removals() {
namespace: None,
arguments: "{}".to_string(),
call_id: "c1".to_string(),
metadata: None,
},
// Orphan output that should be removed
ResponseItem::FunctionCallOutput {
@@ -1201,6 +1218,7 @@ fn normalize_mixed_inserts_and_removals() {
call_id: "t1".to_string(),
name: "tool".to_string(),
input: "{}".to_string(),
metadata: None,
},
// Local shell call also gets an inserted function call output
ResponseItem::LocalShellCall {
@@ -1214,6 +1232,7 @@ fn normalize_mixed_inserts_and_removals() {
env: None,
user: None,
}),
metadata: None,
},
];
let mut h = create_history_with_items(items);
@@ -1229,6 +1248,7 @@ fn normalize_mixed_inserts_and_removals() {
namespace: None,
arguments: "{}".to_string(),
call_id: "c1".to_string(),
metadata: None,
},
ResponseItem::FunctionCallOutput {
call_id: "c1".to_string(),
@@ -1240,6 +1260,7 @@ fn normalize_mixed_inserts_and_removals() {
call_id: "t1".to_string(),
name: "tool".to_string(),
input: "{}".to_string(),
metadata: None,
},
ResponseItem::CustomToolCallOutput {
call_id: "t1".to_string(),
@@ -1273,6 +1294,7 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-x".to_string(),
metadata: None,
}];
let mut h = create_history_with_items(items);
h.normalize_history(&default_input_modalities());
@@ -1285,6 +1307,7 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-x".to_string(),
metadata: None,
},
ResponseItem::FunctionCallOutput {
call_id: "call-x".to_string(),
@@ -1337,6 +1360,7 @@ fn normalize_adds_missing_output_for_custom_tool_call_panics_in_debug() {
call_id: "tool-x".to_string(),
name: "custom".to_string(),
input: "{}".to_string(),
metadata: None,
}];
let mut h = create_history_with_items(items);
h.normalize_history(&default_input_modalities());
@@ -1357,6 +1381,7 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id_panics_in_debug()
env: None,
user: None,
}),
metadata: None,
}];
let mut h = create_history_with_items(items);
h.normalize_history(&default_input_modalities());
@@ -1450,6 +1475,7 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() {
namespace: None,
arguments: "{}".to_string(),
call_id: "c1".to_string(),
metadata: None,
},
ResponseItem::FunctionCallOutput {
call_id: "c2".to_string(),
@@ -1461,6 +1487,7 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() {
call_id: "t1".to_string(),
name: "tool".to_string(),
input: "{}".to_string(),
metadata: None,
},
ResponseItem::LocalShellCall {
id: None,
@@ -1473,6 +1500,7 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() {
env: None,
user: None,
}),
metadata: None,
},
];
let mut h = create_history_with_items(items);
-4
View File
@@ -45,10 +45,6 @@ const ALIASES: &[Alias] = &[
legacy_key: "memory_tool",
feature: Feature::MemoryTool,
},
Alias {
legacy_key: "user_message_type_metadata",
feature: Feature::ItemMetadata,
},
];
pub(crate) fn legacy_feature_keys() -> impl Iterator<Item = &'static str> {
-6
View File
@@ -142,12 +142,6 @@ fn collab_is_legacy_alias_for_multi_agent() {
assert_eq!(feature_for_key("collab"), Some(Feature::Collab));
}
#[test]
fn user_message_type_metadata_is_legacy_alias_for_item_metadata() {
assert_eq!(feature_for_key("user_message_type_metadata"), Some(Feature::ItemMetadata));
assert_eq!(feature_for_key("item_metadata"), Some(Feature::ItemMetadata));
}
#[test]
fn item_metadata_is_under_development_and_disabled_by_default() {
assert_eq!(Feature::ItemMetadata.stage(), Stage::UnderDevelopment);
+2
View File
@@ -115,6 +115,7 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() {
namespace: None,
arguments: "{\"path\":\"README.md\"}".to_string(),
call_id: "call-1".to_string(),
metadata: None,
},
ResponseItem::FunctionCallOutput {
call_id: "call-1".to_string(),
@@ -444,6 +445,7 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot()
namespace: None,
arguments: "{\"repo\":\"openai/codex\"}".to_string(),
call_id: "call-1".to_string(),
metadata: None,
},
ResponseItem::FunctionCallOutput {
call_id: "call-1".to_string(),
@@ -54,6 +54,7 @@ fn truncates_rollout_from_start_before_nth_user_only() {
name: "tool".to_string(),
namespace: None,
arguments: "{}".to_string(),
metadata: None,
},
assistant_msg("a4"),
];
+38
View File
@@ -10,6 +10,7 @@ use tokio_util::task::AbortOnDropHandle;
use codex_protocol::dynamic_tools::DynamicToolResponse;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ReviewDecisionMetadata;
use codex_protocol::models::UserMessageType;
use codex_protocol::request_permissions::RequestPermissionsResponse;
use codex_protocol::request_user_input::RequestUserInputResponse;
@@ -83,6 +84,8 @@ impl ActiveTurn {
#[derive(Default)]
pub(crate) struct TurnState {
pending_approvals: HashMap<String, oneshot::Sender<ReviewDecision>>,
pending_approval_call_ids: HashMap<String, String>,
approval_outcomes_by_call_id: HashMap<String, ReviewDecisionMetadata>,
pending_request_permissions: HashMap<String, oneshot::Sender<RequestPermissionsResponse>>,
pending_user_input: HashMap<String, oneshot::Sender<RequestUserInputResponse>>,
pending_elicitations: HashMap<(String, RequestId), oneshot::Sender<ElicitationResponse>>,
@@ -102,6 +105,39 @@ impl TurnState {
self.pending_approvals.insert(key, tx)
}
pub(crate) fn insert_pending_approval_call_id(
&mut self,
approval_key: String,
call_id: String,
) -> Option<String> {
self.pending_approval_call_ids.insert(approval_key, call_id)
}
pub(crate) fn remove_pending_approval_call_id(&mut self, approval_key: &str) -> Option<String> {
self.pending_approval_call_ids.remove(approval_key)
}
pub(crate) fn record_approval_outcome(
&mut self,
call_id: String,
decision: ReviewDecisionMetadata,
) {
self.approval_outcomes_by_call_id.insert(call_id, decision);
}
pub(crate) fn approval_outcome_for_call_id(
&self,
call_id: &str,
) -> Option<ReviewDecisionMetadata> {
self.approval_outcomes_by_call_id.get(call_id).cloned()
}
pub(crate) fn has_pending_approval_for_call_id(&self, call_id: &str) -> bool {
self.pending_approval_call_ids
.values()
.any(|pending_call_id| pending_call_id == call_id)
}
pub(crate) fn remove_pending_approval(
&mut self,
key: &str,
@@ -111,6 +147,8 @@ impl TurnState {
pub(crate) fn clear_pending(&mut self) {
self.pending_approvals.clear();
self.pending_approval_call_ids.clear();
self.approval_outcomes_by_call_id.clear();
self.pending_request_permissions.clear();
self.pending_user_input.clear();
self.pending_elicitations.clear();
@@ -56,6 +56,7 @@ fn drops_from_last_user_only() {
name: "tool".to_string(),
namespace: None,
arguments: "{}".to_string(),
metadata: None,
},
assistant_msg("a4"),
];
+1
View File
@@ -139,6 +139,7 @@ async fn build_tool_call_uses_namespace_for_registry_name() -> anyhow::Result<()
namespace: Some("mcp__codex_apps__calendar".to_string()),
arguments: "{}".to_string(),
call_id: "call-namespace".to_string(),
metadata: None,
},
)
.await?
+2
View File
@@ -83,6 +83,7 @@ fn response_item_records_turn_ttft_for_first_output_signals() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-1".to_string(),
metadata: None,
}
));
assert!(response_item_records_turn_ttft(
@@ -92,6 +93,7 @@ fn response_item_records_turn_ttft_for_first_output_signals() {
call_id: "call-2".to_string(),
name: "custom".to_string(),
input: "echo hi".to_string(),
metadata: None,
}
));
assert!(response_item_records_turn_ttft(&ResponseItem::Message {
+6
View File
@@ -366,6 +366,7 @@ async fn resume_replays_legacy_js_repl_image_rollout_shapes() {
call_id: "legacy-js-call".to_string(),
name: "js_repl".to_string(),
input: "console.log('legacy image flow')".to_string(),
metadata: None,
};
let legacy_image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
let rollout = vec![
@@ -522,6 +523,7 @@ async fn resume_replays_image_tool_outputs_with_detail() {
namespace: None,
arguments: "{\"path\":\"/tmp/example.webp\"}".to_string(),
call_id: function_call_id.to_string(),
metadata: None,
}),
},
RolloutLine {
@@ -544,6 +546,7 @@ async fn resume_replays_image_tool_outputs_with_detail() {
call_id: custom_call_id.to_string(),
name: "js_repl".to_string(),
input: "console.log('image flow')".to_string(),
metadata: None,
}),
},
RolloutLine {
@@ -1887,6 +1890,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
namespace: None,
arguments: "{}".into(),
call_id: "function-call-id".into(),
metadata: None,
});
prompt.input.push(ResponseItem::FunctionCallOutput {
call_id: "function-call-id".into(),
@@ -1903,6 +1907,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
env: None,
user: None,
}),
metadata: None,
});
prompt.input.push(ResponseItem::CustomToolCall {
id: Some("custom-tool-id".into()),
@@ -1910,6 +1915,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
call_id: "custom-tool-call-id".into(),
name: "custom_tool".into(),
input: "{}".into(),
metadata: None,
});
prompt.input.push(ResponseItem::CustomToolCallOutput {
call_id: "custom-tool-call-id".into(),
+28
View File
@@ -262,11 +262,30 @@ pub enum UserMessageType {
PromptQueued,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum ReviewDecisionMetadata {
Approved,
Denied,
Abort,
ApprovedForSession,
ApprovedWithAmendment,
ApprovedWithNetworkPolicyAllow,
DeniedWithNetworkPolicyDeny,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
pub struct ResponseItemMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub user_message_type: Option<UserMessageType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub is_tool_call_escalated: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub review_decision: Option<ReviewDecisionMetadata>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
@@ -344,6 +363,9 @@ pub enum ResponseItem {
call_id: Option<String>,
status: LocalShellStatus,
action: LocalShellAction,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
metadata: Option<ResponseItemMetadata>,
},
FunctionCall {
#[serde(default, skip_serializing)]
@@ -358,6 +380,9 @@ pub enum ResponseItem {
// Session::handle_function_call parse it into a Value.
arguments: String,
call_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
metadata: Option<ResponseItemMetadata>,
},
ToolSearchCall {
#[serde(default, skip_serializing)]
@@ -391,6 +416,9 @@ pub enum ResponseItem {
call_id: String,
name: String,
input: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
metadata: Option<ResponseItemMetadata>,
},
// `custom_tool_call_output.output` uses the same wire encoding as
// `function_call_output.output` so freeform tools can return either plain