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/DESIGN.md
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# Design
|
|
2
|
+
|
|
3
|
+
ocpipe separates the **what** (Signatures declare input/output contracts), the **how** (Modules compose predictors), and the **when** (Pipelines orchestrate execution).
|
|
4
|
+
|
|
5
|
+
## Core Concepts
|
|
6
|
+
|
|
7
|
+
### Signatures
|
|
8
|
+
|
|
9
|
+
A Signature declares **what** an LLM interaction does - its inputs, outputs, and purpose.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { signature, field } from 'ocpipe'
|
|
13
|
+
import { z } from 'zod'
|
|
14
|
+
|
|
15
|
+
const AnalyzeCode = signature({
|
|
16
|
+
doc: 'Analyze code for potential issues and improvements.',
|
|
17
|
+
inputs: {
|
|
18
|
+
code: field.string('Source code to analyze'),
|
|
19
|
+
language: field.enum(['typescript', 'python', 'rust'] as const),
|
|
20
|
+
},
|
|
21
|
+
outputs: {
|
|
22
|
+
issues: field.array(
|
|
23
|
+
z.object({
|
|
24
|
+
severity: z.enum(['error', 'warning', 'info']),
|
|
25
|
+
message: z.string(),
|
|
26
|
+
line: z.number(),
|
|
27
|
+
}),
|
|
28
|
+
'List of issues found',
|
|
29
|
+
),
|
|
30
|
+
suggestions: field.array(z.string(), 'Improvement suggestions'),
|
|
31
|
+
score: field.number('Code quality score 0-100'),
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Field helpers:**
|
|
37
|
+
|
|
38
|
+
- `field.string(desc?)` - String field
|
|
39
|
+
- `field.number(desc?)` - Number field
|
|
40
|
+
- `field.boolean(desc?)` - Boolean field
|
|
41
|
+
- `field.array(itemType, desc?)` - Array field
|
|
42
|
+
- `field.object(shape, desc?)` - Object field
|
|
43
|
+
- `field.enum(values, desc?)` - Enum field
|
|
44
|
+
- `field.optional(field)` - Optional wrapper
|
|
45
|
+
- `field.nullable(field)` - Nullable wrapper
|
|
46
|
+
- `field.custom(zodType, desc?)` - Custom Zod type
|
|
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
|
+
|
|
62
|
+
### Predict
|
|
63
|
+
|
|
64
|
+
`Predict` bridges a Signature and OpenCode. It handles prompt generation, response parsing, and validation.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { Predict } from 'ocpipe'
|
|
68
|
+
|
|
69
|
+
const predict = new Predict(AnalyzeCode)
|
|
70
|
+
const result = await predict.execute(
|
|
71
|
+
{ code: '...', language: 'typescript' },
|
|
72
|
+
ctx,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
// With configuration
|
|
76
|
+
const predict = new Predict(AnalyzeCode, {
|
|
77
|
+
agent: 'code-reviewer',
|
|
78
|
+
model: { providerID: 'anthropic', modelID: 'claude-opus-4-5' },
|
|
79
|
+
newSession: true,
|
|
80
|
+
template: (inputs) => `...`,
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Module
|
|
85
|
+
|
|
86
|
+
A Module encapsulates a logical unit of work with one or more Predictors.
|
|
87
|
+
|
|
88
|
+
**SignatureModule** - For simple modules wrapping a single signature:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { SignatureModule } from 'ocpipe'
|
|
92
|
+
|
|
93
|
+
class IntentParser extends SignatureModule<typeof ParseIntent> {
|
|
94
|
+
constructor() {
|
|
95
|
+
super(ParseIntent)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async forward(input, ctx) {
|
|
99
|
+
const result = await this.predictor.execute(input, ctx)
|
|
100
|
+
return result.data
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Module** - For complex modules with multiple predictors:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { Module } from 'ocpipe'
|
|
109
|
+
|
|
110
|
+
class CodeAnalyzer extends Module<
|
|
111
|
+
{ code: string; language: string },
|
|
112
|
+
{ issues: Issue[]; score: number }
|
|
113
|
+
> {
|
|
114
|
+
private analyze = this.predict(AnalyzeCode)
|
|
115
|
+
private suggest = this.predict(SuggestFixes, { agent: 'code-fixer' })
|
|
116
|
+
|
|
117
|
+
async forward(input, ctx) {
|
|
118
|
+
const analysis = await this.analyze.execute(input, ctx)
|
|
119
|
+
|
|
120
|
+
if (analysis.data.issues.some((i) => i.severity === 'error')) {
|
|
121
|
+
const fixes = await this.suggest.execute(
|
|
122
|
+
{
|
|
123
|
+
code: input.code,
|
|
124
|
+
issues: analysis.data.issues,
|
|
125
|
+
},
|
|
126
|
+
ctx,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
issues: analysis.data.issues,
|
|
131
|
+
fixes: fixes.data.suggestions,
|
|
132
|
+
score: analysis.data.score,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
issues: analysis.data.issues,
|
|
138
|
+
score: analysis.data.score,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Pipeline
|
|
145
|
+
|
|
146
|
+
Pipeline orchestrates execution with session management, checkpointing, logging, and retry logic.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { Pipeline, createBaseState } from 'ocpipe'
|
|
150
|
+
|
|
151
|
+
const pipeline = new Pipeline(
|
|
152
|
+
{
|
|
153
|
+
name: 'code-review',
|
|
154
|
+
defaultModel: { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
|
|
155
|
+
defaultAgent: 'general',
|
|
156
|
+
checkpointDir: './ckpt',
|
|
157
|
+
logDir: './logs',
|
|
158
|
+
retry: { maxAttempts: 2, onParseError: true },
|
|
159
|
+
timeoutSec: 300,
|
|
160
|
+
},
|
|
161
|
+
createBaseState,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
// Run modules
|
|
165
|
+
const result = await pipeline.run(new CodeAnalyzer(), {
|
|
166
|
+
code: sourceCode,
|
|
167
|
+
language: 'typescript',
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// Run with step options
|
|
171
|
+
const result = await pipeline.run(new CodeAnalyzer(), input, {
|
|
172
|
+
name: 'analyze-main',
|
|
173
|
+
model: { providerID: 'anthropic', modelID: 'claude-opus-4-5' },
|
|
174
|
+
newSession: true,
|
|
175
|
+
retry: { maxAttempts: 3 },
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// Access state
|
|
179
|
+
console.log(pipeline.state.steps)
|
|
180
|
+
console.log(pipeline.getSessionId())
|
|
181
|
+
|
|
182
|
+
// Resume from checkpoint
|
|
183
|
+
const resumed = await Pipeline.loadCheckpoint(config, sessionId)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### State Management
|
|
187
|
+
|
|
188
|
+
Automatic checkpointing after each step:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { createBaseState, extendBaseState } from 'ocpipe'
|
|
192
|
+
|
|
193
|
+
// Basic state
|
|
194
|
+
const state = createBaseState()
|
|
195
|
+
// { sessionId, startedAt, phase, steps, subPipelines }
|
|
196
|
+
|
|
197
|
+
// Extended state
|
|
198
|
+
interface MyState extends BaseState {
|
|
199
|
+
inputPath: string
|
|
200
|
+
results: AnalysisResult[]
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const pipeline = new Pipeline(config, () => ({
|
|
204
|
+
...createBaseState(),
|
|
205
|
+
inputPath: '/path/to/input',
|
|
206
|
+
results: [],
|
|
207
|
+
}))
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Auto-Correction
|
|
211
|
+
|
|
212
|
+
Automatically corrects LLM schema mismatches using JSON Patch (RFC 6902):
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
super(MySignature, {
|
|
216
|
+
correction: {
|
|
217
|
+
method: 'json-patch', // or 'jq'
|
|
218
|
+
maxFields: 5,
|
|
219
|
+
maxRounds: 3,
|
|
220
|
+
},
|
|
221
|
+
})
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The correction system:
|
|
225
|
+
|
|
226
|
+
1. Detects schema validation errors
|
|
227
|
+
2. Finds similar field names in the response
|
|
228
|
+
3. Asks the LLM for patches to fix errors
|
|
229
|
+
4. Applies patches and re-validates
|
|
230
|
+
5. Retries up to configured rounds
|
|
231
|
+
|
|
232
|
+
## Testing
|
|
233
|
+
|
|
234
|
+
Mock backends for unit testing without real LLM calls:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import {
|
|
238
|
+
MockAgentBackend,
|
|
239
|
+
createMockContext,
|
|
240
|
+
generateMockOutputs,
|
|
241
|
+
} from 'ocpipe'
|
|
242
|
+
import { vi } from 'vitest'
|
|
243
|
+
|
|
244
|
+
const mock = new MockAgentBackend()
|
|
245
|
+
mock.addJsonResponse({
|
|
246
|
+
intent: 'greeting',
|
|
247
|
+
confidence: 0.95,
|
|
248
|
+
keywords: ['hello', 'world'],
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
vi.mock('./agent.js', () => ({
|
|
252
|
+
runAgent: mock.createRunner(),
|
|
253
|
+
}))
|
|
254
|
+
|
|
255
|
+
const ctx = createMockContext({
|
|
256
|
+
defaultModel: { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
// Auto-generate mock outputs from schema
|
|
260
|
+
const mockData = generateMockOutputs(ParseIntent.outputs)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Why No ChainOfThought or ReAct?
|
|
264
|
+
|
|
265
|
+
Unlike DSPy, ocpipe does not provide `ChainOfThought` or `ReAct` variants:
|
|
266
|
+
|
|
267
|
+
- OpenCode agents already do chain-of-thought reasoning
|
|
268
|
+
- OpenCode agents already have tool access (ReAct)
|
|
269
|
+
- Adding these would duplicate functionality
|
|
270
|
+
|
|
271
|
+
Configure your OpenCode agent for tool access. The agent handles complexity; ocpipe structures the contract.
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
# Getting Started with ocpipe
|
|
2
|
+
|
|
3
|
+
This guide walks you through building and running a simple "Hello World" application using ocpipe (OpenCode Pipeline).
|
|
4
|
+
|
|
5
|
+
**Repository:** https://github.com/s4wave/ocpipe
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- [Bun](https://bun.sh) runtime
|
|
10
|
+
- [OpenCode](https://opencode.ai) CLI installed and configured
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun add ocpipe zod
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start with REPL
|
|
19
|
+
|
|
20
|
+
The fastest way to explore ocpipe is with `bun repl`:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bun repl
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then paste this:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { signature, field, module, Pipeline, createBaseState } from 'ocpipe'
|
|
30
|
+
|
|
31
|
+
const Greet = signature({
|
|
32
|
+
doc: 'Generate a friendly greeting for the given name.',
|
|
33
|
+
inputs: { name: field.string('The name of the person to greet') },
|
|
34
|
+
outputs: {
|
|
35
|
+
greeting: field.string('A friendly greeting message'),
|
|
36
|
+
emoji: field.string('An appropriate emoji for the greeting'),
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const pipeline = new Pipeline(
|
|
41
|
+
{
|
|
42
|
+
name: 'repl-demo',
|
|
43
|
+
defaultModel: { providerID: 'anthropic', modelID: 'claude-haiku-4-5' },
|
|
44
|
+
defaultAgent: 'code',
|
|
45
|
+
},
|
|
46
|
+
createBaseState,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const result = await pipeline.run(module(Greet), { name: 'World' })
|
|
50
|
+
console.log(result.data.greeting, result.data.emoji)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You'll see the pipeline execute and print something like:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Hello, World! It's wonderful to meet you! :wave:
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Running the Example
|
|
60
|
+
|
|
61
|
+
The `example/` directory contains a complete hello world application. Run it directly:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bun run example/index.ts
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This will:
|
|
68
|
+
|
|
69
|
+
1. Create a pipeline with default configuration
|
|
70
|
+
2. Send a greeting request to the LLM
|
|
71
|
+
3. Print the generated greeting and emoji
|
|
72
|
+
|
|
73
|
+
**Expected output:**
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
============================================================
|
|
77
|
+
STEP 1: Greeter
|
|
78
|
+
============================================================
|
|
79
|
+
|
|
80
|
+
>>> OpenCode [code] [anthropic/claude-haiku-4-5] [new session]: Generate a friendly greeting for the given name...
|
|
81
|
+
|
|
82
|
+
<<< OpenCode done (85 chars) [session:abc123]
|
|
83
|
+
|
|
84
|
+
=== Result ===
|
|
85
|
+
Greeting: Hello, World! It's wonderful to meet you!
|
|
86
|
+
Emoji: :wave:
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Tip:** You can view what the agent did by running `opencode` to open the OpenCode UI, then typing `/sessions` to see the session list. Find the session ID from the output above and select it to see the full conversation.
|
|
90
|
+
|
|
91
|
+
## Understanding the Example
|
|
92
|
+
|
|
93
|
+
The example has three files that demonstrate ocpipe's core concepts:
|
|
94
|
+
|
|
95
|
+
### 1. Signature (`signature.ts`)
|
|
96
|
+
|
|
97
|
+
A **Signature** declares the contract between your code and the LLM. It defines:
|
|
98
|
+
|
|
99
|
+
- `doc`: Instructions for the LLM
|
|
100
|
+
- `inputs`: What data you provide
|
|
101
|
+
- `outputs`: What data you expect back
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { signature, field } from '../index.js'
|
|
105
|
+
|
|
106
|
+
export const Greet = signature({
|
|
107
|
+
doc: 'Generate a friendly greeting for the given name.',
|
|
108
|
+
inputs: {
|
|
109
|
+
name: field.string('The name of the person to greet'),
|
|
110
|
+
},
|
|
111
|
+
outputs: {
|
|
112
|
+
greeting: field.string('A friendly greeting message'),
|
|
113
|
+
emoji: field.string('An appropriate emoji for the greeting'),
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 2. Module (`module.ts`)
|
|
119
|
+
|
|
120
|
+
A **Module** wraps a signature with execution logic. `SignatureModule` is a convenience class that automatically creates a predictor from your signature:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { SignatureModule } from '../index.js'
|
|
124
|
+
import { Greet } from './signature.js'
|
|
125
|
+
|
|
126
|
+
export class Greeter extends SignatureModule<typeof Greet> {
|
|
127
|
+
constructor() {
|
|
128
|
+
super(Greet)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async forward(input: { name: string }, ctx: ExecutionContext) {
|
|
132
|
+
const result = await this.predictor.execute(input, ctx)
|
|
133
|
+
return result.data
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 3. Pipeline (`index.ts`)
|
|
139
|
+
|
|
140
|
+
A **Pipeline** orchestrates execution, managing sessions, checkpoints, and retries:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { Pipeline, createBaseState } from '../index.js'
|
|
144
|
+
import { Greeter } from './module.js'
|
|
145
|
+
|
|
146
|
+
const pipeline = new Pipeline(
|
|
147
|
+
{
|
|
148
|
+
name: 'hello-world',
|
|
149
|
+
defaultModel: { providerID: 'anthropic', modelID: 'claude-haiku-4-5' },
|
|
150
|
+
defaultAgent: 'code',
|
|
151
|
+
checkpointDir: './ckpt',
|
|
152
|
+
logDir: './logs',
|
|
153
|
+
},
|
|
154
|
+
createBaseState,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
const result = await pipeline.run(new Greeter(), { name: 'World' })
|
|
158
|
+
console.log(result.data.greeting)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Modifying the Example
|
|
162
|
+
|
|
163
|
+
Let's extend the example to generate both a greeting and a farewell.
|
|
164
|
+
|
|
165
|
+
### Step 1: Add a new signature
|
|
166
|
+
|
|
167
|
+
Create `farewell-signature.ts`:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { signature, field } from '../index.js'
|
|
171
|
+
|
|
172
|
+
export const Farewell = signature({
|
|
173
|
+
doc: 'Generate a friendly farewell for the given name.',
|
|
174
|
+
inputs: {
|
|
175
|
+
name: field.string('The name of the person to bid farewell'),
|
|
176
|
+
context: field.string(
|
|
177
|
+
'The context of the farewell (e.g., "end of meeting", "going on vacation")',
|
|
178
|
+
),
|
|
179
|
+
},
|
|
180
|
+
outputs: {
|
|
181
|
+
farewell: field.string('A friendly farewell message'),
|
|
182
|
+
emoji: field.string('An appropriate emoji for the farewell'),
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Step 2: Add a new module
|
|
188
|
+
|
|
189
|
+
Create `farewell-module.ts`:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { SignatureModule } from '../index.js'
|
|
193
|
+
import type { ExecutionContext } from '../types.js'
|
|
194
|
+
import { Farewell } from './farewell-signature.js'
|
|
195
|
+
|
|
196
|
+
export class Fareweller extends SignatureModule<typeof Farewell> {
|
|
197
|
+
constructor() {
|
|
198
|
+
super(Farewell)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async forward(
|
|
202
|
+
input: { name: string; context: string },
|
|
203
|
+
ctx: ExecutionContext,
|
|
204
|
+
) {
|
|
205
|
+
const result = await this.predictor.execute(input, ctx)
|
|
206
|
+
return result.data
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Step 3: Run both modules in sequence
|
|
212
|
+
|
|
213
|
+
Update `index.ts`:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { Pipeline, createBaseState } from '../index.js'
|
|
217
|
+
import { Greeter } from './module.js'
|
|
218
|
+
import { Fareweller } from './farewell-module.js'
|
|
219
|
+
|
|
220
|
+
async function main() {
|
|
221
|
+
const pipeline = new Pipeline(
|
|
222
|
+
{
|
|
223
|
+
name: 'hello-goodbye',
|
|
224
|
+
defaultModel: { providerID: 'anthropic', modelID: 'claude-haiku-4-5' },
|
|
225
|
+
defaultAgent: 'code',
|
|
226
|
+
checkpointDir: './ckpt',
|
|
227
|
+
logDir: './logs',
|
|
228
|
+
},
|
|
229
|
+
createBaseState,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
// Run greeter
|
|
233
|
+
const greeting = await pipeline.run(new Greeter(), { name: 'Alice' })
|
|
234
|
+
console.log(`\nGreeting: ${greeting.data.greeting} ${greeting.data.emoji}`)
|
|
235
|
+
|
|
236
|
+
// Run fareweller (reuses the same session for context)
|
|
237
|
+
const farewell = await pipeline.run(new Fareweller(), {
|
|
238
|
+
name: 'Alice',
|
|
239
|
+
context: 'end of meeting',
|
|
240
|
+
})
|
|
241
|
+
console.log(`Farewell: ${farewell.data.farewell} ${farewell.data.emoji}`)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
main().catch(console.error)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Step 4: Run it
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
bun run example/index.ts
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Auto-Correction Example
|
|
254
|
+
|
|
255
|
+
ocpipe automatically corrects schema mismatches using patches when the LLM returns incorrect field names. Run the correction demo:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
bun run example/correction.ts
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
This example uses field names that LLMs sometimes get wrong:
|
|
262
|
+
|
|
263
|
+
- `issue_type` (LLMs may return `type`)
|
|
264
|
+
- `severity` (LLMs may return `priority`)
|
|
265
|
+
- `explanation` (LLMs may return `description` or `reason`)
|
|
266
|
+
- `suggested_tags` (LLMs may return `tags`)
|
|
267
|
+
|
|
268
|
+
**Note:** Modern LLMs like Claude often follow the schema correctly. The correction system is a safety net for when they don't. You may not see correction rounds if the LLM gets it right the first time.
|
|
269
|
+
|
|
270
|
+
If the LLM does return incorrect field names, you'll see correction rounds:
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
>>> Correction round 1/3 [json-patch]: fixing 2 field(s)...
|
|
274
|
+
JSON Patch: [{"op":"move","from":"/type","path":"/issue_type"},{"op":"move","from":"/priority","path":"/severity"}]
|
|
275
|
+
Round 1 complete, 0 error(s) remaining
|
|
276
|
+
Schema correction successful after 1 round(s)!
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The correction system:
|
|
280
|
+
|
|
281
|
+
1. Validates the LLM's response against the output schema
|
|
282
|
+
2. If validation fails, identifies which fields have errors
|
|
283
|
+
3. Asks the LLM to generate patches to fix the errors
|
|
284
|
+
4. Applies patches and re-validates
|
|
285
|
+
5. Retries up to 3 rounds if needed
|
|
286
|
+
|
|
287
|
+
### Correction Methods
|
|
288
|
+
|
|
289
|
+
ocpipe supports two correction methods:
|
|
290
|
+
|
|
291
|
+
| Method | Format | Requirements |
|
|
292
|
+
| ---------------------- | -------------------- | ---------------------- |
|
|
293
|
+
| `json-patch` (default) | RFC 6902 JSON Patch | None (pure TypeScript) |
|
|
294
|
+
| `jq` | jq-style expressions | `jq` binary installed |
|
|
295
|
+
|
|
296
|
+
**JSON Patch** is the default because it requires no external dependencies and uses a standardized format that LLMs are familiar with from API documentation.
|
|
297
|
+
|
|
298
|
+
To use jq instead:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
super(MySignature, {
|
|
302
|
+
correction: {
|
|
303
|
+
method: 'jq', // Use jq-style patches (requires jq binary)
|
|
304
|
+
},
|
|
305
|
+
})
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
To disable auto-correction:
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
super(MySignature, { correction: false })
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Full configuration options:
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
super(MySignature, {
|
|
318
|
+
correction: {
|
|
319
|
+
method: 'json-patch', // 'json-patch' (default) or 'jq'
|
|
320
|
+
maxFields: 5, // Max fields to fix per round
|
|
321
|
+
maxRounds: 3, // Max correction attempts
|
|
322
|
+
},
|
|
323
|
+
})
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Key Concepts
|
|
327
|
+
|
|
328
|
+
### Session Continuity
|
|
329
|
+
|
|
330
|
+
By default, ocpipe reuses the OpenCode session across pipeline steps. This means the LLM maintains context between calls. Use `newSession: true` in run options to start fresh:
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
await pipeline.run(module, input, { newSession: true })
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Checkpointing
|
|
337
|
+
|
|
338
|
+
ocpipe automatically saves state after each step to `checkpointDir`. Resume from a checkpoint:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
const resumed = await Pipeline.loadCheckpoint(config, sessionId)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Field Types
|
|
345
|
+
|
|
346
|
+
ocpipe provides field helpers for common types:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
field.string('description') // string
|
|
350
|
+
field.number('description') // number
|
|
351
|
+
field.boolean('description') // boolean
|
|
352
|
+
field.array(z.string(), 'description') // string[]
|
|
353
|
+
field.object({ key: z.string() }) // { key: string }
|
|
354
|
+
field.enum(['a', 'b'] as const) // 'a' | 'b'
|
|
355
|
+
field.optional(field.string()) // string | undefined
|
|
356
|
+
```
|
|
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
|
+
|
|
383
|
+
### Complex Modules
|
|
384
|
+
|
|
385
|
+
For modules with multiple predictors or transformed outputs, use the base `Module` class:
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
import { Module } from '../index.js'
|
|
389
|
+
|
|
390
|
+
class ComplexModule extends Module<
|
|
391
|
+
{ input: string },
|
|
392
|
+
{ result: string; metadata: object }
|
|
393
|
+
> {
|
|
394
|
+
private step1 = this.predict(Signature1)
|
|
395
|
+
private step2 = this.predict(Signature2, { agent: 'specialist' })
|
|
396
|
+
|
|
397
|
+
async forward(input, ctx) {
|
|
398
|
+
const r1 = await this.step1.execute(input, ctx)
|
|
399
|
+
const r2 = await this.step2.execute({ data: r1.data }, ctx)
|
|
400
|
+
return { result: r2.data.output, metadata: r1.data }
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Next Steps
|
|
406
|
+
|
|
407
|
+
- Read the full [README.md](./README.md) for advanced features
|
|
408
|
+
- Check the test files (`*.test.ts`) for more usage examples
|
|
409
|
+
- Explore `testing.ts` for unit testing without real LLM calls
|