Skip to content

Conversation

@yarolegovich
Copy link
Member

@yarolegovich yarolegovich commented Aug 6, 2025

Details

  • Define core types in package a2a. These match the published specification and will be referenced by both client, server and packages used to customize the server implementation, like custom TaskStores.
  • Define a transport-agnostic RequestHandler interface in a2asrv package along with interfaces used for changing and extending its behavior.
  • Create core type dummies to establish the internal package structure for components that will be used by a2asrv.

Rationale

A decision was made to hand-craft the core types for internal component implementation instead of auto-generating them from the spec. Auto-generated types will be used in translation layers that map transport-specific structures (jsonrpc, grpc) to the manually written structs.

The alternative approach was carefully evaluated and the following reasons influenced the final decision:

  • An ability to provide go-idiomatic types for SDK users. The specification uses union types and inheritance and there’s no good generator that can elegantly translate these concepts to Go.
  • The experience of other projects that used type generation from spec and either abandoned it due to generation pipeline maintenance costs or ended up with complicated custom plugin setups.
  • Schema evolution forces widespread changes throughout the project regardless of whether types are generated or hand-crafted. When the protocol fundamentally changes the impact ripples through tests, business logic and storage layers.
  • This decision is reversible at the current stage given the current protocol complexity. If we see that the decision is not optimal we can revert to the code-generation-first approach.

Type creation

  1. Spec types were generated from the published schema using the script which is a part of this PR.
  2. Another ad-hoc script was run to cleanup most of the json-rpc-related artifacts: (comments, protocol-related fields, Marshal & Unmarshal methods, fields tag) and perform some name replacements (Url -> URL).
  3. A series of manual changes (1, 2) was applied to better match Go conventions, split types into multiple files, remove unused type-wrappers, shorten type names.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Summary of Changes

Hello @yarolegovich, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

I've laid the groundwork for the A2A Go project by defining its core types and interfaces. This includes generating Go types from the A2A specification, establishing key server-side interfaces for handling agent interactions and task management, and setting up the initial internal package structure for future implementations. I also updated the module name to reflect its Go-specific nature and introduced tooling to automate schema-to-Go type conversion.

Highlights

  • Core A2A Types: I've introduced a2a/types.go and a2a/errors.go to define the fundamental data structures and error constants for the A2A protocol, directly derived from its published specification.
  • Server-Side Interfaces: I've established a comprehensive set of transport-agnostic interfaces within the a2asrv package, such as RequestHandler, AgentExecutor, EventQueueManager, and TaskStore, to abstract the server's core functionalities.
  • Internal Package Scaffolding: I've created dummy files in internal/events, internal/push, and internal/task to outline the structure for concrete, internal implementations of the a2asrv interfaces.
  • Module Renaming: I've updated the go.mod file to change the module path from github.com/a2aproject/a2a to github.com/a2aproject/a2a-go, aligning the module with Go's naming conventions.
  • Automated Type Generation: I've added tools/jsonrpc2spec.go and tools/jsonrpc_gen.sh to automate the process of converting the A2A JSON schema into Go types and performing necessary sanitization, ensuring type consistency with the specification.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments or fill out our survey to provide feedback.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@yarolegovich yarolegovich marked this pull request as ready for review August 6, 2025 16:48
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request lays a strong foundation by defining the core types and interfaces for the A2A protocol in Go. The approach of generating types from the JSON schema and then sanitizing them is a good one for ensuring spec compliance.

My review focuses on refining this generation and sanitization process to improve maintainability and idiomatic Go code. I've identified several areas where the generated code can be cleaned up, such as removing duplicate or unnecessary types and fixing issues in the sanitization script itself. Addressing these points will lead to a cleaner and more robust type system.

@@ -0,0 +1,208 @@
package a2a
Copy link
Collaborator

Choose a reason for hiding this comment

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

These types are defined by A2A spec, and we're already generating Go types for them. Could you explain the reasoning and benefits of introducing them here, instead of reusing generated types from grpc?

Copy link
Member Author

@yarolegovich yarolegovich Aug 7, 2025

Choose a reason for hiding this comment

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

I thought about this at the beginning, but it doesn't feel clean to implement a default transport-agnostic handler that works with proto-generated types, which contain:

  1. Enums named like TaskState_TASK_STATE_AUTH_REQUIRED, types named Part_File
  2. Proto-specific fields (even though these are private)
  3. fields whose name is for some reason different (Parts field of type Part in Message is called Content in proto type)
  4. Id interface{} because clients are allowed to send a number or a string

