ocpipe 0.2.1 → 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/README.md CHANGED
@@ -1,22 +1,17 @@
1
- <div align="center">
2
- <h3>OpenCode Pipeline</h3>
3
- <p>SDK for LLM pipelines with <a href="https://opencode.ai">OpenCode</a> and <a href="https://zod.dev">Zod</a>.</p>
4
- </div>
1
+ <p align="center"><strong>ocpipe</strong></p>
2
+ <p align="center">SDK for LLM pipelines with <a href="https://github.com/sst/opencode">OpenCode</a> and <a href="https://zod.dev">Zod</a>.</p>
3
+ <p align="center">
4
+ <a href="https://www.npmjs.com/package/ocpipe"><img alt="npm" src="https://img.shields.io/npm/v/ocpipe?style=flat-square" /></a>
5
+ <a href="https://github.com/s4wave/ocpipe/actions"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/s4wave/ocpipe/tests.yml?style=flat-square&branch=master" /></a>
6
+ </p>
5
7
 
6
- <div align="center">
8
+ ---
7
9
 
8
- ```
9
- Signature → Predict → Module → Pipeline
10
- │ │ │ │
11
- what execute compose orchestrate
12
- ```
13
-
14
- </div>
10
+ ### Quick Start
15
11
 
16
12
  ```typescript
17
13
  import { signature, field, module, Pipeline, createBaseState } from 'ocpipe'
18
14
 
19
- // Define a signature
20
15
  const Greet = signature({
21
16
  doc: 'Generate a friendly greeting for the given name.',
22
17
  inputs: {
@@ -28,33 +23,38 @@ const Greet = signature({
28
23
  },
29
24
  })
30
25
 
31
- // Run in a pipeline
32
- const pipeline = new Pipeline({
33
- name: 'hello-world',
34
- defaultModel: { providerID: 'anthropic', modelID: 'claude-haiku-4-5' },
35
- defaultAgent: 'code',
36
- checkpointDir: './ckpt',
37
- logDir: './logs',
38
- }, createBaseState)
26
+ const pipeline = new Pipeline(
27
+ {
28
+ name: 'hello-world',
29
+ defaultModel: { providerID: 'anthropic', modelID: 'claude-haiku-4-5' },
30
+ defaultAgent: 'code',
31
+ checkpointDir: './ckpt',
32
+ logDir: './logs',
33
+ },
34
+ createBaseState,
35
+ )
39
36
 
40
37
  const result = await pipeline.run(module(Greet), { name: 'World' })
41
- console.log(result.data.greeting) // "Hello, World! It's wonderful to meet you!"
38
+ console.log(result.data.greeting) // "Hello, World! It's wonderful to meet you!"
42
39
  ```
43
40
 
44
- ## Install
41
+ ### Installation
45
42
 
46
43
  ```bash
47
- bun add ocpipe zod
44
+ bun init
45
+ bun add ocpipe
48
46
  ```
49
47
 
