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.
Files changed (129) hide show
  1. package/README.md +326 -326
  2. package/dist/agent/index.js +1 -1
  3. package/dist/agent/index.js.map +1 -1
  4. package/dist/agent/llm.d.ts +4 -0
  5. package/dist/agent/llm.d.ts.map +1 -1
  6. package/dist/agent/project.d.ts +26 -0
  7. package/dist/agent/project.d.ts.map +1 -0
  8. package/dist/agent/project.js +303 -0
  9. package/dist/agent/project.js.map +1 -0
  10. package/dist/agent/prompt-builder.d.ts +1 -1
  11. package/dist/agent/prompt-builder.d.ts.map +1 -1
  12. package/dist/agent/prompt-builder.js +22 -13
  13. package/dist/agent/prompt-builder.js.map +1 -1
  14. package/dist/agent/providers/anthropic.d.ts.map +1 -1
  15. package/dist/agent/providers/anthropic.js +20 -2
  16. package/dist/agent/providers/anthropic.js.map +1 -1
  17. package/dist/agent/providers/google.d.ts.map +1 -1
  18. package/dist/agent/providers/google.js +1 -1
  19. package/dist/agent/providers/google.js.map +1 -1
  20. package/dist/agent/providers/ollama.d.ts.map +1 -1
  21. package/dist/agent/providers/ollama.js +2 -1
  22. package/dist/agent/providers/ollama.js.map +1 -1
  23. package/dist/agent/providers/openai.d.ts.map +1 -1
  24. package/dist/agent/providers/openai.js +63 -3
  25. package/dist/agent/providers/openai.js.map +1 -1
  26. package/dist/agent/session.d.ts +8 -2
  27. package/dist/agent/session.d.ts.map +1 -1
  28. package/dist/agent/session.js +250 -153
  29. package/dist/agent/session.js.map +1 -1
  30. package/dist/config/index.d.ts.map +1 -1
  31. package/dist/config/index.js +7 -1
  32. package/dist/config/index.js.map +1 -1
  33. package/dist/tools/__tests__/bash.test.js +1 -1
  34. package/dist/tools/__tests__/bash.test.js.map +1 -1
  35. package/dist/tools/__tests__/edit.test.js +875 -371
  36. package/dist/tools/__tests__/edit.test.js.map +1 -1
  37. package/dist/tools/__tests__/grep.test.js +32 -0
  38. package/dist/tools/__tests__/grep.test.js.map +1 -1
  39. package/dist/tools/__tests__/sed.test.d.ts +2 -0
  40. package/dist/tools/__tests__/sed.test.d.ts.map +1 -0
  41. package/dist/tools/__tests__/sed.test.js +228 -0
  42. package/dist/tools/__tests__/sed.test.js.map +1 -0
  43. package/dist/tools/bash.d.ts.map +1 -1
  44. package/dist/tools/bash.js +83 -10
  45. package/dist/tools/bash.js.map +1 -1
  46. package/dist/tools/edit.d.ts +58 -0
  47. package/dist/tools/edit.d.ts.map +1 -1
  48. package/dist/tools/edit.js +366 -149
  49. package/dist/tools/edit.js.map +1 -1
  50. package/dist/tools/grep.d.ts.map +1 -1
  51. package/dist/tools/grep.js +54 -30
  52. package/dist/tools/grep.js.map +1 -1
  53. package/dist/tools/read.d.ts +6 -0
  54. package/dist/tools/read.d.ts.map +1 -1
  55. package/dist/tools/read.js +42 -15
  56. package/dist/tools/read.js.map +1 -1
  57. package/dist/tools/registry.d.ts +0 -6
  58. package/dist/tools/registry.d.ts.map +1 -1
  59. package/dist/tools/registry.js +2 -33
  60. package/dist/tools/registry.js.map +1 -1
  61. package/dist/tools/sed.d.ts +4 -0
  62. package/dist/tools/sed.d.ts.map +1 -0
  63. package/dist/tools/sed.js +121 -0
  64. package/dist/tools/sed.js.map +1 -0
  65. package/dist/tools/write_plan.d.ts +1 -1
  66. package/dist/tools/write_plan.d.ts.map +1 -1
  67. package/dist/tools/write_plan.js +14 -4
  68. package/dist/tools/write_plan.js.map +1 -1
  69. package/dist/tui/index.d.ts.map +1 -1
  70. package/dist/tui/index.js +5 -4
  71. package/dist/tui/index.js.map +1 -1
  72. package/dist/web/index.d.ts.map +1 -1
  73. package/dist/web/index.js +30 -7
  74. package/dist/web/index.js.map +1 -1
  75. package/dist/web/public/app.js +5 -855
  76. package/dist/web/public/confirm.js +70 -0
  77. package/dist/web/public/index.html +121 -120
  78. package/dist/web/public/input.js +107 -0
  79. package/dist/web/public/messages.js +395 -0
  80. package/dist/web/public/sidebar.js +82 -0
  81. package/dist/web/public/state.js +72 -0
  82. package/dist/web/public/style.css +996 -949
  83. package/dist/web/public/utils.js +37 -0
  84. package/dist/web/public/websocket.js +267 -0
  85. package/dist/web/public/ws.js +19 -0
  86. package/dist/web/session-bridge.d.ts.map +1 -1
  87. package/dist/web/session-bridge.js +3 -7
  88. package/dist/web/session-bridge.js.map +1 -1
  89. package/package.json +55 -54
  90. package/scripts/copy-native.mjs +24 -0
  91. package/scripts/copy-web.mjs +15 -0
  92. package/src/agent/index.ts +25 -25
  93. package/src/agent/llm.ts +4 -0
  94. package/src/agent/project.ts +366 -0
  95. package/src/agent/prompt-builder.ts +23 -13
  96. package/src/agent/providers/anthropic.ts +179 -172
  97. package/src/agent/providers/google.ts +210 -211
  98. package/src/agent/providers/ollama.ts +186 -182
  99. package/src/agent/providers/openai.ts +73 -5
  100. package/src/agent/session.ts +275 -153
  101. package/src/config/index.ts +342 -335
  102. package/src/tools/__tests__/bash.test.ts +1 -1
  103. package/src/tools/__tests__/edit.test.ts +981 -385
  104. package/src/tools/__tests__/grep.test.ts +35 -0
  105. package/src/tools/bash.ts +97 -11
  106. package/src/tools/edit.ts +414 -154
  107. package/src/tools/git.ts +76 -76
  108. package/src/tools/grep.ts +57 -30
  109. package/src/tools/read.ts +61 -25
  110. package/src/tools/registry.ts +231 -265
  111. package/src/tools/write_plan.ts +21 -5
  112. package/src/tui/index.ts +786 -785
  113. package/src/web/index.ts +29 -7
  114. package/src/web/public/app.js +5 -855
  115. package/src/web/public/confirm.js +70 -0
  116. package/src/web/public/index.html +121 -120
  117. package/src/web/public/input.js +107 -0
  118. package/src/web/public/messages.js +395 -0
  119. package/src/web/public/sidebar.js +82 -0
  120. package/src/web/public/state.js +72 -0
  121. package/src/web/public/style.css +996 -949
  122. package/src/web/public/utils.js +37 -0
  123. package/src/web/public/websocket.js +267 -0
  124. package/src/web/public/ws.js +19 -0
  125. package/src/web/session-bridge.ts +193 -194
  126. package/.kilo/plans/1780064105789-mighty-pixel.md +0 -171
  127. package/.kilo/plans/1780293725888-quick-wizard.md +0 -62
  128. package/.lonny/plan-web-cwd-status.md +0 -38
  129. package/src/tools/exec.ts +0 -348
