| ... | ... | @@ -761,81 +761,70 @@ it.layer(layer)("AntigravityAdapter", (it) => { |
| 761 | 761 | }), |
| 762 | 762 | ); |
| 763 | 763 | |
| 764 | | it.effect( |
| 765 | | "shows concurrent native subagent calls and their results without inventing metadata", |
| 766 | | () => |
| 767 | | Effect.gen(function* () { |
| 768 | | const h = yield* makeHarness(); |
| 769 | | yield* h.adapter.startSession({ |
| 770 | | threadId, |
| 771 | | cwd: process.cwd(), |
| 772 | | runtimeMode: "approval-required", |
| 773 | | }); |
| 774 | | const sending = yield* h.adapter |
| 775 | | .sendTurn({ threadId, input: "Review with subagents" }) |
| 776 | | .pipe(Effect.forkChild); |
| 777 | | const prompt = yield* h.nextPrompt; |
| 778 | | for (const id of ["trajectory:4", "trajectory:5"]) { |
| 779 | | yield* h.emitNative( |
| 780 | | nativeToolUpdate({ |
| 781 | | sessionUpdate: "tool_call", |
| 782 | | toolCallId: id, |
| 783 | | title: "Running start_subagent", |
| 784 | | kind: "other", |
| 785 | | status: "in_progress", |
| 786 | | rawInput: {}, |
| 787 | | }), |
| 788 | | ); |
| 789 | | const running = yield* h.waitForEvent((event) => event.type === "task.progress"); |
| 790 | | expect(running.payload).toEqual({ |
| 791 | | taskId: id, |
| 792 | | taskType: "subagent", |
| 793 | | toolUseId: id, |
| 794 | | title: "Antigravity subagent", |
| 795 | | description: "Antigravity subagent", |
| 796 | | status: "running", |
| 797 | | }); |
| 798 | | yield* h.emitNative( |
| 799 | | nativeToolUpdate({ |
| 800 | | sessionUpdate: "tool_call_update", |
| 801 | | toolCallId: id, |
| 802 | | status: "in_progress", |
| 803 | | }), |
| 804 | | ); |
| 805 | | } |
| 806 | | for (const [id, status, result] of [ |
| 807 | | ["trajectory:4", "completed", "No defects found."], |
| 808 | | ["trajectory:5", "failed", "Subagent exceeded its limit."], |
| 809 | | ] as const) { |
| 810 | | yield* h.emitNative( |
| 811 | | nativeToolUpdate({ |
| 812 | | sessionUpdate: "tool_call_update", |
| 813 | | toolCallId: id, |
| 814 | | status, |
| 815 | | rawOutput: result, |
| 816 | | }), |
| 817 | | ); |
| 818 | | const completed = yield* h.waitForEvent((event) => event.type === "task.completed"); |
| 819 | | expect(completed.payload).toEqual({ |
| 820 | | taskId: id, |
| 821 | | taskType: "subagent", |
| 822 | | toolUseId: id, |
| 823 | | title: "Antigravity subagent", |
| 824 | | status, |
| 825 | | summary: result, |
| 826 | | }); |
| 827 | | } |
| 828 | | yield* Deferred.succeed(prompt.result, { stopReason: "end_turn" }); |
| 829 | | const turn = yield* Fiber.join(sending); |
| 830 | | yield* h.waitForEvent((event) => event.type === "turn.completed"); |
| 831 | | expect( |
| 832 | | h.seen |
| 833 | | .filter((event) => event.type.startsWith("task.")) |
| 834 | | .every((event) => event.turnId === turn.turnId), |
| 835 | | ).toBe(true); |
| 836 | | expect(h.seen.filter((event) => event.type.startsWith("item."))).toHaveLength(0); |
| 837 | | expect(h.seen.filter((event) => event.type === "task.progress")).toHaveLength(2); |
| 838 | | }), |
| 764 | it.effect("keeps a launched batch active while child tools continue", () => |
| 765 | Effect.gen(function* () { |
| 766 | const h = yield* makeHarness(); |
| 767 | yield* h.adapter.startSession({ threadId, cwd: process.cwd(), runtimeMode: "full-access" }); |
| 768 | const sending = yield* h.adapter |
| 769 | .sendTurn({ threadId, input: "Run two readers in one batch" }) |
| 770 | .pipe(Effect.forkChild); |
| 771 | const prompt = yield* h.nextPrompt; |
| 772 | // ACP 1.1.1 capture: one launch call covers both children and returns |
| 773 | // only its description before either child finishes. |
| 774 | const started = nativeToolUpdate({ |
| 775 | sessionUpdate: "tool_call", |
| 776 | toolCallId: `${nativeSessionId}:2`, |
| 777 | title: "Running start_subagent", |
| 778 | kind: "other", |
| 779 | status: "in_progress", |
| 780 | rawInput: {}, |
| 781 | }); |
| 782 | yield* h.emitNative(started); |
| 783 | yield* h.waitForEvent((event) => event.type === "task.progress"); |
| 784 | yield* h.emitNative( |
| 785 | nativeToolUpdate( |
| 786 | { |
| 787 | sessionUpdate: "tool_call_update", |
| 788 | toolCallId: started.toolCall.toolCallId, |
| 789 | status: "completed", |
| 790 | rawOutput: "Launch subagents", |
| 791 | }, |
| 792 | started.toolCall, |
| 793 | ), |
| 794 | ); |
| 795 | const launched = yield* h.waitForEvent((event) => event.type === "task.progress"); |
| 796 | expect(launched.payload).toMatchObject({ |
| 797 | taskId: started.toolCall.toolCallId, |
| 798 | title: "Antigravity subagent batch", |
| 799 | description: "Launch subagents", |
| 800 | status: "running", |
| 801 | }); |
| 802 | for (const child of ["alpha", "beta"]) { |
| 803 | yield* h.emitNative( |
| 804 | nativeToolUpdate({ |
| 805 | sessionUpdate: "tool_call", |
| 806 | toolCallId: `${child}:1`, |
| 807 | title: "Read file", |
| 808 | kind: "read", |
| 809 | status: "completed", |
| 810 | rawOutput: "File contents", |
| 811 | }), |
| 812 | ); |
| 813 | } |
| 814 | yield* h.drainEvents; |
| 815 | expect(h.seen.filter((event) => event.type === "task.completed")).toHaveLength(0); |
| 816 | expect(h.seen.filter((event) => event.type === "task.updated")).toHaveLength(0); |
| 817 | yield* Deferred.succeed(prompt.result, { stopReason: "end_turn" }); |
| 818 | yield* Fiber.join(sending); |
| 819 | yield* h.waitForEvent((event) => event.type === "turn.completed"); |
| 820 | expect(h.seen.filter((event) => event.type === "task.completed")).toHaveLength(0); |
| 821 | expect(h.seen.find((event) => event.type === "task.updated")?.payload).toMatchObject({ |
| 822 | taskId: started.toolCall.toolCallId, |
| 823 | status: "idle", |
| 824 | description: "Turn ended. Individual agent status is unavailable.", |
| 825 | timelineBypass: true, |
| 826 | }); |
| 827 | }), |
| 839 | 828 | ); |
| 840 | 829 | |
| 841 | 830 | it.effect("waits for a replayed subagent's final status and result", () => |
| ... | ... | @@ -870,7 +859,7 @@ it.layer(layer)("AntigravityAdapter", (it) => { |
| 870 | 859 | taskId: "replayed:4", |
| 871 | 860 | taskType: "subagent", |
| 872 | 861 | toolUseId: "replayed:4", |
| 873 | | title: "Antigravity subagent", |
| 862 | title: "Antigravity subagent batch", |
| 874 | 863 | status: "failed", |
| 875 | 864 | summary: "Review failed.", |
| 876 | 865 | }); |
| ... | ... | @@ -878,128 +867,93 @@ it.layer(layer)("AntigravityAdapter", (it) => { |
| 878 | 867 | }), |
| 879 | 868 | ); |
| 880 | 869 | |
| 881 | | it.effect("completes a live subagent delivered in one tool call", () => |
| 870 | it.effect("keeps one-message launches active and ignores late updates after settlement", () => |
| 882 | 871 | Effect.gen(function* () { |
| 883 | 872 | const h = yield* makeHarness(); |
| 884 | | yield* h.adapter.startSession({ |
| 885 | | threadId, |
| 886 | | cwd: process.cwd(), |
| 887 | | runtimeMode: "approval-required", |
| 888 | | }); |
| 889 | | const sending = yield* h.adapter |
| 890 | | .sendTurn({ threadId, input: "Review with subagents" }) |
| 873 | yield* h.adapter.startSession({ threadId, cwd: process.cwd(), runtimeMode: "full-access" }); |
| 874 | const first = yield* h.adapter |
| 875 | .sendTurn({ threadId, input: "Start readers" }) |
| 891 | 876 | .pipe(Effect.forkChild); |
| 892 | | const prompt = yield* h.nextPrompt; |
| 893 | | for (const [id, rawOutput] of [ |
| 894 | | ["live:4", "Review complete."], |
| 895 | | ["live:5", undefined], |
| 896 | | ] as const) { |
| 897 | | yield* h.emitNative( |
| 898 | | nativeToolUpdate({ |
| 899 | | sessionUpdate: "tool_call", |
| 900 | | toolCallId: id, |
| 901 | | title: "Running start_subagent", |
| 902 | | kind: "other", |
| 903 | | status: "completed", |
| 904 | | rawInput: {}, |
| 905 | | ...(rawOutput ? { rawOutput } : {}), |
| 906 | | }), |
| 907 | | ); |
| 908 | | const completed = yield* h.waitForEvent((event) => event.type === "task.completed"); |
| 909 | | expect(completed.payload).toMatchObject({ taskId: id, status: "completed" }); |
| 910 | | expect(completed.payload.summary).toBe(rawOutput); |
| 911 | | } |
| 912 | | yield* Deferred.succeed(prompt.result, { stopReason: "end_turn" }); |
| 913 | | yield* Fiber.join(sending); |
| 914 | | yield* h.waitForEvent((event) => event.type === "turn.completed"); |
| 915 | | expect(h.seen.filter((event) => event.type === "task.updated")).toHaveLength(0); |
| 916 | | }), |
| 917 | | ); |
| 918 | | |
| 919 | | for (const settlement of ["cancelled", "interrupted", "completed"] as const) { |
| 920 | | it.effect(`does not reopen a ${settlement} subagent on late merged updates`, () => |
| 921 | | Effect.gen(function* () { |
| 922 | | const h = yield* makeHarness(); |
| 923 | | yield* h.adapter.startSession({ |
| 924 | | threadId, |
| 925 | | cwd: process.cwd(), |
| 926 | | runtimeMode: "approval-required", |
| 927 | | }); |
| 928 | | const first = yield* h.adapter |
| 929 | | .sendTurn({ threadId, input: "Start review" }) |
| 930 | | .pipe(Effect.forkChild); |
| 931 | | const firstPrompt = yield* h.nextPrompt; |
| 932 | | const started = nativeToolUpdate({ |
| 877 | const firstPrompt = yield* h.nextPrompt; |
| 878 | const launches = ["Launch readers", undefined].map((rawOutput, index) => |
| 879 | nativeToolUpdate({ |
| 933 | 880 | sessionUpdate: "tool_call", |
| 934 | | toolCallId: "old:4", |
| 881 | toolCallId: `old:${index}`, |
| 935 | 882 | title: "Running start_subagent", |
| 936 | 883 | kind: "other", |
| 937 | | status: "in_progress", |
| 884 | status: "completed", |
| 938 | 885 | rawInput: {}, |
| 939 | | }); |
| 940 | | yield* h.emitNative(started); |
| 941 | | yield* h.waitForEvent((event) => event.type === "task.progress"); |
| 942 | | if (settlement === "cancelled") { |
| 943 | | yield* h.adapter.interruptTurn(threadId); |
| 944 | | } else { |
| 945 | | if (settlement === "completed") { |
| 946 | | yield* h.emitNative( |
| 947 | | nativeToolUpdate( |
| 948 | | { |
| 949 | | sessionUpdate: "tool_call_update", |
| 950 | | toolCallId: "old:4", |
| 951 | | status: "completed", |
| 952 | | rawOutput: "Original result.", |
| 953 | | }, |
| 954 | | started.toolCall, |
| 955 | | ), |
| 956 | | ); |
| 957 | | } |
| 958 | | yield* Deferred.succeed(firstPrompt.result, { stopReason: "end_turn" }); |
| 959 | | } |
| 960 | | yield* Fiber.join(first); |
| 961 | | yield* h.waitForEvent((event) => event.type === "turn.completed"); |
| 962 | | const second = yield* h.adapter |
| 963 | | .sendTurn({ threadId, input: "Next review" }) |
| 964 | | .pipe(Effect.forkChild); |
| 965 | | const secondPrompt = yield* h.nextPrompt; |
| 966 | | for (const status of ["in_progress", "completed"] as const) { |
| 886 | ...(rawOutput ? { rawOutput } : {}), |
| 887 | }), |
| 888 | ); |
| 889 | for (const launch of launches) yield* h.emitNative(launch); |
| 890 | yield* h.drainEvents; |
| 891 | expect(h.seen.filter((event) => event.type === "task.progress")).toHaveLength(2); |
| 892 | expect(h.seen.filter((event) => event.type === "task.completed")).toHaveLength(0); |
| 893 | yield* Deferred.succeed(firstPrompt.result, { stopReason: "end_turn" }); |
| 894 | yield* Fiber.join(first); |
| 895 | yield* h.waitForEvent((event) => event.type === "turn.completed"); |
| 896 | const second = yield* h.adapter |
| 897 | .sendTurn({ threadId, input: "Next task" }) |
| 898 | .pipe(Effect.forkChild); |
| 899 | const secondPrompt = yield* h.nextPrompt; |
| 900 | for (const launch of launches) { |
| 901 | for (const status of ["in_progress", "completed", "failed"] as const) { |
| 967 | 902 | yield* h.emitNative( |
| 968 | 903 | nativeToolUpdate( |
| 969 | 904 | { |
| 970 | 905 | sessionUpdate: "tool_call_update", |
| 971 | | toolCallId: "old:4", |
| 906 | toolCallId: launch.toolCall.toolCallId, |
| 972 | 907 | status, |
| 973 | | rawOutput: "Late result.", |
| 908 | rawOutput: "Late update", |
| 974 | 909 | }, |
| 975 | | started.toolCall, |
| 910 | launch.toolCall, |
| 976 | 911 | ), |
| 977 | 912 | ); |
| 978 | 913 | } |
| 979 | | yield* h.emitNative( |
| 980 | | nativeToolUpdate({ |
| 981 | | sessionUpdate: "tool_call", |
| 982 | | toolCallId: "new:4", |
| 983 | | title: "Running start_subagent", |
| 984 | | kind: "other", |
| 914 | } |
| 915 | yield* h.drainEvents; |
| 916 | expect(h.seen.filter((event) => event.type === "task.progress")).toHaveLength(2); |
| 917 | expect(h.seen.filter((event) => event.type === "task.updated")).toHaveLength(2); |
| 918 | expect(h.seen.filter((event) => event.type === "task.completed")).toHaveLength(0); |
| 919 | yield* Deferred.succeed(secondPrompt.result, { stopReason: "end_turn" }); |
| 920 | yield* Fiber.join(second); |
| 921 | }), |
| 922 | ); |
| 923 | |
| 924 | it.effect("does not report a historical launch as running or completed work", () => |
| 925 | Effect.gen(function* () { |
| 926 | const h = yield* makeHarness(); |
| 927 | yield* h.adapter.startSession({ threadId, cwd: process.cwd(), runtimeMode: "full-access" }); |
| 928 | const launch = nativeToolUpdate({ |
| 929 | sessionUpdate: "tool_call", |
| 930 | toolCallId: "history:2", |
| 931 | title: "Running start_subagent", |
| 932 | kind: "other", |
| 933 | status: "completed", |
| 934 | rawInput: {}, |
| 935 | }); |
| 936 | yield* h.emitNative(launch); |
| 937 | yield* h.emitNative( |
| 938 | nativeToolUpdate( |
| 939 | { |
| 940 | sessionUpdate: "tool_call_update", |
| 941 | toolCallId: launch.toolCall.toolCallId, |
| 985 | 942 | status: "completed", |
| 986 | | rawOutput: "New result.", |
| 987 | | }), |
| 988 | | ); |
| 989 | | const completed = yield* h.waitForEvent((event) => event.type === "task.completed"); |
| 990 | | expect(completed.payload.taskId).toBe("new:4"); |
| 991 | | yield* Deferred.succeed(secondPrompt.result, { stopReason: "end_turn" }); |
| 992 | | yield* Fiber.join(second); |
| 993 | | yield* h.waitForEvent((event) => event.type === "turn.completed"); |
| 994 | | expect(h.seen.filter((event) => event.type === "task.progress")).toHaveLength(1); |
| 995 | | expect( |
| 996 | | h.seen.filter( |
| 997 | | (event) => event.type === "task.completed" && event.payload.taskId === "old:4", |
| 998 | | ), |
| 999 | | ).toHaveLength(settlement === "completed" ? 1 : 0); |
| 1000 | | }), |
| 1001 | | ); |
| 1002 | | } |
| 943 | rawOutput: "Launch readers", |
| 944 | }, |
| 945 | launch.toolCall, |
| 946 | ), |
| 947 | ); |
| 948 | yield* h.drainEvents; |
| 949 | expect(h.seen.filter((event) => event.type.startsWith("task."))).toMatchObject([ |
| 950 | { |
| 951 | type: "task.updated", |
| 952 | payload: { status: "idle", timelineBypass: true }, |
| 953 | }, |
| 954 | ]); |
| 955 | }), |
| 956 | ); |
| 1003 | 957 | |
| 1004 | 958 | it.effect("keeps MCP identity when later updates omit metadata", () => |
| 1005 | 959 | Effect.gen(function* () { |
| ... | ... | @@ -1101,6 +1055,15 @@ it.layer(layer)("AntigravityAdapter", (it) => { |
| 1101 | 1055 | }), |
| 1102 | 1056 | ); |
| 1103 | 1057 | yield* h.waitForEvent((event) => event.type === "task.progress"); |
| 1058 | yield* h.emitNative( |
| 1059 | nativeToolUpdate({ |
| 1060 | sessionUpdate: "tool_call_update", |
| 1061 | toolCallId: "trajectory:4", |
| 1062 | status: "completed", |
| 1063 | rawOutput: "Launch subagents", |
| 1064 | }), |
| 1065 | ); |
| 1066 | yield* h.waitForEvent((event) => event.type === "task.progress"); |
| 1104 | 1067 | if (stop === "disconnect") { |
| 1105 | 1068 | yield* h.emitNative({ |
| 1106 | 1069 | _tag: "ConnectionTerminated", |
| ... | ... | @@ -1121,14 +1084,14 @@ it.layer(layer)("AntigravityAdapter", (it) => { |
| 1121 | 1084 | const settled = yield* h.waitForEvent((event) => event.type === "task.updated"); |
| 1122 | 1085 | expect(settled.payload).toMatchObject({ |
| 1123 | 1086 | taskId: "trajectory:4", |
| 1124 | | title: "Antigravity subagent", |
| 1087 | title: "Antigravity subagent batch", |
| 1125 | 1088 | taskType: "subagent", |
| 1126 | 1089 | status: |
| 1127 | 1090 | stop === "disconnect" |
| 1128 | 1091 | ? "failed" |
| 1129 | 1092 | : stop === "cancel" || stop === "steer" |
| 1130 | 1093 | ? "cancelled" |
| 1131 | | : "interrupted", |
| 1094 | : "idle", |
| 1132 | 1095 | }); |
| 1133 | 1096 | if (stop === "disconnect") |
| 1134 | 1097 | yield* h.waitForEvent((event) => event.type === "session.exited"); |