feat: human-in-the-loop (subagents)#1491
feat: human-in-the-loop (subagents)#1491supreme-gg-gg wants to merge 5 commits intokagent-dev:mainfrom
Conversation
There was a problem hiding this comment.
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
KAgentRemoteA2AToolto 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.
|
The following cases has been tested:
output.mp4 |
a701f42 to
79b9cb1
Compare
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>
ae10ea4 to
784d114
Compare
…#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>
jsonmp-k8
left a comment
There was a problem hiding this comment.
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\brequires a word boundary after thet, 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.tsmissing trailing newline_extract_text_from_taskis module-level while_extract_text_from_messageis@staticmethod— minor inconsistencybuildApprovalMessagespecial-caseshitlParts.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 |
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
requireApprovaltool, 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:
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.