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
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,
Url = $"http://{notificationReceiverHost}:{notificationReceiverPort}/notify",
Authentication = new AuthenticationInfo
{
Expand Down
11 changes: 8 additions & 3 deletions src/A2A.AspNetCore/A2AHttpProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,14 @@ internal static Task<IResult> CreatePushNotificationConfigRestAsync(
{
var request = new CreateTaskPushNotificationConfigRequest
{
TaskId = taskId,
Config = config,
ConfigId = config.Id ?? string.Empty,
Config = new TaskPushNotificationConfig
{
Id = config.Id ?? string.Empty,
TaskId = taskId,
Url = config.Url,
Token = config.Token,
Authentication = config.Authentication,
},
};
var result = await requestHandler.CreateTaskPushNotificationConfigAsync(request, ct).ConfigureAwait(false);
return new A2AResponseResult(result);
Expand Down
4 changes: 2 additions & 2 deletions src/A2A.V0_3Compat/V03ClientAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ internal V03ClientAdapter(V03.A2AClient v03Client)
{
var v03Config = new V03.TaskPushNotificationConfig
{
TaskId = request.TaskId,
PushNotificationConfig = V03TypeConverter.ToV03PushNotificationConfig(request.Config),
TaskId = request.Config.TaskId ?? string.Empty,
PushNotificationConfig = V03TypeConverter.ToV03PushNotificationConfigFromTask(request.Config),
};
var v03Result = await _v03Client.SetPushNotificationAsync(v03Config, cancellationToken).ConfigureAwait(false);
return V03TypeConverter.ToV1TaskPushNotificationConfig(v03Result);
Expand Down
11 changes: 5 additions & 6 deletions src/A2A.V0_3Compat/V03ServerProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -367,14 +367,13 @@ private static async Task<IResult> HandleSingleAsync(
var v03Config = DeserializeParams<V03.TaskPushNotificationConfig>(rpcRequest.Params.Value);
var v1Request = new CreateTaskPushNotificationConfigRequest
{
TaskId = v03Config.TaskId,
Config = V03TypeConverter.ToV1PushNotificationConfig(v03Config.PushNotificationConfig),
Config = 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 +389,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
86 changes: 75 additions & 11 deletions src/A2A.V0_3Compat/V03TypeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -424,6 +424,56 @@ internal static A2A.Artifact ToV1Artifact(V03.Artifact artifact) =>

// ──── Push notification config conversion ────

/// <summary>Converts a v1.0 flat TaskPushNotificationConfig to a v0.3 PushNotificationConfig.</summary>
/// <param name="config">The v1.0 task push notification config to convert.</param>
/// <returns>The converted v0.3 push notification config.</returns>
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;
}

/// <summary>Converts a v0.3 PushNotificationConfig to a v1.0 flat TaskPushNotificationConfig.</summary>
/// <param name="config">The v0.3 push notification config to convert.</param>
/// <param name="taskId">The task identifier to include in the result.</param>
/// <returns>The converted v1.0 task push notification config.</returns>
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;
}

/// <summary>Converts a v1.0 push notification config to v0.3.</summary>
/// <param name="config">The v1.0 config to convert.</param>
/// <returns>The converted v0.3 push notification config.</returns>
Expand Down Expand Up @@ -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;
}

/// <summary>Converts a v0.3 task push notification config to v1.0.</summary>
/// <summary>Converts a v0.3 task push notification config to v1.0 (flat structure).</summary>
/// <param name="config">The v0.3 config to convert.</param>
/// <returns>The converted v1.0 task push notification config.</returns>
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) ────

/// <summary>Converts v0.3 message send params to a v1.0 send message request.</summary>
Expand All @@ -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, p.Message.TaskId ?? string.Empty)
: null,
} : null,
Metadata = p.Metadata,
Expand Down
6 changes: 4 additions & 2 deletions src/A2A/Client/A2AHttpJsonClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,10 @@ public async IAsyncEnumerable<StreamResponse> SubscribeToTaskAsync(SubscribeToTa
public async Task<TaskPushNotificationConfig> CreateTaskPushNotificationConfigAsync(
CreateTaskPushNotificationConfigRequest request, CancellationToken cancellationToken = default)
{
return await PostJsonAsync<PushNotificationConfig, TaskPushNotificationConfig>(
$"/tasks/{Uri.EscapeDataString(request.TaskId)}/pushNotificationConfigs",
ArgumentException.ThrowIfNullOrEmpty(request.Config.TaskId, "request.Config.TaskId");

return await PostJsonAsync<TaskPushNotificationConfig, TaskPushNotificationConfig>(
$"/tasks/{Uri.EscapeDataString(request.Config.TaskId)}/pushNotificationConfigs",
request.Config, "CreateTaskPushNotificationConfig", cancellationToken).ConfigureAwait(false);
}
Comment on lines 104 to 112

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.

medium

If request.Config.TaskId is null or empty, the constructed URL will be malformed (e.g., /tasks//pushNotificationConfigs), leading to a generic HTTP error. Adding validation prevents invalid requests from being sent.

    public async Task<TaskPushNotificationConfig> CreateTaskPushNotificationConfigAsync(
        CreateTaskPushNotificationConfigRequest request, CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        if (string.IsNullOrEmpty(request.Config?.TaskId))
        {
            throw new ArgumentException("TaskId must be specified in the configuration.", nameof(request));
        }

        return await PostJsonAsync<TaskPushNotificationConfig, TaskPushNotificationConfig>(
            $"/tasks/{Uri.EscapeDataString(request.Config.TaskId)}/pushNotificationConfigs",
            request.Config, "CreateTaskPushNotificationConfig", cancellationToken).ConfigureAwait(false);
    }

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed added ArgumentException.ThrowIfNullOrEmpty(request.Config.TaskId) validation before constructing the URL.


Expand Down
15 changes: 2 additions & 13 deletions src/A2A/Models/CreateTaskPushNotificationConfigRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,7 @@ namespace A2A;
/// <summary>Represents a request to create a push notification configuration.</summary>
public sealed class CreateTaskPushNotificationConfigRequest
{
/// <summary>Gets or sets the tenant identifier.</summary>
public string? Tenant { get; set; }

/// <summary>Gets or sets the task identifier.</summary>
[JsonRequired]
public string TaskId { get; set; } = string.Empty;

/// <summary>Unique identifier for the configuration.</summary>
[JsonRequired]
public string ConfigId { get; set; } = string.Empty;

/// <summary>Gets or sets the push notification configuration.</summary>
/// <summary>Gets or sets the push notification configuration to create.</summary>
[JsonRequired]
public PushNotificationConfig Config { get; set; } = new();
public TaskPushNotificationConfig Config { get; set; } = new();
}
6 changes: 5 additions & 1 deletion src/A2A/Models/PushNotificationConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ namespace A2A;

using System.Text.Json.Serialization;

/// <summary>Represents a push notification configuration.</summary>
/// <summary>Represents a push notification configuration (v0.3 shape).</summary>
/// <remarks>
/// In v1.0, the flat <see cref="TaskPushNotificationConfig"/> is preferred.
/// This class is retained for backward compatibility with v0.3 and the HTTP+JSON endpoints.
/// </remarks>
public sealed class PushNotificationConfig
{
/// <summary>Unique identifier for the push notification configuration.</summary>
Expand Down
4 changes: 2 additions & 2 deletions src/A2A/Models/SendMessageConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ public sealed class SendMessageConfiguration
/// <summary>Gets or sets the accepted output modes.</summary>
public List<string>? AcceptedOutputModes { get; set; }

/// <summary>Gets or sets the push notification configuration.</summary>
public PushNotificationConfig? PushNotificationConfig { get; set; }
/// <summary>Gets or sets the push notification configuration for this task.</summary>
public TaskPushNotificationConfig? TaskPushNotificationConfig { get; set; }

/// <summary>Gets or sets the history length to include.</summary>
public int? HistoryLength { get; set; }
Expand Down
18 changes: 11 additions & 7 deletions src/A2A/Models/TaskPushNotificationConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@ namespace A2A;

using System.Text.Json.Serialization;

/// <summary>Represents a task-specific push notification configuration.</summary>
/// <summary>Represents a task-specific push notification configuration matching the v1 spec's flat structure.</summary>
public sealed class TaskPushNotificationConfig
{
/// <summary>Gets or sets the configuration identifier.</summary>
[JsonRequired]
public string Id { get; set; } = string.Empty;
public string? Id { get; set; }

/// <summary>Gets or sets the task identifier.</summary>
[JsonRequired]
public string TaskId { get; set; } = string.Empty;
public string? TaskId { get; set; }

/// <summary>Gets or sets the push notification configuration.</summary>
/// <summary>Gets or sets the URL for push notifications.</summary>
[JsonRequired]
public PushNotificationConfig PushNotificationConfig { get; set; } = new();
public string Url { get; set; } = string.Empty;

/// <summary>Gets or sets the token for push notifications.</summary>
public string? Token { get; set; }

/// <summary>Gets or sets the authentication information.</summary>
public AuthenticationInfo? Authentication { get; set; }

/// <summary>Gets or sets the tenant identifier.</summary>
public string? Tenant { get; set; }
Expand Down
16 changes: 7 additions & 9 deletions tests/A2A.AspNetCore.UnitTests/ClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
};

Expand All @@ -120,9 +117,10 @@ public async Task TestCreatePushNotificationConfig()

var createRequest = new CreateTaskPushNotificationConfigRequest
{
TaskId = "test-task",
Config = new PushNotificationConfig()
Config = new TaskPushNotificationConfig()
{
Id = "cfg-1",
TaskId = "test-task",
Url = "http://example.org/notify",
Token = "test-token",
Authentication = new AuthenticationInfo()
Expand Down
6 changes: 3 additions & 3 deletions tests/A2A.UnitTests/Client/A2AClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down Expand Up @@ -339,11 +339,11 @@ 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 CreateTaskPushNotificationConfigRequest { Config = new TaskPushNotificationConfig { Id = "cfg-1", TaskId = "t-1", Url = "http://push" } });

// Assert
Assert.NotNull(capturedBody);
Expand Down
11 changes: 4 additions & 7 deletions tests/A2A.UnitTests/Client/A2AHttpJsonClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,16 +193,14 @@ 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" }
Config = new TaskPushNotificationConfig { Id = "cfg-1", TaskId = "t-1", Url = "http://callback" }
});

Assert.NotNull(captured);
Expand All @@ -218,7 +216,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);
Expand Down Expand Up @@ -556,8 +554,7 @@ public async Task ErrorInfo_PushNotificationNotSupported_DistinguishesFrom400()
var ex = await Assert.ThrowsAsync<A2AException>(() =>
sut.CreateTaskPushNotificationConfigAsync(new CreateTaskPushNotificationConfigRequest
{
TaskId = "t-1",
Config = new PushNotificationConfig { Url = "http://callback" }
Config = new TaskPushNotificationConfig { Id = "cfg-1", TaskId = "t-1", Url = "http://callback" }
}));

Assert.Equal(A2AErrorCode.PushNotificationNotSupported, ex.ErrorCode);
Expand Down