Overview
This document explains the current implementation of Zeus’s conversational assistant in the web app. It is intended for engineers who need to understand how the assistant is wired together today, how to replicate it, and where the boundary sits between:- LLM-generated behavior
- deterministic application code
- the separate plans system
- the user-facing chat page at
/chat - the admin chat manager that can inject an assistant reply into a user thread
- in-chat CRUD for self-managed assets and manual transactions
- in-chat goal creation
- structured assistant UI such as smart actions, quick forms, asset pickers, and welcome cards
What The Assistant Is Today
At a high level, the assistant is a Next.js + AI SDK + ToolLoopAgent system with four major responsibilities:- Persist user and assistant messages into
chat_threadsandchat_messages. - Send the latest chat context into a configured model provider.
- Let the model call a bounded set of typed tools.
- Convert tool outputs into deterministic structured UI specs that the frontend renders and turns back into follow-up chat messages.
Primary Surfaces
Current Responsibility Split
System Map
End-To-End Lifecycle
1. Thread Creation
The frontend creates a thread by callingPOST /api/chat/thread.
- Route:
apps/web/app/api/chat/thread/route.ts - Service:
createChatThread()inapps/web/lib/chat/service.ts - Persistence target:
chat_threads
2. Frontend Message Submission
The main chat page lives inapps/web/components/assistant-chat-page-client.tsx.
The page uses useChat from @ai-sdk/react with a DefaultChatTransport configured for /api/chat.
The transport explicitly preserves the default AI SDK payload fields and adds threadId:
This is important because the backend route expects a valid
threadId, and the project standard is to preserve the default AI SDK payload rather than replacing it with a partial custom body.
3. Backend Request Handling
POST /api/chat in apps/web/app/api/chat/route.ts performs this sequence:
- Authenticates the API user.
- Validates that
messagesandthreadIdexist. - Confirms the thread belongs to the user.
- Finds the latest user message from the submitted message list.
- Persists that latest user message with
upsertChatMessage(). - Loads recent persisted context with
listRecentChatContext(). - Calls
createTransactionAssistant(user, contextMessages). - Streams the assistant response back to the client.
- On stream finish, persists the final assistant message.
4. Context Window vs Stored History
The assistant stores the full thread history for pagination, but only reloads a small recent window into model context.
This split is deliberate. History and model context are not the same thing.
5. Streaming Response Shape
The route streams two kinds of output back to the client:- The AI SDK UI message stream from the agent itself.
- Additional deterministic
data-specparts built after tool execution.
execute block in /api/chat merges:
result.toUIMessageStream(...)buildAssistantResponseDataParts({ messages, toolResults })
- plain text
- reasoning parts
- tool input/output parts
data-specparts for structured UI
6. Frontend Rendering and Refresh
The chat page renders each assistant message by splitting it into:- visible markdown text
- thought/reasoning traces
- tool activity cards
- structured UI via
AssistantStructuredUi
- transactions
- dashboard
- assets
- active AI suggestions
Model Provider Configuration
Model setup lives inapps/web/lib/platform-ai/service.ts.
Configuration Sources
Provider Modes
getConfiguredPlatformAiModel() resolves the effective settings and returns:
settingsmodel
AI_PROVIDER_REQUIRED and the client opens the Google provider modal.
Tracing
When PostHog is available, the model is wrapped withwithTracing(...).
This adds deterministic observability around:
- user id
- workflow name
- model execution
Workflow Selection And Prompting
Workflow routing lives inapps/web/lib/assistant/transaction-agent.ts.
Deterministic Workflow Detection
Before the agent runs, the code derives a workflow signal from recent user messages. Important deterministic steps:getUserMessageText()extracts plain user text from UI message parts.getWorkflowSignalText()combines terse replies likecashoryeswith prior user context when needed.pickWorkflow()uses regex heuristics to classify the request.
This routing is algorithmic, not model-inferred.
Prompt Layer
After workflow selection, the agent builds a long instruction prompt that includes:- assistant persona and tone
- automatic defaults for date, currency, and transaction direction
- rules about read-only synced data
- rules about using UI-first tools
- workflow-specific guidance
- provider/model metadata
prepareStep() recalculates the workflow and narrows activeTools for that step.
That means the assistant runs inside a bounded environment:
- the model chooses whether to call a tool
- deterministic code decides which tools are even available on that step
Iteration Limit
The agent is created with:ToolLoopAgentstopWhen: stepCountIs(6)
Tool Integration
All tools are declared increateTransactionAssistant(). Every tool has:
- a plain-language description
- a Zod input schema
- a deterministic
execute()function
Read Tools
Mutation Tools
UI-Only Tools
These tools do not directly mutate product data. They return descriptors that later become deterministic JSON Render specs.What The Model Decides vs What Code Decides
Structured UI Pipeline
The assistant does not render custom HTML directly from the model. It renders deterministic JSON UI specs.How Specs Are Built
buildAssistantResponseDataParts() in apps/web/lib/assistant/response-parts.ts inspects:
- the current message list
- flattened tool results
- welcome suggestions
- asset suggestion cards
- quick create cards
- quick forms
- smart actions
- asset selectors
- generic tool result summaries
How Specs Are Rendered
AssistantStructuredUi in apps/web/components/assistant-structured-ui.tsx uses:
@json-render/react- a local component registry
UiSmartActions, UiQuickForm, and UiAssetSelector to React components.
How UI Turns Back Into Assistant Input
Structured UI components dispatch browser events such as:assistant:action-selectedassistant:existing-asset-selectedassistant:form-submitassistant:quick-create-selected
- Goal forms include hidden fields like
__intent=create_goal. buildAssistantFormPrompt()converts that form submission into a deterministic prompt:Create a financial goal using this form data JSON exactly: ...
prepareStep()instructs the assistant to treat that JSON as authoritative and callcreateGoaldirectly when enough data exists.
Why This Matters
The current assistant is a hybrid system:- the LLM selects actions and writes conversational text
- deterministic code owns the UI contract and event interpretation
Persistence And Admin Operations
Core Tables
Chat Persistence Rules
upsertChatMessage() stores:
- message id
- thread id
- user id
- role
- full UI message payload
updatedAt is also refreshed on each message write.
History Pagination
GET /api/chat uses cursor-based pagination implemented in listChatHistoryPage().
Important behavior:
- a user can request older persisted messages
- the API returns
hasMore,prevCursor, andcontextLimit - the frontend prepends older messages into the visible thread
- older messages do not automatically expand the LLM context window
Admin-Triggered Replies
Admin reply generation uses:- route:
apps/web/app/api/admin/chat/threads/[threadId]/reply/route.ts - helper:
generateAssistantMessage()inapps/web/lib/chat/assistant.ts
- Admin supplies an internal prompt.
- The route loads the target user’s recent context.
- The route creates a synthetic ephemeral user message for the model call only.
- The assistant reply is generated.
- The assistant reply is marked with
metadata.adminInitiated = true. - Only the assistant reply is persisted into the thread.
Plans Integration Boundary
The current system has two different planning-related data models.financial_goals
This table represents long-lived user goals such as:
- house purchase
- retirement
- savings
- education
listGoalscreateGoalshowGoalPlanningForm
monthly_plans
This table represents month-scoped spending/allocation plans such as:
- expected income
- fixed expenses
- essential spending
- goal contribution
- safe-to-spend
- warning state
- reminder schedule
plan-service.ts and /api/v1/plans.
The chat assistant does not currently expose direct monthly-plan mutation tools.
Current Boundary Summary
How Chat Still Connects To Plans
Even without direct monthly-plan tools, the assistant still intersects with planning in three ways:- It can create a
financial_goalsrecord that later becomes selectable as a plan’sprimaryGoalId. - It reads budgets, goals, and active suggestions to coach the user about next steps.
- It can route users into plan-related surfaces through prompts and CTAs such as:
/plans/create/plans/[id]/chat?prompt=...
Suggestion Layer Interaction
The suggestion system inapps/web/lib/ai-suggestions/service.ts is deterministic, not LLM-generated.
It builds rule-based suggestion candidates such as:
spending_watchbudget_guardrailgoal_nudgenext_best_action
listSmartSuggestions, and the chat page also uses them for starter prompts.
After in-chat goal creation, triggerEventDrivenSuggestionRefresh() runs as a guarded follow-up so suggestion refresh failure does not block goal creation.
Deterministic Plan Engine
Monthly plan logic lives primarily in:apps/web/lib/plan-service.tsapps/web/lib/plan-math.ts/api/v1/plans/*
Core Plan Math
Plan API Surface
Cron And Admin Triggers
The plans system also has explicit lifecycle entrypoints:
The daily plan jobs include:
- finalizing stale active plans
- sending due plan reminders
- sending month-end notifications
LLM vs Algorithm Matrix
Why createGoal Is Hybrid
Goal creation is the clearest hybrid path in the current system:
- deterministic code renders the goal planning form
- deterministic code turns the form into a strict JSON prompt
- the LLM decides to call
createGoal - deterministic service code writes the actual record
Key Source Files
Replication Guide
Minimum Setup
To reproduce the current assistant behavior locally, you need:- The web app running.
- A valid authenticated user session.
- A valid Google AI Studio key saved through the Google AI provider modal.
- At least one test user with data for transactions, assets, budgets, goals, or suggestions.
Replicate A Basic Chat Round Trip
- Open
/chat. - Start a new thread or reuse the existing local thread id.
- Send a simple inspection prompt such as
Show my accounts. - Watch the flow:
- frontend submits
messages + threadId /api/chatpersists the latest user message- recent thread context is loaded
- the agent runs
- tool results are converted into a deterministic UI spec
- the assistant reply is stored on finish
- frontend submits
Replicate A Transaction Flow
Use a prompt such asKFC 150k.
Expected current behavior:
- Workflow routing detects a spending/create intent.
- The model typically calls
listAssets. - If assets exist, the response-parts layer may emit an asset selector.
- Clicking an asset emits a browser event.
- The chat page converts that event back into a follow-up prompt containing
asset_id. - The model can then call
createManualTransaction.
Replicate In-Chat Goal Creation
Use a prompt such asI want to buy a house in 10 years.
Expected current behavior:
- Workflow routing classifies the prompt as
coach. - The model is guided toward
showGoalPlanningForm. - The frontend renders a structured form.
- Submitting the form generates a deterministic JSON prompt.
- The model is instructed to treat that JSON as authoritative and call
createGoal. - The goal record is created.
- Event-driven suggestion refresh runs as a follow-up.
Replicate The Plans System Separately
The monthly plans system should be tested through the plans UI or/api/v1/plans, not through assistant tools.
Recommended manual path:
- Create a goal first if you want a linked
primaryGoalId. - Create a draft monthly plan.
- Activate it.
- Patch spending snapshots to trigger warnings.
- Call rebalance.
- Run cron/admin plan jobs for reminders and month-end behavior.
How To Trace Problems Quickly
Current Limitations
The most important limitations to keep in mind are:- The assistant has no direct monthly-plan mutation tool.
monthly_plansbehavior is currently deterministic service/API logic, not model-generated planning logic.- Suggestion CTAs can route users into chat or plan surfaces, but that is separate from the assistant’s direct write capabilities.
- The assistant context window is intentionally small even though the product stores the full chat transcript.
- The assistant is currently focused on the web app. It is not the same system as the older Flutter local-first architecture documented elsewhere.

