Migrate A2A protocol from 0.3 to 1.0-preview#4
Merged
rockfordlhotka merged 6 commits intomainfrom May 4, 2026
Merged
Conversation
Swap from Microsoft.Agents.Hosting.AspNetCore.A2A.Preview (M365 Agents
SDK lineage, A2A 0.3) to the Microsoft Agent Framework
(Microsoft.Agents.AI.Hosting.A2A.AspNetCore) plus the upstream A2A .NET
SDK (A2A, A2A.AspNetCore), which speak A2A 1.0.
The previous AgentApplication-derived handler does not exist in the new
framework, which models agents as AIAgent + tools. Since SocialAgent's
seven skills are deterministic JSON returners (not LLM tool calls), the
migration uses a custom A2A.IAgentHandler keyed under the agent name —
this bypasses the framework's default LLM-driven A2AAgentHandler and
preserves the existing dispatch semantics. A minimal AIAgent stub
(SocialAgentStubAgent) is registered alongside it solely as a name
carrier required by AddA2AServer; none of its abstract members are ever
invoked.
Endpoint surface changes:
- Discovery moves from /.well-known/agent.json (auto-emitted) to
/.well-known/agent-card.json (A2A 1.0 spec path), built explicitly
from SkillCatalog with ProtocolVersion="1.0".
- /a2a now serves both JSON-RPC (POST /a2a) and HTTP+JSON (e.g.
POST /a2a/message:send, GET /a2a/tasks/{id}) — clients pick either.
- ApiKey auth is reapplied via AuthorizationPolicyBuilder against the
mapped JSON-RPC and HTTP+JSON endpoints in non-Development env;
agent card stays anonymous so consumers can discover capabilities
before authenticating.
Skill code is unchanged behaviorally — extracted from the old
SocialAgentHandler into SkillCatalog (metadata + keyword routing),
SkillDispatcher (dispatch + analytics calls), and SocialAgentA2AHandler
(A2A bridge). LLM-assisted routing via SkillRouter remains optional and
falls back to keyword matching when LLM__Low is unconfigured.
Also bumps OpenTelemetry packages to 1.15.x patched releases to clear
pre-existing NU1902 vulnerability advisories that were blocking restore.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sets <Version>1.3.0</Version> on the host project so the agent card advertises 1.3.0 instead of the default 1.0.0.0, and pins the k8s deployment image to rockylhotka/socialagent:1.3.0 (was :latest) so rollout state is explicit and rollback is a single-line change. Image rockylhotka/socialagent:1.3.0 has been built and pushed; the :latest tag has been updated to point at the same digest. Smoke-tested on the rockbot cluster: agent card reports version 1.3.0.0 and both JSONRPC and HTTPJSON interfaces report protocolVersion 1.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The A2A v1.0 spec example shows supportedInterfaces[].url as absolute URLs (e.g. https://georoute-agent.example.com/a2a/v1) so cross-host clients can resolve the endpoint without depending on the discovery URL's host. We were emitting bare paths ("/a2a"), which works only when the client happens to resolve relative to the card URL. Adds a SocialAgent:PublicBaseUrl configuration value. When set, both JSONRPC and HTTPJSON interface URLs in the card are prefixed with it. When unset (e.g. local dev), the card falls back to the relative "/a2a" path so dotnet run still works without configuration. The k8s ConfigMap now sets PublicBaseUrl to the in-cluster service DNS (http://social-agent.rockbot.svc.cluster.local), wired into the deployment via SocialAgent__PublicBaseUrl. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dispatcher was reading only context.UserText and routing via the optional LLM SkillRouter (or keyword matcher), so when a client put the skill discriminator in message metadata — which RockBot does in its v1 SDK send-request builder (BuildV1SendRequest), per the v0.3-compatible convention — the metadata was ignored and the LLM was free to pick whatever skill it thought matched the body text. In production, with LLM__Low configured, the LLM was returning provider-status for both recent-mentions and other requests, causing both to hand back identical provider-status JSON. SocialAgentA2AHandler now reads "skill" (or "skillId") from message.metadata and request-level metadata. If the value is a known skill ID, it dispatches directly and skips routing entirely. Falls back to the existing LLM/keyword routing only when metadata is absent or unrecognized, so dotnet run -style ad-hoc text invocation still works. Smoke-tested in cluster: three skills with metadata.skill set to provider-status, recent-mentions, engagement-summary now return three distinct payload shapes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Skills previously took no parameters from the A2A request, so any
client-side filtering (e.g. \"recent-mentions for Bluesky only\") was
silently ignored — the dispatcher always called the analytics methods
with their defaults, returning the all-platforms union. The analytics
layer already supported providerId/count/since arguments; this PR wires
them through.
Convention: parameters arrive as flat keys on message.metadata,
alongside metadata.skill. Recognized keys per skill:
engagement-summary: providerId, since
top-posts: providerId, count, since
recent-mentions: providerId, count
follower-insights: providerId, count, since
platform-comparison: since
check-notifications: providerId
provider-status: (none)
Unknown keys are ignored. Strings, numbers, and ISO-8601 timestamps are
all accepted; the dispatcher coerces strings to int/date where needed.
Agent card skill descriptions updated to document the per-skill keys so
v1 clients can discover them via /.well-known/agent-card.json.
Smoke-tested in cluster:
recent-mentions, no filter: 4 items, providers: mastodon
recent-mentions, providerId=mastodon: 4 items, providers: mastodon
recent-mentions, providerId=bluesky: 0 items (no native Bluesky
mentions in the polled window)
The filter is honored — the empty Bluesky result is real, not a bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 1.3.3 metadata-parameter forwarding was hard to verify from logs:
the dispatcher logged only the skill ID, so when RockBot sent a
recent-mentions request with providerId=bluesky there was no way to
tell from the log whether the parameter arrived, was forwarded, or was
silently dropped.
Adds a parameters={...} field to the dispatch log line, summarizing
recognized keys (providerId, count, since) from message.metadata.
Unrecognized keys are intentionally omitted so the log stays compact.
Format: parameters={providerId=bluesky, count=25} or parameters={} when
no recognized keys are present.
Verified in cluster: a recent-mentions request with metadata
{providerId: \"bluesky\", count: 25} now produces:
Dispatching skill recent-mentions (from request metadata)
parameters={providerId=bluesky, count=25}
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Microsoft.Agents.Hosting.AspNetCore.A2A.Preview(M365 Agents SDK lineage, A2A 0.3) to the Microsoft Agent Framework (Microsoft.Agents.AI.Hosting.A2A.AspNetCore) plus the upstream A2A .NET SDK (A2A,A2A.AspNetCore), which speak A2A 1.0.\"protocolVersion\": \"0.3.0\"in its agent card; after this PR it will advertise\"1.0\"./a2a, ApiKey auth, the seven skill IDs, and skill JSON output are all preserved.Why this isn't a simple version bump
A2A 1.0 lives in a different package family, with a different agent abstraction and a different DI/registration shape. The old SDK's
AgentApplication-derived handler doesn't exist in the new framework — the new world models agents asAIAgent+ tools, with optional customIAgentHandlerfor full control of request processing.Approach
SocialAgent's seven skills are deterministic JSON returners, not LLM tool calls — forcing them through an
AIAgentwould have made the LLM mandatory in the request path (currently optional viaSkillRouter).So this PR uses the framework's custom-handler escape hatch:
SocialAgentA2AHandlerimplementsA2A.IAgentHandlerand is registered as a keyed singleton under the agent name\"social-agent\". The framework's defaultA2AAgentHandler(which calls into anAIAgent) is bypassed entirely.SocialAgentStubAgentis a minimalAIAgentsubclass registered alongside it. Required byAddA2AServer's wiring (it pullsAIAgentkeyed by name to read.Name), but all of its abstract members throwNotSupportedException— they are never invoked because our keyedIAgentHandlershort-circuits the path.SkillCatalogholds skill metadata (AgentSkilllist for the card; keyword + LLM router definitions).SkillDispatcherruns the existing skill logic.MapWellKnownAgentCard(agentCard)at/.well-known/agent-card.jsonwithProtocolVersion = \"1.0\"on eachAgentInterface.Endpoint surface
GET /.well-known/agent-card.jsonPOST /a2aSendMessage,GetTask, etc. — seeA2A.A2AMethods)POST /a2a/message:send,GET /a2a/tasks/{id}, …GET /health/ready,/health/liveThe old
/.well-known/agent.json(no hyphen) is gone — that was an artifact of the M365-lineage SDK's auto-emitted card. RockBot is reportedly 1.0-capable, so this should be the right path for it; if RockBot still hits the old URL it'll need a one-line update.ApiKey auth is reapplied with
AuthorizationPolicyBuilderagainst the mapped endpoint groups instead of the oldrequireAuth: boolparameter (the new SDKs don't expose that knob).Tradeoff considered
The other path was to wrap each skill as an
AIFunctiontool on a real LLM-backedAIAgent. Cleaner with the new framework's grain, but it would have:SkillRouteris currently optional with keyword fallback).Sticking with the custom-handler path keeps observable behavior identical to today's 0.3 deployment.
Smoke test (in
Development, no auth)Both transports route correctly.
[]is the expectedprovider-statusoutput when neither Mastodon nor Bluesky is configured in the smoke environment.Side cleanup
OpenTelemetry packages were bumped from 1.15.0/1.15.1 to 1.15.3/1.15.2/1.15.1 (latest available on each) to clear pre-existing NU1902 vulnerability advisories that had been silently breaking
dotnet restoreon a fresh machine. Unrelated to A2A but unavoidable to validate the build.Test plan
dotnet build SocialAgent.slnx— clean (0 warnings, 0 errors)dotnet test SocialAgent.slnx --filter \"FullyQualifiedName!~Integration\"— all unit tests passSendMessage, HTTP+JSON/message:sendall round-trip correctlyprotocolVersion: \"1.0\"; RockBot can still call skills end-to-end/.well-known/agent-card.json(the hyphenated path) and use either the JSON-RPCSendMessagemethod name or the HTTP+JSON/message:sendroute*Integration*) still need liveMASTODON_ACCESS_TOKEN/BLUESKY_*env vars to pass — not changed by this PRCaveats for reviewers
*-preview*—Microsoft.Agents.AI.Hosting.A2A.AspNetCore1.3.0-preview.260423.1,A2A/A2A.AspNetCore1.0.0-preview2. The Agent Framework itself shipped 1.0 GA but the A2A hosting subpackage is still in preview.InMemoryAgentSessionStore/InMemoryTaskStoreare unchanged — fine for our use because our custom handler doesn't use sessions or task continuations. If we ever switch to background/long-running task semantics, we'll need durable stores.[Experimental(...)]attributes flow through to consumers; we may need to suppress diagnostics if any leak in future.🤖 Generated with Claude Code