@ai-sdk/harness-claude-code 1.0.22 → 1.0.24

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.
@@ -89,6 +89,7 @@ const PUBLIC_TO_NATIVE: Readonly<Record<string, string>> = {
89
89
  };
90
90
 
91
91
  const PUBLIC_TOOL_NAMES = Object.keys(PUBLIC_TO_NATIVE);
92
+ const UNRECOVERABLE_API_RETRY_STATUSES = new Set([401, 403, 404]);
92
93
 
93
94
  const NATIVE_TOOL_KINDS: Readonly<
94
95
  Record<string, 'readonly' | 'edit' | 'bash'>
@@ -151,40 +152,6 @@ function resolveInactiveNativeTools(start: StartMessage): string[] {
151
152
  return inactiveToolNames.map(name => toNativeName(name));
152
153
  }
153
154
 
154
- /*
155
- * The harness exposes a coarse `'off' | 'on' | 'adaptive'` thinking setting,
156
- * but the Claude Agent SDK's `thinking` option takes a structured
157
- * `ThinkingConfig` object. Passing the bare string silently disables extended
158
- * thinking (the SDK ignores the malformed value), so the model never emits
159
- * thinking blocks and no reasoning is streamed. Map to the SDK's shape:
160
- * 'adaptive' → { type: 'adaptive' } (Claude decides depth; Opus 4.6+)
161
- * 'on' → { type: 'enabled' } (extended thinking always on)
162
- * 'off' → { type: 'disabled' }
163
- *
164
- * `display: 'summarized'` is required for the model's reasoning to actually be
165
- * streamed: without it the thinking block arrives carrying only a signature
166
- * and empty `thinking_delta`s, so `reasoningText` comes back empty. We default
167
- * it on whenever thinking is enabled so reasoning is visible out of the box;
168
- * `'off'` (disabled) takes no display.
169
- */
170
- function toThinkingConfig(
171
- thinking: 'off' | 'on' | 'adaptive' | undefined,
172
- ):
173
- | { type: 'adaptive' | 'enabled'; display: 'summarized' }
174
- | { type: 'disabled' }
175
- | undefined {
176
- switch (thinking) {
177
- case 'adaptive':
178
- return { type: 'adaptive', display: 'summarized' };
179
- case 'on':
180
- return { type: 'enabled', display: 'summarized' };
181
- case 'off':
182
- return { type: 'disabled' };
183
- default:
184
- return undefined;
185
- }
186
- }
187
-
188
155
  const args = parseArgs(argv.slice(2));
189
156
  const workdir = args.workdir;
190
157
  const bridgeStateDir = args.bridgeStateDir;
@@ -216,6 +183,7 @@ function createPermissionOptions(input: {
216
183
  inactiveNativeTools: readonly string[];
217
184
  turn: BridgeTurn;
218
185
  emit: Emit;
186
+ finishApprovalStep: (approvalId: string) => void;
219
187
  nativeToolCallNames: Map<string, string>;
220
188
  approvalRequestedToolUseIds: Set<string>;
221
189
  }): Record<string, unknown> {
@@ -271,6 +239,7 @@ function createPermissionOptions(input: {
271
239
  approvalId,
272
240
  toolCallId: approvalId,
273
241
  });
242
+ input.finishApprovalStep(approvalId);
274
243
 
275
244
  const decision = await input.turn.requestToolApproval(approvalId);
276
245
  return decision.approved
@@ -342,6 +311,33 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
342
311
  */
343
312
  const nativeToolCallNames = new Map<string, string>();
344
313
  const approvalRequestedToolUseIds = new Set<string>();
314
+ const partialBlocks = new Map<
315
+ number,
316
+ { id: string; kind: 'text' | 'thinking' }
317
+ >();
318
+ let stepUsage: Record<string, unknown> | undefined;
319
+ let pendingStepToolUseIds = new Set<string>();
320
+ let pendingStepUsage: Record<string, unknown> | undefined;
321
+ let stepOpen = false;
322
+
323
+ const emitFinishStep = (usage: Record<string, unknown> | undefined): void => {
324
+ emit({
325
+ type: 'finish-step',
326
+ finishReason: { unified: 'stop', raw: 'stop' },
327
+ usage: usage ?? defaultUsage(),
328
+ });
329
+ stepUsage = usage ?? stepUsage;
330
+ pendingStepUsage = undefined;
331
+ pendingStepToolUseIds = new Set();
332
+ stepOpen = false;
333
+ };
334
+
335
+ const closeStepIfReady = (): void => {
336
+ if (!stepOpen || pendingStepToolUseIds.size > 0 || partialBlocks.size > 0) {
337
+ return;
338
+ }
339
+ emitFinishStep(pendingStepUsage);
340
+ };
345
341
 
346
342
  /*
347
343
  * Tool-use ids that originated from the MCP server hosting user-supplied
@@ -416,6 +412,11 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
416
412
  inactiveNativeTools,
417
413
  turn,
418
414
  emit,
415
+ finishApprovalStep: approvalId => {
416
+ stepOpen = true;
417
+ pendingStepToolUseIds.delete(approvalId);
418
+ closeStepIfReady();
419
+ },
419
420
  nativeToolCallNames,
420
421
  approvalRequestedToolUseIds,
421
422
  });
@@ -430,9 +431,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
430
431
  ...(inactiveNativeTools.length > 0
431
432
  ? { disallowedTools: inactiveNativeTools }
432
433
  : {}),
433
- ...(toThinkingConfig(start.thinking)
434
- ? { thinking: toThinkingConfig(start.thinking) }
435
- : {}),
434
+ thinking: start.thinking,
436
435
  includePartialMessages: true,
437
436
  // The `PostCompact` hook carries the compaction summary, which the
438
437
  // `compact_boundary` system message does not. Latch it for the unified
@@ -462,24 +461,24 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
462
461
  abortSignal: abortCtl.signal,
463
462
  },
464
463
  });
464
+ turn.onInterrupt(() => q.interrupt());
465
465
 
466
- let stepUsage: Record<string, unknown> | undefined;
466
+ let turnUsage: Record<string, unknown> | undefined;
467
467
  let totalCostUsd: number | undefined;
468
468
  let observedTerminalError: string | undefined;
469
469
  let emittedTerminalError = false;
470
470
  let emittedTerminalFinish = false;
471
471
  let streamStarted = false;
472
- const partialBlocks = new Map<
473
- number,
474
- { id: string; kind: 'text' | 'thinking' }
475
- >();
476
472
 
477
473
  const emitTerminalError = (message: string | undefined): void => {
478
474
  const normalized = message?.trim();
479
475
  if (!normalized || emittedTerminalError || emittedTerminalFinish) return;
480
476
  observedTerminalError = normalized;
481
477
  emittedTerminalError = true;
482
- emit({ type: 'error', error: normalized });
478
+ turn.emitError({
479
+ error: normalized,
480
+ message: 'claude-code terminal error',
481
+ });
483
482
  queryInput.close();
484
483
  abortCtl.abort();
485
484
  };
@@ -488,10 +487,6 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
488
487
  for await (const msg of q as AsyncIterable<ClaudeMessage>) {
489
488
  if (abortCtl.signal.aborted) break;
490
489
 
491
- if (typeof msg.error === 'string' && msg.error.trim()) {
492
- observedTerminalError = msg.error.trim();
493
- }
494
-
495
490
  const type = msg.type;
496
491
 
497
492
  // Emit `stream-start` once, on the first message, carrying the model the
@@ -511,6 +506,27 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
511
506
  streamStarted = true;
512
507
  }
513
508
 
509
+ if (type === 'system' && msg.subtype === 'api_retry') {
510
+ if (
511
+ typeof msg.error_status === 'number' &&
512
+ UNRECOVERABLE_API_RETRY_STATUSES.has(msg.error_status)
513
+ ) {
514
+ emitTerminalError(
515
+ `HTTP ${msg.error_status}: ${
516
+ msg.error ?? 'provider request failed'
517
+ }`,
518
+ );
519
+ continue;
520
+ }
521
+
522
+ turn.emitWarning({ message: formatApiRetryWarning(msg) });
523
+ continue;
524
+ }
525
+
526
+ if (typeof msg.error === 'string' && msg.error.trim()) {
527
+ observedTerminalError = msg.error.trim();
528
+ }
529
+
514
530
  if (
515
531
  type === 'auth_status' &&
516
532
  typeof msg.error === 'string' &&
@@ -520,18 +536,6 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
520
536
  continue;
521
537
  }
522
538
 
523
- if (
524
- type === 'system' &&
525
- msg.subtype === 'api_retry' &&
526
- typeof msg.error_status === 'number' &&
527
- [401, 403, 404].includes(msg.error_status)
528
- ) {
529
- emitTerminalError(
530
- `HTTP ${msg.error_status}: ${msg.error ?? 'provider request failed'}`,
531
- );
532
- continue;
533
- }
534
-
535
539
  if (
536
540
  type === 'system' &&
537
541
  msg.subtype === 'task_updated' &&
@@ -564,21 +568,29 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
564
568
  }
565
569
 
566
570
  if (type === 'assistant' && msg.message?.content) {
571
+ const usage = mapUsage(msg.message.usage);
572
+ const toolUseIds: string[] = [];
573
+ let opensStep = false;
567
574
  for (const block of msg.message.content) {
568
575
  if (
569
576
  block.type === 'tool_use' &&
570
577
  typeof block.id === 'string' &&
571
578
  typeof block.name === 'string'
572
579
  ) {
580
+ toolUseIds.push(block.id);
573
581
  const mcpPrefix = 'mcp__harness-tools__';
574
582
  if (block.name.startsWith(mcpPrefix)) {
583
+ pendingStepToolUseIds.add(block.id);
575
584
  mcpToolUseIds.add(block.id);
585
+ opensStep = true;
576
586
  continue;
577
587
  }
578
588
  nativeToolCallNames.set(block.id, block.name);
579
589
  if (approvalRequestedToolUseIds.has(block.id)) {
580
590
  continue;
581
591
  }
592
+ pendingStepToolUseIds.add(block.id);
593
+ opensStep = true;
582
594
  emit({
583
595
  type: 'tool-call',
584
596
  toolCallId: block.id,
@@ -589,6 +601,10 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
589
601
  });
590
602
  }
591
603
  }
604
+ if (opensStep || toolUseIds.length === 0) {
605
+ stepOpen = true;
606
+ if (usage) pendingStepUsage = usage;
607
+ }
592
608
  continue;
593
609
  }
594
610
 
@@ -600,6 +616,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
600
616
  ) {
601
617
  if (mcpToolUseIds.has(block.tool_use_id)) {
602
618
  mcpToolUseIds.delete(block.tool_use_id);
619
+ pendingStepToolUseIds.delete(block.tool_use_id);
603
620
  continue;
604
621
  }
605
622
  approvalRequestedToolUseIds.delete(block.tool_use_id);
@@ -630,8 +647,10 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
630
647
  result,
631
648
  isError,
632
649
  });
650
+ pendingStepToolUseIds.delete(block.tool_use_id);
633
651
  }
634
652
  }
653
+ closeStepIfReady();
635
654
  continue;
636
655
  }
637
656
 
@@ -644,20 +663,11 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
644
663
  }
645
664
  const usage = msg.usage ?? msg.message?.usage;
646
665
  const harnessUsage = mapUsage(usage);
647
- if (harnessUsage) stepUsage = harnessUsage;
666
+ if (harnessUsage) turnUsage = harnessUsage;
648
667
  if (typeof msg.total_cost_usd === 'number') {
649
668
  totalCostUsd = (totalCostUsd ?? 0) + msg.total_cost_usd;
650
669
  }
651
- const metadata =
652
- typeof msg.total_cost_usd === 'number'
653
- ? { 'claude-code': { costUsd: msg.total_cost_usd } }
654
- : undefined;
655
- emit({
656
- type: 'finish-step',
657
- finishReason: { unified: 'stop', raw: 'stop' },
658
- usage: harnessUsage ?? defaultUsage(),
659
- ...(metadata ? { harnessMetadata: metadata } : {}),
660
- });
670
+ if (stepOpen) emitFinishStep(harnessUsage ?? pendingStepUsage);
661
671
  queryInput.close();
662
672
  break;
663
673
  } else {
@@ -673,7 +683,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
673
683
  }
674
684
  } catch (err) {
675
685
  if (!(abortCtl.signal.aborted && emittedTerminalError)) {
676
- emit({ type: 'error', error: serialiseError(err) });
686
+ turn.emitError({ error: err, message: 'claude-code turn failed' });
677
687
  }
678
688
  return;
679
689
  } finally {
@@ -686,7 +696,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
686
696
  emit({
687
697
  type: 'finish',
688
698
  finishReason: { unified: 'stop', raw: 'stop' },
689
- totalUsage: stepUsage ?? defaultUsage(),
699
+ totalUsage: turnUsage ?? stepUsage ?? defaultUsage(),
690
700
  ...(totalCostUsd !== undefined
691
701
  ? { harnessMetadata: { 'claude-code': { costUsd: totalCostUsd } } }
692
702
  : {}),
@@ -697,7 +707,10 @@ type ClaudeMessage = {
697
707
  type?: string;
698
708
  subtype?: string;
699
709
  error?: string;
700
- error_status?: number;
710
+ error_status?: number | null;
711
+ attempt?: number;
712
+ max_retries?: number;
713
+ retry_delay_ms?: number;
701
714
  patch?: { status?: string; error?: string };
702
715
  compact_metadata?: {
703
716
  trigger: 'manual' | 'auto';
@@ -720,6 +733,25 @@ type ClaudeMessage = {
720
733
  total_cost_usd?: number;
721
734
  };
722
735
 
736
+ function formatApiRetryWarning(msg: ClaudeMessage): string {
737
+ const details: string[] = [];
738
+ if (typeof msg.attempt === 'number') {
739
+ const maxRetries =
740
+ typeof msg.max_retries === 'number' ? `/${msg.max_retries}` : '';
741
+ details.push(`attempt ${msg.attempt}${maxRetries}`);
742
+ }
743
+ if (typeof msg.error_status === 'number') {
744
+ details.push(`HTTP ${msg.error_status}`);
745
+ }
746
+ if (typeof msg.retry_delay_ms === 'number') {
747
+ details.push(`retrying in ${msg.retry_delay_ms}ms`);
748
+ }
749
+ if (msg.error) details.push(msg.error);
750
+ return details.length > 0
751
+ ? `Claude Code API retry: ${details.join('; ')}`
752
+ : 'Claude Code API retry';
753
+ }
754
+
723
755
  function handleStreamEvent(
724
756
  event: ClaudeMessage['event'] | undefined,
725
757
  partialBlocks: Map<number, { id: string; kind: 'text' | 'thinking' }>,
@@ -954,13 +986,6 @@ function parseArgs(args: string[]): {
954
986
  return out;
955
987
  }
956
988
 
957
- function serialiseError(err: unknown): unknown {
958
- if (err instanceof Error) {
959
- return { name: err.name, message: err.message, stack: err.stack };
960
- }
961
- return err;
962
- }
963
-
964
989
  function emitFatal(message: string): never {
965
990
  stdout.write(JSON.stringify({ type: 'bridge-fatal', message }) + '\n');
966
991
  process.exit(1);
@@ -16,8 +16,22 @@ import { z } from 'zod/v4';
16
16
  export const outboundMessageSchema = harnessV1BridgeOutboundMessageSchema;
17
17
  export type OutboundMessage = z.infer<typeof outboundMessageSchema>;
18
18
 
19
+ const thinkingDisplaySchema = z.enum(['summarized', 'omitted']).optional();
20
+
21
+ const thinkingSchema = z.discriminatedUnion('type', [
22
+ z.object({
23
+ type: z.literal('adaptive'),
24
+ display: thinkingDisplaySchema,
25
+ }),
26
+ z.object({
27
+ type: z.literal('enabled'),
28
+ display: thinkingDisplaySchema,
29
+ }),
30
+ z.object({ type: z.literal('disabled') }),
31
+ ]);
32
+
19
33
  export const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
20
- thinking: z.enum(['off', 'on', 'adaptive']).optional(),
34
+ thinking: thinkingSchema,
21
35
  maxTurns: z.number().optional(),
22
36
  skills: z.array(z.string()).optional(),
23
37
  // Resume signal. When true, the bridge passes `{ continue: true }` to the
@@ -22,6 +22,10 @@ import {
22
22
  } from '@ai-sdk/harness';
23
23
  import {
24
24
  classifyDiskLog,
25
+ createBridgeErrorHandler,
26
+ createBridgeStartupError,
27
+ drainBridgeProcessStream,
28
+ forwardBridgeProcessStream,
25
29
  markBridgeStarting,
26
30
  resolveSandboxHomeDir,
27
31
  SandboxChannel,
@@ -45,6 +49,7 @@ import {
45
49
  type InboundMessage,
46
50
  type OutboundMessage,
47
51
  } from './claude-code-bridge-protocol';
52
+ import type { ClaudeCodeThinkingConfig } from './claude-code-thinking';
48
53
  import { VERSION } from './version';
49
54
 
50
55
  type ClaudeCodeChannel = SandboxChannel<OutboundMessage, InboundMessage>;
@@ -68,10 +73,10 @@ export type ClaudeCodeHarnessSettings = {
68
73
  */
69
74
  readonly maxTurns?: number;
70
75
  /**
71
- * Controls extended-thinking behavior. `'off'` disables thinking,
72
- * `'on'` forces it on, `'adaptive'` lets the runtime decide.
76
+ * Controls extended-thinking behavior and whether reasoning is summarized or
77
+ * omitted. Defaults to `{ type: 'adaptive', display: 'summarized' }`.
73
78
  */
74
- readonly thinking?: 'off' | 'on' | 'adaptive';
79
+ readonly thinking?: ClaudeCodeThinkingConfig;
75
80
  /**
76
81
  * Override the port the bridge binds inside the sandbox. By default the
77
82
  * adapter uses the first port the sandbox declares via `sandbox.ports`.
@@ -416,6 +421,10 @@ export function createClaudeCode(
416
421
  settings: ClaudeCodeHarnessSettings = {},
417
422
  ): HarnessV1<typeof CLAUDE_CODE_BUILTIN_TOOLS> {
418
423
  let cachedBootstrap: HarnessV1Bootstrap | undefined;
424
+ const thinking = settings.thinking ?? {
425
+ type: 'adaptive',
426
+ display: 'summarized',
427
+ };
419
428
 
420
429
  return {
421
430
  specificationVersion: 'harness-v1',
@@ -480,6 +489,10 @@ export function createClaudeCode(
480
489
  }),
481
490
  )
482
491
  : undefined;
492
+ const onBridgeError = createBridgeErrorHandler({
493
+ harnessId: 'claude-code',
494
+ sessionId: startOpts.sessionId,
495
+ });
483
496
 
484
497
  // Builds the `connect` thunk a `SandboxChannel` re-invokes on every
485
498
  // (re)connect: open the socket, then wait for `bridge-hello` so the
@@ -508,6 +521,7 @@ export function createClaudeCode(
508
521
  outboundSchema: outboundMessageSchema,
509
522
  initialLastSeenEventId: coords.lastSeenEventId,
510
523
  onDiagnostic,
524
+ onBridgeError,
511
525
  });
512
526
  await attachChannel.open(isContinue ? { resume: true } : undefined);
513
527
  return createSession({
@@ -519,7 +533,7 @@ export function createClaudeCode(
519
533
  proc: undefined,
520
534
  model: settings.model,
521
535
  maxTurns: settings.maxTurns,
522
- thinking: settings.thinking,
536
+ thinking,
523
537
  isResume: true,
524
538
  continueOnFirstPrompt: false,
525
539
  rerunContinue: false,
@@ -625,14 +639,10 @@ export function createClaudeCode(
625
639
  });
626
640
 
627
641
  const bridgeStartupStderr: string[] = [];
628
- /*
629
- * Bridge stderr is the only diagnostic channel for sandbox-side crashes
630
- * and startup failures. Start forwarding before `bridge-ready`, otherwise
631
- * module-resolution, auth, and syntax errors can be lost behind the
632
- * generic startup failure below.
633
- */
634
- const bridgeStartupStderrDone = forwardBridgeStderr({
642
+ const bridgeStartupStderrDone = forwardBridgeProcessStream({
635
643
  stream: proc.stderr,
644
+ streamName: 'stderr',
645
+ source: 'claude-code',
636
646
  collectTail: bridgeStartupStderr,
637
647
  });
638
648
  void bridgeStartupStderrDone;
@@ -660,7 +670,7 @@ export function createClaudeCode(
660
670
  stderrDone: bridgeStartupStderrDone,
661
671
  }),
662
672
  });
