ocpipe 0.2.1 → 0.3.1

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 ADDED
@@ -0,0 +1,257 @@
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
+ ### Predict
49
+
50
+ `Predict` bridges a Signature and OpenCode. It handles prompt generation, response parsing, and validation.
51
+
52
+ ```typescript
53
+ import { Predict } from 'ocpipe'
54
+
55
+ const predict = new Predict(AnalyzeCode)
56
+ const result = await predict.execute(
57
+ { code: '...', language: 'typescript' },
58
+ ctx,
59
+ )
60
+
61
+ // With configuration
62
+ const predict = new Predict(AnalyzeCode, {
63
+ agent: 'code-reviewer',
64
+ model: { providerID: 'anthropic', modelID: 'claude-opus-4-5' },
65
+ newSession: true,
66
+ template: (inputs) => `...`,
67
+ })
68
+ ```
69
+
70
+ ### Module
71
+
72
+ A Module encapsulates a logical unit of work with one or more Predictors.
73
+
74
+ **SignatureModule** - For simple modules wrapping a single signature:
75
+
76
+ ```typescript
77
+ import { SignatureModule } from 'ocpipe'
78
+
79
+ class IntentParser extends SignatureModule<typeof ParseIntent> {
80
+ constructor() {
81
+ super(ParseIntent)
82
+ }
83
+
84
+ async forward(input, ctx) {
85
+ const result = await this.predictor.execute(input, ctx)
86
+ return result.data
87
+ }
88
+ }
89
+ ```
90
+
91
+ **Module** - For complex modules with multiple predictors:
92
+
93
+ ```typescript
94
+ import { Module } from 'ocpipe'
95
+
96
+ class CodeAnalyzer extends Module<
97
+ { code: string; language: string },
98
+ { issues: Issue[]; score: number }
99
+ > {
100
+ private analyze = this.predict(AnalyzeCode)
101
+ private suggest = this.predict(SuggestFixes, { agent: 'code-fixer' })
102
+
103
+ async forward(input, ctx) {
104
+ const analysis = await this.analyze.execute(input, ctx)
105
+
106
+ if (analysis.data.issues.some((i) => i.severity === 'error')) {
107
+ const fixes = await this.suggest.execute(
108
+ {
109
+ code: input.code,
110
+ issues: analysis.data.issues,
111
+ },
112
+ ctx,
113
+ )
114
+
115
+ return {
116
+ issues: analysis.data.issues,
117
+ fixes: fixes.data.suggestions,
118
+ score: analysis.data.score,
119
+ }
120
+ }
121
+
122
+ return {
123
+ issues: analysis.data.issues,
124
+ score: analysis.data.score,
125
+ }
126
+ }
127
+ }
128
+ ```
129
+
130
+ ### Pipeline
131
+
132
+ Pipeline orchestrates execution with session management, checkpointing, logging, and retry logic.
133
+
134
+ ```typescript
135
+ import { Pipeline, createBaseState } from 'ocpipe'
136
+
137
+ const pipeline = new Pipeline(
138
+ {
139
+ name: 'code-review',
140
+ defaultModel: { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
141
+ defaultAgent: 'general',
142
+ checkpointDir: './ckpt',
143
+ logDir: './logs',
144
+ retry: { maxAttempts: 2, onParseError: true },
145
+ timeoutSec: 300,
146
+ },
147
+ createBaseState,
148
+ )
149
+
150
+ // Run modules
151
+ const result = await pipeline.run(new CodeAnalyzer(), {
152
+ code: sourceCode,
153
+ language: 'typescript',
154
+ })
155
+
156
+ // Run with step options
157
+ const result = await pipeline.run(new CodeAnalyzer(), input, {
158
+ name: 'analyze-main',
159
+ model: { providerID: 'anthropic', modelID: 'claude-opus-4-5' },
160
+ newSession: true,
161
+ retry: { maxAttempts: 3 },
162
+ })
163
+
164
+ // Access state
165
+ console.log(pipeline.state.steps)
166
+ console.log(pipeline.getSessionId())
167
+
168
+ // Resume from checkpoint
169
+ const resumed = await Pipeline.loadCheckpoint(config, sessionId)
170
+ ```
171
+
172
+ ### State Management
173
+
174
+ Automatic checkpointing after each step:
175
+
176
+ ```typescript
177
+ import { createBaseState, extendBaseState } from 'ocpipe'
178
+
179
+ // Basic state
180
+ const state = createBaseState()
181
+ // { sessionId, startedAt, phase, steps, subPipelines }
182
+
183
+ // Extended state
184
+ interface MyState extends BaseState {
185
+ inputPath: string
186
+ results: AnalysisResult[]
187
+ }
188
+
189
+ const pipeline = new Pipeline(config, () => ({
190
+ ...createBaseState(),
191
+ inputPath: '/path/to/input',
192
+ results: [],
193
+ }))
194
+ ```
195
+
196
+ ## Auto-Correction
197
+
198
+ Automatically corrects LLM schema mismatches using JSON Patch (RFC 6902):
199
+
200
+ ```typescript
201
+ super(MySignature, {
202
+ correction: {
203
+ method: 'json-patch', // or 'jq'
204
+ maxFields: 5,
205
+ maxRounds: 3,
206
+ },
207
+ })
208
+ ```
209
+
210
+ The correction system:
211
+
212
+ 1. Detects schema validation errors
213
+ 2. Finds similar field names in the response
214
+ 3. Asks the LLM for patches to fix errors
215
+ 4. Applies patches and re-validates
216
+ 5. Retries up to configured rounds
217
+
218
+ ## Testing
219
+
220
+ Mock backends for unit testing without real LLM calls:
221
+
222
+ ```typescript
223
+ import {
224
+ MockAgentBackend,
225
+ createMockContext,
226
+ generateMockOutputs,
227
+ } from 'ocpipe'
228
+ import { vi } from 'vitest'
229
+
230
+ const mock = new MockAgentBackend()
231
+ mock.addJsonResponse({
232
+ intent: 'greeting',
233
+ confidence: 0.95,
234
+ keywords: ['hello', 'world'],
235
+ })
236
+
237
+ vi.mock('./agent.js', () => ({
238
+ runAgent: mock.createRunner(),
239
+ }))
240
+
241
+ const ctx = createMockContext({
242
+ defaultModel: { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
243
+ })
244
+
245
+ // Auto-generate mock outputs from schema
246
+ const mockData = generateMockOutputs(ParseIntent.outputs)
247
+ ```
248
+
249
+ ## Why No ChainOfThought or ReAct?
250
+
251
+ Unlike DSPy, ocpipe does not provide `ChainOfThought` or `ReAct` variants:
252
+
253
+ - OpenCode agents already do chain-of-thought reasoning
254
+ - OpenCode agents already have tool access (ReAct)
255
+ - Adding these would duplicate functionality
256
+
257
+ Configure your OpenCode agent for tool access. The agent handles complexity; ocpipe structures the contract.
@@ -0,0 +1,384 @@
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
+ ### Complex Modules
359
+
360
+ For modules with multiple predictors or transformed outputs, use the base `Module` class:
361
+
362
+ ```typescript
363
+ import { Module } from '../index.js'
364
+
365
+ class ComplexModule extends Module<
366
+ { input: string },
367
+ { result: string; metadata: object }
368
+ > {
369
+ private step1 = this.predict(Signature1)
370
+ private step2 = this.predict(Signature2, { agent: 'specialist' })
371
+
372
+ async forward(input, ctx) {
373
+ const r1 = await this.step1.execute(input, ctx)
374
+ const r2 = await this.step2.execute({ data: r1.data }, ctx)
375
+ return { result: r2.data.output, metadata: r1.data }
376
+ }
377
+ }
378
+ ```
379
+
380
+ ## Next Steps
381
+
382
+ - Read the full [README.md](./README.md) for advanced features
383
+ - Check the test files (`*.test.ts`) for more usage examples
384
+ - Explore `testing.ts` for unit testing without real LLM calls