Skip to content

feat: human-in-the-loop (subagents)#1491

Open
supreme-gg-gg wants to merge 5 commits intokagent-dev:mainfrom
supreme-gg-gg:fix/hitl-subagents
Open

feat: human-in-the-loop (subagents)#1491
supreme-gg-gg wants to merge 5 commits intokagent-dev:mainfrom
supreme-gg-gg:fix/hitl-subagents

Conversation

@supreme-gg-gg
Copy link
Contributor

@supreme-gg-gg supreme-gg-gg commented Mar 12, 2026

This PR implements the last part of HITL system in Kagent -- subagent HITL flow. When the parent agent calls one or more subagents and any of them invokes a requireApproval tool, HITL request will be cascaded up to the user (regardless of call stack depth).

In addition, it contains 1. test coverage for previous HITL utils 2. consistent types in python and UI for HITL 3. large enhancement to documentation of HITL implementation in Kagent 4. various HITL cleanups

At a high level, this is what the user expects:

mermaid-diagram-2026-03-11-223608

For more information of HITL system in Kagent, including full sequence diagrams of how these flows are carried out, view docs/architecture/human-in-the-loop.md.

Copilot AI review requested due to automatic review settings March 12, 2026 04:51
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements the remaining Human-in-the-Loop (HITL) subagent flow by propagating subagent input_required states up to the parent agent’s native ADK confirmation flow, while aligning UI/Python HITL wire types, adding unit tests, and expanding HITL architecture documentation.

Changes:

  • Add a new KAgentRemoteA2ATool to propagate subagent HITL prompts and forward decisions back to the subagent task.
  • Extend UI HITL parsing/display to support nested (subagent) approvals via toolConfirmation.payload.hitl_parts, and introduce shared HITL TS types.
  • Add/expand Python HITL utility models + extraction helpers and significantly increase HITL utils test coverage; update HITL docs.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
ui/src/types/index.ts Adds TS HITL wire types for approval requests/decisions and subagent payloads.
ui/src/lib/messageHandlers.ts Builds approval messages from adk_request_confirmation, including nested subagent tool details and decision resolution.
ui/src/components/chat/ToolCallDisplay.tsx Renders nested approval tool cards and supports per-tool (batch) approvalDecision maps.
ui/src/components/chat/ChatInterface.tsx Uses ToolDecision types and stamps decisions as either uniform or per-tool maps for batch flows.
ui/src/components/ToolDisplay.tsx Displays subagent attribution (“via subagent”) on tool cards.
python/packages/kagent-core/src/kagent/core/a2a/_hitl_utils.py Introduces Pydantic HITL models and adds extract_hitl_info_from_task for subagent HITL extraction.
python/packages/kagent-core/src/kagent/core/a2a/init.py Re-exports HITL types/utilities from _hitl_utils.
python/packages/kagent-core/tests/test_hitl_utils.py Adds comprehensive tests for HITL extraction utilities and new models.
python/packages/kagent-core/src/kagent/core/tracing/_utils.py Minor formatting change.
python/packages/kagent-adk/src/kagent/adk/_remote_a2a_tool.py New remote A2A tool implementing two-phase pause/resume with HITL propagation.
python/packages/kagent-adk/src/kagent/adk/types.py Switches remote agent wiring from AgentTool(RemoteA2aAgent) to KAgentRemoteA2ATool.
python/packages/kagent-adk/src/kagent/adk/_agent_executor.py Preserves/merges toolConfirmation.payload on resume to support subagent HITL forwarding (including batch decisions).
python/packages/kagent-adk/tests/unittests/test_proxy_integration.py Updates proxy tests to target KAgentRemoteA2ATool.
python/packages/kagent-adk/tests/unittests/test_hitl.py Updates pending confirmation extraction expectations to include original payload.
docs/architecture/human-in-the-loop.md Reworks HITL architecture doc with direct + nested flows, protocol reference, and payload preservation details.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

@supreme-gg-gg
Copy link
Contributor Author

supreme-gg-gg commented Mar 12, 2026

The following cases has been tested:

  1. Parent agent -> subagent -> one tool / multiple tools / different decision between tools
  2. Parent agent -> multiple subagents -> one / multiple tools
  3. Parent agent -> any subagent -> any more subagents -> ad infinitum -> any tools
  4. Parent agent -> any tools (existing behaviour)
output.mp4

@supreme-gg-gg supreme-gg-gg linked an issue Mar 12, 2026 that may be closed by this pull request
6 tasks
@supreme-gg-gg supreme-gg-gg marked this pull request as draft March 12, 2026 16:46
@supreme-gg-gg supreme-gg-gg marked this pull request as ready for review March 12, 2026 18:39
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
EItanya pushed a commit that referenced this pull request Mar 13, 2026
…#1500)

Fixes a session event duplication bug in
`KAgentSessionService.get_session` where every event loaded from the
database was appended twice to `session.events`. `get_session`
constructed the `Session` with `events=events` (pre-populating the
list), then immediately called `super().append_event()` for each event.
`BaseSessionService.append_event` unconditionally calls
`session.events.append(event)`, so every event ended up in the list
twice.

