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.
- package/RELEASE_NOTES_v0.4.0.md +140 -0
- package/dist/agent/agent.d.ts +17 -2
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +279 -49
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/types.d.ts +15 -1
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/checkpointing/checkpoint-manager.d.ts +24 -0
- package/dist/checkpointing/checkpoint-manager.d.ts.map +1 -1
- package/dist/checkpointing/checkpoint-manager.js +28 -0
- package/dist/checkpointing/checkpoint-manager.js.map +1 -1
- package/dist/cli/components/App.d.ts +8 -0
- package/dist/cli/components/App.d.ts.map +1 -1
- package/dist/cli/components/App.js +478 -36
- package/dist/cli/components/App.js.map +1 -1
- package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
- package/dist/cli/components/CommandSuggestions.js +2 -0
- package/dist/cli/components/CommandSuggestions.js.map +1 -1
- package/dist/cli/components/Header.d.ts +6 -1
- package/dist/cli/components/Header.d.ts.map +1 -1
- package/dist/cli/components/Header.js +3 -3
- package/dist/cli/components/Header.js.map +1 -1
- package/dist/cli/components/Messages.d.ts.map +1 -1
- package/dist/cli/components/Messages.js +7 -9
- package/dist/cli/components/Messages.js.map +1 -1
- package/dist/cli/index.js +3 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/config/types.d.ts +20 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/input/history-manager.d.ts +78 -0
- package/dist/input/history-manager.d.ts.map +1 -0
- package/dist/input/history-manager.js +224 -0
- package/dist/input/history-manager.js.map +1 -0
- package/dist/input/index.d.ts +6 -0
- package/dist/input/index.d.ts.map +1 -0
- package/dist/input/index.js +5 -0
- package/dist/input/index.js.map +1 -0
- package/dist/prompts/index.js +3 -3
- package/dist/prompts/index.js.map +1 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +33 -2
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/google.d.ts +22 -0
- package/dist/providers/google.d.ts.map +1 -0
- package/dist/providers/google.js +297 -0
- package/dist/providers/google.js.map +1 -0
- package/dist/providers/index.d.ts +4 -4
- package/dist/providers/index.js +11 -11
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +6 -0
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/registry.js +3 -3
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/types.d.ts +30 -4
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/session/compression/engine.d.ts +109 -0
- package/dist/session/compression/engine.d.ts.map +1 -0
- package/dist/session/compression/engine.js +311 -0
- package/dist/session/compression/engine.js.map +1 -0
- package/dist/session/compression/index.d.ts +12 -0
- package/dist/session/compression/index.d.ts.map +1 -0
- package/dist/session/compression/index.js +11 -0
- package/dist/session/compression/index.js.map +1 -0
- package/dist/session/compression/types.d.ts +90 -0
- package/dist/session/compression/types.d.ts.map +1 -0
- package/dist/session/compression/types.js +17 -0
- package/dist/session/compression/types.js.map +1 -0
- package/dist/session/manager.d.ts +64 -3
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +254 -2
- package/dist/session/manager.js.map +1 -1
- package/dist/session/types.d.ts +16 -0
- package/dist/session/types.d.ts.map +1 -1
- package/dist/session/types.js.map +1 -1
- package/docs/README.md +1 -0
- package/docs/diagrams/compression-decision.mmd +30 -0
- package/docs/diagrams/compression-workflow.mmd +54 -0
- package/docs/diagrams/layer1-pruning.mmd +45 -0
- package/docs/diagrams/layer2-compaction.mmd +42 -0
- package/docs/proposals/0007-context-management.md +252 -2
- package/docs/proposals/README.md +4 -3
- package/docs/providers.md +3 -3
- package/docs/session-compression.md +695 -0
- package/examples/agent-demo.ts +23 -1
- package/examples/basic.ts +3 -3
- package/package.json +4 -5
- package/src/agent/agent.ts +314 -52
- package/src/agent/types.ts +19 -1
- package/src/checkpointing/checkpoint-manager.ts +48 -0
- package/src/cli/components/App.tsx +553 -34
- package/src/cli/components/CommandSuggestions.tsx +2 -0
- package/src/cli/components/Header.tsx +16 -1
- package/src/cli/components/Messages.tsx +20 -14
- package/src/cli/index.tsx +3 -2
- package/src/config/types.ts +26 -1
- package/src/index.ts +3 -3
- package/src/input/history-manager.ts +289 -0
- package/src/input/index.ts +6 -0
- package/src/prompts/index.test.ts +2 -1
- package/src/prompts/index.ts +3 -3
- package/src/providers/{gemini.ts → google.ts} +69 -18
- package/src/providers/index.ts +14 -14
- package/src/providers/openai.ts +7 -0
- package/src/providers/registry.ts +3 -3
- package/src/providers/types.ts +33 -3
- package/src/session/compression/engine.ts +406 -0
- package/src/session/compression/index.ts +18 -0
- package/src/session/compression/types.ts +102 -0
- package/src/session/manager.ts +326 -3
- package/src/session/types.ts +21 -0
- package/tests/input-history-manager.test.ts +335 -0
- package/tests/session-checkpoint-persistence.test.ts +198 -0
package/examples/agent-demo.ts
CHANGED
|
@@ -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: '
|
|
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
|
-
|
|
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
|
|
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' | '
|
|
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
|
+
"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": "
|
|
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
|
+
}
|
package/src/agent/agent.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
395
|
-
if (
|
|
396
|
-
|
|
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
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
407
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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
|
-
|
|
463
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
type: 'tool_result',
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
499
|
-
|
|
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
|
-
|
|
774
|
+
const messages = this.sessionManager.getMessages();
|
|
775
|
+
if (messages.length === 0) return;
|
|
519
776
|
|
|
520
|
-
const lastMessage =
|
|
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.
|
|
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
|
|
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
|
}
|
package/src/agent/types.ts
CHANGED
|
@@ -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;
|