The format of A2A responses for input-required tasks, triggers the creation of mock function mock_function_call_for_required_user_input for an adk python client connecting to the adk-go a2a agent as RemoteA2aAgent.
In particular it seems that this behavior is triggered by the presence of an artifact section in the a2a response containing the message from the llm.
This is a working a2aresponse from and adk-python server, where there are no artifacts and there is a rich history:
{
"id": "96ef155b-8af3-491b-b31f-2e1e10001b57",
"jsonrpc": "2.0",
"result": {
"contextId": "ea937a3f-c827-42fb-a61b-ea5425e3f480",
"history": [
{
"contextId": "ea937a3f-c827-42fb-a61b-ea5425e3f480",
"kind": "message",
"messageId": "6bef4805-c6aa-47f3-8849-1ded12c8f708",
"parts": [
{
"kind": "text",
"metadata": {
"is_user_input": true
},
"text": "reimburse travel exp 400$"
}
],
"role": "user",
"taskId": "36ff08e9-bb17-46f6-a9c2-213262194a1c"
},
{
"contextId": "ea937a3f-c827-42fb-a61b-ea5425e3f480",
"kind": "message",
"messageId": "6bef4805-c6aa-47f3-8849-1ded12c8f708",
"parts": [
{
"kind": "text",
"metadata": {
"is_user_input": true
},
"text": "reimburse travel exp 400$"
}
],
"role": "user",
"taskId": "36ff08e9-bb17-46f6-a9c2-213262194a1c"
},
{
"kind": "message",
"messageId": "9332328e-ee2d-4522-b8fa-ac68b151c89f",
"parts": [
{
"data": {
"id": "call_73TZfklVT5l3jkQOY3GzLsH6",
"args": {
"amount": 400,
"purpose": "travel exp"
},
"name": "ask_for_approval"
},
"kind": "data",
"metadata": {
"adk_type": "function_call",
"adk_is_long_running": true
}
}
],
"role": "agent"
},
{
"kind": "message",
"messageId": "604f3453-0c5a-4e0f-a3ce-cc45d740ad19",
"parts": [
{
"data": {
"id": "call_73TZfklVT5l3jkQOY3GzLsH6",
"name": "ask_for_approval",
"response": {
"status": "pending",
"amount": 400,
"ticketId": "reimbursement-ticket-001"
}
},
"kind": "data",
"metadata": {
"adk_type": "function_response"
}
}
],
"role": "agent"
},
{
"kind": "message",
"messageId": "4f2e9abe-df35-4980-8cc2-56220f6967d4",
"parts": [
{
"kind": "text",
"text": "The reimbursement request for $400 for travel expenses is currently pending approval from the manager. I will notify you once the decision is made."
}
],
"role": "agent"
}
],
"id": "36ff08e9-bb17-46f6-a9c2-213262194a1c",
"kind": "task",
"metadata": {
"adk_app_name": "human_in_loop",
"adk_user_id": "A2A_USER_ea937a3f-c827-42fb-a61b-ea5425e3f480",
"adk_session_id": "ea937a3f-c827-42fb-a61b-ea5425e3f480",
"adk_invocation_id": "e-ffa3f9d1-fd3b-41df-9822-6966eb2a83ea",
"adk_author": "reimbursement_agent",
"adk_event_id": "bc3e0718-8ab3-4354-a042-37c29faef5d2",
"adk_usage_metadata": {
"cachedContentTokenCount": 0,
"candidatesTokenCount": 29,
"promptTokenCount": 277,
"totalTokenCount": 306
},
"adk_actions": {
"stateDelta": {},
"artifactDelta": {},
"requestedAuthConfigs": {},
"requestedToolConfirmations": {}
}
},
"status": {
"message": {
"kind": "message",
"messageId": "9332328e-ee2d-4522-b8fa-ac68b151c89f",
"parts": [
{
"data": {
"id": "call_73TZfklVT5l3jkQOY3GzLsH6",
"args": {
"amount": 400,
"purpose": "travel exp"
},
"name": "ask_for_approval"
},
"kind": "data",
"metadata": {
"adk_type": "function_call",
"adk_is_long_running": true
}
}
],
"role": "agent"
},
"state": "input-required",
"timestamp": "2026-05-28T20:38:50.410507+00:00"
}
}
}
The same from adk-go looks like:
{
"id": "96ef155b-8af3-491b-b31f-2e1e10001b57",
"jsonrpc": "2.0",
"result": {
"contextId": "ea937a3f-c827-42fb-a61b-ea5425e3f480",
"artifacts": [
{
"artifactId": "019e7053-fff4-7293-91a7-0b53025385d5",
"parts": [
{
"kind": "text",
"text": "The reimbursement request for $400 for travel expenses is currently pending approval from the manager. I will notify you once the decision is made."
}
]
}
],
"history": [
{
"contextId": "ea937a3f-c827-42fb-a61b-ea5425e3f480",
"kind": "message",
"messageId": "6bef4805-c6aa-47f3-8849-1ded12c8f708",
"parts": [
{
"kind": "text",
"metadata": {
"is_user_input": true
},
"text": "reimburse travel exp 400$"
}
],
"role": "user",
"taskId": "36ff08e9-bb17-46f6-a9c2-213262194a1c"
}
],
"id": "36ff08e9-bb17-46f6-a9c2-213262194a1c",
"kind": "task",
"metadata": {
"adk_app_name": "human_in_loop",
"adk_user_id": "A2A_USER_ea937a3f-c827-42fb-a61b-ea5425e3f480",
"adk_session_id": "ea937a3f-c827-42fb-a61b-ea5425e3f480",
"adk_invocation_id": "e-ffa3f9d1-fd3b-41df-9822-6966eb2a83ea",
"adk_author": "reimbursement_agent",
"adk_event_id": "bc3e0718-8ab3-4354-a042-37c29faef5d2",
"adk_usage_metadata": {
"cachedContentTokenCount": 0,
"candidatesTokenCount": 29,
"promptTokenCount": 277,
"totalTokenCount": 306
},
"adk_actions": {
"stateDelta": {},
"artifactDelta": {},
"requestedAuthConfigs": {},
"requestedToolConfirmations": {}
}
},
"status": {
"message": {
"kind": "message",
"messageId": "9332328e-ee2d-4522-b8fa-ac68b151c89f",
"parts": [
{
"data": {
"id": "call_73TZfklVT5l3jkQOY3GzLsH6",
"args": {
"amount": 400,
"purpose": "travel exp"
},
"name": "ask_for_approval"
},
"kind": "data",
"metadata": {
"adk_type": "function_call",
"adk_is_long_running": true
}
},
{
"data": {
"id": "call_73TZfklVT5l3jkQOY3GzLsH6",
"name": "ask_for_approval",
"response": {
"status": "pending",
"amount": 400,
"ticketId": "reimbursement-ticket-001"
}
},
"kind": "data",
"metadata": {
"adk_type": "function_response"
}
}
],
"role": "agent"
},
"state": "input-required",
"timestamp": "2026-05-28T20:38:50.410507+00:00"
}
}
}
I was able to workaround the issue patching the a2aresponse via an a2asrv.CallInterceptor :
reqHandler := &a2asrv.InterceptedHandler{
Handler: a2asrv.NewHandler(executor),
Interceptors: []a2asrv.CallInterceptor{&replyInterceptor{}},
}
mux.Handle("/invoke", a2av0.NewJSONRPCHandler(reqHandler))
func (r *replyInterceptor) After(ctx context.Context, callCtx *a2asrv.CallContext, resp *a2asrv.Response) error {
if resp.Err != nil {
return nil // pass errors through unchanged
}
switch v := resp.Payload.(type) {
case *a2a.Task:
if v.Status.State == a2a.TaskStateInputRequired {
promoteStatusMessageToHistory(v)
stripStatusFunctionResponses(v)
promoteArtifactsToHistory(v)
}
}
return nil
}
// promoteStatusMessageToHistory splits every part of task.Status.Message into
// its own agent history message, one part per message, then nils the status
// message so the parts are not duplicated in the response.
//
// Before:
//
// Status.Message: {parts: [fc_part, fr_part]}
//
// After:
//
// History: [..., {role:ROLE_AGENT, parts:[fc_part]}, {role:ROLE_AGENT, parts:[fr_part]}]
// Status.Message: nil
func promoteStatusMessageToHistory(task *a2a.Task) {
msg := task.Status.Message
if msg == nil {
return
}
for _, part := range msg.Parts {
task.History = append(task.History, &a2a.Message{
ID: a2a.NewMessageID(),
Role: a2a.MessageRoleAgent,
Parts: a2a.ContentParts{part},
})
}
}
// promoteArtifactsToHistory converts every artifact on the task into an agent
// history message carrying the same parts, then clears the Artifacts slice.
//
// Before:
//
// Artifacts: [{artifactId: "...", parts: [{kind:"text", text:"..."}]}]
//
// After:
//
// History: [..., {kind:"message", messageId:"...", role:"ROLE_AGENT", parts:[...]}]
// Artifacts: nil
func promoteArtifactsToHistory(task *a2a.Task) {
for _, artifact := range task.Artifacts {
task.History = append(task.History, &a2a.Message{
ID: a2a.NewMessageID(),
Role: a2a.MessageRoleAgent,
Parts: artifact.Parts,
})
}
task.Artifacts = nil
}
// stripStatusFunctionResponses removes any data part whose metadata marks it as
// adk_type:"function_response" from task.Status.Message.Parts before the
// status message is promoted to history or sent to the client.
func stripStatusFunctionResponses(task *a2a.Task) {
msg := task.Status.Message
if msg == nil {
return
}
msg.Parts = slices.DeleteFunc(msg.Parts, func(p *a2a.Part) bool {
v, _ := p.Metadata["adk_type"].(string)
return v == "function_response"
})
}
From tests looks like the current adk-go output format is intended. Why has been chosen in a way that is not compatible with python clients?
The format of A2A responses for input-required tasks, triggers the creation of mock function
mock_function_call_for_required_user_inputfor an adk python client connecting to the adk-go a2a agent as RemoteA2aAgent.In particular it seems that this behavior is triggered by the presence of an artifact section in the a2a response containing the message from the llm.
This is a working a2aresponse from and adk-python server, where there are no artifacts and there is a rich history:
The same from adk-go looks like:
I was able to workaround the issue patching the a2aresponse via an
a2asrv.CallInterceptor:From tests looks like the current adk-go output format is intended. Why has been chosen in a way that is not compatible with python clients?