diff --git a/provider/a2aprovider/a2a.go b/provider/a2aprovider/a2a.go index 6d9a6949..b2142b1e 100644 --- a/provider/a2aprovider/a2a.go +++ b/provider/a2aprovider/a2a.go @@ -232,6 +232,16 @@ func (a *a2aProvider) subscribeToTaskWithFallback(ctx context.Context, taskID a2 } } +// agentRole maps an A2A message role onto the framework role, preserving a +// user-role response instead of assuming assistant. Mirrors the hosting-side +// toAgentMessage mapping and the Python client. +func agentRole(role a2a.MessageRole) message.Role { + if role == a2a.MessageRoleAgent { + return message.RoleAssistant + } + return message.RoleUser +} + func sendMsg(session *agent.Session, contextID string, seq iter.Seq2[a2a.Event, error], stream bool, yield func(*agent.ResponseUpdate, error) bool) { var taskID string var taskState a2a.TaskState @@ -281,15 +291,17 @@ func sendMsg(session *agent.Session, contextID string, seq iter.Seq2[a2a.Event, } case *a2a.TaskStatusUpdateEvent: var messageID string + role := message.RoleAssistant if e.Status.Message != nil { messageID = e.Status.Message.ID + role = agentRole(e.Status.Message.Role) } contents, err := statusContents(&e.Status) if err != nil { yield(nil, err) return } - update := newResponseUpdate(e, e.Metadata, string(e.TaskID), messageID, message.RoleAssistant, contents) + update := newResponseUpdate(e, e.Metadata, string(e.TaskID), messageID, role, contents) update.FinishReason = finishReasonForTaskState(e.Status.State) if !forward(update, nil) { return @@ -310,7 +322,7 @@ func sendMsg(session *agent.Session, contextID string, seq iter.Seq2[a2a.Event, yield(nil, err) return } - update := newResponseUpdate(e, e.Metadata, e.ID, e.ID, message.RoleAssistant, contents) + update := newResponseUpdate(e, e.Metadata, e.ID, e.ID, agentRole(e.Role), contents) update.FinishReason = "stop" if !forward(update, nil) { return diff --git a/provider/a2aprovider/a2a_test.go b/provider/a2aprovider/a2a_test.go index 642730bc..b427562a 100644 --- a/provider/a2aprovider/a2a_test.go +++ b/provider/a2aprovider/a2a_test.go @@ -410,6 +410,31 @@ func TestRunAllowsNonUserRoleMessages(t *testing.T) { } } +// A response message carrying the A2A user role must map to RoleUser, not be +// hardcoded to assistant, matching the Python client. +func TestRunPreservesInboundUserRole(t *testing.T) { + transport := &mockA2ATransport{ + responseToReturn: &a2a.Message{ + ID: "m1", + Role: a2a.MessageRoleUser, + Parts: a2a.ContentParts{a2a.NewTextPart("hi")}, + }, + } + a := newTestAgent(transport, agent.Config{}) + + result, err := a.RunText(t.Context(), "hello").Collect() + if err != nil { + t.Fatalf("error = %v, want nil", err) + } + if len(result.Messages) != 1 { + t.Fatalf("len(result.Messages) = %d, want 1", len(result.Messages)) + } + if result.Messages[0].Role != message.RoleUser { + t.Errorf("Role = %q, want %q", result.Messages[0].Role, message.RoleUser) + } +} + +// TestRunWithValidUserMessage tests successful run with valid user message. func TestRunRejectsMessagesWithoutSendableParts(t *testing.T) { tests := []struct { name string