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 three-method contract every Paperclip adapter must implement, what the heartbeat calls when, and how to read the TypeScript interface definition.
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>;
}
AdapterTaskThe 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
}
AdapterInvokeResultWhat 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().
AdapterRunStatusinterface 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;
}
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 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.
AdapterTask and what the heartbeat assembles