TUCAN AI Assistant — Multi-Agent Orchestration Core
Audiences: doctor, developer, internal, investor
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:
- Picks the right specialist agent for the request (clinical / scheduling / document / admin / voice operator).
- Decides which of the ~233 catalogued tools that agent is allowed to call.
- Executes the chosen tool(s) — each of which performs a real FHIR or Prisma mutation through normal NestJS service calls.
- 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
MedicationRequestand a PrismaPrescriptionand 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 viacreateTucanTool({...})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:
- resolveTurn — derive
agentTypeand anypendingSuspendedWorkflowsfrom the session's persisted state. Confirms the TUCAN-no-guided rule from project memory: agent dispatch is read fromsession.agentType, not chosen by the LLM ([[project_tucan_no_guided_consumers]]). - 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. Seeagent-dispatcher.service.ts:120-154. - 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]].
- TrivialBypassStage — short-circuit greetings / one-token inputs
before any LLM call. See
dispatch/stages/trivial-bypass.ts. - 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.
- IntentClassifierStage — classify the rewrite into one of the intent
categories declared in
intent/intent-descriptor.registry.ts. - SuspenderStage — if there is a pending suspended workflow on the
session, resume it with the user's new input as
resumeData. Seechat-orchestrator.service.ts:152-300for the Mastraworkflow.resumebridge. - DispatcherStage — call the actual Mastra agent with the resolved tool catalog; this is where the LLM finally runs.
- ReporterStage — narrate the dispatcher's structured outputs into HTML / Markdown for the UI; runs as a separate (cheaper) agent.
- 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
| Component | Choice | Why |
|---|---|---|
| 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 transport | LiteLLM at port 4000 → Dudoxx-hosted dudoxx-gemma | One bearer + one endpoint for every model. Provider swap is a config flip. |
| Default model | dudoxx-gemma with enable_thinking:false | Internal CUDA tier; no token cost on the same VPC; deterministic ("thinking-off") for clinical use. |
| HTTP transport | NestJS 11 controllers, fastify | Reuses the API platform's auth + RBAC + tenant isolation interceptors — no parallel auth stack. |
| Streaming | SSE (canonical contract: @ddx/sse-contract 4.1.0) + ioredis DB1 fan-out + RxJS Subject | One channel naming scheme (SSEChannels.session(id)), Redis-backed for horizontal scale, in-memory ring buffer for replay-on-reconnect. |
| Tool definition | createTucanTool({...}) returns Mastra Tool with category + mutating flag | Tools are typed (Zod v4) and self-describing; the registry filters them per-agent by category, not by prompt-engineered allow-list. |
| Workflow | Mastra createWorkflow().createStep() + suspend() + resume() | Clinical flows (intake, follow-up scheduling, lab order bundle) need true HITL pause — not retries. |
| Persistence | Prisma 7 against ddx_api_ai schema (ChatSession, ChatMessage, …) | Audit trail + replay; the AI database is separate from the clinical main DB. |
| Logging / telemetry | OpenTelemetry via observability/telemetry.config.ts | One 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:
- Voice agent dictates into LiveKit;
tucan-operator/services/peer-bridge.service.tsforwards the transcript viaAgentDispatcherService.processMessage(ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:110).resolveTurnreadssession.agentType = 'clinical'from the persisted session — not chosen by the LLM (ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:113).ContextBuilderService.buildContextresolves the active visit + FHIR Patient from session metadata (ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:165).agentPublisher.publishAgentStartemits SSEagent:starton thesession:{sessionId}channel (ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:177).RewriterStage.runturns the dictated text into a structured rewrite (ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:192).IntentClassifierStage.publishclassifies it asclinical:prescribe(ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:200).DispatcherStage.runinvokes 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).- The LLM calls
create-prescription(Mastra tool) → writes Prisma row
- HAPI FHIR
MedicationRequestviaFHIR_CLIENT/ResourceFacade<MedicationRequest>.- Per-step SSE events stream:
tool:start→tool:progress→tool:complete(ddx-api/src/ai/tucan/sse/event-bus.service.ts).ReporterStage.runsynthesizes the user-facing reply HTML (ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:276).agent:complete+quick_actions:updateclose the turn.- 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 singleTUCAN_MODULEthatAppModuleimports.ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:88— classAgentDispatcherService, the single entry point for HTTP / voice / messenger callers.ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:110— publicprocessMessage(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:163—eventBus.registerSessionUserregisters 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:181—TrivialBypassStage.tryBypassshort-circuit for greetings.ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:192—RewriterStage.run→ structured rewrite.ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:200—IntentClassifierStage.publish.ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:228—DispatcherStage.run— the actual Mastra agent invocation.ddx-api/src/ai/tucan/dispatch/agent-dispatcher.service.ts:276—ReporterStage.run— narration / HTML render.ddx-api/src/ai/tucan/dispatch/transitions.ts:52—ok(output)TurnTransition factory.ddx-api/src/ai/tucan/dispatch/transitions.ts:61—suspend(formId, payload)factory for HITL pauses.ddx-api/src/ai/tucan/dispatch/transitions.ts:73—replan(reason, attemptsRemaining).ddx-api/src/ai/tucan/dispatch/transitions.ts:91—fail(error: TucanError)for typed terminal failures.ddx-api/src/ai/tucan/orchestrator/chat-orchestrator.service.ts:83— classChatOrchestratorService; will eventually replaceAgentDispatcherServiceperplans/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:152—resume(ctx, decision)— Mastra workflow resume bridge.ddx-api/src/ai/tucan/orchestrator/orchestrated-dispatch.workflow.ts:1—buildOrchestratedDispatchWorkflow— 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 resolvesagentType→ MastraAgentinstance with the correct tool catalog bound at construction.
Operational Notes
- Default model is
dudoxx-gemmawithenable_thinking:false— everychat/completionsPOST injectsenable_thinking:false. If you seereasoning_tokens > 0in aconsumption.usage_recordrow, the thinking-off invariant has regressed — file a DEF. - No remote agent gateway. Mastra runs in-process; if
ddx-apiis 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 anAbortSignalsynchronously before spawning the fire-and-forget dispatch (agent-dispatcher.service.ts:126). Any new HTTP entrypoint MUST preserve this contract orStopwill look broken. - Agent model inheritance ([[feedback_agent_model_inheritance]]) —
never pass an explicit
model:tonew Agent({ ... })inside the registry. The registry resolves the model fromcontext.llmConfigat call time so per-tenant model overrides actually take effect. - No guided consumers ([[project_tucan_no_guided_consumers]]) — agent
dispatch is read from
session.agentTypewhich 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=falseenv (verified post-9631dd7d7) gates the pre-v2.0.0 fallback. Re-enabling it breaks the session-register-race guarantees. get_clinical_notesis a hallucination. The real tool islist-clinical-notes. If you see this in a failed dispatch, the prompt for the clinical agent has drifted and needs a regression eval.
Related Topics
- [[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_tokensmust 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]].