ocpipe 0.3.0 → 0.3.2
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/DESIGN.md +271 -0
- package/GETTING_STARTED.md +409 -0
- package/README.md +25 -21
- package/example/correction.ts +2 -2
- package/example/index.ts +1 -1
- package/llms.txt +200 -0
- package/package.json +13 -2
- package/src/.tmp/ocpipe-integration-test/mood-analysis_20251231_092022.json +55 -0
- package/src/agent.ts +1 -1
- package/src/index.ts +2 -2
- package/src/module.ts +17 -12
- package/src/parsing.ts +190 -42
- package/src/pipeline.ts +1 -1
- package/src/predict.ts +103 -5
- package/src/signature.ts +1 -1
- package/src/state.ts +1 -1
- package/src/testing.ts +4 -3
- package/src/types.ts +29 -8
package/src/parsing.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* ocpipe response parsing.
|
|
3
3
|
*
|
|
4
4
|
* Extracts and validates LLM responses using JSON or field marker formats.
|
|
5
5
|
*/
|
|
@@ -79,6 +79,7 @@ export function tryParseJson<T>(
|
|
|
79
79
|
ok: false,
|
|
80
80
|
errors: [
|
|
81
81
|
{
|
|
82
|
+
code: 'no_json_found',
|
|
82
83
|
path: '',
|
|
83
84
|
message: 'No JSON found in response',
|
|
84
85
|
expectedType: 'object',
|
|
@@ -96,6 +97,7 @@ export function tryParseJson<T>(
|
|
|
96
97
|
ok: false,
|
|
97
98
|
errors: [
|
|
98
99
|
{
|
|
100
|
+
code: 'json_parse_failed',
|
|
99
101
|
path: '',
|
|
100
102
|
message: `JSON parse failed: ${parseErr.message}`,
|
|
101
103
|
expectedType: 'object',
|
|
@@ -157,6 +159,7 @@ function zodErrorsToFieldErrors(
|
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
errors.push({
|
|
162
|
+
code: 'schema_validation_failed',
|
|
160
163
|
path,
|
|
161
164
|
message: issue.message,
|
|
162
165
|
expectedType,
|
|
@@ -507,13 +510,13 @@ export function applyJqPatch(
|
|
|
507
510
|
/\binput\b/,
|
|
508
511
|
/\binputs\b/,
|
|
509
512
|
/\bsystem\b/,
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
513
|
+
/@base64d/,
|
|
514
|
+
/@uri/,
|
|
515
|
+
/@csv/,
|
|
516
|
+
/@tsv/,
|
|
517
|
+
/@json/,
|
|
518
|
+
/@text/,
|
|
519
|
+
/@sh/,
|
|
517
520
|
/`[^`]*`/, // Backtick string interpolation
|
|
518
521
|
/\bimport\b/,
|
|
519
522
|
/\binclude\b/,
|
|
@@ -532,7 +535,7 @@ export function applyJqPatch(
|
|
|
532
535
|
|
|
533
536
|
// Only allow patches that look like field operations
|
|
534
537
|
// Valid: .foo = "bar", .items[0].name = .items[0].title, del(.foo) | .bar = 1
|
|
535
|
-
const safePattern = /^[\s\w
|
|
538
|
+
const safePattern = /^[\s\w[\]."'=|,:\-{}]*$/
|
|
536
539
|
if (!safePattern.test(patch)) {
|
|
537
540
|
console.error(` Invalid characters in patch, skipping: ${patch}`)
|
|
538
541
|
return obj
|
|
@@ -648,44 +651,63 @@ export function buildBatchJsonPatchPrompt(
|
|
|
648
651
|
return lines.join('\n')
|
|
649
652
|
}
|
|
650
653
|
|
|
651
|
-
/**
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
)
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
654
|
+
/**
|
|
655
|
+
* extractBalancedArray extracts a balanced JSON array from a string starting at startIdx.
|
|
656
|
+
* Returns the array substring or null if not found/unbalanced.
|
|
657
|
+
*/
|
|
658
|
+
function extractBalancedArray(text: string, startIdx: number): string | null {
|
|
659
|
+
if (startIdx === -1 || startIdx >= text.length) return null
|
|
660
|
+
|
|
661
|
+
let bracketCount = 0
|
|
662
|
+
let endIdx = startIdx
|
|
663
|
+
for (let i = startIdx; i < text.length; i++) {
|
|
664
|
+
if (text[i] === '[') bracketCount++
|
|
665
|
+
else if (text[i] === ']') {
|
|
666
|
+
bracketCount--
|
|
667
|
+
if (bracketCount === 0) {
|
|
668
|
+
endIdx = i + 1
|
|
669
|
+
break
|
|
670
|
+
}
|
|
662
671
|
}
|
|
663
672
|
}
|
|
664
673
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
674
|
+
if (endIdx > startIdx && bracketCount === 0) {
|
|
675
|
+
return text.slice(startIdx, endIdx)
|
|
676
|
+
}
|
|
677
|
+
return null
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** extractJsonPatch extracts a JSON Patch array from an LLM response. */
|
|
681
|
+
export function extractJsonPatch(response: string): JsonPatchOperation[] {
|
|
682
|
+
// Try to find JSON array in code blocks first
|
|
683
|
+
// Use indexOf to find code block boundaries to avoid ReDoS vulnerabilities
|
|
684
|
+
const codeBlockStart = response.indexOf('```')
|
|
685
|
+
if (codeBlockStart !== -1) {
|
|
686
|
+
const codeBlockEnd = response.indexOf('```', codeBlockStart + 3)
|
|
687
|
+
if (codeBlockEnd !== -1) {
|
|
688
|
+
const codeBlockContent = response.slice(codeBlockStart + 3, codeBlockEnd)
|
|
689
|
+
// Skip optional "json" language identifier and whitespace
|
|
690
|
+
const arrayStart = codeBlockContent.indexOf('[')
|
|
691
|
+
if (arrayStart !== -1) {
|
|
692
|
+
const arrayJson = extractBalancedArray(codeBlockContent, arrayStart)
|
|
693
|
+
if (arrayJson) {
|
|
694
|
+
try {
|
|
695
|
+
return JSON.parse(arrayJson) as JsonPatchOperation[]
|
|
696
|
+
} catch {
|
|
697
|
+
// Continue to try other methods
|
|
698
|
+
}
|
|
677
699
|
}
|
|
678
700
|
}
|
|
679
701
|
}
|
|
702
|
+
}
|
|
680
703
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
704
|
+
// Try to find raw JSON array by counting brackets
|
|
705
|
+
const arrayJson = extractBalancedArray(response, response.indexOf('['))
|
|
706
|
+
if (arrayJson) {
|
|
707
|
+
try {
|
|
708
|
+
return JSON.parse(arrayJson) as JsonPatchOperation[]
|
|
709
|
+
} catch {
|
|
710
|
+
// Fall through to empty array
|
|
689
711
|
}
|
|
690
712
|
}
|
|
691
713
|
|
|
@@ -775,6 +797,37 @@ export function applyJsonPatch(
|
|
|
775
797
|
return result
|
|
776
798
|
}
|
|
777
799
|
|
|
800
|
+
/** Keys that could be used for prototype pollution attacks. */
|
|
801
|
+
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
|
|
802
|
+
|
|
803
|
+
function isUnsafeKey(key: string): boolean {
|
|
804
|
+
return UNSAFE_KEYS.has(key)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Unescape a single JSON Pointer path segment according to RFC 6901.
|
|
809
|
+
* This ensures that checks for dangerous keys are applied to the
|
|
810
|
+
* effective property name, not the escaped form.
|
|
811
|
+
*/
|
|
812
|
+
function unescapeJsonPointerSegment(segment: string): string {
|
|
813
|
+
return segment.replace(/~1/g, '/').replace(/~0/g, '~')
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* isSafePathSegment determines whether a JSON Pointer path segment is safe to use
|
|
818
|
+
* as a property key on an object. It rejects keys that are known to enable
|
|
819
|
+
* prototype pollution or that contain characters commonly used in special
|
|
820
|
+
* property notations.
|
|
821
|
+
*/
|
|
822
|
+
function isSafePathSegment(segment: string): boolean {
|
|
823
|
+
// Normalize the segment as it will appear as a property key.
|
|
824
|
+
const normalized = unescapeJsonPointerSegment(String(segment))
|
|
825
|
+
if (isUnsafeKey(normalized)) return false
|
|
826
|
+
// Disallow bracket notation-style segments to avoid unexpected coercions.
|
|
827
|
+
if (normalized.includes('[') || normalized.includes(']')) return false
|
|
828
|
+
return true
|
|
829
|
+
}
|
|
830
|
+
|
|
778
831
|
/** getValueAtPath retrieves a value at a JSON Pointer path. */
|
|
779
832
|
function getValueAtPath(
|
|
780
833
|
obj: Record<string, unknown>,
|
|
@@ -782,6 +835,15 @@ function getValueAtPath(
|
|
|
782
835
|
): unknown {
|
|
783
836
|
let current: unknown = obj
|
|
784
837
|
for (const part of parts) {
|
|
838
|
+
// Block prototype-pollution: reject __proto__, constructor, prototype
|
|
839
|
+
if (
|
|
840
|
+
part === '__proto__' ||
|
|
841
|
+
part === 'constructor' ||
|
|
842
|
+
part === 'prototype'
|
|
843
|
+
) {
|
|
844
|
+
return undefined
|
|
845
|
+
}
|
|
846
|
+
if (!isSafePathSegment(part)) return undefined
|
|
785
847
|
if (current === null || current === undefined) return undefined
|
|
786
848
|
if (Array.isArray(current)) {
|
|
787
849
|
const idx = parseInt(part, 10)
|
|
@@ -806,25 +868,49 @@ function setValueAtPath(
|
|
|
806
868
|
let current: unknown = obj
|
|
807
869
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
808
870
|
const part = parts[i]!
|
|
871
|
+
// Block prototype-pollution: reject __proto__, constructor, prototype
|
|
872
|
+
if (
|
|
873
|
+
part === '__proto__' ||
|
|
874
|
+
part === 'constructor' ||
|
|
875
|
+
part === 'prototype'
|
|
876
|
+
) {
|
|
877
|
+
return
|
|
878
|
+
}
|
|
879
|
+
if (!isSafePathSegment(part)) {
|
|
880
|
+
// Avoid writing to dangerous or malformed prototype-related properties
|
|
881
|
+
return
|
|
882
|
+
}
|
|
809
883
|
if (Array.isArray(current)) {
|
|
810
884
|
const idx = parseInt(part, 10)
|
|
811
885
|
if (current[idx] === undefined) {
|
|
812
886
|
// Create intermediate object or array
|
|
813
887
|
const nextPart = parts[i + 1]!
|
|
814
|
-
current[idx] = /^\d+$/.test(nextPart) ? [] :
|
|
888
|
+
current[idx] = /^\d+$/.test(nextPart) ? [] : Object.create(null)
|
|
815
889
|
}
|
|
816
890
|
current = current[idx]
|
|
817
891
|
} else if (typeof current === 'object' && current !== null) {
|
|
818
892
|
const rec = current as Record<string, unknown>
|
|
819
893
|
if (rec[part] === undefined) {
|
|
820
894
|
const nextPart = parts[i + 1]!
|
|
821
|
-
rec[part] = /^\d+$/.test(nextPart) ? [] :
|
|
895
|
+
rec[part] = /^\d+$/.test(nextPart) ? [] : Object.create(null)
|
|
822
896
|
}
|
|
823
897
|
current = rec[part]
|
|
824
898
|
}
|
|
825
899
|
}
|
|
826
900
|
|
|
827
901
|
const lastPart = parts[parts.length - 1]!
|
|
902
|
+
// Block prototype-pollution: reject __proto__, constructor, prototype
|
|
903
|
+
if (
|
|
904
|
+
lastPart === '__proto__' ||
|
|
905
|
+
lastPart === 'constructor' ||
|
|
906
|
+
lastPart === 'prototype'
|
|
907
|
+
) {
|
|
908
|
+
return
|
|
909
|
+
}
|
|
910
|
+
if (!isSafePathSegment(lastPart)) {
|
|
911
|
+
// Avoid writing to dangerous or malformed prototype-related properties
|
|
912
|
+
return
|
|
913
|
+
}
|
|
828
914
|
if (Array.isArray(current)) {
|
|
829
915
|
const idx = parseInt(lastPart, 10)
|
|
830
916
|
current[idx] = value
|
|
@@ -843,6 +929,18 @@ function removeValueAtPath(
|
|
|
843
929
|
let current: unknown = obj
|
|
844
930
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
845
931
|
const part = parts[i]!
|
|
932
|
+
// Block prototype-pollution: reject __proto__, constructor, prototype
|
|
933
|
+
if (
|
|
934
|
+
part === '__proto__' ||
|
|
935
|
+
part === 'constructor' ||
|
|
936
|
+
part === 'prototype'
|
|
937
|
+
) {
|
|
938
|
+
return
|
|
939
|
+
}
|
|
940
|
+
if (!isSafePathSegment(part)) {
|
|
941
|
+
// Avoid accessing dangerous prototype-related properties
|
|
942
|
+
return
|
|
943
|
+
}
|
|
846
944
|
if (Array.isArray(current)) {
|
|
847
945
|
current = current[parseInt(part, 10)]
|
|
848
946
|
} else if (typeof current === 'object' && current !== null) {
|
|
@@ -853,6 +951,18 @@ function removeValueAtPath(
|
|
|
853
951
|
}
|
|
854
952
|
|
|
855
953
|
const lastPart = parts[parts.length - 1]!
|
|
954
|
+
// Block prototype-pollution: reject __proto__, constructor, prototype
|
|
955
|
+
if (
|
|
956
|
+
lastPart === '__proto__' ||
|
|
957
|
+
lastPart === 'constructor' ||
|
|
958
|
+
lastPart === 'prototype'
|
|
959
|
+
) {
|
|
960
|
+
return
|
|
961
|
+
}
|
|
962
|
+
if (!isSafePathSegment(lastPart)) {
|
|
963
|
+
// Avoid deleting dangerous or malformed properties
|
|
964
|
+
return
|
|
965
|
+
}
|
|
856
966
|
if (Array.isArray(current)) {
|
|
857
967
|
const idx = parseInt(lastPart, 10)
|
|
858
968
|
current.splice(idx, 1)
|
|
@@ -953,3 +1063,41 @@ export function parseJsonFromResponse<T = Record<string, unknown>>(
|
|
|
953
1063
|
response,
|
|
954
1064
|
)
|
|
955
1065
|
}
|
|
1066
|
+
|
|
1067
|
+
/** buildJsonRepairPrompt creates a prompt asking the model to fix malformed JSON. */
|
|
1068
|
+
export function buildJsonRepairPrompt(
|
|
1069
|
+
malformedJson: string,
|
|
1070
|
+
errorMessage: string,
|
|
1071
|
+
schema: Record<string, FieldConfig>,
|
|
1072
|
+
): string {
|
|
1073
|
+
const lines: string[] = []
|
|
1074
|
+
|
|
1075
|
+
lines.push(
|
|
1076
|
+
'Your previous JSON output has a syntax error and cannot be parsed.',
|
|
1077
|
+
)
|
|
1078
|
+
lines.push('')
|
|
1079
|
+
lines.push(`Error: ${errorMessage}`)
|
|
1080
|
+
lines.push('')
|
|
1081
|
+
lines.push('The malformed JSON (may be truncated):')
|
|
1082
|
+
lines.push('```')
|
|
1083
|
+
lines.push(malformedJson.slice(0, 2000))
|
|
1084
|
+
if (malformedJson.length > 2000) {
|
|
1085
|
+
lines.push('... (truncated)')
|
|
1086
|
+
}
|
|
1087
|
+
lines.push('```')
|
|
1088
|
+
lines.push('')
|
|
1089
|
+
lines.push('Please output the COMPLETE, VALID JSON that matches this schema:')
|
|
1090
|
+
lines.push('```json')
|
|
1091
|
+
|
|
1092
|
+
// Build a simple schema description
|
|
1093
|
+
const schemaDesc: Record<string, string> = {}
|
|
1094
|
+
for (const [name, config] of Object.entries(schema)) {
|
|
1095
|
+
schemaDesc[name] = config.desc ?? zodTypeToString(config.type)
|
|
1096
|
+
}
|
|
1097
|
+
lines.push(JSON.stringify(schemaDesc, null, 2))
|
|
1098
|
+
lines.push('```')
|
|
1099
|
+
lines.push('')
|
|
1100
|
+
lines.push('Respond with ONLY the corrected JSON object, no explanation.')
|
|
1101
|
+
|
|
1102
|
+
return lines.join('\n')
|
|
1103
|
+
}
|
package/src/pipeline.ts
CHANGED
package/src/predict.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* ocpipe Predict class.
|
|
3
3
|
*
|
|
4
4
|
* Executes a signature by generating a prompt, calling OpenCode, and parsing the response.
|
|
5
5
|
*/
|
|
@@ -20,6 +20,8 @@ import type {
|
|
|
20
20
|
import { runAgent } from './agent.js'
|
|
21
21
|
import {
|
|
22
22
|
tryParseResponse,
|
|
23
|
+
extractJsonString,
|
|
24
|
+
buildJsonRepairPrompt,
|
|
23
25
|
// jq-style patches
|
|
24
26
|
buildPatchPrompt,
|
|
25
27
|
buildBatchPatchPrompt,
|
|
@@ -47,8 +49,13 @@ export interface PredictConfig {
|
|
|
47
49
|
correction?: CorrectionConfig | false
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
type AnySignature = SignatureDef<
|
|
53
|
+
Record<string, FieldConfig>,
|
|
54
|
+
Record<string, FieldConfig>
|
|
55
|
+
>
|
|
56
|
+
|
|
50
57
|
/** Predict executes a signature by calling an LLM and parsing the response. */
|
|
51
|
-
export class Predict<S extends
|
|
58
|
+
export class Predict<S extends AnySignature> {
|
|
52
59
|
constructor(
|
|
53
60
|
public readonly sig: S,
|
|
54
61
|
public readonly config: PredictConfig = {},
|
|
@@ -73,7 +80,7 @@ export class Predict<S extends SignatureDef<any, any>> {
|
|
|
73
80
|
// Update context with new session ID for continuity
|
|
74
81
|
ctx.sessionId = agentResult.sessionId
|
|
75
82
|
|
|
76
|
-
|
|
83
|
+
let parseResult = tryParseResponse<InferOutputs<S>>(
|
|
77
84
|
agentResult.text,
|
|
78
85
|
this.sig.outputs,
|
|
79
86
|
)
|
|
@@ -89,7 +96,42 @@ export class Predict<S extends SignatureDef<any, any>> {
|
|
|
89
96
|
}
|
|
90
97
|
}
|
|
91
98
|
|
|
92
|
-
//
|
|
99
|
+
// Check if this is a JSON parse error (malformed JSON, not schema validation)
|
|
100
|
+
const isJsonParseError =
|
|
101
|
+
parseResult.errors?.some(
|
|
102
|
+
(e) => e.code === 'json_parse_failed' || e.code === 'no_json_found',
|
|
103
|
+
) ?? false
|
|
104
|
+
|
|
105
|
+
// Attempt JSON repair if enabled and we have a parse error
|
|
106
|
+
if (this.config.correction !== false && isJsonParseError) {
|
|
107
|
+
const rawJson = extractJsonString(agentResult.text)
|
|
108
|
+
const repairedResult = await this.repairJson(
|
|
109
|
+
rawJson ?? agentResult.text,
|
|
110
|
+
parseResult.errors?.[0]?.message ?? 'JSON parse failed',
|
|
111
|
+
ctx,
|
|
112
|
+
agentResult.sessionId,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if (repairedResult) {
|
|
116
|
+
// Re-parse the repaired response
|
|
117
|
+
parseResult = tryParseResponse<InferOutputs<S>>(
|
|
118
|
+
repairedResult,
|
|
119
|
+
this.sig.outputs,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if (parseResult.ok && parseResult.data) {
|
|
123
|
+
return {
|
|
124
|
+
data: parseResult.data,
|
|
125
|
+
raw: agentResult.text,
|
|
126
|
+
sessionId: agentResult.sessionId,
|
|
127
|
+
duration: Date.now() - startTime,
|
|
128
|
+
model: this.config.model ?? ctx.defaultModel,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Parsing failed - attempt field correction if enabled and we have parsed JSON
|
|
93
135
|
if (
|
|
94
136
|
this.config.correction !== false &&
|
|
95
137
|
parseResult.errors &&
|
|
@@ -130,6 +172,62 @@ export class Predict<S extends SignatureDef<any, any>> {
|
|
|
130
172
|
)
|
|
131
173
|
}
|
|
132
174
|
|
|
175
|
+
/** repairJson asks the model to fix malformed JSON. */
|
|
176
|
+
private async repairJson(
|
|
177
|
+
malformedJson: string,
|
|
178
|
+
errorMessage: string,
|
|
179
|
+
ctx: ExecutionContext,
|
|
180
|
+
sessionId: string,
|
|
181
|
+
): Promise<string | null> {
|
|
182
|
+
const correctionConfig =
|
|
183
|
+
typeof this.config.correction === 'object' ? this.config.correction : {}
|
|
184
|
+
const maxRounds = correctionConfig.maxRounds ?? 3
|
|
185
|
+
const correctionModel = correctionConfig.model
|
|
186
|
+
|
|
187
|
+
for (let round = 1; round <= maxRounds; round++) {
|
|
188
|
+
console.error(
|
|
189
|
+
`\n>>> JSON repair round ${round}/${maxRounds}: fixing malformed JSON...`,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
const repairPrompt = buildJsonRepairPrompt(
|
|
193
|
+
malformedJson,
|
|
194
|
+
errorMessage,
|
|
195
|
+
this.sig.outputs,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
// Use same session so the model has context of what it was trying to output
|
|
199
|
+
const repairResult = await runAgent({
|
|
200
|
+
prompt: repairPrompt,
|
|
201
|
+
model: correctionModel ?? ctx.defaultModel,
|
|
202
|
+
sessionId: correctionModel ? undefined : sessionId,
|
|
203
|
+
agent: ctx.defaultAgent,
|
|
204
|
+
timeoutSec: 60,
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// Try to parse the repaired JSON
|
|
208
|
+
const repairedJson = extractJsonString(repairResult.text)
|
|
209
|
+
if (repairedJson) {
|
|
210
|
+
try {
|
|
211
|
+
JSON.parse(repairedJson)
|
|
212
|
+
console.error(` JSON repair successful after ${round} round(s)!`)
|
|
213
|
+
return repairedJson
|
|
214
|
+
} catch (e) {
|
|
215
|
+
const parseErr = e as SyntaxError
|
|
216
|
+
console.error(` Repair attempt ${round} failed: ${parseErr.message}`)
|
|
217
|
+
malformedJson = repairedJson
|
|
218
|
+
errorMessage = parseErr.message
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
console.error(
|
|
222
|
+
` Repair attempt ${round} failed: no JSON found in response`,
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.error(` JSON repair failed after ${maxRounds} rounds`)
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
|
|
133
231
|
/** correctFields attempts to fix field errors using same-session patches with retries. */
|
|
134
232
|
private async correctFields(
|
|
135
233
|
json: Record<string, unknown>,
|
|
@@ -239,7 +337,7 @@ export class Predict<S extends SignatureDef<any, any>> {
|
|
|
239
337
|
|
|
240
338
|
// Input fields as JSON
|
|
241
339
|
const inputsWithDescriptions: Record<string, unknown> = {}
|
|
242
|
-
for (const [name
|
|
340
|
+
for (const [name] of Object.entries(this.sig.inputs) as [
|
|
243
341
|
string,
|
|
244
342
|
FieldConfig,
|
|
245
343
|
][]) {
|
package/src/signature.ts
CHANGED
package/src/state.ts
CHANGED
package/src/testing.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* ocpipe testing utilities.
|
|
3
3
|
*
|
|
4
|
-
* Provides mock backends and test helpers for unit testing
|
|
4
|
+
* Provides mock backends and test helpers for unit testing ocpipe components.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { RunAgentOptions, RunAgentResult, FieldConfig } from './types.js'
|
|
@@ -175,11 +175,12 @@ export function generateMockOutputs(
|
|
|
175
175
|
case 'ZodObject':
|
|
176
176
|
result[name] = {}
|
|
177
177
|
break
|
|
178
|
-
case 'ZodEnum':
|
|
178
|
+
case 'ZodEnum': {
|
|
179
179
|
// Get first enum value via options property
|
|
180
180
|
const enumType = config.type as { options?: readonly string[] }
|
|
181
181
|
result[name] = enumType.options?.[0] ?? 'unknown'
|
|
182
182
|
break
|
|
183
|
+
}
|
|
183
184
|
default:
|
|
184
185
|
result[name] = null
|
|
185
186
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Core type definitions for the Declarative Self-Improving TypeScript SDK.
|
|
2
|
+
* ocpipe shared types.
|
|
5
3
|
*/
|
|
6
4
|
|
|
7
5
|
import type { z } from 'zod/v4'
|
|
@@ -131,14 +129,24 @@ export interface SignatureDef<
|
|
|
131
129
|
}
|
|
132
130
|
|
|
133
131
|
/** Infer the input type from a signature definition. */
|
|
134
|
-
export type InferInputs<
|
|
135
|
-
S extends SignatureDef<
|
|
132
|
+
export type InferInputs<
|
|
133
|
+
S extends SignatureDef<
|
|
134
|
+
Record<string, FieldConfig>,
|
|
135
|
+
Record<string, FieldConfig>
|
|
136
|
+
>,
|
|
137
|
+
> =
|
|
138
|
+
S extends SignatureDef<infer I, Record<string, FieldConfig>> ?
|
|
136
139
|
{ [K in keyof I]: z.infer<I[K]['type']> }
|
|
137
140
|
: never
|
|
138
141
|
|
|
139
142
|
/** Infer the output type from a signature definition. */
|
|
140
|
-
export type InferOutputs<
|
|
141
|
-
S extends SignatureDef<
|
|
143
|
+
export type InferOutputs<
|
|
144
|
+
S extends SignatureDef<
|
|
145
|
+
Record<string, FieldConfig>,
|
|
146
|
+
Record<string, FieldConfig>
|
|
147
|
+
>,
|
|
148
|
+
> =
|
|
149
|
+
S extends SignatureDef<Record<string, FieldConfig>, infer O> ?
|
|
142
150
|
{ [K in keyof O]: z.infer<O[K]['type']> }
|
|
143
151
|
: never
|
|
144
152
|
|
|
@@ -165,7 +173,12 @@ export type CorrectionMethod = 'json-patch' | 'jq'
|
|
|
165
173
|
export interface CorrectionConfig {
|
|
166
174
|
/** Correction method to use (default: 'json-patch'). */
|
|
167
175
|
method?: CorrectionMethod
|
|
168
|
-
/**
|
|
176
|
+
/**
|
|
177
|
+
* Use a different model for corrections.
|
|
178
|
+
* When specified, the correction runs in a new session (no context from the original model).
|
|
179
|
+
* When not specified, corrections reuse the original session so the model has context
|
|
180
|
+
* of what it was trying to output.
|
|
181
|
+
*/
|
|
169
182
|
model?: ModelConfig
|
|
170
183
|
/** Maximum number of fields to attempt correcting per round (default: 5). */
|
|
171
184
|
maxFields?: number
|
|
@@ -173,8 +186,16 @@ export interface CorrectionConfig {
|
|
|
173
186
|
maxRounds?: number
|
|
174
187
|
}
|
|
175
188
|
|
|
189
|
+
/** Error codes for field errors, enabling robust error type detection. */
|
|
190
|
+
export type FieldErrorCode =
|
|
191
|
+
| 'json_parse_failed'
|
|
192
|
+
| 'no_json_found'
|
|
193
|
+
| 'schema_validation_failed'
|
|
194
|
+
|
|
176
195
|
/** A field-level error from schema validation. */
|
|
177
196
|
export interface FieldError {
|
|
197
|
+
/** Error code for programmatic detection. */
|
|
198
|
+
code: FieldErrorCode
|
|
178
199
|
/** The field path that failed (e.g., "issues.0.issue_type"). */
|
|
179
200
|
path: string
|
|
180
201
|
/** Human-readable error message. */
|