@ai-sdk/harness-opencode 1.0.52 → 1.0.54

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.
@@ -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
+ };
@@ -182,7 +182,7 @@ const OPENCODE_BUILTIN_TOOLS = {
182
182
  }),
183
183
  } as const satisfies Record<string, HarnessV1BuiltinTool<any, any>>;
184
184
 
185
- const BOOTSTRAP_DIR = '/tmp/harness/opencode';
185
+ const BOOTSTRAP_DIR = '.harness-bootstrap/opencode';
186
186
 
187
187
  const openCodeBridgeCoordsSchema = z.object({
188
188
  port: z.number(),
@@ -230,12 +230,12 @@ export function createOpenCode(
230
230
  },
231
231
  ],
232
232
  commands: [
233
- { command: `mkdir -p ${BOOTSTRAP_DIR}` },
234
233
  {
235
- command: `pnpm --dir ${BOOTSTRAP_DIR} install --frozen-lockfile --store-dir ${BOOTSTRAP_DIR}/.pnpm-store`,
234
+ command: 'pnpm install --frozen-lockfile --store-dir .pnpm-store',
236
235
  },
237
236
  {
238
- command: `cd ${BOOTSTRAP_DIR} && node node_modules/opencode-ai/postinstall.mjs && ./node_modules/.bin/opencode --version`,
237
+ command:
238
+ 'node node_modules/opencode-ai/postinstall.mjs && ./node_modules/.bin/opencode --version',
239
239
  },
240
240
  ],
241
241
  };
@@ -245,6 +245,10 @@ export function createOpenCode(
245
245
  const sandboxSession = startOpts.sandboxSession;
246
246
  const session = sandboxSession.restricted();
247
247
  const sandboxId = sandboxSession.id;
248
+ const bootstrapDir = path.posix.resolve(
249
+ sandboxSession.defaultWorkingDirectory,
250
+ BOOTSTRAP_DIR,
251
+ );
248
252
  const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
249
253
  const isResume = lifecycleState != null;
250
254
  const isContinue = startOpts.continueFrom != null;
@@ -388,7 +392,7 @@ export function createOpenCode(
388
392
  });
389
393
 
390
394
  const proc = await session.spawn({
391
- command: `node ${BOOTSTRAP_DIR}/bridge.mjs --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)} --bootstrap-dir ${shellQuote(BOOTSTRAP_DIR)}${skillSetup ? ` --skills-dir ${shellQuote(skillSetup.skillsDir)}` : ''}`,
395
+ command: `node ${shellQuote(`${bootstrapDir}/bridge.mjs`)} --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)} --bootstrap-dir ${shellQuote(bootstrapDir)}${skillSetup ? ` --skills-dir ${shellQuote(skillSetup.skillsDir)}` : ''}`,
392
396
  env,
393
397
  abortSignal: startOpts.abortSignal,
394
398
  });