symfonic.capabilities.memory.phases¶
phases ¶
The consolidation phases, as implementations the capability owns.
A roster names phases; this package is where they are. Until now the names on
PHASE_ROSTER had no implementations behind them at all, so
ConsolidationRuntime() composed with nothing ran nothing, reported an
empty phases_run, and answered clean -- a cycle that did nothing and
called it a success.
Each module here holds one phase's logic and nothing else, moved from
symfonic.core.learning rather than rewritten. The legacy consolidator
imports these same functions during the transition: two transcriptions of one
phase is two behaviours, and the second is found by an adopter rather than by
a test.
:mod:.quick holds the four phase objects the QUICK cadence runs and the
factory that builds the complete set.
merge_semantic_duplicates
async
¶
merge_semantic_duplicates(graph: Any, scope: TenantScope, *, chat_model: Any, embedding_provider: Any | None = None, embedding_threshold: float = DEFAULT_EMBEDDING_THRESHOLD, lexical_threshold: float = DEFAULT_LEXICAL_THRESHOLD, max_pairs_per_run: int = 10, on_candidates: Callable[[Iterable[Any]], None] | None = None, on_supersede: Callable[[Any, Any], None] | None = None) -> int
Merge LLM-confirmed duplicate semantic facts; return merge count.
Per-pair failures are logged and skipped -- one bad judge response must never abort the phase.
on_candidates and on_supersede report node ids, and nothing else,
to a caller keeping a cycle ledger. The supersede hook fires where the
retraction is written rather than being derived from the return value, so
the count stays true if one merge ever absorbs more than one row. Both are
optional and the legacy consolidator passes neither.
Source code in src/symfonic/capabilities/memory/phases/merge.py
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 | |
promote_profile_corrections
async
¶
promote_profile_corrections(graph: GraphMemoryStore, scope: TenantScope, recent_nodes: list[MemoryNode], profile_fields: frozenset[str], now: datetime | None = None) -> int
Promote user-corrected profile facts onto the tenant's SOUL node.
Trigger condition (kept verbatim from the pre-redesign
apply_soul_corrections, phases.py:446): only nodes carrying
properties["_last_edited_by"] == "user_manual_edit" are treated as
corrections. This keeps agent self-edits (extraction writes, this
phase's own promotion writes, etc.) out of the promotion loop --
load-bearing, do not relax without also revisiting the idempotency
guarantee below.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
graph
|
GraphMemoryStore
|
Tenant-scoped graph store. Both the read (locating the
canonical SOUL node) and the write (applying corrections) go
through this store with |
required |
scope
|
TenantScope
|
Tenant isolation scope for every graph operation. |
required |
recent_nodes
|
list[MemoryNode]
|
Nodes considered for this consolidation pass
(typically |
required |
profile_fields
|
frozenset[str]
|
The set of field names that constitute "profile"
for this domain. Callers derive this from
|
required |
now
|
datetime | None
|
Instant recorded as each promoted field's provenance. Defaults to wall-clock UTC. Injectable because a caller that pins its own clock -- a test, a replay, a walkthrough -- would otherwise write a stamp it cannot predict, and the rendered provenance would read as being from the future. |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Count of profile fields whose value actually changed on the |
int
|
canonical SOUL node (mirrors the legacy |
int
|
"count of key assignments made" semantics). A guard-marker-only |
int
|
rewrite (see idempotency note below) does not increment this |
int
|
count even though a graph write occurs. |
Idempotency: when the correction source IS the canonical SOUL node
(a human edited it directly rather than via a separate correction
record), its own _last_edited_by guard is flipped from
"user_manual_edit" to PROMOTION_MARKER even if the "corrected"
values already match (i.e. even when the differential count is zero).
Without this, the node would satisfy the guard again on the very next
pass -- reading this promotion's own prior output back as a fresh
user correction and re-promoting forever.
Source code in src/symfonic/capabilities/memory/phases/profile.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | |
summarize_episodic
async
¶
summarize_episodic(episodic_layer: Any, scope: Any, *, llm_summarise: Any | None = None, max_entries: int = 100, summarize_batch: int = 50, gate: DurabilityGate | None = None) -> tuple[str, int]
Summarize oldest episodic entries into a single text, delete originals.
When the total event count exceeds max_entries, the oldest
summarize_batch entries are retrieved, concatenated (or LLM-
summarised when llm_summarise is provided), and the originals
are deleted from the vector store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
episodic_layer
|
Any
|
An |
required |
scope
|
Any
|
Tenant scope for isolation. |
required |
llm_summarise
|
Any | None
|
Optional async callable |
None
|
max_entries
|
int
|
Threshold below which summarization is skipped. |
100
|
summarize_batch
|
int
|
Number of oldest entries to summarize per run. |
50
|
Returns:
| Type | Description |
|---|---|
str
|
Tuple of |
int
|
summarization was needed, returns |