diff --git a/go/adk/pkg/a2a/hitl.go b/go/adk/pkg/a2a/hitl.go index bcd46ab8f..4f96a184a 100644 --- a/go/adk/pkg/a2a/hitl.go +++ b/go/adk/pkg/a2a/hitl.go @@ -361,10 +361,28 @@ func VisibleTools(approval *ToolApprovalRequest, ask *AskUserRequest) []HitlTool return nil } +// askUserQuestionText joins the question text from a typed Questions field, or "" if none carry text. +func askUserQuestionText(questions []map[string]any) string { + texts := make([]string, 0, len(questions)) + for _, q := range questions { + if text, ok := q["question"].(string); ok && text != "" { + texts = append(texts, text) + } + } + return strings.Join(texts, " ") +} + func RemoteHitlHint(state *RemoteHitlState) string { if state == nil { return "Remote agent requires human input before continuing." } + // Read Questions directly: VisibleTools()'s nested Args round-trip through + // JSON to []any, silently losing the question text in the nested case. + if state.AskUserRequest != nil { + if q := askUserQuestionText(state.AskUserRequest.Questions); q != "" { + return fmt.Sprintf("Remote agent '%s' asks: %s", state.SubagentName, q) + } + } tools := VisibleTools(state.ToolApprovalRequest, state.AskUserRequest) names := make([]string, 0, len(tools)) for _, tool := range tools { diff --git a/go/adk/pkg/a2a/hitl_test.go b/go/adk/pkg/a2a/hitl_test.go index 81ee6bd41..c82e50756 100644 --- a/go/adk/pkg/a2a/hitl_test.go +++ b/go/adk/pkg/a2a/hitl_test.go @@ -260,3 +260,61 @@ func TestBuildRemoteHitlStateAndHint(t *testing.T) { t.Fatalf("hint = %q", got) } } + +// A sub-agent's ask_user pause should surface the question, not just the tool name. +func TestBuildRemoteHitlStateAndHintAskUser(t *testing.T) { + task := &a2atype.Task{ + ID: "child-task", ContextID: "child-context", + Status: a2atype.TaskStatus{ + Message: AttachHitlExtension(a2atype.NewMessage(a2atype.MessageRoleAgent, a2atype.NewTextPart("pause")), &AskUserRequest{ + Type: HITLTypeAskUserRequest, + ID: "confirm-1", + Questions: []map[string]any{ + {"question": "What is the GitHub owner/org for the repo?"}, + }, + }), + }, + } + state := BuildRemoteHitlState(task, "github_agent") + if state == nil || state.AskUserRequest == nil { + t.Fatalf("state = %#v", state) + } + want := "Remote agent 'github_agent' asks: What is the GitHub owner/org for the repo?" + if got := RemoteHitlHint(state); got != want { + t.Fatalf("hint = %q, want %q", got, want) + } +} + +// A two-level nested ask_user pause should also surface the question, since +// the nested HitlTool's Args round-trip through JSON and lose their type. +func TestBuildRemoteHitlStateAndHintAskUserNested(t *testing.T) { + question := "What is the GitHub owner/org for the repo?" + task := &a2atype.Task{ + ID: "child-task", ContextID: "child-context", + Status: a2atype.TaskStatus{ + Message: AttachHitlExtension(a2atype.NewMessage(a2atype.MessageRoleAgent, a2atype.NewTextPart("pause")), &AskUserRequest{ + Type: HITLTypeAskUserRequest, + ID: "confirm-1", + Questions: []map[string]any{{"question": question}}, + Nested: &NestedHitlRequest{ + TaskID: "grandchild-task", ContextID: "grandchild-context", SubagentName: "grandchild_agent", + Tools: []HitlTool{{ + ID: "confirm-2", CallID: "confirm-2", Name: "ask_user", + Args: map[string]any{"questions": []map[string]any{{"question": question}}}, + }}, + }, + }), + }, + } + state := BuildRemoteHitlState(task, "github_agent") + if state == nil || state.AskUserRequest == nil || state.AskUserRequest.Nested == nil { + t.Fatalf("state = %#v", state) + } + if _, ok := state.AskUserRequest.Nested.Tools[0].Args["questions"].([]map[string]any); ok { + t.Fatalf("nested tool args decoded as []map[string]any; test no longer exercises the []any round-trip shape") + } + want := "Remote agent 'github_agent' asks: " + question + if got := RemoteHitlHint(state); got != want { + t.Fatalf("hint = %q, want %q", got, want) + } +}