Créer un agent de programmation
Dans ce guide, vous allez créer une petite application d’agent de programmation du même type que Mastra Code, Claude Code ou Codex. Vous créerez l’agent de programmation avec buildBasePrompt() et createCodingAgent(), l’encapsulerez dans un AgentController pour gérer les sessions interactives et l’approbation des outils, puis exécuterez le contrôleur dans une interface de terminal conçue avec pi-tui.
La vidéo ci-dessous montre en action l’agent de programmation que vous allez créer.
PrérequisLien direct vers Prérequis
- Node.js
v22.19.0ou version ultérieure installé - Une clé API provenant d’un fournisseur de modèles pris en charge
- Un projet Mastra existant. Si nécessaire, suivez le guide d’installation.
Installer les dépendances du terminalLien direct vers Installer les dépendances du terminal
Installez pi-tui et tsx :
- npm
- pnpm
- Yarn
- Bun
npm install @earendil-works/pi-tui
npm install --save-dev tsx
pnpm add @earendil-works/pi-tui
pnpm add --save-dev tsx
yarn add @earendil-works/pi-tui
yarn add --dev tsx
bun add @earendil-works/pi-tui
bun add --dev tsx
pi-tui fournit le moteur de rendu du terminal et l’éditeur de saisie. tsx exécute directement le point d’entrée TypeScript.
Créer l’agent de programmationLien direct vers Créer l’agent de programmation
Créez src/mastra/agents/coding-agent.ts. Le prompt décrit le projet actuel et associe les noms d’outils génériques du prompt aux outils fournis par le Workspace par défaut.
import { basename } from 'node:path'
import { buildBasePrompt, createCodingAgent } from '@mastra/core/coding-agent'
export const projectPath = process.cwd()
const model = 'openai/gpt-5.6-sol'
const instructions = buildBasePrompt({
projectPath,
projectName: basename(projectPath),
platform: process.platform,
date: new Date().toISOString().slice(0, 10),
mode: 'build',
modelId: model,
productName: 'My Coding Agent',
coAuthorName: 'My Coding Agent',
coAuthorEmail: 'coding-agent@example.com',
toolGuidance: `# Workspace tools
- Use mastra_workspace_read_file for view.
- Use mastra_workspace_list_files for find_files.
- Use mastra_workspace_grep for search_content.
- Use mastra_workspace_execute_command for execute_command.
- Use mastra_workspace_write_file, mastra_workspace_edit_file, and mastra_workspace_file_stat for writing, editing, and inspecting file metadata.
- Use only the workspace tools provided to you. Do not attempt unavailable capabilities.`,
})
export const codingAgent = createCodingAgent({
id: 'coding-agent',
name: 'Coding Agent',
model,
instructions,
basePath: projectPath,
})
Remplacez les valeurs d’identité visuelle par le nom de votre agent et les informations sur le coauteur. createCodingAgent() fournit un système de fichiers local et un Workspace exécuté dans une sandbox, ainsi que des paramètres par défaut permettant de récupérer après des erreurs temporaires du fournisseur. basePath limite la portée des outils du système de fichiers et définit le répertoire de travail initial des commandes. Toute configuration transmise à la fabrique prévaut sur ses valeurs par défaut.
Le sandbox local par défaut exécute les commandes directement sur l’hôte, sans isolation. basePath ne constitue donc pas une frontière de sécurité du système d’exploitation. Cet exemple ajoute dans le terminal une approbation pour chaque appel d’outil, mais vous ne devez malgré tout l’exécuter que sur un projet local de confiance.
Enregistrer l’agent de programmationLien direct vers Enregistrer l’agent de programmation
Enregistrez l’Agent renvoyé comme n’importe quel autre Agent Mastra dans src/mastra/index.ts. Cet enregistrement rend également l’Agent sous-jacent disponible dans Studio et via le serveur Mastra.
import { Mastra } from '@mastra/core/mastra'
import { codingAgent } from './agents/coding-agent'
export const mastra = new Mastra({
agents: { codingAgent },
})
Tester l’agent de programmationLien direct vers Tester l’agent de programmation
Avant d’ajouter l’interface de terminal, vérifiez l’Agent sous-jacent dans Studio. Lorsque le serveur de développement démarre, son répertoire de travail est src/mastra/public. Ajoutez-y donc un fichier non sensible que l’Agent pourra examiner :
# Project notes
Name: Acme support portal
Status: In development
Owner: Platform team
Démarrez le serveur de développement :
- npm
- pnpm
- Yarn
- Bun
npm run dev
pnpm run dev
yarn dev
bun run dev
Ouvrez Studio, sélectionnez Coding Agent, puis saisissez :
Inspect project-notes.md and report the project name, status, and owner. Do not modify files.
La réponse doit identifier le portail d’assistance Acme et décrire son état de développement. Elle doit indiquer que l’équipe Platform est responsable du projet et laisser le fichier inchangé. La formulation peut varier selon le modèle.
Créer le contrôleur de l’AgentLien direct vers Créer le contrôleur de l’Agent
Créez src/mastra/coding-agent-controller.ts. Le contrôleur gère la session interactive, expose les événements de l’interface utilisateur et suspend les outils du Workspace afin d’obtenir leur approbation.
import { AgentController } from '@mastra/core/agent-controller'
import { codingAgent, projectPath } from './agents/coding-agent'
export async function createCodingAgentSession() {
const workspace = await codingAgent.getWorkspace()
if (!workspace) {
throw new Error('The coding agent requires a workspace.')
}
const controller = new AgentController({
id: 'coding-agent-controller',
agent: codingAgent,
workspace,
modes: [{ id: 'build', name: 'Build', metadata: { default: true } }],
disableBuiltinTools: [
'ask_user',
'submit_plan',
'task_write',
'task_update',
'task_complete',
'task_check',
'subagent',
],
})
await controller.init()
const session = await controller.createSession({
id: 'local-session',
ownerId: 'local-user',
resourceId: projectPath,
})
return { controller, session }
}
Cet exemple utilise un seul mode et désactive les outils intégrés supplémentaires du contrôleur afin que l’interface utilisateur d’introduction puisse se concentrer sur l’exécution dans le Workspace. Cette configuration simplifiée est destinée à ce tutoriel. Dans une application de production, activez les outils intégrés dont votre produit a besoin et implémentez leurs flux d’interface utilisateur : les outils interactifs comme ask_user et submit_plan restent suspendus jusqu’à ce que votre interface les reprenne, tandis que les outils de tâche et de sous-agent possèdent leurs propres événements de cycle de vie. Consultez la section sur l’approbation et la suspension des outils.
L’exemple omet également le stockage : la conversation ne dure donc que le temps du processus en cours. Vous pourrez ajouter un stockage ultérieurement lorsque vous souhaiterez reprendre des sessions.
Créer l’interface utilisateur du terminalLien direct vers Créer l’interface utilisateur du terminal
Créez src/coding-agent-tui.ts. L’interface utilisateur affiche les mises à jour des messages de l’assistant ainsi que l’activité des outils. Elle demande également à l’utilisateur d’approuver ou de refuser chaque appel d’outil du Workspace.
import { pathToFileURL } from 'node:url'
import {
Editor,
matchesKey,
ProcessTerminal,
Text,
TUI,
type EditorTheme,
type Terminal,
} from '@earendil-works/pi-tui'
import { createCodingAgentSession } from './mastra/coding-agent-controller'
const plain = (text: string) => text
const editorTheme: EditorTheme = {
borderColor: plain,
selectList: {
selectedPrefix: plain,
selectedText: plain,
description: plain,
scrollInfo: plain,
noMatch: plain,
},
}
function getText(message: { content: Array<{ type: string; text?: string }> }) {
return message.content
.filter(part => part.type === 'text')
.map(part => part.text ?? '')
.join('')
}
export async function startCodingAgentTui(terminal: Terminal = new ProcessTerminal()) {
const { controller, session } = await createCodingAgentSession()
const tui = new TUI(terminal)
const output = new Text('Ask me to inspect or change this project.', 1, 0)
const editor = new Editor(tui, editorTheme)
let busy = false
let pendingApproval: { toolCallId: string; toolName: string } | undefined
const showError = (error: unknown) => {
output.setText(`Error: ${error instanceof Error ? error.message : String(error)}`)
busy = false
pendingApproval = undefined
tui.requestRender()
}
const unsubscribe = session.subscribe(event => {
if (event.type === 'message_update' && event.message.role === 'assistant') {
output.setText(getText(event.message))
} else if (event.type === 'tool_start') {
output.setText(`Running ${event.toolName}...`)
} else if (event.type === 'tool_approval_required') {
pendingApproval = { toolCallId: event.toolCallId, toolName: event.toolName }
output.setText(`Allow ${event.toolName}? Enter y or n.`)
} else if (event.type === 'agent_end') {
busy = false
} else if (event.type === 'error') {
showError(event.error)
return
}
tui.requestRender()
})
editor.onSubmit = value => {
if (pendingApproval) {
const answer = value.trim().toLowerCase()
if (answer !== 'y' && answer !== 'n') {
output.setText(`Allow ${pendingApproval.toolName}? Enter y or n.`)
tui.requestRender()
return
}
const approval = pendingApproval
pendingApproval = undefined
session.respondToToolApproval({
toolCallId: approval.toolCallId,
decision: answer === 'y' ? 'approve' : 'decline',
})
return
}
if (busy || !value.trim()) return
busy = true
output.setText('Thinking...')
tui.requestRender()
void session.sendMessage({ content: value.trim() }).catch(showError)
}
tui.addChild(new Text('My Coding Agent', 1, 0))
tui.addChild(output)
tui.addChild(editor)
tui.setFocus(editor)
let stopPromise: Promise<void> | undefined
let removeInputListener = () => {}
const stop = () => {
stopPromise ??= (async () => {
process.off('SIGINT', handleExit)
removeInputListener()
session.abort()
unsubscribe()
tui.stop()
await controller.destroy()
})()
return stopPromise
}
const handleExit = () => {
void stop().catch(error => {
console.error(error)
process.exitCode = 1
})
}
removeInputListener = tui.addInputListener(data => {
if (!matchesKey(data, 'ctrl+c')) return
handleExit()
return { consume: true }
})
process.once('SIGINT', handleExit)
tui.start()
return { stop }
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
await startCodingAgentTui()
}
Le paramètre facultatif Terminal permet d’effectuer des tests automatisés, tandis que ProcessTerminal est utilisé lors de l’exécution normale. L’interface utilisateur n’affiche volontairement que la réponse la plus récente. La Session conserve néanmoins le contexte de la conversation pour les prompts de suivi.
Exécuter l’agent de programmationLien direct vers Exécuter l’agent de programmation
Démarrez l’application de terminal depuis la racine du projet afin que process.cwd() pointe vers le projet que l’Agent doit utiliser :
- npm
- pnpm
- Yarn
- Bun
npx tsx src/coding-agent-tui.ts
pnpm dlx tsx src/coding-agent-tui.ts
yarn dlx tsx src/coding-agent-tui.ts
bun x tsx src/coding-agent-tui.ts
Saisissez ce prompt :
Inspect package.json and report the package name and available scripts. Do not modify files.
Lorsque le contrôleur demande s’il faut autoriser mastra_workspace_read_file, saisissez y. L’Agent lit package.json et indique ce qu’il y trouve. La formulation varie selon le modèle, mais la réponse doit inclure le nom du package et ses scripts sans modifier le fichier.
Appuyez sur Ctrl+C pour fermer l’application et détruire le contrôleur.
Étapes suivantesLien direct vers Étapes suivantes
Vous pouvez étendre cette base afin de :
- Ajouter un stockage pour conserver et reprendre les sessions du contrôleur
- Ajouter d’autres modes avec des instructions et des listes d’autorisation d’outils du Workspace différentes
- Remplacer le composant de réponse la plus récente par une transcription qui affiche les appels d’outils et leurs résultats
- Ajouter une isolation sandbox avant d’accepter des prompts non fiables ou de distribuer l’application
Pour en savoir plus :