Skip to content

feat(SubAgentTool) Human-in-the-Loop (HITL) Support for SubAgentTool#723

Open
wuji1428 wants to merge 8 commits intoagentscope-ai:mainfrom
wuji1428:feat-subagent-hitl
Open

feat(SubAgentTool) Human-in-the-Loop (HITL) Support for SubAgentTool#723
wuji1428 wants to merge 8 commits intoagentscope-ai:mainfrom
wuji1428:feat-subagent-hitl

Conversation

@wuji1428
Copy link
Contributor

@wuji1428 wuji1428 commented Feb 4, 2026

AgentScope-Java Version

[The version of AgentScope-Java you are working on, e.g. 1.0.8, check your pom.xml dependency version or run mvn dependency:tree | grep agentscope-parent:pom(only mac/linux)]

Background

In complex AI Agent application scenarios, it is common to use one Agent as a tool to be called by another Agent (via SubAgentTool). When a sub-agent encounters an operation requiring manual confirmation (Human-in-the-Loop, HITL) during execution, the current framework cannot correctly propagate this suspension state to the main agent. This leads to a break in the entire execution chain.

Related Issue Ref: #459

Typical Scenario

User -> Main Agent (Task Planning)
    -> SubAgent (Data Analysis)
        -> Tool (Query Database) // Requires manual confirmation

In this scenario, when the database query tool requires user confirmation, the sub-agent is suspended. However, the main agent is unaware of this state, and the user cannot provide the confirmation result to the sub-agent through the main agent.

Goals

Implement full HITL support for SubAgentTool so that:

  1. The suspension state of the sub-agent can be propagated to the main agent and the user.
  2. The user's confirmation result can be correctly routed back to the sub-agent.
  3. The sub-agent can resume and continue execution from the suspended state.
  4. Multi-turn HITL interactions within the sub-agent are supported.
  5. The main agent can resume operation once the SubAgentTool completes its task.
  6. Compatibility with existing Agent implementations is maintained.

Implementation Plan

1. Suspension State Propagation

When an internal tool of the sub-agent requires user confirmation, SubAgentTool will:

  1. Detect the sub-agent's suspension status (e.g., GenerateReason.TOOL_SUSPENDED, REASONING_STOP_REQUESTED, ACTING_STOP_REQUESTED).
  2. Construct a ToolResultBlock containing suspension metadata:
    • METADATA_SUSPENDED: Marks the suspension state.
    • METADATA_SUBAGENT_SESSION_ID: The session_id of the sub-agent.
    • METADATA_GENERATE_REASON: The reason for suspension.
    • Content: Includes the list of ToolUseBlocks pending confirmation within the sub-agent.

2. Passing User Confirmation Results

Design Principles:

  • The user's interaction style with the ReActAgent remains unchanged.
  • The ToolResultBlock provided by the user is intended for the sub-agent's internal tools and should not be added to the main agent's memory.
  • SessionId management is transparent to the user and handled automatically by the framework.

Mechanism:

  1. Automatic SessionId Registration: When the main agent returns a suspended state, ReActAgent.registerSubAgentSessionIfNeeded() automatically registers the (toolId, sessionId) pair into the SubAgentContext.
  2. User Result Submission: The user submits the tool result via SubAgentContext.submitSubAgentResult(toolId, result).
  3. Result Injection: SubAgentHook intercepts the SubAgentTool call during the PreActingEvent phase and injects the pending result into ToolUseBlock.metadata.
  4. Resuming Execution: Upon detecting the injected result, SubAgentTool invokes the resume() method to continue the sub-agent's execution.

3. Sub-Agent Resumption Mechanism

The callAsync() method in SubAgentTool will:

  1. Check if ToolUseBlock.metadata contains PREVIOUS_TOOL_RESULT.
  2. If present, extract the session_id and toolResults, then call the resume() method.
  3. The resume() method loads the sub-agent's state, injects the tool results, and continues execution.

4. Multi-turn Interaction Support

Support for multiple rounds of suspension and resumption is achieved through session mechanisms and state persistence:

  • Each suspension returns the same session_id.
  • Each resumption uses the same session_id to load the state.
  • SubAgentContext implements the StateModule interface to support state persistence.

Component Description

New Components

1. SubAgentHook

Responsibility: Inject pending sub-agent results before tool execution.
Workflow:

  1. Listen for PreActingEvent.
  2. Retrieve pending results from SubAgentContext based on toolId.
  3. Inject the result into the PREVIOUS_TOOL_RESULT key of ToolUseBlock.metadata.
  4. Inject the session_id into ToolUseBlock.input.

