lucidAGENTS
Packages

@lucid-agents/a2a

Agent Card discovery, remote calls, and owned durable tasks.

The A2A extension adds Agent Card-shaped discovery, direct invocation, streaming clients, and an owned asynchronous task state machine. HTTP task routes appear only when a2a() is installed.

This package uses Lucid-specific /entrypoints and /tasks routes and a Lucid task model. It does not implement an official A2A v1 JSON-RPC, gRPC, or HTTP+JSON binding, does not negotiate A2A-Version, and has not passed the A2A TCK. Treat it as a Lucid interoperability profile, not blanket A2A v1 conformance.

Installation

bun add @lucid-agents/a2a @lucid-agents/core @lucid-agents/http

Enable A2A

import { a2a } from '@lucid-agents/a2a';
import { createAgent } from '@lucid-agents/core';
import { http } from '@lucid-agents/http';

const agent = await createAgent({
  name: 'worker',
  version: '1.0.0',
})
  .use(
    a2a({
      tasks: {
        maxTasks: 1_000,
        retentionMs: 24 * 60 * 60 * 1_000,
        maxRunMs: 15 * 60 * 1_000,
      },
    })
  )
  .use(http())
  .addEntrypoint({
    key: 'summarize',
    async handler({ input }) {
      return { output: { summary: String(input) } };
    },
  })
  .build();

Without a2a(), the Agent Card does not advertise task state transitions and the canonical HTTP plan omits task routes.

Runtime

type A2ARuntime = {
  buildCard(origin: string): AgentCardWithEntrypoints;
  fetchCard(baseUrl: string, fetch?: FetchFunction): Promise<AgentCard>;
  fetchCardWithEntrypoints(
    baseUrl: string,
    fetch?: FetchFunction
  ): Promise<AgentCardWithEntrypoints>;
  client: A2AClient;
  tasks: A2ATaskRuntime;
};

buildCard() builds the A2A-owned portion directly. The HTTP manifest handler runs the complete manifest contribution pipeline, adding payments, identity, AP2, and other installed capabilities. The emitted protocolVersion: "1.0" describes the card shape; it does not change the Lucid routes into an official A2A binding.

Agent Card discovery

The HTTP runtime serves cards at:

  • /.well-known/agent-card.json as the canonical route;
  • /.well-known/agent.json as a legacy compatibility alias.

fetchAgentCard() tries canonical and compatibility discovery paths. Client operations prefer the first HTTP entry in supportedInterfaces and fall back to the deprecated card url field, preserving any advertised base path.

const card = await agent.a2a.fetchCard('https://worker.example');

console.log(card.name);
console.log(card.skills);
console.log(card.supportedInterfaces);

Card parsers preserve unknown protocol fields and supported interfaces. Utility predicates are available for discovery:

import {
  findSkill,
  hasCapability,
  hasSkillTag,
  supportsPayments,
  hasTrustInfo,
} from '@lucid-agents/a2a';

const skill = findSkill(card, 'summarize');
const canStream = hasCapability(card, 'streaming');

Direct invocation

const result = await agent.a2a.client.invoke(
  card,
  'summarize',
  { text: 'Long text' },
  undefined,
  { idempotencyKey: 'summary:document-42:attempt-0001' }
);

console.log(result.output);

An idempotency key must contain 20–256 characters and is forwarded as Idempotency-Key. Reuse the same key when retrying one logical remote operation.

For a priced remote entrypoint, pass a payment-enabled Fetch implementation in the optional fetch position.

Streaming

await agent.a2a.client.stream(
  card,
  'summarize',
  { text: 'Long text' },
  event => {
    console.log(event.type, event.data);
  }
);

Fetch and invoke

const result = await agent.a2a.client.fetchAndInvoke(
  'https://worker.example',
  'summarize',
  { text: 'Long text' },
  paidFetch
);

Owned task operations

Creating a task returns an opaque ownership capability:

const access = await agent.a2a.client.sendMessage(card, 'summarize', {
  text: 'Long text',
});

// access = { taskId, accessToken, status: 'running' }
const task = await agent.a2a.client.getTask(card, access);

Keep accessToken secret. Every read, cancellation, listing, and subscription requires it. The server persists only its SHA-256 hash, and a caller with the wrong token receives the same not-found result as a caller probing an unknown task.

import { waitForTask } from '@lucid-agents/a2a';

const terminal = await waitForTask(agent.a2a.client, card, access, 30_000);

await agent.a2a.client.subscribeTask(card, access, event => {
  console.log(event.type, event.data);
});

const cancellable = await agent.a2a.client.sendMessage(card, 'summarize', {
  text: 'Cancel this work',
});
await agent.a2a.client.cancelTask(card, cancellable);

const owned = await agent.a2a.client.listTasks(card, access.accessToken, {
  status: ['running', 'completed'],
  limit: 50,
});

waitForTask() returns when the task is completed, failed, or cancelled, and throws after its deadline.

Multi-turn and owner grouping

Use contextId to associate related tasks. Reuse options.accessToken when multiple tasks should belong to the same owner and be returned by one list operation.

const first = await agent.a2a.client.sendMessage(
  card,
  'chat',
  { message: 'Hello' },
  undefined,
  { contextId: 'conversation-123' }
);

const second = await agent.a2a.client.sendMessage(
  card,
  'chat',
  { message: 'Tell me more' },
  undefined,
  {
    contextId: 'conversation-123',
    accessToken: first.accessToken,
  }
);

const conversation = await agent.a2a.client.listTasks(card, first.accessToken, {
  contextId: 'conversation-123',
});

HTTP task contract

This table documents the Lucid HTTP profile, not the official A2A v1 HTTP+JSON operation paths or message/task schemas.

With an empty HTTP base path:

OperationRouteAccess token
CreatePOST /tasksSupplied or generated.
List owned tasksGET /tasksRequired.
ReadGET /tasks/:taskIdRequired.
CancelPOST /tasks/:taskId/cancelRequired.
SubscribeGET /tasks/:taskId/subscribeRequired.

The token travels in Task-Access-Token and must contain 20–256 characters. A configured HTTP base path prefixes all task routes and is preserved in the Agent Card interface URL.

Task creation uses the same authorization transaction as direct invoke and stream. A priced or auth-only entrypoint is verified before capacity is reserved, and settlement is finalized before background execution is admitted.

Storage, leases, and shutdown

The default in-memory store is bounded and process-local. It retains at most 1,000 tasks by default, expires terminal tasks after 24 hours, evicts only terminal tasks, rejects work when active tasks exhaust capacity, and isolates subscribers by owner.

Inject a durable store for restart survival or multiple workers:

import type { TaskStore } from '@lucid-agents/types/a2a';

declare const durableStore: TaskStore;

const agent = await createAgent({
  name: 'worker',
  version: '1.0.0',
})
  .use(
    a2a({
      tasks: {
        store: durableStore,
        maxRunMs: 60_000,
      },
    })
  )
  .use(http())
  .build();

TaskStore requires atomic execution claims and fenced state transitions:

  • claimExecution() leases one running task to one worker and permits recovery only after expiry;
  • terminal compareAndSet() writes carry the execution owner ID so stale workers cannot overwrite a recovery worker;
  • creation and every transition must be durable before returning;
  • subscriptions must remain isolated by the persisted owner hash.

runtime.close() aborts local handlers and closes the task runtime/store. It leaves durable running tasks recoverable through their leases instead of marking them cancelled during shutdown.

Exports

export {
  a2a,
  createA2ARuntime,
  buildAgentCard,
  fetchAgentCard,
  fetchAgentCardWithEntrypoints,
  parseAgentCard,
  findSkill,
  hasCapability,
  hasSkillTag,
  supportsPayments,
  hasTrustInfo,
  invokeAgent,
  streamAgent,
  fetchAndInvoke,
  sendMessage,
  fetchAndSendMessage,
  getTask,
  listTasks,
  cancelTask,
  subscribeTask,
  waitForTask,
  createInMemoryTaskStore,
  createTaskRuntime,
  TaskCapacityError,
} from '@lucid-agents/a2a';

Protocol contracts such as AgentCard, A2ARuntime, A2AClient, Task, TaskAccess, TaskStore, and task request/response types are defined in @lucid-agents/types/a2a and are not re-exported.

See A2A compatibility before integrating with a third-party A2A SDK.

On this page