@ai-sdk/harness-claude-code 1.0.21 → 1.0.23

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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @ai-sdk/harness-claude-code
2
2
 
3
+ ## 1.0.23
4
+
5
+ ### Patch Changes
6
+
7
+ - 39c8276: fix(harness): improve opaque sandbox bridge error handling
8
+ - 91fe6d8: fix(harness): emit `finish-step` stream parts correctly per the underlying model steps
9
+ - dcc7ecd: fix(harness-claude-code): fix Claude Code not displaying its reasoning by default
10
+ - Updated dependencies [39c8276]
11
+ - Updated dependencies [91fe6d8]
12
+ - Updated dependencies [0be5014]
13
+ - @ai-sdk/harness@1.0.23
14
+
15
+ ## 1.0.22
16
+
17
+ ### Patch Changes
18
+
19
+ - @ai-sdk/harness@1.0.22
20
+ - @ai-sdk/provider-utils@5.0.7
21
+
3
22
  ## 1.0.21
4
23
 
5
24
  ### Patch Changes
package/README.md CHANGED
@@ -42,7 +42,9 @@ const agent = new HarnessAgent({
42
42
  }),
43
43
  },
44
44
  harnessOptions: {
45
- 'claude-code': { thinking: 'adaptive' },
45
+ 'claude-code': {
46
+ thinking: { type: 'adaptive', display: 'summarized' },
47
+ },
46
48
  },
47
49
  });
48
50
 
@@ -20,6 +20,15 @@ function formatBridgeError(err) {
20
20
  if (err instanceof Error) {
21
21
  return { name: err.name, message: err.message, stack: err.stack };
22
22
  }
23
+ if (typeof err === "string") {
24
+ return { message: err };
25
+ }
26
+ if (err !== null && typeof err === "object") {
27
+ try {
28
+ return { message: JSON.stringify(err) };
29
+ } catch {
30
+ }
31
+ }
23
32
  return { message: String(err) };
24
33
  }
25
34
  function parseEnvList(value) {
@@ -47,6 +56,7 @@ async function runBridge(options) {
47
56
  let isFirstTurn = true;
48
57
  let turnAbort;
49
58
  let currentUserMessages;
59
+ let currentInterruptHandler;
50
60
  let debugConfig;
51
61
  let consoleCaptureInstalled = false;
52
62
  const envDebugEnabled = ENV_TRUTHY.has(
@@ -164,6 +174,38 @@ async function runBridge(options) {
164
174
  };
165
175
  const rawStdoutWrite = process.stdout.write.bind(process.stdout);
166
176
  const rawStderrWrite = process.stderr.write.bind(process.stderr);
177
+ const writeErrorToStderr = (input) => {
178
+ try {
179
+ const formatted = formatBridgeError(input.error);
180
+ rawStderrWrite(
181
+ `[harness:${bridgeType}:error] ${input.message}: ${formatted.message}
182
+ `
183
+ );
184
+ if (formatted.stack) {
185
+ rawStderrWrite(`${formatted.stack}
186
+ `);
187
+ }
188
+ } catch {
189
+ }
190
+ };
191
+ const emitWarning = (input) => {
192
+ try {
193
+ for (const line of input.message.split("\n")) {
194
+ if (line.trim().length > 0) {
195
+ rawStderrWrite(`[harness:${bridgeType}:warn] ${line}
196
+ `);
197
+ }
198
+ }
199
+ } catch {
200
+ }
201
+ };
202
+ const emitError = (input) => {
203
+ writeErrorToStderr({
204
+ message: input.message ?? "bridge error",
205
+ error: input.error
206
+ });
207
+ emit({ type: "error", error: serialiseError(input.error) });
208
+ };
167
209
  const installConsoleCapture = () => {
168
210
  if (consoleCaptureInstalled) return;
169
211
  consoleCaptureInstalled = true;
@@ -221,6 +263,7 @@ async function runBridge(options) {
221
263
  });
222
264
  turnAbort = new AbortController();
223
265
  currentTurnState = "running";
266
+ currentInterruptHandler = void 0;
224
267
  void writeStartConfig(msg);
225
268
  void writeBridgeMeta("running");
226
269
  const startDebug = msg.debug;
@@ -242,6 +285,9 @@ async function runBridge(options) {
242
285
  }),
243
286
  pendingUserMessages: [],
244
287
  abortSignal: turnAbort.signal,
288
+ onInterrupt: (handler) => {
289
+ currentInterruptHandler = handler;
290
+ },
245
291
  firstTurn,
246
292
  bridgeLog: (input) => {
247
293
  const level = input.level ?? "debug";
@@ -254,14 +300,17 @@ async function runBridge(options) {
254
300
  ...input.attrs ? { attrs: input.attrs } : {},
255
301
  ...input.error !== void 0 ? { error: formatBridgeError(input.error) } : {}
256
302
  });
257
- }
303
+ },
304
+ emitWarning,
305
+ emitError
258
306
  };
