- recordConfig no longer writes lastStatus (handled by applySnapshot only) - Add warning if recordConfig called before snapshot exists - Update tests to reflect new separation of concerns - Add AGENT_REFACTOR_PROPOSAL.md with comprehensive design plan 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
7.8 KiB
Agent Architecture Refactor Proposal
Current State Analysis
AgentSnapshot Usage
| Location | Purpose |
|---|---|
src/server/agent/agent-manager.ts:231-247 |
getAgent / listAgents expose AgentSnapshot copies generated via toSnapshot. |
src/server/session.ts:534-577, 1130-1145 |
Session broadcasts snapshots to clients (forwardAgentState, buildAgentPayload). |
src/server/agent/agent-registry.ts:138-185 |
applySnapshot is driven by snapshots (via attachAgentRegistryPersistence). |
src/server/persistence-hooks.ts:23-35 |
Persistence hook subscribes to agent_state and forwards snapshots. |
src/server/agent/mcp-server.ts:12-452 |
MCP endpoints serialize snapshots for diagnostics/listing. |
src/server/messages.ts:19-338 |
AgentSnapshotPayload and serializeAgentSnapshot expect snapshots. |
Tests (src/server/persistence-hooks.test.ts, src/server/agent/agent-registry.test.ts) |
Fabricate snapshots as fixtures. |
Impacted Areas
AgentManagersnapshot creation & event emission.- Session serialization (
buildAgentPayload,forwardAgentState). - Registry persistence (
applySnapshot, tests). - MCP server responses.
- Messaging schema & helper utilities.
Why AgentSnapshot Existed
It attempted to provide a JSON-friendly view stripped of internal handles (e.g., AgentSession, pendingRun). In practice it duplicates the data model, omits config (requiring recordConfig), and forces redundant copies. All downstream consumers just serialize the snapshot immediately, so it adds complexity without real protection.
Proposed Design
Single Source of Truth
Introduce a discriminated union for ManagedAgent that encodes lifecycle-specific invariants:
type ManagedAgent =
| { lifecycle: "initializing"; pendingRun: null; /* ... */ }
| { lifecycle: "running"; pendingRun: AsyncGenerator<AgentStreamEvent>; /* ... */ }
| { lifecycle: "idle"; pendingRun: null; /* ... */ }
| { lifecycle: "error"; pendingRun: null; lastError: string; /* ... */ }
| { lifecycle: "closed"; session: null; pendingRun: null; /* ... */ };
Fields include the live AgentSession, normalized config (AgentSessionConfig), timelines, permissions map, timestamps, persistence handles, etc. Impossible states (e.g., pendingRun non-null while lifecycle "idle") become unrepresentable.
Pure Transformation Functions
-
Persistence Projection
function toStoredAgentRecord(agent: ManagedAgent, options?: { title?: string | null }): StoredAgentRecordCopies provider, cwd, ISO timestamps, lifecycle status,
lastModeId, complete config (modeId,model,extra), persistence handle, title metadata. -
Client Payload Projection
function toAgentPayload(agent: ManagedAgent, options?: { title?: string | null }): AgentSnapshotPayloadConverts dates to ISO strings, normalizes pending permissions map to an array, includes safe config/model fields for UI, hides
AgentSessionreference. -
Additional helpers Reuse the same projections for MCP responses, diagnostics, etc. The functions are deterministic and easy to test.
Unified Persistence Flow
- Remove
AgentSnapshotandrecordConfig. AgentManager.subscribeemitsManagedAgentreferences (or read-only copies) onagent_state.attachAgentRegistryPersistencenow callstoStoredAgentRecordand writes the result. This single path handles both lifecycle and config data atomically.- Tests rely on the pure projection functions instead of crafting ad-hoc snapshots.
Lazy Initialization Strategy
restorePersistedAgentsstill readsStoredAgentRecordentries with full config populated by the projection.- Lazy
ensureAgentLoadeduses the stored record to resume the agent on demand; no special-case status bootstrapping required. - When an agent is resumed/created,
AgentManageremitsagent_stateevents with the liveManagedAgent, ensuring persistence is updated immediately before any other code touches the registry.
Client Communication
- Session and MCP server call
toAgentPayloadbefore emitting events. AgentSnapshotPayloadremains the wire-format schema, but it’s derived directly fromManagedAgent.- Clients continue to receive the same data shape (with potential additions, e.g., config info) without intermediate snapshot objects.
Risk Assessment
| Risk | Mitigation |
|---|---|
All consumer APIs expect AgentSnapshot. |
Update type signatures and provide pure projection helpers; TypeScript will flag missing updates. |
| Accidentally exposing mutable internal state. | Projections must deep-clone arrays/maps; optionally expose read-only ManagedAgentView wrappers. |
| Persistence logic mistakes. | Add dedicated unit tests for toStoredAgentRecord; compare outputs with existing fixtures. |
| Client payload regressions. | Snapshot serialization tests (serializeAgentSnapshot, session tests) must be updated to use the new helper. |
| Integration behavior (lazy init/status updates). | Manual QA and e2e tests verifying “open agent after restart” scenarios. |
Test Coverage
- Existing registry + persistence-hook tests already exercise serialization; they must be ported to the new helpers.
- Need new tests for
toStoredAgentRecordandtoAgentPayloadto validate field-level correctness. - Re-run session/MCP/e2e tests to ensure agent cards, timelines, and lazy loading still function.
Migration Strategy
- Breaking change is acceptable (server + client change together).
agents.jsonschema stays identical; only the producer path changes. - Local migrations not required; new code overwrites entries with full config automatically.
Implementation Plan
-
Introduce transformation helpers & tests
- Implement
toStoredAgentRecordandtoAgentPayload. - Add unit tests covering all lifecycle variants and edge cases (pending permissions map, optional fields).
- Implement
-
Refactor AgentManager emissions
- Remove
AgentSnapshottype andtoSnapshot. - Update
subscribe,getAgent,listAgents, and event dispatchers to passManagedAgent. - Ensure consumers cannot mutate returned references (clone or freeze if necessary).
- Remove
-
Update consumers
- Registry:
applySnapshotnow acceptsManagedAgentand usestoStoredAgentRecord. DeleterecordConfig. - Session/MCP: call
toAgentPayloadwhen broadcasting to clients; update serialized types. - Messages schema: keep
AgentSnapshotPayloadbut note it’s derived fromManagedAgent.
- Registry:
-
Cleanup
- Remove
AgentSnapshotdefinitions, serialization helpers, and obsolete code paths/tests. - Ensure
AgentRegistryno longer importsAgentSnapshot.
- Remove
-
Validation
- Run
npm run test agent-registry, persistence hook tests, session tests. - Manual QA: create agent → verify
agents.jsonincludes status & config; restart server → open agent → ensure UI shows “idle” immediately.
- Run
Validation Plan
-
Unit Tests
toStoredAgentRecordandtoAgentPayloadcoverage (all lifecycle states, optional fields, config propagation).- Update registry/persistence tests to use
ManagedAgentfixtures.
-
Integration Tests
- Session → client agent list (
SessionContextexpectations) to ensure payload format. - Lazy init/resume scenario (open agent after restart without manual refresh).
- MCP “list agents” command returning full info.
- Session → client agent list (
-
Manual Verification
- Create agents with various configs/modes; inspect
agents.json. - Restart server; verify UI cards show accurate status and accept prompts immediately.
- High-load scenario: multiple agents running concurrently to ensure no races in persistence.
- Create agents with various configs/modes; inspect
This refactor removes redundant abstractions, consolidates persistence into deterministic pure functions, and keeps ManagedAgent as the authoritative state—from which every other representation (disk, UI, MCP) is derived.***