Skip to content
Last updated: Sep 25, 2026

Finni Architecture — Runtime & CFO Co-Pilot ​

OwnerClassificationStatusVerified against code
Kris (product) · Raj (architecture)InternalLiving2026-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) ​

  1. Numbers are computed by code; the LLM only narrates. The Finni graph runs llmCall → toolNode → formatResponse → END — formatResponse returns 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.
  2. Compose capabilities, not agents. A cross-domain answer is assembled from multiple domain tools, not by chaining chat agents.
  3. 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 — classifyFinhubRoute makes 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 ──▶ END

The 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":

WhereDefault modelProviderEvidence
Finni chat (all domain graphs + composite + FinHub classify)qwen3Ollama (local)finni-agent/index.ts, modelConfig.ts
CID judge, Match judgeqwen3Ollama (local)cid-judge/index.ts, match-judge/index.ts
GL-coding judgegemini-flashGeminigl-coding-judge/index.ts
Document extraction (Gemini path)gemini-flash-latestGeminilibs/docai/src/gemini.ts
Global lib default (getModel() no-arg)gemma4OllamamodelConfig.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 elementReality in codeTag
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-modelLive-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 orchestratorplanning-agent exists but is skeletal (substring keyword routing) and not wired into finance.◯
Command layer / autonomous executionNot built; gated behind finding F-15.◯
Server-side RBACCaller-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 MCPapps/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.signals as 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 workingCapital capability.

Revision history ​

DateChange
2026-09-15Ported 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.

Finaisse Internal — Confidential. Access-restricted; not for external distribution.