跳到主要内容

构建 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-tuitsx

npm install @earendil-works/pi-tui
npm install --save-dev tsx

pi-tui 提供终端渲染器和输入编辑器。tsx 用于直接运行 TypeScript 入口点。

创建 Coding Agent
创建 Coding Agent的直接链接

创建 src/mastra/agents/coding-agent.ts。提示词会描述当前项目,并将提示词中的通用 Tool 名称映射到默认 Workspace 提供的 Tool。

src/mastra/agents/coding-agent.ts
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。

src/mastra/index.ts
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 检查:

src/mastra/public/project-notes.md
# Project notes

Name: Acme support portal
Status: In development
Owner: Platform team

启动开发服务器:

npm 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 以等待审批。

src/mastra/coding-agent-controller.ts
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_usersubmit_plan 等交互式 Tool 会暂停,直到界面将其恢复;任务和子 Agent Tool 则有自己的生命周期事件。请参阅 Tool 审批和暂停

本示例也未配置存储,因此对话只会在当前进程期间保留。需要恢复会话时,可以在之后添加存储。

构建终端 UI
构建终端 UI的直接链接

创建 src/coding-agent-tui.ts。该 UI 会渲染助手消息更新并显示 Tool 活动,还会让用户批准或拒绝每次 Workspace Tool 调用。

src/coding-agent-tui.ts
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 使用的项目:

npx 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 隔离

了解更多: