← Writing

I handed my bank statements to a deep agent — and it actually remembers

· ia · stack · build · 10 min · FR

My agent series followed a simple arc: the harness that turns an LLM into a doer, then the dispatcher that puts two agents to work, then Flue that wraps the whole thing into a framework — on the Astro side, in TypeScript.

This time it’s LangChain shipping its framework: deepagents. Same idea, different ecosystem (Python, LangGraph). And to test it, not a toy — a real personal need: a companion that reads my bank statements (as PDFs, on my machine) and tells me where the money goes, in an actual chat window.

deepagents in one table

deepagents describes itself as a “batteries-included agent harness” built on LangGraph. The funny part is that its four pillars are exactly the blocks I’ve been describing for three months — only named and shipped out of the box:

deepagents pillarWhat it doesWhat I already called it
Planning (write_todos)A near no-op tool that forces the agent to lay out a plan before actingThe harness loop, but with an explicit todo list
Sub-agentsDelegation with isolated context; only the final result bubbles upMy router article: “calling an agent = a tool call”
File systemRead/write/edit/search over pluggable backends → memory + offloading big outputs to diskFlue’s file pattern, the sandbox
System promptThe persona and the guardrailsMy agent.md / SKILL.md

Why banking justifies all four pillars

My rule, repeated in the router article: architecture is a response to real complexity, not a trophy. A good use case is one where each of the four pillars has a genuine reason to exist. Personal finance ticks all of them — and especially the last one, the one I underrated.

Because managing a budget isn’t a throwaway analysis. It’s longitudinal: next month only means something against this one. And that’s exactly what the file system brings — memory. It’s what turns a one-shot analysis into a companion that remembers. That’s the real finding: the pillar that makes the difference isn’t the plan or the sub-agents, it’s the disk.

1. The plan — the agent lays out its todo before executing

Two kinds of requests land in the chat. A one-off question (“how much on dining this month?”): no plan needed, the dispatcher delegates straight away. A multi-step request (“generate June’s report”): there, the agent writes its plan first via write_todos:

todos:
  ☐ extract the statement from statements/2026-06.pdf (locally)
  ☐ pull the structured data              → sub-agent extractor
  ☐ write the extraction to memory/extraction-2026-06.md
  ☐ generate the report + optimizations   → sub-agent reporter
  ☐ update memory/budget.md

write_todos executes nothing. It’s a no-op that forces decomposition: on a multi-step task, an agent that wrote its plan drifts far less than one that improvises.

2. Three agents, one dispatcher — and they collaborate through files

The main deep agent doesn’t answer itself: it classifies your request and delegates to the right specialist. It’s the pattern from my router article, except here the framework fires task(...)style B, autonomous delegation. Three specialists, three clear roles:

Sub-agentIts taskWhat it does with files
extractorPulls structured data: subscriptions, in/out, by categorywrites memory/extraction-<month>.md
chatAnswers any one-off question about your financesreads the extraction + memory
reporterGenerates the synthetic report: in, out, optimizationsreads the extraction, writes memory/report-<month>.md

The key point is in the last column: the three never talk directly. The extractor writes a file, the reporter and chat re-read it. Their collaboration channel is the file system — exactly the shared workspace deepagents puts at the center. (More on that in section 4: the same disk serves as memory across months and as workspace across agents.)

In code, it’s a single call — and I point deepagents at a Claude model, because the framework is model-agnostic:

from deepagents import create_deep_agent

subagents = [
    {
        "name": "extractor",
        "description": "Pulls structured data from a statement: subscriptions, in/out, by category.",
        "prompt": "Read the statement text. Return JSON { income, outgoings, "
                  "categories, subscriptions[] }. Invent no amount. "
                  "Write the result to memory/extraction-<month>.md.",
    },
    {
        "name": "chat",
        "description": "Answers any one-off question about my finances.",
        "prompt": "Answer using memory/. Quantify, cite the month. "
                  "Missing data → say so, don't invent.",
    },
    {
        "name": "reporter",
        "description": "Generates a synthetic monthly report with optimizations.",
        "prompt": "From memory/extraction-<month>.md, write a report: "
                  "1) income vs outgoings, 2) top line items, 3) change vs "
                  "previous month, 4) three concrete, quantified optimizations. "
                  "No moralizing.",
    },
]

agent = create_deep_agent(
    model="anthropic:claude-opus-4-8",   # model-agnostic: Claude, open-weight, or local
    system_prompt=BUDGET_ADVISOR,
    subagents=subagents,
    tools=[extract_statement],           # local PDF extraction — defined right below
)

Notice what I don’t write: no hand-rolled routing loop. The dispatcher is the main agent itself; the code merely declares the three specialists and hands it the PDF-extraction tool. It decides who works.

3. The statements arrive as PDFs — extracted locally

My bank doesn’t hand me a clean CSV: it hands me a PDF per month. That’s the real format, and also the most sensitive one. So extraction happens on my machine, offline, before anything reaches the model. deepagents accepts custom tools: I write one that reads the PDF locally.

import pdfplumber  # 100% local extraction, no network request

def extract_statement(path: str) -> str:
    """Reads a PDF statement from disk and returns its text. Nothing leaves the machine."""
    with pdfplumber.open(path) as pdf:
        return "\n".join(page.extract_text() or "" for page in pdf.pages)

The agent calls extract_statement("statements/2026-06.pdf") when its plan calls for it. The PDF never leaves the folder; only the extracted text enters the reasoning. It’s the first link in a fully local loop — more on that below.

