symfonic.memory.layers.working¶
working ¶
WorkingLayer -- bounded active conversation context.
Session-scoped FIFO buffer backed by an optional GraphBackend for persistence. When a graph_store is provided, writes are mirrored to the graph so they survive across process restarts and can be counted by the metrics endpoint. The in-memory buffer is still used for fast within-session retrieval. Evicts oldest in-memory entries when the buffer exceeds max_entries.
WorkingLayer ¶
WorkingLayer(max_entries: int = 10, graph_store: object | None = None, *, stm_summary_mode: STMSummaryMode = 'off', working_graph_retention: int | None = None)
Working memory layer for active conversation context.
BaseMemoryStore contract
- retrieve: returns current context entries (from in-memory buffer)
- write: pushes an entry (FIFO eviction) and mirrors to graph if available
- summarize: returns empty string
- link: no-op
- delete: removes entry from graph when graph_store is set
- clear: clears in-memory deque AND all graph nodes for the tenant
When graph_store is provided, each write is also persisted as a MemoryNode with layer=WORKING so the metrics endpoint can count across restarts.
Graph retention cap: when working_graph_retention is set (an integer), the graph side is capped at that many nodes per tenant. On each push that exceeds the cap, the oldest nodes are pruned. The in-memory deque is governed separately by max_entries (FIFO eviction). The cap is intentionally opt-in (None = unbounded, the pre-F3 behaviour) so existing deployments that rely on the graph for long-term auditing are unaffected until they explicitly set a retention value.
Initialize the layer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_entries
|
int
|
FIFO buffer ceiling per tenant. |
10
|
graph_store
|
object | None
|
Optional graph backend for dual-write persistence. |
None
|
stm_summary_mode
|
STMSummaryMode
|
v7.0.1 R3. Controls the default behaviour of
:meth: |
'off'
|
Source code in src/symfonic/memory/layers/working.py
clear
async
¶
Clear all working context for the tenant.
Empties the in-memory deque AND deletes all WORKING-layer graph nodes for this tenant when a graph_store is configured. Nodes belonging to other tenants are untouched (tenant isolation is enforced by the query_nodes filter).
Source code in src/symfonic/memory/layers/working.py
delete
async
¶
Remove a working memory node from the graph backend.
The in-memory deque uses FIFO eviction only; this method targets the graph-persistence side so that explicit deletes (e.g. triggered by retention-cap pruning or a user request) actually remove the node from the durable store.
When graph_store is not configured the call is a no-op (backward compatible with deployments that use in-memory-only working memory).
Source code in src/symfonic/memory/layers/working.py
get_context
async
¶
Get all current working context entries for the tenant.
Returns entries in chronological order (oldest first).
Source code in src/symfonic/memory/layers/working.py
link
async
¶
push
async
¶
Push an entry to the working memory buffer.
If the buffer is at capacity, the oldest entry is evicted (FIFO). If a graph_store is available, the entry is also persisted as a node. When working_graph_retention is set, the oldest graph nodes for this tenant are pruned after the write so the count stays at or below the configured cap.
v7.26.2 (Shape C1): the optional durability kwarg marks the
dual-written graph node so the sleep-consolidation pipeline can
skip promotion of transient/expired rows. None (default)
writes NO durability key -- byte-identical to pre-v7.26.2 (the
node reads as "durable" on the consolidation side). Resolution
order: explicit kwarg > entry.metadata['durability'] > unset.
An invalid value raises :class:ValueError at the call site
(fail-loud, never a silent gate bypass).
Source code in src/symfonic/memory/layers/working.py
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 | |
rehydrate_from_messages
async
¶
rehydrate_from_messages(scope: TenantScope, messages: list[Any], *, max_turns: int | None = None) -> int
Replay the tail of a persisted transcript into the working deque.
v7.27.0 restart-resume (Q7, option-b): on session resume the
in-memory deque is empty (process restarted), but the durable
checkpointer still holds state['messages']. This method
reconstructs the RECENCY WINDOW only -- the last N user/assistant
turns -- as MemoryEntry rows tagged with the SAME
speaker/turn_id/source metadata shape the live
auto-turn writes use (engine.py), so downstream consumers cannot
tell a rehydrated row from a freshly-written one.
Scope of restore (LOCKED, plan §4.c): recency window ONLY. Tool / system messages are skipped (they are not conversational turns and would blow the cap with noise -- R4). Durable HMS state is NOT touched (it was never lost). No graph write is made -- this is a pure in-memory deque replay; persisting these rows again would double-count graph nodes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
TenantScope
|
Tenant scope selecting the target deque. |
required |
messages
|
list[Any]
|
The checkpointer's |
required |
max_turns
|
int | None
|
Cap on user+assistant rows replayed (counted as
individual rows, not pairs). Defaults to |
None
|
Returns:
| Type | Description |
|---|---|
int
|
The number of rows replayed into the deque. |
Source code in src/symfonic/memory/layers/working.py
render_for_prompt ¶
render_for_prompt(entry: Any, scrubber: Callable[[dict[str, Any]], dict[str, Any]] | None = None) -> str
Render a working-memory entry for MEMORY_CONTEXT.
v7.7: same speaker-prefix shape as :class:EpisodicLayer --
per-message rows emit [working] <speaker>: <content>; pre-
v7.7 rows (no speaker, or speaker == 'legacy') delegate
to :func:render_legacy for byte-identical pre-v7.7 output.
v7.7.1: the engine's working-memory pre-hydration block (see
SymfonicAgent._hydrate_impl -- the recency-prefix loop
immediately preceding the polymorphic-dispatch render) calls
this method on each recent entry so the per-layer render
contract covers both code paths. Pre-7.7.1 the recency
prefix hand-built [working] <content> inline, bypassing
the protocol -- a stale docstring previously cited
engine.py:5184-5189 for the call site, which was both the
wrong line range AND a path that never invoked the renderer
(same bypass class as the v7.6.4 procedural-render-bypass).
Source code in src/symfonic/memory/layers/working.py
retrieve
async
¶
Retrieve current working context entries.
v7.22 T-7.22.9: emits :class:MultiScopeIgnoredWarning on first
encounter of a scope with inherits_from (plan §2.d wires
warning here ONLY -- push / get_context have no
scope-merge semantics to ignore, so emitting from them would be
a false-positive in adopter mental models). Working memory is a
per-(tenant, sub_tenant) FIFO buffer; merging inherited
scopes would either leak cross-tenant context or require a
merge policy with no defensible default.
Source code in src/symfonic/memory/layers/working.py
summarize
async
¶
Summarise working-memory entries according to stm_summary_mode.
v7.0 T09 shipped the extractive summariser but the in-layer
summarize() hard-coded mode="extractive" regardless of
config. v7.0.1 R3 wires the stm_summary_mode kwarg through
so direct-API callers see the same behaviour as engine-routed
callers:
"off"(default) — returns"". Preserves v6.1.x byte-for-byte behaviour."extractive"— delegates tosummarize_entrieswith the deterministic first-sentence / last-sentence heuristic. Zero-cost, no LLM call."llm"— the in-layersummarize()is synchronous- equivalent over the MemoryEntry list and does not own a model provider. Falls through to the extractive path so this call never stalls on an upstream outage; callers who want the true LLM micro-call must invoke :func:symfonic.memory.working.stm_summary.summarize_entriesdirectly with amodel_provider(the engine wires that path viaSymfonicAgent).
Empty input always returns the empty string so the v6.1.x observable "no meaningful summary" behaviour is preserved on empty buffers independent of mode.
Source code in src/symfonic/memory/layers/working.py
write
async
¶
Push an entry to the working buffer (FIFO eviction) and persist to graph.