@chispear/vibekit 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,6 +47,19 @@ Isi `apiKey` tiap penyedia (atau gunakan sintaks `"env:NAMA_VAR"` untuk membaca
47
47
 
48
48
  Konfigurasi per proyek: file `./vibekit.config.json` di folder proyek **menimpa** konfigurasi global, jadi tiap proyek bisa punya susunan penyedia sendiri.
49
49
 
50
+ ### Mengatur Plafon Jawaban (`gaya.maksTokenJawaban`)
51
+
52
+ `maksTokenJawaban` adalah **plafon** panjang satu respons model (dikirim sebagai `max_tokens`), bukan target: panjang jawaban sebenarnya diatur oleh `gaya.jawaban`, jadi plafon besar tidak otomatis boros. Plafon hanya berfungsi sebagai rem pengaman — tanpa batas sama sekali, tidak ada yang menghentikan model yang bertele-tele (atau menulis file tanpa henti) selain batas bawaan model itu sendiri.
53
+
54
+ | Nilai | Cocok untuk |
55
+ |---|---|
56
+ | `800` (bawaan template) | Tanya-jawab ringan, pertanyaan singkat |
57
+ | `4000` | Kerja agent dan file berukuran sedang |
58
+ | `8000`–`16000` | Membangun aplikasi, menghasilkan file besar |
59
+ | `0` (atau hapus field-nya) | **Tanpa batas** — `max_tokens` tidak dikirim sama sekali; batas bawaan model yang berlaku |
60
+
61
+ Penting untuk mode agent: isi file yang ditulis model lewat tool `tulis_file`/`edit_file` **ikut dihitung dalam plafon respons yang sama**. File yang lebih besar dari plafon akan terpotong di tengah dan gagal ditulis — bila Anda menyuruh agent membangun aplikasi atau file besar, naikkan plafonnya (atau set `0`).
62
+
50
63
  Lalu:
51
64
 
52
65
  ```bash
@@ -61,9 +74,13 @@ vibekit status
61
74
  | Perintah | Fungsi |
62
75
  |---|---|
63
76
  | `vibekit init` | Buat `~/.vibekit/config.json` dari template (tidak menimpa yang sudah ada) |
64
- | `vibekit ask "<pertanyaan>" [--file <path>]... [--dir <path>]...` | Satu pertanyaan yang boleh berakhir dengan aksi; lampirkan file/folder sebagai konteks terseleksi. Ringkasan hemat token ditulis ke stderr, jawaban bersih di stdout, ditutup baris info model + token terpakai |
65
- | `vibekit chat` | Percakapan interaktif yang juga bisa bertindak di tengah obrolan; riwayat dipangkas otomatis dan sadar-tool (maks ±4.000 token, hanya pada batas giliran utuh); `/keluar` untuk berhenti |
66
- | `vibekit do "<tugas>"` | Mode agent sekali jalan: model mengerjakan tugas nyata dengan tool file/perintah di folder saat ini (lihat bagian Mode Agent di bawah) |
77
+ | `vibekit ask "<pertanyaan>" [--file <path>]... [--dir <path>]... [--auto] [--tanpa-brief]` | Satu pertanyaan yang boleh berakhir dengan aksi; lampirkan file/folder sebagai konteks terseleksi. Ringkasan hemat token ditulis ke stderr, jawaban bersih di stdout, ditutup baris info model + token terpakai |
78
+ | `vibekit chat [--auto] [--lanjut] [--tanpa-brief]` | Percakapan interaktif yang juga bisa bertindak di tengah obrolan; riwayat aksi lama dipadatkan jadi ringkasan lalu dipangkas sadar-tool (maks ±4.000 token, hanya pada batas giliran utuh); `/keluar` untuk berhenti, `/auto` untuk menyala-matikan mode auto-allow, `/sesi` menampilkan id sesi aktif. Sesi tersimpan otomatis dan bisa dilanjutkan dengan `--lanjut` |
79
+ | `vibekit do "<tugas>" [--auto] [--rencana] [--tanpa-brief]` | Mode agent sekali jalan: model mengerjakan tugas nyata dengan tool file/perintah di folder saat ini (lihat bagian Mode Agent di bawah). `--rencana`: susun rencana dulu, persetujuan SATU kali, lalu eksekusi |
80
+ | `vibekit mcp` | Sambungkan semua server MCP dari konfigurasi dan daftar tool yang mereka sediakan (lihat bagian Dukungan MCP di bawah) |
81
+ | `vibekit brief` | Susun ringkasan konteks proyek ke `.vibekit/brief.md`; brief dipakai otomatis sebagai konteks pada ask/chat/do (lihat bagian Brief Proyek) |
82
+ | `vibekit hemat` | Laporan pemakaian token per penyedia (hari ini & 7 hari terakhir) + token yang terukur dihemat kompaksi riwayat |
83
+ | `vibekit sesi` | Daftar sesi chat yang tersimpan (untuk dilanjutkan dengan `chat --lanjut`) |
67
84
  | `vibekit status` | Tabel per penyedia: model, status (aktif / cooldown s.d. jam / anggaran tercapai), token terpakai hari ini |
68
85
  | `vibekit --help` / `--version` | Bantuan / versi |
69
86
 
@@ -93,13 +110,116 @@ Model bekerja langkah demi langkah lewat tool calling (API OpenAI-compatible yan
93
110
 
94
111
  - **Hanya folder kerja (cwd).** Semua path di-resolve terhadap folder tempat perintah dijalankan; path yang keluar dari folder itu (mis. `../`) ditolak eksekutor, bahkan bila pengguna menyetujui.
95
112
  - **Konfirmasi per aksi.** Setiap aksi yang mengubah file atau menjalankan perintah menampilkan deskripsi aksinya (path + pratinjau konten untuk tulis, potongan teks lama→baru untuk edit, perintah lengkap untuk shell) dan hanya berjalan bila Anda menjawab `y`/`ya` — selain itu dianggap menolak, dan penolakan itu dilaporkan ke model agar ia menyesuaikan rencana.
113
+ - **Mode auto-allow (opsional, sejak v0.4.0).** Flag `--auto` (alias `--yes`) pada `ask`, `chat`, dan `do` — atau perintah `/auto` di tengah sesi chat — melewati konfirmasi per aksi: semua aksi tulis/perintah langsung dijalankan. Pagar folder kerja TIDAK ikut dilepas (path traversal tetap ditolak), tetapi risikonya nyata: model bisa menulis atau menjalankan sesuatu tanpa sempat Anda periksa. Hanya aktifkan di folder yang aman (folder uji, proyek sekali pakai) dan untuk tugas yang Anda percayai.
96
114
  - **Batas 12 langkah** per giliran/tugas, dan hasil tiap tool dipangkas (±4.000 karakter) sebelum dikirim balik ke model demi hemat token.
97
115
  - **Riwayat chat yang aman untuk tool.** Pemangkasan riwayat chat hanya terjadi pada batas giliran utuh (satu pesan pengguna + seluruh rangkaian tool-nya), sehingga tidak pernah ada pesan hasil tool yang yatim atau panggilan tool yang kehilangan hasilnya.
98
116
 
117
+ ## Dukungan MCP (Model Context Protocol)
118
+
119
+ Sejak v0.5.0, agent VibeKit bisa memakai **tool dari server MCP mana pun**. VibeKit berperan sebagai MCP *client*: daftarkan server lewat field `mcpServers` di konfigurasi, dan tool mereka tampil **berdampingan dengan 5 tool bawaan** pada `ask`, `chat`, dan `do`. Nama tool ter-namespace menjadi `mcp__<namaServer>__<namaTool>` agar tidak bertabrakan antar server maupun dengan tool bawaan.
120
+
121
+ Ada dua transport:
122
+
123
+ - **stdio** — server lokal dijalankan sebagai proses anak lewat `perintah` + `argumen` (pesan JSON-RPC per baris/NDJSON). Cocok untuk server MCP komunitas yang umumnya didistribusikan sebagai perintah `npx`/`node`/`python`.
124
+ - **HTTP (Streamable HTTP)** — server remote dipanggil lewat POST JSON-RPC ke `url`; header tambahan (mis. API key) diatur lewat `headers`. Jawaban JSON biasa maupun aliran SSE sama-sama didukung, dan `Mcp-Session-Id` dari server diteruskan antar-request.
125
+
126
+ ### Contoh: server lokal (stdio)
127
+
128
+ ```json
129
+ {
130
+ "mcpServers": [
131
+ {
132
+ "nama": "filesystem",
133
+ "perintah": "npx",
134
+ "argumen": ["-y", "@modelcontextprotocol/server-filesystem", "D:/proyek-saya"]
135
+ }
136
+ ]
137
+ }
138
+ ```
139
+
140
+ ### Contoh: server remote (HTTP) — Google Stitch
141
+
142
+ Stitch (alat desain UI Google) menyediakan server MCP remote. Contoh konfigurasinya:
143
+
144
+ ```json
145
+ {
146
+ "mcpServers": [
147
+ {
148
+ "nama": "stitch",
149
+ "url": "https://stitch.googleapis.com/mcp",
150
+ "headers": { "X-Goog-Api-Key": "ISI_API_KEY_STITCH_MU" }
151
+ }
152
+ ]
153
+ }
154
+ ```
155
+
156
+ > **Catatan tegas:** contoh di atas hanya pola konfigurasinya. Anda memerlukan **akses dan API key Stitch milik Anda sendiri** — nilai `ISI_API_KEY_STITCH_MU` adalah placeholder yang harus diganti key Anda (atau simpan key di tempat aman dan jangan commit file konfigurasi yang sudah terisi). Hal yang sama berlaku untuk server remote lain: header autentikasi selalu kredensial Anda sendiri.
157
+
158
+ ### Melihat tool yang tersedia
159
+
160
+ ```bash
161
+ vibekit mcp
162
+ ```
163
+
164
+ Perintah ini menyambungkan semua server di `mcpServers`, mencetak daftar tool per server beserta deskripsi singkatnya, lalu memutuskan sambungan. Saat `ask`/`chat`/`do` berjalan, satu baris `MCP <nama>: N tool` dicetak per server yang berhasil tersambung; server yang gagal cukup dicatat sebagai peringatan — server lain tetap jalan.
165
+
166
+ ### Keamanan tool MCP
167
+
168
+ Tool MCP adalah **aksi pihak ketiga** yang dijalankan atas nama Anda, jadi perlakuannya sama dengan tool mutasi bawaan: **selalu meminta konfirmasi Anda satu per satu** (pratinjau menampilkan nama tool + argumennya), dan penolakan dilaporkan ke model. Mode auto-allow (`--auto`/`--yes`/`/auto`) melewati konfirmasi itu juga untuk tool MCP — aktifkan hanya bila Anda mempercayai server MCP yang terdaftar dan tugasnya. Sambungkan hanya server MCP yang Anda kenal sumbernya.
169
+
170
+ ## Laporan Pemakaian — `vibekit hemat`
171
+
172
+ Menampilkan pemakaian token per penyedia untuk **hari ini** dan **7 hari terakhir**, ditambah token yang **terukur** dibuang oleh kompaksi riwayat chat (hari ini + total tercatat):
173
+
174
+ ```bash
175
+ vibekit hemat
176
+ ```
177
+
178
+ Soal kejujuran angka: hanya penghematan kompaksi yang diangkakan, karena memang bisa dihitung persis dari konten yang dibuang. Penghematan dari gaya jawaban ringkas dan konteks terseleksi bersifat *desain* (mencegah token terpakai sejak awal) dan dinyatakan sebagai catatan, bukan angka karangan.
179
+
180
+ ## Streaming Jawaban
181
+
182
+ Jawaban pada `ask` dan `chat`, serta ringkasan akhir `do`, **dialirkan (streaming)**: teks tampil potongan demi potongan begitu model menulisnya, bukan menunggu selesai semua. Aturan failover-nya:
183
+
184
+ - Error HTTP **sebelum token pertama** (429 kuota, 503, dll.) diperlakukan persis seperti mode biasa: retry/cooldown/failover ke penyedia berikutnya.
185
+ - Error **di tengah aliran, setelah sebagian jawaban tampil**, TIDAK di-failover — mengulang ke penyedia lain akan menggandakan keluaran. Giliran dinyatakan gagal dengan pesan khusus, dan di chat giliran itu digulung balik seperti kegagalan lain.
186
+ - Pemakaian token tetap tercatat: dari `usage` di akhir aliran bila penyedia mengirimnya, atau estimasi tokenizer lokal atas teks yang terakumulasi.
187
+
188
+ ## Mode Rencana — `vibekit do "<tugas>" --rencana`
189
+
190
+ Untuk tugas besar yang ingin Anda periksa dulu arahnya:
191
+
192
+ ```bash
193
+ vibekit do "rapikan struktur folder src dan perbarui README" --rencana
194
+ ```
195
+
196
+ Alurnya: (1) model menyusun **rencana langkah bernomor** (satu panggilan tanpa tool); (2) rencana dicetak dan Anda ditanya **satu kali**: `Jalankan rencana ini? (y/N)`; (3) ditolak → tidak ada satu pun tool yang berjalan, perintah selesai normal; (4) disetujui → rencana disuntikkan ke konteks tugas dan eksekusi berjalan **tanpa konfirmasi per langkah** (persetujuan rencana = persetujuan eksekusi). Pagar folder kerja dan batas 12 langkah tetap berlaku penuh.
197
+
198
+ ## Sesi Chat Tersimpan — `chat --lanjut` & `vibekit sesi`
199
+
200
+ Setiap sesi `vibekit chat` **tersimpan otomatis** (di `~/.vibekit/sesi/`) setelah setiap giliran yang berhasil — riwayat internal disimpan apa adanya, termasuk pesan mentah tool, jadi sesi yang dilanjutkan tetap sah untuk model seperti Gemini 3.
201
+
202
+ ```bash
203
+ vibekit sesi # daftar sesi: id, waktu, jumlah pesan, cuplikan
204
+ vibekit chat --lanjut # lanjutkan sesi terakhir persis dari tempat berhenti
205
+ ```
206
+
207
+ Di tengah sesi, perintah `/sesi` menampilkan id sesi aktif. Tanpa `--lanjut`, `vibekit chat` selalu memulai sesi baru seperti biasa. Kompaksi dan pemangkasan aman tetap diterapkan per giliran pada sesi lanjutan.
208
+
209
+ ## Brief Proyek — `vibekit brief`
210
+
211
+ Memindai folder kerja (pohon maks 3 tingkat, melewati `node_modules`/`.git`/`dist`/`build`/file biner) dan menyusun ringkasan konteks yang padat ke **`.vibekit/brief.md`**: nama proyek, bahasa dominan, stack & dependencies dari `package.json`, scripts, file konfigurasi penting yang terdeteksi (hanya nama — isi `.env` asli tidak pernah dibaca), ekstensi terbanyak, struktur folder, dan potongan awal README.
212
+
213
+ ```bash
214
+ cd proyek-saya && vibekit brief
215
+ ```
216
+
217
+ Setelah brief ada, `ask`/`chat`/`do` **memakai brief itu otomatis** sebagai konteks tambahan (dicetak satu baris `(memakai brief proyek: ±N token)`; brief di atas 4.000 token dipotong dengan penanda). Lewatkan per perintah dengan flag `--tanpa-brief`. Jalankan ulang `vibekit brief` bila struktur proyek berubah banyak.
218
+
99
219
  ## Cara Failover Bekerja
100
220
 
101
221
  1. Sebelum memanggil, router memeriksa state lokal (`~/.vibekit/state.json`): penyedia yang masih **cooldown** atau sudah mencapai **anggaran harian** dilewati tanpa dipanggil.
102
- 2. Saat memanggil, permintaan berisi system prompt hemat sesuai `gaya.jawaban` dan `max_tokens` = `gaya.maksTokenJawaban`. Objek `extraBody` per penyedia (bila ada) digabung ke body — berguna untuk kenop khas model, mis. menekan penalaran.
222
+ 2. Saat memanggil, permintaan berisi system prompt hemat sesuai `gaya.jawaban` dan `max_tokens` = `gaya.maksTokenJawaban` (hanya bila diatur > 0; nilai `0` atau field yang dihapus berarti `max_tokens` tidak dikirim sama sekali — tanpa batas). Objek `extraBody` per penyedia (bila ada) digabung ke body — berguna untuk kenop khas model, mis. menekan penalaran.
103
223
  3. Bila respons menandakan kuota habis — HTTP **429/402** atau pola error `insufficient_quota` / `RESOURCE_EXHAUSTED` / saldo — penyedia ditandai **cooldown sampai akhir hari lokal** dan permintaan yang sama diulangi ke penyedia berikutnya. Rate-limit singkat (429 tanpa pola kuota) → cooldown `jedaCooldownMenit` (bawaan 15 menit). Khusus HTTP 503 (server penyedia sedang sibuk/penuh sesaat), penyedia yang sama dicoba ulang dulu sampai 2 kali dengan jeda 3 dan 8 detik, dan 503 tidak memicu cooldown.
104
224
  4. Error lain (401 kunci salah, 400, jaringan) **tidak** dianggap kuota habis: dicatat apa adanya dan router tetap mencoba penyedia berikutnya.
105
225
  5. Token terpakai dicatat dari field `usage` respons (atau estimasi tokenizer bila tidak ada). Bila semua penyedia gagal/habis, Anda menerima ringkasan status tiap penyedia dan jam paling awal untuk mencoba lagi.
@@ -107,9 +227,78 @@ Model bekerja langkah demi langkah lewat tool calling (API OpenAI-compatible yan
107
227
  ## Penghematan Token
108
228
 
109
229
  - **System prompt ringkas** — gaya `poin-penting` memerintahkan model menjawab hanya poin penting tanpa basa-basi; tersedia juga `normal` dan `detail`.
110
- - **Batas jawaban keras** — `maksTokenJawaban` menjadi `max_tokens` di setiap permintaan.
230
+ - **Plafon jawaban** — `maksTokenJawaban` menjadi `max_tokens` di setiap permintaan bila diatur > 0; `0` (atau field dihapus) berarti tanpa batas. Ini plafon, bukan target — lihat tabel anjuran di bagian Mulai Cepat.
111
231
  - **Konteks terseleksi** — `--file/--dir` melewati file biner & file > 256 KB, memangkas baris komentar penuh dan baris kosong berlebih, dan membatasi total konteks (±16.000 token) dengan memotong berkas terbesar. Selisih token mentah vs terseleksi selalu ditampilkan.
112
- - **Anggaran & pencatatan** — pemakaian per penyedia per hari terlihat di `vibekit status`, sehingga boros selalu kelihatan.
232
+ - **Kompaksi riwayat aksi (chat)** — rangkaian tool giliran-giliran lama dipadatkan menjadi ringkasan satu baris (`[Aksi selesai: tulis_file index.html — OK]`) sebelum giliran baru dimulai, jadi isi file yang pernah ditulis/dibaca model tidak dikirim ulang ke API di setiap giliran. Giliran terakhir tidak pernah disentuh kompaksi, sehingga `thought_signature` Gemini tetap utuh selama dibutuhkan. Token yang dibuang kompaksi **diukur dan dicatat** — lihat laporannya di `vibekit hemat`.
233
+ - **Brief proyek** — `.vibekit/brief.md` memberi model konteks proyek yang padat dan stabil, jauh lebih murah daripada melampirkan banyak file mentah berulang kali.
234
+ - **Anggaran & pencatatan** — pemakaian per penyedia per hari terlihat di `vibekit status` dan laporan lengkapnya di `vibekit hemat`, sehingga boros selalu kelihatan.
235
+
236
+ ## Referensi Konfigurasi — Nilai yang Didukung
237
+
238
+ Ringkasan semua field konfigurasi beserta nilai sahnya, supaya tidak perlu menebak.
239
+
240
+ ### `gaya.jawaban`
241
+
242
+ Mengatur gaya system prompt hemat yang dikirim pada setiap permintaan. Tepat **tiga nilai** yang dikenal:
243
+
244
+ | Nilai | Perilaku |
245
+ |---|---|
246
+ | `poin-penting` | **(bawaan)** Jawaban wajib hanya poin-poin penting dalam format poin; kalimat pembuka/penutup, sapaan, dan basa-basi dilarang keras; kode diberikan sebagai blok kode dengan penjelasan pendamping maksimal satu baris. Paling hemat token. |
247
+ | `normal` | Ringkas dan langsung ke inti, tetapi kalimat biasa yang mengalir — tidak dipaksa bentuk poin, tidak sekaku `poin-penting`. |
248
+ | `detail` | Jawaban jelas dan terstruktur; boleh panjang bila memang perlu, dengan subjudul atau poin agar mudah dibaca. Paling longgar (dan paling boros token) di antara ketiganya. |
249
+
250
+ Nilai lain, nilai kosong, atau field yang tidak diisi **dianggap `poin-penting`**.
251
+
252
+ ### `gaya.maksTokenJawaban`
253
+
254
+ Plafon token untuk **satu respons** model (dikirim sebagai `max_tokens`) — plafon, bukan target:
255
+
256
+ | Nilai | Arti |
257
+ |---|---|
258
+ | Angka bulat `> 0` (mis. `800`, `4000`, `16000`) | Plafon sebesar itu; respons yang melewatinya dipotong. Lihat tabel anjuran pemakaian di bagian Mengatur Plafon Jawaban. |
259
+ | `0`, atau field dihapus / tidak diisi | **Tanpa batas** — `max_tokens` tidak dikirim sama sekali; batas bawaan model yang berlaku. |
260
+ | Bawaan template `init` | `800`. |
261
+
262
+ ### Field lain, sekilas
263
+
264
+ | Field | Nilai / arti |
265
+ |---|---|
266
+ | `providers` | Array penyedia; **urutan = prioritas** — entri pertama adalah bawaan, berikutnya cadangan failover. |
267
+ | `providers[].jedaCooldownMenit` | Menit istirahat sebuah penyedia setelah kena rate-limit singkat (HTTP 429 tanpa pola kuota). Bawaan `15`. Kuota yang benar-benar habis selalu cooldown sampai akhir hari lokal, berapa pun nilai field ini. |
268
+ | `providers[].anggaranHarian` | **Opsional.** Batas token per hari yang dicatat lokal; penyedia yang mencapai batas dilewati sebelum dipanggil. Tidak diisi = tanpa anggaran lokal (hanya kuota asli penyedia yang berlaku). |
269
+ | `providers[].apiKey` | Kunci API langsung, atau `env:NAMA_VAR` untuk membaca dari variabel lingkungan. |
270
+ | `providers[].extraBody` | **Opsional.** Objek bebas yang digabung apa adanya ke body permintaan (kenop khas model/penyedia). |
271
+ | `mcpServers` | **Opsional.** Daftar server MCP (lihat bagian Dukungan MCP): tiap entri wajib punya `nama` plus `perintah` (stdio lokal, dengan `argumen`/`env` opsional) atau `url` (HTTP remote, dengan `headers` opsional). |
272
+
273
+ ## Pemecahan Masalah
274
+
275
+ ### Windows PowerShell: `vibekit : The term 'vibekit' is not recognized as the name of a cmdlet...`
276
+
277
+ Setelah `npm install -g @chispear/vibekit` berhasil, menjalankan `vibekit init` (atau perintah `vibekit` lain) di PowerShell bisa gagal dengan error `CommandNotFoundException`:
278
+
279
+ ```
280
+ vibekit : The term 'vibekit' is not recognized as the name of a cmdlet, function, script file, or operable program.
281
+ ```
282
+
283
+ **Penyebab:** folder tempat npm menyimpan perintah global — hasil `npm config get prefix`, umumnya `C:\Users\<nama>\AppData\Roaming\npm` — belum ada di PATH Windows, jadi PowerShell tidak menemukan perintahnya. Package-nya sendiri sebenarnya sudah terpasang dengan benar; ini murni soal PATH.
284
+
285
+ **Perbaikan cepat** (hanya untuk jendela PowerShell yang sedang terbuka):
286
+
287
+ ```powershell
288
+ $env:Path += ";" + (npm config get prefix)
289
+ ```
290
+
291
+ **Perbaikan permanen** (sekali saja, lalu tutup dan buka lagi PowerShell):
292
+
293
+ ```powershell
294
+ $prefix = npm config get prefix
295
+ $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
296
+ [Environment]::SetEnvironmentVariable("Path", "$userPath;$prefix", "User")
297
+ ```
298
+
299
+ **Verifikasi:** `vibekit --version` harus menampilkan nomor versi (mis. `0.4.1`).
300
+
301
+ **Alternatif tanpa mengubah PATH:** jalankan lewat npx — `npx @chispear/vibekit <perintah>` (mis. `npx @chispear/vibekit init`).
113
302
 
114
303
  ## Pengembangan
115
304
 
@@ -123,7 +312,7 @@ Keputusan repo: `dist/` masuk `.gitignore` dan dibangun saat publish (`prepublis
123
312
 
124
313
  ## Status Nama di npm
125
314
 
126
- Package ini terbit di npm sebagai **`@chispear/vibekit`** (v0.1.0, 25 Oktober 2026; v0.2.0 menambahkan mode agent `vibekit do`; v0.3.0 menggabungkan kemampuan agent itu ke `ask` dan `chat`) karena nama tak-scoped `vibekit` sudah dipakai proyek lain. Nama perintah CLI tetap `vibekit`.
315
+ Package ini terbit di npm sebagai **`@chispear/vibekit`** (v0.1.0, 25 Oktober 2026; v0.2.0 menambahkan mode agent `vibekit do`; v0.3.0 menggabungkan kemampuan agent itu ke `ask` dan `chat`; v0.4.0 menambahkan mode auto-allow dan kompaksi riwayat aksi; v0.5.0 menambahkan dukungan MCP; v0.6.0 menambahkan laporan `hemat`, streaming jawaban, mode `--rencana`, sesi chat yang bisa dilanjutkan, dan brief proyek) karena nama tak-scoped `vibekit` sudah dipakai proyek lain. Nama perintah CLI tetap `vibekit`.
127
316
 
128
317
  ## Lisensi
129
318
 
package/dist/agent.js CHANGED
@@ -95,6 +95,7 @@ export const DEFINISI_TOOL = [
95
95
  const SYSTEM_PROMPT_AGENT = [
96
96
  "Kamu adalah agent pelaksana tugas VibeKit yang bekerja di dalam SATU folder kerja pengguna.",
97
97
  "Kerjakan tugas langkah demi langkah memakai tool yang tersedia: daftar_file, baca_file, tulis_file, edit_file, jalankan_perintah.",
98
+ "Tool tambahan dari server MCP (nama diawali \"mcp__\") mungkin juga tersedia — gunakan bila relevan; tool MCP juga selalu meminta persetujuan pengguna dan penolakan diperlakukan sama seperti tool bawaan.",
98
99
  "Semua path bersifat relatif terhadap folder kerja; jangan pernah mencoba mengakses apa pun di luar folder itu.",
99
100
  "Aksi tulis_file, edit_file, dan jalankan_perintah selalu meminta persetujuan pengguna dulu; bila pengguna menolak, jangan memaksa dan jangan mengulang aksi yang sama — sesuaikan rencana atau akhiri tugas.",
100
101
  "Jangan mengarang isi file: gunakan daftar_file/baca_file dulu bila perlu mengetahui isi folder/file.",
@@ -109,10 +110,11 @@ export const SYSTEM_PROMPT_GABUNGAN = [
109
110
  "Kamu adalah lawan bicara VibeKit yang bekerja di dalam SATU folder kerja pengguna.",
110
111
  "Jawab pertanyaan biasa langsung sebagai teks, singkat dan hanya poin-poin penting.",
111
112
  "Bila pengguna meminta dibuatkan/diubah file atau menjalankan perintah, gunakan tool yang tersedia: daftar_file, baca_file, tulis_file, edit_file, jalankan_perintah.",
113
+ "Tool tambahan dari server MCP (nama diawali \"mcp__\") mungkin juga tersedia — gunakan bila relevan; tool MCP juga selalu meminta persetujuan pengguna dan penolakan diperlakukan sama seperti tool bawaan.",
112
114
  "Semua path bersifat relatif terhadap folder kerja; jangan pernah mencoba mengakses apa pun di luar folder itu.",
113
115
  "Aksi tulis_file, edit_file, dan jalankan_perintah selalu meminta persetujuan pengguna dulu; bila pengguna menolak, jangan memaksa dan jangan mengulang aksi yang sama.",
114
116
  "Jangan mengarang isi file: gunakan daftar_file/baca_file dulu bila perlu mengetahui isi folder/file.",
115
- "Setelah bertindak, tutup giliran dengan balasan teks (tanpa tool) berisi ringkasan singkat: apa yang dilakukan dan hasilnya.",
117
+ "Setelah bertindak, tutup giliran dengan balasan teks (tanpa tool) berisi ringkasan singkat: apa yang dilakukan dan hasilnya. Ringkasan itu TIDAK BOLEH mengulang isi file yang ditulis atau dibaca — cukup sebutkan nama file dan hasilnya.",
116
118
  ].join(" ");
117
119
  /**
118
120
  * Resolve path masukan terhadap cwd. Mengembalikan null bila hasil resolve
@@ -152,6 +154,8 @@ function argumenRingkas(nama, arg) {
152
154
  return potong(JSON.stringify(arg), 80);
153
155
  }
154
156
  }
157
+ /** Nama tool yang mengubah sesuatu — wajib konfirmasi kecuali mode auto-allow. */
158
+ const TOOL_MUTASI = new Set(["tulis_file", "edit_file", "jalankan_perintah"]);
155
159
  /**
156
160
  * Jalankan satu panggilan tool. Selalu mengembalikan teks hasil untuk model
157
161
  * (tidak pernah throw): keberhasilan diawali "OK:", kegagalan "ERROR:",
@@ -167,6 +171,10 @@ export async function eksekusiTool(panggilan, konteks) {
167
171
  return `ERROR: argumen untuk tool ${nama} bukan JSON yang valid.`;
168
172
  }
169
173
  const { cwd, konfirmasi } = konteks;
174
+ // Mode auto-allow: callback konfirmasi pengguna tidak dipanggil sama sekali —
175
+ // aksi mutasi langsung disetujui. Pagar cwd (resolusiAman) TIDAK berubah dan
176
+ // tetap menolak path traversal walau auto-allow aktif.
177
+ const konfirmasiEfektif = konteks.autoAllow ? async () => true : konfirmasi;
170
178
  const ambilPath = (mentah) => {
171
179
  const masukan = typeof mentah === "string" && mentah.trim() !== "" ? mentah : ".";
172
180
  const tujuan = resolusiAman(cwd, masukan);
@@ -203,7 +211,7 @@ export async function eksekusiTool(panggilan, konteks) {
203
211
  const konten = typeof arg.konten === "string" ? arg.konten : "";
204
212
  const deskripsi = `tulis_file → ${p.rel} (${konten.length} karakter)\n` +
205
213
  `Pratinjau konten:\n${potong(konten, 300)}`;
206
- if (!(await konfirmasi(deskripsi))) {
214
+ if (!(await konfirmasiEfektif(deskripsi))) {
207
215
  return `DITOLAK: pengguna menolak aksi tulis_file ke ${p.rel}. File tidak ditulis. Jangan mengulang aksi yang sama; sesuaikan rencana atau akhiri dengan ringkasan.`;
208
216
  }
209
217
  await fs.mkdir(path.dirname(p.tujuan), { recursive: true });
@@ -224,7 +232,7 @@ export async function eksekusiTool(panggilan, konteks) {
224
232
  const deskripsi = `edit_file → ${p.rel}\n` +
225
233
  `Teks lama: ${potong(teksLama, 150)}\n` +
226
234
  `Teks baru: ${potong(teksBaru, 150)}`;
227
- if (!(await konfirmasi(deskripsi))) {
235
+ if (!(await konfirmasiEfektif(deskripsi))) {
228
236
  return `DITOLAK: pengguna menolak aksi edit_file di ${p.rel}. File tidak diubah. Jangan mengulang aksi yang sama; sesuaikan rencana atau akhiri dengan ringkasan.`;
229
237
  }
230
238
  const baru = isi.slice(0, idx) + teksBaru + isi.slice(idx + teksLama.length);
@@ -236,7 +244,7 @@ export async function eksekusiTool(panggilan, konteks) {
236
244
  if (!perintah)
237
245
  return "ERROR: argumen perintah kosong.";
238
246
  const deskripsi = `jalankan_perintah → ${perintah}`;
239
- if (!(await konfirmasi(deskripsi))) {
247
+ if (!(await konfirmasiEfektif(deskripsi))) {
240
248
  return `DITOLAK: pengguna menolak menjalankan perintah "${perintah}". Perintah tidak dijalankan. Jangan mengulang aksi yang sama; sesuaikan rencana atau akhiri dengan ringkasan.`;
241
249
  }
242
250
  const hasil = await new Promise((resolve) => {
@@ -270,6 +278,40 @@ export async function eksekusiTool(panggilan, konteks) {
270
278
  return `ERROR: ${e.message}`;
271
279
  }
272
280
  }
281
+ /**
282
+ * Jalankan satu panggilan tool EKSTERNAL (non-bawaan, mis. dari server MCP).
283
+ * Tool eksternal selalu diperlakukan sebagai AKSI: wajib konfirmasi pengguna
284
+ * (pratinjau nama tool + argumen ringkas ±300 karakter), kecuali mode
285
+ * auto-allow — sama persis seperti tulis_file. Selalu mengembalikan teks
286
+ * hasil dengan awalan "OK:" / "ERROR:" / "DITOLAK:" seperti eksekusiTool.
287
+ */
288
+ export async function eksekusiToolEkstra(panggilan, konteks) {
289
+ const nama = panggilan.function.name;
290
+ let arg;
291
+ try {
292
+ arg = JSON.parse(panggilan.function.arguments || "{}");
293
+ }
294
+ catch {
295
+ return `ERROR: argumen untuk tool ${nama} bukan JSON yang valid.`;
296
+ }
297
+ if (!konteks.eksekutorEkstra) {
298
+ return `ERROR: tool ${nama} tidak tersedia (eksekutor eksternal tidak terpasang pada giliran ini).`;
299
+ }
300
+ const konfirmasiEfektif = konteks.autoAllow ? async () => true : konteks.konfirmasi;
301
+ const deskripsi = `${nama} (tool eksternal/MCP)\nArgumen: ${potong(JSON.stringify(arg), 300)}`;
302
+ if (!(await konfirmasiEfektif(deskripsi))) {
303
+ return `DITOLAK: pengguna menolak aksi ${nama}. Tool tidak dipanggil. Jangan mengulang aksi yang sama; sesuaikan rencana atau akhiri dengan ringkasan.`;
304
+ }
305
+ try {
306
+ const hasil = (await konteks.eksekutorEkstra(nama, arg)).trim();
307
+ if (hasil.startsWith("ERROR:") || hasil.startsWith("DITOLAK:"))
308
+ return hasil;
309
+ return `OK: ${hasil || "(tool selesai tanpa keluaran teks)"}`;
310
+ }
311
+ catch (e) {
312
+ return `ERROR: ${e.message}`;
313
+ }
314
+ }
273
315
  /**
274
316
  * Inti loop agent untuk SATU giliran: panggil router (dengan tools) → jalankan
275
317
  * tool_calls yang diminta model lewat eksekutor aman → kirim hasil tool balik
@@ -284,6 +326,11 @@ export async function eksekusiTool(panggilan, konteks) {
284
326
  export async function jalankanGiliran(opsi) {
285
327
  const maksLangkah = opsi.maksLangkah ?? MAKS_LANGKAH_BAWAAN;
286
328
  const messages = opsi.messages;
329
+ // Tool bawaan + tool ekstra (MCP) ditawarkan bersama; nama ekstra menentukan
330
+ // perutean eksekusi (eksekusiToolEkstra) dan perlakuan sebagai aksi.
331
+ const toolEkstra = opsi.toolEkstra ?? [];
332
+ const namaToolEkstra = new Set(toolEkstra.map((t) => t.function.name));
333
+ const semuaTool = [...DEFINISI_TOOL, ...toolEkstra];
287
334
  const catatan = [];
288
335
  let totalToken = 0;
289
336
  let semuaUsage = true;
@@ -297,7 +344,10 @@ export async function jalankanGiliran(opsi) {
297
344
  messages,
298
345
  lokasiState: opsi.lokasiState,
299
346
  fetchImpl: opsi.fetchImpl,
300
- tools: DEFINISI_TOOL,
347
+ tools: semuaTool,
348
+ stream: opsi.stream,
349
+ onToken: opsi.onToken,
350
+ onTokenPertama: opsi.onTokenPertama,
301
351
  });
302
352
  catatan.push(...hasil.catatan);
303
353
  if (!hasil.ok) {
@@ -310,6 +360,7 @@ export async function jalankanGiliran(opsi) {
310
360
  jumlahLangkah: langkah,
311
361
  catatan,
312
362
  pesanGagal: hasil.pesan,
363
+ ...(hasil.streamTerputus ? { streamTerputus: true } : {}),
313
364
  };
314
365
  }
315
366
  totalToken += hasil.tokenTerpakai;
@@ -352,8 +403,20 @@ export async function jalankanGiliran(opsi) {
352
403
  catch {
353
404
  argRingkas = "";
354
405
  }
355
- opsi.jejak?.(`langkah ${langkah}: ${p.function.name}${argRingkas ? ` (${argRingkas})` : ""}`);
356
- const hasilTool = await eksekusiTool(p, { cwd: opsi.cwd, konfirmasi: opsi.konfirmasi });
406
+ const adalahEkstra = namaToolEkstra.has(p.function.name);
407
+ const penandaAuto = opsi.autoAllow && (TOOL_MUTASI.has(p.function.name) || adalahEkstra) ? " (auto)" : "";
408
+ opsi.jejak?.(`langkah ${langkah}: ${p.function.name}${argRingkas ? ` (${argRingkas})` : ""}${penandaAuto}`);
409
+ const hasilTool = adalahEkstra
410
+ ? await eksekusiToolEkstra(p, {
411
+ konfirmasi: opsi.konfirmasi,
412
+ autoAllow: opsi.autoAllow,
413
+ eksekutorEkstra: opsi.eksekutorEkstra,
414
+ })
415
+ : await eksekusiTool(p, {
416
+ cwd: opsi.cwd,
417
+ konfirmasi: opsi.konfirmasi,
418
+ autoAllow: opsi.autoAllow,
419
+ });
357
420
  opsi.jejak?.(` → ${potong(hasilTool.split("\n")[0], 120)}`);
358
421
  messages.push({
359
422
  role: "tool",
@@ -416,6 +479,131 @@ export function pangkasRiwayatAman(messages, maksToken) {
416
479
  }
417
480
  return [...awalan, ...grup.flat()];
418
481
  }
482
+ /** Target ringkas satu panggilan tool untuk pesan ringkasan kompaksi. */
483
+ function targetRingkasTool(panggilan) {
484
+ const nama = panggilan.function.name;
485
+ // Tool MCP (mcp__<server>__<tool>): targetnya adalah server/tool itu sendiri —
486
+ // argumennya tidak diasumsikan punya path/perintah.
487
+ if (nama.startsWith("mcp__")) {
488
+ const bagian = nama.split("__");
489
+ return bagian.length >= 3 ? `${bagian[1]}/${bagian.slice(2).join("__")}` : nama;
490
+ }
491
+ let arg = {};
492
+ try {
493
+ arg = JSON.parse(panggilan.function.arguments || "{}");
494
+ }
495
+ catch {
496
+ // Argumen rusak: target dikosongkan, ringkasan tetap dibuat dari nama tool.
497
+ }
498
+ if (nama === "jalankan_perintah") {
499
+ return potong(String(arg.perintah ?? ""), 40);
500
+ }
501
+ return String(arg.path ?? ".");
502
+ }
503
+ /** Status satu hasil tool dari awalannya: "OK" | "ERROR" | "DITOLAK" | kata pertama. */
504
+ function statusHasilTool(konten) {
505
+ const t = (konten ?? "").trimStart();
506
+ if (t.startsWith("OK"))
507
+ return "OK";
508
+ if (t.startsWith("ERROR"))
509
+ return "ERROR";
510
+ if (t.startsWith("DITOLAK"))
511
+ return "DITOLAK";
512
+ return t.split(/[:\s]/)[0] || "?";
513
+ }
514
+ /** Padatkan satu giliran LAMA (bukan giliran terakhir) — lihat kompaksiRiwayatAksi. */
515
+ function kompaksiSatuGiliran(giliran) {
516
+ const keluar = [];
517
+ let i = 0;
518
+ while (i < giliran.length) {
519
+ const m = giliran[i];
520
+ if (m.role === "assistant" && m.tool_calls && m.tool_calls.length > 0) {
521
+ // Kumpulkan pesan-pesan tool milik rangkaian ini.
522
+ const hasilTool = [];
523
+ let j = i + 1;
524
+ while (j < giliran.length && giliran[j].role === "tool") {
525
+ hasilTool.push(giliran[j]);
526
+ j++;
527
+ }
528
+ // Teks pendamping pada pesan assistant (jarang ada) dipertahankan.
529
+ if (typeof m.content === "string" && m.content.trim() !== "") {
530
+ keluar.push({ role: "assistant", content: m.content });
531
+ }
532
+ // Satu pesan ringkasan per panggilan tool, menggantikan seluruh
533
+ // rangkaian — argumen lengkap (termasuk isi file) dan thought_signature
534
+ // ikut terbuang di sini, memang itu tujuannya (hemat token).
535
+ for (const p of m.tool_calls) {
536
+ const pasangan = hasilTool.find((t) => t.tool_call_id === p.id) ?? hasilTool[0];
537
+ const status = statusHasilTool(pasangan?.content ?? null);
538
+ const target = targetRingkasTool(p);
539
+ keluar.push({
540
+ role: "assistant",
541
+ content: `[Aksi selesai: ${p.function.name}${target ? ` ${target}` : ""} — ${status}]`,
542
+ });
543
+ }
544
+ i = j;
545
+ }
546
+ else if (m.role === "tool") {
547
+ // Pesan tool yatim tanpa rangkaian assistant pendahulu dalam giliran
548
+ // yang sama: buang (tidak sah dikirim ke API tanpa pendahulunya).
549
+ i++;
550
+ }
551
+ else {
552
+ keluar.push(m);
553
+ i++;
554
+ }
555
+ }
556
+ return keluar;
557
+ }
558
+ /**
559
+ * Kompaksi riwayat aksi untuk chat: ganti rangkaian tool pada giliran-giliran
560
+ * LAMA menjadi ringkasan teks satu baris, agar isi file yang ditulis model
561
+ * (argumen tool_calls) tidak ikut dikirim ulang ke API pada setiap giliran
562
+ * berikutnya. Bentuk ringkasan: `[Aksi selesai: <tool> <target> — <status>]`.
563
+ *
564
+ * Aturan keras:
565
+ * - Giliran TERAKHIR tidak disentuh sama sekali — pesan assistant mentahnya
566
+ * (termasuk thought_signature Gemini di dalam tool_calls) wajib utuh
567
+ * selama giliran itu masih aktif/baru selesai.
568
+ * - Pesan sebelum "user" pertama (mis. system) dipertahankan apa adanya.
569
+ * - Pesan user dan assistant teks biasa dipertahankan apa adanya.
570
+ * - Mengembalikan array BARU; array masukan tidak diubah.
571
+ */
572
+ export function kompaksiRiwayatAksi(messages) {
573
+ let awal = 0;
574
+ while (awal < messages.length && messages[awal].role !== "user")
575
+ awal++;
576
+ const awalan = messages.slice(0, awal);
577
+ const sisa = messages.slice(awal);
578
+ const grup = [];
579
+ for (const m of sisa) {
580
+ if (m.role === "user" || grup.length === 0)
581
+ grup.push([m]);
582
+ else
583
+ grup[grup.length - 1].push(m);
584
+ }
585
+ const hasil = [...awalan];
586
+ grup.forEach((giliran, gi) => {
587
+ if (gi === grup.length - 1)
588
+ hasil.push(...giliran);
589
+ else
590
+ hasil.push(...kompaksiSatuGiliran(giliran));
591
+ });
592
+ return hasil;
593
+ }
594
+ /**
595
+ * Varian terukur dari kompaksiRiwayatAksi: selain riwayat hasil, kembalikan
596
+ * jumlah token yang benar-benar dibuang (dihitung dengan tokenizer atas
597
+ * konten pesan yang digantikan/dibuang, dikurangi ringkasan penggantinya).
598
+ * Inilah angka penghematan yang dicatat `vibekit hemat` — terukur, bukan
599
+ * klaim desain.
600
+ */
601
+ export function kompaksiRiwayatAksiTerukur(messages) {
602
+ const sebelum = messages.reduce((s, m) => s + tokenPesan(m), 0);
603
+ const riwayat = kompaksiRiwayatAksi(messages);
604
+ const sesudah = riwayat.reduce((s, m) => s + tokenPesan(m), 0);
605
+ return { riwayat, tokenTerbuang: Math.max(0, sebelum - sesudah) };
606
+ }
419
607
  /**
420
608
  * Mode tugas sekali jalan (`vibekit do`): satu giliran agent dengan system
421
609
  * prompt pelaksana tugas, dibangun di atas jalankanGiliran. Perilaku dan
@@ -425,6 +613,7 @@ export async function jalankanAgent(opsi) {
425
613
  const maksLangkah = opsi.maksLangkah ?? MAKS_LANGKAH_BAWAAN;
426
614
  const messages = [
427
615
  { role: "system", content: SYSTEM_PROMPT_AGENT },
616
+ ...(opsi.konteksSistem ? [{ role: "system", content: opsi.konteksSistem }] : []),
428
617
  { role: "user", content: opsi.tugas },
429
618
  ];
430
619
  const hasil = await jalankanGiliran({
@@ -433,9 +622,15 @@ export async function jalankanAgent(opsi) {
433
622
  lokasiState: opsi.lokasiState,
434
623
  cwd: opsi.cwd,
435
624
  konfirmasi: opsi.konfirmasi,
625
+ autoAllow: opsi.autoAllow,
436
626
  fetchImpl: opsi.fetchImpl,
437
627
  maksLangkah: opsi.maksLangkah,
438
628
  jejak: opsi.jejak,
629
+ toolEkstra: opsi.toolEkstra,
630
+ eksekutorEkstra: opsi.eksekutorEkstra,
631
+ stream: opsi.stream,
632
+ onToken: opsi.onToken,
633
+ onTokenPertama: opsi.onTokenPertama,
439
634
  });
440
635
  if (hasil.pesanGagal !== undefined) {
441
636
  return { ok: false, ringkasan: hasil.pesanGagal, jumlahLangkah: hasil.jumlahLangkah, catatan: hasil.catatan };
@@ -451,3 +646,100 @@ export async function jalankanAgent(opsi) {
451
646
  }
452
647
  return { ok: true, ringkasan: hasil.jawabanAkhir, jumlahLangkah: hasil.jumlahLangkah, catatan: hasil.catatan };
453
648
  }
649
+ /**
650
+ * Mode rencana (`vibekit do --rencana`): dua tahap.
651
+ * 1. Susun rencana: SATU panggilan router TANPA tools — model hanya menulis
652
+ * rencana langkah bernomor yang ringkas (gaya dari konfigurasi tetap
653
+ * ditambahkan router).
654
+ * 2. Konfirmasi SATU kali lewat `konfirmasiRencana`. Ditolak → selesai
655
+ * tanpa menjalankan tool apa pun. Disetujui → rencana disuntikkan ke
656
+ * konteks tugas dan loop agent berjalan dengan autoAllow AKTIF
657
+ * (persetujuan rencana = persetujuan eksekusi); pagar cwd dan batas
658
+ * langkah tetap berlaku persis seperti mode biasa.
659
+ */
660
+ export async function jalankanAgentRencana(opsi) {
661
+ const maksLangkah = opsi.maksLangkah ?? MAKS_LANGKAH_BAWAAN;
662
+ const hasilRencana = await tanyaRouter({
663
+ config: opsi.config,
664
+ messages: [
665
+ {
666
+ role: "user",
667
+ content: "Susun rencana langkah bernomor yang ringkas untuk tugas berikut. " +
668
+ "Tulis HANYA daftar rencananya — jangan menjalankan apa pun, jangan memanggil tool apa pun.\n\n" +
669
+ `Tugas: ${opsi.tugas}`,
670
+ },
671
+ ],
672
+ lokasiState: opsi.lokasiState,
673
+ fetchImpl: opsi.fetchImpl,
674
+ });
675
+ if (!hasilRencana.ok) {
676
+ return {
677
+ status: "gagal",
678
+ rencana: "",
679
+ ringkasan: hasilRencana.pesan,
680
+ jumlahLangkah: 0,
681
+ catatan: hasilRencana.catatan,
682
+ };
683
+ }
684
+ const rencana = hasilRencana.jawaban.trim();
685
+ if (!rencana) {
686
+ return {
687
+ status: "gagal",
688
+ rencana: "",
689
+ ringkasan: "Model tidak menghasilkan rencana (jawaban kosong).",
690
+ jumlahLangkah: 0,
691
+ catatan: hasilRencana.catatan,
692
+ };
693
+ }
694
+ const disetujui = await opsi.konfirmasiRencana(rencana);
695
+ if (!disetujui) {
696
+ return {
697
+ status: "dibatalkan",
698
+ rencana,
699
+ ringkasan: "Rencana ditolak pengguna — tidak ada tool yang dijalankan.",
700
+ jumlahLangkah: 0,
701
+ catatan: hasilRencana.catatan,
702
+ };
703
+ }
704
+ const messages = [
705
+ { role: "system", content: SYSTEM_PROMPT_AGENT },
706
+ ...(opsi.konteksSistem ? [{ role: "system", content: opsi.konteksSistem }] : []),
707
+ {
708
+ role: "user",
709
+ content: `${opsi.tugas}\n\n` +
710
+ "Rencana berikut SUDAH DISETUJUI pengguna — ikuti rencana ini langkah demi langkah:\n" +
711
+ rencana,
712
+ },
713
+ ];
714
+ const hasil = await jalankanGiliran({
715
+ config: opsi.config,
716
+ messages,
717
+ lokasiState: opsi.lokasiState,
718
+ cwd: opsi.cwd,
719
+ konfirmasi: opsi.konfirmasi,
720
+ autoAllow: true,
721
+ fetchImpl: opsi.fetchImpl,
722
+ maksLangkah: opsi.maksLangkah,
723
+ jejak: opsi.jejak,
724
+ toolEkstra: opsi.toolEkstra,
725
+ eksekutorEkstra: opsi.eksekutorEkstra,
726
+ stream: opsi.stream,
727
+ onToken: opsi.onToken,
728
+ onTokenPertama: opsi.onTokenPertama,
729
+ });
730
+ const catatan = [...hasilRencana.catatan, ...hasil.catatan];
731
+ if (hasil.pesanGagal !== undefined) {
732
+ return { status: "gagal", rencana, ringkasan: hasil.pesanGagal, jumlahLangkah: hasil.jumlahLangkah, catatan };
733
+ }
734
+ if (hasil.batasTercapai) {
735
+ return {
736
+ status: "batas",
737
+ rencana,
738
+ ringkasan: `Batas langkah tercapai (${maksLangkah} langkah) sebelum rencana selesai dijalankan. ` +
739
+ "Jalankan lagi perintah yang sama untuk melanjutkan, atau persempit tugasnya.",
740
+ jumlahLangkah: hasil.jumlahLangkah,
741
+ catatan,
742
+ };
743
+ }
744
+ return { status: "selesai", rencana, ringkasan: hasil.jawabanAkhir, jumlahLangkah: hasil.jumlahLangkah, catatan };
745
+ }