symfonic.core.learning.phases_profile¶
phases_profile ¶
Phase 5: scoped profile-fact promotion (Deep Sleep plan, Stage 2).
Replaces the legacy apply_soul_corrections (formerly in phases.py),
which was broken three ways at once:
- It wrote instance VALUES into
DomainTemplate.soul_schema-- adict[str, str]mapping field names to their EXPECTED TYPES, consumed byextraction.txtasSchema: {{SOUL_SCHEMA}}to tell the model what shape a SOUL node should have. Writing values in place of types corrupted the schema shown to the model on the next turn. - Its result was discarded: the engine call site passed a defensive
dict(domain.soul_schema)copy, mutated it in place, and never wrote it back --report.soul_updatescounted changes that were then garbage-collected. - It was a live cross-tenant accumulator: the scaffolder's worker
template built the dict ONCE before the tenant loop and passed the
SAME object to every tenant's
consolidator.run(). In-place mutation meant tenant A's profile values could leak into the dict handed to tenant B.
promote_profile_corrections fixes all three structurally rather than
by convention:
- It takes no caller-owned mutable dict in its signature at all -- there is nothing to accidentally share across tenants (kills bug 3).
- It is scope-keyed by construction: every read and write goes through
GraphMemoryStorewith the caller'sTenantScope, so a promotion under tenant A's scope structurally cannot touch tenant B's data. - It writes corrections onto the tenant's own
SOUL:node(s) in the SEMANTIC layer -- never into a type schema. The type schema (profile_fields) is READ ONLY, to learn which field names count as "profile" (kills bug 1). - The write IS the result (a graph mutation) -- there is no return value to discard (kills bug 2).
promote_profile_corrections
async
¶
promote_profile_corrections(
graph: GraphMemoryStore,
scope: TenantScope,
recent_nodes: list[MemoryNode],
profile_fields: frozenset[str],
) -> 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 |
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/core/learning/phases_profile.py
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 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 | |