Skip to main content

Unified Agent Platform

Overview

The Unified Agent Platform provides a declarative factory system for building Mastra agents with consistent behavior, centralized configuration, and automatic caching. You declare what your agent needs using the AgentDescriptor pattern, and the platform handles profile loading, model resolution, and caching automatically.

Code: src/mastra/agents/shared/

Benefits

Minimal code - Define agents declaratively with clear interfaces
Consistent behavior - All agents use the same profile loading, model resolution, and override logic
Automatic caching - Profile-hash-based cache invalidation happens automatically
Database validation - Models validated against Core.ModelCapabilities before use
Easy testing - Dependency injection for mocking models and services
Type-safe - Full TypeScript support with clear interfaces

Architecture

The unified platform consists of four core modules:

1. Types (shared/types.ts)

Defines the core contracts:

interface AgentDescriptor {
profileId: string; // Database profile identifier
registryId: string; // Mastra registry ID (can differ from profileId)
defaultName: string; // Fallback name if no profile
defaultPrompt: string; // Fallback prompt if no profile
scopePrefix?: string; // Namespace prefix (e.g., "K12-", "TAP-")
tools?: MastraTool[]; // Agent tools
seedConfig?: Partial<SeedConfigInput>; // Default profile seeding
}

2. Model Utilities (shared/model-utils.ts)

Handles model resolution with database validation:

async function resolveAgentModel(
baseModel: ChatModel,
profile: AgentProfile | null,
agentId: string
): Promise<ResolvedModel>

Resolution Flow:

  1. Check for profile.LlmOverridesJson.deploymentName
  2. Query Core.ModelCapabilities table to validate deployment exists
  3. Use database model name if found (fixes override bugs!)
  4. Apply numeric settings (temperature, maxTokens, topP, frequencyPenalty)
  5. Return resolved model instance

3. Agent Factory (shared/agent-factory.ts)

The core factory class that builds and caches agents:

class AgentFactory implements IAgentFactory {
async getAgent(): Promise<Agent>;
async reload(): Promise<Agent>;
configureForTesting(deps: AgentDependencies): void;
}

Features:

  • Loads current profile from Core.AgentProfiles
  • Seeds default profile if missing (optional)
  • Resolves model using database validation
  • Caches agent instance with profile hash
  • Auto-invalidates cache when profile changes
  • Cleans up old instances on reload

4. Global Configuration

Set up dependencies once for all agents:

import { configureGlobalAgentDeps } from './shared';

// In apps/runtime/src/mastra-runtime.ts
configureGlobalAgentDeps({
getAzureInferenceChatModel,
createAzureInferenceChatModel,
});

Quick Start

1. Define Your Agent Descriptor

// packages/domain-my-feature/src/agents/my-agent.ts
import { AgentDescriptor } from '../shared';
import { myTool } from '../../tools/my-tool';

export const MY_AGENT_DESCRIPTOR: AgentDescriptor = {
profileId: 'my-agent',
registryId: 'my-feature-my-agent',
defaultName: 'My Feature Agent',
defaultPrompt: 'You are an agent that does amazing things...',
scopePrefix: 'MY-FEATURE-',
tools: [myTool],
seedConfig: {
alias: 'My Feature Agent',
shouldSeed: true,
},
};

2. Create and Configure Agent

import { getOrCreateFactory, configureGlobalAgentDeps } from '../shared';

// Optional: Configure specific agent (or use global config)
export function configureMyAgent(deps?: AgentDependencies) {
const factory = getOrCreateFactory(MY_AGENT_DESCRIPTOR);
if (deps) {
factory.configureForTesting(deps);
}
}

// Get agent instance (async)
export async function getMyAgent(): Promise<Agent> {
const factory = getOrCreateFactory(MY_AGENT_DESCRIPTOR);
return factory.getAgent();
}

3. Register in Mastra

// apps/runtime/src/mastra-runtime.ts
import { getMyAgent } from './agents/my-agent';

export const mastra = new Mastra({
agents: {
myAgent: await getMyAgent(),
// ... other agents
},
});

Implementation Pattern

The unified platform uses a declarative approach where you define agent requirements and the factory handles instantiation:

import { AgentDescriptor, getOrCreateFactory } from '../shared';

// Define what your agent needs
export const TESTING_AGENT_DESCRIPTOR: AgentDescriptor = {
profileId: 'testing-agent',
registryId: 'testing-agent',
defaultName: 'Testing Agent',
defaultPrompt: DEFAULT_PROMPT,
scopePrefix: 'TESTING-',
tools: [tool1, tool2],
seedConfig: {
alias: 'Testing Agent',
shouldSeed: true,
},
};

// Get agent instance - factory handles everything
export async function getTestingAgent(): Promise<Agent> {
const factory = getOrCreateFactory(TESTING_AGENT_DESCRIPTOR);
return factory.getAgent();
}

The factory automatically handles:

  • Profile loading from Core.AgentProfiles
  • Default profile seeding (if enabled)
  • Model resolution with database validation
  • Cache management with profile-hash invalidation
  • Error handling and cleanup

Scope Prefixes

Scope prefixes namespace agent instances for multi-tenant scenarios:

Agent TypePrefixExample
K12 AgentsK12-K12-safety-agent
TAP AgentsTAP-TAP-tagging-suggestions-agent
Reasoning EngineREASONING-ENGINE-REASONING-ENGINE-orchestrator
TestingTESTING-TESTING-test-agent

The prefix is prepended to the registryId automatically when creating agent instances.

Profile Management

The platform integrates seamlessly with the Agent Profiles system:

  1. First Load: Factory loads current profile from Core.AgentProfiles
  2. Auto-Seed: If no profile exists and seedConfig.shouldSeed = true, creates default profile
  3. Caching: Caches agent with profile hash
  4. Change Detection: On next load, if profile hash changed, invalidates cache and rebuilds
  5. Hot Reload: Call factory.reload() to force rebuild without restart

Model Override Resolution

The platform provides centralized model resolution with database validation:

// User updates agent profile with new deployment
await adminUpdateAgentProfile({
agentId: 'k12-safety-agent',
llmOverrides: {
deploymentName: 'gpt-5.1',
temperature: 0.7,
maxTokens: 3000,
},
});

// Platform automatically:
// 1. Queries Core.ModelCapabilities for 'gpt-5.1'
// 2. Validates deployment exists and is enabled
// 3. Gets actual model name (e.g., 'gpt-4.1-2025-01-21')
// 4. Creates new model instance with override settings
// 5. Invalidates cache on hash change
// 6. Next getAgent() call returns agent with new model

All model deployments are validated against Core.ModelCapabilities to ensure they exist and are properly configured before use.

Dependency Injection (Testing)

The platform supports dependency injection for testing:

import { configureGlobalAgentDeps } from '../shared';

// Mock model for testing
const mockModel = {
generate: async () => ({ text: 'mocked response' }),
};

// Configure global deps
configureGlobalAgentDeps({
getAzureInferenceChatModel: async () => mockModel,
createAzureInferenceChatModel: (config) => mockModel,
});

// Or configure specific agent
const factory = getOrCreateFactory(MY_AGENT_DESCRIPTOR);
factory.configureForTesting({
getAzureInferenceChatModel: async () => mockModel,
});

// Agent will now use mock model
const agent = await factory.getAgent();

Debugging

Enable debug logging with environment variables:

# Agent factory operations
MASTRA_DEBUG_AGENT_FACTORY=true

# Model resolution details
MASTRA_DEBUG_AGENT_MODELS=true

Sample output:

[AgentFactory] Creating factory for k12-safety-agent (registry: K12-safety-agent)
[AgentFactory] Profile loaded: K12 Safety Agent (hash: a3f5e8...)
[ModelUtils] Resolving model for k12-safety-agent
[ModelUtils] Override deployment: gpt-5.1
[ModelUtils] DB lookup: gpt-5.1 → gpt-4.1-2025-01-21
[ModelUtils] Applying temperature: 0.7
[AgentFactory] Agent cached (k12-safety-agent)

Creating a New Agent

Step 1: Create Descriptor

Define your agent's requirements:

export const MY_AGENT_DESCRIPTOR: AgentDescriptor = {
profileId: 'my-agent',
registryId: 'my-agent',
defaultName: 'My Agent',
defaultPrompt: DEFAULT_PROMPT,
tools: [myTool],
seedConfig: { shouldSeed: true },
};

