Skip to content

zettelkasten.synapse.typing_llm

zettelkasten.synapse.typing_llm

Hands-off LLM typing of cross-store candidate pairs.

Given a candidate (one memory node, one ZK node) that the cheap signals flagged, classify the RELATION the memory (practice) node bears to the ZK (canon) node and how confident that is. Tool-free: the classifier sees only the two node texts + the candidate signals, never the graph, so it cannot invent a third node.

Edge direction is always memory --relation--> zk:

  • applies — the practice uses/applies the canonical method or concept.
  • corroborates — the practice's result supports/confirms the canonical claim.
  • contradicts — the practice's result conflicts with the canonical claim.
  • motivated-by — the practice/decision was motivated by the canonical idea.
  • instantiates — the practice is a concrete instance/example of the concept.
  • related — weak, generic association (kept but low value).
  • none — not actually related; rejected by the build step.

TypingBackendError

Bases: RuntimeError

The LLM typing backend was unreachable or timed out.

Distinct from a per-pair none classification (a valid answer meaning "not related"): this signals the whole typing RUNTIME is unavailable — the cursor-sdk bridge never came up in this process, an error event was streamed, or a call exceeded :data:TYPING_TIMEOUT_S. The build step treats it as a reason to ABORT fast rather than re-attempt a dead backend once per candidate (which would waste max_pairs timeouts).

Source code in zettelkasten/synapse/typing_llm.py
class TypingBackendError(RuntimeError):
    """The LLM typing backend was unreachable or timed out.

    Distinct from a per-pair ``none`` classification (a valid answer meaning "not
    related"): this signals the whole typing RUNTIME is unavailable — the
    cursor-sdk bridge never came up in this process, an ``error`` event was
    streamed, or a call exceeded :data:`TYPING_TIMEOUT_S`. The build step treats
    it as a reason to ABORT fast rather than re-attempt a dead backend once per
    candidate (which would waste ``max_pairs`` timeouts).
    """

parse_typing_response

parse_typing_response(text: str) -> dict[str, Any]

Parse the classifier's JSON reply into {relation, confidence, rationale}.

Robust to code fences and surrounding prose; degrades to none/0.0 on any malformed output so a single bad reply can never crash a build.

Source code in zettelkasten/synapse/typing_llm.py
def parse_typing_response(text: str) -> dict[str, Any]:
    """Parse the classifier's JSON reply into ``{relation, confidence, rationale}``.

    Robust to code fences and surrounding prose; degrades to ``none``/0.0 on any
    malformed output so a single bad reply can never crash a build.
    """
    text = (text or "").strip()
    if not text:
        return {"relation": "none", "confidence": 0.0, "rationale": ""}
    m = re.search(r"\{.*\}", text, re.DOTALL)
    blob = m.group(0) if m else text
    try:
        data = json.loads(blob)
    except Exception:
        return {"relation": "none", "confidence": 0.0, "rationale": ""}
    relation = str(data.get("relation", "none")).strip().lower()
    if relation not in _ALL_RELATIONS:
        relation = "none"
    try:
        confidence = float(data.get("confidence", 0.0))
    except (TypeError, ValueError):
        confidence = 0.0
    confidence = max(0.0, min(1.0, confidence))
    rationale = str(data.get("rationale", "")).strip()
    return {"relation": relation, "confidence": confidence, "rationale": rationale}

llm_typer

llm_typer(memory: dict[str, Any], zk: dict[str, Any], signals: dict[str, Any]) -> dict[str, Any]

Default typer: one tool-free, time-bounded LLM classification.

A malformed/unexpected reply still degrades to none (via :func:parse_typing_response) — that is a valid per-pair answer. But an INFRASTRUCTURE failure (the bridge never came up, an error event, or a :data:TYPING_TIMEOUT_S timeout) is re-raised as :class:TypingBackendError so the caller can abort the build instead of silently classifying every pair as none (which used to mask a wedged backend as an empty overlay).

Source code in zettelkasten/synapse/typing_llm.py
def llm_typer(memory: dict[str, Any], zk: dict[str, Any], signals: dict[str, Any]) -> dict[str, Any]:
    """Default typer: one tool-free, time-bounded LLM classification.

    A malformed/unexpected reply still degrades to ``none`` (via
    :func:`parse_typing_response`) — that is a valid per-pair answer. But an
    INFRASTRUCTURE failure (the bridge never came up, an ``error`` event, or a
    :data:`TYPING_TIMEOUT_S` timeout) is re-raised as :class:`TypingBackendError`
    so the caller can abort the build instead of silently classifying every pair
    as ``none`` (which used to mask a wedged backend as an empty overlay).
    """
    try:
        from zettelkasten.llm_adapter import run_toolless_agent

        text = run_toolless_agent(
            "synapse-connection-typer",
            _SYSTEM,
            _build_prompt(memory, zk, signals),
            error_message="connection typing failed",
            timeout=TYPING_TIMEOUT_S,
        )
    except Exception as exc:
        raise TypingBackendError(str(exc) or exc.__class__.__name__) from exc
    return parse_typing_response(text)