06-ai-tucanwave: W3filled23 citations

Agent Templates — Configurable AI Agent Factory

Audiences: developer, internal

Agent Templates — Configurable AI Agent Factory

Production TUCAN agents are not coded — they are YAML templates loaded at boot, materialized into Mastra Agent instances 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 in agent-type.registry.ts, write the tool categories, ship a PR. No hard-coded Mastra Agent() calls needed in 95% of cases.
  • Internal (ops / support): Tenant-specific tweaks (a clinic that wants temperature: 0.3 for prescription drafting) go through the agent-templates REST 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

diagram
┌────────────────────────────────────────────────────────────────┐
│  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 an agent:resolved SSE 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

ComponentChoiceWhy
Template formatYAML 1.2 with version, kind, metadata, specLooks like Kubernetes; familiar to integrators and reviewable in git diff.
Schema validationZod v4 (frontmatter.schema.ts + registry/__tests__/frontmatter.spec.ts)Catches bad templates at boot, not at first turn.
Prompt templatingJinja2-style {{...}} with {% if ... %} blocksMatches doctor-facing prompt review tools; logic-light enough to stay maintainable.
Variable namespacesidentity.*, organization.*, session.*, session.linkedPatient.*Mirrors the [[tucan-context-builder]] context shape — same names everywhere.
Tool filteringCategory whitelist + alwaysInclude listMatches the [[ai-medical-tools]] catalog; readable in YAML.
RAG settingsragEnabled, ragConfig.topK, ragConfig.scoreThresholdPer-template tuning of build-time tool-RAG rerank (see [[tucan-rag-tools]]).
Model resolutionLLMConfigFactory.buildDudoxxModel(context.llmConfig) at factory call timeTenant model override flows from the request context — NOT from the YAML. The YAML defaults; the context wins.
Agent model inheritanceNo 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 testregistry/__tests__/frontmatter.spec.tsSnapshot-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 AgentSpec objects — 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:

  1. Solutions engineer POSTs the new template body to ddx-api/src/ai/agent-templates/agent-templates.controller.tsagent-templates.service.ts writes a row in ddx_api_ai.
  2. Next turn, dispatcher calls AgentFactory.createAgent(context) (ddx-api/src/ai/tucan/registry/agent-factory.ts:153).
  3. 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).
  4. LLMConfigFactory.buildDudoxxModel(context.llmConfig) resolves the model from the request context (Swiss clinic's tenant config pointed at dudoxx-gemma).
  5. Tool catalog filtered to categories: [fhir, session, clinical-notes, conditions, utility, assessment, visit] (per the YAML at templates/agents/clinical.agent.yaml).
  6. Jinja2 instructions rendered with the patient's name (German spelling), the org name, the doctor's title.
  7. new Agent({ ... }) constructed with rendered instructions + filtered tools (agent-factory.ts:253); LLM model attached through the context, not explicit on the Agent constructor.
  8. agent:resolved SSE event published with specialistId: 'clinical-specialist'.
  9. 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 — class TemplateLoaderService implements OnModuleInit.
  • ddx-api/src/ai/tucan/registry/template-loader.service.ts:73onModuleInit() enumerates + parses + validates YAML files.
  • ddx-api/src/ai/tucan/registry/template-loader.service.ts:98registerAgents(...) 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 — class AgentFactory implements OnModuleInit.
  • ddx-api/src/ai/tucan/registry/agent-factory.ts:153 — public createAgent(context, options).
  • ddx-api/src/ai/tucan/registry/agent-factory.ts:176createTemplateAgent(...) (YAML-backed path).
  • ddx-api/src/ai/tucan/registry/agent-factory.ts:216createLegacyAgent(...) (code-defined fallback, retained for the tucan agent).
  • ddx-api/src/ai/tucan/registry/agent-factory.ts:253 — actual new Agent({...}) construction — model NOT explicit, inherited through context.llmConfig.
  • ddx-api/src/ai/tucan/registry/agent-factory.ts:326createSpecialistAgent(...).
  • ddx-api/src/ai/tucan/registry/agent-factory.ts:346createNetworkAgent(...).
  • ddx-api/src/ai/tucan/registry/specialist-factory.ts:55 — class SpecialistAgentFactory.
  • ddx-api/src/ai/tucan/registry/specialist-factory.ts:151 — emit agent:resolved event 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 — class NetworkAgentFactory.
  • ddx-api/src/ai/tucan/registry/network-factory.ts:97createSpecialists(...) populates the network with each domain specialist.
  • ddx-api/src/ai/tucan/registry/agent-registry.service.ts:48 — class AgentRegistryService implements OnModuleInit.
  • ddx-api/src/ai/tucan/registry/agent-registry.service.ts:207 — the alt new Agent({...}) construction site (registry-direct path).
  • ddx-api/src/ai/tucan/registry/agent-type.registry.ts:96 — class AgentTypeRegistry — canonical agentType → spec lookup.
  • ddx-api/src/ai/tucan/registry/llm-config.factory.ts:301 — class LLMConfigFactory.buildDudoxxModel(llmConfig) is the model resolution call.
  • ddx-api/src/ai/agent-templates/ — REST surface for tenant template overrides persisted to ddx_api_ai.

Operational Notes

  • NEVER pass an explicit model: to new Agent({...}) in any factory site. Model resolution must flow through context.llmConfigLLMConfigFactory.buildDudoxxModel. Explicit Agent-level model: 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 -u intentionally when the change is expected.
  • categories: [] defaults are different per template. The tucan.agent.yaml template 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-builder are 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 NetworkAgentFactory source 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 to ddx_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.
  • [[tucan-assistant]] — orchestration entry point that calls AgentFactory.createAgent per 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.
    Agent Templates — Configurable AI Agent Factory — Dudoxx Docs | Dudoxx