Skip to main content

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
This page describes the code that currently powers:
  • 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
This page does not describe the target-state product vision from the plans BRD. It describes the implementation that exists now.

What The Assistant Is Today

At a high level, the assistant is a Next.js + AI SDK + ToolLoopAgent system with four major responsibilities:
  1. Persist user and assistant messages into chat_threads and chat_messages.
  2. Send the latest chat context into a configured model provider.
  3. Let the model call a bounded set of typed tools.
  4. Convert tool outputs into deterministic structured UI specs that the frontend renders and turns back into follow-up chat messages.

Primary Surfaces

SurfacePurposeCurrent capability
/chatMain assistant UI for end usersInspect synced data, mutate manual transactions/assets, create goals, explain suggestions
/api/chatAssistant APIPersist messages, load context, run the agent, stream output
/api/chat/threadThread bootstrapCreate a new chat thread
Admin chat managerSupport/operator surfaceReview any thread and trigger an assistant reply without persisting an admin-visible user prompt

Current Responsibility Split

AreaCurrent assistant responsibilityNot handled by the assistant directly
TransactionsRead synced data, create/update/delete manual transactionsMutating synced SePay or Plaid transactions
AssetsRead synced/manual assets, create/update/delete manual assetsMutating synced linked assets
GoalsRead goals, create a new financial goal in chatAdvanced goal planning math
Monthly plansExplain related goals, suggestions, and next steps; route users into plan surfacesCreate, activate, update, dismiss, or rebalance monthly_plans through assistant tools
SuggestionsRead active suggestions and explain themGenerate suggestions through the chat model itself

System Map

End-To-End Lifecycle

1. Thread Creation

The frontend creates a thread by calling POST /api/chat/thread.
  • Route: apps/web/app/api/chat/thread/route.ts
  • Service: createChatThread() in apps/web/lib/chat/service.ts
  • Persistence target: chat_threads
This step is fully deterministic. No model call happens here.

2. Frontend Message Submission

The main chat page lives in apps/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:
FieldSourceWhy it matters
idAI SDK transportPreserves the chat session/message contract
messagesAI SDK transportCarries the UI message list
triggerAI SDK transportPreserves the SDK trigger context
messageIdAI SDK transportPreserves message-level tracking when present
threadIdZeus custom fieldTells the backend which persisted thread to use
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:
  1. Authenticates the API user.
  2. Validates that messages and threadId exist.
  3. Confirms the thread belongs to the user.
  4. Finds the latest user message from the submitted message list.
  5. Persists that latest user message with upsertChatMessage().
  6. Loads recent persisted context with listRecentChatContext().
  7. Calls createTransactionAssistant(user, contextMessages).
  8. Streams the assistant response back to the client.
  9. 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.
ConcernCurrent implementation
Full persisted historychat_messages stores the whole thread
History paginationGET /api/chat returns pages using cursors
Model context sizegetAssistantContextLimit() returns 10
Messages actually sent back to the modelThe most recent 10 persisted UI messages for the thread
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:
  1. The AI SDK UI message stream from the agent itself.
  2. Additional deterministic data-spec parts built after tool execution.
The execute block in /api/chat merges:
  • result.toUIMessageStream(...)
  • buildAssistantResponseDataParts({ messages, toolResults })
The result is a single assistant message that can contain:
  • plain text
  • reasoning parts
  • tool input/output parts
  • data-spec parts 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
When the assistant finishes, the page invalidates cached queries for:
  • transactions
  • dashboard
  • assets
  • active AI suggestions
That refresh is deterministic frontend behavior. The model does not decide which caches to refresh.

Model Provider Configuration

Model setup lives in apps/web/lib/platform-ai/service.ts.

Configuration Sources

SourcePurpose
platform_ai_settings tableStores provider, base URL, default model, headers, query params, API key
GOOGLE_GENERATIVE_AI_API_KEYOptional environment fallback for Google provider

Provider Modes

Provider modeImplementation
GooglecreateGoogleGenerativeAI()
OpenAI-compatiblecreateOpenAICompatible()
getConfiguredPlatformAiModel() resolves the effective settings and returns:
  • settings
  • model