I think it's better to keep extension points like TaskStore depend on plain structs and have grpc and in the future jsonrpc as translation layers that wrap RequestHandler.

also I think there should be one client that can talk to both grpc and jsonrpc backends, so using proto types in its APIs also seems like a quick solution, not a clean one

Copy link

Choose a reason for hiding this comment

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

So, we should definitely have all specification-defined data types (both gRPC and JSON-RPC request/responses) publicly exported and accessible to consumers of this library. One way someone might want to use this SDK is purely as a source of data type definitions that serialize to/deserialize from the specification-defined format. They may want to build all of the actual serving components themselves, but having the types auto-generated is a useful time saver. All of the existing SDKs do this.

https://github.com/a2aproject/a2a-java/tree/main/spec/src/main/java/io/a2a/spec
https://github.com/a2aproject/a2a-js/blob/main/src/types.ts
https://github.com/a2aproject/a2a-python/blob/main/src/a2a/types.py

If you then want to have a separate set of more idiomatic Go types, with converters between, that's reasonable (but then you'd really want to justify the improvement they make vs. the core spec types). However, one major advantage of relying on core generated types throughout is that you don't have to hand edit the types to match the spec when it updates -- just re-run the generator. For simple spec changes, that's all you'd need to do to be up-to-date with a new version of the protocol.

The way the Python SDK approached this problem is to standardize on the types defined by the JSON schema part of the spec. Transport-agnostic implementations that developers provide use those types directly. The gRPC types are converted to the JSON schema generated versions of the types. The Java SDK followed this same pattern.

Copy link
Member Author

Choose a reason for hiding this comment

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

One way someone might want to use this SDK is purely as a source of data type definitions that serialize to/deserialize from the specification-defined format. They may want to build all of the actual serving components themselves, but having the types auto-generated is a useful time saver. All of the existing SDKs do this.

Makes sense. I'll regenerate grpc types into a public a2apb package.

If you then want to have a separate set of more idiomatic Go types, with converters between, that's reasonable (but then you'd really want to justify the improvement they make vs. the core spec types)

I see the benefits of using spec types directly especially at the beginning. That's why I made very minimal changes to the result of jsonrpc schema generator tool.

Decoupling at this stage involves more work, but in my opinion it will give us more control over internal architecture, migrations, compatibility management and reduce the room for errors.

Maybe I'm misunderstanding project evolution direction, but here's my reasoning:

Control over changes

A spec changed, and we regenerated types. Now the question is what has just happened to all the places that were using these types:

  1. How many places don't compile?
  2. How many tests do we need to update?
  3. Did this type update break some places which still compile but won't work properly in runtime?
  4. What happens with external modules (eg. custom stores), are they still compatible with our SDK?
  5. Is stored data still compatible?
  6. What about apps that use client for making requests, do they need to be updated?

Translation layers and manual model evolution requires deliberate changes, and these questions will need to be answered explicitly. There'll be a dedicated place to resolve all inconsistencies with internal logic - the translation layer.

Compatibility

Another question is what are our compatibility guarantees? I've just realized that ProtocolVersion in AgentCard is a string (not an array) and the spec doesn't mention protocol negotiation. We can have different card paths or introduce a new field, but if we don't support multi-version servers the first person to update their server to the new protocol gets effectively severed from everyone else because their server can only advertise the latest protocol version without an option to fallback to an older one.

What about clients? Do we plan to explicitly require creating a client of a specific spec version - NewClientV*? Or just a NewClient that'll discover the version and switch to the appropriate spec translator?

To address these scenarios we'll need to build translation layers anyway, but now we'll be translating from older spec types to newer spec types which might be harder, as we won't be able to separate version specific information where required. I understand that depending on how the protocol evolves maintaining many versions in the same codebase might become very problematic, but this can be addressed either by gradual version deprecations for very old versions.

Devex

