@ai-sdk/harness-claude-code 1.0.80 → 1.0.81
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 +10 -0
- package/dist/bridge/index.mjs +252 -38
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +118 -33
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/index.ts +135 -29
- package/src/claude-code-harness.ts +152 -33
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @ai-sdk/harness-claude-code
|
|
2
2
|
|
|
3
|
+
## 1.0.81
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- eace6fb: feat(harness): add experimental support for steering agent conversations mid-turn
|
|
8
|
+
- c0595b4: feat(harness): support passing a filesystem and process restricted sandbox session to `HarnessAgentSession`, using fallbacks in favor of the preferred network sandbox session methods
|
|
9
|
+
- Updated dependencies [eace6fb]
|
|
10
|
+
- Updated dependencies [c0595b4]
|
|
11
|
+
- @ai-sdk/harness@1.0.78
|
|
12
|
+
|
|
3
13
|
## 1.0.80
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/dist/bridge/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// ../harness/dist/bridge/index.js
|
|
2
2
|
import { appendFile, mkdir, writeFile } from "fs/promises";
|
|
3
3
|
import { existsSync, readFileSync } from "fs";
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
4
5
|
import { env as procEnv, pid, stdout } from "process";
|
|
5
6
|
import { WebSocketServer } from "ws";
|
|
6
7
|
var DEBUG_LEVEL_WEIGHT = {
|
|
@@ -31,6 +32,106 @@ function formatBridgeError(err) {
|
|
|
31
32
|
}
|
|
32
33
|
return { message: String(err) };
|
|
33
34
|
}
|
|
35
|
+
function createBridgeUserMessageQueue(options) {
|
|
36
|
+
const messages = [];
|
|
37
|
+
const waiters = [];
|
|
38
|
+
const entries = /* @__PURE__ */ new Map();
|
|
39
|
+
let closed = false;
|
|
40
|
+
let pendingCount = 0;
|
|
41
|
+
const enqueue = (input) => {
|
|
42
|
+
const existing = entries.get(input.messageId);
|
|
43
|
+
if (existing != null) {
|
|
44
|
+
if (existing.response != null) {
|
|
45
|
+
options.respond(existing.response);
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let settled = false;
|
|
50
|
+
const settle = (response) => {
|
|
51
|
+
if (settled) return;
|
|
52
|
+
settled = true;
|
|
53
|
+
pendingCount--;
|
|
54
|
+
const entry = entries.get(input.messageId);
|
|
55
|
+
if (entry != null) entry.response = response;
|
|
56
|
+
options.respond(response);
|
|
57
|
+
};
|
|
58
|
+
const message = {
|
|
59
|
+
messageId: input.messageId,
|
|
60
|
+
text: input.text,
|
|
61
|
+
accept: () => {
|
|
62
|
+
settle({
|
|
63
|
+
type: "user-message-response",
|
|
64
|
+
messageId: input.messageId,
|
|
65
|
+
accepted: true
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
reject: (error) => {
|
|
69
|
+
settle({
|
|
70
|
+
type: "user-message-response",
|
|
71
|
+
messageId: input.messageId,
|
|
72
|
+
accepted: false,
|
|
73
|
+
error: { message: formatBridgeError(error).message }
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
entries.set(input.messageId, {
|
|
78
|
+
reject: message.reject
|
|
79
|
+
});
|
|
80
|
+
pendingCount++;
|
|
81
|
+
if (closed) {
|
|
82
|
+
message.reject(
|
|
83
|
+
new Error("The bridge turn is no longer accepting user messages.")
|
|
84
|
+
);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const waiter = waiters.shift();
|
|
88
|
+
if (waiter != null) {
|
|
89
|
+
waiter({ done: false, value: message });
|
|
90
|
+
} else {
|
|
91
|
+
messages.push(message);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const close = (error) => {
|
|
95
|
+
if (closed) return;
|
|
96
|
+
closed = true;
|
|
97
|
+
const reason = error ?? new Error("The bridge turn ended before accepting the user message.");
|
|
98
|
+
for (const entry of entries.values()) {
|
|
99
|
+
if (entry.response == null) entry.reject(reason);
|
|
100
|
+
}
|
|
101
|
+
messages.length = 0;
|
|
102
|
+
while (waiters.length > 0) {
|
|
103
|
+
waiters.shift()({ done: true, value: void 0 });
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
get pendingCount() {
|
|
108
|
+
return pendingCount;
|
|
109
|
+
},
|
|
110
|
+
enqueue,
|
|
111
|
+
close,
|
|
112
|
+
[Symbol.asyncIterator]() {
|
|
113
|
+
return {
|
|
114
|
+
next: () => {
|
|
115
|
+
const message = messages.shift();
|
|
116
|
+
if (message != null) {
|
|
117
|
+
return Promise.resolve({ done: false, value: message });
|
|
118
|
+
}
|
|
119
|
+
if (closed) {
|
|
120
|
+
return Promise.resolve({
|
|
121
|
+
done: true,
|
|
122
|
+
value: void 0
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return new Promise(
|
|
126
|
+
(resolve) => {
|
|
127
|
+
waiters.push(resolve);
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
34
135
|
function parseEnvList(value) {
|
|
35
136
|
if (!value) return void 0;
|
|
36
137
|
const items = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
@@ -247,6 +348,9 @@ async function runBridge(options) {
|
|
|
247
348
|
switch (msg.type) {
|
|
248
349
|
case "start": {
|
|
249
350
|
activeSocket = ws;
|
|
351
|
+
currentUserMessages?.close(
|
|
352
|
+
new Error("A new bridge turn replaced the active turn.")
|
|
353
|
+
);
|
|
250
354
|
const firstTurn = isFirstTurn;
|
|
251
355
|
isFirstTurn = false;
|
|
252
356
|
eventLog = [];
|
|
@@ -266,6 +370,7 @@ async function runBridge(options) {
|
|
|
266
370
|
if (debugConfig.enabled) {
|
|
267
371
|
installConsoleCapture();
|
|
268
372
|
}
|
|
373
|
+
const userMessages = createBridgeUserMessageQueue({ respond: emit });
|
|
269
374
|
const turn = {
|
|
270
375
|
emit,
|
|
271
376
|
requestToolResult: (toolCallId) => new Promise((resolve) => {
|
|
@@ -274,7 +379,7 @@ async function runBridge(options) {
|
|
|
274
379
|
requestToolApproval: (approvalId) => new Promise((resolve) => {
|
|
275
380
|
pendingToolApprovals.set(approvalId, resolve);
|
|
276
381
|
}),
|
|
277
|
-
|
|
382
|
+
experimental_userMessages: userMessages,
|
|
278
383
|
abortSignal: turnAbort.signal,
|
|
279
384
|
firstTurn,
|
|
280
385
|
bridgeLog: (input) => {
|
|
@@ -292,12 +397,16 @@ async function runBridge(options) {
|
|
|
292
397
|
emitWarning,
|
|
293
398
|
emitError
|
|
294
399
|
};
|
|
295
|
-
currentUserMessages =
|
|
400
|
+
currentUserMessages = userMessages;
|
|
296
401
|
try {
|
|
297
402
|
await onStart(msg, turn);
|
|
298
403
|
} catch (err) {
|
|
299
404
|
emitError({ error: err, message: "bridge turn failed" });
|
|
300
405
|
} finally {
|
|
406
|
+
userMessages.close();
|
|
407
|
+
if (currentUserMessages === userMessages) {
|
|
408
|
+
currentUserMessages = void 0;
|
|
409
|
+
}
|
|
301
410
|
currentTurnState = "waiting";
|
|
302
411
|
void writeBridgeMeta("waiting");
|
|
303
412
|
}
|
|
@@ -319,9 +428,34 @@ async function runBridge(options) {
|
|
|
319
428
|
}
|
|
320
429
|
return;
|
|
321
430
|
}
|
|
322
|
-
case "user-message":
|
|
323
|
-
|
|
431
|
+
case "user-message": {
|
|
432
|
+
const messageId = msg.messageId ?? randomUUID();
|
|
433
|
+
if (currentUserMessages == null) {
|
|
434
|
+
sendControl(ws, {
|
|
435
|
+
type: "user-message-response",
|
|
436
|
+
messageId,
|
|
437
|
+
accepted: false,
|
|
438
|
+
error: { message: "The bridge has no active turn to steer." }
|
|
439
|
+
});
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (ws !== activeSocket) {
|
|
443
|
+
sendControl(ws, {
|
|
444
|
+
type: "user-message-response",
|
|
445
|
+
messageId,
|
|
446
|
+
accepted: false,
|
|
447
|
+
error: {
|
|
448
|
+
message: "The connection does not own the active bridge turn."
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
currentUserMessages.enqueue({
|
|
454
|
+
messageId,
|
|
455
|
+
text: msg.text
|
|
456
|
+
});
|
|
324
457
|
return;
|
|
458
|
+
}
|
|
325
459
|
case "abort":
|
|
326
460
|
turnAbort?.abort();
|
|
327
461
|
return;
|
|
@@ -395,7 +529,8 @@ async function runBridge(options) {
|
|
|
395
529
|
sendControl(ws, {
|
|
396
530
|
type: "bridge-hello",
|
|
397
531
|
state: currentTurnState,
|
|
398
|
-
lastSeq: seqCounter
|
|
532
|
+
lastSeq: seqCounter,
|
|
533
|
+
capabilities: { experimental_userMessageResponses: true }
|
|
399
534
|
});
|
|
400
535
|
ws.on("message", (raw) => {
|
|
401
536
|
let parsed;
|
|
@@ -484,7 +619,7 @@ function createCompactionLatch(emit) {
|
|
|
484
619
|
}
|
|
485
620
|
|
|
486
621
|
// src/bridge/index.ts
|
|
487
|
-
import { randomUUID as
|
|
622
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
488
623
|
import { argv, env as procEnv2, stdout as stdout2 } from "process";
|
|
489
624
|
import * as claudeAgentSdk from "@anthropic-ai/claude-agent-sdk";
|
|
490
625
|
import * as mcpServerModule from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -504,7 +639,7 @@ function toClaudeSkillsOption(skills) {
|
|
|
504
639
|
}
|
|
505
640
|
|
|
506
641
|
// src/bridge/create-emit-stream-event.ts
|
|
507
|
-
import { randomUUID } from "crypto";
|
|
642
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
508
643
|
function createClaudeStreamEventState() {
|
|
509
644
|
return {
|
|
510
645
|
nativeToolCallNames: /* @__PURE__ */ new Map(),
|
|
@@ -710,11 +845,11 @@ function handleStreamEvent(event, partialBlocks, send) {
|
|
|
710
845
|
if (event.type === "content_block_start") {
|
|
711
846
|
const blockType = event.content_block?.type;
|
|
712
847
|
if (blockType === "text") {
|
|
713
|
-
const id =
|
|
848
|
+
const id = randomUUID2();
|
|
714
849
|
partialBlocks.set(index, { id, kind: "text" });
|
|
715
850
|
send({ type: "text-start", id });
|
|
716
851
|
} else if (blockType === "thinking") {
|
|
717
|
-
const id =
|
|
852
|
+
const id = randomUUID2();
|
|
718
853
|
partialBlocks.set(index, { id, kind: "thinking" });
|
|
719
854
|
send({ type: "reasoning-start", id });
|
|
720
855
|
}
|
|
@@ -1071,7 +1206,7 @@ async function runTurn(start, turn) {
|
|
|
1071
1206
|
tool.description ?? "",
|
|
1072
1207
|
shape,
|
|
1073
1208
|
async (input) => {
|
|
1074
|
-
const toolCallId =
|
|
1209
|
+
const toolCallId = randomUUID3();
|
|
1075
1210
|
emit({
|
|
1076
1211
|
type: "tool-call",
|
|
1077
1212
|
toolCallId,
|
|
@@ -1103,7 +1238,7 @@ async function runTurn(start, turn) {
|
|
|
1103
1238
|
const compaction = createCompactionLatch((event) => emit(event));
|
|
1104
1239
|
const queryInput = createQueryInput({
|
|
1105
1240
|
initialUserMessage: start.prompt,
|
|
1106
|
-
|
|
1241
|
+
userMessages: turn.experimental_userMessages,
|
|
1107
1242
|
abortSignal: abortCtl.signal
|
|
1108
1243
|
});
|
|
1109
1244
|
const skillsOption = toClaudeSkillsOption(start.skills);
|
|
@@ -1197,6 +1332,9 @@ async function runTurn(start, turn) {
|
|
|
1197
1332
|
for await (const msg of q) {
|
|
1198
1333
|
if (abortCtl.signal.aborted) break;
|
|
1199
1334
|
const type = msg.type;
|
|
1335
|
+
if (type === "command_lifecycle") {
|
|
1336
|
+
queryInput.handleLifecycle(msg);
|
|
1337
|
+
}
|
|
1200
1338
|
emitStreamEvent(msg);
|
|
1201
1339
|
if (type === "result") {
|
|
1202
1340
|
if (msg.subtype === "success") {
|
|
@@ -1207,12 +1345,12 @@ async function runTurn(start, turn) {
|
|
|
1207
1345
|
}
|
|
1208
1346
|
const usage = msg.usage ?? msg.message?.usage;
|
|
1209
1347
|
const harnessUsage = mapUsage(usage);
|
|
1210
|
-
if (harnessUsage) turnUsage = harnessUsage;
|
|
1348
|
+
if (harnessUsage) turnUsage = addUsage(turnUsage, harnessUsage);
|
|
1211
1349
|
if (typeof msg.total_cost_usd === "number") {
|
|
1212
1350
|
totalCostUsd = (totalCostUsd ?? 0) + msg.total_cost_usd;
|
|
1213
1351
|
}
|
|
1214
1352
|
if (start.responseFormat?.type === "json" && msg.structured_output !== void 0) {
|
|
1215
|
-
const id =
|
|
1353
|
+
const id = randomUUID3();
|
|
1216
1354
|
emit({ type: "text-start", id });
|
|
1217
1355
|
emit({
|
|
1218
1356
|
type: "text-delta",
|
|
@@ -1229,8 +1367,11 @@ async function runTurn(start, turn) {
|
|
|
1229
1367
|
usage: harnessUsage ?? streamEventState.pendingStepUsage
|
|
1230
1368
|
});
|
|
1231
1369
|
}
|
|
1232
|
-
queryInput.
|
|
1233
|
-
|
|
1370
|
+
queryInput.observeResult();
|
|
1371
|
+
if (!queryInput.hasActiveUserMessages()) {
|
|
1372
|
+
queryInput.close();
|
|
1373
|
+
break;
|
|
1374
|
+
}
|
|
1234
1375
|
} else {
|
|
1235
1376
|
emitTerminalError(
|
|
1236
1377
|
(Array.isArray(msg.errors) ? msg.errors.join("\n") : void 0) || streamEventState.observedTerminalError || msg.result || "Unknown error"
|
|
@@ -1238,6 +1379,10 @@ async function runTurn(start, turn) {
|
|
|
1238
1379
|
}
|
|
1239
1380
|
continue;
|
|
1240
1381
|
}
|
|
1382
|
+
if (queryInput.hasObservedResult && !queryInput.hasActiveUserMessages()) {
|
|
1383
|
+
queryInput.close();
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1241
1386
|
}
|
|
1242
1387
|
} catch (err) {
|
|
1243
1388
|
if (!(abortCtl.signal.aborted && emittedTerminalError)) {
|
|
@@ -1259,55 +1404,124 @@ async function runTurn(start, turn) {
|
|
|
1259
1404
|
}
|
|
1260
1405
|
function createQueryInput({
|
|
1261
1406
|
initialUserMessage,
|
|
1262
|
-
|
|
1407
|
+
userMessages,
|
|
1263
1408
|
abortSignal
|
|
1264
1409
|
}) {
|
|
1265
1410
|
let closed = false;
|
|
1266
|
-
|
|
1411
|
+
let observedResult = false;
|
|
1412
|
+
const submittedMessages = /* @__PURE__ */ new Map();
|
|
1413
|
+
const close = (error) => {
|
|
1414
|
+
if (closed) return;
|
|
1267
1415
|
closed = true;
|
|
1416
|
+
userMessages.close(error);
|
|
1268
1417
|
};
|
|
1269
1418
|
if (abortSignal.aborted) {
|
|
1270
|
-
close();
|
|
1419
|
+
close(abortSignal.reason);
|
|
1271
1420
|
} else {
|
|
1272
|
-
abortSignal.addEventListener("abort", close, {
|
|
1421
|
+
abortSignal.addEventListener("abort", () => close(abortSignal.reason), {
|
|
1422
|
+
once: true
|
|
1423
|
+
});
|
|
1273
1424
|
}
|
|
1274
|
-
const toUserMessage = (
|
|
1425
|
+
const toUserMessage = (options) => ({
|
|
1275
1426
|
type: "user",
|
|
1276
1427
|
message: {
|
|
1277
1428
|
role: "user",
|
|
1278
|
-
content: [{ type: "text", text }]
|
|
1279
|
-
}
|
|
1429
|
+
content: [{ type: "text", text: options.text }]
|
|
1430
|
+
},
|
|
1431
|
+
parent_tool_use_id: null,
|
|
1432
|
+
uuid: options.messageId,
|
|
1433
|
+
...options.priority == null ? {} : { priority: options.priority }
|
|
1280
1434
|
});
|
|
1435
|
+
const messageIterator = userMessages[Symbol.asyncIterator]();
|
|
1281
1436
|
return {
|
|
1282
1437
|
close,
|
|
1438
|
+
handleLifecycle: (message) => {
|
|
1439
|
+
const lifecycle = message;
|
|
1440
|
+
if (lifecycle.command_uuid == null || lifecycle.state == null) return;
|
|
1441
|
+
const submitted = submittedMessages.get(lifecycle.command_uuid);
|
|
1442
|
+
if (submitted == null) return;
|
|
1443
|
+
if (lifecycle.state === "queued" || lifecycle.state === "started") {
|
|
1444
|
+
submitted.accept();
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
if (lifecycle.state === "cancelled" || lifecycle.state === "discarded") {
|
|
1448
|
+
submitted.reject(
|
|
1449
|
+
new Error(`Claude Code ${lifecycle.state} the user message.`)
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
submittedMessages.delete(lifecycle.command_uuid);
|
|
1453
|
+
},
|
|
1454
|
+
hasActiveUserMessages: () => submittedMessages.size > 0 || userMessages.pendingCount > 0,
|
|
1455
|
+
observeResult: () => {
|
|
1456
|
+
observedResult = true;
|
|
1457
|
+
},
|
|
1458
|
+
get hasObservedResult() {
|
|
1459
|
+
return observedResult;
|
|
1460
|
+
},
|
|
1283
1461
|
input: {
|
|
1284
1462
|
[Symbol.asyncIterator]() {
|
|
1285
1463
|
let sentInitial = false;
|
|
1286
1464
|
return {
|
|
1287
1465
|
async next() {
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1466
|
+
if (closed || abortSignal.aborted) {
|
|
1467
|
+
return {
|
|
1468
|
+
value: void 0,
|
|
1469
|
+
done: true
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
if (!sentInitial) {
|
|
1473
|
+
sentInitial = true;
|
|
1474
|
+
return {
|
|
1475
|
+
value: toUserMessage({
|
|
1476
|
+
text: initialUserMessage,
|
|
1477
|
+
messageId: randomUUID3()
|
|
1478
|
+
}),
|
|
1479
|
+
done: false
|
|
1480
|
+
};
|
|
1303
1481
|
}
|
|
1304
|
-
|
|
1482
|
+
const nextMessage = await messageIterator.next();
|
|
1483
|
+
if (nextMessage.done) {
|
|
1484
|
+
return {
|
|
1485
|
+
value: void 0,
|
|
1486
|
+
done: true
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
submittedMessages.set(
|
|
1490
|
+
nextMessage.value.messageId,
|
|
1491
|
+
nextMessage.value
|
|
1492
|
+
);
|
|
1493
|
+
return {
|
|
1494
|
+
value: toUserMessage({
|
|
1495
|
+
text: nextMessage.value.text,
|
|
1496
|
+
messageId: nextMessage.value.messageId,
|
|
1497
|
+
priority: "next"
|
|
1498
|
+
}),
|
|
1499
|
+
done: false
|
|
1500
|
+
};
|
|
1305
1501
|
}
|
|
1306
1502
|
};
|
|
1307
1503
|
}
|
|
1308
1504
|
}
|
|
1309
1505
|
};
|
|
1310
1506
|
}
|
|
1507
|
+
function addUsage(total, usage) {
|
|
1508
|
+
if (total == null) return usage;
|
|
1509
|
+
const result = { ...total };
|
|
1510
|
+
for (const [key, value] of Object.entries(usage)) {
|
|
1511
|
+
const previous = result[key];
|
|
1512
|
+
if (typeof value === "number" && typeof previous === "number") {
|
|
1513
|
+
result[key] = previous + value;
|
|
1514
|
+
} else if (value != null && previous != null && typeof value === "object" && typeof previous === "object" && !Array.isArray(value) && !Array.isArray(previous)) {
|
|
1515
|
+
result[key] = addUsage(
|
|
1516
|
+
previous,
|
|
1517
|
+
value
|
|
1518
|
+
);
|
|
1519
|
+
} else {
|
|
1520
|
+
result[key] = value;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
return result;
|
|
1524
|
+
}
|
|
1311
1525
|
function parseArgs(args2) {
|
|
1312
1526
|
const out = {};
|
|
1313
1527
|
for (let i = 0; i < args2.length; i++) {
|