Skip to content

Commit c6bbb24

Browse files
committed
server middleware API
1 parent 6c17632 commit c6bbb24

7 files changed

Lines changed: 853 additions & 0 deletions

File tree

a2a/core.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ func (*Message) isSendMessageResult() {}
3434
// Event interface is used to represent types that can be sent over a streaming connection.
3535
type Event interface {
3636
isEvent()
37+
38+
Meta() map[string]any
3739
}
3840

3941
func (*Message) isEvent() {}
@@ -54,6 +56,8 @@ func NewMessageID() string {
5456
return uuid.NewString()
5557
}
5658

59+
var _ Event = (*Message)(nil)
60+
5761
// Message represents a single message in the conversation between a user and an agent.
5862
type Message struct {
5963
// ID is a unique identifier for the message, typically a UUID, generated by the sender.
@@ -105,6 +109,10 @@ func NewMessageForTask(role MessageRole, task *Task, parts ...Part) *Message {
105109
}
106110
}
107111

112+
func (m *Message) Meta() map[string]any {
113+
return m.Metadata
114+
}
115+
108116
// TaskID is a unique identifier for the task, generated by the server for a new task.
109117
type TaskID string
110118

@@ -142,6 +150,8 @@ func (ts TaskState) Terminal() bool {
142150
ts == TaskStateRejected
143151
}
144152

