authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-03 20:29:56-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 20:29:56-07:00
log2675e3c70327719a99af4ae6e53e7b74fb8a9be0
tree7618484879ec293cffe39cbc2ace2b086f33e152
parentbf40fa786c521b552eb554bbd4f2c75c4123cd03
signature Signed by PGP key B5690EEEBB952194

fix(antigravity): keep subagent batches active after launch (#9579)


7 files changed, 227 insertions(+), 210 deletions(-)

apps/server/src/provider/Layers/AntigravityAdapter.test.ts+146-183
...@@ -761,81 +761,70 @@ it.layer(layer)("AntigravityAdapter", (it) => {...@@ -761,81 +761,70 @@ it.layer(layer)("AntigravityAdapter", (it) => {
761 }),761 }),
762 );762 );
763763
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.adapter774 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 );
840829
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 );
880869
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 }
1003957
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");
apps/server/src/provider/Layers/AntigravityAdapter.ts+36-13
...@@ -71,7 +71,7 @@ import {...@@ -71,7 +71,7 @@ import {
71} from "../acp/AntigravityAcpSupport.ts";71} from "../acp/AntigravityAcpSupport.ts";
72import {72import {
73 antigravityApprovalOptions,73 antigravityApprovalOptions,
74 antigravitySubagentResult,74 antigravitySubagentOutput,
75 classifyAntigravitySubagentToolCall,75 classifyAntigravitySubagentToolCall,
76 extractAntigravityUserInputQuestion,76 extractAntigravityUserInputQuestion,
77 isAntigravityOpenCommand,77 isAntigravityOpenCommand,
...@@ -170,6 +170,7 @@ interface OpenCommand {...@@ -170,6 +170,7 @@ interface OpenCommand {
170interface OpenSubagent {170interface OpenSubagent {
171 readonly turnId: TurnId | undefined;171 readonly turnId: TurnId | undefined;
172 readonly status: "pending" | "running" | undefined;172 readonly status: "pending" | "running" | undefined;
173 readonly description?: string;
173}174}
174175
175function subagentLinkage(toolCallId: string) {176function subagentLinkage(toolCallId: string) {
...@@ -177,7 +178,7 @@ function subagentLinkage(toolCallId: string) {...@@ -177,7 +178,7 @@ function subagentLinkage(toolCallId: string) {
177 taskId: RuntimeTaskId.make(toolCallId),178 taskId: RuntimeTaskId.make(toolCallId),
178 taskType: "subagent",179 taskType: "subagent",
179 toolUseId: toolCallId,180 toolUseId: toolCallId,
180 title: "Antigravity subagent",181 title: "Antigravity subagent batch",
181 };182 };
182}183}
183184
...@@ -389,7 +390,7 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi...@@ -389,7 +390,7 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
389390
390 const finishSubagents = (391 const finishSubagents = (
391 context: SessionContext,392 context: SessionContext,
392 status: Extract<RuntimeTaskStatus, "cancelled" | "failed" | "interrupted">,393 status: Extract<RuntimeTaskStatus, "cancelled" | "failed" | "idle">,
393 error?: string,394 error?: string,
394 ) =>395 ) =>
395 context.commandLock.withPermit(396 context.commandLock.withPermit(
...@@ -405,6 +406,12 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi...@@ -405,6 +406,12 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
405 payload: {406 payload: {
406 ...subagentLinkage(id),407 ...subagentLinkage(id),
407 status,408 status,
409 ...(status === "idle"
410 ? {
411 description: "Turn ended. Individual agent status is unavailable.",
412 timelineBypass: true,
413 }
414 : {}),
408 ...(error ? { error } : {}),415 ...(error ? { error } : {}),
409 },416 },
410 });417 });
...@@ -628,8 +635,8 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi...@@ -628,8 +635,8 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
628 context.subagents.set(toolCall.toolCallId, { turnId, status: undefined });635 context.subagents.set(toolCall.toolCallId, { turnId, status: undefined });
629 return;636 return;
630 }637 }
631 if (toolCall.status === "completed" || toolCall.status === "failed") {638 if (toolCall.status === "failed") {
632 const summary = antigravitySubagentResult(toolCall);639 const summary = antigravitySubagentOutput(toolCall);
633 yield* emit({640 yield* emit({
634 type: "task.completed",641 type: "task.completed",
635 ...(yield* stamp),642 ...(yield* stamp),
...@@ -643,19 +650,38 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi...@@ -643,19 +650,38 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
643 },650 },
644 });651 });
645 context.subagents.set(toolCall.toolCallId, "finished");652 context.subagents.set(toolCall.toolCallId, "finished");
653 } else if (context.activeTurnId === undefined && toolCall.status === "completed") {
654 yield* emit({
655 type: "task.updated",
656 ...(yield* stamp),
657 provider: PROVIDER,
658 threadId: context.threadId,
659 turnId,
660 payload: {
661 ...linkage,
662 status: "idle",
663 description: "Individual agent status is unavailable for this earlier batch.",
664 timelineBypass: true,
665 },
666 });
667 context.subagents.set(toolCall.toolCallId, "finished");
646 } else {668 } else {
669 // start_subagent returns after launching a batch. Its output is
670 // the launch description, not a child result or completion.
647 const status = toolCall.status === "pending" ? "pending" : "running";671 const status = toolCall.status === "pending" ? "pending" : "running";
648 if (subagent?.status !== status) {672 const description =
673 antigravitySubagentOutput(toolCall) ?? subagent?.description ?? linkage.title;
674 if (subagent?.status !== status || subagent?.description !== description) {
649 yield* emit({675 yield* emit({
650 type: "task.progress",676 type: "task.progress",
651 ...(yield* stamp),677 ...(yield* stamp),
652 provider: PROVIDER,678 provider: PROVIDER,
653 threadId: context.threadId,679 threadId: context.threadId,
654 turnId,680 turnId,
655 payload: { ...linkage, description: linkage.title, status },681 payload: { ...linkage, description, summary: description, status },
656 });682 });
657 }683 }
658 context.subagents.set(toolCall.toolCallId, { turnId, status });684 context.subagents.set(toolCall.toolCallId, { turnId, status, description });
659 }685 }
660 return;686 return;
661 }687 }
...@@ -972,11 +998,8 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi...@@ -972,11 +998,8 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
972 ? "cancelled"998 ? "cancelled"
973 : payload.state === "failed"999 : payload.state === "failed"
974 ? "failed"1000 ? "failed"
975 : "interrupted",1001 : "idle",
976 payload.errorMessage ??1002 payload.errorMessage,
977 (payload.state === "completed"
978 ? "Antigravity ended the turn before reporting a subagent result."
979 : undefined),
980 );1003 );
981 context.activeTurnId = undefined;1004 context.activeTurnId = undefined;
982 context.promptFiber = undefined;1005 context.promptFiber = undefined;
apps/server/src/provider/acp/AntigravityProtocol.test.ts+5-5
...@@ -6,7 +6,7 @@ import {...@@ -6,7 +6,7 @@ import {
6 extractAntigravityUserInputQuestion,6 extractAntigravityUserInputQuestion,
7 isAntigravityOpenCommand,7 isAntigravityOpenCommand,
8 antigravityApprovalOptions,8 antigravityApprovalOptions,
9 antigravitySubagentResult,9 antigravitySubagentOutput,
10 isAntigravitySubagentReplayStart,10 isAntigravitySubagentReplayStart,
11 classifyAntigravitySubagentToolCall,11 classifyAntigravitySubagentToolCall,
12 isAntigravityUserInputRequest,12 isAntigravityUserInputRequest,
...@@ -48,7 +48,7 @@ describe("native Antigravity subagent tools", () => {...@@ -48,7 +48,7 @@ describe("native Antigravity subagent tools", () => {
48 ).toBeUndefined();48 ).toBeUndefined();
49 });49 });
5050
51 it("recognizes history starts and bounds the native result", () => {51 it("recognizes history starts and bounds the launch output", () => {
52 expect(52 expect(
53 isAntigravitySubagentReplayStart({53 isAntigravitySubagentReplayStart({
54 update: { sessionUpdate: "tool_call", status: "completed", rawOutput: "Done." },54 update: { sessionUpdate: "tool_call", status: "completed", rawOutput: "Done." },
...@@ -65,19 +65,19 @@ describe("native Antigravity subagent tools", () => {...@@ -65,19 +65,19 @@ describe("native Antigravity subagent tools", () => {
65 }),65 }),
66 ).toBe(false);66 ).toBe(false);
67 expect(67 expect(
68 antigravitySubagentResult({68 antigravitySubagentOutput({
69 toolCallId: "trajectory:4",69 toolCallId: "trajectory:4",
70 data: { rawOutput: " Finished review. " },70 data: { rawOutput: " Finished review. " },
71 }),71 }),
72 ).toBe("Finished review.");72 ).toBe("Finished review.");
73 const result = antigravitySubagentResult({73 const result = antigravitySubagentOutput({
74 toolCallId: "trajectory:4",74 toolCallId: "trajectory:4",
75 data: { rawOutput: `${"x".repeat(16_000)}The result.` },75 data: { rawOutput: `${"x".repeat(16_000)}The result.` },
76 });76 });
77 expect(result?.length).toBeLessThan(8_100);77 expect(result?.length).toBeLessThan(8_100);
78 expect(result?.endsWith("The result.")).toBe(true);78 expect(result?.endsWith("The result.")).toBe(true);
79 expect(79 expect(
80 antigravitySubagentResult({ toolCallId: "trajectory:4", data: { rawOutput: {} } }),80 antigravitySubagentOutput({ toolCallId: "trajectory:4", data: { rawOutput: {} } }),
81 ).toBeUndefined();81 ).toBeUndefined();
82 });82 });
83});83});
apps/server/src/provider/acp/AntigravityProtocol.ts+1-1
...@@ -376,7 +376,7 @@ export function isAntigravitySubagentReplayStart(rawPayload: unknown): boolean {...@@ -376,7 +376,7 @@ export function isAntigravitySubagentReplayStart(rawPayload: unknown): boolean {
376 );376 );
377}377}
378378
379export function antigravitySubagentResult(toolCall: AcpToolCallState): string | undefined {379export function antigravitySubagentOutput(toolCall: AcpToolCallState): string | undefined {
380 const output = toolCall.data.rawOutput;380 const output = toolCall.data.rawOutput;
381 return typeof output === "string" && output.trim() ? boundText(output.trim()) : undefined;381 return typeof output === "string" && output.trim() ? boundText(output.trim()) : undefined;
382}382}
docs/user/providers-antigravity.md+9-8
...@@ -141,14 +141,15 @@ follow-up message or start a new thread instead....@@ -141,14 +141,15 @@ follow-up message or start a new thread instead.
141141
142### Subagents142### Subagents
143143
144Antigravity subagent calls appear in **Agents** on web and desktop, and in the work log on144Subagent launches appear as **Antigravity subagent batch** in **Agents** on web and desktop,
145mobile. Each call shows its status and the result or error returned by Antigravity. Calls145and in the work log on mobile. One launch can start several agents. The batch stays active
146that run at the same time have separate entries.146after launch while the parent turn runs. When that turn ends, the entry becomes idle and
147147states that individual agent status is unavailable. Launch errors remain visible.
148The official ACP agent does not report subagent names, models, token usage, or parent links.148
149Entries use the name **Antigravity subagent**. Child tool calls cannot be assigned to an entry149The official ACP agent does not send individual child status, names, models, token usage,
150because ACP does not include their owning subagent. These entries track each invocation,150or reply ownership. T3 Code cannot show separate child entries or separate child replies
151not a separate thread you can open or control.151from the parent conversation. The launch description is not a child result. Batch entries
152cannot be opened or controlled as separate threads.
152153
153## Accounts and removal154## Accounts and removal
154155
packages/client-runtime/src/state/subagentRuntime.test.ts+28
...@@ -66,6 +66,34 @@ function fold(rows: ReadonlyArray<OrchestrationThreadActivity>) {...@@ -66,6 +66,34 @@ function fold(rows: ReadonlyArray<OrchestrationThreadActivity>) {
66}66}
6767
68describe("foldSubagentActivities", () => {68describe("foldSubagentActivities", () => {
69 it("shows the batch status limit after its parent turn ends without claiming a result", () => {
70 const running = activity("task.progress", {
71 taskId: "batch-1",
72 taskType: "subagent",
73 title: "Antigravity subagent batch",
74 status: "running",
75 summary: "Launch readers",
76 });
77 const agents = fold([
78 running,
79 activity("task.updated", {
80 taskId: "batch-1",
81 taskType: "subagent",
82 status: "idle",
83 detail: "Turn ended. Individual agent status is unavailable.",
84 timelineBypass: true,
85 }),
86 ]);
87 expect(agents).toHaveLength(1);
88 expect(agents[0]).toMatchObject({
89 title: "Antigravity subagent batch",
90 status: "idle",
91 progress: "Turn ended. Individual agent status is unavailable.",
92 result: null,
93 error: null,
94 });
95 });
96
69 it("builds an agent from start → progress → completion", () => {97 it("builds an agent from start → progress → completion", () => {
70 const agents = fold([98 const agents = fold([
71 activity("task.started", {99 activity("task.started", {
packages/client-runtime/src/state/subagentRuntime.ts+2
...@@ -546,6 +546,8 @@ export function foldSubagentActivities(...@@ -546,6 +546,8 @@ export function foldSubagentActivities(
546 if (!agents.has(taskId) && isBackgroundTaskActivity(payload)) break;546 if (!agents.has(taskId) && isBackgroundTaskActivity(payload)) break;
547 const agent = getOrCreate(agents, taskId, payload, at);547 const agent = getOrCreate(agents, taskId, payload, at);
548 fillMetadata(agent, payload);548 fillMetadata(agent, payload);
549 const detail = asString(payload.detail);
550 if (detail) agent.progress = bounded(detail);
549 // A task first seen via task.updated (start row aged out) has run at551 // A task first seen via task.updated (start row aged out) has run at
550 // least once — zero activations would misreport "run 0" and let a552 // least once — zero activations would misreport "run 0" and let a
551 // later start row treat it as never-started (review finding).553 // later start row treat it as never-started (review finding).