Lucid multi-service task example
Compose Agent Card-shaped discovery with Lucid-owned HTTP task operations.
This example demonstrates how Lucid services discover cards and compose Lucid HTTP task operations.
The example does not use an official A2A v1 JSON-RPC, gRPC, or HTTP+JSON
binding. /entrypoints, /tasks, the task states, message.content, access
token, and SSE events are Lucid contracts. A2A-Version and the A2A TCK are
not implemented.
Full integration (three Lucid services)
File: packages/examples/src/a2a/full-integration.ts
This comprehensive example demonstrates the facilitator pattern where an agent acts as both server and client:
Agent 3 (Client) → Agent 2 (Facilitator) → Agent 1 (Worker)
↓
Results flow backArchitecture
| Agent | Role | Description |
|---|---|---|
| Agent 1 | Worker | Does the actual work (echo, process, stream) |
| Agent 2 | Facilitator | Receives calls, delegates to Agent 1, returns results |
| Agent 3 | Client | Initiates requests to Agent 2 |
The code
import {
a2a,
fetchAgentCard,
findSkill,
hasCapability,
supportsPayments,
waitForTask,
} from '@lucid-agents/a2a';
import { createAgent } from '@lucid-agents/core';
import { createAgentApp } from '@lucid-agents/hono';
import { http } from '@lucid-agents/http';
import { z } from 'zod';
// Agent 1: Worker
const agent1 = await createAgent({
name: 'worker-agent',
version: '1.0.0',
description: 'Worker agent that processes tasks',
})
.use(http())
.use(a2a())
.build();
const { app: app1, addEntrypoint: addEntrypoint1 } =
await createAgentApp(agent1);
addEntrypoint1({
key: 'echo',
description: 'Echoes back the input text',
input: z.object({ text: z.string() }),
output: z.object({ text: z.string() }),
handler: async ctx => {
console.log(`[Agent 1] echo called with: "${ctx.input.text}"`);
return {
output: { text: `Echo: ${ctx.input.text}` },
};
},
});
// Agent 2: Facilitator (both server AND client)
const agent2 = await createAgent({
name: 'facilitator-agent',
version: '1.0.0',
description: 'Facilitator that proxies to worker',
})
.use(http())
.use(a2a())
.build();
const {
app: app2,
addEntrypoint: addEntrypoint2,
runtime: runtime2,
} = await createAgentApp(agent2);
// Get the Lucid Agent Card/task client from the facilitator runtime
const a2aClient = runtime2.a2a;
addEntrypoint2({
key: 'echo',
description: 'Proxies echo requests to worker agent',
input: z.object({ text: z.string() }),
output: z.object({ text: z.string() }),
handler: async ctx => {
console.log(`[Agent 2] Received request, forwarding to Agent 1`);
// Fetch Agent 1's card
const agent1Card = await a2aClient.fetchCard('http://localhost:8787');
// Create task on Agent 1
const taskAccess = await a2aClient.client.sendMessage(agent1Card, 'echo', {
text: ctx.input.text,
});
// Wait for result
const task = await waitForTask(a2aClient.client, agent1Card, taskAccess);
if (task.status === 'failed') {
throw new Error(task.error?.message || 'Task failed');
}
// Validate output with Zod (runtime safety)
const outputSchema = z.object({ text: z.string() });
const validatedOutput = outputSchema.parse(task.result?.output);
return { output: validatedOutput };
},
});
// Agent 3: Client (no HTTP server needed)
const agent3 = await createAgent({
name: 'client-agent',
version: '1.0.0',
})
.use(a2a())
.build();
const a2a3 = agent3.a2a;
// Call Agent 2, which calls Agent 1
const card2 = await a2a3.fetchCard('http://localhost:8788');
const taskAccess = await a2a3.client.sendMessage(card2, 'echo', {
text: 'Hello from Agent 3!',
});
const result = await waitForTask(a2a3.client, card2, taskAccess);
console.log('Final result:', result.result?.output);
// Output: { text: "Echo: Hello from Agent 3!" }Lucid profile patterns
Agent Card-shaped discovery
Fetch another agent's capabilities at runtime:
const card = await fetchAgentCard('https://other-agent.com');
console.log(card.name); // Agent name
console.log(card.skills); // Available entrypoints
console.log(hasCapability(card, 'streaming')); // Check capabilities
console.log(supportsPayments(card)); // Check payment supportTask-based operations
Create a task and poll for results:
// Create task (returns immediately)
const taskAccess = await client.sendMessage(card, 'skillId', input);
// taskAccess = { taskId, accessToken, status: 'running' }
// Wait for completion
const task = await waitForTask(client, card, taskAccess);
if (task.status === 'completed') {
console.log(task.result?.output);
} else if (task.status === 'failed') {
console.error(task.error?.message);
}Multi-turn conversations
Group related tasks with contextId:
const contextId = `conversation-${Date.now()}`;
// First message establishes the owner capability
const first = await client.sendMessage(
card,
'chat',
{ text: 'Hello' },
undefined,
{ contextId }
);
// Second message reuses the same owner and conversation
await client.sendMessage(card, 'chat', { text: 'Tell me more' }, undefined, {
contextId,
accessToken: first.accessToken,
});
// List all messages in conversation
const { tasks } = await client.listTasks(card, first.accessToken, {
contextId,
});Task cancellation
Cancel a running task:
try {
const cancelled = await client.cancelTask(card, taskAccess);
console.log('Cancelled:', cancelled.status); // 'cancelled'
} catch (error) {
// Task may have already completed
const task = await client.getTask(card, taskAccess);
console.log('Task status:', task.status);
}Validating remote output
Always validate output from remote agents:
const task = await waitForTask(client, card, taskAccess);
// Don't trust remote data - validate with Zod
const outputSchema = z.object({ result: z.number() });
const validated = outputSchema.parse(task.result?.output);Running the example
bun run packages/examples/src/a2a/full-integration.tsOutput
STEP 1: Creating Agent 1 (Worker Agent)
Agent 1 running at: http://localhost:8787
STEP 2: Creating Agent 2 (Facilitator Agent)
Agent 2 running at: http://localhost:8788
STEP 3: Creating Agent 3 (Client Agent)
Agent 3 ready
STEP 6: A2A Composition (Agent 3 -> Agent 2 -> Agent 1)
[Agent 2] Received request, forwarding to Agent 1
[Agent 1] echo called with: "Hello from Agent 3!"
Final result at Agent 3: {"text":"Echo: Hello from Agent 3!"}What this proves—and does not
The example proves card lookup, Lucid task ownership, polling, composition, and remote-output validation across three local services. It does not prove A2A v1 interoperability, marketplace publication, production load balancing, durable task recovery, payment settlement, or trust in a discovered origin.
For production, add an origin/SSRF allowlist, durable TaskStore, task-token
secret handling, target idempotency, payment policy, timeouts, and trace
correlation. See A2A compatibility and the
@lucid-agents/a2a reference.