beast-agent 2.0.0 → 2.1.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": "2.0.0",
4
+ "version": "2.1.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -6,8 +6,9 @@
6
6
 
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
- const { chatStream, chatStreamAuto, chatOnce } = require('./llm');
10
- const { chatCompletionsUrl } = require('./config');
9
+ const llm = require('./llm');
10
+ const { chatStream, chatStreamAuto, chatOnce } = llm;
11
+ const { chatCompletionsUrl, beastDir } = require('./config');
11
12
  const tools = require('./tools');
12
13
  const research = require('./research');
13
14
  const agentdefs = require('./agentdefs');
@@ -60,7 +61,8 @@ const OWNER_FAIL_EMAIL = 'info@algokod.com';
60
61
  OpenAI (reasoning.effort) ve OpenRouter (reasoning_effort) 2026 itibarıyla:
61
62
  none · minimal · low · medium · high · xhigh · max.
62
63
  0 (Kapalı) parametre göndermez; 1-5 API'ye reasoning_effort olarak gider.
63
- Model desteklemiyorsa llm.js tek seferlik parametresiz retry yapar. */
64
+ Model desteklemiyorsa llm.js parametreyi atlayıp sessizce retry yapar ve
65
+ bu model için KALICI öğrenir (model-caps.json) → bir daha gönderilmez. */
64
66
  const THINK_LEVELS = [
65
67
  { v: 0, label: 'Kapalı', effort: null },
66
68
  { v: 1, label: 'Low', effort: 'low' },
@@ -249,6 +251,9 @@ class Engine {
249
251
  };
250
252
  this.sessionsDir = opts.sessionsDir;
251
253
  fs.mkdirSync(this.sessionsDir, { recursive: true });
254
+ /* model yetenek keşfi: reddedilen parametre/seviyeler kalıcı öğrenilir
255
+ (reasoning_effort seviyeleri, temperature, prompt_cache_key) */
256
+ try { llm.setCapsFile(path.join(beastDir(), 'model-caps.json')); } catch {}
252
257
  this.workspace = opts.workspace || process.env.USERPROFILE || '.';
253
258
  this.cfg.baseChain = (this.cfg.chain || []).slice();
254
259
  this.cfg.chain = [...this.cfg.baseChain, ...customChain(opts.customProviders)];
@@ -758,6 +763,16 @@ class Engine {
758
763
  ? `bağlantı sorunu — internet dönünce ya da yeniden denemeyle sürer (${attempt}/6)`
759
764
  : `sağlayıcı yanıtsız — tekrar deniyor (${attempt}/3)`,
760
765
  }),
766
+ onEffortDowngrade: (from, to) =>
767
+ emitSafe(this, session.id, {
768
+ type: 'status',
769
+ status: `🧠 ${c.model}: düşünme seviyesi '${from}' kabul edilmedi → otomatik '${to}' (bu model için kalıcı)`,
770
+ }),
771
+ onParamDrop: (p) =>
772
+ emitSafe(this, session.id, {
773
+ type: 'status',
774
+ status: `⚠ ${c.model}: '${llm.PARAM_LABELS[p] || p}' kabul edilmedi → atlandı (bu model için kalıcı)`,
775
+ }),
761
776
  }
762
777
  );
763
778
  if (i > 0) {
@@ -3731,7 +3746,19 @@ const skills = require('./skills');
3731
3746
  reasoningEffort: this._thinkEffort(),
3732
3747
  cacheKey: String(sessionId || 'subagent'),
3733
3748
  },
3734
- { signal: ctrl.signal }
3749
+ {
3750
+ signal: ctrl.signal,
3751
+ onEffortDowngrade: (from, to) =>
3752
+ emitSafe(this, sessionId, {
3753
+ type: 'status',
3754
+ status: `🧠 alt-agent: düşünme seviyesi '${from}' → '${to}' (otomatik uyum)`,
3755
+ }),
3756
+ onParamDrop: (p) =>
3757
+ emitSafe(this, sessionId, {
3758
+ type: 'status',
3759
+ status: `⚠ alt-agent: '${llm.PARAM_LABELS[p] || p}' kabul edilmedi → atlandı`,
3760
+ }),
3761
+ }
3735
3762
  );
3736
3763
  const assistant = { role: 'assistant', content: res.content || '' };
