@ai-sdk/harness-claude-code 1.0.22 → 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 +12 -0
- package/README.md +3 -1
- package/dist/bridge/index.mjs +152 -45
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.js +36 -113
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/index.ts +105 -80
- package/src/claude-code-bridge-protocol.ts +15 -1
- package/src/claude-code-harness.ts +33 -140
- package/src/claude-code-thinking.ts +8 -0
- package/src/index.ts +1 -0
package/src/bridge/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
72
|
-
*
|
|
76
|
+
* Controls extended-thinking behavior and whether reasoning is summarized or
|
|
77
|
+
* omitted. Defaults to `{ type: 'adaptive', display: 'summarized' }`.
|
|
73
78
|
*/
|
|
74
|
-
readonly thinking?:
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
*
|
|
1480
|
-
*
|
|
1481
|
-
*
|
|
1482
|
-
*
|
|
1483
|
-
*
|
|
1484
|
-
*
|
|
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',
|
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';
|