Finni Architecture — Runtime & CFO Co-Pilot
| Owner | Classification | Status | Verified against code |
|---|---|---|---|
| Kris (product) · Raj (architecture) | Internal | Living | 2026-09-15 — blitz/src/ |
What Finni is. A cross-domain finance co-pilot. It converses (Layer 3), calls tools that compute in code, and — where it acts — hands typed intents to governed workflows. This page documents the runtime as it ships today first, then the production target architecture (clearly quarantined as design). It consolidates the Z-drive FINNI-PRODUCTION-ARCHITECTURE draft, corrected against code.
Corrections vs the source draft. The typed capability layer and the
cfo.signalsread-model are design, not shipped; server-side RBAC is not enforced on the chat path; and the model map is mixed (not "judges=qwen, chat=Gemini"). Details below.
1. Three principles (all hold in code)
- Numbers are computed by code; the LLM only narrates. The Finni graph runs
llmCall → toolNode → formatResponse → END—formatResponsereturns the tool's markdown verbatim and the graph never loops back to the model to "reconsider" a number (finni-agent/index.ts). This is the core anti-hallucination guarantee. - Compose capabilities, not agents. A cross-domain answer is assembled from multiple domain tools, not by chaining chat agents.
- Act only through governed workflows. Finni never mutates a table directly; its one write path (JV submit) posts through
processjournal(validate → BPD route / approve). See §4.
2. The runtime, as shipped
Agents
- 6 domain agents (LangGraph graphs) —
cashapp,collections,invoice,journal,closehub,reconciliation(financeagents/src/agents/finni-agent/index.ts). - FinHub supervisor —
classifyFinhubRoutemakes one LLM hop (withStructuredOutput(FINHUB_ROUTE_SCHEMA)) to classify a CFO query, then dispatches to one of the 6 domain graphs, or runs a composite graph for cross-domain routes (finhub-composite-tools.ts). This is the concrete "Finni-at-FinHub" surface. - 2 builder agents —
graph-builder,grid-builder(render charts/grids from data). - 6 judge scorers (Layer 2) —
cid,match,po-match,remittance,gl-coding,collection. They score ambiguous candidates in human-review surfaces; they never post.
The graph shape
user turn
│
▼
llmCall ──(bind domain tools; extract params / pick tool)
│
▼
toolNode ──(TOOLS COMPUTE — DuckDB/Postgres/service calls)
│
▼
formatResponse ── returns tool markdown verbatim ──▶ ENDThe model chooses a tool and extracts parameters; the tool does the arithmetic; the model narrates the result. No re-loop, no LLM-authored figures.
Model-provider map
The deployed model is env-driven (DEFAULT_FINNI_MODEL / DEFAULT_MODEL), but the code-committed defaults are mixed — worth knowing before you assume "it's all Gemini":
| Where | Default model | Provider | Evidence |
|---|---|---|---|
| Finni chat (all domain graphs + composite + FinHub classify) | qwen3 | Ollama (local) | finni-agent/index.ts, modelConfig.ts |
| CID judge, Match judge | qwen3 | Ollama (local) | cid-judge/index.ts, match-judge/index.ts |
| GL-coding judge | gemini-flash | Gemini | gl-coding-judge/index.ts |
| Document extraction (Gemini path) | gemini-flash-latest | Gemini | libs/docai/src/gemini.ts |
Global lib default (getModel() no-arg) | gemma4 | Ollama | modelConfig.ts |
OpenAI is wired in modelConfig.ts but no default path selects it. The clean "judges on local Ollama, chat on Gemini Flash" split from the strategy doc is not what's wired — chat defaults to qwen3, and one judge (gl-coding) defaults to Gemini.
3. Governed execution (the write path)
Finni's only mutation today is journal submission. journalSubmitJv → submitManualJvViaApi POSTs to /jvprocessingdata/worklist/manual, which hands the JV to processjournal (validate → BPD route / approve). No Finni tool writes to a table directly. This is the "propose → governed pipeline posts" shape the production design generalises.
4. Production target architecture (DESIGN — ◯)
The draft proposes a five-layer target. It is not built; treat everything in this section as design. The keystone idea is a typed capability contract: one artifact that serves production correctness, the orchestrator, and external (A2A/MCP) agents at once.
◯ Conversation layer CFO orchestrator (productionises the skeletal planning-agent)
◯ Capability layer typed, RBAC'd, versioned — cash.cashflowLevers(),
collections.chase(), ap.dpoOpportunity(), close.readiness()
◯ Command layer typed intents → ApprovalWorkflow.evaluate → Temporal
◯ Signals read-model cfo.signals as a persisted model (does NOT exist)What's actually there today vs the design
| Design element | Reality in code | Tag |
|---|---|---|
Typed capability contract (cash.cashflowLevers(), ap.dpoOpportunity(), close.readiness()) | Absent. Tools are LangChain tool() wrappers emitting markdown. DPO is a table row inside getWorkingCapital, not a typed getDpoOpportunity(). | ◯ |
cfo.signals read-model | Live-computed, not persisted. libs/appsloader/src/finhub/cfo.signals.ts = pure functions over already-assembled module cards; no table, no recompute. | ⚠️ (works, but not the designed read-model) |
| CFO orchestrator | planning-agent exists but is skeletal (substring keyword routing) and not wired into finance. | ◯ |
| Command layer / autonomous execution | Not built; gated behind finding F-15. | ◯ |
| Server-side RBAC | Caller-supplied on the WS path — ws.systemagent.ts passes a UI-session userRole (placeholder pending Finboard #671); graph state defaults role: "controller". The HTTP /finni controller does derive role from businessuserroles. | ⚠️ |
| A2A / "bring your own agent" via MCP | apps/mcp is framework-only (demo math tool); no capability manifest, no per-tenant key wiring. Gated on F-01 / F-11. | ◯ |
The flagship use case (design)
"How do I improve cashflow?" decomposes into 5 levers — accelerate collections, clear unapplied cash, resolve disputes, optimise DPO, tighten terms — shipped as 4 slices: Answer (read-only) → Act (F-15 + approval) → Foresee (forecast) → Open up (A2A/MCP). Rich cards exist in the UI (FinCashflowPlan.vue, FinCloseHealth.vue, FinExposures.vue), but the committed-lever → closehub-ad-hoc-task automation and the recovery-factor model (currently a hardcoded 0.5) are design.
5. Open decisions
- Move the tool-layer compose+rank into the orchestrator graph and unify ranking (currently duplicated cash-service ↔ tool).
- Build
cfo.signalsas a real read-model, or keep it as pure curation over cards. - Close F-01 / F-11 (tenant binding / authz) before any A2A/MCP external exposure.
- Replace the hardcoded 0.5 recovery factor; add a
workingCapitalcapability.
Revision history
| Date | Change |
|---|---|
| 2026-09-15 | Ported from Z FINNI-PRODUCTION-ARCHITECTURE; separated shipped runtime from design; corrected the capability layer (aspirational), cfo.signals (live-computed), RBAC (caller-supplied on WS path), and the model/provider map. |