gencode-ai 0.3.0 → 0.4.1

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 (116) hide show
  1. package/RELEASE_NOTES_v0.4.0.md +140 -0
  2. package/dist/agent/agent.d.ts +17 -2
  3. package/dist/agent/agent.d.ts.map +1 -1
  4. package/dist/agent/agent.js +279 -49
  5. package/dist/agent/agent.js.map +1 -1
  6. package/dist/agent/types.d.ts +15 -1
  7. package/dist/agent/types.d.ts.map +1 -1
  8. package/dist/checkpointing/checkpoint-manager.d.ts +24 -0
  9. package/dist/checkpointing/checkpoint-manager.d.ts.map +1 -1
  10. package/dist/checkpointing/checkpoint-manager.js +28 -0
  11. package/dist/checkpointing/checkpoint-manager.js.map +1 -1
  12. package/dist/cli/components/App.d.ts +8 -0
  13. package/dist/cli/components/App.d.ts.map +1 -1
  14. package/dist/cli/components/App.js +478 -36
  15. package/dist/cli/components/App.js.map +1 -1
  16. package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
  17. package/dist/cli/components/CommandSuggestions.js +2 -0
  18. package/dist/cli/components/CommandSuggestions.js.map +1 -1
  19. package/dist/cli/components/Header.d.ts +6 -1
  20. package/dist/cli/components/Header.d.ts.map +1 -1
  21. package/dist/cli/components/Header.js +3 -3
  22. package/dist/cli/components/Header.js.map +1 -1
  23. package/dist/cli/components/Messages.d.ts.map +1 -1
  24. package/dist/cli/components/Messages.js +7 -9
  25. package/dist/cli/components/Messages.js.map +1 -1
  26. package/dist/cli/index.js +3 -2
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/config/types.d.ts +20 -1
  29. package/dist/config/types.d.ts.map +1 -1
  30. package/dist/config/types.js.map +1 -1
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +2 -2
  33. package/dist/input/history-manager.d.ts +78 -0
  34. package/dist/input/history-manager.d.ts.map +1 -0
  35. package/dist/input/history-manager.js +224 -0
  36. package/dist/input/history-manager.js.map +1 -0
  37. package/dist/input/index.d.ts +6 -0
  38. package/dist/input/index.d.ts.map +1 -0
  39. package/dist/input/index.js +5 -0
  40. package/dist/input/index.js.map +1 -0
  41. package/dist/prompts/index.js +3 -3
  42. package/dist/prompts/index.js.map +1 -1
  43. package/dist/providers/gemini.d.ts.map +1 -1
  44. package/dist/providers/gemini.js +33 -2
  45. package/dist/providers/gemini.js.map +1 -1
  46. package/dist/providers/google.d.ts +22 -0
  47. package/dist/providers/google.d.ts.map +1 -0
  48. package/dist/providers/google.js +297 -0
  49. package/dist/providers/google.js.map +1 -0
  50. package/dist/providers/index.d.ts +4 -4
  51. package/dist/providers/index.js +11 -11
  52. package/dist/providers/index.js.map +1 -1
  53. package/dist/providers/openai.d.ts.map +1 -1
  54. package/dist/providers/openai.js +6 -0
  55. package/dist/providers/openai.js.map +1 -1
  56. package/dist/providers/registry.js +3 -3
  57. package/dist/providers/registry.js.map +1 -1
  58. package/dist/providers/types.d.ts +30 -4
  59. package/dist/providers/types.d.ts.map +1 -1
  60. package/dist/session/compression/engine.d.ts +109 -0
  61. package/dist/session/compression/engine.d.ts.map +1 -0
  62. package/dist/session/compression/engine.js +311 -0
  63. package/dist/session/compression/engine.js.map +1 -0
  64. package/dist/session/compression/index.d.ts +12 -0
  65. package/dist/session/compression/index.d.ts.map +1 -0
  66. package/dist/session/compression/index.js +11 -0
  67. package/dist/session/compression/index.js.map +1 -0
  68. package/dist/session/compression/types.d.ts +90 -0
  69. package/dist/session/compression/types.d.ts.map +1 -0
  70. package/dist/session/compression/types.js +17 -0
  71. package/dist/session/compression/types.js.map +1 -0
  72. package/dist/session/manager.d.ts +64 -3
  73. package/dist/session/manager.d.ts.map +1 -1
  74. package/dist/session/manager.js +254 -2
  75. package/dist/session/manager.js.map +1 -1
  76. package/dist/session/types.d.ts +16 -0
  77. package/dist/session/types.d.ts.map +1 -1
  78. package/dist/session/types.js.map +1 -1
  79. package/docs/README.md +1 -0
  80. package/docs/diagrams/compression-decision.mmd +30 -0
  81. package/docs/diagrams/compression-workflow.mmd +54 -0
  82. package/docs/diagrams/layer1-pruning.mmd +45 -0
  83. package/docs/diagrams/layer2-compaction.mmd +42 -0
  84. package/docs/proposals/0007-context-management.md +252 -2
  85. package/docs/proposals/README.md +4 -3
  86. package/docs/providers.md +3 -3
  87. package/docs/session-compression.md +695 -0
  88. package/examples/agent-demo.ts +23 -1
  89. package/examples/basic.ts +3 -3
  90. package/package.json +4 -5
  91. package/src/agent/agent.ts +314 -52
  92. package/src/agent/types.ts +19 -1
  93. package/src/checkpointing/checkpoint-manager.ts +48 -0
  94. package/src/cli/components/App.tsx +553 -34
  95. package/src/cli/components/CommandSuggestions.tsx +2 -0
  96. package/src/cli/components/Header.tsx +16 -1
  97. package/src/cli/components/Messages.tsx +20 -14
  98. package/src/cli/index.tsx +3 -2
  99. package/src/config/types.ts +26 -1
  100. package/src/index.ts +3 -3
  101. package/src/input/history-manager.ts +289 -0
  102. package/src/input/index.ts +6 -0
  103. package/src/prompts/index.test.ts +2 -1
  104. package/src/prompts/index.ts +3 -3
  105. package/src/providers/{gemini.ts → google.ts} +69 -18
  106. package/src/providers/index.ts +14 -14
  107. package/src/providers/openai.ts +7 -0
  108. package/src/providers/registry.ts +3 -3
  109. package/src/providers/types.ts +33 -3
  110. package/src/session/compression/engine.ts +406 -0
  111. package/src/session/compression/index.ts +18 -0
  112. package/src/session/compression/types.ts +102 -0
  113. package/src/session/manager.ts +326 -3
  114. package/src/session/types.ts +21 -0
  115. package/tests/input-history-manager.test.ts +335 -0
  116. package/tests/session-checkpoint-persistence.test.ts +198 -0
