gencode-ai 0.3.0 → 0.4.0
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 +3 -4
- 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
|
@@ -25,6 +25,8 @@ export const COMMANDS: Command[] = [
|
|
|
25
25
|
{ name: '/memory', description: 'Show memory files' },
|
|
26
26
|
{ name: '/changes', description: 'List file changes' },
|
|
27
27
|
{ name: '/rewind', description: 'Undo file changes' },
|
|
28
|
+
{ name: '/context', description: 'Show context usage stats' },
|
|
29
|
+
{ name: '/compact', description: 'Manually compact conversation' },
|
|
28
30
|
];
|
|
29
31
|
|
|
30
32
|
interface CommandSuggestionsProps {
|
|
@@ -6,9 +6,14 @@ interface HeaderProps {
|
|
|
6
6
|
provider: string;
|
|
7
7
|
model: string;
|
|
8
8
|
cwd: string;
|
|
9
|
+
contextStats?: {
|
|
10
|
+
activeMessages: number;
|
|
11
|
+
totalMessages: number;
|
|
12
|
+
usagePercent: number;
|
|
13
|
+
};
|
|
9
14
|
}
|
|
10
15
|
|
|
11
|
-
export function Header({ model, cwd }: HeaderProps) {
|
|
16
|
+
export function Header({ model, cwd, contextStats }: HeaderProps) {
|
|
12
17
|
return (
|
|
13
18
|
<Box flexDirection="column" marginTop={1}>
|
|
14
19
|
<BigLogo />
|
|
@@ -16,6 +21,16 @@ export function Header({ model, cwd }: HeaderProps) {
|
|
|
16
21
|
<Text color={colors.textSecondary}>{model}</Text>
|
|
17
22
|
<Text color={colors.textMuted}> · </Text>
|
|
18
23
|
<Text color={colors.textMuted}>{cwd}</Text>
|
|
24
|
+
|
|
25
|
+
{contextStats && contextStats.activeMessages > 0 && (
|
|
26
|
+
<>
|
|
27
|
+
<Text color={colors.textMuted}> · </Text>
|
|
28
|
+
<Text color={colors.textSecondary}>
|
|
29
|
+
Context: {contextStats.activeMessages}/{contextStats.totalMessages} msgs
|
|
30
|
+
</Text>
|
|
31
|
+
<Text color={colors.textMuted}> ({Math.round(contextStats.usagePercent)}%)</Text>
|
|
32
|
+
</>
|
|
33
|
+
)}
|
|
19
34
|
</Box>
|
|
20
35
|
</Box>
|
|
21
36
|
);
|
|
@@ -110,29 +110,35 @@ interface ToolCallProps {
|
|
|
110
110
|
function formatToolInput(name: string, input: Record<string, unknown>): string {
|
|
111
111
|
switch (name) {
|
|
112
112
|
case 'Read':
|
|
113
|
-
return input.file_path as string || '';
|
|
114
113
|
case 'Write':
|
|
115
114
|
case 'Edit':
|
|
116
|
-
return input.file_path as string || '';
|
|
115
|
+
return (input.file_path as string) || '';
|
|
116
|
+
|
|
117
117
|
case 'Glob':
|
|
118
|
-
return input.pattern as string || '';
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
return (input.pattern as string) || '';
|
|
119
|
+
|
|
120
|
+
case 'Grep': {
|
|
121
|
+
const pattern = `"${input.pattern}"`;
|
|
122
|
+
return input.path ? `${pattern} in ${input.path}` : pattern;
|
|
123
|
+
}
|
|
124
|
+
|
|
121
125
|
case 'Bash':
|
|
122
|
-
return truncate(input.command as string || '', 50);
|
|
126
|
+
return truncate((input.command as string) || '', 50);
|
|
127
|
+
|
|
123
128
|
case 'WebFetch':
|
|
124
|
-
return input.url as string || '';
|
|
129
|
+
return (input.url as string) || '';
|
|
130
|
+
|
|
125
131
|
case 'WebSearch':
|
|
126
132
|
return `"${input.query}"` || '';
|
|
133
|
+
|
|
127
134
|
case 'TodoWrite': {
|
|
128
|
-
const todos = input.todos as Array<{ content: string; status: string }> || [];
|
|
135
|
+
const todos = (input.todos as Array<{ content: string; status: string }>) || [];
|
|
129
136
|
return `${todos.length} task${todos.length !== 1 ? 's' : ''}`;
|
|
130
137
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
138
|
+
|
|
139
|
+
case 'AskUserQuestion':
|
|
140
|
+
return truncate(JSON.stringify(input), 60);
|
|
141
|
+
|
|
136
142
|
default:
|
|
137
143
|
return truncate(JSON.stringify(input), 40);
|
|
138
144
|
}
|
|
@@ -368,7 +374,7 @@ export function CompletionMessage({ durationMs, usage, cost }: CompletionMessage
|
|
|
368
374
|
);
|
|
369
375
|
}
|
|
370
376
|
|
|
371
|
-
if (cost) {
|
|
377
|
+
if (cost && cost.totalCost > 0) {
|
|
372
378
|
parts.push(`(~${formatCost(cost.totalCost)})`);
|
|
373
379
|
}
|
|
374
380
|
|
package/src/cli/index.tsx
CHANGED
|
@@ -34,7 +34,7 @@ async function setupProxy(): Promise<void> {
|
|
|
34
34
|
// Configuration
|
|
35
35
|
// ============================================================================
|
|
36
36
|
function detectConfig(settings: Settings, providersConfig: ProvidersConfigManager): AgentConfig {
|
|
37
|
-
let provider: Provider = '
|
|
37
|
+
let provider: Provider = 'google';
|
|
38
38
|
let authMethod: AuthMethod | undefined;
|
|
39
39
|
let model = 'gemini-2.0-flash';
|
|
40
40
|
|
|
@@ -60,7 +60,7 @@ function detectConfig(settings: Settings, providersConfig: ProvidersConfigManage
|
|
|
60
60
|
authMethod = 'api_key';
|
|
61
61
|
model = 'gpt-4o';
|
|
62
62
|
} else if (process.env.GOOGLE_API_KEY) {
|
|
63
|
-
provider = '
|
|
63
|
+
provider = 'google';
|
|
64
64
|
authMethod = 'api_key';
|
|
65
65
|
model = 'gemini-2.0-flash';
|
|
66
66
|
}
|
|
@@ -99,6 +99,7 @@ function detectConfig(settings: Settings, providersConfig: ProvidersConfigManage
|
|
|
99
99
|
model,
|
|
100
100
|
cwd: process.cwd(),
|
|
101
101
|
maxTurns: 20,
|
|
102
|
+
compression: settings.compression,
|
|
102
103
|
};
|
|
103
104
|
}
|
|
104
105
|
|
package/src/config/types.ts
CHANGED
|
@@ -17,7 +17,7 @@ import * as path from 'path';
|
|
|
17
17
|
// Provider Types
|
|
18
18
|
// =============================================================================
|
|
19
19
|
|
|
20
|
-
export type Provider = 'openai' | 'anthropic' | '
|
|
20
|
+
export type Provider = 'openai' | 'anthropic' | 'google';
|
|
21
21
|
export type AuthMethod = 'api_key' | 'vertex' | 'bedrock' | 'azure' | 'oauth';
|
|
22
22
|
|
|
23
23
|
// Legacy type alias for backward compatibility
|
|
@@ -70,6 +70,31 @@ export interface Settings {
|
|
|
70
70
|
// Memory configuration
|
|
71
71
|
memoryMergeStrategy?: 'fallback' | 'both' | 'gen-only' | 'claude-only';
|
|
72
72
|
|
|
73
|
+
// Compression configuration
|
|
74
|
+
compression?: {
|
|
75
|
+
enabled?: boolean;
|
|
76
|
+
enablePruning?: boolean;
|
|
77
|
+
enableCompaction?: boolean;
|
|
78
|
+
pruneMinimum?: number;
|
|
79
|
+
pruneProtect?: number;
|
|
80
|
+
reservedOutputTokens?: number;
|
|
81
|
+
model?: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Input history configuration
|
|
85
|
+
inputHistory?: {
|
|
86
|
+
enabled?: boolean;
|
|
87
|
+
maxSize?: number;
|
|
88
|
+
savePath?: string;
|
|
89
|
+
deduplicateConsecutive?: boolean;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Input behavior configuration
|
|
93
|
+
input?: {
|
|
94
|
+
multilineEnabled?: boolean; // Default: true - Enable Shift+Enter for multi-line input
|
|
95
|
+
ctrlCClear?: boolean; // Default: true - Clear input on Ctrl+C if text present
|
|
96
|
+
};
|
|
97
|
+
|
|
73
98
|
// Managed-only fields (cannot be overridden by lower levels)
|
|
74
99
|
strictKnownMarketplaces?: unknown[];
|
|
75
100
|
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Recode - Multi-LLM Agent SDK
|
|
3
3
|
*
|
|
4
4
|
* A unified SDK for building AI agents with support for
|
|
5
|
-
* OpenAI, Anthropic, and Google
|
|
5
|
+
* OpenAI, Anthropic, and Google models.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// Providers
|
|
@@ -24,13 +24,13 @@ export {
|
|
|
24
24
|
type StopReason,
|
|
25
25
|
type OpenAIConfig,
|
|
26
26
|
type AnthropicConfig,
|
|
27
|
-
type
|
|
27
|
+
type GoogleConfig,
|
|
28
28
|
type ProviderConfig,
|
|
29
29
|
type ProviderName,
|
|
30
30
|
// Providers
|
|
31
31
|
OpenAIProvider,
|
|
32
32
|
AnthropicProvider,
|
|
33
|
-
|
|
33
|
+
GoogleProvider,
|
|
34
34
|
// Factory
|
|
35
35
|
createProvider,
|
|
36
36
|
inferProvider,
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input History Manager
|
|
3
|
+
* Manages command history with persistence, navigation, and deduplication
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { promises as fs } from 'fs';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import { join, dirname } from 'path';
|
|
9
|
+
|
|
10
|
+
export interface HistoryEntry {
|
|
11
|
+
text: string;
|
|
12
|
+
timestamp: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface HistoryConfig {
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
maxSize?: number;
|
|
18
|
+
savePath?: string;
|
|
19
|
+
deduplicateConsecutive?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface HistoryFile {
|
|
23
|
+
entries: HistoryEntry[];
|
|
24
|
+
maxSize: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_CONFIG: Required<HistoryConfig> = {
|
|
28
|
+
enabled: true,
|
|
29
|
+
maxSize: 1000,
|
|
30
|
+
savePath: '~/.gen/input-history.json',
|
|
31
|
+
deduplicateConsecutive: true,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export class InputHistoryManager {
|
|
35
|
+
private entries: HistoryEntry[] = [];
|
|
36
|
+
private currentPosition = -1; // -1 means not navigating
|
|
37
|
+
private config: Required<HistoryConfig>;
|
|
38
|
+
private savePath: string;
|
|
39
|
+
private saveTimeout: NodeJS.Timeout | null = null;
|
|
40
|
+
private isLoaded = false;
|
|
41
|
+
|
|
42
|
+
constructor(config: HistoryConfig = {}) {
|
|
43
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
44
|
+
this.savePath = this.resolveTildePath(this.config.savePath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve ~ to home directory
|
|
49
|
+
*/
|
|
50
|
+
private resolveTildePath(path: string): string {
|
|
51
|
+
return path.startsWith('~/') ? join(homedir(), path.slice(2)) : path;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Load history from disk
|
|
56
|
+
*/
|
|
57
|
+
async load(): Promise<void> {
|
|
58
|
+
if (!this.config.enabled) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const data = await fs.readFile(this.savePath, 'utf-8');
|
|
64
|
+
const historyFile: HistoryFile = JSON.parse(data);
|
|
65
|
+
|
|
66
|
+
// Validate and load entries
|
|
67
|
+
if (Array.isArray(historyFile.entries)) {
|
|
68
|
+
this.entries = historyFile.entries.filter(
|
|
69
|
+
(entry) => entry && typeof entry.text === 'string'
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Prune if maxSize changed
|
|
73
|
+
if (this.entries.length > this.config.maxSize) {
|
|
74
|
+
this.entries = this.entries.slice(-this.config.maxSize);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.isLoaded = true;
|
|
79
|
+
} catch (error: unknown) {
|
|
80
|
+
// File doesn't exist or is corrupt - start with empty history
|
|
81
|
+
if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
|
|
82
|
+
console.error('Failed to load input history:', error.message);
|
|
83
|
+
}
|
|
84
|
+
this.entries = [];
|
|
85
|
+
this.isLoaded = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Save history to disk (async, debounced)
|
|
91
|
+
*/
|
|
92
|
+
async save(): Promise<void> {
|
|
93
|
+
if (!this.config.enabled || !this.isLoaded) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Debounce saves to avoid excessive writes
|
|
98
|
+
if (this.saveTimeout) {
|
|
99
|
+
clearTimeout(this.saveTimeout);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.saveTimeout = setTimeout(async () => {
|
|
103
|
+
try {
|
|
104
|
+
// Ensure directory exists
|
|
105
|
+
const dir = dirname(this.savePath);
|
|
106
|
+
await fs.mkdir(dir, { recursive: true });
|
|
107
|
+
|
|
108
|
+
const historyFile: HistoryFile = {
|
|
109
|
+
entries: this.entries,
|
|
110
|
+
maxSize: this.config.maxSize,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
await fs.writeFile(
|
|
114
|
+
this.savePath,
|
|
115
|
+
JSON.stringify(historyFile, null, 2),
|
|
116
|
+
'utf-8'
|
|
117
|
+
);
|
|
118
|
+
} catch (error: unknown) {
|
|
119
|
+
// Log but don't throw - saving history should not break the app
|
|
120
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
121
|
+
console.error('Failed to save input history:', message);
|
|
122
|
+
}
|
|
123
|
+
}, 100); // 100ms debounce
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Flush pending saves immediately (for app exit)
|
|
128
|
+
*/
|
|
129
|
+
async flush(): Promise<void> {
|
|
130
|
+
if (this.saveTimeout) {
|
|
131
|
+
clearTimeout(this.saveTimeout);
|
|
132
|
+
this.saveTimeout = null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!this.config.enabled || !this.isLoaded) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const dir = dirname(this.savePath);
|
|
141
|
+
await fs.mkdir(dir, { recursive: true });
|
|
142
|
+
|
|
143
|
+
const historyFile: HistoryFile = {
|
|
144
|
+
entries: this.entries,
|
|
145
|
+
maxSize: this.config.maxSize,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
await fs.writeFile(
|
|
149
|
+
this.savePath,
|
|
150
|
+
JSON.stringify(historyFile, null, 2),
|
|
151
|
+
'utf-8'
|
|
152
|
+
);
|
|
153
|
+
} catch (error: unknown) {
|
|
154
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
155
|
+
console.error('Failed to flush input history:', message);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Add a new entry to history
|
|
161
|
+
*/
|
|
162
|
+
add(text: string): void {
|
|
163
|
+
if (!this.config.enabled || !text.trim()) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const trimmedText = text.trim();
|
|
168
|
+
|
|
169
|
+
// Deduplicate consecutive entries
|
|
170
|
+
if (this.config.deduplicateConsecutive && this.entries.length > 0) {
|
|
171
|
+
const lastEntry = this.entries[this.entries.length - 1];
|
|
172
|
+
if (lastEntry.text === trimmedText) {
|
|
173
|
+
return; // Skip duplicate
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Add new entry
|
|
178
|
+
this.entries.push({
|
|
179
|
+
text: trimmedText,
|
|
180
|
+
timestamp: new Date().toISOString(),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Prune old entries if exceeding maxSize
|
|
184
|
+
if (this.entries.length > this.config.maxSize) {
|
|
185
|
+
this.entries = this.entries.slice(-this.config.maxSize);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Reset navigation position
|
|
189
|
+
this.currentPosition = -1;
|
|
190
|
+
|
|
191
|
+
// Save asynchronously
|
|
192
|
+
void this.save();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Navigate to previous entry (older)
|
|
197
|
+
* Returns the entry text or null if at beginning
|
|
198
|
+
*/
|
|
199
|
+
previous(): string | null {
|
|
200
|
+
if (!this.config.enabled || this.entries.length === 0) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// First time navigating - start from end
|
|
205
|
+
if (this.currentPosition === -1) {
|
|
206
|
+
this.currentPosition = this.entries.length - 1;
|
|
207
|
+
return this.entries[this.currentPosition].text;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Already at beginning
|
|
211
|
+
if (this.currentPosition === 0) {
|
|
212
|
+
return this.entries[0].text;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Move to previous entry
|
|
216
|
+
this.currentPosition--;
|
|
217
|
+
return this.entries[this.currentPosition].text;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Navigate to next entry (newer)
|
|
222
|
+
* Returns the entry text or null if at end (original input)
|
|
223
|
+
*/
|
|
224
|
+
next(): string | null {
|
|
225
|
+
if (!this.config.enabled || this.entries.length === 0) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Not navigating or at end
|
|
230
|
+
if (this.currentPosition === -1) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Move to next entry
|
|
235
|
+
this.currentPosition++;
|
|
236
|
+
|
|
237
|
+
// Reached end - return to original input
|
|
238
|
+
if (this.currentPosition >= this.entries.length) {
|
|
239
|
+
this.currentPosition = -1;
|
|
240
|
+
return null; // Signal to restore original input
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return this.entries[this.currentPosition].text;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Reset navigation state (cancel history navigation)
|
|
248
|
+
*/
|
|
249
|
+
reset(): void {
|
|
250
|
+
this.currentPosition = -1;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Check if currently navigating history
|
|
255
|
+
*/
|
|
256
|
+
isNavigating(): boolean {
|
|
257
|
+
return this.currentPosition !== -1;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get current position in history (-1 if not navigating)
|
|
262
|
+
*/
|
|
263
|
+
getPosition(): number {
|
|
264
|
+
return this.currentPosition;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get total number of entries
|
|
269
|
+
*/
|
|
270
|
+
size(): number {
|
|
271
|
+
return this.entries.length;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get all entries (for debugging/testing)
|
|
276
|
+
*/
|
|
277
|
+
getEntries(): readonly HistoryEntry[] {
|
|
278
|
+
return this.entries;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Clear all history
|
|
283
|
+
*/
|
|
284
|
+
clear(): void {
|
|
285
|
+
this.entries = [];
|
|
286
|
+
this.currentPosition = -1;
|
|
287
|
+
void this.save();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -104,7 +104,8 @@ describe('Prompt Loader', () => {
|
|
|
104
104
|
it('should map known providers correctly', () => {
|
|
105
105
|
expect(mapProviderToPromptType('anthropic')).toBe('anthropic');
|
|
106
106
|
expect(mapProviderToPromptType('openai')).toBe('openai');
|
|
107
|
-
|
|
107
|
+
// Google provider uses Gemini models, so it maps to gemini prompt
|
|
108
|
+
expect(mapProviderToPromptType('google')).toBe('gemini');
|
|
108
109
|
});
|
|
109
110
|
|
|
110
111
|
it('should return generic for unknown providers', () => {
|
package/src/prompts/index.ts
CHANGED
|
@@ -83,7 +83,7 @@ export function getProviderForModel(model: string): string | null {
|
|
|
83
83
|
* Handles both "provider" and "provider:authMethod" formats
|
|
84
84
|
*/
|
|
85
85
|
export function mapProviderToPromptType(provider: string): ProviderType {
|
|
86
|
-
// Extract provider prefix (e.g., "
|
|
86
|
+
// Extract provider prefix (e.g., "google:api_key" → "google")
|
|
87
87
|
const providerPrefix = provider.split(':')[0];
|
|
88
88
|
|
|
89
89
|
switch (providerPrefix) {
|
|
@@ -91,8 +91,8 @@ export function mapProviderToPromptType(provider: string): ProviderType {
|
|
|
91
91
|
return 'anthropic';
|
|
92
92
|
case 'openai':
|
|
93
93
|
return 'openai';
|
|
94
|
-
case '
|
|
95
|
-
return 'gemini';
|
|
94
|
+
case 'google':
|
|
95
|
+
return 'gemini'; // Google provider uses Gemini models, so use gemini prompt
|
|
96
96
|
default:
|
|
97
97
|
return 'generic';
|
|
98
98
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Google
|
|
3
|
-
* Supports Gemini 1.5 Pro, Gemini 1.5 Flash, Gemini 2.0, etc.
|
|
2
|
+
* Google Provider Implementation
|
|
3
|
+
* Supports Gemini models: Gemini 1.5 Pro, Gemini 1.5 Flash, Gemini 2.0, etc.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { GoogleGenerativeAI, SchemaType } from '@google/generative-ai';
|
|
@@ -16,28 +16,28 @@ import type {
|
|
|
16
16
|
MessageContent,
|
|
17
17
|
ToolDefinition,
|
|
18
18
|
StopReason,
|
|
19
|
-
|
|
19
|
+
GoogleConfig,
|
|
20
20
|
JSONSchema,
|
|
21
21
|
ModelInfo,
|
|
22
22
|
} from './types.js';
|
|
23
23
|
|
|
24
|
-
export class
|
|
24
|
+
export class GoogleProvider implements LLMProvider {
|
|
25
25
|
static readonly meta: ProviderClassMeta = {
|
|
26
|
-
provider: '
|
|
26
|
+
provider: 'google',
|
|
27
27
|
authMethod: 'api_key',
|
|
28
28
|
envVars: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'],
|
|
29
|
-
displayName: '
|
|
30
|
-
description: '
|
|
29
|
+
displayName: 'Google',
|
|
30
|
+
description: 'Google Generative AI (Gemini models)',
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
readonly name = '
|
|
33
|
+
readonly name = 'google';
|
|
34
34
|
private client: GoogleGenerativeAI;
|
|
35
35
|
private apiKey: string;
|
|
36
36
|
|
|
37
|
-
constructor(config:
|
|
37
|
+
constructor(config: GoogleConfig = {}) {
|
|
38
38
|
const apiKey = config.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY;
|
|
39
39
|
if (!apiKey) {
|
|
40
|
-
throw new Error('
|
|
40
|
+
throw new Error('Google API key is required. Set GOOGLE_API_KEY or GEMINI_API_KEY.');
|
|
41
41
|
}
|
|
42
42
|
this.apiKey = apiKey;
|
|
43
43
|
this.client = new GoogleGenerativeAI(apiKey);
|
|
@@ -95,6 +95,12 @@ export class GeminiProvider implements LLMProvider {
|
|
|
95
95
|
const id = `call_${callIndex++}`;
|
|
96
96
|
// Capture thoughtSignature for Gemini 3+ models
|
|
97
97
|
const partAny = part as { thoughtSignature?: string };
|
|
98
|
+
|
|
99
|
+
// Emit reasoning content if available (Gemini 3+ thinking)
|
|
100
|
+
if (partAny.thoughtSignature) {
|
|
101
|
+
yield { type: 'reasoning', text: partAny.thoughtSignature };
|
|
102
|
+
}
|
|
103
|
+
|
|
98
104
|
functionCalls.push({
|
|
99
105
|
id,
|
|
100
106
|
name: fc.name,
|
|
@@ -125,13 +131,31 @@ export class GeminiProvider implements LLMProvider {
|
|
|
125
131
|
const finalResponse = await result.response;
|
|
126
132
|
const stopReason = this.getStopReason(finalResponse, functionCalls.length > 0);
|
|
127
133
|
|
|
134
|
+
// Debug: Log usage metadata
|
|
135
|
+
if (process.env.DEBUG_TOKENS) {
|
|
136
|
+
console.error('[Google] usageMetadata:', JSON.stringify(finalResponse.usageMetadata, null, 2));
|
|
137
|
+
}
|
|
138
|
+
|
|
128
139
|
const usage = finalResponse.usageMetadata
|
|
129
140
|
? {
|
|
130
141
|
inputTokens: finalResponse.usageMetadata.promptTokenCount ?? 0,
|
|
131
|
-
|
|
142
|
+
// Fix: candidatesTokenCount is unreliable, calculate from total - prompt
|
|
143
|
+
// Ensure outputTokens is never negative
|
|
144
|
+
outputTokens: Math.max(
|
|
145
|
+
0,
|
|
146
|
+
(finalResponse.usageMetadata.totalTokenCount ?? 0) - (finalResponse.usageMetadata.promptTokenCount ?? 0)
|
|
147
|
+
),
|
|
132
148
|
}
|
|
133
149
|
: undefined;
|
|
134
150
|
|
|
151
|
+
// Warn if suspicious token count
|
|
152
|
+
if (usage && usage.outputTokens === 0 && content.length > 0) {
|
|
153
|
+
console.warn(
|
|
154
|
+
'[Google] Warning: usageMetadata shows 0 output tokens but content was returned. ' +
|
|
155
|
+
'This may indicate a Gemini API issue.'
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
135
159
|
const cost = usage ? calculateCost(this.name, options.model, usage) : undefined;
|
|
136
160
|
|
|
137
161
|
yield {
|
|
@@ -276,13 +300,31 @@ export class GeminiProvider implements LLMProvider {
|
|
|
276
300
|
|
|
277
301
|
const hasFunctionCalls = parts.some((p) => 'functionCall' in p);
|
|
278
302
|
|
|
303
|
+
// Debug: Log usage metadata
|
|
304
|
+
if (process.env.DEBUG_TOKENS) {
|
|
305
|
+
console.error('[Google complete] usageMetadata:', JSON.stringify(response.usageMetadata, null, 2));
|
|
306
|
+
}
|
|
307
|
+
|
|
279
308
|
const usage = response.usageMetadata
|
|
280
309
|
? {
|
|
281
310
|
inputTokens: response.usageMetadata.promptTokenCount ?? 0,
|
|
282
|
-
|
|
311
|
+
// Fix: candidatesTokenCount is unreliable, calculate from total - prompt
|
|
312
|
+
// Ensure outputTokens is never negative
|
|
313
|
+
outputTokens: Math.max(
|
|
314
|
+
0,
|
|
315
|
+
(response.usageMetadata.totalTokenCount ?? 0) - (response.usageMetadata.promptTokenCount ?? 0)
|
|
316
|
+
),
|
|
283
317
|
}
|
|
284
318
|
: undefined;
|
|
285
319
|
|
|
320
|
+
// Warn if suspicious token count
|
|
321
|
+
if (usage && usage.outputTokens === 0 && content.length > 0) {
|
|
322
|
+
console.warn(
|
|
323
|
+
'[Google] Warning: usageMetadata shows 0 output tokens but content was returned. ' +
|
|
324
|
+
'This may indicate a Gemini API issue.'
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
286
328
|
const cost = usage ? calculateCost(this.name, model, usage) : undefined;
|
|
287
329
|
|
|
288
330
|
return {
|
|
@@ -299,13 +341,22 @@ export class GeminiProvider implements LLMProvider {
|
|
|
299
341
|
}
|
|
300
342
|
|
|
301
343
|
const finishReason = response.candidates?.[0]?.finishReason;
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
344
|
+
|
|
345
|
+
if (finishReason === 'MAX_TOKENS') {
|
|
346
|
+
return 'max_tokens';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (finishReason === 'STOP') {
|
|
350
|
+
return 'end_turn';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (finishReason === 'SAFETY' || finishReason === 'RECITATION') {
|
|
354
|
+
console.warn(`[Google] Content blocked by ${finishReason} filter`);
|
|
355
|
+
return 'end_turn';
|
|
308
356
|
}
|
|
357
|
+
|
|
358
|
+
console.warn(`[Google] Unknown finishReason: ${finishReason}`);
|
|
359
|
+
return 'end_turn';
|
|
309
360
|
}
|
|
310
361
|
|
|
311
362
|
async listModels(): Promise<ModelInfo[]> {
|