06-ai-tucanwave: W3filled32 citations

TUCAN AI Assistant — Multi-Agent Orchestration Core

Audiences: doctor, developer, internal, investor

TUCAN AI Assistant — Multi-Agent Orchestration Core

TUCAN is the in-process Mastra-based orchestrator that turns a single typed message ("schedule Mrs. Garcia for a follow-up next week") into a planned sequence of clinical tools, FHIR mutations, and live SSE updates — without ever calling a remote agent gateway.

Business Purpose

TUCAN ("toucan" — Dudoxx's bird mascot) is the product that takes Dudoxx from "an EMR with a chat widget" to an EMR a doctor can talk to. A doctor opens a visit, types a one-line instruction, and TUCAN:

  1. Picks the right specialist agent for the request (clinical / scheduling / document / admin / voice operator).
  2. Decides which of the ~233 catalogued tools that agent is allowed to call.
  3. Executes the chosen tool(s) — each of which performs a real FHIR or Prisma mutation through normal NestJS service calls.
  4. Streams the result + a synthesized narrative back to the browser over SSE.

The value prop is not "a chatbot." It is billable clinical work done by typing or speaking instead of clicking through forms: chart capture, order entry, prescription writing, slot booking, document drafting. Every tool call maps to a chargeable clinical event already present in the Prisma + FHIR model, so revenue accounting works the same whether the doctor used the form or used TUCAN.

For Dudoxx as a vendor, TUCAN is also the single integration surface for voice (Voxial / FlowAgent), the messenger (WhatsApp / Telegram), and the patient portal copilot — they all dispatch into the same orchestrator instead of each maintaining its own agent stack.

Audiences

  • Investor: TUCAN is the moat. Competitors ship a generic ChatGPT-style chat box; Dudoxx ships an orchestrator with 233 tools wired to a real EMR's FHIR + Prisma schema, multi-tenant by construction, with per-step cost accounting and a deterministic dispatcher state machine. The same engine serves text, voice, and async channels — one cost basis, three product surfaces.
  • Clinical buyer (doctor / nurse / receptionist): Talk to the system. Ask "what's Mrs. Garcia's last A1c?" — TUCAN reads from FHIR + the clinical notes table and answers. Say "prescribe amoxicillin 500mg TID for 7 days" — TUCAN writes a MedicationRequest and a Prisma Prescription and shows you the printable PDF. No new UI to learn.
  • Developer / partner: TUCAN is not a black box. It is a NestJS module (ddx-api/src/ai/tucan/) with a typed dispatcher pipeline, a tool registry you can register against, an SSE contract you can subscribe to, and a Mastra workflow you can suspend / resume. New tools are added via createTucanTool({...}) and a registry entry — no LLM training required.
  • Internal (ops / support): Per-session telemetry (consumption.service) + per-step cost (pricing.service) + SSE observability metrics (sse-observability.service) give support staff a "what did this turn cost" view per session. See tucan-consumption-pricing and tucan-orchestrator for step-level cost accounting.

Architecture

TUCAN is wired as a NestJS module that exposes one inbound HTTP surface (POST /api/v1/agent/sessions/:id/messages) and one outbound SSE surface (GET /api/v1/sse/session?sessionId=...). The same code is reachable from the voice agent (LiveKit Python worker → tucan-operator) and the messenger consumers — they bypass HTTP and call AgentDispatcherService.processMessage directly inside the same Node process.

Inside the module, the request flows through five logical layers:

HTTP / Voice / Messenger
        ↓
SessionController + SessionService          ← (see [[ai-chat-sessions]])
        ↓
AgentDispatcherService.processMessage()     ← dispatch pipeline (10 stages)
        ↓
DispatcherStage → Mastra agent.generate()   ← per-agent tool catalog
        ↓
Tool functions (FHIR / Prisma / SSE side effects)
        ↓
event-bus.service → SSE → browser

The "10 stages" inside processMessage are not just glue — each is a class with a single run() method, dispatched in order:

  1. resolveTurn — derive agentType and any pendingSuspendedWorkflows from the session's persisted state. Confirms the TUCAN-no-guided rule from project memory: agent dispatch is read from session.agentType, not chosen by the LLM (project_tucan_no_guided_consumers).
  2. abort signal registration — a Stop click in the UI must be able to kill an in-flight turn even if it arrived before the dispatcher reached the first await. See agent-dispatcher.service.ts:120-154.
  3. ContextBuilderService.buildContext — assembles the per-request context from 30+ context adapters (patient, visit, FHIR, scheduling, messenger, …). The God-object problem documented in tucan-context-builder.
  4. TrivialBypassStage — short-circuit greetings / one-token inputs before any LLM call. See dispatch/stages/trivial-bypass.ts.
  5. RewriterStage — convert the raw user prompt into a structured rewrite suitable for intent classification + tool selection. Implemented as a Mastra agent with its own (cheap) model config.
  6. IntentClassifierStage — classify the rewrite into one of the intent categories declared in intent/intent-descriptor.registry.ts.
  7. SuspenderStage — if there is a pending suspended workflow on the session, resume it with the user's new input as resumeData. See chat-orchestrator.service.ts:152-300 for the Mastra workflow.resume bridge.
  8. DispatcherStage — call the actual Mastra agent with the resolved tool catalog; this is where the LLM finally runs.
  9. ReporterStage — narrate the dispatcher's structured outputs into HTML / Markdown for the UI; runs as a separate (cheaper) agent.
  10. Fire-and-forget QuickActions — propose 3-5 follow-up actions for the user, with anti-repetition guard against the previous turn.

Each stage returns a typed TurnTransition (dispatch/transitions.ts:52-93) — ok / suspend / replan / escalate / fail — which is the only way the dispatcher engine moves forward. This discriminated union is the heart of TUCAN's reliability: every branch is exhaustively handled, every suspend / replan / escalate is validated by a Zod v4 schema before it crosses an SSE boundary.

The dispatcher pipeline is described in detail in tucan-dispatcher; the multi-step planner-and-executor agent runtime lives in tucan-orchestrator (layered execution L1 intent-planner → L5 persistency).

Tech Stack & Choices

ComponentChoiceWhy
Agent runtime@mastra/core@1.13.0 (ddx-api) / 1.20.0 (ddx-livekit-flowagent-ts)In-process Agent + Workflow + tool registry; no remote gateway, no LangChain churn.
LLM transportLiteLLM at port 4000 → Dudoxx-hosted dudoxx-gemmaOne bearer + one endpoint for every model. Provider swap is a config flip.
Default modeldudoxx-gemma with enable_thinking:falseInternal CUDA tier; no token cost on the same VPC; deterministic ("thinking-off") for clinical use.
HTTP transportNestJS 11 controllers, fastifyReuses the API platform's auth + RBAC + tenant isolation interceptors — no parallel auth stack.
StreamingSSE (canonical contract: @ddx/sse-contract 4.1.0) + ioredis DB1 fan-out + RxJS SubjectOne channel naming scheme (SSEChannels.session(id)), Redis-backed for horizontal scale, in-memory ring buffer for replay-on-reconnect.
Tool definitioncreateTucanTool({...}) returns Mastra Tool with category + mutating flagTools are typed (Zod v4) and self-describing; the registry filters them per-agent by category, not by prompt-engineered allow-list.
WorkflowMastra createWorkflow().createStep() + suspend() + resume()Clinical flows (intake, follow-up scheduling, lab order bundle) need true HITL pause — not retries.
PersistencePrisma 7 against ddx_api_ai schema (ChatSession, ChatMessage, …)Audit trail + replay; the AI database is separate from the clinical main DB.
Logging / telemetryOpenTelemetry via observability/telemetry.config.tsOne trace ID per turn; spans for each stage.

Alternatives rejected:

  • LangChain / LangGraph — too much node churn, the agent-and-tool DSL is not typed end-to-end, and LangSmith is a separate billing line. Mastra ships an in-process workflow engine with first-class TypeScript types.
  • OpenAI Assistants API — closed registry, latency hop, no on-prem story. Dudoxx is sold to clinics that require EU residency. TUCAN must run inside ddx-api's VPC.
  • Per-agent FastAPI sidecar — would have forced a process boundary between the dispatcher and the tool implementations. Tools mutate Prisma
    • FHIR; a process boundary means doubling all transactional boundaries.

Data Flow

The canonical TUCAN turn ("write a prescription"):

Business outcome: Dr. Huber dictates "Prescribe amoxicillin 500 mg TID for 7 days for Mrs. Garcia, send the script to her pharmacy." The script appears in the patient's chart within ~3 seconds, the PDF is downloadable, and the patient portal pushes a notification.

Technical mechanism:

  1. Voice agent dictates into LiveKit; tucan-operator/services/peer-bridge.service.ts forwards the transcript via AgentDispatcherService.processMessage (ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:110).
  2. resolveTurn reads session.agentType = 'clinical' from the persisted session — not chosen by the LLM (ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:113).
  3. ContextBuilderService.buildContext resolves the active visit + FHIR Patient from session metadata (ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:165).
  4. agentPublisher.publishAgentStart emits SSE agent:start on the session:{sessionId} channel (ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:177).
  5. RewriterStage.run turns the dictated text into a structured rewrite (ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:192).
  6. IntentClassifierStage.publish classifies it as clinical:prescribe (ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:200).
  7. DispatcherStage.run invokes the clinical Mastra agent — bound at construction time to the medications / prescriptions / FHIR tools (see ai-medical-tools) (ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:228).
  8. The LLM calls create-prescription (Mastra tool) → writes Prisma row
    • HAPI FHIR MedicationRequest via FHIR_CLIENT / ResourceFacade<MedicationRequest>.
  9. Per-step SSE events stream: tool:starttool:progresstool:complete (ddx-api/src/ai/tucan/sse/event-bus.service.ts).
  10. ReporterStage.run synthesizes the user-facing reply HTML (ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:276).
  11. agent:complete + quick_actions:update close the turn.
  12. Patient portal subscribes to user:{patientUserId} and sees the prescription appear in real time.

A suspend along the way (e.g. "I need to pick a candidate patient first") encodes the pause in session.metadata.suspendedWorkflows[taskId] and emits workflow:suspended over SSE. The next POST to /api/v1/agent/sessions/:id/messages is routed by ChatOrchestratorService.resume (ddx-api/src/ai/tucan/orchestrator/chat-orchestrator.service.ts:152), which reads the entry back, calls workflow.createRun({ runId }) + run.resume({ step, resumeData }), and the same turn continues.

Implicated Code

  • ddx-api/src/ai/tucan/tucan.module.ts:1 — NestJS module surface; wires every service below into a single TUCAN_MODULE that AppModule imports.
  • ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:88 — class AgentDispatcherService, the single entry point for HTTP / voice / messenger callers.
  • ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:110 — public processMessage(dto: ProcessMessageRequestDto): Promise<DispatchResult>; the 10-stage pipeline.
  • ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:126 — abort-signal handshake with the controller so a Stop click cancels even a fire-and-forget turn.
  • ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:163eventBus.registerSessionUser registers the user on the SSE channel before any event is published (eliminates session-register race).
  • ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:181TrivialBypassStage.tryBypass short-circuit for greetings.
  • ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:192RewriterStage.run → structured rewrite.
  • ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:200IntentClassifierStage.publish.
  • ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:228DispatcherStage.run — the actual Mastra agent invocation.
  • ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:276ReporterStage.run — narration / HTML render.
  • ddx-api/src/ai/tucan/dispatch/transitions.ts:52ok(output) TurnTransition factory.
  • ddx-api/src/ai/tucan/dispatch/transitions.ts:61suspend(formId, payload) factory for HITL pauses.
  • ddx-api/src/ai/tucan/dispatch/transitions.ts:73replan(reason, attemptsRemaining).
  • ddx-api/src/ai/tucan/dispatch/transitions.ts:91fail(error: TucanError) for typed terminal failures.
  • ddx-api/src/ai/tucan/orchestrator/chat-orchestrator.service.ts:83 — class ChatOrchestratorService; will eventually replace AgentDispatcherService per plans/tucan-chat-mode/ (currently delegates to the legacy dispatcher at line 138 to preserve the 7-step Sarah-Garcia integration test).
  • ddx-api/src/ai/tucan/orchestrator/chat-orchestrator.service.ts:152resume(ctx, decision) — Mastra workflow resume bridge.
  • ddx-api/src/ai/tucan/orchestrator/orchestrated-dispatch.workflow.ts:1buildOrchestratedDispatchWorkflow — the Mastra workflow definition used by the orchestrator layers L1-L5.
  • ddx-api/src/ai/tucan/sse/event-bus.service.ts:1 — SSE publisher (SSEChannels.session(id) is the only allowed way to construct a session channel name per the SSE contract channel rules — SSEChannels.session(id)).
  • ddx-api/src/ai/tucan/registry/agent-registry.service.ts:1 — central registry that resolves agentType → Mastra Agent instance with the correct tool catalog bound at construction.

Operational Notes

  • Default model is dudoxx-gemma with enable_thinking:false — every chat/completions POST injects enable_thinking:false. If you see reasoning_tokens > 0 in a consumption.usage_record row, the thinking-off invariant has regressed — file a DEF.
  • No remote agent gateway. Mastra runs in-process; if ddx-api is up, TUCAN is up. There is no separate "TUCAN service" to deploy. The standalone Mastra-engine sandbox (ddx-tucan-engine/) is a development surface only — production traffic does not flow through it (see tucan-engine-standalone).
  • Stop button must work in the gap between 201 response and first await. The controller registers an AbortSignal synchronously before spawning the fire-and-forget dispatch (agent-dispatcher.service.ts:126). Any new HTTP entrypoint MUST preserve this contract or Stop will look broken.
  • Agent model inheritance (feedback_agent_model_inheritance) — never pass an explicit model: to new Agent({ ... }) inside the registry. The registry resolves the model from context.llmConfig at call time so per-tenant model overrides actually take effect.
  • No guided consumers (project_tucan_no_guided_consumers) — agent dispatch is read from session.agentType which is set by the caller (the EmbeddableAssistant resolver hardcodes 'clinical-assistant'; the voice operator sets 'tucan'; the messenger consumer sets the channel- appropriate type). The LLM is never asked "which agent should run". Any attempt to add LLM-chosen routing must be discussed with the Mastra specialist first — the SSE contract assumes deterministic routing.
  • SSE legacy POST bridge MUST stay off in production. The SSE_LEGACY_POST_BRIDGE=false env (verified post-9631dd7d7) gates the pre-v2.0.0 fallback. Re-enabling it breaks the session-register-race guarantees.
  • get_clinical_notes is a hallucination. The real tool is list-clinical-notes. If you see this in a failed dispatch, the prompt for the clinical agent has drifted and needs a regression eval.
  • tucan-dispatcher — the 10-stage pipeline in detail (state machine, not LLM choice).
  • tucan-orchestrator — layered execution L1-L5 inside orchestrator/.
  • tucan-context-builder — 30+ context adapters, God-object pitfall.
  • tucan-agent-types — clinical / scheduling / document / admin / flowagent / operator.
  • ai-medical-tools — the 233-tool catalog and the categories assigned to each agent.
  • tucan-rag-tools — Qdrant-backed tool discovery; static per-agent at construction, no per-turn semantic reduction.
  • ai-chat-sessions — session lifecycle, ChatSession Prisma model, metadata schema.
  • tucan-intent-filtering — intent classification + descriptor registry.
  • tucan-pool — per-tenant agent pool / instance management.
  • tucan-consumption-pricing — per-step telemetry and pricing (reasoning_tokens must be 0 invariant).
  • sse-event-engine (W1) — canonical SSE contract v2.0.0 that TUCAN publishes against.
  • docs/claude-context/CLAUDE_MASTRA_CODEBASE_USAGE.md — Mastra primitives in-depth (do not duplicate, link).
  • docs/claude-context/CLAUDE_TUCAN_API_INJECTION.md — tool-RAG filtering pattern referenced by tucan-rag-tools.