@@ -24,7 +24,7 @@ function getConfig() {
24
24
  } else if (process.env.OPENAI_API_KEY) {
25
25
  return { provider: 'openai' as const, model: 'gpt-4o' };
26
26
  } else if (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY) {
27
- return { provider: 'gemini' as const, model: 'gemini-2.0-flash' };
27
+ return { provider: 'google' as const, model: 'gemini-2.0-flash' };
28
28
  }
29
29
  throw new Error('No API key found. Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_API_KEY');
30
30
  }
@@ -93,6 +93,10 @@ Use the Glob and Read tools to explore.`;
93
93
 
94
94
  case 'error':
95
95
  console.log(chalk.red('✗ Error:') + ` ${event.error.message}`);
96
+ // Display full stack trace if DEBUG is enabled
97
+ if (event.error.stack && process.env.DEBUG) {
98
+ console.log(chalk.dim(event.error.stack));
99
+ }
96
100
  break;
97
101
 
98
102
  case 'done':
@@ -101,6 +105,24 @@ Use the Glob and Read tools to explore.`;
101
105
  for (const line of respLines) {
102
106
  console.log(' ' + line);
103
107
  }
108
+ console.log();
109
+
110
+ // Display usage and cost information
111
+ if (event.usage) {
112
+ console.log(
113
+ chalk.dim(
114
+ ` Usage: ${event.usage.inputTokens} in / ${event.usage.outputTokens} out`
115
+ )
116
+ );
117
+ }
118
+ if (event.cost) {
119
+ console.log(chalk.dim(` Cost: ~$${event.cost.total.toFixed(4)}`));
120
+ }
121
+
122
+ // Warn if suspicious token count
123
+ if (event.usage?.outputTokens === 0 && event.text) {
124
+ console.log(chalk.yellow(' ⚠ Warning: 0 output tokens reported but text was returned'));
125
+ }
104
126
  break;
105
127
  }
106
128
  }
package/examples/basic.ts CHANGED
@@ -19,7 +19,7 @@ if (proxyUrl) {
19
19
  import {
20
20
  OpenAIProvider,
21
21
  AnthropicProvider,
22
- GeminiProvider,
22
+ GoogleProvider,
23
23
  createProvider,
24
24
  inferProvider,
25
25
  type LLMProvider,
@@ -126,7 +126,7 @@ async function main() {
126
126
 
127
127
  if (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY) {
128
128
  tests.push({
129
- provider: new GeminiProvider(),
129
+ provider: new GoogleProvider(),
130
130
  model: 'gemini-2.0-flash',
131
131
  envKey: 'GOOGLE_API_KEY',
132
132
  });
@@ -151,7 +151,7 @@ async function main() {
151
151
  // Test createProvider factory (use first available provider)
152
152
  console.log('\n--- Factory Test ---');
153
153
  const firstProvider = tests[0];
154
- const providerName = firstProvider.provider.name as 'openai' | 'anthropic' | 'gemini';
154
+ const providerName = firstProvider.provider.name as 'openai' | 'anthropic' | 'google';
155
155
  const factoryProvider = createProvider({ provider: providerName });
156
156
  console.log(`Created provider via factory: ${factoryProvider.name}`);
157
157
 
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "gencode-ai",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "An open-source AI assistant for your terminal",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "bin": {
9
- "gencode": "./dist/cli/index.js"
9
+ "gencode": "dist/cli/index.js"
10
10
  },
11
11
  "scripts": {
12
12
  "build": "tsc",
@@ -14,8 +14,7 @@
14
14
  "start": "node dist/cli/index.js",
15
15
  "start:dev": "npx tsx src/cli/index.tsx",
16
16
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
17
- "example": "npx tsx examples/basic.ts",
18
- "migrate": "npx tsx scripts/migrate.ts"
17
+ "example": "npx tsx examples/basic.ts"
19
18
  },
20
19
  "keywords": [
21
20
  "agent",
@@ -59,4 +58,4 @@
59
58
  "tsx": "^4.21.0",
60
59
  "typescript": "^5.9.3"
61
60
  }
62
- }
61
+ }
@@ -23,6 +23,7 @@ import {
23
23
  type ModeType,
24
24
  type AllowedPrompt,
25
25
  } from '../planning/index.js';
26
+ import { initCheckpointManager } from '../checkpointing/index.js';
26
27
 
27
28
  // Type for askUser callback
28
29
  export type AskUserCallback = (questions: Question[]) => Promise<QuestionAnswer[]>;
@@ -35,7 +36,6 @@ export class Agent {
35
36
  private memoryManager: MemoryManager;
36
37
  private planModeManager: PlanModeManager;
37
38
  private config: AgentConfig;
38
- private messages: Message[] = [];
39
39
  private sessionId: string | null = null;
40
40
  private loadedMemory: LoadedMemory | null = null;
41
41
  private askUserCallback: AskUserCallback | null = null;
@@ -56,9 +56,14 @@ export class Agent {
56
56
  config: config.permissions,
57
57
  projectPath: config.cwd,
58
58
  });
59
- this.sessionManager = new SessionManager();
59
+ this.sessionManager = new SessionManager({
60
+ compression: config.compression,
61
+ });
60
62
  this.memoryManager = new MemoryManager();
61
63
  this.planModeManager = getPlanModeManager();
64
+
65
+ // Set compression engine with current model
66
+ this.sessionManager.setCompressionEngine(this.provider, this.config.model);
62
67
  }
63
68
 
64
69
  /**
@@ -277,6 +282,8 @@ export class Agent {
277
282
  provider: newProvider,
278
283
  authMethod: newAuthMethod,
279
284
  });
285
+ // Update compression engine with new provider and model
286
+ this.sessionManager.setCompressionEngine(this.provider, model);
280
287
  }
281
288
  }
282
289
 
@@ -294,6 +301,38 @@ export class Agent {
294
301
  return this.config.provider;
295
302
  }
296
303
 
304
+ /**
305
+ * Get model information for compression
306
+ */
307
+ getModelInfo(): { contextWindow: number; outputLimit?: number } {
308
+ // Try to get from provider if available
309
+ if (this.provider.getModelInfo) {
310
+ const info = this.provider.getModelInfo(this.config.model);
311
+ if (info.contextWindow) {
312
+ return { contextWindow: info.contextWindow, outputLimit: info.outputLimit };
313
+ }
314
+ }
315
+
316
+ // Fallback: rough estimates based on model name
317
+ // These should eventually be moved to provider implementations
318
+ const model = this.config.model.toLowerCase();
319
+
320
+ if (model.includes('claude')) {
321
+ return { contextWindow: 200_000, outputLimit: 8192 };
322
+ }
323
+
324
+ if (model.includes('gpt-4') || model.includes('gpt-3.5')) {
325
+ return { contextWindow: 128_000, outputLimit: 4096 };
326
+ }
327
+
328
+ if (model.includes('gemini')) {
329
+ return { contextWindow: 1_000_000, outputLimit: 8192 };
330
+ }
331
+
332
+ // Default fallback
333
+ return { contextWindow: 128_000, outputLimit: 4096 };
334
+ }
335
+
297
336
  /**
298
337
  * List available models from the provider API
299
338
  */
@@ -313,7 +352,9 @@ export class Agent {
313
352
  });
314
353
 
315
354
  this.sessionId = session.metadata.id;
316
- this.messages = [];
355
+
356
+ // Initialize checkpoint manager for this session
357
+ initCheckpointManager(this.sessionId);
317
358
 
318
359
  return this.sessionId;
319
360
  }
@@ -328,7 +369,9 @@ export class Agent {
328
369
  }
329
370
 
330
371
  this.sessionId = session.metadata.id;
331
- this.messages = session.messages;
372
+
373
+ // CheckpointManager already restored by SessionManager.load()
374
+ // No need to call initCheckpointManager again
332
375
 
333
376
  return true;
334
377
  }
@@ -343,7 +386,9 @@ export class Agent {
343
386
  }
344
387
 
345
388
  this.sessionId = session.metadata.id;
346
- this.messages = session.messages;
389
+
390
+ // CheckpointManager already restored by SessionManager.load()
391
+ // No need to call initCheckpointManager again
347
392
 
348
393
  return true;
349
394
  }
@@ -382,7 +427,6 @@ export class Agent {
382
427
  async saveSession(): Promise<void> {
383
428
  const current = this.sessionManager.getCurrent();
384
429
  if (current) {
385
- current.messages = this.messages;
386
430
  await this.sessionManager.save(current);
387
431
  }
388
432
  }
@@ -390,21 +434,42 @@ export class Agent {
390
434
  /**
391
435
  * Run a single query through the agent
392
436
  */
393
- async *run(prompt: string): AsyncGenerator<AgentEvent, void, unknown> {
394
- // Auto-create session if none exists
395
- if (!this.sessionId) {
396
- await this.startSession();
437
+ async *run(prompt: string, signal?: AbortSignal): AsyncGenerator<AgentEvent, void, unknown> {
438
+ // Check for abort before starting
439
+ if (signal?.aborted) {
440
+ yield { type: 'error', error: new Error('Operation cancelled') };
441
+ return;
397
442
  }
398
443
 
399
- // Load memory if not already loaded
400
- if (!this.loadedMemory) {
401
- await this.loadMemory();
444
+ // Auto-create session if none exists
445
+ try {
446
+ if (!this.sessionId) {
447
+ await this.startSession();
448
+ }
449
+
450
+ // Load memory if not already loaded
451
+ if (!this.loadedMemory) {
452
+ await this.loadMemory();
453
+ }
454
+ } catch (error) {
455
+ yield {
456
+ type: 'error',
457
+ error: error instanceof Error ? error : new Error(String(error))
458
+ };
459
+ return;
402
460
  }
403
461
 
404
462
  // Add user message
405
463
  const userMessage: Message = { role: 'user', content: prompt };
406
- this.messages.push(userMessage);
407
- await this.sessionManager.addMessage(userMessage);
464
+ try {
465
+ await this.sessionManager.addMessage(userMessage, this.getModelInfo());
466
+ } catch (error) {
467
+ yield {
468
+ type: 'error',
469
+ error: new Error(`Failed to save user message: ${error instanceof Error ? error.message : String(error)}`)
470
+ };
471
+ return;
472
+ }
408
473
 
409
474
  let turns = 0;
410
475
  const maxTurns = this.config.maxTurns ?? 10;
@@ -417,6 +482,10 @@ export class Agent {
417
482
 
418
483
  // Call LLM
419
484
  let response;
485
+ const processingStartTime = Date.now();
486
+ // Determine if streaming is enabled
487
+ const useStreaming = process.env.GEN_STREAM === '1' || this.config.streaming;
488
+
420
489
  try {
421
490
  // Debug prompt loading (enabled with GENCODE_DEBUG_PROMPTS=1)
422
491
  debugPromptLoading(this.config.model, this.config.provider);
@@ -433,18 +502,145 @@ export class Agent {
433
502
  this.config.provider // Fallback provider if model lookup fails
434
503
  );
435
504
 
436
- response = await this.provider.complete({
437
- model: this.config.model,
438
- messages: this.messages,
439
- tools: toolDefs,
440
- systemPrompt,
441
- maxTokens: 4096,
442
- });
505
+ if (useStreaming) {
506
+ // === STREAMING PATH ===
507
+ // Build response incrementally from stream chunks
508
+ const responseBuilder = {
509
+ content: [] as Array<{ type: 'text'; text: string } | { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> }>,
510
+ textBuffer: '',
511
+ toolCalls: new Map<string, { id: string; name: string; inputBuffer: string }>(),
512
+ stopReason: 'end_turn' as 'end_turn' | 'max_tokens' | 'tool_use' | 'stop_sequence',
513
+ usage: undefined as { inputTokens: number; outputTokens: number } | undefined,
514
+ cost: undefined as { inputCost: number; outputCost: number; totalCost: number; currency: string } | undefined,
515
+ };
516
+
517
+ // Process stream chunks
518
+ for await (const chunk of this.provider.stream({
519
+ model: this.config.model,
520
+ messages: this.sessionManager.getMessagesForLLM(),
521
+ tools: toolDefs,
522
+ systemPrompt,
523
+ maxTokens: 4096,
524
+ signal, // Pass abort signal for cancellation
525
+ })) {
526
+ // Check for abort
527
+ if (signal?.aborted) {
528
+ yield { type: 'error', error: new Error('Operation cancelled by user') };
529
+ return;
530
+ }
531
+ switch (chunk.type) {
532
+ case 'text':
533
+ // Accumulate text and yield immediately for real-time display
534
+ responseBuilder.textBuffer += chunk.text;
535
+ yield { type: 'text', text: chunk.text };
536
+ break;
537
+
538
+ case 'reasoning':
539
+ // Forward reasoning content (o1/o3/Gemini 3+ thinking)
540
+ yield { type: 'reasoning_delta', text: chunk.text };
541
+ break;
542
+
543
+ case 'tool_start':
544
+ // Initialize tool call tracking
545
+ responseBuilder.toolCalls.set(chunk.id, {
546
+ id: chunk.id,
547
+ name: chunk.name,
548
+ inputBuffer: '',
549
+ });
550
+ break;
551
+
552
+ case 'tool_input':
553
+ // Accumulate incremental JSON input and forward delta
554
+ const tool = responseBuilder.toolCalls.get(chunk.id);
555
+ if (tool) {
556
+ tool.inputBuffer += chunk.input;
557
+ // Emit incremental tool input for progressive display
558
+ yield { type: 'tool_input_delta', id: chunk.id, delta: chunk.input };
559
+ }
560
+ break;
561
+
562
+ case 'done':
563
+ // Save final metadata
564
+ responseBuilder.stopReason = chunk.response.stopReason;
565
+ responseBuilder.usage = chunk.response.usage;
566
+ responseBuilder.cost = chunk.response.cost;
567
+ break;
568
+
569
+ case 'error':
570
+ yield { type: 'error', error: chunk.error };
571
+ return;
572
+ }
573
+ }
574
+
575
+ // Build complete response from accumulated data
576
+ if (responseBuilder.textBuffer) {
577
+ responseBuilder.content.push({
578
+ type: 'text',
579
+ text: responseBuilder.textBuffer,
580
+ });
581
+ }
582
+
583
+ for (const [_id, tool] of responseBuilder.toolCalls) {
584
+ try {
585
+ responseBuilder.content.push({
586
+ type: 'tool_use',
587
+ id: tool.id,
588
+ name: tool.name,
589
+ input: JSON.parse(tool.inputBuffer || '{}'),
590
+ });
591
+ } catch (error) {
592
+ // If JSON parsing fails, treat as malformed tool call
593
+ yield {
594
+ type: 'error',
595
+ error: new Error(`Failed to parse tool input for ${tool.name}: ${error instanceof Error ? error.message : String(error)}`),
596
+ };
597
+ return;
598
+ }
599
+ }
600
+
601
+ response = {
602
+ content: responseBuilder.content,
603
+ stopReason: responseBuilder.stopReason,
604
+ usage: responseBuilder.usage,
605
+ cost: responseBuilder.cost,
606
+ };
607
+
608
+ } else {
609
+ // === TRADITIONAL PATH (COMPLETE) ===
610
+ response = await this.provider.complete({
611
+ model: this.config.model,
612
+ messages: this.sessionManager.getMessagesForLLM(),
613
+ tools: toolDefs,
614
+ systemPrompt,
615
+ maxTokens: 4096,
616
+ });
617
+ }
443
618
  } catch (error) {
444
619
  yield { type: 'error', error: error as Error };
445
620
  return;
446
621
  }
447
622
 
623
+ // Validate response completeness
624
+ if (!response || !response.content) {
625
+ yield {
626
+ type: 'error',
627
+ error: new Error('Provider returned null or undefined response')
628
+ };
629
+ return;
630
+ }
631
+
632
+ // Validate content is not empty (excluding max_tokens case)
633
+ if (response.content.length === 0 && response.stopReason !== 'max_tokens') {
634
+ yield {
635
+ type: 'error',
636
+ error: new Error(
637
+ `Provider returned empty content (stopReason: ${response.stopReason}, ` +
638
+ `usage: ${JSON.stringify(response.usage)})`
639
+ )
640
+ };
641
+ return;
642
+ }
643
+
448
644
  // Process response content
449
645
  const toolCalls: Array<{ id: string; name: string; input: Record<string, unknown> }> = [];
450
646
  let textContent = '';
@@ -452,18 +648,51 @@ export class Agent {
452
648
  for (const content of response.content) {
453
649
  if (content.type === 'text') {
454
650
  textContent += content.text;
455
- yield { type: 'text', text: content.text };
651
+ // Only yield text if not in streaming mode (streaming already yielded chunks)
652
+ if (!useStreaming) {
653
+ yield { type: 'text', text: content.text };
654
+ }
456
655
  } else if (content.type === 'tool_use') {
457
656
  toolCalls.push({ id: content.id, name: content.name, input: content.input });
458
657
  }
459
658
  }
460
659
 
461
660
  // Add assistant message and check if done
462
- this.messages.push({ role: 'assistant', content: response.content });
463
- await this.sessionManager.addMessage({ role: 'assistant', content: response.content });
661
+ try {
662
+ await this.sessionManager.addMessage(
663
+ { role: 'assistant', content: response.content },
664
+ this.getModelInfo()
665
+ );
666
+ } catch (error) {
667
+ yield {
668
+ type: 'error',
669
+ error: new Error(`Failed to save assistant message: ${error instanceof Error ? error.message : String(error)}`)
670
+ };
671
+ return;
672
+ }
464
673
 
465
674
  if (response.stopReason !== 'tool_use' || toolCalls.length === 0) {
466
675
  yield { type: 'done', text: textContent, usage: response.usage, cost: response.cost };
676
+
677
+ // Save completion metadata for UI restoration
678
+ if (response.usage || response.cost) {
679
+ const current = this.sessionManager.getCurrent();
680
+ if (current) {
681
+ if (!current.metadata.completions) {
682
+ current.metadata.completions = [];
683
+ }
684
+ current.metadata.completions.push({
685
+ afterMessageIndex: current.messages.length - 1,
686
+ durationMs: Date.now() - processingStartTime,
687
+ usage: response.usage ? {
688
+ inputTokens: response.usage.inputTokens,
689
+ outputTokens: response.usage.outputTokens,
690
+ } : undefined,
691
+ cost: response.cost,
692
+ });
693
+ }
694
+ }
695
+
467
696
  return;
468
697
  }
469
698
 
@@ -480,23 +709,51 @@ export class Agent {
480
709
  for (const call of toolCalls) {
481
710
  yield { type: 'tool_start', id: call.id, name: call.name, input: call.input };
482
711
 
483
- const allowed = await this.permissions.requestPermission(call.name, call.input);
484
- const result = allowed
485
- ? await this.registry.execute(call.name, call.input, toolContext)
486
- : { success: false, output: '', error: 'Permission denied by user' };
487
-
488
- yield { type: 'tool_result', id: call.id, name: call.name, result };
489
- toolResults.push({
490
- type: 'tool_result',
491
- toolUseId: call.id,
492
- content: result.success ? result.output : (result.error ?? 'Unknown error'),
493
- isError: !result.success,
494
- });
712
+ try {
713
+ // Protect permission check and tool execution
714
+ const allowed = await this.permissions.requestPermission(call.name, call.input);
715
+ const result = allowed
716
+ ? await this.registry.execute(call.name, call.input, toolContext)
717
+ : { success: false, output: '', error: 'Permission denied by user' };
718
+
719
+ yield { type: 'tool_result', id: call.id, name: call.name, result };
720
+ toolResults.push({
721
+ type: 'tool_result',
722
+ toolUseId: call.id,
723
+ content: result.success ? result.output : (result.error ?? 'Unknown error'),
724
+ isError: !result.success,
725
+ });
726
+ } catch (error) {
727
+ // Catch permission check or tool execution errors
728
+ const errorMsg = error instanceof Error ? error.message : String(error);
729
+ const errorResult = {
730
+ success: false,
731
+ output: '',
732
+ error: `Tool execution error: ${errorMsg}`
733
+ };
734
+ yield { type: 'tool_result', id: call.id, name: call.name, result: errorResult };
735
+ toolResults.push({
736
+ type: 'tool_result',
737
+ toolUseId: call.id,
738
+ content: errorMsg,
739
+ isError: true,
740
+ });
741
+ }
495
742
  }
496
743
 
497
744
  // Add tool results as user message
498
- this.messages.push({ role: 'user', content: toolResults });
499
- await this.sessionManager.addMessage({ role: 'user', content: toolResults });
745
+ try {
746
+ await this.sessionManager.addMessage(
747
+ { role: 'user', content: toolResults },
748
+ this.getModelInfo()
749
+ );
750
+ } catch (error) {
751
+ yield {
752
+ type: 'error',
753
+ error: new Error(`Failed to save tool results: ${error instanceof Error ? error.message : String(error)}`)
754
+ };
755
+ return;
756
+ }
500
757
  }
501
758
 
502
759
  yield { type: 'error', error: new Error(`Max turns (${maxTurns}) exceeded`) };
@@ -506,7 +763,6 @@ export class Agent {
506
763
  * Clear conversation history
507
764
  */
508
765
  clearHistory(): void {
509
- this.messages = [];
510
766
  this.sessionManager.clearMessages();
511
767
  }
512
768
 
@@ -515,24 +771,18 @@ export class Agent {
515
771
  * Removes the last assistant message if it contains tool_use without corresponding tool_result
516
772
  */
517
773
  cleanupIncompleteMessages(): void {
518
- if (this.messages.length === 0) return;
774
+ const messages = this.sessionManager.getMessages();
775
+ if (messages.length === 0) return;
519
776
 
520
- const lastMessage = this.messages[this.messages.length - 1];
777
+ const lastMessage = messages[messages.length - 1];
521
778
 
522
779
  // Check if last message is an assistant message with tool_use
523
780
  if (lastMessage.role === 'assistant' && Array.isArray(lastMessage.content)) {
524
781
  const hasToolUse = lastMessage.content.some((c) => c.type === 'tool_use');
525
782
 
526
783
  if (hasToolUse) {
527
- // Remove the incomplete assistant message
528
- this.messages.pop();
529
-
530
- // Also remove from session manager
531
- // Note: SessionManager should have corresponding cleanup method
532
- const messages = this.sessionManager.getMessages();
533
- if (messages.length > 0 && messages[messages.length - 1].role === 'assistant') {
534
- this.sessionManager.removeLastMessage();
535
- }
784
+ // Remove the incomplete assistant message from session manager
785
+ this.sessionManager.removeLastMessage();
536
786
  }
537
787
  }
538
788
  }
@@ -541,6 +791,18 @@ export class Agent {
541
791
  * Get conversation history
542
792
  */
543
793
  getHistory(): Message[] {
544
- return [...this.messages];
794
+ return this.sessionManager.getMessages();
795
+ }
796
+
797
+ /**
798
+ * Get compression statistics
799
+ */
800
+ getCompressionStats(): {
801
+ totalMessages: number;
802
+ activeMessages: number;
803
+ summaryCount: number;
804
+ compressionRatio: number;
805
+ } | null {
806
+ return this.sessionManager.getCompressionStats();
545
807
  }
546
808
  }
@@ -5,6 +5,7 @@
5
5
  import type { PermissionConfig } from '../permissions/types.js';
6
6
  import type { CostEstimate } from '../pricing/types.js';
7
7
  import type { Provider, AuthMethod } from '../providers/types.js';
8
+ import type { CompressionConfig } from '../session/compression/types.js';
8
9
 
9
10
  export interface AgentConfig {
10
11
  provider: Provider;
@@ -17,6 +18,10 @@ export interface AgentConfig {
17
18
  permissions?: Partial<PermissionConfig>;
18
19
  memoryMergeStrategy?: 'fallback' | 'both' | 'gen-only' | 'claude-only';
19
20
  verbose?: boolean;
21
+ /** Compression configuration */
22
+ compression?: Partial<CompressionConfig>;
23
+ /** Enable LLM token streaming for real-time output */
24
+ streaming?: boolean;
20
25
  }
21
26
 
22
27
  // Agent Events
@@ -82,6 +87,17 @@ export interface AgentEventAskUser {
82
87
  }>;
83
88
  }
84
89
 
90
+ export interface AgentEventReasoningDelta {
91
+ type: 'reasoning_delta';
92
+ text: string; // Reasoning content from o1/o3/Gemini 3+ models
93
+ }
94
+
95
+ export interface AgentEventToolInputDelta {
96
+ type: 'tool_input_delta';
97
+ id: string;
98
+ delta: string; // Incremental JSON string fragment
99
+ }
100
+
85
101
  export type AgentEvent =
86
102
  | AgentEventText
87
103
  | AgentEventToolStart
@@ -89,4 +105,6 @@ export type AgentEvent =
89
105
  | AgentEventThinking
90
106
  | AgentEventError
91
107
  | AgentEventDone
92
- | AgentEventAskUser;
108
+ | AgentEventAskUser
109
+ | AgentEventReasoningDelta
110
+ | AgentEventToolInputDelta;