Skip to content

feat(agent): persist conversation tree via a Store seam + in-memory Store (Wave 2)#39

Closed
urmzd wants to merge 1 commit into
mainfrom
wave-2-durability-tree-persistence
Closed

feat(agent): persist conversation tree via a Store seam + in-memory Store (Wave 2)#39
urmzd wants to merge 1 commit into
mainfrom
wave-2-durability-tree-persistence

Conversation

@urmzd

@urmzd urmzd commented May 31, 2026

Copy link
Copy Markdown
Owner

Wave 2 — Durability spine

Wires agent tree persistence behind the existing types.Store seam, fully testable without Postgres. This is a bounded, green slice: the in-memory-only path is byte-for-byte unchanged, and a configured Store now durably records the conversation tree.

Stacked on #38 (foundation) — merge after #38.

What landed (with tests)

agent package

  • AgentConfig.Store types.Store + WithStore(...) option. Default nil preserves today's in-memory-only behavior (fully backward compatible).
  • runLoop persists every new node and its branch tip as it is added — at all four AddChild sites (input messages, assistant message, tool-result message, handoff overlay). Persistence goes through Store.Tx so a reader never observes a branch tip pointing at an unsaved node. It is best-effort: a nil Store is a no-op, and any error is logged (slog.Warn), never fatal — a persistence failure cannot break a live run.
  • NewAgent persists the root node + main branch up front when a Store is configured, so LoadTreeFromStore has an anchor even before the first Invoke.
  • LoadTreeFromStore(ctx, store, rootID, active) helper: the read/resume counterpart, built on Store.LoadTree + tree.FromStore. Pass the rebuilt tree to NewAgent via WithTree to resume a persisted session.

New package agent/store/memstore

  • In-memory types.Store implementing the full interface: SaveNode, LoadNode, LoadChildren, LoadPath, SaveBranch, LoadBranch, ListBranches, SaveCheckpoint, LoadCheckpoint, LoadTree, Tx.
  • Concurrency-safe (RWMutex); nodes are defensively copied in and out so callers cannot mutate stored state.
  • LoadTree returns only the subtree reachable from rootID (BFS over child-order), ordered root-first — mirroring pgstore's recursive descendant query closely enough for tree.FromStore.
  • Tx buffers writes and applies them atomically on success; on error nothing is persisted (all-or-nothing, matching pgstore's DB transaction).

Tests

  • agent/store/memstore/memstore_test.go — table/unit tests: save/load round-trip, returned-pointer isolation, child insertion order, path, branches, checkpoints, reachable-subtree LoadTree, and Tx commit + rollback.
  • agent/agent_store_test.go:
    • TestStoreMultiTurnRoundTrip — build an agent with a memstore, run two Invoke turns, reconstruct the tree purely from the Store (LoadTreeFromStoreFromStoreLoadTree), and assert the full flattened message history round-trips message-for-message (plus content spot-checks).
    • TestStoreNilIsBackwardCompatible — no Store: default nil, Invoke still populates the tree.
    • TestStorePersistsRootOnConstruction — root node + main branch are written at construction.

Green

  • go build ./...
  • go vet ./agent/ ./agent/store/memstore/
  • go test ./agent/ ./agent/store/memstore/ ✅ (also ran ./agent/tree/)
  • gofmt clean.
  • Did not run the full go test ./... (rag/kg packages download embedding models).

Deferred (TODO, beyond this slice)

  • agent/pgstore transactional per-turn wiring (this slice only added the in-memory Store + the generic Store-agnostic persistence in runLoop; pgstore already implements the interface but is not yet exercised per-turn here).
  • Versioned migrator for the agent schema.
  • RAG/KG ingest transactions.
  • BM25 rebuild-on-boot.
  • Postgres integration tests.
  • Optional convenience: a WithStore-driven auto-hydrate path inside NewAgent (currently hydration is explicit via LoadTreeFromStore + WithTree, kept simple and correct).

@urmzd urmzd force-pushed the wave-2-durability-tree-persistence branch from 25e6229 to daf80a7 Compare May 31, 2026 20:53
Wire AGENT TREE PERSISTENCE behind the existing types.Store seam, testable
without Postgres.

- Add AgentConfig.Store + WithStore option (default nil = today's
  in-memory-only behavior, fully backward compatible).
- runLoop persists each new node (and branch tip) to the Store as it is
  added, via Store.Tx so the tip never points at an unsaved node.
  Best-effort: errors are logged, never fatal.
- NewAgent persists the root node + main branch up front when a Store is
  configured, giving LoadTreeFromStore an anchor before the first Invoke.
- Add LoadTreeFromStore helper (Store.LoadTree + tree.FromStore) for the
  read/resume path.
- New package agent/store/memstore: in-memory types.Store implementing the
  full interface (SaveNode/LoadNode/LoadChildren/LoadPath/SaveBranch/
  LoadBranch/ListBranches/SaveCheckpoint/LoadCheckpoint/LoadTree/Tx) with
  atomic buffered transactions.

Tests: memstore unit tests (round-trip, children order, path, branches,
checkpoints, reachable-subtree LoadTree, Tx commit/rollback); agent
multi-turn Invoke -> reconstruct tree from memstore -> assert full message
history round-trips; backward-compat (nil Store) and root-on-construction.
@urmzd urmzd force-pushed the wave-2-durability-tree-persistence branch from daf80a7 to ea28a47 Compare May 31, 2026 21:09
@urmzd

urmzd commented May 31, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #44, which was squash-merged into main (53a6aff) and contains every change from this branch — a dry merge of this branch into main is a no-op. main is green (build/vet/golangci-lint, 39 packages, and 8/8 live validation on gpt-4o-mini). Closing as redundant.

@urmzd urmzd closed this May 31, 2026
@urmzd urmzd deleted the wave-2-durability-tree-persistence branch May 31, 2026 21:59
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