13-ai-condorwave: W6filled3 citations

Condor Engine — Ported Agentic Runtime (Tucan→Condor Migration)

Audiences: developer, internal, investor

Condor Engine — Ported Agentic Runtime (Tucan→Condor Migration)

Condor is the second agentic engine in ddx-api, running side-by-side with the legacy TUCAN estate during an in-progress migration. It lives at ddx-api/src/ai/condor/ (~358 files, 5 modules, ~19 controllers) and is wired into the app via ai-group.module.ts. Condor is a full port of the standalone ddx-condor-platform agentic runtime onto HMS conventions (HMS auth chain, no FHIR coupling in the transport, ddx_api_ai session ownership). This shard covers the runtime: the flow router, chat/AG-UI transport, session lifecycle, and the Mastra plan-compiler. The sandbox + low-code builder subsystems are in Condor Sandbox & Builder.

Why Condor (the migration frame)

Condor is the successor to TUCAN. Rather than a greenfield rewrite (an abandoned earlier plan), the strategy is a port + per-flow co-existence: bring the Condor engine in whole, run it beside TUCAN, and cut individual flows over one at a time via a routing table, then decommission TUCAN once traffic is zero. The single seam that makes this safe is the AgenticFlowRouter.

The co-existence linchpin — AgenticFlowRouter

agentic-flow-router.service.ts resolves a flowKey (the dispatch selector at the chat funnel) to exactly one engine: 'tucan' or 'condor'.

  • Single-target: a flowKey reaches exactly one engine, never both.
  • Safe default (Phases 0–4): an unmapped/unknown flowKey → 'tucan'. The legacy engine is the fallback so an unconfigured flow can never silently route to the in-port engine. Phase 5 flips defaultEngine to 'condor' by config alone (full-adoption bias).
  • Runtime-flippable (no redeploy): the table is config-seeded (env) AND mutable in-process via setFlowEngine / clearOverride — per-flow cutover and flip-back need no deploy.
  • Data-only: the router imports nothing from either engine tree (no DI on either dispatcher), so it cannot create a Nest barrel DI cycle and stays free of FHIR / SSE-contract coupling.
Rendering diagram…
  • DIAGNOSIS_ENGINE_FLOW_KEY = 'diagnosis-engine' is bound to 'condor' at bootstrap (CondorModule.onModuleInit via setFlowEngine) — the deterministic clinical flow. condor.controller's buildRuntime turns medicalMode ON when the dispatched flowKey matches this key, unlocking clinical guardrails + clinical role-templates. The key lives at the routing seam so the module binding and the medicalMode gate share ONE source of truth.

Transport — CondorController (@Controller('ai/condor/sessions'))

Two primary entry endpoints plus lifecycle controls (condor.controller.ts):

  1. POST :id/messages — the chat dispatch funnel. body.dispatch selects the workflow def (the routing seam). Streams via pipeUIMessageStreamToResponse (AI-SDK UI Message Stream) — NEVER @Sse() — so the bytes are the AI-SDK useChat passthrough contract.
  2. GET runs/:runId/agui/live — a dedicated raw RFC 8895 AG-UI live-bridge with a 3-tier resolver: tier 0 durable store (AguiEventStore seq-cursor replay+poll) → tier 1 live registry (AiRunRegistry.claimStream) → tier 2 span replay. 404 only when BOTH the durable run row and the registry entry are absent.
  3. Lifecycle: POST :sessionId/runs/:runId/stop, GET :id/active-run, GET :id/stream, POST :sessionId/messages/:userMessageId/resubmit, POST :id/resume, POST :id/approve.

The two entry endpoints are deliberately different wire formats on different sockets — chat bytes are an AI-SDK passthrough, structural frames are raw AG-UI:

Rendering diagram…

Controller invariants (from plans/tucan-condor-port/_invariants.md, cited in the controller header):

  • /agui/live serves raw RFC 8895 frames — DELIBERATELY NOT wrapped in @ddx/sse-contract SseEnvelope/SSEChannels (wrapping would break the 3-tier resolver's per-tier id: cursor). @ddx/sse-contract remains the HMS global bus.
  • NO @RbacExempt anywhere — the full HMS auth chain (global GatewayAuthGuard
    • per-method JwtAuthGuard/RolesGuard + ownership + tenant scope) is mandatory. This removes the Tucan AI/voice anti-pattern.
  • Zero FHIR_CLIENT / ResourceFacade / HAPI imports in the controller — it touches only ddx_api_ai (session ownership) + the ported stream/store/registry.

Session lifecycle — session/ai-sessions-lifecycle.controller.ts

Full AI-session CRUD under the condor session base: create/list/get/delete/patch sessions, delete a message, fetch attachment content, history, usage-rollup, runs-summary, events, and pinned-cards (list/create/reorder/delete). This is the persisted conversation state that dispatch reads (dispatch is persisted session.agentType/flowKey state, NOT LLM-chosen).

Plan compilation — the Mastra orchestrator

mastra/orchestrator/compiler.ts is a pure-function compiler that turns a validated WorkflowPlan (plan.schema.ts) into an ad-hoc Mastra workflow via graph-based lowering:

  • Infers execution-level structure from per-step dependsOn (declaration-order fallback for back-compat) and emits Mastra .then() / .parallel([…]).
  • Steps sharing an upstream set become siblings in .parallel; a downstream step depending on >1 sibling gets an auto-inserted synthesizer step that merges sibling outputs.
  • Step kinds → Mastra primitives: tool-callcreateStep calling ctx.tools[toolId].execute; agent-callcreateStep calling ctx.roleAgents[role].stream(prompt) (never generate — preserves stream events for the UI); final → terminal step delegating to ctx.roleAgents.formatter.
  • Invariants: NO suspend() (v0 has no HITL gates); pure — same plan+ctx → same workflow shape, never cached across turns.

The lowering is the part a table cannot show — dependsOn is a graph, and the compiler's job is to flatten it into Mastra's linear .then() / .parallel() primitives, inserting a synthesizer wherever the flattening loses a fan-in:

Rendering diagram…

Supporting: decompose/diagnosis-decomposer.ts (breaks a clinical query into a plan), intent/intent-taxonomy.registry.ts (+ diagnosis-intent.taxonomy.ts) for query classification, tool-rag/condor-tool-rag.service.ts for tool selection (domain-scoped, multi-entity patterns).

Known runtime-port hazard

A bulk port compiles tsc-green yet can carry layered RUNTIME bugs (prefix doubling, guard-model gaps, unmigrated tables, slug-vs-UUID, env-name drift, transport-path mismatch, @Optional SWC quirks, router default). Any Condor change should be MCP-Chrome-verified end-to-end, not trusted on typecheck alone.