|
27 | 27 | import com.google.adk.agents.InvocationContext; |
28 | 28 | import com.google.adk.agents.LiveRequestQueue; |
29 | 29 | import com.google.adk.agents.LlmAgent; |
30 | | -import com.google.adk.agents.ParallelAgent; |
31 | 30 | import com.google.adk.agents.Role; |
32 | 31 | import com.google.adk.agents.RunConfig; |
33 | 32 | import com.google.adk.agents.SequentialAgent; |
@@ -416,13 +415,14 @@ private Single<Event> createUserMessageEvent( |
416 | 415 | InvocationContext invocationContext, |
417 | 416 | boolean saveInputBlobsAsArtifacts, |
418 | 417 | @Nullable Map<String, Object> stateDelta) { |
| 418 | + // As on the resumable path, a function response takes the branch of the call it answers. |
419 | 419 | return appendNewMessageToSession( |
420 | 420 | session, |
421 | 421 | newMessage, |
422 | 422 | invocationContext, |
423 | 423 | saveInputBlobsAsArtifacts, |
424 | 424 | stateDelta, |
425 | | - /* branch= */ null); |
| 425 | + matchingFunctionCallEvent(session, newMessage).flatMap(Event::branch).orElse(null)); |
426 | 426 | } |
427 | 427 |
|
428 | 428 | private Single<Event> appendNewMessageToSession( |
@@ -768,12 +768,14 @@ private Flowable<Event> runAgentForUserEvent( |
768 | 768 | */ |
769 | 769 | private Flowable<Event> runAgentWithUpdatedSession( |
770 | 770 | InvocationContext initialContext, Session updatedSession, Event event, BaseAgent rootAgent) { |
| 771 | + BaseAgent agentToRun = this.findAgentToRun(updatedSession, rootAgent); |
771 | 772 | // Create context with updated session for beforeRunCallback |
772 | 773 | InvocationContext contextWithUpdatedSession = |
773 | 774 | initialContext.toBuilder() |
774 | 775 | .session(updatedSession) |
775 | | - .agent(this.findAgentToRun(updatedSession, rootAgent)) |
| 776 | + .agent(agentToRun) |
776 | 777 | .userContent(event.content().orElseGet(Content::fromParts)) |
| 778 | + .branch(routedAgentParentBranch(updatedSession, agentToRun, rootAgent)) |
777 | 779 | .build(); |
778 | 780 |
|
779 | 781 | // If beforeRunCallback returns content, emit it and skip agent. |
@@ -1055,26 +1057,79 @@ private Flowable<Event> runResumedAgent( |
1055 | 1057 | } |
1056 | 1058 |
|
1057 | 1059 | /** |
1058 | | - * Branch to seed a resumed context with so {@code resumeAgent} runs under the same branch it |
1059 | | - * originally did. Returns the parent branch (the resolved agent's most recent event branch minus |
1060 | | - * its own trailing name segment, which {@link BaseAgent#runAsync} re-appends), or {@code null} |
1061 | | - * for the root branch. Non-null only for an agent nested under a {@link ParallelAgent}. |
| 1060 | + * Branch to seed a new invocation with so a routed sub-agent runs where it ran before, or {@code |
| 1061 | + * null} when {@code agentToRun} is the root. A function response restores the branch of the call |
| 1062 | + * it answers, since a Java agent's branch depends on the transfer path that reached it; other |
| 1063 | + * routing restores the agent's latest branch, as Python does. |
| 1064 | + */ |
| 1065 | + private static @Nullable String routedAgentParentBranch( |
| 1066 | + Session session, BaseAgent agentToRun, BaseAgent rootAgent) { |
| 1067 | + if (agentToRun.equals(rootAgent)) { |
| 1068 | + return null; |
| 1069 | + } |
| 1070 | + Optional<Event> answeredCall = |
| 1071 | + Functions.findMatchingFunctionCallEvent(session.immutableEvents()); |
| 1072 | + if (answeredCall.isPresent()) { |
| 1073 | + String ownSuffix = sequentialBranchSuffix(agentToRun, answeredCall.get().author()); |
| 1074 | + if (ownSuffix != null) { |
| 1075 | + return parentBranch(answeredCall.get().branch().orElse(null), ownSuffix); |
| 1076 | + } |
| 1077 | + } |
| 1078 | + return resumeParentBranch(session, /* invocationId= */ null, agentToRun); |
| 1079 | + } |
| 1080 | + |
| 1081 | + /** |
| 1082 | + * Branch to seed a context with so {@code resumeAgent} runs under the branch it last ran on: the |
| 1083 | + * parent branch of the newest event by {@code resumeAgent}, or by an agent it reaches through |
| 1084 | + * SequentialAgents, with a non-empty branch. A non-null {@code invocationId} limits the search to |
| 1085 | + * that invocation; {@code null} is returned for the root branch. |
1062 | 1086 | */ |
1063 | 1087 | private static @Nullable String resumeParentBranch( |
1064 | | - Session session, String invocationId, BaseAgent resumeAgent) { |
| 1088 | + Session session, @Nullable String invocationId, BaseAgent resumeAgent) { |
1065 | 1089 | ImmutableList<Event> events = session.immutableEvents(); |
1066 | 1090 | for (int i = events.size() - 1; i >= 0; i--) { |
1067 | 1091 | Event event = events.get(i); |
1068 | | - if (invocationId.equals(event.invocationId()) |
1069 | | - && resumeAgent.name().equals(event.author()) |
1070 | | - && event.branch().isPresent()) { |
1071 | | - String branch = event.branch().get(); |
1072 | | - String ownSegment = "." + resumeAgent.name(); |
1073 | | - if (branch.endsWith(ownSegment)) { |
1074 | | - String parent = branch.substring(0, branch.length() - ownSegment.length()); |
1075 | | - return parent.isEmpty() ? null : parent; |
| 1092 | + if ((invocationId == null || invocationId.equals(event.invocationId())) |
| 1093 | + && event.branch().filter(branch -> !branch.isEmpty()).isPresent()) { |
| 1094 | + String ownSuffix = sequentialBranchSuffix(resumeAgent, event.author()); |
| 1095 | + if (ownSuffix != null) { |
| 1096 | + return parentBranch(event.branch().get(), ownSuffix); |
| 1097 | + } |
| 1098 | + } |
| 1099 | + } |
| 1100 | + return null; |
| 1101 | + } |
| 1102 | + |
| 1103 | + /** |
| 1104 | + * Returns {@code branch} minus the trailing {@code ownSuffix} that {@link BaseAgent#runAsync} |
| 1105 | + * re-appends, or {@code null} for the root branch. |
| 1106 | + */ |
| 1107 | + private static @Nullable String parentBranch(@Nullable String branch, String ownSuffix) { |
| 1108 | + if (branch == null || branch.isEmpty() || branch.equals(ownSuffix)) { |
| 1109 | + return null; |
| 1110 | + } |
| 1111 | + if (branch.endsWith("." + ownSuffix)) { |
| 1112 | + String parent = branch.substring(0, branch.length() - ownSuffix.length() - 1); |
| 1113 | + return parent.isEmpty() ? null : parent; |
| 1114 | + } |
| 1115 | + return branch; |
| 1116 | + } |
| 1117 | + |
| 1118 | + /** |
| 1119 | + * Returns the dot-joined names from {@code agent} down to the agent named {@code author} when |
| 1120 | + * that agent is {@code agent} or below it through SequentialAgents, else {@code null}. The legacy |
| 1121 | + * flow resumes such an ancestor, which records no events of its own. |
| 1122 | + */ |
| 1123 | + private static @Nullable String sequentialBranchSuffix(BaseAgent agent, @Nullable String author) { |
| 1124 | + if (agent.name().equals(author)) { |
| 1125 | + return agent.name(); |
| 1126 | + } |
| 1127 | + if (agent instanceof SequentialAgent) { |
| 1128 | + for (BaseAgent subAgent : agent.subAgents()) { |
| 1129 | + String suffix = sequentialBranchSuffix(subAgent, author); |
| 1130 | + if (suffix != null) { |
| 1131 | + return agent.name() + "." + suffix; |
1076 | 1132 | } |
1077 | | - return branch.equals(resumeAgent.name()) ? null : branch; |
1078 | 1133 | } |
1079 | 1134 | } |
1080 | 1135 | return null; |
@@ -1241,11 +1296,13 @@ private InvocationContext newInvocationContextForLive( |
1241 | 1296 | runConfigBuilder.inputAudioTranscription(AudioTranscriptionConfig.builder().build()); |
1242 | 1297 | } |
1243 | 1298 | } |
| 1299 | + BaseAgent agentToRun = findAgentToRun(session, this.agent); |
1244 | 1300 | InvocationContext.Builder builder = |
1245 | | - newInvocationContextBuilder(session, findAgentToRun(session, this.agent)) |
| 1301 | + newInvocationContextBuilder(session, agentToRun) |
1246 | 1302 | .runConfig(runConfigBuilder.build()) |
1247 | 1303 | .userContent(Content.fromParts()) |
1248 | | - .liveRequestQueue(liveRequestQueue); |
| 1304 | + .liveRequestQueue(liveRequestQueue) |
| 1305 | + .branch(routedAgentParentBranch(session, agentToRun, this.agent)); |
1249 | 1306 |
|
1250 | 1307 | return builder.build(); |
1251 | 1308 | } |
|
0 commit comments