❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️
❄️

Agent-to-Agent protocol (A2A) β€” orchestrating across agents

Agent-to-Agent protocol (A2A) β€” orchestrating across agents

Agent-to-Agent protocol (A2A) β€” orchestrating across agents

When you operate multiple agents (e.g., the Outlook Agent and the Knowledge Agent) it’s useful to formalize a small Agent-to-Agent (A2A) protocol so an orchestrator can discover capabilities, delegate tasks, and handle responses reliably. Below are design principles, message schemas, and an orchestrator sketch showing how tasks flow between agents.

Goals of the A2A protocol

  • Capability discovery: each agent advertises what actions it supports and the scopes it needs.
  • Secure delegation: orchestrator forwards tasks with the correct identity and scope, avoiding over-privilege.
  • Correlation and observability: every delegated task has a correlation id for tracing and audit.
  • Idempotency and retries: tasks include idempotency keys, and outcome semantics are explicit (sync/async, eventual consistency).

Agent capability registry (discovery)

Each agent exposes a small manifest the orchestrator can read (during startup or dynamically). Example manifest:

{
  "agent_id": "knowledge-agent",
  "name": "Knowledge Agent",
  "version": "0.1.0",
  "capabilities": [
    {"id": "search:kb", "description": "semantic search and summarization", "scopes": ["Files.Read","Mail.Read"]},
    {"id": "extract:entities", "description": "extract structured entities from docs", "scopes": ["Mail.Read"]}
  ],
  "endpoints": {"invoke": "https://localhost:8081/agent/invoke"}
}

The orchestrator keeps a capability registry (in-memory or persisted) and uses it to validate whether a target agent can perform the requested action.

Message schema (task envelope)

All A2A messages use a task envelope with three parts: header, task payload, and control metadata.

Header (JSON):

  • task_id: unique id
  • correlation_id: shared for the whole user request
  • from: orchestrator or agent id
  • to: agent id
  • timestamp
  • token: short-lived delegated token or proof-of-possession (optional depending on trust boundary)

Payload: action-specific JSON (e.g., search query, compose draft request)

Control metadata:

  • idempotency_key
  • priority
  • response_mode: sync | async | callback
  • required_scopes: list of Graph scopes the task assumes (or β€˜delegated_identity’ field)

Example envelope (pseudo):

{
  "header": {"task_id":"t-123","correlation_id":"c-999","from":"orchestrator","to":"knowledge-agent","timestamp":"..."},
  "payload": {"action":"search:kb","params":{"query":"project X architecture"}},
  "control": {"idempotency_key":"i-456","response_mode":"sync","required_scopes":["Files.Read"]}
}

Orchestration patterns

  • Push-delegate: orchestrator sends a task and waits for a result (sync) or a callback (async). Good for short operations.
  • Pull-worker: orchestrator enqueues tasks in a queue and agents pull tasks (useful when agents are scaled or offline).
  • Cascade: orchestrator asks Agent A to do work, receives result, then forwards result to Agent B (common when combining knowledge search + action like email send).
  • Parallel fan-out: orchestrator sends the same request to multiple agents (e.g., multiple summarizers) and merges results.

Security and tokens

  • Use short-lived delegated tokens when crossing trust boundaries. The orchestrator should not send its long-lived credentials to agents.
  • For intra-service calls inside a trusted backend, use mTLS or signed JWTs with minimal claims.
  • Agents must validate the token and ensure the token’s principal has permissions for the requested resource.

