Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/ai/agent_sdk/ambient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ impl AmbientAgentRunner {
let harness_override = (args.harness != Harness::Oz).then_some(HarnessConfig {
harness_type: args.harness,
model_id: None,
reasoning_level: None,
});
let harness_auth_secrets = args.claude_auth_secret.clone().map(|name| {
crate::ai::ambient_agents::task::HarnessAuthSecretsConfig {
Expand Down
24 changes: 12 additions & 12 deletions app/src/ai/agent_sdk/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ use crate::{
CancellationReason, RenderableAIError, RequestFileEditsResult,
},
ambient_agents::{
conversation_output_status_from_conversation, AmbientAgentTaskId,
AmbientConversationStatus,
conversation_output_status_from_conversation, task::HarnessModelConfig,
AmbientAgentTaskId, AmbientConversationStatus,
},
blocklist::{
agent_view::AgentViewEntryOrigin,
Expand Down Expand Up @@ -245,8 +245,8 @@ pub struct AgentDriverOptions {
pub environment: Option<AmbientAgentEnvironment>,
/// Selected execution harness for this run.
pub selected_harness: Harness,
/// Model ID for the selected harness. Only used for non-Oz harnesses.
pub third_party_harness_model_id: Option<String>,
/// Model config for the selected harness. Only used for non-Oz harnesses.
pub third_party_harness_model_config: Option<HarnessModelConfig>,
/// Whether to skip end-of-run snapshot upload.
pub snapshot_disabled: Option<bool>,
/// End-of-run snapshot upload timeout override.
Expand Down Expand Up @@ -316,7 +316,7 @@ pub struct AgentDriver {
/// conversation's `parent_agent_id` field at register time so the
/// streamer recognizes the child role in driver-hosted processes.
parent_run_id: Option<String>,
third_party_harness_model_id: Option<String>,
third_party_harness_model_config: Option<HarnessModelConfig>,

/// Async writer that records `file` declarations for paths the agent creates or edits
/// via `RequestFileEdits`. `Some` only when `FeatureFlag::OzHandoff` is enabled, the run
Expand Down Expand Up @@ -518,7 +518,7 @@ impl AgentDriver {
cloud_providers,
environment,
selected_harness,
third_party_harness_model_id,
third_party_harness_model_config,
snapshot_disabled,
snapshot_upload_timeout,
snapshot_script_timeout,
Expand Down Expand Up @@ -573,7 +573,7 @@ impl AgentDriver {
));
env_vars.extend(harness_model_env_vars(
selected_harness,
third_party_harness_model_id.as_deref(),
third_party_harness_model_config.as_ref(),
));

// Signal to third-party harnesses (e.g. Claude Code) that we're in a sandbox
Expand Down Expand Up @@ -646,7 +646,7 @@ impl AgentDriver {
.unwrap_or(snapshot::DEFAULT_DECLARATIONS_SCRIPT_TIMEOUT),
run_conversation_id,
parent_run_id: parent_run_id_for_self,
third_party_harness_model_id,
third_party_harness_model_config,
snapshot_file_writer,
})
}
Expand Down Expand Up @@ -685,7 +685,7 @@ impl AgentDriver {
snapshot_script_timeout: snapshot::DEFAULT_DECLARATIONS_SCRIPT_TIMEOUT,
run_conversation_id: None,
parent_run_id: None,
third_party_harness_model_id: None,
third_party_harness_model_config: None,
snapshot_file_writer: None,
}
}
Expand Down Expand Up @@ -1960,11 +1960,11 @@ impl AgentDriver {
}
};

let (secrets, third_party_harness_model_id) = foreground
let (secrets, third_party_harness_model_config) = foreground
.spawn(|me, _| {
(
Arc::clone(&me.secrets),
me.third_party_harness_model_id.clone(),
me.third_party_harness_model_config.clone(),
)
})
.await
Expand Down Expand Up @@ -2011,7 +2011,7 @@ impl AgentDriver {
&resolved_env_vars,
&secrets_for_harness,
&resolved_mcp_servers,
third_party_harness_model_id.as_deref(),
third_party_harness_model_config.as_ref(),
)?
.into();

Expand Down
4 changes: 2 additions & 2 deletions app/src/ai/agent_sdk/driver/harness/claude_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use warp_cli::agent::Harness;
use warpui::{ModelHandle, ModelSpawner};

use crate::ai::agent::conversation::AIConversationId;
use crate::ai::ambient_agents::AmbientAgentTaskId;
use crate::ai::ambient_agents::{task::HarnessModelConfig, AmbientAgentTaskId};
use crate::ai::mcp::JSONTransportType;
use crate::server::server_api::harness_support::{upload_to_target, HarnessSupportClient};
use crate::server::server_api::ServerApi;
Expand Down Expand Up @@ -106,7 +106,7 @@ impl ThirdPartyHarness for ClaudeHarness {
resolved_env_vars: &HashMap<OsString, OsString>,
_resolved_secrets: &HashMap<String, ManagedSecretValue>,
resolved_mcp_servers: &HashMap<String, JSONMCPServer>,
_third_party_harness_model_id: Option<&str>,
_third_party_harness_model_config: Option<&HarnessModelConfig>,
) -> Result<Box<dyn HarnessRunner>, AgentDriverError> {
// Prepare the environment config files.
prepare_claude_environment_config(working_dir, resolved_env_vars).map_err(|error| {
Expand Down
42 changes: 31 additions & 11 deletions app/src/ai/agent_sdk/driver/harness/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use warp_cli::agent::Harness;
use warpui::{ModelHandle, ModelSpawner, SingletonEntity};

use crate::ai::agent::conversation::AIConversationId;
use crate::ai::ambient_agents::AmbientAgentTaskId;
use crate::ai::ambient_agents::{task::HarnessModelConfig, AmbientAgentTaskId};
use crate::ai::mcp::JSONTransportType;
use crate::server::server_api::harness_support::{upload_to_target, HarnessSupportClient};
use crate::server::server_api::ServerApi;
Expand Down Expand Up @@ -90,7 +90,7 @@ impl ThirdPartyHarness for CodexHarness {
resolved_env_vars: &HashMap<OsString, OsString>,
resolved_secrets: &HashMap<String, ManagedSecretValue>,
resolved_mcp_servers: &HashMap<String, JSONMCPServer>,
third_party_harness_model_id: Option<&str>,
third_party_harness_model_config: Option<&HarnessModelConfig>,
) -> Result<Box<dyn HarnessRunner>, AgentDriverError> {
// Prepare the environment config files.
prepare_codex_environment_config(
Expand All @@ -99,7 +99,7 @@ impl ThirdPartyHarness for CodexHarness {
resolved_env_vars,
resolved_secrets,
resolved_mcp_servers,
third_party_harness_model_id,
third_party_harness_model_config,
)
.map_err(|error| AgentDriverError::HarnessConfigSetupFailed {
harness: self.cli_agent().command_prefix().to_owned(),
Expand Down Expand Up @@ -452,6 +452,7 @@ const CODEX_TRUST_LEVEL_TRUSTED: &str = "trusted";
const CODEX_OPENAI_BASE_URL_KEY: &str = "openai_base_url";
const CODEX_CHECK_FOR_UPDATE_ON_STARTUP_KEY: &str = "check_for_update_on_startup";
const CODEX_MODEL_KEY: &str = "model";
const CODEX_MODEL_REASONING_EFFORT_KEY: &str = "model_reasoning_effort";
/// Target model for the `[notice.model_migrations]` table that suppresses Codex's
/// "choose a newer model" upgrade prompt at session launch. We stamp this for any
/// pinned model id (even when it already matches the target) so the unattended
Expand All @@ -466,7 +467,7 @@ fn prepare_codex_environment_config(
resolved_env_vars: &HashMap<OsString, OsString>,
resolved_secrets: &HashMap<String, ManagedSecretValue>,
resolved_mcp_servers: &HashMap<String, JSONMCPServer>,
third_party_harness_model_id: Option<&str>,
third_party_harness_model_config: Option<&HarnessModelConfig>,
) -> Result<()> {
let home_dir =
dirs::home_dir().ok_or_else(|| anyhow::anyhow!("could not determine home directory"))?;
Expand All @@ -490,7 +491,7 @@ fn prepare_codex_environment_config(
&codex_dir.join(CODEX_CONFIG_TOML_FILE_NAME),
working_dir,
resolved_mcp_servers,
third_party_harness_model_id,
third_party_harness_model_config,
openai_base_url.as_deref(),
)?;
Ok(())
Expand Down Expand Up @@ -634,14 +635,14 @@ fn resolve_openai_base_url_from_secret(
/// field), write it to config.toml. When absent, skip the key entirely so
/// Codex uses the provider's default global endpoint.
/// - update checks: disable Codex's startup update prompt for unattended runs.
/// - model override: when a non-default `third_party_harness_model_id` is
/// - model override: when a non-default harness model config is
/// supplied, write the top-level `model` key so Codex pins the chosen model
/// for new sessions.
fn prepare_codex_config_toml(
config_toml_path: &Path,
working_dir: &Path,
resolved_mcp_servers: &HashMap<String, JSONMCPServer>,
third_party_harness_model_id: Option<&str>,
third_party_harness_model_config: Option<&HarnessModelConfig>,
openai_base_url: Option<&str>,
) -> Result<()> {
let existing = match fs::read_to_string(config_toml_path) {
Expand All @@ -666,7 +667,8 @@ fn prepare_codex_config_toml(
set_codex_openai_base_url(&mut doc, url);
}
set_codex_check_for_update_on_startup(&mut doc, false);
set_codex_model(&mut doc, third_party_harness_model_id);
set_codex_model(&mut doc, third_party_harness_model_config);
set_codex_model_reasoning_effort(&mut doc, third_party_harness_model_config);

let canonical = working_dir.canonicalize().with_context(|| {
format!(
Expand Down Expand Up @@ -709,9 +711,27 @@ fn set_codex_check_for_update_on_startup(doc: &mut toml_edit::DocumentMut, enabl
doc[CODEX_CHECK_FOR_UPDATE_ON_STARTUP_KEY] = toml_edit::value(enabled);
}

fn set_codex_model(doc: &mut toml_edit::DocumentMut, third_party_harness_model_id: Option<&str>) {
let Some(model_id) =
third_party_harness_model_id.filter(|id| !id.is_empty() && *id != "default")
fn set_codex_model_reasoning_effort(
doc: &mut toml_edit::DocumentMut,
third_party_harness_model_config: Option<&HarnessModelConfig>,
) {
let Some(reasoning_level) = third_party_harness_model_config
.and_then(|config| config.reasoning_level.as_deref())
.filter(|level| !level.is_empty())
else {
doc.remove(CODEX_MODEL_REASONING_EFFORT_KEY);
return;
};
doc[CODEX_MODEL_REASONING_EFFORT_KEY] = toml_edit::value(reasoning_level);
}

fn set_codex_model(
doc: &mut toml_edit::DocumentMut,
third_party_harness_model_config: Option<&HarnessModelConfig>,
) {
let Some(model_id) = third_party_harness_model_config
.map(|config| config.model_id.as_str())
.filter(|id| !id.is_empty() && *id != "default")
else {
// No model specified or "default" selected — remove any pre-existing
// key so Codex uses its own default.
Expand Down
48 changes: 45 additions & 3 deletions app/src/ai/agent_sdk/driver/harness/codex_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,13 @@ fn read_codex_config(path: &std::path::Path) -> toml::Table {
toml::from_str(&content).unwrap()
}

fn harness_model_config(model_id: &str, reasoning_level: Option<&str>) -> HarnessModelConfig {
HarnessModelConfig {
model_id: model_id.to_string(),
reasoning_level: reasoning_level.map(str::to_string),
}
}

#[test]
fn prepare_codex_config_toml_writes_fresh_config() {
let tmp = TempDir::new().unwrap();
Expand Down Expand Up @@ -444,7 +451,7 @@ fn prepare_codex_config_toml_writes_model_when_specified() {
&config_path,
&working_dir,
&HashMap::new(),
Some("gpt-5.5"),
Some(&harness_model_config("gpt-5.5", None)),
None,
)
.unwrap();
Expand All @@ -470,7 +477,7 @@ fn prepare_codex_config_toml_writes_model_migration_for_older_model() {
&config_path,
&working_dir,
&HashMap::new(),
Some("gpt-5.2"),
Some(&harness_model_config("gpt-5.2", None)),
None,
)
.unwrap();
Expand All @@ -496,7 +503,7 @@ fn prepare_codex_config_toml_skips_model_for_default_sentinel() {
&config_path,
&working_dir,
&HashMap::new(),
Some("default"),
Some(&harness_model_config("default", None)),
None,
)
.unwrap();
Expand Down Expand Up @@ -534,6 +541,41 @@ fn prepare_codex_config_toml_skips_model_when_none() {
);
}

#[test]
fn prepare_codex_config_toml_writes_model_reasoning_effort_when_specified() {
let tmp = TempDir::new().unwrap();
let config_path = tmp.path().join("config.toml");
let working_dir = tmp.path().join("workspace");
fs::create_dir_all(&working_dir).unwrap();

prepare_codex_config_toml(
&config_path,
&working_dir,
&HashMap::new(),
Some(&harness_model_config("gpt-5.5", Some("medium"))),
None,
)
.unwrap();

let cfg = read_codex_config(&config_path);
assert_eq!(cfg["model"].as_str(), Some("gpt-5.5"));
assert_eq!(cfg["model_reasoning_effort"].as_str(), Some("medium"));
}

#[test]
fn prepare_codex_config_toml_removes_stale_model_reasoning_effort_when_none() {
let tmp = TempDir::new().unwrap();
let config_path = tmp.path().join("config.toml");
let working_dir = tmp.path().join("workspace");
fs::create_dir_all(&working_dir).unwrap();
fs::write(&config_path, "model_reasoning_effort = \"high\"\n").unwrap();

prepare_codex_config_toml(&config_path, &working_dir, &HashMap::new(), None, None).unwrap();

let cfg = read_codex_config(&config_path);
assert!(cfg.get("model_reasoning_effort").is_none());
}

#[test]
fn find_child_git_repos_returns_only_repo_children() {
let tmp = TempDir::new().unwrap();
Expand Down
4 changes: 2 additions & 2 deletions app/src/ai/agent_sdk/driver/harness/gemini.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use warp_cli::agent::Harness;
use warpui::{ModelHandle, ModelSpawner};

use crate::ai::agent::conversation::AIConversationId;
use crate::ai::ambient_agents::AmbientAgentTaskId;
use crate::ai::ambient_agents::{task::HarnessModelConfig, AmbientAgentTaskId};
use crate::server::server_api::harness_support::HarnessSupportClient;
use crate::server::server_api::ServerApi;
use crate::terminal::model::block::BlockId;
Expand Down Expand Up @@ -64,7 +64,7 @@ impl ThirdPartyHarness for GeminiHarness {
_resolved_env_vars: &HashMap<OsString, OsString>,
_resolved_secrets: &HashMap<String, ManagedSecretValue>,
_resolved_mcp_servers: &HashMap<String, JSONMCPServer>,
_third_party_harness_model_id: Option<&str>,
_third_party_harness_model_config: Option<&HarnessModelConfig>,
) -> Result<Box<dyn HarnessRunner>, AgentDriverError> {
// Prepare the environment config files.
prepare_gemini_environment_config(working_dir, system_prompt).map_err(|error| {
Expand Down
11 changes: 7 additions & 4 deletions app/src/ai/agent_sdk/driver/harness/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use warp_cli::agent::Harness;
use warpui::{ModelHandle, ModelSpawner, SingletonEntity};

use crate::ai::agent::conversation::AIConversationId;
use crate::ai::ambient_agents::AmbientAgentTaskId;
use crate::ai::ambient_agents::{task::HarnessModelConfig, AmbientAgentTaskId};
use crate::ai::mcp::JSONMCPServer;
use crate::server::server_api::harness_support::{upload_to_target, HarnessSupportClient};
use crate::server::server_api::ServerApi;
Expand Down Expand Up @@ -185,7 +185,7 @@ pub(crate) trait ThirdPartyHarness: Send + Sync {
resolved_env_vars: &HashMap<OsString, OsString>,
resolved_secrets: &HashMap<String, ManagedSecretValue>,
resolved_mcp_servers: &HashMap<String, JSONMCPServer>,
third_party_harness_model_id: Option<&str>,
third_party_harness_model_config: Option<&HarnessModelConfig>,
) -> Result<Box<dyn HarnessRunner>, AgentDriverError>;
}

Expand Down Expand Up @@ -379,10 +379,13 @@ pub(crate) fn task_env_vars(
/// Claude Code's `settings.json`.
pub(crate) fn harness_model_env_vars(
selected_harness: Harness,
third_party_harness_model_id: Option<&str>,
third_party_harness_model_config: Option<&HarnessModelConfig>,
) -> HashMap<OsString, OsString> {
let mut env_vars = HashMap::new();
let Some(model_id) = third_party_harness_model_id.filter(|id| !id.is_empty()) else {
let Some(model_id) = third_party_harness_model_config
.map(|config| config.model_id.as_str())
.filter(|id| !id.is_empty())
else {
return env_vars;
};

Expand Down
Loading
Loading