@ai-sdk/harness-opencode 1.0.40 → 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.
@@ -53,6 +53,7 @@ async function runBridge(options) {
53
53
  let currentBoundPort = 0;
54
54
  let currentTurnState = "init";
55
55
  let activeSocket;
56
+ let activeSocketReadyForLiveEvents = false;
56
57
  let isFirstTurn = true;
57
58
  let turnAbort;
58
59
  let currentUserMessages;
@@ -152,7 +153,7 @@ async function runBridge(options) {
152
153
  diskBuffer += `${line}
153
154
  `;
154
155
  scheduleEventFlush();
155
- if (activeSocket?.readyState === WS_OPEN) {
156
+ if (activeSocketReadyForLiveEvents && activeSocket?.readyState === WS_OPEN) {
156
157
  try {
157
158
  activeSocket.send(line);
158
159
  } catch {
@@ -255,6 +256,8 @@ async function runBridge(options) {
255
256
  const handleInbound = async (msg, ws) => {
256
257
  switch (msg.type) {
257
258
  case "start": {
259
+ if (activeSocket !== ws) return;
260
+ activeSocketReadyForLiveEvents = true;
258
261
  const firstTurn = isFirstTurn;
259
262
  isFirstTurn = false;
260
263
  eventLog = [];
@@ -357,7 +360,9 @@ async function runBridge(options) {
357
360
  }
358
361
  return;
359
362
  case "resume":
363
+ if (activeSocket !== ws) return;
360
364
  replay(ws, msg.lastSeenEventId);
365
+ activeSocketReadyForLiveEvents = true;
361
366
  return;
362
367
  case "shutdown":
363
368
  currentTurnState = "done";
@@ -422,6 +427,7 @@ async function runBridge(options) {
422
427
  return;
423
428
  }
424
429
  activeSocket = ws;
430
+ activeSocketReadyForLiveEvents = false;
425
431
  sendControl({
426
432
  type: "bridge-hello",
427
433
  state: currentTurnState,
@@ -444,6 +450,7 @@ async function runBridge(options) {
444
450
  ws.on("close", () => {
445
451
  if (activeSocket === ws) {
446
452
  activeSocket = void 0;
453
+ activeSocketReadyForLiveEvents = false;
447
454
  }
448
455
  });
449
456
  ws.on("error", () => {
@@ -490,6 +497,7 @@ import {
490
497
  // src/bridge/opencode-events.ts
491
498
  function createTranslationState() {
492
499
  return {
500
+ streamStarted: false,
493
501
  textDeltas: /* @__PURE__ */ new Map(),
494
502
  reasoningDeltas: /* @__PURE__ */ new Map(),
495
503
  toolInputs: /* @__PURE__ */ new Map(),
@@ -505,6 +513,19 @@ function createTranslationState() {
505
513
  legacyStepFinishPartIds: /* @__PURE__ */ new Set()
506
514
  };
507
515
  }
516
+ function emitOpenCodeStreamStart({
517
+ info,
518
+ state,
519
+ emit
520
+ }) {
521
+ if (state.streamStarted || !isRecord(info)) return;
522
+ if (info.role !== "assistant" && info.type !== "assistant") return;
523
+ const providerID = stringValue(info.providerID);
524
+ const modelID = stringValue(info.modelID);
525
+ const modelId = providerID && modelID ? `${providerID}/${modelID}` : void 0;
526
+ state.streamStarted = true;
527
+ emit({ type: "stream-start", ...modelId ? { modelId } : {} });
528
+ }
508
529
  function unwrapOpenCodeEvent(rawEvent) {
509
530
  if (!rawEvent || typeof rawEvent !== "object") return void 0;
510
531
  const raw = rawEvent;
@@ -810,6 +831,393 @@ function isRecord2(value) {
810
831
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
811
832
  }
812
833
 
834
+ // src/bridge/create-emit-stream-event.ts
835
+ function createEmitStreamEvent({
836
+ state,
837
+ emit,
838
+ emitWarning,
839
+ emitError,
840
+ toWireToolName: toWireToolName2,
841
+ nativeNameField: nativeNameField2,
842
+ getHostToolName: getHostToolName2,
843
+ authorizeHostToolCall: authorizeHostToolCall2,
844
+ stripWorkDir: stripWorkDir2,
845
+ formatError: formatError2
846
+ }) {
847
+ return (event) => {
848
+ const type = event.type;
849
+ const props = event.properties ?? {};
850
+ if (type === "message.updated") {
851
+ const info = props.info;
852
+ if (isRecord3(info)) {
853
+ const id = stringValue2(info.id);
854
+ const role = stringValue2(info.role);
855
+ if (id && role) state.messageRoles.set(id, role);
856
+ }
857
+ return;
858
+ }
859
+ if (type === "message.part.delta") {
860
+ emitLegacyPartDelta({ props, state, emit });
861
+ return;
862
+ }
863
+ if (type === "message.part.updated") {
864
+ if (emitLegacyTextPartUpdate({ part: props.part, state, emit })) return;
865
+ if (emitLegacyStepFinishPart({ part: props.part, state, emit })) return;
866
+ emitLegacyToolPart({
867
+ part: props.part,
868
+ state,
869
+ emit,
870
+ toWireToolName: toWireToolName2,
871
+ nativeNameField: nativeNameField2,
872
+ getHostToolName: getHostToolName2,
873
+ authorizeHostToolCall: authorizeHostToolCall2
874
+ });
875
+ return;
876
+ }
877
+ if (type === "session.next.text.started") {
878
+ emit({ type: "text-start", id: String(props.textID ?? event.id) });
879
+ return;
880
+ }
881
+ if (type === "session.next.text.delta") {
882
+ const id = String(props.textID ?? event.id);
883
+ state.textDeltas.set(
884
+ id,
885
+ `${state.textDeltas.get(id) ?? ""}${String(props.delta ?? "")}`
886
+ );
887
+ emit({
888
+ type: "text-delta",
889
+ id,
890
+ delta: String(props.delta ?? "")
891
+ });
892
+ return;
893
+ }
894
+ if (type === "session.next.text.ended") {
895
+ const id = String(props.textID ?? event.id);
896
+ emitMissingFinalDelta({
897
+ id,
898
+ fullText: typeof props.text === "string" ? props.text : void 0,
899
+ emittedText: state.textDeltas.get(id) ?? "",
900
+ emit,
901
+ type: "text-delta"
902
+ });
903
+ emit({ type: "text-end", id });
904
+ return;
905
+ }
906
+ if (type === "session.next.reasoning.started") {
907
+ emit({
908
+ type: "reasoning-start",
909
+ id: String(props.reasoningID ?? event.id)
910
+ });
911
+ return;
912
+ }
913
+ if (type === "session.next.reasoning.delta") {
914
+ const id = String(props.reasoningID ?? event.id);
915
+ state.reasoningDeltas.set(
916
+ id,
917
+ `${state.reasoningDeltas.get(id) ?? ""}${String(props.delta ?? "")}`
918
+ );
919
+ emit({
920
+ type: "reasoning-delta",
921
+ id,
922
+ delta: String(props.delta ?? "")
923
+ });
924
+ return;
925
+ }
926
+ if (type === "session.next.reasoning.ended") {
927
+ const id = String(props.reasoningID ?? event.id);
928
+ emitMissingFinalDelta({
929
+ id,
930
+ fullText: typeof props.text === "string" ? props.text : void 0,
931
+ emittedText: state.reasoningDeltas.get(id) ?? "",
932
+ emit,
933
+ type: "reasoning-delta"
934
+ });
935
+ emit({ type: "reasoning-end", id });
936
+ return;
937
+ }
938
+ if (type === "session.next.shell.started") {
939
+ const callID = String(props.callID ?? event.id);
940
+ const command = String(props.command ?? "");
941
+ state.shellCommands.set(callID, command);
942
+ emit({
943
+ type: "tool-call",
944
+ toolCallId: callID,
945
+ toolName: "bash",
946
+ nativeName: "bash",
947
+ input: JSON.stringify({ command }),
948
+ providerExecuted: true
949
+ });
950
+ return;
951
+ }
952
+ if (type === "session.next.shell.ended") {
953
+ const callID = String(props.callID ?? event.id);
954
+ emit({
955
+ type: "tool-result",
956
+ toolCallId: callID,
957
+ toolName: "bash",
958
+ result: {
959
+ command: state.shellCommands.get(callID) ?? "",
960
+ output: String(props.output ?? "")
961
+ }
962
+ });
963
+ return;
964
+ }
965
+ if (type === "session.next.tool.input.delta") {
966
+ const callID = String(props.callID ?? event.id);
967
+ state.toolInputs.set(
968
+ callID,
969
+ `${state.toolInputs.get(callID) ?? ""}${String(props.delta ?? "")}`
970
+ );
971
+ return;
972
+ }
973
+ if (type === "session.next.tool.input.ended") {
974
+ state.toolInputs.set(
975
+ String(props.callID ?? event.id),
976
+ String(props.text ?? "")
977
+ );
978
+ return;
979
+ }
980
+ if (type === "session.next.tool.called") {
981
+ const callID = String(props.callID ?? event.id);
982
+ const rawToolName = String(props.tool ?? "unknown");
983
+ const toolName = toWireToolName2(rawToolName);
984
+ state.toolNames.set(callID, { rawToolName, toolName });
985
+ const hostToolName = getHostToolName2(toolName, props.tool);
986
+ if (hostToolName) {
987
+ authorizeHostToolCall2({
988
+ callID,
989
+ toolName: hostToolName,
990
+ input: props.input ?? parseToolInput(state, props)
991
+ });
992
+ return;
993
+ }
994
+ emit({
995
+ type: "tool-call",
996
+ toolCallId: callID,
997
+ toolName,
998
+ ...nativeNameField2({ nativeName: rawToolName, toolName }),
999
+ input: JSON.stringify(props.input ?? parseToolInput(state, props)),
1000
+ providerExecuted: true,
1001
+ ...props.provider?.metadata ? { providerMetadata: props.provider.metadata } : {}
1002
+ });
1003
+ return;
1004
+ }
1005
+ if (type === "session.next.tool.success" || type === "session.next.tool.failed") {
1006
+ const callID = String(props.callID ?? event.id);
1007
+ const cachedTool = state.toolNames.get(callID);
1008
+ const rawToolName = cachedTool?.rawToolName ?? String(props.tool ?? "");
1009
+ const toolName = cachedTool?.toolName ?? toWireToolName2(rawToolName || "unknown");
1010
+ if (getHostToolName2(toolName, rawToolName)) return;
1011
+ emit({
1012
+ type: "tool-result",
1013
+ toolCallId: callID,
1014
+ toolName,
1015
+ result: props.result ?? props.structured ?? ("content" in props ? props.content : null) ?? null,
1016
+ ...type === "session.next.tool.failed" ? { isError: true } : {}
1017
+ });
1018
+ return;
1019
+ }
1020
+ if (type === "session.next.retried") {
1021
+ const error = props.error ?? event;
1022
+ if (isRecord3(error) && error.isRetryable === false) {
1023
+ emitError({
1024
+ error,
1025
+ message: "OpenCode session retry failed"
1026
+ });
1027
+ } else {
1028
+ emitWarning({ message: nextRetryEventMessage({ event, formatError: formatError2 }) });
1029
+ }
1030
+ return;
1031
+ }
1032
+ if (type === "session.next.step.ended") {
1033
+ closeLegacyOpenParts({ state, emit });
1034
+ state.turnUsage = mapUsage(props.tokens);
1035
+ emit({
1036
+ type: "finish-step",
1037
+ finishReason: {
1038
+ unified: mapOpenCodeFinishReason(String(props.finish ?? "stop")),
1039
+ raw: String(props.finish ?? "stop")
1040
+ },
1041
+ usage: state.turnUsage,
1042
+ ...typeof props.cost === "number" ? { harnessMetadata: { opencode: { cost: props.cost } } } : {}
1043
+ });
1044
+ return;
1045
+ }
1046
+ if (type === "session.next.compaction.ended") {
1047
+ emit({
1048
+ type: "compaction",
1049
+ trigger: props.reason === "auto" ? "auto" : "manual",
1050
+ summary: String(props.text ?? ""),
1051
+ harnessMetadata: {
1052
+ opencode: {
1053
+ recent: String(props.recent ?? "")
1054
+ }
1055
+ }
1056
+ });
1057
+ return;
1058
+ }
1059
+ if (type === "file.edited") {
1060
+ emit({
1061
+ type: "file-change",
1062
+ event: "modify",
1063
+ path: stripWorkDir2(String(props.file ?? ""))
1064
+ });
1065
+ return;
1066
+ }
1067
+ if (type === "session.error" || type === "session.next.step.failed") {
1068
+ const error = props.error ?? event;
1069
+ emitError({
1070
+ error,
1071
+ message: type === "session.error" ? "OpenCode session error" : "OpenCode step failed"
1072
+ });
1073
+ }
1074
+ };
1075
+ }
1076
+ function closeLegacyOpenParts({
1077
+ state,
1078
+ emit
1079
+ }) {
1080
+ for (const id of state.legacyReasoningPartIds) {
1081
+ emit({ type: "reasoning-end", id });
1082
+ state.reasoningDeltas.delete(id);
1083
+ }
1084
+ state.legacyReasoningPartIds.clear();
1085
+ for (const id of state.legacyTextPartIds) {
1086
+ emit({ type: "text-end", id });
1087
+ state.textDeltas.delete(id);
1088
+ }
1089
+ state.legacyTextPartIds.clear();
1090
+ }
1091
+ function emitLegacyStepFinishPart({
1092
+ part,
1093
+ state,
1094
+ emit
1095
+ }) {
1096
+ const event = legacyStepFinishPartToFinishStep(part);
1097
+ if (!event) return false;
1098
+ const id = isRecord3(part) ? stringValue2(part.id) : void 0;
1099
+ if (id) {
1100
+ if (state.legacyStepFinishPartIds.has(id)) return true;
1101
+ state.legacyStepFinishPartIds.add(id);
1102
+ }
1103
+ closeLegacyOpenParts({ state, emit });
1104
+ state.turnUsage = event.usage;
1105
+ emit(event);
1106
+ return true;
1107
+ }
1108
+ function emitLegacyToolPart({
1109
+ part,
1110
+ state,
1111
+ emit,
1112
+ toWireToolName: toWireToolName2,
1113
+ nativeNameField: nativeNameField2,
1114
+ getHostToolName: getHostToolName2,
1115
+ authorizeHostToolCall: authorizeHostToolCall2
1116
+ }) {
1117
+ if (!part || typeof part !== "object") return;
1118
+ const toolPart = part;
1119
+ if (toolPart.type !== "tool") return;
1120
+ const status = legacyToolPartStatus(toolPart);
1121
+ if (status !== "running" && status !== "completed" && status !== "error") {
1122
+ return;
1123
+ }
1124
+ if (typeof toolPart.tool !== "string" || typeof toolPart.callID !== "string") {
1125
+ return;
1126
+ }
1127
+ const callID = toolPart.callID;
1128
+ const rawToolName = toolPart.tool;
1129
+ const toolName = toWireToolName2(rawToolName);
1130
+ state.toolNames.set(callID, { rawToolName, toolName });
1131
+ const hostToolName = getHostToolName2(toolName, rawToolName);
1132
+ if (hostToolName) {
1133
+ if (status === "running") {
1134
+ authorizeHostToolCall2({
1135
+ callID,
1136
+ toolName: hostToolName,
1137
+ input: legacyToolPartInput(toolPart)
1138
+ });
1139
+ }
1140
+ return;
1141
+ }
1142
+ if (!state.toolCallsEmitted.has(callID)) {
1143
+ state.toolCallsEmitted.add(callID);
1144
+ emit({
1145
+ type: "tool-call",
1146
+ toolCallId: callID,
1147
+ toolName,
1148
+ ...nativeNameField2({ nativeName: rawToolName, toolName }),
1149
+ input: JSON.stringify(legacyToolPartInput(toolPart)),
1150
+ providerExecuted: true,
1151
+ ...toolPart.provider?.metadata ? { providerMetadata: toolPart.provider.metadata } : {}
1152
+ });
1153
+ }
1154
+ if ((status === "completed" || status === "error") && !state.toolResultsEmitted.has(callID)) {
1155
+ state.toolResultsEmitted.add(callID);
1156
+ emit({
1157
+ type: "tool-result",
1158
+ toolCallId: callID,
1159
+ toolName,
1160
+ result: legacyToolPartOutput(toolPart),
1161
+ ...status === "error" ? { isError: true } : {}
1162
+ });
1163
+ }
1164
+ }
1165
+ function legacyToolPartStatus(part) {
1166
+ return typeof part.state === "string" ? part.state : typeof part.state === "object" && part.state !== null ? String(part.state.status ?? "") : void 0;
1167
+ }
1168
+ function legacyToolPartInput(part) {
1169
+ const state = typeof part.state === "object" && part.state !== null ? part.state : void 0;
1170
+ return {
1171
+ ...isRecord3(part.metadata) ? part.metadata : {},
1172
+ ...isRecord3(state?.metadata) ? state.metadata : {},
1173
+ ...isRecord3(state?.input) ? state.input : {}
1174
+ };
1175
+ }
1176
+ function legacyToolPartOutput(part) {
1177
+ const state = typeof part.state === "object" && part.state !== null ? part.state : void 0;
1178
+ if (state?.status === "error") {
1179
+ return state.error ?? part.error ?? state.result ?? "tool failed";
1180
+ }
1181
+ return state?.output ?? state?.result ?? state?.structured ?? state?.content ?? null;
1182
+ }
1183
+ function parseToolInput(state, props) {
1184
+ const text = state.toolInputs.get(String(props.callID ?? ""));
1185
+ if (!text) return {};
1186
+ try {
1187
+ return JSON.parse(text);
1188
+ } catch {
1189
+ return { input: text };
1190
+ }
1191
+ }
1192
+ function nextRetryEventMessage({
1193
+ event,
1194
+ formatError: formatError2
1195
+ }) {
1196
+ const props = event.properties ?? {};
1197
+ const details = [];
1198
+ if (typeof props.attempt === "number") {
1199
+ details.push(`attempt ${props.attempt}`);
1200
+ }
1201
+ const error = props.error;
1202
+ if (isRecord3(error)) {
1203
+ const message = stringValue2(error.message) ?? (isRecord3(error.data) ? stringValue2(error.data.message) : void 0);
1204
+ const statusCode = error.statusCode;
1205
+ if (typeof statusCode === "number") {
1206
+ details.push(`HTTP ${statusCode}`);
1207
+ }
1208
+ if (message) details.push(message);
1209
+ } else if (error != null) {
1210
+ details.push(formatError2(error));
1211
+ }
1212
+ return details.length > 0 ? `OpenCode session retry: ${details.join("; ")}` : "OpenCode session retry";
1213
+ }
1214
+ function isRecord3(value) {
1215
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
1216
+ }
1217
+ function stringValue2(value) {
1218
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1219
+ }
1220
+
813
1221
  // src/bridge/opencode-path.ts
814
1222
  import path from "path";
815
1223
  var fallbackPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
@@ -1335,25 +1743,6 @@ function legacyRetryStatusMessage(event) {
1335
1743
  }
1336
1744
  return details.length > 0 ? `OpenCode session retry: ${details.join("; ")}` : "OpenCode session retry";
1337
1745
  }
1338
- function nextRetryEventMessage(event) {
1339
- const props = event.properties ?? {};
1340
- const details = [];
1341
- if (typeof props.attempt === "number") {
1342
- details.push(`attempt ${props.attempt}`);
1343
- }
1344
- const error = props.error;
1345
- if (isRecord3(error)) {
1346
- const message = stringValue2(error.message) ?? (isRecord3(error.data) ? stringValue2(error.data.message) : void 0);
1347
- const statusCode = error.statusCode;
1348
- if (typeof statusCode === "number") {
1349
- details.push(`HTTP ${statusCode}`);
1350
- }
1351
- if (message) details.push(message);
1352
- } else if (error != null) {
1353
- details.push(formatError(error));
1354
- }
1355
- return details.length > 0 ? `OpenCode session retry: ${details.join("; ")}` : "OpenCode session retry";
1356
- }
1357
1746
  async function ensureSession({
1358
1747
  client,
1359
1748
  start,
@@ -1396,6 +1785,7 @@ async function runPrompt({
1396
1785
  let sawFinishStep = false;
1397
1786
  let sawBusy = false;
1398
1787
  let terminalError;
1788
+ const state = createTranslationState();
1399
1789
  const initialSessionTokens = await readSessionTokens({
1400
1790
  client,
1401
1791
  sessionId
@@ -1409,6 +1799,7 @@ async function runPrompt({
1409
1799
  permissionMode: start.permissionMode,
1410
1800
  builtinToolFiltering: start.builtinToolFiltering,
1411
1801
  turn,
1802
+ state,
1412
1803
  emit: (msg) => {
1413
1804
  if (msg.type === "text-delta" || msg.type === "reasoning-delta") {
1414
1805
  sawContent = true;
@@ -1425,6 +1816,13 @@ async function runPrompt({
1425
1816
  signal: eventsAbort.signal,
1426
1817
  onSubscribed: () => eventsReady.resolve(void 0),
1427
1818
  onEvent: (event) => {
1819
+ if (event.type === "message.updated") {
1820
+ emitOpenCodeStreamStart({
1821
+ info: event.properties?.info,
1822
+ state,
1823
+ emit
1824
+ });
1825
+ }
1428
1826
  if (event.type === "session.updated") {
1429
1827
  latestSessionTokens = extractSessionTokens(event.properties) ?? latestSessionTokens;
1430
1828
  }
@@ -1452,10 +1850,6 @@ async function runPrompt({
1452
1850
  eventsReady.resolve(void 0);
1453
1851
  turnSettled.resolve();
1454
1852
  });
1455
- emit({
1456
- type: "stream-start",
1457
- ...start.model ? { modelId: start.model } : {}
1458
- });
1459
1853
  await eventsReady.promise;
1460
1854
  const prompted = await legacySessionPrompt({
1461
1855
  client,
@@ -1475,6 +1869,7 @@ async function runPrompt({
1475
1869
  const emittedFallback = await emitContextFallback({
1476
1870
  client,
1477
1871
  sessionId,
1872
+ state,
1478
1873
  emit,
1479
1874
  emitContent: !sawContent
1480
1875
  }).catch(() => false);
@@ -1526,6 +1921,7 @@ async function runCompaction({
1526
1921
  permissionMode: start.permissionMode,
1527
1922
  builtinToolFiltering: start.builtinToolFiltering,
1528
1923
  turn,
1924
+ state: createTranslationState(),
1529
1925
  emit: (msg) => {
1530
1926
  if (msg.type === "compaction") sawCompaction = true;
1531
1927
  emit(msg);
@@ -1586,6 +1982,7 @@ async function consumeEvents({
1586
1982
  permissionMode,
1587
1983
  builtinToolFiltering,
1588
1984
  turn,
1985
+ state,
1589
1986
  emit,
1590
1987
  signal,
1591
1988
  onSubscribed,
@@ -1594,393 +1991,47 @@ async function consumeEvents({
1594
1991
  const stream = await subscribeLegacyEvents({ client, signal });
1595
1992
  onSubscribed?.();
1596
1993
  if (!stream) return;
1597
- const state = createTranslationState();
1994
+ const emitStreamEvent = createEmitStreamEvent({
1995
+ state,
1996
+ emit,
1997
+ emitWarning: turn.emitWarning,
1998
+ emitError: turn.emitError,
1999
+ toWireToolName,
2000
+ nativeNameField,
2001
+ getHostToolName,
2002
+ authorizeHostToolCall: (input) => authorizeHostToolCall({ ...input, state }),
2003
+ stripWorkDir,
2004
+ formatError
2005
+ });
1598
2006
  for await (const rawEvent of stream) {
1599
2007
  if (signal.aborted || turn.abortSignal.aborted) break;
1600
2008
  const event = unwrapOpenCodeEvent(rawEvent);
1601
2009
  const eventSessionId = event ? getOpenCodeEventSessionId(event) : void 0;
1602
2010
  if (!event || eventSessionId && eventSessionId !== sessionId) continue;
1603
- await translateAndEmit({
1604
- event,
1605
- state,
1606
- sessionId,
1607
- permissionMode,
1608
- builtinToolFiltering,
1609
- client,
1610
- turn,
1611
- emit
1612
- });
1613
- if (onEvent?.(event)) break;
1614
- }
1615
- }
1616
- async function translateAndEmit({
1617
- event,
1618
- state,
1619
- sessionId,
1620
- permissionMode,
1621
- builtinToolFiltering,
1622
- client,
1623
- turn,
1624
- emit
1625
- }) {
1626
- const type = event.type;
1627
- const props = event.properties ?? {};
1628
- if (type === "message.updated") {
1629
- const info = props.info;
1630
- if (isRecord3(info)) {
1631
- const id = stringValue2(info.id);
1632
- const role = stringValue2(info.role);
1633
- if (id && role) state.messageRoles.set(id, role);
1634
- }
1635
- return;
1636
- }
1637
- if (type === "message.part.delta") {
1638
- emitLegacyPartDelta({ props, state, emit });
1639
- return;
1640
- }
1641
- if (type === "message.part.updated") {
1642
- if (emitLegacyTextPartUpdate({ part: props.part, state, emit })) return;
1643
- if (emitLegacyStepFinishPart({ part: props.part, state, emit })) return;
1644
- emitLegacyToolPart({ part: props.part, state, emit });
1645
- return;
1646
- }
1647
- if (type === "session.next.text.started") {
1648
- emit({ type: "text-start", id: String(props.textID ?? event.id) });
1649
- return;
1650
- }
1651
- if (type === "session.next.text.delta") {
1652
- const id = String(props.textID ?? event.id);
1653
- state.textDeltas.set(
1654
- id,
1655
- `${state.textDeltas.get(id) ?? ""}${String(props.delta ?? "")}`
1656
- );
1657
- emit({
1658
- type: "text-delta",
1659
- id,
1660
- delta: String(props.delta ?? "")
1661
- });
1662
- return;
1663
- }
1664
- if (type === "session.next.text.ended") {
1665
- const id = String(props.textID ?? event.id);
1666
- emitMissingFinalDelta({
1667
- id,
1668
- fullText: typeof props.text === "string" ? props.text : void 0,
1669
- emittedText: state.textDeltas.get(id) ?? "",
1670
- emit,
1671
- type: "text-delta"
1672
- });
1673
- emit({ type: "text-end", id });
1674
- return;
1675
- }
1676
- if (type === "session.next.reasoning.started") {
1677
- emit({
1678
- type: "reasoning-start",
1679
- id: String(props.reasoningID ?? event.id)
1680
- });
1681
- return;
1682
- }
1683
- if (type === "session.next.reasoning.delta") {
1684
- const id = String(props.reasoningID ?? event.id);
1685
- state.reasoningDeltas.set(
1686
- id,
1687
- `${state.reasoningDeltas.get(id) ?? ""}${String(props.delta ?? "")}`
1688
- );
1689
- emit({
1690
- type: "reasoning-delta",
1691
- id,
1692
- delta: String(props.delta ?? "")
1693
- });
1694
- return;
1695
- }
1696
- if (type === "session.next.reasoning.ended") {
1697
- const id = String(props.reasoningID ?? event.id);
1698
- emitMissingFinalDelta({
1699
- id,
1700
- fullText: typeof props.text === "string" ? props.text : void 0,
1701
- emittedText: state.reasoningDeltas.get(id) ?? "",
1702
- emit,
1703
- type: "reasoning-delta"
1704
- });
1705
- emit({ type: "reasoning-end", id });
1706
- return;
1707
- }
1708
- if (type === "session.next.shell.started") {
1709
- const callID = String(props.callID ?? event.id);
1710
- const command = String(props.command ?? "");
1711
- state.shellCommands.set(callID, command);
1712
- emit({
1713
- type: "tool-call",
1714
- toolCallId: callID,
1715
- toolName: "bash",
1716
- nativeName: "bash",
1717
- input: JSON.stringify({ command }),
1718
- providerExecuted: true
1719
- });
1720
- return;
1721
- }
1722
- if (type === "session.next.shell.ended") {
1723
- const callID = String(props.callID ?? event.id);
1724
- emit({
1725
- type: "tool-result",
1726
- toolCallId: callID,
1727
- toolName: "bash",
1728
- result: {
1729
- command: state.shellCommands.get(callID) ?? "",
1730
- output: String(props.output ?? "")
1731
- }
1732
- });
1733
- return;
1734
- }
1735
- if (type === "session.next.tool.input.delta") {
1736
- const callID = String(props.callID ?? event.id);
1737
- state.toolInputs.set(
1738
- callID,
1739
- `${state.toolInputs.get(callID) ?? ""}${String(props.delta ?? "")}`
1740
- );
1741
- return;
1742
- }
1743
- if (type === "session.next.tool.input.ended") {
1744
- state.toolInputs.set(
1745
- String(props.callID ?? event.id),
1746
- String(props.text ?? "")
1747
- );
1748
- return;
1749
- }
1750
- if (type === "session.next.tool.called") {
1751
- const callID = String(props.callID ?? event.id);
1752
- const rawToolName = String(props.tool ?? "unknown");
1753
- const toolName = toWireToolName(rawToolName);
1754
- state.toolNames.set(callID, { rawToolName, toolName });
1755
- const hostToolName = getHostToolName(toolName, props.tool);
1756
- if (hostToolName) {
1757
- authorizeHostToolCall({
1758
- callID,
1759
- toolName: hostToolName,
1760
- input: props.input ?? parseToolInput(state, props),
1761
- state
2011
+ if (event.type === "permission.v2.asked") {
2012
+ await handlePermissionV2({
2013
+ client,
2014
+ sessionId,
2015
+ permissionMode,
2016
+ builtinToolFiltering,
2017
+ turn,
2018
+ emit,
2019
+ event
1762
2020
  });
1763
- return;
1764
- }
1765
- emit({
1766
- type: "tool-call",
1767
- toolCallId: callID,
1768
- toolName,
1769
- ...nativeNameField({ nativeName: rawToolName, toolName }),
1770
- input: JSON.stringify(props.input ?? parseToolInput(state, props)),
1771
- providerExecuted: true,
1772
- ...props.provider?.metadata ? { providerMetadata: props.provider.metadata } : {}
1773
- });
1774
- return;
1775
- }
1776
- if (type === "session.next.tool.success" || type === "session.next.tool.failed") {
1777
- const callID = String(props.callID ?? event.id);
1778
- const cachedTool = state.toolNames.get(callID);
1779
- const rawToolName = cachedTool?.rawToolName ?? String(props.tool ?? "");
1780
- const toolName = cachedTool?.toolName ?? toWireToolName(rawToolName || "unknown");
1781
- if (getHostToolName(toolName, rawToolName)) return;
1782
- emit({
1783
- type: "tool-result",
1784
- toolCallId: callID,
1785
- toolName,
1786
- result: props.result ?? props.structured ?? ("content" in props ? props.content : null) ?? null,
1787
- ...type === "session.next.tool.failed" ? { isError: true } : {}
1788
- });
1789
- return;
1790
- }
1791
- if (type === "session.next.retried") {
1792
- const error = props.error ?? event;
1793
- if (isRecord3(error) && error.isRetryable === false) {
1794
- turn.emitError({
1795
- error,
1796
- message: "OpenCode session retry failed"
2021
+ } else if (event.type === "permission.asked") {
2022
+ await handlePermission({
2023
+ client,
2024
+ sessionId,
2025
+ permissionMode,
2026
+ builtinToolFiltering,
2027
+ turn,
2028
+ emit,
2029
+ event
1797
2030
  });
1798
2031
  } else {
1799
- turn.emitWarning({ message: nextRetryEventMessage(event) });
1800
- }
1801
- return;
1802
- }
1803
- if (type === "session.next.step.ended") {
1804
- closeLegacyOpenParts({ state, emit });
1805
- state.turnUsage = mapUsage(props.tokens);
1806
- emit({
1807
- type: "finish-step",
1808
- finishReason: {
1809
- unified: mapOpenCodeFinishReason(String(props.finish ?? "stop")),
1810
- raw: String(props.finish ?? "stop")
1811
- },
1812
- usage: state.turnUsage,
1813
- ...typeof props.cost === "number" ? { harnessMetadata: { opencode: { cost: props.cost } } } : {}
1814
- });
1815
- return;
1816
- }
1817
- if (type === "session.next.compaction.ended") {
1818
- emit({
1819
- type: "compaction",
1820
- trigger: props.reason === "auto" ? "auto" : "manual",
1821
- summary: String(props.text ?? ""),
1822
- harnessMetadata: {
1823
- opencode: {
1824
- recent: String(props.recent ?? "")
1825
- }
1826
- }
1827
- });
1828
- return;
1829
- }
1830
- if (type === "file.edited") {
1831
- emit({
1832
- type: "file-change",
1833
- event: "modify",
1834
- path: stripWorkDir(String(props.file ?? ""))
1835
- });
1836
- return;
1837
- }
1838
- if (type === "session.error" || type === "session.next.step.failed") {
1839
- const error = props.error ?? event;
1840
- turn.emitError({
1841
- error,
1842
- message: type === "session.error" ? "OpenCode session error" : "OpenCode step failed"
1843
- });
1844
- return;
1845
- }
1846
- if (type === "permission.v2.asked") {
1847
- await handlePermissionV2({
1848
- client,
1849
- sessionId,
1850
- permissionMode,
1851
- builtinToolFiltering,
1852
- turn,
1853
- emit,
1854
- event
1855
- });
1856
- return;
1857
- }
1858
- if (type === "permission.asked") {
1859
- await handlePermission({
1860
- client,
1861
- sessionId,
1862
- permissionMode,
1863
- builtinToolFiltering,
1864
- turn,
1865
- emit,
1866
- event
1867
- });
1868
- }
1869
- }
1870
- function closeLegacyOpenParts({
1871
- state,
1872
- emit
1873
- }) {
1874
- for (const id of state.legacyReasoningPartIds) {
1875
- emit({ type: "reasoning-end", id });
1876
- state.reasoningDeltas.delete(id);
1877
- }
1878
- state.legacyReasoningPartIds.clear();
1879
- for (const id of state.legacyTextPartIds) {
1880
- emit({ type: "text-end", id });
1881
- state.textDeltas.delete(id);
1882
- }
1883
- state.legacyTextPartIds.clear();
1884
- }
1885
- function emitLegacyStepFinishPart({
1886
- part,
1887
- state,
1888
- emit
1889
- }) {
1890
- const event = legacyStepFinishPartToFinishStep(part);
1891
- if (!event) return false;
1892
- const id = isRecord3(part) ? stringValue2(part.id) : void 0;
1893
- if (id) {
1894
- if (state.legacyStepFinishPartIds.has(id)) return true;
1895
- state.legacyStepFinishPartIds.add(id);
1896
- }
1897
- closeLegacyOpenParts({ state, emit });
1898
- state.turnUsage = event.usage;
1899
- emit(event);
1900
- return true;
1901
- }
1902
- function emitLegacyToolPart({
1903
- part,
1904
- state,
1905
- emit
1906
- }) {
1907
- if (!part || typeof part !== "object") return;
1908
- const toolPart = part;
1909
- if (toolPart.type !== "tool") return;
1910
- const status = legacyToolPartStatus(toolPart);
1911
- if (status !== "running" && status !== "completed" && status !== "error") {
1912
- return;
1913
- }
1914
- if (typeof toolPart.tool !== "string" || typeof toolPart.callID !== "string") {
1915
- return;
1916
- }
1917
- const callID = toolPart.callID;
1918
- const rawToolName = toolPart.tool;
1919
- const toolName = toWireToolName(rawToolName);
1920
- state.toolNames.set(callID, { rawToolName, toolName });
1921
- const hostToolName = getHostToolName(toolName, rawToolName);
1922
- if (hostToolName) {
1923
- if (status === "running") {
1924
- authorizeHostToolCall({
1925
- callID,
1926
- toolName: hostToolName,
1927
- input: legacyToolPartInput(toolPart),
1928
- state
1929
- });
2032
+ emitStreamEvent(event);
1930
2033
  }
1931
- return;
1932
- }
1933
- if (!state.toolCallsEmitted.has(callID)) {
1934
- state.toolCallsEmitted.add(callID);
1935
- emit({
1936
- type: "tool-call",
1937
- toolCallId: callID,
1938
- toolName,
1939
- ...nativeNameField({ nativeName: rawToolName, toolName }),
1940
- input: JSON.stringify(legacyToolPartInput(toolPart)),
1941
- providerExecuted: true,
1942
- ...toolPart.provider?.metadata ? { providerMetadata: toolPart.provider.metadata } : {}
1943
- });
1944
- }
1945
- if ((status === "completed" || status === "error") && !state.toolResultsEmitted.has(callID)) {
1946
- state.toolResultsEmitted.add(callID);
1947
- emit({
1948
- type: "tool-result",
1949
- toolCallId: callID,
1950
- toolName,
1951
- result: legacyToolPartOutput(toolPart),
1952
- ...status === "error" ? { isError: true } : {}
1953
- });
1954
- }
1955
- }
1956
- function legacyToolPartStatus(part) {
1957
- return typeof part.state === "string" ? part.state : typeof part.state === "object" && part.state !== null ? String(part.state.status ?? "") : void 0;
1958
- }
1959
- function legacyToolPartInput(part) {
1960
- const state = typeof part.state === "object" && part.state !== null ? part.state : void 0;
1961
- return {
1962
- ...isRecord3(part.metadata) ? part.metadata : {},
1963
- ...isRecord3(state?.metadata) ? state.metadata : {},
1964
- ...isRecord3(state?.input) ? state.input : {}
1965
- };
1966
- }
1967
- function legacyToolPartOutput(part) {
1968
- const state = typeof part.state === "object" && part.state !== null ? part.state : void 0;
1969
- if (state?.status === "error") {
1970
- return state.error ?? part.error ?? state.result ?? "tool failed";
1971
- }
1972
- return state?.output ?? state?.result ?? state?.structured ?? state?.content ?? null;
1973
- }
1974
- function isRecord3(value) {
1975
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
1976
- }
1977
- function parseToolInput(state, props) {
1978
- const text = state.toolInputs.get(String(props.callID ?? ""));
1979
- if (!text) return {};
1980
- try {
1981
- return JSON.parse(text);
1982
- } catch {
1983
- return { input: text };
2034
+ if (onEvent?.(event)) break;
1984
2035
  }
1985
2036
  }
1986
2037
  async function handlePermissionV2({
@@ -2153,11 +2204,13 @@ function authorizeHostToolCall({
2153
2204
  async function emitContextFallback({
2154
2205
  client,
2155
2206
  sessionId,
2207
+ state,
2156
2208
  emit,
2157
2209
  emitContent
2158
2210
  }) {
2159
2211
  const assistant = await latestAssistantSnapshot({ client, sessionId });
2160
2212
  if (!assistant) return false;
2213
+ emitOpenCodeStreamStart({ info: assistant, state, emit });
2161
2214
  if (emitContent && Array.isArray(assistant.contentParts)) {
2162
2215
  for (const part of assistant.contentParts) {
2163
2216
  emitAssistantContentPart(part, emit);
@@ -2316,9 +2369,6 @@ function modelRefFromValue(value) {
2316
2369
  if (!providerID || !modelID) return void 0;
2317
2370
  return { providerID, modelID };
2318
2371
  }
2319
- function stringValue2(value) {
2320
- return typeof value === "string" && value.length > 0 ? value : void 0;
2321
- }
2322
2372
  function stripWorkDir(file) {
2323
2373
  if (!file) return file;
2324
2374
  const normalized = path2.resolve(file);