@rcrsr/rill-ext-gemini 0.8.3 → 0.8.5
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/dist/index.d.ts +81 -4
- package/dist/index.js +860 -14
- package/package.json +10 -6
- package/dist/factory.d.ts +0 -27
- package/dist/factory.d.ts.map +0 -1
- package/dist/factory.js +0 -712
- package/dist/factory.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types.d.ts +0 -11
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -6
- package/dist/types.js.map +0 -1
package/dist/factory.js
DELETED
|
@@ -1,712 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extension factory for Gemini API integration.
|
|
3
|
-
* Creates extension instance with config validation and SDK lifecycle management.
|
|
4
|
-
*/
|
|
5
|
-
import { GoogleGenAI, Type, } from '@google/genai';
|
|
6
|
-
import { RuntimeError, emitExtensionEvent, createVector, isVector, isCallable, } from '@rcrsr/rill';
|
|
7
|
-
import { validateApiKey, validateModel, validateTemperature, validateEmbedText, validateEmbedBatch, validateEmbedModel, mapProviderError, executeToolLoop, } from '@rcrsr/rill-ext-llm-shared';
|
|
8
|
-
// ============================================================
|
|
9
|
-
// CONSTANTS
|
|
10
|
-
// ============================================================
|
|
11
|
-
const DEFAULT_MAX_TOKENS = 8192;
|
|
12
|
-
// ============================================================
|
|
13
|
-
// HELPER FUNCTIONS
|
|
14
|
-
// ============================================================
|
|
15
|
-
/**
|
|
16
|
-
* Gemini-specific error detector for mapProviderError.
|
|
17
|
-
* Extracts status code and message using string pattern matching.
|
|
18
|
-
*
|
|
19
|
-
* @param error - Unknown error value
|
|
20
|
-
* @returns Object with status and message if Gemini error, null otherwise
|
|
21
|
-
*/
|
|
22
|
-
const detectGeminiError = (error) => {
|
|
23
|
-
if (error instanceof Error) {
|
|
24
|
-
const message = error.message;
|
|
25
|
-
// Extract status code if present in message
|
|
26
|
-
const statusMatch = message.match(/\((\d{3})\)/);
|
|
27
|
-
if (statusMatch && statusMatch[1]) {
|
|
28
|
-
return {
|
|
29
|
-
status: parseInt(statusMatch[1], 10),
|
|
30
|
-
message,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
return {
|
|
34
|
-
message,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
38
|
-
};
|
|
39
|
-
// ============================================================
|
|
40
|
-
// FACTORY
|
|
41
|
-
// ============================================================
|
|
42
|
-
/**
|
|
43
|
-
* Create Gemini extension instance.
|
|
44
|
-
* Validates configuration and returns host functions with cleanup.
|
|
45
|
-
*
|
|
46
|
-
* @param config - Extension configuration
|
|
47
|
-
* @returns ExtensionResult with message, messages, embed, embed_batch, tool_loop and dispose
|
|
48
|
-
* @throws Error for invalid configuration (EC-1 through EC-4)
|
|
49
|
-
*
|
|
50
|
-
* @example
|
|
51
|
-
* ```typescript
|
|
52
|
-
* const ext = createGeminiExtension({
|
|
53
|
-
* api_key: process.env.GOOGLE_API_KEY,
|
|
54
|
-
* model: 'gemini-2.0-flash',
|
|
55
|
-
* temperature: 0.7
|
|
56
|
-
* });
|
|
57
|
-
* // Use with rill runtime...
|
|
58
|
-
* await ext.dispose();
|
|
59
|
-
* ```
|
|
60
|
-
*/
|
|
61
|
-
export function createGeminiExtension(config) {
|
|
62
|
-
// Validate required fields (§4.1)
|
|
63
|
-
validateApiKey(config.api_key);
|
|
64
|
-
validateModel(config.model);
|
|
65
|
-
validateTemperature(config.temperature);
|
|
66
|
-
// Instantiate SDK client at factory time (§4.1)
|
|
67
|
-
const client = new GoogleGenAI({
|
|
68
|
-
apiKey: config.api_key,
|
|
69
|
-
});
|
|
70
|
-
// Extract config values for use in functions
|
|
71
|
-
const factoryModel = config.model;
|
|
72
|
-
const factoryTemperature = config.temperature;
|
|
73
|
-
const factoryMaxTokens = config.max_tokens ?? DEFAULT_MAX_TOKENS;
|
|
74
|
-
const factorySystem = config.system;
|
|
75
|
-
const factoryEmbedModel = config.embed_model;
|
|
76
|
-
// AbortController for cancelling pending requests (§4.9, IR-11)
|
|
77
|
-
let abortController = new AbortController();
|
|
78
|
-
// Dispose function for cleanup (§4.9)
|
|
79
|
-
const dispose = async () => {
|
|
80
|
-
// AC-28: Idempotent cleanup, try-catch each step
|
|
81
|
-
try {
|
|
82
|
-
// Cancel pending API requests via AbortController (IR-11)
|
|
83
|
-
if (abortController) {
|
|
84
|
-
abortController.abort();
|
|
85
|
-
abortController = undefined;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
90
|
-
console.warn(`Failed to abort Gemini requests: ${message}`);
|
|
91
|
-
}
|
|
92
|
-
try {
|
|
93
|
-
// Cleanup SDK HTTP connections
|
|
94
|
-
// Note: Gemini SDK doesn't expose a close() method, but we include
|
|
95
|
-
// this structure for consistency with extension pattern
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
99
|
-
console.warn(`Failed to cleanup Gemini SDK: ${message}`);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
// Return extension result with implementations
|
|
103
|
-
const result = {
|
|
104
|
-
// IR-4: gemini::message
|
|
105
|
-
message: {
|
|
106
|
-
params: [
|
|
107
|
-
{ name: 'text', type: 'string' },
|
|
108
|
-
{ name: 'options', type: 'dict', defaultValue: {} },
|
|
109
|
-
],
|
|
110
|
-
fn: async (args, ctx) => {
|
|
111
|
-
const startTime = Date.now();
|
|
112
|
-
try {
|
|
113
|
-
// Extract arguments
|
|
114
|
-
const text = args[0];
|
|
115
|
-
const options = (args[1] ?? {});
|
|
116
|
-
// EC-5: Validate text is non-empty
|
|
117
|
-
if (text.trim().length === 0) {
|
|
118
|
-
throw new RuntimeError('RILL-R004', 'prompt text cannot be empty');
|
|
119
|
-
}
|
|
120
|
-
// Extract options
|
|
121
|
-
const system = typeof options['system'] === 'string'
|
|
122
|
-
? options['system']
|
|
123
|
-
: factorySystem;
|
|
124
|
-
const maxTokens = typeof options['max_tokens'] === 'number'
|
|
125
|
-
? options['max_tokens']
|
|
126
|
-
: factoryMaxTokens;
|
|
127
|
-
// Build Gemini API request
|
|
128
|
-
// Gemini uses 'contents' array with role: "user" / role: "model"
|
|
129
|
-
const contents = [
|
|
130
|
-
{
|
|
131
|
-
role: 'user',
|
|
132
|
-
parts: [{ text }],
|
|
133
|
-
},
|
|
134
|
-
];
|
|
135
|
-
// Build config object with optional properties
|
|
136
|
-
const apiConfig = {};
|
|
137
|
-
// Add system instruction if present
|
|
138
|
-
if (system !== undefined) {
|
|
139
|
-
apiConfig.systemInstruction = system;
|
|
140
|
-
}
|
|
141
|
-
// Add max_tokens if present
|
|
142
|
-
if (maxTokens !== undefined) {
|
|
143
|
-
apiConfig.maxOutputTokens = maxTokens;
|
|
144
|
-
}
|
|
145
|
-
// Add temperature if present
|
|
146
|
-
if (factoryTemperature !== undefined) {
|
|
147
|
-
apiConfig.temperature = factoryTemperature;
|
|
148
|
-
}
|
|
149
|
-
// Call Gemini API
|
|
150
|
-
const response = await client.models.generateContent({
|
|
151
|
-
model: factoryModel,
|
|
152
|
-
contents,
|
|
153
|
-
config: apiConfig,
|
|
154
|
-
});
|
|
155
|
-
// Extract text content from response
|
|
156
|
-
const content = response.text ?? '';
|
|
157
|
-
// Build normalized response dict (§3.2)
|
|
158
|
-
const result = {
|
|
159
|
-
content,
|
|
160
|
-
model: factoryModel,
|
|
161
|
-
usage: {
|
|
162
|
-
input: 0, // Gemini API doesn't always provide token counts
|
|
163
|
-
output: 0,
|
|
164
|
-
},
|
|
165
|
-
stop_reason: 'stop',
|
|
166
|
-
id: '', // Gemini API doesn't provide request IDs in the same way
|
|
167
|
-
messages: [
|
|
168
|
-
...(system ? [{ role: 'system', content: system }] : []),
|
|
169
|
-
{ role: 'user', content: text },
|
|
170
|
-
{ role: 'assistant', content },
|
|
171
|
-
],
|
|
172
|
-
};
|
|
173
|
-
// Emit success event (§4.10)
|
|
174
|
-
const duration = Date.now() - startTime;
|
|
175
|
-
emitExtensionEvent(ctx, {
|
|
176
|
-
event: 'gemini:message',
|
|
177
|
-
subsystem: 'extension:gemini',
|
|
178
|
-
duration,
|
|
179
|
-
model: factoryModel,
|
|
180
|
-
usage: result.usage,
|
|
181
|
-
});
|
|
182
|
-
return result;
|
|
183
|
-
}
|
|
184
|
-
catch (error) {
|
|
185
|
-
// Map error and emit failure event
|
|
186
|
-
const duration = Date.now() - startTime;
|
|
187
|
-
const rillError = error instanceof RuntimeError
|
|
188
|
-
? error
|
|
189
|
-
: mapProviderError('Gemini', error, detectGeminiError);
|
|
190
|
-
emitExtensionEvent(ctx, {
|
|
191
|
-
event: 'gemini:error',
|
|
192
|
-
subsystem: 'extension:gemini',
|
|
193
|
-
error: rillError.message,
|
|
194
|
-
duration,
|
|
195
|
-
});
|
|
196
|
-
throw rillError;
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
description: 'Send single message to Gemini API',
|
|
200
|
-
returnType: 'dict',
|
|
201
|
-
},
|
|
202
|
-
// IR-5: gemini::messages
|
|
203
|
-
messages: {
|
|
204
|
-
params: [
|
|
205
|
-
{ name: 'messages', type: 'list' },
|
|
206
|
-
{ name: 'options', type: 'dict', defaultValue: {} },
|
|
207
|
-
],
|
|
208
|
-
fn: async (args, ctx) => {
|
|
209
|
-
const startTime = Date.now();
|
|
210
|
-
try {
|
|
211
|
-
// Extract arguments
|
|
212
|
-
const messages = args[0];
|
|
213
|
-
const options = (args[1] ?? {});
|
|
214
|
-
// AC-23: Empty messages list raises error
|
|
215
|
-
if (messages.length === 0) {
|
|
216
|
-
throw new RuntimeError('RILL-R004', 'messages list cannot be empty');
|
|
217
|
-
}
|
|
218
|
-
// Extract options
|
|
219
|
-
const system = typeof options['system'] === 'string'
|
|
220
|
-
? options['system']
|
|
221
|
-
: factorySystem;
|
|
222
|
-
const maxTokens = typeof options['max_tokens'] === 'number'
|
|
223
|
-
? options['max_tokens']
|
|
224
|
-
: factoryMaxTokens;
|
|
225
|
-
// Build Gemini API contents array
|
|
226
|
-
// Gemini uses role: "user" / role: "model" (not "assistant")
|
|
227
|
-
const contents = [];
|
|
228
|
-
// Validate and transform messages
|
|
229
|
-
for (let i = 0; i < messages.length; i++) {
|
|
230
|
-
const msg = messages[i];
|
|
231
|
-
// EC-10: Missing role raises error
|
|
232
|
-
if (!msg || typeof msg !== 'object' || !('role' in msg)) {
|
|
233
|
-
throw new RuntimeError('RILL-R004', "message missing required 'role' field");
|
|
234
|
-
}
|
|
235
|
-
const role = msg['role'];
|
|
236
|
-
// EC-11: Unknown role value raises error
|
|
237
|
-
if (role !== 'user' && role !== 'assistant' && role !== 'tool') {
|
|
238
|
-
throw new RuntimeError('RILL-R004', `invalid role '${role}'`);
|
|
239
|
-
}
|
|
240
|
-
// EC-12: User message missing content
|
|
241
|
-
if (role === 'user' || role === 'tool') {
|
|
242
|
-
if (!('content' in msg) || typeof msg['content'] !== 'string') {
|
|
243
|
-
throw new RuntimeError('RILL-R004', `${role} message requires 'content'`);
|
|
244
|
-
}
|
|
245
|
-
// Gemini uses "user" for both user and tool messages
|
|
246
|
-
contents.push({
|
|
247
|
-
role: 'user',
|
|
248
|
-
parts: [{ text: msg['content'] }],
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
// EC-13: Assistant missing both content and tool_calls
|
|
252
|
-
else if (role === 'assistant') {
|
|
253
|
-
const hasContent = 'content' in msg && msg['content'];
|
|
254
|
-
const hasToolCalls = 'tool_calls' in msg && msg['tool_calls'];
|
|
255
|
-
if (!hasContent && !hasToolCalls) {
|
|
256
|
-
throw new RuntimeError('RILL-R004', "assistant message requires 'content' or 'tool_calls'");
|
|
257
|
-
}
|
|
258
|
-
// For now, we only support content
|
|
259
|
-
if (hasContent) {
|
|
260
|
-
contents.push({
|
|
261
|
-
role: 'model',
|
|
262
|
-
parts: [{ text: msg['content'] }],
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
// Build config object with optional properties
|
|
268
|
-
const apiConfig = {};
|
|
269
|
-
// Add system instruction if present
|
|
270
|
-
if (system !== undefined) {
|
|
271
|
-
apiConfig.systemInstruction = system;
|
|
272
|
-
}
|
|
273
|
-
// Add max_tokens if present
|
|
274
|
-
if (maxTokens !== undefined) {
|
|
275
|
-
apiConfig.maxOutputTokens = maxTokens;
|
|
276
|
-
}
|
|
277
|
-
// Add temperature if present
|
|
278
|
-
if (factoryTemperature !== undefined) {
|
|
279
|
-
apiConfig.temperature = factoryTemperature;
|
|
280
|
-
}
|
|
281
|
-
// Call Gemini API
|
|
282
|
-
const response = await client.models.generateContent({
|
|
283
|
-
model: factoryModel,
|
|
284
|
-
contents,
|
|
285
|
-
config: apiConfig,
|
|
286
|
-
});
|
|
287
|
-
// Extract text content from response
|
|
288
|
-
const content = response.text ?? '';
|
|
289
|
-
// Build full conversation history (§3.2)
|
|
290
|
-
const fullMessages = [
|
|
291
|
-
...messages.map((m) => {
|
|
292
|
-
const normalized = { role: m['role'] };
|
|
293
|
-
if ('content' in m)
|
|
294
|
-
normalized['content'] = m['content'];
|
|
295
|
-
if ('tool_calls' in m)
|
|
296
|
-
normalized['tool_calls'] = m['tool_calls'];
|
|
297
|
-
return normalized;
|
|
298
|
-
}),
|
|
299
|
-
{ role: 'assistant', content },
|
|
300
|
-
];
|
|
301
|
-
// Build normalized response dict (§3.2)
|
|
302
|
-
const result = {
|
|
303
|
-
content,
|
|
304
|
-
model: factoryModel,
|
|
305
|
-
usage: {
|
|
306
|
-
input: 0, // Gemini API doesn't always provide token counts
|
|
307
|
-
output: 0,
|
|
308
|
-
},
|
|
309
|
-
stop_reason: 'stop',
|
|
310
|
-
id: '', // Gemini API doesn't provide request IDs in the same way
|
|
311
|
-
messages: fullMessages,
|
|
312
|
-
};
|
|
313
|
-
// Emit success event (§4.10)
|
|
314
|
-
const duration = Date.now() - startTime;
|
|
315
|
-
emitExtensionEvent(ctx, {
|
|
316
|
-
event: 'gemini:messages',
|
|
317
|
-
subsystem: 'extension:gemini',
|
|
318
|
-
duration,
|
|
319
|
-
model: factoryModel,
|
|
320
|
-
usage: result.usage,
|
|
321
|
-
});
|
|
322
|
-
return result;
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
// Map error and emit failure event
|
|
326
|
-
const duration = Date.now() - startTime;
|
|
327
|
-
const rillError = error instanceof RuntimeError
|
|
328
|
-
? error
|
|
329
|
-
: mapProviderError('Gemini', error, detectGeminiError);
|
|
330
|
-
emitExtensionEvent(ctx, {
|
|
331
|
-
event: 'gemini:error',
|
|
332
|
-
subsystem: 'extension:gemini',
|
|
333
|
-
error: rillError.message,
|
|
334
|
-
duration,
|
|
335
|
-
});
|
|
336
|
-
throw rillError;
|
|
337
|
-
}
|
|
338
|
-
},
|
|
339
|
-
description: 'Send multi-turn conversation to Gemini API',
|
|
340
|
-
returnType: 'dict',
|
|
341
|
-
},
|
|
342
|
-
// IR-6: gemini::embed
|
|
343
|
-
embed: {
|
|
344
|
-
params: [{ name: 'text', type: 'string' }],
|
|
345
|
-
fn: async (args, ctx) => {
|
|
346
|
-
const startTime = Date.now();
|
|
347
|
-
try {
|
|
348
|
-
// Extract arguments
|
|
349
|
-
const text = args[0];
|
|
350
|
-
// Validate using shared functions
|
|
351
|
-
validateEmbedText(text);
|
|
352
|
-
validateEmbedModel(factoryEmbedModel);
|
|
353
|
-
// Call Gemini embedContent API
|
|
354
|
-
const response = await client.models.embedContent({
|
|
355
|
-
model: factoryEmbedModel,
|
|
356
|
-
contents: [text],
|
|
357
|
-
});
|
|
358
|
-
// Extract embedding data from response
|
|
359
|
-
const embedding = response.embeddings?.[0];
|
|
360
|
-
if (!embedding ||
|
|
361
|
-
!embedding.values ||
|
|
362
|
-
embedding.values.length === 0) {
|
|
363
|
-
throw new RuntimeError('RILL-R004', 'Gemini: empty embedding returned');
|
|
364
|
-
}
|
|
365
|
-
// Convert to Float32Array and create RillVector
|
|
366
|
-
const float32Data = new Float32Array(embedding.values);
|
|
367
|
-
const vector = createVector(float32Data, factoryEmbedModel);
|
|
368
|
-
// Emit success event
|
|
369
|
-
const duration = Date.now() - startTime;
|
|
370
|
-
emitExtensionEvent(ctx, {
|
|
371
|
-
event: 'gemini:embed',
|
|
372
|
-
subsystem: 'extension:gemini',
|
|
373
|
-
duration,
|
|
374
|
-
model: factoryEmbedModel,
|
|
375
|
-
dimensions: float32Data.length,
|
|
376
|
-
});
|
|
377
|
-
return vector;
|
|
378
|
-
}
|
|
379
|
-
catch (error) {
|
|
380
|
-
// Map error and emit failure event
|
|
381
|
-
const duration = Date.now() - startTime;
|
|
382
|
-
const rillError = error instanceof RuntimeError
|
|
383
|
-
? error
|
|
384
|
-
: mapProviderError('Gemini', error, detectGeminiError);
|
|
385
|
-
emitExtensionEvent(ctx, {
|
|
386
|
-
event: 'gemini:error',
|
|
387
|
-
subsystem: 'extension:gemini',
|
|
388
|
-
error: rillError.message,
|
|
389
|
-
duration,
|
|
390
|
-
});
|
|
391
|
-
throw rillError;
|
|
392
|
-
}
|
|
393
|
-
},
|
|
394
|
-
description: 'Generate embedding vector for text',
|
|
395
|
-
returnType: 'vector',
|
|
396
|
-
},
|
|
397
|
-
// IR-7: gemini::embed_batch
|
|
398
|
-
embed_batch: {
|
|
399
|
-
params: [{ name: 'texts', type: 'list' }],
|
|
400
|
-
fn: async (args, ctx) => {
|
|
401
|
-
const startTime = Date.now();
|
|
402
|
-
try {
|
|
403
|
-
// Extract arguments
|
|
404
|
-
const texts = args[0];
|
|
405
|
-
// AC-24: Empty list returns empty list
|
|
406
|
-
if (texts.length === 0) {
|
|
407
|
-
return [];
|
|
408
|
-
}
|
|
409
|
-
// Validate using shared functions
|
|
410
|
-
validateEmbedModel(factoryEmbedModel);
|
|
411
|
-
const stringTexts = validateEmbedBatch(texts);
|
|
412
|
-
// Call Gemini embedContent API with array of texts
|
|
413
|
-
const response = await client.models.embedContent({
|
|
414
|
-
model: factoryEmbedModel,
|
|
415
|
-
contents: stringTexts,
|
|
416
|
-
});
|
|
417
|
-
// Convert embeddings to RillVector list
|
|
418
|
-
const vectors = [];
|
|
419
|
-
if (!response.embeddings || response.embeddings.length === 0) {
|
|
420
|
-
throw new RuntimeError('RILL-R004', 'Gemini: empty embeddings returned');
|
|
421
|
-
}
|
|
422
|
-
for (const embedding of response.embeddings) {
|
|
423
|
-
if (!embedding ||
|
|
424
|
-
!embedding.values ||
|
|
425
|
-
embedding.values.length === 0) {
|
|
426
|
-
throw new RuntimeError('RILL-R004', 'Gemini: empty embedding returned');
|
|
427
|
-
}
|
|
428
|
-
const float32Data = new Float32Array(embedding.values);
|
|
429
|
-
const vector = createVector(float32Data, factoryEmbedModel);
|
|
430
|
-
vectors.push(vector);
|
|
431
|
-
}
|
|
432
|
-
// Emit success event
|
|
433
|
-
const duration = Date.now() - startTime;
|
|
434
|
-
const firstVector = vectors[0];
|
|
435
|
-
const dimensions = firstVector && isVector(firstVector) ? firstVector.data.length : 0;
|
|
436
|
-
emitExtensionEvent(ctx, {
|
|
437
|
-
event: 'gemini:embed_batch',
|
|
438
|
-
subsystem: 'extension:gemini',
|
|
439
|
-
duration,
|
|
440
|
-
model: factoryEmbedModel,
|
|
441
|
-
dimensions,
|
|
442
|
-
count: vectors.length,
|
|
443
|
-
});
|
|
444
|
-
return vectors;
|
|
445
|
-
}
|
|
446
|
-
catch (error) {
|
|
447
|
-
// Map error and emit failure event
|
|
448
|
-
const duration = Date.now() - startTime;
|
|
449
|
-
const rillError = error instanceof RuntimeError
|
|
450
|
-
? error
|
|
451
|
-
: mapProviderError('Gemini', error, detectGeminiError);
|
|
452
|
-
emitExtensionEvent(ctx, {
|
|
453
|
-
event: 'gemini:error',
|
|
454
|
-
subsystem: 'extension:gemini',
|
|
455
|
-
error: rillError.message,
|
|
456
|
-
duration,
|
|
457
|
-
});
|
|
458
|
-
throw rillError;
|
|
459
|
-
}
|
|
460
|
-
},
|
|
461
|
-
description: 'Generate embedding vectors for multiple texts',
|
|
462
|
-
returnType: 'list',
|
|
463
|
-
},
|
|
464
|
-
// IR-8: gemini::tool_loop
|
|
465
|
-
tool_loop: {
|
|
466
|
-
params: [
|
|
467
|
-
{ name: 'prompt', type: 'string' },
|
|
468
|
-
{ name: 'options', type: 'dict', defaultValue: {} },
|
|
469
|
-
],
|
|
470
|
-
fn: async (args, ctx) => {
|
|
471
|
-
const startTime = Date.now();
|
|
472
|
-
try {
|
|
473
|
-
// Extract arguments
|
|
474
|
-
const prompt = args[0];
|
|
475
|
-
const options = (args[1] ?? {});
|
|
476
|
-
// EC-22: Validate prompt is non-empty
|
|
477
|
-
if (prompt.trim().length === 0) {
|
|
478
|
-
throw new RuntimeError('RILL-R004', 'prompt text cannot be empty');
|
|
479
|
-
}
|
|
480
|
-
// EC-23: Validate tools option is present
|
|
481
|
-
if (!('tools' in options) || !Array.isArray(options['tools'])) {
|
|
482
|
-
throw new RuntimeError('RILL-R004', "tool_loop requires 'tools' option");
|
|
483
|
-
}
|
|
484
|
-
const toolDescriptors = options['tools'];
|
|
485
|
-
// Convert tool descriptors array to dict for shared tool loop
|
|
486
|
-
const toolsDict = {};
|
|
487
|
-
for (const descriptor of toolDescriptors) {
|
|
488
|
-
const name = typeof descriptor['name'] === 'string'
|
|
489
|
-
? descriptor['name']
|
|
490
|
-
: null;
|
|
491
|
-
if (!name) {
|
|
492
|
-
throw new RuntimeError('RILL-R004', 'tool descriptor missing name');
|
|
493
|
-
}
|
|
494
|
-
const toolFnValue = descriptor['fn'];
|
|
495
|
-
if (!toolFnValue) {
|
|
496
|
-
throw new RuntimeError('RILL-R004', `tool '${name}' missing fn property`);
|
|
497
|
-
}
|
|
498
|
-
// Validate tool is callable
|
|
499
|
-
if (!isCallable(toolFnValue)) {
|
|
500
|
-
throw new RuntimeError('RILL-R004', `tool '${name}' fn must be callable`);
|
|
501
|
-
}
|
|
502
|
-
// Use the callable as-is (Gemini tests don't enhance with params)
|
|
503
|
-
toolsDict[name] = toolFnValue;
|
|
504
|
-
}
|
|
505
|
-
// Extract options with defaults
|
|
506
|
-
const system = typeof options['system'] === 'string'
|
|
507
|
-
? options['system']
|
|
508
|
-
: factorySystem;
|
|
509
|
-
const maxTokens = typeof options['max_tokens'] === 'number'
|
|
510
|
-
? options['max_tokens']
|
|
511
|
-
: factoryMaxTokens;
|
|
512
|
-
const maxTurns = typeof options['max_turns'] === 'number'
|
|
513
|
-
? options['max_turns']
|
|
514
|
-
: 10;
|
|
515
|
-
const maxErrors = typeof options['max_errors'] === 'number'
|
|
516
|
-
? options['max_errors']
|
|
517
|
-
: 3;
|
|
518
|
-
const initialMessages = Array.isArray(options['messages']) && options['messages'].length > 0
|
|
519
|
-
? options['messages']
|
|
520
|
-
: [];
|
|
521
|
-
// Build initial Gemini contents array
|
|
522
|
-
const contents = [];
|
|
523
|
-
// Add history messages if provided
|
|
524
|
-
for (const msg of initialMessages) {
|
|
525
|
-
if (typeof msg === 'object' &&
|
|
526
|
-
msg !== null &&
|
|
527
|
-
'role' in msg &&
|
|
528
|
-
'content' in msg) {
|
|
529
|
-
const role = msg['role'];
|
|
530
|
-
if (role === 'user') {
|
|
531
|
-
contents.push({
|
|
532
|
-
role: 'user',
|
|
533
|
-
parts: [{ text: msg['content'] }],
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
else if (role === 'assistant') {
|
|
537
|
-
contents.push({
|
|
538
|
-
role: 'model',
|
|
539
|
-
parts: [{ text: msg['content'] }],
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
// Add user prompt
|
|
545
|
-
contents.push({
|
|
546
|
-
role: 'user',
|
|
547
|
-
parts: [{ text: prompt }],
|
|
548
|
-
});
|
|
549
|
-
// Define Gemini-specific callbacks for shared tool loop
|
|
550
|
-
const callbacks = {
|
|
551
|
-
// Build Gemini FunctionDeclaration format from tool definitions
|
|
552
|
-
buildTools: (toolDefs) => {
|
|
553
|
-
return toolDefs.map((def) => {
|
|
554
|
-
// Convert JSON Schema properties to Gemini Schema format
|
|
555
|
-
const properties = {};
|
|
556
|
-
for (const [propName, propDef] of Object.entries(def.input_schema.properties)) {
|
|
557
|
-
const prop = propDef;
|
|
558
|
-
const propType = prop['type'];
|
|
559
|
-
// Map JSON Schema types to Gemini Schema types
|
|
560
|
-
let schemaType = Type.STRING;
|
|
561
|
-
if (propType === 'number')
|
|
562
|
-
schemaType = Type.NUMBER;
|
|
563
|
-
if (propType === 'boolean')
|
|
564
|
-
schemaType = Type.BOOLEAN;
|
|
565
|
-
if (propType === 'integer')
|
|
566
|
-
schemaType = Type.INTEGER;
|
|
567
|
-
if (propType === 'array')
|
|
568
|
-
schemaType = Type.ARRAY;
|
|
569
|
-
if (propType === 'object')
|
|
570
|
-
schemaType = Type.OBJECT;
|
|
571
|
-
properties[propName] = {
|
|
572
|
-
type: schemaType,
|
|
573
|
-
description: prop['description'] ?? '',
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
return {
|
|
577
|
-
name: def.name,
|
|
578
|
-
description: def.description,
|
|
579
|
-
parameters: {
|
|
580
|
-
type: Type.OBJECT,
|
|
581
|
-
properties,
|
|
582
|
-
required: def.input_schema.required,
|
|
583
|
-
},
|
|
584
|
-
};
|
|
585
|
-
});
|
|
586
|
-
},
|
|
587
|
-
// Call Gemini API
|
|
588
|
-
callAPI: async (msgs, tools) => {
|
|
589
|
-
const apiConfig = {
|
|
590
|
-
...(system !== undefined && { systemInstruction: system }),
|
|
591
|
-
...(maxTokens !== undefined && { maxOutputTokens: maxTokens }),
|
|
592
|
-
...(factoryTemperature !== undefined && {
|
|
593
|
-
temperature: factoryTemperature,
|
|
594
|
-
}),
|
|
595
|
-
tools: [
|
|
596
|
-
{ functionDeclarations: tools },
|
|
597
|
-
],
|
|
598
|
-
};
|
|
599
|
-
return await client.models.generateContent({
|
|
600
|
-
model: factoryModel,
|
|
601
|
-
contents: msgs,
|
|
602
|
-
config: apiConfig,
|
|
603
|
-
});
|
|
604
|
-
},
|
|
605
|
-
// Extract tool calls from Gemini response
|
|
606
|
-
extractToolCalls: (response) => {
|
|
607
|
-
if (!response ||
|
|
608
|
-
typeof response !== 'object' ||
|
|
609
|
-
!('functionCalls' in response)) {
|
|
610
|
-
return null;
|
|
611
|
-
}
|
|
612
|
-
const functionCalls = response
|
|
613
|
-
.functionCalls;
|
|
614
|
-
if (!functionCalls || functionCalls.length === 0) {
|
|
615
|
-
return null;
|
|
616
|
-
}
|
|
617
|
-
return functionCalls.map((fc) => {
|
|
618
|
-
const call = fc;
|
|
619
|
-
return {
|
|
620
|
-
id: call.id ?? '',
|
|
621
|
-
name: call.name ?? '',
|
|
622
|
-
input: call.args ?? {},
|
|
623
|
-
};
|
|
624
|
-
});
|
|
625
|
-
},
|
|
626
|
-
// Format tool results into Gemini message format
|
|
627
|
-
formatToolResult: (toolResults) => {
|
|
628
|
-
// Convert tool results to Gemini functionResponse parts
|
|
629
|
-
const functionResponseParts = toolResults.map((tr) => ({
|
|
630
|
-
functionResponse: {
|
|
631
|
-
name: tr.name,
|
|
632
|
-
response: {
|
|
633
|
-
result: tr.error
|
|
634
|
-
? `Error: ${tr.error}`
|
|
635
|
-
: typeof tr.result === 'string'
|
|
636
|
-
? tr.result
|
|
637
|
-
: JSON.stringify(tr.result),
|
|
638
|
-
},
|
|
639
|
-
},
|
|
640
|
-
}));
|
|
641
|
-
// Return user message with function responses
|
|
642
|
-
return {
|
|
643
|
-
role: 'user',
|
|
644
|
-
parts: functionResponseParts,
|
|
645
|
-
};
|
|
646
|
-
},
|
|
647
|
-
};
|
|
648
|
-
// Execute shared tool loop
|
|
649
|
-
const loopResult = await executeToolLoop(contents, toolsDict, maxErrors, callbacks, (event, data) => {
|
|
650
|
-
// Map shared events to Gemini-specific events
|
|
651
|
-
const eventMap = {
|
|
652
|
-
tool_call: 'gemini:tool_call',
|
|
653
|
-
tool_result: 'gemini:tool_result',
|
|
654
|
-
};
|
|
655
|
-
emitExtensionEvent(ctx, {
|
|
656
|
-
event: eventMap[event] || event,
|
|
657
|
-
subsystem: 'extension:gemini',
|
|
658
|
-
...data,
|
|
659
|
-
});
|
|
660
|
-
}, maxTurns);
|
|
661
|
-
// Extract response data
|
|
662
|
-
const response = loopResult.response;
|
|
663
|
-
const content = response && typeof response === 'object' && 'text' in response
|
|
664
|
-
? (response.text ?? '')
|
|
665
|
-
: '';
|
|
666
|
-
const result = {
|
|
667
|
-
content,
|
|
668
|
-
model: factoryModel,
|
|
669
|
-
usage: loopResult.totalTokens,
|
|
670
|
-
stop_reason: response ? 'stop' : 'max_turns',
|
|
671
|
-
turns: loopResult.turns,
|
|
672
|
-
messages: [
|
|
673
|
-
...initialMessages,
|
|
674
|
-
{ role: 'user', content: prompt },
|
|
675
|
-
{ role: 'assistant', content },
|
|
676
|
-
],
|
|
677
|
-
};
|
|
678
|
-
// Emit tool_loop event
|
|
679
|
-
const duration = Date.now() - startTime;
|
|
680
|
-
emitExtensionEvent(ctx, {
|
|
681
|
-
event: 'gemini:tool_loop',
|
|
682
|
-
subsystem: 'extension:gemini',
|
|
683
|
-
turns: result.turns,
|
|
684
|
-
total_duration: duration,
|
|
685
|
-
usage: result.usage,
|
|
686
|
-
});
|
|
687
|
-
return result;
|
|
688
|
-
}
|
|
689
|
-
catch (error) {
|
|
690
|
-
// Map error and emit failure event
|
|
691
|
-
const duration = Date.now() - startTime;
|
|
692
|
-
const rillError = error instanceof RuntimeError
|
|
693
|
-
? error
|
|
694
|
-
: mapProviderError('Gemini', error, detectGeminiError);
|
|
695
|
-
emitExtensionEvent(ctx, {
|
|
696
|
-
event: 'gemini:error',
|
|
697
|
-
subsystem: 'extension:gemini',
|
|
698
|
-
error: rillError.message,
|
|
699
|
-
duration,
|
|
700
|
-
});
|
|
701
|
-
throw rillError;
|
|
702
|
-
}
|
|
703
|
-
},
|
|
704
|
-
description: 'Execute tool-use loop with Gemini API',
|
|
705
|
-
returnType: 'dict',
|
|
706
|
-
},
|
|
707
|
-
};
|
|
708
|
-
// IR-11: Attach dispose lifecycle method
|
|
709
|
-
result.dispose = dispose;
|
|
710
|
-
return result;
|
|
711
|
-
}
|
|
712
|
-
//# sourceMappingURL=factory.js.map
|