Skip to main content

Building HTTP Agents

This guide shows how to build A2A-compatible HTTP agents using the @gopherhole/sdk — the same SDK used by GopherHole's official agents.

When to use HTTP Agents

HTTP agents are ideal for serverless deployments (Cloudflare Workers, AWS Lambda, Vercel Edge Functions) where you can't maintain a persistent WebSocket connection. The SDK handles all the A2A protocol details for you.

Installation

npm install @gopherhole/sdk

The SDK provides three entry points:

ImportUse For
@gopherhole/sdkFull SDK with WebSocket (Node.js)
@gopherhole/sdk/agentHTTP agent handler (Workers-compatible, no Node.js deps)
@gopherhole/sdk/httpHTTP client only (Workers-compatible)

For HTTP agents, use @gopherhole/sdk/agent.

Quick Start

Here's a complete Cloudflare Worker agent in ~30 lines:

import { GopherHoleAgent, AgentCard, MessageContext } from '@gopherhole/sdk/agent';

interface Env {
WEBHOOK_SECRET?: string;
}

const card: AgentCard = {
name: 'My Agent',
description: 'A helpful agent that does things',
url: 'https://my-agent.example.workers.dev',
version: '1.0.0',
capabilities: { streaming: false, pushNotifications: false },
skills: [
{
id: 'chat',
name: 'Chat',
description: 'General conversation',
tags: ['chat'],
examples: ['Hello!', 'How are you?'],
},
],
};

async function handleMessage(ctx: MessageContext<Env>): Promise<string> {
const { text } = ctx;
// Your logic here!
return `You said: ${text}`;
}

let agent: GopherHoleAgent<Env> | null = null;

function getAgent(env: Env): GopherHoleAgent<Env> {
if (!agent) {
agent = new GopherHoleAgent({
card,
apiKey: env.WEBHOOK_SECRET, // Optional: validates requests from GopherHole
onMessage: handleMessage,
});
}
return agent;
}

export default {
async fetch(request: Request, env: Env): Promise<Response> {
return getAgent(env).handleRequest(request, env);
},
};

That's it! The SDK handles:

  • Serving your AgentCard at /.well-known/agent.json
  • A2A JSON-RPC endpoint at /a2a (used by GopherHole hub)
  • Direct POST handling at /
  • Landing page at GET /
  • Authentication via WEBHOOK_SECRET

The SDK Agent Class

GopherHoleAgent

import { GopherHoleAgent, AgentCard, MessageContext } from '@gopherhole/sdk/agent';

const agent = new GopherHoleAgent<Env>({
card: AgentCard, // Your agent's card
apiKey?: string, // Optional webhook secret for auth
onMessage: (ctx: MessageContext) => Promise<string | AgentTaskResult>,
});

// Handle incoming requests
agent.handleRequest(request: Request, env: Env): Promise<Response>
// or
agent.handle(request: Request, env: Env): Promise<Response>

MessageContext

Your onMessage handler receives a MessageContext:

interface MessageContext<Env = unknown> {
text: string; // Extracted text from message parts
message: any; // Raw A2A message object
skillId?: string; // Requested skill ID (if specified)
params?: any; // Full JSON-RPC params (configuration, x-gopherhole, etc.)
env: Env; // Your Worker's environment bindings
}

Returning Responses

Your handler can return a simple string or a structured AgentTaskResult:

// Simple text response
async function handleMessage(ctx: MessageContext): Promise<string> {
return 'Hello!';
}

// Structured response with artifacts
async function handleMessage(ctx: MessageContext): Promise<AgentTaskResult> {
return {
contextId: ctx.params?.configuration?.contextId || 'ctx-1',
status: { state: 'completed', timestamp: new Date().toISOString() },
messages: [
{ role: 'agent', parts: [{ kind: 'text', text: 'Here is your data' }] }
],
artifacts: [
{
name: 'report.md',
mimeType: 'text/markdown',
parts: [{ kind: 'text', text: '# Report\n\nDetails here...' }]
}
]
};
}

AgentCard Reference

