06-ai-tucanwave: W3filled18 citations

TUCAN Orchestrator — Step Execution and Dynamic Workflow

Audiences: developer, internal

TUCAN Orchestrator — Step Execution and Dynamic Workflow

The orchestrator is TUCAN's planner + executor: it turns a classified intent into a task plan (L1), resolves session memory (L2), runs the plan as a Mastra workflow with per-step tool resolution (L3), renders the result (L4), and persists state to the session (L5). The L4/L5 stages are still their own layers/ Injectable services; the earlier standalone layers/l1-intent-planner, l2-memory-resolver, and l3-orchestrator directories were consolidated — L1 now lives in orchestrator/agents/meta-planner.agent.ts + steps/meta-plan.step.ts, and L2/L3 (memory resolution + layered plan execution) fold into orchestrator/internal/layered-execution.ts and the steps/ factories.

Migration status (2026-07): this legacy TUCAN orchestrator runs alongside the Condor engine ([[condor-engine]]), whose own Mastra plan-compiler (ai/condor/mastra/orchestrator/compiler.ts) is the next-generation replacement for this layered pipeline. Flows are ported per-intent, not all at once.

Business Purpose

A clinical assistant that can take real action — write prescriptions, order labs, draft referrals, schedule follow-ups, run a patient intake — needs more than "call one tool per turn." Real work is multi-step, stateful, and resumable:

  • A patient-intake conversation iterates through 8-12 forms across potentially multiple sessions.
  • A "schedule the follow-up and send the patient an SMS reminder" request is two tools + a callback if the SMS fails.
  • A "diagnostic engine" turn picks ICD-10 candidates, fetches recent labs, runs a workflow that may suspend at each clinical decision.

The orchestrator gives Dudoxx this structure without bolting on a workflow engine — Mastra's createWorkflow + createStep primitives are wired through five layers (L1 plan → L5 persist) so every turn gets the same lifecycle. The keystone workflow orchestrated-dispatch ties it together and is the long-term replacement for the legacy single-stage dispatcher funnel.

Audiences

  • Investor: Multi-step, suspendable workflows are the moat for clinical AI. Competitors ship single-shot chat; Dudoxx ships resumable clinical conversations with audit-grade per-step persistence.
  • Clinical buyer (doctor / nurse): Pause mid-intake, resume from a different device, never lose state. The system can dial back to "I need one more answer" and continue rather than starting over.
  • Developer / partner: A new clinical flow is createWorkflow(...) .createStep(...) with typed inputSchema and outputSchema. The orchestrator's L3 layer dispatches workflows by ID; the L1 planner picks which workflow to invoke for which intent (and intent-workflow-map.ts is the table). Adding a new flow does not require a dispatcher change.
  • Internal (ops / support): Every L5 persist writes a typed slice into ChatSession.metadataoutcomes, metricsTimeline, sidePanel, executionPlan, renderedSections. Replay is one SELECT.

Architecture

diagram
                Intent (from [[tucan-intent-filtering]])
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│  L1 — IntentPlannerAgent                                       │
│   layers/l1-intent-planner/intent-planner.agent.ts             │
│   · LLM call returns Plan { tasks: TaskSpec[] }                 │
│   · TaskSpec  = { taskType, toolHint?, inputs, dependsOn? }    │
│   · TaskType  = SEARCH | CREATE | UPDATE | NOTIFY | DELETE |   │
│                 UI_ACTION | WORKFLOW_RUN | ...                  │
│   · respects descriptor.allowedTaskTypes + maxReplanAttempts    │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│  L2 — MemoryResolverService                                    │
│   layers/l2-memory-resolver/memory-resolver.ts                 │
│   · resolves recent turns / outcomes for each task              │
│   · elision rules: trim per-turn history to fit context budget │
│   · feeds the task executor with "what the previous step did"  │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│  L3 — WorkflowOrchestrator step + ToolFilter                    │
│   layers/l3-orchestrator/workflow-orchestrator.step.ts          │
│   layers/l3-orchestrator/tool-filter.ts                         │
│   · for each task: resolve tool, run via task-executor          │
│   · task-executor/factory.ts builds the executor by complexity │
│   · run-sub-workflow.ts dispatches a registered workflow        │
│   · `run.watch(...)` bridges Mastra step events → workflow:* SSE│
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│  L4 — RenderOrchestratorService                                │
│   layers/l4-writer/render-orchestrator.service.ts              │
│   layers/l4-writer/render-engine.ts                             │
│   · sanitize-narrative-html + render templates                  │
│   · structured outcomes → HTML/Markdown for the UI             │
│   · drives ChatMessage.renderedSections (L5 slice)              │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│  L5 — PersistencyAnchorService                                  │
│   layers/l5-persistency/persistency-anchor.service.ts           │
│   · persist-then-publish discipline                             │
│   · writes outcomes / metricsTimeline / sidePanel / renderedSections│
│   · creates ChatMessage rows + TucanUsageRecord rows            │
│   · then publishes SSE so consumers never see ephemeral state   │
└────────────────────────────────────────────────────────────────┘