3737
3764
  if (res.toolCalls && res.toolCalls.length) assistant.tool_calls = res.toolCalls;
package/src/agent/llm.js CHANGED
@@ -5,7 +5,11 @@
5
5
  KOPMASINDA (fetch failed) çok daha sabırlı: 6 deneme + internet dönene
6
6
  dek bekleme (main'deki net izleyicisinden beslenir). Akış ortasında kopan
7
7
  bağlantıda: hiç veri akmadıysa sessiz baştan dener, kısmi metin geldiyse
8
- 'length' gibi döner → chatStreamAuto kaldığı yerden DEVAM ETTİRİR. */
8
+ 'length' gibi döner → chatStreamAuto kaldığı yerden DEVAM ETTİRİR.
9
+ Model reddettiği parametre/seviyeleri (reasoning_effort, temperature,
10
+ prompt_cache_key) 400'den otomatik öğrenir → model-caps.json'a kalıcı. */
11
+
12
+ const fs = require('fs');
9
13
 
10
14
  const RETRYABLE_STATUS = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
11
15
  const MAX_RETRIES = 3; /* sağlayıcı geçici HTTP hataları */
@@ -83,6 +87,173 @@ function wantsCacheKey(sel) {
83
87
  );
84
88
  }
85
89
 
90
+ /* ---------- model yetenek keşfi (opencode provider.metadata muadili) ----------
91
+ Her model her parametreyi kabul etmez: Mimo gibi bazıları reasoning_effort'u,
92
+ GPT-5/o1 ailesi temperature'ı reddeder. Sağlayıcı 400 "Invalid request
93
+ parameters" diyorsa parametre TEK TEK atlanıp sessizce yeniden denenir;
94
+ kabul edilen kombinasyon KALICI öğrenilir (model-caps.json) → sonraki
95
+ isteklerde reddedilen parametre hiç gönderilmez. models.dev metadata'sına
96
+ internet bağımlılığı olmadan çalışma-zamanında eşdeğer davranır. */
97
+
98
+ const CAPS_FLAGS = ['noReasoning', 'noTemperature', 'noCacheKey'];
99
+ const PARAM_LABELS = {
100
+ noReasoning: 'reasoning_effort',
101
+ noTemperature: 'temperature',
102
+ noCacheKey: 'prompt_cache_key',
103
+ };
104
+
105
+ let _capsFile = null;
106
+ const _caps = new Map(); /* "providerId::model" -> { noReasoning?, noTemperature?, noCacheKey? } */
107
+
108
+ function capsKey(sel) {
109
+ return String((sel && sel.providerId) || '') + '::' + String((sel && sel.model) || '');
110
+ }
111
+
112
+ function capsFor(sel) {
113
+ return _caps.get(capsKey(sel)) || null;
114
+ }
115
+
116
+ function resetCaps() {
117
+ _caps.clear();
118
+ }
119
+
120
+ /* caps dosyasını bağla: mevcut öğrenimler yüklenir; null → kapat */
121
+ function setCapsFile(p) {
122
+ _caps.clear();
123
+ _capsFile = typeof p === 'string' && p ? p : null;
124
+ if (!_capsFile) return;
125
+ try {
126
+ const j = JSON.parse(fs.readFileSync(_capsFile, 'utf8'));
127
+ for (const [k, v] of Object.entries(j.models || {})) {
128
+ if (v && typeof v === 'object') _caps.set(k, { ...v });
129
+ }
130
+ } catch {}
131
+ }
132
+
133
+ function learnCaps(sel, drops) {
134
+ const k = capsKey(sel);
135
+ const prev = _caps.get(k) || {};
136
+ const next = { ...prev };
137
+ let changed = false;
138
+ for (const f of CAPS_FLAGS) {
139
+ if (drops[f] && !prev[f]) {
140
+ next[f] = true;
141
+ changed = true;
142
+ }
143
+ }
144
+ /* reddedilen reasoning seviyeleri (["max","xhigh",...]) — seviye bazlı öğrenim */
145
+ if (Array.isArray(drops.failedEfforts) && drops.failedEfforts.length) {
146
+ const merged = [...(prev.failedEfforts || [])];
147
+ for (const e of drops.failedEfforts) {
148
+ if (!merged.includes(e)) merged.push(e);
149
+ }
150
+ if (merged.length !== (prev.failedEfforts || []).length) {
151
+ next.failedEfforts = merged;
152
+ changed = true;
153
+ }
154
+ }
155
+ if (!changed) return;
156
+ _caps.set(k, next);
157
+ if (!_capsFile) return;
158
+ try {
159
+ const all = {};
160
+ for (const [key, v] of _caps) all[key] = v;
161
+ fs.writeFileSync(_capsFile, JSON.stringify({ version: 1, models: all }, null, 2));
162
+ } catch {}
163
+ }
164
+
165
+ /* reasoning_effort merdiveni: reddedilen seviyede BİR ALT seviye denenir
166
+ (Mimo 2.5: max/xhigh reddediyor, low çalışıyor). low'un altı yok → null. */
167
+ const EFFORT_LOWER = { max: 'xhigh', xhigh: 'high', high: 'medium', medium: 'low', low: null };
168
+
169
+ /* Hangi hata metni "geçersiz istek" dili (Mimo: "Invalid request parameters").
170
+ friendlyError'ın kendi öneki ("HTTP 400 Bad Request") ile eşleşmemesine dikkat. */
171
+ const GENERIC_400_RE =
172
+ /invalid request param|invalid_param|unsupported|not supported|unknown (param|field|argument)|unrecognized|unexpected/i;
173
+
174
+ /* run(drops, body): verilen atlama seti + gövdeyle isteği çalıştırır.
175
+ 400 gelirse ve hata parametre reddine işaret ediyorsa:
176
+ 1) reasoning_effort önce SEVİYE düşürülerek denenir (max→xhigh→...→low),
177
+ en düşük de reddedilirse parametre tamamen bırakılır (noReasoning);
178
+ 2) sonra temperature, prompt_cache_key atlanır;
179
+ Kabul edilen kombinasyon bu model için KALICI öğrenilir → sonraki isteklerde
180
+ reddedilen seviye/parametre hiç gönderilmez (otomatik reasoning uyumu). */
181
+ async function withParamFallbacks(sel, body0, run, { signal, onParamDrop, onEffortDowngrade } = {}) {
182
+ const active = { ...(capsFor(sel) || {}) };
183
+ for (;;) {
184
+ /* seçilen seviyeyi öğrenilen redlere göre otomatik düşür */
185
+ let eff = body0.reasoningEffort ? String(body0.reasoningEffort) : null;
186
+ if (eff && !active.noReasoning) {
187
+ const failed = active.failedEfforts || [];
188
+ let guard = 0;
189
+ while (eff && failed.includes(eff) && guard++ < 5) eff = EFFORT_LOWER[eff] || null;
190
+ }
191
+ if (active.noReasoning) eff = null;
192
+ const body = eff && eff !== body0.reasoningEffort ? { ...body0, reasoningEffort: eff } : body0;
193
+ let res;
194
+ try {
195
+ res = await run({ ...active }, body);
196
+ learnCaps(sel, active);
197
+ return res;
198
+ } catch (e) {
199
+ if (!e || e.status !== 400 || (signal && signal.aborted)) throw e;
200
+ const msg = String(e.message || '');
201
+ const generic = GENERIC_400_RE.test(msg);
202
+ const reasonHint = /reason|effort|thinking/i.test(msg);
203
+ const tempHint = /temperature/i.test(msg);
204
+
205
+ /* denenecek atlamalar öncelik sırasıyla: önce reasoning merdiveni
206
+ (seviye düşür — Mimo: max reddedilir, low çalışır), sonra diğerleri */
207
+ const actions = [];
208
+ if ((reasonHint || generic) && body0.reasoningEffort && !active.noReasoning) actions.push('reasoning');
209
+ if ((generic || tempHint) && !active.noTemperature) actions.push('temperature');
210
+ if ((generic || /cache/i.test(msg)) && body0.cacheKey && wantsCacheKey(sel) && !active.noCacheKey) {
211
+ actions.push('cacheKey');
212
+ }
213
+ const act = actions[0];
214
+ if (!act) throw e;
215
+
216
+ if (act === 'reasoning') {
217
+ const cur = eff;
218
+ const lower = cur && cur in EFFORT_LOWER ? EFFORT_LOWER[cur] : null;
219
+ /* merdiven: generic red (gerçek sebep bilinmiyor → seviyeleri dene)
220
+ ya da red mesajı SEVİYE değerini zikrediyorsa ("effort 'max' invalid").
221
+ Mesaj parametrenin kendisini reddediyorsa ("not supported") direkt bırak. */
222
+ const valueQuoted = cur ? (msg.includes(`'${cur}'`) || msg.includes(`"${cur}"`)) : false;
223
+ /* ipucu parametrenin kendisini reddediyorsa merdivene gerek yok */
224
+ const ladder = valueQuoted || (generic && !reasonHint);
225
+ if (cur && lower && ladder) {
226
+ /* seviye reddedildi → bir alt seviyeye in, kalıcı öğren */
227
+ if (!Array.isArray(active.failedEfforts)) active.failedEfforts = [];
228
+ if (!active.failedEfforts.includes(cur)) {
229
+ active.failedEfforts.push(cur);
230
+ learnCaps(sel, active);
231
+ }
232
+ try {
233
+ onEffortDowngrade && onEffortDowngrade(cur, lower);
234
+ } catch {}
235
+ } else {
236
+ /* parametre tamamen reddedildi → bir daha gönderme */
237
+ active.noReasoning = true;
238
+ try {
239
+ onParamDrop && onParamDrop('noReasoning');
240
+ } catch {}
241
+ }
242
+ } else if (act === 'temperature') {
243
+ active.noTemperature = true;
244
+ try {
245
+ onParamDrop && onParamDrop('noTemperature');
246
+ } catch {}
247
+ } else if (act === 'cacheKey') {
248
+ active.noCacheKey = true;
249
+ try {
250
+ onParamDrop && onParamDrop('noCacheKey');
251
+ } catch {}
252
+ }
253
+ }
254
+ }
255
+ }
256
+
86
257
  /* Retry-After başlığı: saniye ("2"), milisaniye ("120ms" / retry-after-ms)
87
258
  ya da HTTP-tarih olabilir (opencode session/retry.ts port) */
