@rcrsr/rill-ext-gemini 0.16.0 → 0.18.1

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.js CHANGED
@@ -9,10 +9,11 @@ import {
9
9
  import {
10
10
  RuntimeError as RuntimeError6,
11
11
  emitExtensionEvent,
12
+ createRillStream,
12
13
  createVector,
13
14
  isVector,
14
- isDict as isDict2,
15
- rillTypeToTypeValue
15
+ structureToTypeValue,
16
+ toCallable
16
17
  } from "@rcrsr/rill";
17
18
 
18
19
  // ../../shared/ext-llm/dist/validation.js
@@ -52,7 +53,7 @@ function validateEmbedBatch(texts) {
52
53
  if (typeof item !== "string") {
53
54
  throw new RuntimeError("RILL-R001", "embed_batch requires list of strings");
54
55
  }
55
- if (item === "") {
56
+ if (item.trim() === "") {
56
57
  throw new RuntimeError("RILL-R001", `embed text cannot be empty at index ${i}`);
57
58
  }
58
59
  validated.push(item);
@@ -104,28 +105,29 @@ function mapRillType(rillType) {
104
105
  return jsonType;
105
106
  }
106
107
  function buildPropertyFromStructuralType(rillType) {
107
- if (rillType.type === "closure" || rillType.type === "tuple") {
108
- throw new RuntimeError3("RILL-R004", `unsupported type for JSON Schema: ${rillType.type}`);
108
+ if (rillType.kind === "closure" || rillType.kind === "tuple") {
109
+ throw new RuntimeError3("RILL-R004", `unsupported type for JSON Schema: ${rillType.kind}`);
109
110
  }
110
- if (rillType.type === "any") {
111
+ if (rillType.kind === "any") {
111
112
  return {};
112
113
  }
113
- if (rillType.type === "list") {
114
+ if (rillType.kind === "list") {
115
+ const listType = rillType;
114
116
  const property = { type: "array" };
115
- if (rillType.element !== void 0) {
116
- property.items = buildPropertyFromStructuralType(rillType.element);
117
+ if (listType.element !== void 0) {
118
+ property.items = buildPropertyFromStructuralType(listType.element);
117
119
  }
118
120
  return property;
119
121
  }
120
- if (rillType.type === "dict") {
122
+ if (rillType.kind === "dict") {
121
123
  return { type: "object" };
122
124
  }
123
- return { type: mapRillType(rillType.type) };
125
+ return { type: mapRillType(rillType.kind) };
124
126
  }
125
127
  function buildJsonSchemaFromStructuralType(type, params) {
126
128
  const properties = {};
127
129
  const required = [];
128
- if (type.type === "closure") {
130
+ if (type.kind === "closure") {
129
131
  const closureParams = type.params ?? [];
130
132
  for (let i = 0; i < closureParams.length; i++) {
131
133
  const fieldDef = closureParams[i];
@@ -241,7 +243,7 @@ async function executeToolCall(toolName, toolInput, tools, context) {
241
243
  return value !== void 0 ? value : void 0;
242
244
  });
243
245
  } else {
244
- args = [inputDict];
246
+ args = [];
245
247
  }
246
248
  return await invokeCallable(callable, args, context);
247
249
  }
@@ -326,7 +328,7 @@ function patchResponseToolCallNames(response, nameMap) {
326
328
  }
327
329
  }
328
330
  }
329
- async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent, maxTurns = 10, context) {
331
+ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent, maxTurns = 10, context, yieldChunk, signal) {
330
332
  if (tools === void 0) {
331
333
  throw new RuntimeError4("RILL-R004", "tools parameter is required");
332
334
  }
@@ -348,8 +350,8 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
348
350
  const params = callable.kind === "application" ? callable.params ?? [] : callable.kind === "script" ? callable.params : [];
349
351
  if (params.length > 0) {
350
352
  const closureType = {
351
- type: "closure",
352
- params: params.map((p2) => ({ name: p2.name, type: p2.type ?? { type: "any" } }))
353
+ kind: "closure",
354
+ params: params.map((p2) => ({ name: p2.name, type: p2.type ?? { kind: "any" } }))
353
355
  };
354
356
  const builtSchema = buildJsonSchemaFromStructuralType(closureType, [...params]);
355
357
  inputSchema = {
@@ -374,10 +376,20 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
374
376
  let currentMessages = [...messages];
375
377
  let turnCount = 0;
376
378
  while (turnCount < maxTurns) {
379
+ if (signal?.aborted) {
380
+ throw new RuntimeError4("RILL-R004", "tool_loop cancelled");
381
+ }
377
382
  turnCount++;
378
383
  let response;
379
384
  try {
380
- response = await callbacks.callAPI(currentMessages, providerTools);
385
+ if (yieldChunk !== void 0 && callbacks.callAPIStreaming !== void 0) {
386
+ const onTextDelta = (text) => {
387
+ yieldChunk({ type: "text_delta", text });
388
+ };
389
+ response = await callbacks.callAPIStreaming(currentMessages, providerTools, onTextDelta, signal);
390
+ } else {
391
+ response = await callbacks.callAPI(currentMessages, providerTools, signal);
392
+ }
381
393
  } catch (error) {
382
394
  const message = error instanceof Error ? error.message : "Unknown error";
383
395
  throw new RuntimeError4("RILL-R004", `Provider API error: ${message}`, void 0, { cause: error });
@@ -415,6 +427,13 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
415
427
  for (const toolCall of toolCalls) {
416
428
  const { id, name, input } = toolCall;
417
429
  emitEvent("tool_call", { tool_name: name, args: input });
430
+ if (yieldChunk !== void 0) {
431
+ yieldChunk({
432
+ type: "tool_call",
433
+ name,
434
+ args: input
435
+ });
436
+ }
418
437
  const toolStartTime = Date.now();
419
438
  try {
420
439
  const result = await executeToolCall(name, input, tools, context);
@@ -422,6 +441,9 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
422
441
  toolResults.push({ id, name, result });
423
442
  executedToolCalls.push({ name, result });
424
443
  consecutiveErrors = 0;
444
+ if (yieldChunk !== void 0) {
445
+ yieldChunk({ type: "tool_result", name, result });
446
+ }
425
447
  emitEvent("tool_result", { tool_name: name, duration });
426
448
  } catch (error) {
427
449
  const duration = Date.now() - toolStartTime;
@@ -474,6 +496,9 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
474
496
  turns: turnCount
475
497
  };
476
498
  }
499
+ function buildResponseMessages(inputMessages, assistantContent) {
500
+ return [...inputMessages, { role: "assistant", content: assistantContent }];
501
+ }
477
502
 
478
503
  // ../../shared/ext-param/dist/param.js
479
504
  import { RuntimeError as RuntimeError5 } from "@rcrsr/rill";
@@ -503,7 +528,7 @@ var p = {
503
528
  validateParamName(name);
504
529
  return {
505
530
  name,
506
- type: { type: "string" },
531
+ type: { kind: "string" },
507
532
  defaultValue: void 0,
508
533
  annotations: buildAnnotations(desc)
509
534
  };
@@ -520,7 +545,7 @@ var p = {
520
545
  validateParamName(name);
521
546
  return {
522
547
  name,
523
- type: { type: "number" },
548
+ type: { kind: "number" },
524
549
  defaultValue: def,
525
550
  annotations: buildAnnotations(desc)
526
551
  };
@@ -537,7 +562,7 @@ var p = {
537
562
  validateParamName(name);
538
563
  return {
539
564
  name,
540
- type: { type: "bool" },
565
+ type: { kind: "bool" },
541
566
  defaultValue: def,
542
567
  annotations: buildAnnotations(desc)
543
568
  };
@@ -553,7 +578,7 @@ var p = {
553
578
  */
554
579
  dict(name, desc, def, fields) {
555
580
  validateParamName(name);
556
- const type = fields !== void 0 ? { type: "dict", fields } : { type: "dict" };
581
+ const type = fields !== void 0 ? { kind: "dict", fields } : { kind: "dict" };
557
582
  return {
558
583
  name,
559
584
  type,
@@ -571,7 +596,7 @@ var p = {
571
596
  */
572
597
  list(name, itemType, desc) {
573
598
  validateParamName(name);
574
- const type = itemType !== void 0 ? { type: "list", element: itemType } : { type: "list" };
599
+ const type = itemType !== void 0 ? { kind: "list", element: itemType } : { kind: "list" };
575
600
  return {
576
601
  name,
577
602
  type,
@@ -590,7 +615,7 @@ var p = {
590
615
  validateParamName(name);
591
616
  return {
592
617
  name,
593
- type: { type: "closure" },
618
+ type: { kind: "closure" },
594
619
  defaultValue: void 0,
595
620
  annotations: buildAnnotations(desc)
596
621
  };
@@ -673,235 +698,312 @@ function createGeminiExtension(config) {
673
698
  console.warn(`Failed to cleanup Gemini SDK: ${message}`);
674
699
  }
675
700
  };
676
- const result = {
701
+ const fnDict = {
677
702
  // IR-4: gemini::message
678
703
  message: {
679
704
  params: [
680
705
  p.str("text"),
681
706
  p.dict("options", void 0, {}, {
682
- system: { type: { type: "string" }, defaultValue: "" },
683
- max_tokens: { type: { type: "number" }, defaultValue: 0 }
707
+ system: { type: { kind: "string" }, defaultValue: "" },
708
+ max_tokens: { type: { kind: "number" }, defaultValue: 0 }
684
709
  })
685
710
  ],
686
711
  fn: async (args, ctx) => {
687
- const startTime = Date.now();
688
- try {
689
- const text = args["text"];
690
- const options = args["options"] ?? {};
691
- if (text.trim().length === 0) {
692
- throw new RuntimeError6("RILL-R004", "prompt text cannot be empty");
712
+ const text = args["text"];
713
+ const options = args["options"] ?? {};
714
+ if (text.trim().length === 0) {
715
+ throw new RuntimeError6("RILL-R004", "prompt text cannot be empty");
716
+ }
717
+ const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
718
+ const maxTokens = typeof options["max_tokens"] === "number" && options["max_tokens"] > 0 ? options["max_tokens"] : factoryMaxTokens;
719
+ const contents = [
720
+ {
721
+ role: "user",
722
+ parts: [{ text }]
693
723
  }
694
- const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
695
- const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
696
- const contents = [
697
- {
698
- role: "user",
699
- parts: [{ text }]
724
+ ];
725
+ const apiConfig = {};
726
+ if (system !== void 0) {
727
+ apiConfig.systemInstruction = system;
728
+ }
729
+ if (maxTokens !== void 0) {
730
+ apiConfig.maxOutputTokens = maxTokens;
731
+ }
732
+ if (factoryTemperature !== void 0) {
733
+ apiConfig.temperature = factoryTemperature;
734
+ }
735
+ const collectedChunks = [];
736
+ let streamError;
737
+ const streamAbortController = new AbortController();
738
+ const messageStartTime = Date.now();
739
+ async function* streamChunks() {
740
+ try {
741
+ const stream = await client.models.generateContentStream({
742
+ model: factoryModel,
743
+ contents,
744
+ config: { ...apiConfig, abortSignal: streamAbortController.signal }
745
+ });
746
+ for await (const chunk of stream) {
747
+ const delta = chunk.text ?? "";
748
+ if (delta) {
749
+ collectedChunks.push(delta);
750
+ yield delta;
751
+ }
700
752
  }
701
- ];
702
- const apiConfig = {};
703
- if (system !== void 0) {
704
- apiConfig.systemInstruction = system;
705
- }
706
- if (maxTokens !== void 0) {
707
- apiConfig.maxOutputTokens = maxTokens;
753
+ } catch (error) {
754
+ streamError = error instanceof RuntimeError6 ? error : mapProviderError("Gemini", error, detectGeminiError);
755
+ const duration = Date.now() - messageStartTime;
756
+ emitExtensionEvent(ctx, {
757
+ event: "gemini:error",
758
+ subsystem: "extension:gemini",
759
+ error: streamError.message,
760
+ duration
761
+ });
762
+ throw streamError;
708
763
  }
709
- if (factoryTemperature !== void 0) {
710
- apiConfig.temperature = factoryTemperature;
764
+ }
765
+ const resolve = async () => {
766
+ if (streamError) {
767
+ throw streamError;
711
768
  }
712
- const response = await client.models.generateContent({
713
- model: factoryModel,
714
- contents,
715
- config: apiConfig
716
- });
717
- const content = response.text ?? "";
718
- const result2 = {
769
+ const duration = Date.now() - messageStartTime;
770
+ const content = collectedChunks.join("");
771
+ const result = {
719
772
  content,
720
773
  model: factoryModel,
721
774
  usage: {
722
775
  input: 0,
723
- // Gemini API doesn't always provide token counts
724
776
  output: 0
725
777
  },
726
778
  stop_reason: "stop",
727
779
  id: "",
728
- // Gemini API doesn't provide request IDs in the same way
729
- messages: [
730
- ...system ? [{ role: "system", content: system }] : [],
731
- { role: "user", content: text },
732
- { role: "assistant", content }
733
- ]
780
+ messages: buildResponseMessages(
781
+ [
782
+ ...system ? [{ role: "system", content: system }] : [],
783
+ { role: "user", content: text }
784
+ ],
785
+ content
786
+ )
734
787
  };
735
- const duration = Date.now() - startTime;
736
788
  emitExtensionEvent(ctx, {
737
789
  event: "gemini:message",
738
790
  subsystem: "extension:gemini",
739
791
  duration,
740
792
  model: factoryModel,
741
- usage: result2.usage,
793
+ usage: result.usage,
742
794
  request: contents,
743
795
  content
744
796
  });
745
- return result2;
746
- } catch (error) {
747
- const duration = Date.now() - startTime;
748
- const rillError = error instanceof RuntimeError6 ? error : mapProviderError("Gemini", error, detectGeminiError);
749
- emitExtensionEvent(ctx, {
750
- event: "gemini:error",
751
- subsystem: "extension:gemini",
752
- error: rillError.message,
753
- duration
754
- });
755
- throw rillError;
756
- }
797
+ return result;
798
+ };
799
+ const retTypeStructure = {
800
+ kind: "dict",
801
+ fields: {
802
+ content: { type: { kind: "string" } },
803
+ model: { type: { kind: "string" } },
804
+ usage: { type: { kind: "dict", fields: { input: { type: { kind: "number" } }, output: { type: { kind: "number" } } } } },
805
+ stop_reason: { type: { kind: "string" } },
806
+ id: { type: { kind: "string" } },
807
+ messages: { type: { kind: "list", element: { kind: "dict" } } }
808
+ }
809
+ };
810
+ return createRillStream({
811
+ chunks: streamChunks(),
812
+ resolve,
813
+ dispose: () => {
814
+ streamAbortController.abort();
815
+ },
816
+ chunkType: { kind: "string" },
817
+ retType: retTypeStructure
818
+ });
757
819
  },
758
820
  annotations: { description: "Send single message to Gemini API" },
759
- returnType: rillTypeToTypeValue({
760
- type: "dict",
761
- fields: {
762
- content: { type: { type: "string" } },
763
- model: { type: { type: "string" } },
764
- usage: { type: { type: "dict", fields: { input: { type: { type: "number" } }, output: { type: { type: "number" } } } } },
765
- stop_reason: { type: { type: "string" } },
766
- id: { type: { type: "string" } },
767
- messages: { type: { type: "list", element: { type: "dict" } } }
821
+ returnType: structureToTypeValue({
822
+ kind: "stream",
823
+ chunk: { kind: "string" },
824
+ ret: {
825
+ kind: "dict",
826
+ fields: {
827
+ content: { type: { kind: "string" } },
828
+ model: { type: { kind: "string" } },
829
+ usage: { type: { kind: "dict", fields: { input: { type: { kind: "number" } }, output: { type: { kind: "number" } } } } },
830
+ stop_reason: { type: { kind: "string" } },
831
+ id: { type: { kind: "string" } },
832
+ messages: { type: { kind: "list", element: { kind: "dict" } } }
833
+ }
768
834
  }
769
835
  })
770
836
  },
771
837
  // IR-5: gemini::messages
772
838
  messages: {
773
839
  params: [
774
- p.list("messages", { type: "dict", fields: { role: { type: { type: "string" } }, content: { type: { type: "string" } } } }),
840
+ p.list("messages", { kind: "dict", fields: { role: { type: { kind: "string" } }, content: { type: { kind: "string" } } } }),
775
841
  p.dict("options", void 0, {}, {
776
- system: { type: { type: "string" }, defaultValue: "" },
777
- max_tokens: { type: { type: "number" }, defaultValue: 0 }
842
+ system: { type: { kind: "string" }, defaultValue: "" },
843
+ max_tokens: { type: { kind: "number" }, defaultValue: 0 }
778
844
  })
779
845
  ],
780
846
  fn: async (args, ctx) => {
781
- const startTime = Date.now();
782
- try {
783
- const messages = args["messages"];
784
- const options = args["options"] ?? {};
785
- if (messages.length === 0) {
847
+ const inputMessages = args["messages"];
848
+ const options = args["options"] ?? {};
849
+ if (inputMessages.length === 0) {
850
+ throw new RuntimeError6(
851
+ "RILL-R004",
852
+ "messages list cannot be empty"
853
+ );
854
+ }
855
+ const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
856
+ const maxTokens = typeof options["max_tokens"] === "number" && options["max_tokens"] > 0 ? options["max_tokens"] : factoryMaxTokens;
857
+ const contents = [];
858
+ for (let i = 0; i < inputMessages.length; i++) {
859
+ const msg = inputMessages[i];
860
+ if (!msg || typeof msg !== "object" || !("role" in msg)) {
786
861
  throw new RuntimeError6(
787
862
  "RILL-R004",
788
- "messages list cannot be empty"
863
+ "message missing required 'role' field"
789
864
  );
790
865
  }
791
- const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
792
- const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
793
- const contents = [];
794
- for (let i = 0; i < messages.length; i++) {
795
- const msg = messages[i];
796
- if (!msg || typeof msg !== "object" || !("role" in msg)) {
866
+ const role = msg["role"];
867
+ if (role !== "user" && role !== "assistant" && role !== "tool") {
868
+ throw new RuntimeError6("RILL-R004", `invalid role '${role}'`);
869
+ }
870
+ if (role === "user" || role === "tool") {
871
+ if (!("content" in msg) || typeof msg["content"] !== "string") {
797
872
  throw new RuntimeError6(
798
873
  "RILL-R004",
799
- "message missing required 'role' field"
874
+ `${role} message requires 'content'`
800
875
  );
801
876
  }
802
- const role = msg["role"];
803
- if (role !== "user" && role !== "assistant" && role !== "tool") {
804
- throw new RuntimeError6("RILL-R004", `invalid role '${role}'`);
877
+ contents.push({
878
+ role: "user",
879
+ parts: [{ text: msg["content"] }]
880
+ });
881
+ } else if (role === "assistant") {
882
+ const hasContent = "content" in msg && msg["content"];
883
+ const hasToolCalls = "tool_calls" in msg && msg["tool_calls"];
884
+ if (!hasContent && !hasToolCalls) {
885
+ throw new RuntimeError6(
886
+ "RILL-R004",
887
+ "assistant message requires 'content' or 'tool_calls'"
888
+ );
805
889
  }
806
- if (role === "user" || role === "tool") {
807
- if (!("content" in msg) || typeof msg["content"] !== "string") {
808
- throw new RuntimeError6(
809
- "RILL-R004",
810
- `${role} message requires 'content'`
811
- );
812
- }
890
+ if (hasContent) {
813
891
  contents.push({
814
- role: "user",
892
+ role: "model",
815
893
  parts: [{ text: msg["content"] }]
816
894
  });
817
- } else if (role === "assistant") {
818
- const hasContent = "content" in msg && msg["content"];
819
- const hasToolCalls = "tool_calls" in msg && msg["tool_calls"];
820
- if (!hasContent && !hasToolCalls) {
821
- throw new RuntimeError6(
822
- "RILL-R004",
823
- "assistant message requires 'content' or 'tool_calls'"
824
- );
825
- }
826
- if (hasContent) {
827
- contents.push({
828
- role: "model",
829
- parts: [{ text: msg["content"] }]
830
- });
831
- }
832
895
  }
833
896
  }
834
- const apiConfig = {};
835
- if (system !== void 0) {
836
- apiConfig.systemInstruction = system;
837
- }
838
- if (maxTokens !== void 0) {
839
- apiConfig.maxOutputTokens = maxTokens;
897
+ }
898
+ const apiConfig = {};
899
+ if (system !== void 0) {
900
+ apiConfig.systemInstruction = system;
901
+ }
902
+ if (maxTokens !== void 0) {
903
+ apiConfig.maxOutputTokens = maxTokens;
904
+ }
905
+ if (factoryTemperature !== void 0) {
906
+ apiConfig.temperature = factoryTemperature;
907
+ }
908
+ const collectedChunks = [];
909
+ let streamError;
910
+ const streamAbortController = new AbortController();
911
+ const messagesStartTime = Date.now();
912
+ async function* streamChunks() {
913
+ try {
914
+ const stream = await client.models.generateContentStream({
915
+ model: factoryModel,
916
+ contents,
917
+ config: { ...apiConfig, abortSignal: streamAbortController.signal }
918
+ });
919
+ for await (const chunk of stream) {
920
+ const delta = chunk.text ?? "";
921
+ if (delta) {
922
+ collectedChunks.push(delta);
923
+ yield delta;
924
+ }
925
+ }
926
+ } catch (error) {
927
+ streamError = error instanceof RuntimeError6 ? error : mapProviderError("Gemini", error, detectGeminiError);
928
+ const duration = Date.now() - messagesStartTime;
929
+ emitExtensionEvent(ctx, {
930
+ event: "gemini:error",
931
+ subsystem: "extension:gemini",
932
+ error: streamError.message,
933
+ duration
934
+ });
935
+ throw streamError;
840
936
  }
841
- if (factoryTemperature !== void 0) {
842
- apiConfig.temperature = factoryTemperature;
937
+ }
938
+ const resolve = async () => {
939
+ if (streamError) {
940
+ throw streamError;
843
941
  }
844
- const response = await client.models.generateContent({
845
- model: factoryModel,
846
- contents,
847
- config: apiConfig
848
- });
849
- const content = response.text ?? "";
850
- const fullMessages = [
851
- ...messages.map((m) => {
852
- const normalized = { role: m["role"] };
853
- if ("content" in m) normalized["content"] = m["content"];
854
- if ("tool_calls" in m) normalized["tool_calls"] = m["tool_calls"];
855
- return normalized;
856
- }),
857
- { role: "assistant", content }
858
- ];
859
- const result2 = {
942
+ const duration = Date.now() - messagesStartTime;
943
+ const content = collectedChunks.join("");
944
+ const result = {
860
945
  content,
861
946
  model: factoryModel,
862
947
  usage: {
863
948
  input: 0,
864
- // Gemini API doesn't always provide token counts
865
949
  output: 0
866
950
  },
867
951
  stop_reason: "stop",
868
952
  id: "",
869
- // Gemini API doesn't provide request IDs in the same way
870
- messages: fullMessages
953
+ messages: buildResponseMessages(
954
+ inputMessages.map((m) => ({
955
+ role: m["role"],
956
+ content: m["content"] ?? ""
957
+ })),
958
+ content
959
+ )
871
960
  };
872
- const duration = Date.now() - startTime;
873
961
  emitExtensionEvent(ctx, {
874
962
  event: "gemini:messages",
875
963
  subsystem: "extension:gemini",
876
964
  duration,
877
965
  model: factoryModel,
878
- usage: result2.usage,
966
+ usage: result.usage,
879
967
  request: contents,
880
968
  content
881
969
  });
882
- return result2;
883
- } catch (error) {
884
- const duration = Date.now() - startTime;
885
- const rillError = error instanceof RuntimeError6 ? error : mapProviderError("Gemini", error, detectGeminiError);
886
- emitExtensionEvent(ctx, {
887
- event: "gemini:error",
888
- subsystem: "extension:gemini",
889
- error: rillError.message,
890
- duration
891
- });
892
- throw rillError;
893
- }
970
+ return result;
971
+ };
972
+ const retTypeStructure = {
973
+ kind: "dict",
974
+ fields: {
975
+ content: { type: { kind: "string" } },
976
+ model: { type: { kind: "string" } },
977
+ usage: { type: { kind: "dict", fields: { input: { type: { kind: "number" } }, output: { type: { kind: "number" } } } } },
978
+ stop_reason: { type: { kind: "string" } },
979
+ id: { type: { kind: "string" } },
980
+ messages: { type: { kind: "list", element: { kind: "dict" } } }
981
+ }
982
+ };
983
+ return createRillStream({
984
+ chunks: streamChunks(),
985
+ resolve,
986
+ dispose: () => {
987
+ streamAbortController.abort();
988
+ },
989
+ chunkType: { kind: "string" },
990
+ retType: retTypeStructure
991
+ });
894
992
  },
895
993
  annotations: { description: "Send multi-turn conversation to Gemini API" },
896
- returnType: rillTypeToTypeValue({
897
- type: "dict",
898
- fields: {
899
- content: { type: { type: "string" } },
900
- model: { type: { type: "string" } },
901
- usage: { type: { type: "dict", fields: { input: { type: { type: "number" } }, output: { type: { type: "number" } } } } },
902
- stop_reason: { type: { type: "string" } },
903
- id: { type: { type: "string" } },
904
- messages: { type: { type: "list", element: { type: "dict" } } }
994
+ returnType: structureToTypeValue({
995
+ kind: "stream",
996
+ chunk: { kind: "string" },
997
+ ret: {
998
+ kind: "dict",
999
+ fields: {
1000
+ content: { type: { kind: "string" } },
1001
+ model: { type: { kind: "string" } },
1002
+ usage: { type: { kind: "dict", fields: { input: { type: { kind: "number" } }, output: { type: { kind: "number" } } } } },
1003
+ stop_reason: { type: { kind: "string" } },
1004
+ id: { type: { kind: "string" } },
1005
+ messages: { type: { kind: "list", element: { kind: "dict" } } }
1006
+ }
905
1007
  }
906
1008
  })
907
1009
  },
@@ -949,7 +1051,7 @@ function createGeminiExtension(config) {
949
1051
  }
950
1052
  },
951
1053
  annotations: { description: "Generate embedding vector for text" },
952
- returnType: rillTypeToTypeValue({ type: "vector" })
1054
+ returnType: structureToTypeValue({ kind: "vector" })
953
1055
  },
954
1056
  // IR-7: gemini::embed_batch
955
1057
  embed_batch: {
@@ -1010,227 +1112,347 @@ function createGeminiExtension(config) {
1010
1112
  }
1011
1113
  },
1012
1114
  annotations: { description: "Generate embedding vectors for multiple texts" },
1013
- returnType: rillTypeToTypeValue({ type: "list", element: { type: "vector" } })
1115
+ returnType: structureToTypeValue({ kind: "list", element: { kind: "vector" } })
1014
1116
  },
1015
1117
  // IR-8: gemini::tool_loop
1016
1118
  tool_loop: {
1017
1119
  params: [
1018
1120
  p.str("prompt"),
1019
- p.dict("options", void 0, {}, {
1020
- tools: { type: { type: "dict" } },
1021
- system: { type: { type: "string" }, defaultValue: "" },
1022
- max_tokens: { type: { type: "number" }, defaultValue: 0 },
1023
- max_errors: { type: { type: "number" }, defaultValue: 3 },
1024
- max_turns: { type: { type: "number" }, defaultValue: 10 },
1025
- messages: { type: { type: "list", element: { type: "dict", fields: { role: { type: { type: "string" } }, content: { type: { type: "string" } } } } }, defaultValue: [] }
1121
+ {
1122
+ name: "tools",
1123
+ type: { kind: "dict", valueType: { kind: "closure" } },
1124
+ defaultValue: void 0,
1125
+ annotations: {}
1126
+ },
1127
+ p.dict("options", void 0, void 0, {
1128
+ system: { type: { kind: "string" }, defaultValue: "" },
1129
+ max_tokens: { type: { kind: "number" }, defaultValue: 0 },
1130
+ max_errors: { type: { kind: "number" }, defaultValue: 3 },
1131
+ max_turns: { type: { kind: "number" }, defaultValue: 10 },
1132
+ messages: { type: { kind: "list", element: { kind: "dict", fields: { role: { type: { kind: "string" } }, content: { type: { kind: "string" } } } } }, defaultValue: [] }
1026
1133
  })
1027
1134
  ],
1028
- fn: async (args, ctx) => {
1029
- const startTime = Date.now();
1030
- try {
1031
- const prompt = args["prompt"];
1032
- const options = args["options"] ?? {};
1033
- if (prompt.trim().length === 0) {
1034
- throw new RuntimeError6("RILL-R004", "prompt text cannot be empty");
1135
+ fn: (args, ctx) => {
1136
+ const prompt = args["prompt"];
1137
+ const toolsDict = args["tools"];
1138
+ const options = args["options"] ?? {};
1139
+ if (prompt.trim().length === 0) {
1140
+ throw new RuntimeError6("RILL-R004", "prompt text cannot be empty");
1141
+ }
1142
+ const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
1143
+ const maxTokens = typeof options["max_tokens"] === "number" && options["max_tokens"] > 0 ? options["max_tokens"] : factoryMaxTokens;
1144
+ const maxTurns = typeof options["max_turns"] === "number" ? options["max_turns"] : 10;
1145
+ const maxErrors = typeof options["max_errors"] === "number" ? options["max_errors"] : 3;
1146
+ const initialMessages = Array.isArray(options["messages"]) && options["messages"].length > 0 ? options["messages"] : [];
1147
+ const contents = [];
1148
+ for (const msg of initialMessages) {
1149
+ if (typeof msg === "object" && msg !== null && "role" in msg && "content" in msg) {
1150
+ const role = msg["role"];
1151
+ if (role === "user") {
1152
+ contents.push({
1153
+ role: "user",
1154
+ parts: [{ text: msg["content"] }]
1155
+ });
1156
+ } else if (role === "assistant") {
1157
+ contents.push({
1158
+ role: "model",
1159
+ parts: [{ text: msg["content"] }]
1160
+ });
1161
+ }
1035
1162
  }
1036
- if (!("tools" in options) || !isDict2(options["tools"])) {
1037
- throw new RuntimeError6(
1038
- "RILL-R004",
1039
- "tool_loop requires 'tools' option"
1040
- );
1163
+ }
1164
+ contents.push({
1165
+ role: "user",
1166
+ parts: [{ text: prompt }]
1167
+ });
1168
+ const apiConfig = {
1169
+ ...system !== void 0 && { systemInstruction: system },
1170
+ ...maxTokens !== void 0 && { maxOutputTokens: maxTokens },
1171
+ ...factoryTemperature !== void 0 && {
1172
+ temperature: factoryTemperature
1041
1173
  }
1042
- const toolsDict = options["tools"];
1043
- const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
1044
- const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
1045
- const maxTurns = typeof options["max_turns"] === "number" ? options["max_turns"] : 10;
1046
- const maxErrors = typeof options["max_errors"] === "number" ? options["max_errors"] : 3;
1047
- const initialMessages = Array.isArray(options["messages"]) && options["messages"].length > 0 ? options["messages"] : [];
1048
- const contents = [];
1049
- for (const msg of initialMessages) {
1050
- if (typeof msg === "object" && msg !== null && "role" in msg && "content" in msg) {
1051
- const role = msg["role"];
1052
- if (role === "user") {
1053
- contents.push({
1054
- role: "user",
1055
- parts: [{ text: msg["content"] }]
1056
- });
1057
- } else if (role === "assistant") {
1058
- contents.push({
1059
- role: "model",
1060
- parts: [{ text: msg["content"] }]
1061
- });
1062
- }
1174
+ };
1175
+ const buildToolDeclarations = (toolDefs) => {
1176
+ return toolDefs.map((def) => {
1177
+ const properties = {};
1178
+ for (const [propName, propDef] of Object.entries(
1179
+ def.input_schema.properties
1180
+ )) {
1181
+ const prop = propDef;
1182
+ const propType = prop["type"];
1183
+ let schemaType = Type.STRING;
1184
+ if (propType === "number") schemaType = Type.NUMBER;
1185
+ if (propType === "boolean") schemaType = Type.BOOLEAN;
1186
+ if (propType === "integer") schemaType = Type.INTEGER;
1187
+ if (propType === "array") schemaType = Type.ARRAY;
1188
+ if (propType === "object") schemaType = Type.OBJECT;
1189
+ properties[propName] = {
1190
+ type: schemaType,
1191
+ description: prop["description"] ?? ""
1192
+ };
1063
1193
  }
1064
- }
1065
- contents.push({
1066
- role: "user",
1067
- parts: [{ text: prompt }]
1194
+ return {
1195
+ name: def.name,
1196
+ description: def.description,
1197
+ parameters: {
1198
+ type: Type.OBJECT,
1199
+ properties,
1200
+ required: def.input_schema.required
1201
+ }
1202
+ };
1068
1203
  });
1069
- const callbacks = {
1070
- // Build Gemini FunctionDeclaration format from tool definitions
1071
- buildTools: (toolDefs) => {
1072
- return toolDefs.map((def) => {
1073
- const properties = {};
1074
- for (const [propName, propDef] of Object.entries(
1075
- def.input_schema.properties
1076
- )) {
1077
- const prop = propDef;
1078
- const propType = prop["type"];
1079
- let schemaType = Type.STRING;
1080
- if (propType === "number") schemaType = Type.NUMBER;
1081
- if (propType === "boolean") schemaType = Type.BOOLEAN;
1082
- if (propType === "integer") schemaType = Type.INTEGER;
1083
- if (propType === "array") schemaType = Type.ARRAY;
1084
- if (propType === "object") schemaType = Type.OBJECT;
1085
- properties[propName] = {
1086
- type: schemaType,
1087
- description: prop["description"] ?? ""
1088
- };
1089
- }
1090
- return {
1091
- name: def.name,
1092
- description: def.description,
1093
- parameters: {
1094
- type: Type.OBJECT,
1095
- properties,
1096
- required: def.input_schema.required
1097
- }
1098
- };
1099
- });
1100
- },
1101
- // Call Gemini API
1102
- callAPI: async (msgs, tools) => {
1103
- const apiConfig = {
1104
- ...system !== void 0 && { systemInstruction: system },
1105
- ...maxTokens !== void 0 && { maxOutputTokens: maxTokens },
1106
- ...factoryTemperature !== void 0 && {
1107
- temperature: factoryTemperature
1108
- },
1204
+ };
1205
+ let resolveNext;
1206
+ const chunkQueue = [];
1207
+ let streamDone = false;
1208
+ let streamError;
1209
+ let loopResultHolder;
1210
+ const accumulatedTextDeltas = [];
1211
+ const callbacks = {
1212
+ buildTools: buildToolDeclarations,
1213
+ callAPI: async (msgs, tools, signal) => {
1214
+ return await client.models.generateContent({
1215
+ model: factoryModel,
1216
+ contents: msgs,
1217
+ config: {
1218
+ ...apiConfig,
1219
+ ...signal !== void 0 && { abortSignal: signal },
1109
1220
  tools: [
1110
1221
  { functionDeclarations: tools }
1111
1222
  ]
1112
- };
1113
- return await client.models.generateContent({
1114
- model: factoryModel,
1115
- contents: msgs,
1116
- config: apiConfig
1117
- });
1118
- },
1119
- // Extract tool calls from Gemini response
1120
- extractToolCalls: (response2) => {
1121
- if (!response2 || typeof response2 !== "object" || !("functionCalls" in response2)) {
1122
- return null;
1123
1223
  }
1124
- const functionCalls = response2.functionCalls;
1125
- if (!functionCalls || functionCalls.length === 0) {
1126
- return null;
1127
- }
1128
- return functionCalls.map((fc) => {
1129
- const call = fc;
1130
- return {
1131
- id: call.id ?? "",
1132
- name: call.name ?? "",
1133
- input: call.args ?? {}
1134
- };
1135
- });
1136
- },
1137
- // Extract the model's content from Gemini response for conversation history
1138
- formatAssistantMessage: (response2) => {
1139
- if (!response2 || typeof response2 !== "object" || !("candidates" in response2)) {
1140
- return null;
1141
- }
1142
- const candidates = response2.candidates;
1143
- if (!Array.isArray(candidates) || candidates.length === 0) {
1144
- return null;
1224
+ });
1225
+ },
1226
+ // IR-3: Streaming API path
1227
+ callAPIStreaming: async (msgs, tools, onTextDelta, signal) => {
1228
+ const stream = await client.models.generateContentStream({
1229
+ model: factoryModel,
1230
+ contents: msgs,
1231
+ config: {
1232
+ ...apiConfig,
1233
+ ...signal !== void 0 && { abortSignal: signal },
1234
+ tools: [
1235
+ { functionDeclarations: tools }
1236
+ ]
1145
1237
  }
1146
- const candidate = candidates[0];
1147
- if (!candidate || typeof candidate !== "object" || !("content" in candidate)) {
1148
- return null;
1238
+ });
1239
+ let lastChunk;
1240
+ for await (const chunk of stream) {
1241
+ lastChunk = chunk;
1242
+ const delta = chunk.text ?? "";
1243
+ if (delta) {
1244
+ onTextDelta(delta);
1149
1245
  }
1150
- return candidate.content;
1151
- },
1152
- // Format tool results into Gemini message format
1153
- formatToolResult: (toolResults) => {
1154
- const functionResponseParts = toolResults.map((tr) => ({
1155
- functionResponse: {
1156
- name: tr.name,
1157
- response: {
1158
- result: tr.error ? `Error: ${tr.error}` : typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result)
1159
- }
1160
- }
1161
- }));
1246
+ }
1247
+ return lastChunk;
1248
+ },
1249
+ extractToolCalls: (response) => {
1250
+ if (!response || typeof response !== "object" || !("functionCalls" in response)) {
1251
+ return null;
1252
+ }
1253
+ const functionCalls = response.functionCalls;
1254
+ if (!functionCalls || functionCalls.length === 0) {
1255
+ return null;
1256
+ }
1257
+ return functionCalls.map((fc) => {
1258
+ const call = fc;
1162
1259
  return {
1163
- role: "user",
1164
- parts: functionResponseParts
1260
+ id: call.id ?? "",
1261
+ name: call.name ?? "",
1262
+ input: call.args ?? {}
1165
1263
  };
1264
+ });
1265
+ },
1266
+ formatAssistantMessage: (response) => {
1267
+ if (!response || typeof response !== "object" || !("candidates" in response)) {
1268
+ return null;
1166
1269
  }
1167
- };
1168
- const loopResult = await executeToolLoop(
1169
- contents,
1170
- toolsDict,
1171
- maxErrors,
1172
- callbacks,
1173
- (event, data) => {
1174
- const eventMap = {
1175
- tool_call: "gemini:tool_call",
1176
- tool_result: "gemini:tool_result"
1177
- };
1270
+ const candidates = response.candidates;
1271
+ if (!Array.isArray(candidates) || candidates.length === 0) {
1272
+ return null;
1273
+ }
1274
+ const candidate = candidates[0];
1275
+ if (!candidate || typeof candidate !== "object" || !("content" in candidate)) {
1276
+ return null;
1277
+ }
1278
+ return candidate.content;
1279
+ },
1280
+ formatToolResult: (toolResults) => {
1281
+ const functionResponseParts = toolResults.map((tr) => ({
1282
+ functionResponse: {
1283
+ name: tr.name,
1284
+ response: {
1285
+ result: tr.error ? `Error: ${tr.error}` : typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result)
1286
+ }
1287
+ }
1288
+ }));
1289
+ return {
1290
+ role: "user",
1291
+ parts: functionResponseParts
1292
+ };
1293
+ }
1294
+ };
1295
+ const toolLoopAbortController = new AbortController();
1296
+ const loopPromise = executeToolLoop(
1297
+ contents,
1298
+ toolsDict,
1299
+ maxErrors,
1300
+ callbacks,
1301
+ (event, data) => {
1302
+ const eventMap = {
1303
+ tool_call: "gemini:tool_call",
1304
+ tool_result: "gemini:tool_result"
1305
+ };
1306
+ emitExtensionEvent(ctx, {
1307
+ event: eventMap[event] || event,
1308
+ subsystem: "extension:gemini",
1309
+ ...data
1310
+ });
1311
+ },
1312
+ maxTurns,
1313
+ ctx,
1314
+ // yieldChunk — called from executeToolLoop for each text_delta, tool_call,
1315
+ // or tool_result. Buffers the chunk and signals the generator to wake.
1316
+ // AC-16: Also accumulate text_delta text for partial resolve on disconnect.
1317
+ (chunk) => {
1318
+ const chunkRecord = chunk;
1319
+ if (chunkRecord["type"] === "text_delta" && typeof chunkRecord["text"] === "string") {
1320
+ accumulatedTextDeltas.push(chunkRecord["text"]);
1321
+ }
1322
+ chunkQueue.push(chunk);
1323
+ if (resolveNext) {
1324
+ const r = resolveNext;
1325
+ resolveNext = void 0;
1326
+ r();
1327
+ }
1328
+ },
1329
+ toolLoopAbortController.signal
1330
+ ).then((result) => {
1331
+ loopResultHolder = result;
1332
+ streamDone = true;
1333
+ if (resolveNext) {
1334
+ const r = resolveNext;
1335
+ resolveNext = void 0;
1336
+ r();
1337
+ }
1338
+ }).catch((error) => {
1339
+ streamError = error instanceof RuntimeError6 ? error : mapProviderError("Gemini", error, detectGeminiError);
1340
+ streamDone = true;
1341
+ if (resolveNext) {
1342
+ const r = resolveNext;
1343
+ resolveNext = void 0;
1344
+ r();
1345
+ }
1346
+ });
1347
+ async function* streamGenerator() {
1348
+ while (true) {
1349
+ while (chunkQueue.length > 0) {
1350
+ yield chunkQueue.shift();
1351
+ }
1352
+ if (streamDone) {
1353
+ if (streamError) throw streamError;
1354
+ break;
1355
+ }
1356
+ await new Promise((resolve2) => {
1357
+ resolveNext = resolve2;
1358
+ });
1359
+ }
1360
+ }
1361
+ const inputMessages = [
1362
+ ...initialMessages.map((m) => ({
1363
+ role: m["role"],
1364
+ content: m["content"] ?? ""
1365
+ })),
1366
+ { role: "user", content: prompt }
1367
+ ];
1368
+ const retTypeStructure = {
1369
+ kind: "dict",
1370
+ fields: {
1371
+ content: { type: { kind: "string" } },
1372
+ model: { type: { kind: "string" } },
1373
+ usage: { type: { kind: "dict", fields: { input: { type: { kind: "number" } }, output: { type: { kind: "number" } } } } },
1374
+ stop_reason: { type: { kind: "string" } },
1375
+ turns: { type: { kind: "number" } },
1376
+ messages: { type: { kind: "list", element: { kind: "dict" } } }
1377
+ }
1378
+ };
1379
+ const resolve = async () => {
1380
+ const startTime = Date.now();
1381
+ await loopPromise;
1382
+ if (streamError) {
1383
+ if (accumulatedTextDeltas.length > 0) {
1384
+ const partialContent = accumulatedTextDeltas.join("");
1178
1385
  emitExtensionEvent(ctx, {
1179
- event: eventMap[event] || event,
1386
+ event: "gemini:error",
1180
1387
  subsystem: "extension:gemini",
1181
- ...data
1388
+ error: streamError.message,
1389
+ duration: Date.now()
1182
1390
  });
1183
- },
1184
- maxTurns,
1185
- ctx
1186
- );
1187
- const response = loopResult.response;
1391
+ return {
1392
+ content: partialContent,
1393
+ model: factoryModel,
1394
+ usage: { input: 0, output: 0 },
1395
+ stop_reason: "error",
1396
+ turns: 0,
1397
+ messages: buildResponseMessages(inputMessages, partialContent)
1398
+ };
1399
+ }
1400
+ const duration = Date.now();
1401
+ emitExtensionEvent(ctx, {
1402
+ event: "gemini:error",
1403
+ subsystem: "extension:gemini",
1404
+ error: streamError.message,
1405
+ duration
1406
+ });
1407
+ throw streamError;
1408
+ }
1409
+ const result = loopResultHolder;
1410
+ const response = result.response;
1188
1411
  const content = response && typeof response === "object" && "text" in response ? response.text ?? "" : "";
1189
- const result2 = {
1412
+ const resolvedResult = {
1190
1413
  content,
1191
1414
  model: factoryModel,
1192
- usage: loopResult.totalTokens,
1415
+ usage: result.totalTokens,
1193
1416
  stop_reason: response ? "stop" : "max_turns",
1194
- turns: loopResult.turns,
1195
- messages: [
1196
- ...initialMessages,
1197
- { role: "user", content: prompt },
1198
- { role: "assistant", content }
1199
- ]
1417
+ turns: result.turns,
1418
+ messages: response ? buildResponseMessages(inputMessages, content) : inputMessages
1200
1419
  };
1201
- const duration = Date.now() - startTime;
1420
+ const total_duration = Date.now() - startTime;
1202
1421
  emitExtensionEvent(ctx, {
1203
1422
  event: "gemini:tool_loop",
1204
1423
  subsystem: "extension:gemini",
1205
- turns: result2.turns,
1206
- total_duration: duration,
1207
- usage: result2.usage,
1424
+ turns: resolvedResult.turns,
1425
+ total_duration,
1426
+ usage: resolvedResult.usage,
1208
1427
  request: contents,
1209
1428
  content
1210
1429
  });
1211
- return result2;
1212
- } catch (error) {
1213
- const duration = Date.now() - startTime;
1214
- const rillError = error instanceof RuntimeError6 ? error : mapProviderError("Gemini", error, detectGeminiError);
1215
- emitExtensionEvent(ctx, {
1216
- event: "gemini:error",
1217
- subsystem: "extension:gemini",
1218
- error: rillError.message,
1219
- duration
1220
- });
1221
- throw rillError;
1222
- }
1430
+ return resolvedResult;
1431
+ };
1432
+ return createRillStream({
1433
+ chunks: streamGenerator(),
1434
+ resolve,
1435
+ dispose: () => {
1436
+ toolLoopAbortController.abort();
1437
+ },
1438
+ chunkType: { kind: "dict" },
1439
+ retType: retTypeStructure
1440
+ });
1223
1441
  },
1224
1442
  annotations: { description: "Execute tool-use loop with Gemini API" },
1225
- returnType: rillTypeToTypeValue({
1226
- type: "dict",
1227
- fields: {
1228
- content: { type: { type: "string" } },
1229
- model: { type: { type: "string" } },
1230
- usage: { type: { type: "dict", fields: { input: { type: { type: "number" } }, output: { type: { type: "number" } } } } },
1231
- stop_reason: { type: { type: "string" } },
1232
- turns: { type: { type: "number" } },
1233
- messages: { type: { type: "list", element: { type: "dict" } } }
1443
+ returnType: structureToTypeValue({
1444
+ kind: "stream",
1445
+ chunk: { kind: "dict" },
1446
+ ret: {
1447
+ kind: "dict",
1448
+ fields: {
1449
+ content: { type: { kind: "string" } },
1450
+ model: { type: { kind: "string" } },
1451
+ usage: { type: { kind: "dict", fields: { input: { type: { kind: "number" } }, output: { type: { kind: "number" } } } } },
1452
+ stop_reason: { type: { kind: "string" } },
1453
+ turns: { type: { kind: "number" } },
1454
+ messages: { type: { kind: "list", element: { kind: "dict" } } }
1455
+ }
1234
1456
  }
1235
1457
  })
1236
1458
  },
@@ -1239,10 +1461,10 @@ function createGeminiExtension(config) {
1239
1461
  params: [
1240
1462
  p.str("prompt"),
1241
1463
  p.dict("options", void 0, {}, {
1242
- schema: { type: { type: "dict" } },
1243
- system: { type: { type: "string" }, defaultValue: "" },
1244
- max_tokens: { type: { type: "number" }, defaultValue: 0 },
1245
- messages: { type: { type: "list", element: { type: "dict", fields: { role: { type: { type: "string" } }, content: { type: { type: "string" } } } } }, defaultValue: [] }
1464
+ schema: { type: { kind: "dict" } },
1465
+ system: { type: { kind: "string" }, defaultValue: "" },
1466
+ max_tokens: { type: { kind: "number" }, defaultValue: 0 },
1467
+ messages: { type: { kind: "list", element: { kind: "dict", fields: { role: { type: { kind: "string" } }, content: { type: { kind: "string" } } } } }, defaultValue: [] }
1246
1468
  })
1247
1469
  ],
1248
1470
  fn: async (args, ctx) => {
@@ -1268,7 +1490,7 @@ function createGeminiExtension(config) {
1268
1490
  required: jsonSchema.required
1269
1491
  };
1270
1492
  const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
1271
- const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
1493
+ const maxTokens = typeof options["max_tokens"] === "number" && options["max_tokens"] > 0 ? options["max_tokens"] : factoryMaxTokens;
1272
1494
  const contents = [];
1273
1495
  if ("messages" in options && Array.isArray(options["messages"])) {
1274
1496
  const prependedMessages = options["messages"];
@@ -1361,21 +1583,28 @@ function createGeminiExtension(config) {
1361
1583
  }
1362
1584
  },
1363
1585
  annotations: { description: "Generate structured output from Gemini API" },
1364
- returnType: rillTypeToTypeValue({
1365
- type: "dict",
1586
+ returnType: structureToTypeValue({
1587
+ kind: "dict",
1366
1588
  fields: {
1367
- data: { type: { type: "any" } },
1368
- raw: { type: { type: "string" } },
1369
- model: { type: { type: "string" } },
1370
- usage: { type: { type: "dict", fields: { input: { type: { type: "number" } }, output: { type: { type: "number" } } } } },
1371
- stop_reason: { type: { type: "string" } },
1372
- id: { type: { type: "string" } }
1589
+ data: { type: { kind: "any" } },
1590
+ raw: { type: { kind: "string" } },
1591
+ model: { type: { kind: "string" } },
1592
+ usage: { type: { kind: "dict", fields: { input: { type: { kind: "number" } }, output: { type: { kind: "number" } } } } },
1593
+ stop_reason: { type: { kind: "string" } },
1594
+ id: { type: { kind: "string" } }
1373
1595
  }
1374
1596
  })
1375
1597
  }
1376
1598
  };
1377
- result.dispose = dispose;
1378
- return result;
1599
+ const callableDict = {
1600
+ message: toCallable(fnDict.message),
1601
+ messages: toCallable(fnDict.messages),
1602
+ embed: toCallable(fnDict.embed),
1603
+ embed_batch: toCallable(fnDict.embed_batch),
1604
+ tool_loop: toCallable(fnDict.tool_loop),
1605
+ generate: toCallable(fnDict.generate)
1606
+ };
1607
+ return { value: callableDict, dispose };
1379
1608
  }
1380
1609
 
1381
1610
  // src/index.ts