Merge remote-tracking branch 'origin/main' into nornagon/codex/thread-artifacts-runtime

# Conflicts:
#	codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts
#	codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts
#	codex-rs/app-server/src/request_processors.rs
#	codex-rs/core/src/tools/spec_plan.rs
#	codex-rs/state/src/runtime/threads.rs
#	codex-rs/thread-store/src/in_memory.rs
#	codex-rs/thread-store/src/local/list_threads.rs
#	codex-rs/thread-store/src/local/mod.rs
#	codex-rs/tui/src/lib.rs
This commit is contained in:
Jeremy Rose
2026-06-16 15:16:50 -07:00
1692 changed files with 132109 additions and 41779 deletions
+1 -1
View File
@@ -2,6 +2,6 @@ load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "protocol",
crate_name = "codex_protocol",
compile_data = glob(["src/prompts/**/*.md"]),
crate_name = "codex_protocol",
)
+3 -1
View File
@@ -20,6 +20,7 @@ codex-execpolicy = { workspace = true }
codex-network-proxy = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-image = { workspace = true }
codex-utils-path-uri = { workspace = true }
codex-utils-string = { workspace = true }
encoding_rs = { workspace = true }
globset = { workspace = true }
@@ -59,4 +60,5 @@ tempfile = { workspace = true }
[package.metadata.cargo-shear]
# Required because:
# `icu_provider`: contains a required `sync` feature for `icu_decimal`
ignored = ["icu_provider"]
# `strum`: is referenced by generated `EnumIter` derive implementations
ignored = ["icu_provider", "strum"]
+15 -2
View File
@@ -34,8 +34,21 @@ pub enum PlanType {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProviderAccount {
ApiKey,
Chatgpt { email: String, plan_type: PlanType },
AmazonBedrock,
Chatgpt {
email: String,
plan_type: PlanType,
},
AmazonBedrock {
credential_source: AmazonBedrockCredentialSource,
},
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub enum AmazonBedrockCredentialSource {
CodexManaged,
AwsManaged,
}
impl PlanType {
+30
View File
@@ -0,0 +1,30 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
/// A user-selected root that can expose one or more runtime capabilities.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct SelectedCapabilityRoot {
/// Stable identifier supplied by the capability selection platform.
pub id: String,
/// Where the selected root can be resolved.
pub location: CapabilityRootLocation,
}
/// Location used to resolve a selected capability root.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type")]
#[ts(export_to = "v2/")]
pub enum CapabilityRootLocation {
/// A path owned by an execution environment.
Environment {
#[serde(rename = "environmentId")]
#[ts(rename = "environmentId")]
environment_id: String,
path: String,
},
}
+9 -6
View File
@@ -166,8 +166,8 @@ pub enum ApprovalsReviewer {
#[default]
#[serde(rename = "user")]
User,
#[serde(rename = "guardian_subagent", alias = "auto_review")]
#[strum(serialize = "guardian_subagent")]
#[serde(rename = "auto_review", alias = "guardian_subagent")]
#[strum(serialize = "auto_review")]
AutoReview,
}
@@ -635,7 +635,7 @@ impl CollaborationMode {
}
pub fn reasoning_effort(&self) -> Option<ReasoningEffort> {
self.settings_ref().reasoning_effort
self.settings_ref().reasoning_effort.clone()
}
/// Updates the collaboration mode with new model and/or effort values.
@@ -654,7 +654,7 @@ impl CollaborationMode {
let settings = self.settings_ref();
let updated_settings = Settings {
model: model.unwrap_or_else(|| settings.model.clone()),
reasoning_effort: effort.unwrap_or(settings.reasoning_effort),
reasoning_effort: effort.unwrap_or_else(|| settings.reasoning_effort.clone()),
developer_instructions: developer_instructions
.unwrap_or_else(|| settings.developer_instructions.clone()),
};
@@ -676,7 +676,10 @@ impl CollaborationMode {
mode: mask.mode.unwrap_or(self.mode),
settings: Settings {
model: mask.model.clone().unwrap_or_else(|| settings.model.clone()),
reasoning_effort: mask.reasoning_effort.unwrap_or(settings.reasoning_effort),
reasoning_effort: mask
.reasoning_effort
.clone()
.unwrap_or_else(|| settings.reasoning_effort.clone()),
developer_instructions: mask
.developer_instructions
.clone()
@@ -757,7 +760,7 @@ mod tests {
);
assert_eq!(
serde_json::to_string(&ApprovalsReviewer::AutoReview).expect("serialize reviewer"),
"\"guardian_subagent\""
"\"auto_review\""
);
for value in ["user", "auto_review", "guardian_subagent"] {
+115 -83
View File
@@ -2,21 +2,46 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::de::Error as _;
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use ts_rs::TS;
#[derive(Debug, Clone, Serialize, PartialEq, JsonSchema, TS)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type", export_to = "v2/")]
pub enum DynamicToolSpec {
Function(DynamicToolFunctionSpec),
Namespace(DynamicToolNamespaceSpec),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct DynamicToolSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[ts(export_to = "v2/")]
pub struct DynamicToolFunctionSpec {
pub name: String,
pub description: String,
pub input_schema: JsonValue,
#[serde(default)]
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub defer_loading: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct DynamicToolNamespaceSpec {
pub name: String,
pub description: String,
pub tools: Vec<DynamicToolNamespaceTool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type", export_to = "v2/")]
pub enum DynamicToolNamespaceTool {
Function(DynamicToolFunctionSpec),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct DynamicToolCallRequest {
@@ -47,9 +72,11 @@ pub enum DynamicToolCallOutputContentItem {
InputImage { image_url: String },
}
/// Former flat `SessionMeta` shape, including the old `exposeToContext` flag.
/// Kept so new builds can resume sessions written before explicit namespaces.
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DynamicToolSpecDe {
struct LegacyDynamicToolSpec {
namespace: Option<String>,
name: String,
description: String,
@@ -58,84 +85,89 @@ struct DynamicToolSpecDe {
expose_to_context: Option<bool>,
}
impl<'de> Deserialize<'de> for DynamicToolSpec {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let DynamicToolSpecDe {
namespace,
name,
description,
input_schema,
defer_loading,
expose_to_context,
} = DynamicToolSpecDe::deserialize(deserializer)?;
Ok(Self {
namespace,
name,
description,
input_schema,
defer_loading: defer_loading
.unwrap_or_else(|| expose_to_context.map(|visible| !visible).unwrap_or(false)),
})
pub fn normalize_dynamic_tool_specs(
values: Vec<JsonValue>,
) -> Result<Vec<DynamicToolSpec>, serde_json::Error> {
let has_legacy_fields = |value: &JsonValue| {
value.get("namespace").is_some()
|| value.get("exposeToContext").is_some()
|| value.get("type").is_none()
};
let has_legacy_format = values.iter().any(|value| {
has_legacy_fields(value)
|| value
.get("tools")
.and_then(JsonValue::as_array)
.is_some_and(|tools| tools.iter().any(&has_legacy_fields))
});
let has_canonical_format = values.iter().any(|value| value.get("type").is_some());
if has_legacy_format && has_canonical_format {
return Err(serde_json::Error::custom(
"dynamic tools must use either canonical or legacy format consistently",
));
}
if !has_legacy_format {
return values.into_iter().map(serde_json::from_value).collect();
}
}
#[cfg(test)]
mod tests {
use super::DynamicToolSpec;
use pretty_assertions::assert_eq;
use serde_json::json;
#[test]
fn dynamic_tool_spec_deserializes_defer_loading() {
let value = json!({
"name": "lookup_ticket",
"description": "Fetch a ticket",
"inputSchema": {
"type": "object",
"properties": {
"id": { "type": "string" }
}
},
"deferLoading": true,
});
let actual: DynamicToolSpec = serde_json::from_value(value).expect("deserialize");
assert_eq!(
actual,
DynamicToolSpec {
namespace: None,
name: "lookup_ticket".to_string(),
description: "Fetch a ticket".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"id": { "type": "string" }
}
let tools = values
.into_iter()
.map(|value| {
let tool: LegacyDynamicToolSpec = serde_json::from_value(value)?;
let function = DynamicToolFunctionSpec {
name: tool.name,
description: tool.description,
input_schema: tool.input_schema,
defer_loading: tool.defer_loading.unwrap_or_else(|| {
tool.expose_to_context
.map(|visible| !visible)
.unwrap_or(false)
}),
defer_loading: true,
}
);
}
#[test]
fn dynamic_tool_spec_legacy_expose_to_context_inverts_to_defer_loading() {
let value = json!({
"name": "lookup_ticket",
"description": "Fetch a ticket",
"inputSchema": {
"type": "object",
"properties": {}
},
"exposeToContext": false,
});
let actual: DynamicToolSpec = serde_json::from_value(value).expect("deserialize");
assert!(actual.defer_loading);
}
};
Ok((tool.namespace, function))
})
.collect::<Result<Vec<_>, serde_json::Error>>()?;
Ok(group_dynamic_tools_by_namespace(tools))
}
pub fn group_dynamic_tools_by_namespace(
tools: Vec<(Option<String>, DynamicToolFunctionSpec)>,
) -> Vec<DynamicToolSpec> {
let mut grouped_tools = Vec::with_capacity(tools.len());
let mut namespace_indices = HashMap::<String, usize>::new();
for (namespace, function) in tools {
let Some(namespace) = namespace else {
grouped_tools.push(DynamicToolSpec::Function(function));
continue;
};
let function = DynamicToolNamespaceTool::Function(function);
if let Some(index) = namespace_indices.get(&namespace).copied() {
let DynamicToolSpec::Namespace(namespace) = &mut grouped_tools[index] else {
unreachable!("namespace index must point to a namespace");
};
namespace.tools.push(function);
continue;
}
namespace_indices.insert(namespace.clone(), grouped_tools.len());
grouped_tools.push(DynamicToolSpec::Namespace(DynamicToolNamespaceSpec {
name: namespace,
description: String::new(),
tools: vec![function],
}));
}
grouped_tools
}
pub fn deserialize_dynamic_tool_specs<'de, D>(
deserializer: D,
) -> Result<Option<Vec<DynamicToolSpec>>, D::Error>
where
D: Deserializer<'de>,
{
let Some(values) = Option::<Vec<JsonValue>>::deserialize(deserializer)? else {
return Ok(None);
};
normalize_dynamic_tool_specs(values)
.map(Some)
.map_err(D::Error::custom)
}
+1
View File
@@ -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,
}
+10
View File
@@ -47,6 +47,7 @@ pub enum TurnItem {
Reasoning(ReasoningItem),
WebSearch(WebSearchItem),
ImageView(ImageViewItem),
Sleep(SleepItem),
ImageGeneration(ImageGenerationItem),
FileChange(FileChangeItem),
McpToolCall(McpToolCallItem),
@@ -140,6 +141,12 @@ pub struct ImageViewItem {
pub path: AbsolutePathBuf,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]
pub struct SleepItem {
pub id: String,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
pub struct ImageGenerationItem {
pub id: String,
@@ -391,6 +398,7 @@ pub fn build_hook_prompt_message(fragments: &[HookPromptFragment]) -> Option<Res
role: "user".to_string(),
content,
phase: None,
metadata: None,
})
}
@@ -575,6 +583,7 @@ impl TurnItem {
TurnItem::Reasoning(item) => item.id.clone(),
TurnItem::WebSearch(item) => item.id.clone(),
TurnItem::ImageView(item) => item.id.clone(),
TurnItem::Sleep(item) => item.id.clone(),
TurnItem::ImageGeneration(item) => item.id.clone(),
TurnItem::FileChange(item) => item.id.clone(),
TurnItem::McpToolCall(item) => item.id.clone(),
@@ -595,6 +604,7 @@ impl TurnItem {
path: item.path.clone(),
})]
}
TurnItem::Sleep(_) => Vec::new(),
TurnItem::ImageGeneration(item) => vec![item.as_legacy_event()],
TurnItem::FileChange(item) => item
.as_legacy_end_event(String::new())
+1
View File
@@ -9,6 +9,7 @@ pub use session_id::SessionId;
pub use thread_id::ThreadId;
pub use tool_name::ToolName;
pub mod approvals;
pub mod capabilities;
pub mod config_types;
pub mod dynamic_tools;
pub mod error;
File diff suppressed because it is too large Load Diff
+169 -68
View File
@@ -3,15 +3,22 @@
//! These types are serialized across core, TUI, app-server, and SDK boundaries, so field defaults
//! are used to preserve compatibility when older payloads omit newly introduced attributes.
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use schemars::JsonSchema;
use schemars::r#gen::SchemaGenerator;
use schemars::schema::InstanceType;
use schemars::schema::Metadata;
use schemars::schema::Schema;
use schemars::schema::SchemaObject;
use schemars::schema::StringValidation;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::Serializer;
use serde::de::DeserializeOwned;
use strum::IntoEnumIterator;
use serde::de::Error;
use strum_macros::Display;
use strum_macros::EnumIter;
use tracing::warn;
@@ -22,28 +29,14 @@ 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";
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
#[derive(
Debug,
Serialize,
Deserialize,
Default,
Clone,
Copy,
PartialEq,
Eq,
Display,
JsonSchema,
TS,
EnumIter,
Hash,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
#[derive(Debug, Default, Clone, PartialEq, Eq, TS, Hash)]
#[ts(type = "string")]
pub enum ReasoningEffort {
None,
Minimal,
@@ -52,14 +45,87 @@ pub enum ReasoningEffort {
Medium,
High,
XHigh,
/// A model-defined effort value that this client does not know yet.
Custom(String),
}
impl ReasoningEffort {
/// Returns the exact value used on the wire.
pub fn as_str(&self) -> &str {
match self {
Self::None => "none",
Self::Minimal => "minimal",
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::XHigh => "xhigh",
Self::Custom(effort) => effort,
}
}
}
impl fmt::Display for ReasoningEffort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl JsonSchema for ReasoningEffort {
fn schema_name() -> String {
"ReasoningEffort".to_string()
}
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
metadata: Some(Box::new(Metadata {
description: Some(
"A non-empty reasoning effort value advertised by the model.".to_string(),
),
..Default::default()
})),
string: Some(Box::new(StringValidation {
min_length: Some(1),
..Default::default()
})),
..Default::default()
})
}
}
impl Serialize for ReasoningEffort {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for ReasoningEffort {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let effort = String::deserialize(deserializer)?;
effort.parse().map_err(D::Error::custom)
}
}
impl FromStr for ReasoningEffort {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_value(serde_json::Value::String(s.to_string()))
.map_err(|_| format!("invalid reasoning_effort: {s}"))
match s {
"none" => Ok(Self::None),
"minimal" => Ok(Self::Minimal),
"low" => Ok(Self::Low),
"medium" => Ok(Self::Medium),
"high" => Ok(Self::High),
"xhigh" => Ok(Self::XHigh),
"" => Err("reasoning_effort must not be empty".to_string()),
effort => Ok(Self::Custom(effort.to_string())),
}
}
}
@@ -107,7 +173,6 @@ pub struct ReasoningEffortPreset {
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
pub struct ModelUpgrade {
pub id: String,
pub reasoning_effort_mapping: Option<HashMap<ReasoningEffort, ReasoningEffort>>,
pub migration_config_key: String,
pub model_link: Option<String>,
pub upgrade_copy: Option<String>,
@@ -324,6 +389,9 @@ pub struct ModelInfo {
/// context window when available.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_compact_token_limit: Option<i64>,
/// Opaque identifier for compaction-compatible model configurations.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub comp_hash: Option<String>,
/// Percentage of the context window considered usable for inputs, after
/// reserving headroom for system prompts, tool overhead, and model output.
#[serde(default = "default_effective_context_window_percent")]
@@ -339,6 +407,8 @@ pub struct ModelInfo {
pub used_fallback_model_metadata: bool,
#[serde(default)]
pub supports_search_tool: bool,
#[serde(default)]
pub use_responses_lite: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_review_model_override: Option<String>,
#[serde(
@@ -347,6 +417,12 @@ pub struct ModelInfo {
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 {
@@ -494,9 +570,6 @@ impl From<ModelInfo> for ModelPreset {
is_default: false, // default is the highest priority available model
upgrade: info.upgrade.as_ref().map(|upgrade| ModelUpgrade {
id: upgrade.model.clone(),
reasoning_effort_mapping: reasoning_effort_mapping_from_presets(
&info.supported_reasoning_levels,
),
migration_config_key: info.slug.clone(),
// todo(aibrahim): add the model link here.
model_link: None,
@@ -564,47 +637,12 @@ impl ModelPreset {
}
}
fn reasoning_effort_mapping_from_presets(
presets: &[ReasoningEffortPreset],
) -> Option<HashMap<ReasoningEffort, ReasoningEffort>> {
if presets.is_empty() {
return None;
}
// Map every canonical effort to the closest supported effort for the new model.
let supported: Vec<ReasoningEffort> = presets.iter().map(|p| p.effort).collect();
let mut map = HashMap::new();
for effort in ReasoningEffort::iter() {
let nearest = nearest_effort(effort, &supported);
map.insert(effort, nearest);
}
Some(map)
}
fn effort_rank(effort: ReasoningEffort) -> i32 {
match effort {
ReasoningEffort::None => 0,
ReasoningEffort::Minimal => 1,
ReasoningEffort::Low => 2,
ReasoningEffort::Medium => 3,
ReasoningEffort::High => 4,
ReasoningEffort::XHigh => 5,
}
}
fn nearest_effort(target: ReasoningEffort, supported: &[ReasoningEffort]) -> ReasoningEffort {
let target_rank = effort_rank(target);
supported
.iter()
.copied()
.min_by_key(|candidate| (effort_rank(*candidate) - target_rank).abs())
.unwrap_or(target)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use serde_json::from_str;
use serde_json::to_string;
fn test_model(spec: Option<ModelMessages>) -> ModelInfo {
ModelInfo {
@@ -636,13 +674,16 @@ mod tests {
context_window: None,
max_context_window: None,
auto_compact_token_limit: None,
comp_hash: None,
effective_context_window_percent: 95,
experimental_supported_tools: vec![],
input_modalities: default_input_modalities(),
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
auto_review_model_override: None,
tool_mode: None,
multi_agent_version: None,
}
}
@@ -655,16 +696,58 @@ mod tests {
}
#[test]
fn reasoning_effort_from_str_accepts_known_values() {
assert_eq!("high".parse(), Ok(ReasoningEffort::High));
assert_eq!("minimal".parse(), Ok(ReasoningEffort::Minimal));
fn reasoning_effort_accepts_known_and_custom_values() {
let custom = ReasoningEffort::Custom("max".to_string());
let deserialized = from_str::<ReasoningEffort>(r#""max""#)
.expect("custom reasoning effort should deserialize");
let serialized = to_string(&custom).expect("custom reasoning effort should serialize");
assert_eq!(
(
"high".parse(),
"max".parse(),
deserialized,
serialized,
custom.to_string(),
),
(
Ok(ReasoningEffort::High),
Ok(custom.clone()),
custom,
r#""max""#.to_string(),
"max".to_string(),
)
);
}
#[test]
fn reasoning_effort_from_str_rejects_unknown_values() {
fn reasoning_effort_rejects_empty_values() {
assert_eq!(
"unsupported".parse::<ReasoningEffort>(),
Err("invalid reasoning_effort: unsupported".to_string())
"".parse::<ReasoningEffort>(),
Err("reasoning_effort must not be empty".to_string())
);
}
#[test]
fn reasoning_effort_json_schema_is_an_open_string() {
let mut effort_generator = SchemaGenerator::default();
assert_eq!(
ReasoningEffort::json_schema(&mut effort_generator),
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
metadata: Some(Box::new(Metadata {
description: Some(
"A non-empty reasoning effort value advertised by the model.".to_string(),
),
..Default::default()
})),
string: Some(Box::new(StringValidation {
min_length: Some(1),
..Default::default()
})),
..Default::default()
})
);
}
@@ -860,6 +943,8 @@ mod tests {
assert!(!model.supports_image_detail_original);
assert_eq!(model.web_search_tool_type, WebSearchToolType::Text);
assert!(!model.supports_search_tool);
assert!(!model.use_responses_lite);
assert_eq!(model.comp_hash, None);
assert_eq!(model.auto_review_model_override, None);
assert_eq!(model.tool_mode, None);
}
@@ -901,6 +986,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 {
+65 -14
View File
@@ -6,6 +6,7 @@ use std::path::PathBuf;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::canonicalize_preserving_symlinks;
use codex_utils_path_uri::PathUri;
use globset::GlobBuilder;
use globset::GlobMatcher;
use schemars::JsonSchema;
@@ -176,11 +177,31 @@ impl FileSystemSpecialPath {
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)]
pub struct FileSystemSandboxEntry {
pub path: FileSystemPath,
pub struct FileSystemSandboxEntry<PathType = AbsolutePathBuf> {
pub path: FileSystemPath<PathType>,
pub access: FileSystemAccessMode,
}
impl From<FileSystemSandboxEntry<AbsolutePathBuf>> for FileSystemSandboxEntry<PathUri> {
fn from(value: FileSystemSandboxEntry<AbsolutePathBuf>) -> Self {
FileSystemSandboxEntry {
path: value.path.into(),
access: value.access,
}
}
}
impl TryFrom<FileSystemSandboxEntry<PathUri>> for FileSystemSandboxEntry<AbsolutePathBuf> {
type Error = io::Error;
fn try_from(value: FileSystemSandboxEntry<PathUri>) -> Result<Self, Self::Error> {
Ok(FileSystemSandboxEntry {
path: value.path.try_into()?,
access: value.access,
})
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS,
)]
@@ -338,9 +359,9 @@ enum InvalidDenyReadGlobBehavior {
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
#[ts(tag = "type")]
pub enum FileSystemPath {
pub enum FileSystemPath<PathType = AbsolutePathBuf> {
Path {
path: AbsolutePathBuf,
path: PathType,
},
/// A git-style glob pattern. Pattern entries currently support
/// FileSystemAccessMode::Deny only.
@@ -352,28 +373,58 @@ pub enum FileSystemPath {
},
}
impl From<FileSystemPath<AbsolutePathBuf>> for FileSystemPath<PathUri> {
fn from(value: FileSystemPath<AbsolutePathBuf>) -> Self {
match value {
FileSystemPath::Path { path } => FileSystemPath::Path {
path: PathUri::from_abs_path(&path),
},
FileSystemPath::GlobPattern { pattern } => FileSystemPath::GlobPattern { pattern },
FileSystemPath::Special { value } => FileSystemPath::Special { value },
}
}
}
impl TryFrom<FileSystemPath<PathUri>> for FileSystemPath<AbsolutePathBuf> {
type Error = io::Error;
fn try_from(value: FileSystemPath<PathUri>) -> Result<Self, Self::Error> {
Ok(match value {
FileSystemPath::Path { path } => FileSystemPath::Path {
path: path.to_abs_path()?,
},
FileSystemPath::GlobPattern { pattern } => FileSystemPath::GlobPattern { pattern },
FileSystemPath::Special { value } => FileSystemPath::Special { value },
})
}
}
const PROJECT_ROOTS_GLOB_PATTERN_PREFIX: &str = "codex-project-roots://";
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,
File diff suppressed because it is too large Load Diff
@@ -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")]
@@ -31,6 +31,9 @@ pub struct RequestUserInputQuestion {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct RequestUserInputArgs {
pub questions: Vec<RequestUserInputQuestion>,
#[serde(rename = "autoResolutionMs", skip_serializing_if = "Option::is_none")]
#[schemars(rename = "autoResolutionMs")]
pub auto_resolution_ms: Option<u64>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
@@ -52,4 +55,7 @@ pub struct RequestUserInputEvent {
#[serde(default)]
pub turn_id: String,
pub questions: Vec<RequestUserInputQuestion>,
#[serde(rename = "autoResolutionMs", skip_serializing_if = "Option::is_none")]
#[schemars(rename = "autoResolutionMs")]
pub auto_resolution_ms: Option<u64>,
}