Skip to content

feat(desktop): unified AgentRecord groundwork — record-first resolution + runtime materialization (Phase 1A.1)#1618

Merged
wesbillman merged 8 commits into
mainfrom
feat/unified-agent-record
Jul 8, 2026
Merged

feat(desktop): unified AgentRecord groundwork — record-first resolution + runtime materialization (Phase 1A.1)#1618
wesbillman merged 8 commits into
mainfrom
feat/unified-agent-record

Conversation

@wesbillman

Copy link
Copy Markdown
Collaborator

Summary

Phase 1A (first half) of the unified agent model (#centralize-personas-and-agents): ManagedAgentRecord becomes a strict superset of PersonaRecord's user-facing fields, and all spawn/summary/config paths resolve record-first. Zero user-visible behavior change — this PR is the groundwork that makes the store fold (A2, next PR) a data migration instead of a behavior change.

  • Absorb persona-only fields: display_name, slug (former persona id — preserves the 30175 d_tag coordinate for future key-less definitions), runtime, name_pool, is_builtin, is_active, source_team, source_team_persona_slug. All serde-defaulted; existing stores parse unchanged.
  • Record-first harness resolution: new record_agent_command() — explicit override > record's own runtime > legacy persona lookup > default. All record-based effective_agent_command call sites converted (spawn, restore, summary, spawn-hash, readiness, model discovery, config surface, profile reconcile). Pre-migration records carry no runtime, so they fall through to the persona path and resolve exactly as before.
  • Boot-time runtime materialization: materialize_agent_runtimes copies each persona-linked record's runtime onto the record (idempotent, never overwrites, skips unknown personas). Cutover invariant test-pinned: spawn_config_hash is identical before vs after materialization, so no running agent shows a spurious restart badge.
  • Fold mappings (not yet wired): PersonaRecord::into_agent_record / ManagedAgentRecord::to_persona_view, round-trip pinned by serde-Value equality. Wired in by A2's store fold.
  • Frozen-interface contract made executable: the 30177 exclusion test now seeds real values into all new fields and asserts none reach the wire — publishing them is a Phase 2 relay-canonicalization decision.

Deliberately NOT in this PR (A2): folding personas.json into the unified store, persona-command shims, and the spawn-time persona re-pin collapse (which waits for Phase 1B's UI unification so persona edits keep propagating while the persona UI exists).

Testing

  • just desktop-tauri-test: 1041 passed, 0 failed (17 new tests: resolution ladder, fold round-trip, migration idempotence/skip cases, hash-invariance cutover, wire-projection exclusions)
  • just clippy (workspace), just fmt-check, just desktop-tauri-fmt-check: green
  • Rebased onto d1f3194 (current main)

wesbillman and others added 8 commits July 7, 2026 20:38
…ase 1A slice 1)

Add display_name, name_pool, and is_builtin to ManagedAgentRecord — the
three user-facing fields the persona layer carries that the agent record
did not already snapshot. All serde-default so existing stores deserialize
unchanged; not yet published (the 30177 projection is untouched, per the
frozen Phase-1 interface).

Part of the unified agent model (#centralize-personas-and-agents):
AgentRecord = ManagedAgentRecord + persona-only fields, identity optional.

Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
…ce 2a)

Add slug (former PersonaRecord.id, preserves 30175 d_tag coordinates for
key-less definitions) and is_active (archived-definition flag) to
ManagedAgentRecord, and teach hydrate_keys to skip key-less records —
definitions mint keys on first start, so there is no keyring entry to
look up and no outage to report.

desktop-tauri tests: 1020 passed, 0 failed.

Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Add record.runtime (absorbed from PersonaRecord.runtime) and
record_agent_command(): override > record.runtime > legacy persona
lookup > default. Convert all record-based effective_agent_command call
sites (spawn, restore, summary, spawn_hash, readiness, model discovery,
config surface, profile reconcile) to the record-first path. Records
without a materialized runtime resolve exactly as before via the
persona fallback, so pre-migration stores see zero behavior change.

effective_agent_command remains for persona-scoped callers (persona
dialogs, divergence checks) until the store migration lands.

desktop-tauri tests: 1024 passed (4 new record_agent_command tests).

Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Add materialize_agent_runtimes to run_boot_migrations: copies each
persona-linked record's runtime onto the record itself (idempotent,
never overwrites an existing runtime, skips unlinked/unknown personas).
After this runs, record_agent_command resolves from the record's own
runtime — the persona list is no longer consulted on the spawn path.

Includes Pinky's cutover acceptance test: spawn_config_hash is
IDENTICAL before vs after materialization for a persona-linked record,
so no running agent shows a spurious restart badge at migration.

desktop-tauri tests: 1028 passed.

Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Extend the opt-IN exclusion test with real (non-empty) values for the
Phase 1A record fields (display_name, slug, runtime, name_pool,
is_builtin, is_active) and assert none reach the published projection —
publishing them is a Phase 2 relay-canonicalization decision, and this
makes the frozen-interface contract with the boot reconcile executable.

Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
…slice 3b-1)

source_team + source_team_persona_slug move onto ManagedAgentRecord —
the definition-side team lineage the persona layer carried. With these,
the record is a strict superset of PersonaRecord's user-facing fields
and the store fold no longer loses information.

Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
…3b-2)

PersonaRecord::into_agent_record projects a persona onto a key-less
unified record (id -> slug, preserving the 30175 d_tag coordinate;
identity empty until first start mints keys). The inverse
to_persona_view presents slugged definitions in the legacy shape for
the persona command surface until Phase 1B unifies the UI. Round-trip
is field-exact (test-pinned via serde_json::Value equality).

Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
The file-size guard flagged five files at their limits. Real splits,
not limit bumps:

- discovery.rs / nest.rs: inline test modules move to discovery/tests.rs
  and nest/tests.rs (the repo's established pattern) — both files drop
  under the 1000-line default (802 / 679).
- migration.rs: materialize_agent_runtimes + its tests move to a
  migration/materialize.rs child module (reuses the parent's private
  JSON-patch helpers).
- commands/agents.rs: the kind:0 profile-reconcile surface
  (ProfileReconcileData, reconcile_agent_profile, legacy-avatar
  backfill, needs-sync predicate) moves to agents_profile.rs.

All five ratchet entries lowered to the new sizes so the freed headroom
cannot silently refill.

Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.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