beast-agent 0.29.0 → 1.0.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 +6 -3
- package/src/agent/engine.js +554 -33
- package/src/agent/llm.js +52 -3
- package/src/agent/obscura.js +26 -10
- package/src/agent/skills.js +87 -29
- package/src/agent/tools.js +519 -7
- package/src/main.js +48 -7
- package/src/preload.js +2 -0
- package/src/renderer/i18n.js +11 -7
- package/src/renderer/index.html +5 -2
- package/src/renderer/renderer.js +246 -8
- package/src/renderer/style.css +60 -4
- package/.env.example +0 -8
- package/config.example.yaml +0 -39
- package/eng.traineddata +0 -0
- package/scripts/fix-electron.js +0 -126
- package/scripts/release.js +0 -121
- package/scripts/swap-electron.js +0 -40
- package/store/skills.json +0 -5
- package/tests/approval.test.js +0 -56
- package/tests/bg-jobs.test.js +0 -379
- package/tests/bots-name.test.js +0 -42
- package/tests/bus.test.js +0 -46
- package/tests/computeruse-context.test.js +0 -69
- package/tests/cron.test.js +0 -95
- package/tests/engine.test.js +0 -203
- package/tests/eval.test.js +0 -91
- package/tests/fallout.test.js +0 -188
- package/tests/llm.test.js +0 -205
- package/tests/memory.test.js +0 -33
- package/tests/memoryloop.test.js +0 -26
- package/tests/notegen.test.js +0 -63
- package/tests/obscura.test.js +0 -124
- package/tests/owner.test.js +0 -32
- package/tests/python.test.js +0 -76
- package/tests/research.test.js +0 -116
- package/tests/scenarios.json +0 -81
- package/tests/sessioncode.test.js +0 -50
- package/tests/setup.js +0 -9
- package/tests/tokens.test.js +0 -42
- package/tests/tools.test.js +0 -84
- package/tests/usage.test.js +0 -44
- package/tests/v11.test.js +0 -76
- package/tests/watchers.test.js +0 -206
- package/tests/watext.test.js +0 -53
- package/tests/whatsapp.test.js +0 -103
- package/tests/wherewasi.test.js +0 -40
package/src/agent/engine.js
CHANGED
|
@@ -13,6 +13,7 @@ const research = require('./research');
|
|
|
13
13
|
const memory = require('./memory');
|
|
14
14
|
const skills = require('./skills');
|
|
15
15
|
const { estTokens, estMsgTokens } = require('./tokens');
|
|
16
|
+
const log = require('./logger');
|
|
16
17
|
|
|
17
18
|
const MAX_TURNS = 25;
|
|
18
19
|
const SUB_MAX_TURNS = 8;
|
|
@@ -62,6 +63,38 @@ const THINK_LEVELS = [
|
|
|
62
63
|
{ v: 5, label: 'Max', effort: 'max' },
|
|
63
64
|
];
|
|
64
65
|
|
|
66
|
+
/* ---------- opencode motoru port: bağlam bütçesi + compaction + prune ----------
|
|
67
|
+
Kaynak: opencode-dev/packages/opencode/src/session/{overflow,compaction}.ts
|
|
68
|
+
Sabitler birebir alınmıştır (COMPACTION_BUFFER 20k, PRUNE_PROTECT 40k,
|
|
69
|
+
PRUNE_MINIMUM 20k, DOOM_LOOP 3, özet çıktı kırpma 2000 karakter). */
|
|
70
|
+
const COMPACTION_BUFFER = 20000; // overflow.ts:8 — özet turu için ayrılan pay
|
|
71
|
+
const OUTPUT_TOKEN_MAX = 32000; // transform.ts:18 — çıktı tavanı (rezerv hesabı)
|
|
72
|
+
const PRUNE_PROTECT = 40000; // compaction.ts:29 — en yeni araç çıktısı koruması
|
|
73
|
+
const PRUNE_MINIMUM = 20000; // compaction.ts:28 — bu kadar kazanç yoksa dokunma
|
|
74
|
+
const SUMMARY_OUT_KEEP = 2000; // compaction.ts:30 — özetteki araç çıktısı kırpma
|
|
75
|
+
const MIN_PRESERVE_RECENT = 2000; // compaction.ts:32 — korunan kuyruk alt sınırı
|
|
76
|
+
const MAX_PRESERVE_RECENT = 15000; // compaction.ts:33 — korunan kuyruk üst sınırı
|
|
77
|
+
const DOOM_LOOP_THRESHOLD = 3; // processor.ts:29 — aynı araç+argüman sınırı
|
|
78
|
+
const HISTORY_BUDGET_MAX = 80000; // dinamik bütçe tavanı (cache'siz sağlayıcı koruması)
|
|
79
|
+
|
|
80
|
+
/* Bilinen model ailelerinin bağlam penceresi (models.dev değerleri).
|
|
81
|
+
Eşleşmezse 128k varsayılır — asla gerçek limitin üstünü tahmin etme. */
|
|
82
|
+
const MODEL_CONTEXT_TABLE = [
|
|
83
|
+
[/gemini-[23]/i, 1000000],
|
|
84
|
+
[/gpt-5|gpt-4\.1|gpt-4o|o[134]-/i, 400000],
|
|
85
|
+
[/glm-4\.[67]/i, 200000],
|
|
86
|
+
[/claude/i, 200000],
|
|
87
|
+
[/grok-[34]/i, 256000],
|
|
88
|
+
[/kimi/i, 256000],
|
|
89
|
+
[/qwen.*coder/i, 256000],
|
|
90
|
+
[/deepseek|qwen|glm|llama|mistral|minimax/i, 131072],
|
|
91
|
+
];
|
|
92
|
+
function modelContextOf(sel) {
|
|
93
|
+
const s = String((sel && sel.model) || '');
|
|
94
|
+
for (const [re, ctx] of MODEL_CONTEXT_TABLE) if (re.test(s)) return ctx;
|
|
95
|
+
return 131072;
|
|
96
|
+
}
|
|
97
|
+
|
|
65
98
|
/* Çıktı biçimi kuralı: # ve * karakteri yasak (tüm ajanlar için ortak metin) */
|
|
66
99
|
const FORMAT_RULES =
|
|
67
100
|
'ÇIKTI BİÇİMİ (ZORUNLU):\n' +
|
|
@@ -82,12 +115,21 @@ const PERM_TOOL_SETS = {
|
|
|
82
115
|
read: new Set([
|
|
83
116
|
'web_search', 'http_fetch', 'deep_search',
|
|
84
117
|
'browser_open', 'browser_read', 'browser_snapshot',
|
|
85
|
-
'list_dir', 'read_file',
|
|
118
|
+
'list_dir', 'read_file', 'grep', 'glob',
|
|
86
119
|
]),
|
|
87
120
|
chat: new Set([]), // sadece sohbet
|
|
88
121
|
};
|
|
89
122
|
const PERM_LEVELS = ['all', 'web', 'read', 'chat'];
|
|
90
123
|
|
|
124
|
+
/* opencode plan agent portu: PLAN modu PROMPT düzeyinde değil GERÇEKten
|
|
125
|
+
salt-okurdur — yazma/çalıştırma araçları setten düşer, sadece plan çıkar */
|
|
126
|
+
const PLAN_ALLOW_TOOLS = new Set([
|
|
127
|
+
'read_file', 'list_dir', 'grep', 'glob',
|
|
128
|
+
'web_search', 'http_fetch', 'deep_search',
|
|
129
|
+
'browser_open', 'browser_read', 'browser_snapshot',
|
|
130
|
+
'todo_write', 'memory_search', 'kb_search', 'ocr_read',
|
|
131
|
+
]);
|
|
132
|
+
|
|
91
133
|
/* İzin değerini normalize eder: 'all' → ['all'], 'web' → ['web'],
|
|
92
134
|
'web,read' / ['web','read'] → ['web','read'] (sıra PERM_LEVELS'e göre dizilir).
|
|
93
135
|
Geçersiz değerler atılır; hiçbiri kalmazsa boş dizi döner. */
|
|
@@ -102,7 +144,7 @@ function normalizePerms(p) {
|
|
|
102
144
|
Bunların hepsi run_background ile paralel ajana devredilir — CEO sadece
|
|
103
145
|
konuşur, planlar, emir verir ve takip eder. */
|
|
104
146
|
const CEO_EXEC_TOOLS = new Set([
|
|
105
|
-
'run_command', 'read_file', 'write_file', 'list_dir',
|
|
147
|
+
'run_command', 'read_file', 'write_file', 'edit_file', 'list_dir',
|
|
106
148
|
'python_run',
|
|
107
149
|
'web_search', 'http_fetch', 'deep_search',
|
|
108
150
|
'browser_open', 'browser_read', 'browser_screenshot', 'browser_snapshot',
|
|
@@ -258,6 +300,8 @@ class Engine {
|
|
|
258
300
|
this._bgLimit = Math.max(1, Number(opts.bgLimit || process.env.BEAST_BG_LIMIT) || BG_MAX_CONCURRENT_DEFAULT);
|
|
259
301
|
this._bgPendingStart = new Map(); // sid -> ilk görev metni (slot açılınca gönderilir)
|
|
260
302
|
this._bgGroups = new Map(); // groupId -> {parentId,total,results,dead}
|
|
303
|
+
/* silinen oturum işaretleri: rapor/zincir buraya gönderilmez (hayalet oturum yok) */
|
|
304
|
+
this._deletedSessions = new Set();
|
|
261
305
|
/* #17 kalıcı ajan geçmişi: restart sonrası işler + sohbetler kaybolmasın */
|
|
262
306
|
this._bgJobsFile = path.join(this.sessionsDir, 'bg-jobs.json');
|
|
263
307
|
this._loadBgJobsPersisted();
|
|
@@ -626,7 +670,14 @@ class Engine {
|
|
|
626
670
|
}
|
|
627
671
|
const res = await chatStreamAuto(
|
|
628
672
|
c,
|
|
629
|
-
{
|
|
673
|
+
{
|
|
674
|
+
messages: msgs,
|
|
675
|
+
tools: activeTools,
|
|
676
|
+
reasoningEffort: this._thinkEffortFor(session),
|
|
677
|
+
/* opencode cache disiplini (transform.ts:1270): oturum-sabit
|
|
678
|
+
önbellek anahtarı — destekleyen sağlayıcıda önek cache'i tutar */
|
|
679
|
+
cacheKey: String(session.id || ''),
|
|
680
|
+
},
|
|
630
681
|
{
|
|
631
682
|
signal,
|
|
632
683
|
onDelta,
|
|
@@ -926,6 +977,10 @@ class Engine {
|
|
|
926
977
|
} else if (rec.t === 'notes') {
|
|
927
978
|
session.notes = String(rec.text || '');
|
|
928
979
|
session.notesAt = Number(rec.at) || 0;
|
|
980
|
+
} else if (rec.t === 'summary') {
|
|
981
|
+
/* opencode compaction: konuşma özeti — birebir geçmişin yerine geçmez,
|
|
982
|
+
payload'da system'den sonra sabit user mesajı olarak girer */
|
|
983
|
+
if (rec.text) session.summary = String(rec.text);
|
|
929
984
|
} else if (rec.t === 'botdm') {
|
|
930
985
|
/* botlar arası DM oturumu — admin izleyebilir, sidebar'da görünmez */
|
|
931
986
|
session.isBotDm = true;
|
|
@@ -1126,6 +1181,7 @@ class Engine {
|
|
|
1126
1181
|
s.messages = [];
|
|
1127
1182
|
delete s.notes;
|
|
1128
1183
|
delete s.notesAt;
|
|
1184
|
+
delete s.summary; // özet de sıfırlanır — taze başlangıç
|
|
1129
1185
|
this.todos.set(sid, []);
|
|
1130
1186
|
this.emit({ type: 'sessions' });
|
|
1131
1187
|
return true;
|
|
@@ -1173,13 +1229,14 @@ class Engine {
|
|
|
1173
1229
|
}
|
|
1174
1230
|
|
|
1175
1231
|
deleteSession(id) {
|
|
1232
|
+
const sid = String(id || '');
|
|
1176
1233
|
try {
|
|
1177
|
-
const c = this.ctrls.get(
|
|
1234
|
+
const c = this.ctrls.get(sid);
|
|
1178
1235
|
if (c) {
|
|
1179
1236
|
this._abortReasons = this._abortReasons || new Map();
|
|
1180
|
-
this._abortReasons.set(
|
|
1237
|
+
this._abortReasons.set(sid, 'oturum silindiği için tur iptal edildi');
|
|
1181
1238
|
c.abort();
|
|
1182
|
-
this.ctrls.delete(
|
|
1239
|
+
this.ctrls.delete(sid);
|
|
1183
1240
|
}
|
|
1184
1241
|
/* #17 silinecek ajanın bekleyen öz-kurtarma zamanlayıcısını iptal et —
|
|
1185
1242
|
yoksa send() oturum dosyasını yeniden yaratıp işi diriltir */
|
|
@@ -1206,30 +1263,82 @@ class Engine {
|
|
|
1206
1263
|
this._saveBgJobs();
|
|
1207
1264
|
this._bgEmit();
|
|
1208
1265
|
}
|
|
1266
|
+
/* silinme işareti: bekleyen raporlar/rapor zinciri bu oturuma ASLA düşmez */
|
|
1267
|
+
if (this._deletedSessions) {
|
|
1268
|
+
this._deletedSessions.add(sid);
|
|
1269
|
+
if (this._deletedSessions.size > 300) {
|
|
1270
|
+
const first = this._deletedSessions.values().next().value;
|
|
1271
|
+
if (first !== undefined) this._deletedSessions.delete(first);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1209
1274
|
this.emit({ type: 'sessions' });
|
|
1210
1275
|
return true;
|
|
1211
1276
|
}
|
|
1212
1277
|
|
|
1213
1278
|
/* ---------- prompt assembly (the frugal part) ---------- */
|
|
1214
1279
|
|
|
1280
|
+
/* opencode instruction.ts port: workspace'teki proje talimatları (AGENTS.md,
|
|
1281
|
+
CLAUDE.md, CONTEXT.md) Beast Code system promptuna girer. Oturum başına
|
|
1282
|
+
BİR KEZ okunup session'da saklanır — system promptu epoch boyunca sabit
|
|
1283
|
+
kalır, önek cache bozulmaz (dosya sonradan değişse bile). */
|
|
1284
|
+
_projectInstructions(session) {
|
|
1285
|
+
if (!session) return '';
|
|
1286
|
+
if (session._projCtx !== undefined) return session._projCtx;
|
|
1287
|
+
let out = '';
|
|
1288
|
+
try {
|
|
1289
|
+
const ws = (session && session.workspace) || this.workspace;
|
|
1290
|
+
for (const name of ['AGENTS.md', 'CLAUDE.md', 'CONTEXT.md']) {
|
|
1291
|
+
let txt = '';
|
|
1292
|
+
try { txt = fs.readFileSync(path.join(ws, name), 'utf8'); } catch {}
|
|
1293
|
+
txt = String(txt || '').trim();
|
|
1294
|
+
if (!txt) continue;
|
|
1295
|
+
out += (out ? '\n\n' : '') + `--- ${name} ---\n` + txt.slice(0, 6000);
|
|
1296
|
+
if (out.length >= 8000) break;
|
|
1297
|
+
}
|
|
1298
|
+
} catch {}
|
|
1299
|
+
out = out.slice(0, 9000);
|
|
1300
|
+
session._projCtx = out;
|
|
1301
|
+
return out;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1215
1304
|
/* Beast Code (IDE paneli): VS Code hızında çalışsın diye SECMET prompt.
|
|
1305
|
+
OpenCode kodlama disiplini gömülü: bağlam → plan → küçük diff → doğrula.
|
|
1216
1306
|
Hafıza embedding araması, skills taraması, kişilik/kural blokları YOK —
|
|
1217
1307
|
prompt kısa kalır → ilk token hızlı, tur maliyeti düşük. */
|
|
1218
1308
|
buildBcSystem(session) {
|
|
1219
1309
|
const nowD = new Date();
|
|
1220
1310
|
const localDate = nowD.toLocaleDateString('tr-TR');
|
|
1221
|
-
|
|
1311
|
+
/* önek-cache disiplini (opencode Context Epoch): dakika yerine saat —
|
|
1312
|
+
system prompt her dakika değişse sağlayıcı önbelleği sürekli bozulur */
|
|
1313
|
+
const localTime = String(nowD.getHours()).padStart(2, '0') + ':00';
|
|
1314
|
+
const mode = String((session && session.bcMode) || 'auto').toLowerCase();
|
|
1315
|
+
const modeBlock =
|
|
1316
|
+
mode === 'plan'
|
|
1317
|
+
? 'ÇALIŞMA MODU: PLAN 🔍 — dosyaları oku/incele (read_file, list_dir), KOD YAZMA; mevcut yapıyı özetle ve adım adım UYGULAMA PLANI ver (hangi dosya, ne değişecek, nasıl doğrulanır). Kullanıcı /build deyince plan uygulanır.'
|
|
1318
|
+
: mode === 'build'
|
|
1319
|
+
? 'ÇALIŞMA MODU: BUILD 🛠 — son planı SOHBETTEKİ bağlamdan al ve UYGULA: dosyaları düzenle, komutları çalıştır, doğrula. Yeni plan açma; en fazla 1 cümlelik yön gösterimi + uygulama.'
|
|
1320
|
+
: 'ÇALIŞMA MODU: OTOMATİK ⚡ — önce 2-4 satırlık mini plan (todo_write), sonra hemen uygula + doğrula.';
|
|
1321
|
+
const proj = this._projectInstructions(session);
|
|
1222
1322
|
return (
|
|
1223
|
-
'Sen BEAST CODE\u2019sun — IDE panelinde çalışan hızlı bir kodlama ajanı. VS Code gibi çevik ol.\n' +
|
|
1323
|
+
'Sen BEAST CODE\u2019sun — IDE panelinde çalışan, OpenCode disiplinli hızlı bir kodlama ajanı. VS Code gibi çevik ol.\n' +
|
|
1224
1324
|
`Çalışma klasörü: ${(session && session.workspace) || this.workspace}\n` +
|
|
1225
1325
|
`Yerel zaman: ${localDate} ${localTime}\n` +
|
|
1326
|
+
`${modeBlock}\n` +
|
|
1327
|
+
(proj
|
|
1328
|
+
? '# PROJE TALİMATLARI (workspace AGENTS/CLAUDE/CONTEXT dosyalarından — daima uy)\n' + proj + '\n'
|
|
1329
|
+
: '') +
|
|
1330
|
+
'WORKFLOW (her işte bu sıra):\n' +
|
|
1331
|
+
'1) BAĞLAM: değiştirmeden önce ilgili dosyaları OKU — read_file satır numaralı döner (N: içerik), offset/limit ile büyük dosyayı pencerele; içerik araması için grep (regex), dosya adı araması için glob kullan; varsayım yapma, mevcut stili/konvansiyonu takip et.\n' +
|
|
1332
|
+
'2) PLAN: 2+ adımlı işlerde İLK EYLEM todo_write olsun (2-6 madde); her adım bitince status:"done" ile GÜNCELLE — liste bitene kadar iş bitmiş sayılmaz.\n' +
|
|
1333
|
+
'3) EDİT: VAR OLAN dosyada önce edit_file kullan (old_string/new_string ile yalnız ilgili bölümü değiştir; birden çok eşleşme varsa bağlam ekle ya da replace_all); write_file yalnız YENİ dosya ya da tam yeniden yazım için. İlgisiz yeniden biçimleme/kayıt boşluk değişikliği YAPMA.\n' +
|
|
1334
|
+
'4) DOĞRULA: edit sonrası mümkünse derle/test et/lint çalıştır (run_command); hata varsa DÜZELT ve TEKRAR dene (en fazla 2 doğrulama turu) — kırmızı bırakma.\n' +
|
|
1335
|
+
'5) RAPOR: 1-3 satır — ne değişti + doğrulama sonucu (ör. "npm test ✓ 154/154"). Uzun açıklama yok.\n' +
|
|
1226
1336
|
'HIZ KURALLARI:\n' +
|
|
1227
|
-
'-
|
|
1228
|
-
'-
|
|
1229
|
-
'- Kurulum, test, build, git gibi işler için run_command kullan (Windows PowerShell ortamı).\n' +
|
|
1230
|
-
'- Bağımsız araç çağrılarını AYNI TURDA paralel ver.\n' +
|
|
1337
|
+
'- Kodu ve dosyaları DOĞRUDAN write_file ile yaz; basit dosya oluşturma/düzenleme için Python scripti yazma; python_run yalnız gerçek hesap/veri işleme gerekiyorsa.\n' +
|
|
1338
|
+
'- Bağımsız araç çağrılarını AYNI TURDA paralel ver (birden çok read_file tek turda).\n' +
|
|
1231
1339
|
'- Dosyayı kullanıcı söylememişse list_dir ile yapıyı görüp kendin karar ver.\n' +
|
|
1232
|
-
'-
|
|
1340
|
+
'- Run_command PowerShell ortamında çalışır (Windows): KALICI oturum — cd ve $env değişkenleri çağrılar arasında korunur; büyük çıktıda tam çıktı geçici dosyaya düşer, read_file ile oku. shell:"bash" verirsen git-bash ile bash sözdizimi koşar.\n' +
|
|
1341
|
+
'- Soru sorma, sohbet etme; uygulayıp özetle. Kullanıcı /plan /build /auto ile modu değiştirir.\n' +
|
|
1233
1342
|
FORMAT_RULES
|
|
1234
1343
|
);
|
|
1235
1344
|
}
|
|
@@ -1277,7 +1386,9 @@ class Engine {
|
|
|
1277
1386
|
/* Beast Code (bcCode) oturumları buildBcSystem kullanır — buraya düşmez */
|
|
1278
1387
|
const nowD = new Date();
|
|
1279
1388
|
const localDate = nowD.toLocaleDateString('tr-TR');
|
|
1280
|
-
|
|
1389
|
+
/* önek-cache disiplini (opencode Context Epoch): dakika yerine saat —
|
|
1390
|
+
system prompt her dakika değişse sağlayıcı önbelleği sürekli bozulur */
|
|
1391
|
+
const localTime = String(nowD.getHours()).padStart(2, '0') + ':00';
|
|
1281
1392
|
let tz = '';
|
|
1282
1393
|
try {
|
|
1283
1394
|
tz = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
|
|
@@ -1303,6 +1414,7 @@ class Engine {
|
|
|
1303
1414
|
'# ARAÇ KURALLARI\n' +
|
|
1304
1415
|
[
|
|
1305
1416
|
'Basit soruları araç kullanmadan doğrudan cevapla — hız önceliklidir.',
|
|
1417
|
+
'Kod/dosya işlerinde: içerik araması grep (regex), dosya adı araması glob, VAR OLAN dosyayı değiştirme edit_file (write_file yalnız yeni dosya/tam yeniden yazım). read_file satır numaralı döner; offset/limit ile pencerele.',
|
|
1306
1418
|
'Kullanıcı bir tarihte/saatte hatırlatılmasını isterse set_reminder kullan; when değerini ORTAMdaki bugüne göre hesapla (yerel saat). "Her sabah/gün/hafta" gibi tekrarlı isteklerde repeat alanını da ver (daily/weekly/monthly/weekdays veya cron).',
|
|
1307
1419
|
'Kullanıcı kalıcı bir arka plan takibi isterse (fiyat eşiği, pil seviyesi, sayfa değişikliği) watcher_add ile izleyici kur; kurduktan sonra watcher_list ile doğrula ve kullanıcıya koşulu + kontrol sıklığını kısaca bildir.',
|
|
1308
1420
|
'Anlık olay takipleri için (yeni mail, fiyat eşiği, dosya değişimi, webhook) event_subscribe kullan — cron/polling gerekmez; listeyi event_list ile göster, vazgeçirirse event_unsubscribe.',
|
|
@@ -1367,7 +1479,34 @@ class Engine {
|
|
|
1367
1479
|
return 0;
|
|
1368
1480
|
}
|
|
1369
1481
|
|
|
1370
|
-
|
|
1482
|
+
/* ---------- opencode port: bağlam matematiği (overflow.ts) ---------- */
|
|
1483
|
+
|
|
1484
|
+
/* Seçili model için özet payı düşülmüş kullanılabilir girdi alanı */
|
|
1485
|
+
_usableContext(sel) {
|
|
1486
|
+
const ctx = modelContextOf(sel);
|
|
1487
|
+
if (!ctx) return 0;
|
|
1488
|
+
/* overflow.ts:14-19: reserved = min(COMPACTION_BUFFER, maxOutput) */
|
|
1489
|
+
const reserved = Math.min(COMPACTION_BUFFER, OUTPUT_TOKEN_MAX);
|
|
1490
|
+
return Math.max(0, ctx - reserved);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/* Dinamik geçmiş bütçesi: model bağlamı genişse 18k sabitinin ÜSTÜNE çıkar
|
|
1494
|
+
(asla altına inmez); cache'siz sağlayıcılarda şişmeyi HISTORY_BUDGET_MAX sınırlar */
|
|
1495
|
+
_historyBudget(sel) {
|
|
1496
|
+
const usable = this._usableContext(sel);
|
|
1497
|
+
if (!usable) return this.historyTokenBudget;
|
|
1498
|
+
return Math.max(this.historyTokenBudget, Math.min(Math.floor(usable * 0.85), HISTORY_BUDGET_MAX));
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/* Geçmiş + sistem + araç şemaları için kaba toplam token tahmini */
|
|
1502
|
+
_contextEstimate(session, sel) {
|
|
1503
|
+
const msgs = session.messages.reduce((a, m) => a + estMsgTokens(m), 0);
|
|
1504
|
+
const toolsCost = TOOLS.length ? estTokens(JSON.stringify(TOOLS)) : 0;
|
|
1505
|
+
return Math.ceil((msgs + toolsCost + 3500) * this.tokRatio); // 3500 ≈ system payı
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/* assistant(tool_calls) + sonuç tool mesajlarını BÖLMEYEN birimler */
|
|
1509
|
+
_msgUnits(messages) {
|
|
1371
1510
|
const units = [];
|
|
1372
1511
|
let i = 0;
|
|
1373
1512
|
while (i < messages.length) {
|
|
@@ -1382,19 +1521,22 @@ class Engine {
|
|
|
1382
1521
|
i++;
|
|
1383
1522
|
}
|
|
1384
1523
|
}
|
|
1385
|
-
|
|
1524
|
+
return units;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
_buildPayload(system, messages, notes, budget, summary) {
|
|
1528
|
+
const units = this._msgUnits(messages);
|
|
1529
|
+
const B = Math.max(2000, Number(budget) || this.historyTokenBudget);
|
|
1386
1530
|
const maxUnits = notes ? NOTES_MAX_UNITS : units.length; // notlar varken pencereyi sıkılaştır
|
|
1387
1531
|
const picked = [];
|
|
1388
1532
|
let used = 0;
|
|
1389
1533
|
for (let u = units.length - 1; u >= 0; u--) {
|
|
1390
1534
|
if (picked.length >= maxUnits) break;
|
|
1391
1535
|
const cost = units[u].reduce(
|
|
1392
|
-
(a, m) =>
|
|
1393
|
-
a +
|
|
1394
|
-
clamp(Math.ceil(estMsgTokens(m) * this.tokRatio), 1, this.historyTokenBudget),
|
|
1536
|
+
(a, m) => a + clamp(Math.ceil(estMsgTokens(m) * this.tokRatio), 1, B),
|
|
1395
1537
|
0
|
|
1396
1538
|
);
|
|
1397
|
-
if (picked.length && used + cost >
|
|
1539
|
+
if (picked.length && used + cost > B) break;
|
|
1398
1540
|
picked.unshift(units[u]);
|
|
1399
1541
|
used += cost;
|
|
1400
1542
|
}
|
|
@@ -1416,7 +1558,20 @@ class Engine {
|
|
|
1416
1558
|
flat[k] = { ...m, content: String(m.content).slice(0, TOOL_OUT_KEEP) + '\n…[kırpıldı]' };
|
|
1417
1559
|
}
|
|
1418
1560
|
}
|
|
1419
|
-
|
|
1561
|
+
/* opencode compaction replay (message-v2.ts:521-572): özet, system'den
|
|
1562
|
+
HEMEN sonra sabit user mesajı olarak girer → önek cache'e uygun
|
|
1563
|
+
(compaction sonrası yalnız eklemeli büyür, system bir daha değişmez) */
|
|
1564
|
+
const head = [];
|
|
1565
|
+
if (summary) {
|
|
1566
|
+
head.push({
|
|
1567
|
+
role: 'user',
|
|
1568
|
+
content:
|
|
1569
|
+
'[ÖNCEKİ KONUŞMANIN ÖZETİ — birebir geçmişi buradan hatırla]\n' +
|
|
1570
|
+
summary +
|
|
1571
|
+
'\n[Konuşma aşağıdaki mesajlarla DEVAM EDİYOR]',
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
return [{ role: 'system', content: system }, ...head, ...flat];
|
|
1420
1575
|
}
|
|
1421
1576
|
|
|
1422
1577
|
_lastUserText(session) {
|
|
@@ -1479,6 +1634,7 @@ class Engine {
|
|
|
1479
1634
|
if (seen++ >= keepFrom) msgLines.push(line);
|
|
1480
1635
|
}
|
|
1481
1636
|
out.push(...msgLines);
|
|
1637
|
+
out.push(JSON.stringify({ t: 'summary', text: session.summary || '', at: 0 }));
|
|
1482
1638
|
out.push(JSON.stringify({ t: 'compacted', at: nowIso(), droppedBefore: keepFrom, noteBased: true }));
|
|
1483
1639
|
const tmp = file + '.tmp';
|
|
1484
1640
|
fs.writeFileSync(tmp, out.join('\n') + '\n');
|
|
@@ -1486,6 +1642,202 @@ class Engine {
|
|
|
1486
1642
|
} catch {}
|
|
1487
1643
|
}
|
|
1488
1644
|
|
|
1645
|
+
/* ---------- opencode port: gerçek compaction + prune (compaction.ts) ---------- */
|
|
1646
|
+
|
|
1647
|
+
/* Head bölgesini düz metne serileştirir (compaction.ts:54-85 port).
|
|
1648
|
+
Araç çıktıları SUMMARY_OUT_KEEP ile kırpılır — özet çağrısı küçük kalır. */
|
|
1649
|
+
_serializeHead(msgs) {
|
|
1650
|
+
const lines = [];
|
|
1651
|
+
for (const m of msgs) {
|
|
1652
|
+
if (m.role === 'user') {
|
|
1653
|
+
lines.push('[Kullanıcı]: ' + this._plainText(m).slice(0, 1500));
|
|
1654
|
+
} else if (m.role === 'assistant') {
|
|
1655
|
+
const txt = this._plainText(m).trim();
|
|
1656
|
+
if (txt) lines.push('[Asistan]: ' + txt.slice(0, 1200));
|
|
1657
|
+
for (const tc of m.tool_calls || []) {
|
|
1658
|
+
let args = '';
|
|
1659
|
+
try {
|
|
1660
|
+
args = JSON.stringify(JSON.parse((tc.function && tc.function.arguments) || '{}'));
|
|
1661
|
+
} catch {
|
|
1662
|
+
args = String((tc.function && tc.function.arguments) || '');
|
|
1663
|
+
}
|
|
1664
|
+
lines.push(`[Araç çağrısı]: ${tc.function && tc.function.name}(${args.slice(0, 800)})`);
|
|
1665
|
+
}
|
|
1666
|
+
} else if (m.role === 'tool') {
|
|
1667
|
+
const out = String(m.content || '');
|
|
1668
|
+
lines.push(
|
|
1669
|
+
'[Araç sonucu]: ' + (out.length <= SUMMARY_OUT_KEEP ? out : out.slice(0, SUMMARY_OUT_KEEP) + '\n[kırpıldı]')
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
return lines.filter(Boolean).join('\n');
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
/* compaction.ts:319-557 port. Bağlam taşarsa: kuyruk (son ~%25) birebir
|
|
1677
|
+
korunur, baş metne serileştirilip TEK küçük model çağrısıyla özetlenir,
|
|
1678
|
+
geçmiş [özet + kuyruk] olarak yeniden yazılır. Döndürür: compact oldu mu */
|
|
1679
|
+
async _compactHistory(session, sel, signal) {
|
|
1680
|
+
try {
|
|
1681
|
+
const usable = this._usableContext(sel) || this.historyTokenBudget * 2;
|
|
1682
|
+
const budget = clamp(Math.floor(usable * 0.25), MIN_PRESERVE_RECENT, MAX_PRESERVE_RECENT);
|
|
1683
|
+
const units = this._msgUnits(session.messages);
|
|
1684
|
+
if (units.length < 3) return false;
|
|
1685
|
+
let keepIdx = 0;
|
|
1686
|
+
let used = 0;
|
|
1687
|
+
for (let u = units.length - 1; u >= 0; u--) {
|
|
1688
|
+
const cost = units[u].reduce((a, m) => a + Math.ceil(estMsgTokens(m) * this.tokRatio), 0);
|
|
1689
|
+
if (used + cost > budget) break;
|
|
1690
|
+
keepIdx = u;
|
|
1691
|
+
used += cost;
|
|
1692
|
+
}
|
|
1693
|
+
if (keepIdx <= 0) return false; // özetlenecek head yok
|
|
1694
|
+
const headMsgs = units.slice(0, keepIdx).flat();
|
|
1695
|
+
const transcript = this._serializeHead(headMsgs);
|
|
1696
|
+
if (!transcript.trim()) return false;
|
|
1697
|
+
|
|
1698
|
+
/* core/session/compaction.ts:16-56 şablonu (Türkçe başlıklarla) */
|
|
1699
|
+
const prev = session.summary
|
|
1700
|
+
? `<önceki-özet>\n${session.summary}\n</önceki-özet>\n\n` +
|
|
1701
|
+
'önceki-özet, konuşmadan önceki her şeyi özetler: ikisini BİRLEŞTİREREK yeni özet çıkar. ' +
|
|
1702
|
+
'önceki-özet bu birleşimden sonra atılır — yeni özete taşımadığın her şey KAYBOLUR. ' +
|
|
1703
|
+
'Konuşma daha yenidir; çelişkide konuşma kazanır. Biten işleri "Aktif"ten "Tamamlanan"a taşı.\n\n'
|
|
1704
|
+
: '';
|
|
1705
|
+
const prompt =
|
|
1706
|
+
'Aşağıdaki <konuşma> etiketindeki sohbet geçmişinden, işe başka bir ajan devam edecek şekilde özet çıkar.\n' +
|
|
1707
|
+
'Tam olarak <şablon> içindeki Markdown yapısını koru; şablon etiketlerini yanıtına yazma.\n' +
|
|
1708
|
+
'<şablon>\n' +
|
|
1709
|
+
'## Amaç\n- [kullanıcının ne yapmaya çalıştığı, 1-2 cümle]\n\n' +
|
|
1710
|
+
'## Önemli Detaylar\n- [kısıtlar, kararlar ve gerekçeleri, önemli olgular, kesin yollar/sayılar; yoksa "(yok)"]\n\n' +
|
|
1711
|
+
'## İş Durumu\n### Tamamlanan\n- [biten işler, doğrulanan olgular; yoksa "(yok)"]\n\n' +
|
|
1712
|
+
'### Aktif\n- [sürüyen işler, yarım değişiklikler; yoksa "(yok)"]\n\n' +
|
|
1713
|
+
'### Engelli\n- [hatalar, bilinmeyenler; yoksa "(yok)"]\n\n' +
|
|
1714
|
+
'## Sonraki Adım\n1. [somut ilk eylem; yoksa "(yok)"]\n\n' +
|
|
1715
|
+
'## İlgili Dosyalar\n- [dosya/klasör yolu: neden önemli; yoksa "(yok)"]\n' +
|
|
1716
|
+
'</şablon>\n\n' +
|
|
1717
|
+
'Kurallar:\n- Boş olsa bile her bölümü koru.\n- Kısa madde maddeler yaz, paragraf kurma.\n' +
|
|
1718
|
+
'- Dosya yollarını, komutları, hata metinlerini, URL\u2019leri BİREBİR aktar.\n' +
|
|
1719
|
+
'- Özetleme yaptığını asla belirtme.\n\n' +
|
|
1720
|
+
prev +
|
|
1721
|
+
`<konuşma>\n${transcript}\n</konuşma>`;
|
|
1722
|
+
|
|
1723
|
+
emitSafe(this, session.id, {
|
|
1724
|
+
type: 'status',
|
|
1725
|
+
status: `⇩ bağlam doldu — opencode tarzı özetleme devrede (head ${headMsgs.length} mesaj, kuyruk ${session.messages.length - headMsgs.length} korunuyor)`,
|
|
1726
|
+
});
|
|
1727
|
+
const small = this.modelFor('subagent') || sel || this.sel;
|
|
1728
|
+
const res = await chatOnce(
|
|
1729
|
+
small,
|
|
1730
|
+
{ messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
|
|
1731
|
+
{ signal }
|
|
1732
|
+
);
|
|
1733
|
+
const text = String(res.content || '').replace(/```(?:markdown)?/gi, '').trim();
|
|
1734
|
+
if (!text) return false;
|
|
1735
|
+
session.summary = text.slice(0, 12000);
|
|
1736
|
+
const tail = units.slice(keepIdx).flat();
|
|
1737
|
+
session.messages = tail;
|
|
1738
|
+
session.notesAt = clamp(session.notesAt || 0, 0, tail.length);
|
|
1739
|
+
this._persistCompaction(session);
|
|
1740
|
+
emitSafe(this, session.id, {
|
|
1741
|
+
type: 'status',
|
|
1742
|
+
status: `✓ bağlam özeti yazıldı — geçmiş ${headMsgs.length + tail.length} → ${tail.length} mesaj, önek cache tazelendi`,
|
|
1743
|
+
});
|
|
1744
|
+
return true;
|
|
1745
|
+
} catch {
|
|
1746
|
+
return false; // compaction asla sohbeti bozmasın
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/* Compaction sonrası disk yeniden yazımı: meta/bot/todo/notes/summary +
|
|
1751
|
+
kuyruk mesajları. Atomik — patlarsa eski dosya sağlam kalır. */
|
|
1752
|
+
_persistCompaction(session) {
|
|
1753
|
+
try {
|
|
1754
|
+
const file = this._file(session.id);
|
|
1755
|
+
let meta = null, meta2 = null, bot = null, todo = null;
|
|
1756
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
1757
|
+
if (!line.trim()) continue;
|
|
1758
|
+
let r;
|
|
1759
|
+
try { r = JSON.parse(line); } catch { continue; }
|
|
1760
|
+
if (r.t === 'meta') meta = line;
|
|
1761
|
+
else if (r.t === 'meta2') meta2 = line;
|
|
1762
|
+
else if (r.t === 'bot') bot = line;
|
|
1763
|
+
else if (r.t === 'todo') todo = line; // append-only: son satır kazanır
|
|
1764
|
+
}
|
|
1765
|
+
const out = [];
|
|
1766
|
+
if (meta) out.push(meta);
|
|
1767
|
+
else out.push(JSON.stringify({ t: 'meta', id: session.id, code: session.code, createdAt: session.createdAt }));
|
|
1768
|
+
if (meta2) out.push(meta2);
|
|
1769
|
+
if (bot) out.push(bot);
|
|
1770
|
+
if (todo) out.push(todo);
|
|
1771
|
+
out.push(JSON.stringify({ t: 'notes', text: session.notes || '', at: session.notesAt || 0 }));
|
|
1772
|
+
out.push(JSON.stringify({ t: 'summary', text: session.summary || '', at: nowIso() }));
|
|
1773
|
+
for (const m of session.messages) out.push(JSON.stringify({ t: 'msg', ...m }));
|
|
1774
|
+
out.push(JSON.stringify({ t: 'compacted', at: nowIso(), summary: true }));
|
|
1775
|
+
const tmp = file + '.tmp';
|
|
1776
|
+
fs.writeFileSync(tmp, out.join('\n') + '\n');
|
|
1777
|
+
fs.renameSync(tmp, file);
|
|
1778
|
+
} catch {}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
/* Taşma kontrolü (overflow.ts:22-34): gerçek usage varsa onu esas al,
|
|
1782
|
+
yoksa tahmini kullan. usable'ın %95'i dolunca compaction tetiklenir. */
|
|
1783
|
+
_overContext(session, sel, lastUsageTotal) {
|
|
1784
|
+
const usable = this._usableContext(sel);
|
|
1785
|
+
if (!usable) return false;
|
|
1786
|
+
const total =
|
|
1787
|
+
lastUsageTotal ||
|
|
1788
|
+
session.messages.reduce((a, m) => a + estMsgTokens(m), 0) * this.tokRatio + 6000;
|
|
1789
|
+
return total >= usable * 0.95;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
/* compaction.ts:273-317 prune port: sondan geriye yürür, en yeni kullanıcı
|
|
1793
|
+
turunu korur; PRUNE_PROTECT (40k) aşan eski araç çıktılarını temizler.
|
|
1794
|
+
En az PRUNE_MINIMUM (20k) kazanç yoksa hiç dokunmaz. tool_call_id çiftleri
|
|
1795
|
+
korunur — sadece içerik temizlenir, önek cache bozulmaz. Döndürür: kazanılan token */
|
|
1796
|
+
_pruneSession(session) {
|
|
1797
|
+
try {
|
|
1798
|
+
if (this.ctrls.has(session.id)) return 0;
|
|
1799
|
+
const msgs = session.messages;
|
|
1800
|
+
let total = 0, pruned = 0, turns = 0;
|
|
1801
|
+
const targets = [];
|
|
1802
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
1803
|
+
const m = msgs[i];
|
|
1804
|
+
if (m.role === 'user') turns++;
|
|
1805
|
+
if (turns < 2) continue; // en yeni tur dokunulmaz
|
|
1806
|
+
if (m.role === 'tool' && typeof m.content === 'string' && m.content.length > 400) {
|
|
1807
|
+
const est = Math.ceil(estTokens(m.content) * this.tokRatio);
|
|
1808
|
+
total += est;
|
|
1809
|
+
if (total > PRUNE_PROTECT) {
|
|
1810
|
+
pruned += est;
|
|
1811
|
+
targets.push(m);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
if (pruned <= PRUNE_MINIMUM) return 0;
|
|
1816
|
+
const marker = '[eski araç çıktısı temizlendi — özet ve son çıktılar yeterli]';
|
|
1817
|
+
const ids = new Map(targets.map((m) => [m.tool_call_id, marker]));
|
|
1818
|
+
for (const m of targets) m.content = marker;
|
|
1819
|
+
try {
|
|
1820
|
+
const file = this._file(session.id);
|
|
1821
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
|
1822
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1823
|
+
if (!lines[i].trim()) continue;
|
|
1824
|
+
let r;
|
|
1825
|
+
try { r = JSON.parse(lines[i]); } catch { continue; }
|
|
1826
|
+
if (r.t === 'msg' && r.role === 'tool' && ids.has(r.tool_call_id)) {
|
|
1827
|
+
r.content = marker;
|
|
1828
|
+
lines[i] = JSON.stringify({ t: 'msg', ...r });
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
const tmp = file + '.tmp';
|
|
1832
|
+
fs.writeFileSync(tmp, lines.join('\n'));
|
|
1833
|
+
fs.renameSync(tmp, file);
|
|
1834
|
+
} catch {}
|
|
1835
|
+
return pruned;
|
|
1836
|
+
} catch {
|
|
1837
|
+
return 0;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1489
1841
|
/* Cevap verildikten sonra çağrılır: son not update'ten bu yana NOTES_EVERY
|
|
1490
1842
|
(14) mesaj biriktiginde özet çıkarıp oturum notlarını günceller.
|
|
1491
1843
|
Notlar oturum dosyasına (code ile eşli) yazılır ve _chatTurn'da aynı
|
|
@@ -1801,6 +2153,11 @@ class Engine {
|
|
|
1801
2153
|
_bgGroupRecord(title, status, summary) {
|
|
1802
2154
|
for (const [gid, g] of this._bgGroups) {
|
|
1803
2155
|
if (g.dead) continue;
|
|
2156
|
+
/* üst oturum silinmişse grup ölü sayılır — hayalet oturuma yazma */
|
|
2157
|
+
if (this._deletedSessions && this._deletedSessions.has(g.parentId)) {
|
|
2158
|
+
g.dead = true;
|
|
2159
|
+
continue;
|
|
2160
|
+
}
|
|
1804
2161
|
g.results.push({ title, status, summary: String(summary || '').slice(0, 900) });
|
|
1805
2162
|
if (g.results.length >= g.total) {
|
|
1806
2163
|
this._bgGroups.delete(gid);
|
|
@@ -1998,8 +2355,18 @@ class Engine {
|
|
|
1998
2355
|
const reason = Engine.superviseReason(job, now);
|
|
1999
2356
|
if (!reason) continue;
|
|
2000
2357
|
/* #16 öz-kurtarma: takılan ajan CEO'yu meşgul etmeden kendini toparlar */
|
|
2001
|
-
if (reason === 'stuck'
|
|
2002
|
-
|
|
2358
|
+
if (reason === 'stuck') {
|
|
2359
|
+
if ((job.fixes || 0) < BG_FIX_MAX) {
|
|
2360
|
+
this._bgSelfHeal(job, now);
|
|
2361
|
+
} else {
|
|
2362
|
+
/* öz-kurtarma hakları tükendi + hâlâ takılı → ZORLA KAPAT:
|
|
2363
|
+
ajan sonsuza dek 'running' kalırsa fan-out grubu ASLA tamamlanmaz
|
|
2364
|
+
ve birleşik rapor hiç düşmez — CEO "uyuyor" kalır */
|
|
2365
|
+
this._bgForceClose(
|
|
2366
|
+
job,
|
|
2367
|
+
`superyorizon: ajan ${Math.round((now - Date.parse(job.startedAt)) / 60000)} dk'dır takılı, öz-kurtarma hakları tükendi`
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2003
2370
|
continue;
|
|
2004
2371
|
}
|
|
2005
2372
|
job.lastNudgeAt = nowIso();
|
|
@@ -2040,6 +2407,26 @@ class Engine {
|
|
|
2040
2407
|
this._bgKick(job, text);
|
|
2041
2408
|
}
|
|
2042
2409
|
|
|
2410
|
+
/* Takılı işi ZORLA sonlandır: tur abort edilir, iş kaydı SEBEPİYLE kapanır,
|
|
2411
|
+
grup üyesiyse 'aborted' olarak gruba yazılır → birleşik rapor AKAR,
|
|
2412
|
+
CEO "uyumak" yerine hemen sonucu kullanıcıya aktarır. */
|
|
2413
|
+
_bgForceClose(job, reason) {
|
|
2414
|
+
const sid = String(job.id);
|
|
2415
|
+
job.revive = false; /* kick-in-flight guard'ı devre dışı — bu gerçek bir bitiş */
|
|
2416
|
+
this._clearKicks(sid);
|
|
2417
|
+
this._abortReasons = this._abortReasons || new Map();
|
|
2418
|
+
this._abortReasons.set(sid, this._abortReason(reason));
|
|
2419
|
+
const ctrl = this.ctrls.get(sid);
|
|
2420
|
+
if (ctrl) {
|
|
2421
|
+
try { ctrl.abort(); } catch {}
|
|
2422
|
+
/* _run catch'i sebebi okuyup _bgFinish('aborted') yapacak */
|
|
2423
|
+
} else {
|
|
2424
|
+
/* tur zaten yok — kaydı doğrudan kapat */
|
|
2425
|
+
this._bgFinish(sid, 'aborted', reason);
|
|
2426
|
+
}
|
|
2427
|
+
try { log.info('bg', `iş zorla kapatıldı: ${job.title} — ${reason}`); } catch {}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2043
2430
|
/* Arka plan oturumuna müdahale: asılı turu kes (ctrl.abort), sonra
|
|
2044
2431
|
kurtarma mesajını bas. abort → eski _run sonlanır → send yeni tur açar.
|
|
2045
2432
|
revive bayrağı: /stop veya silme sonrası bekleyen müdahaleler patlamasın. */
|
|
@@ -2093,6 +2480,11 @@ class Engine {
|
|
|
2093
2480
|
try {
|
|
2094
2481
|
const meta = this._bgMeta && this._bgMeta.get(String(bgSessionId));
|
|
2095
2482
|
if (!meta) return false;
|
|
2483
|
+
/* üst oturum silinmişse rapor düşer — hayalet oturum yaratma */
|
|
2484
|
+
if (this._deletedSessions && this._deletedSessions.has(meta.parentId)) {
|
|
2485
|
+
this._bgMeta.delete(String(bgSessionId));
|
|
2486
|
+
return false;
|
|
2487
|
+
}
|
|
2096
2488
|
/* Öz-kurtarma/wrap-up kick'i bekliyorsa bu bir ABORT turudur — gerçek
|
|
2097
2489
|
bitiş DEĞİL; erken "ARKA PLAN BİTTİ (sonuca ulaşmadan)" ilanı basma */
|
|
2098
2490
|
const pendingKick = this._bgJobs.get(String(bgSessionId));
|
|
@@ -2134,14 +2526,19 @@ class Engine {
|
|
|
2134
2526
|
}
|
|
2135
2527
|
}
|
|
2136
2528
|
|
|
2137
|
-
/* Ana oturum işini bitirince bekleyen arka plan raporlarını boşalt
|
|
2529
|
+
/* Ana oturum işini bitirince bekleyen arka plan raporlarını boşalt.
|
|
2530
|
+
Güvenli: oturum SİLİNMİŞSE rapor düşer (hayalet oturum yaratılmaz);
|
|
2531
|
+
send başarısızsa (aniden meşgul oldu) rapor sırada kalır, sonra denenir. */
|
|
2138
2532
|
flushPendingReports(sessionId) {
|
|
2139
2533
|
if (!this._pendingReports || !this._pendingReports.length) return;
|
|
2140
2534
|
const rest = [];
|
|
2141
2535
|
for (const r of this._pendingReports) {
|
|
2142
|
-
if (r.parentId
|
|
2143
|
-
|
|
2144
|
-
|
|
2536
|
+
if (r.parentId !== sessionId) { rest.push(r); continue; }
|
|
2537
|
+
if (this._deletedSessions && this._deletedSessions.has(sessionId)) continue;
|
|
2538
|
+
if (this.isBusy(sessionId)) { rest.push(r); continue; }
|
|
2539
|
+
let sent = false;
|
|
2540
|
+
try { sent = !!this.send(sessionId, { text: r.text }); } catch {}
|
|
2541
|
+
if (!sent) rest.push(r);
|
|
2145
2542
|
}
|
|
2146
2543
|
this._pendingReports = rest;
|
|
2147
2544
|
}
|
|
@@ -2237,6 +2634,10 @@ class Engine {
|
|
|
2237
2634
|
/* CEO: uygulayıcı araçlar kapalı — her şey paralel ajana devredilir */
|
|
2238
2635
|
activeTools = activeTools.filter((t) => !CEO_EXEC_TOOLS.has(t.function.name));
|
|
2239
2636
|
}
|
|
2637
|
+
if (session.bcCode && session.bcMode === 'plan') {
|
|
2638
|
+
/* opencode plan agent: salt-okur zorlaması — araç seti GERÇEKten daralır */
|
|
2639
|
+
activeTools = activeTools.filter((t) => PLAN_ALLOW_TOOLS.has(t.function.name));
|
|
2640
|
+
}
|
|
2240
2641
|
if (perms.length === 1 && perms[0] === 'chat') {
|
|
2241
2642
|
system +=
|
|
2242
2643
|
'\n\n# KISITLI MOD\nTüm araçların (komut, dosya, web, tarayıcı, hafıza) kapalı. Sadece yazarak cevap ver. ' +
|
|
@@ -2257,7 +2658,15 @@ class Engine {
|
|
|
2257
2658
|
`\n\n# OTURUM NOTLARI (oturum ${session.code || '?'} — bu oturumun önceki konuşma özeti; birebir geçmişi buradan hatırla)\n` +
|
|
2258
2659
|
session.notes;
|
|
2259
2660
|
}
|
|
2260
|
-
|
|
2661
|
+
/* önek-cache disiplini (opencode request.ts:184): araç sırası oturum
|
|
2662
|
+
boyunca sabit olmalı — alfabetik sıralama sağlayıcı önbelleğini bozmaz */
|
|
2663
|
+
activeTools = [...activeTools].sort((a, b) =>
|
|
2664
|
+
String(a.function.name).localeCompare(String(b.function.name))
|
|
2665
|
+
);
|
|
2666
|
+
/* opencode port: model bağlamına göre dinamik bütçe + compaction özeti */
|
|
2667
|
+
const selEarly = this.sessionModel.get(String(session.id)) || this.modelFor(null) || this.sel;
|
|
2668
|
+
const dynBudget = this._historyBudget(selEarly);
|
|
2669
|
+
let payload = this._buildPayload(system, session.messages, session.notes, dynBudget, session.summary);
|
|
2261
2670
|
// Vision model sadece mesajda görsel varsa devreye girer; terminal ise prompt'un
|
|
2262
2671
|
// shell/command işi olduğunda. Aksi halde ana model kullanılır.
|
|
2263
2672
|
let role = this._lastUserHasImage(session) ? 'vision' : null;
|
|
@@ -2304,7 +2713,7 @@ class Engine {
|
|
|
2304
2713
|
type: 'status',
|
|
2305
2714
|
status: `\u{1F5BC} bu model görüntü girişini desteklemiyor — sohbetten ${removed} görsel kaldırıldı, mesaj görüntüsüz yanıtlanıyor (/change ile görsel destekli modele geçebilirsin)`,
|
|
2306
2715
|
});
|
|
2307
|
-
payload = this._buildPayload(system, session.messages, session.notes);
|
|
2716
|
+
payload = this._buildPayload(system, session.messages, session.notes, dynBudget, session.summary);
|
|
2308
2717
|
res = await this._streamWithFallbacks(
|
|
2309
2718
|
session,
|
|
2310
2719
|
payload,
|
|
@@ -2327,6 +2736,38 @@ class Engine {
|
|
|
2327
2736
|
return res;
|
|
2328
2737
|
}
|
|
2329
2738
|
|
|
2739
|
+
/* opencode port (prompt.ts:96-100 + processor cleanup): önceki tur yarıda
|
|
2740
|
+
kesildiyse (abort/hata), sonucu gelmeyen tool çağrılarına sentetik hata
|
|
2741
|
+
yanıtı yazılır — katı sağlayıcıdaki "messages illegal" (400) önlenir */
|
|
2742
|
+
_repairOrphanTools(session) {
|
|
2743
|
+
try {
|
|
2744
|
+
const msgs = session.messages;
|
|
2745
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
2746
|
+
const m = msgs[i];
|
|
2747
|
+
if (m.role === 'user') return; // temiz sınır — sorun yok
|
|
2748
|
+
if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
2749
|
+
const answered = new Set();
|
|
2750
|
+
for (let j = i + 1; j < msgs.length; j++) {
|
|
2751
|
+
if (msgs[j].role === 'tool') answered.add(msgs[j].tool_call_id);
|
|
2752
|
+
}
|
|
2753
|
+
for (const tc of m.tool_calls) {
|
|
2754
|
+
if (!tc || !tc.id || answered.has(tc.id)) continue;
|
|
2755
|
+
const toolMsg = {
|
|
2756
|
+
role: 'tool',
|
|
2757
|
+
tool_call_id: tc.id,
|
|
2758
|
+
name: (tc.function && tc.function.name) || '',
|
|
2759
|
+
content:
|
|
2760
|
+
'[araç çalıştırması kesildi — tur iptal/kesinti nedeniyle tamamlanamadı; gerekiyorsa yeniden çağır]',
|
|
2761
|
+
};
|
|
2762
|
+
msgs.push(toolMsg);
|
|
2763
|
+
try { this._append(session, toolMsg); } catch {}
|
|
2764
|
+
}
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
} catch {}
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2330
2771
|
async _run(session) {
|
|
2331
2772
|
const ctrl = new AbortController();
|
|
2332
2773
|
this.ctrls.set(session.id, ctrl);
|
|
@@ -2335,8 +2776,16 @@ class Engine {
|
|
|
2335
2776
|
try {
|
|
2336
2777
|
if (!this.sel && !this.sessionModel.get(String(session.id))) throw new Error('Model yapılandırılmadı — %APPDATA%\\beast\\config.yaml ve .env kontrol et');
|
|
2337
2778
|
|
|
2779
|
+
/* opencode port: yetim tool_calls onarımı — önceki tur iptal/kesintiye
|
|
2780
|
+
uğradıysa sonuç bekleyen araç çağrılarına sentetik hata yanıtı yazılır;
|
|
2781
|
+
yoksa katı sağlayıcılar sonraki istekte 400 verir */
|
|
2782
|
+
this._repairOrphanTools(session);
|
|
2783
|
+
|
|
2338
2784
|
let nudged = false; // görev listesi disiplini: run başına en fazla 1 hatırlatma
|
|
2339
2785
|
let wrapNudged = false; // tur limitine yaklaşınca zarif kapanış (bg ajanlar)
|
|
2786
|
+
let usageTotal = 0; // en yüksek görülen total_tokens — compaction tetiği
|
|
2787
|
+
let compacted = false; // run başına en fazla 1 compaction
|
|
2788
|
+
const recentSigs = []; // doom-loop dedektörü: son araç imzaları
|
|
2340
2789
|
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
2341
2790
|
/* SÜRE SINIRI YOK — ama sonsuz tur da yok: bg ajan son 3 tura gelirken
|
|
2342
2791
|
"raporu yaz ve bitir" uyarısı alır; MAX_TURNS sert tavan olarak kalır. */
|
|
@@ -2353,11 +2802,40 @@ class Engine {
|
|
|
2353
2802
|
} catch {}
|
|
2354
2803
|
emit({ type: 'message', message: wmsg });
|
|
2355
2804
|
}
|
|
2805
|
+
/* opencode port (prompt.ts:1281): ana oturumda son turda zorunlu
|
|
2806
|
+
nihai cevap — tur limiti sessizce dolup boş 'done' dönmesin */
|
|
2807
|
+
if (!session.bgJob && turn === MAX_TURNS - 1 && !wrapNudged) {
|
|
2808
|
+
wrapNudged = true;
|
|
2809
|
+
const wmsg = {
|
|
2810
|
+
role: 'user',
|
|
2811
|
+
content:
|
|
2812
|
+
'[SON TUR] Tur limitine ulaşıldı: YENİ araç çağırma — eldeki bilgilerle NİHAİ cevabını şimdi ver. Eksikler varsa açıkça belirt.',
|
|
2813
|
+
};
|
|
2814
|
+
session.messages.push(wmsg);
|
|
2815
|
+
try {
|
|
2816
|
+
this._append(session, wmsg);
|
|
2817
|
+
} catch {}
|
|
2818
|
+
emit({ type: 'message', message: wmsg });
|
|
2819
|
+
}
|
|
2820
|
+
/* opencode port (prompt.ts:1161 + overflow.ts): bağlam dolduysa
|
|
2821
|
+
compaction checkpoint — kuyruk korunur, baş özetlenir */
|
|
2822
|
+
const selNow = this.sessionModel.get(String(sid)) || this.modelFor(null) || this.sel;
|
|
2823
|
+
if (!compacted && this._overContext(session, selNow, usageTotal)) {
|
|
2824
|
+
compacted = true;
|
|
2825
|
+
await this._compactHistory(session, selNow, ctrl.signal);
|
|
2826
|
+
}
|
|
2356
2827
|
this._bgTrim(session); // #21 bg geçmişini olabildiğince ince tut
|
|
2357
2828
|
emit({ type: 'status', status: 'thinking' });
|
|
2358
2829
|
const res = await this._chatTurn(session, ctrl.signal, (delta) =>
|
|
2359
2830
|
emit({ type: 'token', delta })
|
|
2360
2831
|
);
|
|
2832
|
+
/* opencode port (processor.ts:477-482): step-finish usage'ı compaction
|
|
2833
|
+
tetiğine besle — gerçek toplam biliniyorsa tahmine gerek kalmaz */
|
|
2834
|
+
const uTot =
|
|
2835
|
+
res.usage &&
|
|
2836
|
+
(res.usage.total_tokens ||
|
|
2837
|
+
(res.usage.prompt_tokens || 0) + (res.usage.completion_tokens || 0));
|
|
2838
|
+
if (uTot > usageTotal) usageTotal = uTot;
|
|
2361
2839
|
|
|
2362
2840
|
const assistant = { role: 'assistant', content: res.content || '' };
|
|
2363
2841
|
if (res.toolCalls && res.toolCalls.length) assistant.tool_calls = res.toolCalls;
|
|
@@ -2393,6 +2871,7 @@ class Engine {
|
|
|
2393
2871
|
Not/memory/skill bakımı artık arka planda (_postRunHousekeeping). */
|
|
2394
2872
|
this._clearCrash();
|
|
2395
2873
|
this._maybeCompact(sid);
|
|
2874
|
+
this._pruneSession(session); // opencode port: eski araç çıktıları temizlenir (fork edilen prune)
|
|
2396
2875
|
emit({ type: 'done', usage: res.usage || null, meta: res.meta || null });
|
|
2397
2876
|
this._bgFinish(sid, 'done');
|
|
2398
2877
|
this.flushPendingReports(sid);
|
|
@@ -2402,16 +2881,45 @@ class Engine {
|
|
|
2402
2881
|
|
|
2403
2882
|
// Paralel yürütme — bağımsız çağrılar beklemesin
|
|
2404
2883
|
session.toolsSinceReflect = (session.toolsSinceReflect || 0) + res.toolCalls.length;
|
|
2884
|
+
if (!this._knownToolNames) this._knownToolNames = new Set(TOOLS.map((t) => t.function.name));
|
|
2405
2885
|
await Promise.all(
|
|
2406
2886
|
res.toolCalls.map(async (tc) => {
|
|
2407
|
-
|
|
2887
|
+
let name = tc.function && tc.function.name;
|
|
2888
|
+
/* opencode port (llm.ts:296-312 repairToolCall): model aracı adını
|
|
2889
|
+
yanlış yazdıysa büyük/küçük harf normalizasyonuyle onar — tur kaybı yok */
|
|
2890
|
+
if (name && !this._knownToolNames.has(name)) {
|
|
2891
|
+
const fixed = TOOLS.find((t) => t.function.name.toLowerCase() === String(name).trim().toLowerCase());
|
|
2892
|
+
if (fixed) {
|
|
2893
|
+
emitSafe(this, sid, {
|
|
2894
|
+
type: 'status',
|
|
2895
|
+
status: `🔧 araç adı düzeltildi: ${name} → ${fixed.function.name}`,
|
|
2896
|
+
});
|
|
2897
|
+
name = fixed.function.name;
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2408
2900
|
let args = {};
|
|
2409
2901
|
try {
|
|
2410
2902
|
args = JSON.parse((tc.function && tc.function.arguments) || '{}');
|
|
2411
2903
|
} catch {}
|
|
2412
2904
|
emit({ type: 'tool-start', callId: tc.id, name, args });
|
|
2413
2905
|
emit({ type: 'status', status: name });
|
|
2414
|
-
|
|
2906
|
+
/* opencode port (processor.ts:29 + 356-380): aynı araç + birebir
|
|
2907
|
+
aynı argüman 3 kez koştuysa 4.'yü çalıştırma — döngüye para/hız
|
|
2908
|
+
akıtma; modele hata bildirimiyle yol göster */
|
|
2909
|
+
const sig = name + '\u0000' + String((tc.function && tc.function.arguments) || '');
|
|
2910
|
+
const dups = recentSigs.reduce((a, s) => a + (s === sig ? 1 : 0), 0);
|
|
2911
|
+
recentSigs.push(sig);
|
|
2912
|
+
let out;
|
|
2913
|
+
if (dups >= DOOM_LOOP_THRESHOLD) {
|
|
2914
|
+
out = JSON.stringify({
|
|
2915
|
+
error:
|
|
2916
|
+
`doom-loop: ${name} aynı argümanlarla ${DOOM_LOOP_THRESHOLD} kez çalıştı ve hep aynı sonucu verdi. ` +
|
|
2917
|
+
'Aynı çağrıyı tekrarlamak işe yaramaz — farklı bir yöntem/argüman dene ya da eldeki bilgilerle nihai cevabı ver.',
|
|
2918
|
+
});
|
|
2919
|
+
emitSafe(this, sid, { type: 'status', status: `⛔ doom-loop: ${name} tekrarı engellendi` });
|
|
2920
|
+
} else {
|
|
2921
|
+
out = await this._execTool(name, args, ctrl.signal, sid);
|
|
2922
|
+
}
|
|
2415
2923
|
// Ekran görüntüsü gibi araçlar görseli sonraki tura enjekte eder
|
|
2416
2924
|
let injectedImage = null;
|
|
2417
2925
|
try {
|
|
@@ -2474,6 +2982,14 @@ class Engine {
|
|
|
2474
2982
|
} finally {
|
|
2475
2983
|
this.ctrls.delete(sid);
|
|
2476
2984
|
emit({ type: 'status', status: 'idle' });
|
|
2985
|
+
/* bekleyen paralel-ajan raporları HER bitiş yolunda boşaltılır —
|
|
2986
|
+
hata/durdurma/tur-limiti sonrası bile sonuç raporu kullanıcıya ulaşır.
|
|
2987
|
+
DİKKAT: buradan flush'u DOĞRUDAN çağırma! send → _run senkron-hata
|
|
2988
|
+
yolunda (model-yok gibi) await'siz zincirlenir → stack taşması.
|
|
2989
|
+
Macrotask'a ertele: her hop taze stack alır, pop-gönder sırası bozulmaz. */
|
|
2990
|
+
const fl = () => { try { this.flushPendingReports(sid); } catch {} };
|
|
2991
|
+
const t = setTimeout(fl, 0);
|
|
2992
|
+
if (t.unref) t.unref();
|
|
2477
2993
|
}
|
|
2478
2994
|
}
|
|
2479
2995
|
|
|
@@ -2774,7 +3290,7 @@ class Engine {
|
|
|
2774
3290
|
t.function.name !== 'delegate_task' &&
|
|
2775
3291
|
t.function.name !== 'set_reminder' &&
|
|
2776
3292
|
!t.function.name.startsWith('email_')
|
|
2777
|
-
);
|
|
3293
|
+
).sort((a, b) => String(a.function.name).localeCompare(String(b.function.name))); // önek-cache: sabit sıra
|
|
2778
3294
|
const role = 'subagent';
|
|
2779
3295
|
const sel = this.modelFor(role) || this.sel;
|
|
2780
3296
|
if (this.roleModels[role]) {
|
|
@@ -2796,7 +3312,12 @@ class Engine {
|
|
|
2796
3312
|
for (let turn = 0; turn < SUB_MAX_TURNS; turn++) {
|
|
2797
3313
|
const res = await chatStreamAuto(
|
|
2798
3314
|
sel,
|
|
2799
|
-
{
|
|
3315
|
+
{
|
|
3316
|
+
messages: [{ role: 'system', content: system }, ...msgs.slice(1)],
|
|
3317
|
+
tools: subTools,
|
|
3318
|
+
reasoningEffort: this._thinkEffort(),
|
|
3319
|
+
cacheKey: String(sessionId || 'subagent'),
|
|
3320
|
+
},
|
|
2800
3321
|
{ signal: ctrl.signal }
|
|
2801
3322
|
);
|
|
2802
3323
|
const assistant = { role: 'assistant', content: res.content || '' };
|