Skip to content
Draft
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
16 changes: 9 additions & 7 deletions desktop/src-tauri/src/commands/agent_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ use crate::{
managed_agents::{
build_managed_agent_summary, current_instance_id, default_agent_workdir,
discovery_env_with_baked_floor, find_managed_agent_mut, known_acp_runtime,
load_managed_agents, load_personas, managed_agent_avatar_url, missing_command_message,
normalize_agent_args, resolve_command, save_managed_agents, sync_managed_agent_processes,
try_regenerate_nest, AgentModelInfo, AgentModelsResponse, UpdateManagedAgentRequest,
UpdateManagedAgentResponse, DEFAULT_ACP_COMMAND,
load_managed_agents, load_personas, managed_agent_avatar_url,
maybe_restart_setup_mode_agent, missing_command_message, normalize_agent_args,
resolve_command, save_managed_agents, sync_managed_agent_processes, try_regenerate_nest,
AgentModelInfo, AgentModelsResponse, UpdateManagedAgentRequest, UpdateManagedAgentResponse,
DEFAULT_ACP_COMMAND,
},
relay::{relay_ws_url_with_override, sync_managed_agent_profile},
util::now_iso,
Expand Down Expand Up @@ -786,9 +787,8 @@ async fn run_agent_models_command(

/// Update mutable fields on an existing managed agent record.
///
/// Does NOT auto-restart the agent. Runtime config changes (system prompt,
/// parallelism, commands, toolsets) take effect on the next agent spawn.
/// Name changes are synced to the relay immediately via a kind:0 re-publish.
/// Does NOT auto-restart healthy agents; only setup-mode listeners are respawned.
/// Other runtime config changes take effect on next spawn; renames sync now.
#[tauri::command]
pub async fn update_managed_agent(
input: UpdateManagedAgentRequest,
Expand Down Expand Up @@ -910,6 +910,8 @@ pub async fn update_managed_agent(

record.updated_at = now_iso();

maybe_restart_setup_mode_agent(&app, &state, record, &mut runtimes);

save_managed_agents(&app, &records)?;

let record = records
Expand Down
2 changes: 2 additions & 0 deletions desktop/src-tauri/src/managed_agents/process_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ pub fn finish_spawn(
child: std::process::Child,
log_path: std::path::PathBuf,
spawn_config_hash: u64,
in_setup_mode: bool,
agent_name: &str,
) -> super::ManagedAgentProcess {
let job = create_job_for_child(child.id());
Expand All @@ -147,6 +148,7 @@ pub fn finish_spawn(
child,
log_path,
spawn_config_hash,
in_setup_mode,
job,
}
}
84 changes: 82 additions & 2 deletions desktop/src-tauri/src/managed_agents/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use tauri::AppHandle;
use super::agent_env::build_buzz_agent_provider_defaults;

use crate::{
app_state::AppState,
managed_agents::{
append_log_marker, known_acp_runtime, login_shell_path, managed_agent_log_path,
missing_command_message, normalize_agent_args, open_log_file, resolve_command,
Expand Down Expand Up @@ -1620,7 +1621,7 @@ pub fn spawn_agent_child(
//
// The JSON format mirrors `setup_mode::SetupPayload` in buzz-acp:
// { "agent_name": "...", "agent_pubkey": "...", "requirements": [{ "surface": "...", ... }] }
{
let in_setup_mode = {
use crate::managed_agents::{
agent_readiness, resolve_effective_agent_env, AgentReadiness, Requirement,
};
Expand Down Expand Up @@ -1682,14 +1683,16 @@ pub fn spawn_agent_child(
command.env_remove("BUZZ_ACP_SETUP_PAYLOAD");

// Set the payload only when desktop computed NotReady.
let in_setup_mode = setup_payload_json.is_some();
if let Some(json) = setup_payload_json {
command.env("BUZZ_ACP_SETUP_PAYLOAD", json);
eprintln!(
"buzz-desktop: agent {} not ready — spawning in setup-listener mode",
record.name
);
}
}
in_setup_mode
};
// Only emit BUZZ_ACP_IDLE_TIMEOUT when the user has explicitly set an
// override. When unset, the buzz-acp harness applies its own default
// (see `DEFAULT_IDLE_TIMEOUT_SECS` in crates/buzz-acp/src/config.rs),
Expand Down Expand Up @@ -1890,16 +1893,93 @@ pub fn spawn_agent_child(
child,
log_path,
spawn_config_hash,
in_setup_mode,
&record.name,
));
#[cfg(not(windows))]
Ok(crate::managed_agents::ManagedAgentProcess {
child,
log_path,
spawn_config_hash,
in_setup_mode,
})
}

/// Returns `true` when a process that was spawned in setup mode would pass
/// readiness with the record's current config. Setup-mode buzz-acp is a
/// nudge-only listener and does not re-read desktop config after spawn, so this
/// is the predicate for auto-restarting after an Edit Agent save.
pub fn setup_mode_now_ready(
record: &ManagedAgentRecord,
personas: &[crate::managed_agents::types::PersonaRecord],
was_spawned_in_setup_mode: bool,
) -> bool {
use crate::managed_agents::{agent_readiness, resolve_effective_agent_env, AgentReadiness};

if !was_spawned_in_setup_mode {
return false;
}

let effective_command = crate::managed_agents::effective_agent_command(
record.persona_id.as_deref(),
personas,
record.agent_command_override.as_deref(),
);
let runtime_meta = known_acp_runtime(&effective_command);
let effective = resolve_effective_agent_env(record, personas, runtime_meta);
matches!(agent_readiness(&effective), AgentReadiness::Ready)
}

/// Stop and respawn a setup-mode process if the current record now passes
/// readiness. Called after Edit Agent saves mutate the record but before the
/// store is persisted.
pub fn maybe_restart_setup_mode_agent(
app: &AppHandle,
state: &AppState,
record: &mut ManagedAgentRecord,
runtimes: &mut HashMap<String, ManagedAgentProcess>,
) {
let was_setup_mode = runtimes
.get(&record.pubkey)
.is_some_and(|runtime| runtime.in_setup_mode);
let personas = super::load_personas(app).unwrap_or_default();
if !setup_mode_now_ready(record, &personas, was_setup_mode) {
return;
}

let restart = (|| -> Result<(), String> {
let owner_hex = {
let keys = state.keys.lock().map_err(|e| e.to_string())?;
keys.public_key().to_hex()
};
stop_managed_agent_process(app, record, runtimes)?;
let process = spawn_agent_child(app, record, Some(&owner_hex))?;
let now = now_iso();
record.updated_at = now.clone();
record.runtime_pid = Some(process.child.id());
record.last_started_at = Some(now);
record.last_stopped_at = None;
record.last_exit_code = None;
record.last_error = None;
runtimes.insert(record.pubkey.clone(), process);
Ok(())
})();

match restart {
Ok(()) => eprintln!(
"buzz-desktop: auto-restarted agent {} out of setup mode",
record.name
),
Err(error) => {
eprintln!(
"buzz-desktop: failed to auto-restart agent {} out of setup mode: {error}",
record.name
);
record.last_error = Some(format!("auto-restart failed: {error}"));
}
}
}

fn child_rust_log_filter() -> String {
match std::env::var("RUST_LOG") {
Ok(existing) if existing.contains("buzz_acp") => existing,
Expand Down
42 changes: 41 additions & 1 deletion desktop/src-tauri/src/managed_agents/runtime/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,47 @@ fn non_persona_agent_never_drifts() {
assert!(!orphaned);
}

use super::runtime_metadata_env_vars;
use super::{runtime_metadata_env_vars, setup_mode_now_ready};

#[test]
fn setup_mode_now_ready_requires_setup_mode_stamp() {
let mut record = fixture(RespondTo::Anyone, vec![], Some("tag".into()));
record.agent_command_override = Some("buzz-agent".into());
record.provider = Some("databricks_v2".into());
record.model = Some("goose-claude-4-6-opus".into());
record.env_vars.insert(
"DATABRICKS_HOST".into(),
"https://databricks.example.com".into(),
);

assert!(setup_mode_now_ready(&record, &[], true));
assert!(
!setup_mode_now_ready(&record, &[], false),
"healthy running agents must not be auto-restarted just because they are ready"
);
}

#[test]
fn setup_mode_now_ready_stays_false_until_missing_config_is_fixed() {
let mut record = fixture(RespondTo::Anyone, vec![], Some("tag".into()));
record.agent_command_override = Some("buzz-agent".into());
record.provider = Some("databricks_v2".into());
record.env_vars.insert(
"DATABRICKS_HOST".into(),
"https://databricks.example.com".into(),
);

assert!(
!setup_mode_now_ready(&record, &[], true),
"missing model keeps the setup listener in place"
);

record.model = Some("goose-claude-4-6-opus".into());
assert!(
setup_mode_now_ready(&record, &[], true),
"filling the missing model makes the setup-mode process eligible for restart"
);
}

#[test]
fn runtime_metadata_env_vars_injects_model_and_provider() {
Expand Down
7 changes: 7 additions & 0 deletions desktop/src-tauri/src/managed_agents/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,13 @@ pub struct ManagedAgentProcess {
/// `runtime_pid` have no `ManagedAgentProcess` entry, so their spawn
/// config is unknown and the badge stays off.
pub spawn_config_hash: u64,
/// `true` when this process was spawned with `BUZZ_ACP_SETUP_PAYLOAD` set —
/// readiness failed at spawn and buzz-acp is running as a nudge-only setup
/// listener instead of the real agent pool. Runtime-only — never persisted.
/// `update_managed_agent` uses this stamp to restart the harness once an
/// edit satisfies readiness, because setup-mode processes do not re-read
/// desktop config after launch.
pub in_setup_mode: bool,
/// Win32 Job Object owning the harness + its entire process tree. Closing
/// the handle (via `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`) kills the whole
/// tree — the Windows mirror of the Unix process-group teardown. `None`
Expand Down
Loading