diff --git a/examples/cancel_task_client.cpp b/examples/cancel_task_client.cpp index 3f82d46..73c3edf 100644 --- a/examples/cancel_task_client.cpp +++ b/examples/cancel_task_client.cpp @@ -34,7 +34,7 @@ int main() { a2a::client::A2AClient client(std::move(transport)); lf::a2a::v1::SendMessageRequest send_request; - send_request.mutable_message()->set_role("user"); + send_request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); send_request.mutable_message()->set_task_id("cancel-example-task"); const auto send = client.SendMessage(send_request); if (!send.ok()) { diff --git a/examples/example_support.h b/examples/example_support.h index a6369f2..9f43d72 100644 --- a/examples/example_support.h +++ b/examples/example_support.h @@ -53,8 +53,8 @@ class ExampleExecutor final : public server::AgentExecutor { lf::a2a::v1::Task task; task.set_id(task_id); task.mutable_status()->set_state(lf::a2a::v1::TASK_STATE_WORKING); - task.mutable_status()->mutable_message()->set_role("agent"); - task.mutable_status()->mutable_message()->add_parts()->mutable_text()->set_text("ack"); + task.mutable_status()->mutable_message()->set_role(lf::a2a::v1::ROLE_AGENT); + task.mutable_status()->mutable_message()->add_parts()->set_text("ack"); task_ = task; lf::a2a::v1::SendMessageResponse response; @@ -77,7 +77,6 @@ class ExampleExecutor final : public server::AgentExecutor { completed.mutable_status_update()->set_task_id(request.message().task_id()); completed.mutable_status_update()->mutable_status()->set_state( lf::a2a::v1::TASK_STATE_COMPLETED); - completed.mutable_status_update()->set_final(true); std::vector events; events.push_back(working); @@ -124,21 +123,29 @@ class ExampleExecutor final : public server::AgentExecutor { inline lf::a2a::v1::AgentCard BuildRestAgentCard(std::string_view name, std::string_view url) { lf::a2a::v1::AgentCard card; - card.set_protocol_version("1.0"); card.set_name(std::string(name)); + card.set_description("example rest agent"); + card.set_version("1.0.0"); + card.add_default_input_modes("text/plain"); + card.add_default_output_modes("text/plain"); auto* iface = card.add_supported_interfaces(); - iface->set_transport(lf::a2a::v1::TRANSPORT_PROTOCOL_REST); iface->set_url(std::string(url)); + iface->set_protocol_binding("HTTP+JSON"); + iface->set_protocol_version("1.0"); return card; } inline lf::a2a::v1::AgentCard BuildJsonRpcAgentCard(std::string_view name, std::string_view url) { lf::a2a::v1::AgentCard card; - card.set_protocol_version("1.0"); card.set_name(std::string(name)); + card.set_description("example json-rpc agent"); + card.set_version("1.0.0"); + card.add_default_input_modes("text/plain"); + card.add_default_output_modes("text/plain"); auto* iface = card.add_supported_interfaces(); - iface->set_transport(lf::a2a::v1::TRANSPORT_PROTOCOL_JSON_RPC); iface->set_url(std::string(url)); + iface->set_protocol_binding("JSONRPC"); + iface->set_protocol_version("1.0"); return card; } diff --git a/examples/json_rpc_client.cpp b/examples/json_rpc_client.cpp index 0eef3c0..a548ab6 100644 --- a/examples/json_rpc_client.cpp +++ b/examples/json_rpc_client.cpp @@ -34,7 +34,7 @@ int main() { a2a::client::A2AClient client(std::move(transport)); lf::a2a::v1::SendMessageRequest request; - request.mutable_message()->set_role("user"); + request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); request.mutable_message()->set_task_id("json-rpc-example-task"); const auto send = client.SendMessage(request); diff --git a/examples/list_tasks_client.cpp b/examples/list_tasks_client.cpp index b8b123e..170a379 100644 --- a/examples/list_tasks_client.cpp +++ b/examples/list_tasks_client.cpp @@ -34,7 +34,7 @@ int main() { a2a::client::A2AClient client(std::move(transport)); lf::a2a::v1::SendMessageRequest seed; - seed.mutable_message()->set_role("user"); + seed.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); seed.mutable_message()->set_task_id("list-example-task"); const auto send = client.SendMessage(seed); if (!send.ok()) { diff --git a/examples/push_notification_config_client.cpp b/examples/push_notification_config_client.cpp index be2a18c..e7d9684 100644 --- a/examples/push_notification_config_client.cpp +++ b/examples/push_notification_config_client.cpp @@ -37,7 +37,7 @@ int main() { lf::a2a::v1::TaskPushNotificationConfig cfg; cfg.set_id("cfg-1"); cfg.set_task_id("task-1"); - cfg.set_endpoint("https://callback.example/notify"); + cfg.set_url("https://callback.example/notify"); const auto set_result = client.CreateTaskPushNotificationConfig(cfg); if (!set_result.ok()) { std::cerr << "set failed: " << set_result.error().message() << '\n'; diff --git a/examples/rest_client.cpp b/examples/rest_client.cpp index 961e9b6..e61335b 100644 --- a/examples/rest_client.cpp +++ b/examples/rest_client.cpp @@ -37,7 +37,7 @@ int main() { a2a::client::A2AClient client(std::move(transport)); lf::a2a::v1::SendMessageRequest request; - request.mutable_message()->set_role("user"); + request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); request.mutable_message()->set_task_id("rest-example-task"); const auto send = client.SendMessage(request); diff --git a/examples/streaming_client.cpp b/examples/streaming_client.cpp index 08aa3a6..bf633e9 100644 --- a/examples/streaming_client.cpp +++ b/examples/streaming_client.cpp @@ -66,7 +66,7 @@ int main() { PrintingObserver observer; lf::a2a::v1::SendMessageRequest request; - request.mutable_message()->set_role("user"); + request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); request.mutable_message()->set_task_id("stream-example-task"); const auto handle = client.SendStreamingMessage(request, observer); diff --git a/include/a2a/client/client.h b/include/a2a/client/client.h index db331c7..e8b042a 100644 --- a/include/a2a/client/client.h +++ b/include/a2a/client/client.h @@ -117,7 +117,7 @@ class ClientTransport { [[nodiscard]] virtual core::Result CreateTaskPushNotificationConfig(const lf::a2a::v1::TaskPushNotificationConfig& request, - const CallOptions& options) = 0; + const CallOptions& options) = 0; [[nodiscard]] virtual core::Result GetTaskPushNotificationConfig(const lf::a2a::v1::GetTaskPushNotificationConfigRequest& request, @@ -161,8 +161,9 @@ class A2AClient final { [[nodiscard]] core::Result CancelTask( const lf::a2a::v1::CancelTaskRequest& request, const CallOptions& options = {}); - [[nodiscard]] core::Result CreateTaskPushNotificationConfig( - const lf::a2a::v1::TaskPushNotificationConfig& request, const CallOptions& options = {}); + [[nodiscard]] core::Result + CreateTaskPushNotificationConfig(const lf::a2a::v1::TaskPushNotificationConfig& request, + const CallOptions& options = {}); [[nodiscard]] core::Result GetTaskPushNotificationConfig( const lf::a2a::v1::GetTaskPushNotificationConfigRequest& request, diff --git a/include/a2a/client/grpc_transport.h b/include/a2a/client/grpc_transport.h index 31f5b04..7cf0f87 100644 --- a/include/a2a/client/grpc_transport.h +++ b/include/a2a/client/grpc_transport.h @@ -86,8 +86,9 @@ class GrpcTransport final : public ClientTransport { [[nodiscard]] core::Result CancelTask( const lf::a2a::v1::CancelTaskRequest& request, const CallOptions& options) override; - [[nodiscard]] core::Result CreateTaskPushNotificationConfig( - const lf::a2a::v1::TaskPushNotificationConfig& request, const CallOptions& options) override; + [[nodiscard]] core::Result + CreateTaskPushNotificationConfig(const lf::a2a::v1::TaskPushNotificationConfig& request, + const CallOptions& options) override; [[nodiscard]] core::Result GetTaskPushNotificationConfig( const lf::a2a::v1::GetTaskPushNotificationConfigRequest& request, diff --git a/include/a2a/client/http_json_transport.h b/include/a2a/client/http_json_transport.h index adbb979..b75e75e 100644 --- a/include/a2a/client/http_json_transport.h +++ b/include/a2a/client/http_json_transport.h @@ -60,8 +60,9 @@ class HttpJsonTransport final : public ClientTransport { [[nodiscard]] core::Result CancelTask( const lf::a2a::v1::CancelTaskRequest& request, const CallOptions& options) override; - [[nodiscard]] core::Result CreateTaskPushNotificationConfig( - const lf::a2a::v1::TaskPushNotificationConfig& request, const CallOptions& options) override; + [[nodiscard]] core::Result + CreateTaskPushNotificationConfig(const lf::a2a::v1::TaskPushNotificationConfig& request, + const CallOptions& options) override; [[nodiscard]] core::Result GetTaskPushNotificationConfig( const lf::a2a::v1::GetTaskPushNotificationConfigRequest& request, diff --git a/include/a2a/client/json_rpc_transport.h b/include/a2a/client/json_rpc_transport.h index d2c9957..7570fbe 100644 --- a/include/a2a/client/json_rpc_transport.h +++ b/include/a2a/client/json_rpc_transport.h @@ -31,8 +31,9 @@ class JsonRpcTransport final : public ClientTransport { [[nodiscard]] core::Result CancelTask( const lf::a2a::v1::CancelTaskRequest& request, const CallOptions& options) override; - [[nodiscard]] core::Result CreateTaskPushNotificationConfig( - const lf::a2a::v1::TaskPushNotificationConfig& request, const CallOptions& options) override; + [[nodiscard]] core::Result + CreateTaskPushNotificationConfig(const lf::a2a::v1::TaskPushNotificationConfig& request, + const CallOptions& options) override; [[nodiscard]] core::Result GetTaskPushNotificationConfig( const lf::a2a::v1::GetTaskPushNotificationConfigRequest& request, diff --git a/include/a2a/core/protocol_bindings.h b/include/a2a/core/protocol_bindings.h new file mode 100644 index 0000000..fa2a271 --- /dev/null +++ b/include/a2a/core/protocol_bindings.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace a2a::core::protocol_bindings { + +inline constexpr std::string_view kHttpJson = "HTTP+JSON"; +inline constexpr std::string_view kJsonRpc = "JSONRPC"; +inline constexpr std::string_view kGrpc = "GRPC"; + +} // namespace a2a::core::protocol_bindings diff --git a/src/client/discovery.cpp b/src/client/discovery.cpp index c92dc3a..4e8f155 100644 --- a/src/client/discovery.cpp +++ b/src/client/discovery.cpp @@ -9,6 +9,7 @@ #include #include "a2a/core/error.h" +#include "a2a/core/protocol_bindings.h" #include "a2a/core/protojson.h" #include "a2a/core/version.h" @@ -45,45 +46,38 @@ bool HasHostPortShape(std::string_view endpoint) { return endpoint.find(':') != std::string_view::npos; } -bool IsValidInterfaceEndpoint(lf::a2a::v1::TransportProtocol transport, std::string_view endpoint) { - switch (transport) { - case lf::a2a::v1::TRANSPORT_PROTOCOL_REST: - case lf::a2a::v1::TRANSPORT_PROTOCOL_JSON_RPC: - return HasHttpScheme(endpoint); - case lf::a2a::v1::TRANSPORT_PROTOCOL_GRPC: - return HasGrpcScheme(endpoint) || HasHttpScheme(endpoint) || HasHostPortShape(endpoint); - case lf::a2a::v1::TRANSPORT_PROTOCOL_UNSPECIFIED: - case lf::a2a::v1::TransportProtocol_INT_MIN_SENTINEL_DO_NOT_USE_: - case lf::a2a::v1::TransportProtocol_INT_MAX_SENTINEL_DO_NOT_USE_: - return false; +using a2a::core::protocol_bindings::kGrpc; +using a2a::core::protocol_bindings::kHttpJson; +using a2a::core::protocol_bindings::kJsonRpc; + +bool IsValidInterfaceEndpoint(std::string_view protocol_binding, std::string_view endpoint) { + if (protocol_binding == kHttpJson || protocol_binding == kJsonRpc) { + return HasHttpScheme(endpoint); + } + if (protocol_binding == kGrpc) { + return HasGrpcScheme(endpoint) || HasHttpScheme(endpoint) || HasHostPortShape(endpoint); } return false; } -PreferredTransport ToPreferredTransport(lf::a2a::v1::TransportProtocol transport) { - switch (transport) { - case lf::a2a::v1::TRANSPORT_PROTOCOL_REST: - return PreferredTransport::kRest; - case lf::a2a::v1::TRANSPORT_PROTOCOL_JSON_RPC: - return PreferredTransport::kJsonRpc; - case lf::a2a::v1::TRANSPORT_PROTOCOL_GRPC: - return PreferredTransport::kGrpc; - case lf::a2a::v1::TRANSPORT_PROTOCOL_UNSPECIFIED: - case lf::a2a::v1::TransportProtocol_INT_MIN_SENTINEL_DO_NOT_USE_: - case lf::a2a::v1::TransportProtocol_INT_MAX_SENTINEL_DO_NOT_USE_: - break; +PreferredTransport ToPreferredTransport(std::string_view protocol_binding) { + if (protocol_binding == kHttpJson) { + return PreferredTransport::kRest; } - return PreferredTransport::kRest; + if (protocol_binding == kJsonRpc) { + return PreferredTransport::kJsonRpc; + } + return PreferredTransport::kGrpc; } -std::optional ToWireTransport(PreferredTransport transport) { +std::optional ToWireTransport(PreferredTransport transport) { switch (transport) { case PreferredTransport::kRest: - return lf::a2a::v1::TRANSPORT_PROTOCOL_REST; + return kHttpJson; case PreferredTransport::kJsonRpc: - return lf::a2a::v1::TRANSPORT_PROTOCOL_JSON_RPC; + return kJsonRpc; case PreferredTransport::kGrpc: - return lf::a2a::v1::TRANSPORT_PROTOCOL_GRPC; + return kGrpc; } return std::nullopt; } @@ -197,40 +191,38 @@ core::Result DiscoveryClient::BuildExtendedDiscoveryUrl(std::string } core::Result DiscoveryClient::ValidateAgentCard(const lf::a2a::v1::AgentCard& card) { - if (card.protocol_version().empty()) { - return core::Error::Validation("Agent Card protocol_version is required"); - } - if (!core::Version::IsSupported(card.protocol_version())) { - return core::Error::UnsupportedVersion("Only A2A protocol version 1.0 is supported"); - } if (card.supported_interfaces().empty()) { return core::Error::Validation("Agent Card must include at least one supported interface"); } for (const auto& iface : card.supported_interfaces()) { - if (iface.transport() == lf::a2a::v1::TRANSPORT_PROTOCOL_UNSPECIFIED) { - return core::Error::Validation("Agent Card contains an interface with unspecified transport"); + if (iface.protocol_binding().empty()) { + return core::Error::Validation( + "Agent Card contains an interface with unspecified protocol binding"); + } + if (iface.protocol_version().empty()) { + return core::Error::Validation("Agent Card contains an interface with no protocol version"); + } + if (!core::Version::IsSupported(iface.protocol_version())) { + return core::Error::UnsupportedVersion("Only A2A protocol version 1.0 is supported"); } if (iface.url().empty()) { return core::Error::Validation("Agent Card contains an interface without a URL"); } - if (!IsValidInterfaceEndpoint(iface.transport(), iface.url())) { - return core::Error::Validation("Agent Card interface endpoint is invalid for its transport"); + if (!IsValidInterfaceEndpoint(iface.protocol_binding(), iface.url())) { + return core::Error::Validation( + "Agent Card interface endpoint is invalid for its protocol binding"); } - for (const auto& requirement : iface.security_requirements()) { - if (!card.security_schemes().contains(requirement)) { - return core::Error::Validation( - "Agent Card interface references an unknown security scheme: " + requirement); + for (const auto& requirement : card.security_requirements()) { + for (const auto& [scheme_name, _] : requirement.schemes()) { + if (!card.security_schemes().contains(scheme_name)) { + return core::Error::Validation( + "Agent Card security requirement references an unknown security scheme: " + + scheme_name); + } } } } - - for (const auto& requirement : card.default_security_requirements()) { - if (!card.security_schemes().contains(requirement)) { - return core::Error::Validation("Agent Card default security requirement is not defined: " + - requirement); - } - } return {}; } @@ -241,23 +233,21 @@ core::Result AgentCardResolver::SelectPreferredInterface( return core::Error::Validation("Invalid preferred transport requested"); } - std::array order = {preferred_wire.value(), - lf::a2a::v1::TRANSPORT_PROTOCOL_REST, - lf::a2a::v1::TRANSPORT_PROTOCOL_JSON_RPC}; - if (preferred_wire.value() == lf::a2a::v1::TRANSPORT_PROTOCOL_REST) { - order[1] = lf::a2a::v1::TRANSPORT_PROTOCOL_JSON_RPC; - order[2] = lf::a2a::v1::TRANSPORT_PROTOCOL_GRPC; - } else if (preferred_wire.value() == lf::a2a::v1::TRANSPORT_PROTOCOL_JSON_RPC) { - order[1] = lf::a2a::v1::TRANSPORT_PROTOCOL_REST; - order[2] = lf::a2a::v1::TRANSPORT_PROTOCOL_GRPC; + std::array order = {preferred_wire.value(), kHttpJson, kJsonRpc}; + if (preferred_wire.value() == kHttpJson) { + order[1] = kJsonRpc; + order[2] = kGrpc; + } else if (preferred_wire.value() == kJsonRpc) { + order[1] = kHttpJson; + order[2] = kGrpc; } else { - order[1] = lf::a2a::v1::TRANSPORT_PROTOCOL_REST; - order[2] = lf::a2a::v1::TRANSPORT_PROTOCOL_JSON_RPC; + order[1] = kHttpJson; + order[2] = kJsonRpc; } for (const auto transport : order) { for (const auto& iface : card.supported_interfaces()) { - if (iface.transport() != transport) { + if (iface.protocol_binding() != transport) { continue; } const auto valid = ValidateInterface(iface); @@ -266,16 +256,14 @@ core::Result AgentCardResolver::SelectPreferredInterface( } ResolvedInterface resolved; - resolved.transport = ToPreferredTransport(transport); + resolved.transport = ToPreferredTransport(iface.protocol_binding()); resolved.url = iface.url(); - if (iface.security_requirements().empty()) { - resolved.security_requirements.insert(resolved.security_requirements.end(), - card.default_security_requirements().begin(), - card.default_security_requirements().end()); - } else { - resolved.security_requirements.insert(resolved.security_requirements.end(), - iface.security_requirements().begin(), - iface.security_requirements().end()); + if (!card.security_requirements().empty()) { + for (const auto& requirement : card.security_requirements()) { + for (const auto& [scheme_name, _] : requirement.schemes()) { + resolved.security_requirements.push_back(scheme_name); + } + } } for (const auto& name : resolved.security_requirements) { const auto scheme = card.security_schemes().find(name); @@ -291,14 +279,14 @@ core::Result AgentCardResolver::SelectPreferredInterface( } core::Result AgentCardResolver::ValidateInterface(const lf::a2a::v1::AgentInterface& iface) { - if (iface.transport() == lf::a2a::v1::TRANSPORT_PROTOCOL_UNSPECIFIED) { - return core::Error::Validation("Unsupported transport"); + if (iface.protocol_binding().empty()) { + return core::Error::Validation("Unsupported protocol binding"); } if (iface.url().empty()) { return core::Error::Validation("Missing interface URL"); } - if (!IsValidInterfaceEndpoint(iface.transport(), iface.url())) { - return core::Error::Validation("Interface endpoint is invalid for its transport"); + if (!IsValidInterfaceEndpoint(iface.protocol_binding(), iface.url())) { + return core::Error::Validation("Interface endpoint is invalid for its protocol binding"); } return {}; } diff --git a/src/client/grpc_transport.cpp b/src/client/grpc_transport.cpp index fd628e3..64ba16f 100644 --- a/src/client/grpc_transport.cpp +++ b/src/client/grpc_transport.cpp @@ -217,7 +217,8 @@ core::Result GrpcTransport::CancelTask( return response; } -core::Result GrpcTransport::CreateTaskPushNotificationConfig( +core::Result +GrpcTransport::CreateTaskPushNotificationConfig( const lf::a2a::v1::TaskPushNotificationConfig& request, const CallOptions& options) { auto context_result = BuildContext(options); if (!context_result.ok()) { @@ -225,7 +226,8 @@ core::Result GrpcTransport::CreateTaskP } auto context = std::move(context_result.value()); lf::a2a::v1::TaskPushNotificationConfig response; - const auto status = rpc_client_->CreateTaskPushNotificationConfig(context.get(), request, &response); + const auto status = + rpc_client_->CreateTaskPushNotificationConfig(context.get(), request, &response); if (!status.ok()) { return BuildGrpcError(status); } diff --git a/src/client/http_json_transport.cpp b/src/client/http_json_transport.cpp index 35c6381..11ecd4b 100644 --- a/src/client/http_json_transport.cpp +++ b/src/client/http_json_transport.cpp @@ -356,8 +356,8 @@ core::Result HttpJsonTransport::GetTask( } std::string endpoint = BuildTaskPath(request.id()); - if (!request.history_length().empty()) { - endpoint += "?historyLength=" + request.history_length(); + if (request.has_history_length()) { + endpoint += "?historyLength=" + std::to_string(request.history_length()); } const auto response = SendRequest({.method = "GET", .endpoint = endpoint}, {}, options); @@ -528,8 +528,8 @@ core::Result> HttpJsonTransport::SubscribeTask( } std::string endpoint = BuildTaskPath(request.id()) + ":subscribe"; - if (!request.history_length().empty()) { - endpoint += "?historyLength=" + request.history_length(); + if (request.has_history_length()) { + endpoint += "?historyLength=" + std::to_string(request.history_length()); } return StartSseStream({.method = "GET", .endpoint = endpoint}, {}, observer, options); diff --git a/src/server/rest_server_transport.cpp b/src/server/rest_server_transport.cpp index 008d5fd..a34db65 100644 --- a/src/server/rest_server_transport.cpp +++ b/src/server/rest_server_transport.cpp @@ -184,8 +184,10 @@ RestServerTransport::RestServerTransport(Dispatcher* dispatcher, lf::a2a::v1::Ag RestServerTransportOptions options) : transport_(dispatcher), agent_card_(std::move(agent_card)), options_(std::move(options)) { options_.rest_api_base_path = NormalizeBasePath(options_.rest_api_base_path); - if (agent_card_.protocol_version().empty()) { - agent_card_.set_protocol_version(core::Version::HeaderValue()); + for (auto& iface : *agent_card_.mutable_supported_interfaces()) { + if (iface.protocol_version().empty()) { + iface.set_protocol_version(core::Version::HeaderValue()); + } } } @@ -310,9 +312,6 @@ core::Result RestServerTransport::HandleAgentCard( if (cap_fields->find("pushNotifications") == cap_fields->end()) { (*cap_fields)["pushNotifications"].set_bool_value(false); } - if (cap_fields->find("stateTransitionHistory") == cap_fields->end()) { - (*cap_fields)["stateTransitionHistory"].set_bool_value(false); - } if (fields->find("defaultInputModes") == fields->end()) { auto* modes = EnsureListField(&card, "defaultInputModes")->mutable_list_value(); modes->add_values()->set_string_value("text/plain"); diff --git a/src/server/rest_transport.cpp b/src/server/rest_transport.cpp index 0741380..bee6c38 100644 --- a/src/server/rest_transport.cpp +++ b/src/server/rest_transport.cpp @@ -201,7 +201,11 @@ std::optional RestTransport::BuildDispatchRequest(const RestReq payload.set_id(task_id.value()); if (const auto history_length = LookupQuery(request, "historyLength"); history_length.has_value()) { - payload.set_history_length(*history_length); + const int parsed_history_length = ParsePageSize(*history_length); + if (parsed_history_length < 0) { + return std::nullopt; + } + payload.set_history_length(parsed_history_length); } return DispatchRequest{.operation = DispatcherOperation::kGetTask, .payload = payload}; } diff --git a/tests/fixtures/agent_card_valid.json b/tests/fixtures/agent_card_valid.json index 7e854d2..a22a8ef 100644 --- a/tests/fixtures/agent_card_valid.json +++ b/tests/fixtures/agent_card_valid.json @@ -1,26 +1,24 @@ { - "protocolVersion": "1.0", "name": "Fixture Agent", + "description": "Fixture", + "version": "1.0.0", + "defaultInputModes": ["text/plain"], + "defaultOutputModes": ["text/plain"], "supportedInterfaces": [ - { - "transport": "TRANSPORT_PROTOCOL_REST", - "url": "https://agent.example.com/a2a", - "securityRequirements": [ - "oauth2" - ] - }, - { - "transport": "TRANSPORT_PROTOCOL_JSON_RPC", - "url": "https://agent.example.com/rpc" - } + {"protocolBinding":"HTTP+JSON","protocolVersion":"1.0","url":"https://agent.example.com/a2a"}, + {"protocolBinding":"JSONRPC","protocolVersion":"1.0","url":"https://agent.example.com/rpc"} ], "securitySchemes": { "oauth2": { - "type": "oauth2", - "description": "OAuth2 token" + "oauth2SecurityScheme": { + "flows": { + "clientCredentials": { + "tokenUrl": "https://issuer.example.com/oauth/token", + "scopes": {"agent.read":"Read access"} + } + } + } } }, - "defaultSecurityRequirements": [ - "oauth2" - ] + "securityRequirements": [{"schemes":{"oauth2":{"list":[]}}}] } diff --git a/tests/fixtures/protojson/send_message_request_golden.json b/tests/fixtures/protojson/send_message_request_golden.json index a1df2ec..6253e8a 100644 --- a/tests/fixtures/protojson/send_message_request_golden.json +++ b/tests/fixtures/protojson/send_message_request_golden.json @@ -1 +1 @@ -{"message":{"role":"user","parts":[{"text":{"text":"hello"}}],"taskId":"golden-task"},"configuration":{"acceptedOutputModesOnly":true,"pushNotificationIds":["push-1"]}} +{"message":{"taskId":"golden-task","role":"ROLE_USER","parts":[{"text":"hello"}]},"configuration":{"acceptedOutputModes":["text/plain"],"returnImmediately":true}} diff --git a/tests/fixtures/protojson/task_golden.json b/tests/fixtures/protojson/task_golden.json index c78e371..cb13a20 100644 --- a/tests/fixtures/protojson/task_golden.json +++ b/tests/fixtures/protojson/task_golden.json @@ -1 +1 @@ -{"id":"task-golden","status":{"state":"TASK_STATE_COMPLETED","message":{"role":"agent","parts":[{"text":{"text":"done"}}]}}} +{"id":"task-golden","status":{"state":"TASK_STATE_COMPLETED","message":{"role":"ROLE_AGENT","parts":[{"text":"done"}]}}} diff --git a/tests/functional/examples_functional_test.cpp b/tests/functional/examples_functional_test.cpp index 773c05b..0577619 100644 --- a/tests/functional/examples_functional_test.cpp +++ b/tests/functional/examples_functional_test.cpp @@ -28,7 +28,6 @@ void ExpectStreamEnded(a2a::server::ServerStreamSession* stream) { } TEST(ExamplesFunctionalTest, StreamingExecutorReturnsWorkingThenCompletedEvents) { - constexpr bool kExpectedFinalEvent = true; a2a::examples::ExampleExecutor executor; a2a::server::RequestContext context; @@ -44,7 +43,6 @@ TEST(ExamplesFunctionalTest, StreamingExecutorReturnsWorkingThenCompletedEvents) const auto second = RequireNextEvent(stream.get()); EXPECT_EQ(second.status_update().status().state(), lf::a2a::v1::TASK_STATE_COMPLETED); - EXPECT_EQ(second.status_update().final(), kExpectedFinalEvent); ExpectStreamEnded(stream.get()); } diff --git a/tests/functional/json_rpc_client_functional_test.cpp b/tests/functional/json_rpc_client_functional_test.cpp index b983bb5..7dd3a37 100644 --- a/tests/functional/json_rpc_client_functional_test.cpp +++ b/tests/functional/json_rpc_client_functional_test.cpp @@ -57,9 +57,10 @@ a2a::core::Result HandleFunctionalRequest(const HttpRequest& const std::string id = ExtractFieldOrDefault(envelope, "id"); const std::string method = ExtractFieldOrDefault(envelope, "method"); if (method == "a2a.sendMessage") { - return HttpClientResponse{.status_code = kHttpOk, - .headers = {{"A2A-Version", "1.0"}}, - .body = BuildResultEnvelope(id, R"({"message":{"role":"agent"}})")}; + return HttpClientResponse{ + .status_code = kHttpOk, + .headers = {{"A2A-Version", "1.0"}}, + .body = BuildResultEnvelope(id, R"({"message":{"role":"ROLE_AGENT"}})")}; } if (method == "a2a.getTask") { return HttpClientResponse{.status_code = kHttpOk, @@ -90,12 +91,12 @@ TEST(JsonRpcClientFunctionalTest, SendMessageRoundTripsThroughTransportContract) A2AClient client(std::move(transport)); lf::a2a::v1::SendMessageRequest request; - request.mutable_message()->set_role("user"); + request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); const auto response = client.SendMessage(request); ASSERT_TRUE(response.ok()) << response.error().message(); ASSERT_TRUE(response.value().has_message()); - EXPECT_EQ(response.value().message().role(), "agent"); + EXPECT_EQ(response.value().message().role(), lf::a2a::v1::ROLE_AGENT); } TEST(JsonRpcClientFunctionalTest, GetTaskRoundTripsThroughTransportContract) { diff --git a/tests/functional/json_rpc_server_transport_functional_test.cpp b/tests/functional/json_rpc_server_transport_functional_test.cpp index 248fc25..3322f6b 100644 --- a/tests/functional/json_rpc_server_transport_functional_test.cpp +++ b/tests/functional/json_rpc_server_transport_functional_test.cpp @@ -20,7 +20,7 @@ TEST(JsonRpcServerTransportFunctionalTest, SupportsTaskLifecycleMethodsOverJsonR const auto send_response = server.Handle(a2a::tests::support::MakeHttpRequest( "POST", "/rpc", {{"A2A-Version", "1.0"}}, - R"({"jsonrpc":"2.0","id":"send-1","method":"a2a.sendMessage","params":{"message":{"role":"user","taskId":"task-jsonrpc-functional-1"}}})")); + R"({"jsonrpc":"2.0","id":"send-1","method":"a2a.sendMessage","params":{"message":{"role":"ROLE_USER","taskId":"task-jsonrpc-functional-1"}}})")); ASSERT_TRUE(send_response.ok()); EXPECT_EQ(send_response.value().status_code, kHttpOk); @@ -54,7 +54,7 @@ TEST(JsonRpcServerTransportFunctionalTest, RejectsMissingVersionHeaderWhenRequir const auto response = server.Handle(a2a::tests::support::MakeHttpRequest( "POST", "/rpc", {}, - R"({"jsonrpc":"2.0","id":"send-no-version","method":"a2a.sendMessage","params":{"message":{"role":"user","taskId":"task-a"}}})")); + R"({"jsonrpc":"2.0","id":"send-no-version","method":"a2a.sendMessage","params":{"message":{"role":"ROLE_USER","taskId":"task-a"}}})")); ASSERT_TRUE(response.ok()); EXPECT_EQ(response.value().status_code, kHttpUpgradeRequired); diff --git a/tests/functional/rest_server_transport_functional_test.cpp b/tests/functional/rest_server_transport_functional_test.cpp index 532a8ab..d3249a4 100644 --- a/tests/functional/rest_server_transport_functional_test.cpp +++ b/tests/functional/rest_server_transport_functional_test.cpp @@ -17,7 +17,7 @@ TEST(RestServerTransportFunctionalTest, SupportsCoreTaskLifecycleOverHttpTargetM const auto send_response = server.Handle(a2a::tests::support::MakeHttpRequest( "POST", "/api/messages:send", {{"A2A-Version", "1.0"}}, - R"({"message":{"role":"user","taskId":"task-functional-1"}})")); + R"({"message":{"role":"ROLE_USER","taskId":"task-functional-1"}})")); ASSERT_TRUE(send_response.ok()); EXPECT_EQ(send_response.value().status_code, 200); diff --git a/tests/integration/examples_interop_integration_test.cpp b/tests/integration/examples_interop_integration_test.cpp index bc5942d..6956f1e 100644 --- a/tests/integration/examples_interop_integration_test.cpp +++ b/tests/integration/examples_interop_integration_test.cpp @@ -63,6 +63,9 @@ TEST(ExamplesInteropIntegrationTest, RestExampleServerRoundTripWorksViaDiscovery a2a::client::A2AClient client(std::move(transport)); lf::a2a::v1::SendMessageRequest send; + send.mutable_message()->set_message_id("interop-rest-msg-1"); + send.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); + send.mutable_message()->add_parts()->set_text("hello"); send.mutable_message()->set_task_id("interop-rest-task"); const auto send_result = client.SendMessage(send); ASSERT_TRUE(send_result.ok()) << send_result.error().message(); diff --git a/tests/integration/grpc_transport_integration_test.cpp b/tests/integration/grpc_transport_integration_test.cpp index 8e06ac1..11288b2 100644 --- a/tests/integration/grpc_transport_integration_test.cpp +++ b/tests/integration/grpc_transport_integration_test.cpp @@ -121,7 +121,7 @@ std::unique_ptr StartHarness() { return a2a::core::Error::Internal("Client must not be null"); } lf::a2a::v1::SendMessageRequest send_request; - send_request.mutable_message()->set_role("user"); + send_request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); send_request.mutable_message()->set_task_id("grpc-integration-1"); auto send_response = client->SendMessage(send_request); @@ -159,7 +159,7 @@ std::unique_ptr StartHarness() { return a2a::core::Error::Internal("Client must not be null"); } lf::a2a::v1::SendMessageRequest send_request; - send_request.mutable_message()->set_role("user"); + send_request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); send_request.mutable_message()->set_task_id("grpc-integration-1"); RecordingObserver observer; @@ -226,7 +226,7 @@ std::unique_ptr BuildClient(int port) { constexpr std::string_view kTaskId = "grpc-subscribe-1"; lf::a2a::v1::SendMessageRequest send_request; - send_request.mutable_message()->set_role("user"); + send_request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); send_request.mutable_message()->set_task_id(std::string(kTaskId)); const auto send_response = client->SendMessage(send_request); if (!send_response.ok()) { diff --git a/tests/integration/http_json_client_integration_test.cpp b/tests/integration/http_json_client_integration_test.cpp index 25573bc..0675225 100644 --- a/tests/integration/http_json_client_integration_test.cpp +++ b/tests/integration/http_json_client_integration_test.cpp @@ -42,12 +42,12 @@ TEST(HttpJsonClientIntegrationTest, SendMessageHappyPathSetsHeadersAndParsesResp captured = request; return HttpClientResponse{.status_code = kHttpOk, .headers = {{"A2A-Version", "1.0"}}, - .body = R"({"message":{"role":"agent"}})"}; + .body = R"({"message":{"role":"ROLE_AGENT"}})"}; }); A2AClient client(std::move(transport)); lf::a2a::v1::SendMessageRequest request; - request.mutable_message()->set_role("user"); + request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); CallOptions options; options.timeout = std::chrono::milliseconds(kCustomTimeoutMs); @@ -58,7 +58,7 @@ TEST(HttpJsonClientIntegrationTest, SendMessageHappyPathSetsHeadersAndParsesResp const auto response = client.SendMessage(request, options); ASSERT_TRUE(response.ok()) << response.error().message(); ASSERT_TRUE(response.value().has_message()); - EXPECT_EQ(response.value().message().role(), "agent"); + EXPECT_EQ(response.value().message().role(), lf::a2a::v1::ROLE_AGENT); EXPECT_EQ(captured.method, "POST"); EXPECT_EQ(captured.url, "https://agent.example.com/a2a/messages:send"); @@ -69,7 +69,7 @@ TEST(HttpJsonClientIntegrationTest, SendMessageHappyPathSetsHeadersAndParsesResp EXPECT_EQ(captured.headers.at("X-Request-Id"), "abc123"); EXPECT_EQ(captured.headers.at("Authorization"), "Bearer token"); EXPECT_EQ(captured.headers.at("A2A-Extensions"), "ext.alpha,ext.beta"); - EXPECT_NE(captured.body.find("\"role\":\"user\""), std::string::npos); + EXPECT_NE(captured.body.find("\"role\":\"ROLE_USER\""), std::string::npos); } // NOLINTNEXTLINE(readability-function-cognitive-complexity) @@ -98,7 +98,7 @@ TEST(HttpJsonClientIntegrationTest, GetTaskAndCancelTaskHappyPath) { lf::a2a::v1::GetTaskRequest get_request; get_request.set_id("t-1"); - get_request.set_history_length("2"); + get_request.set_history_length(2); const auto get_response = client.GetTask(get_request); ASSERT_TRUE(get_response.ok()) << get_response.error().message(); EXPECT_EQ(get_response.value().id(), "t-1"); @@ -137,16 +137,14 @@ TEST(HttpJsonClientIntegrationTest, SupportsPushNotificationConfigCrudAndList) { auto transport = std::make_unique( MakeResolvedRest(), [](const HttpRequest& request) -> a2a::core::Result { if (request.method == "POST" && request.url.ends_with("/pushNotificationConfigs")) { - return HttpClientResponse{ - .status_code = kHttpOk, - .headers = {{"A2A-Version", "1.0"}}, - .body = R"({"id":"pn-1","taskId":"t-1","endpoint":"https://cb"})"}; + return HttpClientResponse{.status_code = kHttpOk, + .headers = {{"A2A-Version", "1.0"}}, + .body = R"({"id":"pn-1","taskId":"t-1","url":"https://cb"})"}; } if (request.method == "GET" && request.url.ends_with("/pushNotificationConfigs/pn-1")) { - return HttpClientResponse{ - .status_code = kHttpOk, - .headers = {{"A2A-Version", "1.0"}}, - .body = R"({"id":"pn-1","taskId":"t-1","endpoint":"https://cb"})"}; + return HttpClientResponse{.status_code = kHttpOk, + .headers = {{"A2A-Version", "1.0"}}, + .body = R"({"id":"pn-1","taskId":"t-1","url":"https://cb"})"}; } if (request.method == "GET" && request.url.find("/pushNotificationConfigs?taskId=t-1&pageSize=25") != @@ -165,12 +163,13 @@ TEST(HttpJsonClientIntegrationTest, SupportsPushNotificationConfigCrudAndList) { lf::a2a::v1::TaskPushNotificationConfig set_request; set_request.set_task_id("t-1"); - set_request.set_endpoint("https://cb"); + set_request.set_url("https://cb"); const auto set_response = client.CreateTaskPushNotificationConfig(set_request); ASSERT_TRUE(set_response.ok()) << set_response.error().message(); EXPECT_EQ(set_response.value().id(), "pn-1"); lf::a2a::v1::GetTaskPushNotificationConfigRequest get_request; + get_request.set_task_id("t-1"); get_request.set_id("pn-1"); const auto get_response = client.GetTaskPushNotificationConfig(get_request); ASSERT_TRUE(get_response.ok()) << get_response.error().message(); @@ -185,6 +184,7 @@ TEST(HttpJsonClientIntegrationTest, SupportsPushNotificationConfigCrudAndList) { EXPECT_EQ(list_response.value().configs(0).id(), "pn-1"); lf::a2a::v1::DeleteTaskPushNotificationConfigRequest delete_request; + delete_request.set_task_id("t-1"); delete_request.set_id("pn-1"); const auto delete_response = client.DeleteTaskPushNotificationConfig(delete_request); EXPECT_TRUE(delete_response.ok()) << delete_response.error().message(); diff --git a/tests/integration/http_json_streaming_integration_test.cpp b/tests/integration/http_json_streaming_integration_test.cpp index 5c2569d..e1040f2 100644 --- a/tests/integration/http_json_streaming_integration_test.cpp +++ b/tests/integration/http_json_streaming_integration_test.cpp @@ -103,7 +103,7 @@ class RecordingObserver final : public StreamObserver { TEST(HttpJsonStreamingIntegrationTest, SendStreamingMessageParsesFragmentedEventsInOrder) { const std::vector chunks = { "event: message\ndata: {\"task\":{\"id\":\"t-1\"}}\n\n", - "event: message\ndata: {\"statusUpdate\":{\"taskId\":\"t-1\",\"final\":false,", + "event: message\ndata: {\"statusUpdate\":{\"taskId\":\"t-1\",", "\"status\":{\"state\":\"TASK_STATE_WORKING\"}}}\n\n", "data: {\"artifactUpdate\":{\"taskId\":\"t-1\",\"artifact\":{\"artifactId\":\"a-1\"}}}\n\n"}; @@ -126,7 +126,7 @@ TEST(HttpJsonStreamingIntegrationTest, SendStreamingMessageParsesFragmentedEvent RecordingObserver observer; lf::a2a::v1::SendMessageRequest request; - request.mutable_message()->set_role("user"); + request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); auto stream = client.SendStreamingMessage(request, observer, CallOptions{}); ASSERT_TRUE(stream.ok()) << stream.error().message(); @@ -152,7 +152,7 @@ TEST(HttpJsonStreamingIntegrationTest, MalformedFrameTriggersObserverError) { A2AClient client(std::move(transport)); RecordingObserver observer; lf::a2a::v1::SendMessageRequest request; - request.mutable_message()->set_role("user"); + request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); auto stream = client.SendStreamingMessage(request, observer); ASSERT_TRUE(stream.ok()) << stream.error().message(); @@ -187,7 +187,7 @@ TEST(HttpJsonStreamingIntegrationTest, CancelDuringActiveStreamStopsWithoutCompl RecordingObserver observer; lf::a2a::v1::SendMessageRequest request; - request.mutable_message()->set_role("user"); + request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); auto stream = client.SendStreamingMessage(request, observer); ASSERT_TRUE(stream.ok()) << stream.error().message(); @@ -238,7 +238,7 @@ TEST(HttpJsonStreamingIntegrationTest, RemoteErrorEventMapsToObserverProtocolErr RecordingObserver observer; lf::a2a::v1::SendMessageRequest request; - request.mutable_message()->set_role("user"); + request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); auto stream = client.SendStreamingMessage(request, observer); ASSERT_TRUE(stream.ok()) << stream.error().message(); @@ -265,7 +265,7 @@ TEST(HttpJsonStreamingIntegrationTest, NonSuccessHttpStatusMapsToObserverError) A2AClient client(std::move(transport)); RecordingObserver observer; lf::a2a::v1::SendMessageRequest request; - request.mutable_message()->set_role("user"); + request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); auto stream = client.SendStreamingMessage(request, observer); ASSERT_TRUE(stream.ok()) << stream.error().message(); @@ -302,7 +302,7 @@ TEST(HttpJsonStreamingIntegrationTest, MissingStreamRequesterReturnsInternalErro RecordingObserver observer; lf::a2a::v1::SendMessageRequest request; - request.mutable_message()->set_role("user"); + request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); const auto stream = client.SendStreamingMessage(request, observer); ASSERT_FALSE(stream.ok()); diff --git a/tests/integration/json_rpc_server_transport_integration_test.cpp b/tests/integration/json_rpc_server_transport_integration_test.cpp index b62364b..2bfcc7f 100644 --- a/tests/integration/json_rpc_server_transport_integration_test.cpp +++ b/tests/integration/json_rpc_server_transport_integration_test.cpp @@ -35,9 +35,7 @@ class JsonRpcIntegrationHarness final { dispatcher_(&executor_), card_(a2a::tests::support::BuildJsonRpcAgentCard("Integration JSON-RPC Agent", "http://agent.local/rpc")), - server_(&dispatcher_, {.rpc_path = "/rpc"}) { - card_.set_protocol_version("1.0"); - } + server_(&dispatcher_, {.rpc_path = "/rpc"}) {} a2a::client::DiscoveryClient CreateDiscoveryClient() { return a2a::client::DiscoveryClient( @@ -99,7 +97,7 @@ TEST(JsonRpcServerTransportIntegrationTest, DiscoveryAndClientRoundTripWorks) { a2a::client::A2AClient client(std::move(transport)); lf::a2a::v1::SendMessageRequest send_request; - send_request.mutable_message()->set_role("user"); + send_request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); send_request.mutable_message()->set_task_id("jsonrpc-integration-1"); const auto send_response = client.SendMessage(send_request); diff --git a/tests/integration/rest_server_transport_integration_test.cpp b/tests/integration/rest_server_transport_integration_test.cpp index a61460a..7443235 100644 --- a/tests/integration/rest_server_transport_integration_test.cpp +++ b/tests/integration/rest_server_transport_integration_test.cpp @@ -116,7 +116,9 @@ TEST(RestServerTransportIntegrationTest, DiscoveryAndA2AClientRoundTripWorks) { a2a::client::A2AClient client(std::move(transport)); lf::a2a::v1::SendMessageRequest send_request; - send_request.mutable_message()->set_role("user"); + send_request.mutable_message()->set_message_id("rest-msg-1"); + send_request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); + send_request.mutable_message()->add_parts()->set_text("hello"); send_request.mutable_message()->set_task_id("rest-integration-1"); const auto send_response = client.SendMessage(send_request); @@ -232,7 +234,7 @@ TEST(RestServerTransportIntegrationTest, AuthHeadersPropagateToServerContext) { a2a::client::A2AClient client(std::move(transport)); lf::a2a::v1::SendMessageRequest request; - request.mutable_message()->set_role("user"); + request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); request.mutable_message()->set_task_id("auth-integration-1"); a2a::client::CallOptions options; diff --git a/tests/integration/server_dispatcher_task_store_integration_test.cpp b/tests/integration/server_dispatcher_task_store_integration_test.cpp index c6a0eb8..75553af 100644 --- a/tests/integration/server_dispatcher_task_store_integration_test.cpp +++ b/tests/integration/server_dispatcher_task_store_integration_test.cpp @@ -92,7 +92,7 @@ TEST(ServerDispatcherTaskStoreIntegrationTest, ExecutesTaskLifecycleThroughDispa a2a::server::RequestContext context; lf::a2a::v1::SendMessageRequest send_request; - send_request.mutable_message()->set_role("user"); + send_request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); send_request.mutable_message()->set_task_id("integration-task-1"); const auto send_result = dispatcher.Dispatch( @@ -137,7 +137,7 @@ TEST(ServerDispatcherTaskStoreIntegrationTest, InterceptorsObserveRequestLifecyc a2a::server::RequestContext context; lf::a2a::v1::SendMessageRequest send_request; - send_request.mutable_message()->set_role("user"); + send_request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); send_request.mutable_message()->set_task_id("integration-task-2"); const auto send_result = dispatcher.Dispatch( diff --git a/tests/interop/cpp_grpc_interop_client.cpp b/tests/interop/cpp_grpc_interop_client.cpp index 2774685..d829027 100644 --- a/tests/interop/cpp_grpc_interop_client.cpp +++ b/tests/interop/cpp_grpc_interop_client.cpp @@ -38,7 +38,7 @@ int main(int argc, char** argv) { a2a::client::A2AClient client(std::make_unique(iface, channel)); lf::a2a::v1::SendMessageRequest send; - send.mutable_message()->set_role("user"); + send.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); send.mutable_message()->set_task_id("interop-task-1"); if (auto r = client.SendMessage(send); !r.ok()) { return 1; diff --git a/tests/interop/tck_http_sut.cpp b/tests/interop/tck_http_sut.cpp index 3f29385..b8649dd 100644 --- a/tests/interop/tck_http_sut.cpp +++ b/tests/interop/tck_http_sut.cpp @@ -13,6 +13,7 @@ #include #include +#include "a2a/core/protocol_bindings.h" #include "a2a/server/json_rpc_server_transport.h" #include "a2a/server/rest_server_transport.h" #include "a2a/server/server.h" @@ -84,7 +85,6 @@ int main(int argc, char** argv) { std::signal(SIGTERM, SignalHandler); lf::a2a::v1::AgentCard agent_card; - agent_card.set_protocol_version("1.0"); agent_card.set_name("TCK HTTP SUT"); agent_card.set_version("0.1.0"); agent_card.set_description("Conformance-focused local SUT for A2A TCK"); @@ -93,7 +93,6 @@ int main(int argc, char** argv) { auto* capabilities = agent_card.mutable_capabilities(); capabilities->set_streaming(true); capabilities->set_push_notifications(false); - capabilities->set_state_transition_history(true); auto* skill = agent_card.add_skills(); skill->set_id("echo"); skill->set_name("Echo Skill"); @@ -102,10 +101,12 @@ int main(int argc, char** argv) { skill->add_output_modes("text/plain"); skill->add_tags("conformance"); auto* jsonrpc_interface = agent_card.add_supported_interfaces(); - jsonrpc_interface->set_transport(lf::a2a::v1::TRANSPORT_PROTOCOL_JSON_RPC); + jsonrpc_interface->set_protocol_binding(std::string(a2a::core::protocol_bindings::kJsonRpc)); + jsonrpc_interface->set_protocol_version("1.0"); jsonrpc_interface->set_url("http://localhost:50061/rpc"); auto* rest_interface = agent_card.add_supported_interfaces(); - rest_interface->set_transport(lf::a2a::v1::TRANSPORT_PROTOCOL_REST); + rest_interface->set_protocol_binding(std::string(a2a::core::protocol_bindings::kHttpJson)); + rest_interface->set_protocol_version("1.0"); rest_interface->set_url("http://localhost:50061/a2a"); a2a::examples::ExampleExecutor executor; diff --git a/tests/support/rest_server_test_utils.h b/tests/support/rest_server_test_utils.h index 615cad8..607e1ac 100644 --- a/tests/support/rest_server_test_utils.h +++ b/tests/support/rest_server_test_utils.h @@ -69,7 +69,8 @@ inline lf::a2a::v1::AgentCard BuildRestAgentCard(std::string_view name, std::str lf::a2a::v1::AgentCard card; card.set_name(std::string(name)); auto* iface = card.add_supported_interfaces(); - iface->set_transport(lf::a2a::v1::TRANSPORT_PROTOCOL_REST); + iface->set_protocol_binding("HTTP+JSON"); + iface->set_protocol_version("1.0"); iface->set_url(std::string(url)); return card; } @@ -78,7 +79,8 @@ inline lf::a2a::v1::AgentCard BuildJsonRpcAgentCard(std::string_view name, std:: lf::a2a::v1::AgentCard card; card.set_name(std::string(name)); auto* iface = card.add_supported_interfaces(); - iface->set_transport(lf::a2a::v1::TRANSPORT_PROTOCOL_JSON_RPC); + iface->set_protocol_binding("JSONRPC"); + iface->set_protocol_version("1.0"); iface->set_url(std::string(url)); return card; } diff --git a/tests/unit/client_test.cpp b/tests/unit/client_test.cpp index 2c14c68..dc256bd 100644 --- a/tests/unit/client_test.cpp +++ b/tests/unit/client_test.cpp @@ -18,7 +18,7 @@ class FakeClientTransport final : public a2a::client::ClientTransport { (void)request; (void)options; lf::a2a::v1::SendMessageResponse response; - response.mutable_message()->set_role("assistant"); + response.mutable_message()->set_role(lf::a2a::v1::ROLE_AGENT); return response; } @@ -149,7 +149,7 @@ TEST(A2AClientTest, ReturnsInternalErrorWhenTransportNotConfigured) { } observer; lf::a2a::v1::SendMessageRequest stream_request; - stream_request.mutable_message()->set_role("user"); + stream_request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); const auto stream_response = client.SendStreamingMessage(stream_request, observer); ASSERT_FALSE(stream_response.ok()); EXPECT_EQ(stream_response.error().code(), a2a::core::ErrorCode::kInternal); diff --git a/tests/unit/discovery_test.cpp b/tests/unit/discovery_test.cpp index 7358a4c..d02926f 100644 --- a/tests/unit/discovery_test.cpp +++ b/tests/unit/discovery_test.cpp @@ -55,8 +55,7 @@ TEST(DiscoveryClientTest, ReportsBadJsonWithSerializationError) { TEST(DiscoveryClientTest, RejectsCardsWithoutSupportedInterfaces) { DiscoveryClient client([](std::string_view) -> a2a::core::Result { - return HttpResponse{.status_code = kHttpOk, - .body = R"({"protocolVersion":"1.0","name":"no-interfaces"})"}; + return HttpResponse{.status_code = kHttpOk, .body = R"({"name":"no-interfaces"})"}; }); const auto result = client.Fetch("https://agent.example.com"); @@ -69,7 +68,7 @@ TEST(DiscoveryClientTest, RejectsUnsupportedProtocolVersion) { return HttpResponse{ .status_code = kHttpOk, .body = - R"({"protocolVersion":"2.0","supportedInterfaces":[{"transport":"TRANSPORT_PROTOCOL_REST","url":"https://agent.example.com/a2a"}]})"}; + R"({"supportedInterfaces":[{"protocolBinding":"HTTP+JSON","protocolVersion":"2.0","url":"https://agent.example.com/a2a"}]})"}; }); const auto result = client.Fetch("https://agent.example.com"); @@ -82,7 +81,7 @@ TEST(DiscoveryClientTest, RejectsUnknownSecurityRequirementReferences) { return HttpResponse{ .status_code = kHttpOk, .body = - R"({"protocolVersion":"1.0","supportedInterfaces":[{"transport":"TRANSPORT_PROTOCOL_REST","url":"https://agent.example.com/a2a","securityRequirements":["oauth2"]}]})"}; + R"({"supportedInterfaces":[{"protocolBinding":"HTTP+JSON","protocolVersion":"1.0","url":"https://agent.example.com/a2a"}],"securityRequirements":[{"schemes":{"oauth2":{"list":[]}}}]})"}; }); const auto result = client.Fetch("https://agent.example.com"); @@ -98,7 +97,7 @@ TEST(DiscoveryClientTest, UsesInMemoryCacheWithinTtl) { return HttpResponse{ .status_code = kHttpOk, .body = - R"({"protocolVersion":"1.0","supportedInterfaces":[{"transport":"TRANSPORT_PROTOCOL_REST","url":"https://agent.example.com/a2a"}]})"}; + R"({"supportedInterfaces":[{"protocolBinding":"HTTP+JSON","protocolVersion":"1.0","url":"https://agent.example.com/a2a"}]})"}; }, a2a::client::kDefaultDiscoveryCacheTtl); @@ -116,7 +115,7 @@ TEST(DiscoveryClientTest, FetchExtendedAgentCardUsesExtendedQueryPath) { return HttpResponse{ .status_code = kHttpOk, .body = - R"({"protocolVersion":"1.0","supportedInterfaces":[{"transport":"TRANSPORT_PROTOCOL_REST","url":"https://agent.example.com/a2a"}]})"}; + R"({"supportedInterfaces":[{"protocolBinding":"HTTP+JSON","protocolVersion":"1.0","url":"https://agent.example.com/a2a"}]})"}; }); const auto fetched = client.FetchExtendedAgentCard("https://agent.example.com/"); @@ -125,12 +124,13 @@ TEST(DiscoveryClientTest, FetchExtendedAgentCardUsesExtendedQueryPath) { } TEST(AgentCardResolverTest, SelectsPreferredThenFallsBack) { lf::a2a::v1::AgentCard card; - card.set_protocol_version("1.0"); auto* json_rpc = card.add_supported_interfaces(); - json_rpc->set_transport(lf::a2a::v1::TRANSPORT_PROTOCOL_JSON_RPC); + json_rpc->set_protocol_binding("JSONRPC"); + json_rpc->set_protocol_version("1.0"); json_rpc->set_url("https://agent.example.com/rpc"); auto* grpc = card.add_supported_interfaces(); - grpc->set_transport(lf::a2a::v1::TRANSPORT_PROTOCOL_GRPC); + grpc->set_protocol_binding("GRPC"); + grpc->set_protocol_version("1.0"); grpc->set_url("https://agent.example.com/grpc"); const auto resolved = @@ -142,9 +142,9 @@ TEST(AgentCardResolverTest, SelectsPreferredThenFallsBack) { TEST(AgentCardResolverTest, SelectsGrpcInterfaceWhenPreferred) { lf::a2a::v1::AgentCard card; - card.set_protocol_version("1.0"); auto* grpc = card.add_supported_interfaces(); - grpc->set_transport(lf::a2a::v1::TRANSPORT_PROTOCOL_GRPC); + grpc->set_protocol_binding("GRPC"); + grpc->set_protocol_version("1.0"); grpc->set_url("dns:///agent.example.com:50051"); const auto resolved = @@ -156,11 +156,12 @@ TEST(AgentCardResolverTest, SelectsGrpcInterfaceWhenPreferred) { TEST(AgentCardResolverTest, UsesDefaultSecurityRequirementsWhenInterfaceSpecificNotSet) { lf::a2a::v1::AgentCard card; - card.set_protocol_version("1.0"); - card.add_default_security_requirements("oauth2"); - (*card.mutable_security_schemes())["oauth2"].set_type("oauth2"); + (*card.mutable_security_schemes())["oauth2"].mutable_oauth2_security_scheme(); + auto* requirement = card.add_security_requirements(); + (*requirement->mutable_schemes())["oauth2"]; auto* rest = card.add_supported_interfaces(); - rest->set_transport(lf::a2a::v1::TRANSPORT_PROTOCOL_REST); + rest->set_protocol_binding("HTTP+JSON"); + rest->set_protocol_version("1.0"); rest->set_url("https://agent.example.com/a2a"); const auto resolved = @@ -173,9 +174,8 @@ TEST(AgentCardResolverTest, UsesDefaultSecurityRequirementsWhenInterfaceSpecific TEST(AgentCardResolverTest, ReturnsValidationErrorWhenNoUsableInterfaceExists) { lf::a2a::v1::AgentCard card; - card.set_protocol_version("1.0"); auto* iface = card.add_supported_interfaces(); - iface->set_transport(lf::a2a::v1::TRANSPORT_PROTOCOL_UNSPECIFIED); + iface->set_protocol_binding(""); iface->set_url(""); const auto resolved = diff --git a/tests/unit/json_rpc_server_transport_test.cpp b/tests/unit/json_rpc_server_transport_test.cpp index b56423f..1574b00 100644 --- a/tests/unit/json_rpc_server_transport_test.cpp +++ b/tests/unit/json_rpc_server_transport_test.cpp @@ -71,7 +71,7 @@ TEST(JsonRpcServerTransportTest, HandlesSendMessageEnvelope) { .target = "/rpc", .headers = {{"A2A-Version", "1.0"}}, .body = - R"({"jsonrpc":"2.0","id":"req-1","method":"a2a.sendMessage","params":{"message":{"role":"user","taskId":"task-1"}}})", + R"({"jsonrpc":"2.0","id":"req-1","method":"a2a.sendMessage","params":{"message":{"role":"ROLE_USER","taskId":"task-1"}}})", .remote_address = "127.0.0.1"}); ASSERT_TRUE(response.ok()); @@ -182,7 +182,7 @@ TEST(JsonRpcServerTransportTest, ExtractsAuthMetadataIntoRequestContext) { .target = "/rpc", .headers = {{"A2A-Version", "1.0"}, {"Authorization", "Bearer token-rpc"}}, .body = - R"({"jsonrpc":"2.0","id":"req-auth","method":"a2a.sendMessage","params":{"message":{"role":"user","taskId":"task-auth"}}})", + R"({"jsonrpc":"2.0","id":"req-auth","method":"a2a.sendMessage","params":{"message":{"role":"ROLE_USER","taskId":"task-auth"}}})", .remote_address = "127.0.0.1"}); ASSERT_TRUE(response.ok()); diff --git a/tests/unit/rest_server_transport_test.cpp b/tests/unit/rest_server_transport_test.cpp index 4bd7b28..512ac0d 100644 --- a/tests/unit/rest_server_transport_test.cpp +++ b/tests/unit/rest_server_transport_test.cpp @@ -18,7 +18,7 @@ class EchoExecutor final : public a2a::server::AgentExecutor { observed_api_key = context.auth_metadata["api_key"]; lf::a2a::v1::SendMessageResponse response; response.mutable_message()->set_task_id(request.message().task_id()); - response.mutable_message()->set_role("assistant"); + response.mutable_message()->set_role(lf::a2a::v1::ROLE_AGENT); return response; } @@ -56,7 +56,7 @@ class EchoExecutor final : public a2a::server::AgentExecutor { } std::string observed_request_header; - std::string observed_history_length; + int observed_history_length = -1; std::string observed_bearer_token; std::string observed_api_key; }; @@ -64,8 +64,13 @@ class EchoExecutor final : public a2a::server::AgentExecutor { lf::a2a::v1::AgentCard BuildCard() { lf::a2a::v1::AgentCard card; card.set_name("Unit Agent"); + card.set_description("Unit test agent"); + card.set_version("1.0.0"); + card.add_default_input_modes("text/plain"); + card.add_default_output_modes("text/plain"); auto* iface = card.add_supported_interfaces(); - iface->set_transport(lf::a2a::v1::TRANSPORT_PROTOCOL_REST); + iface->set_protocol_binding("HTTP+JSON"); + iface->set_protocol_version("1.0"); iface->set_url("http://localhost:8080/a2a"); return card; } @@ -87,7 +92,8 @@ TEST(RestServerTransportTest, ServesAgentCardFromWellKnownEndpoint) { lf::a2a::v1::AgentCard parsed; ASSERT_TRUE(a2a::core::JsonToMessage(response.value().body, &parsed).ok()); - EXPECT_EQ(parsed.protocol_version(), "1.0"); + ASSERT_FALSE(parsed.supported_interfaces().empty()); + EXPECT_EQ(parsed.supported_interfaces(0).protocol_version(), "1.0"); ASSERT_EQ(parsed.supported_interfaces_size(), 1); EXPECT_EQ(parsed.supported_interfaces(0).url(), "http://localhost:8080/a2a"); } @@ -97,11 +103,13 @@ TEST(RestServerTransportTest, RoutesRequestUsingConfiguredBasePath) { a2a::server::Dispatcher dispatcher(&executor); a2a::server::RestServerTransport server(&dispatcher, BuildCard(), {.rest_api_base_path = "/a2a"}); - const auto response = server.Handle({.method = "POST", - .target = "/a2a/messages:send", - .headers = {{"A2A-Version", "1.0"}}, - .body = R"({"message":{"role":"user","taskId":"t-1"}})", - .remote_address = {}}); + const auto response = server.Handle( + {.method = "POST", + .target = "/a2a/messages:send", + .headers = {{"A2A-Version", "1.0"}}, + .body = + R"({"message":{"messageId":"msg-1","role":"ROLE_USER","parts":[{"text":"hello"}],"taskId":"t-1"}})", + .remote_address = {}}); ASSERT_TRUE(response.ok()); EXPECT_EQ(response.value().status_code, 200); @@ -131,14 +139,14 @@ TEST(RestServerTransportTest, ParsesAndDecodesQueryString) { a2a::server::RestServerTransport server(&dispatcher, BuildCard(), {.rest_api_base_path = "/a2a"}); const auto response = server.Handle({.method = "GET", - .target = "/a2a/tasks/task-3?historyLength=alpha%20beta", + .target = "/a2a/tasks/task-3?historyLength=20", .headers = {{"A2A-Version", "1.0"}}, .body = {}, .remote_address = {}}); ASSERT_TRUE(response.ok()); EXPECT_EQ(response.value().status_code, 200); - EXPECT_EQ(executor.observed_history_length, "alpha beta"); + EXPECT_EQ(executor.observed_history_length, 20); } TEST(RestServerTransportTest, ExtractsAuthMetadataIntoRequestContext) { @@ -146,13 +154,15 @@ TEST(RestServerTransportTest, ExtractsAuthMetadataIntoRequestContext) { a2a::server::Dispatcher dispatcher(&executor); a2a::server::RestServerTransport server(&dispatcher, BuildCard(), {.rest_api_base_path = "/a2a"}); - const auto response = server.Handle({.method = "POST", - .target = "/a2a/messages:send", - .headers = {{"A2A-Version", "1.0"}, - {"Authorization", "Bearer token-rest"}, - {"X-API-Key", "rest-key"}}, - .body = R"({"message":{"role":"user","taskId":"t-2"}})", - .remote_address = {}}); + const auto response = server.Handle( + {.method = "POST", + .target = "/a2a/messages:send", + .headers = {{"A2A-Version", "1.0"}, + {"Authorization", "Bearer token-rest"}, + {"X-API-Key", "rest-key"}}, + .body = + R"({"message":{"messageId":"msg-2","role":"ROLE_USER","parts":[{"text":"hello"}],"taskId":"t-2"}})", + .remote_address = {}}); ASSERT_TRUE(response.ok()); EXPECT_EQ(response.value().status_code, 200); diff --git a/tests/unit/rest_transport_test.cpp b/tests/unit/rest_transport_test.cpp index d41cc0f..6349334 100644 --- a/tests/unit/rest_transport_test.cpp +++ b/tests/unit/rest_transport_test.cpp @@ -16,7 +16,7 @@ class FakeExecutor final : public a2a::server::AgentExecutor { observed_request_id = context.request_id.value_or("missing"); lf::a2a::v1::SendMessageResponse response; auto* message = response.mutable_message(); - message->set_role("assistant"); + message->set_role(lf::a2a::v1::ROLE_AGENT); message->set_task_id(request.message().task_id()); return response; } @@ -66,7 +66,7 @@ class FakeExecutor final : public a2a::server::AgentExecutor { } std::string observed_request_id; - std::string observed_history_length; + int observed_history_length = -1; std::size_t observed_page_size = 0; std::string observed_page_token; }; @@ -90,13 +90,14 @@ TEST(RestTransportTest, DispatchesSendMessageFromJsonBody) { a2a::server::RestRequest request; request.method = "POST"; request.path = "/messages:send"; - request.body = R"({"message":{"role":"user","taskId":"t-42"}})"; + request.body = + R"({"message":{"messageId":"msg-1","role":"ROLE_USER","parts":[{"text":"hello"}],"taskId":"t-42"}})"; request.context.request_id = "req-9"; const auto response = transport.Handle(request); ASSERT_TRUE(response.ok()); EXPECT_EQ(response.value().http_status, 200); - EXPECT_NE(response.value().body.find("assistant"), std::string::npos); + EXPECT_NE(response.value().body.find("ROLE_AGENT"), std::string::npos); EXPECT_EQ(executor.observed_request_id, "req-9"); } @@ -114,7 +115,7 @@ TEST(RestTransportTest, DispatchesGetTaskUsingPathAndQuery) { ASSERT_TRUE(response.ok()); EXPECT_EQ(response.value().http_status, 200); EXPECT_NE(response.value().body.find("task-99"), std::string::npos); - EXPECT_EQ(executor.observed_history_length, "20"); + EXPECT_EQ(executor.observed_history_length, 20); } TEST(RestTransportTest, DispatchesListTasksUsingQuery) { diff --git a/tests/unit/server_dispatcher_test.cpp b/tests/unit/server_dispatcher_test.cpp index 130fc89..a98b5ad 100644 --- a/tests/unit/server_dispatcher_test.cpp +++ b/tests/unit/server_dispatcher_test.cpp @@ -33,11 +33,11 @@ class FakeExecutor final : public a2a::server::AgentExecutor { const lf::a2a::v1::SendMessageRequest& request, a2a::server::RequestContext& context) override { last_request_id = context.request_id.value_or("missing"); - if (request.message().role().empty()) { + if (request.message().role() == lf::a2a::v1::ROLE_UNSPECIFIED) { return a2a::core::Error::Validation("message role is required"); } lf::a2a::v1::SendMessageResponse response; - response.mutable_message()->set_role("assistant"); + response.mutable_message()->set_role(lf::a2a::v1::ROLE_AGENT); return response; } @@ -47,7 +47,7 @@ class FakeExecutor final : public a2a::server::AgentExecutor { (void)request; (void)context; lf::a2a::v1::StreamResponse event; - event.mutable_message()->set_role("assistant"); + event.mutable_message()->set_role(lf::a2a::v1::ROLE_AGENT); return std::unique_ptr( std::make_unique(std::move(event))); } @@ -126,7 +126,7 @@ TEST(ServerDispatcherTest, DispatchesAllSupportedOperations) { context.request_id = "req-123"; lf::a2a::v1::SendMessageRequest send_request; - send_request.mutable_message()->set_role("user"); + send_request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); const a2a::server::DispatchRequest send_dispatch{ .operation = a2a::server::DispatcherOperation::kSendMessage, .payload = send_request}; const auto send_result = dispatcher.Dispatch(send_dispatch, context); @@ -204,7 +204,7 @@ TEST(ServerDispatcherTest, ReturnsInternalErrorWithoutExecutor) { a2a::server::RequestContext context; lf::a2a::v1::SendMessageRequest request; - request.mutable_message()->set_role("user"); + request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); const a2a::server::DispatchRequest dispatch{ .operation = a2a::server::DispatcherOperation::kSendMessage, .payload = request}; @@ -222,7 +222,7 @@ TEST(ServerDispatcherTest, InterceptorsObserveOrderingAndCanMutateContext) { dispatcher.AddInterceptor(std::make_shared(&events, "i2")); lf::a2a::v1::SendMessageRequest request; - request.mutable_message()->set_role("user"); + request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); a2a::server::RequestContext context; const auto result = dispatcher.Dispatch( @@ -243,7 +243,7 @@ TEST(ServerDispatcherTest, InterceptorFailureShortCircuitsDispatchAndTriggersAft dispatcher.AddInterceptor(std::make_shared(&events, "i2", true)); lf::a2a::v1::SendMessageRequest request; - request.mutable_message()->set_role("user"); + request.mutable_message()->set_role(lf::a2a::v1::ROLE_USER); a2a::server::RequestContext context; const auto result = dispatcher.Dispatch(