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:
- Check for
profile.LlmOverridesJson.deploymentName - Query
Core.ModelCapabilitiestable to validate deployment exists - Use database model name if found (fixes override bugs!)
- Apply numeric settings (temperature, maxTokens, topP, frequencyPenalty)
- 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 Type | Prefix | Example |
|---|---|---|
| K12 Agents | K12- | K12-safety-agent |
| TAP Agents | TAP- | TAP-tagging-suggestions-agent |
| Reasoning Engine | REASONING-ENGINE- | REASONING-ENGINE-orchestrator |
| Testing | TESTING- | 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:
- First Load: Factory loads current profile from
Core.AgentProfiles - Auto-Seed: If no profile exists and
seedConfig.shouldSeed = true, creates default profile - Caching: Caches agent with profile hash
- Change Detection: On next load, if profile hash changed, invalidates cache and rebuilds
- 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 codegetXAgentFactory()- Direct factory access for advanced scenariosbuildXAgentFresh()- Diagnostics and testing without affecting cacheinvalidateXAgentCache()- Force rebuild when profiles updated externallyconfigureXAgent()- 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 inapps/runtime/src/mastra-runtime.ts - Provide
scopePrefixfor multi-tenant scenarios - Use descriptive
profileIdmatching 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
awaitfor 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.tstap-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)
Related Documentation
- Agent Prompt Overrides - Database-driven prompt management
- TAP Agents - TAP-specific agent implementations
- Reasoning Engine Agents - Reasoning engine agent specs
- Admin Tools - Profile management via admin API