fix(auto-drive): align observer tool roster with session config

This commit is contained in:
James Peter
2025-10-22 00:02:14 +10:00
parent b076b9cdce
commit 30fc513bfd
3 changed files with 108 additions and 70 deletions
+74 -1
View File
@@ -23,6 +23,7 @@ use tracing::trace;
use tracing::warn;
use uuid::Uuid;
use crate::agent_defaults::{default_agent_configs, enabled_agent_model_specs};
use crate::chat_completions::AggregateStreamExt;
use crate::chat_completions::stream_chat_completions;
use crate::client_common::Prompt;
@@ -31,7 +32,6 @@ use crate::client_common::ResponseStream;
use crate::client_common::ResponsesApiRequest;
use crate::client_common::create_reasoning_param_for_request;
use crate::config::Config;
use crate::openai_model_info::get_model_info;
use crate::config_types::ReasoningEffort as ReasoningEffortConfig;
use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
use crate::config_types::TextVerbosity as TextVerbosityConfig;
@@ -46,9 +46,14 @@ use crate::flags::CODEX_RS_SSE_FIXTURE;
use crate::model_family::ModelFamily;
use crate::model_provider_info::ModelProviderInfo;
use crate::model_provider_info::WireApi;
use crate::openai_model_info::get_model_info;
use crate::openai_tools::create_tools_json_for_responses_api;
use crate::openai_tools::ConfigShellToolType;
use crate::openai_tools::ToolsConfig;
use crate::protocol::RateLimitSnapshotEvent;
use crate::protocol::SandboxPolicy;
use crate::protocol::TokenUsage;
use crate::slash_commands::get_enabled_agents;
use crate::util::backoff;
use code_otel::otel_event_manager::OtelEventManager;
use std::sync::Arc;
@@ -179,6 +184,74 @@ impl ModelClient {
&self.config.code_home
}
pub fn build_tools_config_with_sandbox(
&self,
sandbox_policy: SandboxPolicy,
) -> ToolsConfig {
let mut tools_config = ToolsConfig::new(
&self.config.model_family,
self.config.approval_policy,
sandbox_policy.clone(),
self.config.include_plan_tool,
self.config.include_apply_patch_tool,
self.config.tools_web_search_request,
self.config.use_experimental_streamable_shell_tool,
self.config.include_view_image_tool,
);
tools_config.web_search_allowed_domains = self.config.tools_web_search_allowed_domains.clone();
let mut agent_models: Vec<String> = if self.config.agents.is_empty() {
default_agent_configs()
.into_iter()
.filter(|cfg| cfg.enabled)
.map(|cfg| cfg.name)
.collect()
} else {
get_enabled_agents(&self.config.agents)
};
if agent_models.is_empty() {
agent_models = enabled_agent_model_specs()
.into_iter()
.map(|spec| spec.slug.to_string())
.collect();
}
agent_models.sort_by(|a, b| a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase()));
agent_models.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
tools_config.set_agent_models(agent_models);
let base_shell_type = tools_config.shell_type.clone();
let base_uses_native_shell = matches!(
&base_shell_type,
ConfigShellToolType::LocalShell | ConfigShellToolType::StreamableShell
);
tools_config.shell_type = match sandbox_policy.clone() {
SandboxPolicy::ReadOnly => {
if base_uses_native_shell {
base_shell_type.clone()
} else {
ConfigShellToolType::ShellWithRequest {
sandbox_policy: SandboxPolicy::ReadOnly,
}
}
}
sp @ SandboxPolicy::WorkspaceWrite { .. } => {
if base_uses_native_shell {
base_shell_type.clone()
} else {
ConfigShellToolType::ShellWithRequest { sandbox_policy: sp }
}
}
SandboxPolicy::DangerFullAccess => base_shell_type,
};
tools_config
}
pub fn build_tools_config(&self) -> ToolsConfig {
self.build_tools_config_with_sandbox(self.config.sandbox_policy.clone())
}
pub fn get_auto_compact_token_limit(&self) -> Option<i64> {
self.config.model_auto_compact_token_limit.or_else(|| {
get_model_info(&self.config.model_family).and_then(|info| info.auto_compact_token_limit)
+22 -33
View File
@@ -5,12 +5,11 @@ use std::time::{Duration, Instant, SystemTime};
use anyhow::{anyhow, Context, Result};
use code_browser::global as browser_global;
use code_core::agent_defaults::{default_agent_configs, enabled_agent_model_specs};
use code_core::config::Config;
use code_core::config_types::{AutoDriveSettings, ReasoningEffort};
use code_core::debug_logger::DebugLogger;
use code_core::model_family::{derive_default_model_family, find_family_for_model};
use code_core::ToolsConfig;
use code_core::get_openai_tools;
use code_core::project_doc::read_auto_drive_docs;
use code_core::protocol::SandboxPolicy;
use code_core::slash_commands::get_enabled_agents;
@@ -46,7 +45,6 @@ use rand::Rng;
use super::auto_observer::{
build_observer_conversation,
start_auto_observer,
observer_tools_for_mode,
AutoObserverCommand,
AutoObserverHandle,
ObserverOutcome,
@@ -701,40 +699,29 @@ fn run_auto_loop(
.build()
.context("creating runtime for auto coordinator")?;
let _browser_enabled = runtime.block_on(async {
let browser_enabled = runtime.block_on(async {
browser_global::get_browser_manager().await.is_some()
});
let mut tools_config = ToolsConfig::new(
&config.model_family,
config.approval_policy,
config.sandbox_policy.clone(),
config.include_plan_tool,
config.include_apply_patch_tool,
config.tools_web_search_request,
config.use_experimental_streamable_shell_tool,
config.include_view_image_tool,
let read_only_tools_config = client
.as_ref()
.build_tools_config_with_sandbox(SandboxPolicy::ReadOnly);
let read_only_tools = get_openai_tools(
&read_only_tools_config,
None,
browser_enabled,
false,
);
tools_config.web_search_allowed_domains = config.tools_web_search_allowed_domains.clone();
let mut agent_models: Vec<String> = if config.agents.is_empty() {
default_agent_configs()
.into_iter()
.filter(|cfg| cfg.enabled)
.map(|cfg| cfg.name)
.collect()
} else {
get_enabled_agents(&config.agents)
};
if agent_models.is_empty() {
agent_models = enabled_agent_model_specs()
.into_iter()
.map(|spec| spec.slug.to_string())
.collect();
}
agent_models.sort_by(|a, b| a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase()));
agent_models.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
tools_config.set_agent_models(agent_models);
let full_access_tools_config = client
.as_ref()
.build_tools_config_with_sandbox(SandboxPolicy::DangerFullAccess);
let full_access_tools = get_openai_tools(
&full_access_tools_config,
None,
browser_enabled,
false,
);
let auto_instructions = match runtime.block_on(read_auto_drive_docs(config.as_ref())) {
Ok(Some(text)) => {
@@ -801,6 +788,7 @@ fn run_auto_loop(
.send(AutoObserverCommand::Bootstrap {
goal_text: goal_text.clone(),
environment_details: environment_details.clone(),
tools: read_only_tools.clone(),
})
.is_err()
{
@@ -903,7 +891,7 @@ fn run_auto_loop(
environment_details: environment_details.clone(),
reason: ObserverReason::Cadence,
turn_snapshot: None,
tools: observer_tools_for_mode(ObserverMode::Cadence),
tools: read_only_tools.clone(),
};
if handle.tx.send(AutoObserverCommand::Trigger(trigger)).is_err() {
warn!("failed to trigger auto observer");
@@ -953,6 +941,7 @@ fn run_auto_loop(
forced: true,
summary: latest_summary.clone(),
focus: None,
tools: full_access_tools.clone(),
}) {
Ok(()) => {
awaiting_cross_check = true;
+12 -36
View File
@@ -56,6 +56,7 @@ pub(super) enum AutoObserverCommand {
Bootstrap {
goal_text: String,
environment_details: String,
tools: Vec<OpenAiTool>,
},
Trigger(ObserverTrigger),
BeginCrossCheck {
@@ -64,42 +65,11 @@ pub(super) enum AutoObserverCommand {
forced: bool,
summary: Option<String>,
focus: Option<String>,
tools: Vec<OpenAiTool>,
},
Stop,
}
#[derive(Clone, Copy, Debug)]
enum ObserverToolPolicy {
ReadOnly,
Limited,
FullAudit,
}
impl ObserverToolPolicy {
fn for_mode(mode: ObserverMode) -> Self {
match mode {
ObserverMode::Bootstrap => ObserverToolPolicy::ReadOnly,
ObserverMode::Cadence => ObserverToolPolicy::Limited,
ObserverMode::CrossCheck => ObserverToolPolicy::FullAudit,
}
}
fn tools(self) -> Vec<OpenAiTool> {
match self {
ObserverToolPolicy::ReadOnly => vec![OpenAiTool::WebSearch(Default::default())],
ObserverToolPolicy::Limited => vec![OpenAiTool::WebSearch(Default::default())],
ObserverToolPolicy::FullAudit => vec![
OpenAiTool::LocalShell {},
OpenAiTool::WebSearch(Default::default()),
],
}
}
}
pub(super) fn observer_tools_for_mode(mode: ObserverMode) -> Vec<OpenAiTool> {
ObserverToolPolicy::for_mode(mode).tools()
}
#[derive(Debug, Clone)]
pub(super) enum ObserverReason {
Cadence,
@@ -180,7 +150,11 @@ pub(super) fn run_observer_once(
evaluate_observer(runtime, client, trigger, tx, ObserverMode::Cadence)
}
fn build_bootstrap_trigger(goal_text: &str, environment_details: &str) -> ObserverTrigger {
fn build_bootstrap_trigger(
goal_text: &str,
environment_details: &str,
tools: Vec<OpenAiTool>,
) -> ObserverTrigger {
let prompt = format!(
"You are the QA observer. Before automation begins, inspect the repository and outline how to validate completion of the primary goal.\n\nPrimary goal:\n{goal}\n\nEnvironment:\n{environment}\n\nReturn a concise readiness summary and any immediate risks to monitor.",
goal = goal_text.trim(),
@@ -196,7 +170,7 @@ fn build_bootstrap_trigger(goal_text: &str, environment_details: &str) -> Observ
environment_details: environment_details.to_string(),
reason: ObserverReason::Cadence,
turn_snapshot: None,
tools: observer_tools_for_mode(ObserverMode::Bootstrap),
tools,
}
}
@@ -221,8 +195,9 @@ fn run_observer_loop(
AutoObserverCommand::Bootstrap {
goal_text,
environment_details,
tools,
} => {
let trigger = build_bootstrap_trigger(&goal_text, &environment_details);
let trigger = build_bootstrap_trigger(&goal_text, &environment_details, tools);
telemetry.trigger_count += 1;
match evaluate_observer(
&runtime,
@@ -370,6 +345,7 @@ fn run_observer_loop(
forced,
summary,
focus,
tools,
} => {
let summary_clone = summary.clone();
let focus_clone = focus.clone();
@@ -383,7 +359,7 @@ fn run_observer_loop(
focus,
},
turn_snapshot: None,
tools: observer_tools_for_mode(ObserverMode::CrossCheck),
tools,
};
telemetry.trigger_count += 1;
match evaluate_observer(