@ai-sdk/harness-claude-code 1.0.97 → 1.0.99
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 +22 -0
- package/dist/bridge/index.mjs +77 -14
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +66 -38
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/bridge/create-emit-stream-event.ts +74 -13
- package/src/bridge/index.ts +65 -12
- package/src/claude-code-bootstrap.ts +19 -15
- package/src/claude-code-bridge-protocol.ts +8 -0
- package/src/claude-code-harness.ts +75 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness-claude-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.99",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ai-sdk/harness": "1.0.
|
|
30
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
29
|
+
"@ai-sdk/harness": "1.0.95",
|
|
30
|
+
"@ai-sdk/provider-utils": "5.0.34",
|
|
31
31
|
"ws": "^8.21.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
@@ -21,8 +21,17 @@ export type ClaudeMessage = {
|
|
|
21
21
|
event?: {
|
|
22
22
|
type?: string;
|
|
23
23
|
index?: number;
|
|
24
|
-
content_block?: {
|
|
25
|
-
|
|
24
|
+
content_block?: {
|
|
25
|
+
type?: string;
|
|
26
|
+
id?: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
};
|
|
29
|
+
delta?: {
|
|
30
|
+
type?: string;
|
|
31
|
+
text?: string;
|
|
32
|
+
thinking?: string;
|
|
33
|
+
partial_json?: string;
|
|
34
|
+
};
|
|
26
35
|
};
|
|
27
36
|
message?: {
|
|
28
37
|
content?: ReadonlyArray<MessageBlock>;
|
|
@@ -56,7 +65,7 @@ export type ClaudeStreamEventState = {
|
|
|
56
65
|
*/
|
|
57
66
|
nativeToolCallNames: Map<string, string>;
|
|
58
67
|
approvalRequestedToolUseIds: Set<string>;
|
|
59
|
-
partialBlocks: Map<number,
|
|
68
|
+
partialBlocks: Map<number, PartialBlock>;
|
|
60
69
|
stepUsage: Record<string, unknown> | undefined;
|
|
61
70
|
pendingStepToolUseIds: Set<string>;
|
|
62
71
|
pendingStepUsage: Record<string, unknown> | undefined;
|
|
@@ -64,9 +73,8 @@ export type ClaudeStreamEventState = {
|
|
|
64
73
|
/*
|
|
65
74
|
* Tool-use ids that originated from the MCP server hosting user-supplied
|
|
66
75
|
* tools. The MCP handler emits its own `tool-call`/`tool-result` pair with
|
|
67
|
-
* the user-facing tool name
|
|
68
|
-
*
|
|
69
|
-
* suppressed.
|
|
76
|
+
* the user-facing tool name, so the duplicate `tool_result` block Claude
|
|
77
|
+
* reports for the underlying native id must be suppressed.
|
|
70
78
|
*/
|
|
71
79
|
mcpToolUseIds: Set<string>;
|
|
72
80
|
externalMcpToolUseIds: Set<string>;
|
|
@@ -74,6 +82,10 @@ export type ClaudeStreamEventState = {
|
|
|
74
82
|
observedTerminalError: string | undefined;
|
|
75
83
|
};
|
|
76
84
|
|
|
85
|
+
type PartialBlock =
|
|
86
|
+
| { id: string; kind: 'text' | 'thinking' }
|
|
87
|
+
| { id: string; kind: 'tool-input' };
|
|
88
|
+
|
|
77
89
|
export function createClaudeStreamEventState(): ClaudeStreamEventState {
|
|
78
90
|
return {
|
|
79
91
|
nativeToolCallNames: new Map(),
|
|
@@ -195,7 +207,12 @@ export function createEmitStreamEvent({
|
|
|
195
207
|
}
|
|
196
208
|
|
|
197
209
|
if (type === 'stream_event') {
|
|
198
|
-
handleStreamEvent(
|
|
210
|
+
handleStreamEvent({
|
|
211
|
+
event: msg.event,
|
|
212
|
+
state,
|
|
213
|
+
send: emit,
|
|
214
|
+
toCommonName,
|
|
215
|
+
});
|
|
199
216
|
return;
|
|
200
217
|
}
|
|
201
218
|
|
|
@@ -371,13 +388,22 @@ function formatApiRetryWarning(msg: ClaudeMessage): string {
|
|
|
371
388
|
: 'Claude Code API retry';
|
|
372
389
|
}
|
|
373
390
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
391
|
+
const HOST_TOOL_PREFIX = 'mcp__harness-tools__';
|
|
392
|
+
|
|
393
|
+
function handleStreamEvent({
|
|
394
|
+
event,
|
|
395
|
+
state,
|
|
396
|
+
send,
|
|
397
|
+
toCommonName,
|
|
398
|
+
}: {
|
|
399
|
+
event: ClaudeMessage['event'] | undefined;
|
|
400
|
+
state: ClaudeStreamEventState;
|
|
401
|
+
send: Emit;
|
|
402
|
+
toCommonName: (nativeName: string) => string;
|
|
403
|
+
}): void {
|
|
379
404
|
if (!event || typeof event.index !== 'number') return;
|
|
380
405
|
const index = event.index;
|
|
406
|
+
const partialBlocks = state.partialBlocks;
|
|
381
407
|
|
|
382
408
|
if (event.type === 'content_block_start') {
|
|
383
409
|
const blockType = event.content_block?.type;
|
|
@@ -389,6 +415,29 @@ function handleStreamEvent(
|
|
|
389
415
|
const id = randomUUID();
|
|
390
416
|
partialBlocks.set(index, { id, kind: 'thinking' });
|
|
391
417
|
send({ type: 'reasoning-start', id });
|
|
418
|
+
} else if (
|
|
419
|
+
blockType === 'tool_use' &&
|
|
420
|
+
typeof event.content_block?.id === 'string' &&
|
|
421
|
+
typeof event.content_block.name === 'string'
|
|
422
|
+
) {
|
|
423
|
+
const id = event.content_block.id;
|
|
424
|
+
const nativeName = event.content_block.name;
|
|
425
|
+
if (nativeName === 'StructuredOutput') {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const hostToolName = nativeName.startsWith(HOST_TOOL_PREFIX)
|
|
429
|
+
? nativeName.slice(HOST_TOOL_PREFIX.length)
|
|
430
|
+
: undefined;
|
|
431
|
+
const dynamic =
|
|
432
|
+
hostToolName === undefined && nativeName.startsWith('mcp__');
|
|
433
|
+
partialBlocks.set(index, { id, kind: 'tool-input' });
|
|
434
|
+
send({
|
|
435
|
+
type: 'tool-input-start',
|
|
436
|
+
id,
|
|
437
|
+
toolName: hostToolName ?? toCommonName(nativeName),
|
|
438
|
+
providerExecuted: hostToolName === undefined,
|
|
439
|
+
...(dynamic ? { dynamic: true } : {}),
|
|
440
|
+
});
|
|
392
441
|
}
|
|
393
442
|
return;
|
|
394
443
|
}
|
|
@@ -412,6 +461,16 @@ function handleStreamEvent(
|
|
|
412
461
|
id: block.id,
|
|
413
462
|
delta: event.delta.thinking,
|
|
414
463
|
});
|
|
464
|
+
} else if (
|
|
465
|
+
block.kind === 'tool-input' &&
|
|
466
|
+
event.delta?.type === 'input_json_delta' &&
|
|
467
|
+
typeof event.delta.partial_json === 'string'
|
|
468
|
+
) {
|
|
469
|
+
send({
|
|
470
|
+
type: 'tool-input-delta',
|
|
471
|
+
id: block.id,
|
|
472
|
+
delta: event.delta.partial_json,
|
|
473
|
+
});
|
|
415
474
|
}
|
|
416
475
|
return;
|
|
417
476
|
}
|
|
@@ -422,8 +481,10 @@ function handleStreamEvent(
|
|
|
422
481
|
partialBlocks.delete(index);
|
|
423
482
|
if (block.kind === 'text') {
|
|
424
483
|
send({ type: 'text-end', id: block.id });
|
|
425
|
-
} else {
|
|
484
|
+
} else if (block.kind === 'thinking') {
|
|
426
485
|
send({ type: 'reasoning-end', id: block.id });
|
|
486
|
+
} else {
|
|
487
|
+
send({ type: 'tool-input-end', id: block.id });
|
|
427
488
|
}
|
|
428
489
|
}
|
|
429
490
|
}
|
package/src/bridge/index.ts
CHANGED
|
@@ -125,13 +125,23 @@ const claudeSdk = claudeAgentSdk as any;
|
|
|
125
125
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
126
126
|
const mcpModule = mcpServerModule as any;
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* The Claude session id most recently reported by the SDK, captured from the
|
|
130
|
+
* message stream. Every turn in this bridge process may fork a new id (the
|
|
131
|
+
* SDK's `continue`/`resume` create a new session linked to the previous one),
|
|
132
|
+
* so the latest observation is the one a later resume must name.
|
|
133
|
+
*/
|
|
134
|
+
let lastClaudeSessionId: string | undefined;
|
|
135
|
+
|
|
128
136
|
await runBridge<StartMessage>({
|
|
129
137
|
bridgeType: 'claude-code',
|
|
130
138
|
bridgeStateDir,
|
|
131
139
|
onStart: runTurn,
|
|
132
|
-
// Claude Code's
|
|
133
|
-
//
|
|
134
|
-
|
|
140
|
+
// Claude Code's conversation state lives in the runtime's own store, keyed
|
|
141
|
+
// by working directory. The resume payload names the exact conversation so a
|
|
142
|
+
// later resume does not have to fall back to "most recent in this workdir".
|
|
143
|
+
onStop: () =>
|
|
144
|
+
lastClaudeSessionId == null ? {} : { claudeSessionId: lastClaudeSessionId },
|
|
135
145
|
});
|
|
136
146
|
|
|
137
147
|
type Emit = (msg: Record<string, unknown>) => void;
|
|
@@ -290,8 +300,18 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
290
300
|
tool.name,
|
|
291
301
|
tool.description ?? '',
|
|
292
302
|
shape,
|
|
293
|
-
async (
|
|
294
|
-
|
|
303
|
+
async (
|
|
304
|
+
...handlerArgs: [
|
|
305
|
+
Record<string, unknown>,
|
|
306
|
+
{ requestId: string | number; _meta?: Record<string, unknown> },
|
|
307
|
+
]
|
|
308
|
+
) => {
|
|
309
|
+
const [input, extra] = handlerArgs;
|
|
310
|
+
const metadataToolCallId = extra._meta?.['claudecode/toolUseId'];
|
|
311
|
+
const toolCallId =
|
|
312
|
+
typeof metadataToolCallId === 'string'
|
|
313
|
+
? metadataToolCallId
|
|
314
|
+
: randomUUID();
|
|
295
315
|
emit({
|
|
296
316
|
type: 'tool-call',
|
|
297
317
|
toolCallId,
|
|
@@ -392,11 +412,24 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
392
412
|
},
|
|
393
413
|
],
|
|
394
414
|
},
|
|
395
|
-
// Continuation rule
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
|
|
415
|
+
// Continuation rule, most specific first.
|
|
416
|
+
//
|
|
417
|
+
// `resumeSessionId` names the exact conversation and is what a
|
|
418
|
+
// cross-process resume should use: `continue` means "most recent thread
|
|
419
|
+
// in this workdir", which silently picks the wrong one once anything
|
|
420
|
+
// else has run there. The bridge also retains the id observed during its
|
|
421
|
+
// previous query, so every later query stays pinned to that conversation
|
|
422
|
+
// even when the host detached and reattached between turns. `resume` and
|
|
423
|
+
// `continue` are mutually exclusive in the SDK.
|
|
424
|
+
//
|
|
425
|
+
// Otherwise the host can force-continue by setting `start.continue`,
|
|
426
|
+
// and turns after the first fall back to the legacy cwd-based behavior
|
|
427
|
+
// when no exact id was observed.
|
|
428
|
+
...((start.resumeSessionId ?? lastClaudeSessionId)
|
|
429
|
+
? { resume: start.resumeSessionId ?? lastClaudeSessionId }
|
|
430
|
+
: start.continue === true || !turn.firstTurn
|
|
431
|
+
? { continue: true }
|
|
432
|
+
: {}),
|
|
400
433
|
...permissionOptions,
|
|
401
434
|
mcpServers,
|
|
402
435
|
cwd: workdir,
|
|
@@ -460,6 +493,14 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
460
493
|
queryInput.handleLifecycle(msg);
|
|
461
494
|
}
|
|
462
495
|
|
|
496
|
+
// Every SDK message carries the session id of the conversation it
|
|
497
|
+
// belongs to. Track the latest so the stop payload and the terminal
|
|
498
|
+
// finish metadata name the exact conversation.
|
|
499
|
+
const sessionId = (msg as { session_id?: unknown }).session_id;
|
|
500
|
+
if (typeof sessionId === 'string' && sessionId.length > 0) {
|
|
501
|
+
lastClaudeSessionId = sessionId;
|
|
502
|
+
}
|
|
503
|
+
|
|
463
504
|
emitStreamEvent(msg);
|
|
464
505
|
|
|
465
506
|
if (type === 'result') {
|
|
@@ -559,8 +600,20 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
559
600
|
type: 'finish',
|
|
560
601
|
finishReason: { unified: 'stop', raw: 'stop' },
|
|
561
602
|
totalUsage: turnUsage ?? streamEventState.stepUsage ?? defaultUsage(),
|
|
562
|
-
...(totalCostUsd !== undefined
|
|
563
|
-
? {
|
|
603
|
+
...(totalCostUsd !== undefined || lastClaudeSessionId !== undefined
|
|
604
|
+
? {
|
|
605
|
+
harnessMetadata: {
|
|
606
|
+
'claude-code': {
|
|
607
|
+
...(totalCostUsd !== undefined ? { costUsd: totalCostUsd } : {}),
|
|
608
|
+
// The conversation this turn belongs to, resumable outside the
|
|
609
|
+
// SDK with `claude --resume <sessionId>` and captured by the
|
|
610
|
+
// adapter for exact cross-process resume.
|
|
611
|
+
...(lastClaudeSessionId !== undefined
|
|
612
|
+
? { sessionId: lastClaudeSessionId }
|
|
613
|
+
: {}),
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
}
|
|
564
617
|
: {}),
|
|
565
618
|
});
|
|
566
619
|
}
|
|
@@ -51,19 +51,23 @@ export async function getClaudeCodeBootstrap(): Promise<HarnessV1Bootstrap> {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
async function readBridgeAsset(name: string): Promise<string> {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
54
|
+
return readFile(
|
|
55
|
+
fileURLToPath(
|
|
56
|
+
resolveBridgeAssetUrl({
|
|
57
|
+
name,
|
|
58
|
+
moduleUrl: import.meta.url,
|
|
59
|
+
}),
|
|
60
|
+
),
|
|
61
|
+
'utf8',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveBridgeAssetUrl({
|
|
66
|
+
name,
|
|
67
|
+
moduleUrl,
|
|
68
|
+
}: {
|
|
69
|
+
name: string;
|
|
70
|
+
moduleUrl: string | URL;
|
|
71
|
+
}): URL {
|
|
72
|
+
return new URL(`./bridge/${name}`, moduleUrl);
|
|
69
73
|
}
|
|
@@ -42,6 +42,14 @@ export const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
|
|
|
42
42
|
// Claude SDK so the in-workdir thread state is rehydrated. The host sets this
|
|
43
43
|
// on the first prompt after a cross-process resume.
|
|
44
44
|
continue: z.boolean().optional(),
|
|
45
|
+
// Exact conversation to resume, as the Claude SDK's `resume` option.
|
|
46
|
+
//
|
|
47
|
+
// Preferred over `continue` whenever the host knows which conversation it
|
|
48
|
+
// means: `continue` resolves to the most recent thread in the workdir, which
|
|
49
|
+
// is the wrong one as soon as anything else has run there since — including
|
|
50
|
+
// a resume that itself started a new thread. Mutually exclusive with
|
|
51
|
+
// `continue` in the SDK, so the bridge sends one or the other.
|
|
52
|
+
resumeSessionId: z.string().optional(),
|
|
45
53
|
});
|
|
46
54
|
|
|
47
55
|
export type StartMessage = z.infer<typeof startMessageSchema>;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
2
1
|
import { posix } from 'node:path';
|
|
3
2
|
import {
|
|
4
3
|
commonTool,
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
applyCredentialForwarding,
|
|
25
24
|
classifyDiskLog,
|
|
26
25
|
createSandboxCredentialEnvironment,
|
|
26
|
+
createBridgeToken,
|
|
27
27
|
experimental_createBridgeUserMessageSubmitter,
|
|
28
28
|
createBridgeErrorHandler,
|
|
29
29
|
createBridgeStartupError,
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
shellQuote,
|
|
38
38
|
warnCredentialBrokeringUnavailable,
|
|
39
39
|
waitForBridgeReady,
|
|
40
|
+
withBridgeToken,
|
|
40
41
|
writeSkills as writeHarnessSkills,
|
|
41
42
|
} from '@ai-sdk/harness/utils';
|
|
42
43
|
import {
|
|
@@ -802,6 +803,17 @@ const claudeCodeBridgeCoordsSchema = z.object({
|
|
|
802
803
|
const claudeCodeResumeStateSchema = z.looseObject({
|
|
803
804
|
bridge: claudeCodeBridgeCoordsSchema.optional(),
|
|
804
805
|
sandboxCredentialEnvironment: z.record(z.string(), z.string()).optional(),
|
|
806
|
+
/**
|
|
807
|
+
* The exact Claude conversation to rehydrate on resume. Written by the
|
|
808
|
+
* adapter on `doStop()`/`doDetach()`/`doSuspendTurn()` from the session id
|
|
809
|
+
* the bridge observed, so a resume names the conversation instead of
|
|
810
|
+
* relying on the SDK's `continue` flag — "most recent thread in this
|
|
811
|
+
* workdir" — which silently picks the wrong one once a second thread
|
|
812
|
+
* exists there. Hosts that captured the id themselves (it is surfaced as
|
|
813
|
+
* `harnessMetadata['claude-code'].sessionId` on `finish` parts) may also
|
|
814
|
+
* set it explicitly.
|
|
815
|
+
*/
|
|
816
|
+
claudeSessionId: z.string().optional(),
|
|
805
817
|
});
|
|
806
818
|
|
|
807
819
|
type ClaudeCodeBridgeCoords = z.infer<typeof claudeCodeBridgeCoordsSchema>;
|
|
@@ -831,7 +843,6 @@ export function createClaudeCode(
|
|
|
831
843
|
lifecycleStateSchema: claudeCodeResumeStateSchema,
|
|
832
844
|
getBootstrap: getClaudeCodeBootstrap,
|
|
833
845
|
doStart: async startOpts => {
|
|
834
|
-
const model = startOpts.model ?? settings.model;
|
|
835
846
|
const sandboxSession = startOpts.sandboxSession;
|
|
836
847
|
const toolSafeSandboxSession =
|
|
837
848
|
getRestrictedSandboxSession(sandboxSession);
|
|
@@ -860,6 +871,7 @@ export function createClaudeCode(
|
|
|
860
871
|
? (lifecycleState?.data as {
|
|
861
872
|
bridge?: ClaudeCodeBridgeCoords;
|
|
862
873
|
sandboxCredentialEnvironment?: Record<string, string>;
|
|
874
|
+
claudeSessionId?: string;
|
|
863
875
|
})
|
|
864
876
|
: undefined;
|
|
865
877
|
const coords = resumeData?.bridge;
|
|
@@ -921,6 +933,10 @@ export function createClaudeCode(
|
|
|
921
933
|
defaultWorkingDirectory,
|
|
922
934
|
BOOTSTRAP_DIR,
|
|
923
935
|
);
|
|
936
|
+
// The conversation the host wants back, when it is known. Absent on
|
|
937
|
+
// state written before this field existed; those resumes fall back to
|
|
938
|
+
// the `continue` flag as before.
|
|
939
|
+
const resumeSessionId = resumeData?.claudeSessionId;
|
|
924
940
|
|
|
925
941
|
const workDir = startOpts.sessionWorkDir;
|
|
926
942
|
const sandboxHomeDir = await resolveSandboxHomeDir({
|
|
@@ -994,11 +1010,12 @@ export function createClaudeCode(
|
|
|
994
1010
|
return createSession({
|
|
995
1011
|
sessionId: startOpts.sessionId,
|
|
996
1012
|
channel: attachChannel,
|
|
1013
|
+
...(resumeSessionId ? { resumeSessionId } : {}),
|
|
997
1014
|
// The live bridge was spawned by another process; this one owns no
|
|
998
1015
|
// process handle. The session lifecycle method decides whether the
|
|
999
1016
|
// sandbox is left running, stopped, or destroyed.
|
|
1000
1017
|
proc: undefined,
|
|
1001
|
-
model,
|
|
1018
|
+
model: settings.model,
|
|
1002
1019
|
maxTurns: settings.maxTurns,
|
|
1003
1020
|
env: sandboxClaudeEnvironment,
|
|
1004
1021
|
thinking,
|
|
@@ -1053,7 +1070,7 @@ export function createClaudeCode(
|
|
|
1053
1070
|
});
|
|
1054
1071
|
const token =
|
|
1055
1072
|
settings.mintBridgeToken == null
|
|
1056
|
-
?
|
|
1073
|
+
? createBridgeToken()
|
|
1057
1074
|
: settings.mintBridgeToken(sandboxId!);
|
|
1058
1075
|
const env = {
|
|
1059
1076
|
BRIDGE_CHANNEL_TOKEN: token,
|
|
@@ -1149,13 +1166,14 @@ export function createClaudeCode(
|
|
|
1149
1166
|
sessionId: startOpts.sessionId,
|
|
1150
1167
|
channel,
|
|
1151
1168
|
proc,
|
|
1152
|
-
model,
|
|
1169
|
+
model: settings.model,
|
|
1153
1170
|
maxTurns: settings.maxTurns,
|
|
1154
1171
|
env: sandboxClaudeEnvironment,
|
|
1155
1172
|
thinking,
|
|
1156
1173
|
effort: settings.effort,
|
|
1157
1174
|
isResume: respawnStrategy !== undefined,
|
|
1158
1175
|
continueOnFirstPrompt: respawnStrategy !== undefined,
|
|
1176
|
+
...(resumeSessionId ? { resumeSessionId } : {}),
|
|
1159
1177
|
rerunContinue: respawnStrategy === 'rerun',
|
|
1160
1178
|
bridgePort: boundPort,
|
|
1161
1179
|
bridgeToken: token,
|
|
@@ -1451,18 +1469,6 @@ function webSocketMessageToString(raw: unknown): string {
|
|
|
1451
1469
|
return String(raw);
|
|
1452
1470
|
}
|
|
1453
1471
|
|
|
1454
|
-
function withBridgeToken({
|
|
1455
|
-
endpoint,
|
|
1456
|
-
token,
|
|
1457
|
-
}: {
|
|
1458
|
-
endpoint: HarnessV1PortEndpoint;
|
|
1459
|
-
token: string;
|
|
1460
|
-
}): HarnessV1PortEndpoint {
|
|
1461
|
-
const bridgeUrl = new URL(endpoint.url);
|
|
1462
|
-
bridgeUrl.searchParams.set('agent_bridge_token', token);
|
|
1463
|
-
return { ...endpoint, url: bridgeUrl.toString() };
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
1472
|
function sleep(ms: number): Promise<void> {
|
|
1467
1473
|
return new Promise(resolve => {
|
|
1468
1474
|
const timer = setTimeout(resolve, ms);
|
|
@@ -1486,6 +1492,7 @@ function createSession({
|
|
|
1486
1492
|
effort,
|
|
1487
1493
|
isResume,
|
|
1488
1494
|
continueOnFirstPrompt,
|
|
1495
|
+
resumeSessionId,
|
|
1489
1496
|
rerunContinue,
|
|
1490
1497
|
bridgePort,
|
|
1491
1498
|
bridgeToken,
|
|
@@ -1510,6 +1517,12 @@ function createSession({
|
|
|
1510
1517
|
effort: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | undefined;
|
|
1511
1518
|
isResume: boolean;
|
|
1512
1519
|
continueOnFirstPrompt: boolean;
|
|
1520
|
+
/**
|
|
1521
|
+
* Exact conversation to rehydrate whenever a fresh SDK query starts. Takes
|
|
1522
|
+
* precedence over the `continue` flag, which can only mean "most recent in
|
|
1523
|
+
* this workdir".
|
|
1524
|
+
*/
|
|
1525
|
+
resumeSessionId?: string;
|
|
1513
1526
|
rerunContinue: boolean;
|
|
1514
1527
|
bridgePort: number;
|
|
1515
1528
|
bridgeToken: string;
|
|
@@ -1527,12 +1540,21 @@ function createSession({
|
|
|
1527
1540
|
let stopPromise: Promise<void> | undefined;
|
|
1528
1541
|
/*
|
|
1529
1542
|
* Force the Claude SDK's `continue: true` on the first prompt only when the
|
|
1530
|
-
* bridge was respawned (rerun/replay): a
|
|
1531
|
-
* first turn as new, so it must be told to
|
|
1532
|
-
*
|
|
1543
|
+
* bridge was respawned without an exact conversation id (rerun/replay): a
|
|
1544
|
+
* fresh bridge process treats its first turn as new, so it must be told to
|
|
1545
|
+
* rehydrate the workdir thread. Exact ids are sent independently of this
|
|
1546
|
+
* fallback on every fresh query.
|
|
1533
1547
|
*/
|
|
1534
1548
|
let pendingResumeFlag = continueOnFirstPrompt;
|
|
1535
1549
|
|
|
1550
|
+
/*
|
|
1551
|
+
* The Claude conversation this session currently embodies. Seeded from the
|
|
1552
|
+
* resume state and updated from every `finish` part's metadata — each turn
|
|
1553
|
+
* may fork a new id, so the latest observation is the one a later
|
|
1554
|
+
* stop/detach must record for exact resume.
|
|
1555
|
+
*/
|
|
1556
|
+
let lastClaudeSessionId = resumeSessionId;
|
|
1557
|
+
|
|
1536
1558
|
/*
|
|
1537
1559
|
* Wire the channel into one turn's worth of events and return the control
|
|
1538
1560
|
* surface. Shared by `doPromptTurn` (which sends a `start` afterwards) and
|
|
@@ -1588,6 +1610,9 @@ function createSession({
|
|
|
1588
1610
|
'reasoning-start',
|
|
1589
1611
|
'reasoning-delta',
|
|
1590
1612
|
'reasoning-end',
|
|
1613
|
+
'tool-input-start',
|
|
1614
|
+
'tool-input-delta',
|
|
1615
|
+
'tool-input-end',
|
|
1591
1616
|
'tool-call',
|
|
1592
1617
|
'tool-approval-request',
|
|
1593
1618
|
'tool-result',
|
|
@@ -1603,6 +1628,14 @@ function createSession({
|
|
|
1603
1628
|
}
|
|
1604
1629
|
unsubs.push(
|
|
1605
1630
|
channel.on('finish', msg => {
|
|
1631
|
+
const metadata = (
|
|
1632
|
+
msg as {
|
|
1633
|
+
harnessMetadata?: { 'claude-code'?: { sessionId?: unknown } };
|
|
1634
|
+
}
|
|
1635
|
+
).harnessMetadata?.['claude-code']?.sessionId;
|
|
1636
|
+
if (typeof metadata === 'string' && metadata.length > 0) {
|
|
1637
|
+
lastClaudeSessionId = metadata;
|
|
1638
|
+
}
|
|
1606
1639
|
forward(msg);
|
|
1607
1640
|
settleSuccess();
|
|
1608
1641
|
}),
|
|
@@ -1683,7 +1716,6 @@ function createSession({
|
|
|
1683
1716
|
return {
|
|
1684
1717
|
sessionId,
|
|
1685
1718
|
isResume,
|
|
1686
|
-
modelId: model,
|
|
1687
1719
|
doPromptTurn: async promptOpts => {
|
|
1688
1720
|
if (
|
|
1689
1721
|
promptOpts.responseFormat?.type === 'json' &&
|
|
@@ -1730,7 +1762,7 @@ function createSession({
|
|
|
1730
1762
|
...(promptOpts.instructions
|
|
1731
1763
|
? { instructions: promptOpts.instructions }
|
|
1732
1764
|
: {}),
|
|
1733
|
-
model,
|
|
1765
|
+
model: promptOpts.model ?? model,
|
|
1734
1766
|
maxTurns,
|
|
1735
1767
|
...(env !== undefined ? { env } : {}),
|
|
1736
1768
|
thinking,
|
|
@@ -1742,7 +1774,11 @@ function createSession({
|
|
|
1742
1774
|
...(permissionMode ? { permissionMode } : {}),
|
|
1743
1775
|
...(builtinToolFiltering ? { builtinToolFiltering } : {}),
|
|
1744
1776
|
...(debug ? { debug } : {}),
|
|
1745
|
-
...(
|
|
1777
|
+
...(lastClaudeSessionId
|
|
1778
|
+
? { resumeSessionId: lastClaudeSessionId }
|
|
1779
|
+
: pendingResumeFlag
|
|
1780
|
+
? { continue: true }
|
|
1781
|
+
: {}),
|
|
1746
1782
|
};
|
|
1747
1783
|
pendingResumeFlag = false;
|
|
1748
1784
|
channel.send(startMessage);
|
|
@@ -1809,7 +1845,7 @@ function createSession({
|
|
|
1809
1845
|
...(continueOpts.instructions
|
|
1810
1846
|
? { instructions: continueOpts.instructions }
|
|
1811
1847
|
: {}),
|
|
1812
|
-
model,
|
|
1848
|
+
model: continueOpts.model ?? model,
|
|
1813
1849
|
maxTurns,
|
|
1814
1850
|
...(env !== undefined ? { env } : {}),
|
|
1815
1851
|
thinking,
|
|
@@ -1821,7 +1857,9 @@ function createSession({
|
|
|
1821
1857
|
...(permissionMode ? { permissionMode } : {}),
|
|
1822
1858
|
...(builtinToolFiltering ? { builtinToolFiltering } : {}),
|
|
1823
1859
|
...(debug ? { debug } : {}),
|
|
1824
|
-
|
|
1860
|
+
...(lastClaudeSessionId
|
|
1861
|
+
? { resumeSessionId: lastClaudeSessionId }
|
|
1862
|
+
: { continue: true }),
|
|
1825
1863
|
});
|
|
1826
1864
|
}
|
|
1827
1865
|
|
|
@@ -1863,6 +1901,9 @@ function createSession({
|
|
|
1863
1901
|
lastSeenEventId,
|
|
1864
1902
|
...(sandboxId == null ? {} : { sandboxId }),
|
|
1865
1903
|
},
|
|
1904
|
+
...(lastClaudeSessionId
|
|
1905
|
+
? { claudeSessionId: lastClaudeSessionId }
|
|
1906
|
+
: {}),
|
|
1866
1907
|
},
|
|
1867
1908
|
};
|
|
1868
1909
|
return payload;
|
|
@@ -1974,7 +2015,13 @@ function createSession({
|
|
|
1974
2015
|
type: 'resume-session',
|
|
1975
2016
|
harnessId: 'claude-code',
|
|
1976
2017
|
specificationVersion: 'harness-v1',
|
|
2018
|
+
// The bridge's stop reply carries the session id it observed; the
|
|
2019
|
+
// adapter's own record backfills it when the reply predates the
|
|
2020
|
+
// field or the channel was already closed.
|
|
1977
2021
|
data: {
|
|
2022
|
+
...(lastClaudeSessionId
|
|
2023
|
+
? { claudeSessionId: lastClaudeSessionId }
|
|
2024
|
+
: {}),
|
|
1978
2025
|
...lifecycleData,
|
|
1979
2026
|
...(sandboxCredentialEnvironment == null
|
|
1980
2027
|
? {}
|
|
@@ -2013,6 +2060,9 @@ function createSession({
|
|
|
2013
2060
|
lastSeenEventId,
|
|
2014
2061
|
...(sandboxId == null ? {} : { sandboxId }),
|
|
2015
2062
|
},
|
|
2063
|
+
...(lastClaudeSessionId
|
|
2064
|
+
? { claudeSessionId: lastClaudeSessionId }
|
|
2065
|
+
: {}),
|
|
2016
2066
|
},
|
|
2017
2067
|
};
|
|
2018
2068
|
return payload;
|