orchestrated-dispatch keystone workflow

The Mastra workflow that ties L1-L5 together is buildOrchestratedDispatchWorkflow(deps) in orchestrated-dispatch.workflow.ts (Mastra workflow id: 'orchestrated-dispatch'). It is built once per process from a NestJS factory that resolves its deps (WorkflowService, WorkflowEventsPublisher, etc.), composed of named steps:

  • resolveAndRunStep — for each TaskSpec in the plan: resolve the tool, optionally short-circuit subsumed peers (composite-workflow revocation pass), execute via task-executor/factory.ts.
  • reportAndFinalizeStep — synthesize the DispatchReport, publish the L4 narrated output, finalize L5 persistence.

PlanWithResultsSchema carries the per-task outcomes through the steps; OrchestratedDispatchOutputSchema is the workflow's terminal output (plan + results + DispatchReport).

task-executor — the per-task dispatch core

Under orchestrator/steps/task-executor/ is a small factory pattern:

FilePurpose
factory.tsBuilds the executor by task complexity (run-by-complexity.ts picks the strategy)
dispatch-helpers.tsShared dispatch helpers
run-sub-workflow.tsWhen the task is WORKFLOW_RUN, dispatch a registered Mastra workflow by ID
l4-render.tsPer-task L4 render hook (feeds RenderOrchestrator)
response-shapes.tsTyped per-task response shapes
types.tsShared task-executor types

Registered workflows

15 standalone workflows live under tucan/workflows/:

  • agentic-diagnosis/ — multi-agent diagnostic reasoning
  • deep-search/ — Qdrant + document RAG search
  • diagnosis-editor/ — interactive diagnosis editor
  • document-generation/ — letter / report generation
  • follow-up-scheduler/ — multi-step follow-up booking
  • free-chat/ — generic chat fallback
  • lab-order-bundle/ — multi-test order assembly
  • mail-processing/ — incoming email triage
  • patient-intake/ — 8-12 step intake conversation
  • patient-search/ — disambiguation with clarify-candidate suspend
  • pre-visit-briefing/ — pre-visit context assembly
  • prescribe-and-check/ — prescription + drug interaction
  • symptom-analysis/ — symptom → differential
  • treatment-plan/ — multi-step treatment-plan composition
  • visit-clinical-capture/ — chart-capture workflow

intent-workflow-map.ts maps intent → workflow ID for the planner's fallback when no other workflow is named.

Tech Stack & Choices

