Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 111 additions & 1 deletion docs/migration-guide-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |

---
Expand Down Expand Up @@ -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<TaskPushNotificationConfig> CreateTaskPushNotificationConfigAsync(
CreateTaskPushNotificationConfigRequest request, CancellationToken ct);
Task<ListTaskPushNotificationConfigResponse> ListTaskPushNotificationConfigAsync(
ListTaskPushNotificationConfigRequest request, CancellationToken ct);

// V1
Task<TaskPushNotificationConfig> CreateTaskPushNotificationConfigAsync(
TaskPushNotificationConfig config, CancellationToken ct);
Task<ListTaskPushNotificationConfigsResponse> ListTaskPushNotificationConfigsAsync(
ListTaskPushNotificationConfigsRequest request, CancellationToken ct);
```

---

## Server API

V1 introduces `ITaskManager` for server implementations. Use `TaskManager` as a base or implement the interface directly.
Expand Down
4 changes: 3 additions & 1 deletion samples/A2ACli/Host/A2ACli.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,10 @@ private static async Task<bool> 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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the standalone create API, the config is being added to an already-known task, so taskId identifies the parent task for that operation. But in SendMessage, the push config is attached while the message is being sent, and the server should associate it with the task created or selected by that SendMessage request.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TaskId = taskId says which task this config belongs to. In the embedded SendMessage path, that should come from the SendMessage task context, so I think this should be omitted.

Url = $"http://{notificationReceiverHost}:{notificationReceiverPort}/notify",
Authentication = new AuthenticationInfo
{
Expand Down
2 changes: 1 addition & 1 deletion src/A2A.AspNetCore/A2AEndpointRouteBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 7 additions & 11 deletions src/A2A.AspNetCore/A2AHttpProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,12 @@ internal static Task<IResult> GetExtendedAgentCardRestAsync(

// REST handler: Create push notification config
internal static Task<IResult> CreatePushNotificationConfigRestAsync(
IA2ARequestHandler requestHandler, ILogger logger, string taskId, PushNotificationConfig config, CancellationToken cancellationToken)
IA2ARequestHandler requestHandler, ILogger logger, string taskId, TaskPushNotificationConfig config, CancellationToken cancellationToken)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One REST behavior to clarify: this endpoint now binds the body as TaskPushNotificationConfig, which includes Tenant, then forwards the config after only overriding TaskId from the route.

That means a non-tenant REST call to /tasks/{id}/pushNotificationConfigs can still pass tenant in the body to the handler. But MapHttpA2A currently documents tenant REST variants as unsupported and says request Tenant fields are always null for REST calls.

Could we make this consistent one way or the other?

If tenant REST is out of scope, reject or clear config.Tenant on the non-tenant REST route and add a test for that behavior.
If body tenant is intentionally supported on the non-tenant route, update the REST limitation docs and add a route/body test so this is explicit.

=> 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);

Expand All @@ -215,13 +211,13 @@ internal static Task<IResult> 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);
Expand Down Expand Up @@ -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)
{
Expand Down
8 changes: 4 additions & 4 deletions src/A2A.AspNetCore/A2AJsonRpcProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ internal static async Task<JsonRpcResponseResult> SingleResponseAsync(IA2AReques
response = JsonRpcResponse.CreateJsonRpcResponse(requestId, cancelledTask);
break;
case A2AMethods.CreateTaskPushNotificationConfig:
var createPnConfig = DeserializeAndValidate<CreateTaskPushNotificationConfigRequest>(parameters.Value);
var createPnConfig = DeserializeAndValidate<TaskPushNotificationConfig>(parameters.Value);
var createdConfig = await requestHandler.CreateTaskPushNotificationConfigAsync(createPnConfig, cancellationToken).ConfigureAwait(false);
response = JsonRpcResponse.CreateJsonRpcResponse(requestId, createdConfig);
break;
Expand All @@ -151,9 +151,9 @@ internal static async Task<JsonRpcResponseResult> SingleResponseAsync(IA2AReques
var gotConfig = await requestHandler.GetTaskPushNotificationConfigAsync(getPnConfig, cancellationToken).ConfigureAwait(false);
response = JsonRpcResponse.CreateJsonRpcResponse(requestId, gotConfig);
break;
case A2AMethods.ListTaskPushNotificationConfig:
var listPnConfig = DeserializeAndValidate<ListTaskPushNotificationConfigRequest>(parameters.Value);
var listPnResult = await requestHandler.ListTaskPushNotificationConfigAsync(listPnConfig, cancellationToken).ConfigureAwait(false);
case A2AMethods.ListTaskPushNotificationConfigs:
var listPnConfig = DeserializeAndValidate<ListTaskPushNotificationConfigsRequest>(parameters.Value);
var listPnResult = await requestHandler.ListTaskPushNotificationConfigsAsync(listPnConfig, cancellationToken).ConfigureAwait(false);
response = JsonRpcResponse.CreateJsonRpcResponse(requestId, listPnResult);
break;
case A2AMethods.DeleteTaskPushNotificationConfig:
Expand Down
10 changes: 5 additions & 5 deletions src/A2A.V0_3Compat/V03ClientAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,13 @@ internal V03ClientAdapter(V03.A2AClient v03Client)

/// <inheritdoc />
public async Task<A2A.TaskPushNotificationConfig> 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);
Expand All @@ -116,8 +116,8 @@ internal V03ClientAdapter(V03.A2AClient v03Client)
}

/// <inheritdoc />
public Task<A2A.ListTaskPushNotificationConfigResponse> ListTaskPushNotificationConfigAsync(
A2A.ListTaskPushNotificationConfigRequest request,
public Task<A2A.ListTaskPushNotificationConfigsResponse> ListTaskPushNotificationConfigsAsync(
A2A.ListTaskPushNotificationConfigsRequest request,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException("v0.3 does not support listing push notification configs.");

Expand Down
22 changes: 9 additions & 13 deletions src/A2A.V0_3Compat/V03ServerProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ private static async Task<IResult> DispatchV1SingleAsync(
}
case A2AMethods.CreateTaskPushNotificationConfig:
{
var req = DeserializeV1Params<CreateTaskPushNotificationConfigRequest>(paramsEl.Value, id);
var req = DeserializeV1Params<TaskPushNotificationConfig>(paramsEl.Value, id);
response = JsonRpcResponse.CreateJsonRpcResponse(id, await handler.CreateTaskPushNotificationConfigAsync(req, ct).ConfigureAwait(false));
break;
}
Expand All @@ -241,10 +241,10 @@ private static async Task<IResult> DispatchV1SingleAsync(
response = JsonRpcResponse.CreateJsonRpcResponse(id, await handler.GetTaskPushNotificationConfigAsync(req, ct).ConfigureAwait(false));
break;
}
case A2AMethods.ListTaskPushNotificationConfig:
case A2AMethods.ListTaskPushNotificationConfigs:
{
var req = DeserializeV1Params<ListTaskPushNotificationConfigRequest>(paramsEl.Value, id);
response = JsonRpcResponse.CreateJsonRpcResponse(id, await handler.ListTaskPushNotificationConfigAsync(req, ct).ConfigureAwait(false));
var req = DeserializeV1Params<ListTaskPushNotificationConfigsRequest>(paramsEl.Value, id);
response = JsonRpcResponse.CreateJsonRpcResponse(id, await handler.ListTaskPushNotificationConfigsAsync(req, ct).ConfigureAwait(false));
break;
}
case A2AMethods.DeleteTaskPushNotificationConfig:
Expand Down Expand Up @@ -365,16 +365,12 @@ private static async Task<IResult> HandleSingleAsync(
case V03.A2AMethods.TaskPushNotificationConfigSet:
{
var v03Config = DeserializeParams<V03.TaskPushNotificationConfig>(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));
}
Expand All @@ -390,8 +386,8 @@ private static async Task<IResult> 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));
}
Expand Down
Loading