use super::{
ProviderRunOptions, append_primary_agent_to_main_prompt, auto_compaction_eligible,
auto_compaction_policy,
};
use crate::{
agent::{AgentSession, AgentSessionConfig},
cancellation::AgentCancellation,
config::{AutoCompactionSettings, EffectiveConfig, Settings},
context::ContextBudget,
fast::FastWorkload,
instructions::InstructionFile,
output::OutputEvent,
providers::{
OPENAI_CODEX_PROVIDER, Provider, ProviderSelection,
provider_from_selection_with_settings_for_workload,
provider_from_selection_with_settings_for_workload_with_resolved_auth,
supported_custom_provider,
},
skills::SkillDiscovery,
tools::ToolRuntime,
};
use anyhow::Result;
use std::{
borrow::Cow,
path::Path,
sync::{Arc, Mutex},
};
pub(super) struct PreparedRun<'config> {
pub(super) active_config: Cow<'config, EffectiveConfig>,
pub(super) settings: Settings,
pub(super) context_budget: ContextBudget,
pub(super) cancellation: AgentCancellation,
pub(super) parent_agent_for_provider: AgentSession,
pub(super) provider: Arc<dyn Provider>,
pub(super) hooks: crate::hooks::HookRuntime,
pub(super) tools: ToolRuntime,
pub(super) title_job: Option<crate::session_titles::SessionTitleJob>,
pub(super) auto: AutoCompactionSettings,
pub(super) auto_eligible: bool,
pub(super) auto_policy: Option<(usize, String)>,
pub(super) herdr_reporter: Option<crate::herdr::HerdrReporter>,
}
pub(super) fn prepare<'config>(
config: &'config EffectiveConfig,
instructions: &[InstructionFile],
skills: &SkillDiscovery,
options: &mut ProviderRunOptions<'_, '_>,
cancellation: AgentCancellation,
) -> Result<PreparedRun<'config>> {
prepare_with_codex_auth(
config,
instructions,
skills,
options,
cancellation,
|config| config.resolve_provider_auth_for_runtime(),
)
}
#[cfg(test)]
pub(super) fn prepare_with_codex_exchange<'config>(
config: &'config EffectiveConfig,
instructions: &[InstructionFile],
skills: &SkillDiscovery,
options: &mut ProviderRunOptions<'_, '_>,
cancellation: AgentCancellation,
exchange: impl FnOnce(&str) -> anyhow::Result<crate::config::NormalizedToken>,
) -> Result<PreparedRun<'config>> {
prepare_with_codex_auth(
config,
instructions,
skills,
options,
cancellation,
move |config| config.resolve_provider_auth_for_runtime_with_exchange(exchange),
)
}
fn prepare_with_codex_auth<'config>(
config: &'config EffectiveConfig,
instructions: &[InstructionFile],
skills: &SkillDiscovery,
options: &mut ProviderRunOptions<'_, '_>,
cancellation: AgentCancellation,
resolve_codex_auth: impl FnOnce(
&EffectiveConfig,
) -> anyhow::Result<crate::config::ProviderCredential>,
) -> Result<PreparedRun<'config>> {
let (active_config, resolved_auth) = if config.provider_id() == OPENAI_CODEX_PROVIDER {
let mut refreshed_config = config.clone();
let credential = resolve_codex_auth(config)?;
refreshed_config.auth = Some(credential.clone());
(Cow::Owned(refreshed_config), Some(credential))
} else {
(Cow::Borrowed(config), None)
};
let config = active_config.as_ref();
let selection = ProviderSelection::from_config(config)?;
let selected_custom_provider = supported_custom_provider(config, &selection)?;
let settings = options
.settings
.take()
.map(Ok)
.unwrap_or_else(|| crate::config::read_settings(&config.paths))?;
let disabled_tools = options.disabled_tools.clone().unwrap_or_else(|| {
Arc::new(Mutex::new(
crate::config::disabled_tool_names_from_settings(&settings)
.into_iter()
.collect(),
))
});
let disabled_tool_names = disabled_tools
.lock()
.map(|disabled| disabled.clone())
.map_err(|_| anyhow::anyhow!("disabled tools lock poisoned"))?;
let base_tools = ToolRuntime::new_with_full_settings_and_mcp_with_disabled_tools(
options.cwd,
config.paths.clone(),
settings.clone(),
options.mcp.clone(),
Arc::clone(&disabled_tools),
)?
.with_skills(skills);
let herdr_reporter = options.herdr_reporter.clone();
let context_budget = super::context_budget_for_selection(config, &settings, &selection);
let model = selection.model.clone();
let mut cached_thinking_metadata = crate::model_catalog::cached_model_thinking_metadata(
&config.paths,
&selection.provider,
&selection.model,
);
if selected_custom_provider.is_some_and(|_| {
cached_thinking_metadata.as_ref().is_none_or(|metadata| {
metadata.supports_reasoning.is_none() && metadata.reasoning_efforts.is_none()
})
}) {
let refresh_result = if crate::model_catalog::automatic_refresh_allowed(&selection.provider)
{
crate::model_catalog::refresh_catalog_for_provider(
&config.paths,
&selection.provider,
&selection.model,
)
} else {
Ok(crate::model_catalog::CatalogRefreshOutcome::Updated)
};
if matches!(
&refresh_result,
Ok(crate::model_catalog::CatalogRefreshOutcome::NoMatch)
) {
crate::model_catalog::automatic_refresh_suppressed(&selection.provider);
}
if let Err(error) = refresh_result {
crate::model_catalog::automatic_refresh_failed(&selection.provider);
let message = format!(
"metadata refresh failed for provider '{}' (category: {}); automatic retry temporarily suppressed",
selection.provider,
error.category().as_str()
);
let _ = crate::sessions::record_session_event(
options.session,
options.cwd,
crate::sessions::SessionEventKind::Diagnostic,
serde_json::json!({"level":"warning", "message": message.clone()}),
);
if let Some(sink) = options.output_sink.as_deref_mut() {
sink.output_event(OutputEvent::Diagnostic {
level: "warning".to_string(),
message,
})?;
}
}
cached_thinking_metadata = crate::model_catalog::cached_model_thinking_metadata(
&config.paths,
&selection.provider,
&selection.model,
);
}
let capability_scope = match selected_custom_provider {
Some(custom) => crate::thinking::ThinkingCapabilityScope::Custom(custom.reasoning_protocol),
None => crate::thinking::ThinkingCapabilityScope::BuiltIn,
};
let thinking_levels = crate::thinking::available_thinking_levels(
&selection.provider,
&selection.model,
cached_thinking_metadata.as_ref(),
capability_scope,
);
let thinking_level =
crate::thinking::resolve_thinking_level(&thinking_levels, config.thinking_level);
let send_default_reasoning_summary = thinking_levels.len() > 1;
let subagent_profile_discovery =
options
.subagent_profile_discovery
.clone()
.unwrap_or_else(|| {
crate::subagents::profiles::discover_subagent_profiles(&config.paths.subagents)
});
let disabled_subagent_profiles = options
.disabled_subagent_profiles
.as_ref()
.map(|disabled| {
disabled
.lock()
.map(|disabled| disabled.clone())
.map_err(|_| anyhow::anyhow!("disabled subagent profiles lock poisoned"))
})
.transpose()?
.unwrap_or_else(|| {
crate::config::disabled_subagent_profile_names_from_settings(&settings)
.into_iter()
.collect()
});
let enabled_subagent_profile_discovery = crate::subagents::profiles::filter_enabled_profiles(
&subagent_profile_discovery,
&disabled_subagent_profiles,
);
let subagent_profiles_prompt =
if disabled_tool_names.contains(crate::tools::contract::tool_name::SUBAGENTS) {
None
} else {
crate::subagents::profiles::render_subagent_profiles_prompt(
Some(&config.paths.prompts),
&enabled_subagent_profile_discovery,
)?
};
let agent_config = AgentSessionConfig::new(selection.provider.clone(), model.clone())
.with_thinking_level(thinking_level)
.with_text_verbosity(settings.text_verbosity_for(&selection.provider))
.with_thinking_levels(thinking_levels)
.with_default_reasoning_summary(send_default_reasoning_summary)
.with_context_budget(context_budget.clone());
let agent = AgentSession::new_with_prompt_dir_and_subagents(
model.clone(),
Some(&config.paths.prompts),
instructions,
skills,
None,
)?
.with_config(agent_config.clone());
let parent_agent_for_provider = AgentSession::new_with_prompt_dir_and_subagents(
model.clone(),
Some(&config.paths.prompts),
instructions,
skills,
subagent_profiles_prompt.as_deref(),
)?
.with_config(agent_config);
let parent_agent_for_provider = append_primary_agent_to_main_prompt(
parent_agent_for_provider,
options.selected_primary_agent.as_ref(),
);
if let Some(sink) = options.output_sink.as_deref_mut() {
sink.output_event(OutputEvent::SessionHeader {
session_id: options.session.map(|session| session.id().to_string()),
model,
cwd: options.cwd.to_path_buf(),
})?;
}
let provider = match resolved_auth {
Some(auth) => provider_from_selection_with_settings_for_workload_with_resolved_auth(
config,
&selection,
options.cwd,
&settings,
FastWorkload::Primary,
auth,
),
None => provider_from_selection_with_settings_for_workload(
config,
&selection,
options.cwd,
&settings,
FastWorkload::Primary,
),
}?;
let title_job = match settings.session_titles.eligible_config() {
Ok(Some(title_config)) => {
options
.session
.cloned()
.map(|session| crate::session_titles::SessionTitleJob {
paths: config.paths.clone(),
session,
cwd: options.cwd.to_path_buf(),
config: title_config,
settings: settings.clone(),
first_prompt: options.prompt.to_string(),
cancellation: cancellation.clone(),
notifier: options.session_title_notifier.clone(),
})
}
Ok(None) => None,
Err(message) => {
let sanitized = crate::output::redact_sensitive_text(&message);
if let Err(persistence_error) = crate::sessions::record_session_event(
options.session,
options.cwd,
crate::sessions::SessionEventKind::Diagnostic,
serde_json::json!({"level":"warning", "message": sanitized}),
) && let Some(sink) = options.output_sink.as_deref_mut()
{
sink.output_event(OutputEvent::Diagnostic {
level: "warning".to_string(),
message: format!(
"session persistence failed; future resume may be incomplete: {persistence_error}",
),
})?;
}
if let Some(sink) = options.output_sink.as_deref_mut() {
sink.output_event(OutputEvent::Diagnostic {
level: "warning".to_string(),
message: sanitized,
})?;
}
None
}
};
let hook_settings = settings.hooks.clone();
let tool_settings = settings.tools.clone();
let hooks = crate::hooks::HookRuntime::new_with_tool_settings(
options.cwd,
hook_settings,
&tool_settings,
)?;
let inherited_hooks = if hooks.is_inert() {
None
} else {
Some(hooks.clone())
};
let subagent_provider_override = crate::subagents::SubagentProviderOverride::new(
config.paths.clone(),
Arc::new({
let paths = config.paths.clone();
let settings = settings.clone();
move |selection: &ProviderSelection, cwd: &Path| {
let provider_config = crate::config::load_effective_provider_selection(
&paths,
&selection.provider,
&selection.model,
)?;
let provider_selection =
ProviderSelection::from_config_without_auth(&provider_config);
let provider = provider_from_selection_with_settings_for_workload(
&provider_config,
&provider_selection,
cwd,
&settings,
FastWorkload::Subagent,
)?;
let scope = crate::thinking::capability_scope_for_provider(
&provider_config.custom_providers,
&provider_selection.provider,
);
let context_budget = super::context_budget_for_selection(
&provider_config,
&settings,
&provider_selection,
);
Ok(crate::subagents::ResolvedProviderOverride {
provider,
scope,
active_config: provider_config,
context_budget,
settings: settings.clone(),
})
}
}),
);
let subagent_config = crate::subagents::SubagentRunConfig {
parent_agent: agent.clone(),
provider: Arc::clone(&provider),
provider_override: Some(subagent_provider_override),
parent_tools: base_tools.clone(),
parent_cwd: options.cwd.to_path_buf(),
cancellation: cancellation.clone(),
profiles: enabled_subagent_profile_discovery.profiles,
subagent_profiles_prompt: subagent_profiles_prompt.clone(),
sessions_root: options
.session
.and_then(|active| active.path().parent().map(std::path::Path::to_path_buf)),
parent_session_id: None,
depth: 0,
parent_activity_id: None,
activity_sender: None,
inherited_hooks,
semantic_progress_timeout: settings
.provider_stream
.subagent_semantic_progress_timeout(),
schema_validation_max_retries: settings.subagents.schema_validation_max_retries(),
compaction: Some(crate::subagents::config::SubagentCompactionConfig::new(
config.clone(),
settings.clone(),
)),
};
let tools = base_tools.with_subagents(move |arguments, context| {
let mut config = subagent_config.clone();
config.parent_session_id = context.hook_context.session_id;
config.parent_activity_id = context.parent_activity_id;
config.activity_sender = context.activity_sender;
crate::subagents::dispatch_subagents(arguments, config)
});
let auto = settings.compaction.auto.clone();
let auto_eligible = auto_compaction_eligible(
auto.is_enabled(),
context_budget.enabled,
options.session.is_some(),
options.invocation_mode,
);
let auto_policy = auto_eligible
.then(|| auto_compaction_policy(&auto, parent_agent_for_provider.context_max_tokens()))
.flatten();
Ok(PreparedRun {
active_config,
settings,
context_budget,
cancellation,
parent_agent_for_provider,
provider,
hooks,
tools,
title_job,
auto,
auto_eligible,
auto_policy,
herdr_reporter,
})
}