Skip to content

Commit 0bb1da0

Browse files
authored
Merge branch 'main' into fix/1618-cancel-propagation
2 parents a63b9cd + 9f5d5ce commit 0bb1da0

2 files changed

Lines changed: 114 additions & 2 deletions

File tree

‎core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,7 @@ private Flowable<Event> followTransfer(
806806
return self.concatWith(
807807
Flowable.error(new IllegalStateException("Agent not found: " + agentToTransfer)));
808808
}
809-
BaseAgent target = nextAgent.get();
809+
BaseAgent target;
810810
// Resumable only: restricting the default flow too would move behavior for existing callers.
811811
if (context.isResumable() && context.agent() instanceof LlmAgent llmAgent) {
812812
Optional<BaseAgent> declared = declaredTransferTarget(llmAgent, agentToTransfer);
@@ -819,8 +819,16 @@ private Flowable<Event> followTransfer(
819819
+ " is not allowed to transfer to the requested agent.")));
820820
}
821821
target = declared.get();
822+
} else {
823+
target = nextAgent.get();
822824
}
823-
return self.concatWith(target.runAsync(context).compose(Tracing.withContext(spanContext)));
825+
// Otherwise the target can build its request from a session missing the transfer event.
826+
return self.concatWith(
827+
PersistBarrier.awaitPersisted(context, ImmutableList.of(event))
828+
.andThen(
829+
// Not around the barrier: andThen subscribes the target on the append's thread.
830+
Flowable.defer(() -> target.runAsync(context))
831+
.compose(Tracing.withContext(spanContext))));
824832
}
825833

826834
/**

‎core/src/test/java/com/google/adk/runner/RunnerTest.java‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
import com.google.common.collect.ImmutableList;
8888
import com.google.common.collect.ImmutableMap;
8989
import com.google.common.collect.Iterables;
90+
import com.google.common.truth.Correspondence;
9091
import com.google.genai.types.Content;
9192
import com.google.genai.types.FunctionCall;
9293
import com.google.genai.types.FunctionDeclaration;
@@ -1101,6 +1102,109 @@ public void runAsync_slowAppendEvent_doesNotCauseStaleSessionInNextStep() throws
11011102
assertThat(foundToolResponse).isTrue();
11021103
}
11031104

1105+
/**
1106+
* Ensures a slow appendEvent does not let a transfer target build its request from a stale
1107+
* session, in either direction: the sub-agent must see the root's transfer, and after the
1108+
* transfer back the root must see its own transfer response.
1109+
*/
1110+
@Test
1111+
public void runAsync_slowAppendEvent_doesNotCauseStaleSessionInTransferTarget() throws Exception {
1112+
TestLlm subTestLlm =
1113+
createTestLlm(
1114+
createFunctionCallLlmResponse(
1115+
"call_sub", "transfer_to_agent", ImmutableMap.of("agent_name", "test agent")));
1116+
LlmAgent subAgent = createTestAgentBuilder(subTestLlm).name("sub_agent").build();
1117+
1118+
TestLlm rootTestLlm =
1119+
createTestLlm(
1120+
createFunctionCallLlmResponse(
1121+
"call_root", "transfer_to_agent", ImmutableMap.of("agent_name", "sub_agent")),
1122+
createTextLlmResponse("done"));
1123+
1124+
LlmAgent rootAgent = createTestAgentBuilder(rootTestLlm).subAgents(subAgent).build();
1125+
1126+
Runner runnerForRace =
1127+
Runner.builder()
1128+
.app(App.builder().name("test").rootAgent(rootAgent).build())
1129+
.sessionService(new AppendDelayingSessionService(new InMemorySessionService(), 50))
1130+
.build();
1131+
Session raceSession =
1132+
runnerForRace.sessionService().createSession("test", "user").blockingGet();
1133+
1134+
var unused =
1135+
runnerForRace
1136+
.runAsync("user", raceSession.id(), createContent("start"))
1137+
.toList()
1138+
.blockingGet();
1139+
1140+
// The root's transfer reaches the sub-agent as relayed text, since another agent authored it.
1141+
ImmutableList<LlmRequest> subRequests = subTestLlm.getRequests();
1142+
assertThat(subRequests).hasSize(1);
1143+
assertThat(
1144+
subRequests.get(0).contents().stream()
1145+
.flatMap(c -> c.parts().stream().flatMap(List::stream))
1146+
.flatMap(part -> part.text().stream())
1147+
.collect(toImmutableList()))
1148+
.comparingElementsUsing(
1149+
Correspondence.<String, String>from(String::startsWith, "starts with"))
1150+
.contains("[test agent] `transfer_to_agent` tool returned result:");
1151+
1152+
// After the transfer back, the root must see its own transfer response.
1153+
ImmutableList<LlmRequest> rootRequests = rootTestLlm.getRequests();
1154+
assertThat(rootRequests).hasSize(2);
1155+
assertThat(
1156+
rootRequests.get(1).contents().stream()
1157+
.flatMap(c -> c.parts().stream().flatMap(List::stream))
1158+
.flatMap(part -> part.functionResponse().flatMap(FunctionResponse::id).stream())
1159+
.collect(toImmutableList()))
1160+
.contains("call_root");
1161+
assertThat(Iterables.getLast(rootRequests.get(1).contents()).role()).hasValue("user");
1162+
}
1163+
1164+
/**
1165+
* With a slow appendEvent, the append's thread releases the transfer target; the target's {@code
1166+
* invoke_agent} span must still be a child of the transferring agent's, not start a new trace.
1167+
*/
1168+
@Test
1169+
public void runAsync_slowAppendEvent_transferTargetSpanIsChildOfParentAgentSpan() {
1170+
LlmAgent subAgent =
1171+
createTestAgentBuilder(createTestLlm(createTextLlmResponse("sub response")))
1172+
.name("sub_agent")
1173+
.build();
1174+
LlmAgent rootAgent =
1175+
createTestAgentBuilder(
1176+
createTestLlm(
1177+
createFunctionCallLlmResponse(
1178+
"call_root",
1179+
"transfer_to_agent",
1180+
ImmutableMap.of("agent_name", "sub_agent"))))
1181+
.subAgents(subAgent)
1182+
.build();
1183+
Runner runnerForRace =
1184+
Runner.builder()
1185+
.app(App.builder().name("test").rootAgent(rootAgent).build())
1186+
.sessionService(new AppendDelayingSessionService(new InMemorySessionService(), 50))
1187+
.build();
1188+
Session raceSession =
1189+
runnerForRace.sessionService().createSession("test", "user").blockingGet();
1190+
1191+
var unused =
1192+
runnerForRace
1193+
.runAsync("user", raceSession.id(), createContent("start"))
1194+
.toList()
1195+
.blockingGet();
1196+
1197+
List<SpanData> spans = openTelemetryRule.getSpans();
1198+
Optional<SpanData> parentSpan =
1199+
spans.stream().filter(s -> s.getName().equals("invoke_agent test agent")).findFirst();
1200+
Optional<SpanData> targetSpan =
1201+
spans.stream().filter(s -> s.getName().equals("invoke_agent sub_agent")).findFirst();
1202+
assertThat(parentSpan).isPresent();
1203+
assertThat(targetSpan).isPresent();
1204+
assertThat(targetSpan.get().getParentSpanContext().getSpanId())
1205+
.isEqualTo(parentSpan.get().getSpanContext().getSpanId());
1206+
}
1207+
11041208
/**
11051209
* When an LlmAgent transfers to a sub-LlmAgent, the sub-agent's events flow back up through the
11061210
* parent's flow and must each be appended to the session exactly once.

0 commit comments

Comments
 (0)