A minimal, single-file interactive client for communicating with Work IQ agents using the Agent-to-Agent (A2A) protocol.
Uses the A2A .NET SDK (NuGet: A2A) for JSON-RPC transport. Sends synchronous (SendMessage) requests against the Work IQ Gateway.
Streaming responses are coming soon and not yet supported by this sample.
For the lower-level sample with no SDK (raw HTTP + JSON), see ../a2a-raw/.
The Agent-to-Agent (A2A) Protocol is an open standard for communication between AI agents. It defines JSON-RPC methods for sending messages, managing tasks, and streaming responses via Server-Sent Events.
- Microsoft 365 Copilot license on your test user.
- An Entra app registration configured with the right permissions and redirect URIs. One-time task.
- If you're the tenant admin:
# Bash ../../scripts/admin-setup.sh # PowerShell ..\..\scripts\admin-setup.ps1
- Otherwise, hand
../../ADMIN_SETUP.mdto your admin. They'll give you an App ID and Tenant ID.
- If you're the tenant admin:
- .NET 8 SDK or later — download.
dotnet run -- --token WAM --appid <APP_ID> --tenant <TENANT_ID>Type a message, see a response, type quit to exit.
Without --agent-id, the sample posts directly to the gateway endpoint (the default agent). To invoke a specific agent, pass --agent-id <id>:
dotnet run -- --token WAM --agent-id <AGENT_ID> \
--appid <APP_ID> --tenant <TENANT_ID>The sample then:
- Fetches the agent card from
{gateway}/{agent-id}/.well-known/agent-card.jsonvia the A2A SDK'sA2ACardResolver. - Reads
agentCard.urlandagentCard.namefrom the response. - Uses
agentCard.url(not the gateway endpoint) as the target forA2AClient.
Use the WorkIQ CLI to list the agents available to your signed-in user. The list command is currently behind an experimental flag:
npm install -g @microsoft/workiq # or: dotnet tool install --global WorkIQ
workiq accept-eula
workiq config set experimental=true
workiq list-agentsYou can also copy the agent ID from the address bar in the Microsoft 365 Copilot Chat website — the segment after /chat/agent/. Treat the ID as an opaque string.
Without --agent-id, the sample posts to the gateway's default agent.
dotnet run -- --token eyJ0eXAi...macOS / Linux users: WAM is only available on Windows. Use
--token <JWT>with a pre-obtained token instead.
── TOKEN ──
aud fdcc1f02-fc51-4226-8753-f668596af7f7
appid <APP_ID>
tid <TENANT_ID>
name <Your Name>
scp WorkIQAgent.Ask
expires ...
── READY — Work IQ Gateway — Sync — https://workiq.svc.cloud.microsoft/a2a/ ──
Type a message. 'quit' to exit.
You > Summarize my recent emails from alice.
Agent > You've exchanged 8 emails with Alice this week. Key threads:
- ...
Citations: 4 Annotations: 1
(2145 ms)
You > quit
If the ── TOKEN ── block shows aud matching the Work IQ Gateway and scp includes WorkIQAgent.Ask, auth is working.
| Flag | Description |
|---|---|
--token, -t |
WAM for Windows broker auth, or a pre-obtained JWT string |
--appid, -a |
Entra app client ID (required with WAM) |
--tenant, -T |
Tenant ID or domain. Required with WAM for single-tenant apps; defaults to common for multi-tenant. |
--account |
Account hint for WAM (e.g., user@contoso.com) |
--agent-id, -A |
Invoke a specific agent (fetches {gateway}/{agent-id}/.well-known/agent-card.json and posts to agentCard.url). See How to find an agent ID above. |
--show-wire |
Pretty-print raw JSON-RPC request/response bodies. Independent of --verbosity. Useful for protocol debugging. |
--header, -H |
Custom request header (repeatable) |
--show-token |
Print the raw access token (use only for diagnostics — treat tokens as sensitive) |
-v, --verbosity |
0 response only, 1 default, 2 full wire |
┌──────────────┐ JSON-RPC POST ┌──────────────────┐
│ This Sample │ ────────────────> │ Work IQ Gateway │
│ (A2A Client)│ <──────────────── │ / Agent │
└──────────────┘ AgentMessage └──────────────────┘
or AgentTask
- Auth: acquires a token via WAM or accepts a pre-obtained JWT.
- A2A Client: creates an
A2AClientfrom the A2A .NET SDK pointed at the gateway endpoint. - Send:
SendMessage(sync). - Receive: parses
AgentMessageorAgentTask; extracts text parts and citations frommetadata["attributions"]. - Multi-turn: maintains
contextIdacross turns for conversation continuity.
| A2A Capability | Status | Notes |
|---|---|---|
SendMessage (sync) |
Available | Full request/response cycle |
SendStreamingMessage (SSE) |
Coming soon | Streaming responses are not yet supported by this sample |
Multi-turn (contextId) |
Available | Conversation state maintained across turns |
Text parts (Part.FromText) |
Available | User and agent text messages |
| Citations | Available | Via Microsoft-specific metadata["attributions"] (see below) |
Agent card (/.well-known/agent.json) |
Coming soon | Connects to the gateway endpoint directly for now |
| Agent discovery / listing | Coming soon | Connects to the gateway's default agent directly for now |
Citations are delivered via a Microsoft-specific extension to the A2A protocol under AgentMessage.Metadata["attributions"]. This is not part of the A2A spec and is subject to change.
[
{
"attributionType": "Citation",
"attributionSource": "Model",
"providerDisplayName": "Q3 Planning Meeting",
"seeMoreWebUrl": "https://teams.microsoft.com/..."
}
]| Type | Meaning |
|---|---|
Citation |
Source explicitly referenced in the response text (e.g., [1]) |
Annotation |
Entity recognized in the response but not numbered |
Use -v 2 to see full citation details in the output.
if (agentMessage.Metadata?.TryGetValue("attributions", out var attrs) == true
&& attrs.ValueKind == JsonValueKind.Array)
{
foreach (var attr in attrs.EnumerateArray())
{
var name = attr.GetProperty("providerDisplayName").GetString();
var url = attr.GetProperty("seeMoreWebUrl").GetString();
Console.WriteLine($" [{name}] {url}");
}
}- This sample targets A2A v1.0 wire format: SCREAMING_SNAKE_CASE enums (
ROLE_USER,TASK_STATE_COMPLETED), flat field-presence parts (nokinddiscriminator), and named result wrappers (result.task,result.message). The method name isSendMessage. - Answer text comes back in
Artifact.Parts.Status.Messagecarries citation metadata, not the final answer text. - Citations and annotations live in
Status.Message.Metadata["attributions"]. A subsequent change will move citations to aDataParton the artifact (with media typeapplication/vnd.workiq-reference); the sample will be updated when that ships. - Streaming responses are coming soon and not yet supported by this sample.
Use -v 2 to see full HTTP request/response details:
> POST https://workiq.svc.cloud.microsoft/a2a/
Authorization: Bearer ...(3089c)
Content-Type: application/json
Body: {"jsonrpc":"2.0","id":"...","method":"SendMessage","params":{"message":{...}}}
< 200 OK
request-id: d7d0989c-...
| Package | Purpose |
|---|---|
A2A (1.0.0-preview2) |
A2A protocol client |
Microsoft.Identity.Client |
MSAL token acquisition |
Microsoft.Identity.Client.Broker |
Windows WAM broker |
System.IdentityModel.Tokens.Jwt |
JWT decoding for diagnostics |
Note: This sample uses A2A SDK v1.0.0-preview2. For the v0.3 → v1.0 wire-format and API delta, see the SDK migration guide. Work IQ also accepts v0.3 wire format via the
A2A-Version: 0.3request header.
| Symptom | Fix |
|---|---|
Empty response / [Task ... — Working] never completes |
Increase the client timeout (Timeout = TimeSpan.FromMinutes(5) by default in Program.cs). Long-running grounded queries can take 30-60s in some environments. |
401 Unauthorized |
Token audience doesn't match the gateway. Check the aud claim in the ── TOKEN ── block. |
See the root README for the full troubleshooting matrix.