ComponentChoiceWhy
Workflow engineMastra createWorkflow + createStep (@mastra/core@1.13.0)In-process, typed inputSchema / outputSchema, native suspend/resume.
Step schemasZod v4 throughoutRun-time validation + compile-time type derivation.
Layer split L1-L5Each layer is an @Injectable() (NestJS DI)Replaceable independently; testable in isolation.
Plan typingPlanSchema (schemas/plan.schema.ts) with TaskType enum + TaskSpec shapePlanner output is typed; downstream consumers do not need runtime type guards.
TaskType enum`SEARCHCREATE
Composite workflow revocationworkflow-tool-capabilities.ts subsumes fieldWhen a composite workflow is bound, peer tools it subsumes get short-circuited — prevents double-execution defect.
Memory elisionorchestrator/memory/elision-rules.tsPer-rule history truncation so the planner sees relevant turns, not every turn.
run.watch(...) bridgeMastra run.watch events → SSE workflow:step:*Frontend gets per-step "doing X" hints without polling.
Persist-then-publishL5 writes Prisma first, SSE secondConsumer must never see a state that is not durable; tested by __tests__/persist-then-publish.spec.ts.
renderedSections sliceL4 output stored on ChatMessage.renderedSectionsUI hydrates rendered sections on reload without re-running L4.
Outcome shapesschemas/outcome-shapes/ typed payloads (clinical / misc)Per-task outcome is typed end-to-end — no string-shaped result bags.
Suspend / resumeMastra suspend() inside a step → session.metadata.suspendedWorkflows[taskId]HITL pauses survive process restart, browser reload, different device.

Alternatives rejected:

  • Temporal / Camunda / Step Functions — too heavyweight; would add a separate deploy artifact and a network hop. Mastra workflows are in-process.
  • Hand-rolled state machine — Mastra's createWorkflow already handles step typing, suspend/resume, and run.watch event bridging. Re-implementing is yak shaving.
  • Single-stage dispatcher only — what we had; cannot model multi-step clinical conversations.

Data Flow

Business outcome: A doctor says "do the symptom analysis for Mrs. Garcia." The system runs a 5-step workflow: get patient context, ask for symptom severity (suspend), compute differential, propose tests, render the result. If the doctor closes the browser at step 2, she resumes on her phone and the conversation continues from step 2.

Technical mechanism:

  1. Dispatcher emits intent clinical (or patient-data). Intent descriptor allows WORKFLOW_RUN.
  2. L1 IntentPlannerAgent picks workflow ID 'symptom-analysis' via intent-workflow-map.ts fallback or explicit hint.
  3. L2 MemoryResolver hydrates patient context + the last 6 turns of relevant history.
  4. L3 resolveAndRunStep resolves the tool to the WORKFLOW_RUN executor → run-sub-workflow.ts dispatches Mastra workflow 'symptom-analysis'.
  5. Step 2 of symptom-analysis calls suspend({ stepId: 'severity', payload: { prompt: 'Rate severity 1-10' } }).
  6. Outer orchestrator catches the suspend, writes session.metadata.suspendedWorkflows[taskId] = { runId, stepId, workflowId, ... }, publishes workflow:suspended SSE event with taskId.
  7. Doctor closes the browser. Tomorrow on her phone: POST /api/v1/agent/sessions/:id/messagesChatOrchestratorService.resume (ddx-api/src/ai/tucan/orchestrator/chat-orchestrator.service.ts:152).
  8. resume → workflow.createRun({ runId })run.resume({ step, resumeData: validatedDecision }) (ddx-api/src/ai/tucan/orchestrator/chat-orchestrator.service.ts:267).
  9. Workflow continues from step 3; run.watch publishes workflow:step:* SSE events as the remaining steps execute.
  10. L4 RenderOrchestrator narrates the differential. L5 persists renderedSections, finalizes outcomes.

Implicated Code

  • ddx-api/src/ai/tucan/orchestrator/orchestrated-dispatch.workflow.ts:30 — Mastra createStep / createWorkflow imports.
  • ddx-api/src/ai/tucan/orchestrator/orchestrated-dispatch.workflow.ts:93OrchestratedDispatchInputSchema = PlanInputSchema.
  • ddx-api/src/ai/tucan/orchestrator/orchestrated-dispatch.workflow.ts:99PlanWithResultsSchema.
  • ddx-api/src/ai/tucan/orchestrator/orchestrated-dispatch.workflow.ts:109OrchestratedDispatchOutputSchema.
  • ddx-api/src/ai/tucan/orchestrator/orchestrated-dispatch.workflow.ts:168buildOrchestratedDispatchWorkflow(deps).
  • ddx-api/src/ai/tucan/orchestrator/orchestrated-dispatch.workflow.ts:227resolveAndRunStep = createStep({ ... }).
  • ddx-api/src/ai/tucan/orchestrator/orchestrated-dispatch.workflow.ts:250 — composite-workflow revocation pass.
  • ddx-api/src/ai/tucan/orchestrator/orchestrated-dispatch.workflow.ts:536reportAndFinalizeStep.
  • ddx-api/src/ai/tucan/orchestrator/orchestrated-dispatch.workflow.ts:582workflow = createWorkflow({ id:'orchestrated-dispatch', ... }).
  • ddx-api/src/ai/tucan/orchestrator/chat-orchestrator.service.ts:83 — class ChatOrchestratorService (entry; eventually replaces legacy dispatcher).
  • ddx-api/src/ai/tucan/orchestrator/chat-orchestrator.service.ts:152resume(ctx, decision) — Mastra workflow resume bridge.
  • ddx-api/src/ai/tucan/orchestrator/chat-orchestrator.service.ts:267run.resume({ step: entry.stepId, resumeData: validatedDecision }).
  • ddx-api/src/ai/tucan/orchestrator/agents/meta-planner.agent.ts + orchestrator/steps/meta-plan.step.tsL1 intent→task-plan meta-planner (replaced the retired layers/l1-intent-planner/; L1_INTENT_PLANNER_MODEL_KEY no longer exists as a named symbol).
  • ddx-api/src/ai/tucan/orchestrator/internal/layered-execution.ts + orchestrator/internal/memory-sanitize.tsL2 memory resolution + per-turn history sanitization (replaced layers/l2-memory-resolver/).
  • ddx-api/src/ai/tucan/orchestrator/steps/task-executor/ + orchestrator/internal/layered-execution.tsL3 layered plan execution (replaced layers/l3-orchestrator/workflow-orchestrator.step.ts).
  • ddx-api/src/ai/tucan/orchestrator/steps/tool-resolve.step.ts + orchestrator/tool-resolver/ — per-task tool resolver (was layers/l3-orchestrator/tool-filter.ts).
  • ddx-api/src/ai/tucan/orchestrator/layers/l4-writer/render-orchestrator.service.ts:137 — class RenderOrchestratorService.
  • ddx-api/src/ai/tucan/orchestrator/layers/l4-writer/render-engine.ts — L4 render engine.
  • ddx-api/src/ai/tucan/orchestrator/layers/l5-persistency/persistency-anchor.service.ts:129 — class PersistencyAnchorService.
  • ddx-api/src/ai/tucan/orchestrator/layers/l5-persistency/__tests__/persist-then-publish.spec.ts — persist-then-publish invariant test.
  • ddx-api/src/ai/tucan/orchestrator/steps/task-executor/factory.ts — task executor factory.
  • ddx-api/src/ai/tucan/orchestrator/steps/task-executor/run-sub-workflow.tsWORKFLOW_RUN dispatcher.
  • ddx-api/src/ai/tucan/orchestrator/steps/task-executor/run-by-complexity.ts — strategy by task complexity.
  • ddx-api/src/ai/tucan/orchestrator/schemas/plan.schema.tsPlanSchema + TaskSpec.
  • ddx-api/src/ai/tucan/orchestrator/schemas/outcome-shapes/payloads-clinical.ts — typed clinical outcome payloads.
  • ddx-api/src/ai/tucan/orchestrator/intent-workflow-map.ts — intent → workflow ID fallback table.
  • ddx-api/src/ai/tucan/tools/registry/workflow-tool-capabilities.tssubsumes source of truth for composite revocation.
  • ddx-api/src/ai/tucan/workflows/symptom-analysis/ — example registered workflow.
  • ddx-api/src/ai/tucan/workflows/patient-intake/ — multi-step intake with suspend per form.

Operational Notes

  • Persist-then-publish. L5 writes Prisma before publishing the SSE event; the test l5-persistency/__tests__/persist-then-publish.spec.ts enforces it. Don't reorder.
  • Composite-workflow revocation is a defect guard. When a task binds a composite workflow tool, the orchestrator runs a revocation pass that short-circuits any peer task whose tool ID is in the composite's subsumes set (orchestrated-dispatch.workflow.ts:250). Adding a new composite without populating subsumes causes double-execution.
  • Sub-workflow run.watch is the SSE pipe. Per-step workflow:step:* SSE events come from run.watch(...) inside task-executor/run-sub-workflow.ts. If new workflows don't emit step events, the orchestrator did not subscribe run.watch for them.
  • NestJS services cannot inject into Mastra steps. Workflows use fetch() calls to NestJS HTTP endpoints, not direct service injection ([[ddx-mastra-specialist_memory.md]]). Persistence side-effects happen via the calling NestJS service path after the workflow completes.
  • renderedSections on ChatMessage is L4's output. Reloading the conversation must NOT re-run L4 — hydration reads renderedSections from the DB. If you see L4 cost on reload-only paths, the hydration is broken.
  • Suspend is per-task. Multi-suspend is not currently supported — first-entry-wins on resume (chat-orchestrator.service.ts:197).
  • run.watch events must not race the agent:complete. When the workflow completes, the orchestrator's reportAndFinalizeStep waits for run.watch flushes before emitting the terminal ok transition.
  • L1_INTENT_PLANNER_MODEL_KEY is the RequestContext key used to override the L1 planner model per-tenant. Setting it on the request context allows partner clinics to run the planner on a different model than the dispatcher's LLM.
  • free-chat, patient-search, patient-intake are the only three workflow IDs allowed as fallbackWorkflowId in intent descriptors. Adding a new fallback requires extending the union in IntentDescriptor.
  • Dynamic workflow creation is at register time, not run time. Workflows are registered in WorkflowModule at boot. "Dynamic" here means the dispatcher picks which workflow ID to invoke per turn; not that the workflow definition is built at call time.
  • [[tucan-assistant]] — orchestration entry that calls into the orchestrated-dispatch workflow.
  • [[tucan-dispatcher]] — legacy dispatcher pipeline; orchestrator eventually replaces it (chat-mode flag flip per plans/tucan-chat-mode/).
  • [[tucan-intent-filtering]] — supplies the descriptor read by L1 planner.
  • [[ai-medical-tools]] — provides the tools each task executor resolves and runs.
  • [[ai-chat-sessions]] — owns the L5-persisted metadata slices (outcomes, metricsTimeline, sidePanel, executionPlan, renderedSections).
  • [[tucan-consumption-pricing]] — TucanUsageRecord 1:1 with ChatMessage rows L5 persists; per-step cost rollup.
  • [[sse-event-engine]] (W1) — receives workflow:step:*, workflow:suspended, workflow:complete events.
  • docs/claude-context/CLAUDE_MASTRA_CODEBASE_USAGE.md — Mastra createWorkflow / createStep reference (link, don't duplicate).
    TUCAN Orchestrator — Step Execution and Dynamic Workflow — Dudoxx Docs | Dudoxx