@rcrsr/rill-ext-gemini 0.8.6 → 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,77 +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;
169
- default:
170
- jsonSchemaType = "string";
171
- 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
+ }
172
319
  }
173
- const property = {
174
- type: jsonSchemaType
175
- };
176
- if (param.description) {
177
- 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;
178
329
  }
179
330
  properties[param.name] = property;
180
331
  if (param.defaultValue === null) {
181
332
  required.push(param.name);
182
333
  }
183
334
  }
335
+ inputSchema = {
336
+ type: "object",
337
+ properties,
338
+ required
339
+ };
340
+ } else {
341
+ inputSchema = { type: "object", properties: {}, required: [] };
184
342
  }
185
343
  return {
186
344
  name,
187
345
  description,
188
- input_schema: {
189
- type: "object",
190
- properties,
191
- required
192
- }
346
+ input_schema: inputSchema
193
347
  };
194
348
  });
195
349
  const providerTools = callbacks.buildTools(toolDescriptors);
@@ -206,7 +360,7 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
206
360
  response = await callbacks.callAPI(currentMessages, providerTools);
207
361
  } catch (error) {
208
362
  const message = error instanceof Error ? error.message : "Unknown error";
209
- 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 });
210
364
  }
211
365
  if (typeof response === "object" && response !== null && "usage" in response) {
212
366
  const usage = response["usage"];
@@ -218,7 +372,17 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
218
372
  totalOutputTokens += outputTokens;
219
373
  }
220
374
  }
