@ai-sdk/harness-opencode 1.0.36 → 1.0.38

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.36" : "0.0.0-test";
171
+ var VERSION = true ? "1.0.38" : "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.36",
3
+ "version": "1.0.38",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -27,8 +27,8 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "ws": "8.21.0",
30
- "@ai-sdk/harness": "1.0.35",
31
- "@ai-sdk/provider-utils": "5.0.10"
30
+ "@ai-sdk/harness": "1.0.37",
31
+ "@ai-sdk/provider-utils": "5.0.11"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "zod": "^3.25.76 || ^4.1.8"
@@ -14,10 +14,14 @@ import {
14
14
  createOpencodeServer,
15
15
  } from '@opencode-ai/sdk/v2';
16
16
  import {
17
+ createTranslationState,
18
+ emitLegacyPartDelta,
19
+ emitLegacyTextPartUpdate,
17
20
  emitMissingFinalDelta,
18
21
  getOpenCodeEventSessionId,
19
22
  isStepSettlementEvent,
20
23
  type OpenCodeEvent,
24
+ type TranslationState,
21
25
  unwrapOpenCodeEvent,
22
26
  } from './opencode-events';
23
27
  import {
@@ -798,40 +802,6 @@ async function consumeEvents({
798
802
  }
799
803
  }
800
804
 
801
- type TranslationState = {
802
- textDeltas: Map<string, string>;
803
- reasoningDeltas: Map<string, string>;
804
- toolInputs: Map<string, string>;
805
- toolNames: Map<string, { rawToolName: string; toolName: string }>;
806
- toolCallsEmitted: Set<string>;
807
- toolResultsEmitted: Set<string>;
808
- hostToolCallsAuthorized: Set<string>;
809
- shellCommands: Map<string, string>;
810
- messageRoles: Map<string, string>;
811
- turnUsage: Record<string, unknown> | undefined;
812
- legacyTextPartIds: Set<string>;
813
- legacyReasoningPartIds: Set<string>;
814
- legacyStepFinishPartIds: Set<string>;
815
- };
816
-
817
- function createTranslationState(): TranslationState {
818
- return {
819
- textDeltas: new Map(),
820
- reasoningDeltas: new Map(),
821
- toolInputs: new Map(),
822
- toolNames: new Map(),
823
- toolCallsEmitted: new Set(),
824
- toolResultsEmitted: new Set(),
825
- hostToolCallsAuthorized: new Set(),
826
- shellCommands: new Map(),
827
- messageRoles: new Map(),
828
- turnUsage: undefined,
829
- legacyTextPartIds: new Set(),
830
- legacyReasoningPartIds: new Set(),
831
- legacyStepFinishPartIds: new Set(),
832
- };
833
- }
834
-
835
805
  async function translateAndEmit({
836
806
  event,
837
807
  state,
@@ -865,32 +835,7 @@ async function translateAndEmit({
865
835
  }
866
836
 
867
837
  if (type === 'message.part.delta') {
868
- const field = String(props.field ?? '');
869
- const delta = String(props.delta ?? '');
870
- if (!delta) return;
871
- const messageID = stringValue(props.messageID);
872
- if (messageID && state.messageRoles.get(messageID) === 'user') return;
873
- if (field === 'text') {
874
- const id = legacyPartId({ value: props, fallback: 'legacy-text' });
875
- startLegacyPart({ ids: state.legacyTextPartIds, id, emit, type: 'text' });
876
- state.textDeltas.set(id, `${state.textDeltas.get(id) ?? ''}${delta}`);
877
- emit({ type: 'text-delta', id, delta });
878
- return;
879
- }
880
- if (field === 'reasoning') {
881
- const id = legacyPartId({ value: props, fallback: 'legacy-reasoning' });
882
- startLegacyPart({
883
- ids: state.legacyReasoningPartIds,
884
- id,
885
- emit,
886
- type: 'reasoning',
887
- });
888
- state.reasoningDeltas.set(
889
- id,
890
- `${state.reasoningDeltas.get(id) ?? ''}${delta}`,
891
- );
892
- emit({ type: 'reasoning-delta', id, delta });
893
- }
838
+ emitLegacyPartDelta({ props, state, emit });
894
839
  return;
895
840
  }
896
841
 
@@ -1142,88 +1087,6 @@ async function translateAndEmit({
1142
1087
  }
1143
1088
  }
1144
1089
 
1145
- function legacyPartId({
1146
- value,
1147
- fallback,
1148
- }: {
1149
- value: Record<string, unknown>;
1150
- fallback: string;
1151
- }): string {
1152
- return stringValue(value.partID) ?? stringValue(value.id) ?? fallback;
1153
- }
1154
-
1155
- function startLegacyPart({
1156
- ids,
1157
- id,
1158
- emit,
1159
- type,
1160
- }: {
1161
- ids: Set<string>;
1162
- id: string;
1163
- emit: Emit;
1164
- type: 'text' | 'reasoning';
1165
- }): void {
1166
- if (ids.has(id)) return;
1167
- ids.add(id);
1168
- emit({ type: `${type}-start`, id });
1169
- }
1170
-
1171
- function emitLegacyTextPartUpdate({
1172
- part,
1173
- state,
1174
- emit,
1175
- }: {
1176
- part: unknown;
1177
- state: TranslationState;
1178
- emit: Emit;
1179
- }): boolean {
1180
- if (!isRecord(part)) return false;
1181
- if (part.type !== 'text' && part.type !== 'reasoning') return false;
1182
- const id = stringValue(part.id);
1183
- if (!id) return true;
1184
-
1185
- const messageID = stringValue(part.messageID);
1186
- if (messageID && state.messageRoles.get(messageID) === 'user') return true;
1187
-
1188
- const isReasoning = part.type === 'reasoning';
1189
- const ids = isReasoning
1190
- ? state.legacyReasoningPartIds
1191
- : state.legacyTextPartIds;
1192
- const deltaMap = isReasoning ? state.reasoningDeltas : state.textDeltas;
1193
- const deltaType = isReasoning ? 'reasoning-delta' : 'text-delta';
1194
- const text = typeof part.text === 'string' ? part.text : undefined;
1195
-
1196
- startLegacyPart({
1197
- ids,
1198
- id,
1199
- emit,
1200
- type: isReasoning ? 'reasoning' : 'text',
1201
- });
1202
-
1203
- if (text !== undefined) {
1204
- emitMissingFinalDelta({
1205
- id,
1206
- fullText: text,
1207
- emittedText: deltaMap.get(id) ?? '',
1208
- emit,
1209
- type: deltaType,
1210
- });
1211
- deltaMap.set(id, text);
1212
- }
1213
-
1214
- if (legacyPartEnded(part)) {
1215
- ids.delete(id);
1216
- deltaMap.delete(id);
1217
- emit({ type: isReasoning ? 'reasoning-end' : 'text-end', id });
1218
- }
1219
-
1220
- return true;
1221
- }
1222
-
1223
- function legacyPartEnded(part: Record<string, unknown>): boolean {
1224
- return isRecord(part.time) && part.time.end != null;
1225
- }
1226
-
1227
1090
  function closeLegacyOpenParts({
1228
1091
  state,
1229
1092
  emit,
@@ -4,6 +4,42 @@ export type OpenCodeEvent = {
4
4
  properties?: Record<string, any>;
5
5
  };
6
6
 
7
+ type Emit = (msg: Record<string, unknown>) => void;
8
+
9
+ export type TranslationState = {
10
+ textDeltas: Map<string, string>;
11
+ reasoningDeltas: Map<string, string>;
12
+ toolInputs: Map<string, string>;
13
+ toolNames: Map<string, { rawToolName: string; toolName: string }>;
14
+ toolCallsEmitted: Set<string>;
15
+ toolResultsEmitted: Set<string>;
16
+ hostToolCallsAuthorized: Set<string>;
17
+ shellCommands: Map<string, string>;
18
+ messageRoles: Map<string, string>;
19
+ turnUsage: Record<string, unknown> | undefined;
20
+ legacyTextPartIds: Set<string>;
21
+ legacyReasoningPartIds: Set<string>;
22
+ legacyStepFinishPartIds: Set<string>;
23
+ };
24
+
25
+ export function createTranslationState(): TranslationState {
26
+ return {
27
+ textDeltas: new Map(),
28
+ reasoningDeltas: new Map(),
29
+ toolInputs: new Map(),
30
+ toolNames: new Map(),
31
+ toolCallsEmitted: new Set(),
32
+ toolResultsEmitted: new Set(),
33
+ hostToolCallsAuthorized: new Set(),
34
+ shellCommands: new Map(),
35
+ messageRoles: new Map(),
36
+ turnUsage: undefined,
37
+ legacyTextPartIds: new Set(),
38
+ legacyReasoningPartIds: new Set(),
39
+ legacyStepFinishPartIds: new Set(),
40
+ };
41
+ }
42
+
7
43
  export function unwrapOpenCodeEvent(
8
44
  rawEvent: unknown,
9
45
  ): OpenCodeEvent | undefined {
@@ -77,6 +113,114 @@ export function emitMissingFinalDelta({
77
113
  emit({ type, id, delta: fullText.slice(emittedText.length) });
78
114
  }
79
115
 
116
+ /**
117
+ * Translates an OpenCode `message.part.delta` event (a streaming text or
118
+ * reasoning delta) into legacy stream parts.
119
+ */
120
+ export function emitLegacyPartDelta({
121
+ props,
122
+ state,
123
+ emit,
124
+ }: {
125
+ props: Record<string, unknown>;
126
+ state: TranslationState;
127
+ emit: Emit;
128
+ }): void {
129
+ const field = String(props.field ?? '');
130
+ const delta = String(props.delta ?? '');
131
+ if (!delta) return;
132
+ const messageID = stringValue(props.messageID);
133
+ if (messageID && state.messageRoles.get(messageID) === 'user') return;
134
+ if (field === 'text') {
135
+ const id = legacyPartId({ value: props, fallback: 'legacy-text' });
136
+ // OpenCode publishes reasoning deltas with field:"text". If this id was
137
+ // already announced as a reasoning part, route it to reasoning so it is not
138
+ // duplicated as a text part.
139
+ if (state.legacyReasoningPartIds.has(id)) {
140
+ state.reasoningDeltas.set(
141
+ id,
142
+ `${state.reasoningDeltas.get(id) ?? ''}${delta}`,
143
+ );
144
+ emit({ type: 'reasoning-delta', id, delta });
145
+ return;
146
+ }
147
+ startLegacyPart({ ids: state.legacyTextPartIds, id, emit, type: 'text' });
148
+ state.textDeltas.set(id, `${state.textDeltas.get(id) ?? ''}${delta}`);
149
+ emit({ type: 'text-delta', id, delta });
150
+ return;
151
+ }
152
+ if (field === 'reasoning') {
153
+ const id = legacyPartId({ value: props, fallback: 'legacy-reasoning' });
154
+ startLegacyPart({
155
+ ids: state.legacyReasoningPartIds,
156
+ id,
157
+ emit,
158
+ type: 'reasoning',
159
+ });
160
+ state.reasoningDeltas.set(
161
+ id,
162
+ `${state.reasoningDeltas.get(id) ?? ''}${delta}`,
163
+ );
164
+ emit({ type: 'reasoning-delta', id, delta });
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Translates an OpenCode `message.part.updated` event for a text or reasoning
170
+ * part. Returns `true` when it handled the part.
171
+ */
172
+ export function emitLegacyTextPartUpdate({
173
+ part,
174
+ state,
175
+ emit,
176
+ }: {
177
+ part: unknown;
178
+ state: TranslationState;
179
+ emit: Emit;
180
+ }): boolean {
181
+ if (!isRecord(part)) return false;
182
+ if (part.type !== 'text' && part.type !== 'reasoning') return false;
183
+ const id = stringValue(part.id);
184
+ if (!id) return true;
185
+
186
+ const messageID = stringValue(part.messageID);
187
+ if (messageID && state.messageRoles.get(messageID) === 'user') return true;
188
+
189
+ const isReasoning = part.type === 'reasoning';
190
+ const ids = isReasoning
191
+ ? state.legacyReasoningPartIds
192
+ : state.legacyTextPartIds;
193
+ const deltaMap = isReasoning ? state.reasoningDeltas : state.textDeltas;
194
+ const deltaType = isReasoning ? 'reasoning-delta' : 'text-delta';
195
+ const text = typeof part.text === 'string' ? part.text : undefined;
196
+
197
+ startLegacyPart({
198
+ ids,
199
+ id,
200
+ emit,
201
+ type: isReasoning ? 'reasoning' : 'text',
202
+ });
203
+
204
+ if (text !== undefined) {
205
+ emitMissingFinalDelta({
206
+ id,
207
+ fullText: text,
208
+ emittedText: deltaMap.get(id) ?? '',
209
+ emit,
210
+ type: deltaType,
211
+ });
212
+ deltaMap.set(id, text);
213
+ }
214
+
215
+ if (legacyPartEnded(part)) {
216
+ ids.delete(id);
217
+ deltaMap.delete(id);
218
+ emit({ type: isReasoning ? 'reasoning-end' : 'text-end', id });
219
+ }
220
+
221
+ return true;
222
+ }
223
+
80
224
  function stripSyncVersion(type: string): string {
81
225
  return type.replace(/\.\d+$/, '');
82
226
  }
@@ -86,3 +230,41 @@ function asRecord(value: unknown): Record<string, any> | undefined {
86
230
  return undefined;
87
231
  return value as Record<string, any>;
88
232
  }
233
+
234
+ function isRecord(value: unknown): value is Record<string, unknown> {
235
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
236
+ }
237
+
238
+ function stringValue(value: unknown): string | undefined {
239
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
240
+ }
241
+
242
+ function legacyPartId({
243
+ value,
244
+ fallback,
245
+ }: {
246
+ value: Record<string, unknown>;
247
+ fallback: string;
248
+ }): string {
249
+ return stringValue(value.partID) ?? stringValue(value.id) ?? fallback;
250
+ }
251
+
252
+ function startLegacyPart({
253
+ ids,
254
+ id,
255
+ emit,
256
+ type,
257
+ }: {
258
+ ids: Set<string>;
259
+ id: string;
260
+ emit: Emit;
261
+ type: 'text' | 'reasoning';
262
+ }): void {
263
+ if (ids.has(id)) return;
264
+ ids.add(id);
265
+ emit({ type: `${type}-start`, id });
266
+ }
267
+
268
+ function legacyPartEnded(part: Record<string, unknown>): boolean {
269
+ return isRecord(part.time) && part.time.end != null;
270
+ }