@rcrsr/rill-ext-gemini 0.8.5 → 0.9.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @rcrsr/rill-ext-gemini
2
2
 
3
- [rill](https://rill.run) extension for [Google Gemini](https://ai.google.dev/docs) API integration. Provides `message`, `messages`, `embed`, `embed_batch`, and `tool_loop` host functions.
3
+ [rill](https://rill.run) extension for [Google Gemini](https://ai.google.dev/docs) API integration. Provides `message`, `messages`, `embed`, `embed_batch`, `tool_loop`, and `generate` host functions.
4
4
 
5
5
  > **Experimental.** Breaking changes will occur before stabilization.
6
6
 
@@ -36,131 +36,15 @@ const result = await execute(parse(script), ctx);
36
36
  dispose?.();
37
37
  ```
38
38
 
39
- ## Host Functions
40
-
41
- All functions return a dict with `content`, `model`, `usage`, `stop_reason`, `id`, and `messages`.
42
-
43
- ### gemini::message(text, options?)
44
-
45
- Send a single message to Gemini.
46
-
47
- ```rill
48
- gemini::message("Analyze this code for security issues") => $response
49
- $response.content -> log
50
- $response.usage.output -> log
51
- ```
52
-
53
- ### gemini::messages(messages, options?)
54
-
55
- Send a multi-turn conversation.
56
-
57
- ```rill
58
- gemini::messages([
59
- [role: "user", content: "What is rill?"],
60
- [role: "assistant", content: "A scripting language for AI agents."],
61
- [role: "user", content: "Show me an example."]
62
- ]) => $response
63
- $response.content -> log
64
- ```
65
-
66
- ### gemini::embed(text)
67
-
68
- Generate an embedding vector for text. Requires `embed_model` in config.
69
-
70
- ```rill
71
- gemini::embed("Hello world") => $vector
72
- ```
73
-
74
- ### gemini::embed_batch(texts)
75
-
76
- Generate embedding vectors for multiple texts in a single API call.
77
-
78
- ```rill
79
- gemini::embed_batch(["Hello", "World"]) => $vectors
80
- ```
81
-
82
- ### gemini::tool_loop(prompt, options)
83
-
84
- Execute a tool-use loop where the model calls rill functions iteratively.
85
-
86
- ```rill
87
- gemini::tool_loop("Find the weather", [tools: $my_tools]) => $result
88
- $result.content -> log
89
- $result.turns -> log
90
- ```
91
-
92
- ## Configuration
93
-
94
- ```typescript
95
- const ext = createGeminiExtension({
96
- api_key: process.env.GOOGLE_API_KEY!,
97
- model: 'gemini-2.0-flash',
98
- temperature: 0.7,
99
- max_tokens: 8192,
100
- system: 'You are a helpful assistant.',
101
- embed_model: 'text-embedding-004',
102
- });
103
- ```
104
-
105
- | Option | Type | Default | Description |
106
- |--------|------|---------|-------------|
107
- | `api_key` | string | required | Google API key |
108
- | `model` | string | required | Model identifier |
109
- | `temperature` | number | undefined | Temperature (0.0-2.0) |
110
- | `base_url` | string | undefined | Custom API endpoint URL |
111
- | `max_tokens` | number | `8192` | Max tokens in response |
112
- | `max_retries` | number | undefined | Max retry attempts |
113
- | `timeout` | number | undefined | Request timeout in ms |
114
- | `system` | string | undefined | Default system instruction |
115
- | `embed_model` | string | undefined | Embedding model identifier |
116
-
117
- ## Result Shape
118
-
119
- ```typescript
120
- interface GeminiResult {
121
- content: string; // response text
122
- model: string; // model used
123
- usage: {
124
- input: number; // prompt tokens
125
- output: number; // completion tokens
126
- };
127
- stop_reason: string; // finish reason
128
- id: string; // request ID
129
- messages: Array<{ // full conversation history
130
- role: string;
131
- content: string;
132
- }>;
133
- }
134
- ```
135
-
136
- ## Lifecycle
137
-
138
- Call `dispose()` on the extension to cancel pending requests:
139
-
140
- ```typescript
141
- const ext = createGeminiExtension({ ... });
142
- // ... use extension ...
143
- await ext.dispose?.();
144
- ```
145
-
146
- ## Test Host
147
-
148
- A runnable example at `examples/test-host.ts` wires up the extension with the rill runtime:
149
-
150
- ```bash
151
- pnpm exec tsx examples/test-host.ts
152
- pnpm exec tsx examples/test-host.ts -e 'gemini::message("Tell me a joke") -> log'
153
- pnpm exec tsx examples/test-host.ts script.rill
154
- ```
39
+ ## Documentation
155
40
 
156
- Requires `GOOGLE_API_KEY` environment variable.
41
+ See [full documentation](docs/extension-llm-gemini.md) for configuration, functions, error handling, events, and examples.
157
42
 
158
- ## Documentation
43
+ ## Related
159
44
 
160
- | Document | Description |
161
- |----------|-------------|
162
- | [Extensions Guide](https://github.com/rcrsr/rill/blob/main/docs/integration-extensions.md) | Extension contract and patterns |
163
- | [Host API Reference](https://github.com/rcrsr/rill/blob/main/docs/ref-host-api.md) | Runtime context and host functions |
45
+ - [rill](https://github.com/rcrsr/rill) Core language runtime
46
+ - [Extensions Guide](https://github.com/rcrsr/rill/blob/main/docs/integration-extensions.md) — Extension contract and patterns
47
+ - [Host API Reference](https://github.com/rcrsr/rill/blob/main/docs/ref-host-api.md) Runtime context and host functions
164
48
 
165
49
  ## License
166
50
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Generated by dts-bundle-generator v9.5.1
2
2
 
3
- import { ExtensionResult } from '@rcrsr/rill';
3
+ import { ExtensionConfigSchema, ExtensionResult } from '@rcrsr/rill';
4
4
 
5
5
  /**
6
6
  * Base configuration for LLM extensions
@@ -73,12 +73,8 @@ export type GeminiExtensionConfig = LLMProviderConfig;
73
73
  * ```
74
74
  */
75
75
  export declare function createGeminiExtension(config: GeminiExtensionConfig): ExtensionResult;
76
- /**
77
- * @rcrsr/rill-ext-gemini
78
- *
79
- * Extension for Google Gemini API integration with rill scripts.
80
- */
81
76
  export declare const VERSION = "0.0.1";
77
+ export declare const configSchema: ExtensionConfigSchema;
82
78
 
83
79
  export {
84
80
  LLMProviderConfig as LLMExtensionConfig,
package/dist/index.js CHANGED
@@ -4,11 +4,11 @@ import {
4
4
  Type
5
5
  } from "@google/genai";
6
6
  import {
7
- RuntimeError as RuntimeError4,
7
+ RuntimeError as RuntimeError5,
8
8
  emitExtensionEvent,
9
9
  createVector,
10
10
  isVector,
11
- isCallable as isCallable2
11
+ isDict as isDict2
12
12
  } from "@rcrsr/rill";
13
13
 
14
14
  // ../../shared/ext-llm/dist/validation.js
@@ -79,29 +79,108 @@ function mapProviderError(providerName, error, detect) {
79
79
  }
80
80
 
81
81
  // ../../shared/ext-llm/dist/tool-loop.js
82
- import { isCallable, isDict, RuntimeError as RuntimeError3 } from "@rcrsr/rill";
82
+ import { invokeCallable, isCallable, isDict, isRuntimeCallable, RuntimeError as RuntimeError4 } from "@rcrsr/rill";
83
+
84
+ // ../../shared/ext-llm/dist/schema.js
85
+ import { RuntimeError as RuntimeError3 } from "@rcrsr/rill";
86
+ var RILL_TYPE_MAP = {
87
+ string: "string",
88
+ number: "number",
89
+ bool: "boolean",
90
+ list: "array",
91
+ dict: "object",
92
+ vector: "object",
93
+ shape: "object"
94
+ };
95
+ function mapRillType(rillType) {
96
+ const jsonType = RILL_TYPE_MAP[rillType];
97
+ if (jsonType === void 0) {
98
+ throw new RuntimeError3("RILL-R004", `unsupported type: ${rillType}`);
99
+ }
100
+ return jsonType;
101
+ }
102
+ function buildJsonSchema(rillSchema) {
103
+ const properties = {};
104
+ const required = [];
105
+ for (const [key, value] of Object.entries(rillSchema)) {
106
+ if (typeof value === "string") {
107
+ properties[key] = buildProperty(value);
108
+ } else if (typeof value === "object" && value !== null) {
109
+ properties[key] = buildProperty(value);
110
+ } else {
111
+ throw new RuntimeError3("RILL-R004", `unsupported type: ${String(value)}`);
112
+ }
113
+ required.push(key);
114
+ }
115
+ return { type: "object", properties, required, additionalProperties: false };
116
+ }
117
+ function buildProperty(descriptor) {
118
+ if (typeof descriptor === "string") {
119
+ const jsonType2 = mapRillType(descriptor);
120
+ return { type: jsonType2 };
121
+ }
122
+ const rillType = descriptor["type"];
123
+ if (typeof rillType !== "string") {
124
+ throw new RuntimeError3("RILL-R004", `unsupported type: ${String(rillType)}`);
125
+ }
126
+ const jsonType = mapRillType(rillType);
127
+ const property = { type: jsonType };
128
+ const description = descriptor["description"];
129
+ if (typeof description === "string") {
130
+ property.description = description;
131
+ }
132
+ if ("enum" in descriptor) {
133
+ if (rillType !== "string") {
134
+ throw new RuntimeError3("RILL-R004", "enum is only valid for string type");
135
+ }
136
+ const enumValues = descriptor["enum"];
137
+ if (Array.isArray(enumValues)) {
138
+ property.enum = enumValues;
139
+ }
140
+ }
141
+ if (rillType === "list" && "items" in descriptor) {
142
+ const items = descriptor["items"];
143
+ if (typeof items === "string") {
144
+ property.items = buildProperty(items);
145
+ } else if (typeof items === "object" && items !== null) {
146
+ property.items = buildProperty(items);
147
+ }
148
+ }
149
+ if (rillType === "dict" && "properties" in descriptor) {
150
+ const nestedProps = descriptor["properties"];
151
+ if (typeof nestedProps === "object" && nestedProps !== null) {
152
+ const subSchema = buildJsonSchema(nestedProps);
153
+ property.properties = subSchema.properties;
154
+ property.required = subSchema.required;
155
+ property.additionalProperties = false;
156
+ }
157
+ }
158
+ return property;
159
+ }
160
+
161
+ // ../../shared/ext-llm/dist/tool-loop.js
83
162
  async function executeToolCall(toolName, toolInput, tools, context) {
84
163
  if (!isDict(tools)) {
85
- throw new RuntimeError3("RILL-R004", "tools must be a dict mapping tool names to functions");
164
+ throw new RuntimeError4("RILL-R004", "tool_loop: tools must be a dict of name \u2192 callable");
86
165
  }
87
166
  const toolsDict = tools;
88
167
  const toolFn = toolsDict[toolName];
89
168
  if (toolFn === void 0 || toolFn === null) {
90
- throw new RuntimeError3("RILL-R004", `Unknown tool: ${toolName}`);
169
+ throw new RuntimeError4("RILL-R004", `Unknown tool: ${toolName}`);
91
170
  }
92
171
  if (!isCallable(toolFn)) {
93
- throw new RuntimeError3("RILL-R004", `Invalid tool input for ${toolName}: tool must be callable`);
172
+ throw new RuntimeError4("RILL-R004", `Invalid tool input for ${toolName}: tool must be callable`);
94
173
  }
95
174
  if (typeof toolInput !== "object" || toolInput === null) {
96
- throw new RuntimeError3("RILL-R004", `Invalid tool input for ${toolName}: input must be an object`);
175
+ throw new RuntimeError4("RILL-R004", `Invalid tool input for ${toolName}: input must be an object`);
97
176
  }
98
177
  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`);
178
+ if (callable.kind !== "runtime" && callable.kind !== "application" && callable.kind !== "script") {
179
+ throw new RuntimeError4("RILL-R004", `Invalid tool input for ${toolName}: tool must be application, runtime, or script callable`);
101
180
  }
102
181
  try {
103
182
  let args;
104
- if (callable.kind === "application" && callable.params) {
183
+ if ((callable.kind === "application" || callable.kind === "script") && callable.params && callable.params.length > 0) {
105
184
  const params = callable.params;
106
185
  const inputDict = toolInput;
107
186
  args = params.map((param) => {
@@ -111,6 +190,12 @@ async function executeToolCall(toolName, toolInput, tools, context) {
111
190
  } else {
112
191
  args = [toolInput];
113
192
  }
193
+ if (callable.kind === "script") {
194
+ if (!context) {
195
+ throw new RuntimeError4("RILL-R004", `Invalid tool input for ${toolName}: script callable requires a runtime context`);
196
+ }
197
+ return await invokeCallable(callable, args, context);
198
+ }
114
199
  const ctx = context ?? {
115
200
  parent: void 0,
116
201
  variables: /* @__PURE__ */ new Map(),
@@ -119,74 +204,146 @@ async function executeToolCall(toolName, toolInput, tools, context) {
119
204
  const result = callable.fn(args, ctx);
120
205
  return result instanceof Promise ? await result : result;
121
206
  } catch (error) {
122
- if (error instanceof RuntimeError3) {
207
+ if (error instanceof RuntimeError4) {
123
208
  throw error;
124
209
  }
125
210
  const message = error instanceof Error ? error.message : "Unknown error";
126
- throw new RuntimeError3("RILL-R004", `Invalid tool input for ${toolName}: ${message}`);
211
+ throw new RuntimeError4("RILL-R004", `Invalid tool input for ${toolName}: ${message}`);
212
+ }
213
+ }
214
+ function sanitizeToolName(name) {
215
+ const match = name.match(/^[a-zA-Z0-9_-]*/);
216
+ const sanitized = match ? match[0] : "";
217
+ return sanitized.length > 0 ? sanitized : name;
218
+ }
219
+ function patchResponseToolCallNames(response, nameMap) {
220
+ if (!nameMap.size || !response || typeof response !== "object")
221
+ return;
222
+ const resp = response;
223
+ if (Array.isArray(resp["choices"])) {
224
+ for (const choice of resp["choices"]) {
225
+ const msg = choice?.["message"];
226
+ const tcs = msg?.["tool_calls"];
227
+ if (Array.isArray(tcs)) {
228
+ for (const tc of tcs) {
229
+ const fn = tc?.["function"];
230
+ if (fn && typeof fn["name"] === "string") {
231
+ const orig = fn["name"];
232
+ const san = nameMap.get(orig);
233
+ if (san !== void 0)
234
+ fn["name"] = san;
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+ if (Array.isArray(resp["content"])) {
241
+ for (const block of resp["content"]) {
242
+ const b = block;
243
+ if (b?.["type"] === "tool_use" && typeof b?.["name"] === "string") {
244
+ const orig = b["name"];
245
+ const san = nameMap.get(orig);
246
+ if (san !== void 0)
247
+ b["name"] = san;
248
+ }
249
+ }
250
+ }
251
+ if (Array.isArray(resp["functionCalls"])) {
252
+ for (const fc of resp["functionCalls"]) {
253
+ const f = fc;
254
+ if (typeof f?.["name"] === "string") {
255
+ const orig = f["name"];
256
+ const san = nameMap.get(orig);
257
+ if (san !== void 0)
258
+ f["name"] = san;
259
+ }
260
+ }
261
+ }
262
+ if (Array.isArray(resp["candidates"])) {
263
+ for (const cand of resp["candidates"]) {
264
+ const content = cand?.["content"];
265
+ const parts = content?.["parts"];
266
+ if (Array.isArray(parts)) {
267
+ for (const part of parts) {
268
+ const fc = part?.["functionCall"];
269
+ if (fc && typeof fc["name"] === "string") {
270
+ const orig = fc["name"];
271
+ const san = nameMap.get(orig);
272
+ if (san !== void 0)
273
+ fc["name"] = san;
274
+ }
275
+ }
276
+ }
277
+ }
127
278
  }
128
279
  }
129
280
  async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent, maxTurns = 10, context) {
130
281
  if (tools === void 0) {
131
- throw new RuntimeError3("RILL-R004", "tools parameter is required");
282
+ throw new RuntimeError4("RILL-R004", "tools parameter is required");
132
283
  }
133
284
  if (!isDict(tools)) {
134
- throw new RuntimeError3("RILL-R004", "tools must be a dict mapping tool names to functions");
285
+ throw new RuntimeError4("RILL-R004", "tool_loop: tools must be a dict of name \u2192 callable");
135
286
  }
136
287
  const toolsDict = tools;
137
288
  const toolDescriptors = Object.entries(toolsDict).map(([name, fn]) => {
138
289
  const fnValue = fn;
290
+ if (isRuntimeCallable(fnValue)) {
291
+ throw new RuntimeError4("RILL-R004", `tool_loop: builtin "${name}" cannot be used as a tool \u2014 wrap in a closure`);
292
+ }
139
293
  if (!isCallable(fnValue)) {
140
- throw new RuntimeError3("RILL-R004", `tool '${name}' must be callable function`);
294
+ throw new RuntimeError4("RILL-R004", `tool_loop: tool "${name}" is not a callable`);
141
295
  }
142
296
  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;
297
+ let description;
298
+ if (callable.kind === "script") {
299
+ description = callable.annotations["description"] ?? "";
300
+ } else {
301
+ description = callable.description ?? "";
302
+ }
303
+ let inputSchema;
304
+ const params = callable.kind === "application" ? callable.params ?? [] : callable.kind === "script" ? callable.params : [];
305
+ if (params.length > 0) {
306
+ const properties = {};
307
+ const required = [];
308
+ for (const param of params) {
309
+ const property = {};
310
+ if (param.typeName !== null) {
311
+ const descriptor = {
312
+ [param.name]: { type: param.typeName }
313
+ };
314
+ const schema = buildJsonSchema(descriptor);
315
+ const built = schema.properties[param.name];
316
+ if (built !== void 0) {
317
+ Object.assign(property, built);
318
+ }
169
319
  }
170
- const property = {
171
- type: jsonSchemaType
172
- };
173
- if (param.description) {
174
- property["description"] = param.description;
320
+ let paramDesc;
321
+ if (callable.kind === "script") {
322
+ const annot = callable.paramAnnotations[param.name];
323
+ paramDesc = annot?.["description"] ?? "";
324
+ } else {
325
+ paramDesc = param.description ?? "";
326
+ }
327
+ if (paramDesc) {
328
+ property["description"] = paramDesc;
175
329
  }
176
330
  properties[param.name] = property;
177
331
  if (param.defaultValue === null) {
178
332
  required.push(param.name);
179
333
  }
180
334
  }
335
+ inputSchema = {
336
+ type: "object",
337
+ properties,
338
+ required
339
+ };
340
+ } else {
341
+ inputSchema = { type: "object", properties: {}, required: [] };
181
342
  }
182
343
  return {
183
344
  name,
184
345
  description,
185
- input_schema: {
186
- type: "object",
187
- properties,
188
- required
189
- }
346
+ input_schema: inputSchema
190
347
  };
191
348
  });
192
349
  const providerTools = callbacks.buildTools(toolDescriptors);
@@ -203,7 +360,7 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
203
360
  response = await callbacks.callAPI(currentMessages, providerTools);
204
361
  } catch (error) {
205
362
  const message = error instanceof Error ? error.message : "Unknown error";
206
- throw new RuntimeError3("RILL-R004", `Provider API error: ${message}`, void 0, { cause: error });
363
+ throw new RuntimeError4("RILL-R004", `Provider API error: ${message}`, void 0, { cause: error });
207
364
  }
208
365
  if (typeof response === "object" && response !== null && "usage" in response) {
209
366
  const usage = response["usage"];
@@ -215,7 +372,17 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
215
372
  totalOutputTokens += outputTokens;
216
373
  }
217
374
  }
218
- const toolCalls = callbacks.extractToolCalls(response);
375
+ const rawToolCalls = callbacks.extractToolCalls(response);
376
+ const nameMap = /* @__PURE__ */ new Map();
377
+ const toolCalls = rawToolCalls?.map((tc) => {
378
+ const sanitized = sanitizeToolName(tc.name);
379
+ if (sanitized !== tc.name)
380
+ nameMap.set(tc.name, sanitized);
381
+ return sanitized !== tc.name ? { ...tc, name: sanitized } : tc;
382
+ }) ?? null;
383
+ if (nameMap.size > 0) {
384
+ patchResponseToolCallNames(response, nameMap);
385
+ }
219
386
  if (toolCalls === null || toolCalls.length === 0) {
220
387
  return {
221
388
  response,
@@ -240,7 +407,7 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
240
407
  const duration = Date.now() - toolStartTime;
241
408
  consecutiveErrors++;
242
409
  let originalError;
243
- if (error instanceof RuntimeError3) {
410
+ if (error instanceof RuntimeError4) {
244
411
  const prefix = `Invalid tool input for ${name}: `;
245
412
  if (error.message.startsWith(prefix)) {
246
413
  originalError = error.message.slice(prefix.length);
@@ -265,12 +432,20 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
265
432
  duration
266
433
  });
267
434
  if (consecutiveErrors >= maxErrors) {
268
- throw new RuntimeError3("RILL-R004", `Tool execution failed: ${maxErrors} consecutive errors`);
435
+ throw new RuntimeError4("RILL-R004", `Tool execution failed: ${maxErrors} consecutive errors (last: ${name}: ${originalError})`);
269
436
  }
270
437
  }
271
438
  }
439
+ const assistantMessage = callbacks.formatAssistantMessage(response);
440
+ if (assistantMessage != null) {
441
+ currentMessages.push(assistantMessage);
442
+ }
272
443
  const toolResultMessage = callbacks.formatToolResult(toolResults);
273
- currentMessages.push(toolResultMessage);
444
+ if (Array.isArray(toolResultMessage)) {
445
+ currentMessages.push(...toolResultMessage);
446
+ } else {
447
+ currentMessages.push(toolResultMessage);
448
+ }
274
449
  }
275
450
  return {
276
451
  response: null,
@@ -298,6 +473,35 @@ var detectGeminiError = (error) => {
298
473
  }
299
474
  return null;
300
475
  };
476
+ function toGeminiSchema(prop) {
477
+ let schemaType = Type.STRING;
478
+ if (prop.type === "number") schemaType = Type.NUMBER;
479
+ if (prop.type === "boolean") schemaType = Type.BOOLEAN;
480
+ if (prop.type === "integer") schemaType = Type.INTEGER;
481
+ if (prop.type === "array") schemaType = Type.ARRAY;
482
+ if (prop.type === "object") schemaType = Type.OBJECT;
483
+ const schema = { type: schemaType };
484
+ if (prop.description !== void 0) {
485
+ schema.description = prop.description;
486
+ }
487
+ if (prop.enum !== void 0) {
488
+ schema.enum = prop.enum;
489
+ }
490
+ if (prop.type === "array" && prop.items !== void 0) {
491
+ schema.items = toGeminiSchema(prop.items);
492
+ }
493
+ if (prop.type === "object" && prop.properties !== void 0) {
494
+ const nestedProperties = {};
495
+ for (const [key, nestedProp] of Object.entries(prop.properties)) {
496
+ nestedProperties[key] = toGeminiSchema(nestedProp);
497
+ }
498
+ schema.properties = nestedProperties;
499
+ if (prop.required !== void 0) {
500
+ schema.required = prop.required;
501
+ }
502
+ }
503
+ return schema;
504
+ }
301
505
  function createGeminiExtension(config) {
302
506
  validateApiKey(config.api_key);
303
507
  validateModel(config.model);
@@ -340,7 +544,7 @@ function createGeminiExtension(config) {
340
544
  const text = args[0];
341
545
  const options = args[1] ?? {};
342
546
  if (text.trim().length === 0) {
343
- throw new RuntimeError4("RILL-R004", "prompt text cannot be empty");
547
+ throw new RuntimeError5("RILL-R004", "prompt text cannot be empty");
344
548
  }
345
549
  const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
346
550
  const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
@@ -389,12 +593,14 @@ function createGeminiExtension(config) {
389
593
  subsystem: "extension:gemini",
390
594
  duration,
391
595
  model: factoryModel,
392
- usage: result2.usage
596
+ usage: result2.usage,
597
+ request: contents,
598
+ content
393
599
  });
394
600
  return result2;
395
601
  } catch (error) {
396
602
  const duration = Date.now() - startTime;
397
- const rillError = error instanceof RuntimeError4 ? error : mapProviderError("Gemini", error, detectGeminiError);
603
+ const rillError = error instanceof RuntimeError5 ? error : mapProviderError("Gemini", error, detectGeminiError);
398
604
  emitExtensionEvent(ctx, {
399
605
  event: "gemini:error",
400
606
  subsystem: "extension:gemini",
@@ -419,7 +625,7 @@ function createGeminiExtension(config) {
419
625
  const messages = args[0];
420
626
  const options = args[1] ?? {};
421
627
  if (messages.length === 0) {
422
- throw new RuntimeError4(
628
+ throw new RuntimeError5(
423
629
  "RILL-R004",
424
630
  "messages list cannot be empty"
425
631
  );
@@ -430,18 +636,18 @@ function createGeminiExtension(config) {
430
636
  for (let i = 0; i < messages.length; i++) {
431
637
  const msg = messages[i];
432
638
  if (!msg || typeof msg !== "object" || !("role" in msg)) {
433
- throw new RuntimeError4(
639
+ throw new RuntimeError5(
434
640
  "RILL-R004",
435
641
  "message missing required 'role' field"
436
642
  );
437
643
  }
438
644
  const role = msg["role"];
439
645
  if (role !== "user" && role !== "assistant" && role !== "tool") {
440
- throw new RuntimeError4("RILL-R004", `invalid role '${role}'`);
646
+ throw new RuntimeError5("RILL-R004", `invalid role '${role}'`);
441
647
  }
442
648
  if (role === "user" || role === "tool") {
443
649
  if (!("content" in msg) || typeof msg["content"] !== "string") {
444
- throw new RuntimeError4(
650
+ throw new RuntimeError5(
445
651
  "RILL-R004",
446
652
  `${role} message requires 'content'`
447
653
  );
@@ -454,7 +660,7 @@ function createGeminiExtension(config) {
454
660
  const hasContent = "content" in msg && msg["content"];
455
661
  const hasToolCalls = "tool_calls" in msg && msg["tool_calls"];
456
662
  if (!hasContent && !hasToolCalls) {
457
- throw new RuntimeError4(
663
+ throw new RuntimeError5(
458
664
  "RILL-R004",
459
665
  "assistant message requires 'content' or 'tool_calls'"
460
666
  );
@@ -511,12 +717,14 @@ function createGeminiExtension(config) {
511
717
  subsystem: "extension:gemini",
512
718
  duration,
513
719
  model: factoryModel,
514
- usage: result2.usage
720
+ usage: result2.usage,
721
+ request: contents,
722
+ content
515
723
  });
516
724
  return result2;
517
725
  } catch (error) {
518
726
  const duration = Date.now() - startTime;
519
- const rillError = error instanceof RuntimeError4 ? error : mapProviderError("Gemini", error, detectGeminiError);
727
+ const rillError = error instanceof RuntimeError5 ? error : mapProviderError("Gemini", error, detectGeminiError);
520
728
  emitExtensionEvent(ctx, {
521
729
  event: "gemini:error",
522
730
  subsystem: "extension:gemini",
@@ -544,7 +752,7 @@ function createGeminiExtension(config) {
544
752
  });
545
753
  const embedding = response.embeddings?.[0];
546
754
  if (!embedding || !embedding.values || embedding.values.length === 0) {
547
- throw new RuntimeError4(
755
+ throw new RuntimeError5(
548
756
  "RILL-R004",
549
757
  "Gemini: empty embedding returned"
550
758
  );
@@ -562,7 +770,7 @@ function createGeminiExtension(config) {
562
770
  return vector;
563
771
  } catch (error) {
564
772
  const duration = Date.now() - startTime;
565
- const rillError = error instanceof RuntimeError4 ? error : mapProviderError("Gemini", error, detectGeminiError);
773
+ const rillError = error instanceof RuntimeError5 ? error : mapProviderError("Gemini", error, detectGeminiError);
566
774
  emitExtensionEvent(ctx, {
567
775
  event: "gemini:error",
568
776
  subsystem: "extension:gemini",
@@ -593,14 +801,14 @@ function createGeminiExtension(config) {
593
801
  });
594
802
  const vectors = [];
595
803
  if (!response.embeddings || response.embeddings.length === 0) {
596
- throw new RuntimeError4(
804
+ throw new RuntimeError5(
597
805
  "RILL-R004",
598
806
  "Gemini: empty embeddings returned"
599
807
  );
600
808
  }
601
809
  for (const embedding of response.embeddings) {
602
810
  if (!embedding || !embedding.values || embedding.values.length === 0) {
603
- throw new RuntimeError4(
811
+ throw new RuntimeError5(
604
812
  "RILL-R004",
605
813
  "Gemini: empty embedding returned"
606
814
  );
@@ -623,7 +831,7 @@ function createGeminiExtension(config) {
623
831
  return vectors;
624
832
  } catch (error) {
625
833
  const duration = Date.now() - startTime;
626
- const rillError = error instanceof RuntimeError4 ? error : mapProviderError("Gemini", error, detectGeminiError);
834
+ const rillError = error instanceof RuntimeError5 ? error : mapProviderError("Gemini", error, detectGeminiError);
627
835
  emitExtensionEvent(ctx, {
628
836
  event: "gemini:error",
629
837
  subsystem: "extension:gemini",
@@ -648,39 +856,15 @@ function createGeminiExtension(config) {
648
856
  const prompt = args[0];
649
857
  const options = args[1] ?? {};
650
858
  if (prompt.trim().length === 0) {
651
- throw new RuntimeError4("RILL-R004", "prompt text cannot be empty");
859
+ throw new RuntimeError5("RILL-R004", "prompt text cannot be empty");
652
860
  }
653
- if (!("tools" in options) || !Array.isArray(options["tools"])) {
654
- throw new RuntimeError4(
861
+ if (!("tools" in options) || !isDict2(options["tools"])) {
862
+ throw new RuntimeError5(
655
863
  "RILL-R004",
656
864
  "tool_loop requires 'tools' option"
657
865
  );
658
866
  }
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
- }
867
+ const toolsDict = options["tools"];
684
868
  const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
685
869
  const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
686
870
  const maxTurns = typeof options["max_turns"] === "number" ? options["max_turns"] : 10;
@@ -775,6 +959,21 @@ function createGeminiExtension(config) {
775
959
  };
776
960
  });
777
961
  },
962
+ // Extract the model's content from Gemini response for conversation history
963
+ formatAssistantMessage: (response2) => {
964
+ if (!response2 || typeof response2 !== "object" || !("candidates" in response2)) {
965
+ return null;
966
+ }
967
+ const candidates = response2.candidates;
968
+ if (!Array.isArray(candidates) || candidates.length === 0) {
969
+ return null;
970
+ }
971
+ const candidate = candidates[0];
972
+ if (!candidate || typeof candidate !== "object" || !("content" in candidate)) {
973
+ return null;
974
+ }
975
+ return candidate.content;
976
+ },
778
977
  // Format tool results into Gemini message format
779
978
  formatToolResult: (toolResults) => {
780
979
  const functionResponseParts = toolResults.map((tr) => ({
@@ -807,7 +1006,8 @@ function createGeminiExtension(config) {
807
1006
  ...data
808
1007
  });
809
1008
  },
810
- maxTurns
1009
+ maxTurns,
1010
+ ctx
811
1011
  );
812
1012
  const response = loopResult.response;
813
1013
  const content = response && typeof response === "object" && "text" in response ? response.text ?? "" : "";
@@ -829,12 +1029,14 @@ function createGeminiExtension(config) {
829
1029
  subsystem: "extension:gemini",
830
1030
  turns: result2.turns,
831
1031
  total_duration: duration,
832
- usage: result2.usage
1032
+ usage: result2.usage,
1033
+ request: contents,
1034
+ content
833
1035
  });
834
1036
  return result2;
835
1037
  } catch (error) {
836
1038
  const duration = Date.now() - startTime;
837
- const rillError = error instanceof RuntimeError4 ? error : mapProviderError("Gemini", error, detectGeminiError);
1039
+ const rillError = error instanceof RuntimeError5 ? error : mapProviderError("Gemini", error, detectGeminiError);
838
1040
  emitExtensionEvent(ctx, {
839
1041
  event: "gemini:error",
840
1042
  subsystem: "extension:gemini",
@@ -846,6 +1048,130 @@ function createGeminiExtension(config) {
846
1048
  },
847
1049
  description: "Execute tool-use loop with Gemini API",
848
1050
  returnType: "dict"
1051
+ },
1052
+ // IR-3: gemini::generate
1053
+ generate: {
1054
+ params: [
1055
+ { name: "prompt", type: "string" },
1056
+ { name: "options", type: "dict" }
1057
+ ],
1058
+ fn: async (args, ctx) => {
1059
+ const startTime = Date.now();
1060
+ try {
1061
+ const prompt = args[0];
1062
+ const options = args[1] ?? {};
1063
+ if (!("schema" in options) || options["schema"] === null || options["schema"] === void 0) {
1064
+ throw new RuntimeError5(
1065
+ "RILL-R004",
1066
+ "generate requires 'schema' option"
1067
+ );
1068
+ }
1069
+ const rillSchema = options["schema"];
1070
+ const jsonSchema = buildJsonSchema(rillSchema);
1071
+ const geminiProperties = {};
1072
+ for (const [key, prop] of Object.entries(jsonSchema.properties)) {
1073
+ geminiProperties[key] = toGeminiSchema(prop);
1074
+ }
1075
+ const responseSchema = {
1076
+ type: Type.OBJECT,
1077
+ properties: geminiProperties,
1078
+ required: jsonSchema.required
1079
+ };
1080
+ const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
1081
+ const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
1082
+ const contents = [];
1083
+ if ("messages" in options && Array.isArray(options["messages"])) {
1084
+ const prependedMessages = options["messages"];
1085
+ for (const msg of prependedMessages) {
1086
+ if (typeof msg === "object" && msg !== null && "role" in msg && "content" in msg) {
1087
+ const role = msg["role"];
1088
+ if (role === "user") {
1089
+ contents.push({
1090
+ role: "user",
1091
+ parts: [{ text: msg["content"] }]
1092
+ });
1093
+ } else if (role === "assistant") {
1094
+ contents.push({
1095
+ role: "model",
1096
+ parts: [{ text: msg["content"] }]
1097
+ });
1098
+ }
1099
+ }
1100
+ }
1101
+ }
1102
+ contents.push({
1103
+ role: "user",
1104
+ parts: [{ text: prompt }]
1105
+ });
1106
+ const apiConfig = {
1107
+ responseSchema,
1108
+ responseMimeType: "application/json"
1109
+ };
1110
+ if (system !== void 0) {
1111
+ apiConfig.systemInstruction = system;
1112
+ }
1113
+ if (maxTokens !== void 0) {
1114
+ apiConfig.maxOutputTokens = maxTokens;
1115
+ }
1116
+ if (factoryTemperature !== void 0) {
1117
+ apiConfig.temperature = factoryTemperature;
1118
+ }
1119
+ const response = await client.models.generateContent({
1120
+ model: factoryModel,
1121
+ contents,
1122
+ config: apiConfig
1123
+ });
1124
+ const raw = response.text ?? "";
1125
+ let data;
1126
+ try {
1127
+ data = JSON.parse(raw);
1128
+ } catch (parseError) {
1129
+ const detail = parseError instanceof Error ? parseError.message : String(parseError);
1130
+ throw new RuntimeError5(
1131
+ "RILL-R004",
1132
+ `generate: failed to parse response JSON: ${detail}`
1133
+ );
1134
+ }
1135
+ const inputTokens = response.usageMetadata?.promptTokenCount ?? 0;
1136
+ const outputTokens = response.usageMetadata?.candidatesTokenCount ?? 0;
1137
+ const stopReason = response.candidates?.[0]?.finishReason ?? "stop";
1138
+ const id = response.responseId ?? "";
1139
+ const generateResult = {
1140
+ data,
1141
+ raw,
1142
+ model: factoryModel,
1143
+ usage: {
1144
+ input: inputTokens,
1145
+ output: outputTokens
1146
+ },
1147
+ stop_reason: stopReason,
1148
+ id
1149
+ };
1150
+ const duration = Date.now() - startTime;
1151
+ emitExtensionEvent(ctx, {
1152
+ event: "gemini:generate",
1153
+ subsystem: "extension:gemini",
1154
+ duration,
1155
+ model: factoryModel,
1156
+ usage: generateResult.usage,
1157
+ request: contents,
1158
+ content: raw
1159
+ });
1160
+ return generateResult;
1161
+ } catch (error) {
1162
+ const duration = Date.now() - startTime;
1163
+ const rillError = error instanceof RuntimeError5 ? error : mapProviderError("Gemini", error, detectGeminiError);
1164
+ emitExtensionEvent(ctx, {
1165
+ event: "gemini:error",
1166
+ subsystem: "extension:gemini",
1167
+ error: rillError.message,
1168
+ duration
1169
+ });
1170
+ throw rillError;
1171
+ }
1172
+ },
1173
+ description: "Generate structured output from Gemini API",
1174
+ returnType: "dict"
849
1175
  }
850
1176
  };
851
1177
  result.dispose = dispose;
@@ -854,7 +1180,19 @@ function createGeminiExtension(config) {
854
1180
 
855
1181
  // src/index.ts
856
1182
  var VERSION = "0.0.1";
1183
+ var configSchema = {
1184
+ api_key: { type: "string", required: true, secret: true },
1185
+ model: { type: "string", required: true },
1186
+ base_url: { type: "string" },
1187
+ temperature: { type: "number" },
1188
+ max_tokens: { type: "number" },
1189
+ timeout: { type: "number" },
1190
+ max_retries: { type: "number" },
1191
+ system: { type: "string" },
1192
+ embed_model: { type: "string" }
1193
+ };
857
1194
  export {
858
1195
  VERSION,
1196
+ configSchema,
859
1197
  createGeminiExtension
860
1198
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rcrsr/rill-ext-gemini",
3
- "version": "0.8.5",
3
+ "version": "0.9.0",
4
4
  "description": "rill extension for Google Gemini API integration",
5
5
  "license": "MIT",
6
6
  "author": "Andre Bremer",
@@ -17,14 +17,14 @@
17
17
  "scripting"
18
18
  ],
19
19
  "peerDependencies": {
20
- "@rcrsr/rill": "^0.8.5"
20
+ "@rcrsr/rill": "^0.9.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@types/node": "^25.2.3",
23
+ "@rcrsr/rill": "^0.9.0",
24
+ "@types/node": "^25.3.0",
24
25
  "dts-bundle-generator": "^9.5.1",
25
- "tsup": "^8.5.0",
26
- "undici-types": "^7.21.0",
27
- "@rcrsr/rill": "^0.8.5",
26
+ "tsup": "^8.5.1",
27
+ "undici-types": "^7.22.0",
28
28
  "@rcrsr/rill-ext-llm-shared": "^0.0.1"
29
29
  },
30
30
  "files": [
@@ -32,18 +32,18 @@
32
32
  ],
33
33
  "repository": {
34
34
  "type": "git",
35
- "url": "git+https://github.com/rcrsr/rill.git",
36
- "directory": "packages/ext/gemini"
35
+ "url": "git+https://github.com/rcrsr/rill-ext.git",
36
+ "directory": "packages/ext/llm-gemini"
37
37
  },
38
- "homepage": "https://rill.run/docs/extensions/gemini/",
38
+ "homepage": "https://github.com/rcrsr/rill-ext/tree/main/packages/ext/llm-gemini#readme",
39
39
  "bugs": {
40
- "url": "https://github.com/rcrsr/rill/issues"
40
+ "url": "https://github.com/rcrsr/rill-ext/issues"
41
41
  },
42
42
  "publishConfig": {
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@google/genai": "^1.40.0"
46
+ "@google/genai": "^1.42.0"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsup && dts-bundle-generator --config dts-bundle-generator.config.cjs",