beast-agent 0.26.0 → 0.26.2

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.
@@ -6,9 +6,10 @@
6
6
 
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
- const { chatStream, chatOnce } = require('./llm');
9
+ const { chatStream, chatStreamAuto, chatOnce } = require('./llm');
10
10
  const { chatCompletionsUrl } = require('./config');
11
11
  const tools = require('./tools');
12
+ const research = require('./research');
12
13
  const memory = require('./memory');
13
14
  const skills = require('./skills');
14
15
  const { estTokens, estMsgTokens } = require('./tokens');
@@ -34,9 +35,10 @@ const SUP_IDLE_MIN = 2; // ...bu kadar süredir hiç aktivite yoksa TAKILDI
34
35
  const SUP_LONG_MIN = 6; // bu kadar süredir koşuyorsa ara kontrol nödü
35
36
  const SUP_NUDGE_COOLDOWN_MIN = 4; // aynı iş için iki uyarı arası min süre
36
37
  const BG_FIX_MAX = 2; // paralel ajanın otomatik öz-kurtarma hakkı (bitince CEO devreye girer)
37
- /* Zarif bitirme: BU sürede bitmezse ajan artık yeni araştırma yapmaz,
38
- elindekiyle SON RAPORU yazar ve düzgün biter "görev bitmeden patlama" yok */
39
- const BG_WRAP_MIN = 4;
38
+ /* SÜRE SINIRI YOK: bg ajanlar dakika bazında kesilmez. Sonsuz döngü koruması
39
+ üç katmanla sağlanır: 1) aktivite yoksa öz-kurtarma + CEO (stuck denetimi),
40
+ 2) MAX_TURNS sert tur tavanı, 3) tur limitine yaklaşınca zarif "raporu yaz"
41
+ uyarısı (_run içinde). Dakika bazlı wrap-up KALDIRILDI (BG_WRAP_MIN). */
40
42
  /* #5 hız: eşzamanlı arka plan LLM turu sınırı — rate-limit yemeden maksimum paralellik */
41
43
  const BG_MAX_CONCURRENT_DEFAULT = 4;
42
44
  /* #17 kalıcı başarısızlıkta owner'a anlık uyarı */
