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