@ai-sdk/harness-claude-code 1.0.96 → 1.0.98
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 +28 -0
- package/dist/bridge/index.mjs +137 -27
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/bridge/package.json +3 -3
- package/dist/bridge/pnpm-lock.yaml +128 -126
- package/dist/bridge/pnpm-workspace.yaml +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +74 -32
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/bridge/create-emit-stream-event.ts +74 -13
- package/src/bridge/index.ts +134 -20
- package/src/bridge/package.json +3 -3
- package/src/bridge/pnpm-lock.yaml +128 -126
- package/src/bridge/pnpm-workspace.yaml +1 -1
- package/src/claude-code-bridge-protocol.ts +8 -0
- package/src/claude-code-harness.ts +116 -41
|
@@ -90,6 +90,8 @@ export type ClaudeCodeHarnessSettings = {
|
|
|
90
90
|
/**
|
|
91
91
|
* Anthropic model id the underlying `claude` CLI should use. Leaving this
|
|
92
92
|
* unset defers to the CLI's default.
|
|
93
|
+
*
|
|
94
|
+
* @deprecated Use `model` on `HarnessAgent` instead.
|
|
93
95
|
*/
|
|
94
96
|
readonly model?: string;
|
|
95
97
|
/**
|
|
@@ -800,6 +802,17 @@ const claudeCodeBridgeCoordsSchema = z.object({
|
|
|
800
802
|
const claudeCodeResumeStateSchema = z.looseObject({
|
|
801
803
|
bridge: claudeCodeBridgeCoordsSchema.optional(),
|
|
802
804
|
sandboxCredentialEnvironment: z.record(z.string(), z.string()).optional(),
|
|
805
|
+
/**
|
|
806
|
+
* The exact Claude conversation to rehydrate on resume. Written by the
|
|
807
|
+
* adapter on `doStop()`/`doDetach()`/`doSuspendTurn()` from the session id
|
|
808
|
+
* the bridge observed, so a resume names the conversation instead of
|
|
809
|
+
* relying on the SDK's `continue` flag — "most recent thread in this
|
|
810
|
+
* workdir" — which silently picks the wrong one once a second thread
|
|
811
|
+
* exists there. Hosts that captured the id themselves (it is surfaced as
|
|
812
|
+
* `harnessMetadata['claude-code'].sessionId` on `finish` parts) may also
|
|
813
|
+
* set it explicitly.
|
|
814
|
+
*/
|
|
815
|
+
claudeSessionId: z.string().optional(),
|
|
803
816
|
});
|
|
804
817
|
|
|
805
818
|
type ClaudeCodeBridgeCoords = z.infer<typeof claudeCodeBridgeCoordsSchema>;
|
|
@@ -857,6 +870,7 @@ export function createClaudeCode(
|
|
|
857
870
|
? (lifecycleState?.data as {
|
|
858
871
|
bridge?: ClaudeCodeBridgeCoords;
|
|
859
872
|
sandboxCredentialEnvironment?: Record<string, string>;
|
|
873
|
+
claudeSessionId?: string;
|
|
860
874
|
})
|
|
861
875
|
: undefined;
|
|
862
876
|
const coords = resumeData?.bridge;
|
|
@@ -918,8 +932,16 @@ export function createClaudeCode(
|
|
|
918
932
|
defaultWorkingDirectory,
|
|
919
933
|
BOOTSTRAP_DIR,
|
|
920
934
|
);
|
|
935
|
+
// The conversation the host wants back, when it is known. Absent on
|
|
936
|
+
// state written before this field existed; those resumes fall back to
|
|
937
|
+
// the `continue` flag as before.
|
|
938
|
+
const resumeSessionId = resumeData?.claudeSessionId;
|
|
921
939
|
|
|
922
940
|
const workDir = startOpts.sessionWorkDir;
|
|
941
|
+
const sandboxHomeDir = await resolveSandboxHomeDir({
|
|
942
|
+
sandbox: toolSafeSandboxSession,
|
|
943
|
+
abortSignal: startOpts.abortSignal,
|
|
944
|
+
});
|
|
923
945
|
const sessionDataDir = `${defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`;
|
|
924
946
|
const bridgeStateDir = `${sessionDataDir}/bridge`;
|
|
925
947
|
const timeoutMs = settings.startupTimeoutMs ?? 120_000;
|
|
@@ -987,6 +1009,7 @@ export function createClaudeCode(
|
|
|
987
1009
|
return createSession({
|
|
988
1010
|
sessionId: startOpts.sessionId,
|
|
989
1011
|
channel: attachChannel,
|
|
1012
|
+
...(resumeSessionId ? { resumeSessionId } : {}),
|
|
990
1013
|
// The live bridge was spawned by another process; this one owns no
|
|
991
1014
|
// process handle. The session lifecycle method decides whether the
|
|
992
1015
|
// sandbox is left running, stopped, or destroyed.
|
|
@@ -1006,7 +1029,8 @@ export function createClaudeCode(
|
|
|
1006
1029
|
debug: startOpts.observability?.debug,
|
|
1007
1030
|
permissionMode: startOpts.permissionMode,
|
|
1008
1031
|
builtinToolFiltering: startOpts.builtinToolFiltering,
|
|
1009
|
-
|
|
1032
|
+
sandbox: toolSafeSandboxSession,
|
|
1033
|
+
sandboxHomeDir,
|
|
1010
1034
|
mcpServers: settings.mcpServers,
|
|
1011
1035
|
supportsUserMessageResponses: () => supportsUserMessageResponses,
|
|
1012
1036
|
});
|
|
@@ -1039,13 +1063,6 @@ export function createClaudeCode(
|
|
|
1039
1063
|
}
|
|
1040
1064
|
}
|
|
1041
1065
|
|
|
1042
|
-
const sandboxHomeDir =
|
|
1043
|
-
startOpts.skills && startOpts.skills.length > 0
|
|
1044
|
-
? await resolveSandboxHomeDir({
|
|
1045
|
-
sandbox: toolSafeSandboxSession,
|
|
1046
|
-
abortSignal: startOpts.abortSignal,
|
|
1047
|
-
})
|
|
1048
|
-
: undefined;
|
|
1049
1066
|
const port = resolveBridgePort({
|
|
1050
1067
|
sandboxSession,
|
|
1051
1068
|
override: settings.port,
|
|
@@ -1057,35 +1074,21 @@ export function createClaudeCode(
|
|
|
1057
1074
|
const env = {
|
|
1058
1075
|
BRIDGE_CHANNEL_TOKEN: token,
|
|
1059
1076
|
BRIDGE_WS_PORT: String(port),
|
|
1060
|
-
...(sandboxHomeDir ? { HOME: sandboxHomeDir } : {}),
|
|
1061
1077
|
...(respawnStrategy === 'replay'
|
|
1062
1078
|
? { BRIDGE_REPLAY_FROM_DISK: '1' }
|
|
1063
1079
|
: {}),
|
|
1064
1080
|
};
|
|
1065
1081
|
|
|
1066
1082
|
/*
|
|
1067
|
-
* On a fresh start the workdir
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
* spawn — `BRIDGE_CHANNEL_TOKEN` rotates per start.
|
|
1083
|
+
* On a fresh start the workdir and bridge-state directory must be
|
|
1084
|
+
* created. The env is sent fresh on every spawn —
|
|
1085
|
+
* `BRIDGE_CHANNEL_TOKEN` rotates per start.
|
|
1071
1086
|
*/
|
|
1072
1087
|
if (respawnStrategy === undefined) {
|
|
1073
1088
|
await toolSafeSandboxSession.run({
|
|
1074
1089
|
command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)}`,
|
|
1075
1090
|
abortSignal: startOpts.abortSignal,
|
|
1076
1091
|
});
|
|
1077
|
-
|
|
1078
|
-
if (startOpts.skills && startOpts.skills.length > 0) {
|
|
1079
|
-
if (!sandboxHomeDir) {
|
|
1080
|
-
throw new Error('Unable to resolve sandbox HOME directory.');
|
|
1081
|
-
}
|
|
1082
|
-
await writeClaudeCodeSkills({
|
|
1083
|
-
sandbox: toolSafeSandboxSession,
|
|
1084
|
-
homeDir: sandboxHomeDir,
|
|
1085
|
-
skills: startOpts.skills,
|
|
1086
|
-
abortSignal: startOpts.abortSignal,
|
|
1087
|
-
});
|
|
1088
|
-
}
|
|
1089
1092
|
}
|
|
1090
1093
|
|
|
1091
1094
|
await markBridgeStarting({
|
|
@@ -1169,6 +1172,7 @@ export function createClaudeCode(
|
|
|
1169
1172
|
effort: settings.effort,
|
|
1170
1173
|
isResume: respawnStrategy !== undefined,
|
|
1171
1174
|
continueOnFirstPrompt: respawnStrategy !== undefined,
|
|
1175
|
+
...(resumeSessionId ? { resumeSessionId } : {}),
|
|
1172
1176
|
rerunContinue: respawnStrategy === 'rerun',
|
|
1173
1177
|
bridgePort: boundPort,
|
|
1174
1178
|
bridgeToken: token,
|
|
@@ -1177,7 +1181,8 @@ export function createClaudeCode(
|
|
|
1177
1181
|
debug: startOpts.observability?.debug,
|
|
1178
1182
|
permissionMode: startOpts.permissionMode,
|
|
1179
1183
|
builtinToolFiltering: startOpts.builtinToolFiltering,
|
|
1180
|
-
|
|
1184
|
+
sandbox: toolSafeSandboxSession,
|
|
1185
|
+
sandboxHomeDir,
|
|
1181
1186
|
mcpServers: settings.mcpServers,
|
|
1182
1187
|
supportsUserMessageResponses: () => supportsUserMessageResponses,
|
|
1183
1188
|
});
|
|
@@ -1498,6 +1503,7 @@ function createSession({
|
|
|
1498
1503
|
effort,
|
|
1499
1504
|
isResume,
|
|
1500
1505
|
continueOnFirstPrompt,
|
|
1506
|
+
resumeSessionId,
|
|
1501
1507
|
rerunContinue,
|
|
1502
1508
|
bridgePort,
|
|
1503
1509
|
bridgeToken,
|
|
@@ -1506,7 +1512,8 @@ function createSession({
|
|
|
1506
1512
|
debug,
|
|
1507
1513
|
permissionMode,
|
|
1508
1514
|
builtinToolFiltering,
|
|
1509
|
-
|
|
1515
|
+
sandbox,
|
|
1516
|
+
sandboxHomeDir,
|
|
1510
1517
|
mcpServers,
|
|
1511
1518
|
supportsUserMessageResponses,
|
|
1512
1519
|
}: {
|
|
@@ -1521,6 +1528,12 @@ function createSession({
|
|
|
1521
1528
|
effort: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | undefined;
|
|
1522
1529
|
isResume: boolean;
|
|
1523
1530
|
continueOnFirstPrompt: boolean;
|
|
1531
|
+
/**
|
|
1532
|
+
* Exact conversation to rehydrate whenever a fresh SDK query starts. Takes
|
|
1533
|
+
* precedence over the `continue` flag, which can only mean "most recent in
|
|
1534
|
+
* this workdir".
|
|
1535
|
+
*/
|
|
1536
|
+
resumeSessionId?: string;
|
|
1524
1537
|
rerunContinue: boolean;
|
|
1525
1538
|
bridgePort: number;
|
|
1526
1539
|
bridgeToken: string;
|
|
@@ -1529,7 +1542,8 @@ function createSession({
|
|
|
1529
1542
|
debug: HarnessV1DebugConfig | undefined;
|
|
1530
1543
|
permissionMode: HarnessV1PermissionMode | undefined;
|
|
1531
1544
|
builtinToolFiltering: HarnessV1BuiltinToolFiltering | undefined;
|
|
1532
|
-
|
|
1545
|
+
sandbox: SandboxSession;
|
|
1546
|
+
sandboxHomeDir: string;
|
|
1533
1547
|
mcpServers: Record<string, unknown> | undefined;
|
|
1534
1548
|
supportsUserMessageResponses: () => boolean;
|
|
1535
1549
|
}): HarnessV1Session {
|
|
@@ -1537,12 +1551,21 @@ function createSession({
|
|
|
1537
1551
|
let stopPromise: Promise<void> | undefined;
|
|
1538
1552
|
/*
|
|
1539
1553
|
* Force the Claude SDK's `continue: true` on the first prompt only when the
|
|
1540
|
-
* bridge was respawned (rerun/replay): a
|
|
1541
|
-
* first turn as new, so it must be told to
|
|
1542
|
-
*
|
|
1554
|
+
* bridge was respawned without an exact conversation id (rerun/replay): a
|
|
1555
|
+
* fresh bridge process treats its first turn as new, so it must be told to
|
|
1556
|
+
* rehydrate the workdir thread. Exact ids are sent independently of this
|
|
1557
|
+
* fallback on every fresh query.
|
|
1543
1558
|
*/
|
|
1544
1559
|
let pendingResumeFlag = continueOnFirstPrompt;
|
|
1545
1560
|
|
|
1561
|
+
/*
|
|
1562
|
+
* The Claude conversation this session currently embodies. Seeded from the
|
|
1563
|
+
* resume state and updated from every `finish` part's metadata — each turn
|
|
1564
|
+
* may fork a new id, so the latest observation is the one a later
|
|
1565
|
+
* stop/detach must record for exact resume.
|
|
1566
|
+
*/
|
|
1567
|
+
let lastClaudeSessionId = resumeSessionId;
|
|
1568
|
+
|
|
1546
1569
|
/*
|
|
1547
1570
|
* Wire the channel into one turn's worth of events and return the control
|
|
1548
1571
|
* surface. Shared by `doPromptTurn` (which sends a `start` afterwards) and
|
|
@@ -1598,6 +1621,9 @@ function createSession({
|
|
|
1598
1621
|
'reasoning-start',
|
|
1599
1622
|
'reasoning-delta',
|
|
1600
1623
|
'reasoning-end',
|
|
1624
|
+
'tool-input-start',
|
|
1625
|
+
'tool-input-delta',
|
|
1626
|
+
'tool-input-end',
|
|
1601
1627
|
'tool-call',
|
|
1602
1628
|
'tool-approval-request',
|
|
1603
1629
|
'tool-result',
|
|
@@ -1613,6 +1639,14 @@ function createSession({
|
|
|
1613
1639
|
}
|
|
1614
1640
|
unsubs.push(
|
|
1615
1641
|
channel.on('finish', msg => {
|
|
1642
|
+
const metadata = (
|
|
1643
|
+
msg as {
|
|
1644
|
+
harnessMetadata?: { 'claude-code'?: { sessionId?: unknown } };
|
|
1645
|
+
}
|
|
1646
|
+
).harnessMetadata?.['claude-code']?.sessionId;
|
|
1647
|
+
if (typeof metadata === 'string' && metadata.length > 0) {
|
|
1648
|
+
lastClaudeSessionId = metadata;
|
|
1649
|
+
}
|
|
1616
1650
|
forward(msg);
|
|
1617
1651
|
settleSuccess();
|
|
1618
1652
|
}),
|
|
@@ -1693,7 +1727,6 @@ function createSession({
|
|
|
1693
1727
|
return {
|
|
1694
1728
|
sessionId,
|
|
1695
1729
|
isResume,
|
|
1696
|
-
modelId: model,
|
|
1697
1730
|
doPromptTurn: async promptOpts => {
|
|
1698
1731
|
if (
|
|
1699
1732
|
promptOpts.responseFormat?.type === 'json' &&
|
|
@@ -1705,11 +1738,27 @@ function createSession({
|
|
|
1705
1738
|
harnessId: 'claude-code',
|
|
1706
1739
|
});
|
|
1707
1740
|
}
|
|
1741
|
+
await writeClaudeCodeSkills({
|
|
1742
|
+
sandbox,
|
|
1743
|
+
homeDir: sandboxHomeDir,
|
|
1744
|
+
skills: promptOpts.skills,
|
|
1745
|
+
abortSignal: promptOpts.abortSignal,
|
|
1746
|
+
});
|
|
1708
1747
|
const control = wireTurn({
|
|
1709
1748
|
emit: promptOpts.emit,
|
|
1710
1749
|
abortSignal: promptOpts.abortSignal,
|
|
1711
1750
|
});
|
|
1712
1751
|
|
|
1752
|
+
/*
|
|
1753
|
+
* A signal that was already aborted has settled the turn inside
|
|
1754
|
+
* `wireTurn`. Sending `start` anyway would run a full unattended turn
|
|
1755
|
+
* in the bridge — consuming tokens and possibly acting on the
|
|
1756
|
+
* workspace — after the caller has already observed the abort.
|
|
1757
|
+
*/
|
|
1758
|
+
if (promptOpts.abortSignal?.aborted) {
|
|
1759
|
+
return control;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1713
1762
|
const startMessage = {
|
|
1714
1763
|
type: 'start' as const,
|
|
1715
1764
|
prompt: extractUserText(promptOpts.prompt),
|
|
@@ -1724,19 +1773,23 @@ function createSession({
|
|
|
1724
1773
|
...(promptOpts.instructions
|
|
1725
1774
|
? { instructions: promptOpts.instructions }
|
|
1726
1775
|
: {}),
|
|
1727
|
-
model,
|
|
1776
|
+
model: promptOpts.model ?? model,
|
|
1728
1777
|
maxTurns,
|
|
1729
1778
|
...(env !== undefined ? { env } : {}),
|
|
1730
1779
|
thinking,
|
|
1731
1780
|
...(effort !== undefined ? { effort } : {}),
|
|
1732
|
-
...(skills.length > 0
|
|
1733
|
-
? { skills: skills.map(skill => skill.name) }
|
|
1781
|
+
...(promptOpts.skills.length > 0
|
|
1782
|
+
? { skills: promptOpts.skills.map(skill => skill.name) }
|
|
1734
1783
|
: {}),
|
|
1735
1784
|
...(mcpServers == null ? {} : { mcpServers }),
|
|
1736
1785
|
...(permissionMode ? { permissionMode } : {}),
|
|
1737
1786
|
...(builtinToolFiltering ? { builtinToolFiltering } : {}),
|
|
1738
1787
|
...(debug ? { debug } : {}),
|
|
1739
|
-
...(
|
|
1788
|
+
...(lastClaudeSessionId
|
|
1789
|
+
? { resumeSessionId: lastClaudeSessionId }
|
|
1790
|
+
: pendingResumeFlag
|
|
1791
|
+
? { continue: true }
|
|
1792
|
+
: {}),
|
|
1740
1793
|
};
|
|
1741
1794
|
pendingResumeFlag = false;
|
|
1742
1795
|
channel.send(startMessage);
|
|
@@ -1754,6 +1807,12 @@ function createSession({
|
|
|
1754
1807
|
harnessId: 'claude-code',
|
|
1755
1808
|
});
|
|
1756
1809
|
}
|
|
1810
|
+
await writeClaudeCodeSkills({
|
|
1811
|
+
sandbox,
|
|
1812
|
+
homeDir: sandboxHomeDir,
|
|
1813
|
+
skills: continueOpts.skills,
|
|
1814
|
+
abortSignal: continueOpts.abortSignal,
|
|
1815
|
+
});
|
|
1757
1816
|
const control = wireTurn({
|
|
1758
1817
|
emit: continueOpts.emit,
|
|
1759
1818
|
abortSignal: continueOpts.abortSignal,
|
|
@@ -1772,7 +1831,9 @@ function createSession({
|
|
|
1772
1831
|
* recomputed. This is the rare bridge-died fallback; the common slice path
|
|
1773
1832
|
* is `attach`.
|
|
1774
1833
|
*/
|
|
1775
|
-
|
|
1834
|
+
// Same as `doPromptTurn`: an already-aborted signal settled the turn in
|
|
1835
|
+
// `wireTurn`, so don't re-drive the thread nobody is waiting on.
|
|
1836
|
+
if (rerunContinue && !continueOpts.abortSignal?.aborted) {
|
|
1776
1837
|
pendingResumeFlag = false;
|
|
1777
1838
|
channel.send({
|
|
1778
1839
|
type: 'start' as const,
|
|
@@ -1795,19 +1856,21 @@ function createSession({
|
|
|
1795
1856
|
...(continueOpts.instructions
|
|
1796
1857
|
? { instructions: continueOpts.instructions }
|
|
1797
1858
|
: {}),
|
|
1798
|
-
model,
|
|
1859
|
+
model: continueOpts.model ?? model,
|
|
1799
1860
|
maxTurns,
|
|
1800
1861
|
...(env !== undefined ? { env } : {}),
|
|
1801
1862
|
thinking,
|
|
1802
1863
|
...(effort !== undefined ? { effort } : {}),
|
|
1803
|
-
...(skills.length > 0
|
|
1804
|
-
? { skills: skills.map(skill => skill.name) }
|
|
1864
|
+
...(continueOpts.skills.length > 0
|
|
1865
|
+
? { skills: continueOpts.skills.map(skill => skill.name) }
|
|
1805
1866
|
: {}),
|
|
1806
1867
|
...(mcpServers == null ? {} : { mcpServers }),
|
|
1807
1868
|
...(permissionMode ? { permissionMode } : {}),
|
|
1808
1869
|
...(builtinToolFiltering ? { builtinToolFiltering } : {}),
|
|
1809
1870
|
...(debug ? { debug } : {}),
|
|
1810
|
-
|
|
1871
|
+
...(lastClaudeSessionId
|
|
1872
|
+
? { resumeSessionId: lastClaudeSessionId }
|
|
1873
|
+
: { continue: true }),
|
|
1811
1874
|
});
|
|
1812
1875
|
}
|
|
1813
1876
|
|
|
@@ -1849,6 +1912,9 @@ function createSession({
|
|
|
1849
1912
|
lastSeenEventId,
|
|
1850
1913
|
...(sandboxId == null ? {} : { sandboxId }),
|
|
1851
1914
|
},
|
|
1915
|
+
...(lastClaudeSessionId
|
|
1916
|
+
? { claudeSessionId: lastClaudeSessionId }
|
|
1917
|
+
: {}),
|
|
1852
1918
|
},
|
|
1853
1919
|
};
|
|
1854
1920
|
return payload;
|
|
@@ -1960,7 +2026,13 @@ function createSession({
|
|
|
1960
2026
|
type: 'resume-session',
|
|
1961
2027
|
harnessId: 'claude-code',
|
|
1962
2028
|
specificationVersion: 'harness-v1',
|
|
2029
|
+
// The bridge's stop reply carries the session id it observed; the
|
|
2030
|
+
// adapter's own record backfills it when the reply predates the
|
|
2031
|
+
// field or the channel was already closed.
|
|
1963
2032
|
data: {
|
|
2033
|
+
...(lastClaudeSessionId
|
|
2034
|
+
? { claudeSessionId: lastClaudeSessionId }
|
|
2035
|
+
: {}),
|
|
1964
2036
|
...lifecycleData,
|
|
1965
2037
|
...(sandboxCredentialEnvironment == null
|
|
1966
2038
|
? {}
|
|
@@ -1999,6 +2071,9 @@ function createSession({
|
|
|
1999
2071
|
lastSeenEventId,
|
|
2000
2072
|
...(sandboxId == null ? {} : { sandboxId }),
|
|
2001
2073
|
},
|
|
2074
|
+
...(lastClaudeSessionId
|
|
2075
|
+
? { claudeSessionId: lastClaudeSessionId }
|
|
2076
|
+
: {}),
|
|
2002
2077
|
},
|
|
2003
2078
|
};
|
|
2004
2079
|
return payload;
|