50
- Requires [Bun](https://bun.sh) and [OpenCode](https://opencode.ai) CLI.
48
+ OpenCode CLI is bundled — run `bun run opencode` or use your system `opencode` if installed (preferred).
49
+
50
+ See [example/](./example) for a complete example.
51
51
 
52
- ## Documentation
52
+ ### Documentation
53
53
 
54
54
  - [Getting Started](./GETTING_STARTED.md) - Tutorial with examples
55
55
  - [Design](./DESIGN.md) - Architecture and concepts
56
56
  - [Contributing](./CONTRIBUTING.md) - Development setup
57
57
 
58
- ## License
58
+ ---
59
59
 
60
- [MIT](./LICENSE)
60
+ **Join the OpenCode community** [Discord](https://opencode.ai/discord) | follow on [X.com](https://x.com/opencode)
@@ -0,0 +1,27 @@
1
+ {
2
+ "sessionId": "20251227_044217",
3
+ "startedAt": "2025-12-27T04:42:17.756Z",
4
+ "phase": "init",
5
+ "steps": [
6
+ {
7
+ "stepName": "Greeter",
8
+ "timestamp": "2025-12-27T04:42:22.145Z",
9
+ "result": {
10
+ "data": {
11
+ "greeting": "Hello, World! Welcome!",
12
+ "emoji": "👋"
13
+ },
14
+ "stepName": "Greeter",
15
+ "duration": 4389,
16
+ "sessionId": "ses_4a1e2aaceffedog5Q2374azn79",
17
+ "model": {
18
+ "providerID": "anthropic",
19
+ "modelID": "claude-haiku-4-5"
20
+ },
21
+ "attempt": 1
22
+ }
23
+ }
24
+ ],
25
+ "subPipelines": [],
26
+ "opencodeSessionId": "ses_4a1e2aaceffedog5Q2374azn79"
27
+ }
@@ -15,7 +15,13 @@
15
15
  */
16
16
 
17
17
  import { z } from 'zod/v4'
18
- import { Pipeline, createBaseState, signature, field, SignatureModule } from '../src/index.js'
18
+ import {
19
+ Pipeline,
20
+ createBaseState,
21
+ signature,
22
+ field,
23
+ SignatureModule,
24
+ } from '../src/index.js'
19
25
  import type { CorrectionMethod, ExecutionContext } from '../src/types.js'
20
26
 
21
27
  // A signature with field names that LLMs often get wrong
@@ -28,9 +34,15 @@ IMPORTANT: Use the EXACT field names specified in the schema.`,
28
34
  },
29
35
  outputs: {
30
36
  // LLMs often return "type" instead of "issue_type"
31
- issue_type: field.enum(['bug', 'feature', 'refactor', 'docs'] as const, 'Category of the issue'),
37
+ issue_type: field.enum(
38
+ ['bug', 'feature', 'refactor', 'docs'] as const,
39
+ 'Category of the issue',
40
+ ),
32
41
  // LLMs often return "priority" instead of "severity"
33
- severity: field.enum(['low', 'medium', 'high', 'critical'] as const, 'How severe is the issue'),
42
+ severity: field.enum(
43
+ ['low', 'medium', 'high', 'critical'] as const,
44
+ 'How severe is the issue',
45
+ ),
34
46
  // LLMs often return "description" or "reason" instead of "explanation"
35
47
  explanation: field.string('Detailed explanation of the issue'),
36
48
  // LLMs often return just "tags" or "labels"
@@ -53,7 +65,8 @@ class IssueAnalyzer extends SignatureModule<typeof AnalyzeIssue> {
53
65
 
54
66
  async function main() {
55
67
  // Check for --jq flag
56
- const method: CorrectionMethod = process.argv.includes('--jq') ? 'jq' : 'json-patch'
68
+ const method: CorrectionMethod =
69
+ process.argv.includes('--jq') ? 'jq' : 'json-patch'
57
70
 
58
71
  const pipeline = new Pipeline(
59
72
  {
@@ -72,7 +85,8 @@ async function main() {
72
85
  console.log('Watch the correction rounds fix schema mismatches.\n')
73
86
 
74
87
  const result = await pipeline.run(new IssueAnalyzer(method), {
75
- description: 'The login button does not respond when clicked on mobile devices',
88
+ description:
89
+ 'The login button does not respond when clicked on mobile devices',
76
90
  })
77
91
 
78
92
  console.log('\n=== Final Result ===')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocpipe",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "SDK for LLM pipelines with OpenCode and Zod",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -28,15 +28,20 @@
28
28
  "engines": {
29
29
  "bun": ">=1.0.0"
30
30
  },
31
+ "dependencies": {
32
+ "opencode-ai": "latest"
33
+ },
31
34
  "peerDependencies": {
32
35
  "zod": "^4.0.0"
33
36
  },
34
37
  "devDependencies": {
35
38
  "bun-types": "^1.3.5",
39
+ "prettier": "^3.7.4",
36
40
  "typescript": "^5.0.0",
37
41
  "vitest": "^4.0.0"
38
42
  },
39
43
  "scripts": {
44
+ "format": "bun run prettier --write .",
40
45
  "typecheck": "tsc --noEmit",
41
46
  "test": "vitest run",
42
47
  "test:watch": "vitest",
package/src/agent.ts CHANGED
@@ -4,11 +4,30 @@
4
4
  * Wraps the OpenCode CLI for running LLM agents with session management.
5
5
  */
6
6
 
7
- import { spawn } from 'child_process'
7
+ import { spawn, execSync } from 'child_process'
8
8
  import { mkdir } from 'fs/promises'
9
9
  import { PROJECT_ROOT, TMP_DIR } from './paths.js'
10
10
  import type { RunAgentOptions, RunAgentResult } from './types.js'
11
11
 
12
+ /** Check if opencode is available in system PATH */
13
+ function hasSystemOpencode(): boolean {
14
+ try {
15
+ execSync('which opencode', { stdio: 'ignore' })
16
+ return true
17
+ } catch {
18
+ return false
19
+ }
20
+ }
21
+
22
+ /** Get command and args to invoke opencode */
23
+ function getOpencodeCommand(args: string[]): { cmd: string; args: string[] } {
24
+ if (hasSystemOpencode()) {
25
+ return { cmd: 'opencode', args }
26
+ }
27
+ // Fallback to bunx with ocpipe package (which has opencode-ai as dependency)
28
+ return { cmd: 'bunx', args: ['-p', 'ocpipe', 'opencode', ...args] }
29
+ }
30
+
12
31
  /** runAgent executes an OpenCode agent with a prompt, streaming output in real-time. */
13
32
  export async function runAgent(
14
33
  options: RunAgentOptions,
@@ -23,14 +42,23 @@ export async function runAgent(
23
42
  `\n>>> OpenCode [${agent}] [${modelStr}] ${sessionInfo}: ${promptPreview}...`,
24
43
  )
25
44
 
26
- const args = ['run', '--format', 'default', '--agent', agent, '--model', modelStr]
45
+ const args = [
46
+ 'run',
47
+ '--format',
48
+ 'default',
49
+ '--agent',
50
+ agent,
51
+ '--model',
52
+ modelStr,
53
+ ]
27
54
 
28
55
  if (sessionId) {
29
56
  args.push('--session', sessionId)
30
57
  }
31
58
 
32
59
  return new Promise((resolve, reject) => {
33
- const proc = spawn('opencode', args, {
60
+ const opencodeCmd = getOpencodeCommand(args)
61
+ const proc = spawn(opencodeCmd.cmd, opencodeCmd.args, {
34
62
  cwd: PROJECT_ROOT,
35
63
  stdio: ['pipe', 'pipe', 'pipe'],
36
64
  })
@@ -116,23 +144,20 @@ async function exportSession(sessionId: string): Promise<string | null> {
116
144
 
117
145
  try {
118
146
  await mkdir(TMP_DIR, { recursive: true })
119
- const proc = Bun.spawn(
120
- [
121
- 'opencode',
122
- 'session',
123
- 'export',
124
- sessionId,
125
- '--format',
126
- 'json',
127
- '-o',
128
- tmpPath,
129
- ],
130
- {
131
- cwd: PROJECT_ROOT,
132
- stdout: 'pipe',
133
- stderr: 'pipe',
134
- },
135
- )
147
+ const opencodeCmd = getOpencodeCommand([
148
+ 'session',
149
+ 'export',
150
+ sessionId,
151
+ '--format',
152
+ 'json',
153
+ '-o',
154
+ tmpPath,
155
+ ])
156
+ const proc = Bun.spawn([opencodeCmd.cmd, ...opencodeCmd.args], {
157
+ cwd: PROJECT_ROOT,
158
+ stdout: 'pipe',
159
+ stderr: 'pipe',
160
+ })
136
161
 
137
162
  await proc.exited
138
163
 
package/src/module.ts CHANGED
@@ -51,12 +51,17 @@ export abstract class SignatureModule<
51
51
  }
52
52
 
53
53
  /** SimpleModule is a SignatureModule that just executes the predictor. */
54
- class SimpleModule<S extends SignatureDef<any, any>> extends SignatureModule<S> {
54
+ class SimpleModule<
55
+ S extends SignatureDef<any, any>,
56
+ > extends SignatureModule<S> {
55
57
  constructor(sig: S, config?: PredictConfig) {
56
58
  super(sig, config)
57
59
  }
58
60
 
59
- async forward(input: InferInputs<S>, ctx: ExecutionContext): Promise<InferOutputs<S>> {
61
+ async forward(
62
+ input: InferInputs<S>,
63
+ ctx: ExecutionContext,
64
+ ): Promise<InferOutputs<S>> {
60
65
  return (await this.predictor.execute(input, ctx)).data
61
66
  }
62
67
  }
package/src/parsing.ts CHANGED
@@ -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/pipeline.ts CHANGED
@@ -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
package/src/predict.ts CHANGED
@@ -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) {
package/src/testing.ts CHANGED
@@ -34,7 +34,10 @@ export class MockAgentBackend {
34
34
  }
35
35
 
36
36
  /** addJsonResponse adds a mock JSON response. */
37
- addJsonResponse(data: Record<string, unknown>, options?: Partial<MockResponse>): this {
37
+ addJsonResponse(
38
+ data: Record<string, unknown>,
39
+ options?: Partial<MockResponse>,
40
+ ): this {
38
41
  return this.addResponse({
39
42
  response: JSON.stringify(data, null, 2),
40
43
  ...options,
@@ -76,7 +79,7 @@ export class MockAgentBackend {
76
79
  for (let i = 0; i < this.responses.length; i++) {
77
80
  const r = this.responses[i]
78
81
  if (!r) continue
79
-
82
+
80
83
  if (!r.match) {
81
84
  response = r
82
85
  responseIndex = i
@@ -116,7 +119,8 @@ export class MockAgentBackend {
116
119
 
117
120
  return {
118
121
  text: response.response ?? '',
119
- sessionId: response.sessionId ?? options.sessionId ?? this.defaultSessionId,
122
+ sessionId:
123
+ response.sessionId ?? options.sessionId ?? this.defaultSessionId,
120
124
  }
121
125
  }
122
126
 
@@ -127,12 +131,14 @@ export class MockAgentBackend {
127
131
  }
128
132
 
129
133
  /** createMockContext creates a test execution context. */
130
- export function createMockContext(overrides?: Partial<{
131
- sessionId: string
132
- defaultModel: { providerID: string; modelID: string }
133
- defaultAgent: string
134
- timeoutSec: number
135
- }>) {
134
+ export function createMockContext(
135
+ overrides?: Partial<{
136
+ sessionId: string
137
+ defaultModel: { providerID: string; modelID: string }
138
+ defaultAgent: string
139
+ timeoutSec: number
140
+ }>,
141
+ ) {
136
142
  return {
137
143
  sessionId: overrides?.sessionId,
138
144
  defaultModel: overrides?.defaultModel ?? {
@@ -145,12 +151,14 @@ export function createMockContext(overrides?: Partial<{
145
151
  }
146
152
 
147
153
  /** generateMockOutputs creates mock output data based on a schema. */
148
- export function generateMockOutputs(schema: Record<string, FieldConfig>): Record<string, unknown> {
154
+ export function generateMockOutputs(
155
+ schema: Record<string, FieldConfig>,
156
+ ): Record<string, unknown> {
149
157
  const result: Record<string, unknown> = {}
150
158
  for (const [name, config] of Object.entries(schema)) {
151
159
  // Use constructor name for type detection (works across zod versions)
152
160
  const typeName = config.type.constructor.name
153
-
161
+
154
162
  switch (typeName) {
155
163
  case 'ZodString':
156
164
  result[name] = `mock_${name}`
package/src/types.ts CHANGED
@@ -132,15 +132,15 @@ export interface SignatureDef<
132
132
 
133
133
  /** Infer the input type from a signature definition. */
134
134
  export type InferInputs<S extends SignatureDef<any, any>> =
135
- S extends SignatureDef<infer I, any>
136
- ? { [K in keyof I]: z.infer<I[K]['type']> }
137
- : never
135
+ S extends SignatureDef<infer I, any> ?
136
+ { [K in keyof I]: z.infer<I[K]['type']> }
137
+ : never
138
138
 
139
139
  /** Infer the output type from a signature definition. */
140
140
  export type InferOutputs<S extends SignatureDef<any, any>> =
141
- S extends SignatureDef<any, infer O>
142
- ? { [K in keyof O]: z.infer<O[K]['type']> }
143
- : never
141
+ S extends SignatureDef<any, infer O> ?
142
+ { [K in keyof O]: z.infer<O[K]['type']> }
143
+ : never
144
144
 
145
145
  // ============================================================================
146
146
  // Retry Configuration