beast-agent 1.1.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "1.1.0",
4
+ "version": "1.2.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -0,0 +1,122 @@
1
+ 'use strict';
2
+
3
+ /* ---------- opencode agent.ts port: özel ajan tanımları ----------
4
+ Kullanıcı ajanları: %APPDATA%\beast\agents\<isim>.md
5
+ Markdown frontmatter + prompt gövdesi:
6
+
7
+ ---
8
+ model: providerId::model # opsiyonel — o ajanın modeli
9
+ tools: [run_command, web_search] # opsiyonel araç beyaz listesi
10
+ steps: 12 # opsiyonel tur limiti (2-60)
11
+ mode: all # chat | bg | all (nerede kullanılabilir)
12
+ ---
13
+
14
+ Buradan itibaren ajanın sistem prompt eki gelir.
15
+
16
+ Kullanım: sohbette /agent <isim> · paralel ajanda run_background(agent:"isim")
17
+ Ana akış bunları engine.setSessionAgent / runBackground(opts.agent) ile tüketir. */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { parseYaml, beastDir } = require('./config');
22
+
23
+ function dir() {
24
+ return path.join(beastDir(), 'agents');
25
+ }
26
+
27
+ /* İlk açılışta klasörü + örnek tanımı tohumla (kullanıcı hemen görür) */
28
+ function seedIfEmpty() {
29
+ try {
30
+ const d = dir();
31
+ if (fs.existsSync(d)) return;
32
+ fs.mkdirSync(d, { recursive: true });
33
+ fs.writeFileSync(
34
+ path.join(d, 'ornek-arastirmaci.md'),
35
+ [
36
+ '---',
37
+ '# Bu dosya bir ÖRNEK ajan tanımıdır — dilediğin gibi düzenle/kopyala.',
38
+ '# model: providerId::model → bu ajana özel model (boşsa aktif model)',
39
+ '# tools: [web_search, http_fetch, deep_search] → araç beyaz listesi',
40
+ '# steps: 10 → tur limiti (2-60)',
41
+ '# mode: all → chat | bg | all',
42
+ 'mode: all',
43
+ 'steps: 10',
44
+ '---',
45
+ '',
46
+ 'Sen odaklı bir ARAŞTIRMACI ajansın. Görevi baştan sona yürüt:',
47
+ '1) Kaynakları paralel tara (3-5 yeterli), 2) çelişkileri işaretle,',
48
+ '3) bulguları madde madde + kaynak linkleriyle raporla.',
49
+ '2-3 denemede bulunamayan bilgiyi bırak — kısmi sonuçla gel, takılma.',
50
+ '',
51
+ ].join('\n')
52
+ );
53
+ } catch {}
54
+ }
55
+
56
+ function _parseFile(file) {
57
+ const name = path.basename(file).replace(/\.md$/i, '');
58
+ let text = '';
59
+ try {
60
+ text = fs.readFileSync(file, 'utf8');
61
+ } catch {
62
+ return null;
63
+ }
64
+ const t = String(text || '').replace(/\r\n/g, '\n').trim();
65
+ if (!t) return null;
66
+ let meta = {};
67
+ let body = t;
68
+ const m = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(t);
69
+ if (m) {
70
+ try {
71
+ meta = parseYaml(m[1]) || {};
72
+ } catch {
73
+ meta = {};
74
+ }
75
+ body = m[2] || '';
76
+ }
77
+ let tools = null;
78
+ if (Array.isArray(meta.tools)) tools = meta.tools.map(String).map((s) => s.trim()).filter(Boolean);
79
+ else if (meta.tools) tools = String(meta.tools).split(',').map((s) => s.trim()).filter(Boolean);
80
+ const steps = Math.round(Number(meta.steps) || 0);
81
+ return {
82
+ name,
83
+ model: String(meta.model || '').trim() || null,
84
+ tools: tools && tools.length ? tools : null,
85
+ steps: steps ? Math.max(2, Math.min(60, steps)) : null,
86
+ mode: ['chat', 'bg', 'all'].includes(String(meta.mode || '').toLowerCase())
87
+ ? String(meta.mode).toLowerCase()
88
+ : 'all',
89
+ prompt: body.trim(),
90
+ };
91
+ }
92
+
93
+ function list() {
94
+ let files = [];
95
+ try {
96
+ files = fs.readdirSync(dir()).filter((f) => f.toLowerCase().endsWith('.md'));
97
+ } catch {
98
+ return [];
99
+ }
100
+ const out = [];
101
+ const seen = new Set();
102
+ for (const f of files) {
103
+ const def = _parseFile(path.join(dir(), f));
104
+ if (def && def.prompt && !seen.has(def.name.toLowerCase())) {
105
+ seen.add(def.name.toLowerCase());
106
+ out.push(def);
107
+ }
108
+ }
109
+ return out;
110
+ }
111
+
112
+ function get(name) {
113
+ const n = String(name || '').trim().toLowerCase();
114
+ if (!n) return null;
115
+ return (
116
+ list().find((d) => d.name.toLowerCase() === n) ||
117
+ list().find((d) => d.name.toLowerCase().startsWith(n)) ||
118
+ null
119
+ );
120
+ }
121
+
122
+ module.exports = { dir, list, get, seedIfEmpty };
@@ -10,6 +10,7 @@ const { chatStream, chatStreamAuto, chatOnce } = require('./llm');
10
10
  const { chatCompletionsUrl } = require('./config');
