ocpipe 0.5.2 → 0.5.4
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 +15 -22
- package/package.json +1 -1
- package/src/agent.ts +55 -8
- package/src/claude-code.ts +28 -7
- package/src/predict.ts +3 -0
- package/src/types.ts +6 -1
package/README.md
CHANGED
|
@@ -54,33 +54,26 @@ ocpipe supports two backends for running LLM agents:
|
|
|
54
54
|
**OpenCode** (default) - Requires `opencode` CLI in your PATH. Supports 75+ providers.
|
|
55
55
|
|
|
56
56
|
```typescript
|
|
57
|
-
const pipeline = new Pipeline(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
const pipeline = new Pipeline(
|
|
58
|
+
{
|
|
59
|
+
name: 'my-pipeline',
|
|
60
|
+
defaultModel: {
|
|
61
|
+
providerID: 'anthropic',
|
|
62
|
+
modelID: 'claude-sonnet-4-20250514',
|
|
63
|
+
},
|
|
64
|
+
defaultAgent: 'default',
|
|
65
|
+
},
|
|
66
|
+
createBaseState,
|
|
67
|
+
)
|
|
62
68
|
```
|
|
63
69
|
|
|
64
70
|
**Claude Code** - Uses `@anthropic-ai/claude-agent-sdk`. Install as a peer dependency.
|
|
65
71
|
|
|
66
72
|
```typescript
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// Permission mode controls what Claude Code can do without prompting
|
|
72
|
-
// Options: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan'
|
|
73
|
-
claudeCode: { permissionMode: 'acceptEdits' }, // default: auto-approve file writes
|
|
74
|
-
}, createBaseState)
|
|
75
|
-
|
|
76
|
-
// To bypass all permission prompts (use with caution):
|
|
77
|
-
const pipeline = new Pipeline({
|
|
78
|
-
...config,
|
|
79
|
-
claudeCode: {
|
|
80
|
-
permissionMode: 'bypassPermissions',
|
|
81
|
-
dangerouslySkipPermissions: true, // required safety flag
|
|
82
|
-
},
|
|
83
|
-
}, createBaseState)
|
|
73
|
+
// modelID: 'opus', 'sonnet', or 'haiku'
|
|
74
|
+
defaultModel: { backend: 'claude-code', modelID: 'sonnet' },
|
|
75
|
+
// permissionMode: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan'
|
|
76
|
+
claudeCode: { permissionMode: 'acceptEdits' },
|
|
84
77
|
```
|
|
85
78
|
|
|
86
79
|
### Requirements
|
package/package.json
CHANGED
package/src/agent.ts
CHANGED
|
@@ -34,8 +34,11 @@ export async function runAgent(
|
|
|
34
34
|
async function runOpencodeAgent(
|
|
35
35
|
options: RunAgentOptions,
|
|
36
36
|
): Promise<RunAgentResult> {
|
|
37
|
-
const { prompt, agent, model, sessionId, timeoutSec = 600, workdir } = options
|
|
37
|
+
const { prompt, agent, model, sessionId, timeoutSec = 600, workdir, signal } = options
|
|
38
38
|
|
|
39
|
+
if (!model.providerID) {
|
|
40
|
+
throw new Error('providerID is required for OpenCode backend')
|
|
41
|
+
}
|
|
39
42
|
const modelStr = `${model.providerID}/${model.modelID}`
|
|
40
43
|
const sessionInfo = sessionId ? `[session:${sessionId}]` : '[new session]'
|
|
41
44
|
const promptPreview = prompt.slice(0, 50).replace(/\n/g, ' ')
|
|
@@ -44,6 +47,11 @@ async function runOpencodeAgent(
|
|
|
44
47
|
`\n>>> OpenCode [${agent}] [${modelStr}] ${sessionInfo}: ${promptPreview}...`,
|
|
45
48
|
)
|
|
46
49
|
|
|
50
|
+
// Check if already aborted
|
|
51
|
+
if (signal?.aborted) {
|
|
52
|
+
throw new Error('Request aborted')
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
// Write prompt to .opencode/prompts/ within the working directory
|
|
48
56
|
const cwd = workdir ?? PROJECT_ROOT
|
|
49
57
|
const promptsDir = join(cwd, '.opencode', 'prompts')
|
|
@@ -71,7 +79,9 @@ async function runOpencodeAgent(
|
|
|
71
79
|
|
|
72
80
|
return new Promise((resolve, reject) => {
|
|
73
81
|
const opencodeCmd = getOpencodeCommand(args)
|
|
74
|
-
console.error(
|
|
82
|
+
console.error(
|
|
83
|
+
`[DEBUG] Running: ${opencodeCmd.cmd} ${opencodeCmd.args.join(' ')}`,
|
|
84
|
+
)
|
|
75
85
|
const proc = spawn(opencodeCmd.cmd, opencodeCmd.args, {
|
|
76
86
|
cwd,
|
|
77
87
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -80,6 +90,22 @@ async function runOpencodeAgent(
|
|
|
80
90
|
let newSessionId = sessionId || ''
|
|
81
91
|
const stdoutChunks: string[] = []
|
|
82
92
|
const stderrChunks: string[] = []
|
|
93
|
+
let aborted = false
|
|
94
|
+
|
|
95
|
+
// Handle abort signal - kill subprocess when aborted
|
|
96
|
+
const abortHandler = async () => {
|
|
97
|
+
if (aborted) return
|
|
98
|
+
aborted = true
|
|
99
|
+
console.error(`\n[abort] Killing OpenCode subprocess...`)
|
|
100
|
+
proc.kill('SIGTERM')
|
|
101
|
+
// Give it a moment to clean up, then force kill
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
if (!proc.killed) proc.kill('SIGKILL')
|
|
104
|
+
}, 1000)
|
|
105
|
+
await unlink(promptFile).catch(() => {})
|
|
106
|
+
reject(new Error('Request aborted'))
|
|
107
|
+
}
|
|
108
|
+
signal?.addEventListener('abort', abortHandler, { once: true })
|
|
83
109
|
|
|
84
110
|
// Stream stderr in real-time (OpenCode progress output)
|
|
85
111
|
proc.stderr.on('data', (data: Buffer) => {
|
|
@@ -120,6 +146,10 @@ async function runOpencodeAgent(
|
|
|
120
146
|
|
|
121
147
|
proc.on('close', async (code) => {
|
|
122
148
|
if (timeout) clearTimeout(timeout)
|
|
149
|
+
signal?.removeEventListener('abort', abortHandler)
|
|
150
|
+
|
|
151
|
+
// If aborted, we already rejected
|
|
152
|
+
if (aborted) return
|
|
123
153
|
|
|
124
154
|
// Clean up prompt file
|
|
125
155
|
await unlink(promptFile).catch(() => {})
|
|
@@ -135,19 +165,32 @@ async function runOpencodeAgent(
|
|
|
135
165
|
|
|
136
166
|
// Check for OpenCode errors that exit with code 0 but produce no output
|
|
137
167
|
const knownErrors = [
|
|
138
|
-
{
|
|
168
|
+
{
|
|
169
|
+
pattern: /ProviderModelNotFoundError/,
|
|
170
|
+
message: 'Provider/model not found',
|
|
171
|
+
},
|
|
139
172
|
{ pattern: /ModelNotFoundError/, message: 'Model not found' },
|
|
140
173
|
{ pattern: /ProviderNotFoundError/, message: 'Provider not found' },
|
|
141
174
|
{ pattern: /API key.*not.*found/i, message: 'API key not configured' },
|
|
142
|
-
{
|
|
175
|
+
{
|
|
176
|
+
pattern: /authentication.*failed/i,
|
|
177
|
+
message: 'Authentication failed',
|
|
178
|
+
},
|
|
143
179
|
]
|
|
144
180
|
|
|
145
181
|
for (const { pattern, message } of knownErrors) {
|
|
146
182
|
if (pattern.test(stderr)) {
|
|
147
183
|
// Extract the relevant error lines
|
|
148
|
-
const errorLines = stderr
|
|
149
|
-
|
|
150
|
-
|
|
184
|
+
const errorLines = stderr
|
|
185
|
+
.split('\n')
|
|
186
|
+
.filter(
|
|
187
|
+
(line) =>
|
|
188
|
+
pattern.test(line) ||
|
|
189
|
+
line.includes('Error') ||
|
|
190
|
+
line.includes('error:'),
|
|
191
|
+
)
|
|
192
|
+
.slice(0, 5)
|
|
193
|
+
.join('\n')
|
|
151
194
|
reject(new Error(`OpenCode ${message}:\n${errorLines}`))
|
|
152
195
|
return
|
|
153
196
|
}
|
|
@@ -166,7 +209,11 @@ async function runOpencodeAgent(
|
|
|
166
209
|
// Check for empty response with errors in stderr (likely a silent failure)
|
|
167
210
|
if (response.length === 0 && stderr.includes('Error')) {
|
|
168
211
|
const lastLines = stderr.split('\n').slice(-10).join('\n')
|
|
169
|
-
reject(
|
|
212
|
+
reject(
|
|
213
|
+
new Error(
|
|
214
|
+
`OpenCode returned empty response with errors:\n${lastLines}`,
|
|
215
|
+
),
|
|
216
|
+
)
|
|
170
217
|
return
|
|
171
218
|
}
|
|
172
219
|
|
package/src/claude-code.ts
CHANGED
|
@@ -62,7 +62,12 @@ const logToolCall: HookCallback = async (input) => {
|
|
|
62
62
|
export async function runClaudeCodeAgent(
|
|
63
63
|
options: RunAgentOptions,
|
|
64
64
|
): Promise<RunAgentResult> {
|
|
65
|
-
const { prompt, model, sessionId, timeoutSec = 600, claudeCode } = options
|
|
65
|
+
const { prompt, model, sessionId, timeoutSec = 600, claudeCode, signal } = options
|
|
66
|
+
|
|
67
|
+
// Check if already aborted
|
|
68
|
+
if (signal?.aborted) {
|
|
69
|
+
throw new Error('Request aborted')
|
|
70
|
+
}
|
|
66
71
|
|
|
67
72
|
// Claude Code understands simple names: opus, sonnet, haiku
|
|
68
73
|
const modelStr = normalizeModelId(model.modelID)
|
|
@@ -94,6 +99,13 @@ export async function runClaudeCodeAgent(
|
|
|
94
99
|
unstable_v2_resumeSession(sessionId, sessionOptions)
|
|
95
100
|
: unstable_v2_createSession(sessionOptions)
|
|
96
101
|
|
|
102
|
+
// Handle abort signal
|
|
103
|
+
const abortHandler = () => {
|
|
104
|
+
console.error(`\n[abort] Closing Claude Code session...`)
|
|
105
|
+
session.close()
|
|
106
|
+
}
|
|
107
|
+
signal?.addEventListener('abort', abortHandler, { once: true })
|
|
108
|
+
|
|
97
109
|
try {
|
|
98
110
|
// Send the prompt
|
|
99
111
|
await session.send(prompt)
|
|
@@ -113,6 +125,15 @@ export async function runClaudeCodeAgent(
|
|
|
113
125
|
})
|
|
114
126
|
: null
|
|
115
127
|
|
|
128
|
+
// Set up abort promise
|
|
129
|
+
const abortPromise = signal ?
|
|
130
|
+
new Promise<never>((_, reject) => {
|
|
131
|
+
signal.addEventListener('abort', () => {
|
|
132
|
+
reject(new Error('Request aborted'))
|
|
133
|
+
}, { once: true })
|
|
134
|
+
})
|
|
135
|
+
: null
|
|
136
|
+
|
|
116
137
|
// Stream the response
|
|
117
138
|
const streamPromise = (async () => {
|
|
118
139
|
for await (const msg of session.stream()) {
|
|
@@ -129,12 +150,11 @@ export async function runClaudeCodeAgent(
|
|
|
129
150
|
}
|
|
130
151
|
})()
|
|
131
152
|
|
|
132
|
-
// Race between stream and
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
153
|
+
// Race between stream, timeout, and abort
|
|
154
|
+
const promises: Promise<void | never>[] = [streamPromise]
|
|
155
|
+
if (timeoutPromise) promises.push(timeoutPromise)
|
|
156
|
+
if (abortPromise) promises.push(abortPromise)
|
|
157
|
+
await Promise.race(promises)
|
|
138
158
|
|
|
139
159
|
const response = textParts.join('')
|
|
140
160
|
const sessionStr = newSessionId || 'none'
|
|
@@ -147,6 +167,7 @@ export async function runClaudeCodeAgent(
|
|
|
147
167
|
sessionId: newSessionId,
|
|
148
168
|
}
|
|
149
169
|
} finally {
|
|
170
|
+
signal?.removeEventListener('abort', abortHandler)
|
|
150
171
|
session.close()
|
|
151
172
|
}
|
|
152
173
|
}
|
package/src/predict.ts
CHANGED
|
@@ -77,6 +77,7 @@ export class Predict<S extends AnySignature> {
|
|
|
77
77
|
timeoutSec: ctx.timeoutSec,
|
|
78
78
|
workdir: ctx.workdir,
|
|
79
79
|
claudeCode: ctx.claudeCode,
|
|
80
|
+
signal: ctx.signal,
|
|
80
81
|
})
|
|
81
82
|
|
|
82
83
|
// Update context with new session ID for continuity
|
|
@@ -206,6 +207,7 @@ export class Predict<S extends AnySignature> {
|
|
|
206
207
|
timeoutSec: 60,
|
|
207
208
|
workdir: ctx.workdir,
|
|
208
209
|
claudeCode: ctx.claudeCode,
|
|
210
|
+
signal: ctx.signal,
|
|
209
211
|
})
|
|
210
212
|
|
|
211
213
|
// Try to parse the repaired JSON
|
|
@@ -282,6 +284,7 @@ export class Predict<S extends AnySignature> {
|
|
|
282
284
|
timeoutSec: 60, // Short timeout for simple patches
|
|
283
285
|
workdir: ctx.workdir,
|
|
284
286
|
claudeCode: ctx.claudeCode,
|
|
287
|
+
signal: ctx.signal,
|
|
285
288
|
})
|
|
286
289
|
|
|
287
290
|
// Extract and apply the patch based on method
|
package/src/types.ts
CHANGED
|
@@ -30,7 +30,8 @@ export interface ClaudeCodeOptions {
|
|
|
30
30
|
export interface ModelConfig {
|
|
31
31
|
/** Backend to use (default: 'opencode'). */
|
|
32
32
|
backend?: BackendType
|
|
33
|
-
|
|
33
|
+
/** Provider ID (required for OpenCode, ignored for Claude Code). */
|
|
34
|
+
providerID?: string
|
|
34
35
|
modelID: string
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -52,6 +53,8 @@ export interface ExecutionContext {
|
|
|
52
53
|
workdir?: string
|
|
53
54
|
/** Claude Code specific options. */
|
|
54
55
|
claudeCode?: ClaudeCodeOptions
|
|
56
|
+
/** AbortSignal for cancelling requests. When aborted, kills subprocesses. */
|
|
57
|
+
signal?: AbortSignal
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
// ============================================================================
|
|
@@ -302,6 +305,8 @@ export interface RunAgentOptions {
|
|
|
302
305
|
workdir?: string
|
|
303
306
|
/** Claude Code specific options. */
|
|
304
307
|
claudeCode?: ClaudeCodeOptions
|
|
308
|
+
/** AbortSignal for cancelling the request. When aborted, kills the subprocess. */
|
|
309
|
+
signal?: AbortSignal
|
|
305
310
|
}
|
|
306
311
|
|
|
307
312
|
/** Result from running an OpenCode agent. */
|