diff --git a/docs/migration-guide-v1.md b/docs/migration-guide-v1.md index a1ec2ed3..be7c64c6 100644 --- a/docs/migration-guide-v1.md +++ b/docs/migration-guide-v1.md @@ -324,7 +324,7 @@ All JSON wire format changes follow the A2A v1 ProtoJSON conventions. | `"tasks/resubscribe"` | `"SubscribeToTask"` | | N/A | `"ListTasks"` (new) | | N/A | `"DeleteTaskPushNotificationConfig"` (new) | -| N/A | `"ListTaskPushNotificationConfig"` (new) | +| N/A | `"ListTaskPushNotificationConfigs"` (new, pluralized) | | N/A | `"GetExtendedAgentCard"` (new) | --- @@ -460,6 +460,116 @@ var extCard = await client.GetExtendedAgentCardAsync( --- +## Push Notification Configuration + +The push notification config API has been restructured to match the v1 spec proto. + +### Flattened Model + +V0.3 used a nested two-class structure. V1 uses a single flat `TaskPushNotificationConfig`: + +```csharp +// V0.3 — nested structure +var config = new TaskPushNotificationConfig +{ + Id = "cfg-1", + TaskId = "t-1", + PushNotificationConfig = new PushNotificationConfig + { + Url = "http://example.com/notify", + Token = "my-token", + Authentication = new AuthenticationInfo { Scheme = "Bearer", Credentials = "..." } + } +}; + +// V1 — flat structure (matches proto TaskPushNotificationConfig) +var config = new TaskPushNotificationConfig +{ + Id = "cfg-1", + TaskId = "t-1", + Url = "http://example.com/notify", + Token = "my-token", + Authentication = new AuthenticationInfo { Scheme = "Bearer", Credentials = "..." } +}; +``` + +**Key changes:** + +| Aspect | V0.3 | V1 | +|--------|------|-----| +| `Url`, `Token`, `Authentication` | Nested in `PushNotificationConfig` | Flat on `TaskPushNotificationConfig` | +| `Id`, `TaskId` | `[JsonRequired]` `string` | Nullable `string?` (server may assign) | +| `PushNotificationConfig` property | Required, nested object | Removed | + +### Removed Request Wrapper + +The `CreateTaskPushNotificationConfigRequest` wrapper class has been removed. The spec's `CreateTaskPushNotificationConfig` RPC takes `TaskPushNotificationConfig` directly: + +```csharp +// V0.3 +await client.CreateTaskPushNotificationConfigAsync( + new CreateTaskPushNotificationConfigRequest + { + TaskId = "t-1", + ConfigId = "cfg-1", + Config = new PushNotificationConfig { Url = "http://callback" } + }); + +// V1 +await client.CreateTaskPushNotificationConfigAsync( + new TaskPushNotificationConfig + { + TaskId = "t-1", + Url = "http://callback" + }); +``` + +### SendMessageConfiguration Property Rename + +```csharp +// V0.3 +var config = new SendMessageConfiguration +{ + PushNotificationConfig = new PushNotificationConfig { Url = "http://push" } +}; + +// V1 +var config = new SendMessageConfiguration +{ + TaskPushNotificationConfig = new TaskPushNotificationConfig { Url = "http://push" } +}; +``` + +### Pluralized List Types + +The List operation types and method names are now pluralized to match the proto (`ListTaskPushNotificationConfigs`): + +| V0.3 | V1 | +|------|-----| +| `ListTaskPushNotificationConfigRequest` | `ListTaskPushNotificationConfigsRequest` | +| `ListTaskPushNotificationConfigResponse` | `ListTaskPushNotificationConfigsResponse` | +| `ListTaskPushNotificationConfigAsync()` | `ListTaskPushNotificationConfigsAsync()` | + +### Handler Interface + +If you implement `IA2ARequestHandler`, update these signatures: + +```csharp +// V0.3 +Task CreateTaskPushNotificationConfigAsync( + CreateTaskPushNotificationConfigRequest request, CancellationToken ct); +Task ListTaskPushNotificationConfigAsync( + ListTaskPushNotificationConfigRequest request, CancellationToken ct); + +// V1 +Task CreateTaskPushNotificationConfigAsync( + TaskPushNotificationConfig config, CancellationToken ct); +Task ListTaskPushNotificationConfigsAsync( + ListTaskPushNotificationConfigsRequest request, CancellationToken ct); +``` + +--- + ## Server API V1 introduces `ITaskManager` for server implementations. Use `TaskManager` as a base or implement the interface directly. diff --git a/samples/A2ACli/Host/A2ACli.cs b/samples/A2ACli/Host/A2ACli.cs index 07f28ae5..38115e56 100644 --- a/samples/A2ACli/Host/A2ACli.cs +++ b/samples/A2ACli/Host/A2ACli.cs @@ -220,8 +220,10 @@ private static async Task CompleteTaskAsync( // Add push notification configuration if enabled if (usePushNotifications) { - payload.Configuration.PushNotificationConfig = new PushNotificationConfig + payload.Configuration.TaskPushNotificationConfig = new TaskPushNotificationConfig { + Id = Guid.NewGuid().ToString(), + TaskId = taskId, Url = $"http://{notificationReceiverHost}:{notificationReceiverPort}/notify", Authentication = new AuthenticationInfo { diff --git a/src/A2A.AspNetCore/A2AEndpointRouteBuilderExtensions.cs b/src/A2A.AspNetCore/A2AEndpointRouteBuilderExtensions.cs index 171c987d..9400e0d5 100644 --- a/src/A2A.AspNetCore/A2AEndpointRouteBuilderExtensions.cs +++ b/src/A2A.AspNetCore/A2AEndpointRouteBuilderExtensions.cs @@ -120,7 +120,7 @@ public static IEndpointConventionBuilder MapHttpA2A( // Push notification config operations routeGroup.MapPost("/tasks/{id}/pushNotificationConfigs", - (string id, [FromBody] PushNotificationConfig config, CancellationToken ct) + (string id, [FromBody] TaskPushNotificationConfig config, CancellationToken ct) => A2AHttpProcessor.CreatePushNotificationConfigRestAsync(requestHandler, logger, id, config, ct)); routeGroup.MapGet("/tasks/{id}/pushNotificationConfigs", diff --git a/src/A2A.AspNetCore/A2AHttpProcessor.cs b/src/A2A.AspNetCore/A2AHttpProcessor.cs index 34e16973..ccda655b 100644 --- a/src/A2A.AspNetCore/A2AHttpProcessor.cs +++ b/src/A2A.AspNetCore/A2AHttpProcessor.cs @@ -196,16 +196,12 @@ internal static Task GetExtendedAgentCardRestAsync( // REST handler: Create push notification config internal static Task CreatePushNotificationConfigRestAsync( - IA2ARequestHandler requestHandler, ILogger logger, string taskId, PushNotificationConfig config, CancellationToken cancellationToken) + IA2ARequestHandler requestHandler, ILogger logger, string taskId, TaskPushNotificationConfig config, CancellationToken cancellationToken) => WithExceptionHandlingAsync(logger, "REST.CreatePushNotificationConfig", async ct => { - var request = new CreateTaskPushNotificationConfigRequest - { - TaskId = taskId, - Config = config, - ConfigId = config.Id ?? string.Empty, - }; - var result = await requestHandler.CreateTaskPushNotificationConfigAsync(request, ct).ConfigureAwait(false); + // Route provides the authoritative taskId; override whatever the body sent + config.TaskId = taskId; + var result = await requestHandler.CreateTaskPushNotificationConfigAsync(config, ct).ConfigureAwait(false); return new A2AResponseResult(result); }, taskId, cancellationToken); @@ -215,13 +211,13 @@ internal static Task ListPushNotificationConfigRestAsync( CancellationToken cancellationToken) => WithExceptionHandlingAsync(logger, "REST.ListPushNotificationConfig", async ct => { - var request = new ListTaskPushNotificationConfigRequest + var request = new ListTaskPushNotificationConfigsRequest { TaskId = taskId, PageSize = pageSize, PageToken = pageToken, }; - var result = await requestHandler.ListTaskPushNotificationConfigAsync(request, ct) + var result = await requestHandler.ListTaskPushNotificationConfigsAsync(request, ct) .ConfigureAwait(false); return new A2AResponseResult(result); }, taskId, cancellationToken); @@ -258,7 +254,7 @@ internal sealed class A2AResponseResult : IResult internal A2AResponseResult(ListTasksResponse response) { _response = response; _responseType = typeof(ListTasksResponse); } internal A2AResponseResult(AgentCard card) { _response = card; _responseType = typeof(AgentCard); } internal A2AResponseResult(TaskPushNotificationConfig config) { _response = config; _responseType = typeof(TaskPushNotificationConfig); } - internal A2AResponseResult(ListTaskPushNotificationConfigResponse response) { _response = response; _responseType = typeof(ListTaskPushNotificationConfigResponse); } + internal A2AResponseResult(ListTaskPushNotificationConfigsResponse response) { _response = response; _responseType = typeof(ListTaskPushNotificationConfigsResponse); } public async Task ExecuteAsync(HttpContext httpContext) { diff --git a/src/A2A.AspNetCore/A2AJsonRpcProcessor.cs b/src/A2A.AspNetCore/A2AJsonRpcProcessor.cs index 33123f3a..e29ca70f 100644 --- a/src/A2A.AspNetCore/A2AJsonRpcProcessor.cs +++ b/src/A2A.AspNetCore/A2AJsonRpcProcessor.cs @@ -142,7 +142,7 @@ internal static async Task SingleResponseAsync(IA2AReques response = JsonRpcResponse.CreateJsonRpcResponse(requestId, cancelledTask); break; case A2AMethods.CreateTaskPushNotificationConfig: - var createPnConfig = DeserializeAndValidate(parameters.Value); + var createPnConfig = DeserializeAndValidate(parameters.Value); var createdConfig = await requestHandler.CreateTaskPushNotificationConfigAsync(createPnConfig, cancellationToken).ConfigureAwait(false); response = JsonRpcResponse.CreateJsonRpcResponse(requestId, createdConfig); break; @@ -151,9 +151,9 @@ internal static async Task SingleResponseAsync(IA2AReques var gotConfig = await requestHandler.GetTaskPushNotificationConfigAsync(getPnConfig, cancellationToken).ConfigureAwait(false); response = JsonRpcResponse.CreateJsonRpcResponse(requestId, gotConfig); break; - case A2AMethods.ListTaskPushNotificationConfig: - var listPnConfig = DeserializeAndValidate(parameters.Value); - var listPnResult = await requestHandler.ListTaskPushNotificationConfigAsync(listPnConfig, cancellationToken).ConfigureAwait(false); + case A2AMethods.ListTaskPushNotificationConfigs: + var listPnConfig = DeserializeAndValidate(parameters.Value); + var listPnResult = await requestHandler.ListTaskPushNotificationConfigsAsync(listPnConfig, cancellationToken).ConfigureAwait(false); response = JsonRpcResponse.CreateJsonRpcResponse(requestId, listPnResult); break; case A2AMethods.DeleteTaskPushNotificationConfig: diff --git a/src/A2A.V0_3Compat/V03ClientAdapter.cs b/src/A2A.V0_3Compat/V03ClientAdapter.cs index 7c8e1f7e..d4a99131 100644 --- a/src/A2A.V0_3Compat/V03ClientAdapter.cs +++ b/src/A2A.V0_3Compat/V03ClientAdapter.cs @@ -89,13 +89,13 @@ internal V03ClientAdapter(V03.A2AClient v03Client) /// public async Task CreateTaskPushNotificationConfigAsync( - A2A.CreateTaskPushNotificationConfigRequest request, + A2A.TaskPushNotificationConfig config, CancellationToken cancellationToken = default) { var v03Config = new V03.TaskPushNotificationConfig { - TaskId = request.TaskId, - PushNotificationConfig = V03TypeConverter.ToV03PushNotificationConfig(request.Config), + TaskId = config.TaskId ?? string.Empty, + PushNotificationConfig = V03TypeConverter.ToV03PushNotificationConfigFromTask(config), }; var v03Result = await _v03Client.SetPushNotificationAsync(v03Config, cancellationToken).ConfigureAwait(false); return V03TypeConverter.ToV1TaskPushNotificationConfig(v03Result); @@ -116,8 +116,8 @@ internal V03ClientAdapter(V03.A2AClient v03Client) } /// - public Task ListTaskPushNotificationConfigAsync( - A2A.ListTaskPushNotificationConfigRequest request, + public Task ListTaskPushNotificationConfigsAsync( + A2A.ListTaskPushNotificationConfigsRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException("v0.3 does not support listing push notification configs."); diff --git a/src/A2A.V0_3Compat/V03ServerProcessor.cs b/src/A2A.V0_3Compat/V03ServerProcessor.cs index 9fbede65..c2a0c0b3 100644 --- a/src/A2A.V0_3Compat/V03ServerProcessor.cs +++ b/src/A2A.V0_3Compat/V03ServerProcessor.cs @@ -231,7 +231,7 @@ private static async Task DispatchV1SingleAsync( } case A2AMethods.CreateTaskPushNotificationConfig: { - var req = DeserializeV1Params(paramsEl.Value, id); + var req = DeserializeV1Params(paramsEl.Value, id); response = JsonRpcResponse.CreateJsonRpcResponse(id, await handler.CreateTaskPushNotificationConfigAsync(req, ct).ConfigureAwait(false)); break; } @@ -241,10 +241,10 @@ private static async Task DispatchV1SingleAsync( response = JsonRpcResponse.CreateJsonRpcResponse(id, await handler.GetTaskPushNotificationConfigAsync(req, ct).ConfigureAwait(false)); break; } - case A2AMethods.ListTaskPushNotificationConfig: + case A2AMethods.ListTaskPushNotificationConfigs: { - var req = DeserializeV1Params(paramsEl.Value, id); - response = JsonRpcResponse.CreateJsonRpcResponse(id, await handler.ListTaskPushNotificationConfigAsync(req, ct).ConfigureAwait(false)); + var req = DeserializeV1Params(paramsEl.Value, id); + response = JsonRpcResponse.CreateJsonRpcResponse(id, await handler.ListTaskPushNotificationConfigsAsync(req, ct).ConfigureAwait(false)); break; } case A2AMethods.DeleteTaskPushNotificationConfig: @@ -365,16 +365,12 @@ private static async Task HandleSingleAsync( case V03.A2AMethods.TaskPushNotificationConfigSet: { var v03Config = DeserializeParams(rpcRequest.Params.Value); - var v1Request = new CreateTaskPushNotificationConfigRequest - { - TaskId = v03Config.TaskId, - Config = V03TypeConverter.ToV1PushNotificationConfig(v03Config.PushNotificationConfig), - }; + var v1Request = V03TypeConverter.ToV1TaskPushNotificationConfig(v03Config); var v1Result = await handler.CreateTaskPushNotificationConfigAsync(v1Request, ct).ConfigureAwait(false); var v03Result = new V03.TaskPushNotificationConfig { - TaskId = v1Result.TaskId, - PushNotificationConfig = V03TypeConverter.ToV03PushNotificationConfig(v1Result.PushNotificationConfig), + TaskId = v1Result.TaskId ?? string.Empty, + PushNotificationConfig = V03TypeConverter.ToV03PushNotificationConfigFromTask(v1Result), }; return MakeSuccessResult(rpcRequest.Id, v03Result, typeof(V03.TaskPushNotificationConfig)); } @@ -390,8 +386,8 @@ private static async Task HandleSingleAsync( var v1Result = await handler.GetTaskPushNotificationConfigAsync(v1Request, ct).ConfigureAwait(false); var v03Result = new V03.TaskPushNotificationConfig { - TaskId = v1Result.TaskId, - PushNotificationConfig = V03TypeConverter.ToV03PushNotificationConfig(v1Result.PushNotificationConfig), + TaskId = v1Result.TaskId ?? string.Empty, + PushNotificationConfig = V03TypeConverter.ToV03PushNotificationConfigFromTask(v1Result), }; return MakeSuccessResult(rpcRequest.Id, v03Result, typeof(V03.TaskPushNotificationConfig)); } diff --git a/src/A2A.V0_3Compat/V03TypeConverter.cs b/src/A2A.V0_3Compat/V03TypeConverter.cs index 1cb2b471..04007002 100644 --- a/src/A2A.V0_3Compat/V03TypeConverter.cs +++ b/src/A2A.V0_3Compat/V03TypeConverter.cs @@ -194,9 +194,9 @@ internal static V03.MessageSendParams ToV03(A2A.SendMessageRequest request) Blocking = !config.ReturnImmediately, }; - if (config.PushNotificationConfig is { } pushConfig) + if (config.TaskPushNotificationConfig is { } pushConfig) { - result.Configuration.PushNotification = ToV03PushNotificationConfig(pushConfig); + result.Configuration.PushNotification = ToV03PushNotificationConfigFromTask(pushConfig); } } @@ -424,6 +424,56 @@ internal static A2A.Artifact ToV1Artifact(V03.Artifact artifact) => // ──── Push notification config conversion ──── + /// Converts a v1.0 flat TaskPushNotificationConfig to a v0.3 PushNotificationConfig. + /// The v1.0 task push notification config to convert. + /// The converted v0.3 push notification config. + internal static V03.PushNotificationConfig ToV03PushNotificationConfigFromTask(A2A.TaskPushNotificationConfig config) + { + var result = new V03.PushNotificationConfig + { + Url = config.Url, + Id = config.Id, + Token = config.Token, + }; + + if (config.Authentication is { } auth) + { + result.Authentication = new V03.PushNotificationAuthenticationInfo + { + Schemes = [auth.Scheme], + Credentials = auth.Credentials, + }; + } + + return result; + } + + /// Converts a v0.3 PushNotificationConfig to a v1.0 flat TaskPushNotificationConfig. + /// The v0.3 push notification config to convert. + /// The task identifier to include in the result. + /// The converted v1.0 task push notification config. + internal static A2A.TaskPushNotificationConfig ToV1TaskPushNotificationConfigFromV03PushNotification(V03.PushNotificationConfig config, string taskId) + { + var result = new A2A.TaskPushNotificationConfig + { + Id = config.Id ?? string.Empty, + TaskId = taskId, + Url = config.Url, + Token = config.Token, + }; + + if (config.Authentication is { Schemes: { Count: > 0 } schemes }) + { + result.Authentication = new A2A.AuthenticationInfo + { + Scheme = schemes[0], + Credentials = config.Authentication.Credentials, + }; + } + + return result; + } + /// Converts a v1.0 push notification config to v0.3. /// The v1.0 config to convert. /// The converted v0.3 push notification config. @@ -460,31 +510,45 @@ internal static A2A.PushNotificationConfig ToV1PushNotificationConfig(V03.PushNo Token = config.Token, }; - if (config.Authentication is { } auth && auth.Schemes.Count > 0) + if (config.Authentication is { Schemes: { Count: > 0 } schemes }) { // v0.3 PushNotificationAuthenticationInfo.Schemes is a list; v1.0 AuthenticationInfo.Scheme // is a single string. Only the first scheme is preserved — multi-scheme configs lose data here. result.Authentication = new A2A.AuthenticationInfo { - Scheme = auth.Schemes[0], - Credentials = auth.Credentials, + Scheme = schemes[0], + Credentials = config.Authentication.Credentials, }; } return result; } - /// Converts a v0.3 task push notification config to v1.0. + /// Converts a v0.3 task push notification config to v1.0 (flat structure). /// The v0.3 config to convert. /// The converted v1.0 task push notification config. - internal static A2A.TaskPushNotificationConfig ToV1TaskPushNotificationConfig(V03.TaskPushNotificationConfig config) => - new() + internal static A2A.TaskPushNotificationConfig ToV1TaskPushNotificationConfig(V03.TaskPushNotificationConfig config) + { + var result = new A2A.TaskPushNotificationConfig { Id = config.PushNotificationConfig.Id ?? string.Empty, TaskId = config.TaskId, - PushNotificationConfig = ToV1PushNotificationConfig(config.PushNotificationConfig), + Url = config.PushNotificationConfig.Url, + Token = config.PushNotificationConfig.Token, }; + if (config.PushNotificationConfig.Authentication is { Schemes: { Count: > 0 } schemes }) + { + result.Authentication = new A2A.AuthenticationInfo + { + Scheme = schemes[0], + Credentials = config.PushNotificationConfig.Authentication.Credentials, + }; + } + + return result; + } + // ──── v0.3 request params → v1.0 request types (server-side compat) ──── /// Converts v0.3 message send params to a v1.0 send message request. @@ -498,8 +562,8 @@ internal static A2A.SendMessageRequest ToV1SendMessageRequest(V03.MessageSendPar AcceptedOutputModes = cfg.AcceptedOutputModes, HistoryLength = cfg.HistoryLength, ReturnImmediately = !cfg.Blocking, - PushNotificationConfig = cfg.PushNotification is { } pn - ? ToV1PushNotificationConfig(pn) + TaskPushNotificationConfig = cfg.PushNotification is { } pn + ? ToV1TaskPushNotificationConfigFromV03PushNotification(pn, string.Empty) : null, } : null, Metadata = p.Metadata, diff --git a/src/A2A/A2AJsonUtilities.cs b/src/A2A/A2AJsonUtilities.cs index 2fba300a..4e6298fd 100644 --- a/src/A2A/A2AJsonUtilities.cs +++ b/src/A2A/A2AJsonUtilities.cs @@ -70,7 +70,7 @@ public static partial class A2AJsonUtilities [JsonSerializable(typeof(SendMessageResponse))] [JsonSerializable(typeof(StreamResponse))] [JsonSerializable(typeof(ListTasksResponse))] - [JsonSerializable(typeof(ListTaskPushNotificationConfigResponse))] + [JsonSerializable(typeof(ListTaskPushNotificationConfigsResponse))] // Agent discovery [JsonSerializable(typeof(AgentCard))] @@ -106,9 +106,8 @@ public static partial class A2AJsonUtilities [JsonSerializable(typeof(ListTasksRequest))] [JsonSerializable(typeof(CancelTaskRequest))] [JsonSerializable(typeof(SubscribeToTaskRequest))] - [JsonSerializable(typeof(CreateTaskPushNotificationConfigRequest))] [JsonSerializable(typeof(GetTaskPushNotificationConfigRequest))] - [JsonSerializable(typeof(ListTaskPushNotificationConfigRequest))] + [JsonSerializable(typeof(ListTaskPushNotificationConfigsRequest))] [JsonSerializable(typeof(DeleteTaskPushNotificationConfigRequest))] [JsonSerializable(typeof(GetExtendedAgentCardRequest))] diff --git a/src/A2A/Client/A2AClient.cs b/src/A2A/Client/A2AClient.cs index 1a883480..8dead549 100644 --- a/src/A2A/Client/A2AClient.cs +++ b/src/A2A/Client/A2AClient.cs @@ -71,9 +71,9 @@ public async IAsyncEnumerable SubscribeToTaskAsync(SubscribeToTa } /// - public async Task CreateTaskPushNotificationConfigAsync(CreateTaskPushNotificationConfigRequest request, CancellationToken cancellationToken = default) + public async Task CreateTaskPushNotificationConfigAsync(TaskPushNotificationConfig config, CancellationToken cancellationToken = default) { - return await SendJsonRpcRequestAsync(A2AMethods.CreateTaskPushNotificationConfig, request, cancellationToken).ConfigureAwait(false); + return await SendJsonRpcRequestAsync(A2AMethods.CreateTaskPushNotificationConfig, config, cancellationToken).ConfigureAwait(false); } /// @@ -83,9 +83,9 @@ public async Task GetTaskPushNotificationConfigAsync } /// - public async Task ListTaskPushNotificationConfigAsync(ListTaskPushNotificationConfigRequest request, CancellationToken cancellationToken = default) + public async Task ListTaskPushNotificationConfigsAsync(ListTaskPushNotificationConfigsRequest request, CancellationToken cancellationToken = default) { - return await SendJsonRpcRequestAsync(A2AMethods.ListTaskPushNotificationConfig, request, cancellationToken).ConfigureAwait(false); + return await SendJsonRpcRequestAsync(A2AMethods.ListTaskPushNotificationConfigs, request, cancellationToken).ConfigureAwait(false); } /// diff --git a/src/A2A/Client/A2AHttpJsonClient.cs b/src/A2A/Client/A2AHttpJsonClient.cs index 77062f4b..a862235f 100644 --- a/src/A2A/Client/A2AHttpJsonClient.cs +++ b/src/A2A/Client/A2AHttpJsonClient.cs @@ -102,11 +102,13 @@ public async IAsyncEnumerable SubscribeToTaskAsync(SubscribeToTa /// public async Task CreateTaskPushNotificationConfigAsync( - CreateTaskPushNotificationConfigRequest request, CancellationToken cancellationToken = default) + TaskPushNotificationConfig config, CancellationToken cancellationToken = default) { - return await PostJsonAsync( - $"/tasks/{Uri.EscapeDataString(request.TaskId)}/pushNotificationConfigs", - request.Config, "CreateTaskPushNotificationConfig", cancellationToken).ConfigureAwait(false); + ArgumentException.ThrowIfNullOrEmpty(config.TaskId, nameof(config.TaskId)); + + return await PostJsonAsync( + $"/tasks/{Uri.EscapeDataString(config.TaskId)}/pushNotificationConfigs", + config, "CreateTaskPushNotificationConfig", cancellationToken).ConfigureAwait(false); } /// @@ -119,16 +121,16 @@ public async Task GetTaskPushNotificationConfigAsync } /// - public async Task ListTaskPushNotificationConfigAsync( - ListTaskPushNotificationConfigRequest request, CancellationToken cancellationToken = default) + public async Task ListTaskPushNotificationConfigsAsync( + ListTaskPushNotificationConfigsRequest request, CancellationToken cancellationToken = default) { var query = BuildQueryString( ("pageSize", request.PageSize?.ToString(System.Globalization.CultureInfo.InvariantCulture)), ("pageToken", request.PageToken)); - return await GetJsonAsync( + return await GetJsonAsync( $"/tasks/{Uri.EscapeDataString(request.TaskId)}/pushNotificationConfigs{query}", - "ListTaskPushNotificationConfig", cancellationToken).ConfigureAwait(false); + "ListTaskPushNotificationConfigs", cancellationToken).ConfigureAwait(false); } /// diff --git a/src/A2A/Client/IA2AClient.cs b/src/A2A/Client/IA2AClient.cs index 2ee87869..6acfce07 100644 --- a/src/A2A/Client/IA2AClient.cs +++ b/src/A2A/Client/IA2AClient.cs @@ -40,10 +40,10 @@ public interface IA2AClient IAsyncEnumerable SubscribeToTaskAsync(SubscribeToTaskRequest request, CancellationToken cancellationToken = default); /// Creates a push notification configuration. - /// The create push notification config request. + /// The push notification configuration to create. /// A cancellation token. /// The created push notification configuration. - Task CreateTaskPushNotificationConfigAsync(CreateTaskPushNotificationConfigRequest request, CancellationToken cancellationToken = default); + Task CreateTaskPushNotificationConfigAsync(TaskPushNotificationConfig config, CancellationToken cancellationToken = default); /// Gets a push notification configuration. /// The get push notification config request. @@ -54,8 +54,8 @@ public interface IA2AClient /// Lists push notification configurations. /// The list push notification configs request. /// A cancellation token. - /// The list push notification config response. - Task ListTaskPushNotificationConfigAsync(ListTaskPushNotificationConfigRequest request, CancellationToken cancellationToken = default); + /// The list push notification configs response. + Task ListTaskPushNotificationConfigsAsync(ListTaskPushNotificationConfigsRequest request, CancellationToken cancellationToken = default); /// Deletes a push notification configuration. /// The delete push notification config request. diff --git a/src/A2A/JsonRpc/A2AMethods.cs b/src/A2A/JsonRpc/A2AMethods.cs index e81221f9..ec0e3e25 100644 --- a/src/A2A/JsonRpc/A2AMethods.cs +++ b/src/A2A/JsonRpc/A2AMethods.cs @@ -28,7 +28,7 @@ public static class A2AMethods public const string GetTaskPushNotificationConfig = "GetTaskPushNotificationConfig"; /// List push notification configurations. - public const string ListTaskPushNotificationConfig = "ListTaskPushNotificationConfig"; + public const string ListTaskPushNotificationConfigs = "ListTaskPushNotificationConfigs"; /// Delete a push notification configuration. public const string DeleteTaskPushNotificationConfig = "DeleteTaskPushNotificationConfig"; @@ -48,7 +48,7 @@ public static class A2AMethods /// /// The method name to check. /// True if the method is a push notification method, false otherwise. - public static bool IsPushNotificationMethod(string method) => method is CreateTaskPushNotificationConfig or GetTaskPushNotificationConfig or ListTaskPushNotificationConfig or DeleteTaskPushNotificationConfig; + public static bool IsPushNotificationMethod(string method) => method is CreateTaskPushNotificationConfig or GetTaskPushNotificationConfig or ListTaskPushNotificationConfigs or DeleteTaskPushNotificationConfig; /// /// Determines if a method name is valid for A2A JSON-RPC. diff --git a/src/A2A/Models/CreateTaskPushNotificationConfigRequest.cs b/src/A2A/Models/CreateTaskPushNotificationConfigRequest.cs deleted file mode 100644 index 94eaf979..00000000 --- a/src/A2A/Models/CreateTaskPushNotificationConfigRequest.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace A2A; - -using System.Text.Json.Serialization; - -/// Represents a request to create a push notification configuration. -public sealed class CreateTaskPushNotificationConfigRequest -{ - /// Gets or sets the tenant identifier. - public string? Tenant { get; set; } - - /// Gets or sets the task identifier. - [JsonRequired] - public string TaskId { get; set; } = string.Empty; - - /// Unique identifier for the configuration. - [JsonRequired] - public string ConfigId { get; set; } = string.Empty; - - /// Gets or sets the push notification configuration. - [JsonRequired] - public PushNotificationConfig Config { get; set; } = new(); -} diff --git a/src/A2A/Models/ListTaskPushNotificationConfigRequest.cs b/src/A2A/Models/ListTaskPushNotificationConfigsRequest.cs similarity index 88% rename from src/A2A/Models/ListTaskPushNotificationConfigRequest.cs rename to src/A2A/Models/ListTaskPushNotificationConfigsRequest.cs index 4569e9d5..57be08c9 100644 --- a/src/A2A/Models/ListTaskPushNotificationConfigRequest.cs +++ b/src/A2A/Models/ListTaskPushNotificationConfigsRequest.cs @@ -1,20 +1,20 @@ -namespace A2A; - -using System.Text.Json.Serialization; - -/// Represents a request to list push notification configurations. -public sealed class ListTaskPushNotificationConfigRequest -{ - /// Gets or sets the tenant identifier. - public string? Tenant { get; set; } - - /// Gets or sets the task identifier. - [JsonRequired] - public string TaskId { get; set; } = string.Empty; - - /// Maximum number of configs to return. - public int? PageSize { get; set; } - - /// Token for cursor-based pagination. - public string? PageToken { get; set; } -} +namespace A2A; + +using System.Text.Json.Serialization; + +/// Represents a request to list push notification configurations. +public sealed class ListTaskPushNotificationConfigsRequest +{ + /// Gets or sets the tenant identifier. + public string? Tenant { get; set; } + + /// Gets or sets the task identifier. + [JsonRequired] + public string TaskId { get; set; } = string.Empty; + + /// Maximum number of configs to return. + public int? PageSize { get; set; } + + /// Token for cursor-based pagination. + public string? PageToken { get; set; } +} diff --git a/src/A2A/Models/ListTaskPushNotificationConfigResponse.cs b/src/A2A/Models/ListTaskPushNotificationConfigsResponse.cs similarity index 80% rename from src/A2A/Models/ListTaskPushNotificationConfigResponse.cs rename to src/A2A/Models/ListTaskPushNotificationConfigsResponse.cs index 1ee76878..e3241480 100644 --- a/src/A2A/Models/ListTaskPushNotificationConfigResponse.cs +++ b/src/A2A/Models/ListTaskPushNotificationConfigsResponse.cs @@ -1,13 +1,13 @@ -namespace A2A; - -using System.Text.Json.Serialization; - -/// Represents the response to a list push notification config request. -public sealed class ListTaskPushNotificationConfigResponse -{ - /// Gets or sets the list of push notification configurations. - public List? Configs { get; set; } - - /// Gets or sets the token for the next page of results. - public string? NextPageToken { get; set; } -} +namespace A2A; + +using System.Text.Json.Serialization; + +/// Represents the response to a list push notification configs request. +public sealed class ListTaskPushNotificationConfigsResponse +{ + /// Gets or sets the list of push notification configurations. + public List? Configs { get; set; } + + /// Gets or sets the token for the next page of results. + public string? NextPageToken { get; set; } +} diff --git a/src/A2A/Models/PushNotificationConfig.cs b/src/A2A/Models/PushNotificationConfig.cs index 0515b95a..7de616f0 100644 --- a/src/A2A/Models/PushNotificationConfig.cs +++ b/src/A2A/Models/PushNotificationConfig.cs @@ -2,7 +2,11 @@ namespace A2A; using System.Text.Json.Serialization; -/// Represents a push notification configuration. +/// Represents a push notification configuration (v0.3 shape). +/// +/// In v1.0, the flat is preferred. +/// This class is retained for backward compatibility with v0.3 and the HTTP+JSON endpoints. +/// public sealed class PushNotificationConfig { /// Unique identifier for the push notification configuration. diff --git a/src/A2A/Models/SendMessageConfiguration.cs b/src/A2A/Models/SendMessageConfiguration.cs index 86ab60cd..a8e3a2da 100644 --- a/src/A2A/Models/SendMessageConfiguration.cs +++ b/src/A2A/Models/SendMessageConfiguration.cs @@ -8,8 +8,8 @@ public sealed class SendMessageConfiguration /// Gets or sets the accepted output modes. public List? AcceptedOutputModes { get; set; } - /// Gets or sets the push notification configuration. - public PushNotificationConfig? PushNotificationConfig { get; set; } + /// Gets or sets the push notification configuration for this task. + public TaskPushNotificationConfig? TaskPushNotificationConfig { get; set; } /// Gets or sets the history length to include. public int? HistoryLength { get; set; } diff --git a/src/A2A/Models/TaskPushNotificationConfig.cs b/src/A2A/Models/TaskPushNotificationConfig.cs index 36558721..ea0e455e 100644 --- a/src/A2A/Models/TaskPushNotificationConfig.cs +++ b/src/A2A/Models/TaskPushNotificationConfig.cs @@ -2,20 +2,24 @@ namespace A2A; using System.Text.Json.Serialization; -/// Represents a task-specific push notification configuration. +/// Represents a task-specific push notification configuration matching the v1 spec's flat structure. public sealed class TaskPushNotificationConfig { /// Gets or sets the configuration identifier. - [JsonRequired] - public string Id { get; set; } = string.Empty; + public string? Id { get; set; } /// Gets or sets the task identifier. - [JsonRequired] - public string TaskId { get; set; } = string.Empty; + public string? TaskId { get; set; } - /// Gets or sets the push notification configuration. + /// Gets or sets the URL for push notifications. [JsonRequired] - public PushNotificationConfig PushNotificationConfig { get; set; } = new(); + public string Url { get; set; } = string.Empty; + + /// Gets or sets the token for push notifications. + public string? Token { get; set; } + + /// Gets or sets the authentication information. + public AuthenticationInfo? Authentication { get; set; } /// Gets or sets the tenant identifier. public string? Tenant { get; set; } diff --git a/src/A2A/Server/A2AServer.cs b/src/A2A/Server/A2AServer.cs index 8d244d34..478e2587 100644 --- a/src/A2A/Server/A2AServer.cs +++ b/src/A2A/Server/A2AServer.cs @@ -517,7 +517,7 @@ public virtual async IAsyncEnumerable SubscribeToTaskAsync( /// public virtual Task CreateTaskPushNotificationConfigAsync( - CreateTaskPushNotificationConfigRequest request, CancellationToken cancellationToken = default) + TaskPushNotificationConfig config, CancellationToken cancellationToken = default) { throw new A2AException("Push notifications not supported.", A2AErrorCode.PushNotificationNotSupported); } @@ -530,8 +530,8 @@ public virtual Task GetTaskPushNotificationConfigAsy } /// - public virtual Task ListTaskPushNotificationConfigAsync( - ListTaskPushNotificationConfigRequest request, CancellationToken cancellationToken = default) + public virtual Task ListTaskPushNotificationConfigsAsync( + ListTaskPushNotificationConfigsRequest request, CancellationToken cancellationToken = default) { throw new A2AException("Push notifications not supported.", A2AErrorCode.PushNotificationNotSupported); } diff --git a/src/A2A/Server/IA2ARequestHandler.cs b/src/A2A/Server/IA2ARequestHandler.cs index 3dd760a8..dc53706c 100644 --- a/src/A2A/Server/IA2ARequestHandler.cs +++ b/src/A2A/Server/IA2ARequestHandler.cs @@ -43,10 +43,10 @@ public interface IA2ARequestHandler IAsyncEnumerable SubscribeToTaskAsync(SubscribeToTaskRequest request, CancellationToken cancellationToken = default); /// Creates a push notification configuration. - /// The create push notification config request. + /// The push notification configuration to create. /// A cancellation token. /// The created push notification configuration. - Task CreateTaskPushNotificationConfigAsync(CreateTaskPushNotificationConfigRequest request, CancellationToken cancellationToken = default); + Task CreateTaskPushNotificationConfigAsync(TaskPushNotificationConfig config, CancellationToken cancellationToken = default); /// Gets a push notification configuration. /// The get push notification config request. @@ -57,8 +57,8 @@ public interface IA2ARequestHandler /// Lists push notification configurations. /// The list push notification configs request. /// A cancellation token. - /// The list push notification config response. - Task ListTaskPushNotificationConfigAsync(ListTaskPushNotificationConfigRequest request, CancellationToken cancellationToken = default); + /// The list push notification configs response. + Task ListTaskPushNotificationConfigsAsync(ListTaskPushNotificationConfigsRequest request, CancellationToken cancellationToken = default); /// Deletes a push notification configuration. /// The delete push notification config request. diff --git a/tests/A2A.AspNetCore.UnitTests/A2AJsonRpcProcessorTests.cs b/tests/A2A.AspNetCore.UnitTests/A2AJsonRpcProcessorTests.cs index 72e2db15..046d8a12 100644 --- a/tests/A2A.AspNetCore.UnitTests/A2AJsonRpcProcessorTests.cs +++ b/tests/A2A.AspNetCore.UnitTests/A2AJsonRpcProcessorTests.cs @@ -602,7 +602,7 @@ public async Task ProcessRequestAsync_PushNotificationMethod_ReturnsNotSupported "jsonrpc": "2.0", "method": "{{A2AMethods.CreateTaskPushNotificationConfig}}", "id": "pn-1", - "params": { "taskId": "some-task", "pushNotificationConfig": { "url": "https://example.com/callback" } } + "params": { "taskId": "some-task", "url": "https://example.com/callback" } } """; diff --git a/tests/A2A.AspNetCore.UnitTests/ClientTests.cs b/tests/A2A.AspNetCore.UnitTests/ClientTests.cs index aedc70ba..9934dbb9 100644 --- a/tests/A2A.AspNetCore.UnitTests/ClientTests.cs +++ b/tests/A2A.AspNetCore.UnitTests/ClientTests.cs @@ -100,14 +100,11 @@ public async Task TestCreatePushNotificationConfig() { Id = "response-config-id", TaskId = "test-task", - PushNotificationConfig = new PushNotificationConfig + Url = "http://example.org/notify", + Token = "test-token", + Authentication = new AuthenticationInfo { - Url = "http://example.org/notify", - Token = "test-token", - Authentication = new AuthenticationInfo - { - Scheme = "Bearer" - } + Scheme = "Bearer" } }; @@ -118,17 +115,15 @@ public async Task TestCreatePushNotificationConfig() }; }; - var createRequest = new CreateTaskPushNotificationConfigRequest + var createRequest = new TaskPushNotificationConfig() { + Id = "cfg-1", TaskId = "test-task", - Config = new PushNotificationConfig() + Url = "http://example.org/notify", + Token = "test-token", + Authentication = new AuthenticationInfo() { - Url = "http://example.org/notify", - Token = "test-token", - Authentication = new AuthenticationInfo() - { - Scheme = "Bearer", - } + Scheme = "Bearer", } }; diff --git a/tests/A2A.UnitTests/Client/A2AClientTests.cs b/tests/A2A.UnitTests/Client/A2AClientTests.cs index 73604b3d..5e954617 100644 --- a/tests/A2A.UnitTests/Client/A2AClientTests.cs +++ b/tests/A2A.UnitTests/Client/A2AClientTests.cs @@ -33,7 +33,7 @@ public async Task SendMessageAsync_MapsRequestParamsCorrectly() Configuration = new SendMessageConfiguration { AcceptedOutputModes = ["mode1"], - PushNotificationConfig = new PushNotificationConfig { Url = "http://push" }, + TaskPushNotificationConfig = new TaskPushNotificationConfig { Id = "cfg-1", TaskId = "t-1", Url = "http://push" }, HistoryLength = 5, ReturnImmediately = true }, @@ -339,17 +339,21 @@ public async Task CreatePushNotificationConfigAsync_SendsCorrectMethod() string? capturedBody = null; var sut = CreateA2AClient( - new TaskPushNotificationConfig { Id = "cfg-1", TaskId = "t-1", PushNotificationConfig = new PushNotificationConfig { Url = "http://push" } }, + new TaskPushNotificationConfig { Id = "cfg-1", TaskId = "t-1", Url = "http://push" }, req => capturedBody = req.Content!.ReadAsStringAsync().GetAwaiter().GetResult()); // Act - await sut.CreateTaskPushNotificationConfigAsync(new CreateTaskPushNotificationConfigRequest { TaskId = "t-1", ConfigId = "cfg-1", Config = new PushNotificationConfig { Url = "http://push" } }); + await sut.CreateTaskPushNotificationConfigAsync(new TaskPushNotificationConfig { Id = "cfg-1", TaskId = "t-1", Url = "http://push" }); // Assert Assert.NotNull(capturedBody); var requestJson = JsonDocument.Parse(capturedBody); Assert.Equal(A2AMethods.CreateTaskPushNotificationConfig, requestJson.RootElement.GetProperty("method").GetString()); + var parameters = requestJson.RootElement.GetProperty("params"); + Assert.Equal("t-1", parameters.GetProperty("taskId").GetString()); + Assert.Equal("http://push", parameters.GetProperty("url").GetString()); + Assert.False(parameters.TryGetProperty("config", out _)); } [Fact] diff --git a/tests/A2A.UnitTests/Client/A2AHttpJsonClientTests.cs b/tests/A2A.UnitTests/Client/A2AHttpJsonClientTests.cs index 8651dd0d..7ef718aa 100644 --- a/tests/A2A.UnitTests/Client/A2AHttpJsonClientTests.cs +++ b/tests/A2A.UnitTests/Client/A2AHttpJsonClientTests.cs @@ -193,17 +193,12 @@ public async Task CreateTaskPushNotificationConfigAsync_PostsCorrectBody() { Id = "cfg-1", TaskId = "t-1", - PushNotificationConfig = new PushNotificationConfig { Url = "http://callback" } + Url = "http://callback" }; var sut = CreateClient(expected, req => captured = req); - await sut.CreateTaskPushNotificationConfigAsync(new CreateTaskPushNotificationConfigRequest - { - TaskId = "t-1", - ConfigId = "cfg-1", - Config = new PushNotificationConfig { Url = "http://callback" } - }); + await sut.CreateTaskPushNotificationConfigAsync(new TaskPushNotificationConfig { Id = "cfg-1", TaskId = "t-1", Url = "http://callback" }); Assert.NotNull(captured); Assert.Equal(HttpMethod.Post, captured.Method); @@ -218,7 +213,7 @@ public async Task GetTaskPushNotificationConfigAsync_UsesCorrectGetPath() { Id = "cfg-1", TaskId = "t-1", - PushNotificationConfig = new PushNotificationConfig { Url = "http://callback" } + Url = "http://callback" }; var sut = CreateClient(expected, req => captured = req); @@ -231,14 +226,14 @@ public async Task GetTaskPushNotificationConfigAsync_UsesCorrectGetPath() } [Fact] - public async Task ListTaskPushNotificationConfigAsync_UsesCorrectGetPathWithQuery() + public async Task ListTaskPushNotificationConfigsAsync_UsesCorrectGetPathWithQuery() { HttpRequestMessage? captured = null; - var expected = new ListTaskPushNotificationConfigResponse(); + var expected = new ListTaskPushNotificationConfigsResponse(); var sut = CreateClient(expected, req => captured = req); - await sut.ListTaskPushNotificationConfigAsync(new ListTaskPushNotificationConfigRequest + await sut.ListTaskPushNotificationConfigsAsync(new ListTaskPushNotificationConfigsRequest { TaskId = "t-1", PageSize = 5, @@ -554,11 +549,7 @@ public async Task ErrorInfo_PushNotificationNotSupported_DistinguishesFrom400() "PUSH_NOTIFICATION_NOT_SUPPORTED", "Push notifications not supported"); var ex = await Assert.ThrowsAsync(() => - sut.CreateTaskPushNotificationConfigAsync(new CreateTaskPushNotificationConfigRequest - { - TaskId = "t-1", - Config = new PushNotificationConfig { Url = "http://callback" } - })); + sut.CreateTaskPushNotificationConfigAsync(new TaskPushNotificationConfig { Id = "cfg-1", TaskId = "t-1", Url = "http://callback" })); Assert.Equal(A2AErrorCode.PushNotificationNotSupported, ex.ErrorCode); } diff --git a/tests/A2A.UnitTests/JsonRpc/JsonRpcRequestConverterTests.cs b/tests/A2A.UnitTests/JsonRpc/JsonRpcRequestConverterTests.cs index 88ed4e9b..df6d77a5 100644 --- a/tests/A2A.UnitTests/JsonRpc/JsonRpcRequestConverterTests.cs +++ b/tests/A2A.UnitTests/JsonRpc/JsonRpcRequestConverterTests.cs @@ -140,7 +140,7 @@ public void Read_ValidIdTypes_ReturnsCorrectId(string idJson, string? expectedSt [InlineData(A2AMethods.SubscribeToTask)] [InlineData(A2AMethods.CreateTaskPushNotificationConfig)] [InlineData(A2AMethods.GetTaskPushNotificationConfig)] - [InlineData(A2AMethods.ListTaskPushNotificationConfig)] + [InlineData(A2AMethods.ListTaskPushNotificationConfigs)] [InlineData(A2AMethods.DeleteTaskPushNotificationConfig)] [InlineData(A2AMethods.GetExtendedAgentCard)] public void Read_ValidMethods_ReturnsCorrectMethod(string method) @@ -489,7 +489,7 @@ public void RoundTrip_ValidJsonRpcRequest_PreservesAllData() [InlineData(A2AMethods.SubscribeToTask)] [InlineData(A2AMethods.CreateTaskPushNotificationConfig)] [InlineData(A2AMethods.GetTaskPushNotificationConfig)] - [InlineData(A2AMethods.ListTaskPushNotificationConfig)] + [InlineData(A2AMethods.ListTaskPushNotificationConfigs)] [InlineData(A2AMethods.DeleteTaskPushNotificationConfig)] [InlineData(A2AMethods.GetExtendedAgentCard)] public void RoundTrip_AllValidMethods_PreservesMethod(string method) diff --git a/tests/A2A.UnitTests/Server/A2AServerTests.cs b/tests/A2A.UnitTests/Server/A2AServerTests.cs index 34a74b89..dd505c84 100644 --- a/tests/A2A.UnitTests/Server/A2AServerTests.cs +++ b/tests/A2A.UnitTests/Server/A2AServerTests.cs @@ -443,7 +443,7 @@ public async Task PushNotificationConfig_ThrowsNotSupported() // Act & Assert await Assert.ThrowsAsync(() => - server.CreateTaskPushNotificationConfigAsync(new CreateTaskPushNotificationConfigRequest())); + server.CreateTaskPushNotificationConfigAsync(new TaskPushNotificationConfig())); await Assert.ThrowsAsync(() => server.GetTaskPushNotificationConfigAsync(new GetTaskPushNotificationConfigRequest())); }