Skip to content
Night Owls.dev
Jump to a page

@nightowlsdev/engine-a2a

EngineAdapter, reduced governance

The A2A (Agent2Agent) protocol adapter engine, renders any A2A v0.3-wire endpoint (Bedrock AgentCore, Azure AI Foundry/Copilot Studio, Google ADK/Vertex Agent Engine) as a single Night Owls agent. Adapter tier: reduced governance, tier-1 events (opaque remote, no tool visibility).

What it does

**Use this when:** you already run an agent on a standards-based A2A (Agent2Agent) endpoint — Amazon Bedrock AgentCore, Azure AI Foundry / Microsoft Copilot Studio, or Google ADK/Vertex Agent Engine — and want to front it as a single agent inside a Night Owls swarm without re-implementing it. v1 speaks the **A2A v0.3 wire** (the version every shipping A2A SDK/runtime actually talks today; the v0.3-wire endpoint is what Bedrock/Azure/Google expose — the v1.0-spec branch is scaffolded but spike-gated and not shipped). One client covers all three hyperscaler runtimes; the only per-target work is auth. **How.** `a2aEngine({ endpoint, auth?, agentCard?, dialect? })` fetches the remote's Agent Card and speaks the A2A v0.3 wire under the hood. **Implemented auth in v1:** static headers, bearer tokens, and API keys. `sigV4Auth` (Bedrock) and `oauth2ClientCredentials` (Copilot Studio / Vertex / Azure Entra) are **exported as spike stubs that throw `not yet implemented`** — the seams exist but are not functional yet, so wire your own header strategy for those until they land. Then `defineSwarm({ engine })` or `owl install engine-a2a`. **What it can and can't do (adapter tier).** The loop runs on the remote, so the in-process governance plane can't reach it: no fail-closed tool gate, no cost caps, no pre-generation veto, and `events.tier: 1` — message/status/HITL only, because A2A agents are opaque and expose no tool-call granularity (this is the one engine where you can't see the remote's tool calls; `engine-eve` and `engine-trigger-chat` are tier-2). What DOES cross the boundary: HITL — A2A's `input-required`/`auth-required` states map onto the normal ask/answer loop — and durable resume, which is native to A2A (the remote owns the `taskId`/`contextId`), independent of your own storage. Engine-wall clean: peers `@nightowlsdev/core` and bundles `@a2a-js/sdk` as a direct dependency.

Install

pnpm add @nightowlsdev/engine-a2a

Key exports

  • a2aEngine
  • A2aEngine
  • A2A_ENGINE_CAPABILITIES
  • createA2aTransport
  • noneAuth / staticHeaders / bearerToken / apiKey / sigV4Auth / oauth2ClientCredentials
  • mapTaskEvent
  • nightOwlsPlugin

Usage

engine-a2a.ts
import { defineSwarm } from "@nightowlsdev/core";
import { a2aEngine, A2A_ENGINE_CAPABILITIES } from "@nightowlsdev/engine-a2a";

// An ADAPTER, not a native engine, renders any A2A v0.3-wire endpoint (Bedrock AgentCore, Azure AI
// Foundry/Copilot Studio, Google ADK/Vertex Agent Engine) as one opaque Night Owls agent.
const swarm = defineSwarm({ agents, engine: a2aEngine({ endpoint: "https://agent.example.com" }) });

// Reduced-governance adapter tier, our pre-generation/tool gate can't reach a remote we don't run.
console.log(A2A_ENGINE_CAPABILITIES.kind); // "adapter"
console.log(A2A_ENGINE_CAPABILITIES.governance.preToolCall); // "none", nothing to gate (opaque remote)

What it provides

engine-a2a is a protocol ADAPTER, not a native engine: it renders any A2A (Agent2Agent) endpoint as a single, opaque Night Owls agent. One client covers three hyperscaler runtimes — Amazon Bedrock AgentCore, Azure AI Foundry / Microsoft Copilot Studio, and Google ADK / Vertex Agent Engine — for near-zero marginal engineering beyond auth wiring. It fetches the remote's Agent Card and speaks the A2A wire underneath (@a2a-js/sdk); because the remote agent is opaque, the capability descriptor is honestly reduced: kind adapter, tier-1 events (message + status + HITL only, no tool granularity), and governance all off.

When to use it

  • You need to call an A2A endpoint deployed on Bedrock AgentCore, Azure AI Foundry / Copilot Studio, or Google ADK / Vertex Agent Engine and surface it as one Night Owls agent.
  • You want one wire and one auth story across those hyperscaler runtimes instead of three bespoke SDK integrations.
  • The remote already owns its own tools, generation, and durability, and you only need to relay its messages, status, and input-required asks into a Night Owls run.

When not to

  • You need tool visibility — A2A agents are opaque, so there are NO tool_call/tool_result events (tier 1); use engine-eve or engine-trigger-chat for a remote with tool granularity.
  • You need Night Owls' governance to actually enforce — the pre-generation veto, fail-closed tool gate, and cost caps physically cannot reach a remote you don't execute inside of (governance all false/'none').
  • You need per-tool approval, rules injection, scratchpad, delegation, or usage/token metering — none exist across the opaque A2A boundary (approval false, rules false, no swarm.usage).

