The Adapter Interface

The three-method contract every adapter must implement: invoke(task) → runId, status(runId) → RunStatus, cancel(runId) → void. TypeScript interface definition and why it's designed this way.

The Adapter Interface

What you'll learn

The three-method contract every Paperclip adapter must implement, what the heartbeat calls when, and how to read the TypeScript interface definition.

The Contract

Every adapter in Paperclip AI implements exactly three methods:

interface Adapter {
  /**
   * Invoke the underlying model/service with the assembled task prompt.
   * Returns a run ID the heartbeat can use to poll for status.
   */
  invoke(task: AdapterTask): Promise<AdapterInvokeResult>;

  /**
   * Check the status of a previously invoked run.
   * Returns 'running', 'completed', or 'failed'.
   */
  status(runId: string): Promise<AdapterRunStatus>;

  /**
   * Cancel a running invocation.
   */
  cancel(runId: string): Promise<void>;
}

AdapterTask

The data the heartbeat assembles and passes to invoke:

interface AdapterTask {
  runId:        string;      // Unique ID for this invocation
  agentId:      string;      // Which agent is being invoked
  systemPrompt: string;      // From agents.system_prompt
  taskPrompt:   string;      // Assembled from issue title + description
  budgetUsd:    number;      // Remaining budget for this agent
  companyId:    string;      // For multi-tenant logging
  metadata:     Record<string, unknown>; // Adapter-specific extras
}

AdapterInvokeResult

What invoke returns immediately after starting work:

interface AdapterInvokeResult {
  runId:   string;  // May be same as input runId or a new external run ID
  status:  'running' | 'completed' | 'failed';
  output?: string;  // Present if synchronous adapter completed immediately
  cost?:   AdapterCost;
}

Some adapters (like claude-code) complete synchronously and return status: 'completed' immediately with output populated. Async adapters (like a long-running http endpoint) return status: 'running' and the heartbeat polls with status().

AdapterRunStatus

interface AdapterRunStatus {
  status:  'running' | 'completed' | 'failed';
  output?: string;   // Present when status is 'completed'
  error?:  string;   // Present when status is 'failed'
  cost?:   AdapterCost;
}

interface AdapterCost {
  inputTokens:  number;
  outputTokens: number;
  costUsd:      number;
  model:        string;
}

Where the Interface Lives

packages/server/src/lib/adapters/
├── interface.ts          ← the three-method contract
├── claude-code.ts        ← implementation
├── openai.ts
├── http.ts
├── shell.ts
└── factory.ts            ← creates the right adapter from adapter_type

The Factory Pattern

The heartbeat never instantiates adapters directly. It calls the factory:

// factory.ts
export function createAdapter(agent: Agent): Adapter {
  switch (agent.adapter_type) {
    case 'claude-code': return new ClaudeCodeAdapter(agent.adapter_config);
    case 'openai':      return new OpenAIAdapter(agent.adapter_config);
    case 'http':        return new HttpAdapter(agent.adapter_config);
    case 'shell':       return new ShellAdapter(agent.adapter_config);
    default: throw new Error(`Unknown adapter type: ${agent.adapter_type}`);
  }
}

Adding a new adapter means implementing the interface and adding one case to the factory.

Checkpoint ✓

Sign in for progress tracking & Q&A →