External Agents
For advanced use cases, connect your own agents to LeftFold via the Realtime relay or A2A protocol. External agents receive tasks, process them, and reply with results — just like internal agents.
Overview
LeftFold's 7 internal agents handle common enrichments (summaries, entity extraction, structured data). External agents let you extend the pipeline with custom logic — your own models, business rules, or integrations.
External agents connect via two mechanisms:
- Realtime relay — WebSocket-based, using the
@leftfold/agentsnpm package. Best for long-running agent processes. - A2A protocol — HTTP-based task submission and polling. Best for stateless, webhook-style agents.
Install
Add the package
bun add @leftfold/agentsOr with npm: npm install @leftfold/agents
Connect your agent
Call connect() with your API key. The package exchanges the key for Realtime credentials and subscribes to your workspace's agent channels automatically.
Write the connect script
import { connect } from "@leftfold/agents";
const runtime = await connect({
apiKey: process.env.LEFTFOLD_API_KEY,
agents: [
{ name: "lead", description: "Orchestrates research tasks" },
],
});
console.log("Connected — listening for tasks");Agents self-register on first connection — no dashboard setup needed. If an agent with that name already exists in your workspace, it reconnects. The url option defaults to https://leftfold.io. For local development, point it at your Supabase instance:
const runtime = await connect({
apiKey: process.env.LEFTFOLD_API_KEY,
url: "http://127.0.0.1:54321/functions/v1",
agents: [
{ name: "lead", description: "Orchestrates research" },
{ name: "researcher", description: "Deep dives" },
],
});Handle tasks
Register a handler for each agent by name. When a task arrives via the relay, your handler is called with the task payload. Return a result to respond.
Register handlers
runtime.handle("summarizer", async (task) => {
const summary = await generateSummary(task.messages);
return {
status: "completed",
message: summary,
artifacts: [
{
name: "summary",
type: "text",
data: { content: summary },
},
],
};
});IncomingTask fields
| Field | Description |
|---|---|
| task_id | Unique task identifier |
| workspace_id | The workspace this task belongs to |
| title | Human-readable task title |
| description | Optional task description |
| messages | Array of messages with role, content, and content_type |
TaskResult fields
| Field | Description |
|---|---|
| status | completed, accepted, or rejected |
| message | Optional response text |
| artifacts | Optional array of output artifacts (name, type, data) |
A2A Protocol
The A2A (Agent-to-Agent) endpoint enables stateful task communication over HTTP. Every task is stored in the mailbox — the same system used for internal agent enrichment review.
Sending a task
POST /a2a/tasks
Authorization: Bearer sk_...
Content-Type: application/json
{
"task": {
"assignee_agent_name": "entity_extractor",
"title": "Extract entities from article",
"description": "Find all people, places, and organizations.",
"messages": [
{
"role": "user",
"content": "The quick brown fox jumped over the lazy dog in London.",
"content_type": "text"
}
]
}
}Agents can be addressed by ID (assignee_agent_id) or name (assignee_agent_name). If no assignee is specified, the task goes to the workspace owner's mailbox.
Task lifecycle
| Status | Description |
|---|---|
| pending | Task created, waiting for agent or human |
| in_progress | Agent acknowledged via relay ACK |
| completed | Agent finished and replied with results |
| approved | Human approved the output in the mailbox |
| rejected | Human rejected or agent declined |
| deferred | Human deferred for later |
| failed | Task encountered an error |
Agent replies
POST /a2a/tasks/f47ac10b-.../reply
Authorization: Bearer sk_...
Content-Type: application/json
{
"message": "Found 3 entities: 2 people, 1 place.",
"content_type": "text",
"status": "completed",
"artifacts": [
{
"name": "entities",
"type": "extraction",
"data": {
"people": ["Alice", "Bob"],
"places": ["London"]
}
}
]
}The @leftfold/agents package calls this endpoint automatically after your handler returns a completed result. You only need to call it manually if building a custom HTTP-based agent.
Realtime relay
When an agent is connected via @leftfold/agents, task delivery uses Supabase Realtime broadcast channels instead of HTTP polling:
- Agent calls
connect()— exchanges API key for Realtime credentials, subscribes to agent channels - Heartbeat every 30 seconds updates the agent's online status
- When a task targets an online agent, the A2A endpoint broadcasts a
task:sendevent on the agent's channel - Agent receives the task, runs the handler, broadcasts
task:ackwithin 30 seconds - For completed tasks with artifacts, the package calls POST /tasks/:id/reply to persist results
If the agent is offline or does not ACK within 30 seconds, the task falls back to polling or the callback URL (if configured).
Messages
Each task has an ordered message thread forming a conversation. Messages have a role, content, and content type.
| Field | Description |
|---|---|
| role | user, agent, or system |
| content | The message body |
| content_type | text, markdown, or json |
Artifacts
Artifacts are tangible outputs produced by agents — summaries, JSON-LD markup, entity extractions, cross-link suggestions. Each artifact has a name, type, and data payload.
{
"name": "entity-extraction",
"type": "entities",
"data": {
"people": ["Alice", "Bob"],
"places": ["London"],
"organizations": ["Acme Corp"]
}
}Artifacts start with status proposed. In the mailbox, you approve, reject, or edit them before they reach the aggregate's projection.