diff --git a/a2a/agent.go b/a2a/agent.go index b4541bac..45818488 100644 --- a/a2a/agent.go +++ b/a2a/agent.go @@ -29,16 +29,18 @@ type AgentCapabilities struct { ExtendedAgentCard bool `json:"extendedAgentCard,omitempty" yaml:"extendedAgentCard,omitempty" mapstructure:"extendedAgentCard,omitempty"` } -// SecurityRequirements describes a set of security requirements that must be present on a request. +// SecurityRequirement describes a set of security requirements that must be present on a request. // For example, to specify that mutual TLS AND an oauth2 token for specific scopes is required, the // following requirements object needs to be created: // -// SecurityRequirements{ -// SecuritySchemeName("oauth2"): SecuritySchemeScopes{"read", "write"}, -// SecuritySchemeName("mTLS"): {} +// SecurityRequirement{ +// Schemes: map[SecuritySchemeName]SecuritySchemeScopes{ +// "oauth2": SecuritySchemeScopes{"read", "write"}, +// "mTLS": SecuritySchemeScopes{}, +// }, // } type SecurityRequirement struct { - Scheme map[SecuritySchemeName]SecuritySchemeScopes `json:"scheme" yaml:"scheme" mapstructure:"scheme"` + Schemes map[SecuritySchemeName]SecuritySchemeScopes `json:"scheme" yaml:"scheme" mapstructure:"scheme"` } // AgentCard is a self-describing manifest for an agent. It provides essential @@ -85,14 +87,14 @@ type AgentCard struct { // Provider contains information about the agent's service provider. Provider *AgentProvider `json:"provider,omitempty" yaml:"provider,omitempty" mapstructure:"provider,omitempty"` - // Security is a list of security requirement objects that apply to all agent interactions. + // SecurityRequirements is a list of security requirement objects that apply to all agent interactions. // Each object lists security schemes that can be used. // Follows the OpenAPI 3.0 Security Requirement Object. // This list can be seen as an OR of ANDs. Each object in the list describes one // possible set of security requirements that must be present on a request. // This allows specifying, for example, "callers must either use OAuth OR an API Key AND mTLS.": // - // Security: []SecurityRequirements{ + // SecurityRequirements: []SecurityRequirements{ // {"oauth2": SecuritySchemeScopes{"read"}}, // {"mTLS": SecuritySchemeScopes{}, "apiKey": SecuritySchemeScopes{"read"}} // } diff --git a/a2a/push.go b/a2a/push.go index c6280bcb..5599dc2a 100644 --- a/a2a/push.go +++ b/a2a/push.go @@ -86,9 +86,6 @@ type CreateTaskPushConfigRequest struct { // TaskID is the ID of the task. TaskID TaskID `json:"taskId" yaml:"taskId" mapstructure:"taskId"` - - // ID is the ID of the push notification configuration. - ConfigID string `json:"configId" yaml:"configId" mapstructure:"configId"` } // PushConfig defines the configuration for setting up push notifications for task updates. diff --git a/a2apb/v1/pbconv/from_proto.go b/a2apb/v1/pbconv/from_proto.go index 13a6b734..7897e597 100644 --- a/a2apb/v1/pbconv/from_proto.go +++ b/a2apb/v1/pbconv/from_proto.go @@ -31,22 +31,27 @@ func fromProtoMap(meta *structpb.Struct) map[string]any { return meta.AsMap() } -func FromProtoSendMessageRequest(req *a2apb.SendMessageRequest) (*a2a.MessageSendParams, error) { +func FromProtoSendMessageRequest(req *a2apb.SendMessageRequest) (*a2a.SendMessageRequest, error) { if req == nil { return nil, nil } - msg, err := FromProtoMessage(req.GetRequest()) + msg, err := FromProtoMessage(req.GetMessage()) if err != nil { return nil, err } + if msg == nil { + return nil, fmt.Errorf("message cannot be nil") + } + config, err := fromProtoSendMessageConfig(req.GetConfiguration()) if err != nil { return nil, err } - params := &a2a.MessageSendParams{ + params := &a2a.SendMessageRequest{ + Tenant: req.GetTenant(), Message: msg, Config: config, Metadata: fromProtoMap(req.GetMetadata()), @@ -58,17 +63,22 @@ func FromProtoMessage(pMsg *a2apb.Message) (*a2a.Message, error) { if pMsg == nil { return nil, nil } - - parts, err := fromProtoParts(pMsg.GetParts()) + contentParts, err := fromProtoParts(pMsg.GetParts()) if err != nil { return nil, err } + if pMsg.GetMessageId() == "" { + return nil, fmt.Errorf("message id cannot be empty") + } + if pMsg.GetRole() == a2apb.Role_ROLE_UNSPECIFIED { + return nil, fmt.Errorf("message role cannot be unspecified") + } msg := &a2a.Message{ ID: pMsg.GetMessageId(), ContextID: pMsg.GetContextId(), Extensions: pMsg.GetExtensions(), - Parts: parts, + Parts: contentParts, TaskID: a2a.TaskID(pMsg.GetTaskId()), Role: fromProtoRole(pMsg.GetRole()), Metadata: fromProtoMap(pMsg.GetMetadata()), @@ -85,41 +95,29 @@ func FromProtoMessage(pMsg *a2apb.Message) (*a2a.Message, error) { return msg, nil } -func fromProtoFilePart(pPart *a2apb.FilePart, meta map[string]any) (a2a.FilePart, error) { - switch f := pPart.GetFile().(type) { - case *a2apb.FilePart_FileWithBytes: - return a2a.FilePart{ - File: a2a.FileBytes{ - FileMeta: a2a.FileMeta{MimeType: pPart.GetMimeType(), Name: pPart.GetName()}, - Bytes: string(f.FileWithBytes), - }, - Metadata: meta, - }, nil - case *a2apb.FilePart_FileWithUri: - return a2a.FilePart{ - File: a2a.FileURI{ - FileMeta: a2a.FileMeta{MimeType: pPart.GetMimeType(), Name: pPart.GetName()}, - URI: f.FileWithUri, - }, - Metadata: meta, - }, nil - default: - return a2a.FilePart{}, fmt.Errorf("unsupported FilePart type: %T", f) - } -} - func fromProtoPart(p *a2apb.Part) (a2a.Part, error) { + if p == nil { + return a2a.Part{}, fmt.Errorf("part cannot be nil") + } meta := fromProtoMap(p.Metadata) - switch part := p.GetPart().(type) { + part := a2a.Part{ + Filename: p.GetFilename(), + MediaType: p.GetMediaType(), + Metadata: meta, + } + switch content := p.GetContent().(type) { case *a2apb.Part_Text: - return a2a.TextPart{Text: part.Text, Metadata: meta}, nil + part.Content = a2a.Text(content.Text) + case *a2apb.Part_Raw: + part.Content = a2a.Raw(content.Raw) case *a2apb.Part_Data: - return a2a.DataPart{Data: part.Data.GetData().AsMap(), Metadata: meta}, nil - case *a2apb.Part_File: - return fromProtoFilePart(part.File, meta) + part.Content = a2a.Data(content.Data.GetStructValue().AsMap()) + case *a2apb.Part_Url: + part.Content = a2a.URL(content.Url) default: - return nil, fmt.Errorf("unsupported part type: %T", part) + return a2a.Part{}, fmt.Errorf("unsupported part type: %T", content) } + return part, nil } func fromProtoRole(role a2apb.Role) a2a.MessageRole { @@ -138,61 +136,117 @@ func fromProtoPushConfig(pConf *a2apb.PushNotificationConfig) (*a2a.PushConfig, return nil, nil } + auth, err := fromProtoAuthenticationInfo(pConf.GetAuthentication()) + if err != nil { + return nil, fmt.Errorf("failed to convert authentication info: %w", err) + } + url := pConf.GetUrl() + if url == "" { + return nil, fmt.Errorf("url cannot be empty") + } + result := &a2a.PushConfig{ ID: pConf.GetId(), - URL: pConf.GetUrl(), + URL: url, Token: pConf.GetToken(), + Auth: auth, } - if pConf.GetAuthentication() != nil { - result.Auth = &a2a.PushAuthInfo{ - Schemes: pConf.GetAuthentication().GetSchemes(), - Credentials: pConf.GetAuthentication().GetCredentials(), - } - } + return result, nil } -func fromProtoSendMessageConfig(conf *a2apb.SendMessageConfiguration) (*a2a.MessageSendConfig, error) { +func fromProtoAuthenticationInfo(pAuth *a2apb.AuthenticationInfo) (*a2a.PushAuthInfo, error) { + if pAuth == nil { + return nil, nil + } + scheme := pAuth.GetScheme() + if scheme == "" { + return nil, fmt.Errorf("authentication scheme cannot be empty") + } + return &a2a.PushAuthInfo{ + Scheme: scheme, + Credentials: pAuth.GetCredentials(), + }, nil +} + +func fromProtoSendMessageConfig(conf *a2apb.SendMessageConfiguration) (*a2a.SendMessageConfig, error) { if conf == nil { return nil, nil } - pConf, err := fromProtoPushConfig(conf.GetPushNotification()) + pConf, err := fromProtoPushConfig(conf.GetPushNotificationConfig()) if err != nil { return nil, fmt.Errorf("failed to convert push config: %w", err) } - result := &a2a.MessageSendConfig{ + result := &a2a.SendMessageConfig{ AcceptedOutputModes: conf.GetAcceptedOutputModes(), Blocking: proto.Bool(conf.GetBlocking()), PushConfig: pConf, } // TODO: consider the approach after resolving https://github.com/a2aproject/A2A/issues/1072 - if conf.HistoryLength > 0 { - hl := int(conf.HistoryLength) + if conf.HistoryLength != nil && *conf.HistoryLength >= 0 { + hl := int(*conf.HistoryLength) result.HistoryLength = &hl } return result, nil } -func FromProtoGetTaskRequest(req *a2apb.GetTaskRequest) (*a2a.TaskQueryParams, error) { +func FromProtoGetTaskRequest(req *a2apb.GetTaskRequest) (*a2a.GetTaskRequest, error) { if req == nil { return nil, nil } // TODO: consider throwing an error when the path - req.GetName() is unexpected, e.g. tasks/taskID/someExtraText - taskID, err := ExtractTaskID(req.GetName()) - if err != nil { - return nil, fmt.Errorf("failed to extract task id: %w", err) + taskID := a2a.TaskID(req.GetId()) + if taskID == "" { + return nil, fmt.Errorf("task id cannot be empty") } - params := &a2a.TaskQueryParams{ID: taskID} - if req.GetHistoryLength() > 0 { - historyLength := int(req.GetHistoryLength()) - params.HistoryLength = &historyLength + request := &a2a.GetTaskRequest{ + Tenant: req.GetTenant(), + ID: taskID, } - return params, nil + if req.HistoryLength != nil && *req.HistoryLength >= 0 { + historyLength := int(*req.HistoryLength) + request.HistoryLength = &historyLength + } + return request, nil +} + +func FromProtoCancelTaskRequest(req *a2apb.CancelTaskRequest) (*a2a.CancelTaskRequest, error) { + if req == nil { + return nil, nil + } + + taskID := a2a.TaskID(req.GetId()) + if taskID == "" { + return nil, fmt.Errorf("task id cannot be empty") + } + + request := &a2a.CancelTaskRequest{ + Tenant: req.GetTenant(), + ID: taskID, + } + return request, nil +} + +func FromProtoSubscribeToTaskRequest(req *a2apb.SubscribeToTaskRequest) (*a2a.SubscribeToTaskRequest, error) { + if req == nil { + return nil, nil + } + + taskID := a2a.TaskID(req.GetId()) + if taskID == "" { + return nil, fmt.Errorf("task id cannot be empty") + } + + request := &a2a.SubscribeToTaskRequest{ + Tenant: req.GetTenant(), + ID: taskID, + } + return request, nil } func FromProtoListTasksRequest(req *a2apb.ListTasksRequest) (*a2a.ListTasksRequest, error) { @@ -201,8 +255,8 @@ func FromProtoListTasksRequest(req *a2apb.ListTasksRequest) (*a2a.ListTasksReque } var lastUpdatedAfter *time.Time - if req.GetLastUpdatedTime() != nil { - t := req.GetLastUpdatedTime().AsTime() + if req.GetStatusTimestampAfter() != nil { + t := req.GetStatusTimestampAfter().AsTime() lastUpdatedAfter = &t } @@ -211,15 +265,22 @@ func FromProtoListTasksRequest(req *a2apb.ListTasksRequest) (*a2a.ListTasksReque status = a2a.TaskState(req.GetStatus().String()) } - return &a2a.ListTasksRequest{ - ContextID: req.GetContextId(), - Status: status, - PageSize: int(req.GetPageSize()), - PageToken: req.GetPageToken(), - HistoryLength: int(req.GetHistoryLength()), - LastUpdatedAfter: lastUpdatedAfter, - IncludeArtifacts: req.GetIncludeArtifacts(), - }, nil + listTasksRequest := a2a.ListTasksRequest{ + Tenant: req.GetTenant(), + ContextID: req.GetContextId(), + Status: status, + PageSize: int(req.GetPageSize()), + PageToken: req.GetPageToken(), + StatusTimestampAfter: lastUpdatedAfter, + IncludeArtifacts: req.GetIncludeArtifacts(), + } + + if req.HistoryLength != nil && *req.HistoryLength >= 0 { + hl := int(*req.HistoryLength) + listTasksRequest.HistoryLength = hl + } + + return &listTasksRequest, nil } func FromProtoListTasksResponse(resp *a2apb.ListTasksResponse) (*a2a.ListTasksResponse, error) { @@ -235,72 +296,80 @@ func FromProtoListTasksResponse(resp *a2apb.ListTasksResponse) (*a2a.ListTasksRe } tasks = append(tasks, t) } - return &a2a.ListTasksResponse{ Tasks: tasks, TotalSize: int(resp.GetTotalSize()), - PageSize: len(tasks), + PageSize: int(resp.GetPageSize()), NextPageToken: resp.GetNextPageToken(), }, nil } -func FromProtoCreateTaskPushConfigRequest(req *a2apb.CreateTaskPushNotificationConfigRequest) (*a2a.TaskPushConfig, error) { +func FromProtoCreateTaskPushConfigRequest(req *a2apb.CreateTaskPushNotificationConfigRequest) (*a2a.CreateTaskPushConfigRequest, error) { if req == nil { return nil, nil } config := req.GetConfig() - if config.GetPushNotificationConfig() == nil { - return nil, fmt.Errorf("invalid config") + if config == nil { + return nil, fmt.Errorf("config is required") } - pConf, err := fromProtoPushConfig(config.GetPushNotificationConfig()) + pConf, err := fromProtoPushConfig(config) if err != nil { return nil, fmt.Errorf("failed to convert push config: %w", err) } - - taskID, err := ExtractTaskID(req.GetParent()) - if err != nil { - return nil, fmt.Errorf("failed to extract task id: %w", err) + taskID := a2a.TaskID(req.GetTaskId()) + if taskID == "" { + return nil, fmt.Errorf("task id cannot be empty") } - return &a2a.TaskPushConfig{TaskID: taskID, Config: *pConf}, nil + return &a2a.CreateTaskPushConfigRequest{ + Tenant: req.GetTenant(), + TaskID: taskID, + Config: *pConf, + }, nil } -func FromProtoGetTaskPushConfigRequest(req *a2apb.GetTaskPushNotificationConfigRequest) (*a2a.GetTaskPushConfigParams, error) { +func FromProtoGetTaskPushConfigRequest(req *a2apb.GetTaskPushNotificationConfigRequest) (*a2a.GetTaskPushConfigRequest, error) { if req == nil { return nil, nil } - taskID, err := ExtractTaskID(req.GetName()) - if err != nil { - return nil, fmt.Errorf("failed to extract task id: %w", err) + taskID := a2a.TaskID(req.GetTaskId()) + if taskID == "" { + return nil, fmt.Errorf("task id cannot be empty") } - - configID, err := ExtractConfigID(req.GetName()) - if err != nil { - return nil, fmt.Errorf("failed to extract config id: %w", err) + id := req.GetId() + if id == "" { + return nil, fmt.Errorf("config id cannot be empty") } - return &a2a.GetTaskPushConfigParams{TaskID: taskID, ConfigID: configID}, nil + return &a2a.GetTaskPushConfigRequest{ + Tenant: req.GetTenant(), + TaskID: taskID, + ID: id, + }, nil } -func FromProtoDeleteTaskPushConfigRequest(req *a2apb.DeleteTaskPushNotificationConfigRequest) (*a2a.DeleteTaskPushConfigParams, error) { +func FromProtoDeleteTaskPushConfigRequest(req *a2apb.DeleteTaskPushNotificationConfigRequest) (*a2a.DeleteTaskPushConfigRequest, error) { if req == nil { return nil, nil } - taskID, err := ExtractTaskID(req.GetName()) - if err != nil { - return nil, fmt.Errorf("failed to extract task id: %w", err) + taskID := a2a.TaskID(req.GetTaskId()) + if taskID == "" { + return nil, fmt.Errorf("task id cannot be empty") } - - configID, err := ExtractConfigID(req.GetName()) - if err != nil { - return nil, fmt.Errorf("failed to extract config id: %w", err) + id := req.GetId() + if id == "" { + return nil, fmt.Errorf("config id cannot be empty") } - return &a2a.DeleteTaskPushConfigParams{TaskID: taskID, ConfigID: configID}, nil + return &a2a.DeleteTaskPushConfigRequest{ + Tenant: req.GetTenant(), + TaskID: taskID, + ID: id, + }, nil } func FromProtoSendMessageResponse(resp *a2apb.SendMessageResponse) (a2a.SendMessageResult, error) { @@ -309,8 +378,8 @@ func FromProtoSendMessageResponse(resp *a2apb.SendMessageResponse) (a2a.SendMess } switch p := resp.Payload.(type) { - case *a2apb.SendMessageResponse_Msg: - return FromProtoMessage(p.Msg) + case *a2apb.SendMessageResponse_Message: + return FromProtoMessage(p.Message) case *a2apb.SendMessageResponse_Task: return FromProtoTask(p.Task) default: @@ -324,20 +393,28 @@ func FromProtoStreamResponse(resp *a2apb.StreamResponse) (a2a.Event, error) { } switch p := resp.Payload.(type) { - case *a2apb.StreamResponse_Msg: - return FromProtoMessage(p.Msg) + case *a2apb.StreamResponse_Message: + msg, err := FromProtoMessage(p.Message) + if err != nil { + return nil, err + } + return msg, nil case *a2apb.StreamResponse_Task: - return FromProtoTask(p.Task) + task, err := FromProtoTask(p.Task) + if err != nil { + return nil, err + } + return task, nil case *a2apb.StreamResponse_StatusUpdate: status, err := fromProtoTaskStatus(p.StatusUpdate.GetStatus()) if err != nil { return nil, err } + taskID := a2a.TaskID(p.StatusUpdate.GetTaskId()) return &a2a.TaskStatusUpdateEvent{ ContextID: p.StatusUpdate.GetContextId(), - Final: p.StatusUpdate.GetFinal(), Status: status, - TaskID: a2a.TaskID(p.StatusUpdate.GetTaskId()), + TaskID: taskID, Metadata: fromProtoMap(p.StatusUpdate.GetMetadata()), }, nil case *a2apb.StreamResponse_ArtifactUpdate: @@ -345,12 +422,13 @@ func FromProtoStreamResponse(resp *a2apb.StreamResponse) (a2a.Event, error) { if err != nil { return nil, err } + taskID := a2a.TaskID(p.ArtifactUpdate.GetTaskId()) return &a2a.TaskArtifactUpdateEvent{ Append: p.ArtifactUpdate.GetAppend(), Artifact: artifact, ContextID: p.ArtifactUpdate.GetContextId(), LastChunk: p.ArtifactUpdate.GetLastChunk(), - TaskID: a2a.TaskID(p.ArtifactUpdate.GetTaskId()), + TaskID: taskID, Metadata: fromProtoMap(p.ArtifactUpdate.GetMetadata()), }, nil default: @@ -370,23 +448,23 @@ func fromProtoMessages(pMsgs []*a2apb.Message) ([]*a2a.Message, error) { return msgs, nil } -func fromProtoParts(pParts []*a2apb.Part) ([]a2a.Part, error) { - parts := make([]a2a.Part, len(pParts)) +func fromProtoParts(pParts []*a2apb.Part) (a2a.ContentParts, error) { + contentParts := make([]*a2a.Part, len(pParts)) for i, pPart := range pParts { part, err := fromProtoPart(pPart) if err != nil { return nil, fmt.Errorf("failed to convert part: %w", err) } - parts[i] = part + contentParts[i] = &part } - return parts, nil + return contentParts, nil } func fromProtoTaskState(state a2apb.TaskState) a2a.TaskState { switch state { case a2apb.TaskState_TASK_STATE_AUTH_REQUIRED: return a2a.TaskStateAuthRequired - case a2apb.TaskState_TASK_STATE_CANCELLED: + case a2apb.TaskState_TASK_STATE_CANCELED: return a2a.TaskStateCanceled case a2apb.TaskState_TASK_STATE_COMPLETED: return a2a.TaskStateCompleted @@ -410,7 +488,7 @@ func fromProtoTaskStatus(pStatus *a2apb.TaskStatus) (a2a.TaskStatus, error) { return a2a.TaskStatus{}, fmt.Errorf("invalid status") } - message, err := FromProtoMessage(pStatus.GetUpdate()) + message, err := FromProtoMessage(pStatus.GetMessage()) if err != nil { return a2a.TaskStatus{}, fmt.Errorf("failed to convert message for task status: %w", err) } @@ -433,16 +511,20 @@ func fromProtoArtifact(pArtifact *a2apb.Artifact) (*a2a.Artifact, error) { return nil, nil } - parts, err := fromProtoParts(pArtifact.GetParts()) + contentParts, err := fromProtoParts(pArtifact.GetParts()) if err != nil { return nil, fmt.Errorf("failed to convert from proto parts: %w", err) } + id := a2a.ArtifactID(pArtifact.GetArtifactId()) + if id == "" { + return nil, fmt.Errorf("artifact id cannot be empty") + } return &a2a.Artifact{ - ID: a2a.ArtifactID(pArtifact.GetArtifactId()), + ID: id, Name: pArtifact.GetName(), Description: pArtifact.GetDescription(), - Parts: parts, + Parts: contentParts, Extensions: pArtifact.GetExtensions(), Metadata: fromProtoMap(pArtifact.GetMetadata()), }, nil @@ -464,7 +546,14 @@ func FromProtoTask(pTask *a2apb.Task) (*a2a.Task, error) { if pTask == nil { return nil, nil } - + id := a2a.TaskID(pTask.GetId()) + if id == "" { + return nil, fmt.Errorf("task id cannot be empty") + } + contextID := pTask.GetContextId() + if contextID == "" { + return nil, fmt.Errorf("context id cannot be empty") + } status, err := fromProtoTaskStatus(pTask.Status) if err != nil { return nil, fmt.Errorf("failed to convert status: %w", err) @@ -481,8 +570,8 @@ func FromProtoTask(pTask *a2apb.Task) (*a2a.Task, error) { } result := &a2a.Task{ - ID: a2a.TaskID(pTask.GetId()), - ContextID: pTask.GetContextId(), + ID: id, + ContextID: contextID, Status: status, Artifacts: artifacts, History: history, @@ -497,23 +586,22 @@ func FromProtoTaskPushConfig(pTaskConfig *a2apb.TaskPushNotificationConfig) (*a2 return nil, nil } - taskID, err := ExtractTaskID(pTaskConfig.GetName()) - if err != nil { - return nil, fmt.Errorf("failed to extract task id: %w", err) + taskID := a2a.TaskID(pTaskConfig.GetTaskId()) + if taskID == "" { + return nil, fmt.Errorf("task id cannot be empty") } - - configID, err := ExtractConfigID(pTaskConfig.GetName()) - if err != nil { - return nil, fmt.Errorf("failed to extract config id: %w", err) + id := pTaskConfig.GetId() + if id == "" { + return nil, fmt.Errorf("config id cannot be empty") } - + pConf := pTaskConfig.GetPushNotificationConfig() if pConf == nil { - return nil, fmt.Errorf("push notification config is nil") + return nil, fmt.Errorf("push notification config cannot be empty") } - if pConf.GetId() != configID { - return nil, fmt.Errorf("config id mismatch: %q != %q", pConf.GetId(), configID) + if pConf.GetId() != id { + return nil, fmt.Errorf("config id mismatch: %q != %q", pConf.GetId(), id) } config, err := fromProtoPushConfig(pConf) @@ -521,41 +609,82 @@ func FromProtoTaskPushConfig(pTaskConfig *a2apb.TaskPushNotificationConfig) (*a2 return nil, fmt.Errorf("failed to convert push config: %w", err) } - return &a2a.TaskPushConfig{TaskID: taskID, Config: *config}, nil + return &a2a.TaskPushConfig{ + Tenant: pTaskConfig.GetTenant(), + Config: *config, + TaskID: taskID, + ID: id, + }, nil } -func FromProtoListTaskPushConfig(resp *a2apb.ListTaskPushNotificationConfigResponse) ([]*a2a.TaskPushConfig, error) { - if resp == nil { - return nil, fmt.Errorf("response is nil") - } - - configs := make([]*a2a.TaskPushConfig, len(resp.GetConfigs())) +func FromProtoListTaskPushConfigResponse(resp *a2apb.ListTaskPushNotificationConfigResponse) (*a2a.ListTaskPushConfigResponse, error) { + configs := make([]a2a.TaskPushConfig, len(resp.GetConfigs())) for i, pConfig := range resp.GetConfigs() { config, err := FromProtoTaskPushConfig(pConfig) if err != nil { return nil, fmt.Errorf("failed to convert config: %w", err) } - configs[i] = config + configs[i] = *config } - return configs, nil + return &a2a.ListTaskPushConfigResponse{ + Configs: configs, + NextPageToken: resp.GetNextPageToken(), + }, nil } -func fromProtoAdditionalInterfaces(pInterfaces []*a2apb.AgentInterface) []a2a.AgentInterface { +func FromProtoListTaskPushConfigRequest(req *a2apb.ListTaskPushNotificationConfigRequest) (*a2a.ListTaskPushConfigRequest, error) { + if req == nil { + return nil, nil + } + taskID := a2a.TaskID(req.GetTaskId()) + if taskID == "" { + return nil, fmt.Errorf("task id cannot be empty") + } + return &a2a.ListTaskPushConfigRequest{ + Tenant: req.GetTenant(), + TaskID: taskID, + PageSize: int(req.GetPageSize()), + PageToken: req.GetPageToken(), + }, nil +} + +func fromProtoSupportedInterfaces(pInterfaces []*a2apb.AgentInterface) ([]a2a.AgentInterface, error) { + if pInterfaces == nil { + return nil, nil + } interfaces := make([]a2a.AgentInterface, len(pInterfaces)) for i, pIface := range pInterfaces { + url := pIface.GetUrl() + if url == "" { + return nil, fmt.Errorf("url cannot be empty") + } + pb := pIface.GetProtocolBinding() + if pb == "" { + return nil, fmt.Errorf("protocol binding cannot be empty") + } interfaces[i] = a2a.AgentInterface{ - Transport: a2a.TransportProtocol(pIface.GetTransport()), - URL: pIface.GetUrl(), + URL: url, + ProtocolBinding: a2a.TransportProtocol(pb), + Tenant: pIface.GetTenant(), + ProtocolVersion: a2a.ProtocolVersion(pIface.GetProtocolVersion()), } } - return interfaces + return interfaces, nil } -func fromProtoAgentProvider(pProvider *a2apb.AgentProvider) *a2a.AgentProvider { +func fromProtoAgentProvider(pProvider *a2apb.AgentProvider) (*a2a.AgentProvider, error) { if pProvider == nil { - return nil + return nil, nil + } + org := pProvider.GetOrganization() + if org == "" { + return nil, fmt.Errorf("organization cannot be empty") } - return &a2a.AgentProvider{Org: pProvider.GetOrganization(), URL: pProvider.GetUrl()} + url := pProvider.GetUrl() + if url == "" { + return nil, fmt.Errorf("url cannot be empty") + } + return &a2a.AgentProvider{Org: org, URL: url}, nil } func fromProtoAgentExtensions(pExtensions []*a2apb.AgentExtension) ([]a2a.AgentExtension, error) { @@ -572,6 +701,9 @@ func fromProtoAgentExtensions(pExtensions []*a2apb.AgentExtension) ([]a2a.AgentE } func fromProtoCapabilities(pCapabilities *a2apb.AgentCapabilities) (a2a.AgentCapabilities, error) { + if pCapabilities == nil { + return a2a.AgentCapabilities{}, fmt.Errorf("capabilities is nil") + } extensions, err := fromProtoAgentExtensions(pCapabilities.GetExtensions()) if err != nil { return a2a.AgentCapabilities{}, fmt.Errorf("failed to convert extensions: %w", err) @@ -580,47 +712,53 @@ func fromProtoCapabilities(pCapabilities *a2apb.AgentCapabilities) (a2a.AgentCap result := a2a.AgentCapabilities{ PushNotifications: pCapabilities.GetPushNotifications(), Streaming: pCapabilities.GetStreaming(), - StateTransitionHistory: pCapabilities.GetStateTransitionHistory(), Extensions: extensions, + ExtendedAgentCard: pCapabilities.GetExtendedAgentCard(), } return result, nil } func fromProtoOAuthFlows(pFlows *a2apb.OAuthFlows) (a2a.OAuthFlows, error) { - flows := a2a.OAuthFlows{} if pFlows == nil { - return flows, fmt.Errorf("oauth flows is nil") - } + return nil, fmt.Errorf("oauth flows is nil") + } switch f := pFlows.Flow.(type) { case *a2apb.OAuthFlows_AuthorizationCode: - flows.AuthorizationCode = &a2a.AuthorizationCodeOAuthFlow{ - AuthorizationURL: f.AuthorizationCode.GetAuthorizationUrl(), - TokenURL: f.AuthorizationCode.GetTokenUrl(), - RefreshURL: f.AuthorizationCode.GetRefreshUrl(), - Scopes: f.AuthorizationCode.GetScopes(), - } + return &a2a.AuthorizationCodeOAuthFlow{ + AuthorizationURL: f.AuthorizationCode.GetAuthorizationUrl(), + TokenURL: f.AuthorizationCode.GetTokenUrl(), + RefreshURL: f.AuthorizationCode.GetRefreshUrl(), + Scopes: f.AuthorizationCode.GetScopes(), + PKCERequired: f.AuthorizationCode.GetPkceRequired(), + },nil case *a2apb.OAuthFlows_ClientCredentials: - flows.ClientCredentials = &a2a.ClientCredentialsOAuthFlow{ - TokenURL: f.ClientCredentials.GetTokenUrl(), - RefreshURL: f.ClientCredentials.GetRefreshUrl(), - Scopes: f.ClientCredentials.GetScopes(), - } + return &a2a.ClientCredentialsOAuthFlow{ + TokenURL: f.ClientCredentials.GetTokenUrl(), + RefreshURL: f.ClientCredentials.GetRefreshUrl(), + Scopes: f.ClientCredentials.GetScopes(), + }, nil case *a2apb.OAuthFlows_Implicit: - flows.Implicit = &a2a.ImplicitOAuthFlow{ - AuthorizationURL: f.Implicit.GetAuthorizationUrl(), - RefreshURL: f.Implicit.GetRefreshUrl(), - Scopes: f.Implicit.GetScopes(), - } + return &a2a.ImplicitOAuthFlow{ + AuthorizationURL: f.Implicit.GetAuthorizationUrl(), + RefreshURL: f.Implicit.GetRefreshUrl(), + Scopes: f.Implicit.GetScopes(), + }, nil case *a2apb.OAuthFlows_Password: - flows.Password = &a2a.PasswordOAuthFlow{ - TokenURL: f.Password.GetTokenUrl(), - RefreshURL: f.Password.GetRefreshUrl(), - Scopes: f.Password.GetScopes(), - } + return &a2a.PasswordOAuthFlow{ + TokenURL: f.Password.GetTokenUrl(), + RefreshURL: f.Password.GetRefreshUrl(), + Scopes: f.Password.GetScopes(), + }, nil + case *a2apb.OAuthFlows_DeviceCode: + return &a2a.DeviceCodeOAuthFlow{ + DeviceAuthorizationURL: f.DeviceCode.GetDeviceAuthorizationUrl(), + TokenURL: f.DeviceCode.GetTokenUrl(), + RefreshURL: f.DeviceCode.GetRefreshUrl(), + Scopes: f.DeviceCode.GetScopes(), + }, nil default: - return flows, fmt.Errorf("unsupported oauth flow type: %T", f) + return nil, fmt.Errorf("unsupported oauth flow type: %T", f) } - return flows, nil } func fromProtoSecurityScheme(pScheme *a2apb.SecurityScheme) (a2a.SecurityScheme, error) { @@ -632,7 +770,7 @@ func fromProtoSecurityScheme(pScheme *a2apb.SecurityScheme) (a2a.SecurityScheme, case *a2apb.SecurityScheme_ApiKeySecurityScheme: return a2a.APIKeySecurityScheme{ Name: s.ApiKeySecurityScheme.GetName(), - In: a2a.APIKeySecuritySchemeIn(s.ApiKeySecurityScheme.GetLocation()), + Location: a2a.APIKeySecuritySchemeLocation(s.ApiKeySecurityScheme.GetLocation()), Description: s.ApiKeySecurityScheme.GetDescription(), }, nil case *a2apb.SecurityScheme_HttpAuthSecurityScheme: @@ -653,7 +791,7 @@ func fromProtoSecurityScheme(pScheme *a2apb.SecurityScheme) (a2a.SecurityScheme, } return a2a.OAuth2SecurityScheme{ Flows: flows, - Oauth2MetadataURL: s.Oauth2SecurityScheme.Oauth2MetadataUrl, + Oauth2MetadataURL: s.Oauth2SecurityScheme.GetOauth2MetadataUrl(), Description: s.Oauth2SecurityScheme.GetDescription(), }, nil case *a2apb.SecurityScheme_MtlsSecurityScheme: @@ -677,85 +815,161 @@ func fromProtoSecuritySchemes(pSchemes map[string]*a2apb.SecurityScheme) (a2a.Na return schemes, nil } -func fromProtoSecurity(pSecurity []*a2apb.Security) []a2a.SecurityRequirements { - security := make([]a2a.SecurityRequirements, len(pSecurity)) +func fromProtoSecurity(pSecurity []*a2apb.SecurityRequirement) []a2a.SecurityRequirement { + security := make([]a2a.SecurityRequirement, len(pSecurity)) for i, pSec := range pSecurity { - schemes := make(a2a.SecurityRequirements) + schemes := make(map[a2a.SecuritySchemeName]a2a.SecuritySchemeScopes) for name, scopes := range pSec.Schemes { schemes[a2a.SecuritySchemeName(name)] = scopes.GetList() } - security[i] = schemes + security[i] = a2a.SecurityRequirement{ + Schemes: schemes, + } } return security } -func fromProtoSkills(pSkills []*a2apb.AgentSkill) []a2a.AgentSkill { +func fromProtoSkills(pSkills []*a2apb.AgentSkill) ([]a2a.AgentSkill, error) { + if pSkills == nil { + return nil, nil + } skills := make([]a2a.AgentSkill, len(pSkills)) for i, pSkill := range pSkills { + id := pSkill.GetId() + if id == "" { + return nil, fmt.Errorf("skill ID cannot be empty") + } + name := pSkill.GetName() + if name == "" { + return nil, fmt.Errorf("skill name cannot be empty") + } + description := pSkill.GetDescription() + if description == "" { + return nil, fmt.Errorf("skill description cannot be empty") + } + tags := pSkill.GetTags() + if tags == nil { + return nil, fmt.Errorf("skill tags cannot be empty") + } + skills[i] = a2a.AgentSkill{ - ID: pSkill.GetId(), - Name: pSkill.GetName(), - Description: pSkill.GetDescription(), - Tags: pSkill.GetTags(), + ID: id, + Name: name, + Description: description, + Tags: tags, Examples: pSkill.GetExamples(), InputModes: pSkill.GetInputModes(), OutputModes: pSkill.GetOutputModes(), - Security: fromProtoSecurity(pSkill.GetSecurity()), + Security: fromProtoSecurity(pSkill.GetSecurityRequirements()), } } - return skills + return skills, nil } -func fromProtoAgentCardSignatures(in []*a2apb.AgentCardSignature) []a2a.AgentCardSignature { +func fromProtoAgentCardSignatures(in []*a2apb.AgentCardSignature) ([]a2a.AgentCardSignature, error) { if in == nil { - return nil + return nil, nil } out := make([]a2a.AgentCardSignature, len(in)) for i, v := range in { + protected := v.GetProtected() + if protected == "" { + return nil, fmt.Errorf("signature protected cannot be empty") + } + signature := v.GetSignature() + if signature == "" { + return nil, fmt.Errorf("signature cannot be empty") + } out[i] = a2a.AgentCardSignature{ - Protected: v.GetProtected(), - Signature: v.GetSignature(), + Protected: protected, + Signature: signature, Header: fromProtoMap(v.GetHeader()), } } - return out + return out, nil } func FromProtoAgentCard(pCard *a2apb.AgentCard) (*a2a.AgentCard, error) { if pCard == nil { return nil, nil } - + name := pCard.GetName() + if name == "" { + return nil, fmt.Errorf("agent card name cannot be empty") + } + description := pCard.GetDescription() + if description == "" { + return nil, fmt.Errorf("agent card description cannot be empty") + } + interfaces, err := fromProtoSupportedInterfaces(pCard.GetSupportedInterfaces()) + if err != nil { + return nil, fmt.Errorf("failed to convert supported interfaces: %w", err) + } + if interfaces == nil { + return nil, fmt.Errorf("supported interfaces cannot be empty") + } + provider, err := fromProtoAgentProvider(pCard.GetProvider()) + if err != nil { + return nil, fmt.Errorf("failed to convert agent provider: %w", err) + } + version := pCard.GetVersion() + if version == "" { + return nil, fmt.Errorf("agent card version cannot be empty") + } capabilities, err := fromProtoCapabilities(pCard.GetCapabilities()) if err != nil { return nil, fmt.Errorf("failed to convert agent capabilities: %w", err) } - + defaultInputModes := pCard.GetDefaultInputModes() + if defaultInputModes == nil { + return nil, fmt.Errorf("default input modes cannot be empty") + } + defaultOutputModes := pCard.GetDefaultOutputModes() + if defaultOutputModes == nil { + return nil, fmt.Errorf("default output modes cannot be empty") + } schemes, err := fromProtoSecuritySchemes(pCard.GetSecuritySchemes()) if err != nil { return nil, fmt.Errorf("failed to convert security schemes: %w", err) } + skills, err := fromProtoSkills(pCard.GetSkills()) + if err != nil { + return nil, fmt.Errorf("failed to convert skills: %w", err) + } + if skills == nil { + return nil, fmt.Errorf("skills cannot be empty") + } + signatures, err := fromProtoAgentCardSignatures(pCard.GetSignatures()) + if err != nil { + return nil, fmt.Errorf("failed to convert signatures: %w", err) + } result := &a2a.AgentCard{ - ProtocolVersion: pCard.GetProtocolVersion(), - Name: pCard.GetName(), - Description: pCard.GetDescription(), - URL: pCard.GetUrl(), - PreferredTransport: a2a.TransportProtocol(pCard.GetPreferredTransport()), - Version: pCard.GetVersion(), + Name: name, + Description: description, + SupportedInterfaces: interfaces, + Version: version, DocumentationURL: pCard.GetDocumentationUrl(), Capabilities: capabilities, - DefaultInputModes: pCard.GetDefaultInputModes(), - DefaultOutputModes: pCard.GetDefaultOutputModes(), - SupportsAuthenticatedExtendedCard: pCard.GetSupportsAuthenticatedExtendedCard(), + DefaultInputModes: defaultInputModes, + DefaultOutputModes: defaultOutputModes, SecuritySchemes: schemes, - Provider: fromProtoAgentProvider(pCard.GetProvider()), - AdditionalInterfaces: fromProtoAdditionalInterfaces(pCard.GetAdditionalInterfaces()), - Security: fromProtoSecurity(pCard.GetSecurity()), - Skills: fromProtoSkills(pCard.GetSkills()), + Provider: provider, + SecurityRequirements: fromProtoSecurity(pCard.GetSecurityRequirements()), + Skills: skills, IconURL: pCard.GetIconUrl(), - Signatures: fromProtoAgentCardSignatures(pCard.GetSignatures()), + Signatures: signatures, } return result, nil } + +func FromProtoGetExtendedAgentCardRequest(req *a2apb.GetExtendedAgentCardRequest) (*a2a.GetExtendedAgentCardRequest, error) { + if req == nil { + return nil, nil + } + + return &a2a.GetExtendedAgentCardRequest{ + Tenant: req.GetTenant(), + }, nil +} \ No newline at end of file diff --git a/a2apb/v1/pbconv/from_proto_test.go b/a2apb/v1/pbconv/from_proto_test.go index 795855de..38d847f4 100644 --- a/a2apb/v1/pbconv/from_proto_test.go +++ b/a2apb/v1/pbconv/from_proto_test.go @@ -28,7 +28,7 @@ import ( ) func TestFromProto_fromProtoPart(t *testing.T) { - pData, _ := structpb.NewStruct(map[string]any{"key": "value"}) + pData, _ := structpb.NewValue(map[string]any{"key": "value"}) tests := []struct { name string p *a2apb.Part @@ -37,79 +37,65 @@ func TestFromProto_fromProtoPart(t *testing.T) { }{ { name: "text", - p: &a2apb.Part{Part: &a2apb.Part_Text{Text: "hello"}}, - want: a2a.TextPart{Text: "hello"}, + p: &a2apb.Part{Content: &a2apb.Part_Text{Text: "hello"}}, + want: *a2a.NewTextPart("hello"), }, { name: "data", - p: &a2apb.Part{Part: &a2apb.Part_Data{Data: &a2apb.DataPart{Data: pData}}}, - want: a2a.DataPart{Data: map[string]any{"key": "value"}}, + p: &a2apb.Part{Content: &a2apb.Part_Data{Data: pData}}, + want: *a2a.NewDataPart(map[string]any{"key": "value"}), }, { name: "file with bytes", - p: &a2apb.Part{Part: &a2apb.Part_File{File: &a2apb.FilePart{ - MimeType: "text/plain", - File: &a2apb.FilePart_FileWithBytes{FileWithBytes: []byte("content")}, - }}}, - want: a2a.FilePart{ - File: a2a.FileBytes{ - FileMeta: a2a.FileMeta{ - MimeType: "text/plain", - }, - Bytes: "content", - }, + p: &a2apb.Part{Content: &a2apb.Part_Raw{Raw: []byte("content")}, MediaType: "text/plain", Filename: "Test File"}, + want: a2a.Part{ + Content: a2a.Raw([]byte("content")), + Filename: "Test File", + MediaType: "text/plain", }, }, { name: "file with uri", - p: &a2apb.Part{Part: &a2apb.Part_File{File: &a2apb.FilePart{ - MimeType: "text/plain", - Name: "example", - File: &a2apb.FilePart_FileWithUri{FileWithUri: "http://example.com/file"}, - }}}, - want: a2a.FilePart{ - File: a2a.FileURI{ - FileMeta: a2a.FileMeta{ - Name: "example", - MimeType: "text/plain", - }, - URI: "http://example.com/file", - }, + p: &a2apb.Part{Content: &a2apb.Part_Url{Url: "http://example.com/file"}, + Filename: "example", + MediaType: "text/plain", + }, + want: a2a.Part{ + Content: a2a.URL("http://example.com/file"), + Filename: "example", + MediaType: "text/plain", }, }, { name: "unsupported", - p: &a2apb.Part{Part: nil}, + p: &a2apb.Part{Content: nil}, wantErr: true, }, { name: "text with meta", p: &a2apb.Part{ - Part: &a2apb.Part_Text{Text: "hello"}, + Content: &a2apb.Part_Text{Text: "hello"}, Metadata: mustMakeProtoMetadata(t, map[string]any{"hello": "world"}), }, - want: a2a.TextPart{Text: "hello", Metadata: map[string]any{"hello": "world"}}, + want: a2a.Part{Content: a2a.Text("hello"), Metadata: map[string]any{"hello": "world"}}, }, { name: "data with meta", p: &a2apb.Part{ - Part: &a2apb.Part_Data{Data: &a2apb.DataPart{Data: pData}}, + Content: &a2apb.Part_Data{Data: pData}, Metadata: mustMakeProtoMetadata(t, map[string]any{"hello": "world"}), }, - want: a2a.DataPart{Data: map[string]any{"key": "value"}, Metadata: map[string]any{"hello": "world"}}, + want: a2a.Part{Content: a2a.Data(map[string]any{"key": "value"}), Metadata: map[string]any{"hello": "world"}}, }, { name: "file with meta", p: &a2apb.Part{ - Part: &a2apb.Part_File{File: &a2apb.FilePart{ - File: &a2apb.FilePart_FileWithBytes{FileWithBytes: []byte("content")}, - }}, + Content: &a2apb.Part_Raw{Raw: []byte("content")}, + Filename: "Test File", + MediaType: "text/plain", Metadata: mustMakeProtoMetadata(t, map[string]any{"hello": "world"}), }, - want: a2a.FilePart{ - File: a2a.FileBytes{Bytes: "content"}, - Metadata: map[string]any{"hello": "world"}, - }, + want: a2a.Part{Content: a2a.Raw([]byte("content")), Filename: "Test File", MediaType: "text/plain", Metadata: map[string]any{"hello": "world"}}, }, } @@ -165,12 +151,14 @@ func TestFromProto_fromProtoRole(t *testing.T) { func TestFromProto_fromProtoSendMessageConfig(t *testing.T) { historyLen := int32(10) + zeroHistoryLen := int32(0) a2aHistoryLen := int(historyLen) + a2aZeroHistoryLen := int(zeroHistoryLen) tests := []struct { name string in *a2apb.SendMessageConfiguration - want *a2a.MessageSendConfig + want *a2a.SendMessageConfig wantErr bool }{ { @@ -178,18 +166,18 @@ func TestFromProto_fromProtoSendMessageConfig(t *testing.T) { in: &a2apb.SendMessageConfiguration{ AcceptedOutputModes: []string{"text/plain"}, Blocking: true, - HistoryLength: historyLen, - PushNotification: &a2apb.PushNotificationConfig{ + HistoryLength: &historyLen, + PushNotificationConfig: &a2apb.PushNotificationConfig{ Id: "test-push-config", Url: "http://example.com/hook", Token: "secret", Authentication: &a2apb.AuthenticationInfo{ - Schemes: []string{"Bearer"}, + Scheme: "Bearer", Credentials: "token", }, }, }, - want: &a2a.MessageSendConfig{ + want: &a2a.SendMessageConfig{ AcceptedOutputModes: []string{"text/plain"}, Blocking: proto.Bool(true), HistoryLength: &a2aHistoryLen, @@ -198,7 +186,7 @@ func TestFromProto_fromProtoSendMessageConfig(t *testing.T) { URL: "http://example.com/hook", Token: "secret", Auth: &a2a.PushAuthInfo{ - Schemes: []string{"Bearer"}, + Scheme: "Bearer", Credentials: "token", }, }, @@ -207,16 +195,22 @@ func TestFromProto_fromProtoSendMessageConfig(t *testing.T) { { name: "config with unlimited history only", in: &a2apb.SendMessageConfiguration{ - HistoryLength: 0, }, - want: &a2a.MessageSendConfig{Blocking: proto.Bool(false)}, + want: &a2a.SendMessageConfig{Blocking: proto.Bool(false), HistoryLength: nil}, + }, + { + name: "config with zero history", + in: &a2apb.SendMessageConfiguration{ + HistoryLength: &zeroHistoryLen, + }, + want: &a2a.SendMessageConfig{Blocking: proto.Bool(false), HistoryLength: &a2aZeroHistoryLen}, }, { name: "config with no push notification", in: &a2apb.SendMessageConfiguration{ - PushNotification: nil, + PushNotificationConfig: nil, }, - want: &a2a.MessageSendConfig{Blocking: proto.Bool(false)}, + want: &a2a.SendMessageConfig{Blocking: proto.Bool(false)}, }, { name: "nil config", @@ -243,28 +237,28 @@ func TestFromProto_fromProtoSendMessageRequest(t *testing.T) { TaskId: "test-task", Role: a2apb.Role_ROLE_USER, Parts: []*a2apb.Part{ - {Part: &a2apb.Part_Text{Text: "hello"}}, + {Content: &a2apb.Part_Text{Text: "hello"}}, }, } a2aMsg := a2a.Message{ ID: "test-msg", TaskID: "test-task", Role: a2a.MessageRoleUser, - Parts: []a2a.Part{a2a.TextPart{Text: "hello"}}, + Parts: a2a.ContentParts{{Content: a2a.Text("hello")}}, } historyLen := int32(10) a2aHistoryLen := int(historyLen) pConf := &a2apb.SendMessageConfiguration{ Blocking: true, - HistoryLength: historyLen, - PushNotification: &a2apb.PushNotificationConfig{ + HistoryLength: &historyLen, + PushNotificationConfig: &a2apb.PushNotificationConfig{ Id: "push-config", Url: "http://example.com/hook", Token: "secret", }, } - a2aConf := &a2a.MessageSendConfig{ + a2aConf := &a2a.SendMessageConfig{ Blocking: proto.Bool(true), HistoryLength: &a2aHistoryLen, PushConfig: &a2a.PushConfig{ @@ -280,17 +274,17 @@ func TestFromProto_fromProtoSendMessageRequest(t *testing.T) { tests := []struct { name string req *a2apb.SendMessageRequest - want *a2a.MessageSendParams + want *a2a.SendMessageRequest wantErr bool }{ { name: "full request", req: &a2apb.SendMessageRequest{ - Request: pMsg, + Message: pMsg, Configuration: pConf, Metadata: pMeta, }, - want: &a2a.MessageSendParams{ + want: &a2a.SendMessageRequest{ Message: &a2aMsg, Config: a2aConf, Metadata: a2aMeta, @@ -298,39 +292,40 @@ func TestFromProto_fromProtoSendMessageRequest(t *testing.T) { }, { name: "missing metadata", - req: &a2apb.SendMessageRequest{Request: pMsg, Configuration: pConf}, - want: &a2a.MessageSendParams{Message: &a2aMsg, Config: a2aConf}, + req: &a2apb.SendMessageRequest{Message: pMsg, Configuration: pConf}, + want: &a2a.SendMessageRequest{Message: &a2aMsg, Config: a2aConf}, }, { name: "missing config", - req: &a2apb.SendMessageRequest{Request: pMsg, Metadata: pMeta}, - want: &a2a.MessageSendParams{Message: &a2aMsg, Metadata: a2aMeta}, + req: &a2apb.SendMessageRequest{Message: pMsg, Metadata: pMeta}, + want: &a2a.SendMessageRequest{Message: &a2aMsg, Metadata: a2aMeta}, }, { - name: "nil request message", - req: &a2apb.SendMessageRequest{}, - want: &a2a.MessageSendParams{}, + name: "nil request message", + req: &a2apb.SendMessageRequest{}, + wantErr: true, }, { name: "nil part in message", req: &a2apb.SendMessageRequest{ - Request: &a2apb.Message{ - Parts: []*a2apb.Part{{Part: nil}}, - }, + Message: &a2apb.Message{ + Parts: []*a2apb.Part{nil}}, }, wantErr: true, }, { name: "config with missing id", req: &a2apb.SendMessageRequest{ - Request: pMsg, + Message: pMsg, Configuration: &a2apb.SendMessageConfiguration{ - PushNotification: &a2apb.PushNotificationConfig{}, + PushNotificationConfig: &a2apb.PushNotificationConfig{ + Url: "http://example.com/hook", + }, }, }, - want: &a2a.MessageSendParams{ + want: &a2a.SendMessageRequest{ Message: &a2aMsg, - Config: &a2a.MessageSendConfig{PushConfig: &a2a.PushConfig{}, Blocking: proto.Bool(false)}, + Config: &a2a.SendMessageConfig{PushConfig: &a2a.PushConfig{URL: "http://example.com/hook"}, Blocking: proto.Bool(false)}, }, }, } @@ -367,23 +362,18 @@ func TestFromProto_fromProtoGetTaskRequest(t *testing.T) { tests := []struct { name string req *a2apb.GetTaskRequest - want *a2a.TaskQueryParams + want *a2a.GetTaskRequest wantErr bool }{ { name: "with history", - req: &a2apb.GetTaskRequest{Name: "tasks/test", HistoryLength: 10}, - want: &a2a.TaskQueryParams{ID: "test", HistoryLength: &historyLen}, + req: &a2apb.GetTaskRequest{Id: "test", HistoryLength: proto.Int32(int32(historyLen))}, + want: &a2a.GetTaskRequest{ID: "test", HistoryLength: &historyLen}, }, { name: "without history", - req: &a2apb.GetTaskRequest{Name: "tasks/test"}, - want: &a2a.TaskQueryParams{ID: "test"}, - }, - { - name: "invalid name", - req: &a2apb.GetTaskRequest{Name: "invalid/test"}, - wantErr: true, + req: &a2apb.GetTaskRequest{Id: "test"}, + want: &a2a.GetTaskRequest{ID: "test"}, }, } for _, tt := range tests { @@ -409,7 +399,7 @@ func TestFromProto_fromProtoListTasksRequest(t *testing.T) { }{ { name: "with pageSize", - req: &a2apb.ListTasksRequest{PageSize: 10}, + req: &a2apb.ListTasksRequest{PageSize: proto.Int32(10)}, want: &a2a.ListTasksRequest{PageSize: 10}, }, { @@ -419,23 +409,35 @@ func TestFromProto_fromProtoListTasksRequest(t *testing.T) { }, { name: "with historyLength", - req: &a2apb.ListTasksRequest{HistoryLength: 10}, + req: &a2apb.ListTasksRequest{HistoryLength: proto.Int32(10)}, want: &a2a.ListTasksRequest{HistoryLength: 10}, }, { name: "with lastUpdatedAfter", - req: &a2apb.ListTasksRequest{LastUpdatedTime: timestamppb.New(cutOffTime)}, - want: &a2a.ListTasksRequest{LastUpdatedAfter: &cutOffTime}, + req: &a2apb.ListTasksRequest{StatusTimestampAfter: timestamppb.New(cutOffTime)}, + want: &a2a.ListTasksRequest{StatusTimestampAfter: &cutOffTime}, }, { name: "with includeArtifacts", - req: &a2apb.ListTasksRequest{IncludeArtifacts: true}, + req: &a2apb.ListTasksRequest{IncludeArtifacts: proto.Bool(true)}, want: &a2a.ListTasksRequest{IncludeArtifacts: true}, }, { name: "with all filters", - req: &a2apb.ListTasksRequest{PageSize: 10, PageToken: "test", HistoryLength: 10, IncludeArtifacts: true, LastUpdatedTime: timestamppb.New(cutOffTime)}, - want: &a2a.ListTasksRequest{PageSize: 10, PageToken: "test", HistoryLength: 10, IncludeArtifacts: true, LastUpdatedAfter: &cutOffTime}, + req: &a2apb.ListTasksRequest{ + PageSize: proto.Int32(10), + PageToken: "test", + HistoryLength: proto.Int32(10), + IncludeArtifacts: proto.Bool(true), + StatusTimestampAfter: timestamppb.New(cutOffTime), + }, + want: &a2a.ListTasksRequest{ + PageSize: 10, + PageToken: "test", + HistoryLength: 10, + IncludeArtifacts: true, + StatusTimestampAfter: &cutOffTime, + }, }, { name: "without filters", @@ -469,11 +471,13 @@ func TestFromProto_fromProtoListTasksResponse(t *testing.T) { req: &a2apb.ListTasksResponse{ Tasks: []*a2apb.Task{ { - Id: string(taskID), - Status: &a2apb.TaskStatus{State: a2apb.TaskState_TASK_STATE_WORKING}, + Id: string(taskID), + ContextId: "test-context", + Status: &a2apb.TaskStatus{State: a2apb.TaskState_TASK_STATE_WORKING}, }, }, TotalSize: 1, + PageSize: 1, NextPageToken: "test", }, want: &a2a.ListTasksResponse{ @@ -483,7 +487,7 @@ func TestFromProto_fromProtoListTasksResponse(t *testing.T) { Status: a2a.TaskStatus{State: a2a.TaskStateWorking}, History: []*a2a.Message{}, Artifacts: []*a2a.Artifact{}, - ContextID: "", + ContextID: "test-context", }, }, TotalSize: 1, @@ -518,50 +522,42 @@ func TestFromProto_fromProtoCreateTaskPushConfigRequest(t *testing.T) { tests := []struct { name string req *a2apb.CreateTaskPushNotificationConfigRequest - want *a2a.TaskPushConfig + want *a2a.CreateTaskPushConfigRequest wantErr bool }{ { name: "success", req: &a2apb.CreateTaskPushNotificationConfigRequest{ - Parent: "tasks/test", - Config: &a2apb.TaskPushNotificationConfig{ - PushNotificationConfig: &a2apb.PushNotificationConfig{Id: "test-config"}, + TaskId: "test-task", + Config: &a2apb.PushNotificationConfig{ + Url: "http://example.com/hook", + Id: "test-config", }, }, - want: &a2a.TaskPushConfig{TaskID: "test", Config: a2a.PushConfig{ID: "test-config"}}, + want: &a2a.CreateTaskPushConfigRequest{TaskID: "test-task", Config: a2a.PushConfig{ID: "test-config", URL: "http://example.com/hook"}}, }, { name: "nil config", req: &a2apb.CreateTaskPushNotificationConfigRequest{ - Parent: "tasks/test", + TaskId: "test", }, wantErr: true, }, { name: "nil push config", req: &a2apb.CreateTaskPushNotificationConfigRequest{ - Parent: "tasks/test", - Config: &a2apb.TaskPushNotificationConfig{}, - }, - wantErr: true, - }, - { - name: "bad parent", - req: &a2apb.CreateTaskPushNotificationConfigRequest{ - Parent: "foo/bar", + TaskId: "test", + Config: &a2apb.PushNotificationConfig{}, }, wantErr: true, }, { name: "empty optional ID conversion push config conversion", req: &a2apb.CreateTaskPushNotificationConfigRequest{ - Parent: "tasks/t1", - Config: &a2apb.TaskPushNotificationConfig{ - PushNotificationConfig: &a2apb.PushNotificationConfig{Id: ""}, - }, + TaskId: "t1", + Config: &a2apb.PushNotificationConfig{Id: "", Url: "http://example.com/hook"}, }, - want: &a2a.TaskPushConfig{TaskID: "t1", Config: a2a.PushConfig{}}, + want: &a2a.CreateTaskPushConfigRequest{TaskID: "t1", Config: a2a.PushConfig{URL: "http://example.com/hook"}}, }, } for _, tt := range tests { @@ -582,32 +578,19 @@ func TestFromProto_fromProtoGetTaskPushConfigRequest(t *testing.T) { tests := []struct { name string req *a2apb.GetTaskPushNotificationConfigRequest - want *a2a.GetTaskPushConfigParams + want *a2a.GetTaskPushConfigRequest wantErr bool }{ { name: "success", req: &a2apb.GetTaskPushNotificationConfigRequest{ - Name: "tasks/test-task/pushNotificationConfigs/test-config", - }, - want: &a2a.GetTaskPushConfigParams{ - TaskID: "test-task", - ConfigID: "test-config", + TaskId: "test-task", + Id: "test-config", }, - }, - { - name: "bad keyword for task id", - req: &a2apb.GetTaskPushNotificationConfigRequest{ - Name: "foo/test-task/pushNotificationConfigs/test-config", - }, - wantErr: true, - }, - { - name: "bad keyword for config id", - req: &a2apb.GetTaskPushNotificationConfigRequest{ - Name: "tasks/test-task/bar/test-config", + want: &a2a.GetTaskPushConfigRequest{ + TaskID: "test-task", + ID: "test-config", }, - wantErr: true, }, } for _, tt := range tests { @@ -628,32 +611,19 @@ func TestFromProto_fromProtoDeleteTaskPushConfigRequest(t *testing.T) { tests := []struct { name string req *a2apb.DeleteTaskPushNotificationConfigRequest - want *a2a.DeleteTaskPushConfigParams + want *a2a.DeleteTaskPushConfigRequest wantErr bool }{ { name: "success", req: &a2apb.DeleteTaskPushNotificationConfigRequest{ - Name: "tasks/test-task/pushNotificationConfigs/test-config", - }, - want: &a2a.DeleteTaskPushConfigParams{ - TaskID: "test-task", - ConfigID: "test-config", + TaskId: "test-task", + Id: "test-config", }, - }, - { - name: "bad keyword for task id", - req: &a2apb.DeleteTaskPushNotificationConfigRequest{ - Name: "foo/test-task/pushNotificationConfigs/test-config", + want: &a2a.DeleteTaskPushConfigRequest{ + TaskID: "test-task", + ID: "test-config", }, - wantErr: true, - }, - { - name: "bad keyword for config id", - req: &a2apb.DeleteTaskPushNotificationConfigRequest{ - Name: "tasks/test-task/bar/test-config", - }, - wantErr: true, }, } for _, tt := range tests { diff --git a/a2apb/v1/pbconv/id_codec.go b/a2apb/v1/pbconv/id_codec.go deleted file mode 100644 index 1f127270..00000000 --- a/a2apb/v1/pbconv/id_codec.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2025 The A2A Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package pbconv - -import ( - "fmt" - "regexp" - - "github.com/a2aproject/a2a-go/a2a" -) - -var ( - taskIDRegex = regexp.MustCompile(`tasks/([^/]+)`) - configIDRegex = regexp.MustCompile(`pushNotificationConfigs/([^/]*)`) -) - -func ExtractTaskID(name string) (a2a.TaskID, error) { - matches := taskIDRegex.FindStringSubmatch(name) - if len(matches) < 2 { - return "", fmt.Errorf("invalid or missing task ID in name: %q", name) - } - return a2a.TaskID(matches[1]), nil -} - -func MakeTaskName(taskID a2a.TaskID) string { - return "tasks/" + string(taskID) -} - -func ExtractConfigID(name string) (string, error) { - matches := configIDRegex.FindStringSubmatch(name) - if len(matches) < 2 { - return "", fmt.Errorf("invalid or missing config ID in name: %q", name) - } - return matches[1], nil -} - -func MakeConfigName(taskID a2a.TaskID, configID string) string { - return MakeTaskName(taskID) + "/pushNotificationConfigs/" + configID -} diff --git a/a2apb/v1/pbconv/id_codec_test.go b/a2apb/v1/pbconv/id_codec_test.go deleted file mode 100644 index 58bf8570..00000000 --- a/a2apb/v1/pbconv/id_codec_test.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2025 The A2A Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package pbconv - -import ( - "testing" - - "github.com/a2aproject/a2a-go/a2a" -) - -func TestPathExtractors(t *testing.T) { - t.Run("extractTaskID", func(t *testing.T) { - tests := []struct { - name string - path string - want a2a.TaskID - wantErr bool - }{ - { - name: "simple path", - path: "tasks/12345", - want: "12345", - }, - { - name: "complex path", - path: "projects/p/locations/l/tasks/abc-def", - want: "abc-def", - }, - { - name: "missing value", - path: "tasks/", - wantErr: true, - }, - { - name: "missing keyword in path", - path: "configs/123", - wantErr: true, - }, - { - name: "empty path", - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ExtractTaskID(tt.path) - if (err != nil) != tt.wantErr { - t.Errorf("extractTaskID() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("extractTaskID() = %v, want %v", got, tt.want) - } - }) - } - }) - - t.Run("extractConfigID", func(t *testing.T) { - tests := []struct { - name string - path string - want string - wantErr bool - }{ - { - name: "simple path", - path: "pushNotificationConfigs/abc-123", - want: "abc-123", - }, - { - name: "complex path", - path: "tasks/12345/pushNotificationConfigs/abc-123", - want: "abc-123", - }, - { - name: "missing value", // push notification config ID is optional - path: "pushNotificationConfigs/", - want: "", - }, - { - name: "missing keyword in path", - path: "tasks/123", - wantErr: true, - }, - { - name: "empty path", - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ExtractConfigID(tt.path) - if (err != nil) != tt.wantErr { - t.Errorf("extractConfigID() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("extractConfigID() = %v, want %v", got, tt.want) - } - }) - } - }) -} diff --git a/a2apb/v1/pbconv/to_proto.go b/a2apb/v1/pbconv/to_proto.go index 1215dbda..f747edc6 100644 --- a/a2apb/v1/pbconv/to_proto.go +++ b/a2apb/v1/pbconv/to_proto.go @@ -19,6 +19,7 @@ import ( "github.com/a2aproject/a2a-go/a2a" "github.com/a2aproject/a2a-go/a2apb/v1" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -34,54 +35,68 @@ func toProtoMap(meta map[string]any) (*structpb.Struct, error) { return s, nil } -func ToProtoSendMessageRequest(params *a2a.MessageSendParams) (*a2apb.SendMessageRequest, error) { - if params == nil { +func ToProtoSendMessageRequest(req *a2a.SendMessageRequest) (*a2apb.SendMessageRequest, error) { + // TODO: add validation + if req == nil { return nil, nil } - pMsg, err := toProtoMessage(params.Message) + pMsg, err := toProtoMessage(req.Message) if err != nil { return nil, err } - pConf, err := toProtoSendMessageConfig(params.Config) + pConf, err := toProtoSendMessageConfig(req.Config) if err != nil { return nil, err } - pMeta, err := toProtoMap(params.Metadata) + pMeta, err := toProtoMap(req.Metadata) if err != nil { return nil, fmt.Errorf("failed to convert metadata to proto struct: %w", err) } - req := &a2apb.SendMessageRequest{ - Request: pMsg, + return &a2apb.SendMessageRequest{ + Tenant: req.Tenant, + Message: pMsg, Configuration: pConf, Metadata: pMeta, - } - return req, nil + }, nil } func toProtoPushConfig(config *a2a.PushConfig) (*a2apb.PushNotificationConfig, error) { + // TODO: add validation if config == nil { return nil, nil } + auth, err := toProtoAuthenticationInfo(config.Auth) + if err != nil { + return nil, fmt.Errorf("failed to convert authentication info: %w", err) + } + pConf := &a2apb.PushNotificationConfig{ - Id: config.ID, - Url: config.URL, - Token: config.Token, - } - if config.Auth != nil { - pConf.Authentication = &a2apb.AuthenticationInfo{ - Schemes: config.Auth.Schemes, - Credentials: config.Auth.Credentials, - } + Id: config.ID, + Url: config.URL, + Token: config.Token, + Authentication: auth, } + return pConf, nil } -func toProtoSendMessageConfig(config *a2a.MessageSendConfig) (*a2apb.SendMessageConfiguration, error) { +func toProtoAuthenticationInfo(auth *a2a.PushAuthInfo) (*a2apb.AuthenticationInfo, error) { + // TODO: add validation + if auth == nil { + return nil, nil + } + return &a2apb.AuthenticationInfo{ + Scheme: auth.Scheme, + Credentials: auth.Credentials, + }, nil +} + +func toProtoSendMessageConfig(config *a2a.SendMessageConfig) (*a2apb.SendMessageConfiguration, error) { if config == nil { return nil, nil } @@ -93,74 +108,91 @@ func toProtoSendMessageConfig(config *a2a.MessageSendConfig) (*a2apb.SendMessage pConf := &a2apb.SendMessageConfiguration{ AcceptedOutputModes: config.AcceptedOutputModes, - PushNotification: pushConf, + PushNotificationConfig: pushConf, } if config.Blocking != nil { pConf.Blocking = *config.Blocking } if config.HistoryLength != nil { - pConf.HistoryLength = int32(*config.HistoryLength) + pConf.HistoryLength = proto.Int32(int32(*config.HistoryLength)) } return pConf, nil } -func ToProtoGetTaskRequest(params *a2a.TaskQueryParams) (*a2apb.GetTaskRequest, error) { - if params == nil { +func ToProtoGetTaskRequest(req *a2a.GetTaskRequest) (*a2apb.GetTaskRequest, error) { + // TODO: add validation + if req == nil { return nil, nil } - req := &a2apb.GetTaskRequest{Name: MakeTaskName(params.ID)} - if params.HistoryLength != nil { - req.HistoryLength = int32(*params.HistoryLength) - } - return req, nil + return &a2apb.GetTaskRequest{ + Tenant: req.Tenant, + Id: string(req.ID), + HistoryLength: proto.Int32(int32(*req.HistoryLength)), + }, nil } -func ToProtoCancelTaskRequest(params *a2a.TaskIDParams) (*a2apb.CancelTaskRequest, error) { - if params == nil { +func ToProtoCancelTaskRequest(req *a2a.CancelTaskRequest) (*a2apb.CancelTaskRequest, error) { + // TODO: add validation + if req == nil { return nil, nil } - return &a2apb.CancelTaskRequest{Name: MakeTaskName(params.ID)}, nil + return &a2apb.CancelTaskRequest{ + Tenant: req.Tenant, + Id: string(req.ID), + }, nil } -func ToProtoTaskSubscriptionRequest(params *a2a.TaskIDParams) (*a2apb.TaskSubscriptionRequest, error) { - if params == nil { +func ToProtoSubscribeToTaskRequest(req *a2a.SubscribeToTaskRequest) (*a2apb.SubscribeToTaskRequest, error) { + // TODO: add validation + if req == nil { return nil, nil } - return &a2apb.TaskSubscriptionRequest{Name: MakeTaskName(params.ID)}, nil + return &a2apb.SubscribeToTaskRequest{ + Tenant: req.Tenant, + Id: string(req.ID), + }, nil } -func ToProtoCreateTaskPushConfigRequest(config *a2a.TaskPushConfig) (*a2apb.CreateTaskPushNotificationConfigRequest, error) { +func ToProtoCreateTaskPushConfigRequest(config *a2a.CreateTaskPushConfigRequest) (*a2apb.CreateTaskPushNotificationConfigRequest, error) { + // TODO: add validation if config == nil { return nil, nil } - pnc, err := toProtoPushConfig(&config.Config) + pConfig, err := toProtoPushConfig(&config.Config) if err != nil { return nil, fmt.Errorf("failed to convert push config: %w", err) } return &a2apb.CreateTaskPushNotificationConfigRequest{ - Parent: MakeTaskName(config.TaskID), - Config: &a2apb.TaskPushNotificationConfig{PushNotificationConfig: pnc}, + Tenant: config.Tenant, + TaskId: string(config.TaskID), + Config: pConfig, }, nil } -func ToProtoGetTaskPushConfigRequest(params *a2a.GetTaskPushConfigParams) (*a2apb.GetTaskPushNotificationConfigRequest, error) { - if params == nil { +func ToProtoGetTaskPushConfigRequest(req *a2a.GetTaskPushConfigRequest) (*a2apb.GetTaskPushNotificationConfigRequest, error) { + // TODO: add validation + if req == nil { return nil, nil } return &a2apb.GetTaskPushNotificationConfigRequest{ - Name: MakeConfigName(params.TaskID, params.ConfigID), + Tenant: req.Tenant, + TaskId: string(req.TaskID), + Id: string(req.ID), }, nil } -func ToProtoDeleteTaskPushConfigRequest(params *a2a.DeleteTaskPushConfigParams) (*a2apb.DeleteTaskPushNotificationConfigRequest, error) { - if params == nil { +func ToProtoDeleteTaskPushConfigRequest(req *a2a.DeleteTaskPushConfigRequest) (*a2apb.DeleteTaskPushNotificationConfigRequest, error) { + // TODO: add validation + if req == nil { return nil, nil } return &a2apb.DeleteTaskPushNotificationConfigRequest{ - Name: MakeConfigName(params.TaskID, params.ConfigID), + Tenant: req.Tenant, + TaskId: string(req.TaskID), + Id: string(req.ID), }, nil } @@ -172,7 +204,7 @@ func ToProtoSendMessageResponse(result a2a.SendMessageResult) (*a2apb.SendMessag if err != nil { return nil, err } - resp.Payload = &a2apb.SendMessageResponse_Msg{Msg: pMsg} + resp.Payload = &a2apb.SendMessageResponse_Message{Message: pMsg} case *a2a.Task: pTask, err := ToProtoTask(r) if err != nil { @@ -193,7 +225,7 @@ func ToProtoStreamResponse(event a2a.Event) (*a2apb.StreamResponse, error) { if err != nil { return nil, err } - resp.Payload = &a2apb.StreamResponse_Msg{Msg: pMsg} + resp.Payload = &a2apb.StreamResponse_Message{Message: pMsg} case *a2a.Task: pTask, err := ToProtoTask(e) if err != nil { @@ -211,7 +243,6 @@ func ToProtoStreamResponse(event a2a.Event) (*a2apb.StreamResponse, error) { } resp.Payload = &a2apb.StreamResponse_StatusUpdate{StatusUpdate: &a2apb.TaskStatusUpdateEvent{ ContextId: e.ContextID, - Final: e.Final, Status: pStatus, TaskId: string(e.TaskID), Metadata: metadata, @@ -241,6 +272,7 @@ func ToProtoStreamResponse(event a2a.Event) (*a2apb.StreamResponse, error) { } func toProtoMessage(msg *a2a.Message) (*a2apb.Message, error) { + // TODO: add validation if msg == nil { return nil, nil } @@ -286,71 +318,49 @@ func toProtoMessages(msgs []*a2a.Message) ([]*a2apb.Message, error) { return pMsgs, nil } -func toProtoFilePart(part a2a.FilePart) (*a2apb.Part, error) { - meta, err := toProtoMap(part.Metadata) +func toProtoDataPart(part a2a.Data) (*a2apb.Part_Data, error) { + s, err := toProtoMap(part) if err != nil { - return nil, err - } - switch fc := part.File.(type) { - case a2a.FileBytes: - return &a2apb.Part{ - Part: &a2apb.Part_File{File: &a2apb.FilePart{ - MimeType: fc.MimeType, - Name: fc.Name, - File: &a2apb.FilePart_FileWithBytes{FileWithBytes: []byte(fc.Bytes)}, - }}, - Metadata: meta, - }, nil - case a2a.FileURI: - return &a2apb.Part{ - Part: &a2apb.Part_File{File: &a2apb.FilePart{ - MimeType: fc.MimeType, - Name: fc.Name, - File: &a2apb.FilePart_FileWithUri{FileWithUri: fc.URI}, - }}, - Metadata: meta, - }, nil - default: - return nil, fmt.Errorf("unsupported FilePartContent type: %T", fc) + return nil, fmt.Errorf("failed to convert data to proto struct: %w", err) } + return &a2apb.Part_Data{Data: structpb.NewStructValue(s)}, nil } -func toProtoDataPart(part a2a.DataPart) (*a2apb.Part, error) { - s, err := toProtoMap(part.Data) - if err != nil { - return nil, fmt.Errorf("failed to convert data to proto struct: %w", err) - } +func toProtoPart(part a2a.Part) (*a2apb.Part, error) { + // TODO: add validation meta, err := toProtoMap(part.Metadata) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert metadata to proto struct: %w", err) } - return &a2apb.Part{ - Part: &a2apb.Part_Data{Data: &a2apb.DataPart{Data: s}}, + + pPart := &a2apb.Part{ Metadata: meta, - }, nil -} + Filename: part.Filename, + MediaType: part.MediaType, + } -func toProtoPart(part a2a.Part) (*a2apb.Part, error) { - switch p := part.(type) { - case a2a.TextPart: - meta, err := toProtoMap(p.Metadata) + switch content := part.Content.(type) { + case a2a.Text: + pPart.Content = &a2apb.Part_Text{Text: string(content)} + case a2a.Raw: + pPart.Content = &a2apb.Part_Raw{Raw: content} + case a2a.Data: + pPart.Content, err = toProtoDataPart(content) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert data to proto struct: %w", err) } - return &a2apb.Part{Part: &a2apb.Part_Text{Text: p.Text}, Metadata: meta}, nil - case a2a.DataPart: - return toProtoDataPart(p) - case a2a.FilePart: - return toProtoFilePart(p) + case a2a.URL: + pPart.Content = &a2apb.Part_Url{Url: string(content)} default: - return nil, fmt.Errorf("unsupported part type: %T", p) + return nil, fmt.Errorf("unsupported part type: %T", content) } + return pPart, nil } -func toProtoParts(parts []a2a.Part) ([]*a2apb.Part, error) { +func toProtoParts(parts a2a.ContentParts) ([]*a2apb.Part, error) { pParts := make([]*a2apb.Part, len(parts)) for i, part := range parts { - pPart, err := toProtoPart(part) + pPart, err := toProtoPart(*part) if err != nil { return nil, fmt.Errorf("failed to convert part: %w", err) } @@ -375,7 +385,7 @@ func toProtoTaskState(state a2a.TaskState) a2apb.TaskState { case a2a.TaskStateAuthRequired: return a2apb.TaskState_TASK_STATE_AUTH_REQUIRED case a2a.TaskStateCanceled: - return a2apb.TaskState_TASK_STATE_CANCELLED + return a2apb.TaskState_TASK_STATE_CANCELED case a2a.TaskStateCompleted: return a2apb.TaskState_TASK_STATE_COMPLETED case a2a.TaskStateFailed: @@ -400,8 +410,8 @@ func toProtoTaskStatus(status a2a.TaskStatus) (*a2apb.TaskStatus, error) { } pStatus := &a2apb.TaskStatus{ - State: toProtoTaskState(status.State), - Update: message, + State: toProtoTaskState(status.State), + Message: message, } if status.Timestamp != nil { pStatus.Timestamp = timestamppb.New(*status.Timestamp) @@ -411,6 +421,7 @@ func toProtoTaskStatus(status a2a.TaskStatus) (*a2apb.TaskStatus, error) { } func toProtoArtifact(artifact *a2a.Artifact) (*a2apb.Artifact, error) { + // TODO: add validation if artifact == nil { return nil, nil } @@ -450,6 +461,7 @@ func toProtoArtifacts(artifacts []*a2a.Artifact) ([]*a2apb.Artifact, error) { } func ToProtoTask(task *a2a.Task) (*a2apb.Task, error) { + // TODO: add validation if task == nil { return nil, nil } @@ -491,21 +503,26 @@ func ToProtoListTasksRequest(request *a2a.ListTasksRequest) (*a2apb.ListTasksReq } var lastUpdatedAfter *timestamppb.Timestamp - if request.LastUpdatedAfter != nil { - lastUpdatedAfter = timestamppb.New(*request.LastUpdatedAfter) + if request.StatusTimestampAfter != nil { + lastUpdatedAfter = timestamppb.New(*request.StatusTimestampAfter) } + pageSize := int32(request.PageSize) + historyLength := proto.Int32(int32(request.HistoryLength)) + return &a2apb.ListTasksRequest{ - ContextId: request.ContextID, - Status: toProtoTaskState(request.Status), - PageSize: int32(request.PageSize), - PageToken: request.PageToken, - HistoryLength: int32(request.HistoryLength), - LastUpdatedTime: lastUpdatedAfter, - IncludeArtifacts: request.IncludeArtifacts, + Tenant: request.Tenant, + ContextId: request.ContextID, + Status: toProtoTaskState(request.Status), + PageSize: &pageSize, + PageToken: request.PageToken, + HistoryLength: historyLength, + StatusTimestampAfter: lastUpdatedAfter, + IncludeArtifacts: proto.Bool(request.IncludeArtifacts), }, nil } func ToProtoListTasksResponse(response *a2a.ListTasksResponse) (*a2apb.ListTasksResponse, error) { + // TODO: add validation if response == nil { return nil, nil } @@ -521,12 +538,14 @@ func ToProtoListTasksResponse(response *a2a.ListTasksResponse) (*a2apb.ListTasks result := &a2apb.ListTasksResponse{ Tasks: tasks, TotalSize: int32(response.TotalSize), + PageSize: int32(response.PageSize), NextPageToken: response.NextPageToken, } return result, nil } func ToProtoTaskPushConfig(config *a2a.TaskPushConfig) (*a2apb.TaskPushNotificationConfig, error) { + // TODO: add validation if config == nil { return nil, nil } @@ -541,45 +560,60 @@ func ToProtoTaskPushConfig(config *a2a.TaskPushConfig) (*a2apb.TaskPushNotificat } return &a2apb.TaskPushNotificationConfig{ - Name: MakeConfigName(config.TaskID, pConfig.GetId()), + Tenant: config.Tenant, + Id: string(config.ID), + TaskId: string(config.TaskID), PushNotificationConfig: pConfig, }, nil } -func ToProtoListTaskPushConfig(configs []*a2a.TaskPushConfig) (*a2apb.ListTaskPushNotificationConfigResponse, error) { - pConfigs := make([]*a2apb.TaskPushNotificationConfig, len(configs)) - for i, config := range configs { - pConfig, err := ToProtoTaskPushConfig(config) +func ToProtoListTaskPushConfigResponse(req *a2a.ListTaskPushConfigResponse) (*a2apb.ListTaskPushNotificationConfigResponse, error) { + if req == nil { + return nil, nil + } + configs := make([]*a2apb.TaskPushNotificationConfig, len(req.Configs)) + for i, config := range req.Configs { + configProto, err := ToProtoTaskPushConfig(&config) if err != nil { return nil, fmt.Errorf("failed to convert config: %w", err) } - pConfigs[i] = pConfig + configs[i] = configProto } return &a2apb.ListTaskPushNotificationConfigResponse{ - Configs: pConfigs, - NextPageToken: "", // todo: add pagination + Configs: configs, + NextPageToken: req.NextPageToken, }, nil } -func ToProtoListTaskPushConfigRequest(req *a2a.ListTaskPushConfigParams) (*a2apb.ListTaskPushNotificationConfigRequest, error) { +func ToProtoListTaskPushConfigRequest(req *a2a.ListTaskPushConfigRequest) (*a2apb.ListTaskPushNotificationConfigRequest, error) { + // TODO: add validation if req == nil { return nil, nil } - return &a2apb.ListTaskPushNotificationConfigRequest{Parent: MakeTaskName(req.TaskID)}, nil + return &a2apb.ListTaskPushNotificationConfigRequest{ + Tenant: req.Tenant, + TaskId: string(req.TaskID), + PageSize: int32(req.PageSize), + PageToken: req.PageToken, + }, nil } -func toProtoAdditionalInterfaces(interfaces []a2a.AgentInterface) []*a2apb.AgentInterface { +func toProtoSupportedInterfaces(interfaces []a2a.AgentInterface) []*a2apb.AgentInterface { + // TODO: add validation pInterfaces := make([]*a2apb.AgentInterface, len(interfaces)) for i, iface := range interfaces { pInterfaces[i] = &a2apb.AgentInterface{ - Transport: string(iface.Transport), - Url: iface.URL, + Url: iface.URL, + ProtocolBinding: string(iface.ProtocolBinding), + Tenant: iface.Tenant, + ProtocolVersion: string(iface.ProtocolVersion), } } return pInterfaces } func toProtoAgentProvider(provider *a2a.AgentProvider) *a2apb.AgentProvider { + // TODO: add validation if provider == nil { return nil } @@ -604,16 +638,17 @@ func toProtoAgentExtensions(extensions []a2a.AgentExtension) ([]*a2apb.AgentExte } func toProtoCapabilities(capabilities a2a.AgentCapabilities) (*a2apb.AgentCapabilities, error) { + // TODO: add validation extensions, err := toProtoAgentExtensions(capabilities.Extensions) if err != nil { return nil, fmt.Errorf("failed to convert extensions: %w", err) } result := &a2apb.AgentCapabilities{ - PushNotifications: capabilities.PushNotifications, - Streaming: capabilities.Streaming, - StateTransitionHistory: capabilities.StateTransitionHistory, + Streaming: proto.Bool(capabilities.Streaming), + PushNotifications: proto.Bool(capabilities.PushNotifications), Extensions: extensions, + ExtendedAgentCard: proto.Bool(capabilities.ExtendedAgentCard), } return result, nil } @@ -667,20 +702,32 @@ func toProtoPasswordOAuthFlows(f *a2a.PasswordOAuthFlow) *a2apb.OAuthFlows { } } +func toProtoDeviceCodeOAuthFlow(f *a2a.DeviceCodeOAuthFlow) *a2apb.OAuthFlows { + return &a2apb.OAuthFlows{ + Flow: &a2apb.OAuthFlows_DeviceCode{ + DeviceCode: &a2apb.DeviceCodeOAuthFlow{ + DeviceAuthorizationUrl: f.DeviceAuthorizationURL, + TokenUrl: f.TokenURL, + RefreshUrl: f.RefreshURL, + Scopes: f.Scopes, + }, + }, + } +} + func toProtoOAuthFlows(flows a2a.OAuthFlows) (*a2apb.OAuthFlows, error) { var result []*a2apb.OAuthFlows - - if flows.AuthorizationCode != nil { - result = append(result, toProtoAuthzOAuthCodeFlow(flows.AuthorizationCode)) - } - if flows.ClientCredentials != nil { - result = append(result, toProtoCredentialsOAuthFlow(flows.ClientCredentials)) - } - if flows.Implicit != nil { - result = append(result, toProtoImplicitOAuthFlow(flows.Implicit)) - } - if flows.Password != nil { - result = append(result, toProtoPasswordOAuthFlows(flows.Password)) + switch f := flows.(type) { + case *a2a.AuthorizationCodeOAuthFlow: + result = append(result, toProtoAuthzOAuthCodeFlow(f)) + case *a2a.ClientCredentialsOAuthFlow: + result = append(result, toProtoCredentialsOAuthFlow(f)) + case *a2a.ImplicitOAuthFlow: + result = append(result, toProtoImplicitOAuthFlow(f)) + case *a2a.PasswordOAuthFlow: + result = append(result, toProtoPasswordOAuthFlows(f)) + case *a2a.DeviceCodeOAuthFlow: + result = append(result, toProtoDeviceCodeOAuthFlow(f)) } if len(result) == 0 { @@ -701,7 +748,7 @@ func toProtoSecurityScheme(scheme a2a.SecurityScheme) (*a2apb.SecurityScheme, er Scheme: &a2apb.SecurityScheme_ApiKeySecurityScheme{ ApiKeySecurityScheme: &a2apb.APIKeySecurityScheme{ Name: s.Name, - Location: string(s.In), + Location: string(s.Location), Description: s.Description, }, }, @@ -734,15 +781,17 @@ func toProtoSecurityScheme(scheme a2a.SecurityScheme) (*a2apb.SecurityScheme, er }, }, nil case a2a.OAuth2SecurityScheme: - flows, err := toProtoOAuthFlows(s.Flows) + var pFlows *a2apb.OAuthFlows + var err error + pFlows, err = toProtoOAuthFlows(s.Flows) if err != nil { - return nil, fmt.Errorf("failed to convert OAuthFlows: %w", err) + return nil, fmt.Errorf("failed to convert OAuth flows: %w", err) } return &a2apb.SecurityScheme{ Scheme: &a2apb.SecurityScheme_Oauth2SecurityScheme{ Oauth2SecurityScheme: &a2apb.OAuth2SecurityScheme{ - Flows: flows, Description: s.Description, + Flows: pFlows, Oauth2MetadataUrl: s.Oauth2MetadataURL, }, }, @@ -766,36 +815,38 @@ func toProtoSecuritySchemes(schemes a2a.NamedSecuritySchemes) (map[string]*a2apb return pSchemes, nil } -func toProtoSecurity(security []a2a.SecurityRequirements) []*a2apb.Security { - pSecurity := make([]*a2apb.Security, len(security)) +func toProtoSecurity(security []a2a.SecurityRequirement) []*a2apb.SecurityRequirement { + pSecurity := make([]*a2apb.SecurityRequirement, len(security)) for i, sec := range security { pSchemes := make(map[string]*a2apb.StringList) - for name, scopes := range sec { + for name, scopes := range sec.Schemes { pSchemes[string(name)] = &a2apb.StringList{List: scopes} } - pSecurity[i] = &a2apb.Security{Schemes: pSchemes} + pSecurity[i] = &a2apb.SecurityRequirement{Schemes: pSchemes} } return pSecurity } func toProtoSkills(skills []a2a.AgentSkill) []*a2apb.AgentSkill { + // TODO: add validation pSkills := make([]*a2apb.AgentSkill, len(skills)) for i, skill := range skills { pSkills[i] = &a2apb.AgentSkill{ - Id: skill.ID, - Name: skill.Name, - Description: skill.Description, - Tags: skill.Tags, - Examples: skill.Examples, - InputModes: skill.InputModes, - OutputModes: skill.OutputModes, - Security: toProtoSecurity(skill.Security), + Id: skill.ID, + Name: skill.Name, + Description: skill.Description, + Tags: skill.Tags, + Examples: skill.Examples, + InputModes: skill.InputModes, + OutputModes: skill.OutputModes, + SecurityRequirements: toProtoSecurity(skill.Security), } } return pSkills } func toProtoAgentCardSignatures(in []a2a.AgentCardSignature) ([]*a2apb.AgentCardSignature, error) { + // TODO: add validation if in == nil { return nil, nil } @@ -815,6 +866,7 @@ func toProtoAgentCardSignatures(in []a2a.AgentCardSignature) ([]*a2apb.AgentCard } func ToProtoAgentCard(card *a2a.AgentCard) (*a2apb.AgentCard, error) { + // TODO: add validation if card == nil { return nil, nil } @@ -835,25 +887,31 @@ func ToProtoAgentCard(card *a2a.AgentCard) (*a2apb.AgentCard, error) { } result := &a2apb.AgentCard{ - ProtocolVersion: card.ProtocolVersion, Name: card.Name, Description: card.Description, - Url: card.URL, - PreferredTransport: string(card.PreferredTransport), + SupportedInterfaces: toProtoSupportedInterfaces(card.SupportedInterfaces), + Provider: toProtoAgentProvider(card.Provider), Version: card.Version, - DocumentationUrl: card.DocumentationURL, + DocumentationUrl: &card.DocumentationURL, Capabilities: capabilities, + SecuritySchemes: schemes, + SecurityRequirements: toProtoSecurity(card.SecurityRequirements), DefaultInputModes: card.DefaultInputModes, DefaultOutputModes: card.DefaultOutputModes, - SupportsAuthenticatedExtendedCard: card.SupportsAuthenticatedExtendedCard, - SecuritySchemes: schemes, - Provider: toProtoAgentProvider(card.Provider), - AdditionalInterfaces: toProtoAdditionalInterfaces(card.AdditionalInterfaces), - Security: toProtoSecurity(card.Security), Skills: toProtoSkills(card.Skills), - IconUrl: card.IconURL, Signatures: signatures, + IconUrl: &card.IconURL, } return result, nil } + +func ToProtoGetExtendedAgentCardRequest(req *a2a.GetExtendedAgentCardRequest) (*a2apb.GetExtendedAgentCardRequest, error) { + if req == nil { + return nil, nil + } + + return &a2apb.GetExtendedAgentCardRequest{ + Tenant: req.Tenant, + }, nil +} diff --git a/a2apb/v1/pbconv/to_proto_test.go b/a2apb/v1/pbconv/to_proto_test.go index 4cfec802..7abf9a7d 100644 --- a/a2apb/v1/pbconv/to_proto_test.go +++ b/a2apb/v1/pbconv/to_proto_test.go @@ -52,7 +52,7 @@ func TestToProto_toProtoMessage(t *testing.T) { TaskID: "test-task", Role: a2a.MessageRoleUser, ReferenceTasks: []a2a.TaskID{"task-123"}, - Parts: []a2a.Part{a2a.TextPart{Text: "hello"}}, + Parts: a2a.ContentParts{{Content: a2a.Text("hello")}}, Metadata: a2aMeta, }, want: &a2apb.Message{ @@ -61,7 +61,7 @@ func TestToProto_toProtoMessage(t *testing.T) { TaskId: "test-task", ReferenceTaskIds: []string{"task-123"}, Role: a2apb.Role_ROLE_USER, - Parts: []*a2apb.Part{{Part: &a2apb.Part_Text{Text: "hello"}}}, + Parts: []*a2apb.Part{{Content: &a2apb.Part_Text{Text: "hello"}}}, Metadata: pMeta, }, }, @@ -157,7 +157,7 @@ func TestToProto_toProtoMessges(t *testing.T) { } func TestToProto_toProtoPart(t *testing.T) { - pData, _ := structpb.NewStruct(map[string]any{"key": "value"}) + pData, _ := structpb.NewValue(map[string]any{"key": "value"}) tests := []struct { name string p a2a.Part @@ -166,84 +166,71 @@ func TestToProto_toProtoPart(t *testing.T) { }{ { name: "text", - p: a2a.TextPart{Text: "hello"}, - want: &a2apb.Part{Part: &a2apb.Part_Text{Text: "hello"}}, + p: a2a.Part{Content: a2a.Text("hello")}, + want: &a2apb.Part{Content: &a2apb.Part_Text{Text: "hello"}}, }, { name: "data", - p: a2a.DataPart{Data: map[string]any{"key": "value"}}, - want: &a2apb.Part{Part: &a2apb.Part_Data{Data: &a2apb.DataPart{Data: pData}}}, + p: a2a.Part{Content: a2a.Data(map[string]any{"key": "value"})}, + want: &a2apb.Part{Content: &a2apb.Part_Data{Data: pData}}, }, { name: "file with bytes", - p: a2a.FilePart{ - File: a2a.FileBytes{ - FileMeta: a2a.FileMeta{ - MimeType: "text/plain", - }, - Bytes: "content", - }, + p: a2a.Part{Content: a2a.Raw([]byte("content")), + Filename: "Test File", + MediaType: "text/plain", + }, + want: &a2apb.Part{Content: &a2apb.Part_Raw{Raw: []byte("content")}, + Filename: "Test File", + MediaType: "text/plain", }, - want: &a2apb.Part{Part: &a2apb.Part_File{File: &a2apb.FilePart{ - MimeType: "text/plain", - File: &a2apb.FilePart_FileWithBytes{FileWithBytes: []byte("content")}, - }}}, }, { name: "file with uri", - p: a2a.FilePart{ - File: a2a.FileURI{ - FileMeta: a2a.FileMeta{ - Name: "example", - MimeType: "text/plain", - }, - URI: "http://example.com/file", - }, + p: a2a.Part{ + Content: a2a.URL("http://example.com/file"), + Filename: "example", + MediaType: "text/plain", + }, + want: &a2apb.Part{Content: &a2apb.Part_Url{Url: "http://example.com/file"}, + Filename: "example", + MediaType: "text/plain", }, - want: &a2apb.Part{Part: &a2apb.Part_File{File: &a2apb.FilePart{ - MimeType: "text/plain", - Name: "example", - File: &a2apb.FilePart_FileWithUri{FileWithUri: "http://example.com/file"}, - }}}, }, { name: "unsupported", - p: (a2a.Part)(nil), // Use a nil a2a.Part to represent an unsupported type + p: a2a.Part{Content: nil}, // Use a nil a2a.Part to represent an unsupported type wantErr: true, }, { name: "bad data", - p: a2a.DataPart{ - Data: map[string]any{"bad": func() {}}, - }, + p: a2a.Part{Content: a2a.Data(map[string]any{"bad": func() {}})}, wantErr: true, }, { name: "text with meta", - p: a2a.TextPart{Text: "hello", Metadata: map[string]any{"hello": "world"}}, + p: a2a.Part{Content: a2a.Text("hello"), Metadata: map[string]any{"hello": "world"}}, want: &a2apb.Part{ - Part: &a2apb.Part_Text{Text: "hello"}, + Content: &a2apb.Part_Text{Text: "hello"}, Metadata: mustMakeProtoMetadata(t, map[string]any{"hello": "world"}), }, }, { name: "data with meta", - p: a2a.DataPart{Data: map[string]any{"key": "value"}, Metadata: map[string]any{"hello": "world"}}, + p: a2a.Part{Content: a2a.Data(map[string]any{"key": "value"}), Metadata: map[string]any{"hello": "world"}}, want: &a2apb.Part{ - Part: &a2apb.Part_Data{Data: &a2apb.DataPart{Data: pData}}, + Content: &a2apb.Part_Data{Data: pData}, Metadata: mustMakeProtoMetadata(t, map[string]any{"hello": "world"}), }, }, { name: "file with meta", - p: a2a.FilePart{ - File: a2a.FileBytes{Bytes: "content"}, + p: a2a.Part{ + Content: a2a.Raw([]byte("content")), Metadata: map[string]any{"hello": "world"}, }, want: &a2apb.Part{ - Part: &a2apb.Part_File{File: &a2apb.FilePart{ - File: &a2apb.FilePart_FileWithBytes{FileWithBytes: []byte("content")}, - }}, + Content: &a2apb.Part_Raw{Raw: []byte("content")}, Metadata: mustMakeProtoMetadata(t, map[string]any{"hello": "world"}), }, }, @@ -313,7 +300,7 @@ func TestToProto_toProtoTaskState(t *testing.T) { { name: string(a2a.TaskStateCanceled), state: a2a.TaskStateCanceled, - want: a2apb.TaskState_TASK_STATE_CANCELLED, + want: a2apb.TaskState_TASK_STATE_CANCELED, }, { name: string(a2a.TaskStateCompleted), @@ -381,7 +368,7 @@ func TestToProto_toProtoTaskStatus(t *testing.T) { }, want: &a2apb.TaskStatus{ State: a2apb.TaskState_TASK_STATE_WORKING, - Update: pMsg, + Message: pMsg, Timestamp: pNow, }, }, @@ -404,7 +391,7 @@ func TestToProto_toProtoTaskStatus(t *testing.T) { }, want: &a2apb.TaskStatus{ State: a2apb.TaskState_TASK_STATE_WORKING, - Update: pMsg, + Message: pMsg, }, }, { @@ -452,24 +439,26 @@ func TestToProto_toProtoTaskPushConfig(t *testing.T) { name: "full config", config: &a2a.TaskPushConfig{ TaskID: "t1", + ID: "tc1", Config: a2a.PushConfig{ ID: "c1", URL: "http://a.com", Token: "tok", Auth: &a2a.PushAuthInfo{ - Schemes: []string{"Bearer"}, + Scheme: "Bearer", Credentials: "cred", }, }, }, want: &a2apb.TaskPushNotificationConfig{ - Name: "tasks/t1/pushNotificationConfigs/c1", + TaskId: "t1", + Id: "tc1", PushNotificationConfig: &a2apb.PushNotificationConfig{ Id: "c1", Url: "http://a.com", Token: "tok", Authentication: &a2apb.AuthenticationInfo{ - Schemes: []string{"Bearer"}, + Scheme: "Bearer", Credentials: "cred", }, }, @@ -479,10 +468,12 @@ func TestToProto_toProtoTaskPushConfig(t *testing.T) { name: "config without auth", config: &a2a.TaskPushConfig{ TaskID: "t1", + ID: "tc1", Config: a2a.PushConfig{ID: "c1", URL: "http://a.com"}, }, want: &a2apb.TaskPushNotificationConfig{ - Name: "tasks/t1/pushNotificationConfigs/c1", + TaskId: "t1", + Id: "tc1", PushNotificationConfig: &a2apb.PushNotificationConfig{ Id: "c1", Url: "http://a.com", @@ -501,7 +492,8 @@ func TestToProto_toProtoTaskPushConfig(t *testing.T) { Config: a2a.PushConfig{}, }, want: &a2apb.TaskPushNotificationConfig{ - Name: "tasks/test-task/pushNotificationConfigs/", + TaskId: "test-task", + Id: "", PushNotificationConfig: &a2apb.PushNotificationConfig{}, }, }, @@ -521,17 +513,20 @@ func TestToProto_toProtoTaskPushConfig(t *testing.T) { } } -func TestToProto_toProtoListTaskPushConfig(t *testing.T) { - configs := []*a2a.TaskPushConfig{ - {TaskID: "test-task", Config: a2a.PushConfig{ID: "test-config1"}}, - {TaskID: "test-task", Config: a2a.PushConfig{ID: "test-config2"}}, +func TestToProto_toProtoListTaskPushConfigResponse(t *testing.T) { + configs := &a2a.ListTaskPushConfigResponse{ + Configs: []a2a.TaskPushConfig{ + {TaskID: "test-task", Config: a2a.PushConfig{ID: "test-config1"}}, + {TaskID: "test-task", Config: a2a.PushConfig{ID: "test-config2"}}, + }, + NextPageToken: "next", } - pConf1, _ := ToProtoTaskPushConfig(configs[0]) - pConf2, _ := ToProtoTaskPushConfig(configs[1]) + pConf1, _ := ToProtoTaskPushConfig(&configs.Configs[0]) + pConf2, _ := ToProtoTaskPushConfig(&configs.Configs[1]) tests := []struct { name string - configs []*a2a.TaskPushConfig + configs *a2a.ListTaskPushConfigResponse want *a2apb.ListTaskPushNotificationConfigResponse wantErr bool }{ @@ -539,26 +534,27 @@ func TestToProto_toProtoListTaskPushConfig(t *testing.T) { name: "success", configs: configs, want: &a2apb.ListTaskPushNotificationConfigResponse{ - Configs: []*a2apb.TaskPushNotificationConfig{pConf1, pConf2}, + Configs: []*a2apb.TaskPushNotificationConfig{pConf1, pConf2}, + NextPageToken: "next", }, }, { name: "empty slice", - configs: []*a2a.TaskPushConfig{}, + configs: &a2a.ListTaskPushConfigResponse{}, want: &a2apb.ListTaskPushNotificationConfigResponse{ Configs: []*a2apb.TaskPushNotificationConfig{}, }, }, { name: "conversion error", - configs: []*a2a.TaskPushConfig{{TaskID: "", Config: a2a.PushConfig{ID: "test"}}}, + configs: &a2a.ListTaskPushConfigResponse{Configs: []a2a.TaskPushConfig{{TaskID: "", Config: a2a.PushConfig{ID: "test"}}}}, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := ToProtoListTaskPushConfig(tt.configs) + got, err := ToProtoListTaskPushConfigResponse(tt.configs) if (err != nil) != tt.wantErr { t.Errorf("toProtoListTaskPushConfig() error = %v, wantErr %v", err, tt.wantErr) return @@ -583,17 +579,17 @@ func TestToProto_toProtoTask(t *testing.T) { pMeta, _ := structpb.NewStruct(a2aMeta) a2aHistory := []*a2a.Message{ - {ID: a2aMsgID, Role: a2a.MessageRoleUser, Parts: []a2a.Part{a2a.TextPart{Text: "history"}}}, + {ID: a2aMsgID, Role: a2a.MessageRoleUser, Parts: a2a.ContentParts{a2a.NewTextPart("history")}}, } pHistory := []*a2apb.Message{ - {MessageId: a2aMsgID, Role: a2apb.Role_ROLE_USER, Parts: []*a2apb.Part{{Part: &a2apb.Part_Text{Text: "history"}}}}, + {MessageId: a2aMsgID, Role: a2apb.Role_ROLE_USER, Parts: []*a2apb.Part{{Content: &a2apb.Part_Text{Text: "history"}}}}, } a2aArtifacts := []*a2a.Artifact{ - {ID: a2aArtifactID, Name: "artifact1", Parts: []a2a.Part{a2a.TextPart{Text: "artifact content"}}}, + {ID: a2aArtifactID, Name: "artifact1", Parts: a2a.ContentParts{a2a.NewTextPart("artifact content")}}, } pArtifacts := []*a2apb.Artifact{ - {ArtifactId: string(a2aArtifactID), Name: "artifact1", Parts: []*a2apb.Part{{Part: &a2apb.Part_Text{Text: "artifact content"}}}}, + {ArtifactId: string(a2aArtifactID), Name: "artifact1", Parts: []*a2apb.Part{{Content: &a2apb.Part_Text{Text: "artifact content"}}}}, } a2aStatus := a2a.TaskStatus{ @@ -603,7 +599,7 @@ func TestToProto_toProtoTask(t *testing.T) { } pStatus := &a2apb.TaskStatus{ State: a2apb.TaskState_TASK_STATE_WORKING, - Update: &a2apb.Message{MessageId: "status-msg", Role: a2apb.Role_ROLE_AGENT}, + Message: &a2apb.Message{MessageId: "status-msg", Role: a2apb.Role_ROLE_AGENT}, Timestamp: pNow, } @@ -774,13 +770,11 @@ func TestToProto_toProtoListTasksResponse(t *testing.T) { func TestToProto_toProtoAgentCard(t *testing.T) { a2aCard := &a2a.AgentCard{ - ProtocolVersion: "1.0", Name: "Test Agent", Description: "An agent for testing.", - URL: "https://example.com/agent", - PreferredTransport: a2a.TransportProtocolGRPC, - AdditionalInterfaces: []a2a.AgentInterface{ - {Transport: a2a.TransportProtocolJSONRPC, URL: "https://example.com/agent/jsonrpc"}, + SupportedInterfaces: []a2a.AgentInterface{ + {ProtocolBinding: "http+json", URL: "https://example.com/agent/http+json", ProtocolVersion: "1.0"}, + {ProtocolBinding: "grpc", URL: "https://example.com/agent/grpc", ProtocolVersion: "1.0"}, }, Provider: &a2a.AgentProvider{ Org: "Test Org", @@ -789,9 +783,9 @@ func TestToProto_toProtoAgentCard(t *testing.T) { Version: "0.1.0", DocumentationURL: "https://example.com/docs", Capabilities: a2a.AgentCapabilities{ - Streaming: true, - PushNotifications: true, - StateTransitionHistory: true, + Streaming: true, + PushNotifications: true, + ExtendedAgentCard: true, Extensions: []a2a.AgentExtension{ {URI: "ext-uri", Description: "ext-desc", Required: true, Params: map[string]any{"key": "val"}}, }, @@ -799,23 +793,25 @@ func TestToProto_toProtoAgentCard(t *testing.T) { SecuritySchemes: a2a.NamedSecuritySchemes{ "apiKey": a2a.APIKeySecurityScheme{ Name: "X-API-KEY", - In: a2a.APIKeySecuritySchemeInHeader, + Location: a2a.APIKeySecuritySchemeLocationHeader, Description: "API Key", }, "oauth2": a2a.OAuth2SecurityScheme{ Description: "OAuth2", - Flows: a2a.OAuthFlows{ - AuthorizationCode: &a2a.AuthorizationCodeOAuthFlow{ - AuthorizationURL: "https://example.com/auth", - TokenURL: "https://example.com/token", - Scopes: map[string]string{"read": "read scope"}, - }, + Flows: &a2a.AuthorizationCodeOAuthFlow{ + AuthorizationURL: "https://example.com/auth", + TokenURL: "https://example.com/token", + Scopes: map[string]string{"read": "read scope"}, }, }, }, - Security: []a2a.SecurityRequirements{ - {"apiKey": {}}, - {"oauth2": {"read"}}, + SecurityRequirements: []a2a.SecurityRequirement{ + { + Schemes: map[a2a.SecuritySchemeName]a2a.SecuritySchemeScopes{ + a2a.SecuritySchemeName("apiKey"): a2a.SecuritySchemeScopes{}, + a2a.SecuritySchemeName("oauth2"): a2a.SecuritySchemeScopes{"read"}, + }, + }, }, DefaultInputModes: []string{"text/plain"}, DefaultOutputModes: []string{"text/plain"}, @@ -828,38 +824,39 @@ func TestToProto_toProtoAgentCard(t *testing.T) { Examples: []string{"do a test"}, InputModes: []string{"text/markdown"}, OutputModes: []string{"text/markdown"}, - Security: []a2a.SecurityRequirements{ - {"apiKey": {}}, + Security: []a2a.SecurityRequirement{ + { + Schemes: map[a2a.SecuritySchemeName]a2a.SecuritySchemeScopes{ + a2a.SecuritySchemeName("apiKey"): a2a.SecuritySchemeScopes{}, + }, + }, }, }, }, Signatures: []a2a.AgentCardSignature{ {Protected: "abc", Signature: "def", Header: map[string]any{"version": "1"}}, }, - IconURL: "https://icons.com/icon.png", - SupportsAuthenticatedExtendedCard: true, + IconURL: "https://icons.com/icon.png", } extParams, _ := structpb.NewStruct(map[string]any{"key": "val"}) pCard := &a2apb.AgentCard{ - ProtocolVersion: "1.0", Name: "Test Agent", Description: "An agent for testing.", - Url: "https://example.com/agent", - PreferredTransport: string(a2a.TransportProtocolGRPC), - AdditionalInterfaces: []*a2apb.AgentInterface{ - {Transport: string(a2a.TransportProtocolJSONRPC), Url: "https://example.com/agent/jsonrpc"}, + SupportedInterfaces: []*a2apb.AgentInterface{ + {ProtocolBinding: "http+json", Url: "https://example.com/agent/http+json", ProtocolVersion: "1.0"}, + {ProtocolBinding: "grpc", Url: "https://example.com/agent/grpc", ProtocolVersion: "1.0"}, }, Provider: &a2apb.AgentProvider{ Organization: "Test Org", Url: "https://example.com/org", }, Version: "0.1.0", - DocumentationUrl: "https://example.com/docs", + DocumentationUrl: proto.String("https://example.com/docs"), Capabilities: &a2apb.AgentCapabilities{ - Streaming: true, - PushNotifications: true, - StateTransitionHistory: true, + Streaming: proto.Bool(true), + PushNotifications: proto.Bool(true), + ExtendedAgentCard: proto.Bool(true), Extensions: []*a2apb.AgentExtension{ {Uri: "ext-uri", Description: "ext-desc", Required: true, Params: extParams}, }, @@ -869,7 +866,7 @@ func TestToProto_toProtoAgentCard(t *testing.T) { Scheme: &a2apb.SecurityScheme_ApiKeySecurityScheme{ ApiKeySecurityScheme: &a2apb.APIKeySecurityScheme{ Name: "X-API-KEY", - Location: string(a2a.APIKeySecuritySchemeInHeader), + Location: string(a2a.APIKeySecuritySchemeLocationHeader), Description: "API Key", }, }, @@ -891,9 +888,11 @@ func TestToProto_toProtoAgentCard(t *testing.T) { }, }, }, - Security: []*a2apb.Security{ - {Schemes: map[string]*a2apb.StringList{"apiKey": {List: []string{}}}}, - {Schemes: map[string]*a2apb.StringList{"oauth2": {List: []string{"read"}}}}, + SecurityRequirements: []*a2apb.SecurityRequirement{ + {Schemes: map[string]*a2apb.StringList{ + "apiKey": {List: []string{}}, + "oauth2": {List: []string{"read"}}, + }}, }, DefaultInputModes: []string{"text/plain"}, DefaultOutputModes: []string{"text/plain"}, @@ -906,7 +905,7 @@ func TestToProto_toProtoAgentCard(t *testing.T) { Examples: []string{"do a test"}, InputModes: []string{"text/markdown"}, OutputModes: []string{"text/markdown"}, - Security: []*a2apb.Security{ + SecurityRequirements: []*a2apb.SecurityRequirement{ {Schemes: map[string]*a2apb.StringList{"apiKey": {List: []string{}}}}, }, }, @@ -914,8 +913,7 @@ func TestToProto_toProtoAgentCard(t *testing.T) { Signatures: []*a2apb.AgentCardSignature{ {Protected: "abc", Signature: "def", Header: mustMakeProtoMetadata(t, map[string]any{"version": "1"})}, }, - IconUrl: "https://icons.com/icon.png", - SupportsAuthenticatedExtendedCard: true, + IconUrl: proto.String("https://icons.com/icon.png"), } tests := []struct { @@ -980,14 +978,12 @@ func TestToProto_toProtoOAuthFlows(t *testing.T) { }{ { name: "authorization code flow", - flows: a2a.OAuthFlows{ - AuthorizationCode: &a2a.AuthorizationCodeOAuthFlow{ + flows: &a2a.AuthorizationCodeOAuthFlow{ AuthorizationURL: "https://auth.com/auth", TokenURL: "https://auth.com/token", RefreshURL: "https://auth.com/refresh", Scopes: map[string]string{"read": "read data"}, }, - }, want: &a2apb.OAuthFlows{ Flow: &a2apb.OAuthFlows_AuthorizationCode{ AuthorizationCode: &a2apb.AuthorizationCodeOAuthFlow{ @@ -1001,13 +997,11 @@ func TestToProto_toProtoOAuthFlows(t *testing.T) { }, { name: "client credentials flow", - flows: a2a.OAuthFlows{ - ClientCredentials: &a2a.ClientCredentialsOAuthFlow{ + flows: &a2a.ClientCredentialsOAuthFlow{ TokenURL: "https://auth.com/token", RefreshURL: "https://auth.com/refresh", Scopes: map[string]string{"write": "write data"}, }, - }, want: &a2apb.OAuthFlows{ Flow: &a2apb.OAuthFlows_ClientCredentials{ ClientCredentials: &a2apb.ClientCredentialsOAuthFlow{ @@ -1020,13 +1014,11 @@ func TestToProto_toProtoOAuthFlows(t *testing.T) { }, { name: "implicit flow", - flows: a2a.OAuthFlows{ - Implicit: &a2a.ImplicitOAuthFlow{ + flows: &a2a.ImplicitOAuthFlow{ AuthorizationURL: "https://auth.com/auth", RefreshURL: "https://auth.com/refresh", Scopes: map[string]string{"profile": "read profile"}, }, - }, want: &a2apb.OAuthFlows{ Flow: &a2apb.OAuthFlows_Implicit{ Implicit: &a2apb.ImplicitOAuthFlow{ @@ -1039,13 +1031,11 @@ func TestToProto_toProtoOAuthFlows(t *testing.T) { }, { name: "password flow", - flows: a2a.OAuthFlows{ - Password: &a2a.PasswordOAuthFlow{ + flows: &a2a.PasswordOAuthFlow{ TokenURL: "https://auth.com/token", RefreshURL: "https://auth.com/refresh", Scopes: map[string]string{"user": "user info"}, }, - }, want: &a2apb.OAuthFlows{ Flow: &a2apb.OAuthFlows_Password{ Password: &a2apb.PasswordOAuthFlow{ @@ -1058,26 +1048,11 @@ func TestToProto_toProtoOAuthFlows(t *testing.T) { }, { name: "no flows specified", - flows: a2a.OAuthFlows{}, - wantErr: true, - }, - { - name: "multiple flows specified", - flows: a2a.OAuthFlows{ - ClientCredentials: &a2a.ClientCredentialsOAuthFlow{ - TokenURL: "https://auth.com/token", - RefreshURL: "https://auth.com/refresh", - Scopes: map[string]string{"write": "write data"}, - }, - Password: &a2a.PasswordOAuthFlow{ - TokenURL: "https://auth.com/token", - RefreshURL: "https://auth.com/refresh", - Scopes: map[string]string{"user": "user info"}, - }, - }, + flows: nil, wantErr: true, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1106,14 +1081,14 @@ func TestToProto_toProtoSecurityScheme(t *testing.T) { name: "api key scheme", scheme: a2a.APIKeySecurityScheme{ Name: "X-API-KEY", - In: a2a.APIKeySecuritySchemeInHeader, + Location: a2a.APIKeySecuritySchemeLocationHeader, Description: "API Key", }, want: &a2apb.SecurityScheme{ Scheme: &a2apb.SecurityScheme_ApiKeySecurityScheme{ ApiKeySecurityScheme: &a2apb.APIKeySecurityScheme{ Name: "X-API-KEY", - Location: string(a2a.APIKeySecuritySchemeInHeader), + Location: string(a2a.APIKeySecuritySchemeLocationHeader), Description: "API Key", }, }, @@ -1164,12 +1139,10 @@ func TestToProto_toProtoSecurityScheme(t *testing.T) { name: "oauth2 scheme", scheme: a2a.OAuth2SecurityScheme{ Description: "OAuth2", - Flows: a2a.OAuthFlows{ - AuthorizationCode: &a2a.AuthorizationCodeOAuthFlow{ - AuthorizationURL: "https://auth.com/auth", - TokenURL: "https://auth.com/token", - Scopes: map[string]string{"read": "read data"}, - }, + Flows: &a2a.AuthorizationCodeOAuthFlow{ + AuthorizationURL: "https://auth.com/auth", + TokenURL: "https://auth.com/token", + Scopes: map[string]string{"read": "read data"}, }, }, want: &a2apb.SecurityScheme{ @@ -1211,15 +1184,13 @@ func TestToProto_toProtoSendMessageResponse(t *testing.T) { a2aMsg := &a2a.Message{ ID: "test-message", Role: a2a.MessageRoleAgent, - Parts: []a2a.Part{ - a2a.TextPart{Text: "response"}, - }, + Parts: a2a.ContentParts{a2a.NewTextPart("response")}, } pMsg := &a2apb.Message{ MessageId: "test-message", Role: a2apb.Role_ROLE_AGENT, Parts: []*a2apb.Part{ - {Part: &a2apb.Part_Text{Text: "response"}}, + {Content: &a2apb.Part_Text{Text: "response"}}, }, } @@ -1239,7 +1210,7 @@ func TestToProto_toProtoSendMessageResponse(t *testing.T) { Status: &a2apb.TaskStatus{ State: a2apb.TaskState_TASK_STATE_COMPLETED, Timestamp: timestamppb.New(now), - Update: pMsg, + Message: pMsg, }, } @@ -1253,7 +1224,7 @@ func TestToProto_toProtoSendMessageResponse(t *testing.T) { name: "message response", result: a2aMsg, want: &a2apb.SendMessageResponse{ - Payload: &a2apb.SendMessageResponse_Msg{Msg: pMsg}, + Payload: &a2apb.SendMessageResponse_Message{Message: pMsg}, }, }, { @@ -1296,7 +1267,6 @@ func TestToProto_toProtoStreamResponse(t *testing.T) { a2aStatusEvent := &a2a.TaskStatusUpdateEvent{ TaskID: "test-task", ContextID: "test-ctx", - Final: true, Status: a2a.TaskStatus{ State: a2a.TaskStateCompleted, Timestamp: &now, @@ -1306,7 +1276,6 @@ func TestToProto_toProtoStreamResponse(t *testing.T) { StatusUpdate: &a2apb.TaskStatusUpdateEvent{ TaskId: "test-task", ContextId: "test-ctx", - Final: true, Status: &a2apb.TaskStatus{ State: a2apb.TaskState_TASK_STATE_COMPLETED, Timestamp: pNow, @@ -1336,7 +1305,7 @@ func TestToProto_toProtoStreamResponse(t *testing.T) { name: "message", event: a2aMsg, want: &a2apb.StreamResponse{ - Payload: &a2apb.StreamResponse_Msg{Msg: pMsg}, + Payload: &a2apb.StreamResponse_Message{Message: pMsg}, }, }, {