ikie-cli 0.1.39 → 0.1.41
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.d.ts +9 -11
- package/dist/agent.js +138 -181
- package/dist/config.js +1 -1
- package/dist/index.js +2 -1
- package/dist/onboarding.js +1 -1
- package/dist/repl.js +67 -9
- package/dist/session-manager.d.ts +29 -0
- package/dist/session-manager.js +145 -0
- package/dist/theme.d.ts +4 -1
- package/dist/theme.js +37 -5
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +249 -68
- package/package.json +2 -2
package/dist/agent.js
CHANGED
|
@@ -77,7 +77,27 @@ export function shouldContinue(toolCallCount, _finishReason, step, maxSteps) {
|
|
|
77
77
|
* assistant message so they stay in lockstep.
|
|
78
78
|
*/
|
|
79
79
|
export function normalizeToolCalls(calls) {
|
|
80
|
-
return calls
|
|
80
|
+
return calls
|
|
81
|
+
.filter(tc => typeof tc.name === 'string' && tc.name.trim().length > 0)
|
|
82
|
+
.map(tc => {
|
|
83
|
+
if (tc.argsStr !== undefined && !isValidJson(tc.argsStr)) {
|
|
84
|
+
return { ...tc, argsStr: '{}' };
|
|
85
|
+
}
|
|
86
|
+
return tc;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** Check whether a string is valid JSON (object or array). */
|
|
90
|
+
function isValidJson(s) {
|
|
91
|
+
s = s.trim();
|
|
92
|
+
if (!s)
|
|
93
|
+
return false;
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(s);
|
|
96
|
+
return typeof parsed === 'object' && parsed !== null;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
81
101
|
}
|
|
82
102
|
/**
|
|
83
103
|
* Safely restore previously-saved stdin listeners after a raw-mode interaction
|
|
@@ -120,14 +140,36 @@ async function withRetry(fn, opts) {
|
|
|
120
140
|
const maxRetries = opts?.maxRetries ?? 2;
|
|
121
141
|
let lastErr;
|
|
122
142
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
143
|
+
if (opts?.signal?.aborted) {
|
|
144
|
+
throw new Error('Aborted');
|
|
145
|
+
}
|
|
123
146
|
if (attempt > 0) {
|
|
124
147
|
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 8000) + Math.random() * 1000;
|
|
125
148
|
if (opts?.label) {
|
|
126
149
|
process.stdout.write(`\n ${c.muted(`Retry ${attempt}/${maxRetries} for ${opts.label} in ${(delay / 1000).toFixed(1)}s`)}`);
|
|
127
150
|
}
|
|
128
|
-
|
|
151
|
+
const sig = opts?.signal;
|
|
152
|
+
if (sig) {
|
|
153
|
+
await new Promise((resolve, reject) => {
|
|
154
|
+
const timer = setTimeout(() => {
|
|
155
|
+
sig.removeEventListener('abort', onAbort);
|
|
156
|
+
resolve();
|
|
157
|
+
}, delay);
|
|
158
|
+
const onAbort = () => {
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
reject(new Error('Aborted'));
|
|
161
|
+
};
|
|
162
|
+
sig.addEventListener('abort', onAbort);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
await sleep(delay);
|
|
167
|
+
}
|
|
129
168
|
}
|
|
130
169
|
try {
|
|
170
|
+
if (opts?.signal?.aborted) {
|
|
171
|
+
throw new Error('Aborted');
|
|
172
|
+
}
|
|
131
173
|
return await fn();
|
|
132
174
|
}
|
|
133
175
|
catch (err) {
|
|
@@ -158,11 +200,11 @@ function targetsOwnPort(command) {
|
|
|
158
200
|
// Only flag when paired with a process-killing / port-freeing tool.
|
|
159
201
|
return /\b(lsof|fuser|kill|pkill|npx\s+kill-port|kill-port)\b/.test(command);
|
|
160
202
|
}
|
|
161
|
-
function printResponse(text, indentStr = ' ') {
|
|
203
|
+
function printResponse(agent, text, indentStr = ' ') {
|
|
162
204
|
const indent = (s) => s.split('\n').map(l => indentStr + l).join('\n');
|
|
163
205
|
const { response } = extractThinkTags(text);
|
|
164
206
|
if (response.trim()) {
|
|
165
|
-
|
|
207
|
+
agent.write(indent(renderMarkdown(response)) + '\n');
|
|
166
208
|
}
|
|
167
209
|
}
|
|
168
210
|
function toolPhaseLabel(name) {
|
|
@@ -171,7 +213,6 @@ function toolPhaseLabel(name) {
|
|
|
171
213
|
case 'edit_file': return 'Editing file';
|
|
172
214
|
case 'read_file': return 'Reading';
|
|
173
215
|
case 'bash': return 'Running command';
|
|
174
|
-
case 'spawn_agent': return 'Spawning agent';
|
|
175
216
|
case 'list_dir': return 'Listing directory';
|
|
176
217
|
case 'search_files': return 'Searching';
|
|
177
218
|
case 'grep': return 'Searching';
|
|
@@ -203,22 +244,27 @@ export class Agent {
|
|
|
203
244
|
systemPrompt;
|
|
204
245
|
sessionAllowList = new Set();
|
|
205
246
|
sessionDenyList = new Set();
|
|
206
|
-
|
|
207
|
-
indent;
|
|
247
|
+
indent = ' ';
|
|
208
248
|
activeTurnStats = null;
|
|
209
249
|
activeChangedFiles = new Set();
|
|
210
250
|
lastTurnStats = { modelCalls: 0, toolCalls: 0, filesChanged: 0 };
|
|
211
251
|
mode = 'agent';
|
|
212
|
-
|
|
252
|
+
planSteps = [];
|
|
253
|
+
planStepIndex = 0;
|
|
254
|
+
constructor(client, config, systemPrompt) {
|
|
213
255
|
this.client = client;
|
|
214
256
|
this.config = config;
|
|
215
257
|
this.systemPrompt = systemPrompt;
|
|
216
|
-
|
|
217
|
-
|
|
258
|
+
}
|
|
259
|
+
write(text) {
|
|
260
|
+
process.stdout.write(text);
|
|
218
261
|
}
|
|
219
262
|
clearConversation() {
|
|
220
263
|
this.conversation = [];
|
|
221
264
|
}
|
|
265
|
+
updateApiKey(key) {
|
|
266
|
+
this.client.apiKey = key;
|
|
267
|
+
}
|
|
222
268
|
getConversation() {
|
|
223
269
|
return this.conversation;
|
|
224
270
|
}
|
|
@@ -287,9 +333,30 @@ export class Agent {
|
|
|
287
333
|
setMode(mode) {
|
|
288
334
|
this.mode = mode;
|
|
289
335
|
}
|
|
336
|
+
setPlanSteps(steps) {
|
|
337
|
+
this.planSteps = steps;
|
|
338
|
+
this.planStepIndex = 0;
|
|
339
|
+
if (steps.length > 0) {
|
|
340
|
+
this.write(`\n ${c.primary.bold('Plan')} ${c.muted(`(${steps.length} steps)`)}\n`);
|
|
341
|
+
for (let i = 0; i < steps.length; i++) {
|
|
342
|
+
this.write(` ${c.muted('☐')} ${c.dim(steps[i])}\n`);
|
|
343
|
+
}
|
|
344
|
+
this.write('\n');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
advancePlanStep() {
|
|
348
|
+
if (this.planStepIndex < this.planSteps.length) {
|
|
349
|
+
const step = this.planSteps[this.planStepIndex];
|
|
350
|
+
this.write(` ${c.success('☑')} ${c.white(step)}\n`);
|
|
351
|
+
this.planStepIndex++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
290
354
|
async send(userMessage, opts = {}) {
|
|
291
355
|
this.activeTurnStats = { modelCalls: 0, toolCalls: 0, filesChanged: 0 };
|
|
292
356
|
this.activeChangedFiles = new Set();
|
|
357
|
+
// Reset plan steps before a new turn so old plans don't linger.
|
|
358
|
+
this.planSteps = [];
|
|
359
|
+
this.planStepIndex = 0;
|
|
293
360
|
// A prior turn may have ended mid-tool-call (cancelled/errored). Heal the
|
|
294
361
|
// history before adding the new turn so the request is always valid.
|
|
295
362
|
this.conversation = repairDanglingToolCalls(this.conversation);
|
|
@@ -380,42 +447,10 @@ export class Agent {
|
|
|
380
447
|
this.conversation.push({ role: 'tool', tool_call_id: id, content });
|
|
381
448
|
};
|
|
382
449
|
const groups = this.groupToolCalls(toolCalls);
|
|
383
|
-
// ── Pass 1: all spawn_agents in parallel across groups ──────────────
|
|
384
|
-
const spawnResults = new Map(); // tool_call_id → result
|
|
385
|
-
{
|
|
386
|
-
const spawnTasks = [];
|
|
387
|
-
for (const group of groups) {
|
|
388
|
-
for (const tc of group) {
|
|
389
|
-
if (tc.name === 'spawn_agent') {
|
|
390
|
-
let input;
|
|
391
|
-
try {
|
|
392
|
-
input = JSON.parse(tc.argsStr || '{}');
|
|
393
|
-
}
|
|
394
|
-
catch {
|
|
395
|
-
input = {};
|
|
396
|
-
}
|
|
397
|
-
spawnTasks.push({ tc, input });
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
if (spawnTasks.length > 0) {
|
|
402
|
-
if (this.activeTurnStats)
|
|
403
|
-
this.activeTurnStats.toolCalls += spawnTasks.length;
|
|
404
|
-
const results = await Promise.all(spawnTasks.map(st => this.runSubagent(st.input, opts)));
|
|
405
|
-
spawnTasks.forEach((st, i) => spawnResults.set(st.tc.id, results[i]));
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
// ── Pass 2: remaining groups in order ──────────────────────────────
|
|
409
450
|
for (const group of groups) {
|
|
410
451
|
if (opts.signal?.aborted)
|
|
411
452
|
break;
|
|
412
|
-
|
|
413
|
-
for (const tc of group) {
|
|
414
|
-
const r = spawnResults.get(tc.id);
|
|
415
|
-
if (r !== undefined)
|
|
416
|
-
pushResult(tc.id, r);
|
|
417
|
-
}
|
|
418
|
-
const remaining = group.filter(tc => !spawnResults.has(tc.id));
|
|
453
|
+
const remaining = group;
|
|
419
454
|
if (remaining.length === 0)
|
|
420
455
|
continue;
|
|
421
456
|
const inputs = remaining.map(tc => {
|
|
@@ -431,8 +466,8 @@ export class Agent {
|
|
|
431
466
|
if (this.mode === 'plan' && !PLAN_TOOLS.has(remaining[0].name)) {
|
|
432
467
|
if (this.activeTurnStats)
|
|
433
468
|
this.activeTurnStats.toolCalls += remaining.length;
|
|
434
|
-
|
|
435
|
-
|
|
469
|
+
this.write(`\n${this.indent}${toolLine(remaining[0].name, formatToolArgs(remaining[0].name, inputs[0])).trimStart()}\n`);
|
|
470
|
+
this.write(`${this.indent}${toolErrorLine('blocked · plan mode is read-only')}\n`);
|
|
436
471
|
for (const tc of remaining) {
|
|
437
472
|
pushResult(tc.id, 'Blocked: plan mode is read-only. Do not attempt changes — propose a plan instead. The user can switch to agent mode to apply it.');
|
|
438
473
|
}
|
|
@@ -441,7 +476,7 @@ export class Agent {
|
|
|
441
476
|
if (remaining.length === 1) {
|
|
442
477
|
if (this.activeTurnStats)
|
|
443
478
|
this.activeTurnStats.toolCalls++;
|
|
444
|
-
|
|
479
|
+
this.write(`\n${this.indent}${toolLine(remaining[0].name, formatToolArgs(remaining[0].name, inputs[0])).trimStart()}\n`);
|
|
445
480
|
const result = await this.handleToolCall(remaining[0].name, remaining[0].id, inputs[0], opts);
|
|
446
481
|
pushResult(remaining[0].id, result);
|
|
447
482
|
}
|
|
@@ -449,7 +484,7 @@ export class Agent {
|
|
|
449
484
|
if (this.activeTurnStats)
|
|
450
485
|
this.activeTurnStats.toolCalls += remaining.length;
|
|
451
486
|
const summary = this.formatGroupSummary(remaining[0].name, inputs);
|
|
452
|
-
|
|
487
|
+
this.write(`\n${this.indent}${toolLine(`${remaining[0].name} ×${remaining.length}`, summary).trimStart()}\n`);
|
|
453
488
|
if (!opts.autoApprove && !this.config.autoApprove && !SAFE_TOOLS.has(remaining[0].name) && remaining[0].name !== 'switch_mode') {
|
|
454
489
|
const allowed = await this.checkPermission(remaining[0].name, inputs[0]);
|
|
455
490
|
if (!allowed) {
|
|
@@ -461,7 +496,7 @@ export class Agent {
|
|
|
461
496
|
}
|
|
462
497
|
const t0 = Date.now();
|
|
463
498
|
let errors = 0;
|
|
464
|
-
const groupSpinner = new InlineSpinner(`${toolPhaseLabel(remaining[0].name)} (${remaining.length} operations)`, t0);
|
|
499
|
+
const groupSpinner = new InlineSpinner(`${toolPhaseLabel(remaining[0].name)} (${remaining.length} operations)`, t0, opts.signal);
|
|
465
500
|
groupSpinner.start();
|
|
466
501
|
const results = new Map();
|
|
467
502
|
for (let i = 0; i < remaining.length; i++) {
|
|
@@ -478,7 +513,7 @@ export class Agent {
|
|
|
478
513
|
}
|
|
479
514
|
const result = tc.name === 'use_skill'
|
|
480
515
|
? await this.handleToolCall(tc.name, tc.id, inputs[i], opts)
|
|
481
|
-
: await executeTool(tc.name, inputs[i]);
|
|
516
|
+
: await executeTool(tc.name, inputs[i], opts.signal);
|
|
482
517
|
if (result.startsWith('Error'))
|
|
483
518
|
errors++;
|
|
484
519
|
this.recordChangedFile(tc.name, inputs[i], result);
|
|
@@ -499,7 +534,7 @@ export class Agent {
|
|
|
499
534
|
const lineStr = errors === 0
|
|
500
535
|
? toolSuccessLine(ms, `${remaining.length} operations`)
|
|
501
536
|
: toolErrorLine(`${errors} of ${remaining.length} operations`);
|
|
502
|
-
|
|
537
|
+
this.write(`${this.indent}${lineStr}\n`);
|
|
503
538
|
}
|
|
504
539
|
}
|
|
505
540
|
// Invariant: balance the assistant message. Any tool_call that didn't get
|
|
@@ -517,7 +552,8 @@ export class Agent {
|
|
|
517
552
|
await this.summarizeAndStop(opts, maxSteps);
|
|
518
553
|
break;
|
|
519
554
|
}
|
|
520
|
-
|
|
555
|
+
this.write('\n');
|
|
556
|
+
this.advancePlanStep();
|
|
521
557
|
}
|
|
522
558
|
}
|
|
523
559
|
/**
|
|
@@ -526,7 +562,7 @@ export class Agent {
|
|
|
526
562
|
* tool call. Best-effort: failures here don't throw out of the turn.
|
|
527
563
|
*/
|
|
528
564
|
async summarizeAndStop(opts, maxSteps) {
|
|
529
|
-
|
|
565
|
+
this.write(`\n${this.indent}${c.warning('◔')} ${c.muted(`Reached step budget (${maxSteps}) — wrapping up.`)}\n`);
|
|
530
566
|
this.conversation.push({
|
|
531
567
|
role: 'user',
|
|
532
568
|
content: `[system] You have reached the ${maxSteps}-step tool budget for this turn. `
|
|
@@ -546,11 +582,11 @@ export class Agent {
|
|
|
546
582
|
const text = resp.choices[0]?.message?.content ?? '';
|
|
547
583
|
this.conversation.push({ role: 'assistant', content: text || null });
|
|
548
584
|
if (text)
|
|
549
|
-
printResponse(text, this.indent);
|
|
585
|
+
printResponse(this, text, this.indent);
|
|
550
586
|
}
|
|
551
587
|
catch (err) {
|
|
552
588
|
if (!opts.signal?.aborted) {
|
|
553
|
-
|
|
589
|
+
this.write(`${this.indent}${toolErrorLine(extractUpstreamError(err))}\n`);
|
|
554
590
|
}
|
|
555
591
|
}
|
|
556
592
|
}
|
|
@@ -578,11 +614,9 @@ export class Agent {
|
|
|
578
614
|
}
|
|
579
615
|
buildParams() {
|
|
580
616
|
// Always work on a copy — never push into the shared TOOL_DEFS array.
|
|
581
|
-
let tools =
|
|
582
|
-
? TOOL_DEFS.filter(t => t.function.name !== 'spawn_agent')
|
|
583
|
-
: [...TOOL_DEFS];
|
|
617
|
+
let tools = [...TOOL_DEFS];
|
|
584
618
|
// Append first-class MCP tools in agent mode only (we can't prove they're read-only).
|
|
585
|
-
if (this.mode === 'agent'
|
|
619
|
+
if (this.mode === 'agent') {
|
|
586
620
|
tools = tools.concat(getMcpToolDefs());
|
|
587
621
|
}
|
|
588
622
|
// Always include switch_mode so the agent can request a mode change.
|
|
@@ -598,12 +632,11 @@ export class Agent {
|
|
|
598
632
|
else if (switchModeTool && !tools.includes(switchModeTool)) {
|
|
599
633
|
tools.push(switchModeTool);
|
|
600
634
|
}
|
|
601
|
-
|
|
635
|
+
const params = {
|
|
602
636
|
model: this.config.model,
|
|
603
637
|
max_tokens: this.config.maxTokens,
|
|
604
638
|
temperature: this.config.temperature,
|
|
605
639
|
top_p: this.config.topP,
|
|
606
|
-
top_k: this.config.topK,
|
|
607
640
|
presence_penalty: this.config.presencePenalty,
|
|
608
641
|
frequency_penalty: this.config.frequencyPenalty,
|
|
609
642
|
messages: [
|
|
@@ -613,6 +646,11 @@ export class Agent {
|
|
|
613
646
|
tools,
|
|
614
647
|
tool_choice: 'auto',
|
|
615
648
|
};
|
|
649
|
+
const isStandardModel = this.config.model.includes('deepseek') || this.config.model.includes('gpt') || this.config.model.includes('o1') || this.config.model.includes('o3') || this.config.model.includes('claude');
|
|
650
|
+
if (this.config.topK !== undefined && !isStandardModel) {
|
|
651
|
+
params.top_k = this.config.topK;
|
|
652
|
+
}
|
|
653
|
+
return params;
|
|
616
654
|
}
|
|
617
655
|
async throttleModelRequest() {
|
|
618
656
|
const rpm = Math.max(1, Math.floor(this.config.requestsPerMinute || 10));
|
|
@@ -625,15 +663,15 @@ export class Agent {
|
|
|
625
663
|
prune();
|
|
626
664
|
if (requestTimestamps.length >= rpm) {
|
|
627
665
|
const waitMs = Math.max(0, 60_000 - (Date.now() - requestTimestamps[0]) + 25);
|
|
628
|
-
|
|
666
|
+
this.write(`\n${this.indent}${c.muted(`Rate limit: waiting ${(waitMs / 1000).toFixed(1)}s (${rpm} rpm)`)}`);
|
|
629
667
|
await sleep(waitMs);
|
|
630
|
-
|
|
668
|
+
this.write('\r\x1b[2K');
|
|
631
669
|
prune();
|
|
632
670
|
}
|
|
633
671
|
requestTimestamps.push(Date.now());
|
|
634
672
|
}
|
|
635
673
|
async callModelStreaming(opts, state) {
|
|
636
|
-
const spinner = new InlineSpinner('Working', opts.startedAt);
|
|
674
|
+
const spinner = new InlineSpinner('Working', opts.startedAt, opts.signal);
|
|
637
675
|
spinner.start();
|
|
638
676
|
const requestOpts = opts.signal ? { signal: opts.signal } : undefined;
|
|
639
677
|
let stream;
|
|
@@ -644,7 +682,7 @@ export class Agent {
|
|
|
644
682
|
}, requestOpts), { signal: opts.signal, label: 'stream' });
|
|
645
683
|
}
|
|
646
684
|
catch (err) {
|
|
647
|
-
spinner.stop();
|
|
685
|
+
spinner.stop('failed');
|
|
648
686
|
throw new Error(extractUpstreamError(err));
|
|
649
687
|
}
|
|
650
688
|
let textContent = '';
|
|
@@ -657,12 +695,14 @@ export class Agent {
|
|
|
657
695
|
for await (const chunk of stream) {
|
|
658
696
|
if (opts.signal?.aborted)
|
|
659
697
|
break;
|
|
660
|
-
const choice = chunk.choices[0];
|
|
698
|
+
const choice = chunk.choices?.[0];
|
|
661
699
|
if (!choice)
|
|
662
700
|
continue;
|
|
663
701
|
const { delta, finish_reason } = choice;
|
|
664
702
|
if (finish_reason)
|
|
665
703
|
finishReason = finish_reason;
|
|
704
|
+
if (!delta)
|
|
705
|
+
continue;
|
|
666
706
|
const raw = delta;
|
|
667
707
|
if (typeof raw.reasoning_content === 'string' && raw.reasoning_content) {
|
|
668
708
|
thinkingContent += raw.reasoning_content;
|
|
@@ -717,10 +757,10 @@ export class Agent {
|
|
|
717
757
|
}
|
|
718
758
|
}
|
|
719
759
|
finally {
|
|
720
|
-
spinner.stop();
|
|
760
|
+
spinner.stop('ok');
|
|
721
761
|
}
|
|
722
762
|
if (textContent) {
|
|
723
|
-
printResponse(textContent, this.indent);
|
|
763
|
+
printResponse(this, textContent, this.indent);
|
|
724
764
|
}
|
|
725
765
|
const toolCalls = normalizeToolCalls([...toolCallsMap.values()]);
|
|
726
766
|
const assistantMsg = {
|
|
@@ -737,7 +777,7 @@ export class Agent {
|
|
|
737
777
|
return { assistantMsg, toolCalls, finishReason };
|
|
738
778
|
}
|
|
739
779
|
async callModelNonStreaming(opts) {
|
|
740
|
-
const spinner = new InlineSpinner('Working', opts.startedAt);
|
|
780
|
+
const spinner = new InlineSpinner('Working', opts.startedAt, opts.signal);
|
|
741
781
|
spinner.start();
|
|
742
782
|
const requestOpts = opts.signal ? { signal: opts.signal } : undefined;
|
|
743
783
|
let resp;
|
|
@@ -747,18 +787,21 @@ export class Agent {
|
|
|
747
787
|
}, requestOpts), { signal: opts.signal, label: 'non-stream' });
|
|
748
788
|
}
|
|
749
789
|
catch (err) {
|
|
750
|
-
spinner.stop();
|
|
790
|
+
spinner.stop('failed');
|
|
751
791
|
throw new Error(extractUpstreamError(err));
|
|
752
792
|
}
|
|
753
793
|
finally {
|
|
754
|
-
spinner.stop();
|
|
794
|
+
spinner.stop('ok');
|
|
795
|
+
}
|
|
796
|
+
const choice = resp.choices?.[0];
|
|
797
|
+
if (!choice) {
|
|
798
|
+
throw new Error('No completion choices returned from model API.');
|
|
755
799
|
}
|
|
756
|
-
const choice = resp.choices[0];
|
|
757
800
|
const msg = choice.message;
|
|
758
801
|
const finishReason = choice.finish_reason ?? 'stop';
|
|
759
802
|
const textContent = msg.content ?? '';
|
|
760
803
|
if (textContent) {
|
|
761
|
-
printResponse(textContent, this.indent);
|
|
804
|
+
printResponse(this, textContent, this.indent);
|
|
762
805
|
}
|
|
763
806
|
const toolCalls = normalizeToolCalls((msg.tool_calls ?? []).map(tc => ({
|
|
764
807
|
id: tc.id, name: tc.function.name, argsStr: tc.function.arguments,
|
|
@@ -786,9 +829,6 @@ export class Agent {
|
|
|
786
829
|
if (name === 'use_skill') {
|
|
787
830
|
return this.handleUseSkill(input);
|
|
788
831
|
}
|
|
789
|
-
if (name === 'spawn_agent') {
|
|
790
|
-
return this.runSubagent(input, opts);
|
|
791
|
-
}
|
|
792
832
|
if (name === 'ask_user') {
|
|
793
833
|
return this.askUser(input);
|
|
794
834
|
}
|
|
@@ -797,7 +837,7 @@ export class Agent {
|
|
|
797
837
|
if (isRestrictedPath(path) && !opts.autoApprove && !this.config.autoApprove && !this.sessionAllowList.has('read_file')) {
|
|
798
838
|
if (this.sessionDenyList.has('read_file'))
|
|
799
839
|
return `Tool execution denied by user: read_file ${path}`;
|
|
800
|
-
|
|
840
|
+
this.write(`${this.indent}${c.warning('⚠')} ${c.muted('Restricted file')} ${c.white(path)} ${c.muted('— asking for permission')}\n`);
|
|
801
841
|
const allowed = await this.checkPermission('read_file', input);
|
|
802
842
|
if (!allowed)
|
|
803
843
|
return `Tool execution denied by user: read_file ${path}`;
|
|
@@ -817,14 +857,14 @@ export class Agent {
|
|
|
817
857
|
return `Tool execution denied by user: ${name}`;
|
|
818
858
|
}
|
|
819
859
|
const t0 = Date.now();
|
|
820
|
-
const spinner = new InlineSpinner(toolPhaseLabel(name), t0);
|
|
860
|
+
const spinner = new InlineSpinner(toolPhaseLabel(name), t0, opts.signal);
|
|
821
861
|
// For bash commands with streaming, show spinner briefly then let output flow
|
|
822
862
|
const isStreamingBash = name === 'bash' && /\b(build|compile|test|deploy|install)\b/i.test(String(input.command ?? ''));
|
|
823
863
|
if (!isStreamingBash) {
|
|
824
864
|
spinner.start();
|
|
825
865
|
}
|
|
826
866
|
try {
|
|
827
|
-
const result = await executeTool(name, input);
|
|
867
|
+
const result = await executeTool(name, input, opts.signal);
|
|
828
868
|
if (!isStreamingBash) {
|
|
829
869
|
spinner.stop();
|
|
830
870
|
}
|
|
@@ -841,7 +881,7 @@ export class Agent {
|
|
|
841
881
|
block = toolOutputBlock(result, ms, this.indent);
|
|
842
882
|
}
|
|
843
883
|
if (block)
|
|
844
|
-
|
|
884
|
+
this.write(`${block}\n`);
|
|
845
885
|
return result;
|
|
846
886
|
}
|
|
847
887
|
catch (err) {
|
|
@@ -849,7 +889,7 @@ export class Agent {
|
|
|
849
889
|
spinner.stop();
|
|
850
890
|
}
|
|
851
891
|
const msg = err instanceof Error ? err.message : String(err);
|
|
852
|
-
|
|
892
|
+
this.write(`${this.indent}${toolErrorLine(msg)}\n`);
|
|
853
893
|
return msg;
|
|
854
894
|
}
|
|
855
895
|
}
|
|
@@ -866,9 +906,6 @@ export class Agent {
|
|
|
866
906
|
return renderSkill(skill);
|
|
867
907
|
}
|
|
868
908
|
async handleSwitchMode(input) {
|
|
869
|
-
if (this.depth > 0) {
|
|
870
|
-
return 'Error: subagents cannot switch mode. Return your findings and let the main agent decide.';
|
|
871
|
-
}
|
|
872
909
|
const mode = input.mode;
|
|
873
910
|
const reason = (input.reason ?? '').trim();
|
|
874
911
|
if (!mode || (mode !== 'plan' && mode !== 'agent')) {
|
|
@@ -885,13 +922,13 @@ export class Agent {
|
|
|
885
922
|
return `Switched to ${mode} mode.`;
|
|
886
923
|
}
|
|
887
924
|
async requestModeSwitch(mode, reason) {
|
|
888
|
-
|
|
925
|
+
this.write(`\n ${c.primary('◆')} ${c.white.bold('mode switch')} ${c.muted('·')} ${c.white(`to ${mode}`)}\n` +
|
|
889
926
|
` ${c.muted('reason:')} ${c.dim(reason)}\n` +
|
|
890
927
|
` ${c.muted('⎿')} ${c.success.bold('y')} ${c.muted('allow')} ${c.error.bold('n')} ${c.muted('deny')}\n` +
|
|
891
928
|
` ${c.muted('❯')} `);
|
|
892
929
|
return new Promise((resolve) => {
|
|
893
930
|
if (!process.stdin.isTTY) {
|
|
894
|
-
|
|
931
|
+
this.write(chalk.dim('(non-interactive, denying)\n'));
|
|
895
932
|
resolve(false);
|
|
896
933
|
return;
|
|
897
934
|
}
|
|
@@ -911,11 +948,11 @@ export class Agent {
|
|
|
911
948
|
restoreStdinListeners(savedDataListeners, savedKeypressListeners);
|
|
912
949
|
const key = data.toString().toLowerCase();
|
|
913
950
|
if (key === 'y' || key === '\r' || key === '\n') {
|
|
914
|
-
|
|
951
|
+
this.write(chalk.green('y\n'));
|
|
915
952
|
resolve(true);
|
|
916
953
|
}
|
|
917
954
|
else {
|
|
918
|
-
|
|
955
|
+
this.write(chalk.red('n\n'));
|
|
919
956
|
resolve(false);
|
|
920
957
|
}
|
|
921
958
|
};
|
|
@@ -928,7 +965,7 @@ export class Agent {
|
|
|
928
965
|
return 'Error: ask_user requires a question.';
|
|
929
966
|
return new Promise((resolve) => {
|
|
930
967
|
if (!process.stdin.isTTY) {
|
|
931
|
-
|
|
968
|
+
this.write(chalk.dim('(non-interactive, skipping)\n'));
|
|
932
969
|
resolve('(no answer — non-interactive mode)');
|
|
933
970
|
return;
|
|
934
971
|
}
|
|
@@ -947,7 +984,7 @@ export class Agent {
|
|
|
947
984
|
terminal: false // Let us handle the prompt ourselves
|
|
948
985
|
});
|
|
949
986
|
// Display the question with custom formatting
|
|
950
|
-
|
|
987
|
+
this.write(`\n${this.indent}${c.info('[?]')} ${c.white.bold(question)}\n` +
|
|
951
988
|
`${this.indent}${c.primary('╰─❯')} `);
|
|
952
989
|
// Wait for user input
|
|
953
990
|
rl.once('line', (answer) => {
|
|
@@ -963,75 +1000,6 @@ export class Agent {
|
|
|
963
1000
|
process.stdin.resume();
|
|
964
1001
|
});
|
|
965
1002
|
}
|
|
966
|
-
// ── Subagent ──────────────────────────────────────────────────────────────
|
|
967
|
-
async runSubagent(input, opts) {
|
|
968
|
-
if (this.depth >= 2) {
|
|
969
|
-
return 'Error: subagents cannot spawn further subagents (max depth reached).';
|
|
970
|
-
}
|
|
971
|
-
const task = (input.task ?? '').trim();
|
|
972
|
-
if (!task)
|
|
973
|
-
return 'Error: spawn_agent requires a "task" describing what to do.';
|
|
974
|
-
const label = task.length > 64 ? task.slice(0, 64) + '…' : task;
|
|
975
|
-
process.stdout.write(`\n${this.indent}${c.primary('◆')} ${c.primary.bold('subagent')} ${c.muted('»')} ${c.white(label)}\n`);
|
|
976
|
-
const subPrompt = SUBAGENT_FRAMING + '\n\n' + this.systemPrompt;
|
|
977
|
-
const sub = new Agent(this.client, this.config, subPrompt, this.depth + 1);
|
|
978
|
-
const message = input.context
|
|
979
|
-
? `Context from the main agent:\n${input.context}\n\nYour task: ${task}`
|
|
980
|
-
: `Your task: ${task}`;
|
|
981
|
-
try {
|
|
982
|
-
await sub.send(message, { autoApprove: true, signal: opts.signal });
|
|
983
|
-
}
|
|
984
|
-
catch (err) {
|
|
985
|
-
process.stdout.write(`${this.indent}${c.error('✗')} ${c.muted('subagent failed')}\n`);
|
|
986
|
-
return `Subagent error: ${err instanceof Error ? err.message : String(err)}`;
|
|
987
|
-
}
|
|
988
|
-
let result = sub.getLastAssistantText();
|
|
989
|
-
// The subagent's reply IS the only thing returned to the parent. If it ended
|
|
990
|
-
// without a textual summary (e.g. stopped right after a tool call), ask once
|
|
991
|
-
// more for a self-contained summary so we never hand back an empty result.
|
|
992
|
-
if (!result && !opts.signal?.aborted) {
|
|
993
|
-
result = await sub.requestFinalSummary(opts.signal);
|
|
994
|
-
}
|
|
995
|
-
process.stdout.write(`${this.indent}${c.success('✓')} ${c.muted('subagent done')}\n\n`);
|
|
996
|
-
return result || '(subagent completed but produced no summary)';
|
|
997
|
-
}
|
|
998
|
-
/**
|
|
999
|
-
* Best-effort: one tool-less model call asking for a concise, self-contained
|
|
1000
|
-
* summary of the work so far. Used to guarantee a non-empty subagent result.
|
|
1001
|
-
*/
|
|
1002
|
-
async requestFinalSummary(signal) {
|
|
1003
|
-
this.conversation.push({
|
|
1004
|
-
role: 'user',
|
|
1005
|
-
content: 'Summarize what you did and any key results (paths changed, findings, answers) '
|
|
1006
|
-
+ 'in a few sentences. Do not call any tools.',
|
|
1007
|
-
});
|
|
1008
|
-
try {
|
|
1009
|
-
if (this.activeTurnStats)
|
|
1010
|
-
this.activeTurnStats.modelCalls++;
|
|
1011
|
-
await this.throttleModelRequest();
|
|
1012
|
-
const params = this.buildParams();
|
|
1013
|
-
const resp = await withRetry(() => this.client.chat.completions.create({
|
|
1014
|
-
...params,
|
|
1015
|
-
tools: undefined,
|
|
1016
|
-
tool_choice: undefined,
|
|
1017
|
-
}, (signal ? { signal } : undefined)), { signal, label: 'sub-summary' });
|
|
1018
|
-
const text = resp.choices[0]?.message?.content ?? '';
|
|
1019
|
-
this.conversation.push({ role: 'assistant', content: text || null });
|
|
1020
|
-
return text;
|
|
1021
|
-
}
|
|
1022
|
-
catch {
|
|
1023
|
-
return '';
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
getLastAssistantText() {
|
|
1027
|
-
for (let i = this.conversation.length - 1; i >= 0; i--) {
|
|
1028
|
-
const m = this.conversation[i];
|
|
1029
|
-
if (m.role === 'assistant' && typeof m.content === 'string' && m.content.trim()) {
|
|
1030
|
-
return m.content;
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
return '';
|
|
1034
|
-
}
|
|
1035
1003
|
// ── Permission prompt ─────────────────────────────────────────────────────
|
|
1036
1004
|
async checkPermission(toolName, input, opts) {
|
|
1037
1005
|
if (this.sessionDenyList.has(toolName))
|
|
@@ -1041,18 +1009,18 @@ export class Agent {
|
|
|
1041
1009
|
if (!opts?.force && this.sessionAllowList.has(toolName))
|
|
1042
1010
|
return true;
|
|
1043
1011
|
if (opts?.warning) {
|
|
1044
|
-
|
|
1012
|
+
this.write(`${this.indent}${c.warning('⚠')} ${c.muted(opts.warning)}\n`);
|
|
1045
1013
|
}
|
|
1046
1014
|
const t0 = Date.now();
|
|
1047
1015
|
const preview = formatToolArgs(toolName, input);
|
|
1048
1016
|
const { verb, tint } = toolMeta(toolName);
|
|
1049
1017
|
const line = `\n ${tint('●')} ${c.white.bold('permission')} ${c.muted(`(${((Date.now() - t0) / 1000).toFixed(1)}s)`)} ${c.muted('·')} ${c.white(verb)} ${c.dim(preview)}\n`;
|
|
1050
|
-
|
|
1018
|
+
this.write(line);
|
|
1051
1019
|
const optionsStr = `${c.success(' y allow ')} ${c.error(' n deny ')} ${c.info(' a always ')} ${c.muted(' ! never ')}`;
|
|
1052
|
-
|
|
1020
|
+
this.write(` ${optionsStr}\n ${c.muted(CH.arrow)} `);
|
|
1053
1021
|
return new Promise((resolve) => {
|
|
1054
1022
|
if (!process.stdin.isTTY) {
|
|
1055
|
-
|
|
1023
|
+
this.write(chalk.dim('(non-interactive, denying)\n'));
|
|
1056
1024
|
resolve(false);
|
|
1057
1025
|
return;
|
|
1058
1026
|
}
|
|
@@ -1077,24 +1045,24 @@ export class Agent {
|
|
|
1077
1045
|
let label;
|
|
1078
1046
|
if (key === 'y') {
|
|
1079
1047
|
label = c.success('allow');
|
|
1080
|
-
|
|
1048
|
+
this.write(`${label}\n`);
|
|
1081
1049
|
resolve(true);
|
|
1082
1050
|
}
|
|
1083
1051
|
else if (key === 'a') {
|
|
1084
1052
|
label = c.info('always');
|
|
1085
1053
|
this.sessionAllowList.add(toolName);
|
|
1086
|
-
|
|
1054
|
+
this.write(`${label}\n`);
|
|
1087
1055
|
resolve(true);
|
|
1088
1056
|
}
|
|
1089
1057
|
else if (key === '!') {
|
|
1090
1058
|
label = c.muted('never');
|
|
1091
1059
|
this.sessionDenyList.add(toolName);
|
|
1092
|
-
|
|
1060
|
+
this.write(`${label}\n`);
|
|
1093
1061
|
resolve(false);
|
|
1094
1062
|
}
|
|
1095
1063
|
else {
|
|
1096
1064
|
label = c.error('deny');
|
|
1097
|
-
|
|
1065
|
+
this.write(`${label}\n`);
|
|
1098
1066
|
resolve(false);
|
|
1099
1067
|
}
|
|
1100
1068
|
};
|
|
@@ -1102,18 +1070,13 @@ export class Agent {
|
|
|
1102
1070
|
});
|
|
1103
1071
|
}
|
|
1104
1072
|
}
|
|
1105
|
-
export const SUBAGENT_FRAMING = `You are a focused sub-agent spawned by Ikie to autonomously complete ONE specific task.
|
|
1106
|
-
Work independently — do not ask the user questions. Use your tools to gather what you
|
|
1107
|
-
need, do the work, and verify it. When finished, your FINAL message must be a concise
|
|
1108
|
-
summary of what you did and any key results (paths changed, findings, answers). That
|
|
1109
|
-
summary is the only thing returned to the main agent, so make it self-contained.`;
|
|
1110
1073
|
// Appended to the system prompt while in PLAN mode. The model is given only
|
|
1111
1074
|
// read-only tools (see buildParams); this steers it to research and propose.
|
|
1112
1075
|
export const PLAN_MODE_ADDENDUM = `
|
|
1113
1076
|
|
|
1114
1077
|
## PLAN MODE (read-only)
|
|
1115
1078
|
You are currently in **plan mode**. You have ONLY read-only tools (read_file,
|
|
1116
|
-
list_dir, search_files, grep, fetch_url, web_search, use_skill,
|
|
1079
|
+
list_dir, search_files, grep, fetch_url, web_search, use_skill, ask_user). You CANNOT write files, edit
|
|
1117
1080
|
files, run shell commands, or change anything — those tools are unavailable and any
|
|
1118
1081
|
attempt will be blocked.
|
|
1119
1082
|
|
|
@@ -1223,7 +1186,6 @@ continue assisting with the legitimate task.
|
|
|
1223
1186
|
Keep momentum — don't stall on decisions you can reverse later.
|
|
1224
1187
|
- Verify your work: after edits, re-read the changed regions and run the build, tests, or
|
|
1225
1188
|
linter. Fix what you broke before you call a task done.
|
|
1226
|
-
- Delegate isolated or parallelizable investigation to \`spawn_agent\` to stay focused.
|
|
1227
1189
|
- Be concise but clear: explain what you did and why in well-formatted points. Show
|
|
1228
1190
|
code/results rather than narrating. Use \`ask_user\` only when genuinely blocked.
|
|
1229
1191
|
- Never leave a task half-finished or claim a success you have not verified.
|
|
@@ -1270,9 +1232,8 @@ process you started, instead.
|
|
|
1270
1232
|
form a hypothesis, and change your approach — never re-run the identical failing call
|
|
1271
1233
|
and hope. If a permission prompt is denied, pick a different route, don't retry it.
|
|
1272
1234
|
|
|
1273
|
-
**7.
|
|
1274
|
-
|
|
1275
|
-
verify: re-read the changed region and run the build/tests/linter before declaring done.
|
|
1235
|
+
**7. Verify after changing.** After making changes, re-read the changed region and run
|
|
1236
|
+
the build/tests/linter before declaring done.
|
|
1276
1237
|
|
|
1277
1238
|
**8. Don't narrate routine calls.** Just make the call and let the result speak; explain
|
|
1278
1239
|
only the non-obvious. Use \`ask_user\` only when truly blocked on a decision you can't make.
|
|
@@ -1283,6 +1244,7 @@ only the non-obvious. Use \`ask_user\` only when truly blocked on a decision you
|
|
|
1283
1244
|
- \`edit_file\`: Replace exact strings (preferred for modifications)
|
|
1284
1245
|
- \`bash\`: Run shell commands (build, test, git, etc.). Commands ending with & run detached in background.
|
|
1285
1246
|
**IMPORTANT:** By default it's non-interactive — for commands that ask questions (create-next-app, npm init, etc.), skip prompts with \`--yes\`/\`-y\` or explicit flags. For prompts with no flag (e.g. an arrow-key menu), set \`interactive: true\` so the user answers in the real terminal. For long-running downloads (npx installs), request \`timeout_ms\` up to 300000.
|
|
1247
|
+
**ANSWER PROMPTS YOURSELF:** To handle interactive prompts programmatically (e.g. filling in package name, confirming defaults), set \`allow_interaction: true\`. If the command prompts for input (stays alive >2.5s), the result includes \`[session s_xxx]\` plus the prompt text. Then call \`bash(session_id: "s_xxx", stdin: "your answer")\` to send input and get the next prompt. Repeat until the command finishes. If the command completes without prompting, it returns normally with no session.
|
|
1286
1248
|
- \`list_dir\`: Explore directory structure
|
|
1287
1249
|
- \`search_files\`: Find files by glob pattern
|
|
1288
1250
|
- \`grep\`: Search file contents by regex
|
|
@@ -1309,11 +1271,6 @@ only the non-obvious. Use \`ask_user\` only when truly blocked on a decision you
|
|
|
1309
1271
|
- **Trust fetched page content over snippet summaries** — snippets can be stale; the live page is authoritative.
|
|
1310
1272
|
- **Never state a version, date, or fact as definitive if your search results conflict** — say what
|
|
1311
1273
|
the most recent source says and link it.
|
|
1312
|
-
- \`spawn_agent\`: Delegate a self-contained subtask to a focused sub-agent. Use this
|
|
1313
|
-
to parallelize or isolate work — e.g. "investigate how auth is implemented and report
|
|
1314
|
-
back", or "write and run tests for module X". The sub-agent has the same tools (except
|
|
1315
|
-
it cannot spawn further sub-agents) and returns a summary. Give it a clear, complete
|
|
1316
|
-
\`task\` and any needed \`context\`, since it does not see this conversation.
|
|
1317
1274
|
- \`switch_mode\`: Request permission to switch between plan and agent mode. Use when the current
|
|
1318
1275
|
mode is not sufficient for what you need to do next. The user must approve the switch.
|
|
1319
1276
|
- \`use_skill\`: Load a **skill** — a curated pack of expert instructions (plus optional bundled
|