These are minor, but I think decoupling can slightly improve devex for both SDK consumers and developers.

  1. We can provide more concise names for some fields, we can match naming conventions like capitalizing ID or URL.
  2. We don't want interface implementers to create errors like &{Code: ..., Data: ..., Message: ... }, and we would introduce a a2asrv.NewTaskNotFoundErr() or something like that. But return nil, ErrTaskNotFound I think looks more idiomatic and easier to work with for consumers (errors.Is vs errors.As).
  3. We won't need to deal with generation artifacts throughout the project. Json schema to go conversion is not always clean (I think the main reason is lack of inheritance).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thank you for the detailed reasoning. Let me address your points below:

Control over changes

I share Mike's concern here. While the idea of a translation layer acting as a single choke-point for spec changes sounds appealing, in practice, it often just shifts the problem.

If a field is changed/removed from a proto message, the first compile error will be in the translation layer. However, once you fix that, you'll get a cascade of compile errors in every place that used the corresponding field from the internal type. You still have to answer the same hard questions ("How many tests to update?", "Is stored data compatible?"), but now you also have the added maintenance burden of keeping the translator and the duplicated struct in sync.

Compatibility

You've raised an excellent and critical point: the current spec seems to lack a clear protocol versioning and negotiation strategy. This is a fundamental problem, but I think it needs to be addressed at the protocol level.

Trying to solve this inside our SDK with a translation layer is likely premature and could lead us to build a complex abstraction for a problem that might be solved differently in a future spec update. For instance, if we build a translation layer from spec_v1 -> internal_model and spec_v2 -> internal_model, we risk our internal_model becoming a complex superset of all features, making it hard to maintain.

The more robust approach is to push for this to be clarified in the A2A spec itself. Until then, we could build for the current spec and treat significant breaking changes in the protocol as a trigger for a major version bump of our SDK.

Devex

This is for sure the strongest point in favor for translation layer, as the generated types can indeed be un-idiomatic. I wonder if there's a workaround for this. Do you think we could introduce a lightweight "facade" layer—not by duplicating structs, but by adding helper methods to the generated types or creating utility functions?

For now I'd defer building a full-blown translation layer until we have a more concrete needs (e.g. we understand how multi-version handling should work), which aligns closely with Mike's advice and established patterns in other SDKs. WDYT?

Copy link
Member Author

@yarolegovich yarolegovich Aug 8, 2025

Choose a reason for hiding this comment

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

If a field is changed/removed from a proto message, the first compile error will be in the translation layer. However, once you fix that, you'll get a cascade of compile errors in every place that used the corresponding field from the internal type.

This won't be true for every spec change, some can be handled at the translation layer level.

You still have to answer the same hard questions

Correct. My point there was that these might be easier to answer as we'll have more flexibility with types and changes made to these types would be decisions controlled on the SDK level, not driven by spec evolution.

For instance, if we build a translation layer from spec_v1 -> internal_model and spec_v2 -> internal_model, we risk our internal_model becoming a complex superset of all features, making it hard to maintain.

I agree, that might become a real problem. Eventually older versions need to be deprecated and the code cleaned up.

The more robust approach is to push for this to be clarified in the A2A spec itself. Until then, we could build for the current spec and treat significant breaking changes in the protocol as a trigger for a major version bump of our SDK.

I agree that deserves a separate technology-independent discussion. A major version bump doesn't sound like the right strategy for an interoperability protocol.

Do you think we could introduce a lightweight "facade" layer—not by duplicating structs, but by adding helper methods to the generated types or creating utility functions?

We can. Doing it "the right" (clean, simple, intuitive) way is harder than (for example) using time.Time instead of a string field.

From my point of view the spec is a contract for communication and should stay at the boundary. Its evolution is not concerned with abstractions we invented for the reference implementation (task store, queues etc).

But I'm yielding if nobody is convinced, maybe it's speculative overengineering 🙂

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the biggest challenge for automatic generation is that the go ecosystem lacks a magic jsonschema-to-go generator. Some types in spec are not easily translatable (e.g. sum types) without manual intervention. As a result, we will anyway end up with writing our own custom generator and have to maintain it actively.

