mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
Merge remote-tracking branch 'origin/main' into codex/interrupt-hook-contract-tightening
This commit is contained in:
@@ -35,6 +35,7 @@ fn rate_limit_snapshot() -> RateLimitSnapshot {
|
||||
resets_at: Some(secondary_reset_at),
|
||||
}),
|
||||
credits: None,
|
||||
individual_limit: None,
|
||||
plan_type: None,
|
||||
rate_limit_reached_type: None,
|
||||
}
|
||||
|
||||
@@ -374,16 +374,9 @@ impl Default for PermissionProfile {
|
||||
impl PermissionProfile {
|
||||
/// Managed read-only filesystem access with restricted network access.
|
||||
pub fn read_only() -> Self {
|
||||
let file_system = FileSystemSandboxPolicy::read_only();
|
||||
Self::Managed {
|
||||
file_system: ManagedFileSystemPermissions::Restricted {
|
||||
entries: vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}],
|
||||
glob_scan_max_depth: None,
|
||||
},
|
||||
file_system: ManagedFileSystemPermissions::from_sandbox_policy(&file_system),
|
||||
network: NetworkSandboxPolicy::Restricted,
|
||||
}
|
||||
}
|
||||
@@ -1026,9 +1019,10 @@ pub fn local_image_label_text(label_number: usize) -> String {
|
||||
format!("[Image #{label_number}]")
|
||||
}
|
||||
|
||||
pub fn local_image_open_tag_text(label_number: usize) -> String {
|
||||
pub fn local_image_open_tag_text_with_path(label_number: usize, path: &std::path::Path) -> String {
|
||||
let label = local_image_label_text(label_number);
|
||||
format!("{LOCAL_IMAGE_OPEN_TAG_PREFIX}{label}{LOCAL_IMAGE_OPEN_TAG_SUFFIX}")
|
||||
let path = path.display();
|
||||
format!("{LOCAL_IMAGE_OPEN_TAG_PREFIX}{label} path=\"{path}\"{LOCAL_IMAGE_OPEN_TAG_SUFFIX}")
|
||||
}
|
||||
|
||||
pub fn is_local_image_open_tag_text(text: &str) -> bool {
|
||||
@@ -1087,7 +1081,7 @@ pub fn local_image_content_items_with_label_number(
|
||||
let mut items = Vec::with_capacity(3);
|
||||
if let Some(label_number) = label_number {
|
||||
items.push(ContentItem::InputText {
|
||||
text: local_image_open_tag_text(label_number),
|
||||
text: local_image_open_tag_text_with_path(label_number, path),
|
||||
});
|
||||
}
|
||||
items.push(ContentItem::InputImage {
|
||||
@@ -2873,7 +2867,7 @@ mod tests {
|
||||
detail: None,
|
||||
},
|
||||
UserInput::LocalImage {
|
||||
path: local_path,
|
||||
path: local_path.clone(),
|
||||
detail: None,
|
||||
},
|
||||
]);
|
||||
@@ -2890,7 +2884,10 @@ mod tests {
|
||||
assert_eq!(
|
||||
content.get(1),
|
||||
Some(&ContentItem::InputText {
|
||||
text: local_image_open_tag_text(/*label_number*/ 2),
|
||||
text: local_image_open_tag_text_with_path(
|
||||
/*label_number*/ 2,
|
||||
&local_path
|
||||
),
|
||||
})
|
||||
);
|
||||
assert!(matches!(
|
||||
@@ -2910,6 +2907,17 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_image_open_tag_preserves_path() {
|
||||
assert_eq!(
|
||||
local_image_open_tag_text_with_path(
|
||||
/*label_number*/ 1,
|
||||
std::path::Path::new(r#"/tmp/a&"<b>.png"#),
|
||||
),
|
||||
r#"<image name=[Image #1] path="/tmp/a&"<b>.png">"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_image_user_input_preserves_requested_detail() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
|
||||
@@ -22,6 +22,7 @@ use crate::config_types::ReasoningSummary;
|
||||
use crate::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use crate::config_types::ServiceTier;
|
||||
use crate::config_types::Verbosity;
|
||||
use crate::protocol::MultiAgentVersion;
|
||||
|
||||
const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}";
|
||||
pub const SPEED_TIER_FAST: &str = "fast";
|
||||
@@ -339,12 +340,20 @@ pub struct ModelInfo {
|
||||
pub used_fallback_model_metadata: bool,
|
||||
#[serde(default)]
|
||||
pub supports_search_tool: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub auto_review_model_override: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
deserialize_with = "deserialize_optional_model_selector"
|
||||
)]
|
||||
pub tool_mode: Option<ToolMode>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
deserialize_with = "deserialize_optional_model_selector"
|
||||
)]
|
||||
pub multi_agent_version: Option<MultiAgentVersion>,
|
||||
}
|
||||
|
||||
impl ModelInfo {
|
||||
@@ -639,7 +648,9 @@ mod tests {
|
||||
input_modalities: default_input_modalities(),
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -857,6 +868,7 @@ mod tests {
|
||||
assert!(!model.supports_image_detail_original);
|
||||
assert_eq!(model.web_search_tool_type, WebSearchToolType::Text);
|
||||
assert!(!model.supports_search_tool);
|
||||
assert_eq!(model.auto_review_model_override, None);
|
||||
assert_eq!(model.tool_mode, None);
|
||||
}
|
||||
|
||||
@@ -897,6 +909,22 @@ mod tests {
|
||||
assert!(!object.contains_key("tool_mode"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_info_treats_unknown_multi_agent_version_as_omitted() {
|
||||
let mut value =
|
||||
serde_json::to_value(test_model(/*spec*/ None)).expect("serialize test model");
|
||||
let object = value
|
||||
.as_object_mut()
|
||||
.expect("model info should be an object");
|
||||
object.insert(
|
||||
"multi_agent_version".to_string(),
|
||||
serde_json::Value::String("future_multi_agent_version".to_string()),
|
||||
);
|
||||
let model = serde_json::from_value::<ModelInfo>(value).expect("deserialize model info");
|
||||
|
||||
assert_eq!(model.multi_agent_version, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_context_window_prefers_context_window() {
|
||||
let model = ModelInfo {
|
||||
|
||||
@@ -358,22 +358,26 @@ pub fn project_roots_glob_pattern(subpath: &Path) -> String {
|
||||
format!("{PROJECT_ROOTS_GLOB_PATTERN_PREFIX}{}", subpath.display())
|
||||
}
|
||||
|
||||
fn read_only_file_system_entries() -> Vec<FileSystemSandboxEntry> {
|
||||
vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}]
|
||||
}
|
||||
|
||||
impl Default for FileSystemSandboxPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
kind: FileSystemSandboxKind::Restricted,
|
||||
glob_scan_max_depth: None,
|
||||
entries: vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}],
|
||||
}
|
||||
Self::read_only()
|
||||
}
|
||||
}
|
||||
|
||||
impl FileSystemSandboxPolicy {
|
||||
pub fn read_only() -> Self {
|
||||
Self::restricted(read_only_file_system_entries())
|
||||
}
|
||||
|
||||
pub fn unrestricted() -> Self {
|
||||
Self {
|
||||
kind: FileSystemSandboxKind::Unrestricted,
|
||||
|
||||
@@ -1398,6 +1398,7 @@ pub enum HookSource {
|
||||
SessionFlags,
|
||||
Plugin,
|
||||
CloudRequirements,
|
||||
CloudManagedConfig,
|
||||
LegacyManagedConfigFile,
|
||||
LegacyManagedConfigMdm,
|
||||
#[default]
|
||||
@@ -2009,6 +2010,7 @@ pub struct RateLimitSnapshot {
|
||||
pub primary: Option<RateLimitWindow>,
|
||||
pub secondary: Option<RateLimitWindow>,
|
||||
pub credits: Option<CreditsSnapshot>,
|
||||
pub individual_limit: Option<SpendControlLimitSnapshot>,
|
||||
pub plan_type: Option<crate::account::PlanType>,
|
||||
pub rate_limit_reached_type: Option<RateLimitReachedType>,
|
||||
}
|
||||
@@ -2058,6 +2060,14 @@ pub struct CreditsSnapshot {
|
||||
pub balance: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct SpendControlLimitSnapshot {
|
||||
pub limit: String,
|
||||
pub used: String,
|
||||
pub remaining_percent: i32,
|
||||
pub resets_at: i64,
|
||||
}
|
||||
|
||||
// Includes prompts, tools and space to call compact.
|
||||
const BASELINE_TOKENS: i64 = 12000;
|
||||
|
||||
@@ -2181,8 +2191,9 @@ pub struct UserMessageEvent {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub image_details: Vec<Option<ImageDetail>>,
|
||||
/// Local file paths sourced from `UserInput::LocalImage`. These are kept so
|
||||
/// the UI can reattach images when editing history, and should not be sent
|
||||
/// to the model or treated as API-ready URLs.
|
||||
/// the UI can reattach images when editing history. Local image prompts may
|
||||
/// include a display form of the path, but these should not be treated as
|
||||
/// API-ready URLs.
|
||||
#[serde(default)]
|
||||
pub local_images: Vec<std::path::PathBuf>,
|
||||
/// Detail hints for `local_images`, indexed in parallel. Missing entries
|
||||
@@ -2441,6 +2452,18 @@ impl InitialHistory {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_multi_agent_version(&self) -> Option<MultiAgentVersion> {
|
||||
match self {
|
||||
InitialHistory::New | InitialHistory::Cleared => None,
|
||||
InitialHistory::Resumed(resumed) => {
|
||||
multi_agent_version_from_items(&resumed.history, Some(resumed.conversation_id))
|
||||
}
|
||||
InitialHistory::Forked(items) => {
|
||||
multi_agent_version_from_items(items, /*thread_id*/ None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_resumed_session_sources(&self) -> Option<(SessionSource, Option<ThreadSource>)> {
|
||||
let meta = self.get_resumed_session_meta()?;
|
||||
Some((meta.source.clone(), meta.thread_source))
|
||||
@@ -2451,6 +2474,11 @@ impl InitialHistory {
|
||||
.and_then(|meta| meta.thread_source)
|
||||
}
|
||||
|
||||
pub fn get_resumed_parent_thread_id(&self) -> Option<ThreadId> {
|
||||
self.get_resumed_session_meta()
|
||||
.and_then(|meta| meta.parent_thread_id)
|
||||
}
|
||||
|
||||
fn get_resumed_session_meta(&self) -> Option<&SessionMeta> {
|
||||
match self {
|
||||
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => None,
|
||||
@@ -2706,6 +2734,39 @@ impl fmt::Display for InternalSessionSource {
|
||||
}
|
||||
}
|
||||
|
||||
fn multi_agent_version_from_items(
|
||||
items: &[RolloutItem],
|
||||
thread_id: Option<ThreadId>,
|
||||
) -> Option<MultiAgentVersion> {
|
||||
let session_meta_version = items.iter().rev().find_map(|item| match item {
|
||||
RolloutItem::SessionMeta(meta_line)
|
||||
if thread_id.is_none_or(|thread_id| meta_line.meta.id == thread_id) =>
|
||||
{
|
||||
meta_line.meta.multi_agent_version
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
|
||||
session_meta_version.or_else(|| {
|
||||
items.iter().rev().find_map(|item| match item {
|
||||
RolloutItem::TurnContext(turn_context) => turn_context.multi_agent_version,
|
||||
RolloutItem::SessionMeta(_)
|
||||
| RolloutItem::ResponseItem(_)
|
||||
| RolloutItem::Compacted(_)
|
||||
| RolloutItem::EventMsg(_) => None,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
pub enum MultiAgentVersion {
|
||||
Disabled,
|
||||
V1,
|
||||
V2,
|
||||
}
|
||||
|
||||
/// SessionMeta contains session-level data that doesn't correspond to a specific turn.
|
||||
///
|
||||
/// NOTE: There used to be an `instructions` field here, which stored user_instructions, but we
|
||||
@@ -2716,6 +2777,8 @@ pub struct SessionMeta {
|
||||
pub id: ThreadId,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub forked_from_id: Option<ThreadId>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub parent_thread_id: Option<ThreadId>,
|
||||
pub timestamp: String,
|
||||
pub cwd: PathBuf,
|
||||
pub originator: String,
|
||||
@@ -2743,6 +2806,8 @@ pub struct SessionMeta {
|
||||
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub memory_mode: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub multi_agent_version: Option<MultiAgentVersion>,
|
||||
}
|
||||
|
||||
impl Default for SessionMeta {
|
||||
@@ -2750,6 +2815,7 @@ impl Default for SessionMeta {
|
||||
SessionMeta {
|
||||
id: ThreadId::default(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp: String::new(),
|
||||
cwd: PathBuf::new(),
|
||||
originator: String::new(),
|
||||
@@ -2763,6 +2829,7 @@ impl Default for SessionMeta {
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
multi_agent_version: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2842,6 +2909,8 @@ pub struct TurnContextItem {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub collaboration_mode: Option<CollaborationMode>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub multi_agent_version: Option<MultiAgentVersion>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub realtime_active: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub effort: Option<ReasoningEffortConfig>,
|
||||
@@ -3421,6 +3490,8 @@ pub struct SessionConfiguredEvent {
|
||||
pub thread_id: ThreadId,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub forked_from_id: Option<ThreadId>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub parent_thread_id: Option<ThreadId>,
|
||||
/// Optional analytics source classification for this thread.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub thread_source: Option<ThreadSource>,
|
||||
@@ -3490,6 +3561,7 @@ impl<'de> Deserialize<'de> for SessionConfiguredEvent {
|
||||
#[serde(default)]
|
||||
thread_id: Option<ThreadId>,
|
||||
forked_from_id: Option<ThreadId>,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
#[serde(default)]
|
||||
thread_source: Option<ThreadSource>,
|
||||
#[serde(default)]
|
||||
@@ -3530,6 +3602,7 @@ impl<'de> Deserialize<'de> for SessionConfiguredEvent {
|
||||
session_id: wire.session_id,
|
||||
thread_id: wire.thread_id.unwrap_or_else(|| wire.session_id.into()),
|
||||
forked_from_id: wire.forked_from_id,
|
||||
parent_thread_id: wire.parent_thread_id,
|
||||
thread_source: wire.thread_source,
|
||||
thread_name: wire.thread_name,
|
||||
model: wire.model,
|
||||
@@ -5259,6 +5332,39 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_agent_version_uses_newest_present_session_meta_value() -> Result<()> {
|
||||
let thread_id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
|
||||
let older_meta = SessionMetaLine {
|
||||
meta: SessionMeta {
|
||||
id: thread_id,
|
||||
multi_agent_version: Some(MultiAgentVersion::V2),
|
||||
..Default::default()
|
||||
},
|
||||
git: None,
|
||||
};
|
||||
let newer_meta_without_version = SessionMetaLine {
|
||||
meta: SessionMeta {
|
||||
id: thread_id,
|
||||
multi_agent_version: None,
|
||||
..Default::default()
|
||||
},
|
||||
git: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
multi_agent_version_from_items(
|
||||
&[
|
||||
RolloutItem::SessionMeta(older_meta),
|
||||
RolloutItem::SessionMeta(newer_meta_without_version),
|
||||
],
|
||||
Some(thread_id),
|
||||
),
|
||||
Some(MultiAgentVersion::V2)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_context_item_serializes_network_when_present() -> Result<()> {
|
||||
let item = TurnContextItem {
|
||||
@@ -5285,6 +5391,7 @@ mod tests {
|
||||
model: "gpt-5".to_string(),
|
||||
personality: None,
|
||||
collaboration_mode: None,
|
||||
multi_agent_version: None,
|
||||
realtime_active: None,
|
||||
effort: None,
|
||||
summary: ReasoningSummaryConfig::Auto,
|
||||
@@ -5329,6 +5436,7 @@ mod tests {
|
||||
session_id,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
thread_name: None,
|
||||
model: "codex-mini-latest".to_string(),
|
||||
|
||||
@@ -48,6 +48,14 @@ impl From<AdditionalPermissionProfile> for RequestPermissionProfile {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct RequestPermissionsArgs {
|
||||
#[serde(
|
||||
default,
|
||||
rename = "environment_id",
|
||||
alias = "environmentId",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional)]
|
||||
pub environment_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
pub permissions: RequestPermissionProfile,
|
||||
@@ -71,6 +79,15 @@ pub struct RequestPermissionsEvent {
|
||||
/// Uses `#[serde(default)]` for backwards compatibility.
|
||||
#[serde(default)]
|
||||
pub turn_id: String,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "environmentId",
|
||||
alias = "environment_id",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional)]
|
||||
#[ts(rename = "environmentId")]
|
||||
pub environment_id: Option<String>,
|
||||
#[ts(type = "number")]
|
||||
pub started_at_ms: i64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
Reference in New Issue
Block a user