Skip to content

feat(desktop): global agent config defaults with readiness/spawn parity#1448

Open
wpfleger96 wants to merge 4 commits into
duncan/agent-readinessfrom
duncan/global-agent-config
Open

feat(desktop): global agent config defaults with readiness/spawn parity#1448
wpfleger96 wants to merge 4 commits into
duncan/agent-readinessfrom
duncan/global-agent-config

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Adds a global agent configuration layer that lets users set default LLM provider, model, and environment variables applied to all agents unless overridden per-agent or per-persona.

What's here

Global config layer

  • New GlobalAgentConfig struct (provider, model, env_vars) persisted to global-agent-config.json.
  • Tauri commands: get_global_agent_config, set_global_agent_config (with validation — reserved and derived keys rejected).
  • Env-var merge at spawn: global < per-agent (last wins); reserved Buzz keys stripped before merge.
  • Readiness evaluation (resolve_effective_agent_env) uses global as the final fallback tier.
  • validate_global_config rejects GOOSE_PROVIDER, GOOSE_MODEL, BUZZ_AGENT_PROVIDER, BUZZ_AGENT_MODEL — those are derived from structured fields and must not be set in the global env map.

Fix 1 (CRITICAL — readiness/spawn divergence)

Extracted resolve_effective_model_provider(record, personas, global) in global_config/mod.rs encoding the agent→persona→global→None precedence chain, and wired it into both resolve_effective_agent_env (readiness) and spawn_agent_child (runtime). Before the fix, readiness used the full fallback chain but the spawn path read only record.model/record.provider, so a global-default-only agent reported Ready but spawned without provider/model env. Also fixes the same drift in build_deploy_payload. Hoists the global config load out of the readiness scoped block to reuse it for the env-var merge, eliminating a duplicate load.

Fix 2 (IMPORTANT — override baseline for global-default agents)

In resolve_config_surface, when !had_model && persona_model.is_none() && model_overridden, use (global.model, ConfigOrigin::GlobalDefault) as the baseline. Before the fix, baseline was None in this case, so a live model switch on a global-default-only agent had no secondary row to show in the config surface UI.

Fix 3 (IMPORTANT — required rows in EnvVarsEditor)

Extracted isRequiredKeyMissing from EnvVarsEditor.tsx and taught it to check inheritedFrom?.[key] in addition to the agent-local value. Before the fix, a globally-satisfied required key still rendered the amber "Required" badge because isMissing only checked the local map.

Tests

  • global_config/tests.rs: 6 new unit tests for resolve_effective_model_provider covering all precedence tiers (record wins, persona fallback, global fallback, no-persona, all-None, independent per-field resolution). Red-before/green-after verified — tests fail on pre-fix runtime.rs spawn path.
  • commands/agent_config.rs: new test global_default_live_switch_renders_global_model_as_secondary_global_default — fails on pre-fix code where baseline was None for the !had_model && no-persona && model_overridden case.
  • envVarsEditorMissing.test.mjs: 7 new unit tests for isRequiredKeyMissing covering local-set, inherited-set, both-set, neither-set, empty-inherited, empty-local, and wrong-key-in-inherited cases.

Stack: #1411 → this PR

@wpfleger96 wpfleger96 changed the base branch from main to duncan/agent-readiness July 1, 2026 22:42
@wpfleger96 wpfleger96 force-pushed the duncan/global-agent-config branch from 34f9571 to 3903f9c Compare July 1, 2026 23:13
@wpfleger96 wpfleger96 changed the title feat(desktop): add global agent configuration defaults layer feat(desktop): global agent config defaults with readiness/spawn parity Jul 1, 2026
@wpfleger96 wpfleger96 force-pushed the duncan/global-agent-config branch from ef15981 to 828f73d Compare July 1, 2026 23:33
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 3 commits July 1, 2026 19:37
Introduce a global agent config record (global-agent-config.json) that
applies to ALL managed agents as the lowest user-settable precedence
layer. Per-agent and persona configs always win on collision.

Precedence: baked build floor < global < persona < per-agent.

## Backend (Rust)

- New global_config module: GlobalAgentConfig struct (env_vars,
  provider, model), load/save with atomic_write_json_restricted (0600),
  validate_global_config (strips empty values, rejects reserved and
  derived-provider-model keys), strip_empty_env_vars helper.