For example, in gopls (Go language server), we had to build a non-trivial custom spec-to-proto generator, and had to inspect the translator and spec carefully whenever we update the LSP spec, even though LSP had been relatively stable for a while. Another extreme example is the MCP Go SDK. They started with a custom generator but decided to remove it and manually managing types (modelcontextprotocol/go-sdk#7). I heard from the maintainer that maintaining the generator code added more work than manually adding necessary types.

Is there anything we can reuse/learn from other projects (e.g. Google Cloud SDK, google.golang.org/genai) for managing grpc and json api types?

reusing generated types from grpc

That will make json rpc depend on protobuf (and if we keep the service def type in the package, grpc). I am not sure if that's a good thing.

Another question: is the a2a grpc proto complete and suitable enough to be used to generate types and codes needed for json rpc? I don't see A2AError type in proto for example. TaskState is a proto enum type, but in json rpc, it's string enum type. (For Google Cloud SDK, it seems like the SOT is the proto file, not jsonschema, so they may not have this issue).

Copy link
Member Author

Choose a reason for hiding this comment

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

Just to provide more context for the discussion:

I think the biggest challenge for automatic generation is that the go ecosystem lacks a magic jsonschema-to-go generator.

Yeah, go-jsonschema is the best tool I found. I think it produces satisfactory output for the latest spec, but inheritance and union types are a problem:

OpenApi tooling can also be used for creating types from json-schema. But it doesn't support codec validations and doesn't handle the above casese any better (I tried).

Is there anything we can reuse/learn from other projects (e.g. Google Cloud SDK, google.golang.org/genai) for managing grpc and json api types?

I couldn't find anything type-generation related in genai repo. From the commit history it looks like they're manually maintaining a 5k-line file with hand-crafted types.

An interesting proto compiler I came across is gogo/protobuf. It is used by a number of big projects like etcd, mesos, tidb, but is unfortunately deprecated. Among its features it provides many extensions some of which can be used to generate more idiomatic Go structs. Tried the default configuration here.

Another question: is the a2a grpc proto complete and suitable enough to be used to generate types and codes needed for json rpc?

Good point, in proto-first approach we'll need to hand-craft errors for RequestHandler to return, errors are not a part of proto spec.

@@ -0,0 +1,208 @@
package a2a
Copy link

Choose a reason for hiding this comment

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

So, we should definitely have all specification-defined data types (both gRPC and JSON-RPC request/responses) publicly exported and accessible to consumers of this library. One way someone might want to use this SDK is purely as a source of data type definitions that serialize to/deserialize from the specification-defined format. They may want to build all of the actual serving components themselves, but having the types auto-generated is a useful time saver. All of the existing SDKs do this.

https://github.com/a2aproject/a2a-java/tree/main/spec/src/main/java/io/a2a/spec
https://github.com/a2aproject/a2a-js/blob/main/src/types.ts
https://github.com/a2aproject/a2a-python/blob/main/src/a2a/types.py

If you then want to have a separate set of more idiomatic Go types, with converters between, that's reasonable (but then you'd really want to justify the improvement they make vs. the core spec types). However, one major advantage of relying on core generated types throughout is that you don't have to hand edit the types to match the spec when it updates -- just re-run the generator. For simple spec changes, that's all you'd need to do to be up-to-date with a new version of the protocol.

The way the Python SDK approached this problem is to standardize on the types defined by the JSON schema part of the spec. Transport-agnostic implementations that developers provide use those types directly. The gRPC types are converted to the JSON schema generated versions of the types. The Java SDK followed this same pattern.

Copy link
Collaborator

@mazas-google mazas-google left a comment

Choose a reason for hiding this comment

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

Discussed offline with Yaroslav and Hana. Arguments in favor of hand-crafted types makes us want to try it out. We'll reassess later if the maintenance cost is a burden, and how we want to handle consistency between other SDKs. Yaroslav will document this design decision.

@yarolegovich yarolegovich merged commit 69b96ea into main Aug 12, 2025
2 checks passed
@yarolegovich yarolegovich deleted the yarolegovich/core-types branch August 12, 2025 09:23
pull bot pushed a commit to joshuafuller/a2a-go that referenced this pull request Nov 4, 2025
🤖 I have created a release *beep* *boop*
---


## 0.3.0 (2025-11-04)


### Features