The bug was latent for normal usage because the LLM tolerates redundant
history in simple Q&A flows. It was discovered while testing #1491
locally, where the duplicated context caused incorrect agent behaviour.
However, it does cause context bloat since, ultimately, every
conversation turn currently sends 2× the necessary history to the model.
😬

---------

Signed-off-by: Brian Fox <878612+onematchfox@users.noreply.github.com>
Copy link
Contributor

@jsonmp-k8 jsonmp-k8 left a comment

Choose a reason for hiding this comment

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

.

Copy link
Contributor

@jsonmp-k8 jsonmp-k8 left a comment

Choose a reason for hiding this comment

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

Review: Subagent HITL flow

Well-structured change overall. The two-phase pause/resume design on KAgentRemoteA2ATool is clean, payload preservation through _build_confirmation_payload solves the state-routing problem neatly, and the docs rewrite with mermaid diagrams is excellent. Issues below, roughly in order of severity.

1. Rejecting HITL approvals from the UI breaks Go-backed agents

The frontend now sends decision_type: "reject" (DataPart) and display text "Rejected" (TextPart) in ChatInterface.tsx:480. The Go HITL parser in hitl.go has two resolution paths:

  • Priority 1 (structured DataPart): Only checks for "deny" and "approve" (hitl.go:31). "reject" is not recognized — falls through.
  • Priority 2 (text fallback): The deny keyword list (hitl.go:36) includes "reject" but uses word-boundary regex \breject\b. The UI sends "Rejected" as the TextPart, which does not match because \breject\b requires a word boundary after the t, and "ed" continues the word.

Result: a reject click from the new UI is not recognized by the Go runtime. The approval flow won't resume, leaving the task stuck at input_required. This is a cross-language protocol break.

Fix: Either update hitl.go to accept "reject" in the structured path (and add "rejected" to the deny keywords), or keep the UI sending "deny" on the wire and only rename it cosmetically in the frontend.

2. Cross-turn remote session continuity is broken

types.py:369 replaces AgentTool(RemoteA2aAgent(...)) with KAgentRemoteA2ATool, which tracks remote agent context in the instance field _last_context_id (_remote_a2a_tool.py:105). But kagent creates a fresh Runner per request (_agent_executor.py:128, docstring explicitly states this) and a fresh root agent inside that runner (_a2a.py:112 calls self.root_agent_factory()). So KAgentRemoteA2ATool instances are recreated on every top-level user turn, and _last_context_id resets to None.

Stateful remote A2A agents will start a new conversation each turn instead of continuing the existing one. The old RemoteA2aAgent likely had the same limitation, but it's worth calling out since this is a rewrite.

Fix: Persist the remote agent's context_id in the ADK session state (via tool_context.state or the session service) rather than on the tool instance. Or document that stateful remote agents are not supported across turns.

3. KAgentRemoteA2ATool has zero test coverage

387-line file with two-phase async logic, HITL propagation, error handling, lazy client initialization, and resource management — no tests. The proxy integration tests were updated to find the tool but none exercise run_async, _handle_first_call, _handle_resume, or _handle_input_required.

At minimum: phase 1 happy path, phase 1 input_required with HITL propagation, phase 2 uniform/batch decisions, phase 2 sequential HITL (subagent returns another input_required on resume), error paths.

4. Resource leak — close() is never called

KAgentRemoteA2ATool.close() exists but types.py:to_agent() creates instances with no lifecycle management. When _owns_httpx_client is True, the httpx client is never closed. Over many agent runs this leaks connections.

5. Breaking change: "deny" → "reject" with no backward compatibility

KAGENT_HITL_DECISION_TYPE_DENY is deleted, DecisionType drops "deny", and extract_decision_from_data_part no longer recognizes it. Persisted tasks with decision_type: "deny" in their message history will have their decisions silently ignored — extract_decision_from_message returns None, and the ADK executor's _process_hitl_decision bails early. Keep "deny" as an accepted alias for one release if in-flight tasks are possible.

6. _build_confirmation_payload — shallow merge can silently overwrite

extra keys overwrite original_payload keys. Currently safe (disjoint key sets), but fragile. A warning on overlap would catch future regressions.

7. Minor

  • types/index.ts missing trailing newline
  • _extract_text_from_task is module-level while _extract_text_from_message is @staticmethod — minor inconsistency
  • buildApprovalMessage special-cases hitlParts.length === 1 && name === "ask_user" but a subagent with ask_user plus other HITL tools in the same batch loses the ask_user UX silently

Summary

Issue Severity
UI sends "reject" but Go parser only accepts "deny" — breaks Go agents High
_last_context_id resets per request — breaks stateful remote agents High
No tests for KAgentRemoteA2ATool (387 lines) High
httpx client resource leak Medium
"deny" → "reject" backward compat for persisted tasks Medium
_build_confirmation_payload silent key overwrite Low
Missing newline, minor inconsistencies Nit

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.

[BUG] Subagents get stuck when using human in the loop

3 participants