Alternatives

  • A native engine (engine-mastra / engine-ai-sdk / engine-openai-agents)You control the loop and want the full governance plane, tool visibility, and (mastra) delegation/workflows — run in-process instead of adapting a remote.
  • engine-eve / engine-trigger-chatYour remote loop exposes tool events (tier 2) and you want that richer visibility than A2A's opaque tier-1 surface.
  • The @a2a-js/sdk directlyYou want to speak A2A without the Night Owls event/HITL mapping, storage-backed history, or the swappable-engine contract.

Strengths

  • One adapter, three hyperscaler runtimes — Bedrock AgentCore, Azure/Copilot Studio, Google ADK/Vertex — behind a single client with just auth to wire.
  • Auth strategies: noneAuth, staticHeaders, bearerToken, and apiKey work today; sigV4Auth (Bedrock) and oauth2ClientCredentials (Copilot Studio / Vertex / Azure Entra) are exported as spike stubs that throw 'not yet implemented' in v1 — the seams exist, wire your own header strategy for those until they land.
  • durableResume is STATICALLY true — A2A tasks are durable and resumable by the remote via taskId/contextId, independent of what your storage persists.
  • Honest by construction: the descriptor never claims governance or tool visibility the opaque wire can't back, and it's engine-wall clean (peers @nightowlsdev/core; bundles @a2a-js/sdk as a direct dependency).

Limits & trade-offs

  • Reduced-governance adapter tier: governance is all false/'none' — no pre-generation veto, no fail-closed tool gate, no cost kill-switch reach into the remote.
  • Tier-1 events only: message + status + HITL; no tool_call/tool_result (opaque agents) and no swarm.usage (A2A carries no token data — swarm.turn_usage fires once with an empty breakdown).
  • No per-tool approval (approval:false), no rules injection, no scratchpad, no delegation lane, and no OTel telemetry (metering is turn-usage only).
  • The shipping wire dialect is v0.3 (the only wire current SDKs speak); the v1.0 dialect is scaffolded but SPIKE-GATED — do not pass it in production. Cancellation is advisory (between-steps), with no guaranteed mid-stream remote abort.

How it works

a2aEngine({ endpoint, auth?, agentCard?, dialect? }) builds a real @a2a-js/sdk transport (or takes a test transport override) and returns (opts) => new A2aEngine(opts, cfg). At run time it fetches the remote's Agent Card (from .well-known/agent-card.json unless overridden) and speaks the A2A wire, mapping task events into SwarmEvents. A2A's input-required / auth-required task states map onto the normal ask / suspend / resume loop; there is no per-tool approval surface to gate, so hitl.approval is false. hitl.durableResume is static true because the remote owns task durability. Reads/history come from OUR persisted event log, not the remote.

Examples

Adapt a remote A2A endpoint with a bearer token

One factory renders the remote A2A agent as a single opaque Night Owls agent. bearerToken/apiKey/staticHeaders are the functional strategies in v1; sigV4Auth (Bedrock) and oauth2ClientCredentials are exported but throw 'not yet implemented' — until they land, front a SigV4/OAuth endpoint with a gateway that accepts a bearer/API key, or supply your own signed headers via staticHeaders.

engine-a2a-example-1.ts
import { defineSwarm } from "@nightowlsdev/core";
import { a2aEngine, bearerToken } from "@nightowlsdev/engine-a2a";

export default defineSwarm({
  agents,
  engine: a2aEngine({
    endpoint: "https://agent.example.com",
    auth: bearerToken(() => process.env.A2A_BEARER_TOKEN!),
  }),
});

Confirm the reduced-governance adapter tier

The descriptor is honest that an opaque remote leaves nothing for us to gate — check it before assuming governance applies.

engine-a2a-example-2.ts
import { A2A_ENGINE_CAPABILITIES } from "@nightowlsdev/engine-a2a";

console.log(A2A_ENGINE_CAPABILITIES.kind); // "adapter"
console.log(A2A_ENGINE_CAPABILITIES.events.tier); // 1 — no tool events
console.log(A2A_ENGINE_CAPABILITIES.governance.preToolCall); // "none"
console.log(A2A_ENGINE_CAPABILITIES.hitl.durableResume); // true (static — the remote owns it)

Doing the parts it doesn't support

  • Governing the remote's tool callsYou can't — A2A agents are opaque, so there are no tool events to gate and governance is off. If you need the fail-closed tool gate and cost caps, run a native engine in-process, or move governance to the boundary the remote itself enforces.
  • Seeing what tools the remote ranNot available at tier 1. Use engine-eve or engine-trigger-chat (tier 2, tool events visible) if your remote loop can expose actions/tool results.
  • Injecting rules / soft policy into the remoterules is false — you can't compose Night Owls' advise/enforce prompt into a remote opaque agent. Configure policy on the remote's own side (its system prompt / guardrails).

Related

  • coreThe required base — the adapter peers core (its only peer) and bundles @a2a-js/sdk, mapping into core's SwarmEvent / ask loop.
  • engine-eveThe other remote adapter with a richer tier-2 surface (tool events + usage visible).
  • engine-mastraThe full-governance native default, for when you control the loop instead of adapting a remote.
  • reactuseEngineCapabilities() reads A2A_ENGINE_CAPABILITIES to hide affordances the opaque tier-1 wire can't back.