From 090ff8ddfe655c021f945ee5016e8f6d8d6a4efd Mon Sep 17 00:00:00 2001 From: DarumaDocker Date: Sat, 14 Mar 2026 20:52:11 +0800 Subject: [PATCH] feat: migrate to A2A protocol v1.0.0 stable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breaking changes from v1.0.0-rc proto (lf.a2a.v1): TaskPushNotificationConfig — flatten nested config - Remove PushNotificationConfig as a standalone type - Inline url, token, authentication directly into TaskPushNotificationConfig - tenant field moved to first position matching proto field order SendMessageConfiguration — field renames - blocking -> return_immediately (JSON: "returnImmediately") - push_notification_config: Option -> task_push_notification_config: Option CreateTaskPushNotificationConfig — simplified input - Remove CreateTaskPushNotificationConfigRequest wrapper type - RPC and REST handler now take TaskPushNotificationConfig directly - REST handler injects task_id/tenant from path/query into body ListTaskPushNotificationConfig -> ListTaskPushNotificationConfigs (plural) - Rename request/response types, handler method, client method - JSON-RPC method constant value updated to "ListTaskPushNotificationConfigs" CancelTaskRequest — add optional metadata field Task.context_id — String -> Option - RC had field_behavior = REQUIRED; stable removes it Crate version 0.1.0 -> 1.0.0 to track protocol stable release Update normative sources in CLAUDE.md, AGENTS.md, README, lib.rs, store.rs, examples, templates to reference v1.0.0 stable. ImplicitOAuthFlow/PasswordOAuthFlow retained: release notes say removed but official published definitions still include them as deprecated (lines 596-608, 634-647). Will drop when the published definitions stop advertising them. Co-Authored-By: Claude Sonnet 4.6 --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- AGENTS.md | 22 +- CLAUDE.md | 4 +- Cargo.toml | 2 +- README.md | 18 +- docs/proto-first-design.md | 476 ++++++++++++++++++++++ examples/echo_server.rs | 2 +- src/client/api.rs | 58 +-- src/jsonrpc.rs | 4 +- src/lib.rs | 2 +- src/server/handler.rs | 23 +- src/server/jsonrpc.rs | 17 +- src/server/rest.rs | 80 ++-- src/server/router.rs | 4 +- src/store.rs | 18 +- src/types/auth.rs | 4 +- src/types/push.rs | 33 +- src/types/requests.rs | 40 +- src/types/responses.rs | 10 +- src/types/task.rs | 7 +- tests/client_integration.rs | 73 ++-- tests/client_wiremock.rs | 2 +- tests/server_integration.rs | 40 +- tests/spec_examples.rs | 2 +- 26 files changed, 688 insertions(+), 259 deletions(-) create mode 100644 docs/proto-first-design.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e1a5ed5..031068c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -30,7 +30,7 @@ What actually happened instead. - **OS**: [e.g., macOS 14.0, Ubuntu 22.04] - **Rust version**: [e.g., 1.75.0] -- **a2a-rust version**: [e.g., 0.1.0] +- **a2a-rust version**: [e.g., 1.0.0] - **Features enabled**: [e.g., server, client, both] ## Code Example diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 8e2836a..59114e0 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -35,7 +35,7 @@ Describe specific use cases where this feature would be valuable: If this relates to an A2A protocol feature, link to the relevant section: - Spec: https://a2a-protocol.org/latest/specification/ -- Proto: https://github.com/a2aproject/A2A/blob/v1.0.0-rc/specification/a2a.proto +- Proto: https://github.com/a2aproject/A2A/blob/v1.0.0/specification/a2a.proto ## Checklist diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e305120..2fa8fce 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -26,7 +26,7 @@ -- [ ] Changes align with A2A v1.0 RC spec (tag `v1.0.0-rc`) +- [ ] Changes align with A2A v1.0 spec (tag `v1.0.0`) - [ ] Serde serialization matches proto3 JSON mapping - [ ] N/A — no protocol-related changes diff --git a/AGENTS.md b/AGENTS.md index c89cd66..a1645b6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ This file provides guidance to AI coding agents working in this repository. ## Project Overview -`a2a-rust` is a generic Rust SDK for A2A (Agent-to-Agent) Protocol v1.0 RC. +`a2a-rust` is a generic Rust SDK for A2A (Agent-to-Agent) Protocol v1.0. Current implementation status: @@ -17,9 +17,9 @@ This crate has zero Clawhive-specific logic. Keep it generic and protocol-focuse Protocol lock: -- tag: `v1.0.0-rc` -- commit: `6292104` -- proto package: `a2a.v1` +- tag: `v1.0.0` +- commit: `1736957` +- proto package: `lf.a2a.v1` Normative source precedence: @@ -131,7 +131,7 @@ Use proto enum strings exactly, for example: ### JSON-RPC method names -Use PascalCase v1.0 RC method names, for example: +Use PascalCase v1.0 method names, for example: - `SendMessage` - `GetTask` @@ -161,7 +161,7 @@ Important: - the field is `location`, not OpenAPI's `in` - `OAuthFlows` is modeled as a oneof-style enum - deserialization also accepts the Python SDK `type`-discriminator shape for interop -- deprecated OAuth flows still exist in the tagged proto and remain part of the wire model +- deprecated OAuth flows still exist in the published v1.0 definitions and remain part of the wire model ### AgentId @@ -184,10 +184,12 @@ for the repository's `TASK_STATE_AUTH_REQUIRED` metadata convention: ### Required shape corrections already reflected in code -- `Task.context_id` is required +- `Task.context_id` is optional - `TaskStatusUpdateEvent.context_id` is required - `TaskArtifactUpdateEvent.context_id` is required -- `ListTaskPushNotificationConfigResponse.next_page_token` is a string, with empty string meaning no next page +- `ListTaskPushNotificationConfigsResponse.next_page_token` is a string, with empty string meaning no next page +- `TaskPushNotificationConfig` is a flattened object with `url`, `token`, and `authentication` +- `SendMessageConfiguration` uses `return_immediately` and `task_push_notification_config` ### Error codes @@ -225,6 +227,6 @@ Prefer: ## References - [Proto-first design](docs/proto-first-design.md) -- [A2A Protocol Spec v1.0 RC](https://a2a-protocol.org/latest/specification/) -- [A2A Proto v1.0.0-rc](https://github.com/a2aproject/A2A/blob/v1.0.0-rc/specification/a2a.proto) +- [A2A Protocol Spec v1.0](https://a2a-protocol.org/latest/specification/) +- [A2A Proto v1.0.0](https://github.com/a2aproject/A2A/blob/v1.0.0/specification/a2a.proto) - [JSON-RPC 2.0 Spec](https://www.jsonrpc.org/specification) diff --git a/CLAUDE.md b/CLAUDE.md index 896685c..8230139 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,8 +5,8 @@ Read `AGENTS.md` first. ## Quick Reference - Language: Rust, edition 2024 -- Protocol: A2A v1.0 RC, locked to tag `v1.0.0-rc` -- Proto package: `a2a.v1` +- Protocol: A2A v1.0, locked to tag `v1.0.0` +- Proto package: `lf.a2a.v1` - Current implemented surface: `types`, `error`, `jsonrpc`, `server`, `client`, `store` - Remaining work: docs, examples, release polish - Zero Clawhive dependency diff --git a/Cargo.toml b/Cargo.toml index 0df2b18..8d3e669 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "a2a-rust" -version = "0.1.0" +version = "1.0.0" edition = "2024" rust-version = "1.85" description = "Rust SDK for the A2A (Agent-to-Agent) protocol" diff --git a/README.md b/README.md index 36ffd28..ad60beb 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![docs.rs](https://docs.rs/a2a-rust/badge.svg)](https://docs.rs/a2a-rust) [![License](https://img.shields.io/crates/l/a2a-rust.svg)](LICENSE-MIT) -Rust SDK for A2A Protocol v1.0 RC. +Rust SDK for A2A Protocol v1.0. `a2a-rust` provides: @@ -18,8 +18,8 @@ This crate has zero Clawhive-specific logic. ## Status -- Protocol lock: `v1.0.0-rc` -- Proto package: `a2a.v1` +- Protocol lock: `v1.0.0` +- Proto package: `lf.a2a.v1` - Implemented transports: `JSONRPC`, `HTTP+JSON` - Out of scope: gRPC @@ -36,7 +36,7 @@ Types-only usage: ```toml [dependencies] -a2a-rust = { version = "0.1", default-features = false } +a2a-rust = { version = "1", default-features = false } ``` ## Quick Start @@ -45,7 +45,7 @@ Add the crate: ```toml [dependencies] -a2a-rust = "0.1" +a2a-rust = "1" ``` ### Server @@ -84,7 +84,7 @@ impl A2AHandler for EchoAgent { }, ], provider: None, - version: "0.1.0".to_owned(), + version: "1.0.0".to_owned(), documentation_url: None, capabilities: AgentCapabilities { streaming: Some(false), @@ -210,7 +210,7 @@ cargo run --example ping_client --features client - server default endpoint: `POST /rpc` - compatibility alias: `POST /jsonrpc` -- method names use PascalCase v1.0 RC bindings such as `SendMessage`, `GetTask`, and `ListTasks` +- method names use PascalCase v1.0 bindings such as `SendMessage`, `GetTask`, and `ListTasks` ### REST @@ -274,8 +274,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for contributor workflow details. ## References -- [A2A Protocol Spec v1.0 RC](https://a2a-protocol.org/latest/specification/) -- [A2A Proto Source (v1.0.0-rc)](https://github.com/a2aproject/A2A/blob/v1.0.0-rc/specification/a2a.proto) +- [A2A Protocol Spec v1.0](https://a2a-protocol.org/latest/specification/) +- [A2A Proto Source (v1.0.0)](https://github.com/a2aproject/A2A/blob/v1.0.0/specification/a2a.proto) - [A2A Agent Discovery](https://a2a-protocol.org/latest/topics/agent-discovery/) ## License diff --git a/docs/proto-first-design.md b/docs/proto-first-design.md new file mode 100644 index 0000000..b0acb31 --- /dev/null +++ b/docs/proto-first-design.md @@ -0,0 +1,476 @@ +# a2a-rust Proto-First Design + +Status: implementation contract +Date: 2026-03-12 + +This document is the repository-local design for `a2a-rust`. It intentionally does not modify the earlier external planning note. For work in this repository, this document supersedes earlier notes when they disagree. + +## 1. Purpose + +`a2a-rust` is a generic Rust SDK for A2A Protocol v1.0. It provides: + +- a protocol-accurate type layer +- a server framework for REST, JSON-RPC, and SSE +- a client for discovery and remote invocation +- a stable foundation for downstream projects without any Clawhive-specific logic + +The crate is not a concrete agent implementation and does not include hub-specific behavior. + +## 2. Normative Sources + +The repository follows these sources in this order: + +1. Tagged proto: `v1.0.0`, commit `1736957` +2. Latest v1.0 specification text +3. Repository-local design documents + +Normative links: + +- Spec: +- Proto: + +Rules: + +- The proto is authoritative for data objects and request/response message shapes. +- The spec is authoritative for behavioral requirements, transport semantics, and interoperability guidance unless it conflicts with the proto. +- If spec prose and proto disagree, `a2a-rust` follows the proto for wire shape and documents the divergence. + +Version lock: + +- A2A protocol version: `1.0` +- Proto package: `lf.a2a.v1` +- Supported upstream release tag: `v1.0.0` + +## 3. Scope + +### In scope for `1.0.0` + +- Full core type system from the tagged proto +- REST server endpoints from the proto HTTP annotations +- JSON-RPC support using the v1.0 method names +- SSE streaming for `SendStreamingMessage` and `SubscribeToTask` +- Agent discovery from `/.well-known/agent-card.json` +- Client support for discovery, unary calls, and streaming calls +- Push-notification request and response types +- Push-notification server and client API surface with default "not supported" behavior +- Extended agent card API surface + +### Out of scope for `1.0.0` + +- Agent card signature verification +- Generated proto bindings as the public API +- gRPC transport implementation +- Clawhive-specific storage, config, terminology, or adapters + +## 4. Known Spec Drift and Repository Policy + +The spec and the tagged proto are not perfectly aligned. This crate uses the following policy: + +### 4.1 Method names + +Use PascalCase JSON-RPC methods from the latest v1.0 binding tables: + +- `SendMessage` +- `SendStreamingMessage` +- `GetTask` +- `ListTasks` +- `CancelTask` +- `SubscribeToTask` +- `CreateTaskPushNotificationConfig` +- `GetTaskPushNotificationConfig` +- `ListTaskPushNotificationConfigs` +- `DeleteTaskPushNotificationConfig` +- `GetExtendedAgentCard` + +The old slash-style methods from earlier A2A versions are not the primary design target. + +### 4.2 JSON-RPC endpoint path + +The agent card declares the actual JSON-RPC URL in `supportedInterfaces`. The crate should not hardcode `/jsonrpc` as the protocol contract. + +Repository decision: + +- server router defaults the JSON-RPC endpoint to `/rpc` +- router configuration may add compatibility aliases such as `/jsonrpc` +- the advertised `AgentInterface.url` must match the actual deployed endpoint + +### 4.3 Subscribe REST method + +The spec prose and proto HTTP annotation disagree for `SubscribeToTask`. + +- proto canonical binding: `GET /tasks/{id}:subscribe` +- spec prose still contains `POST /tasks/{id}:subscribe` + +Repository decision: + +- server exposes `GET` as canonical behavior +- server may optionally accept `POST` as a compatibility alias +- client uses `GET` + +### 4.4 SendMessageConfiguration field naming + +The final tagged proto defines: + +- `acceptedOutputModes` +- `taskPushNotificationConfig` +- `historyLength` +- `returnImmediately` + +Repository decision: + +- canonical Rust and JSON field name is `return_immediately` +- canonical push-config field is `task_push_notification_config` +- no RC compatibility alias is required in `1.0.0` + +## 5. Crate Architecture + +The crate is layered so that `types` remains transport-agnostic and low-dependency. + +```text +types/jsonrpc/error + ^ + | + server client + ^ + | + store +``` + +### 5.1 Modules + +```text +src/ + lib.rs + error.rs + jsonrpc.rs + types/ + mod.rs + agent_card.rs + task.rs + message.rs + security.rs + push.rs + requests.rs + responses.rs + server/ + mod.rs + handler.rs + router.rs + rest.rs + jsonrpc.rs + streaming.rs + client/ + mod.rs + discovery.rs + api.rs + store.rs +``` + +### 5.2 Feature flags + +- default: `server`, `client` +- `server`: enables `axum`, SSE helpers, router, and server-side error mapping +- `client`: enables `reqwest`, discovery, streaming parsers, and client transport errors + +Types, JSON-RPC envelopes, and core errors must compile with `default-features = false`. + +## 6. Rust Data Model Decisions + +### 6.1 Core mapping rules + +Use `serde(rename_all = "camelCase")` on all protocol structs unless a field requires special handling. + +Mapping choices: + +- `google.protobuf.Struct` -> `JsonObject = serde_json::Map` +- `google.protobuf.Value` -> `serde_json::Value` +- `google.protobuf.Timestamp` -> `String` containing RFC 3339 UTC text +- `bytes` -> `Vec` with custom base64 serde to match ProtoJSON +- proto maps -> `BTreeMap<_, _>` for deterministic serialization in tests and docs + +Rationale: + +- The public API should match ProtoJSON exactly. +- `types` should not force a time crate on downstream users in `1.0.0`. +- Deterministic map ordering simplifies serde tests and snapshots. + +### 6.2 Oneof handling + +Use enums for pure wrapper `oneof` messages: + +- `SendMessageResponse` +- `StreamResponse` +- `SecurityScheme` +- `OAuthFlows` + +Use structs with validation for mixed-content messages where `oneof` fields coexist with shared fields: + +- `Part` + +Validation helpers will reject invalid states such as: + +- `Part` containing zero or more than one of `text`, `raw`, `url`, `data` +- `SendMessageResponse` or `StreamResponse` with impossible payload combinations + +### 6.3 Proto-driven corrections to the previous design + +These decisions are required by the tagged proto: + +- `Task.context_id` is optional +- `TaskStatusUpdateEvent.context_id` is required +- `TaskArtifactUpdateEvent.context_id` is required +- `Role` includes `ROLE_UNSPECIFIED` +- `Artifact` does not have an `index` field +- `SecurityRequirement` is a wrapper object with `schemes` +- `OAuth2SecurityScheme.flows` is a typed object, not raw JSON +- `ListTasksRequest` and `ListTasksResponse` are richer than the earlier planning note +- `SubscribeToTaskRequest` only contains `tenant` and `id` + +### 6.4 Compatibility leniency + +`a2a-rust` should serialize canonically and deserialize strictly by default. The only planned lenient handling in `1.0.0` is: + +- alternate `SecurityScheme` JSON shapes used by known SDKs, if they can be accepted without ambiguity + +Any broader compatibility behavior should live behind explicit helper code, not weaken the canonical serializer. + +## 7. Protocol Objects to Implement + +The type layer must include at least: + +- `AgentCard`, `AgentInterface`, `AgentProvider`, `AgentCapabilities`, `AgentExtension`, `AgentSkill`, `AgentCardSignature` +- `Task`, `TaskStatus`, `TaskState` +- `Message`, `Role`, `Part`, `Artifact` +- `TaskStatusUpdateEvent`, `TaskArtifactUpdateEvent` +- `AuthenticationInfo`, `TaskPushNotificationConfig` +- `SecurityRequirement`, `SecurityScheme`, all security scheme variants, `OAuthFlows`, and OAuth flow structs +- all request messages from the tagged proto +- all response messages from the tagged proto + +The public type names should match the operation and object names from the tagged proto unless there is a compelling Rust API reason not to. + +## 8. Error Model + +The core error layer must cover all A2A-specific errors currently defined by the latest spec: + +- `TaskNotFoundError` -> `-32001` +- `TaskNotCancelableError` -> `-32002` +- `PushNotificationNotSupportedError` -> `-32003` +- `UnsupportedOperationError` -> `-32004` +- `ContentTypeNotSupportedError` -> `-32005` +- `InvalidAgentResponseError` -> `-32006` +- `ExtendedAgentCardNotConfiguredError` -> `-32007` +- `ExtensionSupportRequiredError` -> `-32008` +- `VersionNotSupportedError` -> `-32009` + +Design rules: + +- `A2AError` is transport-neutral in the core crate +- JSON-RPC and HTTP mapping helpers live alongside the core error type +- `reqwest` conversions are only compiled under the `client` feature +- server transport errors and parsing failures map to standard JSON-RPC errors or HTTP problem details as appropriate + +## 9. Server Design + +### 9.1 Handler trait + +Expose a user-implemented `A2AHandler` trait with default methods for optional functionality. + +Required methods: + +- `get_agent_card` +- `send_message` + +Defaulted methods: + +- `send_streaming_message` +- `get_task` +- `list_tasks` +- `cancel_task` +- `subscribe_to_task` +- push notification config CRUD/list +- `get_extended_agent_card` + +Defaults should return the appropriate A2A error instead of panicking. + +### 9.2 Router behavior + +The server router should: + +- expose the well-known discovery document at `/.well-known/agent-card.json` +- expose canonical REST endpoints from the proto annotations +- support tenant-prefixed additional bindings such as `/{tenant}/tasks` +- expose a configurable JSON-RPC endpoint, default `/rpc` +- return SSE with `data: \n\n` framing for streaming endpoints + +### 9.3 Streaming behavior + +`SendStreamingMessage`: + +- message-only flow: emit exactly one `StreamResponse::Message` and close +- task flow: emit initial `StreamResponse::Task`, then zero or more status or artifact updates, then close when interrupted or terminal + +`SubscribeToTask`: + +- emit current `Task` first +- then stream updates +- reject terminal tasks with `UnsupportedOperationError` + +### 9.4 Capability gates + +The server must enforce card-declared capabilities: + +- if `capabilities.streaming != Some(true)`, streaming operations return `UnsupportedOperationError` +- if `capabilities.push_notifications != Some(true)`, push-config operations return `PushNotificationNotSupportedError` +- if `capabilities.extended_agent_card != Some(true)`, extended card requests return `ExtendedAgentCardNotConfiguredError` + +## 10. Client Design + +### 10.1 Discovery + +`AgentCardDiscovery` should: + +- fetch `/.well-known/agent-card.json` +- cache by base URL with TTL +- return the cached result unless refresh is requested + +### 10.2 Transport selection + +The client should inspect `supportedInterfaces` and prefer: + +1. `JSONRPC` +2. `HTTP+JSON` + +`GRPC` is out of scope for `1.0.0`. + +The client always sends `A2A-Version: 1.0` and optionally `A2A-Extensions`. + +### 10.3 JSON-RPC client behavior + +The client must: + +- use the PascalCase v1.0 method names +- preserve the request `id` on response matching +- distinguish JSON-RPC transport errors from A2A application errors +- parse SSE streams as sequences of `StreamResponse` objects + +### 10.4 REST behavior + +The client should support REST at least for: + +- discovery +- `SendMessage` +- `SendStreamingMessage` +- `GetTask` +- `ListTasks` +- `CancelTask` +- `SubscribeToTask` +- `GetExtendedAgentCard` + +Push-notification REST methods should exist in the API surface even if many servers return "not supported". + +## 11. TaskStore + +`TaskStore` is server-side infrastructure and stays outside `types`. + +Required behavior: + +- get by task id +- upsert task +- list tasks with cursor pagination semantics +- delete task + +The built-in `InMemoryTaskStore` should support: + +- TTL-based expiration +- bounded capacity +- deterministic ordering for `ListTasks` + +`ListTasks` ordering requirement: + +- descending by task status timestamp + +## 12. Validation Strategy + +Serde alone is not sufficient. The crate should provide explicit validation for: + +- required proto-oneof invariants +- object-only metadata and params fields +- page size bounds +- required first-event behavior in subscription streams +- capability and extension checks + +Validation should happen in three places: + +1. deserialization-time when it is cheap and unambiguous +2. explicit `validate()` helpers on public request and response types +3. server/client operation boundaries before wire transmission + +## 13. Testing Strategy + +### 13.1 Type tests + +Most important tests: + +- serde round trips for every protocol object +- canonical examples from spec and proto +- rejection tests for invalid `oneof` states +- JSON base64 handling for `Part.raw` +- security scheme deserialization + +### 13.2 Server integration + +Use `tower::ServiceExt` against the axum router to test: + +- well-known discovery +- tenant and non-tenant REST routes +- JSON-RPC dispatch +- SSE framing and ordering +- push and extended-card default errors + +### 13.3 Client integration + +Use `wiremock` to test: + +- discovery caching +- JSON-RPC unary calls +- REST fallback behavior +- SSE parsing +- error mapping + +## 14. Implementation Order + +Phase 1: + +- `Cargo.toml` feature wiring +- `lib.rs` +- `error.rs` +- `jsonrpc.rs` +- `types/*` + +Phase 2: + +- `server/*` +- `store.rs` +- server integration tests + +Phase 3: + +- `client/*` +- client integration tests +- examples and docs cleanup + +## 15. Non-Negotiable Constraints + +- no Clawhive-specific logic +- no `unsafe` +- no `unwrap()` outside tests +- no invented A2A error codes +- no divergence from tagged proto field names or shapes without an explicitly documented compatibility reason + +## 16. Open Issues to Revisit Later + +- whether to add optional compatibility aliases for older RC-era field names if real interop requires them +- whether to expose typed timestamp wrappers in a future opt-in feature +- whether to accept additional JSON-RPC endpoint aliases beyond `/rpc` +- whether a separate `compat` module is needed for cross-SDK interoperability quirks beyond `SecurityScheme` diff --git a/examples/echo_server.rs b/examples/echo_server.rs index 807357f..e73e16f 100644 --- a/examples/echo_server.rs +++ b/examples/echo_server.rs @@ -29,7 +29,7 @@ impl A2AHandler for EchoAgent { }, ], provider: None, - version: "0.1.0".to_owned(), + version: "1.0.0".to_owned(), documentation_url: None, capabilities: AgentCapabilities { streaming: Some(false), diff --git a/src/client/api.rs b/src/client/api.rs index 4fa7231..4bf97b0 100644 --- a/src/client/api.rs +++ b/src/client/api.rs @@ -17,17 +17,16 @@ use crate::jsonrpc::{ JsonRpcError, JsonRpcId, JsonRpcRequest, JsonRpcResponse, METHOD_CANCEL_TASK, METHOD_CREATE_TASK_PUSH_NOTIFICATION_CONFIG, METHOD_DELETE_TASK_PUSH_NOTIFICATION_CONFIG, METHOD_GET_EXTENDED_AGENT_CARD, METHOD_GET_TASK, METHOD_GET_TASK_PUSH_NOTIFICATION_CONFIG, - METHOD_LIST_TASK_PUSH_NOTIFICATION_CONFIG, METHOD_LIST_TASKS, METHOD_NOT_FOUND, + METHOD_LIST_TASK_PUSH_NOTIFICATION_CONFIGS, METHOD_LIST_TASKS, METHOD_NOT_FOUND, METHOD_SEND_MESSAGE, PARSE_ERROR, PROTOCOL_VERSION, PUSH_NOTIFICATION_NOT_SUPPORTED, TASK_NOT_CANCELABLE, TASK_NOT_FOUND, UNSUPPORTED_OPERATION, VERSION_NOT_SUPPORTED, }; use crate::types::{ - AgentCard, AgentInterface, CancelTaskRequest, CreateTaskPushNotificationConfigRequest, - DeleteTaskPushNotificationConfigRequest, GetExtendedAgentCardRequest, - GetTaskPushNotificationConfigRequest, GetTaskRequest, ListTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigResponse, ListTasksRequest, ListTasksResponse, - SendMessageRequest, SendMessageResponse, StreamResponse, SubscribeToTaskRequest, Task, - TaskPushNotificationConfig, + AgentCard, AgentInterface, CancelTaskRequest, DeleteTaskPushNotificationConfigRequest, + GetExtendedAgentCardRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, + ListTaskPushNotificationConfigsRequest, ListTaskPushNotificationConfigsResponse, + ListTasksRequest, ListTasksResponse, SendMessageRequest, SendMessageResponse, StreamResponse, + SubscribeToTaskRequest, Task, TaskPushNotificationConfig, }; use super::discovery::{ @@ -237,12 +236,15 @@ impl A2AClient { request.tenant.as_deref(), &["tasks", &cancel_segment], )?; - self.read_json_response( - self.apply_protocol_headers(self.client.post(url)) - .send() - .await?, - ) - .await + let builder = self.apply_protocol_headers(self.client.post(url)); + let builder = if let Some(metadata) = &request.metadata { + builder.json(&CancelTaskBody { + metadata: Some(metadata.clone()), + }) + } else { + builder + }; + self.read_json_response(builder.send().await?).await } } } @@ -272,7 +274,7 @@ impl A2AClient { /// Create or replace a push-notification configuration for a task. pub async fn create_task_push_notification_config( &self, - request: CreateTaskPushNotificationConfigRequest, + request: TaskPushNotificationConfig, ) -> Result { match self.transport().await? { TransportEndpoint::JsonRpc(url) => { @@ -287,10 +289,7 @@ impl A2AClient { )?; self.read_json_response( self.apply_protocol_headers(self.client.post(url)) - .query(&CreateTaskPushNotificationConfigQuery { - config_id: request.config_id, - }) - .json(&request.config) + .json(&request) .send() .await?, ) @@ -331,15 +330,15 @@ impl A2AClient { } /// List push-notification configurations for a task. - pub async fn list_task_push_notification_config( + pub async fn list_task_push_notification_configs( &self, - request: ListTaskPushNotificationConfigRequest, - ) -> Result { + request: ListTaskPushNotificationConfigsRequest, + ) -> Result { request.validate()?; match self.transport().await? { TransportEndpoint::JsonRpc(url) => { - self.jsonrpc_call(&url, METHOD_LIST_TASK_PUSH_NOTIFICATION_CONFIG, &request) + self.jsonrpc_call(&url, METHOD_LIST_TASK_PUSH_NOTIFICATION_CONFIGS, &request) .await } TransportEndpoint::HttpJson(base_url) => { @@ -350,7 +349,7 @@ impl A2AClient { )?; self.read_json_response( self.apply_protocol_headers(self.client.get(url)) - .query(&ListTaskPushNotificationConfigQuery { + .query(&ListTaskPushNotificationConfigsQuery { page_size: request.page_size, page_token: request.page_token, }) @@ -788,17 +787,18 @@ struct ListTasksQuery { #[derive(serde::Serialize)] #[serde(rename_all = "camelCase")] -struct CreateTaskPushNotificationConfigQuery { - config_id: String, +struct ListTaskPushNotificationConfigsQuery { + #[serde(skip_serializing_if = "Option::is_none")] + page_size: Option, + #[serde(skip_serializing_if = "Option::is_none")] + page_token: Option, } #[derive(serde::Serialize)] #[serde(rename_all = "camelCase")] -struct ListTaskPushNotificationConfigQuery { +struct CancelTaskBody { #[serde(skip_serializing_if = "Option::is_none")] - page_size: Option, - #[serde(skip_serializing_if = "Option::is_none")] - page_token: Option, + metadata: Option, } #[derive(serde::Deserialize)] diff --git a/src/jsonrpc.rs b/src/jsonrpc.rs index b968770..0ce22fb 100644 --- a/src/jsonrpc.rs +++ b/src/jsonrpc.rs @@ -52,8 +52,8 @@ pub const METHOD_SUBSCRIBE_TO_TASK: &str = "SubscribeToTask"; pub const METHOD_CREATE_TASK_PUSH_NOTIFICATION_CONFIG: &str = "CreateTaskPushNotificationConfig"; /// JSON-RPC method name for `GetTaskPushNotificationConfig`. pub const METHOD_GET_TASK_PUSH_NOTIFICATION_CONFIG: &str = "GetTaskPushNotificationConfig"; -/// JSON-RPC method name for `ListTaskPushNotificationConfig`. -pub const METHOD_LIST_TASK_PUSH_NOTIFICATION_CONFIG: &str = "ListTaskPushNotificationConfig"; +/// JSON-RPC method name for `ListTaskPushNotificationConfigs`. +pub const METHOD_LIST_TASK_PUSH_NOTIFICATION_CONFIGS: &str = "ListTaskPushNotificationConfigs"; /// JSON-RPC method name for `DeleteTaskPushNotificationConfig`. pub const METHOD_DELETE_TASK_PUSH_NOTIFICATION_CONFIG: &str = "DeleteTaskPushNotificationConfig"; /// JSON-RPC method name for `GetExtendedAgentCard`. diff --git a/src/lib.rs b/src/lib.rs index d9b396d..1b1c708 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//! Rust SDK for the A2A Protocol v1.0 RC. +//! Rust SDK for the A2A Protocol v1.0. //! //! This crate provides a protocol-accurate type layer plus optional server and //! client implementations behind feature flags. diff --git a/src/server/handler.rs b/src/server/handler.rs index 1307524..cef1208 100644 --- a/src/server/handler.rs +++ b/src/server/handler.rs @@ -8,12 +8,11 @@ use futures_core::Stream; use crate::A2AError; use crate::jsonrpc::PROTOCOL_VERSION; use crate::types::{ - AgentCard, CancelTaskRequest, CreateTaskPushNotificationConfigRequest, - DeleteTaskPushNotificationConfigRequest, GetExtendedAgentCardRequest, - GetTaskPushNotificationConfigRequest, GetTaskRequest, ListTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigResponse, ListTasksRequest, ListTasksResponse, - SendMessageRequest, SendMessageResponse, StreamResponse, SubscribeToTaskRequest, Task, - TaskPushNotificationConfig, + AgentCard, CancelTaskRequest, DeleteTaskPushNotificationConfigRequest, + GetExtendedAgentCardRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, + ListTaskPushNotificationConfigsRequest, ListTaskPushNotificationConfigsResponse, + ListTasksRequest, ListTasksResponse, SendMessageRequest, SendMessageResponse, StreamResponse, + SubscribeToTaskRequest, Task, TaskPushNotificationConfig, }; /// Server-side stream of A2A `StreamResponse` values. @@ -81,7 +80,7 @@ pub trait A2AHandler: Send + Sync + 'static { /// Create or replace a push-notification configuration. async fn create_task_push_notification_config( &self, - _request: CreateTaskPushNotificationConfigRequest, + _request: TaskPushNotificationConfig, ) -> Result { self.require_push_notifications_capability("CreateTaskPushNotificationConfig") .await?; @@ -103,14 +102,14 @@ pub trait A2AHandler: Send + Sync + 'static { } /// List stored push-notification configurations. - async fn list_task_push_notification_config( + async fn list_task_push_notification_configs( &self, - _request: ListTaskPushNotificationConfigRequest, - ) -> Result { - self.require_push_notifications_capability("ListTaskPushNotificationConfig") + _request: ListTaskPushNotificationConfigsRequest, + ) -> Result { + self.require_push_notifications_capability("ListTaskPushNotificationConfigs") .await?; Err(A2AError::UnsupportedOperation( - "ListTaskPushNotificationConfig".to_owned(), + "ListTaskPushNotificationConfigs".to_owned(), )) } diff --git a/src/server/jsonrpc.rs b/src/server/jsonrpc.rs index bca1084..4d30ea2 100644 --- a/src/server/jsonrpc.rs +++ b/src/server/jsonrpc.rs @@ -10,14 +10,13 @@ use crate::jsonrpc::{ JSONRPC_VERSION, JsonRpcId, JsonRpcRequest, JsonRpcResponse, METHOD_CANCEL_TASK, METHOD_CREATE_TASK_PUSH_NOTIFICATION_CONFIG, METHOD_DELETE_TASK_PUSH_NOTIFICATION_CONFIG, METHOD_GET_EXTENDED_AGENT_CARD, METHOD_GET_TASK, METHOD_GET_TASK_PUSH_NOTIFICATION_CONFIG, - METHOD_LIST_TASK_PUSH_NOTIFICATION_CONFIG, METHOD_LIST_TASKS, METHOD_SEND_MESSAGE, + METHOD_LIST_TASK_PUSH_NOTIFICATION_CONFIGS, METHOD_LIST_TASKS, METHOD_SEND_MESSAGE, METHOD_SEND_STREAMING_MESSAGE, METHOD_SUBSCRIBE_TO_TASK, }; use crate::types::{ - CancelTaskRequest, CreateTaskPushNotificationConfigRequest, - DeleteTaskPushNotificationConfigRequest, GetExtendedAgentCardRequest, - GetTaskPushNotificationConfigRequest, GetTaskRequest, ListTaskPushNotificationConfigRequest, - ListTasksRequest, SendMessageRequest, SubscribeToTaskRequest, + CancelTaskRequest, DeleteTaskPushNotificationConfigRequest, GetExtendedAgentCardRequest, + GetTaskPushNotificationConfigRequest, GetTaskRequest, ListTaskPushNotificationConfigsRequest, + ListTasksRequest, SendMessageRequest, SubscribeToTaskRequest, TaskPushNotificationConfig, }; use super::handler::A2AHandler; @@ -102,7 +101,7 @@ where .await } METHOD_CREATE_TASK_PUSH_NOTIFICATION_CONFIG => { - parse_params::(request.params) + parse_params::(request.params) .and_then_async(|params| handler.create_task_push_notification_config(params)) .await .map(serde_json::to_value) @@ -115,10 +114,10 @@ where .map(serde_json::to_value) .and_then(map_serialization_error) } - METHOD_LIST_TASK_PUSH_NOTIFICATION_CONFIG => { - parse_params::(request.params) + METHOD_LIST_TASK_PUSH_NOTIFICATION_CONFIGS => { + parse_params::(request.params) .and_then(|params| params.validate().map(|_| params)) - .and_then_async(|params| handler.list_task_push_notification_config(params)) + .and_then_async(|params| handler.list_task_push_notification_configs(params)) .await .map(serde_json::to_value) .and_then(map_serialization_error) diff --git a/src/server/rest.rs b/src/server/rest.rs index c39e433..3a7cb85 100644 --- a/src/server/rest.rs +++ b/src/server/rest.rs @@ -9,12 +9,11 @@ use serde::Deserialize; use crate::A2AError; use crate::error::ProblemDetails; use crate::types::{ - AgentCard, CancelTaskRequest, CreateTaskPushNotificationConfigRequest, - DeleteTaskPushNotificationConfigRequest, GetExtendedAgentCardRequest, - GetTaskPushNotificationConfigRequest, GetTaskRequest, ListTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigResponse, ListTasksRequest, ListTasksResponse, - PushNotificationConfig, SendMessageRequest, SendMessageResponse, SubscribeToTaskRequest, Task, - TaskPushNotificationConfig, + AgentCard, CancelTaskRequest, DeleteTaskPushNotificationConfigRequest, + GetExtendedAgentCardRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, + ListTaskPushNotificationConfigsRequest, ListTaskPushNotificationConfigsResponse, + ListTasksRequest, ListTasksResponse, SendMessageRequest, SendMessageResponse, + SubscribeToTaskRequest, Task, TaskPushNotificationConfig, }; use super::handler::A2AHandler; @@ -216,6 +215,7 @@ pub(super) async fn cancel_task( headers: HeaderMap, Path(id): Path, Query(query): Query, + body: Option>, ) -> Result, RestErrorResponse> where H: A2AHandler, @@ -231,6 +231,7 @@ where .cancel_task(CancelTaskRequest { id: id.to_owned(), tenant: query.tenant, + metadata: body.and_then(|body| body.metadata.clone()), }) .await .map(Json) @@ -242,6 +243,7 @@ pub(super) async fn tenant_cancel_task( headers: HeaderMap, Path((tenant, id)): Path<(String, String)>, Query(mut query): Query, + body: Option>, ) -> Result, RestErrorResponse> where H: A2AHandler, @@ -257,6 +259,7 @@ where .cancel_task(CancelTaskRequest { id: id.to_owned(), tenant: query.tenant, + metadata: body.and_then(|body| body.metadata.clone()), }) .await .map(Json) @@ -308,8 +311,8 @@ pub(super) async fn create_task_push_notification_config( State(handler): State>, headers: HeaderMap, Path(task_id): Path, - Query(query): Query, - Json(config): Json, + Query(query): Query, + Json(mut config): Json, ) -> Result, RestErrorResponse> where H: A2AHandler, @@ -317,13 +320,11 @@ where handler.validate_protocol_headers(&headers).await?; reject_query_tenant(&query.tenant)?; + config.task_id = task_id; + config.tenant = query.tenant; + handler - .create_task_push_notification_config(CreateTaskPushNotificationConfigRequest { - task_id, - config_id: query.config_id, - config, - tenant: query.tenant, - }) + .create_task_push_notification_config(config) .await .map(Json) .map_err(rest_error) @@ -333,22 +334,18 @@ pub(super) async fn tenant_create_task_push_notification_config( State(handler): State>, headers: HeaderMap, Path((tenant, task_id)): Path<(String, String)>, - Query(mut query): Query, - Json(config): Json, + Query(_query): Query, + Json(mut config): Json, ) -> Result, RestErrorResponse> where H: A2AHandler, { handler.validate_protocol_headers(&headers).await?; - query.tenant = Some(tenant); + config.task_id = task_id; + config.tenant = Some(tenant); handler - .create_task_push_notification_config(CreateTaskPushNotificationConfigRequest { - task_id, - config_id: query.config_id, - config, - tenant: query.tenant, - }) + .create_task_push_notification_config(config) .await .map(Json) .map_err(rest_error) @@ -400,19 +397,19 @@ where .map_err(rest_error) } -pub(super) async fn list_task_push_notification_config( +pub(super) async fn list_task_push_notification_configs( State(handler): State>, headers: HeaderMap, Path(task_id): Path, - Query(query): Query, -) -> Result, RestErrorResponse> + Query(query): Query, +) -> Result, RestErrorResponse> where H: A2AHandler, { handler.validate_protocol_headers(&headers).await?; reject_query_tenant(&query.tenant)?; - let request = ListTaskPushNotificationConfigRequest { + let request = ListTaskPushNotificationConfigsRequest { task_id, page_size: query.page_size, page_token: query.page_token, @@ -421,25 +418,25 @@ where request.validate()?; handler - .list_task_push_notification_config(request) + .list_task_push_notification_configs(request) .await .map(Json) .map_err(rest_error) } -pub(super) async fn tenant_list_task_push_notification_config( +pub(super) async fn tenant_list_task_push_notification_configs( State(handler): State>, headers: HeaderMap, Path((tenant, task_id)): Path<(String, String)>, - Query(mut query): Query, -) -> Result, RestErrorResponse> + Query(mut query): Query, +) -> Result, RestErrorResponse> where H: A2AHandler, { handler.validate_protocol_headers(&headers).await?; query.tenant = Some(tenant); - let request = ListTaskPushNotificationConfigRequest { + let request = ListTaskPushNotificationConfigsRequest { task_id, page_size: query.page_size, page_token: query.page_token, @@ -448,7 +445,7 @@ where request.validate()?; handler - .list_task_push_notification_config(request) + .list_task_push_notification_configs(request) .await .map(Json) .map_err(rest_error) @@ -509,17 +506,9 @@ pub(super) struct GetTaskQuery { pub history_length: Option, } -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(super) struct CreateTaskPushNotificationConfigQuery { - pub config_id: String, - #[serde(default)] - pub tenant: Option, -} - #[derive(Debug, Clone, Default, Deserialize)] #[serde(rename_all = "camelCase")] -pub(super) struct ListTaskPushNotificationConfigQuery { +pub(super) struct ListTaskPushNotificationConfigsQuery { #[serde(default)] pub tenant: Option, #[serde(default)] @@ -535,6 +524,13 @@ pub(super) struct TenantQuery { pub tenant: Option, } +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct CancelTaskBody { + #[serde(default)] + pub metadata: Option, +} + pub(super) fn rest_error(error: A2AError) -> RestErrorResponse { RestErrorResponse { status: error.status_code(), diff --git a/src/server/router.rs b/src/server/router.rs index 298f555..e753c9a 100644 --- a/src/server/router.rs +++ b/src/server/router.rs @@ -44,12 +44,12 @@ where .route( "/tasks/{task_id}/pushNotificationConfigs", post(rest::create_task_push_notification_config::) - .get(rest::list_task_push_notification_config::), + .get(rest::list_task_push_notification_configs::), ) .route( "/{tenant}/tasks/{task_id}/pushNotificationConfigs", post(rest::tenant_create_task_push_notification_config::) - .get(rest::tenant_list_task_push_notification_config::), + .get(rest::tenant_list_task_push_notification_configs::), ) .route( "/tasks/{task_id}/pushNotificationConfigs/{id}", diff --git a/src/store.rs b/src/store.rs index cd1efb7..b428282 100644 --- a/src/store.rs +++ b/src/store.rs @@ -195,7 +195,7 @@ fn enforce_capacity(tasks: &mut BTreeMap, max_entries: Optio fn task_matches(task: &Task, req: &ListTasksRequest) -> bool { if let Some(context_id) = &req.context_id - && &task.context_id != context_id + && task.context_id.as_deref() != Some(context_id.as_str()) { return false; } @@ -260,7 +260,7 @@ mod tests { store .put(&Task { id: "task-1".to_owned(), - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::Submitted, message: None, @@ -276,7 +276,7 @@ mod tests { store .put(&Task { id: "task-2".to_owned(), - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::Working, message: None, @@ -316,7 +316,7 @@ mod tests { store .put(&Task { id: "task-1".to_owned(), - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::Completed, message: None, @@ -373,7 +373,7 @@ mod tests { store .put(&Task { id: "task-1".to_owned(), - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::Submitted, message: None, @@ -402,7 +402,7 @@ mod tests { store .put(&Task { id: "task-1".to_owned(), - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::Submitted, message: None, @@ -419,7 +419,7 @@ mod tests { store .put(&Task { id: "task-2".to_owned(), - context_id: "ctx-2".to_owned(), + context_id: Some("ctx-2".to_owned()), status: TaskStatus { state: TaskState::Working, message: None, @@ -445,7 +445,7 @@ mod tests { store .put(&Task { id: "task-3".to_owned(), - context_id: "ctx-3".to_owned(), + context_id: Some("ctx-3".to_owned()), status: TaskStatus { state: TaskState::Completed, message: None, @@ -497,7 +497,7 @@ mod tests { store .put(&Task { id: task_id.clone(), - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::Working, message: None, diff --git a/src/types/auth.rs b/src/types/auth.rs index 30b8c66..23ea2ee 100644 --- a/src/types/auth.rs +++ b/src/types/auth.rs @@ -212,7 +212,7 @@ mod tests { let task = Task { id: "task-1".to_owned(), - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::AuthRequired, message: Some(message), @@ -231,7 +231,7 @@ mod tests { fn task_rejects_auth_required_without_metadata() { let task = Task { id: "task-1".to_owned(), - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::AuthRequired, message: Some(Message { diff --git a/src/types/push.rs b/src/types/push.rs index 925c7eb..8db6006 100644 --- a/src/types/push.rs +++ b/src/types/push.rs @@ -1,22 +1,5 @@ use serde::{Deserialize, Serialize}; -/// Push-notification delivery target. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PushNotificationConfig { - #[serde(default, skip_serializing_if = "Option::is_none")] - /// Optional provider-specific configuration identifier. - pub id: Option, - /// Destination URL for push delivery. - pub url: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - /// Optional opaque bearer token or shared secret. - pub token: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - /// Optional authentication description. - pub authentication: Option, -} - /// Authentication details for a push target. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -32,13 +15,21 @@ pub struct AuthenticationInfo { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TaskPushNotificationConfig { + #[serde(default, skip_serializing_if = "Option::is_none")] + /// Optional tenant associated with the configuration. + pub tenant: Option, + #[serde(default, skip_serializing_if = "String::is_empty")] /// Unique configuration identifier. pub id: String, + #[serde(default, skip_serializing_if = "String::is_empty")] /// Task identifier that owns the configuration. pub task_id: String, - /// Push delivery settings. - pub push_notification_config: PushNotificationConfig, + /// Destination URL for push delivery. + pub url: String, #[serde(default, skip_serializing_if = "Option::is_none")] - /// Optional tenant associated with the configuration. - pub tenant: Option, + /// Optional opaque bearer token or shared secret. + pub token: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + /// Optional authentication description. + pub authentication: Option, } diff --git a/src/types/requests.rs b/src/types/requests.rs index ef1d70d..9ac553e 100644 --- a/src/types/requests.rs +++ b/src/types/requests.rs @@ -4,7 +4,7 @@ use crate::A2AError; use crate::types::JsonObject; use super::message::Message; -use super::push::PushNotificationConfig; +use super::push::TaskPushNotificationConfig; use super::task::TaskState; /// Optional configuration for `SendMessage`. @@ -15,14 +15,14 @@ pub struct SendMessageConfiguration { /// Output modes the caller can accept. pub accepted_output_modes: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] - /// Optional push configuration to attach to the request. - pub push_notification_config: Option, + /// Optional task push configuration to attach to the request. + pub task_push_notification_config: Option, #[serde(default, skip_serializing_if = "Option::is_none")] /// Maximum history items requested in task responses. pub history_length: Option, #[serde(default, skip_serializing_if = "crate::types::is_false")] - /// Whether the server should block for a final response when possible. - pub blocking: bool, + /// Whether the server should return immediately instead of waiting. + pub return_immediately: bool, } /// Request payload for `SendMessage`. @@ -117,6 +117,9 @@ pub struct CancelTaskRequest { #[serde(default, skip_serializing_if = "Option::is_none")] /// Optional tenant identifier. pub tenant: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + /// Optional request metadata. + pub metadata: Option, } /// Request payload for `GetTaskPushNotificationConfig`. @@ -145,21 +148,6 @@ pub struct DeleteTaskPushNotificationConfigRequest { pub tenant: Option, } -/// Request payload for `CreateTaskPushNotificationConfig`. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateTaskPushNotificationConfigRequest { - /// Owning task identifier. - pub task_id: String, - /// Desired push configuration identifier. - pub config_id: String, - /// Push delivery configuration. - pub config: PushNotificationConfig, - #[serde(default, skip_serializing_if = "Option::is_none")] - /// Optional tenant identifier. - pub tenant: Option, -} - /// Request payload for `SubscribeToTask`. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -171,10 +159,10 @@ pub struct SubscribeToTaskRequest { pub tenant: Option, } -/// Request payload for `ListTaskPushNotificationConfig`. +/// Request payload for `ListTaskPushNotificationConfigs`. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ListTaskPushNotificationConfigRequest { +pub struct ListTaskPushNotificationConfigsRequest { /// Owning task identifier. pub task_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -188,7 +176,7 @@ pub struct ListTaskPushNotificationConfigRequest { pub tenant: Option, } -impl ListTaskPushNotificationConfigRequest { +impl ListTaskPushNotificationConfigsRequest { /// Validate required identifiers. pub fn validate(&self) -> Result<(), A2AError> { if self.task_id.is_empty() { @@ -203,12 +191,12 @@ impl ListTaskPushNotificationConfigRequest { #[cfg(test)] mod tests { - use super::{ListTaskPushNotificationConfigRequest, ListTasksRequest, SendMessageRequest}; + use super::{ListTaskPushNotificationConfigsRequest, ListTasksRequest, SendMessageRequest}; use crate::types::{Message, Part, Role}; #[test] - fn list_task_push_notification_config_request_rejects_empty_task_id() { - let request = ListTaskPushNotificationConfigRequest { + fn list_task_push_notification_configs_request_rejects_empty_task_id() { + let request = ListTaskPushNotificationConfigsRequest { task_id: String::new(), page_size: None, page_token: None, diff --git a/src/types/responses.rs b/src/types/responses.rs index a74ad59..edc03b0 100644 --- a/src/types/responses.rs +++ b/src/types/responses.rs @@ -126,10 +126,10 @@ pub struct ListTasksResponse { pub total_size: i32, } -/// Paginated response for `ListTaskPushNotificationConfig`. +/// Paginated response for `ListTaskPushNotificationConfigs`. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ListTaskPushNotificationConfigResponse { +pub struct ListTaskPushNotificationConfigsResponse { #[serde(default, skip_serializing_if = "Vec::is_empty")] /// Returned push-notification configuration page. pub configs: Vec, @@ -141,7 +141,7 @@ pub struct ListTaskPushNotificationConfigResponse { #[cfg(test)] mod tests { use super::{ - ListTaskPushNotificationConfigResponse, SendMessageResponse, StreamResponse, + ListTaskPushNotificationConfigsResponse, SendMessageResponse, StreamResponse, TaskArtifactUpdateEvent, TaskStatusUpdateEvent, }; use crate::types::{Artifact, Message, Part, Role, Task, TaskState, TaskStatus}; @@ -205,7 +205,7 @@ mod tests { #[test] fn list_push_notification_response_uses_empty_string_for_no_next_page() { - let response = ListTaskPushNotificationConfigResponse { + let response = ListTaskPushNotificationConfigsResponse { configs: Vec::new(), next_page_token: String::new(), }; @@ -291,7 +291,7 @@ mod tests { fn stream_response_round_trip_serialization() { let response = StreamResponse::Task(Task { id: "task-1".to_owned(), - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::Submitted, message: None, diff --git a/src/types/task.rs b/src/types/task.rs index ad1367c..c098a88 100644 --- a/src/types/task.rs +++ b/src/types/task.rs @@ -10,8 +10,9 @@ use super::message::{Artifact, Message}; pub struct Task { /// Unique task identifier. pub id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] /// Context identifier shared with related messages and updates. - pub context_id: String, + pub context_id: Option, /// Current task status. pub status: TaskStatus, #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -88,7 +89,7 @@ mod tests { fn task_round_trip_serialization() { let task = Task { id: "task-1".to_owned(), - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::Completed, message: Some(Message { @@ -120,7 +121,7 @@ mod tests { let round_trip: Task = serde_json::from_str(&json).expect("task should deserialize"); assert_eq!(round_trip.id, "task-1"); - assert_eq!(round_trip.context_id, "ctx-1"); + assert_eq!(round_trip.context_id.as_deref(), Some("ctx-1")); assert_eq!(round_trip.status.state, TaskState::Completed); } } diff --git a/tests/client_integration.rs b/tests/client_integration.rs index 3cb7d0c..c748f67 100644 --- a/tests/client_integration.rs +++ b/tests/client_integration.rs @@ -13,11 +13,10 @@ use a2a_rust::client::{A2AClient, A2AClientConfig, AgentCardDiscovery, AgentCard use a2a_rust::server::{A2AHandler, A2AStream, router}; use a2a_rust::types::{ AgentCapabilities, AgentCard, AgentInterface, CancelTaskRequest, - CreateTaskPushNotificationConfigRequest, DeleteTaskPushNotificationConfigRequest, - GetExtendedAgentCardRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, - ListTaskPushNotificationConfigRequest, ListTaskPushNotificationConfigResponse, - ListTasksRequest, ListTasksResponse, Message, Part, PushNotificationConfig, Role, - SendMessageRequest, SendMessageResponse, StreamResponse, SubscribeToTaskRequest, Task, + DeleteTaskPushNotificationConfigRequest, GetExtendedAgentCardRequest, + GetTaskPushNotificationConfigRequest, GetTaskRequest, ListTaskPushNotificationConfigsRequest, + ListTaskPushNotificationConfigsResponse, ListTasksRequest, ListTasksResponse, Message, Part, + Role, SendMessageRequest, SendMessageResponse, StreamResponse, SubscribeToTaskRequest, Task, TaskPushNotificationConfig, TaskState, TaskStatus, TaskStatusUpdateEvent, }; @@ -111,7 +110,7 @@ impl A2AHandler for ClientTestHandler { Ok(Task { id: request.id, - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::Working, message: None, @@ -127,7 +126,7 @@ impl A2AHandler for ClientTestHandler { Ok(ListTasksResponse { tasks: vec![Task { id: "task-1".to_owned(), - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::Submitted, message: None, @@ -146,7 +145,7 @@ impl A2AHandler for ClientTestHandler { async fn cancel_task(&self, request: CancelTaskRequest) -> Result { Ok(Task { id: request.id, - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::Canceled, message: None, @@ -167,7 +166,7 @@ impl A2AHandler for ClientTestHandler { Ok(Box::pin(stream::iter(vec![ StreamResponse::Task(Task { id: request.id.clone(), - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::Working, message: None, @@ -192,14 +191,9 @@ impl A2AHandler for ClientTestHandler { async fn create_task_push_notification_config( &self, - request: CreateTaskPushNotificationConfigRequest, + request: TaskPushNotificationConfig, ) -> Result { - Ok(TaskPushNotificationConfig { - id: request.config_id.clone(), - task_id: request.task_id, - push_notification_config: request.config, - tenant: request.tenant, - }) + Ok(request) } async fn get_task_push_notification_config( @@ -209,31 +203,25 @@ impl A2AHandler for ClientTestHandler { Ok(TaskPushNotificationConfig { id: request.id.clone(), task_id: request.task_id, - push_notification_config: PushNotificationConfig { - id: Some(request.id), - url: "https://example.com/push".to_owned(), - token: Some("secret".to_owned()), - authentication: None, - }, tenant: request.tenant, + url: "https://example.com/push".to_owned(), + token: Some("secret".to_owned()), + authentication: None, }) } - async fn list_task_push_notification_config( + async fn list_task_push_notification_configs( &self, - request: ListTaskPushNotificationConfigRequest, - ) -> Result { - Ok(ListTaskPushNotificationConfigResponse { + request: ListTaskPushNotificationConfigsRequest, + ) -> Result { + Ok(ListTaskPushNotificationConfigsResponse { configs: vec![TaskPushNotificationConfig { id: "cfg-1".to_owned(), task_id: request.task_id, - push_notification_config: PushNotificationConfig { - id: Some("cfg-1".to_owned()), - url: "https://example.com/push".to_owned(), - token: None, - authentication: None, - }, tenant: request.tenant, + url: "https://example.com/push".to_owned(), + token: None, + authentication: None, }], next_page_token: String::new(), }) @@ -419,6 +407,7 @@ async fn client_supports_unary_rest_and_jsonrpc_operations() { .cancel_task(CancelTaskRequest { id: "task-1".to_owned(), tenant: None, + metadata: None, }) .await .expect("cancel should succeed"); @@ -523,16 +512,13 @@ async fn client_supports_push_notification_config_operations() { let client = A2AClient::new(&server.base_url).expect("client should build"); let created = client - .create_task_push_notification_config(CreateTaskPushNotificationConfigRequest { + .create_task_push_notification_config(TaskPushNotificationConfig { task_id: "task-1".to_owned(), - config_id: "cfg-1".to_owned(), - config: PushNotificationConfig { - id: Some("cfg-1".to_owned()), - url: "https://example.com/push".to_owned(), - token: Some("secret".to_owned()), - authentication: None, - }, + id: "cfg-1".to_owned(), tenant: Some("tenant-a".to_owned()), + url: "https://example.com/push".to_owned(), + token: Some("secret".to_owned()), + authentication: None, }) .await .expect("create should succeed"); @@ -545,7 +531,7 @@ async fn client_supports_push_notification_config_operations() { .await .expect("get should succeed"); let listed = client - .list_task_push_notification_config(ListTaskPushNotificationConfigRequest { + .list_task_push_notification_configs(ListTaskPushNotificationConfigsRequest { task_id: "task-1".to_owned(), page_size: Some(10), page_token: None, @@ -564,10 +550,7 @@ async fn client_supports_push_notification_config_operations() { assert_eq!(created.id, "cfg-1"); assert_eq!(created.tenant.as_deref(), Some("tenant-a")); - assert_eq!( - fetched.push_notification_config.url, - "https://example.com/push" - ); + assert_eq!(fetched.url, "https://example.com/push"); assert_eq!(listed.configs.len(), 1); } diff --git a/tests/client_wiremock.rs b/tests/client_wiremock.rs index 9a3a2fb..8b4b74a 100644 --- a/tests/client_wiremock.rs +++ b/tests/client_wiremock.rs @@ -444,7 +444,7 @@ fn send_message_response(text: &str, tenant: Option) -> SendMessageRespo fn task(id: &str) -> Task { Task { id: id.to_owned(), - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::Working, message: None, diff --git a/tests/server_integration.rs b/tests/server_integration.rs index ef3ecd2..9c9be20 100644 --- a/tests/server_integration.rs +++ b/tests/server_integration.rs @@ -87,7 +87,7 @@ impl A2AHandler for TestHandler { async fn get_task(&self, request: GetTaskRequest) -> Result { Ok(Task { id: request.id, - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::Working, message: None, @@ -103,7 +103,7 @@ impl A2AHandler for TestHandler { Ok(ListTasksResponse { tasks: vec![Task { id: "task-1".to_owned(), - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::Submitted, message: None, @@ -191,7 +191,7 @@ impl A2AHandler for StreamingHandler { Ok(Box::pin(stream::iter(vec![ StreamResponse::Task(Task { id: request.id.clone(), - context_id: "ctx-1".to_owned(), + context_id: Some("ctx-1".to_owned()), status: TaskStatus { state: TaskState::Working, message: None, @@ -299,7 +299,7 @@ impl A2AHandler for TenantEchoHandler { async fn get_task(&self, request: GetTaskRequest) -> Result { Ok(Task { id: request.id, - context_id: "ctx-tenant".to_owned(), + context_id: Some("ctx-tenant".to_owned()), status: TaskStatus { state: TaskState::Working, message: None, @@ -315,7 +315,7 @@ impl A2AHandler for TenantEchoHandler { Ok(ListTasksResponse { tasks: vec![Task { id: "tenant-task-1".to_owned(), - context_id: "ctx-tenant".to_owned(), + context_id: Some("ctx-tenant".to_owned()), status: TaskStatus { state: TaskState::Submitted, message: None, @@ -337,7 +337,7 @@ impl A2AHandler for TenantEchoHandler { ) -> Result { Ok(Box::pin(stream::iter(vec![StreamResponse::Task(Task { id: request.id, - context_id: "ctx-tenant".to_owned(), + context_id: Some("ctx-tenant".to_owned()), status: TaskStatus { state: TaskState::Working, message: None, @@ -349,21 +349,18 @@ impl A2AHandler for TenantEchoHandler { })]))) } - async fn list_task_push_notification_config( + async fn list_task_push_notification_configs( &self, - request: a2a_rust::types::ListTaskPushNotificationConfigRequest, - ) -> Result { - Ok(a2a_rust::types::ListTaskPushNotificationConfigResponse { + request: a2a_rust::types::ListTaskPushNotificationConfigsRequest, + ) -> Result { + Ok(a2a_rust::types::ListTaskPushNotificationConfigsResponse { configs: vec![a2a_rust::types::TaskPushNotificationConfig { id: "cfg-1".to_owned(), task_id: request.task_id, - push_notification_config: a2a_rust::types::PushNotificationConfig { - id: Some("cfg-1".to_owned()), - url: "https://example.com/push".to_owned(), - token: None, - authentication: None, - }, tenant: request.tenant, + url: "https://example.com/push".to_owned(), + token: None, + authentication: None, }], next_page_token: String::new(), }) @@ -376,13 +373,10 @@ impl A2AHandler for TenantEchoHandler { Ok(a2a_rust::types::TaskPushNotificationConfig { id: request.id, task_id: request.task_id, - push_notification_config: a2a_rust::types::PushNotificationConfig { - id: Some("cfg-1".to_owned()), - url: "https://example.com/push".to_owned(), - token: None, - authentication: None, - }, tenant: request.tenant, + url: "https://example.com/push".to_owned(), + token: None, + authentication: None, }) } @@ -1010,7 +1004,7 @@ async fn jsonrpc_push_config_returns_not_supported_when_capability_is_disabled() let body = serde_json::json!({ "jsonrpc": "2.0", "id": "req-4", - "method": "ListTaskPushNotificationConfig", + "method": "ListTaskPushNotificationConfigs", "params": { "taskId": "task-1" } diff --git a/tests/spec_examples.rs b/tests/spec_examples.rs index 8fa1760..5afcfff 100644 --- a/tests/spec_examples.rs +++ b/tests/spec_examples.rs @@ -118,7 +118,7 @@ fn send_message_response_deserializes_proto_first_input_required_example() { match response { SendMessageResponse::Task(task) => { assert_eq!(task.id, "task-123"); - assert_eq!(task.context_id, "ctx-123"); + assert_eq!(task.context_id.as_deref(), Some("ctx-123")); assert_eq!(task.status.state, TaskState::InputRequired); } SendMessageResponse::Message(_) => panic!("expected task response"),