Tampilan
Bagian 10 — Arsitektur & Studi Kasus
Sembilan bagian sebelumnya mengajarkan potongan Mastra: agent, tool, memory, workflow, RAG, dan seterusnya. Masing-masing dijelaskan sendiri-sendiri, dengan contoh kecil yang berdiri sendiri.
Itu jurang yang paling sering membuat orang tersendat. Kamu tahu cara membuat tool. Kamu tahu cara memasang memory. Tapi saat diminta membangun bot WhatsApp untuk 500 pelanggan, pertanyaan pertamanya bukan "bagaimana cara membuat tool" — melainkan:
- Di mana kode webhook-nya ditaruh?
- Satu agent untuk semua orang, atau satu per pelanggan?
- Nomor telepon jadi
threadatauresource? - Kalau dua orang mengirim pesan bersamaan, apa yang terjadi?
- Kalau server restart saat ada yang menunggu balasan?
Tidak satu pun dijawab dengan mengetahui API. Semuanya keputusan arsitektur — dan itulah isi bagian ini.
Cara bagian ini bekerja
Lima belas studi kasus, disusun dari yang paling sederhana ke yang paling kompleks. Tiap kasus memuat:
| Bagian | Isinya |
|---|---|
| Masalahnya | Kebutuhan nyata, bukan latihan buatan |
| Keputusan arsitektur | 2–4 keputusan menentukan, masing-masing dengan pro dan kontra dan alasan pilihan |
| Struktur direktori | Pohon berkas lengkap, siap disalin |
| Diagram sequence | Alur permintaan dari masuk sampai keluar |
| Kode implementasi | Berkas per berkas, bukan potongan menggantung |
| Best practice | Cara menata kodenya agar tetap terbaca setelah enam bulan |
| Kapan pola ini salah | Batas pemakaiannya — sama pentingnya |
Setiap kasus dirancang untuk bisa kamu tiru langsung. Bukan pseudo-code, bukan "dan seterusnya".
Lima keputusan yang selalu muncul
Sebelum masuk kasus, ini kerangka berpikirnya. Hampir semua desain sistem agent adalah kombinasi jawaban atas lima pertanyaan berikut.
1. Agent atau workflow?
Pertanyaan paling menentukan, dan sudah kamu kenal dari 0.1.
| Agent | Workflow | |
|---|---|---|
| Langkahnya | Diputuskan model | Kamu tentukan |
| Hasilnya | Bervariasi | Deterministik |
| Cocok untuk | Percakapan, riset, klasifikasi | Proses bisnis, transaksi |
| Diuji dengan | Scorer | Assertion biasa |
Aturan praktis: kalau salah langkah berarti kerugian uang atau pelanggaran aturan, itu workflow. Kalau salah langkah cuma berarti jawaban kurang bagus, itu boleh agent.
Kebanyakan sistem nyata memakai keduanya: agent di lapisan percakapan, workflow di lapisan eksekusi.
2. Di mana state hidup?
Empat tempat, empat umur yang berbeda:
text
┌──────────────────────────────────────────────────────────┐
│ RequestContext satu permintaan userId, tenantId │
│ Memory (thread) satu percakapan riwayat pesan │
│ Memory (resource) lintas percakapan preferensi user │
│ Storage/DB selamanya pesanan, booking │
└──────────────────────────────────────────────────────────┘Kesalahan paling mahal di sini: menaruh data bisnis di memory. Memory untuk konteks percakapan, bukan untuk sumber kebenaran. Status pesanan tinggal di database; memory hanya boleh mengingat bahwa pengguna pernah menanyakannya.
3. Siapa yang memulai?
| Pemicu | Mekanisme Mastra | Kasus |
|---|---|---|
| Pengguna mengirim pesan | Server + custom route | 10.3, 10.4 |
| Waktu | Schedules (cron) | 10.6 |
| Peristiwa eksternal | Signal providers | 10.7 |
| Sistem lain | MCP / A2A | 10.13 |
Sistem yang hanya bereaksi pada pengguna jauh lebih sederhana daripada yang juga bereaksi pada waktu dan peristiwa. Naikkan kerumitan hanya saat memang dibutuhkan.
4. Sinkron atau asinkron?
Batas praktisnya sederhana: kalau pekerjaan bisa lebih lama dari timeout HTTP, ia tidak boleh sinkron.
text
< 30 detik → request-response biasa
> 30 detik → background task / durable agent
menunggu manusia → workflow suspend
menunggu peristiwa → signal5. Satu pengguna atau banyak?
Ini yang paling sering diabaikan sampai terlambat, dan paling berbahaya.
Begitu ada pengguna kedua, kamu butuh isolasi: resource yang benar, identitas dari sesi terautentikasi, dan storage yang bisa dibagi antar proses. Menambahkannya belakangan berarti memigrasikan data yang sudah tercampur.
Dibahas tuntas di 10.10.
Tiga topologi dasar
Hampir semua kasus di bagian ini adalah variasi dari tiga bentuk berikut.
Topologi A — Agent tunggal
Paling sederhana. Cocok sampai agent punya ~7 tool atau instruksi masih muat satu layar. Kasus 10.1–10.5.
Topologi B — Agent + Workflow
Agent menangani percakapan dan memutuskan kapan proses dijalankan; workflow memastikan prosesnya benar setiap kali. Kasus 10.8, 10.9.
Topologi C — Supervisor + Subagent
Dipakai saat satu agent kewalahan memegang terlalu banyak keahlian sekaligus. Kasus 10.12.
Struktur direktori yang bertahan
Sebelum kasus pertama, ini pola penataan berkas yang dipakai konsisten di seluruh bagian ini — dan alasannya.
text
src/
├── mastra/
│ ├── agents/ satu berkas per agent
│ ├── tools/ satu berkas per tool
│ ├── workflows/ satu berkas per workflow
│ ├── scorers/ penilai mutu
│ └── index.ts titik daftar SEMUA komponen
├── domain/ ← logika bisnis, TANPA Mastra
│ ├── booking.ts
│ └── pricing.ts
├── infra/ ← akses dunia luar
│ ├── db.ts
│ └── whatsapp.ts
└── server/ ← route HTTP, webhook, middleware
└── routes.tsTiga aturan yang membuat struktur ini bertahan:
1. domain/ tidak boleh mengimpor apa pun dari Mastra.
Ini aturan terpenting. Logika bisnis — cara menghitung harga, aturan siapa boleh membatalkan booking — harus bisa diuji tanpa memanggil model sama sekali.
| Untung | Rugi |
|---|---|
| Bisa diuji dengan unit test biasa, cepat dan gratis | Satu lapisan tambahan untuk dilewati |
| Aturan bisnis tidak ikut berubah saat model diganti | Perlu disiplin; mudah tergoda memanggil agent langsung |
| Bisa dipakai ulang di luar konteks agent |
2. Tool tipis, domain tebal.
Tool seharusnya cuma penerjemah: validasi input, panggil domain/, bentuk hasilnya. Kalau execute sebuah tool sudah lebih dari 20 baris, logikanya kemungkinan salah tempat.
ts
// ✅ Tool tipis
execute: async ({ bookingId }) => {
const hasil = await batalkanBooking(bookingId) // logika ada di domain/
return hasil.ok
? { status: 'berhasil', refund: hasil.refund }
: { status: hasil.alasan, refund: null }
}3. index.ts adalah satu-satunya tempat pendaftaran.
Ingat dari 0.2: komponen yang tidak terdaftar tidak dikenali runtime. Menjaga semua pendaftaran di satu berkas membuat "apa saja yang ada di sistem ini" bisa dijawab dengan membuka satu berkas.
Di mana type tinggal?
Pertanyaan yang wajar melihat struktur di atas: kenapa tidak ada direktori types/? Apakah type sebaiknya dipisah dari logika bisnis?
Jawabannya butuh satu pembedaan yang tidak selalu terlihat: di proyek Mastra ada dua jenis "type", dan keduanya punya tempat berbeda.
Dua jenis kontrak
| Zod schema | TypeScript type | |
|---|---|---|
| Berlaku saat | Runtime | Compile-time saja |
| Memeriksa | Data yang benar-benar datang | Kode yang kamu tulis |
| Dipakai di | Batas sistem — tool, step, structured output | Di dalam kodemu sendiri |
| Dikirim ke model? | Ya — jadi bagian prompt | Tidak |
| Hilang saat build? | Tidak | Ya |
Baris terakhir yang paling menentukan. TypeScript type menguap saat kompilasi — ia tidak ada saat program berjalan. Zod schema tetap ada dan benar-benar memeriksa.
Konsekuensinya sederhana:
Di batas sistem, pakai Zod. Di dalam kodemu, pakai TypeScript type.
Batas sistem itu tempat data masuk dari luar kendalimu: argumen dari model, badan permintaan HTTP, respons API pihak ketiga, baris database.
Kenapa itu mendesak dibahas tuntas di 2.2 — lengkap dengan demonstrasi apa yang rusak kalau batasnya bocor.
Lalu type-nya ditaruh di mana?
Default: berdampingan dengan yang memilikinya. Lihat domain/karyawan.ts di 10.2 — ia mengekspor HasilCutidan ambilSisaCuti() dari berkas yang sama.
| Untung colocation | Rugi |
|---|---|
| Type dan logikanya berubah bersamaan, cukup satu berkas | Berkas sedikit lebih panjang |
| Tidak ada berkas yang harus selalu diedit berpasangan | Type tidak terlihat sebagai satu daftar |
| Impor jelas asalnya |
Direktori types/ terpisah punya masalah yang khas: ia jadi berkas yang harus diedit setiap kali apa pun berubah, dan lama-lama menjadi tempat sampah berisi type yang tidak jelas lagi siapa pemiliknya.
Kapan memisahkan type memang benar
Tiga situasi, dan hanya tiga:
| Situasi | Solusi |
|---|---|
| Type dipakai 3 modul atau lebih yang tidak saling impor | domain/types.ts |
| Ada impor melingkar yang harus diputus | Pindahkan type bersama ke berkas ketiga |
| Type mendeskripsikan kontrak lintas layanan | Berkas sendiri, bahkan paket sendiri |
Di luar itu, colocation menang.
Jangan menulis type dua kali
Kesalahan yang sering muncul justru saat orang mencoba merapikan: mendefinisikan Zod schema dan TypeScript type untuk hal yang sama.
ts
// Dua sumber kebenaran yang bisa tidak sinkron
const bookingSchema = z.object({ nama: z.string(), jumlahOrang: z.number() })
export type Booking = {
nama: string
jumlahOrang: number // lupa diubah saat schema berubah
}Turunkan salah satu dari yang lain:
ts
// Satu sumber kebenaran
export const bookingSchema = z.object({
nama: z.string(),
jumlahOrang: z.number().int().min(1).max(20),
})
export type Booking = z.infer<typeof bookingSchema>Arah turunannya pun ada aturannya:
| Arah | Kapan |
|---|---|
Zod lalu z.infer | Bentuk itu melewati batas sistem — tool, step, API |
| TypeScript type saja | Bentuk itu hanya hidup di dalam kodemu |
Jangan memaksakan Zod untuk tipe internal yang tidak pernah menyeberangi batas. Itu menambah biaya runtime tanpa manfaat.
Menerapkannya pada struktur direktori
text
src/
├── domain/
│ ├── booking.ts ← type + fungsi bisnis, berdampingan
│ │ (TypeScript type, tanpa Zod)
│ └── types.ts ← HANYA type yang dibagi 3+ modul
├── mastra/
│ ├── tools/
│ │ └── proses-booking.ts ← Zod di sini
│ │ (batas: model ke kodemu)
│ └── workflows/
│ └── booking-workflow.ts ← Zod per step
└── server/
└── routes.ts ← Zod untuk badan permintaan HTTP
(batas: internet ke kodemu)Perhatikan polanya: Zod muncul di mastra/ dan server/ — tempat data masuk dari luar. domain/ cukup TypeScript type, karena data yang sampai ke sana sudah melewati validasi.
Itu juga alasan sebenarnya kenapa domain/ tidak boleh mengimpor Mastra: ia hidup di dalam batas, bukan di batasnya.
Cara cepat memutuskan
Tanya satu hal: "apakah data ini bisa datang dalam bentuk yang tidak saya duga?"
- Ya, bisa → batas sistem → Zod, lalu turunkan type-nya dengan
z.infer - Tidak → di dalam kodemu → TypeScript type saja, taruh berdampingan dengan fungsinya
Peta 15 studi kasus
Diurutkan dari yang paling sederhana. Kolom terakhir menunjukkan materi mana yang diimplementasikan — setiap bab dari Bagian 0 sampai IX muncul minimal sekali.
| # | Kasus | Topologi | Materi yang dipakai |
|---|---|---|---|
| 10.1 | FAQ Bot Internal | A | Bagian 0, 1.1, 1.2, 1.4 |
| 10.2 | Asisten Data Eksternal | A | 2.1, 2.2, 2.3 |
| 10.3 | Chatbot WhatsApp CS | A | 3.1, 1.3, 9.2 |
| 10.4 | Bot Telegram Personal | A | 3.2, 3.3 |
| 10.5 | Helpdesk Dokumen | A | 5.1, 5.2, 5.3 |
| 10.6 | Laporan Saham Terjadwal | A | 7.2 schedules |
| 10.7 | Alert Harga Kondisional | A | 7.2 signals, 1.4 |
| 10.8 | Antrian Booking | B | 4.1, 4.2, 4.3 |
| 10.9 | Approval Pengeluaran | B | 4.4, 4.5, 2.3 |
| 10.10 | SaaS Multi-tenant | B | 9.1, 9.3, 1.2 dinamis |
| 10.11 | Satu Agent, Ribuan Pengguna | B | 7.1, 9.1, 9.4 |
| 10.12 | Riset Panjang & Subagent | C | 7.3, 7.2 goals, 3.4 |
| 10.13 | Integrasi lewat MCP | C | 6.1, 6.2, 6.3 |
| 10.14 | Coding Agent & Sandbox | C | 7.4, 6.3 ACP |
| 10.15 | Platform Siap Produksi | C | 8.1, 8.2, 8.3, 8.4, 9.4 |
Verifikasi cakupan terbalik
Dibaca dari arah sebaliknya — tiap bab materi dan kasus yang mengimplementasikannya:
| Materi | Diimplementasikan di |
|---|---|
| 0.1–0.3 Orientasi | 10.1 |
| 1.1 Agent · 1.2 Instructions | 10.1, 10.10 |
| 1.3 generate/stream | 10.3 |
| 1.4 Structured output | 10.1, 10.7 |
| 2.1 Tool · 2.2 Schema | 10.2 |
| 2.3 Error & approval | 10.2, 10.9 |
| 3.1 Message history | 10.3 |
| 3.2 Working memory · 3.3 Semantic recall | 10.4 |
| 3.4 Observational memory | 10.12 |
| 4.1–4.3 Workflow & state | 10.8 |
| 4.4 Agent di workflow · 4.5 Suspend/resume | 10.9 |
| 5.1–5.3 RAG | 10.5 |
| 6.1 MCP client · 6.2 MCP server | 10.13 |
| 6.3 A2A & ACP | 10.13, 10.14 |
| 7.1 Durable agents | 10.11 |
| 7.2 Goals · Schedules · Signals | 10.12, 10.6, 10.7 |
| 7.3 Subagents & skills | 10.12 |
| 7.4 Sandbox & browser | 10.14 |
| 8.1–8.4 Observability & eval | 10.15 |
| 9.1 Storage | 10.10, 10.11 |
| 9.2 Server & routes | 10.3 |
| 9.3 Auth | 10.10 |
| 9.4 Deployment | 10.11, 10.15 |
Tidak ada baris kosong. Setiap yang diajarkan punya tempat dipakai.
Cara memakai bagian ini
Kalau kamu belajar berurutan: baca semuanya dari 10.1. Tiap kasus menambahkan satu-dua konsep baru di atas kasus sebelumnya.
Kalau kamu punya kebutuhan spesifik: langsung ke kasus yang paling mirip lewat tabel di atas, lalu baca kasus yang nomornya lebih kecil kalau ada istilah yang belum jelas.
Kalau kamu ingin membangun sesuatu: ambil kasus terdekat, salin struktur direktorinya, ganti domainnya. Bagian "Kapan pola ini salah" akan memberitahu kalau kasus itu ternyata tidak cocok untukmu.
Kasus ini saling menumpuk
Kasus 10.11 (skala besar) mengasumsikan kamu sudah paham 10.3 (webhook) dan 10.10 (multi-tenant). Kalau langsung meloncat ke sana, prasyaratnya disebutkan di awal tiap kasus.
Lanjut ke mana
Mulai dari yang paling sederhana — 10.1 FAQ Bot Internal.