mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(agents): validate new configs with smoke test
This commit is contained in:
@@ -11,9 +11,11 @@ use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use tokio::process::Command;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::runtime::Builder as TokioRuntimeBuilder;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::{timeout, Duration as TokioDuration};
|
||||
|
||||
use crate::agent_defaults::{agent_model_spec, default_params_for};
|
||||
use crate::config_types::AgentConfig;
|
||||
@@ -1011,6 +1013,62 @@ fn strip_model_flags(args: &mut Vec<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
const AGENT_SMOKE_TEST_PROMPT: &str = "Reply only with the string \"ok\". Do not include any other words.";
|
||||
const AGENT_SMOKE_TEST_EXPECTED: &str = "ok";
|
||||
const AGENT_SMOKE_TEST_TIMEOUT: TokioDuration = TokioDuration::from_secs(20);
|
||||
|
||||
async fn run_agent_smoke_test(cfg: AgentConfig) -> Result<String, String> {
|
||||
let model_name = cfg.name.clone();
|
||||
let response = timeout(
|
||||
AGENT_SMOKE_TEST_TIMEOUT,
|
||||
execute_model_with_permissions(&model_name, AGENT_SMOKE_TEST_PROMPT, true, None, Some(cfg)),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
format!(
|
||||
"agent validation timed out after {}s",
|
||||
AGENT_SMOKE_TEST_TIMEOUT.as_secs()
|
||||
)
|
||||
})??;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn summarize_agent_output(output: &str) -> String {
|
||||
let trimmed = output.trim();
|
||||
if trimmed.is_empty() {
|
||||
return "<empty response>".to_string();
|
||||
}
|
||||
const MAX_LEN: usize = 240;
|
||||
if trimmed.len() <= MAX_LEN {
|
||||
trimmed.to_string()
|
||||
} else {
|
||||
format!("{}…", &trimmed[..MAX_LEN])
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn smoke_test_agent(cfg: AgentConfig) -> Result<(), String> {
|
||||
let output = run_agent_smoke_test(cfg).await?;
|
||||
if output
|
||||
.to_ascii_lowercase()
|
||||
.contains(AGENT_SMOKE_TEST_EXPECTED)
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"agent response missing \"ok\": {}",
|
||||
summarize_agent_output(&output)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn smoke_test_agent_blocking(cfg: AgentConfig) -> Result<(), String> {
|
||||
TokioRuntimeBuilder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|e| format!("failed to build validation runtime: {}", e))?
|
||||
.block_on(smoke_test_agent(cfg))
|
||||
}
|
||||
|
||||
/// Execute the built-in cloud agent via the current `code` binary, streaming
|
||||
/// stderr lines into the HUD as progress and returning final stdout. Applies a
|
||||
/// modest truncation cap to very large outputs to keep UI responsive.
|
||||
|
||||
@@ -115,6 +115,7 @@ pub mod util;
|
||||
|
||||
pub use apply_patch::CODEX_APPLY_PATCH_ARG1;
|
||||
pub use command_safety::is_safe_command;
|
||||
pub use agent_tool::smoke_test_agent_blocking;
|
||||
pub use safety::get_platform_sandbox;
|
||||
pub use housekeeping::run_housekeeping_if_due;
|
||||
pub use housekeeping::CleanupOutcome;
|
||||
|
||||
@@ -31,6 +31,7 @@ use code_common::model_presets::ModelPreset;
|
||||
use code_common::model_presets::builtin_model_presets;
|
||||
use code_core::ConversationManager;
|
||||
use code_core::agent_defaults::{agent_model_spec, enabled_agent_model_specs};
|
||||
use code_core::smoke_test_agent_blocking;
|
||||
use code_core::config::Config;
|
||||
use code_core::git_info::CommitLogEntry;
|
||||
use code_core::config_types::AgentConfig;
|
||||
@@ -17519,42 +17520,58 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
description: Option<String>,
|
||||
command: String,
|
||||
) {
|
||||
let mut updated_existing = false;
|
||||
let provided_command = if command.trim().is_empty() { None } else { Some(command.as_str()) };
|
||||
let mut command_to_persist: Option<String> = None;
|
||||
if let Some(slot) = self
|
||||
let existing_index = self
|
||||
.config
|
||||
.agents
|
||||
.iter_mut()
|
||||
.find(|a| a.name.eq_ignore_ascii_case(name))
|
||||
{
|
||||
slot.enabled = enabled;
|
||||
slot.args_read_only = args_ro.clone();
|
||||
slot.args_write = args_wr.clone();
|
||||
slot.instructions = instr.clone();
|
||||
slot.description = description.clone();
|
||||
let resolved = Self::resolve_agent_command(name, provided_command, Some(slot.command.as_str()));
|
||||
slot.command = resolved.clone();
|
||||
command_to_persist = Some(resolved);
|
||||
updated_existing = true;
|
||||
.iter()
|
||||
.position(|a| a.name.eq_ignore_ascii_case(name));
|
||||
|
||||
let existing_command = existing_index
|
||||
.and_then(|idx| self.config.agents.get(idx))
|
||||
.map(|cfg| cfg.command.clone());
|
||||
let resolved = Self::resolve_agent_command(
|
||||
name,
|
||||
provided_command,
|
||||
existing_command.as_deref(),
|
||||
);
|
||||
|
||||
let env_clone = existing_index
|
||||
.and_then(|idx| self.config.agents.get(idx))
|
||||
.and_then(|cfg| cfg.env.clone());
|
||||
|
||||
let candidate_cfg = AgentConfig {
|
||||
name: name.to_string(),
|
||||
command: resolved.clone(),
|
||||
args: Vec::new(),
|
||||
read_only: false,
|
||||
enabled,
|
||||
description: description.clone(),
|
||||
env: env_clone,
|
||||
args_read_only: args_ro.clone(),
|
||||
args_write: args_wr.clone(),
|
||||
instructions: instr.clone(),
|
||||
};
|
||||
|
||||
let requires_validation = !self.test_mode && existing_index.is_none();
|
||||
if requires_validation && !self.validate_agent_candidate(&candidate_cfg) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !updated_existing {
|
||||
let resolved = Self::resolve_agent_command(name, provided_command, None);
|
||||
let new_cfg = AgentConfig {
|
||||
name: name.to_string(),
|
||||
command: resolved.clone(),
|
||||
args: Vec::new(),
|
||||
read_only: false,
|
||||
enabled,
|
||||
description: description.clone(),
|
||||
env: None,
|
||||
args_read_only: args_ro.clone(),
|
||||
args_write: args_wr.clone(),
|
||||
instructions: instr.clone(),
|
||||
};
|
||||
self.config.agents.push(new_cfg);
|
||||
command_to_persist = Some(resolved);
|
||||
let mut command_to_persist: Option<String> = None;
|
||||
if let Some(idx) = existing_index {
|
||||
if let Some(slot) = self.config.agents.get_mut(idx) {
|
||||
slot.enabled = enabled;
|
||||
slot.args_read_only = args_ro.clone();
|
||||
slot.args_write = args_wr.clone();
|
||||
slot.instructions = instr.clone();
|
||||
slot.description = description.clone();
|
||||
slot.command = resolved.clone();
|
||||
command_to_persist = Some(resolved.clone());
|
||||
}
|
||||
} else {
|
||||
self.config.agents.push(candidate_cfg);
|
||||
command_to_persist = Some(resolved.clone());
|
||||
}
|
||||
// Persist asynchronously
|
||||
if let Ok(home) = code_core::config::find_code_home() {
|
||||
@@ -17580,6 +17597,28 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
self.refresh_settings_overview_rows();
|
||||
}
|
||||
|
||||
fn validate_agent_candidate(&mut self, cfg: &AgentConfig) -> bool {
|
||||
let start_message = format!("🧪 Testing agent `{}` (expecting \"ok\")…", cfg.name);
|
||||
self.push_background_tail(start_message);
|
||||
match smoke_test_agent_blocking(cfg.clone()) {
|
||||
Ok(()) => {
|
||||
self.push_background_tail(format!(
|
||||
"✅ Agent `{}` responded with \"ok\".",
|
||||
cfg.name
|
||||
));
|
||||
true
|
||||
}
|
||||
Err(err) => {
|
||||
self.history_push_plain_state(history_cell::new_error_event(format!(
|
||||
"❌ Agent `{}` validation failed: {}",
|
||||
cfg.name, err
|
||||
)));
|
||||
self.request_redraw();
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_agent_command(
|
||||
name: &str,
|
||||
provided: Option<&str>,
|
||||
|
||||
Reference in New Issue
Block a user