Skip to content

Conversation

leweii
Copy link

@leweii leweii commented Oct 15, 2025

No description provided.

Copy link
Contributor

Summary of Changes

Hello @leweii, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a comprehensive planning framework for LLM agents in Java, effectively porting a previously existing Python implementation. It establishes a BasePlanner interface and provides two concrete implementations: a BuiltInPlanner for basic cognitive configurations and a PlanReActPlanner for advanced natural language planning and response parsing. The LlmAgent is now capable of utilizing these planners, and the SingleFlow has been updated to seamlessly integrate this planning logic into its request and response processing, thereby significantly enhancing the agent's ability to strategize and execute complex tasks.

Highlights

  • New Planner Interface: Introduced a BasePlanner interface that defines the contract for generating planning instructions and processing LLM responses within a planning workflow.
  • Built-in Planner Implementation: Added a BuiltInPlanner which allows for applying a ThinkingConfig to an LlmRequest, enabling native cognitive processing features for LLMs.
  • Plan-ReAct Planner Implementation: Implemented a PlanReActPlanner that provides detailed natural language instructions for a Plan-ReAct methodology, including specific tags for planning, reasoning, actions, and final answers. It also processes LLM responses to identify and handle function calls and thought annotations.
  • LlmAgent Integration: Modified the LlmAgent to include an optional BasePlanner instance, allowing agents to be configured with a specific planning strategy during their creation.
  • NLPlanning Flow Processors: Created NlPlanningRequestProcessor and NlPlanningResponseProcessor to integrate planning logic into the LLM flow. These processors leverage the BasePlanner to modify requests (e.g., add planning instructions, remove thought annotations) and process responses (e.g., extract function calls, generate state update events).
  • SingleFlow Integration: Updated the SingleFlow to incorporate the new NlPlanningRequestProcessor and NlPlanningResponseProcessor into its request and response processing pipelines, enabling planning capabilities within this flow.
  • Testing Utilities: Enhanced TestUtils to support the creation of LlmAgent instances with a BasePlanner, and added a new test (NlPlanningTest) to verify the behavior of the BuiltInPlanner.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request successfully ports a Python-based Plan-ReAct planner to Java, introducing a BasePlanner interface and integrating it into the LlmAgent and processing flows. The implementation includes the PlanReActPlanner with its specific prompting and response parsing logic, a BuiltInPlanner for models with native cognitive features, and new flow processors (NLPlanning) to handle the planning lifecycle. The changes are well-structured, but I've identified a few areas for improvement regarding code clarity, robustness, and testing. Specifically, some redundant checks can be removed, passing null to a constructor can be avoided, and a new test is flawed and requires correction to properly validate the intended behavior.

Comment on lines +21 to +56
@Test
public void builtInPlanner_contentListUnchanged() {

Content content = Content.fromParts(Part.fromText("LLM response"));
TestLlm testLlm = createTestLlm(createLlmResponse(content));
BuiltInPlanner planner = BuiltInPlanner.buildPlanner(ThinkingConfig.builder().build());
LlmAgent agent = createTestAgent(testLlm, planner);
var invocationContext = com.google.adk.testing.TestUtils.createInvocationContext(agent);

List<Content> contents = List.of(
Content.builder()
.role("user")
.parts(Part.fromText("Hello"))
.build(),
Content.builder()
.role("model")
.parts(
Part.builder().thought(true).text("thinking....").build(),
Part.fromText("Here is my response")
)
.build(),
Content.builder()
.role("user")
.parts(Part.fromText("Follow up"))
.build()
);
LlmRequest llmRequest = LlmRequest.builder().contents(contents).build();
List<Content> originalContents = List.copyOf(llmRequest.contents());

// Act
agent.runAsync(invocationContext).blockingSubscribe();

// Assert
assertThat(llmRequest.contents()).isEqualTo(originalContents);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This test is flawed and does not validate the intended behavior. The LlmRequest object created on line 47 is a local variable that is never passed into the agent's execution flow initiated by agent.runAsync(). The BaseLlmFlow creates its own LlmRequest internally, so the assertion on line 54 will always pass vacuously.

To properly test that the request processing logic does not mutate its input, you should unit test NlPlanningRequestProcessor.processRequest directly. Here is an example of a more effective test:

@Test
public void nlPlanningRequestProcessor_doesNotMutateOriginalRequest() {
    // Arrange
    var processor = new NLPlanning.NlPlanningRequestProcessor();
    BuiltInPlanner planner = BuiltInPlanner.buildPlanner(ThinkingConfig.builder().build());
    LlmAgent agent = createTestAgent(createTestLlm(createLlmResponse(Content.fromText(""))), planner);
    var invocationContext = com.google.adk.testing.TestUtils.createInvocationContext(agent);

    List<Content> contents = List.of(
        Content.builder()
            .role("model")
            .parts(Part.builder().thought(true).text("thinking....").build())
            .build()
    );
    LlmRequest originalRequest = LlmRequest.builder().contents(contents).build();
    List<Content> originalContents = List.copyOf(originalRequest.contents());

    // Act
    RequestProcessor.RequestProcessingResult result = processor.processRequest(invocationContext, originalRequest).blockingGet();

    // Assert
    // Check that the original request object is not mutated.
    assertThat(originalRequest.contents()).isEqualTo(originalContents);
    assertThat(originalRequest.contents().get(0).parts().get().get(0).thought()).isTrue();

    // Check that the new request has the thoughts removed.
    LlmRequest processedRequest = result.updatedRequest();
    assertThat(processedRequest.contents().get(0).parts().get().get(0).thought()).isFalse();
}

Comment on lines +29 to +32
if (!(context.agent() instanceof LlmAgent)) {
throw new IllegalArgumentException(
"Agent in InvocationContext is not an instance of LlmAgent.");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This instanceof check for LlmAgent is redundant. The getPlanner method called on line 34 already performs this check and returns an empty Optional if the agent is not an LlmAgent. The subsequent check if (plannerOpt.isEmpty()) on line 35 is sufficient to handle this case. Removing these lines will simplify the code and avoid duplication, as the same pattern is used in NlPlanningResponseProcessor.

Comment on lines +67 to +70
if (!(context.agent() instanceof LlmAgent)) {
throw new IllegalArgumentException(
"Agent in InvocationContext is not an instance of LlmAgent.");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the NlPlanningRequestProcessor, this instanceof check is redundant. The getPlanner method on line 79 handles the case where the agent is not an LlmAgent by returning an empty Optional, which is correctly handled by the check on line 80. You can remove these lines to improve code clarity and reduce duplication.

LlmResponse.Builder responseBuilder = llmResponse.toBuilder();

// Process the planning response
CallbackContext callbackContext = new CallbackContext(context, null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Passing null for the eventActions parameter in the CallbackContext constructor is not ideal, even though the constructor currently handles it. It's better to be explicit to make the code more robust and readable, especially if the constructor's null-handling logic were to change in the future. Please pass an empty EventActions object instead.

Suggested change
CallbackContext callbackContext = new CallbackContext(context, null);
CallbackContext callbackContext = new CallbackContext(context, EventActions.builder().build());

public class BuiltInPlanner implements BasePlanner {
private ThinkingConfig cognitiveConfig;

private BuiltInPlanner() {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This private no-argument constructor appears to be unused. The only way to create an instance of BuiltInPlanner is through the static factory method buildPlanner, which uses the other private constructor. You can remove this unused constructor to simplify the class.

@leweii leweii force-pushed the feat/planner-support-oct branch from 750d6e5 to 1e0bed3 Compare October 17, 2025 02:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant