Skip to content

symfonic.capabilities.memory.phases.deep

deep

Deep Sleep: roster parity with the shipped consolidator, plus one.

The taxonomy matters here more than anywhere, because two of the three cadences are this capability's own. The shipped system has one main roster of sixteen phases -- the body of SleepConsolidator.run -- and one four-phase subset, quick_nap. nightly_nap calls run, so "nightly" and "Deep Sleep" are the same sixteen on two schedules.

So:

  • :func:~.quick.quick_phases transcribes quick_nap.
  • :func:~.nightly.nightly_phases is a profile this capability defines -- the sixteen minus entity_links -- and nothing in the shipped system runs it.
  • :func:deep_phases is where parity is judged: the sixteen, in legacy's order, plus scope_promotion.

The two DEEP adds. entity_links is legacy's, at its legacy position between synthetic_links and procedural_promotion -- which is why DEEP cannot be spelled (*nightly, extra). scope_promotion is not legacy's at all: the shipped tree promotes a memory toward the root outside the consolidator, so this roster runs it last and owns the fact that it is an addition rather than a transcription.

EntityLinksPhase

EntityLinksPhase(graph: Any, evidence: EpisodicEvidence | None, *, extractor: Any = None, **settings: Any)

Phase 12.5. Names a scope keeps mentioning become nodes it can traverse.

Two numbers, not one: it mints entity nodes and the mention edges that point at them, and PHASE_COUNTERS maps a phase to counters through its single headline count. The headline is the nodes; the edges go through the cycle's legacy-counter channel, which is what that channel was added for.

Source code in src/symfonic/capabilities/memory/phases/deep.py
def __init__(
    self,
    graph: Any,
    evidence: EpisodicEvidence | None,
    *,
    extractor: Any = None,
    **settings: Any,
) -> None:
    self._graph = graph
    self._evidence = evidence
    self._extractor = extractor
    self._settings = settings

applies

applies(context: PhaseContext) -> bool

Legacy's guard: default-off, and needs both an extractor and turns.

enable_entity_linker is off in the shipped configuration, so a deployment that composes no extractor is the ordinary case rather than a misconfiguration -- and it declines rather than reporting a zero.

Source code in src/symfonic/capabilities/memory/phases/deep.py
def applies(self, context: PhaseContext) -> bool:
    """Legacy's guard: default-off, and needs both an extractor and turns.

    ``enable_entity_linker`` is off in the shipped configuration, so a
    deployment that composes no extractor is the ordinary case rather than
    a misconfiguration -- and it declines rather than reporting a zero.
    """
    return self._evidence is not None and self._extractor is not None

ScopePromotionPhase

ScopePromotionPhase(promoter: Any = None)

A memory corroborated in a session moves toward the tenant that owns it.

The one phase on this roster that is not a transcription. The shipped tree promotes outside the consolidator, so there is no legacy position to restore and no legacy behaviour to match -- it runs last, after every phase that could produce a candidate, and it declines unless a deployment gave it somewhere to promote from.

Source code in src/symfonic/capabilities/memory/phases/deep.py
def __init__(self, promoter: Any = None) -> None:
    self._promoter = promoter

deep_phases

deep_phases(*, graph: Any, store: Any = None, policy: Any = None, entity_extractor: Any = None, scope_promoter: Any = None, entity_min_mention_count: int | None = None, entity_max_episodics_per_run: int | None = None, entity_confidence_threshold: float | None = None, **nightly: Any) -> tuple[ConsolidationPhase, ...]

Build the complete DEEP roster, in roster order.

Parameters:

Name Type Description Default
graph Any

the GraphBackend the store reads through, or a GraphMemoryStore.

required
store Any

the memory store, for the phases that learn from a scope's turns. Without it, three of the seventeen decline.

None
entity_extractor Any

what turns an episode into candidate entities. Absent, phase 12.5 declines -- which is the shipped default, not a misconfiguration.

None
scope_promoter Any

async (context) -> int, promoting corroborated memories toward the root. Absent, the phase declines.

None
**nightly Any

forwarded to :func:~.nightly.nightly_phases.

{}
Source code in src/symfonic/capabilities/memory/phases/deep.py
def deep_phases(
    *,
    graph: Any,
    store: Any = None,
    policy: Any = None,
    entity_extractor: Any = None,
    scope_promoter: Any = None,
    entity_min_mention_count: int | None = None,
    entity_max_episodics_per_run: int | None = None,
    entity_confidence_threshold: float | None = None,
    **nightly: Any,
) -> tuple[ConsolidationPhase, ...]:
    """Build the complete DEEP roster, in roster order.

    Args:
        graph: the ``GraphBackend`` the store reads through, or a
            ``GraphMemoryStore``.
        store: the memory store, for the phases that learn from a scope's
            turns. Without it, three of the seventeen decline.
        entity_extractor: what turns an episode into candidate entities.
            Absent, phase 12.5 declines -- which is the shipped default, not a
            misconfiguration.
        scope_promoter: ``async (context) -> int``, promoting corroborated
            memories toward the root. Absent, the phase declines.
        **nightly: forwarded to :func:`~.nightly.nightly_phases`.
    """
    from symfonic.capabilities.memory.rosters import PHASE_ROSTER
    from symfonic.capabilities.memory.schedule import ConsolidationCycle

    if policy is not None:
        # The deployment's own numbers, under the names the factories use.
        # Explicit arguments still win: a caller that passed both meant the one
        # it wrote at the call site, not the one its settings file carries.
        tuned = policy.as_phase_kwargs()
        if entity_min_mention_count is None:
            entity_min_mention_count = tuned.pop("entity_min_mention_count", None)
        if entity_max_episodics_per_run is None:
            entity_max_episodics_per_run = tuned.pop(
                "entity_max_episodics_per_run", None
            )
        if entity_confidence_threshold is None:
            entity_confidence_threshold = tuned.pop(
                "entity_confidence_threshold", None
            )
        for name in (
            "entity_min_mention_count",
            "entity_max_episodics_per_run",
            "entity_confidence_threshold",
        ):
            tuned.pop(name, None)
        nightly = {**tuned, **nightly}

    resolved = phase_graph(graph)
    settings = {
        key: value
        for key, value in (
            ("min_mention_count", entity_min_mention_count),
            ("max_episodics_per_run", entity_max_episodics_per_run),
            ("confidence_threshold", entity_confidence_threshold),
        )
        if value is not None
    }
    evidence = EpisodicEvidence(store) if store is not None else None
    if entity_extractor is not None and store is None:
        raise MemoryContractError(
            "deep_phases was given an entity extractor and no store, so the "
            "phase that mints entities has no turns to read them from. Pass "
            "the memory store, or neither."
        )

    by_name: dict[str, Any] = {
        "entity_links": EntityLinksPhase(
            resolved, evidence, extractor=entity_extractor, **settings
        ),
        "scope_promotion": ScopePromotionPhase(scope_promoter),
    }
    # Nightly's fifteen come from the factory that owns them -- one definition
    # of "what is a strengthen phase", not two.
    for phase in nightly_phases(graph=resolved, store=store, **nightly):
        by_name[phase.name] = phase

    order = PHASE_ROSTER[ConsolidationCycle.DEEP]
    return PhaseRoster((by_name[name] for name in order), resolved)