Tampilan
2.3 Siklus Agent-Tool & Error
Tujuan bab
Setelah bab ini kamu bisa:
- Menelusuri siklus agent-tool langkah demi langkah dan menjelaskan apa yang terjadi di tiap titik
- Menangani kegagalan tool sebagai hasil yang bisa diolah agent, bukan sebagai ledakan yang menghentikan semuanya
- Menghentikan tool berisiko sebelum dijalankan dan meminta persetujuan manusia
- Membedakan
requireApprovaldanrequireToolApproval, serta tahu kapan memakai masing-masing - Menjelaskan kenapa persetujuan manusia butuh storage
Prasyarat
Siklus itu, dilihat dari dekat
Di 1.1 kita menyebut agent sebagai loop. Sekarang setelah ada tool, mari lihat isi loop itu dengan lebih teliti.
Dua hal yang perlu benar-benar melekat dari diagram ini.
Model dipanggil dua kali untuk satu pertanyaan. Sekali untuk memutuskan tool apa yang dipanggil, sekali lagi untuk menyusun jawaban dari hasilnya. Kalau agent memanggil tiga tool berurutan, model dipanggil empat kali. Ini penjelasan langsung untuk pertanyaan "kenapa agent saya lambat dan mahal padahal jawabannya pendek".
Hasil tool tidak pergi ke pengguna. Ia kembali ke model. Tool tidak pernah menjawab pertanyaan; ia memberi bahan. Konsekuensi praktisnya: mengembalikan kalimat yang sudah cantik dari tool itu sia-sia — model akan menulis ulang dengan kalimatnya sendiri. Yang berguna dikembalikan adalah data, bukan prosa.
Ketika tool gagal
Tool berinteraksi dengan dunia nyata, dan dunia nyata sering tidak ramah: API mati, jaringan putus, data tidak ditemukan, kredensial kedaluwarsa. Pertanyaannya bukan apakah tool akan gagal, tapi apa yang terjadi saat ia gagal.
Pendekatan yang buruk: membiarkan error naik
ts
// ❌ Kurang baik
execute: async ({ location }) => {
const response = await fetch(`https://api.cuaca.example/${location}`)
const data = await response.json()
return {
location,
temperatureCelsius: data.temp,
conditions: data.desc,
}
}Kalau API mengembalikan 404 untuk kota yang tidak dikenal, data.temp menjadi undefined dan hasilnya gagal divalidasi terhadap outputSchema. Kalau jaringan putus, fetch melempar. Dalam kedua kasus, agent tidak punya kesempatan menjelaskan apa pun kepada pengguna.
Pendekatan yang lebih baik: kegagalan sebagai data
ts
// ✅ Lebih baik
// src/mastra/tools/weather-tool.ts
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'
export const weatherTool = createTool({
id: 'get-weather',
description:
'Ambil kondisi cuaca terkini untuk satu kota. ' +
'Pakai saat pengguna menanyakan cuaca atau suhu saat ini.',
inputSchema: z.object({
location: z.string().describe('Nama kota'),
}),
outputSchema: z.object({
status: z.enum(['ok', 'kota-tidak-ditemukan', 'layanan-gangguan']),
location: z.string(),
temperatureCelsius: z.number().nullable(),
conditions: z.string().nullable(),
pesan: z.string().nullable().describe('Penjelasan bila status bukan ok'),
}),
execute: async ({ location }) => {
try {
const response = await fetch(`https://api.cuaca.example/${location}`)
if (response.status === 404) {
return {
status: 'kota-tidak-ditemukan' as const,
location,
temperatureCelsius: null,
conditions: null,
pesan: `Kota "${location}" tidak ada dalam basis data cuaca.`,
}
}
const data = (await response.json()) as { temp: number; desc: string }
return {
status: 'ok' as const,
location,
temperatureCelsius: data.temp,
conditions: data.desc,
pesan: null,
}
} catch {
return {
status: 'layanan-gangguan' as const,
location,
temperatureCelsius: null,
conditions: null,
pesan: 'Layanan cuaca sedang tidak dapat dihubungi.',
}
}
},
})Yang berubah secara mendasar: kegagalan menjadi bagian dari kontrak outputSchema, bukan pengecualian yang lolos keluar. Karena hasilnya kembali ke model, agent bisa membacanya dan bertindak masuk akal — memberitahu pengguna bahwa kotanya tidak ditemukan, atau menawarkan mencoba lagi nanti, atau memakai tool lain sebagai gantinya.
Bandingkan pengalamannya:
| Pendekatan | Yang dialami pengguna |
|---|---|
| Error dibiarkan naik | Permintaan gagal, atau muncul pesan error teknis |
| Kegagalan sebagai data | "Saya tidak menemukan kota Surabya — mungkin maksud Anda Surabaya?" |
Tiga hal yang membuat pola ini bekerja:
- Field
statusbertipe enum memberi model sinyal yang jelas dan terbatas, bukan teks error yang harus ditafsirkan - Field data bertipe nullable sehingga schema tetap valid saat data tidak tersedia
- Field
pesanmemberi konteks yang bisa disampaikan model kepada pengguna dengan bahasanya sendiri
Jangan kembalikan pesan error teknis mentah
ECONNREFUSED 10.0.3.14:5432 tidak berguna bagi model dan berbahaya bagi pengguna — ia membocorkan detail infrastruktur. Terjemahkan menjadi status yang bermakna, dan catat detail teknisnya lewat logging (8.2).
Menghentikan tool sebelum ia berjalan
Beberapa tindakan terlalu berisiko untuk diserahkan sepenuhnya pada keputusan model: menghapus data, mengirim email, memproses pembayaran, memanggil API pihak ketiga yang mahal.
Mastra menyediakan human-in-the-loop untuk kasus ini. Mekanismenya menghentikan pemanggilan tool sebelum execute berjalan, dan menunggu manusia menyetujui atau menolak.
ts
// src/mastra/tools/delete-tool.ts
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'
export const deleteTool = createTool({
id: 'delete-record',
description: 'Hapus satu record berdasarkan ID',
inputSchema: z.object({ id: z.string() }),
outputSchema: z.object({ deleted: z.boolean() }),
requireApproval: true,
execute: async ({ id }) => {
await db.delete(id)
return { deleted: true }
},
})Satu properti — requireApproval: true — dan tool ini tidak akan pernah berjalan tanpa izin.
Sisi pemanggilnya:
ts
const stream = await agent.stream('Hapus record abc-123')
for await (const chunk of stream.fullStream) {
if (chunk.type === 'tool-call-approval') {
const approved = await agent.approveToolCall({ runId: stream.runId })
for await (const c of approved.textStream) process.stdout.write(c)
}
}Perhatikan stream.fullStream, bukan textStream. Yang pertama memancarkan seluruh peristiwa termasuk tool-call-approval; yang kedua hanya potongan teks. Untuk alur persetujuan, kamu butuh yang pertama.
Potongan tool-call-approval membawa toolCallId, toolName, dan args — cukup untuk menampilkan ke manusia: "Agent ingin menghapus record abc-123. Setujui?" Dari situ kamu memanggil salah satu dari dua method:
ts
await agent.approveToolCall({ runId: stream.runId }) // lanjutkan
await agent.declineToolCall({ runId: stream.runId }) // batalkanDua tingkat kendali
Ada dua bendera yang mengatur ini, dan keduanya digabung dengan logika OR — kalau salah satu bernilai true, panggilan berhenti untuk minta izin.
| Bendera | Ditaruh di | Cakupan |
|---|---|---|
requireApproval: true | Definisi createTool() | Hanya tool itu |
requireToolApproval: true | Opsi stream() / generate() | Semua tool di permintaan itu |
Kapan memakai yang mana:
requireApprovaluntuk sifat yang melekat pada tool itu sendiri. Tool penghapus data selalu berisiko, siapa pun penggunanya.requireToolApprovaluntuk keadaan yang melekat pada permintaan. Misalnya pengguna baru yang belum terverifikasi, atau mode "tinjau dulu" yang diaktifkan penggunanya sendiri.
ts
const stream = await agent.stream('Bagaimana cuaca di London?', {
requireToolApproval: true,
})Persetujuan butuh storage
Dokumentasi menyebutkan ini dengan tegas: human-in-the-loop memakai snapshot untuk menyimpan keadaan permintaan yang tertunda. Tanpa storage provider yang terpasang di instance Mastra, kamu akan menemui error "snapshot not found".
Masuk akal kalau dipikir: antara agent berhenti dan manusia menekan tombol setuju, bisa lewat beberapa menit — dan prosesnya mungkin sudah berpindah atau restart. Keadaan itu harus tersimpan di suatu tempat.
Yang perlu dicatat: snapshot untuk agent bersifat minimal dan sementara — isinya hanya yang diperlukan untuk melanjutkan, dan dihapus setelah proses selesai. Untuk catatan eksekusi pakai tracing, untuk riwayat percakapan pakai memory.
Suspensi dari dalam tool
Selain persetujuan sebelum eksekusi, ada mekanisme kedua: tool yang menghentikan dirinya sendiri di tengah jalan karena menemukan bahwa ia butuh konfirmasi atau data tambahan dari pengguna.
Untuk itu tersedia suspendSchema dan resumeSchema pada createTool(). Pola ini bersaudara dekat dengan suspend/resume pada workflow, dan akan jauh lebih mudah dipahami setelah kamu melihatnya di 4.5. Untuk sekarang cukup tahu bahwa perbedaannya begini:
| Kapan berhenti | Dipicu oleh | |
|---|---|---|
| Pre-execution approval | Sebelum execute jalan | Konfigurasi (requireApproval) |
| Runtime suspension | Di tengah execute | Kode tool-mu sendiri |
Hook siklus hidup tool
createTool() menerima beberapa callback yang berjalan di titik-titik tertentu: onInputStart saat input mulai mengalir, onInputDelta untuk tiap potongan input, onInputAvailable saat input lengkap dan sudah diurai, dan onOutput setelah tool mengembalikan hasil.
Ini berguna untuk menampilkan progres di UI — misalnya menampilkan "sedang mencari…" begitu input tool mulai terbentuk, sebelum tool benar-benar jalan.
Satu perilaku penting yang perlu diketahui: error di dalam hook ditangkap dan dicatat otomatis, tapi tidak menghentikan eksekusi tool. Hook yang melempar akan tercatat di konsol, tapi pemanggilan tool tetap berlanjut. Jangan taruh logika yang menentukan benar-tidaknya sebuah tindakan di dalam hook — taruh di execute.
Kesalahan umum
Gejala: Saat API yang dipanggil tool sedang bermasalah, seluruh permintaan gagal dan pengguna melihat pesan error teknis — bukan penjelasan yang bisa dimengerti. Penyebab: Tool membiarkan exception naik keluar dari execute. Agent tidak pernah menerima informasi apa pun tentang kegagalan itu, jadi tidak bisa menjelaskannya. Perbaikan: Tangkap kegagalan yang bisa diperkirakan dan kembalikan sebagai hasil terstruktur dengan field status bertipe enum. Biarkan agent yang menerjemahkannya menjadi kalimat untuk pengguna.
Gejala: Error "snapshot not found" muncul saat mencoba menyetujui pemanggilan tool. Penyebab: Tidak ada storage provider yang dikonfigurasi di instance Mastra. Human-in-the-loop menyimpan keadaan permintaan yang tertunda sebagai snapshot, dan tanpa storage tidak ada tempat menyimpannya. Perbaikan: Pasang storage adapter di new Mastra({ storage }). Untuk pengembangan lokal, libSQL berbasis file sudah cukup — caranya di 3.1.
Coba sendiri
Tantangan: Kembangkan pustaka-agent dari bab sebelumnya dengan tool keempat: pinjam-buku, yang mencatat peminjaman satu buku oleh satu anggota. Ini tindakan yang mengubah keadaan, jadi ia butuh perlakuan berbeda dari tiga tool sebelumnya.
Ketentuan:
pinjam-bukumemakairequireApproval: true- Tool ini harus menangani minimal tiga kondisi kegagalan sebagai hasil terstruktur, bukan sebagai exception: buku tidak ditemukan, buku sedang dipinjam orang lain, dan anggota masih punya denda belum dibayar
outputSchema-nya memakai fieldstatusbertipez.enum()yang mencakup sukses dan ketiga kegagalan itu- Ubah
cari-bukusupaya mensimulasikan gangguan layanan bila kata kuncinya persis"__error__", dan kembalikan itu sebagai status, bukan exception - Tulis skrip pemanggil yang membaca
fullStream, menampilkan detail permintaan persetujuan ke terminal, lalu menyetujuinya - Jalankan sekali lagi dengan
declineToolCall()dan amati apa yang dikatakan agent kepada pengguna
Checklist penerimaan:
- [ ]
pinjam-bukutidak pernah berjalan sebelum disetujui — buktikan dengan menaruhconsole.logdi dalamexecutedan memastikan ia tidak muncul sampai persetujuan diberikan - [ ] Ketiga kondisi kegagalan menghasilkan jawaban agent yang menjelaskan masalahnya dengan bahasa manusia, bukan pesan error
- [ ] Kata kunci
"__error__"tidak membuat aplikasi jatuh - [ ] Skrip persetujuanmu menampilkan
toolNamedanargssebelum meminta keputusan - [ ] Saat ditolak, agent tetap membalas pengguna dengan sopan alih-alih menggantung
- [ ] Kamu memasang storage di instance Mastra — dan bisa menjelaskan kenapa itu diperlukan
Petunjuk: Ketentuan 6 sering terlewat. Setelah declineToolCall(), agent melanjutkan dengan mengetahui bahwa tindakannya ditolak. Kalau balasannya terasa aneh atau ia malah mencoba lagi, tambahkan aturan di instruksi agent tentang apa yang harus dilakukan saat sebuah tindakan tidak disetujui.
Ikhtisar
- Satu pertanyaan yang melibatkan tool berarti model dipanggil minimal dua kali: sekali memutuskan tool, sekali menyusun jawaban dari hasilnya.
- Hasil tool kembali ke model, bukan ke pengguna. Kembalikan data, bukan prosa — model akan menulis ulang kalimatnya sendiri.
- Tangani kegagalan sebagai hasil terstruktur dengan field
statusbertipe enum dan field data yang nullable, bukan sebagai exception yang lolos keluar. - Jangan kembalikan pesan error teknis mentah — tidak berguna bagi model, berbahaya bagi pengguna.
requireApprovalmenghentikan satu tool tertentu;requireToolApprovalmenghentikan semua tool pada satu permintaan. Keduanya digabung dengan OR.- Alur persetujuan dibaca dari
fullStream, dan dilanjutkan denganapproveToolCall()ataudeclineToolCall()memakairunId. - Human-in-the-loop memerlukan storage. Tanpa itu muncul error "snapshot not found".
- Error di dalam hook siklus hidup dicatat tapi tidak menghentikan eksekusi tool — jangan taruh logika penentu di sana.
Lanjut ke mana
Bagian 2 selesai. Agent-mu sekarang bisa bertindak, gagal dengan anggun, dan meminta izin saat perlu. Tapi ia masih melupakan segalanya begitu percakapan berakhir — dan itu yang diperbaiki Bagian 3 — Memory.