Skip to content

symfonic.capabilities.memory.events

events

Turning an activation log into events something outside this layer can emit.

The framework streams one activation event per lit-up memory so a consumer can animate a graph while the model is still thinking. That event type does not live here, and importing it would make this capability depend on the runtime it is supposed to be composable into. So this module emits a spec — the same shape, none of the coupling — and the composition root expands it, exactly as :func:~.contribution.contribution_spec does for the prompt compiler.

The vocabulary is held against the real event by tests/capabilities/memory/test_activation_events.py, so a rename on either side of the seam is a red test rather than a TypeError in an adopter's stream.

One behavioural change from the facade, and it was a bug. The engine deduplicated activated nodes by label, keeping the highest score. Two distinct memories that happened to render the same display name — the common case for people, projects, and anything else a slug is derived from — collapsed into one, and the one that vanished was the lower-scored of two real memories. Dedup here is by record id, which is what identity actually means in this capability.

ActivationEventSpec dataclass

ActivationEventSpec(node_id: str, node_label: str, score: float, layer: str, source_node_id: str | None = None)

One activation, in the vocabulary the framework's event uses.

activation_events

activation_events(log: ActivationLog) -> tuple[ActivationEventSpec, ...]

Project log's nodes into emittable specs, strongest first.

Deduplicated by record id keeping the highest score, because the same memory can be reached by more than one path and a consumer animating a graph wants one event per node, at the strength that actually lit it.

A degraded log still emits: whatever the walk reached before the graph went away is real, and suppressing it would make a partial expansion look like an empty one.

Source code in src/symfonic/capabilities/memory/events.py
def activation_events(log: ActivationLog) -> tuple[ActivationEventSpec, ...]:
    """Project ``log``'s nodes into emittable specs, strongest first.

    Deduplicated by record id keeping the highest score, because the same memory
    can be reached by more than one path and a consumer animating a graph wants
    one event per node, at the strength that actually lit it.

    A degraded log still emits: whatever the walk reached before the graph went
    away is real, and suppressing it would make a partial expansion look like an
    empty one.
    """
    best: dict[str, ActivatedNode] = {}
    for node in log.nodes:
        known = best.get(node.record_id)
        if known is None or node.score > known.score:
            best[node.record_id] = node
    ordered = sorted(best.values(), key=lambda node: (-node.score, node.record_id))
    return tuple(_spec(node) for node in ordered)

event_spec

event_spec(spec: ActivationEventSpec) -> Mapping[str, object]

Project a spec into keyword values for the framework's activation event.

A read-only mapping rather than the event itself: the composition root is the component allowed to see both this capability and the runtime's event vocabulary, so it does the construction and neither side learns the other's types. The layer is emitted as its string value, which is what the event declares.

Source code in src/symfonic/capabilities/memory/events.py
def event_spec(spec: ActivationEventSpec) -> Mapping[str, object]:
    """Project a spec into keyword values for the framework's activation event.

    A read-only mapping rather than the event itself: the composition root is
    the component allowed to see both this capability and the runtime's event
    vocabulary, so it does the construction and neither side learns the other's
    types. The layer is emitted as its string value, which is what the event
    declares.
    """
    return MappingProxyType(
        {
            "node_id": spec.node_id,
            "node_label": spec.node_label,
            "score": spec.score,
            "layer": spec.layer,
            "source_node_id": spec.source_node_id,
        }
    )