ocpipe 0.1.0 → 0.3.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.
@@ -5,8 +5,7 @@
5
5
  *
6
6
  * @example
7
7
  * ```typescript
8
- * import { signature, field, SignatureModule, Pipeline } from './dsts/index.js'
9
- * import { z } from 'zod'
8
+ * import { signature, field, module, Pipeline, createBaseState } from 'ocpipe'
10
9
  *
11
10
  * // Define a signature
12
11
  * const ParseIntent = signature({
@@ -20,18 +19,6 @@
20
19
  * },
21
20
  * })
22
21
  *
23
- * // Create a module (types inferred from signature)
24
- * class IntentParser extends SignatureModule<typeof ParseIntent> {
25
- * constructor() {
26
- * super(ParseIntent)
27
- * }
28
- *
29
- * async forward(input, ctx) {
30
- * const result = await this.predictor.execute(input, ctx)
31
- * return result.data
32
- * }
33
- * }
34
- *
35
22
  * // Run in a pipeline
36
23
  * const pipeline = new Pipeline({
37
24
  * name: 'my-workflow',
@@ -41,7 +28,7 @@
41
28
  * logDir: './logs',
42
29
  * }, createBaseState)
43
30
  *
44
- * const result = await pipeline.run(new IntentParser(), { description: 'Hello world' })
31
+ * const result = await pipeline.run(module(ParseIntent), { description: 'Hello world' })
45
32
  * ```
46
33
  */
47
34
 
@@ -53,7 +40,7 @@ export { Predict } from './predict.js'
53
40
  export type { PredictConfig } from './predict.js'
54
41
 
55
42
  // Module base class
56
- export { Module, SignatureModule } from './module.js'
43
+ export { Module, SignatureModule, module } from './module.js'
57
44
 
58
45
  // Pipeline orchestrator
59
46
  export { Pipeline } from './pipeline.js'
@@ -10,6 +10,7 @@ import type {
10
10
  InferOutputs,
11
11
  SignatureDef,
12
12
  } from './types.js'
13
+ export type { ExecutionContext } from './types.js'
13
14
  import { Predict, type PredictConfig } from './predict.js'
14
15
 
15
16
  /** Module is the abstract base class for composable workflow units. */
@@ -48,3 +49,27 @@ export abstract class SignatureModule<
48
49
  this.predictor = this.predict(sig, config)
49
50
  }
50
51
  }
52
+
53
+ /** SimpleModule is a SignatureModule that just executes the predictor. */
54
+ class SimpleModule<
55
+ S extends SignatureDef<any, any>,
56
+ > extends SignatureModule<S> {
57
+ constructor(sig: S, config?: PredictConfig) {
58
+ super(sig, config)
59
+ }
60
+
61
+ async forward(
62
+ input: InferInputs<S>,
63
+ ctx: ExecutionContext,
64
+ ): Promise<InferOutputs<S>> {
65
+ return (await this.predictor.execute(input, ctx)).data
66
+ }
67
+ }
68
+
69
+ /** module creates a simple Module from a Signature (syntactic sugar). */
70
+ export function module<S extends SignatureDef<any, any>>(
71
+ sig: S,
72
+ config?: PredictConfig,
73
+ ): Module<InferInputs<S>, InferOutputs<S>> {
74
+ return new SimpleModule(sig, config)
75
+ }
@@ -4,7 +4,7 @@
4
4
  * Extracts and validates LLM responses using JSON or field marker formats.
5
5
  */
6
6
 
7
- import { z } from 'zod'
7
+ import { z } from 'zod/v4'
8
8
  import type { FieldConfig, FieldError, TryParseResult } from './types.js'
9
9
 
10
10
  /** JSON Patch operation (RFC 6902). */