2. SubAgentContext

Responsibility: Manage the pending states of sub-agent tool calls.
Core Functions:

  • setSessionId(toolId, sessionId): Register a session_id (must be done before result submission).
  • submitSubAgentResult(toolId, result): Submit tool results.
  • consumePendingResult(toolId): Consume and remove pending results.
  • extractSessionId(result): Static method to extract session_id from ToolResultBlock.
  • isSubAgentResult(result): Static method to determine if a result belongs to a sub-agent.
    State Persistence: Implements StateModule for saveTo() and loadFrom().

3. SubAgentPendingStore

Responsibility: Thread-safe storage for pending states.
Data Structure: Map<String, SubAgentPendingContext> (toolId -> context).
Design: Uses ConcurrentHashMap for thread safety and implements State for serialization.

4. SubAgentPendingContext

Responsibility: Encapsulates the complete pending state for a single sub-agent tool.

Modified Components

1. SubAgentTool

New Features:

  • resume() method: Handles resumption logic.
  • buildSuspendedResult(): Constructs results for suspended states.
  • extractToolResults(): Extracts injected results from metadata.
  • HITL Compatibility Check: Currently restricted to ReActAgent.

2. SubAgentConfig

New Configuration:

  • enableHITL(boolean): Enable/disable HITL support (default: false).

3. ReActAgent

New Features:

  • subAgentContext field: Manages sub-agent HITL states.
  • enableSubAgentHITL(boolean) Builder method: Enables HITL support.
  • registerSubAgentSessionIfNeeded(): Automatically registers sessionId.
  • configureSubAgentHitl(): Automatically configures SubAgentContext and SubAgentHook.
    State Persistence:
  • StatePersistence.subAgentContextManaged(): Controls whether SubAgentContext is persisted.

Agent Scope

HITL support for SubAgentTool is currently limited to ReActAgent:

  • Compatibility is checked during SubAgentTool construction.
  • Using a non-ReActAgent with HITL enabled will throw an IllegalArgumentException.

Compatibility

  • Backward Compatibility: All changes are incremental and do not affect existing APIs.
  • Disabled by Default: HITL is disabled by default (enableHITL = false). Existing code requires no changes.
  • Progressive Adoption: HITL can be enabled specifically on SubAgentTool instances as needed.

Checklist

Please check the following items before code is ready to be reviewed.

  • Code has been formatted with mvn spotless:apply
  • All tests are passing (mvn test)
  • Javadoc comments are complete and follow project conventions
  • Related documentation has been updated (e.g. links, examples, etc.)
  • Code is ready for review

@wuji1428 wuji1428 requested a review from a team February 4, 2026 12:04
@gemini-code-assist
Copy link

Summary of Changes

Hello @wuji1428, 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 significantly enhances the AgentScope-Java framework by introducing comprehensive Human-in-the-Loop (HITL) support for SubAgentTool. This feature addresses the critical need for interactive control over sub-agent execution, particularly when internal tools require manual confirmation. By enabling the propagation of sub-agent suspension states and facilitating the submission of user-provided results, the framework now supports more complex, multi-turn interactive agent workflows. The implementation includes new dedicated components for managing sub-agent contexts and hooks, alongside targeted modifications to core agent and tool classes to seamlessly integrate this interactive capability.

Highlights

  • Human-in-the-Loop (HITL) Support: Introduced comprehensive Human-in-the-Loop (HITL) capabilities for SubAgentTool, allowing sub-agents to pause execution and request user confirmation for internal tool calls. This enables interactive confirmation and resumption of sub-agent execution.
  • State Propagation and Resumption: The suspension state of a sub-agent can now be propagated to the main agent and the user. User confirmation results can be correctly routed back to the sub-agent, allowing it to resume execution from the suspended state, supporting multi-turn HITL interactions.
  • New Core Components: Added SubAgentContext for managing pending sub-agent tool call results, SubAgentHook for injecting results during tool execution, SubAgentPendingContext to encapsulate pending state, and SubAgentPendingStore for thread-safe storage of these states.
  • ReActAgent Integration: The ReActAgent has been updated to manage sub-agent HITL states, including new methods for enabling HITL, submitting results, and automatically registering sub-agent session IDs. State persistence for SubAgentContext is also integrated.
  • Compatibility and Default Behavior: The changes are backward compatible, with HITL functionality disabled by default. It is currently restricted to ReActAgent to ensure proper integration and behavior.
