Add Agent Config Core: harness capability model behind agent config surfaces (#2158)

This commit is contained in:
morgmart
2026-07-20 07:16:00 -07:00
committed by GitHub
parent 6da407742a
commit 6881a3376f
12 changed files with 629 additions and 118 deletions
@@ -9,85 +9,9 @@ use crate::managed_agents::{
AcpAvailabilityStatus, AcpRuntimeCatalogEntry, AuthStatus, CommandAvailabilityInfo,
};
pub(crate) struct KnownAcpRuntime {
pub id: &'static str,
pub label: &'static str,
pub commands: &'static [&'static str],
pub aliases: &'static [&'static str],
pub avatar_url: &'static str,
/// Legacy MCP server binary field. Vestigial — all agents now use the bundled CLI.
/// directly. Will be removed when runtime discovery is simplified.
pub mcp_command: Option<&'static str>,
/// Whether to enable MCP hook tools (`_Stop`, `_PostCompact`) for this agent.
pub mcp_hooks: bool,
/// CLI binary that indicates partial install (e.g. `"claude"` when `claude-agent-acp` is missing).
pub underlying_cli: Option<&'static str>,
/// Shell commands to install the runtime CLI itself (run sequentially).
pub cli_install_commands: &'static [&'static str],
/// Windows-specific CLI install commands (e.g. PowerShell installers).
/// When non-empty on Windows, these are used instead of `cli_install_commands`.
#[allow(dead_code)] // read only on Windows via cli_install_commands_for_os()
pub cli_install_commands_windows: &'static [&'static str],
/// Shell commands to install the ACP adapter (run sequentially, after CLI).
pub adapter_install_commands: &'static [&'static str],
/// Link to docs/repo for manual instructions.
pub install_instructions_url: &'static str,
/// Human-readable hint about installing the CLI binary.
pub cli_install_hint: &'static str,
/// Human-readable hint about installing the ACP adapter.
pub adapter_install_hint: &'static str,
/// Harness-specific skill discovery directory (e.g. `.goose/skills`).
/// `Some(dir)` → Buzz creates a symlink at `<nest>/<dir>/buzz-cli`
/// pointing to the canonical `.agents/skills/buzz-cli`. `None` → this
/// runtime reads the canonical path directly or has no skill support.
pub skill_dir: Option<&'static str>,
/// Whether this runtime handles model switching via ACP protocol natively.
/// Currently unused — env var injection runs unconditionally regardless of
/// this value. Retained as scaffolding for when ACP model switching matures.
#[allow(dead_code)]
pub supports_acp_model_switching: bool,
pub model_env_var: Option<&'static str>,
pub provider_env_var: Option<&'static str>,
pub provider_locked: bool,
pub default_env: &'static [(&'static str, &'static str)],
pub config_file_path: Option<&'static str>,
#[allow(dead_code)] // reserved for format-based dispatch when readers are unified
pub config_file_format: Option<&'static str>,
pub supports_acp_native_config: bool, // tier 1a: config/read+write
pub thinking_env_var: Option<&'static str>,
/// Env var for normalizing `max_output_tokens`. `None` when the harness
/// does not have a first-class env var for this field (config-file only).
pub max_tokens_env_var: Option<&'static str>,
/// Env var for normalizing `context_limit`. `None` when not applicable.
pub context_limit_env_var: Option<&'static str>,
/// Normalized field keys that must be set for this harness to function.
/// Used by the config bridge to mark fields as required in the UI.
/// Keys match the camelCase names used in `NormalizedConfig` (e.g. "model", "provider").
pub required_normalized_fields: &'static [&'static str],
/// Human-readable hint shown in Doctor when the runtime is available but not
/// authenticated. `None` for runtimes that have no login step (goose, buzz-agent).
pub login_hint: Option<&'static str>,
/// CLI args for probing authentication status. `args[0]` is the binary name;
/// the remainder are the subcommand. `None` for runtimes with no login step.
pub auth_probe_args: Option<&'static [&'static str]>,
}
mod runtime_metadata;
impl KnownAcpRuntime {
/// Return the CLI install commands for the current platform.
///
/// On Windows, returns `cli_install_commands_windows` when non-empty,
/// falling back to the default `cli_install_commands`. On other platforms
/// always returns `cli_install_commands`.
pub fn cli_install_commands_for_os(&self) -> &[&str] {
#[cfg(windows)]
{
if !self.cli_install_commands_windows.is_empty() {
return self.cli_install_commands_windows;
}
}
self.cli_install_commands
}
}
pub(crate) use runtime_metadata::KnownAcpRuntime;
const GOOSE_AVATAR_URL: &str = "https://goose-docs.ai/img/logo_dark.png";
const CLAUDE_CODE_AVATAR_URL: &str = "https://anthropic.gallerycdn.vsassets.io/extensions/anthropic/claude-code/2.1.77/1773707456892/Microsoft.VisualStudio.Services.Icons.Default";
@@ -1318,6 +1242,9 @@ pub fn discover_acp_runtimes() -> Vec<AcpRuntimeCatalogEntry> {
binary_path,
default_args,
mcp_command: runtime.mcp_command.map(str::to_string),
model_env_var: runtime.model_env_var.map(str::to_string),
provider_env_var: runtime.provider_env_var.map(str::to_string),
thinking_env_var: runtime.thinking_env_var.map(str::to_string),
install_hint,
install_instructions_url: runtime.install_instructions_url.to_string(),
can_auto_install,
@@ -0,0 +1,80 @@
/// Static capabilities and installation metadata for a known ACP runtime.
pub(crate) struct KnownAcpRuntime {
pub id: &'static str,
pub label: &'static str,
pub commands: &'static [&'static str],
pub aliases: &'static [&'static str],
pub avatar_url: &'static str,
/// Legacy MCP server binary field. Vestigial — all agents now use the bundled CLI
/// directly. Will be removed when runtime discovery is simplified.
pub mcp_command: Option<&'static str>,
/// Whether to enable MCP hook tools (`_Stop`, `_PostCompact`) for this agent.
pub mcp_hooks: bool,
/// CLI binary that indicates partial install (e.g. `"claude"` when `claude-agent-acp` is missing).
pub underlying_cli: Option<&'static str>,
/// Shell commands to install the runtime CLI itself (run sequentially).
pub cli_install_commands: &'static [&'static str],
/// Windows-specific CLI install commands (e.g. PowerShell installers).
/// When non-empty on Windows, these are used instead of `cli_install_commands`.
#[allow(dead_code)] // read only on Windows via cli_install_commands_for_os()
pub cli_install_commands_windows: &'static [&'static str],
/// Shell commands to install the ACP adapter (run sequentially, after CLI).
pub adapter_install_commands: &'static [&'static str],
/// Link to docs/repo for manual instructions.
pub install_instructions_url: &'static str,
/// Human-readable hint about installing the CLI binary.
pub cli_install_hint: &'static str,
/// Human-readable hint about installing the ACP adapter.
pub adapter_install_hint: &'static str,
/// Harness-specific skill discovery directory (e.g. `.goose/skills`).
/// `Some(dir)` → Buzz creates a symlink at `<nest>/<dir>/buzz-cli`
/// pointing to the canonical `.agents/skills/buzz-cli`. `None` → this
/// runtime reads the canonical path directly or has no skill support.
pub skill_dir: Option<&'static str>,
/// Whether this runtime handles model switching via ACP protocol natively.
/// Currently unused — env var injection runs unconditionally regardless of
/// this value. Retained as scaffolding for when ACP model switching matures.
#[allow(dead_code)]
pub supports_acp_model_switching: bool,
pub model_env_var: Option<&'static str>,
pub provider_env_var: Option<&'static str>,
pub provider_locked: bool,
pub default_env: &'static [(&'static str, &'static str)],
pub config_file_path: Option<&'static str>,
#[allow(dead_code)] // reserved for format-based dispatch when readers are unified
pub config_file_format: Option<&'static str>,
pub supports_acp_native_config: bool, // tier 1a: config/read+write
pub thinking_env_var: Option<&'static str>,
/// Env var for normalizing `max_output_tokens`. `None` when the harness
/// does not have a first-class env var for this field (config-file only).
pub max_tokens_env_var: Option<&'static str>,
/// Env var for normalizing `context_limit`. `None` when not applicable.
pub context_limit_env_var: Option<&'static str>,
/// Normalized field keys that must be set for this harness to function.
/// Used by the config bridge to mark fields as required in the UI.
/// Keys match the camelCase names used in `NormalizedConfig` (e.g. "model", "provider").
pub required_normalized_fields: &'static [&'static str],
/// Human-readable hint shown in Doctor when the runtime is available but not
/// authenticated. `None` for runtimes that have no login step (goose, buzz-agent).
pub login_hint: Option<&'static str>,
/// CLI args for probing authentication status. `args[0]` is the binary name;
/// the remainder are the subcommand. `None` for runtimes with no login step.
pub auth_probe_args: Option<&'static [&'static str]>,
}
impl KnownAcpRuntime {
/// Return the CLI install commands for the current platform.
///
/// On Windows, returns `cli_install_commands_windows` when non-empty,
/// falling back to the default `cli_install_commands`. On other platforms
/// always returns `cli_install_commands`.
pub fn cli_install_commands_for_os(&self) -> &[&str] {
#[cfg(windows)]
{
if !self.cli_install_commands_windows.is_empty() {
return self.cli_install_commands_windows;
}
}
self.cli_install_commands
}
}
@@ -573,6 +573,12 @@ pub struct AcpRuntimeCatalogEntry {
pub binary_path: Option<String>,
pub default_args: Vec<String>,
pub mcp_command: Option<String>,
/// Environment variable used to apply the initial model, when supported.
pub model_env_var: Option<String>,
/// Environment variable used to apply the selected LLM provider, when supported.
pub provider_env_var: Option<String>,
/// Environment variable used to apply thinking effort, when supported.
pub thinking_env_var: Option<String>,
pub install_hint: String,
pub install_instructions_url: String,
/// true when at least one automated install step is available
+77
View File
@@ -0,0 +1,77 @@
# Agent Configuration — Contributor Rules
Scope: `desktop/src/features/agents/` (config surfaces, shared config renderer,
and the agent config core). Read this before changing how harness / provider /
model / effort configuration is modeled, rendered, persisted, or applied.
Plan of record: `Buzz/Harness-Provider-Model.md` in Morgan's Obsidian vault
(PR sequence, decisions log). PRs: #2140 (rename), #2148 (flag reduction),
#2156 (honest model states), #2158 (Agent Config Core).
## The one rule
**Harness capability facts have exactly one source: the Rust runtime catalog.**
`KnownAcpRuntime` (`desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs`)
declares each harness's model/provider/effort env keys and capabilities. Spawn
applies them; `AcpRuntimeCatalogEntry` exposes them over IPC; and
`lib/agentConfigCore.ts` projects them into field descriptors. The frontend
never maintains a rival copy of this table.
If you need a new capability fact (a new env key, a native option, a "supports
X" flag): add it to `KnownAcpRuntime` first, expose it on
`AcpRuntimeCatalogEntry`, then project it through the core. Do not shortcut
with a TypeScript lookup table or an id comparison in a component.
## Rules
1. **No hardcoded harness-ID checks in render code.** `runtime.id === "claude"`
belongs in `deriveAgentConfigFieldModel` (once, with a named reason), never
in a component. Components ask the field model what exists
(`hasRenderableAgentConfigField`, `getRenderableEffortField`).
2. **Effort reads/writes go through the descriptor.** Use the effort
descriptor's `currentPersistence` key — never a raw
`BUZZ_AGENT_THINKING_EFFORT` literal in UI code. `currentPersistence` is
where the value lives *today*; `targetApplication` is how the harness
*should* receive it. They intentionally differ until PR 2.7 migrates
Goose/Claude — do not "fix" one to match the other without doing the
migration work.
3. **Field absence has a named reason, not a boolean.** Codex effort is
`ownedByModelId`; Claude effort is `deferredUntilNativeOptionsAvailable`.
New absences get new named reasons in `AgentConfigOmission` /
`render` — never a `showX` prop.
4. **The clearing policy is the named types.** `onContextChange:
"resetDependentValues"` (user changed harness/provider → dependent values
reset everywhere) vs `onCatalogMismatch: "explainOnly" | "onboardingCleanup"`
(an async catalog miss never silently erases saved state outside
onboarding's named cleanup). Do not add mutation booleans like
`clearInvalidModel`; extend the policy types.
5. **"Metadata unknown" ≠ "harness lacks the capability".** Passing
`runtime: undefined` to the core means fields won't render. Surfaces must
gate on the runtime catalog query settling (loading/error states) rather
than letting fields silently vanish — see `AgentDefaultsEditor` /
`DefaultConfigStep` for the pattern.
6. **One canonical behavior, disclosure presets for visibility.** Behavior
flags were deliberately killed in #2148 (`CANONICAL_CONFIG_BEHAVIORS`).
Surface differences are expressed via the `disclosure` preset, not new
boolean props.
7. **Onboarding does not change.** `onboarding-agent-defaults.spec.ts` is the
acceptance gate for anything touching the shared renderer.
## The tests that enforce this
- `lib/agentConfigCore.test.mjs` — field model per harness × scope, clearing
policy. Update when the capability model changes.
- `ui/agentConfigFieldsContract.test.mjs` — canonical behaviors + disclosure
presets. If this fails, you probably reintroduced a per-surface flag.
- `desktop/tests/e2e/onboarding-agent-defaults.spec.ts` — onboarding behavior
pin (35 tests).
- Rust: `runtime_metadata_env_vars` tests pin spawn-time key application.
## Keep this file true
**If you change how agent configuration is modeled, rendered, persisted,
applied, or cleared — update this file in the same PR.** A rule that no longer
matches the code is worse than no rule; a new pattern that isn't written down
here will be broken by the next agent that never learns it existed. Reviewers:
treat a config-behavior diff without a matching AGENTS.md diff (or an explicit
"no rules changed" note) as incomplete.
@@ -0,0 +1,154 @@
import assert from "node:assert/strict";
import test from "node:test";
import { deriveAgentConfigFieldModel } from "./agentConfigCore.ts";
const config = {
env_vars: { BUZZ_AGENT_THINKING_EFFORT: "high" },
model: "test-model",
preferred_runtime: null,
provider: "anthropic",
};
function runtime(id, metadata = {}) {
return {
id,
label: id,
avatarUrl: "",
availability: "available",
command: id,
binaryPath: id,
defaultArgs: [],
mcpCommand: null,
modelEnvVar: null,
providerEnvVar: null,
thinkingEnvVar: null,
installHint: "",
installInstructionsUrl: "",
canAutoInstall: false,
underlyingCliPath: null,
nodeRequired: false,
authStatus: { status: "not_applicable" },
loginHint: null,
...metadata,
};
}
function field(model, kind) {
return model.fields.find((candidate) => candidate.kind === kind);
}
test("Buzz Agent exposes provider, model, and Buzz-owned effort", () => {
const model = deriveAgentConfigFieldModel({
config,
runtime: runtime("buzz-agent", {
modelEnvVar: "BUZZ_AGENT_MODEL",
providerEnvVar: "BUZZ_AGENT_PROVIDER",
thinkingEnvVar: "BUZZ_AGENT_THINKING_EFFORT",
}),
scope: "global",
});
assert.deepEqual(
model.fields.map((item) => item.kind),
["provider", "model", "effort"],
);
assert.equal(field(model, "effort").optionSource, "buzzAgentCatalog");
assert.deepEqual(field(model, "effort").targetApplication, {
kind: "envVar",
key: "BUZZ_AGENT_THINKING_EFFORT",
});
});
test("Goose exposes provider, model, and its real effort application key", () => {
const model = deriveAgentConfigFieldModel({
config,
runtime: runtime("goose", {
modelEnvVar: "GOOSE_MODEL",
providerEnvVar: "GOOSE_PROVIDER",
thinkingEnvVar: "GOOSE_THINKING_EFFORT",
}),
scope: "global",
});
assert.equal(
field(model, "effort").optionSource,
"legacyProviderModelCatalog",
);
assert.deepEqual(field(model, "effort").currentPersistence, {
kind: "envVar",
key: "BUZZ_AGENT_THINKING_EFFORT",
});
assert.deepEqual(field(model, "effort").targetApplication, {
kind: "envVar",
key: "GOOSE_THINKING_EFFORT",
});
});
test("Claude models effort as a deferred native ACP option", () => {
const model = deriveAgentConfigFieldModel({
config,
runtime: runtime("claude"),
scope: "global",
});
assert.deepEqual(
model.fields.map((item) => item.kind),
["model", "effort"],
);
assert.equal(
field(model, "effort").render,
"deferredUntilNativeOptionsAvailable",
);
assert.deepEqual(field(model, "effort").currentPersistence, {
kind: "unavailable",
});
assert.deepEqual(field(model, "effort").targetApplication, {
kind: "acpConfigOption",
id: "effort",
category: "thought_level",
});
});
test("Codex omits separate effort because model IDs own it", () => {
const model = deriveAgentConfigFieldModel({
config,
runtime: runtime("codex"),
scope: "global",
});
assert.deepEqual(
model.fields.map((item) => item.kind),
["model"],
);
assert.deepEqual(model.omissions, [
{ kind: "effort", reason: "ownedByModelId" },
]);
});
test("catalog mismatch cleanup is named and restricted to onboarding", () => {
const selectedRuntime = runtime("buzz-agent", {
modelEnvVar: "BUZZ_AGENT_MODEL",
providerEnvVar: "BUZZ_AGENT_PROVIDER",
thinkingEnvVar: "BUZZ_AGENT_THINKING_EFFORT",
});
const onboarding = deriveAgentConfigFieldModel({
config,
runtime: selectedRuntime,
scope: "onboarding",
});
const evergreen = deriveAgentConfigFieldModel({
config,
runtime: selectedRuntime,
scope: "instance",
});
assert.deepEqual(onboarding.dependentValuePolicy, {
onContextChange: "resetDependentValues",
onCatalogMismatch: "onboardingCleanup",
});
assert.deepEqual(evergreen.dependentValuePolicy, {
onContextChange: "resetDependentValues",
onCatalogMismatch: "explainOnly",
});
});
@@ -0,0 +1,193 @@
import type {
AcpRuntimeCatalogEntry,
GlobalAgentConfig,
} from "@/shared/api/types";
import { BUZZ_AGENT_THINKING_EFFORT } from "../ui/buzzAgentConfig";
export type AgentConfigScope =
| "onboarding"
| "global"
| "definition"
| "instance";
export type DependentValuePolicy = {
onContextChange: "resetDependentValues";
onCatalogMismatch: "explainOnly" | "onboardingCleanup";
};
type NormalizedFieldPersistence = {
kind: "normalizedField";
field: "provider" | "model";
};
type EnvVarPersistence = {
kind: "envVar";
key: string;
};
type AcpConfigOptionPersistence = {
kind: "acpConfigOption";
id: string;
category: string;
};
type UnavailablePersistence = {
kind: "unavailable";
};
export type AgentConfigFieldDescriptor =
| {
kind: "provider";
optionSource: "providerCatalog";
persistence: NormalizedFieldPersistence;
targetApplication: { kind: "envVar"; key: string };
render: "control";
value: string | null;
}
| {
kind: "model";
optionSource: "acpModels";
persistence: NormalizedFieldPersistence;
targetApplication:
| { kind: "envVar"; key: string }
| { kind: "acpNative" };
render: "control";
value: string | null;
}
| {
kind: "effort";
optionSource:
| "buzzAgentCatalog"
| "legacyProviderModelCatalog"
| "harnessNative";
currentPersistence:
| EnvVarPersistence
| AcpConfigOptionPersistence
| UnavailablePersistence;
targetApplication:
| { kind: "envVar"; key: string }
| { kind: "acpConfigOption"; id: string; category: string };
render: "control" | "deferredUntilNativeOptionsAvailable";
value: string | null;
};
export type AgentConfigOmission = {
kind: "effort";
reason: "ownedByModelId" | "unsupportedByHarness";
};
export type AgentConfigFieldModel = {
fields: AgentConfigFieldDescriptor[];
omissions: AgentConfigOmission[];
dependentValuePolicy: DependentValuePolicy;
};
function valueFromEnv(config: GlobalAgentConfig, key: string) {
return config.env_vars[key]?.trim() || null;
}
/**
* Derives the harness-scoped field model consumed by agent config renderers.
*
* The runtime catalog is authoritative for environment-variable application.
* Harness-native ACP options are named here until discovery exposes them to the
* desktop; descriptors marked deferred must not be rendered as generic fields.
*/
export function deriveAgentConfigFieldModel({
config,
runtime,
scope,
}: {
config: GlobalAgentConfig;
runtime: AcpRuntimeCatalogEntry | undefined;
scope: AgentConfigScope;
}): AgentConfigFieldModel {
const fields: AgentConfigFieldDescriptor[] = [];
const omissions: AgentConfigOmission[] = [];
if (runtime?.providerEnvVar) {
fields.push({
kind: "provider",
optionSource: "providerCatalog",
persistence: { kind: "normalizedField", field: "provider" },
targetApplication: { kind: "envVar", key: runtime.providerEnvVar },
render: "control",
value: config.provider,
});
}
fields.push({
kind: "model",
optionSource: "acpModels",
persistence: { kind: "normalizedField", field: "model" },
targetApplication: runtime?.modelEnvVar
? { kind: "envVar", key: runtime.modelEnvVar }
: { kind: "acpNative" },
render: "control",
value: config.model,
});
if (runtime?.thinkingEnvVar) {
fields.push({
kind: "effort",
optionSource:
runtime.id === "buzz-agent"
? "buzzAgentCatalog"
: "legacyProviderModelCatalog",
currentPersistence: {
kind: "envVar",
key: BUZZ_AGENT_THINKING_EFFORT,
},
targetApplication: { kind: "envVar", key: runtime.thinkingEnvVar },
render: "control",
value: valueFromEnv(config, BUZZ_AGENT_THINKING_EFFORT),
});
} else if (runtime?.id === "claude") {
fields.push({
kind: "effort",
optionSource: "harnessNative",
currentPersistence: { kind: "unavailable" },
targetApplication: {
kind: "acpConfigOption",
id: "effort",
category: "thought_level",
},
render: "deferredUntilNativeOptionsAvailable",
value: null,
});
} else {
omissions.push({
kind: "effort",
reason:
runtime?.id === "codex" ? "ownedByModelId" : "unsupportedByHarness",
});
}
return {
fields,
omissions,
dependentValuePolicy: {
onContextChange: "resetDependentValues",
onCatalogMismatch:
scope === "onboarding" ? "onboardingCleanup" : "explainOnly",
},
};
}
export function hasRenderableAgentConfigField(
model: AgentConfigFieldModel,
kind: AgentConfigFieldDescriptor["kind"],
) {
return model.fields.some(
(field) => field.kind === kind && field.render === "control",
);
}
export function getRenderableEffortField(
model: AgentConfigFieldModel,
): Extract<AgentConfigFieldDescriptor, { kind: "effort" }> | undefined {
return model.fields.find(
(field): field is Extract<AgentConfigFieldDescriptor, { kind: "effort" }> =>
field.kind === "effort" && field.render === "control",
);
}
@@ -16,6 +16,11 @@ import type {
import { cn } from "@/shared/lib/cn";
import { EnvVarsEditor } from "@/features/agents/ui/EnvVarsEditor";
import type { InheritedEnvRow } from "@/features/agents/ui/EnvVarsEditor";
import {
deriveAgentConfigFieldModel,
getRenderableEffortField,
hasRenderableAgentConfigField,
} from "@/features/agents/lib/agentConfigCore";
import {
getBakedProviderInheritLabel,
getGlobalModelFallback,
@@ -132,12 +137,6 @@ export type AgentConfigFieldsProps = {
* match onboarding.
*/
disclosure?: "full" | "onboarding-essential";
/**
* Harness-conditional, not part of the disclosure preset: provider choice
* only exists for runtimes that support LLM provider selection. PR 3
* derives this internally from selectedRuntime and deletes the prop.
*/
showProviderField?: boolean;
unstyled?: boolean;
useCustomSelect?: boolean;
useChevronSelectIcon?: boolean;
@@ -156,7 +155,6 @@ export function AgentConfigFields({
placeholderClassName,
selectClassName,
disclosure = "full",
showProviderField = true,
unstyled = false,
useCustomSelect = false,
useChevronSelectIcon = false,
@@ -172,12 +170,29 @@ export function AgentConfigFields({
showUnavailableEffortOptions,
} = resolveDisclosure(disclosure);
const fieldModel = React.useMemo(
() =>
deriveAgentConfigFieldModel({
config,
runtime: selectedRuntime,
scope: disclosure === "onboarding-essential" ? "onboarding" : "global",
}),
[config, disclosure, selectedRuntime],
);
const effortField = getRenderableEffortField(fieldModel);
const effortPersistenceKey =
effortField?.currentPersistence.kind === "envVar"
? effortField.currentPersistence.key
: null;
const bakedProvider = React.useMemo(
() => bakedEnv.find((e) => e.key === "BUZZ_AGENT_PROVIDER")?.value ?? null,
[bakedEnv],
);
const selectedRuntimeId = selectedRuntime?.id ?? "";
const providerFieldVisible = showProviderField;
const providerFieldVisible = hasRenderableAgentConfigField(
fieldModel,
"provider",
);
const effectiveProvider = providerFieldVisible
? config.provider?.trim() || bakedProvider || ""
: "";
@@ -254,7 +269,8 @@ export function AgentConfigFields({
// Evergreen surfaces (Settings, dialogs) edit saved data that may pair with
// higher layers (see PR #2148 review thread), so they only act after the
// user explicitly edits the provider in this session.
const healOnMount = disclosure === "onboarding-essential";
const healOnMount =
fieldModel.dependentValuePolicy.onCatalogMismatch === "onboardingCleanup";
const userEditedProviderRef = React.useRef(false);
// Read inside effects via ref so biome's exhaustive-deps stays honest:
// refs are stable, and healOnMount is captured at declaration.
@@ -295,8 +311,9 @@ export function AgentConfigFields({
selectedRuntimeId,
]);
const currentEffortForAutoClear =
config.env_vars[BUZZ_AGENT_THINKING_EFFORT] ?? "";
const currentEffortForAutoClear = effortPersistenceKey
? (config.env_vars[effortPersistenceKey] ?? "")
: "";
// When the selected harness changes outside this component (Back → setup
// page → choose a different harness → Next), the saved model can belong to
@@ -315,7 +332,7 @@ export function AgentConfigFields({
}
const nextEnvVars = { ...config.env_vars };
delete nextEnvVars[BUZZ_AGENT_THINKING_EFFORT];
if (effortPersistenceKey) delete nextEnvVars[effortPersistenceKey];
onCustomModelEditingChange(false);
onConfigChange({ ...config, env_vars: nextEnvVars, model: null });
}, [
@@ -325,6 +342,7 @@ export function AgentConfigFields({
onConfigChange,
onCustomModelEditingChange,
healOnMount,
effortPersistenceKey,
]);
// Orphan-model clearing follows the mount-time healing policy above: the
@@ -346,7 +364,7 @@ export function AgentConfigFields({
}
const nextEnvVars = { ...config.env_vars };
delete nextEnvVars[BUZZ_AGENT_THINKING_EFFORT];
if (effortPersistenceKey) delete nextEnvVars[effortPersistenceKey];
onCustomModelEditingChange(false);
onConfigChange({ ...config, env_vars: nextEnvVars, model: null });
}, [
@@ -355,6 +373,7 @@ export function AgentConfigFields({
dependentFieldsDisabled,
onConfigChange,
onCustomModelEditingChange,
effortPersistenceKey,
]);
const { validValues: effortValidForAutoClear } = getProviderEffortConfig(
config.provider ?? "",
@@ -365,7 +384,7 @@ export function AgentConfigFields({
effortValid: effortValidForAutoClear,
onClear: () => {
const nextEnvVars = { ...config.env_vars };
delete nextEnvVars[BUZZ_AGENT_THINKING_EFFORT];
if (effortPersistenceKey) delete nextEnvVars[effortPersistenceKey];
onConfigChange({ ...config, env_vars: nextEnvVars });
},
});
@@ -423,11 +442,13 @@ export function AgentConfigFields({
}
function handleEnvVarsChange(next: Record<string, string>) {
const effort = config.env_vars[BUZZ_AGENT_THINKING_EFFORT];
const merged =
effort !== undefined
? { ...next, [BUZZ_AGENT_THINKING_EFFORT]: effort }
: next;
const effort = effortPersistenceKey
? config.env_vars[effortPersistenceKey]
: undefined;
const merged = { ...next };
if (effortPersistenceKey && effort !== undefined) {
merged[effortPersistenceKey] = effort;
}
onConfigChange({ ...config, env_vars: merged });
}
@@ -481,16 +502,10 @@ export function AgentConfigFields({
: implicitEffortProvider;
const { validValues: effortValid, defaultValue: effortDefault } =
getProviderEffortConfig(effortProvider, config.model ?? "");
const currentEffort = config.env_vars[BUZZ_AGENT_THINKING_EFFORT] ?? "";
// Claude Code and Codex both have harness-native effort semantics that are
// not represented by Buzz Agent's generic BUZZ_AGENT_THINKING_EFFORT env var:
// Claude exposes ACP config option `effort`; Codex currently encodes effort
// into model ids (e.g. `gpt-5.5[low]`). Hide the generic control until the
// config core can render harness-native options honestly.
const effortFieldVisible =
showEffortField &&
selectedRuntimeId !== "claude" &&
selectedRuntimeId !== "codex";
const currentEffort = effortPersistenceKey
? (config.env_vars[effortPersistenceKey] ?? "")
: "";
const effortFieldVisible = showEffortField && effortField !== undefined;
const fieldClassName = unstyled ? "space-y-4" : "space-y-1.5 p-3";
const blockClassName = unstyled ? "" : "p-3";
@@ -684,9 +699,11 @@ export function AgentConfigFields({
onChange={(value) => {
const nextEnvVars = { ...config.env_vars };
if (value === "") {
delete nextEnvVars[BUZZ_AGENT_THINKING_EFFORT];
if (effortPersistenceKey)
delete nextEnvVars[effortPersistenceKey];
} else {
nextEnvVars[BUZZ_AGENT_THINKING_EFFORT] = value;
if (effortPersistenceKey)
nextEnvVars[effortPersistenceKey] = value;
}
onConfigChange({ ...config, env_vars: nextEnvVars });
}}
@@ -111,6 +111,11 @@ export function AgentDefaultsEditor({
() => (runtimesQuery.data ?? []).find((r) => r.id === "buzz-agent"),
[runtimesQuery.data],
);
const configSurfaceLoading = isLoading || runtimesQuery.isLoading;
const configSurfaceError =
loadError ||
runtimesQuery.isError ||
(!configSurfaceLoading && buzzAgentRuntime === undefined);
function handleConfigChange(next: GlobalAgentConfig) {
configRef.current = next;
@@ -167,12 +172,12 @@ export function AgentDefaultsEditor({
return (
<div className="min-w-0 space-y-4">
{isLoading ? (
{configSurfaceLoading ? (
<div className="flex items-center gap-2 py-4 text-sm text-muted-foreground">
<Loader className="size-4 animate-spin" />
Loading
</div>
) : loadError ? (
) : configSurfaceError ? (
<div className="flex items-center gap-2 py-4 text-sm text-destructive">
<AlertCircle className="size-4" />
Couldn't load agent defaults. Restart the app to try again.
@@ -192,7 +197,7 @@ export function AgentDefaultsEditor({
)}
{/* Save bar */}
{!isLoading && !loadError && (
{!configSurfaceLoading && !configSurfaceError && (
<div className="mt-4 flex flex-wrap items-center gap-3">
{saveState === "saved" && (
<span className="flex min-w-0 items-center gap-1 text-sm text-green-600 dark:text-green-400">
@@ -129,8 +129,12 @@ function AgentDefaultsSection({
}, [config.preferred_runtime, selectedRuntimes]);
const selectedRuntimeId =
selectedRuntime?.id ?? config.preferred_runtime ?? "";
const selectedRuntimeSupportsModelProvider =
runtimeSupportsLlmProviderSelection(selectedRuntimeId);
const configSurfaceLoading = isLoading || runtimesQuery.isLoading;
const configSurfaceError =
runtimesQuery.isError ||
(!configSurfaceLoading &&
selectedRuntimeIds.length > 0 &&
!selectedRuntime);
const harnessOptions = React.useMemo(
() =>
selectedRuntimes.map((runtime) => ({
@@ -182,11 +186,15 @@ function AgentDefaultsSection({
return (
<section className="w-full space-y-4 text-left text-sm">
{isLoading ? (
{configSurfaceLoading ? (
<div className="flex items-center justify-center gap-2 py-4 text-sm text-muted-foreground">
<Spinner className="h-4 w-4 border-2" />
Loading
</div>
) : configSurfaceError ? (
<p className="py-4 text-center text-sm text-destructive">
Couldn't load harness settings. Go back and try again.
</p>
) : (
<div className="space-y-7">
<div className="space-y-4">
@@ -226,7 +234,6 @@ function AgentDefaultsSection({
placeholderClassName="text-foreground/70"
selectClassName="h-12 rounded-2xl border-foreground/15 bg-white px-4 py-2 text-sm shadow-none hover:bg-white/95"
disclosure="onboarding-essential"
showProviderField={selectedRuntimeSupportsModelProvider}
unstyled
useCustomSelect
/>
+6
View File
@@ -174,6 +174,9 @@ export type RawAcpRuntimeCatalogEntry = {
binary_path: string | null;
default_args: string[];
mcp_command: string | null;
model_env_var?: string | null;
provider_env_var?: string | null;
thinking_env_var?: string | null;
install_hint: string;
install_instructions_url: string;
can_auto_install: boolean;
@@ -710,6 +713,9 @@ function fromRawAcpRuntimeCatalogEntry(
binaryPath: entry.binary_path,
defaultArgs: entry.default_args,
mcpCommand: entry.mcp_command,
modelEnvVar: entry.model_env_var ?? null,
providerEnvVar: entry.provider_env_var ?? null,
thinkingEnvVar: entry.thinking_env_var ?? null,
installHint: entry.install_hint,
installInstructionsUrl: entry.install_instructions_url,
canAutoInstall: entry.can_auto_install,
+6
View File
@@ -517,6 +517,12 @@ export type AcpRuntimeCatalogEntry = {
binaryPath: string | null;
defaultArgs: string[];
mcpCommand: string | null;
/** Environment variable used to apply the initial model, when supported. */
modelEnvVar: string | null;
/** Environment variable used to apply the selected LLM provider, when supported. */
providerEnvVar: string | null;
/** Environment variable used to apply thinking effort, when supported. */
thinkingEnvVar: string | null;
installHint: string;
installInstructionsUrl: string;
canAutoInstall: boolean;
+35 -2
View File
@@ -6689,6 +6689,38 @@ async function handleListRelayAgents(
return mockRelayAgents.map(cloneRelayAgent);
}
function withMockRuntimeConfigMetadata(
runtime: RawAcpRuntimeCatalogEntry,
): RawAcpRuntimeCatalogEntry {
return {
...runtime,
model_env_var:
"model_env_var" in runtime
? runtime.model_env_var
: runtime.id === "buzz-agent"
? "BUZZ_AGENT_MODEL"
: runtime.id === "goose"
? "GOOSE_MODEL"
: null,
provider_env_var:
"provider_env_var" in runtime
? runtime.provider_env_var
: runtime.id === "buzz-agent"
? "BUZZ_AGENT_PROVIDER"
: runtime.id === "goose"
? "GOOSE_PROVIDER"
: null,
thinking_env_var:
"thinking_env_var" in runtime
? runtime.thinking_env_var
: runtime.id === "buzz-agent"
? "BUZZ_AGENT_THINKING_EFFORT"
: runtime.id === "goose"
? "GOOSE_THINKING_EFFORT"
: null,
};
}
async function handleDiscoverAcpRuntimes(
config: E2eConfig | undefined,
): Promise<RawAcpRuntimeCatalogEntry[]> {
@@ -6701,9 +6733,9 @@ async function handleDiscoverAcpRuntimes(
const configured = config?.mock?.acpRuntimesCatalog;
if (configured) {
return configured;
return configured.map(withMockRuntimeConfigMetadata);
}
return [
const defaultCatalog: RawAcpRuntimeCatalogEntry[] = [
{
id: "goose",
label: "Goose",
@@ -6775,6 +6807,7 @@ async function handleDiscoverAcpRuntimes(
login_hint: undefined,
},
];
return defaultCatalog.map(withMockRuntimeConfigMetadata);
}
async function handleDiscoverAcpAuthMethods(