
























@@ -767,6 +767,154 @@ describe("agentLoop tool termination", () => {
767767expect(events.filter((event) => event.type === "tool_execution_start")).toHaveLength(1);
768768expect(events.at(-1)).toMatchObject({ type: "agent_end" });
769769});
770+771+it("does not request another model turn after a tool aborts the run", async () => {
772+const controller = new AbortController();
773+let streamCalls = 0;
774+const streamFn: StreamFn = () => {
775+streamCalls += 1;
776+if (streamCalls > 1) {
777+throw new Error("model was called after abort");
778+}
779+const stream = createAssistantMessageEventStream();
780+queueMicrotask(() => {
781+const message = makeAssistantMessage([
782+{ type: "toolCall", id: "call-abort", name: "abort_tool", arguments: {} },
783+]);
784+stream.push({ type: "done", reason: "toolUse", message });
785+stream.end();
786+});
787+return stream;
788+};
789+const abortTool: AgentTool = {
790+name: "abort_tool",
791+label: "abort_tool",
792+description: "Abort the active run",
793+parameters: Type.Object({}, { additionalProperties: false }),
794+execute: async () => {
795+controller.abort(new Error("user aborted"));
796+return {
797+content: [{ type: "text", text: "aborted" }],
798+details: { aborted: true },
799+};
800+},
801+};
802+const events: AgentEvent[] = [];
803+804+const messages = await runAgentLoop(
805+[{ role: "user", content: "abort during tool", timestamp: 1 }],
806+{
807+systemPrompt: "",
808+messages: [],
809+tools: [abortTool],
810+},
811+config,
812+(event) => {
813+events.push(event);
814+},
815+controller.signal,
816+streamFn,
817+);
818+819+expect(streamCalls).toBe(1);
820+expect(messages.map((message) => message.role)).toEqual([
821+"user",
822+"assistant",
823+"toolResult",
824+"assistant",
825+]);
826+expect(messages.at(-1)).toMatchObject({ role: "assistant", stopReason: "aborted" });
827+expect(events.map((event) => event.type)).toEqual([
828+"agent_start",
829+"turn_start",
830+"message_start",
831+"message_end",
832+"message_start",
833+"message_end",
834+"tool_execution_start",
835+"tool_execution_end",
836+"message_start",
837+"message_end",
838+"turn_end",
839+"turn_start",
840+"message_start",
841+"message_end",
842+"turn_end",
843+"agent_end",
844+]);
845+expect(events.at(-1)).toMatchObject({ type: "agent_end" });
846+});
847+848+it("does not request another model turn when an async turn hook aborts the run", async () => {
849+const controller = new AbortController();
850+let streamCalls = 0;
851+const streamFn: StreamFn = () => {
852+streamCalls += 1;
853+if (streamCalls > 1) {
854+throw new Error("model was called after abort");
855+}
856+const stream = createAssistantMessageEventStream();
857+queueMicrotask(() => {
858+const message = makeAssistantMessage([
859+{ type: "toolCall", id: "call-hook-abort", name: "hook_abort", arguments: {} },
860+]);
861+stream.push({ type: "done", reason: "toolUse", message });
862+stream.end();
863+});
864+return stream;
865+};
866+const events: AgentEvent[] = [];
867+868+const messages = await runAgentLoop(
869+[{ role: "user", content: "abort from hook", timestamp: 1 }],
870+{
871+systemPrompt: "",
872+messages: [],
873+tools: [makeTool("hook_abort", [])],
874+},
875+{
876+ ...config,
877+prepareNextTurn: async () => {
878+await Promise.resolve();
879+controller.abort(new Error("user aborted"));
880+return undefined;
881+},
882+},
883+(event) => {
884+events.push(event);
885+},
886+controller.signal,
887+streamFn,
888+);
889+890+expect(streamCalls).toBe(1);
891+expect(messages.map((message) => message.role)).toEqual([
892+"user",
893+"assistant",
894+"toolResult",
895+"assistant",
896+]);
897+expect(messages.at(-1)).toMatchObject({ role: "assistant", stopReason: "aborted" });
898+expect(events.map((event) => event.type)).toEqual([
899+"agent_start",
900+"turn_start",
901+"message_start",
902+"message_end",
903+"message_start",
904+"message_end",
905+"tool_execution_start",
906+"tool_execution_end",
907+"message_start",
908+"message_end",
909+"turn_end",
910+"turn_start",
911+"message_start",
912+"message_end",
913+"turn_end",
914+"agent_end",
915+]);
916+expect(events.at(-1)).toMatchObject({ type: "agent_end" });
917+});
770918});
771919772920describe("agentLoop thinking state", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。