Spaces:
Running
Learning path
Read the codebase in this order. Each file answers an interview question that actually gets asked about agent systems β the question is stated first, so you know what you are reading for.
Budget: about three focused hours for the six stops, plus an hour reading traces.
1. app/graph/state.py β state design and reducers
"Two agents run in parallel and both write to shared state. What stops one from clobbering the other?"
Read for: why every field written by more than one node carries an
Annotated[..., reducer], and why the reducers are all commutative merges
rather than read-modify-write.
The answer, in one sentence: nothing in this system ever reads state, mutates it, and writes it back β nodes return partial updates and the runtime merges them with a declared reducer, so ordering between the two workers cannot matter.
Check yourself:
- Which fields are written by both agents? (
errors,tool_calls,attempts,token_spend,agents_completed) - What happens if you remove the reducer from one of them? (LangGraph raises
InvalidUpdateErroron concurrent writes β try it.) - Why is
iterationsoperator.addrather than last-write-wins? - Why are
append_recordsandappend_errorsbounded?
Then read tests/test_reducers.py β the second test drives a real superstep
with await points that force interleaving.
2. app/graph/supervisor.py β routing, caps and termination
"How do you stop an agent loop from running forever or burning your budget?"
Read for: the two independent brakes, and how the fan-out is expressed.
The answer: a hard iteration cap and a token/USD budget guard that can trip from inside any node. They are independent on purpose β a cheap infinite loop is caught by the first, an expensive short one by the second.
Check yourself:
- Why does
route_from_supervisorreturn a list of node names? (That is what puts both workers in one superstep.) - Where is "retry a failed ticker exactly once" actually implemented?
(
attempts_for(...) < MAX_ATTEMPTS_PER_TICKERin the pending-ticker helpers.) - The supervisor calls Haiku, but the dispatch set is computed from state. Why does that matter? (A model hiccup can slow the run; it cannot route it somewhere unsafe.)
Then read app/core/budget.py β note ensure_headroom, the pre-flight
check, and why an unknown model prices at the most expensive tier.
3. app/mcp_server/ β tool schemas, and why MCP
"Why MCP? Couldn't you just call a Python function?"
Read for: registry.py (the tool contracts and docstrings), providers.py
(caching, rate limiting, failure-as-data) and client.py (transport, whitelist,
telemetry).
The answer: three things a function call does not give you β a standard other agent frameworks and clients can consume, runtime discoverability of schemas and documentation, and a hard security boundary around a closed tool surface.
Check yourself:
- Why does every tool return an
errorfield instead of raising? (It is what makes per-ticker graceful degradation possible at all.) - Where is the whitelist enforced? (Both ends β server and client.)
- What is the difference between the MCP tools and the "output tools" in
prompts.py? (Data versus structured response. Only the first are executed.) - Run
make mcpand speak to the server yourself.
4. app/graph/verify.py + recompute.py β determinism
"How do you know the model didn't make a number up?"
Read for: the three-layer guarantee, and why recompute.py refuses to import
the tool's maths.
The answer: the model cannot type a number at all. app/models/brief.py
rejects any narrative string containing a bare numeral, so figures exist only as
{{cN}} references into a claim table minted from tool output β and every claim
is then recomputed from the raw price bars by a different implementation.
Check yourself:
- Name the three implementation differences between
metrics.pyandrecompute.py. (Welford vs two-pass variance;accumulatevs a running-peak loop;bisectvs a forward scan.) - Why is
unverifiabletreated as a failure rather than a pass? - What are the tolerances, and why is USD
0.005? - What happens to a quoted headline the model paraphrased?
Then read tests/test_verifier.py::TestDualPathAgreement β the proof the two
paths agree on live data, which is what makes the dual check real.
5. app/graph/gate.py β interrupts and checkpointers
"How does a graph pause for a human and resume hours later?"
Read for: interrupt(), and the comment explaining why the node performs no
side effects before it.
The answer: interrupt() suspends the graph and checkpoints state. The
process can exit entirely. ainvoke(Command(resume=decision), config) continues
from the checkpoint β which is why approval is a separate authenticated HTTP
request and not a callback held in memory.
Check yourself:
- Why must the node avoid side effects before
interrupt()? (It re-executes from the top on resume.) - Where is the "awaiting approval" telemetry emitted instead, and why?
- A reviewer edits the headline to "stock jumped 42 percent". What happens? (Rejected β narrative validation still applies to human edits.)
- Which checkpointer is used locally, which in production, and why does the serializer have an allowlist?
Then read app/services/runner.py β the two ainvoke calls that bracket the
pause.
6. app/graph/context.py β the LangGraph lesson that cost time
"How do you get a live handle β an open MCP session β into a graph node?"
The answer: not through config["configurable"]. LangGraph filters unknown
configurable keys once a checkpointer is attached, because that dictionary is
checkpoint state. Live handles belong in the runtime context channel
(StateGraph(..., context_schema=RunContext) + ainvoke(..., context=ctx)),
which is deliberately not persisted.
Also worth knowing: a node parameter annotated RunnableConfig | None under
from __future__ import annotations is a string at inspection time, so
LangGraph silently ignores it. The parameter looks right and does nothing.
Both were found by running the thing and reading warnings with -W error, not by
reading documentation. That is the story to tell.
7. Twenty traces, end to end
Set LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY, run make eval, then read the
traces rather than the summary.
Look for:
- Span shape β is the supervisor being called more often than the number of dispatches justifies?
- Token spend per role β is Haiku actually taking the routing load?
- Tool latency β which provider call dominates? (
get_price_history, ~1s cold, ~0ms cached.) - The regeneration path β run
demo_mismatchand follow writer β verify β writer β verify β gate. - A degraded run β run
demo_faultand watch the retry-once logic drop a dead ticker at attempt two.
"I debug agents by reading traces, not by guessing" is only credible if you can describe something you actually found this way.
Questions you should be able to answer cold
- Why LangGraph over CrewAI or AutoGen β and what you give up.
- What a reducer is, and what breaks without one.
- Why the tool layer is MCP rather than plain functions.
- How a hallucinated number is prevented β all three layers, in order.
- Why the verifier does not import the tool's maths.
- What happens on a mismatch, exactly, and how many times.
- How the graph survives the process dying between assembly and approval.
- Where the run stops if the budget is exhausted mid-flight, and what status it reports.
- How a prompt-injected headline is prevented from redirecting the run.
- What it costs per brief, and which knob moves that number.
Portfolio context
LedgerLens β Lexora β AlphaBrief: extraction, retrieval, agents. One design language, three live demos, zero dollars spent.