Distinguish implicit harness fallback pins

Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
kenny lopez
2026-07-27 13:31:59 +01:00
parent 7b46102d04
commit fdc1ef2c6d
40 changed files with 245 additions and 43 deletions
@@ -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,
}
+16 -14
View File
@@ -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,
+20 -11
View File
@@ -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 {
+5 -1
View File
@@ -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,
+1
View File
@@ -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,
+5
View File
@@ -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;