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 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