kiosapi 0.1.26 → 0.1.28
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 +32 -21
- package/dist/agent/team.js +119 -36
- package/package.json +1 -1
package/dist/agent/run.js
CHANGED
|
@@ -56,32 +56,36 @@ function trimContext(messages) {
|
|
|
56
56
|
const dropped = clean.length - tail.length;
|
|
57
57
|
if (dropped <= 0)
|
|
58
58
|
return [system, ...clean];
|
|
59
|
-
// Collect baca_file
|
|
60
|
-
// already has context for — prevents
|
|
59
|
+
// Collect baca_file and daftar_file calls from the dropped messages so the model knows which
|
|
60
|
+
// files/directories it already has context for — prevents re-orientation loops after trimming.
|
|
61
61
|
const droppedHead = clean.slice(0, clean.length - tail.length + skip);
|
|
62
62
|
const droppedPaths = [];
|
|
63
|
+
const droppedDirs = [];
|
|
63
64
|
for (const msg of droppedHead) {
|
|
64
65
|
if (msg.role === 'assistant' && msg.tool_calls) {
|
|
65
66
|
for (const tc of msg.tool_calls) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
try {
|
|
68
|
+
const a = JSON.parse(tc.function.arguments ?? '{}');
|
|
69
|
+
if (tc.function?.name === 'baca_file' && a.path)
|
|
70
|
+
droppedPaths.push(a.path);
|
|
71
|
+
if (tc.function?.name === 'daftar_file')
|
|
72
|
+
droppedDirs.push(a.path || '.');
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* ignore */
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
const filesNote = droppedPaths.length > 0
|
|
80
|
-
? ` File sudah dibaca
|
|
81
|
+
? ` File sudah dibaca: ${[...new Set(droppedPaths)].join(', ')}. JANGAN baca ulang — gunakan cari untuk teks spesifik.`
|
|
82
|
+
: '';
|
|
83
|
+
const dirsNote = droppedDirs.length > 0
|
|
84
|
+
? ` Direktori sudah terdaftar: ${[...new Set(droppedDirs)].join(', ')}. JANGAN list ulang — gunakan baca_file untuk file spesifik.`
|
|
81
85
|
: '';
|
|
82
86
|
const note = {
|
|
83
87
|
role: 'system',
|
|
84
|
-
content: `[Kiosapi: ${dropped} pesan awal dihapus dari konteks.${filesNote}]`,
|
|
88
|
+
content: `[Kiosapi: ${dropped} pesan awal dihapus dari konteks.${filesNote}${dirsNote}]`,
|
|
85
89
|
};
|
|
86
90
|
return [system, note, ...tail];
|
|
87
91
|
}
|
|
@@ -482,12 +486,15 @@ export async function runTurn(s, userText) {
|
|
|
482
486
|
}
|
|
483
487
|
};
|
|
484
488
|
// Loop detection: count total calls per signature (tool+args) across the whole runTurn.
|
|
485
|
-
// Consecutive-3 catches obvious tight loops; count-
|
|
486
|
-
//
|
|
489
|
+
// Consecutive-3 catches obvious tight loops; count-N catches spread-out repetition.
|
|
490
|
+
// daftar_file has a lower limit (3) because re-listing the same directory is almost never
|
|
491
|
+
// useful; baca_file legitimately needs more chances (range reads + post-edit re-reads).
|
|
487
492
|
const callCounts = new Map();
|
|
488
493
|
const lastSigs = []; // last 3 sigs for consecutive detection
|
|
489
|
-
const
|
|
494
|
+
const COUNT_LIMITS = { daftar_file: 3, cari: 4 };
|
|
495
|
+
const DEFAULT_COUNT_LIMIT = 6;
|
|
490
496
|
const CONSEC_LIMIT = 3; // same sig 3× in a row → loop
|
|
497
|
+
const countLimitFor = (toolName) => COUNT_LIMITS[toolName] ?? DEFAULT_COUNT_LIMIT;
|
|
491
498
|
// Read-only tool cache: on 2nd+ identical call, return the previous result with a warning
|
|
492
499
|
// instead of re-running. This gives the model early feedback so it can self-correct before
|
|
493
500
|
// COUNT_LIMIT is reached, preventing the common "list root, list root, list root" pattern.
|
|
@@ -553,9 +560,9 @@ export async function runTurn(s, userText) {
|
|
|
553
560
|
lastSigs.push(sig);
|
|
554
561
|
if (lastSigs.length > CONSEC_LIMIT)
|
|
555
562
|
lastSigs.shift();
|
|
556
|
-
// Total count exceeded, or 3 in a row
|
|
563
|
+
// Total count exceeded (per-tool limit), or 3 in a row
|
|
557
564
|
const consecutive = lastSigs.length === CONSEC_LIMIT && lastSigs.every((s) => s === sig);
|
|
558
|
-
if (count >=
|
|
565
|
+
if (count >= countLimitFor(call.function.name) || consecutive) {
|
|
559
566
|
loopSig = sig;
|
|
560
567
|
break;
|
|
561
568
|
}
|
|
@@ -588,9 +595,13 @@ export async function runTurn(s, userText) {
|
|
|
588
595
|
// trimContext drops old messages from its window. The escalating warning text
|
|
589
596
|
// discourages repetition; withholding the content at count=3+ only causes the model
|
|
590
597
|
// to keep retrying — counter-productive to stopping the loop.
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
598
|
+
const toolLimit = countLimitFor(toolName);
|
|
599
|
+
const isLastChance = count >= toolLimit - 1;
|
|
600
|
+
const stopNote = isLastChance
|
|
601
|
+
? `\n\n⚠ STOP (ke-${count}/${toolLimit}): JANGAN panggil "${toolName}" lagi dengan argumen yang sama. Gunakan tool "selesai" dan jelaskan kendalanya, atau gunakan cari/baca_file dengan path/range BERBEDA.`
|
|
602
|
+
: toolName === 'daftar_file'
|
|
603
|
+
? `\n\n⚠ Cache ke-${count}: direktori ini SUDAH terdaftar. JANGAN list ulang — gunakan baca_file(path) untuk file spesifik di dalamnya.`
|
|
604
|
+
: `\n\n⚠ Cache ke-${count}: "${toolName}" sudah dipanggil ${count}× dengan argumen sama. Gunakan cari atau baca_file(path, mulai=N) untuk bagian berbeda.`;
|
|
594
605
|
const warn = `[Cache ke-${count}]\n${cachedOut}${stopNote}`;
|
|
595
606
|
const cachePathHint = (() => {
|
|
596
607
|
try {
|
package/dist/agent/team.js
CHANGED
|
@@ -1,49 +1,128 @@
|
|
|
1
1
|
import { modelSupportsTools } from '../api.js';
|
|
2
2
|
import { bold, dim, green, yellow } from '../ui.js';
|
|
3
3
|
import { newSession, runTurn } from './run.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Try to extract a StructuredPlan from the planner's selesai output.
|
|
6
|
+
* Handles: raw JSON, ```json block, or JSON embedded in prose.
|
|
7
|
+
*/
|
|
8
|
+
function tryParsePlan(output) {
|
|
9
|
+
const candidates = [output.trim()];
|
|
10
|
+
const codeBlock = output.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
11
|
+
if (codeBlock?.[1])
|
|
12
|
+
candidates.push(codeBlock[1].trim());
|
|
13
|
+
const embedded = output.match(/\{[\s\S]*?"langkah"[\s\S]*\}/);
|
|
14
|
+
if (embedded?.[0])
|
|
15
|
+
candidates.push(embedded[0]);
|
|
16
|
+
for (const c of candidates) {
|
|
17
|
+
try {
|
|
18
|
+
const p = JSON.parse(c);
|
|
19
|
+
if (p !== null &&
|
|
20
|
+
typeof p === 'object' &&
|
|
21
|
+
Array.isArray(p.langkah) &&
|
|
22
|
+
p.langkah.length > 0 &&
|
|
23
|
+
typeof p.langkah[0]?.tugas === 'string') {
|
|
24
|
+
return p;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
/* try next */
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
29
32
|
}
|
|
30
|
-
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Built-in team: planner-executor
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
const PLANNER_BRIEF = `Peranmu: PERENCANA. Telusuri kode seperlunya lalu buat rencana langkah kecil & terstruktur.
|
|
37
|
+
|
|
38
|
+
WAJIB: panggil tool "selesai" dengan parameter ringkasan berisi JSON dalam format TEPAT ini (tidak ada teks lain sebelum/sesudah JSON):
|
|
39
|
+
{"ringkasan":"<ringkasan singkat>","langkah":[{"tugas":"<deskripsi spesifik & actionable>","file":["path/file1.ts"],"catatan":"<konteks tambahan, opsional>"},...]}
|
|
40
|
+
|
|
41
|
+
Aturan rencana:
|
|
42
|
+
- Masing-masing langkah: 1–3 file, satu perubahan jelas, bisa selesai dalam ~15 tool calls.
|
|
43
|
+
- Maksimal 10 langkah — pecah yang kompleks, gabung yang trivial.
|
|
44
|
+
- Sebutkan path file LENGKAP dan AKURAT (gunakan daftar_file untuk memastikan).
|
|
45
|
+
- JANGAN menulis kode — hanya rencanakan.`;
|
|
46
|
+
const REVIEWER_BRIEF = 'Peranmu: PENINJAU. Baca file yang relevan dan tinjau hasil implementasi: sebutkan masalah/risiko & saran perbaikan singkat.';
|
|
31
47
|
export async function runTeam(task, opts) {
|
|
32
|
-
console.log(bold('👥 Tim agen
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
console.log(bold('👥 Tim agen — mode perencana-eksekutor'));
|
|
49
|
+
// ① Planner
|
|
50
|
+
console.log(`\n${bold('① Perencana')} ${dim(`(${opts.models.perencana})`)}`);
|
|
51
|
+
const plannerSession = newSession(opts.models.perencana, 'rencana', opts.otomatis);
|
|
52
|
+
plannerSession.messages.push({ role: 'system', content: PLANNER_BRIEF });
|
|
53
|
+
const planOutput = await runTurn(plannerSession, task);
|
|
54
|
+
const plan = tryParsePlan(planOutput);
|
|
55
|
+
if (!plan) {
|
|
56
|
+
console.log(yellow('⚠ Perencana tidak menghasilkan rencana terstruktur — jalankan sebagai sesi pengkode tunggal.'));
|
|
57
|
+
const s = newSession(opts.models.pengkode, 'buat', opts.otomatis);
|
|
58
|
+
s.messages.push({
|
|
59
|
+
role: 'system',
|
|
60
|
+
content: 'Peranmu: PENGKODE. Implementasikan tugas mengikuti rencana. Buat perubahan kecil & jelas, lalu panggil selesai.',
|
|
61
|
+
});
|
|
62
|
+
await runTurn(s, `Tugas: ${task}\n\nRencana:\n${planOutput || '(tidak ada rencana eksplisit)'}`);
|
|
63
|
+
console.log(`\n${bold('③ Peninjau')} ${dim(`(${opts.models.peninjau})`)}`);
|
|
64
|
+
const rev = newSession(opts.models.peninjau, 'rencana', opts.otomatis);
|
|
65
|
+
rev.messages.push({ role: 'system', content: REVIEWER_BRIEF });
|
|
66
|
+
await runTurn(rev, `Tinjau hasil implementasi untuk tugas: ${task}`);
|
|
67
|
+
console.log(green('\n✓ Tim selesai.'));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Show plan
|
|
71
|
+
console.log(bold(`\n📋 ${plan.ringkasan}`));
|
|
72
|
+
for (const [i, step] of plan.langkah.entries()) {
|
|
73
|
+
console.log(` ${dim(`${i + 1}.`)} ${step.tugas}`);
|
|
74
|
+
console.log(dim(` 📄 ${step.file.join(', ')}`));
|
|
75
|
+
if (step.catatan)
|
|
76
|
+
console.log(dim(` 💬 ${step.catatan}`));
|
|
77
|
+
}
|
|
78
|
+
console.log('');
|
|
79
|
+
// ② Execute each step in a fresh focused session
|
|
80
|
+
for (const [i, step] of plan.langkah.entries()) {
|
|
81
|
+
const label = `${i + 1}/${plan.langkah.length}`;
|
|
82
|
+
console.log(bold(`\n② Pengkode — Langkah ${label}`) + dim(`: ${step.tugas}`));
|
|
83
|
+
console.log(dim(` 📄 ${step.file.join(', ')}`));
|
|
84
|
+
const s = newSession(opts.models.pengkode, 'buat', opts.otomatis);
|
|
85
|
+
s.messages.push({
|
|
86
|
+
role: 'system',
|
|
87
|
+
content: [
|
|
88
|
+
`Peranmu: PENGKODE — langkah ${label} dari rencana keseluruhan.`,
|
|
89
|
+
`TUGAS: ${step.tugas}`,
|
|
90
|
+
`File relevan: ${step.file.join(', ')}`,
|
|
91
|
+
step.catatan ? `Catatan: ${step.catatan}` : '',
|
|
92
|
+
'Fokus HANYA pada tugas ini. Baca file relevan, buat perubahan, panggil selesai.',
|
|
93
|
+
'Jangan baca file lain kecuali diperlukan langsung untuk memahami konteks.',
|
|
94
|
+
]
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.join('\n'),
|
|
97
|
+
});
|
|
98
|
+
const stepPrompt = [
|
|
99
|
+
step.tugas,
|
|
100
|
+
`\nFile: ${step.file.join(', ')}`,
|
|
101
|
+
step.catatan ? `\nCatatan: ${step.catatan}` : '',
|
|
102
|
+
]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join('');
|
|
105
|
+
await runTurn(s, stepPrompt);
|
|
106
|
+
}
|
|
107
|
+
// ③ Reviewer — pass the plan so it knows which files were touched
|
|
108
|
+
console.log(`\n${bold('③ Peninjau')} ${dim(`(${opts.models.peninjau})`)}`);
|
|
109
|
+
const stepSummary = plan.langkah
|
|
110
|
+
.map((s, i) => ` ${i + 1}. ${s.tugas} [${s.file.join(', ')}]`)
|
|
111
|
+
.join('\n');
|
|
112
|
+
const rev = newSession(opts.models.peninjau, 'rencana', opts.otomatis);
|
|
113
|
+
rev.messages.push({ role: 'system', content: REVIEWER_BRIEF });
|
|
114
|
+
await runTurn(rev, `Tinjau hasil implementasi untuk tugas: ${task}\n\nLangkah yang sudah dikerjakan:\n${stepSummary}`);
|
|
36
115
|
console.log(green('\n✓ Tim selesai.'));
|
|
37
116
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Custom team (unchanged)
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Cap output passed between roles to prevent context inflation across a long pipeline.
|
|
121
|
+
const MAX_ROLE_OUTPUT = 3_000;
|
|
42
122
|
export async function runCustomTeam(task, opts) {
|
|
43
123
|
const { config, otomatis, modelOverrides = {} } = opts;
|
|
44
124
|
const alur = config.alur.filter((r) => r in config.peran);
|
|
45
125
|
console.log(bold(`👥 Tim "${config.nama}": ${alur.join(' → ')}`));
|
|
46
|
-
// Warn about models that may lack tool support (best-effort, silent on network error)
|
|
47
126
|
const uniqueModels = [
|
|
48
127
|
...new Set(alur.map((r) => modelOverrides[r] ?? config.peran[r]?.model).filter(Boolean)),
|
|
49
128
|
];
|
|
@@ -67,7 +146,11 @@ export async function runCustomTeam(task, opts) {
|
|
|
67
146
|
if (roleCfg.brief)
|
|
68
147
|
s.messages.push({ role: 'system', content: roleCfg.brief });
|
|
69
148
|
const result = await runTurn(s, context);
|
|
70
|
-
|
|
149
|
+
const trimmed = (result ?? '').trim();
|
|
150
|
+
const capped = trimmed.length > MAX_ROLE_OUTPUT
|
|
151
|
+
? `${trimmed.slice(0, MAX_ROLE_OUTPUT)}\n…[dipotong — ${trimmed.length - MAX_ROLE_OUTPUT} char lebih]`
|
|
152
|
+
: trimmed;
|
|
153
|
+
context = `Tugas asal: ${task}\n\nOutput dari ${roleName}:\n${capped || '(tidak ada output eksplisit)'}`;
|
|
71
154
|
}
|
|
72
155
|
console.log(green(`\n✓ Tim "${config.nama}" selesai.`));
|
|
73
156
|
}
|