Agent Templates — Configurable AI Agent Factory
Audiences: developer, internal
Production TUCAN agents are not coded — they are YAML templates loaded at boot, materialized into Mastra
Agentinstances by an agent factory, with tool catalogs filtered by category, system prompts rendered through Jinja2-style variables, and LLM config resolved from the per-request context.
Business Purpose
Hard-coding agents — a new Agent({...}) per specialist — does not scale:
- Every new agent variation (German-only clinical assistant for a partner clinic, a high-temperature creative agent for letter drafting) requires a code change and a deploy.
- Per-tenant overrides (model, temperature, categories, alwaysInclude)
need a place to live that is not
if (tenant === 'X')inside the factory. - A non-engineer (a partner integrator, a customer-success engineer) needs to inspect "what is the clinical agent allowed to do here?" without reading TypeScript.
Agent templates ship as YAML files under
ddx-api/src/ai/tucan/templates/agents/ plus a runtime
agent-templates/ REST surface for tenant-scoped customization.
Each template fully describes:
- The agent's identity (id, name, description)
- The system instructions as a Jinja2-style string with
{{identity.*}},{{organization.*}},{{session.linkedPatient.*}}placeholders, rendered against the request context - The model config (provider, default model slug, temperature, maxTokens) — defaults only; tenant overrides resolve at call time
- The tool selection policy (categories whitelist, alwaysInclude list, ragEnabled flag, ragConfig topK + scoreThreshold)
- Memory policy for cross-turn continuity
This gives the team a single editable surface for agent behavior,
auditable in git diff, swappable per tenant.
Audiences
- Developer / partner: To add a new specialist agent, drop a YAML
file under
templates/agents/, register the type inagent-type.registry.ts, write the tool categories, ship a PR. No hard-coded MastraAgent()calls needed in 95% of cases. - Internal (ops / support): Tenant-specific tweaks (a clinic that
wants
temperature: 0.3for prescription drafting) go through theagent-templatesREST surface (ddx-api/src/ai/agent-templates/) and persist to the AI database — no deploy. - Investor: Productizing "the clinical AI" stops being "we hire more ML engineers"; it becomes "we tune templates and add tool categories." Scales with partner integrations, not headcount.
- Clinical buyer: Different clinics get different defaults — a dermatology clinic gets the dermatology-specialist template wired to imaging tools; an internal-medicine clinic gets the clinical specialist with diagnostic-engine wiring.
Architecture
┌────────────────────────────────────────────────────────────────┐
│ Template source │
│ ddx-api/src/ai/tucan/templates/agents/ │
│ clinical.agent.yaml scheduling.agent.yaml │
│ document.agent.yaml admin.agent.yaml │
│ tucan.agent.yaml deep-search.agent.yaml │
│ │
│ Per-template YAML fields (top-level): │
│ metadata: { id, name, description } │
│ spec.instructions: Jinja2 string │
│ spec.model: { provider, default, temperature, maxTokens } │
│ spec.tools: { categories[], alwaysInclude[], │
│ ragEnabled, ragConfig:{topK, scoreThreshold} } │
│ spec.memory: { window, semanticRecall, threadStrategy }│
└────────────────────────────────────────────────────────────────┘
│
▼ (boot)
┌────────────────────────────────────────────────────────────────┐
│ TemplateLoaderService (onModuleInit) │
│ · enumerate yaml files │
│ · parse + validate against frontmatter.schema (Zod v4) │
│ · registerAgents(...) into AgentTypeRegistry │
└────────────────────────────────────────────────────────────────┘
│
▼ (per turn, via dispatcher)
┌────────────────────────────────────────────────────────────────┐
│ AgentFactory.createAgent(context) │
│ · createTemplateAgent (YAML-backed path) │
│ OR createLegacyAgent (code-defined path, kept for tucan/...) │
│ · resolves model from LLMConfigFactory(context.llmConfig) │
│ · renders Jinja2 instructions against {identity, org, session} │
│ · filters tool catalog by spec.tools.categories │
│ · adds alwaysInclude tools │
│ · returns `new Agent({...})` — model resolved at this site │
└────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ Mastra Agent (in process) │
│ · ready to dispatch via DispatcherStage │
└────────────────────────────────────────────────────────────────┘
Specialist factory + Network factory
Two specialized factories sit alongside the base AgentFactory:
SpecialistAgentFactory— creates domain specialists (clinical, scheduling, document, admin) with a category-filtered tool list and a domain-specific prompt. It emits anagent:resolvedSSE event so the frontend can show which specialist answered.NetworkAgentFactory— creates a network orchestrator that delegates to multiple specialists for cross-domain queries. Mastra 1.x networks require memory on the orchestrator for cross-specialist continuity. Marked for refactor (single supervisor agent with tool-based delegation) in the file header comment.
Tenant-customizable runtime templates
The module ddx-api/src/ai/agent-templates/ is a separate concern —
it is the REST surface that lets tenant admins persist customized
templates to the ddx_api_ai DB. Those persisted templates layer on
top of the file-shipped defaults; the factory reads both.
Tech Stack & Choices
| Component | Choice | Why |
|---|---|---|
| Template format | YAML 1.2 with version, kind, metadata, spec | Looks like Kubernetes; familiar to integrators and reviewable in git diff. |
| Schema validation | Zod v4 (frontmatter.schema.ts + registry/__tests__/frontmatter.spec.ts) | Catches bad templates at boot, not at first turn. |
| Prompt templating | Jinja2-style {{...}} with {% if ... %} blocks | Matches doctor-facing prompt review tools; logic-light enough to stay maintainable. |
| Variable namespaces | identity.*, organization.*, session.*, session.linkedPatient.* | Mirrors the [[tucan-context-builder]] context shape — same names everywhere. |
| Tool filtering | Category whitelist + alwaysInclude list | Matches the [[ai-medical-tools]] catalog; readable in YAML. |
| RAG settings | ragEnabled, ragConfig.topK, ragConfig.scoreThreshold | Per-template tuning of build-time tool-RAG rerank (see [[tucan-rag-tools]]). |
| Model resolution | LLMConfigFactory.buildDudoxxModel(context.llmConfig) at factory call time | Tenant model override flows from the request context — NOT from the YAML. The YAML defaults; the context wins. |
| Agent model inheritance | No explicit model: on new Agent({...}) | [[feedback_agent_model_inheritance]] — explicit model on Agent break per-tenant overrides. Production factory paths set the model only through context.llmConfig. |
| Frontmatter test | registry/__tests__/frontmatter.spec.ts | Snapshot-tests every YAML; new template without frontmatter ≠ green CI. |
Alternatives rejected:
- JSON Schema — does not handle the Jinja2 strings well; less human-readable.
- Code-defined
AgentSpecobjects — what we had; brittle for tenant overrides and required a deploy for every prompt change. - Prompt files separate from spec files — splits the unit of review. Keep prompt + tools + memory in one YAML.
Data Flow
Business outcome: A partner clinic in Switzerland requests "the clinical assistant must use German-only and run at temperature 0.3." A solutions engineer writes a YAML override; the next session uses the customized prompt and temperature.
Technical mechanism:
- Solutions engineer POSTs the new template body to
ddx-api/src/ai/agent-templates/agent-templates.controller.ts→agent-templates.service.tswrites a row inddx_api_ai.- Next turn, dispatcher calls
AgentFactory.createAgent(context)(ddx-api/src/ai/tucan/registry/agent-factory.ts:153).- Factory looks up
agentType('clinical') in the registry, finds a tenant-scoped override (Swiss clinic), merges with the YAML-shipped default (createTemplateAgent,agent-factory.ts:176).LLMConfigFactory.buildDudoxxModel(context.llmConfig)resolves the model from the request context (Swiss clinic's tenant config pointed atdudoxx-gemma).- Tool catalog filtered to
categories: [fhir, session, clinical-notes, conditions, utility, assessment, visit](per the YAML attemplates/agents/clinical.agent.yaml).- Jinja2 instructions rendered with the patient's name (German spelling), the org name, the doctor's title.
new Agent({ ... })constructed with rendered instructions + filtered tools (agent-factory.ts:253); LLM model attached through the context, not explicit on the Agent constructor.agent:resolvedSSE event published withspecialistId: 'clinical-specialist'.- Dispatcher runs the turn.
Implicated Code
ddx-api/src/ai/tucan/templates/agents/clinical.agent.yaml:1— example clinical specialist template (instructions, model config, tool categories, RAG settings).ddx-api/src/ai/tucan/templates/agents/scheduling.agent.yaml— scheduling specialist.ddx-api/src/ai/tucan/templates/agents/document.agent.yaml— document specialist.ddx-api/src/ai/tucan/templates/agents/admin.agent.yaml— billing / insurance / tasks specialist.ddx-api/src/ai/tucan/templates/agents/tucan.agent.yaml— full-catalog TUCAN agent.ddx-api/src/ai/tucan/templates/agents/deep-search.agent.yaml— deep-search specialist.ddx-api/src/ai/tucan/registry/template-loader.service.ts:41— classTemplateLoaderService implements OnModuleInit.ddx-api/src/ai/tucan/registry/template-loader.service.ts:73—onModuleInit()enumerates + parses + validates YAML files.ddx-api/src/ai/tucan/registry/template-loader.service.ts:98—registerAgents(...)into the registry with bootstrap log.ddx-api/src/ai/tucan/registry/frontmatter.schema.ts— Zod v4 schema for the YAML frontmatter (metadata,spec).ddx-api/src/ai/tucan/registry/agent-factory.ts:94— classAgentFactory implements OnModuleInit.ddx-api/src/ai/tucan/registry/agent-factory.ts:153— publiccreateAgent(context, options).ddx-api/src/ai/tucan/registry/agent-factory.ts:176—createTemplateAgent(...)(YAML-backed path).ddx-api/src/ai/tucan/registry/agent-factory.ts:216—createLegacyAgent(...)(code-defined fallback, retained for thetucanagent).ddx-api/src/ai/tucan/registry/agent-factory.ts:253— actualnew Agent({...})construction — model NOT explicit, inherited throughcontext.llmConfig.ddx-api/src/ai/tucan/registry/agent-factory.ts:326—createSpecialistAgent(...).ddx-api/src/ai/tucan/registry/agent-factory.ts:346—createNetworkAgent(...).ddx-api/src/ai/tucan/registry/specialist-factory.ts:55— classSpecialistAgentFactory.ddx-api/src/ai/tucan/registry/specialist-factory.ts:151— emitagent:resolvedevent for specialist agents.ddx-api/src/ai/tucan/registry/specialist-factory.ts:227— filter tools by specialist categories.ddx-api/src/ai/tucan/registry/network-factory.ts:47— classNetworkAgentFactory.ddx-api/src/ai/tucan/registry/network-factory.ts:97—createSpecialists(...)populates the network with each domain specialist.ddx-api/src/ai/tucan/registry/agent-registry.service.ts:48— classAgentRegistryService implements OnModuleInit.ddx-api/src/ai/tucan/registry/agent-registry.service.ts:207— the altnew Agent({...})construction site (registry-direct path).ddx-api/src/ai/tucan/registry/agent-type.registry.ts:96— classAgentTypeRegistry— canonicalagentType→ spec lookup.ddx-api/src/ai/tucan/registry/llm-config.factory.ts:301— classLLMConfigFactory—.buildDudoxxModel(llmConfig)is the model resolution call.ddx-api/src/ai/agent-templates/— REST surface for tenant template overrides persisted toddx_api_ai.
Operational Notes
- NEVER pass an explicit
model:tonew Agent({...})in any factory site. Model resolution must flow throughcontext.llmConfig→LLMConfigFactory.buildDudoxxModel. Explicit Agent-levelmodel:breaks per-tenant overrides — verified pitfall ([[feedback_agent_model_inheritance]]). - Frontmatter snapshot test guards templates. Adding a new YAML
without updating the snapshot fails CI. Run
pnpm jest src/ai/tucan/registry/__tests__/frontmatter.spec.ts -uintentionally when the change is expected. categories: []defaults are different per template. Thetucan.agent.yamltemplate has the all category set (all 35); domain specialists are narrow (clinical = 7 categories). When authoring a new template, copy a sibling and edit categories surgically — do not start from the full set.- Jinja2 variables are NOT free-form. Only the namespaces wired
by
tucan-context-builderare populated (identity,organization,session,session.linkedPatient). Anything else renders as the empty string and you get prompts with awkward whitespace. - Memory config differs per agent type. Clinical retains more context (larger window); voice-channel agents use shorter windows (latency budget). Tune per template; don't apply one window to all.
- Network agent path is deprecated. The
NetworkAgentFactorysource comments call out the planned refactor to a single supervisor agent with tool-based specialist delegation. Don't build new product on the network path. - Tenant overrides persist; defaults ship in git. The
agent-templates/REST surface writes toddx_api_ai, not back to the YAML file. If a tenant's customization should become the global default, it must be lifted manually to YAML. - Ephemeral
new Agent(...)calls are banned. Project rule ([[ddx-mastra-specialist_memory.md]] 2026-04-28) — every Agent construction must go through an allowlisted factory site. This prevents the "let me just spin up an agent here" anti-pattern that bypasses LLM config, tool filtering, and audit.
Related Topics
- [[tucan-assistant]] — orchestration entry point that calls
AgentFactory.createAgentper turn. - [[tucan-agent-types]] — canonical agent type list (clinical / scheduling / document / admin / flowagent / operator / tucan).
- [[ai-medical-tools]] — the tool catalog filtered by
spec.tools.categories. - [[tucan-rag-tools]] — the build-time tool-RAG used when
ragEnabled: true. - [[tucan-context-builder]] — supplies the variable namespaces
(
identity,organization,session). - [[tucan-pool]] — instance management for resolved agents per tenant.
- [[tucan-dispatcher]] — the dispatcher pipeline that ultimately invokes the constructed agent.
docs/claude-context/CLAUDE_MASTRA_CODEBASE_USAGE.md— Mastra primitives in-depth (link, do not duplicate).~/.claude/rules/shared/review-checklists/mastra.md— mandatory pre-commit checklist for tool + agent changes.