Tampilan
1.3 generate() dan stream()
Kenapa bab ini penting
Pilihan antara kedua method ini tidak mengubah apa yang dijawab agent, tapi sangat mengubah bagaimana rasanya dipakai. Untuk jawaban panjang, selisihnya adalah antara pengguna menatap layar kosong beberapa detik, atau melihat teks mengalir hampir seketika.
Kalau kamu melewatinya: antarmuka chat-mu akan terasa menggantung, dan kamu tidak punya cara mendiagnosis jawaban yang terpotong di tengah.
Kalau kamu tidak butuh: agent-mu dipanggil dari job latar belakang atau workflow, di mana tidak ada manusia yang menunggu di depan layar.
Tujuan bab
Setelah bab ini kamu bisa:
- Memanggil agent dengan
generate()danstream(), dan memilih yang tepat untuk sebuah kebutuhan - Membaca
textStreamsecara bertahap denganfor await - Mengambil properti hasil selain teks:
usage,steps, danfinishReason - Mengirim pesan dalam tiga bentuk — string, array string, dan array objek pesan — serta tahu kapan tiap bentuk berguna
- Membatasi loop agent dengan
maxStepsdanstopWhen
Prasyarat
Satu perbedaan, dua pengalaman
Kedua method memanggil agent yang sama dengan cara kerja internal yang sama. Yang berbeda hanya kapan kamu menerima hasilnya.
generate() ──────────────────────────────────▶ [jawaban utuh]
(menunggu)
stream() ─▶ Un ─▶ tuk ─▶ me ─▶ re ─▶ set ─▶ ... ─▶ [selesai]
(potongan demi potongan)Untuk jawaban satu kalimat, perbedaannya tidak terasa. Untuk jawaban tiga paragraf, generate() berarti pengguna menatap layar kosong selama beberapa detik, sementara stream() mulai menampilkan kata pertama hampir seketika.
Waktu total sampai selesai kurang lebih sama. Yang berubah adalah waktu sampai tanda kehidupan pertama, dan bagi pengguna itu perbedaan antara "cepat" dan "menggantung".
Diukur sungguhan
Pertanyaan yang sama — "Jelaskan cara kerja cache pada aplikasi web" dalam 3 paragraf — dijalankan dengan kedua method memakai openai/gpt-5-mini:
text
generate() : karakter pertama muncul setelah 6674 ms
(yaitu saat SELURUH jawaban selesai)
total 1797 karakter
stream() : karakter pertama muncul setelah 3069 ms
selesai seluruhnya setelah 8670 ms, total 2174 karakterDua hal yang perlu dibaca dari angka ini, dan yang kedua sering mengejutkan:
- Pengguna menunggu 6674 ms vs 3069 ms sebelum melihat apa pun. Lebih dari dua kali lipat.
stream()justru selesai lebih lambat — 8670 ms vs 6674 ms.
Jadi streaming tidak membuat agent lebih cepat. Ia memindahkan penantian: dari "diam total lalu semuanya muncul" menjadi "mulai cepat lalu mengalir". Untuk manusia yang menunggu, yang kedua terasa jauh lebih baik meski totalnya lebih lama.
Dari pemanggilan yang sama, usage mengungkap sesuatu yang tidak terlihat:
text
finishReason : stop
usage : { inputTokens: 35, outputTokens: 879, totalTokens: 914,
reasoningTokens: 384 }384 dari 879 token keluaran adalah reasoning — penalaran internal model yang tidak pernah muncul di layar, tapi tetap kamu bayar. Ini alasan konkret kenapa usage layak dicatat, bukan diabaikan.
generate()
ts
// run.mjs
import { mastra } from './src/mastra/index.ts'
const agent = mastra.getAgentById('support-agent')
const result = await agent.generate('Bagaimana cara reset password saya?')
console.log(result.text)Method ini menunggu sampai seluruh proses selesai, lalu mengembalikan hasil lengkapnya sekaligus. Pakai ini saat:
- Hasilnya diproses program, bukan ditampilkan ke manusia
- Kamu berada di dalam job latar belakang atau workflow
- Kamu butuh keluaran terstruktur yang harus utuh dulu sebelum berguna (lihat 1.4)
stream()
ts
// run.mjs
import { mastra } from './src/mastra/index.ts'
const agent = mastra.getAgentById('support-agent')
const stream = await agent.stream([
{ role: 'user', content: 'Bantu saya menyusun rencana hari ini' },
])
for await (const chunk of stream.textStream) {
process.stdout.write(chunk)
}stream() mengembalikan objek MastraModelOutput. Properti textStream adalah readable stream yang memancarkan potongan teks; for await mengonsumsinya sepotong demi sepotong.
Perhatikan process.stdout.write() alih-alih console.log() — yang kedua menambahkan baris baru di tiap potongan, sehingga jawaban akan tercetak menurun satu kata per baris.
Yang bisa diambil dari sebuah stream
Ini bagian yang sering terlewat: stream() bukan hanya soal teks bertahap. Objek yang dikembalikan menyediakan sejumlah properti, sebagian berupa stream dan sebagian berupa promise yang selesai saat proses berakhir.
| Properti | Jenis | Isi |
|---|---|---|
textStream | stream | Potongan teks saat dihasilkan |
text | promise | Teks lengkap setelah selesai |
steps | promise | Langkah-langkah model yang telah dijalankan |
finishReason | promise | Alasan agent berhenti |
usage | promise | Informasi pemakaian token |
objectStream, object | stream, promise | Keluaran terstruktur, parsial dan final |
Artinya kamu bisa melakukan keduanya sekaligus — menampilkan teks secara bertahap ke pengguna, lalu setelah selesai mengambil data pemakaian token untuk dicatat:
ts
// run.mjs
import { mastra } from './src/mastra/index.ts'
const agent = mastra.getAgentById('support-agent')
const stream = await agent.stream('Jelaskan cara kerja cache dalam 3 paragraf')
for await (const chunk of stream.textStream) {
process.stdout.write(chunk)
}
const usage = await stream.usage
const finishReason = await stream.finishReason
console.log('\n---')
console.log('Alasan berhenti:', finishReason)
console.log('Pemakaian token:', usage)finishReason layak diperhatikan sejak awal. Nilainya memberitahu kenapa agent berhenti — misalnya karena memang selesai, karena menabrak batas panjang, atau karena masih menunggu hasil tool. Saat jawaban terpotong di tengah kalimat, ini properti pertama yang harus kamu periksa.
Tiga bentuk pesan
Kedua method menerima pesan dalam beberapa bentuk:
ts
// 1. String tunggal — paling ringkas
await agent.generate('Bantu saya menyusun rencana hari ini')
// 2. Array string — beberapa potong konteks
await agent.generate([
'Saya punya rapat jam 10 dan jam 14.',
'Saya perlu menyiapkan presentasi.',
'Bagaimana sebaiknya saya susun hari ini?',
])
// 3. Array objek pesan — kontrol penuh atas peran
await agent.generate([
{ role: 'user', content: 'Saya perlu bantuan menyusun jadwal' },
{ role: 'assistant', content: 'Tentu. Apa saja agenda Anda hari ini?' },
{ role: 'user', content: 'Dua rapat dan satu presentasi' },
])Bentuk ketiga memberi kendali penuh atas peran dan alur percakapan. Ini berguna saat kamu mengelola riwayat sendiri — misalnya memuat percakapan dari database milikmu.
Perlu dicatat: kalau yang kamu butuhkan adalah agent yang mengingat percakapan sebelumnya secara otomatis, jangan menyusun riwayat manual seperti ini. Itu tugas memory, dan dibahas di Bagian 3.
Membatasi loop
Ingat dari 1.1 bahwa agent adalah loop. Loop tanpa batas bisa berputar lebih lama dari yang kamu mau. Dua opsi mengendalikannya:
ts
const result = await agent.generate('Riset topik ini dan rangkum', {
maxSteps: 5,
})maxSteps membatasi jumlah langkah maksimum selama eksekusi. Sederhana dan biasanya cukup.
Untuk kondisi berhenti yang lebih spesifik — misalnya berdasarkan batas token, bukan hanya jumlah langkah — tersedia stopWhen.
Ada juga onIterationComplete, sebuah callback yang dipanggil setiap kali satu iterasi selesai. Ia menerima konteks berisi teks iterasi saat ini, tool apa yang dipanggil, dan alasan iterasi itu berakhir — lalu boleh mengembalikan continue: false untuk menghentikan lebih awal, atau feedback berupa pesan untuk mengarahkan iterasi berikutnya. Ini alat yang berguna saat kamu ingin mengawasi agent yang berjalan panjang, dan akan lebih masuk akal setelah kamu melihat trace di Bagian 8.
Memilih di antara keduanya
Kesalahan umum
Gejala: Jawaban tercetak menurun, satu kata atau satu potongan per baris, padahal seharusnya menyambung jadi paragraf. Penyebab: Memakai console.log() di dalam loop for await. Setiap pemanggilan menambahkan baris baru. Perbaikan: Pakai process.stdout.write(chunk) untuk keluaran terminal. Di antarmuka web, tambahkan potongan ke state teks yang sudah ada, bukan merender tiap potongan sebagai elemen terpisah.
Gejala: Jawaban berhenti di tengah kalimat, dan tidak ada error apa pun yang muncul. Penyebab: Agent berhenti karena alasan selain "selesai" — biasanya menabrak batas panjang keluaran, atau menyentuh batas langkah yang kamu tetapkan lewat maxSteps. Karena ini bukan error, tidak ada yang dilempar. Perbaikan: Periksa finishReason. Kalau nilainya menunjukkan batas panjang, perpendek permintaan atau naikkan batasnya; kalau menunjukkan batas langkah, naikkan maxSteps — tapi periksa dulu lewat trace apakah agent memang butuh langkah sebanyak itu, atau sedang berputar-putar karena instruksi yang ambigu.
Coba sendiri
Tujuannya: mengukur sendiri selisih waktu tunggu, bukan mempercayai angka di bab ini.
Kode awal
Salin ke demo/uji-stream.mjs, lalu jalankan node demo/uji-stream.mjs.
js
// demo/uji-stream.mjs
import { Agent } from '@mastra/core/agent'
const agent = new Agent({
id: 's', name: 'S',
instructions: 'Kamu penjelas teknis. Jawab dalam 3 paragraf.',
model: 'openai/gpt-5-mini',
})
const PESAN = 'Jelaskan cara kerja cache pada aplikasi web.'
// --- generate() ---
let t0 = Date.now()
const g = await agent.generate(PESAN)
const tGenerate = Date.now() - t0
console.log(`generate() : ${tGenerate} ms sampai karakter pertama (= sampai selesai)`)
// --- stream() ---
t0 = Date.now()
const s = await agent.stream(PESAN)
let tFirst = null
for await (const chunk of s.textStream) {
// TODO 1: catat waktu HANYA pada potongan pertama.
// Petunjuk: `if (tFirst === null) tFirst = Date.now() - t0`
}
const tTotal = Date.now() - t0
console.log(`stream() : ${tFirst} ms sampai karakter pertama`)
console.log(` ${tTotal} ms sampai selesai`)
// TODO 2: cetak finishReason dan usage. Keduanya PROMISE — pakai await.
// TODO 3: dari usage, cetak berapa persen token keluaran yang berupa reasoning.Bentuk hasil yang diharapkan
Angka di mesinmu akan berbeda — yang harus muncul adalah polanya:
text
generate() : 6674 ms sampai karakter pertama (= sampai selesai)
stream() : 3069 ms sampai karakter pertama
8670 ms sampai selesai
finishReason: stop
reasoning : 384 dari 879 token keluaran (44%)Selesai kalau
- [ ] Waktu karakter pertama
stream()jauh lebih kecil darigenerate() - [ ] Waktu total
stream()tidak lebih cepat — biasanya justru sedikit lebih lama - [ ] Kamu bisa menjelaskan kenapa keduanya benar sekaligus
- [ ]
finishReasonbernilaistop - [ ] Kamu tahu berapa persen token keluaranmu terpakai untuk reasoning
Tantangan lanjutan
Ubah instruksi menjadi 'Jawab dalam SATU kalimat.' lalu jalankan ulang. Selisih waktu karakter pertama akan menyusut drastis — dan itu menjelaskan kenapa stream() tidak selalu sepadan.
Kalau macet
tFirst bernilai null di akhir? Blok for await-mu kosong, atau kondisinya salah. Ia harus mengisi tFirst pada iterasi pertama saja.
finishReason mencetak Promise { <pending> }? Ia promise. Tulis await s.finishReason, bukan s.finishReason.
Teks tercetak menurun satu kata per baris? Kamu memakai console.log di dalam loop. Pakai process.stdout.write(chunk).
Ikhtisar
generate()danstream()menjalankan proses yang sama; yang berbeda adalah kapan hasil sampai ke tanganmu.- Pakai
stream()saat hasilnya dibaca manusia secara langsung dan berpotensi panjang; pakaigenerate()saat hasilnya diproses program, di dalam workflow, atau berupa keluaran terstruktur. stream()mengembalikanMastraModelOutput— bukan hanyatextStream, tapi juga promisetext,steps,finishReason,usage, danobject/objectStream.finishReasonadalah properti diagnostik pertama saat jawaban terpotong atau berhenti tak terduga.- Pesan bisa dikirim sebagai string, array string, atau array objek pesan. Bentuk terakhir untuk kendali penuh — tapi riwayat otomatis adalah tugas memory, bukan tugasmu.
- Loop dibatasi dengan
maxStepsataustopWhen, dan diawasi denganonIterationComplete.
Lanjut ke mana
Sejauh ini keluaran agent selalu berupa teks. Untuk dipakai program, teks harus diurai dan diverifikasi — kecuali kalau kamu memintanya berbentuk objek sejak awal: 1.4 Structured Output.