beast-agent 0.25.5 → 0.25.7

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "0.25.5",
4
+ "version": "0.25.7",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
package/src/agent/bots.js CHANGED
@@ -327,6 +327,14 @@ function update(id, patch) {
327
327
  changes.push('dış tarayıcı komutu güncellendi');
328
328
  b.extCommand = String(patch.extCommand).slice(0, 200);
329
329
  }
330
+ if (typeof patch.model === 'string') {
331
+ /* bot bazlı model override — boş string = global seçim */
332
+ const next = patch.model.trim().slice(0, 160);
333
+ if (next !== (b.model || '')) {
334
+ changes.push('model güncellendi');
335
+ b.model = next;
336
+ }
337
+ }
330
338
  if (Array.isArray(patch.plugins)) {
331
339
  const next = [...new Set(patch.plugins.map((p) => String(p).slice(0, 40)).filter((p) => PLUGIN_LIST.includes(p)))];
332
340
  if (JSON.stringify(next) !== JSON.stringify(b.plugins || [])) {
@@ -0,0 +1,234 @@
1
+ 'use strict';
2
+
3
+ /* DISCORD KÖPRÜSÜ — Telegram köprüsünün Discord hali.
4
+ Bağımlılık: yalnızca `ws` (paket zaten kurulu — baileys ile geliyor).
5
+ - Gateway WebSocket: Identify → READY → MESSAGE_CREATE dispatch
6
+ - REST (api/v10): mesaj gönderme (2000 karakter sınırında bölerek)
7
+ - Sunucu (guild) mesajlarında yalnız @mention'a cevap verilir (spam koruması);
8
+ DM'de her izinli kullanıcıya cevap verilir.
9
+ - Bağlantı koparsa backoff ile yeniden bağlanır (yeniden Identify;
10
+ çevrimdışıyken gelen mesajlar backfill edilmez — Telegram long-poll'un aksine).
11
+ ÖNEMLİ: Discord Developer Portal'da bot için "MESSAGE CONTENT INTENT"
12
+ açılmalıdır — yoksa mesaj içerikleri boş gelir. */
13
+
14
+ const WebSocket = require('ws');
15
+ const https = require('https');
16
+
17
+ const API_BASE = 'https://discord.com/api/v10';
18
+ const GATEWAY_URL = 'wss://gateway.discord.gg/?v=10&encoding=json';
19
+ const SEND_CHUNK = 1900; // Discord mesaj sınırı 2000 — güvenli pay
20
+ /* GUILDS(1) | GUILD_MESSAGES(512) | DIRECT_MESSAGES(4096) | MESSAGE_CONTENT(32768) */
21
+ const INTENTS = 1 | 512 | 4096 | 32768;
22
+
23
+ class DiscordBridge {
24
+ constructor({ token, emit, onIncoming }) {
25
+ this.token = String(token || '').trim();
26
+ this.emit = emit || (() => {});
27
+ this.onIncoming = onIncoming || null;
28
+ this.connected = false;
29
+ this.stopping = false;
30
+ this.status = 'disconnected';
31
+ this.user = null; // { id, username, ... }
32
+ this._ws = null;
33
+ this._hbTimer = null;
34
+ this._seq = null;
35
+ this._backoff = 2000;
36
+ }
37
+
38
+ _setStatus(status, user) {
39
+ this.status = status;
40
+ this.connected = status === 'connected';
41
+ this.emit({ type: 'status', status, user: user || null });
42
+ }
43
+
44
+ /* REST çağrısı — JSON (token: "Bot <token>") */
45
+ api(method, path, body) {
46
+ return new Promise((resolve, reject) => {
47
+ const payload = body ? JSON.stringify(body) : null;
48
+ const req = https.request(
49
+ `${API_BASE}${path}`,
50
+ {
51
+ method,
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ Authorization: 'Bot ' + this.token,
55
+ ...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
56
+ },
57
+ timeout: 15000,
58
+ },
59
+ (res) => {
60
+ let data = '';
61
+ res.setEncoding('utf8');
62
+ res.on('data', (c) => (data += c));
63
+ res.on('end', () => {
64
+ try {
65
+ const j = data ? JSON.parse(data) : null;
66
+ if (res.statusCode >= 200 && res.statusCode < 300) resolve(j);
67
+ else reject(new Error(`discord ${path}: HTTP ${res.statusCode} ${(j && j.message) || ''}`.trim()));
68
+ } catch {
69
+ reject(new Error(`discord ${path}: bozuk yanıt`));
70
+ }
71
+ });
72
+ }
73
+ );
74
+ req.on('timeout', () => req.destroy(new Error('zaman aşımı')));
75
+ req.on('error', reject);
76
+ if (payload) req.write(payload);
77
+ req.end();
78
+ });
79
+ }
80
+
81
+ async start() {
82
+ if (!this.token) {
83
+ this._setStatus('error');
84
+ return false;
85
+ }
86
+ this.stopping = false;
87
+ this._setStatus('connecting');
88
+ try {
89
+ const me = await this.api('GET', '/users/@me');
90
+ this.user = me; // { id, username, ... }
91
+ this._setStatus('connected', '@' + (me.username || 'bot'));
92
+ } catch (e) {
93
+ this._setStatus('error');
94
+ throw e;
95
+ }
96
+ this._connect();
97
+ return true;
98
+ }
99
+
100
+ _connect() {
101
+ if (this.stopping) return;
102
+ clearInterval(this._hbTimer);
103
+ this._hbTimer = null;
104
+ let identified = false;
105
+ const ws = new WebSocket(GATEWAY_URL);
106
+ this._ws = ws;
107
+
108
+ ws.on('message', (raw) => {
109
+ let p = null;
110
+ try { p = JSON.parse(String(raw)); } catch { return; }
111
+ const op = p.op;
112
+ const d = p.d;
113
+ const t = p.t;
114
+ if (typeof p.s === 'number') this._seq = p.s;
115
+
116
+ if (op === 10) {
117
+ /* Hello: heartbeat aralığı gelıyor → hafif erken at (güvenli pay) */
118
+ const iv = (d && d.heartbeat_interval) || 41250;
119
+ clearInterval(this._hbTimer);
120
+ this._hbTimer = setInterval(() => {
121
+ try {
122
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ op: 1, d: this._seq }));
123
+ } catch {}
124
+ }, Math.max(5000, Math.floor(iv * 0.8)));
125
+ if (!identified) {
126
+ identified = true;
127
+ try {
128
+ ws.send(
129
+ JSON.stringify({
130
+ op: 2,
131
+ d: {
132
+ token: this.token,
133
+ intents: INTENTS,
134
+ properties: { os: 'windows', browser: 'beast-agent', device: 'beast-agent' },
135
+ },
136
+ })
137
+ );
138
+ } catch {}
139
+ }
140
+ return;
141
+ }
142
+ if (op === 1) {
143
+ /* sunucu heartbeat istedi — hemen at */
144
+ try {
145
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ op: 1, d: this._seq }));
146
+ } catch {}
147
+ return;
148
+ }
149
+ if (op === 9 || op === 7) {
150
+ /* invalid session / reconnect isteği — kapat, close handler yeniden bağlanır */
151
+ try { ws.close(); } catch {}
152
+ return;
153
+ }
154
+ if (op === 0 && t === 'READY') {
155
+ this._backoff = 2000;
156
+ this._setStatus('connected', '@' + ((d && d.user && d.user.username) || 'bot'));
157
+ return;
158
+ }
159
+ if (op === 0 && t === 'MESSAGE_CREATE' && this.onIncoming) {
160
+ try {
161
+ const m = d || {};
162
+ if (!m.author || m.author.bot || m.webhook_id) return;
163
+ let text = String(m.content || '').trim();
164
+ if (!text) return;
165
+ const botId = this.user && this.user.id;
166
+ const mentioned = !!(botId && text.includes('<@' + botId + '>'));
167
+ if (botId) text = text.split('<@' + botId + '>').join(' ').replace(/\s+/g, ' ').trim();
168
+ const payload = {
169
+ text: text.slice(0, 6000),
170
+ senderId: String((m.author && m.author.id) || ''),
171
+ username: String((m.author && m.author.username) || ''),
172
+ senderName: String((m.author && (m.author.global_name || m.author.username)) || ''),
173
+ isGroup: !!m.guild_id,
174
+ mentioned,
175
+ channelId: String(m.channel_id || ''),
176
+ };
177
+ if (!payload.channelId) return;
178
+ /* Sunucu mesajlarında yalnız @mention (spam koruması); DM'de hepsi */
179
+ if (payload.isGroup && !mentioned) return;
180
+ this.onIncoming(payload.channelId, payload);
181
+ } catch {}
182
+ return;
183
+ }
184
+ });
185
+
186
+ ws.on('close', () => {
187
+ clearInterval(this._hbTimer);
188
+ this._hbTimer = null;
189
+ if (this.stopping) return;
190
+ this._setStatus('connecting');
191
+ const wait = this._backoff;
192
+ this._backoff = Math.min(30000, this._backoff * 2);
193
+ setTimeout(() => this._connect(), wait);
194
+ });
195
+
196
+ ws.on('error', () => {
197
+ /* close tetiklenir — yeniden bağlanma orada */
198
+ });
199
+ }
200
+
201
+ async stop() {
202
+ this.stopping = true;
203
+ clearInterval(this._hbTimer);
204
+ this._hbTimer = null;
205
+ try {
206
+ if (this._ws) this._ws.close();
207
+ } catch {}
208
+ this._ws = null;
209
+ this.connected = false;
210
+ this.status = 'disconnected';
211
+ }
212
+
213
+ snapshot() {
214
+ return {
215
+ status: this.status,
216
+ user: this.user ? '@' + (this.user.username || 'bot') : null,
217
+ connected: this.connected,
218
+ };
219
+ }
220
+
221
+ /* Metin gönder — 2000 karakter sınırı için parçalara böl */
222
+ async send(channelId, text) {
223
+ const t = String(text || '');
224
+ if (!t.trim() || !channelId) return false;
225
+ const chunks = [];
226
+ for (let i = 0; i < t.length; i += SEND_CHUNK) chunks.push(t.slice(i, i + SEND_CHUNK));
227
+ for (const part of chunks) {
228
+ await this.api('POST', `/channels/${channelId}/messages`, { content: part });
229
+ }
230
+ return true;
231
+ }
232
+ }
233
+
234
+ module.exports = { DiscordBridge };
@@ -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ı */
@@ -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,7 +1298,8 @@ 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.',
@@ -1300,6 +1314,7 @@ class Engine {
1300
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.',
1301
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.',
1302
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).',
1303
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.',
1304
1319
  'Bir oturumda 3+ kez memory_write yaptıysan iş bitince memory_hygiene çağır (duplike/eskime temizliği).',