Changelog
  • agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java
    • Added SubAgentContext field and methods for HITL management (isEnableSubAgentHITL, submitSubAgentResult, submitSubAgentResults).
    • Modified buildSuspendedMsg to register SubAgentTool session IDs for transparent handling.
    • Updated saveTo and loadFrom to persist SubAgentContext state.
    • Introduced enableSubAgentHITL builder method and configureSubAgentHitl for automatic setup of HITL components.
  • agentscope-core/src/main/java/io/agentscope/core/state/StatePersistence.java
    • Added subAgentContextManaged parameter to the StatePersistence record and its builder.
    • Updated all(), none(), and memoryOnly() factory methods to reflect the new subAgentContextManaged field.
  • agentscope-core/src/main/java/io/agentscope/core/tool/subagent/SubAgentConfig.java
    • Added enableHITL field and corresponding getter/builder method to control HITL support for SubAgentTool.
  • agentscope-core/src/main/java/io/agentscope/core/tool/subagent/SubAgentContext.java
    • New class: Manages sub-agent tool call results during HITL interactions.
    • Provides APIs for setSessionId, submitSubAgentResult(s), consumePendingResult, extractSessionId, getSubAgentGenerateReason, and isSubAgentResult.
    • Implements StateModule for state persistence.
  • agentscope-core/src/main/java/io/agentscope/core/tool/subagent/SubAgentHook.java
    • New class: A Hook that intercepts PreActingEvent to inject pending sub-agent tool results into ToolUseBlock metadata for resumption.
  • agentscope-core/src/main/java/io/agentscope/core/tool/subagent/SubAgentPendingContext.java
    • New class: An immutable record encapsulating the pending state for a single sub-agent tool (tool ID, session ID, pending results).
  • agentscope-core/src/main/java/io/agentscope/core/tool/subagent/SubAgentPendingStore.java
    • New class: Provides thread-safe storage and management for pending states of multiple sub-agent tools.
    • Enforces a 'sessionId-first' constraint for adding results and implements State for serialization.
  • agentscope-core/src/main/java/io/agentscope/core/tool/subagent/SubAgentTool.java
    • Added HITL compatibility checks during construction, ensuring the parent agent is a ReActAgent and warning if parent HITL is disabled.
    • Modified executeConversation to check for and handle injected results from SubAgentHook for resuming execution.
    • Updated executeWithStreaming and executeWithoutStreaming to accept a list of messages and integrate HITL logic.
    • Introduced buildSuspendedResult to construct ToolResultBlocks for suspended states, including metadata like session ID and generate reason.
    • Modified buildResult to conditionally use buildSuspendedResult when HITL is enabled and the sub-agent is suspended.
  • agentscope-core/src/test/java/io/agentscope/core/e2e/HITLBasicE2ETest.java
    • Added testReactAgentWithSubAgentToolHITL to validate end-to-end HITL functionality for ReActAgent using SubAgentTool.
  • agentscope-core/src/test/java/io/agentscope/core/state/StatePersistenceTest.java
    • Updated constructor calls in tests to accommodate the new subAgentContextManaged parameter.
  • agentscope-core/src/test/java/io/agentscope/core/tool/subagent/SubAgentConfigTest.java
    • Added tests for the enableHITL configuration option and its default value.
  • agentscope-core/src/test/java/io/agentscope/core/tool/subagent/SubAgentContextTest.java
    • New class: Comprehensive unit tests for SubAgentContext covering pending result management, session ID handling, null handling, state consistency, and persistence.
  • agentscope-core/src/test/java/io/agentscope/core/tool/subagent/SubAgentHITLTest.java
    • New class: Integration tests for SubAgent HITL functionality with ReActAgent, covering suspension detection, result submission, multi-turn interactions, and error handling.
  • agentscope-core/src/test/java/io/agentscope/core/tool/subagent/SubAgentHookTest.java
    • New class: Unit tests for SubAgentHook focusing on result injection, null handling, boundary conditions, and state consistency.
  • agentscope-core/src/test/java/io/agentscope/core/tool/subagent/SubAgentPendingStoreTest.java
    • New class: Comprehensive unit tests for SubAgentPendingStore covering session ID and pending result management, sessionId-first constraint, null handling, and store consistency.
  • docs/en/multi-agent/agent-as-tool.md
    • Added a new section 'Human-in-the-Loop (HITL) Support' with detailed explanations and code examples for enabling HITL, handling suspend and resume operations, and a quick reference for related APIs.
  • docs/zh/multi-agent/agent-as-tool.md
    • Added a new section '人机交互支持(HITL)' with detailed explanations and code examples for enabling HITL, handling suspend and resume operations, and a quick reference for related APIs (Chinese translation).