259
307
  currentUserMessages = turn.pendingUserMessages;
260
308
  try {
261
309
  await onStart(msg, turn);
262
310
  } catch (err) {
263
- emit({ type: "error", error: serialiseError(err) });
311
+ emitError({ error: err, message: "bridge turn failed" });
264
312
  } finally {
313
+ currentInterruptHandler = void 0;
265
314
  currentTurnState = "waiting";
266
315
  void writeBridgeMeta("waiting");
267
316
  }
@@ -289,6 +338,22 @@ async function runBridge(options) {
289
338
  case "abort":
290
339
  turnAbort?.abort();
291
340
  return;
341
+ case "interrupt":
342
+ try {
343
+ if (currentInterruptHandler) {
344
+ await currentInterruptHandler();
345
+ } else {
346
+ turnAbort?.abort();
347
+ }
348
+ sendControl({ type: "bridge-interrupted", ok: true });
349
+ } catch (err) {
350
+ sendControl({
351
+ type: "bridge-interrupted",
352
+ ok: false,
353
+ error: serialiseError(err)
354
+ });
355
+ }
356
+ return;
292
357
  case "resume":
293
358
  replay(ws, msg.lastSeenEventId);
294
359
  return;
@@ -383,10 +448,10 @@ async function runBridge(options) {
383
448
  });
384
449
  });
385
450
  process.on("uncaughtException", (err) => {
386
- emit({ type: "error", error: serialiseError(err) });
451
+ emitError({ error: err, message: "uncaught exception" });
387
452
  });
388
453
  process.on("unhandledRejection", (err) => {
389
- emit({ type: "error", error: serialiseError(err) });
454
+ emitError({ error: err, message: "unhandled rejection" });
390
455
  });
