Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions a2a_agents/go/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# A2UI Agent Go SDK

This directory contains the Go implementation of the A2UI agent library,
enabling agents to "speak UI" using the A2UI protocol.

## Overview

The `a2ui` package provides the core infrastructure for an agent to:

1. **Declare Capability**: Signal support for the A2UI extension (
`https://a2ui.org/a2a-extension/a2ui/v0.8`).
2. **Validate Payloads**: Ensure generated A2UI JSON conforms to the required
schema before sending.
3. **Transport UI**: Encapsulate A2UI payloads within A2A `DataPart`s for
transport to the client.

## Components

The SDK mirrors the structure of the reference Python implementation:

* **`a2ui.go`**: Core constants (`ExtensionURI`, MIME types), types, and helper
functions for extension management (`GetA2UIAgentExtension`,
`TryActivateA2UIExtension`) and A2UI part manipulation (`CreateA2UIPart`,
`IsA2UIPart`, `GetA2UIDataPart`).
* **`schema.go`**: Utilities for A2UI schema manipulation, specifically wrapping
the schema in a JSON array to support streaming lists of components.
* **`toolset.go`**:
* **`SendA2UIToClientToolset`**: Manages the A2UI toolset lifecycle.
* **`SendA2UIJsonToClientTool`**: A tool exposed to the LLM that validates
generated JSON against the provided schema using
`github.com/santhosh-tekuri/jsonschema/v5` and prepares it for sending.
* **`ConvertSendA2UIToClientGenAIPartToA2APart`**: A helper to convert LLM
tool responses into A2A `Part`s.

## Dependencies

