@harapter/adapter-opencode 0.1.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/LICENSE +201 -0
- package/README.md +159 -0
- package/dist/adapter.d.ts +38 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +1179 -0
- package/dist/adapter.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +124 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +839 -0
- package/dist/protocol.js.map +1 -0
- package/package.json +53 -0
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { HarnessError, providerId, } from '@harapter/core';
|
|
3
|
+
/** Stable Provider identity owned by the OpenCode Adapter. */
|
|
4
|
+
export const OPENCODE_PROVIDER_ID = providerId('opencode');
|
|
5
|
+
/** Stable protocol family used to validate resumable Session references. */
|
|
6
|
+
export const OPENCODE_SESSION_COMPATIBILITY_REF = `${OPENCODE_PROVIDER_ID};http-openapi=stable`;
|
|
7
|
+
/** Provider-native prompt part accepted through the explicit Core escape hatch. */
|
|
8
|
+
export const OPENCODE_NATIVE_PART = 'opencode.part';
|
|
9
|
+
const maximumTimerMilliseconds = 2_147_483_647;
|
|
10
|
+
const safeRawKeys = new Set([
|
|
11
|
+
'cache',
|
|
12
|
+
'callID',
|
|
13
|
+
'data',
|
|
14
|
+
'delta',
|
|
15
|
+
'error',
|
|
16
|
+
'id',
|
|
17
|
+
'info',
|
|
18
|
+
'input',
|
|
19
|
+
'messageID',
|
|
20
|
+
'name',
|
|
21
|
+
'output',
|
|
22
|
+
'part',
|
|
23
|
+
'partID',
|
|
24
|
+
'patterns',
|
|
25
|
+
'permission',
|
|
26
|
+
'permissionID',
|
|
27
|
+
'properties',
|
|
28
|
+
'read',
|
|
29
|
+
'reasoning',
|
|
30
|
+
'response',
|
|
31
|
+
'requestID',
|
|
32
|
+
'reply',
|
|
33
|
+
'role',
|
|
34
|
+
'sessionID',
|
|
35
|
+
'state',
|
|
36
|
+
'status',
|
|
37
|
+
'time',
|
|
38
|
+
'tokens',
|
|
39
|
+
'tool',
|
|
40
|
+
'type',
|
|
41
|
+
'write',
|
|
42
|
+
]);
|
|
43
|
+
const maximumRawDepth = 5;
|
|
44
|
+
const maximumRawNodes = 128;
|
|
45
|
+
const maximumRawArrayItems = 16;
|
|
46
|
+
const maximumRawObjectFields = 32;
|
|
47
|
+
const maximumTrackedMessages = 64;
|
|
48
|
+
const maximumTrackedParts = 256;
|
|
49
|
+
const completedFinishReasons = new Set([
|
|
50
|
+
'content-filter',
|
|
51
|
+
'length',
|
|
52
|
+
'other',
|
|
53
|
+
'stop',
|
|
54
|
+
]);
|
|
55
|
+
/** Validate the OpenCode health response and capture runtime identity. */
|
|
56
|
+
export function parseOpenCodeHealth(value) {
|
|
57
|
+
const response = record(value);
|
|
58
|
+
if (response?.['healthy'] !== true)
|
|
59
|
+
throw incompatible('health response');
|
|
60
|
+
return { version: nonEmptyString(response['version'], 'runtime version') };
|
|
61
|
+
}
|
|
62
|
+
/** Validate the stable Session fields used by Harapter. */
|
|
63
|
+
export function parseOpenCodeSession(value) {
|
|
64
|
+
const session = record(value);
|
|
65
|
+
if (session === undefined ||
|
|
66
|
+
typeof session['projectID'] !== 'string' ||
|
|
67
|
+
typeof session['title'] !== 'string' ||
|
|
68
|
+
record(session['time']) === undefined) {
|
|
69
|
+
throw incompatible('Session response');
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
id: nonEmptyString(session['id'], 'Session identifier'),
|
|
73
|
+
directory: nonEmptyString(session['directory'], 'Session directory'),
|
|
74
|
+
version: nonEmptyString(session['version'], 'Session runtime version'),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Validate the directory-scoped status map for one resumable Session. */
|
|
78
|
+
export function parseOpenCodeSessionStatus(value, sessionId) {
|
|
79
|
+
const statuses = record(value);
|
|
80
|
+
if (statuses === undefined)
|
|
81
|
+
throw incompatible('Session status response');
|
|
82
|
+
const valueForSession = statuses[sessionId];
|
|
83
|
+
if (valueForSession === undefined)
|
|
84
|
+
return 'idle';
|
|
85
|
+
const status = record(valueForSession)?.['type'];
|
|
86
|
+
if (status === 'busy' || status === 'idle' || status === 'retry')
|
|
87
|
+
return status;
|
|
88
|
+
throw incompatible('Session status response');
|
|
89
|
+
}
|
|
90
|
+
/** Convert portable Session creation fields into the stable OpenCode request. */
|
|
91
|
+
export function prepareOpenCodeSession(input = {}) {
|
|
92
|
+
const options = validatedOptions(input.providerOptions, [
|
|
93
|
+
'parentId',
|
|
94
|
+
'title',
|
|
95
|
+
]);
|
|
96
|
+
const body = {};
|
|
97
|
+
assignNonEmptyString(body, 'title', options['title'], 'Session title');
|
|
98
|
+
assignNonEmptyString(body, 'parentID', options['parentId'], 'parent Session id');
|
|
99
|
+
const defaults = {};
|
|
100
|
+
if (input.systemContext !== undefined) {
|
|
101
|
+
defaults.system = inputString(input.systemContext, 'systemContext');
|
|
102
|
+
}
|
|
103
|
+
if (input.model !== undefined)
|
|
104
|
+
defaults.model = coreModel(input.model);
|
|
105
|
+
return {
|
|
106
|
+
body,
|
|
107
|
+
...(input.workspace === undefined
|
|
108
|
+
? {}
|
|
109
|
+
: { directory: workspacePath(input.workspace.uri) }),
|
|
110
|
+
defaults,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Convert portable Run input and options into one stable prompt body. */
|
|
114
|
+
export function prepareOpenCodePrompt(input, options = {}, defaults = {}) {
|
|
115
|
+
if (input.parts.length === 0) {
|
|
116
|
+
throw invalidRequest('An OpenCode Run requires at least one input part.');
|
|
117
|
+
}
|
|
118
|
+
if (options.timeoutMs !== undefined) {
|
|
119
|
+
positiveTimer(options.timeoutMs, 'timeoutMs');
|
|
120
|
+
}
|
|
121
|
+
const providerOptions = validatedOptions(options.providerOptions, [
|
|
122
|
+
'agent',
|
|
123
|
+
'model',
|
|
124
|
+
'system',
|
|
125
|
+
'tools',
|
|
126
|
+
]);
|
|
127
|
+
const prompt = {
|
|
128
|
+
parts: input.parts.map(preparePart),
|
|
129
|
+
};
|
|
130
|
+
const system = providerOptions['system'] ?? defaults.system;
|
|
131
|
+
if (system !== undefined) {
|
|
132
|
+
prompt['system'] = inputString(system, 'Run system');
|
|
133
|
+
}
|
|
134
|
+
const model = providerOptions['model'] === undefined
|
|
135
|
+
? defaults.model
|
|
136
|
+
: nativeModel(providerOptions['model'], 'Run model');
|
|
137
|
+
if (model !== undefined) {
|
|
138
|
+
prompt['model'] = {
|
|
139
|
+
providerID: model.providerId,
|
|
140
|
+
modelID: model.modelId,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (providerOptions['agent'] !== undefined) {
|
|
144
|
+
prompt['agent'] = inputString(providerOptions['agent'], 'Run agent');
|
|
145
|
+
}
|
|
146
|
+
if (providerOptions['tools'] !== undefined) {
|
|
147
|
+
prompt['tools'] = booleanMap(providerOptions['tools'], 'Run tools');
|
|
148
|
+
}
|
|
149
|
+
return prompt;
|
|
150
|
+
}
|
|
151
|
+
/** Decode and validate one SSE event's JSON envelope. */
|
|
152
|
+
export function parseOpenCodeEvent(data) {
|
|
153
|
+
let value;
|
|
154
|
+
try {
|
|
155
|
+
value = JSON.parse(data);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
throw incompatible('SSE event JSON');
|
|
159
|
+
}
|
|
160
|
+
const event = record(value);
|
|
161
|
+
const properties = record(event?.['properties']);
|
|
162
|
+
if (event === undefined || properties === undefined) {
|
|
163
|
+
throw incompatible('SSE event envelope');
|
|
164
|
+
}
|
|
165
|
+
const type = nonEmptyString(event['type'], 'SSE event type');
|
|
166
|
+
const id = event['id'];
|
|
167
|
+
if (id !== undefined && typeof id !== 'string') {
|
|
168
|
+
throw incompatible('SSE event identifier');
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
type,
|
|
172
|
+
properties,
|
|
173
|
+
...(id === undefined ? {} : { id }),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** Create fresh event-routing state for one Run. */
|
|
177
|
+
export function createOpenCodeEventState() {
|
|
178
|
+
return {
|
|
179
|
+
assistantMessageIds: new Set(),
|
|
180
|
+
partTypes: new Map(),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** Map one stable or unknown OpenCode event for an owning Session. */
|
|
184
|
+
export function mapOpenCodeEvent(event, sessionId, state) {
|
|
185
|
+
const routedSessionId = eventSessionId(event);
|
|
186
|
+
if (routedSessionId !== undefined && routedSessionId !== sessionId) {
|
|
187
|
+
return { events: [], routed: false };
|
|
188
|
+
}
|
|
189
|
+
if (event.type === 'server.connected')
|
|
190
|
+
return { events: [], routed: false };
|
|
191
|
+
if (event.type === 'message.updated') {
|
|
192
|
+
const info = record(event.properties['info']);
|
|
193
|
+
if (event.properties['sessionID'] !== sessionId &&
|
|
194
|
+
info?.['sessionID'] !== sessionId) {
|
|
195
|
+
return { events: [], routed: false };
|
|
196
|
+
}
|
|
197
|
+
if (info === undefined)
|
|
198
|
+
return providerMapping(event);
|
|
199
|
+
if (info['role'] !== 'assistant')
|
|
200
|
+
return { events: [], routed: true };
|
|
201
|
+
const messageId = info['id'];
|
|
202
|
+
if (typeof messageId !== 'string' || messageId.length === 0) {
|
|
203
|
+
return providerMapping(event);
|
|
204
|
+
}
|
|
205
|
+
boundedSetAdd(state.assistantMessageIds, messageId, maximumTrackedMessages);
|
|
206
|
+
const usage = usageSummary(info['tokens']);
|
|
207
|
+
return usage === undefined
|
|
208
|
+
? { events: [], routed: true }
|
|
209
|
+
: {
|
|
210
|
+
events: [{ type: 'usage.updated', data: usage, usage }],
|
|
211
|
+
routed: true,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (event.type === 'message.part.updated') {
|
|
215
|
+
const part = record(event.properties['part']);
|
|
216
|
+
if (event.properties['sessionID'] !== sessionId &&
|
|
217
|
+
part?.['sessionID'] !== sessionId) {
|
|
218
|
+
return { events: [], routed: false };
|
|
219
|
+
}
|
|
220
|
+
if (part === undefined)
|
|
221
|
+
return providerMapping(event);
|
|
222
|
+
const partId = part['id'];
|
|
223
|
+
const partType = part['type'];
|
|
224
|
+
if (typeof partId === 'string' &&
|
|
225
|
+
partId.length > 0 &&
|
|
226
|
+
typeof partType === 'string') {
|
|
227
|
+
boundedMapSet(state.partTypes, partId, partType, maximumTrackedParts);
|
|
228
|
+
}
|
|
229
|
+
const messageId = part['messageID'];
|
|
230
|
+
if (typeof messageId !== 'string' ||
|
|
231
|
+
!state.assistantMessageIds.has(messageId)) {
|
|
232
|
+
return providerMapping(event);
|
|
233
|
+
}
|
|
234
|
+
return mapPartEvent(event, part);
|
|
235
|
+
}
|
|
236
|
+
if (event.type === 'message.part.delta') {
|
|
237
|
+
if (event.properties['sessionID'] !== sessionId) {
|
|
238
|
+
return { events: [], routed: false };
|
|
239
|
+
}
|
|
240
|
+
const messageId = event.properties['messageID'];
|
|
241
|
+
const partId = event.properties['partID'];
|
|
242
|
+
const field = event.properties['field'];
|
|
243
|
+
const delta = event.properties['delta'];
|
|
244
|
+
if (typeof messageId !== 'string' ||
|
|
245
|
+
!state.assistantMessageIds.has(messageId) ||
|
|
246
|
+
typeof partId !== 'string' ||
|
|
247
|
+
field !== 'text' ||
|
|
248
|
+
typeof delta !== 'string') {
|
|
249
|
+
return providerMapping(event);
|
|
250
|
+
}
|
|
251
|
+
const partType = state.partTypes.get(partId);
|
|
252
|
+
if (partType === 'text') {
|
|
253
|
+
return {
|
|
254
|
+
events: [{ type: 'message.delta', data: { delta } }],
|
|
255
|
+
routed: true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (partType === 'reasoning') {
|
|
259
|
+
return {
|
|
260
|
+
events: [{ type: 'reasoning.delta', data: { delta } }],
|
|
261
|
+
routed: true,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return providerMapping(event);
|
|
265
|
+
}
|
|
266
|
+
if (event.type === 'permission.asked' ||
|
|
267
|
+
event.type === 'permission.updated') {
|
|
268
|
+
if (event.properties['sessionID'] !== sessionId) {
|
|
269
|
+
return { events: [], routed: false };
|
|
270
|
+
}
|
|
271
|
+
const permissionId = event.properties['id'];
|
|
272
|
+
const permissionType = event.type === 'permission.asked'
|
|
273
|
+
? event.properties['permission']
|
|
274
|
+
: event.properties['type'];
|
|
275
|
+
const title = event.type === 'permission.asked'
|
|
276
|
+
? typeof permissionType === 'string'
|
|
277
|
+
? `OpenCode ${permissionType} permission`
|
|
278
|
+
: undefined
|
|
279
|
+
: event.properties['title'];
|
|
280
|
+
if (typeof permissionId !== 'string' ||
|
|
281
|
+
permissionId.length === 0 ||
|
|
282
|
+
typeof permissionType !== 'string' ||
|
|
283
|
+
permissionType.length === 0 ||
|
|
284
|
+
typeof title !== 'string' ||
|
|
285
|
+
title.length === 0) {
|
|
286
|
+
return providerMapping(event);
|
|
287
|
+
}
|
|
288
|
+
const pattern = permissionPattern(event.type === 'permission.asked'
|
|
289
|
+
? event.properties['patterns']
|
|
290
|
+
: event.properties['pattern']);
|
|
291
|
+
return {
|
|
292
|
+
events: [],
|
|
293
|
+
routed: true,
|
|
294
|
+
permission: {
|
|
295
|
+
permissionId,
|
|
296
|
+
title,
|
|
297
|
+
type: permissionType,
|
|
298
|
+
...(pattern === undefined ? {} : { pattern }),
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (event.type === 'permission.replied') {
|
|
303
|
+
if (event.properties['sessionID'] !== sessionId) {
|
|
304
|
+
return { events: [], routed: false };
|
|
305
|
+
}
|
|
306
|
+
const permissionId = event.properties['requestID'] ?? event.properties['permissionID'];
|
|
307
|
+
return typeof permissionId === 'string' && permissionId.length > 0
|
|
308
|
+
? { events: [], routed: true, resolvedPermissionId: permissionId }
|
|
309
|
+
: providerMapping(event);
|
|
310
|
+
}
|
|
311
|
+
if (event.type === 'session.status' ||
|
|
312
|
+
event.type === 'session.idle' ||
|
|
313
|
+
event.type === 'session.created' ||
|
|
314
|
+
event.type === 'session.updated') {
|
|
315
|
+
return { events: [], routed: routedSessionId === sessionId };
|
|
316
|
+
}
|
|
317
|
+
return routedSessionId === sessionId
|
|
318
|
+
? providerMapping(event)
|
|
319
|
+
: { events: [], routed: false };
|
|
320
|
+
}
|
|
321
|
+
/** Validate the synchronous prompt response as the authoritative Run result. */
|
|
322
|
+
export function parseOpenCodePromptResponse(value, sessionId) {
|
|
323
|
+
const response = record(value);
|
|
324
|
+
const info = record(response?.['info']);
|
|
325
|
+
const parts = response?.['parts'];
|
|
326
|
+
if (info?.['role'] !== 'assistant' ||
|
|
327
|
+
info['sessionID'] !== sessionId ||
|
|
328
|
+
!Array.isArray(parts)) {
|
|
329
|
+
throw incompatible('prompt response');
|
|
330
|
+
}
|
|
331
|
+
const messageId = nonEmptyString(info['id'], 'assistant message identifier');
|
|
332
|
+
const usage = usageSummary(info['tokens']);
|
|
333
|
+
if (usage === undefined)
|
|
334
|
+
throw incompatible('assistant message usage');
|
|
335
|
+
const error = record(info['error']);
|
|
336
|
+
if (error !== undefined) {
|
|
337
|
+
const errorName = safeProviderCode(error['name']);
|
|
338
|
+
const providerResult = { error: errorName, messageId };
|
|
339
|
+
return {
|
|
340
|
+
result: {
|
|
341
|
+
status: errorName === 'MessageAbortedError' ? 'cancelled' : 'failed',
|
|
342
|
+
usage,
|
|
343
|
+
providerResult,
|
|
344
|
+
},
|
|
345
|
+
usage,
|
|
346
|
+
providerResult,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const finish = info['finish'];
|
|
350
|
+
if (typeof finish !== 'string' || finish.length === 0) {
|
|
351
|
+
throw incompatible('assistant message finish reason');
|
|
352
|
+
}
|
|
353
|
+
const providerResult = { finish, messageId };
|
|
354
|
+
if (finish === 'error') {
|
|
355
|
+
return {
|
|
356
|
+
result: {
|
|
357
|
+
status: 'failed',
|
|
358
|
+
usage,
|
|
359
|
+
providerResult,
|
|
360
|
+
},
|
|
361
|
+
usage,
|
|
362
|
+
providerResult,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (!completedFinishReasons.has(finish) &&
|
|
366
|
+
!(finish === 'tool-calls' &&
|
|
367
|
+
settledToolResponse(parts, sessionId, messageId))) {
|
|
368
|
+
throw incompatible('assistant message finish reason');
|
|
369
|
+
}
|
|
370
|
+
const finalMessage = finalText(parts, sessionId, messageId);
|
|
371
|
+
return {
|
|
372
|
+
result: {
|
|
373
|
+
status: 'completed',
|
|
374
|
+
...(finalMessage.length === 0 ? {} : { finalMessage }),
|
|
375
|
+
usage,
|
|
376
|
+
providerResult,
|
|
377
|
+
},
|
|
378
|
+
...(finalMessage.length === 0 ? {} : { finalMessage }),
|
|
379
|
+
usage,
|
|
380
|
+
providerResult,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/** Validate Provider state required to resume the same OpenCode Session. */
|
|
384
|
+
export function sessionStateFromRef(ref) {
|
|
385
|
+
if (ref.providerId !== OPENCODE_PROVIDER_ID ||
|
|
386
|
+
ref.compatibilityRef !== OPENCODE_SESSION_COMPATIBILITY_REF) {
|
|
387
|
+
throw sessionStateMismatch();
|
|
388
|
+
}
|
|
389
|
+
const state = record(ref.providerState);
|
|
390
|
+
if (state === undefined)
|
|
391
|
+
throw sessionStateMismatch();
|
|
392
|
+
const directory = state['directory'];
|
|
393
|
+
if (typeof directory !== 'string' || directory.length === 0) {
|
|
394
|
+
throw sessionStateMismatch();
|
|
395
|
+
}
|
|
396
|
+
const system = state['system'];
|
|
397
|
+
if (system !== undefined && typeof system !== 'string') {
|
|
398
|
+
throw sessionStateMismatch();
|
|
399
|
+
}
|
|
400
|
+
const model = state['model'];
|
|
401
|
+
let parsedModel;
|
|
402
|
+
if (model !== undefined) {
|
|
403
|
+
try {
|
|
404
|
+
parsedModel = nativeModel(model, 'Session model state');
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
throw sessionStateMismatch();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
directory,
|
|
412
|
+
...(system === undefined ? {} : { system }),
|
|
413
|
+
...(parsedModel === undefined ? {} : { model: parsedModel }),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/** Produce a bounded structural summary that never retains string values. */
|
|
417
|
+
export function redactOpenCodeEvent(event) {
|
|
418
|
+
const state = { nodes: 0 };
|
|
419
|
+
return {
|
|
420
|
+
type: safeEventType(event.type),
|
|
421
|
+
properties: redact(event.properties, 0, state),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function preparePart(part) {
|
|
425
|
+
switch (part.type) {
|
|
426
|
+
case 'text':
|
|
427
|
+
return { type: 'text', text: part.text };
|
|
428
|
+
case 'file_ref':
|
|
429
|
+
return filePart(part.uri, part.mediaType, false);
|
|
430
|
+
case 'image_ref':
|
|
431
|
+
return filePart(part.uri, part.mediaType, true);
|
|
432
|
+
case 'provider':
|
|
433
|
+
if (part.name !== OPENCODE_NATIVE_PART ||
|
|
434
|
+
!isOpenCodeNativePart(part.value)) {
|
|
435
|
+
throw unsupported('input.provider', 'The Provider input part is not a supported OpenCode prompt part.');
|
|
436
|
+
}
|
|
437
|
+
return part.value;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function filePart(uri, mediaType, image) {
|
|
441
|
+
if (mediaType === undefined || mediaType.length === 0) {
|
|
442
|
+
throw invalidRequest('OpenCode file and image input requires mediaType.');
|
|
443
|
+
}
|
|
444
|
+
if (image && !mediaType.startsWith('image/')) {
|
|
445
|
+
throw invalidRequest('OpenCode image input mediaType must be an image type.');
|
|
446
|
+
}
|
|
447
|
+
absoluteUri(uri, 'input URI');
|
|
448
|
+
return { type: 'file', mime: mediaType, url: uri };
|
|
449
|
+
}
|
|
450
|
+
function isOpenCodeNativePart(value) {
|
|
451
|
+
const part = record(value);
|
|
452
|
+
if (part?.['type'] === 'text')
|
|
453
|
+
return typeof part['text'] === 'string';
|
|
454
|
+
if (part?.['type'] === 'file') {
|
|
455
|
+
if (typeof part['mime'] !== 'string' ||
|
|
456
|
+
part['mime'].length === 0 ||
|
|
457
|
+
typeof part['url'] !== 'string') {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
absoluteUri(part['url'], 'native file URL');
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
return (part['filename'] === undefined || typeof part['filename'] === 'string');
|
|
467
|
+
}
|
|
468
|
+
if (part?.['type'] === 'agent') {
|
|
469
|
+
return typeof part['name'] === 'string' && part['name'].length > 0;
|
|
470
|
+
}
|
|
471
|
+
if (part?.['type'] === 'subtask') {
|
|
472
|
+
return (typeof part['prompt'] === 'string' &&
|
|
473
|
+
typeof part['description'] === 'string' &&
|
|
474
|
+
typeof part['agent'] === 'string' &&
|
|
475
|
+
part['agent'].length > 0);
|
|
476
|
+
}
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
function mapPartEvent(event, part) {
|
|
480
|
+
const partType = part['type'];
|
|
481
|
+
const delta = event.properties['delta'];
|
|
482
|
+
if (partType === 'text' && typeof delta === 'string') {
|
|
483
|
+
return {
|
|
484
|
+
events: [{ type: 'message.delta', data: { delta } }],
|
|
485
|
+
routed: true,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
if (partType === 'reasoning' && typeof delta === 'string') {
|
|
489
|
+
return {
|
|
490
|
+
events: [{ type: 'reasoning.delta', data: { delta } }],
|
|
491
|
+
routed: true,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
if (partType === 'reasoning' && record(part['time'])?.['end'] !== undefined) {
|
|
495
|
+
const raw = redactOpenCodeEvent(event);
|
|
496
|
+
return {
|
|
497
|
+
events: [
|
|
498
|
+
{
|
|
499
|
+
type: 'reasoning.completed',
|
|
500
|
+
data: {},
|
|
501
|
+
providerEventType: raw.type,
|
|
502
|
+
raw,
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
routed: true,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
if (partType === 'tool') {
|
|
509
|
+
const state = record(part['state']);
|
|
510
|
+
const status = state?.['status'];
|
|
511
|
+
const type = status === 'pending'
|
|
512
|
+
? 'tool.started'
|
|
513
|
+
: status === 'completed' || status === 'error'
|
|
514
|
+
? 'tool.completed'
|
|
515
|
+
: status === 'running'
|
|
516
|
+
? 'tool.updated'
|
|
517
|
+
: undefined;
|
|
518
|
+
if (type === undefined)
|
|
519
|
+
return providerMapping(event);
|
|
520
|
+
const raw = redactOpenCodeEvent(event);
|
|
521
|
+
return {
|
|
522
|
+
events: [
|
|
523
|
+
{
|
|
524
|
+
type,
|
|
525
|
+
data: {
|
|
526
|
+
status,
|
|
527
|
+
tool: typeof part['tool'] === 'string'
|
|
528
|
+
? safeProviderCode(part['tool'])
|
|
529
|
+
: 'unknown',
|
|
530
|
+
},
|
|
531
|
+
providerEventType: raw.type,
|
|
532
|
+
raw,
|
|
533
|
+
},
|
|
534
|
+
],
|
|
535
|
+
routed: true,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
if (partType === 'file') {
|
|
539
|
+
const mime = part['mime'];
|
|
540
|
+
const uri = part['url'];
|
|
541
|
+
if (typeof mime !== 'string' || typeof uri !== 'string') {
|
|
542
|
+
return providerMapping(event);
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
events: [
|
|
546
|
+
{
|
|
547
|
+
type: 'artifact.created',
|
|
548
|
+
data: {
|
|
549
|
+
mediaType: mime,
|
|
550
|
+
uri,
|
|
551
|
+
...(typeof part['filename'] === 'string'
|
|
552
|
+
? { filename: part['filename'] }
|
|
553
|
+
: {}),
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
],
|
|
557
|
+
routed: true,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
if (partType === 'step-finish') {
|
|
561
|
+
const usage = usageSummary(part['tokens']);
|
|
562
|
+
return usage === undefined
|
|
563
|
+
? providerMapping(event)
|
|
564
|
+
: {
|
|
565
|
+
events: [{ type: 'usage.updated', data: usage, usage }],
|
|
566
|
+
routed: true,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
return { events: [], routed: true };
|
|
570
|
+
}
|
|
571
|
+
function providerMapping(event) {
|
|
572
|
+
const raw = redactOpenCodeEvent(event);
|
|
573
|
+
return {
|
|
574
|
+
events: [
|
|
575
|
+
{
|
|
576
|
+
type: 'provider',
|
|
577
|
+
data: { providerEventType: raw.type },
|
|
578
|
+
providerEventType: raw.type,
|
|
579
|
+
raw,
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
routed: true,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function eventSessionId(event) {
|
|
586
|
+
const direct = event.properties['sessionID'];
|
|
587
|
+
if (typeof direct === 'string')
|
|
588
|
+
return direct;
|
|
589
|
+
const info = record(event.properties['info']);
|
|
590
|
+
if (typeof info?.['sessionID'] === 'string')
|
|
591
|
+
return info['sessionID'];
|
|
592
|
+
const part = record(event.properties['part']);
|
|
593
|
+
return typeof part?.['sessionID'] === 'string'
|
|
594
|
+
? part['sessionID']
|
|
595
|
+
: undefined;
|
|
596
|
+
}
|
|
597
|
+
function usageSummary(value) {
|
|
598
|
+
const tokens = record(value);
|
|
599
|
+
const input = tokens?.['input'];
|
|
600
|
+
const output = tokens?.['output'];
|
|
601
|
+
if (typeof input !== 'number' ||
|
|
602
|
+
!Number.isSafeInteger(input) ||
|
|
603
|
+
input < 0 ||
|
|
604
|
+
typeof output !== 'number' ||
|
|
605
|
+
!Number.isSafeInteger(output) ||
|
|
606
|
+
output < 0) {
|
|
607
|
+
return undefined;
|
|
608
|
+
}
|
|
609
|
+
return { inputTokens: input, outputTokens: output };
|
|
610
|
+
}
|
|
611
|
+
function finalText(parts, sessionId, messageId) {
|
|
612
|
+
const text = [];
|
|
613
|
+
for (const value of parts) {
|
|
614
|
+
const part = record(value);
|
|
615
|
+
if (part?.['type'] !== 'text' ||
|
|
616
|
+
part['sessionID'] !== sessionId ||
|
|
617
|
+
part['messageID'] !== messageId ||
|
|
618
|
+
part['ignored'] === true ||
|
|
619
|
+
part['synthetic'] === true) {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
if (typeof part['text'] !== 'string')
|
|
623
|
+
throw incompatible('text result part');
|
|
624
|
+
text.push(part['text']);
|
|
625
|
+
}
|
|
626
|
+
return text.join('\n');
|
|
627
|
+
}
|
|
628
|
+
function settledToolResponse(parts, sessionId, messageId) {
|
|
629
|
+
let observed = false;
|
|
630
|
+
for (const value of parts) {
|
|
631
|
+
const part = record(value);
|
|
632
|
+
if (part?.['type'] !== 'tool' ||
|
|
633
|
+
part['sessionID'] !== sessionId ||
|
|
634
|
+
part['messageID'] !== messageId) {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
observed = true;
|
|
638
|
+
const status = record(part['state'])?.['status'];
|
|
639
|
+
if (status !== 'completed' && status !== 'error')
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
return observed;
|
|
643
|
+
}
|
|
644
|
+
function coreModel(model) {
|
|
645
|
+
const options = validatedOptions(model.providerOptions, ['providerId']);
|
|
646
|
+
return {
|
|
647
|
+
providerId: inputString(options['providerId'], 'model providerId'),
|
|
648
|
+
modelId: inputString(model.id, 'model id'),
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function nativeModel(value, label) {
|
|
652
|
+
const model = record(value);
|
|
653
|
+
if (model === undefined)
|
|
654
|
+
throw invalidRequest(`${label} must be an object.`);
|
|
655
|
+
assertKnownKeys(model, ['modelId', 'providerId']);
|
|
656
|
+
return {
|
|
657
|
+
providerId: inputString(model['providerId'], `${label} providerId`),
|
|
658
|
+
modelId: inputString(model['modelId'], `${label} modelId`),
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function booleanMap(value, label) {
|
|
662
|
+
const input = record(value);
|
|
663
|
+
if (input === undefined)
|
|
664
|
+
throw invalidRequest(`${label} must be an object.`);
|
|
665
|
+
const output = {};
|
|
666
|
+
for (const [name, enabled] of Object.entries(input)) {
|
|
667
|
+
if (name.length === 0 || typeof enabled !== 'boolean') {
|
|
668
|
+
throw invalidRequest(`${label} values must be booleans.`);
|
|
669
|
+
}
|
|
670
|
+
output[name] = enabled;
|
|
671
|
+
}
|
|
672
|
+
return output;
|
|
673
|
+
}
|
|
674
|
+
function permissionPattern(value) {
|
|
675
|
+
if (typeof value === 'string')
|
|
676
|
+
return value;
|
|
677
|
+
if (Array.isArray(value) && value.every((item) => typeof item === 'string')) {
|
|
678
|
+
return value;
|
|
679
|
+
}
|
|
680
|
+
return undefined;
|
|
681
|
+
}
|
|
682
|
+
function workspacePath(uri) {
|
|
683
|
+
let parsed;
|
|
684
|
+
try {
|
|
685
|
+
parsed = new URL(uri);
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
throw invalidRequest('OpenCode workspace must be an absolute file URI.');
|
|
689
|
+
}
|
|
690
|
+
if (parsed.protocol !== 'file:') {
|
|
691
|
+
throw invalidRequest('OpenCode workspace must use the file URI scheme.');
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
return fileURLToPath(parsed);
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
throw invalidRequest('OpenCode workspace file URI is invalid.');
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function absoluteUri(value, label) {
|
|
701
|
+
try {
|
|
702
|
+
new URL(value);
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
throw invalidRequest(`OpenCode ${label} must be an absolute URI.`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function validatedOptions(value, allowed) {
|
|
709
|
+
if (value === undefined)
|
|
710
|
+
return {};
|
|
711
|
+
const options = record(value);
|
|
712
|
+
if (options === undefined)
|
|
713
|
+
throw invalidRequest('OpenCode options must be an object.');
|
|
714
|
+
assertKnownKeys(options, allowed);
|
|
715
|
+
return options;
|
|
716
|
+
}
|
|
717
|
+
function assertKnownKeys(value, allowed) {
|
|
718
|
+
const allowedKeys = new Set(allowed);
|
|
719
|
+
const unknown = Object.keys(value).find((key) => !allowedKeys.has(key));
|
|
720
|
+
if (unknown !== undefined) {
|
|
721
|
+
throw invalidRequest(`OpenCode option ${unknown} is unknown.`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function assignNonEmptyString(output, name, value, label) {
|
|
725
|
+
if (value !== undefined)
|
|
726
|
+
output[name] = inputString(value, label);
|
|
727
|
+
}
|
|
728
|
+
function inputString(value, label) {
|
|
729
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
730
|
+
throw invalidRequest(`OpenCode ${label} must be a non-empty string.`);
|
|
731
|
+
}
|
|
732
|
+
return value;
|
|
733
|
+
}
|
|
734
|
+
function nonEmptyString(value, label) {
|
|
735
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
736
|
+
throw incompatible(label);
|
|
737
|
+
}
|
|
738
|
+
return value;
|
|
739
|
+
}
|
|
740
|
+
function safeProviderCode(value) {
|
|
741
|
+
return typeof value === 'string' &&
|
|
742
|
+
/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/u.test(value)
|
|
743
|
+
? value
|
|
744
|
+
: 'UnknownUpstreamError';
|
|
745
|
+
}
|
|
746
|
+
function safeEventType(value) {
|
|
747
|
+
return /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/u.test(value) ? value : 'unknown';
|
|
748
|
+
}
|
|
749
|
+
function redact(value, depth, state) {
|
|
750
|
+
state.nodes += 1;
|
|
751
|
+
if (state.nodes > maximumRawNodes || depth > maximumRawDepth) {
|
|
752
|
+
return '<truncated>';
|
|
753
|
+
}
|
|
754
|
+
if (value === null)
|
|
755
|
+
return null;
|
|
756
|
+
if (typeof value === 'string')
|
|
757
|
+
return '<string>';
|
|
758
|
+
if (typeof value === 'number')
|
|
759
|
+
return Number.isFinite(value) ? '<number>' : '<non-finite>';
|
|
760
|
+
if (typeof value === 'boolean')
|
|
761
|
+
return value;
|
|
762
|
+
if (Array.isArray(value)) {
|
|
763
|
+
return value
|
|
764
|
+
.slice(0, maximumRawArrayItems)
|
|
765
|
+
.map((item) => redact(item, depth + 1, state));
|
|
766
|
+
}
|
|
767
|
+
const object = record(value);
|
|
768
|
+
if (object === undefined)
|
|
769
|
+
return `<${typeof value}>`;
|
|
770
|
+
const output = {};
|
|
771
|
+
let included = 0;
|
|
772
|
+
let omitted = 0;
|
|
773
|
+
for (const [key, child] of Object.entries(object)) {
|
|
774
|
+
if (!safeRawKeys.has(key) || included >= maximumRawObjectFields) {
|
|
775
|
+
omitted += 1;
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
output[key] = redact(child, depth + 1, state);
|
|
779
|
+
included += 1;
|
|
780
|
+
}
|
|
781
|
+
if (omitted > 0)
|
|
782
|
+
output['omittedFields'] = omitted;
|
|
783
|
+
return output;
|
|
784
|
+
}
|
|
785
|
+
function record(value) {
|
|
786
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
787
|
+
? value
|
|
788
|
+
: undefined;
|
|
789
|
+
}
|
|
790
|
+
function boundedSetAdd(values, value, capacity) {
|
|
791
|
+
if (!values.has(value) && values.size >= capacity) {
|
|
792
|
+
const oldest = values.values().next().value;
|
|
793
|
+
if (typeof oldest === 'string')
|
|
794
|
+
values.delete(oldest);
|
|
795
|
+
}
|
|
796
|
+
values.add(value);
|
|
797
|
+
}
|
|
798
|
+
function boundedMapSet(values, key, value, capacity) {
|
|
799
|
+
if (!values.has(key) && values.size >= capacity) {
|
|
800
|
+
const oldest = values.keys().next().value;
|
|
801
|
+
if (typeof oldest === 'string')
|
|
802
|
+
values.delete(oldest);
|
|
803
|
+
}
|
|
804
|
+
values.set(key, value);
|
|
805
|
+
}
|
|
806
|
+
function positiveTimer(value, label) {
|
|
807
|
+
if (!Number.isSafeInteger(value) ||
|
|
808
|
+
value <= 0 ||
|
|
809
|
+
value > maximumTimerMilliseconds) {
|
|
810
|
+
throw invalidRequest(`OpenCode ${label} must be a positive supported timer.`);
|
|
811
|
+
}
|
|
812
|
+
return value;
|
|
813
|
+
}
|
|
814
|
+
function incompatible(surface) {
|
|
815
|
+
return new HarnessError('provider_api_incompatible', `OpenCode returned an incompatible ${surface}.`, {
|
|
816
|
+
retryable: false,
|
|
817
|
+
providerId: OPENCODE_PROVIDER_ID,
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
function invalidRequest(message) {
|
|
821
|
+
return new HarnessError('invalid_request', message, {
|
|
822
|
+
retryable: false,
|
|
823
|
+
providerId: OPENCODE_PROVIDER_ID,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
function unsupported(capability, message) {
|
|
827
|
+
return new HarnessError('unsupported_capability', message, {
|
|
828
|
+
retryable: false,
|
|
829
|
+
providerId: OPENCODE_PROVIDER_ID,
|
|
830
|
+
details: { capability },
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
function sessionStateMismatch() {
|
|
834
|
+
return new HarnessError('session_provider_mismatch', 'OpenCode Session state is missing or incompatible.', {
|
|
835
|
+
retryable: false,
|
|
836
|
+
providerId: OPENCODE_PROVIDER_ID,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
//# sourceMappingURL=protocol.js.map
|