Skip to content

State and flow

In one paragraph, for a non-engineer

Each step of the system can only see the information it needs. The step that classifies your question, for example, never sees any of your clinical data at all.

The real state model arrives in Phase 3; Phase 0 ships only a walking-skeleton state that exists to prove the streaming path.

Narrow projections

Every node receives a read-only projection of state rather than the whole blob.

Node Sees
classify_intent The question and the patient's literacy level. No clinical content
investigate The policy decision, the tool scope, evidence gathered so far
extract_claims The draft answer only
post_flight Draft, evidence records, pre-flight decision. Not the raw message history
publish The judged answer and the policy record

classify_intent not seeing clinical content is a deliberate privacy property, not an optimisation.

Bounded loops

Both counted in state, both with a test asserting termination:

  • evidence_pass ≤ 1 — one repair pass for uncited claims
  • tool_calls ≤ max_tool_calls — plus a token and wall-clock ceiling

On breach the run terminates with a conservative templated response and the reason recorded. It never silently truncates a clinical answer.

Streaming

The API bridges astream_events to Server-Sent Events, one event per node. The node filter is name == metadata["langgraph_node"], because conditional-edge routing functions also emit on_chain_end carrying the source node in their metadata under their own function name.

Graph state.

SkeletonState is the Phase 0 walking skeleton, kept so the streaming path stays proven end to end. NavigatorState is the real state (docs/PLAN.md §5.1), carrying the pre-flight decision, the scoped-executor output, and the draft.

The narrow per-node projections (§5.4) are enforced by what each node reads, not by separate TypedDicts: classify_intent reads only the question and the patient's literacy level — never clinical content — which is a deliberate, defensible privacy property stated in the docs.

Bases: TypedDict

The real graph state (§5.1).

total=False because the state accumulates: intake sets the patient header, the pre-flight gate sets the decision, investigate appends evidence, and draft_answer sets the draft. Keys are present only after the node that owns them has run.

Source code in src/navigator/graph/state.py
class NavigatorState(TypedDict, total=False):
    """The real graph state (§5.1).

    `total=False` because the state accumulates: intake sets the patient header,
    the pre-flight gate sets the decision, investigate appends evidence, and
    draft_answer sets the draft. Keys are present only after the node that owns
    them has run.
    """

    # intake
    question: str
    # Earlier turns of this conversation, oldest first, each {"question", "answer"}.
    history: list[dict[str, str]]
    # contextualize: set only when a follow-up was rewritten. `question` then holds the
    # standalone rewrite and this holds the patient's own words.
    original_question: str
    patient_id: str
    run_id: str
    literacy_level: str
    language: str
    autonomy_level: str
    started_at: float

    # pre-flight gate
    rule_matches: list[RuleMatch]
    intent: IntentAssessment
    question_language: str
    policy_decision: PolicyDecision

    # investigate (the scoped executor's output)
    messages: list[BaseMessage]
    evidence: list[EvidenceRecord]
    security_events: list[SecurityEvent]
    tool_call_count: int

    # draft
    draft: PatientAnswer

    # post-flight (the centrepiece — §5.3)
    claims: list[Claim]
    evidence_pass: int
    post_flight: PostFlightResult
    published: PatientAnswer

Bases: TypedDict

Walking-skeleton state — replaced by NavigatorState in the real graph.

Source code in src/navigator/graph/state.py
class SkeletonState(TypedDict):
    """Walking-skeleton state — replaced by `NavigatorState` in the real graph."""

    question: str
    steps: list[str]