alphabrief / LEARNING.md
Abdr007's picture
AlphaBrief β€” deployed tree
69e310f
|
Raw
History Blame Contribute Delete
8.14 kB
# 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
`InvalidUpdateError` on concurrent writes β€” try it.)
- Why is `iterations` `operator.add` rather than last-write-wins?
- Why are `append_records` and `append_errors` bounded?
**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_supervisor` return 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_TICKER` in 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 `error` field 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 mcp` and 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.py` and
`recompute.py`. (Welford vs two-pass variance; `accumulate` vs a running-peak
loop; `bisect` vs a forward scan.)
- Why is `unverifiable` treated 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_mismatch` and follow writer β†’ verify β†’
writer β†’ verify β†’ gate.
- **A degraded run** β€” run `demo_fault` and 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
1. Why LangGraph over CrewAI or AutoGen β€” and what you give up.
2. What a reducer is, and what breaks without one.
3. Why the tool layer is MCP rather than plain functions.
4. How a hallucinated number is prevented β€” all three layers, in order.
5. Why the verifier does not import the tool's maths.
6. What happens on a mismatch, exactly, and how many times.
7. How the graph survives the process dying between assembly and approval.
8. Where the run stops if the budget is exhausted mid-flight, and what status it
reports.
9. How a prompt-injected headline is prevented from redirecting the run.
10. 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.