kiosapi 0.1.13 → 0.1.17
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/dist/agent/run.js +67 -5
- package/dist/agent/schemas.js +11 -4
- package/dist/agent/tools.js +33 -10
- package/package.json +1 -1
package/dist/agent/run.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { extname } from 'node:path';
|
|
2
|
+
import { extname, join } from 'node:path';
|
|
3
3
|
import { chatComplete, generateImage, resolveMediaModel, streamVision, } from '../api.js';
|
|
4
4
|
import { CHECKPOINT_PATH } from '../config.js';
|
|
5
5
|
import { cyan, dim, green, idn, prompt, red, rupiah, thinking, yellow } from '../ui.js';
|
|
@@ -235,9 +235,12 @@ async function runTool(call, otomatis, model) {
|
|
|
235
235
|
case 'baca_file':
|
|
236
236
|
console.log(dim(` baca ${str('path')}`));
|
|
237
237
|
return { output: bacaFile(str('path')) };
|
|
238
|
-
case 'daftar_file':
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
case 'daftar_file': {
|
|
239
|
+
const ked = typeof args['kedalaman'] === 'number' ? args['kedalaman'] : undefined;
|
|
240
|
+
const dPath = str('path') || '.';
|
|
241
|
+
console.log(dim(` daftar ${dPath}${ked != null ? ` (kedalaman ${ked})` : ''}`));
|
|
242
|
+
return { output: daftarFile(dPath, ked) };
|
|
243
|
+
}
|
|
241
244
|
case 'cari':
|
|
242
245
|
console.log(dim(` cari "${str('pola')}"`));
|
|
243
246
|
return { output: cari(str('pola'), str('ext') || undefined) };
|
|
@@ -370,6 +373,34 @@ export function undoLastTurn(s) {
|
|
|
370
373
|
* agent's final text (last answer or the `selesai` summary) — useful for chaining agents in a team.
|
|
371
374
|
*/
|
|
372
375
|
export async function runTurn(s, userText) {
|
|
376
|
+
// On the very first turn of a fresh session, append project metadata to the existing system
|
|
377
|
+
// message so the model starts oriented without needing daftar_file(".") for basic orientation.
|
|
378
|
+
// We APPEND (not push a new system message) because some providers reject requests that have
|
|
379
|
+
// more than one system-role message — a second {role:'system'} at index 1 causes HTTP 400.
|
|
380
|
+
if (s.messages.filter((m) => m.role === 'user').length === 0) {
|
|
381
|
+
const snippets = [];
|
|
382
|
+
try {
|
|
383
|
+
snippets.push(`<root-directory>\n${daftarFile('.')}\n</root-directory>`);
|
|
384
|
+
}
|
|
385
|
+
catch { /* ignore */ }
|
|
386
|
+
for (const f of ['package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod', 'README.md']) {
|
|
387
|
+
const abs = join(process.cwd(), f);
|
|
388
|
+
if (existsSync(abs)) {
|
|
389
|
+
try {
|
|
390
|
+
const content = readFileSync(abs, 'utf8');
|
|
391
|
+
snippets.push(`<file path="${f}">\n${content.slice(0, 4000)}\n</file>`);
|
|
392
|
+
}
|
|
393
|
+
catch { /* unreadable — skip */ }
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (snippets.length > 0) {
|
|
397
|
+
const sysMsg = s.messages[0];
|
|
398
|
+
if (sysMsg?.role === 'system') {
|
|
399
|
+
sysMsg.content +=
|
|
400
|
+
`\n\n## Konteks Proyek (auto-injected — jangan panggil daftar_file(".") lagi)\n${snippets.join('\n\n')}`;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
373
404
|
s.messages.push({ role: 'user', content: userText });
|
|
374
405
|
const tools = toolsForMode(s.mode);
|
|
375
406
|
let lastText = '';
|
|
@@ -387,6 +418,11 @@ export async function runTurn(s, userText) {
|
|
|
387
418
|
const lastSigs = []; // last 3 sigs for consecutive detection
|
|
388
419
|
const COUNT_LIMIT = 4; // same tool+args called 4× total → loop
|
|
389
420
|
const CONSEC_LIMIT = 3; // same sig 3× in a row → loop
|
|
421
|
+
// Read-only tool cache: on 2nd+ identical call, return the previous result with a warning
|
|
422
|
+
// instead of re-running. This gives the model early feedback so it can self-correct before
|
|
423
|
+
// COUNT_LIMIT is reached, preventing the common "list root, list root, list root" pattern.
|
|
424
|
+
const READ_ONLY_TOOLS = new Set(['baca_file', 'daftar_file', 'cari', 'lihat_gambar']);
|
|
425
|
+
const toolCache = new Map(); // sig → first output
|
|
390
426
|
const stepLimit = s.maxSteps ?? MAX_STEPS;
|
|
391
427
|
for (let step = 0; step < stepLimit; step++) {
|
|
392
428
|
const stop = thinking();
|
|
@@ -418,7 +454,13 @@ export async function runTurn(s, userText) {
|
|
|
418
454
|
// (null content + no tool_calls) can appear from a truncated stream or a reasoning-only
|
|
419
455
|
// step; pushing it corrupts the history and causes providers to reject subsequent calls.
|
|
420
456
|
if (reply.content !== null || calls.length > 0) {
|
|
421
|
-
|
|
457
|
+
// When tool_calls are present, set content to null regardless of what text the model
|
|
458
|
+
// prefaced the call with. The text was already streamed to the user via onText; keeping
|
|
459
|
+
// it in history causes strict OpenAI-compat providers (DeepSeek, Workers AI, etc.) to
|
|
460
|
+
// return HTTP 400 on the next call because they require content: null when tool_calls
|
|
461
|
+
// is non-empty. Anthropic also works correctly with null here.
|
|
462
|
+
const storedContent = calls.length > 0 ? null : reply.content;
|
|
463
|
+
s.messages.push({ role: 'assistant', content: storedContent, tool_calls: reply.tool_calls });
|
|
422
464
|
}
|
|
423
465
|
if (reply.content)
|
|
424
466
|
lastText = reply.content;
|
|
@@ -466,10 +508,30 @@ export async function runTurn(s, userText) {
|
|
|
466
508
|
}
|
|
467
509
|
const stepModified = new Set();
|
|
468
510
|
for (const call of calls) {
|
|
511
|
+
const sig = `${call.function.name}:${call.function.arguments}`;
|
|
512
|
+
const count = callCounts.get(sig) ?? 1;
|
|
513
|
+
// For read-only tools called more than once with the same args: return the cached result
|
|
514
|
+
// with an escalating warning instead of re-running. The model gets immediate feedback so
|
|
515
|
+
// it can self-correct (pick a subfolder, go deeper) rather than looping to COUNT_LIMIT.
|
|
516
|
+
if (count > 1 && READ_ONLY_TOOLS.has(call.function.name) && toolCache.has(sig)) {
|
|
517
|
+
const toolName = call.function.name;
|
|
518
|
+
const cachedOut = toolCache.get(sig) ?? '';
|
|
519
|
+
const warn = count >= COUNT_LIMIT - 1
|
|
520
|
+
? `⚠ STOP: "${toolName}" sudah dipanggil ${count}× dengan argumen yang sama — ` +
|
|
521
|
+
`hasilnya tidak berubah. WAJIB gunakan tool "selesai" atau eksplorasi path BERBEDA.`
|
|
522
|
+
: `[Cache — identik dengan panggilan sebelumnya]\n${cachedOut}\n\n` +
|
|
523
|
+
`⚠ Kamu sudah memanggil "${toolName}" dengan argumen yang sama ${count}×. ` +
|
|
524
|
+
`Jangan ulangi — masuk ke subfolder spesifik atau gunakan "selesai".`;
|
|
525
|
+
console.log(dim(` ↩ ${toolName} (cache ke-${count})`));
|
|
526
|
+
s.messages.push({ role: 'tool', content: warn, tool_call_id: call.id });
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
469
529
|
const result = await runTool(call, s.otomatis, s.model);
|
|
470
530
|
s.messages.push({ role: 'tool', content: result.output, tool_call_id: call.id });
|
|
471
531
|
if (result.modifiedPath)
|
|
472
532
|
stepModified.add(result.modifiedPath);
|
|
533
|
+
if (READ_ONLY_TOOLS.has(call.function.name))
|
|
534
|
+
toolCache.set(sig, result.output);
|
|
473
535
|
if (result.done) {
|
|
474
536
|
if (stepModified.size > 0)
|
|
475
537
|
console.log(dim(` ✎ ${[...stepModified].join(' · ')}`));
|
package/dist/agent/schemas.js
CHANGED
|
@@ -16,10 +16,16 @@ const TOOLS = {
|
|
|
16
16
|
type: 'function',
|
|
17
17
|
function: {
|
|
18
18
|
name: 'daftar_file',
|
|
19
|
-
description: 'Daftar isi sebuah folder.',
|
|
19
|
+
description: 'Daftar isi sebuah folder sebagai tree. Default: 3 level untuk ".", 1 level untuk subfolder. Gunakan kedalaman=2-4 untuk subfolder yang dalam.',
|
|
20
20
|
parameters: {
|
|
21
21
|
type: 'object',
|
|
22
|
-
properties: {
|
|
22
|
+
properties: {
|
|
23
|
+
path: { type: 'string', description: 'Path folder (default ".")' },
|
|
24
|
+
kedalaman: {
|
|
25
|
+
type: 'number',
|
|
26
|
+
description: 'Kedalaman tree 1–5. Default: 3 untuk ".", 1 untuk subfolder.',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
23
29
|
},
|
|
24
30
|
},
|
|
25
31
|
},
|
|
@@ -192,10 +198,11 @@ Direktori kerja: ${process.cwd()}
|
|
|
192
198
|
OS: ${osName} · ${shellNote}
|
|
193
199
|
Aturan:
|
|
194
200
|
- Bekerja langkah demi langkah: pakai tool untuk membaca sebelum mengubah.
|
|
201
|
+
- Strategi eksplorasi: daftar_file(".") sudah tersedia di konteks awal — identifikasi file yang relevan LANGSUNG, lalu baca dengan baca_file. Jangan ulangi daftar_file pada path yang sama.
|
|
202
|
+
- JANGAN memanggil tool APAPUN dengan argumen identik lebih dari 1× dalam satu sesi. Jika tool mengembalikan peringatan cache "⚠", langsung ganti ke path atau argumen BERBEDA.
|
|
195
203
|
- Path selalu relatif ke direktori kerja; akses ke luar ditolak.
|
|
196
204
|
- Gunakan hapus_file/pindah_file untuk menghapus/memindahkan file (lebih aman dari jalankan del/rm).
|
|
197
205
|
- Buat perubahan kecil dan jelas. Setelah tugas beres, panggil tool "selesai" dengan ringkasan.
|
|
198
|
-
-
|
|
199
|
-
- Jika tidak tahu harus berbuat apa selanjutnya, gunakan tool "selesai" dan jelaskan apa yang sudah ditemukan.
|
|
206
|
+
- Jika tidak tahu harus berbuat apa: gunakan tool "selesai" dan jelaskan apa yang sudah ditemukan.
|
|
200
207
|
- Jawab dan jelaskan dalam Bahasa Indonesia.`;
|
|
201
208
|
}
|
package/dist/agent/tools.js
CHANGED
|
@@ -52,20 +52,43 @@ export function bacaFile(path) {
|
|
|
52
52
|
? `${text.slice(0, MAX_READ)}\n…[dipotong, ${text.length} char total]`
|
|
53
53
|
: text;
|
|
54
54
|
}
|
|
55
|
-
/** daftar_file — list a directory (ignored entries hidden).
|
|
56
|
-
|
|
55
|
+
/** daftar_file — list a directory (ignored entries hidden).
|
|
56
|
+
* kedalaman controls tree depth (1–5). Defaults: 3 for root ".", 1 for subdirs.
|
|
57
|
+
* Output is capped at 300 lines to keep API payloads reasonable. */
|
|
58
|
+
export function daftarFile(path, kedalaman) {
|
|
57
59
|
const { abs, rel } = safePath(path || '.');
|
|
58
60
|
if (!existsSync(abs))
|
|
59
61
|
return `Error: folder tidak ada: ${path}`;
|
|
60
62
|
const patterns = ignorePatterns();
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
// Default depth: 3 for root (wide orientation), 1 for any subdir (focused)
|
|
64
|
+
const depth = kedalaman != null ? Math.min(Math.max(1, kedalaman), 5) : rel === '.' ? 3 : 1;
|
|
65
|
+
const listDir = (dirAbs, dirRel, d, indent, lines) => {
|
|
66
|
+
let entries;
|
|
67
|
+
try {
|
|
68
|
+
entries = readdirSync(dirAbs)
|
|
69
|
+
.filter((name) => !isProtected(dirRel === '.' ? name : `${dirRel}/${name}`, patterns))
|
|
70
|
+
.sort()
|
|
71
|
+
.map((name) => ({ name, isDir: statSync(join(dirAbs, name)).isDirectory() }));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return; // unreadable dir — skip silently
|
|
75
|
+
}
|
|
76
|
+
for (const { name, isDir } of entries) {
|
|
77
|
+
if (lines.length >= 300) {
|
|
78
|
+
lines.push(' … (dipotong — panggil lagi dengan subfolder spesifik)');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
lines.push(`${indent}${name}${isDir ? '/' : ''}`);
|
|
82
|
+
if (isDir && d > 1) {
|
|
83
|
+
const subAbs = join(dirAbs, name);
|
|
84
|
+
const subRel = dirRel === '.' ? name : `${dirRel}/${name}`;
|
|
85
|
+
listDir(subAbs, subRel, d - 1, `${indent} `, lines);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const lines = [];
|
|
90
|
+
listDir(abs, rel, depth, '', lines);
|
|
91
|
+
return lines.length > 0 ? lines.join('\n') : '(kosong)';
|
|
69
92
|
}
|
|
70
93
|
/** cari — grep file contents under the working dir (case-insensitive). */
|
|
71
94
|
export function cari(pola, ext) {
|