@@ -4,6 +4,7 @@ import * as os from 'node:os';
4
4
  import * as path from 'node:path';
5
5
  import { saveTokenUsage } from '../config/tokens.js';
6
6
  import { FileReadTracker } from '../diff/apply.js';
7
+ import { fmtErr } from '../tools/errors.js';
7
8
  import { ToolRegistry } from '../tools/registry.js';
8
9
  import { compact, shouldCompact } from './compaction.js';
9
10
  import { EventChannels, getGlobalEventBus } from './event-bus.js';
@@ -179,14 +180,33 @@ export function formatToolInput(tc) {
179
180
  }
180
181
  return parts.join(' \u2502 ');
181
182
  }
182
- function printTokenStats(turnIn, turnOut, totalIn, totalOut, turnApi, totalApi, output) {
183
+ function printTokenStats(turnIn, turnOut, totalIn, totalOut, turnApi, totalApi, output, turnCacheHit, turnCacheMiss, totalCacheHit, totalCacheMiss) {
183
184
  const bus = getGlobalEventBus();
184
- bus.emit(EventChannels.TOKEN_STATS, { turnIn, turnOut, totalIn, totalOut, turnApi, totalApi });
185
+ bus.emit(EventChannels.TOKEN_STATS, {
186
+ turnIn,
187
+ turnOut,
188
+ totalIn,
189
+ totalOut,
190
+ turnApi,
191
+ totalApi,
192
+ turnCacheHit,
193
+ turnCacheMiss,
194
+ totalCacheHit,
195
+ totalCacheMiss,
196
+ });
185
197
  // Skip terminal output in Web UI mode
186
198
  if (output?.suppressToolOutput)
187
199
  return;
188
200
  const total = totalIn + totalOut;
189
- const msg = ` ${GY}┃${RS} ${GY}${BLD}▴${RS}${GY}${turnIn}${RS} ${GY}${BLD}▾${RS}${GY}${turnOut}${RS} ${GY}total${RS} ${total} ${GY}calls${RS} ${turnApi}(${totalApi})`;
201
+ 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})`;
202
+ // Show cache hit rate if available
203
+ const cacheHit = totalCacheHit ?? 0;
204
+ const cacheMiss = totalCacheMiss ?? 0;
205
+ const cacheTotal = cacheHit + cacheMiss;
206
+ if (cacheTotal > 0) {
207
+ const pct = Math.round((cacheHit / cacheTotal) * 100);
208
+ msg += ` ${GY}cached${RS} ${pct}%`;
209
+ }
190
210
  writeOut(`\n${msg}\n`, output);
191
211
  }
192
212
  export class Session {
@@ -216,6 +236,10 @@ export class Session {
216
236
  turnOutputTokens = 0;
217
237
  turnApiCalls = 0;
218
238
  totalApiCalls = 0;
239
+ turnCacheHitTokens = 0;
240
+ turnCacheMissTokens = 0;
241
+ totalCacheHitTokens = 0;
242
+ totalCacheMissTokens = 0;
219
243
  stopped = false;
220
244
  abortController = null;
221
245
  constructor(config, output) {
@@ -241,7 +265,16 @@ export class Session {
241
265
  else {
242
266
  this.provider = new AnthropicProvider(config.apiKey, config.baseUrl, config.model);
243
267
  }
244
- this.messages = [{ role: 'system', content: buildSystemPrompt(config) }];
268
+ // Placeholder until initSystemPrompt() is called
269
+ this.messages = [{ role: 'system', content: '' }];
270
+ // Initialize system prompt asynchronously
271
+ this.initSystemPrompt(config);
272
+ }
273
+ /** Initialize the system prompt asynchronously */
274
+ initSystemPrompt(config) {
275
+ buildSystemPrompt(config).then(prompt => {
276
+ this.messages = [{ role: 'system', content: prompt }];
277
+ });
245
278
  }
246
279
  /** Persist the current session to ~/.lonny/sessions/ */
247
280
  save() {
@@ -254,6 +287,8 @@ export class Session {
254
287
  totalInputTokens: this.totalInputTokens,
255
288
  totalOutputTokens: this.totalOutputTokens,
256
289
  totalApiCalls: this.totalApiCalls,
290
+ totalCacheHitTokens: this.totalCacheHitTokens || undefined,
291
+ totalCacheMissTokens: this.totalCacheMissTokens || undefined,
257
292
  mode: this.config.mode,
258
293
  model: this.config.model,
259
294
  provider: this.config.provider,
@@ -262,7 +297,7 @@ export class Session {
262
297
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
263
298
  }
264
299
  /** Try to load a saved session for the given cwd. Returns null if none exists. */
265
- static load(config, output) {
300
+ static async load(config, output) {
266
301
  const filePath = getSessionFilePath(config.cwd);
267
302
  let data;
268
303
  try {
@@ -285,12 +320,15 @@ export class Session {
285
320
  if (data.model !== config.model ||
286
321
  data.provider !== config.provider ||
287
322
  data.mode !== config.mode) {
288
- session.messages[0] = { role: 'system', content: buildSystemPrompt(config) };
323
+ const prompt = await buildSystemPrompt(config);
324
+ session.messages[0] = { role: 'system', content: prompt };
289
325
  }
290
326
  // Restore token stats
291
327
  session.totalInputTokens = data.totalInputTokens;
292
328
  session.totalOutputTokens = data.totalOutputTokens;
293
329
  session.totalApiCalls = data.totalApiCalls;
330
+ session.totalCacheHitTokens = data.totalCacheHitTokens ?? 0;
331
+ session.totalCacheMissTokens = data.totalCacheMissTokens ?? 0;
294
332
  return session;
295
333
  }
296
334
  /** Remove the saved session file for the given cwd. */
@@ -303,9 +341,10 @@ export class Session {
303
341
  // Ignore if file doesn't exist
304
342
  }
305
343
  }
306
- setMode(mode) {
344
+ async setMode(mode) {
307
345
  this.config.mode = mode;
308
- this.messages[0] = { role: 'system', content: buildSystemPrompt(this.config) };
346
+ const prompt = await buildSystemPrompt(this.config);
347
+ this.messages[0] = { role: 'system', content: prompt };
309
348
  this.registry.setMode(mode);
310
349
  this.save();
311
350
  }
@@ -322,6 +361,8 @@ export class Session {
322
361
  /** Reset the stopped flag for a new conversation */
323
362
  resetStopped() {
324
363
  this.stopped = false;
364
+ // Create new AbortController for next conversation
365
+ this.abortController = new AbortController();
325
366
  }
326
367
  async chat(userPrompt) {
327
368
  const bus = getGlobalEventBus();
@@ -334,6 +375,8 @@ export class Session {
334
375
  this.turnInputTokens = 0;
335
376
  this.turnOutputTokens = 0;
336
377
  this.turnApiCalls = 0;
378
+ this.turnCacheHitTokens = 0;
379
+ this.turnCacheMissTokens = 0;
337
380
  let iterations = 0;
338
381
  const maxIterations = 30;
339
382
  // Reset stopped flag for new conversation
@@ -361,171 +404,214 @@ export class Session {
361
404
  bus.emit(EventChannels.TURN_START, { prompt: userPrompt, iteration: iterations });
362
405
  bus.emit(EventChannels.LLM_STREAM_START, { iteration: iterations });
363
406
  const stream = this.provider.chat(this.messages, this.registry.getDefinitions(), this.abortController.signal);
364
- for await (const chunk of stream) {
365
- if (chunk.reasoning_content) {
366
- reasoningContent = chunk.reasoning_content;
367
- // Stream reasoning content in real-time (only when no text in same chunk)
368
- if (!chunk.text) {
369
- // Emit thinking via EventBus for Web UI
370
- bus.emit(EventChannels.THINKING, { text: chunk.reasoning_content });
371
- if (!reasoningOutput) {
372
- reasoningOutput = true;
373
- reasoningLineStart = true;
374
- if (!out?.suppressToolOutput) {
375
- writeOut(thinkTopBorder(), out);
376
- }
377
- }
378
- // Terminal display with box drawing (skip in Web UI mode, handled by EventBus)
379
- if (!out?.suppressToolOutput) {
380
- // Track column position on current line for wrapping
381
- let thinkCol = 0;
382
- // Handle newlines in streamed content - add left border on each new line
383
- // Also manually wrap long lines so wrapped lines keep the │ prefix.
384
- let remaining = chunk.reasoning_content;
385
- const maxContentWidth = termWidth() - THINK_PREFIX_WIDTH;
386
- while (remaining.length > 0) {
387
- if (reasoningLineStart) {
388
- writeOut(` ${GY}│${RS}${TH}`, out);
389
- reasoningLineStart = false;
390
- thinkCol = 0;
407
+ try {
408
+ for await (const chunk of stream) {
409
+ if (chunk.reasoning_content) {
410
+ reasoningContent = chunk.reasoning_content;
411
+ // Stream reasoning content in real-time (only when no text in same chunk)
412
+ if (!chunk.text) {
413
+ // Emit thinking via EventBus for Web UI
414
+ bus.emit(EventChannels.THINKING, { text: chunk.reasoning_content });
415
+ if (!reasoningOutput) {
416
+ reasoningOutput = true;
417
+ reasoningLineStart = true;
418
+ if (!out?.suppressToolOutput) {
419
+ writeOut(thinkTopBorder(), out);
391
420
  }
392
- const nlIdx = remaining.indexOf('\n');
393
- if (nlIdx === -1) {
394
- // No newline — write as much as fits on current line, wrap if needed
395
- while (remaining.length > 0) {
396
- const segWidth = visibleWidth(remaining);
421
+ }
422
+ // Terminal display with box drawing (skip in Web UI mode, handled by EventBus)
423
+ if (!out?.suppressToolOutput) {
424
+ // Track column position on current line for wrapping
425
+ let thinkCol = 0;
426
+ // Handle newlines in streamed content - add left border on each new line
427
+ // Also manually wrap long lines so wrapped lines keep the │ prefix.
428
+ let remaining = chunk.reasoning_content;
429
+ const maxContentWidth = termWidth() - THINK_PREFIX_WIDTH;
430
+ while (remaining.length > 0) {
431
+ if (reasoningLineStart) {
432
+ writeOut(` ${GY}│${RS}${TH}`, out);
433
+ reasoningLineStart = false;
434
+ thinkCol = 0;
435
+ }
436
+ const nlIdx = remaining.indexOf('\n');
437
+ if (nlIdx === -1) {
438
+ // No newline — write as much as fits on current line, wrap if needed
439
+ while (remaining.length > 0) {
440
+ const segWidth = visibleWidth(remaining);
441
+ const avail = maxContentWidth - thinkCol;
442
+ if (segWidth <= avail) {
443
+ // Fits entirely on current line
444
+ writeOut(remaining, out);
445
+ thinkCol += segWidth;
446
+ remaining = '';
447
+ }
448
+ else if (avail <= 0) {
449
+ // Current line is full, wrap to next
450
+ writeOut(`${RS}\n`, out);
451
+ writeOut(` ${GY}│${RS}${TH}`, out);
452
+ thinkCol = 0;
453
+ }
454
+ else {
455
+ // Write first part that fits, then wrap
456
+ // Find character boundary that fits within avail
457
+ let cut = avail;
458
+ while (cut > 0 && visibleWidth(remaining.slice(0, cut)) > avail)
459
+ cut--;
460
+ if (cut <= 0)
461
+ cut = 1;
462
+ writeOut(remaining.slice(0, cut), out);
463
+ writeOut(`${RS}\n`, out);
464
+ writeOut(` ${GY}│${RS}${TH}`, out);
465
+ thinkCol = 0;
466
+ remaining = remaining.slice(cut);
467
+ }
468
+ }
469
+ }
470
+ else {
471
+ // Has newline — process the segment up to newline
472
+ const segment = remaining.slice(0, nlIdx);
473
+ const segWidth = visibleWidth(segment);
397
474
  const avail = maxContentWidth - thinkCol;
398
475
  if (segWidth <= avail) {
399
- // Fits entirely on current line
400
- writeOut(remaining, out);
401
- thinkCol += segWidth;
402
- remaining = '';
403
- }
404
- else if (avail <= 0) {
405
- // Current line is full, wrap to next
476
+ // Segment fits on current line
477
+ writeOut(segment, out);
406
478
  writeOut(`${RS}\n`, out);
407
- writeOut(` ${GY}│${RS}${TH}`, out);
479
+ reasoningLineStart = true;
408
480
  thinkCol = 0;
409
481
  }
410
482
  else {
411
- // Write first part that fits, then wrap
412
- // Find character boundary that fits within avail
413
- let cut = avail;
414
- while (cut > 0 && visibleWidth(remaining.slice(0, cut)) > avail)
415
- cut--;
416
- if (cut <= 0)
417
- cut = 1;
418
- writeOut(remaining.slice(0, cut), out);
483
+ // Segment too long write what fits, wrap, then rest
484
+ let rest = segment;
485
+ // Write remainder of current line
486
+ if (avail > 0) {
487
+ let cut = avail;
488
+ while (cut > 0 && visibleWidth(rest.slice(0, cut)) > avail)
489
+ cut--;
490
+ if (cut <= 0)
491
+ cut = 1;
492
+ writeOut(rest.slice(0, cut), out);
493
+ rest = rest.slice(cut);
494
+ }
419
495
  writeOut(`${RS}\n`, out);
420
- writeOut(` ${GY}│${RS}${TH}`, out);
496
+ reasoningLineStart = true;
421
497
  thinkCol = 0;
422
- remaining = remaining.slice(cut);
423
- }
424
- }
425
- }
426
- else {
427
- // Has newline — process the segment up to newline
428
- const segment = remaining.slice(0, nlIdx);
429
- const segWidth = visibleWidth(segment);
430
- const avail = maxContentWidth - thinkCol;
431
- if (segWidth <= avail) {
432
- // Segment fits on current line
433
- writeOut(segment, out);
434
- writeOut(`${RS}\n`, out);
435
- reasoningLineStart = true;
436
- thinkCol = 0;
437
- }
438
- else {
439
- // Segment too long — write what fits, wrap, then rest
440
- let rest = segment;
441
- // Write remainder of current line
442
- if (avail > 0) {
443
- let cut = avail;
444
- while (cut > 0 && visibleWidth(rest.slice(0, cut)) > avail)
445
- cut--;
446
- if (cut <= 0)
447
- cut = 1;
448
- writeOut(rest.slice(0, cut), out);
449
- rest = rest.slice(cut);
450
- }
451
- writeOut(`${RS}\n`, out);
452
- reasoningLineStart = true;
453
- thinkCol = 0;
454
- // Write rest of segment on continuation line(s)
455
- if (rest.length > 0) {
456
- writeOut(` ${GY}│${RS}${TH}`, out);
457
- reasoningLineStart = false;
458
- while (rest.length > 0) {
459
- const rw = visibleWidth(rest);
460
- if (rw <= maxContentWidth) {
461
- writeOut(rest, out);
462
- thinkCol = rw;
463
- rest = '';
464
- }
465
- else {
466
- let cut = maxContentWidth;
467
- while (cut > 0 && visibleWidth(rest.slice(0, cut)) > maxContentWidth)
468
- cut--;
469
- if (cut <= 0)
470
- cut = 1;
471
- writeOut(rest.slice(0, cut), out);
472
- writeOut(`${RS}\n`, out);
473
- writeOut(` ${GY}│${RS}${TH}`, out);
474
- rest = rest.slice(cut);
498
+ // Write rest of segment on continuation line(s)
499
+ if (rest.length > 0) {
500
+ writeOut(` ${GY}│${RS}${TH}`, out);
501
+ reasoningLineStart = false;
502
+ while (rest.length > 0) {
503
+ const rw = visibleWidth(rest);
504
+ if (rw <= maxContentWidth) {
505
+ writeOut(rest, out);
506
+ thinkCol = rw;
507
+ rest = '';
508
+ }
509
+ else {
510
+ let cut = maxContentWidth;
511
+ while (cut > 0 && visibleWidth(rest.slice(0, cut)) > maxContentWidth)
512
+ cut--;
513
+ if (cut <= 0)
514
+ cut = 1;
515
+ writeOut(rest.slice(0, cut), out);
516
+ writeOut(`${RS}\n`, out);
517
+ writeOut(` ${GY}│${RS}${TH}`, out);
518
+ rest = rest.slice(cut);
519
+ }
475
520
  }
476
521
  }
522
+ writeOut(`${RS}\n`, out);
523
+ reasoningLineStart = true;
524
+ thinkCol = 0;
477
525
  }
478
- writeOut(`${RS}\n`, out);
479
- reasoningLineStart = true;
480
- thinkCol = 0;
526
+ remaining = remaining.slice(nlIdx + 1);
481
527
  }
482
- remaining = remaining.slice(nlIdx + 1);
483
528
  }
484
529
  }
485
530
  }
486
531
  }
487
- }
488
- if (chunk.type === 'text' && chunk.text) {
489
- if (reasoningOutput) {
490
- bus.emit(EventChannels.THINKING_END, {});
491
- if (!out?.suppressToolOutput) {
492
- writeOut(`${RS}\n`, out);
493
- writeOut(thinkBottomBorder(), out);
532
+ if (chunk.type === 'text' && chunk.text) {
533
+ if (reasoningOutput) {
534
+ bus.emit(EventChannels.THINKING_END, {});
535
+ if (!out?.suppressToolOutput) {
536
+ writeOut(`${RS}\n`, out);
537
+ writeOut(thinkBottomBorder(), out);
538
+ }
539
+ reasoningOutput = false;
540
+ reasoningLineStart = false;
494
541
  }
495
- reasoningOutput = false;
496
- reasoningLineStart = false;
542
+ fullResponse += chunk.text;
543
+ writeOut(chunk.text, out);
497
544
  }
498
- fullResponse += chunk.text;
499
- writeOut(chunk.text, out);
500
- }
501
- else if (chunk.type === 'tool_use' && chunk.tool_call) {
502
- toolCalls.push(chunk.tool_call);
503
- }
504
- else if (chunk.type === 'complete') {
505
- if (chunk.usage) {
506
- this.turnInputTokens += chunk.usage.input_tokens;
507
- this.turnOutputTokens += chunk.usage.output_tokens;
508
- this.totalInputTokens += chunk.usage.input_tokens;
509
- this.totalOutputTokens += chunk.usage.output_tokens;
545
+ else if (chunk.type === 'tool_use' && chunk.tool_call) {
546
+ toolCalls.push(chunk.tool_call);
510
547
  }
511
- if (chunk.finish_reason === 'stop' || chunk.finish_reason === 'end_turn') {
512
- if (toolCalls.length === 0) {
513
- const finalAssistantMsg = {
514
- role: 'assistant',
515
- content: fullResponse || null,
516
- reasoning_content: reasoningContent,
517
- };
518
- this.messages.push(finalAssistantMsg);
519
- printTokenStats(this.turnInputTokens, this.turnOutputTokens, this.totalInputTokens, this.totalOutputTokens, this.turnApiCalls, this.totalApiCalls, out);
520
- writeOut('\n\n', out);
521
- saveTokenUsage(this.config.cwd, this.turnInputTokens, this.turnOutputTokens, this.turnApiCalls);
522
- bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 });
523
- this.save();
524
- return;
548
+ else if (chunk.type === 'complete') {
549
+ if (chunk.usage) {
550
+ this.turnInputTokens += chunk.usage.input_tokens;
551
+ this.turnOutputTokens += chunk.usage.output_tokens;
552
+ this.totalInputTokens += chunk.usage.input_tokens;
553
+ this.totalOutputTokens += chunk.usage.output_tokens;
554
+ // Accumulate DeepSeek cache hit/miss tokens
555
+ if (chunk.usage.prompt_cache_hit_tokens != null) {
556
+ this.turnCacheHitTokens += chunk.usage.prompt_cache_hit_tokens;
557
+ this.totalCacheHitTokens += chunk.usage.prompt_cache_hit_tokens;
558
+ }
559
+ if (chunk.usage.prompt_cache_miss_tokens != null) {
560
+ this.turnCacheMissTokens += chunk.usage.prompt_cache_miss_tokens;
561
+ this.totalCacheMissTokens += chunk.usage.prompt_cache_miss_tokens;
562
+ }
563
+ }
564
+ if (chunk.finish_reason === 'stop' || chunk.finish_reason === 'end_turn') {
565
+ if (toolCalls.length === 0) {
566
+ const finalAssistantMsg = {
567
+ role: 'assistant',
568
+ content: fullResponse || null,
569
+ reasoning_content: reasoningContent,
570
+ };
571
+ this.messages.push(finalAssistantMsg);
572
+ printTokenStats(this.turnInputTokens, this.turnOutputTokens, this.totalInputTokens, this.totalOutputTokens, this.turnApiCalls, this.totalApiCalls, out, this.turnCacheHitTokens, this.turnCacheMissTokens, this.totalCacheHitTokens, this.totalCacheMissTokens);
573
+ writeOut('\n\n', out);
574
+ saveTokenUsage(this.config.cwd, this.turnInputTokens, this.turnOutputTokens, this.turnApiCalls);
575
+ bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 });
576
+ this.save();
577
+ return;
578
+ }
525
579
  }
526
580
  }
527
581
  }
528
582
  }
583
+ catch (e) {
584
+ const errMsg = fmtErr(e);
585
+ const partialContent = fullResponse ? fullResponse.slice(0, 500) : '(empty)';
586
+ if (!out?.suppressToolOutput) {
587
+ writeOut(`\n${RE}Stream error:${RS} ${errMsg}`, out);
588
+ writeOut(`\n ${GY}┃${RS} Partial response: ${partialContent}\n`, out);
589
+ }
590
+ console.error('[session] Stream error:', errMsg, '| Partial response:', partialContent);
591
+ if (reasoningOutput) {
592
+ bus.emit(EventChannels.THINKING_END, {});
593
+ if (!out?.suppressToolOutput) {
594
+ writeOut(`${RS}\n`, out);
595
+ writeOut(thinkBottomBorder(), out);
596
+ }
597
+ }
598
+ bus.emit(EventChannels.LLM_STREAM_END, { iteration: iterations, toolCallCount: 0 });
599
+ bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 });
600
+ // If there are pending toolCalls that weren't executed due to abort,
601
+ // add an assistant message so the model knows they weren't executed
602
+ if (toolCalls.length > 0) {
603
+ const interruptedMsg = {
604
+ role: 'assistant',
605
+ content: null,
606
+ tool_calls: toolCalls,
607
+ reasoning_content: reasoningContent,
608
+ };
609
+ this.messages.push(interruptedMsg);
610
+ }
611
+ saveTokenUsage(this.config.cwd, this.turnInputTokens, this.turnOutputTokens, this.turnApiCalls);
612
+ this.save();
613
+ return;
614
+ }
529
615
  bus.emit(EventChannels.LLM_STREAM_END, {
530
616
  iteration: iterations,
531
617
  toolCallCount: toolCalls.length,
@@ -548,7 +634,7 @@ export class Session {
548
634
  reasoning_content: reasoningContent,
549
635
  };
550
636
  this.messages.push(finalAssistantMsg);
551
- printTokenStats(this.turnInputTokens, this.turnOutputTokens, this.totalInputTokens, this.totalOutputTokens, this.turnApiCalls, this.totalApiCalls, out);
637
+ printTokenStats(this.turnInputTokens, this.turnOutputTokens, this.totalInputTokens, this.totalOutputTokens, this.turnApiCalls, this.totalApiCalls, out, this.turnCacheHitTokens, this.turnCacheMissTokens, this.totalCacheHitTokens, this.totalCacheMissTokens);
552
638
  writeOut('\n\n', out);
553
639
  }
554
640
  saveTokenUsage(this.config.cwd, this.turnInputTokens, this.turnOutputTokens, this.turnApiCalls);
@@ -566,7 +652,7 @@ export class Session {
566
652
  this.save();
567
653
  // ── User confirmation for write-type tool calls ──
568
654
  if (!this.config.autoApprove && this.output?.confirmTool && toolCalls.length > 0) {
569
- const writeTools = ['edit', 'bash', 'write_plan', 'exec', 'install_skill'];
655
+ const writeTools = ['edit', 'bash', 'write_plan', 'install_skill'];
570
656
  const needsConfirm = toolCalls.filter(tc => writeTools.includes(tc.name));
571
657
  if (needsConfirm.length > 0) {
572
658
  const approved = await this.output.confirmTool(needsConfirm);
@@ -583,9 +669,20 @@ export class Session {
583
669
  }
584
670
  }
585
671
  }
586
- for (const tc of toolCalls) {
672
+ for (let i = 0; i < toolCalls.length; i++) {
673
+ const tc = toolCalls[i];
587
674
  // Check if stop was requested after each tool call
588
675
  if (this.isStopped()) {
676
+ // Add assistant message with remaining tool_calls so model knows they weren't executed
677
+ const remainingToolCalls = toolCalls.slice(i);
678
+ if (remainingToolCalls.length > 0) {
679
+ const interruptedMsg = {
680
+ role: 'assistant',
681
+ content: null,
682
+ tool_calls: remainingToolCalls,
683
+ };
684
+ this.messages.push(interruptedMsg);
685
+ }
589
686
  bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length });
590
687
  this.save();
591
688
  return;
@@ -629,7 +726,7 @@ export class Session {
629
726
  }
630
727
  }
631
728
  if (iterations >= maxIterations) {
632
- printTokenStats(this.turnInputTokens, this.turnOutputTokens, this.totalInputTokens, this.totalOutputTokens, this.turnApiCalls, this.totalApiCalls, out);
729
+ printTokenStats(this.turnInputTokens, this.turnOutputTokens, this.totalInputTokens, this.totalOutputTokens, this.turnApiCalls, this.totalApiCalls, out, this.turnCacheHitTokens, this.turnCacheMissTokens, this.totalCacheHitTokens, this.totalCacheMissTokens);
633
730
  writeOut('\nAgent reached maximum iterations. Stopping.\n', out);
634
731
  }
635
732
  saveTokenUsage(this.config.cwd, this.turnInputTokens, this.turnOutputTokens, this.turnApiCalls);