Skip to content

feat(agents): harness-agnostic effort write-side + spawn bridge for Goose - #4625

Draft
wpfleger96 wants to merge 3 commits into
mainfrom
duncan/effort-write-genericize
Draft

feat(agents): harness-agnostic effort write-side + spawn bridge for Goose#4625
wpfleger96 wants to merge 3 commits into
mainfrom
duncan/effort-write-genericize

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Aug 3, 2026

Copy link
Copy Markdown
Member

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/

  • B-collapse for same-value live effort. Previously a live thought_level option whose raw value equalled the global default was reported as a live override instead of collapsing to GlobalDefault. Fixed: compare via normalized values after applying effort_norm.
  • thought_level ACP identity. When both thought_level and thinking_effort ACP categories are present, thought_level wins. The winning entry's config_id is captured and forwarded to write_via; it is always entry-supplied, never hardcoded.
  • ACP effort normalization. The selected ACP option's value is normalized through effort_norm before candidate-list construction and B comparison. An invalid ACP value (e.g. minimal for Goose) is omitted as a value candidate so a lower tier wins; write_via stays AcpSetConfigOption with the entry-supplied config_id because the route targets the option, not its current value.
  • Alias normalization (TS). normalizeEffortValue in buzzAgentConfig.ts uses the runtime-supplied effortAliases descriptor as the single normalization authority. Absent effortAliases means no aliases — the built-in Goose fallback table has been removed. Canonical values pass through; unrecognised values return null.

Phase 2 — Canonical effort contract + native-key persistence + mutation contract

desktop/src/features/agents/

Single read policy. resolveEffortFromEnv(envVars, nativeKey, legacyKey, acceptedValues, effortAliases) in agentConfigCore.ts is 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' legacyEffortConsumed memos all call it.

Descriptor. deriveAgentConfigFieldModel emits an effort field with:

  • optionSource: "harnessNative" for runtimes with a static acceptedEffortValues vocabulary (Goose); "buzzAgentCatalog" for buzz-agent; "legacyProviderModelCatalog" otherwise.
  • acceptedEffortValues from runtime metadata — never the seven-value buzz-agent list.
  • Legacy fallback (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), and EditAgentAdvancedFields (record/instance) all branch on optionSource === "harnessNative". For harness-native runtimes they render HarnessNativeEffortFields with runtime metadata; EffortSelectField receives canonical choices from acceptedEffortValues, not the Buzz catalog.

HarnessNativeEffortFields. Accepts legacyEnvKey: string | null (required — null at global/onboarding enforces the tier boundary and prevents silent legacy key deletion). Reads via resolveEffortFromEnv. 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 and AgentConfigFields's onChange. Encodes the scope-split policy: legacyKey = BUZZ_AGENT_THINKING_EFFORT at record/persona; legacyKey = null at global. Tests import and exercise it directly.

Model-coupled mutation guard. Heal-on-mount, orphan-clear, and useEffortAutoClear in AgentConfigFields skip the native effort key when isHarnessNativeEffort. Goose effort is model-independent — the vocabulary is static; the reader's skip-as-absent contract handles invalid values non-destructively. The useEffortAutoClear hook receives "" as currentEffort for harness-native runtimes, making it a no-op.

Inherit-transition strip. resolveInheritedRuntimeSubmission accepts excludePersonaEnvKeys to strip effort keys from the persona merge layer on a pinned→inherit transition. AgentInstanceEditDialog passes [thinkingEnvVar, ...allKnownEffortKeys(runtimes)] when the runtime has thinkingEnvVar — derives the known-key set from the catalog, not a static constant.

Legacy row visibility. legacyConsumedKey controls 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.rs exposes effort_tier_alias(value, accepted_values, allow_legacy_alias) — the shared normalizer used by build_thinking_field and apply_effort_bridge. The boolean parameter is allow_legacy_alias with 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.rs bridges canonical effort onto the spawn payload:

  • Strips all known foreign effort keys using all_known_effort_keys() (derived from KNOWN_ACP_RUNTIMES::thinking_env_var, not a static constant).
  • Resolves the canonical winner from all tiers: record native → record legacy → persona native → persona legacy → global native → definition native → baked native.
  • Baked tier is native-key-only (no legacy alias); baked xhigh normalizes to max; invalid baked values skip as absent.
  • Definition tier uses allow_legacy_alias=false — mirrors the reader's contract (plan v3 Delta 2).
  • Writes to launch_config.launch.env for deploy/cloud agents; top-level env_vars is left unbridged.

One metadata contract. EffortNormalization.aliases is serialized through AcpRuntimeCatalogEntry.effort_aliases in discovery.rs and surfaced through RawAcpRuntimeCatalogEntry / AcpRuntimeCatalogEntry in TS. normalizeEffortValue uses the descriptor-supplied alias table; absent effortAliases means no aliases (not Goose aliases). All production call sites pass the descriptor: AgentConfigFields via resolveBakedEffort, both dialogs via useAgentDialogDefaults(runtime: ...), HarnessNativeEffortFields via normalizeEffortValue(raw, acceptedEffortValues, effortAliases). The static ALL_KNOWN_EFFORT_KEYS constant (Rust and TS) is replaced by all_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) in bakedEnvHelpers.ts is the shared resolver for every baked-effort display surface: AgentConfigFields (global/onboarding) and getInheritedAgentDefaults. Both normalize through alias→canonical / invalid→absent so display parity with spawn is maintained. useAgentDialogDefaults gains a runtime param; callers pass runtime: runtimes.find(...) or runtime: prospectiveRuntime in one line rather than three separate props.


Test coverage

  • Rust: ACP B-collapse, thought_level priority over thinking_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).
  • TS: resolveEffortFromEnv read policy (native-first, legacy fallback, tier boundary, null-legacyKey); applyHarnessNativeEffortChange scope-split mutation; deriveAgentConfigFieldModel at global/definition/instance scope; HarnessNativeEffortFields legacy migration; excludePersonaEnvKeys inherit-transition strip; isHarnessNativeEffort guard (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; resolveBakedEffort direct pins (xhigh→max, minimal→null — the production helper used by global/onboarding); normalizeEffortValue absent-aliases pin and non-Goose ultra→max pin (TS normalization comes from the supplied descriptor, not any runtime-specific table); fromRawAcpRuntimeCatalogEntry API-boundary mapping (definition_env → definitionEnv, max_parallelism → maxParallelism, source preservation, env round-trip).

Deferred

  • useAgentDialogDefaults retains three unused optional inputs (nativeEffortKey, acceptedEffortValues, effortAliases) superseded by the runtime param. Follow-up cleanup: remove them and require runtime.

Split out

@wpfleger96
wpfleger96 force-pushed the duncan/effort-write-genericize branch from 2d8f97a to e9e8f6a Compare August 4, 2026 01:36
@wpfleger96 wpfleger96 changed the title feat(config-bridge): persist Goose effort under native key + spawn legacy bridge feat(agents): harness-agnostic effort write-side + spawn legacy bridge Aug 4, 2026
@wpfleger96
wpfleger96 force-pushed the duncan/effort-write-genericize branch 7 times, most recently from c93e1c4 to febf701 Compare August 4, 2026 18:32
@wpfleger96 wpfleger96 changed the title feat(agents): harness-agnostic effort write-side + spawn legacy bridge feat(agents): harness-agnostic effort write-side + spawn legacy bridge (Goose + Claude Code) Aug 4, 2026
@wpfleger96
wpfleger96 force-pushed the duncan/effort-write-genericize branch from 80df98f to 7c4c183 Compare August 4, 2026 21:58
@wpfleger96 wpfleger96 changed the title feat(agents): harness-agnostic effort write-side + spawn legacy bridge (Goose + Claude Code) feat(agents): harness-agnostic effort write-side + spawn bridge for Goose Aug 12, 2026
@wpfleger96
wpfleger96 force-pushed the duncan/effort-write-genericize branch 3 times, most recently from c60c87c to 5882e44 Compare August 12, 2026 18:00

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 failed
  • just desktop-tauri-test: passed (workspace; one pre-existing ignored native performance test)
  • git diff --check a8e5c89e2..HEAD: clean

@wpfleger96
wpfleger96 force-pushed the duncan/effort-write-genericize branch 3 times, most recently from 4d89491 to c4a6e99 Compare August 19, 2026 20:51
Duncan and others added 2 commits August 19, 2026 18:03
…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>
@wpfleger96
wpfleger96 force-pushed the duncan/effort-write-genericize branch from c4a6e99 to 513a75c Compare August 19, 2026 22:14
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants