Skip to content

AI Advisor Request Lifecycle

This page follows one question through every layer: the chat panel, the transport, the Hono route, the use case, the Mastra adapter, the agents and tools, the guardrails, and finally the audit row.


sequenceDiagram
    autonumber
    actor User
    participant UI as AdvisorPanel and useAdvisorChat
    participant RA as RestAdvisorAdapter
    participant R as routes/advisor.ts
    participant UC as AskAdvisorUC
    participant A as MastraAdvisorAdapter
    participant S as advisor (supervisor)
    participant T as taxAnalyst
    participant U as Use case behind a tool
    participant L as SqliteAdvisorRunLogAdapter

    User->>UI: type a question, Enter
    UI->>RA: ask({ message, threadId? }, signal)
    RA->>R: POST /api/advisor/stream
    R->>R: zValidator(askAdvisorRequestSchema)
    R->>UC: execute({ message, threadId })
    UC->>UC: read language and base_currency settings, resolve threadId
    UC->>A: ask({ message, threadId, locale, baseCurrency })
    A->>A: read execution profiles, resolve credentialed chain
    alt no usable chain entry
        A-->>UC: failed (NO_MODEL_AVAILABLE or VAULT_LOCKED)
    else chain resolved
        A->>A: derive execution profile, build tool configs, agents and model config
        A->>S: stream(message, requestContext, maxSteps, memory)
        S->>T: delegate (agents map)
        T->>U: tool call, strict input
        U-->>T: use case result, budget-gated
        T-->>S: answer with verbatim figures
        S-->>A: text-delta, tool-*, step-finish, finish or tripwire or error chunks
        A-->>UC: AdvisorEvent stream
    end
    UC-->>R: events (token, tool-start, tool-result, tool-error, then one terminal)
    R->>R: toWireEvent, advisorStreamEventSchema.parse
    R-->>RA: SSE frames
    RA-->>UI: validated AdvisorStreamEvent
    UC->>L: appendRun(receipt, outcome) after the terminal event
    UI-->>User: streamed Markdown, tool activity, badges, footers

  1. UI send. AdvisorPanel.vue trims the draft and calls useAdvisorChat.send. A send while a run is in flight is ignored. The request carries threadId only after a previous terminal frame supplied one.
  2. Transport. RestAdvisorAdapter.ask issues a plain fetch POST to /api/advisor/stream and parses the SSE body by hand. Every frame is validated against the shared advisorStreamEventSchema; an invalid frame becomes a local invalid-frame transport error.
  3. Route. createAdvisorApi validates the body (message 1 to 4000 characters, optional UUID threadId; otherwise HTTP 400). It opens an SSE stream, starts a 15-second : keep-alive comment timer, and iterates AskAdvisorUC.execute.
  4. Use case. AskAdvisorUC reads language (default en) and base_currency (default USD) through IUserSettingsPort, allocates a threadId if absent, and delegates the run to IAdvisorPort. It persists the audit row when a terminal event passes through, or from finally as an aborted row if the consumer stops early.
  5. Adapter, chain resolution. MastraAdvisorAdapter reads the execution-profile setting, then resolveChainForRequest: read ai_advisor_model_chain, drop entries whose credential is unavailable, decrypt keys, derive the run profile. An empty result short-circuits to a failed event with no model call.
  6. Adapter, agent graph. A fresh taxAnalyst and advisor pair is built per request, because the resolved chain, budgets, and RunBudgetTracker are per-request facts. Memory is the only long-lived object (constructed once against ai-advisor.db).
  7. Agent run. advisor.stream(...) is called with maxSteps from the profile, lastMessages history, semanticRecall: false, working memory off, and a request context carrying locale and baseCurrency (never model inputs).
  8. Tools. advisor delegates to taxAnalyst, which calls read-only tools. Each tool validates strict input, calls its use case, projects a narrow payload, and passes it through enforceBudget.
  9. Chunk mapping. applyChunk (mastraChunk.ts) converts Mastra stream chunks into domain AdvisorEvents and updates the receipt draft. Unrecognised chunk types are dropped silently. Delegation calls (agent-taxAnalyst) are not catalogue tools and are not surfaced as tool events.
  10. Guardrails. Two output processors inspect the supervisor output at the finish chunk. A trip becomes a refused event; a run that touched investment content marks the finish payload so the terminal frame carries disclaimer: true.
  11. Wire mapping. The route calls toWireEvent (the single domain-to-wire conversion) and re-validates the result before writing each frame. completed becomes done.
  12. Audit. The route deliberately keeps iterating after the terminal frame so the use case can reach appendRun. One row is written per run.
  13. Cancellation. If the client closes the connection, stream.onAbort calls iterator.return(); the adapter’s finally aborts the provider call and the use case writes an aborted row.

[!NOTE] The non-streaming POST /api/advisor/ask runs the same use case, concatenates token events, and returns one JSON answer (see API & Streaming). The UI uses the streaming route only.


Where it fails Result
Invalid request body HTTP 400 from zValidator; no run starts.
No usable chain entry failed with NO_MODEL_AVAILABLE or VAULT_LOCKED; no model call, pre-run receipt.
Every chain entry fails after retries failed with ALL_PROVIDERS_FAILED and a classified cause.
A tool rejects its input or its use case throws tool-error frame (INVALID_TOOL_INPUT or USE_CASE_FAILED); the run may still complete.
Guardrail trip refused with the processorId.
Client disconnects or presses Stop aborted audit row; no terminal frame is needed.
Unexpected server error failed with INTERNAL_ERROR.

For the provider-side details see Providers & Models; for the frame shapes see API & Streaming.