-
Notifications
You must be signed in to change notification settings - Fork 83
feat: method extensions prototyping #138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -20,6 +20,7 @@ import ( | |||||
| "iter" | ||||||
|
|
||||||
| "github.com/a2aproject/a2a-go/a2a" | ||||||
| "github.com/a2aproject/a2a-go/a2aext" | ||||||
| ) | ||||||
|
|
||||||
| // A2AClient defines a transport-agnostic interface for making A2A requests. | ||||||
|
|
@@ -56,6 +57,9 @@ type Transport interface { | |||||
| // If extended card is supported calls the 'agent/getAuthenticatedExtendedCard' protocol method. | ||||||
| GetAgentCard(ctx context.Context) (*a2a.AgentCard, error) | ||||||
|
|
||||||
| // Invoke calls the provided extension method. | ||||||
| Invoke(ctx context.Context, client a2aext.Method, arg any) (any, error) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For clarity and consistency, consider renaming the
Suggested change
|
||||||
|
|
||||||
| // Clean up resources associated with the transport (eg. close a gRPC channel). | ||||||
| Destroy() error | ||||||
| } | ||||||
|
|
@@ -120,6 +124,10 @@ func (unimplementedTransport) GetAgentCard(ctx context.Context) (*a2a.AgentCard, | |||||
| return nil, errNotImplemented | ||||||
| } | ||||||
|
|
||||||
| func (unimplementedTransport) Invoke(ctx context.Context, client a2aext.Method, arg any) (any, error) { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For consistency with the
Suggested change
|
||||||
| return nil, errNotImplemented | ||||||
| } | ||||||
|
|
||||||
| func (unimplementedTransport) Destroy() error { | ||||||
| return nil | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| package a2aext | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "iter" | ||
|
|
||
| "github.com/a2aproject/a2a-go/a2a" | ||
| ) | ||
|
|
||
| type Binding interface { | ||
| Protocol() a2a.TransportProtocol | ||
| } | ||
|
|
||
| type Method interface { | ||
| Name() string | ||
| Binding(a2a.TransportProtocol) (Binding, bool) | ||
| Streaming() bool | ||
| } | ||
|
|
||
| type ServerMethod interface { | ||
| Method | ||
|
|
||
| InvokeUnary(ctx context.Context, arg any) (any, error) | ||
| InvokeStreaming(ctx context.Context, arg any) iter.Seq2[any, error] | ||
| } | ||
|
|
||
| type methodDescriptor struct { | ||
| name string | ||
| bindings map[a2a.TransportProtocol]Binding | ||
| streaming bool | ||
| } | ||
|
|
||
| func (d *methodDescriptor) Name() string { return d.name } | ||
|
|
||
| func (d *methodDescriptor) Binding(protocol a2a.TransportProtocol) (Binding, bool) { | ||
| b, ok := d.bindings[protocol] | ||
| return b, ok | ||
| } | ||
|
|
||
| func (d *methodDescriptor) Streaming() bool { return d.streaming } | ||
|
|
||
| type UnaryClientMethod[Req, Resp any] struct { | ||
| methodDescriptor | ||
| } | ||
|
|
||
| var _ Method = (*UnaryClientMethod[any, any])(nil) | ||
|
|
||
| // NewUnaryClientMethod creates a new unary extension method definition. | ||
| func NewUnaryClientMethod[Arg, Res any]( | ||
| name string, | ||
| bindings ...Binding, | ||
| ) *UnaryClientMethod[Arg, Res] { | ||
| bmap := map[a2a.TransportProtocol]Binding{} | ||
| for _, b := range bindings { | ||
| bmap[b.Protocol()] = b | ||
| } | ||
| return &UnaryClientMethod[Arg, Res]{ | ||
| methodDescriptor: methodDescriptor{name: name, bindings: bmap, streaming: false}, | ||
| } | ||
| } | ||
|
|
||
| type unaryServerMethod[Arg, Res any] struct { | ||
| methodDescriptor | ||
| call func(context.Context, *Arg) (*Res, error) | ||
| } | ||
|
|
||
| // NewUnaryMethod creates a new unary extension method definition. | ||
| func NewUnaryServerMethod[Arg, Res any]( | ||
| name string, | ||
| call func(context.Context, *Arg) (*Res, error), | ||
| bindings ...Binding, | ||
| ) ServerMethod { | ||
| bmap := map[a2a.TransportProtocol]Binding{} | ||
| for _, b := range bindings { | ||
| bmap[b.Protocol()] = b | ||
| } | ||
| return &unaryServerMethod[Arg, Res]{ | ||
| methodDescriptor: methodDescriptor{name: name, bindings: bmap, streaming: false}, | ||
| call: call, | ||
| } | ||
| } | ||
|
|
||
| func (m *unaryServerMethod[Arg, Res]) InvokeUnary(ctx context.Context, arg any) (any, error) { | ||
| return m.call(ctx, arg.(*Arg)) | ||
| } | ||
|
Comment on lines
+84
to
+86
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The direct type assertion func (m *unaryServerMethod[Arg, Res]) InvokeUnary(ctx context.Context, arg any) (any, error) {
typedArg, ok := arg.(*Arg)
if !ok {
return nil, errors.New("internal error: unexpected argument type")
}
return m.call(ctx, typedArg)
} |
||
|
|
||
| func (m *unaryServerMethod[Arg, Res]) InvokeStreaming(ctx context.Context, arg any) iter.Seq2[any, error] { | ||
| return func(yield func(any, error) bool) { | ||
| yield(nil, errors.New("unary method invoked as streaming")) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ import ( | |
| "log/slog" | ||
|
|
||
| "github.com/a2aproject/a2a-go/a2a" | ||
| "github.com/a2aproject/a2a-go/a2aext" | ||
| "github.com/a2aproject/a2a-go/a2asrv/eventqueue" | ||
| "github.com/a2aproject/a2a-go/a2asrv/limiter" | ||
| "github.com/a2aproject/a2a-go/a2asrv/push" | ||
|
|
@@ -59,6 +60,15 @@ type RequestHandler interface { | |
|
|
||
| // GetAgentCard returns an extended a2a.AgentCard if configured. | ||
| OnGetExtendedAgentCard(ctx context.Context) (*a2a.AgentCard, error) | ||
|
|
||
| // GetAgentCard returns an extended a2a.AgentCard if configured. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| GetExtensionMethods() []a2aext.ServerMethod | ||
|
|
||
| // Invoke calls a registered extension method. | ||
| Invoke(ctx context.Context, method a2aext.ServerMethod, arg any) (any, error) | ||
|
|
||
| // InvokeStreaming calls a registered streaming extension method. | ||
| InvokeStreaming(ctx context.Context, method a2aext.ServerMethod, arg any) iter.Seq2[any, error] | ||
| } | ||
|
|
||
| // Implements a2asrv.RequestHandler. | ||
|
|
@@ -75,6 +85,8 @@ type defaultRequestHandler struct { | |
| reqContextInterceptors []RequestContextInterceptor | ||
|
|
||
| authenticatedCardProducer AgentCardProducer | ||
|
|
||
| extensionMethods []a2aext.ServerMethod | ||
| } | ||
|
|
||
| // RequestHandlerOption can be used to customize the default [RequestHandler] implementation behavior. | ||
|
|
@@ -104,12 +116,20 @@ func WithConcurrencyConfig(config limiter.ConcurrencyConfig) RequestHandlerOptio | |
| } | ||
| } | ||
|
|
||
| // WithConcurrencyConfig allows to set limits on the number of concurrent executions. | ||
| func WithExtensionMethod(method a2aext.ServerMethod) RequestHandlerOption { | ||
| return func(ih *InterceptedHandler, h *defaultRequestHandler) { | ||
| h.extensionMethods = append(h.extensionMethods, method) | ||
| } | ||
| } | ||
|
|
||
| // NewHandler creates a new request handler. | ||
| func NewHandler(executor AgentExecutor, options ...RequestHandlerOption) RequestHandler { | ||
| h := &defaultRequestHandler{ | ||
| agentExecutor: executor, | ||
| queueManager: eventqueue.NewInMemoryManager(), | ||
| taskStore: taskstore.NewMem(), | ||
| agentExecutor: executor, | ||
| extensionMethods: []a2aext.ServerMethod{}, | ||
| queueManager: eventqueue.NewInMemoryManager(), | ||
| taskStore: taskstore.NewMem(), | ||
| // push notifications are not supported by default | ||
| } | ||
| ih := &InterceptedHandler{Handler: h, Logger: slog.Default()} | ||
|
|
@@ -307,6 +327,18 @@ func (h *defaultRequestHandler) OnGetExtendedAgentCard(ctx context.Context) (*a2 | |
| return h.authenticatedCardProducer.Card(ctx) | ||
| } | ||
|
|
||
| func (h *defaultRequestHandler) GetExtensionMethods() []a2aext.ServerMethod { | ||
| return h.extensionMethods | ||
| } | ||
|
|
||
| func (h *defaultRequestHandler) Invoke(ctx context.Context, method a2aext.ServerMethod, arg any) (any, error) { | ||
| return method.InvokeUnary(ctx, arg) | ||
| } | ||
|
|
||
| func (h *defaultRequestHandler) InvokeStreaming(ctx context.Context, method a2aext.ServerMethod, arg any) iter.Seq2[any, error] { | ||
| return method.InvokeStreaming(ctx, arg) | ||
| } | ||
|
|
||
| func shouldInterruptNonStreaming(params *a2a.MessageSendParams, event a2a.Event) (a2a.TaskID, bool) { | ||
| // Non-blocking clients receive a result on the first task event, default Blocking to TRUE | ||
| if params.Config != nil && params.Config.Blocking != nil && !(*params.Config.Blocking) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message here is identical to the one for the check above, which can be misleading. If the binding exists but has the wrong type, the error should reflect that instead of saying the method is not bound.