@rcrsr/rill-ext-gemini 0.8.3 → 0.8.5

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