planner-lab: a financial planning agent where the LLM never does the math
planner-lab is an open-source tool that reads a household’s financial facts and writes a short retirement-readiness report. The division of labor is strict: a language model writes the sentences, tested code computes every number, and if any number in the draft cannot be traced back to a real computation, no report comes out.
I released planner-lab 0.1.1 on PyPI this week. It is an experimental framework for building personal-finance planning agents with one rule: the LLM never does the math. The model appears at exactly two points, drafting the memo prose and reviewing its tone. Everything else runs in deterministic, seeded code: a typed case file, closed-form calculators, an optional Monte Carlo adapter, and a computation ledger that assigns every result a citable id. A critic gate then blocks any memo whose numbers cannot be traced back to that ledger.
I have made this argument before for phased-array design: put the domain math in real tools, confine the model to orchestration and prose, and record enough to audit the run afterward. Retirement planning is a good second domain for it, because a fabricated number in a planning memo is worse than no memo at all: a wrong figure reads exactly as fluently as a right one, and the reader is the person least equipped to recheck it. planner-lab is MIT-licensed, needs Python 3.11+, and is a research and education project: not a financial advisor, no trade execution, no stock picking, synthetic households only.
pip install "planner-lab[agent]"

The pipeline. The two purple boxes are the only places a model appears: drafting the memo and reviewing its tone. Every number in the output comes from the ledger in the blue lane.
Every number has a receipt
A run starts from a typed case file, a YAML document validated into Pydantic models: household members, accounts, income, spending, goals. Before anything is computed, an assumptions gate shows three assumption sets (base, conservative, optimistic, with rates stated in real, after-inflation terms) and waits for confirmation.
The calculators then run per assumption set. The core math is deliberately simple and closed-form:
the funded ratio divides the portfolio by the capital required to support the spending target at a
chosen withdrawal rate, and the financial-independence timeline solves the compound-growth equation
for the number of years until that target is reached. An optional adapter adds Monte Carlo
simulation behind a generic ScenarioSimulator interface (2,000 seeded paths per assumption set,
plus crash and sequence-risk stress runs), and further adapters add a funded-ratio health metric
and portfolio allocation diagnostics. The core installs with none of them; its only dependencies
are Pydantic, Typer, Rich, and PyYAML.
Every result, whichever engine produced it, lands in the computation ledger with an id. When the
LLM drafts the memo, it receives a menu of those ids and may cite numbers only from that menu. The
memo command writes the markdown memo plus a .audit.json sidecar holding the full ledger and the
critic report, so every figure can be checked by hand.
The critic gate
The critic is the enforcement mechanism for the traceability rule. After the model drafts a memo, nine deterministic checks run against the ledger and the case file: every number must resolve to a recorded computation or a case-file field (within a small numeric tolerance), the memo must contain no securities recommendations, the mandatory disclaimer must be present, assumptions and missing inputs must be disclosed, citations must match documents that were actually fetched, real and nominal figures must be labeled consistently, the language must not overstate certainty, and results must be framed as diagnostics. An LLM tone review runs on top of the deterministic pass, and a failure from either side fails the check.
A rejected draft gets one revision attempt. If the second draft also fails, the run stops, the failing checks are printed, and no memo is written. This is a real rejection captured during development, running with research enabled against a local 8B-class model:
$ planner-lab memo examples/cases/sample_household.yaml -o memo.md --yes --research
Memo rejected by the critic after one revision:
FAIL numbers_traceable: 2 number(s) failed to trace
Retirement age: source_id 'case:household.persons[0].planned_retirement_age' does not resolve
Spending target: source_id 'case:goals[0].annual_amount_today' does not resolve
FAIL citations_present: citation set inconsistent with fetched research
research was fetched but the memo cites nothing
FAIL certainty_not_overstated: [llm] The prose implies certainty about retirement
feasibility, stating 'retirement at 62 with $60,000 annual spending is feasible
under the base assumption set' without qualifying this as a probabilistic outcome.
Three guards fired at once: the model cited two real case-file values with a path syntax the resolver did not accept, it ignored the research it had fetched, and the tone review (a second model) flagged “is feasible” as a promise the underlying simulation does not make. The worst case is no memo rather than a confidently wrong one. The case study of a rejected memo walks through this run and the code changes it led to.
What the model still does
A rule this strict sounds like it leaves the model nothing to do. What it leaves is judgment over verified quantities. The model reads the base, conservative, and optimistic results side by side and says what the spread means for the household. It articulates the trade-offs the numbers imply: retiring later versus spending less versus saving more, and which recorded data gaps actually matter. In interactive mode it decides which engines to run and what to ask during intake. And it chooses which results and citations belong in the memo, with every recorded gap disclosed. The mental model is an analyst who is required to use the approved models but still writes the memo.
The ceiling is equally deliberate. If a scenario needs math no calculator implements, the model cannot improvise it in prose; the critic rejects the numbers as untraceable. The path to more capability is adding a tested calculator behind the generic interfaces, not loosening the model’s constraints. The memo presents funded ratios, probabilities, and levers; a person confirms the assumptions before anything simulates, and the person decides.
Cited research over MCP
The memo can ground its methodology discussion (safe withdrawal rates, sequence risk) in published
guides. planner-lab is an MCP client here: point PLANNER_LAB_RESEARCH_MCP_URL at any
streamable-HTTP Model Context Protocol server that exposes search and fetch tools, and run with
--research. Every fetch is recorded in the ledger, and the citation check verifies memo citations
against those fetch records. The model cannot cite a document that was never retrieved, which
closes off the standard fabricated-reference failure mode.
Runs locally
The agent layer is built on the Strands Agents SDK and defaults to a local
Ollama server with the qwen3 model, so a full run costs nothing and no
data leaves the machine. Small local models occasionally mis-copy a number or skip a citation; the
critic then rejects the memo instead of emitting it, which is the design working as intended. A
larger model (OLLAMA_MODEL=gpt-oss:20b) makes full runs with simulation, diagnostics, and
research noticeably more reliable, and Amazon Bedrock is available as an alternative provider.
Configuration is entirely environment variables, and --trace prints OpenTelemetry spans to stdout
(or export them to any OTLP endpoint).
Try it
The core works offline with no LLM:
pip install planner-lab
planner-lab validate examples/cases/sample_household.yaml
planner-lab calc funded-ratio --portfolio 900000 --spending 50000
planner-lab calc fi-timeline --portfolio 250000 --savings 50000 --spending 60000

