Skip to content

v0.9.0 — memory foundation

Choose a tag to compare

@dberry37388 dberry37388 released this 04 Jun 17:00
· 191 commits to main since this release
77b4b92

Memory Foundation — first-class scoped, snapshot-replayable memory subsystem with RunContext consolidation. Foundation for v0.10.0 propagation/operator surface and v0.11.0 memory-as-tool DX. Vector-backed recall ships as the laravel-swarm-memory-vector companion package.

Added

  • SwarmMemory contract, DatabaseMemoryStore, CacheMemoryStore, and Swarm facade integration (#108). Foundation of the v0.9.0 memory subsystem. Introduces the SwarmMemory contract as the central read/write authority for scoped, key-value memory in a swarm run. Four memory scopes: MemoryScope::Run (bound to a runId, frozen for replay), MemoryScope::Conversation (spans multiple runs on the same conversation handle), MemoryScope::Agent (per-agent-class within a run), and MemoryScope::Swarm (shared across all runs of a swarm class). Two store implementations ship: DatabaseMemoryStore (persists to swarm_memories via Eloquent, first-class MemoryEntry value objects, event dispatch) and CacheMemoryStore (Laravel Cache-backed ephemeral store, zero-migration path for smaller workloads). Both stores are injected with MemoryEventDispatcher to emit lifecycle events. SwarmMemory is bound in the container as a singleton resolved by SwarmServiceProvider::resolvePersistenceStore()database driver → DatabaseMemoryStore; any other driver → CacheMemoryStore. NullMemoryStore ships for test isolation. The Swarm facade gains a memory() helper returning the resolved SwarmMemory instance. MemoryScope, MemoryEntry, MemoryKey, and related value objects form the type vocabulary used throughout the subsystem.
  • Two new database tables: swarm_memories and swarm_memory_snapshots (#109, #110). swarm_memories stores individual scoped memory entries: scope (enum), scope_id (polymorphic owner), run_id (nullable FK → swarm_run_histories.run_id for Run-scope cascade-delete on run completion), key, value (JSON), type (PHP type tag), expires_at, and timestamps. swarm_memory_snapshots captures frozen point-in-time views at agent invocation: run_id (FK → swarm_run_histories.run_id), step_index, snapshot (JSON array of entries), created_at. Composite unique on (run_id, step_index) ties each snapshot to exactly one step without a literal composite FK to swarm_run_steps. Both tables are published as standard package migrations and registered in the swarm.tables config map. Run php artisan migrate on the database persistence driver to add both tables. The swarm:install:memory sub-installer verifies their presence at setup time.
  • Memory snapshot mechanism — frozen Run-scope view at agent invocation (#111). DatabaseMemorySnapshotRecorder captures a serialized snapshot of all MemoryScope::Run entries for a given runId before each agent invocation and writes it to swarm_memory_snapshots. Wired into all four durable runners (DurableSequentialStepAdvancer, DurableBranchAdvancer, DurableHierarchicalCoordinator, DurableStaticHierarchicalCoordinator) via the SwarmMemorySnapshotRecorder binding. NullSnapshotsMemory ships for non-durable and cache-backed paths; missing-table errors are narrowed to QueryException on table-not-found and swallowed gracefully so non-durable runs are unaffected.
  • Durable replay reads from frozen snapshot — MemoryReplayCoordinator and ReplaySwarmMemory (#112, #142). When a durable agent retries after a crash, MemoryReplayCoordinator::during() wraps the agent invocation and swaps the container's SwarmMemory binding to a ReplaySwarmMemory decorator — a frozen, read-only view of the snapshot recorded at the original invocation. The agent sees the same MemoryScope::Run state it saw before the crash, regardless of any memory mutations made between the failed attempt and the retry (for example, observability writes, operator corrections, or concurrent agents). The coordinator handles all three durable topologies: DurableBranchAdvancer (parallel), DurableSequentialStepAdvancer, and DurableHierarchicalCoordinator::runStep(). The frozen binding is always restored in a finally block for exception safety. ReplayMode enum controls the policy: FrozenView (default) — serve the frozen snapshot; FreshExecution — bypass the decorator and pass live memory through. Configure globally via swarm.memory.replay_mode (env SWARM_MEMORY_REPLAY_MODE) or per-swarm via #[MemoryReplay(mode: ReplayMode::FreshExecution)]. ReplayDriftException and SnapshotFrozenException ship for v0.10.0 operator-surface and as programmer-safety guards on frozen-view writes.
  • Memory lifecycle events (#115). Four events dispatched by the store layer on every memory operation: MemoryWritten (on put()), MemoryRead (on get()), MemoryForgotten (on forget(); carries an existed flag so listeners can distinguish no-op forgets from actual removals), and MemorySnapshotted (on snapshot record). All four extend MemoryEvent and carry scope, scopeId, key, and a timestamp. Subscribe via standard Laravel event listeners in EventServiceProvider or AppServiceProvider::boot(). Events are dispatched through MemoryEventDispatcher injected into both stores.
  • php artisan swarm:install:memory sub-installer (#114). Targeted setup command for the v0.9.0 memory subsystem. Verifies the swarm_memories and swarm_memory_snapshots tables are present (offering to run php artisan migrate --force when they are absent, or warning when --skip-migrate is set); prints the effective memory driver (checks swarm.memory.driver first, falls back to swarm.persistence.driver, mirroring SwarmServiceProvider::resolvePersistenceStore()); prints the current SWARM_MEMORY_REPLAY_MODE with a one-line explainer so the operator confirms their replay strategy before shipping; and cross-links to swarm:health, docs/memory.md, and the #[MemoryReplay] attribute. Unlike swarm:install:durable, a non-database driver produces a warning rather than a refusal — CacheMemoryStore is a valid ephemeral workload. SWARM_MEMORY_REPLAY_MODE=frozen_view is seeded into .env by the main swarm:install orchestrator (not this sub-installer) as part of its global env-seeding pass. Flags: --migrate / --skip-migrate for non-interactive control; --no-interaction defaults to skip-and-warn rather than migrate. Registered in SwarmServiceProvider; wired into swarm:install as default-on (suppressible via --without-memory). SWARM_MEMORY_REPLAY_MODE added to InstallCommand::ENV_DEFAULTS. Eight tests in tests/Installer/InstallMemoryCommandTest.php cover: success path, idempotency, cache-driver warning (exits 0), per-subsystem swarm.memory.driver override, missing-tables warning under --skip-migrate, --migrate flag runs migrations and reports tables present, --no-interaction skips without prompting, custom replay_mode output, and next-step hints. Documented in docs/getting-started.md (sub-installer list + non-interactive example) and docs/advanced-setup.md (new ## Set up Swarm Memory section with replay-mode table and manual migration steps).
  • docs/memory.md — Swarm Memory keystone reference (#116). New page covering the full memory subsystem: four-scope hierarchy (Run / Conversation / Agent / Swarm), SwarmMemory contract read/write API with examples, MemoryEntry and MemoryScope value object reference, store drivers (DatabaseMemoryStore / CacheMemoryStore / custom), all five lifecycle events (MemoryWritten, MemoryRead, MemoryForgotten, MemorySnapshotted, MemoryScopeOutOfSnapshot) with listener examples, snapshot mechanism and MemorySnapshot value object, replay semantics (frozen_view vs fresh_execution) including the #[MemoryReplay] per-swarm attribute, the binding-restore constraint on MemoryReplayCoordinator::during(), and the RunContext ArrayAccess write-through bridge. Cross-linked from README.md, docs/README.md, docs/getting-started.md, and docs/advanced-setup.md.
  • Crash-resume replay-determinism test suite (#118). tests/Feature/Memory/ReplayDeterminismTest.php proves the frozen-snapshot guarantee end-to-end across all three durable topologies: Sequential, Parallel, and Hierarchical. Each test dispatches a durable swarm, writes a known value to MemoryScope::Run, lets the spy agent crash on attempt 1 (recording what it saw), mutates memory in between, time-travels past the retry backoff, recovers, and asserts the retry agent sees the original frozen value — not the mutated one. A fourth test covers the FreshExecution opt-out anti-test (asserts the mutated value is visible under ReplayMode::FreshExecution). 1,110 tests / 4,307 assertions green.

Changed

  • RunContext now implements ArrayAccess and writes through to SwarmMemory (Run scope) (#113). RunContext::mergeData() — the path all internal mutation goes through — now mirrors each key into SwarmMemory via put(MemoryScope::Run, $runId, $key, $value) in addition to updating the in-memory $data array. RunContext itself implements ArrayAccess so callers can use it as a direct memory accessor: $context['my-key'] = 'value' and $context['my-key'] work transparently alongside the existing mergeData() and fluent builder API. The in-memory cache ($data) is retained for prompt-compose performance (hot path) and inter-step relays; write-through is silently skipped when no SwarmMemory binding is present (null-bind tolerance — POPO test setups keep working). This change is non-breaking: an audit of src/ found zero direct $context->data['x'] = ... mutations — all internal state changes already route through mergeData(). Consuming code requires no changes. The $data and $metadata arrays remain as the primary in-memory state containers in v0.9.x; a future v1.0 release will promote SwarmMemory as the sole state source and remove them.

Full entry in the CHANGELOG.