merge main into resume fork optimization

This commit is contained in:
anais
2026-06-23 08:53:41 -07:00
554 changed files with 19603 additions and 5441 deletions
+62 -8
View File
@@ -440,12 +440,18 @@
"auth_elicitation": {
"type": "boolean"
},
"auto_compaction": {
"type": "boolean"
},
"browser_use": {
"type": "boolean"
},
"browser_use_external": {
"type": "boolean"
},
"browser_use_full_cdp_access": {
"type": "boolean"
},
"chronicle": {
"type": "boolean"
},
@@ -669,7 +675,7 @@
"type": "boolean"
},
"token_budget": {
"type": "boolean"
"$ref": "#/definitions/FeatureToml_for_TokenBudgetConfigToml"
},
"tool_call_mcp_elicitation": {
"type": "boolean"
@@ -967,6 +973,16 @@
}
]
},
"FeatureToml_for_TokenBudgetConfigToml": {
"anyOf": [
{
"type": "boolean"
},
{
"$ref": "#/definitions/TokenBudgetConfigToml"
}
]
},
"FeedbackConfigToml": {
"additionalProperties": false,
"properties": {
@@ -1308,6 +1324,9 @@
],
"description": "One action binding value in config.\n\nThis accepts either:\n\n1. A single key spec string (`\"ctrl-a\"`). 2. A list of key spec strings (`[\"ctrl-a\", \"alt-a\"]`).\n\nAn empty list explicitly unbinds the action in that scope. Because an explicit empty list is still a configured value, runtime resolution must not fall through to global or built-in defaults for that action."
},
"LegacyAppPathString": {
"type": "string"
},
"MarketplaceConfig": {
"additionalProperties": false,
"properties": {
@@ -1712,6 +1731,7 @@
"type": "string"
},
"usage_hint_enabled": {
"description": "Deprecated compatibility field. Its value is ignored.",
"type": "boolean"
},
"usage_hint_text": {
@@ -2423,8 +2443,12 @@
"type": "string"
},
"cwd": {
"default": null,
"type": "string"
"allOf": [
{
"$ref": "#/definitions/LegacyAppPathString"
}
],
"default": null
},
"default_tools_approval_mode": {
"allOf": [
@@ -2660,10 +2684,13 @@
"minimum": 0.0,
"type": "number"
},
"reminder_interval_tokens": {
"format": "int64",
"minimum": 1.0,
"type": "integer"
"reminder_at_remaining_tokens": {
"description": "Remaining weighted-token values that trigger reminders when crossed.",
"items": {
"format": "int64",
"type": "integer"
},
"type": "array"
},
"sampling_token_weight": {
"format": "double",
@@ -2836,6 +2863,27 @@
}
]
},
"TokenBudgetConfigToml": {
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"reminder_message_template": {
"description": "Reminder template. `{n_remaining}` is replaced with the tokens remaining before auto-compaction.",
"maxLength": 1000,
"minLength": 1,
"type": "string"
},
"reminder_threshold_tokens": {
"description": "Number of tokens remaining before auto-compaction when the wrap-up reminder is emitted.",
"format": "int64",
"minimum": 1.0,
"type": "integer"
}
},
"type": "object"
},
"ToolSuggestConfig": {
"additionalProperties": false,
"properties": {
@@ -4703,12 +4751,18 @@
"auth_elicitation": {
"type": "boolean"
},
"auto_compaction": {
"type": "boolean"
},
"browser_use": {
"type": "boolean"
},
"browser_use_external": {
"type": "boolean"
},
"browser_use_full_cdp_access": {
"type": "boolean"
},
"chronicle": {
"type": "boolean"
},
@@ -4932,7 +4986,7 @@
"type": "boolean"
},
"token_budget": {
"type": "boolean"
"$ref": "#/definitions/FeatureToml_for_TokenBudgetConfigToml"
},
"tool_call_mcp_elicitation": {
"type": "boolean"
+4 -4
View File
@@ -41,7 +41,8 @@ fn keep_forked_rollout_item(item: &RolloutItem, preserve_reference_context_item:
_ => false,
},
RolloutItem::ResponseItem(
ResponseItem::AgentMessage { .. }
ResponseItem::AdditionalTools { .. }
| ResponseItem::AgentMessage { .. }
| ResponseItem::Reasoning { .. }
| ResponseItem::LocalShellCall { .. }
| ResponseItem::FunctionCall { .. }
@@ -142,7 +143,7 @@ impl AgentControl {
.items;
let initial_history = InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history,
history: Arc::new(history),
rollout_path: stored_thread.rollout_path,
});
if initial_history.get_multi_agent_version() != Some(MultiAgentVersion::V2) {
@@ -498,7 +499,6 @@ impl AgentControl {
}
if preserve_reference_context_item
&& multi_agent_version == MultiAgentVersion::V2
&& config.multi_agent_v2.usage_hint_enabled
&& let Some(subagent_usage_hint_text) =
config.multi_agent_v2.subagent_usage_hint_text.clone()
&& let Some(subagent_usage_hint_message) =
@@ -626,7 +626,7 @@ impl AgentControl {
.items;
let initial_history = InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history,
history: Arc::new(history),
rollout_path: stored_thread.rollout_path,
});
let parent_thread_id = stored_thread.parent_thread_id;
+44 -56
View File
@@ -72,7 +72,7 @@ fn assistant_message(text: &str, phase: Option<MessagePhase>) -> ResponseItem {
text: text.to_string(),
}],
phase,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -92,7 +92,7 @@ fn spawn_agent_call(call_id: &str) -> ResponseItem {
namespace: None,
arguments: "{}".to_string(),
call_id: call_id.to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -850,10 +850,7 @@ async fn spawn_thread_subagent_uses_supplied_initial_multi_agent_mode_without_hi
.config_snapshot()
.await;
assert_eq!(
child_snapshot.multi_agent_mode,
Some(MultiAgentMode::Proactive)
);
assert_eq!(child_snapshot.multi_agent_mode, MultiAgentMode::Proactive);
}
#[tokio::test]
@@ -890,6 +887,15 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
parent_thread
.inject_user_message_without_turn("parent seed context".to_string())
.await;
let expected_parent_seed = parent_thread
.codex
.session
.clone_history()
.await
.raw_items()
.first()
.cloned()
.expect("parent seed should be recorded");
let turn_context = parent_thread.codex.session.new_default_turn().await;
let parent_spawn_call_id = "spawn-call-history".to_string();
let trigger_message = InterAgentCommunication::new(
@@ -912,7 +918,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
text: "Parent root guidance.".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -921,7 +927,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
text: "Parent subagent guidance.".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
assistant_message("parent commentary", Some(MessagePhase::Commentary)),
assistant_message("parent final answer", Some(MessagePhase::FinalAnswer)),
@@ -931,7 +937,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
summary: Vec::new(),
content: None,
encrypted_content: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
trigger_message.to_response_input_item().into(),
spawn_agent_call(&parent_spawn_call_id),
@@ -987,21 +993,16 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
.expect("child thread should be registered");
assert_eq!(
child_thread.config_snapshot().await.multi_agent_mode,
Some(MultiAgentMode::Proactive)
MultiAgentMode::Proactive
);
assert_ne!(child_thread_id, parent_thread_id);
let history = child_thread.codex.session.clone_history().await;
let mut expected_final_answer =
assistant_message("parent final answer", Some(MessagePhase::FinalAnswer));
expected_final_answer.set_turn_id_if_missing(&turn_context.sub_id);
let expected_history = [
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "parent seed context".to_string(),
}],
phase: None,
metadata: None,
},
assistant_message("parent final answer", Some(MessagePhase::FinalAnswer)),
expected_parent_seed,
expected_final_answer,
ResponseItem::Message {
id: None,
role: "developer".to_string(),
@@ -1009,7 +1010,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
text: "Child subagent guidance.".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
assert_eq!(
@@ -1025,18 +1026,13 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
"full-history forked child should preserve the parent diff baseline"
);
let mut disabled_hint_child_config = harness.config.clone();
let _ = disabled_hint_child_config
.features
.enable(Feature::MultiAgentV2);
disabled_hint_child_config.multi_agent_v2.usage_hint_enabled = false;
disabled_hint_child_config
.multi_agent_v2
.subagent_usage_hint_text = Some("Disabled child subagent guidance.".to_string());
let disabled_hint_child_thread_id = harness
let mut no_hint_child_config = harness.config.clone();
let _ = no_hint_child_config.features.enable(Feature::MultiAgentV2);
no_hint_child_config.multi_agent_v2.subagent_usage_hint_text = None;
let no_hint_child_thread_id = harness
.control
.spawn_agent_with_metadata(
disabled_hint_child_config,
no_hint_child_config,
text_input("child task without hints"),
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
@@ -1052,24 +1048,17 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
},
)
.await
.expect("forked spawn should honor disabled usage hints")
.expect("forked spawn should honor an empty subagent usage hint")
.thread_id;
let disabled_hint_child_thread = harness
let no_hint_child_thread = harness
.manager
.get_thread(disabled_hint_child_thread_id)
.get_thread(no_hint_child_thread_id)
.await
.expect("disabled-hint child thread should be registered");
let disabled_hint_history = disabled_hint_child_thread
.codex
.session
.clone_history()
.await;
.expect("no-hint child thread should be registered");
let no_hint_history = no_hint_child_thread.codex.session.clone_history().await;
assert!(
!history_contains_text(
disabled_hint_history.raw_items(),
"Disabled child subagent guidance.",
),
"full-history forked child should not add subagent guidance when usage hints are disabled"
!history_contains_text(no_hint_history.raw_items(), "Child subagent guidance."),
"full-history forked child should not add empty subagent guidance"
);
let expected = (
@@ -1099,9 +1088,9 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
.expect("child shutdown should submit");
let _ = harness
.control
.shutdown_live_agent(disabled_hint_child_thread_id)
.shutdown_live_agent(no_hint_child_thread_id)
.await
.expect("disabled-hint child shutdown should submit");
.expect("no-hint child shutdown should submit");
let _ = parent_thread
.submit(Op::Shutdown {})
.await
@@ -1140,7 +1129,7 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() {
text: "compacted parent summary".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -1149,7 +1138,7 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() {
text: "Parent root guidance.".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
parent_thread
@@ -1160,6 +1149,8 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() {
message: String::new(),
replacement_history: Some(replacement_history),
window_number: None,
first_window_id: None,
previous_window_id: None,
window_id: None,
}),
RolloutItem::TurnContext(turn_context.to_turn_context_item()),
@@ -1404,7 +1395,7 @@ async fn spawn_agent_fork_last_n_turns_keeps_only_recent_turns() {
.expect("child thread should be registered");
assert_eq!(
child_thread.config_snapshot().await.multi_agent_mode,
Some(MultiAgentMode::Proactive)
MultiAgentMode::Proactive
);
let history = child_thread.codex.session.clone_history().await;
@@ -1462,7 +1453,7 @@ async fn spawn_agent_fork_last_n_turns_drops_parent_startup_prefix_when_under_li
text: "parent startup developer context".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}],
)
.await;
@@ -1584,7 +1575,7 @@ async fn spawn_agent_fork_last_n_turns_strips_parent_usage_hints() {
text: "Parent root guidance.".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
spawn_agent_call(&parent_spawn_call_id),
],
@@ -2420,10 +2411,7 @@ async fn resume_thread_subagent_restores_stored_metadata_and_effective_multi_age
assert_eq!(resumed_agent_path, Some(agent_path));
assert_eq!(resumed_nickname, Some(original_nickname));
assert_eq!(resumed_role, Some("explorer".to_string()));
assert_eq!(
resumed_snapshot.multi_agent_mode,
Some(MultiAgentMode::Proactive)
);
assert_eq!(resumed_snapshot.multi_agent_mode, MultiAgentMode::Proactive);
let _ = harness
.control
+1 -1
View File
@@ -8,7 +8,7 @@ use tempfile::tempdir;
fn convert_apply_patch_maps_add_variant() {
let tmp = tempdir().expect("tmp");
let path = tmp.path().join("a.txt");
let path_uri = PathUri::from_path(&path).expect("absolute test path");
let path_uri = PathUri::from_host_native_path(&path).expect("absolute test path");
let action = ApplyPatchAction::new_add_for_test(&path_uri, "hello".to_string());
let got = convert_apply_patch_to_protocol(&action);
+68 -16
View File
@@ -73,6 +73,7 @@ use codex_otel::current_span_w3c_trace_context;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::config_types::Verbosity as VerbosityConfig;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
@@ -112,7 +113,6 @@ use crate::feedback_tags;
use crate::responses_metadata::CodexResponsesMetadata;
use crate::responses_metadata::subagent_header_value;
use crate::util::emit_feedback_auth_recovery_tags;
use codex_api::map_api_error;
use codex_feedback::FeedbackRequestTags;
use codex_feedback::emit_feedback_request_tags_with_auth_env;
use codex_login::auth_env_telemetry::AuthEnvTelemetry;
@@ -163,6 +163,19 @@ pub(crate) struct CompactConversationRequestSettings {
pub(crate) service_tier: Option<String>,
}
fn session_telemetry_for_request(
session_telemetry: &SessionTelemetry,
request: &ResponsesApiRequest,
) -> SessionTelemetry {
session_telemetry.clone().with_inference_request(
request.service_tier.as_deref(),
request
.reasoning
.as_ref()
.and_then(|reasoning| reasoning.effort.as_ref()),
)
}
/// Session-scoped state shared by all [`ModelClient`] clones.
///
/// This is intentionally kept minimal so `ModelClient` does not need to hold a full `Config`. Most
@@ -577,7 +590,7 @@ impl ModelClient {
turn_state.as_deref(),
)
.await
.map_err(map_api_error);
.map_err(|error| self.state.provider.map_api_error(error));
trace_attempt.record_result(result.as_deref());
result
}
@@ -604,7 +617,7 @@ impl ModelClient {
let response = ApiRealtimeCallClient::new(transport, api_provider, client_setup.api_auth)
.create_with_session_and_headers(sdp, session_config, extra_headers)
.await
.map_err(map_api_error)?;
.map_err(|error| self.state.provider.map_api_error(error))?;
Ok(RealtimeWebrtcCallStart {
sdp: response.sdp,
call_id: response.call_id,
@@ -658,7 +671,7 @@ impl ModelClient {
client
.summarize_input(&payload, self.build_subagent_headers())
.await
.map_err(map_api_error)
.map_err(|error| self.state.provider.map_api_error(error))
}
fn build_subagent_headers(&self) -> ApiHeaderMap {
@@ -778,12 +791,35 @@ impl ModelClient {
service_tier: Option<String>,
responses_metadata: &CodexResponsesMetadata,
) -> Result<ResponsesApiRequest> {
let instructions = &prompt.base_instructions.text;
let mut input = prompt.get_formatted_input_for_request(model_info.use_responses_lite);
if !self.state.provider.info().is_openai() {
input.iter_mut().for_each(ResponseItem::clear_metadata);
input
.iter_mut()
.for_each(ResponseItem::clear_internal_chat_message_metadata_passthrough);
}
let tools = create_tools_json_for_responses_api(&prompt.tools)?;
let (instructions, tools) = if model_info.use_responses_lite {
let mut prefix = vec![ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools,
}];
if !prompt.base_instructions.text.is_empty() {
prefix.push(ResponseItem::Message {
id: None,
role: "developer".to_string(),
content: vec![ContentItem::InputText {
text: prompt.base_instructions.text.clone(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
});
}
input.splice(0..0, prefix);
(String::new(), None)
} else {
(prompt.base_instructions.text.clone(), Some(tools))
};
let reasoning = Self::build_reasoning(model_info, effort, summary);
let include = if reasoning.is_some() {
vec!["reasoning.encrypted_content".to_string()]
@@ -810,7 +846,7 @@ impl ModelClient {
let service_tier = model_info.service_tier_for_request(service_tier);
let request = ResponsesApiRequest {
model: model_info.slug.clone(),
instructions: instructions.clone(),
instructions,
input,
tools,
tool_choice: "auto".to_string(),
@@ -1072,7 +1108,7 @@ impl ModelClientSession {
if !self.client.state.provider.info().is_openai() {
response_items
.iter_mut()
.for_each(ResponseItem::clear_metadata);
.for_each(ResponseItem::clear_internal_chat_message_metadata_passthrough);
}
let Some(incremental_items) = after_previous_input.strip_prefix(response_items.as_slice())
else {
@@ -1318,6 +1354,8 @@ impl ModelClientSession {
let store = request.store;
self.client
.prepare_response_items_for_request(&mut request.input, store);
let request_session_telemetry =
session_telemetry_for_request(session_telemetry, &request);
let inference_trace_attempt = inference_trace.start_attempt();
inference_trace_attempt.add_request_headers(&mut options.extra_headers);
inference_trace_attempt.record_started(&request);
@@ -1333,8 +1371,9 @@ impl ModelClientSession {
Ok(stream) => {
let (stream, _) = map_response_stream(
stream,
session_telemetry.clone(),
request_session_telemetry,
inference_trace_attempt,
Arc::clone(&self.client.state.provider),
);
return Ok(stream);
}
@@ -1353,6 +1392,7 @@ impl ModelClientSession {
unauthorized_transport,
&mut auth_recovery,
session_telemetry,
&self.client.state.provider,
)
.await?,
);
@@ -1361,7 +1401,7 @@ impl ModelClientSession {
Err(err) => {
let response_debug_context =
extract_response_debug_context_from_api_error(&err);
let err = map_api_error(err);
let err = self.client.state.provider.map_api_error(err);
inference_trace_attempt.record_failed(
&err,
response_debug_context.request_id.as_deref(),
@@ -1423,6 +1463,12 @@ impl ModelClientSession {
service_tier.clone(),
responses_metadata,
)?;
let request_session_telemetry = if warmup {
// `generate=false` prewarm is connection setup, not an inference request.
session_telemetry.clone()
} else {
session_telemetry_for_request(session_telemetry, &request)
};
let mut client_metadata = self
.client
.build_ws_client_metadata(responses_metadata, model_info.use_responses_lite);
@@ -1467,12 +1513,13 @@ impl ModelClientSession {
unauthorized_transport,
&mut auth_recovery,
session_telemetry,
&self.client.state.provider,
)
.await?,
);
continue;
}
Err(err) => return Err(map_api_error(err)),
Err(err) => return Err(self.client.state.provider.map_api_error(err)),
}
let (mut ws_request, previous_response_id_from_untraced_warmup) =
@@ -1501,7 +1548,7 @@ impl ModelClientSession {
self.websocket_session.last_response_from_untraced_warmup = warmup;
let websocket_connection =
self.websocket_session.connection.as_ref().ok_or_else(|| {
map_api_error(ApiError::Stream(
self.client.state.provider.map_api_error(ApiError::Stream(
"websocket connection is unavailable".to_string(),
))
})?;
@@ -1515,7 +1562,7 @@ impl ModelClientSession {
.map_err(|err| {
let response_debug_context =
extract_response_debug_context_from_api_error(&err);
let err = map_api_error(err);
let err = self.client.state.provider.map_api_error(err);
inference_trace_attempt.record_failed(
&err,
response_debug_context.request_id.as_deref(),
@@ -1525,8 +1572,9 @@ impl ModelClientSession {
})?;
let (stream, last_request_rx) = map_response_stream(
stream_result,
session_telemetry.clone(),
request_session_telemetry,
inference_trace_attempt,
Arc::clone(&self.client.state.provider),
);
self.websocket_session.last_response_rx = Some(last_request_rx);
return Ok(WebsocketStreamOutcome::Stream(stream));
@@ -1759,6 +1807,7 @@ fn map_response_stream(
api_stream: codex_api::ResponseStream,
session_telemetry: SessionTelemetry,
inference_trace_attempt: InferenceTraceAttempt,
provider: SharedModelProvider,
) -> (ResponseStream, oneshot::Receiver<LastResponse>) {
let codex_api::ResponseStream {
rx_event,
@@ -1773,6 +1822,7 @@ fn map_response_stream(
api_stream,
session_telemetry,
inference_trace_attempt,
provider,
)
}
@@ -1781,6 +1831,7 @@ fn map_response_events<S>(
api_stream: S,
session_telemetry: SessionTelemetry,
inference_trace_attempt: InferenceTraceAttempt,
provider: SharedModelProvider,
) -> (ResponseStream, oneshot::Receiver<LastResponse>)
where
S: futures::Stream<Item = std::result::Result<ResponseEvent, ApiError>>
@@ -1891,7 +1942,7 @@ where
if let Some(upstream_request_id) = upstream_request_id {
feedback_tags!(last_model_request_id = upstream_request_id);
}
let mapped = map_api_error(err);
let mapped = provider.map_api_error(err);
inference_trace_attempt.record_failed(
&mapped,
upstream_request_id,
@@ -1997,6 +2048,7 @@ async fn handle_unauthorized(
transport: TransportError,
auth_recovery: &mut Option<UnauthorizedRecovery>,
session_telemetry: &SessionTelemetry,
provider: &SharedModelProvider,
) -> Result<UnauthorizedRecoveryExecution> {
let debug = extract_response_debug_context(&transport);
if let Some(recovery) = auth_recovery
@@ -2106,7 +2158,7 @@ async fn handle_unauthorized(
debug.auth_error_code.as_deref(),
);
Err(map_api_error(ApiError::Transport(transport)))
Err(provider.map_api_error(ApiError::Transport(transport)))
}
fn api_error_http_status(error: &ApiError) -> Option<u16> {
+2 -1
View File
@@ -83,7 +83,8 @@ fn strip_image_details(items: &mut [ResponseItem]) {
}
}
}
ResponseItem::Reasoning { .. }
ResponseItem::AdditionalTools { .. }
| ResponseItem::Reasoning { .. }
| ResponseItem::AgentMessage { .. }
| ResponseItem::LocalShellCall { .. }
| ResponseItem::FunctionCall { .. }
+10 -10
View File
@@ -20,7 +20,7 @@ fn prompt_with_image_outputs() -> Prompt {
detail: Some(ImageDetail::Original),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCallOutput {
id: None,
@@ -31,7 +31,7 @@ fn prompt_with_image_outputs() -> Prompt {
detail: Some(ImageDetail::High),
},
]),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::CustomToolCallOutput {
id: None,
@@ -43,7 +43,7 @@ fn prompt_with_image_outputs() -> Prompt {
detail: Some(ImageDetail::Auto),
},
]),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
],
..Default::default()
@@ -68,7 +68,7 @@ fn responses_lite_request_copies_strip_image_details() {
detail: None,
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCallOutput {
id: None,
@@ -79,7 +79,7 @@ fn responses_lite_request_copies_strip_image_details() {
detail: None,
},
]),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::CustomToolCallOutput {
id: None,
@@ -91,7 +91,7 @@ fn responses_lite_request_copies_strip_image_details() {
detail: None,
},
]),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
]
);
@@ -110,7 +110,7 @@ fn serializes_text_verbosity_when_set() {
model: "gpt-5.4".to_string(),
instructions: "i".to_string(),
input,
tools,
tools: Some(tools),
tool_choice: "auto".to_string(),
parallel_tool_calls: true,
reasoning: None,
@@ -157,7 +157,7 @@ fn serializes_text_schema_with_strict_format() {
model: "gpt-5.4".to_string(),
instructions: "i".to_string(),
input,
tools,
tools: Some(tools),
tool_choice: "auto".to_string(),
parallel_tool_calls: true,
reasoning: None,
@@ -218,7 +218,7 @@ fn omits_text_when_not_set() {
model: "gpt-5.4".to_string(),
instructions: "i".to_string(),
input,
tools,
tools: Some(tools),
tool_choice: "auto".to_string(),
parallel_tool_calls: true,
reasoning: None,
@@ -241,7 +241,7 @@ fn serializes_flex_service_tier_when_set() {
model: "gpt-5.4".to_string(),
instructions: "i".to_string(),
input: vec![],
tools: vec![],
tools: Some(vec![]),
tool_choice: "auto".to_string(),
parallel_tool_calls: true,
reasoning: None,
+44 -1
View File
@@ -15,10 +15,13 @@ use crate::test_support::TestCodexResponsesRequestKind;
use crate::test_support::responses_metadata as test_responses_metadata;
use codex_api::ApiError;
use codex_api::ResponseEvent;
use codex_api::TransportError;
use codex_app_server_protocol::AuthMode;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_model_provider::BearerAuthProvider;
use codex_model_provider::SharedModelProvider;
use codex_model_provider::create_model_provider;
use codex_model_provider_info::CHATGPT_CODEX_BASE_URL;
use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::WireApi;
@@ -81,6 +84,10 @@ fn test_model_client(session_source: SessionSource) -> ModelClient {
)
}
fn test_model_provider() -> SharedModelProvider {
test_model_client(SessionSource::Cli).state.provider.clone()
}
fn test_responses_metadata_for_client(
client: &ModelClient,
turn_id: Option<&str>,
@@ -226,7 +233,7 @@ fn output_message(id: &str, text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -391,6 +398,7 @@ async fn dropped_response_stream_traces_cancelled_partial_output() -> anyhow::Re
api_stream,
test_session_telemetry(),
attempt,
test_model_provider(),
);
let observed = stream
@@ -440,6 +448,7 @@ async fn response_stream_records_last_model_feedback_ids() {
api_stream,
test_session_telemetry(),
InferenceTraceAttempt::disabled(),
test_model_provider(),
);
while stream.next().await.is_some() {}
@@ -455,6 +464,39 @@ async fn response_stream_records_last_model_feedback_ids() {
);
}
#[tokio::test]
async fn bedrock_unauthorized_error_uses_provider_mapping() {
let provider = create_model_provider(
ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None),
/*auth_manager*/ None,
);
let mut auth_recovery = None;
let url = "https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses";
let error = super::handle_unauthorized(
TransportError::Http {
status: http::StatusCode::UNAUTHORIZED,
url: Some(url.to_string()),
headers: None,
body: Some(
"Signature expired: 20260609T133205Z is now earlier than 20260614T062525Z"
.to_string(),
),
},
&mut auth_recovery,
&test_session_telemetry(),
&provider,
)
.await
.expect_err("expired Bedrock signature should fail");
assert_eq!(
error.to_string(),
format!(
"Amazon Bedrock rejected the request because its AWS signature has expired. Refresh your AWS credentials and retry. If `AWS_BEARER_TOKEN_BEDROCK` is set, update or unset it, then restart Codex, url: {url}"
)
);
}
#[tokio::test]
async fn dropped_backpressured_response_stream_traces_cancelled_partial_output()
-> anyhow::Result<()> {
@@ -481,6 +523,7 @@ async fn dropped_backpressured_response_stream_traces_cancelled_partial_output()
api_stream,
test_session_telemetry(),
attempt,
test_model_provider(),
);
// Fill the mapper channel with non-terminal events, then yield one output
+1 -1
View File
@@ -81,7 +81,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,
internal_chat_message_metadata_passthrough: None,
},
}),
})
+2 -2
View File
@@ -72,7 +72,7 @@ pub struct ThreadConfigSnapshot {
pub reasoning_summary: Option<ReasoningSummary>,
pub personality: Option<Personality>,
pub collaboration_mode: CollaborationMode,
pub multi_agent_mode: Option<MultiAgentMode>,
pub multi_agent_mode: MultiAgentMode,
pub session_source: SessionSource,
pub forked_from_thread_id: Option<ThreadId>,
pub parent_thread_id: Option<ThreadId>,
@@ -452,7 +452,7 @@ impl CodexThread {
role: "user".to_string(),
content: vec![ContentItem::InputText { text: message }],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
self.codex
.session
+23 -9
View File
@@ -30,9 +30,9 @@ use codex_protocol::error::Result as CodexResult;
use codex_protocol::items::ContextCompactionItem;
use codex_protocol::items::TurnItem;
use codex_protocol::models::ContentItem;
use codex_protocol::models::InternalChatMessageMetadataPassthrough;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::models::ResponseItemMetadata;
use codex_protocol::protocol::CompactedItem;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::TurnStartedEvent;
@@ -302,7 +302,12 @@ async fn run_compact_task_inner_impl(
let user_messages = collect_user_messages(history_items);
let mut new_history = build_compacted_history(Vec::new(), &user_messages, &summary_text);
let (window_number, window_id) = sess.advance_auto_compact_window().await;
if let Some(summary_item) = new_history.last_mut() {
// This replacement history skips `record_conversation_items`; only the appended summary
// belongs to this compaction turn.
summary_item.set_turn_id_if_missing(&turn_context.sub_id);
}
let (window_number, window_ids) = sess.advance_auto_compact_window().await;
if matches!(
initial_context_injection,
@@ -320,7 +325,9 @@ async fn run_compact_task_inner_impl(
message: summary_text.clone(),
replacement_history: Some(new_history.clone()),
window_number: Some(window_number),
window_id: Some(window_id),
first_window_id: Some(window_ids.first_window_id.to_string()),
previous_window_id: window_ids.previous_window_id.map(|id| id.to_string()),
window_id: Some(window_ids.window_id.to_string()),
};
sess.replace_compacted_history(
turn_context.as_ref(),
@@ -457,7 +464,7 @@ pub fn content_items_to_text(content: &[ContentItem]) -> Option<String> {
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct CompactedUserMessage {
message: String,
metadata: Option<ResponseItemMetadata>,
internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough>,
}
pub(crate) fn collect_user_messages(items: &[ResponseItem]) -> Vec<CompactedUserMessage> {
@@ -470,8 +477,11 @@ pub(crate) fn collect_user_messages(items: &[ResponseItem]) -> Vec<CompactedUser
} else {
Some(CompactedUserMessage {
message: user.message(),
metadata: match item {
ResponseItem::Message { metadata, .. } => metadata.clone(),
internal_chat_message_metadata_passthrough: match item {
ResponseItem::Message {
internal_chat_message_metadata_passthrough,
..
} => internal_chat_message_metadata_passthrough.clone(),
_ => None,
},
})
@@ -578,7 +588,9 @@ fn build_compacted_history_with_limit(
truncate_text(&message.message, TruncationPolicy::Tokens(remaining));
selected_messages.push(CompactedUserMessage {
message: truncated,
metadata: message.metadata.clone(),
internal_chat_message_metadata_passthrough: message
.internal_chat_message_metadata_passthrough
.clone(),
});
break;
}
@@ -594,7 +606,9 @@ fn build_compacted_history_with_limit(
text: message.message.clone(),
}],
phase: None,
metadata: message.metadata.clone(),
internal_chat_message_metadata_passthrough: message
.internal_chat_message_metadata_passthrough
.clone(),
});
}
@@ -609,7 +623,7 @@ fn build_compacted_history_with_limit(
role: "user".to_string(),
content: vec![ContentItem::InputText { text: summary_text }],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
});
history
+12 -9
View File
@@ -258,7 +258,7 @@ async fn run_remote_compact_task_inner_impl(
&responses_metadata,
)
.await?;
let (new_window_number, new_window_id) = sess.advance_auto_compact_window().await;
let (new_window_number, new_window_ids) = sess.advance_auto_compact_window().await;
new_history = process_compacted_history(
sess.as_ref(),
turn_context.as_ref(),
@@ -275,7 +275,9 @@ async fn run_remote_compact_task_inner_impl(
message: String::new(),
replacement_history: Some(new_history.clone()),
window_number: Some(new_window_number),
window_id: Some(new_window_id),
first_window_id: Some(new_window_ids.first_window_id.to_string()),
previous_window_id: new_window_ids.previous_window_id.map(|id| id.to_string()),
window_id: Some(new_window_ids.window_id.to_string()),
};
// Install is the semantic boundary where the compact endpoint's output becomes live
// thread history. Keep it distinct from the later inference request so the reducer can
@@ -350,7 +352,8 @@ pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
ResponseItem::AgentMessage { .. } => true,
ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true,
ResponseItem::CompactionTrigger { .. } => false,
ResponseItem::Reasoning { .. }
ResponseItem::AdditionalTools { .. }
| ResponseItem::Reasoning { .. }
| ResponseItem::LocalShellCall { .. }
| ResponseItem::FunctionCall { .. }
| ResponseItem::ToolSearchCall { .. }
@@ -412,31 +415,31 @@ fn rewritten_output_for_context_window(item: &ResponseItem) -> Option<ResponseIt
id,
call_id,
output,
metadata,
internal_chat_message_metadata_passthrough: metadata,
} => ResponseItem::FunctionCallOutput {
id: id.clone(),
call_id: call_id.clone(),
output: truncated_output_payload(output),
metadata: metadata.clone(),
internal_chat_message_metadata_passthrough: metadata.clone(),
},
ResponseItem::CustomToolCallOutput {
id,
call_id,
name,
output,
metadata,
internal_chat_message_metadata_passthrough: metadata,
} => ResponseItem::CustomToolCallOutput {
id: id.clone(),
call_id: call_id.clone(),
name: name.clone(),
output: truncated_output_payload(output),
metadata: metadata.clone(),
internal_chat_message_metadata_passthrough: metadata.clone(),
},
ResponseItem::ToolSearchOutput {
call_id,
status,
execution,
metadata,
internal_chat_message_metadata_passthrough: metadata,
..
} => ResponseItem::ToolSearchOutput {
id: item.id().map(str::to_string),
@@ -444,7 +447,7 @@ fn rewritten_output_for_context_window(item: &ResponseItem) -> Option<ResponseIt
status: status.clone(),
execution: execution.clone(),
tools: Vec::new(),
metadata: metadata.clone(),
internal_chat_message_metadata_passthrough: metadata.clone(),
},
_ => return None,
})
+30 -28
View File
@@ -165,18 +165,18 @@ async fn run_remote_compact_task_inner(
attempt
.track(sess.as_ref(), status, codex_error, analytics_details)
.await;
if matches!(&result, Err(CodexErr::TurnAborted)) {
return result;
match result {
Ok(()) => Ok(()),
Err(err @ CodexErr::TurnAborted) => Err(err),
Err(err) => {
sess.track_turn_codex_error(turn_context, &err);
let event = EventMsg::Error(
err.to_error_event(Some("Error running remote compact task".to_string())),
);
sess.send_event(turn_context, event).await;
Err(err)
}
}
if let Err(err) = result {
sess.track_turn_codex_error(turn_context, &err);
let event = EventMsg::Error(
err.to_error_event(Some("Error running remote compact task".to_string())),
);
sess.send_event(turn_context, event).await;
return Err(err);
}
Ok(())
}
async fn run_remote_compact_task_inner_impl(
@@ -234,7 +234,7 @@ async fn run_remote_compact_task_inner_impl(
)
.await?;
let mut input = prompt_input.clone();
input.push(ResponseItem::CompactionTrigger { metadata: None });
input.push(ResponseItem::CompactionTrigger {});
let prompt = Prompt {
input,
tools: tool_router.model_visible_specs(),
@@ -292,7 +292,7 @@ async fn run_remote_compact_task_inner_impl(
let (compacted_history, retained_images) =
build_v2_compacted_history(&prompt_input, compaction_output);
analytics_details.retained_image_count = Some(retained_images);
let (new_window_number, new_window_id) = sess.advance_auto_compact_window().await;
let (new_window_number, new_window_ids) = sess.advance_auto_compact_window().await;
let new_history = process_compacted_history(
sess.as_ref(),
turn_context.as_ref(),
@@ -309,7 +309,9 @@ async fn run_remote_compact_task_inner_impl(
message: String::new(),
replacement_history: Some(new_history.clone()),
window_number: Some(new_window_number),
window_id: Some(new_window_id),
first_window_id: Some(new_window_ids.first_window_id.to_string()),
previous_window_id: new_window_ids.previous_window_id.map(|id| id.to_string()),
window_id: Some(new_window_ids.window_id.to_string()),
};
compaction_trace.record_installed(&CompactionCheckpointTracePayload {
input_history: &trace_input_history,
@@ -524,7 +526,7 @@ fn truncate_message_text_to_token_budget(
role,
content,
phase,
metadata,
internal_chat_message_metadata_passthrough: metadata,
} = item
else {
return Some(item);
@@ -563,7 +565,7 @@ fn truncate_message_text_to_token_budget(
role,
content: truncated_content,
phase,
metadata,
internal_chat_message_metadata_passthrough: metadata,
})
}
@@ -584,7 +586,7 @@ mod tests {
text: text.to_string(),
}],
phase,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -616,18 +618,18 @@ mod tests {
namespace: None,
arguments: "{}".to_string(),
call_id: "call_1".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Compaction {
id: None,
encrypted_content: "old".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
let output = ResponseItem::Compaction {
id: None,
encrypted_content: "new".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let (history, _) = build_v2_compacted_history(&input, output.clone());
@@ -656,7 +658,7 @@ mod tests {
let output = ResponseItem::Compaction {
id: None,
encrypted_content: "new".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let (history, _) = build_v2_compacted_history(&input, output.clone());
@@ -683,12 +685,12 @@ mod tests {
},
],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let output = ResponseItem::Compaction {
id: None,
encrypted_content: "new".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let (_, retained_image_count) = build_v2_compacted_history(&input, output);
@@ -736,7 +738,7 @@ mod tests {
},
],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let truncated =
@@ -760,7 +762,7 @@ mod tests {
},
],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}]
);
}
@@ -775,7 +777,7 @@ mod tests {
detail: None,
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let newest = message("user", "new", /*phase*/ None);
let retained = vec![
@@ -800,7 +802,7 @@ mod tests {
detail: None,
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let newest = message("user", "new", /*phase*/ None);
let retained = vec![image_only_message, newest.clone()];
@@ -816,7 +818,7 @@ mod tests {
let compaction = ResponseItem::Compaction {
id: None,
encrypted_content: "encrypted".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let stream = response_stream(vec![
Ok(ResponseEvent::OutputItemDone(message(
+46 -45
View File
@@ -2,7 +2,7 @@ use super::*;
use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::WireApi;
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
use codex_protocol::models::ResponseItemMetadata;
use codex_protocol::models::InternalChatMessageMetadataPassthrough;
use pretty_assertions::assert_eq;
async fn process_compacted_history_with_test_session(
@@ -32,14 +32,14 @@ fn user_message(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
fn compacted_user_message(text: &str) -> CompactedUserMessage {
CompactedUserMessage {
message: text.to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -84,7 +84,7 @@ fn collect_user_messages_extracts_user_text_only() {
text: "ignored".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: Some("user".to_string()),
@@ -93,7 +93,7 @@ fn collect_user_messages_extracts_user_text_only() {
text: "first".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Other,
];
@@ -118,7 +118,7 @@ do things
.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -127,7 +127,7 @@ do things
text: "<ENVIRONMENT_CONTEXT>cwd=/tmp</ENVIRONMENT_CONTEXT>".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -136,7 +136,7 @@ do things
text: "real user message".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
@@ -231,15 +231,16 @@ fn build_token_limited_compacted_history_appends_summary_message() {
}
#[test]
fn build_compacted_history_preserves_user_message_metadata() {
fn build_compacted_history_preserves_user_message_passthrough_metadata() {
let history = build_compacted_history(
Vec::new(),
&[CompactedUserMessage {
message: "first user message".to_string(),
metadata: Some(ResponseItemMetadata {
turn_id: Some("turn-1".to_string()),
..Default::default()
}),
internal_chat_message_metadata_passthrough: Some(
InternalChatMessageMetadataPassthrough {
turn_id: Some("turn-1".to_string()),
},
),
}],
"summary text",
);
@@ -282,7 +283,7 @@ async fn process_compacted_history_replaces_developer_messages() {
text: "stale permissions".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -291,7 +292,7 @@ async fn process_compacted_history_replaces_developer_messages() {
text: "summary".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -300,7 +301,7 @@ async fn process_compacted_history_replaces_developer_messages() {
text: "stale personality".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
let (refreshed, mut expected) = process_compacted_history_with_test_session(
@@ -315,7 +316,7 @@ async fn process_compacted_history_replaces_developer_messages() {
text: "summary".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
});
assert_eq!(refreshed, expected);
}
@@ -329,7 +330,7 @@ async fn process_compacted_history_reinjects_full_initial_context() {
text: "summary".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let (refreshed, mut expected) = process_compacted_history_with_test_session(
compacted_history,
@@ -343,7 +344,7 @@ async fn process_compacted_history_reinjects_full_initial_context() {
text: "summary".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
});
assert_eq!(refreshed, expected);
}
@@ -363,7 +364,7 @@ keep me updated
.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -376,7 +377,7 @@ keep me updated
.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -389,7 +390,7 @@ keep me updated
.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -398,7 +399,7 @@ keep me updated
text: "summary".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -407,7 +408,7 @@ keep me updated
text: "stale developer instructions".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
let (refreshed, mut expected) = process_compacted_history_with_test_session(
@@ -422,7 +423,7 @@ keep me updated
text: "summary".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
});
assert_eq!(refreshed, expected);
}
@@ -462,7 +463,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
text: "older user".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -471,7 +472,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
text: format!("{SUMMARY_PREFIX}\nsummary text"),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -480,7 +481,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
text: "latest user".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
@@ -497,7 +498,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
text: "older user".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -506,7 +507,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
text: format!("{SUMMARY_PREFIX}\nsummary text"),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
expected.extend(initial_context);
@@ -517,7 +518,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
text: "latest user".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
});
assert_eq!(refreshed, expected);
}
@@ -531,7 +532,7 @@ async fn process_compacted_history_reinjects_model_switch_message() {
text: "summary".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let previous_turn_settings = PreviousTurnSettings {
model: "previous-regular-model".to_string(),
@@ -562,7 +563,7 @@ async fn process_compacted_history_reinjects_model_switch_message() {
text: "summary".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
});
assert_eq!(refreshed, expected);
}
@@ -577,7 +578,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
text: "older user".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -586,7 +587,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
text: "latest user".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -595,7 +596,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
text: format!("{SUMMARY_PREFIX}\nsummary text"),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
let initial_context = vec![ResponseItem::Message {
@@ -605,7 +606,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
text: "fresh permissions".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let refreshed =
@@ -618,7 +619,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
text: "older user".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -627,7 +628,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
text: "fresh permissions".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -636,7 +637,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
text: "latest user".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -645,7 +646,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
text: format!("{SUMMARY_PREFIX}\nsummary text"),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
assert_eq!(refreshed, expected);
@@ -656,7 +657,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last
let compacted_history = vec![ResponseItem::Compaction {
id: None,
encrypted_content: "encrypted".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let initial_context = vec![ResponseItem::Message {
id: None,
@@ -665,7 +666,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last
text: "fresh permissions".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let refreshed =
@@ -678,12 +679,12 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last
text: "fresh permissions".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Compaction {
id: None,
encrypted_content: "encrypted".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
assert_eq!(refreshed, expected);
@@ -1,6 +1,8 @@
use crate::config::ConfigBuilder;
use crate::config::ConfigOverrides;
use crate::config::ConstraintError;
use crate::config::PermissionProfileCatalogEntry;
use crate::config::permission_profile_catalog;
use codex_app_server_protocol::ConfigLayerSource;
use codex_config::CONFIG_TOML_FILE;
use codex_config::CloudConfigBundleLoadError;
@@ -1733,6 +1735,74 @@ managed-standard = true
Ok(())
}
#[tokio::test]
async fn permission_profile_catalog_marks_profiles_disallowed_by_requirements() -> anyhow::Result<()>
{
let tmp = tempdir()?;
let codex_home = tmp.path().join("home");
tokio::fs::create_dir_all(&codex_home).await?;
let requirements_path = tmp.path().join("requirements.toml");
tokio::fs::write(
&requirements_path,
r#"
allowed_sandbox_modes = ["read-only", "workspace-write"]
default_permissions = "managed-standard"
[allowed_permission_profiles]
managed-standard = true
[permissions.managed-standard]
extends = ":workspace"
[permissions.managed-disabled]
extends = ":workspace"
"#,
)
.await?;
let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?;
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
overrides.system_requirements_path = Some(requirements_path);
let config = ConfigBuilder::default()
.codex_home(codex_home)
.fallback_cwd(Some(cwd.to_path_buf()))
.loader_overrides(overrides)
.build()
.await?;
assert_eq!(
permission_profile_catalog(&config.config_layer_stack)?,
vec![
PermissionProfileCatalogEntry {
id: ":read-only".to_string(),
description: None,
allowed: false,
},
PermissionProfileCatalogEntry {
id: ":workspace".to_string(),
description: None,
allowed: false,
},
PermissionProfileCatalogEntry {
id: ":danger-full-access".to_string(),
description: None,
allowed: false,
},
PermissionProfileCatalogEntry {
id: "managed-disabled".to_string(),
description: None,
allowed: false,
},
PermissionProfileCatalogEntry {
id: "managed-standard".to_string(),
description: None,
allowed: true,
},
]
);
Ok(())
}
#[tokio::test]
async fn system_requirements_preserve_allowed_configured_permission_default() -> anyhow::Result<()>
{
+97 -9
View File
@@ -89,6 +89,7 @@ use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::NetworkAccess;
use codex_protocol::protocol::RealtimeVoice;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_path_uri::LegacyAppPathString;
use serde::Deserialize;
use tempfile::tempdir;
@@ -460,6 +461,90 @@ direct_only_tool_namespaces = ["mcp__history", "mcp__notes"]
Ok(())
}
#[tokio::test]
async fn load_config_resolves_token_budget_config() -> std::io::Result<()> {
for (config_toml, expected) in [
(
"[features]\ntoken_budget = true\n",
TokenBudgetConfig::default(),
),
(
r#"
[features.token_budget]
enabled = true
reminder_threshold_tokens = 16000
reminder_message_template = "Custom reminder: {n_remaining} tokens."
"#,
TokenBudgetConfig {
reminder_threshold_tokens: Some(16_000),
reminder_message_template: "Custom reminder: {n_remaining} tokens.".to_string(),
},
),
] {
let codex_home = tempdir()?;
let config_toml = toml::from_str(config_toml).expect("TOML should deserialize");
let config = Config::load_from_base_config_with_overrides(
config_toml,
ConfigOverrides::default(),
codex_home.abs(),
)
.await?;
assert!(config.features.enabled(Feature::TokenBudget));
assert_eq!(config.token_budget, Some(expected));
}
Ok(())
}
#[tokio::test]
async fn load_config_rejects_invalid_token_budget_reminder_template() -> std::io::Result<()> {
for reminder_message_template in [
String::new(),
"x".repeat(TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE_MAX_BYTES + 1),
] {
let codex_home = tempdir()?;
let config_toml = toml::from_str(&format!(
"[features.token_budget]\nenabled = true\nreminder_message_template = {reminder_message_template:?}\n"
))
.expect("TOML should deserialize");
let error = Config::load_from_base_config_with_overrides(
config_toml,
ConfigOverrides::default(),
codex_home.abs(),
)
.await
.expect_err("invalid reminder template should be rejected");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
}
Ok(())
}
#[tokio::test]
async fn load_config_rejects_non_positive_token_budget_reminder_threshold() -> std::io::Result<()> {
for reminder_threshold_tokens in [-1, 0] {
let codex_home = tempdir()?;
let config_toml = toml::from_str(&format!(
"[features.token_budget]\nenabled = true\nreminder_threshold_tokens = {reminder_threshold_tokens}\n"
))
.expect("TOML should deserialize");
let error = Config::load_from_base_config_with_overrides(
config_toml,
ConfigOverrides::default(),
codex_home.abs(),
)
.await
.expect_err("non-positive reminder threshold should be rejected");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
error.to_string(),
"features.token_budget.reminder_threshold_tokens must be positive"
);
}
Ok(())
}
#[tokio::test]
async fn load_config_resolves_rollout_budget() -> std::io::Result<()> {
let codex_home = tempdir()?;
@@ -468,7 +553,7 @@ async fn load_config_resolves_rollout_budget() -> std::io::Result<()> {
[features.rollout_budget]
enabled = true
limit_tokens = 100000
reminder_interval_tokens = 10000
reminder_at_remaining_tokens = [50000, 25000, 10000]
sampling_token_weight = 1.0
prefill_token_weight = 0.1
"#,
@@ -487,7 +572,7 @@ prefill_token_weight = 0.1
config.rollout_budget,
Some(RolloutBudgetConfig {
limit_tokens: 100_000,
reminder_interval_tokens: 10_000,
reminder_at_remaining_tokens: vec![50_000, 25_000, 10_000],
sampling_token_weight: 1.0,
prefill_token_weight: 0.1,
})
@@ -3108,9 +3193,10 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
assert_eq!(
config.custom_permission_profiles,
vec![CustomPermissionProfileSummary {
vec![PermissionProfileCatalogEntry {
id: "dev".to_string(),
description: Some("Workspace access.".to_string()),
allowed: true,
}]
);
assert!(
@@ -5469,6 +5555,7 @@ async fn load_global_mcp_servers_returns_empty_if_missing() -> anyhow::Result<()
#[tokio::test]
async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let expected_cwd = LegacyAppPathString::from_path(codex_home.path());
let mut servers = BTreeMap::new();
servers.insert(
@@ -5479,7 +5566,7 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
args: vec!["hello".to_string()],
env: None,
env_vars: Vec::new(),
cwd: Some(codex_home.path().to_path_buf()),
cwd: Some(expected_cwd.clone()),
},
environment_id: "remote".to_string(),
enabled: true,
@@ -5518,7 +5605,7 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
assert_eq!(args, &vec!["hello".to_string()]);
assert!(env.is_none());
assert!(env_vars.is_empty());
assert_eq!(cwd, &Some(codex_home.path().to_path_buf()));
assert_eq!(cwd, &Some(expected_cwd));
}
other => panic!("unexpected transport {other:?}"),
}
@@ -6019,6 +6106,7 @@ async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let cwd_path = PathBuf::from("/tmp/codex-mcp");
let cwd = LegacyAppPathString::from_path(&cwd_path);
let servers = BTreeMap::from([(
"docs".to_string(),
McpServerConfig {
@@ -6027,7 +6115,7 @@ async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
args: Vec::new(),
env: None,
env_vars: Vec::new(),
cwd: Some(cwd_path.clone()),
cwd: Some(cwd.clone()),
},
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
@@ -6062,7 +6150,7 @@ async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
let docs = loaded.get("docs").expect("docs entry");
match &docs.transport {
McpServerTransportConfig::Stdio { cwd, .. } => {
assert_eq!(cwd.as_deref(), Some(Path::new("/tmp/codex-mcp")));
assert_eq!(cwd, &Some(LegacyAppPathString::from_path(&cwd_path)));
}
other => panic!("unexpected transport {other:?}"),
}
@@ -9667,6 +9755,7 @@ async fn browser_feature_requirements_are_valid() -> std::io::Result<()> {
[features]
in_app_browser = false
browser_use = false
browser_use_full_cdp_access = false
"#,
),
)
@@ -9675,6 +9764,7 @@ browser_use = false
assert!(!config.features.enabled(Feature::InAppBrowser));
assert!(!config.features.enabled(Feature::BrowserUse));
assert!(!config.features.enabled(Feature::BrowserUseFullCdpAccess));
Ok(())
}
@@ -10030,7 +10120,6 @@ max_concurrent_threads_per_session = 5
min_wait_timeout_ms = 2500
max_wait_timeout_ms = 120000
default_wait_timeout_ms = 30000
usage_hint_enabled = false
usage_hint_text = "Custom delegation guidance."
root_agent_usage_hint_text = "Root guidance."
subagent_usage_hint_text = "Subagent guidance."
@@ -10058,7 +10147,6 @@ non_code_mode_only = true
),
(None, Some(4))
);
assert!(!config.multi_agent_v2.usage_hint_enabled);
assert_eq!(
config.multi_agent_v2.usage_hint_text.as_deref(),
Some("Custom delegation guidance.")
@@ -69,7 +69,7 @@ fn serialize_mcp_server_table(config: &McpServerConfig) -> TomlTable {
entry["env_vars"] = array_from_env_vars(env_vars);
}
if let Some(cwd) = cwd {
entry["cwd"] = value(cwd.to_string_lossy().to_string());
entry["cwd"] = value(cwd.as_str());
}
}
McpServerTransportConfig::StreamableHttp {
+142 -53
View File
@@ -68,9 +68,11 @@ use codex_features::Features;
use codex_features::FeaturesToml;
use codex_features::MultiAgentV2ConfigToml;
use codex_features::NetworkProxyConfigToml;
use codex_features::TokenBudgetConfigToml;
use codex_git_utils::resolve_root_git_project_for_trust;
use codex_install_context::InstallContext;
use codex_login::AuthManagerConfig;
use codex_login::AuthRouteConfig;
use codex_mcp::McpConfig;
use codex_mcp::McpPluginAttribution;
use codex_mcp::McpServerRegistration;
@@ -146,6 +148,7 @@ pub mod edit;
mod managed_features;
mod network_proxy_spec;
mod otel;
mod permission_profile_catalog;
mod permissions;
mod resolved_permission_profile;
#[cfg(test)]
@@ -162,6 +165,11 @@ pub use codex_sandboxing::system_bwrap_warning;
pub use managed_features::ManagedFeatures;
pub use network_proxy_spec::NetworkProxySpec;
pub use network_proxy_spec::StartedNetworkProxy;
pub use permission_profile_catalog::PermissionProfileCatalogEntry;
pub use permission_profile_catalog::permission_profile_catalog;
use permission_profile_catalog::permission_profile_catalog_from_permissions;
use permission_profile_catalog::permission_profile_is_allowed;
use permission_profile_catalog::validate_permission_profile_for_deny_read;
pub(crate) use permissions::is_builtin_permission_profile_name;
pub(crate) use permissions::reject_unknown_builtin_permission_profile;
pub(crate) use permissions::resolve_permission_profile;
@@ -238,9 +246,8 @@ Payload:
```
You may also see them addressed as to=/root/..., which indicates your identity is /root/...
"#;
const DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT: &str = r#"Note that collaboration tools cannot be called from inside `functions.exec`. Call `spawn_agent`, `send_message`, `followup_task`, `wait_agent`, `interrupt_agent`, and `list_agents` only as direct tool calls using the recipient shown in their tool definitions, such as `to=functions.spawn_agent` without a configured namespace or `to=functions.agents.spawn_agent` with `tool_namespace = "agents"`, since they are intentionally absent from the `functions.exec` `tools.*` namespace. Available tools in `functions.exec` are explicitly described with a `tools` namespace in the developer message.
The goal is to correctly solve the problem in as little time as possible. Therefore, if at any point you can parallelize work by delegating tasks to another agent, you should do so to save time.
const DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE: &str = "collaboration";
const DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT: &str = r#"Note that collaboration tools cannot be called from inside `functions.exec`. Call `spawn_agent`, `send_message`, `followup_task`, `wait_agent`, `interrupt_agent`, and `list_agents` only as direct tool calls using the recipient shown in their tool definitions, such as `to=functions.collaboration.spawn_agent`, since they are intentionally absent from the `functions.exec` `tools.*` namespace. Available tools in `functions.exec` are explicitly described with a `tools` namespace in the developer message.
All agents share the same directory. In detail:
- All agents have access to the same container and filesystem as you.
@@ -645,7 +652,7 @@ pub struct Config {
pub explicit_permission_profile_mode: bool,
/// User-defined permission profiles available from effective config.
pub custom_permission_profiles: Vec<CustomPermissionProfileSummary>,
pub custom_permission_profiles: Vec<PermissionProfileCatalogEntry>,
/// Configures who approval requests are routed to for review once they have
/// been escalated. This does not disable separate safety checks such as
@@ -1026,6 +1033,8 @@ pub struct Config {
/// Settings specific to the task-path-based multi-agent tool surface.
pub multi_agent_v2: MultiAgentV2Config,
/// Context-window token budget configuration, when enabled.
pub token_budget: Option<TokenBudgetConfig>,
/// Shared token budget for the root thread and its sub-agents.
pub rollout_budget: Option<RolloutBudgetConfig>,
/// Current-time reminder configuration, when enabled.
@@ -1075,10 +1084,31 @@ pub struct CodeModeConfig {
pub direct_only_tool_namespaces: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
pub(crate) const DEFAULT_TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE: &str = concat!(
"Your context window is nearly exhausted (only {n_remaining} tokens remaining) and will be automatically reset for you soon. ",
"Once reset, message items in current context window will be cleared in the new window, but notes and history items will be persistent across windows."
);
const TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE_MAX_BYTES: usize = 1000;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct TokenBudgetConfig {
pub reminder_threshold_tokens: Option<i64>,
pub reminder_message_template: String,
}
impl Default for TokenBudgetConfig {
fn default() -> Self {
Self {
reminder_threshold_tokens: None,
reminder_message_template: DEFAULT_TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE.to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct RolloutBudgetConfig {
pub limit_tokens: i64,
pub reminder_interval_tokens: i64,
pub reminder_at_remaining_tokens: Vec<i64>,
pub sampling_token_weight: f64,
pub prefill_token_weight: f64,
}
@@ -1104,7 +1134,6 @@ pub struct MultiAgentV2Config {
pub min_wait_timeout_ms: i64,
pub max_wait_timeout_ms: i64,
pub default_wait_timeout_ms: i64,
pub usage_hint_enabled: bool,
pub usage_hint_text: Option<String>,
pub root_agent_usage_hint_text: Option<String>,
pub subagent_usage_hint_text: Option<String>,
@@ -1120,7 +1149,6 @@ impl MultiAgentV2Config {
min_wait_timeout_ms: DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS,
max_wait_timeout_ms: DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS,
default_wait_timeout_ms: DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS,
usage_hint_enabled: true,
usage_hint_text: None,
root_agent_usage_hint_text: Some(default_multi_agent_v2_usage_hint_text(
DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT,
@@ -1130,7 +1158,7 @@ impl MultiAgentV2Config {
DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT,
max_concurrent_threads_per_session,
)),
tool_namespace: None,
tool_namespace: Some(DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE.to_string()),
hide_spawn_agent_metadata: true,
non_code_mode_only: true,
}
@@ -1181,6 +1209,10 @@ impl AuthManagerConfig for Config {
fn chatgpt_base_url(&self) -> String {
self.chatgpt_base_url.clone()
}
fn auth_route_config(&self) -> Option<AuthRouteConfig> {
Config::auth_route_config(self)
}
}
#[derive(Clone, Default)]
@@ -1434,6 +1466,11 @@ impl Config {
}
}
pub fn auth_route_config(&self) -> Option<AuthRouteConfig> {
self.respect_system_proxy
.then(AuthRouteConfig::respect_system_proxy)
}
/// Build the plugin-manager input from the effective config.
pub fn plugins_config_input(&self) -> PluginsConfigInput {
PluginsConfigInput::new(
@@ -2120,12 +2157,6 @@ pub struct AgentRoleConfig {
pub nickname_candidates: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomPermissionProfileSummary {
pub id: String,
pub description: Option<String>,
}
fn resolve_tool_suggest_config(
config_toml: &ConfigToml,
config_layer_stack: &ConfigLayerStack,
@@ -2468,9 +2499,6 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config
let default_wait_timeout_ms = base
.and_then(|config| config.default_wait_timeout_ms)
.unwrap_or(default.default_wait_timeout_ms);
let usage_hint_enabled = base
.and_then(|config| config.usage_hint_enabled)
.unwrap_or(default.usage_hint_enabled);
let usage_hint_text = base
.and_then(|config| config.usage_hint_text.as_ref())
.cloned()
@@ -2499,7 +2527,6 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config
min_wait_timeout_ms,
max_wait_timeout_ms,
default_wait_timeout_ms,
usage_hint_enabled,
usage_hint_text,
root_agent_usage_hint_text,
subagent_usage_hint_text,
@@ -2509,6 +2536,48 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config
}
}
fn resolve_token_budget_config(
config_toml: &ConfigToml,
features: &ManagedFeatures,
) -> std::io::Result<Option<TokenBudgetConfig>> {
if !features.enabled(Feature::TokenBudget) {
return Ok(None);
}
let token_budget_config = token_budget_toml_config(config_toml.features.as_ref());
let reminder_threshold_tokens =
token_budget_config.and_then(|config| config.reminder_threshold_tokens);
if reminder_threshold_tokens.is_some_and(|tokens| tokens <= 0) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"features.token_budget.reminder_threshold_tokens must be positive",
));
}
let reminder_message_template = token_budget_config
.and_then(|config| config.reminder_message_template.clone())
.unwrap_or_else(|| DEFAULT_TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE.to_string());
if reminder_message_template.trim().is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"features.token_budget.reminder_message_template must not be empty",
));
}
if reminder_message_template.len() > TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE_MAX_BYTES {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"features.token_budget.reminder_message_template must not exceed {TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE_MAX_BYTES} bytes"
),
));
}
Ok(Some(TokenBudgetConfig {
reminder_threshold_tokens,
reminder_message_template,
}))
}
fn resolve_rollout_budget_config(
config_toml: &ConfigToml,
features: &ManagedFeatures,
@@ -2538,13 +2607,23 @@ fn resolve_rollout_budget_config(
"features.rollout_budget.limit_tokens must be positive",
));
}
let reminder_interval_tokens = config
.reminder_interval_tokens
.unwrap_or_else(|| (limit_tokens / 10).max(1));
if reminder_interval_tokens <= 0 {
let reminder_at_remaining_tokens =
config
.reminder_at_remaining_tokens
.clone()
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"features.rollout_budget.reminder_at_remaining_tokens is required when rollout_budget is enabled",
)
})?;
if reminder_at_remaining_tokens
.iter()
.any(|&tokens| tokens <= 0 || tokens >= limit_tokens)
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"features.rollout_budget.reminder_interval_tokens must be positive",
"features.rollout_budget.reminder_at_remaining_tokens must contain only positive values below limit_tokens",
));
}
let sampling_token_weight = config.sampling_token_weight.unwrap_or(1.0);
@@ -2562,7 +2641,7 @@ fn resolve_rollout_budget_config(
}
Ok(Some(RolloutBudgetConfig {
limit_tokens,
reminder_interval_tokens,
reminder_at_remaining_tokens,
sampling_token_weight,
prefill_token_weight,
}))
@@ -2635,6 +2714,13 @@ fn multi_agent_v2_toml_config(features: Option<&FeaturesToml>) -> Option<&MultiA
}
}
fn token_budget_toml_config(features: Option<&FeaturesToml>) -> Option<&TokenBudgetConfigToml> {
match features?.token_budget.as_ref()? {
FeatureToml::Enabled(_) => None,
FeatureToml::Config(config) => Some(config),
}
}
fn current_time_reminder_toml_config(
features: Option<&FeaturesToml>,
) -> Option<&CurrentTimeReminderConfigToml> {
@@ -2672,6 +2758,15 @@ pub fn resolve_bootstrap_respect_system_proxy(
Ok(features.get().enabled(Feature::RespectSystemProxy))
}
/// Resolves auth route settings for the initial cloud-config bootstrap.
pub fn resolve_bootstrap_auth_route_config(
cfg: &ConfigToml,
feature_requirements: Option<&Sourced<FeatureRequirementsToml>>,
) -> std::io::Result<Option<AuthRouteConfig>> {
resolve_bootstrap_respect_system_proxy(cfg, feature_requirements)
.map(|enabled| enabled.then(AuthRouteConfig::respect_system_proxy))
}
pub(crate) fn resolve_web_search_mode_for_turn(
web_search_mode: &Constrained<WebSearchMode>,
permission_profile: &PermissionProfile,
@@ -3038,19 +3133,13 @@ impl Config {
permission_config_syntax,
Some(PermissionConfigSyntax::Profiles)
);
let custom_permission_profiles = cfg
.permissions
.as_ref()
.map_or_else(Vec::new, |permissions| {
permissions
.entries
.iter()
.map(|(id, profile)| CustomPermissionProfileSummary {
id: id.clone(),
description: profile.description.clone(),
})
.collect()
});
let custom_permission_profiles = permission_profile_catalog_from_permissions(
&config_layer_stack,
effective_permission_selection.profiles.as_ref(),
)?
.into_iter()
.filter(|profile| !is_builtin_permission_profile_name(&profile.id))
.collect();
let using_implicit_builtin_profile = permission_config_syntax.is_none()
&& effective_permission_selection.selected_profile_id.is_none();
let should_seed_legacy_workspace_roots = effective_permission_selection
@@ -3142,7 +3231,6 @@ impl Config {
effective_permission_selection.profiles.as_ref(),
default_permissions,
builtin_workspace_write_settings,
resolved_cwd.as_path(),
&mut startup_warnings,
)?;
let mut configured_workspace_roots = compile_permission_profile_workspace_roots(
@@ -3287,6 +3375,7 @@ impl Config {
resolve_experimental_request_user_input_enabled(&cfg);
let code_mode = resolve_code_mode_config(&cfg);
let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg);
let token_budget = resolve_token_budget_config(&cfg, &features)?;
let rollout_budget = resolve_rollout_budget_config(&cfg, &features)?;
let current_time_reminder = resolve_current_time_reminder_config(&cfg, &features)?;
let terminal_resize_reflow = resolve_terminal_resize_reflow_config(&cfg);
@@ -3562,20 +3651,10 @@ impl Config {
constrained_permission_profile
.value
.add_validator(move |permission_profile| {
let mode = sandbox_mode_requirement_for_permission_profile(permission_profile);
match mode {
SandboxModeRequirement::ReadOnly
| SandboxModeRequirement::WorkspaceWrite => Ok(()),
SandboxModeRequirement::DangerFullAccess
| SandboxModeRequirement::ExternalSandbox => {
Err(ConstraintError::InvalidValue {
field_name: "sandbox_mode",
candidate: format!("{mode:?}"),
allowed: "[read-only, workspace-write]".to_string(),
requirement_source: requirement_source.clone(),
})
}
}
validate_permission_profile_for_deny_read(
permission_profile,
&requirement_source,
)
})
.map_err(std::io::Error::from)?;
}
@@ -3830,6 +3909,7 @@ impl Config {
background_terminal_max_timeout,
ghost_snapshot,
multi_agent_v2,
token_budget,
rollout_budget,
current_time_reminder,
features,
@@ -4027,6 +4107,15 @@ impl Config {
pub fn bundled_skills_enabled(&self) -> bool {
crate::skills::service::bundled_skills_enabled_from_stack(&self.config_layer_stack)
}
/// Returns whether effective requirements allow selecting a concrete profile.
pub fn is_permission_profile_allowed(
&self,
profile_id: &str,
permission_profile: &PermissionProfile,
) -> bool {
permission_profile_is_allowed(&self.config_layer_stack, profile_id, permission_profile)
}
}
fn guardian_policy_config_from_requirements(
@@ -0,0 +1,140 @@
use codex_config::ConfigLayerStack;
use codex_config::RequirementSource;
use codex_config::SandboxModeRequirement;
use codex_config::Sourced;
use codex_config::permissions_toml::PermissionsToml;
use codex_config::sandbox_mode_requirement_for_permission_profile;
use codex_protocol::models::PermissionProfile;
use super::ConstraintError;
use super::ConstraintResult;
use super::is_permission_allowed;
use super::merge_managed_permission_profiles;
use super::permissions::BUILT_IN_DANGER_FULL_ACCESS_PROFILE;
use super::permissions::BUILT_IN_READ_ONLY_PROFILE;
use super::permissions::BUILT_IN_WORKSPACE_PROFILE;
use super::permissions::compile_permission_profile_selection;
use super::permissions::validate_user_permission_profile_names;
use super::validate_required_permission_profile_catalog;
/// A permission profile exposed to clients together with its effective availability.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PermissionProfileCatalogEntry {
pub id: String,
pub description: Option<String>,
pub allowed: bool,
}
/// Builds the effective permission profile catalog for a config layer stack.
pub fn permission_profile_catalog(
config_layer_stack: &ConfigLayerStack,
) -> std::io::Result<Vec<PermissionProfileCatalogEntry>> {
let permissions = config_layer_stack
.effective_config()
.get("permissions")
.cloned()
.map(toml::Value::try_into::<PermissionsToml>)
.transpose()
.map_err(std::io::Error::other)?;
let requirements_toml = config_layer_stack.requirements_toml();
let permissions = merge_managed_permission_profiles(permissions.as_ref(), requirements_toml)?;
permission_profile_catalog_from_permissions(config_layer_stack, permissions.as_ref())
}
pub(super) fn permission_profile_catalog_from_permissions(
config_layer_stack: &ConfigLayerStack,
permissions: Option<&PermissionsToml>,
) -> std::io::Result<Vec<PermissionProfileCatalogEntry>> {
let requirements_toml = config_layer_stack.requirements_toml();
validate_user_permission_profile_names(permissions)?;
validate_required_permission_profile_catalog(requirements_toml, permissions)?;
let mut catalog = [
(BUILT_IN_READ_ONLY_PROFILE, PermissionProfile::read_only()),
(
BUILT_IN_WORKSPACE_PROFILE,
PermissionProfile::workspace_write(),
),
(
BUILT_IN_DANGER_FULL_ACCESS_PROFILE,
PermissionProfile::Disabled,
),
]
.into_iter()
.map(|(id, permission_profile)| PermissionProfileCatalogEntry {
id: id.to_string(),
description: None,
allowed: permission_profile_is_allowed(config_layer_stack, id, &permission_profile),
})
.collect::<Vec<_>>();
if let Some(permissions) = permissions {
catalog.extend(permissions.entries.iter().map(|(id, profile)| {
let mut warnings = Vec::new();
let allowed = compile_permission_profile_selection(
Some(permissions),
id,
/*workspace_write*/ None,
&mut warnings,
)
.map(|(file_system, network)| {
PermissionProfile::from_runtime_permissions(&file_system, network)
})
.is_ok_and(|permission_profile| {
permission_profile_is_allowed(config_layer_stack, id, &permission_profile)
});
PermissionProfileCatalogEntry {
id: id.clone(),
description: profile.description.clone(),
allowed,
}
}));
}
Ok(catalog)
}
pub(super) fn permission_profile_is_allowed(
config_layer_stack: &ConfigLayerStack,
profile_id: &str,
permission_profile: &PermissionProfile,
) -> bool {
let allowed_by_id = config_layer_stack
.requirements_toml()
.allowed_permission_profiles
.as_ref()
.is_none_or(|allowed| is_permission_allowed(allowed, profile_id));
let allowed_by_sandbox_mode = config_layer_stack
.requirements()
.permission_profile
.can_set(permission_profile)
.is_ok();
let allowed_by_filesystem = config_layer_stack
.requirements()
.filesystem
.as_ref()
.is_none_or(|Sourced { value, source }| {
value.deny_read.is_empty()
|| validate_permission_profile_for_deny_read(permission_profile, source).is_ok()
});
allowed_by_id && allowed_by_sandbox_mode && allowed_by_filesystem
}
pub(super) fn validate_permission_profile_for_deny_read(
permission_profile: &PermissionProfile,
requirement_source: &RequirementSource,
) -> ConstraintResult<()> {
let mode = sandbox_mode_requirement_for_permission_profile(permission_profile);
match mode {
SandboxModeRequirement::ReadOnly | SandboxModeRequirement::WorkspaceWrite => Ok(()),
SandboxModeRequirement::DangerFullAccess | SandboxModeRequirement::ExternalSandbox => {
Err(ConstraintError::InvalidValue {
field_name: "sandbox_mode",
candidate: format!("{mode:?}"),
allowed: "[read-only, workspace-write]".to_string(),
requirement_source: requirement_source.clone(),
})
}
}
}
+2 -9
View File
@@ -346,7 +346,6 @@ pub(crate) fn network_proxy_config_for_profile_selection(
pub(crate) fn compile_permission_profile(
permissions: &PermissionsToml,
profile_name: &str,
policy_cwd: &Path,
startup_warnings: &mut Vec<String>,
) -> io::Result<(FileSystemSandboxPolicy, NetworkSandboxPolicy)> {
let profile = resolve_permission_profile(permissions, profile_name)?;
@@ -383,7 +382,6 @@ pub(crate) fn compile_permission_profile(
.extend(compile_filesystem_permission(
path,
permission,
policy_cwd,
startup_warnings,
)?);
}
@@ -412,7 +410,6 @@ pub(crate) fn compile_permission_profile_selection(
permissions: Option<&PermissionsToml>,
profile_name: &str,
workspace_write: Option<&SandboxWorkspaceWrite>,
policy_cwd: &Path,
startup_warnings: &mut Vec<String>,
) -> io::Result<(FileSystemSandboxPolicy, NetworkSandboxPolicy)> {
if let Some(permission_profile) = builtin_permission_profile(profile_name, workspace_write) {
@@ -426,7 +423,7 @@ pub(crate) fn compile_permission_profile_selection(
"default_permissions requires a `[permissions]` table",
)
})?;
compile_permission_profile(permissions, profile_name, policy_cwd, startup_warnings)
compile_permission_profile(permissions, profile_name, startup_warnings)
}
pub(crate) fn compile_permission_profile_workspace_roots(
@@ -524,7 +521,6 @@ fn compile_network_sandbox_policy(
fn compile_filesystem_permission(
path: &str,
permission: &FilesystemPermissionToml,
policy_cwd: &Path,
startup_warnings: &mut Vec<String>,
) -> io::Result<Vec<FileSystemSandboxEntry>> {
let mut entries = Vec::new();
@@ -548,9 +544,7 @@ fn compile_filesystem_permission(
// exact-path parser so existing path semantics stay intact.
let entry = FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: compile_scoped_filesystem_pattern(
path, subpath, *access, policy_cwd,
)?,
pattern: compile_scoped_filesystem_pattern(path, subpath, *access)?,
},
access: *access,
};
@@ -643,7 +637,6 @@ fn compile_scoped_filesystem_pattern(
path: &str,
subpath: &str,
access: FileSystemAccessMode,
_policy_cwd: &Path,
) -> io::Result<String> {
// Pattern entries currently mean deny-read only. Supporting broader access
// modes here would imply glob-based read/write allow semantics that the
@@ -543,7 +543,6 @@ fn glob_scan_max_depth_must_be_positive() {
#[test]
fn read_write_trailing_glob_suffix_compiles_as_subpath() -> std::io::Result<()> {
let cwd = TempDir::new()?;
let mut startup_warnings = Vec::new();
let (file_system_policy, _) = compile_permission_profile(
&PermissionsToml {
@@ -568,7 +567,6 @@ fn read_write_trailing_glob_suffix_compiles_as_subpath() -> std::io::Result<()>
)]),
},
"workspace",
cwd.path(),
&mut startup_warnings,
)?;
-3
View File
@@ -40,7 +40,6 @@ use codex_mcp::ToolPluginProvenance;
use codex_mcp::codex_apps_tools_cache_key;
use codex_mcp::compute_auth_statuses;
use codex_mcp::effective_mcp_servers;
use codex_mcp::host_owned_codex_apps_enabled;
use codex_mcp::tool_plugin_provenance;
const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30);
@@ -240,7 +239,6 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
let mut mcp_servers = effective_mcp_servers(&mcp_config, auth.as_ref());
mcp_servers.retain(|name, _| name == CODEX_APPS_MCP_SERVER_NAME);
let host_owned_codex_apps_enabled = host_owned_codex_apps_enabled(&mcp_config, auth.as_ref());
if mcp_servers.is_empty() {
return Ok(AccessibleConnectorsStatus {
connectors: Vec::new(),
@@ -275,7 +273,6 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
McpRuntimeContext::new(environment_manager, config.cwd.to_path_buf()),
config.codex_home.to_path_buf(),
codex_apps_tools_cache_key(auth.as_ref()),
host_owned_codex_apps_enabled,
mcp_config.prefix_mcp_tool_names,
mcp_config.client_elicitation_capability,
/*supports_openai_form_elicitation*/ false,
@@ -3,7 +3,6 @@ use codex_protocol::items::parse_hook_prompt_fragment;
use codex_protocol::models::ContentItem;
use super::AdditionalContextUserFragment;
use super::EnvironmentContext;
use super::FragmentRegistration;
use super::FragmentRegistrationProxy;
use super::InternalModelContextFragment;
@@ -16,10 +15,11 @@ use super::SubagentNotification;
use super::TurnAborted;
use super::UserInstructions;
use super::UserShellCommand;
use super::world_state::EnvironmentsState;
static USER_INSTRUCTIONS_REGISTRATION: FragmentRegistrationProxy<UserInstructions> =
FragmentRegistrationProxy::new();
static ENVIRONMENT_CONTEXT_REGISTRATION: FragmentRegistrationProxy<EnvironmentContext> =
static ENVIRONMENT_CONTEXT_REGISTRATION: FragmentRegistrationProxy<EnvironmentsState> =
FragmentRegistrationProxy::new();
static ADDITIONAL_CONTEXT_REGISTRATION: FragmentRegistrationProxy<AdditionalContextUserFragment> =
FragmentRegistrationProxy::new();
@@ -1,99 +1,13 @@
use crate::session::turn_context::TurnContext;
use crate::session::turn_context::TurnEnvironment;
use crate::shell::Shell;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::TurnContextItem;
use codex_protocol::protocol::TurnContextNetworkItem;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use std::collections::HashSet;
use std::path::PathBuf;
use super::ContextualUserFragment;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct EnvironmentContext {
pub(crate) environments: EnvironmentContextEnvironments,
pub(crate) current_date: Option<String>,
pub(crate) timezone: Option<String>,
pub(crate) network: Option<NetworkContext>,
pub(crate) filesystem: Option<FileSystemContext>,
pub(crate) subagents: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct EnvironmentContextEnvironment {
pub(crate) id: String,
pub(crate) cwd: PathUri,
pub(crate) shell: String,
}
impl EnvironmentContextEnvironment {
fn legacy(cwd: PathUri, shell: String) -> Self {
Self {
id: String::new(),
cwd,
shell,
}
}
fn from_turn_environments(environments: &[TurnEnvironment], shell: &Shell) -> Vec<Self> {
environments
.iter()
.map(|environment| Self {
id: environment.environment_id.clone(),
cwd: environment.cwd().clone(),
shell: environment
.shell
.as_ref()
.map(|shell| shell.name().to_string())
.unwrap_or_else(|| shell.name().to_string()),
})
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum EnvironmentContextEnvironments {
None,
Single(EnvironmentContextEnvironment),
Multiple(Vec<EnvironmentContextEnvironment>),
}
impl EnvironmentContextEnvironments {
fn from_vec(environments: Vec<EnvironmentContextEnvironment>) -> Self {
let mut environments = environments;
match environments.pop() {
None => Self::None,
Some(environment) if environments.is_empty() => Self::Single(environment),
Some(environment) => {
environments.push(environment);
Self::Multiple(environments)
}
}
}
fn equals_except_shell(&self, other: &Self) -> bool {
match (self, other) {
(Self::None, Self::None) => true,
(Self::Single(left), Self::Single(right)) => left.cwd == right.cwd,
(Self::Multiple(left), Self::Multiple(right)) => {
left.len() == right.len()
&& left
.iter()
.zip(right.iter())
.all(|(left, right)| left.id == right.id && left.cwd == right.cwd)
}
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FileSystemContext {
workspace_roots: Vec<String>,
@@ -117,7 +31,7 @@ enum ManagedFileSystemContext {
}
impl FileSystemContext {
fn from_permission_profile(
pub(super) fn from_permission_profile(
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
) -> Self {
@@ -143,7 +57,7 @@ impl FileSystemContext {
}
}
fn render(&self) -> String {
pub(super) fn render(&self) -> String {
let mut rendered = "<filesystem>".to_string();
if !self.workspace_roots.is_empty() {
rendered.push_str("<workspace_roots>");
@@ -283,7 +197,7 @@ fn push_text_element(rendered: &mut String, name: &str, value: &str) {
rendered.push_str(&format!("</{name}>"));
}
fn push_xml_escaped_text(rendered: &mut String, value: &str) {
pub(crate) fn push_xml_escaped_text(rendered: &mut String, value: &str) {
for ch in value.chars() {
match ch {
'&' => rendered.push_str("&amp;"),
@@ -310,7 +224,7 @@ impl NetworkContext {
}
}
fn render(&self) -> String {
pub(super) fn render(&self) -> String {
let mut rendered = "<network enabled=\"true\">".to_string();
Self::push_rendered_domain_element(&mut rendered, "allowed", &self.allowed_domains);
Self::push_rendered_domain_element(&mut rendered, "denied", &self.denied_domains);
@@ -328,258 +242,3 @@ impl NetworkContext {
rendered_network.push_str(&format!("</{name}>"));
}
}
impl EnvironmentContext {
pub(crate) fn new(
environments: Vec<EnvironmentContextEnvironment>,
current_date: Option<String>,
timezone: Option<String>,
network: Option<NetworkContext>,
subagents: Option<String>,
) -> Self {
Self {
environments: EnvironmentContextEnvironments::from_vec(environments),
current_date,
timezone,
network,
filesystem: None,
subagents,
}
}
fn new_with_environments(
environments: EnvironmentContextEnvironments,
current_date: Option<String>,
timezone: Option<String>,
network: Option<NetworkContext>,
filesystem: Option<FileSystemContext>,
subagents: Option<String>,
) -> Self {
Self {
environments,
current_date,
timezone,
network,
filesystem,
subagents,
}
}
/// Compares two environment contexts, ignoring the shell. Useful when
/// comparing turn to turn, since the initial environment_context will
/// include the shell, and then it is not configurable from turn to turn.
pub(crate) fn equals_except_shell(&self, other: &EnvironmentContext) -> bool {
self.environments.equals_except_shell(&other.environments)
&& self.current_date == other.current_date
&& self.timezone == other.timezone
&& self.network == other.network
&& self.filesystem == other.filesystem
&& self.subagents == other.subagents
}
pub(crate) fn diff_from_turn_context_item(
before: &TurnContextItem,
after: &EnvironmentContext,
) -> Self {
let before_network = Self::network_from_turn_context_item(before);
let before_filesystem = Self::filesystem_from_turn_context_item(before);
let environments = match &after.environments {
EnvironmentContextEnvironments::Single(environment) => {
if PathUri::from_abs_path(&before.cwd) != environment.cwd {
EnvironmentContextEnvironments::Single(EnvironmentContextEnvironment::legacy(
environment.cwd.clone(),
environment.shell.clone(),
))
} else {
EnvironmentContextEnvironments::None
}
}
EnvironmentContextEnvironments::Multiple(environments) => {
EnvironmentContextEnvironments::Multiple(environments.clone())
}
EnvironmentContextEnvironments::None => EnvironmentContextEnvironments::None,
};
let network = if before_network != after.network {
after.network.clone()
} else {
before_network
};
let filesystem = if before_filesystem != after.filesystem {
after.filesystem.clone()
} else {
before_filesystem
};
EnvironmentContext::new_with_environments(
environments,
after.current_date.clone(),
after.timezone.clone(),
network,
filesystem,
/*subagents*/ None,
)
}
pub(crate) fn from_turn_context(turn_context: &TurnContext, shell: &Shell) -> Self {
let mut context = Self::new(
EnvironmentContextEnvironment::from_turn_environments(
&turn_context.environments.turn_environments,
shell,
),
turn_context.current_date.clone(),
turn_context.timezone.clone(),
Self::network_from_turn_context(turn_context),
/*subagents*/ None,
);
context.filesystem = Some(FileSystemContext::from_permission_profile(
&turn_context.permission_profile,
&turn_context.config.effective_workspace_roots(),
));
context
}
pub(crate) fn from_turn_context_item(
turn_context_item: &TurnContextItem,
shell: String,
) -> Self {
Self::new_with_environments(
EnvironmentContextEnvironments::from_vec(vec![EnvironmentContextEnvironment::legacy(
PathUri::from_abs_path(&turn_context_item.cwd),
shell,
)]),
turn_context_item.current_date.clone(),
turn_context_item.timezone.clone(),
Self::network_from_turn_context_item(turn_context_item),
Self::filesystem_from_turn_context_item(turn_context_item),
/*subagents*/ None,
)
}
pub(crate) fn with_subagents(mut self, subagents: String) -> Self {
if !subagents.is_empty() {
self.subagents = Some(subagents);
}
self
}
fn network_from_turn_context(turn_context: &TurnContext) -> Option<NetworkContext> {
let network = turn_context
.config
.config_layer_stack
.requirements()
.network
.as_ref()?;
Some(NetworkContext::new(
network
.domains
.as_ref()
.and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains)
.unwrap_or_default(),
network
.domains
.as_ref()
.and_then(codex_config::NetworkDomainPermissionsToml::denied_domains)
.unwrap_or_default(),
))
}
fn network_from_turn_context_item(
turn_context_item: &TurnContextItem,
) -> Option<NetworkContext> {
let TurnContextNetworkItem {
allowed_domains,
denied_domains,
} = turn_context_item.network.as_ref()?;
Some(NetworkContext::new(
allowed_domains.clone(),
denied_domains.clone(),
))
}
fn filesystem_from_turn_context_item(
turn_context_item: &TurnContextItem,
) -> Option<FileSystemContext> {
Some(FileSystemContext::from_permission_profile(
&turn_context_item.permission_profile(),
&workspace_roots_from_turn_context_item(turn_context_item),
))
}
}
fn workspace_roots_from_turn_context_item(
turn_context_item: &TurnContextItem,
) -> Vec<AbsolutePathBuf> {
if let Some(workspace_roots) = turn_context_item.workspace_roots.as_ref() {
return workspace_roots.clone();
}
vec![turn_context_item.cwd.clone()]
}
impl ContextualUserFragment for EnvironmentContext {
fn role(&self) -> &'static str {
"user"
}
fn markers(&self) -> (&'static str, &'static str) {
Self::type_markers()
}
fn type_markers() -> (&'static str, &'static str) {
(
codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG,
codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG,
)
}
fn body(&self) -> String {
let mut lines = Vec::new();
match &self.environments {
EnvironmentContextEnvironments::Single(environment) => {
let cwd = environment.cwd.inferred_native_path_string();
lines.push(format!(" <cwd>{cwd}</cwd>"));
lines.push(format!(" <shell>{}</shell>", environment.shell));
}
EnvironmentContextEnvironments::Multiple(environments) => {
lines.push(" <environments>".to_string());
for environment in environments {
lines.push(format!(" <environment id=\"{}\">", environment.id));
let cwd = environment.cwd.inferred_native_path_string();
lines.push(format!(" <cwd>{cwd}</cwd>"));
lines.push(format!(" <shell>{}</shell>", environment.shell));
lines.push(" </environment>".to_string());
}
lines.push(" </environments>".to_string());
}
EnvironmentContextEnvironments::None => {}
}
if let Some(current_date) = &self.current_date {
lines.push(format!(" <current_date>{current_date}</current_date>"));
}
if let Some(timezone) = &self.timezone {
lines.push(format!(" <timezone>{timezone}</timezone>"));
}
match &self.network {
Some(network) => {
lines.push(format!(" {}", network.render()));
}
None => {
// TODO(mbolin): Include this line if it helps the model.
// lines.push(" <network enabled=\"false\" />".to_string());
}
}
if let Some(filesystem) = &self.filesystem {
lines.push(format!(" {}", filesystem.render()));
}
if let Some(subagents) = &self.subagents {
lines.push(" <subagents>".to_string());
lines.extend(subagents.lines().map(|line| format!(" {line}")));
lines.push(" </subagents>".to_string());
}
format!("\n{}\n", lines.join("\n"))
}
}
#[cfg(test)]
#[path = "environment_context_tests.rs"]
mod tests;
+2 -1
View File
@@ -32,6 +32,7 @@ mod token_budget_context;
mod turn_aborted;
mod user_instructions;
mod user_shell_command;
pub(crate) mod world_state;
pub(crate) use approved_command_prefix_saved::ApprovedCommandPrefixSaved;
pub(crate) use apps_instructions::AppsInstructions;
@@ -47,7 +48,6 @@ pub(crate) use collaboration_mode_instructions::CollaborationModeInstructions;
pub(crate) use contextual_user_message::is_contextual_user_fragment;
pub(crate) use contextual_user_message::parse_visible_hook_prompt_message;
pub(crate) use current_time_reminder::CurrentTimeReminder;
pub(crate) use environment_context::EnvironmentContext;
pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder;
pub(crate) use hook_additional_context::HookAdditionalContext;
pub(crate) use image_generation_instructions::ImageGenerationInstructions;
@@ -73,6 +73,7 @@ pub(crate) use rollout_budget::RolloutBudgetContext;
pub(crate) use subagent_notification::SubagentNotification;
pub(crate) use token_budget_context::TokenBudgetContext;
pub(crate) use token_budget_context::TokenBudgetRemainingContext;
pub(crate) use token_budget_context::TokenBudgetReminder;
pub(crate) use turn_aborted::TurnAborted;
pub(crate) use user_instructions::UserInstructions;
pub(crate) use user_shell_command::UserShellCommand;
@@ -4,6 +4,7 @@ use codex_protocol::protocol::MULTI_AGENT_MODE_CLOSE_TAG;
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
const EXPLICIT_REQUEST_ONLY_MULTI_AGENT_MODE_TEXT: &str = "Do not spawn sub-agents unless the user explicitly asks for sub-agents, delegation, or parallel agent work.";
const NO_MULTI_AGENT_MODE_TEXT: &str = "Multi-agent delegation mode instructions are inactive. Any earlier multi-agent mode developer message no longer applies.";
const PROACTIVE_MULTI_AGENT_MODE_TEXT: &str = "Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Use sub-agents when parallel work would materially improve speed or quality. This mode remains active until a later multi-agent mode developer message changes it.";
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -32,6 +33,7 @@ impl ContextualUserFragment for MultiAgentModeInstructions {
fn body(&self) -> String {
match self.multi_agent_mode {
MultiAgentMode::None => NO_MULTI_AGENT_MODE_TEXT.to_string(),
MultiAgentMode::ExplicitRequestOnly => {
EXPLICIT_REQUEST_ONLY_MULTI_AGENT_MODE_TEXT.to_string()
}
@@ -1,20 +1,32 @@
use super::ContextualUserFragment;
use codex_protocol::ThreadId;
use codex_protocol::protocol::CONTEXT_WINDOW_CLOSE_TAG;
use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TokenBudgetContext {
thread_id: ThreadId,
first_window_id: Uuid,
previous_window_id: Option<Uuid>,
window_id: Uuid,
tokens_left: i64,
mcp_result: Option<String>,
}
impl TokenBudgetContext {
pub(crate) fn new(thread_id: ThreadId, window_id: Uuid, tokens_left: i64) -> Self {
pub(crate) fn new(
thread_id: ThreadId,
first_window_id: Uuid,
previous_window_id: Option<Uuid>,
window_id: Uuid,
mcp_result: Option<String>,
) -> Self {
Self {
thread_id,
first_window_id,
previous_window_id,
window_id,
tokens_left,
mcp_result,
}
}
}
@@ -29,16 +41,25 @@ impl ContextualUserFragment for TokenBudgetContext {
}
fn type_markers() -> (&'static str, &'static str) {
("<token_budget>\n", "\n</token_budget>")
(CONTEXT_WINDOW_OPEN_TAG, CONTEXT_WINDOW_CLOSE_TAG)
}
fn body(&self) -> String {
let thread_id = self.thread_id;
let first_window_id = self.first_window_id;
let window_id = self.window_id;
let tokens_left = self.tokens_left;
format!(
"Thread id {thread_id}.\nCurrent context window id {window_id}.\nYou have {tokens_left} tokens left in this context window."
)
let mut lines = vec![
format!("Thread id: {thread_id}"),
format!("First context window id: {first_window_id}"),
format!("Current context window id: {window_id}"),
];
if let Some(previous_window_id) = self.previous_window_id {
lines.push(format!("Previous context window id: {previous_window_id}"));
}
if let Some(mcp_result) = &self.mcp_result {
lines.push(mcp_result.clone());
}
format!("\n{}\n", lines.join("\n"))
}
}
@@ -69,7 +90,7 @@ impl ContextualUserFragment for TokenBudgetRemainingContext {
}
fn type_markers() -> (&'static str, &'static str) {
("<token_budget>\n", "\n</token_budget>")
("", "")
}
fn body(&self) -> String {
@@ -81,3 +102,34 @@ impl ContextualUserFragment for TokenBudgetRemainingContext {
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TokenBudgetReminder {
message: String,
}
impl TokenBudgetReminder {
pub(crate) fn new(message_template: &str, n_remaining: i64) -> Self {
Self {
message: message_template.replace("{n_remaining}", &n_remaining.to_string()),
}
}
}
impl ContextualUserFragment for TokenBudgetReminder {
fn role(&self) -> &'static str {
"developer"
}
fn markers(&self) -> (&'static str, &'static str) {
Self::type_markers()
}
fn type_markers() -> (&'static str, &'static str) {
("", "")
}
fn body(&self) -> String {
self.message.clone()
}
}
@@ -0,0 +1,387 @@
use super::WorldStateSection;
use crate::context::ContextualUserFragment;
use crate::context::environment_context::FileSystemContext;
use crate::context::environment_context::NetworkContext;
use crate::context::environment_context::push_xml_escaped_text;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::session::turn_context::TurnContext;
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_protocol::protocol::TurnContextItem;
use codex_protocol::protocol::TurnContextNetworkItem;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use std::collections::BTreeMap;
/// Environment values visible to the model.
#[derive(Clone, Debug, Default)]
pub(crate) struct EnvironmentsState {
environments: BTreeMap<String, EnvironmentState>,
current_date: Option<String>,
timezone: Option<String>,
network: Option<NetworkContext>,
filesystem: Option<FileSystemContext>,
subagents: Option<String>,
}
impl EnvironmentsState {
pub(crate) fn from_turn_context(turn_context: &TurnContext) -> Self {
Self::from_turn_context_with_environments(turn_context, &turn_context.environments)
}
pub(crate) fn from_turn_context_with_environments(
turn_context: &TurnContext,
environments: &TurnEnvironmentSnapshot,
) -> Self {
Self {
environments: environment_states(environments),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
network: network_from_turn_context(turn_context),
filesystem: Some(FileSystemContext::from_permission_profile(
&turn_context.permission_profile,
&turn_context.config.effective_workspace_roots(),
)),
subagents: None,
}
}
pub(crate) fn from_turn_context_item(turn_context_item: &TurnContextItem) -> Self {
Self {
environments: [(
LOCAL_ENVIRONMENT_ID.to_string(),
EnvironmentState {
cwd: PathUri::from_abs_path(&turn_context_item.cwd),
status: EnvironmentStatus::Available,
shell: None,
},
)]
.into_iter()
.collect(),
current_date: turn_context_item.current_date.clone(),
timezone: turn_context_item.timezone.clone(),
network: network_from_turn_context_item(turn_context_item),
filesystem: Some(FileSystemContext::from_permission_profile(
&turn_context_item.permission_profile(),
&workspace_roots_from_turn_context_item(turn_context_item),
)),
subagents: None,
}
}
pub(crate) fn with_subagents(mut self, subagents: String) -> Self {
if !subagents.is_empty() {
self.subagents = Some(subagents);
}
self
}
fn rendered_full(&self) -> RenderedEnvironments {
RenderedEnvironments {
updates: self
.environments
.iter()
.map(|(id, environment)| {
(id.clone(), EnvironmentUpdate::Current(environment.clone()))
})
.collect(),
legacy_single: is_legacy_single(&self.environments),
current_date: self.current_date.clone(),
timezone: self.timezone.clone(),
network: self.network.clone(),
filesystem: self.filesystem.clone(),
subagents: self.subagents.clone(),
}
}
}
impl WorldStateSection for EnvironmentsState {
fn render_diff(&self, previous: Option<&Self>) -> Option<Box<dyn ContextualUserFragment>> {
let empty = Self::default();
let previous = previous.unwrap_or(&empty);
let turn_context_values_changed = self.current_date != previous.current_date
|| self.timezone != previous.timezone
|| self.network != previous.network
|| self.filesystem != previous.filesystem;
let mut updates = self
.environments
.iter()
.filter(|(id, environment)| {
previous
.environments
.get(*id)
.is_none_or(|previous| !environment.has_same_diff_value(previous))
})
.map(|(id, environment)| (id.clone(), EnvironmentUpdate::Current(environment.clone())))
.collect::<BTreeMap<_, _>>();
updates.extend(
previous
.environments
.keys()
.filter(|id| !self.environments.contains_key(*id))
.map(|id| (id.clone(), EnvironmentUpdate::Unavailable)),
);
let legacy_single = is_legacy_single(&self.environments)
&& updates
.values()
.all(|update| matches!(update, EnvironmentUpdate::Current(_)));
(!updates.is_empty() || turn_context_values_changed).then(|| {
Box::new(RenderedEnvironments {
updates,
legacy_single,
current_date: self.current_date.clone(),
timezone: self.timezone.clone(),
network: self.network.clone(),
filesystem: self.filesystem.clone(),
subagents: self.subagents.clone(),
}) as Box<dyn ContextualUserFragment>
})
}
}
impl ContextualUserFragment for EnvironmentsState {
fn role(&self) -> &'static str {
"user"
}
fn markers(&self) -> (&'static str, &'static str) {
Self::type_markers()
}
fn type_markers() -> (&'static str, &'static str) {
environment_context_markers()
}
fn body(&self) -> String {
self.rendered_full().body()
}
}
struct RenderedEnvironments {
updates: BTreeMap<String, EnvironmentUpdate>,
legacy_single: bool,
current_date: Option<String>,
timezone: Option<String>,
network: Option<NetworkContext>,
filesystem: Option<FileSystemContext>,
subagents: Option<String>,
}
enum EnvironmentUpdate {
Current(EnvironmentState),
Unavailable,
}
impl ContextualUserFragment for RenderedEnvironments {
fn role(&self) -> &'static str {
"user"
}
fn markers(&self) -> (&'static str, &'static str) {
Self::type_markers()
}
fn type_markers() -> (&'static str, &'static str) {
environment_context_markers()
}
fn body(&self) -> String {
let mut rendered = "\n".to_string();
if self.legacy_single {
if let Some(EnvironmentUpdate::Current(environment)) = self.updates.values().next() {
push_environment_values(&mut rendered, environment, " ");
}
} else if !self.updates.is_empty() {
rendered.push_str(" <environments>\n");
for (id, update) in &self.updates {
match update {
EnvironmentUpdate::Current(environment) => {
rendered.push_str(" <environment id=\"");
push_xml_escaped_text(&mut rendered, id);
rendered.push('"');
rendered.push_str(">\n");
push_environment_values(&mut rendered, environment, " ");
rendered.push_str(" </environment>\n");
}
EnvironmentUpdate::Unavailable => {
rendered.push_str(" <environment id=\"");
push_xml_escaped_text(&mut rendered, id);
rendered.push_str("\" status=\"unavailable\" />\n");
}
}
}
rendered.push_str(" </environments>\n");
}
push_optional_element(&mut rendered, "current_date", self.current_date.as_deref());
push_optional_element(&mut rendered, "timezone", self.timezone.as_deref());
if let Some(network) = &self.network {
rendered.push_str(" ");
rendered.push_str(&network.render());
rendered.push('\n');
}
if let Some(filesystem) = &self.filesystem {
rendered.push_str(" ");
rendered.push_str(&filesystem.render());
rendered.push('\n');
}
if let Some(subagents) = &self.subagents {
rendered.push_str(" <subagents>\n");
for line in subagents.lines() {
rendered.push_str(" ");
rendered.push_str(line);
rendered.push('\n');
}
rendered.push_str(" </subagents>\n");
}
rendered
}
}
fn push_environment_values(rendered: &mut String, environment: &EnvironmentState, indent: &str) {
rendered.push_str(indent);
rendered.push_str("<cwd>");
push_xml_escaped_text(rendered, &environment.cwd.inferred_native_path_string());
rendered.push_str("</cwd>\n");
if environment.status == EnvironmentStatus::Starting {
rendered.push_str(indent);
rendered.push_str("<status>starting</status>\n");
}
if let Some(shell) = &environment.shell {
rendered.push_str(indent);
rendered.push_str("<shell>");
push_xml_escaped_text(rendered, shell);
rendered.push_str("</shell>\n");
}
}
fn push_optional_element(rendered: &mut String, name: &str, value: Option<&str>) {
let Some(value) = value else {
return;
};
rendered.push_str(" <");
rendered.push_str(name);
rendered.push('>');
push_xml_escaped_text(rendered, value);
rendered.push_str("</");
rendered.push_str(name);
rendered.push_str(">\n");
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct EnvironmentState {
cwd: PathUri,
status: EnvironmentStatus,
shell: Option<String>,
}
impl EnvironmentState {
fn has_same_diff_value(&self, other: &Self) -> bool {
self.cwd == other.cwd
&& self.status == other.status
&& self
.shell
.as_ref()
.zip(other.shell.as_ref())
.is_none_or(|(current, previous)| current == previous)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EnvironmentStatus {
Starting,
Available,
}
fn environment_states(snapshot: &TurnEnvironmentSnapshot) -> BTreeMap<String, EnvironmentState> {
let mut environments = snapshot
.turn_environments
.iter()
.map(|environment| {
(
environment.environment_id.clone(),
EnvironmentState {
cwd: environment.cwd().clone(),
status: EnvironmentStatus::Available,
shell: environment
.shell
.as_ref()
.map(|shell| shell.name().to_string()),
},
)
})
.collect::<BTreeMap<_, _>>();
for environment in &snapshot.starting {
environments
.entry(environment.selection.environment_id.clone())
.or_insert_with(|| EnvironmentState {
cwd: environment.selection.cwd.clone(),
status: EnvironmentStatus::Starting,
shell: None,
});
}
environments
}
fn is_legacy_single(environments: &BTreeMap<String, EnvironmentState>) -> bool {
environments.len() == 1
&& environments
.values()
.all(|environment| environment.status == EnvironmentStatus::Available)
}
fn environment_context_markers() -> (&'static str, &'static str) {
(
codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG,
codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG,
)
}
fn network_from_turn_context(turn_context: &TurnContext) -> Option<NetworkContext> {
let network = turn_context
.config
.config_layer_stack
.requirements()
.network
.as_ref()?;
Some(NetworkContext::new(
network
.domains
.as_ref()
.and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains)
.unwrap_or_default(),
network
.domains
.as_ref()
.and_then(codex_config::NetworkDomainPermissionsToml::denied_domains)
.unwrap_or_default(),
))
}
fn network_from_turn_context_item(turn_context_item: &TurnContextItem) -> Option<NetworkContext> {
let TurnContextNetworkItem {
allowed_domains,
denied_domains,
} = turn_context_item.network.as_ref()?;
Some(NetworkContext::new(
allowed_domains.clone(),
denied_domains.clone(),
))
}
fn workspace_roots_from_turn_context_item(
turn_context_item: &TurnContextItem,
) -> Vec<AbsolutePathBuf> {
if let Some(workspace_roots) = turn_context_item.workspace_roots.as_ref() {
return workspace_roots.clone();
}
vec![turn_context_item.cwd.clone()]
}
#[cfg(test)]
#[path = "environment_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "environment_render_tests.rs"]
mod render_tests;
@@ -30,15 +30,43 @@ fn test_abs_path(unix_path: &str) -> AbsolutePathBuf {
test_path_buf(unix_path).abs()
}
fn environment(id: &str, cwd: PathUri, shell: impl Into<String>) -> (String, EnvironmentState) {
(
id.to_string(),
EnvironmentState {
cwd,
status: EnvironmentStatus::Available,
shell: Some(shell.into()),
},
)
}
fn environment_state(
environments: impl IntoIterator<Item = (String, EnvironmentState)>,
current_date: Option<String>,
timezone: Option<String>,
network: Option<NetworkContext>,
subagents: Option<String>,
) -> EnvironmentsState {
EnvironmentsState {
environments: environments.into_iter().collect(),
current_date,
timezone,
network,
filesystem: None,
subagents,
}
}
#[test]
fn serialize_workspace_write_environment_context() {
let cwd = test_path_buf("/repo");
let context = EnvironmentContext::new(
vec![EnvironmentContextEnvironment {
id: "local".to_string(),
cwd: PathUri::from_abs_path(&cwd.abs()),
shell: fake_shell_name(),
}],
let context = environment_state(
[environment(
"local",
PathUri::from_abs_path(&cwd.abs()),
fake_shell_name(),
)],
Some("2026-02-26".to_string()),
Some("America/Los_Angeles".to_string()),
/*network*/ None,
@@ -60,12 +88,12 @@ fn serialize_workspace_write_environment_context() {
#[test]
fn serialize_environment_context_with_foreign_windows_cwd() {
let context = EnvironmentContext::new(
vec![EnvironmentContextEnvironment {
id: "remote".to_string(),
cwd: PathUri::parse("file:///C:/windows").expect("Windows cwd URI"),
shell: "powershell".to_string(),
}],
let context = environment_state(
[environment(
"remote",
PathUri::parse("file:///C:/windows").expect("Windows cwd URI"),
"powershell",
)],
/*current_date*/ None,
/*timezone*/ None,
/*network*/ None,
@@ -87,12 +115,12 @@ fn serialize_environment_context_with_network() {
vec!["api.example.com".to_string(), "*.openai.com".to_string()],
vec!["blocked.example.com".to_string()],
);
let context = EnvironmentContext::new(
vec![EnvironmentContextEnvironment {
id: "local".to_string(),
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
shell: fake_shell_name(),
}],
let context = environment_state(
[environment(
"local",
PathUri::from_abs_path(&test_abs_path("/repo")),
fake_shell_name(),
)],
Some("2026-02-26".to_string()),
Some("America/Los_Angeles".to_string()),
Some(network),
@@ -149,12 +177,12 @@ fn serialize_environment_context_with_full_filesystem_profile() {
AbsolutePathBuf::resolve_path_against_base(Path::new("private/**"), repo.as_path());
let other_repo_private_glob =
AbsolutePathBuf::resolve_path_against_base(Path::new("private/**"), other_repo.as_path());
let mut context = EnvironmentContext::new(
vec![EnvironmentContextEnvironment {
id: "local".to_string(),
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
shell: fake_shell_name(),
}],
let mut context = environment_state(
[environment(
"local",
PathUri::from_abs_path(&test_abs_path("/repo")),
fake_shell_name(),
)],
/*current_date*/ None,
/*timezone*/ None,
/*network*/ None,
@@ -210,7 +238,7 @@ fn turn_context_item_filesystem_uses_workspace_roots_instead_of_cwd() {
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
let context = EnvironmentContext::from_turn_context_item(&item, fake_shell_name()).render();
let context = EnvironmentsState::from_turn_context_item(&item).render();
assert!(
context.contains(&format!(
@@ -237,7 +265,7 @@ fn turn_context_item_filesystem_uses_workspace_roots_instead_of_cwd() {
#[test]
fn serialize_read_only_environment_context() {
let context = EnvironmentContext::new(
let context = environment_state(
Vec::new(),
Some("2026-02-26".to_string()),
Some("America/Los_Angeles".to_string()),
@@ -253,97 +281,14 @@ fn serialize_read_only_environment_context() {
assert_eq!(context.render(), expected);
}
#[test]
fn equals_except_shell_compares_cwd() {
let context1 = EnvironmentContext::new(
vec![EnvironmentContextEnvironment {
id: "local".to_string(),
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
shell: fake_shell_name(),
}],
/*current_date*/ None,
/*timezone*/ None,
/*network*/ None,
/*subagents*/ None,
);
let context2 = EnvironmentContext::new(
vec![EnvironmentContextEnvironment {
id: "local".to_string(),
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
shell: fake_shell_name(),
}],
/*current_date*/ None,
/*timezone*/ None,
/*network*/ None,
/*subagents*/ None,
);
assert!(context1.equals_except_shell(&context2));
}
#[test]
fn equals_except_shell_compares_cwd_differences() {
let context1 = EnvironmentContext::new(
vec![EnvironmentContextEnvironment {
id: "local".to_string(),
cwd: PathUri::from_abs_path(&test_abs_path("/repo1")),
shell: fake_shell_name(),
}],
/*current_date*/ None,
/*timezone*/ None,
/*network*/ None,
/*subagents*/ None,
);
let context2 = EnvironmentContext::new(
vec![EnvironmentContextEnvironment {
id: "local".to_string(),
cwd: PathUri::from_abs_path(&test_abs_path("/repo2")),
shell: fake_shell_name(),
}],
/*current_date*/ None,
/*timezone*/ None,
/*network*/ None,
/*subagents*/ None,
);
assert!(!context1.equals_except_shell(&context2));
}
#[test]
fn equals_except_shell_ignores_shell() {
let context1 = EnvironmentContext::new(
vec![EnvironmentContextEnvironment {
id: "local".to_string(),
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
shell: "bash".to_string(),
}],
/*current_date*/ None,
/*timezone*/ None,
/*network*/ None,
/*subagents*/ None,
);
let context2 = EnvironmentContext::new(
vec![EnvironmentContextEnvironment {
id: "other".to_string(),
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
shell: "zsh".to_string(),
}],
/*current_date*/ None,
/*timezone*/ None,
/*network*/ None,
/*subagents*/ None,
);
assert!(context1.equals_except_shell(&context2));
}
#[test]
fn serialize_environment_context_with_subagents() {
let context = EnvironmentContext::new(
vec![EnvironmentContextEnvironment {
id: "local".to_string(),
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
shell: fake_shell_name(),
}],
let context = environment_state(
[environment(
"local",
PathUri::from_abs_path(&test_abs_path("/repo")),
fake_shell_name(),
)],
Some("2026-02-26".to_string()),
Some("America/Los_Angeles".to_string()),
/*network*/ None,
@@ -371,18 +316,10 @@ fn serialize_environment_context_with_subagents() {
fn serialize_environment_context_with_multiple_selected_environments() {
let local_cwd = test_path_buf("/repo/local");
let remote_cwd = test_path_buf("/repo/remote");
let context = EnvironmentContext::new(
vec![
EnvironmentContextEnvironment {
id: "local".to_string(),
cwd: PathUri::from_abs_path(&local_cwd.abs()),
shell: "bash".to_string(),
},
EnvironmentContextEnvironment {
id: "remote".to_string(),
cwd: PathUri::from_abs_path(&remote_cwd.abs()),
shell: "bash".to_string(),
},
let context = environment_state(
[
environment("local", PathUri::from_abs_path(&local_cwd.abs()), "bash"),
environment("remote", PathUri::from_abs_path(&remote_cwd.abs()), "bash"),
],
Some("2026-02-26".to_string()),
Some("America/Los_Angeles".to_string()),
@@ -416,18 +353,14 @@ fn serialize_environment_context_with_multiple_selected_environments() {
fn serialize_environment_context_prefers_environment_shell_when_present() {
let local_cwd = test_path_buf("/repo/local");
let remote_cwd = test_path_buf("/repo/remote");
let context = EnvironmentContext::new(
vec![
EnvironmentContextEnvironment {
id: "local".to_string(),
cwd: PathUri::from_abs_path(&local_cwd.abs()),
shell: "powershell".to_string(),
},
EnvironmentContextEnvironment {
id: "remote".to_string(),
cwd: PathUri::from_abs_path(&remote_cwd.abs()),
shell: "cmd".to_string(),
},
let context = environment_state(
[
environment(
"local",
PathUri::from_abs_path(&local_cwd.abs()),
"powershell",
),
environment("remote", PathUri::from_abs_path(&remote_cwd.abs()), "cmd"),
],
/*current_date*/ None,
/*timezone*/ None,
@@ -0,0 +1,256 @@
use super::*;
use crate::context::ContextualUserFragment;
use crate::context::world_state::WorldState;
use anyhow::Result;
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_protocol::models::ContentItem;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseItem;
use codex_protocol::permissions::NetworkSandboxPolicy;
use pretty_assertions::assert_eq;
#[test]
fn renders_full_environment_state() -> Result<()> {
let context = EnvironmentsState {
environments: [
("laptop".to_string(), available("file:///repo", "zsh")?),
(
"devbox".to_string(),
available("file:///workspace", "bash")?,
),
]
.into_iter()
.collect(),
..Default::default()
};
let mut world_state = WorldState::default();
world_state.add_section(context);
assert_eq!(
vec![user_message(
r#"<environment_context>
<environments>
<environment id="devbox">
<cwd>/workspace</cwd>
<shell>bash</shell>
</environment>
<environment id="laptop">
<cwd>/repo</cwd>
<shell>zsh</shell>
</environment>
</environments>
</environment_context>"#,
)],
render_fragments(world_state.render_full()),
);
Ok(())
}
#[test]
fn renders_only_changed_environments() -> Result<()> {
let mut previous = WorldState::default();
previous.add_section(EnvironmentsState {
environments: [
("laptop".to_string(), available("file:///repo", "bash")?),
("devbox".to_string(), starting("file:///workspace")?),
("old".to_string(), available("file:///old", "sh")?),
]
.into_iter()
.collect(),
..Default::default()
});
let mut current = WorldState::default();
current.add_section(EnvironmentsState {
environments: [
("laptop".to_string(), available("file:///repo", "zsh")?),
(
"devbox".to_string(),
available("file:///workspace", "powershell")?,
),
("remote".to_string(), starting("file:///remote")?),
]
.into_iter()
.collect(),
..Default::default()
});
assert_eq!(
vec![user_message(
r#"<environment_context>
<environments>
<environment id="devbox">
<cwd>/workspace</cwd>
<shell>powershell</shell>
</environment>
<environment id="laptop">
<cwd>/repo</cwd>
<shell>zsh</shell>
</environment>
<environment id="old" status="unavailable" />
<environment id="remote">
<cwd>/remote</cwd>
<status>starting</status>
</environment>
</environments>
</environment_context>"#,
)],
render_fragments(current.render_diff(&previous)),
);
Ok(())
}
#[test]
fn persisted_turn_context_values_render_a_diff() -> Result<()> {
let environments = EnvironmentsState {
environments: [(
LOCAL_ENVIRONMENT_ID.to_string(),
available("file:///repo", "zsh")?,
)]
.into_iter()
.collect(),
..Default::default()
};
let mut previous = WorldState::default();
previous.add_section(EnvironmentsState {
current_date: Some("2026-06-19".to_string()),
timezone: Some("UTC".to_string()),
network: Some(NetworkContext::new(
vec!["old.example.com".to_string()],
vec![],
)),
filesystem: Some(FileSystemContext::from_permission_profile(
&PermissionProfile::Disabled,
&[],
)),
..environments.clone()
});
let mut current = WorldState::default();
current.add_section(EnvironmentsState {
current_date: Some("2026-06-20".to_string()),
timezone: Some("America/Los_Angeles".to_string()),
network: Some(NetworkContext::new(
vec!["new.example.com".to_string()],
vec!["blocked.example.com".to_string()],
)),
filesystem: Some(FileSystemContext::from_permission_profile(
&PermissionProfile::External {
network: NetworkSandboxPolicy::Restricted,
},
&[],
)),
..environments
});
assert_eq!(
vec![user_message(
r#"<environment_context>
<current_date>2026-06-20</current_date>
<timezone>America/Los_Angeles</timezone>
<network enabled="true"><allowed>new.example.com</allowed><denied>blocked.example.com</denied></network>
<filesystem><permission_profile type="external"><file_system type="external" /></permission_profile></filesystem>
</environment_context>"#,
)],
render_fragments(current.render_diff(&previous)),
);
Ok(())
}
#[test]
fn single_environment_diff_ignores_unknown_shell() -> Result<()> {
let previous = EnvironmentsState {
environments: [(
LOCAL_ENVIRONMENT_ID.to_string(),
EnvironmentState {
cwd: PathUri::parse("file:///repo")?,
status: EnvironmentStatus::Available,
shell: None,
},
)]
.into_iter()
.collect(),
..Default::default()
};
let current = EnvironmentsState {
environments: [(
LOCAL_ENVIRONMENT_ID.to_string(),
available("file:///repo", "zsh")?,
)]
.into_iter()
.collect(),
..Default::default()
};
assert_eq!(
None,
render_fragment(WorldStateSection::render_diff(&current, Some(&previous)))
);
Ok(())
}
#[test]
fn removed_legacy_environment_renders_unavailable() -> Result<()> {
let previous = EnvironmentsState {
environments: [(
LOCAL_ENVIRONMENT_ID.to_string(),
available("file:///repo", "bash")?,
)]
.into_iter()
.collect(),
..Default::default()
};
assert_eq!(
Some(user_message(
r#"<environment_context>
<environments>
<environment id="local" status="unavailable" />
</environments>
</environment_context>"#,
)),
render_fragment(WorldStateSection::render_diff(
&EnvironmentsState::default(),
Some(&previous),
)),
);
Ok(())
}
fn available(cwd: &str, shell: &str) -> Result<EnvironmentState> {
Ok(EnvironmentState {
cwd: PathUri::parse(cwd)?,
status: EnvironmentStatus::Available,
shell: Some(shell.to_string()),
})
}
fn starting(cwd: &str) -> Result<EnvironmentState> {
Ok(EnvironmentState {
cwd: PathUri::parse(cwd)?,
status: EnvironmentStatus::Starting,
shell: None,
})
}
fn render_fragments(fragments: Vec<Box<dyn ContextualUserFragment>>) -> Vec<ResponseItem> {
fragments
.into_iter()
.map(ContextualUserFragment::into_boxed_response_item)
.collect()
}
fn render_fragment(fragment: Option<Box<dyn ContextualUserFragment>>) -> Option<ResponseItem> {
fragment.map(ContextualUserFragment::into_boxed_response_item)
}
fn user_message(text: &str) -> ResponseItem {
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: text.to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -0,0 +1,80 @@
mod environment;
use crate::context::ContextualUserFragment;
use indexmap::IndexMap;
use std::any::Any;
use std::any::TypeId;
use std::fmt;
pub(crate) use environment::EnvironmentsState;
trait ErasedWorldStateSection: Send + Sync {
fn as_any(&self) -> &dyn Any;
fn render_diff(&self, previous: Option<&dyn Any>) -> Option<Box<dyn ContextualUserFragment>>;
}
impl<S: WorldStateSection> ErasedWorldStateSection for S {
fn as_any(&self) -> &dyn Any {
self
}
fn render_diff(&self, previous: Option<&dyn Any>) -> Option<Box<dyn ContextualUserFragment>> {
let previous = match previous {
Some(previous) => {
let Some(previous) = previous.downcast_ref::<S>() else {
unreachable!("world-state section type must match its type ID");
};
Some(previous)
}
None => None,
};
WorldStateSection::render_diff(self, previous)
}
}
/// A typed portion of the state visible to the model.
///
/// Implementations own how their current state is rendered relative to an
/// earlier value of the same section type. A missing previous value requests
/// the section's complete current representation.
pub(crate) trait WorldStateSection: Any + Send + Sync {
fn render_diff(&self, previous: Option<&Self>) -> Option<Box<dyn ContextualUserFragment>>;
}
/// A snapshot of the model-visible world with one section per concrete type.
#[derive(Default)]
pub(crate) struct WorldState {
sections: IndexMap<TypeId, Box<dyn ErasedWorldStateSection>>,
}
impl fmt::Debug for WorldState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WorldState")
.field("section_count", &self.sections.len())
.finish()
}
}
impl WorldState {
pub(crate) fn add_section<S: WorldStateSection>(&mut self, section: S) {
self.sections.insert(TypeId::of::<S>(), Box::new(section));
}
pub(crate) fn render_full(&self) -> Vec<Box<dyn ContextualUserFragment>> {
self.render_diff(&Self::default())
}
pub(crate) fn render_diff(&self, previous: &Self) -> Vec<Box<dyn ContextualUserFragment>> {
self.sections
.iter()
.filter_map(|(type_id, section)| {
let previous = previous
.sections
.get(type_id)
.map(|section| section.as_any());
section.render_diff(previous)
})
.collect()
}
}
+34 -7
View File
@@ -1,3 +1,5 @@
use crate::context::ContextualUserFragment;
use crate::context::world_state::WorldState;
use crate::context_manager::normalize;
use crate::event_mapping::has_non_contextual_dev_message_content;
use crate::event_mapping::is_contextual_dev_message_content;
@@ -27,6 +29,7 @@ use codex_utils_output_truncation::truncate_function_output_items_with_policy;
use codex_utils_output_truncation::truncate_text;
use std::num::NonZeroUsize;
use std::ops::Deref;
use std::sync::Arc;
use std::sync::LazyLock;
/// Transcript of thread history
@@ -48,6 +51,8 @@ pub(crate) struct ContextManager {
/// also clear this when it trims a mixed initial-context developer bundle
/// whose non-diff fragments no longer exist in the surviving history.
reference_context_item: Option<TurnContextItem>,
/// World state most recently appended to model-visible history.
world_state_baseline: Option<Arc<WorldState>>,
}
impl ContextManager {
@@ -59,6 +64,7 @@ impl ContextManager {
&None, &None, /*model_context_window*/ None,
),
reference_context_item: None,
world_state_baseline: None,
}
}
@@ -78,6 +84,22 @@ impl ContextManager {
self.reference_context_item.clone()
}
pub(crate) fn update_world_state(
&mut self,
world_state: WorldState,
) -> Vec<Box<dyn ContextualUserFragment>> {
let fragments = self.world_state_baseline.as_deref().map_or_else(
|| world_state.render_full(),
|previous| world_state.render_diff(previous),
);
self.world_state_baseline = Some(Arc::new(world_state));
fragments
}
pub(crate) fn set_world_state_baseline(&mut self, world_state: WorldState) {
self.world_state_baseline = Some(Arc::new(world_state));
}
pub(crate) fn set_token_usage_full(&mut self, context_window: i64) {
match &mut self.token_info {
Some(info) => info.fill_to_context_window(context_window),
@@ -163,12 +185,14 @@ impl ContextManager {
// its corresponding counterpart to keep the invariants intact without
// running a full normalization pass.
normalize::remove_corresponding_for(&mut self.items, &removed);
self.world_state_baseline = None;
}
}
pub(crate) fn replace(&mut self, items: Vec<ResponseItem>) {
self.items = items;
self.history_version = self.history_version.saturating_add(1);
self.world_state_baseline = None;
}
/// Replace image content in the last turn if it originated from a tool output.
@@ -342,27 +366,28 @@ impl ContextManager {
id,
call_id,
output,
metadata,
internal_chat_message_metadata_passthrough: metadata,
} => ResponseItem::FunctionCallOutput {
id: id.clone(),
call_id: call_id.clone(),
output: truncate_function_output_payload(output, policy_with_serialization_budget),
metadata: metadata.clone(),
internal_chat_message_metadata_passthrough: metadata.clone(),
},
ResponseItem::CustomToolCallOutput {
id,
call_id,
name,
output,
metadata,
internal_chat_message_metadata_passthrough: metadata,
} => ResponseItem::CustomToolCallOutput {
id: id.clone(),
call_id: call_id.clone(),
name: name.clone(),
output: truncate_function_output_payload(output, policy_with_serialization_budget),
metadata: metadata.clone(),
internal_chat_message_metadata_passthrough: metadata.clone(),
},
ResponseItem::Message { .. }
ResponseItem::AdditionalTools { .. }
| ResponseItem::Message { .. }
| ResponseItem::AgentMessage { .. }
| ResponseItem::Reasoning { .. }
| ResponseItem::LocalShellCall { .. }
@@ -452,7 +477,8 @@ pub(crate) fn truncate_function_output_payload(
fn is_api_message(message: &ResponseItem) -> bool {
match message {
ResponseItem::Message { role, .. } => role.as_str() != "system",
ResponseItem::AgentMessage { .. }
ResponseItem::AdditionalTools { .. }
| ResponseItem::AgentMessage { .. }
| ResponseItem::FunctionCallOutput { .. }
| ResponseItem::FunctionCall { .. }
| ResponseItem::ToolSearchCall { .. }
@@ -698,7 +724,8 @@ fn is_model_generated_item(item: &ResponseItem) -> bool {
| ResponseItem::Compaction { .. }
| ResponseItem::ContextCompaction { .. } => true,
ResponseItem::CompactionTrigger { .. } => false,
ResponseItem::FunctionCallOutput { .. }
ResponseItem::AdditionalTools { .. }
| ResponseItem::FunctionCallOutput { .. }
| ResponseItem::ToolSearchOutput { .. }
| ResponseItem::CustomToolCallOutput { .. }
| ResponseItem::AgentMessage { .. }
+124 -104
View File
@@ -1,4 +1,6 @@
use super::*;
use crate::context::world_state::EnvironmentsState;
use crate::context::world_state::WorldState;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_protocol::AgentPath;
@@ -9,12 +11,12 @@ use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ImageDetail;
use codex_protocol::models::InternalChatMessageMetadataPassthrough;
use codex_protocol::models::LocalShellAction;
use codex_protocol::models::LocalShellExecAction;
use codex_protocol::models::LocalShellStatus;
use codex_protocol::models::ReasoningItemContent;
use codex_protocol::models::ReasoningItemReasoningSummary;
use codex_protocol::models::ResponseItemMetadata;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::default_input_modalities;
use codex_protocol::protocol::AskForApproval;
@@ -42,7 +44,7 @@ fn assistant_msg(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -61,7 +63,7 @@ fn inter_agent_assistant_msg(text: &str) -> ResponseItem {
text: serde_json::to_string(&communication).unwrap(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -73,6 +75,25 @@ fn create_history_with_items(items: Vec<ResponseItem>) -> ContextManager {
h
}
#[test]
fn world_state_baseline_deduplicates_until_history_is_replaced() {
let world_state = || {
let mut state = WorldState::default();
state.add_section(EnvironmentsState::from_turn_context_item(
&reference_context_item(),
));
state
};
let mut history = ContextManager::new();
assert_eq!(1, history.update_world_state(world_state()).len());
assert!(history.update_world_state(world_state()).is_empty());
history.replace(Vec::new());
assert_eq!(1, history.update_world_state(world_state()).len());
}
fn user_msg(text: &str) -> ResponseItem {
ResponseItem::Message {
id: None,
@@ -81,7 +102,7 @@ fn user_msg(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -93,7 +114,7 @@ fn user_input_text_msg(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -105,7 +126,7 @@ fn developer_msg(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -120,7 +141,7 @@ fn developer_msg_with_fragments(texts: &[&str]) -> ResponseItem {
})
.collect(),
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -159,7 +180,7 @@ fn custom_tool_call_output(call_id: &str, output: &str) -> ResponseItem {
call_id: call_id.to_string(),
name: None,
output: FunctionCallOutputPayload::from_text(output.to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -173,7 +194,7 @@ fn reasoning_msg(text: &str) -> ResponseItem {
text: text.to_string(),
}]),
encrypted_content: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -185,7 +206,7 @@ fn reasoning_with_encrypted_content(len: usize) -> ResponseItem {
}],
content: None,
encrypted_content: Some("a".repeat(len)),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -209,7 +230,7 @@ fn filters_non_api_messages() {
text: "ignored".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let reasoning = reasoning_msg("thinking...");
h.record_items([&system, &reasoning, &ResponseItem::Other], policy);
@@ -232,7 +253,7 @@ fn filters_non_api_messages() {
text: "thinking...".to_string(),
}]),
encrypted_content: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -241,7 +262,7 @@ fn filters_non_api_messages() {
text: "hi".to_string()
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -250,7 +271,7 @@ fn filters_non_api_messages() {
text: "hello".to_string()
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
]
);
@@ -400,7 +421,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
},
],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCall {
id: None,
@@ -408,7 +429,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,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCallOutput {
id: None,
@@ -422,7 +443,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
detail: Some(DEFAULT_IMAGE_DETAIL),
},
]),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::CustomToolCall {
id: None,
@@ -430,7 +451,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,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::CustomToolCallOutput {
id: None,
@@ -445,7 +466,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
detail: Some(DEFAULT_IMAGE_DETAIL),
},
]),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
let history = create_history_with_items(items);
@@ -469,7 +490,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
},
],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCall {
id: None,
@@ -477,7 +498,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,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCallOutput {
id: None,
@@ -491,7 +512,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
.to_string(),
},
]),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::CustomToolCall {
id: None,
@@ -499,7 +520,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,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::CustomToolCallOutput {
id: None,
@@ -514,7 +535,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
.to_string(),
},
]),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
assert_eq!(stripped, expected);
@@ -534,7 +555,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
},
],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}]);
let preserved = with_images.for_prompt(&modalities);
assert_eq!(preserved.len(), 1);
@@ -554,7 +575,7 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
status: "generating".to_string(),
revised_prompt: Some("lobster".to_string()),
result: "Zm9v".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -563,7 +584,7 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
text: "hi".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
]);
@@ -575,7 +596,7 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
status: "generating".to_string(),
revised_prompt: Some("lobster".to_string()),
result: "Zm9v".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -584,7 +605,7 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
text: "hi".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
]
);
@@ -600,14 +621,14 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
text: "generate a lobster".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::ImageGenerationCall {
id: Some("ig_123".to_string()),
status: "completed".to_string(),
revised_prompt: Some("lobster".to_string()),
result: "Zm9v".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
]);
@@ -621,14 +642,14 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
text: "generate a lobster".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::ImageGenerationCall {
id: Some("ig_123".to_string()),
status: "completed".to_string(),
revised_prompt: Some("lobster".to_string()),
result: String::new(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
]
);
@@ -665,13 +686,13 @@ fn remove_first_item_removes_matching_output_for_function_call() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-1".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCallOutput {
id: None,
call_id: "call-1".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
let mut h = create_history_with_items(items);
@@ -686,7 +707,7 @@ fn remove_first_item_removes_matching_call_for_output() {
id: None,
call_id: "call-2".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCall {
id: None,
@@ -694,7 +715,7 @@ fn remove_first_item_removes_matching_call_for_output() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-2".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
let mut h = create_history_with_items(items);
@@ -718,7 +739,7 @@ fn replace_last_turn_images_replaces_tool_output_images() {
]),
success: Some(true),
},
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
let mut history = create_history_with_items(items);
@@ -740,7 +761,7 @@ fn replace_last_turn_images_replaces_tool_output_images() {
]),
success: Some(true),
},
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
]
);
@@ -756,7 +777,7 @@ fn replace_last_turn_images_does_not_touch_user_images() {
detail: Some(DEFAULT_IMAGE_DETAIL),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let mut history = create_history_with_items(items.clone());
@@ -778,13 +799,13 @@ fn remove_first_item_handles_local_shell_pair() {
env: None,
user: None,
}),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCallOutput {
id: None,
call_id: "call-3".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
let mut h = create_history_with_items(items);
@@ -1007,14 +1028,14 @@ fn remove_first_item_handles_custom_tool_pair() {
call_id: "tool-1".to_string(),
name: "my_tool".to_string(),
input: "{}".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::CustomToolCallOutput {
id: None,
call_id: "tool-1".to_string(),
name: None,
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
let mut h = create_history_with_items(items);
@@ -1036,13 +1057,13 @@ fn normalization_retains_local_shell_outputs() {
env: None,
user: None,
}),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCallOutput {
id: None,
call_id: "shell-1".to_string(),
output: FunctionCallOutputPayload::from_text("Total output lines: 1\n\nok".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
@@ -1067,9 +1088,8 @@ fn record_items_truncates_function_call_output_content() {
body: FunctionCallOutputBody::Text(long_output.clone()),
success: Some(true),
},
metadata: Some(ResponseItemMetadata {
internal_chat_message_metadata_passthrough: Some(InternalChatMessageMetadataPassthrough {
turn_id: Some("turn-1".to_string()),
..Default::default()
}),
};
@@ -1105,7 +1125,7 @@ fn record_items_truncates_custom_tool_call_output_content() {
call_id: "tool-200".to_string(),
name: None,
output: FunctionCallOutputPayload::from_text(long_output.clone()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
history.record_items([&item], policy);
@@ -1140,7 +1160,7 @@ fn record_items_respects_custom_token_limit() {
body: FunctionCallOutputBody::Text(long_output),
success: Some(true),
},
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
history.record_items([&item], policy);
@@ -1260,7 +1280,7 @@ fn normalize_adds_missing_output_for_function_call() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-x".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let mut h = create_history_with_items(items);
@@ -1275,13 +1295,13 @@ fn normalize_adds_missing_output_for_function_call() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-x".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCallOutput {
id: None,
call_id: "call-x".to_string(),
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
]
);
@@ -1296,7 +1316,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,
internal_chat_message_metadata_passthrough: None,
}];
let mut h = create_history_with_items(items);
@@ -1311,14 +1331,14 @@ fn normalize_adds_missing_output_for_custom_tool_call() {
call_id: "tool-x".to_string(),
name: "custom".to_string(),
input: "{}".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::CustomToolCallOutput {
id: None,
call_id: "tool-x".to_string(),
name: None,
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
]
);
@@ -1338,7 +1358,7 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id() {
env: None,
user: None,
}),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let mut h = create_history_with_items(items);
@@ -1358,13 +1378,13 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id() {
env: None,
user: None,
}),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCallOutput {
id: None,
call_id: "shell-1".to_string(),
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
]
);
@@ -1377,7 +1397,7 @@ fn normalize_removes_orphan_function_call_output() {
id: None,
call_id: "orphan-1".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let mut h = create_history_with_items(items);
@@ -1394,7 +1414,7 @@ fn normalize_removes_orphan_custom_tool_call_output() {
call_id: "orphan-2".to_string(),
name: None,
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let mut h = create_history_with_items(items);
@@ -1414,14 +1434,14 @@ fn normalize_mixed_inserts_and_removals() {
namespace: None,
arguments: "{}".to_string(),
call_id: "c1".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
// Orphan output that should be removed
ResponseItem::FunctionCallOutput {
id: None,
call_id: "c2".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
// Will get an inserted custom tool output
ResponseItem::CustomToolCall {
@@ -1430,7 +1450,7 @@ fn normalize_mixed_inserts_and_removals() {
call_id: "t1".to_string(),
name: "tool".to_string(),
input: "{}".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
// Local shell call also gets an inserted function call output
ResponseItem::LocalShellCall {
@@ -1444,7 +1464,7 @@ fn normalize_mixed_inserts_and_removals() {
env: None,
user: None,
}),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
let mut h = create_history_with_items(items);
@@ -1460,13 +1480,13 @@ fn normalize_mixed_inserts_and_removals() {
namespace: None,
arguments: "{}".to_string(),
call_id: "c1".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCallOutput {
id: None,
call_id: "c1".to_string(),
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::CustomToolCall {
id: None,
@@ -1474,14 +1494,14 @@ fn normalize_mixed_inserts_and_removals() {
call_id: "t1".to_string(),
name: "tool".to_string(),
input: "{}".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::CustomToolCallOutput {
id: None,
call_id: "t1".to_string(),
name: None,
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::LocalShellCall {
id: None,
@@ -1494,13 +1514,13 @@ fn normalize_mixed_inserts_and_removals() {
env: None,
user: None,
}),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCallOutput {
id: None,
call_id: "s1".to_string(),
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
]
);
@@ -1514,7 +1534,7 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-x".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let mut h = create_history_with_items(items);
h.normalize_history(&default_input_modalities());
@@ -1527,13 +1547,13 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-x".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCallOutput {
id: None,
call_id: "call-x".to_string(),
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
]
);
@@ -1547,7 +1567,7 @@ fn normalize_adds_missing_output_for_tool_search_call() {
status: Some("completed".to_string()),
execution: "client".to_string(),
arguments: "{}".into(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let mut h = create_history_with_items(items);
@@ -1562,7 +1582,7 @@ fn normalize_adds_missing_output_for_tool_search_call() {
status: Some("completed".to_string()),
execution: "client".to_string(),
arguments: "{}".into(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::ToolSearchOutput {
id: None,
@@ -1570,7 +1590,7 @@ fn normalize_adds_missing_output_for_tool_search_call() {
status: "completed".to_string(),
execution: "client".to_string(),
tools: Vec::new(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
]
);
@@ -1586,7 +1606,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,
internal_chat_message_metadata_passthrough: None,
}];
let mut h = create_history_with_items(items);
h.normalize_history(&default_input_modalities());
@@ -1607,7 +1627,7 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id_panics_in_debug()
env: None,
user: None,
}),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let mut h = create_history_with_items(items);
h.normalize_history(&default_input_modalities());
@@ -1621,7 +1641,7 @@ fn normalize_removes_orphan_function_call_output_panics_in_debug() {
id: None,
call_id: "orphan-1".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let mut h = create_history_with_items(items);
h.normalize_history(&default_input_modalities());
@@ -1636,7 +1656,7 @@ fn normalize_removes_orphan_custom_tool_call_output_panics_in_debug() {
call_id: "orphan-2".to_string(),
name: None,
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let mut h = create_history_with_items(items);
h.normalize_history(&default_input_modalities());
@@ -1651,7 +1671,7 @@ fn normalize_removes_orphan_client_tool_search_output() {
status: "completed".to_string(),
execution: "client".to_string(),
tools: Vec::new(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let mut h = create_history_with_items(items);
@@ -1670,7 +1690,7 @@ fn normalize_removes_orphan_client_tool_search_output_panics_in_debug() {
status: "completed".to_string(),
execution: "client".to_string(),
tools: Vec::new(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let mut h = create_history_with_items(items);
h.normalize_history(&default_input_modalities());
@@ -1684,7 +1704,7 @@ fn normalize_keeps_server_tool_search_output_without_matching_call() {
status: "completed".to_string(),
execution: "server".to_string(),
tools: Vec::new(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
let mut h = create_history_with_items(items);
@@ -1698,7 +1718,7 @@ fn normalize_keeps_server_tool_search_output_without_matching_call() {
status: "completed".to_string(),
execution: "server".to_string(),
tools: Vec::new(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}]
);
}
@@ -1714,13 +1734,13 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() {
namespace: None,
arguments: "{}".to_string(),
call_id: "c1".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCallOutput {
id: None,
call_id: "c2".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::CustomToolCall {
id: None,
@@ -1728,7 +1748,7 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() {
call_id: "t1".to_string(),
name: "tool".to_string(),
input: "{}".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::LocalShellCall {
id: None,
@@ -1741,7 +1761,7 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() {
env: None,
user: None,
}),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
let mut h = create_history_with_items(items);
@@ -1765,7 +1785,7 @@ fn image_data_url_payload_does_not_dominate_message_estimate() {
},
],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let text_only_item = ResponseItem::Message {
id: None,
@@ -1774,7 +1794,7 @@ fn image_data_url_payload_does_not_dominate_message_estimate() {
text: "Here is the screenshot".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let raw_len = serde_json::to_string(&image_item).unwrap().len() as i64;
@@ -1803,7 +1823,7 @@ fn image_data_url_payload_does_not_dominate_function_call_output_estimate() {
detail: Some(DEFAULT_IMAGE_DETAIL),
},
]),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1831,7 +1851,7 @@ fn image_data_url_payload_does_not_dominate_custom_tool_call_output_estimate() {
detail: Some(DEFAULT_IMAGE_DETAIL),
},
]),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1852,7 +1872,7 @@ fn non_base64_image_urls_are_unchanged() {
detail: Some(DEFAULT_IMAGE_DETAIL),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let function_output_item = ResponseItem::FunctionCallOutput {
id: None,
@@ -1863,7 +1883,7 @@ fn non_base64_image_urls_are_unchanged() {
detail: Some(DEFAULT_IMAGE_DETAIL),
},
]),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
assert_eq!(
@@ -1887,7 +1907,7 @@ fn encrypted_function_output_uses_plaintext_byte_estimate() {
encrypted_content: encrypted_content.clone(),
},
]),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1908,7 +1928,7 @@ fn data_url_without_base64_marker_is_unchanged() {
detail: Some(DEFAULT_IMAGE_DETAIL),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
assert_eq!(
@@ -1930,7 +1950,7 @@ fn non_image_base64_data_url_is_unchanged() {
detail: Some(DEFAULT_IMAGE_DETAIL),
},
]),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1951,7 +1971,7 @@ fn mixed_case_data_url_markers_are_adjusted() {
detail: Some(DEFAULT_IMAGE_DETAIL),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1984,7 +2004,7 @@ fn multiple_inline_images_apply_multiple_fixed_costs() {
},
],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -2019,7 +2039,7 @@ fn original_detail_images_scale_with_dimensions() {
detail: Some(ImageDetail::Original),
},
]),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -2051,7 +2071,7 @@ fn original_detail_images_are_capped_at_max_patch_count() {
detail: Some(ImageDetail::Original),
},
]),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -2086,7 +2106,7 @@ fn original_detail_webp_images_scale_with_dimensions() {
detail: Some(ImageDetail::Original),
},
]),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -2105,7 +2125,7 @@ fn text_only_items_unchanged() {
text: "Hello world, this is a response.".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let estimated = estimate_response_item_model_visible_bytes(&item);
@@ -50,7 +50,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItem>) {
id: None,
call_id: call_id.clone(),
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
));
}
@@ -67,7 +67,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItem>) {
status: "completed".to_string(),
execution: "client".to_string(),
tools: Vec::new(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
));
}
@@ -84,7 +84,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItem>) {
call_id: call_id.clone(),
name: None,
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
));
}
@@ -102,7 +102,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItem>) {
id: None,
call_id: call_id.clone(),
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
));
}
+27 -36
View File
@@ -1,6 +1,5 @@
use crate::context::CollaborationModeInstructions;
use crate::context::ContextualUserFragment;
use crate::context::EnvironmentContext;
use crate::context::ModelSwitchInstructions;
use crate::context::MultiAgentModeInstructions;
use crate::context::PermissionsInstructions;
@@ -10,7 +9,6 @@ use crate::context::RealtimeStartInstructions;
use crate::context::RealtimeStartWithInstructions;
use crate::session::PreviousTurnSettings;
use crate::session::turn_context::TurnContext;
use crate::shell::Shell;
use codex_execpolicy::Policy;
use codex_features::Feature;
use codex_protocol::config_types::MultiAgentMode;
@@ -20,27 +18,6 @@ use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::protocol::TurnContextItem;
fn build_environment_update_item(
previous: Option<&TurnContextItem>,
next: &TurnContext,
shell: &Shell,
) -> Option<ResponseItem> {
if !next.config.include_environment_context {
return None;
}
let prev = previous?;
let prev_context = EnvironmentContext::from_turn_context_item(prev, shell.name().to_string());
let next_context = EnvironmentContext::from_turn_context(next, shell);
if prev_context.equals_except_shell(&next_context) {
return None;
}
Some(ContextualUserFragment::into(
EnvironmentContext::diff_from_turn_context_item(prev, &next_context),
))
}
fn build_permissions_update_item(
previous: Option<&TurnContextItem>,
next: &TurnContext,
@@ -103,10 +80,8 @@ fn build_multi_agent_mode_update_item(
) -> Option<String> {
let effective_multi_agent_mode = crate::session::multi_agents::effective_multi_agent_mode(
next.multi_agent_version,
&next.config.multi_agent_v2,
&next.session_source,
next.multi_agent_mode,
next.config.features.enabled(Feature::MultiAgentMode),
);
let previous = previous?;
if previous.multi_agent_mode == effective_multi_agent_mode {
@@ -114,6 +89,9 @@ fn build_multi_agent_mode_update_item(
}
match effective_multi_agent_mode {
Some(MultiAgentMode::None) => {
Some(MultiAgentModeInstructions::new(MultiAgentMode::None).render())
}
Some(multi_agent_mode) => Some(MultiAgentModeInstructions::new(multi_agent_mode).render()),
None if previous.multi_agent_mode == Some(MultiAgentMode::Proactive) => {
Some(MultiAgentModeInstructions::new(MultiAgentMode::ExplicitRequestOnly).render())
@@ -219,6 +197,26 @@ pub(crate) fn build_contextual_user_message(text_sections: Vec<String>) -> Optio
build_text_message("user", text_sections)
}
pub(crate) fn merge_contextual_fragments(
fragments: Vec<Box<dyn ContextualUserFragment>>,
) -> Vec<ResponseItem> {
let mut messages: Vec<(&str, Vec<String>)> = Vec::with_capacity(fragments.len());
for fragment in fragments {
let role = fragment.role();
let text = fragment.render();
match messages.last_mut() {
Some((previous_role, text_sections)) if *previous_role == role => {
text_sections.push(text);
}
_ => messages.push((role, vec![text])),
}
}
messages
.into_iter()
.filter_map(|(role, text_sections)| build_text_message(role, text_sections))
.collect()
}
fn build_text_message(role: &str, text_sections: Vec<String>) -> Option<ResponseItem> {
if text_sections.is_empty() {
return None;
@@ -234,7 +232,7 @@ fn build_text_message(role: &str, text_sections: Vec<String>) -> Option<Response
role: role.to_string(),
content,
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
})
}
@@ -242,7 +240,6 @@ pub(crate) fn build_settings_update_items(
previous: Option<&TurnContextItem>,
previous_turn_settings: Option<&PreviousTurnSettings>,
next: &TurnContext,
shell: &Shell,
exec_policy: &Policy,
personality_feature_enabled: bool,
) -> Vec<ResponseItem> {
@@ -250,7 +247,6 @@ pub(crate) fn build_settings_update_items(
// model-visible item emitted by build_initial_context. Persist the remaining
// inputs or add explicit replay events so fork/resume can diff everything
// deterministically.
let contextual_user_message = build_environment_update_item(previous, next, shell);
let developer_update_sections = [
// Keep model-switch instructions first so model-specific guidance is read before
// any other context diffs on this turn.
@@ -265,12 +261,7 @@ pub(crate) fn build_settings_update_items(
.flatten()
.collect();
let mut items = Vec::with_capacity(2);
if let Some(developer_message) = build_developer_update_item(developer_update_sections) {
items.push(developer_message);
}
if let Some(contextual_user_message) = contextual_user_message {
items.push(contextual_user_message);
}
items
build_developer_update_item(developer_update_sections)
.into_iter()
.collect()
}
+3
View File
@@ -15,6 +15,7 @@ use codex_protocol::models::is_image_open_tag_text;
use codex_protocol::models::is_local_image_close_tag_text;
use codex_protocol::models::is_local_image_open_tag_text;
use codex_protocol::protocol::COLLABORATION_MODE_OPEN_TAG;
use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG;
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
@@ -34,7 +35,9 @@ const CONTEXTUAL_DEVELOPER_PREFIXES: &[&str] = &[
REALTIME_CONVERSATION_OPEN_TAG,
SKILLS_INSTRUCTIONS_OPEN_TAG,
"<personality_spec>",
// Keep recognizing token-budget wrappers persisted by older versions.
"<token_budget>",
CONTEXT_WINDOW_OPEN_TAG,
"<rollout_budget>",
];
+36 -20
View File
@@ -15,6 +15,8 @@ use codex_protocol::models::ReasoningItemContent;
use codex_protocol::models::ReasoningItemReasoningSummary;
use codex_protocol::models::ResponseItem;
use codex_protocol::models::WebSearchAction;
use codex_protocol::protocol::CONTEXT_WINDOW_CLOSE_TAG;
use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::user_input::UserInput;
use pretty_assertions::assert_eq;
@@ -29,7 +31,7 @@ fn recognizes_skills_instructions_as_contextual_developer_content() {
}
#[test]
fn recognizes_token_budget_as_contextual_developer_content() {
fn recognizes_legacy_token_budget_as_contextual_developer_content() {
let content = vec![ContentItem::InputText {
text: "<token_budget>\nYou have 710 tokens left in this context window.\n</token_budget>"
.to_string(),
@@ -39,6 +41,20 @@ fn recognizes_token_budget_as_contextual_developer_content() {
assert!(!has_non_contextual_dev_message_content(&content));
}
#[test]
fn recognizes_context_window_as_contextual_developer_content() {
let content = vec![ContentItem::InputText {
text: format!(
r#"{CONTEXT_WINDOW_OPEN_TAG}
Thread id: 00000000-0000-0000-0000-000000000000
{CONTEXT_WINDOW_CLOSE_TAG}"#
),
}];
assert!(is_contextual_dev_message_content(&content));
assert!(!has_non_contextual_dev_message_content(&content));
}
#[test]
fn parses_user_message_with_text_and_two_images() {
let img1 = "https://example.com/one.png".to_string();
@@ -61,7 +77,7 @@ fn parses_user_message_with_text_and_two_images() {
},
],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let turn_item = parse_turn_item(&item).expect("expected user message turn item");
@@ -111,7 +127,7 @@ fn skips_local_image_label_text() {
},
],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let turn_item = parse_turn_item(&item).expect("expected user message turn item");
@@ -144,7 +160,7 @@ fn parses_assistant_message_input_text_for_backward_compatibility() {
.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let turn_item = parse_turn_item(&item).expect("expected assistant message turn item");
@@ -194,7 +210,7 @@ fn skips_unnamed_image_label_text() {
},
],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let turn_item = parse_turn_item(&item).expect("expected user message turn item");
@@ -227,7 +243,7 @@ fn skips_user_instructions_and_env() {
text: "# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".to_string(),
}],
phase: None,
metadata: None,},
internal_chat_message_metadata_passthrough: None,},
ResponseItem::Message {
id: None,
role: "user".to_string(),
@@ -235,7 +251,7 @@ fn skips_user_instructions_and_env() {
text: "<environment_context>test_text</environment_context>".to_string(),
}],
phase: None,
metadata: None,},
internal_chat_message_metadata_passthrough: None,},
ResponseItem::Message {
id: None,
role: "user".to_string(),
@@ -243,7 +259,7 @@ fn skips_user_instructions_and_env() {
text: "# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".to_string(),
}],
phase: None,
metadata: None,},
internal_chat_message_metadata_passthrough: None,},
ResponseItem::Message {
id: None,
role: "user".to_string(),
@@ -252,7 +268,7 @@ fn skips_user_instructions_and_env() {
.to_string(),
}],
phase: None,
metadata: None,},
internal_chat_message_metadata_passthrough: None,},
ResponseItem::Message {
id: None,
role: "user".to_string(),
@@ -260,7 +276,7 @@ fn skips_user_instructions_and_env() {
text: "<user_shell_command>echo 42</user_shell_command>".to_string(),
}],
phase: None,
metadata: None,},
internal_chat_message_metadata_passthrough: None,},
ResponseItem::Message {
id: None,
role: "user".to_string(),
@@ -275,7 +291,7 @@ fn skips_user_instructions_and_env() {
},
],
phase: None,
metadata: None,},
internal_chat_message_metadata_passthrough: None,},
];
for item in items {
@@ -325,7 +341,7 @@ fn parses_hook_prompt_and_hides_other_contextual_fragments() {
},
],
phase: None,
metadata: None,};
internal_chat_message_metadata_passthrough: None,};
let turn_item = parse_turn_item(&item).expect("expected hook prompt turn item");
@@ -357,7 +373,7 @@ fn internal_model_context_does_not_parse_as_visible_turn_item() {
.render(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
assert!(parse_turn_item(&item).is_none());
@@ -372,7 +388,7 @@ fn parses_agent_message() {
text: "Hello from Codex".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let turn_item = parse_turn_item(&item).expect("expected agent message turn item");
@@ -404,7 +420,7 @@ fn parses_reasoning_summary_and_raw_content() {
text: "raw details".to_string(),
}]),
encrypted_content: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let turn_item = parse_turn_item(&item).expect("expected reasoning turn item");
@@ -437,7 +453,7 @@ fn parses_reasoning_including_raw_content() {
},
]),
encrypted_content: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let turn_item = parse_turn_item(&item).expect("expected reasoning turn item");
@@ -463,7 +479,7 @@ fn parses_web_search_call() {
query: Some("weather".to_string()),
queries: None,
}),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let turn_item = parse_turn_item(&item).expect("expected web search turn item");
@@ -492,7 +508,7 @@ fn parses_web_search_open_page_call() {
action: Some(WebSearchAction::OpenPage {
url: Some("https://example.com".to_string()),
}),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let turn_item = parse_turn_item(&item).expect("expected web search turn item");
@@ -521,7 +537,7 @@ fn parses_web_search_find_in_page_call() {
url: Some("https://example.com".to_string()),
pattern: Some("needle".to_string()),
}),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let turn_item = parse_turn_item(&item).expect("expected web search turn item");
@@ -548,7 +564,7 @@ fn parses_partial_web_search_call_without_action_as_other() {
id: Some("ws_partial".to_string()),
status: Some("in_progress".to_string()),
action: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let turn_item = parse_turn_item(&item).expect("expected web search turn item");
+3 -62
View File
@@ -42,6 +42,7 @@ use codex_sandboxing::SandboxTransformRequest;
use codex_sandboxing::SandboxType;
use codex_sandboxing::SandboxablePreference;
use codex_sandboxing::WindowsSandboxFilesystemOverrides;
pub(crate) use codex_sandboxing::is_likely_sandbox_denied;
#[cfg(test)]
use codex_sandboxing::permission_profile_supports_windows_restricted_token_sandbox;
use codex_sandboxing::resolve_windows_elevated_filesystem_overrides;
@@ -456,6 +457,8 @@ pub(crate) async fn execute_exec_request(
windows_sandbox_filesystem_overrides,
network_environment_id,
arg0,
exec_server_sandbox: _,
exec_server_enforce_managed_network: _,
} = exec_request;
// TODO(anp): Keep PathUri through the local process launch boundary.
@@ -817,68 +820,6 @@ fn finalize_exec_result(
}
}
/// We don't have a fully deterministic way to tell if our command failed
/// because of the sandbox - a command in the user's zshrc file might hit an
/// error, but the command itself might fail or succeed for other reasons.
/// For now, we conservatively check for well known command failure exit codes and
/// also look for common sandbox denial keywords in the command output.
pub(crate) fn is_likely_sandbox_denied(
sandbox_type: SandboxType,
exec_output: &ExecToolCallOutput,
) -> bool {
if sandbox_type == SandboxType::None || exec_output.exit_code == 0 {
return false;
}
// Quick rejects: well-known non-sandbox shell exit codes
// 2: misuse of shell builtins
// 126: permission denied
// 127: command not found
const SANDBOX_DENIED_KEYWORDS: [&str; 7] = [
"operation not permitted",
"permission denied",
"read-only file system",
"seccomp",
"sandbox",
"landlock",
"failed to write file",
];
let has_sandbox_keyword = [
&exec_output.stderr.text,
&exec_output.stdout.text,
&exec_output.aggregated_output.text,
]
.into_iter()
.any(|section| {
let lower = section.to_lowercase();
SANDBOX_DENIED_KEYWORDS
.iter()
.any(|needle| lower.contains(needle))
});
if has_sandbox_keyword {
return true;
}
const QUICK_REJECT_EXIT_CODES: [i32; 3] = [2, 126, 127];
if QUICK_REJECT_EXIT_CODES.contains(&exec_output.exit_code) {
return false;
}
#[cfg(unix)]
{
const SIGSYS_CODE: i32 = libc::SIGSYS;
if sandbox_type == SandboxType::LinuxSeccomp
&& exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + SIGSYS_CODE
{
return true;
}
}
false
}
#[derive(Debug)]
struct RawExecToolCallOutput {
pub exit_status: ExitStatus,
+63 -45
View File
@@ -665,40 +665,23 @@ pub(crate) fn spawn_approval_request_review(
rx
}
/// Runs the guardian in a locked-down reusable review session.
///
/// The guardian itself should not mutate state or trigger further approvals, so
/// it is pinned to a read-only sandbox with `approval_policy = never` and
/// nonessential agent features disabled. When the cached trunk session is idle,
/// later approvals append onto that same guardian conversation to preserve a
/// stable prompt-cache key. If the trunk is already busy, the review runs in an
/// ephemeral fork from the last committed trunk rollout so parallel approvals
/// do not block each other or mutate the cached thread. The trunk is recreated
/// when the effective review-session config changes, and any future compaction
/// must continue to preserve the guardian policy as exact top-level developer
/// context. It may still reuse the parent's managed-network allowlist for
/// read-only checks, but it intentionally runs without inherited exec-policy
/// rules.
async fn run_guardian_review_session_before_deadline(
session: Arc<Session>,
turn: Arc<TurnContext>,
request: GuardianApprovalRequest,
retry_reason: Option<String>,
schema: serde_json::Value,
external_cancel: Option<CancellationToken>,
deadline: Instant,
) -> (GuardianReviewOutcome, GuardianReviewAnalyticsResult) {
pub(super) struct GuardianReviewSessionConfig {
pub(super) spawn_config: crate::config::Config,
model: String,
reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
default_review_model_id: String,
catalog_contains_auto_review: bool,
model_overridden: bool,
model_override: Option<String>,
}
pub(super) async fn guardian_review_session_config(
session: &Session,
turn: &TurnContext,
) -> anyhow::Result<GuardianReviewSessionConfig> {
let network_proxy = session.services.network_proxy.load_full();
let live_network_config = match network_proxy.as_ref() {
Some(network_proxy) => match network_proxy.proxy().current_cfg().await {
Ok(config) => Some(config),
Err(err) => {
return (
GuardianReviewOutcome::Error(GuardianReviewError::prompt_build(err)),
GuardianReviewAnalyticsResult::without_session(),
);
}
},
Some(network_proxy) => Some(network_proxy.proxy().current_cfg().await?),
None => None,
};
let available_models = session
@@ -750,14 +733,50 @@ async fn run_guardian_review_session_before_deadline(
reasoning_effort,
)
};
let guardian_config = build_guardian_review_session_config(
let spawn_config = build_guardian_review_session_config(
turn.config.as_ref(),
live_network_config.clone(),
live_network_config,
guardian_model.as_str(),
guardian_reasoning_effort.clone(),
);
let guardian_config = match guardian_config {
Ok(config) => config,
)?;
Ok(GuardianReviewSessionConfig {
spawn_config,
model: guardian_model,
reasoning_effort: guardian_reasoning_effort,
default_review_model_id: default_review_model_id.to_string(),
catalog_contains_auto_review: guardian_catalog_contains_auto_review,
model_overridden: guardian_review_model_overridden,
model_override: guardian_review_model_override,
})
}
/// Runs the guardian in a locked-down reusable review session.
///
/// The guardian itself should not mutate state or trigger further approvals, so
/// it is pinned to a read-only sandbox with `approval_policy = never` and
/// nonessential agent features disabled. When the cached trunk session is idle,
/// later approvals append onto that same guardian conversation to preserve a
/// stable prompt-cache key. If the trunk is already busy, the review runs in an
/// ephemeral fork from the last committed trunk rollout so parallel approvals
/// do not block each other or mutate the cached thread. The trunk is recreated
/// when the effective review-session config changes, and any future compaction
/// must continue to preserve the guardian policy as exact top-level developer
/// context. It may still reuse the parent's managed-network allowlist for
/// read-only checks, but it intentionally runs without inherited exec-policy
/// rules.
async fn run_guardian_review_session_before_deadline(
session: Arc<Session>,
turn: Arc<TurnContext>,
request: GuardianApprovalRequest,
retry_reason: Option<String>,
schema: serde_json::Value,
external_cancel: Option<CancellationToken>,
deadline: Instant,
) -> (GuardianReviewOutcome, GuardianReviewAnalyticsResult) {
let session_config = match guardian_review_session_config(session.as_ref(), turn.as_ref()).await
{
Ok(session_config) => session_config,
Err(err) => {
return (
GuardianReviewOutcome::Error(GuardianReviewError::prompt_build(err)),
@@ -765,23 +784,22 @@ async fn run_guardian_review_session_before_deadline(
);
}
};
let (session_outcome, session_analytics_result) = Box::pin(
session
.guardian_review_session
.run_review(GuardianReviewSessionParams {
parent_session: Arc::clone(&session),
parent_turn: turn.clone(),
spawn_config: guardian_config,
spawn_config: session_config.spawn_config,
request,
retry_reason,
schema,
model: guardian_model,
reasoning_effort: guardian_reasoning_effort,
guardian_default_review_model_id: default_review_model_id.to_string(),
guardian_catalog_contains_auto_review,
guardian_review_model_overridden,
guardian_review_model_override,
model: session_config.model,
reasoning_effort: session_config.reasoning_effort,
guardian_default_review_model_id: session_config.default_review_model_id,
guardian_catalog_contains_auto_review: session_config.catalog_contains_auto_review,
guardian_review_model_overridden: session_config.model_overridden,
guardian_review_model_override: session_config.model_override,
reasoning_summary: turn.reasoning_summary,
personality: turn.personality,
external_cancel,
+53 -9
View File
@@ -27,6 +27,7 @@ use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_protocol::protocol::TokenUsage;
use futures::future::BoxFuture;
use serde_json::Value;
use tokio::sync::Mutex;
use tokio::sync::Semaphore;
@@ -56,6 +57,7 @@ use super::prompt::GuardianTranscriptCursor;
use super::prompt::build_guardian_prompt_items_with_parent_turn;
use super::prompt::guardian_policy_prompt;
use super::prompt::guardian_policy_prompt_with_config;
use super::review::guardian_review_session_config;
const GUARDIAN_INTERRUPT_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug)]
@@ -92,6 +94,7 @@ pub(crate) struct GuardianReviewSessionParams {
#[derive(Default)]
pub(crate) struct GuardianReviewSessionManager {
state: Arc<Mutex<GuardianReviewSessionState>>,
cancellation_token: CancellationToken,
}
#[derive(Default)]
@@ -289,6 +292,42 @@ impl Drop for EphemeralReviewCleanup {
}
impl GuardianReviewSessionManager {
pub(crate) fn initialize(
&self,
parent_session: Arc<Session>,
parent_turn: Arc<TurnContext>,
) -> BoxFuture<'_, anyhow::Result<()>> {
// Boxing breaks the Session::new -> Guardian -> Session::new future recursion.
Box::pin(async move {
let spawn_config = guardian_review_session_config(&parent_session, &parent_turn)
.await?
.spawn_config;
let reuse_key = GuardianReviewSessionReuseKey::from_spawn_config(
&spawn_config,
parent_session.user_instructions().await,
);
let spawn_cancel_token = self.cancellation_token.child_token();
let spawn_cancel_guard = spawn_cancel_token.clone().drop_guard();
let review_session = spawn_guardian_review_session(
&parent_session,
&parent_turn,
spawn_config,
reuse_key,
spawn_cancel_token.clone(),
/*fork_snapshot*/ None,
)
.await?;
// A first review or shutdown may win while eager initialization is in flight;
// install only if neither has happened.
let mut state = self.state.lock().await;
if !spawn_cancel_token.is_cancelled() && state.trunk.is_none() {
state.trunk = Some(Arc::new(review_session));
drop(spawn_cancel_guard.disarm());
}
Ok(())
})
}
pub(crate) async fn trunk_rollout_path(&self) -> Option<PathBuf> {
let trunk = self.state.lock().await.trunk.clone()?;
trunk.codex.session.ensure_rollout_materialized().await;
@@ -302,6 +341,7 @@ impl GuardianReviewSessionManager {
}
pub(crate) async fn shutdown(&self) {
self.cancellation_token.cancel();
let (review_session, ephemeral_reviews) = {
let mut state = self.state.lock().await;
(
@@ -348,13 +388,14 @@ impl GuardianReviewSessionManager {
}
if state.trunk.is_none() {
let spawn_cancel_token = CancellationToken::new();
let spawn_cancel_token = self.cancellation_token.child_token();
let review_session = match run_before_review_deadline_with_cancel(
deadline,
params.external_cancel.as_ref(),
&spawn_cancel_token,
Box::pin(spawn_guardian_review_session(
&params,
&params.parent_session,
&params.parent_turn,
params.spawn_config.clone(),
next_reuse_key.clone(),
spawn_cancel_token.clone(),
@@ -556,7 +597,7 @@ impl GuardianReviewSessionManager {
deadline: tokio::time::Instant,
fork_snapshot: Option<GuardianReviewForkSnapshot>,
) -> (GuardianReviewSessionOutcome, GuardianReviewAnalyticsResult) {
let spawn_cancel_token = CancellationToken::new();
let spawn_cancel_token = self.cancellation_token.child_token();
let mut fork_config = params.spawn_config.clone();
fork_config.ephemeral = true;
let review_session = match run_before_review_deadline_with_cancel(
@@ -564,7 +605,8 @@ impl GuardianReviewSessionManager {
params.external_cancel.as_ref(),
&spawn_cancel_token,
Box::pin(spawn_guardian_review_session(
&params,
&params.parent_session,
&params.parent_turn,
fork_config,
reuse_key,
spawn_cancel_token.clone(),
@@ -605,7 +647,8 @@ impl GuardianReviewSessionManager {
}
async fn spawn_guardian_review_session(
params: &GuardianReviewSessionParams,
parent_session: &Arc<Session>,
parent_turn: &Arc<TurnContext>,
spawn_config: Config,
reuse_key: GuardianReviewSessionReuseKey,
cancel_token: CancellationToken,
@@ -621,10 +664,10 @@ async fn spawn_guardian_review_session(
};
let codex = Box::pin(run_codex_thread_interactive(
spawn_config,
params.parent_session.services.auth_manager.clone(),
params.parent_session.services.models_manager.clone(),
Arc::clone(&params.parent_session),
Arc::clone(&params.parent_turn),
parent_session.services.auth_manager.clone(),
parent_session.services.models_manager.clone(),
Arc::clone(parent_session),
Arc::clone(parent_turn),
cancel_token.clone(),
SubAgentSource::Other(GUARDIAN_REVIEWER_NAME.to_string()),
initial_history,
@@ -1546,6 +1589,7 @@ mod tests {
trunk: Some(Arc::new(review_session)),
ephemeral_reviews: Vec::new(),
})),
..Default::default()
};
drop(tx_event);
+27 -27
View File
@@ -298,7 +298,7 @@ async fn seed_guardian_parent_history(session: &Arc<Session>, turn: &Arc<TurnCon
.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCall {
id: None,
@@ -306,7 +306,7 @@ async fn seed_guardian_parent_history(session: &Arc<Session>, turn: &Arc<TurnCon
namespace: None,
arguments: "{\"repo\":\"openai/codex\"}".to_string(),
call_id: "call-1".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCallOutput {
id: None,
@@ -314,7 +314,7 @@ async fn seed_guardian_parent_history(session: &Arc<Session>, turn: &Arc<TurnCon
output: codex_protocol::models::FunctionCallOutputPayload::from_text(
"repo visibility: public".to_string(),
),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -324,7 +324,7 @@ async fn seed_guardian_parent_history(session: &Arc<Session>, turn: &Arc<TurnCon
.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
],
)
@@ -531,7 +531,7 @@ async fn build_guardian_prompt_delta_mode_preserves_original_numbering() -> anyh
text: "Please also push the second docs fix.".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -540,7 +540,7 @@ async fn build_guardian_prompt_delta_mode_preserves_original_numbering() -> anyh
text: "I need approval for the second push.".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
],
)
@@ -663,7 +663,7 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() -
text: "Compacted retained user request.".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -672,7 +672,7 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() -
text: "Compacted summary of earlier guardian context.".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
],
/*reference_context_item*/ None,
@@ -689,7 +689,7 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() -
text: "Please push after the compaction.".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -698,7 +698,7 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() -
text: "I need approval for the post-compaction push.".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
],
)
@@ -746,7 +746,7 @@ fn collect_guardian_transcript_entries_skips_contextual_user_messages() {
text: "<environment_context>\n<cwd>/tmp</cwd>\n</environment_context>".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -755,7 +755,7 @@ fn collect_guardian_transcript_entries_skips_contextual_user_messages() {
text: "hello".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
@@ -783,7 +783,7 @@ fn collect_guardian_transcript_entries_keeps_manual_approval_developer_message()
text: "ordinary developer context".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -792,7 +792,7 @@ fn collect_guardian_transcript_entries_keeps_manual_approval_developer_message()
text: approval_text.clone(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
@@ -817,7 +817,7 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() {
text: "check the repo".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCall {
id: None,
@@ -825,7 +825,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,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCallOutput {
id: None,
@@ -833,7 +833,7 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() {
output: codex_protocol::models::FunctionCallOutputPayload::from_text(
"repo is public".to_string(),
),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -842,7 +842,7 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() {
text: "I need to push a fix".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
@@ -1698,7 +1698,7 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot()
),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}],
)
.await;
@@ -1930,7 +1930,7 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
text: "Please push the second docs fix too.".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -1939,7 +1939,7 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
text: "I need approval for the second docs fix.".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
],
)
@@ -1977,7 +1977,7 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
text: "Please push the third docs fix too.".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -1986,7 +1986,7 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
text: "I need approval for the third docs fix.".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
],
)
@@ -2726,7 +2726,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() ->
text: "Please inspect pending changes before pushing.".to_string(),
}],
phase: None,
metadata: None,},
internal_chat_message_metadata_passthrough: None,},
ResponseItem::Message {
id: None,
role: "assistant".to_string(),
@@ -2734,7 +2734,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() ->
text: "I need approval to run git diff.".to_string(),
}],
phase: None,
metadata: None,},
internal_chat_message_metadata_passthrough: None,},
],
)
.await;
@@ -2793,7 +2793,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() ->
text: "Now inspect whether pushing is safe.".to_string(),
}],
phase: None,
metadata: None,},
internal_chat_message_metadata_passthrough: None,},
ResponseItem::Message {
id: None,
role: "assistant".to_string(),
@@ -2801,7 +2801,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() ->
text: "I need approval to push after the diff check.".to_string(),
}],
phase: None,
metadata: None,},
internal_chat_message_metadata_passthrough: None,},
],
)
.await;
+20 -3
View File
@@ -13,6 +13,8 @@ pub(crate) const IMAGE_PROCESSING_ERROR_PLACEHOLDER: &str =
const IMAGE_TOO_LARGE_PLACEHOLDER: &str =
"image content omitted because it exceeded the supported size limit; use a smaller image";
const UNSUPPORTED_LOW_DETAIL_PLACEHOLDER: &str = "image content omitted because detail 'low' is not supported; use 'high', 'original', or 'auto'";
const REMOTE_IMAGE_URL_PLACEHOLDER: &str =
"image content omitted because remote image URLs are not supported";
const HIGH_DETAIL_LIMITS: PromptImageResizeLimits = PromptImageResizeLimits {
max_dimension: 2048,
@@ -24,6 +26,8 @@ const ORIGINAL_DETAIL_LIMITS: PromptImageResizeLimits = PromptImageResizeLimits
};
#[derive(Debug, thiserror::Error)]
enum ImagePreparationError {
#[error("remote image URLs are not supported")]
RemoteUrlUnsupported,
#[error("image detail `low` is not supported")]
UnsupportedLowDetail,
#[error(transparent)]
@@ -33,6 +37,7 @@ enum ImagePreparationError {
impl ImagePreparationError {
fn placeholder(&self) -> &'static str {
match self {
ImagePreparationError::RemoteUrlUnsupported => REMOTE_IMAGE_URL_PLACEHOLDER,
ImagePreparationError::UnsupportedLowDetail => UNSUPPORTED_LOW_DETAIL_PLACEHOLDER,
ImagePreparationError::Processing(ImageProcessingError::ImageTooLarge { .. }) => {
IMAGE_TOO_LARGE_PLACEHOLDER
@@ -52,7 +57,8 @@ pub(crate) fn prepare_response_items(items: &mut [ResponseItem]) {
prepare_tool_output_content(content);
}
}
ResponseItem::Reasoning { .. }
ResponseItem::AdditionalTools { .. }
| ResponseItem::Reasoning { .. }
| ResponseItem::AgentMessage { .. }
| ResponseItem::LocalShellCall { .. }
| ResponseItem::FunctionCall { .. }
@@ -72,7 +78,6 @@ pub(crate) fn prepare_response_items(items: &mut [ResponseItem]) {
fn prepare_message_content(items: &mut [ContentItem]) {
for item in items {
if let ContentItem::InputImage { image_url, detail } = item
&& is_data_url(image_url)
&& let Err(error) = prepare_image(image_url, *detail)
{
warn!(%error, "failed to prepare message image");
@@ -86,7 +91,6 @@ fn prepare_message_content(items: &mut [ContentItem]) {
fn prepare_tool_output_content(items: &mut [FunctionCallOutputContentItem]) {
for item in items {
if let FunctionCallOutputContentItem::InputImage { image_url, detail } = item
&& is_data_url(image_url)
&& let Err(error) = prepare_image(image_url, *detail)
{
warn!(%error, "failed to prepare tool output image");
@@ -97,6 +101,12 @@ fn prepare_tool_output_content(items: &mut [FunctionCallOutputContentItem]) {
}
}
fn is_remote_image_url(image_url: &str) -> bool {
image_url.split_once(':').is_some_and(|(scheme, _)| {
scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https")
})
}
fn is_data_url(image_url: &str) -> bool {
image_url
.get(.."data:".len())
@@ -107,6 +117,13 @@ fn prepare_image(
image_url: &mut String,
detail: Option<ImageDetail>,
) -> Result<(), ImagePreparationError> {
if is_remote_image_url(image_url) {
return Err(ImagePreparationError::RemoteUrlUnsupported);
}
if !is_data_url(image_url) {
return Ok(());
}
let limits = match detail {
None | Some(ImageDetail::Auto | ImageDetail::High) => HIGH_DETAIL_LIMITS,
Some(ImageDetail::Original) => ORIGINAL_DETAIL_LIMITS,
+12 -12
View File
@@ -32,9 +32,8 @@ fn decoded_image(image_url: &str) -> (Vec<u8>, DynamicImage) {
}
#[test]
fn preparation_preserves_small_image_bytes_and_non_data_urls() {
fn preparation_preserves_small_image_bytes_and_replaces_remote_urls() {
let (data_url, original_bytes) = png_data_url(/*width*/ 64, /*height*/ 32);
let http_url = "https://example.com/image.png".to_string();
let mut items = vec![ResponseItem::Message {
id: None,
role: "user".to_string(),
@@ -44,12 +43,12 @@ fn preparation_preserves_small_image_bytes_and_non_data_urls() {
detail: Some(ImageDetail::High),
},
ContentItem::InputImage {
image_url: http_url.clone(),
image_url: "https://example.com/image.png".to_string(),
detail: Some(ImageDetail::Low),
},
],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
prepare_response_items(&mut items);
@@ -59,16 +58,13 @@ fn preparation_preserves_small_image_bytes_and_non_data_urls() {
};
let [
ContentItem::InputImage { image_url, .. },
ContentItem::InputImage {
image_url: preserved_http_url,
detail: Some(ImageDetail::Low),
},
ContentItem::InputText { text },
] = content.as_slice()
else {
panic!("expected two images");
};
assert_eq!(decoded_image(image_url).0, original_bytes);
assert_eq!(preserved_http_url, &http_url);
assert_eq!(text, REMOTE_IMAGE_URL_PLACEHOLDER);
}
#[test]
@@ -86,7 +82,7 @@ fn detail_policies_apply_the_expected_budgets() {
role: "user".to_string(),
content: vec![ContentItem::InputImage { image_url, detail }],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
prepare_response_items(&mut items);
@@ -133,7 +129,7 @@ fn preparation_replaces_only_failed_tool_images_and_preserves_metadata() {
]),
success: Some(true),
},
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
prepare_response_items(&mut items);
@@ -165,7 +161,7 @@ fn preparation_replaces_only_failed_tool_images_and_preserves_metadata() {
]),
success: Some(true),
},
metadata: None,
internal_chat_message_metadata_passthrough: None,
}]
);
}
@@ -173,6 +169,10 @@ fn preparation_replaces_only_failed_tool_images_and_preserves_metadata() {
#[test]
fn preparation_errors_use_bounded_actionable_placeholders() {
let cases = [
(
ImagePreparationError::RemoteUrlUnsupported,
REMOTE_IMAGE_URL_PLACEHOLDER,
),
(
ImagePreparationError::UnsupportedLowDetail,
UNSUPPORTED_LOW_DETAIL_PLACEHOLDER,
+2 -2
View File
@@ -8,8 +8,8 @@
//! and rewrite only the declared arguments into the provided-file payload
//! shape expected by the downstream Apps tool.
//!
//! Model-visible schema masking is owned by `codex-mcp` alongside MCP tool
//! inventory, so this module only handles the execution-time argument rewrite.
//! The model-facing local-path schema is owned by `codex-mcp` alongside MCP tool inventory, so this
//! module only handles uploading the files and rewriting the execution-time arguments.
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
+1 -1
View File
@@ -744,7 +744,7 @@ async fn augment_mcp_tool_request_meta_with_sandbox_state(
};
let permission_profile = turn_context.permission_profile();
let sandbox_state = serde_json::to_value(SandboxState {
permission_profile: Some(permission_profile),
permission_profile,
codex_linux_sandbox_exe: turn_context.config.codex_linux_sandbox_exe.clone(),
sandbox_cwd,
use_legacy_landlock: turn_context.config.features.use_legacy_landlock(),
+13 -4
View File
@@ -1335,15 +1335,25 @@ fn codex_apps_auth_failure_metadata() -> McpToolApprovalMetadata {
async fn install_host_owned_codex_apps_manager(session: &Session, turn_context: &TurnContext) {
let auth = session.services.auth_manager.auth().await;
let startup_cancellation_token = CancellationToken::new();
startup_cancellation_token.cancel();
let (tx_event, _rx_event) = async_channel::unbounded();
let mcp_servers = HashMap::from([(
CODEX_APPS_MCP_SERVER_NAME.to_string(),
codex_mcp::EffectiveMcpServer::configured(codex_mcp::codex_apps_mcp_server_config(
"https://chatgpt.com",
/*apps_mcp_product_sku*/ None,
)),
)]);
let manager = codex_mcp::McpConnectionManager::new(
&HashMap::new(),
&mcp_servers,
turn_context.config.mcp_oauth_credentials_store_mode,
turn_context.config.auth_keyring_backend_kind(),
HashMap::new(),
&turn_context.approval_policy,
turn_context.sub_id.clone(),
session.get_tx_event(),
CancellationToken::new(),
tx_event,
startup_cancellation_token,
turn_context.permission_profile(),
codex_mcp::McpRuntimeContext::new(
session.services.turn_environments.environment_manager(),
@@ -1354,7 +1364,6 @@ async fn install_host_owned_codex_apps_manager(session: &Session, turn_context:
),
turn_context.config.codex_home.to_path_buf(),
codex_mcp::codex_apps_tools_cache_key(auth.as_ref()),
/*host_owned_codex_apps_enabled*/ true,
turn_context.config.prefix_mcp_tool_names(),
rmcp::model::ElicitationCapability::default(),
/*supports_openai_form_elicitation*/ false,
+1 -10
View File
@@ -2,7 +2,6 @@ use std::collections::HashSet;
use codex_connectors::AppToolPolicyEvaluator;
use codex_connectors::AppToolPolicyInput;
use codex_features::Feature;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_mcp::ToolInfo as McpToolInfo;
use codex_mcp::tool_is_model_visible;
@@ -11,8 +10,6 @@ use tracing::instrument;
use crate::config::Config;
use crate::connectors;
pub(crate) const DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD: usize = 100;
pub(crate) struct McpToolExposure {
pub(crate) direct_tools: Vec<McpToolInfo>,
pub(crate) deferred_tools: Option<Vec<McpToolInfo>>,
@@ -34,13 +31,7 @@ pub(crate) fn build_mcp_tool_exposure(
));
}
let should_defer = search_tool_enabled
&& (config
.features
.enabled(Feature::ToolSearchAlwaysDeferMcpTools)
|| deferred_tools.len() >= DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD);
if !should_defer {
if !search_tool_enabled {
return McpToolExposure {
direct_tools: deferred_tools,
deferred_tools: None,
+8 -13
View File
@@ -1,7 +1,6 @@
use std::collections::HashSet;
use std::sync::Arc;
use codex_features::Feature;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_mcp::ToolInfo;
use codex_tools::ToolName;
@@ -95,12 +94,12 @@ fn with_visibility(mut tool: ToolInfo, visibility: &[&str]) -> ToolInfo {
}
#[tokio::test]
async fn directly_exposes_small_effective_tool_sets() {
async fn directly_exposes_effective_tool_sets_when_search_is_unavailable() {
let config = test_config().await;
let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1);
let mcp_tools = numbered_mcp_tools(/*count*/ 2);
let exposure = build_mcp_tool_exposure(
&mcp_tools, /*connectors*/ None, &config, /*search_tool_enabled*/ true,
&mcp_tools, /*connectors*/ None, &config, /*search_tool_enabled*/ false,
);
assert_eq!(tool_names(&exposure.direct_tools), tool_names(&mcp_tools));
@@ -237,9 +236,9 @@ enabled = true
}
#[tokio::test]
async fn searches_large_effective_tool_sets() {
async fn defers_effective_tool_sets_when_search_is_available() {
let config = test_config().await;
let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD);
let mcp_tools = numbered_mcp_tools(/*count*/ 2);
let exposure = build_mcp_tool_exposure(
&mcp_tools, /*connectors*/ None, &config, /*search_tool_enabled*/ true,
@@ -249,17 +248,13 @@ async fn searches_large_effective_tool_sets() {
let deferred_tools = exposure
.deferred_tools
.as_ref()
.expect("large tool sets should be discoverable through tool_search");
.expect("MCP tools should be discoverable through tool_search");
assert_eq!(tool_names(deferred_tools), tool_names(&mcp_tools));
}
#[tokio::test]
async fn always_defer_feature_defers_apps_too() {
let mut config = test_config().await;
config
.features
.enable(Feature::ToolSearchAlwaysDeferMcpTools)
.expect("test config should allow feature update");
async fn defers_apps_and_non_app_mcp_tools() {
let config = test_config().await;
let mcp_tools = vec![
make_mcp_tool(
"rmcp",
@@ -43,6 +43,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R
let session_meta = SessionMetaLine {
meta: SessionMeta {
session_id: thread_id.into(),
id: thread_id,
forked_from_id: None,
parent_thread_id: None,
+1 -1
View File
@@ -81,7 +81,7 @@ pub(crate) async fn build_prompt_input_from_session(
.await;
if !input.is_empty() {
let response_item = sess.response_item_from_user_input(turn_context.as_ref(), input);
let response_item = sess.response_item_from_user_input(input);
sess.record_conversation_items(turn_context.as_ref(), std::slice::from_ref(&response_item))
.await;
}
+1 -1
View File
@@ -78,7 +78,7 @@ fn message(role: &str, content: ContentItem) -> ResponseItem {
role: role.to_string(),
content: vec![content],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
+8
View File
@@ -10,6 +10,7 @@ use codex_protocol::ThreadId;
use codex_protocol::protocol::InternalSessionSource;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_protocol::protocol::ThreadSource;
use codex_utils_string::to_ascii_json_string;
use http::HeaderMap as ApiHeaderMap;
use http::HeaderValue;
@@ -34,6 +35,7 @@ pub(crate) const TURN_STARTED_AT_UNIX_MS_KEY: &str = "turn_started_at_unix_ms";
pub(crate) const FORKED_FROM_THREAD_ID_KEY: &str = "forked_from_thread_id";
pub(crate) const PARENT_THREAD_ID_KEY: &str = "parent_thread_id";
pub(crate) const SUBAGENT_KIND_KEY: &str = "subagent_kind";
pub(crate) const THREAD_SOURCE_KEY: &str = "thread_source";
pub(crate) const SANDBOX_KEY: &str = "sandbox";
pub(crate) const WORKSPACES_KEY: &str = "workspaces";
@@ -56,6 +58,7 @@ const RESERVED_METADATA_KEYS: &[&str] = &[
FORKED_FROM_THREAD_ID_KEY,
PARENT_THREAD_ID_KEY,
SUBAGENT_KIND_KEY,
THREAD_SOURCE_KEY,
SANDBOX_KEY,
WORKSPACES_KEY,
];
@@ -143,6 +146,7 @@ pub struct CodexResponsesMetadata {
pub(crate) parent_thread_id: Option<ThreadId>,
pub(crate) subagent_header: Option<String>,
pub(crate) subagent_kind: Option<String>,
pub(crate) thread_source: Option<ThreadSource>,
pub(crate) sandbox: Option<String>,
pub(crate) workspaces: BTreeMap<String, TurnMetadataWorkspace>,
pub(crate) turn_started_at_unix_ms: Option<i64>,
@@ -167,6 +171,7 @@ impl CodexResponsesMetadata {
parent_thread_id: None,
subagent_header: None,
subagent_kind: None,
thread_source: None,
sandbox: None,
workspaces: BTreeMap::new(),
turn_started_at_unix_ms: None,
@@ -268,6 +273,7 @@ impl CodexResponsesMetadata {
forked_from_thread_id: self.forked_from_thread_id,
parent_thread_id: self.parent_thread_id,
subagent_kind: self.subagent_kind.as_deref(),
thread_source: self.thread_source.as_ref(),
sandbox: self.sandbox.as_deref(),
workspaces: non_empty_workspaces(&self.workspaces),
turn_started_at_unix_ms: self.turn_started_at_unix_ms,
@@ -354,6 +360,8 @@ struct CodexTurnMetadataPayload<'a> {
#[serde(default, skip_serializing_if = "Option::is_none")]
subagent_kind: Option<&'a str>,
#[serde(default, skip_serializing_if = "Option::is_none")]
thread_source: Option<&'a ThreadSource>,
#[serde(default, skip_serializing_if = "Option::is_none")]
sandbox: Option<&'a str>,
#[serde(default, skip_serializing_if = "Option::is_none")]
workspaces: Option<&'a BTreeMap<String, TurnMetadataWorkspace>>,
+9 -5
View File
@@ -57,18 +57,22 @@ impl RolloutBudget {
window_id: &str,
) -> Option<RolloutBudgetReminder> {
let state = self.lock()?;
let reminder_index = (state.weighted_tokens_used
/ state.config.reminder_interval_tokens as f64)
let remaining_tokens = (state.config.limit_tokens as f64 - state.weighted_tokens_used)
.max(0.0)
.floor() as i64;
let reminder_index = state
.config
.reminder_at_remaining_tokens
.iter()
.filter(|&&threshold| remaining_tokens <= threshold)
.count() as i64;
if state.deliveries.get(&thread_id).is_some_and(|delivery| {
delivery.window_id.as_str() == window_id && delivery.reminder_index >= reminder_index
}) {
return None;
}
Some(RolloutBudgetReminder {
remaining_tokens: (state.config.limit_tokens as f64 - state.weighted_tokens_used)
.max(0.0)
.floor() as i64,
remaining_tokens,
reminder_index,
})
}
+7
View File
@@ -14,6 +14,7 @@ use crate::exec::execute_exec_request;
#[cfg(target_os = "macos")]
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use codex_file_system::FileSystemSandboxContext;
use codex_network_proxy::NetworkProxy;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::exec_output::ExecToolCallOutput;
@@ -60,6 +61,8 @@ pub struct ExecRequest {
pub network_sandbox_policy: NetworkSandboxPolicy,
pub(crate) windows_sandbox_filesystem_overrides: Option<WindowsSandboxFilesystemOverrides>,
pub arg0: Option<String>,
pub(crate) exec_server_sandbox: Option<FileSystemSandboxContext>,
pub(crate) exec_server_enforce_managed_network: bool,
}
impl ExecRequest {
@@ -102,6 +105,8 @@ impl ExecRequest {
network_sandbox_policy,
windows_sandbox_filesystem_overrides: None,
arg0,
exec_server_sandbox: None,
exec_server_enforce_managed_network: false,
}
}
@@ -158,6 +163,8 @@ impl ExecRequest {
network_sandbox_policy,
windows_sandbox_filesystem_overrides: None,
arg0,
exec_server_sandbox: None,
exec_server_enforce_managed_network: false,
}
}
}
@@ -0,0 +1,26 @@
use codex_features::Feature;
use codex_features::Features;
use codex_protocol::openai_models::ModelInfo;
pub(super) fn unsupported_code_mode_warning(
model_info: &ModelInfo,
features: &Features,
) -> Option<String> {
let code_mode_enabled =
features.enabled(Feature::CodeMode) || features.enabled(Feature::CodeModeOnly);
if !code_mode_enabled
|| model_info.tool_mode.is_some()
|| model_info.used_fallback_model_metadata
{
return None;
}
let model = &model_info.slug;
Some(format!(
"Code Mode is enabled in configuration, but model `{model}` does not advertise Code Mode support. This may degrade model performance. Disable `features.code_mode` and `features.code_mode_only`, or select a model whose metadata enables Code Mode."
))
}
#[cfg(test)]
#[path = "code_mode_warning_tests.rs"]
mod tests;
@@ -0,0 +1,70 @@
use super::unsupported_code_mode_warning;
use codex_features::Feature;
use codex_features::Features;
use codex_models_manager::model_info::model_info_from_slug;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ToolMode;
use pretty_assertions::assert_eq;
const MODEL_SLUG: &str = "test-model";
fn known_model_info() -> ModelInfo {
ModelInfo {
used_fallback_model_metadata: false,
..model_info_from_slug(MODEL_SLUG)
}
}
#[test]
fn warns_when_code_mode_is_enabled_without_model_selector() {
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);
assert_eq!(
unsupported_code_mode_warning(&known_model_info(), &features),
Some(format!(
"Code Mode is enabled in configuration, but model `{MODEL_SLUG}` does not advertise Code Mode support. This may degrade model performance. Disable `features.code_mode` and `features.code_mode_only`, or select a model whose metadata enables Code Mode."
))
);
}
#[test]
fn warns_when_code_mode_only_is_enabled_without_model_selector() {
let mut features = Features::with_defaults();
features.enable(Feature::CodeModeOnly);
assert!(unsupported_code_mode_warning(&known_model_info(), &features).is_some());
}
#[test]
fn does_not_warn_when_code_mode_is_disabled() {
assert_eq!(
unsupported_code_mode_warning(&known_model_info(), &Features::with_defaults()),
None
);
}
#[test]
fn does_not_warn_when_model_has_tool_mode_selector() {
let mut features = Features::with_defaults();
features.enable(Feature::CodeModeOnly);
for tool_mode in [ToolMode::Direct, ToolMode::CodeMode, ToolMode::CodeModeOnly] {
let model_info = ModelInfo {
tool_mode: Some(tool_mode),
..known_model_info()
};
assert_eq!(unsupported_code_mode_warning(&model_info, &features), None);
}
}
#[test]
fn fallback_metadata_only_uses_existing_warning() {
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);
assert_eq!(
unsupported_code_mode_warning(&model_info_from_slug(MODEL_SLUG), &features),
None
);
}
+28 -3
View File
@@ -10,6 +10,7 @@ use codex_features::FeatureToml;
use codex_features::FeaturesToml;
use codex_features::MultiAgentV2ConfigToml;
use codex_features::RolloutBudgetConfigToml;
use codex_features::TokenBudgetConfigToml;
use codex_protocol::ThreadId;
use crate::config::Config;
@@ -152,6 +153,12 @@ fn save_config_resolved_fields(
resolved_config_to_toml(&config.multi_agent_v2, "features.multi_agent_v2")?;
multi_agent_v2.enabled = Some(config.features.enabled(Feature::MultiAgentV2));
features.multi_agent_v2 = Some(FeatureToml::Config(multi_agent_v2));
if let Some(token_budget) = config.token_budget.as_ref() {
let mut token_budget: TokenBudgetConfigToml =
resolved_config_to_toml(token_budget, "features.token_budget")?;
token_budget.enabled = Some(config.features.enabled(Feature::TokenBudget));
features.token_budget = Some(FeatureToml::Config(token_budget));
}
if let Some(rollout_budget) = config.rollout_budget.as_ref() {
let mut rollout_budget: RolloutBudgetConfigToml =
resolved_config_to_toml(rollout_budget, "features.rollout_budget")?;
@@ -238,9 +245,17 @@ mod tests {
async fn lock_contains_prompts_and_materializes_features() {
let mut sc = crate::session::tests::make_session_configuration_for_tests().await;
let mut config = (*sc.original_config_do_not_use).clone();
config.token_budget = Some(crate::config::TokenBudgetConfig {
reminder_threshold_tokens: Some(16_000),
reminder_message_template: "Locked reminder: {n_remaining} tokens.".to_string(),
});
config
.features
.enable(Feature::TokenBudget)
.expect("token_budget should be enableable in tests");
config.rollout_budget = Some(crate::config::RolloutBudgetConfig {
limit_tokens: 100_000,
reminder_interval_tokens: 10_000,
reminder_at_remaining_tokens: vec![50_000, 25_000, 10_000],
sampling_token_weight: 1.0,
prefill_token_weight: 0.25,
});
@@ -312,18 +327,28 @@ mod tests {
min_wait_timeout_ms: Some(_),
max_wait_timeout_ms: Some(_),
default_wait_timeout_ms: Some(_),
usage_hint_enabled: Some(_),
hide_spawn_agent_metadata: Some(_),
..
})
));
assert_eq!(
features.token_budget,
Some(FeatureToml::Config(TokenBudgetConfigToml {
enabled: Some(true),
reminder_threshold_tokens: Some(16_000),
reminder_message_template: Some(
"Locked reminder: {n_remaining} tokens.".to_string()
),
}))
);
assert_eq!(
features.rollout_budget,
Some(FeatureToml::Config(RolloutBudgetConfigToml {
enabled: Some(true),
limit_tokens: Some(100_000),
reminder_interval_tokens: Some(10_000),
reminder_at_remaining_tokens: Some(vec![50_000, 25_000, 10_000]),
sampling_token_weight: Some(1.0),
prefill_token_weight: Some(0.25),
}))
+2 -2
View File
@@ -218,7 +218,7 @@ pub(super) async fn user_input_or_turn_inner(
})
.await;
}
sess.maybe_emit_unknown_model_warning_for_turn(current_context.as_ref())
sess.maybe_emit_model_warnings_for_turn(current_context.as_ref())
.await;
match sess
.steer_input(
@@ -673,7 +673,7 @@ pub async fn review(
review_request: ReviewRequest,
) {
let turn_context = sess.new_default_turn_with_sub_id(sub_id.clone()).await;
sess.maybe_emit_unknown_model_warning_for_turn(turn_context.as_ref())
sess.maybe_emit_model_warnings_for_turn(turn_context.as_ref())
.await;
sess.refresh_mcp_servers_if_requested(&turn_context, Some(sess.mcp_elicitation_reviewer()))
.await;
+1 -1
View File
@@ -94,7 +94,7 @@ impl Session {
input,
));
}
self.maybe_emit_unknown_model_warning_for_turn(turn_context.as_ref())
self.maybe_emit_model_warnings_for_turn(turn_context.as_ref())
.await;
if self.input_queue.has_trigger_turn_mailbox_items().await {
self.clear_reserved_idle_turn(&turn_state).await;
-3
View File
@@ -317,8 +317,6 @@ impl Session {
let tool_plugin_provenance = codex_mcp::tool_plugin_provenance(&mcp_config);
let mcp_servers =
effective_mcp_servers_from_configured(mcp_servers, &mcp_config, auth.as_ref());
let host_owned_codex_apps_enabled =
host_owned_codex_apps_enabled(&mcp_config, auth.as_ref());
let auth_statuses = compute_auth_statuses(
mcp_servers.iter(),
store_mode,
@@ -359,7 +357,6 @@ impl Session {
mcp_runtime_context,
config.codex_home.to_path_buf(),
codex_apps_tools_cache_key(auth.as_ref()),
host_owned_codex_apps_enabled,
mcp_config.prefix_mcp_tool_names,
mcp_config.client_elicitation_capability,
self.services
+191 -86
View File
@@ -32,6 +32,7 @@ use crate::context::NetworkRuleSaved;
use crate::context::PermissionsInstructions;
use crate::context::PersonalitySpecInstructions;
use crate::context::RecommendedPluginsInstructions;
use crate::context::world_state::WorldState;
use crate::current_time::TimeProvider;
use crate::default_skill_metadata_budget;
use crate::environment_selection::TurnEnvironmentSnapshot;
@@ -207,6 +208,7 @@ use codex_protocol::error::Result as CodexResult;
#[cfg(test)]
use codex_protocol::exec_output::StreamOutput;
mod code_mode_warning;
mod config_lock;
mod handlers;
mod inject;
@@ -218,10 +220,13 @@ mod rollout_budget;
mod rollout_reconstruction;
#[allow(clippy::module_inception)]
pub(crate) mod session;
pub(crate) mod step_context;
pub(crate) mod time_reminder;
mod token_budget;
pub(crate) mod turn;
pub(crate) mod turn_context;
mod world_state;
use self::code_mode_warning::unsupported_code_mode_warning;
use self::config_lock::export_config_lock_if_configured;
use self::config_lock::validate_config_lock_if_configured;
#[cfg(test)]
@@ -242,6 +247,9 @@ use self::turn::collect_explicit_app_ids_from_skill_items;
use self::turn::realtime_text_for_event;
use self::turn_context::TurnContext;
use self::turn_context::TurnSkillsContext;
use self::world_state::build_world_state_from_environment_snapshot;
use self::world_state::build_world_state_from_turn_context;
use self::world_state::build_world_state_from_turn_context_item;
#[cfg(test)]
mod rollout_reconstruction_tests;
@@ -311,6 +319,7 @@ use crate::session_startup_prewarm::SessionStartupPrewarmHandle;
use crate::shell;
#[cfg(test)]
use crate::skills::SkillLoadOutcome;
use crate::state::AutoCompactWindowIds;
use crate::state::AutoCompactWindowSnapshot;
use crate::state::PendingRequestPermissions;
use crate::state::SessionServices;
@@ -336,7 +345,6 @@ use codex_git_utils::get_git_repo_root;
use codex_mcp::McpConfig;
use codex_mcp::compute_auth_statuses;
use codex_mcp::effective_mcp_servers_from_configured;
use codex_mcp::host_owned_codex_apps_enabled;
use codex_otel::SessionTelemetry;
use codex_otel::THREAD_STARTED_METRIC;
use codex_otel::TelemetryAuthMode;
@@ -593,7 +601,7 @@ impl Codex {
.await;
let multi_agent_version =
resolve_multi_agent_version(&conversation_history, inherited_multi_agent_version);
let multi_agent_mode = initial_multi_agent_mode;
let multi_agent_mode = initial_multi_agent_mode.unwrap_or_default();
config
.validate_multi_agent_v2_config()
.map_err(|err| CodexErr::InvalidRequest(err.to_string()))?;
@@ -1387,26 +1395,36 @@ impl Session {
previous_turn_settings,
reference_context_item,
window_number,
first_window_id,
previous_window_id,
window_id,
} = self
.reconstruct_history_from_rollout(turn_context, rollout_items)
.await;
if turn_context
.config
.features
.enabled(Feature::ResizeAllImages)
{
// Keep the recorded rollout unchanged. Prepare its reconstructed history before
// installing it, so legacy images are processed once for this resume or fork and
// will be processed again if the rollout is reconstructed in a future session.
// This meets image resizing requirements without modifying persisted rollouts.
prepare_response_items(&mut history);
}
// Keep the recorded rollout unchanged. Prepare its reconstructed history before
// installing it, so legacy images are processed once for this resume or fork and
// will be processed again if the rollout is reconstructed in a future session.
// This meets image resizing requirements without modifying persisted rollouts.
prepare_response_items(&mut history);
let world_state_baseline = reference_context_item
.as_ref()
.map(build_world_state_from_turn_context_item);
{
let mut state = self.state.lock().await;
state.replace_history(history, reference_context_item);
let window_id = window_id.unwrap_or_else(|| state.auto_compact_window_id());
state.restore_auto_compact_window(window_number, window_id);
if let Some(world_state) = world_state_baseline {
state.history.set_world_state_baseline(world_state);
}
let fallback_ids = state.auto_compact_window_ids();
let window_id = window_id.unwrap_or(fallback_ids.window_id);
state.restore_auto_compact_window(
window_number,
AutoCompactWindowIds {
first_window_id: first_window_id.unwrap_or(window_id),
previous_window_id,
window_id,
},
);
state.set_previous_turn_settings(previous_turn_settings.clone());
}
let prefix_tokens = if matches!(
@@ -1691,19 +1709,17 @@ impl Session {
) -> Vec<ResponseItem> {
// TODO: Make context updates a pure diff of persisted previous/current TurnContextItem
// state so replay/backtracking is deterministic. Runtime inputs that affect model-visible
// context (shell, exec policy, feature gates, previous-turn bridge) should be persisted
// context (exec policy, feature gates, previous-turn bridge) should be persisted
// state or explicit non-state replay events.
let previous_turn_settings = {
let state = self.state.lock().await;
state.previous_turn_settings()
};
let shell = self.user_shell();
let exec_policy = self.services.exec_policy.current();
crate::context_manager::updates::build_settings_update_items(
reference_context_item,
previous_turn_settings.as_ref(),
current_context,
shell.as_ref(),
exec_policy.as_ref(),
self.features.enabled(Feature::Personality),
)
@@ -2700,12 +2716,10 @@ impl Session {
items: &'a [ResponseItem],
) -> Cow<'a, [ResponseItem]> {
let mut items = Cow::Borrowed(items);
if turn_context
.config
.features
.enabled(Feature::ResizeAllImages)
{
prepare_response_items(items.to_mut());
prepare_response_items(items.to_mut());
// Most response items get their passthrough turn ID at the durable history boundary.
for item in items.to_mut() {
item.set_turn_id_if_missing(&turn_context.sub_id);
}
if turn_context.config.features.enabled(Feature::ItemIds) {
Self::assign_missing_response_item_ids(items)
@@ -2724,6 +2738,7 @@ impl Session {
continue;
}
let prefix = match item {
ResponseItem::AdditionalTools { .. } => "at",
ResponseItem::Message { .. } => "msg",
ResponseItem::Reasoning { .. } => "rs",
ResponseItem::LocalShellCall { .. } => "lsh",
@@ -2745,23 +2760,10 @@ impl Session {
items
}
pub(crate) fn response_item_from_user_input(
&self,
turn_context: &TurnContext,
input: Vec<UserInput>,
) -> ResponseItem {
let local_image_preparation = if turn_context
.config
.features
.enabled(Feature::ResizeAllImages)
{
LocalImagePreparation::Defer
} else {
LocalImagePreparation::Process
};
pub(crate) fn response_item_from_user_input(&self, input: Vec<UserInput>) -> ResponseItem {
ResponseItem::from(ResponseInputItem::from_user_input(
input,
local_image_preparation,
LocalImagePreparation::Defer,
))
}
@@ -2784,11 +2786,36 @@ impl Session {
self.send_raw_response_items(turn_context, items).await;
}
pub(crate) async fn record_step_environment_context_if_changed(
&self,
turn_context: &TurnContext,
step_context: &step_context::StepContext,
) {
if !turn_context.config.include_environment_context {
return;
}
let world_state =
build_world_state_from_environment_snapshot(turn_context, &step_context.environments);
let items = {
let mut state = self.state.lock().await;
crate::context_manager::updates::merge_contextual_fragments(
state.history.update_world_state(world_state),
)
};
if items.is_empty() {
return;
}
self.record_conversation_items(turn_context, &items).await;
}
pub(crate) async fn record_inter_agent_communication(
&self,
turn_context: &TurnContext,
communication: InterAgentCommunication,
mut communication: InterAgentCommunication,
) {
communication.set_turn_id_if_missing(&turn_context.sub_id);
let response_item = communication.to_model_input_item();
let items = self.prepare_conversation_items_for_history(
turn_context,
@@ -2893,9 +2920,17 @@ impl Session {
replacement_history: Some(items.clone()),
..compacted_item
};
let world_state_baseline = if reference_context_item.is_some() {
Some(self.build_world_state(turn_context).await)
} else {
None
};
{
let mut state = self.state.lock().await;
state.replace_history(items, reference_context_item.clone());
if let Some(world_state) = world_state_baseline {
state.history.set_world_state_baseline(world_state);
}
}
self.persist_rollout_items(&[RolloutItem::Compacted(compacted_item)])
@@ -3024,6 +3059,28 @@ impl Session {
pub(crate) async fn build_initial_context(
&self,
turn_context: &TurnContext,
) -> Vec<ResponseItem> {
let world_state = self.build_world_state(turn_context).await;
self.build_initial_context_with_world_state(turn_context, &world_state)
.await
}
async fn build_world_state(&self, turn_context: &TurnContext) -> WorldState {
let environment_subagents = if turn_context.config.include_environment_context {
self.services
.agent_control
.format_environment_context_subagents(self.thread_id)
.await
} else {
String::new()
};
build_world_state_from_turn_context(turn_context, &environment_subagents)
}
async fn build_initial_context_with_world_state(
&self,
turn_context: &TurnContext,
world_state: &WorldState,
) -> Vec<ResponseItem> {
let mut developer_sections = Vec::<String>::with_capacity(8);
let mut contextual_user_sections = Vec::<String>::with_capacity(2);
@@ -3034,7 +3091,7 @@ impl Session {
collaboration_mode,
base_instructions,
session_source,
auto_compact_window_id,
auto_compact_window_ids,
) = {
let state = self.state.lock().await;
(
@@ -3043,7 +3100,7 @@ impl Session {
state.session_configuration.collaboration_mode.clone(),
state.session_configuration.base_instructions.clone(),
state.session_configuration.session_source.clone(),
state.auto_compact_window_id(),
state.auto_compact_window_ids(),
)
};
if let Some(model_switch_message) =
@@ -3228,29 +3285,48 @@ impl Session {
}
// This is full-context metadata. Steady-state context diffs should not re-emit it.
if turn_context.config.features.enabled(Feature::TokenBudget)
&& let Some(model_context_window) = turn_context.model_context_window()
&& turn_context.model_context_window().is_some()
{
let mcp_result = self
.call_tool(
"notes",
"thread_hint",
/*arguments*/ None,
Some(serde_json::json!({
"threadId": self.thread_id().to_string(),
})),
)
.await
.ok()
.and_then(|result| {
let text = result
.content
.iter()
.filter_map(|content| {
content.get("text").and_then(serde_json::Value::as_str)
})
.filter(|text| !text.is_empty())
.collect::<Vec<_>>()
.join("\n");
(!text.is_empty()).then_some(text)
});
developer_sections.push(
crate::context::TokenBudgetContext::new(
self.thread_id(),
auto_compact_window_id,
model_context_window,
auto_compact_window_ids.first_window_id,
auto_compact_window_ids.previous_window_id,
auto_compact_window_ids.window_id,
mcp_result,
)
.render(),
);
}
if turn_context.config.include_environment_context {
let shell = self.user_shell();
let subagents = self
.services
.agent_control
.format_environment_context_subagents(self.thread_id)
.await;
contextual_user_sections.push(
crate::context::EnvironmentContext::from_turn_context(turn_context, shell.as_ref())
.with_subagents(subagents)
.render(),
);
for fragment in world_state.render_full() {
match fragment.role() {
"developer" => developer_sections.push(fragment.render()),
"user" => contextual_user_sections.push(fragment.render()),
_ => {}
}
}
let multi_agent_v2_usage_hint_text =
@@ -3277,19 +3353,20 @@ impl Session {
{
items.push(usage_hint_message);
}
if let Some(multi_agent_mode) = multi_agents::effective_multi_agent_mode(
match multi_agents::effective_multi_agent_mode(
turn_context.multi_agent_version,
&turn_context.config.multi_agent_v2,
&session_source,
turn_context.multi_agent_mode,
turn_context
.config
.features
.enabled(Feature::MultiAgentMode),
) {
items.push(ContextualUserFragment::into(
MultiAgentModeInstructions::new(multi_agent_mode),
));
Some(
multi_agent_mode
@ (MultiAgentMode::ExplicitRequestOnly | MultiAgentMode::Proactive),
) => {
items.push(ContextualUserFragment::into(
MultiAgentModeInstructions::new(multi_agent_mode),
));
}
Some(MultiAgentMode::None) | None => {}
}
if let Some(contextual_user_message) =
crate::context_manager::updates::build_contextual_user_message(contextual_user_sections)
@@ -3308,6 +3385,10 @@ impl Session {
{
items.push(guardian_developer_message);
}
// New context windows and compaction install these items directly into replacement history.
for item in &mut items {
item.set_turn_id_if_missing(&turn_context.sub_id);
}
items
}
@@ -3332,10 +3413,9 @@ impl Session {
format!("{thread_id}:{window_number}")
}
pub(crate) async fn advance_auto_compact_window(&self) -> (u64, String) {
pub(crate) async fn advance_auto_compact_window(&self) -> (u64, AutoCompactWindowIds) {
let mut state = self.state.lock().await;
let (window_number, window_id) = state.advance_auto_compact_window();
(window_number, window_id.to_string())
state.advance_auto_compact_window()
}
pub(crate) async fn request_new_context_window(&self) {
@@ -3351,20 +3431,26 @@ impl Session {
let mut state = self.state.lock().await;
state.start_new_context_window_if_requested()
};
let (window_number, window_id) = window?;
let context_items = self.build_initial_context(turn_context).await;
let (window_number, window_ids) = window?;
let world_state = self.build_world_state(turn_context).await;
let context_items = self
.build_initial_context_with_world_state(turn_context, &world_state)
.await;
let turn_context_item = turn_context.to_turn_context_item();
let replacement_history = context_items;
{
let mut state = self.state.lock().await;
state.replace_history(replacement_history.clone(), Some(turn_context_item.clone()));
state.history.set_world_state_baseline(world_state);
};
self.persist_rollout_items(&[
RolloutItem::Compacted(CompactedItem {
message: String::new(),
replacement_history: Some(replacement_history),
window_number: Some(window_number),
window_id: Some(window_id.to_string()),
first_window_id: Some(window_ids.first_window_id.to_string()),
previous_window_id: window_ids.previous_window_id.map(|id| id.to_string()),
window_id: Some(window_ids.window_id.to_string()),
}),
RolloutItem::TurnContext(turn_context_item),
])
@@ -3386,15 +3472,15 @@ impl Session {
/// steady-state turns that emit model-visible context updates.
///
/// When the reference snapshot is missing, this injects full initial context. Otherwise, it
/// emits only settings diff items.
/// emits only context diffs.
///
/// If full context is injected and a model switch occurred, this prepends the
/// `<model_switch>` developer message so model-specific instructions are not lost.
///
/// This is the normal runtime path that establishes a new `reference_context_item`.
/// Mid-turn compaction is the other path that can re-establish that baseline when it
/// reinjects full initial context into replacement history. Other non-regular tasks
/// intentionally do not update the baseline.
/// Mid-turn compaction is the other path that can re-establish that reference when it
/// reinjects full initial context into replacement history. Live world-state changes may
/// independently advance their in-memory baseline within a turn.
#[instrument(level = "trace", skip_all)]
pub(crate) async fn record_context_updates_and_set_reference_context_item(
&self,
@@ -3405,24 +3491,43 @@ impl Session {
state.reference_context_item()
};
let turn_context_item = turn_context.to_turn_context_item();
if reference_context_item.as_ref() == Some(&turn_context_item) {
return;
}
let turn_context_changed = reference_context_item.as_ref() != Some(&turn_context_item);
let should_inject_full_context = reference_context_item.is_none();
let world_state = self.build_world_state(turn_context).await;
let mut context_items = if should_inject_full_context {
self.build_initial_context(turn_context).await
let context_items = self
.build_initial_context_with_world_state(turn_context, &world_state)
.await;
self.state
.lock()
.await
.history
.set_world_state_baseline(world_state);
context_items
} else {
// Steady-state path: append only built-in context diffs here; turn-scoped extension
// context is added below.
self.build_settings_update_items(reference_context_item.as_ref(), turn_context)
.await
let mut context_items = self
.build_settings_update_items(reference_context_item.as_ref(), turn_context)
.await;
let world_state_items = {
let mut state = self.state.lock().await;
crate::context_manager::updates::merge_contextual_fragments(
state.history.update_world_state(world_state),
)
};
context_items.extend(world_state_items);
context_items
};
if !should_inject_full_context {
if !should_inject_full_context && turn_context_changed {
context_items.extend(
self.build_turn_context_contribution_items(turn_context)
.await,
);
}
if !turn_context_changed && context_items.is_empty() {
return;
}
if !context_items.is_empty() {
self.record_conversation_items(turn_context, &context_items)
.await;
@@ -3432,8 +3537,8 @@ impl Session {
self.persist_rollout_items(&[RolloutItem::TurnContext(turn_context_item.clone())])
.await;
// Advance the in-memory diff baseline even when this turn emitted no model-visible
// context items. This keeps later runtime diffing aligned with the current turn state.
// Advance the persisted-settings baseline even when this turn emitted no model-visible
// context items.
let mut state = self.state.lock().await;
state.set_reference_context_item(Some(turn_context_item));
}
@@ -3600,7 +3705,7 @@ impl Session {
// Persist the user message to history, but emit the turn item from `UserInput` so
// UI-only `text_elements` are preserved. `ResponseItem::Message` does not carry
// those spans, and `record_response_item_and_emit_turn_item` would drop them.
let response_item = self.response_item_from_user_input(turn_context, input.to_vec());
let response_item = self.response_item_from_user_input(input.to_vec());
self.record_conversation_items(turn_context, std::slice::from_ref(&response_item))
.await;
let mut user_message_item = UserMessageItem::new(input);
+12 -23
View File
@@ -14,10 +14,6 @@ pub(super) fn usage_hint_text<'a>(
}
let multi_agent_v2 = &turn_context.config.multi_agent_v2;
if !multi_agent_v2.usage_hint_enabled {
return None;
}
configured_usage_hint_text_for_source(multi_agent_v2, session_source)
}
@@ -39,30 +35,23 @@ fn configured_usage_hint_text_for_source<'a>(
}
}
fn multi_agent_mode_is_applicable(
multi_agent_version: MultiAgentVersion,
multi_agent_v2: &MultiAgentV2Config,
session_source: &SessionSource,
) -> bool {
multi_agent_version == MultiAgentVersion::V2
&& multi_agent_v2.usage_hint_enabled
&& configured_usage_hint_text_for_source(multi_agent_v2, session_source).is_some()
}
pub(crate) fn effective_multi_agent_mode(
multi_agent_version: MultiAgentVersion,
multi_agent_v2: &MultiAgentV2Config,
session_source: &SessionSource,
requested_multi_agent_mode: Option<MultiAgentMode>,
multi_agent_mode_enabled: bool,
multi_agent_mode: MultiAgentMode,
) -> Option<MultiAgentMode> {
if !multi_agent_mode_is_applicable(multi_agent_version, multi_agent_v2, session_source) {
if multi_agent_version != MultiAgentVersion::V2 {
return None;
}
Some(if multi_agent_mode_enabled {
requested_multi_agent_mode.unwrap_or_default()
} else {
MultiAgentMode::ExplicitRequestOnly
})
match session_source {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. })
| SessionSource::Cli
| SessionSource::VSCode
| SessionSource::Exec
| SessionSource::Mcp
| SessionSource::Custom(_)
| SessionSource::Unknown => Some(multi_agent_mode),
SessionSource::Internal(_) | SessionSource::SubAgent(_) => None,
}
}
+6 -2
View File
@@ -75,9 +75,12 @@ pub(super) async fn spawn_review_thread(
.model_reasoning_summary
.unwrap_or(model_info.default_reasoning_summary);
let session_source = parent_turn_context.session_source.clone();
let forked_from_thread_id = {
let (forked_from_thread_id, thread_source) = {
let state = sess.state.lock().await;
state.session_configuration.forked_from_thread_id
(
state.session_configuration.forked_from_thread_id,
state.session_configuration.thread_source.clone(),
)
};
let per_turn_config = Arc::new(per_turn_config);
@@ -88,6 +91,7 @@ pub(super) async fn spawn_review_thread(
forked_from_thread_id,
parent_turn_context.parent_thread_id,
&session_source,
thread_source,
review_turn_id.clone(),
#[allow(deprecated)]
parent_turn_context.cwd.clone(),
@@ -10,12 +10,16 @@ pub(super) struct RolloutReconstruction {
pub(super) previous_turn_settings: Option<PreviousTurnSettings>,
pub(super) reference_context_item: Option<TurnContextItem>,
pub(super) window_number: u64,
pub(super) first_window_id: Option<Uuid>,
pub(super) previous_window_id: Option<Uuid>,
pub(super) window_id: Option<Uuid>,
}
#[derive(Debug, Clone, Copy)]
struct ReconstructedWindow {
number: u64,
first_id: Option<Uuid>,
previous_id: Option<Uuid>,
id: Option<Uuid>,
}
@@ -133,6 +137,11 @@ impl Session {
{
active_segment.window = Some(ReconstructedWindow {
number: window_number,
first_id: compacted.first_window_id.as_deref().and_then(parse_uuid_v7),
previous_id: compacted
.previous_window_id
.as_deref()
.and_then(parse_uuid_v7),
id: compacted.window_id.as_deref().and_then(parse_uuid_v7),
});
}
@@ -341,13 +350,17 @@ impl Session {
let window = window.unwrap_or(ReconstructedWindow {
number: fallback_window_number,
first_id: None,
previous_id: None,
id: None,
});
RolloutReconstruction {
history: history.raw_items().to_vec(),
history: history.into_raw_items(),
previous_turn_settings,
reference_context_item,
window_number: window.number,
first_window_id: window.first_id,
previous_window_id: window.previous_id,
window_id: window.id,
}
}
@@ -20,7 +20,7 @@ fn user_message(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -32,7 +32,7 @@ fn assistant_message(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -51,7 +51,7 @@ fn inter_agent_assistant_message(text: &str) -> ResponseItem {
text: serde_json::to_string(&communication).unwrap(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -69,7 +69,9 @@ async fn record_initial_history_reconstructs_typed_inter_agent_message() {
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: vec![RolloutItem::InterAgentCommunication(communication.clone())],
history: Arc::new(vec![RolloutItem::InterAgentCommunication(
communication.clone(),
)]),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -112,7 +114,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -189,7 +191,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -757,7 +759,7 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() {
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -829,6 +831,8 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp
message: String::new(),
replacement_history: Some(Vec::new()),
window_number: None,
first_window_id: None,
previous_window_id: None,
window_id: None,
}),
RolloutItem::EventMsg(EventMsg::ThreadRolledBack(
@@ -839,7 +843,7 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -869,7 +873,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_seed_referenc
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -887,6 +891,8 @@ async fn record_initial_history_resumed_does_not_seed_reference_context_item_aft
message: String::new(),
replacement_history: Some(Vec::new()),
window_number: None,
first_window_id: None,
previous_window_id: None,
window_id: None,
}),
];
@@ -894,7 +900,7 @@ async fn record_initial_history_resumed_does_not_seed_reference_context_item_aft
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -914,6 +920,8 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_does_
message: "legacy summary".to_string(),
replacement_history: None,
window_number: None,
first_window_id: None,
previous_window_id: None,
window_id: None,
}),
];
@@ -947,6 +955,8 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_clear
message: "legacy summary".to_string(),
replacement_history: None,
window_number: None,
first_window_id: None,
previous_window_id: None,
window_id: None,
}),
RolloutItem::EventMsg(EventMsg::TurnStarted(
@@ -1043,6 +1053,8 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
message: String::new(),
replacement_history: Some(Vec::new()),
window_number: None,
first_window_id: None,
previous_window_id: None,
window_id: None,
}),
RolloutItem::TurnContext(previous_context_item),
@@ -1060,7 +1072,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -1196,6 +1208,8 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
message: String::new(),
replacement_history: Some(Vec::new()),
window_number: None,
first_window_id: None,
previous_window_id: None,
window_id: None,
}),
];
@@ -1203,7 +1217,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -1326,7 +1340,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -1433,6 +1447,8 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
message: String::new(),
replacement_history: Some(Vec::new()),
window_number: None,
first_window_id: None,
previous_window_id: None,
window_id: None,
}),
];
@@ -1440,7 +1456,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -1491,7 +1507,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_preserves_turn_
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -1599,6 +1615,8 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
message: String::new(),
replacement_history: Some(Vec::new()),
window_number: None,
first_window_id: None,
previous_window_id: None,
window_id: None,
}),
// A newer TurnStarted replaces the incomplete compacted turn without a matching
@@ -1617,7 +1635,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
+30 -18
View File
@@ -52,7 +52,7 @@ pub(crate) struct SessionConfiguration {
pub(super) provider: ModelProviderInfo,
pub(super) collaboration_mode: CollaborationMode,
pub(super) multi_agent_mode: Option<MultiAgentMode>,
pub(super) multi_agent_mode: MultiAgentMode,
pub(super) model_reasoning_summary: Option<ReasoningSummaryConfig>,
pub(super) service_tier: Option<String>,
@@ -233,7 +233,7 @@ impl SessionConfiguration {
next_configuration.collaboration_mode = collaboration_mode;
}
if let Some(multi_agent_mode) = updates.multi_agent_mode {
next_configuration.multi_agent_mode = Some(multi_agent_mode);
next_configuration.multi_agent_mode = multi_agent_mode;
}
if let Some(summary) = updates.reasoning_summary {
next_configuration.model_reasoning_summary = Some(summary);
@@ -527,6 +527,33 @@ impl Session {
}
InitialHistory::Resumed(resumed_history) => resumed_history.conversation_id,
};
let resumed_session_id = match &initial_history {
InitialHistory::Resumed(resumed) => {
resumed.history.iter().find_map(|item| match item {
RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.session_id),
_ => None,
})
}
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => None,
};
// Legacy subagent rollouts synthesize session_id from their own thread id.
let resumed_session_id = resumed_session_id.filter(|session_id| {
!session_configuration.session_source.is_non_root_agent()
|| *session_id != SessionId::from(thread_id)
});
let session_id = resumed_session_id.unwrap_or_else(|| {
if session_configuration.session_source.is_non_root_agent() {
agent_control.session_id()
} else {
SessionId::from(thread_id)
}
});
let agent_control = agent_control.with_session_id(
session_id,
config
.effective_agent_max_threads(MultiAgentVersion::V2)
.unwrap_or(usize::MAX),
);
let time_provider = crate::current_time::resolve_time_provider(
config.current_time_reminder.as_ref(),
external_time_provider,
@@ -558,6 +585,7 @@ impl Session {
let live_thread = match &initial_history {
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => {
let params = CreateThreadParams {
session_id,
thread_id,
extra_config: config.extra_config.clone(),
forked_from_id,
@@ -985,17 +1013,6 @@ impl Session {
config.analytics_enabled,
)
});
let session_id = if session_configuration.session_source.is_non_root_agent() {
agent_control.session_id()
} else {
SessionId::from(thread_id)
};
let agent_control = agent_control.with_session_id(
session_id,
config
.effective_agent_max_threads(MultiAgentVersion::V2)
.unwrap_or(usize::MAX),
);
// Keep one stable manager handle for the session so extension resource clients
// automatically observe the manager installed at startup and on later refreshes.
let mcp_connection_manager = Arc::new(arc_swap::ArcSwap::from_pointee(
@@ -1154,9 +1171,6 @@ impl Session {
sess.send_event_raw(event).await;
}
let host_owned_codex_apps_enabled = config
.features
.apps_enabled_for_auth(auth.as_ref().is_some_and(|auth| auth.uses_codex_backend()));
let client_elicitation_capability = if config.features.enabled(Feature::AuthElicitation) {
ElicitationCapability {
form: Some(FormElicitationCapability::default()),
@@ -1199,7 +1213,6 @@ impl Session {
mcp_runtime_context,
config.codex_home.to_path_buf(),
codex_apps_tools_cache_key(auth),
host_owned_codex_apps_enabled,
config.prefix_mcp_tool_names(),
client_elicitation_capability,
sess.services
@@ -1233,7 +1246,6 @@ impl Session {
let mut state = sess.state.lock().await;
state.queue_pending_session_start_source(session_start_source);
}
Ok(sess)
}
.await;
@@ -0,0 +1,7 @@
use crate::environment_selection::TurnEnvironmentSnapshot;
/// Request-scoped state that may change between model sampling requests.
#[derive(Debug)]
pub(crate) struct StepContext {
pub(crate) environments: TurnEnvironmentSnapshot,
}
+239 -119
View File
@@ -107,8 +107,8 @@ use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::ContentItem;
use codex_protocol::models::InternalChatMessageMetadataPassthrough;
use codex_protocol::models::ResponseItem;
use codex_protocol::models::ResponseItemMetadata;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::CompactedItem;
@@ -152,6 +152,7 @@ use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server;
use core_test_support::responses::strip_metadata_from_items;
use core_test_support::test_codex::local;
use core_test_support::test_codex::test_codex;
use core_test_support::test_path_buf;
@@ -196,7 +197,7 @@ fn user_message(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -210,7 +211,7 @@ fn assign_missing_response_item_ids_skips_agent_messages() {
content: vec![AgentMessageInputContent::InputText {
text: "done".to_string(),
}],
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
user_message("hello"),
]);
@@ -221,6 +222,19 @@ fn assign_missing_response_item_ids_skips_agent_messages() {
assert!(items[1].id().is_some_and(|id| id.starts_with("msg_")));
}
#[test]
fn assign_missing_response_item_ids_assigns_additional_tools_ids() {
let items = Cow::Owned(vec![ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools: Vec::new(),
}]);
let items = Session::assign_missing_response_item_ids(items);
assert!(items[0].id().is_some_and(|id| id.starts_with("at_")));
}
fn assistant_message(text: &str) -> ResponseItem {
ResponseItem::Message {
id: None,
@@ -229,7 +243,7 @@ fn assistant_message(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -289,7 +303,7 @@ fn skill_message(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -1636,9 +1650,8 @@ async fn reconstruct_history_uses_replacement_history_verbatim() {
text: "summary".to_string(),
}],
phase: None,
metadata: Some(ResponseItemMetadata {
internal_chat_message_metadata_passthrough: Some(InternalChatMessageMetadataPassthrough {
turn_id: Some("compact-turn".to_string()),
..Default::default()
}),
};
let replacement_history = vec![
@@ -1650,14 +1663,18 @@ async fn reconstruct_history_uses_replacement_history_verbatim() {
text: "stale developer instructions".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
let first_window_id = Uuid::now_v7();
let previous_window_id = Uuid::now_v7();
let window_id = Uuid::now_v7();
let rollout_items = vec![RolloutItem::Compacted(CompactedItem {
message: String::new(),
replacement_history: Some(replacement_history.clone()),
window_number: Some(42),
first_window_id: Some(first_window_id.to_string()),
previous_window_id: Some(previous_window_id.to_string()),
window_id: Some(window_id.to_string()),
})];
@@ -1667,6 +1684,8 @@ async fn reconstruct_history_uses_replacement_history_verbatim() {
assert_eq!(reconstructed.history, replacement_history);
assert_eq!(42, reconstructed.window_number);
assert_eq!(Some(first_window_id), reconstructed.first_window_id);
assert_eq!(Some(previous_window_id), reconstructed.previous_window_id);
assert_eq!(Some(window_id), reconstructed.window_id);
}
@@ -1678,7 +1697,7 @@ async fn record_initial_history_reconstructs_resumed_transcript() {
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -1688,12 +1707,72 @@ async fn record_initial_history_reconstructs_resumed_transcript() {
}
#[tokio::test]
async fn resize_all_images_prepares_failures_before_history_insertion() {
async fn record_conversation_items_stamps_missing_turn_id_and_preserves_existing_turn_id() {
let (session, turn_context) = make_session_and_context().await;
let fresh_item = user_message("fresh");
let mut existing_item = assistant_message("existing");
existing_item.set_turn_id_if_missing("older-turn");
session
.record_conversation_items(&turn_context, &[fresh_item.clone(), existing_item.clone()])
.await;
let mut expected_fresh_item = fresh_item;
expected_fresh_item.set_turn_id_if_missing(&turn_context.sub_id);
let expected_items = vec![expected_fresh_item, existing_item];
assert_eq!(
session.clone_history().await.raw_items(),
expected_items.as_slice()
);
}
#[tokio::test]
async fn record_inter_agent_communication_sets_turn_id_in_rollout_and_resume() {
let (mut session, turn_context) = make_session_and_context().await;
let rollout_path = attach_thread_persistence(&mut session).await;
let communication = InterAgentCommunication::new(
AgentPath::root().join("worker").expect("worker path"),
AgentPath::root(),
Vec::new(),
"child done".to_string(),
/*trigger_turn*/ false,
);
let mut expected_item = communication.to_model_input_item();
expected_item.set_turn_id_if_missing(&turn_context.sub_id);
session
.record_inter_agent_communication(&turn_context, communication)
.await;
assert_eq!(
session.clone_history().await.raw_items(),
std::slice::from_ref(&expected_item)
);
session.flush_rollout().await.expect("rollout should flush");
let InitialHistory::Resumed(resumed) = RolloutRecorder::get_rollout_history(&rollout_path)
.await
.expect("read rollout history")
else {
panic!("expected resumed rollout history");
};
let (resumed_session, _resumed_turn_context) = make_session_and_context().await;
resumed_session
.record_initial_history(InitialHistory::Resumed(resumed))
.await;
assert_eq!(
resumed_session.clone_history().await.raw_items(),
std::slice::from_ref(&expected_item)
);
}
#[tokio::test]
async fn prepares_image_failures_before_history_insertion() {
let (session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx(
CodexAuth::from_api_key("Test API Key"),
Vec::new(),
|config| {
let _ = config.features.enable(Feature::ResizeAllImages);
let _ = config.features.enable(Feature::ItemIds);
},
)
@@ -1717,7 +1796,7 @@ async fn resize_all_images_prepares_failures_before_history_insertion() {
]),
success: Some(true),
},
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
session
@@ -1745,28 +1824,21 @@ async fn resize_all_images_prepares_failures_before_history_insertion() {
FunctionCallOutputContentItem::InputText {
text: "image content omitted because it could not be processed".to_string(),
},
FunctionCallOutputContentItem::InputImage {
image_url: "https://example.com/image.png".to_string(),
detail: Some(ImageDetail::High),
FunctionCallOutputContentItem::InputText {
text: "image content omitted because remote image URLs are not supported"
.to_string(),
},
]),
success: Some(true),
},
metadata: None,
internal_chat_message_metadata_passthrough: None,
}];
assert_eq!(history.raw_items(), expected.as_slice());
assert_eq!(strip_metadata_from_items(history.raw_items()), expected);
}
#[tokio::test]
async fn resize_all_images_prepares_resumed_history_before_installing_it() {
let (session, _turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx(
CodexAuth::from_api_key("Test API Key"),
Vec::new(),
|config| {
let _ = config.features.enable(Feature::ResizeAllImages);
},
)
.await;
async fn prepares_resumed_history_before_installing_it() {
let (session, _turn_context) = make_session_and_context().await;
let resumed_item = ResponseItem::Message {
id: None,
role: "user".to_string(),
@@ -1775,18 +1847,22 @@ async fn resize_all_images_prepares_resumed_history_before_installing_it() {
image_url: "data:image/png;base64,%%%".to_string(),
detail: Some(ImageDetail::High),
},
ContentItem::InputImage {
image_url: "https://example.com/image.png".to_string(),
detail: Some(ImageDetail::High),
},
ContentItem::InputText {
text: "keep me".to_string(),
},
],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: vec![RolloutItem::ResponseItem(resumed_item)],
history: Arc::new(vec![RolloutItem::ResponseItem(resumed_item)]),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -1800,12 +1876,16 @@ async fn resize_all_images_prepares_resumed_history_before_installing_it() {
ContentItem::InputText {
text: "image content omitted because it could not be processed".to_string(),
},
ContentItem::InputText {
text: "image content omitted because remote image URLs are not supported"
.to_string(),
},
ContentItem::InputText {
text: "keep me".to_string(),
},
],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}]
);
}
@@ -1825,7 +1905,7 @@ fn resolve_multi_agent_version_handles_unset_and_legacy_history() {
resolve_multi_agent_version(
&InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history: Vec::new(),
history: Arc::new(Vec::new()),
rollout_path: None,
}),
/*inherited_multi_agent_version*/ None,
@@ -1836,7 +1916,7 @@ fn resolve_multi_agent_version_handles_unset_and_legacy_history() {
resolve_multi_agent_version(
&InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history: Vec::new(),
history: Arc::new(Vec::new()),
rollout_path: None,
}),
Some(MultiAgentVersion::V2),
@@ -1847,10 +1927,10 @@ fn resolve_multi_agent_version_handles_unset_and_legacy_history() {
resolve_multi_agent_version(
&InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history: vec![session_meta_item(
history: Arc::new(vec![session_meta_item(
thread_id,
Some(MultiAgentVersion::Disabled)
)],
)]),
rollout_path: None,
}),
Some(MultiAgentVersion::V2),
@@ -1894,6 +1974,7 @@ fn session_meta_item(
) -> RolloutItem {
RolloutItem::SessionMeta(SessionMetaLine {
meta: SessionMeta {
session_id: thread_id.into(),
id: thread_id,
multi_agent_version,
..SessionMeta::default()
@@ -1910,7 +1991,7 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only()
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -2004,7 +2085,7 @@ async fn record_initial_history_seeds_token_info_from_rollout() {
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: rollout_items,
history: Arc::new(rollout_items),
rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")),
}))
.await;
@@ -3059,6 +3140,8 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
user_message("turn 1 user"),
user_message("summary after compaction"),
];
let first_window_id = Uuid::now_v7();
let previous_window_id = Uuid::now_v7();
let compacted_window_id = Uuid::now_v7();
sess.persist_rollout_items(&[
@@ -3102,6 +3185,8 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
message: "summary after compaction".to_string(),
replacement_history: Some(compacted_history.clone()),
window_number: Some(7),
first_window_id: Some(first_window_id.to_string()),
previous_window_id: Some(previous_window_id.to_string()),
window_id: Some(compacted_window_id.to_string()),
}),
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
@@ -3152,7 +3237,14 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
.await;
{
let mut state = sess.state.lock().await;
state.restore_auto_compact_window(/*window_number*/ 99, Uuid::now_v7());
state.restore_auto_compact_window(
/*window_number*/ 99,
AutoCompactWindowIds {
first_window_id: Uuid::now_v7(),
previous_window_id: Some(Uuid::now_v7()),
window_id: Uuid::now_v7(),
},
);
}
handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await;
@@ -3162,8 +3254,12 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
assert_eq!(sess.clone_history().await.raw_items(), compacted_history);
assert!(sess.reference_context_item().await.is_none());
assert_eq!(
sess.state.lock().await.auto_compact_window_id(),
compacted_window_id
sess.state.lock().await.auto_compact_window_ids(),
AutoCompactWindowIds {
first_window_id,
previous_window_id: Some(previous_window_id),
window_id: compacted_window_id,
}
);
assert!(sess.current_window_id().await.ends_with(":7"));
}
@@ -3731,6 +3827,7 @@ async fn attach_thread_persistence(session: &mut Session) -> PathBuf {
let live_thread = LiveThread::create(
Arc::clone(&session.services.thread_store),
CreateThreadParams {
session_id: session.session_id(),
thread_id: session.thread_id,
extra_config: None,
forked_from_id: None,
@@ -5419,7 +5516,7 @@ async fn resumed_root_session_uses_thread_id_as_session_id() {
let (session, rx_event) = make_session_with_history_source_and_agent_control_and_rx(
InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history: Vec::new(),
history: Arc::new(Vec::new()),
rollout_path: None,
}),
SessionSource::Exec,
@@ -5440,7 +5537,7 @@ async fn resumed_root_session_uses_thread_id_as_session_id() {
}
#[tokio::test]
async fn resumed_subagent_session_keeps_inherited_session_id() {
async fn resumed_subagent_session_restores_persisted_session_id() {
let parent_thread_id = ThreadId::new();
let parent_session_id = SessionId::from(parent_thread_id);
let thread_id = ThreadId::new();
@@ -5454,11 +5551,19 @@ async fn resumed_subagent_session_keeps_inherited_session_id() {
let (session, rx_event) = make_session_with_history_source_and_agent_control_and_rx(
InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history: Vec::new(),
history: Arc::new(vec![RolloutItem::SessionMeta(SessionMetaLine {
meta: SessionMeta {
session_id: parent_session_id,
id: thread_id,
source: session_source.clone(),
..SessionMeta::default()
},
git: None,
})]),
rollout_path: None,
}),
session_source,
AgentControl::default().with_session_id(parent_session_id, /*max_threads*/ usize::MAX),
AgentControl::default(),
)
.await
.expect("resume should succeed");
@@ -6577,6 +6682,7 @@ async fn shutdown_complete_does_not_append_to_thread_store_after_shutdown() {
let live_thread = LiveThread::create(
Arc::clone(&thread_store),
CreateThreadParams {
session_id: session.session_id(),
thread_id: session.thread_id,
extra_config: None,
forked_from_id: None,
@@ -7336,7 +7442,7 @@ async fn spawn_task_does_not_update_previous_turn_settings_for_non_run_turn_task
}
#[tokio::test]
async fn build_settings_update_items_emits_environment_item_for_network_changes() {
async fn record_context_updates_emits_environment_item_for_network_changes() {
let (session, previous_context) = make_session_and_context().await;
let previous_context = Arc::new(previous_context);
let mut current_context = previous_context
@@ -7383,10 +7489,8 @@ async fn build_settings_update_items_emits_environment_item_for_network_changes(
.expect("rebuild config layer stack with network requirements");
current_context.config = Arc::new(config);
let reference_context_item = previous_context.to_turn_context_item();
let update_items = session
.build_settings_update_items(Some(&reference_context_item), &current_context)
.await;
let update_items =
record_context_update_items(&session, &previous_context, &current_context).await;
let environment_update = user_input_texts(&update_items)
.into_iter()
@@ -7398,50 +7502,40 @@ async fn build_settings_update_items_emits_environment_item_for_network_changes(
}
#[tokio::test]
async fn environment_context_uses_session_shell_when_environment_shell_is_absent() {
let (mut session, mut turn_context) = make_session_and_context().await;
session.services.user_shell = Arc::new(crate::shell::Shell {
shell_type: crate::shell::ShellType::PowerShell,
shell_path: PathBuf::from("powershell"),
});
for environment in &mut turn_context.environments.turn_environments {
environment.shell = None;
}
let session_shell = session.user_shell();
let environment_context = crate::context::EnvironmentContext::from_turn_context(
&turn_context,
session_shell.as_ref(),
)
.render();
assert!(
environment_context.contains("<shell>powershell</shell>"),
"{environment_context}"
async fn record_context_updates_emits_environment_item_for_cwd_changes() {
let (session, previous_context) = make_session_and_context().await;
let previous_context = Arc::new(previous_context);
let mut current_context = previous_context
.with_model(
previous_context.model_info.slug.clone(),
&session.services.models_manager,
)
.await;
let cwd = test_path_buf("/new-repo").abs();
let environment = current_context.environments.turn_environments[0].clone();
current_context.environments.turn_environments[0] = TurnEnvironment::new(
environment.environment_id,
environment.environment,
PathUri::from_abs_path(&cwd),
environment.shell,
);
let primary_environment = turn_context
.environments
.turn_environments
.first_mut()
.expect("primary environment");
primary_environment.shell = Some(crate::shell::Shell {
shell_type: crate::shell::ShellType::Cmd,
shell_path: PathBuf::from("cmd"),
});
let update_items =
record_context_update_items(&session, &previous_context, &current_context).await;
let environment_context = crate::context::EnvironmentContext::from_turn_context(
&turn_context,
session_shell.as_ref(),
)
.render();
let environment_update = user_input_texts(&update_items)
.into_iter()
.find(|text| text.contains("<environment_context>"))
.expect("environment update item should be emitted");
assert!(
environment_context.contains("<shell>cmd</shell>"),
"{environment_context}"
environment_update.contains(&format!("<cwd>{}</cwd>", cwd.display())),
"{environment_update}"
);
assert!(!environment_update.contains("<environments>"));
}
#[tokio::test]
async fn build_settings_update_items_emits_environment_item_for_time_changes() {
async fn record_context_updates_emits_environment_item_for_time_changes() {
let (session, previous_context) = make_session_and_context().await;
let previous_context = Arc::new(previous_context);
let mut current_context = previous_context
@@ -7453,10 +7547,8 @@ async fn build_settings_update_items_emits_environment_item_for_time_changes() {
current_context.current_date = Some("2026-02-27".to_string());
current_context.timezone = Some("Europe/Berlin".to_string());
let reference_context_item = previous_context.to_turn_context_item();
let update_items = session
.build_settings_update_items(Some(&reference_context_item), &current_context)
.await;
let update_items =
record_context_update_items(&session, &previous_context, &current_context).await;
let environment_update = user_input_texts(&update_items)
.into_iter()
@@ -7467,7 +7559,7 @@ async fn build_settings_update_items_emits_environment_item_for_time_changes() {
}
#[tokio::test]
async fn build_settings_update_items_omits_environment_item_when_disabled() {
async fn record_context_updates_omits_environment_item_when_disabled() {
let (session, previous_context) = make_session_and_context().await;
let previous_context = Arc::new(previous_context);
let mut current_context = previous_context
@@ -7479,12 +7571,16 @@ async fn build_settings_update_items_omits_environment_item_when_disabled() {
let mut config = (*current_context.config).clone();
config.include_environment_context = false;
current_context.config = Arc::new(config);
current_context.current_date = Some("2026-02-27".to_string());
let environment = current_context.environments.turn_environments[0].clone();
current_context.environments.turn_environments[0] = TurnEnvironment::new(
environment.environment_id,
environment.environment,
PathUri::from_abs_path(&test_path_buf("/new-repo").abs()),
environment.shell,
);
let reference_context_item = previous_context.to_turn_context_item();
let update_items = session
.build_settings_update_items(Some(&reference_context_item), &current_context)
.await;
let update_items =
record_context_update_items(&session, &previous_context, &current_context).await;
let user_texts = user_input_texts(&update_items);
assert!(
@@ -7495,6 +7591,23 @@ async fn build_settings_update_items_omits_environment_item_when_disabled() {
);
}
async fn record_context_update_items(
session: &Session,
previous_context: &TurnContext,
current_context: &TurnContext,
) -> Vec<ResponseItem> {
session
.record_context_updates_and_set_reference_context_item(previous_context)
.await;
let previous_len = session.clone_history().await.raw_items().len();
session
.record_context_updates_and_set_reference_context_item(current_context)
.await;
let history = session.clone_history().await;
history.raw_items()[previous_len..].to_vec()
}
#[tokio::test]
async fn build_settings_update_items_emits_realtime_start_when_session_becomes_live() {
let (session, previous_context) = make_session_and_context().await;
@@ -7756,9 +7869,11 @@ async fn record_context_updates_includes_turn_context_fragments_on_steady_state_
});
let mut previous_context_item = turn_context.to_turn_context_item();
previous_context_item.turn_id = Some("previous-turn-id".to_string());
let world_state = session.build_world_state(&turn_context).await;
{
let mut state = session.state.lock().await;
state.set_reference_context_item(Some(previous_context_item));
state.history.set_world_state_baseline(world_state);
}
session
@@ -7873,15 +7988,14 @@ async fn build_initial_context_omits_multi_agent_v2_usage_hints_when_feature_dis
}
#[tokio::test]
async fn build_initial_context_omits_multi_agent_v2_usage_hints_when_hint_disabled() {
async fn build_initial_context_omits_multi_agent_v2_usage_hints_when_hint_is_empty() {
let (session, turn_context, _rx_event) = make_session_and_context_with_auth_and_config_and_rx(
CodexAuth::from_api_key("Test API Key"),
Vec::new(),
|config| {
let _ = config.features.enable(Feature::MultiAgentV2);
config.multi_agent_v2.usage_hint_enabled = false;
config.multi_agent_v2.root_agent_usage_hint_text = Some("Root guidance.".to_string());
config.multi_agent_v2.subagent_usage_hint_text = Some("Subagent guidance.".to_string());
config.multi_agent_v2.root_agent_usage_hint_text = None;
config.multi_agent_v2.subagent_usage_hint_text = None;
},
)
.await;
@@ -7910,7 +8024,7 @@ async fn build_initial_context_omits_default_image_save_location_with_image_hist
status: "completed".to_string(),
revised_prompt: Some("a tiny blue square".to_string()),
result: "Zm9v".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}],
/*reference_context_item*/ None,
)
@@ -8163,7 +8277,7 @@ async fn handle_output_item_done_records_image_save_history_message() {
status: "completed".to_string(),
revised_prompt: Some("a tiny blue square".to_string()),
result: "Zm9v".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let mut ctx = HandleOutputCtx {
@@ -8195,7 +8309,7 @@ async fn handle_output_item_done_records_image_save_history_message() {
),
);
let expected = vec![image_message, item];
assert_eq!(history.raw_items(), expected.as_slice());
assert_eq!(strip_metadata_from_items(history.raw_items()), expected);
assert_eq!(
std::fs::read(&expected_saved_path).expect("saved file"),
b"foo"
@@ -8220,7 +8334,7 @@ async fn handle_output_item_done_skips_image_save_message_when_save_fails() {
status: "completed".to_string(),
revised_prompt: Some("broken payload".to_string()),
result: "_-8".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let mut ctx = HandleOutputCtx {
@@ -8238,7 +8352,7 @@ async fn handle_output_item_done_skips_image_save_message_when_save_fails() {
let history = session.clone_history().await;
let expected = vec![item];
assert_eq!(history.raw_items(), expected.as_slice());
assert_eq!(strip_metadata_from_items(history.raw_items()), expected);
assert!(!expected_saved_path.exists());
}
@@ -8386,7 +8500,7 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co
text: format!("{}\nsummary", crate::compact::SUMMARY_PREFIX),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
session
.record_conversation_items(&turn_context, std::slice::from_ref(&compacted_summary))
@@ -8429,9 +8543,11 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline
.with_model(next_model.to_string(), &session.services.models_manager)
.await;
let previous_context_item = previous_context.to_turn_context_item();
let world_state = session.build_world_state(&previous_context).await;
{
let mut state = session.state.lock().await;
state.set_reference_context_item(Some(previous_context_item.clone()));
state.history.set_world_state_baseline(world_state);
}
let rollout_path = attach_thread_persistence(&mut session).await;
@@ -8985,10 +9101,10 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
text: "late pending input".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
assert!(
history.raw_items().iter().any(|item| item == &expected),
strip_metadata_from_items(history.raw_items()).contains(&expected),
"expected pending input to be persisted into history on turn completion"
);
@@ -9418,7 +9534,7 @@ async fn abort_empty_active_turn_preserves_pending_input() {
text: "late pending input".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let turn_state = {
let mut active = sess.active_turn.lock().await;
@@ -9677,7 +9793,7 @@ async fn tool_calls_reopen_mailbox_delivery_for_current_turn() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-1".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let mut ctx = HandleOutputCtx {
sess: Arc::clone(&sess),
@@ -9805,7 +9921,7 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
call_id: "call-1".to_string(),
name: "shell_command".to_string(),
input: "{}".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let call = ToolRouter::build_tool_call(item.clone())
@@ -9890,7 +10006,7 @@ async fn sample_rollout(
text: "first user".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
live_history.record_items(
std::iter::once(&user1),
@@ -9905,7 +10021,7 @@ async fn sample_rollout(
text: "assistant reply one".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
live_history.record_items(
std::iter::once(&assistant1),
@@ -9920,12 +10036,14 @@ async fn sample_rollout(
let user_messages1 = collect_user_messages(&snapshot1);
let rebuilt1 = compact::build_compacted_history(Vec::new(), &user_messages1, summary1);
live_history.replace(rebuilt1);
let (window_number, window_id) = session.advance_auto_compact_window().await;
let (window_number, window_ids) = session.advance_auto_compact_window().await;
rollout_items.push(RolloutItem::Compacted(CompactedItem {
message: summary1.to_string(),
replacement_history: None,
window_number: Some(window_number),
window_id: Some(window_id),
first_window_id: Some(window_ids.first_window_id.to_string()),
previous_window_id: window_ids.previous_window_id.map(|id| id.to_string()),
window_id: Some(window_ids.window_id.to_string()),
}));
let user2 = ResponseItem::Message {
@@ -9935,7 +10053,7 @@ async fn sample_rollout(
text: "second user".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
live_history.record_items(
std::iter::once(&user2),
@@ -9950,7 +10068,7 @@ async fn sample_rollout(
text: "assistant reply two".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
live_history.record_items(
std::iter::once(&assistant2),
@@ -9965,12 +10083,14 @@ async fn sample_rollout(
let user_messages2 = collect_user_messages(&snapshot2);
let rebuilt2 = compact::build_compacted_history(Vec::new(), &user_messages2, summary2);
live_history.replace(rebuilt2);
let (window_number, window_id) = session.advance_auto_compact_window().await;
let (window_number, window_ids) = session.advance_auto_compact_window().await;
rollout_items.push(RolloutItem::Compacted(CompactedItem {
message: summary2.to_string(),
replacement_history: None,
window_number: Some(window_number),
window_id: Some(window_id),
first_window_id: Some(window_ids.first_window_id.to_string()),
previous_window_id: window_ids.previous_window_id.map(|id| id.to_string()),
window_id: Some(window_ids.window_id.to_string()),
}));
let user3 = ResponseItem::Message {
@@ -9980,7 +10100,7 @@ async fn sample_rollout(
text: "third user".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
live_history.record_items(
std::iter::once(&user3),
@@ -9995,7 +10115,7 @@ async fn sample_rollout(
text: "assistant reply three".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
live_history.record_items(
std::iter::once(&assistant3),
@@ -10141,7 +10261,7 @@ while :; do sleep 1; done"#,
})
.to_string(),
call_id: "shell-cleanup-call".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
let call = ToolRouter::build_tool_call(item)?
.expect("shell command response item should build a tool call");
@@ -535,7 +535,7 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message
text: "stale developer message".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
@@ -544,7 +544,7 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message
text: "summary".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
],
InitialContextInjection::BeforeLastUserMessage,
+18 -25
View File
@@ -3,42 +3,35 @@ use super::turn_context::TurnContext;
use crate::context::ContextualUserFragment;
use codex_features::Feature;
const TOKEN_BUDGET_USAGE_THRESHOLDS: [i64; 3] = [25, 50, 75];
pub(super) async fn maybe_record_token_budget_remaining_context(
pub(super) async fn maybe_record(
sess: &Session,
turn_context: &TurnContext,
tokens_before_sampling: i64,
tokens_after_sampling: i64,
tokens_until_compaction: i64,
) {
if !turn_context.config.features.enabled(Feature::TokenBudget) {
return;
}
let Some(model_context_window) = turn_context.model_context_window() else {
let Some(config) = turn_context.config.token_budget.as_ref().filter(|config| {
config
.reminder_threshold_tokens
.is_some_and(|threshold| tokens_until_compaction <= threshold)
}) else {
return;
};
if model_context_window <= 0 || tokens_after_sampling <= tokens_before_sampling {
let reminder_due = {
let mut state = sess.state.lock().await;
state.claim_token_budget_reminder()
};
if !reminder_due {
return;
}
let tokens_before_sampling = tokens_before_sampling.max(0);
let tokens_after_sampling = tokens_after_sampling.max(0);
let crossed_threshold = TOKEN_BUDGET_USAGE_THRESHOLDS.iter().any(|threshold| {
tokens_before_sampling.saturating_mul(100) < model_context_window.saturating_mul(*threshold)
&& tokens_after_sampling.saturating_mul(100)
>= model_context_window.saturating_mul(*threshold)
});
if !crossed_threshold {
return;
}
let tokens_left = model_context_window
.saturating_sub(tokens_after_sampling)
.max(0);
let response_item = ContextualUserFragment::into(
crate::context::TokenBudgetRemainingContext::new(tokens_left),
);
let response_item = ContextualUserFragment::into(crate::context::TokenBudgetReminder::new(
&config.reminder_message_template,
tokens_until_compaction,
));
sess.record_conversation_items(turn_context, std::slice::from_ref(&response_item))
.await;
}
+73 -22
View File
@@ -42,6 +42,7 @@ use crate::responses_retry::handle_retryable_response_stream_error;
use crate::session::PreviousTurnSettings;
use crate::session::TurnInput;
use crate::session::session::Session;
use crate::session::step_context::StepContext;
use crate::session::turn_context::TurnContext;
use crate::stream_events_utils::HandleOutputCtx;
use crate::stream_events_utils::TurnItemContributorPolicy;
@@ -100,6 +101,7 @@ use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::PlanDeltaEvent;
use codex_protocol::protocol::ReasoningContentDeltaEvent;
use codex_protocol::protocol::ReasoningRawContentDeltaEvent;
use codex_protocol::protocol::SafetyBufferingEvent;
use codex_protocol::protocol::TurnDiffEvent;
use codex_protocol::protocol::WarningEvent;
use codex_protocol::user_input::UserInput;
@@ -240,6 +242,21 @@ pub(crate) async fn run_turn(
)
.await?;
if turn_context
.config
.features
.enabled(Feature::DeferredExecutor)
{
let step_context = StepContext {
environments: sess.services.turn_environments.snapshot().await,
};
sess.record_step_environment_context_if_changed(
turn_context.as_ref(),
&step_context,
)
.await;
}
// Construct the input that we will send to the model.
let sampling_request_input: Vec<ResponseItem> = async {
sess.clone_history()
@@ -254,8 +271,7 @@ pub(crate) async fn run_turn(
window_id,
CodexResponsesRequestKind::Turn,
);
let tokens_before_sampling = sess.get_total_token_usage().await;
let (sampling_request_output, sampling_request_input) = run_sampling_request(
run_sampling_request(
Arc::clone(&sess),
Arc::clone(&turn_context),
Arc::clone(&turn_extension_data),
@@ -265,17 +281,11 @@ pub(crate) async fn run_turn(
sampling_request_input,
cancellation_token.child_token(),
)
.await?;
Ok((
tokens_before_sampling,
sampling_request_output,
sampling_request_input,
))
.await
}
.await;
match sampling_request_result {
Ok((tokens_before_sampling, sampling_request_output, sampling_request_input)) => {
Ok((sampling_request_output, sampling_request_input)) => {
let SamplingRequestResult {
needs_follow_up: model_needs_follow_up,
last_agent_message: sampling_request_last_agent_message,
@@ -313,11 +323,20 @@ pub(crate) async fn run_turn(
);
let tokens_after_sampling = token_status.active_context_tokens;
super::token_budget::maybe_record_token_budget_remaining_context(
let full_context_remaining = token_status
.full_context_window_limit
.map_or(i64::MAX, |limit| {
limit.saturating_sub(tokens_after_sampling)
});
let tokens_until_compaction = token_status
.auto_compact_scope_limit
.saturating_sub(token_status.auto_compact_scope_tokens)
.min(full_context_remaining)
.max(0);
super::token_budget::maybe_record(
sess.as_ref(),
turn_context.as_ref(),
tokens_before_sampling,
tokens_after_sampling,
tokens_until_compaction,
)
.await;
@@ -331,7 +350,13 @@ pub(crate) async fn run_turn(
}
// as long as compaction works well in getting us way below the token limit, we shouldn't worry about being in an infinite loop.
if token_limit_reached && needs_follow_up {
if turn_context
.config
.features
.enabled(Feature::AutoCompaction)
&& token_limit_reached
&& needs_follow_up
{
if let Err(err) = run_auto_compact(
&sess,
&turn_context,
@@ -631,13 +656,16 @@ async fn build_skills_and_plugins(
sess.services
.analytics_events_client
.track_app_mentioned(tracking.clone(), mentioned_app_invocations);
for plugin in mentioned_plugins
.iter()
.filter_map(crate::plugins::PluginCapabilitySummary::telemetry_metadata)
{
sess.services
.analytics_events_client
.track_plugin_used(tracking.clone(), plugin);
for summary in &mentioned_plugins {
if let Some(plugin) = sess
.services
.plugins_manager
.telemetry_metadata_for_capability_summary(summary)
{
sess.services
.analytics_events_client
.track_plugin_used(tracking.clone(), plugin);
}
}
let mut injection_items: Vec<ResponseItem> = match injected_host_skill_prompts {
@@ -842,6 +870,14 @@ async fn run_pre_sampling_compact(
turn_context: &Arc<TurnContext>,
client_session: &mut ModelClientSession,
) -> CodexResult<()> {
if !turn_context
.config
.features
.enabled(Feature::AutoCompaction)
{
return Ok(());
}
maybe_run_previous_model_inline_compact(sess, turn_context, client_session).await?;
let token_status = auto_compact_token_status(sess.as_ref(), turn_context.as_ref()).await;
// Compact if the configured auto-compaction budget or usable context window is exhausted.
@@ -1518,6 +1554,7 @@ pub(super) fn realtime_text_for_event(msg: &EventMsg) -> Option<(String, Option<
| EventMsg::ModelReroute(_)
| EventMsg::ModelVerification(_)
| EventMsg::TurnModerationMetadata(_)
| EventMsg::SafetyBuffering(_)
| EventMsg::ContextCompacted(_)
| EventMsg::ThreadRolledBack(_)
| EventMsg::TurnStarted(_)
@@ -2054,7 +2091,8 @@ async fn try_run_sampling_request(
}
ResponseItem::Reasoning { .. } => true,
ResponseItem::AgentMessage { .. } => false,
ResponseItem::LocalShellCall { .. }
ResponseItem::AdditionalTools { .. }
| ResponseItem::LocalShellCall { .. }
| ResponseItem::FunctionCall { .. }
| ResponseItem::ToolSearchCall { .. }
| ResponseItem::FunctionCallOutput { .. }
@@ -2190,6 +2228,19 @@ async fn try_run_sampling_request(
sess.emit_turn_moderation_metadata(&turn_context, metadata)
.await;
}
ResponseEvent::SafetyBuffering(buffering) => {
sess.send_event(
&turn_context,
EventMsg::SafetyBuffering(SafetyBufferingEvent {
model: turn_context.model_info.slug.clone(),
use_cases: buffering.use_cases,
reasons: buffering.reasons,
show_buffering_ui: buffering.show_buffering_ui,
faster_model: buffering.faster_model,
}),
)
.await;
}
ResponseEvent::ServerReasoningIncluded(included) => {
sess.set_server_reasoning_included(included).await;
}
+10 -4
View File
@@ -125,7 +125,7 @@ pub struct TurnContext {
pub(crate) developer_instructions: Option<String>,
pub(crate) user_instructions: Option<String>,
pub(crate) collaboration_mode: CollaborationMode,
pub(crate) multi_agent_mode: Option<MultiAgentMode>,
pub(crate) multi_agent_mode: MultiAgentMode,
pub(crate) multi_agent_version: MultiAgentVersion,
pub(crate) personality: Option<Personality>,
pub(crate) approval_policy: Constrained<AskForApproval>,
@@ -386,10 +386,8 @@ impl TurnContext {
multi_agent_version: Some(self.multi_agent_version),
multi_agent_mode: super::multi_agents::effective_multi_agent_mode(
self.multi_agent_version,
&self.config.multi_agent_v2,
&self.session_source,
self.multi_agent_mode,
self.config.features.enabled(Feature::MultiAgentMode),
),
realtime_active: Some(self.realtime_active),
effort: self.reasoning_effort.clone(),
@@ -538,6 +536,7 @@ impl Session {
session_configuration.forked_from_thread_id,
session_configuration.parent_thread_id,
&session_configuration.session_source,
session_configuration.thread_source.clone(),
sub_id.clone(),
cwd.clone(),
&session_configuration.permission_profile(),
@@ -798,7 +797,7 @@ impl Session {
turn_context
}
pub(crate) async fn maybe_emit_unknown_model_warning_for_turn(&self, tc: &TurnContext) {
pub(crate) async fn maybe_emit_model_warnings_for_turn(&self, tc: &TurnContext) {
if tc.model_info.used_fallback_model_metadata {
self.send_event(
tc,
@@ -811,6 +810,13 @@ impl Session {
)
.await;
}
if let Some(message) =
unsupported_code_mode_warning(&tc.model_info, tc.config.features.get())
{
self.send_event(tc, EventMsg::Warning(WarningEvent { message }))
.await;
}
}
pub(crate) async fn new_default_turn(&self) -> Arc<TurnContext> {
+1 -1
View File
@@ -33,7 +33,7 @@ fn assistant_output_text(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
+41
View File
@@ -0,0 +1,41 @@
use super::turn_context::TurnContext;
use crate::context::world_state::EnvironmentsState;
use crate::context::world_state::WorldState;
use crate::environment_selection::TurnEnvironmentSnapshot;
use codex_protocol::protocol::TurnContextItem;
pub(super) fn build_world_state_from_turn_context(
turn_context: &TurnContext,
environment_subagents: &str,
) -> WorldState {
let mut world_state = WorldState::default();
if turn_context.config.include_environment_context {
world_state.add_section(
EnvironmentsState::from_turn_context(turn_context)
.with_subagents(environment_subagents.to_string()),
);
}
world_state
}
pub(super) fn build_world_state_from_environment_snapshot(
turn_context: &TurnContext,
environments: &TurnEnvironmentSnapshot,
) -> WorldState {
let mut world_state = WorldState::default();
if turn_context.config.include_environment_context {
world_state.add_section(EnvironmentsState::from_turn_context_with_environments(
turn_context,
environments,
));
}
world_state
}
pub(super) fn build_world_state_from_turn_context_item(
turn_context_item: &TurnContextItem,
) -> WorldState {
let mut world_state = WorldState::default();
world_state.add_section(EnvironmentsState::from_turn_context_item(turn_context_item));
world_state
}
+14 -1
View File
@@ -10,6 +10,7 @@ use tracing::instrument;
use tracing::warn;
use crate::client::ModelClientSession;
use crate::guardian::routes_approval_to_guardian;
use crate::responses_metadata::CodexResponsesRequestKind;
use crate::session::INITIAL_SUBMIT_ID;
use crate::session::session::Session;
@@ -242,6 +243,19 @@ async fn schedule_startup_prewarm_inner(
prewarm_started_at.elapsed(),
/*status*/ None,
);
if routes_approval_to_guardian(&startup_turn_context) {
let guardian_session = Arc::clone(&session);
let guardian_parent_turn = Arc::clone(&startup_turn_context);
drop(tokio::spawn(async move {
if let Err(err) = guardian_session
.guardian_review_session
.initialize(Arc::clone(&guardian_session), guardian_parent_turn)
.await
{
warn!("failed to initialize guardian review session: {err:#}");
}
}));
}
let startup_cancellation_token = CancellationToken::new();
let built_tools_started_at = Instant::now();
let startup_router = built_tools(
@@ -295,6 +309,5 @@ async fn schedule_startup_prewarm_inner(
websocket_warmup_started_at.elapsed(),
/*status*/ None,
);
Ok(client_session)
}
+59 -16
View File
@@ -1,6 +1,13 @@
use codex_protocol::protocol::TokenUsage;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct AutoCompactWindowIds {
pub(crate) first_window_id: Uuid,
pub(crate) previous_window_id: Option<Uuid>,
pub(crate) window_id: Uuid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct AutoCompactWindowSnapshot {
pub(crate) prefill_input_tokens: Option<i64>,
@@ -15,7 +22,7 @@ enum AutoCompactWindowPrefill {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct AutoCompactWindow {
window_number: u64,
window_id: Uuid,
ids: AutoCompactWindowIds,
new_context_window_requested: bool,
/// Absolute input-token baseline for the current compaction window.
///
@@ -23,15 +30,22 @@ pub(super) struct AutoCompactWindow {
/// not the growth itself; server-observed usage replaces estimated
/// resume/recompute baselines when available.
prefill_input_tokens: Option<AutoCompactWindowPrefill>,
token_budget_reminder_delivered: bool,
}
impl AutoCompactWindow {
pub(super) fn new() -> Self {
let window_id = Uuid::now_v7();
Self {
window_number: 0,
window_id: Uuid::now_v7(),
ids: AutoCompactWindowIds {
first_window_id: window_id,
previous_window_id: None,
window_id,
},
new_context_window_requested: false,
prefill_input_tokens: None,
token_budget_reminder_delivered: false,
}
}
@@ -43,20 +57,26 @@ impl AutoCompactWindow {
self.window_number
}
pub(super) fn window_id(&self) -> Uuid {
self.window_id
pub(super) fn ids(&self) -> AutoCompactWindowIds {
self.ids
}
pub(super) fn restore(&mut self, window_number: u64, window_id: Uuid) {
pub(super) fn restore(&mut self, window_number: u64, ids: AutoCompactWindowIds) {
self.window_number = window_number;
self.window_id = window_id;
self.ids = ids;
}
pub(super) fn advance(&mut self) -> (u64, Uuid) {
pub(super) fn advance(&mut self) -> (u64, AutoCompactWindowIds) {
self.window_number = self.window_number.saturating_add(1);
self.window_id = Uuid::now_v7();
self.ids.previous_window_id = Some(self.ids.window_id);
self.ids.window_id = Uuid::now_v7();
self.new_context_window_requested = false;
(self.window_number, self.window_id)
self.token_budget_reminder_delivered = false;
(self.window_number, self.ids)
}
pub(super) fn claim_token_budget_reminder(&mut self) -> bool {
!std::mem::replace(&mut self.token_budget_reminder_delivered, true)
}
pub(super) fn request_new_context_window(&mut self) {
@@ -118,22 +138,45 @@ mod tests {
let mut window = AutoCompactWindow::new();
assert_eq!(window.window_number(), 0);
assert_eq!(window.window_id().get_version_num(), 7);
let initial_window_id = window.ids().window_id;
assert_eq!(initial_window_id.get_version_num(), 7);
assert_eq!(
window.ids(),
AutoCompactWindowIds {
first_window_id: initial_window_id,
previous_window_id: None,
window_id: initial_window_id,
}
);
let first_window_id = initial_window_id;
let restored_window_id = Uuid::now_v7();
window.restore(/*window_number*/ 3, restored_window_id);
let restored_previous_window_id = Uuid::now_v7();
window.restore(
/*window_number*/ 3,
AutoCompactWindowIds {
first_window_id,
previous_window_id: Some(restored_previous_window_id),
window_id: restored_window_id,
},
);
assert_eq!(window.window_number(), 3);
assert_eq!(window.window_id(), restored_window_id);
assert_eq!(window.ids().window_id, restored_window_id);
assert!(window.claim_token_budget_reminder());
assert!(!window.claim_token_budget_reminder());
window.request_new_context_window();
assert!(window.take_new_context_window_request());
assert!(!window.take_new_context_window_request());
window.request_new_context_window();
let (window_number, window_id) = window.advance();
let (window_number, ids) = window.advance();
assert_eq!(window_number, 4);
assert_eq!(window.window_number(), 4);
assert_eq!(window.window_id(), window_id);
assert_eq!(window_id.get_version_num(), 7);
assert_ne!(window_id, restored_window_id);
assert_eq!(window.ids(), ids);
assert_eq!(ids.first_window_id, first_window_id);
assert_eq!(ids.previous_window_id, Some(restored_window_id));
assert_eq!(ids.window_id.get_version_num(), 7);
assert_ne!(ids.window_id, restored_window_id);
assert!(!window.take_new_context_window_request());
assert!(window.claim_token_budget_reminder());
assert_eq!(
window.snapshot(),
+1
View File
@@ -5,6 +5,7 @@ mod session;
mod turn;
pub(crate) use additional_context::AdditionalContextStore;
pub(crate) use auto_compact_window::AutoCompactWindowIds;
pub(crate) use auto_compact_window::AutoCompactWindowSnapshot;
pub(crate) use service::SessionServices;
pub(crate) use session::SessionState;
+17 -7
View File
@@ -6,10 +6,10 @@ use codex_sandboxing::policy_transforms::merge_permission_profiles;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
use uuid::Uuid;
use super::AdditionalContextStore;
use super::auto_compact_window::AutoCompactWindow;
use super::auto_compact_window::AutoCompactWindowIds;
use super::auto_compact_window::AutoCompactWindowSnapshot;
use crate::context_manager::ContextManager;
use crate::session::PreviousTurnSettings;
@@ -148,19 +148,27 @@ impl SessionState {
self.auto_compact_window.snapshot()
}
pub(crate) fn claim_token_budget_reminder(&mut self) -> bool {
self.auto_compact_window.claim_token_budget_reminder()
}
pub(crate) fn auto_compact_window_number(&self) -> u64 {
self.auto_compact_window.window_number()
}
pub(crate) fn auto_compact_window_id(&self) -> Uuid {
self.auto_compact_window.window_id()
pub(crate) fn auto_compact_window_ids(&self) -> AutoCompactWindowIds {
self.auto_compact_window.ids()
}
pub(crate) fn restore_auto_compact_window(&mut self, window_number: u64, window_id: Uuid) {
self.auto_compact_window.restore(window_number, window_id);
pub(crate) fn restore_auto_compact_window(
&mut self,
window_number: u64,
ids: AutoCompactWindowIds,
) {
self.auto_compact_window.restore(window_number, ids);
}
pub(crate) fn advance_auto_compact_window(&mut self) -> (u64, Uuid) {
pub(crate) fn advance_auto_compact_window(&mut self) -> (u64, AutoCompactWindowIds) {
self.auto_compact_window.advance()
}
@@ -168,7 +176,9 @@ impl SessionState {
self.auto_compact_window.request_new_context_window();
}
pub(crate) fn start_new_context_window_if_requested(&mut self) -> Option<(u64, Uuid)> {
pub(crate) fn start_new_context_window_if_requested(
&mut self,
) -> Option<(u64, AutoCompactWindowIds)> {
if !self.auto_compact_window.take_new_context_window_request() {
return None;
}
+4 -4
View File
@@ -627,7 +627,7 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti
id: None,
call_id: call_id.clone(),
output: output.clone(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
})
}
ResponseInputItem::CustomToolCallOutput {
@@ -639,7 +639,7 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti
call_id: call_id.clone(),
name: name.clone(),
output: output.clone(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}),
ResponseInputItem::McpToolCallOutput { call_id, output } => {
let output = output.as_function_call_output_payload();
@@ -647,7 +647,7 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti
id: None,
call_id: call_id.clone(),
output,
metadata: None,
internal_chat_message_metadata_passthrough: None,
})
}
ResponseInputItem::ToolSearchOutput {
@@ -661,7 +661,7 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti
status: status.clone(),
execution: execution.clone(),
tools: tools.clone(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}),
_ => None,
}
+10 -10
View File
@@ -42,7 +42,7 @@ fn assistant_output_text_with_phase(text: &str, phase: Option<MessagePhase>) ->
text: text.to_string(),
}],
phase,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -53,7 +53,7 @@ fn external_context_pollution_items_include_web_search_and_tool_search() {
id: None,
status: Some("completed".to_string()),
action: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::ToolSearchCall {
id: None,
@@ -61,7 +61,7 @@ fn external_context_pollution_items_include_web_search_and_tool_search() {
status: None,
execution: "client".to_string(),
arguments: serde_json::json!({"query": "calendar"}),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::ToolSearchOutput {
id: None,
@@ -69,7 +69,7 @@ fn external_context_pollution_items_include_web_search_and_tool_search() {
status: "completed".to_string(),
execution: "client".to_string(),
tools: Vec::new(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
];
@@ -94,7 +94,7 @@ fn external_context_pollution_items_exclude_local_tool_calls() {
env: None,
user: None,
}),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCall {
id: None,
@@ -102,13 +102,13 @@ fn external_context_pollution_items_exclude_local_tool_calls() {
namespace: None,
arguments: "{}".to_string(),
call_id: "call-1".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCallOutput {
id: None,
call_id: "call-1".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::CustomToolCall {
id: None,
@@ -116,14 +116,14 @@ fn external_context_pollution_items_exclude_local_tool_calls() {
call_id: "custom-1".to_string(),
name: "apply_patch".to_string(),
input: "*** Begin Patch\n*** End Patch\n".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::CustomToolCallOutput {
id: None,
call_id: "custom-1".to_string(),
name: Some("apply_patch".to_string()),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
assistant_output_text("plain assistant text"),
];
@@ -425,7 +425,7 @@ fn completed_item_defers_mailbox_delivery_for_image_generation_calls() {
status: "completed".to_string(),
revised_prompt: None,
result: "Zm9v".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
assert!(completed_item_defers_mailbox_delivery_to_next_turn(
+2 -2
View File
@@ -113,7 +113,7 @@ pub(crate) fn interrupted_turn_history_marker(
text: marker.render(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
})
}
}
@@ -483,7 +483,7 @@ impl Session {
}
let turn_context = self.new_default_turn_with_sub_id(sub_id).await;
self.maybe_emit_unknown_model_warning_for_turn(turn_context.as_ref())
self.maybe_emit_model_warnings_for_turn(turn_context.as_ref())
.await;
self.start_task(turn_context, Vec::new(), RegularTask::new())
.await;
+2 -2
View File
@@ -248,7 +248,7 @@ pub(crate) async fn exit_review_mode(
role: "user".to_string(),
content: vec![ContentItem::InputText { text: user_message }],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}],
)
.await;
@@ -269,7 +269,7 @@ pub(crate) async fn exit_review_mode(
text: assistant_message,
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
)
.await;
+2
View File
@@ -224,6 +224,8 @@ pub(crate) async fn execute_user_shell_command(
network_sandbox_policy: permission_profile.network_sandbox_policy(),
windows_sandbox_filesystem_overrides: None,
arg0: None,
exec_server_sandbox: None,
exec_server_enforce_managed_network: false,
};
let stdout_stream = Some(StdoutStream {
+14 -11
View File
@@ -1010,7 +1010,7 @@ impl ThreadManager {
};
let initial_multi_agent_mode = match source_thread_id {
Some(thread_id) => match self.get_thread(thread_id).await {
Ok(thread) => thread.config_snapshot().await.multi_agent_mode,
Ok(thread) => Some(thread.config_snapshot().await.multi_agent_mode),
Err(_) => history.get_latest_effective_multi_agent_mode(),
},
None => history.get_latest_effective_multi_agent_mode(),
@@ -1070,7 +1070,7 @@ impl ThreadManager {
}
fn agent_control_for_config(&self, config: &Config) -> AgentControl {
AgentControl::new(Arc::downgrade(&self.state), config.rollout_budget)
AgentControl::new(Arc::downgrade(&self.state), config.rollout_budget.clone())
}
#[cfg(test)]
@@ -1654,7 +1654,7 @@ fn stored_thread_to_initial_history(
})?;
Ok(InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history: history.items,
history: Arc::new(history.items),
rollout_path: rollout_path.or(stored_thread.rollout_path),
}))
}
@@ -1691,7 +1691,7 @@ fn truncate_before_nth_user_message(
n: usize,
snapshot_state: &SnapshotTurnState,
) -> InitialHistory {
let items: Vec<RolloutItem> = history.get_rollout_items();
let items = history.get_rollout_items().to_vec();
let user_positions = truncation::user_message_positions_in_rollout(&items);
let rolled = if snapshot_state.ends_mid_turn && n >= user_positions.len() {
if let Some(cut_idx) = snapshot_state
@@ -1723,7 +1723,7 @@ struct SnapshotTurnState {
fn snapshot_turn_state(history: &InitialHistory) -> SnapshotTurnState {
let rollout_items = history.get_rollout_items();
let mut builder = ThreadHistoryBuilder::new();
for item in &rollout_items {
for item in rollout_items {
builder.handle_rollout_item(item);
}
let active_turn_id = builder.active_turn_id_if_explicit();
@@ -1747,7 +1747,7 @@ fn snapshot_turn_state(history: &InitialHistory) -> SnapshotTurnState {
};
}
let Some(last_user_position) = truncation::user_message_positions_in_rollout(&rollout_items)
let Some(last_user_position) = truncation::user_message_positions_in_rollout(rollout_items)
.last()
.copied()
else {
@@ -1788,7 +1788,9 @@ fn fork_history_from_snapshot(
InitialHistory::New => InitialHistory::New,
InitialHistory::Cleared => InitialHistory::Cleared,
InitialHistory::Forked(history) => InitialHistory::Forked(history),
InitialHistory::Resumed(resumed) => InitialHistory::Forked(resumed.history),
InitialHistory::Resumed(resumed) => {
InitialHistory::Forked(Arc::unwrap_or_clone(resumed.history))
}
};
if snapshot_state.ends_mid_turn {
append_interrupted_boundary(
@@ -1834,12 +1836,13 @@ fn append_interrupted_boundary(
history.push(aborted_event);
InitialHistory::Forked(history)
}
InitialHistory::Resumed(mut resumed) => {
InitialHistory::Resumed(resumed) => {
let mut history = Arc::unwrap_or_clone(resumed.history);
if let Some(marker) = interrupted_turn_history_marker(interrupted_marker) {
resumed.history.push(RolloutItem::ResponseItem(marker));
history.push(RolloutItem::ResponseItem(marker));
}
resumed.history.push(aborted_event);
InitialHistory::Forked(resumed.history)
history.push(aborted_event);
InitialHistory::Forked(history)
}
}
}
+11 -11
View File
@@ -42,7 +42,7 @@ fn user_msg(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
fn assistant_msg(text: &str) -> ResponseItem {
@@ -53,7 +53,7 @@ fn assistant_msg(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -82,7 +82,7 @@ fn truncates_before_requested_user_message() {
}],
content: None,
encrypted_content: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCall {
id: None,
@@ -90,7 +90,7 @@ fn truncates_before_requested_user_message() {
name: "tool".to_string(),
namespace: None,
arguments: "{}".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
assistant_msg("a4"),
];
@@ -116,7 +116,7 @@ fn truncates_before_requested_user_message() {
RolloutItem::ResponseItem(items[2].clone()),
];
assert_eq!(
serde_json::to_value(&got_items).unwrap(),
serde_json::to_value(got_items).unwrap(),
serde_json::to_value(&expected_items).unwrap()
);
@@ -256,7 +256,7 @@ async fn ignores_session_prefix_messages_when_truncating() {
];
assert_eq!(
serde_json::to_value(&got_items).unwrap(),
serde_json::to_value(got_items).unwrap(),
serde_json::to_value(&expected).unwrap()
);
}
@@ -963,7 +963,7 @@ async fn rollout_path_resume_and_fork_read_history_through_thread_store() {
config.clone(),
InitialHistory::Resumed(ResumedHistory {
conversation_id: source.thread_id,
history: vec![RolloutItem::ResponseItem(user_msg("hello"))],
history: Arc::new(vec![RolloutItem::ResponseItem(user_msg("hello"))]),
rollout_path: Some(rollout_path.clone()),
}),
auth_manager.clone(),
@@ -1315,7 +1315,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
assert!(!snapshot_turn_state(&history).ends_mid_turn);
let rollout_items: Vec<_> = history
.get_rollout_items()
.into_iter()
.iter()
.filter(|item| !matches!(item, RolloutItem::SessionMeta(_)))
.collect();
let interrupted_marker_json = serde_json::to_value(RolloutItem::ResponseItem(
@@ -1435,7 +1435,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
.expect("read forked rollout history");
let rollout_items: Vec<_> = history
.get_rollout_items()
.into_iter()
.iter()
.filter(|item| !matches!(item, RolloutItem::SessionMeta(_)))
.collect();
@@ -1522,7 +1522,7 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
let forked_rollout_items: Vec<_> = history
.get_rollout_items()
.into_iter()
.iter()
.filter(|item| !matches!(item, RolloutItem::SessionMeta(_)))
.collect();
let interrupted_marker_json = serde_json::to_value(RolloutItem::ResponseItem(
@@ -1560,7 +1560,7 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
.expect("read re-forked rollout history");
let reforked_rollout_items: Vec<_> = reforked_history
.get_rollout_items()
.into_iter()
.iter()
.filter(|item| !matches!(item, RolloutItem::SessionMeta(_)))
.collect();
@@ -15,7 +15,7 @@ fn user_msg(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -27,7 +27,7 @@ fn assistant_msg(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -39,7 +39,7 @@ fn developer_msg(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
}
}
@@ -79,7 +79,7 @@ fn truncates_rollout_from_start_before_nth_user_only() {
}],
content: None,
encrypted_content: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCall {
id: None,
@@ -87,7 +87,7 @@ fn truncates_rollout_from_start_before_nth_user_only() {
name: "tool".to_string(),
namespace: None,
arguments: "{}".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
},
assistant_msg("a4"),
];
@@ -303,7 +303,7 @@ impl CoreTurnHost {
call_id,
name: Some(PUBLIC_TOOL_NAME.to_string()),
output: FunctionCallOutputPayload::from_text(text),
metadata: None,
internal_chat_message_metadata_passthrough: None,
}])
.await
.map_err(|_| {
+3 -3
View File
@@ -645,7 +645,7 @@ mod tests {
make_session_and_context_with_dynamic_tools_and_rx(Vec::new()).await;
let tracker = Arc::new(Mutex::new(TurnDiffTracker::new()));
let dir = tempdir().expect("tempdir");
let cwd = PathUri::from_path(dir.path()).expect("absolute cwd");
let cwd = PathUri::from_host_native_path(dir.path()).expect("absolute cwd");
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let delta = codex_apply_patch::apply_patch(
@@ -729,7 +729,7 @@ mod tests {
make_session_and_context_with_dynamic_tools_and_rx(Vec::new()).await;
let tracker = Arc::new(Mutex::new(TurnDiffTracker::new()));
let dir = tempdir().expect("tempdir");
let cwd = PathUri::from_path(dir.path()).expect("absolute cwd");
let cwd = PathUri::from_host_native_path(dir.path()).expect("absolute cwd");
for patch in [
"*** Begin Patch\n*** Add File: a.txt\n+one\n*** End Patch",
@@ -782,7 +782,7 @@ mod tests {
make_session_and_context_with_dynamic_tools_and_rx(Vec::new()).await;
let tracker = Arc::new(Mutex::new(TurnDiffTracker::new()));
let dir = tempdir().expect("tempdir");
let cwd = PathUri::from_path(dir.path()).expect("absolute cwd");
let cwd = PathUri::from_host_native_path(dir.path()).expect("absolute cwd");
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let delta = codex_apply_patch::apply_patch(
@@ -329,16 +329,18 @@ mod tests {
text: "extension history".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
session
.record_conversation_items(&turn, std::slice::from_ref(&history_item))
.await;
let mut expected_history_item = history_item.clone();
expected_history_item.set_turn_id_if_missing(&turn_id);
let raw_history_event = rx.recv().await.expect("history raw response item event");
let EventMsg::RawResponseItem(raw_history_item) = raw_history_event.msg else {
panic!("expected raw response item event");
};
assert_eq!(raw_history_item.item, history_item);
assert_eq!(raw_history_item.item, expected_history_item);
let invocation = ToolInvocation {
session,
turn,
@@ -377,7 +379,7 @@ mod tests {
);
assert_eq!(
captured_call.conversation_history.items(),
std::slice::from_ref(&history_item)
std::slice::from_ref(&expected_history_item)
);
match captured_call.payload {
ToolPayload::Function { arguments } => {
@@ -24,7 +24,6 @@ pub struct SpawnAgentToolOptions {
pub available_models: Vec<ModelPreset>,
pub agent_type_description: String,
pub hide_agent_type_model_reasoning: bool,
pub include_usage_hint: bool,
pub usage_hint_text: Option<String>,
}
@@ -66,7 +65,6 @@ pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions) -> ToolSpec {
available_models_description.as_deref(),
inherited_model_guidance,
return_value_description,
options.include_usage_hint,
options.usage_hint_text,
),
strict: false,
@@ -99,7 +97,6 @@ pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions) -> ToolSpec {
description: spawn_agent_tool_description_v2(
available_models_description.as_deref(),
inherited_model_guidance,
options.include_usage_hint,
options.usage_hint_text,
),
strict: false,
@@ -648,7 +645,6 @@ fn spawn_agent_tool_description(
available_models_description: Option<&str>,
inherited_model_guidance: Option<&str>,
return_value_description: &str,
include_usage_hint: bool,
usage_hint_text: Option<String>,
) -> String {
let agent_role_guidance = available_models_description.unwrap_or_default();
@@ -660,9 +656,6 @@ fn spawn_agent_tool_description(
Spawn a sub-agent for a well-scoped task. {return_value_description} {inherited_model_guidance}"#
);
if !include_usage_hint {
return tool_description;
}
if let Some(usage_hint_text) = usage_hint_text {
return format!(
r#"
@@ -711,7 +704,6 @@ Do not spawn sub-agents unless the user explicitly asks for sub-agents, delegati
fn spawn_agent_tool_description_v2(
available_models_description: Option<&str>,
inherited_model_guidance: Option<&str>,
include_usage_hint: bool,
usage_hint_text: Option<String>,
) -> String {
let agent_role_guidance = available_models_description.unwrap_or_default();
@@ -731,9 +723,6 @@ The new agent's canonical task name will be provided to it along with the messag
Note that passing `fork_turns="none"` will not pass any surrounding context to the spawned subagent, which may cause the agent to lack the context it needs to complete its task, whereas `fork_turns="all"` will provide the subagent with all surrounding context."#
);
if !include_usage_hint {
return tool_description;
}
if let Some(usage_hint_text) = usage_hint_text {
return format!(
r#"
@@ -45,7 +45,6 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
],
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: false,
include_usage_hint: true,
usage_hint_text: None,
});
@@ -121,7 +120,6 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
available_models: Vec::new(),
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: false,
include_usage_hint: true,
usage_hint_text: None,
});
@@ -178,7 +176,6 @@ fn spawn_agent_tool_caps_visible_model_summaries() {
],
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: false,
include_usage_hint: true,
usage_hint_text: None,
});
@@ -222,7 +219,6 @@ fn spawn_agent_tool_hides_service_tier_with_spawn_metadata() {
available_models: vec![model_preset("visible", /*show_in_picker*/ true)],
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: true,
include_usage_hint: true,
usage_hint_text: None,
});
@@ -1148,11 +1148,7 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
config
.features
.enable(Feature::MultiAgentMode)
.expect("test config should allow feature update");
turn.multi_agent_mode = Some(MultiAgentMode::Proactive);
turn.multi_agent_mode = MultiAgentMode::Proactive;
set_turn_config(&mut turn, config);
let session = Arc::new(session);
@@ -1191,10 +1187,7 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
child_snapshot.session_source.get_agent_path().as_deref(),
Some("/root/test_process")
);
assert_eq!(
child_snapshot.multi_agent_mode,
Some(MultiAgentMode::Proactive)
);
assert_eq!(child_snapshot.multi_agent_mode, MultiAgentMode::Proactive);
assert!(manager.captured_ops().iter().any(|(id, op)| {
*id == child_thread_id
&& matches!(
@@ -1205,11 +1198,6 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
&& communication.other_recipients.is_empty()
&& communication.content.is_empty()
&& communication.encrypted_content.as_deref() == Some("encrypted-spawn-message")
&& communication
.metadata
.as_ref()
.and_then(|metadata| metadata.source_call_id.as_deref())
== Some("call-1")
&& communication.trigger_turn
)
}));
@@ -1237,11 +1225,6 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
&& communication.other_recipients.is_empty()
&& communication.content.is_empty()
&& communication.encrypted_content.as_deref() == Some("encrypted-send-message")
&& communication
.metadata
.as_ref()
.and_then(|metadata| metadata.source_call_id.as_deref())
== Some("call-1")
&& !communication.trigger_turn
)
}));
@@ -2055,11 +2038,6 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn()
if communication.author == AgentPath::root()
&& communication.recipient == worker_path
&& communication.encrypted_content.as_deref() == Some("continue")
&& communication
.metadata
.as_ref()
.and_then(|metadata| metadata.source_call_id.as_deref())
== Some("call-1")
&& communication.trigger_turn
)
}));
@@ -2838,7 +2816,7 @@ async fn resume_agent_restores_closed_agent_and_accepts_send_input() {
text: "materialized".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
})]),
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy")),
/*parent_trace*/ None,
@@ -6,7 +6,6 @@
use super::*;
use crate::tools::context::FunctionToolOutput;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_protocol::models::ResponseItemMetadata;
use codex_protocol::protocol::InterAgentCommunication;
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -100,12 +99,8 @@ pub(crate) async fn handle_message_string_tool(
.session_source
.get_agent_path()
.unwrap_or_else(AgentPath::root);
let mut communication =
let communication =
communication_from_tool_message(author, receiver_agent_path.clone(), message);
communication
.metadata
.get_or_insert_with(ResponseItemMetadata::default)
.source_call_id = Some(call_id.clone());
let result = session
.services
.agent_control
@@ -8,7 +8,6 @@ use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions;
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_protocol::AgentPath;
use codex_protocol::models::ResponseItemMetadata;
use codex_protocol::protocol::Op;
use codex_tools::ToolSpec;
@@ -52,12 +51,8 @@ async fn handle_spawn_agent(
let fork_mode = args.fork_mode()?;
let multi_agent_mode = crate::session::multi_agents::effective_multi_agent_mode(
turn.multi_agent_version,
&turn.config.multi_agent_v2,
&turn.session_source,
turn.multi_agent_mode,
turn.config
.features
.enabled(codex_features::Feature::MultiAgentMode),
);
let role_name = args
.agent_type
@@ -127,12 +122,8 @@ async fn handle_spawn_agent(
.session_source
.get_agent_path()
.unwrap_or_else(AgentPath::root);
let mut communication =
let communication =
communication_from_tool_message(author, new_agent_path.clone(), message);
communication
.metadata
.get_or_insert_with(ResponseItemMetadata::default)
.source_call_id = Some(call_id.clone());
Op::InterAgentCommunication { communication }
}
initial_operation => initial_operation,
+3 -25
View File
@@ -1,4 +1,3 @@
use codex_features::Feature;
use codex_protocol::items::ImageViewItem;
use codex_protocol::items::TurnItem;
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
@@ -8,9 +7,7 @@ use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ImageDetail;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::openai_models::InputModality;
use codex_utils_image::PromptImageMode;
use codex_utils_image::data_url_from_bytes;
use codex_utils_image::load_for_prompt_bytes;
use serde::Deserialize;
use crate::function_tool::FunctionCallError;
@@ -195,24 +192,8 @@ impl ViewImageHandler {
DEFAULT_IMAGE_DETAIL
};
let image_url = if turn.config.features.enabled(Feature::ResizeAllImages) {
// The history insertion path owns image decoding and resizing when this is enabled.
data_url_from_bytes("application/octet-stream", &file_bytes)
} else {
let image_mode = if use_original_detail {
PromptImageMode::Original
} else {
PromptImageMode::ResizeToFit
};
load_for_prompt_bytes(abs_path.as_path(), file_bytes, image_mode)
.map_err(|error| {
FunctionCallError::RespondToModel(format!(
"unable to process image at `{}`: {error}",
abs_path.display()
))
})?
.into_data_url()
};
// The history insertion path owns image decoding and resizing.
let image_url = data_url_from_bytes("application/octet-stream", &file_bytes);
let item = TurnItem::ImageView(ImageViewItem {
id: call_id,
@@ -420,9 +401,6 @@ mod tests {
})
.await;
let Err(FunctionCallError::RespondToModel(message)) = result else {
panic!("expected image processing error");
};
assert!(message.contains("unable to process image"), "{message}");
result.expect("explicit high detail should be accepted");
}
}
+33 -9
View File
@@ -84,7 +84,9 @@ impl ToolOrchestrator {
};
let attempt_with_network_approval = SandboxAttempt {
sandbox: attempt.sandbox,
sandbox_requested: attempt.sandbox_requested,
permissions: attempt.permissions,
exec_server_permissions: attempt.exec_server_permissions,
enforce_managed_network: attempt.enforce_managed_network,
manager: attempt.manager,
sandbox_cwd: attempt.sandbox_cwd,
@@ -225,16 +227,27 @@ impl ToolOrchestrator {
&file_system_sandbox_policy,
);
let managed_network_active = turn_ctx.network.is_some();
let initial_sandbox = match sandbox_override {
SandboxOverride::BypassSandboxFirstAttempt => SandboxType::None,
SandboxOverride::NoOverride => self.sandbox.select_initial(
let sandbox_preference = tool.sandbox_preference();
let sandbox_requested = match sandbox_override {
SandboxOverride::BypassSandboxFirstAttempt => false,
SandboxOverride::NoOverride => self.sandbox.should_sandbox(
&file_system_sandbox_policy,
network_sandbox_policy,
tool.sandbox_preference(),
turn_ctx.windows_sandbox_level,
sandbox_preference,
managed_network_active,
),
};
let initial_sandbox = if sandbox_requested {
self.sandbox.select_initial(
&file_system_sandbox_policy,
network_sandbox_policy,
sandbox_preference,
turn_ctx.windows_sandbox_level,
managed_network_active,
)
} else {
SandboxType::None
};
// Platform-specific flag gating is handled by SandboxManager::select_initial.
let use_legacy_landlock = turn_ctx.config.features.use_legacy_landlock();
@@ -246,7 +259,9 @@ impl ToolOrchestrator {
let workspace_roots = turn_ctx.config.effective_workspace_roots();
let initial_attempt = SandboxAttempt {
sandbox: initial_sandbox,
sandbox_requested,
permissions: &turn_ctx.permission_profile,
exec_server_permissions: turn_ctx.config.permissions.permission_profile(),
enforce_managed_network: managed_network_active,
manager: &self.sandbox,
sandbox_cwd: &sandbox_policy_cwd,
@@ -401,16 +416,23 @@ impl ToolOrchestrator {
.await?;
}
let retry_sandbox = if unsandboxed_allowed {
SandboxType::None
} else {
let retry_sandbox_requested = !unsandboxed_allowed
&& self.sandbox.should_sandbox(
&file_system_sandbox_policy,
network_sandbox_policy,
sandbox_preference,
managed_network_active,
);
let retry_sandbox = if retry_sandbox_requested {
self.sandbox.select_initial(
&file_system_sandbox_policy,
network_sandbox_policy,
tool.sandbox_preference(),
sandbox_preference,
turn_ctx.windows_sandbox_level,
managed_network_active,
)
} else {
SandboxType::None
};
let retry_codex_linux_sandbox_exe = if unsandboxed_allowed {
None
@@ -419,7 +441,9 @@ impl ToolOrchestrator {
};
let retry_attempt = SandboxAttempt {
sandbox: retry_sandbox,
sandbox_requested: retry_sandbox_requested,
permissions: &turn_ctx.permission_profile,
exec_server_permissions: turn_ctx.config.permissions.permission_profile(),
enforce_managed_network: managed_network_active,
manager: &self.sandbox,
sandbox_cwd: &sandbox_policy_cwd,
+6 -4
View File
@@ -157,7 +157,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,
internal_chat_message_metadata_passthrough: None,
})?
.expect("function_call should produce a tool call");
@@ -340,11 +340,13 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
text: "extension history".to_string(),
}],
phase: None,
metadata: None,
internal_chat_message_metadata_passthrough: None,
};
session
.record_conversation_items(&turn, std::slice::from_ref(&history_item))
.await;
let mut expected_history_item = history_item.clone();
expected_history_item.set_turn_id_if_missing(&turn.sub_id);
let router = ToolRouter::from_turn_context(
&turn,
@@ -376,7 +378,7 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
namespace: Some("extension/".to_string()),
arguments: json!({ "message": "hello" }).to_string(),
call_id: "call-extension".to_string(),
metadata: None,
internal_chat_message_metadata_passthrough: None,
})?
.expect("function_call should produce a tool call");
let result = router
@@ -404,7 +406,7 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
json!({
"arguments": { "message": "hello" },
"callId": "call-extension",
"conversationHistory": [history_item],
"conversationHistory": [expected_history_item],
"ok": true,
})
);
@@ -220,7 +220,9 @@ async fn file_system_sandbox_context_uses_active_attempt() {
let sandbox_policy_cwd = PathUri::from_abs_path(&path);
let attempt = SandboxAttempt {
sandbox: SandboxType::MacosSeatbelt,
sandbox_requested: true,
permissions: &permissions,
exec_server_permissions: &permissions,
enforce_managed_network: false,
manager: &manager,
sandbox_cwd: &sandbox_policy_cwd,
@@ -286,7 +288,9 @@ async fn no_sandbox_attempt_has_no_file_system_context() {
let sandbox_policy_cwd = PathUri::from_abs_path(&path);
let attempt = SandboxAttempt {
sandbox: SandboxType::None,
sandbox_requested: false,
permissions: &permissions,
exec_server_permissions: &permissions,
enforce_managed_network: false,
manager: &manager,
sandbox_cwd: &sandbox_policy_cwd,
@@ -106,7 +106,9 @@ async fn explicit_escalation_prepares_exec_without_managed_network() -> anyhow::
let manager = SandboxManager::new();
let attempt = SandboxAttempt {
sandbox: SandboxType::None,
sandbox_requested: false,
permissions: &permissions,
exec_server_permissions: &permissions,
enforce_managed_network: false,
manager: &manager,
sandbox_cwd: &sandbox_policy_cwd,
@@ -166,6 +166,8 @@ pub(super) async fn try_run_zsh_fork(
network_sandbox_policy,
windows_sandbox_filesystem_overrides: _windows_sandbox_filesystem_overrides,
arg0,
exec_server_sandbox: _,
exec_server_enforce_managed_network: _,
} = sandbox_exec_request;
let ParsedShellCommand { script, login, .. } = extract_shell_script(&command)?;
let effective_timeout = Duration::from_millis(
@@ -898,6 +900,8 @@ impl CoreShellCommandExecutor {
network_sandbox_policy: self.network_sandbox_policy,
windows_sandbox_filesystem_overrides: None,
arg0: self.arg0.clone(),
exec_server_sandbox: None,
exec_server_enforce_managed_network: false,
},
/*stdout_stream*/ None,
after_spawn,

Some files were not shown because too many files have changed in this diff Show More