beast-agent 0.30.0 → 1.0.1

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.
Files changed (44) hide show
  1. package/package.json +8 -3
  2. package/src/agent/engine.js +463 -21
  3. package/src/agent/llm.js +52 -3
  4. package/src/agent/obscura.js +26 -10
  5. package/src/agent/skills.js +2 -2
  6. package/src/agent/tools.js +519 -7
  7. package/src/main.js +28 -13
  8. package/src/renderer/i18n.js +7 -7
  9. package/src/renderer/index.html +5 -2
  10. package/src/renderer/renderer.js +154 -5
  11. package/src/renderer/style.css +45 -4
  12. package/.env.example +0 -8
  13. package/config.example.yaml +0 -39
  14. package/eng.traineddata +0 -0
  15. package/src/agent/scripts/__pycache__/websearch.cpython-312.pyc +0 -0
  16. package/store/skills.json +0 -5
  17. package/tests/approval.test.js +0 -56
  18. package/tests/bg-jobs.test.js +0 -424
  19. package/tests/bots-name.test.js +0 -42
  20. package/tests/bus.test.js +0 -46
  21. package/tests/computeruse-context.test.js +0 -69
  22. package/tests/cron.test.js +0 -95
  23. package/tests/engine.test.js +0 -203
  24. package/tests/eval.test.js +0 -91
  25. package/tests/fallout.test.js +0 -188
  26. package/tests/llm.test.js +0 -205
  27. package/tests/memory.test.js +0 -33
  28. package/tests/memoryloop.test.js +0 -26
  29. package/tests/notegen.test.js +0 -63
  30. package/tests/obscura.test.js +0 -124
  31. package/tests/owner.test.js +0 -32
  32. package/tests/python.test.js +0 -76
  33. package/tests/research.test.js +0 -116
  34. package/tests/scenarios.json +0 -81
  35. package/tests/sessioncode.test.js +0 -50
  36. package/tests/setup.js +0 -9
  37. package/tests/tokens.test.js +0 -42
  38. package/tests/tools.test.js +0 -84
  39. package/tests/usage.test.js +0 -44
  40. package/tests/v11.test.js +0 -76
  41. package/tests/watchers.test.js +0 -206
  42. package/tests/watext.test.js +0 -53
  43. package/tests/whatsapp.test.js +0 -103
  44. package/tests/wherewasi.test.js +0 -40
@@ -63,6 +63,38 @@ const THINK_LEVELS = [
63
63
  { v: 5, label: 'Max', effort: 'max' },
64
64
  ];
65
65
 
66
+ /* ---------- opencode motoru port: bağlam bütçesi + compaction + prune ----------
67
+ Kaynak: opencode-dev/packages/opencode/src/session/{overflow,compaction}.ts
68
+ Sabitler birebir alınmıştır (COMPACTION_BUFFER 20k, PRUNE_PROTECT 40k,
69
+ PRUNE_MINIMUM 20k, DOOM_LOOP 3, özet çıktı kırpma 2000 karakter). */
70
+ const COMPACTION_BUFFER = 20000; // overflow.ts:8 — özet turu için ayrılan pay
71
+ const OUTPUT_TOKEN_MAX = 32000; // transform.ts:18 — çıktı tavanı (rezerv hesabı)
72
+ const PRUNE_PROTECT = 40000; // compaction.ts:29 — en yeni araç çıktısı koruması
73
+ const PRUNE_MINIMUM = 20000; // compaction.ts:28 — bu kadar kazanç yoksa dokunma
74
+ const SUMMARY_OUT_KEEP = 2000; // compaction.ts:30 — özetteki araç çıktısı kırpma
75
+ const MIN_PRESERVE_RECENT = 2000; // compaction.ts:32 — korunan kuyruk alt sınırı
76
+ const MAX_PRESERVE_RECENT = 15000; // compaction.ts:33 — korunan kuyruk üst sınırı
77
+ const DOOM_LOOP_THRESHOLD = 3; // processor.ts:29 — aynı araç+argüman sınırı
78
+ const HISTORY_BUDGET_MAX = 80000; // dinamik bütçe tavanı (cache'siz sağlayıcı koruması)
79
+
80
+ /* Bilinen model ailelerinin bağlam penceresi (models.dev değerleri).
81
+ Eşleşmezse 128k varsayılır — asla gerçek limitin üstünü tahmin etme. */
82
+ const MODEL_CONTEXT_TABLE = [
83
+ [/gemini-[23]/i, 1000000],
84
+ [/gpt-5|gpt-4\.1|gpt-4o|o[134]-/i, 400000],
85
+ [/glm-4\.[67]/i, 200000],
86
+ [/claude/i, 200000],
87
+ [/grok-[34]/i, 256000],
88
+ [/kimi/i, 256000],
89
+ [/qwen.*coder/i, 256000],
90
+ [/deepseek|qwen|glm|llama|mistral|minimax/i, 131072],
91
+ ];
92
+ function modelContextOf(sel) {
93
+ const s = String((sel && sel.model) || '');
94
+ for (const [re, ctx] of MODEL_CONTEXT_TABLE) if (re.test(s)) return ctx;
95
+ return 131072;
96
+ }
97
+
66
98
  /* Çıktı biçimi kuralı: # ve * karakteri yasak (tüm ajanlar için ortak metin) */
67
99
  const FORMAT_RULES =
68
100
  'ÇIKTI BİÇİMİ (ZORUNLU):\n' +
@@ -83,12 +115,21 @@ const PERM_TOOL_SETS = {
83
115
  read: new Set([
84
116
  'web_search', 'http_fetch', 'deep_search',
85
117
  'browser_open', 'browser_read', 'browser_snapshot',
86
- 'list_dir', 'read_file',
118
+ 'list_dir', 'read_file', 'grep', 'glob',
87
119
  ]),
88
120
  chat: new Set([]), // sadece sohbet
89
121
  };
90
122
  const PERM_LEVELS = ['all', 'web', 'read', 'chat'];
91
123
 
