mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Distinguish implicit harness fallback pins
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -692,6 +692,7 @@ mod tests {
|
||||
definition_parallelism: None,
|
||||
relay_mesh: None,
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
persona_source_version: None,
|
||||
provider: None,
|
||||
}
|
||||
|
||||
@@ -759,20 +759,21 @@ pub async fn create_managed_agent(
|
||||
// Load personas once for harness/pack/avatar resolution below.
|
||||
let personas = load_personas(&app).unwrap_or_default();
|
||||
|
||||
// Harness resolution: the persona's runtime is authoritative. A
|
||||
// persona-backed create stores an `agent_command_override` ONLY when the
|
||||
// user deliberately picked a divergent runtime (`harness_override`) —
|
||||
// e.g. AddChannelBotDialog's runtime selector. A divergence WITHOUT that
|
||||
// flag is a missing-runtime fallback from `resolvePersonaRuntime`, not a
|
||||
// pin, and must inherit so it doesn't freeze on the fallback harness once
|
||||
// the persona's runtime is installed. A persona-less create always
|
||||
// preserves the picked command as a real pin.
|
||||
let agent_command_override = crate::managed_agents::create_time_agent_command_override(
|
||||
requested_persona_id.as_deref(),
|
||||
&personas,
|
||||
input.agent_command.as_deref(),
|
||||
input.harness_override,
|
||||
);
|
||||
// The persona runtime is authoritative. Persist an override only when
|
||||
// the selected runtime must remain pinned (`harness_override`). This
|
||||
// includes explicit picks and the visible fallback for a runtime-less
|
||||
// persona. `implicit_harness_fallback` distinguishes those intents so
|
||||
// the pin cannot adopt defaults owned by another harness. A divergence
|
||||
// without `harness_override` remains inherited so it can recover when
|
||||
// its configured runtime is installed. Persona-less creates always pin.
|
||||
let (agent_command_override, agent_command_override_is_implicit) =
|
||||
crate::managed_agents::create_time_agent_command_override_state(
|
||||
requested_persona_id.as_deref(),
|
||||
&personas,
|
||||
input.agent_command.as_deref(),
|
||||
input.harness_override,
|
||||
input.implicit_harness_fallback,
|
||||
);
|
||||
// The create-time snapshot used for arg/mcp/avatar derivations and
|
||||
// legacy reconcile. Authoritative spawn resolution re-derives this via
|
||||
// `effective_agent_command` at use-time.
|
||||
@@ -891,6 +892,7 @@ pub async fn create_managed_agent(
|
||||
.to_string(),
|
||||
agent_command,
|
||||
agent_command_override,
|
||||
agent_command_override_is_implicit,
|
||||
agent_args,
|
||||
mcp_command,
|
||||
// BUZZ_ACP_TURN_TIMEOUT is deprecated and ignored by the harness;
|
||||
|
||||
@@ -19,6 +19,7 @@ fn bare_agent_record(
|
||||
acp_command: "buzz-acp".to_string(),
|
||||
agent_command: "goose".to_string(),
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec![],
|
||||
mcp_command: "".to_string(),
|
||||
turn_timeout_seconds: 300,
|
||||
@@ -235,6 +236,26 @@ fn deploy_resolver_ignores_defaults_from_a_different_implicit_runtime() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deploy_resolver_masks_mismatched_defaults_for_an_implicit_fallback_override() {
|
||||
let mut record = bare_agent_record(Some("p1"), None, None);
|
||||
record.agent_command = "goose".to_string();
|
||||
record.agent_command_override = Some("goose".to_string());
|
||||
record.agent_command_override_is_implicit = true;
|
||||
let personas = vec![persona_record("p1", None, None)];
|
||||
let global = crate::managed_agents::GlobalAgentConfig {
|
||||
model: Some("auto".to_string()),
|
||||
provider: Some("relay-mesh".to_string()),
|
||||
preferred_runtime: Some("buzz-agent".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
resolve_deploy_model_provider(&record, &personas, &global),
|
||||
(None, None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deploy_resolver_ignores_mismatched_defaults_without_a_stored_override() {
|
||||
let mut record = bare_agent_record(Some("p1"), None, None);
|
||||
|
||||
@@ -27,6 +27,7 @@ fn make_agent(
|
||||
acp_command: "buzz-acp".to_string(),
|
||||
agent_command: "buzz-agent".to_string(),
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec![],
|
||||
mcp_command: "".to_string(),
|
||||
turn_timeout_seconds: 300,
|
||||
|
||||
@@ -165,6 +165,7 @@ fn local_agent() -> ManagedAgentRecord {
|
||||
acp_command: "buzz-acp".to_string(),
|
||||
agent_command: "goose".to_string(),
|
||||
agent_command_override: Some("claude".to_string()),
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec![],
|
||||
mcp_command: "buzz-dev-mcp".to_string(),
|
||||
turn_timeout_seconds: 320,
|
||||
|
||||
@@ -15,6 +15,7 @@ fn agent(persona_id: &str, name: &str, display_name: Option<&str>) -> ManagedAge
|
||||
acp_command: String::new(),
|
||||
agent_command: String::new(),
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec![],
|
||||
mcp_command: String::new(),
|
||||
turn_timeout_seconds: 0,
|
||||
|
||||
@@ -441,6 +441,7 @@ pub async fn confirm_agent_snapshot_import(
|
||||
acp_command: crate::managed_agents::DEFAULT_ACP_COMMAND.to_string(),
|
||||
agent_command: String::new(),
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec![],
|
||||
mcp_command: String::new(),
|
||||
turn_timeout_seconds: 0,
|
||||
|
||||
@@ -31,6 +31,7 @@ fn make_definition(slug: &str) -> ManagedAgentRecord {
|
||||
acp_command: String::new(),
|
||||
agent_command: String::new(),
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec![],
|
||||
mcp_command: String::new(),
|
||||
turn_timeout_seconds: 0,
|
||||
|
||||
@@ -560,6 +560,7 @@ pub async fn confirm_team_snapshot_import(
|
||||
acp_command: crate::managed_agents::DEFAULT_ACP_COMMAND.to_string(),
|
||||
agent_command: String::new(),
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec![],
|
||||
mcp_command: String::new(),
|
||||
turn_timeout_seconds: 0,
|
||||
|
||||
@@ -183,6 +183,7 @@ fn team_export_with_instance_and_memory_level_uses_supplied_entries() {
|
||||
acp_command: crate::managed_agents::DEFAULT_ACP_COMMAND.to_string(),
|
||||
agent_command: String::new(),
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec![],
|
||||
mcp_command: String::new(),
|
||||
turn_timeout_seconds: 0,
|
||||
|
||||
@@ -168,6 +168,7 @@ mod tests {
|
||||
acp_command: "buzz-acp".to_string(),
|
||||
agent_command: "goose".to_string(),
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec!["--flag".to_string()],
|
||||
mcp_command: "buzz-dev-mcp".to_string(),
|
||||
turn_timeout_seconds: 320,
|
||||
|
||||
@@ -485,9 +485,10 @@ mod tests {
|
||||
acp_command: "/usr/local/bin/acp".to_string(), // MUST NOT appear in snapshot
|
||||
agent_command: "goose".to_string(), // MUST NOT appear in snapshot
|
||||
agent_command_override: Some("goose-override".to_string()), // MUST NOT appear
|
||||
agent_args: vec!["--arg".to_string()], // MUST NOT appear in snapshot
|
||||
mcp_command: "mcp-server".to_string(), // MUST NOT appear in snapshot
|
||||
turn_timeout_seconds: 120, // deprecated, MUST NOT appear
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec!["--arg".to_string()], // MUST NOT appear in snapshot
|
||||
mcp_command: "mcp-server".to_string(), // MUST NOT appear in snapshot
|
||||
turn_timeout_seconds: 120, // deprecated, MUST NOT appear
|
||||
idle_timeout_seconds: Some(30),
|
||||
max_turn_duration_seconds: Some(600),
|
||||
parallelism: 2,
|
||||
|
||||
@@ -113,6 +113,7 @@ fn test_record() -> ManagedAgentRecord {
|
||||
definition_parallelism: None,
|
||||
relay_mesh: None,
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
persona_source_version: None,
|
||||
provider: None,
|
||||
}
|
||||
|
||||
@@ -362,7 +362,10 @@ pub fn effective_agent_command(
|
||||
}
|
||||
|
||||
mod overrides;
|
||||
pub use overrides::{apply_agent_command_update, create_time_agent_command_override};
|
||||
pub use overrides::{
|
||||
apply_agent_command_update, create_time_agent_command_override,
|
||||
create_time_agent_command_override_state,
|
||||
};
|
||||
|
||||
/// Prefix of the typed dangling-harness error produced by
|
||||
/// `try_record_agent_command` / `resolve_effective_harness_descriptor`.
|
||||
|
||||
@@ -102,6 +102,7 @@ pub fn apply_agent_command_update(
|
||||
Some(agent_command),
|
||||
harness_override,
|
||||
);
|
||||
record.agent_command_override_is_implicit = false;
|
||||
if agent_command.trim().is_empty() && record.persona_id.is_some() {
|
||||
record.runtime = None;
|
||||
}
|
||||
@@ -151,3 +152,26 @@ pub fn create_time_agent_command_override(
|
||||
|
||||
divergent_agent_command_override(persona_id, personas, picked_command)
|
||||
}
|
||||
|
||||
/// Resolve the create-time harness pin and whether it represents an automatic
|
||||
/// runtime-less fallback rather than explicit selection intent.
|
||||
pub fn create_time_agent_command_override_state(
|
||||
persona_id: Option<&str>,
|
||||
personas: &[crate::managed_agents::types::AgentDefinition],
|
||||
picked_command: Option<&str>,
|
||||
harness_override: bool,
|
||||
implicit_harness_fallback: bool,
|
||||
) -> (Option<String>, bool) {
|
||||
let command =
|
||||
create_time_agent_command_override(persona_id, personas, picked_command, harness_override);
|
||||
let is_runtime_less_persona = persona_id
|
||||
.and_then(|id| personas.iter().find(|persona| persona.id == id))
|
||||
.is_some_and(|persona| {
|
||||
persona
|
||||
.runtime
|
||||
.as_deref()
|
||||
.is_none_or(|runtime| runtime.trim().is_empty())
|
||||
});
|
||||
let is_implicit = command.is_some() && implicit_harness_fallback && is_runtime_less_persona;
|
||||
(command, is_implicit)
|
||||
}
|
||||
|
||||
@@ -324,6 +324,7 @@ fn record_with(
|
||||
acp_command: String::new(),
|
||||
agent_command: String::new(),
|
||||
agent_command_override: override_cmd.map(str::to_string),
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec![],
|
||||
mcp_command: String::new(),
|
||||
turn_timeout_seconds: 0,
|
||||
@@ -698,10 +699,12 @@ fn apply_agent_command_update_inherit_sentinel_clears_pin_and_runtime() {
|
||||
// live definition immediately — not on the next spawn.
|
||||
let personas = vec![persona_with_runtime("p1", Some("goose"))];
|
||||
let mut record = record_with(Some("claude"), Some("p1"), Some("codex-acp"));
|
||||
record.agent_command_override_is_implicit = true;
|
||||
|
||||
apply_agent_command_update(&mut record, &personas, "", false);
|
||||
|
||||
assert_eq!(record.agent_command_override, None);
|
||||
assert!(!record.agent_command_override_is_implicit);
|
||||
assert_eq!(record.runtime, None);
|
||||
assert_eq!(record_agent_command(&record, &personas), "goose");
|
||||
}
|
||||
@@ -726,10 +729,12 @@ fn apply_agent_command_update_concrete_pin_keeps_materialized_runtime() {
|
||||
// the next snapshot apply. The pin shadows it in resolution either way.
|
||||
let personas = vec![persona_with_runtime("p1", Some("goose"))];
|
||||
let mut record = record_with(Some("claude"), Some("p1"), None);
|
||||
record.agent_command_override_is_implicit = true;
|
||||
|
||||
apply_agent_command_update(&mut record, &personas, "codex-acp", true);
|
||||
|
||||
assert_eq!(record.agent_command_override.as_deref(), Some("codex-acp"));
|
||||
assert!(!record.agent_command_override_is_implicit);
|
||||
assert_eq!(record.runtime.as_deref(), Some("claude"));
|
||||
assert_eq!(record_agent_command(&record, &personas), "codex-acp");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::super::create_time_agent_command_override_state;
|
||||
use super::{create_time_agent_command_override, persona_with_runtime};
|
||||
|
||||
#[test]
|
||||
@@ -17,3 +18,30 @@ fn pins_visible_fallback_for_runtime_less_persona() {
|
||||
Some("goose".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marks_only_runtime_less_automatic_fallback_pins_as_implicit() {
|
||||
let runtime_less = vec![persona_with_runtime("p1", None)];
|
||||
assert_eq!(
|
||||
create_time_agent_command_override_state(
|
||||
Some("p1"),
|
||||
&runtime_less,
|
||||
Some("goose"),
|
||||
true,
|
||||
true,
|
||||
),
|
||||
(Some("goose".to_string()), true)
|
||||
);
|
||||
|
||||
let configured = vec![persona_with_runtime("p1", Some("claude"))];
|
||||
assert_eq!(
|
||||
create_time_agent_command_override_state(
|
||||
Some("p1"),
|
||||
&configured,
|
||||
Some("codex-acp"),
|
||||
true,
|
||||
true,
|
||||
),
|
||||
(Some("codex-acp".to_string()), false)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -211,11 +211,15 @@ pub fn save_global_agent_config(app: &AppHandle, config: &GlobalAgentConfig) ->
|
||||
///
|
||||
/// A runtime-less definition can be started on a fallback runtime when its
|
||||
/// saved global preference is hidden or unavailable. In that case the selected
|
||||
/// command is pinned on the record, and provider/model defaults belonging to a
|
||||
/// different preferred runtime must not cross the harness boundary. Explicitly
|
||||
/// configured definitions and standalone agents keep normal global inheritance.
|
||||
/// Configs written before `preferred_runtime` existed implicitly belong to
|
||||
/// Buzz Agent.
|
||||
/// command is stored as the record snapshot, and provider/model defaults
|
||||
/// belonging to a different preferred runtime must not cross the harness
|
||||
/// boundary. A non-empty `agent_command_override` keeps normal global
|
||||
/// inheritance only when it represents an explicit harness selection. An
|
||||
/// automatic runtime-less fallback carries a separate durable marker so its
|
||||
/// pin survives provisioning without adopting defaults owned by another
|
||||
/// harness. Explicitly configured definitions and standalone agents keep
|
||||
/// normal global inheritance. Configs written before `preferred_runtime`
|
||||
/// existed implicitly belong to Buzz Agent.
|
||||
pub(crate) fn global_model_provider_for_record<'a>(
|
||||
record: &ManagedAgentRecord,
|
||||
personas: &[AgentDefinition],
|
||||
@@ -237,6 +241,15 @@ pub(crate) fn global_model_provider_for_record<'a>(
|
||||
return global_values;
|
||||
}
|
||||
|
||||
if !record.agent_command_override_is_implicit
|
||||
&& record
|
||||
.agent_command_override
|
||||
.as_deref()
|
||||
.is_some_and(|command| !command.trim().is_empty())
|
||||
{
|
||||
return global_values;
|
||||
}
|
||||
|
||||
let preferred_runtime = global
|
||||
.preferred_runtime
|
||||
.as_deref()
|
||||
|
||||
@@ -309,6 +309,7 @@ fn bare_record() -> ManagedAgentRecord {
|
||||
acp_command: "buzz-acp".to_string(),
|
||||
agent_command: "goose".to_string(),
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec![],
|
||||
mcp_command: "".to_string(),
|
||||
turn_timeout_seconds: 300,
|
||||
@@ -473,11 +474,36 @@ fn resolve_global_fallback_when_record_and_persona_have_none() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_fallback_does_not_inherit_defaults_from_a_different_preferred_runtime() {
|
||||
fn explicit_harness_override_keeps_defaults_from_a_different_preferred_runtime() {
|
||||
let mut record = bare_record();
|
||||
record.persona_id = Some("p1".to_string());
|
||||
record.agent_command_override = Some("goose".to_string());
|
||||
let personas = vec![persona("p1", None, None)];
|
||||
let global = GlobalAgentConfig {
|
||||
env_vars: BTreeMap::from([("BUZZ_AGENT_THINKING_EFFORT".to_string(), "high".to_string())]),
|
||||
model: Some("auto".to_string()),
|
||||
provider: Some("relay-mesh".to_string()),
|
||||
preferred_runtime: Some("buzz-agent".to_string()),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
resolve_effective_model_provider(&record, &personas, &global),
|
||||
(Some("auto".to_string()), Some("relay-mesh".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
global_env_vars_for_record(&record, &personas, &global),
|
||||
global.env_vars
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_fallback_override_masks_defaults_from_a_different_preferred_runtime() {
|
||||
let mut record = bare_record();
|
||||
record.persona_id = Some("p1".to_string());
|
||||
record.agent_command = "goose".to_string();
|
||||
record.agent_command_override = Some("goose".to_string());
|
||||
record.agent_command_override_is_implicit = true;
|
||||
let personas = vec![persona("p1", None, None)];
|
||||
let global = GlobalAgentConfig {
|
||||
model: Some("auto".to_string()),
|
||||
provider: Some("relay-mesh".to_string()),
|
||||
@@ -492,7 +518,7 @@ fn runtime_fallback_does_not_inherit_defaults_from_a_different_preferred_runtime
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_runtime_fallback_does_not_inherit_implicit_buzz_agent_defaults() {
|
||||
fn explicit_harness_override_keeps_legacy_global_defaults() {
|
||||
let mut record = bare_record();
|
||||
record.persona_id = Some("p1".to_string());
|
||||
record.agent_command_override = Some("goose".to_string());
|
||||
@@ -506,7 +532,7 @@ fn legacy_runtime_fallback_does_not_inherit_implicit_buzz_agent_defaults() {
|
||||
|
||||
assert_eq!(
|
||||
resolve_effective_model_provider(&record, &personas, &global),
|
||||
(None, None)
|
||||
(Some("auto".to_string()), Some("relay-mesh".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -445,6 +445,7 @@ fn make_agent(name: &str, persona_id: Option<&str>) -> ManagedAgentRecord {
|
||||
acp_command: String::new(),
|
||||
agent_command: String::new(),
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec![],
|
||||
mcp_command: String::new(),
|
||||
turn_timeout_seconds: 0,
|
||||
|
||||
@@ -439,6 +439,7 @@ pub fn apply_persona_snapshot(record: &mut ManagedAgentRecord, persona: &AgentDe
|
||||
{
|
||||
if !std::ptr::eq(pin_runtime, def_runtime) {
|
||||
record.agent_command_override = None;
|
||||
record.agent_command_override_is_implicit = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1475,6 +1475,7 @@ mod tests {
|
||||
acp_command: "buzz-acp".to_string(),
|
||||
agent_command: "buzz-agent".to_string(),
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec![],
|
||||
mcp_command: String::new(),
|
||||
turn_timeout_seconds: 320,
|
||||
|
||||
@@ -140,6 +140,7 @@ fn fixture(
|
||||
acp_command: "buzz-acp".into(),
|
||||
agent_command: "goose".into(),
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec![],
|
||||
mcp_command: String::new(),
|
||||
turn_timeout_seconds: 320,
|
||||
|
||||
@@ -14,6 +14,7 @@ fn record() -> ManagedAgentRecord {
|
||||
acp_command: "buzz-acp".into(),
|
||||
agent_command: "goose".into(),
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec![],
|
||||
mcp_command: String::new(),
|
||||
turn_timeout_seconds: 320,
|
||||
|
||||
@@ -263,6 +263,7 @@ mod tests {
|
||||
acp_command: "/usr/local/bin/acp".to_string(), // MUST NOT appear
|
||||
agent_command: "goose".to_string(), // MUST NOT appear
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec![],
|
||||
mcp_command: String::new(),
|
||||
turn_timeout_seconds: 120,
|
||||
|
||||
@@ -174,6 +174,7 @@ fn managed_agent(name: &str) -> ManagedAgentRecord {
|
||||
acp_command: "buzz-acp".to_string(),
|
||||
agent_command: "buzz-agent".to_string(),
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: vec![],
|
||||
mcp_command: String::new(),
|
||||
turn_timeout_seconds: 300,
|
||||
|
||||
@@ -95,6 +95,7 @@ impl AgentDefinition {
|
||||
acp_command: DEFAULT_ACP_COMMAND.to_string(),
|
||||
agent_command: String::new(),
|
||||
agent_command_override: None,
|
||||
agent_command_override_is_implicit: false,
|
||||
agent_args: Vec::new(),
|
||||
mcp_command: String::new(),
|
||||
turn_timeout_seconds: DEFAULT_AGENT_TURN_TIMEOUT_SECONDS,
|
||||
@@ -225,15 +226,21 @@ pub struct ManagedAgentRecord {
|
||||
pub avatar_url: Option<String>,
|
||||
pub acp_command: String,
|
||||
pub agent_command: String,
|
||||
/// Explicit per-instance harness pin. `None` (the default) means inherit
|
||||
/// the harness from the linked persona's `runtime`, so persona harness
|
||||
/// edits propagate on the next spawn — mirroring the opt-in `model`
|
||||
/// override. `Some` is set only when the user deliberately picks a harness
|
||||
/// that diverges from the persona. Resolved via `effective_agent_command`;
|
||||
/// `agent_command` above is the create-time snapshot kept for avatar/legacy
|
||||
/// derivations and is not authoritative for spawn.
|
||||
/// Per-instance harness pin. `None` (the default) means inherit the harness
|
||||
/// from the linked persona's `runtime`, so persona harness edits propagate
|
||||
/// on the next spawn — mirroring the opt-in `model` override. `Some` is
|
||||
/// normally an explicit divergent selection, but may preserve the visible
|
||||
/// automatic fallback chosen for a runtime-less persona; the accompanying
|
||||
/// marker distinguishes those intents. Resolved via
|
||||
/// `effective_agent_command`; `agent_command` above is the create-time
|
||||
/// snapshot kept for avatar/legacy derivations and is not authoritative for
|
||||
/// spawn.
|
||||
#[serde(default)]
|
||||
pub agent_command_override: Option<String>,
|
||||
/// Whether `agent_command_override` preserves an automatic runtime-less
|
||||
/// fallback rather than an explicit per-instance harness selection.
|
||||
#[serde(default)]
|
||||
pub agent_command_override_is_implicit: bool,
|
||||
pub agent_args: Vec<String>,
|
||||
/// Create-time snapshot of the catalog MCP command. Never read at spawn —
|
||||
/// the effective MCP command is always re-derived from the runtime catalog
|
||||
@@ -473,10 +480,12 @@ pub struct ManagedAgentSummary {
|
||||
pub relay_url: String,
|
||||
pub acp_command: String,
|
||||
pub agent_command: String,
|
||||
/// Mirrors `ManagedAgentRecord.agent_command_override`: `Some` when the user
|
||||
/// has explicitly pinned this instance's harness, `None` when it inherits
|
||||
/// from the persona. Lets the Edit dialog seed "Inherit from persona" vs a
|
||||
/// concrete pin (`agent_command` above is the resolved/effective command).
|
||||
/// Mirrors `ManagedAgentRecord.agent_command_override`: `Some` when this
|
||||
/// instance has a durable harness pin, `None` when it inherits from the
|
||||
/// persona. A pin may be an explicit selection or an automatic fallback
|
||||
/// for a runtime-less persona. Lets the Edit dialog seed "Inherit from
|
||||
/// persona" vs a concrete pin (`agent_command` above is the
|
||||
/// resolved/effective command).
|
||||
pub agent_command_override: Option<String>,
|
||||
pub agent_args: Vec<String>,
|
||||
/// Catalog-derived from the effective harness (not the record's stored
|
||||
|
||||
@@ -136,10 +136,14 @@ pub struct CreateManagedAgentRequest {
|
||||
pub acp_command: Option<String>,
|
||||
pub agent_command: Option<String>,
|
||||
/// True when `agent_command` must survive linked-persona inheritance.
|
||||
/// Includes explicit selections, installed aliases, and the visible
|
||||
/// implicit fallback for a runtime-less persona.
|
||||
/// Includes explicit selections, installed aliases, and automatic
|
||||
/// fallbacks for runtime-less personas.
|
||||
#[serde(default)]
|
||||
pub harness_override: bool,
|
||||
/// True when `harness_override` preserves an automatic runtime-less
|
||||
/// fallback rather than an explicit harness selection.
|
||||
#[serde(default)]
|
||||
pub implicit_harness_fallback: bool,
|
||||
#[serde(default)]
|
||||
pub agent_args: Vec<String>,
|
||||
/// Accepted for wire compatibility; not applied to the record. The
|
||||
|
||||
@@ -272,6 +272,22 @@ fn create_request_deserializes_camel_case_relay_mesh() {
|
||||
model_ref: "Qwen3".to_string()
|
||||
})
|
||||
);
|
||||
assert!(!request.implicit_harness_fallback);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_request_deserializes_implicit_harness_fallback_intent() {
|
||||
let request: CreateManagedAgentRequest = serde_json::from_str(
|
||||
r#"{
|
||||
"name": "fallback-agent",
|
||||
"harnessOverride": true,
|
||||
"implicitHarnessFallback": true
|
||||
}"#,
|
||||
)
|
||||
.expect("camelCase implicit fallback intent should deserialize");
|
||||
|
||||
assert!(request.harness_override);
|
||||
assert!(request.implicit_harness_fallback);
|
||||
}
|
||||
|
||||
/// Persisted records use snake_case; the camelCase alias must not break
|
||||
@@ -442,6 +458,7 @@ fn managed_agent_record_without_key_deserializes_empty() {
|
||||
.expect("keyring-backed record without inline key should deserialize");
|
||||
|
||||
assert_eq!(record.private_key_nsec, "");
|
||||
assert!(!record.agent_command_override_is_implicit);
|
||||
}
|
||||
|
||||
fn sample_agent_record() -> ManagedAgentRecord {
|
||||
|
||||
@@ -98,7 +98,11 @@ with a TypeScript lookup table or an id comparison in a component.
|
||||
without changing the persisted configuration used by existing agents.
|
||||
Legacy global defaults with no saved preferred runtime are treated as
|
||||
Buzz Agent-owned for this masking boundary, so hiding Buzz Agent cannot
|
||||
leak its provider/model into another implicit fallback.
|
||||
leak its provider/model into another implicit fallback. A persisted
|
||||
per-instance harness override carries a durable explicit-vs-implicit marker:
|
||||
explicit selections continue to inherit global provider/model defaults even
|
||||
when the selected harness differs from the global preference, while
|
||||
automatic runtime-less fallback pins keep mismatched defaults masked.
|
||||
Create-mode dialogs use the implicit masked config; existing definition and
|
||||
instance edit dialogs use the raw persisted config.
|
||||
`resolvePersonaRuntime` is the shared visibility boundary for every
|
||||
|
||||
@@ -66,6 +66,8 @@ export type CreateChannelManagedAgentInput = {
|
||||
* unpinned so the persona remains authoritative.
|
||||
*/
|
||||
harnessOverride?: boolean;
|
||||
/** The pin preserves an automatic runtime-less fallback, not an explicit pick. */
|
||||
implicitHarnessFallback?: boolean;
|
||||
/** Preferred model ID from the persona. Passed to createManagedAgent. */
|
||||
model?: string;
|
||||
role?: Exclude<ChannelRole, "owner">;
|
||||
@@ -355,6 +357,7 @@ export async function provisionChannelManagedAgent(
|
||||
acpCommand: "buzz-acp",
|
||||
agentCommand: input.runtime.command,
|
||||
harnessOverride: input.harnessOverride ?? false,
|
||||
implicitHarnessFallback: input.implicitHarnessFallback ?? false,
|
||||
// Do NOT seed agentArgs from runtime.defaultArgs (see instanceInputForDefinition.ts
|
||||
// for the rationale — empty args let spawn resolve definition args live).
|
||||
agentArgs: [],
|
||||
|
||||
@@ -84,12 +84,14 @@ test("row 2: harnessOverride follows the backend-aligned formula", async () => {
|
||||
gooseRuntime,
|
||||
);
|
||||
assert.equal(match.harnessOverride, true, "picked == configured → true");
|
||||
assert.equal(match.implicitHarnessFallback, false);
|
||||
|
||||
const noPreference = await buildInstanceInputForDefinition(
|
||||
persona({ runtime: undefined }),
|
||||
gooseRuntime,
|
||||
);
|
||||
assert.equal(noPreference.harnessOverride, true, "no preference → true");
|
||||
assert.equal(noPreference.implicitHarnessFallback, true);
|
||||
|
||||
const differs = await buildInstanceInputForDefinition(
|
||||
persona({ runtime: "claude" }),
|
||||
@@ -100,6 +102,7 @@ test("row 2: harnessOverride follows the backend-aligned formula", async () => {
|
||||
false,
|
||||
"picked != configured → false (definition stays authoritative)",
|
||||
);
|
||||
assert.equal(differs.implicitHarnessFallback, false);
|
||||
});
|
||||
|
||||
test("provisioning pins a visible fallback for a runtime-less definition", () => {
|
||||
@@ -111,6 +114,7 @@ test("provisioning pins a visible fallback for a runtime-less definition", () =>
|
||||
);
|
||||
assert.equal(resolved.runtime, gooseRuntime);
|
||||
assert.equal(resolved.harnessOverride, true);
|
||||
assert.equal(resolved.implicitHarnessFallback, true);
|
||||
});
|
||||
|
||||
test("provisioning uses product ordering and leaves unavailable configured fallbacks unpinned", () => {
|
||||
@@ -121,12 +125,14 @@ test("provisioning uses product ordering and leaves unavailable configured fallb
|
||||
]);
|
||||
assert.equal(ordered.runtime, buzzAgentRuntime);
|
||||
assert.equal(ordered.harnessOverride, true);
|
||||
assert.equal(ordered.implicitHarnessFallback, true);
|
||||
|
||||
const unavailable = resolveProvisioningRuntimeForDefinition("missing", [
|
||||
gooseRuntime,
|
||||
]);
|
||||
assert.equal(unavailable.runtime, gooseRuntime);
|
||||
assert.equal(unavailable.harnessOverride, false);
|
||||
assert.equal(unavailable.implicitHarnessFallback, false);
|
||||
});
|
||||
|
||||
test("row 3: plain avatar URLs pass through; base64 data URIs upload via the injectable", async () => {
|
||||
@@ -205,6 +211,7 @@ test("no backend intent is byte-identical to the pre-intent mapping", async () =
|
||||
agentArgs: [],
|
||||
mcpCommand: "goose-mcp",
|
||||
harnessOverride: true,
|
||||
implicitHarnessFallback: false,
|
||||
model: undefined,
|
||||
provider: undefined,
|
||||
spawnAfterCreate: true,
|
||||
|
||||
@@ -120,7 +120,10 @@ export function resolveProvisioningRuntimeForDefinition(
|
||||
runtimes: readonly AcpRuntime[],
|
||||
preferredRuntimeId?: string | null,
|
||||
disabledRuntimeIds: readonly string[] = getDisabledAcpRuntimeIdsSnapshot(),
|
||||
): ResolvePersonaRuntimeResult & { harnessOverride: boolean } {
|
||||
): ResolvePersonaRuntimeResult & {
|
||||
harnessOverride: boolean;
|
||||
implicitHarnessFallback: boolean;
|
||||
} {
|
||||
const defaultRuntime = getDefaultPersonaRuntime(runtimes, preferredRuntimeId);
|
||||
const resolved = resolvePersonaRuntime(
|
||||
definitionRuntimeId,
|
||||
@@ -137,6 +140,8 @@ export function resolveProvisioningRuntimeForDefinition(
|
||||
definitionRuntimeId,
|
||||
resolved.runtime.id,
|
||||
),
|
||||
implicitHarnessFallback:
|
||||
resolved.runtime !== null && !definitionRuntimeId?.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -207,6 +212,7 @@ export async function buildInstanceInputForDefinition(
|
||||
persona.runtime,
|
||||
runtime.id,
|
||||
),
|
||||
implicitHarnessFallback: !persona.runtime?.trim(),
|
||||
model: persona.model ?? undefined,
|
||||
provider: persona.provider ?? undefined,
|
||||
spawnAfterCreate: true,
|
||||
|
||||
@@ -160,6 +160,7 @@ export function AddTeamToChannelDialog({
|
||||
persona.runtime,
|
||||
personaRuntime.id,
|
||||
),
|
||||
implicitHarnessFallback: !persona.runtime?.trim(),
|
||||
model: persona.model ?? undefined,
|
||||
personaId: persona.id,
|
||||
teamId: team.id,
|
||||
|
||||
@@ -93,6 +93,7 @@ export function useApplyTemplate() {
|
||||
name: persona.displayName,
|
||||
personaId: persona.id,
|
||||
harnessOverride: resolved.harnessOverride,
|
||||
implicitHarnessFallback: resolved.implicitHarnessFallback,
|
||||
systemPrompt: persona.systemPrompt,
|
||||
avatarUrl: persona.avatarUrl ?? undefined,
|
||||
model: entry.model ?? persona.model ?? undefined,
|
||||
@@ -121,6 +122,7 @@ export function useApplyTemplate() {
|
||||
name: persona.displayName,
|
||||
personaId: persona.id,
|
||||
harnessOverride: resolved.harnessOverride,
|
||||
implicitHarnessFallback: resolved.implicitHarnessFallback,
|
||||
systemPrompt: persona.systemPrompt,
|
||||
avatarUrl: persona.avatarUrl ?? undefined,
|
||||
model: teamEntry.model ?? persona.model ?? undefined,
|
||||
|
||||
@@ -156,6 +156,7 @@ export function AddChannelBotDialog({
|
||||
name: persona.displayName,
|
||||
personaId: persona.id,
|
||||
harnessOverride: resolved.harnessOverride,
|
||||
implicitHarnessFallback: resolved.implicitHarnessFallback,
|
||||
systemPrompt: persona.systemPrompt,
|
||||
avatarUrl: persona.avatarUrl ?? undefined,
|
||||
model: persona.model ?? undefined,
|
||||
|
||||
@@ -32,7 +32,7 @@ export function useQuickBotDrop(channelId: string | null) {
|
||||
setState({ pending: true, error: null });
|
||||
|
||||
try {
|
||||
const { harnessOverride, runtime } =
|
||||
const { harnessOverride, implicitHarnessFallback, runtime } =
|
||||
resolveProvisioningRuntimeForDefinition(persona.runtime, providers);
|
||||
|
||||
if (!runtime) {
|
||||
@@ -51,6 +51,7 @@ export function useQuickBotDrop(channelId: string | null) {
|
||||
personaId: persona.id,
|
||||
model: persona.model ?? undefined,
|
||||
harnessOverride,
|
||||
implicitHarnessFallback,
|
||||
});
|
||||
|
||||
setState({ pending: false, error: null });
|
||||
|
||||
@@ -326,7 +326,7 @@ export function useMentionSendFlow({
|
||||
}
|
||||
seenPersonaIds.add(persona.id);
|
||||
|
||||
const { harnessOverride, runtime } =
|
||||
const { harnessOverride, implicitHarnessFallback, runtime } =
|
||||
resolveProvisioningRuntimeForDefinition(persona.runtime, runtimes);
|
||||
if (!runtime) {
|
||||
errors.push(`${displayName}: No agent runtime available.`);
|
||||
@@ -342,6 +342,7 @@ export function useMentionSendFlow({
|
||||
name: persona.displayName,
|
||||
personaId: persona.id,
|
||||
harnessOverride,
|
||||
implicitHarnessFallback,
|
||||
systemPrompt: persona.systemPrompt,
|
||||
avatarUrl: persona.avatarUrl ?? undefined,
|
||||
model: persona.model ?? undefined,
|
||||
|
||||
@@ -874,6 +874,7 @@ export async function createManagedAgent(input: CreateManagedAgentInput) {
|
||||
acpCommand: input.acpCommand,
|
||||
agentCommand: input.agentCommand,
|
||||
harnessOverride: input.harnessOverride ?? false,
|
||||
implicitHarnessFallback: input.implicitHarnessFallback ?? false,
|
||||
agentArgs: input.agentArgs,
|
||||
mcpCommand: input.mcpCommand,
|
||||
turnTimeoutSeconds: input.turnTimeoutSeconds,
|
||||
|
||||
@@ -456,6 +456,11 @@ export type CreateManagedAgentInput = {
|
||||
* same persona runtime id, while still ignoring missing-runtime fallbacks.
|
||||
*/
|
||||
harnessOverride?: boolean;
|
||||
/**
|
||||
* True when `harnessOverride` pins an automatically selected runtime for a
|
||||
* runtime-less persona rather than a runtime the user explicitly selected.
|
||||
*/
|
||||
implicitHarnessFallback?: boolean;
|
||||
agentArgs?: string[];
|
||||
mcpCommand?: string;
|
||||
turnTimeoutSeconds?: number;
|
||||
|
||||
Reference in New Issue
Block a user