@@ -77,7 +77,13 @@ export function tryParseJson<T>(
77
77
  if (!jsonStr) {
78
78
  return {
79
79
  ok: false,
80
- errors: [{ path: '', message: 'No JSON found in response', expectedType: 'object' }],
80
+ errors: [
81
+ {
82
+ path: '',
83
+ message: 'No JSON found in response',
84
+ expectedType: 'object',
85
+ },
86
+ ],
81
87
  }
82
88
  }
83
89
 
@@ -88,7 +94,13 @@ export function tryParseJson<T>(
88
94
  const parseErr = e as SyntaxError
89
95
  return {
90
96
  ok: false,
91
- errors: [{ path: '', message: `JSON parse failed: ${parseErr.message}`, expectedType: 'object' }],
97
+ errors: [
98
+ {
99
+ path: '',
100
+ message: `JSON parse failed: ${parseErr.message}`,
101
+ expectedType: 'object',
102
+ },
103
+ ],
92
104
  }
93
105
  }
94
106
 
@@ -131,9 +143,10 @@ function zodErrorsToFieldErrors(
131
143
  let foundValue: unknown
132
144
 
133
145
  // Check if field is missing (received undefined)
134
- const isMissing = issue.code === 'invalid_type' &&
146
+ const isMissing =
147
+ issue.code === 'invalid_type' &&
135
148
  (issue as { received?: string }).received === 'undefined'
136
-
149
+
137
150
  if (isMissing) {
138
151
  // Field is missing - look for similar field names in parsed data
139
152
  const similar = findSimilarField(fieldName, parsed, schemaKeys)
@@ -187,14 +200,21 @@ function findSimilarField(
187
200
  for (const key of extraKeys) {
188
201
  const normalizedKey = key.toLowerCase().replace(/_/g, '')
189
202
  if (normalizedKey === normalized) return key
190
- if (normalizedKey.includes(normalized) || normalized.includes(normalizedKey)) return key
203
+ if (
204
+ normalizedKey.includes(normalized) ||
205
+ normalized.includes(normalizedKey)
206
+ )
207
+ return key
191
208
  }
192
209
 
193
210
  return undefined
194
211
  }
195
212
 
196
213
  /** getExpectedType extracts a human-readable type description from a Zod issue. */
197
- function getExpectedType(issue: z.ZodIssue, schema: Record<string, FieldConfig>): string {
214
+ function getExpectedType(
215
+ issue: z.ZodIssue,
216
+ schema: Record<string, FieldConfig>,
217
+ ): string {
198
218
  const fieldName = issue.path[0] as string
199
219
  const fieldConfig = schema[fieldName]
200
220
 
@@ -225,7 +245,9 @@ export function zodTypeToString(zodType: z.ZodType): string {
225
245
  return `enum[${opts.map((v) => `"${v}"`).join(', ')}]`
226
246
  }
227
247
  // Fallback to _def
228
- const def = (zodType as unknown as { _def?: { values?: readonly string[] } })._def
248
+ const def = (
249
+ zodType as unknown as { _def?: { values?: readonly string[] } }
250
+ )._def
229
251
  const values = def?.values ?? []
230
252
  if (values.length > 0) {
231
253
  return `enum[${values.map((v) => `"${v}"`).join(', ')}]`
@@ -242,7 +264,9 @@ export function zodTypeToString(zodType: z.ZodType): string {
242
264
  }
243
265
  if (zodType instanceof z.ZodObject) {
244
266
  // ZodObject has .shape property
245
- const shapeObj = (zodType as unknown as { shape?: Record<string, z.ZodType> }).shape
267
+ const shapeObj = (
268
+ zodType as unknown as { shape?: Record<string, z.ZodType> }
269
+ ).shape
246
270
  if (shapeObj) {
247
271
  const fields = Object.keys(shapeObj).slice(0, 3).join(', ')
248
272
  return `object{${fields}${Object.keys(shapeObj).length > 3 ? ', ...' : ''}}`
@@ -251,14 +275,18 @@ export function zodTypeToString(zodType: z.ZodType): string {
251
275
  }
252
276
  if (zodType instanceof z.ZodOptional) {
253
277
  // ZodOptional has .unwrap() method
254
- const unwrapped = (zodType as unknown as { unwrap?: () => z.ZodType }).unwrap?.()
278
+ const unwrapped = (
279
+ zodType as unknown as { unwrap?: () => z.ZodType }
280
+ ).unwrap?.()
255
281
  if (unwrapped) {
256
282
  return `optional<${zodTypeToString(unwrapped)}>`
257
283
  }
258
284
  return 'optional'
259
285
  }
260
286
  if (zodType instanceof z.ZodNullable) {
261
- const unwrapped = (zodType as unknown as { unwrap?: () => z.ZodType }).unwrap?.()
287
+ const unwrapped = (
288
+ zodType as unknown as { unwrap?: () => z.ZodType }
289
+ ).unwrap?.()
262
290
  if (unwrapped) {
263
291
  return `nullable<${zodTypeToString(unwrapped)}>`
264
292
  }
@@ -266,7 +294,8 @@ export function zodTypeToString(zodType: z.ZodType): string {
266
294
  }
267
295
  if (zodType instanceof z.ZodDefault) {
268
296
  // ZodDefault wraps inner type
269
- const inner = (zodType as unknown as { _def?: { innerType?: z.ZodType } })._def?.innerType
297
+ const inner = (zodType as unknown as { _def?: { innerType?: z.ZodType } })
298
+ ._def?.innerType
270
299
  if (inner) {
271
300
  return `default<${zodTypeToString(inner)}>`
272
301
  }
@@ -322,7 +351,9 @@ export function buildPatchPrompt(
322
351
  lines.push(`Expected type: ${error.expectedType}`)
323
352
 
324
353
  if (error.foundField) {
325
- lines.push(`Found similar field: "${error.foundField}" with value: ${JSON.stringify(error.foundValue)}`)
354
+ lines.push(
355
+ `Found similar field: "${error.foundField}" with value: ${JSON.stringify(error.foundValue)}`,
356
+ )
326
357
  }
327
358
 
328
359
  lines.push('')
@@ -354,9 +385,13 @@ export function buildBatchPatchPrompt(
354
385
  lines.push('ERRORS:')
355
386
  for (let i = 0; i < errors.length; i++) {
356
387
  const error = errors[i]!
357
- lines.push(`${i + 1}. Field "${error.path}": ${error.message} (expected: ${error.expectedType})`)
388
+ lines.push(
389
+ `${i + 1}. Field "${error.path}": ${error.message} (expected: ${error.expectedType})`,
390
+ )
358
391
  if (error.foundField) {
359
- lines.push(` Found similar: "${error.foundField}" = ${JSON.stringify(error.foundValue)}`)
392
+ lines.push(
393
+ ` Found similar: "${error.foundField}" = ${JSON.stringify(error.foundValue)}`,
394
+ )
360
395
  }
361
396
  }
362
397
 
@@ -366,7 +401,9 @@ export function buildBatchPatchPrompt(
366
401
  lines.push(JSON.stringify(abbreviateJson(currentJson), null, 2))
367
402
  lines.push('```')
368
403
  lines.push('')
369
- lines.push('Respond with jq-style patches to fix ALL errors. Use | to chain multiple patches.')
404
+ lines.push(
405
+ 'Respond with jq-style patches to fix ALL errors. Use | to chain multiple patches.',
406
+ )
370
407
  lines.push('Examples:')
371
408
  lines.push('- .field1 = "value" | .field2 = 123')
372
409
  lines.push('- .items[0].name = .items[0].title | del(.items[0].title)')
@@ -378,7 +415,10 @@ export function buildBatchPatchPrompt(
378
415
  }
379
416
 
380
417
  /** abbreviateJson truncates large values for display in prompts. */
381
- function abbreviateJson(obj: Record<string, unknown>, maxLength = 100): Record<string, unknown> {
418
+ function abbreviateJson(
419
+ obj: Record<string, unknown>,
420
+ maxLength = 100,
421
+ ): Record<string, unknown> {
382
422
  const result: Record<string, unknown> = {}
383
423
 
384
424
  for (const [key, value] of Object.entries(obj)) {
@@ -401,7 +441,9 @@ function abbreviateJson(obj: Record<string, unknown>, maxLength = 100): Record<s
401
441
  }
402
442
 
403
443
  /** convertNullToUndefined recursively converts null values to undefined (for optional fields). */
404
- function convertNullToUndefined(obj: Record<string, unknown>): Record<string, unknown> {
444
+ function convertNullToUndefined(
445
+ obj: Record<string, unknown>,
446
+ ): Record<string, unknown> {
405
447
  const result: Record<string, unknown> = {}
406
448
 
407
449
  for (const [key, value] of Object.entries(obj)) {
@@ -441,7 +483,10 @@ export function extractPatch(response: string): string {
441
483
  // If no clear patch found, try the whole response (minus markdown)
442
484
  const cleaned = response.replace(/```[^`]*```/g, '').trim()
443
485
  const firstLine = cleaned.split('\n')[0]?.trim()
444
- if (firstLine && (firstLine.startsWith('.') || firstLine.startsWith('del('))) {
486
+ if (
487
+ firstLine &&
488
+ (firstLine.startsWith('.') || firstLine.startsWith('del('))
489
+ ) {
445
490
  return firstLine
446
491
  }
447
492
 
@@ -475,7 +520,7 @@ export function applyJqPatch(
475
520
  /\bdebug\b/,
476
521
  /\berror\b/,
477
522
  /\bhalt\b/,
478
- /\$/, // Any variable reference (safest to disallow all)
523
+ /\$/, // Any variable reference (safest to disallow all)
479
524
  ]
480
525
 
481
526
  for (const pattern of unsafePatterns) {
@@ -535,7 +580,9 @@ export function buildJsonPatchPrompt(
535
580
  lines.push(`Expected type: ${error.expectedType}`)
536
581
 
537
582
  if (error.foundField) {
538
- lines.push(`Found similar field: "${error.foundField}" with value: ${JSON.stringify(error.foundValue)}`)
583
+ lines.push(
584
+ `Found similar field: "${error.foundField}" with value: ${JSON.stringify(error.foundValue)}`,
585
+ )
539
586
  }
540
587
 
541
588
  lines.push('')
@@ -544,11 +591,17 @@ export function buildJsonPatchPrompt(
544
591
  lines.push(JSON.stringify(abbreviateJson(currentJson), null, 2))
545
592
  lines.push('```')
546
593
  lines.push('')
547
- lines.push('Respond with ONLY a JSON Patch array (RFC 6902) to fix this field. Examples:')
594
+ lines.push(
595
+ 'Respond with ONLY a JSON Patch array (RFC 6902) to fix this field. Examples:',
596
+ )
548
597
  lines.push('- [{"op": "add", "path": "/field_name", "value": "new_value"}]')
549
598
  lines.push('- [{"op": "replace", "path": "/field_name", "value": 123}]')
550
- lines.push('- [{"op": "move", "from": "/wrong_field", "path": "/correct_field"}]')
551
- lines.push('- [{"op": "remove", "path": "/wrong_field"}, {"op": "add", "path": "/correct_field", "value": "..."}]')
599
+ lines.push(
600
+ '- [{"op": "move", "from": "/wrong_field", "path": "/correct_field"}]',
601
+ )
602
+ lines.push(
603
+ '- [{"op": "remove", "path": "/wrong_field"}, {"op": "add", "path": "/correct_field", "value": "..."}]',
604
+ )
552
605
  lines.push('')
553
606
  lines.push('Your JSON Patch:')
554
607
 
@@ -567,9 +620,13 @@ export function buildBatchJsonPatchPrompt(
567
620
  lines.push('ERRORS:')
568
621
  for (let i = 0; i < errors.length; i++) {
569
622
  const error = errors[i]!
570
- lines.push(`${i + 1}. Field "${error.path}": ${error.message} (expected: ${error.expectedType})`)
623
+ lines.push(
624
+ `${i + 1}. Field "${error.path}": ${error.message} (expected: ${error.expectedType})`,
625
+ )
571
626
  if (error.foundField) {
572
- lines.push(` Found similar: "${error.foundField}" = ${JSON.stringify(error.foundValue)}`)
627
+ lines.push(
628
+ ` Found similar: "${error.foundField}" = ${JSON.stringify(error.foundValue)}`,
629
+ )
573
630
  }
574
631
  }
575
632
 
@@ -579,7 +636,9 @@ export function buildBatchJsonPatchPrompt(
579
636
  lines.push(JSON.stringify(abbreviateJson(currentJson), null, 2))
580
637
  lines.push('```')
581
638
  lines.push('')
582
- lines.push('Respond with a JSON Patch array (RFC 6902) to fix ALL errors. Examples:')
639
+ lines.push(
640
+ 'Respond with a JSON Patch array (RFC 6902) to fix ALL errors. Examples:',
641
+ )
583
642
  lines.push('- [{"op": "move", "from": "/type", "path": "/issue_type"}]')
584
643
  lines.push('- [{"op": "replace", "path": "/items/0/name", "value": "fixed"}]')
585
644
  lines.push('- [{"op": "add", "path": "/missing_field", "value": "default"}]')
@@ -592,7 +651,9 @@ export function buildBatchJsonPatchPrompt(
592
651
  /** extractJsonPatch extracts a JSON Patch array from an LLM response. */
593
652
  export function extractJsonPatch(response: string): JsonPatchOperation[] {
594
653
  // Try to find JSON array in code blocks first
595
- const codeBlockMatch = response.match(/```(?:json)?\s*(\[[\s\S]*?\])[\s\S]*?```/)
654
+ const codeBlockMatch = response.match(
655
+ /```(?:json)?\s*(\[[\s\S]*?\])[\s\S]*?```/,
656
+ )
596
657
  if (codeBlockMatch?.[1]) {
597
658
  try {
598
659
  return JSON.parse(codeBlockMatch[1]) as JsonPatchOperation[]
@@ -619,14 +680,18 @@ export function extractJsonPatch(response: string): JsonPatchOperation[] {
619
680
 
620
681
  if (endIdx > startIdx) {
621
682
  try {
622
- return JSON.parse(response.slice(startIdx, endIdx)) as JsonPatchOperation[]
683
+ return JSON.parse(
684
+ response.slice(startIdx, endIdx),
685
+ ) as JsonPatchOperation[]
623
686
  } catch {
624
687
  // Fall through to empty array
625
688
  }
626
689
  }
627
690
  }
628
691
 
629
- console.error(` Could not extract JSON Patch from response: ${response.slice(0, 100)}...`)
692
+ console.error(
693
+ ` Could not extract JSON Patch from response: ${response.slice(0, 100)}...`,
694
+ )
630
695
  return []
631
696
  }
632
697
 
@@ -649,7 +714,7 @@ export function applyJsonPatch(
649
714
  for (const op of operations) {
650
715
  const path = toJsonPointer(op.path)
651
716
  const pathParts = path.split('/').filter(Boolean)
652
-
717
+
653
718
  try {
654
719
  switch (op.op) {
655
720
  case 'add':
@@ -682,20 +747,28 @@ export function applyJsonPatch(
682
747
  const srcPath = toJsonPointer(op.from)
683
748
  const srcParts = srcPath.split('/').filter(Boolean)
684
749
  const srcValue = getValueAtPath(result, srcParts)
685
- setValueAtPath(result, pathParts, JSON.parse(JSON.stringify(srcValue)))
750
+ setValueAtPath(
751
+ result,
752
+ pathParts,
753
+ JSON.parse(JSON.stringify(srcValue)),
754
+ )
686
755
  break
687
756
  }
688
757
  case 'test': {
689
758
  // Test operation - verify value matches, throw if not
690
759
  const actualValue = getValueAtPath(result, pathParts)
691
760
  if (JSON.stringify(actualValue) !== JSON.stringify(op.value)) {
692
- console.error(` JSON Patch test failed: ${path} expected ${JSON.stringify(op.value)}, got ${JSON.stringify(actualValue)}`)
761
+ console.error(
762
+ ` JSON Patch test failed: ${path} expected ${JSON.stringify(op.value)}, got ${JSON.stringify(actualValue)}`,
763
+ )
693
764
  }
694
765
  break
695
766
  }
696
767
  }
697
768
  } catch (e) {
698
- console.error(` JSON Patch operation failed: ${JSON.stringify(op)} - ${e}`)
769
+ console.error(
770
+ ` JSON Patch operation failed: ${JSON.stringify(op)} - ${e}`,
771
+ )
699
772
  }
700
773
  }
701
774
 
@@ -703,7 +776,10 @@ export function applyJsonPatch(
703
776
  }
704
777
 
705
778
  /** getValueAtPath retrieves a value at a JSON Pointer path. */
706
- function getValueAtPath(obj: Record<string, unknown>, parts: string[]): unknown {
779
+ function getValueAtPath(
780
+ obj: Record<string, unknown>,
781
+ parts: string[],
782
+ ): unknown {
707
783
  let current: unknown = obj
708
784
  for (const part of parts) {
709
785
  if (current === null || current === undefined) return undefined
@@ -720,9 +796,13 @@ function getValueAtPath(obj: Record<string, unknown>, parts: string[]): unknown
720
796
  }
721
797
 
722
798
  /** setValueAtPath sets a value at a JSON Pointer path. */
723
- function setValueAtPath(obj: Record<string, unknown>, parts: string[], value: unknown): void {
799
+ function setValueAtPath(
800
+ obj: Record<string, unknown>,
801
+ parts: string[],
802
+ value: unknown,
803
+ ): void {
724
804
  if (parts.length === 0) return
725
-
805
+
726
806
  let current: unknown = obj
727
807
  for (let i = 0; i < parts.length - 1; i++) {
728
808
  const part = parts[i]!
@@ -749,12 +829,15 @@ function setValueAtPath(obj: Record<string, unknown>, parts: string[], value: un
749
829
  const idx = parseInt(lastPart, 10)
750
830
  current[idx] = value
751
831
  } else if (typeof current === 'object' && current !== null) {
752
- (current as Record<string, unknown>)[lastPart] = value
832
+ ;(current as Record<string, unknown>)[lastPart] = value
753
833
  }
754
834
  }
755
835
 
756
836
  /** removeValueAtPath removes a value at a JSON Pointer path. */
757
- function removeValueAtPath(obj: Record<string, unknown>, parts: string[]): void {
837
+ function removeValueAtPath(
838
+ obj: Record<string, unknown>,
839
+ parts: string[],
840
+ ): void {
758
841
  if (parts.length === 0) return
759
842
 
760
843
  let current: unknown = obj
@@ -805,8 +888,15 @@ export function parseJson<T>(
805
888
  const zodResult = zodSchema.safeParse(result.json)
806
889
 
807
890
  if (!zodResult.success) {
808
- const issues = zodResult.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')
809
- throw new ValidationError(`Output validation failed: ${issues}`, response, zodResult.error, errors)
891
+ const issues = zodResult.error.issues
892
+ .map((i) => `${i.path.join('.')}: ${i.message}`)
893
+ .join('; ')
894
+ throw new ValidationError(
895
+ `Output validation failed: ${issues}`,
896
+ response,
897
+ zodResult.error,
898
+ errors,
899
+ )
810
900
  }
811
901
 
812
902
  // Shouldn't reach here
package/src/paths.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { join } from 'path'
2
+
3
+ export const PROJECT_ROOT = import.meta.dirname
4
+ export const TMP_DIR = join(PROJECT_ROOT, '.tmp')
@@ -58,7 +58,8 @@ export class Pipeline<S extends BaseState> {
58
58
 
59
59
  const startTime = Date.now()
60
60
  let lastError: Error | undefined
61
- const retryConfig = options?.retry ?? this.config.retry ?? { maxAttempts: 1 }
61
+ const retryConfig = options?.retry ??
62
+ this.config.retry ?? { maxAttempts: 1 }
62
63
 
63
64
  for (let attempt = 1; attempt <= retryConfig.maxAttempts; attempt++) {
64
65
  try {
@@ -176,7 +177,7 @@ export class Pipeline<S extends BaseState> {
176
177
  sessionId: string,
177
178
  ): Promise<Pipeline<S> | null> {
178
179
  const path = `${config.checkpointDir}/${config.name}_${sessionId}.json`
179
-
180
+
180
181
  try {
181
182
  const content = await readFile(path, 'utf-8')
182
183
  const state = JSON.parse(content) as S
@@ -4,7 +4,7 @@
4
4
  * Executes a signature by generating a prompt, calling OpenCode, and parsing the response.
5
5
  */
6
6
 
7
- import { z } from 'zod'
7
+ import { z } from 'zod/v4'
8
8
  import type {
9
9
  CorrectionConfig,
10
10
  CorrectionMethod,
@@ -90,7 +90,11 @@ export class Predict<S extends SignatureDef<any, any>> {
90
90
  }
91
91
 
92
92
  // Parsing failed - attempt correction if enabled
93
- if (this.config.correction !== false && parseResult.errors && parseResult.json) {
93
+ if (
94
+ this.config.correction !== false &&
95
+ parseResult.errors &&
96
+ parseResult.json
97
+ ) {
94
98
  const corrected = await this.correctFields(
95
99
  parseResult.json,
96
100
  parseResult.errors,
@@ -111,9 +115,19 @@ export class Predict<S extends SignatureDef<any, any>> {
111
115
 
112
116
  // Correction failed or disabled - throw SchemaValidationError (non-retryable)
113
117
  const errors = parseResult.errors ?? []
114
- const errorMessages = errors.map((e) => `${e.path}: ${e.message}`).join('; ') || 'Unknown error'
115
- const correctionAttempts = this.config.correction !== false ? (typeof this.config.correction === 'object' ? this.config.correction.maxRounds ?? 3 : 3) : 0
116
- throw new SchemaValidationError(`Schema validation failed: ${errorMessages}`, errors, correctionAttempts)
118
+ const errorMessages =
119
+ errors.map((e) => `${e.path}: ${e.message}`).join('; ') || 'Unknown error'
120
+ const correctionAttempts =
121
+ this.config.correction !== false ?
122
+ typeof this.config.correction === 'object' ?
123
+ (this.config.correction.maxRounds ?? 3)
124
+ : 3
125
+ : 0
126
+ throw new SchemaValidationError(
127
+ `Schema validation failed: ${errorMessages}`,
128
+ errors,
129
+ correctionAttempts,
130
+ )
117
131
  }
118
132
 
119
133
  /** correctFields attempts to fix field errors using same-session patches with retries. */
@@ -123,32 +137,39 @@ export class Predict<S extends SignatureDef<any, any>> {
123
137
  ctx: ExecutionContext,
124
138
  sessionId: string,
125
139
  ): Promise<InferOutputs<S> | null> {
126
- const correctionConfig = typeof this.config.correction === 'object' ? this.config.correction : {}
140
+ const correctionConfig =
141
+ typeof this.config.correction === 'object' ? this.config.correction : {}
127
142
  const method: CorrectionMethod = correctionConfig.method ?? 'json-patch'
128
143
  const maxFields = correctionConfig.maxFields ?? 5
129
144
  const maxRounds = correctionConfig.maxRounds ?? 3
130
145
  const correctionModel = correctionConfig.model
131
146
 
132
- let currentJson = JSON.parse(JSON.stringify(json)) as Record<string, unknown>
147
+ let currentJson = JSON.parse(JSON.stringify(json)) as Record<
148
+ string,
149
+ unknown
150
+ >
133
151
  let currentErrors = initialErrors
134
152
 
135
153
  for (let round = 1; round <= maxRounds; round++) {
136
154
  const errorsToFix = currentErrors.slice(0, maxFields)
137
-
155
+
138
156
  if (errorsToFix.length === 0) {
139
157
  break
140
158
  }
141
159
 
142
- console.error(`\n>>> Correction round ${round}/${maxRounds} [${method}]: fixing ${errorsToFix.length} field(s)...`)
160
+ console.error(
161
+ `\n>>> Correction round ${round}/${maxRounds} [${method}]: fixing ${errorsToFix.length} field(s)...`,
162
+ )
143
163
 
144
164
  // Build prompt based on correction method
145
- const patchPrompt = method === 'jq'
146
- ? (errorsToFix.length === 1
147
- ? buildPatchPrompt(errorsToFix[0]!, currentJson, this.sig.outputs)
148
- : buildBatchPatchPrompt(errorsToFix, currentJson))
149
- : (errorsToFix.length === 1
150
- ? buildJsonPatchPrompt(errorsToFix[0]!, currentJson, this.sig.outputs)
151
- : buildBatchJsonPatchPrompt(errorsToFix, currentJson))
165
+ const patchPrompt =
166
+ method === 'jq' ?
167
+ errorsToFix.length === 1 ?
168
+ buildPatchPrompt(errorsToFix[0]!, currentJson, this.sig.outputs)
169
+ : buildBatchPatchPrompt(errorsToFix, currentJson)
170
+ : errorsToFix.length === 1 ?
171
+ buildJsonPatchPrompt(errorsToFix[0]!, currentJson, this.sig.outputs)
172
+ : buildBatchJsonPatchPrompt(errorsToFix, currentJson)
152
173
 
153
174
  // Use same session (model has context) unless correction model specified
154
175
  const patchResult = await runAgent({
@@ -183,14 +204,16 @@ export class Predict<S extends SignatureDef<any, any>> {
183
204
 
184
205
  // Update errors for next round
185
206
  currentErrors = revalidated.errors ?? []
186
-
207
+
187
208
  if (currentErrors.length === 0) {
188
209
  // No errors but also no data? Shouldn't happen, but handle gracefully
189
210
  console.error(` Unexpected state: no errors but validation failed`)
190
211
  break
191
212
  }
192
213
 
193
- console.error(` Round ${round} complete, ${currentErrors.length} error(s) remaining`)
214
+ console.error(
215
+ ` Round ${round} complete, ${currentErrors.length} error(s) remaining`,
216
+ )
194
217
  }
195
218
 
196
219
  console.error(` Schema correction failed after ${maxRounds} rounds`)
@@ -231,7 +254,9 @@ export class Predict<S extends SignatureDef<any, any>> {
231
254
  // Output format with JSON Schema
232
255
  lines.push('OUTPUT FORMAT:')
233
256
  lines.push('Return a JSON object matching this schema EXACTLY.')
234
- lines.push('IMPORTANT: For optional fields, OMIT the field entirely - do NOT use null.')
257
+ lines.push(
258
+ 'IMPORTANT: For optional fields, OMIT the field entirely - do NOT use null.',
259
+ )
235
260
  lines.push('')
236
261
  lines.push('```json')
237
262
  lines.push(JSON.stringify(this.buildOutputJsonSchema(), null, 2))
@@ -244,7 +269,10 @@ export class Predict<S extends SignatureDef<any, any>> {
244
269
  private buildOutputJsonSchema(): Record<string, unknown> {
245
270
  // Build a Zod object from the output fields
246
271
  const shape: Record<string, z.ZodType> = {}
247
- for (const [name, config] of Object.entries(this.sig.outputs) as [string, FieldConfig][]) {
272
+ for (const [name, config] of Object.entries(this.sig.outputs) as [
273
+ string,
274
+ FieldConfig,
275
+ ][]) {
248
276
  shape[name] = config.type
249
277
  }
250
278
  const outputSchema = z.object(shape)
@@ -254,9 +282,14 @@ export class Predict<S extends SignatureDef<any, any>> {
254
282
 
255
283
  // Add field descriptions from our config (toJSONSchema uses .describe() metadata)
256
284
  // Since our FieldConfig has a separate desc field, merge it in
257
- const props = jsonSchema.properties as Record<string, Record<string, unknown>> | undefined
285
+ const props = jsonSchema.properties as
286
+ | Record<string, Record<string, unknown>>
287
+ | undefined
258
288
  if (props) {
259
- for (const [name, config] of Object.entries(this.sig.outputs) as [string, FieldConfig][]) {
289
+ for (const [name, config] of Object.entries(this.sig.outputs) as [
290
+ string,
291
+ FieldConfig,
292
+ ][]) {
260
293
  if (config.desc && props[name]) {
261
294
  // Only add if not already set by .describe()
262
295
  if (!props[name].description) {
@@ -4,7 +4,7 @@
4
4
  * Signatures declare input/output contracts for LLM interactions using Zod for validation.
5
5
  */
6
6
 
7
- import { z } from 'zod'
7
+ import { z } from 'zod/v4'
8
8
  import type { FieldConfig, SignatureDef } from './types.js'
9
9
 
10
10
  /** signature creates a new signature definition. */