beast-agent 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agent/bots.js +8 -0
- package/src/agent/engine.js +96 -55
- package/src/agent/llm.js +153 -71
- package/src/agent/obscura.js +276 -0
- package/src/agent/skills.js +14 -10
- package/src/agent/tools.js +105 -51
- package/src/main.js +117 -30
- package/src/preload.js +6 -3
- package/src/renderer/i18n.js +72 -26
- package/src/renderer/index.html +1 -0
- package/src/renderer/renderer.js +177 -41
- package/src/renderer/style.css +38 -1
- package/tests/bg-jobs.test.js +71 -3
- package/tests/bots-name.test.js +42 -0
- package/tests/llm.test.js +70 -1
- package/tests/obscura.test.js +124 -0
package/package.json
CHANGED
package/src/agent/bots.js
CHANGED
|
@@ -199,6 +199,8 @@ function add({ name, icon, prompt }) {
|
|
|
199
199
|
loadRegistry();
|
|
200
200
|
const n = String(name || '').trim().slice(0, 40);
|
|
201
201
|
if (!n) return { ok: false, error: 'bot adı zorunlu' };
|
|
202
|
+
/* İLK HARF ZORUNLU: bot adı harf karakteriyle başlamalı */
|
|
203
|
+
if (!/^\p{L}/u.test(n)) return { ok: false, error: 'bot adı harf ile başlamalı — ilk karakter harf olmalı' };
|
|
202
204
|
if (REG.bots.length >= MAX_BOTS) return { ok: false, error: `en fazla ${MAX_BOTS} bot olabilir (1 admin + ${MAX_BOTS - 1} müşteri)` };
|
|
203
205
|
let id = slugify(n);
|
|
204
206
|
while (get(id)) id = slugify(n) + '-' + uid().slice(-4);
|
|
@@ -261,6 +263,12 @@ function update(id, patch) {
|
|
|
261
263
|
loadRegistry();
|
|
262
264
|
const b = get(id);
|
|
263
265
|
if (!b) return { ok: false, error: 'bot yok' };
|
|
266
|
+
/* İLK HARF ZORUNLU: ad değişikliği harf karakteriyle başlamalı —
|
|
267
|
+
diğer alanlara dokunmadan reddet */
|
|
268
|
+
if (patch && typeof patch.name === 'string' && patch.name.trim() &&
|
|
269
|
+
!/^\p{L}/u.test(patch.name.trim())) {
|
|
270
|
+
return { ok: false, error: 'bot adı harf ile başlamalı — ilk karakter harf olmalı' };
|
|
271
|
+
}
|
|
264
272
|
const changes = [];
|
|
265
273
|
if (patch && typeof patch === 'object') {
|
|
266
274
|
if (typeof patch.name === 'string' && patch.name.trim() && patch.name.trim() !== b.name) {
|
package/src/agent/engine.js
CHANGED
|
@@ -630,10 +630,13 @@ class Engine {
|
|
|
630
630
|
{
|
|
631
631
|
signal,
|
|
632
632
|
onDelta,
|
|
633
|
-
onRetry: (attempt) =>
|
|
633
|
+
onRetry: (attempt, st) =>
|
|
634
634
|
emitSafe(this, session.id, {
|
|
635
635
|
type: 'status',
|
|
636
|
-
status:
|
|
636
|
+
status:
|
|
637
|
+
st === undefined || st === null
|
|
638
|
+
? `bağlantı sorunu — internet dönünce ya da yeniden denemeyle sürer (${attempt}/6)`
|
|
639
|
+
: `sağlayıcı yanıtsız — tekrar deniyor (${attempt}/3)`,
|
|
637
640
|
}),
|
|
638
641
|
}
|
|
639
642
|
);
|
|
@@ -1173,6 +1176,8 @@ class Engine {
|
|
|
1173
1176
|
try {
|
|
1174
1177
|
const c = this.ctrls.get(id);
|
|
1175
1178
|
if (c) {
|
|
1179
|
+
this._abortReasons = this._abortReasons || new Map();
|
|
1180
|
+
this._abortReasons.set(String(id), 'oturum silindiği için tur iptal edildi');
|
|
1176
1181
|
c.abort();
|
|
1177
1182
|
this.ctrls.delete(id);
|
|
1178
1183
|
}
|
|
@@ -1182,6 +1187,7 @@ class Engine {
|
|
|
1182
1187
|
const j = this._bgJobs.get(String(id));
|
|
1183
1188
|
j.revive = false;
|
|
1184
1189
|
j.status = 'aborted';
|
|
1190
|
+
j.error = j.error || 'oturum silindi — paralel ajan iptal edildi';
|
|
1185
1191
|
this._clearKicks(id);
|
|
1186
1192
|
}
|
|
1187
1193
|
fs.unlinkSync(this._file(String(id)));
|
|
@@ -1316,7 +1322,7 @@ class Engine {
|
|
|
1316
1322
|
'Tarayıcı eylemlerinin yanıtındaki recent günlüğü ve navigated bilgisini takip et; eylem yanıtları zaten taze snapshot içerir — refler tutarsız görünürse yeni snapshot al.',
|
|
1317
1323
|
'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.',
|
|
1318
1324
|
'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.',
|
|
1319
|
-
|
|
1325
|
+
'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 Obscura stealth tarayıcı + TinyFish + Python çoklu-motor destekli), script gerektiğinde python_run yaz.',
|
|
1320
1326
|
'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).',
|
|
1321
1327
|
'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.',
|
|
1322
1328
|
'Bir oturumda 3+ kez memory_write yaptıysan iş bitince memory_hygiene çağır (duplike/eskime temizliği).',
|
|
@@ -1335,7 +1341,8 @@ class Engine {
|
|
|
1335
1341
|
'Görev tanımı kendi başına YETERLİ olsun: tüm bağlam, dosya/URL yolları, kısıtlar ve beklenen çıktı görev metnine yazılır — paralel ajan senin bağlamını göremez.',
|
|
1336
1342
|
'Birden fazla bağımsız iş varsa hepsini aynı anda ayrı run_background çağrılarıyla devret (paralel çalışsınlar).',
|
|
1337
1343
|
'HIZ FAN-OUT: aynı işin farklı adımlarını run_background_many ile TEK SEFERDE paralel aç (örnek: haber taraması → TR / Dünya / Ekonomi ayrı ajan; araştırma+veri çekme+analiz ayrı ajan). Hepsi bitince sana TEK birleşik rapor düşer — hız dramatik artar. Bağımsız 2+ adım görürsen bunu tercih et.',
|
|
1338
|
-
'Takip araçları: tasks_list (genel tablo), task_status (tek ajanın canlı dökümü), task_cancel (iptal). Kullanıcı ilerleme sorarsa buradan raporla.',
|
|
1344
|
+
'Takip araçları: tasks_list (genel tablo), task_status (tek ajanın canlı dökümü), task_cancel (iptal — reason ZORUNLU). Kullanıcı ilerleme sorarsa buradan raporla.',
|
|
1345
|
+
'İPTAL DİSİPLİNİ: bir paralel ajanı task_cancel ile iptal edersen ya da bir işi yarıda bırakırsan, kullanıcıya TEK CÜMLEyle MUTLAKA SEBEBİNİ yaz (neden iptal ettin, ne kayboldu, yerine ne yapacaksın). Sebepsiz iptal YASAK.',
|
|
1339
1346
|
'Bilgi sorularını, sohbeti, planlamayı, hafıza/hatırlatıcı/izleyici kurulumunu YİNE SEN yaparsın — bunlar "iş" değildir.',
|
|
1340
1347
|
].join('\n')
|
|
1341
1348
|
);
|
|
@@ -1847,7 +1854,7 @@ class Engine {
|
|
|
1847
1854
|
if (j.status === 'running' || j.status === 'queued') {
|
|
1848
1855
|
j.status = 'aborted';
|
|
1849
1856
|
j.endedAt = new Date().toISOString();
|
|
1850
|
-
j.error = String(j.error || '') || 'uygulama kapatıldı';
|
|
1857
|
+
j.error = String(j.error || '') || 'uygulama kapatıldı — paralel ajan yarıda kesildi';
|
|
1851
1858
|
migrated = true;
|
|
1852
1859
|
}
|
|
1853
1860
|
j._persisted = true;
|
|
@@ -1940,9 +1947,18 @@ class Engine {
|
|
|
1940
1947
|
this._pendingReports = this._pendingReports || [];
|
|
1941
1948
|
this._pendingReports.push({ parentId: job.parentId, text });
|
|
1942
1949
|
this.flushPendingReports(job.parentId);
|
|
1943
|
-
} else if (status === 'aborted'
|
|
1944
|
-
/*
|
|
1945
|
-
|
|
1950
|
+
} else if (status === 'aborted') {
|
|
1951
|
+
/* İPTAL SEBEBİ ZORUNLU: sebebi kayda yaz ve üst sohbete/gruba bildir */
|
|
1952
|
+
const why = String(errorMsg || job.error || 'sebep belirtilmedi').slice(0, 300);
|
|
1953
|
+
if (grouped) {
|
|
1954
|
+
/* iptal edilen üye gruptan düşmüş sayılır — birleşik raporda SEBEPİYLE görünür */
|
|
1955
|
+
this._bgGroupRecord(job.title, 'aborted', 'İPTAL: ' + why);
|
|
1956
|
+
} else {
|
|
1957
|
+
const text = `[ARKA PLAN İPTAL: ${job.title}]\nSebep: ${why}`;
|
|
1958
|
+
this._pendingReports = this._pendingReports || [];
|
|
1959
|
+
this._pendingReports.push({ parentId: job.parentId, text });
|
|
1960
|
+
this.flushPendingReports(job.parentId);
|
|
1961
|
+
}
|
|
1946
1962
|
}
|
|
1947
1963
|
}
|
|
1948
1964
|
|
|
@@ -2432,8 +2448,21 @@ class Engine {
|
|
|
2432
2448
|
const aborted = e && (e.name === 'AbortError' || ctrl.signal.aborted);
|
|
2433
2449
|
if (aborted) {
|
|
2434
2450
|
this._clearCrash(); // kullanıcı durdurdu — kurtarma yok
|
|
2435
|
-
|
|
2436
|
-
|
|
2451
|
+
/* öz-kurtarma/superyorizon kick'i bekliyorsa bu tur BİTİŞ değildir —
|
|
2452
|
+
işi aborted KAPATMA, kick mesajı yeni turu açar */
|
|
2453
|
+
const kickJob = this._bgJobs && this._bgJobs.get(sid);
|
|
2454
|
+
if (kickJob && kickJob.revive === true) {
|
|
2455
|
+
emit({ type: 'done', aborted: true });
|
|
2456
|
+
} else {
|
|
2457
|
+
/* İPTAL SEBEBİ ZORUNLU: interrupt() tarafından kaydedilir */
|
|
2458
|
+
const why =
|
|
2459
|
+
(this._abortReasons && this._abortReasons.get(sid)) ||
|
|
2460
|
+
(kickJob && kickJob.error) ||
|
|
2461
|
+
'kullanıcı talebiyle iptal edildi';
|
|
2462
|
+
if (this._abortReasons) this._abortReasons.delete(sid);
|
|
2463
|
+
emit({ type: 'done', aborted: true, reason: why });
|
|
2464
|
+
this._bgFinish(sid, 'aborted', why);
|
|
2465
|
+
}
|
|
2437
2466
|
} else {
|
|
2438
2467
|
// FALLOUT: tüm zincir tükendi — durumu kaydet, açılışta kaldığı yerden devam edilir
|
|
2439
2468
|
this._saveCrash(sid, e, 'chain-exhausted');
|
|
@@ -2676,7 +2705,7 @@ class Engine {
|
|
|
2676
2705
|
if (!this.ctrls.has(pairId)) break;
|
|
2677
2706
|
}
|
|
2678
2707
|
if (this.ctrls.has(pairId)) {
|
|
2679
|
-
try { this.interrupt(pairId); } catch {}
|
|
2708
|
+
try { this.interrupt(pairId, 'bot DM zaman aşımına uğradı (120 sn) — tur kesildi'); } catch {}
|
|
2680
2709
|
return { ok: false, error: 'hedef bot zaman aşımına uğradı (120 sn)' };
|
|
2681
2710
|
}
|
|
2682
2711
|
const after = this.cache.get(pairId) || pair;
|
|
@@ -2790,39 +2819,19 @@ class Engine {
|
|
|
2790
2819
|
}
|
|
2791
2820
|
}
|
|
2792
2821
|
|
|
2793
|
-
/* web_search zinciri — web_search VE deep_search aynı zinciri kullanır
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
4)
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
async _webSearchChain(q, n, sessionId, signal) {
|
|
2801
|
-
let r = null;
|
|
2802
|
-
let banned = false;
|
|
2822
|
+
/* web_search zinciri — web_search VE deep_search aynı zinciri kullanır.
|
|
2823
|
+
Sıra + aç/kapa Ayarlar → Web Arama'dan değiştirilir (tools.searchChainWeb):
|
|
2824
|
+
varsayılan: 1) dahili tarayıcı (DİREK GOOGLE) 2) Obscura (stealth headless
|
|
2825
|
+
→ DuckDuckGo; otomatik kurulur, varsayılan AKTİF) 3) TinyFish (anahtar
|
|
2826
|
+
girildiyse) 4) python çoklu-motor. Tarayıcı CAPTCHA/trafik verirse 10 dk
|
|
2827
|
+
atlanır — sıradaki motor hemen devreye girer. */
|
|
2828
|
+
_webSearchChain(q, n, sessionId, signal) {
|
|
2803
2829
|
const browserUsable = this.browser && typeof this.browser.search === 'function';
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
this._webSearchBrowserBanUntil = Date.now() + 10 * 60 * 1000;
|
|
2810
|
-
r = null;
|
|
2811
|
-
banned = true;
|
|
2812
|
-
}
|
|
2813
|
-
}
|
|
2814
|
-
if (!r || !r.ok || !(r.results || []).length) {
|
|
2815
|
-
try { r = await tools.tinyfishSearch(q, n, signal); } catch {}
|
|
2816
|
-
}
|
|
2817
|
-
if (!r || !r.ok || !(r.results || []).length) {
|
|
2818
|
-
try {
|
|
2819
|
-
r = JSON.parse(await tools.exec('web_search', { query: q, max_results: n }, { cwd: this._sessionWorkspace(sessionId), signal }));
|
|
2820
|
-
} catch {}
|
|
2821
|
-
}
|
|
2822
|
-
const out = r || { ok: false, error: 'web arama başarısız — tüm motorlar boş döndü' };
|
|
2823
|
-
if (out && out.ok && banned) out.note = 'tarayıcı araması 10 dk askıda (CAPTCHA/trafik) — TinyFish/alternatif zincir kullanıldı';
|
|
2824
|
-
return out;
|
|
2825
|
-
}
|
|
2830
|
+
return tools.searchChainWeb(q, n, {
|
|
2831
|
+
signal,
|
|
2832
|
+
browser: browserUsable ? () => this.browser.search(q, signal, { sessionId }) : null,
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2826
2835
|
|
|
2827
2836
|
/* Dahili OCR aracı: görüntüden metin çıkarır (görsel desteklemeyen modeller için).
|
|
2828
2837
|
Kaynak: 'browser' (dahili tarayıcı görüntüsü, varsayılan) | 'screen' (masaüstü) | dosya yolu. */
|
|
@@ -3051,8 +3060,11 @@ class Engine {
|
|
|
3051
3060
|
}
|
|
3052
3061
|
if (name === 'task_cancel') {
|
|
3053
3062
|
const id = String(args.id || '');
|
|
3054
|
-
const
|
|
3055
|
-
|
|
3063
|
+
const why = String(args.reason || '').trim() || 'ana ajan (CEO) bu ajanı task_cancel ile iptal etti';
|
|
3064
|
+
const ok = this.interrupt(id, why);
|
|
3065
|
+
return JSON.stringify(ok
|
|
3066
|
+
? { ok: true, id, reason: why, note: 'Ajan iptal edildi — kullanıcıya MUTLAKA iptal sebebini tek cümleyle açıkla: ' + why }
|
|
3067
|
+
: { ok: false, error: 'çalışan ajan yok: ' + id });
|
|
3056
3068
|
}
|
|
3057
3069
|
if (name === 'set_reminder') {
|
|
3058
3070
|
if (!this.reminders || typeof this.reminders.add !== 'function') {
|
|
@@ -3174,10 +3186,8 @@ class Engine {
|
|
|
3174
3186
|
}
|
|
3175
3187
|
if (name === 'deep_search') {
|
|
3176
3188
|
/* agentic derin araştırma: çoklu sorgu + gizli tarayıcıda sayfa okuma.
|
|
3177
|
-
ARAMA zinciri web_search ile BİREBİR AYNI —
|
|
3178
|
-
|
|
3179
|
-
boş dönünce "tüm motorlar boş döndü" hatası veriyordu. Artık önce
|
|
3180
|
-
DAHİLİ TARAYICI (Google) denenir, sonra TinyFish → python → Exa. */
|
|
3189
|
+
ARAMA zinciri web_search ile BİREBİR AYNI — sıralı zincir
|
|
3190
|
+
(tarayıcı → Obscura → TinyFish → python; Ayarlar'dan değiştirilir). */
|
|
3181
3191
|
try {
|
|
3182
3192
|
emitSafe(this, sessionId, { type: 'status', status: 'derin araştırma: çoklu sorgu + gizli sayfa okuma' });
|
|
3183
3193
|
} catch {}
|
|
@@ -3242,9 +3252,34 @@ class Engine {
|
|
|
3242
3252
|
}
|
|
3243
3253
|
}
|
|
3244
3254
|
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3255
|
+
/* İptal SEBEBİ zorunlu: abort edilen her tur/_bgFinish bu haritadan sebebini
|
|
3256
|
+
okur; ajan neden durdurulduğunu kullanıcıya MUTLAKA bildirir. */
|
|
3257
|
+
_abortReason(reason) {
|
|
3258
|
+
const r = String(reason || '').trim();
|
|
3259
|
+
return (r || 'sebep belirtilmedi — iptal talebi').slice(0, 400);
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
interrupt(sessionId, reason) {
|
|
3263
|
+
const sid = String(sessionId || '');
|
|
3264
|
+
const why = this._abortReason(reason);
|
|
3265
|
+
const c = this.ctrls.get(sid);
|
|
3266
|
+
if (!c) {
|
|
3267
|
+
/* koşan tur yok — kuyrukta bekleyen / öz-kurtarma kick'i bekleyen
|
|
3268
|
+
paralel ajanı da SEBEPİYLE kes */
|
|
3269
|
+
const j = this._bgJobs && this._bgJobs.get(sid);
|
|
3270
|
+
if (j && (j.status === 'queued' || j.revive === true)) {
|
|
3271
|
+
j.status = 'aborted';
|
|
3272
|
+
j.revive = false;
|
|
3273
|
+
j.endedAt = nowIso();
|
|
3274
|
+
j.error = why;
|
|
3275
|
+
this._clearKicks(sid);
|
|
3276
|
+
this._bgEmit();
|
|
3277
|
+
return true;
|
|
3278
|
+
}
|
|
3279
|
+
return false;
|
|
3280
|
+
}
|
|
3281
|
+
this._abortReasons = this._abortReasons || new Map();
|
|
3282
|
+
this._abortReasons.set(sid, why);
|
|
3248
3283
|
c.abort();
|
|
3249
3284
|
return true;
|
|
3250
3285
|
}
|
|
@@ -3254,19 +3289,22 @@ class Engine {
|
|
|
3254
3289
|
main katmanında BAĞIMSIZ yaşar — /stop onlara dokunmaz. */
|
|
3255
3290
|
stopAll() {
|
|
3256
3291
|
let aborted = 0;
|
|
3292
|
+
const why = '/stop: kullanıcı tüm ajanları ve turları durdurdu';
|
|
3257
3293
|
/* fan-out gruplarını kapat — yarım grup artık birleşik rapor beklemesin */
|
|
3258
3294
|
if (this._bgGroups) {
|
|
3259
3295
|
for (const g of this._bgGroups.values()) g.dead = true;
|
|
3260
3296
|
}
|
|
3261
3297
|
if (this._bgPendingStart) this._bgPendingStart.clear();
|
|
3298
|
+
this._abortReasons = this._abortReasons || new Map();
|
|
3262
3299
|
for (const [sid, c] of this.ctrls) {
|
|
3263
|
-
try { c.abort(); aborted++; } catch {}
|
|
3300
|
+
try { this._abortReasons.set(sid, why); c.abort(); aborted++; } catch {}
|
|
3264
3301
|
/* abort sonrası _run finally bloğu temizler; paralel ajan kaydını biz işaretleyelim */
|
|
3265
3302
|
if (this._bgJobs.has(sid)) {
|
|
3266
3303
|
const job = this._bgJobs.get(sid);
|
|
3267
3304
|
if (job.status === 'running' || job.status === 'queued') {
|
|
3268
3305
|
job.status = 'aborted';
|
|
3269
3306
|
job.endedAt = nowIso();
|
|
3307
|
+
job.error = job.error || why;
|
|
3270
3308
|
}
|
|
3271
3309
|
job.revive = false; // bekleyen öz-kurtarma gönderimini de öldür
|
|
3272
3310
|
this._clearKicks(sid);
|
|
@@ -3277,6 +3315,7 @@ class Engine {
|
|
|
3277
3315
|
if (j.status === 'queued') {
|
|
3278
3316
|
j.status = 'aborted';
|
|
3279
3317
|
j.endedAt = nowIso();
|
|
3318
|
+
j.error = j.error || why;
|
|
3280
3319
|
aborted++;
|
|
3281
3320
|
}
|
|
3282
3321
|
}
|
|
@@ -3648,13 +3687,15 @@ const TOOLS = [
|
|
|
3648
3687
|
type: 'function',
|
|
3649
3688
|
function: {
|
|
3650
3689
|
name: 'task_cancel',
|
|
3651
|
-
description:
|
|
3690
|
+
description:
|
|
3691
|
+
'Cancel a running background agent by id (from tasks_list). ALWAYS pass reason: a short sentence explaining WHY you are cancelling — it is recorded on the job and you must report it to the user.',
|
|
3652
3692
|
parameters: {
|
|
3653
3693
|
type: 'object',
|
|
3654
3694
|
properties: {
|
|
3655
3695
|
id: { type: 'string' },
|
|
3696
|
+
reason: { type: 'string', description: 'REQUIRED: why you are cancelling this agent (one short sentence)' },
|
|
3656
3697
|
},
|
|
3657
|
-
required: ['id'],
|
|
3698
|
+
required: ['id', 'reason'],
|
|
3658
3699
|
},
|
|
3659
3700
|
},
|
|
3660
3701
|
},
|
package/src/agent/llm.js
CHANGED
|
@@ -1,10 +1,51 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/* OpenAI-compatible streaming chat client with SSE parsing.
|
|
4
|
-
Geçici sağlayıcı hatalarında (5xx/429
|
|
4
|
+
Geçici sağlayıcı hatalarında (5xx/429) üstel beklemeyle retry; İNTERNET
|
|
5
|
+
KOPMASINDA (fetch failed) çok daha sabırlı: 6 deneme + internet dönene
|
|
6
|
+
dek bekleme (main'deki net izleyicisinden beslenir). Akış ortasında kopan
|
|
7
|
+
bağlantıda: hiç veri akmadıysa sessiz baştan dener, kısmi metin geldiyse
|
|
8
|
+
'length' gibi döner → chatStreamAuto kaldığı yerden DEVAM ETTİRİR. */
|
|
5
9
|
|
|
6
10
|
const RETRYABLE_STATUS = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
|
|
7
|
-
const MAX_RETRIES = 3;
|
|
11
|
+
const MAX_RETRIES = 3; /* sağlayıcı geçici HTTP hataları */
|
|
12
|
+
const NET_MAX_RETRIES = 6; /* bağlantı kopması — daha sabırlı */
|
|
13
|
+
const NET_BACKOFF = [1000, 2000, 4000, 8000, 15000, 25000]; /* ≈55 sn toplam */
|
|
14
|
+
const NET_WAIT_CAP_MS = 120000; /* internet dönene dek bekleme üst sınırı */
|
|
15
|
+
|
|
16
|
+
/* main process'teki net izleyicisi besler: () => boolean | Promise<boolean> */
|
|
17
|
+
let _netProbe = null;
|
|
18
|
+
function setNetProbe(fn) {
|
|
19
|
+
_netProbe = typeof fn === 'function' ? fn : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* fetch failed / ENOTFOUND / ECONNRESET / socket hang up / terminated —
|
|
23
|
+
HTTP yanıtı ALINAMADI demektir (status yok) */
|
|
24
|
+
function isNetworkError(e) {
|
|
25
|
+
if (!e) return false;
|
|
26
|
+
if (e.name === 'AbortError') return false;
|
|
27
|
+
if (e.status !== undefined) return false;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* internet yoksa dönene kadar bekle (en fazla NET_WAIT_CAP_MS); probe yoksa
|
|
32
|
+
ya da durum bilinmiyorsa hemen geç */
|
|
33
|
+
async function waitForNet(signal) {
|
|
34
|
+
if (!_netProbe) return true;
|
|
35
|
+
const t0 = Date.now();
|
|
36
|
+
for (;;) {
|
|
37
|
+
if (signal && signal.aborted) {
|
|
38
|
+
const e = new Error('iptal');
|
|
39
|
+
e.name = 'AbortError';
|
|
40
|
+
throw e;
|
|
41
|
+
}
|
|
42
|
+
let online;
|
|
43
|
+
try { online = await _netProbe(); } catch { online = null; }
|
|
44
|
+
if (online !== false) return true; /* false DEĞİLSE devam (bilinmiyor = iyimser) */
|
|
45
|
+
if (Date.now() - t0 > NET_WAIT_CAP_MS) return false;
|
|
46
|
+
await sleep(2000, signal);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
8
49
|
|
|
9
50
|
const HINTS = {
|
|
10
51
|
401: 'API anahtarı geçersiz',
|
|
@@ -47,8 +88,9 @@ function sleep(ms, signal) {
|
|
|
47
88
|
});
|
|
48
89
|
}
|
|
49
90
|
|
|
50
|
-
/* fn: denenecek istek.
|
|
51
|
-
|
|
91
|
+
/* fn: denenecek istek. Sağlayıcı geçici HTTP hatalarında 0.8s→1.6s→3.2s;
|
|
92
|
+
İNTERNET KOPMASINDA (status yok) 6 denemeye kadar 1s→2s→4s→8s→15s→25s ve
|
|
93
|
+
net izleyicisi "çevrimdışı" diyorsa internet dönene kadar bekler. */
|
|
52
94
|
async function withRetries(fn, { signal, onRetry } = {}) {
|
|
53
95
|
let attempt = 0;
|
|
54
96
|
for (;;) {
|
|
@@ -57,14 +99,23 @@ async function withRetries(fn, { signal, onRetry } = {}) {
|
|
|
57
99
|
} catch (e) {
|
|
58
100
|
const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));
|
|
59
101
|
const status = e && e.status;
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
if (!retriable
|
|
102
|
+
const netErr = !aborted && status === undefined;
|
|
103
|
+
const retriable = !aborted && (status === undefined || RETRYABLE_STATUS.has(status));
|
|
104
|
+
if (!retriable) throw e;
|
|
105
|
+
const cap = netErr ? NET_MAX_RETRIES : MAX_RETRIES;
|
|
106
|
+
if (attempt >= cap) throw e;
|
|
107
|
+
if (netErr && _netProbe) {
|
|
108
|
+
/* internet kopmuş: dönene kadar bekle — kısa kopmalarda görev ölmez */
|
|
109
|
+
await waitForNet(signal);
|
|
110
|
+
}
|
|
63
111
|
attempt++;
|
|
64
112
|
try {
|
|
65
113
|
onRetry && onRetry(attempt, status);
|
|
66
114
|
} catch {}
|
|
67
|
-
|
|
115
|
+
const wait = netErr
|
|
116
|
+
? NET_BACKOFF[Math.min(attempt - 1, NET_BACKOFF.length - 1)]
|
|
117
|
+
: 800 * Math.pow(2, attempt - 1);
|
|
118
|
+
await sleep(wait, signal);
|
|
68
119
|
}
|
|
69
120
|
}
|
|
70
121
|
}
|
|
@@ -113,79 +164,101 @@ async function chatStream(sel, body, { signal, onDelta, onRetry } = {}) {
|
|
|
113
164
|
}
|
|
114
165
|
|
|
115
166
|
async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReasoning = false) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
167
|
+
/* akış durumu dışarıda tutulur: gövde okunurken bağlantı koparsa
|
|
168
|
+
ne kadarı geldiğine bakılır (kısmi metin → devam; boş/araç → baştan) */
|
|
169
|
+
const state = { content: '', reasoning: '', toolCalls: [], usage: null, finishReason: null };
|
|
170
|
+
let dropAttempt = 0;
|
|
171
|
+
|
|
172
|
+
for (;;) {
|
|
173
|
+
const res = await withRetries(
|
|
174
|
+
async () => {
|
|
175
|
+
const r = await openChat(sel, body, { stream: true, signal, omitReasoning });
|
|
176
|
+
if (!r.ok) {
|
|
177
|
+
let detail = '';
|
|
178
|
+
try {
|
|
179
|
+
detail = (await r.text()).slice(0, 300);
|
|
180
|
+
} catch {}
|
|
181
|
+
const err = new Error(friendlyError(r.status, r.statusText, detail));
|
|
182
|
+
err.status = r.status;
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
return r;
|
|
186
|
+
},
|
|
187
|
+
{ signal, onRetry }
|
|
188
|
+
);
|
|
132
189
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
let finishReason = null;
|
|
190
|
+
try {
|
|
191
|
+
const reader = res.body.getReader();
|
|
192
|
+
const decoder = new TextDecoder();
|
|
193
|
+
let buf = '';
|
|
138
194
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
195
|
+
while (true) {
|
|
196
|
+
const { done, value } = await reader.read();
|
|
197
|
+
if (done) break;
|
|
198
|
+
buf += decoder.decode(value, { stream: true });
|
|
142
199
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
200
|
+
let idx;
|
|
201
|
+
while ((idx = buf.indexOf('\n')) !== -1) {
|
|
202
|
+
const line = buf.slice(0, idx).trim();
|
|
203
|
+
buf = buf.slice(idx + 1);
|
|
204
|
+
if (!line.startsWith('data:')) continue;
|
|
205
|
+
const data = line.slice(5).trim();
|
|
206
|
+
if (!data || data === '[DONE]') continue;
|
|
147
207
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (!data || data === '[DONE]') continue;
|
|
208
|
+
let json;
|
|
209
|
+
try {
|
|
210
|
+
json = JSON.parse(data);
|
|
211
|
+
} catch {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
155
214
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
215
|
+
if (json.usage) state.usage = json.usage;
|
|
216
|
+
const ch = json.choices && json.choices[0];
|
|
217
|
+
if (!ch) continue;
|
|
218
|
+
if (ch.finish_reason) state.finishReason = ch.finish_reason;
|
|
219
|
+
const d = ch.delta || {};
|
|
220
|
+
if (d.content) {
|
|
221
|
+
state.content += d.content;
|
|
222
|
+
onDelta && onDelta(d.content, state.content);
|
|
223
|
+
}
|
|
224
|
+
if (d.reasoning_content) state.reasoning += d.reasoning_content;
|
|
225
|
+
if (d.reasoning) state.reasoning += d.reasoning;
|
|
226
|
+
for (const tc of d.tool_calls || []) {
|
|
227
|
+
const i = typeof tc.index === 'number' ? tc.index : state.toolCalls.length;
|
|
228
|
+
while (state.toolCalls.length <= i) {
|
|
229
|
+
state.toolCalls.push({ id: '', type: 'function', function: { name: '', arguments: '' } });
|
|
230
|
+
}
|
|
231
|
+
if (tc.id) state.toolCalls[i].id = tc.id;
|
|
232
|
+
if (tc.function) {
|
|
233
|
+
if (tc.function.name) state.toolCalls[i].function.name += tc.function.name;
|
|
234
|
+
if (tc.function.arguments) state.toolCalls[i].function.arguments += tc.function.arguments;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
161
238
|
}
|
|
162
239
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (d.reasoning) reasoning += d.reasoning;
|
|
174
|
-
for (const tc of d.tool_calls || []) {
|
|
175
|
-
const i = typeof tc.index === 'number' ? tc.index : toolCalls.length;
|
|
176
|
-
while (toolCalls.length <= i) {
|
|
177
|
-
toolCalls.push({ id: '', type: 'function', function: { name: '', arguments: '' } });
|
|
178
|
-
}
|
|
179
|
-
if (tc.id) toolCalls[i].id = tc.id;
|
|
180
|
-
if (tc.function) {
|
|
181
|
-
if (tc.function.name) toolCalls[i].function.name += tc.function.name;
|
|
182
|
-
if (tc.function.arguments) toolCalls[i].function.arguments += tc.function.arguments;
|
|
240
|
+
return { ...state };
|
|
241
|
+
} catch (e) {
|
|
242
|
+
/* okuma sırasında bağlantı koptu */
|
|
243
|
+
const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));
|
|
244
|
+
if (!aborted && isNetworkError(e)) {
|
|
245
|
+
if (state.content && !state.toolCalls.length) {
|
|
246
|
+
/* kısmi metin geldi, araç çağrısı yok → hata balonu YOK:
|
|
247
|
+
'length' gibi dön, chatStreamAuto CONTINUE_PROMPT ile kaldığı
|
|
248
|
+
yerden sürdürür ve metni birleştirir */
|
|
249
|
+
return { ...state, finishReason: 'length' };
|
|
183
250
|
}
|
|
251
|
+
/* hiç veri akmadı YA DA araç çağrısı yarım kaldı → BAŞTAN dene */
|
|
252
|
+
if (dropAttempt >= NET_MAX_RETRIES) throw e;
|
|
253
|
+
dropAttempt++;
|
|
254
|
+
try { onRetry && onRetry(dropAttempt, undefined); } catch {}
|
|
255
|
+
if (_netProbe) await waitForNet(signal);
|
|
256
|
+
await sleep(NET_BACKOFF[Math.min(dropAttempt - 1, NET_BACKOFF.length - 1)], signal);
|
|
257
|
+
continue; /* yeni stream — UI'daki kısmi token'lar nihai mesajla ezilir */
|
|
184
258
|
}
|
|
259
|
+
throw e;
|
|
185
260
|
}
|
|
186
261
|
}
|
|
187
|
-
|
|
188
|
-
return { content, reasoning, toolCalls, usage, finishReason };
|
|
189
262
|
}
|
|
190
263
|
|
|
191
264
|
/* usage toplama: devam turları gerçek faturalamayı yansıtsın (prompt yeniden sayılır) */
|
|
@@ -261,4 +334,13 @@ async function chatOnce(sel, body, { signal, onRetry } = {}) {
|
|
|
261
334
|
};
|
|
262
335
|
}
|
|
263
336
|
|
|
264
|
-
module.exports = {
|
|
337
|
+
module.exports = {
|
|
338
|
+
chatStream,
|
|
339
|
+
chatStreamAuto,
|
|
340
|
+
chatOnce,
|
|
341
|
+
withRetries,
|
|
342
|
+
friendlyError,
|
|
343
|
+
sumUsage,
|
|
344
|
+
setNetProbe,
|
|
345
|
+
isNetworkError,
|
|
346
|
+
};
|