@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.
@@ -0,0 +1,1179 @@
1
+ import { ExtensionRegistry, HarnessError, assertSessionCompatibility, assertSessionOwnership, providerSessionId, runId, } from '@harapter/core';
2
+ import { HttpSseTransport, HttpTransportError, } from '@harapter/transport-http-sse';
3
+ import { OPENCODE_PROVIDER_ID, OPENCODE_SESSION_COMPATIBILITY_REF, createOpenCodeEventState, mapOpenCodeEvent, parseOpenCodeEvent, parseOpenCodeHealth, parseOpenCodePromptResponse, parseOpenCodeSession, parseOpenCodeSessionStatus, prepareOpenCodePrompt, prepareOpenCodeSession, redactOpenCodeEvent, sessionStateFromRef, } from './protocol.js';
4
+ const descriptor = {
5
+ providerId: OPENCODE_PROVIDER_ID,
6
+ displayName: 'OpenCode Server',
7
+ connectionKinds: ['endpoint'],
8
+ documentationUrl: 'https://opencode.ai/docs/server/',
9
+ };
10
+ const defaultMaxRunEvents = 128;
11
+ const defaultRunRequestTimeoutMs = 30 * 60 * 1000;
12
+ const defaultCancelSettlementTimeoutMs = 10_000;
13
+ const defaultEventDrainTimeoutMs = 250;
14
+ const maximumTimerMilliseconds = 2_147_483_647;
15
+ const maximumRunEventCapacity = 4096;
16
+ const uncertainRequestProviderCodes = new Set([
17
+ 'network_failure',
18
+ 'request_aborted',
19
+ 'request_timeout',
20
+ 'response_stream_failed',
21
+ 'transport_closed',
22
+ ]);
23
+ /** Create a fresh OpenCode HTTP Adapter factory. */
24
+ export function createOpenCodeProviderFactory(options = {}) {
25
+ return {
26
+ descriptor: () => ({
27
+ ...descriptor,
28
+ connectionKinds: [...descriptor.connectionKinds],
29
+ }),
30
+ connect: async (profile) => connectOpenCode(profile, options),
31
+ };
32
+ }
33
+ async function connectOpenCode(profile, factoryOptions) {
34
+ validateProfile(profile, factoryOptions);
35
+ const options = connectionOptions(profile.providerOptions);
36
+ const headers = await resolveHeaders(profile, factoryOptions);
37
+ let transport;
38
+ try {
39
+ transport = new HttpSseTransport({
40
+ baseUrl: endpointUrl(profile),
41
+ ...(factoryOptions.fetch === undefined
42
+ ? {}
43
+ : { fetch: factoryOptions.fetch }),
44
+ ...(headers === undefined ? {} : { defaultHeaders: headers }),
45
+ ...(options.requestTimeoutMs === undefined
46
+ ? {}
47
+ : { requestTimeoutMs: options.requestTimeoutMs }),
48
+ ...(options.sseConnectTimeoutMs === undefined
49
+ ? {}
50
+ : { sseConnectTimeoutMs: options.sseConnectTimeoutMs }),
51
+ });
52
+ }
53
+ catch (error) {
54
+ throw mapError(error, profile, 'configure transport', true);
55
+ }
56
+ try {
57
+ const health = parseOpenCodeHealth(await requestProviderJson(transport, 'global/health', {}, profile, 'health probe', 'compatibility', true));
58
+ return new OpenCodeClient(snapshotProfile(profile), transport, health.version, options);
59
+ }
60
+ catch (error) {
61
+ await transport.close().catch(() => undefined);
62
+ throw mapError(error, profile, 'connect', true);
63
+ }
64
+ }
65
+ class OpenCodeClient {
66
+ profile;
67
+ transport;
68
+ runtimeVersion;
69
+ options;
70
+ activeBySession = new Map();
71
+ extensionRegistry = new ExtensionRegistry(OPENCODE_PROVIDER_ID);
72
+ nativeClient;
73
+ pendingByLocalId = new Map();
74
+ pendingByProviderId = new Map();
75
+ quarantinedSessions = new Set();
76
+ unknownListeners = new Set();
77
+ closePromise;
78
+ closed = false;
79
+ interactionSerial = 0;
80
+ runSerial = 0;
81
+ constructor(profile, transport, runtimeVersion, options) {
82
+ this.profile = profile;
83
+ this.transport = transport;
84
+ this.runtimeVersion = runtimeVersion;
85
+ this.options = options;
86
+ this.nativeClient = Object.freeze({
87
+ runtimeIdentity: this.runtimeIdentity(),
88
+ request: (path, requestOptions) => this.nativeRequest(path, requestOptions),
89
+ onUnknownEvent: (listener) => {
90
+ this.unknownListeners.add(listener);
91
+ return () => this.unknownListeners.delete(listener);
92
+ },
93
+ });
94
+ }
95
+ descriptor() {
96
+ return Promise.resolve({
97
+ providerId: OPENCODE_PROVIDER_ID,
98
+ profileId: this.profile.profileId,
99
+ displayName: this.profile.displayName,
100
+ connectionKind: 'endpoint',
101
+ runtime: {
102
+ name: 'OpenCode Server',
103
+ version: this.runtimeVersion,
104
+ protocol: 'HTTP/OpenAPI + SSE',
105
+ protocolVersion: 'stable',
106
+ },
107
+ compatibility: 'supported',
108
+ });
109
+ }
110
+ capabilities() {
111
+ return Promise.resolve(this.capabilityManifest());
112
+ }
113
+ async createSession(input = {}) {
114
+ this.assertOpen();
115
+ const prepared = prepareOpenCodeSession(input);
116
+ try {
117
+ const response = await requestProviderJson(this.transport, withDirectory('session', prepared.directory), {
118
+ method: 'POST',
119
+ body: jsonBody(prepared.body),
120
+ headers: jsonHeaders,
121
+ }, this.profile, 'create Session', 'session');
122
+ const session = parseOpenCodeSession(response);
123
+ const sessionId = providerSessionId(session.id);
124
+ this.assertSessionReusable(sessionId);
125
+ return new OpenCodeSession(this, sessionId, {
126
+ directory: session.directory,
127
+ ...prepared.defaults,
128
+ });
129
+ }
130
+ catch (error) {
131
+ throw mapError(error, this.profile, 'create Session');
132
+ }
133
+ }
134
+ async resumeSession(ref) {
135
+ this.assertOpen();
136
+ assertSessionOwnership(ref, OPENCODE_PROVIDER_ID, this.profile.profileId);
137
+ assertSessionCompatibility(ref, OPENCODE_SESSION_COMPATIBILITY_REF);
138
+ this.assertSessionReusable(ref.providerSessionId);
139
+ const state = sessionStateFromRef(ref);
140
+ try {
141
+ const status = parseOpenCodeSessionStatus(await requestProviderJson(this.transport, withDirectory('session/status', state.directory), {}, this.profile, 'resume Session status probe', 'session'), ref.providerSessionId);
142
+ if (status !== 'idle')
143
+ throw sessionUnsafe(this.profile);
144
+ const response = await requestProviderJson(this.transport, withDirectory(sessionPath(ref.providerSessionId), state.directory), {}, this.profile, 'resume Session', 'session');
145
+ const session = parseOpenCodeSession(response);
146
+ if (session.id !== ref.providerSessionId ||
147
+ session.directory !== state.directory) {
148
+ throw sessionMismatch(this.profile);
149
+ }
150
+ return new OpenCodeSession(this, providerSessionId(session.id), state);
151
+ }
152
+ catch (error) {
153
+ throw mapError(error, this.profile, 'resume Session');
154
+ }
155
+ }
156
+ extensions() {
157
+ return this.extensionRegistry;
158
+ }
159
+ native(guard) {
160
+ const value = this.nativeClient;
161
+ return guard !== undefined && !guard(value) ? undefined : value;
162
+ }
163
+ close() {
164
+ this.closePromise ??= this.closeOnce();
165
+ return this.closePromise;
166
+ }
167
+ sessionRef(sessionId, state) {
168
+ return {
169
+ providerId: OPENCODE_PROVIDER_ID,
170
+ profileId: this.profile.profileId,
171
+ providerSessionId: sessionId,
172
+ compatibilityRef: OPENCODE_SESSION_COMPATIBILITY_REF,
173
+ providerState: snapshotSessionState(state),
174
+ };
175
+ }
176
+ capabilityManifest() {
177
+ return openCodeCapabilities(this.profile, this.runtimeIdentity());
178
+ }
179
+ async startRun(owner, sessionId, state, input, options = {}) {
180
+ this.assertOpen();
181
+ this.assertSessionReusable(sessionId);
182
+ if (this.activeBySession.has(sessionId)) {
183
+ throw new HarnessError('run_conflict', 'OpenCode Session already has an active Run.', {
184
+ retryable: false,
185
+ providerId: OPENCODE_PROVIDER_ID,
186
+ profileId: this.profile.profileId,
187
+ });
188
+ }
189
+ const prompt = prepareOpenCodePrompt(input, options, state);
190
+ const run = new OpenCodeRun({
191
+ providerId: OPENCODE_PROVIDER_ID,
192
+ profileId: this.profile.profileId,
193
+ sessionId,
194
+ runId: runId(`opencode-run-${String(++this.runSerial)}`),
195
+ }, owner, state.directory, prompt, options.timeoutMs, this.transport, this.profile, this.options, (activeRun, permission) => {
196
+ this.registerPermission(activeRun, permission);
197
+ }, (activeRun, permissionId) => {
198
+ this.resolvePermission(activeRun, permissionId, 'provider');
199
+ }, (event) => {
200
+ this.emitUnknown(event);
201
+ }, (activeRun) => this.abortRun(activeRun), (activeRun) => {
202
+ this.markSessionUnsafe(activeRun);
203
+ }, (activeRun) => {
204
+ this.onRunSettling(activeRun);
205
+ });
206
+ this.activeBySession.set(sessionId, run);
207
+ try {
208
+ await run.open();
209
+ return run;
210
+ }
211
+ catch (error) {
212
+ if (this.activeBySession.get(sessionId) === run) {
213
+ this.activeBySession.delete(sessionId);
214
+ }
215
+ throw mapError(error, this.profile, 'start Run');
216
+ }
217
+ }
218
+ async respond(owner, sessionId, requestId, response) {
219
+ this.assertOpen();
220
+ const pending = this.pendingByLocalId.get(requestId);
221
+ if (pending?.run.owner !== owner ||
222
+ pending.run.ref().sessionId !== sessionId ||
223
+ pending.run.isTerminal() ||
224
+ pending.claimed) {
225
+ throw invalidInteraction(this.profile);
226
+ }
227
+ const decision = permissionDecision(response);
228
+ pending.claimed = true;
229
+ try {
230
+ const accepted = await requestProviderJson(this.transport, withDirectory(`${sessionPath(sessionId)}/permissions/${encodeURIComponent(pending.permissionId)}`, pending.run.directory), {
231
+ method: 'POST',
232
+ headers: jsonHeaders,
233
+ body: jsonBody({ response: decision }),
234
+ }, this.profile, 'respond to permission', 'session');
235
+ if (accepted !== true) {
236
+ throw providerRejected(this.profile, 'permission response');
237
+ }
238
+ this.resolvePermission(pending.run, pending.permissionId, 'host');
239
+ }
240
+ catch (error) {
241
+ if (this.pendingByLocalId.get(requestId) === pending &&
242
+ !pending.run.isTerminal()) {
243
+ pending.claimed = false;
244
+ }
245
+ throw mapError(error, this.profile, 'respond to permission');
246
+ }
247
+ }
248
+ closeSession(owner, sessionId) {
249
+ const active = this.activeBySession.get(sessionId);
250
+ if (active?.owner === owner)
251
+ active.abortConnection();
252
+ }
253
+ async nativeRequest(path, options = {}) {
254
+ this.assertOpen();
255
+ const requestOptions = {
256
+ ...(options.method === undefined ? {} : { method: options.method }),
257
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
258
+ ...(options.timeoutMs === undefined
259
+ ? {}
260
+ : { timeoutMs: options.timeoutMs }),
261
+ ...(options.body === undefined
262
+ ? {}
263
+ : { body: jsonBody(options.body), headers: jsonHeaders }),
264
+ };
265
+ try {
266
+ const response = await this.transport.request(path, requestOptions);
267
+ return {
268
+ status: response.status,
269
+ body: parseJsonResponse(response, 'native response'),
270
+ };
271
+ }
272
+ catch (error) {
273
+ throw mapError(error, this.profile, 'native request');
274
+ }
275
+ }
276
+ async abortRun(run) {
277
+ this.assertOpen();
278
+ try {
279
+ const response = await requestProviderJson(this.transport, withDirectory(`${sessionPath(run.ref().sessionId)}/abort`, run.directory), { method: 'POST' }, this.profile, 'abort Run', 'session');
280
+ return response === true;
281
+ }
282
+ catch (error) {
283
+ throw mapError(error, this.profile, 'abort Run');
284
+ }
285
+ }
286
+ registerPermission(run, permission) {
287
+ const providerKey = permissionKey(run.ref().sessionId, permission.permissionId);
288
+ if (this.pendingByProviderId.has(providerKey))
289
+ return;
290
+ const localId = `opencode-interaction-${String(++this.interactionSerial)}`;
291
+ const pending = {
292
+ claimed: false,
293
+ localId,
294
+ permissionId: permission.permissionId,
295
+ run,
296
+ };
297
+ this.pendingByLocalId.set(localId, pending);
298
+ this.pendingByProviderId.set(providerKey, pending);
299
+ run.emit({
300
+ type: 'interaction.requested',
301
+ data: {
302
+ requestId: localId,
303
+ kind: 'approval',
304
+ title: permission.title,
305
+ prompt: permissionPrompt(permission),
306
+ },
307
+ });
308
+ }
309
+ resolvePermission(run, permissionId, resolution) {
310
+ const key = permissionKey(run.ref().sessionId, permissionId);
311
+ const pending = this.pendingByProviderId.get(key);
312
+ if (pending?.run !== run)
313
+ return;
314
+ this.pendingByProviderId.delete(key);
315
+ this.pendingByLocalId.delete(pending.localId);
316
+ run.emit({
317
+ type: 'interaction.resolved',
318
+ data: { requestId: pending.localId, resolution },
319
+ });
320
+ }
321
+ onRunSettling(run) {
322
+ const sessionId = run.ref().sessionId;
323
+ if (this.activeBySession.get(sessionId) === run) {
324
+ this.activeBySession.delete(sessionId);
325
+ }
326
+ for (const pending of [...this.pendingByLocalId.values()]) {
327
+ if (pending.run !== run)
328
+ continue;
329
+ this.pendingByLocalId.delete(pending.localId);
330
+ this.pendingByProviderId.delete(permissionKey(sessionId, pending.permissionId));
331
+ run.emitSettlement({
332
+ type: 'interaction.resolved',
333
+ data: { requestId: pending.localId, resolution: 'terminal' },
334
+ });
335
+ }
336
+ }
337
+ emitUnknown(event) {
338
+ for (const listener of this.unknownListeners) {
339
+ try {
340
+ listener(structuredClone(event));
341
+ }
342
+ catch {
343
+ // Host observers cannot alter Provider lifecycle settlement.
344
+ }
345
+ }
346
+ }
347
+ markSessionUnsafe(run) {
348
+ const sessionId = run.ref().sessionId;
349
+ this.quarantinedSessions.add(sessionId);
350
+ run.owner.markUnsafe();
351
+ }
352
+ assertSessionReusable(sessionId) {
353
+ if (!this.quarantinedSessions.has(sessionId))
354
+ return;
355
+ throw sessionUnsafe(this.profile);
356
+ }
357
+ runtimeIdentity() {
358
+ return `${OPENCODE_SESSION_COMPATIBILITY_REF};runtime=${this.runtimeVersion}`;
359
+ }
360
+ assertOpen() {
361
+ if (!this.closed && this.transport.isOpen())
362
+ return;
363
+ throw new HarnessError('connection_aborted', 'The OpenCode Client connection is closed.', {
364
+ retryable: false,
365
+ providerId: OPENCODE_PROVIDER_ID,
366
+ profileId: this.profile.profileId,
367
+ });
368
+ }
369
+ async closeOnce() {
370
+ if (this.closed)
371
+ return;
372
+ this.closed = true;
373
+ for (const run of [...this.activeBySession.values()])
374
+ run.abortConnection();
375
+ this.pendingByLocalId.clear();
376
+ this.pendingByProviderId.clear();
377
+ try {
378
+ await this.transport.close();
379
+ }
380
+ catch (error) {
381
+ throw mapError(error, this.profile, 'close connection');
382
+ }
383
+ }
384
+ }
385
+ class OpenCodeSession {
386
+ client;
387
+ sessionId;
388
+ state;
389
+ closed = false;
390
+ unsafe = false;
391
+ constructor(client, sessionId, state) {
392
+ this.client = client;
393
+ this.sessionId = sessionId;
394
+ this.state = state;
395
+ }
396
+ ref() {
397
+ return this.client.sessionRef(this.sessionId, this.state);
398
+ }
399
+ capabilities() {
400
+ return Promise.resolve(this.client.capabilityManifest());
401
+ }
402
+ start(input, options = {}) {
403
+ this.assertOpen();
404
+ return this.client.startRun(this, this.sessionId, this.state, input, options);
405
+ }
406
+ respond(requestId, response) {
407
+ this.assertOpen();
408
+ return this.client.respond(this, this.sessionId, requestId, response);
409
+ }
410
+ close() {
411
+ if (!this.closed) {
412
+ this.closed = true;
413
+ this.client.closeSession(this, this.sessionId);
414
+ }
415
+ return Promise.resolve();
416
+ }
417
+ markUnsafe() {
418
+ this.unsafe = true;
419
+ }
420
+ assertOpen() {
421
+ if (!this.closed && !this.unsafe)
422
+ return;
423
+ throw new HarnessError('connection_aborted', 'The OpenCode Session is closed.', {
424
+ retryable: false,
425
+ providerId: OPENCODE_PROVIDER_ID,
426
+ });
427
+ }
428
+ }
429
+ class OpenCodeRun {
430
+ reference;
431
+ owner;
432
+ directory;
433
+ prompt;
434
+ timeoutMs;
435
+ transport;
436
+ profile;
437
+ options;
438
+ onPermission;
439
+ onPermissionResolved;
440
+ onUnknown;
441
+ nativeAbort;
442
+ onUnsafe;
443
+ onSettling;
444
+ controller = new AbortController();
445
+ eventQueue;
446
+ eventState = createOpenCodeEventState();
447
+ idleSeen;
448
+ providerRunStarted;
449
+ settlement;
450
+ cancelPromise;
451
+ finalResult;
452
+ idleResolve;
453
+ providerRunStartedResolve;
454
+ resolveSettlement;
455
+ sequence = 0;
456
+ timeout;
457
+ timeoutTriggered = false;
458
+ constructor(reference, owner, directory, prompt, timeoutMs, transport, profile, options, onPermission, onPermissionResolved, onUnknown, nativeAbort, onUnsafe, onSettling) {
459
+ this.reference = reference;
460
+ this.owner = owner;
461
+ this.directory = directory;
462
+ this.prompt = prompt;
463
+ this.timeoutMs = timeoutMs;
464
+ this.transport = transport;
465
+ this.profile = profile;
466
+ this.options = options;
467
+ this.onPermission = onPermission;
468
+ this.onPermissionResolved = onPermissionResolved;
469
+ this.onUnknown = onUnknown;
470
+ this.nativeAbort = nativeAbort;
471
+ this.onUnsafe = onUnsafe;
472
+ this.onSettling = onSettling;
473
+ this.eventQueue = new EventQueue(options.maxRunEvents);
474
+ this.settlement = new Promise((resolve) => {
475
+ this.resolveSettlement = resolve;
476
+ });
477
+ this.idleSeen = new Promise((resolve) => {
478
+ this.idleResolve = resolve;
479
+ });
480
+ this.providerRunStarted = new Promise((resolve) => {
481
+ this.providerRunStartedResolve = resolve;
482
+ });
483
+ }
484
+ async open() {
485
+ const iterable = this.transport.subscribe(withDirectory('event', this.directory), { signal: this.controller.signal });
486
+ const iterator = iterable[Symbol.asyncIterator]();
487
+ let first;
488
+ try {
489
+ first = await iterator.next();
490
+ if (first.done) {
491
+ throw providerIncompatible(this.profile, 'SSE connection event');
492
+ }
493
+ validateSseDispatch(first.value.event);
494
+ const connected = parseOpenCodeEvent(first.value.data);
495
+ if (connected.type !== 'server.connected') {
496
+ throw providerIncompatible(this.profile, 'SSE connection event');
497
+ }
498
+ }
499
+ catch (error) {
500
+ await iterator.return?.().catch(() => undefined);
501
+ if (error instanceof HttpTransportError &&
502
+ error.code === 'stream_ended') {
503
+ throw providerIncompatible(this.profile, 'SSE connection event');
504
+ }
505
+ throw error;
506
+ }
507
+ this.emit({ type: 'run.started', data: {} });
508
+ void this.pump(iterator).catch(() => undefined);
509
+ void this.executePrompt();
510
+ if (this.timeoutMs !== undefined) {
511
+ this.timeout = setTimeout(() => {
512
+ this.timeoutTriggered = true;
513
+ void this.cancel().catch(() => {
514
+ this.abortConnection();
515
+ });
516
+ }, this.timeoutMs);
517
+ this.timeout.unref();
518
+ }
519
+ }
520
+ ref() {
521
+ return { ...this.reference };
522
+ }
523
+ events() {
524
+ return this.eventQueue.iterable();
525
+ }
526
+ cancel() {
527
+ if (this.isTerminal())
528
+ return Promise.resolve({ mode: 'already_terminal' });
529
+ this.cancelPromise ??= this.cancelOnce();
530
+ return this.cancelPromise;
531
+ }
532
+ result() {
533
+ return this.settlement;
534
+ }
535
+ isTerminal() {
536
+ return this.finalResult !== undefined;
537
+ }
538
+ emit(mapped) {
539
+ if (this.isTerminal())
540
+ return;
541
+ if (!this.eventQueue.push(this.portableEvent(mapped)))
542
+ this.abortConnection();
543
+ }
544
+ emitSettlement(mapped) {
545
+ if (this.isTerminal())
546
+ return;
547
+ this.eventQueue.pushTerminal(this.portableEvent(mapped));
548
+ }
549
+ abortConnection() {
550
+ if (this.isTerminal())
551
+ return;
552
+ this.onUnsafe(this);
553
+ this.finish({ status: 'connection_aborted' }, 'connection.aborted');
554
+ }
555
+ async pump(iterator) {
556
+ try {
557
+ for (;;) {
558
+ const next = await iterator.next();
559
+ if (next.done)
560
+ break;
561
+ validateSseDispatch(next.value.event);
562
+ const event = parseOpenCodeEvent(next.value.data);
563
+ if (event.type === 'message.updated' &&
564
+ (event.properties['sessionID'] === this.reference.sessionId ||
565
+ runtimeRecord(event.properties['info'])?.['sessionID'] ===
566
+ this.reference.sessionId)) {
567
+ this.providerRunStartedResolve();
568
+ }
569
+ const mapping = mapOpenCodeEvent(event, this.reference.sessionId, this.eventState);
570
+ if (event.type === 'session.idle' &&
571
+ event.properties['sessionID'] === this.reference.sessionId) {
572
+ this.idleResolve();
573
+ }
574
+ if (mapping.permission !== undefined) {
575
+ this.onPermission(this, mapping.permission);
576
+ }
577
+ if (mapping.resolvedPermissionId !== undefined) {
578
+ this.onPermissionResolved(this, mapping.resolvedPermissionId);
579
+ }
580
+ for (const mapped of mapping.events)
581
+ this.emit(mapped);
582
+ if (!mapping.routed)
583
+ this.onUnknown(redactOpenCodeEvent(event));
584
+ }
585
+ if (!this.isTerminal())
586
+ this.abortConnection();
587
+ }
588
+ catch (error) {
589
+ if (this.isTerminal())
590
+ return;
591
+ if (error instanceof HarnessError) {
592
+ this.onUnsafe(this);
593
+ this.fail(error);
594
+ }
595
+ else {
596
+ const mapped = mapError(error, this.profile, 'consume event stream');
597
+ if (mapped.code === 'provider_api_incompatible') {
598
+ this.onUnsafe(this);
599
+ this.fail(mapped);
600
+ }
601
+ else
602
+ this.abortConnection();
603
+ }
604
+ }
605
+ }
606
+ async executePrompt() {
607
+ try {
608
+ const response = await requestProviderJson(this.transport, withDirectory(`${sessionPath(this.reference.sessionId)}/message`, this.directory), {
609
+ method: 'POST',
610
+ headers: jsonHeaders,
611
+ body: jsonBody(this.prompt),
612
+ signal: this.controller.signal,
613
+ timeoutMs: this.options.runRequestTimeoutMs,
614
+ }, this.profile, 'execute Run', 'session');
615
+ const terminal = parseOpenCodePromptResponse(response, this.reference.sessionId);
616
+ await Promise.race([
617
+ this.idleSeen,
618
+ boundedDelay(this.options.eventDrainTimeoutMs),
619
+ ]);
620
+ if (this.isTerminal())
621
+ return;
622
+ if (terminal.result.status === 'completed') {
623
+ if (terminal.finalMessage !== undefined) {
624
+ this.emit({
625
+ type: 'message.completed',
626
+ data: { message: terminal.finalMessage },
627
+ });
628
+ }
629
+ this.emit({
630
+ type: 'usage.updated',
631
+ data: terminal.usage,
632
+ usage: terminal.usage,
633
+ });
634
+ }
635
+ const result = this.timeoutTriggered && terminal.result.status === 'cancelled'
636
+ ? {
637
+ ...terminal.result,
638
+ providerResult: {
639
+ ...terminal.providerResult,
640
+ reason: 'timeout',
641
+ },
642
+ }
643
+ : terminal.result;
644
+ this.finish(result, terminalEventType(result.status));
645
+ }
646
+ catch (error) {
647
+ if (this.isTerminal())
648
+ return;
649
+ const mapped = mapError(error, this.profile, 'execute Run');
650
+ if (mapped.code === 'connection_aborted')
651
+ this.abortConnection();
652
+ else {
653
+ if (uncertainRequestFailure(mapped))
654
+ this.onUnsafe(this);
655
+ this.fail(mapped);
656
+ }
657
+ }
658
+ }
659
+ async cancelOnce() {
660
+ await Promise.race([
661
+ this.providerRunStarted,
662
+ this.settlement.then(() => undefined),
663
+ boundedDelay(this.options.eventDrainTimeoutMs),
664
+ ]);
665
+ if (this.isTerminal())
666
+ return { mode: 'already_terminal' };
667
+ const acknowledged = await this.nativeAbort(this);
668
+ if (!acknowledged) {
669
+ if (this.isTerminal())
670
+ return { mode: 'already_terminal' };
671
+ throw providerRejected(this.profile, 'Run abort');
672
+ }
673
+ const outcome = await Promise.race([
674
+ this.settlement,
675
+ boundedDelay(this.options.cancelSettlementTimeoutMs).then(() => undefined),
676
+ ]);
677
+ if (outcome === undefined) {
678
+ this.abortConnection();
679
+ return { mode: 'connection_aborted' };
680
+ }
681
+ if (outcome.status === 'cancelled')
682
+ return { mode: 'native' };
683
+ return outcome.status === 'connection_aborted'
684
+ ? { mode: 'connection_aborted' }
685
+ : { mode: 'already_terminal' };
686
+ }
687
+ fail(error) {
688
+ const result = {
689
+ status: 'failed',
690
+ providerResult: {
691
+ error: error.code,
692
+ ...(error.providerCode === undefined
693
+ ? {}
694
+ : { providerCode: error.providerCode }),
695
+ },
696
+ };
697
+ this.finish(result, 'run.failed');
698
+ }
699
+ finish(result, type) {
700
+ if (this.isTerminal())
701
+ return;
702
+ this.onSettling(this);
703
+ this.finalResult = result;
704
+ if (this.timeout !== undefined)
705
+ clearTimeout(this.timeout);
706
+ this.controller.abort();
707
+ this.eventQueue.pushTerminal(this.portableEvent({ type, data: result }));
708
+ this.eventQueue.close();
709
+ this.resolveSettlement(result);
710
+ }
711
+ portableEvent(mapped) {
712
+ const sequence = this.sequence++;
713
+ return {
714
+ id: `${this.reference.runId}:event:${String(sequence)}`,
715
+ type: mapped.type,
716
+ providerId: this.reference.providerId,
717
+ profileId: this.reference.profileId,
718
+ sessionId: this.reference.sessionId,
719
+ runId: this.reference.runId,
720
+ sequence,
721
+ timestamp: new Date().toISOString(),
722
+ data: mapped.data,
723
+ ...(mapped.providerEventType === undefined
724
+ ? {}
725
+ : { providerEventType: mapped.providerEventType }),
726
+ ...(mapped.raw === undefined ? {} : { raw: mapped.raw }),
727
+ };
728
+ }
729
+ }
730
+ class EventQueue {
731
+ capacity;
732
+ values = [];
733
+ closed = false;
734
+ consumed = false;
735
+ waiter;
736
+ constructor(capacity) {
737
+ this.capacity = capacity;
738
+ }
739
+ push(event) {
740
+ if (this.closed)
741
+ return false;
742
+ if (this.waiter !== undefined) {
743
+ const waiter = this.waiter;
744
+ this.waiter = undefined;
745
+ waiter({ done: false, value: event });
746
+ return true;
747
+ }
748
+ if (this.values.length >= this.capacity - 1)
749
+ return false;
750
+ this.values.push(event);
751
+ return true;
752
+ }
753
+ pushTerminal(event) {
754
+ if (this.waiter !== undefined) {
755
+ const waiter = this.waiter;
756
+ this.waiter = undefined;
757
+ waiter({ done: false, value: event });
758
+ return;
759
+ }
760
+ this.values.push(event);
761
+ }
762
+ close() {
763
+ this.closed = true;
764
+ }
765
+ iterable() {
766
+ if (this.consumed) {
767
+ return {
768
+ [Symbol.asyncIterator]: () => ({
769
+ next: () => Promise.reject(new HarnessError('run_conflict', 'OpenCode Run events already have a consumer.', { retryable: false, providerId: OPENCODE_PROVIDER_ID })),
770
+ }),
771
+ };
772
+ }
773
+ this.consumed = true;
774
+ return {
775
+ [Symbol.asyncIterator]: () => ({ next: () => this.next() }),
776
+ };
777
+ }
778
+ next() {
779
+ const event = this.values.shift();
780
+ if (event !== undefined)
781
+ return Promise.resolve({ done: false, value: event });
782
+ if (this.closed)
783
+ return Promise.resolve({ done: true, value: undefined });
784
+ if (this.waiter !== undefined) {
785
+ return Promise.reject(new HarnessError('run_conflict', 'An OpenCode event read is already pending.', { retryable: false, providerId: OPENCODE_PROVIDER_ID }));
786
+ }
787
+ return new Promise((resolve) => {
788
+ this.waiter = resolve;
789
+ });
790
+ }
791
+ }
792
+ const jsonHeaders = { 'content-type': 'application/json' };
793
+ function openCodeCapabilities(profile, runtimeIdentity) {
794
+ const native = { mode: 'native', source: 'schema' };
795
+ return {
796
+ providerId: OPENCODE_PROVIDER_ID,
797
+ profileId: profile.profileId,
798
+ observedAt: new Date().toISOString(),
799
+ runtimeIdentity,
800
+ capabilities: {
801
+ 'session.create': native,
802
+ 'session.resume': native,
803
+ 'session.close': {
804
+ mode: 'adapter_controlled',
805
+ source: 'configuration',
806
+ reason: 'Portable close releases only the local Session handle.',
807
+ },
808
+ 'session.workspace': native,
809
+ 'run.stream': native,
810
+ 'run.cancel': native,
811
+ 'run.timeout': {
812
+ mode: 'emulated',
813
+ source: 'configuration',
814
+ reason: 'A local timer invokes the documented native abort route.',
815
+ },
816
+ 'run.concurrent': {
817
+ mode: 'unsupported',
818
+ source: 'configuration',
819
+ limits: { perSession: 1 },
820
+ },
821
+ 'interaction.approval': native,
822
+ 'input.text': native,
823
+ 'input.file': native,
824
+ 'input.image': native,
825
+ 'unknown_event.raw': {
826
+ mode: 'adapter_controlled',
827
+ source: 'configuration',
828
+ },
829
+ 'native.client': native,
830
+ },
831
+ };
832
+ }
833
+ function connectionOptions(value) {
834
+ const options = value === undefined ? {} : runtimeRecord(value);
835
+ if (options === undefined) {
836
+ throw profileInvalid('OpenCode providerOptions must be an object.');
837
+ }
838
+ const allowed = new Set([
839
+ 'cancelSettlementTimeoutMs',
840
+ 'eventDrainTimeoutMs',
841
+ 'maxRunEvents',
842
+ 'requestTimeoutMs',
843
+ 'runRequestTimeoutMs',
844
+ 'sseConnectTimeoutMs',
845
+ ]);
846
+ const unknown = Object.keys(options).find((name) => !allowed.has(name));
847
+ if (unknown !== undefined) {
848
+ throw profileInvalid(`OpenCode Profile option ${unknown} is unknown.`);
849
+ }
850
+ const maxRunEvents = profileInteger(options['maxRunEvents'], defaultMaxRunEvents, 'maxRunEvents');
851
+ if (maxRunEvents < 2 || maxRunEvents > maximumRunEventCapacity) {
852
+ throw profileInvalid('OpenCode maxRunEvents must be between 2 and the supported upper bound.');
853
+ }
854
+ return {
855
+ maxRunEvents,
856
+ cancelSettlementTimeoutMs: profileTimer(options['cancelSettlementTimeoutMs'], defaultCancelSettlementTimeoutMs, 'cancelSettlementTimeoutMs'),
857
+ eventDrainTimeoutMs: profileTimer(options['eventDrainTimeoutMs'], defaultEventDrainTimeoutMs, 'eventDrainTimeoutMs'),
858
+ runRequestTimeoutMs: profileTimer(options['runRequestTimeoutMs'], defaultRunRequestTimeoutMs, 'runRequestTimeoutMs'),
859
+ requestTimeoutMs: options['requestTimeoutMs'] === undefined
860
+ ? undefined
861
+ : profileTimer(options['requestTimeoutMs'], 0, 'requestTimeoutMs'),
862
+ sseConnectTimeoutMs: options['sseConnectTimeoutMs'] === undefined
863
+ ? undefined
864
+ : profileTimer(options['sseConnectTimeoutMs'], 0, 'sseConnectTimeoutMs'),
865
+ };
866
+ }
867
+ function validateProfile(profile, factoryOptions) {
868
+ if (profile.providerId !== OPENCODE_PROVIDER_ID ||
869
+ profile.connection.kind !== 'endpoint' ||
870
+ (profile.connection.transport !== undefined &&
871
+ profile.connection.transport !== 'http') ||
872
+ profile.connection.url.length === 0) {
873
+ throw profileInvalid('OpenCode requires an HTTP endpoint Profile owned by the host or an external service.', profile);
874
+ }
875
+ let endpoint;
876
+ try {
877
+ endpoint = new URL(profile.connection.url);
878
+ }
879
+ catch {
880
+ throw profileInvalid('OpenCode endpoint URL must be absolute.', profile);
881
+ }
882
+ if (endpoint.protocol !== 'http:' && endpoint.protocol !== 'https:') {
883
+ throw profileInvalid('OpenCode endpoint URL must use HTTP or HTTPS.', profile);
884
+ }
885
+ if (profile.connection.authRef !== undefined &&
886
+ factoryOptions.resolveAuthHeaders === undefined) {
887
+ throw profileInvalid('OpenCode authRef requires a host-provided authentication-header resolver.', profile);
888
+ }
889
+ }
890
+ async function resolveHeaders(profile, factoryOptions) {
891
+ if (profile.connection.kind !== 'endpoint')
892
+ throw profileInvalid(undefined, profile);
893
+ const reference = profile.connection.authRef;
894
+ if (reference === undefined)
895
+ return undefined;
896
+ try {
897
+ const headers = await factoryOptions.resolveAuthHeaders?.(reference);
898
+ if (headers === undefined ||
899
+ runtimeRecord(headers) === undefined ||
900
+ Object.values(headers).some((value) => typeof value !== 'string')) {
901
+ throw new TypeError('invalid authentication headers');
902
+ }
903
+ return headers;
904
+ }
905
+ catch {
906
+ throw new HarnessError('authentication_failed', 'The host could not resolve OpenCode authentication headers.', {
907
+ retryable: false,
908
+ providerId: OPENCODE_PROVIDER_ID,
909
+ profileId: profile.profileId,
910
+ });
911
+ }
912
+ }
913
+ async function requestProviderJson(transport, path, options, profile, phase, notFound, connecting = false) {
914
+ let response;
915
+ try {
916
+ response = await transport.request(path, options);
917
+ }
918
+ catch (error) {
919
+ throw mapError(error, profile, phase, connecting);
920
+ }
921
+ if (response.status < 200 || response.status >= 300) {
922
+ throw responseError(response.status, profile, phase, notFound);
923
+ }
924
+ return parseJsonResponse(response, phase);
925
+ }
926
+ function parseJsonResponse(response, phase) {
927
+ if (!response.contentType?.toLowerCase().startsWith('application/json')) {
928
+ throw providerIncompatible(undefined, `${phase} Content-Type`);
929
+ }
930
+ let text;
931
+ try {
932
+ text = new TextDecoder('utf-8', { fatal: true }).decode(response.body);
933
+ }
934
+ catch {
935
+ throw providerIncompatible(undefined, `${phase} encoding`);
936
+ }
937
+ try {
938
+ return JSON.parse(text);
939
+ }
940
+ catch {
941
+ throw providerIncompatible(undefined, `${phase} JSON`);
942
+ }
943
+ }
944
+ function responseError(status, profile, phase, notFound) {
945
+ if (status === 401 || status === 403) {
946
+ return new HarnessError('authentication_failed', `OpenCode rejected ${phase} authentication.`, {
947
+ retryable: false,
948
+ providerId: OPENCODE_PROVIDER_ID,
949
+ profileId: profile.profileId,
950
+ providerCode: String(status),
951
+ });
952
+ }
953
+ if (status === 404) {
954
+ return new HarnessError(notFound === 'session'
955
+ ? 'session_not_found'
956
+ : 'provider_api_incompatible', notFound === 'session'
957
+ ? `OpenCode could not find the Session for ${phase}.`
958
+ : 'The OpenCode endpoint does not expose the required stable interface.', {
959
+ retryable: false,
960
+ providerId: OPENCODE_PROVIDER_ID,
961
+ profileId: profile.profileId,
962
+ providerCode: String(status),
963
+ });
964
+ }
965
+ const code = status === 400
966
+ ? 'invalid_request'
967
+ : status === 409
968
+ ? 'run_conflict'
969
+ : 'provider_error';
970
+ return new HarnessError(code, `OpenCode rejected ${phase}.`, {
971
+ retryable: status >= 500,
972
+ providerId: OPENCODE_PROVIDER_ID,
973
+ profileId: profile.profileId,
974
+ providerCode: String(status),
975
+ });
976
+ }
977
+ function mapError(error, profile, phase, connecting = false) {
978
+ if (error instanceof HarnessError)
979
+ return error;
980
+ if (error instanceof HttpTransportError) {
981
+ if (error.code === 'http_status' && error.status !== undefined) {
982
+ return responseError(error.status, profile, phase, 'compatibility');
983
+ }
984
+ const code = error.code === 'request_timeout'
985
+ ? 'timeout'
986
+ : error.code === 'transport_closed'
987
+ ? 'connection_aborted'
988
+ : connecting && error.code === 'network_failure'
989
+ ? 'connection_failed'
990
+ : 'provider_error';
991
+ return new HarnessError(code, `OpenCode ${phase} did not complete.`, {
992
+ retryable: error.code === 'request_timeout' ||
993
+ error.code === 'capacity_exceeded' ||
994
+ error.code === 'network_failure',
995
+ providerId: OPENCODE_PROVIDER_ID,
996
+ profileId: profile.profileId,
997
+ providerCode: error.code,
998
+ });
999
+ }
1000
+ return new HarnessError(connecting ? 'connection_failed' : 'provider_error', `OpenCode ${phase} failed.`, {
1001
+ retryable: false,
1002
+ providerId: OPENCODE_PROVIDER_ID,
1003
+ profileId: profile.profileId,
1004
+ });
1005
+ }
1006
+ function uncertainRequestFailure(error) {
1007
+ return (error.code === 'timeout' ||
1008
+ (error.providerCode !== undefined &&
1009
+ uncertainRequestProviderCodes.has(error.providerCode)));
1010
+ }
1011
+ function jsonBody(value) {
1012
+ try {
1013
+ const body = JSON.stringify(value);
1014
+ if (typeof body !== 'string')
1015
+ throw new TypeError('undefined JSON');
1016
+ return body;
1017
+ }
1018
+ catch {
1019
+ throw new HarnessError('invalid_request', 'OpenCode request data must be JSON serializable.', { retryable: false, providerId: OPENCODE_PROVIDER_ID });
1020
+ }
1021
+ }
1022
+ function permissionDecision(response) {
1023
+ if (response.kind === 'provider') {
1024
+ const value = runtimeRecord(response.value);
1025
+ const decision = value?.['response'];
1026
+ if (decision === 'always' || decision === 'once' || decision === 'reject') {
1027
+ return decision;
1028
+ }
1029
+ throw invalidInteraction();
1030
+ }
1031
+ if (response.kind !== 'approval')
1032
+ throw invalidInteraction();
1033
+ if (response.decision === 'deny')
1034
+ return 'reject';
1035
+ if (response.providerOptions === undefined)
1036
+ return 'once';
1037
+ const options = runtimeRecord(response.providerOptions);
1038
+ if (options !== undefined &&
1039
+ Object.keys(options).length === 1 &&
1040
+ options['scope'] === 'always') {
1041
+ return 'always';
1042
+ }
1043
+ throw invalidInteraction();
1044
+ }
1045
+ function validateSseDispatch(event) {
1046
+ if (event !== undefined && event !== 'message') {
1047
+ throw providerIncompatible(undefined, 'SSE event name');
1048
+ }
1049
+ }
1050
+ function terminalEventType(status) {
1051
+ const mapping = {
1052
+ completed: 'run.completed',
1053
+ cancelled: 'run.cancelled',
1054
+ failed: 'run.failed',
1055
+ connection_aborted: 'connection.aborted',
1056
+ };
1057
+ return mapping[status];
1058
+ }
1059
+ function withDirectory(path, directory) {
1060
+ return directory === undefined
1061
+ ? path
1062
+ : `${path}?directory=${encodeURIComponent(directory)}`;
1063
+ }
1064
+ function sessionPath(sessionId) {
1065
+ return `session/${encodeURIComponent(sessionId)}`;
1066
+ }
1067
+ function permissionKey(sessionId, permissionId) {
1068
+ return `${sessionId}\u0000${permissionId}`;
1069
+ }
1070
+ function permissionPrompt(permission) {
1071
+ if (typeof permission.pattern === 'string')
1072
+ return permission.pattern;
1073
+ if (permission.pattern !== undefined)
1074
+ return permission.pattern.join('\n');
1075
+ return `OpenCode requests ${permission.type} permission.`;
1076
+ }
1077
+ function snapshotSessionState(state) {
1078
+ return {
1079
+ directory: state.directory,
1080
+ ...(state.system === undefined ? {} : { system: state.system }),
1081
+ ...(state.model === undefined
1082
+ ? {}
1083
+ : {
1084
+ model: {
1085
+ providerId: state.model.providerId,
1086
+ modelId: state.model.modelId,
1087
+ },
1088
+ }),
1089
+ };
1090
+ }
1091
+ function snapshotProfile(profile) {
1092
+ return {
1093
+ ...profile,
1094
+ connection: {
1095
+ ...profile.connection,
1096
+ ...(profile.connection.kind === 'endpoint' &&
1097
+ profile.connection.authRef !== undefined
1098
+ ? { authRef: { ...profile.connection.authRef } }
1099
+ : {}),
1100
+ },
1101
+ ...(profile.providerOptions === undefined
1102
+ ? {}
1103
+ : { providerOptions: { ...profile.providerOptions } }),
1104
+ };
1105
+ }
1106
+ function endpointUrl(profile) {
1107
+ if (profile.connection.kind !== 'endpoint')
1108
+ throw profileInvalid(undefined, profile);
1109
+ return profile.connection.url;
1110
+ }
1111
+ function profileInteger(value, fallback, label) {
1112
+ if (value === undefined)
1113
+ return fallback;
1114
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
1115
+ throw profileInvalid(`OpenCode ${label} must be a positive integer.`);
1116
+ }
1117
+ return value;
1118
+ }
1119
+ function profileTimer(value, fallback, label) {
1120
+ const timer = profileInteger(value, fallback, label);
1121
+ if (timer > maximumTimerMilliseconds) {
1122
+ throw profileInvalid(`OpenCode ${label} exceeds the supported timer range.`);
1123
+ }
1124
+ return timer;
1125
+ }
1126
+ function boundedDelay(milliseconds) {
1127
+ return new Promise((resolve) => {
1128
+ const timer = setTimeout(resolve, milliseconds);
1129
+ timer.unref();
1130
+ });
1131
+ }
1132
+ function runtimeRecord(value) {
1133
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
1134
+ ? value
1135
+ : undefined;
1136
+ }
1137
+ function profileInvalid(message = 'The OpenCode Profile is invalid.', profile) {
1138
+ return new HarnessError('profile_invalid', message, {
1139
+ retryable: false,
1140
+ providerId: OPENCODE_PROVIDER_ID,
1141
+ ...(profile === undefined ? {} : { profileId: profile.profileId }),
1142
+ });
1143
+ }
1144
+ function providerIncompatible(profile, surface) {
1145
+ return new HarnessError('provider_api_incompatible', `OpenCode returned an incompatible ${surface}.`, {
1146
+ retryable: false,
1147
+ providerId: OPENCODE_PROVIDER_ID,
1148
+ ...(profile === undefined ? {} : { profileId: profile.profileId }),
1149
+ });
1150
+ }
1151
+ function providerRejected(profile, operation) {
1152
+ return new HarnessError('provider_error', `OpenCode did not acknowledge ${operation}.`, {
1153
+ retryable: false,
1154
+ providerId: OPENCODE_PROVIDER_ID,
1155
+ profileId: profile.profileId,
1156
+ });
1157
+ }
1158
+ function invalidInteraction(profile) {
1159
+ return new HarnessError('invalid_request', 'The OpenCode interaction response is invalid or no longer pending.', {
1160
+ retryable: false,
1161
+ providerId: OPENCODE_PROVIDER_ID,
1162
+ ...(profile === undefined ? {} : { profileId: profile.profileId }),
1163
+ });
1164
+ }
1165
+ function sessionMismatch(profile) {
1166
+ return new HarnessError('session_provider_mismatch', 'OpenCode returned a different Session or workspace instance.', {
1167
+ retryable: false,
1168
+ providerId: OPENCODE_PROVIDER_ID,
1169
+ profileId: profile.profileId,
1170
+ });
1171
+ }
1172
+ function sessionUnsafe(profile) {
1173
+ return new HarnessError('connection_aborted', 'The OpenCode Session cannot be reused after uncertain remote settlement.', {
1174
+ retryable: false,
1175
+ providerId: OPENCODE_PROVIDER_ID,
1176
+ profileId: profile.profileId,
1177
+ });
1178
+ }
1179
+ //# sourceMappingURL=adapter.js.map