natureco-cli 5.23.0 → 5.26.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/CHANGELOG.md +37 -0
- package/package.json +1 -1
- package/src/commands/repl.js +44 -13
- package/src/tools/agentic-runner.js +322 -0
- package/src/tools/workflow.js +179 -91
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to NatureCo CLI will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [5.26.0] - 2026-07-04 — "CANLI STREAMING + KEŞFET→DÜZENLE→DOĞRULA"
|
|
6
|
+
|
|
7
|
+
### ✨ Yeni
|
|
8
|
+
- **Canlı streaming (TTY)**: yanit artik token token akarak gelir (Hermes/Claude Code hissi). SSE → tool-call/skill XML'ini gizleyen filtre → model-adi sanitizer ("Ben MiniMax"→"Ben {bot}") zinciri; kelime chunk sinirinda bolunse bile sizinti yok. Non-TTY (pipe/CI) bloklu yola duser. `makeStreamFilter` + `makeSanitizeStream` (birim testli).
|
|
9
|
+
- **Keşfet→düzenle→doğrula**: ajana `file_search` (glob), `list_dir` ve MEVCUT dosyayi hedefli degistiren `edit_file` eklendi; sistem prompt'u "once oku, sonra edit_file ile duzenle, sonra calistirip dogrula" akisini ogretir. E2E dogrulandi (dosyayi oku→edit→node ile calistir→sonucu raporla).
|
|
10
|
+
|
|
11
|
+
### 🐛 Düzeltme
|
|
12
|
+
- **Okuma araclari icerigi modele donmuyordu**: `read_file`/`list_dir`/`file_search`/`bash` sonucu ajana sadece "OK" olarak donuyordu → model icerigi goremeyip "okudum ama bos" diye takiliyordu. Feedback artik gercek ciktiyi (content/output/results) modele verir.
|
|
13
|
+
|
|
14
|
+
Doğrulama: MiniMax ile E2E (oku→edit→calistir; streaming XML/model-adi sizintisiz); **484 test yeşil**, ESLint temiz.
|
|
15
|
+
|
|
16
|
+
## [5.25.0] - 2026-07-04 — "PROVIDER-AGNOSTİK + STREAMING ALTYAPISI"
|
|
17
|
+
|
|
18
|
+
### ✨ İyileştirme
|
|
19
|
+
- **Provider-agnostik**: agentic düzeltmeler MiniMax'e özel DEĞİL — `workflow.js`'in paylaşılan non-tool-calling dalında çalışır (MiniMax, Groq, Ollama, yerel modeller). Tool-calling sağlayıcıları (OpenAI, Anthropic, Gemini) kendi native `tool_calls` yolunu kullanır (dokunulmadı). `agentic-runner` hem native `tool_calls` hem native XML parse eder → sağlayıcıdan bağımsız çalışır.
|
|
20
|
+
- **Streaming altyapısı**: canlı akışta tool-call/skill protokol jetonlarını (`<minimax:tool_call>`, `<invoke>`, `<skill>`) gizleyip düz metni gösteren, chunk sınırında bölünen tag'leri doğru işleyen akış filtresi (`makeStreamFilter`, 4 test). Tam devreye alma (streaming-güvenli model-adı temizleme ile birlikte) sonraki adım.
|
|
21
|
+
|
|
22
|
+
> Not: v5.24.0'daki büyük düzeltmeler (MiniMax dosya yazma, komut çalıştırma, memory recall) bu sürümde de yer alır.
|
|
23
|
+
|
|
24
|
+
## [5.24.0] - 2026-07-04 — "AGENTIC SERTLEŞTİRME" (MiniMax native tool-call + komut çalıştırma)
|
|
25
|
+
|
|
26
|
+
Kök neden: **MiniMax M2.5 agentic bir model** — tool call'ları OpenAI `tool_calls` JSON'u yerine metin içinde native XML olarak üretir (`<minimax:tool_call><invoke name="write_file">...`) ve skill'i `<skill>ad</skill>` ile yükler. 5.23.0'ın tek-atış JSON planı bunu yakalayamıyordu; `JSON.parse` patlayıp boş `catch{}` yutunca dosya sessizce yazılmıyordu ("masaüstünde yarış oyunu yapamadı").
|
|
27
|
+
|
|
28
|
+
### ✨ Yeni
|
|
29
|
+
- **`src/tools/agentic-runner.js`**: MiniMax'in native XML/skill protokolünü parse edip gerçek araçları çalıştıran bounded agentic döngü (parse→execute→sonucu geri besle→dur, max 15 iterasyon). `workflow.js`'in non-tool-calling dalı buna bağlandı. 13 yeni birim testi.
|
|
30
|
+
- **Komut çalıştırma (onaylı)**: ajan artık `bash` ile npm/git/node/test çalıştırıp çıktıya göre devam edebilir — gerçek **yaz→çalıştır→test→düzelt** döngüsü.
|
|
31
|
+
|
|
32
|
+
### 🐛 Düzeltme
|
|
33
|
+
- **MiniMax dosya yazma**: native `<invoke>`/`<skill>` parse edilip gerçek araçlara yönlendiriliyor; büyük içerikteki JSON-kaçış sorunu ortadan kalktı.
|
|
34
|
+
- **Memory recall split-brain**: hafıza `default.json`'da tutulurken okuyucular `<userName>.json` arıyordu → chat/code kullanıcıyı hiç hatırlamıyordu ("adım ne?"→"bilmiyorum"). `loadUserMemory` + `repl.loadMemory` artık `<user>.json` + legacy `default.json`'ı birleştirir (isim-eşleşme guard'ı).
|
|
35
|
+
- **Bot personası**: jenerik "Asistan" placeholder'ı gerçek persona (örn. "Hinata") ile eziliyor.
|
|
36
|
+
|
|
37
|
+
### 🔒 Güvenlik
|
|
38
|
+
- **Ajan modunda yıkıcı komut guard'ı**: varsayılan 'full' politika `rm -rf /`'i bile geçiriyordu (insan için bilinçli olabilir, model için değil). Agentic-runner artık `isDangerousCommand`'ı politikadan bağımsız uygular — yıkıcı komutlar ajan tarafından **çalıştırılmaz**. Diğer ~85 araç allowlist dışı (yalnızca write_file/read_file/edit_file/skill_view/bash).
|
|
39
|
+
|
|
40
|
+
Doğrulama: gerçek MiniMax API ile uçtan uca (yarış oyunu + "node script yaz&çalıştır"→42); **474 test yeşil**.
|
|
41
|
+
|
|
5
42
|
## [5.23.0] - 2026-07-02 — "NON-TOOL-CALLING FIX" (MiniMax dosya yazma)
|
|
6
43
|
|
|
7
44
|
### 🐛 Düzeltme
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "natureco-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.26.0",
|
|
4
4
|
"description": "OpenClaw'dan daha güvenli, daha hızlı, daha ucuz AI agent CLI. Multi-agent, self-evolving skills, audit log, maliyet optimizasyonu ve NatureCo platform-native.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"natureco": "bin/natureco.js"
|
package/src/commands/repl.js
CHANGED
|
@@ -145,11 +145,36 @@ function getConfig() {
|
|
|
145
145
|
const { isMiniMax, isGemini, buildChatEndpoint } = require('../utils/provider-detect');
|
|
146
146
|
|
|
147
147
|
function loadMemory(username) {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
const uname = (username || 'default').toLowerCase();
|
|
149
|
+
const base = { name: username || 'Kullanıcı', nickname: null, botName: null, facts: [], preferences: [], history: [] };
|
|
150
|
+
const readJson = (f) => { try { return fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, 'utf8')) : null; } catch { return null; } };
|
|
151
|
+
|
|
152
|
+
const userMem = readJson(path.join(MEMORY_DIR, `${uname}.json`));
|
|
153
|
+
const merged = userMem
|
|
154
|
+
? { ...base, ...userMem, facts: [...(userMem.facts || [])], preferences: [...(userMem.preferences || [])], history: [...(userMem.history || [])] }
|
|
155
|
+
: { ...base };
|
|
156
|
+
|
|
157
|
+
// Legacy default.json'i birlestir (isim eslesiyorsa ya da isimsizse): eski kurulumlarda
|
|
158
|
+
// hafiza + bot personasi default.json'da kalmis olabiliyordu; <user>.json ile split-brain
|
|
159
|
+
// olusuyordu. Birlestirince recall calisir ve ilk kayitta konsolide olur.
|
|
160
|
+
if (uname !== 'default') {
|
|
161
|
+
const def = readJson(path.join(MEMORY_DIR, 'default.json'));
|
|
162
|
+
if (def && (!def.name || String(def.name).toLowerCase() === uname)) {
|
|
163
|
+
// Jenerik "Asistan" placeholder'ini gercek persona (orn. Hinata) ile ez
|
|
164
|
+
const isGeneric = (b) => !b || /^asistan$/i.test(String(b));
|
|
165
|
+
if (isGeneric(merged.botName) && def.botName && !isGeneric(def.botName)) merged.botName = def.botName;
|
|
166
|
+
if ((!merged.name || merged.name === 'Kullanıcı') && def.name) merged.name = def.name;
|
|
167
|
+
const factVal = (f) => ((f && (f.value != null ? f.value : f)) || '').toString().trim();
|
|
168
|
+
const seen = new Set(merged.facts.map(f => factVal(f).toLowerCase()));
|
|
169
|
+
for (const f of (def.facts || [])) {
|
|
170
|
+
const v = factVal(f);
|
|
171
|
+
if (v && !seen.has(v.toLowerCase())) { seen.add(v.toLowerCase()); merged.facts.push(f); }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!merged.botName) merged.botName = 'Asistan';
|
|
177
|
+
return merged;
|
|
153
178
|
}
|
|
154
179
|
|
|
155
180
|
function saveMemory(username, memory) {
|
|
@@ -1468,16 +1493,20 @@ async function startRepl(args) {
|
|
|
1468
1493
|
messages[0] = { role: 'system', content: systemPrompt, _internal: true };
|
|
1469
1494
|
|
|
1470
1495
|
// v5.13.0: Run workflow FIRST for every request
|
|
1471
|
-
|
|
1496
|
+
// v5.26: TTY'de canli streaming — spinner yerine yanit akarak gelir (workflow stdout'a akitir)
|
|
1497
|
+
const wantStream = !!(process.stdout && process.stdout.isTTY);
|
|
1498
|
+
if (!wantStream) process.stdout.write(tui.styled('\r 🔧 workflow... ', { color: tui.PALETTE.muted }));
|
|
1472
1499
|
const wfToolDefs = getToolDefs();
|
|
1473
1500
|
const recentHistory = messages.length > 1 ? messages.slice(-10) : [];
|
|
1474
|
-
const wfResult = await executeTool('workflow', { action: 'run', task: line, conversationHistory: recentHistory }, wfToolDefs);
|
|
1501
|
+
const wfResult = await executeTool('workflow', { action: 'run', task: line, conversationHistory: recentHistory, stream: wantStream }, wfToolDefs);
|
|
1475
1502
|
const wf = wfResult?.result || {};
|
|
1476
|
-
if (wf.
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1503
|
+
if (!wf.streamed) {
|
|
1504
|
+
if (wf.success !== false) {
|
|
1505
|
+
const loaded = wf.skillsLoaded && wf.skillsLoaded.length > 0 ? ` [skill: ${wf.skillsLoaded.join(', ')}]` : '';
|
|
1506
|
+
process.stdout.write(tui.styled(` ✓ workflow${loaded}\n`, { color: tui.PALETTE.success }));
|
|
1507
|
+
} else {
|
|
1508
|
+
process.stdout.write(tui.styled(' ✗ workflow\n', { color: tui.PALETTE.danger }));
|
|
1509
|
+
}
|
|
1481
1510
|
}
|
|
1482
1511
|
|
|
1483
1512
|
if (wf.passthrough && wf.reply !== undefined && wf.reply !== null) {
|
|
@@ -1497,7 +1526,9 @@ async function startRepl(args) {
|
|
|
1497
1526
|
fixedReply = fixedReply.replace(/Ben\s+GPT[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
|
|
1498
1527
|
fixedReply = fixedReply.replace(/Ben\s+Asistan[\s\w\.]*/gi, 'Ben ' + displayBotName);
|
|
1499
1528
|
fixedReply = fixedReply.replace(/\*\*(?:MiniMax|Claude|GPT|M2\.5|M2)[^\*]*\*\*/gi, '**' + displayBotName + '**');
|
|
1500
|
-
|
|
1529
|
+
// streamed ise yanit zaten canli basildi (sanitize edilerek) — tekrar basma
|
|
1530
|
+
if (wf.streamed) process.stdout.write('\n');
|
|
1531
|
+
else process.stdout.write('\n' + fixedReply + '\n');
|
|
1501
1532
|
messages.push({ role: 'assistant', content: fixedReply });
|
|
1502
1533
|
totalInputTokens += Math.ceil(line.length / 4);
|
|
1503
1534
|
totalOutputTokens += Math.ceil(fullReply.length / 4);
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentic-runner — "agentic-text" modeller (MiniMax M2.x gibi) icin bounded tool döngüsü.
|
|
3
|
+
*
|
|
4
|
+
* Neden: MiniMax M2.5 tool call'larini OpenAI tarzi `message.tool_calls` JSON'u yerine
|
|
5
|
+
* dogrudan metin icinde native XML olarak uretir:
|
|
6
|
+
* <minimax:tool_call><invoke name="write_file"><parameter name="path">...</parameter>
|
|
7
|
+
* <parameter name="content">...</parameter></invoke></minimax:tool_call>
|
|
8
|
+
* Ayrica skill'leri <skill>ad</skill> ile yuklemek ister. Eski passthrough bunlari
|
|
9
|
+
* hic islemedigi icin (JSON.parse patliyor, bos catch yutuyor) dosya asla yazilmiyordu.
|
|
10
|
+
*
|
|
11
|
+
* Bu modul o XML'i parse eder, gercek araclari (write_file/read_file/edit_file/skill_view)
|
|
12
|
+
* calistirir, sonuclari modele geri besler ve is bitene kadar (ya da MAX adima kadar) doner.
|
|
13
|
+
*
|
|
14
|
+
* Guvenlik: sadece allowlist'teki araclar calisir. bash/shell gibi keyfi komut calistirma
|
|
15
|
+
* bu modda KAPALIDIR (onay katmanini atlamamak icin).
|
|
16
|
+
*/
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
|
|
20
|
+
// Agentic dongude izin verilen araclar. bash BURADA ama guvenli: bash.js kendi
|
|
21
|
+
// icinde approvals politikasini uyguluyor (isSafeCommand → direkt; tehlikeli → red;
|
|
22
|
+
// digerleri → allowlist/full moda gore). Yani keyfi/yikici komut calismaz.
|
|
23
|
+
// Diger ~85 arac (discord, telegram, cron, browser...) bilerek DISARIDA.
|
|
24
|
+
const DEFAULT_ALLOWED = ['write_file', 'read_file', 'edit_file', 'skill_view', 'bash', 'file_search', 'list_dir'];
|
|
25
|
+
|
|
26
|
+
const TOOL_ALIASES = {
|
|
27
|
+
write_file: 'write_file', create_file: 'write_file', writefile: 'write_file', write: 'write_file', create: 'write_file', save_file: 'write_file', new_file: 'write_file',
|
|
28
|
+
read_file: 'read_file', readfile: 'read_file', view_file: 'read_file', open_file: 'read_file', cat: 'read_file',
|
|
29
|
+
edit_file: 'edit_file', editfile: 'edit_file', str_replace: 'edit_file', str_replace_editor: 'edit_file', replace_in_file: 'edit_file',
|
|
30
|
+
skill_view: 'skill_view', skillview: 'skill_view', load_skill: 'skill_view', view_skill: 'skill_view', skill: 'skill_view',
|
|
31
|
+
bash: 'bash', run_command: 'bash', shell: 'bash', shell_command: 'bash', exec: 'bash', run_terminal: 'bash', terminal: 'bash', run: 'bash', command: 'bash',
|
|
32
|
+
file_search: 'file_search', glob: 'file_search', find_files: 'file_search', search_files: 'file_search', find: 'file_search',
|
|
33
|
+
list_dir: 'list_dir', ls: 'list_dir', dir: 'list_dir', list_directory: 'list_dir', filesystem: 'list_dir',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function expandHome(p) {
|
|
37
|
+
if (!p || typeof p !== 'string') return p;
|
|
38
|
+
if (p.startsWith('~')) return path.join(os.homedir(), p.slice(1));
|
|
39
|
+
return p;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Ajanin urettigi komutlar icin ekstra koruma. bash.js kendi politikasini uygular
|
|
43
|
+
// ama varsayilan 'full' mod yikici komutlari (rm -rf gibi) bile gecirir — bu insan
|
|
44
|
+
// icin bilincli olabilir, ama MODELIN urettigi komut icin degil. Bu yuzden agentic
|
|
45
|
+
// yolda yikici komutlari politikadan BAGIMSIZ olarak engelliyoruz.
|
|
46
|
+
function defaultIsDangerous(cmd) {
|
|
47
|
+
try { return require('../utils/approvals').isDangerousCommand(cmd); } catch { return false; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Model metninden agentic tool cagrilarini cikar.
|
|
52
|
+
* Destekler: <invoke name><parameter name> (opsiyonel <minimax:tool_call> sarmali),
|
|
53
|
+
* <skill>ad</skill> kisayolu, ve yan yana gelen native OpenAI tool_calls.
|
|
54
|
+
*/
|
|
55
|
+
function parseAgenticCalls(content, nativeToolCalls) {
|
|
56
|
+
const calls = [];
|
|
57
|
+
content = content || '';
|
|
58
|
+
|
|
59
|
+
for (const tc of nativeToolCalls || []) {
|
|
60
|
+
const fn = tc.function || {};
|
|
61
|
+
if (!fn.name) continue;
|
|
62
|
+
let args = {};
|
|
63
|
+
try { args = typeof fn.arguments === 'string' ? JSON.parse(fn.arguments || '{}') : (fn.arguments || {}); } catch {}
|
|
64
|
+
calls.push({ tool: fn.name, args });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const invokeRe = /<invoke\s+name=["']([^"']+)["']\s*>([\s\S]*?)<\/invoke>/g;
|
|
68
|
+
let m;
|
|
69
|
+
while ((m = invokeRe.exec(content)) !== null) {
|
|
70
|
+
const tool = m[1].trim();
|
|
71
|
+
const body = m[2];
|
|
72
|
+
const args = {};
|
|
73
|
+
const paramRe = /<parameter\s+name=["']([^"']+)["']\s*>([\s\S]*?)<\/parameter>/g;
|
|
74
|
+
let pm;
|
|
75
|
+
while ((pm = paramRe.exec(body)) !== null) {
|
|
76
|
+
args[pm[1].trim()] = pm[2];
|
|
77
|
+
}
|
|
78
|
+
calls.push({ tool, args });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const skillRe = /<skill>\s*([^<>\n]+?)\s*<\/skill>/g;
|
|
82
|
+
while ((m = skillRe.exec(content)) !== null) {
|
|
83
|
+
const name = m[1].trim();
|
|
84
|
+
if (!calls.some(c => /skill/i.test(c.tool || '') && c.args && c.args.name === name)) {
|
|
85
|
+
calls.push({ tool: 'skill_view', args: { name } });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return calls;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Final yanittan protokol jetonlarini temizle (kullaniciya gosterim icin). */
|
|
93
|
+
function stripProtocolTokens(s) {
|
|
94
|
+
return (s || '')
|
|
95
|
+
.replace(/<minimax:tool_call>[\s\S]*?<\/minimax:tool_call>/g, '')
|
|
96
|
+
.replace(/<invoke[\s\S]*?<\/invoke>/g, '')
|
|
97
|
+
.replace(/<skill>[\s\S]*?<\/skill>/g, '')
|
|
98
|
+
.replace(/<available_skills>[\s\S]*?<\/available_skills>/g, '')
|
|
99
|
+
.replace(/<\/?(minimax:tool_call|available_skills|available_skins)>/g, '')
|
|
100
|
+
.trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Streaming ekran filtresi: model canli akarken duz metni goster, ama tool-call/skill
|
|
105
|
+
* protokol bloklarini (<minimax:tool_call>, <invoke>, <skill>, <available_skills>)
|
|
106
|
+
* kullaniciya HAM gosterme. Bir protokol jetonu gorunce o tur icin gostermeyi keser
|
|
107
|
+
* (icerik yine tam biriktirilir; parse sonra yapilir). Kismi tag chunk sinirinda
|
|
108
|
+
* bolunebilir — sonda olasi kismi-tag kuyrugunu tutar.
|
|
109
|
+
*
|
|
110
|
+
* const f = makeStreamFilter(t => process.stdout.write(t), () => process.stdout.write(' 🔧'));
|
|
111
|
+
* f.push(deltaChunk); ... ; f.end();
|
|
112
|
+
*/
|
|
113
|
+
function makeStreamFilter(onText, onTool) {
|
|
114
|
+
const MARKERS = ['<minimax:tool_call', '<invoke', '<skill>', '<available_skills'];
|
|
115
|
+
let buf = '';
|
|
116
|
+
let suppressed = false;
|
|
117
|
+
return {
|
|
118
|
+
push(chunk) {
|
|
119
|
+
if (suppressed) return;
|
|
120
|
+
buf += (chunk || '');
|
|
121
|
+
let idx = -1;
|
|
122
|
+
for (const mk of MARKERS) { const i = buf.indexOf(mk); if (i !== -1 && (idx === -1 || i < idx)) idx = i; }
|
|
123
|
+
if (idx !== -1) {
|
|
124
|
+
if (idx > 0 && onText) onText(buf.slice(0, idx));
|
|
125
|
+
buf = '';
|
|
126
|
+
suppressed = true;
|
|
127
|
+
if (onTool) onTool();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const lastLt = buf.lastIndexOf('<');
|
|
131
|
+
if (lastLt === -1) { if (buf && onText) onText(buf); buf = ''; return; }
|
|
132
|
+
if (lastLt > 0 && onText) onText(buf.slice(0, lastLt));
|
|
133
|
+
buf = buf.slice(lastLt);
|
|
134
|
+
// buf artik '<' ile basliyor; bir marker'a buyuyebilecekse tut, degilse yaz (orn. literal <div>)
|
|
135
|
+
if (!MARKERS.some(mk => mk.startsWith(buf))) { if (onText) onText(buf); buf = ''; }
|
|
136
|
+
},
|
|
137
|
+
end() { if (!suppressed && buf && onText) onText(buf); buf = ''; return suppressed; },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Streaming model-adi temizleyici: canli akarken model kendi adini soylerse
|
|
143
|
+
* (MiniMax/Claude/GPT...) persona adiyla degistir. Kelime sinirinda (bosluk)
|
|
144
|
+
* emit eder — boylece bir chunk'ta "Mini", digerinde "Max" gelse bile kelime
|
|
145
|
+
* tam olusmadan gonderilmez, sizinti olmaz. onText'e temizlenmis metin gider.
|
|
146
|
+
*/
|
|
147
|
+
function makeSanitizeStream(botName, onText) {
|
|
148
|
+
const name = botName || 'Asistan';
|
|
149
|
+
let buf = '';
|
|
150
|
+
const clean = (s) => s
|
|
151
|
+
.replace(/\bMiniMaxi?\b/gi, name)
|
|
152
|
+
.replace(/\bChatGPT\b/gi, name)
|
|
153
|
+
.replace(/\bGPT-?[0-9.]*\b/gi, name)
|
|
154
|
+
.replace(/\bClaude(?:\s*[0-9.]+)?\b/gi, name)
|
|
155
|
+
.replace(/\bM2\.5\b/gi, name);
|
|
156
|
+
return {
|
|
157
|
+
push(chunk) {
|
|
158
|
+
buf += (chunk || '');
|
|
159
|
+
const lastWs = Math.max(buf.lastIndexOf(' '), buf.lastIndexOf('\n'), buf.lastIndexOf('\t'));
|
|
160
|
+
if (lastWs >= 0) {
|
|
161
|
+
const safe = buf.slice(0, lastWs + 1);
|
|
162
|
+
buf = buf.slice(lastWs + 1);
|
|
163
|
+
if (onText) onText(clean(safe));
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
end() { if (buf && onText) onText(clean(buf)); buf = ''; },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function sanitizeArgs(args) {
|
|
171
|
+
const a = { ...args };
|
|
172
|
+
if (typeof a.content === 'string' && a.content.length > 160) a.content = `[${a.content.length} chars]`;
|
|
173
|
+
if (typeof a.new_string === 'string' && a.new_string.length > 160) a.new_string = `[${a.new_string.length} chars]`;
|
|
174
|
+
return a;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Modele geri beslenecek metin. KRITIK: read_file/list_dir/file_search/bash gibi
|
|
178
|
+
// araclarda modelin GORMESI gereken asil cikti (content/output/results/items) geri
|
|
179
|
+
// verilmeli — yoksa model "okudum ama icerik yok" diye takilir.
|
|
180
|
+
function buildFeedback(norm, res) {
|
|
181
|
+
if (typeof res === 'string') return `${norm} sonucu:\n` + res.slice(0, 2500);
|
|
182
|
+
const ok = res && res.success !== false;
|
|
183
|
+
if (!ok) return `${norm} HATA: ${(res && res.error) || 'bilinmeyen hata'}`;
|
|
184
|
+
if (norm === 'write_file') return `write_file OK: ${res.path || ''} (${res.size != null ? res.size : '?'} bytes)`;
|
|
185
|
+
let body = '';
|
|
186
|
+
if (res.content != null) body = String(res.content).slice(0, 2500);
|
|
187
|
+
else if (res.output != null) body = String(res.output).slice(0, 2000);
|
|
188
|
+
else if (res.stdout != null) body = String(res.stdout).slice(0, 2000);
|
|
189
|
+
else if (res.results != null) body = JSON.stringify(res.results).slice(0, 2000);
|
|
190
|
+
else if (res.items != null) body = JSON.stringify(res.items).slice(0, 2000);
|
|
191
|
+
const head = `${norm} OK` + (res.path ? ` (${res.path})` : '');
|
|
192
|
+
return body ? `${head}:\n${body}` : head;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Tek bir parse edilmis cagriyi calistir.
|
|
197
|
+
* Donus: { records: [...], feedback: '...' }
|
|
198
|
+
*/
|
|
199
|
+
async function executeCall(call, opts = {}) {
|
|
200
|
+
const toolsDir = opts.toolsDir || __dirname;
|
|
201
|
+
const loadTool = opts.loadTool || ((n) => require(path.join(toolsDir, n + '.js')));
|
|
202
|
+
const allowed = opts.allowed || new Set(DEFAULT_ALLOWED);
|
|
203
|
+
const rawTool = (call.tool || '').trim();
|
|
204
|
+
const norm = TOOL_ALIASES[rawTool.toLowerCase()] || rawTool;
|
|
205
|
+
const records = [];
|
|
206
|
+
const feedbacks = [];
|
|
207
|
+
|
|
208
|
+
// "files" JSON dizisi (bulk-file-operations veya files parametresi) → coklu write_file
|
|
209
|
+
let files = call.args && call.args.files;
|
|
210
|
+
if (typeof files === 'string') { try { files = JSON.parse(files); } catch { files = null; } }
|
|
211
|
+
if (Array.isArray(files) && (/bulk/i.test(rawTool) || /file/i.test(rawTool) || norm === 'write_file')) {
|
|
212
|
+
let wf;
|
|
213
|
+
try { wf = loadTool('write_file'); } catch { wf = null; }
|
|
214
|
+
for (const f of files) {
|
|
215
|
+
if (!wf || !f || !f.path) { records.push({ tool: 'write_file', status: 'error', error: 'gecersiz dosya girisi' }); feedbacks.push('write_file HATA: gecersiz giris'); continue; }
|
|
216
|
+
try {
|
|
217
|
+
const res = await wf.execute({ path: expandHome(f.path), content: f.content != null ? String(f.content) : '' });
|
|
218
|
+
const ok = res && res.success !== false;
|
|
219
|
+
records.push({ tool: 'write_file', status: ok ? 'done' : 'error', args: { path: f.path }, result: res, error: ok ? undefined : (res && res.error) });
|
|
220
|
+
feedbacks.push(ok ? `write_file OK: ${res.path || f.path} (${res.size != null ? res.size : '?'} bytes)` : `write_file HATA: ${res && res.error}`);
|
|
221
|
+
} catch (e) {
|
|
222
|
+
records.push({ tool: 'write_file', status: 'error', args: { path: f.path }, error: e.message });
|
|
223
|
+
feedbacks.push(`write_file HATA: ${e.message}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { records, feedback: feedbacks.join('\n') };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!allowed.has(norm)) {
|
|
230
|
+
records.push({ tool: rawTool, status: 'error', error: 'Bu modda kullanilamayan arac: ' + rawTool });
|
|
231
|
+
return { records, feedback: `${rawTool}: bu arac bu modda kullanilamaz (izin verilenler: ${[...allowed].join(', ')})` };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const args = { ...(call.args || {}) };
|
|
235
|
+
for (const k of ['path', 'name', 'command', 'old_string', 'new_string']) {
|
|
236
|
+
if (typeof args[k] === 'string') args[k] = args[k].trim();
|
|
237
|
+
}
|
|
238
|
+
if (args.path) args.path = expandHome(args.path);
|
|
239
|
+
|
|
240
|
+
// Ajan modu guvenlik guard'i: yikici/tehlikeli kabuk komutlarini calistirmadan engelle
|
|
241
|
+
if (norm === 'bash') {
|
|
242
|
+
const cmd = (args.command || args.cmd || '').toString();
|
|
243
|
+
if (!cmd.trim()) {
|
|
244
|
+
records.push({ tool: 'bash', status: 'error', error: 'Bos komut' });
|
|
245
|
+
return { records, feedback: 'bash HATA: bos komut' };
|
|
246
|
+
}
|
|
247
|
+
const isDangerous = opts.isDangerous || defaultIsDangerous;
|
|
248
|
+
if (isDangerous(cmd)) {
|
|
249
|
+
records.push({ tool: 'bash', status: 'error', args: { command: cmd }, error: 'Yikici/tehlikeli komut ajan modunda engellendi' });
|
|
250
|
+
return { records, feedback: `bash: "${cmd.slice(0, 80)}" yikici/tehlikeli goruldugu icin ajan tarafindan CALISTIRILMADI. Gerekirse kullanici komutu kendisi calistirabilir.` };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let mod;
|
|
255
|
+
try { mod = loadTool(norm); } catch {
|
|
256
|
+
records.push({ tool: rawTool, status: 'error', error: 'Bilinmeyen arac: ' + rawTool });
|
|
257
|
+
return { records, feedback: `${rawTool} HATA: bilinmeyen arac` };
|
|
258
|
+
}
|
|
259
|
+
const fn = mod.execute || (mod.default && mod.default.execute);
|
|
260
|
+
if (typeof fn !== 'function') {
|
|
261
|
+
records.push({ tool: norm, status: 'error', error: 'execute yok' });
|
|
262
|
+
return { records, feedback: `${norm} HATA: execute fonksiyonu yok` };
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
const res = await fn(args);
|
|
266
|
+
const ok = typeof res === 'string' ? true : (res && res.success !== false);
|
|
267
|
+
const status = ok ? 'done' : 'error';
|
|
268
|
+
const feedback = buildFeedback(norm, res);
|
|
269
|
+
records.push({ tool: norm, status, args: sanitizeArgs(args), result: res });
|
|
270
|
+
return { records, feedback };
|
|
271
|
+
} catch (e) {
|
|
272
|
+
records.push({ tool: norm, status: 'error', args: sanitizeArgs(args), error: e.message });
|
|
273
|
+
return { records, feedback: `${norm} HATA: ${e.message}` };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Ana agentic dongu.
|
|
279
|
+
* callModel(messages) => Promise<{ content, toolCalls }>
|
|
280
|
+
* Donus: { records, reply, iterations }
|
|
281
|
+
*/
|
|
282
|
+
async function runAgentic({ callModel, systemPrompt, historyMessages, task, toolsDir, loadTool, allowed, maxIterations = 15 }) {
|
|
283
|
+
const messages = [{ role: 'system', content: systemPrompt }];
|
|
284
|
+
for (const mm of historyMessages || []) messages.push({ role: mm.role, content: mm.content || '' });
|
|
285
|
+
messages.push({ role: 'user', content: task });
|
|
286
|
+
|
|
287
|
+
const allowedSet = allowed ? new Set(allowed) : new Set(DEFAULT_ALLOWED);
|
|
288
|
+
const allRecords = [];
|
|
289
|
+
let finalReply = '';
|
|
290
|
+
let iterations = 0;
|
|
291
|
+
|
|
292
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
293
|
+
iterations++;
|
|
294
|
+
const { content, toolCalls } = await callModel(messages);
|
|
295
|
+
const calls = parseAgenticCalls(content, toolCalls);
|
|
296
|
+
|
|
297
|
+
if (calls.length === 0) {
|
|
298
|
+
finalReply = stripProtocolTokens(content);
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
messages.push({ role: 'assistant', content: content || '' });
|
|
303
|
+
const feedbacks = [];
|
|
304
|
+
for (const call of calls) {
|
|
305
|
+
const { records, feedback } = await executeCall(call, { toolsDir, loadTool, allowed: allowedSet });
|
|
306
|
+
allRecords.push(...records);
|
|
307
|
+
feedbacks.push(feedback);
|
|
308
|
+
}
|
|
309
|
+
messages.push({
|
|
310
|
+
role: 'user',
|
|
311
|
+
content: '<tool_results>\n' + feedbacks.join('\n') + '\n</tool_results>\nGorev tamamlandiysa ARAC CAGIRMADAN tek cumlelik ozet yaz. Devam gerekiyorsa sonraki araci cagir.',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (i === maxIterations - 1) {
|
|
315
|
+
finalReply = stripProtocolTokens(content) || 'Islem maksimum adima ulasti.';
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return { records: allRecords, reply: finalReply, iterations };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
module.exports = { parseAgenticCalls, stripProtocolTokens, executeCall, runAgentic, expandHome, makeStreamFilter, makeSanitizeStream, TOOL_ALIASES, DEFAULT_ALLOWED };
|
package/src/tools/workflow.js
CHANGED
|
@@ -24,23 +24,58 @@ function allToolNames() {
|
|
|
24
24
|
|
|
25
25
|
function loadUserMemory(username) {
|
|
26
26
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
const dir = path.join(os.homedir(), '.natureco', 'memory');
|
|
28
|
+
const uname = (username || 'default').toLowerCase();
|
|
29
|
+
// Kullanici-ozel dosya (`<user>.json`) + legacy `default.json`'i birlestir.
|
|
30
|
+
// Eski kurulumlarda hafiza default.json'a yazilmis olabilir; okuyucu sadece
|
|
31
|
+
// <user>.json'a bakinca eski kayitlar (ve bot personasi) hic yuklenmiyordu.
|
|
32
|
+
// default.json'i yalnizca ismi aktif kullaniciyla eslesiyorsa (ya da isimsizse)
|
|
33
|
+
// katariz — coklu kullanicida baska birinin hafizasini sizdirmamak icin.
|
|
34
|
+
const files = [];
|
|
35
|
+
const userFile = path.join(dir, `${uname}.json`);
|
|
36
|
+
if (fs.existsSync(userFile)) files.push(userFile);
|
|
37
|
+
if (uname !== 'default') {
|
|
38
|
+
const defFile = path.join(dir, 'default.json');
|
|
39
|
+
if (fs.existsSync(defFile)) {
|
|
40
|
+
try {
|
|
41
|
+
const dm = JSON.parse(fs.readFileSync(defFile, 'utf8'));
|
|
42
|
+
const dn = (dm.name || '').toLowerCase();
|
|
43
|
+
if (!dn || dn === uname) files.push(defFile);
|
|
44
|
+
} catch {}
|
|
38
45
|
}
|
|
39
|
-
const parts = [];
|
|
40
|
-
if (name) parts.push(`Kullanici adi: ${name}`);
|
|
41
|
-
if (facts.length > 0) parts.push(`Bilinenler: ${facts.slice(0, 10).join('; ')}`);
|
|
42
|
-
return parts.join('\n');
|
|
43
46
|
}
|
|
47
|
+
if (files.length === 0) return '';
|
|
48
|
+
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
const facts = [];
|
|
51
|
+
let name = '', botName = '';
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
let mem;
|
|
54
|
+
try { mem = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { continue; }
|
|
55
|
+
if (!name && mem.name) name = mem.name;
|
|
56
|
+
// Jenerik "Asistan" placeholder yerine gercek persona adini tercih et
|
|
57
|
+
if ((!botName || /^asistan$/i.test(botName)) && mem.botName && !/^asistan$/i.test(mem.botName)) botName = mem.botName;
|
|
58
|
+
for (const f of (mem.facts || [])) {
|
|
59
|
+
const v = (f && (f.value != null ? f.value : f));
|
|
60
|
+
if (!v || typeof v !== 'string') continue;
|
|
61
|
+
const key = v.trim().toLowerCase();
|
|
62
|
+
if (!key || seen.has(key)) continue;
|
|
63
|
+
seen.add(key);
|
|
64
|
+
facts.push(v.trim());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// isim memory.name'de yoksa fact'lerden cikar
|
|
68
|
+
if (!name) {
|
|
69
|
+
for (const f of facts) {
|
|
70
|
+
const match = f.toLowerCase().match(/(?:kullanici\s*adi?|kullanıcı\s*adı?|isim|name)\s*:?\s*(.+)/);
|
|
71
|
+
if (match && match[1].trim().length > 2) { name = match[1].trim(); break; }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const parts = [];
|
|
75
|
+
if (name) parts.push(`Kullanici adi: ${name}`);
|
|
76
|
+
if (botName) parts.push(`Bot adi: ${botName}`);
|
|
77
|
+
if (facts.length > 0) parts.push(`Bilinenler: ${facts.slice(0, 15).join('; ')}`);
|
|
78
|
+
return parts.join('\n');
|
|
44
79
|
} catch {}
|
|
45
80
|
return '';
|
|
46
81
|
}
|
|
@@ -82,6 +117,58 @@ function apiCall(providerUrl, apiKey, body) {
|
|
|
82
117
|
});
|
|
83
118
|
}
|
|
84
119
|
|
|
120
|
+
// Streaming (SSE) varyanti — onDelta(token) canli cagirilir; sonunda {content, toolCalls} doner.
|
|
121
|
+
// OpenAI-uyumlu delta formati (MiniMax chatcompletion_v2, Gemini /openai, OpenAI, ...).
|
|
122
|
+
function apiCallStream(providerUrl, apiKey, body, onDelta) {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const base = providerUrl.replace(/\/+$/, '');
|
|
125
|
+
const endpoint = isMiniMax(base)
|
|
126
|
+
? base + '/v1/text/chatcompletion_v2'
|
|
127
|
+
: isGemini(base)
|
|
128
|
+
? base + '/openai/chat/completions'
|
|
129
|
+
: base + '/chat/completions';
|
|
130
|
+
const req = https.request(endpoint, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
|
|
133
|
+
timeout: 120000,
|
|
134
|
+
}, (res) => {
|
|
135
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
136
|
+
let data = '';
|
|
137
|
+
res.on('data', c => data += c);
|
|
138
|
+
res.on('end', () => reject(new Error('HTTP ' + res.statusCode + ': ' + data.slice(0, 300))));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
res.setEncoding('utf8');
|
|
142
|
+
let sseBuf = '';
|
|
143
|
+
let content = '';
|
|
144
|
+
const toolCalls = [];
|
|
145
|
+
res.on('data', (chunk) => {
|
|
146
|
+
sseBuf += chunk;
|
|
147
|
+
const parts = sseBuf.split('\n');
|
|
148
|
+
sseBuf = parts.pop(); // eksik son satiri buffer'da tut
|
|
149
|
+
for (const line of parts) {
|
|
150
|
+
const l = line.trim();
|
|
151
|
+
if (!l.startsWith('data:')) continue;
|
|
152
|
+
const payload = l.slice(5).trim();
|
|
153
|
+
if (!payload || payload === '[DONE]') continue;
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(payload);
|
|
156
|
+
const delta = parsed.choices?.[0]?.delta;
|
|
157
|
+
if (!delta) continue;
|
|
158
|
+
if (delta.content) { content += delta.content; if (onDelta) onDelta(delta.content); }
|
|
159
|
+
if (Array.isArray(delta.tool_calls)) { for (const tc of delta.tool_calls) toolCalls.push(tc); }
|
|
160
|
+
} catch { /* eksik/parcali JSON — atla */ }
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
res.on('end', () => resolve({ content, toolCalls }));
|
|
164
|
+
});
|
|
165
|
+
req.on('error', reject);
|
|
166
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
|
167
|
+
req.write(JSON.stringify(body));
|
|
168
|
+
req.end();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
85
172
|
async function workflow(params) {
|
|
86
173
|
const { action, task, steps, name, workflowId, regenerateStep, conversationHistory } = params;
|
|
87
174
|
const cfg = loadConfig();
|
|
@@ -126,89 +213,90 @@ async function workflow(params) {
|
|
|
126
213
|
if (action === 'run') {
|
|
127
214
|
if (!task) return { success: false, error: 'task gerekli' };
|
|
128
215
|
|
|
129
|
-
// Non-tool-calling
|
|
216
|
+
// Non-tool-calling / agentic-text modeller (MiniMax M2.x): model tool call'larini
|
|
217
|
+
// native XML olarak (<invoke> / <minimax:tool_call>) ve skill yuklemeyi <skill> ile
|
|
218
|
+
// uretir. Bounded agentic dongu ile parse edip gercek araclari calistir.
|
|
130
219
|
if (!supportsToolCalls()) {
|
|
220
|
+
const { runAgentic } = require('./agentic-runner');
|
|
131
221
|
const memCtx = memoryContext();
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
222
|
+
const desktop = path.join(os.homedir(), 'Desktop');
|
|
223
|
+
const sysMsg = [
|
|
224
|
+
'Sen NatureCo adli, arac kullanabilen bir yapay zeka ajanisin. Kullanicinin istegini SADECE anlatarak degil, ARACLARI cagirarak fiilen gerceklestir.',
|
|
225
|
+
memCtx ? '\nKullanici bilgisi:\n' + memCtx : '',
|
|
226
|
+
'\n\nOrtam:\n- Isletim sistemi: ' + process.platform + '\n- Kullanici home: ' + os.homedir() + '\n- Masaustu: ' + desktop + '\n- Calisma dizini: ' + process.cwd(),
|
|
227
|
+
'\n\nArac cagirmak icin TAM olarak su formati kullan:\n<minimax:tool_call>\n<invoke name="ARAC_ADI">\n<parameter name="PARAM">DEGER</parameter>\n</invoke>\n</minimax:tool_call>',
|
|
228
|
+
'\n\nKullanabilecegin araclar:',
|
|
229
|
+
'- write_file: dosya olustur/uzerine yaz. parametreler: path (TAM yol), content (dosyanin TAM icerigi). Kod/oyun/site isteniyorsa TUM icerigi content icine yaz, kisaltma.',
|
|
230
|
+
'- read_file: dosya oku. parametre: path',
|
|
231
|
+
'- edit_file: MEVCUT dosyada metin degistir. parametreler: path, old_string (birebir mevcut metin), new_string. Bir dosyanin bir kismini degistirirken write_file yerine bunu kullan (tum dosyayi yeniden yazma).',
|
|
232
|
+
'- file_search: glob ile dosya bul. parametre: pattern (orn. "**/*.js", "src/**/*.json").',
|
|
233
|
+
'- list_dir: dizin icerigini listele. parametre: path',
|
|
234
|
+
'- bash: kabuk komutu calistir (npm, git, node, python, test, ls, grep/findstr, mkdir...). parametre: command. Guvenli komutlar dogrudan calisir; yikici/tehlikeli komutlar guvenlik politikasiyla engellenir. Icerik aramasi icin grep/findstr kullan.',
|
|
235
|
+
'- skill_view: gorevle ilgili bir skill yukle. parametre: name',
|
|
236
|
+
'\nKurallar:',
|
|
237
|
+
'- MEVCUT bir dosyayi degistirmeden ONCE read_file ile oku; sonra edit_file ile hedefli degisiklik yap (tum dosyayi write_file ile ezme).',
|
|
238
|
+
'- Bir seyi nerede oldugunu bilmiyorsan once file_search/list_dir/bash(grep) ile kesfet.',
|
|
239
|
+
'- Kod yazdiktan/degistirdikten sonra gerektiginde bash ile calistir/test et (orn. "node dosya.js", "npm test"); hata cikarsa duzelt.',
|
|
240
|
+
'- Birden fazla dosya gerekiyorsa her biri icin AYRI write_file cagir.',
|
|
241
|
+
'- Kullanici "masaustu"/"desktop" dediyse ve tam yol vermediyse dosyayi buraya yaz: ' + desktop,
|
|
242
|
+
'- Goreceli yol yerine TAM yol kullan.',
|
|
243
|
+
'- Arac sonuclari <tool_results> icinde geri gelir; gorev bitince ARAC CAGIRMADAN tek cumlelik ozet yaz.',
|
|
244
|
+
'- Basit sohbet/selamlasma ise arac cagirma, dogrudan kisa yanit ver.',
|
|
245
|
+
skillsIndexBlock ? '\n\n' + skillsIndexBlock : '',
|
|
246
|
+
].filter(Boolean).join('\n');
|
|
247
|
+
|
|
248
|
+
const historyMessages = [];
|
|
249
|
+
if (conversationHistory && Array.isArray(conversationHistory)) {
|
|
250
|
+
for (const hm of conversationHistory) { if (hm._internal) continue; historyMessages.push({ role: hm.role, content: hm.content || '' }); }
|
|
156
251
|
}
|
|
157
252
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const jsonEnd = reply.lastIndexOf('}');
|
|
173
|
-
let stepResults = [];
|
|
174
|
-
let fileWriteSuccess = false;
|
|
175
|
-
|
|
176
|
-
if (jsonStart !== -1 && jsonEnd > jsonStart) {
|
|
177
|
-
try {
|
|
178
|
-
const plan = JSON.parse(reply.slice(jsonStart, jsonEnd + 1));
|
|
179
|
-
if (plan.files && Array.isArray(plan.files)) {
|
|
180
|
-
// Execute file writes locally
|
|
181
|
-
for (const f of plan.files) {
|
|
182
|
-
try {
|
|
183
|
-
const wfPath = f.path ? require('path').resolve(f.path.replace(/^~/, require('os').homedir())) : '';
|
|
184
|
-
if (!wfPath) continue;
|
|
185
|
-
require('fs').mkdirSync(require('path').dirname(wfPath), { recursive: true });
|
|
186
|
-
require('fs').writeFileSync(wfPath, f.content || '', 'utf-8');
|
|
187
|
-
const stats = require('fs').statSync(wfPath);
|
|
188
|
-
stepResults.push({ step: 1, tool: 'write_file', status: 'done', args: { path: wfPath }, result: { success: true, path: wfPath, size: stats.size } });
|
|
189
|
-
fileWriteSuccess = true;
|
|
190
|
-
} catch (wfErr) {
|
|
191
|
-
stepResults.push({ step: 1, tool: 'write_file', status: 'error', error: wfErr.message });
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
} catch {}
|
|
253
|
+
// Streaming: yalnizca TTY'de ve caller (repl) stream:true gecince. SSE token'lari
|
|
254
|
+
// XML-gizleyen filtre + model-adi sanitizer zincirinden gecip stdout'a canli akar.
|
|
255
|
+
const streamOn = params.stream === true && !!(process.stdout && process.stdout.isTTY);
|
|
256
|
+
const botName = cfg.botName || 'Asistan';
|
|
257
|
+
const { makeStreamFilter, makeSanitizeStream } = require('./agentic-runner');
|
|
258
|
+
|
|
259
|
+
async function callModel(msgs) {
|
|
260
|
+
if (streamOn) {
|
|
261
|
+
const sani = makeSanitizeStream(botName, t => process.stdout.write(t));
|
|
262
|
+
const filter = makeStreamFilter(t => sani.push(t), null);
|
|
263
|
+
const body = { model, stream: true, messages: msgs, temperature: 0.3, max_tokens: 16000 };
|
|
264
|
+
const out = await apiCallStream(providerUrl, providerApiKey, body, d => filter.push(d));
|
|
265
|
+
filter.end(); sani.end();
|
|
266
|
+
return { content: out.content || '', toolCalls: out.toolCalls || [] };
|
|
196
267
|
}
|
|
268
|
+
const body = { model, stream: false, messages: msgs, temperature: 0.3, max_tokens: 16000 };
|
|
269
|
+
const r = await apiCall(providerUrl, providerApiKey, body);
|
|
270
|
+
const msg = r.choices?.[0]?.message || {};
|
|
271
|
+
return { content: msg.content || '', toolCalls: msg.tool_calls || [] };
|
|
272
|
+
}
|
|
197
273
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
274
|
+
try {
|
|
275
|
+
const { records, reply } = await runAgentic({
|
|
276
|
+
callModel, systemPrompt: sysMsg, historyMessages, task,
|
|
277
|
+
toolsDir: __dirname, maxIterations: 15,
|
|
278
|
+
});
|
|
279
|
+
const fileWrites = records.filter(r => r.tool === 'write_file' && r.status === 'done');
|
|
280
|
+
let finalReply = reply || '';
|
|
281
|
+
if (fileWrites.length > 0) {
|
|
282
|
+
const lines = fileWrites.map(r => ` ✓ ${path.basename((r.result && r.result.path) || (r.args && r.args.path) || '')} olusturuldu (${(r.result && r.result.size != null) ? r.result.size : '?'} bytes)`).join('\n');
|
|
283
|
+
const fileSummary = 'Dosya(lar):\n' + lines;
|
|
284
|
+
finalReply = (finalReply ? finalReply + '\n\n' : '') + fileSummary;
|
|
285
|
+
if (streamOn) process.stdout.write('\n\n' + fileSummary + '\n');
|
|
208
286
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
287
|
+
const done = records.filter(r => r.status === 'done').length;
|
|
288
|
+
return {
|
|
289
|
+
success: true,
|
|
290
|
+
workflowId: (records.length ? 'agentic_' : 'passthrough_') + Date.now().toString(36),
|
|
291
|
+
name: records.length ? 'Agentic Run' : 'Direct Chat',
|
|
292
|
+
status: 'completed',
|
|
293
|
+
totalSteps: records.length,
|
|
294
|
+
completedSteps: done,
|
|
295
|
+
results: records.map((r, i) => ({ step: i + 1, ...r })),
|
|
296
|
+
passthrough: true,
|
|
297
|
+
streamed: streamOn,
|
|
298
|
+
reply: finalReply || 'Tamamlandi.',
|
|
299
|
+
};
|
|
212
300
|
} catch (e) {
|
|
213
301
|
return { success: false, error: 'Yanit alinamadi: ' + e.message };
|
|
214
302
|
}
|