跳至主要內容

建置 coding agent

本指南將帶你建置一個與 Mastra Code、Claude Code 或 Codex 同類的小型 coding agent 應用程式。你會使用 buildBasePrompt()createCodingAgent() 建立 coding agent,將它包裝在 AgentController 中以支援互動式 session 與 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 提供終端機 renderer 與輸入編輯器。tsx 會直接執行 TypeScript 進入點。

建立 coding agent
「建立 coding agent」的直接連結

建立 src/mastra/agents/coding-agent.ts。prompt 會描述目前的專案,並將 prompt 中的一般 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() 會提供本機 filesystem 與 Sandbox Workspace,以及從暫時性 Provider 錯誤中復原的預設設定。basePath 會限定 filesystem Tool 的作用範圍,並設定命令的初始工作目錄。你傳給 factory 的任何設定都會優先於其預設值。

預設的本機 Sandbox 會直接在 host 上執行命令,不提供隔離,因此 basePath 並非作業系統層級的安全邊界。此範例在終端機中加入逐次 Tool 呼叫核准,但你仍應只針對可信任的本機專案執行。

註冊 coding agent
「註冊 coding agent」的直接連結

src/mastra/index.ts 中,像註冊其他 Mastra Agent 一樣註冊回傳的 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 負責,且不變更檔案。Model 的措辭可能有所不同。

建立 Agent controller
「建立 Agent controller」的直接連結

建立 src/mastra/coding-agent-controller.ts。controller 負責管理互動式 session、公開 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 }
}

此範例使用一種 mode,並停用 controller 的其他內建 Tool,讓入門 UI 能專注於 Workspace 執行。這個簡化設定是為本教學而設計。在正式環境的應用程式中,請啟用產品所需的內建 Tool 並實作其 UI 流程:ask_usersubmit_plan 等互動式 Tool 會暫停,直到介面恢復執行;task 與 subagent Tool 則有各自的生命週期事件。請參閱 Tool 核准與暫停

此範例也省略了 storage,因此對話只會在目前處理程序存續期間保留。日後需要恢復 session 時,可以再加入 storage。

建置終端機 UI
「建置終端機 UI」的直接連結

建立 src/coding-agent-tui.ts。UI 會 render 助理訊息更新並顯示 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 刻意只 render 最新回應;Session 仍會保留對話 context,以供後續 prompt 使用。

執行 coding agent
「執行 coding agent」的直接連結

從專案根目錄啟動終端機應用程式,讓 process.cwd() 指向你要 Agent 使用的專案:

npx tsx src/coding-agent-tui.ts

輸入以下 prompt:

Inspect package.json and report the package name and available scripts. Do not modify files.

當 controller 詢問是否允許 mastra_workspace_read_file 時,輸入 y。Agent 會讀取 package.json 並回報找到的內容。Model 的措辭可能有所不同,但回應應包含 package 名稱及其 scripts,且不變更檔案。

按下 Ctrl+C 關閉應用程式並銷毀 controller。

後續步驟
「後續步驟」的直接連結

你可以在此基礎上進一步擴充:

  • 新增 storage,以保存及恢復 controller session
  • 新增更多具有不同 instructions 與 Workspace Tool allowlist 的 mode
  • 將最新回應元件替換為能 render Tool 呼叫與結果的逐字稿
  • 在接受不可信任的 prompt 或散布應用程式前,加入 Sandbox 隔離

深入瞭解: