feat(agents): harness-agnostic effort write-side + spawn bridge for Goose - #4625
feat(agents): harness-agnostic effort write-side + spawn bridge for Goose#4625wpfleger96 wants to merge 3 commits into
Conversation
2d8f97a to
e9e8f6a
Compare
c93e1c4 to
febf701
Compare
80df98f to
7c4c183
Compare
c60c87c to
5882e44
Compare
kalvinnchau
left a comment
There was a problem hiding this comment.
[P2] Pin-to-inherit can retain stale effort when the current runtime has no effort key — desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx:500
handleInheritHarnessChange decides whether to clear effort from the pre-toggle prospectiveRuntime. For an agent pinned to Claude (no thinkingEnvVar) whose linked persona uses Goose, selecting Inherit skips the clear. On the next render the prospective runtime becomes Goose, but the record's existing BUZZ_AGENT_THINKING_EFFORT remains; resolveInheritedRuntimeSubmission only excludes effort keys from the persona layer, so that record value is saved and the spawn bridge aliases it into Goose effort. This violates the stated pin↔inherit clear contract and can silently resurrect an effort value from an unrelated prior runtime. Resolve the runtime for nextInherit inside the handler (or clear whenever either the before or after runtime owns effort), and pin the Claude-pin → Goose-persona transition with an integration-level state/submit test.
[P2] Consumed legacy effort is also emitted as an advanced field — desktop/src-tauri/src/managed_agents/config_bridge/reader.rs:125
For a Goose record containing only a valid legacy BUZZ_AGENT_THINKING_EFFORT, build_thinking_field consumes it and exposes canonical thinking_effort, but normalized_env_keys contains only GOOSE_THINKING_EFFORT. The loop at line 140 therefore also emits the legacy key in surface.advanced. The config panel presents one persisted setting twice, with conflicting semantics, and an edit can appear to target a generic env var while the normalized field is already interpreting it. Invalid or unconsumed legacy values should remain advanced, so hide the legacy key only when record-tier alias resolution actually consumed it; add a surface test for valid-consumed versus invalid-visible legacy values.
Validation at exact PR tip 5882e4494656b0ce3deeffb2c1d1c1dea25e47f3:
just desktop-test: 4,774 passed, 0 failedjust desktop-tauri-test: passed (workspace; one pre-existing ignored native performance test)git diff --check a8e5c89e2..HEAD: clean
4d89491 to
c4a6e99
Compare
…ojection The persisted `record.effort_level` column is the single canonical effort authority for every runtime. One projection (`config_bridge::effort`) resolves the effective launch value over the canonical column AND all sanitized env tiers in the reader's CLEAR order (record-native > column > record-legacy > persona > global > definition > baked), then reduces the launch env to exactly one destination key: Goose emits GOOSE_THINKING_EFFORT, buzz-agent emits BUZZ_AGENT_THINKING_EFFORT, Claude/Codex/keyless-ACP and any unknown runtime emit the retained BUZZ_ACP_EFFORT_LEVEL startup sentinel. Local spawn, remote deploy, and the restart snapshot all consume the single already-projected `descriptor.env`, so a launched process, a remote payload, and a restart badge can never carry two effort authorities or disagree. On the pin->inherit transition the column and runtime clear inside the locked save; the record effort env aliases are stripped at the update boundary AFTER caller `env_vars` is applied, so a same-request env map cannot reintroduce a stale alias. Runtime switches preserve the canonical column (invalid values skip at projection time and fall through to inherited tiers; switching back restores the preference) and clear only stale native/legacy env aliases. Extracts the KNOWN_ACP_RUNTIMES data catalog to `discovery/catalog.rs` and the buzz-agent OpenRouter readiness tests to `readiness/openrouter_tests.rs` to keep both files under the desktop file-size ratchet (extract, never raise). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Restore the reader behaviors omitted from the rebuilt effort projection: consumed legacy keys stay out of Advanced fields, and transition adapters retain their effort category fallback. Replace the simulated Cancel assertion with a rendered dialog seam test so an accidental submit wiring change is observable. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
c4a6e99 to
513a75c
Compare
A valid legacy effort value remains persisted when the canonical column wins, so retain it in Advanced rather than hiding an unconsumed value. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Summary
Genericizes the agent effort write-side so Goose participates in the same canonical effort contract as buzz-agent, with a spawn bridge that translates the canonical key to whatever the target harness expects at launch time. The read/write/spawn paths all derive their vocabulary from runtime metadata rather than a hardcoded buzz-agent list, so adding a runtime that declares an effort vocabulary participates automatically.
Four commits on top of
main.Phase 1 — B-collapse + ACP identity + alias normalization
desktop/src-tauri/src/managed_agents/config_bridge/thought_leveloption whose raw value equalled the global default was reported as a live override instead of collapsing toGlobalDefault. Fixed: compare via normalized values after applyingeffort_norm.thought_levelACP identity. When boththought_levelandthinking_effortACP categories are present,thought_levelwins. The winning entry'sconfig_idis captured and forwarded towrite_via; it is always entry-supplied, never hardcoded.effort_normbefore candidate-list construction and B comparison. An invalid ACP value (e.g.minimalfor Goose) is omitted as a value candidate so a lower tier wins;write_viastaysAcpSetConfigOptionwith the entry-suppliedconfig_idbecause the route targets the option, not its current value.normalizeEffortValueinbuzzAgentConfig.tsuses the runtime-suppliedeffortAliasesdescriptor as the single normalization authority. AbsenteffortAliasesmeans no aliases — the built-in Goose fallback table has been removed. Canonical values pass through; unrecognised values returnnull.Phase 2 — Canonical effort contract + native-key persistence + mutation contract
desktop/src/features/agents/Single read policy.
resolveEffortFromEnv(envVars, nativeKey, legacyKey, acceptedValues, effortAliases)inagentConfigCore.tsis the authoritative read path for all effort surfaces. It normalizes, applies native-first / valid-legacy-fallback, and returns{ value, legacyConsumed }.deriveAgentConfigFieldModel,HarnessNativeEffortFields, and both editors'legacyEffortConsumedmemos all call it.Descriptor.
deriveAgentConfigFieldModelemits an effort field with:optionSource: "harnessNative"for runtimes with a staticacceptedEffortValuesvocabulary (Goose);"buzzAgentCatalog"for buzz-agent;"legacyProviderModelCatalog"otherwise.acceptedEffortValuesfrom runtime metadata — never the seven-value buzz-agent list.legacyKey = BUZZ_AGENT_THINKING_EFFORT) at record/persona scope only; omitted at global/onboarding (Delta 5: that key is buzz-agent's own native key there).UI surfaces.
AgentConfigFields(global/onboarding),PersonaAdvancedFields(persona/definition), andEditAgentAdvancedFields(record/instance) all branch onoptionSource === "harnessNative". For harness-native runtimes they renderHarnessNativeEffortFieldswith runtime metadata;EffortSelectFieldreceives canonical choices fromacceptedEffortValues, not the Buzz catalog.HarnessNativeEffortFields. AcceptslegacyEnvKey: string | null(required —nullat global/onboarding enforces the tier boundary and prevents silent legacy key deletion). Reads viaresolveEffortFromEnv. Save = canonical native write + atomic legacy delete at record/persona scope; native-key-only at global scope (preserves coexisting foreign native keys — Delta 5).applyHarnessNativeEffortChange. Exported production mutation helper called by both the component andAgentConfigFields's onChange. Encodes the scope-split policy:legacyKey = BUZZ_AGENT_THINKING_EFFORTat record/persona;legacyKey = nullat global. Tests import and exercise it directly.Model-coupled mutation guard. Heal-on-mount, orphan-clear, and
useEffortAutoClearinAgentConfigFieldsskip the native effort key whenisHarnessNativeEffort. Goose effort is model-independent — the vocabulary is static; the reader's skip-as-absent contract handles invalid values non-destructively. TheuseEffortAutoClearhook receives""ascurrentEffortfor harness-native runtimes, making it a no-op.Inherit-transition strip.
resolveInheritedRuntimeSubmissionacceptsexcludePersonaEnvKeysto strip effort keys from the persona merge layer on a pinned→inherit transition.AgentInstanceEditDialogpasses[thinkingEnvVar, ...allKnownEffortKeys(runtimes)]when the runtime hasthinkingEnvVar— derives the known-key set from the catalog, not a static constant.Legacy row visibility.
legacyConsumedKeycontrols advanced-row suppression. The legacy row is hidden only when the key was validly consumed; invalid or unrecognised legacy values remain visible as advanced env rows.Phase 3 — Shared alias resolver + spawn effort bridge + deploy test
desktop/src-tauri/effort_tier_alias.managed_agents/config_bridge/mod.rsexposeseffort_tier_alias(value, accepted_values, allow_legacy_alias)— the shared normalizer used bybuild_thinking_fieldandapply_effort_bridge. The boolean parameter isallow_legacy_aliaswith an explicit per-tier doc table (record: true, persona: true, global: false, definition: false, baked: false) so no tier can silently diverge.apply_effort_bridge.managed_agents/config_bridge/mod.rsbridges canonical effort onto the spawn payload:all_known_effort_keys()(derived fromKNOWN_ACP_RUNTIMES::thinking_env_var, not a static constant).xhighnormalizes tomax; invalid baked values skip as absent.allow_legacy_alias=false— mirrors the reader's contract (plan v3 Delta 2).launch_config.launch.envfor deploy/cloud agents; top-levelenv_varsis left unbridged.One metadata contract.
EffortNormalization.aliasesis serialized throughAcpRuntimeCatalogEntry.effort_aliasesindiscovery.rsand surfaced throughRawAcpRuntimeCatalogEntry/AcpRuntimeCatalogEntryin TS.normalizeEffortValueuses the descriptor-supplied alias table; absenteffortAliasesmeans no aliases (not Goose aliases). All production call sites pass the descriptor:AgentConfigFieldsviaresolveBakedEffort, both dialogs viauseAgentDialogDefaults(runtime: ...),HarnessNativeEffortFieldsvianormalizeEffortValue(raw, acceptedEffortValues, effortAliases). The staticALL_KNOWN_EFFORT_KEYSconstant (Rust and TS) is replaced byall_known_effort_keys()/allKnownEffortKeys(runtimes)derived from runtime declarations — adding a runtime automatically participates in foreign-key stripping without touching a constant.Baked-effort display.
resolveBakedEffort(bakedEnv, nativeKey, runtime)inbakedEnvHelpers.tsis the shared resolver for every baked-effort display surface:AgentConfigFields(global/onboarding) andgetInheritedAgentDefaults. Both normalize through alias→canonical / invalid→absent so display parity with spawn is maintained.useAgentDialogDefaultsgains aruntimeparam; callers passruntime: runtimes.find(...)orruntime: prospectiveRuntimein one line rather than three separate props.Test coverage
thought_levelpriority overthinking_effort, ACP alias/case/invalid normalization, alias resolver tiers, bridge descriptor hygiene, deploy parity boundary (deploy_parity_launch_env_carries_native_goose_effort); baked tier (baked_high_value_spawns_as_effort,baked_xhigh_normalizes_to_max,baked_invalid_minimal_skipped,baked_legacy_key_not_aliased_in_baked_tier,baked_beaten_by_record); Goose record tier (goose_record_native_effort_is_translated_and_foreign_key_stripped,goose_record_legacy_effort_migrated_to_native_key,goose_alias_none_is_normalized_to_off,goose_alias_xhigh_is_normalized_to_max); buzz-agent coexistence (buzz_agent_global_effort_survives_when_goose_record_effort_migrated,buzz_agent_global_effort_descriptor_strips_goose_native_key).resolveEffortFromEnvread policy (native-first, legacy fallback, tier boundary, null-legacyKey);applyHarnessNativeEffortChangescope-split mutation;deriveAgentConfigFieldModelat global/definition/instance scope;HarnessNativeEffortFieldslegacy migration;excludePersonaEnvKeysinherit-transition strip;isHarnessNativeEffortguard (xhigh survives model-heal; minimal survives; buzz-agent auto-clear unchanged); inherited-defaults normalization (global xhigh→max, baked xhigh→max, baked minimal→absent); global coexisting native-pair preservation;resolveBakedEffortdirect pins (xhigh→max, minimal→null — the production helper used by global/onboarding);normalizeEffortValueabsent-aliases pin and non-Goose ultra→max pin (TS normalization comes from the supplied descriptor, not any runtime-specific table);fromRawAcpRuntimeCatalogEntryAPI-boundary mapping (definition_env → definitionEnv, max_parallelism → maxParallelism, source preservation, env round-trip).Deferred
useAgentDialogDefaultsretains three unused optional inputs (nativeEffortKey,acceptedEffortValues,effortAliases) superseded by theruntimeparam. Follow-up cleanup: remove them and requireruntime.Split out
archive/effort-write-claude-fold, superseded by feat(managed-agents): close five Claude Code agent-config gaps #4557's ACP-based effort path.