@ai-sdk/harness-opencode 1.0.95 → 1.0.97

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.
@@ -16,6 +16,7 @@ import {
16
16
  createTranslationState,
17
17
  emitOpenCodeStreamStart,
18
18
  getOpenCodeEventSessionId,
19
+ openCodeMessageInfoFromValue,
19
20
  type TranslationState,
20
21
  unwrapOpenCodeEvent,
21
22
  } from './opencode-events';
@@ -144,6 +145,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
144
145
  await client.instance.dispose({ directory: workdir });
145
146
  }
146
147
  const sessionId = await ensureSession({ client, start, emit });
148
+ await switchSessionModel({ client, sessionId, start });
147
149
 
148
150
  if (start.operation === 'compact') {
149
151
  await runCompaction({ client, sessionId, start, turn, emit });
@@ -162,6 +164,27 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
162
164
  }
163
165
  }
164
166
 
167
+ async function switchSessionModel({
168
+ client,
169
+ sessionId,
170
+ start,
171
+ }: {
172
+ client: OpenCodeClient;
173
+ sessionId: string;
174
+ start: StartMessage;
175
+ }): Promise<void> {
176
+ const model = modelRefFromStart(start);
177
+ if (model == null) return;
178
+ const response = await client.v2.session.switchModel({
179
+ sessionID: sessionId,
180
+ model: {
181
+ id: model.modelID,
182
+ providerID: model.providerID,
183
+ },
184
+ });
185
+ if (response.error != null) throw response.error;
186
+ }
187
+
165
188
  async function ensureRuntime({
166
189
  start,
167
190
  turn,
@@ -943,9 +966,33 @@ async function consumeEvents({
943
966
  let processEvent = descendantEventProcessors.get(descendantSessionId);
944
967
  if (!processEvent) {
945
968
  const descendantState = createTranslationState();
969
+ let currentEvent: OpenCodeEvent | undefined;
970
+ let modelId: string | undefined;
971
+ const emittedUsageStepIds = new Set<string>();
946
972
  processEvent = createEmitStreamEvent({
947
973
  state: descendantState,
948
- emit: () => undefined,
974
+ emit: message => {
975
+ if (message.type !== 'finish-step') return;
976
+ const stepId = getSubagentStepId(currentEvent);
977
+ if (!stepId || emittedUsageStepIds.has(stepId)) return;
978
+ emittedUsageStepIds.add(stepId);
979
+ const opencodeMetadata = asOpenCodeObject(
980
+ message.harnessMetadata,
981
+ )?.opencode;
982
+ const cost = asOpenCodeObject(opencodeMetadata)?.cost;
983
+ emit({
984
+ type: 'raw',
985
+ rawValue: {
986
+ type: 'opencode.subagent-usage',
987
+ version: 1,
988
+ sessionId: descendantSessionId,
989
+ stepId,
990
+ ...(modelId ? { modelId } : {}),
991
+ usage: message.usage,
992
+ ...(typeof cost === 'number' ? { cost } : {}),
993
+ },
994
+ });
995
+ },
949
996
  emitWarning: () => undefined,
950
997
  emitError: () => undefined,
951
998
  toWireToolName,
@@ -958,6 +1005,19 @@ async function consumeEvents({
958
1005
  stripWorkDir,
959
1006
  formatError,
960
1007
  });
1008
+ const emitDescendantEvent = processEvent;
1009
+ processEvent = descendantEvent => {
1010
+ currentEvent = descendantEvent;
1011
+ if (descendantEvent.type === 'message.updated') {
1012
+ const info = openCodeMessageInfoFromValue(
1013
+ descendantEvent.properties?.info,
1014
+ );
1015
+ const providerID = stringValue(info?.providerID);
1016
+ const modelID = stringValue(info?.modelID);
1017
+ if (providerID && modelID) modelId = `${providerID}/${modelID}`;
1018
+ }
1019
+ emitDescendantEvent(descendantEvent);
1020
+ };
961
1021
  descendantEventProcessors.set(descendantSessionId, processEvent);
962
1022
  }
963
1023
  processEvent(event);
@@ -1005,6 +1065,16 @@ async function consumeEvents({
1005
1065
  }
1006
1066
  }
1007
1067
 
1068
+ function getSubagentStepId(event: OpenCodeEvent | undefined) {
1069
+ if (event?.type === 'message.part.updated') {
1070
+ const part = asOpenCodeObject(event.properties?.part);
1071
+ if (part?.type !== 'step-finish') return undefined;
1072
+ return stringValue(part.id) ?? stringValue(part.messageID) ?? event.id;
1073
+ }
1074
+ if (event?.type !== 'session.next.step.ended') return undefined;
1075
+ return stringValue(event.properties?.stepID) ?? event.id;
1076
+ }
1077
+
1008
1078
  function sanitizeMcpToolName(value: string): string {
1009
1079
  return value.replace(/[^a-zA-Z0-9_-]/g, '_');
1010
1080
  }
@@ -44,19 +44,23 @@ export async function getOpenCodeBootstrap(): Promise<HarnessV1Bootstrap> {
44
44
  }
45
45
 
46
46
  async function readBridgeAsset(name: string): Promise<string> {
47
- const candidates = [
48
- new URL(`./bridge/${name}`, import.meta.url),
49
- new URL(`../bridge/${name}`, import.meta.url),
50
- ];
51
- let lastErr: unknown;
52
- for (const url of candidates) {
53
- try {
54
- return await readFile(fileURLToPath(url), 'utf8');
55
- } catch (err) {
56
- const code = (err as NodeJS.ErrnoException).code;
57
- if (code !== 'ENOENT') throw err;
58
- lastErr = err;
59
- }
60
- }
61
- throw lastErr ?? new Error(`bridge asset not found: ${name}`);
47
+ return readFile(
48
+ fileURLToPath(
49
+ resolveBridgeAssetUrl({
50
+ name,
51
+ moduleUrl: import.meta.url,
52
+ }),
53
+ ),
54
+ 'utf8',
55
+ );
56
+ }
57
+
58
+ export function resolveBridgeAssetUrl({
59
+ name,
60
+ moduleUrl,
61
+ }: {
62
+ name: string;
63
+ moduleUrl: string | URL;
64
+ }): URL {
65
+ return new URL(`./bridge/${name}`, moduleUrl);
62
66
  }
@@ -1,4 +1,3 @@
1
- import { randomBytes } from 'node:crypto';
2
1
  import path from 'node:path';
3
2
  import {
4
3
  commonTool,
@@ -24,6 +23,7 @@ import {
24
23
  applyCredentialForwarding,
25
24
  classifyDiskLog,
26
25
  createSandboxCredentialEnvironment,
26
+ createBridgeToken,
27
27
  experimental_createBridgeUserMessageSubmitter,
28
28
  createBridgeErrorHandler,
29
29
  createBridgeStartupError,
@@ -37,6 +37,7 @@ import {
37
37
  shellQuote,
38
38
  warnCredentialBrokeringUnavailable,
39
39
  waitForBridgeReady,
40
+ withBridgeToken,
40
41
  writeSkills as writeHarnessSkills,
41
42
  type WriteSkillsResult,
42
43
  } from '@ai-sdk/harness/utils';
@@ -256,7 +257,7 @@ export function createOpenCode(
256
257
  lifecycleStateSchema: openCodeResumeStateSchema,
257
258
  getBootstrap: getOpenCodeBootstrap,
258
259
  doStart: async startOpts => {
259
- const configuredModel = startOpts.model ?? settings.model;
260
+ const configuredModel = settings.model;
260
261
  const sandboxSession = startOpts.sandboxSession;
261
262
  const toolSafeSandboxSession =
262
263
  getRestrictedSandboxSession(sandboxSession);
@@ -446,7 +447,7 @@ export function createOpenCode(
446
447
  });
447
448
  const token =
448
449
  settings.mintBridgeToken == null
449
- ? randomBytes(32).toString('hex')
450
+ ? createBridgeToken()
450
451
  : settings.mintBridgeToken(sandboxId!);
451
452
  const forwardedAuthEnvironment = credentialsBrokered
452
453
  ? sandboxAuthEnvironment
@@ -764,18 +765,6 @@ function webSocketMessageToString(raw: unknown): string {
764
765
  return String(raw);
765
766
  }
766
767
 
767
- function withBridgeToken({
768
- endpoint,
769
- token,
770
- }: {
771
- endpoint: HarnessV1PortEndpoint;
772
- token: string;
773
- }): HarnessV1PortEndpoint {
774
- const bridgeUrl = new URL(endpoint.url);
775
- bridgeUrl.searchParams.set('agent_bridge_token', token);
776
- return { ...endpoint, url: bridgeUrl.toString() };
777
- }
778
-
779
768
  function createSession({
780
769
  sessionId,
781
770
  channel,
@@ -829,6 +818,7 @@ function createSession({
829
818
  let pendingResumeSessionId = seedResumeSessionOnFirstPrompt
830
819
  ? openCodeSessionId
831
820
  : undefined;
821
+ let selectedModel = model;
832
822
  let activeTurn = false;
833
823
  const pendingCompactionParts: HarnessV1StreamPart[] = [];
834
824
 
@@ -979,8 +969,8 @@ function createSession({
979
969
  };
980
970
  };
981
971
 
982
- const startBase = () => ({
983
- model,
972
+ const startBase = (turnModel: string | undefined) => ({
973
+ model: turnModel,
984
974
  provider,
985
975
  ...(reasoningVariant ? { variant: reasoningVariant } : {}),
986
976
  ...(openCodeConfig == null ? {} : { openCodeConfig }),
@@ -998,7 +988,6 @@ function createSession({
998
988
  return {
999
989
  sessionId,
1000
990
  isResume,
1001
- modelId: model,
1002
991
  doPromptTurn: async promptOpts => {
1003
992
  if (
1004
993
  promptOpts.responseFormat?.type === 'json' &&
@@ -1020,6 +1009,8 @@ function createSession({
1020
1009
  emit: promptOpts.emit,
1021
1010
  abortSignal: promptOpts.abortSignal,
1022
1011
  });
1012
+ const turnModel = promptOpts.model ?? selectedModel;
1013
+ if (turnModel) selectedModel = turnModel;
1023
1014
  channel.send({
1024
1015
  type: 'start',
1025
1016
  operation: 'prompt',
@@ -1036,7 +1027,7 @@ function createSession({
1036
1027
  ? { instructions: promptOpts.instructions }
1037
1028
  : {}),
1038
1029
  skillsChanged: skillWriteResult.changed,
1039
- ...startBase(),
1030
+ ...startBase(turnModel),
1040
1031
  });
1041
1032
  pendingResumeSessionId = undefined;
1042
1033
  return control;
@@ -1063,6 +1054,8 @@ function createSession({
1063
1054
  abortSignal: continueOpts.abortSignal,
1064
1055
  });
1065
1056
  if (rerunContinue) {
1057
+ const turnModel = continueOpts.model ?? selectedModel;
1058
+ if (turnModel) selectedModel = turnModel;
1066
1059
  channel.send({
1067
1060
  type: 'start',
1068
1061
  operation: 'prompt',
@@ -1079,7 +1072,7 @@ function createSession({
1079
1072
  ? { instructions: continueOpts.instructions }
1080
1073
  : {}),
1081
1074
  skillsChanged: skillWriteResult.changed,
1082
- ...startBase(),
1075
+ ...startBase(turnModel),
1083
1076
  });
1084
1077
  pendingResumeSessionId = undefined;
1085
1078
  }
@@ -1102,7 +1095,7 @@ function createSession({
1102
1095
  }
1103
1096
  await runCompactOperation({
1104
1097
  channel,
1105
- model,
1098
+ model: selectedModel,
1106
1099
  provider,
1107
1100
  permissionMode,
1108
1101
  debug,