153+
var _ Event = (*Task)(nil)
154+
145155
// Task represents a single, stateful operation or conversation between a client and an agent.
146156
type Task struct {
147157
// ID is a unique identifier for the task, generated by the server for a new task.
@@ -176,6 +186,10 @@ type TaskStatus struct {
176186
Timestamp *time.Time `json:"timestamp,omitempty" yaml:"timestamp,omitempty" mapstructure:"timestamp,omitempty"`
177187
}
178188

189+
func (m *Task) Meta() map[string]any {
190+
return m.Metadata
191+
}
192+
179193
// ArtifactID is a unique identifier for the artifact within the scope of the task.
180194
type ArtifactID string
181195

@@ -205,6 +219,8 @@ type Artifact struct {
205219
Parts ContentParts `json:"parts" yaml:"parts" mapstructure:"parts"`
206220
}
207221

222+
var _ Event = (*TaskArtifactUpdateEvent)(nil)
223+
208224
// TaskArtifactUpdateEvent is an event sent by the agent to notify the client that an artifact has been
209225
// generated or updated. This is typically used in streaming models.
210226
type TaskArtifactUpdateEvent struct {
@@ -228,6 +244,10 @@ type TaskArtifactUpdateEvent struct {
228244
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty" mapstructure:"metadata,omitempty"`
229245
}
230246

247+
func (a *TaskArtifactUpdateEvent) Meta() map[string]any {
248+
return a.Metadata
249+
}
250+
231251
// NewArtifactEvent create a TaskArtifactUpdateEvent for an Artifact with a random ID.
232252
func NewArtifactEvent(task *Task, parts ...Part) *TaskArtifactUpdateEvent {
233253
return &TaskArtifactUpdateEvent{
@@ -253,6 +273,8 @@ func NewArtifactUpdateEvent(task *Task, id ArtifactID, parts ...Part) *TaskArtif
253273
}
254274
}
255275

276+
var _ Event = (*TaskStatusUpdateEvent)(nil)
277+
256278
// TaskStatusUpdateEvent is an event sent by the agent to notify the client of a change in a task's status.
257279
// This is typically used in streaming or subscription models.
258280
type TaskStatusUpdateEvent struct {
@@ -286,6 +308,10 @@ func NewStatusUpdateEvent(task *Task, state TaskState, msg *Message) *TaskStatus
286308
}
287309
}
288310

311+
func (a *TaskStatusUpdateEvent) Meta() map[string]any {
312+
return a.Metadata
313+
}
314+
289315
// ContentParts is an array of content parts that form the message body or an artifact.
290316
type ContentParts []Part
291317

a2asrv/auth.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package a2asrv
2+
3+
// User can be attached to call context by authentication middleware.
4+
type User interface {
5+
// Name returns a username.
6+
Name() string
7+
// Authenticated returns true if requested was authenticated.
8+
Authenticated() bool
9+
}
10+
11+
// AuthenticatedUser is a simple implementation of User interface which can be configured with a username.
12+
type AuthenticatedUser struct {
13+
UserName string
14+
}
15+
16+
func (u *AuthenticatedUser) Name() string {
17+
return u.UserName
18+
}
19+
20+
func (u *AuthenticatedUser) Authenticated() bool {
21+
return true
22+
}
23+
24+
type unauthenticatedUser struct {
25+
UserName string
26+
}
27+
28+
func (unauthenticatedUser) Name() string {
29+
return ""
30+
}
31+
32+
func (unauthenticatedUser) Authenticated() bool {
33+
return false
34+
}

a2asrv/extensions.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package a2asrv
2+
3+
import (
4+
"context"
5+
"slices"
6+
7+
"github.com/a2aproject/a2a-go/a2a"
8+
)
9+
10+
const ExtensionsMetaKey = "X-A2A-Extensions"
11+
12+
// Extensions provides utility methods for accessing extensions requested by the client and keeping track of extensions
13+
// activated during request processing.
14+
type Extensions struct {
15+
callCtx *CallContext
16+
}
17+
18+
// ExtensionsFrom is a helper function for quick access to Extensions in the current CallContext.
19+
func ExtensionsFrom(ctx context.Context) (*Extensions, bool) {
20+
serverCallCtx, ok := CallContextFrom(ctx)
21+
if !ok {
22+
return nil, false
23+
}
24+
return serverCallCtx.Extensions(), true
25+
}
26+
27+
// Active returns true if an extension has already been activated in the current CallContext using ExtensionContext.Activate.
28+
func (e *Extensions) Active(extension *a2a.AgentExtension) bool {
29+
return slices.Contains(e.callCtx.activatedExtensions, extension.URI)
30+
}
31+
32+
// Activate marks extension as activated in the current CallContext. A list of activated extensions might be attached as
33+
// response metadata by a transport implementation.
34+
func (e *Extensions) Activate(extension *a2a.AgentExtension) {
35+
if e.Active(extension) {
36+
return
37+
}
38+
e.callCtx.activatedExtensions = append(e.callCtx.activatedExtensions, extension.URI)
39+
}
40+
41+
// ActivatedURIs returns all URIs activated during call execution.
42+
func (e *Extensions) ActivatedURIs() []string {
43+
return slices.Clone(e.callCtx.activatedExtensions)
44+
}
45+
46+
// Requested returns true if the provided extension was requested by the client.
47+
func (e *Extensions) Requested(extension *a2a.AgentExtension) bool {
48+
return slices.Contains(e.RequestedURIs(), extension.URI)
49+
}
50+
51+
// RequestedURIs returns all URIs of extensions requested by the client.
52+
func (e *Extensions) RequestedURIs() []string {
53+
requested, ok := e.callCtx.RequestMeta().Get(ExtensionsMetaKey)
54+
if !ok {
55+
return []string{}
56+
}
57+
return slices.Clone(requested)
58+
}

a2asrv/intercepted_handler.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package a2asrv
2+
3+
import (
4+
"context"
5+
"iter"
6+
7+
"github.com/a2aproject/a2a-go/a2a"
8+
)
9+
10+
// InterceptedHandler implements RequestHandler. It can be used to attach call interceptors and initialize
11+
// call context for every method of the wrapped handler.
12+
type InterceptedHandler struct {
13+
// Handler is responsible for the actual processing of every call.
14+
Handler RequestHandler
15+
// Interceptors is a list of call interceptors which will be applied before and after each call.
16+
Interceptors []CallInterceptor
17+
}
18+
19+
func (h *InterceptedHandler) OnGetTask(ctx context.Context, query *a2a.TaskQueryParams) (*a2a.Task, error) {
20+
ctx, callCtx := withMethodCallContext(ctx, "OnGetTask")
21+
ctx, err := h.interceptBefore(ctx, callCtx, query)
22+
if err != nil {
23+
return nil, err
24+
}
25+
response, err := h.Handler.OnGetTask(ctx, query)
26+
if errOverride := h.interceptAfter(ctx, callCtx, response, err); errOverride != nil {
27+
return nil, errOverride
28+
}
29+
return response, err
30+
}
31+
32+
func (h *InterceptedHandler) OnCancelTask(ctx context.Context, params *a2a.TaskIDParams) (*a2a.Task, error) {
33+
ctx, callCtx := withMethodCallContext(ctx, "OnCancelTask")
34+
ctx, err := h.interceptBefore(ctx, callCtx, params)
35+
if err != nil {
36+
return nil, err
37+
}
38+
response, err := h.Handler.OnCancelTask(ctx, params)
39+
if errOverride := h.interceptAfter(ctx, callCtx, response, err); errOverride != nil {
40+
return nil, errOverride
41+
}
42+
return response, err
43+
}
44+
45+
func (h *InterceptedHandler) OnSendMessage(ctx context.Context, params *a2a.MessageSendParams) (a2a.SendMessageResult, error) {
46+
ctx, callCtx := withMethodCallContext(ctx, "OnSendMessage")
47+
ctx, err := h.interceptBefore(ctx, callCtx, params)
48+
if err != nil {
49+
return nil, err
50+
}
51+
response, err := h.Handler.OnSendMessage(ctx, params)
52+
if errOverride := h.interceptAfter(ctx, callCtx, response, err); errOverride != nil {
53+
return nil, errOverride
54+
}
55+
return response, err
56+
}
57+
58+
func (h *InterceptedHandler) OnSendMessageStream(ctx context.Context, params *a2a.MessageSendParams) iter.Seq2[a2a.Event, error] {
59+
return func(yield func(a2a.Event, error) bool) {
60+
ctx, callCtx := withMethodCallContext(ctx, "OnSendMessageStream")
61+
ctx, err := h.interceptBefore(ctx, callCtx, params)
62+
if err != nil {
63+
yield(nil, err)
64+
return
65+
}
66+
for event, err := range h.Handler.OnSendMessageStream(ctx, params) {
67+
if errOverride := h.interceptAfter(ctx, callCtx, event, err); errOverride != nil {
68+
yield(nil, errOverride)
69+
return
70+
}
71+
if !yield(event, err) {
72+
return
73+
}
74+
}
75+
}
76+
}
77+
78+
func (h *InterceptedHandler) OnResubscribeToTask(ctx context.Context, params *a2a.TaskIDParams) iter.Seq2[a2a.Event, error] {
79+
return func(yield func(a2a.Event, error) bool) {
80+
ctx, callCtx := withMethodCallContext(ctx, "OnResubscribeToTask")
81+
ctx, err := h.interceptBefore(ctx, callCtx, params)
82+
if err != nil {
83+
yield(nil, err)
84+
return
85+
}
86+
for event, err := range h.Handler.OnResubscribeToTask(ctx, params) {
87+
if errOverride := h.interceptAfter(ctx, callCtx, event, err); errOverride != nil {
88+
yield(nil, errOverride)
89+
return
90+
}
91+
if !yield(event, err) {
92+
return
93+
}
94+
}
95+
}
96+
}
97+
98+
func (h *InterceptedHandler) OnGetTaskPushConfig(ctx context.Context, params *a2a.GetTaskPushConfigParams) (*a2a.TaskPushConfig, error) {
99+
ctx, callCtx := withMethodCallContext(ctx, "OnGetTaskPushConfig")
100+
ctx, err := h.interceptBefore(ctx, callCtx, params)
101+
if err != nil {
102+
return nil, err
103+
}
104+
response, err := h.Handler.OnGetTaskPushConfig(ctx, params)
105+
if errOverride := h.interceptAfter(ctx, callCtx, response, err); errOverride != nil {
106+
return nil, errOverride
107+
}
108+
return response, err
109+
}
110+
111+
func (h *InterceptedHandler) OnListTaskPushConfig(ctx context.Context, params *a2a.ListTaskPushConfigParams) ([]*a2a.TaskPushConfig, error) {
112+
ctx, callCtx := withMethodCallContext(ctx, "OnListTaskPushConfig")
113+
ctx, err := h.interceptBefore(ctx, callCtx, params)
114+
if err != nil {
115+
return nil, err
116+
}
117+
response, err := h.Handler.OnListTaskPushConfig(ctx, params)
118+
if errOverride := h.interceptAfter(ctx, callCtx, response, err); errOverride != nil {
119+
return nil, errOverride
120+
}
121+
return response, err
122+
}
123+
124+
func (h *InterceptedHandler) OnSetTaskPushConfig(ctx context.Context, params *a2a.TaskPushConfig) (*a2a.TaskPushConfig, error) {
125+
ctx, callCtx := withMethodCallContext(ctx, "OnSetTaskPushConfig")
126+
ctx, err := h.interceptBefore(ctx, callCtx, params)
127+
if err != nil {
128+
return nil, err
129+
}
130+
response, err := h.Handler.OnSetTaskPushConfig(ctx, params)
131+
if errOverride := h.interceptAfter(ctx, callCtx, response, err); errOverride != nil {
132+
return nil, errOverride
133+
}
134+
return response, err
135+
}
136+
137+
func (h *InterceptedHandler) OnDeleteTaskPushConfig(ctx context.Context, params *a2a.DeleteTaskPushConfigParams) error {
138+
ctx, callCtx := withMethodCallContext(ctx, "OnDeleteTaskPushConfig")
139+
ctx, err := h.interceptBefore(ctx, callCtx, params)
140+
if err != nil {
141+
return err
142+
}
143+
err = h.Handler.OnDeleteTaskPushConfig(ctx, params)
144+
if errOverride := h.interceptAfter(ctx, callCtx, nil, err); errOverride != nil {
145+
return errOverride
146+
}
147+
return err
148+
}
149+
150+
func (h *InterceptedHandler) interceptBefore(ctx context.Context, callCtx *CallContext, payload any) (context.Context, error) {
151+
request := &Request{Payload: payload}
152+
153+
for _, interceptor := range h.Interceptors {
154+
localCtx, err := interceptor.Before(ctx, callCtx, request)
155+
if err != nil {
156+
return ctx, err
157+
}
158+
ctx = localCtx
159+
}
160+
161+
return ctx, nil
162+
}
163+
164+
func (h *InterceptedHandler) interceptAfter(ctx context.Context, callCtx *CallContext, payload any, responseErr error) error {
165+
response := &Response{Payload: payload, Err: responseErr}
166+
167+
for _, interceptor := range h.Interceptors {
168+
if err := interceptor.After(ctx, callCtx, response); err != nil {
169+
return err
170+
}
171+
}
172+
173+
return nil
174+
}
175+
176+
// withMethodCallContext is a private utility function which modifies CallContext.method if a CallContext
177+
// was passed by a transport implementation or initializes a new CallContext with the provided method.
178+
func withMethodCallContext(ctx context.Context, method string) (context.Context, *CallContext) {
179+
callCtx, ok := CallContextFrom(ctx)
180+
if !ok {
181+
ctx, callCtx = WithCallContext(ctx, nil)
182+
}
183+
callCtx.method = method
184+
return ctx, callCtx
185+
}

0 commit comments

Comments
 (0)