@osdlc/agent-call 0.0.1-stage → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +187 -0
- package/CONTRIBUTING.md +65 -0
- package/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +298 -1
- package/SECURITY.md +23 -0
- package/dist/adapters/antigravity.d.ts +15 -0
- package/dist/adapters/antigravity.js +262 -0
- package/dist/adapters/claude.d.ts +29 -0
- package/dist/adapters/claude.js +314 -0
- package/dist/adapters/codex.d.ts +18 -0
- package/dist/adapters/codex.js +374 -0
- package/dist/adapters/custom.d.ts +31 -0
- package/dist/adapters/custom.js +42 -0
- package/dist/adapters/opencode.d.ts +12 -0
- package/dist/adapters/opencode.js +207 -0
- package/dist/adapters/shared.d.ts +40 -0
- package/dist/adapters/shared.js +101 -0
- package/dist/adapters/template.d.ts +43 -0
- package/dist/adapters/template.js +62 -0
- package/dist/adapters/types.d.ts +55 -0
- package/dist/adapters/types.js +1 -0
- package/dist/conversation.d.ts +73 -0
- package/dist/conversation.js +37 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +12 -0
- package/dist/model-args.d.ts +37 -0
- package/dist/model-args.js +58 -0
- package/dist/models.d.ts +9 -0
- package/dist/models.js +12 -0
- package/dist/plan.d.ts +56 -0
- package/dist/plan.js +9 -0
- package/dist/registry.d.ts +21 -0
- package/dist/registry.js +41 -0
- package/dist/run.d.ts +75 -0
- package/dist/run.js +172 -0
- package/dist/spawn.d.ts +10 -0
- package/dist/spawn.js +143 -0
- package/dist/spec.d.ts +132 -0
- package/dist/spec.js +11 -0
- package/package.json +67 -4
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { CONVERSATION_SCHEMA, } from '../conversation.js';
|
|
2
|
+
import { note, TMP_TOKEN } from '../plan.js';
|
|
3
|
+
import { applyPermissionMode, asNumber, asString, buildEffectivePrompt, get, modelFlag, parseJsonLines, tryParseJson, } from './shared.js';
|
|
4
|
+
const LAST_MESSAGE_FILE = 'last-message.txt';
|
|
5
|
+
const SCHEMA_FILE = 'output-schema.json';
|
|
6
|
+
/**
|
|
7
|
+
* OpenAI Codex CLI (`codex exec`). Sandbox modes map cleanly onto the
|
|
8
|
+
* canonical permission postures; structured output is native via
|
|
9
|
+
* `--output-schema`; tool-level rules are not expressible.
|
|
10
|
+
*/
|
|
11
|
+
export function codexAdapter(options = {}) {
|
|
12
|
+
const binary = options.binary ?? 'codex';
|
|
13
|
+
return {
|
|
14
|
+
name: 'codex',
|
|
15
|
+
binary,
|
|
16
|
+
plan(spec) {
|
|
17
|
+
return planCodex(binary, spec);
|
|
18
|
+
},
|
|
19
|
+
parseResult(planned, raw) {
|
|
20
|
+
return parseCodex(planned, raw);
|
|
21
|
+
},
|
|
22
|
+
toConversation(planned, raw) {
|
|
23
|
+
return codexConversation(planned, raw);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* `-c model_reasoning_effort="<level>"`, quoted as codex's TOML override
|
|
29
|
+
* expects. The level passes through as given; which levels a model takes
|
|
30
|
+
* is the caller's to know (`codex debug models` lists them).
|
|
31
|
+
*/
|
|
32
|
+
export function codexEffortArgv(effort) {
|
|
33
|
+
return ['-c', `model_reasoning_effort="${effort}"`];
|
|
34
|
+
}
|
|
35
|
+
function planCodex(binary, spec) {
|
|
36
|
+
const notes = [];
|
|
37
|
+
const tempFiles = [];
|
|
38
|
+
const argv = [binary, 'exec'];
|
|
39
|
+
const session = spec.session;
|
|
40
|
+
const resuming = session?.resume !== undefined || session?.continueLast === true;
|
|
41
|
+
if (resuming) {
|
|
42
|
+
argv.push('resume');
|
|
43
|
+
if (session?.resume !== undefined)
|
|
44
|
+
argv.push(session.resume);
|
|
45
|
+
else
|
|
46
|
+
argv.push('--last');
|
|
47
|
+
note(notes, session?.resume !== undefined ? 'session.resume' : 'session.continueLast', 'native', 'codex exec resume');
|
|
48
|
+
}
|
|
49
|
+
if (session?.id !== undefined) {
|
|
50
|
+
note(notes, 'session.id', 'dropped', 'codex assigns thread ids itself');
|
|
51
|
+
}
|
|
52
|
+
if (session?.persist === false) {
|
|
53
|
+
argv.push('--ephemeral');
|
|
54
|
+
note(notes, 'session.persist', 'native', '--ephemeral');
|
|
55
|
+
}
|
|
56
|
+
// Requested format → CLI reality: `--json` is a JSONL event stream, so it
|
|
57
|
+
// backs both "json" (we aggregate) and "stream-json" (events exposed).
|
|
58
|
+
const format = spec.output?.format ?? 'json';
|
|
59
|
+
if (format !== 'text') {
|
|
60
|
+
argv.push('--json');
|
|
61
|
+
if (format === 'json') {
|
|
62
|
+
note(notes, 'output.format', 'approximated', 'codex emits JSONL events; aggregated into a single result');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
note(notes, 'output.format', 'approximated', 'codex text output interleaves progress logs; final answer read from --output-last-message');
|
|
67
|
+
}
|
|
68
|
+
// Clean final answer regardless of format.
|
|
69
|
+
argv.push('--output-last-message', `${TMP_TOKEN}/${LAST_MESSAGE_FILE}`);
|
|
70
|
+
tempFiles.push({ name: LAST_MESSAGE_FILE, content: '', readBack: true });
|
|
71
|
+
argv.push('--color', 'never');
|
|
72
|
+
// The canonical spec is directory-driven, not git-driven; don't refuse
|
|
73
|
+
// to run in non-repo working directories.
|
|
74
|
+
argv.push('--skip-git-repo-check');
|
|
75
|
+
if (spec.model !== undefined)
|
|
76
|
+
argv.push(...modelFlag(spec.model));
|
|
77
|
+
if (spec.effort !== undefined) {
|
|
78
|
+
argv.push(...codexEffortArgv(spec.effort));
|
|
79
|
+
note(notes, 'effort', 'native', `model_reasoning_effort="${spec.effort}"`);
|
|
80
|
+
}
|
|
81
|
+
if (spec.mcp?.servers !== undefined) {
|
|
82
|
+
for (const [name, server] of Object.entries(spec.mcp.servers)) {
|
|
83
|
+
for (const [key, value] of Object.entries(server)) {
|
|
84
|
+
if (value === null || value === undefined)
|
|
85
|
+
continue;
|
|
86
|
+
argv.push('-c', `mcp_servers.${name}.${key}=${toToml(value)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
note(notes, 'mcp.servers', 'approximated', 'rendered as -c mcp_servers.* config overrides');
|
|
90
|
+
if (spec.mcp.strict === true) {
|
|
91
|
+
note(notes, 'mcp.strict', 'dropped', 'codex has no ignore-other-servers flag; overrides merge with config.toml');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const permissions = spec.permissions;
|
|
95
|
+
applyPermissionMode(argv, notes, permissions?.mode, {
|
|
96
|
+
'read-only': {
|
|
97
|
+
argv: ['--sandbox', 'read-only'],
|
|
98
|
+
support: 'native',
|
|
99
|
+
detail: '--sandbox read-only',
|
|
100
|
+
},
|
|
101
|
+
edits: {
|
|
102
|
+
argv: ['--sandbox', 'workspace-write'],
|
|
103
|
+
support: 'approximated',
|
|
104
|
+
detail: 'edits mapped to --sandbox workspace-write (also permits workspace commands)',
|
|
105
|
+
},
|
|
106
|
+
'full-auto': {
|
|
107
|
+
argv: ['--sandbox', 'workspace-write'],
|
|
108
|
+
support: 'native',
|
|
109
|
+
detail: '--sandbox workspace-write',
|
|
110
|
+
},
|
|
111
|
+
bypass: {
|
|
112
|
+
argv: ['--dangerously-bypass-approvals-and-sandbox'],
|
|
113
|
+
support: 'native',
|
|
114
|
+
detail: '--dangerously-bypass-approvals-and-sandbox',
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
if ((permissions?.allowTools ?? []).length > 0) {
|
|
118
|
+
note(notes, 'permissions.allowTools', 'dropped', 'codex has no per-tool allow rules; use permissions.mode');
|
|
119
|
+
}
|
|
120
|
+
if ((permissions?.denyTools ?? []).length > 0) {
|
|
121
|
+
note(notes, 'permissions.denyTools', 'dropped', 'codex has no per-tool deny rules; use permissions.mode');
|
|
122
|
+
}
|
|
123
|
+
for (const dir of spec.context?.dirs ?? []) {
|
|
124
|
+
argv.push('--add-dir', dir);
|
|
125
|
+
}
|
|
126
|
+
if ((spec.context?.dirs ?? []).length > 0) {
|
|
127
|
+
note(notes, 'context.dirs', 'native', '--add-dir');
|
|
128
|
+
}
|
|
129
|
+
if (spec.output?.schema !== undefined) {
|
|
130
|
+
tempFiles.push({
|
|
131
|
+
name: SCHEMA_FILE,
|
|
132
|
+
content: JSON.stringify(spec.output.schema),
|
|
133
|
+
});
|
|
134
|
+
argv.push('--output-schema', `${TMP_TOKEN}/${SCHEMA_FILE}`);
|
|
135
|
+
note(notes, 'output.schema', 'native', '--output-schema');
|
|
136
|
+
}
|
|
137
|
+
if (spec.plugins !== undefined) {
|
|
138
|
+
note(notes, 'plugins', 'dropped', 'session-only plugins are claude only');
|
|
139
|
+
}
|
|
140
|
+
if (spec.budgetUsd !== undefined) {
|
|
141
|
+
note(notes, 'budgetUsd', 'dropped', 'codex has no budget cap flag');
|
|
142
|
+
}
|
|
143
|
+
argv.push(...(spec.extraArgs ?? []));
|
|
144
|
+
const prompt = buildEffectivePrompt(spec, { schemaNative: true }, notes);
|
|
145
|
+
// `-` reads the prompt from stdin — avoids argv length limits.
|
|
146
|
+
argv.push('-');
|
|
147
|
+
return {
|
|
148
|
+
agent: 'codex',
|
|
149
|
+
argv,
|
|
150
|
+
stdin: prompt,
|
|
151
|
+
env: { ...spec.env },
|
|
152
|
+
cwd: spec.cwd,
|
|
153
|
+
outputFormat: format,
|
|
154
|
+
tempFiles,
|
|
155
|
+
notes,
|
|
156
|
+
spec,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** Render a JSON value as a TOML literal for `-c key=value` overrides. */
|
|
160
|
+
function toToml(value) {
|
|
161
|
+
if (typeof value === 'string')
|
|
162
|
+
return JSON.stringify(value);
|
|
163
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
164
|
+
return String(value);
|
|
165
|
+
}
|
|
166
|
+
if (Array.isArray(value)) {
|
|
167
|
+
return `[${value.map(toToml).join(', ')}]`;
|
|
168
|
+
}
|
|
169
|
+
if (value !== null && typeof value === 'object') {
|
|
170
|
+
const entries = Object.entries(value)
|
|
171
|
+
.map(([k, v]) => `${k} = ${toToml(v)}`)
|
|
172
|
+
.join(', ');
|
|
173
|
+
return `{${entries}}`;
|
|
174
|
+
}
|
|
175
|
+
return JSON.stringify(value);
|
|
176
|
+
}
|
|
177
|
+
function parseCodex(planned, raw) {
|
|
178
|
+
const lastMessage = raw.files?.[LAST_MESSAGE_FILE]?.trim();
|
|
179
|
+
if (planned.outputFormat === 'text') {
|
|
180
|
+
const text = lastMessage !== undefined && lastMessage !== '' ? lastMessage : raw.stdout.trim();
|
|
181
|
+
return withStructured(planned, { text });
|
|
182
|
+
}
|
|
183
|
+
const events = parseJsonLines(raw.stdout);
|
|
184
|
+
let text = lastMessage ?? '';
|
|
185
|
+
let answered = lastMessage !== undefined && lastMessage !== '';
|
|
186
|
+
let sessionId;
|
|
187
|
+
let usage;
|
|
188
|
+
let errorDetail;
|
|
189
|
+
let turnFailed = false;
|
|
190
|
+
for (const event of events) {
|
|
191
|
+
const type = get(event, 'type');
|
|
192
|
+
if (type === 'thread.started') {
|
|
193
|
+
sessionId = asString(get(event, 'thread_id'));
|
|
194
|
+
}
|
|
195
|
+
else if (type === 'turn.completed') {
|
|
196
|
+
usage = codexUsage(get(event, 'usage'));
|
|
197
|
+
}
|
|
198
|
+
else if (type === 'turn.failed') {
|
|
199
|
+
turnFailed = true;
|
|
200
|
+
errorDetail = asString(get(event, 'error', 'message')) ?? errorDetail ?? 'turn failed';
|
|
201
|
+
}
|
|
202
|
+
else if (type === 'item.completed') {
|
|
203
|
+
const item = get(event, 'item');
|
|
204
|
+
const itemType = get(item, 'item_type') ?? get(item, 'type');
|
|
205
|
+
if (itemType === 'agent_message') {
|
|
206
|
+
const itemText = asString(get(item, 'text'));
|
|
207
|
+
if (itemText !== undefined) {
|
|
208
|
+
text = itemText;
|
|
209
|
+
answered = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else if (itemType === 'error') {
|
|
213
|
+
errorDetail = asString(get(item, 'message'));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else if (type === 'error') {
|
|
217
|
+
errorDetail = asString(get(event, 'message')) ?? 'codex reported an error';
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// Legacy event shape: {"id": ..., "msg": {"type": "agent_message", ...}}
|
|
221
|
+
const msgType = get(event, 'msg', 'type');
|
|
222
|
+
if (msgType === 'agent_message') {
|
|
223
|
+
const legacyText = asString(get(event, 'msg', 'message'));
|
|
224
|
+
if (legacyText !== undefined) {
|
|
225
|
+
text = legacyText;
|
|
226
|
+
answered = true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (msgType === 'token_count') {
|
|
230
|
+
usage = codexUsage(get(event, 'msg', 'info', 'total_token_usage')) ?? usage;
|
|
231
|
+
}
|
|
232
|
+
else if (msgType === 'error') {
|
|
233
|
+
errorDetail = asString(get(event, 'msg', 'message'));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// codex emits warning-level `error` items (e.g. config deprecations) on
|
|
238
|
+
// runs that still answer; only fail when the turn failed or nothing came back.
|
|
239
|
+
const isError = turnFailed || (errorDetail !== undefined && !answered);
|
|
240
|
+
return withStructured(planned, {
|
|
241
|
+
text,
|
|
242
|
+
sessionId,
|
|
243
|
+
usage,
|
|
244
|
+
events,
|
|
245
|
+
isError,
|
|
246
|
+
errorDetail,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
function withStructured(planned, parsed) {
|
|
250
|
+
if (planned.spec.output?.schema !== undefined && parsed.text !== '') {
|
|
251
|
+
parsed.structured = tryParseJson(parsed.text);
|
|
252
|
+
}
|
|
253
|
+
return parsed;
|
|
254
|
+
}
|
|
255
|
+
function codexUsage(usage) {
|
|
256
|
+
if (usage === null || typeof usage !== 'object')
|
|
257
|
+
return undefined;
|
|
258
|
+
return {
|
|
259
|
+
inputTokens: asNumber(get(usage, 'input_tokens')),
|
|
260
|
+
outputTokens: asNumber(get(usage, 'output_tokens')),
|
|
261
|
+
cacheReadTokens: asNumber(get(usage, 'cached_input_tokens')),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function codexConversation(planned, raw) {
|
|
265
|
+
const parsed = parseCodex(planned, raw);
|
|
266
|
+
const messages = [
|
|
267
|
+
{ role: 'user', parts: [{ type: 'text', text: planned.stdin ?? '' }] },
|
|
268
|
+
];
|
|
269
|
+
for (const event of parsed.events ?? []) {
|
|
270
|
+
if (get(event, 'type') !== 'item.completed')
|
|
271
|
+
continue;
|
|
272
|
+
const item = get(event, 'item');
|
|
273
|
+
const itemType = get(item, 'item_type') ?? get(item, 'type');
|
|
274
|
+
if (itemType === 'agent_message') {
|
|
275
|
+
const text = asString(get(item, 'text'));
|
|
276
|
+
if (text !== undefined) {
|
|
277
|
+
messages.push({ role: 'assistant', parts: [{ type: 'text', text }] });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else if (itemType === 'reasoning') {
|
|
281
|
+
const text = asString(get(item, 'text'));
|
|
282
|
+
if (text !== undefined) {
|
|
283
|
+
messages.push({ role: 'assistant', parts: [{ type: 'reasoning', text }] });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else if (itemType === 'command_execution') {
|
|
287
|
+
const callId = asString(get(item, 'id'));
|
|
288
|
+
messages.push({
|
|
289
|
+
role: 'assistant',
|
|
290
|
+
parts: [
|
|
291
|
+
{
|
|
292
|
+
type: 'tool-call',
|
|
293
|
+
callId,
|
|
294
|
+
name: 'shell',
|
|
295
|
+
input: { command: get(item, 'command') },
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
});
|
|
299
|
+
messages.push({
|
|
300
|
+
role: 'tool',
|
|
301
|
+
parts: [
|
|
302
|
+
{
|
|
303
|
+
type: 'tool-result',
|
|
304
|
+
callId,
|
|
305
|
+
name: 'shell',
|
|
306
|
+
output: {
|
|
307
|
+
output: get(item, 'aggregated_output'),
|
|
308
|
+
exitCode: get(item, 'exit_code'),
|
|
309
|
+
},
|
|
310
|
+
isError: asNumber(get(item, 'exit_code')) !== 0,
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
else if (itemType === 'file_change') {
|
|
316
|
+
messages.push({
|
|
317
|
+
role: 'assistant',
|
|
318
|
+
parts: [
|
|
319
|
+
{
|
|
320
|
+
type: 'tool-call',
|
|
321
|
+
callId: asString(get(item, 'id')),
|
|
322
|
+
name: 'apply_patch',
|
|
323
|
+
input: { changes: get(item, 'changes') },
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
else if (itemType === 'mcp_tool_call') {
|
|
329
|
+
messages.push({
|
|
330
|
+
role: 'assistant',
|
|
331
|
+
parts: [
|
|
332
|
+
{
|
|
333
|
+
type: 'tool-call',
|
|
334
|
+
callId: asString(get(item, 'id')),
|
|
335
|
+
name: asString(get(item, 'tool')) ?? 'mcp',
|
|
336
|
+
input: get(item, 'arguments'),
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
else if (itemType === 'web_search') {
|
|
342
|
+
messages.push({
|
|
343
|
+
role: 'assistant',
|
|
344
|
+
parts: [
|
|
345
|
+
{
|
|
346
|
+
type: 'tool-call',
|
|
347
|
+
callId: asString(get(item, 'id')),
|
|
348
|
+
name: 'web_search',
|
|
349
|
+
input: { query: get(item, 'query') },
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// No events (text format): still record the final answer.
|
|
356
|
+
if (messages.length === 1 && parsed.text !== '') {
|
|
357
|
+
messages.push({
|
|
358
|
+
role: 'assistant',
|
|
359
|
+
parts: [{ type: 'text', text: parsed.text }],
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
schema: CONVERSATION_SCHEMA,
|
|
364
|
+
agent: 'codex',
|
|
365
|
+
sessionId: parsed.sessionId,
|
|
366
|
+
messages,
|
|
367
|
+
usage: parsed.usage,
|
|
368
|
+
stopReason: parsed.isError === true ? 'error' : undefined,
|
|
369
|
+
raw: {
|
|
370
|
+
format: 'codex/jsonl',
|
|
371
|
+
data: (parsed.events?.length ?? 0) > 0 ? parsed.events : raw.stdout,
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AgentCallSpec } from '../spec.js';
|
|
2
|
+
import type { AgentAdapter, ParsedOutput } from './types.js';
|
|
3
|
+
/** Options for {@link customAdapter}. */
|
|
4
|
+
export interface CustomAdapterOptions {
|
|
5
|
+
/** Adapter name, reported as `PlannedCall.agent`. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Binary to spawn. */
|
|
8
|
+
binary: string;
|
|
9
|
+
/**
|
|
10
|
+
* Build the args (after the binary) for a spec. The effective prompt —
|
|
11
|
+
* spec.prompt with context/system/schema folded in — is provided so simple
|
|
12
|
+
* CLIs get the emulation layer for free. Place it in the returned args, or
|
|
13
|
+
* set `promptVia: 'stdin'` to send it on stdin instead. Whatever `args`
|
|
14
|
+
* throws propagates out of planning.
|
|
15
|
+
*/
|
|
16
|
+
args: (spec: AgentCallSpec, effectivePrompt: string) => string[];
|
|
17
|
+
/** How the prompt travels. Default: `"argv"` (your `args` must place it). */
|
|
18
|
+
promptVia?: 'argv' | 'stdin';
|
|
19
|
+
/**
|
|
20
|
+
* Optional custom parse; default returns trimmed stdout as text. If it
|
|
21
|
+
* throws, `callAgent` resolves `ok: false` with reason `agent-error`.
|
|
22
|
+
*/
|
|
23
|
+
parse?: (stdout: string, stderr: string) => ParsedOutput;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Escape hatch for agents this package has no adapter for: aider, goose,
|
|
27
|
+
* anything with a one-shot CLI. Context, system prompt and schema are folded
|
|
28
|
+
* into the prompt, and `permissions.mode` is noted as dropped. Any other spec
|
|
29
|
+
* field is the `args` builder's to handle; it is not noted if left out.
|
|
30
|
+
*/
|
|
31
|
+
export declare function customAdapter(options: CustomAdapterOptions): AgentAdapter;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { note } from '../plan.js';
|
|
2
|
+
import { buildEffectivePrompt } from './shared.js';
|
|
3
|
+
/**
|
|
4
|
+
* Escape hatch for agents this package has no adapter for: aider, goose,
|
|
5
|
+
* anything with a one-shot CLI. Context, system prompt and schema are folded
|
|
6
|
+
* into the prompt, and `permissions.mode` is noted as dropped. Any other spec
|
|
7
|
+
* field is the `args` builder's to handle; it is not noted if left out.
|
|
8
|
+
*/
|
|
9
|
+
export function customAdapter(options) {
|
|
10
|
+
return {
|
|
11
|
+
name: options.name,
|
|
12
|
+
binary: options.binary,
|
|
13
|
+
plan(spec) {
|
|
14
|
+
const notes = [];
|
|
15
|
+
const effectivePrompt = buildEffectivePrompt(spec, {}, notes);
|
|
16
|
+
if (spec.permissions?.mode !== undefined) {
|
|
17
|
+
note(notes, 'permissions.mode', 'dropped', `custom adapter "${options.name}" defines no permission mapping`);
|
|
18
|
+
}
|
|
19
|
+
const argv = [
|
|
20
|
+
options.binary,
|
|
21
|
+
...options.args(spec, effectivePrompt),
|
|
22
|
+
...(spec.extraArgs ?? []),
|
|
23
|
+
];
|
|
24
|
+
return {
|
|
25
|
+
agent: options.name,
|
|
26
|
+
argv,
|
|
27
|
+
stdin: options.promptVia === 'stdin' ? effectivePrompt : undefined,
|
|
28
|
+
env: { ...spec.env },
|
|
29
|
+
cwd: spec.cwd,
|
|
30
|
+
outputFormat: spec.output?.format ?? 'text',
|
|
31
|
+
tempFiles: [],
|
|
32
|
+
notes,
|
|
33
|
+
spec,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
parseResult(_planned, raw) {
|
|
37
|
+
if (options.parse)
|
|
38
|
+
return options.parse(raw.stdout, raw.stderr);
|
|
39
|
+
return { text: raw.stdout.trim() };
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AgentAdapter } from './types.js';
|
|
2
|
+
/** Options for {@link opencodeAdapter}. */
|
|
3
|
+
export interface OpencodeAdapterOptions {
|
|
4
|
+
/** Binary to spawn. Default: the CLI's own name. */
|
|
5
|
+
binary?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* opencode (`opencode run`). Context files attach natively (`-f`);
|
|
9
|
+
* permissions travel via the OPENCODE_PERMISSION env JSON; `--format json`
|
|
10
|
+
* is a raw event stream backing both json and stream-json requests.
|
|
11
|
+
*/
|
|
12
|
+
export declare function opencodeAdapter(options?: OpencodeAdapterOptions): AgentAdapter;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { CONVERSATION_SCHEMA, } from '../conversation.js';
|
|
2
|
+
import { note } from '../plan.js';
|
|
3
|
+
import { asString, buildEffectivePrompt, get, parseJsonLines, tryParseJson } from './shared.js';
|
|
4
|
+
/**
|
|
5
|
+
* opencode (`opencode run`). Context files attach natively (`-f`);
|
|
6
|
+
* permissions travel via the OPENCODE_PERMISSION env JSON; `--format json`
|
|
7
|
+
* is a raw event stream backing both json and stream-json requests.
|
|
8
|
+
*/
|
|
9
|
+
export function opencodeAdapter(options = {}) {
|
|
10
|
+
const binary = options.binary ?? 'opencode';
|
|
11
|
+
return {
|
|
12
|
+
name: 'opencode',
|
|
13
|
+
binary,
|
|
14
|
+
plan(spec) {
|
|
15
|
+
return planOpencode(binary, spec);
|
|
16
|
+
},
|
|
17
|
+
parseResult(planned, raw) {
|
|
18
|
+
return parseOpencode(planned, raw);
|
|
19
|
+
},
|
|
20
|
+
toConversation(planned, raw) {
|
|
21
|
+
return opencodeConversation(planned, raw);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const PERMISSION_ENV = {
|
|
26
|
+
'read-only': { edit: 'deny', bash: 'deny', webfetch: 'allow' },
|
|
27
|
+
edits: { edit: 'allow', bash: 'deny', webfetch: 'allow' },
|
|
28
|
+
'full-auto': { edit: 'allow', bash: 'allow', webfetch: 'allow' },
|
|
29
|
+
bypass: { edit: 'allow', bash: 'allow', webfetch: 'allow' },
|
|
30
|
+
};
|
|
31
|
+
function planOpencode(binary, spec) {
|
|
32
|
+
const notes = [];
|
|
33
|
+
const env = { ...spec.env };
|
|
34
|
+
const argv = [binary, 'run'];
|
|
35
|
+
const format = spec.output?.format ?? 'json';
|
|
36
|
+
if (format !== 'text') {
|
|
37
|
+
argv.push('--format', 'json');
|
|
38
|
+
if (format === 'json') {
|
|
39
|
+
note(notes, 'output.format', 'approximated', 'opencode emits raw JSON events; aggregated into a single result');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (spec.model !== undefined) {
|
|
43
|
+
argv.push('--model', spec.model);
|
|
44
|
+
if (!spec.model.includes('/')) {
|
|
45
|
+
note(notes, 'model', 'approximated', 'opencode expects "provider/model"; passed through as given');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (spec.effort !== undefined) {
|
|
49
|
+
argv.push('--variant', spec.effort);
|
|
50
|
+
note(notes, 'effort', 'approximated', '--variant (provider-specific reasoning effort; values depend on the model)');
|
|
51
|
+
}
|
|
52
|
+
if (spec.mcp?.servers !== undefined) {
|
|
53
|
+
note(notes, 'mcp.servers', 'dropped', 'opencode takes MCP servers from opencode.json, not a flag');
|
|
54
|
+
}
|
|
55
|
+
const permissions = spec.permissions;
|
|
56
|
+
if (permissions?.mode !== undefined && permissions.mode !== 'default') {
|
|
57
|
+
env['OPENCODE_PERMISSION'] = JSON.stringify(PERMISSION_ENV[permissions.mode]);
|
|
58
|
+
note(notes, 'permissions.mode', 'approximated', `mapped to OPENCODE_PERMISSION=${env['OPENCODE_PERMISSION']} ("ask" would hang a headless run, so gated tools are denied)`);
|
|
59
|
+
}
|
|
60
|
+
if ((permissions?.allowTools ?? []).length > 0) {
|
|
61
|
+
note(notes, 'permissions.allowTools', 'dropped', 'no CLI flag; use permission patterns in opencode.json');
|
|
62
|
+
}
|
|
63
|
+
if ((permissions?.denyTools ?? []).length > 0) {
|
|
64
|
+
note(notes, 'permissions.denyTools', 'dropped', 'no CLI flag; use permission patterns in opencode.json');
|
|
65
|
+
}
|
|
66
|
+
for (const file of spec.context?.files ?? []) {
|
|
67
|
+
argv.push('--file', file);
|
|
68
|
+
}
|
|
69
|
+
if ((spec.context?.files ?? []).length > 0) {
|
|
70
|
+
note(notes, 'context.files', 'native', '--file attachments');
|
|
71
|
+
}
|
|
72
|
+
if ((spec.context?.dirs ?? []).length > 0) {
|
|
73
|
+
note(notes, 'context.dirs', 'dropped', 'opencode scopes to its working directory; no add-dir flag');
|
|
74
|
+
}
|
|
75
|
+
const session = spec.session;
|
|
76
|
+
if (session?.resume !== undefined) {
|
|
77
|
+
argv.push('--session', session.resume);
|
|
78
|
+
note(notes, 'session.resume', 'native', '--session');
|
|
79
|
+
}
|
|
80
|
+
else if (session?.continueLast === true) {
|
|
81
|
+
argv.push('--continue');
|
|
82
|
+
note(notes, 'session.continueLast', 'native', '--continue');
|
|
83
|
+
}
|
|
84
|
+
if (session?.id !== undefined) {
|
|
85
|
+
note(notes, 'session.id', 'dropped', 'opencode assigns session ids itself');
|
|
86
|
+
}
|
|
87
|
+
if (session?.persist === false) {
|
|
88
|
+
note(notes, 'session.persist', 'dropped', 'opencode always persists sessions');
|
|
89
|
+
}
|
|
90
|
+
if (spec.plugins !== undefined) {
|
|
91
|
+
note(notes, 'plugins', 'dropped', 'session-only plugins are claude only');
|
|
92
|
+
}
|
|
93
|
+
if (spec.budgetUsd !== undefined) {
|
|
94
|
+
note(notes, 'budgetUsd', 'dropped', 'opencode has no budget cap flag');
|
|
95
|
+
}
|
|
96
|
+
argv.push(...(spec.extraArgs ?? []));
|
|
97
|
+
const prompt = buildEffectivePrompt(spec, { filesNative: true }, notes);
|
|
98
|
+
argv.push(prompt);
|
|
99
|
+
return {
|
|
100
|
+
agent: 'opencode',
|
|
101
|
+
argv,
|
|
102
|
+
env,
|
|
103
|
+
cwd: spec.cwd,
|
|
104
|
+
prompt,
|
|
105
|
+
outputFormat: format,
|
|
106
|
+
tempFiles: [],
|
|
107
|
+
notes,
|
|
108
|
+
spec,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function parseOpencode(planned, raw) {
|
|
112
|
+
if (planned.outputFormat === 'text') {
|
|
113
|
+
const parsed = { text: raw.stdout.trim() };
|
|
114
|
+
if (planned.spec.output?.schema !== undefined) {
|
|
115
|
+
parsed.structured = tryParseJson(parsed.text);
|
|
116
|
+
}
|
|
117
|
+
return parsed;
|
|
118
|
+
}
|
|
119
|
+
const events = parseJsonLines(raw.stdout);
|
|
120
|
+
const textByPart = new Map();
|
|
121
|
+
let sessionId;
|
|
122
|
+
let errorDetail;
|
|
123
|
+
let order = 0;
|
|
124
|
+
for (const event of events) {
|
|
125
|
+
// Both shapes seen in the wild: a bare part object, or an event wrapping
|
|
126
|
+
// one at `properties.part` / `part`.
|
|
127
|
+
const part = get(event, 'properties', 'part') ?? get(event, 'part') ?? event;
|
|
128
|
+
const partType = get(part, 'type');
|
|
129
|
+
if (partType === 'text') {
|
|
130
|
+
const text = asString(get(part, 'text'));
|
|
131
|
+
const id = asString(get(part, 'id')) ?? `#${order++}`;
|
|
132
|
+
if (text !== undefined)
|
|
133
|
+
textByPart.set(id, text);
|
|
134
|
+
sessionId = asString(get(part, 'sessionID')) ?? asString(get(part, 'sessionId')) ?? sessionId;
|
|
135
|
+
}
|
|
136
|
+
else if (partType === 'error' || get(event, 'type') === 'error') {
|
|
137
|
+
errorDetail =
|
|
138
|
+
asString(get(part, 'message')) ??
|
|
139
|
+
asString(get(event, 'properties', 'message')) ??
|
|
140
|
+
'opencode reported an error';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const text = [...textByPart.values()].join('\n').trim();
|
|
144
|
+
const parsed = {
|
|
145
|
+
text,
|
|
146
|
+
sessionId,
|
|
147
|
+
events,
|
|
148
|
+
isError: errorDetail !== undefined,
|
|
149
|
+
errorDetail,
|
|
150
|
+
};
|
|
151
|
+
if (planned.spec.output?.schema !== undefined && text !== '') {
|
|
152
|
+
parsed.structured = tryParseJson(text);
|
|
153
|
+
}
|
|
154
|
+
return parsed;
|
|
155
|
+
}
|
|
156
|
+
function opencodeConversation(planned, raw) {
|
|
157
|
+
const parsed = parseOpencode(planned, raw);
|
|
158
|
+
const prompt = planned.prompt ?? '';
|
|
159
|
+
const messages = [
|
|
160
|
+
{ role: 'user', parts: [{ type: 'text', text: prompt }] },
|
|
161
|
+
];
|
|
162
|
+
const assistantParts = [];
|
|
163
|
+
for (const event of parsed.events ?? []) {
|
|
164
|
+
const part = get(event, 'properties', 'part') ?? get(event, 'part') ?? event;
|
|
165
|
+
const partType = get(part, 'type');
|
|
166
|
+
if (partType === 'tool') {
|
|
167
|
+
assistantParts.push({
|
|
168
|
+
type: 'tool-call',
|
|
169
|
+
callId: asString(get(part, 'callID')) ?? asString(get(part, 'id')),
|
|
170
|
+
name: asString(get(part, 'tool')) ?? 'unknown',
|
|
171
|
+
input: get(part, 'state', 'input'),
|
|
172
|
+
});
|
|
173
|
+
const output = get(part, 'state', 'output');
|
|
174
|
+
if (output !== undefined) {
|
|
175
|
+
assistantParts.push({
|
|
176
|
+
type: 'tool-result',
|
|
177
|
+
callId: asString(get(part, 'callID')) ?? asString(get(part, 'id')),
|
|
178
|
+
name: asString(get(part, 'tool')) ?? undefined,
|
|
179
|
+
output,
|
|
180
|
+
isError: get(part, 'state', 'status') === 'error',
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else if (partType === 'reasoning') {
|
|
185
|
+
const text = asString(get(part, 'text'));
|
|
186
|
+
if (text !== undefined)
|
|
187
|
+
assistantParts.push({ type: 'reasoning', text });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (parsed.text !== '') {
|
|
191
|
+
assistantParts.push({ type: 'text', text: parsed.text });
|
|
192
|
+
}
|
|
193
|
+
if (assistantParts.length > 0) {
|
|
194
|
+
messages.push({ role: 'assistant', parts: assistantParts });
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
schema: CONVERSATION_SCHEMA,
|
|
198
|
+
agent: 'opencode',
|
|
199
|
+
sessionId: parsed.sessionId,
|
|
200
|
+
messages,
|
|
201
|
+
stopReason: parsed.isError === true ? 'error' : undefined,
|
|
202
|
+
raw: {
|
|
203
|
+
format: 'opencode/json-events',
|
|
204
|
+
data: (parsed.events?.length ?? 0) > 0 ? parsed.events : raw.stdout,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|