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 +28 -28
- package/example/ckpt/hello-world_20251227_044217.json +27 -0
- package/example/correction.ts +19 -5
- package/package.json +6 -1
- package/src/agent.ts +45 -20
- package/src/module.ts +7 -2
- package/src/parsing.ts +130 -40
- package/src/pipeline.ts +3 -2
- package/src/predict.ts +54 -21
- package/src/testing.ts +19 -11
- package/src/types.ts +6 -6
package/README.md
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
},
|
|
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)
|
|
38
|
+
console.log(result.data.greeting) // "Hello, World! It's wonderful to meet you!"
|
|
42
39
|
```
|
|
43
40
|
|
|
44
|
-
|
|
41
|
+
### Installation
|
|
45
42
|
|
|
46
43
|
```bash
|
|
47
|
-
bun
|
|
44
|
+
bun init
|
|
45
|
+
bun add ocpipe
|
|
48
46
|
```
|
|
49
47
|
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
+
---
|
|
59
59
|
|
|
60
|
-
[
|
|
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
|
+
}
|
package/example/correction.ts
CHANGED
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { z } from 'zod/v4'
|
|
18
|
-
import {
|
|
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(
|
|
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(
|
|
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 =
|
|
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:
|
|
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.
|
|
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 = [
|
|
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
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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<
|
|
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(
|
|
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: [
|
|
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/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 ??
|
|
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 (
|
|
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) {
|
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(
|
|
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:
|
|
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(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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(
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|