11
11
  const tools = require('./tools');
12
12
  const research = require('./research');
13
+ const agentdefs = require('./agentdefs');
13
14
  const memory = require('./memory');
14
15
  const skills = require('./skills');
15
16
  const { estTokens, estMsgTokens } = require('./tokens');
@@ -311,6 +312,59 @@ class Engine {
311
312
  }, SUP_CHECK_MS);
312
313
  if (this._supTimer.unref) this._supTimer.unref();
313
314
  skills.seedIfEmpty();
315
+ agentdefs.seedIfEmpty();
316
+ }
317
+
318
+ /* ---------- opencode agent.ts port: özel ajanlar ---------- */
319
+
320
+ /* Oturumu bir özel ajana bağla (%APPDATA%\beast\agents\<isim>.md) —
321
+ o oturumun prompt/model/araç/tur limiti ajan tanımından gelir */
322
+ setSessionAgent(sessionId, name) {
323
+ const sid = String(sessionId || '');
324
+ if (!sid) return { ok: false, error: 'oturum yok' };
325
+ const n = String(name || '').trim();
326
+ let s;
327
+ try { s = this._load(sid); } catch { return { ok: false, error: 'oturum açılamadı' }; }
328
+ if (!n || n === 'off' || n === 'kapalı') {
329
+ delete s.agentName;
330
+ this.cache.set(sid, s);
331
+ try {
332
+ fs.appendFileSync(this._file(sid), JSON.stringify({ t: 'agent', name: null, at: nowIso() }) + '\n');
333
+ } catch {}
334
+ return { ok: true, agent: null };
335
+ }
336
+ const def = agentdefs.get(n);
337
+ if (!def) {
338
+ const have = agentdefs.list().map((d) => d.name).join(', ') || '(tanım yok)';
339
+ return { ok: false, error: `ajan bulunamadı: ${n} — tanımlı: ${have}` };
340
+ }
341
+ s.agentName = def.name;
342
+ this.cache.set(sid, s);
343
+ try {
344
+ fs.appendFileSync(this._file(sid), JSON.stringify({ t: 'agent', name: def.name, at: nowIso() }) + '\n');
345
+ } catch {}
346
+ return { ok: true, agent: def.name, model: def.model, tools: def.tools, steps: def.steps };
347
+ }
348
+
349
+ /* Oturumun (varsa) özel ajan tanımı — mode filtresiyle: chat oturumunda
350
+ mode:bg ajanı kullanılmaz, bg oturumunda mode:chat kullanılmaz */
351
+ _agentDefFor(session, forBg = false) {
352
+ const n = session && session.agentName;
353
+ if (!n) return null;
354
+ const def = agentdefs.get(n);
355
+ if (!def) return null;
356
+ if (def.mode !== 'all' && def.mode !== (forBg ? 'bg' : 'chat')) return null;
357
+ return def;
358
+ }
359
+
360
+ listAgents() {
361
+ return agentdefs.list().map((d) => ({
362
+ name: d.name,
363
+ model: d.model,
364
+ tools: d.tools,
365
+ steps: d.steps,
366
+ mode: d.mode,
367
+ }));
314
368
  }
315
369
 
316
370
  /* Zinciri yeniden kur: base + custom, silinenleri ayıkla */
