Coding Agent を構築する
このガイドでは、Mastra Code、Claude Code、Codex と同じカテゴリに属する小規模な Coding Agent アプリケーションを構築します。buildBasePrompt() と createCodingAgent() で Coding Agent を作成し、対話型セッションと Tool の承認を処理するために AgentController でラップして、pi-tui で構築したターミナル UI から Controller を実行します。
以下の動画では、このガイドで構築する Coding Agent の動作を確認できます。
前提条件前提条件への直接リンク
- Node.js
v22.19.0以降がインストールされていること - サポートされているモデル 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() はローカル Filesystem と Sandbox Workspace に加えて、一時的な Provider エラーから復旧するためのデフォルト設定を提供します。basePath は Filesystem Tool のスコープを制限し、コマンドの初期作業ディレクトリを設定します。Factory に渡した設定は、デフォルト設定より優先されます。
デフォルトのローカル Sandbox は分離を行わず、ホスト上でコマンドを直接実行します。そのため、basePath はオペレーティングシステム上のセキュリティ境界ではありません。この例ではターミナルで Tool 呼び出しごとの承認を追加しますが、信頼できるローカルプロジェクトに対してのみ実行してください。
Coding Agent を登録するCoding Agent を登録するへの直接リンク
返された Agent を、ほかの Mastra Agent と同じように src/mastra/index.ts へ登録します。登録すると、基になる Agent を Studio および Mastra サーバーからも利用できるようになります。
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 }
}
この例ではモードを 1 つだけ使用し、Controller に追加される組み込み Tool を無効化して、入門用 UI が Workspace での実行に集中できるようにしています。この簡略化したセットアップはチュートリアル向けです。本番アプリケーションでは、製品に必要な組み込み Tool を有効化し、それぞれの UI フローを実装してください。ask_user や submit_plan などの対話型 Tool は、インターフェースから再開されるまで一時停止します。一方、タスク Tool と Subagent Tool には固有のライフサイクルイベントがあります。Tool の承認と一時停止を参照してください。
この例では Storage も省略しているため、会話は現在のプロセスが実行されている間だけ保持されます。後から Storage を追加すれば、セッションを再開できます。
ターミナル UI を構築するターミナル UI を構築するへの直接リンク
src/coding-agent-tui.ts を作成します。UI は Assistant メッセージの更新と 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 が破棄されます。
次のステップ次のステップへの直接リンク
この基盤は、次のように拡張できます。
- Storage を追加して Controller セッションを永続化し、再開できるようにする
- 指示と Workspace Tool の許可リストが異なるモードを追加する
- 最新レスポンスだけを表示するコンポーネントを、Tool の呼び出しと結果も表示するトランスクリプトに置き換える
- 信頼できないプロンプトを受け入れたり、アプリケーションを配布したりする前に、Sandbox の分離を追加する
詳細: