@ai-sdk/harness-opencode 1.0.41 → 1.0.43

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/dist/index.js CHANGED
@@ -168,7 +168,7 @@ var inboundMessageSchema = z.discriminatedUnion("type", [
168
168
  ]);
169
169
 
170
170
  // src/version.ts
171
- var VERSION = true ? "1.0.41" : "0.0.0-test";
171
+ var VERSION = true ? "1.0.43" : "0.0.0-test";
172
172
 
173
173
  // src/opencode-harness.ts
174
174
  var OPENCODE_CLIENT_APP = `ai-sdk/harness-opencode/${VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-opencode",
3
- "version": "1.0.41",
3
+ "version": "1.0.43",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "ws": "8.21.0",
30
- "@ai-sdk/harness": "1.0.40",
30
+ "@ai-sdk/harness": "1.0.42",
31
31
  "@ai-sdk/provider-utils": "5.0.12"
32
32
  },
33
33
  "peerDependencies": {
@@ -0,0 +1,512 @@
1
+ import type { BridgeTurn } from '@ai-sdk/harness/bridge';
2
+ import {
3
+ emitLegacyPartDelta,
4
+ emitLegacyTextPartUpdate,
5
+ emitMissingFinalDelta,
6
+ type OpenCodeEvent,
7
+ type TranslationState,
8
+ } from './opencode-events';
9
+ import {
10
+ legacyStepFinishPartToFinishStep,
11
+ mapOpenCodeFinishReason,
12
+ } from './opencode-finish-step';
13
+ import { mapUsage } from './opencode-usage';
14
+
15
+ type Emit = (message: Record<string, unknown>) => void;
16
+
17
+ export function createEmitStreamEvent({
18
+ state,
19
+ emit,
20
+ emitWarning,
21
+ emitError,
22
+ toWireToolName,
23
+ nativeNameField,
24
+ getHostToolName,
25
+ authorizeHostToolCall,
26
+ stripWorkDir,
27
+ formatError,
28
+ }: {
29
+ state: TranslationState;
30
+ emit: Emit;
31
+ emitWarning: BridgeTurn['emitWarning'];
32
+ emitError: BridgeTurn['emitError'];
33
+ toWireToolName: (nativeName: string) => string;
34
+ nativeNameField: (input: { nativeName: string; toolName: string }) => {
35
+ nativeName?: string;
36
+ };
37
+ getHostToolName: (
38
+ toolName: string,
39
+ rawToolName: unknown,
40
+ ) => string | undefined;
41
+ authorizeHostToolCall: (input: {
42
+ callID: string;
43
+ toolName: string;
44
+ input: unknown;
45
+ }) => void;
46
+ stripWorkDir: (file: string) => string;
47
+ formatError: (error: unknown) => string;
48
+ }): (event: OpenCodeEvent) => void {
49
+ return event => {
50
+ const type = event.type;
51
+ const props = event.properties ?? {};
52
+
53
+ if (type === 'message.updated') {
54
+ const info = props.info;
55
+ if (isRecord(info)) {
56
+ const id = stringValue(info.id);
57
+ const role = stringValue(info.role);
58
+ if (id && role) state.messageRoles.set(id, role);
59
+ }
60
+ return;
61
+ }
62
+
63
+ if (type === 'message.part.delta') {
64
+ emitLegacyPartDelta({ props, state, emit });
65
+ return;
66
+ }
67
+
68
+ if (type === 'message.part.updated') {
69
+ if (emitLegacyTextPartUpdate({ part: props.part, state, emit })) return;
70
+ if (emitLegacyStepFinishPart({ part: props.part, state, emit })) return;
71
+ emitLegacyToolPart({
72
+ part: props.part,
73
+ state,
74
+ emit,
75
+ toWireToolName,
76
+ nativeNameField,
77
+ getHostToolName,
78
+ authorizeHostToolCall,
79
+ });
80
+ return;
81
+ }
82
+
83
+ if (type === 'session.next.text.started') {
84
+ emit({ type: 'text-start', id: String(props.textID ?? event.id) });
85
+ return;
86
+ }
87
+ if (type === 'session.next.text.delta') {
88
+ const id = String(props.textID ?? event.id);
89
+ state.textDeltas.set(
90
+ id,
91
+ `${state.textDeltas.get(id) ?? ''}${String(props.delta ?? '')}`,
92
+ );
93
+ emit({
94
+ type: 'text-delta',
95
+ id,
96
+ delta: String(props.delta ?? ''),
97
+ });
98
+ return;
99
+ }
100
+ if (type === 'session.next.text.ended') {
101
+ const id = String(props.textID ?? event.id);
102
+ emitMissingFinalDelta({
103
+ id,
104
+ fullText: typeof props.text === 'string' ? props.text : undefined,
105
+ emittedText: state.textDeltas.get(id) ?? '',
106
+ emit,
107
+ type: 'text-delta',
108
+ });
109
+ emit({ type: 'text-end', id });
110
+ return;
111
+ }
112
+ if (type === 'session.next.reasoning.started') {
113
+ emit({
114
+ type: 'reasoning-start',
115
+ id: String(props.reasoningID ?? event.id),
116
+ });
117
+ return;
118
+ }
119
+ if (type === 'session.next.reasoning.delta') {
120
+ const id = String(props.reasoningID ?? event.id);
121
+ state.reasoningDeltas.set(
122
+ id,
123
+ `${state.reasoningDeltas.get(id) ?? ''}${String(props.delta ?? '')}`,
124
+ );
125
+ emit({
126
+ type: 'reasoning-delta',
127
+ id,
128
+ delta: String(props.delta ?? ''),
129
+ });
130
+ return;
131
+ }
132
+ if (type === 'session.next.reasoning.ended') {
133
+ const id = String(props.reasoningID ?? event.id);
134
+ emitMissingFinalDelta({
135
+ id,
136
+ fullText: typeof props.text === 'string' ? props.text : undefined,
137
+ emittedText: state.reasoningDeltas.get(id) ?? '',
138
+ emit,
139
+ type: 'reasoning-delta',
140
+ });
141
+ emit({ type: 'reasoning-end', id });
142
+ return;
143
+ }
144
+ if (type === 'session.next.shell.started') {
145
+ const callID = String(props.callID ?? event.id);
146
+ const command = String(props.command ?? '');
147
+ state.shellCommands.set(callID, command);
148
+ emit({
149
+ type: 'tool-call',
150
+ toolCallId: callID,
151
+ toolName: 'bash',
152
+ nativeName: 'bash',
153
+ input: JSON.stringify({ command }),
154
+ providerExecuted: true,
155
+ });
156
+ return;
157
+ }
158
+ if (type === 'session.next.shell.ended') {
159
+ const callID = String(props.callID ?? event.id);
160
+ emit({
161
+ type: 'tool-result',
162
+ toolCallId: callID,
163
+ toolName: 'bash',
164
+ result: {
165
+ command: state.shellCommands.get(callID) ?? '',
166
+ output: String(props.output ?? ''),
167
+ },
168
+ });
169
+ return;
170
+ }
171
+ if (type === 'session.next.tool.input.delta') {
172
+ const callID = String(props.callID ?? event.id);
173
+ state.toolInputs.set(
174
+ callID,
175
+ `${state.toolInputs.get(callID) ?? ''}${String(props.delta ?? '')}`,
176
+ );
177
+ return;
178
+ }
179
+ if (type === 'session.next.tool.input.ended') {
180
+ state.toolInputs.set(
181
+ String(props.callID ?? event.id),
182
+ String(props.text ?? ''),
183
+ );
184
+ return;
185
+ }
186
+ if (type === 'session.next.tool.called') {
187
+ const callID = String(props.callID ?? event.id);
188
+ const rawToolName = String(props.tool ?? 'unknown');
189
+ const toolName = toWireToolName(rawToolName);
190
+ state.toolNames.set(callID, { rawToolName, toolName });
191
+ const hostToolName = getHostToolName(toolName, props.tool);
192
+ if (hostToolName) {
193
+ authorizeHostToolCall({
194
+ callID,
195
+ toolName: hostToolName,
196
+ input: props.input ?? parseToolInput(state, props),
197
+ });
198
+ return;
199
+ }
200
+ emit({
201
+ type: 'tool-call',
202
+ toolCallId: callID,
203
+ toolName,
204
+ ...nativeNameField({ nativeName: rawToolName, toolName }),
205
+ input: JSON.stringify(props.input ?? parseToolInput(state, props)),
206
+ providerExecuted: true,
207
+ ...(props.provider?.metadata
208
+ ? { providerMetadata: props.provider.metadata }
209
+ : {}),
210
+ });
211
+ return;
212
+ }
213
+ if (
214
+ type === 'session.next.tool.success' ||
215
+ type === 'session.next.tool.failed'
216
+ ) {
217
+ const callID = String(props.callID ?? event.id);
218
+ const cachedTool = state.toolNames.get(callID);
219
+ const rawToolName =
220
+ cachedTool?.rawToolName ??
221
+ String((props as { tool?: unknown }).tool ?? '');
222
+ const toolName =
223
+ cachedTool?.toolName ?? toWireToolName(rawToolName || 'unknown');
224
+ if (getHostToolName(toolName, rawToolName)) return;
225
+ emit({
226
+ type: 'tool-result',
227
+ toolCallId: callID,
228
+ toolName,
229
+ result:
230
+ props.result ??
231
+ props.structured ??
232
+ ('content' in props ? props.content : null) ??
233
+ null,
234
+ ...(type === 'session.next.tool.failed' ? { isError: true } : {}),
235
+ });
236
+ return;
237
+ }
238
+ if (type === 'session.next.retried') {
239
+ const error = props.error ?? event;
240
+ if (isRecord(error) && error.isRetryable === false) {
241
+ emitError({
242
+ error,
243
+ message: 'OpenCode session retry failed',
244
+ });
245
+ } else {
246
+ emitWarning({ message: nextRetryEventMessage({ event, formatError }) });
247
+ }
248
+ return;
249
+ }
250
+ if (type === 'session.next.step.ended') {
251
+ closeLegacyOpenParts({ state, emit });
252
+ state.turnUsage = mapUsage(props.tokens);
253
+ emit({
254
+ type: 'finish-step',
255
+ finishReason: {
256
+ unified: mapOpenCodeFinishReason(String(props.finish ?? 'stop')),
257
+ raw: String(props.finish ?? 'stop'),
258
+ },
259
+ usage: state.turnUsage,
260
+ ...(typeof props.cost === 'number'
261
+ ? { harnessMetadata: { opencode: { cost: props.cost } } }
262
+ : {}),
263
+ });
264
+ return;
265
+ }
266
+ if (type === 'session.next.compaction.ended') {
267
+ emit({
268
+ type: 'compaction',
269
+ trigger: props.reason === 'auto' ? 'auto' : 'manual',
270
+ summary: String(props.text ?? ''),
271
+ harnessMetadata: {
272
+ opencode: {
273
+ recent: String(props.recent ?? ''),
274
+ },
275
+ },
276
+ });
277
+ return;
278
+ }
279
+ if (type === 'file.edited') {
280
+ emit({
281
+ type: 'file-change',
282
+ event: 'modify',
283
+ path: stripWorkDir(String(props.file ?? '')),
284
+ });
285
+ return;
286
+ }
287
+ if (type === 'session.error' || type === 'session.next.step.failed') {
288
+ const error = props.error ?? event;
289
+ emitError({
290
+ error,
291
+ message:
292
+ type === 'session.error'
293
+ ? 'OpenCode session error'
294
+ : 'OpenCode step failed',
295
+ });
296
+ }
297
+ };
298
+ }
299
+
300
+ function closeLegacyOpenParts({
301
+ state,
302
+ emit,
303
+ }: {
304
+ state: TranslationState;
305
+ emit: Emit;
306
+ }): void {
307
+ for (const id of state.legacyReasoningPartIds) {
308
+ emit({ type: 'reasoning-end', id });
309
+ state.reasoningDeltas.delete(id);
310
+ }
311
+ state.legacyReasoningPartIds.clear();
312
+ for (const id of state.legacyTextPartIds) {
313
+ emit({ type: 'text-end', id });
314
+ state.textDeltas.delete(id);
315
+ }
316
+ state.legacyTextPartIds.clear();
317
+ }
318
+
319
+ function emitLegacyStepFinishPart({
320
+ part,
321
+ state,
322
+ emit,
323
+ }: {
324
+ part: unknown;
325
+ state: TranslationState;
326
+ emit: Emit;
327
+ }): boolean {
328
+ const event = legacyStepFinishPartToFinishStep(part);
329
+ if (!event) return false;
330
+ const id = isRecord(part) ? stringValue(part.id) : undefined;
331
+ if (id) {
332
+ if (state.legacyStepFinishPartIds.has(id)) return true;
333
+ state.legacyStepFinishPartIds.add(id);
334
+ }
335
+ closeLegacyOpenParts({ state, emit });
336
+ state.turnUsage = event.usage as Record<string, unknown>;
337
+ emit(event);
338
+ return true;
339
+ }
340
+
341
+ function emitLegacyToolPart({
342
+ part,
343
+ state,
344
+ emit,
345
+ toWireToolName,
346
+ nativeNameField,
347
+ getHostToolName,
348
+ authorizeHostToolCall,
349
+ }: {
350
+ part: unknown;
351
+ state: TranslationState;
352
+ emit: Emit;
353
+ toWireToolName: (nativeName: string) => string;
354
+ nativeNameField: (input: { nativeName: string; toolName: string }) => {
355
+ nativeName?: string;
356
+ };
357
+ getHostToolName: (
358
+ toolName: string,
359
+ rawToolName: unknown,
360
+ ) => string | undefined;
361
+ authorizeHostToolCall: (input: {
362
+ callID: string;
363
+ toolName: string;
364
+ input: unknown;
365
+ }) => void;
366
+ }): void {
367
+ if (!part || typeof part !== 'object') return;
368
+ const toolPart = part as Record<string, any>;
369
+ if (toolPart.type !== 'tool') return;
370
+ const status = legacyToolPartStatus(toolPart);
371
+ if (status !== 'running' && status !== 'completed' && status !== 'error') {
372
+ return;
373
+ }
374
+ if (
375
+ typeof toolPart.tool !== 'string' ||
376
+ typeof toolPart.callID !== 'string'
377
+ ) {
378
+ return;
379
+ }
380
+ const callID = toolPart.callID;
381
+ const rawToolName = toolPart.tool;
382
+ const toolName = toWireToolName(rawToolName);
383
+ state.toolNames.set(callID, { rawToolName, toolName });
384
+ const hostToolName = getHostToolName(toolName, rawToolName);
385
+ if (hostToolName) {
386
+ if (status === 'running') {
387
+ authorizeHostToolCall({
388
+ callID,
389
+ toolName: hostToolName,
390
+ input: legacyToolPartInput(toolPart),
391
+ });
392
+ }
393
+ return;
394
+ }
395
+ if (!state.toolCallsEmitted.has(callID)) {
396
+ state.toolCallsEmitted.add(callID);
397
+ emit({
398
+ type: 'tool-call',
399
+ toolCallId: callID,
400
+ toolName,
401
+ ...nativeNameField({ nativeName: rawToolName, toolName }),
402
+ input: JSON.stringify(legacyToolPartInput(toolPart)),
403
+ providerExecuted: true,
404
+ ...(toolPart.provider?.metadata
405
+ ? { providerMetadata: toolPart.provider.metadata }
406
+ : {}),
407
+ });
408
+ }
409
+ if (
410
+ (status === 'completed' || status === 'error') &&
411
+ !state.toolResultsEmitted.has(callID)
412
+ ) {
413
+ state.toolResultsEmitted.add(callID);
414
+ emit({
415
+ type: 'tool-result',
416
+ toolCallId: callID,
417
+ toolName,
418
+ result: legacyToolPartOutput(toolPart),
419
+ ...(status === 'error' ? { isError: true } : {}),
420
+ });
421
+ }
422
+ }
423
+
424
+ function legacyToolPartStatus(part: Record<string, any>): string | undefined {
425
+ return typeof part.state === 'string'
426
+ ? part.state
427
+ : typeof part.state === 'object' && part.state !== null
428
+ ? String(part.state.status ?? '')
429
+ : undefined;
430
+ }
431
+
432
+ function legacyToolPartInput(
433
+ part: Record<string, any>,
434
+ ): Record<string, unknown> {
435
+ const state =
436
+ typeof part.state === 'object' && part.state !== null
437
+ ? (part.state as Record<string, any>)
438
+ : undefined;
439
+ return {
440
+ ...(isRecord(part.metadata) ? part.metadata : {}),
441
+ ...(isRecord(state?.metadata) ? state.metadata : {}),
442
+ ...(isRecord(state?.input) ? state.input : {}),
443
+ };
444
+ }
445
+
446
+ function legacyToolPartOutput(part: Record<string, any>): unknown {
447
+ const state =
448
+ typeof part.state === 'object' && part.state !== null
449
+ ? (part.state as Record<string, any>)
450
+ : undefined;
451
+ if (state?.status === 'error') {
452
+ return state.error ?? part.error ?? state.result ?? 'tool failed';
453
+ }
454
+ return (
455
+ state?.output ??
456
+ state?.result ??
457
+ state?.structured ??
458
+ state?.content ??
459
+ null
460
+ );
461
+ }
462
+
463
+ function parseToolInput(
464
+ state: TranslationState,
465
+ props: Record<string, any>,
466
+ ): unknown {
467
+ const text = state.toolInputs.get(String(props.callID ?? ''));
468
+ if (!text) return {};
469
+ try {
470
+ return JSON.parse(text);
471
+ } catch {
472
+ return { input: text };
473
+ }
474
+ }
475
+
476
+ function nextRetryEventMessage({
477
+ event,
478
+ formatError,
479
+ }: {
480
+ event: OpenCodeEvent;
481
+ formatError: (error: unknown) => string;
482
+ }): string {
483
+ const props = event.properties ?? {};
484
+ const details: string[] = [];
485
+ if (typeof props.attempt === 'number') {
486
+ details.push(`attempt ${props.attempt}`);
487
+ }
488
+ const error = props.error;
489
+ if (isRecord(error)) {
490
+ const message =
491
+ stringValue(error.message) ??
492
+ (isRecord(error.data) ? stringValue(error.data.message) : undefined);
493
+ const statusCode = error.statusCode;
494
+ if (typeof statusCode === 'number') {
495
+ details.push(`HTTP ${statusCode}`);
496
+ }
497
+ if (message) details.push(message);
498
+ } else if (error != null) {
499
+ details.push(formatError(error));
500
+ }
501
+ return details.length > 0
502
+ ? `OpenCode session retry: ${details.join('; ')}`
503
+ : 'OpenCode session retry';
504
+ }
505
+
506
+ export function isRecord(value: unknown): value is Record<string, unknown> {
507
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
508
+ }
509
+
510
+ export function stringValue(value: unknown): string | undefined {
511
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
512
+ }