Condor Intent & Planner — User Turn to Plan Object
Audiences: developer, internal
This is the front half of the Condor control plane: everything between a user's chat turn arriving at the dispatcher and a validated, normalized
WorkflowPlanobject being handed to the compiler. The back half — compiling that plan into a Mastra workflow and executing it — is Condor Workflow Compiler. The runtime surrounding both (router, transport, session lifecycle) is Condor Engine.
⚠ Read this first — the intake stage is NOT the planner
The most misleading thing about this stage is its own directory name. Condor has
a directory called intake/ containing an intent classifier and a request
decomposer, both of which run on every turn. A reader naturally assumes they
classify the request and hand a structured intent to a planner.
They do not. As of 8ee29c4a:
- The flow selection is still the static
flowKeyresolved byagentic-flow-router.service.ts— a routing label carried on the dispatch (e.g.DIAGNOSIS_ENGINE_FLOW_KEY), not computed from the request text. - The plan DAG is still authored by ONE fused LLM call,
generateAndParsePlanatworkflow-runner.plan.ts:901. intake/is explicitly additive / observational — its own file headers say so (intake-stage.ts:6-9,intent-classifier.ts:12-18). Its output (IntentClass+Subtask[]) is threaded forward for logging and for future downstream consumption; it does not gate, replace, or feed the plan-authoring call.
So: generateAndParsePlan is the real planner. Any statement of the form
"the intent classifier drives the plan" is wrong today. It is a staging ground
for a future load-bearing intent layer, running in shadow so its accuracy can be
measured against real traffic before anything depends on it.
Control flow — turn to normalized plan
Two things worth naming in that graph:
- The dotted branch to
intake/is a dynamicimport(), not a static one. That is deliberate: it keeps@mastra/coreout of the router's static import graph, which is DI-cycle sensitive in Nest. - Repair and first-pass generation converge on the same
normalizePlanpipeline. A repaired plan is therefore indistinguishable from a first-pass-valid one at execution time — there is no "was repaired" branch downstream.
The real planner — generateAndParsePlan
A single-shot call against the Mastra orchestrator agent using structured
output (output: workflowPlanSchema). What it assembles into the prompt:
| Input | Why it is there |
|---|---|
| Context preamble (now / locale / localTime) | Lets the planner resolve "latest" and relative time, and reply in the UI locale |
buildSessionMemorySnapshot | Working-memory snapshot — memory-informed planning |
buildPriorRunsBlock | Prior-run history for the session |
Attachment hints (real fileId + mime + ingestStatus) | Lets the planner name a reader tool directly, skipping a sessionFilesList round-trip |
If structured output drops, it falls back to text-parsing the reply.
Mastra trap: the orchestrator handle must not be destructured before calling
.generate(...). Destructuring losesthis, and Mastra's private-field access then throws at runtime. The inline comment aboveworkflow-runner.plan.ts:901guards this.
The plan object — WorkflowPlan
workflowPlanSchema (mastra/orchestrator/types.ts:472):
| Field | Shape | Note |
|---|---|---|
id | string (min 1) | |
steps | WorkflowPlanStep[] (min 1) | the discriminated union below |
rationale | string? | planner's own justification |
role | RoleTemplateId? | role-template binding |
autonomy | 'autonomous' | 'supervised' | feeds the HITL gate |
intent | PlanIntentKind? | optional for back-compat; compiler defaults to 'execute' |
userMessage | string? | verbatim last user message |
costCeilingCents | number? | cumulative spend ceiling enforced by the pump |
userMessage exists for a specific reason: the compiler threads it into every
leaf's prompt. Without it, role agents re-asked for a brief the user had
already given — the 2026-05-07 "phantom-execution" bug.
The 8-way step union
approvalStepSchema is the plan-level HITL seat — the mutation/approval
machinery that consumes it is in
Condor Mutation & Observers.
Naming caveat (verified 2026-07-29):
plan.schema.ts:70exports a separate, narrowerWORKFLOW_PLAN_STEP_KINDSconst listing only five kinds (tool-call,agent-call,parallel,sequential,final). The authoritative 8-way Zod union is the one intypes.ts:460. Do not read theplan.schema.tsconst as the step taxonomy.
Two functions named "normalize" — do not conflate
This is the second trap in this stage, and it is purely a naming collision:
normalizePlanCandidate | normalizePlan | |
|---|---|---|
| File | workflow-dispatcher.helpers.ts:278 | mastra/orchestrator/normalize-plan.ts:201 |
| Stage | before schema validation | after schema validation |
| Input | raw unknown (parsed JSON) | a schema-valid WorkflowPlan |
| Job | coerce an unknown shape into something the schema can accept (role-snapshot diffing, DEF-202) | the 3-stage post-schema pipeline below |
Getting these backwards misplaces where schema validation sits in the chain.
The normalizePlan pipeline (3 ordered stages)
ensurePlanFinalStep(normalize-plan.ts:163) — guarantees a synthesisfinalleaf exists. It is a deliberate pure re-implementation ofensureFinalStep(workflow-runner.plan.ts:823) so this module stays Mastra-free and unit-testable without@mastra/core.lowerPlan(compiler/graph-lowering.ts) — dependency repair plus execution-graph lowering. An explicitdependsOn: []means root; producers of aninputRefare folded ahead of their consumers. This is the join into the compiler topic.validatePlanStructure(plan.schema.ts:572) — validates against the active tool-catalog snapshot: off-list tool ids, phantominputRefs, id/kind mismatches, and prose-claimed mutations (viaplan-mutation-rewrite.ts).
shapeHash — a dedupe key, and nothing else
shapeHash (mastra/orchestrator/shape-hash.ts:97) is a deterministic sha256
over a canonicalized plan structure: object keys sorted, __synth_*-prefixed
keys stripped, array order preserved.
Its one job is the orchestrator dedupe gate (AC-3): if an identical plan shape is generated twice inside a ~5s window, the second execution collapses into the first.
It is not a cache key for LLM responses, and not a replan-detection
signal. Replan detection is a separate mechanism (plan-revision.ts, covered in
the mutation/observer topic). Describing shapeHash as content-addressable
caching overstates it considerably.
workflow-templates/ — pre-authored plan skeletons
workflow-template.registry.ts is a pure in-memory, DI-free registry of
reusable plan skeletons. Each skeleton is a complete, schema-valid
WorkflowPlan — not a partial or a fragment.
diagnosis-engine.workflow.tsis the one shipped skeleton: anamnesis → differential → drug-check → referral → documenter → final.DETERMINISTIC_TEMPLATE_IDS(:171) marks skeletons whose execution must not vary;assertSkeletonsValid(:200) is a boot-time self-check.- When a turn's
flowConfig.workflowTemplateIdsnames one, the pump — not the compiler — merges the skeleton with the live orchestrator-generated plan beforecompilePlanruns.
That last point is the design decision worth keeping: the registry only seeds. The compiler remains the sole execution path, so adding templates required no compiler changes at all.
medicalMode — the single canonical write-site
medical-mode-derivation.ts is not part of plan shaping, but it is derived at
this stage and every downstream guardrail reads it. It exists to collapse what
were two duplicate ctx.set('medicalMode', …) sites into one.
deriveMedicalMode (:115) ORs four sources:
- the flow-config default,
- a legacy
flowKeycheck (this is theDIAGNOSIS_ENGINE_FLOW_KEY → medicalMode ONedge drawn in Condor Engine's router diagram), - the persisted session flag,
- a clinical-candidate mismatch repair — if the surfaced tool-candidate set
contains clinical-prefixed ids (
CLINICAL_TOOL_ID_PREFIXESat:40—patient.,medication.,condition., …) whilemedicalModeis OFF, it deterministically repairs to ON.
Source 4 is a fail-closed rule: the ambiguous case resolves toward stricter
guardrails, never looser. Outside the module's own internal helpers there are
exactly two call sites — condor.controller.ts:2345 (turn entry) and
mastra/orchestrator/compiler.ts:473 (compile time) — which is what makes
"single canonical write-site" true rather than aspirational.
mastra/request-context.schema.ts:10 states the same rule as a contract comment.
Related Topics
- Condor Engine — the runtime this stage sits inside: flow router, transport, session lifecycle.
- Condor Workflow Compiler — the back half: the normalized plan becomes a Mastra workflow and runs.
- Condor Mutation & Observers — where
approvalStepSchema, autonomy, and replan detection are consumed. - Condor Diagnosis Engine — the flow whose
skeleton lives in
workflow-templates/and which drivesmedicalMode. - Condor RAG Tool Search — produces the
tool-candidate set that
deriveMedicalModesource 4 inspects.