Skip to content

symfonic.capabilities.memory.turn_records

turn_records

The turn itself, as records — the half of consolidation that is not policy.

_consolidate_impl does two separable things and TA8.61 conflated them. It processes the LLM's extraction ops, which decide which facts are worth keeping and are squarely the adopter's to choose; and it writes the exchange — the question asked and the answer given, to WORKING and to EPISODIC, tagged with one turn_id that pairs them.

Only the second is here. It is the baseline every adopter already has without asking for it, and omitting it is why a kernel turn currently files nothing: the migrated write half was wired and handed a producer that returned nothing.

Nothing in this module decides what is worth remembering. The same exchange always yields the same rows, whatever it happens to be about — a test says so, because a producer that started weighing significance would have quietly become an extraction policy this engine has no business inventing.

turn_records

turn_records(*, query: str, answer: str, scope: MemoryScope, extra_metadata: Mapping[str, Any] | None = None, turn_id: str | None = None) -> tuple[MemoryRecord, ...]

The records one exchange persists, in the two layers legacy writes.

turn_id is generated per call unless supplied, and shared by every row the call produces: the two halves of an exchange are one exchange, and a reader that cannot pair them has the messages without the conversation.

Source code in src/symfonic/capabilities/memory/turn_records.py
def turn_records(
    *,
    query: str,
    answer: str,
    scope: MemoryScope,
    extra_metadata: Mapping[str, Any] | None = None,
    turn_id: str | None = None,
) -> tuple[MemoryRecord, ...]:
    """The records one exchange persists, in the two layers legacy writes.

    ``turn_id`` is generated per call unless supplied, and shared by every row
    the call produces: the two halves of an exchange are one exchange, and a
    reader that cannot pair them has the messages without the conversation.
    """
    identifier = turn_id or uuid.uuid4().hex
    spoken = (
        ("user", (query or "").strip()),
        ("assistant", (answer or "").strip()),
    )
    records: list[MemoryRecord] = []
    for speaker, text in spoken:
        # Legacy skips a blank side rather than writing it, and so does this: a
        # memory with no content costs a retrieval slot and renders as nothing.
        if not text:
            continue
        for layer in _LAYERS:
            records.append(
                MemoryRecord(
                    record_id=f"{identifier}:{speaker}:{layer.value}",
                    text=text,
                    scope_path=scope.path,
                    layer=layer,
                    origin=TURN_ORIGIN,
                    metadata={
                        **dict(extra_metadata or {}),
                        "speaker": speaker,
                        "turn_id": identifier,
                        "source": TURN_ORIGIN,
                    },
                )
            )
    return tuple(records)