391
456
  await new Promise((resolve, reject) => {
392
457
  if (wss.address() != null) {
@@ -488,6 +553,7 @@ var PUBLIC_TO_NATIVE = {
488
553
  Skill: "Skill"
489
554
  };
490
555
  var PUBLIC_TOOL_NAMES = Object.keys(PUBLIC_TO_NATIVE);
556
+ var UNRECOVERABLE_API_RETRY_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
491
557
  var NATIVE_TOOL_KINDS = {
492
558
  Read: "readonly",
493
559
  Glob: "readonly",
@@ -536,18 +602,6 @@ function resolveInactiveNativeTools(start) {
536
602
  ) : toolFiltering.toolNames;
537
603
  return inactiveToolNames.map((name) => toNativeName(name));
538
604
  }
539
- function toThinkingConfig(thinking) {
540
- switch (thinking) {
541
- case "adaptive":
542
- return { type: "adaptive", display: "summarized" };
543
- case "on":
544
- return { type: "enabled", display: "summarized" };
545
- case "off":
546
- return { type: "disabled" };
547
- default:
548
- return void 0;
549
- }
550
- }
551
605
  var args = parseArgs(argv.slice(2));
552
606
  var workdir = args.workdir;
553
607
  var bridgeStateDir = args.bridgeStateDir;
@@ -610,6 +664,7 @@ function createPermissionOptions(input) {
610
664
  approvalId,
611
665
  toolCallId: approvalId
612
666
  });
667
+ input.finishApprovalStep(approvalId);
613
668
  const decision = await input.turn.requestToolApproval(approvalId);
614
669
  return decision.approved ? { behavior: "allow", updatedInput: toolInput, toolUseID: approvalId } : {
615
670
  behavior: "deny",
@@ -650,6 +705,28 @@ async function runTurn(start, turn) {
650
705
  }
651
706
  const nativeToolCallNames = /* @__PURE__ */ new Map();
652
707
  const approvalRequestedToolUseIds = /* @__PURE__ */ new Set();
708
+ const partialBlocks = /* @__PURE__ */ new Map();
709
+ let stepUsage;
710
+ let pendingStepToolUseIds = /* @__PURE__ */ new Set();
711
+ let pendingStepUsage;
712
+ let stepOpen = false;
713
+ const emitFinishStep = (usage) => {
714
+ emit({
715
+ type: "finish-step",
716
+ finishReason: { unified: "stop", raw: "stop" },
717
+ usage: usage ?? defaultUsage()
718
+ });
719
+ stepUsage = usage ?? stepUsage;
720
+ pendingStepUsage = void 0;
721
+ pendingStepToolUseIds = /* @__PURE__ */ new Set();
722
+ stepOpen = false;
723
+ };
724
+ const closeStepIfReady = () => {
725
+ if (!stepOpen || pendingStepToolUseIds.size > 0 || partialBlocks.size > 0) {
726
+ return;
727
+ }
728
+ emitFinishStep(pendingStepUsage);
729
+ };
653
730
  const mcpToolUseIds = /* @__PURE__ */ new Set();
654
731
  const mcpServers = {};
655
732
  if (start.tools && start.tools.length > 0) {
@@ -707,6 +784,11 @@ async function runTurn(start, turn) {
707
784
  inactiveNativeTools,
708
785
  turn,
709
786
  emit,
787
+ finishApprovalStep: (approvalId) => {
788
+ stepOpen = true;
789
+ pendingStepToolUseIds.delete(approvalId);
790
+ closeStepIfReady();
791
+ },
710
792
  nativeToolCallNames,
711
793
  approvalRequestedToolUseIds
712
794
  });
@@ -718,7 +800,7 @@ async function runTurn(start, turn) {
718
800
  ...skillsOption ? { skills: skillsOption } : {},
719
801
  ...nativeTools !== void 0 ? { tools: nativeTools } : {},
720
802
  ...inactiveNativeTools.length > 0 ? { disallowedTools: inactiveNativeTools } : {},
721
- ...toThinkingConfig(start.thinking) ? { thinking: toThinkingConfig(start.thinking) } : {},
803
+ thinking: start.thinking,
722
804
  includePartialMessages: true,
723
805
  // The `PostCompact` hook carries the compaction summary, which the
724
806
  // `compact_boundary` system message does not. Latch it for the unified
@@ -748,28 +830,28 @@ async function runTurn(start, turn) {
748
830
  abortSignal: abortCtl.signal
749
831
  }
750
832
  });
751
- let stepUsage;
833
+ turn.onInterrupt(() => q.interrupt());
834
+ let turnUsage;
752
835
  let totalCostUsd;
753
836
  let observedTerminalError;
754
837
  let emittedTerminalError = false;
755
838
  let emittedTerminalFinish = false;
756
839
  let streamStarted = false;
757
- const partialBlocks = /* @__PURE__ */ new Map();
758
840
  const emitTerminalError = (message) => {
759
841
  const normalized = message?.trim();
760
842
  if (!normalized || emittedTerminalError || emittedTerminalFinish) return;
761
843
  observedTerminalError = normalized;
762
844
  emittedTerminalError = true;
763
- emit({ type: "error", error: normalized });
845
+ turn.emitError({
846
+ error: normalized,
847
+ message: "claude-code terminal error"
848
+ });
764
849
  queryInput.close();
765
850
  abortCtl.abort();
766
851
  };
767
852
  try {
768
853
  for await (const msg of q) {
769
854
  if (abortCtl.signal.aborted) break;
770
- if (typeof msg.error === "string" && msg.error.trim()) {
771
- observedTerminalError = msg.error.trim();
772
- }
773
855
  const type = msg.type;
774
856
  if (!streamStarted) {
775
857
  const initModel = type === "system" && msg.subtype === "init" && typeof msg.model === "string" ? msg.model : void 0;
@@ -779,14 +861,21 @@ async function runTurn(start, turn) {
779
861
  });
780
862
  streamStarted = true;
781
863
  }
782
- if (type === "auth_status" && typeof msg.error === "string" && msg.error.trim()) {
783
- emitTerminalError(msg.error);
864
+ if (type === "system" && msg.subtype === "api_retry") {
865
+ if (typeof msg.error_status === "number" && UNRECOVERABLE_API_RETRY_STATUSES.has(msg.error_status)) {
866
+ emitTerminalError(
867
+ `HTTP ${msg.error_status}: ${msg.error ?? "provider request failed"}`
868
+ );
869
+ continue;
870
+ }
871
+ turn.emitWarning({ message: formatApiRetryWarning(msg) });
784
872
  continue;
785
873
  }
786
- if (type === "system" && msg.subtype === "api_retry" && typeof msg.error_status === "number" && [401, 403, 404].includes(msg.error_status)) {
787
- emitTerminalError(
788
- `HTTP ${msg.error_status}: ${msg.error ?? "provider request failed"}`
789
- );
874
+ if (typeof msg.error === "string" && msg.error.trim()) {
875
+ observedTerminalError = msg.error.trim();
876
+ }
877
+ if (type === "auth_status" && typeof msg.error === "string" && msg.error.trim()) {
878
+ emitTerminalError(msg.error);
790
879
  continue;
791
880
  }
792
881
  if (type === "system" && msg.subtype === "task_updated" && msg.patch?.status === "failed" && typeof msg.patch.error === "string") {
@@ -809,17 +898,25 @@ async function runTurn(start, turn) {
809
898
  continue;
810
899
  }
811
900
  if (type === "assistant" && msg.message?.content) {
901
+ const usage = mapUsage(msg.message.usage);
902
+ const toolUseIds = [];
903
+ let opensStep = false;
812
904
  for (const block of msg.message.content) {
813
905
  if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
906
+ toolUseIds.push(block.id);
814
907
  const mcpPrefix = "mcp__harness-tools__";
815
908
  if (block.name.startsWith(mcpPrefix)) {
909
+ pendingStepToolUseIds.add(block.id);
816
910
  mcpToolUseIds.add(block.id);
911
+ opensStep = true;
817
912
  continue;
818
913
  }
819
914
  nativeToolCallNames.set(block.id, block.name);
820
915
  if (approvalRequestedToolUseIds.has(block.id)) {
821
916
  continue;
822
917
  }
918
+ pendingStepToolUseIds.add(block.id);
919
+ opensStep = true;
823
920
  emit({
824
921
  type: "tool-call",
825
922
  toolCallId: block.id,
@@ -830,6 +927,10 @@ async function runTurn(start, turn) {
830
927
  });
831
928
  }
832
929
  }
930
+ if (opensStep || toolUseIds.length === 0) {
931
+ stepOpen = true;
932
+ if (usage) pendingStepUsage = usage;
933
+ }
833
934
  continue;
834
935
  }
835
936
  if (type === "user" && msg.message?.content) {
@@ -837,6 +938,7 @@ async function runTurn(start, turn) {
837
938
  if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
838
939
  if (mcpToolUseIds.has(block.tool_use_id)) {
839
940
  mcpToolUseIds.delete(block.tool_use_id);
941
+ pendingStepToolUseIds.delete(block.tool_use_id);
840
942
  continue;
841
943
  }
842
944
  approvalRequestedToolUseIds.delete(block.tool_use_id);
@@ -853,8 +955,10 @@ async function runTurn(start, turn) {
853
955
  result,
854
956
  isError
855
957
  });
958
+ pendingStepToolUseIds.delete(block.tool_use_id);
856
959
  }
857
960
  }
961
+ closeStepIfReady();
858
962
  continue;
859
963
  }
860
964
  if (type === "result") {
@@ -866,17 +970,11 @@ async function runTurn(start, turn) {
866
970
  }
867
971
  const usage = msg.usage ?? msg.message?.usage;
868
972
  const harnessUsage = mapUsage(usage);
869
- if (harnessUsage) stepUsage = harnessUsage;
973
+ if (harnessUsage) turnUsage = harnessUsage;
870
974
  if (typeof msg.total_cost_usd === "number") {
871
975
  totalCostUsd = (totalCostUsd ?? 0) + msg.total_cost_usd;
872
976
  }
873
- const metadata = typeof msg.total_cost_usd === "number" ? { "claude-code": { costUsd: msg.total_cost_usd } } : void 0;
874
- emit({
875
- type: "finish-step",
876
- finishReason: { unified: "stop", raw: "stop" },
877
- usage: harnessUsage ?? defaultUsage(),
878
- ...metadata ? { harnessMetadata: metadata } : {}
879
- });
977
+ if (stepOpen) emitFinishStep(harnessUsage ?? pendingStepUsage);
880
978
  queryInput.close();
881
979
  break;
882
980
  } else {
@@ -889,7 +987,7 @@ async function runTurn(start, turn) {
889
987
  }
890
988
  } catch (err) {
891
989
  if (!(abortCtl.signal.aborted && emittedTerminalError)) {
892
- emit({ type: "error", error: serialiseError2(err) });
990
+ turn.emitError({ error: err, message: "claude-code turn failed" });
893
991
  }
894
992
  return;
895
993
  } finally {
@@ -901,10 +999,25 @@ async function runTurn(start, turn) {
901
999
  emit({
902
1000
  type: "finish",
903
1001
  finishReason: { unified: "stop", raw: "stop" },
904
- totalUsage: stepUsage ?? defaultUsage(),
1002
+ totalUsage: turnUsage ?? stepUsage ?? defaultUsage(),
905
1003
  ...totalCostUsd !== void 0 ? { harnessMetadata: { "claude-code": { costUsd: totalCostUsd } } } : {}
906
1004
  });
907
1005
  }
1006
+ function formatApiRetryWarning(msg) {
1007
+ const details = [];
1008
+ if (typeof msg.attempt === "number") {
1009
+ const maxRetries = typeof msg.max_retries === "number" ? `/${msg.max_retries}` : "";
1010
+ details.push(`attempt ${msg.attempt}${maxRetries}`);
1011
+ }
1012
+ if (typeof msg.error_status === "number") {
1013
+ details.push(`HTTP ${msg.error_status}`);
1014
+ }
1015
+ if (typeof msg.retry_delay_ms === "number") {
1016
+ details.push(`retrying in ${msg.retry_delay_ms}ms`);
1017
+ }
1018
+ if (msg.error) details.push(msg.error);
1019
+ return details.length > 0 ? `Claude Code API retry: ${details.join("; ")}` : "Claude Code API retry";
1020
+ }
908
1021
  function handleStreamEvent(event, partialBlocks, send) {
909
1022
  if (!event || typeof event.index !== "number") return;
910
1023
  const index = event.index;
@@ -1071,12 +1184,6 @@ function parseArgs(args2) {
1071
1184
  }
1072
1185
  return out;
1073
1186
  }
1074
- function serialiseError2(err) {
1075
- if (err instanceof Error) {
1076
- return { name: err.name, message: err.message, stack: err.stack };
1077
- }
1078
- return err;
1079
- }
1080
1187
  function emitFatal(message) {
1081
1188
  stdout2.write(JSON.stringify({ type: "bridge-fatal", message }) + "\n");
1082
1189
  process.exit(1);