lonny-agent 0.2.3 → 0.2.4
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/.claude/settings.local.json +14 -0
- package/dist/agent/index.js +1 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/project.d.ts +26 -0
- package/dist/agent/project.d.ts.map +1 -0
- package/dist/agent/project.js +303 -0
- package/dist/agent/project.js.map +1 -0
- package/dist/agent/prompt-builder.d.ts +1 -1
- package/dist/agent/prompt-builder.d.ts.map +1 -1
- package/dist/agent/prompt-builder.js +10 -5
- package/dist/agent/prompt-builder.js.map +1 -1
- package/dist/agent/providers/anthropic.d.ts.map +1 -1
- package/dist/agent/providers/anthropic.js +20 -2
- package/dist/agent/providers/anthropic.js.map +1 -1
- package/dist/agent/providers/google.js +1 -1
- package/dist/agent/providers/google.js.map +1 -1
- package/dist/agent/providers/ollama.d.ts.map +1 -1
- package/dist/agent/providers/ollama.js +2 -1
- package/dist/agent/providers/ollama.js.map +1 -1
- package/dist/agent/providers/openai.d.ts.map +1 -1
- package/dist/agent/providers/openai.js +23 -3
- package/dist/agent/providers/openai.js.map +1 -1
- package/dist/agent/session.d.ts +4 -2
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +206 -147
- package/dist/agent/session.js.map +1 -1
- package/dist/tools/__tests__/edit.test.js +14 -14
- package/dist/tools/__tests__/edit.test.js.map +1 -1
- package/dist/tools/edit.d.ts +17 -0
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +81 -34
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/read.d.ts +6 -0
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +42 -15
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +0 -4
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/write_plan.d.ts +1 -1
- package/dist/tools/write_plan.d.ts.map +1 -1
- package/dist/tools/write_plan.js +14 -4
- package/dist/tools/write_plan.js.map +1 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +5 -4
- package/dist/tui/index.js.map +1 -1
- package/dist/web/index.js +5 -5
- package/dist/web/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agent/index.ts +1 -1
- package/src/agent/project.ts +366 -0
- package/src/agent/prompt-builder.ts +11 -5
- package/src/agent/providers/anthropic.ts +9 -2
- package/src/agent/providers/google.ts +1 -1
- package/src/agent/providers/ollama.ts +5 -1
- package/src/agent/providers/openai.ts +15 -3
- package/src/agent/session.ts +216 -150
- package/src/tools/__tests__/edit.test.ts +14 -14
- package/src/tools/edit.ts +103 -36
- package/src/tools/read.ts +61 -25
- package/src/tools/registry.ts +260 -265
- package/src/tools/write_plan.ts +21 -5
- package/src/tui/index.ts +5 -4
- package/src/web/index.ts +5 -5
package/src/agent/session.ts
CHANGED
|
@@ -5,6 +5,7 @@ import * as path from 'node:path'
|
|
|
5
5
|
import type { Config } from '../config/index.js'
|
|
6
6
|
import { saveTokenUsage } from '../config/tokens.js'
|
|
7
7
|
import { FileReadTracker } from '../diff/apply.js'
|
|
8
|
+
import { fmtErr } from '../tools/errors.js'
|
|
8
9
|
import { ToolRegistry } from '../tools/registry.js'
|
|
9
10
|
import type { ToolCall, ToolResult } from '../tools/types.js'
|
|
10
11
|
import { compact, estimateMessagesTokens, shouldCompact } from './compaction.js'
|
|
@@ -291,7 +292,17 @@ export class Session {
|
|
|
291
292
|
this.provider = new AnthropicProvider(config.apiKey, config.baseUrl, config.model)
|
|
292
293
|
}
|
|
293
294
|
|
|
294
|
-
|
|
295
|
+
// Placeholder until initSystemPrompt() is called
|
|
296
|
+
this.messages = [{ role: 'system', content: '' }]
|
|
297
|
+
// Initialize system prompt asynchronously
|
|
298
|
+
this.initSystemPrompt(config)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Initialize the system prompt asynchronously */
|
|
302
|
+
private initSystemPrompt(config: Config): void {
|
|
303
|
+
buildSystemPrompt(config).then(prompt => {
|
|
304
|
+
this.messages = [{ role: 'system', content: prompt }]
|
|
305
|
+
})
|
|
295
306
|
}
|
|
296
307
|
|
|
297
308
|
/** Persist the current session to ~/.lonny/sessions/ */
|
|
@@ -314,7 +325,7 @@ export class Session {
|
|
|
314
325
|
}
|
|
315
326
|
|
|
316
327
|
/** Try to load a saved session for the given cwd. Returns null if none exists. */
|
|
317
|
-
static load(config: Config, output?: SessionOutput): Session | null {
|
|
328
|
+
static async load(config: Config, output?: SessionOutput): Promise<Session | null> {
|
|
318
329
|
const filePath = getSessionFilePath(config.cwd)
|
|
319
330
|
let data: SessionData
|
|
320
331
|
try {
|
|
@@ -340,7 +351,8 @@ export class Session {
|
|
|
340
351
|
data.provider !== config.provider ||
|
|
341
352
|
data.mode !== config.mode
|
|
342
353
|
) {
|
|
343
|
-
|
|
354
|
+
const prompt = await buildSystemPrompt(config)
|
|
355
|
+
session.messages[0] = { role: 'system', content: prompt }
|
|
344
356
|
}
|
|
345
357
|
// Restore token stats
|
|
346
358
|
session.totalInputTokens = data.totalInputTokens
|
|
@@ -359,9 +371,10 @@ export class Session {
|
|
|
359
371
|
}
|
|
360
372
|
}
|
|
361
373
|
|
|
362
|
-
setMode(mode: 'code' | 'plan' | 'ask'): void {
|
|
374
|
+
async setMode(mode: 'code' | 'plan' | 'ask'): Promise<void> {
|
|
363
375
|
this.config.mode = mode
|
|
364
|
-
|
|
376
|
+
const prompt = await buildSystemPrompt(this.config)
|
|
377
|
+
this.messages[0] = { role: 'system', content: prompt }
|
|
365
378
|
this.registry.setMode(mode)
|
|
366
379
|
this.save()
|
|
367
380
|
}
|
|
@@ -381,6 +394,8 @@ export class Session {
|
|
|
381
394
|
/** Reset the stopped flag for a new conversation */
|
|
382
395
|
resetStopped(): void {
|
|
383
396
|
this.stopped = false
|
|
397
|
+
// Create new AbortController for next conversation
|
|
398
|
+
this.abortController = new AbortController()
|
|
384
399
|
}
|
|
385
400
|
|
|
386
401
|
async chat(userPrompt: string): Promise<void> {
|
|
@@ -433,171 +448,211 @@ export class Session {
|
|
|
433
448
|
this.abortController.signal,
|
|
434
449
|
)
|
|
435
450
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
reasoningOutput
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
// Terminal display with box drawing (skip in Web UI mode, handled by EventBus)
|
|
451
|
-
if (!out?.suppressToolOutput) {
|
|
452
|
-
// Track column position on current line for wrapping
|
|
453
|
-
let thinkCol = 0
|
|
454
|
-
// Handle newlines in streamed content - add left border on each new line
|
|
455
|
-
// Also manually wrap long lines so wrapped lines keep the │ prefix.
|
|
456
|
-
let remaining = chunk.reasoning_content
|
|
457
|
-
const maxContentWidth = termWidth() - THINK_PREFIX_WIDTH
|
|
458
|
-
while (remaining.length > 0) {
|
|
459
|
-
if (reasoningLineStart) {
|
|
460
|
-
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
461
|
-
reasoningLineStart = false
|
|
462
|
-
thinkCol = 0
|
|
451
|
+
try {
|
|
452
|
+
for await (const chunk of stream) {
|
|
453
|
+
if (chunk.reasoning_content) {
|
|
454
|
+
reasoningContent = chunk.reasoning_content
|
|
455
|
+
// Stream reasoning content in real-time (only when no text in same chunk)
|
|
456
|
+
if (!chunk.text) {
|
|
457
|
+
// Emit thinking via EventBus for Web UI
|
|
458
|
+
bus.emit(EventChannels.THINKING, { text: chunk.reasoning_content })
|
|
459
|
+
if (!reasoningOutput) {
|
|
460
|
+
reasoningOutput = true
|
|
461
|
+
reasoningLineStart = true
|
|
462
|
+
if (!out?.suppressToolOutput) {
|
|
463
|
+
writeOut(thinkTopBorder(), out)
|
|
463
464
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
465
|
+
}
|
|
466
|
+
// Terminal display with box drawing (skip in Web UI mode, handled by EventBus)
|
|
467
|
+
if (!out?.suppressToolOutput) {
|
|
468
|
+
// Track column position on current line for wrapping
|
|
469
|
+
let thinkCol = 0
|
|
470
|
+
// Handle newlines in streamed content - add left border on each new line
|
|
471
|
+
// Also manually wrap long lines so wrapped lines keep the │ prefix.
|
|
472
|
+
let remaining = chunk.reasoning_content
|
|
473
|
+
const maxContentWidth = termWidth() - THINK_PREFIX_WIDTH
|
|
474
|
+
while (remaining.length > 0) {
|
|
475
|
+
if (reasoningLineStart) {
|
|
476
|
+
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
477
|
+
reasoningLineStart = false
|
|
478
|
+
thinkCol = 0
|
|
479
|
+
}
|
|
480
|
+
const nlIdx = remaining.indexOf('\n')
|
|
481
|
+
if (nlIdx === -1) {
|
|
482
|
+
// No newline — write as much as fits on current line, wrap if needed
|
|
483
|
+
while (remaining.length > 0) {
|
|
484
|
+
const segWidth = visibleWidth(remaining)
|
|
485
|
+
const avail = maxContentWidth - thinkCol
|
|
486
|
+
if (segWidth <= avail) {
|
|
487
|
+
// Fits entirely on current line
|
|
488
|
+
writeOut(remaining, out)
|
|
489
|
+
thinkCol += segWidth
|
|
490
|
+
remaining = ''
|
|
491
|
+
} else if (avail <= 0) {
|
|
492
|
+
// Current line is full, wrap to next
|
|
493
|
+
writeOut(`${RS}\n`, out)
|
|
494
|
+
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
495
|
+
thinkCol = 0
|
|
496
|
+
} else {
|
|
497
|
+
// Write first part that fits, then wrap
|
|
498
|
+
// Find character boundary that fits within avail
|
|
499
|
+
let cut = avail
|
|
500
|
+
while (cut > 0 && visibleWidth(remaining.slice(0, cut)) > avail) cut--
|
|
501
|
+
if (cut <= 0) cut = 1
|
|
502
|
+
writeOut(remaining.slice(0, cut), out)
|
|
503
|
+
writeOut(`${RS}\n`, out)
|
|
504
|
+
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
505
|
+
thinkCol = 0
|
|
506
|
+
remaining = remaining.slice(cut)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
// Has newline — process the segment up to newline
|
|
511
|
+
const segment = remaining.slice(0, nlIdx)
|
|
512
|
+
const segWidth = visibleWidth(segment)
|
|
469
513
|
const avail = maxContentWidth - thinkCol
|
|
470
514
|
if (segWidth <= avail) {
|
|
471
|
-
//
|
|
472
|
-
writeOut(
|
|
473
|
-
thinkCol += segWidth
|
|
474
|
-
remaining = ''
|
|
475
|
-
} else if (avail <= 0) {
|
|
476
|
-
// Current line is full, wrap to next
|
|
515
|
+
// Segment fits on current line
|
|
516
|
+
writeOut(segment, out)
|
|
477
517
|
writeOut(`${RS}\n`, out)
|
|
478
|
-
|
|
518
|
+
reasoningLineStart = true
|
|
479
519
|
thinkCol = 0
|
|
480
520
|
} else {
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
521
|
+
// Segment too long — write what fits, wrap, then rest
|
|
522
|
+
let rest = segment
|
|
523
|
+
// Write remainder of current line
|
|
524
|
+
if (avail > 0) {
|
|
525
|
+
let cut = avail
|
|
526
|
+
while (cut > 0 && visibleWidth(rest.slice(0, cut)) > avail) cut--
|
|
527
|
+
if (cut <= 0) cut = 1
|
|
528
|
+
writeOut(rest.slice(0, cut), out)
|
|
529
|
+
rest = rest.slice(cut)
|
|
530
|
+
}
|
|
487
531
|
writeOut(`${RS}\n`, out)
|
|
488
|
-
|
|
532
|
+
reasoningLineStart = true
|
|
489
533
|
thinkCol = 0
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
while (cut > 0 && visibleWidth(rest.slice(0, cut)) > avail) cut--
|
|
511
|
-
if (cut <= 0) cut = 1
|
|
512
|
-
writeOut(rest.slice(0, cut), out)
|
|
513
|
-
rest = rest.slice(cut)
|
|
514
|
-
}
|
|
515
|
-
writeOut(`${RS}\n`, out)
|
|
516
|
-
reasoningLineStart = true
|
|
517
|
-
thinkCol = 0
|
|
518
|
-
// Write rest of segment on continuation line(s)
|
|
519
|
-
if (rest.length > 0) {
|
|
520
|
-
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
521
|
-
reasoningLineStart = false
|
|
522
|
-
while (rest.length > 0) {
|
|
523
|
-
const rw = visibleWidth(rest)
|
|
524
|
-
if (rw <= maxContentWidth) {
|
|
525
|
-
writeOut(rest, out)
|
|
526
|
-
thinkCol = rw
|
|
527
|
-
rest = ''
|
|
528
|
-
} else {
|
|
529
|
-
let cut = maxContentWidth
|
|
530
|
-
while (cut > 0 && visibleWidth(rest.slice(0, cut)) > maxContentWidth)
|
|
531
|
-
cut--
|
|
532
|
-
if (cut <= 0) cut = 1
|
|
533
|
-
writeOut(rest.slice(0, cut), out)
|
|
534
|
-
writeOut(`${RS}\n`, out)
|
|
535
|
-
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
536
|
-
rest = rest.slice(cut)
|
|
534
|
+
// Write rest of segment on continuation line(s)
|
|
535
|
+
if (rest.length > 0) {
|
|
536
|
+
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
537
|
+
reasoningLineStart = false
|
|
538
|
+
while (rest.length > 0) {
|
|
539
|
+
const rw = visibleWidth(rest)
|
|
540
|
+
if (rw <= maxContentWidth) {
|
|
541
|
+
writeOut(rest, out)
|
|
542
|
+
thinkCol = rw
|
|
543
|
+
rest = ''
|
|
544
|
+
} else {
|
|
545
|
+
let cut = maxContentWidth
|
|
546
|
+
while (cut > 0 && visibleWidth(rest.slice(0, cut)) > maxContentWidth)
|
|
547
|
+
cut--
|
|
548
|
+
if (cut <= 0) cut = 1
|
|
549
|
+
writeOut(rest.slice(0, cut), out)
|
|
550
|
+
writeOut(`${RS}\n`, out)
|
|
551
|
+
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
552
|
+
rest = rest.slice(cut)
|
|
553
|
+
}
|
|
537
554
|
}
|
|
538
555
|
}
|
|
556
|
+
writeOut(`${RS}\n`, out)
|
|
557
|
+
reasoningLineStart = true
|
|
558
|
+
thinkCol = 0
|
|
539
559
|
}
|
|
540
|
-
|
|
541
|
-
reasoningLineStart = true
|
|
542
|
-
thinkCol = 0
|
|
560
|
+
remaining = remaining.slice(nlIdx + 1)
|
|
543
561
|
}
|
|
544
|
-
remaining = remaining.slice(nlIdx + 1)
|
|
545
562
|
}
|
|
546
563
|
}
|
|
547
564
|
}
|
|
548
565
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
566
|
+
if (chunk.type === 'text' && chunk.text) {
|
|
567
|
+
if (reasoningOutput) {
|
|
568
|
+
bus.emit(EventChannels.THINKING_END, {})
|
|
569
|
+
if (!out?.suppressToolOutput) {
|
|
570
|
+
writeOut(`${RS}\n`, out)
|
|
571
|
+
writeOut(thinkBottomBorder(), out)
|
|
572
|
+
}
|
|
573
|
+
reasoningOutput = false
|
|
574
|
+
reasoningLineStart = false
|
|
556
575
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
576
|
+
fullResponse += chunk.text
|
|
577
|
+
writeOut(chunk.text, out)
|
|
578
|
+
} else if (chunk.type === 'tool_use' && chunk.tool_call) {
|
|
579
|
+
toolCalls.push(chunk.tool_call)
|
|
580
|
+
} else if (chunk.type === 'complete') {
|
|
581
|
+
if (chunk.usage) {
|
|
582
|
+
this.turnInputTokens += chunk.usage.input_tokens
|
|
583
|
+
this.turnOutputTokens += chunk.usage.output_tokens
|
|
584
|
+
this.totalInputTokens += chunk.usage.input_tokens
|
|
585
|
+
this.totalOutputTokens += chunk.usage.output_tokens
|
|
586
|
+
}
|
|
587
|
+
if (chunk.finish_reason === 'stop' || chunk.finish_reason === 'end_turn') {
|
|
588
|
+
if (toolCalls.length === 0) {
|
|
589
|
+
const finalAssistantMsg: LLMMessage = {
|
|
590
|
+
role: 'assistant',
|
|
591
|
+
content: fullResponse || null,
|
|
592
|
+
reasoning_content: reasoningContent,
|
|
593
|
+
}
|
|
594
|
+
this.messages.push(finalAssistantMsg)
|
|
595
|
+
printTokenStats(
|
|
596
|
+
this.turnInputTokens,
|
|
597
|
+
this.turnOutputTokens,
|
|
598
|
+
this.totalInputTokens,
|
|
599
|
+
this.totalOutputTokens,
|
|
600
|
+
this.turnApiCalls,
|
|
601
|
+
this.totalApiCalls,
|
|
602
|
+
out,
|
|
603
|
+
)
|
|
604
|
+
writeOut('\n\n', out)
|
|
605
|
+
saveTokenUsage(
|
|
606
|
+
this.config.cwd,
|
|
607
|
+
this.turnInputTokens,
|
|
608
|
+
this.turnOutputTokens,
|
|
609
|
+
this.turnApiCalls,
|
|
610
|
+
)
|
|
611
|
+
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 })
|
|
612
|
+
this.save()
|
|
613
|
+
return
|
|
577
614
|
}
|
|
578
|
-
this.messages.push(finalAssistantMsg)
|
|
579
|
-
printTokenStats(
|
|
580
|
-
this.turnInputTokens,
|
|
581
|
-
this.turnOutputTokens,
|
|
582
|
-
this.totalInputTokens,
|
|
583
|
-
this.totalOutputTokens,
|
|
584
|
-
this.turnApiCalls,
|
|
585
|
-
this.totalApiCalls,
|
|
586
|
-
out,
|
|
587
|
-
)
|
|
588
|
-
writeOut('\n\n', out)
|
|
589
|
-
saveTokenUsage(
|
|
590
|
-
this.config.cwd,
|
|
591
|
-
this.turnInputTokens,
|
|
592
|
-
this.turnOutputTokens,
|
|
593
|
-
this.turnApiCalls,
|
|
594
|
-
)
|
|
595
|
-
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 })
|
|
596
|
-
this.save()
|
|
597
|
-
return
|
|
598
615
|
}
|
|
599
616
|
}
|
|
600
617
|
}
|
|
618
|
+
} catch (e) {
|
|
619
|
+
const errMsg = fmtErr(e)
|
|
620
|
+
const partialContent = fullResponse ? fullResponse.slice(0, 500) : '(empty)'
|
|
621
|
+
if (!out?.suppressToolOutput) {
|
|
622
|
+
writeOut(`\n${RE}Stream error:${RS} ${errMsg}`, out)
|
|
623
|
+
writeOut(`\n ${GY}┃${RS} Partial response: ${partialContent}\n`, out)
|
|
624
|
+
}
|
|
625
|
+
console.error('[session] Stream error:', errMsg, '| Partial response:', partialContent)
|
|
626
|
+
if (reasoningOutput) {
|
|
627
|
+
bus.emit(EventChannels.THINKING_END, {})
|
|
628
|
+
if (!out?.suppressToolOutput) {
|
|
629
|
+
writeOut(`${RS}\n`, out)
|
|
630
|
+
writeOut(thinkBottomBorder(), out)
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
bus.emit(EventChannels.LLM_STREAM_END, { iteration: iterations, toolCallCount: 0 })
|
|
634
|
+
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 })
|
|
635
|
+
|
|
636
|
+
// If there are pending toolCalls that weren't executed due to abort,
|
|
637
|
+
// add an assistant message so the model knows they weren't executed
|
|
638
|
+
if (toolCalls.length > 0) {
|
|
639
|
+
const interruptedMsg: LLMMessage = {
|
|
640
|
+
role: 'assistant',
|
|
641
|
+
content: null,
|
|
642
|
+
tool_calls: toolCalls,
|
|
643
|
+
reasoning_content: reasoningContent,
|
|
644
|
+
}
|
|
645
|
+
this.messages.push(interruptedMsg)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
saveTokenUsage(
|
|
649
|
+
this.config.cwd,
|
|
650
|
+
this.turnInputTokens,
|
|
651
|
+
this.turnOutputTokens,
|
|
652
|
+
this.turnApiCalls,
|
|
653
|
+
)
|
|
654
|
+
this.save()
|
|
655
|
+
return
|
|
601
656
|
}
|
|
602
657
|
|
|
603
658
|
bus.emit(EventChannels.LLM_STREAM_END, {
|
|
@@ -676,9 +731,20 @@ export class Session {
|
|
|
676
731
|
}
|
|
677
732
|
}
|
|
678
733
|
|
|
679
|
-
for (
|
|
734
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
735
|
+
const tc = toolCalls[i]
|
|
680
736
|
// Check if stop was requested after each tool call
|
|
681
737
|
if (this.isStopped()) {
|
|
738
|
+
// Add assistant message with remaining tool_calls so model knows they weren't executed
|
|
739
|
+
const remainingToolCalls = toolCalls.slice(i)
|
|
740
|
+
if (remainingToolCalls.length > 0) {
|
|
741
|
+
const interruptedMsg: LLMMessage = {
|
|
742
|
+
role: 'assistant',
|
|
743
|
+
content: null,
|
|
744
|
+
tool_calls: remainingToolCalls,
|
|
745
|
+
}
|
|
746
|
+
this.messages.push(interruptedMsg)
|
|
747
|
+
}
|
|
682
748
|
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length })
|
|
683
749
|
this.save()
|
|
684
750
|
return
|
|
@@ -295,8 +295,8 @@ describe('edit tool', () => {
|
|
|
295
295
|
expect(r.success).toBe(true)
|
|
296
296
|
// Should contain green ANSI code for added lines
|
|
297
297
|
expect(r.output).toContain('\x1b[38;2;0;200;100m') // green
|
|
298
|
-
expect(r.output).toContain('
|
|
299
|
-
expect(r.output).toContain('
|
|
298
|
+
expect(r.output).toContain('hello')
|
|
299
|
+
expect(r.output).toContain('world')
|
|
300
300
|
// Should NOT contain red ANSI code (no removed lines)
|
|
301
301
|
expect(r.output).not.toContain('\x1b[38;2;255;80;80m')
|
|
302
302
|
expect(r.output).toContain('Created')
|
|
@@ -315,10 +315,10 @@ describe('edit tool', () => {
|
|
|
315
315
|
expect(r.success).toBe(true)
|
|
316
316
|
// Should contain red ANSI code for removed content
|
|
317
317
|
expect(r.output).toContain('\x1b[38;2;255;80;80m')
|
|
318
|
-
expect(r.output).toContain('
|
|
318
|
+
expect(r.output).toContain('line two')
|
|
319
319
|
// Should contain green ANSI code for added content
|
|
320
320
|
expect(r.output).toContain('\x1b[38;2;0;200;100m')
|
|
321
|
-
expect(r.output).toContain('
|
|
321
|
+
expect(r.output).toContain('line TWO')
|
|
322
322
|
expect(r.output).toContain('Edited')
|
|
323
323
|
})
|
|
324
324
|
|
|
@@ -333,12 +333,12 @@ describe('edit tool', () => {
|
|
|
333
333
|
],
|
|
334
334
|
})
|
|
335
335
|
expect(r.success).toBe(true)
|
|
336
|
-
// Removed lines
|
|
337
|
-
expect(r.output).toContain('
|
|
338
|
-
expect(r.output).toContain('
|
|
339
|
-
// Added lines
|
|
340
|
-
expect(r.output).toContain('
|
|
341
|
-
expect(r.output).toContain('
|
|
336
|
+
// Removed lines (red)
|
|
337
|
+
expect(r.output).toContain('\x1b[38;2;255;80;80mline one')
|
|
338
|
+
expect(r.output).toContain('\x1b[38;2;255;80;80mline TWO')
|
|
339
|
+
// Added lines (green)
|
|
340
|
+
expect(r.output).toContain('\x1b[38;2;0;200;100mline 1')
|
|
341
|
+
expect(r.output).toContain('\x1b[38;2;0;200;100mline 2')
|
|
342
342
|
})
|
|
343
343
|
|
|
344
344
|
it('handles CRLF in old_string (Windows compatibility)', async () => {
|
|
@@ -357,8 +357,8 @@ describe('edit tool', () => {
|
|
|
357
357
|
const content = fs.readFileSync(path.join(tmpDir, 'crlf-test.txt'), 'utf8')
|
|
358
358
|
expect(content).toContain('B')
|
|
359
359
|
// Should show the normalized diff (with \\n, not \\r\\n)
|
|
360
|
-
expect(r.output).toContain('
|
|
361
|
-
expect(r.output).toContain('
|
|
360
|
+
expect(r.output).toContain('\x1b[38;2;255;80;80mb')
|
|
361
|
+
expect(r.output).toContain('\x1b[38;2;0;200;100mB')
|
|
362
362
|
})
|
|
363
363
|
|
|
364
364
|
it('does not show empty red line when old_string is empty', async () => {
|
|
@@ -366,8 +366,8 @@ describe('edit tool', () => {
|
|
|
366
366
|
edits: [{ file_path: 'empty-old.txt', old_string: '', new_string: 'only added' }],
|
|
367
367
|
})
|
|
368
368
|
expect(r.success).toBe(true)
|
|
369
|
-
// Should have green
|
|
370
|
-
expect(r.output).toContain('
|
|
369
|
+
// Should have green colored line
|
|
370
|
+
expect(r.output).toContain('\x1b[38;2;0;200;100monly added')
|
|
371
371
|
// Count red-start markers — should be 0 (no removed content)
|
|
372
372
|
const redCount = (r.output.match(/\x1b\[38;2;255;80;80m/g) || []).length
|
|
373
373
|
expect(redCount).toBe(0)
|