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
5 changes: 0 additions & 5 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,6 @@ linters:
linters:
- staticcheck
text: "SA1019:.*FunctionCall.*deprecated"
# adka2a/v2 migration requires a full a2a-go v2 upgrade (separate PR)
- path: pkg/a2a/
linters:
- staticcheck
text: "SA1019:.*adka2a.*deprecated"
# MCP sampling (SEP-2577) is deprecated upstream but functional during
# the deprecation window; migrating off it is a separate effort
- path: ^(e2e/sampling_test|pkg/runtime/sampling(_test)?|pkg/tools/sampling|pkg/tools/codemode/codemode_test|pkg/tools/mcp/(mcp|session_client)(_test)?)\.go$
Expand Down
58 changes: 30 additions & 28 deletions e2e/a2a_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (
"testing"
"time"

"github.com/a2aproject/a2a-go/a2a"
"github.com/a2aproject/a2a-go/a2asrv"
"github.com/a2aproject/a2a-go/v2/a2a"
"github.com/a2aproject/a2a-go/v2/a2asrv"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

Expand All @@ -27,27 +27,24 @@ type a2aResponse struct {
ID string `json:"id"`
Error any `json:"error,omitempty"`

// Result holds the task with its artifacts.
Result *struct {
Artifacts []struct {
Parts []struct {
Kind string `json:"kind"`
Text string `json:"text"`
} `json:"parts"`
} `json:"artifacts"`
} `json:"result,omitempty"`
// Result holds the produced event, e.g. the task with its artifacts.
Result *a2a.StreamResponse `json:"result,omitempty"`
}

// textParts returns all text parts across every artifact in the response.
func (r *a2aResponse) textParts() []string {
if r.Result == nil {
return nil
}
task, ok := r.Result.Event.(*a2a.Task)
if !ok {
return nil
}
var texts []string
for _, a := range r.Result.Artifacts {
for _, a := range task.Artifacts {
for _, p := range a.Parts {
if p.Kind == "text" {
texts = append(texts, p.Text)
if _, ok := p.Content.(a2a.Text); ok {
texts = append(texts, p.Text())
}
}
}
Expand All @@ -62,8 +59,9 @@ func TestA2AServer_AgentCard(t *testing.T) {

assert.Equal(t, "basic", agentCard.Name)
assert.NotEmpty(t, agentCard.Description)
assert.Equal(t, a2a.TransportProtocolJSONRPC, agentCard.PreferredTransport)
assert.Contains(t, agentCard.URL, "/invoke")
require.Len(t, agentCard.SupportedInterfaces, 1)
assert.Equal(t, a2a.TransportProtocolJSONRPC, agentCard.SupportedInterfaces[0].ProtocolBinding)
assert.Contains(t, agentCard.SupportedInterfaces[0].URL, "/invoke")
assert.True(t, agentCard.Capabilities.Streaming)
assert.NotEmpty(t, agentCard.Version)
}
Expand All @@ -74,7 +72,7 @@ func TestA2AServer_Invoke(t *testing.T) {
_, runConfig := startRecordingAIProxy(t)
agentCard := startA2AServer(t, "testdata/basic.yaml", runConfig)

resp := sendA2AMessage(t, agentCard.URL, "test-request-1", "msg-1", "What is 2+2? Answer with just the number.")
resp := sendA2AMessage(t, invokeURL(t, agentCard), "test-request-1", "msg-1", "What is 2+2? Answer with just the number.")

assert.Equal(t, "2.0", resp.Jsonrpc)
assert.Equal(t, "test-request-1", resp.ID)
Expand Down Expand Up @@ -104,7 +102,7 @@ func TestA2AServer_MultipleRequests(t *testing.T) {
requestID := fmt.Sprintf("test-request-%d", i)
msgID := fmt.Sprintf("msg-%d", i)

resp := sendA2AMessage(t, agentCard.URL, requestID, msgID, message)
resp := sendA2AMessage(t, invokeURL(t, agentCard), requestID, msgID, message)

assert.Equal(t, requestID, resp.ID)
assert.Nil(t, resp.Error)
Expand All @@ -119,7 +117,7 @@ func TestA2AServer_MultiAgent(t *testing.T) {
_, runConfig := startRecordingAIProxy(t)
agentCard := startA2AServer(t, "testdata/multi.yaml", runConfig)

resp := sendA2AMessage(t, agentCard.URL, "test-multi-1", "msg-multi-1", "Say hello.")
resp := sendA2AMessage(t, invokeURL(t, agentCard), "test-multi-1", "msg-multi-1", "Say hello.")

assert.Equal(t, "test-multi-1", resp.ID)
assert.Nil(t, resp.Error)
Expand All @@ -130,21 +128,25 @@ func TestA2AServer_MultiAgent(t *testing.T) {
assert.Contains(t, texts[len(texts)-1], "Hello")
}

// sendA2AMessage sends a message/send JSON-RPC request and returns the parsed response.
// invokeURL returns the JSON-RPC endpoint advertised by the agent card.
func invokeURL(t *testing.T, card a2a.AgentCard) string {
t.Helper()

require.NotEmpty(t, card.SupportedInterfaces)
return card.SupportedInterfaces[0].URL
}

// sendA2AMessage sends a SendMessage JSON-RPC request and returns the parsed response.
func sendA2AMessage(t *testing.T, url, requestID, messageID, text string) a2aResponse {
t.Helper()

message := a2a.NewMessage(a2a.MessageRoleUser, a2a.NewTextPart(text))
message.ID = messageID
body, err := json.Marshal(map[string]any{
"jsonrpc": "2.0",
"id": requestID,
"method": "message/send",
"params": map[string]any{
"message": map[string]any{
"messageId": messageID,
"role": "user",
"parts": []map[string]any{{"kind": "text", "text": text}},
},
},
"method": "SendMessage",
"params": &a2a.SendMessageRequest{Message: message},
})
require.NoError(t, err)

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/Masterminds/semver/v3 v3.5.0
github.com/Microsoft/go-winio v0.6.2
github.com/a2aproject/a2a-go v0.3.15
github.com/a2aproject/a2a-go/v2 v2.4.0
github.com/alecthomas/chroma/v2 v2.27.0
github.com/alpkeskin/gotoon v0.1.1
github.com/anthropics/anthropic-sdk-go v1.66.0
Expand Down Expand Up @@ -104,7 +105,6 @@ require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/JohannesKaufmann/dom v0.3.1 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/a2aproject/a2a-go/v2 v2.4.0 // indirect
github.com/agext/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/apparentlymart/go-textseg/v17 v17.0.1 // indirect
Expand Down
65 changes: 31 additions & 34 deletions pkg/a2a/executor_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package a2a

import (
"context"
"iter"

"github.com/a2aproject/a2a-go/a2a"
"github.com/a2aproject/a2a-go/a2asrv"
"github.com/a2aproject/a2a-go/a2asrv/eventqueue"
"google.golang.org/adk/v2/server/adka2a"
"github.com/a2aproject/a2a-go/v2/a2a"
"github.com/a2aproject/a2a-go/v2/a2asrv"
adka2a "google.golang.org/adk/v2/server/adka2a/v2"
)

// executorWrapper wraps an ADK executor and fixes artifact update events
Expand All @@ -15,48 +15,45 @@ type executorWrapper struct {
executor *adka2a.Executor
}

var (
_ a2asrv.AgentExecutor = (*executorWrapper)(nil)
_ a2asrv.AgentExecutionCleaner = (*executorWrapper)(nil)
)

func newExecutorWrapper(config adka2a.ExecutorConfig) *executorWrapper {
return &executorWrapper{
executor: adka2a.NewExecutor(config),
}
}

func (w *executorWrapper) Execute(ctx context.Context, reqCtx *a2asrv.RequestContext, queue eventqueue.Queue) error {
// Create a wrapping queue that fixes events before sending them
fixedQueue := &fixingQueue{
queue: queue,
}
return w.executor.Execute(ctx, reqCtx, fixedQueue)
func (w *executorWrapper) Execute(ctx context.Context, execCtx *a2asrv.ExecutorContext) iter.Seq2[a2a.Event, error] {
return fixArtifactEvents(w.executor.Execute(ctx, execCtx))
}

func (w *executorWrapper) Cancel(ctx context.Context, reqCtx *a2asrv.RequestContext, queue eventqueue.Queue) error {
return w.executor.Cancel(ctx, reqCtx, queue)
func (w *executorWrapper) Cancel(ctx context.Context, execCtx *a2asrv.ExecutorContext) iter.Seq2[a2a.Event, error] {
return w.executor.Cancel(ctx, execCtx)
}

// fixingQueue wraps an eventqueue.Queue and fixes artifact update events
type fixingQueue struct {
queue eventqueue.Queue
// Cleanup delegates to the ADK executor, which implements
// a2asrv.AgentExecutionCleaner; dropping it would change cleanup semantics.
func (w *executorWrapper) Cleanup(ctx context.Context, execCtx *a2asrv.ExecutorContext, result a2a.SendMessageResult, err error) {
w.executor.Cleanup(ctx, execCtx, result, err)
}

func (fq *fixingQueue) Write(ctx context.Context, event a2a.Event) error {
// Fix artifact update events with nil Parts
if artifactEvent, ok := event.(*a2a.TaskArtifactUpdateEvent); ok {
if artifactEvent.Artifact != nil && artifactEvent.Artifact.Parts == nil {
// Replace nil with an empty slice
artifactEvent.Artifact.Parts = []a2a.Part{}
// fixArtifactEvents wraps an event sequence and fixes artifact update events
// with nil Parts before yielding them. Everything else passes through unchanged.
func fixArtifactEvents(events iter.Seq2[a2a.Event, error]) iter.Seq2[a2a.Event, error] {
return func(yield func(a2a.Event, error) bool) {
for event, err := range events {
if artifactEvent, ok := event.(*a2a.TaskArtifactUpdateEvent); ok {
if artifactEvent.Artifact != nil && artifactEvent.Artifact.Parts == nil {
// Replace nil with an empty slice
artifactEvent.Artifact.Parts = a2a.ContentParts{}
}
}
if !yield(event, err) {
return
}
}
}
return fq.queue.Write(ctx, event)
}

func (fq *fixingQueue) Read(ctx context.Context) (a2a.Event, a2a.TaskVersion, error) {
return fq.queue.Read(ctx)
}

func (fq *fixingQueue) WriteVersioned(ctx context.Context, event a2a.Event, version a2a.TaskVersion) error {
return fq.queue.WriteVersioned(ctx, event, version)
}

func (fq *fixingQueue) Close() error {
return fq.queue.Close()
}
Loading
Loading