beast-agent 1.4.2 → 1.5.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/engine.js +239 -15
- package/src/agent/llm.js +62 -39
- package/src/main.js +478 -75
- package/src/preload.js +5 -1
- package/src/renderer/i18n.js +12 -2
- package/src/renderer/index.html +30 -7
- package/src/renderer/renderer.js +477 -60
- package/src/renderer/style.css +288 -20
package/package.json
CHANGED
package/src/agent/engine.js
CHANGED
|
@@ -216,6 +216,7 @@ function sanitizeTodoItems(items) {
|
|
|
216
216
|
return out;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
|
|
219
220
|
function customChain(list) {
|
|
220
221
|
const out = [];
|
|
221
222
|
for (const p of Array.isArray(list) ? list : []) {
|
|
@@ -269,6 +270,11 @@ class Engine {
|
|
|
269
270
|
this._customProviders = opts.customProviders || [];
|
|
270
271
|
this.cache = new Map();
|
|
271
272
|
this.ctrls = new Map();
|
|
273
|
+
/* /STOP KAPISI: stopAll sonrası SİSTEM tetikli gönderimler (rapor, kick,
|
|
274
|
+
self-heal, cron, izleyici, olay merkezi, bg kuyruk) engellenir — ajan
|
|
275
|
+
kendi kendine yeni sorgu açamaz. Gerçek kullanıcı mesajı (userAction)
|
|
276
|
+
veya /start (clearStop) kapıyı açar. */
|
|
277
|
+
this._stopped = false;
|
|
272
278
|
this.todos = new Map(); // sessionId -> [{title,status}]
|
|
273
279
|
this.tokRatio = 1; // gerçek prompt_tokens ile kalibre edilir
|
|
274
280
|
this._codeIndex = new Map(); // kısa oturum kodu -> session id
|
|
@@ -946,7 +952,7 @@ class Engine {
|
|
|
946
952
|
|
|
947
953
|
/* Oturum dosyasındaki son 'msg' satırını (user) güncel içerikle değiştir —
|
|
948
954
|
yanıtsız kalmış user mesajıyla yeni mesaj birleştirildiğinde kullanılır */
|
|
949
|
-
_rewriteLastMsg(id, content) {
|
|
955
|
+
_rewriteLastMsg(id, content, attachments) {
|
|
950
956
|
try {
|
|
951
957
|
const file = this._file(String(id));
|
|
952
958
|
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
|
@@ -955,7 +961,9 @@ class Engine {
|
|
|
955
961
|
let r;
|
|
956
962
|
try { r = JSON.parse(lines[i]); } catch { continue; }
|
|
957
963
|
if (r.t === 'msg' && r.role === 'user') {
|
|
958
|
-
|
|
964
|
+
const out = { t: 'msg', role: 'user', content: String(content || '') };
|
|
965
|
+
if (Array.isArray(attachments) && attachments.length) out.attachments = attachments;
|
|
966
|
+
lines[i] = JSON.stringify(out);
|
|
959
967
|
break;
|
|
960
968
|
}
|
|
961
969
|
}
|
|
@@ -1394,7 +1402,8 @@ class Engine {
|
|
|
1394
1402
|
'3) EDİT: VAR OLAN dosyada önce edit_file kullan (old_string/new_string ile yalnız ilgili bölümü değiştir; birden çok eşleşme varsa bağlam ekle ya da replace_all); write_file yalnız YENİ dosya ya da tam yeniden yazım için. Dosya işlemleri için ÖZEL ARAÇLARI kullan (edit_file/write_file/read_file/grep/glob); run_command terminal işlerindir (build, git, kurulum, paket) — dosya düzenlemeyi komut/scripte yedirme, edit_file ile yap. İlgisiz yeniden biçimleme/kayıp boşluk değişikliği YAPMA. edit_file/write_file sonucu additions/deletions döner ve değişiklik diske ANINDA uygulanır — doğrulamak için dosyayı TEKRAR OKUMA YASAK; sonraki editi önceki okuduğun içerik + kendi değişikliklerin üzerinden zincirle.\n' +
|
|
1395
1403
|
'4) DOĞRULA: edit sonrası mümkünse derle/test et/lint çalıştır (run_command); hata varsa DÜZELT ve TEKRAR dene (en fazla 2 doğrulama turu) — kırmızı bırakma. Doğrulama read_file ile DEĞİL run_command ile yapılır.\n' +
|
|
1396
1404
|
'5) RAPOR: 1-3 satır — ne değişti + doğrulama sonucu (ör. "npm test ✓ 154/154"). Uzun açıklama yok.\n' +
|
|
1397
|
-
'CANLI ÖNİZLEME:
|
|
1405
|
+
'CANLI ÖNİZLEME: panel Preview\u2019da DAHİLİ STATİK SUNUCUYU kendisi yönetir — statik site için sunucu başlatma DENEME (python -m http.server vs. GEREKMEZ); index.html\u2019i hazır bırak, kullanıcı Preview\u2019a basınca site sunucudan canlı açılır ve sana observe ile haber verilir. Yalnızca gerçek dev-server/build gerektiren projelerde (React/Vite/Next/Expo) kendi sunucunu BLOKLAMADAN arka planda başlat ve çalışan adresi (http://localhost:PORT) raporda yaz. file:// protokolü ASLA kullanılmaz.\n' +
|
|
1406
|
+
'MOBİL UYGULAMA: React Native/Expo ile yap — `npx create-expo-app <ad>` + `npx expo start` (BLOKLAMADAN arka planda; başlangıç çıktısında http://localhost:8081 görünür, panel Telefon Modunda canlı açar, dosya kaydında hot reload çalışır). APK: `npx expo prebuild` + android\\gradlew assembleDebug; iOS: Windows\u2019ta simülatör YOK — gerçek cihazda Expo Go (QR) veya EAS cloud build kullan.\n' +
|
|
1398
1407
|
'HIZ KURALLARI:\n' +
|
|
1399
1408
|
'- Kodu ve dosyaları DOĞRUDAN write_file ile yaz; basit dosya oluşturma/düzenleme için Python scripti yazma; python_run yalnız gerçek hesap/veri işleme gerekiyorsa.\n' +
|
|
1400
1409
|
'- Bağımsız araç çağrılarını AYNI TURDA paralel ver (birden çok read_file tek turda).\n' +
|
|
@@ -1593,7 +1602,37 @@ class Engine {
|
|
|
1593
1602
|
}
|
|
1594
1603
|
|
|
1595
1604
|
_buildPayload(system, messages, notes, budget, summary) {
|
|
1596
|
-
|
|
1605
|
+
/* DOSYA EK ENJEKSİYONU (payload-only): kullanıcı mesajlarındaki dosya
|
|
1606
|
+
ekleri SADECE burada, LLM isteğine metin olarak eklenir. Oturum
|
|
1607
|
+
mesajı/ekran temiz kalır — chat'te yalnız dosya kartı görünür. */
|
|
1608
|
+
let payloadSrc = messages;
|
|
1609
|
+
const withFiles = messages.map((m) => {
|
|
1610
|
+
if (!m || m.role !== 'user' || !Array.isArray(m.attachments)) return m;
|
|
1611
|
+
const docs = m.attachments.filter((a) => a && a.type === 'file' && a.content);
|
|
1612
|
+
if (!docs.length) return m;
|
|
1613
|
+
const extra = docs
|
|
1614
|
+
.map((f) => `[Ek dosya: ${f.name}]\n${String(f.content || '')}`)
|
|
1615
|
+
.join('\n\n');
|
|
1616
|
+
const { attachments: _drop, ...rest } = m;
|
|
1617
|
+
if (typeof rest.content === 'string') {
|
|
1618
|
+
rest.content = (rest.content + (rest.content ? '\n\n' : '') + extra).trim();
|
|
1619
|
+
} else if (Array.isArray(rest.content)) {
|
|
1620
|
+
let hit = false;
|
|
1621
|
+
rest.content = rest.content.map((p) => {
|
|
1622
|
+
if (p && p.type === 'text' && !hit) {
|
|
1623
|
+
hit = true;
|
|
1624
|
+
return { ...p, text: ((p.text || '') + (p.text ? '\n\n' : '') + extra).trim() };
|
|
1625
|
+
}
|
|
1626
|
+
return p;
|
|
1627
|
+
});
|
|
1628
|
+
if (!hit) rest.content = [...rest.content, { type: 'text', text: extra }];
|
|
1629
|
+
} else {
|
|
1630
|
+
rest.content = extra;
|
|
1631
|
+
}
|
|
1632
|
+
return rest;
|
|
1633
|
+
});
|
|
1634
|
+
payloadSrc = withFiles;
|
|
1635
|
+
const units = this._msgUnits(payloadSrc);
|
|
1597
1636
|
const B = Math.max(2000, Number(budget) || this.historyTokenBudget);
|
|
1598
1637
|
const maxUnits = notes ? NOTES_MAX_UNITS : units.length; // notlar varken pencereyi sıkılaştır
|
|
1599
1638
|
const picked = [];
|
|
@@ -1657,6 +1696,13 @@ class Engine {
|
|
|
1657
1696
|
'\n[Konuşma aşağıdaki mesajlarla DEVAM EDİYOR]',
|
|
1658
1697
|
});
|
|
1659
1698
|
}
|
|
1699
|
+
/* emniyet: sağlayıcıya GİDEN hiçbir mesajda `attachments` alanı olmasın */
|
|
1700
|
+
for (let k = 0; k < flat.length; k++) {
|
|
1701
|
+
if (flat[k] && flat[k].attachments) {
|
|
1702
|
+
const { attachments: _drop, ...rest } = flat[k];
|
|
1703
|
+
flat[k] = rest;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1660
1706
|
return [{ role: 'system', content: system }, ...head, ...flat];
|
|
1661
1707
|
}
|
|
1662
1708
|
|
|
@@ -2009,14 +2055,16 @@ class Engine {
|
|
|
2009
2055
|
const sid = session.id;
|
|
2010
2056
|
if (session.bgJob || session.bcCode || session.isBotDm) return;
|
|
2011
2057
|
if (prevSignal && prevSignal.aborted) return;
|
|
2058
|
+
/* /stop kapısı: not/memory/yansıma bakımı YENİ LLM SORGUSU açmaz */
|
|
2059
|
+
if (this._stopped) return;
|
|
2012
2060
|
if (!this._housekeepingTail) this._housekeepingTail = new Map();
|
|
2013
2061
|
const job = this._housekeepingTail.get(sid) || Promise.resolve();
|
|
2014
2062
|
const next = job.catch(() => {}).then(async () => {
|
|
2015
2063
|
const ctl = new AbortController();
|
|
2016
2064
|
try { await this._updateSessionNotes(session, ctl.signal); } catch {}
|
|
2017
|
-
if (ctl.signal.aborted || this.ctrls.has(sid)) return;
|
|
2065
|
+
if (ctl.signal.aborted || this._stopped || this.ctrls.has(sid)) return;
|
|
2018
2066
|
try { await this._autoMemory(session, ctl.signal); } catch {}
|
|
2019
|
-
if (this.reflection.enabled && !ctl.signal.aborted && !this.ctrls.has(sid)) {
|
|
2067
|
+
if (this.reflection.enabled && !ctl.signal.aborted && !this._stopped && !this.ctrls.has(sid)) {
|
|
2020
2068
|
try { await this._maybeReflectSkill(session); } catch {}
|
|
2021
2069
|
}
|
|
2022
2070
|
try { memory.calibratePersona(); } catch {}
|
|
@@ -2026,7 +2074,12 @@ class Engine {
|
|
|
2026
2074
|
|
|
2027
2075
|
/* ---------- agent loop ---------- */
|
|
2028
2076
|
|
|
2029
|
-
send(sessionId, payload) {
|
|
2077
|
+
send(sessionId, payload, opts = {}) {
|
|
2078
|
+
const userAction = !!(opts && opts.userAction);
|
|
2079
|
+
/* /stop kapısı: sistem tetikli gönderim durdurulur; kullanıcının kendi
|
|
2080
|
+
mesajı (userAction) kapıyı kaldırır — "devam için bir şeyler yaz" */
|
|
2081
|
+
if (this._stopped && !userAction) return false;
|
|
2082
|
+
if (userAction) this._stopped = false;
|
|
2030
2083
|
const s = this._load(String(sessionId));
|
|
2031
2084
|
if (!s || this.ctrls.has(s.id)) return false;
|
|
2032
2085
|
|
|
@@ -2037,17 +2090,28 @@ class Engine {
|
|
|
2037
2090
|
const text = String((payload && payload.text) || '').slice(0, USER_MAX);
|
|
2038
2091
|
const atts = Array.isArray(payload && payload.attachments) ? payload.attachments : [];
|
|
2039
2092
|
const imgs = atts.filter((a) => a.type === 'image' && a.dataUrl).slice(0, 4);
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2093
|
+
/* DOSYA EKLERİ İÇERİĞE GÖMÜLMEZ: mesajda `attachments` alanında taşınır —
|
|
2094
|
+
chat ekranında DOSYA KARTI olarak görünür, içerik ekrana yazılmaz.
|
|
2095
|
+
İçeriği yalnızca _buildPayload LLM isteğine enjekte eder. */
|
|
2096
|
+
const files = atts
|
|
2097
|
+
.filter((a) => a.type === 'file')
|
|
2098
|
+
.map((a) => ({ name: String(a.name || 'dosya'), content: String(a.content || '').slice(0, 20000) }));
|
|
2099
|
+
const body = (text.trim() || (files.length || imgs.length ? '[dosya ekleri]' : '(ek)')).slice(0, USER_MAX);
|
|
2045
2100
|
if (imgs.length) {
|
|
2046
|
-
const parts = [{ type: 'text', text:
|
|
2101
|
+
const parts = [{ type: 'text', text: body }];
|
|
2047
2102
|
for (const im of imgs) parts.push({ type: 'image_url', image_url: { url: im.dataUrl } });
|
|
2048
2103
|
msg = { role: 'user', content: parts };
|
|
2049
2104
|
} else {
|
|
2050
|
-
msg = { role: 'user', content:
|
|
2105
|
+
msg = { role: 'user', content: body };
|
|
2106
|
+
}
|
|
2107
|
+
if (imgs.length || files.length) {
|
|
2108
|
+
/* UI dosya kartları + payload enjeksiyonu buradan beslenir:
|
|
2109
|
+
dosyalar İÇERİKLE birlikte `attachments`ta yaşar (görüntüler yalnız
|
|
2110
|
+
ad taşır — veri content parçalarındadır). Chat ekranı yalnız ADI basar. */
|
|
2111
|
+
msg.attachments = [
|
|
2112
|
+
...imgs.map((im) => ({ type: 'image', name: String(im.name || 'resim') })),
|
|
2113
|
+
...files,
|
|
2114
|
+
];
|
|
2051
2115
|
}
|
|
2052
2116
|
}
|
|
2053
2117
|
|
|
@@ -2067,8 +2131,11 @@ class Engine {
|
|
|
2067
2131
|
typeof msg.content === 'string'
|
|
2068
2132
|
) {
|
|
2069
2133
|
lastMsg.content = lastMsg.content + '\n' + msg.content;
|
|
2134
|
+
if (msg.attachments || lastMsg.attachments) {
|
|
2135
|
+
lastMsg.attachments = [...(lastMsg.attachments || []), ...(msg.attachments || [])];
|
|
2136
|
+
}
|
|
2070
2137
|
msg = lastMsg;
|
|
2071
|
-
this._rewriteLastMsg(s.id, lastMsg.content);
|
|
2138
|
+
this._rewriteLastMsg(s.id, lastMsg.content, lastMsg.attachments);
|
|
2072
2139
|
} else {
|
|
2073
2140
|
s.messages.push(msg);
|
|
2074
2141
|
try {
|
|
@@ -2141,6 +2208,8 @@ class Engine {
|
|
|
2141
2208
|
runBackground(parentSessionId, task, title, opts = {}) {
|
|
2142
2209
|
const t = String(task || '').trim();
|
|
2143
2210
|
if (!t) return { ok: false, error: 'görev boş' };
|
|
2211
|
+
/* /stop kapısı: durdurulmuş sistemde yeni arka plan işi başlamaz */
|
|
2212
|
+
if (this._stopped) return { ok: false, error: '/stop aktif — arka plan işi başlatılamadı' };
|
|
2144
2213
|
const parent = String(parentSessionId || '');
|
|
2145
2214
|
/* createSession() view döndürür (messages yok) — tam oturumu cache'ten al.
|
|
2146
2215
|
Aksi halde send() s.messages.push'da sessizce patlardı ve iş koşmazdı. */
|
|
@@ -2203,6 +2272,8 @@ class Engine {
|
|
|
2203
2272
|
|
|
2204
2273
|
_bgAdmit(sid, fullTask) {
|
|
2205
2274
|
try {
|
|
2275
|
+
/* /stop kapısı: kuyruğa alma ve başlatma durdurulur */
|
|
2276
|
+
if (this._stopped) return;
|
|
2206
2277
|
const j = this._bgJobs.get(String(sid));
|
|
2207
2278
|
if (!j || j.status !== 'running' || j.slot) return;
|
|
2208
2279
|
if (this._runningBgCount() >= this._bgLimit) {
|
|
@@ -2221,6 +2292,9 @@ class Engine {
|
|
|
2221
2292
|
}
|
|
2222
2293
|
|
|
2223
2294
|
_bgResumeQueue() {
|
|
2295
|
+
/* /stop kapısı: sıradaki iş BAŞLATILMAZ (status mutate edilmeden dönülür —
|
|
2296
|
+
hayalet 'running' kaydı oluşmasın) */
|
|
2297
|
+
if (this._stopped) return;
|
|
2224
2298
|
while (this._runningBgCount() < this._bgLimit) {
|
|
2225
2299
|
let next = null;
|
|
2226
2300
|
for (const j of this._bgJobs.values()) {
|
|
@@ -2274,6 +2348,7 @@ class Engine {
|
|
|
2274
2348
|
|
|
2275
2349
|
/* #17 kalıcı arka plan hatası → owner'a tek seferlik uyarı maili (fire-and-forget) */
|
|
2276
2350
|
_notifyOwnerTaskFailed(job, msg) {
|
|
2351
|
+
if (this._stopped) return; /* /stop: durdurulan iş için mail UYANDIRMA */
|
|
2277
2352
|
if (this.notifyOwnerFail === false) return;
|
|
2278
2353
|
if (!this.email || typeof this.email.send !== 'function') return;
|
|
2279
2354
|
if (job._mailed) return;
|
|
@@ -2597,6 +2672,8 @@ class Engine {
|
|
|
2597
2672
|
|
|
2598
2673
|
_bgKickSend(sid, text) {
|
|
2599
2674
|
try {
|
|
2675
|
+
/* /stop kapısı: durdurulan işe kurtarma kick'i GİTMEZ */
|
|
2676
|
+
if (this._stopped) return;
|
|
2600
2677
|
const j = this._bgJobs.get(String(sid));
|
|
2601
2678
|
/* silinmiş/durdurulmuş işe geri dönüş yok — hayalet dosya yaratma */
|
|
2602
2679
|
if (!j || j.revive !== true) return;
|
|
@@ -2672,6 +2749,12 @@ class Engine {
|
|
|
2672
2749
|
send başarısızsa (aniden meşgul oldu) rapor sırada kalır, sonra denenir. */
|
|
2673
2750
|
flushPendingReports(sessionId) {
|
|
2674
2751
|
if (!this._pendingReports || !this._pendingReports.length) return;
|
|
2752
|
+
/* /stop kapısı: abort edilen işlerin iptal raporları ebeveynde YENİ SORGU
|
|
2753
|
+
başlatmasın — bekleyen raporlar düşürülür */
|
|
2754
|
+
if (this._stopped) {
|
|
2755
|
+
this._pendingReports = [];
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2675
2758
|
const rest = [];
|
|
2676
2759
|
for (const r of this._pendingReports) {
|
|
2677
2760
|
if (r.parentId !== sessionId) { rest.push(r); continue; }
|
|
@@ -2960,6 +3043,13 @@ class Engine {
|
|
|
2960
3043
|
iş bitince dahili tarayıcıda CANLI açılır (site/dev server/HTML) */
|
|
2961
3044
|
const bcArtifacts = session.bcCode ? { html: [], serverUrl: null } : null;
|
|
2962
3045
|
for (let turn = 0; turn < maxTurns; turn++) {
|
|
3046
|
+
/* /STOP: abort edilmişse yeni tur/araç döngüsü AÇILMAZ — hemen
|
|
3047
|
+
AbortError fırlat, catch bloğu done(aborted) basar */
|
|
3048
|
+
if (ctrl.signal.aborted) {
|
|
3049
|
+
const e = new Error('iptal');
|
|
3050
|
+
e.name = 'AbortError';
|
|
3051
|
+
throw e;
|
|
3052
|
+
}
|
|
2963
3053
|
/* SÜRE SINIRI YOK. Ana oturum sınırsız tur koşar (opencode birebir);
|
|
2964
3054
|
yalnız bg ajan son 3 tura gelirken "raporu yaz ve bitir" uyarısı alır. */
|
|
2965
3055
|
if (session.bgJob && turn === maxTurns - 3 && !wrapNudged) {
|
|
@@ -3214,6 +3304,8 @@ class Engine {
|
|
|
3214
3304
|
teşhis görevini AYNI oturuma düşürür; agent kendi hatasını analiz eder. */
|
|
3215
3305
|
_maybeSelfHeal(sid, err) {
|
|
3216
3306
|
try {
|
|
3307
|
+
/* /stop kapısı: durdurulan oturuma teşhis sorgusu AÇILMAZ */
|
|
3308
|
+
if (this._stopped) return;
|
|
3217
3309
|
const key = String(sid);
|
|
3218
3310
|
this._errCount = this._errCount || new Map();
|
|
3219
3311
|
const n = (this._errCount.get(key) || 0) + 1;
|
|
@@ -3701,6 +3793,8 @@ class Engine {
|
|
|
3701
3793
|
});
|
|
3702
3794
|
}
|
|
3703
3795
|
}
|
|
3796
|
+
/* GERİ ALMA GÜNLÜĞÜ: dosya yazımından ÖNCE eski içerik kayda geçer */
|
|
3797
|
+
this._journalBefore(sessionId, name, args);
|
|
3704
3798
|
if (name === 'memory_write') {
|
|
3705
3799
|
/* bot oturumu → botun KENDİ MEMORY.md'sine yaz (global Beast hafızasına değil) */
|
|
3706
3800
|
const bctx = this._sessionBotCtx(sessionId ? this.cache.get(String(sessionId)) : null);
|
|
@@ -3736,6 +3830,7 @@ class Engine {
|
|
|
3736
3830
|
}
|
|
3737
3831
|
if (name === 'todo_write') {
|
|
3738
3832
|
const items = sanitizeTodoItems(args.items);
|
|
3833
|
+
this._tagTodoIds(sessionId, items); /* her madde kalıcı ID taşır — geri alma buna bağlanır */
|
|
3739
3834
|
this.todos.set(sessionId, items);
|
|
3740
3835
|
try {
|
|
3741
3836
|
fs.appendFileSync(
|
|
@@ -4041,6 +4136,10 @@ class Engine {
|
|
|
4041
4136
|
stopAll() {
|
|
4042
4137
|
let aborted = 0;
|
|
4043
4138
|
const why = '/stop: kullanıcı tüm ajanları ve turları durdurdu';
|
|
4139
|
+
/* STOP KAPISI: abort sonrası rapor/kick/kurtarma zincirleri YENİ SORGU
|
|
4140
|
+
AÇAMAZ — kullanıcı gerçek bir mesaj yazana ya da /start deyinceye dek
|
|
4141
|
+
ajan faaliyeti tamamen durur */
|
|
4142
|
+
this._stopped = true;
|
|
4044
4143
|
/* fan-out gruplarını kapat — yarım grup artık birleşik rapor beklemesin */
|
|
4045
4144
|
if (this._bgGroups) {
|
|
4046
4145
|
for (const g of this._bgGroups.values()) g.dead = true;
|
|
@@ -4077,6 +4176,11 @@ class Engine {
|
|
|
4077
4176
|
return aborted;
|
|
4078
4177
|
}
|
|
4079
4178
|
|
|
4179
|
+
/* /stop kapısını kaldır (/start ya da gerçek kullanıcı mesajıyla) */
|
|
4180
|
+
clearStop() {
|
|
4181
|
+
this._stopped = false;
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4080
4184
|
isBusy(sessionId) {
|
|
4081
4185
|
return this.ctrls.has(String(sessionId));
|
|
4082
4186
|
}
|
|
@@ -4670,6 +4774,126 @@ const TOOLS = [
|
|
|
4670
4774
|
},
|
|
4671
4775
|
];
|
|
4672
4776
|
|
|
4777
|
+
/* ---------- GÖREV (TODO) ID + GERİ ALMA SİSTEMİ ----------
|
|
4778
|
+
todo_write maddelerine kalıcı ID (T1, T2…) atanır; write_file/edit_file
|
|
4779
|
+
ÖNCESİ dosyanın eski içeriği günlüğe yazılır. Panel bir maddenin
|
|
4780
|
+
değişikliklerini TEK TUŞLA önceki kod tabanına döndürebilir. */
|
|
4781
|
+
|
|
4782
|
+
Engine.prototype._tagTodoIds = function (sid, items) {
|
|
4783
|
+
const key = String(sid || '');
|
|
4784
|
+
this._todoSeq = this._todoSeq || new Map();
|
|
4785
|
+
this._todoIds = this._todoIds || new Map();
|
|
4786
|
+
const map = this._todoIds.get(key) || new Map();
|
|
4787
|
+
let seq = this._todoSeq.get(key) || 0;
|
|
4788
|
+
for (const it of items || []) {
|
|
4789
|
+
const t = String((it && it.title) || '').trim();
|
|
4790
|
+
if (!t) continue;
|
|
4791
|
+
if (!map.has(t)) map.set(t, 'T' + (++seq));
|
|
4792
|
+
it.id = map.get(t);
|
|
4793
|
+
}
|
|
4794
|
+
this._todoIds.set(key, map);
|
|
4795
|
+
this._todoSeq.set(key, seq);
|
|
4796
|
+
return items;
|
|
4797
|
+
};
|
|
4798
|
+
|
|
4799
|
+
Engine.prototype._journalBefore = function (sid, name, args) {
|
|
4800
|
+
try {
|
|
4801
|
+
if (name !== 'write_file' && name !== 'edit_file') return;
|
|
4802
|
+
const rel = String((args && args.path) || '').trim();
|
|
4803
|
+
if (!rel) return;
|
|
4804
|
+
const ws = this._sessionWorkspace(String(sid || ''));
|
|
4805
|
+
const abs = path.isAbsolute(rel) ? path.resolve(rel) : path.resolve(ws, rel);
|
|
4806
|
+
let before = null;
|
|
4807
|
+
try { before = fs.readFileSync(abs, 'utf8'); } catch {}
|
|
4808
|
+
const todos = this.todos.get(String(sid || '')) || [];
|
|
4809
|
+
const cur = todos.find((t) => t && t.status !== 'done') || null;
|
|
4810
|
+
const key = String(sid || '');
|
|
4811
|
+
this._undoJournal = this._undoJournal || new Map();
|
|
4812
|
+
const j = this._undoJournal.get(key) || [];
|
|
4813
|
+
j.push({
|
|
4814
|
+
todoId: (cur && cur.id) || 'T0',
|
|
4815
|
+
title: (cur && cur.title) || '(görev listesi dışı)',
|
|
4816
|
+
path: abs,
|
|
4817
|
+
before,
|
|
4818
|
+
at: nowIso(),
|
|
4819
|
+
});
|
|
4820
|
+
while (j.length > 120) j.shift();
|
|
4821
|
+
this._undoJournal.set(key, j);
|
|
4822
|
+
} catch {}
|
|
4823
|
+
};
|
|
4824
|
+
|
|
4825
|
+
Engine.prototype.todoUndoInfo = function (sid) {
|
|
4826
|
+
const key = String(sid || '');
|
|
4827
|
+
this._undoJournal = this._undoJournal || new Map();
|
|
4828
|
+
const todos = this.todos.get(key) || [];
|
|
4829
|
+
const j = this._undoJournal.get(key) || [];
|
|
4830
|
+
const counts = new Map();
|
|
4831
|
+
for (const e of j) counts.set(e.todoId, (counts.get(e.todoId) || 0) + 1);
|
|
4832
|
+
return {
|
|
4833
|
+
ok: true,
|
|
4834
|
+
sessionId: key,
|
|
4835
|
+
todos,
|
|
4836
|
+
undo: todos.map((t) => ({ id: t.id, files: counts.get(t.id) || 0 })),
|
|
4837
|
+
lastTodoId: j.length ? j[j.length - 1].todoId : null,
|
|
4838
|
+
busy: this.ctrls.has(key),
|
|
4839
|
+
};
|
|
4840
|
+
};
|
|
4841
|
+
|
|
4842
|
+
Engine.prototype.undoTodo = function (sid, todoId) {
|
|
4843
|
+
const key = String(sid || '');
|
|
4844
|
+
const id = String(todoId || '');
|
|
4845
|
+
if (this.ctrls.has(key)) {
|
|
4846
|
+
return { ok: false, error: 'ajan şu an çalışıyor — önce ■ ile durdur, sonra geri al' };
|
|
4847
|
+
}
|
|
4848
|
+
this._undoJournal = this._undoJournal || new Map();
|
|
4849
|
+
const j = this._undoJournal.get(key) || [];
|
|
4850
|
+
if (!j.some((e) => e.todoId === id)) {
|
|
4851
|
+
return { ok: false, error: 'bu görev için kayıtlı değişiklik yok' };
|
|
4852
|
+
}
|
|
4853
|
+
const done = [];
|
|
4854
|
+
/* sondan başa geri sar — en son yazım önce restore edilir */
|
|
4855
|
+
for (let i = j.length - 1; i >= 0; i--) {
|
|
4856
|
+
const e = j[i];
|
|
4857
|
+
if (e.todoId !== id) continue;
|
|
4858
|
+
try {
|
|
4859
|
+
if (e.before === null) {
|
|
4860
|
+
fs.rmSync(e.path, { force: true }); /* madde bu dosyayı YARATMIŞ → sil */
|
|
4861
|
+
} else {
|
|
4862
|
+
fs.mkdirSync(path.dirname(e.path), { recursive: true });
|
|
4863
|
+
fs.writeFileSync(e.path, e.before);
|
|
4864
|
+
}
|
|
4865
|
+
done.push(e.path);
|
|
4866
|
+
j.splice(i, 1);
|
|
4867
|
+
} catch {}
|
|
4868
|
+
}
|
|
4869
|
+
/* madde listede 'pending' kalır — ajan yeniden ele alabilir */
|
|
4870
|
+
const todos = this.todos.get(key) || [];
|
|
4871
|
+
for (const t of todos) {
|
|
4872
|
+
if (t && t.id === id && t.status === 'done') t.status = 'pending';
|
|
4873
|
+
}
|
|
4874
|
+
this.todos.set(key, todos);
|
|
4875
|
+
try {
|
|
4876
|
+
fs.appendFileSync(this._file(key), JSON.stringify({ t: 'todo', items: todos }) + '\n');
|
|
4877
|
+
} catch {}
|
|
4878
|
+
emitSafe(this, key, { type: 'todos', sessionId: key, todos });
|
|
4879
|
+
return {
|
|
4880
|
+
ok: true,
|
|
4881
|
+
todoId: id,
|
|
4882
|
+
reverted: done.length,
|
|
4883
|
+
paths: [...new Set(done)],
|
|
4884
|
+
note: done.length
|
|
4885
|
+
? 'görev geri alındı — dosyalar önceki kod tabanına döndü'
|
|
4886
|
+
: 'geri alınacak dosya yok',
|
|
4887
|
+
};
|
|
4888
|
+
};
|
|
4889
|
+
|
|
4890
|
+
Engine.prototype.undoLastTodo = function (sid) {
|
|
4891
|
+
this._undoJournal = this._undoJournal || new Map();
|
|
4892
|
+
const j = this._undoJournal.get(String(sid || '')) || [];
|
|
4893
|
+
if (!j.length) return { ok: false, error: 'geri alınacak değişiklik yok' };
|
|
4894
|
+
return this.undoTodo(sid, j[j.length - 1].todoId);
|
|
4895
|
+
};
|
|
4896
|
+
|
|
4673
4897
|
module.exports = Engine;
|
|
4674
4898
|
module.exports.Engine = Engine;
|
|
4675
4899
|
module.exports.sanitizeTodoItems = sanitizeTodoItems;
|
package/src/agent/llm.js
CHANGED
|
@@ -235,53 +235,76 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReas
|
|
|
235
235
|
const reader = res.body.getReader();
|
|
236
236
|
const decoder = new TextDecoder();
|
|
237
237
|
let buf = '';
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
let idx;
|
|
245
|
-
while ((idx = buf.indexOf('\n')) !== -1) {
|
|
246
|
-
const line = buf.slice(0, idx).trim();
|
|
247
|
-
buf = buf.slice(idx + 1);
|
|
248
|
-
if (!line.startsWith('data:')) continue;
|
|
249
|
-
const data = line.slice(5).trim();
|
|
250
|
-
if (!data || data === '[DONE]') continue;
|
|
251
|
-
|
|
252
|
-
let json;
|
|
238
|
+
/* SERT İPTAL: abort gelirse gövdeyi fiziksel kapat (reader.cancel) —
|
|
239
|
+
sağlayıcı/fetch abort'u gövdeye taşımasa bile okuma döngüsü ANINDA
|
|
240
|
+
biter, token akışı kesilir */
|
|
241
|
+
let onAbort = null;
|
|
242
|
+
if (signal) {
|
|
243
|
+
onAbort = () => {
|
|
253
244
|
try {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
245
|
+
const p = reader.cancel();
|
|
246
|
+
if (p && typeof p.catch === 'function') p.catch(() => {});
|
|
247
|
+
} catch {}
|
|
248
|
+
};
|
|
249
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
while (true) {
|
|
253
|
+
const { done, value } = await reader.read();
|
|
254
|
+
if (signal && signal.aborted) {
|
|
255
|
+
const e = new Error('iptal');
|
|
256
|
+
e.name = 'AbortError';
|
|
257
|
+
throw e;
|
|
257
258
|
}
|
|
259
|
+
if (done) break;
|
|
260
|
+
buf += decoder.decode(value, { stream: true });
|
|
258
261
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
262
|
+
let idx;
|
|
263
|
+
while ((idx = buf.indexOf('\n')) !== -1) {
|
|
264
|
+
const line = buf.slice(0, idx).trim();
|
|
265
|
+
buf = buf.slice(idx + 1);
|
|
266
|
+
if (!line.startsWith('data:')) continue;
|
|
267
|
+
const data = line.slice(5).trim();
|
|
268
|
+
if (!data || data === '[DONE]') continue;
|
|
269
|
+
|
|
270
|
+
let json;
|
|
271
|
+
try {
|
|
272
|
+
json = JSON.parse(data);
|
|
273
|
+
} catch {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (json.usage) state.usage = json.usage;
|
|
278
|
+
const ch = json.choices && json.choices[0];
|
|
279
|
+
if (!ch) continue;
|
|
280
|
+
if (ch.finish_reason) state.finishReason = ch.finish_reason;
|
|
281
|
+
const d = ch.delta || {};
|
|
282
|
+
if (d.content) {
|
|
283
|
+
state.content += d.content;
|
|
284
|
+
onDelta && onDelta(d.content, state.content);
|
|
274
285
|
}
|
|
275
|
-
if (
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
286
|
+
if (d.reasoning_content) state.reasoning += d.reasoning_content;
|
|
287
|
+
if (d.reasoning) state.reasoning += d.reasoning;
|
|
288
|
+
for (const tc of d.tool_calls || []) {
|
|
289
|
+
const i = typeof tc.index === 'number' ? tc.index : state.toolCalls.length;
|
|
290
|
+
while (state.toolCalls.length <= i) {
|
|
291
|
+
state.toolCalls.push({ id: '', type: 'function', function: { name: '', arguments: '' } });
|
|
292
|
+
}
|
|
293
|
+
if (tc.id) state.toolCalls[i].id = tc.id;
|
|
294
|
+
if (tc.function) {
|
|
295
|
+
if (tc.function.name) state.toolCalls[i].function.name += tc.function.name;
|
|
296
|
+
if (tc.function.arguments) state.toolCalls[i].function.arguments += tc.function.arguments;
|
|
297
|
+
}
|
|
279
298
|
}
|
|
280
299
|
}
|
|
281
300
|
}
|
|
282
|
-
}
|
|
283
301
|
|
|
284
|
-
|
|
302
|
+
return { ...state };
|
|
303
|
+
} finally {
|
|
304
|
+
if (signal && onAbort) {
|
|
305
|
+
try { signal.removeEventListener('abort', onAbort); } catch {}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
285
308
|
} catch (e) {
|
|
286
309
|
/* okuma sırasında bağlantı koptu */
|
|
287
310
|
const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));
|