@rcrsr/rill-ext-gemini 0.8.2 → 0.8.4
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/index.js
CHANGED
|
@@ -1,14 +1,860 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
// src/factory.ts
|
|
2
|
+
import {
|
|
3
|
+
GoogleGenAI,
|
|
4
|
+
Type
|
|
5
|
+
} from "@google/genai";
|
|
6
|
+
import {
|
|
7
|
+
RuntimeError as RuntimeError4,
|
|
8
|
+
emitExtensionEvent,
|
|
9
|
+
createVector,
|
|
10
|
+
isVector,
|
|
11
|
+
isCallable as isCallable2
|
|
12
|
+
} from "@rcrsr/rill";
|
|
13
|
+
|
|
14
|
+
// ../../shared/ext-llm/dist/validation.js
|
|
15
|
+
import { RuntimeError } from "@rcrsr/rill";
|
|
16
|
+
var MIN_TEMPERATURE = 0;
|
|
17
|
+
var MAX_TEMPERATURE = 2;
|
|
18
|
+
function validateApiKey(key) {
|
|
19
|
+
if (key === void 0) {
|
|
20
|
+
throw new Error("api_key is required");
|
|
21
|
+
}
|
|
22
|
+
if (key === "") {
|
|
23
|
+
throw new Error("api_key cannot be empty");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function validateModel(model) {
|
|
27
|
+
if (!model) {
|
|
28
|
+
throw new Error("model is required");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function validateTemperature(temperature) {
|
|
32
|
+
if (temperature === void 0) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (temperature < MIN_TEMPERATURE || temperature > MAX_TEMPERATURE) {
|
|
36
|
+
throw new Error("temperature must be between 0 and 2");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function validateEmbedText(text) {
|
|
40
|
+
if (text === "") {
|
|
41
|
+
throw new RuntimeError("RILL-R001", "embed text cannot be empty");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function validateEmbedBatch(texts) {
|
|
45
|
+
const validated = [];
|
|
46
|
+
for (let i = 0; i < texts.length; i++) {
|
|
47
|
+
const item = texts[i];
|
|
48
|
+
if (typeof item !== "string") {
|
|
49
|
+
throw new RuntimeError("RILL-R001", "embed_batch requires list of strings");
|
|
50
|
+
}
|
|
51
|
+
if (item === "") {
|
|
52
|
+
throw new RuntimeError("RILL-R001", `embed text cannot be empty at index ${i}`);
|
|
53
|
+
}
|
|
54
|
+
validated.push(item);
|
|
55
|
+
}
|
|
56
|
+
return validated;
|
|
57
|
+
}
|
|
58
|
+
function validateEmbedModel(model) {
|
|
59
|
+
if (!model) {
|
|
60
|
+
throw new RuntimeError("RILL-R001", "embed_model not configured");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ../../shared/ext-llm/dist/errors.js
|
|
65
|
+
import { RuntimeError as RuntimeError2 } from "@rcrsr/rill";
|
|
66
|
+
function mapProviderError(providerName, error, detect) {
|
|
67
|
+
const detected = detect(error);
|
|
68
|
+
if (detected !== null) {
|
|
69
|
+
const { status, message } = detected;
|
|
70
|
+
if (status !== void 0) {
|
|
71
|
+
return new RuntimeError2("RILL-R004", `${providerName} API error (HTTP ${status}): ${message}`, void 0, { cause: error });
|
|
72
|
+
}
|
|
73
|
+
return new RuntimeError2("RILL-R004", `${providerName} API error: ${message}`, void 0, { cause: error });
|
|
74
|
+
}
|
|
75
|
+
if (error instanceof Error) {
|
|
76
|
+
return new RuntimeError2("RILL-R004", `${providerName} error: ${error.message}`, void 0, { cause: error });
|
|
77
|
+
}
|
|
78
|
+
return new RuntimeError2("RILL-R004", `${providerName} error: Unknown error`, void 0, { cause: error });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ../../shared/ext-llm/dist/tool-loop.js
|
|
82
|
+
import { isCallable, isDict, RuntimeError as RuntimeError3 } from "@rcrsr/rill";
|
|
83
|
+
async function executeToolCall(toolName, toolInput, tools, context) {
|
|
84
|
+
if (!isDict(tools)) {
|
|
85
|
+
throw new RuntimeError3("RILL-R004", "tools must be a dict mapping tool names to functions");
|
|
86
|
+
}
|
|
87
|
+
const toolsDict = tools;
|
|
88
|
+
const toolFn = toolsDict[toolName];
|
|
89
|
+
if (toolFn === void 0 || toolFn === null) {
|
|
90
|
+
throw new RuntimeError3("RILL-R004", `Unknown tool: ${toolName}`);
|
|
91
|
+
}
|
|
92
|
+
if (!isCallable(toolFn)) {
|
|
93
|
+
throw new RuntimeError3("RILL-R004", `Invalid tool input for ${toolName}: tool must be callable`);
|
|
94
|
+
}
|
|
95
|
+
if (typeof toolInput !== "object" || toolInput === null) {
|
|
96
|
+
throw new RuntimeError3("RILL-R004", `Invalid tool input for ${toolName}: input must be an object`);
|
|
97
|
+
}
|
|
98
|
+
const callable = toolFn;
|
|
99
|
+
if (callable.kind !== "runtime" && callable.kind !== "application") {
|
|
100
|
+
throw new RuntimeError3("RILL-R004", `Invalid tool input for ${toolName}: tool must be application or runtime callable`);
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
let args;
|
|
104
|
+
if (callable.kind === "application" && callable.params) {
|
|
105
|
+
const params = callable.params;
|
|
106
|
+
const inputDict = toolInput;
|
|
107
|
+
args = params.map((param) => {
|
|
108
|
+
const value = inputDict[param.name];
|
|
109
|
+
return value !== void 0 ? value : void 0;
|
|
110
|
+
});
|
|
111
|
+
} else {
|
|
112
|
+
args = [toolInput];
|
|
113
|
+
}
|
|
114
|
+
const ctx = context ?? {
|
|
115
|
+
parent: void 0,
|
|
116
|
+
variables: /* @__PURE__ */ new Map(),
|
|
117
|
+
pipeValue: null
|
|
118
|
+
};
|
|
119
|
+
const result = callable.fn(args, ctx);
|
|
120
|
+
return result instanceof Promise ? await result : result;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error instanceof RuntimeError3) {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
126
|
+
throw new RuntimeError3("RILL-R004", `Invalid tool input for ${toolName}: ${message}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent, maxTurns = 10, context) {
|
|
130
|
+
if (tools === void 0) {
|
|
131
|
+
throw new RuntimeError3("RILL-R004", "tools parameter is required");
|
|
132
|
+
}
|
|
133
|
+
if (!isDict(tools)) {
|
|
134
|
+
throw new RuntimeError3("RILL-R004", "tools must be a dict mapping tool names to functions");
|
|
135
|
+
}
|
|
136
|
+
const toolsDict = tools;
|
|
137
|
+
const toolDescriptors = Object.entries(toolsDict).map(([name, fn]) => {
|
|
138
|
+
const fnValue = fn;
|
|
139
|
+
if (!isCallable(fnValue)) {
|
|
140
|
+
throw new RuntimeError3("RILL-R004", `tool '${name}' must be callable function`);
|
|
141
|
+
}
|
|
142
|
+
const callable = fnValue;
|
|
143
|
+
const description = callable.kind === "application" && callable.description ? callable.description : "";
|
|
144
|
+
const properties = {};
|
|
145
|
+
const required = [];
|
|
146
|
+
if (callable.kind === "application" && callable.params) {
|
|
147
|
+
for (const param of callable.params) {
|
|
148
|
+
let jsonSchemaType;
|
|
149
|
+
switch (param.typeName) {
|
|
150
|
+
case "string":
|
|
151
|
+
jsonSchemaType = "string";
|
|
152
|
+
break;
|
|
153
|
+
case "number":
|
|
154
|
+
jsonSchemaType = "number";
|
|
155
|
+
break;
|
|
156
|
+
case "bool":
|
|
157
|
+
jsonSchemaType = "boolean";
|
|
158
|
+
break;
|
|
159
|
+
case "list":
|
|
160
|
+
jsonSchemaType = "array";
|
|
161
|
+
break;
|
|
162
|
+
case "dict":
|
|
163
|
+
case "vector":
|
|
164
|
+
jsonSchemaType = "object";
|
|
165
|
+
break;
|
|
166
|
+
case null:
|
|
167
|
+
jsonSchemaType = "string";
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
const property = {
|
|
171
|
+
type: jsonSchemaType
|
|
172
|
+
};
|
|
173
|
+
if (param.description) {
|
|
174
|
+
property["description"] = param.description;
|
|
175
|
+
}
|
|
176
|
+
properties[param.name] = property;
|
|
177
|
+
if (param.defaultValue === null) {
|
|
178
|
+
required.push(param.name);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
name,
|
|
184
|
+
description,
|
|
185
|
+
input_schema: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties,
|
|
188
|
+
required
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
const providerTools = callbacks.buildTools(toolDescriptors);
|
|
193
|
+
let consecutiveErrors = 0;
|
|
194
|
+
let totalInputTokens = 0;
|
|
195
|
+
let totalOutputTokens = 0;
|
|
196
|
+
const executedToolCalls = [];
|
|
197
|
+
let currentMessages = [...messages];
|
|
198
|
+
let turnCount = 0;
|
|
199
|
+
while (turnCount < maxTurns) {
|
|
200
|
+
turnCount++;
|
|
201
|
+
let response;
|
|
202
|
+
try {
|
|
203
|
+
response = await callbacks.callAPI(currentMessages, providerTools);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
206
|
+
throw new RuntimeError3("RILL-R004", `Provider API error: ${message}`, void 0, { cause: error });
|
|
207
|
+
}
|
|
208
|
+
if (typeof response === "object" && response !== null && "usage" in response) {
|
|
209
|
+
const usage = response["usage"];
|
|
210
|
+
if (typeof usage === "object" && usage !== null) {
|
|
211
|
+
const usageRecord = usage;
|
|
212
|
+
const inputTokens = typeof usageRecord["input_tokens"] === "number" ? usageRecord["input_tokens"] : 0;
|
|
213
|
+
const outputTokens = typeof usageRecord["output_tokens"] === "number" ? usageRecord["output_tokens"] : 0;
|
|
214
|
+
totalInputTokens += inputTokens;
|
|
215
|
+
totalOutputTokens += outputTokens;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const toolCalls = callbacks.extractToolCalls(response);
|
|
219
|
+
if (toolCalls === null || toolCalls.length === 0) {
|
|
220
|
+
return {
|
|
221
|
+
response,
|
|
222
|
+
toolCalls: executedToolCalls,
|
|
223
|
+
totalTokens: { input: totalInputTokens, output: totalOutputTokens },
|
|
224
|
+
turns: turnCount
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const toolResults = [];
|
|
228
|
+
for (const toolCall of toolCalls) {
|
|
229
|
+
const { id, name, input } = toolCall;
|
|
230
|
+
emitEvent("tool_call", { tool_name: name, args: input });
|
|
231
|
+
const toolStartTime = Date.now();
|
|
232
|
+
try {
|
|
233
|
+
const result = await executeToolCall(name, input, tools, context);
|
|
234
|
+
const duration = Date.now() - toolStartTime;
|
|
235
|
+
toolResults.push({ id, name, result });
|
|
236
|
+
executedToolCalls.push({ name, result });
|
|
237
|
+
consecutiveErrors = 0;
|
|
238
|
+
emitEvent("tool_result", { tool_name: name, duration });
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const duration = Date.now() - toolStartTime;
|
|
241
|
+
consecutiveErrors++;
|
|
242
|
+
let originalError;
|
|
243
|
+
if (error instanceof RuntimeError3) {
|
|
244
|
+
const prefix = `Invalid tool input for ${name}: `;
|
|
245
|
+
if (error.message.startsWith(prefix)) {
|
|
246
|
+
originalError = error.message.slice(prefix.length);
|
|
247
|
+
} else {
|
|
248
|
+
originalError = error.message;
|
|
249
|
+
}
|
|
250
|
+
} else if (error instanceof Error) {
|
|
251
|
+
originalError = error.message;
|
|
252
|
+
} else {
|
|
253
|
+
originalError = "Unknown error";
|
|
254
|
+
}
|
|
255
|
+
const errorResult = originalError;
|
|
256
|
+
toolResults.push({
|
|
257
|
+
id,
|
|
258
|
+
name,
|
|
259
|
+
result: errorResult,
|
|
260
|
+
error: originalError
|
|
261
|
+
});
|
|
262
|
+
emitEvent("tool_result", {
|
|
263
|
+
tool_name: name,
|
|
264
|
+
error: originalError,
|
|
265
|
+
duration
|
|
266
|
+
});
|
|
267
|
+
if (consecutiveErrors >= maxErrors) {
|
|
268
|
+
throw new RuntimeError3("RILL-R004", `Tool execution failed: ${maxErrors} consecutive errors`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const toolResultMessage = callbacks.formatToolResult(toolResults);
|
|
273
|
+
currentMessages.push(toolResultMessage);
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
response: null,
|
|
277
|
+
toolCalls: executedToolCalls,
|
|
278
|
+
totalTokens: { input: totalInputTokens, output: totalOutputTokens },
|
|
279
|
+
turns: turnCount
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/factory.ts
|
|
284
|
+
var DEFAULT_MAX_TOKENS = 8192;
|
|
285
|
+
var detectGeminiError = (error) => {
|
|
286
|
+
if (error instanceof Error) {
|
|
287
|
+
const message = error.message;
|
|
288
|
+
const statusMatch = message.match(/\((\d{3})\)/);
|
|
289
|
+
if (statusMatch && statusMatch[1]) {
|
|
290
|
+
return {
|
|
291
|
+
status: parseInt(statusMatch[1], 10),
|
|
292
|
+
message
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
message
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
};
|
|
301
|
+
function createGeminiExtension(config) {
|
|
302
|
+
validateApiKey(config.api_key);
|
|
303
|
+
validateModel(config.model);
|
|
304
|
+
validateTemperature(config.temperature);
|
|
305
|
+
const client = new GoogleGenAI({
|
|
306
|
+
apiKey: config.api_key
|
|
307
|
+
});
|
|
308
|
+
const factoryModel = config.model;
|
|
309
|
+
const factoryTemperature = config.temperature;
|
|
310
|
+
const factoryMaxTokens = config.max_tokens ?? DEFAULT_MAX_TOKENS;
|
|
311
|
+
const factorySystem = config.system;
|
|
312
|
+
const factoryEmbedModel = config.embed_model;
|
|
313
|
+
let abortController = new AbortController();
|
|
314
|
+
const dispose = async () => {
|
|
315
|
+
try {
|
|
316
|
+
if (abortController) {
|
|
317
|
+
abortController.abort();
|
|
318
|
+
abortController = void 0;
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
322
|
+
console.warn(`Failed to abort Gemini requests: ${message}`);
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
} catch (error) {
|
|
326
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
327
|
+
console.warn(`Failed to cleanup Gemini SDK: ${message}`);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
const result = {
|
|
331
|
+
// IR-4: gemini::message
|
|
332
|
+
message: {
|
|
333
|
+
params: [
|
|
334
|
+
{ name: "text", type: "string" },
|
|
335
|
+
{ name: "options", type: "dict", defaultValue: {} }
|
|
336
|
+
],
|
|
337
|
+
fn: async (args, ctx) => {
|
|
338
|
+
const startTime = Date.now();
|
|
339
|
+
try {
|
|
340
|
+
const text = args[0];
|
|
341
|
+
const options = args[1] ?? {};
|
|
342
|
+
if (text.trim().length === 0) {
|
|
343
|
+
throw new RuntimeError4("RILL-R004", "prompt text cannot be empty");
|
|
344
|
+
}
|
|
345
|
+
const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
|
|
346
|
+
const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
|
|
347
|
+
const contents = [
|
|
348
|
+
{
|
|
349
|
+
role: "user",
|
|
350
|
+
parts: [{ text }]
|
|
351
|
+
}
|
|
352
|
+
];
|
|
353
|
+
const apiConfig = {};
|
|
354
|
+
if (system !== void 0) {
|
|
355
|
+
apiConfig.systemInstruction = system;
|
|
356
|
+
}
|
|
357
|
+
if (maxTokens !== void 0) {
|
|
358
|
+
apiConfig.maxOutputTokens = maxTokens;
|
|
359
|
+
}
|
|
360
|
+
if (factoryTemperature !== void 0) {
|
|
361
|
+
apiConfig.temperature = factoryTemperature;
|
|
362
|
+
}
|
|
363
|
+
const response = await client.models.generateContent({
|
|
364
|
+
model: factoryModel,
|
|
365
|
+
contents,
|
|
366
|
+
config: apiConfig
|
|
367
|
+
});
|
|
368
|
+
const content = response.text ?? "";
|
|
369
|
+
const result2 = {
|
|
370
|
+
content,
|
|
371
|
+
model: factoryModel,
|
|
372
|
+
usage: {
|
|
373
|
+
input: 0,
|
|
374
|
+
// Gemini API doesn't always provide token counts
|
|
375
|
+
output: 0
|
|
376
|
+
},
|
|
377
|
+
stop_reason: "stop",
|
|
378
|
+
id: "",
|
|
379
|
+
// Gemini API doesn't provide request IDs in the same way
|
|
380
|
+
messages: [
|
|
381
|
+
...system ? [{ role: "system", content: system }] : [],
|
|
382
|
+
{ role: "user", content: text },
|
|
383
|
+
{ role: "assistant", content }
|
|
384
|
+
]
|
|
385
|
+
};
|
|
386
|
+
const duration = Date.now() - startTime;
|
|
387
|
+
emitExtensionEvent(ctx, {
|
|
388
|
+
event: "gemini:message",
|
|
389
|
+
subsystem: "extension:gemini",
|
|
390
|
+
duration,
|
|
391
|
+
model: factoryModel,
|
|
392
|
+
usage: result2.usage
|
|
393
|
+
});
|
|
394
|
+
return result2;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
const duration = Date.now() - startTime;
|
|
397
|
+
const rillError = error instanceof RuntimeError4 ? error : mapProviderError("Gemini", error, detectGeminiError);
|
|
398
|
+
emitExtensionEvent(ctx, {
|
|
399
|
+
event: "gemini:error",
|
|
400
|
+
subsystem: "extension:gemini",
|
|
401
|
+
error: rillError.message,
|
|
402
|
+
duration
|
|
403
|
+
});
|
|
404
|
+
throw rillError;
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
description: "Send single message to Gemini API",
|
|
408
|
+
returnType: "dict"
|
|
409
|
+
},
|
|
410
|
+
// IR-5: gemini::messages
|
|
411
|
+
messages: {
|
|
412
|
+
params: [
|
|
413
|
+
{ name: "messages", type: "list" },
|
|
414
|
+
{ name: "options", type: "dict", defaultValue: {} }
|
|
415
|
+
],
|
|
416
|
+
fn: async (args, ctx) => {
|
|
417
|
+
const startTime = Date.now();
|
|
418
|
+
try {
|
|
419
|
+
const messages = args[0];
|
|
420
|
+
const options = args[1] ?? {};
|
|
421
|
+
if (messages.length === 0) {
|
|
422
|
+
throw new RuntimeError4(
|
|
423
|
+
"RILL-R004",
|
|
424
|
+
"messages list cannot be empty"
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
|
|
428
|
+
const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
|
|
429
|
+
const contents = [];
|
|
430
|
+
for (let i = 0; i < messages.length; i++) {
|
|
431
|
+
const msg = messages[i];
|
|
432
|
+
if (!msg || typeof msg !== "object" || !("role" in msg)) {
|
|
433
|
+
throw new RuntimeError4(
|
|
434
|
+
"RILL-R004",
|
|
435
|
+
"message missing required 'role' field"
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
const role = msg["role"];
|
|
439
|
+
if (role !== "user" && role !== "assistant" && role !== "tool") {
|
|
440
|
+
throw new RuntimeError4("RILL-R004", `invalid role '${role}'`);
|
|
441
|
+
}
|
|
442
|
+
if (role === "user" || role === "tool") {
|
|
443
|
+
if (!("content" in msg) || typeof msg["content"] !== "string") {
|
|
444
|
+
throw new RuntimeError4(
|
|
445
|
+
"RILL-R004",
|
|
446
|
+
`${role} message requires 'content'`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
contents.push({
|
|
450
|
+
role: "user",
|
|
451
|
+
parts: [{ text: msg["content"] }]
|
|
452
|
+
});
|
|
453
|
+
} else if (role === "assistant") {
|
|
454
|
+
const hasContent = "content" in msg && msg["content"];
|
|
455
|
+
const hasToolCalls = "tool_calls" in msg && msg["tool_calls"];
|
|
456
|
+
if (!hasContent && !hasToolCalls) {
|
|
457
|
+
throw new RuntimeError4(
|
|
458
|
+
"RILL-R004",
|
|
459
|
+
"assistant message requires 'content' or 'tool_calls'"
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (hasContent) {
|
|
463
|
+
contents.push({
|
|
464
|
+
role: "model",
|
|
465
|
+
parts: [{ text: msg["content"] }]
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const apiConfig = {};
|
|
471
|
+
if (system !== void 0) {
|
|
472
|
+
apiConfig.systemInstruction = system;
|
|
473
|
+
}
|
|
474
|
+
if (maxTokens !== void 0) {
|
|
475
|
+
apiConfig.maxOutputTokens = maxTokens;
|
|
476
|
+
}
|
|
477
|
+
if (factoryTemperature !== void 0) {
|
|
478
|
+
apiConfig.temperature = factoryTemperature;
|
|
479
|
+
}
|
|
480
|
+
const response = await client.models.generateContent({
|
|
481
|
+
model: factoryModel,
|
|
482
|
+
contents,
|
|
483
|
+
config: apiConfig
|
|
484
|
+
});
|
|
485
|
+
const content = response.text ?? "";
|
|
486
|
+
const fullMessages = [
|
|
487
|
+
...messages.map((m) => {
|
|
488
|
+
const normalized = { role: m["role"] };
|
|
489
|
+
if ("content" in m) normalized["content"] = m["content"];
|
|
490
|
+
if ("tool_calls" in m) normalized["tool_calls"] = m["tool_calls"];
|
|
491
|
+
return normalized;
|
|
492
|
+
}),
|
|
493
|
+
{ role: "assistant", content }
|
|
494
|
+
];
|
|
495
|
+
const result2 = {
|
|
496
|
+
content,
|
|
497
|
+
model: factoryModel,
|
|
498
|
+
usage: {
|
|
499
|
+
input: 0,
|
|
500
|
+
// Gemini API doesn't always provide token counts
|
|
501
|
+
output: 0
|
|
502
|
+
},
|
|
503
|
+
stop_reason: "stop",
|
|
504
|
+
id: "",
|
|
505
|
+
// Gemini API doesn't provide request IDs in the same way
|
|
506
|
+
messages: fullMessages
|
|
507
|
+
};
|
|
508
|
+
const duration = Date.now() - startTime;
|
|
509
|
+
emitExtensionEvent(ctx, {
|
|
510
|
+
event: "gemini:messages",
|
|
511
|
+
subsystem: "extension:gemini",
|
|
512
|
+
duration,
|
|
513
|
+
model: factoryModel,
|
|
514
|
+
usage: result2.usage
|
|
515
|
+
});
|
|
516
|
+
return result2;
|
|
517
|
+
} catch (error) {
|
|
518
|
+
const duration = Date.now() - startTime;
|
|
519
|
+
const rillError = error instanceof RuntimeError4 ? error : mapProviderError("Gemini", error, detectGeminiError);
|
|
520
|
+
emitExtensionEvent(ctx, {
|
|
521
|
+
event: "gemini:error",
|
|
522
|
+
subsystem: "extension:gemini",
|
|
523
|
+
error: rillError.message,
|
|
524
|
+
duration
|
|
525
|
+
});
|
|
526
|
+
throw rillError;
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
description: "Send multi-turn conversation to Gemini API",
|
|
530
|
+
returnType: "dict"
|
|
531
|
+
},
|
|
532
|
+
// IR-6: gemini::embed
|
|
533
|
+
embed: {
|
|
534
|
+
params: [{ name: "text", type: "string" }],
|
|
535
|
+
fn: async (args, ctx) => {
|
|
536
|
+
const startTime = Date.now();
|
|
537
|
+
try {
|
|
538
|
+
const text = args[0];
|
|
539
|
+
validateEmbedText(text);
|
|
540
|
+
validateEmbedModel(factoryEmbedModel);
|
|
541
|
+
const response = await client.models.embedContent({
|
|
542
|
+
model: factoryEmbedModel,
|
|
543
|
+
contents: [text]
|
|
544
|
+
});
|
|
545
|
+
const embedding = response.embeddings?.[0];
|
|
546
|
+
if (!embedding || !embedding.values || embedding.values.length === 0) {
|
|
547
|
+
throw new RuntimeError4(
|
|
548
|
+
"RILL-R004",
|
|
549
|
+
"Gemini: empty embedding returned"
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
const float32Data = new Float32Array(embedding.values);
|
|
553
|
+
const vector = createVector(float32Data, factoryEmbedModel);
|
|
554
|
+
const duration = Date.now() - startTime;
|
|
555
|
+
emitExtensionEvent(ctx, {
|
|
556
|
+
event: "gemini:embed",
|
|
557
|
+
subsystem: "extension:gemini",
|
|
558
|
+
duration,
|
|
559
|
+
model: factoryEmbedModel,
|
|
560
|
+
dimensions: float32Data.length
|
|
561
|
+
});
|
|
562
|
+
return vector;
|
|
563
|
+
} catch (error) {
|
|
564
|
+
const duration = Date.now() - startTime;
|
|
565
|
+
const rillError = error instanceof RuntimeError4 ? error : mapProviderError("Gemini", error, detectGeminiError);
|
|
566
|
+
emitExtensionEvent(ctx, {
|
|
567
|
+
event: "gemini:error",
|
|
568
|
+
subsystem: "extension:gemini",
|
|
569
|
+
error: rillError.message,
|
|
570
|
+
duration
|
|
571
|
+
});
|
|
572
|
+
throw rillError;
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
description: "Generate embedding vector for text",
|
|
576
|
+
returnType: "vector"
|
|
577
|
+
},
|
|
578
|
+
// IR-7: gemini::embed_batch
|
|
579
|
+
embed_batch: {
|
|
580
|
+
params: [{ name: "texts", type: "list" }],
|
|
581
|
+
fn: async (args, ctx) => {
|
|
582
|
+
const startTime = Date.now();
|
|
583
|
+
try {
|
|
584
|
+
const texts = args[0];
|
|
585
|
+
if (texts.length === 0) {
|
|
586
|
+
return [];
|
|
587
|
+
}
|
|
588
|
+
validateEmbedModel(factoryEmbedModel);
|
|
589
|
+
const stringTexts = validateEmbedBatch(texts);
|
|
590
|
+
const response = await client.models.embedContent({
|
|
591
|
+
model: factoryEmbedModel,
|
|
592
|
+
contents: stringTexts
|
|
593
|
+
});
|
|
594
|
+
const vectors = [];
|
|
595
|
+
if (!response.embeddings || response.embeddings.length === 0) {
|
|
596
|
+
throw new RuntimeError4(
|
|
597
|
+
"RILL-R004",
|
|
598
|
+
"Gemini: empty embeddings returned"
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
for (const embedding of response.embeddings) {
|
|
602
|
+
if (!embedding || !embedding.values || embedding.values.length === 0) {
|
|
603
|
+
throw new RuntimeError4(
|
|
604
|
+
"RILL-R004",
|
|
605
|
+
"Gemini: empty embedding returned"
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
const float32Data = new Float32Array(embedding.values);
|
|
609
|
+
const vector = createVector(float32Data, factoryEmbedModel);
|
|
610
|
+
vectors.push(vector);
|
|
611
|
+
}
|
|
612
|
+
const duration = Date.now() - startTime;
|
|
613
|
+
const firstVector = vectors[0];
|
|
614
|
+
const dimensions = firstVector && isVector(firstVector) ? firstVector.data.length : 0;
|
|
615
|
+
emitExtensionEvent(ctx, {
|
|
616
|
+
event: "gemini:embed_batch",
|
|
617
|
+
subsystem: "extension:gemini",
|
|
618
|
+
duration,
|
|
619
|
+
model: factoryEmbedModel,
|
|
620
|
+
dimensions,
|
|
621
|
+
count: vectors.length
|
|
622
|
+
});
|
|
623
|
+
return vectors;
|
|
624
|
+
} catch (error) {
|
|
625
|
+
const duration = Date.now() - startTime;
|
|
626
|
+
const rillError = error instanceof RuntimeError4 ? error : mapProviderError("Gemini", error, detectGeminiError);
|
|
627
|
+
emitExtensionEvent(ctx, {
|
|
628
|
+
event: "gemini:error",
|
|
629
|
+
subsystem: "extension:gemini",
|
|
630
|
+
error: rillError.message,
|
|
631
|
+
duration
|
|
632
|
+
});
|
|
633
|
+
throw rillError;
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
description: "Generate embedding vectors for multiple texts",
|
|
637
|
+
returnType: "list"
|
|
638
|
+
},
|
|
639
|
+
// IR-8: gemini::tool_loop
|
|
640
|
+
tool_loop: {
|
|
641
|
+
params: [
|
|
642
|
+
{ name: "prompt", type: "string" },
|
|
643
|
+
{ name: "options", type: "dict", defaultValue: {} }
|
|
644
|
+
],
|
|
645
|
+
fn: async (args, ctx) => {
|
|
646
|
+
const startTime = Date.now();
|
|
647
|
+
try {
|
|
648
|
+
const prompt = args[0];
|
|
649
|
+
const options = args[1] ?? {};
|
|
650
|
+
if (prompt.trim().length === 0) {
|
|
651
|
+
throw new RuntimeError4("RILL-R004", "prompt text cannot be empty");
|
|
652
|
+
}
|
|
653
|
+
if (!("tools" in options) || !Array.isArray(options["tools"])) {
|
|
654
|
+
throw new RuntimeError4(
|
|
655
|
+
"RILL-R004",
|
|
656
|
+
"tool_loop requires 'tools' option"
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
const toolDescriptors = options["tools"];
|
|
660
|
+
const toolsDict = {};
|
|
661
|
+
for (const descriptor of toolDescriptors) {
|
|
662
|
+
const name = typeof descriptor["name"] === "string" ? descriptor["name"] : null;
|
|
663
|
+
if (!name) {
|
|
664
|
+
throw new RuntimeError4(
|
|
665
|
+
"RILL-R004",
|
|
666
|
+
"tool descriptor missing name"
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
const toolFnValue = descriptor["fn"];
|
|
670
|
+
if (!toolFnValue) {
|
|
671
|
+
throw new RuntimeError4(
|
|
672
|
+
"RILL-R004",
|
|
673
|
+
`tool '${name}' missing fn property`
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
if (!isCallable2(toolFnValue)) {
|
|
677
|
+
throw new RuntimeError4(
|
|
678
|
+
"RILL-R004",
|
|
679
|
+
`tool '${name}' fn must be callable`
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
toolsDict[name] = toolFnValue;
|
|
683
|
+
}
|
|
684
|
+
const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
|
|
685
|
+
const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
|
|
686
|
+
const maxTurns = typeof options["max_turns"] === "number" ? options["max_turns"] : 10;
|
|
687
|
+
const maxErrors = typeof options["max_errors"] === "number" ? options["max_errors"] : 3;
|
|
688
|
+
const initialMessages = Array.isArray(options["messages"]) && options["messages"].length > 0 ? options["messages"] : [];
|
|
689
|
+
const contents = [];
|
|
690
|
+
for (const msg of initialMessages) {
|
|
691
|
+
if (typeof msg === "object" && msg !== null && "role" in msg && "content" in msg) {
|
|
692
|
+
const role = msg["role"];
|
|
693
|
+
if (role === "user") {
|
|
694
|
+
contents.push({
|
|
695
|
+
role: "user",
|
|
696
|
+
parts: [{ text: msg["content"] }]
|
|
697
|
+
});
|
|
698
|
+
} else if (role === "assistant") {
|
|
699
|
+
contents.push({
|
|
700
|
+
role: "model",
|
|
701
|
+
parts: [{ text: msg["content"] }]
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
contents.push({
|
|
707
|
+
role: "user",
|
|
708
|
+
parts: [{ text: prompt }]
|
|
709
|
+
});
|
|
710
|
+
const callbacks = {
|
|
711
|
+
// Build Gemini FunctionDeclaration format from tool definitions
|
|
712
|
+
buildTools: (toolDefs) => {
|
|
713
|
+
return toolDefs.map((def) => {
|
|
714
|
+
const properties = {};
|
|
715
|
+
for (const [propName, propDef] of Object.entries(
|
|
716
|
+
def.input_schema.properties
|
|
717
|
+
)) {
|
|
718
|
+
const prop = propDef;
|
|
719
|
+
const propType = prop["type"];
|
|
720
|
+
let schemaType = Type.STRING;
|
|
721
|
+
if (propType === "number") schemaType = Type.NUMBER;
|
|
722
|
+
if (propType === "boolean") schemaType = Type.BOOLEAN;
|
|
723
|
+
if (propType === "integer") schemaType = Type.INTEGER;
|
|
724
|
+
if (propType === "array") schemaType = Type.ARRAY;
|
|
725
|
+
if (propType === "object") schemaType = Type.OBJECT;
|
|
726
|
+
properties[propName] = {
|
|
727
|
+
type: schemaType,
|
|
728
|
+
description: prop["description"] ?? ""
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
name: def.name,
|
|
733
|
+
description: def.description,
|
|
734
|
+
parameters: {
|
|
735
|
+
type: Type.OBJECT,
|
|
736
|
+
properties,
|
|
737
|
+
required: def.input_schema.required
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
});
|
|
741
|
+
},
|
|
742
|
+
// Call Gemini API
|
|
743
|
+
callAPI: async (msgs, tools) => {
|
|
744
|
+
const apiConfig = {
|
|
745
|
+
...system !== void 0 && { systemInstruction: system },
|
|
746
|
+
...maxTokens !== void 0 && { maxOutputTokens: maxTokens },
|
|
747
|
+
...factoryTemperature !== void 0 && {
|
|
748
|
+
temperature: factoryTemperature
|
|
749
|
+
},
|
|
750
|
+
tools: [
|
|
751
|
+
{ functionDeclarations: tools }
|
|
752
|
+
]
|
|
753
|
+
};
|
|
754
|
+
return await client.models.generateContent({
|
|
755
|
+
model: factoryModel,
|
|
756
|
+
contents: msgs,
|
|
757
|
+
config: apiConfig
|
|
758
|
+
});
|
|
759
|
+
},
|
|
760
|
+
// Extract tool calls from Gemini response
|
|
761
|
+
extractToolCalls: (response2) => {
|
|
762
|
+
if (!response2 || typeof response2 !== "object" || !("functionCalls" in response2)) {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
const functionCalls = response2.functionCalls;
|
|
766
|
+
if (!functionCalls || functionCalls.length === 0) {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
return functionCalls.map((fc) => {
|
|
770
|
+
const call = fc;
|
|
771
|
+
return {
|
|
772
|
+
id: call.id ?? "",
|
|
773
|
+
name: call.name ?? "",
|
|
774
|
+
input: call.args ?? {}
|
|
775
|
+
};
|
|
776
|
+
});
|
|
777
|
+
},
|
|
778
|
+
// Format tool results into Gemini message format
|
|
779
|
+
formatToolResult: (toolResults) => {
|
|
780
|
+
const functionResponseParts = toolResults.map((tr) => ({
|
|
781
|
+
functionResponse: {
|
|
782
|
+
name: tr.name,
|
|
783
|
+
response: {
|
|
784
|
+
result: tr.error ? `Error: ${tr.error}` : typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result)
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}));
|
|
788
|
+
return {
|
|
789
|
+
role: "user",
|
|
790
|
+
parts: functionResponseParts
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
const loopResult = await executeToolLoop(
|
|
795
|
+
contents,
|
|
796
|
+
toolsDict,
|
|
797
|
+
maxErrors,
|
|
798
|
+
callbacks,
|
|
799
|
+
(event, data) => {
|
|
800
|
+
const eventMap = {
|
|
801
|
+
tool_call: "gemini:tool_call",
|
|
802
|
+
tool_result: "gemini:tool_result"
|
|
803
|
+
};
|
|
804
|
+
emitExtensionEvent(ctx, {
|
|
805
|
+
event: eventMap[event] || event,
|
|
806
|
+
subsystem: "extension:gemini",
|
|
807
|
+
...data
|
|
808
|
+
});
|
|
809
|
+
},
|
|
810
|
+
maxTurns
|
|
811
|
+
);
|
|
812
|
+
const response = loopResult.response;
|
|
813
|
+
const content = response && typeof response === "object" && "text" in response ? response.text ?? "" : "";
|
|
814
|
+
const result2 = {
|
|
815
|
+
content,
|
|
816
|
+
model: factoryModel,
|
|
817
|
+
usage: loopResult.totalTokens,
|
|
818
|
+
stop_reason: response ? "stop" : "max_turns",
|
|
819
|
+
turns: loopResult.turns,
|
|
820
|
+
messages: [
|
|
821
|
+
...initialMessages,
|
|
822
|
+
{ role: "user", content: prompt },
|
|
823
|
+
{ role: "assistant", content }
|
|
824
|
+
]
|
|
825
|
+
};
|
|
826
|
+
const duration = Date.now() - startTime;
|
|
827
|
+
emitExtensionEvent(ctx, {
|
|
828
|
+
event: "gemini:tool_loop",
|
|
829
|
+
subsystem: "extension:gemini",
|
|
830
|
+
turns: result2.turns,
|
|
831
|
+
total_duration: duration,
|
|
832
|
+
usage: result2.usage
|
|
833
|
+
});
|
|
834
|
+
return result2;
|
|
835
|
+
} catch (error) {
|
|
836
|
+
const duration = Date.now() - startTime;
|
|
837
|
+
const rillError = error instanceof RuntimeError4 ? error : mapProviderError("Gemini", error, detectGeminiError);
|
|
838
|
+
emitExtensionEvent(ctx, {
|
|
839
|
+
event: "gemini:error",
|
|
840
|
+
subsystem: "extension:gemini",
|
|
841
|
+
error: rillError.message,
|
|
842
|
+
duration
|
|
843
|
+
});
|
|
844
|
+
throw rillError;
|
|
845
|
+
}
|
|
846
|
+
},
|
|
847
|
+
description: "Execute tool-use loop with Gemini API",
|
|
848
|
+
returnType: "dict"
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
result.dispose = dispose;
|
|
852
|
+
return result;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// src/index.ts
|
|
856
|
+
var VERSION = "0.0.1";
|
|
857
|
+
export {
|
|
858
|
+
VERSION,
|
|
859
|
+
createGeminiExtension
|
|
860
|
+
};
|