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
| Surface | Purpose | Current capability |
|---|---|---|
/chat | Main assistant UI for end users | Inspect synced data, mutate manual transactions/assets, create goals, explain suggestions |
/api/chat | Assistant API | Persist messages, load context, run the agent, stream output |
/api/chat/thread | Thread bootstrap | Create a new chat thread |
| Admin chat manager | Support/operator surface | Review any thread and trigger an assistant reply without persisting an admin-visible user prompt |
Current Responsibility Split
| Area | Current assistant responsibility | Not handled by the assistant directly |
|---|---|---|
| Transactions | Read synced data, create/update/delete manual transactions | Mutating synced SePay or Plaid transactions |
| Assets | Read synced/manual assets, create/update/delete manual assets | Mutating synced linked assets |
| Goals | Read goals, create a new financial goal in chat | Advanced goal planning math |
| Monthly plans | Explain related goals, suggestions, and next steps; route users into plan surfaces | Create, activate, update, dismiss, or rebalance monthly_plans through assistant tools |
| Suggestions | Read active suggestions and explain them | Generate suggestions through the chat model itself |
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:
| Field | Source | Why it matters |
|---|---|---|
id | AI SDK transport | Preserves the chat session/message contract |
messages | AI SDK transport | Carries the UI message list |
trigger | AI SDK transport | Preserves the SDK trigger context |
messageId | AI SDK transport | Preserves message-level tracking when present |
threadId | Zeus custom field | Tells the backend which persisted thread to use |
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.| Concern | Current implementation |
|---|---|
| Full persisted history | chat_messages stores the whole thread |
| History pagination | GET /api/chat returns pages using cursors |
| Model context size | getAssistantContextLimit() returns 10 |
| Messages actually sent back to the model | The most recent 10 persisted UI messages for the thread |
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
| Source | Purpose |
|---|---|
platform_ai_settings table | Stores provider, base URL, default model, headers, query params, API key |
GOOGLE_GENERATIVE_AI_API_KEY | Optional environment fallback for Google provider |
Provider Modes
| Provider mode | Implementation |
|---|---|
createGoogleGenerativeAI() | |
| OpenAI-compatible | createOpenAICompatible() |
getConfiguredPlatformAiModel() resolves the effective settings and returns:
settingsmodel
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.
| Workflow | Trigger shape | Active tool subset |
|---|---|---|
coach | Budgets, goals, suggestions, house/retirement/planning intents | Read tools + goal planning UI + createGoal |
bundle-create | Spending + amount hints | Asset listing/creation + transaction creation helpers |
asset-create / asset-update / asset-delete | Asset-specific CRUD intents | Manual asset tools |
create / update / delete | Manual transaction CRUD intents | Manual transaction tools |
inspect | Generic read/query flows | Read-only inspection tools |
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
| Tool | Backing service | Current purpose | Deterministic enforcement |
|---|---|---|---|
listTransactions | listUserTransactions | Read latest synced/manual transactions | Limits, source filtering, returned fields |
listLinkedAccounts | listUserBankAccounts | Read linked bank accounts | Read-only output mapping |
listAssets | listUserAssets | Read synced + manual assets | Source filtering and normalized output |
listBudgets | listBudgets | Read budgets for coaching | Numeric parsing and normalized response |
listGoals | listGoals | Read goals/plans for coaching | Numeric parsing and normalized response |
listSmartSuggestions | listUserSuggestions | Read active AI suggestions | Status/limit filtering |
Mutation Tools
| Tool | Backing service | Current write scope | Deterministic enforcement |
|---|---|---|---|
createGoal | createGoal | Create a financial_goals record | Zod schema, decimal normalization, deadline normalization |
createManualTransaction | createManualTransaction | Create manual ledger entries | Input schema, service-layer validation, asset id handoff |
updateManualTransaction | updateManualTransaction | Update manual transactions only | Fails on synced transactions |
deleteManualTransaction | deleteManualTransaction | Delete manual transactions only | Fails on synced transactions |
createManualAsset | createManualAsset | Create self-managed assets | Kind enum, default currency, service validation |
updateManualAsset | updateManualAsset | Update self-managed assets only | Fails on synced assets |
deleteManualAsset | deleteManualAsset | Delete self-managed assets only | Fails on synced assets |
UI-Only Tools
These tools do not directly mutate product data. They return descriptors that later become deterministic JSON Render specs.| Tool | Purpose | Output style |
|---|---|---|
suggestAssetCreation | Offer first asset choices when no assets exist | Asset suggestion options |
showWelcomeSuggestions | Show starter prompts | Welcome suggestions |
showQuickForm | Collect multiple fields at once | Generic quick form |
showGoalPlanningForm | Collect goal creation fields inside chat | Goal-specific quick form |
showQuickCreateSuggestion | Offer one-tap asset creation choices | Quick-create cards |
showSmartActions | Offer tappable follow-up buttons | Smart action buttons |
What The Model Decides vs What Code Decides
| Decision | Owner |
|---|---|
| Whether to answer in text only or call a tool | LLM |
| Which tool to call from the allowed set | LLM |
| Whether the tool input matches the schema | Deterministic validation |
| What the tool actually does in the database/services | Deterministic code |
| How tool results become structured UI | Deterministic code |
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
| Table | Purpose |
|---|---|
chat_threads | Groups messages into a user-owned thread |
chat_messages | Stores each UI message as JSONB |
platform_ai_settings | Stores runtime model-provider configuration |
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
| Capability | Current owner |
|---|---|
| Create a long-lived goal in chat | Assistant tools |
| Read budgets/goals/suggestions for coaching | Assistant tools |
Create or update monthly_plans | Plans API + plans UI |
| Activate a draft monthly plan | Plans API + plans UI |
| Rebalance a monthly plan | Plans API + plans UI |
| Dismiss monthly warnings | Plans API + plans UI |
| Daily reminder and month-end jobs | Plans service + cron/admin trigger |
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
| Function | Purpose | LLM involvement |
|---|---|---|
computeAllocations | Split income into essential spending, goal contribution, and safe-to-spend | None |
computeWarningState | Detect spend pace depletion and goal pace drift | None |
computeDailyGuide | Compute per-day safe-to-spend | None |
computeRebalanceRecommendation | Recommend tightening spending or boosting goal contribution | None |
evaluateReminderSchedule | Decide whether a daily reminder is due in the user’s timezone | None |
buildMonthEndSummary | Generate deterministic planned-vs-actual summary and next suggestion | None |
Plan API Surface
| Route | Current purpose |
|---|---|
GET /api/v1/plans | List the user’s monthly plans |
POST /api/v1/plans | Create a draft monthly plan |
GET /api/v1/plans/[id] | Fetch one plan |
PATCH /api/v1/plans/[id] | Update allocations or spending snapshot |
DELETE /api/v1/plans/[id] | Delete a draft plan |
POST /api/v1/plans/[id]/activate | Activate a draft plan |
POST /api/v1/plans/[id]/rebalance | Apply deterministic rebalance logic |
POST /api/v1/plans/[id]/dismiss-warning | Dismiss the current warning |
Cron And Admin Triggers
The plans system also has explicit lifecycle entrypoints:| Entry point | Purpose |
|---|---|
POST /api/cron/plans | Run daily plan jobs behind a cron secret |
POST /api/admin/plans/trigger | Allow admins to run plan jobs or accept rebalance for one user |
- finalizing stale active plans
- sending due plan reminders
- sending month-end notifications
LLM vs Algorithm Matrix
| Behavior | Owner |
|---|---|
| Chat thread creation | Deterministic |
| Chat history pagination | Deterministic |
| Deciding which workflow a prompt belongs to | Deterministic |
| Writing the assistant’s conversational text | LLM |
| Deciding whether to call a tool | LLM |
| Restricting which tools are available on a step | Deterministic |
| Validating tool inputs | Deterministic |
| Executing tool side effects | Deterministic |
| Turning tool outputs into JSON Render specs | Deterministic |
| Rendering quick forms, buttons, and pickers | Deterministic |
| Turning structured UI clicks into follow-up prompts | Deterministic |
| Choosing starter prompts from active suggestions | Deterministic |
Creating a financial_goals record from a completed in-chat form | Hybrid |
Creating a monthly_plans record | Deterministic |
| Monthly plan allocation math | Deterministic |
| Warning detection and rebalance recommendations for plans | Deterministic |
| Daily reminder scheduling | Deterministic |
Suggestion generation in ai-suggestions/service.ts | Deterministic |
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
| Responsibility | Main files |
|---|---|
| Chat page and transport | apps/web/components/assistant-chat-page-client.tsx |
| Chat API | apps/web/app/api/chat/route.ts |
| Thread creation | apps/web/app/api/chat/thread/route.ts |
| Agent, workflows, and tools | apps/web/lib/assistant/transaction-agent.ts |
| Structured UI spec generation | apps/web/lib/assistant/response-parts.ts |
| JSON UI builders | apps/web/lib/assistant/chat-ui.ts |
| Structured UI renderer | apps/web/components/assistant-structured-ui.tsx |
| Form-to-prompt translation | apps/web/lib/assistant/form-submissions.ts |
| Chat persistence | apps/web/lib/chat/service.ts |
| Admin-triggered replies | apps/web/lib/chat/assistant.ts |
| Provider configuration | apps/web/lib/platform-ai/service.ts |
| Suggestions | apps/web/lib/ai-suggestions/service.ts |
| Plans API/service/math | apps/web/app/api/v1/plans/*, apps/web/lib/plan-service.ts, apps/web/lib/plan-math.ts |
Replication Guide
Minimum Setup
To reproduce the current assistant behavior locally, you need:- The web app running.
- A valid authenticated user session.
- Platform AI enabled through
platform_ai_settingsorGOOGLE_GENERATIVE_AI_API_KEY. - 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
| Symptom | First place to inspect |
|---|---|
| No assistant reply | /api/chat and provider configuration |
| Wrong tool behavior | transaction-agent.ts tool schema + execute function |
| Wrong structured UI | response-parts.ts |
| Button/form click does nothing | assistant-structured-ui.tsx and chat page event listeners |
| Missing persisted messages | chat/service.ts and chat_messages |
| Goal creation works but plans do not update | ai-suggestions/events.ts, plan-service.ts, and /api/v1/plans |
| Monthly plan issue | plan-service.ts and plan-math.ts, not the assistant model |
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.