124
+ /* opencode plan agent portu: PLAN modu PROMPT düzeyinde değil GERÇEKten
125
+ salt-okurdur — yazma/çalıştırma araçları setten düşer, sadece plan çıkar */
126
+ const PLAN_ALLOW_TOOLS = new Set([
127
+ 'read_file', 'list_dir', 'grep', 'glob',
128
+ 'web_search', 'http_fetch', 'deep_search',
129
+ 'browser_open', 'browser_read', 'browser_snapshot',
130
+ 'todo_write', 'memory_search', 'kb_search', 'ocr_read',
131
+ ]);
132
+
92
133
  /* İzin değerini normalize eder: 'all' → ['all'], 'web' → ['web'],
93
134
  'web,read' / ['web','read'] → ['web','read'] (sıra PERM_LEVELS'e göre dizilir).
94
135
  Geçersiz değerler atılır; hiçbiri kalmazsa boş dizi döner. */
@@ -103,7 +144,7 @@ function normalizePerms(p) {
103
144
  Bunların hepsi run_background ile paralel ajana devredilir — CEO sadece
104
145
  konuşur, planlar, emir verir ve takip eder. */
105
146
  const CEO_EXEC_TOOLS = new Set([
106
- 'run_command', 'read_file', 'write_file', 'list_dir',
147
+ 'run_command', 'read_file', 'write_file', 'edit_file', 'list_dir',
107
148
  'python_run',
108
149
  'web_search', 'http_fetch', 'deep_search',
109
150
  'browser_open', 'browser_read', 'browser_screenshot', 'browser_snapshot',
@@ -629,7 +670,14 @@ class Engine {
629
670
  }
630
671
  const res = await chatStreamAuto(
631
672
  c,
632
- { messages: msgs, tools: activeTools, reasoningEffort: this._thinkEffortFor(session) },
673
+ {
674
+ messages: msgs,
675
+ tools: activeTools,
676
+ reasoningEffort: this._thinkEffortFor(session),
677
+ /* opencode cache disiplini (transform.ts:1270): oturum-sabit
678
+ önbellek anahtarı — destekleyen sağlayıcıda önek cache'i tutar */
679
+ cacheKey: String(session.id || ''),
680
+ },
633
681
  {
634
682
  signal,
635
683
  onDelta,
@@ -929,6 +977,10 @@ class Engine {
929
977
  } else if (rec.t === 'notes') {
930
978
  session.notes = String(rec.text || '');
931
979
  session.notesAt = Number(rec.at) || 0;
980
+ } else if (rec.t === 'summary') {
981
+ /* opencode compaction: konuşma özeti — birebir geçmişin yerine geçmez,
982
+ payload'da system'den sonra sabit user mesajı olarak girer */
983
+ if (rec.text) session.summary = String(rec.text);
932
984
  } else if (rec.t === 'botdm') {
933
985
  /* botlar arası DM oturumu — admin izleyebilir, sidebar'da görünmez */
934
986
  session.isBotDm = true;
@@ -1129,6 +1181,7 @@ class Engine {
1129
1181
  s.messages = [];
1130
1182
  delete s.notes;
1131
1183
  delete s.notesAt;
1184
+ delete s.summary; // özet de sıfırlanır — taze başlangıç
1132
1185
  this.todos.set(sid, []);
1133
1186
  this.emit({ type: 'sessions' });
1134
1187
  return true;
@@ -1224,6 +1277,30 @@ class Engine {
1224
1277
 
1225
1278
  /* ---------- prompt assembly (the frugal part) ---------- */
1226
1279
 
1280
+ /* opencode instruction.ts port: workspace'teki proje talimatları (AGENTS.md,
1281
+ CLAUDE.md, CONTEXT.md) Beast Code system promptuna girer. Oturum başına
1282
+ BİR KEZ okunup session'da saklanır — system promptu epoch boyunca sabit
1283
+ kalır, önek cache bozulmaz (dosya sonradan değişse bile). */
1284
+ _projectInstructions(session) {
1285
+ if (!session) return '';
1286
+ if (session._projCtx !== undefined) return session._projCtx;
1287
+ let out = '';
1288
+ try {
1289
+ const ws = (session && session.workspace) || this.workspace;
1290
+ for (const name of ['AGENTS.md', 'CLAUDE.md', 'CONTEXT.md']) {
1291
+ let txt = '';
1292
+ try { txt = fs.readFileSync(path.join(ws, name), 'utf8'); } catch {}
1293
+ txt = String(txt || '').trim();
1294
+ if (!txt) continue;
1295
+ out += (out ? '\n\n' : '') + `--- ${name} ---\n` + txt.slice(0, 6000);
1296
+ if (out.length >= 8000) break;
1297
+ }
1298
+ } catch {}
1299
+ out = out.slice(0, 9000);
1300
+ session._projCtx = out;
1301
+ return out;
1302
+ }
1303
+
1227
1304
  /* Beast Code (IDE paneli): VS Code hızında çalışsın diye SECMET prompt.
1228
1305
  OpenCode kodlama disiplini gömülü: bağlam → plan → küçük diff → doğrula.
1229
1306
  Hafıza embedding araması, skills taraması, kişilik/kural blokları YOK —
@@ -1231,7 +1308,9 @@ class Engine {
1231
1308
  buildBcSystem(session) {
1232
1309
  const nowD = new Date();
1233
1310
  const localDate = nowD.toLocaleDateString('tr-TR');
1234
- const localTime = nowD.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' });
1311
+ /* önek-cache disiplini (opencode Context Epoch): dakika yerine saat
1312
+ system prompt her dakika değişse sağlayıcı önbelleği sürekli bozulur */
1313
+ const localTime = String(nowD.getHours()).padStart(2, '0') + ':00';
1235
1314
  const mode = String((session && session.bcMode) || 'auto').toLowerCase();
1236
1315
  const modeBlock =
1237
1316
  mode === 'plan'
@@ -1239,22 +1318,26 @@ class Engine {
1239
1318
  : mode === 'build'
1240
1319
  ? 'ÇALIŞMA MODU: BUILD 🛠 — son planı SOHBETTEKİ bağlamdan al ve UYGULA: dosyaları düzenle, komutları çalıştır, doğrula. Yeni plan açma; en fazla 1 cümlelik yön gösterimi + uygulama.'
1241
1320
  : 'ÇALIŞMA MODU: OTOMATİK ⚡ — önce 2-4 satırlık mini plan (todo_write), sonra hemen uygula + doğrula.';
1321
+ const proj = this._projectInstructions(session);
1242
1322
  return (
1243
1323
  'Sen BEAST CODE\u2019sun — IDE panelinde çalışan, OpenCode disiplinli hızlı bir kodlama ajanı. VS Code gibi çevik ol.\n' +
1244
1324
  `Çalışma klasörü: ${(session && session.workspace) || this.workspace}\n` +
1245
1325
  `Yerel zaman: ${localDate} ${localTime}\n` +
1246
1326
  `${modeBlock}\n` +
1327
+ (proj
1328
+ ? '# PROJE TALİMATLARI (workspace AGENTS/CLAUDE/CONTEXT dosyalarından — daima uy)\n' + proj + '\n'
1329
+ : '') +
1247
1330
  'WORKFLOW (her işte bu sıra):\n' +
1248
- '1) BAĞLAM: değiştirmeden önce ilgili dosyaları OKU (read_file / list_dir / grep mantığıyla hedefli ara) varsayım yapma, mevcut stili/konvansiyonu takip et.\n' +
1331
+ '1) BAĞLAM: değiştirmeden önce ilgili dosyaları OKU read_file satır numaralı döner (N: içerik), offset/limit ile büyük dosyayı pencerele; içerik araması için grep (regex), dosya adı araması için glob kullan; varsayım yapma, mevcut stili/konvansiyonu takip et.\n' +
1249
1332
  '2) PLAN: 2+ adımlı işlerde İLK EYLEM todo_write olsun (2-6 madde); her adım bitince status:"done" ile GÜNCELLE — liste bitene kadar iş bitmiş sayılmaz.\n' +
1250
- '3) EDİT: küçük, HEDEFLİ diff\u2019ler yaz (write_file ile yalnız ilgili bölümü etkile); dosyayı baştan yazma; ilgisiz yeniden biçimleme/kayıt boşluk değişikliği YAPMA.\n' +
1333
+ '3) EDİT: VAR OLAN dosyada önce edit_file kullan (old_string/new_string ile yalnız ilgili bölümü değiştir; birden çok eşleşme varsa bağlam ekle ya da replace_all); write_file yalnız YENİ dosya ya da tam yeniden yazım için. İlgisiz yeniden biçimleme/kayıt boşluk değişikliği YAPMA.\n' +
1251
1334
  '4) DOĞRULA: edit sonrası mümkünse derle/test et/lint çalıştır (run_command); hata varsa DÜZELT ve TEKRAR dene (en fazla 2 doğrulama turu) — kırmızı bırakma.\n' +
1252
1335
  '5) RAPOR: 1-3 satır — ne değişti + doğrulama sonucu (ör. "npm test ✓ 154/154"). Uzun açıklama yok.\n' +
1253
1336
  'HIZ KURALLARI:\n' +
1254
1337
  '- Kodu ve dosyaları DOĞRUDAN write_file ile yaz; basit dosya oluşturma/düzenleme için Python scripti yazma; python_run yalnız gerçek hesap/veri işleme gerekiyorsa.\n' +
1255
1338
  '- Bağımsız araç çağrılarını AYNI TURDA paralel ver (birden çok read_file tek turda).\n' +
1256
1339
  '- Dosyayı kullanıcı söylememişse list_dir ile yapıyı görüp kendin karar ver.\n' +
1257
- '- Run_command PowerShell ortamında çalışır (Windows).\n' +
1340
+ '- Run_command PowerShell ortamında çalışır (Windows): KALICI oturum — cd ve $env değişkenleri çağrılar arasında korunur; büyük çıktıda tam çıktı geçici dosyaya düşer, read_file ile oku. shell:"bash" verirsen git-bash ile bash sözdizimi koşar.\n' +
1258
1341
  '- Soru sorma, sohbet etme; uygulayıp özetle. Kullanıcı /plan /build /auto ile modu değiştirir.\n' +
1259
1342
  FORMAT_RULES
1260
1343
  );
@@ -1303,7 +1386,9 @@ class Engine {
1303
1386
  /* Beast Code (bcCode) oturumları buildBcSystem kullanır — buraya düşmez */
1304
1387
  const nowD = new Date();
1305
1388
  const localDate = nowD.toLocaleDateString('tr-TR');
1306
- const localTime = nowD.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' });
1389
+ /* önek-cache disiplini (opencode Context Epoch): dakika yerine saat
1390
+ system prompt her dakika değişse sağlayıcı önbelleği sürekli bozulur */
1391
+ const localTime = String(nowD.getHours()).padStart(2, '0') + ':00';
1307
1392
  let tz = '';
1308
1393
  try {
1309
1394
  tz = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
@@ -1329,6 +1414,7 @@ class Engine {
1329
1414
  '# ARAÇ KURALLARI\n' +
1330
1415
  [
1331
1416
  'Basit soruları araç kullanmadan doğrudan cevapla — hız önceliklidir.',
1417
+ 'Kod/dosya işlerinde: içerik araması grep (regex), dosya adı araması glob, VAR OLAN dosyayı değiştirme edit_file (write_file yalnız yeni dosya/tam yeniden yazım). read_file satır numaralı döner; offset/limit ile pencerele.',
1332
1418
  'Kullanıcı bir tarihte/saatte hatırlatılmasını isterse set_reminder kullan; when değerini ORTAMdaki bugüne göre hesapla (yerel saat). "Her sabah/gün/hafta" gibi tekrarlı isteklerde repeat alanını da ver (daily/weekly/monthly/weekdays veya cron).',
1333
1419
  'Kullanıcı kalıcı bir arka plan takibi isterse (fiyat eşiği, pil seviyesi, sayfa değişikliği) watcher_add ile izleyici kur; kurduktan sonra watcher_list ile doğrula ve kullanıcıya koşulu + kontrol sıklığını kısaca bildir.',
1334
1420
  'Anlık olay takipleri için (yeni mail, fiyat eşiği, dosya değişimi, webhook) event_subscribe kullan — cron/polling gerekmez; listeyi event_list ile göster, vazgeçirirse event_unsubscribe.',
@@ -1393,7 +1479,34 @@ class Engine {
1393
1479
  return 0;
1394
1480
  }
1395
1481
 
1396
- _buildPayload(system, messages, notes) {
1482
+ /* ---------- opencode port: bağlam matematiği (overflow.ts) ---------- */
1483
+
1484
+ /* Seçili model için özet payı düşülmüş kullanılabilir girdi alanı */
1485
+ _usableContext(sel) {
1486
+ const ctx = modelContextOf(sel);
1487
+ if (!ctx) return 0;
1488
+ /* overflow.ts:14-19: reserved = min(COMPACTION_BUFFER, maxOutput) */
1489
+ const reserved = Math.min(COMPACTION_BUFFER, OUTPUT_TOKEN_MAX);
1490
+ return Math.max(0, ctx - reserved);
1491
+ }
1492
+
1493
+ /* Dinamik geçmiş bütçesi: model bağlamı genişse 18k sabitinin ÜSTÜNE çıkar
1494
+ (asla altına inmez); cache'siz sağlayıcılarda şişmeyi HISTORY_BUDGET_MAX sınırlar */
1495
+ _historyBudget(sel) {
1496
+ const usable = this._usableContext(sel);
1497
+ if (!usable) return this.historyTokenBudget;
1498
+ return Math.max(this.historyTokenBudget, Math.min(Math.floor(usable * 0.85), HISTORY_BUDGET_MAX));
1499
+ }
1500
+
1501
+ /* Geçmiş + sistem + araç şemaları için kaba toplam token tahmini */
1502
+ _contextEstimate(session, sel) {
1503
+ const msgs = session.messages.reduce((a, m) => a + estMsgTokens(m), 0);
1504
+ const toolsCost = TOOLS.length ? estTokens(JSON.stringify(TOOLS)) : 0;
1505
+ return Math.ceil((msgs + toolsCost + 3500) * this.tokRatio); // 3500 ≈ system payı
1506
+ }
1507
+
1508
+ /* assistant(tool_calls) + sonuç tool mesajlarını BÖLMEYEN birimler */
1509
+ _msgUnits(messages) {
1397
1510
  const units = [];
1398
1511
  let i = 0;
1399
1512
  while (i < messages.length) {
@@ -1408,19 +1521,22 @@ class Engine {
1408
1521
  i++;
1409
1522
  }
1410
1523
  }
1411
- const budget = this.historyTokenBudget;
1524
+ return units;
1525
+ }
1526
+
1527
+ _buildPayload(system, messages, notes, budget, summary) {
1528
+ const units = this._msgUnits(messages);
1529
+ const B = Math.max(2000, Number(budget) || this.historyTokenBudget);
1412
1530
  const maxUnits = notes ? NOTES_MAX_UNITS : units.length; // notlar varken pencereyi sıkılaştır
1413
1531
  const picked = [];
1414
1532
  let used = 0;
1415
1533
  for (let u = units.length - 1; u >= 0; u--) {
1416
1534
  if (picked.length >= maxUnits) break;
1417
1535
  const cost = units[u].reduce(
1418
- (a, m) =>
1419
- a +
1420
- clamp(Math.ceil(estMsgTokens(m) * this.tokRatio), 1, this.historyTokenBudget),
1536
+ (a, m) => a + clamp(Math.ceil(estMsgTokens(m) * this.tokRatio), 1, B),
1421
1537
  0
1422
1538
  );
1423
- if (picked.length && used + cost > budget) break;
1539
+ if (picked.length && used + cost > B) break;
1424
1540
  picked.unshift(units[u]);
1425
1541
  used += cost;
1426
1542
  }
@@ -1442,7 +1558,20 @@ class Engine {
1442
1558
  flat[k] = { ...m, content: String(m.content).slice(0, TOOL_OUT_KEEP) + '\n…[kırpıldı]' };
1443
1559
  }
1444
1560
  }
1445
- return [{ role: 'system', content: system }, ...flat];
1561
+ /* opencode compaction replay (message-v2.ts:521-572): özet, system'den
1562
+ HEMEN sonra sabit user mesajı olarak girer → önek cache'e uygun
1563
+ (compaction sonrası yalnız eklemeli büyür, system bir daha değişmez) */
1564
+ const head = [];
1565
+ if (summary) {
1566
+ head.push({
1567
+ role: 'user',
1568
+ content:
1569
+ '[ÖNCEKİ KONUŞMANIN ÖZETİ — birebir geçmişi buradan hatırla]\n' +
1570
+ summary +
1571
+ '\n[Konuşma aşağıdaki mesajlarla DEVAM EDİYOR]',
1572
+ });
1573
+ }
1574
+ return [{ role: 'system', content: system }, ...head, ...flat];
1446
1575
  }
1447
1576
 
1448
1577
  _lastUserText(session) {
@@ -1505,6 +1634,7 @@ class Engine {
1505
1634
  if (seen++ >= keepFrom) msgLines.push(line);
1506
1635
  }
1507
1636
  out.push(...msgLines);
1637
+ out.push(JSON.stringify({ t: 'summary', text: session.summary || '', at: 0 }));
1508
1638
  out.push(JSON.stringify({ t: 'compacted', at: nowIso(), droppedBefore: keepFrom, noteBased: true }));
1509
1639
  const tmp = file + '.tmp';
1510
1640
  fs.writeFileSync(tmp, out.join('\n') + '\n');
@@ -1512,6 +1642,202 @@ class Engine {
1512
1642
  } catch {}
1513
1643
  }
1514
1644
 
1645
+ /* ---------- opencode port: gerçek compaction + prune (compaction.ts) ---------- */
1646
+
1647
+ /* Head bölgesini düz metne serileştirir (compaction.ts:54-85 port).
1648
+ Araç çıktıları SUMMARY_OUT_KEEP ile kırpılır — özet çağrısı küçük kalır. */
1649
+ _serializeHead(msgs) {
1650
+ const lines = [];
1651
+ for (const m of msgs) {
1652
+ if (m.role === 'user') {
1653
+ lines.push('[Kullanıcı]: ' + this._plainText(m).slice(0, 1500));
1654
+ } else if (m.role === 'assistant') {
1655
+ const txt = this._plainText(m).trim();
1656
+ if (txt) lines.push('[Asistan]: ' + txt.slice(0, 1200));
1657
+ for (const tc of m.tool_calls || []) {
1658
+ let args = '';
1659
+ try {
1660
+ args = JSON.stringify(JSON.parse((tc.function && tc.function.arguments) || '{}'));
1661
+ } catch {
1662
+ args = String((tc.function && tc.function.arguments) || '');
1663
+ }
1664
+ lines.push(`[Araç çağrısı]: ${tc.function && tc.function.name}(${args.slice(0, 800)})`);
1665
+ }
1666
+ } else if (m.role === 'tool') {
1667
+ const out = String(m.content || '');
1668
+ lines.push(
1669
+ '[Araç sonucu]: ' + (out.length <= SUMMARY_OUT_KEEP ? out : out.slice(0, SUMMARY_OUT_KEEP) + '\n[kırpıldı]')
1670
+ );
1671
+ }
1672
+ }
1673
+ return lines.filter(Boolean).join('\n');
1674
+ }
1675
+
1676
+ /* compaction.ts:319-557 port. Bağlam taşarsa: kuyruk (son ~%25) birebir
1677
+ korunur, baş metne serileştirilip TEK küçük model çağrısıyla özetlenir,
1678
+ geçmiş [özet + kuyruk] olarak yeniden yazılır. Döndürür: compact oldu mu */
1679
+ async _compactHistory(session, sel, signal) {
1680
+ try {
1681
+ const usable = this._usableContext(sel) || this.historyTokenBudget * 2;
1682
+ const budget = clamp(Math.floor(usable * 0.25), MIN_PRESERVE_RECENT, MAX_PRESERVE_RECENT);
1683
+ const units = this._msgUnits(session.messages);
1684
+ if (units.length < 3) return false;
1685
+ let keepIdx = 0;
1686
+ let used = 0;
1687
+ for (let u = units.length - 1; u >= 0; u--) {
1688
+ const cost = units[u].reduce((a, m) => a + Math.ceil(estMsgTokens(m) * this.tokRatio), 0);
1689
+ if (used + cost > budget) break;
1690
+ keepIdx = u;
1691
+ used += cost;
1692
+ }
1693
+ if (keepIdx <= 0) return false; // özetlenecek head yok
1694
+ const headMsgs = units.slice(0, keepIdx).flat();
1695
+ const transcript = this._serializeHead(headMsgs);
1696
+ if (!transcript.trim()) return false;
1697
+
1698
+ /* core/session/compaction.ts:16-56 şablonu (Türkçe başlıklarla) */
1699
+ const prev = session.summary
1700
+ ? `<önceki-özet>\n${session.summary}\n</önceki-özet>\n\n` +
1701
+ 'önceki-özet, konuşmadan önceki her şeyi özetler: ikisini BİRLEŞTİREREK yeni özet çıkar. ' +
1702
+ 'önceki-özet bu birleşimden sonra atılır — yeni özete taşımadığın her şey KAYBOLUR. ' +
1703
+ 'Konuşma daha yenidir; çelişkide konuşma kazanır. Biten işleri "Aktif"ten "Tamamlanan"a taşı.\n\n'
1704
+ : '';
1705
+ const prompt =
1706
+ 'Aşağıdaki <konuşma> etiketindeki sohbet geçmişinden, işe başka bir ajan devam edecek şekilde özet çıkar.\n' +
1707
+ 'Tam olarak <şablon> içindeki Markdown yapısını koru; şablon etiketlerini yanıtına yazma.\n' +
1708
+ '<şablon>\n' +
1709
+ '## Amaç\n- [kullanıcının ne yapmaya çalıştığı, 1-2 cümle]\n\n' +
1710
+ '## Önemli Detaylar\n- [kısıtlar, kararlar ve gerekçeleri, önemli olgular, kesin yollar/sayılar; yoksa "(yok)"]\n\n' +
1711
+ '## İş Durumu\n### Tamamlanan\n- [biten işler, doğrulanan olgular; yoksa "(yok)"]\n\n' +
1712
+ '### Aktif\n- [sürüyen işler, yarım değişiklikler; yoksa "(yok)"]\n\n' +
1713
+ '### Engelli\n- [hatalar, bilinmeyenler; yoksa "(yok)"]\n\n' +
1714
+ '## Sonraki Adım\n1. [somut ilk eylem; yoksa "(yok)"]\n\n' +
1715
+ '## İlgili Dosyalar\n- [dosya/klasör yolu: neden önemli; yoksa "(yok)"]\n' +
1716
+ '</şablon>\n\n' +
1717
+ 'Kurallar:\n- Boş olsa bile her bölümü koru.\n- Kısa madde maddeler yaz, paragraf kurma.\n' +
1718
+ '- Dosya yollarını, komutları, hata metinlerini, URL\u2019leri BİREBİR aktar.\n' +
1719
+ '- Özetleme yaptığını asla belirtme.\n\n' +
1720
+ prev +
1721
+ `<konuşma>\n${transcript}\n</konuşma>`;
1722
+
1723
+ emitSafe(this, session.id, {
1724
+ type: 'status',
1725
+ status: `⇩ bağlam doldu — opencode tarzı özetleme devrede (head ${headMsgs.length} mesaj, kuyruk ${session.messages.length - headMsgs.length} korunuyor)`,
1726
+ });
1727
+ const small = this.modelFor('subagent') || sel || this.sel;
1728
+ const res = await chatOnce(
1729
+ small,
1730
+ { messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
1731
+ { signal }
1732
+ );
1733
+ const text = String(res.content || '').replace(/```(?:markdown)?/gi, '').trim();
1734
+ if (!text) return false;
1735
+ session.summary = text.slice(0, 12000);
1736
+ const tail = units.slice(keepIdx).flat();
1737
+ session.messages = tail;
1738
+ session.notesAt = clamp(session.notesAt || 0, 0, tail.length);
1739
+ this._persistCompaction(session);
1740
+ emitSafe(this, session.id, {
1741
+ type: 'status',
1742
+ status: `✓ bağlam özeti yazıldı — geçmiş ${headMsgs.length + tail.length} → ${tail.length} mesaj, önek cache tazelendi`,
1743
+ });
1744
+ return true;
1745
+ } catch {
1746
+ return false; // compaction asla sohbeti bozmasın
1747
+ }
1748
+ }
1749
+
1750
+ /* Compaction sonrası disk yeniden yazımı: meta/bot/todo/notes/summary +
1751
+ kuyruk mesajları. Atomik — patlarsa eski dosya sağlam kalır. */
1752
+ _persistCompaction(session) {
1753
+ try {
1754
+ const file = this._file(session.id);
1755
+ let meta = null, meta2 = null, bot = null, todo = null;
1756
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
1757
+ if (!line.trim()) continue;
1758
+ let r;
1759
+ try { r = JSON.parse(line); } catch { continue; }
1760
+ if (r.t === 'meta') meta = line;
1761
+ else if (r.t === 'meta2') meta2 = line;
1762
+ else if (r.t === 'bot') bot = line;
1763
+ else if (r.t === 'todo') todo = line; // append-only: son satır kazanır
1764
+ }
1765
+ const out = [];
1766
+ if (meta) out.push(meta);
1767
+ else out.push(JSON.stringify({ t: 'meta', id: session.id, code: session.code, createdAt: session.createdAt }));
1768
+ if (meta2) out.push(meta2);
1769
+ if (bot) out.push(bot);
1770
+ if (todo) out.push(todo);
1771
+ out.push(JSON.stringify({ t: 'notes', text: session.notes || '', at: session.notesAt || 0 }));
1772
+ out.push(JSON.stringify({ t: 'summary', text: session.summary || '', at: nowIso() }));
1773
+ for (const m of session.messages) out.push(JSON.stringify({ t: 'msg', ...m }));
1774
+ out.push(JSON.stringify({ t: 'compacted', at: nowIso(), summary: true }));
1775
+ const tmp = file + '.tmp';
1776
+ fs.writeFileSync(tmp, out.join('\n') + '\n');
1777
+ fs.renameSync(tmp, file);
1778
+ } catch {}
1779
+ }
1780
+
1781
+ /* Taşma kontrolü (overflow.ts:22-34): gerçek usage varsa onu esas al,
1782
+ yoksa tahmini kullan. usable'ın %95'i dolunca compaction tetiklenir. */
1783
+ _overContext(session, sel, lastUsageTotal) {
1784
+ const usable = this._usableContext(sel);
1785
+ if (!usable) return false;
1786
+ const total =
1787
+ lastUsageTotal ||
1788
+ session.messages.reduce((a, m) => a + estMsgTokens(m), 0) * this.tokRatio + 6000;
1789
+ return total >= usable * 0.95;
1790
+ }
1791
+
1792
+ /* compaction.ts:273-317 prune port: sondan geriye yürür, en yeni kullanıcı
1793
+ turunu korur; PRUNE_PROTECT (40k) aşan eski araç çıktılarını temizler.
1794
+ En az PRUNE_MINIMUM (20k) kazanç yoksa hiç dokunmaz. tool_call_id çiftleri
1795
+ korunur — sadece içerik temizlenir, önek cache bozulmaz. Döndürür: kazanılan token */
1796
+ _pruneSession(session) {
1797
+ try {
1798
+ if (this.ctrls.has(session.id)) return 0;
1799
+ const msgs = session.messages;
1800
+ let total = 0, pruned = 0, turns = 0;
1801
+ const targets = [];
1802
+ for (let i = msgs.length - 1; i >= 0; i--) {
1803
+ const m = msgs[i];
1804
+ if (m.role === 'user') turns++;
1805
+ if (turns < 2) continue; // en yeni tur dokunulmaz
1806
+ if (m.role === 'tool' && typeof m.content === 'string' && m.content.length > 400) {
1807
+ const est = Math.ceil(estTokens(m.content) * this.tokRatio);
1808
+ total += est;
1809
+ if (total > PRUNE_PROTECT) {
1810
+ pruned += est;
1811
+ targets.push(m);
1812
+ }
1813
+ }
1814
+ }
1815
+ if (pruned <= PRUNE_MINIMUM) return 0;
1816
+ const marker = '[eski araç çıktısı temizlendi — özet ve son çıktılar yeterli]';
1817
+ const ids = new Map(targets.map((m) => [m.tool_call_id, marker]));
1818
+ for (const m of targets) m.content = marker;
1819
+ try {
1820
+ const file = this._file(session.id);
1821
+ const lines = fs.readFileSync(file, 'utf8').split('\n');
1822
+ for (let i = 0; i < lines.length; i++) {
1823
+ if (!lines[i].trim()) continue;
1824
+ let r;
1825
+ try { r = JSON.parse(lines[i]); } catch { continue; }
1826
+ if (r.t === 'msg' && r.role === 'tool' && ids.has(r.tool_call_id)) {
1827
+ r.content = marker;
1828
+ lines[i] = JSON.stringify({ t: 'msg', ...r });
1829
+ }
1830
+ }
1831
+ const tmp = file + '.tmp';
1832
+ fs.writeFileSync(tmp, lines.join('\n'));
1833
+ fs.renameSync(tmp, file);
1834
+ } catch {}
1835
+ return pruned;
1836
+ } catch {
1837
+ return 0;
1838
+ }
1839
+ }
1840
+
1515
1841
  /* Cevap verildikten sonra çağrılır: son not update'ten bu yana NOTES_EVERY
1516
1842
  (14) mesaj biriktiginde özet çıkarıp oturum notlarını günceller.
1517
1843
  Notlar oturum dosyasına (code ile eşli) yazılır ve _chatTurn'da aynı
@@ -2308,6 +2634,10 @@ class Engine {
2308
2634
  /* CEO: uygulayıcı araçlar kapalı — her şey paralel ajana devredilir */
2309
2635
  activeTools = activeTools.filter((t) => !CEO_EXEC_TOOLS.has(t.function.name));
2310
2636
  }
2637
+ if (session.bcCode && session.bcMode === 'plan') {
2638
+ /* opencode plan agent: salt-okur zorlaması — araç seti GERÇEKten daralır */
2639
+ activeTools = activeTools.filter((t) => PLAN_ALLOW_TOOLS.has(t.function.name));
2640
+ }
2311
2641
  if (perms.length === 1 && perms[0] === 'chat') {
2312
2642
  system +=
2313
2643
  '\n\n# KISITLI MOD\nTüm araçların (komut, dosya, web, tarayıcı, hafıza) kapalı. Sadece yazarak cevap ver. ' +
@@ -2328,7 +2658,15 @@ class Engine {
2328
2658
  `\n\n# OTURUM NOTLARI (oturum ${session.code || '?'} — bu oturumun önceki konuşma özeti; birebir geçmişi buradan hatırla)\n` +
2329
2659
  session.notes;
2330
2660
  }
2331
- let payload = this._buildPayload(system, session.messages, session.notes);
2661
+ /* önek-cache disiplini (opencode request.ts:184): araç sırası oturum
2662
+ boyunca sabit olmalı — alfabetik sıralama sağlayıcı önbelleğini bozmaz */
2663
+ activeTools = [...activeTools].sort((a, b) =>
2664
+ String(a.function.name).localeCompare(String(b.function.name))
2665
+ );
2666
+ /* opencode port: model bağlamına göre dinamik bütçe + compaction özeti */
2667
+ const selEarly = this.sessionModel.get(String(session.id)) || this.modelFor(null) || this.sel;
2668
+ const dynBudget = this._historyBudget(selEarly);
2669
+ let payload = this._buildPayload(system, session.messages, session.notes, dynBudget, session.summary);
2332
2670
  // Vision model sadece mesajda görsel varsa devreye girer; terminal ise prompt'un
2333
2671
  // shell/command işi olduğunda. Aksi halde ana model kullanılır.
2334
2672
  let role = this._lastUserHasImage(session) ? 'vision' : null;
@@ -2375,7 +2713,7 @@ class Engine {
2375
2713
  type: 'status',
2376
2714
  status: `\u{1F5BC} bu model görüntü girişini desteklemiyor — sohbetten ${removed} görsel kaldırıldı, mesaj görüntüsüz yanıtlanıyor (/change ile görsel destekli modele geçebilirsin)`,
2377
2715
  });
2378
- payload = this._buildPayload(system, session.messages, session.notes);
2716
+ payload = this._buildPayload(system, session.messages, session.notes, dynBudget, session.summary);
2379
2717
  res = await this._streamWithFallbacks(
2380
2718
  session,
2381
2719
  payload,
@@ -2398,6 +2736,38 @@ class Engine {
2398
2736
  return res;
2399
2737
  }
2400
2738
 
2739
+ /* opencode port (prompt.ts:96-100 + processor cleanup): önceki tur yarıda
2740
+ kesildiyse (abort/hata), sonucu gelmeyen tool çağrılarına sentetik hata
2741
+ yanıtı yazılır — katı sağlayıcıdaki "messages illegal" (400) önlenir */
2742
+ _repairOrphanTools(session) {
2743
+ try {
2744
+ const msgs = session.messages;
2745
+ for (let i = msgs.length - 1; i >= 0; i--) {
2746
+ const m = msgs[i];
2747
+ if (m.role === 'user') return; // temiz sınır — sorun yok
2748
+ if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
2749
+ const answered = new Set();
2750
+ for (let j = i + 1; j < msgs.length; j++) {
2751
+ if (msgs[j].role === 'tool') answered.add(msgs[j].tool_call_id);
2752
+ }
2753
+ for (const tc of m.tool_calls) {
2754
+ if (!tc || !tc.id || answered.has(tc.id)) continue;
2755
+ const toolMsg = {
2756
+ role: 'tool',
2757
+ tool_call_id: tc.id,
2758
+ name: (tc.function && tc.function.name) || '',
2759
+ content:
2760
+ '[araç çalıştırması kesildi — tur iptal/kesinti nedeniyle tamamlanamadı; gerekiyorsa yeniden çağır]',
2761
+ };
2762
+ msgs.push(toolMsg);
2763
+ try { this._append(session, toolMsg); } catch {}
2764
+ }
2765
+ return;
2766
+ }
2767
+ }
2768
+ } catch {}
2769
+ }
2770
+
2401
2771
  async _run(session) {
2402
2772
  const ctrl = new AbortController();
2403
2773
  this.ctrls.set(session.id, ctrl);
@@ -2406,8 +2776,16 @@ class Engine {
2406
2776
  try {
2407
2777
  if (!this.sel && !this.sessionModel.get(String(session.id))) throw new Error('Model yapılandırılmadı — %APPDATA%\\beast\\config.yaml ve .env kontrol et');
2408
2778
 
2779
+ /* opencode port: yetim tool_calls onarımı — önceki tur iptal/kesintiye
2780
+ uğradıysa sonuç bekleyen araç çağrılarına sentetik hata yanıtı yazılır;
2781
+ yoksa katı sağlayıcılar sonraki istekte 400 verir */
2782
+ this._repairOrphanTools(session);
2783
+
2409
2784
  let nudged = false; // görev listesi disiplini: run başına en fazla 1 hatırlatma
2410
2785
  let wrapNudged = false; // tur limitine yaklaşınca zarif kapanış (bg ajanlar)
2786
+ let usageTotal = 0; // en yüksek görülen total_tokens — compaction tetiği
2787
+ let compacted = false; // run başına en fazla 1 compaction
2788
+ const recentSigs = []; // doom-loop dedektörü: son araç imzaları
2411
2789
  for (let turn = 0; turn < MAX_TURNS; turn++) {
2412
2790
  /* SÜRE SINIRI YOK — ama sonsuz tur da yok: bg ajan son 3 tura gelirken
2413
2791
  "raporu yaz ve bitir" uyarısı alır; MAX_TURNS sert tavan olarak kalır. */
@@ -2424,11 +2802,40 @@ class Engine {
2424
2802
  } catch {}
2425
2803
  emit({ type: 'message', message: wmsg });
2426
2804
  }
2805
+ /* opencode port (prompt.ts:1281): ana oturumda son turda zorunlu
2806
+ nihai cevap — tur limiti sessizce dolup boş 'done' dönmesin */
2807
+ if (!session.bgJob && turn === MAX_TURNS - 1 && !wrapNudged) {
2808
+ wrapNudged = true;
2809
+ const wmsg = {
2810
+ role: 'user',
2811
+ content:
2812
+ '[SON TUR] Tur limitine ulaşıldı: YENİ araç çağırma — eldeki bilgilerle NİHAİ cevabını şimdi ver. Eksikler varsa açıkça belirt.',
2813
+ };
2814
+ session.messages.push(wmsg);
2815
+ try {
2816
+ this._append(session, wmsg);
2817
+ } catch {}
2818
+ emit({ type: 'message', message: wmsg });
2819
+ }
2820
+ /* opencode port (prompt.ts:1161 + overflow.ts): bağlam dolduysa
2821
+ compaction checkpoint — kuyruk korunur, baş özetlenir */
2822
+ const selNow = this.sessionModel.get(String(sid)) || this.modelFor(null) || this.sel;
2823
+ if (!compacted && this._overContext(session, selNow, usageTotal)) {
2824
+ compacted = true;
2825
+ await this._compactHistory(session, selNow, ctrl.signal);
2826
+ }
2427
2827
  this._bgTrim(session); // #21 bg geçmişini olabildiğince ince tut
2428
2828
  emit({ type: 'status', status: 'thinking' });
2429
2829
  const res = await this._chatTurn(session, ctrl.signal, (delta) =>
2430
2830
  emit({ type: 'token', delta })
2431
2831
  );
2832
+ /* opencode port (processor.ts:477-482): step-finish usage'ı compaction
2833
+ tetiğine besle — gerçek toplam biliniyorsa tahmine gerek kalmaz */
2834
+ const uTot =
2835
+ res.usage &&
2836
+ (res.usage.total_tokens ||
2837
+ (res.usage.prompt_tokens || 0) + (res.usage.completion_tokens || 0));
2838
+ if (uTot > usageTotal) usageTotal = uTot;
2432
2839
 
2433
2840
  const assistant = { role: 'assistant', content: res.content || '' };
2434
2841
  if (res.toolCalls && res.toolCalls.length) assistant.tool_calls = res.toolCalls;
@@ -2464,6 +2871,7 @@ class Engine {
2464
2871
  Not/memory/skill bakımı artık arka planda (_postRunHousekeeping). */
2465
2872
  this._clearCrash();
2466
2873
  this._maybeCompact(sid);
2874
+ this._pruneSession(session); // opencode port: eski araç çıktıları temizlenir (fork edilen prune)
2467
2875
  emit({ type: 'done', usage: res.usage || null, meta: res.meta || null });
2468
2876
  this._bgFinish(sid, 'done');
2469
2877
  this.flushPendingReports(sid);
@@ -2473,16 +2881,45 @@ class Engine {
2473
2881
 
2474
2882
  // Paralel yürütme — bağımsız çağrılar beklemesin
2475
2883
  session.toolsSinceReflect = (session.toolsSinceReflect || 0) + res.toolCalls.length;
2884
+ if (!this._knownToolNames) this._knownToolNames = new Set(TOOLS.map((t) => t.function.name));
2476
2885
  await Promise.all(
2477
2886
  res.toolCalls.map(async (tc) => {
2478
- const name = tc.function && tc.function.name;
2887
+ let name = tc.function && tc.function.name;
2888
+ /* opencode port (llm.ts:296-312 repairToolCall): model aracı adını
2889
+ yanlış yazdıysa büyük/küçük harf normalizasyonuyle onar — tur kaybı yok */
2890
+ if (name && !this._knownToolNames.has(name)) {
2891
+ const fixed = TOOLS.find((t) => t.function.name.toLowerCase() === String(name).trim().toLowerCase());
2892
+ if (fixed) {
2893
+ emitSafe(this, sid, {
2894
+ type: 'status',
2895
+ status: `🔧 araç adı düzeltildi: ${name} → ${fixed.function.name}`,
2896
+ });
2897
+ name = fixed.function.name;
2898
+ }
2899
+ }
2479
2900
  let args = {};
2480
2901
  try {
2481
2902
  args = JSON.parse((tc.function && tc.function.arguments) || '{}');
2482
2903
  } catch {}
2483
2904
  emit({ type: 'tool-start', callId: tc.id, name, args });
2484
2905
  emit({ type: 'status', status: name });
2485
- let out = await this._execTool(name, args, ctrl.signal, sid);
2906
+ /* opencode port (processor.ts:29 + 356-380): aynı araç + birebir
2907
+ aynı argüman 3 kez koştuysa 4.'yü çalıştırma — döngüye para/hız
2908
+ akıtma; modele hata bildirimiyle yol göster */
2909
+ const sig = name + '\u0000' + String((tc.function && tc.function.arguments) || '');
2910
+ const dups = recentSigs.reduce((a, s) => a + (s === sig ? 1 : 0), 0);
2911
+ recentSigs.push(sig);
2912
+ let out;
2913
+ if (dups >= DOOM_LOOP_THRESHOLD) {
2914
+ out = JSON.stringify({
2915
+ error:
2916
+ `doom-loop: ${name} aynı argümanlarla ${DOOM_LOOP_THRESHOLD} kez çalıştı ve hep aynı sonucu verdi. ` +
2917
+ 'Aynı çağrıyı tekrarlamak işe yaramaz — farklı bir yöntem/argüman dene ya da eldeki bilgilerle nihai cevabı ver.',
2918
+ });
2919
+ emitSafe(this, sid, { type: 'status', status: `⛔ doom-loop: ${name} tekrarı engellendi` });
2920
+ } else {
2921
+ out = await this._execTool(name, args, ctrl.signal, sid);
2922
+ }
2486
2923
  // Ekran görüntüsü gibi araçlar görseli sonraki tura enjekte eder
2487
2924
  let injectedImage = null;
2488
2925
  try {
@@ -2853,7 +3290,7 @@ class Engine {
2853
3290
  t.function.name !== 'delegate_task' &&
2854
3291
  t.function.name !== 'set_reminder' &&
2855
3292
  !t.function.name.startsWith('email_')
2856
- );
3293
+ ).sort((a, b) => String(a.function.name).localeCompare(String(b.function.name))); // önek-cache: sabit sıra
2857
3294
  const role = 'subagent';
2858
3295
  const sel = this.modelFor(role) || this.sel;
2859
3296
  if (this.roleModels[role]) {
@@ -2875,7 +3312,12 @@ class Engine {
2875
3312
  for (let turn = 0; turn < SUB_MAX_TURNS; turn++) {
2876
3313
  const res = await chatStreamAuto(
2877
3314
  sel,
2878
- { messages: [{ role: 'system', content: system }, ...msgs.slice(1)], tools: subTools, reasoningEffort: this._thinkEffort() },
3315
+ {
3316
+ messages: [{ role: 'system', content: system }, ...msgs.slice(1)],
3317
+ tools: subTools,
3318
+ reasoningEffort: this._thinkEffort(),
3319
+ cacheKey: String(sessionId || 'subagent'),
3320
+ },
2879
3321
  { signal: ctrl.signal }
2880
3322
  );
2881
3323
  const assistant = { role: 'assistant', content: res.content || '' };