@ai-sdk/harness-opencode 1.0.41 → 1.0.42

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.
@@ -15,9 +15,7 @@ import {
15
15
  } from '@opencode-ai/sdk/v2';
16
16
  import {
17
17
  createTranslationState,
18
- emitLegacyPartDelta,
19
- emitLegacyTextPartUpdate,
20
- emitMissingFinalDelta,
18
+ emitOpenCodeStreamStart,
21
19
  getOpenCodeEventSessionId,
22
20
  isStepSettlementEvent,
23
21
  type OpenCodeEvent,
@@ -25,9 +23,11 @@ import {
25
23
  unwrapOpenCodeEvent,
26
24
  } from './opencode-events';
27
25
  import {
28
- legacyStepFinishPartToFinishStep,
29
- mapOpenCodeFinishReason,
30
- } from './opencode-finish-step';
26
+ createEmitStreamEvent,
27
+ isRecord,
28
+ stringValue,
29
+ } from './create-emit-stream-event';
30
+ import { mapOpenCodeFinishReason } from './opencode-finish-step';
31
31
  import { prependOpenCodeBinToPath } from './opencode-path';
32
32
  import {
33
33
  addUsage,
@@ -485,30 +485,6 @@ function legacyRetryStatusMessage(event: OpenCodeEvent): string {
485
485
  : 'OpenCode session retry';
486
486
  }
487
487
 
488
- function nextRetryEventMessage(event: OpenCodeEvent): string {
489
- const props = event.properties ?? {};
490
- const details: string[] = [];
491
- if (typeof props.attempt === 'number') {
492
- details.push(`attempt ${props.attempt}`);
493
- }
494
- const error = props.error;
495
- if (isRecord(error)) {
496
- const message =
497
- stringValue(error.message) ??
498
- (isRecord(error.data) ? stringValue(error.data.message) : undefined);
499
- const statusCode = error.statusCode;
500
- if (typeof statusCode === 'number') {
501
- details.push(`HTTP ${statusCode}`);
502
- }
503
- if (message) details.push(message);
504
- } else if (error != null) {
505
- details.push(formatError(error));
506
- }
507
- return details.length > 0
508
- ? `OpenCode session retry: ${details.join('; ')}`
509
- : 'OpenCode session retry';
510
- }
511
-
512
488
  async function ensureSession({
513
489
  client,
514
490
  start,
@@ -562,6 +538,7 @@ async function runPrompt({
562
538
  let sawFinishStep = false;
563
539
  let sawBusy = false;
564
540
  let terminalError: string | undefined;
541
+ const state = createTranslationState();
565
542
  const initialSessionTokens = await readSessionTokens({
566
543
  client,
567
544
  sessionId,
@@ -575,6 +552,7 @@ async function runPrompt({
575
552
  permissionMode: start.permissionMode,
576
553
  builtinToolFiltering: start.builtinToolFiltering,
577
554
  turn,
555
+ state,
578
556
  emit: msg => {
579
557
  if (msg.type === 'text-delta' || msg.type === 'reasoning-delta') {
580
558
  sawContent = true;
@@ -591,6 +569,13 @@ async function runPrompt({
591
569
  signal: eventsAbort.signal,
592
570
  onSubscribed: () => eventsReady.resolve(undefined),
593
571
  onEvent: event => {
572
+ if (event.type === 'message.updated') {
573
+ emitOpenCodeStreamStart({
574
+ info: event.properties?.info,
575
+ state,
576
+ emit,
577
+ });
578
+ }
594
579
  if (event.type === 'session.updated') {
595
580
  latestSessionTokens =
596
581
  extractSessionTokens(event.properties) ?? latestSessionTokens;
@@ -619,10 +604,6 @@ async function runPrompt({
619
604
  eventsReady.resolve(undefined);
620
605
  turnSettled.resolve();
621
606
  });
622
- emit({
623
- type: 'stream-start',
624
- ...(start.model ? { modelId: start.model } : {}),
625
- });
626
607
  await eventsReady.promise;
627
608
  const prompted = await legacySessionPrompt({
628
609
  client,
@@ -641,6 +622,7 @@ async function runPrompt({
641
622
  const emittedFallback = await emitContextFallback({
642
623
  client,
643
624
  sessionId,
625
+ state,
644
626
  emit,
645
627
  emitContent: !sawContent,
646
628
  }).catch(() => false);
@@ -701,6 +683,7 @@ async function runCompaction({
701
683
  permissionMode: start.permissionMode,
702
684
  builtinToolFiltering: start.builtinToolFiltering,
703
685
  turn,
686
+ state: createTranslationState(),
704
687
  emit: msg => {
705
688
  if (msg.type === 'compaction') sawCompaction = true;
706
689
  emit(msg);
@@ -764,6 +747,7 @@ async function consumeEvents({
764
747
  permissionMode,
765
748
  builtinToolFiltering,
766
749
  turn,
750
+ state,
767
751
  emit,
768
752
  signal,
769
753
  onSubscribed,
@@ -774,6 +758,7 @@ async function consumeEvents({
774
758
  permissionMode: StartMessage['permissionMode'];
775
759
  builtinToolFiltering: StartMessage['builtinToolFiltering'];
776
760
  turn: BridgeTurn;
761
+ state: TranslationState;
777
762
  emit: Emit;
778
763
  signal: AbortSignal;
779
764
  onSubscribed?: () => void;
@@ -782,472 +767,47 @@ async function consumeEvents({
782
767
  const stream = await subscribeLegacyEvents({ client, signal });
783
768
  onSubscribed?.();
784
769
  if (!stream) return;
785
- const state = createTranslationState();
770
+ const emitStreamEvent = createEmitStreamEvent({
771
+ state,
772
+ emit,
773
+ emitWarning: turn.emitWarning,
774
+ emitError: turn.emitError,
775
+ toWireToolName,
776
+ nativeNameField,
777
+ getHostToolName,
778
+ authorizeHostToolCall: input => authorizeHostToolCall({ ...input, state }),
779
+ stripWorkDir,
780
+ formatError,
781
+ });
786
782
  for await (const rawEvent of stream) {
787
783
  if (signal.aborted || turn.abortSignal.aborted) break;
788
784
  const event = unwrapOpenCodeEvent(rawEvent);
789
785
  const eventSessionId = event ? getOpenCodeEventSessionId(event) : undefined;
790
786
  if (!event || (eventSessionId && eventSessionId !== sessionId)) continue;
791
- await translateAndEmit({
792
- event,
793
- state,
794
- sessionId,
795
- permissionMode,
796
- builtinToolFiltering,
797
- client,
798
- turn,
799
- emit,
800
- });
801
- if (onEvent?.(event)) break;
802
- }
803
- }
804
-
805
- async function translateAndEmit({
806
- event,
807
- state,
808
- sessionId,
809
- permissionMode,
810
- builtinToolFiltering,
811
- client,
812
- turn,
813
- emit,
814
- }: {
815
- event: OpenCodeEvent;
816
- state: TranslationState;
817
- sessionId: string;
818
- permissionMode: StartMessage['permissionMode'];
819
- builtinToolFiltering: StartMessage['builtinToolFiltering'];
820
- client: OpenCodeClient;
821
- turn: BridgeTurn;
822
- emit: Emit;
823
- }): Promise<void> {
824
- const type = event.type;
825
- const props = event.properties ?? {};
826
-
827
- if (type === 'message.updated') {
828
- const info = props.info;
829
- if (isRecord(info)) {
830
- const id = stringValue(info.id);
831
- const role = stringValue(info.role);
832
- if (id && role) state.messageRoles.set(id, role);
833
- }
834
- return;
835
- }
836
-
837
- if (type === 'message.part.delta') {
838
- emitLegacyPartDelta({ props, state, emit });
839
- return;
840
- }
841
-
842
- if (type === 'message.part.updated') {
843
- if (emitLegacyTextPartUpdate({ part: props.part, state, emit })) return;
844
- if (emitLegacyStepFinishPart({ part: props.part, state, emit })) return;
845
- emitLegacyToolPart({ part: props.part, state, emit });
846
- return;
847
- }
848
-
849
- if (type === 'session.next.text.started') {
850
- emit({ type: 'text-start', id: String(props.textID ?? event.id) });
851
- return;
852
- }
853
- if (type === 'session.next.text.delta') {
854
- const id = String(props.textID ?? event.id);
855
- state.textDeltas.set(
856
- id,
857
- `${state.textDeltas.get(id) ?? ''}${String(props.delta ?? '')}`,
858
- );
859
- emit({
860
- type: 'text-delta',
861
- id,
862
- delta: String(props.delta ?? ''),
863
- });
864
- return;
865
- }
866
- if (type === 'session.next.text.ended') {
867
- const id = String(props.textID ?? event.id);
868
- emitMissingFinalDelta({
869
- id,
870
- fullText: typeof props.text === 'string' ? props.text : undefined,
871
- emittedText: state.textDeltas.get(id) ?? '',
872
- emit,
873
- type: 'text-delta',
874
- });
875
- emit({ type: 'text-end', id });
876
- return;
877
- }
878
- if (type === 'session.next.reasoning.started') {
879
- emit({
880
- type: 'reasoning-start',
881
- id: String(props.reasoningID ?? event.id),
882
- });
883
- return;
884
- }
885
- if (type === 'session.next.reasoning.delta') {
886
- const id = String(props.reasoningID ?? event.id);
887
- state.reasoningDeltas.set(
888
- id,
889
- `${state.reasoningDeltas.get(id) ?? ''}${String(props.delta ?? '')}`,
890
- );
891
- emit({
892
- type: 'reasoning-delta',
893
- id,
894
- delta: String(props.delta ?? ''),
895
- });
896
- return;
897
- }
898
- if (type === 'session.next.reasoning.ended') {
899
- const id = String(props.reasoningID ?? event.id);
900
- emitMissingFinalDelta({
901
- id,
902
- fullText: typeof props.text === 'string' ? props.text : undefined,
903
- emittedText: state.reasoningDeltas.get(id) ?? '',
904
- emit,
905
- type: 'reasoning-delta',
906
- });
907
- emit({ type: 'reasoning-end', id });
908
- return;
909
- }
910
- if (type === 'session.next.shell.started') {
911
- const callID = String(props.callID ?? event.id);
912
- const command = String(props.command ?? '');
913
- state.shellCommands.set(callID, command);
914
- emit({
915
- type: 'tool-call',
916
- toolCallId: callID,
917
- toolName: 'bash',
918
- nativeName: 'bash',
919
- input: JSON.stringify({ command }),
920
- providerExecuted: true,
921
- });
922
- return;
923
- }
924
- if (type === 'session.next.shell.ended') {
925
- const callID = String(props.callID ?? event.id);
926
- emit({
927
- type: 'tool-result',
928
- toolCallId: callID,
929
- toolName: 'bash',
930
- result: {
931
- command: state.shellCommands.get(callID) ?? '',
932
- output: String(props.output ?? ''),
933
- },
934
- });
935
- return;
936
- }
937
- if (type === 'session.next.tool.input.delta') {
938
- const callID = String(props.callID ?? event.id);
939
- state.toolInputs.set(
940
- callID,
941
- `${state.toolInputs.get(callID) ?? ''}${String(props.delta ?? '')}`,
942
- );
943
- return;
944
- }
945
- if (type === 'session.next.tool.input.ended') {
946
- state.toolInputs.set(
947
- String(props.callID ?? event.id),
948
- String(props.text ?? ''),
949
- );
950
- return;
951
- }
952
- if (type === 'session.next.tool.called') {
953
- const callID = String(props.callID ?? event.id);
954
- const rawToolName = String(props.tool ?? 'unknown');
955
- const toolName = toWireToolName(rawToolName);
956
- state.toolNames.set(callID, { rawToolName, toolName });
957
- const hostToolName = getHostToolName(toolName, props.tool);
958
- if (hostToolName) {
959
- authorizeHostToolCall({
960
- callID,
961
- toolName: hostToolName,
962
- input: props.input ?? parseToolInput(state, props),
963
- state,
787
+ if (event.type === 'permission.v2.asked') {
788
+ await handlePermissionV2({
789
+ client,
790
+ sessionId,
791
+ permissionMode,
792
+ builtinToolFiltering,
793
+ turn,
794
+ emit,
795
+ event,
964
796
  });
965
- return;
966
- }
967
- emit({
968
- type: 'tool-call',
969
- toolCallId: callID,
970
- toolName,
971
- ...nativeNameField({ nativeName: rawToolName, toolName }),
972
- input: JSON.stringify(props.input ?? parseToolInput(state, props)),
973
- providerExecuted: true,
974
- ...(props.provider?.metadata
975
- ? { providerMetadata: props.provider.metadata }
976
- : {}),
977
- });
978
- return;
979
- }
980
- if (
981
- type === 'session.next.tool.success' ||
982
- type === 'session.next.tool.failed'
983
- ) {
984
- const callID = String(props.callID ?? event.id);
985
- const cachedTool = state.toolNames.get(callID);
986
- const rawToolName =
987
- cachedTool?.rawToolName ??
988
- String((props as { tool?: unknown }).tool ?? '');
989
- const toolName =
990
- cachedTool?.toolName ?? toWireToolName(rawToolName || 'unknown');
991
- if (getHostToolName(toolName, rawToolName)) return;
992
- emit({
993
- type: 'tool-result',
994
- toolCallId: callID,
995
- toolName,
996
- result:
997
- props.result ??
998
- props.structured ??
999
- ('content' in props ? props.content : null) ??
1000
- null,
1001
- ...(type === 'session.next.tool.failed' ? { isError: true } : {}),
1002
- });
1003
- return;
1004
- }
1005
- if (type === 'session.next.retried') {
1006
- const error = props.error ?? event;
1007
- if (isRecord(error) && error.isRetryable === false) {
1008
- turn.emitError({
1009
- error,
1010
- message: 'OpenCode session retry failed',
797
+ } else if (event.type === 'permission.asked') {
798
+ await handlePermission({
799
+ client,
800
+ sessionId,
801
+ permissionMode,
802
+ builtinToolFiltering,
803
+ turn,
804
+ emit,
805
+ event,
1011
806
  });
1012
807
  } else {
1013
- turn.emitWarning({ message: nextRetryEventMessage(event) });
808
+ emitStreamEvent(event);
1014
809
  }
1015
- return;
1016
- }
1017
- if (type === 'session.next.step.ended') {
1018
- closeLegacyOpenParts({ state, emit });
1019
- state.turnUsage = mapUsage(props.tokens);
1020
- emit({
1021
- type: 'finish-step',
1022
- finishReason: {
1023
- unified: mapOpenCodeFinishReason(String(props.finish ?? 'stop')),
1024
- raw: String(props.finish ?? 'stop'),
1025
- },
1026
- usage: state.turnUsage,
1027
- ...(typeof props.cost === 'number'
1028
- ? { harnessMetadata: { opencode: { cost: props.cost } } }
1029
- : {}),
1030
- });
1031
- return;
1032
- }
1033
- if (type === 'session.next.compaction.ended') {
1034
- emit({
1035
- type: 'compaction',
1036
- trigger: props.reason === 'auto' ? 'auto' : 'manual',
1037
- summary: String(props.text ?? ''),
1038
- harnessMetadata: {
1039
- opencode: {
1040
- recent: String(props.recent ?? ''),
1041
- },
1042
- },
1043
- });
1044
- return;
1045
- }
1046
- if (type === 'file.edited') {
1047
- emit({
1048
- type: 'file-change',
1049
- event: 'modify',
1050
- path: stripWorkDir(String(props.file ?? '')),
1051
- });
1052
- return;
1053
- }
1054
- if (type === 'session.error' || type === 'session.next.step.failed') {
1055
- const error = props.error ?? event;
1056
- turn.emitError({
1057
- error,
1058
- message:
1059
- type === 'session.error'
1060
- ? 'OpenCode session error'
1061
- : 'OpenCode step failed',
1062
- });
1063
- return;
1064
- }
1065
- if (type === 'permission.v2.asked') {
1066
- await handlePermissionV2({
1067
- client,
1068
- sessionId,
1069
- permissionMode,
1070
- builtinToolFiltering,
1071
- turn,
1072
- emit,
1073
- event,
1074
- });
1075
- return;
1076
- }
1077
- if (type === 'permission.asked') {
1078
- await handlePermission({
1079
- client,
1080
- sessionId,
1081
- permissionMode,
1082
- builtinToolFiltering,
1083
- turn,
1084
- emit,
1085
- event,
1086
- });
1087
- }
1088
- }
1089
-
1090
- function closeLegacyOpenParts({
1091
- state,
1092
- emit,
1093
- }: {
1094
- state: TranslationState;
1095
- emit: Emit;
1096
- }): void {
1097
- for (const id of state.legacyReasoningPartIds) {
1098
- emit({ type: 'reasoning-end', id });
1099
- state.reasoningDeltas.delete(id);
1100
- }
1101
- state.legacyReasoningPartIds.clear();
1102
- for (const id of state.legacyTextPartIds) {
1103
- emit({ type: 'text-end', id });
1104
- state.textDeltas.delete(id);
1105
- }
1106
- state.legacyTextPartIds.clear();
1107
- }
1108
-
1109
- function emitLegacyStepFinishPart({
1110
- part,
1111
- state,
1112
- emit,
1113
- }: {
1114
- part: unknown;
1115
- state: TranslationState;
1116
- emit: Emit;
1117
- }): boolean {
1118
- const event = legacyStepFinishPartToFinishStep(part);
1119
- if (!event) return false;
1120
- const id = isRecord(part) ? stringValue(part.id) : undefined;
1121
- if (id) {
1122
- if (state.legacyStepFinishPartIds.has(id)) return true;
1123
- state.legacyStepFinishPartIds.add(id);
1124
- }
1125
- closeLegacyOpenParts({ state, emit });
1126
- state.turnUsage = event.usage as Record<string, unknown>;
1127
- emit(event);
1128
- return true;
1129
- }
1130
-
1131
- function emitLegacyToolPart({
1132
- part,
1133
- state,
1134
- emit,
1135
- }: {
1136
- part: unknown;
1137
- state: TranslationState;
1138
- emit: Emit;
1139
- }): void {
1140
- if (!part || typeof part !== 'object') return;
1141
- const toolPart = part as Record<string, any>;
1142
- if (toolPart.type !== 'tool') return;
1143
- const status = legacyToolPartStatus(toolPart);
1144
- if (status !== 'running' && status !== 'completed' && status !== 'error') {
1145
- return;
1146
- }
1147
- if (
1148
- typeof toolPart.tool !== 'string' ||
1149
- typeof toolPart.callID !== 'string'
1150
- ) {
1151
- return;
1152
- }
1153
- const callID = toolPart.callID;
1154
- const rawToolName = toolPart.tool;
1155
- const toolName = toWireToolName(rawToolName);
1156
- state.toolNames.set(callID, { rawToolName, toolName });
1157
- const hostToolName = getHostToolName(toolName, rawToolName);
1158
- if (hostToolName) {
1159
- if (status === 'running') {
1160
- authorizeHostToolCall({
1161
- callID,
1162
- toolName: hostToolName,
1163
- input: legacyToolPartInput(toolPart),
1164
- state,
1165
- });
1166
- }
1167
- return;
1168
- }
1169
- if (!state.toolCallsEmitted.has(callID)) {
1170
- state.toolCallsEmitted.add(callID);
1171
- emit({
1172
- type: 'tool-call',
1173
- toolCallId: callID,
1174
- toolName,
1175
- ...nativeNameField({ nativeName: rawToolName, toolName }),
1176
- input: JSON.stringify(legacyToolPartInput(toolPart)),
1177
- providerExecuted: true,
1178
- ...(toolPart.provider?.metadata
1179
- ? { providerMetadata: toolPart.provider.metadata }
1180
- : {}),
1181
- });
1182
- }
1183
- if (
1184
- (status === 'completed' || status === 'error') &&
1185
- !state.toolResultsEmitted.has(callID)
1186
- ) {
1187
- state.toolResultsEmitted.add(callID);
1188
- emit({
1189
- type: 'tool-result',
1190
- toolCallId: callID,
1191
- toolName,
1192
- result: legacyToolPartOutput(toolPart),
1193
- ...(status === 'error' ? { isError: true } : {}),
1194
- });
1195
- }
1196
- }
1197
-
1198
- function legacyToolPartStatus(part: Record<string, any>): string | undefined {
1199
- return typeof part.state === 'string'
1200
- ? part.state
1201
- : typeof part.state === 'object' && part.state !== null
1202
- ? String(part.state.status ?? '')
1203
- : undefined;
1204
- }
1205
-
1206
- function legacyToolPartInput(
1207
- part: Record<string, any>,
1208
- ): Record<string, unknown> {
1209
- const state =
1210
- typeof part.state === 'object' && part.state !== null
1211
- ? (part.state as Record<string, any>)
1212
- : undefined;
1213
- return {
1214
- ...(isRecord(part.metadata) ? part.metadata : {}),
1215
- ...(isRecord(state?.metadata) ? state.metadata : {}),
1216
- ...(isRecord(state?.input) ? state.input : {}),
1217
- };
1218
- }
1219
-
1220
- function legacyToolPartOutput(part: Record<string, any>): unknown {
1221
- const state =
1222
- typeof part.state === 'object' && part.state !== null
1223
- ? (part.state as Record<string, any>)
1224
- : undefined;
1225
- if (state?.status === 'error') {
1226
- return state.error ?? part.error ?? state.result ?? 'tool failed';
1227
- }
1228
- return (
1229
- state?.output ??
1230
- state?.result ??
1231
- state?.structured ??
1232
- state?.content ??
1233
- null
1234
- );
1235
- }
1236
-
1237
- function isRecord(value: unknown): value is Record<string, unknown> {
1238
- return Boolean(value && typeof value === 'object' && !Array.isArray(value));
1239
- }
1240
-
1241
- function parseToolInput(
1242
- state: TranslationState,
1243
- props: Record<string, any>,
1244
- ): unknown {
1245
- const text = state.toolInputs.get(String(props.callID ?? ''));
1246
- if (!text) return {};
1247
- try {
1248
- return JSON.parse(text);
1249
- } catch {
1250
- return { input: text };
810
+ if (onEvent?.(event)) break;
1251
811
  }
1252
812
  }
1253
813
 
@@ -1509,16 +1069,19 @@ function authorizeHostToolCall({
1509
1069
  async function emitContextFallback({
1510
1070
  client,
1511
1071
  sessionId,
1072
+ state,
1512
1073
  emit,
1513
1074
  emitContent,
1514
1075
  }: {
1515
1076
  client: OpenCodeClient;
1516
1077
  sessionId: string;
1078
+ state: TranslationState;
1517
1079
  emit: Emit;
1518
1080
  emitContent: boolean;
1519
1081
  }): Promise<boolean> {
1520
1082
  const assistant = await latestAssistantSnapshot({ client, sessionId });
1521
1083
  if (!assistant) return false;
1084
+ emitOpenCodeStreamStart({ info: assistant, state, emit });
1522
1085
  if (emitContent && Array.isArray(assistant.contentParts)) {
1523
1086
  for (const part of assistant.contentParts) {
1524
1087
  emitAssistantContentPart(part, emit);
@@ -1771,10 +1334,6 @@ function modelRefFromValue(value: unknown): OpenCodeModelRef | undefined {
1771
1334
  return { providerID, modelID };
1772
1335
  }
1773
1336
 
1774
- function stringValue(value: unknown): string | undefined {
1775
- return typeof value === 'string' && value.length > 0 ? value : undefined;
1776
- }
1777
-
1778
1337
  function stripWorkDir(file: string): string {
1779
1338
  if (!file) return file;
1780
1339
  const normalized = path.resolve(file);
@@ -7,6 +7,7 @@ export type OpenCodeEvent = {
7
7
  type Emit = (msg: Record<string, unknown>) => void;
8
8
 
9
9
  export type TranslationState = {
10
+ streamStarted: boolean;
10
11
  textDeltas: Map<string, string>;
11
12
  reasoningDeltas: Map<string, string>;
12
13
  toolInputs: Map<string, string>;
@@ -24,6 +25,7 @@ export type TranslationState = {
24
25
 
25
26
  export function createTranslationState(): TranslationState {
26
27
  return {
28
+ streamStarted: false,
27
29
  textDeltas: new Map(),
28
30
  reasoningDeltas: new Map(),
29
31
  toolInputs: new Map(),
@@ -40,6 +42,26 @@ export function createTranslationState(): TranslationState {
40
42
  };
41
43
  }
42
44
 
45
+ export function emitOpenCodeStreamStart({
46
+ info,
47
+ state,
48
+ emit,
49
+ }: {
50
+ info: unknown;
51
+ state: TranslationState;
52
+ emit: Emit;
53
+ }): void {
54
+ if (state.streamStarted || !isRecord(info)) return;
55
+ if (info.role !== 'assistant' && info.type !== 'assistant') return;
56
+ const providerID = stringValue(info.providerID);
57
+ const modelID = stringValue(info.modelID);
58
+ const modelId =
59
+ providerID && modelID ? `${providerID}/${modelID}` : undefined;
60
+
61
+ state.streamStarted = true;
62
+ emit({ type: 'stream-start', ...(modelId ? { modelId } : {}) });
63
+ }
64
+
43
65
  export function unwrapOpenCodeEvent(
44
66
  rawEvent: unknown,
45
67
  ): OpenCodeEvent | undefined {