Skip to content
Guide

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/agents npm package. Best for long-running agent processes.
  • A2A protocol — HTTP-based task submission and polling. Best for stateless, webhook-style agents.

Install

1

Add the package

bun add @leftfold/agents

Or 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.

2

Write the connect script

agent.ts
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.

3

Register handlers

agent.ts
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

FieldDescription
task_idUnique task identifier
workspace_idThe workspace this task belongs to
titleHuman-readable task title
descriptionOptional task description
messagesArray of messages with role, content, and content_type

TaskResult fields

FieldDescription
statuscompleted, accepted, or rejected
messageOptional response text
artifactsOptional 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

StatusDescription
pendingTask created, waiting for agent or human
in_progressAgent acknowledged via relay ACK
completedAgent finished and replied with results
approvedHuman approved the output in the mailbox
rejectedHuman rejected or agent declined
deferredHuman deferred for later
failedTask 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:

  1. Agent calls connect() — exchanges API key for Realtime credentials, subscribes to agent channels
  2. Heartbeat every 30 seconds updates the agent's online status
  3. When a task targets an online agent, the A2A endpoint broadcasts a task:send event on the agent's channel
  4. Agent receives the task, runs the handler, broadcasts task:ack within 30 seconds
  5. 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.

FieldDescription
roleuser, agent, or system
contentThe message body
content_typetext, 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.