- Tauri commands get_global_agent_config / set_global_agent_config with
  save-time validation; registered in invoke_handler.
- ConfigOrigin::GlobalDefault added; resolve_config_surface re-tags
  injected global fields via retag_global_default (mirrors PersonaDefault
  pattern), so AgentConfigPanel shows 'Inherited from global defaults'.
- Merge seams: resolve_effective_agent_env (readiness.rs — fixes readiness
  gate + nudge in one place), spawn_agent_child (runtime.rs — replaces
  empty lower-map with global env), build_deploy_payload (agents.rs —
  global < persona < agent for env and model/provider fallback chain).

Global config is live-resolved at spawn/readiness/deploy — no
delete+respawn required when the global record changes.

## Frontend (TypeScript)

- tauriGlobalAgentConfig.ts: getGlobalAgentConfig / setGlobalAgentConfig
  invokeTauri wrappers.
- useGlobalAgentConfig hook: loads once on mount, fails safe (no global
  config is not an error state for callers).
- GlobalAgentConfigSettingsCard: Settings screen card with provider
  picker (reuses getPersonaProviderOptions), model input, EnvVarsEditor,
  and save-with-feedback button. Added to Settings → Agents section.
- computeLocalModeGate: accepts optional globalEnvVars param; keys present
  in global count as satisfied so Create dialog does not flag a key missing
  that global already provides.
- CreateAgentDialog: passes globalConfig.env_vars as inheritedFrom to
  EnvVarsEditor (shows 'global defaults' hint row).
- EditAgentDialog: merges global + persona into inheritedWithGlobal for
  the EnvVarsEditor inherited hint so both sources are visible.
- AgentConfigPanel: provenanceSentence handles new GlobalDefault case.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…obal defaults

Three bugs found by Thufir in pass-1 review; all verified red-before/green-after.

Fix 1 (CRITICAL — readiness/spawn divergence): extract
`resolve_effective_model_provider(record, personas, global)` in
`global_config/mod.rs` encoding the agent→persona→global→None precedence
chain, and use it in both `resolve_effective_agent_env` (readiness) and
`spawn_agent_child` (runtime). Before the fix, readiness used the full
fallback chain but the spawn path read only `record.model`/`record.provider`,
so a global-default-only agent reported Ready but spawned without
provider/model env. Also fixes the same drift in `build_deploy_payload`.
Hoists the global config load out of the readiness scoped block in runtime.rs
to reuse it for the env-var merge, eliminating the duplicate load.

Fix 2 (IMPORTANT — override baseline for global-default agents): in
`resolve_config_surface`, when `!had_model && persona_model.is_none() &&
model_overridden`, use `(global.model, ConfigOrigin::GlobalDefault)` as the
baseline. Before the fix, baseline was None in this case, so a live model
switch on a global-default-only agent had no secondary row to show.

Fix 3 (IMPORTANT — required rows): extract `isRequiredKeyMissing` from
`EnvVarsEditor.tsx` and teach it to check `inheritedFrom?.[key]` in addition
to the agent-local value. Before the fix, a globally-satisfied required key
still rendered the amber "Required" badge because `isMissing` only checked the
local map.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…ed copy

build_deploy_payload now uses a dedicated deploy-specific resolver
(live-persona → record → global) instead of the shared readiness/spawn
resolver (record → persona → global). Provider start does not
re-snapshot the linked persona onto the record before deploy, so the
record may hold a stale snapshot while the live persona has moved on.
Deploy needs live-persona-first to ensure remote agents receive the
current config after a persona update, without requiring delete+recreate.

Also fix EnvVarsEditor hint copy: rows where the local value is empty
but an inherited (global/persona) value satisfies the key now read
'Inherited from {label} value …' instead of 'Overrides {label} value …'.

Regression tests added:
- deploy_resolver_uses_live_persona_over_stale_record_snapshot
- deploy_resolver_falls_back_to_record_when_persona_has_none
- deploy_resolver_falls_back_to_global_when_persona_and_record_have_none

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 force-pushed the duncan/global-agent-config branch from 828f73d to 05d2467 Compare July 1, 2026 23:37
resolve_deploy_model_provider fn + doc comment adds 6 lines on top of
the prior global-agent-config bump. Queued to split with the rest of
the file.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
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.

1 participant