AgentController
La fonctionnalité AgentController est en version bêta. Elle peut subir des changements incompatibles dans les versions mineures jusqu’à sa sortie de la phase bêta.
AgentController est un environnement d’exécution partagé pour les applications d’agents interactives. Il coordonne les modes, les modèles, le stockage, les Workspace, l’approbation des outils, les sous-agents et les canaux. Chaque utilisateur ou tâche active travaille dans une Session isolée.
Mastra Code est l’implémentation phare d’AgentController. Cet agent de programmation en ligne de commande prend en charge plusieurs modèles, les conversations persistantes et les flux de travail qui séparent planification et exécution. Consultez Créer un agent de programmation pour suivre un guide détaillé.
Quand utiliser AgentControllerLien direct vers Quand utiliser AgentController
Utilisez AgentController lorsque votre application nécessite :
- Plusieurs modes d’agent partageant un même fil de discussion (par exemple, planification → implémentation → révision)
- Une couche de contrôle entre votre interface utilisateur et la boucle de l’agent (changement de modèle, persistance de l’état et gestion des fils de discussion)
- Des processus d’approbation des outils et des politiques d’autorisation pour intégrer une validation humaine
- L’orchestration de sous-agents afin de déléguer des sous-tâches ciblées avec un ensemble d’outils restreint
- Des fils de discussion persistants et certains paramètres associés conservés après les redémarrages, avec un état actif isolé pour chaque Session
Vous pourriez assembler vous-même tous ces éléments à partir de la classe Agent, qui expose l’intégralité de la boucle de l’agent, ses outils et sa mémoire. AgentController fournit des choix par défaut structurants pour une session continue dans laquelle l’agent agit comme un collaborateur plutôt que comme un point de terminaison à usage unique. Utilisez directement la classe Agent si vous souhaitez un contrôle total ou un appel de type requête-réponse. Choisissez AgentController si vous souhaitez bénéficier du modèle de session collaborative sans avoir à construire l’environnement d’exécution correspondant.
Démarrage rapideLien direct vers Démarrage rapide
Créez l’Agent sous-jacent, le stockage et le Workspace. Appelez controller.init() une seule fois, puis utilisez controller.createSession() pour créer une Session. Abonnez-vous avec session.subscribe(), puis envoyez une tâche avec session.sendMessage() :
import { Agent } from '@mastra/core/agent'
import { AgentController } from '@mastra/core/agent-controller'
import { LocalFilesystem, Workspace } from '@mastra/core/workspace'
import { LibSQLStore } from '@mastra/libsql'
const agent = new Agent({
id: 'assistant',
name: 'Assistant',
instructions: 'Help the user plan and complete tasks.',
model: 'openai/gpt-5.6-sol',
})
const controller = new AgentController({
id: 'assistant-controller',
agent,
storage: new LibSQLStore({
id: 'agent-controller-storage',
url: 'file:./mastra.db',
}),
workspace: new Workspace({
id: 'assistant-workspace',
filesystem: new LocalFilesystem({ basePath: './workspace' }),
}),
modes: [
{
id: 'plan',
name: 'Plan',
metadata: { default: true },
instructions: 'Reason about the task before making changes.',
},
{
id: 'build',
name: 'Build',
instructions: 'Implement the approved plan.',
},
],
})
await controller.init()
const session = await controller.createSession({
resourceId: 'user-123',
})
const unsubscribe = session.subscribe(event => {
if (event.type === 'message_update') {
console.log(event.message)
}
})
await session.sendMessage({ content: 'Plan a small TypeScript CLI.' })
unsubscribe()
Réutilisez le même contrôleur pour plusieurs Sessions. Ne stockez pas de Session courante dans le contrôleur et ne transmettez pas les tâches par des méthodes de messagerie au niveau du contrôleur.
Comprendre le modèle d’exécutionLien direct vers Comprendre le modèle d’exécution
Le contrôleur, la Session et le fil de discussion ont des cycles de vie différents :
- Contrôleur : un environnement partagé pour la configuration et les services d’exécution. Initialisez-le une seule fois, puis réutilisez-le.
- Session : un environnement d’exécution actif et isolé pour un utilisateur, une tâche ou un périmètre de travail concurrent. Elle possède le mode, le modèle et l’état actifs, le bus d’événements, l’état d’exécution, les autorisations accordées et l’association au fil de discussion courant.
- Fil de discussion : une conversation stockée qui contient des messages et des paramètres associés. Si vous configurez le stockage, les fils de discussion peuvent subsister après la recréation du contrôleur et du processus.
Une Session représente un état actif. Le contenu arbitraire de session.state, les autorisations accordées, les approbations en attente et les exécutions actives ne subsistent pas automatiquement après la recréation du processus. Le stockage peut conserver les messages du fil de discussion et certains de ses paramètres, notamment le mode et le modèle choisi pour chaque mode.
Sessions et fils de discussionLien direct vers Sessions et fils de discussion
createSession() récupère ou crée une Session à partir de resourceId et, éventuellement, de scope :
const webSession = await controller.createSession({
resourceId: 'user-123',
scope: 'web',
})
const sameWebSession = await controller.createSession({
resourceId: 'user-123',
scope: 'web',
})
const workerSession = await controller.createSession({
resourceId: 'user-123',
scope: 'background-worker',
})
console.log(webSession === sameWebSession) // true
console.log(webSession === workerSession) // false
Les Sessions dont les portées diffèrent possèdent des bus d’événements, des boucles d’exécution, des états, des sélections de mode et de modèle ainsi que des associations au fil de discussion courant distincts. Leurs fils de discussion stockés restent toutefois rattachés au même resourceId.
Transmettez threadId lorsque l’environnement doit associer la Session à un fil de discussion précis. Le contrôleur bascule vers le fil de discussion existant ou, s’il n’existe pas, le crée avec cet ID. Ce comportement s’applique également lorsque createSession() renvoie une Session mise en cache :
const session = await controller.createSession({
resourceId: 'user-123',
scope: 'web',
threadId: 'support-ticket-42',
})
Utilisez session.thread.create() et session.thread.switch() pour faire passer une Session active d’une conversation à une autre.
Changer de mode et de modèleLien direct vers Changer de mode et de modèle
Les modes modifient les instructions et les outils utilisés par l’agent sous-jacent partagé sans remplacer la Session ni le fil de discussion. Configurez dans le contrôleur les outils propres à chaque mode et leur visibilité :
const modes = [
{
id: 'plan',
name: 'Plan',
metadata: { default: true },
instructions: 'Investigate the task and propose a plan.',
additionalTools: { searchDocs },
availableTools: ['searchDocs', 'submit_plan'],
transitionsTo: 'build',
},
{
id: 'build',
name: 'Build',
instructions: 'Implement the approved plan.',
},
]
tools et additionalTools sont deux options mutuellement exclusives pour ajouter des outils propres à un mode. Lorsque le contrôleur possède un agent sous-jacent partagé, chacune de ces options ajoute les outils concernés à ceux de l’agent. Utilisez availableTools pour limiter les noms d’outils finalement exposés dans un mode. Les refus définis par les autorisations restent prioritaires sur cette liste d’autorisation.
Changez le mode de la Session active avec session.mode.switch(). Consultez le mode actif avec session.mode.get() ou session.mode.resolve() :
await session.mode.switch({ modeId: 'build' })
console.log(session.mode.get()) // "build"
console.log(session.mode.resolve().instructions)
Changez de modèle indépendamment avec session.model.switch(), puis consultez la sélection active avec session.model.get(). Les sélections propres au fil de discussion sont stockées par mode et restaurées lorsque la Session revient à ce mode :
await session.model.switch({
modelId: 'anthropic/claude-sonnet-4-6',
scope: 'thread',
})
console.log(session.model.get())
Utilisez scope: 'global' pour une sélection conservée en mémoire qui ne doit pas être inscrite dans les paramètres du fil de discussion.
Gérer les fils de discussion et l’étatLien direct vers Gérer les fils de discussion et l’état
Répertoriez les conversations stockées avec session.thread.list() :
const thread = await session.thread.create({ title: 'Release planning' })
const threads = await session.thread.list()
await session.thread.switch({ threadId: thread.id })
console.log(threads.length)
Utilisez session.state pour l’état actif structuré associé à la Session. Consultez-le avec session.state.get() et mettez-le à jour avec session.state.set(). Définissez stateSchema et initialState dans le contrôleur si vous avez besoin d’une validation et de valeurs par défaut :
console.log(session.state.get())
await session.state.set({ activeProject: 'docs-site' })
session.state.get() renvoie un instantané. set() valide la mise à jour et la fusionne dans l’état de la Session. Considérez cet état comme des données actives de la Session, sauf si votre environnement les conserve et les restaure explicitement.
Approuver les outils et reprendre les suspensionsLien direct vers Approuver les outils et reprendre les suspensions
Les politiques d’autorisation déterminent si un outil est autorisé, refusé ou soumis à l’interface utilisateur pour approbation. Associez les outils personnalisés à des catégories avec toolCategoryResolver dans le contrôleur :
const controller = new AgentController({
toolCategoryResolver: toolName => {
if (toolName === 'delete_project') return 'execute'
return null
},
})
Configurez les politiques par catégorie avec session.permissions.setForCategory() et celles propres aux outils avec session.permissions.setForTool() :
await session.permissions.setForCategory({
category: 'execute',
policy: 'ask',
})
await session.permissions.setForTool({
toolName: 'delete_project',
policy: 'deny',
})
Lorsqu’une politique aboutit à ask, abonnez-vous à l’événement d’approbation et renvoyez la décision de l’utilisateur avec session.respondToToolApproval() :
session.subscribe(event => {
if (event.type === 'tool_approval_required') {
session.respondToToolApproval({
toolCallId: event.toolCallId,
decision: 'approve',
})
}
})
La décision always_allow_category autorise la catégorie d’outils pour toute la durée restante de la Session active. Les autorisations accordées dans une Session ne sont pas des autorisations persistantes au niveau du processus.
Les outils interactifs tels que ask_user et submit_plan utilisent plutôt des suspensions d’outil qui peuvent être reprises. Reprenez leur exécution avec session.respondToToolSuspension() :
session.subscribe(event => {
if (event.type === 'tool_suspended' && event.toolName === 'ask_user') {
void session.respondToToolSuspension({
toolCallId: event.toolCallId,
resumeData: 'Use SQLite.',
})
}
})
Pour submit_plan, reprenez l’exécution avec { action: 'approved' } ou { action: 'rejected', feedback }. Avant la poursuite de l’exécution, un plan approuvé peut déclencher le passage au mode configuré par transitionsTo.
Déléguer à des sous-agentsLien direct vers Déléguer à des sous-agents
Configurez les types de sous-agents disponibles dans le contrôleur. L’outil intégré subagent peut ensuite s’appuyer sur ces définitions pour déléguer des tâches ciblées :
const controller = new AgentController({
tools: {
searchDocs,
},
subagents: [
{
id: 'code-reviewer',
name: 'Code reviewer',
description: 'Review a change for correctness and regressions.',
instructions: 'Inspect the change and report actionable findings.',
allowedControllerTools: ['searchDocs'],
allowedWorkspaceTools: ['view', 'find_files'],
defaultModelId: 'openai/gpt-5-mini',
},
],
})
Un sous-agent standard démarre avec les instructions configurées et l’ensemble d’outils restreint qui lui est attribué. Définissez forked: true lorsque l’enfant doit cloner le fil de discussion parent et s’exécuter avec les instructions et les outils de l’agent parent. Les sous-agents dérivés conservent le préfixe de l’invite parente, ignorent les instructions, les outils, les listes d’autorisation et le modèle par défaut de leur définition, et nécessitent que la mémoire soit configurée dans le contrôleur.
Utilisez session.subagents.model.set() pour stocker un modèle de sous-agent par défaut ou un modèle propre à un type d’agent. Consultez la sélection avec session.subagents.model.get() :
await session.subagents.model.set({
modelId: 'openai/gpt-5-mini',
})
await session.subagents.model.set({
agentType: 'code-reviewer',
modelId: 'anthropic/claude-sonnet-4-6',
})
const reviewerModel = session.subagents.model.get({
agentType: 'code-reviewer',
})
Ces sélections sont enregistrées dans les paramètres du fil de discussion. La sélection propre à un type d’agent est prioritaire sur le modèle de sous-agent par défaut de la Session.
Connecter des canaux de discussionLien direct vers Connecter des canaux de discussion
Transmettez les adaptateurs de canaux au contrôleur, puis enregistrez celui-ci dans une instance de Mastra :
import { Mastra } from '@mastra/core'
import { AgentController } from '@mastra/core/agent-controller'
import { createSlackAdapter } from '@chat-adapter/slack'
const controller = new AgentController({
id: 'support-controller',
agent,
storage,
workspace,
modes,
channels: {
adapters: {
slack: createSlackAdapter(),
},
resolveResourceId: ({ thread, message, defaultResourceId }) => {
if (thread.isDM) return message.author.userId
return defaultResourceId
},
onSessionStart: async ({ session, thread }) => {
const plan = await billing.planFor(thread.resourceId)
await session.model.switch({ modelId: plan.modelId })
},
},
})
export const mastra = new Mastra({
agentControllers: { controller },
storage,
})
Faites pointer le webhook de chaque plateforme vers la route propre au contrôleur :
/api/agent-controllers/<CONTROLLER_ID>/channels/<PLATFORM>/webhook
Chaque fil de discussion externe correspond à une Session du contrôleur et à un fil de discussion Mastra. Par défaut, les nouvelles sessions utilisent un ID de ressource dérivé de l’ID du fil de discussion fourni par l’adaptateur et préfixé par channel:. Utilisez resolveResourceId pour associer les messages directs à un utilisateur existant de l’application ou choisir un autre propriétaire de la mémoire. Cette fonction de rappel ne concerne que les nouveaux fils de discussion ; un fil existant conserve l’ID de ressource stocké.
Les sessions de canal sont créées par le contrôleur et non par votre code : configurez-les donc dans onSessionStart. Cette fonction de rappel s’exécute une seule fois par session, après l’association de celle-ci au fil de discussion correspondant et avant le traitement du premier message. Utilisez-la pour appliquer un modèle, des paramètres de mémoire ou un état de session dont la session de canal ne disposerait pas autrement. Les messages suivants du même fil réutilisent la session sans rappeler cette fonction. Les erreurs sont consignées puis absorbées, afin qu’une session qui ne peut pas être configurée réponde tout de même au message.
Les sessions de canal du contrôleur et l’état d’approbation automatique sont conservés en mémoire : utilisez donc un serveur à longue durée de vie. Les approbations en attente et l’état actif de la Session ne subsistent pas après le redémarrage du processus. Les adaptateurs qui ne peuvent pas afficher de commandes d’approbation exécutent automatiquement les outils sans demander d’approbation, afin que l’exécution ne reste pas suspendue.
Consultez la page Canaux pour configurer les adaptateurs et les webhooks propres à chaque plateforme.
Connecter une interface utilisateurLien direct vers Connecter une interface utilisateur
Abonnez-vous aux événements de la Session pour recevoir les mises à jour incrémentielles. Lorsque l’interface utilisateur a besoin d’un instantané complet pour le rendu, consultez l’état d’affichage agrégé avec session.displayState.get() :
const unsubscribe = session.subscribe(event => {
if (event.type === 'display_state_changed') {
render(event.displayState)
}
})
render(session.displayState.get())
// Call when the UI disconnects.
unsubscribe()
Les abonnements sont isolés par Session. Les événements d’une autre Session du même contrôleur ne sont pas transmis à cet écouteur. Consultez le guide Créer un agent de programmation pour découvrir un exemple complet d’interface utilisateur en mode texte.