メインコンテンツへ移動

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-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() はローカル 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 サーバーからも利用できるようになります。

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 }
}

この例ではモードを 1 つだけ使用し、Controller に追加される組み込み Tool を無効化して、入門用 UI が Workspace での実行に集中できるようにしています。この簡略化したセットアップはチュートリアル向けです。本番アプリケーションでは、製品に必要な組み込み Tool を有効化し、それぞれの UI フローを実装してください。ask_usersubmit_plan などの対話型 Tool は、インターフェースから再開されるまで一時停止します。一方、タスク Tool と Subagent Tool には固有のライフサイクルイベントがあります。Tool の承認と一時停止を参照してください。

この例では Storage も省略しているため、会話は現在のプロセスが実行されている間だけ保持されます。後から Storage を追加すれば、セッションを再開できます。

ターミナル UI を構築する
ターミナル UI を構築するへの直接リンク

src/coding-agent-tui.ts を作成します。UI は Assistant メッセージの更新と 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 が破棄されます。

次のステップ
次のステップへの直接リンク

この基盤は、次のように拡張できます。

  • Storage を追加して Controller セッションを永続化し、再開できるようにする
  • 指示と Workspace Tool の許可リストが異なるモードを追加する
  • 最新レスポンスだけを表示するコンポーネントを、Tool の呼び出しと結果も表示するトランスクリプトに置き換える
  • 信頼できないプロンプトを受け入れたり、アプリケーションを配布したりする前に、Sandbox の分離を追加する

詳細: