Skip to content

fix(hitl): compose the same pause text in every runtime and always declare the extension - #2472

Open
QuentinBisson wants to merge 6 commits into
kagent-dev:mainfrom
QuentinBisson:fix/hitl-card-advertisement-and-fallback
Open

fix(hitl): compose the same pause text in every runtime and always declare the extension#2472
QuentinBisson wants to merge 6 commits into
kagent-dev:mainfrom
QuentinBisson:fix/hitl-card-advertisement-and-fallback

Conversation

@QuentinBisson

@QuentinBisson QuentinBisson commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

The HITL extension is optional, and the architecture doc states what a client that does
not opt in should still get:

a client that does not opt in can still receive an ordinary input-required task with
human-readable text

Today that is one constant sentence in every runtime, so the tool name, the tool's hint
and the ask_user question are all lost. On the Go path an agent could also serve a card
that never declares the extension, leaving a client no way to discover it.

The pause text

One function per runtime builds the text from the pending tools and the hints they
supplied:

Pause Text
ask_user asking "Which namespace?" Which namespace?
delete_file with a tool hint Deleting this file requires approval (delete_file)
delete_file with no hint Approval is required for tool(s): delete_file
delete_file and restart_pod, one hint Deleting this file requires approval (delete_file, restart_pod)
no confirmation call at all Human input is required before the agent can continue.

Every hint is kept rather than one of them winning: Go used to keep the last, Python the
first, so the same pause read differently per runtime. Tool names always accompany a
hint, so a pause on several tools no longer hides the ones the hint does not mention. The
text is identical whether or not the extension is activated, and an activated payload
repeats it in hint.

Two payload-visible changes: tool_approval_request.hint is now always populated, and a
LangGraph question interrupt emits an ask_user_request instead of an approval for a tool
named ask_user, which the resume side already handled.

Malformed input degrades instead of failing the task. A confirmation part with no usable
tool call, or a LangGraph action_request without a name or an id, is skipped, and a
pause left with no usable request stays text-only.

The card declaration

app.New declares the extension whether or not an ADK agent was supplied for skill
derivation, matching what both Python builders already do in build(). EnrichAgentCard
keeps declaring it, so a caller using that exported function directly is unaffected.

The Go entrypoint replaced Capabilities wholesale to set Streaming, discarding the
extensions the control plane renders into agent-card.json. It now sets the one field.

Not included

kagent-crewai and kagent-openai do not declare the extension. Neither has a pause
path: their executors only emit submitted, working, completed and failed, so
CrewAI's human_input and the Agents SDK's tool interruptions never reach A2A. Wiring
those runtimes up is worth doing separately.

@github-actions github-actions Bot added the bug Something isn't working label Aug 18, 2026

@supreme-gg-gg supreme-gg-gg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skimmed over the changes here and they make sense to me. When this is ready for review, I'll take another look. I think that Go ADK wasn't originally intended to be used as BYO, but it's good to fix the sharp edges

EDIT: It would be helpful if you could rewrite the PR description a bit. I read it at first as “Go agents fail to declare the HITL extension,” which was confusing. What you actually mean is that Go only declares it when cfg.Agent is set for skill derivation, and you want that declaration to happen unconditionally, the same way Python does.

@EItanya

EItanya commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

ADK wasn't originally intended to be used as BYO,

This isn't true, we DEFINITELY want to support go for BYO

@QuentinBisson QuentinBisson changed the title fix(hitl): keep the non-activated fallback lossless and always declare the extension fix(hitl): compose the same pause text in every runtime and always declare the extension Aug 18, 2026
@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Aug 18, 2026
@QuentinBisson
QuentinBisson force-pushed the fix/hitl-card-advertisement-and-fallback branch from 7eadeca to 4983c25 Compare August 18, 2026 23:30
@QuentinBisson
QuentinBisson marked this pull request as ready for review August 18, 2026 23:30
@QuentinBisson
QuentinBisson requested a review from a team as a code owner August 18, 2026 23:30
Copilot AI lite review requested due to automatic review settings August 18, 2026 23:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR standardizes Human-in-the-Loop (HITL) pause/status text composition across the Go ADK runtime and Python runtimes (kagent-adk + kagent-langgraph), and makes Go always declare the optional HITL extension on the served AgentCard so clients can reliably negotiate it.

Changes:

  • Add a shared “pause text” composition rule (hitl_status_text / hitlStatusText) that preserves tool hints and always names tools (and renders ask_user as its question text).
  • Update runtimes to emit identical human-readable text whether or not the HITL extension is activated, and ensure activated payloads carry the same string in hint.
  • Ensure the Go ADK app always declares the HITL extension on the AgentCard and avoid clobbering existing capabilities when enabling streaming.

Reviewed changes

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

Show a summary per file
File Description
python/packages/kagent-langgraph/tests/test_langgraph_executor.py Adds coverage for interrupt handling (activated vs non-activated, ask_user vs approvals).
python/packages/kagent-langgraph/src/kagent/langgraph/_executor.py Uses shared HITL text composition; emits AskUserRequest for ask_user interrupts when activated.
python/packages/kagent-core/tests/test_hitl_utils.py Adds unit tests for the new HITL status text composition behavior.
python/packages/kagent-core/src/kagent/core/a2a/_hitl.py Introduces GENERIC_HITL_TEXT and hitl_status_text() for consistent pause text rendering.
python/packages/kagent-core/src/kagent/core/a2a/init.py Re-exports hitl_status_text.
python/packages/kagent-adk/tests/unittests/test_hitl.py Expands tests to validate consistent status text and activation behavior.
python/packages/kagent-adk/src/kagent/adk/_hitl.py Uses hitl_status_text, preserves all hints, and skips malformed confirmation parts.
go/adk/pkg/app/app.go Ensures HITL extension is declared on the served AgentCard regardless of whether an ADK agent is provided.
go/adk/pkg/app/app_test.go Adds tests validating HITL extension declaration and non-mutation of caller-provided cards.
go/adk/pkg/a2a/hitl.go Implements Go-side pause text composition and ensures payload hint matches the rendered text.
go/adk/pkg/a2a/hitl_test.go Extends tests for consistent text composition and activation behavior.
go/adk/pkg/a2a/agentcard.go Refactors HITL declaration into EnsureHITLExtension.
go/adk/pkg/a2a/agentcard_test.go Adds unit tests for EnsureHITLExtension behavior.
go/adk/cmd/main.go Stops overwriting Capabilities wholesale when setting Streaming.
docs/architecture/human-in-the-loop.md Documents the unified status text composition rules and updates examples.
Suppressed comments (1)

python/packages/kagent-adk/src/kagent/adk/_hitl.py:131

  • _tool_from_confirmation_data currently defaults a missing originalFunctionCall.name to the literal string "tool", which means malformed confirmation parts may no longer be skipped and can surface as an approval for a fake tool name. If the tool name isn't a non-empty string, it should be treated as unusable so the caller can skip the part via the existing ValidationError handling.
    return HitlTool(
        id=str(data.get("id") or ""),
        call_id=str(original.get("id") or data.get("id") or ""),
        name=str(original.get("name") or "tool"),
        args=original.get("args") if isinstance(original.get("args"), dict) else {},

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread python/packages/kagent-langgraph/src/kagent/langgraph/_executor.py Outdated
Comment thread python/packages/kagent-langgraph/src/kagent/langgraph/_executor.py Outdated
@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Aug 18, 2026
A client that does not activate the HITL extension received a single fixed
sentence on the input-required status message, so the tool name, the tool's
hint and the ask_user question were all dropped. The extension is optional
and the architecture doc promises such a client "an ordinary input-required
task with human-readable text", which one constant sentence is not.

Parse the confirmation parts before deciding on activation, and build the
text part from the tool's hint, falling back to the tool names. The text is
the same whether or not the extension is activated; activation adds the
typed payload on top. The ask_user tools already derive their hint from the
questions, so the question survives too.

In Go the tool approval payload's hint no longer defaults to that sentence
when no tool supplied one; it is omitted, as it already was in Python.

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
The Go card declaration lived in EnrichAgentCard, which app.New only calls
when an ADK agent is supplied for skill derivation. An agent built around
its own executor therefore served a card without the extension, leaving a
client no way to discover it and negotiate.

Move the declaration into EnsureHITLExtension and call it unconditionally,
before the card is copied into the server. Skill derivation, the description
backfill and the interface default stay behind the agent check.

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
The non-activated text is built by one function per language, from the tools
and the hints they supplied: an ask_user pause renders its questions, any
other pause renders the hints followed by the tool names, and a pause with no
tool call keeps the generic sentence. An activated payload carries that same
string as its hint, so a client can render either field.

Go kept the last hint and Python the first when several tools supplied one;
both now keep all of them, so a batch pause no longer hides the tools it did
not quote. LangGraph question interrupts become ask_user requests instead of
rendering as an approval for a tool named ask_user.

A confirmation part that carries no usable tool call is skipped rather than
raising, matching what the Go parser already tolerated.

The LangGraph executor test is renamed: sharing a basename with the CrewAI
one made pytest abort collection for the whole suite.

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
EnrichAgentCard declares the HITL extension again, so a caller using it
directly instead of app.New keeps the behaviour it had. app.New assembles the
served card in buildAgentCard, which declares the extension whether or not an
ADK agent was supplied and leaves the caller's card untouched.

The Go entrypoint replaced Capabilities wholesale to set Streaming, dropping
the extensions the controller renders into agent-card.json. It now sets the
one field.

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
An action_request missing a name or an id is skipped, its args are normalized
to an object, and a pause left with no usable request stays text-only rather
than carrying an empty tools list that fails validation.

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
@QuentinBisson
QuentinBisson force-pushed the fix/hitl-card-advertisement-and-fallback branch from 92bc033 to 55feba5 Compare August 18, 2026 23:47
@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants