1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
|
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
GenerateContentConfig,
GoogleGenAI,
Part,
Chat,
SchemaUnion,
PartListUnion,
Content,
Tool,
} from '@google/genai';
import process from 'node:process';
import { getFolderStructure } from '../utils/getFolderStructure.js';
import { Turn, ServerGeminiStreamEvent } from './turn.js';
import { Config } from '../config/config.js';
import { getCoreSystemPrompt } from './prompts.js';
import { ReadManyFilesTool } from '../tools/read-many-files.js';
import { getResponseText } from '../utils/generateContentResponseUtilities.js';
export class GeminiClient {
private client: GoogleGenAI;
private model: string;
private generateContentConfig: GenerateContentConfig = {
temperature: 0,
topP: 1,
};
private readonly MAX_TURNS = 100;
constructor(private config: Config) {
this.client = new GoogleGenAI({ apiKey: config.getApiKey() });
this.model = config.getModel();
}
private async getEnvironment(): Promise<Part[]> {
const cwd = process.cwd();
const today = new Date().toLocaleDateString(undefined, {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
const platform = process.platform;
const folderStructure = await getFolderStructure(cwd);
const context = `
Okay, just setting up the context for our chat.
Today is ${today}.
My operating system is: ${platform}
I'm currently working in the directory: ${cwd}
${folderStructure}
`.trim();
const initialParts: Part[] = [{ text: context }];
// Add full file context if the flag is set
if (this.config.getFullContext()) {
try {
const readManyFilesTool = this.config
.getToolRegistry()
.getTool('read_many_files') as ReadManyFilesTool;
if (readManyFilesTool) {
// Read all files in the target directory
const result = await readManyFilesTool.execute({
paths: ['**/*'], // Read everything recursively
useDefaultExcludes: true, // Use default excludes
});
if (result.llmContent) {
initialParts.push({
text: `\n--- Full File Context ---\n${result.llmContent}`,
});
} else {
console.warn(
'Full context requested, but read_many_files returned no content.',
);
}
} else {
console.warn(
'Full context requested, but read_many_files tool not found.',
);
}
} catch (error) {
console.error('Error reading full file context:', error);
// Optionally add an error message part to the context
initialParts.push({
text: '\n--- Error reading full file context ---',
});
}
}
return initialParts;
}
async startChat(): Promise<Chat> {
const envParts = await this.getEnvironment();
const toolDeclarations = this.config
.getToolRegistry()
.getFunctionDeclarations();
const tools: Tool[] = [{ functionDeclarations: toolDeclarations }];
try {
return this.client.chats.create({
model: this.model,
config: {
systemInstruction: getCoreSystemPrompt(),
...this.generateContentConfig,
tools,
},
history: [
{
role: 'user',
parts: envParts,
},
{
role: 'model',
parts: [{ text: 'Got it. Thanks for the context!' }],
},
],
});
} catch (error) {
console.error('Error initializing Gemini chat session:', error);
const message = error instanceof Error ? error.message : 'Unknown error.';
throw new Error(`Failed to initialize chat: ${message}`);
}
}
async *sendMessageStream(
chat: Chat,
request: PartListUnion,
signal?: AbortSignal,
): AsyncGenerator<ServerGeminiStreamEvent> {
let turns = 0;
const availableTools = this.config.getToolRegistry().getAllTools();
while (turns < this.MAX_TURNS) {
turns++;
const turn = new Turn(chat, availableTools);
const resultStream = turn.run(request, signal);
for await (const event of resultStream) {
yield event;
}
const confirmations = turn.getConfirmationDetails();
if (confirmations.length > 0) {
break;
}
// What do we do when we have both function responses and confirmations?
const fnResponses = turn.getFunctionResponses();
if (fnResponses.length === 0) {
break; // user's turn to respond
}
request = fnResponses;
}
if (turns >= this.MAX_TURNS) {
console.warn('sendMessageStream: Reached maximum tool call turns limit.');
}
}
async generateJson(
contents: Content[],
schema: SchemaUnion,
): Promise<Record<string, unknown>> {
try {
const result = await this.client.models.generateContent({
model: this.model,
config: {
...this.generateContentConfig,
systemInstruction: getCoreSystemPrompt(),
responseSchema: schema,
responseMimeType: 'application/json',
},
contents,
});
const text = getResponseText(result);
if (!text) {
throw new Error('API returned an empty response.');
}
try {
return JSON.parse(text);
} catch (parseError) {
console.error('Failed to parse JSON response:', text);
throw new Error(
`Failed to parse API response as JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
);
}
} catch (error) {
console.error('Error generating JSON content:', error);
const message =
error instanceof Error ? error.message : 'Unknown API error.';
throw new Error(`Failed to generate JSON content: ${message}`);
}
}
}
|