Activity
  • The pull request introduces a significant new feature: Human-in-the-Loop (HITL) support for sub-agents, which required the creation of four new core classes (SubAgentContext, SubAgentHook, SubAgentPendingContext, SubAgentPendingStore).
  • Existing core components like ReActAgent, SubAgentConfig, SubAgentTool, and StatePersistence were modified to integrate the new HITL functionality.
  • Extensive unit and integration tests were added for all new and modified components to ensure the robustness and correctness of the HITL feature.
  • Documentation in both English and Chinese (docs/en/multi-agent/agent-as-tool.md and docs/zh/multi-agent/agent-as-tool.md) was updated to reflect the new HITL capabilities, including usage examples and API references.
  • The author has completed the checklist, confirming code formatting, passing tests, complete Javadoc, and updated documentation, indicating the PR is ready for review.
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

@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 introduces significant new functionality for Human-in-the-Loop (HITL) support in SubAgentTool. The changes are extensive, adding new classes like SubAgentContext, SubAgentHook, and SubAgentPendingStore to manage the state of suspended sub-agents. The implementation correctly propagates suspension states and allows for resumption. Overall, this is a well-designed feature. I've identified a few areas for improvement, including a critical bug related to state loading that could cause a NullPointerException, a race condition in state consumption, and some minor issues in documentation and error messages. Addressing these points will make the implementation more robust and reliable.

Comment on lines 306 to 310
public void loadFrom(Session session, SessionKey sessionKey) {
this.pendingStore = null;
session.get(sessionKey, "subagent_context", SubAgentPendingStore.class)
.ifPresent(state -> this.pendingStore = state);
}

Choose a reason for hiding this comment

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

critical

There is a critical bug here. If the session does not contain the subagent_context state, this.pendingStore will remain null after being explicitly set to null on line 307. Any subsequent call to a method on this SubAgentContext instance will result in a NullPointerException.

You should ensure pendingStore is always initialized, for example by providing a default new instance if one is not found in the session.

    public void loadFrom(Session session, SessionKey sessionKey) {
        this.pendingStore = session.get(sessionKey, "subagent_context", SubAgentPendingStore.class)
                .orElse(new SubAgentPendingStore());
    }

Comment on lines 154 to 165
public Optional<SubAgentPendingContext> consumePendingResult(String toolId) {
if (!pendingStore.contains(toolId)) {
return Optional.empty();
}
SubAgentPendingContext pending =
new SubAgentPendingContext(
toolId,
pendingStore.getSessionId(toolId),
pendingStore.getPendingResults(toolId));
clearToolResult(toolId);
return Optional.of(pending);
}

Choose a reason for hiding this comment

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

high

This implementation of consumePendingResult is not atomic and has a race condition. Between checking for the tool's existence and clearing it, another thread could modify the state. This can lead to inconsistent state or lost updates in a concurrent environment.

To fix this, you should make the consumption atomic. I recommend modifying SubAgentPendingStore.remove() to return the removed SubAgentPendingContext (since ConcurrentHashMap.remove() does this atomically). Then, this method can be simplified to a single, thread-safe call.

    public Optional<SubAgentPendingContext> consumePendingResult(String toolId) {
        if (toolId == null) {
            return Optional.empty();
        }
        return Optional.ofNullable(pendingStore.remove(toolId));
    }

Comment on lines 203 to 207
public void remove(String toolId) {
if (toolId != null) {
toolIdToContext.remove(toolId);
}
}

Choose a reason for hiding this comment

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

high

To support an atomic consume operation and fix the race condition in SubAgentContext, this remove method should return the value that was removed. ConcurrentHashMap.remove() already provides this functionality atomically.

Suggested change
public void remove(String toolId) {
if (toolId != null) {
toolIdToContext.remove(toolId);
}
}
public SubAgentPendingContext remove(String toolId) {
if (toolId != null) {
return toolIdToContext.remove(toolId);
}
return null;
}

Comment on lines 287 to 289
throw new IllegalStateException(
"SubAgent HITL is not enabled. Please enable it via"
+ " builder.enableSubAgentHitl(true)");

Choose a reason for hiding this comment

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

medium

There's a typo in the exception message. The builder method is named enableSubAgentHITL, but the message suggests enableSubAgentHitl. To avoid confusion, it's best to match the method name exactly.

                    "SubAgent HITL is not enabled. Please enable it via"                            + " builder.enableSubAgentHITL(true)");

