diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..4eb10e60b
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,136 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual
+identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall
+ community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or advances of
+ any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email address,
+ without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement:
+
+Farah Juma - fjuma@redhat.com \
+Kabir Khan - kkhan@redhat.com \
+Stefano Maestri - smaestri@redhat.com
+
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
+[https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..479228199
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,91 @@
+Contributing to a2a-java
+==================================
+
+Welcome to the A2A Java SDK project! We welcome contributions from the community. This guide will walk you through the steps for getting started on our project.
+
+- [Forking the Project](#forking-the-project)
+- [Issues](#issues)
+ - [Good First Issues](#good-first-issues)
+- [Setting up your Developer Environment](#setting-up-your-developer-environment)
+- [Contributing Guidelines](#contributing-guidelines)
+- [Community](#community)
+
+
+## Forking the Project
+To contribute, you will first need to fork the [a2a-java](https://github.com/a2aproject/a2a-java) repository.
+
+This can be done by looking in the top-right corner of the repository page and clicking "Fork".
+
+
+The next step is to clone your newly forked repository onto your local workspace. This can be done by going to your newly forked repository, which should be at `https://github.com/USERNAME/a2a-java`.
+
+Then, there will be a green button that says "Code". Click on that and copy the URL.
+
+Then, in your terminal, paste the following command:
+```bash
+git clone [URL]
+```
+Be sure to replace [URL] with the URL that you copied.
+
+Now you have the repository on your computer!
+
+## Issues
+The `a2a-java` project uses GitHub to manage issues. All issues can be found [here](https://github.com/a2aproject/a2a-java/issues).
+
+To create a new issue, comment on an existing issue, or assign an issue to yourself, you'll need to first [create a GitHub account](https://github.com/).
+
+
+### Good First Issues
+Want to contribute to the a2a-java project but aren't quite sure where to start? Check out our issues with the `good-first-issue` label. These are a triaged set of issues that are great for getting started on our project. These can be found [here](https://github.com/a2aproject/a2a-java/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22).
+
+Once you have selected an issue you'd like to work on, make sure it's not already assigned to someone else, and assign it to yourself.
+
+It is recommended that you use a separate branch for every issue you work on. To keep things straightforward and memorable, you can name each branch using the GitHub issue number. This way, you can have multiple PRs open for different issues. For example, if you were working on [issue-20](https://github.com/a2aproject/a2a-java/issues/20), you could use `issue-20` as your branch name.
+
+## Setting up your Developer Environment
+You will need:
+
+* Java 17+
+* Git
+* An IDE (e.g., IntelliJ IDEA, Eclipse, VSCode, etc.)
+
+To set up your development environment you need to:
+
+1. First `cd` to the directory where you cloned the project (eg: `cd a2a-java`)
+
+2. Add a remote ref to upstream, for pulling future updates. For example:
+
+ ```
+ git remote add upstream https://github.com/a2aproject/a2a-java
+ ```
+
+3. To build `a2a-java` and run the tests, use the following command:
+
+ ```
+ mvn clean install
+ ```
+
+4. To skip the tests:
+
+ ```
+ mvn clean install -DskipTests=true
+ ```
+
+## Contributing Guidelines
+
+When submitting a PR, please keep the following guidelines in mind:
+
+1. In general, it's good practice to squash all of your commits into a single commit. For larger changes, it's ok to have multiple meaningful commits. If you need help with squashing your commits, feel free to ask us how to do this on your pull request. We're more than happy to help!
+
+2. Please link the issue you worked on in the description of your pull request and in your commit message. For example, for issue-20, the PR description and commit message could be: ```Add tests to A2AClientTest for sending a task with a FilePart and with a DataPart
+ Fixes #20```
+
+3. Your PR should include tests for the functionality that you are adding.
+
+4. Your PR should include appropriate [documentation](https://github.com/a2aproject/a2a-java/blob/main/README.md) for the functionality that you are adding.
+
+## Code Reviews
+
+All submissions, including submissions by project members, need to be reviewed by at least one `a2a-java` committer before being merged.
+
+The [GitHub Pull Request Review Process](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/about-pull-request-reviews) is followed for every pull request.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..7a4a3ea24
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..b76694f2c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,358 @@
+# A2A Java SDK
+
+[](LICENSE)
+
+
+
+
+
+
+
+ A Java library that helps run agentic applications as A2AServers following the Agent2Agent (A2A) Protocol.
+
+
+## Installation
+
+You can build the A2A Java SDK using `mvn`:
+
+```bash
+mvn clean install
+```
+
+## Examples
+
+You can find an example of how to use the A2A Java SDK [here](https://github.com/fjuma/a2a-samples/tree/java-sdk-example/samples/multi_language/python_and_java_multiagent/weather_agent).
+
+More examples will be added soon.
+
+## A2A Server
+
+The A2A Java SDK provides a Java server implementation of the [Agent2Agent (A2A) Protocol](https://google-a2a.github.io/A2A). To run your agentic Java application as an A2A server, simply follow the steps below.
+
+- [Add the A2A Java SDK Core Maven dependency to your project](#1-add-the-a2a-java-sdk-core-maven-dependency-to-your-project)
+- [Add a class that creates an A2A Agent Card](#2-add-a-class-that-creates-an-a2a-agent-card)
+- [Add a class that creates an A2A Agent Executor](#3-add-a-class-that-creates-an-a2a-agent-executor)
+- [Add an A2A Java SDK Server Maven dependency to your project](#4-add-an-a2a-java-sdk-server-maven-dependency-to-your-project)
+
+### 1. Add the A2A Java SDK Core Maven dependency to your project
+
+> **Note**: The A2A Java SDK isn't available yet in Maven Central but will be soon. For now, be
+> sure to check out the latest tag (you can see the tags [here](https://github.com/a2aproject/a2a-java/tags)), build from the tag, and reference that version below. For example, if the latest tag is `0.2.3`, you can use the following dependency.
+
+```xml
+
+ io.a2a.sdk
+ a2a-java-sdk-core
+ 0.2.3
+
+```
+
+### 2. Add a class that creates an A2A Agent Card
+
+```java
+import io.a2a.server.PublicAgentCard;
+import io.a2a.spec.AgentCapabilities;
+import io.a2a.spec.AgentCard;
+import io.a2a.spec.AgentSkill;
+...
+
+@ApplicationScoped
+public class WeatherAgentCardProducer {
+
+ @Produces
+ @PublicAgentCard
+ public AgentCard agentCard() {
+ return new AgentCard.Builder()
+ .name("Weather Agent")
+ .description("Helps with weather")
+ .url("http://localhost:10001")
+ .version("1.0.0")
+ .capabilities(new AgentCapabilities.Builder()
+ .streaming(true)
+ .pushNotifications(false)
+ .stateTransitionHistory(false)
+ .build())
+ .defaultInputModes(Collections.singletonList("text"))
+ .defaultOutputModes(Collections.singletonList("text"))
+ .skills(Collections.singletonList(new AgentSkill.Builder()
+ .id("weather_search")
+ .name("Search weather")
+ .description("Helps with weather in city, or states")
+ .tags(Collections.singletonList("weather"))
+ .examples(List.of("weather in LA, CA"))
+ .build()))
+ .build();
+ }
+}
+```
+
+### 3. Add a class that creates an A2A Agent Executor
+
+```java
+import io.a2a.server.agentexecution.AgentExecutor;
+import io.a2a.server.agentexecution.RequestContext;
+import io.a2a.server.events.EventQueue;
+import io.a2a.server.tasks.TaskUpdater;
+import io.a2a.spec.JSONRPCError;
+import io.a2a.spec.Message;
+import io.a2a.spec.Part;
+import io.a2a.spec.Task;
+import io.a2a.spec.TaskNotCancelableError;
+import io.a2a.spec.TaskState;
+import io.a2a.spec.TextPart;
+...
+
+@ApplicationScoped
+public class WeatherAgentExecutorProducer {
+
+ @Inject
+ WeatherAgent weatherAgent;
+
+ @Produces
+ public AgentExecutor agentExecutor() {
+ return new WeatherAgentExecutor(weatherAgent);
+ }
+
+ private static class WeatherAgentExecutor implements AgentExecutor {
+
+ private final WeatherAgent weatherAgent;
+
+ public WeatherAgentExecutor(WeatherAgent weatherAgent) {
+ this.weatherAgent = weatherAgent;
+ }
+
+ @Override
+ public void execute(RequestContext context, EventQueue eventQueue) throws JSONRPCError {
+ TaskUpdater updater = new TaskUpdater(context, eventQueue);
+
+ // mark the task as submitted and start working on it
+ if (context.getTask() == null) {
+ updater.submit();
+ }
+ updater.startWork();
+
+ // extract the text from the message
+ String userMessage = extractTextFromMessage(context.getMessage());
+
+ // call the weather agent with the user's message
+ String response = weatherAgent.chat(userMessage);
+
+ // create the response part
+ TextPart responsePart = new TextPart(response, null);
+ List> parts = List.of(responsePart);
+
+ // add the response as an artifact and complete the task
+ updater.addArtifact(parts, null, null, null);
+ updater.complete();
+ }
+
+ @Override
+ public void cancel(RequestContext context, EventQueue eventQueue) throws JSONRPCError {
+ Task task = context.getTask();
+
+ if (task.getStatus().state() == TaskState.CANCELED) {
+ // task already cancelled
+ throw new TaskNotCancelableError();
+ }
+
+ if (task.getStatus().state() == TaskState.COMPLETED) {
+ // task already completed
+ throw new TaskNotCancelableError();
+ }
+
+ // cancel the task
+ TaskUpdater updater = new TaskUpdater(context, eventQueue);
+ updater.cancel();
+ }
+
+ private String extractTextFromMessage(Message message) {
+ StringBuilder textBuilder = new StringBuilder();
+ if (message.getParts() != null) {
+ for (Part part : message.getParts()) {
+ if (part instanceof TextPart textPart) {
+ textBuilder.append(textPart.getText());
+ }
+ }
+ }
+ return textBuilder.toString();
+ }
+ }
+}
+```
+
+### 4. Add an A2A Java SDK Server Maven dependency to your project
+
+> **Note**: The A2A Java SDK isn't available yet in Maven Central but will be soon. For now, be
+> sure to check out the latest tag (you can see the tags [here](https://github.com/a2aproject/a2a-java/tags)), build from the tag, and reference that version below. For example, if the latest tag is `0.2.3`, you can use the following dependency.
+
+Adding a dependency on an A2A Java SDK Server will allow you to run your agentic Java application as an A2A server.
+
+The A2A Java SDK provides two A2A server endpoint implementations, one based on Jakarta REST (`a2a-java-sdk-server-jakarta`) and one based on Quarkus Reactive Routes (`a2a-java-sdk-server-quarkus`). You can choose the one that best fits your application.
+
+Add **one** of the following dependencies to your project:
+
+```xml
+
+ io.a2a.sdk
+ a2a-java-sdk-server-jakarta
+ ${io.a2a.sdk.version}
+
+```
+
+OR
+
+```xml
+
+ io.a2a.sdk
+ a2a-java-sdk-server-quarkus
+ ${io.a2a.sdk.version}
+
+```
+
+## A2A Client
+
+The A2A Java SDK provides a Java client implementation of the [Agent2Agent (A2A) Protocol](https://google-a2a.github.io/A2A), allowing communication with A2A servers.
+
+### Sample Usage
+
+#### Create an A2A client
+
+```java
+// Create an A2AClient (the URL specified is the server agent's URL, be sure to replace it with the actual URL of the A2A server you want to connect to)
+A2AClient client = new A2AClient("http://localhost:1234");
+```
+
+#### Send a message to the A2A server agent
+
+```java
+// Send a text message to the A2A server agent
+Message message = A2A.toUserMessage("tell me a joke"); // the message ID will be automatically generated for you
+MessageSendParams params = new MessageSendParams.Builder()
+ .message(message)
+ .build();
+SendMessageResponse response = client.sendMessage(params);
+```
+
+Note that `A2A#toUserMessage` will automatically generate a message ID for you when creating the `Message`
+if you don't specify it. You can also explicitly specify a message ID like this:
+
+```java
+Message message = A2A.toUserMessage("tell me a joke", "message-1234"); // messageId is message-1234
+```
+
+#### Get the current state of a task
+
+```java
+// Retrieve the task with id "task-1234"
+GetTaskResponse response = client.getTask("task-1234");
+
+// You can also specify the maximum number of items of history for the task
+// to include in the response
+GetTaskResponse response = client.getTask(new TaskQueryParams("task-1234", 10));
+```
+
+#### Cancel an ongoing task
+
+```java
+// Cancel the task we previously submitted with id "task-1234"
+CancelTaskResponse response = client.cancelTask("task-1234");
+
+// You can also specify additional properties using a map
+Map metadata = ...
+CancelTaskResponse response = client.cancelTask(new TaskIdParams("task-1234", metadata));
+```
+
+#### Get the push notification configuration for a task
+
+```java
+// Get task push notification configuration
+GetTaskPushNotificationConfigResponse response = client.getTaskPushNotificationConfig("task-1234");
+
+// You can also specify additional properties using a map
+Map metadata = ...
+GetTaskPushNotificationConfigResponse response = client.getTaskPushNotificationConfig(new TaskIdParams("task-1234", metadata));
+```
+
+#### Set the push notification configuration for a task
+
+```java
+// Set task push notification configuration
+PushNotificationConfig pushNotificationConfig = new PushNotificationConfig.Builder()
+ .url("https://example.com/callback")
+ .authenticationInfo(new AuthenticationInfo(Collections.singletonList("jwt"), null))
+ .build();
+SetTaskPushNotificationResponse response = client.setTaskPushNotificationConfig("task-1234", pushNotificationConfig);
+```
+
+#### Send a streaming message
+
+```java
+// Send a text message to the remote agent
+Message message = A2A.toUserMessage("tell me some jokes"); // the message ID will be automatically generated for you
+MessageSendParams params = new MessageSendParams.Builder()
+ .message(message)
+ .build();
+
+// Create a handler that will be invoked for Task, Message, TaskStatusUpdateEvent, and TaskArtifactUpdateEvent
+Consumer eventHandler = event -> {...};
+
+// Create a handler that will be invoked if an error is received
+Consumer errorHandler = error -> {...};
+
+// Create a handler that will be invoked in the event of a failure
+Runnable failureHandler = () -> {...};
+
+// Send the streaming message to the remote agent
+client.sendStreamingMessage(params, eventHandler, errorHandler, failureHandler);
+```
+
+#### Resubscribe to a task
+
+```java
+// Create a handler that will be invoked for Task, Message, TaskStatusUpdateEvent, and TaskArtifactUpdateEvent
+Consumer eventHandler = event -> {...};
+
+// Create a handler that will be invoked if an error is received
+Consumer errorHandler = error -> {...};
+
+// Create a handler that will be invoked in the event of a failure
+Runnable failureHandler = () -> {...};
+
+// Resubscribe to an ongoing task with id "task-1234"
+TaskIdParams taskIdParams = new TaskIdParams("task-1234");
+client.resubscribeToTask("request-1234", taskIdParams, eventHandler, errorHandler, failureHandler);
+```
+
+#### Retrieve details about the server agent that this client agent is communicating with
+```java
+AgentCard serverAgentCard = client.getAgentCard();
+```
+
+An agent card can also be retrieved using the `A2A#getAgentCard` method:
+```java
+// http://localhost:1234 is the base URL for the agent whose card we want to retrieve
+AgentCard agentCard = A2A.getAgentCard("http://localhost:1234");
+```
+
+## Additional Examples
+
+### Hello World Example
+
+A complete example of an A2A client communicating with a Python A2A server is available in the [examples/helloworld](src/main/java/io/a2a/examples/helloworld) directory. This example demonstrates:
+
+- Setting up and using the A2A Java client
+- Sending regular and streaming messages
+- Receiving and processing responses
+
+The example includes detailed instructions on how to run both the Python server and the Java client using JBang. Check out the [example's README](examples/client/src/main/java/io/a2a/examples/helloworld/README.md) for more information.
+
+## License
+
+This project is licensed under the terms of the [Apache 2.0 License](LICENSE).
+
+## Contributing
+
+See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.
+
+
+
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..36c3d8d4b
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,31 @@
+# Reporting of CVEs and Security Issues
+
+## The A2A Java SDK community takes security bugs very seriously
+
+We aim to take immediate action to address serious security-related problems that involve our project.
+
+Note that we will only fix such issues in the most recent minor release of the A2A Java SDK.
+
+## Reporting of Security Issues
+
+When reporting a security vulnerability it is important to not accidentally broadcast to the world that
+the issue exists, as this makes it easier for people to exploit it. The software industry uses the term
+embargo to describe the time a
+security issue is known internally until it is public knowledge.
+
+Our preferred way of reporting security issues is listed below.
+
+### Email the A2A Java SDK team
+
+To report a security issue, please email fjuma@redhat.com,
+kkhan@redhat.com, and/or smaestri@redhat.com. A member of the team will open the required issues.
+
+### Other considerations
+
+If you would like to work with us on a fix for the security vulnerability, please include your GitHub username
+in the above email, and we will provide you access to a temporary private fork where we can collaborate on a
+fix without it being disclosed publicly, **including in your own publicly visible git repository**.
+
+Do not open a public issue, send a pull request, or disclose any information about the suspected vulnerability
+publicly, **including in your own publicly visible git repository**. If you discover any publicly disclosed
+security vulnerabilities, please notify us immediately through the emails listed in the section above.
\ No newline at end of file
diff --git a/core/pom.xml b/core/pom.xml
new file mode 100644
index 000000000..8aed247f4
--- /dev/null
+++ b/core/pom.xml
@@ -0,0 +1,45 @@
+
+
+ 4.0.0
+
+
+ io.a2a.sdk
+ a2a-java-sdk-parent
+ 0.2.4-SNAPSHOT
+
+ a2a-java-sdk-core
+
+ jar
+
+ Java SDK A2A Core
+ Java SDK for the Agent2Agent Protocol (A2A) - Core
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mock-server
+ mockserver-netty
+ test
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/java/io/a2a/client/A2ACardResolver.java b/core/src/main/java/io/a2a/client/A2ACardResolver.java
new file mode 100644
index 000000000..1266f7219
--- /dev/null
+++ b/core/src/main/java/io/a2a/client/A2ACardResolver.java
@@ -0,0 +1,100 @@
+package io.a2a.client;
+
+import static io.a2a.util.Utils.unmarshalFrom;
+
+import java.io.IOException;
+import java.util.Map;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import io.a2a.http.A2AHttpClient;
+import io.a2a.http.A2AHttpResponse;
+import io.a2a.spec.A2AClientError;
+import io.a2a.spec.A2AClientJSONError;
+import io.a2a.spec.AgentCard;
+
+public class A2ACardResolver {
+ private final A2AHttpClient httpClient;
+ private final String url;
+ private final Map authHeaders;
+
+ static String DEFAULT_AGENT_CARD_PATH = "/.well-known/agent.json";
+
+ static final TypeReference AGENT_CARD_TYPE_REFERENCE = new TypeReference<>() {};
+ /**
+ * @param httpClient the http client to use
+ * @param baseUrl the base URL for the agent whose agent card we want to retrieve
+ */
+ public A2ACardResolver(A2AHttpClient httpClient, String baseUrl) {
+ this(httpClient, baseUrl, null, null);
+ }
+
+ /**
+ * @param httpClient the http client to use
+ * @param baseUrl the base URL for the agent whose agent card we want to retrieve
+ * @param agentCardPath optional path to the agent card endpoint relative to the base
+ * agent URL, defaults to ".well-known/agent.json"
+ */
+ public A2ACardResolver(A2AHttpClient httpClient, String baseUrl, String agentCardPath) {
+ this(httpClient, baseUrl, agentCardPath, null);
+ }
+
+ /**
+ * @param httpClient the http client to use
+ * @param baseUrl the base URL for the agent whose agent card we want to retrieve
+ * @param agentCardPath optional path to the agent card endpoint relative to the base
+ * agent URL, defaults to ".well-known/agent.json"
+ * @param authHeaders the HTTP authentication headers to use. May be {@code null}
+ */
+ public A2ACardResolver(A2AHttpClient httpClient, String baseUrl, String agentCardPath, Map authHeaders) {
+ this.httpClient = httpClient;
+ if (!baseUrl.endsWith("/")) {
+ baseUrl += "/";
+ }
+ agentCardPath = agentCardPath == null || agentCardPath.isEmpty() ? DEFAULT_AGENT_CARD_PATH : agentCardPath;
+ if (agentCardPath.startsWith("/")) {
+ agentCardPath = agentCardPath.substring(1);
+ }
+ this.url = baseUrl + agentCardPath;
+ this.authHeaders = authHeaders;
+ }
+
+ /**
+ * Get the agent card for the configured A2A agent.
+ *
+ * @return the agent card
+ * @throws A2AClientError If an HTTP error occurs fetching the card
+ * @throws A2AClientJSONError f the response body cannot be decoded as JSON or validated against the AgentCard schema
+ */
+ public AgentCard getAgentCard() throws A2AClientError, A2AClientJSONError {
+ A2AHttpClient.GetBuilder builder = httpClient.createGet()
+ .url(url)
+ .addHeader("Content-Type", "application/json");
+
+ if (authHeaders != null) {
+ for (Map.Entry entry : authHeaders.entrySet()) {
+ builder.addHeader(entry.getKey(), entry.getValue());
+ }
+ }
+
+ String body;
+ try {
+ A2AHttpResponse response = builder.get();
+ if (!response.success()) {
+ throw new A2AClientError("Failed to obtain agent card: " + response.status());
+ }
+ body = response.body();
+ } catch (IOException | InterruptedException e) {
+ throw new A2AClientError("Failed to obtain agent card", e);
+ }
+
+ try {
+ return unmarshalFrom(body, AGENT_CARD_TYPE_REFERENCE);
+ } catch (JsonProcessingException e) {
+ throw new A2AClientJSONError("Could not unmarshal agent card response", e);
+ }
+
+ }
+
+
+}
diff --git a/core/src/main/java/io/a2a/client/A2AClient.java b/core/src/main/java/io/a2a/client/A2AClient.java
new file mode 100644
index 000000000..3a35f0b67
--- /dev/null
+++ b/core/src/main/java/io/a2a/client/A2AClient.java
@@ -0,0 +1,518 @@
+package io.a2a.client;
+
+import static io.a2a.spec.A2A.CANCEL_TASK_METHOD;
+import static io.a2a.spec.A2A.GET_TASK_METHOD;
+import static io.a2a.spec.A2A.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
+import static io.a2a.spec.A2A.JSONRPC_VERSION;
+import static io.a2a.spec.A2A.SEND_MESSAGE_METHOD;
+import static io.a2a.spec.A2A.SEND_STREAMING_MESSAGE_METHOD;
+import static io.a2a.spec.A2A.SEND_TASK_RESUBSCRIPTION_METHOD;
+import static io.a2a.spec.A2A.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
+import static io.a2a.util.Assert.checkNotNullParam;
+import static io.a2a.util.Utils.OBJECT_MAPPER;
+import static io.a2a.util.Utils.unmarshalFrom;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import io.a2a.client.sse.SSEEventListener;
+import io.a2a.http.A2AHttpClient;
+import io.a2a.http.A2AHttpResponse;
+import io.a2a.http.JdkA2AHttpClient;
+import io.a2a.spec.A2A;
+import io.a2a.spec.A2AClientError;
+import io.a2a.spec.A2AClientJSONError;
+import io.a2a.spec.A2AServerException;
+import io.a2a.spec.AgentCard;
+import io.a2a.spec.CancelTaskRequest;
+import io.a2a.spec.CancelTaskResponse;
+import io.a2a.spec.GetTaskPushNotificationConfigRequest;
+import io.a2a.spec.GetTaskPushNotificationConfigResponse;
+import io.a2a.spec.GetTaskRequest;
+import io.a2a.spec.GetTaskResponse;
+import io.a2a.spec.JSONRPCError;
+import io.a2a.spec.JSONRPCResponse;
+import io.a2a.spec.MessageSendParams;
+import io.a2a.spec.PushNotificationConfig;
+import io.a2a.spec.SendMessageRequest;
+import io.a2a.spec.SendMessageResponse;
+import io.a2a.spec.SendStreamingMessageRequest;
+import io.a2a.spec.SetTaskPushNotificationConfigRequest;
+import io.a2a.spec.SetTaskPushNotificationConfigResponse;
+import io.a2a.spec.StreamingEventKind;
+import io.a2a.spec.TaskIdParams;
+import io.a2a.spec.TaskPushNotificationConfig;
+import io.a2a.spec.TaskQueryParams;
+import io.a2a.spec.TaskResubscriptionRequest;
+
+/**
+ * An A2A client.
+ */
+public class A2AClient {
+
+ private static final TypeReference SEND_MESSAGE_RESPONSE_REFERENCE = new TypeReference<>() {};
+ private static final TypeReference GET_TASK_RESPONSE_REFERENCE = new TypeReference<>() {};
+ private static final TypeReference CANCEL_TASK_RESPONSE_REFERENCE = new TypeReference<>() {};
+ private static final TypeReference GET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = new TypeReference<>() {};
+ private static final TypeReference SET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = new TypeReference<>() {};
+ private final A2AHttpClient httpClient;
+ private final String agentUrl;
+ private AgentCard agentCard;
+
+
+ /**
+ * Create a new A2AClient.
+ *
+ * @param agentCard the agent card for the A2A server this client will be communicating with
+ */
+ public A2AClient(AgentCard agentCard) {
+ checkNotNullParam("agentCard", agentCard);
+ this.agentCard = agentCard;
+ this.agentUrl = agentCard.url();
+ this.httpClient = new JdkA2AHttpClient();
+ }
+
+ /**
+ * Create a new A2AClient.
+ *
+ * @param agentUrl the URL for the A2A server this client will be communicating with
+ */
+ public A2AClient(String agentUrl) {
+ checkNotNullParam("agentUrl", agentUrl);
+ this.agentUrl = agentUrl;
+ this.httpClient = new JdkA2AHttpClient();
+ }
+
+ /**
+ * Fetches the agent card and initialises an A2A client.
+ *
+ * @param httpClient the {@link A2AHttpClient} to use
+ * @param baseUrl the base URL of the agent's host
+ * @param agentCardPath the path to the agent card endpoint, relative to the {@code baseUrl}. If {@code null}, the
+ * value {@link A2ACardResolver#DEFAULT_AGENT_CARD_PATH} will be used
+ * @return an initialised {@code A2AClient} instance
+ * @throws A2AClientError If an HTTP error occurs fetching the card
+ * @throws A2AClientJSONError if the agent card response is invalid
+ */
+ public static A2AClient getClientFromAgentCardUrl(A2AHttpClient httpClient, String baseUrl,
+ String agentCardPath) throws A2AClientError, A2AClientJSONError {
+ A2ACardResolver resolver = new A2ACardResolver(httpClient, baseUrl, agentCardPath);
+ AgentCard card = resolver.getAgentCard();
+ return new A2AClient(card);
+ }
+
+ /**
+ * Get the agent card for the A2A server this client will be communicating with from
+ * the default public agent card endpoint.
+ *
+ * @return the agent card for the A2A server
+ * @throws A2AClientError If an HTTP error occurs fetching the card
+ * @throws A2AClientJSONError f the response body cannot be decoded as JSON or validated against the AgentCard schema
+ */
+ public AgentCard getAgentCard() throws A2AClientError, A2AClientJSONError {
+ if (this.agentCard == null) {
+ this.agentCard = A2A.getAgentCard(this.httpClient, this.agentUrl);
+ }
+ return this.agentCard;
+ }
+
+ /**
+ * Get the agent card for the A2A server this client will be communicating with.
+ *
+ * @param relativeCardPath the path to the agent card endpoint relative to the base URL of the A2A server
+ * @param authHeaders the HTTP authentication headers to use
+ * @return the agent card for the A2A server
+ * @throws A2AClientError If an HTTP error occurs fetching the card
+ * @throws A2AClientJSONError f the response body cannot be decoded as JSON or validated against the AgentCard schema
+ */
+ public AgentCard getAgentCard(String relativeCardPath, Map authHeaders) throws A2AClientError, A2AClientJSONError {
+ if (this.agentCard == null) {
+ this.agentCard = A2A.getAgentCard(this.httpClient, this.agentUrl, relativeCardPath, authHeaders);
+ }
+ return this.agentCard;
+ }
+
+ /**
+ * Send a message to the remote agent.
+ *
+ * @param messageSendParams the parameters for the message to be sent
+ * @return the response, may contain a message or a task
+ * @throws A2AServerException if sending the message fails for any reason
+ */
+ public SendMessageResponse sendMessage(MessageSendParams messageSendParams) throws A2AServerException {
+ return sendMessage(null, messageSendParams);
+ }
+
+ /**
+ * Send a message to the remote agent.
+ *
+ * @param requestId the request ID to use
+ * @param messageSendParams the parameters for the message to be sent
+ * @return the response, may contain a message or a task
+ * @throws A2AServerException if sending the message fails for any reason
+ */
+ public SendMessageResponse sendMessage(String requestId, MessageSendParams messageSendParams) throws A2AServerException {
+ SendMessageRequest.Builder sendMessageRequestBuilder = new SendMessageRequest.Builder()
+ .jsonrpc(JSONRPC_VERSION)
+ .method(SEND_MESSAGE_METHOD)
+ .params(messageSendParams);
+
+ if (requestId != null) {
+ sendMessageRequestBuilder.id(requestId);
+ }
+
+ SendMessageRequest sendMessageRequest = sendMessageRequestBuilder.build();
+
+ try {
+ String httpResponseBody = sendPostRequest(sendMessageRequest);
+ return unmarshalResponse(httpResponseBody, SEND_MESSAGE_RESPONSE_REFERENCE);
+ } catch (IOException | InterruptedException e) {
+ throw new A2AServerException("Failed to send message: " + e);
+ }
+ }
+
+ /**
+ * Retrieve a task from the A2A server. This method can be used to retrieve the generated
+ * artifacts for a task.
+ *
+ * @param id the task ID
+ * @return the response containing the task
+ * @throws A2AServerException if retrieving the task fails for any reason
+ */
+ public GetTaskResponse getTask(String id) throws A2AServerException {
+ return getTask(null, new TaskQueryParams(id));
+ }
+
+ /**
+ * Retrieve a task from the A2A server. This method can be used to retrieve the generated
+ * artifacts for a task.
+ *
+ * @param taskQueryParams the params for the task to be queried
+ * @return the response containing the task
+ * @throws A2AServerException if retrieving the task fails for any reason
+ */
+ public GetTaskResponse getTask(TaskQueryParams taskQueryParams) throws A2AServerException {
+ return getTask(null, taskQueryParams);
+ }
+
+ /**
+ * Retrieve the generated artifacts for a task.
+ *
+ * @param requestId the request ID to use
+ * @param taskQueryParams the params for the task to be queried
+ * @return the response containing the task
+ * @throws A2AServerException if retrieving the task fails for any reason
+ */
+ public GetTaskResponse getTask(String requestId, TaskQueryParams taskQueryParams) throws A2AServerException {
+ GetTaskRequest.Builder getTaskRequestBuilder = new GetTaskRequest.Builder()
+ .jsonrpc(JSONRPC_VERSION)
+ .method(GET_TASK_METHOD)
+ .params(taskQueryParams);
+
+ if (requestId != null) {
+ getTaskRequestBuilder.id(requestId);
+ }
+
+ GetTaskRequest getTaskRequest = getTaskRequestBuilder.build();
+
+ try {
+ String httpResponseBody = sendPostRequest(getTaskRequest);
+ return unmarshalResponse(httpResponseBody, GET_TASK_RESPONSE_REFERENCE);
+ } catch (IOException | InterruptedException e) {
+ throw new A2AServerException("Failed to get task: " + e);
+ }
+ }
+
+ /**
+ * Cancel a task that was previously submitted to the A2A server.
+ *
+ * @param id the task ID
+ * @return the response indicating if the task was cancelled
+ * @throws A2AServerException if cancelling the task fails for any reason
+ */
+ public CancelTaskResponse cancelTask(String id) throws A2AServerException {
+ return cancelTask(null, new TaskIdParams(id));
+ }
+
+ /**
+ * Cancel a task that was previously submitted to the A2A server.
+ *
+ * @param taskIdParams the params for the task to be cancelled
+ * @return the response indicating if the task was cancelled
+ * @throws A2AServerException if cancelling the task fails for any reason
+ */
+ public CancelTaskResponse cancelTask(TaskIdParams taskIdParams) throws A2AServerException {
+ return cancelTask(null, taskIdParams);
+ }
+
+ /**
+ * Cancel a task that was previously submitted to the A2A server.
+ *
+ * @param requestId the request ID to use
+ * @param taskIdParams the params for the task to be cancelled
+ * @return the response indicating if the task was cancelled
+ * @throws A2AServerException if retrieving the task fails for any reason
+ */
+ public CancelTaskResponse cancelTask(String requestId, TaskIdParams taskIdParams) throws A2AServerException {
+ CancelTaskRequest.Builder cancelTaskRequestBuilder = new CancelTaskRequest.Builder()
+ .jsonrpc(JSONRPC_VERSION)
+ .method(CANCEL_TASK_METHOD)
+ .params(taskIdParams);
+
+ if (requestId != null) {
+ cancelTaskRequestBuilder.id(requestId);
+ }
+
+ CancelTaskRequest cancelTaskRequest = cancelTaskRequestBuilder.build();
+
+ try {
+ String httpResponseBody = sendPostRequest(cancelTaskRequest);
+ return unmarshalResponse(httpResponseBody, CANCEL_TASK_RESPONSE_REFERENCE);
+ } catch (IOException | InterruptedException e) {
+ throw new A2AServerException("Failed to cancel task: " + e);
+ }
+ }
+
+ /**
+ * Get the push notification configuration for a task.
+ *
+ * @param id the task ID
+ * @return the response containing the push notification configuration
+ * @throws A2AServerException if getting the push notification configuration fails for any reason
+ */
+ public GetTaskPushNotificationConfigResponse getTaskPushNotificationConfig(String id) throws A2AServerException {
+ return getTaskPushNotificationConfig(null, new TaskIdParams(id));
+ }
+
+ /**
+ * Get the push notification configuration for a task.
+ *
+ * @param taskIdParams the params for the task
+ * @return the response containing the push notification configuration
+ * @throws A2AServerException if getting the push notification configuration fails for any reason
+ */
+ public GetTaskPushNotificationConfigResponse getTaskPushNotificationConfig(TaskIdParams taskIdParams) throws A2AServerException {
+ return getTaskPushNotificationConfig(null, taskIdParams);
+ }
+
+ /**
+ * Get the push notification configuration for a task.
+ *
+ * @param requestId the request ID to use
+ * @param taskIdParams the params for the task
+ * @return the response containing the push notification configuration
+ * @throws A2AServerException if getting the push notification configuration fails for any reason
+ */
+ public GetTaskPushNotificationConfigResponse getTaskPushNotificationConfig(String requestId, TaskIdParams taskIdParams) throws A2AServerException {
+ GetTaskPushNotificationConfigRequest.Builder getTaskPushNotificationRequestBuilder = new GetTaskPushNotificationConfigRequest.Builder()
+ .jsonrpc(JSONRPC_VERSION)
+ .method(GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD)
+ .params(taskIdParams);
+
+ if (requestId != null) {
+ getTaskPushNotificationRequestBuilder.id(requestId);
+ }
+
+ GetTaskPushNotificationConfigRequest getTaskPushNotificationRequest = getTaskPushNotificationRequestBuilder.build();
+
+ try {
+ String httpResponseBody = sendPostRequest(getTaskPushNotificationRequest);
+ return unmarshalResponse(httpResponseBody, GET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE);
+ } catch (IOException | InterruptedException e) {
+ throw new A2AServerException("Failed to get task push notification config: " + e);
+ }
+ }
+
+ /**
+ * Set push notification configuration for a task.
+ *
+ * @param taskId the task ID
+ * @param pushNotificationConfig the push notification configuration
+ * @return the response indicating whether setting the task push notification configuration succeeded
+ * @throws A2AServerException if setting the push notification configuration fails for any reason
+ */
+ public SetTaskPushNotificationConfigResponse setTaskPushNotificationConfig(String taskId,
+ PushNotificationConfig pushNotificationConfig) throws A2AServerException {
+ return setTaskPushNotificationConfig(null, taskId, pushNotificationConfig);
+ }
+
+ /**
+ * Set push notification configuration for a task.
+ *
+ * @param requestId the request ID to use
+ * @param taskId the task ID
+ * @param pushNotificationConfig the push notification configuration
+ * @return the response indicating whether setting the task push notification configuration succeeded
+ * @throws A2AServerException if setting the push notification configuration fails for any reason
+ */
+ public SetTaskPushNotificationConfigResponse setTaskPushNotificationConfig(String requestId, String taskId,
+ PushNotificationConfig pushNotificationConfig) throws A2AServerException {
+ SetTaskPushNotificationConfigRequest.Builder setTaskPushNotificationRequestBuilder = new SetTaskPushNotificationConfigRequest.Builder()
+ .jsonrpc(JSONRPC_VERSION)
+ .method(SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD)
+ .params(new TaskPushNotificationConfig(taskId, pushNotificationConfig));
+
+ if (requestId != null) {
+ setTaskPushNotificationRequestBuilder.id(requestId);
+ }
+
+ SetTaskPushNotificationConfigRequest setTaskPushNotificationRequest = setTaskPushNotificationRequestBuilder.build();
+
+ try {
+ String httpResponseBody = sendPostRequest(setTaskPushNotificationRequest);
+ return unmarshalResponse(httpResponseBody, SET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE);
+ } catch (IOException | InterruptedException e) {
+ throw new A2AServerException("Failed to set task push notification config: " + e);
+ }
+ }
+
+ /**
+ * Send a streaming message to the remote agent.
+ *
+ * @param messageSendParams the parameters for the message to be sent
+ * @param eventHandler a consumer that will be invoked for each event received from the remote agent
+ * @param errorHandler a consumer that will be invoked if the remote agent returns an error
+ * @param failureHandler a consumer that will be invoked if a failure occurs when processing events
+ * @throws A2AServerException if sending the streaming message fails for any reason
+ */
+ public void sendStreamingMessage(MessageSendParams messageSendParams, Consumer eventHandler,
+ Consumer errorHandler, Runnable failureHandler) throws A2AServerException {
+ sendStreamingMessage(null, messageSendParams, eventHandler, errorHandler, failureHandler);
+ }
+
+ /**
+ * Send a streaming message to the remote agent.
+ *
+ * @param requestId the request ID to use
+ * @param messageSendParams the parameters for the message to be sent
+ * @param eventHandler a consumer that will be invoked for each event received from the remote agent
+ * @param errorHandler a consumer that will be invoked if the remote agent returns an error
+ * @param failureHandler a consumer that will be invoked if a failure occurs when processing events
+ * @throws A2AServerException if sending the streaming message fails for any reason
+ */
+ public void sendStreamingMessage(String requestId, MessageSendParams messageSendParams, Consumer eventHandler,
+ Consumer errorHandler, Runnable failureHandler) throws A2AServerException {
+ checkNotNullParam("messageSendParams", messageSendParams);
+ checkNotNullParam("eventHandler", eventHandler);
+ checkNotNullParam("errorHandler", errorHandler);
+ checkNotNullParam("failureHandler", failureHandler);
+
+ SendStreamingMessageRequest.Builder sendStreamingMessageRequestBuilder = new SendStreamingMessageRequest.Builder()
+ .jsonrpc(JSONRPC_VERSION)
+ .method(SEND_STREAMING_MESSAGE_METHOD)
+ .params(messageSendParams);
+
+ if (requestId != null) {
+ sendStreamingMessageRequestBuilder.id(requestId);
+ }
+
+ AtomicReference> ref = new AtomicReference<>();
+ SSEEventListener sseEventListener = new SSEEventListener(eventHandler, errorHandler, failureHandler);
+ SendStreamingMessageRequest sendStreamingMessageRequest = sendStreamingMessageRequestBuilder.build();
+ try {
+ A2AHttpClient.PostBuilder builder = createPostBuilder(sendStreamingMessageRequest);
+ ref.set(builder.postAsyncSSE(
+ msg -> sseEventListener.onMessage(msg, ref.get()),
+ throwable -> sseEventListener.onError(throwable, ref.get()),
+ () -> {
+ // We don't need to do anything special on completion
+ }));
+
+ } catch (IOException e) {
+ throw new A2AServerException("Failed to send streaming message request: " + e);
+ } catch (InterruptedException e) {
+ throw new A2AServerException("Send streaming message request timed out: " + e);
+ }
+ }
+
+ /**
+ * Resubscribe to an ongoing task.
+ *
+ * @param taskIdParams the params for the task to resubscribe to
+ * @param eventHandler a consumer that will be invoked for each event received from the remote agent
+ * @param errorHandler a consumer that will be invoked if the remote agent returns an error
+ * @param failureHandler a consumer that will be invoked if a failure occurs when processing events
+ * @throws A2AServerException if resubscribing to the task fails for any reason
+ */
+ public void resubscribeToTask(TaskIdParams taskIdParams, Consumer eventHandler,
+ Consumer errorHandler, Runnable failureHandler) throws A2AServerException {
+ resubscribeToTask(null, taskIdParams, eventHandler, errorHandler, failureHandler);
+ }
+
+ /**
+ * Resubscribe to an ongoing task.
+ *
+ * @param requestId the request ID to use
+ * @param taskIdParams the params for the task to resubscribe to
+ * @param eventHandler a consumer that will be invoked for each event received from the remote agent
+ * @param errorHandler a consumer that will be invoked if the remote agent returns an error
+ * @param failureHandler a consumer that will be invoked if a failure occurs when processing events
+ * @throws A2AServerException if resubscribing to the task fails for any reason
+ */
+ public void resubscribeToTask(String requestId, TaskIdParams taskIdParams, Consumer eventHandler,
+ Consumer errorHandler, Runnable failureHandler) throws A2AServerException {
+ checkNotNullParam("taskIdParams", taskIdParams);
+ checkNotNullParam("eventHandler", eventHandler);
+ checkNotNullParam("errorHandler", errorHandler);
+ checkNotNullParam("failureHandler", failureHandler);
+
+ TaskResubscriptionRequest.Builder taskResubscriptionRequestBuilder = new TaskResubscriptionRequest.Builder()
+ .jsonrpc(JSONRPC_VERSION)
+ .method(SEND_TASK_RESUBSCRIPTION_METHOD)
+ .params(taskIdParams);
+
+ if (requestId != null) {
+ taskResubscriptionRequestBuilder.id(requestId);
+ }
+
+ AtomicReference> ref = new AtomicReference<>();
+ SSEEventListener sseEventListener = new SSEEventListener(eventHandler, errorHandler, failureHandler);
+ TaskResubscriptionRequest taskResubscriptionRequest = taskResubscriptionRequestBuilder.build();
+ try {
+ A2AHttpClient.PostBuilder builder = createPostBuilder(taskResubscriptionRequest);
+ ref.set(builder.postAsyncSSE(
+ msg -> sseEventListener.onMessage(msg, ref.get()),
+ throwable -> sseEventListener.onError(throwable, ref.get()),
+ () -> {
+ // We don't need to do anything special on completion
+ }));
+
+ } catch (IOException e) {
+ throw new A2AServerException("Failed to send task resubscription request: " + e);
+ } catch (InterruptedException e) {
+ throw new A2AServerException("Task resubscription request timed out: " + e);
+ }
+ }
+
+ private String sendPostRequest(Object value) throws IOException, InterruptedException {
+ A2AHttpClient.PostBuilder builder = createPostBuilder(value);
+ A2AHttpResponse response = builder.post();
+ if (!response.success()) {
+ throw new IOException("Request failed " + response.status());
+ }
+ return response.body();
+ }
+
+ private A2AHttpClient.PostBuilder createPostBuilder(Object value) throws JsonProcessingException {
+ return httpClient.createPost()
+ .url(agentUrl)
+ .addHeader("Content-Type", "application/json")
+ .body(OBJECT_MAPPER.writeValueAsString(value));
+
+ }
+
+ private T unmarshalResponse(String response, TypeReference typeReference)
+ throws A2AServerException, JsonProcessingException {
+ T value = unmarshalFrom(response, typeReference);
+ JSONRPCError error = value.getError();
+ if (error != null) {
+ throw new A2AServerException(error.getMessage() + (error.getData() != null ? ": " + error.getData() : ""));
+ }
+ return value;
+ }
+}
diff --git a/core/src/main/java/io/a2a/client/sse/SSEEventListener.java b/core/src/main/java/io/a2a/client/sse/SSEEventListener.java
new file mode 100644
index 000000000..8ed0e9aa3
--- /dev/null
+++ b/core/src/main/java/io/a2a/client/sse/SSEEventListener.java
@@ -0,0 +1,61 @@
+package io.a2a.client.sse;
+
+import static io.a2a.util.Utils.OBJECT_MAPPER;
+
+import java.util.concurrent.Future;
+import java.util.function.Consumer;
+import java.util.logging.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.a2a.spec.JSONRPCError;
+import io.a2a.spec.StreamingEventKind;
+import io.a2a.spec.TaskStatusUpdateEvent;
+
+public class SSEEventListener {
+ private static final Logger log = Logger.getLogger(SSEEventListener.class.getName());
+ private final Consumer eventHandler;
+ private final Consumer errorHandler;
+ private final Runnable failureHandler;
+
+ public SSEEventListener(Consumer eventHandler, Consumer errorHandler, Runnable failureHandler) {
+ this.eventHandler = eventHandler;
+ this.errorHandler = errorHandler;
+ this.failureHandler = failureHandler;
+ }
+
+ public void onMessage(String message, Future completableFuture) {
+ try {
+ handleMessage(OBJECT_MAPPER.readTree(message),completableFuture);
+ } catch (JsonProcessingException e) {
+ log.warning("Failed to parse JSON message: " + message);
+ }
+ }
+
+ public void onError(Throwable throwable, Future future) {
+ failureHandler.run();
+ future.cancel(true); // close SSE channel
+ }
+
+ private void handleMessage(JsonNode jsonNode, Future future) {
+ try {
+ if (jsonNode.has("error")) {
+ JSONRPCError error = OBJECT_MAPPER.treeToValue(jsonNode.get("error"), JSONRPCError.class);
+ errorHandler.accept(error);
+ } else if (jsonNode.has("result")) {
+ // result can be a Task, Message, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent
+ JsonNode result = jsonNode.path("result");
+ StreamingEventKind event = OBJECT_MAPPER.treeToValue(result, StreamingEventKind.class);
+ eventHandler.accept(event);
+ if (event instanceof TaskStatusUpdateEvent && ((TaskStatusUpdateEvent) event).isFinal()) {
+ future.cancel(true); // close SSE channel
+ }
+ } else {
+ throw new IllegalArgumentException("Unknown message type");
+ }
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/core/src/main/java/io/a2a/http/A2AHttpClient.java b/core/src/main/java/io/a2a/http/A2AHttpClient.java
new file mode 100644
index 000000000..7a246843a
--- /dev/null
+++ b/core/src/main/java/io/a2a/http/A2AHttpClient.java
@@ -0,0 +1,34 @@
+package io.a2a.http;
+
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+public interface A2AHttpClient {
+
+ GetBuilder createGet();
+
+ PostBuilder createPost();
+
+ interface Builder> {
+ T url(String s);
+ T addHeader(String name, String value);
+ }
+
+ interface GetBuilder extends Builder {
+ A2AHttpResponse get() throws IOException, InterruptedException;
+ CompletableFuture getAsyncSSE(
+ Consumer messageConsumer,
+ Consumer errorConsumer,
+ Runnable completeRunnable) throws IOException, InterruptedException;
+ }
+
+ interface PostBuilder extends Builder {
+ PostBuilder body(String body);
+ A2AHttpResponse post() throws IOException, InterruptedException;
+ CompletableFuture postAsyncSSE(
+ Consumer messageConsumer,
+ Consumer errorConsumer,
+ Runnable completeRunnable) throws IOException, InterruptedException;
+ }
+}
diff --git a/core/src/main/java/io/a2a/http/A2AHttpResponse.java b/core/src/main/java/io/a2a/http/A2AHttpResponse.java
new file mode 100644
index 000000000..d6973a5dc
--- /dev/null
+++ b/core/src/main/java/io/a2a/http/A2AHttpResponse.java
@@ -0,0 +1,9 @@
+package io.a2a.http;
+
+public interface A2AHttpResponse {
+ int status();
+
+ boolean success();
+
+ String body();
+}
diff --git a/core/src/main/java/io/a2a/http/JdkA2AHttpClient.java b/core/src/main/java/io/a2a/http/JdkA2AHttpClient.java
new file mode 100644
index 000000000..e3b5c0c66
--- /dev/null
+++ b/core/src/main/java/io/a2a/http/JdkA2AHttpClient.java
@@ -0,0 +1,210 @@
+package io.a2a.http;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandler;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Flow;
+import java.util.function.Consumer;
+
+public class JdkA2AHttpClient implements A2AHttpClient {
+
+ private final HttpClient httpClient;
+
+ public JdkA2AHttpClient() {
+ httpClient = HttpClient.newBuilder()
+ .version(HttpClient.Version.HTTP_2)
+ .followRedirects(HttpClient.Redirect.NORMAL)
+ .build();
+ }
+
+ @Override
+ public GetBuilder createGet() {
+ return new JdkGetBuilder();
+ }
+
+ @Override
+ public PostBuilder createPost() {
+ return new JdkPostBuilder();
+ }
+
+ private abstract class JdkBuilder> implements Builder {
+ private String url;
+ private Map headers = new HashMap<>();
+
+ @Override
+ public T url(String url) {
+ this.url = url;
+ return self();
+ }
+
+ @Override
+ public T addHeader(String name, String value) {
+ headers.put(name, value);
+ return self();
+ }
+
+ @SuppressWarnings("unchecked")
+ T self() {
+ return (T) this;
+ }
+
+ protected HttpRequest.Builder createRequestBuilder() throws IOException {
+ HttpRequest.Builder builder = HttpRequest.newBuilder()
+ .uri(URI.create(url));
+ for (Map.Entry headerEntry : headers.entrySet()) {
+ builder.header(headerEntry.getKey(), headerEntry.getValue());
+ }
+ return builder;
+ }
+
+ protected CompletableFuture asyncRequest(
+ HttpRequest request,
+ Consumer messageConsumer,
+ Consumer errorConsumer,
+ Runnable completeRunnable
+ ) {
+ Flow.Subscriber subscriber = new Flow.Subscriber() {
+ private Flow.Subscription subscription;
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ this.subscription = subscription;
+ subscription.request(1);
+ }
+
+ @Override
+ public void onNext(String item) {
+ // SSE messages sometimes start with "data:". Strip that off
+ if (item != null && item.startsWith("data:")) {
+ item = item.substring(5).trim();
+ if (!item.isEmpty()) {
+ messageConsumer.accept(item);
+ }
+ }
+ subscription.request(1);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ errorConsumer.accept(throwable);
+ subscription.cancel();
+ }
+
+ @Override
+ public void onComplete() {
+ completeRunnable.run();
+ subscription.cancel();
+ }
+ };
+
+ BodyHandler bodyHandler = BodyHandlers.fromLineSubscriber(subscriber);
+
+ // Send the response async, and let the subscriber handle the lines.
+ return httpClient.sendAsync(request, bodyHandler)
+ .thenAccept(response -> {
+ if (!JdkHttpResponse.success(response)) {
+ subscriber.onError(new IOException("Request failed " + response.statusCode()));
+ }
+ });
+ }
+ }
+
+ private class JdkGetBuilder extends JdkBuilder implements A2AHttpClient.GetBuilder {
+
+ private HttpRequest.Builder createRequestBuilder(boolean SSE) throws IOException {
+ HttpRequest.Builder builder = super.createRequestBuilder().GET();
+ if (SSE) {
+ builder.header("Accept", "text/event-stream");
+ }
+ return builder;
+ }
+
+ @Override
+ public A2AHttpResponse get() throws IOException, InterruptedException {
+ HttpRequest request = createRequestBuilder(false)
+ .build();
+ HttpResponse response =
+ httpClient.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8));
+ return new JdkHttpResponse(response);
+ }
+
+ @Override
+ public CompletableFuture getAsyncSSE(
+ Consumer messageConsumer,
+ Consumer errorConsumer,
+ Runnable completeRunnable) throws IOException, InterruptedException {
+ HttpRequest request = createRequestBuilder(false)
+ .build();
+ return super.asyncRequest(request, messageConsumer, errorConsumer, completeRunnable);
+ }
+ }
+
+ private class JdkPostBuilder extends JdkBuilder implements A2AHttpClient.PostBuilder {
+ String body = "";
+
+ @Override
+ public PostBuilder body(String body) {
+ this.body = body;
+ return self();
+ }
+
+ private HttpRequest.Builder createRequestBuilder(boolean SSE) throws IOException {
+ HttpRequest.Builder builder = super.createRequestBuilder()
+ .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8));
+ if (SSE) {
+ builder.header("Accept", "text/event-stream");
+ }
+ return builder;
+ }
+
+ @Override
+ public A2AHttpResponse post() throws IOException, InterruptedException {
+ HttpRequest request = createRequestBuilder(false)
+ .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
+ .build();
+ HttpResponse response =
+ httpClient.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8));
+ return new JdkHttpResponse(response);
+ }
+
+ @Override
+ public CompletableFuture postAsyncSSE(
+ Consumer messageConsumer,
+ Consumer errorConsumer,
+ Runnable completeRunnable) throws IOException, InterruptedException {
+ HttpRequest request = createRequestBuilder(false)
+ .build();
+ return super.asyncRequest(request, messageConsumer, errorConsumer, completeRunnable);
+ }
+ }
+
+ private record JdkHttpResponse(HttpResponse response) implements A2AHttpResponse {
+
+ @Override
+ public int status() {
+ return response.statusCode();
+ }
+
+ @Override
+ public boolean success() {// Send the request and get the response
+ return success(response);
+ }
+
+ static boolean success(HttpResponse> response) {
+ return response.statusCode() >= 200 && response.statusCode() < 300;
+ }
+
+ @Override
+ public String body() {
+ return response.body();
+ }
+ }
+}
diff --git a/core/src/main/java/io/a2a/spec/A2A.java b/core/src/main/java/io/a2a/spec/A2A.java
new file mode 100644
index 000000000..4f3d2381c
--- /dev/null
+++ b/core/src/main/java/io/a2a/spec/A2A.java
@@ -0,0 +1,148 @@
+package io.a2a.spec;
+
+import java.util.Collections;
+import java.util.Map;
+
+import io.a2a.client.A2ACardResolver;
+import io.a2a.http.A2AHttpClient;
+import io.a2a.http.JdkA2AHttpClient;
+
+
+/**
+ * Constants and utility methods related to the A2A protocol.
+ */
+public class A2A {
+
+ public static final String CANCEL_TASK_METHOD = "tasks/cancel";
+ public static final String GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD = "tasks/pushNotificationConfig/get";
+ public static final String GET_TASK_METHOD = "tasks/get";
+ public static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD = "tasks/pushNotificationConfig/set";
+ public static final String SEND_TASK_RESUBSCRIPTION_METHOD = "tasks/resubscribe";
+ public static final String SEND_STREAMING_MESSAGE_METHOD = "message/stream";
+ public static final String SEND_MESSAGE_METHOD = "message/send";
+
+ public static final String JSONRPC_VERSION = "2.0";
+
+
+ /**
+ * Convert the given text to a user message.
+ *
+ * @param text the message text
+ * @return the user message
+ */
+ public static Message toUserMessage(String text) {
+ return toMessage(text, Message.Role.USER, null);
+ }
+
+ /**
+ * Convert the given text to a user message.
+ *
+ * @param text the message text
+ * @param messageId the message ID to use
+ * @return the user message
+ */
+ public static Message toUserMessage(String text, String messageId) {
+ return toMessage(text, Message.Role.USER, messageId);
+ }
+
+ /**
+ * Convert the given text to an agent message.
+ *
+ * @param text the message text
+ * @return the agent message
+ */
+ public static Message toAgentMessage(String text) {
+ return toMessage(text, Message.Role.AGENT, null);
+ }
+
+ /**
+ * Convert the given text to an agent message.
+ *
+ * @param text the message text
+ * @param messageId the message ID to use
+ * @return the agent message
+ */
+ public static Message toAgentMessage(String text, String messageId) {
+ return toMessage(text, Message.Role.AGENT, messageId);
+ }
+
+
+ private static Message toMessage(String text, Message.Role role, String messageId) {
+ Message.Builder messageBuilder = new Message.Builder()
+ .role(role)
+ .parts(Collections.singletonList(new TextPart(text)));
+ if (messageId != null) {
+ messageBuilder.messageId(messageId);
+ }
+ return messageBuilder.build();
+ }
+
+ /**
+ * Get the agent card for an A2A agent.
+ *
+ * @param agentUrl the base URL for the agent whose agent card we want to retrieve
+ * @return the agent card
+ * @throws A2AClientError If an HTTP error occurs fetching the card
+ * @throws A2AClientJSONError f the response body cannot be decoded as JSON or validated against the AgentCard schema
+ */
+ public static AgentCard getAgentCard(String agentUrl) throws A2AClientError, A2AClientJSONError {
+ return getAgentCard(new JdkA2AHttpClient(), agentUrl);
+ }
+
+ /**
+ * Get the agent card for an A2A agent.
+ *
+ * @param httpClient the http client to use
+ * @param agentUrl the base URL for the agent whose agent card we want to retrieve
+ * @return the agent card
+ * @throws A2AClientError If an HTTP error occurs fetching the card
+ * @throws A2AClientJSONError f the response body cannot be decoded as JSON or validated against the AgentCard schema
+ */
+ public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl) throws A2AClientError, A2AClientJSONError {
+ return getAgentCard(httpClient, agentUrl, null, null);
+ }
+
+ /**
+ * Get the agent card for an A2A agent.
+ *
+ * @param agentUrl the base URL for the agent whose agent card we want to retrieve
+ * @param relativeCardPath optional path to the agent card endpoint relative to the base
+ * agent URL, defaults to ".well-known/agent.json"
+ * @param authHeaders the HTTP authentication headers to use
+ * @return the agent card
+ * @throws A2AClientError If an HTTP error occurs fetching the card
+ * @throws A2AClientJSONError f the response body cannot be decoded as JSON or validated against the AgentCard schema
+ */
+ public static AgentCard getAgentCard(String agentUrl, String relativeCardPath, Map authHeaders) throws A2AClientError, A2AClientJSONError {
+ return getAgentCard(new JdkA2AHttpClient(), agentUrl, relativeCardPath, authHeaders);
+ }
+
+ /**
+ * Get the agent card for an A2A agent.
+ *
+ * @param httpClient the http client to use
+ * @param agentUrl the base URL for the agent whose agent card we want to retrieve
+ * @param relativeCardPath optional path to the agent card endpoint relative to the base
+ * agent URL, defaults to ".well-known/agent.json"
+ * @param authHeaders the HTTP authentication headers to use
+ * @return the agent card
+ * @throws A2AClientError If an HTTP error occurs fetching the card
+ * @throws A2AClientJSONError f the response body cannot be decoded as JSON or validated against the AgentCard schema
+ */
+ public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl, String relativeCardPath, Map authHeaders) throws A2AClientError, A2AClientJSONError {
+ A2ACardResolver resolver = new A2ACardResolver(httpClient, agentUrl, relativeCardPath, authHeaders);
+ return resolver.getAgentCard();
+ }
+
+ protected static boolean isValidMethodName(String methodName) {
+ return methodName != null && (methodName.equals(CANCEL_TASK_METHOD)
+ || methodName.equals(GET_TASK_METHOD)
+ || methodName.equals(GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD)
+ || methodName.equals(SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD)
+ || methodName.equals(SEND_TASK_RESUBSCRIPTION_METHOD)
+ || methodName.equals(SEND_MESSAGE_METHOD)
+ || methodName.equals(SEND_STREAMING_MESSAGE_METHOD));
+
+ }
+
+}
diff --git a/core/src/main/java/io/a2a/spec/A2AClientError.java b/core/src/main/java/io/a2a/spec/A2AClientError.java
new file mode 100644
index 000000000..2ec8c864e
--- /dev/null
+++ b/core/src/main/java/io/a2a/spec/A2AClientError.java
@@ -0,0 +1,14 @@
+package io.a2a.spec;
+
+public class A2AClientError extends Exception {
+ public A2AClientError() {
+ }
+
+ public A2AClientError(String message) {
+ super(message);
+ }
+
+ public A2AClientError(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/core/src/main/java/io/a2a/spec/A2AClientHTTPError.java b/core/src/main/java/io/a2a/spec/A2AClientHTTPError.java
new file mode 100644
index 000000000..95b59a764
--- /dev/null
+++ b/core/src/main/java/io/a2a/spec/A2AClientHTTPError.java
@@ -0,0 +1,33 @@
+package io.a2a.spec;
+
+import io.a2a.util.Assert;
+
+public class A2AClientHTTPError extends A2AClientError {
+ private final int code;
+ private final String message;
+
+ public A2AClientHTTPError(int code, String message, Object data) {
+ Assert.checkNotNullParam("code", code);
+ Assert.checkNotNullParam("message", message);
+ this.code = code;
+ this.message = message;
+ }
+
+ /**
+ * Gets the error code
+ *
+ * @return the error code
+ */
+ public int getCode() {
+ return code;
+ }
+
+ /**
+ * Gets the error message
+ *
+ * @return the error message
+ */
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/core/src/main/java/io/a2a/spec/A2AClientJSONError.java b/core/src/main/java/io/a2a/spec/A2AClientJSONError.java
new file mode 100644
index 000000000..75988da1c
--- /dev/null
+++ b/core/src/main/java/io/a2a/spec/A2AClientJSONError.java
@@ -0,0 +1,15 @@
+package io.a2a.spec;
+
+public class A2AClientJSONError extends A2AClientError {
+
+ public A2AClientJSONError() {
+ }
+
+ public A2AClientJSONError(String message) {
+ super(message);
+ }
+
+ public A2AClientJSONError(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/core/src/main/java/io/a2a/spec/A2AError.java b/core/src/main/java/io/a2a/spec/A2AError.java
new file mode 100644
index 000000000..4c9951df2
--- /dev/null
+++ b/core/src/main/java/io/a2a/spec/A2AError.java
@@ -0,0 +1,4 @@
+package io.a2a.spec;
+
+public interface A2AError extends Event {
+}
diff --git a/core/src/main/java/io/a2a/spec/A2AException.java b/core/src/main/java/io/a2a/spec/A2AException.java
new file mode 100644
index 000000000..22b8363e2
--- /dev/null
+++ b/core/src/main/java/io/a2a/spec/A2AException.java
@@ -0,0 +1,46 @@
+package io.a2a.spec;
+
+import java.io.IOException;
+
+/**
+ * Exception to indicate a general failure related to the A2A protocol.
+ */
+public class A2AException extends IOException {
+
+ /**
+ * Constructs a new {@code A2AException} instance. The message is left blank ({@code null}), and no
+ * cause is specified.
+ */
+ public A2AException() {
+ }
+
+ /**
+ * Constructs a new {@code A2AException} instance with an initial message. No cause is specified.
+ *
+ * @param msg the message
+ */
+ public A2AException(final String msg) {
+ super(msg);
+ }
+
+ /**
+ * Constructs a new {@code A2AException} instance with an initial cause. If a non-{@code null} cause
+ * is specified, its message is used to initialize the message of this {@code A2AException}; otherwise
+ * the message is left blank ({@code null}).
+ *
+ * @param cause the cause
+ */
+ public A2AException(final Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Constructs a new {@code A2AException} instance with an initial message and cause.
+ *
+ * @param msg the message
+ * @param cause the cause
+ */
+ public A2AException(final String msg, final Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/core/src/main/java/io/a2a/spec/A2AServerException.java b/core/src/main/java/io/a2a/spec/A2AServerException.java
new file mode 100644
index 000000000..ca2611c2f
--- /dev/null
+++ b/core/src/main/java/io/a2a/spec/A2AServerException.java
@@ -0,0 +1,23 @@
+package io.a2a.spec;
+
+/**
+ * Exception to indicate a general failure related to an A2A server.
+ */
+public class A2AServerException extends A2AException {
+
+ public A2AServerException() {
+ super();
+ }
+
+ public A2AServerException(final String msg) {
+ super(msg);
+ }
+
+ public A2AServerException(final Throwable cause) {
+ super(cause);
+ }
+
+ public A2AServerException(final String msg, final Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/core/src/main/java/io/a2a/spec/APIKeySecurityScheme.java b/core/src/main/java/io/a2a/spec/APIKeySecurityScheme.java
new file mode 100644
index 000000000..acf33feba
--- /dev/null
+++ b/core/src/main/java/io/a2a/spec/APIKeySecurityScheme.java
@@ -0,0 +1,118 @@
+package io.a2a.spec;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+import io.a2a.util.Assert;
+
+/**
+ * Represents an API Key security scheme.
+ */
+@JsonInclude(JsonInclude.Include.NON_ABSENT)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public final class APIKeySecurityScheme implements SecurityScheme {
+
+ public static final String API_KEY = "apiKey";
+ private final String in;
+ private final String name;
+ private final String type;
+ private final String description;
+
+ /**
+ * Represents the location of the API key.
+ */
+ public enum Location {
+ COOKIE("cookie"),
+ HEADER("header"),
+ QUERY("query");
+
+ private final String location;
+
+ Location(String location) {
+ this.location = location;
+ }
+
+ @JsonValue
+ public String asString() {
+ return location;
+ }
+
+ @JsonCreator
+ public static Location fromString(String location) {
+ switch (location) {
+ case "cookie":
+ return COOKIE;
+ case "header":
+ return HEADER;
+ case "query":
+ return QUERY;
+ default:
+ throw new IllegalArgumentException("Invalid API key location: " + location);
+ }
+ }
+ }
+
+ public APIKeySecurityScheme(String in, String name, String description) {
+ this(in, name, description, API_KEY);
+ }
+
+ @JsonCreator
+ public APIKeySecurityScheme(@JsonProperty("in") String in, @JsonProperty("name") String name,
+ @JsonProperty("description") String description, @JsonProperty("type") String type) {
+ Assert.checkNotNullParam("in", in);
+ Assert.checkNotNullParam("name", name);
+ if (! type.equals(API_KEY)) {
+ throw new IllegalArgumentException("Invalid type for APIKeySecurityScheme");
+ }
+ this.in = in;
+ this.name = name;
+ this.description = description;
+ this.type = type;
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+
+ public String getIn() {
+ return in;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public static class Builder {
+ private String in;
+ private String name;
+ private String description;
+
+ public Builder in(String in) {
+ this.in = in;
+ return this;
+ }
+
+ public Builder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder description(String description) {
+ this.description = description;
+ return this;
+ }
+
+ public APIKeySecurityScheme build() {
+ return new APIKeySecurityScheme(in, name, description);
+ }
+ }
+}
diff --git a/core/src/main/java/io/a2a/spec/AgentCapabilities.java b/core/src/main/java/io/a2a/spec/AgentCapabilities.java
new file mode 100644
index 000000000..641f56ccb
--- /dev/null
+++ b/core/src/main/java/io/a2a/spec/AgentCapabilities.java
@@ -0,0 +1,47 @@
+package io.a2a.spec;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+/**
+ * An agent's capabilities.
+ */
+@JsonInclude(JsonInclude.Include.NON_ABSENT)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record AgentCapabilities(boolean streaming, boolean pushNotifications, boolean stateTransitionHistory,
+ List extensions) {
+
+ public static class Builder {
+
+ private boolean streaming;
+ private boolean pushNotifications;
+ private boolean stateTransitionHistory;
+ private List extensions;
+
+ public Builder streaming(boolean streaming) {
+ this.streaming = streaming;
+ return this;
+ }
+
+ public Builder pushNotifications(boolean pushNotifications) {
+ this.pushNotifications = pushNotifications;
+ return this;
+ }
+
+ public Builder stateTransitionHistory(boolean stateTransitionHistory) {
+ this.stateTransitionHistory = stateTransitionHistory;
+ return this;
+ }
+
+ public Builder extensions(List extensions) {
+ this.extensions = extensions;
+ return this;
+ }
+
+ public AgentCapabilities build() {
+ return new AgentCapabilities(streaming, pushNotifications, stateTransitionHistory, extensions);
+ }
+ }
+}
diff --git a/core/src/main/java/io/a2a/spec/AgentCard.java b/core/src/main/java/io/a2a/spec/AgentCard.java
new file mode 100644
index 000000000..8429b16f5
--- /dev/null
+++ b/core/src/main/java/io/a2a/spec/AgentCard.java
@@ -0,0 +1,127 @@
+package io.a2a.spec;
+
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.a2a.util.Assert;
+
+/**
+ * A public metadata file that describes an agent's capabilities, skills, endpoint URL, and
+ * authentication requirements. Clients use this for discovery.
+ */
+@JsonInclude(JsonInclude.Include.NON_ABSENT)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record AgentCard(String name, String description, String url, AgentProvider provider,
+ String version, String documentationUrl, AgentCapabilities capabilities,
+ List defaultInputModes, List defaultOutputModes, List skills,
+ boolean supportsAuthenticatedExtendedCard, Map securitySchemes,
+ List