























@@ -4,6 +4,7 @@ import {
44extractCliErrorMessage,
55parseCliJson,
66parseCliJsonl,
7+type CliToolUseStartDelta,
78} from "./cli-output.js";
89import { createClaudeApiErrorFixture } from "./test-helpers/claude-api-error-fixture.js";
910@@ -537,4 +538,331 @@ describe("createCliJsonlStreamingParser", () => {
537538total: undefined,
538539});
539540});
541+542+it("surfaces Claude tool_use start and result events", () => {
543+const starts: CliToolUseStartDelta[] = [];
544+const results: Array<{ toolCallId: string; name: string; isError: boolean; result?: unknown }> =
545+[];
546+const parser = createCliJsonlStreamingParser({
547+backend: {
548+command: "local-cli",
549+output: "jsonl",
550+jsonlDialect: "claude-stream-json",
551+sessionIdFields: ["session_id"],
552+},
553+providerId: "claude-cli",
554+onAssistantDelta: () => undefined,
555+onToolUseStart: (delta) => starts.push(delta),
556+onToolResult: (delta) => results.push(delta),
557+});
558+559+parser.push(
560+[
561+JSON.stringify({
562+type: "assistant",
563+message: {
564+role: "assistant",
565+content: [
566+{ type: "tool_use", id: "toolu_1", name: "Bash", input: { command: "ls -la" } },
567+],
568+},
569+}),
570+JSON.stringify({
571+type: "user",
572+message: {
573+role: "user",
574+content: [
575+{
576+type: "tool_result",
577+tool_use_id: "toolu_1",
578+content: "total 0\n",
579+is_error: false,
580+},
581+],
582+},
583+}),
584+].join("\n") + "\n",
585+);
586+parser.finish();
587+588+expect(starts).toEqual([{ toolCallId: "toolu_1", name: "Bash", args: { command: "ls -la" } }]);
589+expect(results).toEqual([
590+{ toolCallId: "toolu_1", name: "Bash", isError: false, result: "total 0\n" },
591+]);
592+});
593+594+it("reassembles streamed tool args from input_json_delta chunks", () => {
595+const starts: CliToolUseStartDelta[] = [];
596+const parser = createCliJsonlStreamingParser({
597+backend: {
598+command: "local-cli",
599+output: "jsonl",
600+jsonlDialect: "claude-stream-json",
601+sessionIdFields: ["session_id"],
602+},
603+providerId: "claude-cli",
604+onAssistantDelta: () => undefined,
605+onToolUseStart: (delta) => starts.push(delta),
606+});
607+608+parser.push(
609+[
610+JSON.stringify({
611+type: "stream_event",
612+event: {
613+type: "content_block_start",
614+index: 0,
615+content_block: { type: "tool_use", id: "toolu_chunked", name: "Bash", input: {} },
616+},
617+}),
618+JSON.stringify({
619+type: "stream_event",
620+event: {
621+type: "content_block_delta",
622+index: 0,
623+delta: { type: "input_json_delta", partial_json: '{"command":' },
624+},
625+}),
626+JSON.stringify({
627+type: "stream_event",
628+event: {
629+type: "content_block_delta",
630+index: 0,
631+delta: { type: "input_json_delta", partial_json: ' "echo hi"}' },
632+},
633+}),
634+JSON.stringify({
635+type: "stream_event",
636+event: { type: "content_block_stop", index: 0 },
637+}),
638+].join("\n") + "\n",
639+);
640+parser.finish();
641+642+expect(starts).toEqual([
643+{ toolCallId: "toolu_chunked", name: "Bash", args: { command: "echo hi" } },
644+]);
645+});
646+647+it("emits empty args when streamed tool args are malformed", () => {
648+const starts: CliToolUseStartDelta[] = [];
649+const parser = createCliJsonlStreamingParser({
650+backend: {
651+command: "local-cli",
652+output: "jsonl",
653+jsonlDialect: "claude-stream-json",
654+sessionIdFields: ["session_id"],
655+},
656+providerId: "claude-cli",
657+onAssistantDelta: () => undefined,
658+onToolUseStart: (delta) => starts.push(delta),
659+});
660+661+parser.push(
662+[
663+JSON.stringify({
664+type: "stream_event",
665+event: {
666+type: "content_block_start",
667+index: 0,
668+content_block: { type: "tool_use", id: "toolu_bad", name: "Bash", input: {} },
669+},
670+}),
671+JSON.stringify({
672+type: "stream_event",
673+event: {
674+type: "content_block_delta",
675+index: 0,
676+delta: { type: "input_json_delta", partial_json: '{"command": "ls' },
677+},
678+}),
679+JSON.stringify({
680+type: "stream_event",
681+event: { type: "content_block_stop", index: 0 },
682+}),
683+].join("\n") + "\n",
684+);
685+parser.finish();
686+687+expect(starts).toEqual([{ toolCallId: "toolu_bad", name: "Bash", args: {} }]);
688+});
689+690+it.each(["server_tool_use", "mcp_tool_use"])("recognizes %s blocks", (type) => {
691+const starts: CliToolUseStartDelta[] = [];
692+const parser = createCliJsonlStreamingParser({
693+backend: {
694+command: "local-cli",
695+output: "jsonl",
696+jsonlDialect: "claude-stream-json",
697+sessionIdFields: ["session_id"],
698+},
699+providerId: "claude-cli",
700+onAssistantDelta: () => undefined,
701+onToolUseStart: (delta) => starts.push(delta),
702+});
703+704+parser.push(
705+[
706+JSON.stringify({
707+type: "stream_event",
708+event: {
709+type: "content_block_start",
710+index: 0,
711+content_block: { type, id: "toolu_hosted", name: "web_search", input: {} },
712+},
713+}),
714+JSON.stringify({
715+type: "stream_event",
716+event: {
717+type: "content_block_delta",
718+index: 0,
719+delta: { type: "input_json_delta", partial_json: '{"query":"openclaw"}' },
720+},
721+}),
722+JSON.stringify({
723+type: "stream_event",
724+event: { type: "content_block_stop", index: 0 },
725+}),
726+].join("\n") + "\n",
727+);
728+parser.finish();
729+730+expect(starts).toEqual([
731+{ toolCallId: "toolu_hosted", name: "web_search", args: { query: "openclaw" } },
732+]);
733+});
734+735+it.each([
736+{
737+useType: "server_tool_use",
738+resultType: "web_search_tool_result",
739+toolCallId: "srvtoolu_1",
740+name: "web_search",
741+input: { query: "openclaw" },
742+result: [{ type: "web_search_result", title: "OpenClaw", url: "https://example.com" }],
743+isError: false,
744+},
745+{
746+useType: "mcp_tool_use",
747+resultType: "mcp_tool_result",
748+toolCallId: "mcptoolu_1",
749+name: "echo",
750+input: { value: "hello" },
751+result: [{ type: "text", text: "hello" }],
752+isError: false,
753+},
754+])("emits hosted result events for $useType", (fixture) => {
755+const starts: CliToolUseStartDelta[] = [];
756+const results: Array<{ toolCallId: string; name: string; isError: boolean; result?: unknown }> =
757+[];
758+const parser = createCliJsonlStreamingParser({
759+backend: {
760+command: "local-cli",
761+output: "jsonl",
762+jsonlDialect: "claude-stream-json",
763+sessionIdFields: ["session_id"],
764+},
765+providerId: "claude-cli",
766+onAssistantDelta: () => undefined,
767+onToolUseStart: (delta) => starts.push(delta),
768+onToolResult: (delta) => results.push(delta),
769+});
770+771+parser.push(
772+[
773+JSON.stringify({
774+type: "assistant",
775+message: {
776+role: "assistant",
777+content: [
778+{
779+type: fixture.useType,
780+id: fixture.toolCallId,
781+name: fixture.name,
782+input: fixture.input,
783+},
784+{
785+type: fixture.resultType,
786+tool_use_id: fixture.toolCallId,
787+content: fixture.result,
788+is_error: fixture.isError,
789+},
790+],
791+},
792+}),
793+].join("\n") + "\n",
794+);
795+parser.finish();
796+797+expect(starts).toEqual([
798+{ toolCallId: fixture.toolCallId, name: fixture.name, args: fixture.input },
799+]);
800+expect(results).toEqual([
801+{
802+toolCallId: fixture.toolCallId,
803+name: fixture.name,
804+isError: fixture.isError,
805+result: fixture.result,
806+},
807+]);
808+});
809+810+it("emits streamed server tool result blocks", () => {
811+const results: Array<{ toolCallId: string; name: string; isError: boolean; result?: unknown }> =
812+[];
813+const parser = createCliJsonlStreamingParser({
814+backend: {
815+command: "local-cli",
816+output: "jsonl",
817+jsonlDialect: "claude-stream-json",
818+sessionIdFields: ["session_id"],
819+},
820+providerId: "claude-cli",
821+onAssistantDelta: () => undefined,
822+onToolUseStart: () => undefined,
823+onToolResult: (delta) => results.push(delta),
824+});
825+826+parser.push(
827+[
828+JSON.stringify({
829+type: "stream_event",
830+event: {
831+type: "content_block_start",
832+index: 0,
833+content_block: { type: "server_tool_use", id: "srvtoolu_stream", name: "web_search" },
834+},
835+}),
836+JSON.stringify({
837+type: "stream_event",
838+event: {
839+type: "content_block_stop",
840+index: 0,
841+},
842+}),
843+JSON.stringify({
844+type: "stream_event",
845+event: {
846+type: "content_block_start",
847+index: 1,
848+content_block: {
849+type: "web_search_tool_result",
850+tool_use_id: "srvtoolu_stream",
851+content: { type: "web_search_tool_result_error", error_code: "unavailable" },
852+},
853+},
854+}),
855+].join("\n") + "\n",
856+);
857+parser.finish();
858+859+expect(results).toEqual([
860+{
861+toolCallId: "srvtoolu_stream",
862+name: "web_search",
863+isError: true,
864+result: { type: "web_search_tool_result_error", error_code: "unavailable" },
865+},
866+]);
867+});
540868});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。