ocpipe 0.3.1 → 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 CHANGED
@@ -45,6 +45,20 @@ const AnalyzeCode = signature({
45
45
  - `field.nullable(field)` - Nullable wrapper
46
46
  - `field.custom(zodType, desc?)` - Custom Zod type
47
47
 
48
+ **Type inference:**
49
+
50
+ Use `InferInputs<S>` and `InferOutputs<S>` to extract TypeScript types from a signature:
51
+
52
+ ```typescript
53
+ import { InferInputs, InferOutputs } from 'ocpipe'
54
+
55
+ type AnalyzeInputs = InferInputs<typeof AnalyzeCode>
56
+ // { code: string; language: 'typescript' | 'python' | 'rust' }
57
+
58
+ type AnalyzeOutputs = InferOutputs<typeof AnalyzeCode>
59
+ // { issues: { severity: 'error' | 'warning' | 'info'; message: string; line: number }[]; suggestions: string[]; score: number }
60
+ ```
61
+
48
62
  ### Predict
49
63
 
50
64
  `Predict` bridges a Signature and OpenCode. It handles prompt generation, response parsing, and validation.
@@ -355,6 +355,31 @@ field.enum(['a', 'b'] as const) // 'a' | 'b'
355
355
  field.optional(field.string()) // string | undefined
356
356
  ```
357
357
 
358
+ ### Type Inference
359
+
360
+ Use `InferInputs` and `InferOutputs` to extract TypeScript types from a signature:
361
+
362
+ ```typescript
363
+ import { signature, field, InferInputs, InferOutputs } from 'ocpipe'
364
+
365
+ const Greet = signature({
366
+ doc: 'Generate a greeting.',
367
+ inputs: { name: field.string('Name to greet') },
368
+ outputs: { greeting: field.string('The greeting message') },
369
+ })
370
+
371
+ // Extract types from the signature
372
+ type GreetInputs = InferInputs<typeof Greet> // { name: string }
373
+ type GreetOutputs = InferOutputs<typeof Greet> // { greeting: string }
374
+
375
+ // Use in functions
376
+ function processGreeting(input: GreetInputs): void {
377
+ console.log(`Processing greeting for: ${input.name}`)
378
+ }
379
+ ```
380
+
381
+ This is useful for typing function parameters, return types, or when building generic utilities around signatures.
382
+
358
383
  ### Complex Modules
359
384
 
360
385
  For modules with multiple predictors or transformed outputs, use the base `Module` class:
package/README.md CHANGED
@@ -40,6 +40,11 @@ const pipeline = new Pipeline(
40
40
 
41
41
  const result = await pipeline.run(module(Greet), { name: 'World' })
42
42
  console.log(result.data.greeting)
43
+
44
+ // Extract types from signatures
45
+ import { InferInputs, InferOutputs } from 'ocpipe'
46
+ type GreetIn = InferInputs<typeof Greet> // { name: string }
47
+ type GreetOut = InferOutputs<typeof Greet> // { greeting: string }
43
48
  ```
44
49
 
45
50
  OpenCode CLI is bundled — run `bun run opencode` or use your system `opencode` if installed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocpipe",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "SDK for LLM pipelines with OpenCode and Zod",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -0,0 +1,55 @@
1
+ {
2
+ "sessionId": "20251231_092022",
3
+ "startedAt": "2025-12-31T09:20:22.199Z",
4
+ "phase": "init",
5
+ "steps": [
6
+ {
7
+ "stepName": "MoodAnalyzer",
8
+ "timestamp": "2025-12-31T09:20:25.808Z",
9
+ "result": {
10
+ "data": {
11
+ "mood": "happy",
12
+ "keywords": [
13
+ "happy",
14
+ "so",
15
+ "today"
16
+ ]
17
+ },
18
+ "stepName": "MoodAnalyzer",
19
+ "duration": 3608,
20
+ "sessionId": "ses_48c4aa762ffeP1pxFsOURK4cw2",
21
+ "model": {
22
+ "providerID": "github-copilot",
23
+ "modelID": "grok-code-fast-1"
24
+ },
25
+ "attempt": 1
26
+ }
27
+ },
28
+ {
29
+ "stepName": "ResponseSuggester",
30
+ "timestamp": "2025-12-31T09:20:30.186Z",
31
+ "result": {
32
+ "data": {
33
+ "suggestion": "That's wonderful! What's making you so happy today?"
34
+ },
35
+ "stepName": "ResponseSuggester",
36
+ "duration": 4377,
37
+ "sessionId": "ses_48c4aa762ffeP1pxFsOURK4cw2",
38
+ "model": {
39
+ "providerID": "github-copilot",
40
+ "modelID": "grok-code-fast-1"
41
+ },
42
+ "attempt": 1
43
+ }
44
+ }
45
+ ],
46
+ "subPipelines": [],
47
+ "inputText": "I am so happy today!",
48
+ "opencodeSessionId": "ses_48c4aa762ffeP1pxFsOURK4cw2",
49
+ "mood": "happy",
50
+ "keywords": [
51
+ "happy",
52
+ "so",
53
+ "today"
54
+ ]
55
+ }
package/src/parsing.ts CHANGED
@@ -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,
@@ -1060,3 +1063,41 @@ export function parseJsonFromResponse<T = Record<string, unknown>>(
1060
1063
  response,
1061
1064
  )
1062
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/predict.ts CHANGED
@@ -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,
@@ -78,7 +80,7 @@ export class Predict<S extends AnySignature> {
78
80
  // Update context with new session ID for continuity
79
81
  ctx.sessionId = agentResult.sessionId
80
82
 
81
- const parseResult = tryParseResponse<InferOutputs<S>>(
83
+ let parseResult = tryParseResponse<InferOutputs<S>>(
82
84
  agentResult.text,
83
85
  this.sig.outputs,
84
86
  )
@@ -94,7 +96,42 @@ export class Predict<S extends AnySignature> {
94
96
  }
95
97
  }
96
98
 
97
- // Parsing failed - attempt correction if enabled
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
98
135
  if (
99
136
  this.config.correction !== false &&
100
137
  parseResult.errors &&
@@ -135,6 +172,62 @@ export class Predict<S extends AnySignature> {
135
172
  )
136
173
  }
137
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
+
138
231
  /** correctFields attempts to fix field errors using same-session patches with retries. */
139
232
  private async correctFields(
140
233
  json: Record<string, unknown>,
package/src/types.ts CHANGED
@@ -173,7 +173,12 @@ export type CorrectionMethod = 'json-patch' | 'jq'
173
173
  export interface CorrectionConfig {
174
174
  /** Correction method to use (default: 'json-patch'). */
175
175
  method?: CorrectionMethod
176
- /** Use a different model for corrections (default: same model, same session). */
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
+ */
177
182
  model?: ModelConfig
178
183
  /** Maximum number of fields to attempt correcting per round (default: 5). */
179
184
  maxFields?: number
@@ -181,8 +186,16 @@ export interface CorrectionConfig {
181
186
  maxRounds?: number
182
187
  }
183
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
+
184
195
  /** A field-level error from schema validation. */
185
196
  export interface FieldError {
197
+ /** Error code for programmatic detection. */
198
+ code: FieldErrorCode
186
199
  /** The field path that failed (e.g., "issues.0.issue_type"). */
187
200
  path: string
188
201
  /** Human-readable error message. */