Step 2: Create Standard Exports

All agents using the unified platform export a standardized set of 5 functions:

import { getOrCreateFactory, type IAgentFactory } from '../shared';

// 1. Cached getter - Primary way to access the agent
export async function getMyAgent(): Promise<Agent> {
return getMyAgentFactory().getAgent();
}

// 2. Factory accessor - For advanced usage (testing, cache management)
export function getMyAgentFactory(): IAgentFactory {
return getOrCreateFactory(MY_AGENT_DESCRIPTOR);
}

// 3. Build fresh - Bypasses cache for diagnostics
export async function buildMyAgentFresh() {
return getMyAgentFactory().buildFresh();
}

// 4. Invalidate cache - Forces rebuild on next access
export function invalidateMyAgentCache(): void {
getMyAgentFactory().invalidateCache();
}

// 5. Configure - Dependency injection for testing
export function configureMyAgent(deps: AgentDependencies): void {
configureGlobalAgentDeps(deps);
}

Why 5 functions?

  • getXAgent() - Standard way to access agents in production code
  • getXAgentFactory() - Direct factory access for advanced scenarios
  • buildXAgentFresh() - Diagnostics and testing without affecting cache
  • invalidateXAgentCache() - Force rebuild when profiles updated externally
  • configureXAgent() - Test dependency injection and setup

Step 3: Export Interface

Export the standard 5 functions for your agent:

export {
MY_AGENT_DESCRIPTOR,
getMyAgent,
getMyAgentFactory,
buildMyAgentFresh,
invalidateMyAgentCache,
configureMyAgent,
};

This standardized interface ensures all agents are accessed consistently across the codebase.

Step 4: Register in Mastra

Add your agent to the Mastra instance:

import { getMyAgent } from './agents/my-feature/my-agent';

export const mastra = new Mastra({
agents: {
myAgent: await getMyAgent(),
},
});

Best Practices

✅ DO

  • Use getOrCreateFactory() for lazy agent loading
  • Call configureGlobalAgentDeps() once in apps/runtime/src/mastra-runtime.ts
  • Provide scopePrefix for multi-tenant scenarios
  • Use descriptive profileId matching database records
  • Enable debug logging during development
  • Leverage dependency injection for testing

❌ DON'T

  • Don't create agents before configureGlobalAgentDeps() is called
  • Don't use top-level await for agent creation (causes circular deps)
  • Don't bypass the factory to create agents directly
  • Don't implement custom caching logic (factory handles it)
  • Don't manually query Core.AgentProfiles (factory does it)
  • Don't cache factories yourself (use getOrCreateFactory())

API Reference

AgentDescriptor

interface AgentDescriptor {
profileId: string;
registryId: string;
defaultName: string;
defaultPrompt: string;
scopePrefix?: string;
tools?: MastraTool[];
seedConfig?: Partial<SeedConfigInput>;
}

getOrCreateFactory()

function getOrCreateFactory(descriptor: AgentDescriptor): IAgentFactory

Gets existing factory or creates new one. Factories are singletons per descriptor.

configureGlobalAgentDeps()

function configureGlobalAgentDeps(deps: AgentDependencies): void

Configures dependencies for all agents. Call once in apps/runtime/src/mastra-runtime.ts.

IAgentFactory

interface IAgentFactory {
getAgent(): Promise<Agent>;
reload(): Promise<Agent>;
configureForTesting(deps: AgentDependencies): void;
}

Real-World Examples

K12 Safety Agent

The K12 safety agent uses the unified platform for profile management and model resolution.

See source (packages/domain-k12/src/agents/k12-safety-agent.ts)

TAP Agents

TAP agents use the platform with the TAP- scope prefix:

  • tap-caption-tagging-agent.ts
  • tap-query-agent.ts

See TAP agents (packages/domain-tap/src/agents/)

Reasoning Engine Agents

Reasoning engine agents use the platform via a shared common module with the REASONING-ENGINE- scope prefix:

See correlation/common.ts (src/mastra/agents/reasoning-engine/correlation/common.ts)