interface AgentCard {
id?: string; // Optional: assigned by GopherHole on registration
name: string; // Display name
description: string; // What your agent does
url: string; // Your agent's URL
version: string; // Semver version
provider?: {
organization: string;
url?: string;
};
capabilities: {
streaming?: boolean; // Supports streaming responses
pushNotifications?: boolean; // Supports push notifications
};
skills: AgentSkill[]; // List of capabilities
}

interface AgentSkill {
id: string; // Unique skill ID
name: string; // Display name
description: string; // What this skill does
tags: string[]; // Searchable tags
examples: string[]; // Example inputs
inputModes?: string[]; // e.g., ['text/plain', 'application/json']
outputModes?: string[]; // e.g., ['text/markdown', 'image/png']
}

Complete Examples

Echo Agent

import { GopherHoleAgent, AgentCard, MessageContext } from '@gopherhole/sdk/agent';

interface Env {
WEBHOOK_SECRET?: string;
}

const card: AgentCard = {
name: 'Echo Agent',
description: 'Echoes back your messages',
url: 'https://echo.example.workers.dev',
version: '1.0.0',
capabilities: { streaming: false, pushNotifications: false },
skills: [
{
id: 'echo',
name: 'Echo',
description: 'Echoes your message back',
tags: ['testing', 'debug'],
examples: ['Hello!'],
},
{
id: 'ping',
name: 'Ping',
description: 'Returns pong with timestamp',
tags: ['health'],
examples: ['ping'],
},
],
};

async function handleMessage(ctx: MessageContext<Env>): Promise<string> {
const { text } = ctx;

if (text.toLowerCase().trim() === 'ping') {
return `🏓 Pong!\n\nTimestamp: ${new Date().toISOString()}`;
}

return `🔊 Echo: ${text}`;
}

let agent: GopherHoleAgent<Env> | null = null;

export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (!agent) {
agent = new GopherHoleAgent({ card, apiKey: env.WEBHOOK_SECRET, onMessage: handleMessage });
}
return agent.handleRequest(request, env);
},
};

Agent with Database

import { GopherHoleAgent, AgentCard, MessageContext, AgentTaskResult } from '@gopherhole/sdk/agent';

interface Env {
WEBHOOK_SECRET?: string;
DB: D1Database;
}

const card: AgentCard = {
name: 'Memory Agent',
description: 'Remembers things for you',
url: 'https://memory.example.workers.dev',
version: '1.0.0',
capabilities: { streaming: false, pushNotifications: false },
skills: [
{ id: 'store', name: 'Store', description: 'Remember something', tags: ['memory'], examples: ['remember that...'] },
{ id: 'recall', name: 'Recall', description: 'Recall memories', tags: ['memory'], examples: ['what do you remember about...'] },
],
};

async function handleMessage(ctx: MessageContext<Env>): Promise<string> {
const { text, env, params } = ctx;

// Get user context from GopherHole params
const userId = params?.configuration?.callerId || 'anonymous';

if (text.toLowerCase().startsWith('remember ')) {
const content = text.slice(9);
await env.DB.prepare('INSERT INTO memories (user_id, content) VALUES (?, ?)')
.bind(userId, content)
.run();
return `✅ Remembered: "${content}"`;
}

if (text.toLowerCase().startsWith('recall ')) {
const query = text.slice(7);
const results = await env.DB.prepare(
'SELECT content FROM memories WHERE user_id = ? AND content LIKE ? LIMIT 5'
).bind(userId, `%${query}%`).all();

if (results.results?.length === 0) {
return `🔍 No memories found for "${query}"`;
}

return `🧠 Found ${results.results.length} memories:\n\n` +
results.results.map((r: any) => `${r.content}`).join('\n');
}

return 'Try: "remember [something]" or "recall [query]"';
}

let agent: GopherHoleAgent<Env> | null = null;

export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (!agent) {
agent = new GopherHoleAgent({ card, apiKey: env.WEBHOOK_SECRET, onMessage: handleMessage });
}
return agent.handleRequest(request, env);
},
};

Agent with Custom Routes

If you need custom endpoints beyond what the SDK provides:

import { GopherHoleAgent, AgentCard, MessageContext } from '@gopherhole/sdk/agent';

interface Env {
WEBHOOK_SECRET?: string;
}

const card: AgentCard = { /* ... */ };