221
- 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
+ }
222
386
  if (toolCalls === null || toolCalls.length === 0) {
223
387
  return {
224
388
  response,
@@ -243,7 +407,7 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
243
407
  const duration = Date.now() - toolStartTime;
244
408
  consecutiveErrors++;
245
409
  let originalError;
246
- if (error instanceof RuntimeError3) {
410
+ if (error instanceof RuntimeError4) {
247
411
  const prefix = `Invalid tool input for ${name}: `;
248
412
  if (error.message.startsWith(prefix)) {
249
413
  originalError = error.message.slice(prefix.length);
@@ -268,12 +432,20 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
268
432
  duration
269
433
  });
270
434
  if (consecutiveErrors >= maxErrors) {
271
- 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})`);
272
436
  }
273
437
  }
274
438
  }
439
+ const assistantMessage = callbacks.formatAssistantMessage(response);
440
+ if (assistantMessage != null) {
441
+ currentMessages.push(assistantMessage);
442
+ }
275
443
  const toolResultMessage = callbacks.formatToolResult(toolResults);
276
- currentMessages.push(toolResultMessage);
444
+ if (Array.isArray(toolResultMessage)) {
445
+ currentMessages.push(...toolResultMessage);
446
+ } else {
447
+ currentMessages.push(toolResultMessage);
448
+ }
277
449
  }
278
450
  return {
279
451
  response: null,
@@ -301,6 +473,35 @@ var detectGeminiError = (error) => {
301
473
  }
302
474
  return null;
303
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
+ }
304
505
  function createGeminiExtension(config) {
305
506
  validateApiKey(config.api_key);
306
507
  validateModel(config.model);
@@ -343,7 +544,7 @@ function createGeminiExtension(config) {
343
544
  const text = args[0];
344
545
  const options = args[1] ?? {};
345
546
  if (text.trim().length === 0) {
346
- throw new RuntimeError4("RILL-R004", "prompt text cannot be empty");
547
+ throw new RuntimeError5("RILL-R004", "prompt text cannot be empty");
347
548
  }
348
549
  const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
349
550
  const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
@@ -392,12 +593,14 @@ function createGeminiExtension(config) {
392
593
  subsystem: "extension:gemini",
393
594
  duration,
394
595
  model: factoryModel,
395
- usage: result2.usage
596
+ usage: result2.usage,
597
+ request: contents,
598
+ content
396
599
  });
397
600
  return result2;
398
601
  } catch (error) {
399
602
  const duration = Date.now() - startTime;
400
- const rillError = error instanceof RuntimeError4 ? error : mapProviderError("Gemini", error, detectGeminiError);
603
+ const rillError = error instanceof RuntimeError5 ? error : mapProviderError("Gemini", error, detectGeminiError);
401
604
  emitExtensionEvent(ctx, {
402
605
  event: "gemini:error",
403
606
  subsystem: "extension:gemini",
@@ -422,7 +625,7 @@ function createGeminiExtension(config) {
422
625
  const messages = args[0];
423
626
  const options = args[1] ?? {};
424
627
  if (messages.length === 0) {
425
- throw new RuntimeError4(
628
+ throw new RuntimeError5(
426
629
  "RILL-R004",
427
630
  "messages list cannot be empty"
428
631
  );
@@ -433,18 +636,18 @@ function createGeminiExtension(config) {
433
636
  for (let i = 0; i < messages.length; i++) {
434
637
  const msg = messages[i];
435
638
  if (!msg || typeof msg !== "object" || !("role" in msg)) {
436
- throw new RuntimeError4(
639
+ throw new RuntimeError5(
437
640
  "RILL-R004",
438
641
  "message missing required 'role' field"
439
642
  );
440
643
  }
441
644
  const role = msg["role"];
442
645
  if (role !== "user" && role !== "assistant" && role !== "tool") {
443
- throw new RuntimeError4("RILL-R004", `invalid role '${role}'`);
646
+ throw new RuntimeError5("RILL-R004", `invalid role '${role}'`);
444
647
  }
445
648
  if (role === "user" || role === "tool") {
446
649
  if (!("content" in msg) || typeof msg["content"] !== "string") {
447
- throw new RuntimeError4(
650
+ throw new RuntimeError5(
448
651
  "RILL-R004",
449
652
  `${role} message requires 'content'`
450
653
  );
@@ -457,7 +660,7 @@ function createGeminiExtension(config) {
457
660
  const hasContent = "content" in msg && msg["content"];
458
661
  const hasToolCalls = "tool_calls" in msg && msg["tool_calls"];
459
662
  if (!hasContent && !hasToolCalls) {
460
- throw new RuntimeError4(
663
+ throw new RuntimeError5(
461
664
  "RILL-R004",
462
665
  "assistant message requires 'content' or 'tool_calls'"
463
666
  );
@@ -514,12 +717,14 @@ function createGeminiExtension(config) {
514
717
  subsystem: "extension:gemini",
515
718
  duration,
516
719
  model: factoryModel,
517
- usage: result2.usage
720
+ usage: result2.usage,
721
+ request: contents,
722
+ content
518
723
  });
519
724
  return result2;
520
725
  } catch (error) {
521
726
  const duration = Date.now() - startTime;
522
- const rillError = error instanceof RuntimeError4 ? error : mapProviderError("Gemini", error, detectGeminiError);
727
+ const rillError = error instanceof RuntimeError5 ? error : mapProviderError("Gemini", error, detectGeminiError);
523
728
  emitExtensionEvent(ctx, {
524
729
  event: "gemini:error",
525
730
  subsystem: "extension:gemini",
@@ -547,7 +752,7 @@ function createGeminiExtension(config) {
547
752
  });
548
753
  const embedding = response.embeddings?.[0];
549
754
  if (!embedding || !embedding.values || embedding.values.length === 0) {
550
- throw new RuntimeError4(
755
+ throw new RuntimeError5(
551
756
  "RILL-R004",
552
757
  "Gemini: empty embedding returned"
553
758
  );
@@ -565,7 +770,7 @@ function createGeminiExtension(config) {
565
770
  return vector;
566
771
  } catch (error) {
567
772
  const duration = Date.now() - startTime;
568
- const rillError = error instanceof RuntimeError4 ? error : mapProviderError("Gemini", error, detectGeminiError);
773
+ const rillError = error instanceof RuntimeError5 ? error : mapProviderError("Gemini", error, detectGeminiError);
569
774
  emitExtensionEvent(ctx, {
570
775
  event: "gemini:error",
571
776
  subsystem: "extension:gemini",
@@ -596,14 +801,14 @@ function createGeminiExtension(config) {
596
801
  });
597
802
  const vectors = [];
598
803
  if (!response.embeddings || response.embeddings.length === 0) {
599
- throw new RuntimeError4(
804
+ throw new RuntimeError5(
600
805
  "RILL-R004",
601
806
  "Gemini: empty embeddings returned"
602
807
  );
603
808
  }
604
809
  for (const embedding of response.embeddings) {
605
810
  if (!embedding || !embedding.values || embedding.values.length === 0) {
606
- throw new RuntimeError4(
811
+ throw new RuntimeError5(
607
812
  "RILL-R004",
608
813
  "Gemini: empty embedding returned"
609
814
  );
@@ -626,7 +831,7 @@ function createGeminiExtension(config) {
626
831
  return vectors;
627
832
  } catch (error) {
628
833
  const duration = Date.now() - startTime;
629
- const rillError = error instanceof RuntimeError4 ? error : mapProviderError("Gemini", error, detectGeminiError);
834
+ const rillError = error instanceof RuntimeError5 ? error : mapProviderError("Gemini", error, detectGeminiError);
630
835
  emitExtensionEvent(ctx, {
631
836
  event: "gemini:error",
632
837
  subsystem: "extension:gemini",
@@ -651,39 +856,15 @@ function createGeminiExtension(config) {
651
856
  const prompt = args[0];
652
857
  const options = args[1] ?? {};
653
858
  if (prompt.trim().length === 0) {
654
- throw new RuntimeError4("RILL-R004", "prompt text cannot be empty");
859
+ throw new RuntimeError5("RILL-R004", "prompt text cannot be empty");
655
860
  }
656
- if (!("tools" in options) || !Array.isArray(options["tools"])) {
657
- throw new RuntimeError4(
861
+ if (!("tools" in options) || !isDict2(options["tools"])) {
862
+ throw new RuntimeError5(
658
863
  "RILL-R004",
659
864
  "tool_loop requires 'tools' option"
660
865
  );
661
866
  }
662
- const toolDescriptors = options["tools"];
663
- const toolsDict = {};
664
- for (const descriptor of toolDescriptors) {
665
- const name = typeof descriptor["name"] === "string" ? descriptor["name"] : null;
666
- if (!name) {
667
- throw new RuntimeError4(
668
- "RILL-R004",
669
- "tool descriptor missing name"
670
- );
671
- }
672
- const toolFnValue = descriptor["fn"];
673
- if (!toolFnValue) {
674
- throw new RuntimeError4(
675
- "RILL-R004",
676
- `tool '${name}' missing fn property`
677
- );
678
- }
679
- if (!isCallable2(toolFnValue)) {
680
- throw new RuntimeError4(
681
- "RILL-R004",
682
- `tool '${name}' fn must be callable`
683
- );
684
- }
685
- toolsDict[name] = toolFnValue;
686
- }
867
+ const toolsDict = options["tools"];
687
868
  const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
688
869
  const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
689
870
  const maxTurns = typeof options["max_turns"] === "number" ? options["max_turns"] : 10;
@@ -778,6 +959,21 @@ function createGeminiExtension(config) {
778
959
  };
779
960
  });
780
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
+ },
781
977
  // Format tool results into Gemini message format
782
978
  formatToolResult: (toolResults) => {
783
979
  const functionResponseParts = toolResults.map((tr) => ({
@@ -810,7 +1006,8 @@ function createGeminiExtension(config) {
810
1006
  ...data
811
1007
  });
812
1008
  },
813
- maxTurns
1009
+ maxTurns,
1010
+ ctx
814
1011
  );
815
1012
  const response = loopResult.response;
816
1013
  const content = response && typeof response === "object" && "text" in response ? response.text ?? "" : "";
@@ -832,12 +1029,14 @@ function createGeminiExtension(config) {
832
1029
  subsystem: "extension:gemini",
833
1030
  turns: result2.turns,
834
1031
  total_duration: duration,
835
- usage: result2.usage
1032
+ usage: result2.usage,
1033
+ request: contents,
1034
+ content
836
1035
  });
837
1036
  return result2;
838
1037
  } catch (error) {
839
1038
  const duration = Date.now() - startTime;
840
- const rillError = error instanceof RuntimeError4 ? error : mapProviderError("Gemini", error, detectGeminiError);
1039
+ const rillError = error instanceof RuntimeError5 ? error : mapProviderError("Gemini", error, detectGeminiError);
841
1040
  emitExtensionEvent(ctx, {
842
1041
  event: "gemini:error",
843
1042
  subsystem: "extension:gemini",
@@ -849,6 +1048,130 @@ function createGeminiExtension(config) {
849
1048
  },
850
1049
  description: "Execute tool-use loop with Gemini API",
851
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"
852
1175
  }
853
1176
  };
854
1177
  result.dispose = dispose;
@@ -857,7 +1180,19 @@ function createGeminiExtension(config) {
857
1180
 
858
1181
  // src/index.ts
859
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
+ };
860
1194
  export {
861
1195
  VERSION,
1196
+ configSchema,
862
1197
  createGeminiExtension
863
1198
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rcrsr/rill-ext-gemini",
3
- "version": "0.8.6",
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.6"
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.6",
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",
35
+ "url": "git+https://github.com/rcrsr/rill-ext.git",
36
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",