663
- void drainRest(proc.stdout);
673
+ void drainBridgeProcessStream(proc.stdout);
664
674
 
665
675
  const wsUrl =
666
676
  (await sandboxSession.getPortUrl({
@@ -672,6 +682,7 @@ export function createClaudeCode(
672
682
  connect: buildConnect(wsUrl),
673
683
  outboundSchema: outboundMessageSchema,
674
684
  onDiagnostic,
685
+ onBridgeError,
675
686
  // In replay mode the respawned bridge reloaded the finished turn from
676
687
  // disk; seed the cursor and resume so it streams the tail (incl.
677
688
  // `finish`) rather than starting empty.
@@ -689,7 +700,7 @@ export function createClaudeCode(
689
700
  proc,
690
701
  model: settings.model,
691
702
  maxTurns: settings.maxTurns,
692
- thinking: settings.thinking,
703
+ thinking,
693
704
  isResume: respawnStrategy !== undefined,
694
705
  continueOnFirstPrompt: respawnStrategy !== undefined,
695
706
  rerunContinue: respawnStrategy === 'rerun',
@@ -768,126 +779,6 @@ async function readBridgeAsset(name: string): Promise<string> {
768
779
  throw lastErr ?? new Error(`bridge asset not found: ${name}`);
769
780
  }
770
781
 
771
- async function createBridgeStartupError({
772
- message,
773
- proc,
774
- stdoutTail,
775
- stderrTail,
776
- stderrDone,
777
- }: {
778
- message: string;
779
- proc: Experimental_SandboxProcess;
780
- stdoutTail: string[];
781
- stderrTail: string[];
782
- stderrDone: Promise<void>;
783
- }): Promise<Error> {
784
- await Promise.race([
785
- stderrDone,
786
- new Promise<void>(resolve => setTimeout(resolve, 250)),
787
- ]).catch(() => {});
788
-
789
- let exitStatus = '';
790
- try {
791
- const result = (await Promise.race([
792
- proc.wait(),
793
- new Promise<undefined>(resolve => setTimeout(resolve, 250)),
794
- ])) as { exitCode?: number } | undefined;
795
- if (result?.exitCode !== undefined) {
796
- exitStatus = ` Exit code: ${result.exitCode}.`;
797
- }
798
- } catch {}
799
-
800
- const details: string[] = [];
801
- if (stdoutTail.length > 0) {
802
- details.push(`stdout:\n${stdoutTail.join('\n')}`);
803
- }
804
- if (stderrTail.length > 0) {
805
- details.push(`stderr:\n${stderrTail.join('\n')}`);
806
- }
807
-
808
- return new Error(
809
- `${message}${exitStatus}${
810
- details.length > 0 ? `\n\n${details.join('\n\n')}` : ''
811
- }`,
812
- );
813
- }
814
-
815
- function lineDecoder() {
816
- let buffer = '';
817
- return {
818
- push(chunk: string): string[] {
819
- buffer += chunk;
820
- const lines: string[] = [];
821
- let nl: number;
822
- while ((nl = buffer.indexOf('\n')) !== -1) {
823
- const raw = buffer.slice(0, nl);
824
- buffer = buffer.slice(nl + 1);
825
- const line = raw.replace(/\r$/, '').trim();
826
- if (line.length > 0) lines.push(line);
827
- }
828
- return lines;
829
- },
830
- flush(): string[] {
831
- const line = buffer.replace(/\r$/, '').trim();
832
- buffer = '';
833
- return line.length > 0 ? [line] : [];
834
- },
835
- };
836
- }
837
-
838
- async function forwardBridgeStderr({
839
- stream,
840
- collectTail,
841
- }: {
842
- stream: ReadableStream<Uint8Array>;
843
- collectTail?: string[];
844
- }): Promise<void> {
845
- try {
846
- const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
847
- const decoder = lineDecoder();
848
- while (true) {
849
- const { value, done } = await reader.read();
850
- if (done) {
851
- for (const line of decoder.flush()) {
852
- const trimmed = line.trim();
853
- if (!trimmed) continue;
854
- if (collectTail) {
855
- collectTail.push(trimmed);
856
- if (collectTail.length > 20) collectTail.shift();
857
- }
858
- // eslint-disable-next-line no-console
859
- console.log(`[bridge stderr] ${trimmed}`);
860
- }
861
- return;
862
- }
863
- if (value) {
864
- for (const line of decoder.push(value)) {
865
- const trimmed = line.trim();
866
- if (!trimmed) continue;
867
- if (collectTail) {
868
- collectTail.push(trimmed);
869
- if (collectTail.length > 20) collectTail.shift();
870
- }
871
- // eslint-disable-next-line no-console
872
- console.log(`[bridge stderr] ${trimmed}`);
873
- }
874
- }
875
- }
876
- } catch {
877
- // Reader errors are non-fatal — best-effort diagnostic only.
878
- }
879
- }
880
-
881
- async function drainRest(stream: ReadableStream<Uint8Array>): Promise<void> {
882
- try {
883
- const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
884
- while (true) {
885
- const { done } = await reader.read();
886
- if (done) return;
887
- }
888
- } catch {}
889
- }
890
-
891
782
  /**
892
783
  * Wait for the bridge's `bridge-hello` message to arrive on the freshly
893
784
  * opened WebSocket before any other host-side code touches it.
@@ -1069,7 +960,7 @@ function createSession({
1069
960
  proc: Experimental_SandboxProcess | undefined;
1070
961
  model: string | undefined;
1071
962
  maxTurns: number | undefined;
1072
- thinking: 'off' | 'on' | 'adaptive' | undefined;
963
+ thinking: ClaudeCodeThinkingConfig;
1073
964
  isResume: boolean;
1074
965
  continueOnFirstPrompt: boolean;
1075
966
  rerunContinue: boolean;
@@ -1476,14 +1367,16 @@ function createSession({
1476
1367
  }
1477
1368
  stopped = true;
1478
1369
  /*
1479
- * Gracefully freeze the active turn at a precise cursor. `channel.suspend`
1480
- * stops processing inbound frames (the cursor stops advancing exactly at
1481
- * the last delivered event), drains what was already dispatched, then
1482
- * closes the host socket with reason `'suspended'` which `wireTurn`'s
1483
- * `onClose` treats as a clean turn end. The bridge keeps the turn running
1484
- * and accumulates events past the cursor for the next slice to replay. The
1370
+ * First ask the runtime to interrupt the active model turn, then freeze
1371
+ * the host at a precise cursor. `channel.suspend` stops processing
1372
+ * inbound frames (the cursor stops advancing exactly at the last
1373
+ * delivered event), drains what was already dispatched, then closes the
1374
+ * host socket with reason `'suspended'` which `wireTurn`'s `onClose`
1375
+ * treats as a clean turn end. The bridge keeps the turn running and
1376
+ * accumulates events past the cursor for the next slice to replay. The
1485
1377
  * sandbox process is deliberately left alive (no `shutdown`/`detach`).
1486
1378
  */
1379
+ await channel.interrupt();
1487
1380
  const lastSeenEventId = await channel.suspend();
1488
1381
  const payload: HarnessV1ContinueTurnState = {
1489
1382
  type: 'continue-turn',
@@ -0,0 +1,8 @@
1
+ export type ClaudeCodeThinkingConfig =
2
+ | {
3
+ readonly type: 'adaptive' | 'enabled';
4
+ readonly display?: 'summarized' | 'omitted';
5
+ }
6
+ | {
7
+ readonly type: 'disabled';
8
+ };
package/src/index.ts CHANGED
@@ -11,3 +11,4 @@ export { createClaudeCode } from './claude-code-harness';
11
11
  export { VERSION } from './version';
12
12
  export type { ClaudeCodeHarnessSettings } from './claude-code-harness';
13
13
  export type { ClaudeCodeAuthOptions } from './claude-code-auth';
14
+ export type { ClaudeCodeThinkingConfig } from './claude-code-thinking';