构建 Coding Agent
在本指南中,你将构建一个与 Mastra Code、Claude Code 或 Codex 同类的小型 Coding Agent 应用。你会使用 buildBasePrompt() 和 createCodingAgent() 创建 Coding Agent,再用 AgentController 封装它,以支持交互式会话和 Tool 审批,最后在使用 pi-tui 构建的终端 UI 中运行 controller。
下面的视频展示了你将构建的 Coding Agent 的实际运行效果。
前提条件前提条件的直接链接
- 已安装 Node.js
v22.19.0或更高版本 - 受支持的 Model Provider 提供的 API 密钥
- 现有的 Mastra 项目。如有需要,请按照安装指南操作。
安装终端依赖项安装终端依赖项的直接链接
安装 pi-tui 和 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 提供终端渲染器和输入编辑器。tsx 用于直接运行 TypeScript 入口点。
创建 Coding Agent创建 Coding Agent的直接链接
创建 src/mastra/agents/coding-agent.ts。提示词会描述当前项目,并将提示词中的通用 Tool 名称映射到默认 Workspace 提供的 Tool。
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,
})
请将品牌相关值替换为 Agent 的名称和共同作者信息。createCodingAgent() 会提供本地文件系统和 Sandbox Workspace,并附带从临时 Provider 错误中恢复的默认设置。basePath 会限定文件系统 Tool 的作用域,并设置命令的初始工作目录。传给 factory 的任何配置都会优先于其默认值。
默认的本地 Sandbox 直接在宿主机上运行命令,不提供隔离,因此 basePath 并不是操作系统级的安全边界。本示例会在终端中为每次 Tool 调用添加审批,但你仍然应该只针对可信的本地项目运行它。
注册 Coding Agent注册 Coding Agent的直接链接
像注册其他 Mastra Agent 一样,在 src/mastra/index.ts 中注册返回的 Agent。注册后,还可以在 Studio 中以及通过 Mastra server 使用底层 Agent。
import { Mastra } from '@mastra/core/mastra'
import { codingAgent } from './agents/coding-agent'
export const mastra = new Mastra({
agents: { codingAgent },
})
测试 Coding Agent测试 Coding Agent的直接链接
添加终端界面前,请先在 Studio 中验证底层 Agent。开发服务器启动时,其工作目录是 src/mastra/public,因此请在该目录中添加一个不含敏感信息的文件供 Agent 检查:
# Project notes
Name: Acme support portal
Status: In development
Owner: Platform team
启动开发服务器:
- npm
- pnpm
- Yarn
- Bun
npm run dev
pnpm run dev
yarn dev
bun run dev
打开 Studio,选择 Coding Agent,然后输入:
Inspect project-notes.md and report the project name, status, and owner. Do not modify files.
响应应该指出项目是 Acme support portal,并说明其开发状态。它还应该报告项目由 Platform team 负责,且不更改文件。模型的具体措辞可能有所不同。
创建 Agent controller创建 Agent controller的直接链接
创建 src/mastra/coding-agent-controller.ts。controller 负责管理交互式会话、提供 UI 事件,并暂停 Workspace Tool 以等待审批。
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 }
}
本示例只使用一种模式,并禁用 controller 的额外内置 Tool,让入门 UI 可以专注于 Workspace 执行。这项简化设置仅用于本教程。在生产应用中,请启用产品需要的内置 Tool 并实现其 UI 流程:ask_user 和 submit_plan 等交互式 Tool 会暂停,直到界面将其恢复;任务和子 Agent Tool 则有自己的生命周期事件。请参阅 Tool 审批和暂停。
本示例也未配置存储,因此对话只会在当前进程期间保留。需要恢复会话时,可以在之后添加存储。
构建终端 UI构建终端 UI的直接链接
创建 src/coding-agent-tui.ts。该 UI 会渲染助手消息更新并显示 Tool 活动,还会让用户批准或拒绝每次 Workspace Tool 调用。
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()
}
可选的 Terminal 参数支持自动化测试,正常执行时则使用 ProcessTerminal。该 UI 有意只渲染最新响应。Session 仍会保留对话上下文,以供后续提示使用。
运行 Coding Agent运行 Coding Agent的直接链接
从项目根目录启动终端应用,使 process.cwd() 指向要让 Agent 使用的项目:
- 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
输入以下提示词:
Inspect package.json and report the package name and available scripts. Do not modify files.
当 controller 询问是否允许 mastra_workspace_read_file 时,输入 y。Agent 会读取 package.json 并报告其中内容。模型的具体措辞会有所不同,但响应应该包含软件包名称及其脚本,且不会更改文件。
按 Ctrl+C 关闭应用并销毁 controller。
后续步骤后续步骤的直接链接
你可以扩展此基础实现:
- 添加存储,以持久化并恢复 controller 会话
- 添加更多具有不同 instructions 和 Workspace Tool allowlist 的模式
- 将只显示最新响应的组件替换成可渲染 Tool 调用和结果的对话记录
- 在接受不可信提示或分发应用前添加 Sandbox 隔离
了解更多: