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.
- package/LICENSE +21 -0
- package/README.md +37 -311
- package/example/ckpt/hello-world_20251227_044217.json +27 -0
- package/example/correction.ts +21 -7
- package/example/index.ts +1 -1
- package/example/module.ts +2 -2
- package/example/signature.ts +1 -1
- package/package.json +24 -12
- package/{agent.ts → src/agent.ts} +46 -21
- package/{index.ts → src/index.ts} +3 -16
- package/{module.ts → src/module.ts} +25 -0
- package/{parsing.ts → src/parsing.ts} +131 -41
- package/src/paths.ts +4 -0
- package/{pipeline.ts → src/pipeline.ts} +3 -2
- package/{predict.ts → src/predict.ts} +55 -22
- package/{signature.ts → src/signature.ts} +1 -1
- package/{testing.ts → src/testing.ts} +19 -11
- package/{types.ts → src/types.ts} +7 -7
- /package/{state.ts → src/state.ts} +0 -0
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @example
|
|
7
7
|
* ```typescript
|
|
8
|
-
* import { signature, field,
|
|
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(
|
|
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: [
|
|
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: [
|
|
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 =
|
|
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 (
|
|
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(
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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 } })
|
|
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(
|
|
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(
|
|
388
|
+
lines.push(
|
|
389
|
+
`${i + 1}. Field "${error.path}": ${error.message} (expected: ${error.expectedType})`,
|
|
390
|
+
)
|
|
358
391
|
if (error.foundField) {
|
|
359
|
-
lines.push(
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
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
|
-
/\$/,
|
|
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(
|
|
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(
|
|
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(
|
|
551
|
-
|
|
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(
|
|
623
|
+
lines.push(
|
|
624
|
+
`${i + 1}. Field "${error.path}": ${error.message} (expected: ${error.expectedType})`,
|
|
625
|
+
)
|
|
571
626
|
if (error.foundField) {
|
|
572
|
-
lines.push(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
809
|
-
|
|
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
|
@@ -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 ??
|
|
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 (
|
|
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 =
|
|
115
|
-
|
|
116
|
-
|
|
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 =
|
|
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<
|
|
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(
|
|
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 =
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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(
|
|
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(
|
|
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 [
|
|
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
|
|
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 [
|
|
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. */
|