The offline core: validate, calc, and import-cashflow, with no model involved.
With Ollama running, the full pipeline:
pip install "planner-lab[agent]"
planner-lab memo examples/cases/sample_household.yaml -o memo.md --yes
planner-lab analyze examples/cases/sample_household.yaml --simulate --health --allocation --yes
planner-lab import-cashflow transactions.csv --format monarch --case my_case.yaml --write
planner-lab intake -o my_case.yaml # interactive chat that builds a case file
The repo has the architecture diagram,
the schemas, and three case studies with real generated memos checked in verbatim: a
full analysis of an on-track couple,
a
run over incomplete data with CSV import,
and the
rejected memo.
The package is on PyPI as planner-lab, and each release
is archived on Zenodo with a citable DOI
(10.5281/zenodo.21301713).
planner-lab is a research and education project. Nothing it produces is financial, investment, tax, or legal advice, and the examples use synthetic households only. It is an independent project I build on my own time; the views are my own and do not represent any current or former employer.
Frequently asked questions
Is planner-lab financial advice?
No. It is a research and education project: not a financial advisor, no trade execution, no stock picking, no account anywhere. Every memo it emits carries a disclaimer saying so, and one of the critic checks fails the memo if that disclaimer is missing. The examples use synthetic households only.
Does planner-lab run locally?
Yes. The default provider is a local Ollama server with the qwen3 model; a larger model such as gpt-oss:20b makes full runs more reliable. Amazon Bedrock is available as an alternative provider, and the core calculators and CSV import run with no LLM at all.
What does the critic gate check?
Nine deterministic checks, including that every number in the memo traces to a recorded computation or case-file field, that citations match documents actually fetched, that real and nominal figures are labeled consistently, and that the language does not overstate certainty. An LLM tone review runs on top. One revision is attempted; a second rejection means no memo is written.
Can the agent reason about complex financial scenarios?
Yes for interpretation, deliberately no for improvised math. The model compares scenario results, articulates trade-offs, decides which engines to run, and chooses what belongs in the memo. It cannot compute anything no calculator implements: the critic rejects untraceable numbers, so the analytical ceiling is the tool library, which grows by adding tested calculators.
What does the LLM actually do?
Two things: it drafts the memo prose, citing numbers only from a menu of ledger ids it is given, and it reviews the draft's tone. All arithmetic, simulation, and diagnostics run in deterministic, seeded code before the model ever sees a number.