Skip to content

Commit 5b3c542

Browse files
Merge branch 'main' into serob/migrate-clustermode-examples-to-use-generic-PullQueue
2 parents 5c1cb8e + d52d5a1 commit 5b3c542

2 files changed

Lines changed: 173 additions & 0 deletions

File tree

a2asrv/eventqueue/queue.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package eventqueue
1616

1717
import (
1818
"context"
19+
"encoding/json"
1920
"errors"
2021

2122
"github.com/a2aproject/a2a-go/v2/a2a"
@@ -38,6 +39,45 @@ type Message struct {
3839
Protocol a2a.ProtocolVersion
3940
}
4041

42+
// messageJSON is the JSON-serializable representation of Message.
43+
// It uses a2a.StreamResponse as an intermediate type for the Event field
44+
// so that a2a.Event (an interface) survives a JSON roundtrip.
45+
type messageJSON struct {
46+
Event a2a.StreamResponse `json:"event"`
47+
TaskVersion taskstore.TaskVersion `json:"taskVersion"`
48+
Protocol a2a.ProtocolVersion `json:"protocol"`
49+
}
50+
51+
// ErrNilEvent indicates that a Message has a nil Event field, which is an invalid state.
52+
var ErrNilEvent = errors.New("Message.Event is nil")
53+
54+
// MarshalJSON implements json.Marshaler.
55+
func (m Message) MarshalJSON() ([]byte, error) {
56+
if m.Event == nil {
57+
return nil, ErrNilEvent
58+
}
59+
return json.Marshal(messageJSON{
60+
Event: a2a.StreamResponse{Event: m.Event},
61+
TaskVersion: m.TaskVersion,
62+
Protocol: m.Protocol,
63+
})
64+
}
65+
66+
// UnmarshalJSON implements json.Unmarshaler.
67+
func (m *Message) UnmarshalJSON(data []byte) error {
68+
var wrapper messageJSON
69+
if err := json.Unmarshal(data, &wrapper); err != nil {
70+
return err
71+
}
72+
if wrapper.Event.Event == nil {
73+
return ErrNilEvent
74+
}
75+
m.Event = wrapper.Event.Event
76+
m.TaskVersion = wrapper.TaskVersion
77+
m.Protocol = wrapper.Protocol
78+
return nil
79+
}
80+
4181
// Reader defines the interface for reading events from a queue.
4282
// A2A server stack reads events written by [a2asrv.AgentExecutor].
4383
type Reader interface {

a2asrv/eventqueue/queue_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright 2026 The A2A Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package eventqueue
16+
17+
import (
18+
"encoding/json"
19+
"testing"
20+
21+
"github.com/a2aproject/a2a-go/v2/a2a"
22+
"github.com/a2aproject/a2a-go/v2/a2asrv/taskstore"
23+
"github.com/google/go-cmp/cmp"
24+
)
25+
26+
func TestMessageNilEventError(t *testing.T) {
27+
t.Parallel()
28+
29+
t.Run("MarshalJSON", func(t *testing.T) {
30+
t.Parallel()
31+
msg := Message{Event: nil}
32+
_, err := msg.MarshalJSON()
33+
if err == nil {
34+
t.Fatal("MarshalJSON with nil Event should return error")
35+
}
36+
if err != ErrNilEvent {
37+
t.Fatalf("MarshalJSON with nil Event returned %v, want ErrNilEvent", err)
38+
}
39+
})
40+
41+
t.Run("UnmarshalJSON", func(t *testing.T) {
42+
t.Parallel()
43+
// JSON missing "event" field entirely
44+
data := []byte(`{"taskVersion":1,"protocol":"1.0"}`)
45+
var msg Message
46+
err := msg.UnmarshalJSON(data)
47+
if err == nil {
48+
t.Fatal("UnmarshalJSON without event field should return error")
49+
}
50+
if err != ErrNilEvent {
51+
t.Fatalf("UnmarshalJSON without event field returned %v, want ErrNilEvent", err)
52+
}
53+
})
54+
}
55+
56+
func TestMessageJSONRoundtrip(t *testing.T) {
57+
t.Parallel()
58+
59+
tests := []struct {
60+
name string
61+
msg Message
62+
}{
63+
{
64+
name: "TaskStatusUpdateEvent",
65+
msg: Message{
66+
Event: &a2a.TaskStatusUpdateEvent{
67+
TaskID: "task-1",
68+
ContextID: "ctx-1",
69+
Status: a2a.TaskStatus{
70+
State: a2a.TaskStateWorking,
71+
},
72+
},
73+
TaskVersion: 42,
74+
Protocol: "1.0",
75+
},
76+
},
77+
{
78+
name: "TaskArtifactUpdateEvent",
79+
msg: Message{
80+
Event: &a2a.TaskArtifactUpdateEvent{
81+
TaskID: "task-2",
82+
ContextID: "ctx-2",
83+
Artifact: &a2a.Artifact{
84+
ID: "artifact-1",
85+
Name: "output",
86+
Parts: a2a.ContentParts{a2a.NewTextPart("hello")},
87+
},
88+
},
89+
TaskVersion: taskstore.TaskVersionMissing,
90+
Protocol: "1.0",
91+
},
92+
},
93+
{
94+
name: "MessageEvent",
95+
msg: Message{
96+
Event: a2a.NewMessage(a2a.MessageRoleAgent, a2a.NewTextPart("response")),
97+
TaskVersion: 7,
98+
Protocol: "1.0",
99+
},
100+
},
101+
{
102+
name: "TaskEvent",
103+
msg: Message{
104+
Event: &a2a.Task{
105+
ID: "task-3",
106+
Status: a2a.TaskStatus{State: a2a.TaskStateCompleted},
107+
},
108+
TaskVersion: 0,
109+
Protocol: a2a.Version,
110+
},
111+
},
112+
}
113+
114+
for _, tt := range tests {
115+
t.Run(tt.name, func(t *testing.T) {
116+
t.Parallel()
117+
118+
data, err := json.Marshal(tt.msg)
119+
if err != nil {
120+
t.Fatalf("Marshal returned error: %v", err)
121+
}
122+
123+
var got Message
124+
if err := json.Unmarshal(data, &got); err != nil {
125+
t.Fatalf("Unmarshal returned error: %v", err)
126+
}
127+
128+
if diff := cmp.Diff(tt.msg, got); diff != "" {
129+
t.Fatalf("Message JSON roundtrip wrong result (-want +got) diff = %s", diff)
130+
}
131+
})
132+
}
133+
}

0 commit comments

Comments
 (0)