@ai-sdk/harness-opencode 1.0.52 → 1.0.53

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.52" : "0.0.0-test";
171
+ var VERSION = true ? "1.0.53" : "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.52",
3
+ "version": "1.0.53",
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.51",
30
+ "@ai-sdk/harness": "1.0.52",
31
31
  "@ai-sdk/provider-utils": "5.0.17"
32
32
  },
33
33
  "peerDependencies": {
@@ -1,17 +1,21 @@
1
1
  import type { BridgeTurn } from '@ai-sdk/harness/bridge';
2
- import { isRecord } from '@ai-sdk/provider-utils';
3
2
  import {
4
3
  emitLegacyPartDelta,
5
4
  emitLegacyTextPartUpdate,
6
5
  emitMissingFinalDelta,
7
- type OpenCodeEvent,
6
+ openCodeMessageInfoFromValue,
8
7
  type TranslationState,
9
8
  } from './opencode-events';
10
9
  import {
11
- legacyStepFinishPartToFinishStep,
12
10
  mapOpenCodeFinishReason,
11
+ translateLegacyStepFinishPart,
13
12
  } from './opencode-finish-step';
14
13
  import { mapUsage } from './opencode-usage';
14
+ import {
15
+ asOpenCodeObject,
16
+ type OpenCodeEvent,
17
+ type OpenCodeEventProperties,
18
+ } from './opencode-types';
15
19
 
16
20
  type Emit = (message: Record<string, unknown>) => void;
17
21
 
@@ -52,8 +56,8 @@ export function createEmitStreamEvent({
52
56
  const props = event.properties ?? {};
53
57
 
54
58
  if (type === 'message.updated') {
55
- const info = props.info;
56
- if (isRecord(info)) {
59
+ const info = openCodeMessageInfoFromValue(props.info);
60
+ if (info) {
57
61
  const id = stringValue(info.id);
58
62
  const role = stringValue(info.role);
59
63
  if (id && role) state.messageRoles.set(id, role);
@@ -238,7 +242,7 @@ export function createEmitStreamEvent({
238
242
  }
239
243
  if (type === 'session.next.retried') {
240
244
  const error = props.error ?? event;
241
- if (isRecord(error) && error.isRetryable === false) {
245
+ if (openCodeErrorFromValue(error)?.isRetryable === false) {
242
246
  emitError({
243
247
  error,
244
248
  message: 'OpenCode session retry failed',
@@ -326,9 +330,9 @@ function emitLegacyStepFinishPart({
326
330
  state: TranslationState;
327
331
  emit: Emit;
328
332
  }): boolean {
329
- const event = legacyStepFinishPartToFinishStep(part);
330
- if (!event) return false;
331
- const id = isRecord(part) ? stringValue(part.id) : undefined;
333
+ const translated = translateLegacyStepFinishPart(part);
334
+ if (!translated) return false;
335
+ const { event, partId: id } = translated;
332
336
  if (id) {
333
337
  if (state.legacyStepFinishPartIds.has(id)) return true;
334
338
  state.legacyStepFinishPartIds.add(id);
@@ -365,19 +369,12 @@ function emitLegacyToolPart({
365
369
  input: unknown;
366
370
  }) => void;
367
371
  }): void {
368
- if (!part || typeof part !== 'object') return;
369
- const toolPart = part as Record<string, any>;
370
- if (toolPart.type !== 'tool') return;
371
- const status = legacyToolPartStatus(toolPart);
372
+ const toolPart = legacyToolPartFromValue(part);
373
+ if (!toolPart) return;
374
+ const status = toolPart.status;
372
375
  if (status !== 'running' && status !== 'completed' && status !== 'error') {
373
376
  return;
374
377
  }
375
- if (
376
- typeof toolPart.tool !== 'string' ||
377
- typeof toolPart.callID !== 'string'
378
- ) {
379
- return;
380
- }
381
378
  const callID = toolPart.callID;
382
379
  const rawToolName = toolPart.tool;
383
380
  const toolName = toWireToolName(rawToolName);
@@ -402,8 +399,8 @@ function emitLegacyToolPart({
402
399
  ...nativeNameField({ nativeName: rawToolName, toolName }),
403
400
  input: JSON.stringify(legacyToolPartInput(toolPart)),
404
401
  providerExecuted: true,
405
- ...(toolPart.provider?.metadata
406
- ? { providerMetadata: toolPart.provider.metadata }
402
+ ...(toolPart.providerMetadata
403
+ ? { providerMetadata: toolPart.providerMetadata }
407
404
  : {}),
408
405
  });
409
406
  }
@@ -422,33 +419,16 @@ function emitLegacyToolPart({
422
419
  }
423
420
  }
424
421
 
425
- function legacyToolPartStatus(part: Record<string, any>): string | undefined {
426
- return typeof part.state === 'string'
427
- ? part.state
428
- : typeof part.state === 'object' && part.state !== null
429
- ? String(part.state.status ?? '')
430
- : undefined;
431
- }
432
-
433
- function legacyToolPartInput(
434
- part: Record<string, any>,
435
- ): Record<string, unknown> {
436
- const state =
437
- typeof part.state === 'object' && part.state !== null
438
- ? (part.state as Record<string, any>)
439
- : undefined;
422
+ function legacyToolPartInput(part: LegacyToolPart): Record<string, unknown> {
440
423
  return {
441
- ...(isRecord(part.metadata) ? part.metadata : {}),
442
- ...(isRecord(state?.metadata) ? state.metadata : {}),
443
- ...(isRecord(state?.input) ? state.input : {}),
424
+ ...(part.metadata ?? {}),
425
+ ...(part.state?.metadata ?? {}),
426
+ ...(part.state?.input ?? {}),
444
427
  };
445
428
  }
446
429
 
447
- function legacyToolPartOutput(part: Record<string, any>): unknown {
448
- const state =
449
- typeof part.state === 'object' && part.state !== null
450
- ? (part.state as Record<string, any>)
451
- : undefined;
430
+ function legacyToolPartOutput(part: LegacyToolPart): unknown {
431
+ const state = part.state;
452
432
  if (state?.status === 'error') {
453
433
  return state.error ?? part.error ?? state.result ?? 'tool failed';
454
434
  }
@@ -463,7 +443,7 @@ function legacyToolPartOutput(part: Record<string, any>): unknown {
463
443
 
464
444
  function parseToolInput(
465
445
  state: TranslationState,
466
- props: Record<string, any>,
446
+ props: OpenCodeEventProperties,
467
447
  ): unknown {
468
448
  const text = state.toolInputs.get(String(props.callID ?? ''));
469
449
  if (!text) return {};
@@ -487,11 +467,12 @@ function nextRetryEventMessage({
487
467
  details.push(`attempt ${props.attempt}`);
488
468
  }
489
469
  const error = props.error;
490
- if (isRecord(error)) {
470
+ const errorDetails = openCodeErrorFromValue(error);
471
+ if (errorDetails) {
491
472
  const message =
492
- stringValue(error.message) ??
493
- (isRecord(error.data) ? stringValue(error.data.message) : undefined);
494
- const statusCode = error.statusCode;
473
+ stringValue(errorDetails.message) ??
474
+ stringValue(errorDetails.data?.message);
475
+ const statusCode = errorDetails.statusCode;
495
476
  if (typeof statusCode === 'number') {
496
477
  details.push(`HTTP ${statusCode}`);
497
478
  }
@@ -507,3 +488,87 @@ function nextRetryEventMessage({
507
488
  export function stringValue(value: unknown): string | undefined {
508
489
  return typeof value === 'string' && value.length > 0 ? value : undefined;
509
490
  }
491
+
492
+ type LegacyToolState = {
493
+ status?: string;
494
+ input?: Record<string, unknown>;
495
+ metadata?: Record<string, unknown>;
496
+ output?: unknown;
497
+ result?: unknown;
498
+ structured?: unknown;
499
+ content?: unknown;
500
+ error?: unknown;
501
+ };
502
+
503
+ type LegacyToolPart = {
504
+ type: 'tool';
505
+ callID: string;
506
+ tool: string;
507
+ status?: string;
508
+ state?: LegacyToolState;
509
+ metadata?: Record<string, unknown>;
510
+ providerMetadata?: unknown;
511
+ error?: unknown;
512
+ };
513
+
514
+ function legacyToolPartFromValue(value: unknown): LegacyToolPart | undefined {
515
+ const part = asOpenCodeObject(value);
516
+ if (
517
+ !part ||
518
+ part.type !== 'tool' ||
519
+ typeof part.tool !== 'string' ||
520
+ typeof part.callID !== 'string'
521
+ ) {
522
+ return undefined;
523
+ }
524
+
525
+ const stateObject = asOpenCodeObject(part.state);
526
+ const provider = asOpenCodeObject(part.provider);
527
+ const state = stateObject
528
+ ? {
529
+ status: stringValue(stateObject.status),
530
+ input: asOpenCodeObject(stateObject.input),
531
+ metadata: asOpenCodeObject(stateObject.metadata),
532
+ output: stateObject.output,
533
+ result: stateObject.result,
534
+ structured: stateObject.structured,
535
+ content: stateObject.content,
536
+ error: stateObject.error,
537
+ }
538
+ : undefined;
539
+
540
+ return {
541
+ type: 'tool',
542
+ callID: part.callID,
543
+ tool: part.tool,
544
+ status:
545
+ typeof part.state === 'string'
546
+ ? part.state
547
+ : (state?.status ?? undefined),
548
+ state,
549
+ metadata: asOpenCodeObject(part.metadata),
550
+ providerMetadata: provider?.metadata,
551
+ error: part.error,
552
+ };
553
+ }
554
+
555
+ type OpenCodeErrorDetails = {
556
+ isRetryable?: unknown;
557
+ message?: unknown;
558
+ statusCode?: unknown;
559
+ data?: { message?: unknown };
560
+ };
561
+
562
+ function openCodeErrorFromValue(
563
+ value: unknown,
564
+ ): OpenCodeErrorDetails | undefined {
565
+ const error = asOpenCodeObject(value);
566
+ if (!error) return undefined;
567
+ const data = asOpenCodeObject(error.data);
568
+ return {
569
+ isRetryable: error.isRetryable,
570
+ message: error.message,
571
+ statusCode: error.statusCode,
572
+ data: data ? { message: data.message } : undefined,
573
+ };
574
+ }
@@ -3,7 +3,6 @@ import {
3
3
  type BridgeEvent,
4
4
  type BridgeTurn,
5
5
  } from '@ai-sdk/harness/bridge';
6
- import { isRecord } from '@ai-sdk/provider-utils';
7
6
  import { randomUUID } from 'node:crypto';
8
7
  import { mkdirSync } from 'node:fs';
9
8
  import path from 'node:path';
@@ -19,7 +18,6 @@ import {
19
18
  emitOpenCodeStreamStart,
20
19
  getOpenCodeEventSessionId,
21
20
  isStepSettlementEvent,
22
- type OpenCodeEvent,
23
21
  type TranslationState,
24
22
  unwrapOpenCodeEvent,
25
23
  } from './opencode-events';
@@ -35,6 +33,11 @@ import {
35
33
  type HarnessUsage,
36
34
  type OpenCodeTokenUsage,
37
35
  } from './opencode-usage';
36
+ import {
37
+ asOpenCodeObject,
38
+ type OpenCodeEvent,
39
+ type OpenCodeObject,
40
+ } from './opencode-types';
38
41
  import { startAuthorizedToolRelay, type ToolRelay } from './tool-relay';
39
42
 
40
43
  type Emit = (msg: Record<string, unknown>) => void;
@@ -834,12 +837,7 @@ async function handlePermissionV2({
834
837
  ? props.resources.map(String)
835
838
  : [],
836
839
  requestID,
837
- toolCallId:
838
- typeof props.source === 'object' &&
839
- props.source !== null &&
840
- 'callID' in props.source
841
- ? String((props.source as { callID?: unknown }).callID)
842
- : requestID,
840
+ toolCallId: String(props.source?.callID ?? requestID),
843
841
  permissionMode,
844
842
  builtinToolFiltering,
845
843
  turn,
@@ -873,16 +871,12 @@ async function handlePermission({
873
871
  const props = event.properties ?? {};
874
872
  const requestID = String(props.id ?? '');
875
873
  if (!requestID) return;
874
+ const tool = asOpenCodeObject(props.tool);
876
875
  const reply = await selectPermissionReply({
877
876
  action: String(props.permission ?? ''),
878
877
  resources: Array.isArray(props.patterns) ? props.patterns.map(String) : [],
879
878
  requestID,
880
- toolCallId:
881
- typeof props.tool === 'object' &&
882
- props.tool !== null &&
883
- 'callID' in props.tool
884
- ? String((props.tool as { callID?: unknown }).callID)
885
- : requestID,
879
+ toolCallId: String(tool?.callID ?? requestID),
886
880
  permissionMode,
887
881
  builtinToolFiltering,
888
882
  turn,
@@ -1302,15 +1296,13 @@ function modelRefFromAssistantSnapshot(
1302
1296
  const direct = modelRefFromValue(assistant);
1303
1297
  if (direct) return direct;
1304
1298
 
1305
- if (isRecord(assistant.metadata)) {
1306
- return modelRefFromValue(assistant.metadata.assistant);
1307
- }
1308
- return undefined;
1299
+ return modelRefFromValue(asOpenCodeObject(assistant.metadata)?.assistant);
1309
1300
  }
1310
1301
 
1311
1302
  function modelRefFromSessionInfo(data: unknown): OpenCodeModelRef | undefined {
1312
- if (!isRecord(data)) return undefined;
1313
- return modelRefFromValue(data.model) ?? modelRefFromValue(data);
1303
+ const session = asOpenCodeObject(data);
1304
+ if (!session) return undefined;
1305
+ return modelRefFromValue(session.model) ?? modelRefFromObject(session);
1314
1306
  }
1315
1307
 
1316
1308
  function modelRefFromStart(start: StartMessage): OpenCodeModelRef | undefined {
@@ -1324,7 +1316,13 @@ function modelRefFromStart(start: StartMessage): OpenCodeModelRef | undefined {
1324
1316
  }
1325
1317
 
1326
1318
  function modelRefFromValue(value: unknown): OpenCodeModelRef | undefined {
1327
- if (!isRecord(value)) return undefined;
1319
+ const model = asOpenCodeObject(value);
1320
+ return model ? modelRefFromObject(model) : undefined;
1321
+ }
1322
+
1323
+ function modelRefFromObject(
1324
+ value: OpenCodeObject,
1325
+ ): OpenCodeModelRef | undefined {
1328
1326
  const providerID = stringValue(value.providerID);
1329
1327
  const modelID = stringValue(value.modelID ?? value.id);
1330
1328
  if (!providerID || !modelID) return undefined;
@@ -1,10 +1,9 @@
1
- import { isRecord } from '@ai-sdk/provider-utils';
2
-
3
- export type OpenCodeEvent = {
4
- id?: string;
5
- type?: string;
6
- properties?: Record<string, any>;
7
- };
1
+ import {
2
+ asOpenCodeObject,
3
+ type OpenCodeEvent,
4
+ type OpenCodeEventProperties,
5
+ type OpenCodeMessageInfo,
6
+ } from './opencode-types';
8
7
 
9
8
  type Emit = (msg: Record<string, unknown>) => void;
10
9
 
@@ -53,10 +52,12 @@ export function emitOpenCodeStreamStart({
53
52
  state: TranslationState;
54
53
  emit: Emit;
55
54
  }): void {
56
- if (state.streamStarted || !isRecord(info)) return;
57
- if (info.role !== 'assistant' && info.type !== 'assistant') return;
58
- const providerID = stringValue(info.providerID);
59
- const modelID = stringValue(info.modelID);
55
+ if (state.streamStarted) return;
56
+ const message = openCodeMessageInfoFromValue(info);
57
+ if (!message) return;
58
+ if (message.role !== 'assistant' && message.type !== 'assistant') return;
59
+ const providerID = stringValue(message.providerID);
60
+ const modelID = stringValue(message.modelID);
60
61
  const modelId =
61
62
  providerID && modelID ? `${providerID}/${modelID}` : undefined;
62
63
 
@@ -67,20 +68,24 @@ export function emitOpenCodeStreamStart({
67
68
  export function unwrapOpenCodeEvent(
68
69
  rawEvent: unknown,
69
70
  ): OpenCodeEvent | undefined {
70
- if (!rawEvent || typeof rawEvent !== 'object') return undefined;
71
- const raw = rawEvent as Record<string, any>;
71
+ const raw = asOpenCodeObject(rawEvent);
72
+ if (!raw) return undefined;
72
73
  if (raw.type === 'sync' && raw.syncEvent) {
73
- const sync = raw.syncEvent as Record<string, any>;
74
+ const sync = asOpenCodeObject(raw.syncEvent);
75
+ if (!sync) return undefined;
74
76
  return {
75
77
  id: String(sync.id ?? raw.id ?? ''),
76
78
  type: stripSyncVersion(String(sync.type ?? '')),
77
- properties: asRecord(sync.data) ?? {},
79
+ properties: openCodeEventPropertiesFromValue(sync.data) ?? {},
78
80
  };
79
81
  }
80
82
  return {
81
83
  id: typeof raw.id === 'string' ? raw.id : undefined,
82
84
  type: typeof raw.type === 'string' ? stripSyncVersion(raw.type) : undefined,
83
- properties: asRecord(raw.properties) ?? asRecord(raw.data) ?? {},
85
+ properties:
86
+ openCodeEventPropertiesFromValue(raw.properties) ??
87
+ openCodeEventPropertiesFromValue(raw.data) ??
88
+ {},
84
89
  };
85
90
  }
86
91
 
@@ -95,14 +100,8 @@ export function getOpenCodeEventSessionId(
95
100
  return props.id;
96
101
  }
97
102
  const part = props.part;
98
- if (
99
- part &&
100
- typeof part === 'object' &&
101
- !Array.isArray(part) &&
102
- typeof (part as { sessionID?: unknown }).sessionID === 'string'
103
- ) {
104
- return (part as { sessionID: string }).sessionID;
105
- }
103
+ const partObject = asOpenCodeObject(part);
104
+ if (typeof partObject?.sessionID === 'string') return partObject.sessionID;
106
105
  return undefined;
107
106
  }
108
107
 
@@ -202,21 +201,21 @@ export function emitLegacyTextPartUpdate({
202
201
  state: TranslationState;
203
202
  emit: Emit;
204
203
  }): boolean {
205
- if (!isRecord(part)) return false;
206
- if (part.type !== 'text' && part.type !== 'reasoning') return false;
207
- const id = stringValue(part.id);
204
+ const textPart = legacyTextPartFromValue(part);
205
+ if (!textPart) return false;
206
+ const id = stringValue(textPart.id);
208
207
  if (!id) return true;
209
208
 
210
- const messageID = stringValue(part.messageID);
209
+ const messageID = stringValue(textPart.messageID);
211
210
  if (messageID && state.messageRoles.get(messageID) === 'user') return true;
212
211
 
213
- const isReasoning = part.type === 'reasoning';
212
+ const isReasoning = textPart.type === 'reasoning';
214
213
  const ids = isReasoning
215
214
  ? state.legacyReasoningPartIds
216
215
  : state.legacyTextPartIds;
217
216
  const deltaMap = isReasoning ? state.reasoningDeltas : state.textDeltas;
218
217
  const deltaType = isReasoning ? 'reasoning-delta' : 'text-delta';
219
- const text = typeof part.text === 'string' ? part.text : undefined;
218
+ const text = typeof textPart.text === 'string' ? textPart.text : undefined;
220
219
 
221
220
  startLegacyPart({
222
221
  ids,
@@ -236,7 +235,7 @@ export function emitLegacyTextPartUpdate({
236
235
  deltaMap.set(id, text);
237
236
  }
238
237
 
239
- if (legacyPartEnded(part)) {
238
+ if (textPart.time?.end != null) {
240
239
  ids.delete(id);
241
240
  deltaMap.delete(id);
242
241
  emit({ type: isReasoning ? 'reasoning-end' : 'text-end', id });
@@ -249,10 +248,6 @@ function stripSyncVersion(type: string): string {
249
248
  return type.replace(/\.\d+$/, '');
250
249
  }
251
250
 
252
- function asRecord(value: unknown): Record<string, any> | undefined {
253
- return isRecord(value) ? value : undefined;
254
- }
255
-
256
251
  function stringValue(value: unknown): string | undefined {
257
252
  return typeof value === 'string' && value.length > 0 ? value : undefined;
258
253
  }
@@ -283,6 +278,36 @@ function startLegacyPart({
283
278
  emit({ type: `${type}-start`, id });
284
279
  }
285
280
 
286
- function legacyPartEnded(part: Record<string, unknown>): boolean {
287
- return isRecord(part.time) && part.time.end != null;
281
+ export function openCodeMessageInfoFromValue(
282
+ value: unknown,
283
+ ): OpenCodeMessageInfo | undefined {
284
+ return asOpenCodeObject(value) as OpenCodeMessageInfo | undefined;
285
+ }
286
+
287
+ function openCodeEventPropertiesFromValue(
288
+ value: unknown,
289
+ ): OpenCodeEventProperties | undefined {
290
+ return asOpenCodeObject(value) as OpenCodeEventProperties | undefined;
291
+ }
292
+
293
+ type LegacyTextPart = {
294
+ type: 'text' | 'reasoning';
295
+ id?: unknown;
296
+ messageID?: unknown;
297
+ text?: unknown;
298
+ time?: { end?: unknown };
299
+ };
300
+
301
+ function legacyTextPartFromValue(value: unknown): LegacyTextPart | undefined {
302
+ const part = asOpenCodeObject(value);
303
+ if (!part || (part.type !== 'text' && part.type !== 'reasoning')) {
304
+ return undefined;
305
+ }
306
+ return {
307
+ type: part.type,
308
+ id: part.id,
309
+ messageID: part.messageID,
310
+ text: part.text,
311
+ time: asOpenCodeObject(part.time),
312
+ };
288
313
  }
@@ -1,6 +1,6 @@
1
1
  import type { HarnessV1StreamPart } from '@ai-sdk/harness';
2
- import { isRecord } from '@ai-sdk/provider-utils';
3
2
  import { mapUsage } from './opencode-usage';
3
+ import { asOpenCodeObject } from './opencode-types';
4
4
 
5
5
  type FinishStepEvent = Extract<HarnessV1StreamPart, { type: 'finish-step' }>;
6
6
 
@@ -20,9 +20,16 @@ export function mapOpenCodeFinishReason(
20
20
  export function legacyStepFinishPartToFinishStep(
21
21
  part: unknown,
22
22
  ): FinishStepEvent | undefined {
23
- if (!isRecord(part) || part.type !== 'step-finish') return undefined;
23
+ return translateLegacyStepFinishPart(part)?.event;
24
+ }
25
+
26
+ export function translateLegacyStepFinishPart(
27
+ value: unknown,
28
+ ): { event: FinishStepEvent; partId?: string } | undefined {
29
+ const part = asOpenCodeObject(value);
30
+ if (!part || part.type !== 'step-finish') return undefined;
24
31
  const rawFinish = typeof part.reason === 'string' ? part.reason : 'stop';
25
- return {
32
+ const event: FinishStepEvent = {
26
33
  type: 'finish-step',
27
34
  finishReason: {
28
35
  unified: mapOpenCodeFinishReason(rawFinish),
@@ -33,4 +40,8 @@ export function legacyStepFinishPartToFinishStep(
33
40
  ? { harnessMetadata: { opencode: { cost: part.cost } } }
34
41
  : {}),
35
42
  };
43
+ return {
44
+ event,
45
+ ...(typeof part.id === 'string' ? { partId: part.id } : {}),
46
+ };
36
47
  }
@@ -0,0 +1,33 @@
1
+ export type OpenCodeObject = Record<string, unknown>;
2
+
3
+ export type OpenCodeMessageInfo = {
4
+ id?: unknown;
5
+ role?: unknown;
6
+ type?: unknown;
7
+ providerID?: unknown;
8
+ modelID?: unknown;
9
+ tokens?: unknown;
10
+ };
11
+
12
+ export type OpenCodeEventProperties = OpenCodeObject & {
13
+ info?: OpenCodeMessageInfo;
14
+ provider?: { metadata?: unknown };
15
+ source?: { callID?: unknown };
16
+ status?: {
17
+ type?: unknown;
18
+ attempt?: unknown;
19
+ message?: unknown;
20
+ };
21
+ };
22
+
23
+ export type OpenCodeEvent = {
24
+ id?: string;
25
+ type?: string;
26
+ properties?: OpenCodeEventProperties;
27
+ };
28
+
29
+ export function asOpenCodeObject(value: unknown): OpenCodeObject | undefined {
30
+ return value != null && typeof value === 'object' && !Array.isArray(value)
31
+ ? (value as OpenCodeObject)
32
+ : undefined;
33
+ }
@@ -1,5 +1,5 @@
1
1
  import type { HarnessV1StreamPart } from '@ai-sdk/harness';
2
- import { isRecord } from '@ai-sdk/provider-utils';
2
+ import { asOpenCodeObject } from './opencode-types';
3
3
 
4
4
  type FinishStepEvent = Extract<HarnessV1StreamPart, { type: 'finish-step' }>;
5
5
 
@@ -40,13 +40,13 @@ export function defaultUsage(): HarnessUsage {
40
40
  export function extractSessionTokens(
41
41
  value: unknown,
42
42
  ): OpenCodeTokenUsage | undefined {
43
- const record = asRecord(value);
43
+ const record = asOpenCodeObject(value) as SessionTokenEnvelope | undefined;
44
44
  if (!record) return undefined;
45
45
  const tokens =
46
46
  extractOpenCodeTokens(record.tokens) ??
47
- extractOpenCodeTokens(asRecord(record.info)?.tokens) ??
48
- extractOpenCodeTokens(asRecord(record.data)?.tokens) ??
49
- extractOpenCodeTokens(asRecord(asRecord(record.data)?.data)?.tokens);
47
+ extractOpenCodeTokens(record.info?.tokens) ??
48
+ extractOpenCodeTokens(record.data?.tokens) ??
49
+ extractOpenCodeTokens(record.data?.data?.tokens);
50
50
  return tokens;
51
51
  }
52
52
 
@@ -105,8 +105,8 @@ export function addUsage({
105
105
  }
106
106
 
107
107
  function extractOpenCodeTokens(value: unknown): OpenCodeTokenUsage | undefined {
108
- const record = asRecord(value);
109
- const cache = asRecord(record?.cache);
108
+ const record = asOpenCodeObject(value);
109
+ const cache = asOpenCodeObject(record?.cache);
110
110
  if (!record || !cache) return undefined;
111
111
  return {
112
112
  input: numberValue(record.input),
@@ -129,11 +129,11 @@ function zeroOpenCodeTokens(): OpenCodeTokenUsage {
129
129
  }
130
130
 
131
131
  function asTokenGroup(value: unknown): Record<string, number | undefined> {
132
- return asRecord(value) ?? {};
133
- }
134
-
135
- function asRecord(value: unknown): Record<string, any> | undefined {
136
- return isRecord(value) ? value : undefined;
132
+ return (
133
+ (asOpenCodeObject(value) as
134
+ | Record<string, number | undefined>
135
+ | undefined) ?? {}
136
+ );
137
137
  }
138
138
 
139
139
  function numberValue(value: unknown): number {
@@ -157,3 +157,12 @@ function add({
157
157
  ? undefined
158
158
  : (leftNumber ?? 0) + (rightNumber ?? 0);
159
159
  }
160
+
161
+ type SessionTokenEnvelope = {
162
+ tokens?: unknown;
163
+ info?: { tokens?: unknown };
164
+ data?: {
165
+ tokens?: unknown;
166
+ data?: { tokens?: unknown };
167
+ };
168
+ };