88
259
  function parseRetryAfter(v) {
@@ -157,21 +328,25 @@ async function withRetries(fn, { signal, onRetry } = {}) {
157
328
  }
158
329
  }
159
330
 
160
- async function openChat(sel, body, { stream, signal, omitReasoning } = {}) {
331
+ async function openChat(sel, body, { stream, signal, drops } = {}) {
332
+ const d = drops || {};
161
333
  const payload = {
162
334
  model: sel.model,
163
335
  messages: body.messages,
164
336
  stream,
165
337
  ...(body.tools && body.tools.length ? { tools: body.tools } : {}),
166
- temperature: body.temperature ?? 0.6,
167
338
  };
339
+ if (!d.noTemperature) {
340
+ payload.temperature = body.temperature ?? 0.6;
341
+ }
168
342
  /* Düşünme (reasoning) seviyesi: OpenAI-style reasoning_effort —
169
- OpenRouter ve GPT-5 ailesi dahil uyumlu sağlayıcılar kabul eder. */
170
- if (body.reasoningEffort && !omitReasoning) {
343
+ OpenRouter ve GPT-5 ailesi dahil uyumlu sağlayıcılar kabul eder.
344
+ Kabul etmeyenlerde (Mimo vb.): 400 parametre atlanır + KALICI öğrenilir. */
345
+ if (body.reasoningEffort && !d.noReasoning) {
171
346
  payload.reasoning_effort = String(body.reasoningEffort);
172
347
  }
173
348
  /* prompt cache anahtarı: oturum başına sabit → sağlayıcı önek önbelleği tutar */
174
- if (body.cacheKey && wantsCacheKey(sel)) {
349
+ if (body.cacheKey && wantsCacheKey(sel) && !d.noCacheKey) {
175
350
  payload.prompt_cache_key = String(body.cacheKey);
176
351
  }
177
352
  return fetch(sel.url, {
@@ -187,24 +362,18 @@ async function openChat(sel, body, { stream, signal, omitReasoning } = {}) {
187
362
  });
188
363
  }
189
364
 
190
- async function chatStream(sel, body, { signal, onDelta, onRetry } = {}) {
191
- try {
192
- return await streamOnce(sel, body, { signal, onDelta });
193
- } catch (e) {
194
- /* Model reasoning_effort'u desteklemiyorsa (400) parametreyi atlayıp
195
- TEK seferlik sessiz retry — kullanıcı hatayı görmez. */
196
- if (
197
- body.reasoningEffort &&
198
- e && e.status === 400 &&
199
- /reason|effort|thinking/i.test(String(e.message || ''))
200
- ) {
201
- return await streamOnce(sel, body, { signal, onDelta }, true);
202
- }
203
- throw e;
204
- }
365
+ async function chatStream(sel, body, { signal, onDelta, onRetry, onParamDrop, onEffortDowngrade } = {}) {
366
+ /* 400 parametre/seviye reddinde: otomatik uyum (seviye düşür → parametre atla),
367
+ başarılı set kalıcı öğrenilir */
368
+ return withParamFallbacks(
369
+ sel,
370
+ body,
371
+ (drops, b) => streamOnce(sel, b, { signal, onDelta }, drops),
372
+ { signal, onParamDrop, onEffortDowngrade }
373
+ );
205
374
  }
206
375
 
207
- async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReasoning = false) {
376
+ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, drops = {}) {
208
377
  /* akış durumu dışarıda tutulur: gövde okunurken bağlantı koparsa
209
378
  ne kadarı geldiğine bakılır (kısmi metin → devam; boş/araç → baştan) */
210
379
  const state = { content: '', reasoning: '', toolCalls: [], usage: null, finishReason: null };
@@ -213,7 +382,7 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReas
213
382
  for (;;) {
214
383
  const res = await withRetries(
215
384
  async () => {
216
- const r = await openChat(sel, body, { stream: true, signal, omitReasoning });
385
+ const r = await openChat(sel, body, { stream: true, signal, drops });
217
386
  if (!r.ok) {
218
387
  let detail = '';
219
388
  try {
@@ -385,25 +554,31 @@ async function chatStreamAuto(sel, body, opts = {}) {
385
554
  return last;
386
555
  }
387
556
 
388
- async function chatOnce(sel, body, { signal, onRetry } = {}) {
389
- const res = await withRetries(
390
- async () => {
391
- const r = await openChat(sel, body, { stream: false, signal });
392
- if (!r.ok) {
393
- let detail = '';
394
- try {
395
- detail = (await r.text()).slice(0, 300);
396
- } catch {}
397
- const err = new Error(friendlyError(r.status, r.statusText, detail));
398
- err.status = r.status;
399
- err.retryAfterMs = parseRetryAfter(
400
- r.headers && (r.headers.get('retry-after-ms') || r.headers.get('retry-after'))
401
- );
402
- throw err;
403
- }
404
- return r;
405
- },
406
- { signal, onRetry }
557
+ async function chatOnce(sel, body, { signal, onRetry, onParamDrop, onEffortDowngrade } = {}) {
558
+ const res = await withParamFallbacks(
559
+ sel,
560
+ body,
561
+ (drops, b) =>
562
+ withRetries(
563
+ async () => {
564
+ const r = await openChat(sel, b, { stream: false, signal, drops });
565
+ if (!r.ok) {
566
+ let detail = '';
567
+ try {
568
+ detail = (await r.text()).slice(0, 300);
569
+ } catch {}
570
+ const err = new Error(friendlyError(r.status, r.statusText, detail));
571
+ err.status = r.status;
572
+ err.retryAfterMs = parseRetryAfter(
573
+ r.headers && (r.headers.get('retry-after-ms') || r.headers.get('retry-after'))
574
+ );
575
+ throw err;
576
+ }
577
+ return r;
578
+ },
579
+ { signal, onRetry }
580
+ ),
581
+ { signal, onParamDrop, onEffortDowngrade }
407
582
  );
408
583
  const json = await res.json();
409
584
  const msg = json.choices?.[0]?.message || {};
@@ -421,9 +596,14 @@ module.exports = {
421
596
  chatStreamAuto,
422
597
  chatOnce,
423
598
  withRetries,
599
+ withParamFallbacks,
424
600
  friendlyError,
425
601
  sumUsage,
426
602
  setNetProbe,
603
+ setCapsFile,
604
+ capsFor,
605
+ resetCaps,
606
+ PARAM_LABELS,
427
607
  isNetworkError,
428
608
  wantsCacheKey,
429
609
  parseRetryAfter,