From 4a107d1e21be9cdf2cb5c1294a78d036d97bf7eb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 06:31:14 +0000 Subject: [PATCH 1/2] [dotnet-port-api] Forward A2A request config to hosted agents Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- agent/options.go | 9 ++ docs/dotnet-go-sdk-feature-comparison.md | 2 +- .../a2a_client_server/a2a_server/main.go | 5 +- provider/a2aprovider/executor.go | 4 + provider/a2aprovider/handler.go | 50 +++++++++++ provider/a2aprovider/hosting_test.go | 82 ++++++++++++++++++- 6 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 provider/a2aprovider/handler.go diff --git a/agent/options.go b/agent/options.go index 309b6213..1aca81b5 100644 --- a/agent/options.go +++ b/agent/options.go @@ -4,6 +4,7 @@ package agent import ( "iter" + "maps" "reflect" "slices" "strings" @@ -31,6 +32,7 @@ type ( toolModeOpt tool.ToolMode streamOpt bool allowBackgroundResponsesOpt bool + additionalPropertiesOpt struct{ props map[string]any } structuredOutputOpt struct{ any } ) @@ -40,6 +42,7 @@ func (o streamOpt) Value() any { return bool(o) } func (o continuationTokenOpt) Value() any { return string(o) } func (o instructionsOpt) Value() any { return string(o) } func (o allowBackgroundResponsesOpt) Value() any { return bool(o) } +func (o additionalPropertiesOpt) Value() any { return o.props } func (o toolModeOpt) Value() any { return tool.ToolMode(o) } func (o toolOpt) Value() any { return o.Tool } func (o structuredOutputOpt) Value() any { return o.any } @@ -169,3 +172,9 @@ func WithInstructions(instructions string) Option { func AllowBackgroundResponses(allow bool) Option { return allowBackgroundResponsesOpt(allow) } + +// WithAdditionalProperties sets provider-specific or protocol-specific request +// metadata for an agent run. +func WithAdditionalProperties(props map[string]any) Option { + return additionalPropertiesOpt{props: maps.Clone(props)} +} diff --git a/docs/dotnet-go-sdk-feature-comparison.md b/docs/dotnet-go-sdk-feature-comparison.md index 22f52a52..2ac391ce 100644 --- a/docs/dotnet-go-sdk-feature-comparison.md +++ b/docs/dotnet-go-sdk-feature-comparison.md @@ -49,7 +49,7 @@ Within overlapping features, the main misalignments are API shape and ecosystem | Anthropic provider | Anthropic packages and reasoning/skills/function-tool samples. | `anthropicprovider.NewAgent`, message params option. | Partial | Go has provider support, but sample coverage is smaller. | | Gemini/provider ecosystem | Google Gemini sample through provider adapters. | `geminiprovider.NewAgent`, generate content config option. | Aligned | .NET reaches more providers through generic `IChatClient` adapters; Go has a direct Gemini package. | | A2A agent client | `Microsoft.Agents.AI.A2A`, card/client extensions, task/session support. | `provider/a2aprovider`, task ID option, task ID/session helpers. | Aligned | Go exposes task IDs through options/session helpers; .NET exposes extension methods over A2A clients/cards/resolvers. | -| A2A hosting | A2A hosting packages, ASP.NET Core hosting, samples. | `provider/a2aprovider`, executor for a2a-go JSON-RPC and JSON HTTP handlers, end-to-end client/server sample. | Partial | Go integrates with a2a-go HTTP handlers but does not provide ASP.NET-style hosting/DI integration. | +| A2A hosting | A2A hosting packages, ASP.NET Core hosting, samples. | `provider/a2aprovider`, executor and handler helpers for a2a-go JSON-RPC and JSON HTTP handlers, inbound request metadata/config forwarding, end-to-end client/server sample. | Partial | Go integrates with a2a-go HTTP handlers and now forwards inbound request metadata/config to hosted agents, but does not provide ASP.NET-style hosting/DI integration. | | AGUI agent/client | AGUI chat client and shared conversions. | `provider/aguiprovider`, AGUI SSE client integration. | Aligned | Type models differ but feature categories line up. | | AGUI hosting | ASP.NET Core AGUI hosting, end-to-end web chat samples. | `provider/aguiprovider`, JSON HTTP handler, backend/frontend tools, HITL, state examples, reasoning event emission. | Partial | Go has handlers and examples, but no ASP.NET/Blazor-style end-to-end web app equivalent. | | Azure AI Persistent agents | `Microsoft.Agents.AI.AzureAI.Persistent`, lifecycle and persistent conversation samples. | No equivalent package. | .NET only | Go currently uses OpenAI/Azure OpenAI clients, not Azure AI Persistent Agents. | diff --git a/examples/05-end-to-end/a2a_client_server/a2a_server/main.go b/examples/05-end-to-end/a2a_client_server/a2a_server/main.go index c0d9ad9f..073b93c2 100644 --- a/examples/05-end-to-end/a2a_client_server/a2a_server/main.go +++ b/examples/05-end-to-end/a2a_client_server/a2a_server/main.go @@ -118,8 +118,9 @@ func main() { a2a.NewAgentInterface(url, a2a.TransportProtocolJSONRPC), } mux := http.NewServeMux() - requestHandler := a2asrv.NewHandler( - a2aprovider.NewExecutor(hostAgent, a2aprovider.ExecutorConfig{}), + requestHandler := a2aprovider.NewHandler( + hostAgent, + a2aprovider.ExecutorConfig{}, a2asrv.WithExtendedAgentCard(card), ) mux.Handle("/", a2asrv.NewJSONRPCHandler(requestHandler)) diff --git a/provider/a2aprovider/executor.go b/provider/a2aprovider/executor.go index 31e44c09..a6871d2d 100644 --- a/provider/a2aprovider/executor.go +++ b/provider/a2aprovider/executor.go @@ -6,6 +6,7 @@ import ( "context" "errors" "iter" + "maps" "github.com/a2aproject/a2a-go/v2/a2a" "github.com/a2aproject/a2a-go/v2/a2asrv" @@ -287,6 +288,9 @@ func (e *executor) newRunOptions(ctx context.Context, execCtx *a2asrv.ExecutorCo agent.WithSession(session), agent.AllowBackgroundResponses(allowBackground), } + if len(execCtx.Metadata) > 0 { + runOptions = append(runOptions, agent.WithAdditionalProperties(maps.Clone(execCtx.Metadata))) + } if stream { runOptions = append(runOptions, agent.Stream(true)) } diff --git a/provider/a2aprovider/handler.go b/provider/a2aprovider/handler.go new file mode 100644 index 00000000..1561bd8c --- /dev/null +++ b/provider/a2aprovider/handler.go @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. + +package a2aprovider + +import ( + "context" + "maps" + + "github.com/a2aproject/a2a-go/v2/a2a" + "github.com/a2aproject/a2a-go/v2/a2asrv" + "github.com/microsoft/agent-framework-go/agent" +) + +// RequestConfigurationPropertyKey is the agent additional-properties key under +// which inbound A2A message/send configuration is forwarded to hosted agents. +const RequestConfigurationPropertyKey = "a2a.configuration" + +// NewHandler creates an A2A request handler for a hosted agent and forwards +// inbound A2A request configuration to the hosted agent's run options. +func NewHandler(hostedAgent *agent.Agent, cfg ExecutorConfig, options ...a2asrv.RequestHandlerOption) a2asrv.RequestHandler { + options = append([]a2asrv.RequestHandlerOption{WithRequestConfigForwarding()}, options...) + return a2asrv.NewHandler(NewExecutor(hostedAgent, cfg), options...) +} + +// WithRequestConfigForwarding returns an A2A request-handler option that +// forwards the inbound [a2a.SendMessageRequest.Config] to hosted agents through +// [agent.WithAdditionalProperties] under [RequestConfigurationPropertyKey]. +func WithRequestConfigForwarding() a2asrv.RequestHandlerOption { + return a2asrv.WithCallInterceptors(requestConfigForwardingInterceptor{}) +} + +type requestConfigForwardingInterceptor struct { + a2asrv.PassthroughCallInterceptor +} + +func (requestConfigForwardingInterceptor) Before(ctx context.Context, _ *a2asrv.CallContext, req *a2asrv.Request) (context.Context, any, error) { + sendReq, ok := req.Payload.(*a2a.SendMessageRequest) + if !ok || sendReq == nil || sendReq.Config == nil { + return ctx, nil, nil + } + + cloned := *sendReq + cloned.Metadata = maps.Clone(sendReq.Metadata) + if cloned.Metadata == nil { + cloned.Metadata = map[string]any{} + } + cloned.Metadata[RequestConfigurationPropertyKey] = cloned.Config + req.Payload = &cloned + return ctx, nil, nil +} diff --git a/provider/a2aprovider/hosting_test.go b/provider/a2aprovider/hosting_test.go index 5a1bfbf2..9da04c5a 100644 --- a/provider/a2aprovider/hosting_test.go +++ b/provider/a2aprovider/hosting_test.go @@ -21,7 +21,7 @@ func newHostedTestAgent(runFn func(context.Context, []*message.Message, ...agent } func newRequestHandler(hostedAgent *agent.Agent, cfg a2aprovider.ExecutorConfig, options ...a2asrv.RequestHandlerOption) a2asrv.RequestHandler { - return a2asrv.NewHandler(a2aprovider.NewExecutor(hostedAgent, cfg), options...) + return a2aprovider.NewHandler(hostedAgent, cfg, options...) } func TestNewExecutor_PanicsWithoutAgent(t *testing.T) { @@ -113,6 +113,86 @@ func TestRequestHandler_OnSendMessage_PreservesContextID(t *testing.T) { } } +func TestRequestHandler_OnSendMessage_ForwardsMetadataAndConfigurationToHostedAgent(t *testing.T) { + historyLength := 10 + config := &a2a.SendMessageConfig{ + AcceptedOutputModes: []string{"text/plain", "image/png"}, + HistoryLength: &historyLength, + } + + var additionalProperties map[string]any + a := newHostedTestAgent(func(_ context.Context, _ []*message.Message, options ...agent.Option) iter.Seq2[*agent.ResponseUpdate, error] { + additionalProperties, _ = agent.GetOption(options, agent.WithAdditionalProperties) + return func(yield func(*agent.ResponseUpdate, error) bool) { + yield(&agent.ResponseUpdate{ + MessageID: "m-config", + Role: message.RoleAssistant, + Contents: message.Contents{&message.TextContent{Text: "done"}}, + }, nil) + } + }) + + h := newRequestHandler(a, a2aprovider.ExecutorConfig{}) + _, err := h.SendMessage(context.Background(), &a2a.SendMessageRequest{ + Config: config, + Message: a2a.NewMessage( + a2a.MessageRoleUser, + a2a.NewTextPart("ping"), + ), + Metadata: map[string]any{ + "key1": "value1", + }, + }) + if err != nil { + t.Fatalf("OnSendMessage returned error: %v", err) + } + + if len(additionalProperties) != 2 { + t.Fatalf("additional property count = %d, want %d", len(additionalProperties), 2) + } + if got := additionalProperties["key1"]; got != "value1" { + t.Fatalf("metadata key1 = %v, want %q", got, "value1") + } + if got := additionalProperties[a2aprovider.RequestConfigurationPropertyKey]; got != config { + t.Fatalf("forwarded config = %#v, want %#v", got, config) + } +} + +func TestRequestHandler_OnSendMessage_WhenConfigurationRequestsImmediateReturn_DoesNotOverrideRunMode(t *testing.T) { + var ( + allowBackground bool + additionalProperties map[string]any + ) + config := &a2a.SendMessageConfig{ReturnImmediately: true} + a := newHostedTestAgent(func(_ context.Context, _ []*message.Message, options ...agent.Option) iter.Seq2[*agent.ResponseUpdate, error] { + allowBackground, _ = agent.GetOption(options, agent.AllowBackgroundResponses) + additionalProperties, _ = agent.GetOption(options, agent.WithAdditionalProperties) + return func(yield func(*agent.ResponseUpdate, error) bool) { + yield(&agent.ResponseUpdate{ + MessageID: "m-run-mode", + Role: message.RoleAssistant, + Contents: message.Contents{&message.TextContent{Text: "done"}}, + }, nil) + } + }) + + h := newRequestHandler(a, a2aprovider.ExecutorConfig{}) + _, err := h.SendMessage(context.Background(), &a2a.SendMessageRequest{ + Config: config, + Message: a2a.NewMessage(a2a.MessageRoleUser, a2a.NewTextPart("ping")), + }) + if err != nil { + t.Fatalf("OnSendMessage returned error: %v", err) + } + + if allowBackground { + t.Fatal("expected AllowBackgroundResponses=false") + } + if got := additionalProperties[a2aprovider.RequestConfigurationPropertyKey]; got != config { + t.Fatalf("forwarded config = %#v, want %#v", got, config) + } +} + func TestRequestHandler_OnSendMessageContinuation_UsesStoredTaskHistoryOnly(t *testing.T) { var callCount int var continuationInputs []string From c21481582f619bd75689b65b81eb2ab05ba9ac79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 5 Sep 2026 03:48:03 +0000 Subject: [PATCH 2/2] Set conclusion job permissions Co-authored-by: michelle-clayton-work <262183035+michelle-clayton-work@users.noreply.github.com> --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e44037ed..a7dd2790 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,6 +78,7 @@ jobs: needs: [test, copilot-integration] runs-on: ubuntu-latest if: ${{ always() }} + permissions: {} steps: - name: Result run: |