From 25fe1074b094ad862491df792c803c5a18727189 Mon Sep 17 00:00:00 2001 From: svetanis Date: Sat, 8 Aug 2026 12:09:25 -0700 Subject: [PATCH] fix(flows): compare branch paths on segment boundaries, not raw string prefixes --- .../google/adk/flows/llmflows/Contents.java | 6 +- .../adk/flows/llmflows/ContentsTest.java | 92 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java index 1f81bfa3e..17a0b00a8 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java @@ -443,9 +443,13 @@ private static String convertMapToJson(Map struct) { private static boolean isEventBelongsToBranch(@Nullable String invocationBranch, Event event) { @Nullable String eventBranch = event.branch().orElse(null); + // Branches are dot-joined agent names, so a raw prefix match would make "root.agent_10" belong + // to the branch "root.agent_1". Require either an exact match, or a prefix that ends on a + // segment boundary. return Strings.isNullOrEmpty(invocationBranch) || Strings.isNullOrEmpty(eventBranch) - || invocationBranch.startsWith(eventBranch); + || invocationBranch.equals(eventBranch) + || invocationBranch.startsWith(eventBranch + "."); } /** diff --git a/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java b/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java index 5d2c3d5fc..7b8d32c83 100644 --- a/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java +++ b/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java @@ -1008,6 +1008,61 @@ public Stream stream() { var unused = contentsProcessor.processRequest(context, initialRequest).blockingGet(); } + @Test + public void processRequest_siblingBranchSharesNamePrefix_excludesSiblingEvent() { + Event siblingEvent = + createBranchedAgentEvent("agent_1", "e1", "sibling output", "root.agent_1"); + + List result = + runContentsProcessorOnBranch(ImmutableList.of(siblingEvent), "agent_10", "root.agent_10"); + + assertThat(result).isEmpty(); + } + + @Test + public void processRequest_sameBranch_includesEvent() { + Event ownEvent = createBranchedAgentEvent("agent_10", "e1", "own output", "root.agent_10"); + + List result = + runContentsProcessorOnBranch(ImmutableList.of(ownEvent), "agent_10", "root.agent_10"); + + assertThat(result).isEqualTo(eventsToContents(ImmutableList.of(ownEvent))); + } + + @Test + public void processRequest_ancestorBranch_includesEvent() { + Event ancestorEvent = createBranchedAgentEvent("agent_10", "e1", "ancestor output", "root"); + + List result = + runContentsProcessorOnBranch(ImmutableList.of(ancestorEvent), "agent_10", "root.agent_10"); + + assertThat(result).isEqualTo(eventsToContents(ImmutableList.of(ancestorEvent))); + } + + @Test + public void processRequest_eventWithoutBranch_includesEvent() { + Event userEvent = createUserEvent("u1", "user input"); + + List result = + runContentsProcessorOnBranch(ImmutableList.of(userEvent), "agent_10", "root.agent_10"); + + assertThat(result).isEqualTo(eventsToContents(ImmutableList.of(userEvent))); + } + + @Test + public void processRequest_noInvocationBranch_includesBranchedEvent() { + Event siblingEvent = + createBranchedAgentEvent("agent_1", "e1", "sibling output", "root.agent_1"); + + List result = + runContentsProcessorOnBranch(ImmutableList.of(siblingEvent), "agent_10", null); + + assertThat(result) + .containsExactly( + Content.fromParts( + Part.fromText("For context:"), Part.fromText("[agent_1] said: sibling output"))); + } + private static Event createUserEvent(String id, String text) { return Event.builder() .id(id) @@ -1042,6 +1097,18 @@ private static Event createAgentEvent(String agent, String id, String text) { .build(); } + private static Event createBranchedAgentEvent( + String agent, String id, String text, String branch) { + return Event.builder() + .id(id) + .author(agent) + .content( + Content.builder().role("model").parts(ImmutableList.of(Part.fromText(text))).build()) + .invocationId("invocationId") + .branch(branch) + .build(); + } + private static Event createFunctionCallEvent(String id, String toolName, String callId) { return createFunctionCallEvent(AGENT, id, toolName, callId); } @@ -1226,6 +1293,31 @@ private List runContentsProcessorGrouped(List events) { return result.updatedRequest().contents(); } + private List runContentsProcessorOnBranch( + List events, String agentName, String invocationBranch) { + LlmAgent agent = + LlmAgent.builder() + .name(agentName) + .includeContents(LlmAgent.IncludeContents.DEFAULT) + .build(); + Session session = + sessionService.createSession("test-app", "test-user", null, "test-session").blockingGet(); + session.events().addAll(events); + InvocationContext context = + InvocationContext.builder() + .invocationId("test-invocation") + .agent(agent) + .session(session) + .sessionService(sessionService) + .branch(invocationBranch) + .build(); + + LlmRequest initialRequest = LlmRequest.builder().build(); + RequestProcessor.RequestProcessingResult result = + contentsProcessor.processRequest(context, initialRequest).blockingGet(); + return result.updatedRequest().contents(); + } + private List runContentsProcessorWithModel( List events, String modelName, RunConfig runConfig) { LlmAgent agent =