ocpipe 0.6.10 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center"><strong>ocpipe</strong></p>
2
- <p align="center">Build LLM pipelines with <a href="https://github.com/sst/opencode">OpenCode</a>, <a href="https://github.com/anthropics/claude-code">Claude Code</a>, Oh My Pi, Pi, and <a href="https://zod.dev">Zod</a>.</p>
2
+ <p align="center">Build LLM pipelines with Oh My Pi, OpenCode, Claude Code, Codex, Pi, and <a href="https://zod.dev">Zod</a>.</p>
3
3
  <p align="center">Inspired by <a href="https://github.com/stanfordnlp/dspy">DSPy</a>.</p>
4
4
  <p align="center">
5
5
  <a href="https://www.npmjs.com/package/ocpipe"><img alt="npm" src="https://img.shields.io/npm/v/ocpipe?style=flat-square" /></a>
@@ -11,7 +11,7 @@
11
11
  - **Type-safe** Define inputs and outputs with Zod schemas
12
12
  - **Modular** Compose modules into complex pipelines
13
13
  - **Checkpoints** Resume from any step
14
- - **Multi-backend** Choose between OpenCode (75+ providers), Claude Code SDK, Codex SDK, Oh My Pi, or Pi
14
+ - **Multi-backend** Choose between Oh My Pi, OpenCode, Claude Code SDK, Codex SDK, or Pi
15
15
  - **Auto-correction** Fixes schema mismatches automatically
16
16
 
17
17
  ### Quick Start