* to handle sub-agent suspension and resumption. This allows sub-agents to be
* suspended when they need user input and resumed when the user provides results.
*
* @param enableSubAgentHITL true to enable sub-agent HITL support (default: true)

Choose a reason for hiding this comment

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

medium

The Javadoc states that the default value for enableSubAgentHITL is true, but the field is initialized to false on line 1122. The pull request description also mentions that HITL is disabled by default. Please correct the Javadoc to reflect the actual default value.

         * @param enableSubAgentHITL true to enable sub-agent HITL support (default: false)

metadata.put(PREVIOUS_TOOL_RESULT, pendingResult.get());
Map<String, Object> newInput = new HashMap<>(toolUse.getInput());
newInput.put("session_id", pendingContext.get().sessionId());
context.clearToolResult(toolUse.getId());

Choose a reason for hiding this comment

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

medium

The call to context.clearToolResult(toolUse.getId()) is redundant. The consumePendingResult method on line 129 already removes the pending context from the store. Removing this line will make the logic clearer.

Comment on lines 212 to 216
List<ToolResultBlock> cancelResults = pendingTools.stream()
.map(t -> ToolResultBlock.of(t.getId(), t.getName(),
TextBlock.builder().text("Operation cancelled").build()))
.toList();
mainAgent.submitSubAgentResult(resultBlock.getId(), cancelResults);

Choose a reason for hiding this comment

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

medium

There's a typo in the method name. For submitting a list of results, the method is submitSubAgentResults (plural). The example here uses submitSubAgentResult.

Suggested change
List<ToolResultBlock> cancelResults = pendingTools.stream()
.map(t -> ToolResultBlock.of(t.getId(), t.getName(),
TextBlock.builder().text("Operation cancelled").build()))
.toList();
mainAgent.submitSubAgentResult(resultBlock.getId(), cancelResults);
List<ToolResultBlock> cancelResults = pendingTools.stream()
.map(t -> ToolResultBlock.of(t.getId(), t.getName(),
TextBlock.builder().text("Operation cancelled").build()))
.toList();
mainAgent.submitSubAgentResults(resultBlock.getId(), cancelResults);

**Resume methods**:
- `mainAgent.call()` — Continue executing pending tools
- `mainAgent.submitSubAgentResult(String, ToolResultBlock)` — Submit single tool result
- `mainAgent.submitSubAgentResult(String, List<ToolResultBlock>)` — Submit multiple tool results No newline at end of file

Choose a reason for hiding this comment

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

medium

There's a typo in the API reference. The method for submitting multiple results is submitSubAgentResults (plural), not submitSubAgentResult.

Suggested change
- `mainAgent.submitSubAgentResult(String, List<ToolResultBlock>)` — Submit multiple tool results
- `mainAgent.submitSubAgentResults(String, List<ToolResultBlock>)` — Submit multiple tool results

.map(t -> ToolResultBlock.of(t.getId(), t.getName(),
TextBlock.builder().text("操作已取消").build()))
.toList();
mainAgent.submitSubAgentResult(resultBlock.getId(), cancelResults);

Choose a reason for hiding this comment

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

medium

There's a typo in the method name in this example. When submitting a list of results, the method should be submitSubAgentResults (plural).

Suggested change
mainAgent.submitSubAgentResult(resultBlock.getId(), cancelResults);
mainAgent.submitSubAgentResults(resultBlock.getId(), cancelResults);


**恢复方法**:
- `ReActAgent.submitSubAgentResult(String, ToolResultBlock)` — 提交单个工具结果
- `ReActAgent.submitSubAgentResult(String, List<ToolResultBlock>)` — 批量提交工具结果 No newline at end of file

Choose a reason for hiding this comment

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

medium

There's a typo in the API reference. The method for submitting a list of tool results is submitSubAgentResults (plural).

Suggested change
- `ReActAgent.submitSubAgentResult(String, List<ToolResultBlock>)` — 批量提交工具结果
- `ReActAgent.submitSubAgentResults(String, List<ToolResultBlock>)` — 批量提交工具结果

1. The `remove` method in the `SubAgentPendingStore` class now returns the removed object instead of void type.
2. Correspondingly, the places where this method is called in the `SubAgentContext` class have been updated, directly using the returned result for subsequent processing.
3. At the same time, some unnecessary method definitions, such as `clearToolResult`, have been cleaned up.
4. The relevant documentation and test cases have been updated to match the new API changes.
These changes have made the code more concise and easier to maintain.
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