lonny-agent 0.2.3 → 0.2.6
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/README.md +326 -326
- package/dist/agent/index.js +1 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/llm.d.ts +4 -0
- package/dist/agent/llm.d.ts.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 +22 -13
- 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.d.ts.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 +63 -3
- package/dist/agent/providers/openai.js.map +1 -1
- package/dist/agent/session.d.ts +8 -2
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +250 -153
- package/dist/agent/session.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +7 -1
- package/dist/config/index.js.map +1 -1
- package/dist/tools/__tests__/bash.test.js +1 -1
- package/dist/tools/__tests__/bash.test.js.map +1 -1
- package/dist/tools/__tests__/edit.test.js +875 -371
- package/dist/tools/__tests__/edit.test.js.map +1 -1
- package/dist/tools/__tests__/grep.test.js +32 -0
- package/dist/tools/__tests__/grep.test.js.map +1 -1
- package/dist/tools/__tests__/sed.test.d.ts +2 -0
- package/dist/tools/__tests__/sed.test.d.ts.map +1 -0
- package/dist/tools/__tests__/sed.test.js +228 -0
- package/dist/tools/__tests__/sed.test.js.map +1 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +83 -10
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +58 -0
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +366 -149
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +54 -30
- package/dist/tools/grep.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 +0 -6
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +2 -33
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/sed.d.ts +4 -0
- package/dist/tools/sed.d.ts.map +1 -0
- package/dist/tools/sed.js +121 -0
- package/dist/tools/sed.js.map +1 -0
- 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.d.ts.map +1 -1
- package/dist/web/index.js +30 -7
- package/dist/web/index.js.map +1 -1
- package/dist/web/public/app.js +5 -855
- package/dist/web/public/confirm.js +70 -0
- package/dist/web/public/index.html +121 -120
- package/dist/web/public/input.js +107 -0
- package/dist/web/public/messages.js +395 -0
- package/dist/web/public/sidebar.js +82 -0
- package/dist/web/public/state.js +72 -0
- package/dist/web/public/style.css +996 -949
- package/dist/web/public/utils.js +37 -0
- package/dist/web/public/websocket.js +267 -0
- package/dist/web/public/ws.js +19 -0
- package/dist/web/session-bridge.d.ts.map +1 -1
- package/dist/web/session-bridge.js +3 -7
- package/dist/web/session-bridge.js.map +1 -1
- package/package.json +55 -54
- package/scripts/copy-native.mjs +24 -0
- package/scripts/copy-web.mjs +15 -0
- package/src/agent/index.ts +25 -25
- package/src/agent/llm.ts +4 -0
- package/src/agent/project.ts +366 -0
- package/src/agent/prompt-builder.ts +23 -13
- package/src/agent/providers/anthropic.ts +179 -172
- package/src/agent/providers/google.ts +210 -211
- package/src/agent/providers/ollama.ts +186 -182
- package/src/agent/providers/openai.ts +73 -5
- package/src/agent/session.ts +275 -153
- package/src/config/index.ts +342 -335
- package/src/tools/__tests__/bash.test.ts +1 -1
- package/src/tools/__tests__/edit.test.ts +981 -385
- package/src/tools/__tests__/grep.test.ts +35 -0
- package/src/tools/bash.ts +97 -11
- package/src/tools/edit.ts +414 -154
- package/src/tools/git.ts +76 -76
- package/src/tools/grep.ts +57 -30
- package/src/tools/read.ts +61 -25
- package/src/tools/registry.ts +231 -265
- package/src/tools/write_plan.ts +21 -5
- package/src/tui/index.ts +786 -785
- package/src/web/index.ts +29 -7
- package/src/web/public/app.js +5 -855
- package/src/web/public/confirm.js +70 -0
- package/src/web/public/index.html +121 -120
- package/src/web/public/input.js +107 -0
- package/src/web/public/messages.js +395 -0
- package/src/web/public/sidebar.js +82 -0
- package/src/web/public/state.js +72 -0
- package/src/web/public/style.css +996 -949
- package/src/web/public/utils.js +37 -0
- package/src/web/public/websocket.js +267 -0
- package/src/web/public/ws.js +19 -0
- package/src/web/session-bridge.ts +193 -194
- package/.kilo/plans/1780064105789-mighty-pixel.md +0 -171
- package/.kilo/plans/1780293725888-quick-wizard.md +0 -62
- package/.lonny/plan-web-cwd-status.md +0 -38
- package/src/tools/exec.ts +0 -348
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'
|
|
@@ -24,6 +25,8 @@ interface SessionData {
|
|
|
24
25
|
totalInputTokens: number
|
|
25
26
|
totalOutputTokens: number
|
|
26
27
|
totalApiCalls: number
|
|
28
|
+
totalCacheHitTokens?: number
|
|
29
|
+
totalCacheMissTokens?: number
|
|
27
30
|
mode: 'code' | 'plan' | 'ask'
|
|
28
31
|
model: string
|
|
29
32
|
provider: string
|
|
@@ -221,13 +224,36 @@ function printTokenStats(
|
|
|
221
224
|
turnApi: number,
|
|
222
225
|
totalApi: number,
|
|
223
226
|
output?: SessionOutput,
|
|
227
|
+
turnCacheHit?: number,
|
|
228
|
+
turnCacheMiss?: number,
|
|
229
|
+
totalCacheHit?: number,
|
|
230
|
+
totalCacheMiss?: number,
|
|
224
231
|
): void {
|
|
225
232
|
const bus = getGlobalEventBus()
|
|
226
|
-
bus.emit(EventChannels.TOKEN_STATS, {
|
|
233
|
+
bus.emit(EventChannels.TOKEN_STATS, {
|
|
234
|
+
turnIn,
|
|
235
|
+
turnOut,
|
|
236
|
+
totalIn,
|
|
237
|
+
totalOut,
|
|
238
|
+
turnApi,
|
|
239
|
+
totalApi,
|
|
240
|
+
turnCacheHit,
|
|
241
|
+
turnCacheMiss,
|
|
242
|
+
totalCacheHit,
|
|
243
|
+
totalCacheMiss,
|
|
244
|
+
})
|
|
227
245
|
// Skip terminal output in Web UI mode
|
|
228
246
|
if (output?.suppressToolOutput) return
|
|
229
247
|
const total = totalIn + totalOut
|
|
230
|
-
|
|
248
|
+
let msg = ` ${GY}┃${RS} ${GY}${BLD}▴${RS}${GY}${turnIn}${RS} ${GY}${BLD}▾${RS}${GY}${turnOut}${RS} ${GY}total${RS} ${total} ${GY}calls${RS} ${turnApi}(${totalApi})`
|
|
249
|
+
// Show cache hit rate if available
|
|
250
|
+
const cacheHit = totalCacheHit ?? 0
|
|
251
|
+
const cacheMiss = totalCacheMiss ?? 0
|
|
252
|
+
const cacheTotal = cacheHit + cacheMiss
|
|
253
|
+
if (cacheTotal > 0) {
|
|
254
|
+
const pct = Math.round((cacheHit / cacheTotal) * 100)
|
|
255
|
+
msg += ` ${GY}cached${RS} ${pct}%`
|
|
256
|
+
}
|
|
231
257
|
writeOut(`\n${msg}\n`, output)
|
|
232
258
|
}
|
|
233
259
|
|
|
@@ -258,6 +284,10 @@ export class Session {
|
|
|
258
284
|
turnOutputTokens: number = 0
|
|
259
285
|
turnApiCalls: number = 0
|
|
260
286
|
totalApiCalls: number = 0
|
|
287
|
+
turnCacheHitTokens: number = 0
|
|
288
|
+
turnCacheMissTokens: number = 0
|
|
289
|
+
totalCacheHitTokens: number = 0
|
|
290
|
+
totalCacheMissTokens: number = 0
|
|
261
291
|
private stopped: boolean = false
|
|
262
292
|
private abortController: AbortController | null = null
|
|
263
293
|
|
|
@@ -291,7 +321,17 @@ export class Session {
|
|
|
291
321
|
this.provider = new AnthropicProvider(config.apiKey, config.baseUrl, config.model)
|
|
292
322
|
}
|
|
293
323
|
|
|
294
|
-
|
|
324
|
+
// Placeholder until initSystemPrompt() is called
|
|
325
|
+
this.messages = [{ role: 'system', content: '' }]
|
|
326
|
+
// Initialize system prompt asynchronously
|
|
327
|
+
this.initSystemPrompt(config)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Initialize the system prompt asynchronously */
|
|
331
|
+
private initSystemPrompt(config: Config): void {
|
|
332
|
+
buildSystemPrompt(config).then(prompt => {
|
|
333
|
+
this.messages = [{ role: 'system', content: prompt }]
|
|
334
|
+
})
|
|
295
335
|
}
|
|
296
336
|
|
|
297
337
|
/** Persist the current session to ~/.lonny/sessions/ */
|
|
@@ -305,6 +345,8 @@ export class Session {
|
|
|
305
345
|
totalInputTokens: this.totalInputTokens,
|
|
306
346
|
totalOutputTokens: this.totalOutputTokens,
|
|
307
347
|
totalApiCalls: this.totalApiCalls,
|
|
348
|
+
totalCacheHitTokens: this.totalCacheHitTokens || undefined,
|
|
349
|
+
totalCacheMissTokens: this.totalCacheMissTokens || undefined,
|
|
308
350
|
mode: this.config.mode,
|
|
309
351
|
model: this.config.model,
|
|
310
352
|
provider: this.config.provider,
|
|
@@ -314,7 +356,7 @@ export class Session {
|
|
|
314
356
|
}
|
|
315
357
|
|
|
316
358
|
/** Try to load a saved session for the given cwd. Returns null if none exists. */
|
|
317
|
-
static load(config: Config, output?: SessionOutput): Session | null {
|
|
359
|
+
static async load(config: Config, output?: SessionOutput): Promise<Session | null> {
|
|
318
360
|
const filePath = getSessionFilePath(config.cwd)
|
|
319
361
|
let data: SessionData
|
|
320
362
|
try {
|
|
@@ -340,12 +382,15 @@ export class Session {
|
|
|
340
382
|
data.provider !== config.provider ||
|
|
341
383
|
data.mode !== config.mode
|
|
342
384
|
) {
|
|
343
|
-
|
|
385
|
+
const prompt = await buildSystemPrompt(config)
|
|
386
|
+
session.messages[0] = { role: 'system', content: prompt }
|
|
344
387
|
}
|
|
345
388
|
// Restore token stats
|
|
346
389
|
session.totalInputTokens = data.totalInputTokens
|
|
347
390
|
session.totalOutputTokens = data.totalOutputTokens
|
|
348
391
|
session.totalApiCalls = data.totalApiCalls
|
|
392
|
+
session.totalCacheHitTokens = data.totalCacheHitTokens ?? 0
|
|
393
|
+
session.totalCacheMissTokens = data.totalCacheMissTokens ?? 0
|
|
349
394
|
return session
|
|
350
395
|
}
|
|
351
396
|
|
|
@@ -359,9 +404,10 @@ export class Session {
|
|
|
359
404
|
}
|
|
360
405
|
}
|
|
361
406
|
|
|
362
|
-
setMode(mode: 'code' | 'plan' | 'ask'): void {
|
|
407
|
+
async setMode(mode: 'code' | 'plan' | 'ask'): Promise<void> {
|
|
363
408
|
this.config.mode = mode
|
|
364
|
-
|
|
409
|
+
const prompt = await buildSystemPrompt(this.config)
|
|
410
|
+
this.messages[0] = { role: 'system', content: prompt }
|
|
365
411
|
this.registry.setMode(mode)
|
|
366
412
|
this.save()
|
|
367
413
|
}
|
|
@@ -381,6 +427,8 @@ export class Session {
|
|
|
381
427
|
/** Reset the stopped flag for a new conversation */
|
|
382
428
|
resetStopped(): void {
|
|
383
429
|
this.stopped = false
|
|
430
|
+
// Create new AbortController for next conversation
|
|
431
|
+
this.abortController = new AbortController()
|
|
384
432
|
}
|
|
385
433
|
|
|
386
434
|
async chat(userPrompt: string): Promise<void> {
|
|
@@ -395,6 +443,8 @@ export class Session {
|
|
|
395
443
|
this.turnInputTokens = 0
|
|
396
444
|
this.turnOutputTokens = 0
|
|
397
445
|
this.turnApiCalls = 0
|
|
446
|
+
this.turnCacheHitTokens = 0
|
|
447
|
+
this.turnCacheMissTokens = 0
|
|
398
448
|
|
|
399
449
|
let iterations = 0
|
|
400
450
|
const maxIterations = 30
|
|
@@ -433,171 +483,224 @@ export class Session {
|
|
|
433
483
|
this.abortController.signal,
|
|
434
484
|
)
|
|
435
485
|
|
|
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
|
|
486
|
+
try {
|
|
487
|
+
for await (const chunk of stream) {
|
|
488
|
+
if (chunk.reasoning_content) {
|
|
489
|
+
reasoningContent = chunk.reasoning_content
|
|
490
|
+
// Stream reasoning content in real-time (only when no text in same chunk)
|
|
491
|
+
if (!chunk.text) {
|
|
492
|
+
// Emit thinking via EventBus for Web UI
|
|
493
|
+
bus.emit(EventChannels.THINKING, { text: chunk.reasoning_content })
|
|
494
|
+
if (!reasoningOutput) {
|
|
495
|
+
reasoningOutput = true
|
|
496
|
+
reasoningLineStart = true
|
|
497
|
+
if (!out?.suppressToolOutput) {
|
|
498
|
+
writeOut(thinkTopBorder(), out)
|
|
463
499
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
500
|
+
}
|
|
501
|
+
// Terminal display with box drawing (skip in Web UI mode, handled by EventBus)
|
|
502
|
+
if (!out?.suppressToolOutput) {
|
|
503
|
+
// Track column position on current line for wrapping
|
|
504
|
+
let thinkCol = 0
|
|
505
|
+
// Handle newlines in streamed content - add left border on each new line
|
|
506
|
+
// Also manually wrap long lines so wrapped lines keep the │ prefix.
|
|
507
|
+
let remaining = chunk.reasoning_content
|
|
508
|
+
const maxContentWidth = termWidth() - THINK_PREFIX_WIDTH
|
|
509
|
+
while (remaining.length > 0) {
|
|
510
|
+
if (reasoningLineStart) {
|
|
511
|
+
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
512
|
+
reasoningLineStart = false
|
|
513
|
+
thinkCol = 0
|
|
514
|
+
}
|
|
515
|
+
const nlIdx = remaining.indexOf('\n')
|
|
516
|
+
if (nlIdx === -1) {
|
|
517
|
+
// No newline — write as much as fits on current line, wrap if needed
|
|
518
|
+
while (remaining.length > 0) {
|
|
519
|
+
const segWidth = visibleWidth(remaining)
|
|
520
|
+
const avail = maxContentWidth - thinkCol
|
|
521
|
+
if (segWidth <= avail) {
|
|
522
|
+
// Fits entirely on current line
|
|
523
|
+
writeOut(remaining, out)
|
|
524
|
+
thinkCol += segWidth
|
|
525
|
+
remaining = ''
|
|
526
|
+
} else if (avail <= 0) {
|
|
527
|
+
// Current line is full, wrap to next
|
|
528
|
+
writeOut(`${RS}\n`, out)
|
|
529
|
+
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
530
|
+
thinkCol = 0
|
|
531
|
+
} else {
|
|
532
|
+
// Write first part that fits, then wrap
|
|
533
|
+
// Find character boundary that fits within avail
|
|
534
|
+
let cut = avail
|
|
535
|
+
while (cut > 0 && visibleWidth(remaining.slice(0, cut)) > avail) cut--
|
|
536
|
+
if (cut <= 0) cut = 1
|
|
537
|
+
writeOut(remaining.slice(0, cut), out)
|
|
538
|
+
writeOut(`${RS}\n`, out)
|
|
539
|
+
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
540
|
+
thinkCol = 0
|
|
541
|
+
remaining = remaining.slice(cut)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
// Has newline — process the segment up to newline
|
|
546
|
+
const segment = remaining.slice(0, nlIdx)
|
|
547
|
+
const segWidth = visibleWidth(segment)
|
|
469
548
|
const avail = maxContentWidth - thinkCol
|
|
470
549
|
if (segWidth <= avail) {
|
|
471
|
-
//
|
|
472
|
-
writeOut(
|
|
473
|
-
thinkCol += segWidth
|
|
474
|
-
remaining = ''
|
|
475
|
-
} else if (avail <= 0) {
|
|
476
|
-
// Current line is full, wrap to next
|
|
550
|
+
// Segment fits on current line
|
|
551
|
+
writeOut(segment, out)
|
|
477
552
|
writeOut(`${RS}\n`, out)
|
|
478
|
-
|
|
553
|
+
reasoningLineStart = true
|
|
479
554
|
thinkCol = 0
|
|
480
555
|
} else {
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
556
|
+
// Segment too long — write what fits, wrap, then rest
|
|
557
|
+
let rest = segment
|
|
558
|
+
// Write remainder of current line
|
|
559
|
+
if (avail > 0) {
|
|
560
|
+
let cut = avail
|
|
561
|
+
while (cut > 0 && visibleWidth(rest.slice(0, cut)) > avail) cut--
|
|
562
|
+
if (cut <= 0) cut = 1
|
|
563
|
+
writeOut(rest.slice(0, cut), out)
|
|
564
|
+
rest = rest.slice(cut)
|
|
565
|
+
}
|
|
487
566
|
writeOut(`${RS}\n`, out)
|
|
488
|
-
|
|
567
|
+
reasoningLineStart = true
|
|
489
568
|
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)
|
|
569
|
+
// Write rest of segment on continuation line(s)
|
|
570
|
+
if (rest.length > 0) {
|
|
571
|
+
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
572
|
+
reasoningLineStart = false
|
|
573
|
+
while (rest.length > 0) {
|
|
574
|
+
const rw = visibleWidth(rest)
|
|
575
|
+
if (rw <= maxContentWidth) {
|
|
576
|
+
writeOut(rest, out)
|
|
577
|
+
thinkCol = rw
|
|
578
|
+
rest = ''
|
|
579
|
+
} else {
|
|
580
|
+
let cut = maxContentWidth
|
|
581
|
+
while (cut > 0 && visibleWidth(rest.slice(0, cut)) > maxContentWidth)
|
|
582
|
+
cut--
|
|
583
|
+
if (cut <= 0) cut = 1
|
|
584
|
+
writeOut(rest.slice(0, cut), out)
|
|
585
|
+
writeOut(`${RS}\n`, out)
|
|
586
|
+
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
587
|
+
rest = rest.slice(cut)
|
|
588
|
+
}
|
|
537
589
|
}
|
|
538
590
|
}
|
|
591
|
+
writeOut(`${RS}\n`, out)
|
|
592
|
+
reasoningLineStart = true
|
|
593
|
+
thinkCol = 0
|
|
539
594
|
}
|
|
540
|
-
|
|
541
|
-
reasoningLineStart = true
|
|
542
|
-
thinkCol = 0
|
|
595
|
+
remaining = remaining.slice(nlIdx + 1)
|
|
543
596
|
}
|
|
544
|
-
remaining = remaining.slice(nlIdx + 1)
|
|
545
597
|
}
|
|
546
598
|
}
|
|
547
599
|
}
|
|
548
600
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
601
|
+
if (chunk.type === 'text' && chunk.text) {
|
|
602
|
+
if (reasoningOutput) {
|
|
603
|
+
bus.emit(EventChannels.THINKING_END, {})
|
|
604
|
+
if (!out?.suppressToolOutput) {
|
|
605
|
+
writeOut(`${RS}\n`, out)
|
|
606
|
+
writeOut(thinkBottomBorder(), out)
|
|
607
|
+
}
|
|
608
|
+
reasoningOutput = false
|
|
609
|
+
reasoningLineStart = false
|
|
556
610
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
content: fullResponse || null,
|
|
576
|
-
reasoning_content: reasoningContent,
|
|
611
|
+
fullResponse += chunk.text
|
|
612
|
+
writeOut(chunk.text, out)
|
|
613
|
+
} else if (chunk.type === 'tool_use' && chunk.tool_call) {
|
|
614
|
+
toolCalls.push(chunk.tool_call)
|
|
615
|
+
} else if (chunk.type === 'complete') {
|
|
616
|
+
if (chunk.usage) {
|
|
617
|
+
this.turnInputTokens += chunk.usage.input_tokens
|
|
618
|
+
this.turnOutputTokens += chunk.usage.output_tokens
|
|
619
|
+
this.totalInputTokens += chunk.usage.input_tokens
|
|
620
|
+
this.totalOutputTokens += chunk.usage.output_tokens
|
|
621
|
+
// Accumulate DeepSeek cache hit/miss tokens
|
|
622
|
+
if (chunk.usage.prompt_cache_hit_tokens != null) {
|
|
623
|
+
this.turnCacheHitTokens += chunk.usage.prompt_cache_hit_tokens
|
|
624
|
+
this.totalCacheHitTokens += chunk.usage.prompt_cache_hit_tokens
|
|
625
|
+
}
|
|
626
|
+
if (chunk.usage.prompt_cache_miss_tokens != null) {
|
|
627
|
+
this.turnCacheMissTokens += chunk.usage.prompt_cache_miss_tokens
|
|
628
|
+
this.totalCacheMissTokens += chunk.usage.prompt_cache_miss_tokens
|
|
577
629
|
}
|
|
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
630
|
}
|
|
631
|
+
if (chunk.finish_reason === 'stop' || chunk.finish_reason === 'end_turn') {
|
|
632
|
+
if (toolCalls.length === 0) {
|
|
633
|
+
const finalAssistantMsg: LLMMessage = {
|
|
634
|
+
role: 'assistant',
|
|
635
|
+
content: fullResponse || null,
|
|
636
|
+
reasoning_content: reasoningContent,
|
|
637
|
+
}
|
|
638
|
+
this.messages.push(finalAssistantMsg)
|
|
639
|
+
printTokenStats(
|
|
640
|
+
this.turnInputTokens,
|
|
641
|
+
this.turnOutputTokens,
|
|
642
|
+
this.totalInputTokens,
|
|
643
|
+
this.totalOutputTokens,
|
|
644
|
+
this.turnApiCalls,
|
|
645
|
+
this.totalApiCalls,
|
|
646
|
+
out,
|
|
647
|
+
this.turnCacheHitTokens,
|
|
648
|
+
this.turnCacheMissTokens,
|
|
649
|
+
this.totalCacheHitTokens,
|
|
650
|
+
this.totalCacheMissTokens,
|
|
651
|
+
)
|
|
652
|
+
writeOut('\n\n', out)
|
|
653
|
+
saveTokenUsage(
|
|
654
|
+
this.config.cwd,
|
|
655
|
+
this.turnInputTokens,
|
|
656
|
+
this.turnOutputTokens,
|
|
657
|
+
this.turnApiCalls,
|
|
658
|
+
)
|
|
659
|
+
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 })
|
|
660
|
+
this.save()
|
|
661
|
+
return
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
} catch (e) {
|
|
667
|
+
const errMsg = fmtErr(e)
|
|
668
|
+
const partialContent = fullResponse ? fullResponse.slice(0, 500) : '(empty)'
|
|
669
|
+
if (!out?.suppressToolOutput) {
|
|
670
|
+
writeOut(`\n${RE}Stream error:${RS} ${errMsg}`, out)
|
|
671
|
+
writeOut(`\n ${GY}┃${RS} Partial response: ${partialContent}\n`, out)
|
|
672
|
+
}
|
|
673
|
+
console.error('[session] Stream error:', errMsg, '| Partial response:', partialContent)
|
|
674
|
+
if (reasoningOutput) {
|
|
675
|
+
bus.emit(EventChannels.THINKING_END, {})
|
|
676
|
+
if (!out?.suppressToolOutput) {
|
|
677
|
+
writeOut(`${RS}\n`, out)
|
|
678
|
+
writeOut(thinkBottomBorder(), out)
|
|
599
679
|
}
|
|
600
680
|
}
|
|
681
|
+
bus.emit(EventChannels.LLM_STREAM_END, { iteration: iterations, toolCallCount: 0 })
|
|
682
|
+
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 })
|
|
683
|
+
|
|
684
|
+
// If there are pending toolCalls that weren't executed due to abort,
|
|
685
|
+
// add an assistant message so the model knows they weren't executed
|
|
686
|
+
if (toolCalls.length > 0) {
|
|
687
|
+
const interruptedMsg: LLMMessage = {
|
|
688
|
+
role: 'assistant',
|
|
689
|
+
content: null,
|
|
690
|
+
tool_calls: toolCalls,
|
|
691
|
+
reasoning_content: reasoningContent,
|
|
692
|
+
}
|
|
693
|
+
this.messages.push(interruptedMsg)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
saveTokenUsage(
|
|
697
|
+
this.config.cwd,
|
|
698
|
+
this.turnInputTokens,
|
|
699
|
+
this.turnOutputTokens,
|
|
700
|
+
this.turnApiCalls,
|
|
701
|
+
)
|
|
702
|
+
this.save()
|
|
703
|
+
return
|
|
601
704
|
}
|
|
602
705
|
|
|
603
706
|
bus.emit(EventChannels.LLM_STREAM_END, {
|
|
@@ -632,6 +735,10 @@ export class Session {
|
|
|
632
735
|
this.turnApiCalls,
|
|
633
736
|
this.totalApiCalls,
|
|
634
737
|
out,
|
|
738
|
+
this.turnCacheHitTokens,
|
|
739
|
+
this.turnCacheMissTokens,
|
|
740
|
+
this.totalCacheHitTokens,
|
|
741
|
+
this.totalCacheMissTokens,
|
|
635
742
|
)
|
|
636
743
|
writeOut('\n\n', out)
|
|
637
744
|
}
|
|
@@ -657,7 +764,7 @@ export class Session {
|
|
|
657
764
|
|
|
658
765
|
// ── User confirmation for write-type tool calls ──
|
|
659
766
|
if (!this.config.autoApprove && this.output?.confirmTool && toolCalls.length > 0) {
|
|
660
|
-
const writeTools = ['edit', 'bash', 'write_plan', '
|
|
767
|
+
const writeTools = ['edit', 'bash', 'write_plan', 'install_skill']
|
|
661
768
|
const needsConfirm = toolCalls.filter(tc => writeTools.includes(tc.name))
|
|
662
769
|
if (needsConfirm.length > 0) {
|
|
663
770
|
const approved = await this.output.confirmTool(needsConfirm)
|
|
@@ -676,9 +783,20 @@ export class Session {
|
|
|
676
783
|
}
|
|
677
784
|
}
|
|
678
785
|
|
|
679
|
-
for (
|
|
786
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
787
|
+
const tc = toolCalls[i]
|
|
680
788
|
// Check if stop was requested after each tool call
|
|
681
789
|
if (this.isStopped()) {
|
|
790
|
+
// Add assistant message with remaining tool_calls so model knows they weren't executed
|
|
791
|
+
const remainingToolCalls = toolCalls.slice(i)
|
|
792
|
+
if (remainingToolCalls.length > 0) {
|
|
793
|
+
const interruptedMsg: LLMMessage = {
|
|
794
|
+
role: 'assistant',
|
|
795
|
+
content: null,
|
|
796
|
+
tool_calls: remainingToolCalls,
|
|
797
|
+
}
|
|
798
|
+
this.messages.push(interruptedMsg)
|
|
799
|
+
}
|
|
682
800
|
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length })
|
|
683
801
|
this.save()
|
|
684
802
|
return
|
|
@@ -736,6 +854,10 @@ export class Session {
|
|
|
736
854
|
this.turnApiCalls,
|
|
737
855
|
this.totalApiCalls,
|
|
738
856
|
out,
|
|
857
|
+
this.turnCacheHitTokens,
|
|
858
|
+
this.turnCacheMissTokens,
|
|
859
|
+
this.totalCacheHitTokens,
|
|
860
|
+
this.totalCacheMissTokens,
|
|
739
861
|
)
|
|
740
862
|
writeOut('\nAgent reached maximum iterations. Stopping.\n', out)
|
|
741
863
|
}
|