* add JSON-RPC client transport implementation
([a2aproject#79](a2aproject#79))
([1690088](a2aproject@1690088))
* agent card resolver
([a2aproject#48](a2aproject#48))
([0951293](a2aproject@0951293))
* blocking flag handling
([a2aproject#97](a2aproject#97))
([f7aa465](a2aproject@f7aa465)),
closes [a2aproject#96](a2aproject#96)
* client API proposal
([a2aproject#32](a2aproject#32))
([b6ca54f](a2aproject@b6ca54f))
* client auth interceptor
([a2aproject#90](a2aproject#90))
([25b9aae](a2aproject@25b9aae))
* client interceptor invocations
([a2aproject#51](a2aproject#51))
([3e9f2ae](a2aproject@3e9f2ae))
* core types JSON codec
([a2aproject#42](a2aproject#42))
([c5b3982](a2aproject@c5b3982))
* define core types and interfaces
([#16](a2aproject#16))
([69b96ea](a2aproject@69b96ea))
* disallow custom types and circular refs in Metadata
([a2aproject#43](a2aproject#43))
([53bc928](a2aproject@53bc928))
* get task implementation
([a2aproject#59](a2aproject#59))
([f74d854](a2aproject@f74d854))
* grpc authenticated agent card and producer utils
([a2aproject#85](a2aproject#85))
([9d82f31](a2aproject@9d82f31)),
closes [a2aproject#82](a2aproject#82)
* grpc client transport
([a2aproject#66](a2aproject#66))
([fee703e](a2aproject@fee703e))
* grpc code generation from A2A .proto spec
([#11](a2aproject#11))
([2993b98](a2aproject@2993b98))
* handling artifacts and implementing send message stream
([a2aproject#52](a2aproject#52))
([c3fa631](a2aproject@c3fa631))
* implement an a2aclient.Factory
([a2aproject#50](a2aproject#50))
([49deee7](a2aproject@49deee7))
* implementing grpc server wrapper
([a2aproject#37](a2aproject#37))
([071e952](a2aproject@071e952))
* implementing message-message interaction
([a2aproject#34](a2aproject#34))
([b568979](a2aproject@b568979))
* implementing task pushes
([a2aproject#86](a2aproject#86))
([c210240](a2aproject@c210240))
* input-required and auth-required handling
([a2aproject#70](a2aproject#70))
([3ac89ba](a2aproject@3ac89ba))
* jsonrpc server ([a2aproject#91](a2aproject#91))
([5491030](a2aproject@5491030))
* logger ([a2aproject#56](a2aproject#56))
([86ab9d2](a2aproject@86ab9d2))
* request context loading
([a2aproject#60](a2aproject#60))
([ab7a29b](a2aproject@ab7a29b))
* result aggregation part 1 - task store
([a2aproject#38](a2aproject#38))
([d3c02f5](a2aproject@d3c02f5))
* result aggregation part 3 - concurrent task executor
([a2aproject#40](a2aproject#40))
([265c3e7](a2aproject@265c3e7))
* result aggregation part 4 - integration
([a2aproject#41](a2aproject#41))
([bab72d9](a2aproject@bab72d9))
* SDK type utilities
([a2aproject#31](a2aproject#31))
([32b77b4](a2aproject@32b77b4))
* server middleware API
([a2aproject#63](a2aproject#63))
([738bf85](a2aproject@738bf85))
* server middleware integration
([a2aproject#64](a2aproject#64))
([5dc8be0](a2aproject@5dc8be0))
* smarter a2aclient
([a2aproject#88](a2aproject#88))
([322d05b](a2aproject@322d05b))
* task event factory
([a2aproject#95](a2aproject#95))
([fbf3bcf](a2aproject@fbf3bcf)),
closes [a2aproject#84](a2aproject#84)
* task executor docs
([a2aproject#36](a2aproject#36))
([b6868df](a2aproject@b6868df))
* task update logic
([0ac987f](a2aproject@0ac987f))


### Bug Fixes

* Execute() callers missing events
([a2aproject#74](a2aproject#74))
([4c3389f](a2aproject@4c3389f))
* mark task failed when execution fails
([a2aproject#94](a2aproject#94))
([ee0e7ed](a2aproject@ee0e7ed))
* push semantics update
([a2aproject#93](a2aproject#93))
([76bff9f](a2aproject@76bff9f))
* race detector queue closed access
([c07b7d0](a2aproject@c07b7d0))
* regenerate proto and update converters
([a2aproject#81](a2aproject#81))
([c732060](a2aproject@c732060))
* streaming ([a2aproject#92](a2aproject#92))
([ca7a64b](a2aproject@ca7a64b))


### Miscellaneous Chores

* release 0.3.0
([fa7cfba](a2aproject@fa7cfba))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants