remote-codex 0.1.10 → 0.11.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/apps/supervisor-api/dist/index.js +11159 -27875
- package/apps/supervisor-web/dist/assets/{highlighted-body-OFNGDK62-CyMcatlD.js → highlighted-body-OFNGDK62-ChrwAL9u.js} +1 -1
- package/apps/supervisor-web/dist/assets/index-DHf2HOXx.js +381 -0
- package/apps/supervisor-web/dist/assets/index-DpWxXCgt.css +32 -0
- package/apps/supervisor-web/dist/assets/{xterm-DbYWMNQ0.js → xterm-D4sevve4.js} +1 -1
- package/apps/supervisor-web/dist/index.html +2 -2
- package/package.json +2 -3
- package/packages/agent-runtime/src/index.ts +4 -0
- package/packages/agent-runtime/src/management-errors.ts +11 -0
- package/packages/agent-runtime/src/model-pricing.ts +312 -0
- package/packages/agent-runtime/src/registry.ts +19 -4
- package/packages/agent-runtime/src/runtime-errors.ts +97 -0
- package/packages/agent-runtime/src/types.ts +36 -3
- package/packages/agent-runtime/src/unavailable-runtime.ts +169 -0
- package/packages/claude/src/runtimeAdapter.test.ts +95 -6
- package/packages/claude/src/runtimeAdapter.ts +421 -65
- package/packages/codex/src/historyItems.test.ts +110 -0
- package/packages/codex/src/historyItems.ts +96 -15
- package/packages/codex/src/hookHistory.test.ts +59 -0
- package/packages/codex/src/index.ts +7 -0
- package/packages/codex/src/local-session-store.ts +390 -0
- package/packages/codex/src/management/codex-management-service.ts +454 -0
- package/packages/codex/src/management/codexHostConfig.test.ts +88 -0
- package/packages/codex/src/management/codexHostConfig.ts +188 -0
- package/packages/codex/src/management/errors.ts +20 -0
- package/packages/codex/src/modelPricing.test.ts +184 -0
- package/packages/codex/src/modelPricing.ts +9 -0
- package/packages/codex/src/runtime-errors.test.ts +72 -0
- package/packages/codex/src/runtime-errors.ts +37 -0
- package/packages/codex/src/runtimeAdapter.ts +15 -0
- package/packages/codex/src/thread-title.ts +1 -0
- package/packages/opencode/src/historyItems.test.ts +504 -0
- package/packages/opencode/src/historyItems.ts +896 -0
- package/packages/opencode/src/index.ts +2 -0
- package/packages/opencode/src/runtimeAdapter.test.ts +1355 -0
- package/packages/opencode/src/runtimeAdapter.ts +1469 -0
- package/packages/shared/src/agent-providers.ts +56 -0
- package/packages/shared/src/index.ts +170 -35
- package/apps/supervisor-web/dist/assets/index-BlAhoIuq.js +0 -379
- package/apps/supervisor-web/dist/assets/index-DI0NRNgr.css +0 -32
|
@@ -0,0 +1,1469 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
AgentHistoryItem,
|
|
12
|
+
AgentModel,
|
|
13
|
+
AgentProviderCapabilities,
|
|
14
|
+
AgentRuntime,
|
|
15
|
+
AgentRuntimeEvent,
|
|
16
|
+
AgentRuntimeManagementSchema,
|
|
17
|
+
AgentRuntimeStatus,
|
|
18
|
+
AgentSessionDetail,
|
|
19
|
+
AgentSessionSummary,
|
|
20
|
+
AgentTurn,
|
|
21
|
+
InterruptAgentTurnInput,
|
|
22
|
+
ReadAgentSessionOptions,
|
|
23
|
+
ResumeAgentSessionInput,
|
|
24
|
+
StartAgentSessionInput,
|
|
25
|
+
StartAgentSessionResult,
|
|
26
|
+
StartAgentTurnInput,
|
|
27
|
+
} from '../../agent-runtime/src/index';
|
|
28
|
+
import type {
|
|
29
|
+
AgentBackendInstallationDto,
|
|
30
|
+
} from '../../shared/src/index';
|
|
31
|
+
import {
|
|
32
|
+
AgentRuntimeError,
|
|
33
|
+
contextWindowForModel,
|
|
34
|
+
} from '../../agent-runtime/src/index';
|
|
35
|
+
import {
|
|
36
|
+
openCodeMessagesToTurns,
|
|
37
|
+
openCodeMessageToHistoryItems,
|
|
38
|
+
openCodeMessagesToPlanUpdate,
|
|
39
|
+
} from './historyItems';
|
|
40
|
+
|
|
41
|
+
const execFileAsync = promisify(execFile);
|
|
42
|
+
const openCodeWaitTimeoutMs = 1_500;
|
|
43
|
+
const openCodePromptPollIntervalMs = 500;
|
|
44
|
+
const openCodePromptTimeoutMs = 120_000;
|
|
45
|
+
|
|
46
|
+
interface OpenCodeSdkModule {
|
|
47
|
+
createOpencode(options?: unknown): Promise<{
|
|
48
|
+
client: OpenCodeClient;
|
|
49
|
+
server: {
|
|
50
|
+
url: string;
|
|
51
|
+
close(): void;
|
|
52
|
+
};
|
|
53
|
+
}>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface OpenCodeClient {
|
|
57
|
+
config?: {
|
|
58
|
+
get(parameters?: unknown, options?: unknown): Promise<OpenCodeResult<unknown>>;
|
|
59
|
+
providers(parameters?: unknown, options?: unknown): Promise<OpenCodeResult<unknown>>;
|
|
60
|
+
};
|
|
61
|
+
v2?: {
|
|
62
|
+
session?: {
|
|
63
|
+
messages(parameters: unknown, options?: unknown): Promise<OpenCodeResult<{ items?: unknown[] } | unknown[]>>;
|
|
64
|
+
prompt?(parameters: unknown, options?: unknown): Promise<OpenCodeResult<unknown>>;
|
|
65
|
+
wait?(parameters: unknown, options?: unknown): Promise<OpenCodeResult<unknown>>;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
model?: {
|
|
69
|
+
list(parameters?: unknown, options?: unknown): Promise<OpenCodeResult<unknown[]>>;
|
|
70
|
+
};
|
|
71
|
+
provider?: {
|
|
72
|
+
list(parameters?: unknown, options?: unknown): Promise<OpenCodeResult<unknown[]>>;
|
|
73
|
+
};
|
|
74
|
+
session: {
|
|
75
|
+
list(parameters?: unknown, options?: unknown): Promise<OpenCodeResult<{ items?: unknown[] } | unknown[]>>;
|
|
76
|
+
create(parameters?: unknown, options?: unknown): Promise<OpenCodeResult<unknown>>;
|
|
77
|
+
status?(parameters?: unknown, options?: unknown): Promise<OpenCodeResult<Record<string, unknown>>>;
|
|
78
|
+
get(parameters: unknown, options?: unknown): Promise<OpenCodeResult<unknown>>;
|
|
79
|
+
update?(parameters: unknown, options?: unknown): Promise<OpenCodeResult<unknown>>;
|
|
80
|
+
messages(parameters: unknown, options?: unknown): Promise<OpenCodeResult<{ items?: unknown[] } | unknown[]>>;
|
|
81
|
+
prompt(parameters: unknown, options?: unknown): Promise<OpenCodeResult<unknown>>;
|
|
82
|
+
wait?(parameters: unknown, options?: unknown): Promise<OpenCodeResult<unknown>>;
|
|
83
|
+
abort(parameters: unknown, options?: unknown): Promise<OpenCodeResult<unknown>>;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type OpenCodeResult<T> =
|
|
88
|
+
| T
|
|
89
|
+
| {
|
|
90
|
+
data?: T;
|
|
91
|
+
error?: unknown;
|
|
92
|
+
};
|
|
93
|
+
type OpenCodePromptInput = {
|
|
94
|
+
sessionID: string;
|
|
95
|
+
directory?: string | null | undefined;
|
|
96
|
+
model?: {
|
|
97
|
+
providerID: string;
|
|
98
|
+
modelID: string;
|
|
99
|
+
};
|
|
100
|
+
agent?: string;
|
|
101
|
+
variant?: string;
|
|
102
|
+
parts?: Array<{
|
|
103
|
+
type: 'text';
|
|
104
|
+
text: string;
|
|
105
|
+
}>;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
type OpenCodePermissionRule = {
|
|
109
|
+
permission: string;
|
|
110
|
+
pattern: string;
|
|
111
|
+
action: 'allow' | 'deny' | 'ask';
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
type OpenCodeLocationInput = {
|
|
115
|
+
sessionID?: string;
|
|
116
|
+
directory?: string | null | undefined;
|
|
117
|
+
workspace?: string | null | undefined;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export interface OpenCodeRuntimeAdapterOptions {
|
|
121
|
+
home: string;
|
|
122
|
+
command?: string;
|
|
123
|
+
clientInfo?: {
|
|
124
|
+
name: string;
|
|
125
|
+
title?: string;
|
|
126
|
+
version?: string;
|
|
127
|
+
};
|
|
128
|
+
sdk?: OpenCodeSdkModule;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const opencodeCapabilities: AgentProviderCapabilities = {
|
|
132
|
+
sessions: {
|
|
133
|
+
list: true,
|
|
134
|
+
read: true,
|
|
135
|
+
resume: true,
|
|
136
|
+
importLocal: false,
|
|
137
|
+
},
|
|
138
|
+
turns: {
|
|
139
|
+
start: true,
|
|
140
|
+
streamInput: false,
|
|
141
|
+
steer: false,
|
|
142
|
+
interrupt: true,
|
|
143
|
+
compact: true,
|
|
144
|
+
},
|
|
145
|
+
branching: {
|
|
146
|
+
fork: true,
|
|
147
|
+
hardRollback: false,
|
|
148
|
+
resumeAt: false,
|
|
149
|
+
rewindFiles: false,
|
|
150
|
+
},
|
|
151
|
+
controls: {
|
|
152
|
+
planMode: true,
|
|
153
|
+
permissionRequests: false,
|
|
154
|
+
sandboxMode: true,
|
|
155
|
+
performanceMode: false,
|
|
156
|
+
goals: false,
|
|
157
|
+
},
|
|
158
|
+
management: {
|
|
159
|
+
models: true,
|
|
160
|
+
mcpStatus: false,
|
|
161
|
+
skills: false,
|
|
162
|
+
hooks: false,
|
|
163
|
+
hookTrust: false,
|
|
164
|
+
hostConfigFiles: false,
|
|
165
|
+
providerSettings: false,
|
|
166
|
+
},
|
|
167
|
+
usage: {
|
|
168
|
+
contextWindow: true,
|
|
169
|
+
tokenUsage: true,
|
|
170
|
+
costUsd: true,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
175
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function stringValue(value: unknown): string | null {
|
|
179
|
+
return typeof value === 'string' && value.trim() ? value : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function numberValue(value: unknown): number | null {
|
|
183
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function nonNegativeNumberValue(value: unknown) {
|
|
187
|
+
const number = numberValue(value);
|
|
188
|
+
return number !== null && number >= 0 ? number : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isoFromMs(value: unknown) {
|
|
192
|
+
const time = numberValue(value);
|
|
193
|
+
return time === null ? null : new Date(time).toISOString();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function unwrapResult<T>(result: OpenCodeResult<T>): T {
|
|
197
|
+
if (isRecord(result) && 'error' in result && result.error !== undefined) {
|
|
198
|
+
const error = result.error;
|
|
199
|
+
const message = isRecord(error) && typeof error.message === 'string'
|
|
200
|
+
? error.message
|
|
201
|
+
: String(error);
|
|
202
|
+
throw new Error(message);
|
|
203
|
+
}
|
|
204
|
+
if (isRecord(result) && 'data' in result) {
|
|
205
|
+
return result.data as T;
|
|
206
|
+
}
|
|
207
|
+
return result as T;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function resultItems(result: { items?: unknown[] } | unknown[]) {
|
|
211
|
+
return Array.isArray(result) ? result : result.items ?? [];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function modelKey(providerID: string, modelID: string, variant?: string | null) {
|
|
215
|
+
return variant ? `${providerID}/${modelID}@${variant}` : `${providerID}/${modelID}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function providerModelKey(providerID: string, modelID: string) {
|
|
219
|
+
return `${providerID}/${modelID}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseModelKey(value: string | null | undefined) {
|
|
223
|
+
const [providerAndModel = '', variant] = (value ?? '').split('@', 2);
|
|
224
|
+
const [providerID, modelID] = providerAndModel.split('/', 2);
|
|
225
|
+
if (!providerID || !modelID) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
providerID,
|
|
230
|
+
modelID,
|
|
231
|
+
...(variant ? { variant } : {}),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function mapModel(record: unknown, index: number): AgentModel | null {
|
|
236
|
+
if (!isRecord(record)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const providerID = stringValue(record.providerID);
|
|
240
|
+
const id = stringValue(record.id);
|
|
241
|
+
if (!providerID || !id) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
const name = stringValue(record.name) ?? id;
|
|
245
|
+
const variants = Array.isArray(record.variants) ? record.variants : [];
|
|
246
|
+
const defaultVariant = isRecord(record.options) ? stringValue(record.options.variant) : null;
|
|
247
|
+
const variantIds = variants
|
|
248
|
+
.map((variant) => (isRecord(variant) ? stringValue(variant.id) : null))
|
|
249
|
+
.filter((variant): variant is string => Boolean(variant));
|
|
250
|
+
const firstVariant = defaultVariant ?? variantIds[0] ?? null;
|
|
251
|
+
const model = modelKey(providerID, id, firstVariant);
|
|
252
|
+
const enabled = record.enabled !== false;
|
|
253
|
+
return {
|
|
254
|
+
id: model,
|
|
255
|
+
model,
|
|
256
|
+
displayName: `${name} (${providerID})`,
|
|
257
|
+
description: `OpenCode ${providerID}/${id}${firstVariant ? ` variant ${firstVariant}` : ''}`,
|
|
258
|
+
isDefault: index === 0,
|
|
259
|
+
hidden: !enabled,
|
|
260
|
+
supportedReasoningEfforts: [],
|
|
261
|
+
defaultReasoningEffort: null,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function configuredProviderModelRecords(config: unknown) {
|
|
266
|
+
const data = isRecord(config) && isRecord(config.data) ? config.data : config;
|
|
267
|
+
const providerConfig = isRecord(data) && isRecord(data.provider)
|
|
268
|
+
? data.provider
|
|
269
|
+
: isRecord(data) && isRecord(data.providers)
|
|
270
|
+
? data.providers
|
|
271
|
+
: null;
|
|
272
|
+
const configured = new Map<string, Record<string, unknown>>();
|
|
273
|
+
if (!providerConfig) {
|
|
274
|
+
return configured;
|
|
275
|
+
}
|
|
276
|
+
Object.entries(providerConfig).forEach(([providerID, provider]) => {
|
|
277
|
+
if (!isRecord(provider) || !isRecord(provider.models)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
Object.entries(provider.models).forEach(([modelID, model]) => {
|
|
281
|
+
configured.set(providerModelKey(providerID, modelID), isRecord(model) ? model : {});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
return configured;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function mapProviderModel(
|
|
288
|
+
provider: unknown,
|
|
289
|
+
record: unknown,
|
|
290
|
+
index: number,
|
|
291
|
+
configuredRecord?: Record<string, unknown>,
|
|
292
|
+
): AgentModel | null {
|
|
293
|
+
if (!isRecord(provider) || !isRecord(record)) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const providerID = stringValue(record.providerID) ?? stringValue(provider.id);
|
|
297
|
+
const id = stringValue(record.id);
|
|
298
|
+
if (!providerID || !id) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const name = stringValue(configuredRecord?.name) ?? stringValue(record.name) ?? id;
|
|
302
|
+
const variants = configuredRecord
|
|
303
|
+
? isRecord(configuredRecord.variants)
|
|
304
|
+
? Object.keys(configuredRecord.variants)
|
|
305
|
+
: []
|
|
306
|
+
: isRecord(record.variants)
|
|
307
|
+
? Object.keys(record.variants)
|
|
308
|
+
: [];
|
|
309
|
+
const model = providerModelKey(providerID, id);
|
|
310
|
+
const providerName = stringValue(provider.name) ?? providerID;
|
|
311
|
+
const disabled = configuredRecord?.status === 'disabled' || record.status === 'disabled' || provider.disabled === true;
|
|
312
|
+
const reasoningEfforts = variants
|
|
313
|
+
.map((variant) => ({
|
|
314
|
+
reasoningEffort: variant,
|
|
315
|
+
description: variant === 'none'
|
|
316
|
+
? 'No reasoning'
|
|
317
|
+
: variant === 'xhigh'
|
|
318
|
+
? 'Maximum reasoning'
|
|
319
|
+
: `${variant[0]?.toUpperCase() ?? ''}${variant.slice(1)} reasoning`,
|
|
320
|
+
}));
|
|
321
|
+
return {
|
|
322
|
+
id: model,
|
|
323
|
+
model,
|
|
324
|
+
displayName: `${name} (${providerName})`,
|
|
325
|
+
description: variants.length > 0
|
|
326
|
+
? `OpenCode ${providerID}/${id} variants ${variants.join(', ')}`
|
|
327
|
+
: `OpenCode ${providerID}/${id}`,
|
|
328
|
+
isDefault: index === 0,
|
|
329
|
+
hidden: disabled,
|
|
330
|
+
supportedReasoningEfforts: reasoningEfforts,
|
|
331
|
+
defaultReasoningEffort: variants.length > 0
|
|
332
|
+
? variants.includes('medium')
|
|
333
|
+
? 'medium'
|
|
334
|
+
: variants[0] ?? null
|
|
335
|
+
: null,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function providerModels(result: unknown, configuredModels?: Map<string, Record<string, unknown>>) {
|
|
340
|
+
const data = isRecord(result) && Array.isArray(result.providers)
|
|
341
|
+
? result
|
|
342
|
+
: isRecord(result) && isRecord(result.data) && Array.isArray(result.data.providers)
|
|
343
|
+
? result.data
|
|
344
|
+
: null;
|
|
345
|
+
const providers = Array.isArray(data?.providers) ? data.providers : [];
|
|
346
|
+
const models: AgentModel[] = [];
|
|
347
|
+
providers.forEach((provider: unknown) => {
|
|
348
|
+
if (!isRecord(provider) || !isRecord(provider.models)) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
Object.values(provider.models).forEach((model) => {
|
|
352
|
+
if (!isRecord(model)) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const providerID = stringValue(model.providerID) ?? stringValue(provider.id);
|
|
356
|
+
const modelID = stringValue(model.id);
|
|
357
|
+
if (!providerID || !modelID) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const configuredRecord = configuredModels?.get(providerModelKey(providerID, modelID));
|
|
361
|
+
if (configuredModels && configuredModels.size > 0 && !configuredRecord) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const mapped = mapProviderModel(provider, model, models.length, configuredRecord);
|
|
365
|
+
if (mapped) {
|
|
366
|
+
models.push(mapped);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
return models;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function promptModel(model: ReturnType<typeof parseModelKey>) {
|
|
374
|
+
return model
|
|
375
|
+
? {
|
|
376
|
+
providerID: model.providerID,
|
|
377
|
+
modelID: model.modelID,
|
|
378
|
+
}
|
|
379
|
+
: null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function promptInput(
|
|
383
|
+
input: StartAgentTurnInput,
|
|
384
|
+
directory: string | null | undefined,
|
|
385
|
+
model: ReturnType<typeof parseModelKey>,
|
|
386
|
+
): OpenCodePromptInput {
|
|
387
|
+
const modelSelection = promptModel(model);
|
|
388
|
+
const variant = input.reasoningEffort ?? model?.variant;
|
|
389
|
+
const common = {
|
|
390
|
+
sessionID: input.providerSessionId,
|
|
391
|
+
directory,
|
|
392
|
+
...(input.collaborationMode === 'plan' ? { agent: 'plan' } : {}),
|
|
393
|
+
...(modelSelection
|
|
394
|
+
? {
|
|
395
|
+
model: modelSelection,
|
|
396
|
+
...(variant ? { variant } : {}),
|
|
397
|
+
}
|
|
398
|
+
: {}),
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
...common,
|
|
403
|
+
parts: [{ type: 'text', text: input.prompt }],
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function locationInput(
|
|
408
|
+
providerSessionId?: string,
|
|
409
|
+
directory?: string | null | undefined,
|
|
410
|
+
): OpenCodeLocationInput {
|
|
411
|
+
return {
|
|
412
|
+
...(providerSessionId ? { sessionID: providerSessionId } : {}),
|
|
413
|
+
directory,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function modelContextWindowFromTokens(tokens: Record<string, unknown>) {
|
|
418
|
+
return nonNegativeNumberValue(
|
|
419
|
+
tokens.contextWindow ??
|
|
420
|
+
tokens.context_window ??
|
|
421
|
+
tokens.contextWindowTokens ??
|
|
422
|
+
tokens.context_window_tokens ??
|
|
423
|
+
tokens.modelContextWindow ??
|
|
424
|
+
tokens.model_context_window,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function openCodeUsageFromTokens(tokens: unknown) {
|
|
429
|
+
if (!isRecord(tokens)) {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const inputTokens = nonNegativeNumberValue(tokens.input ?? tokens.inputTokens ?? tokens.input_tokens) ?? 0;
|
|
434
|
+
const outputTokens = nonNegativeNumberValue(tokens.output ?? tokens.outputTokens ?? tokens.output_tokens) ?? 0;
|
|
435
|
+
const reasoningOutputTokens = nonNegativeNumberValue(
|
|
436
|
+
tokens.reasoning ?? tokens.reasoningOutputTokens ?? tokens.reasoning_output_tokens,
|
|
437
|
+
) ?? 0;
|
|
438
|
+
const cache = isRecord(tokens.cache) ? tokens.cache : null;
|
|
439
|
+
const cachedInputTokens = nonNegativeNumberValue(
|
|
440
|
+
tokens.cachedInputTokens ?? tokens.cached_input_tokens ?? cache?.read,
|
|
441
|
+
) ?? 0;
|
|
442
|
+
const totalTokens = nonNegativeNumberValue(tokens.total ?? tokens.totalTokens ?? tokens.total_tokens)
|
|
443
|
+
?? inputTokens + outputTokens;
|
|
444
|
+
|
|
445
|
+
if (totalTokens <= 0) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
usage: {
|
|
451
|
+
totalTokens,
|
|
452
|
+
inputTokens,
|
|
453
|
+
cachedInputTokens,
|
|
454
|
+
outputTokens,
|
|
455
|
+
reasoningOutputTokens,
|
|
456
|
+
},
|
|
457
|
+
modelContextWindow: modelContextWindowFromTokens(tokens),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function messageTokenUsage(message: unknown) {
|
|
462
|
+
if (!isRecord(message)) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const legacyMessage = isRecord(message.info) && Array.isArray(message.parts)
|
|
467
|
+
? {
|
|
468
|
+
type: stringValue(message.info.role) ?? stringValue(message.info.type),
|
|
469
|
+
content: message.parts,
|
|
470
|
+
tokens: message.info.tokens,
|
|
471
|
+
}
|
|
472
|
+
: null;
|
|
473
|
+
if (legacyMessage) {
|
|
474
|
+
return messageTokenUsage(legacyMessage);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const directTokens = openCodeUsageFromTokens(message.tokens);
|
|
478
|
+
if (directTokens) {
|
|
479
|
+
return directTokens;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const parts = Array.isArray(message.parts)
|
|
483
|
+
? message.parts
|
|
484
|
+
: Array.isArray(message.content)
|
|
485
|
+
? message.content
|
|
486
|
+
: [];
|
|
487
|
+
for (const part of parts) {
|
|
488
|
+
if (!isRecord(part)) {
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
const partType = stringValue(part.type);
|
|
492
|
+
if (partType !== 'step-finish') {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const usage = openCodeUsageFromTokens(part.tokens);
|
|
496
|
+
if (usage) {
|
|
497
|
+
return usage;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function modelContextWindowFromModel(model: ReturnType<typeof parseModelKey>) {
|
|
504
|
+
if (!model) {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
return contextWindowForModel(providerModelKey(model.providerID, model.modelID))
|
|
508
|
+
?? contextWindowForModel(model.modelID);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function turnTokenUsage(messages: unknown[], model: ReturnType<typeof parseModelKey>) {
|
|
512
|
+
const usageRecords = messages
|
|
513
|
+
.map(messageTokenUsage)
|
|
514
|
+
.filter((usage): usage is NonNullable<ReturnType<typeof messageTokenUsage>> => Boolean(usage));
|
|
515
|
+
if (usageRecords.length === 0) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const [firstRecord, ...remainingRecords] = usageRecords.map((record) => record.usage);
|
|
520
|
+
if (!firstRecord) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
const total = remainingRecords.reduce((sum, usage) => ({
|
|
524
|
+
totalTokens: sum.totalTokens + usage.totalTokens,
|
|
525
|
+
inputTokens: sum.inputTokens + usage.inputTokens,
|
|
526
|
+
cachedInputTokens: sum.cachedInputTokens + usage.cachedInputTokens,
|
|
527
|
+
outputTokens: sum.outputTokens + usage.outputTokens,
|
|
528
|
+
reasoningOutputTokens: sum.reasoningOutputTokens + usage.reasoningOutputTokens,
|
|
529
|
+
}), firstRecord);
|
|
530
|
+
const modelContextWindow = usageRecords.find((record) => record.modelContextWindow)?.modelContextWindow
|
|
531
|
+
?? modelContextWindowFromModel(model);
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
total,
|
|
535
|
+
last: total,
|
|
536
|
+
modelContextWindow,
|
|
537
|
+
cumulative: false,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function openCodeStatusType(value: unknown): 'idle' | 'busy' | 'retry' | null {
|
|
542
|
+
if (!isRecord(value)) {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
const type = stringValue(value.type);
|
|
546
|
+
return type === 'idle' || type === 'busy' || type === 'retry' ? type : null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function permissionRule(
|
|
550
|
+
permission: string,
|
|
551
|
+
action: OpenCodePermissionRule['action'],
|
|
552
|
+
pattern = '*',
|
|
553
|
+
): OpenCodePermissionRule {
|
|
554
|
+
return { permission, pattern, action };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function openCodePermissionsForSandboxMode(
|
|
558
|
+
sandboxMode: StartAgentTurnInput['sandboxMode'],
|
|
559
|
+
): OpenCodePermissionRule[] | undefined {
|
|
560
|
+
switch (sandboxMode) {
|
|
561
|
+
case 'read-only':
|
|
562
|
+
return [
|
|
563
|
+
permissionRule('read', 'allow'),
|
|
564
|
+
permissionRule('list', 'allow'),
|
|
565
|
+
permissionRule('glob', 'allow'),
|
|
566
|
+
permissionRule('grep', 'allow'),
|
|
567
|
+
permissionRule('edit', 'deny'),
|
|
568
|
+
permissionRule('bash', 'deny'),
|
|
569
|
+
permissionRule('task', 'deny'),
|
|
570
|
+
permissionRule('external_directory', 'deny'),
|
|
571
|
+
permissionRule('repo_clone', 'deny'),
|
|
572
|
+
permissionRule('repo_overview', 'allow'),
|
|
573
|
+
permissionRule('webfetch', 'allow'),
|
|
574
|
+
permissionRule('websearch', 'allow'),
|
|
575
|
+
permissionRule('todowrite', 'allow'),
|
|
576
|
+
permissionRule('question', 'allow'),
|
|
577
|
+
permissionRule('skill', 'allow'),
|
|
578
|
+
permissionRule('lsp', 'allow'),
|
|
579
|
+
permissionRule('doom_loop', 'deny'),
|
|
580
|
+
];
|
|
581
|
+
case 'workspace-write':
|
|
582
|
+
return [
|
|
583
|
+
permissionRule('read', 'allow'),
|
|
584
|
+
permissionRule('list', 'allow'),
|
|
585
|
+
permissionRule('glob', 'allow'),
|
|
586
|
+
permissionRule('grep', 'allow'),
|
|
587
|
+
permissionRule('edit', 'allow'),
|
|
588
|
+
permissionRule('bash', 'ask'),
|
|
589
|
+
permissionRule('task', 'ask'),
|
|
590
|
+
permissionRule('external_directory', 'ask'),
|
|
591
|
+
permissionRule('repo_clone', 'ask'),
|
|
592
|
+
permissionRule('repo_overview', 'allow'),
|
|
593
|
+
permissionRule('webfetch', 'allow'),
|
|
594
|
+
permissionRule('websearch', 'allow'),
|
|
595
|
+
permissionRule('todowrite', 'allow'),
|
|
596
|
+
permissionRule('question', 'allow'),
|
|
597
|
+
permissionRule('skill', 'allow'),
|
|
598
|
+
permissionRule('lsp', 'allow'),
|
|
599
|
+
permissionRule('doom_loop', 'ask'),
|
|
600
|
+
];
|
|
601
|
+
case 'danger-full-access':
|
|
602
|
+
return [
|
|
603
|
+
permissionRule('read', 'allow'),
|
|
604
|
+
permissionRule('list', 'allow'),
|
|
605
|
+
permissionRule('glob', 'allow'),
|
|
606
|
+
permissionRule('grep', 'allow'),
|
|
607
|
+
permissionRule('edit', 'allow'),
|
|
608
|
+
permissionRule('bash', 'allow'),
|
|
609
|
+
permissionRule('task', 'allow'),
|
|
610
|
+
permissionRule('external_directory', 'allow'),
|
|
611
|
+
permissionRule('repo_clone', 'allow'),
|
|
612
|
+
permissionRule('repo_overview', 'allow'),
|
|
613
|
+
permissionRule('webfetch', 'allow'),
|
|
614
|
+
permissionRule('websearch', 'allow'),
|
|
615
|
+
permissionRule('todowrite', 'allow'),
|
|
616
|
+
permissionRule('question', 'allow'),
|
|
617
|
+
permissionRule('skill', 'allow'),
|
|
618
|
+
permissionRule('lsp', 'allow'),
|
|
619
|
+
permissionRule('doom_loop', 'allow'),
|
|
620
|
+
];
|
|
621
|
+
default:
|
|
622
|
+
return undefined;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function hasMeaningfulTurnResult(turn: AgentTurn | null) {
|
|
627
|
+
return Boolean(turn?.error) || Boolean(turn?.items.some(isTerminalRuntimeItem));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function liveHistoryItemsForTurn(turn: AgentTurn | null): AgentHistoryItem[] {
|
|
631
|
+
if (!turn) {
|
|
632
|
+
return [];
|
|
633
|
+
}
|
|
634
|
+
return turn.items.filter(isLiveRuntimeItem);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function isLiveRuntimeItem(item: AgentHistoryItem) {
|
|
638
|
+
return (
|
|
639
|
+
item.kind !== 'userMessage' &&
|
|
640
|
+
item.kind !== 'other' &&
|
|
641
|
+
item.kind !== 'contextCompaction'
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function isTerminalRuntimeItem(item: AgentHistoryItem) {
|
|
646
|
+
return item.kind === 'agentMessage' || item.kind === 'plan' || item.status === 'failed';
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function turnWithPlanItemForCollaborationMode(
|
|
650
|
+
turn: AgentTurn,
|
|
651
|
+
collaborationMode: StartAgentTurnInput['collaborationMode'],
|
|
652
|
+
): AgentTurn {
|
|
653
|
+
if (collaborationMode !== 'plan' || turn.items.some((item) => item.kind === 'plan')) {
|
|
654
|
+
return turn;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const lastAgentMessageIndex = turn.items.findLastIndex((item) => item.kind === 'agentMessage');
|
|
658
|
+
if (lastAgentMessageIndex < 0) {
|
|
659
|
+
return turn;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
...turn,
|
|
664
|
+
items: turn.items.map((item, index) => (
|
|
665
|
+
index === lastAgentMessageIndex
|
|
666
|
+
? {
|
|
667
|
+
...item,
|
|
668
|
+
kind: 'plan' as const,
|
|
669
|
+
previewText: item.previewText ?? 'Plan ready for review.',
|
|
670
|
+
}
|
|
671
|
+
: item
|
|
672
|
+
)),
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function messageId(message: unknown) {
|
|
677
|
+
if (!isRecord(message)) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
return stringValue(message.id) ?? (isRecord(message.info) ? stringValue(message.info.id) : null);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function sessionSummary(record: unknown): AgentSessionSummary | null {
|
|
684
|
+
if (!isRecord(record)) {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
const id = stringValue(record.id);
|
|
688
|
+
if (!id) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
const time = isRecord(record.time) ? record.time : {};
|
|
692
|
+
const model = isRecord(record.model)
|
|
693
|
+
? modelKey(
|
|
694
|
+
stringValue(record.model.providerID) ?? 'unknown',
|
|
695
|
+
stringValue(record.model.id) ?? 'unknown',
|
|
696
|
+
stringValue(record.model.variant),
|
|
697
|
+
)
|
|
698
|
+
: null;
|
|
699
|
+
return {
|
|
700
|
+
provider: 'opencode',
|
|
701
|
+
providerSessionId: id,
|
|
702
|
+
cwd: stringValue(record.directory) ?? stringValue(record.path) ?? '',
|
|
703
|
+
title: stringValue(record.title),
|
|
704
|
+
preview: model,
|
|
705
|
+
createdAt: isoFromMs(time.created),
|
|
706
|
+
updatedAt: isoFromMs(time.updated),
|
|
707
|
+
status: 'idle',
|
|
708
|
+
rawSession: record,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function errorMessage(error: unknown) {
|
|
713
|
+
return error instanceof Error ? error.message : String(error);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function sleep(ms: number) {
|
|
717
|
+
return new Promise((resolve) => {
|
|
718
|
+
setTimeout(resolve, ms);
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
|
723
|
+
let timeout: NodeJS.Timeout | null = null;
|
|
724
|
+
try {
|
|
725
|
+
return await Promise.race([
|
|
726
|
+
promise,
|
|
727
|
+
new Promise<T>((_, reject) => {
|
|
728
|
+
timeout = setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
729
|
+
}),
|
|
730
|
+
]);
|
|
731
|
+
} finally {
|
|
732
|
+
if (timeout) {
|
|
733
|
+
clearTimeout(timeout);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async function availablePort() {
|
|
739
|
+
return new Promise<number>((resolve, reject) => {
|
|
740
|
+
const server = net.createServer();
|
|
741
|
+
server.unref();
|
|
742
|
+
server.on('error', reject);
|
|
743
|
+
server.listen(0, '127.0.0.1', () => {
|
|
744
|
+
const address = server.address();
|
|
745
|
+
server.close(() => {
|
|
746
|
+
if (address && typeof address === 'object') {
|
|
747
|
+
resolve(address.port);
|
|
748
|
+
} else {
|
|
749
|
+
reject(new Error('Unable to allocate a local OpenCode server port.'));
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export class OpenCodeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
757
|
+
readonly provider = 'opencode' as const;
|
|
758
|
+
readonly displayName = 'OpenCode';
|
|
759
|
+
readonly description = 'Local OpenCode runtime.';
|
|
760
|
+
readonly capabilities = opencodeCapabilities;
|
|
761
|
+
readonly installation: AgentBackendInstallationDto = {
|
|
762
|
+
packageName: 'opencode-ai',
|
|
763
|
+
installed: false,
|
|
764
|
+
installedVersion: null,
|
|
765
|
+
latestVersion: null,
|
|
766
|
+
installCommand: 'npm install -g opencode-ai @opencode-ai/sdk',
|
|
767
|
+
updateCommand: 'npm install -g opencode-ai@latest @opencode-ai/sdk@latest',
|
|
768
|
+
busy: false,
|
|
769
|
+
lastError: null,
|
|
770
|
+
};
|
|
771
|
+
readonly managementSchema: AgentRuntimeManagementSchema = {
|
|
772
|
+
hostConfigFiles: [],
|
|
773
|
+
toolboxItems: [
|
|
774
|
+
{ action: 'compact', command: '/compact', label: 'Compact' },
|
|
775
|
+
{ action: 'fork', command: '/fork', label: 'Fork', panel: 'fork' },
|
|
776
|
+
{ action: 'mcp', command: '/mcp', label: 'MCP', panel: 'mcp' },
|
|
777
|
+
],
|
|
778
|
+
hookCommandTemplates: [],
|
|
779
|
+
providerConfigFormat: 'json',
|
|
780
|
+
mcpConfigFormat: 'none',
|
|
781
|
+
configArchives: false,
|
|
782
|
+
buildRestart: false,
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
private status: AgentRuntimeStatus = {
|
|
786
|
+
state: 'stopped',
|
|
787
|
+
transport: 'sdk',
|
|
788
|
+
lastStartedAt: null,
|
|
789
|
+
lastError: null,
|
|
790
|
+
restartCount: 0,
|
|
791
|
+
};
|
|
792
|
+
private client: OpenCodeClient | null = null;
|
|
793
|
+
private server: { url: string; close(): void } | null = null;
|
|
794
|
+
private readonly sessionCwds = new Map<string, string>();
|
|
795
|
+
private readonly sessionModels = new Map<string, string | null>();
|
|
796
|
+
private readonly sessionSandboxModes = new Map<string, StartAgentTurnInput['sandboxMode']>();
|
|
797
|
+
private readonly activeTurns = new Map<string, {
|
|
798
|
+
providerSessionId: string;
|
|
799
|
+
aborted: boolean;
|
|
800
|
+
completedItemIds: Set<string>;
|
|
801
|
+
runningItemIds: Set<string>;
|
|
802
|
+
lastPlanSignature: string | null;
|
|
803
|
+
}>();
|
|
804
|
+
|
|
805
|
+
constructor(private readonly options: OpenCodeRuntimeAdapterOptions) {
|
|
806
|
+
super();
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
getStatus(): AgentRuntimeStatus {
|
|
810
|
+
return { ...this.status };
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
async start() {
|
|
814
|
+
try {
|
|
815
|
+
await fs.mkdir(this.options.home, { recursive: true });
|
|
816
|
+
const sdk = this.options.sdk ?? await this.loadSdk();
|
|
817
|
+
const port = this.options.sdk ? undefined : await availablePort();
|
|
818
|
+
const instance = await sdk.createOpencode({
|
|
819
|
+
hostname: '127.0.0.1',
|
|
820
|
+
...(port ? { port } : {}),
|
|
821
|
+
});
|
|
822
|
+
this.installation.installed = true;
|
|
823
|
+
this.installation.lastError = null;
|
|
824
|
+
this.client = instance.client;
|
|
825
|
+
this.server = instance.server;
|
|
826
|
+
this.status = {
|
|
827
|
+
...this.status,
|
|
828
|
+
state: 'ready',
|
|
829
|
+
lastStartedAt: new Date().toISOString(),
|
|
830
|
+
lastError: null,
|
|
831
|
+
restartCount: this.status.state === 'stopped' ? this.status.restartCount : this.status.restartCount + 1,
|
|
832
|
+
};
|
|
833
|
+
this.emit('status', this.getStatus());
|
|
834
|
+
} catch (error) {
|
|
835
|
+
this.client = null;
|
|
836
|
+
this.server = null;
|
|
837
|
+
this.installation.installed = false;
|
|
838
|
+
this.installation.lastError = errorMessage(error);
|
|
839
|
+
this.status = {
|
|
840
|
+
...this.status,
|
|
841
|
+
state: 'stopped',
|
|
842
|
+
lastError: `OpenCode is not installed or could not start. ${errorMessage(error)}`,
|
|
843
|
+
};
|
|
844
|
+
this.emit('status', this.getStatus());
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async stop() {
|
|
849
|
+
this.server?.close();
|
|
850
|
+
this.server = null;
|
|
851
|
+
this.client = null;
|
|
852
|
+
this.sessionSandboxModes.clear();
|
|
853
|
+
this.activeTurns.clear();
|
|
854
|
+
this.status = {
|
|
855
|
+
...this.status,
|
|
856
|
+
state: 'stopped',
|
|
857
|
+
lastError: null,
|
|
858
|
+
};
|
|
859
|
+
this.emit('status', this.getStatus());
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
async listModels(): Promise<AgentModel[]> {
|
|
863
|
+
const client = await this.requireClient();
|
|
864
|
+
if (client.config?.providers) {
|
|
865
|
+
const configuredModels = client.config.get
|
|
866
|
+
? configuredProviderModelRecords(unwrapResult(await client.config.get()))
|
|
867
|
+
: undefined;
|
|
868
|
+
const providers = providerModels(
|
|
869
|
+
unwrapResult(await client.config.providers()),
|
|
870
|
+
configuredModels,
|
|
871
|
+
);
|
|
872
|
+
if (providers.length > 0) {
|
|
873
|
+
return providers.map((model, index) => ({ ...model, isDefault: index === 0 }));
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const result = client.model?.list
|
|
877
|
+
? unwrapResult(await client.model.list({ query: { location: {} } }))
|
|
878
|
+
: [];
|
|
879
|
+
return result
|
|
880
|
+
.map(mapModel)
|
|
881
|
+
.filter((model): model is AgentModel => Boolean(model))
|
|
882
|
+
.map((model, index) => ({ ...model, isDefault: index === 0 }));
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
async listSessions(): Promise<AgentSessionSummary[]> {
|
|
886
|
+
const client = await this.requireClient();
|
|
887
|
+
const sessions = resultItems(unwrapResult(await client.session.list({ limit: 100 })));
|
|
888
|
+
return sessions
|
|
889
|
+
.map(sessionSummary)
|
|
890
|
+
.filter((session): session is AgentSessionSummary => Boolean(session));
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async listLoadedSessions(): Promise<string[]> {
|
|
894
|
+
return (await this.listSessions()).map((session) => session.providerSessionId);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
async readSession(providerSessionId: string, options: ReadAgentSessionOptions = {}): Promise<AgentSessionDetail> {
|
|
898
|
+
const client = await this.requireClient();
|
|
899
|
+
const directory = options.workspacePath ?? this.sessionCwds.get(providerSessionId);
|
|
900
|
+
const [sessionRecord, messages] = await Promise.all([
|
|
901
|
+
client.session.get(locationInput(providerSessionId, directory)),
|
|
902
|
+
this.readSessionMessages(client, providerSessionId, directory),
|
|
903
|
+
]);
|
|
904
|
+
const summary = sessionSummary(unwrapResult(sessionRecord)) ?? {
|
|
905
|
+
provider: 'opencode' as const,
|
|
906
|
+
providerSessionId,
|
|
907
|
+
cwd: options.workspacePath ?? this.sessionCwds.get(providerSessionId) ?? '',
|
|
908
|
+
title: null,
|
|
909
|
+
preview: null,
|
|
910
|
+
createdAt: null,
|
|
911
|
+
updatedAt: null,
|
|
912
|
+
status: 'idle' as const,
|
|
913
|
+
rawSession: null,
|
|
914
|
+
};
|
|
915
|
+
return {
|
|
916
|
+
...summary,
|
|
917
|
+
turns: openCodeMessagesToTurns(messages, {
|
|
918
|
+
workspacePath: directory ?? summary.cwd,
|
|
919
|
+
}),
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
async startSession(input: StartAgentSessionInput): Promise<StartAgentSessionResult> {
|
|
924
|
+
const client = await this.requireClient();
|
|
925
|
+
const model = parseModelKey(input.model);
|
|
926
|
+
const permission = openCodePermissionsForSandboxMode(input.sandboxMode);
|
|
927
|
+
const session = unwrapResult(await client.session.create({
|
|
928
|
+
...locationInput(undefined, input.cwd),
|
|
929
|
+
...(model
|
|
930
|
+
? {
|
|
931
|
+
model: {
|
|
932
|
+
id: model.modelID,
|
|
933
|
+
providerID: model.providerID,
|
|
934
|
+
...(model.variant ? { variant: model.variant } : {}),
|
|
935
|
+
},
|
|
936
|
+
}
|
|
937
|
+
: {}),
|
|
938
|
+
...(permission ? { permission } : {}),
|
|
939
|
+
}));
|
|
940
|
+
const summary = sessionSummary(session);
|
|
941
|
+
if (!summary) {
|
|
942
|
+
throw new AgentRuntimeError('OpenCode did not return a session id.', 'opencode', 'invalid_response');
|
|
943
|
+
}
|
|
944
|
+
this.sessionCwds.set(summary.providerSessionId, input.cwd);
|
|
945
|
+
this.sessionModels.set(summary.providerSessionId, input.model);
|
|
946
|
+
if (input.sandboxMode !== undefined) {
|
|
947
|
+
this.sessionSandboxModes.set(summary.providerSessionId, input.sandboxMode);
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
provider: 'opencode',
|
|
951
|
+
providerSessionId: summary.providerSessionId,
|
|
952
|
+
model: input.model,
|
|
953
|
+
reasoningEffort: null,
|
|
954
|
+
sandboxMode: input.sandboxMode ?? null,
|
|
955
|
+
session: {
|
|
956
|
+
...summary,
|
|
957
|
+
cwd: summary.cwd || input.cwd,
|
|
958
|
+
turns: [],
|
|
959
|
+
},
|
|
960
|
+
rawSession: session,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async resumeSession(input: ResumeAgentSessionInput): Promise<StartAgentSessionResult> {
|
|
965
|
+
const session = await this.readSession(input.providerSessionId);
|
|
966
|
+
if (input.model !== undefined) {
|
|
967
|
+
this.sessionModels.set(input.providerSessionId, input.model);
|
|
968
|
+
}
|
|
969
|
+
if (input.sandboxMode !== undefined) {
|
|
970
|
+
this.sessionSandboxModes.set(input.providerSessionId, input.sandboxMode);
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
provider: 'opencode',
|
|
974
|
+
providerSessionId: input.providerSessionId,
|
|
975
|
+
model: input.model ?? this.sessionModels.get(input.providerSessionId) ?? null,
|
|
976
|
+
reasoningEffort: null,
|
|
977
|
+
sandboxMode: input.sandboxMode ?? null,
|
|
978
|
+
session,
|
|
979
|
+
rawSession: session.rawSession,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
async startTurn(input: StartAgentTurnInput): Promise<AgentTurn> {
|
|
984
|
+
const client = await this.requireClient();
|
|
985
|
+
const providerTurnId = input.displayTurnId ?? crypto.randomUUID();
|
|
986
|
+
const startedAt = new Date().toISOString();
|
|
987
|
+
const model = parseModelKey(input.model ?? this.sessionModels.get(input.providerSessionId));
|
|
988
|
+
this.activeTurns.set(providerTurnId, {
|
|
989
|
+
providerSessionId: input.providerSessionId,
|
|
990
|
+
aborted: false,
|
|
991
|
+
completedItemIds: new Set(),
|
|
992
|
+
runningItemIds: new Set(),
|
|
993
|
+
lastPlanSignature: null,
|
|
994
|
+
});
|
|
995
|
+
const initialItems = input.hidden
|
|
996
|
+
? []
|
|
997
|
+
: [{
|
|
998
|
+
id: `${providerTurnId}:user`,
|
|
999
|
+
kind: 'userMessage' as const,
|
|
1000
|
+
text: input.displayPrompt ?? input.prompt,
|
|
1001
|
+
}];
|
|
1002
|
+
const startedTurn: AgentTurn = {
|
|
1003
|
+
providerTurnId,
|
|
1004
|
+
startedAt,
|
|
1005
|
+
status: 'inProgress',
|
|
1006
|
+
error: null,
|
|
1007
|
+
items: initialItems,
|
|
1008
|
+
};
|
|
1009
|
+
this.emitRuntimeEvent({
|
|
1010
|
+
type: 'turn.started',
|
|
1011
|
+
provider: 'opencode',
|
|
1012
|
+
providerSessionId: input.providerSessionId,
|
|
1013
|
+
turn: startedTurn,
|
|
1014
|
+
});
|
|
1015
|
+
void this.runPrompt(client, input, providerTurnId, startedAt, model);
|
|
1016
|
+
return startedTurn;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async interruptTurn(input: InterruptAgentTurnInput): Promise<AgentTurn | null> {
|
|
1020
|
+
const client = await this.requireClient();
|
|
1021
|
+
const active = this.activeTurns.get(input.providerTurnId);
|
|
1022
|
+
if (!active) {
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
active.aborted = true;
|
|
1026
|
+
await client.session.abort(locationInput(input.providerSessionId, this.sessionCwds.get(input.providerSessionId)));
|
|
1027
|
+
this.activeTurns.delete(input.providerTurnId);
|
|
1028
|
+
return {
|
|
1029
|
+
providerTurnId: input.providerTurnId,
|
|
1030
|
+
status: 'interrupted',
|
|
1031
|
+
error: null,
|
|
1032
|
+
items: [],
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
async compactSession(providerSessionId: string) {
|
|
1037
|
+
const client = await this.requireClient();
|
|
1038
|
+
if ('compact' in client.session && typeof client.session.compact === 'function') {
|
|
1039
|
+
await (client.session.compact as (input: unknown) => Promise<unknown>)({
|
|
1040
|
+
...locationInput(providerSessionId, this.sessionCwds.get(providerSessionId)),
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
private async runPrompt(
|
|
1046
|
+
client: OpenCodeClient,
|
|
1047
|
+
input: StartAgentTurnInput,
|
|
1048
|
+
providerTurnId: string,
|
|
1049
|
+
startedAt: string,
|
|
1050
|
+
model: ReturnType<typeof parseModelKey>,
|
|
1051
|
+
) {
|
|
1052
|
+
try {
|
|
1053
|
+
const directory = input.workspacePath ?? this.sessionCwds.get(input.providerSessionId);
|
|
1054
|
+
await this.updateSessionSandboxMode(client, input.providerSessionId, directory, input.sandboxMode);
|
|
1055
|
+
const baselineMessages = await this.readSessionMessages(client, input.providerSessionId, directory);
|
|
1056
|
+
const baselineMessageIds = new Set(baselineMessages.map(messageId).filter((id): id is string => Boolean(id)));
|
|
1057
|
+
let promptResponse: unknown = null;
|
|
1058
|
+
let promptError: unknown = null;
|
|
1059
|
+
const promptPromise = client.session.prompt(promptInput(input, directory, model))
|
|
1060
|
+
.then((response) => {
|
|
1061
|
+
promptResponse = unwrapResult(response);
|
|
1062
|
+
})
|
|
1063
|
+
.catch((error) => {
|
|
1064
|
+
promptError = error;
|
|
1065
|
+
});
|
|
1066
|
+
void promptPromise;
|
|
1067
|
+
this.emitRuntimeEvent({
|
|
1068
|
+
type: 'session.status.changed',
|
|
1069
|
+
provider: 'opencode',
|
|
1070
|
+
providerSessionId: input.providerSessionId,
|
|
1071
|
+
status: 'running',
|
|
1072
|
+
});
|
|
1073
|
+
await this.waitForPrompt(client, input.providerSessionId, directory);
|
|
1074
|
+
const active = this.activeTurns.get(providerTurnId);
|
|
1075
|
+
if (active?.aborted) {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const readOptions: ReadAgentSessionOptions = {};
|
|
1079
|
+
const workspacePath = input.workspacePath ?? undefined;
|
|
1080
|
+
if (workspacePath) {
|
|
1081
|
+
readOptions.workspacePath = workspacePath;
|
|
1082
|
+
}
|
|
1083
|
+
const turn = await this.waitForTurnResult(
|
|
1084
|
+
input.providerSessionId,
|
|
1085
|
+
readOptions,
|
|
1086
|
+
providerTurnId,
|
|
1087
|
+
baselineMessageIds,
|
|
1088
|
+
model,
|
|
1089
|
+
promptPromise,
|
|
1090
|
+
() => promptResponse,
|
|
1091
|
+
() => promptError,
|
|
1092
|
+
);
|
|
1093
|
+
const completedTurn = turnWithPlanItemForCollaborationMode(turn ?? {
|
|
1094
|
+
providerTurnId,
|
|
1095
|
+
startedAt,
|
|
1096
|
+
status: 'completed' as const,
|
|
1097
|
+
error: promptError ? { message: errorMessage(promptError) } : null,
|
|
1098
|
+
items: openCodeMessageToHistoryItems(
|
|
1099
|
+
promptResponse,
|
|
1100
|
+
directory ? { workspacePath: directory } : {},
|
|
1101
|
+
),
|
|
1102
|
+
}, input.collaborationMode);
|
|
1103
|
+
this.activeTurns.delete(providerTurnId);
|
|
1104
|
+
this.emitRuntimeEvent({
|
|
1105
|
+
type: 'turn.completed',
|
|
1106
|
+
provider: 'opencode',
|
|
1107
|
+
providerSessionId: input.providerSessionId,
|
|
1108
|
+
turn: {
|
|
1109
|
+
...completedTurn,
|
|
1110
|
+
providerTurnId,
|
|
1111
|
+
startedAt: completedTurn.startedAt ?? startedAt,
|
|
1112
|
+
},
|
|
1113
|
+
});
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
this.activeTurns.delete(providerTurnId);
|
|
1116
|
+
this.emitRuntimeEvent({
|
|
1117
|
+
type: 'turn.failed',
|
|
1118
|
+
provider: 'opencode',
|
|
1119
|
+
providerSessionId: input.providerSessionId,
|
|
1120
|
+
providerTurnId,
|
|
1121
|
+
error: errorMessage(error),
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
private async waitForTurnResult(
|
|
1127
|
+
providerSessionId: string,
|
|
1128
|
+
readOptions: ReadAgentSessionOptions,
|
|
1129
|
+
providerTurnId: string,
|
|
1130
|
+
baselineMessageIds: Set<string>,
|
|
1131
|
+
model: ReturnType<typeof parseModelKey>,
|
|
1132
|
+
promptPromise: Promise<void>,
|
|
1133
|
+
promptResponse: () => unknown,
|
|
1134
|
+
promptError: () => unknown,
|
|
1135
|
+
) {
|
|
1136
|
+
const deadline = Date.now() + openCodePromptTimeoutMs;
|
|
1137
|
+
let promptSettled = false;
|
|
1138
|
+
promptPromise.finally(() => {
|
|
1139
|
+
promptSettled = true;
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
while (Date.now() < deadline) {
|
|
1143
|
+
const active = this.activeTurns.get(providerTurnId);
|
|
1144
|
+
if (active?.aborted) {
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const client = await this.requireClient();
|
|
1149
|
+
const directory = readOptions.workspacePath ?? this.sessionCwds.get(providerSessionId);
|
|
1150
|
+
const messages = await this.readSessionMessages(client, providerSessionId, directory);
|
|
1151
|
+
const newMessages = messages.filter((message) => {
|
|
1152
|
+
const id = messageId(message);
|
|
1153
|
+
return !id || !baselineMessageIds.has(id);
|
|
1154
|
+
});
|
|
1155
|
+
const turns = openCodeMessagesToTurns(
|
|
1156
|
+
newMessages,
|
|
1157
|
+
directory ? { workspacePath: directory } : {},
|
|
1158
|
+
);
|
|
1159
|
+
const turn = turns[turns.length - 1] ?? null;
|
|
1160
|
+
this.emitPlanUpdate(providerSessionId, providerTurnId, newMessages);
|
|
1161
|
+
this.emitLiveTurnItems(providerSessionId, providerTurnId, turn);
|
|
1162
|
+
const sessionStatus = await this.readOpenCodeSessionStatus(client, providerSessionId, directory);
|
|
1163
|
+
const sessionIdle = sessionStatus === 'idle';
|
|
1164
|
+
if (sessionIdle && hasMeaningfulTurnResult(turn)) {
|
|
1165
|
+
this.emitTurnUsage(providerSessionId, providerTurnId, turnTokenUsage(newMessages, model));
|
|
1166
|
+
return turn;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (promptSettled && promptError()) {
|
|
1170
|
+
throw promptError();
|
|
1171
|
+
}
|
|
1172
|
+
if (promptSettled) {
|
|
1173
|
+
const responseItems = openCodeMessageToHistoryItems(
|
|
1174
|
+
promptResponse(),
|
|
1175
|
+
directory ? { workspacePath: directory } : {},
|
|
1176
|
+
);
|
|
1177
|
+
if (responseItems.length > 0) {
|
|
1178
|
+
responseItems.forEach((item) => {
|
|
1179
|
+
if (isLiveRuntimeItem(item)) {
|
|
1180
|
+
this.emitRuntimeItem(providerSessionId, providerTurnId, item);
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
if (!sessionIdle && sessionStatus !== null) {
|
|
1184
|
+
await sleep(openCodePromptPollIntervalMs);
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
if (!responseItems.some(isTerminalRuntimeItem)) {
|
|
1188
|
+
await sleep(openCodePromptPollIntervalMs);
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
this.emitTurnUsage(providerSessionId, providerTurnId, turnTokenUsage([promptResponse()], model));
|
|
1192
|
+
return {
|
|
1193
|
+
providerTurnId,
|
|
1194
|
+
startedAt: null,
|
|
1195
|
+
status: 'completed' as const,
|
|
1196
|
+
error: null,
|
|
1197
|
+
items: responseItems,
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
await sleep(openCodePromptPollIntervalMs);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
throw new Error('Timed out waiting for OpenCode to write a response.');
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
private async readOpenCodeSessionStatus(
|
|
1208
|
+
client: OpenCodeClient,
|
|
1209
|
+
providerSessionId: string,
|
|
1210
|
+
directory: string | null | undefined,
|
|
1211
|
+
) {
|
|
1212
|
+
if (!client.session.status) {
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
try {
|
|
1216
|
+
const statusMap = unwrapResult(await client.session.status({
|
|
1217
|
+
query: {
|
|
1218
|
+
...(directory ? { directory } : {}),
|
|
1219
|
+
},
|
|
1220
|
+
}));
|
|
1221
|
+
if (!isRecord(statusMap)) {
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
return openCodeStatusType(statusMap[providerSessionId]);
|
|
1225
|
+
} catch {
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
private emitTurnUsage(
|
|
1231
|
+
providerSessionId: string,
|
|
1232
|
+
providerTurnId: string,
|
|
1233
|
+
usage: ReturnType<typeof turnTokenUsage>,
|
|
1234
|
+
) {
|
|
1235
|
+
if (!usage) {
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
this.emitRuntimeEvent({
|
|
1239
|
+
type: 'usage.updated',
|
|
1240
|
+
provider: 'opencode',
|
|
1241
|
+
providerSessionId,
|
|
1242
|
+
providerTurnId,
|
|
1243
|
+
usage,
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
private emitPlanUpdate(
|
|
1248
|
+
providerSessionId: string,
|
|
1249
|
+
providerTurnId: string,
|
|
1250
|
+
messages: unknown[],
|
|
1251
|
+
) {
|
|
1252
|
+
const active = this.activeTurns.get(providerTurnId);
|
|
1253
|
+
if (!active) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const planUpdate = openCodeMessagesToPlanUpdate(messages);
|
|
1257
|
+
if (!planUpdate) {
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const signature = JSON.stringify(planUpdate);
|
|
1261
|
+
if (active.lastPlanSignature === signature) {
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
active.lastPlanSignature = signature;
|
|
1265
|
+
this.emitRuntimeEvent({
|
|
1266
|
+
type: 'plan.updated',
|
|
1267
|
+
provider: 'opencode',
|
|
1268
|
+
providerSessionId,
|
|
1269
|
+
providerTurnId,
|
|
1270
|
+
explanation: planUpdate.explanation,
|
|
1271
|
+
plan: planUpdate.plan,
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
private emitLiveTurnItems(
|
|
1276
|
+
providerSessionId: string,
|
|
1277
|
+
providerTurnId: string,
|
|
1278
|
+
turn: AgentTurn | null,
|
|
1279
|
+
) {
|
|
1280
|
+
for (const item of liveHistoryItemsForTurn(turn)) {
|
|
1281
|
+
this.emitRuntimeItem(providerSessionId, providerTurnId, item);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
private emitRuntimeItem(
|
|
1286
|
+
providerSessionId: string,
|
|
1287
|
+
providerTurnId: string,
|
|
1288
|
+
item: AgentHistoryItem,
|
|
1289
|
+
) {
|
|
1290
|
+
const active = this.activeTurns.get(providerTurnId);
|
|
1291
|
+
if (!active || !isLiveRuntimeItem(item)) {
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
const isRunning = item.status === 'running';
|
|
1295
|
+
if (isRunning) {
|
|
1296
|
+
if (active.completedItemIds.has(item.id) || active.runningItemIds.has(item.id)) {
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
active.runningItemIds.add(item.id);
|
|
1300
|
+
} else if (active.completedItemIds.has(item.id)) {
|
|
1301
|
+
return;
|
|
1302
|
+
} else {
|
|
1303
|
+
active.completedItemIds.add(item.id);
|
|
1304
|
+
}
|
|
1305
|
+
this.emitRuntimeEvent({
|
|
1306
|
+
type: isRunning ? 'item.started' : 'item.completed',
|
|
1307
|
+
provider: 'opencode',
|
|
1308
|
+
providerSessionId,
|
|
1309
|
+
providerTurnId,
|
|
1310
|
+
item,
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
private async updateSessionSandboxMode(
|
|
1315
|
+
client: OpenCodeClient,
|
|
1316
|
+
providerSessionId: string,
|
|
1317
|
+
directory: string | null | undefined,
|
|
1318
|
+
sandboxMode: StartAgentTurnInput['sandboxMode'],
|
|
1319
|
+
) {
|
|
1320
|
+
const permission = openCodePermissionsForSandboxMode(sandboxMode);
|
|
1321
|
+
if (!permission || !client.session.update) {
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
if (this.sessionSandboxModes.get(providerSessionId) === sandboxMode) {
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
unwrapResult(await client.session.update({
|
|
1328
|
+
...locationInput(providerSessionId, directory),
|
|
1329
|
+
permission,
|
|
1330
|
+
}));
|
|
1331
|
+
this.sessionSandboxModes.set(providerSessionId, sandboxMode);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
private async readSessionMessages(
|
|
1335
|
+
client: OpenCodeClient,
|
|
1336
|
+
providerSessionId: string,
|
|
1337
|
+
directory: string | null | undefined,
|
|
1338
|
+
) {
|
|
1339
|
+
const parameters = {
|
|
1340
|
+
...locationInput(providerSessionId, directory),
|
|
1341
|
+
};
|
|
1342
|
+
const legacyMessages = async () => resultItems(unwrapResult(await client.session.messages(parameters)));
|
|
1343
|
+
try {
|
|
1344
|
+
const messages = await legacyMessages();
|
|
1345
|
+
if (messages.length > 0 || !client.v2?.session?.messages) {
|
|
1346
|
+
return messages;
|
|
1347
|
+
}
|
|
1348
|
+
} catch (legacyError) {
|
|
1349
|
+
if (!client.v2?.session?.messages) {
|
|
1350
|
+
throw legacyError;
|
|
1351
|
+
}
|
|
1352
|
+
try {
|
|
1353
|
+
return resultItems(unwrapResult(await client.v2.session.messages(parameters)));
|
|
1354
|
+
} catch (v2Error) {
|
|
1355
|
+
throw new Error(
|
|
1356
|
+
`OpenCode session messages failed. legacy: ${errorMessage(legacyError)}; v2: ${errorMessage(v2Error)}`,
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
return resultItems(unwrapResult(await client.v2!.session!.messages(parameters)));
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
private async waitForPrompt(
|
|
1364
|
+
client: OpenCodeClient,
|
|
1365
|
+
providerSessionId: string,
|
|
1366
|
+
directory: string | null | undefined,
|
|
1367
|
+
) {
|
|
1368
|
+
const parameters = {
|
|
1369
|
+
...locationInput(providerSessionId, directory),
|
|
1370
|
+
};
|
|
1371
|
+
if (client.v2?.session?.wait) {
|
|
1372
|
+
try {
|
|
1373
|
+
unwrapResult(await withTimeout(client.v2.session.wait(parameters), openCodeWaitTimeoutMs));
|
|
1374
|
+
return;
|
|
1375
|
+
} catch {
|
|
1376
|
+
// OpenCode's v2 session surface is still incomplete in some releases.
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
if (client.session.wait) {
|
|
1380
|
+
try {
|
|
1381
|
+
unwrapResult(await withTimeout(client.session.wait(parameters), openCodeWaitTimeoutMs));
|
|
1382
|
+
} catch {
|
|
1383
|
+
// Prompt polling below is the source of truth; OpenCode wait can outlive useful response writes.
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
private async requireClient() {
|
|
1389
|
+
if (!this.client) {
|
|
1390
|
+
await this.start();
|
|
1391
|
+
}
|
|
1392
|
+
if (!this.client) {
|
|
1393
|
+
throw new AgentRuntimeError(
|
|
1394
|
+
this.status.lastError ?? 'OpenCode is unavailable.',
|
|
1395
|
+
'opencode',
|
|
1396
|
+
'provider_unavailable',
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
return this.client;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
private async loadSdk(): Promise<OpenCodeSdkModule> {
|
|
1403
|
+
try {
|
|
1404
|
+
return await importOptionalPackage('@opencode-ai/sdk/v2') as OpenCodeSdkModule;
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
throw new AgentRuntimeError(
|
|
1407
|
+
'Install OpenCode support with npm install -g opencode-ai and npm install -g @opencode-ai/sdk, or add @opencode-ai/sdk to this checkout.',
|
|
1408
|
+
'opencode',
|
|
1409
|
+
'provider_unavailable',
|
|
1410
|
+
undefined,
|
|
1411
|
+
error,
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
private emitRuntimeEvent(event: AgentRuntimeEvent) {
|
|
1417
|
+
this.emit('event', event);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
async function importOptionalPackage(specifier: string) {
|
|
1422
|
+
const dynamicImport = new Function('specifier', 'return import(specifier);') as (
|
|
1423
|
+
specifier: string,
|
|
1424
|
+
) => Promise<unknown>;
|
|
1425
|
+
try {
|
|
1426
|
+
return await dynamicImport(specifier);
|
|
1427
|
+
} catch (localError) {
|
|
1428
|
+
const globalRoot = await npmGlobalRoot();
|
|
1429
|
+
if (!globalRoot) {
|
|
1430
|
+
throw localError;
|
|
1431
|
+
}
|
|
1432
|
+
try {
|
|
1433
|
+
const requireFromGlobal = createRequire(path.join(globalRoot, 'remote-codex-global.cjs'));
|
|
1434
|
+
const resolved = resolveOptionalPackage(requireFromGlobal, globalRoot, specifier);
|
|
1435
|
+
return await dynamicImport(pathToFileURL(resolved).href);
|
|
1436
|
+
} catch {
|
|
1437
|
+
throw localError;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function resolveOptionalPackage(
|
|
1443
|
+
requireFromGlobal: ReturnType<typeof createRequire>,
|
|
1444
|
+
globalRoot: string,
|
|
1445
|
+
specifier: string,
|
|
1446
|
+
) {
|
|
1447
|
+
try {
|
|
1448
|
+
return requireFromGlobal.resolve(specifier);
|
|
1449
|
+
} catch (error) {
|
|
1450
|
+
if (
|
|
1451
|
+
(error as NodeJS.ErrnoException).code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' &&
|
|
1452
|
+
specifier === '@opencode-ai/sdk/v2'
|
|
1453
|
+
) {
|
|
1454
|
+
return path.join(globalRoot, '@opencode-ai', 'sdk', 'dist', 'v2', 'index.js');
|
|
1455
|
+
}
|
|
1456
|
+
throw error;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
async function npmGlobalRoot() {
|
|
1461
|
+
try {
|
|
1462
|
+
const { stdout } = await execFileAsync('npm', ['root', '-g'], {
|
|
1463
|
+
timeout: 3_000,
|
|
1464
|
+
});
|
|
1465
|
+
return stdout.trim() || null;
|
|
1466
|
+
} catch {
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
}
|