Skip to content
Closed
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
88 changes: 78 additions & 10 deletions desktop/src-tauri/src/commands/agent_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,31 @@ use crate::{
config_bridge::{
reader::read_config_surface,
types::{
AcpConfigOptionEntry, AcpConfigOptionValue, AcpModelEntry, ConfigWriteMechanism,
RuntimeConfigSurface, SessionConfigCache, WriteConfigFieldRequest,
WriteConfigResult, WriteConfigTarget,
AcpConfigOptionEntry, AcpConfigOptionValue, AcpModelEntry, ConfigOrigin,
ConfigWriteMechanism, RuntimeConfigSurface, SessionConfigCache,
WriteConfigFieldRequest, WriteConfigResult, WriteConfigTarget,
},
writer::plan_config_write,
},
known_acp_runtime, load_managed_agents, save_managed_agents, sync_managed_agent_processes,
known_acp_runtime, load_managed_agents, load_personas,
resolve_effective_prompt_model_provider, save_managed_agents,
sync_managed_agent_processes,
},
};

/// Get the full config surface for a managed agent.
///
/// Returns normalized + advanced config from all available tiers.
/// Pre-spawn agents show config file values with ACP tiers marked as pending.
/// Persona-sourced values are resolved here (call-site) and re-tagged as
/// `PersonaDefault` after the reader produces the surface.
#[tauri::command]
pub async fn get_agent_config_surface(
pubkey: String,
app: AppHandle,
state: State<'_, AppState>,
) -> Result<RuntimeConfigSurface, String> {
let record = {
let mut record = {
let _store_guard = state
.managed_agents_store_lock
.lock()
Expand All @@ -45,21 +49,85 @@ pub async fn get_agent_config_surface(
.ok_or_else(|| format!("agent {pubkey} not found"))?
};

// Resolve persona values at the call site (not inside the reader).
// Track which fields were absent on the record so we can re-tag them
// as PersonaDefault after the reader produces the surface.
let personas = load_personas(&app).unwrap_or_default();
let had_prompt = record.system_prompt.is_some()
|| record.env_vars.contains_key("BUZZ_ACP_SYSTEM_PROMPT");
let had_model = record.model.is_some();

let runtime_meta = known_acp_runtime(&record.agent_command);
let provider_env_key = runtime_meta
.and_then(|m| m.provider_env_var)
.unwrap_or("");
let had_provider = record.env_vars.contains_key(provider_env_key);

let (persona_prompt, persona_model, persona_provider) =
resolve_effective_prompt_model_provider(
record.persona_id.as_deref(),
&personas,
record.system_prompt.clone(),
record.model.clone(),
);

// Inject resolved persona values into the record where absent.
if !had_prompt {
if let Some(ref p) = persona_prompt {
record
.env_vars
.insert("BUZZ_ACP_SYSTEM_PROMPT".to_string(), p.clone());
}
}
if !had_model {
record.model = persona_model;
}
if !had_provider && !provider_env_key.is_empty() {
if let Some(ref prov) = persona_provider {
record
.env_vars
.insert(provider_env_key.to_string(), prov.clone());
}
}

let session_cache = state.get_session_cache(&pubkey);
let mut surface = read_config_surface(&record, runtime_meta, session_cache.as_ref());

// Re-tag persona-sourced fields from BuzzExplicit to PersonaDefault.
if !had_prompt {
if let Some(ref mut field) = surface.normalized.system_prompt {
if field.origin == ConfigOrigin::BuzzExplicit {
field.origin = ConfigOrigin::PersonaDefault;
}
}
}
if !had_model {
if let Some(ref mut field) = surface.normalized.model {
if field.origin == ConfigOrigin::BuzzExplicit {
field.origin = ConfigOrigin::PersonaDefault;
}
}
}
if !had_provider {
if let Some(ref mut field) = surface.normalized.provider {
if field.origin == ConfigOrigin::BuzzExplicit {
field.origin = ConfigOrigin::PersonaDefault;
}
}
}

Ok(read_config_surface(
&record,
runtime_meta,
session_cache.as_ref(),
))
Ok(surface)
}

/// Write a config field value for a managed agent.
///
/// Plans the write mechanism based on the current config surface, then
/// executes: either updating the record (for env var respawn) or returning
/// the mechanism for the frontend to send via observer control (for ACP writes).
///
// TODO: When inline editing lands, this function needs the same persona
// injection as get_agent_config_surface — without it, plan_config_write
// won't see persona-sourced fields and will return "field not available".
#[tauri::command]
pub async fn write_agent_config_field(
request: WriteConfigFieldRequest,
Expand Down
117 changes: 117 additions & 0 deletions desktop/src-tauri/src/managed_agents/config_bridge/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,4 +579,121 @@ mod tests {
// the actual file in a unit test, just verify the override fields are populated
// when we manually construct the scenario via build_model_field.
}

// ── Persona resolution integration tests ────────────────────────────
//
// These simulate the call-site pattern in agent_config.rs:
// 1. Inject persona-resolved values into the record (as if absent)
// 2. Call read_config_surface (reader tags them BuzzExplicit)
// 3. Re-tag injected fields to PersonaDefault
//
// This exercises the same logic path as get_agent_config_surface without
// requiring Tauri AppHandle/State infrastructure.

#[test]
fn persona_model_injection_produces_persona_default_origin() {
let mut record = test_record();
// Simulate: record has no model, persona provides one.
// The call-site injects it before calling the reader.
record.model = Some("persona-model".to_string());
let runtime = test_runtime();

let mut surface = read_config_surface(&record, Some(runtime), None);

// Reader sees injected model as BuzzExplicit.
let model = surface.normalized.model.as_ref().unwrap();
assert_eq!(model.value.as_deref(), Some("persona-model"));
assert_eq!(model.origin, ConfigOrigin::BuzzExplicit);

// Call-site re-tags (simulating had_model == false).
if let Some(ref mut field) = surface.normalized.model {
if field.origin == ConfigOrigin::BuzzExplicit {
field.origin = ConfigOrigin::PersonaDefault;
}
}

let model = surface.normalized.model.unwrap();
assert_eq!(model.value.as_deref(), Some("persona-model"));
assert_eq!(model.origin, ConfigOrigin::PersonaDefault);
}

#[test]
fn persona_provider_injection_produces_persona_default_origin() {
let mut record = test_record();
// Simulate: record has no provider env var, persona provides one.
// The call-site injects it as GOOSE_PROVIDER before calling the reader.
record
.env_vars
.insert("GOOSE_PROVIDER".to_string(), "anthropic".to_string());
let runtime = test_runtime();

let mut surface = read_config_surface(&record, Some(runtime), None);

// Reader sees injected provider as BuzzExplicit.
let provider = surface.normalized.provider.as_ref().unwrap();
assert_eq!(provider.value.as_deref(), Some("anthropic"));
assert_eq!(provider.origin, ConfigOrigin::BuzzExplicit);

// Call-site re-tags (simulating had_provider == false).
if let Some(ref mut field) = surface.normalized.provider {
if field.origin == ConfigOrigin::BuzzExplicit {
field.origin = ConfigOrigin::PersonaDefault;
}
}

let provider = surface.normalized.provider.unwrap();
assert_eq!(provider.value.as_deref(), Some("anthropic"));
assert_eq!(provider.origin, ConfigOrigin::PersonaDefault);
}

#[test]
fn persona_system_prompt_injection_produces_persona_default_origin() {
let mut record = test_record();
// Simulate: record has no system_prompt, persona provides one via env var.
// The call-site injects it as BUZZ_ACP_SYSTEM_PROMPT before calling the reader.
record.env_vars.insert(
"BUZZ_ACP_SYSTEM_PROMPT".to_string(),
"You are a helpful assistant.".to_string(),
);
let runtime = test_runtime();

let mut surface = read_config_surface(&record, Some(runtime), None);

// Reader sees injected prompt as BuzzExplicit.
let prompt = surface.normalized.system_prompt.as_ref().unwrap();
assert_eq!(
prompt.value.as_deref(),
Some("You are a helpful assistant.")
);
assert_eq!(prompt.origin, ConfigOrigin::BuzzExplicit);

// Call-site re-tags (simulating had_prompt == false).
if let Some(ref mut field) = surface.normalized.system_prompt {
if field.origin == ConfigOrigin::BuzzExplicit {
field.origin = ConfigOrigin::PersonaDefault;
}
}

let prompt = surface.normalized.system_prompt.unwrap();
assert_eq!(
prompt.value.as_deref(),
Some("You are a helpful assistant.")
);
assert_eq!(prompt.origin, ConfigOrigin::PersonaDefault);
}

#[test]
fn explicit_record_model_not_retagged_when_already_present() {
let mut record = test_record();
// Record already has its own model — persona resolution should NOT re-tag.
record.model = Some("explicit-model".to_string());
let runtime = test_runtime();

let surface = read_config_surface(&record, Some(runtime), None);

// had_model == true, so no re-tagging occurs. Origin stays BuzzExplicit.
let model = surface.normalized.model.unwrap();
assert_eq!(model.value.as_deref(), Some("explicit-model"));
assert_eq!(model.origin, ConfigOrigin::BuzzExplicit);
}
}
5 changes: 3 additions & 2 deletions desktop/src-tauri/src/managed_agents/config_bridge/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ pub enum ConfigOrigin {
/// Read from harness config file on disk (tier 2b, lowest precedence).
ConfigFile,
/// Value inherited from persona defaults.
/// Forward slot — not yet populated by any reader. Will be wired when
/// persona pack config resolution is added to `read_config_surface`.
/// Populated by the `get_agent_config_surface` call site: persona values are
/// resolved before calling the reader, then the surface is post-processed to
/// re-tag injected fields from `BuzzExplicit` to `PersonaDefault`.
PersonaDefault,
}

Expand Down
Loading
Loading