mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Scope defaults to visible harness fallbacks
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -217,6 +217,24 @@ fn deploy_resolver_returns_none_for_orphaned_instance() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deploy_resolver_ignores_defaults_from_a_different_implicit_runtime() {
|
||||
let mut record = bare_agent_record(Some("p1"), None, None);
|
||||
record.agent_command_override = Some("goose".to_string());
|
||||
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 normalize_relay_mesh_rejects_empty_model_ref() {
|
||||
let config = RelayMeshConfig {
|
||||
|
||||
@@ -207,6 +207,53 @@ pub fn save_global_agent_config(app: &AppHandle, config: &GlobalAgentConfig) ->
|
||||
atomic_write_json_restricted(&path, &payload)
|
||||
}
|
||||
|
||||
/// Return the global provider/model values that are safe for this record.
|
||||
///
|
||||
/// 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.
|
||||
pub(crate) fn global_model_provider_for_record<'a>(
|
||||
record: &ManagedAgentRecord,
|
||||
personas: &[AgentDefinition],
|
||||
global: &'a GlobalAgentConfig,
|
||||
) -> (Option<&'a str>, Option<&'a str>) {
|
||||
let global_values = (global.model.as_deref(), global.provider.as_deref());
|
||||
if record.persona_id.is_none() {
|
||||
return global_values;
|
||||
}
|
||||
|
||||
let definition_runtime = record.runtime.as_deref().or_else(|| {
|
||||
record
|
||||
.persona_id
|
||||
.as_deref()
|
||||
.and_then(|id| personas.iter().find(|persona| persona.id == id))
|
||||
.and_then(|persona| persona.runtime.as_deref())
|
||||
});
|
||||
if definition_runtime.is_some_and(|runtime| !runtime.trim().is_empty()) {
|
||||
return global_values;
|
||||
}
|
||||
|
||||
let Some(selected_runtime) = record
|
||||
.agent_command_override
|
||||
.as_deref()
|
||||
.and_then(crate::managed_agents::known_acp_runtime)
|
||||
else {
|
||||
return global_values;
|
||||
};
|
||||
let preferred_runtime = global
|
||||
.preferred_runtime
|
||||
.as_deref()
|
||||
.and_then(crate::managed_agents::known_acp_runtime);
|
||||
|
||||
if preferred_runtime.is_some_and(|preferred| std::ptr::eq(preferred, selected_runtime)) {
|
||||
global_values
|
||||
} else {
|
||||
(None, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the effective model and provider for an agent.
|
||||
///
|
||||
/// Delegates to `effective_config::resolve_effective_config` which enforces
|
||||
|
||||
@@ -472,6 +472,63 @@ fn resolve_global_fallback_when_record_and_persona_have_none() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_fallback_does_not_inherit_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 {
|
||||
model: Some("auto".to_string()),
|
||||
provider: Some("relay-mesh".to_string()),
|
||||
preferred_runtime: Some("buzz-agent".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
resolve_effective_model_provider(&record, &personas, &global),
|
||||
(None, None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_fallback_inherits_defaults_when_it_matches_the_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 {
|
||||
model: Some("global-model".to_string()),
|
||||
provider: Some("global-provider".to_string()),
|
||||
preferred_runtime: Some("goose".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
resolve_effective_model_provider(&record, &personas, &global),
|
||||
(Some("global-model"), Some("global-provider"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_runtime_keeps_global_defaults_when_another_runtime_is_preferred() {
|
||||
let mut record = bare_record();
|
||||
record.persona_id = Some("p1".to_string());
|
||||
record.runtime = Some("goose".to_string());
|
||||
let personas = vec![persona("p1", None, None)];
|
||||
let global = GlobalAgentConfig {
|
||||
model: Some("global-model".to_string()),
|
||||
provider: Some("global-provider".to_string()),
|
||||
preferred_runtime: Some("buzz-agent".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
resolve_effective_model_provider(&record, &personas, &global),
|
||||
(Some("global-model"), Some("global-provider"))
|
||||
);
|
||||
}
|
||||
|
||||
/// Tier 4 — no persona linked: record.persona_id is None, record has no
|
||||
/// model/provider; global defaults must still fill in (persona lookup skipped).
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
|
||||
@@ -52,8 +52,8 @@ pub use env_vars::*;
|
||||
pub(crate) use git_bash::git_bash_available;
|
||||
pub(crate) use git_bash::{discover_git_bash, GitBashPrerequisite};
|
||||
pub(crate) use global_config::{
|
||||
load_global_agent_config, resolve_effective_model_provider, save_global_agent_config,
|
||||
validate_global_config, GlobalAgentConfig,
|
||||
global_model_provider_for_record, load_global_agent_config, resolve_effective_model_provider,
|
||||
save_global_agent_config, validate_global_config, GlobalAgentConfig,
|
||||
};
|
||||
pub(crate) use managed_node_paths::*;
|
||||
pub use nest::*;
|
||||
|
||||
@@ -93,9 +93,12 @@ with a TypeScript lookup table or an id comparison in a component.
|
||||
runtime off must not uninstall it, stop it, or invalidate an existing
|
||||
agent that already uses it. If the disabled runtime was the saved global
|
||||
default, consumers immediately ignore that preference and the defaults
|
||||
editor persists its visible fallback on the next save. Runtime-less agent
|
||||
starts and team deploys must filter through the same visible-runtime set;
|
||||
definitions already pinned to a hidden runtime remain runnable.
|
||||
editor persists its visible fallback on the next save. Its dependent
|
||||
provider/model defaults are also ignored for new implicit fallback agents,
|
||||
without changing the persisted configuration used by existing agents.
|
||||
Runtime-less agent starts and team deploys must filter through the same
|
||||
visible-runtime set; definitions already pinned to a hidden runtime remain
|
||||
runnable.
|
||||
10. **The defaults modal is progressively disclosed.** An unset global config
|
||||
starts on the Buzz Agent-first deployment fallback and carries that visible
|
||||
harness into the next saved edit. The `progressive-defaults` disclosure
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
availableRuntimesForStart,
|
||||
buildInstanceInputForDefinition,
|
||||
resolveStartRuntimeForDefinition,
|
||||
shouldPinSelectedRuntimeForDefinition,
|
||||
} from "./instanceInputForDefinition.ts";
|
||||
|
||||
// ── Phase 1B.3.5: the single definition→instance mapping ────────────────────
|
||||
@@ -74,6 +75,9 @@ test("row 4: create input never contains definition env vars", async () => {
|
||||
});
|
||||
|
||||
test("row 2: harnessOverride follows the backend-aligned formula", async () => {
|
||||
assert.equal(shouldPinSelectedRuntimeForDefinition(undefined, "goose"), true);
|
||||
assert.equal(shouldPinSelectedRuntimeForDefinition("claude", "goose"), false);
|
||||
|
||||
const match = await buildInstanceInputForDefinition(
|
||||
persona({ runtime: "goose" }),
|
||||
gooseRuntime,
|
||||
|
||||
@@ -88,6 +88,16 @@ export type BackendIntent = {
|
||||
config: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/** Keep every definition-start surface aligned on runtime pinning semantics. */
|
||||
export function shouldPinSelectedRuntimeForDefinition(
|
||||
definitionRuntimeId: string | null | undefined,
|
||||
selectedRuntimeId: string,
|
||||
): boolean {
|
||||
return (
|
||||
!definitionRuntimeId || definitionRuntimeId.trim() === selectedRuntimeId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The single definition→instance mapping (Phase 1B.3.5 rows 2–4). Every
|
||||
* surface that creates a running instance from a definition builds its
|
||||
@@ -151,7 +161,10 @@ export async function buildInstanceInputForDefinition(
|
||||
// at top of this function).
|
||||
agentArgs: [],
|
||||
mcpCommand: runtime.mcpCommand ?? "",
|
||||
harnessOverride: !persona.runtime || persona.runtime === runtime.id,
|
||||
harnessOverride: shouldPinSelectedRuntimeForDefinition(
|
||||
persona.runtime,
|
||||
runtime.id,
|
||||
),
|
||||
model: persona.model ?? undefined,
|
||||
provider: persona.provider ?? undefined,
|
||||
spawnAfterCreate: true,
|
||||
|
||||
@@ -66,16 +66,18 @@ test("stored runtime visibility is read from the versioned device key", () => {
|
||||
assert.deepEqual(readDisabledAcpRuntimeIds(storage), ["claude"]);
|
||||
});
|
||||
|
||||
test("a disabled saved runtime is removed from the effective preference", () => {
|
||||
test("a disabled saved runtime and its dependent defaults are masked", () => {
|
||||
const config = {
|
||||
env_vars: {},
|
||||
provider: null,
|
||||
model: null,
|
||||
provider: "relay-mesh",
|
||||
model: "auto",
|
||||
preferred_runtime: "Goose",
|
||||
};
|
||||
|
||||
assert.deepEqual(maskDisabledAcpRuntimePreference(config, ["goose"]), {
|
||||
...config,
|
||||
provider: null,
|
||||
model: null,
|
||||
preferred_runtime: null,
|
||||
});
|
||||
assert.equal(maskDisabledAcpRuntimePreference(config, ["claude"]), config);
|
||||
|
||||
@@ -110,13 +110,20 @@ export function runtimesForImplicitAcpSelection<T extends { id: string }>(
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent a disabled runtime from remaining the effective global preference.
|
||||
* Prevent a disabled runtime and its dependent defaults from remaining
|
||||
* effective for new implicit selections.
|
||||
*
|
||||
* The persisted config is left untouched until the user next saves defaults;
|
||||
* consumers immediately fall back through the normal runtime selection path.
|
||||
* existing agents keep their configuration while new consumers immediately
|
||||
* fall back through the normal runtime selection path without carrying a
|
||||
* provider or model selected for the hidden harness.
|
||||
*/
|
||||
export function maskDisabledAcpRuntimePreference<
|
||||
T extends { preferred_runtime: string | null },
|
||||
T extends {
|
||||
model: string | null;
|
||||
preferred_runtime: string | null;
|
||||
provider: string | null;
|
||||
},
|
||||
>(config: T, disabledRuntimeIds: readonly string[]): T {
|
||||
const preferredRuntime = config.preferred_runtime;
|
||||
if (
|
||||
@@ -128,7 +135,12 @@ export function maskDisabledAcpRuntimePreference<
|
||||
return config;
|
||||
}
|
||||
|
||||
return { ...config, preferred_runtime: null };
|
||||
return {
|
||||
...config,
|
||||
model: null,
|
||||
preferred_runtime: null,
|
||||
provider: null,
|
||||
};
|
||||
}
|
||||
|
||||
function getDisabledRuntimeIdsSnapshot(): readonly string[] {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
emptyResolvedTeamPersonas,
|
||||
resolveTeamPersonas,
|
||||
} from "@/features/agents/lib/teamPersonas";
|
||||
import { shouldPinSelectedRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
|
||||
import { useSelectableAcpRuntimes } from "@/features/agents/lib/runtimeVisibilityPreference";
|
||||
import {
|
||||
collectRuntimeWarnings,
|
||||
@@ -147,6 +148,10 @@ export function AddTeamToChannelDialog({
|
||||
name: persona.displayName,
|
||||
systemPrompt: persona.systemPrompt,
|
||||
avatarUrl: persona.avatarUrl ?? undefined,
|
||||
harnessOverride: shouldPinSelectedRuntimeForDefinition(
|
||||
persona.runtime,
|
||||
runtimeToUse.id,
|
||||
),
|
||||
model: persona.model ?? undefined,
|
||||
personaId: persona.id,
|
||||
teamId: team.id,
|
||||
|
||||
Reference in New Issue
Block a user