@ai-sdk/harness-claude-code 1.0.40 → 1.0.42
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 +17 -0
- package/dist/bridge/index.mjs +292 -237
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/bridge/create-emit-stream-event.ts +448 -0
- package/src/bridge/index.ts +35 -386
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @ai-sdk/harness-claude-code
|
|
2
2
|
|
|
3
|
+
## 1.0.42
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 07c977a: chore(harness): refactor bridge code to break out stream event emission from launcher to make it testable
|
|
8
|
+
- Updated dependencies [a94425b]
|
|
9
|
+
- Updated dependencies [2de0611]
|
|
10
|
+
- @ai-sdk/harness@1.0.41
|
|
11
|
+
|
|
12
|
+
## 1.0.41
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Updated dependencies [59a2306]
|
|
17
|
+
- Updated dependencies [5f65e61]
|
|
18
|
+
- @ai-sdk/harness@1.0.40
|
|
19
|
+
|
|
3
20
|
## 1.0.40
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/dist/bridge/index.mjs
CHANGED
|
@@ -53,6 +53,7 @@ async function runBridge(options) {
|
|
|
53
53
|
let currentBoundPort = 0;
|
|
54
54
|
let currentTurnState = "init";
|
|
55
55
|
let activeSocket;
|
|
56
|
+
let activeSocketReadyForLiveEvents = false;
|
|
56
57
|
let isFirstTurn = true;
|
|
57
58
|
let turnAbort;
|
|
58
59
|
let currentUserMessages;
|
|
@@ -152,7 +153,7 @@ async function runBridge(options) {
|
|
|
152
153
|
diskBuffer += `${line}
|
|
153
154
|
`;
|
|
154
155
|
scheduleEventFlush();
|
|
155
|
-
if (activeSocket?.readyState === WS_OPEN) {
|
|
156
|
+
if (activeSocketReadyForLiveEvents && activeSocket?.readyState === WS_OPEN) {
|
|
156
157
|
try {
|
|
157
158
|
activeSocket.send(line);
|
|
158
159
|
} catch {
|
|
@@ -255,6 +256,8 @@ async function runBridge(options) {
|
|
|
255
256
|
const handleInbound = async (msg, ws) => {
|
|
256
257
|
switch (msg.type) {
|
|
257
258
|
case "start": {
|
|
259
|
+
if (activeSocket !== ws) return;
|
|
260
|
+
activeSocketReadyForLiveEvents = true;
|
|
258
261
|
const firstTurn = isFirstTurn;
|
|
259
262
|
isFirstTurn = false;
|
|
260
263
|
eventLog = [];
|
|
@@ -357,7 +360,9 @@ async function runBridge(options) {
|
|
|
357
360
|
}
|
|
358
361
|
return;
|
|
359
362
|
case "resume":
|
|
363
|
+
if (activeSocket !== ws) return;
|
|
360
364
|
replay(ws, msg.lastSeenEventId);
|
|
365
|
+
activeSocketReadyForLiveEvents = true;
|
|
361
366
|
return;
|
|
362
367
|
case "shutdown":
|
|
363
368
|
currentTurnState = "done";
|
|
@@ -422,6 +427,7 @@ async function runBridge(options) {
|
|
|
422
427
|
return;
|
|
423
428
|
}
|
|
424
429
|
activeSocket = ws;
|
|
430
|
+
activeSocketReadyForLiveEvents = false;
|
|
425
431
|
sendControl({
|
|
426
432
|
type: "bridge-hello",
|
|
427
433
|
state: currentTurnState,
|
|
@@ -444,6 +450,7 @@ async function runBridge(options) {
|
|
|
444
450
|
ws.on("close", () => {
|
|
445
451
|
if (activeSocket === ws) {
|
|
446
452
|
activeSocket = void 0;
|
|
453
|
+
activeSocketReadyForLiveEvents = false;
|
|
447
454
|
}
|
|
448
455
|
});
|
|
449
456
|
ws.on("error", () => {
|
|
@@ -506,7 +513,7 @@ function createCompactionLatch(emit) {
|
|
|
506
513
|
}
|
|
507
514
|
|
|
508
515
|
// src/bridge/index.ts
|
|
509
|
-
import { randomUUID } from "crypto";
|
|
516
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
510
517
|
import { argv, stdout as stdout2 } from "process";
|
|
511
518
|
import * as claudeAgentSdk from "@anthropic-ai/claude-agent-sdk";
|
|
512
519
|
import * as mcpServerModule from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -516,6 +523,263 @@ function toClaudeSkillsOption(skills) {
|
|
|
516
523
|
return skills && skills.length > 0 ? "all" : void 0;
|
|
517
524
|
}
|
|
518
525
|
|
|
526
|
+
// src/bridge/create-emit-stream-event.ts
|
|
527
|
+
import { randomUUID } from "crypto";
|
|
528
|
+
function createClaudeStreamEventState() {
|
|
529
|
+
return {
|
|
530
|
+
nativeToolCallNames: /* @__PURE__ */ new Map(),
|
|
531
|
+
approvalRequestedToolUseIds: /* @__PURE__ */ new Set(),
|
|
532
|
+
partialBlocks: /* @__PURE__ */ new Map(),
|
|
533
|
+
stepUsage: void 0,
|
|
534
|
+
pendingStepToolUseIds: /* @__PURE__ */ new Set(),
|
|
535
|
+
pendingStepUsage: void 0,
|
|
536
|
+
stepOpen: false,
|
|
537
|
+
mcpToolUseIds: /* @__PURE__ */ new Set(),
|
|
538
|
+
observedTerminalError: void 0
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
var UNRECOVERABLE_API_RETRY_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
|
|
542
|
+
function createEmitStreamEvent({
|
|
543
|
+
state,
|
|
544
|
+
emit,
|
|
545
|
+
emitWarning,
|
|
546
|
+
emitTerminalError,
|
|
547
|
+
onCompactionBoundary,
|
|
548
|
+
toCommonName: toCommonName2
|
|
549
|
+
}) {
|
|
550
|
+
let streamStarted = false;
|
|
551
|
+
return (msg) => {
|
|
552
|
+
const type = msg.type;
|
|
553
|
+
if (!streamStarted) {
|
|
554
|
+
const initModel = type === "system" && msg.subtype === "init" && typeof msg.model === "string" ? msg.model : void 0;
|
|
555
|
+
emit({
|
|
556
|
+
type: "stream-start",
|
|
557
|
+
...initModel ? { modelId: initModel } : {}
|
|
558
|
+
});
|
|
559
|
+
streamStarted = true;
|
|
560
|
+
}
|
|
561
|
+
if (type === "system" && msg.subtype === "api_retry") {
|
|
562
|
+
if (typeof msg.error_status === "number" && UNRECOVERABLE_API_RETRY_STATUSES.has(msg.error_status)) {
|
|
563
|
+
emitTerminalError(
|
|
564
|
+
`HTTP ${msg.error_status}: ${msg.error ?? "provider request failed"}`
|
|
565
|
+
);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
emitWarning({ message: formatApiRetryWarning(msg) });
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (typeof msg.error === "string" && msg.error.trim()) {
|
|
572
|
+
state.observedTerminalError = msg.error.trim();
|
|
573
|
+
}
|
|
574
|
+
if (type === "auth_status" && typeof msg.error === "string" && msg.error.trim()) {
|
|
575
|
+
emitTerminalError(msg.error);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (type === "system" && msg.subtype === "task_updated" && msg.patch?.status === "failed" && typeof msg.patch.error === "string") {
|
|
579
|
+
emitTerminalError(msg.patch.error);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (type === "system" && msg.subtype === "compact_boundary") {
|
|
583
|
+
const meta = msg.compact_metadata;
|
|
584
|
+
if (meta) {
|
|
585
|
+
onCompactionBoundary({
|
|
586
|
+
trigger: meta.trigger,
|
|
587
|
+
...typeof meta.pre_tokens === "number" ? { tokensBefore: meta.pre_tokens } : {},
|
|
588
|
+
...typeof meta.post_tokens === "number" ? { tokensAfter: meta.post_tokens } : {}
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (type === "stream_event") {
|
|
594
|
+
handleStreamEvent(msg.event, state.partialBlocks, emit);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (type === "assistant" && msg.message?.content) {
|
|
598
|
+
const usage = mapUsage(msg.message.usage);
|
|
599
|
+
const toolUseIds = [];
|
|
600
|
+
let opensStep = false;
|
|
601
|
+
for (const block of msg.message.content) {
|
|
602
|
+
if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
|
|
603
|
+
toolUseIds.push(block.id);
|
|
604
|
+
const mcpPrefix = "mcp__harness-tools__";
|
|
605
|
+
if (block.name.startsWith(mcpPrefix)) {
|
|
606
|
+
state.pendingStepToolUseIds.add(block.id);
|
|
607
|
+
state.mcpToolUseIds.add(block.id);
|
|
608
|
+
opensStep = true;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
state.nativeToolCallNames.set(block.id, block.name);
|
|
612
|
+
if (state.approvalRequestedToolUseIds.has(block.id)) {
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
state.pendingStepToolUseIds.add(block.id);
|
|
616
|
+
opensStep = true;
|
|
617
|
+
emit({
|
|
618
|
+
type: "tool-call",
|
|
619
|
+
toolCallId: block.id,
|
|
620
|
+
toolName: toCommonName2(block.name),
|
|
621
|
+
nativeName: block.name,
|
|
622
|
+
input: JSON.stringify(block.input ?? {}),
|
|
623
|
+
providerExecuted: true
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (opensStep || toolUseIds.length === 0) {
|
|
628
|
+
state.stepOpen = true;
|
|
629
|
+
if (usage) state.pendingStepUsage = usage;
|
|
630
|
+
}
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (type === "user" && msg.message?.content) {
|
|
634
|
+
for (const block of msg.message.content) {
|
|
635
|
+
if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
|
|
636
|
+
if (state.mcpToolUseIds.has(block.tool_use_id)) {
|
|
637
|
+
state.mcpToolUseIds.delete(block.tool_use_id);
|
|
638
|
+
state.pendingStepToolUseIds.delete(block.tool_use_id);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
state.approvalRequestedToolUseIds.delete(block.tool_use_id);
|
|
642
|
+
const nativeName = state.nativeToolCallNames.get(block.tool_use_id) ?? "unknown";
|
|
643
|
+
state.nativeToolCallNames.delete(block.tool_use_id);
|
|
644
|
+
const toolName = toCommonName2(nativeName);
|
|
645
|
+
const isError = !!block.is_error;
|
|
646
|
+
const content = stringifyContent(block.content);
|
|
647
|
+
const result = toolName === "bash" ? { exitCode: isError ? 1 : 0, stdout: content } : content;
|
|
648
|
+
emit({
|
|
649
|
+
type: "tool-result",
|
|
650
|
+
toolCallId: block.tool_use_id,
|
|
651
|
+
toolName,
|
|
652
|
+
result,
|
|
653
|
+
isError
|
|
654
|
+
});
|
|
655
|
+
state.pendingStepToolUseIds.delete(block.tool_use_id);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
closeStepIfReady({ state, emit });
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function finishApprovalStep({
|
|
663
|
+
state,
|
|
664
|
+
emit,
|
|
665
|
+
approvalId
|
|
666
|
+
}) {
|
|
667
|
+
state.stepOpen = true;
|
|
668
|
+
state.pendingStepToolUseIds.delete(approvalId);
|
|
669
|
+
closeStepIfReady({ state, emit });
|
|
670
|
+
}
|
|
671
|
+
function emitFinishStep({
|
|
672
|
+
state,
|
|
673
|
+
emit,
|
|
674
|
+
usage
|
|
675
|
+
}) {
|
|
676
|
+
emit({
|
|
677
|
+
type: "finish-step",
|
|
678
|
+
finishReason: { unified: "stop", raw: "stop" },
|
|
679
|
+
usage: usage ?? defaultUsage()
|
|
680
|
+
});
|
|
681
|
+
state.stepUsage = usage ?? state.stepUsage;
|
|
682
|
+
state.pendingStepUsage = void 0;
|
|
683
|
+
state.pendingStepToolUseIds = /* @__PURE__ */ new Set();
|
|
684
|
+
state.stepOpen = false;
|
|
685
|
+
}
|
|
686
|
+
function closeStepIfReady({
|
|
687
|
+
state,
|
|
688
|
+
emit
|
|
689
|
+
}) {
|
|
690
|
+
if (!state.stepOpen || state.pendingStepToolUseIds.size > 0 || state.partialBlocks.size > 0) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
emitFinishStep({ state, emit, usage: state.pendingStepUsage });
|
|
694
|
+
}
|
|
695
|
+
function formatApiRetryWarning(msg) {
|
|
696
|
+
const details = [];
|
|
697
|
+
if (typeof msg.attempt === "number") {
|
|
698
|
+
const maxRetries = typeof msg.max_retries === "number" ? `/${msg.max_retries}` : "";
|
|
699
|
+
details.push(`attempt ${msg.attempt}${maxRetries}`);
|
|
700
|
+
}
|
|
701
|
+
if (typeof msg.error_status === "number") {
|
|
702
|
+
details.push(`HTTP ${msg.error_status}`);
|
|
703
|
+
}
|
|
704
|
+
if (typeof msg.retry_delay_ms === "number") {
|
|
705
|
+
details.push(`retrying in ${msg.retry_delay_ms}ms`);
|
|
706
|
+
}
|
|
707
|
+
if (msg.error) details.push(msg.error);
|
|
708
|
+
return details.length > 0 ? `Claude Code API retry: ${details.join("; ")}` : "Claude Code API retry";
|
|
709
|
+
}
|
|
710
|
+
function handleStreamEvent(event, partialBlocks, send) {
|
|
711
|
+
if (!event || typeof event.index !== "number") return;
|
|
712
|
+
const index = event.index;
|
|
713
|
+
if (event.type === "content_block_start") {
|
|
714
|
+
const blockType = event.content_block?.type;
|
|
715
|
+
if (blockType === "text") {
|
|
716
|
+
const id = randomUUID();
|
|
717
|
+
partialBlocks.set(index, { id, kind: "text" });
|
|
718
|
+
send({ type: "text-start", id });
|
|
719
|
+
} else if (blockType === "thinking") {
|
|
720
|
+
const id = randomUUID();
|
|
721
|
+
partialBlocks.set(index, { id, kind: "thinking" });
|
|
722
|
+
send({ type: "reasoning-start", id });
|
|
723
|
+
}
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (event.type === "content_block_delta") {
|
|
727
|
+
const block = partialBlocks.get(index);
|
|
728
|
+
if (!block) return;
|
|
729
|
+
if (block.kind === "text" && event.delta?.type === "text_delta" && typeof event.delta.text === "string") {
|
|
730
|
+
send({ type: "text-delta", id: block.id, delta: event.delta.text });
|
|
731
|
+
} else if (block.kind === "thinking" && event.delta?.type === "thinking_delta" && typeof event.delta.thinking === "string") {
|
|
732
|
+
send({
|
|
733
|
+
type: "reasoning-delta",
|
|
734
|
+
id: block.id,
|
|
735
|
+
delta: event.delta.thinking
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (event.type === "content_block_stop") {
|
|
741
|
+
const block = partialBlocks.get(index);
|
|
742
|
+
if (!block) return;
|
|
743
|
+
partialBlocks.delete(index);
|
|
744
|
+
if (block.kind === "text") {
|
|
745
|
+
send({ type: "text-end", id: block.id });
|
|
746
|
+
} else {
|
|
747
|
+
send({ type: "reasoning-end", id: block.id });
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function stringifyContent(content) {
|
|
752
|
+
if (typeof content === "string") return content;
|
|
753
|
+
if (Array.isArray(content)) {
|
|
754
|
+
return content.map(
|
|
755
|
+
(entry) => entry && typeof entry === "object" && "text" in entry ? String(entry.text ?? "") : JSON.stringify(entry)
|
|
756
|
+
).join("");
|
|
757
|
+
}
|
|
758
|
+
return JSON.stringify(content);
|
|
759
|
+
}
|
|
760
|
+
function mapUsage(usage) {
|
|
761
|
+
if (!usage || typeof usage !== "object") return void 0;
|
|
762
|
+
const u = usage;
|
|
763
|
+
return {
|
|
764
|
+
inputTokens: {
|
|
765
|
+
total: (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0),
|
|
766
|
+
noCache: u.input_tokens ?? 0,
|
|
767
|
+
cacheRead: u.cache_read_input_tokens ?? 0,
|
|
768
|
+
cacheWrite: u.cache_creation_input_tokens ?? 0
|
|
769
|
+
},
|
|
770
|
+
outputTokens: {
|
|
771
|
+
total: u.output_tokens ?? 0,
|
|
772
|
+
text: u.output_tokens ?? 0
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function defaultUsage() {
|
|
777
|
+
return {
|
|
778
|
+
inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
|
|
779
|
+
outputTokens: { total: 0, text: 0 }
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
519
783
|
// src/bridge/json-schema-to-zod.ts
|
|
520
784
|
import { z } from "zod/v4";
|
|
521
785
|
function jsonSchemaToZodShape(input) {
|
|
@@ -646,7 +910,6 @@ var PUBLIC_TO_NATIVE = {
|
|
|
646
910
|
ToolSearch: "ToolSearch"
|
|
647
911
|
};
|
|
648
912
|
var PUBLIC_TOOL_NAMES = Object.keys(PUBLIC_TO_NATIVE);
|
|
649
|
-
var UNRECOVERABLE_API_RETRY_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
|
|
650
913
|
var NATIVE_TOOL_KINDS = {
|
|
651
914
|
Read: "readonly",
|
|
652
915
|
Glob: "readonly",
|
|
@@ -797,31 +1060,7 @@ async function runTurn(start, turn) {
|
|
|
797
1060
|
once: true
|
|
798
1061
|
});
|
|
799
1062
|
}
|
|
800
|
-
const
|
|
801
|
-
const approvalRequestedToolUseIds = /* @__PURE__ */ new Set();
|
|
802
|
-
const partialBlocks = /* @__PURE__ */ new Map();
|
|
803
|
-
let stepUsage;
|
|
804
|
-
let pendingStepToolUseIds = /* @__PURE__ */ new Set();
|
|
805
|
-
let pendingStepUsage;
|
|
806
|
-
let stepOpen = false;
|
|
807
|
-
const emitFinishStep = (usage) => {
|
|
808
|
-
emit({
|
|
809
|
-
type: "finish-step",
|
|
810
|
-
finishReason: { unified: "stop", raw: "stop" },
|
|
811
|
-
usage: usage ?? defaultUsage()
|
|
812
|
-
});
|
|
813
|
-
stepUsage = usage ?? stepUsage;
|
|
814
|
-
pendingStepUsage = void 0;
|
|
815
|
-
pendingStepToolUseIds = /* @__PURE__ */ new Set();
|
|
816
|
-
stepOpen = false;
|
|
817
|
-
};
|
|
818
|
-
const closeStepIfReady = () => {
|
|
819
|
-
if (!stepOpen || pendingStepToolUseIds.size > 0 || partialBlocks.size > 0) {
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
emitFinishStep(pendingStepUsage);
|
|
823
|
-
};
|
|
824
|
-
const mcpToolUseIds = /* @__PURE__ */ new Set();
|
|
1063
|
+
const streamEventState = createClaudeStreamEventState();
|
|
825
1064
|
const mcpServers = {};
|
|
826
1065
|
if (start.tools && start.tools.length > 0) {
|
|
827
1066
|
const server = new mcpModule.McpServer({
|
|
@@ -835,7 +1074,7 @@ async function runTurn(start, turn) {
|
|
|
835
1074
|
tool.description ?? "",
|
|
836
1075
|
shape,
|
|
837
1076
|
async (input) => {
|
|
838
|
-
const toolCallId =
|
|
1077
|
+
const toolCallId = randomUUID2();
|
|
839
1078
|
emit({
|
|
840
1079
|
type: "tool-call",
|
|
841
1080
|
toolCallId,
|
|
@@ -879,12 +1118,10 @@ async function runTurn(start, turn) {
|
|
|
879
1118
|
turn,
|
|
880
1119
|
emit,
|
|
881
1120
|
finishApprovalStep: (approvalId) => {
|
|
882
|
-
|
|
883
|
-
pendingStepToolUseIds.delete(approvalId);
|
|
884
|
-
closeStepIfReady();
|
|
1121
|
+
finishApprovalStep({ state: streamEventState, emit, approvalId });
|
|
885
1122
|
},
|
|
886
|
-
nativeToolCallNames,
|
|
887
|
-
approvalRequestedToolUseIds
|
|
1123
|
+
nativeToolCallNames: streamEventState.nativeToolCallNames,
|
|
1124
|
+
approvalRequestedToolUseIds: streamEventState.approvalRequestedToolUseIds
|
|
888
1125
|
});
|
|
889
1126
|
const q = claudeSdk.query({
|
|
890
1127
|
prompt: queryInput.input,
|
|
@@ -927,14 +1164,12 @@ async function runTurn(start, turn) {
|
|
|
927
1164
|
turn.onInterrupt(() => q.interrupt());
|
|
928
1165
|
let turnUsage;
|
|
929
1166
|
let totalCostUsd;
|
|
930
|
-
let observedTerminalError;
|
|
931
1167
|
let emittedTerminalError = false;
|
|
932
1168
|
let emittedTerminalFinish = false;
|
|
933
|
-
let streamStarted = false;
|
|
934
1169
|
const emitTerminalError = (message) => {
|
|
935
1170
|
const normalized = message?.trim();
|
|
936
1171
|
if (!normalized || emittedTerminalError || emittedTerminalFinish) return;
|
|
937
|
-
observedTerminalError = normalized;
|
|
1172
|
+
streamEventState.observedTerminalError = normalized;
|
|
938
1173
|
emittedTerminalError = true;
|
|
939
1174
|
turn.emitError({
|
|
940
1175
|
error: normalized,
|
|
@@ -943,123 +1178,24 @@ async function runTurn(start, turn) {
|
|
|
943
1178
|
queryInput.close();
|
|
944
1179
|
abortCtl.abort();
|
|
945
1180
|
};
|
|
1181
|
+
const emitStreamEvent = createEmitStreamEvent({
|
|
1182
|
+
state: streamEventState,
|
|
1183
|
+
emit,
|
|
1184
|
+
emitWarning: turn.emitWarning,
|
|
1185
|
+
emitTerminalError,
|
|
1186
|
+
onCompactionBoundary: (boundary) => compaction.onBoundary(boundary),
|
|
1187
|
+
toCommonName
|
|
1188
|
+
});
|
|
946
1189
|
try {
|
|
947
1190
|
for await (const msg of q) {
|
|
948
1191
|
if (abortCtl.signal.aborted) break;
|
|
949
1192
|
const type = msg.type;
|
|
950
|
-
|
|
951
|
-
const initModel = type === "system" && msg.subtype === "init" && typeof msg.model === "string" ? msg.model : void 0;
|
|
952
|
-
emit({
|
|
953
|
-
type: "stream-start",
|
|
954
|
-
...initModel ? { modelId: initModel } : {}
|
|
955
|
-
});
|
|
956
|
-
streamStarted = true;
|
|
957
|
-
}
|
|
958
|
-
if (type === "system" && msg.subtype === "api_retry") {
|
|
959
|
-
if (typeof msg.error_status === "number" && UNRECOVERABLE_API_RETRY_STATUSES.has(msg.error_status)) {
|
|
960
|
-
emitTerminalError(
|
|
961
|
-
`HTTP ${msg.error_status}: ${msg.error ?? "provider request failed"}`
|
|
962
|
-
);
|
|
963
|
-
continue;
|
|
964
|
-
}
|
|
965
|
-
turn.emitWarning({ message: formatApiRetryWarning(msg) });
|
|
966
|
-
continue;
|
|
967
|
-
}
|
|
968
|
-
if (typeof msg.error === "string" && msg.error.trim()) {
|
|
969
|
-
observedTerminalError = msg.error.trim();
|
|
970
|
-
}
|
|
971
|
-
if (type === "auth_status" && typeof msg.error === "string" && msg.error.trim()) {
|
|
972
|
-
emitTerminalError(msg.error);
|
|
973
|
-
continue;
|
|
974
|
-
}
|
|
975
|
-
if (type === "system" && msg.subtype === "task_updated" && msg.patch?.status === "failed" && typeof msg.patch.error === "string") {
|
|
976
|
-
emitTerminalError(msg.patch.error);
|
|
977
|
-
continue;
|
|
978
|
-
}
|
|
979
|
-
if (type === "system" && msg.subtype === "compact_boundary") {
|
|
980
|
-
const meta = msg.compact_metadata;
|
|
981
|
-
if (meta) {
|
|
982
|
-
compaction.onBoundary({
|
|
983
|
-
trigger: meta.trigger,
|
|
984
|
-
...typeof meta.pre_tokens === "number" ? { tokensBefore: meta.pre_tokens } : {},
|
|
985
|
-
...typeof meta.post_tokens === "number" ? { tokensAfter: meta.post_tokens } : {}
|
|
986
|
-
});
|
|
987
|
-
}
|
|
988
|
-
continue;
|
|
989
|
-
}
|
|
990
|
-
if (type === "stream_event") {
|
|
991
|
-
handleStreamEvent(msg.event, partialBlocks, emit);
|
|
992
|
-
continue;
|
|
993
|
-
}
|
|
994
|
-
if (type === "assistant" && msg.message?.content) {
|
|
995
|
-
const usage = mapUsage(msg.message.usage);
|
|
996
|
-
const toolUseIds = [];
|
|
997
|
-
let opensStep = false;
|
|
998
|
-
for (const block of msg.message.content) {
|
|
999
|
-
if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
|
|
1000
|
-
toolUseIds.push(block.id);
|
|
1001
|
-
const mcpPrefix = "mcp__harness-tools__";
|
|
1002
|
-
if (block.name.startsWith(mcpPrefix)) {
|
|
1003
|
-
pendingStepToolUseIds.add(block.id);
|
|
1004
|
-
mcpToolUseIds.add(block.id);
|
|
1005
|
-
opensStep = true;
|
|
1006
|
-
continue;
|
|
1007
|
-
}
|
|
1008
|
-
nativeToolCallNames.set(block.id, block.name);
|
|
1009
|
-
if (approvalRequestedToolUseIds.has(block.id)) {
|
|
1010
|
-
continue;
|
|
1011
|
-
}
|
|
1012
|
-
pendingStepToolUseIds.add(block.id);
|
|
1013
|
-
opensStep = true;
|
|
1014
|
-
emit({
|
|
1015
|
-
type: "tool-call",
|
|
1016
|
-
toolCallId: block.id,
|
|
1017
|
-
toolName: toCommonName(block.name),
|
|
1018
|
-
nativeName: block.name,
|
|
1019
|
-
input: JSON.stringify(block.input ?? {}),
|
|
1020
|
-
providerExecuted: true
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
if (opensStep || toolUseIds.length === 0) {
|
|
1025
|
-
stepOpen = true;
|
|
1026
|
-
if (usage) pendingStepUsage = usage;
|
|
1027
|
-
}
|
|
1028
|
-
continue;
|
|
1029
|
-
}
|
|
1030
|
-
if (type === "user" && msg.message?.content) {
|
|
1031
|
-
for (const block of msg.message.content) {
|
|
1032
|
-
if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
|
|
1033
|
-
if (mcpToolUseIds.has(block.tool_use_id)) {
|
|
1034
|
-
mcpToolUseIds.delete(block.tool_use_id);
|
|
1035
|
-
pendingStepToolUseIds.delete(block.tool_use_id);
|
|
1036
|
-
continue;
|
|
1037
|
-
}
|
|
1038
|
-
approvalRequestedToolUseIds.delete(block.tool_use_id);
|
|
1039
|
-
const nativeName = nativeToolCallNames.get(block.tool_use_id) ?? "unknown";
|
|
1040
|
-
nativeToolCallNames.delete(block.tool_use_id);
|
|
1041
|
-
const toolName = toCommonName(nativeName);
|
|
1042
|
-
const isError = !!block.is_error;
|
|
1043
|
-
const content = stringifyContent(block.content);
|
|
1044
|
-
const result = toolName === "bash" ? { exitCode: isError ? 1 : 0, stdout: content } : content;
|
|
1045
|
-
emit({
|
|
1046
|
-
type: "tool-result",
|
|
1047
|
-
toolCallId: block.tool_use_id,
|
|
1048
|
-
toolName,
|
|
1049
|
-
result,
|
|
1050
|
-
isError
|
|
1051
|
-
});
|
|
1052
|
-
pendingStepToolUseIds.delete(block.tool_use_id);
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
closeStepIfReady();
|
|
1056
|
-
continue;
|
|
1057
|
-
}
|
|
1193
|
+
emitStreamEvent(msg);
|
|
1058
1194
|
if (type === "result") {
|
|
1059
1195
|
if (msg.subtype === "success") {
|
|
1060
1196
|
const emptyResult = !msg.result?.trim?.();
|
|
1061
|
-
if (emptyResult && observedTerminalError) {
|
|
1062
|
-
emitTerminalError(observedTerminalError);
|
|
1197
|
+
if (emptyResult && streamEventState.observedTerminalError) {
|
|
1198
|
+
emitTerminalError(streamEventState.observedTerminalError);
|
|
1063
1199
|
continue;
|
|
1064
1200
|
}
|
|
1065
1201
|
const usage = msg.usage ?? msg.message?.usage;
|
|
@@ -1068,12 +1204,18 @@ async function runTurn(start, turn) {
|
|
|
1068
1204
|
if (typeof msg.total_cost_usd === "number") {
|
|
1069
1205
|
totalCostUsd = (totalCostUsd ?? 0) + msg.total_cost_usd;
|
|
1070
1206
|
}
|
|
1071
|
-
if (stepOpen)
|
|
1207
|
+
if (streamEventState.stepOpen) {
|
|
1208
|
+
emitFinishStep({
|
|
1209
|
+
state: streamEventState,
|
|
1210
|
+
emit,
|
|
1211
|
+
usage: harnessUsage ?? streamEventState.pendingStepUsage
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1072
1214
|
queryInput.close();
|
|
1073
1215
|
break;
|
|
1074
1216
|
} else {
|
|
1075
1217
|
emitTerminalError(
|
|
1076
|
-
(Array.isArray(msg.errors) ? msg.errors.join("\n") : void 0) || observedTerminalError || msg.result || "Unknown error"
|
|
1218
|
+
(Array.isArray(msg.errors) ? msg.errors.join("\n") : void 0) || streamEventState.observedTerminalError || msg.result || "Unknown error"
|
|
1077
1219
|
);
|
|
1078
1220
|
}
|
|
1079
1221
|
continue;
|
|
@@ -1093,66 +1235,10 @@ async function runTurn(start, turn) {
|
|
|
1093
1235
|
emit({
|
|
1094
1236
|
type: "finish",
|
|
1095
1237
|
finishReason: { unified: "stop", raw: "stop" },
|
|
1096
|
-
totalUsage: turnUsage ?? stepUsage ?? defaultUsage(),
|
|
1238
|
+
totalUsage: turnUsage ?? streamEventState.stepUsage ?? defaultUsage(),
|
|
1097
1239
|
...totalCostUsd !== void 0 ? { harnessMetadata: { "claude-code": { costUsd: totalCostUsd } } } : {}
|
|
1098
1240
|
});
|
|
1099
1241
|
}
|
|
1100
|
-
function formatApiRetryWarning(msg) {
|
|
1101
|
-
const details = [];
|
|
1102
|
-
if (typeof msg.attempt === "number") {
|
|
1103
|
-
const maxRetries = typeof msg.max_retries === "number" ? `/${msg.max_retries}` : "";
|
|
1104
|
-
details.push(`attempt ${msg.attempt}${maxRetries}`);
|
|
1105
|
-
}
|
|
1106
|
-
if (typeof msg.error_status === "number") {
|
|
1107
|
-
details.push(`HTTP ${msg.error_status}`);
|
|
1108
|
-
}
|
|
1109
|
-
if (typeof msg.retry_delay_ms === "number") {
|
|
1110
|
-
details.push(`retrying in ${msg.retry_delay_ms}ms`);
|
|
1111
|
-
}
|
|
1112
|
-
if (msg.error) details.push(msg.error);
|
|
1113
|
-
return details.length > 0 ? `Claude Code API retry: ${details.join("; ")}` : "Claude Code API retry";
|
|
1114
|
-
}
|
|
1115
|
-
function handleStreamEvent(event, partialBlocks, send) {
|
|
1116
|
-
if (!event || typeof event.index !== "number") return;
|
|
1117
|
-
const index = event.index;
|
|
1118
|
-
if (event.type === "content_block_start") {
|
|
1119
|
-
const blockType = event.content_block?.type;
|
|
1120
|
-
if (blockType === "text") {
|
|
1121
|
-
const id = randomUUID();
|
|
1122
|
-
partialBlocks.set(index, { id, kind: "text" });
|
|
1123
|
-
send({ type: "text-start", id });
|
|
1124
|
-
} else if (blockType === "thinking") {
|
|
1125
|
-
const id = randomUUID();
|
|
1126
|
-
partialBlocks.set(index, { id, kind: "thinking" });
|
|
1127
|
-
send({ type: "reasoning-start", id });
|
|
1128
|
-
}
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
if (event.type === "content_block_delta") {
|
|
1132
|
-
const block = partialBlocks.get(index);
|
|
1133
|
-
if (!block) return;
|
|
1134
|
-
if (block.kind === "text" && event.delta?.type === "text_delta" && typeof event.delta.text === "string") {
|
|
1135
|
-
send({ type: "text-delta", id: block.id, delta: event.delta.text });
|
|
1136
|
-
} else if (block.kind === "thinking" && event.delta?.type === "thinking_delta" && typeof event.delta.thinking === "string") {
|
|
1137
|
-
send({
|
|
1138
|
-
type: "reasoning-delta",
|
|
1139
|
-
id: block.id,
|
|
1140
|
-
delta: event.delta.thinking
|
|
1141
|
-
});
|
|
1142
|
-
}
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
if (event.type === "content_block_stop") {
|
|
1146
|
-
const block = partialBlocks.get(index);
|
|
1147
|
-
if (!block) return;
|
|
1148
|
-
partialBlocks.delete(index);
|
|
1149
|
-
if (block.kind === "text") {
|
|
1150
|
-
send({ type: "text-end", id: block.id });
|
|
1151
|
-
} else {
|
|
1152
|
-
send({ type: "reasoning-end", id: block.id });
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
1242
|
function createQueryInput({
|
|
1157
1243
|
initialUserMessage,
|
|
1158
1244
|
pendingUserMessages,
|
|
@@ -1204,37 +1290,6 @@ function createQueryInput({
|
|
|
1204
1290
|
}
|
|
1205
1291
|
};
|
|
1206
1292
|
}
|
|
1207
|
-
function stringifyContent(content) {
|
|
1208
|
-
if (typeof content === "string") return content;
|
|
1209
|
-
if (Array.isArray(content)) {
|
|
1210
|
-
return content.map(
|
|
1211
|
-
(entry) => entry && typeof entry === "object" && "text" in entry ? String(entry.text ?? "") : JSON.stringify(entry)
|
|
1212
|
-
).join("");
|
|
1213
|
-
}
|
|
1214
|
-
return JSON.stringify(content);
|
|
1215
|
-
}
|
|
1216
|
-
function mapUsage(usage) {
|
|
1217
|
-
if (!usage || typeof usage !== "object") return void 0;
|
|
1218
|
-
const u = usage;
|
|
1219
|
-
return {
|
|
1220
|
-
inputTokens: {
|
|
1221
|
-
total: (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0),
|
|
1222
|
-
noCache: u.input_tokens ?? 0,
|
|
1223
|
-
cacheRead: u.cache_read_input_tokens ?? 0,
|
|
1224
|
-
cacheWrite: u.cache_creation_input_tokens ?? 0
|
|
1225
|
-
},
|
|
1226
|
-
outputTokens: {
|
|
1227
|
-
total: u.output_tokens ?? 0,
|
|
1228
|
-
text: u.output_tokens ?? 0
|
|
1229
|
-
}
|
|
1230
|
-
};
|
|
1231
|
-
}
|
|
1232
|
-
function defaultUsage() {
|
|
1233
|
-
return {
|
|
1234
|
-
inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1235
|
-
outputTokens: { total: 0, text: 0 }
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
1293
|
function parseArgs(args2) {
|
|
1239
1294
|
const out = {};
|
|
1240
1295
|
for (let i = 0; i < args2.length; i++) {
|