@@ -981,6 +1035,9 @@ class Engine {
981
1035
  /* opencode compaction: konuşma özeti — birebir geçmişin yerine geçmez,
982
1036
  payload'da system'den sonra sabit user mesajı olarak girer */
983
1037
  if (rec.text) session.summary = String(rec.text);
1038
+ } else if (rec.t === 'agent') {
1039
+ /* opencode agent port: oturumun bağlı olduğu özel ajan */
1040
+ if (rec.name) session.agentName = String(rec.name);
984
1041
  } else if (rec.t === 'botdm') {
985
1042
  /* botlar arası DM oturumu — admin izleyebilir, sidebar'da görünmez */
986
1043
  session.isBotDm = true;
@@ -1328,10 +1385,10 @@ class Engine {
1328
1385
  ? '# PROJE TALİMATLARI (workspace AGENTS/CLAUDE/CONTEXT dosyalarından — daima uy)\n' + proj + '\n'
1329
1386
  : '') +
1330
1387
  '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' +
1388
+ '1) BAĞLAM: değiştirmeden önce ilgili dosyaları OKU — read_file satır numaralı döner (N: içerik); büyük dosyada devamını offset parametresiyle oku, ASLA baştan okuma; bir dosyayı aynı oturumda BİR KEZ okumak yeter — içeriği bağlamda zaten var. İçerik araması için grep (regex), dosya adı araması için glob kullan; varsayım yapma, mevcut stili/konvansiyonu takip et.\n' +
1332
1389
  '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' +
1390
+ '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. 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' +
1391
+ '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' +
1335
1392
  '5) RAPOR: 1-3 satır — ne değişti + doğrulama sonucu (ör. "npm test ✓ 154/154"). Uzun açıklama yok.\n' +
1336
1393
  'CANLI ÖNİZLEME: web sitesi/app üretirsen son adımda ÇALIŞIR halde bırak (dev server ayakta ya da index.html yazılmış) — panel iş bitince ürettiğin şeyi dahili tarayıcıda otomatik canlı açar.\n' +
1337
1394
  'HIZ KURALLARI:\n' +
@@ -1384,6 +1441,12 @@ class Engine {
1384
1441
  '# SKILLS\nKullanmadan önce ilgili SKILL.md dosyasını read_file ile oku.\n' + sk.join('\n')
1385
1442
  );
1386
1443
  }
1444
+ /* opencode instruction port (ajan modu): workspace AGENTS/CLAUDE/CONTEXT
1445
+ talimatları — oturum başına bir kez okunur, epoch boyunca sabit */
1446
+ const projChat = this._projectInstructions(session);
1447
+ if (projChat) {
1448
+ parts.push('# PROJE TALİMATLARI (workspace AGENTS/CLAUDE/CONTEXT dosyalarından — daima uy)\n' + projChat);
1449
+ }
1387
1450
  /* Beast Code (bcCode) oturumları buildBcSystem kullanır — buraya düşmez */
1388
1451
  const nowD = new Date();
1389
1452
  const localDate = nowD.toLocaleDateString('tr-TR');