Orchestrator sketch (C#) β€” delegate and combine results

The following C# sketch mirrors the Python example but uses idiomatic .NET: an Envelope model, an HttpClient-based call to agents, and an orchestration flow that asks the Knowledge Agent for a summary and then asks the Outlook Agent to compose a draft. This is intentionally minimal and uses synchronous patterns via async/await.

using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

// Models
public record Header(string TaskId, string CorrelationId, string From, string To, DateTimeOffset Timestamp);
public record Control(string IdempotencyKey, string ResponseMode = "sync");
public record Envelope(Header Header, object Payload, Control Control);

public record AgentManifest(string AgentId, string Name, string Version, Endpoints Endpoints);
public record Endpoints(string Invoke);

public static class Orchestrator
{
  private static readonly HttpClient _http = new HttpClient();

  public static Envelope MakeEnvelope(string fromId, string toId, object actionPayload, string idempotency = null, string correlation = null)
  {
    var header = new Header(
      TaskId: Guid.NewGuid().ToString(),
      CorrelationId: correlation ?? Guid.NewGuid().ToString(),
      From: fromId,
      To: toId,
      Timestamp: DateTimeOffset.UtcNow
    );
    var control = new Control(idempotency ?? Guid.NewGuid().ToString());
    // default envelope payload: include action payload and optional trigger metadata
    return new Envelope(header, actionPayload, control);
  }

  public static async Task<JsonElement> CallAgentAsync(AgentManifest manifest, Envelope envelope, string bearerToken = null)
  {
    using var req = new HttpRequestMessage(HttpMethod.Post, manifest.Endpoints.Invoke);
    if (!string.IsNullOrEmpty(bearerToken))
      req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", bearerToken);
    req.Content = JsonContent.Create(envelope);

    using var resp = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
    resp.EnsureSuccessStatusCode();
    var json = await resp.Content.ReadFromJsonAsync<JsonElement>().ConfigureAwait(false);
    return json;
  }

  // Example orchestrator flow
  public static async Task<object> OrchestrateSendSummaryEmailAsync(string orchestratorId, string userIdentityToken, string query, string[] stakeholders)
  {
    // 1) Ask Knowledge Agent to summarize
    var ka = CapabilityRegistry.Get("knowledge-agent");
    // example: decide trigger and system prompt
    var triggerId = "summarize-project";
    var systemPrompt = TriggerRegistry.GetSystemPrompt(triggerId);
    var env1 = MakeEnvelope(orchestratorId, "knowledge-agent", new { action = "search:kb", @params = new { query }, meta = new { trigger_id = triggerId, system_prompt = systemPrompt } });
    var kaRes = await CallAgentAsync(ka, env1, userIdentityToken);

    // extract summary and citations (path depends on agent contract)
    var summary = kaRes.GetProperty("result").GetProperty("summary").GetString();
    var citations = kaRes.GetProperty("result").TryGetProperty("citations", out var c) ? c : default;

    // 2) Ask Outlook Agent to compose a draft using the summary
    var oa = CapabilityRegistry.Get("outlook-agent");
    var payload = new { subject = $"Summary: {query}", body = summary, to = stakeholders, citations };
    var emailTrigger = "email-draft";
    var emailPrompt = TriggerRegistry.GetSystemPrompt(emailTrigger);
    var env2 = MakeEnvelope(orchestratorId, "outlook-agent", new { action = "compose:draft", @params = payload, meta = new { trigger_id = emailTrigger, system_prompt = emailPrompt } }, correlation: env1.Header.CorrelationId);
    var oaRes = await CallAgentAsync(oa, env2, userIdentityToken);

    var draftId = oaRes.GetProperty("result").TryGetProperty("draft_id", out var d) ? d.GetString() : null;
    return new { draft_id = draftId, correlation_id = env1.Header.CorrelationId };
  }
}

// Placeholder capability registry - replace with real discovery
public static class CapabilityRegistry
{
  public static AgentManifest Get(string agentId) => agentId switch
  {
    "knowledge-agent" => new AgentManifest("knowledge-agent", "Knowledge Agent", "0.1.0", new Endpoints("https://localhost:8081/agent/invoke")),
    "outlook-agent" => new AgentManifest("outlook-agent", "Outlook Agent", "0.1.0", new Endpoints("https://localhost:8082/agent/invoke")),
    _ => throw new InvalidOperationException("Unknown agent")
  };
}

// Simple trigger registry for examples. In production store triggers in config or DB.
public static class TriggerRegistry
{
    private static readonly Dictionary<string, string> _prompts = new()
    {
        { "summarize-project", "You are a concise technical summarizer. Base answers only on the provided citations and avoid speculation." },
        { "email-draft", "You are a helpful assistant drafting a professional email; be concise and include citations where relevant." }
    };

    public static string GetSystemPrompt(string triggerId) => _prompts.TryGetValue(triggerId, out var p) ? p : null;
}

Notes:

  • userIdentityToken is a delegated access token for the end-user. In production use token-exchange or OBO flows to avoid over-scoping.
  • The orchestrator preserves the correlation id so telemetry and audit logs can be correlated end-to-end.

Triggers & system prompts

An orchestrator often exposes named triggers (events or user actions) that define the base workflow to run. Each trigger can include a default system prompt β€” a short instruction or context that sets the LLM’s behavior or the agent’s default processing pipeline for that trigger. The trigger mechanism helps standardize behavior (tone, safety checks, retrieval defaults) and makes workflows repeatable and auditable.

Trigger manifest example:

{
  "trigger_id": "summarize-project",
  "name": "Summarize Project",
  "description": "Run a knowledge search and produce a concise summary suitable for stakeholders.",
  "default_system_prompt": "You are a concise technical summarizer. Base answers only on the provided citations and avoid speculation.",
  "required_scopes": ["Files.Read","Mail.Read"],
  "run_mode": "on_demand"
}

Where to store triggers: a small trigger registry (DB table or config) that the orchestrator reads at startup or dynamically.

How triggers affect tasks:

  • The orchestrator injects the trigger_id and system_prompt into the task envelope (either header or payload metadata). Agents should honor the system_prompt as the LLM system instruction when performing semantic tasks.
  • System prompts are additive: agents can merge the trigger’s system prompt with agent-default prompts, with trigger prompt taking precedence for the duration of the task.
  • Triggers also communicate operational defaults: desired chunk size, reranker policy, citation limits, and timeouts.

Envelope extension (recommendation): add trigger_id and system_prompt to the envelope control or payload.meta so agents can pick them up easily.

Example envelope fragment with trigger/system prompt:

{
  "header": { ... },
  "payload": { "action": "search:kb", "params": {"query":"..."}, "meta": {"trigger_id":"summarize-project", "system_prompt":"You are a concise technical summarizer..."} },
  "control": { ... }
}

Agents should:

  • Validate the trigger (does it exist and is the caller allowed to use it?).
  • Use the system_prompt as the LLM system instruction or as part of a prompt template for semantic functions.
  • Log which trigger invoked the action for audit and cost accounting.

C#: MakeEnvelope with trigger/systemPrompt

Below the orchestrator sketch we’ve updated MakeEnvelope and the orchestrator flow to demonstrate how to pass a triggerId and systemPrompt into the envelope so downstream agents can use them as LLM system prompts or processing hints.

Task lifecycle, retries, and error modes

  • Agents should return structured status: { status: 'queued'|'running'|'success'|'failed', result: ..., error: { code, message }, retry_after_seconds }.
  • Use idempotency keys for operations that create side effects (send mail, create calendar event). The orchestrator can retry safely using the same idempotency key.
  • For async tasks use a callback URL or event bus; include callback_url in the control metadata.

Observability and audits

  • Emit tracing spans with correlation_id; capture input envelope (without PII), task durations, and vector-store counts used.
  • Persist an audit record for side-effectful tasks (who requested it, which agent performed it, task_id, correlation_id, result summary).

Example: why this helps

  • User asks: β€œEmail stakeholders a summary of Project X latest design decisions.” Orchestrator routes the query to Knowledge Agent, receives a grounded summary plus citations, then delegates a compose+draft task to Outlook Agent. The orchestrator then requests user confirmation before asking Outlook Agent to send the message. Each step is auditable, scoped, and can be retried safely.