* **A2A Protocol**: Uses the [A2A Go SDK](https://github.com/a2aproject/a2a-go)
for core definitions (`Part`, `DataPart`, `AgentExtension`, etc.).
* **JSON Schema Validation**: Uses `github.com/santhosh-tekuri/jsonschema/v5`
for robust runtime validation of agent-generated UI.

## Usage

### Initializing the Toolset

```go
import (
"github.com/google/A2UI/a2a_agents/go/a2ui"
// ... other imports
)

// Define a provider for your A2UI schema (e.g., loaded from a file)
schemaProvider := func (ctx context.Context) (map[string]interface{}, error) {
return loadMySchema(), nil
}

// Check if A2UI should be enabled for this request
enabledProvider := func (ctx context.Context) (bool, error) {
// Logic to check if the client supports A2UI (e.g., checking requested extensions)
return a2ui.TryActivateA2UIExtension(ctx), nil
}

// Create the toolset
toolset := a2ui.NewSendA2UIToClientToolset(
a2ui.A2UIEnabledProvider(enabledProvider),
a2ui.A2UISchemaProvider(schemaProvider),
)

// Get the tools to register with your LLM agent
tools, err := toolset.GetTools(ctx)
if err != nil {
// handle error
}
```

## Building the SDK

To build the SDK, run the following command from the `a2a_agents/go` directory:

```bash
go build ./a2ui
```

This will compile the `a2ui` package and report any syntax or dependency errors.
Since this is a library, it will not produce an executable binary.

## Running Tests

To run the test suite from the `a2a_agents/go` directory:

```bash
go test -v ./a2ui
```

## Disclaimer

Important: The sample code provided is for demonstration purposes and
illustrates the mechanics of A2UI and the Agent-to-Agent (A2A) protocol. When
building production applications, it is critical to treat any agent operating
outside of your direct control as a potentially untrusted entity.

All operational data received from an external agent—including its AgentCard,
messages, artifacts, and task statuses—should be handled as untrusted input. For
example, a malicious agent could provide crafted data in its fields (e.g., name,
skills.description) that, if used without sanitization to construct prompts for
a Large Language Model (LLM), could expose your application to prompt injection
attacks.

Similarly, any UI definition or data stream received must be treated as
untrusted. Malicious agents could attempt to spoof legitimate interfaces to
deceive users (phishing), inject malicious scripts via property values (XSS), or
generate excessive layout complexity to degrade client performance (DoS). If
your application supports optional embedded content (such as iframes or web
views), additional care must be taken to prevent exposure to malicious external
sites.

Developer Responsibility: Failure to properly validate data and strictly sandbox
rendered content can introduce severe vulnerabilities. Developers are
responsible for implementing appropriate security measures—such as input
sanitization, Content Security Policies (CSP), strict isolation for optional
embedded content, and secure credential handling—to protect their systems and
users.
115 changes: 115 additions & 0 deletions a2a_agents/go/a2ui/a2ui.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package a2ui

// Copyright 2026 Google LLC
//
// 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
//
// https://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.

import (
"context"
"fmt"
"log"

"github.com/a2aproject/a2a-go/a2a"
"github.com/a2aproject/a2a-go/a2asrv"
)

const (
// ExtensionURI is the URI for the A2UI extension.
ExtensionURI = "https://a2ui.org/a2a-extension/a2ui/v0.8"

// MIMETypeKey is the key for the MIME type in metadata.
MIMETypeKey = "mimeType"
// MIMEType is the MIME type for A2UI data.
MIMEType = "application/json+a2ui"

// ClientCapabilitiesKey is the key for A2UI client capabilities.
ClientCapabilitiesKey = "a2uiClientCapabilities"
// SupportedCatalogIDsKey is the key for supported catalog IDs.
SupportedCatalogIDsKey = "supportedCatalogIds"
// InlineCatalogsKey is the key for inline catalogs.
InlineCatalogsKey = "inlineCatalogs"

// StandardCatalogID is the ID for the standard catalog.
StandardCatalogID = "https://github.com/google/A2UI/blob/main/specification/v0_8/json/standard_catalog_definition.json"

// AgentExtensionSupportedCatalogIDsKey is the parameter key for supported catalogs in the agent extension.
AgentExtensionSupportedCatalogIDsKey = "supportedCatalogIds"
// AgentExtensionAcceptsInlineCatalogsKey is the parameter key for accepting inline catalogs.
AgentExtensionAcceptsInlineCatalogsKey = "acceptsInlineCatalogs"
)

// CreateA2UIPart creates an A2A Part containing A2UI data.
func CreateA2UIPart(a2uiData map[string]interface{}) a2a.Part {
return &a2a.DataPart{
Data: a2uiData,
Metadata: map[string]interface{}{
MIMETypeKey: MIMEType,
},
}
}

// GetA2UIDataPart extracts the DataPart containing A2UI data from an A2A Part, if present.
func GetA2UIDataPart(part a2a.Part) (*a2a.DataPart, error) {
dp, ok := part.(*a2a.DataPart)
if !ok {
return nil, fmt.Errorf("part is not a DataPart")
}
if dp.Metadata != nil && dp.Metadata[MIMETypeKey] == MIMEType {
return dp, nil
}
return nil, fmt.Errorf("part is not an A2UI part")
}

// GetA2UIAgentExtension creates the A2UI AgentExtension configuration.
func GetA2UIAgentExtension(acceptsInlineCatalogs bool, supportedCatalogIDs []string) *a2a.AgentExtension {
params := make(map[string]interface{})

if acceptsInlineCatalogs {
params[AgentExtensionAcceptsInlineCatalogsKey] = true
}

if len(supportedCatalogIDs) > 0 {
params[AgentExtensionSupportedCatalogIDsKey] = supportedCatalogIDs
}

var paramsOrNil map[string]interface{}
if len(params) > 0 {
paramsOrNil = params
}

return &a2a.AgentExtension{
URI: ExtensionURI,
Description: "Provides agent driven UI using the A2UI JSON format.",
Params: paramsOrNil,
}
}

// TryActivateA2UIExtension activates the A2UI extension if requested.
func TryActivateA2UIExtension(ctx context.Context) bool {
exts, ok := a2asrv.ExtensionsFrom(ctx)
if !ok {
log.Println("TryActivateA2UIExtension: No extensions found in context")
return false
}

a2uiExt := &a2a.AgentExtension{URI: ExtensionURI}
requested := exts.Requested(a2uiExt)

log.Printf("TryActivateA2UIExtension: Checking URI %s. Requested: %v", ExtensionURI, requested)

if requested {
exts.Activate(a2uiExt)
return true
}
return false
}
Loading