The assistant does not hardcode a single model name in the agent. It asks the configured provider service for the default model at runtime.

Tracing

When PostHog is available, the model is wrapped with withTracing(...). This adds deterministic observability around:
  • user id
  • workflow name
  • model execution
The model still generates the content, but the tracing wrapper and workflow labeling are deterministic application concerns.

Workflow Selection And Prompting

Workflow routing lives in apps/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 like cash or yes with prior user context when needed.
  • pickWorkflow() uses regex heuristics to classify the request.
Current workflows include:
WorkflowTrigger shapeActive tool subset
coachBudgets, goals, suggestions, house/retirement/planning intentsRead tools + goal planning UI + createGoal
bundle-createSpending + amount hintsAsset listing/creation + transaction creation helpers
asset-create / asset-update / asset-deleteAsset-specific CRUD intentsManual asset tools
create / update / deleteManual transaction CRUD intentsManual transaction tools
inspectGeneric read/query flowsRead-only inspection tools
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
Then 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:
  • ToolLoopAgent
  • stopWhen: stepCountIs(6)
That six-step ceiling is deterministic guardrail logic.

Tool Integration

All tools are declared in createTransactionAssistant(). Every tool has:
  • a plain-language description
  • a Zod input schema
  • a deterministic execute() function
The model can decide when to use a tool and which valid inputs to propose. It cannot bypass the schema or the service-layer rules.

Read Tools

ToolBacking serviceCurrent purposeDeterministic enforcement
listTransactionslistUserTransactionsRead latest synced/manual transactionsLimits, source filtering, returned fields
listLinkedAccountslistUserBankAccountsRead linked bank accountsRead-only output mapping
listAssetslistUserAssetsRead synced + manual assetsSource filtering and normalized output
listBudgetslistBudgetsRead budgets for coachingNumeric parsing and normalized response
listGoalslistGoalsRead goals/plans for coachingNumeric parsing and normalized response
listSmartSuggestionslistUserSuggestionsRead active AI suggestionsStatus/limit filtering

Mutation Tools

ToolBacking serviceCurrent write scopeDeterministic enforcement
createGoalcreateGoalCreate a financial_goals recordZod schema, decimal normalization, deadline normalization
createManualTransactioncreateManualTransactionCreate manual ledger entriesInput schema, service-layer validation, asset id handoff
updateManualTransactionupdateManualTransactionUpdate manual transactions onlyFails on synced transactions
deleteManualTransactiondeleteManualTransactionDelete manual transactions onlyFails on synced transactions
createManualAssetcreateManualAssetCreate self-managed assetsKind enum, default currency, service validation
updateManualAssetupdateManualAssetUpdate self-managed assets onlyFails on synced assets
deleteManualAssetdeleteManualAssetDelete self-managed assets onlyFails on synced assets

UI-Only Tools

These tools do not directly mutate product data. They return descriptors that later become deterministic JSON Render specs.
ToolPurposeOutput style
suggestAssetCreationOffer first asset choices when no assets existAsset suggestion options
showWelcomeSuggestionsShow starter promptsWelcome suggestions
showQuickFormCollect multiple fields at onceGeneric quick form
showGoalPlanningFormCollect goal creation fields inside chatGoal-specific quick form
showQuickCreateSuggestionOffer one-tap asset creation choicesQuick-create cards
showSmartActionsOffer tappable follow-up buttonsSmart action buttons

What The Model Decides vs What Code Decides

DecisionOwner
Whether to answer in text only or call a toolLLM
Which tool to call from the allowed setLLM
Whether the tool input matches the schemaDeterministic validation
What the tool actually does in the database/servicesDeterministic code
How tool results become structured UIDeterministic 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
It then chooses a deterministic UI spec such as:
  • welcome suggestions
  • asset suggestion cards
  • quick create cards
  • quick forms
  • smart actions
  • asset selectors
  • generic tool result summaries
This step is pure application logic. The model does not author the rendered component tree.

How Specs Are Rendered

AssistantStructuredUi in apps/web/components/assistant-structured-ui.tsx uses:
  • @json-render/react
  • a local component registry
The registry maps spec element types like UiSmartActions, UiQuickForm, and UiAssetSelector to React components.

How UI Turns Back Into Assistant Input

Structured UI components dispatch browser events such as:
  • assistant:action-selected
  • assistant:existing-asset-selected
  • assistant:form-submit
  • assistant:quick-create-selected
The main chat page listens for those events and converts them back into chat prompts. Important example:
  • 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 call createGoal directly 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
That makes the UI reproducible and easier to debug than free-form model-rendered HTML.

Persistence And Admin Operations

Core Tables

TablePurpose
chat_threadsGroups messages into a user-owned thread
chat_messagesStores each UI message as JSONB
platform_ai_settingsStores runtime model-provider configuration

Chat Persistence Rules

upsertChatMessage() stores:
  • message id
  • thread id
  • user id
  • role
  • full UI message payload
Thread 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, and contextLimit
  • 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() in apps/web/lib/chat/assistant.ts
Current behavior:
  1. Admin supplies an internal prompt.
  2. The route loads the target user’s recent context.
  3. The route creates a synthetic ephemeral user message for the model call only.
  4. The assistant reply is generated.
  5. The assistant reply is marked with metadata.adminInitiated = true.
  6. Only the assistant reply is persisted into the thread.
The admin prompt itself is not stored as a visible user message.

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
The assistant can directly interact with this subsystem today:
  • listGoals
  • createGoal
  • showGoalPlanningForm

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
This subsystem is managed through plan-service.ts and /api/v1/plans. The chat assistant does not currently expose direct monthly-plan mutation tools.

Current Boundary Summary

CapabilityCurrent owner
Create a long-lived goal in chatAssistant tools
Read budgets/goals/suggestions for coachingAssistant tools
Create or update monthly_plansPlans API + plans UI
Activate a draft monthly planPlans API + plans UI
Rebalance a monthly planPlans API + plans UI
Dismiss monthly warningsPlans API + plans UI
Daily reminder and month-end jobsPlans 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:
  1. It can create a financial_goals record that later becomes selectable as a plan’s primaryGoalId.
  2. It reads budgets, goals, and active suggestions to coach the user about next steps.
  3. 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 in apps/web/lib/ai-suggestions/service.ts is deterministic, not LLM-generated. It builds rule-based suggestion candidates such as:
  • spending_watch
  • budget_guardrail
  • goal_nudge
  • next_best_action
The assistant consumes these suggestions through 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.ts
  • apps/web/lib/plan-math.ts
  • /api/v1/plans/*
This subsystem is separate from the LLM assistant.

Core Plan Math

FunctionPurposeLLM involvement
computeAllocationsSplit income into essential spending, goal contribution, and safe-to-spendNone
computeWarningStateDetect spend pace depletion and goal pace driftNone
computeDailyGuideCompute per-day safe-to-spendNone
computeRebalanceRecommendationRecommend tightening spending or boosting goal contributionNone
evaluateReminderScheduleDecide whether a daily reminder is due in the user’s timezoneNone
buildMonthEndSummaryGenerate deterministic planned-vs-actual summary and next suggestionNone

Plan API Surface

RouteCurrent purpose
GET /api/v1/plansList the user’s monthly plans
POST /api/v1/plansCreate 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]/activateActivate a draft plan
POST /api/v1/plans/[id]/rebalanceApply deterministic rebalance logic
POST /api/v1/plans/[id]/dismiss-warningDismiss the current warning

Cron And Admin Triggers

The plans system also has explicit lifecycle entrypoints:
Entry pointPurpose
POST /api/cron/plansRun daily plan jobs behind a cron secret
POST /api/admin/plans/triggerAllow admins to run plan jobs or accept rebalance for one user
The daily plan jobs include:
  • finalizing stale active plans
  • sending due plan reminders
  • sending month-end notifications
All of that is deterministic service logic. The assistant model is not used there.

LLM vs Algorithm Matrix

BehaviorOwner
Chat thread creationDeterministic
Chat history paginationDeterministic
Deciding which workflow a prompt belongs toDeterministic
Writing the assistant’s conversational textLLM
Deciding whether to call a toolLLM
Restricting which tools are available on a stepDeterministic
Validating tool inputsDeterministic
Executing tool side effectsDeterministic
Turning tool outputs into JSON Render specsDeterministic
Rendering quick forms, buttons, and pickersDeterministic
Turning structured UI clicks into follow-up promptsDeterministic
Choosing starter prompts from active suggestionsDeterministic
Creating a financial_goals record from a completed in-chat formHybrid
Creating a monthly_plans recordDeterministic
Monthly plan allocation mathDeterministic
Warning detection and rebalance recommendations for plansDeterministic
Daily reminder schedulingDeterministic
Suggestion generation in ai-suggestions/service.tsDeterministic

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

ResponsibilityMain files
Chat page and transportapps/web/components/assistant-chat-page-client.tsx
Chat APIapps/web/app/api/chat/route.ts
Thread creationapps/web/app/api/chat/thread/route.ts
Agent, workflows, and toolsapps/web/lib/assistant/transaction-agent.ts
Structured UI spec generationapps/web/lib/assistant/response-parts.ts
JSON UI buildersapps/web/lib/assistant/chat-ui.ts
Structured UI rendererapps/web/components/assistant-structured-ui.tsx
Form-to-prompt translationapps/web/lib/assistant/form-submissions.ts
Chat persistenceapps/web/lib/chat/service.ts
Admin-triggered repliesapps/web/lib/chat/assistant.ts
Provider configurationapps/web/lib/platform-ai/service.ts
Suggestionsapps/web/lib/ai-suggestions/service.ts
Plans API/service/mathapps/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:
  1. The web app running.
  2. A valid authenticated user session.
  3. Platform AI enabled through platform_ai_settings or GOOGLE_GENERATIVE_AI_API_KEY.
  4. At least one test user with data for transactions, assets, budgets, goals, or suggestions.

Replicate A Basic Chat Round Trip

  1. Open /chat.
  2. Start a new thread or reuse the existing local thread id.
  3. Send a simple inspection prompt such as Show my accounts.
  4. Watch the flow:
    • frontend submits messages + threadId
    • /api/chat persists 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

Replicate A Transaction Flow

Use a prompt such as KFC 150k. Expected current behavior:
  1. Workflow routing detects a spending/create intent.
  2. The model typically calls listAssets.
  3. If assets exist, the response-parts layer may emit an asset selector.
  4. Clicking an asset emits a browser event.
  5. The chat page converts that event back into a follow-up prompt containing asset_id.
  6. The model can then call createManualTransaction.

Replicate In-Chat Goal Creation

Use a prompt such as I want to buy a house in 10 years. Expected current behavior:
  1. Workflow routing classifies the prompt as coach.
  2. The model is guided toward showGoalPlanningForm.
  3. The frontend renders a structured form.
  4. Submitting the form generates a deterministic JSON prompt.
  5. The model is instructed to treat that JSON as authoritative and call createGoal.
  6. The goal record is created.
  7. 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:
  1. Create a goal first if you want a linked primaryGoalId.
  2. Create a draft monthly plan.
  3. Activate it.
  4. Patch spending snapshots to trigger warnings.
  5. Call rebalance.
  6. Run cron/admin plan jobs for reminders and month-end behavior.

How To Trace Problems Quickly

SymptomFirst place to inspect
No assistant reply/api/chat and provider configuration
Wrong tool behaviortransaction-agent.ts tool schema + execute function
Wrong structured UIresponse-parts.ts
Button/form click does nothingassistant-structured-ui.tsx and chat page event listeners
Missing persisted messageschat/service.ts and chat_messages
Goal creation works but plans do not updateai-suggestions/events.ts, plan-service.ts, and /api/v1/plans
Monthly plan issueplan-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_plans behavior 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.