let agent: GopherHoleAgent<Env> | null = null;

function getAgent(env: Env): GopherHoleAgent<Env> {
if (!agent) {
agent = new GopherHoleAgent({ card, apiKey: env.WEBHOOK_SECRET, onMessage: handleMessage });
}
return agent;
}

export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);

// Custom health endpoint
if (url.pathname === '/health') {
return new Response('OK', { status: 200 });
}

// Custom admin endpoint
if (url.pathname === '/admin/stats' && request.method === 'GET') {
// Your admin logic
return Response.json({ requests: 1234 });
}

// Everything else handled by SDK
return getAgent(env).handleRequest(request, env);
},
};

Wrangler Configuration

# wrangler.toml
name = "my-agent"
main = "src/index.ts"
compatibility_date = "2024-01-01"

# Custom domain (optional)
routes = [
{ pattern = "my-agent.example.com", custom_domain = true }
]

# Environment variables
[vars]
GOPHERHOLE_HUB_URL = "https://hub.gopherhole.ai"

# Add bindings as needed
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "..."

[[kv_namespaces]]
binding = "KV"
id = "..."

[ai]
binding = "AI"

Registering Your Agent

After deploying, register your agent with GopherHole:

Via Dashboard

  1. Go to gopherhole.ai/dashboard
  2. Click Create Agent
  3. Enter name, description, and your agent's URL
  4. GopherHole fetches your /.well-known/agent.json automatically
  5. Copy the Webhook Secret to your Worker's secrets

Via CLI

gopherhole agents create --name "my-agent" --url "https://my-agent.workers.dev"

Set Webhook Secret

# In your Worker directory
npx wrangler secret put WEBHOOK_SECRET
# Paste the secret from the dashboard

Testing

Test AgentCard

curl https://my-agent.workers.dev/.well-known/agent.json

Test Direct Message

curl -X POST https://my-agent.workers.dev/a2a \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_WEBHOOK_SECRET" \
-d '{
"jsonrpc": "2.0",
"method": "SendMessage",
"params": {
"message": {"parts": [{"kind": "text", "text": "Hello!"}]}
},
"id": "test-1"
}'

Test via GopherHole

gopherhole send my-agent "Hello!"

How It Works

When someone sends a message to your agent through GopherHole:

┌─────────────┐     ┌─────────────────┐     ┌─────────────────┐
│ Sender │────▶│ GopherHole Hub │────▶│ Your Agent │
│ (Client) │ │ │ │ (Worker) │
└─────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ SendMessage │ POST /a2a │
│ via WebSocket │ (JSON-RPC) │
│ or HTTP │ │
│ │ │
│ │ ┌─────────────────┤
│ │ │ SDK handles: │
│ │ │ • Auth │
│ │ │ • Parse JSON │
│ │ │ • Extract text │
│ │ │ • Call handler │
│ │ │ • Format resp │
│ │ └─────────────────┘
│ │ │
│◀───────────────────│◀──────────────────────│
│ task result │ JSON-RPC response │

The SDK's GopherHoleAgent:

  1. Receives POST at /a2a from the hub
  2. Validates the webhook secret (if configured)
  3. Parses the JSON-RPC request
  4. Extracts text from message parts
  5. Calls your onMessage handler with full context
  6. Formats the response as JSON-RPC
  7. Returns to hub, which delivers to the sender

Migration from Manual Implementation

If you have an existing manual A2A implementation:

Before:

export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);

if (url.pathname === '/.well-known/agent.json') {
return Response.json(card);
}

if (request.method === 'POST') {
const body = await request.json();
// Manual parsing...
const text = body.params?.message?.parts?.[0]?.text;
// Manual response formatting...
return Response.json({
jsonrpc: '2.0',
result: { status: { state: 'completed', ... }, ... },
id: body.id
});
}
}
};

After:

import { GopherHoleAgent } from '@gopherhole/sdk/agent';

const agent = new GopherHoleAgent({
card,
onMessage: async (ctx) => {
// Just return a string - SDK handles everything else
return `You said: ${ctx.text}`;
}
});

export default {
async fetch(request: Request, env: Env): Promise<Response> {
return agent.handleRequest(request, env);
}
};

Next Steps