react-native-ai-core 0.1.0 → 0.2.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.
@@ -0,0 +1,729 @@
1
+ "use strict";
2
+
3
+ import { Platform } from 'react-native';
4
+ import { z } from 'zod';
5
+ import NativeAiCore from "./NativeAiCore.js";
6
+ export class StructuredOutputError extends Error {
7
+ constructor(message, rawResponse, issues = []) {
8
+ super(message);
9
+ this.name = 'StructuredOutputError';
10
+ this.rawResponse = rawResponse;
11
+ this.issues = issues;
12
+ }
13
+ }
14
+ const STRUCTURED_PROMPT_BUDGET = 2600;
15
+ const STRUCTURED_REPAIR_RESPONSE_BUDGET = 1200;
16
+ const STRUCTURED_ISSUES_BUDGET = 600;
17
+ const STRUCTURED_CONTINUATION_BUDGET = 1400;
18
+ const DEFAULT_MAX_STRUCTURED_CONTINUATIONS = 8;
19
+ const CONTINUATION_OVERLAP_WINDOW = 160;
20
+ const DEFAULT_STRUCTURED_TIMEOUT_MS = 300000; // 5 min default
21
+ const QUOTA_ERROR_CODE = 9;
22
+ const QUOTA_RETRY_DELAY_MS = 1800;
23
+ const MAX_QUOTA_RETRIES = 2;
24
+ function sleep(ms) {
25
+ return new Promise(resolve => setTimeout(resolve, ms));
26
+ }
27
+ function toErrorMessage(error) {
28
+ if (error instanceof Error) return error.message;
29
+ if (typeof error === 'string') return error;
30
+ try {
31
+ return JSON.stringify(error);
32
+ } catch {
33
+ return String(error);
34
+ }
35
+ }
36
+ function readNumericErrorCode(error) {
37
+ if (typeof error === 'object' && error !== null) {
38
+ const directCode = error.errorCode;
39
+ if (typeof directCode === 'number') return directCode;
40
+ const code = error.code;
41
+ if (typeof code === 'number') return code;
42
+ if (typeof code === 'string') {
43
+ const parsed = Number.parseInt(code, 10);
44
+ if (Number.isFinite(parsed)) return parsed;
45
+ }
46
+ }
47
+ const message = toErrorMessage(error);
48
+ const match = message.match(/error\s*code\s*[:=]?\s*(\d+)/i);
49
+ if (!match) return null;
50
+ const capturedCode = match[1];
51
+ if (!capturedCode) return null;
52
+ const parsed = Number.parseInt(capturedCode, 10);
53
+ return Number.isFinite(parsed) ? parsed : null;
54
+ }
55
+ function isQuotaError(error) {
56
+ const code = readNumericErrorCode(error);
57
+ if (code === QUOTA_ERROR_CODE) return true;
58
+ const message = toErrorMessage(error).toLowerCase();
59
+ return message.includes('out of quota') || message.includes('quota exceeded') || message.includes('error code: 9');
60
+ }
61
+ function truncateStart(text, maxChars) {
62
+ if (text.length <= maxChars) return text;
63
+ return text.slice(text.length - maxChars);
64
+ }
65
+ function truncateEnd(text, maxChars) {
66
+ if (text.length <= maxChars) return text;
67
+ return text.slice(0, maxChars);
68
+ }
69
+ function compactWhitespace(text) {
70
+ return text.replace(/\s+/g, ' ').trim();
71
+ }
72
+ function stringifyInput(value, maxChars) {
73
+ return truncateStart(JSON.stringify(value, null, 2), maxChars);
74
+ }
75
+ function fitStructuredPrompt(parts) {
76
+ return truncateStart(parts.filter(Boolean).join('\n'), STRUCTURED_PROMPT_BUDGET);
77
+ }
78
+ function fitContinuationPrompt(parts) {
79
+ return truncateStart(parts.filter(Boolean).join('\n'), STRUCTURED_CONTINUATION_BUDGET);
80
+ }
81
+ function zodTypeToDescription(schema) {
82
+ if (schema instanceof z.ZodString) return 'string';
83
+ if (schema instanceof z.ZodNumber) return 'number';
84
+ if (schema instanceof z.ZodBoolean) return 'boolean';
85
+ if (schema instanceof z.ZodNull) return 'null';
86
+ if (schema instanceof z.ZodEnum) {
87
+ return `enum(${schema.options.map(value => JSON.stringify(value)).join(', ')})`;
88
+ }
89
+ if (schema instanceof z.ZodLiteral) {
90
+ return `literal(${JSON.stringify(schema.value)})`;
91
+ }
92
+ if (schema instanceof z.ZodArray) {
93
+ return `Array<${zodTypeToDescription(schema.element)}>`;
94
+ }
95
+ if (schema instanceof z.ZodOptional) {
96
+ return `${zodTypeToDescription(schema.unwrap())} | undefined`;
97
+ }
98
+ if (schema instanceof z.ZodNullable) {
99
+ return `${zodTypeToDescription(schema.unwrap())} | null`;
100
+ }
101
+ if (schema instanceof z.ZodUnion) {
102
+ return schema.options.map(option => zodTypeToDescription(option)).join(' | ');
103
+ }
104
+ if (schema instanceof z.ZodObject) {
105
+ const shape = schema.shape;
106
+ const entries = Object.entries(shape).map(([key, value]) => ` ${key}: ${zodTypeToDescription(value)}`);
107
+ return `{
108
+ ${entries.join(',\n')}
109
+ }`;
110
+ }
111
+ return 'unknown';
112
+ }
113
+ function formatIssues(error) {
114
+ return error.issues.map(issue => ({
115
+ path: issue.path.join('.') || '$',
116
+ message: issue.message
117
+ }));
118
+ }
119
+ function extractFencedJson(text) {
120
+ const match = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
121
+ return match?.[1]?.trim() ?? null;
122
+ }
123
+ function extractBalancedJson(text) {
124
+ let start = -1;
125
+ let depth = 0;
126
+ let inString = false;
127
+ let escaped = false;
128
+ for (let index = 0; index < text.length; index++) {
129
+ const char = text[index];
130
+ if (inString) {
131
+ if (escaped) {
132
+ escaped = false;
133
+ } else if (char === '\\') {
134
+ escaped = true;
135
+ } else if (char === '"') {
136
+ inString = false;
137
+ }
138
+ continue;
139
+ }
140
+ if (char === '"') {
141
+ inString = true;
142
+ continue;
143
+ }
144
+ if (char === '{' || char === '[') {
145
+ if (depth === 0) start = index;
146
+ depth++;
147
+ continue;
148
+ }
149
+ if (char === '}' || char === ']') {
150
+ depth--;
151
+ if (depth === 0 && start >= 0) {
152
+ return text.slice(start, index + 1);
153
+ }
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+ function extractJsonPayload(raw) {
159
+ const trimmed = raw.trim();
160
+ const fenced = extractFencedJson(trimmed);
161
+ if (fenced) return fenced;
162
+ const balanced = extractBalancedJson(trimmed);
163
+ if (balanced) return balanced;
164
+ return trimmed;
165
+ }
166
+ function stripCodeFences(text) {
167
+ return text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '');
168
+ }
169
+ function mergeStructuredFragments(existing, incoming) {
170
+ const normalizedIncoming = stripCodeFences(incoming).trimStart();
171
+ if (!normalizedIncoming) return existing;
172
+ if (!existing) return normalizedIncoming;
173
+ const maxOverlap = Math.min(CONTINUATION_OVERLAP_WINDOW, existing.length, normalizedIncoming.length);
174
+ for (let size = maxOverlap; size > 0; size--) {
175
+ if (existing.endsWith(normalizedIncoming.slice(0, size))) {
176
+ return existing + normalizedIncoming.slice(size);
177
+ }
178
+ }
179
+ return existing + normalizedIncoming;
180
+ }
181
+ function looksLikeIncompleteJson(text) {
182
+ const candidate = extractJsonPayload(text).trim();
183
+ if (!candidate) return false;
184
+ let depth = 0;
185
+ let inString = false;
186
+ let escaped = false;
187
+ for (let index = 0; index < candidate.length; index++) {
188
+ const char = candidate[index];
189
+ if (inString) {
190
+ if (escaped) {
191
+ escaped = false;
192
+ } else if (char === '\\') {
193
+ escaped = true;
194
+ } else if (char === '"') {
195
+ inString = false;
196
+ }
197
+ continue;
198
+ }
199
+ if (char === '"') {
200
+ inString = true;
201
+ continue;
202
+ }
203
+ if (char === '{' || char === '[') {
204
+ depth++;
205
+ continue;
206
+ }
207
+ if (char === '}' || char === ']') {
208
+ depth = Math.max(0, depth - 1);
209
+ }
210
+ }
211
+ const lastChar = candidate[candidate.length - 1];
212
+ return inString || depth > 0 || lastChar === ',' || lastChar === ':';
213
+ }
214
+ function buildStructuredContinuationPrompt(prompt, outputSchema, partialResponse, input) {
215
+ const schemaDescription = truncateEnd(zodTypeToDescription(outputSchema), 700);
216
+ const normalizedPrompt = truncateEnd(compactWhitespace(prompt), 500);
217
+ const serializedInput = input === undefined ? '' : `Input JSON:\n${stringifyInput(input, 500)}`;
218
+ const partialTail = truncateStart(stripCodeFences(partialResponse), 900);
219
+ return fitContinuationPrompt(['Continue the same JSON value from the next character.', 'Do not restart, repeat, explain, summarize, or wrap in markdown.', 'Output only the missing suffix needed to complete the same valid JSON.', 'Schema:', schemaDescription, `Task:\n${normalizedPrompt}`, serializedInput, 'Current partial JSON tail:', partialTail]);
220
+ }
221
+ function buildJsonParseIssues(error) {
222
+ if (error instanceof z.ZodError) {
223
+ return formatIssues(error);
224
+ }
225
+ if (error instanceof Error) {
226
+ return [{
227
+ path: '$',
228
+ message: error.message
229
+ }];
230
+ }
231
+ return [{
232
+ path: '$',
233
+ message: 'Unknown structured output error'
234
+ }];
235
+ }
236
+ async function generateStructuredRawResponse(prompt, output, input, maxContinuations, timeoutMs) {
237
+ let combined = await generateStatelessWithQuotaRetry(buildStructuredPrompt(prompt, output, input), timeoutMs);
238
+ for (let attempt = 0; attempt < maxContinuations; attempt++) {
239
+ try {
240
+ JSON.parse(extractJsonPayload(combined));
241
+ return combined;
242
+ } catch {
243
+ if (!looksLikeIncompleteJson(combined)) {
244
+ return combined;
245
+ }
246
+ }
247
+ const continuationPrompt = buildStructuredContinuationPrompt(prompt, output, combined, input);
248
+ const continuation = await tryGenerateWithQuotaTolerance(continuationPrompt, timeoutMs);
249
+ if (continuation === null) {
250
+ break;
251
+ }
252
+ const merged = mergeStructuredFragments(combined, continuation);
253
+ if (merged === combined) {
254
+ break;
255
+ }
256
+ combined = merged;
257
+ }
258
+ return combined;
259
+ }
260
+ function buildStructuredPrompt(prompt, outputSchema, input) {
261
+ const schemaDescription = truncateEnd(zodTypeToDescription(outputSchema), 900);
262
+ const normalizedPrompt = truncateEnd(compactWhitespace(prompt), 900);
263
+ const serializedInput = input === undefined ? '' : `Input JSON:\n${stringifyInput(input, 900)}`;
264
+ return fitStructuredPrompt(['Return only valid JSON.', 'Do not include markdown, code fences, comments, prose, or explanations.', 'The JSON must match this exact TypeScript-like schema:', schemaDescription, `Task:\n${normalizedPrompt}`, serializedInput]);
265
+ }
266
+ function buildRepairPrompt(prompt, outputSchema, invalidResponse, issues) {
267
+ const issueText = issues.map(issue => `- ${issue.path}: ${issue.message}`).join('\n');
268
+ const normalizedIssues = truncateEnd(issueText, STRUCTURED_ISSUES_BUDGET);
269
+ const normalizedInvalidResponse = truncateStart(compactWhitespace(extractJsonPayload(invalidResponse)), STRUCTURED_REPAIR_RESPONSE_BUDGET);
270
+ return fitStructuredPrompt([buildStructuredPrompt(prompt, outputSchema), 'Your previous response was invalid.', 'Fix it and return only corrected JSON.', 'Validation errors:', normalizedIssues, 'Previous response:', normalizedInvalidResponse]);
271
+ }
272
+ async function generateStateless(prompt) {
273
+ if (!NativeAiCore) {
274
+ throw new Error(`react-native-ai-core: native module unavailable on ${Platform.OS}. This feature requires Android.`);
275
+ }
276
+ if (NativeAiCore?.generateResponseStateless) {
277
+ return NativeAiCore.generateResponseStateless(prompt);
278
+ }
279
+ return NativeAiCore.generateResponse(prompt);
280
+ }
281
+ async function generateStatelessWithTimeout(prompt, timeoutMs) {
282
+ if (timeoutMs <= 0) {
283
+ return generateStateless(prompt);
284
+ }
285
+ let timer;
286
+ const timeout = new Promise((_, reject) => {
287
+ timer = setTimeout(() => {
288
+ reject(new Error(`Structured generation timed out after ${timeoutMs}ms`));
289
+ }, timeoutMs);
290
+ });
291
+ try {
292
+ return await Promise.race([generateStateless(prompt), timeout]);
293
+ } finally {
294
+ if (timer) clearTimeout(timer);
295
+ }
296
+ }
297
+ async function generateStatelessWithQuotaRetry(prompt, timeoutMs) {
298
+ let quotaRetries = 0;
299
+ while (true) {
300
+ try {
301
+ return await generateStatelessWithTimeout(prompt, timeoutMs);
302
+ } catch (error) {
303
+ if (!isQuotaError(error) || quotaRetries >= MAX_QUOTA_RETRIES) {
304
+ throw error;
305
+ }
306
+ quotaRetries += 1;
307
+ await sleep(QUOTA_RETRY_DELAY_MS);
308
+ }
309
+ }
310
+ }
311
+ async function tryGenerateWithQuotaTolerance(prompt, timeoutMs) {
312
+ try {
313
+ return await generateStatelessWithQuotaRetry(prompt, timeoutMs);
314
+ } catch (error) {
315
+ if (isQuotaError(error)) {
316
+ return null;
317
+ }
318
+ throw error;
319
+ }
320
+ }
321
+
322
+ // ── Chunked strategy — tree-walker ───────────────────────────────────────────
323
+ //
324
+ // Walks the schema recursively choosing the optimal call granularity:
325
+ //
326
+ // • Leaf (string/number/bool/enum) → 1 call with coercion
327
+ // • Object/array whose schema fits in MAX_COMPACT_SCHEMA_CHARS → 1 walkCompact
328
+ // call (with continuation if truncated)
329
+ // • Array of compact elements → askCount + 1 walkCompact per element
330
+ // • Complex object → per-field walkLeaf/walkCompact
331
+ //
332
+ // This minimises the total number of calls without exceeding the token limit.
333
+
334
+ const INTER_CALL_DELAY_MS = 100;
335
+ const DEFAULT_ARRAY_COUNT = 5;
336
+ const MAX_ARRAY_COUNT = 7;
337
+ const MAX_WHOLE_ARRAY_COMPACT_ITEMS = 2;
338
+ const LEAF_TIMEOUT_CAP_MS = 15000;
339
+ const COMPACT_TIMEOUT_CAP_MS = 30000;
340
+ const ARRAY_COUNT_TIMEOUT_CAP_MS = 8000;
341
+ const LEAF_MAX_RETRIES_CAP = 1;
342
+ // Schema description length threshold below which a single compact call is used.
343
+ const MAX_COMPACT_SCHEMA_CHARS = 500;
344
+ function readZodLengthValue(value) {
345
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
346
+ if (typeof value === 'object' && value !== null) {
347
+ const maybe = value.value;
348
+ if (typeof maybe === 'number' && Number.isFinite(maybe)) return maybe;
349
+ }
350
+ return null;
351
+ }
352
+ function getArrayBounds(arraySchema, fieldName) {
353
+ const def = arraySchema._def;
354
+ const exact = readZodLengthValue(def?.exactLength);
355
+ const minLen = readZodLengthValue(def?.minLength);
356
+ const maxLen = readZodLengthValue(def?.maxLength);
357
+ if (exact !== null) {
358
+ const fixed = Math.max(0, Math.round(exact));
359
+ return {
360
+ min: fixed,
361
+ max: fixed,
362
+ preferred: fixed
363
+ };
364
+ }
365
+ let min = minLen !== null ? Math.max(0, Math.round(minLen)) : 1;
366
+ let max = maxLen !== null ? Math.max(min, Math.round(maxLen)) : MAX_ARRAY_COUNT;
367
+
368
+ // Domain heuristics when schema does not constrain lengths explicitly.
369
+ const name = fieldName.toLowerCase();
370
+ if (name === 'days') {
371
+ min = Math.max(min, 7);
372
+ max = Math.min(max, 7);
373
+ }
374
+ if (name === 'exercises') {
375
+ min = Math.max(min, 3);
376
+ max = Math.min(max, 4);
377
+ }
378
+ const preferred = Math.max(min, Math.min(max, DEFAULT_ARRAY_COUNT));
379
+ return {
380
+ min,
381
+ max,
382
+ preferred
383
+ };
384
+ }
385
+ function unwrapModifiers(schema) {
386
+ if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable) {
387
+ return unwrapModifiers(schema.unwrap());
388
+ }
389
+ return schema;
390
+ }
391
+ function isLeafSchema(schema) {
392
+ const inner = unwrapModifiers(schema);
393
+ return inner instanceof z.ZodString || inner instanceof z.ZodNumber || inner instanceof z.ZodBoolean || inner instanceof z.ZodEnum || inner instanceof z.ZodLiteral || inner instanceof z.ZodNull;
394
+ }
395
+
396
+ // A schema is "compact" if its description fits within MAX_COMPACT_SCHEMA_CHARS.
397
+ // Arrays are always excluded because their output can multiply by N.
398
+ function isCompactSchema(schema) {
399
+ const inner = unwrapModifiers(schema);
400
+ if (isLeafSchema(inner)) return true;
401
+ if (inner instanceof z.ZodArray) return false;
402
+ if (hasNestedArray(inner)) return false;
403
+ return zodTypeToDescription(inner).length <= MAX_COMPACT_SCHEMA_CHARS;
404
+ }
405
+
406
+ // Returns true if each array element can be generated in a single compact call.
407
+ function isCompactElement(schema) {
408
+ const inner = unwrapModifiers(schema);
409
+ if (isLeafSchema(inner)) return true;
410
+ if (hasNestedArray(inner)) return false;
411
+ return zodTypeToDescription(inner).length <= MAX_COMPACT_SCHEMA_CHARS;
412
+ }
413
+
414
+ // Returns true if the schema contains arrays at any depth.
415
+ // Such schemas are never compacted to avoid exceeding the 256-token output limit.
416
+ function hasNestedArray(schema) {
417
+ const inner = unwrapModifiers(schema);
418
+ if (inner instanceof z.ZodArray) return true;
419
+ if (inner instanceof z.ZodObject) {
420
+ const shape = inner.shape;
421
+ return Object.values(shape).some(value => hasNestedArray(value));
422
+ }
423
+ if (inner instanceof z.ZodUnion) {
424
+ return inner.options.some(option => hasNestedArray(option));
425
+ }
426
+ return false;
427
+ }
428
+ function isStringSchema(schema) {
429
+ return unwrapModifiers(schema) instanceof z.ZodString;
430
+ }
431
+ function isNumberSchema(schema) {
432
+ return unwrapModifiers(schema) instanceof z.ZodNumber;
433
+ }
434
+ function isBooleanSchema(schema) {
435
+ return unwrapModifiers(schema) instanceof z.ZodBoolean;
436
+ }
437
+ function getEnumOptions(schema) {
438
+ const inner = unwrapModifiers(schema);
439
+ if (inner instanceof z.ZodEnum) {
440
+ return [...inner.options];
441
+ }
442
+ return null;
443
+ }
444
+ function getLiteralValue(schema) {
445
+ const inner = unwrapModifiers(schema);
446
+ if (inner instanceof z.ZodLiteral) {
447
+ return inner.value;
448
+ }
449
+ return null;
450
+ }
451
+ function synthesizeLeafFallback(schema, path) {
452
+ const inner = unwrapModifiers(schema);
453
+ const field = (path[path.length - 1] ?? '').toLowerCase();
454
+ if (inner instanceof z.ZodLiteral) {
455
+ return inner.value;
456
+ }
457
+ if (inner instanceof z.ZodEnum) {
458
+ // For paths like days.0.day, map index -> weekday to improve coherence.
459
+ if (field === 'day' && path.length >= 2) {
460
+ const maybeIndex = Number.parseInt(path[path.length - 2] ?? '', 10);
461
+ if (Number.isFinite(maybeIndex)) {
462
+ const idx = Math.max(0, Math.min(inner.options.length - 1, maybeIndex));
463
+ return inner.options[idx];
464
+ }
465
+ }
466
+ return inner.options[0] ?? null;
467
+ }
468
+ if (inner instanceof z.ZodString) {
469
+ if (field === 'goal') return 'Strength + fat loss';
470
+ if (field === 'focus') return 'Strength + conditioning';
471
+ if (field === 'notes') return 'Progressively increase load week to week.';
472
+ if (field === 'reps') return '8-12';
473
+ if (field === 'name') return 'Accessory Lift';
474
+ return 'TBD';
475
+ }
476
+ if (inner instanceof z.ZodNumber) {
477
+ if (field === 'durationweeks') return 1;
478
+ if (field === 'sets') return 3;
479
+ if (field === 'restseconds') return 90;
480
+ return 0;
481
+ }
482
+ if (inner instanceof z.ZodBoolean) {
483
+ return false;
484
+ }
485
+ if (inner instanceof z.ZodNull) {
486
+ return null;
487
+ }
488
+ return null;
489
+ }
490
+ function coercePrimitiveField(raw, schema) {
491
+ const trimmed = raw.trim().replace(/^```[\w]*\n?|```$/g, '').trim();
492
+ try {
493
+ return JSON.parse(trimmed);
494
+ } catch {
495
+ /* sigue */
496
+ }
497
+ if (isStringSchema(schema)) return trimmed;
498
+ if (isNumberSchema(schema)) {
499
+ const n = Number(trimmed.replace(/[^\d.-]/g, ''));
500
+ if (!Number.isNaN(n)) return n;
501
+ }
502
+ if (isBooleanSchema(schema)) {
503
+ if (/^true$/i.test(trimmed)) return true;
504
+ if (/^false$/i.test(trimmed)) return false;
505
+ }
506
+ const enumOptions = getEnumOptions(schema);
507
+ if (enumOptions) {
508
+ const normalized = trimmed.replace(/^"|"$/g, '').trim();
509
+ const direct = enumOptions.find(opt => opt === normalized);
510
+ if (direct) return direct;
511
+ const ci = enumOptions.find(opt => opt.toLowerCase() === normalized.toLowerCase());
512
+ if (ci) return ci;
513
+ }
514
+ const literalValue = getLiteralValue(schema);
515
+ if (literalValue !== null) {
516
+ const normalized = trimmed.replace(/^"|"$/g, '').trim();
517
+ if (String(literalValue) === normalized) return literalValue;
518
+ }
519
+ return null;
520
+ }
521
+ async function walkLeaf(schema, ctx) {
522
+ const fieldPath = ctx.progressLabel ?? (ctx.path.join('.') || 'root');
523
+ const fieldName = ctx.path[ctx.path.length - 1] ?? 'value';
524
+ ctx.onProgress?.(fieldPath, false);
525
+ const fieldType = truncateEnd(zodTypeToDescription(schema), 200);
526
+ const itemCtx = ctx.itemHint ? `Context: ${ctx.itemHint}.` : '';
527
+ const isStr = isStringSchema(schema);
528
+ const enumOptions = getEnumOptions(schema);
529
+ const literalValue = getLiteralValue(schema);
530
+ const valueInstruction = enumOptions ? `one of: ${enumOptions.join(' | ')}. Reply exact value only, no key.` : literalValue !== null ? `exact literal value: ${JSON.stringify(literalValue)}.` : isStr ? 'a plain phrase — no quotes, no JSON' : 'raw JSON value only — no key, no markdown';
531
+ const prompt = fitStructuredPrompt([`"${fieldName}" (${fieldType}): output ${valueInstruction}.`, `Task: ${truncateEnd(compactWhitespace(ctx.taskPrompt), 250)}`, ctx.input !== undefined ? `Input: ${stringifyInput(ctx.input, 150)}` : '', itemCtx]);
532
+ const leafTimeoutMs = Math.min(ctx.timeoutMs, LEAF_TIMEOUT_CAP_MS);
533
+ const leafMaxRetries = Math.min(ctx.maxRetries, LEAF_MAX_RETRIES_CAP);
534
+ let lastRaw = '';
535
+ let lastErrorMessage = '';
536
+ for (let attempt = 0; attempt <= leafMaxRetries; attempt++) {
537
+ if (attempt > 0) await sleep(INTER_CALL_DELAY_MS);
538
+ try {
539
+ lastRaw = await generateStatelessWithQuotaRetry(prompt, leafTimeoutMs);
540
+ const coerced = coercePrimitiveField(lastRaw, schema);
541
+ const validated = schema.safeParse(coerced);
542
+ if (validated.success) {
543
+ ctx.onProgress?.(fieldPath, true);
544
+ return validated.data;
545
+ }
546
+ } catch (error) {
547
+ lastErrorMessage = toErrorMessage(error);
548
+ }
549
+ }
550
+ const fallbackFromRaw = isStr ? lastRaw.trim() : null;
551
+ const fallbackSynth = synthesizeLeafFallback(schema, ctx.path);
552
+ const fallback = fallbackFromRaw ?? fallbackSynth;
553
+ const fallbackValidated = schema.safeParse(fallback);
554
+ if (fallbackValidated.success) {
555
+ ctx.onProgress?.(fieldPath, true);
556
+ return fallbackValidated.data;
557
+ }
558
+ throw new StructuredOutputError(`Could not generate leaf "${ctx.path.join('.')}"`, lastRaw || lastErrorMessage, [{
559
+ path: ctx.path.join('.'),
560
+ message: (lastRaw || lastErrorMessage || 'leaf generation failed').slice(0, 120)
561
+ }]);
562
+ }
563
+ async function walkCompact(schema, ctx) {
564
+ const fieldPath = ctx.progressLabel ?? (ctx.path.join('.') || 'root');
565
+ const fieldName = ctx.path[ctx.path.length - 1] ?? 'value';
566
+ ctx.onProgress?.(fieldPath, false);
567
+ const typeDesc = truncateEnd(zodTypeToDescription(schema), 400);
568
+ const itemCtx = ctx.itemHint ? `Context: ${ctx.itemHint}.` : '';
569
+ const prompt = fitStructuredPrompt([`"${fieldName}" (${typeDesc}): return JSON value only — no key, no markdown.`, `Task: ${truncateEnd(compactWhitespace(ctx.taskPrompt), 250)}`, ctx.input !== undefined ? `Input: ${stringifyInput(ctx.input, 150)}` : '', itemCtx]);
570
+ const compactTimeoutMs = Math.min(ctx.timeoutMs, COMPACT_TIMEOUT_CAP_MS);
571
+ let combined = await generateStatelessWithQuotaRetry(prompt, compactTimeoutMs);
572
+ for (let cont = 0; cont < 6; cont++) {
573
+ try {
574
+ const result = JSON.parse(extractJsonPayload(combined));
575
+ ctx.onProgress?.(fieldPath, true);
576
+ return result;
577
+ } catch {
578
+ if (!looksLikeIncompleteJson(combined)) break;
579
+ }
580
+ await sleep(INTER_CALL_DELAY_MS);
581
+ const extra = await tryGenerateWithQuotaTolerance(buildStructuredContinuationPrompt(ctx.taskPrompt, schema, combined, ctx.input), compactTimeoutMs);
582
+ if (!extra) break;
583
+ const merged = mergeStructuredFragments(combined, extra);
584
+ if (merged === combined) break;
585
+ combined = merged;
586
+ }
587
+ const finalResult = JSON.parse(extractJsonPayload(combined));
588
+ ctx.onProgress?.(fieldPath, true);
589
+ return finalResult;
590
+ }
591
+ async function askArrayCount(arraySchema, fieldName, ctx) {
592
+ const bounds = getArrayBounds(arraySchema, fieldName);
593
+ const prompt = fitContinuationPrompt([`"${fieldName}" array count (${bounds.min}-${bounds.max}). Reply: single integer only.`, `Task: ${truncateEnd(compactWhitespace(ctx.taskPrompt), 150)}`]);
594
+ try {
595
+ await sleep(INTER_CALL_DELAY_MS);
596
+ const countTimeoutMs = Math.min(ctx.timeoutMs, ARRAY_COUNT_TIMEOUT_CAP_MS);
597
+ const raw = await generateStatelessWithQuotaRetry(prompt, countTimeoutMs);
598
+ const n = parseInt(raw.trim().replace(/\D/g, ''), 10);
599
+ if (Number.isFinite(n) && n >= bounds.min && n <= bounds.max) return n;
600
+ } catch {
601
+ /* usa defecto */
602
+ }
603
+ return bounds.preferred;
604
+ }
605
+ async function walkSchema(schema, ctx) {
606
+ const inner = unwrapModifiers(schema);
607
+ if (isLeafSchema(inner)) {
608
+ await sleep(INTER_CALL_DELAY_MS);
609
+ return walkLeaf(inner, ctx);
610
+ }
611
+ if (inner instanceof z.ZodArray) {
612
+ const elementSchema = unwrapModifiers(inner.element);
613
+ const fieldName = ctx.path[ctx.path.length - 1] ?? 'items';
614
+ const arrayLabel = ctx.path.join('.') || fieldName;
615
+ const count = await askArrayCount(inner, fieldName, ctx);
616
+
617
+ // Fast path: if each element fits in a compact call and the array is small,
618
+ // generate the entire array in one walkCompact call instead of per-element.
619
+ if (isCompactElement(elementSchema) && count <= MAX_WHOLE_ARRAY_COMPACT_ITEMS) {
620
+ ctx.onProgress?.(arrayLabel, false);
621
+ await sleep(INTER_CALL_DELAY_MS);
622
+ const result = await walkCompact(inner, {
623
+ ...ctx,
624
+ progressLabel: arrayLabel
625
+ });
626
+ ctx.onProgress?.(arrayLabel, true);
627
+ return result;
628
+ }
629
+
630
+ // Slow path: non-compact elements → generate one by one
631
+ ctx.onProgress?.(arrayLabel, false);
632
+ const items = [];
633
+ for (let i = 0; i < count; i++) {
634
+ await sleep(INTER_CALL_DELAY_MS);
635
+ const elemLabel = `${arrayLabel}[${i + 1}/${count}]`;
636
+ const elemCtx = {
637
+ ...ctx,
638
+ path: [...ctx.path, String(i)],
639
+ itemHint: `item ${i + 1} of ${count}`,
640
+ progressLabel: elemLabel
641
+ };
642
+ items.push(await walkSchema(elementSchema, elemCtx));
643
+ }
644
+ ctx.onProgress?.(arrayLabel, true);
645
+ return items;
646
+ }
647
+
648
+ // Compact object: single call (except at root where per-field gives better progress).
649
+ if (isCompactSchema(inner) && ctx.path.length > 0) {
650
+ await sleep(INTER_CALL_DELAY_MS);
651
+ return walkCompact(inner, ctx);
652
+ }
653
+ if (inner instanceof z.ZodObject) {
654
+ const shape = inner.shape;
655
+ const entries = Object.entries(shape);
656
+ const result = {};
657
+ const compactEntries = entries.filter(([, s]) => isCompactSchema(s));
658
+ const complexEntries = entries.filter(([, s]) => !isCompactSchema(s));
659
+ if (compactEntries.length > 0) {
660
+ for (const [key, fieldSchema] of compactEntries) {
661
+ const fieldCtx = {
662
+ ...ctx,
663
+ path: [...ctx.path, key],
664
+ progressLabel: [...ctx.path, key].join('.')
665
+ };
666
+ if (isLeafSchema(unwrapModifiers(fieldSchema))) {
667
+ await sleep(INTER_CALL_DELAY_MS);
668
+ result[key] = await walkLeaf(fieldSchema, fieldCtx);
669
+ } else {
670
+ await sleep(INTER_CALL_DELAY_MS);
671
+ result[key] = await walkCompact(fieldSchema, fieldCtx);
672
+ }
673
+ }
674
+ }
675
+ for (const [key, fieldSchema] of complexEntries) {
676
+ result[key] = await walkSchema(fieldSchema, {
677
+ ...ctx,
678
+ path: [...ctx.path, key]
679
+ });
680
+ }
681
+ return result;
682
+ }
683
+ await sleep(INTER_CALL_DELAY_MS);
684
+ return walkLeaf(inner, ctx);
685
+ }
686
+ async function generateChunked(prompt, schema, input, timeoutMs, maxRetries, onProgress) {
687
+ return walkSchema(schema, {
688
+ taskPrompt: prompt,
689
+ input,
690
+ timeoutMs,
691
+ maxRetries,
692
+ path: [],
693
+ onProgress
694
+ });
695
+ }
696
+ export async function generateStructuredResponse(options) {
697
+ const {
698
+ prompt,
699
+ output,
700
+ input,
701
+ inputSchema,
702
+ maxRetries = 2,
703
+ maxContinuations = DEFAULT_MAX_STRUCTURED_CONTINUATIONS,
704
+ timeoutMs = DEFAULT_STRUCTURED_TIMEOUT_MS,
705
+ strategy = 'single',
706
+ onProgress
707
+ } = options;
708
+ if (inputSchema && input !== undefined) {
709
+ inputSchema.parse(input);
710
+ }
711
+ if (strategy === 'chunked') {
712
+ const assembled = await generateChunked(prompt, output, input, timeoutMs, maxRetries, onProgress);
713
+ return output.parse(assembled);
714
+ }
715
+ let lastRawResponse = '';
716
+ let lastIssues = [];
717
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
718
+ lastRawResponse = attempt === 0 ? await generateStructuredRawResponse(prompt, output, input, maxContinuations, timeoutMs) : await generateStatelessWithQuotaRetry(buildRepairPrompt(prompt, output, lastRawResponse, lastIssues), timeoutMs);
719
+ try {
720
+ const parsed = JSON.parse(extractJsonPayload(lastRawResponse));
721
+ return output.parse(parsed);
722
+ } catch (error) {
723
+ lastIssues = buildJsonParseIssues(error);
724
+ if (attempt === maxRetries) break;
725
+ }
726
+ }
727
+ throw new StructuredOutputError('Unable to produce valid structured output after retries.', lastRawResponse, lastIssues);
728
+ }
729
+ //# sourceMappingURL=structured.js.map