
























@@ -784,6 +784,120 @@ describe("sanitizeToolCallInputs allowed-name filtering", () => {
784784expect(ids).toEqual(expectedIds);
785785});
786786787+it("keeps finalized OpenAI Responses calls and drops partialJson streaming artifacts", () => {
788+const input = castAgentMessages([
789+{
790+role: "assistant",
791+stopReason: "toolUse",
792+content: [
793+// complete tool call — kept as-is
794+{ type: "toolCall", id: "call_ok", name: "read", arguments: { path: "/a" } },
795+// Legacy generic Responses transport persisted finalized toolUse
796+// turns with partialJson; repair strips the scratch field.
797+{
798+type: "toolCall",
799+id: "call_partial|fc_123",
800+name: "Bash",
801+arguments: { command: "ls" },
802+partialJson: '{"command": "ls"}',
803+},
804+{
805+type: "toolCall",
806+id: "call_empty|fc_789",
807+name: "session_status",
808+arguments: {},
809+partialJson: "",
810+},
811+// Anthropic can persist initialized tool calls with arguments: {}
812+// plus partialJson if the stream aborts before content_block_stop.
813+// Those incomplete artifacts must be dropped.
814+{
815+type: "toolCall",
816+id: "toolu_123",
817+name: "Bash",
818+arguments: {},
819+partialJson: '{"command":',
820+},
821+// An OpenAI-shaped id and parsed partial arguments do not prove that
822+// response.output_item.done arrived.
823+{
824+type: "toolCall",
825+id: "call_truncated|fc_456",
826+name: "Bash",
827+arguments: { command: "ls" },
828+partialJson: '{"command":"ls"',
829+},
830+// Missing required input is also an interrupted artifact and should drop.
831+{
832+type: "toolUse",
833+id: "call_partial2",
834+name: "read",
835+input: null,
836+partialJson: '{"path":',
837+},
838+],
839+},
840+{ role: "user", content: "retry" },
841+]);
842+const out = sanitizeToolCallInputs(input);
843+const toolCalls = getAssistantToolCallBlocks(out);
844+const ids = toolCalls.map((t) => (t as { id?: unknown }).id);
845+expect(ids).toEqual(["call_ok", "call_partial|fc_123", "call_empty|fc_789"]);
846+expect(toolCalls[1]).not.toHaveProperty("partialJson");
847+expect(toolCalls[2]).not.toHaveProperty("partialJson");
848+});
849+850+it("strips finalized partialJson without rewriting sessions_spawn arguments", () => {
851+const input = castAgentMessages([
852+{
853+role: "assistant",
854+stopReason: "toolUse",
855+content: [
856+{
857+type: "toolCall",
858+id: "call_spawn|fc_456",
859+name: "sessions_spawn",
860+arguments: { attachments: [{ content: "secret data" }] },
861+partialJson: '{"attachments":[{"content":"secret data"}]}',
862+},
863+],
864+},
865+]);
866+867+const out = sanitizeToolCallInputs(input);
868+const toolCalls = getAssistantToolCallBlocks(out);
869+expect(toolCalls).toHaveLength(1);
870+expect(toolCalls[0]).not.toHaveProperty("partialJson");
871+expect((toolCalls[0] as { arguments?: unknown }).arguments).toEqual({
872+attachments: [{ content: "secret data" }],
873+});
874+});
875+876+it.each(["stop", "aborted", "error", "length"] as const)(
877+"drops OpenAI Responses partialJson blocks on %s assistant turns",
878+(stopReason) => {
879+const input = castAgentMessages([
880+{
881+role: "assistant",
882+ stopReason,
883+content: [
884+{
885+type: "toolCall",
886+id: "call_partial|fc_123",
887+name: "Bash",
888+arguments: { command: "ls" },
889+partialJson: '{"command":"ls"}',
890+},
891+],
892+},
893+{ role: "user", content: "retry" },
894+]);
895+896+const out = sanitizeToolCallInputs(input);
897+expect(getAssistantToolCallBlocks(out)).toHaveLength(0);
898+},
899+);
900+787901it("keeps valid tool calls and preserves text blocks", () => {
788902const input = castAgentMessages([
789903{
@@ -835,6 +949,36 @@ describe("sanitizeToolCallInputs allowed-name filtering", () => {
835949expect(out).toStrictEqual([]);
836950});
837951952+it("drops signed-thinking assistant turns with partialJson tool calls", () => {
953+const input = castAgentMessages([
954+{
955+role: "assistant",
956+stopReason: "toolUse",
957+content: [
958+{
959+type: "thinking",
960+thinking: "Let me run a command.",
961+thinkingSignature: "sig_partial",
962+},
963+{
964+type: "toolCall",
965+id: "call_partial|fc_123",
966+name: "exec",
967+arguments: {},
968+partialJson: '{"command":"ls"}',
969+},
970+],
971+},
972+]);
973+974+const out = sanitizeToolCallInputs(input, {
975+allowedToolNames: ["exec"],
976+allowProviderOwnedThinkingReplay: true,
977+});
978+979+expect(out).toStrictEqual([]);
980+});
981+838982it("drops signed-thinking assistant turns when sibling tool calls reuse an id", () => {
839983const input = castAgentMessages([
840984{
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。