@smithers-orchestrator/agents 0.16.9 → 0.18.0
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/package.json +6 -4
- package/src/AgentLike.ts +4 -1
- package/src/AmpAgent.js +2 -0
- package/src/AnthropicAgent.js +10 -3
- package/src/BaseCliAgent/AgentGenerateOptions.ts +24 -0
- package/src/BaseCliAgent/BaseCliAgent.js +166 -32
- package/src/BaseCliAgent/extractPrompt.js +0 -1
- package/src/BaseCliAgent/extractTextFromJsonValue.js +2 -0
- package/src/BaseCliAgent/index.js +1 -0
- package/src/ClaudeCodeAgent.js +10 -3
- package/src/ClaudeCodeAgentOptions.ts +16 -0
- package/src/CodexAgent.js +6 -0
- package/src/CodexAgentOptions.ts +15 -0
- package/src/ForgeAgent.js +2 -0
- package/src/GeminiAgent.js +6 -2
- package/src/GeminiAgentOptions.ts +12 -0
- package/src/KimiAgent.js +211 -6
- package/src/KimiAgentOptions.ts +8 -0
- package/src/OpenAIAgent.js +10 -3
- package/src/OpenCodeAgent.js +495 -0
- package/src/OpenCodeAgent.ts +43 -0
- package/src/PiAgent.js +1 -1
- package/src/__type-tests__/AgentLike.assignability.test-d.ts +31 -0
- package/src/capability-registry/AgentCapabilityRegistry.ts +1 -1
- package/src/cli-capabilities/CliAgentCapabilityAdapterId.ts +1 -0
- package/src/cli-capabilities/getCliAgentCapabilityReport.js +6 -0
- package/src/index.d.ts +65 -64
- package/src/index.js +3 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseCliAgent,
|
|
3
|
+
pushFlag,
|
|
4
|
+
isRecord,
|
|
5
|
+
asString,
|
|
6
|
+
truncate,
|
|
7
|
+
toolKindFromName,
|
|
8
|
+
shouldSurfaceUnparsedStdout,
|
|
9
|
+
createSyntheticIdGenerator,
|
|
10
|
+
} from "./BaseCliAgent/index.js";
|
|
11
|
+
import { normalizeCapabilityStringList } from "./capability-registry/index.js";
|
|
12
|
+
|
|
13
|
+
/** @typedef {import("./BaseCliAgent/index.ts").BaseCliAgentOptions} BaseCliAgentOptions */
|
|
14
|
+
/** @typedef {import("./capability-registry/index.ts").AgentCapabilityRegistry} AgentCapabilityRegistry */
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {BaseCliAgentOptions & {
|
|
18
|
+
* model?: string;
|
|
19
|
+
* agentName?: string;
|
|
20
|
+
* attachFiles?: string[];
|
|
21
|
+
* continueSession?: boolean;
|
|
22
|
+
* sessionId?: string;
|
|
23
|
+
* variant?: "high" | "medium" | "low";
|
|
24
|
+
* }} OpenCodeAgentOptions
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** @typedef {import("./BaseCliAgent/index.ts").CliOutputInterpreter} CliOutputInterpreter */
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {OpenCodeAgentOptions} [opts] Currently unused — kept for API
|
|
31
|
+
* consistency with other agents (e.g. ClaudeCodeAgent uses opts to resolve
|
|
32
|
+
* builtIns based on tool allow/deny lists). OpenCode does not yet expose
|
|
33
|
+
* CLI flags for restricting built-in tools, so the set is static.
|
|
34
|
+
* @returns {AgentCapabilityRegistry}
|
|
35
|
+
*/
|
|
36
|
+
export function createOpenCodeCapabilityRegistry(opts = {}) {
|
|
37
|
+
return {
|
|
38
|
+
version: 1,
|
|
39
|
+
engine: "opencode",
|
|
40
|
+
runtimeTools: {},
|
|
41
|
+
mcp: {
|
|
42
|
+
bootstrap: "project-config",
|
|
43
|
+
supportsProjectScope: true,
|
|
44
|
+
supportsUserScope: true,
|
|
45
|
+
},
|
|
46
|
+
skills: {
|
|
47
|
+
supportsSkills: true,
|
|
48
|
+
installMode: "plugin",
|
|
49
|
+
smithersSkillIds: [],
|
|
50
|
+
},
|
|
51
|
+
humanInteraction: {
|
|
52
|
+
supportsUiRequests: false,
|
|
53
|
+
methods: [],
|
|
54
|
+
},
|
|
55
|
+
builtIns: normalizeCapabilityStringList([
|
|
56
|
+
"read",
|
|
57
|
+
"write",
|
|
58
|
+
"edit",
|
|
59
|
+
"apply_patch",
|
|
60
|
+
"bash",
|
|
61
|
+
"glob",
|
|
62
|
+
"grep",
|
|
63
|
+
"list",
|
|
64
|
+
"webfetch",
|
|
65
|
+
"websearch",
|
|
66
|
+
"codesearch",
|
|
67
|
+
"question",
|
|
68
|
+
"task",
|
|
69
|
+
"todowrite",
|
|
70
|
+
"skill",
|
|
71
|
+
]),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* CLI agent wrapper for OpenCode (https://opencode.ai).
|
|
77
|
+
*
|
|
78
|
+
* Shells out to `opencode run` in non-interactive mode with `--format json`
|
|
79
|
+
* for streaming nd-JSON output. Parses AgentCliEvents from the JSON stream.
|
|
80
|
+
*
|
|
81
|
+
* Usage:
|
|
82
|
+
* const agent = new OpenCodeAgent({
|
|
83
|
+
* model: "anthropic/claude-opus-4-20250514",
|
|
84
|
+
* yolo: true,
|
|
85
|
+
* });
|
|
86
|
+
* const result = await agent.generate({
|
|
87
|
+
* messages: [{ role: "user", content: "Fix the bug" }],
|
|
88
|
+
* });
|
|
89
|
+
*/
|
|
90
|
+
export class OpenCodeAgent extends BaseCliAgent {
|
|
91
|
+
/** @type {OpenCodeAgentOptions} */
|
|
92
|
+
opts;
|
|
93
|
+
/** @type {AgentCapabilityRegistry} */
|
|
94
|
+
capabilities;
|
|
95
|
+
/** @type {"opencode"} */
|
|
96
|
+
cliEngine = "opencode";
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {OpenCodeAgentOptions} [opts]
|
|
100
|
+
*/
|
|
101
|
+
constructor(opts = {}) {
|
|
102
|
+
super(opts);
|
|
103
|
+
this.opts = opts;
|
|
104
|
+
this.capabilities = createOpenCodeCapabilityRegistry(opts);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create an output interpreter that parses OpenCode's nd-JSON streaming format.
|
|
109
|
+
*
|
|
110
|
+
* OpenCode `--format json` emits one JSON object per line (verified from source:
|
|
111
|
+
* packages/opencode/src/cli/cmd/run.ts). The envelope is:
|
|
112
|
+
*
|
|
113
|
+
* { type, timestamp: number, sessionID: string, ...payload }
|
|
114
|
+
*
|
|
115
|
+
* Event types:
|
|
116
|
+
* step_start → { part: { type:"step-start", id, sessionID, messageID } }
|
|
117
|
+
* text → { part: { type:"text", text, time: { start, end } } }
|
|
118
|
+
* tool_use → { part: { type:"tool", tool, callID, state: { status, ... } } }
|
|
119
|
+
* step_finish → { part: { type:"step-finish", reason, tokens, cost } }
|
|
120
|
+
* reasoning → { part: { type:"reasoning", text } }
|
|
121
|
+
* error → { error: { name, data: { message } } }
|
|
122
|
+
*
|
|
123
|
+
* We map these to Smithers' AgentCliEvent union (started | action | completed).
|
|
124
|
+
*
|
|
125
|
+
* @returns {CliOutputInterpreter}
|
|
126
|
+
*/
|
|
127
|
+
createOutputInterpreter() {
|
|
128
|
+
let fullText = "";
|
|
129
|
+
let sessionId = "";
|
|
130
|
+
let didEmitStarted = false;
|
|
131
|
+
let didEmitCompleted = false;
|
|
132
|
+
let terminalError = null;
|
|
133
|
+
|
|
134
|
+
// Accumulate tokens across multiple step_finish events
|
|
135
|
+
let totalInputTokens = 0;
|
|
136
|
+
let totalOutputTokens = 0;
|
|
137
|
+
let totalTokens = 0;
|
|
138
|
+
|
|
139
|
+
const nextSyntheticId = createSyntheticIdGenerator();
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {string} title
|
|
143
|
+
* @param {string} message
|
|
144
|
+
* @param {"warning" | "error"} [level]
|
|
145
|
+
* @returns {import("./BaseCliAgent/index.ts").AgentCliEvent}
|
|
146
|
+
*/
|
|
147
|
+
const warningAction = (title, message, level = "warning") => ({
|
|
148
|
+
type: "action",
|
|
149
|
+
engine: this.cliEngine,
|
|
150
|
+
phase: "completed",
|
|
151
|
+
entryType: "thought",
|
|
152
|
+
action: {
|
|
153
|
+
id: nextSyntheticId("opencode-warning"),
|
|
154
|
+
kind: "warning",
|
|
155
|
+
title,
|
|
156
|
+
detail: {},
|
|
157
|
+
},
|
|
158
|
+
message,
|
|
159
|
+
ok: level !== "error",
|
|
160
|
+
level,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {string} line
|
|
165
|
+
* @returns {import("./BaseCliAgent/index.ts").AgentCliEvent[]}
|
|
166
|
+
*/
|
|
167
|
+
const parseLine = (line) => {
|
|
168
|
+
// Strip OSC terminal escape sequences (e.g. title-setting "\x1b]0;...\x07")
|
|
169
|
+
// that OpenCode emits inline with JSON events on stdout.
|
|
170
|
+
const cleaned = line.replace(/\x1b\]0;[^\x07]*\x07/g, "");
|
|
171
|
+
const trimmed = cleaned.trim();
|
|
172
|
+
if (!trimmed) return [];
|
|
173
|
+
|
|
174
|
+
/** @type {Record<string, unknown>} */
|
|
175
|
+
let payload;
|
|
176
|
+
try {
|
|
177
|
+
payload = JSON.parse(trimmed);
|
|
178
|
+
} catch {
|
|
179
|
+
if (!shouldSurfaceUnparsedStdout(trimmed)) return [];
|
|
180
|
+
return [warningAction("stdout", truncate(trimmed, 220))];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!isRecord(payload)) return [];
|
|
184
|
+
|
|
185
|
+
const eventType = asString(payload.type);
|
|
186
|
+
if (!eventType) return [];
|
|
187
|
+
|
|
188
|
+
// Capture sessionID from the envelope (present on every event)
|
|
189
|
+
const envelopeSessionId = asString(payload.sessionID);
|
|
190
|
+
if (envelopeSessionId) {
|
|
191
|
+
sessionId = envelopeSessionId;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const part = isRecord(payload.part) ? payload.part : null;
|
|
195
|
+
|
|
196
|
+
// --- step_start: session/step beginning ---
|
|
197
|
+
if (eventType === "step_start") {
|
|
198
|
+
if (!didEmitStarted) {
|
|
199
|
+
didEmitStarted = true;
|
|
200
|
+
return [
|
|
201
|
+
{
|
|
202
|
+
type: "started",
|
|
203
|
+
engine: this.cliEngine,
|
|
204
|
+
title: "OpenCode",
|
|
205
|
+
resume: sessionId || undefined,
|
|
206
|
+
detail: sessionId ? { sessionId } : undefined,
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --- text: finalized text chunk from the model ---
|
|
214
|
+
if (eventType === "text") {
|
|
215
|
+
const text = part ? asString(part.text) : null;
|
|
216
|
+
if (text) {
|
|
217
|
+
fullText += text;
|
|
218
|
+
return [
|
|
219
|
+
{
|
|
220
|
+
type: "action",
|
|
221
|
+
engine: this.cliEngine,
|
|
222
|
+
phase: "updated",
|
|
223
|
+
entryType: "message",
|
|
224
|
+
action: {
|
|
225
|
+
id: nextSyntheticId("opencode-text"),
|
|
226
|
+
kind: "note",
|
|
227
|
+
title: "assistant",
|
|
228
|
+
detail: {},
|
|
229
|
+
},
|
|
230
|
+
message: text,
|
|
231
|
+
ok: true,
|
|
232
|
+
level: "info",
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
}
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// --- tool_use: tool completed or errored ---
|
|
240
|
+
if (eventType === "tool_use" && part) {
|
|
241
|
+
const toolName = asString(part.tool) ?? "tool";
|
|
242
|
+
const callID = asString(part.callID) ?? nextSyntheticId("opencode-tool");
|
|
243
|
+
const state = isRecord(part.state) ? part.state : null;
|
|
244
|
+
const status = state ? asString(state.status) : null;
|
|
245
|
+
const isError = status === "error";
|
|
246
|
+
|
|
247
|
+
const events = [];
|
|
248
|
+
|
|
249
|
+
// Emit a "started" action for the tool
|
|
250
|
+
events.push({
|
|
251
|
+
type: "action",
|
|
252
|
+
engine: this.cliEngine,
|
|
253
|
+
phase: "started",
|
|
254
|
+
entryType: "thought",
|
|
255
|
+
action: {
|
|
256
|
+
id: callID,
|
|
257
|
+
kind: toolKindFromName(toolName),
|
|
258
|
+
title: toolName,
|
|
259
|
+
detail: state && isRecord(state.input)
|
|
260
|
+
? { input: state.input }
|
|
261
|
+
: {},
|
|
262
|
+
},
|
|
263
|
+
message: `Running ${toolName}`,
|
|
264
|
+
level: "info",
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Emit a "completed" action for the tool
|
|
268
|
+
const output = state
|
|
269
|
+
? asString(state.output) ?? asString(state.error)
|
|
270
|
+
: undefined;
|
|
271
|
+
events.push({
|
|
272
|
+
type: "action",
|
|
273
|
+
engine: this.cliEngine,
|
|
274
|
+
phase: "completed",
|
|
275
|
+
entryType: "thought",
|
|
276
|
+
action: {
|
|
277
|
+
id: callID,
|
|
278
|
+
kind: toolKindFromName(toolName),
|
|
279
|
+
title: toolName,
|
|
280
|
+
detail: {},
|
|
281
|
+
},
|
|
282
|
+
message: output ? truncate(output, 300) : undefined,
|
|
283
|
+
ok: !isError,
|
|
284
|
+
level: isError ? "warning" : "info",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return events;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- step_finish: step completed with token usage ---
|
|
291
|
+
if (eventType === "step_finish" && part) {
|
|
292
|
+
const tokens = isRecord(part.tokens) ? part.tokens : null;
|
|
293
|
+
if (tokens) {
|
|
294
|
+
const input = typeof tokens.input === "number" ? tokens.input : 0;
|
|
295
|
+
const output = typeof tokens.output === "number" ? tokens.output : 0;
|
|
296
|
+
const total = typeof tokens.total === "number" ? tokens.total : 0;
|
|
297
|
+
totalInputTokens += input;
|
|
298
|
+
totalOutputTokens += output;
|
|
299
|
+
totalTokens += total;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const reason = asString(part.reason);
|
|
303
|
+
// Only emit "completed" on the final step (reason: "stop")
|
|
304
|
+
if (reason === "stop") {
|
|
305
|
+
if (didEmitCompleted) return [];
|
|
306
|
+
didEmitCompleted = true;
|
|
307
|
+
|
|
308
|
+
return [
|
|
309
|
+
{
|
|
310
|
+
type: "completed",
|
|
311
|
+
engine: this.cliEngine,
|
|
312
|
+
ok: true,
|
|
313
|
+
answer: fullText || undefined,
|
|
314
|
+
resume: sessionId || undefined,
|
|
315
|
+
usage: {
|
|
316
|
+
inputTokens: totalInputTokens,
|
|
317
|
+
outputTokens: totalOutputTokens,
|
|
318
|
+
totalTokens: totalTokens,
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// --- reasoning: model thinking (only with --thinking flag) ---
|
|
328
|
+
if (eventType === "reasoning") {
|
|
329
|
+
// Surface reasoning as a thought action, don't accumulate into fullText
|
|
330
|
+
const text = part ? asString(part.text) : null;
|
|
331
|
+
if (text) {
|
|
332
|
+
return [
|
|
333
|
+
{
|
|
334
|
+
type: "action",
|
|
335
|
+
engine: this.cliEngine,
|
|
336
|
+
phase: "updated",
|
|
337
|
+
entryType: "thought",
|
|
338
|
+
action: {
|
|
339
|
+
id: nextSyntheticId("opencode-reasoning"),
|
|
340
|
+
kind: "note",
|
|
341
|
+
title: "reasoning",
|
|
342
|
+
detail: {},
|
|
343
|
+
},
|
|
344
|
+
message: truncate(text, 500),
|
|
345
|
+
ok: true,
|
|
346
|
+
level: "info",
|
|
347
|
+
},
|
|
348
|
+
];
|
|
349
|
+
}
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// --- error: session error ---
|
|
354
|
+
if (eventType === "error") {
|
|
355
|
+
const errorObj = isRecord(payload.error) ? payload.error : null;
|
|
356
|
+
const errorData = errorObj && isRecord(errorObj.data) ? errorObj.data : null;
|
|
357
|
+
const errorName = errorObj ? asString(errorObj.name) : null;
|
|
358
|
+
const errorMessage = errorData
|
|
359
|
+
? asString(errorData.message)
|
|
360
|
+
: errorName ?? "OpenCode reported an error";
|
|
361
|
+
terminalError = errorMessage ?? "OpenCode reported an error";
|
|
362
|
+
|
|
363
|
+
if (didEmitCompleted) {
|
|
364
|
+
return [warningAction("error", errorMessage ?? "OpenCode reported an error", "error")];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
didEmitCompleted = true;
|
|
368
|
+
return [
|
|
369
|
+
{
|
|
370
|
+
type: "completed",
|
|
371
|
+
engine: this.cliEngine,
|
|
372
|
+
ok: false,
|
|
373
|
+
answer: fullText || undefined,
|
|
374
|
+
error: errorMessage ?? "OpenCode reported an error",
|
|
375
|
+
},
|
|
376
|
+
];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return [];
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
onStdoutLine: parseLine,
|
|
384
|
+
|
|
385
|
+
onStderrLine: (line) => {
|
|
386
|
+
const trimmed = line.trim();
|
|
387
|
+
if (!trimmed) return [];
|
|
388
|
+
return [warningAction("stderr", truncate(trimmed, 220))];
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
onExit: (result) => {
|
|
392
|
+
if (didEmitCompleted) return [];
|
|
393
|
+
const isSuccess = (result.exitCode ?? 0) === 0 && !terminalError;
|
|
394
|
+
didEmitCompleted = true;
|
|
395
|
+
return [
|
|
396
|
+
{
|
|
397
|
+
type: "completed",
|
|
398
|
+
engine: this.cliEngine,
|
|
399
|
+
ok: isSuccess,
|
|
400
|
+
answer: isSuccess ? fullText || undefined : undefined,
|
|
401
|
+
error: isSuccess
|
|
402
|
+
? undefined
|
|
403
|
+
: terminalError ?? `OpenCode exited with code ${result.exitCode ?? -1}`,
|
|
404
|
+
},
|
|
405
|
+
];
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Build the CLI command spec for `opencode run`.
|
|
412
|
+
*
|
|
413
|
+
* @param {{ prompt: string; systemPrompt?: string; cwd: string; options: any }} params
|
|
414
|
+
*/
|
|
415
|
+
async buildCommand(params) {
|
|
416
|
+
const resumeSession = typeof params.options?.resumeSession === "string"
|
|
417
|
+
? params.options.resumeSession
|
|
418
|
+
: undefined;
|
|
419
|
+
const args = ["run"];
|
|
420
|
+
|
|
421
|
+
// Model selection
|
|
422
|
+
pushFlag(args, "-m", this.opts.model ?? this.model);
|
|
423
|
+
|
|
424
|
+
// Working directory
|
|
425
|
+
pushFlag(args, "--dir", params.cwd);
|
|
426
|
+
|
|
427
|
+
// Streaming nd-JSON output
|
|
428
|
+
pushFlag(args, "--format", "json");
|
|
429
|
+
|
|
430
|
+
// Named agent config
|
|
431
|
+
pushFlag(args, "--agent", this.opts.agentName);
|
|
432
|
+
|
|
433
|
+
// File attachments: -f file1 -f file2 (repeated flag)
|
|
434
|
+
if (this.opts.attachFiles) {
|
|
435
|
+
for (const file of this.opts.attachFiles) {
|
|
436
|
+
args.push("-f", file);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Session continuation
|
|
441
|
+
const explicitSession = resumeSession ?? this.opts.sessionId;
|
|
442
|
+
if (this.opts.continueSession && !explicitSession) {
|
|
443
|
+
args.push("--continue");
|
|
444
|
+
}
|
|
445
|
+
pushFlag(args, "--session", explicitSession);
|
|
446
|
+
|
|
447
|
+
// Variant / reasoning effort
|
|
448
|
+
pushFlag(args, "--variant", this.opts.variant);
|
|
449
|
+
|
|
450
|
+
// Yolo mode: auto-approve all tool calls.
|
|
451
|
+
// OpenCode parses OPENCODE_PERMISSION with JSON.parse() and expects a
|
|
452
|
+
// permission object. '{"*":"allow"}' grants blanket approval for every
|
|
453
|
+
// tool category. See: packages/opencode/src/config/config.ts
|
|
454
|
+
const yoloEnabled = this.opts.yolo ?? this.yolo;
|
|
455
|
+
const env = {};
|
|
456
|
+
if (yoloEnabled) {
|
|
457
|
+
env.OPENCODE_PERMISSION = '{"*":"allow"}';
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Extra args from constructor
|
|
461
|
+
if (this.extraArgs?.length) {
|
|
462
|
+
args.push(...this.extraArgs);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const systemPrefix = params.systemPrompt
|
|
466
|
+
? `${params.systemPrompt}\n\n`
|
|
467
|
+
: "";
|
|
468
|
+
const fullPrompt = `${systemPrefix}${params.prompt ?? ""}`;
|
|
469
|
+
|
|
470
|
+
// When flags like -f (yargs [array] type) are present, subsequent
|
|
471
|
+
// positional arguments can be consumed as flag values. Insert '--'
|
|
472
|
+
// to tell yargs to stop parsing flags and treat the rest as positional.
|
|
473
|
+
if (fullPrompt) {
|
|
474
|
+
args.push("--", fullPrompt);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
command: "opencode",
|
|
479
|
+
args,
|
|
480
|
+
outputFormat: "stream-json",
|
|
481
|
+
env: Object.keys(env).length > 0 ? env : undefined,
|
|
482
|
+
stdoutBannerPatterns: [
|
|
483
|
+
// OpenCode may print a version banner
|
|
484
|
+
/^opencode\s+v[\d.]+/gim,
|
|
485
|
+
// Strip OSC terminal title-setting sequences (ESC ] 0 ; ... BEL)
|
|
486
|
+
// OpenCode emits these even with --format json
|
|
487
|
+
/\x1b\]0;[^\x07]*\x07/g,
|
|
488
|
+
],
|
|
489
|
+
stdoutErrorPatterns: [
|
|
490
|
+
/^error:/im,
|
|
491
|
+
/^fatal:/im,
|
|
492
|
+
],
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type CliOutputInterpreter, BaseCliAgent } from "./BaseCliAgent";
|
|
2
|
+
import type { BaseCliAgentOptions } from "./BaseCliAgent";
|
|
3
|
+
import { type AgentCapabilityRegistry } from "./capability-registry";
|
|
4
|
+
|
|
5
|
+
export type OpenCodeAgentOptions = BaseCliAgentOptions & {
|
|
6
|
+
/** Model identifier (e.g., "anthropic/claude-opus-4-20250514", "openai/gpt-5.4") */
|
|
7
|
+
model?: string;
|
|
8
|
+
/** OpenCode agent name (maps to --agent flag, selects predefined agent config) */
|
|
9
|
+
agentName?: string;
|
|
10
|
+
/** Files to attach to the prompt via -f flags */
|
|
11
|
+
attachFiles?: string[];
|
|
12
|
+
/** Continue a previous session */
|
|
13
|
+
continueSession?: boolean;
|
|
14
|
+
/** Resume a specific session by ID */
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
/** Provider-specific model variant/reasoning effort level */
|
|
17
|
+
variant?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export declare function createOpenCodeCapabilityRegistry(
|
|
21
|
+
opts?: OpenCodeAgentOptions
|
|
22
|
+
): AgentCapabilityRegistry;
|
|
23
|
+
|
|
24
|
+
export declare class OpenCodeAgent extends BaseCliAgent {
|
|
25
|
+
private readonly opts: OpenCodeAgentOptions;
|
|
26
|
+
readonly capabilities: AgentCapabilityRegistry;
|
|
27
|
+
readonly cliEngine: "opencode";
|
|
28
|
+
constructor(opts?: OpenCodeAgentOptions);
|
|
29
|
+
createOutputInterpreter(): CliOutputInterpreter;
|
|
30
|
+
buildCommand(params: {
|
|
31
|
+
prompt: string;
|
|
32
|
+
systemPrompt?: string;
|
|
33
|
+
cwd: string;
|
|
34
|
+
options: any;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
command: string;
|
|
37
|
+
args: string[];
|
|
38
|
+
outputFormat: "stream-json";
|
|
39
|
+
env?: Record<string, string>;
|
|
40
|
+
stdoutBannerPatterns: RegExp[];
|
|
41
|
+
stdoutErrorPatterns: RegExp[];
|
|
42
|
+
}>;
|
|
43
|
+
}
|
package/src/PiAgent.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// @smithers-type-exports-end
|
|
5
5
|
|
|
6
6
|
import { Effect } from "effect";
|
|
7
|
-
import { BaseCliAgent, buildGenerateResult, combineNonEmpty, extractPrompt, extractTextFromJsonValue, pushFlag, resolveTimeouts, runAgentPromise, runRpcCommandEffect,
|
|
7
|
+
import { BaseCliAgent, buildGenerateResult, combineNonEmpty, extractPrompt, extractTextFromJsonValue, pushFlag, resolveTimeouts, runAgentPromise, runRpcCommandEffect, asString, truncate, toolKindFromName, } from "./BaseCliAgent/index.js";
|
|
8
8
|
import { normalizeCapabilityStringList, } from "./capability-registry/index.js";
|
|
9
9
|
import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
|
|
10
10
|
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentLike,
|
|
3
|
+
AmpAgent,
|
|
4
|
+
AnthropicAgent,
|
|
5
|
+
ClaudeCodeAgent,
|
|
6
|
+
CodexAgent,
|
|
7
|
+
ForgeAgent,
|
|
8
|
+
GeminiAgent,
|
|
9
|
+
KimiAgent,
|
|
10
|
+
OpenAIAgent,
|
|
11
|
+
PiAgent,
|
|
12
|
+
} from "../index.js";
|
|
13
|
+
|
|
14
|
+
type AssertAssignable<T extends AgentLike> = T;
|
|
15
|
+
|
|
16
|
+
type _CustomNativeStructuredAgent = AssertAssignable<{
|
|
17
|
+
supportsNativeStructuredOutput: true;
|
|
18
|
+
generate: () => Promise<unknown>;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
type _ConcreteAgentsAreAgentLike = [
|
|
22
|
+
AssertAssignable<AmpAgent>,
|
|
23
|
+
AssertAssignable<AnthropicAgent>,
|
|
24
|
+
AssertAssignable<ClaudeCodeAgent>,
|
|
25
|
+
AssertAssignable<CodexAgent>,
|
|
26
|
+
AssertAssignable<ForgeAgent>,
|
|
27
|
+
AssertAssignable<GeminiAgent>,
|
|
28
|
+
AssertAssignable<KimiAgent>,
|
|
29
|
+
AssertAssignable<OpenAIAgent>,
|
|
30
|
+
AssertAssignable<PiAgent>,
|
|
31
|
+
];
|
|
@@ -2,7 +2,7 @@ import type { AgentToolDescriptor } from "./AgentToolDescriptor";
|
|
|
2
2
|
|
|
3
3
|
export type AgentCapabilityRegistry = {
|
|
4
4
|
version: 1;
|
|
5
|
-
engine: "claude-code" | "codex" | "gemini" | "kimi" | "pi" | "amp" | "forge";
|
|
5
|
+
engine: "claude-code" | "codex" | "gemini" | "kimi" | "pi" | "amp" | "forge" | "opencode";
|
|
6
6
|
runtimeTools: Record<string, AgentToolDescriptor>;
|
|
7
7
|
mcp: {
|
|
8
8
|
bootstrap: "inline-config" | "project-config" | "allow-list" | "unsupported";
|
|
@@ -3,6 +3,7 @@ import { createClaudeCodeCapabilityRegistry } from "../ClaudeCodeAgent.js";
|
|
|
3
3
|
import { createCodexCapabilityRegistry } from "../CodexAgent.js";
|
|
4
4
|
import { createGeminiCapabilityRegistry } from "../GeminiAgent.js";
|
|
5
5
|
import { createKimiCapabilityRegistry } from "../KimiAgent.js";
|
|
6
|
+
import { createOpenCodeCapabilityRegistry } from "../OpenCodeAgent.js";
|
|
6
7
|
import { createPiCapabilityRegistry } from "../PiAgent.js";
|
|
7
8
|
/** @typedef {import("./CliAgentCapabilityReportEntry.ts").CliAgentCapabilityReportEntry} CliAgentCapabilityReportEntry */
|
|
8
9
|
|
|
@@ -27,6 +28,11 @@ const CLI_AGENT_CAPABILITY_ADAPTERS = [
|
|
|
27
28
|
binary: "kimi",
|
|
28
29
|
buildRegistry: () => createKimiCapabilityRegistry(),
|
|
29
30
|
},
|
|
31
|
+
{
|
|
32
|
+
id: "opencode",
|
|
33
|
+
binary: "opencode",
|
|
34
|
+
buildRegistry: () => createOpenCodeCapabilityRegistry(),
|
|
35
|
+
},
|
|
30
36
|
{
|
|
31
37
|
id: "pi",
|
|
32
38
|
binary: "pi",
|