@@ -1415,7 +1478,7 @@ class Engine {
1415
1478
  '# ARAÇ KURALLARI\n' +
1416
1479
  [
1417
1480
  'Basit soruları araç kullanmadan doğrudan cevapla — hız önceliklidir.',
1418
- '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.',
1481
+ '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; büyük dosyada devamını offset parametresiyle oku, ASLA baştan okuma; bir dosyayı aynı oturumda BİR KEZ okumak yeter. edit_file/write_file sonucu additions/deletions döner ve değişiklik diske ANINDA uygulanır — doğrulamak için dosyayı TEKRAR OKUMA; önceki okuduğun içerik + kendi değişikliklerin üzerinden devam et.',
1419
1482
  '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).',
1420
1483
  '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.',
1421
1484
  '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.',
@@ -1555,8 +1618,15 @@ class Engine {
1555
1618
  });
1556
1619
  for (let k = 0; k < flat.length; k++) {
1557
1620
  const m = flat[k];
1558
- if (m.role === 'tool' && String(m.content || '').length > TOOL_OUT_KEEP * 2) {
1559
- flat[k] = { ...m, content: String(m.content).slice(0, TOOL_OUT_KEEP) + '\n…[kırpıldı]' };
1621
+ if (m.role !== 'tool') continue;
1622
+ const tooLong = String(m.content || '').length > TOOL_OUT_KEEP * 2;
1623
+ /* diffView UI-only metadata'dır — sağlayıcıya GİTMEZ (bilinmeyen alan
1624
+ reddi + token israfı önlenir); kırpma ile aynı kopyada birleştirilir */
1625
+ if (tooLong || m.diffView) {
1626
+ const rest = { ...m };
1627
+ delete rest.diffView;
1628
+ if (tooLong) rest.content = String(m.content).slice(0, TOOL_OUT_KEEP) + '\n…[kırpıldı]';
1629
+ flat[k] = rest;
1560
1630
  }
1561
1631
  }
1562
1632
  /* opencode compaction replay (message-v2.ts:521-572): özet, system'den
@@ -1816,7 +1886,10 @@ class Engine {
1816
1886
  if (pruned <= PRUNE_MINIMUM) return 0;
1817
1887
  const marker = '[eski araç çıktısı temizlendi — özet ve son çıktılar yeterli]';
1818
1888
  const ids = new Map(targets.map((m) => [m.tool_call_id, marker]));
1819
- for (const m of targets) m.content = marker;
1889
+ for (const m of targets) {
1890
+ m.content = marker;
1891
+ delete m.diffView; /* prune edilen çıktının UI diff'i de düşer */
1892
+ }
1820
1893
  try {
1821
1894
  const file = this._file(session.id);
1822
1895
  const lines = fs.readFileSync(file, 'utf8').split('\n');
@@ -1826,6 +1899,7 @@ class Engine {
1826
1899
  try { r = JSON.parse(lines[i]); } catch { continue; }
1827
1900
  if (r.t === 'msg' && r.role === 'tool' && ids.has(r.tool_call_id)) {
1828
1901
  r.content = marker;
1902
+ delete r.diffView;
1829
1903
  lines[i] = JSON.stringify({ t: 'msg', ...r });
1830
1904
  }
1831
1905
  }
@@ -2006,6 +2080,27 @@ class Engine {
2006
2080
  return true;
2007
2081
  }
2008
2082
 
2083
+ /* Paralel ajan GEÇMİŞİNİ topluca sil: çalışanlar/ bekleyenler iptal edilir,
2084
+ tüm bg oturum dosyaları + kalıcı kayıt temizlenir. Döndürür: silinen iş sayısı */
2085
+ clearAllBgJobs() {
2086
+ let n = 0;
2087
+ for (const id of [...this._bgJobs.keys()]) {
2088
+ try {
2089
+ const j = this._bgJobs.get(id);
2090
+ if (j && (j.status === 'running' || j.status === 'queued')) {
2091
+ this._abortReasons = this._abortReasons || new Map();
2092
+ this._abortReasons.set(id, 'paralel ajan geçmişi topluca temizlendi');
2093
+ const c = this.ctrls.get(id);
2094
+ if (c) c.abort();
2095
+ }
2096
+ if (this.deleteSession(id)) n++;
2097
+ } catch {}
2098
+ }
2099
+ try { fs.unlinkSync(this._bgJobsFile); } catch {}
2100
+ this._bgEmit();
2101
+ return n;
2102
+ }
2103
+
2009
2104
  /* #14 paralel ajanlar: ANA oturumu hiç bloklamayan arka plan çalıştırıcı.
2010
2105
  Ayrı gizli oturum açar, işi orada koşturur; bitince özet bildirim
2011
2106
  istenen sohbete düşer. Birden fazla iş aynı anda paralel koşabilir —
@@ -2019,6 +2114,11 @@ class Engine {
2019
2114
  const bg = this._load(this.createSession().id);
2020
2115
  bg.messages = bg.messages || [];
2021
2116
  bg.bgJob = true; // arka plan oturumu: auto-memory/kalibrasyon kapalı
2117
+ /* opencode agent port: iş özel ajana bağlıysa prompt/model/araç/steps ondan */
2118
+ if (opts.agent) {
2119
+ const adef = agentdefs.get(String(opts.agent));
2120
+ if (adef && (adef.mode === 'bg' || adef.mode === 'all')) bg.agentName = adef.name;
2121
+ }
2022
2122
  this.cache.set(bg.id, bg);
2023
2123
  const ttl = String(title || t).replace(/\s+/g, ' ').trim().slice(0, 80) || 'arka plan görevi';
2024
2124
  /* #17 cache'teki nesneye de işaret koy — listSessions temiz başlık göstersin */
@@ -2030,12 +2130,16 @@ class Engine {
2030
2130
  this._file(bg.id),
2031
2131
  JSON.stringify({ t: 'meta2', bgOf: parent, title: ttl, at: new Date().toISOString() }) + '\n'
2032
2132
  );
2133
+ if (bg.agentName) {
2134
+ fs.appendFileSync(this._file(bg.id), JSON.stringify({ t: 'agent', name: bg.agentName, at: nowIso() }) + '\n');
2135
+ }
2033
2136
  } catch {}
2034
2137
  this._bgJobs.set(bg.id, {
2035
2138
  id: bg.id,
2036
2139
  code: bg.code,
2037
2140
  title: ttl,
2038
2141
  task: t.slice(0, 500),
2142
+ agent: bg.agentName || null,
2039
2143
  parentId: parent,
2040
2144
  groupId: opts.groupId || null,
2041
2145
  status: 'running', // queued | running | done | error | aborted
@@ -2120,7 +2224,10 @@ class Engine {
2120
2224
  for (const item of list) {
2121
2225
  const task = String((item && item.task) || item || '').trim();
2122
2226
  if (!task) continue;
2123
- const r = this.runBackground(parent, task, (item && item.title) || `${groupTitle || 'grup'} #${++n}`, { groupId });
2227
+ const r = this.runBackground(parent, task, (item && item.title) || `${groupTitle || 'grup'} #${++n}`, {
2228
+ groupId,
2229
+ agent: (item && item.agent) || null,
2230
+ });
2124
2231
  if (r.ok) ids.push(r.backgroundId);
2125
2232
  }
2126
2233
  if (!ids.length) {
@@ -2548,10 +2655,14 @@ class Engine {
2548
2655
  → her turda çok daha az prompt tokeni = hızlı ön-bellek + düşük maliyet */
2549
2656
  buildBgSystem(session) {
2550
2657
  const job = (session && this._bgJobs.get(String(session.id))) || {};
2658
+ /* opencode instruction port (bg ajanlar): workspace talimatları + genel ajan disiplini */
2659
+ const proj = this._projectInstructions(session);
2551
2660
  return (
2552
2661
  `Sen odaklı bir ARKA PLAN ajanısın. Tek görevini baştan sona bitir.\n` +
2553
2662
  (job.task ? `GÖREV: ${job.task}\n` : '') +
2554
2663
  `Ortam: Windows + PowerShell; çalışma klasörü: ${this.workspace}\n` +
2664
+ `GENEL AJAN DİSİPLİNİ: görevi A'dan Z'ye yürüt — bağlam topla, uygula, DOĞRULA; yarım bırakma; doğrulama sonucunu rapora yaz.\n` +
2665
+ (proj ? `PROJE TALİMATLARI (workspace AGENTS/CLAUDE/CONTEXT — daima uy):\n${proj}\n` : '') +
2555
2666
  `HIZ KURALLARI:\n` +
2556
2667
  `- Döngülü işleri (çok URL/dosya/sayfa, tekrarlı parse-hesap) TEK python_run betiğinde topluca bitir.\n` +
2557
2668
  `- Web için web_search kullan (zincir dahili tarayıcıyla başlar — gerçek Chromium ile Google); tek aramada bulunamazsa veya çok kaynaklı derin araştırma gerekiyorsa deep_search kullan (çoklu sorgu paralel + gizli tarayıcıda tam sayfa okuma). Sayfa açma/göstermenin VARSAYILANI DAHİLİ tarayıcıdır: browser_open → browser_snapshot → browser_click/type/read. Kullanıcı açıkça dış tarayıcı (chrome/firefox/başka/normal/kendi tarayıcım) istediyse run_command ile \`start "" <url>\` çalıştır. Görseli göremiyorsan metni ocr_read ile oku (source:"browser").\n` +
@@ -2592,6 +2703,8 @@ class Engine {
2592
2703
  }
2593
2704
 
2594
2705
  async _chatTurn(session, signal, onDelta, toolsList = TOOLS) {
2706
+ /* opencode agent.ts port: özel ajan tanımı — prompt/model/araç/steps */
2707
+ const adef = this._agentDefFor(session, !!session.bgJob);
2595
2708
  /* Beast Code: todo_write açıklaması "3+ adım" kısıtı içerir ve model küçük
2596
2709
  işlerde atlar — bcCode oturumunda HER işte İLK araç olacak şekilde değiştir */
2597
2710
  if (session && session.bcCode) {
@@ -2639,6 +2752,11 @@ class Engine {
2639
2752
  /* opencode plan agent: salt-okur zorlaması — araç seti GERÇEKten daralır */
2640
2753
  activeTools = activeTools.filter((t) => PLAN_ALLOW_TOOLS.has(t.function.name));
2641
2754
  }
2755
+ if (adef && adef.tools) {
2756
+ /* özel ajan araç beyaz listesi — tanımda olmayan araç görünmez */
2757
+ const allow = new Set(adef.tools);
2758
+ activeTools = activeTools.filter((t) => allow.has(t.function.name));
2759
+ }
2642
2760
  if (perms.length === 1 && perms[0] === 'chat') {
2643
2761
  system +=
2644
2762
  '\n\n# KISITLI MOD\nTüm araçların (komut, dosya, web, tarayıcı, hafıza) kapalı. Sadece yazarak cevap ver. ' +
@@ -2659,6 +2777,9 @@ class Engine {
2659
2777
  `\n\n# OTURUM NOTLARI (oturum ${session.code || '?'} — bu oturumun önceki konuşma özeti; birebir geçmişi buradan hatırla)\n` +
2660
2778
  session.notes;
2661
2779
  }
2780
+ if (adef && adef.prompt) {
2781
+ system += `\n\n# AJAN: ${adef.name}\n${adef.prompt}`;
2782
+ }
2662
2783
  /* önek-cache disiplini (opencode request.ts:184): araç sırası oturum
2663
2784
  boyunca sabit olmalı — alfabetik sıralama sağlayıcı önbelleğini bozmaz */
2664
2785
  activeTools = [...activeTools].sort((a, b) =>
@@ -2680,6 +2801,11 @@ class Engine {
2680
2801
  const ssel = this.sessionModel.get(String(session.id));
2681
2802
  if (ssel) sel = ssel;
2682
2803
  }
2804
+ /* özel ajan model override — en son söz ajan tanımının */
2805
+ if (adef && adef.model) {
2806
+ const am = this._resolveRole(String(adef.model));
2807
+ if (am) sel = am;
2808
+ }
2683
2809
  if (role && this.roleModels[role]) {
2684
2810
  emitSafe(this, session.id, { type: 'status', status: `rol: ${role} → ${sel.providerName} · ${sel.model}` });
2685
2811
  }
@@ -2786,14 +2912,17 @@ class Engine {
2786
2912
  let wrapNudged = false; // tur limitine yaklaşınca zarif kapanış (bg ajanlar)
2787
2913
  let usageTotal = 0; // en yüksek görülen total_tokens — compaction tetiği
2788
2914
  let compacted = false; // run başına en fazla 1 compaction
2915
+ /* opencode agent.steps port: özel ajanın kendi tur limiti (2-60) */
2916
+ const runDef = this._agentDefFor(session, !!session.bgJob);
2917
+ const maxTurns = Math.max(2, Math.min(60, (runDef && runDef.steps) || MAX_TURNS));
2789
2918
  const recentSigs = []; // doom-loop dedektörü: son araç imzaları
2790
2919
  /* Beast Code canlı önizleme: run boyunca üretilen eserleri topla —
2791
2920
  iş bitince dahili tarayıcıda CANLI açılır (site/dev server/HTML) */
2792
2921
  const bcArtifacts = session.bcCode ? { html: [], serverUrl: null } : null;
2793
- for (let turn = 0; turn < MAX_TURNS; turn++) {
2922
+ for (let turn = 0; turn < maxTurns; turn++) {
2794
2923
  /* SÜRE SINIRI YOK — ama sonsuz tur da yok: bg ajan son 3 tura gelirken
2795
- "raporu yaz ve bitir" uyarısı alır; MAX_TURNS sert tavan olarak kalır. */
2796
- if (session.bgJob && turn === MAX_TURNS - 3 && !wrapNudged) {
2924
+ "raporu yaz ve bitir" uyarısı alır; maxTurns sert tavan olarak kalır. */
2925
+ if (session.bgJob && turn === maxTurns - 3 && !wrapNudged) {
2797
2926
  wrapNudged = true;
2798
2927
  const wmsg = {
2799
2928
  role: 'user',
@@ -2808,7 +2937,7 @@ class Engine {
2808
2937
  }
2809
2938
  /* opencode port (prompt.ts:1281): ana oturumda son turda zorunlu
2810
2939
  nihai cevap — tur limiti sessizce dolup boş 'done' dönmesin */
2811
- if (!session.bgJob && turn === MAX_TURNS - 1 && !wrapNudged) {
2940
+ if (!session.bgJob && turn === maxTurns - 1 && !wrapNudged) {
2812
2941
  wrapNudged = true;
2813
2942
  const wmsg = {
2814
2943
  role: 'user',
@@ -2968,20 +3097,28 @@ class Engine {
2968
3097
  }
2969
3098
  // Ekran görüntüsü gibi araçlar görseli sonraki tura enjekte eder
2970
3099
  let injectedImage = null;
3100
+ /* opencode ctx.metadata portu: edit/write sonucundaki diffView, LLM
3101
+ bağlamından AYRI tutulur — yalnız UI'ye gider (kırmızı/yeşil diff) */
3102
+ let diffView = null;
2971
3103
  try {
2972
3104
  const obj = JSON.parse(out);
2973
3105
  if (obj && obj.__injectImage) {
2974
3106
  injectedImage = String(obj.__injectImage);
2975
3107
  delete obj.__injectImage;
2976
- out = JSON.stringify(obj);
2977
3108
  }
3109
+ if (obj && obj.diffView) {
3110
+ diffView = obj.diffView;
3111
+ delete obj.diffView;
3112
+ }
3113
+ if (injectedImage || diffView) out = JSON.stringify(obj);
2978
3114
  } catch {}
2979
3115
  const toolMsg = { role: 'tool', tool_call_id: tc.id, name, content: out.slice(0, TOOL_OUT_KEEP * 6) };
3116
+ if (diffView) toolMsg.diffView = diffView; /* oturum dosyasına da düşer — geçmiş açılınca diff yeniden çizilir */
2980
3117
  session.messages.push(toolMsg);
2981
3118
  try {
2982
3119
  this._append(session, toolMsg);
2983
3120
  } catch {}
2984
- emit({ type: 'tool-end', callId: tc.id, ok: !/"error"/.test(out.slice(0, 200)), result: out.slice(0, 4000) });
3121
+ emit({ type: 'tool-end', callId: tc.id, ok: !/"error"/.test(out.slice(0, 200)), result: out.slice(0, 4000), diff: diffView });
2985
3122
  if (injectedImage) {
2986
3123
  const imgMsg = {
2987
3124
  role: 'user',
@@ -2998,7 +3135,7 @@ class Engine {
2998
3135
  );
2999
3136
  }
3000
3137
  emit({ type: 'done', usage: null });
3001
- this._bgFinish(sid, 'error', 'tur limiti doldu (MAX_TURNS)'); } catch (e) {
3138
+ this._bgFinish(sid, 'error', 'tur limiti doldu (maxTurns)'); } catch (e) {
3002
3139
  const aborted = e && (e.name === 'AbortError' || ctrl.signal.aborted);
3003
3140
  if (aborted) {
3004
3141
  this._clearCrash(); // kullanıcı durdurdu — kurtarma yok
@@ -3449,7 +3586,9 @@ class Engine {
3449
3586
  });
3450
3587
  }
3451
3588
  if (name === 'ocr_read') return JSON.stringify(await this._ocrRead(args || {}, signal));
3452
- if (!(name === 'run_command' || name === 'read_file' || name === 'write_file' || name === 'list_dir' ||
3589
+ /* opencode general-agent portu: alt-ajan da edit/grep/glob kullanır */
3590
+ if (!(name === 'run_command' || name === 'read_file' || name === 'write_file' || name === 'edit_file' ||
3591
+ name === 'list_dir' || name === 'grep' || name === 'glob' ||
3453
3592
  name === 'web_search' || name === 'http_fetch' || name === 'python_run')) {
3454
3593
  return JSON.stringify({ ok: false, error: `unknown tool ${name}` });
3455
3594
  }
@@ -3461,8 +3600,9 @@ class Engine {
3461
3600
 
3462
3601
  /* ---------- tool dispatch ---------- */
3463
3602
 
3464
- /* Onay gerektiren araçlar — tepkiyle onay kapısına takılır */
3465
- static RISKY_TOOLS = new Set(['run_command', 'write_file', 'python_run', 'email_send', 'watcher_add']);
3603
+ /* Onay gerektiren araçlar — tepkiyle onay kapısına takılır.
3604
+ opencode'da edit+write aynı "edit" iznine takılır burada da ikisi birlikte */
3605
+ static RISKY_TOOLS = new Set(['run_command', 'write_file', 'edit_file', 'python_run', 'email_send', 'watcher_add']);
3466
3606
 
3467
3607
  /* ANA KOD KİLİDİ: yıkıcı işlem desenleri (korumalı yol + bu desen = engel) */
3468
3608
  static DESTRUCTIVE_RE =
@@ -3498,6 +3638,7 @@ class Engine {
3498
3638
  'Okumak serbesttir; kod yalnızca kullanıcının kendi eliyle, dışarıdan değiştirilebilir.',
3499
3639
  });
3500
3640
  if (name === 'write_file' && this._isProtectedPath(args.path)) return deny('yazma');
3641
+ if (name === 'edit_file' && this._isProtectedPath(args.path)) return deny('düzenleme');
3501
3642
  if (name === 'run_command' && this._cmdTouchesProtected(String(args.command || ''))) return deny('komutla değiştirme');
3502
3643
  if (name === 'python_run' && this._cmdTouchesProtected(String(args.code || ''))) return deny('betikle değiştirme');
3503
3644
  return null;
@@ -3599,7 +3740,15 @@ class Engine {
3599
3740
  return JSON.stringify(r);
3600
3741
  }
3601
3742
  if (name === 'run_background') {
3602
- const r = this.runBackground(sessionId, args.task, args.title);
3743
+ if (args.agent && !agentdefs.get(String(args.agent))) {
3744
+ return JSON.stringify({
3745
+ ok: false,
3746
+ error:
3747
+ `ajan tanımı bulunamadı: ${args.agent} — tanımlı ajanlar: ` +
3748
+ (agentdefs.list().map((d) => d.name).join(', ') || '(yok)'),
3749
+ });
3750
+ }
3751
+ const r = this.runBackground(sessionId, args.task, args.title, { agent: args.agent ? String(args.agent) : null });
3603
3752
  return JSON.stringify({
3604
3753
  ...r,
3605
3754
  note: 'paralel ajan başladı — ana sohbet açık; bittiğinde özet otomatik gelir',
@@ -3608,7 +3757,7 @@ class Engine {
3608
3757
  if (name === 'run_background_many') {
3609
3758
  const rawList = Array.isArray(args.tasks) ? args.tasks : [];
3610
3759
  const tasks = rawList
3611
- .map((t) => ({ title: String((t && t.title) || ''), task: String((t && t.task) || t || '') }))
3760
+ .map((t) => ({ title: String((t && t.title) || ''), task: String((t && t.task) || t || ''), agent: (t && t.agent) ? String(t.agent) : null }))
3612
3761
  .filter((t) => t.task.trim());
3613
3762
  const r = tasks.length
3614
3763
  ? this.runBackgroundMany(sessionId, tasks, String(args.title || ''))
@@ -3809,11 +3958,14 @@ class Engine {
3809
3958
  const r = await this.browser.act(name.slice(8), args, signal, { sessionId });
3810
3959
  return JSON.stringify(r);
3811
3960
  }
3812
- if (!(name === 'run_command' || name === 'read_file' || name === 'write_file' || name === 'list_dir' ||
3961
+ /* opencode tool registry portu: edit_file/grep/glob ana ajanın da araçları
3962
+ bunlar olmadan model write_file + tam dosya okuma döngüsüne düşer */
3963
+ if (!(name === 'run_command' || name === 'read_file' || name === 'write_file' || name === 'edit_file' ||
3964
+ name === 'list_dir' || name === 'grep' || name === 'glob' ||
3813
3965
  name === 'web_search' || name === 'http_fetch' || name === 'python_run')) {
3814
3966
  return JSON.stringify({ ok: false, error: `unknown tool ${name}` });
3815
3967
  }
3816
- return await tools.exec(name, args, { cwd: this._sessionWorkspace(sessionId), signal });
3968
+ return await tools.exec(name, args, { cwd: this._sessionWorkspace(sessionId), signal, wantDiff: true });
3817
3969
  } catch (e) {
3818
3970
  return JSON.stringify({ ok: false, error: String((e && e.message) || e) });
3819
3971
  }
@@ -4178,6 +4330,7 @@ const TOOLS = [
4178
4330
  properties: {
4179
4331
  task: { type: 'string', description: 'Self-contained task description with ALL needed context (paths, URLs, constraints, expected output) — the agent cannot see your context.' },
4180
4332
  title: { type: 'string', description: 'Short label shown in the agents tab and notification.' },
4333
+ agent: { type: 'string', description: 'Optional custom agent name (defined in %APPDATA%\\beast\\agents\\*.md) — that agent\u2019s prompt/model/tools/step-limit apply.' },
4181
4334
  },
4182
4335
  required: ['task'],
4183
4336
  },
@@ -4210,6 +4363,7 @@ const TOOLS = [
4210
4363
  properties: {
4211
4364
  title: { type: 'string', description: 'Short label of this step' },
4212
4365
  task: { type: 'string', description: 'Self-contained instruction with all context' },
4366
+ agent: { type: 'string', description: 'Optional custom agent name for this step (%APPDATA%\\beast\\agents\\*.md)' },
4213
4367
  },
4214
4368
  required: ['task'],
4215
4369
  },
@@ -125,6 +125,7 @@ Get-Content "$env:APPDATA\\beast\\sessions\\bg-jobs.json" | ConvertFrom-Json | %
125
125
  | \`agent/engine.js\` | OTURUM MOTORU: send/_run tur döngüsü, paralel ajanlar (run_background, run_background_many, tasks_list, task_status, task_cancel — task_cancel reason ZORUNLU), superyorizon (takılan işi zorla kapatır), iptal sebep disiplini, flushPendingReports |
126
126
  | \`agent/tools.js\` | Araçlar + web_search SIRALI ZİNCİRİ (searchChain: 1 dahili tarayıcı(Google) → 2 Obscura(stealth→DuckDuckGo) → 3 TinyFish(anahtarsa) → 4 python çoklu-motor; sıra Ayarlar→Web Arama'dan değişir), run_command, read/write_file, python_run, http_fetch, deep_search |
127
127
  | \`agent/llm.js\` | Sağlayıcı çağrısı + retry: ağ kopmasında 6 deneme + internet dönene kadar bekler; akış ortası kopmada kaldığı yerden devam |
128
+ | \`agent/agentdefs.js\` | Özel ajan tanımları: %APPDATA%\beast\agents\\*.md (frontmatter: model/tools/steps/mode + prompt gövdesi); /agent ile bağlanır, run_background(agent:) ile paralel ajana verilir |
128
129
  | \`agent/obscura.js\` | Obscura: otomatik kurulum (GitHub releases, stealth zip) + DuckDuckGo html/lite arama ayrıştırıcı |
129
130
  | \`agent/bots.js\` | Bot CRUD, 5 haneli bot kodu, izolasyon; ad-ilk-harf doğrulaması |
130
131
  | \`agent/skills.js\` | Builtin tohumlar (help, email, python-web-search, pdf, gold-trading) + SKILL.md taraması + taslaklar |