4. The file system — memory, the real star

Here’s the piece that changes everything. deepagents gives the agent file tools (ls, read_file, write_file, edit_file) over a pluggable backend. I use it in three distinct roles:

finances/
├─ statements/
│  ├─ 2026-05.pdf              ← raw monthly statement (the bank's PDF)
│  └─ 2026-06.pdf              ← heavy data, offloaded (out of context)
└─ memory/
   ├─ extraction-2026-06.md    ← structured data, written by the extractor
   ├─ budget.md                ← persistent memory: my baseline budget
   └─ report-2026-06.md        ← the month's report, written by the reporter
  • statements/*.pdf: the heavy data lives on disk, not in context. The agent extracts what it needs, when it needs it.
  • memory/extraction-*.md: written by the extractor, re-read by chat and reporter. It’s the shared workspace across the three agents — their only communication channel.
  • memory/budget.md: next month, the agent re-reads this file and compares. That’s what remembering is — memory across months.
  • memory/report-*.md: the deliverable, versioned month after month.

And since we’re talking bank data, the security point isn’t optional. deepagents offers declarative per-path permissions — the direct echo of the “perimeter security” from my router article, except here the perimeter is over files, not tools:

from deepagents.backends import FilesystemBackend

backend = FilesystemBackend(
    root_dir="./finances",
    permissions={
        "statements/**": "read",      # it reads statements, never touches them
        "memory/**": "read-write",    # it keeps its memory current
    },
)

5. The persona — factual, no moralizing

The system_prompt plays the role of my agent.md. Here, a clear stance:

BUDGET_ADVISOR
You are a factual budget advisor.
- Quantify everything. No invented amounts: if you lack the data, say so.
- No moralizing. "€340 on dining" is a fact, not a scolding.
- Essentials first: where the money goes, then what's cuttable.
- Always compare to the previous month via memory/budget.md.

6. The UI — assistant-ui, because deepagents = LangGraph

A companion is something you talk to. Not an agent.invoke() in a terminal — a real chat window. And here, a happy technical coincidence: deepagents runs on LangGraph, and assistant-ui — the React AI-chat library — happens to ship a first-class LangGraph runtime. The two snap together with no glue.

You serve your deep agent with langgraph dev (a local server), and on the front end, useLangGraphRuntime bridges the gap:

import { AssistantRuntimeProvider, Thread } from "@assistant-ui/react";
import { useLangGraphRuntime } from "@assistant-ui/react-langgraph";
import { Client } from "@langchain/langgraph-sdk";

// LOCAL LangGraph server: `langgraph dev` serves the deep agent on localhost
const client = new Client({ apiUrl: "http://localhost:2024" });

export function BudgetChat({ threadId }: { threadId: string }) {
  const runtime = useLangGraphRuntime({
    stream: async (messages, { command }) =>
      client.runs.stream(threadId, "agent", { input: { messages }, command }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <Thread />   {/* conversation thread + composer, ready to use */}
    </AssistantRuntimeProvider>
  );
}

The bonus is interrupts. deepagents can request a human approval before a sensitive action; assistant-ui’s LangGraph runtime handles those interrupts natively. Concretely: before rewriting memory/budget.md, the agent pauses, the UI shows “update your baseline budget?”, you confirm. Human-in-the-loop shipped by the wiring, not hand-coded.

The full map

Four floors, one reading direction: the view talks to the harness, which splits the work across three agents that collaborate through the documents — and the answer streams back into the chat.

VUE assistant-ui · local chat useLangGraphRuntime · stream + interrupts HARNESS · deep agent (LangGraph) write_todos · dispatch · the loop THE SPLIT — 3 agents, 1 dispatcher extractor subscriptions · in/out → writes extraction.md writes chat any question reads memory reads reporter report + optims → writes report.md reads · writes DOCUMENTS · 100% local statements/2026-06.pdf raw statement (out of context) memory/extraction.md shared workspace across agents memory/budget.md persistent memory memory/report.md the month's deliverable answer / report streamed
From view to disk: the UI talks to the harness, which splits the work across three agents that collaborate through the documents.

Honest: where it stands

Three caveats, in the spirit of what I said about Flue.

It’s Python/LangGraph for the agent, and React for the UI — not my Astro zero-JS turf. So this companion is a separate little tool, not something I bolt onto this static portfolio. There’s deepagentsjs if you want to keep it all in TS, but the mature ecosystem (LangSmith traces, backends) lives on the Python side. Pick your slope with eyes open.

The loop can stay 100% local, and that’s the whole point. PDF extracted on your machine (pdfplumber, offline) + an open-weight or local model (deepagents is agnostic) + a self-hosted LangGraph server (langgraph dev) + a locally served UI: the bank data never leaves your machine. Filesystem permissions are one more guardrail, not a blessing.

And like Flue: I wired it to see, not yet run it in production. Take this as a map, not a battle report.

What it teaches me

My whole agent series, until now, was memoryless. The harness, the router, Flue: every time, a blank context, a task, an answer, then forgetting. deepagents adds the piece I’d never really used — the disk as memory — and that’s what tips it over.

An agent that analyzes your month is a tool. An agent that remembers last month to judge this one, that you talk to in a real UI, and whose statements never leave your machine — that’s a little product. The difference fits in a memory/ folder, two markdown files, and a runtime that snaps on by itself. The plan and the sub-agents, I already knew how to build; memory was the missing piece that stops the agent from starting over every time — like me, staring at my statements, every month, before it.