@@ -69,13 +71,13 @@ const FORMAT_RULES =
69
71
  const PERM_TOOL_SETS = {
70
72
  all: null, // tüm araçlar (filtre yok)
71
73
  web: new Set([
72
- 'web_search', 'http_fetch',
74
+ 'web_search', 'http_fetch', 'deep_search',
73
75
  'browser_open', 'browser_read', 'browser_snapshot', 'browser_screenshot',
74
76
  'browser_click', 'browser_type', 'browser_press', 'browser_scroll', 'browser_select',
75
77
  'ocr_read',
76
78
  ]),
77
79
  read: new Set([
78
- 'web_search', 'http_fetch',
80
+ 'web_search', 'http_fetch', 'deep_search',
79
81
  'browser_open', 'browser_read', 'browser_snapshot',
80
82
  'list_dir', 'read_file',
81
83
  ]),
@@ -99,7 +101,7 @@ function normalizePerms(p) {
99
101
  const CEO_EXEC_TOOLS = new Set([
100
102
  'run_command', 'read_file', 'write_file', 'list_dir',
101
103
  'python_run',
102
- 'web_search', 'http_fetch',
104
+ 'web_search', 'http_fetch', 'deep_search',
103
105
  'browser_open', 'browser_read', 'browser_screenshot', 'browser_snapshot',
104
106
  'browser_click', 'browser_type', 'browser_press', 'browser_scroll', 'browser_select',
105
107
  'computer_look', 'computer_act',
@@ -202,6 +204,7 @@ class Engine {
202
204
  this.lockdown = !!opts.lockdown; // varsayılan kısıt (oturum bazlı override edilmezse)
203
205
  this.sessionPerm = new Map(); // sessionId -> ['web'] | ['web','read'] | ['chat'] (kişi/bot bazlı izin)
204
206
  this.sessionTools = new Map(); // sessionId -> Set(araç adları) — bot skill kısıtı
207
+ this.sessionModel = new Map(); // sessionId -> chain entry — bot bazlı model override (setSessionModel)
205
208
  this.resolveBot = opts.resolveBot || null; // botId -> bot bilgisi (main enjekte eder)
206
209
  /* bot oturumu hafıza köprüsü: botun kendi SOUL/USER/MEMORY dosyaları */
207
210
  this.botMemory = opts.botMemory || null;
@@ -333,6 +336,16 @@ class Engine {
333
336
  else this.sessionTools.delete(id);
334
337
  }
335
338
 
339
+ /* BOT MODEL OVERRIDE: her bot farklı model kullanabilir (sel boşsa global seçim).
340
+ sel biçimi: 'providerId::model' — zincirde yoksa sessizce global'e düşer. */
341
+ setSessionModel(sessionId, sel) {
342
+ const id = String(sessionId || '');
343
+ if (!id) return;
344
+ const resolved = sel ? this._resolve(String(sel)) : null;
345
+ if (resolved) this.sessionModel.set(id, resolved);
346
+ else this.sessionModel.delete(id);
347
+ }
348
+
336
349
  /* Oturumun bağlı olduğu MÜŞTERİ botu (admin/seasız → null → global hafıza) */
337
350
  _sessionBotCtx(session) {
338
351
  if (!session || !session.botId || typeof this.resolveBot !== 'function') return null;
@@ -608,7 +621,7 @@ class Engine {
608
621
  }
609
622
  }
610
623
  }
611
- const res = await chatStream(
624
+ const res = await chatStreamAuto(
612
625
  c,
613
626
  { messages: msgs, tools: activeTools, reasoningEffort: this._thinkEffortFor(session) },
614
627
  {
@@ -1285,11 +1298,13 @@ class Engine {
1285
1298
  '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.',
1286
1299
  '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.',
1287
1300
  'Kullanıcı "artık hep böyle yap / bunu unutma" tarzı kalıcı talimat verirse kural olarak kaydet: sohbette /rule <metin> kullanmasını söyle VEYA kullanıcı isterse event_subscribe ile olaya bağlan (mail/fiyat/dosya/webhook).',
1288
- 'Kullanıcının mesajında 2+ ayrı iş/hedef varsa (örn "X yap ve sonra Y\u2019i kontrol et") KODLAMAYA/İŞE BAŞLAMADAN önce todo_write ile plan çıkar ve sırayla yürüt; her adımı tamamlarken güncelle.',
1301
+ 'Kullanıcının mesajında 2+ ayrı iş/hedef varsa (örn "X yap ve sonra Y\u2019i kontrol et") KODLAMAYA/İŞE BAŞLAMADAN önce todo_write ile plan çıkar ve sırayla yürüt; her adımı tamamlarken güncelle. LİSTE DİSİPLİNİ: her adım bittiği AN status:"done" yap; son cevabını vermeden önce tüm maddeler done olmalı — yapılmayacaksa listeden düş. Listeyi yarım bırakma.',
1302
+ 'HIZ KURALI: Bağımsız işleri AYNI turda birden çok tool_calls ile PARALEL ver. Küçük işleri tek tek çağırma — her ayrı araç turu 5-15 sn LLM gecikmesidir: 3+ küçük komutu TEK run_command\u2019te `;` ile zincirle (örn `git status; node -v; dir`), döngülü/çoklu işleri TEK python_run betiğinde topla, çok dosyalık değişikliği TEK script ile yap. Her küçük işlem için ayrı araç çağrısı açmak yavaşlığın 1 numaralı sebebidir.',
1289
1303
  this.ceoMode
1290
1304
  ? 'Bağımsız alt-işleri run_background ile PARALEL ajana devret; işi KENDİN YÜRÜTME — emri ver, takip et, raporla.'
1291
1305
  : 'Bağımsız alt-işleri delegate_task ile devret; kendi başına halledebileceğin işleri devretme.',
1292
1306
  'Güncel/dış bilgi gerekiyorsa web_search kullan (zincir DAHİLİ tarayıcıyla başlar — gerçek Chromium ile Google); hızlı ham metin okuması için http_fetch kullan.',
1307
+ 'DERİN ARAŞTIRMA: web_search\u2019in sonucu yetersizse/istenen bilgi listede YOKSA aramayı tekrar tekrar deneme yerine deep_search kullan — 1-4 sorgu varyantını (eş anlamlı, Türkçe+İngilizce yazımlar) PARALEL aratır ve ilk sayfaları GİZLİ gerçek Chromium\u2019da açıp tam metin okur (paneli açmaz, kullanıcıyı rahatsız etmez; JS/SPA sayfalar çalışır). Fiyat karşılaştırma, çok kaynaklı araştırma, "her şeyi bul" işleri ve Türkçe sorgularda sonuç zayıfsa doğrudan deep_search seç. read_top=0 verirsen yalnız harmanlanmış sonuç listesi döner.',
1293
1308
  'ARAMA-DİSİPLİN: bir bilgiyi 2-3 denemede bulamazsan TAKILMA — farklı bir açıya/sorguya geç, yine yoksa bulabildiğin kısmi bilgiyle cevap ver ve neyi bulamadığını açıkça söyle. Kapalı/gizli içerik (private profil, login arkası veri) peşinde KOŞMA — bulunamayacağı belliyse hemen vazgeç.',
1294
1309
  'TARAYICI VARSAYILANI (ZORUNLU): kullanıcı "şu siteyi aç", "bunu ara / google\u2019da ara", "şu sayfaya git" gibi bir web isteği verdiğinde HEP DAHİLİ TARAYICIYI KULLAN (browser_open ile aç → açılış yanıtında hazır snapshot gelir → ref numaralarıyla browser_click/browser_type/browser_select ile hareket et; HER eylem yanıtında taze snapshot döner — ayrıca browser_snapshot çağırma, yanıttaki refleri kullan; browser_read ile metin oku; sadece görsel yerleşim/grafik gerekiyorsa browser_screenshot çek). Tarih/saat alanlarını (type=date/time) browser_type ile DÜZ METİN yaz ("15.03.2026", "2026-03-15") — takvimden tıklamaya çalışma, alan programatik ayarlanır. Panel ekranın sağında açılır ve kullanıcı da sayfayı anında görür; JS/login/SPA/dinamik içerik için idealdir. Bu kural TÜM oturumları kapsar — masaüstü sohbeti ve çok kullanıcılı botlar (WhatsApp vb.) dahil.' +
1295
1310
  ' DIŞ TARAYICI İSTİSNASI: kullanıcı AÇIKÇA "chrome\u2019da aç", "firefox\u2019ta aç", "başka tarayıcıda aç", "normal tarayıcıda aç", "kendi tarayıcımda aç" derse O ZAMAN run_command ile `start "" <url>` çalıştır — sistem varsayılan (dış) tarayıcısında açılır. Kullanıcı dış tarayıcı istemedikçe ASLA dış tarayıcı açma.' +
@@ -1299,6 +1314,7 @@ class Engine {
1299
1314
  'CONUŞMA ODAĞI SENDE KALSIN: kullanıcı seninle konuşurken iş çıkmışsa — uzun da olsa UFACIK da (tek komutluk dizin listesi, tek dosya okuma, tek arama…) — run_background ile PARALEL ajana devret; ana sohbet hiçbir işi beklemez; bittiğinde özet otomatik düşer.',
1300
1315
  'Python işleri için python_run kullan: küçük betikler inline code ile; tekrarlayan işler %APPDATA%\\beast\\scripts klasöründeki dosyalarla (ör. news.py = RSS haber toplayıcı: args ["--limit","8","--json"]). Python kurulu olmasa bile ilk çağrıda taşınabilir gömülü runtime otomatik iner.',
1301
1316
  'PYTHON DURUMU: makinede sistem Python\'u görünmese bile ŞAŞIRMA ve "python yok" DEME — python_run aracı kendi taşınabilir runtime\'ını (%APPDATA%\\beast\\py\\python.exe) otomatik indirir/kullanır ve bu klasör run_command PATH\'inde önceliklidir; yani run_command içinde de `python` çalışır. Ham Google/Bing scrape yerine önce web_search aracını kullan (zaten Python çoklu-motor + Exa destekli), script gerektiğinde python_run yaz.',
1317
+ 'PDF ÇIKTI KURALI: PDF üretirken pip\u2019ten pdf paketi (fpdf, fpdf2, markdown-pdf, weasyprint, reportlab vb.) KURMA/KULLANMA — bunlar Türkçe karakterleri bozar. Doğru kit Node tarafında ZATEN kurulu: `pdf-lib` + `@pdf-lib/fontkit` (Türkçe font gömme) ve `pdfkit`. python_run ile DEĞİL; write_file ile .js script yazıp run_command ile `node script.js` çalıştır. md→pdf çevirici YOKTUR ve kurulmaz: kullanıcıya rapor/özet/belge çıktısı vereceksen .md dosyası gönderme — aynı içeriği DOĞRUDAN pdf-lib/pdfkit ile PDF olarak üret ve send_file ile o PDF\u2019i gönder. Ayrıntılı örnekler: pdf skill\u2019i (SKILL.md).',
1302
1318
  'Eski bir hafıza kaydına ihtiyacın olursa memory_search ile ara; kalıcı bilgi/birikim için kb_search kullan, yeni bilgi öğrenirsen kb_add ile kaynak belirt.',
1303
1319
  'Bir oturumda 3+ kez memory_write yaptıysan iş bitince memory_hygiene çağır (duplike/eskime temizliği).',
1304
1320
  'BEAST KAYNAK KORUMASI: Beast\u2019in kendi kurulum/kaynak kod klasörü KİLİTLİDİR — oraya dosya yazamaz, silamaz, komut/betikle değiştiremezsin; okumak serbest. Kullanıcı kodu ancak kendi eliyle dışarıdan değiştirir; böyle bir istek gelirse yapamayacağını söyle ve kullanıcının elle yapması için yol göster.',
@@ -1907,7 +1923,7 @@ class Engine {
1907
1923
  job.lastActivityAt = nowIso();
1908
1924
  }
1909
1925
 
1910
- /* Saf sınıflandırma (test edilebilir): null | 'stuck' | 'wrapup' | 'long' */
1926
+ /* Saf sınıflandırma (test edilebilir): null | 'stuck' | 'long' */
1911
1927
  static superviseReason(job, nowMs) {
1912
1928
  if (!job || job.status !== 'running') return null;
1913
1929
  const started = Date.parse(job.startedAt || '') || nowMs;
@@ -1919,7 +1935,6 @@ class Engine {
1919
1935
  : Infinity;
1920
1936
  if (nudgedMin < SUP_NUDGE_COOLDOWN_MIN) return null; // yeni uyardık — boğmayalım
1921
1937
  if (runMin >= SUP_STUCK_START_MIN && idleMin >= SUP_IDLE_MIN) return 'stuck';
1922
- if (!job.wrapAt && runMin >= BG_WRAP_MIN) return 'wrapup';
1923
1938
  if (runMin >= SUP_LONG_MIN) return 'long';
1924
1939
  return null;
1925
1940
  }
@@ -1936,11 +1951,6 @@ class Engine {
1936
1951
  this._bgSelfHeal(job, now);
1937
1952
  continue;
1938
1953
  }
1939
- /* zarif bitirme: süre dolunca yeni araştırma yasak → SON RAPORU yazsın */
1940
- if (reason === 'wrapup') {
1941
- this._bgWrapUp(job, now);
1942
- continue;
1943
- }
1944
1954
  job.lastNudgeAt = nowIso();
1945
1955
  job.checks = (job.checks || 0) + 1;
1946
1956
  const det = this.bgDetail(job.id);
@@ -1979,24 +1989,6 @@ class Engine {
1979
1989
  this._bgKick(job, text);
1980
1990
  }
1981
1991
 
1982
- /* Zarif bitirme: süre dolunca ajanı kesip SON RAPORU yazmasını iste.
1983
- Uzatma yok, öldürme yok — mevcut bulgularla düzgün kapanış. */
1984
- _bgWrapUp(job, now) {
1985
- job.wrapAt = nowIso();
1986
- job.lastNudgeAt = nowIso();
1987
- job.checks = (job.checks || 0) + 1;
1988
- const runMin = Math.max(1, Math.round((now - Date.parse(job.startedAt)) / 60000));
1989
- const det = this.bgDetail(job.id);
1990
- const tail = det.ok && det.messages.length ? det.messages.slice(-3).join('\n') : '';
1991
- const text =
1992
- `[SÜRE UYARISI] "${job.title}" görevi ${runMin} dk'dır sürüyor — süre doldu.\n` +
1993
- `YENİ arama/fetch/sayfa açma YAPMA. Şu ana kadar bulduklarınla SON RAPORU şimdi yaz:\n` +
1994
- `- 3-5 madde: net sonuç + yapılamayanlar açıkça "bulunamadı" diye.\n` +
1995
- `Raporu yazınca görev tamamlanır — fazladan tur harcama.` +
1996
- (tail ? `\n\nSon çıktın:\n${tail}` : '');
1997
- this._bgKick(job, text);
1998
- }
1999
-
2000
1992
  /* Arka plan oturumuna müdahale: asılı turu kes (ctrl.abort), sonra
2001
1993
  kurtarma mesajını bas. abort → eski _run sonlanır → send yeni tur açar.
2002
1994
  revive bayrağı: /stop veya silme sonrası bekleyen müdahaleler patlamasın. */
@@ -2113,8 +2105,9 @@ class Engine {
2113
2105
  `Ortam: Windows + PowerShell; çalışma klasörü: ${this.workspace}\n` +
2114
2106
  `HIZ KURALLARI:\n` +
2115
2107
  `- Döngülü işleri (çok URL/dosya/sayfa, tekrarlı parse-hesap) TEK python_run betiğinde topluca bitir.\n` +
2116
- `- Web için web_search kullan (zincir dahili tarayıcıyla başlar — gerçek Chromium ile Google); sayfa açma/göstermenin VARSAYILANI DAHİLİ tarayıcıdır: browser_open → browser_snapshot → browser_click/type/read. Kullanıcı açıkça dış tarayıcı (chrome/firefox/başka/normal/kendi tarayıcım) istediyse run_command ile \`start "" <url>\` çalıştır. Görseli göremiyorsan metni ocr_read ile oku (source:"browser").\n` +
2108
+ `- Web için web_search kullan (zincir dahili tarayıcıyla başlar — gerçek Chromium ile Google); tek aramada bulunamazsa veya çok kaynaklı derin araştırma gerekiyorsa deep_search kullan (çoklu sorgu paralel + gizli tarayıcıda tam sayfa okuma). Sayfa açma/göstermenin VARSAYILANI DAHİLİ tarayıcıdır: browser_open → browser_snapshot → browser_click/type/read. Kullanıcı açıkça dış tarayıcı (chrome/firefox/başka/normal/kendi tarayıcım) istediyse run_command ile \`start "" <url>\` çalıştır. Görseli göremiyorsan metni ocr_read ile oku (source:"browser").\n` +
2117
2109
  `- Bağımsız araç çağrılarını aynı turda PARALEL ver.\n` +
2110
+ `- PDF gerekirse pip\u2019ten paket kurma (Türkçe bozar); Node\u2019un kurulu \`pdf-lib\`+fontkit\u2019iyle .js script yazıp \`node\` ile çalıştır. md→pdf çevirme: belge çıktısını doğrudan PDF olarak üret.\n` +
2118
2111
  `- ARAŞTIRMA SINIRI: 3-5 kaynak yeter; süre hedefi ~3 dakika. 2-3 denemede bulunamayan bilgiyi BIRAK — bulabildiğin kısmi sonucu raporla ve neyi bulamadığını yaz. Kapalı/gizli içerik peşinde koşma.\n` +
2119
2112
  FORMAT_RULES + '\n' +
2120
2113
  `SON ÇIKTI: 3-5 satırlık net sonuç raporu, madde madde. Soru sorma, sohbet etme.`
@@ -2221,6 +2214,11 @@ class Engine {
2221
2214
  role = 'terminal';
2222
2215
  }
2223
2216
  let sel = this.modelFor(role);
2217
+ /* bot model override: rol modeli (vision/terminal) yoksa botun kendi seçimi kazanır */
2218
+ if (!role) {
2219
+ const ssel = this.sessionModel.get(String(session.id));
2220
+ if (ssel) sel = ssel;
2221
+ }
2224
2222
  if (role && this.roleModels[role]) {
2225
2223
  emitSafe(this, session.id, { type: 'status', status: `rol: ${role} → ${sel.providerName} · ${sel.model}` });
2226
2224
  }
@@ -2284,9 +2282,26 @@ class Engine {
2284
2282
  const sid = session.id;
2285
2283
  const emit = (ev) => this.emit({ ...ev, sessionId: sid });
2286
2284
  try {
2287
- if (!this.sel) throw new Error('Model yapılandırılmadı — %APPDATA%\\beast\\config.yaml ve .env kontrol et');
2285
+ 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');
2288
2286
 
2287
+ let nudged = false; // görev listesi disiplini: run başına en fazla 1 hatırlatma
2288
+ let wrapNudged = false; // tur limitine yaklaşınca zarif kapanış (bg ajanlar)
2289
2289
  for (let turn = 0; turn < MAX_TURNS; turn++) {
2290
+ /* SÜRE SINIRI YOK — ama sonsuz tur da yok: bg ajan son 3 tura gelirken
2291
+ "raporu yaz ve bitir" uyarısı alır; MAX_TURNS sert tavan olarak kalır. */
2292
+ if (session.bgJob && turn === MAX_TURNS - 3 && !wrapNudged) {
2293
+ wrapNudged = true;
2294
+ const wmsg = {
2295
+ role: 'user',
2296
+ content:
2297
+ '[TUR LİMİTİ YAKLAŞIYOR] Kalın son turlar: yeni araştırma/iş AÇMA — şu ana kadarki bulgularınla SON RAPORU yaz ve bitir. Yapılamayanları açıkça "bulunamadı" diye belirt.',
2298
+ };
2299
+ session.messages.push(wmsg);
2300
+ try {
2301
+ this._append(session, wmsg);
2302
+ } catch {}
2303
+ emit({ type: 'message', message: wmsg });
2304
+ }
2290
2305
  this._bgTrim(session); // #21 bg geçmişini olabildiğince ince tut
2291
2306
  emit({ type: 'status', status: 'thinking' });
2292
2307
  const res = await this._chatTurn(session, ctrl.signal, (delta) =>
@@ -2302,6 +2317,27 @@ class Engine {
2302
2317
  emit({ type: 'message', message: assistant });
2303
2318
 
2304
2319
  if (!res.toolCalls || !res.toolCalls.length) {
2320
+ /* GÖREV LİSTESİ DİSİPLİNİ: ajan işi bitti sanıyor ama listede hâlâ
2321
+ bekleyen/aktif madde varsa BİR KEZ hatırlat ve tura devam et —
2322
+ liste ya tamamlanmalı ya kalan maddeler listeden düşürülmeli. */
2323
+ const todos = session.todos || this.todos.get(sid) || [];
2324
+ const pending = todos.filter((t) => t && t.status !== 'done');
2325
+ if (pending.length && !nudged) {
2326
+ nudged = true;
2327
+ const nudge = {
2328
+ role: 'user',
2329
+ content:
2330
+ '[OTOMATİK HATIRLATMA] Görev listende hâlâ tamamlanmamış maddeler var: ' +
2331
+ pending.map((t) => '"' + t.title + '"').join(', ') +
2332
+ '. Görevi yarım bırakma: eksik adımları ŞİMDİ tamamla; gerçekten yapılmayacaksa listeden düş. todo_write ile listeyi güncelle (status:"done") ve işi kapat.',
2333
+ };
2334
+ session.messages.push(nudge);
2335
+ try {
2336
+ this._append(session, nudge);
2337
+ } catch {}
2338
+ emit({ type: 'message', message: nudge });
2339
+ continue;
2340
+ }
2305
2341
  /* cevap tamam → done HEMEN: kullanıcı beklemeden yeni iş yazabilsin.
2306
2342
  Not/memory/skill bakımı artık arka planda (_postRunHousekeeping). */
2307
2343
  this._clearCrash();
@@ -2694,7 +2730,7 @@ class Engine {
2694
2730
  ];
2695
2731
  try {
2696
2732
  for (let turn = 0; turn < SUB_MAX_TURNS; turn++) {
2697
- const res = await chatStream(
2733
+ const res = await chatStreamAuto(
2698
2734
  sel,
2699
2735
  { messages: [{ role: 'system', content: system }, ...msgs.slice(1)], tools: subTools, reasoningEffort: this._thinkEffort() },
2700
2736
  { signal: ctrl.signal }
@@ -2719,9 +2755,43 @@ class Engine {
2719
2755
  }
2720
2756
  }
2721
2757
 
2758
+ /* web_search zinciri — web_search VE deep_search aynı zinciri kullanır:
2759
+ 1) dahili tarayıcı (DİREK GOOGLE — ücretsiz + AI cevabı)
2760
+ 2) TinyFish (tarayıcı sorun çıkarsa HEMEN devreye girer — anahtar girildiyse)
2761
+ 3) python çoklu-motor (ddgs / DDG+Bing+Mojeek)
2762
+ 4) Exa (anahtar girildiyse, son çare)
2763
+ Tarayıcıda CAPTCHA/"olağandışı trafik"/boş sonuç algılanırsa 10 dk boyunca
2764
+ tarayıcı ATLANIR — her arama TinyFish'ten başlar, süre dolunca tarayıcı geri döner. */
2765
+ async _webSearchChain(q, n, sessionId, signal) {
2766
+ let r = null;
2767
+ let banned = false;
2768
+ const browserUsable = this.browser && typeof this.browser.search === 'function';
2769
+ if (Date.now() < (this._webSearchBrowserBanUntil || 0)) banned = true;
2770
+ if (browserUsable && !banned) {
2771
+ try { r = await this.browser.search(q, signal, { sessionId }); } catch {}
2772
+ if (!r || !r.ok || !(r.results || []).length) {
2773
+ /* tarayıcı sorunlu — 10 dk atla, alternatif zincir hemen devrede */
2774
+ this._webSearchBrowserBanUntil = Date.now() + 10 * 60 * 1000;
2775
+ r = null;
2776
+ banned = true;
2777
+ }
2778
+ }
2779
+ if (!r || !r.ok || !(r.results || []).length) {
2780
+ try { r = await tools.tinyfishSearch(q, n, signal); } catch {}
2781
+ }
2782
+ if (!r || !r.ok || !(r.results || []).length) {
2783
+ try {
2784
+ r = JSON.parse(await tools.exec('web_search', { query: q, max_results: n }, { cwd: this._sessionWorkspace(sessionId), signal }));
2785
+ } catch {}
2786
+ }
2787
+ const out = r || { ok: false, error: 'web arama başarısız — tüm motorlar boş döndü' };
2788
+ if (out && out.ok && banned) out.note = 'tarayıcı araması 10 dk askıda (CAPTCHA/trafik) — TinyFish/alternatif zincir kullanıldı';
2789
+ return out;
2790
+ }
2791
+
2722
2792
  /* Dahili OCR aracı: görüntüden metin çıkarır (görsel desteklemeyen modeller için).
2723
2793
  Kaynak: 'browser' (dahili tarayıcı görüntüsü, varsayılan) | 'screen' (masaüstü) | dosya yolu. */
2724
- async _ocrRead(args, signal) {
2794
+ async _ocrRead(args, signal, sessionId) {
2725
2795
  if (!this.ocr) return { ok: false, error: 'OCR kullanılamıyor' };
2726
2796
  const src = String((args && args.source) || 'browser');
2727
2797
  let image = null;
@@ -2732,7 +2802,7 @@ class Engine {
2732
2802
  if (!this.browser || typeof this.browser.screenshot !== 'function') {
2733
2803
  return { ok: false, error: 'dahili tarayıcı kullanılamıyor' };
2734
2804
  }
2735
- const shot = await this.browser.screenshot(signal);
2805
+ const shot = await this.browser.screenshot(signal, { sessionId });
2736
2806
  if (!shot || !shot.ok) return { ok: false, error: (shot && shot.error) || 'görüntü alınamadı' };
2737
2807
  image = shot.__injectImage || shot.image || null;
2738
2808
  } else if (src === 'screen') {
@@ -3067,76 +3137,64 @@ class Engine {
3067
3137
  const r = await this.computer.act(op, args || {});
3068
3138
  return JSON.stringify(r);
3069
3139
  }
3140
+ if (name === 'deep_search') {
3141
+ /* agentic derin araştırma: çoklu sorgu + gizli tarayıcıda sayfa okuma.
3142
+ ARAMA zinciri web_search ile BİREBİR AYNI — eski sürüm yalnız python
3143
+ motorlarını (ddgs/DDG/Bing) kullanıyordu; CAPTCHA/engel sonucu motorlar
3144
+ boş dönünce "tüm motorlar boş döndü" hatası veriyordu. Artık önce
3145
+ DAHİLİ TARAYICI (Google) denenir, sonra TinyFish → python → Exa. */
3146
+ try {
3147
+ emitSafe(this, sessionId, { type: 'status', status: 'derin araştırma: çoklu sorgu + gizli sayfa okuma' });
3148
+ } catch {}
3149
+ const deps = {
3150
+ search: (q) => this._webSearchChain(q, 10, sessionId, signal),
3151
+ };
3152
+ if (this.research && typeof this.research.readPage === 'function') {
3153
+ deps.readPage = (u) => this.research.readPage(u, signal);
3154
+ }
3155
+ return JSON.stringify(await research.deepSearch(args || {}, deps, signal));
3156
+ }
3070
3157
  if (name === 'web_search') {
3071
- /* SIRA: 1) dahili tarayıcı (DİREK GOOGLE — ücretsiz + AI cevabı)
3072
- 2) TinyFish (tarayıcı sorun çıkarsa HEMEN devreye girer — anahtar girildiyse)
3073
- 3) python çoklu-motor (ddgs / DDG+Bing+Mojeek)
3074
- 4) Exa (anahtar girildiyse, son çare)
3075
- Tarayıcıda CAPTCHA/"olağandışı trafik"/boş sonuç algılanırsa 10 dk boyunca
3076
- tarayıcı ATLANIR — her arama TinyFish'ten başlar, süre dolunca tarayıcı geri döner. */
3077
3158
  const q = String((args && args.query) || '');
3078
3159
  const n = Number((args && args.max_results) || 8);
3079
- let r = null;
3080
- let banned = false;
3081
- const browserUsable = this.browser && typeof this.browser.search === 'function';
3082
- if (Date.now() < (this._webSearchBrowserBanUntil || 0)) banned = true;
3083
- if (browserUsable && !banned) {
3084
- try { r = await this.browser.search(q, signal); } catch {}
3085
- if (!r || !r.ok || !(r.results || []).length) {
3086
- /* tarayıcı sorunlu — 10 dk atla, alternatif zincir hemen devrede */
3087
- this._webSearchBrowserBanUntil = Date.now() + 10 * 60 * 1000;
3088
- r = null;
3089
- banned = true;
3090
- }
3091
- }
3092
- if (!r || !r.ok || !(r.results || []).length) {
3093
- try { r = await tools.tinyfishSearch(q, n, signal); } catch {}
3094
- }
3095
- if (!r || !r.ok || !(r.results || []).length) {
3096
- try {
3097
- r = JSON.parse(await tools.exec('web_search', args, { cwd: this._sessionWorkspace(sessionId), signal }));
3098
- } catch {}
3099
- }
3100
- const out = r || { ok: false, error: 'web arama başarısız — tüm motorlar boş döndü' };
3101
- if (out && out.ok && banned) out.note = 'tarayıcı araması 10 dk askıda (CAPTCHA/trafik) — TinyFish/alternatif zincir kullanıldı';
3102
- return JSON.stringify(out);
3103
- }
3160
+ return JSON.stringify(await this._webSearchChain(q, n, sessionId, signal));
3161
+ }
3104
3162
  if (name === 'ocr_read') {
3105
- return JSON.stringify(await this._ocrRead(args || {}, signal));
3163
+ return JSON.stringify(await this._ocrRead(args || {}, signal, sessionId));
3106
3164
  }
3107
3165
  if (name === 'browser_open') {
3108
3166
  if (!this.browser || typeof this.browser.openUrl !== 'function') {
3109
3167
  return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
3110
3168
  }
3111
- const r = await this.browser.openUrl(String(args.url || ''), signal);
3169
+ const r = await this.browser.openUrl(String(args.url || ''), signal, { sessionId });
3112
3170
  return JSON.stringify(r);
3113
3171
  }
3114
3172
  if (name === 'browser_read') {
3115
3173
  if (!this.browser || typeof this.browser.readText !== 'function') {
3116
3174
  return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
3117
3175
  }
3118
- const r = await this.browser.readText(signal);
3176
+ const r = await this.browser.readText(signal, { sessionId });
3119
3177
  return JSON.stringify(r);
3120
3178
  }
3121
3179
  if (name === 'browser_screenshot') {
3122
3180
  if (!this.browser || typeof this.browser.screenshot !== 'function') {
3123
3181
  return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
3124
3182
  }
3125
- const r = await this.browser.screenshot(signal);
3183
+ const r = await this.browser.screenshot(signal, { sessionId });
3126
3184
  return JSON.stringify(r);
3127
3185
  }
3128
3186
  if (name === 'browser_snapshot') {
3129
3187
  if (!this.browser || typeof this.browser.snapshot !== 'function') {
3130
3188
  return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
3131
3189
  }
3132
- const r = await this.browser.snapshot(signal);
3190
+ const r = await this.browser.snapshot(signal, { sessionId });
3133
3191
  return JSON.stringify(r);
3134
3192
  }
3135
3193
  if (name === 'browser_click' || name === 'browser_type' || name === 'browser_press' || name === 'browser_scroll' || name === 'browser_select') {
3136
3194
  if (!this.browser || typeof this.browser.act !== 'function') {
3137
3195
  return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
3138
3196
  }
3139
- const r = await this.browser.act(name.slice(8), args, signal);
3197
+ const r = await this.browser.act(name.slice(8), args, signal, { sessionId });
3140
3198
  return JSON.stringify(r);
3141
3199
  }
3142
3200
  if (!(name === 'run_command' || name === 'read_file' || name === 'write_file' || name === 'list_dir' ||
@@ -3256,7 +3314,7 @@ const TOOLS = [
3256
3314
  function: {
3257
3315
  name: 'todo_write',
3258
3316
  description:
3259
- 'Replace the visible task checklist for this chat. Use ONLY for multi-step work (3+ steps); do not use for simple questions. Keep titles short; update statuses as you progress; clear the list when done.',
3317
+ 'Replace the visible task checklist for this chat. Use ONLY for multi-step work (3+ steps); do not use for simple questions. Keep titles short; update statuses as you progress; clear the list when done. DISCIPLINE: mark each step done THE MOMENT it is completed; NEVER end your reply while items are still pending/active — the system bounces unfinished lists back.',
3260
3318
  parameters: {
3261
3319
  type: 'object',
3262
3320
  properties: {
package/src/agent/llm.js CHANGED
@@ -135,7 +135,6 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReas
135
135
  const toolCalls = [];
136
136
  let usage = null;
137
137
  let finishReason = null;
138
- let sawDone = false;
139
138
 
140
139
  const reader = res.body.getReader();
141
140
  const decoder = new TextDecoder();
@@ -152,8 +151,7 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReas
152
151
  buf = buf.slice(idx + 1);
153
152
  if (!line.startsWith('data:')) continue;
154
153
  const data = line.slice(5).trim();
155
- if (!data) continue;
156
- if (data === '[DONE]') { sawDone = true; continue; }
154
+ if (!data || data === '[DONE]') continue;
157
155
 
158
156
  let json;
159
157
  try {
@@ -187,21 +185,52 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReas
187
185
  }
188
186
  }
189
187
 
190
- /* YARIDA KESİLEN AKIŞ: stream erişilmeyen bir yerde koptuysa reader sessizce
191
- done=true döner — yarım metni "tamamlanmış" sanmak yerine işaretle.
192
- ([DONE] geldi ama finish_reason yok = sağlayıcı tuhaflığı → kabul) */
193
- if (!finishReason && !sawDone) {
194
- if (toolCalls.length) {
195
- const e = new Error('cevap akışı yarıda kesildi (bağlantı koptu) araç çağrısı tamamlanamadı, tekrar deneyin');
196
- e.partialStream = true;
197
- throw e;
198
- }
199
- if (content) {
200
- content += '\n\n⚠ _akış yarıda kesildi (bağlantı koptu) devam etmesini istersen tekrar yaz._';
201
- }
188
+ return { content, reasoning, toolCalls, usage, finishReason };
189
+ }
190
+
191
+ /* usage toplama: devam turları gerçek faturalamayı yansıtsın (prompt yeniden sayılır) */
192
+ function sumUsage(a, b) {
193
+ if (!a) return b || null;
194
+ if (!b) return a;
195
+ const out = {};
196
+ for (const k of new Set([...Object.keys(a), ...Object.keys(b)])) {
197
+ if (typeof a[k] === 'number' && typeof b[k] === 'number') out[k] = a[k] + b[k];
198
+ else out[k] = b[k] !== undefined ? b[k] : a[k];
202
199
  }
200
+ return out;
201
+ }
203
202
 
204
- return { content, reasoning, toolCalls, usage, finishReason };
203
+ /* OTOMATİK DEVAM: model cevabı çıkış limiti (finish_reason='length') yüzünden
204
+ kelime ortasında kesilirse — özellikle free modellerin düşük varsayılan
205
+ limitlerinde olur — yarım cümle kullanıcıya gitmesin. Kaldığı yerden devam
206
+ istenip metin birleştirilir (en fazla 2 devam turu). Araç çağrılı yanıtlarda
207
+ ve içerik hiç gelmediyse devreye girmez; orijinal mesaj dizisi DEĞİŞTİRİLMEZ. */
208
+ const CONTINUE_PROMPT =
209
+ 'Yanıtın çıkış limiti nedeniyle ortadan kesildi. Kaldığın yerden DEVAM ET — baştan başlama, özetleme, açıklama yapma; yalnızca kesilen kısmın DEVAMINI yaz.';
210
+
211
+ async function chatStreamAuto(sel, body, opts = {}) {
212
+ const first = await chatStream(sel, body, opts);
213
+ if (first.finishReason !== 'length' || !first.content || (first.toolCalls && first.toolCalls.length)) {
214
+ return first;
215
+ }
216
+ let full = first.content;
217
+ let last = first;
218
+ for (let i = 0; i < 2; i++) {
219
+ const msgs2 = body.messages.slice();
220
+ msgs2.push({ role: 'assistant', content: full });
221
+ msgs2.push({ role: 'user', content: CONTINUE_PROMPT });
222
+ let cont;
223
+ try {
224
+ cont = await chatStream(sel, { ...body, messages: msgs2 }, opts);
225
+ } catch {
226
+ break; // devam turu patlarsa elimizdeki kısmi yanıtla döneriz
227
+ }
228
+ if (!cont.content || (cont.toolCalls && cont.toolCalls.length)) break;
229
+ full += cont.content;
230
+ last = { ...cont, content: full, usage: sumUsage(first.usage, cont.usage) };
231
+ if (cont.finishReason !== 'length') break;
232
+ }
233
+ return last;
205
234
  }
206
235
 
207
236
  async function chatOnce(sel, body, { signal, onRetry } = {}) {
@@ -232,4 +261,4 @@ async function chatOnce(sel, body, { signal, onRetry } = {}) {
232
261
  };
233
262
  }
234
263
 
235
- module.exports = { chatStream, chatOnce, withRetries, friendlyError };
264
+ module.exports = { chatStream, chatStreamAuto, chatOnce, withRetries, friendlyError, sumUsage };
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ /* BEAST — deep_search: agentic araştırma katmanı.
4
+ web_search snippet'te kalır; bu katman derine iner:
5
+ 1) 1-4 sorgu varyantını PARALEL aratır (toplam süre ≈ tek arama)
6
+ 2) Sonuçları URL bazlı tekilleştirip round-robin harmanlar (kaynak çeşitliliği)
7
+ 3) İlk N sonucu GİZLİ gerçek tarayıcıda açıp tam metin okur
8
+ (deps.readPage verilmişse; yoksa yalnız sonuç listesi döner)
9
+ Saf modüldür: search/readPage enjekte edilir — test edilebilir. */
10
+
11
+ /* İçerik okumada anlamsız/ikili dosyalar — tarayıcıda açmanın anlamı yok */
12
+ const SKIP_FILE_RE = /\.(pdf|docx?|xlsx?|pptx?|zip|rar|7z|exe|dmg|apk|mp[34]|m4a|avi|mkv|webm|jpe?g|png|gif|webp|svg|ico|csv)([?#]|$)/i;
13
+
14
+ /* URL anahtarı: scheme/www/slash/hash farklarını yok sayar — tekilleştirme için */
15
+ function normKey(u) {
16
+ try {
17
+ const x = new URL(u);
18
+ x.hash = '';
19
+ let s = x.toString();
20
+ s = s.replace(/^http:\/\//i, 'https://');
21
+ s = s.replace(/^https:\/\/www\./i, 'https://');
22
+ return s.replace(/\/+$/, '').toLowerCase();
23
+ } catch {
24
+ return String(u || '').toLowerCase();
25
+ }
26
+ }
27
+
28
+ /* search() geri dönüşünü normalize eder: dizi YA DA {results:[...]} kabul;
29
+ satır anahtarları title/url/snippet VEYA name/href/link/body/description olabilir */
30
+ function normalizeRows(out) {
31
+ if (!out) return [];
32
+ const rows = Array.isArray(out) ? out : Array.isArray(out.results) ? out.results : [];
33
+ return rows
34
+ .map((r) => ({
35
+ title: String((r && (r.title || r.name)) || '').replace(/\s+/g, ' ').trim(),
36
+ url: String((r && (r.url || r.href || r.link)) || '').trim(),
37
+ snippet: String((r && (r.snippet || r.body || r.description)) || '').replace(/\s+/g, ' ').trim(),
38
+ }))
39
+ .filter((r) => r.url && /^https?:\/\//i.test(r.url) && r.title);
40
+ }
41
+
42
+ /* Liste harmanı: her listenin i. elemanı sırayla alınır (round-robin) —
43
+ tek motorun sonuçları öne çökmez, kaynak çeşitliliği korunur */
44
+ function mergeResults(lists, limit) {
45
+ const seen = new Set();
46
+ const out = [];
47
+ const max = lists.reduce((m, l) => Math.max(m, l.length), 0);
48
+ for (let i = 0; i < max && out.length < limit; i++) {
49
+ for (const list of lists) {
50
+ if (out.length >= limit) break;
51
+ const r = list[i];
52
+ if (!r) continue;
53
+ const key = normKey(r.url);
54
+ if (seen.has(key)) continue;
55
+ seen.add(key);
56
+ out.push(r);
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+
62
+ /* Eşzamanlılığı sınırlı paralel map — hata tek elemanı bozar, diziyi bozmaz */
63
+ function mapLimit(items, n, fn) {
64
+ const out = new Array(items.length);
65
+ let i = 0;
66
+ const worker = async () => {
67
+ while (i < items.length) {
68
+ const idx = i++;
69
+ try {
70
+ out[idx] = await fn(items[idx], idx);
71
+ } catch (e) {
72
+ out[idx] = { ok: false, url: items[idx] && items[idx].url, error: String((e && e.message) || e) };
73
+ }
74
+ }
75
+ };
76
+ const workers = Array.from({ length: Math.max(1, Math.min(n, items.length)) }, worker);
77
+ return Promise.all(workers).then(() => out);
78
+ }
79
+
80
+ async function deepSearch(args, deps, signal) {
81
+ const a = args || {};
82
+ let queries = a.queries;
83
+ if (typeof queries === 'string') queries = [queries];
84
+ if (!Array.isArray(queries) || !queries.length) {
85
+ const q = String(a.query || '').trim();
86
+ if (q) queries = [q];
87
+ }
88
+ queries = [...new Set((queries || []).map((s) => String(s || '').trim()).filter(Boolean))].slice(0, 4);
89
+ if (!queries.length) return { ok: false, error: 'sorgu yok — queries: ["...", "..."] ver' };
90
+ if (signal && signal.aborted) return { ok: false, error: 'iptal edildi', queries };
91
+
92
+ const maxResults = Math.max(5, Math.min(25, Number(a.max_results) || 16));
93
+ const readTop = Math.max(0, Math.min(6, Number(a.read_top != null ? a.read_top : (a.readTop != null ? a.readTop : 3))));
94
+ const search = deps && typeof deps.search === 'function' ? deps.search : null;
95
+ if (!search) return { ok: false, error: 'arama altyapısı yok', queries };
96
+
97
+ /* 1) paralel arama — bir sorgu çökse diğerleri devam eder */
98
+ const lists = await Promise.all(
99
+ queries.map((q) =>
100
+ Promise.resolve()
101
+ .then(() => search(q))
102
+ .then(normalizeRows)
103
+ .catch(() => [])
104
+ )
105
+ );
106
+ const results = mergeResults(lists, maxResults);
107
+
108
+ /* 2) ilk N sonucu gizli tarayıcıda açıp oku (ikili dosyaları atla) */
109
+ let pages = [];
110
+ if (readTop && results.length && deps && typeof deps.readPage === 'function') {
111
+ const targets = results.filter((r) => !SKIP_FILE_RE.test(r.url)).slice(0, readTop);
112
+ if (targets.length) {
113
+ pages = (await mapLimit(targets, 2, (r) => deps.readPage(r.url))).filter(Boolean);
114
+ }
115
+ }
116
+ const anyPage = pages.some((p) => p && p.ok);
117
+
118
+ return {
119
+ ok: !!(results.length || anyPage),
120
+ queries,
121
+ results,
122
+ ...(pages.length ? { pages } : {}),
123
+ ...(results.length || anyPage
124
+ ? {
125
+ note: anyPage
126
+ ? 'sayfalar gizli tarayıcıda açılıp okundu — excerpt\u2019ler pages içinde; bir sayfayı kullanıcıya da göstermek istersen browser_open ile panelde aç'
127
+ : 'sonuç listesi hazır — tam metin okuma kapalı/başarısız; URL\u2019leri http_fetch ile okuyabilirsin',
128
+ }
129
+ : { error: 'tüm motorlar boş döndü — sorguları farklı ifade etmeyi dene' }),
130
+ };
131
+ }
132
+
133
+ module.exports = { deepSearch, mergeResults, normalizeRows, normKey, mapLimit, SKIP_FILE_RE };