1305
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.',
@@ -1908,7 +1923,7 @@ class Engine {
1908
1923
  job.lastActivityAt = nowIso();
1909
1924
  }
1910
1925
 
1911
- /* Saf sınıflandırma (test edilebilir): null | 'stuck' | 'wrapup' | 'long' */
1926
+ /* Saf sınıflandırma (test edilebilir): null | 'stuck' | 'long' */
1912
1927
  static superviseReason(job, nowMs) {
1913
1928
  if (!job || job.status !== 'running') return null;
1914
1929
  const started = Date.parse(job.startedAt || '') || nowMs;
@@ -1920,7 +1935,6 @@ class Engine {
1920
1935
  : Infinity;
1921
1936
  if (nudgedMin < SUP_NUDGE_COOLDOWN_MIN) return null; // yeni uyardık — boğmayalım
1922
1937
  if (runMin >= SUP_STUCK_START_MIN && idleMin >= SUP_IDLE_MIN) return 'stuck';
1923
- if (!job.wrapAt && runMin >= BG_WRAP_MIN) return 'wrapup';
1924
1938
  if (runMin >= SUP_LONG_MIN) return 'long';
1925
1939
  return null;
1926
1940
  }
@@ -1937,11 +1951,6 @@ class Engine {
1937
1951
  this._bgSelfHeal(job, now);
1938
1952
  continue;
1939
1953
  }
1940
- /* zarif bitirme: süre dolunca yeni araştırma yasak → SON RAPORU yazsın */
1941
- if (reason === 'wrapup') {
1942
- this._bgWrapUp(job, now);
1943
- continue;
1944
- }
1945
1954
  job.lastNudgeAt = nowIso();
1946
1955
  job.checks = (job.checks || 0) + 1;
1947
1956
  const det = this.bgDetail(job.id);
@@ -1980,24 +1989,6 @@ class Engine {
1980
1989
  this._bgKick(job, text);
1981
1990
  }
1982
1991
 
1983
- /* Zarif bitirme: süre dolunca ajanı kesip SON RAPORU yazmasını iste.
1984
- Uzatma yok, öldürme yok — mevcut bulgularla düzgün kapanış. */
1985
- _bgWrapUp(job, now) {
1986
- job.wrapAt = nowIso();
1987
- job.lastNudgeAt = nowIso();
1988
- job.checks = (job.checks || 0) + 1;
1989
- const runMin = Math.max(1, Math.round((now - Date.parse(job.startedAt)) / 60000));
1990
- const det = this.bgDetail(job.id);
1991
- const tail = det.ok && det.messages.length ? det.messages.slice(-3).join('\n') : '';
1992
- const text =
1993
- `[SÜRE UYARISI] "${job.title}" görevi ${runMin} dk'dır sürüyor — süre doldu.\n` +
1994
- `YENİ arama/fetch/sayfa açma YAPMA. Şu ana kadar bulduklarınla SON RAPORU şimdi yaz:\n` +
1995
- `- 3-5 madde: net sonuç + yapılamayanlar açıkça "bulunamadı" diye.\n` +
1996
- `Raporu yazınca görev tamamlanır — fazladan tur harcama.` +
1997
- (tail ? `\n\nSon çıktın:\n${tail}` : '');
1998
- this._bgKick(job, text);
1999
- }
2000
-
2001
1992
  /* Arka plan oturumuna müdahale: asılı turu kes (ctrl.abort), sonra
2002
1993
  kurtarma mesajını bas. abort → eski _run sonlanır → send yeni tur açar.
2003
1994
  revive bayrağı: /stop veya silme sonrası bekleyen müdahaleler patlamasın. */
@@ -2116,6 +2107,7 @@ class Engine {
2116
2107
  `- Döngülü işleri (çok URL/dosya/sayfa, tekrarlı parse-hesap) TEK python_run betiğinde topluca bitir.\n` +
2117
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` +
2118
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` +
2119
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` +
2120
2112
  FORMAT_RULES + '\n' +
2121
2113
  `SON ÇIKTI: 3-5 satırlık net sonuç raporu, madde madde. Soru sorma, sohbet etme.`
@@ -2222,6 +2214,11 @@ class Engine {
2222
2214
  role = 'terminal';
2223
2215
  }
2224
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
+ }
2225
2222
  if (role && this.roleModels[role]) {
2226
2223
  emitSafe(this, session.id, { type: 'status', status: `rol: ${role} → ${sel.providerName} · ${sel.model}` });
2227
2224
  }
@@ -2285,9 +2282,26 @@ class Engine {
2285
2282
  const sid = session.id;
2286
2283
  const emit = (ev) => this.emit({ ...ev, sessionId: sid });
2287
2284
  try {
2288
- 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');
2289
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)
2290
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
+ }
2291
2305
  this._bgTrim(session); // #21 bg geçmişini olabildiğince ince tut
2292
2306
  emit({ type: 'status', status: 'thinking' });
2293
2307
  const res = await this._chatTurn(session, ctrl.signal, (delta) =>
@@ -2303,6 +2317,27 @@ class Engine {
2303
2317
  emit({ type: 'message', message: assistant });
2304
2318
 
2305
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
+ }
2306
2341
  /* cevap tamam → done HEMEN: kullanıcı beklemeden yeni iş yazabilsin.
2307
2342
  Not/memory/skill bakımı artık arka planda (_postRunHousekeeping). */
2308
2343
  this._clearCrash();
@@ -2695,7 +2730,7 @@ class Engine {
2695
2730
  ];
2696
2731
  try {
2697
2732
  for (let turn = 0; turn < SUB_MAX_TURNS; turn++) {
2698
- const res = await chatStream(
2733
+ const res = await chatStreamAuto(
2699
2734
  sel,
2700
2735
  { messages: [{ role: 'system', content: system }, ...msgs.slice(1)], tools: subTools, reasoningEffort: this._thinkEffort() },
2701
2736
  { signal: ctrl.signal }
@@ -2720,6 +2755,40 @@ class Engine {
2720
2755
  }
2721
2756
  }
2722
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
+
2723
2792
  /* Dahili OCR aracı: görüntüden metin çıkarır (görsel desteklemeyen modeller için).
2724
2793
  Kaynak: 'browser' (dahili tarayıcı görüntüsü, varsayılan) | 'screen' (masaüstü) | dosya yolu. */
2725
2794
  async _ocrRead(args, signal, sessionId) {
@@ -3070,49 +3139,26 @@ class Engine {
3070
3139
  }
3071
3140
  if (name === 'deep_search') {
3072
3141
  /* agentic derin araştırma: çoklu sorgu + gizli tarayıcıda sayfa okuma.
3073
- Hook (main process) yoksa tools.exec'teki arama-tek yol devreye girer. */
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. */
3074
3146
  try {
3075
3147
  emitSafe(this, sessionId, { type: 'status', status: 'derin araştırma: çoklu sorgu + gizli sayfa okuma' });
3076
3148
  } catch {}
3077
- const run = this.research && typeof this.research.deepSearch === 'function'
3078
- ? this.research.deepSearch(args || {}, signal)
3079
- : tools.exec('deep_search', args || {}, { cwd: this._sessionWorkspace(sessionId), signal });
3080
- return JSON.stringify(await run);
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));
3081
3156
  }
3082
3157
  if (name === 'web_search') {
3083
- /* SIRA: 1) dahili tarayıcı (DİREK GOOGLE — ücretsiz + AI cevabı)
3084
- 2) TinyFish (tarayıcı sorun çıkarsa HEMEN devreye girer — anahtar girildiyse)
3085
- 3) python çoklu-motor (ddgs / DDG+Bing+Mojeek)
3086
- 4) Exa (anahtar girildiyse, son çare)
3087
- Tarayıcıda CAPTCHA/"olağandışı trafik"/boş sonuç algılanırsa 10 dk boyunca
3088
- tarayıcı ATLANIR — her arama TinyFish'ten başlar, süre dolunca tarayıcı geri döner. */
3089
3158
  const q = String((args && args.query) || '');
3090
3159
  const n = Number((args && args.max_results) || 8);
3091
- let r = null;
3092
- let banned = false;
3093
- const browserUsable = this.browser && typeof this.browser.search === 'function';
3094
- if (Date.now() < (this._webSearchBrowserBanUntil || 0)) banned = true;
3095
- if (browserUsable && !banned) {
3096
- try { r = await this.browser.search(q, signal, { sessionId }); } catch {}
3097
- if (!r || !r.ok || !(r.results || []).length) {
3098
- /* tarayıcı sorunlu — 10 dk atla, alternatif zincir hemen devrede */
3099
- this._webSearchBrowserBanUntil = Date.now() + 10 * 60 * 1000;
3100
- r = null;
3101
- banned = true;
3102
- }
3103
- }
3104
- if (!r || !r.ok || !(r.results || []).length) {
3105
- try { r = await tools.tinyfishSearch(q, n, signal); } catch {}
3106
- }
3107
- if (!r || !r.ok || !(r.results || []).length) {
3108
- try {
3109
- r = JSON.parse(await tools.exec('web_search', args, { cwd: this._sessionWorkspace(sessionId), signal }));
3110
- } catch {}
3111
- }
3112
- const out = r || { ok: false, error: 'web arama başarısız — tüm motorlar boş döndü' };
3113
- if (out && out.ok && banned) out.note = 'tarayıcı araması 10 dk askıda (CAPTCHA/trafik) — TinyFish/alternatif zincir kullanıldı';
3114
- return JSON.stringify(out);
3115
- }
3160
+ return JSON.stringify(await this._webSearchChain(q, n, sessionId, signal));
3161
+ }
3116
3162
  if (name === 'ocr_read') {
3117
3163
  return JSON.stringify(await this._ocrRead(args || {}, signal, sessionId));
3118
3164
  }
@@ -3268,7 +3314,7 @@ const TOOLS = [
3268
3314
  function: {
3269
3315
  name: 'todo_write',
3270
3316
  description:
3271
- '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.',
3272
3318
  parameters: {
3273
3319
  type: 'object',
3274
3320
  properties: {
package/src/agent/llm.js CHANGED
@@ -188,6 +188,51 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReas
188
188
  return { content, reasoning, toolCalls, usage, finishReason };
189
189
  }
190
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];
199
+ }
200
+ return out;
201
+ }
202
+
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;
234
+ }
235
+
191
236
  async function chatOnce(sel, body, { signal, onRetry } = {}) {
192
237
  const res = await withRetries(
193
238
  async () => {
@@ -216,4 +261,4 @@ async function chatOnce(sel, body, { signal, onRetry } = {}) {
216
261
  };
217
262
  }
218
263
 
219
- module.exports = { chatStream, chatOnce, withRetries, friendlyError };
264
+ module.exports = { chatStream, chatStreamAuto, chatOnce, withRetries, friendlyError, sumUsage };