@@ -32,8 +32,9 @@ const Greet = signature({
32
32
  const pipeline = new Pipeline(
33
33
  {
34
34
  name: 'hello-world',
35
- defaultModel: { providerID: 'opencode', modelID: 'minimax-m2.1-free' },
36
- defaultAgent: 'default',
35
+ defaultModel: { backend: 'omp', modelID: 'gpt-5.5' },
36
+ checkpointDir: './ckpt',
37
+ logDir: './logs',
37
38
  },
38
39
  createBaseState,
39
40
  )
@@ -49,24 +50,28 @@ type GreetOut = InferOutputs<typeof Greet> // { greeting: string }
49
50
 
50
51
  ### Backends
51
52
 
52
- ocpipe supports five backends for running LLM agents:
53
+ ocpipe supports five backends for running LLM prompts:
53
54
 
54
- **OpenCode** (default) - Requires `opencode` CLI in your PATH. Supports 75+ providers.
55
+ **Oh My Pi** (default) - Uses the `omp` CLI in headless JSON print mode.
55
56
 
56
57
  ```typescript
57
58
  const pipeline = new Pipeline(
58
59
  {
59
60
  name: 'my-pipeline',
60
- defaultModel: {
61
- providerID: 'anthropic',
62
- modelID: 'claude-sonnet-4-20250514',
63
- },
64
- defaultAgent: 'default',
61
+ defaultModel: { backend: 'omp', modelID: 'gpt-5.5' },
62
+ omp: { command: 'omp', approvalMode: 'yolo', thinking: 'high' },
65
63
  },
66
64
  createBaseState,
67
65
  )
68
66
  ```
69
67
 
68
+ **OpenCode** - Uses the `opencode` CLI. ocpipe passes prompt files directly and does not create or reference `.opencode/agents` definitions.
69
+
70
+ ```typescript
71
+ defaultModel: { backend: 'opencode', providerID: 'anthropic', modelID: 'claude-sonnet-4' },
72
+ opencode: { command: 'opencode' },
73
+ ```
74
+
70
75
  **Claude Code** - Uses `@anthropic-ai/claude-agent-sdk`. Install as a peer dependency.
71
76
 
72
77
  ```typescript
@@ -83,13 +88,6 @@ defaultModel: { backend: 'codex', modelID: 'gpt-5.4' },
83
88
  codex: { sandbox: 'read-only', reasoningEffort: 'high' },
84
89
  ```
85
90
 
86
- **Oh My Pi** - Uses the `omp` CLI in headless JSON print mode.
87
-
88
- ```typescript
89
- defaultModel: { backend: 'omp', modelID: 'gpt-5.5' },
90
- omp: { command: 'omp', approvalMode: 'yolo', thinking: 'high' },
91
- ```
92
-
93
91
  **Pi** - Uses the `pi` coding-agent CLI JSONL RPC mode.
94
92
 
95
93
  ```typescript
@@ -99,10 +97,9 @@ pi: { command: 'pi' },
99
97
 
100
98
  ### Requirements
101
99
 
102
- **For OpenCode backend:** Currently requires [this OpenCode fork](https://github.com/paralin/opencode). Once the following PRs are merged, the official release will work:
100
+ **For Oh My Pi:** Install the `omp` CLI and authenticate the models it uses.
103
101
 
104
- - [#5426](https://github.com/anomalyco/opencode/pull/5426) - Adds session export command
105
- - [#5339](https://github.com/anomalyco/opencode/pull/5339) - Adds `--tools` flag to limit available tools (optional)
102
+ **For OpenCode backend:** Install the `opencode` CLI and authenticate the providers it uses. ocpipe does not require a `.opencode` directory.
106
103
 
107
104
  **For Claude Code backend:** Install the SDK as a peer dependency:
108
105
 
@@ -126,6 +123,6 @@ bun add @openai/codex-sdk
126
123
 
127
124
  ---
128
125
 
129
- [Discord](https://discord.gg/opencode) · [OpenCode](https://github.com/sst/opencode)
126
+ [Oh My Pi](https://github.com/aperturerobotics/oh-my-pi) · [OpenCode](https://github.com/sst/opencode)
130
127
 
131
128
  <sub>An [Aperture Robotics](https://github.com/aperturerobotics) project.</sub>
@@ -71,8 +71,7 @@ async function main() {
71
71
  const pipeline = new Pipeline(
72
72
  {
73
73
  name: 'correction-demo',
74
- defaultModel: { providerID: 'opencode', modelID: 'minimax-m2.1-free' },
75
- defaultAgent: 'default',
74
+ defaultModel: { backend: 'omp', modelID: 'gpt-5.5' },
76
75
  checkpointDir: './ckpt',
77
76
  logDir: './logs',
78
77
  },
package/example/index.ts CHANGED
@@ -3,9 +3,8 @@
3
3
  *
4
4
  * Demonstrates running an ocpipe module in a pipeline.
5
5
  *
6
- * Usage:
7
- * npx tsx example/index.ts # Use opencode backend
8
- * npx tsx example/index.ts --claude-code # Use claude-code backend
6
+ * npx tsx example/index.ts # Use OMP backend
7
+ * npx tsx example/index.ts --claude-code # Use Claude Code backend
9
8
  */
10
9
 
11
10
  import { Pipeline, createBaseState } from '../src/index.js'
@@ -18,10 +17,10 @@ async function main() {
18
17
  const pipeline = new Pipeline(
19
18
  {
20
19
  name: 'hello-world',
21
- defaultModel: useClaudeCode
22
- ? { backend: 'claude-code', modelID: 'sonnet' }
23
- : { providerID: 'opencode', modelID: 'minimax-m2.1-free' },
24
- defaultAgent: useClaudeCode ? 'claude-code' : 'default',
20
+ defaultModel:
21
+ useClaudeCode ?
22
+ { backend: 'claude-code', modelID: 'sonnet' }
23
+ : { backend: 'omp', modelID: 'gpt-5.5' },
25
24
  checkpointDir: './ckpt',
26
25
  logDir: './logs',
27
26
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ocpipe",
3
- "version": "0.6.10",
4
- "description": "SDK for LLM pipelines with OpenCode, Codex, and Zod",
3
+ "version": "0.6.11",
4
+ "description": "SDK for LLM pipelines with OpenCode, OMP, Codex, Claude Code, Pi, and Zod",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "types": "src/index.ts",
@@ -17,9 +17,10 @@
17
17
  "author": "s4wave",
18
18
  "keywords": [
19
19
  "dspy",
20
- "llm",
20
+ "omp",
21
21
  "opencode",
22
22
  "codex",
23
+ "claude-code",
23
24
  "typescript",
24
25
  "ai",
25
26
  "workflow",
package/src/agent.ts CHANGED
@@ -1,41 +1,38 @@
1
1
  /**
2
2
  * ocpipe agent integration.
3
3
  *
4
- * Dispatches to OpenCode CLI, Claude Code SDK, Codex SDK, or Pi based on backend
5
- * configuration.
4
+ * Dispatches prompts to configured coding-agent backends.
6
5
  */
7
6
 
8
7
  import { spawn } from 'child_process'
9
- import { mkdir, writeFile, unlink } from 'fs/promises'
8
+ import { randomUUID } from 'crypto'
9
+ import { mkdir, unlink, writeFile } from 'fs/promises'
10
10
  import { join } from 'path'
11
11
  import { PROJECT_ROOT, TMP_DIR } from './paths.js'
12
+ import type { RunAgentOptions, RunAgentResult } from './types.js'
12
13
  import { runOmpAgent } from './omp.js'
13
14
  import { runPiAgent } from './pi.js'
14
- import type { RunAgentOptions, RunAgentResult } from './types.js'
15
-
16
- /** Get command and args to invoke opencode from PATH */
17
- function getOpencodeCommand(args: string[]): { cmd: string; args: string[] } {
18
- return { cmd: 'opencode', args }
19
- }
20
15
 
21
- /** runAgent dispatches to the appropriate backend based on model configuration. */
16
+ /** runAgent dispatches to the selected backend. */
22
17
  export async function runAgent(
23
18
  options: RunAgentOptions,
24
19
  ): Promise<RunAgentResult> {
25
- const backend = options.model.backend ?? 'opencode'
20
+ const backend = options.model.backend ?? 'omp'
26
21
 
27
22
  if (backend === 'claude-code') {
28
- // Dynamic import is required: @anthropic-ai/claude-agent-sdk is an optional peer absent for other backends.
29
23
  const { runClaudeCodeAgent } = await import('./claude-code.js')
30
24
  return runClaudeCodeAgent(options)
31
25
  }
32
26
 
33
27
  if (backend === 'codex') {
34
- // Dynamic import is required: @openai/codex-sdk is an optional peer absent for other backends.
35
28
  const { runCodexAgent } = await import('./codex.js')
36
29
  return runCodexAgent(options)
37
30
  }
38
31
 
32
+ if (backend === 'opencode') {
33
+ return runOpencodeAgent(options)
34
+ }
35
+
39
36
  if (backend === 'pi') {
40
37
  return runPiAgent(options)
41
38
  }
@@ -44,52 +41,48 @@ export async function runAgent(
44
41
  return runOmpAgent(options)
45
42
  }
46
43
 
47
- return runOpencodeAgent(options)
44
+ const unreachable: never = backend
45
+ throw new Error(`Unsupported backend: ${unreachable}`)
48
46
  }
49
47
 
50
- /** runOpencodeAgent executes an OpenCode agent with a prompt, streaming output in real-time. */
51
48
  async function runOpencodeAgent(
52
49
  options: RunAgentOptions,
53
50
  ): Promise<RunAgentResult> {
54
51
  const {
55
52
  prompt,
56
- agent,
57
53
  model,
58
54
  sessionId,
59
55
  timeoutSec = 3600,
60
56
  workdir,
57
+ opencode,
61
58
  signal,
62
59
  } = options
63
60
 
64
61
  if (!model.providerID) {
65
62
  throw new Error('providerID is required for OpenCode backend')
66
63
  }
67
- const modelStr = `${model.providerID}/${model.modelID}`
68
- const sessionInfo = sessionId ? `[session:${sessionId}]` : '[new session]'
69
- const promptPreview = prompt.slice(0, 50).replace(/\n/g, ' ')
70
64
 
71
- console.error(
72
- `\n>>> OpenCode [${agent}] [${modelStr}] ${sessionInfo}: ${promptPreview}...`,
73
- )
74
-
75
- // Check if already aborted
76
65
  if (signal?.aborted) {
77
66
  throw new Error('Request aborted')
78
67
  }
79
68
 
80
- // Write prompt to .opencode/prompts/ within the working directory
81
69
  const cwd = workdir ?? PROJECT_ROOT
82
- const promptsDir = join(cwd, '.opencode', 'prompts')
83
- await mkdir(promptsDir, { recursive: true })
84
- const promptFile = join(promptsDir, `prompt_${Date.now()}.txt`)
70
+ const modelStr = `${model.providerID}/${model.modelID}`
71
+ const sessionInfo = sessionId ? `[session:${sessionId}]` : '[new session]'
72
+ const promptPreview = prompt.slice(0, 50).replace(/\n/g, ' ')
73
+ console.error(
74
+ `\n>>> OpenCode [${modelStr}] ${sessionInfo}: ${promptPreview}...`,
75
+ )
76
+
77
+ const promptDir = opencode?.promptDir ?? TMP_DIR
78
+ await mkdir(promptDir, { recursive: true })
79
+ const promptFile = join(promptDir, `ocpipe_prompt_${randomUUID()}.txt`)
85
80
  await writeFile(promptFile, prompt)
86
81
 
87
82
  const args = [
88
83
  'run',
89
84
  '--format',
90
85
  'default',
91
- '--agent',
92
- agent,
93
86
  '--model',
94
87
  modelStr,
95
88
  '--prompt-file',
@@ -99,11 +92,9 @@ async function runOpencodeAgent(
99
92
  if (sessionId) {
100
93
  args.push('--session', sessionId)
101
94
  }
102
-
103
95
  if (model.variant) {
104
96
  args.push('--model-variant', model.variant)
105
97
  }
106
-
107
98
  if (model.variantThinkingBudget !== undefined) {
108
99
  args.push(
109
100
  '--model-variant-thinking-budget',
@@ -111,194 +102,164 @@ async function runOpencodeAgent(
111
102
  )
112
103
  }
113
104
 
114
- return new Promise((resolve, reject) => {
115
- const opencodeCmd = getOpencodeCommand(args)
116
- console.error(
117
- `[DEBUG] Running: ${opencodeCmd.cmd} ${opencodeCmd.args.join(' ')}`,
118
- )
119
- const proc = spawn(opencodeCmd.cmd, opencodeCmd.args, {
120
- cwd,
121
- stdio: ['ignore', 'pipe', 'pipe'],
122
- })
123
-
124
- let newSessionId = sessionId || ''
125
- const stdoutChunks: string[] = []
126
- const stderrChunks: string[] = []
127
- let aborted = false
128
-
129
- // Handle abort signal - kill subprocess when aborted
130
- const abortHandler = async () => {
131
- if (aborted) return
132
- aborted = true
133
- console.error(`\n[abort] Killing OpenCode subprocess...`)
134
- proc.kill('SIGTERM')
135
- // Give it a moment to clean up, then force kill
136
- setTimeout(() => {
137
- if (!proc.killed) proc.kill('SIGKILL')
138
- }, 1000)
139
- await unlink(promptFile).catch(() => {})
140
- reject(new Error('Request aborted'))
141
- }
142
- signal?.addEventListener('abort', abortHandler, { once: true })
143
-
144
- // Stream stderr in real-time (OpenCode progress output)
145
- proc.stderr.on('data', (data: Buffer) => {
146
- const text = data.toString()
147
- stderrChunks.push(text)
148
-
149
- // Parse session ID from output
150
- for (const line of text.split('\n')) {
151
- if (line.startsWith('[session:')) {
152
- newSessionId = line.trim().slice(9, -1)
153
- continue
154
- }
155
- // Filter noise
156
- if (line.includes('baseline-browser-mapping')) continue
157
- if (line.startsWith('$ bun run')) continue
158
- if (line.trim()) {
159
- process.stderr.write(line + '\n')
160
- }
161
- }
162
- })
163
-
164
- // Collect stdout
165
- proc.stdout.on('data', (data: Buffer) => {
166
- const text = data.toString()
167
- stdoutChunks.push(text)
168
- process.stderr.write(text)
169
- })
170
-
171
- // Timeout handling (0 = no timeout)
172
- const timeout =
173
- timeoutSec > 0 ?
174
- setTimeout(async () => {
175
- proc.kill()
176
- await unlink(promptFile).catch(() => {})
177
- reject(new Error(`Timeout after ${timeoutSec}s`))
178
- }, timeoutSec * 1000)
179
- : null
180
-
181
- proc.on('close', async (code) => {
182
- if (timeout) clearTimeout(timeout)
183
- signal?.removeEventListener('abort', abortHandler)
184
-
185
- // If aborted, we already rejected
186
- if (aborted) return
187
-
188
- // Clean up prompt file
189
- await unlink(promptFile).catch(() => {})
190
-
191
- const stderr = stderrChunks.join('').trim()
192
-
193
- if (code !== 0) {
194
- const lastLines = stderr.split('\n').slice(-5).join('\n')
195
- const detail = lastLines ? `\n${lastLines}` : ''
196
- reject(new Error(`OpenCode exited with code ${code}${detail}`))
197
- return
198
- }
105
+ const command = opencode?.command ?? 'opencode'
106
+ console.error(`[DEBUG] Running: ${command} ${args.join(' ')}`)
107
+ const proc = spawn(command, args, {
108
+ cwd,
109
+ stdio: ['ignore', 'pipe', 'pipe'],
110
+ env: { ...process.env, ...opencode?.env },
111
+ })
199
112
 
200
- // Check for OpenCode errors that exit with code 0 but produce no output
201
- const knownErrors = [
202
- {
203
- pattern: /ProviderModelNotFoundError/,
204
- message: 'Provider/model not found',
205
- },
206
- { pattern: /ModelNotFoundError/, message: 'Model not found' },
207
- { pattern: /ProviderNotFoundError/, message: 'Provider not found' },
208
- { pattern: /API key.*not.*found/i, message: 'API key not configured' },
209
- {
210
- pattern: /authentication.*failed/i,
211
- message: 'Authentication failed',
212
- },
213
- ]
214
-
215
- for (const { pattern, message } of knownErrors) {
216
- if (pattern.test(stderr)) {
217
- // Extract the relevant error lines
218
- const errorLines = stderr
219
- .split('\n')
220
- .filter(
221
- (line) =>
222
- pattern.test(line) ||
223
- line.includes('Error') ||
224
- line.includes('error:'),
225
- )
226
- .slice(0, 5)
227
- .join('\n')
228
- reject(new Error(`OpenCode ${message}:\n${errorLines}`))
229
- return
230
- }
231
- }
113
+ const { promise, resolve, reject } = Promise.withResolvers<RunAgentResult>()
114
+ let newSessionId = sessionId || ''
115
+ const stdoutChunks: string[] = []
116
+ const stderrChunks: string[] = []
117
+ let aborted = false
118
+
119
+ const abortHandler = () => {
120
+ if (aborted) return
121
+ aborted = true
122
+ console.error(`\n[abort] Killing OpenCode subprocess...`)
123
+ proc.kill('SIGTERM')
124
+ setTimeout(() => {
125
+ if (!proc.killed) proc.kill('SIGKILL')
126
+ }, 1000)
127
+ void unlink(promptFile).catch(() => {})
128
+ reject(new Error('Request aborted'))
129
+ }
130
+ signal?.addEventListener('abort', abortHandler, { once: true })
232
131
 
233
- // Export session to get structured response
234
- let response = stdoutChunks.join('').trim()
132
+ proc.stderr.on('data', (data: Buffer) => {
133
+ const text = data.toString()
134
+ stderrChunks.push(text)
235
135
 
236
- if (newSessionId) {
237
- const exported = await exportSession(newSessionId, workdir)
238
- if (exported) {
239
- response = exported
240
- }
136
+ for (const line of text.split('\n')) {
137
+ if (line.startsWith('[session:')) {
138
+ newSessionId = line.trim().slice(9, -1)
139
+ continue
140
+ }
141
+ if (line.includes('baseline-browser-mapping')) continue
142
+ if (line.startsWith('$ bun run')) continue
143
+ if (line.trim()) {
144
+ process.stderr.write(line + '\n')
241
145
  }
146
+ }
147
+ })
148
+
149
+ proc.stdout.on('data', (data: Buffer) => {
150
+ const text = data.toString()
151
+ stdoutChunks.push(text)
152
+ process.stderr.write(text)
153
+ })
154
+
155
+ const timeout =
156
+ timeoutSec > 0 ?
157
+ setTimeout(() => {
158
+ proc.kill()
159
+ void unlink(promptFile).catch(() => {})
160
+ reject(new Error(`Timeout after ${timeoutSec}s`))
161
+ }, timeoutSec * 1000)
162
+ : undefined
163
+
164
+ proc.on('close', async (code) => {
165
+ clearTimeout(timeout)
166
+ signal?.removeEventListener('abort', abortHandler)
167
+ if (aborted) return
168
+
169
+ void unlink(promptFile).catch(() => {})
170
+ const stderr = stderrChunks.join('').trim()
171
+
172
+ if (code !== 0) {
173
+ const lastLines = stderr.split('\n').slice(-5).join('\n')
174
+ const detail = lastLines ? `\n${lastLines}` : ''
175
+ reject(new Error(`OpenCode exited with code ${code}${detail}`))
176
+ return
177
+ }
242
178
 
243
- // Check for empty response with errors in stderr (likely a silent failure)
244
- if (response.length === 0 && stderr.includes('Error')) {
245
- const lastLines = stderr.split('\n').slice(-10).join('\n')
246
- reject(
247
- new Error(
248
- `OpenCode returned empty response with errors:\n${lastLines}`,
249
- ),
250
- )
179
+ for (const { pattern, message } of knownOpenCodeErrors) {
180
+ if (pattern.test(stderr)) {
181
+ const errorLines = stderr
182
+ .split('\n')
183
+ .filter(
184
+ (line) =>
185
+ pattern.test(line) ||
186
+ line.includes('Error') ||
187
+ line.includes('error:'),
188
+ )
189
+ .slice(0, 5)
190
+ .join('\n')
191
+ reject(new Error(`OpenCode ${message}:\n${errorLines}`))
251
192
  return
252
193
  }
194
+ }
195
+
196
+ let response = stdoutChunks.join('').trim()
197
+ if (newSessionId) {
198
+ response =
199
+ (await exportSession(newSessionId, cwd, command, opencode?.env)) ??
200
+ response
201
+ }
253
202
 
254
- const sessionStr = newSessionId || 'none'
255
- console.error(
256
- `<<< OpenCode done (${response.length} chars) [session:${sessionStr}]`,
203
+ if (response.length === 0 && stderr.includes('Error')) {
204
+ const lastLines = stderr.split('\n').slice(-10).join('\n')
205
+ reject(
206
+ new Error(
207
+ `OpenCode returned empty response with errors:\n${lastLines}`,
208
+ ),
257
209
  )
210
+ return
211
+ }
212
+
213
+ const sessionStr = newSessionId || 'none'
214
+ console.error(
215
+ `<<< OpenCode done (${response.length} chars) [session:${sessionStr}]`,
216
+ )
217
+ resolve({ text: response, sessionId: newSessionId })
218
+ })
258
219
 
259
- resolve({
260
- text: response,
261
- sessionId: newSessionId,
262
- })
263
- })
264
-
265
- proc.on('error', async (err) => {
266
- if (timeout) clearTimeout(timeout)
267
- await unlink(promptFile).catch(() => {})
268
- reject(err)
269
- })
220
+ proc.on('error', (err) => {
221
+ clearTimeout(timeout)
222
+ signal?.removeEventListener('abort', abortHandler)
223
+ void unlink(promptFile).catch(() => {})
224
+ reject(err)
270
225
  })
226
+
227
+ return promise
271
228
  }
272
229
 
273
- /** exportSession exports a session and extracts assistant text responses. */
274
230
  async function exportSession(
275
231
  sessionId: string,
276
- workdir?: string,
232
+ workdir: string,
233
+ command: string,
234
+ env: Record<string, string> | undefined,
277
235
  ): Promise<string | null> {
278
- const tmpPath = `${TMP_DIR}/opencode_export_${Date.now()}.json`
236
+ const exportFile = join(TMP_DIR, `opencode_export_${randomUUID()}.json`)
279
237
 
280
238
  try {
281
239
  await mkdir(TMP_DIR, { recursive: true })
282
- const opencodeCmd = getOpencodeCommand([
283
- 'session',
284
- 'export',
285
- sessionId,
286
- '--format',
287
- 'json',
288
- '--turn',
289
- '-1',
290
- '-o',
291
- tmpPath,
292
- ])
293
- const proc = Bun.spawn([opencodeCmd.cmd, ...opencodeCmd.args], {
294
- cwd: workdir ?? PROJECT_ROOT,
295
- stdout: 'pipe',
296
- stderr: 'pipe',
297
- })
240
+ const proc = Bun.spawn(
241
+ [
242
+ command,
243
+ 'session',
244
+ 'export',
245
+ sessionId,
246
+ '--format',
247
+ 'json',
248
+ '--turn',
249
+ '-1',
250
+ '-o',
251
+ exportFile,
252
+ ],
253
+ {
254
+ cwd: workdir,
255
+ stdout: 'pipe',
256
+ stderr: 'pipe',
257
+ env: { ...process.env, ...env },
258
+ },
259
+ )
298
260
 
299
261
  await proc.exited
300
-
301
- const file = Bun.file(tmpPath)
262
+ const file = Bun.file(exportFile)
302
263
  if (!(await file.exists())) return null
303
264
 
304
265
  const data = (await file.json()) as {
@@ -307,29 +268,37 @@ async function exportSession(
307
268
  parts?: Array<{ type?: string; text?: string }>
308
269
  }>
309
270
  }
310
- await Bun.write(tmpPath, '') // Clean up
271
+ void unlink(exportFile).catch(() => {})
311
272
 
312
- // Extract all assistant text parts from the exported turn.
313
- // The --turn -1 flag ensures we only get the last turn's messages.
314
- const messages = data.messages || []
315
273
  const textParts: string[] = []
316
-
317
- for (const msg of messages) {
318
- if (msg.info?.role === 'assistant') {
319
- for (const part of msg.parts || []) {
320
- if (part.type === 'text' && part.text) {
321
- textParts.push(part.text)
322
- }
274
+ for (const msg of data.messages ?? []) {
275
+ if (msg.info?.role !== 'assistant') continue
276
+ for (const part of msg.parts ?? []) {
277
+ if (part.type === 'text' && part.text) {
278
+ textParts.push(part.text)
323
279
  }
324
280
  }
325
281
  }
326
282
 
327
283
  return textParts.length > 0 ? textParts.join('\n') : null
328
284
  } catch {
285
+ void unlink(exportFile).catch(() => {})
329
286
  return null
330
287
  }
331
288
  }
332
289
 
290
+ const knownOpenCodeErrors: ReadonlyArray<{ pattern: RegExp; message: string }> =
291
+ [
292
+ {
293
+ pattern: /ProviderModelNotFoundError/,
294
+ message: 'Provider/model not found',
295
+ },
296
+ { pattern: /ModelNotFoundError/, message: 'Model not found' },
297
+ { pattern: /ProviderNotFoundError/, message: 'Provider not found' },
298
+ { pattern: /API key.*not.*found/i, message: 'API key not configured' },
299
+ { pattern: /authentication.*failed/i, message: 'Authentication failed' },
300
+ ]
301
+
333
302
  /** logStep logs a step header for workflow progress. */
334
303
  export function logStep(step: number, title: string, detail = ''): void {
335
304
  const detailStr = detail ? ` (${detail})` : ''
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { execSync } from 'child_process'
14
- import { existsSync, readFileSync } from 'fs'
14
+ import { existsSync } from 'fs'
15
15
  import { join } from 'path'
16
16
  import { homedir } from 'os'
17
17
  import {
@@ -66,9 +66,14 @@ function resolveClaudeCodePath(explicit?: string): string | undefined {
66
66
  if (explicit) return explicit
67
67
 
68
68
  try {
69
- const found = execSync('which claude', { encoding: 'utf8', timeout: 3000 }).trim()
69
+ const found = execSync('which claude', {
70
+ encoding: 'utf8',
71
+ timeout: 3000,
72
+ }).trim()
70
73
  if (found) return found
71
- } catch { /* which not found or timed out — fall through to defaults */ }
74
+ } catch {
75
+ /* which not found or timed out — fall through to defaults */
76
+ }
72
77
 
73
78
  const defaults = [
74
79
  join(homedir(), '.local', 'bin', 'claude'),
@@ -90,21 +95,6 @@ function normalizeModelId(modelId: string): string {
90
95
  return modelId
91
96
  }
92
97
 
93
- /** loadAgentDefinition loads an OpenCode agent definition file and extracts the
94
- * markdown body (stripping the YAML frontmatter) for use as a system prompt. */
95
- function loadAgentDefinition(agent: string, workdir?: string): string | undefined {
96
- if (!agent || !workdir) return undefined
97
-
98
- const agentPath = join(workdir, '.opencode', 'agents', `${agent}.md`)
99
- if (!existsSync(agentPath)) return undefined
100
-
101
- const content = readFileSync(agentPath, 'utf8')
102
-
103
- // Strip YAML frontmatter (--- delimited block at the start).
104
- const stripped = content.replace(/^---\n[\s\S]*?\n---\n*/, '')
105
- return stripped.trim() || undefined
106
- }
107
-
108
98
  /** Extract text from assistant messages. */
109
99
  function getAssistantText(msg: SDKMessage): string | null {
110
100
  if (msg.type !== 'assistant') return null
@@ -147,7 +137,6 @@ export async function runClaudeCodeAgent(
147
137
  ): Promise<RunAgentResult> {
148
138
  const {
149
139
  prompt,
150
- agent,
151
140
  model,
152
141
  sessionId,
153
142
  timeoutSec = 3600,
@@ -169,8 +158,8 @@ export async function runClaudeCodeAgent(
169
158
  // Build query options with configurable permission mode (default: acceptEdits)
170
159
  const permissionMode = claudeCode?.permissionMode ?? 'acceptEdits'
171
160
 
172
- // Resolve system prompt: explicit option > agent definition file > none
173
- const systemPrompt = claudeCode?.systemPrompt ?? loadAgentDefinition(agent, workdir)
161
+ // Resolve system prompt from explicit configuration only.
162
+ const systemPrompt = claudeCode?.systemPrompt
174
163
 
175
164
  // Bridge external abort signal to an AbortController for the SDK
176
165
  const abortController = new AbortController()
@@ -217,16 +206,22 @@ export async function runClaudeCodeAgent(
217
206
  }),
218
207
  // Resolve executable path: explicit option > PATH lookup > default locations
219
208
  ...(() => {
220
- const resolved = resolveClaudeCodePath(claudeCode?.pathToClaudeCodeExecutable)
209
+ const resolved = resolveClaudeCodePath(
210
+ claudeCode?.pathToClaudeCodeExecutable,
211
+ )
221
212
  return resolved ? { pathToClaudeCodeExecutable: resolved } : {}
222
213
  })(),
223
214
  }
224
215
 
225
216
  if (claudeCode?.agents) {
226
- console.error(`[ocpipe] Subagents defined: ${Object.keys(claudeCode.agents).join(', ')}`)
217
+ console.error(
218
+ `[ocpipe] Subagents defined: ${Object.keys(claudeCode.agents).join(', ')}`,
219
+ )
227
220
  }
228
221
  if (claudeCode?.allowedTools) {
229
- console.error(`[ocpipe] Allowed tools: ${claudeCode.allowedTools.join(', ')}`)
222
+ console.error(
223
+ `[ocpipe] Allowed tools: ${claudeCode.allowedTools.join(', ')}`,
224
+ )
230
225
  }
231
226
  console.error(
232
227
  `\n>>> Claude Code [${modelStr}] [${permissionMode}] ${sessionInfo}: ${promptPreview}...`,
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * ocpipe: LLM pipelines with OpenCode and Zod.
2
+ * ocpipe: LLM pipelines with coding-agent backends and Zod.
3
3
  *
4
4
  * Inspired by DSPy.
5
5
  *
@@ -22,8 +22,7 @@
22
22
  * // Run in a pipeline
23
23
  * const pipeline = new Pipeline({
24
24
  * name: 'my-workflow',
25
- * defaultModel: { providerID: 'opencode', modelID: 'minimax-m2.1-free' },
26
- * defaultAgent: 'general',
25
+ * defaultModel: { backend: 'omp', modelID: 'gpt-5.5' },
27
26
  * checkpointDir: './ckpt',
28
27
  * logDir: './logs',
29
28
  * }, createBaseState)
package/src/pipeline.ts CHANGED
@@ -30,9 +30,9 @@ export class Pipeline<S extends BaseState> {
30
30
  this.ctx = {
31
31
  sessionId: undefined,
32
32
  defaultModel: config.defaultModel,
33
- defaultAgent: config.defaultAgent,
34
33
  timeoutSec: config.timeoutSec ?? 3600,
35
34
  workdir: config.workdir,
35
+ opencode: config.opencode,
36
36
  claudeCode: config.claudeCode,
37
37
  codex: config.codex,
38
38
  pi: config.pi,
@@ -89,8 +89,8 @@ export class Pipeline<S extends BaseState> {
89
89
  result: result as StepResult<unknown>,
90
90
  })
91
91
 
92
- // Update opencode session in state
93
- this.state.opencodeSessionId = this.ctx.sessionId
92
+ // Update backend session in state.
93
+ this.state.agentSessionId = this.ctx.sessionId
94
94
 
95
95
  await this.saveCheckpoint()
96
96
  return result
@@ -159,7 +159,7 @@ export class Pipeline<S extends BaseState> {
159
159
  }
160
160
  }
161
161
 
162
- /** getSessionId returns the current OpenCode session ID. */
162
+ /** getSessionId returns the current backend session ID. */
163
163
  getSessionId(): string | undefined {
164
164
  return this.ctx.sessionId
165
165
  }
@@ -189,9 +189,9 @@ export class Pipeline<S extends BaseState> {
189
189
  const pipeline = new Pipeline<S>(config, () => state)
190
190
  pipeline.state = state
191
191
 
192
- // Restore context from state
193
- if (state.opencodeSessionId) {
194
- pipeline.ctx.sessionId = state.opencodeSessionId
192
+ // Restore context from state.
193
+ if (state.agentSessionId) {
194
+ pipeline.ctx.sessionId = state.agentSessionId
195
195
  }
196
196
 
197
197
  // Restore step number
package/src/predict.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * ocpipe Predict class.
3
3
  *
4
- * Executes a signature by generating a prompt, calling OpenCode, and parsing the response.
4
+ * Executes a signature by generating a prompt, calling a backend, and parsing
5
+ * the response.
5
6
  */
6
7
 
7
8
  import { z } from 'zod/v4'
@@ -37,8 +38,6 @@ import {
37
38
 
38
39
  /** Configuration for a Predict instance. */
39
40
  export interface PredictConfig {
40
- /** Override the pipeline's default agent. */
41
- agent?: string
42
41
  /** Override the pipeline's default model. */
43
42
  model?: ModelConfig
44
43
  /** Start a fresh session (default: false, reuses context). */
@@ -71,12 +70,12 @@ export class Predict<S extends AnySignature> {
71
70
 
72
71
  const agentResult = await runAgent({
73
72
  prompt,
74
- agent: this.config.agent ?? ctx.defaultAgent,
75
73
  model: this.config.model ?? ctx.defaultModel,
76
74
  sessionId: this.config.newSession ? undefined : ctx.sessionId,
77
75
  timeoutSec: ctx.timeoutSec,
78
76
  workdir: ctx.workdir,
79
77
  claudeCode: ctx.claudeCode,
78
+ opencode: ctx.opencode,
80
79
  codex: ctx.codex,
81
80
  pi: ctx.pi,
82
81
  omp: ctx.omp,
@@ -206,10 +205,10 @@ export class Predict<S extends AnySignature> {
206
205
  prompt: repairPrompt,
207
206
  model: correctionModel ?? ctx.defaultModel,
208
207
  sessionId: correctionModel ? undefined : sessionId,
209
- agent: ctx.defaultAgent,
210
208
  timeoutSec: ctx.timeoutSec,
211
209
  workdir: ctx.workdir,
212
210
  claudeCode: ctx.claudeCode,
211
+ opencode: ctx.opencode,
213
212
  codex: ctx.codex,
214
213
  pi: ctx.pi,
215
214
  omp: ctx.omp,
@@ -286,10 +285,10 @@ export class Predict<S extends AnySignature> {
286
285
  prompt: patchPrompt,
287
286
  model: correctionModel ?? ctx.defaultModel,
288
287
  sessionId: correctionModel ? undefined : sessionId,
289
- agent: ctx.defaultAgent,
290
288
  timeoutSec: ctx.timeoutSec,
291
289
  workdir: ctx.workdir,
292
290
  claudeCode: ctx.claudeCode,
291
+ opencode: ctx.opencode,
293
292
  codex: ctx.codex,
294
293
  pi: ctx.pi,
295
294
  omp: ctx.omp,
@@ -399,8 +398,7 @@ export class Predict<S extends AnySignature> {
399
398
  // Add field descriptions from our config (toJSONSchema uses .describe() metadata)
400
399
  // Since our FieldConfig has a separate desc field, merge it in
401
400
  const props = jsonSchema.properties as
402
- | Record<string, Record<string, unknown>>
403
- | undefined
401
+ Record<string, Record<string, unknown>> | undefined
404
402
  if (props) {
405
403
  for (const [name, config] of Object.entries(this.sig.outputs) as [
406
404
  string,
package/src/testing.ts CHANGED
@@ -134,18 +134,20 @@ export class MockAgentBackend {
134
134
  export function createMockContext(
135
135
  overrides?: Partial<{
136
136
  sessionId: string
137
- defaultModel: { providerID: string; modelID: string }
138
- defaultAgent: string
137
+ defaultModel: {
138
+ backend?: 'claude-code' | 'codex' | 'pi' | 'omp'
139
+ providerID?: string
140
+ modelID: string
141
+ }
139
142
  timeoutSec: number
140
143
  }>,
141
144
  ) {
142
145
  return {
143
146
  sessionId: overrides?.sessionId,
144
147
  defaultModel: overrides?.defaultModel ?? {
145
- providerID: 'github-copilot',
146
- modelID: 'grok-code-fast-1',
148
+ backend: 'omp',
149
+ modelID: 'gpt-5.5',
147
150
  },
148
- defaultAgent: overrides?.defaultAgent ?? 'general',
149
151
  timeoutSec: overrides?.timeoutSec ?? 60,
150
152
  }
151
153
  }
package/src/types.ts CHANGED
@@ -8,29 +8,20 @@ import type { z } from 'zod/v4'
8
8
  // Model Configuration
9
9
  // ============================================================================
10
10
 
11
- /** Backend type for running agents. */
11
+ /** Backend type for running prompts. */
12
12
  export type BackendType = 'opencode' | 'claude-code' | 'codex' | 'pi' | 'omp'
13
13
 
14
14
  /** Reasoning effort for Codex SDK threads. */
15
15
  export type CodexReasoningEffort =
16
- | 'minimal'
17
- | 'low'
18
- | 'medium'
19
- | 'high'
20
- | 'xhigh'
16
+ 'minimal' | 'low' | 'medium' | 'high' | 'xhigh'
21
17
 
22
18
  /** Codex sandbox mode. */
23
19
  export type CodexSandboxMode =
24
- | 'read-only'
25
- | 'workspace-write'
26
- | 'danger-full-access'
20
+ 'read-only' | 'workspace-write' | 'danger-full-access'
27
21
 
28
22
  /** Codex approval policy. */
29
23
  export type CodexApprovalPolicy =
30
- | 'never'
31
- | 'on-request'
32
- | 'on-failure'
33
- | 'untrusted'
24
+ 'never' | 'on-request' | 'on-failure' | 'untrusted'
34
25
 
35
26
  /** Codex web search mode. */
36
27
  export type CodexWebSearchMode = 'disabled' | 'cached' | 'live'
@@ -45,10 +36,7 @@ export type CodexConfigValue =
45
36
 
46
37
  /** Permission mode for Claude Code sessions. */
47
38
  export type PermissionMode =
48
- | 'default'
49
- | 'acceptEdits'
50
- | 'bypassPermissions'
51
- | 'plan'
39
+ 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan'
52
40
 
53
41
  /** Subagent definition for Claude Code's Task tool dispatch. */
54
42
  export interface AgentDefinition {
@@ -75,8 +63,7 @@ export interface ClaudeCodeOptions {
75
63
  * Can be a full string or use the preset format with append.
76
64
  */
77
65
  systemPrompt?:
78
- | string
79
- | { type: 'preset'; preset: 'claude_code'; append: string }
66
+ string | { type: 'preset'; preset: 'claude_code'; append: string }
80
67
  /**
81
68
  * Subagent definitions for parallel task dispatch via the Task tool.
82
69
  * Keys are agent names, values are agent definitions.
@@ -133,6 +120,16 @@ export interface PiOptions {
133
120
  env?: Record<string, string>
134
121
  }
135
122
 
123
+ /** OpenCode CLI specific session options. */
124
+ export interface OpenCodeOptions {
125
+ /** Path to the OpenCode executable (default: `opencode` from PATH). */
126
+ command?: string
127
+ /** Prompt-file directory; defaults to ocpipe's temporary directory, not `.opencode`. */
128
+ promptDir?: string
129
+ /** Extra environment variables passed to the OpenCode subprocess. */
130
+ env?: Record<string, string>
131
+ }
132
+
136
133
  /** Oh My Pi CLI specific session options. */
137
134
  export interface OmpOptions {
138
135
  /** Path to the Oh My Pi executable (default: `omp` from PATH). */
@@ -167,9 +164,9 @@ export interface OmpOptions {
167
164
 
168
165
  /** Model configuration for LLM backends. */
169
166
  export interface ModelConfig {
170
- /** Backend to use (default: 'opencode'). */
167
+ /** Backend to use (default: 'omp'). */
171
168
  backend?: BackendType
172
- /** Provider ID (required for OpenCode, ignored for Claude Code). */
169
+ /** Provider ID for provider-qualified backends such as OpenCode. */
173
170
  providerID?: string
174
171
  modelID: string
175
172
  /** Model variant for reasoning effort (e.g. 'low', 'medium', 'high', 'max'). */
@@ -184,16 +181,16 @@ export interface ModelConfig {
184
181
 
185
182
  /** Execution context passed through pipeline execution. */
186
183
  export interface ExecutionContext {
187
- /** Current OpenCode session ID (for continuity). */
184
+ /** Current backend session ID for continuity. */
188
185
  sessionId?: string
189
186
  /** Default model for predictions. */
190
187
  defaultModel: ModelConfig
191
- /** Default agent for predictions. */
192
- defaultAgent: string
193
188
  /** Timeout in seconds for agent calls. */
194
189
  timeoutSec: number
195
- /** Working directory for opencode (where .opencode/agents/ lives). */
190
+ /** Target working directory for backend tools. */
196
191
  workdir?: string
192
+ /** OpenCode CLI specific options. */
193
+ opencode?: OpenCodeOptions
197
194
  /** Claude Code specific options. */
198
195
  claudeCode?: ClaudeCodeOptions
199
196
  /** Codex SDK specific options. */
@@ -218,7 +215,7 @@ export interface StepResult<T> {
218
215
  stepName: string
219
216
  /** Execution duration in milliseconds. */
220
217
  duration: number
221
- /** OpenCode session ID used. */
218
+ /** Backend session ID used. */
222
219
  sessionId: string
223
220
  /** Model used for this step. */
224
221
  model: ModelConfig
@@ -251,8 +248,8 @@ export interface BaseState {
251
248
  sessionId: string
252
249
  /** ISO timestamp when pipeline started. */
253
250
  startedAt: string
254
- /** Current OpenCode session ID (for continuity). */
255
- opencodeSessionId?: string
251
+ /** Current backend session ID for continuity. */
252
+ agentSessionId?: string
256
253
  /** Current phase name (for resume). */
257
254
  phase: string
258
255
  /** All completed steps. */
@@ -271,7 +268,7 @@ export interface PredictResult<T> {
271
268
  data: T
272
269
  /** Raw response text from the LLM. */
273
270
  raw: string
274
- /** OpenCode session ID. */
271
+ /** Backend session ID. */
275
272
  sessionId: string
276
273
  /** Execution duration in milliseconds. */
277
274
  duration: number
@@ -364,9 +361,7 @@ export interface CorrectionConfig {
364
361
 
365
362
  /** Error codes for field errors, enabling robust error type detection. */
366
363
  export type FieldErrorCode =
367
- | 'json_parse_failed'
368
- | 'no_json_found'
369
- | 'schema_validation_failed'
364
+ 'json_parse_failed' | 'no_json_found' | 'schema_validation_failed'
370
365
 
371
366
  /** A field-level error from schema validation. */
372
367
  export interface FieldError {
@@ -406,8 +401,6 @@ export interface PipelineConfig {
406
401
  name: string
407
402
  /** Default model for predictions. */
408
403
  defaultModel: ModelConfig
409
- /** Default agent for predictions. */
410
- defaultAgent: string
411
404
  /** Directory for checkpoint files. */
412
405
  checkpointDir: string
413
406
  /** Directory for log files. */
@@ -416,8 +409,10 @@ export interface PipelineConfig {
416
409
  retry?: RetryConfig
417
410
  /** Default timeout in seconds. */
418
411
  timeoutSec?: number
419
- /** Working directory for opencode (where .opencode/agents/ lives). */
412
+ /** Target working directory for backend tools. */
420
413
  workdir?: string
414
+ /** OpenCode CLI specific options. */
415
+ opencode?: OpenCodeOptions
421
416
  /** Claude Code specific options. */
422
417
  claudeCode?: ClaudeCodeOptions
423
418
  /** Codex SDK specific options. */
@@ -444,20 +439,20 @@ export interface RunOptions {
444
439
  // Agent Types
445
440
  // ============================================================================
446
441
 
447
- /** Options for running an OpenCode agent. */
442
+ /** Options for running a prompt through a backend. */
448
443
  export interface RunAgentOptions {
449
- /** The prompt to send to the agent. */
444
+ /** The prompt to send to the backend. */
450
445
  prompt: string
451
- /** Agent type (e.g., "journey-creator", "explore", "general"). */
452
- agent: string
453
446
  /** Model to use. */
454
447
  model: ModelConfig
455
448
  /** Existing session ID to continue. */
456
449
  sessionId?: string
457
450
  /** Timeout in seconds. */
458
451
  timeoutSec?: number
459
- /** Working directory for opencode (where .opencode/agents/ lives). */
452
+ /** Target working directory for backend tools. */
460
453
  workdir?: string
454
+ /** OpenCode CLI specific options. */
455
+ opencode?: OpenCodeOptions
461
456
  /** Claude Code specific options. */
462
457
  claudeCode?: ClaudeCodeOptions
463
458
  /** Codex SDK specific options. */
@@ -470,7 +465,7 @@ export interface RunAgentOptions {
470
465
  signal?: AbortSignal
471
466
  }
472
467
 
473
- /** Result from running an OpenCode agent. */
468
+ /** Result from running a prompt through a backend. */
474
469
  export interface RunAgentResult {
475
470
  /** The text response from the agent. */
476
471
  text: string