feat(agents): model-tuning parity in global Agent Defaults editor (#4578)

## Overview

The global Agent Defaults surface (Settings card, defaults modal,
onboarding) exposed structured controls for Effort but left Max Output
Tokens, Context Limit, and Max Rounds as raw env vars. Per-agent dialogs
had structured numeric fields but only for `isBuzzAgentRuntime` —
incorrectly excluding Goose. This PR unifies numeric-tuning capability
across all surfaces, fixes a pre-existing dual-editor defect, and adds
full test coverage.

## What changed

### Phase 1 — Catalog projection

- Add `max_rounds_env_var` to `KnownAcpRuntime` in `runtime_metadata.rs`
(`Some("BUZZ_AGENT_MAX_ROUNDS")` for buzz-agent, `None` elsewhere).
- Project all three numeric env-var fields (`max_tokens_env_var`,
`context_limit_env_var`, `max_rounds_env_var`) end-to-end:
`AcpRuntimeCatalogEntry` Rust struct, TS `types.ts`,
`RawAcpRuntimeCatalogEntry` + `fromRawAcpRuntimeCatalogEntry` in
`tauri.ts`, and the e2e mock bridge (`withMockRuntimeConfigMetadata`).

### Phase 2 — Field model

- `deriveAgentConfigFieldModel` now derives `maxOutputTokens` /
`contextLimit` / `maxRounds` descriptors from catalog-projected fields.
- `structuredEnvKeys(descriptors)` — exported helper that takes the
**rendered** descriptor set (not the whole model). Hidden keys follow
what is actually rendered per surface: global hides effort + all three
numeric keys for buzz-agent / two for Goose; per-agent buzz-agent hides
effort + three numeric keys; per-agent Goose hides only its two numeric
keys. `BUZZ_AGENT_THINKING_EFFORT` stays a visible generic env row
per-agent because no effort control renders there.

### Phase 3 — UI

- Extract `NumericTuningFields` from `buzzAgentModelTuningFields.tsx` as
a shared descriptor-driven component (`descriptors`, `envVars`,
`inheritedEnvVars`, `onEnvVarChange`). Kind-specific minima:
`NUMERIC_KIND_MIN` map (`maxOutputTokens`/`contextLimit`: 1,
`maxRounds`: 0) applied to `<input min>`.
- **Global surface** (`AgentConfigFields.tsx`): deduplicate the
previously duplicated Advanced env-editor block; render
`NumericTuningFields` below the env editor when descriptors exist;
`hiddenKeys` and `bakedGenericRows` exclusions use `structuredEnvKeys`
so structured keys are never double-rendered. Under 1000 lines.
- **Per-agent surfaces** (`EditAgentAdvancedFields`,
`PersonaAdvancedFields`): replace `isBuzzAgentRuntime` as the
numeric-field gate with `deriveNumericDescriptors(selectedRuntime)` from
`agentConfigCore`; hidden keys come from
`structuredEnvKeys(numericDescriptors)` — the same rendered descriptor
set, no local rebuilding (fixes pre-existing dual-editor defect).
Catalog status carried as `RuntimeCatalogStatus` (`loading | ready |
error`); both error and loading withhold structured controls and leave
saved values visible as generic rows, making error distinguishable from
"runtime not capable" (`ready` + no runtime).
- **Dialogs** (`AgentDefinitionDialog`, `AgentInstanceEditDialog`,
callers): `AgentDefinitionDialog` accepts `runtimeCatalogStatus?:
"loading" | "ready" | "error"` (replaces separate
`runtimesLoading`/`runtimesError` booleans); all call sites —
`AgentManagementDialogs`, `AgentsView`, `RequestedAgentCreateDialogs`,
`UserProfilePersonaDialogs` — compute and pass the status.

### Phase 4 — Tests

- `buildRecord` exported from `EnvVarsEditor.tsx` as a pure `(nextRows,
value, requiredKeys, hiddenKeys) => Record<string, string>` helper for
isolation testing.
- **17 new node tests** in `agentConfigCore.test.mjs`:
`deriveNumericDescriptors` (all three fields, partial, undefined
runtime, matches field-model subset); `structuredEnvKeys` per surface
including discriminating Goose per-agent effort-key invariant;
`NUMERIC_KIND_MIN` values.
- **4 new node tests** in `EnvVarsEditor.test.mjs`: hidden tuning key
preserved through generic row edits; runtime-switch then generic edit
(derives both descriptor sets, asserts new-runtime hidden key survives
`buildRecord` via `hiddenKeys` and old-runtime key survives via generic
rows); baked numeric key excluded via `filterBakedGenericRows` with
`numericTuningPlaceholder` assertion; clearing a structured override —
`numericTuningPlaceholder` verifies placeholder text.
- **5 new Playwright tests** in `agent-numeric-tuning.spec.ts` (added to
smoke project `testMatch`): global numeric fields visible for
buzz-agent; global: non-capable runtime hides numeric controls; Goose
per-agent shows `Inherit (16384)` after saving global value through the
UI; delayed catalog: saved values visible as generic rows while loading
then structured controls appear after settle; failed catalog: saved
values remain visible as generic rows (never the "unsupported" empty
state).

## Result

- buzz-agent global defaults: Max output tokens, Context limit, Max
rounds as structured inputs with `Inherit (N)` placeholders from baked
env.
- Goose global defaults: Max output tokens, Context limit as structured
inputs.
- A Goose global value surfaces as `Inherit (<value>)` in the per-agent
Goose edit dialog.
- No structured key is editable in two places on any surface; no
persisted key has zero editors.
- No `runtime.id === "buzz-agent"` comparison decides numeric-field
visibility anywhere — capability flows catalog →
`AcpRuntimeCatalogEntry` → field model → UI.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
This commit is contained in:
Will Pfleger
2026-08-03 18:09:24 -04:00
committed by GitHub
co-authored by npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent 027a74a61c
commit 985cdcc6ea
32 changed files with 1769 additions and 322 deletions
+12 -14
View File
@@ -188,6 +188,9 @@ export type RawAcpRuntimeCatalogEntry = {
model_env_var?: string | null;
provider_env_var?: string | null;
thinking_env_var?: string | null;
max_tokens_env_var?: string | null;
context_limit_env_var?: string | null;
max_rounds_env_var?: string | null;
install_hint: string;
install_instructions_url: string;
can_auto_install: boolean;
@@ -199,10 +202,7 @@ export type RawAcpRuntimeCatalogEntry = {
auth_status: AuthStatus;
login_hint?: string;
source: "builtin" | "preset" | "custom";
/**
* Definition-level env vars for `source: custom` entries.
* Omitted/absent for builtin and preset — skipped in Rust serialization when empty.
*/
/** Definition-level env vars for `source: custom` entries; absent for builtin/preset. */
definition_env?: Record<string, string>;
};
@@ -749,6 +749,9 @@ export function fromRawAcpRuntimeCatalogEntry(
modelEnvVar: entry.model_env_var ?? null,
providerEnvVar: entry.provider_env_var ?? null,
thinkingEnvVar: entry.thinking_env_var ?? null,
maxTokensEnvVar: entry.max_tokens_env_var ?? null,
contextLimitEnvVar: entry.context_limit_env_var ?? null,
maxRoundsEnvVar: entry.max_rounds_env_var ?? null,
installHint: entry.install_hint,
installInstructionsUrl: entry.install_instructions_url,
canAutoInstall: entry.can_auto_install,
@@ -1024,9 +1027,8 @@ export type RuntimeFileConfigSubset = {
};
/**
* Get the file-layer config for a runtime so dialogs can show
* "Set in goose config" instead of surfacing a false required-field marker.
* Returns `null` when the runtime has no config file or it cannot be parsed.
* Get the file-layer config for a runtime so dialogs can show "Set in goose config" instead of
* surfacing a false required-field marker. Returns `null` when unavailable or unparseable.
*/
export async function getRuntimeFileConfig(
runtimeId: string,
@@ -1040,13 +1042,9 @@ export async function getRuntimeFileConfig(
}
/**
* Return the key names of all non-empty baked build env vars.
*
* Internal (Block) builds bake provider credentials into the binary at compile
* time. This returns the *key names only* — never the values — so dialogs can
* treat them as satisfied without exposing secrets to the frontend.
*
* OSS builds return an empty array (no baked env).
* Return the key names of all non-empty baked build env vars. Internal (Block) builds bake
* provider credentials into the binary at compile time; this returns *key names only* (never
* values) so dialogs treat them as satisfied without exposing secrets. OSS builds return [].
*/
export async function getBakedBuildEnvKeys(): Promise<string[]> {
return invokeTauri<string[]>("get_baked_build_env_keys");
+4 -6
View File
@@ -517,6 +517,9 @@ export type AcpRuntimeCatalogEntry = {
providerEnvVar: string | null;
/** Environment variable used to apply thinking effort, when supported. */
thinkingEnvVar: string | null;
maxTokensEnvVar: string | null;
contextLimitEnvVar: string | null;
maxRoundsEnvVar: string | null;
installHint: string;
installInstructionsUrl: string;
canAutoInstall: boolean;
@@ -529,12 +532,7 @@ export type AcpRuntimeCatalogEntry = {
authStatus: AuthStatus;
/** Hint for completing authentication; null when not applicable or already logged in. */
loginHint: string | null;
/**
* Whether this entry is compiled into the app ("builtin"), a bundled preset
* ("preset" — PATH-probed, not editable/deletable), or loaded from a user
* JSON file in `custom_harnesses/` ("custom"). Controls editability in the
* UI — only "custom" entries can be edited or deleted.
*/
/** "builtin" (compiled in), "preset" (PATH-probed, not editable), or "custom" (user JSON). Controls UI editability. */
source: "builtin" | "preset" | "custom";
/**
* Definition-level environment variables for `source: custom` entries.