Most new agent runtimes fit into the Process adapter or the HTTP adapter without any code. Before writing a custom adapter, check whether one of those two will do. If your runtime:
  • Reads/writes JSON lines on stdio → use Process
  • Exposes HTTP with SSE → use HTTP
  • Has its own weird protocol, bespoke event shapes, or needs custom tool translation → write a custom adapter
This guide is for the third case.

What the SDK gives you

Adapters inherit from one of two base classes in packages/adapters/sdk/:
  • ProcessAdapter — for adapters that launch a local binary
  • HttpAdapter — for adapters that hold a session with a remote
The base class handles:
  • Workspace setup
  • Env var and argument templating
  • Process group isolation (detached: true, negative PID signaling)
  • Lease renewal loop
  • Stdio/stream parsing skeleton
  • Shutdown sequencing (SIGTERM → grace → SIGKILL)
  • Retry and timeout handling
  • Error classification (crash, timeout, lease_expired, kill, auth)
You override three or four methods to describe your runtime:
  • launch() — how to start the process or session
  • onOutput() — how to interpret output events
  • onToolResult() — how to pass tool results back
  • extractCost() — optional, how to pull token/cost data out of your runtime’s events

Minimum shape of a custom adapter

import { ProcessAdapter, RunContext, Event } from "@company-agents/adapters-sdk";

export class MyAdapter extends ProcessAdapter {
  readonly name = "my-runtime";
  readonly kind = "process";

  launch(ctx: RunContext) {
    return this.spawn({
      bin: "my-runtime",
      args: [
        "--task", ctx.taskId,
        "--lease", ctx.leaseToken,
        "--mcp", ctx.mcpUrl,
      ],
      env: {
        MY_RUNTIME_API_KEY: process.env.MY_RUNTIME_API_KEY!,
      },
      cwd: ctx.workspacePath,
    });
  }

  onOutput(line: string, ctx: RunContext): Event | null {
    // Parse your runtime's output format and return a normalized event.
    if (line.startsWith("PROGRESS:")) {
      return { type: "progress", summary: line.slice(9).trim() };
    }
    if (line.startsWith("TOOL:")) {
      const parsed = JSON.parse(line.slice(5));
      return { type: "tool_call", id: parsed.id, tool: parsed.tool, args: parsed.args };
    }
    if (line.startsWith("DONE:")) {
      return { type: "complete", output: JSON.parse(line.slice(5)) };
    }
    return null;
  }

  onToolResult(result: ToolResult, ctx: RunContext) {
    // Send the tool result back to the runtime in its expected format.
    this.writeToStdin(`RESULT: ${JSON.stringify(result)}\n`);
  }

  extractCost(event: Event): Cost | null {
    if (event.type === "complete" && event.output.tokens) {
      return {
        model: "my-runtime-v1",
        inputTokens: event.output.tokens.in,
        outputTokens: event.output.tokens.out,
      };
    }
    return null;
  }
}
Register the adapter in the adapter index:
// packages/adapters/src/index.ts
import { MyAdapter } from "./my-runtime";

export const adapters = {
  "claude-code": new ClaudeCodeAdapter(),
  "codex": new CodexAdapter(),
  // ...
  "my-runtime": new MyAdapter(),
};
That is it. The orchestrator picks up the new adapter and agents can be hired against it.

Testing your adapter

The SDK ships with a test harness that runs a mock orchestrator, a mock MCP server, and a fake task, then asserts that your adapter:
  • Launches the runtime with the right argv and env
  • Translates output events correctly
  • Calls the MCP server for tool calls
  • Renews the lease on schedule
  • Shuts down cleanly on a forced kill
import { AdapterTestHarness } from "@company-agents/adapters-sdk/testing";
import { MyAdapter } from "../src/my-runtime";

test("my adapter handles a full run", async () => {
  const harness = new AdapterTestHarness(new MyAdapter());
  const result = await harness.runFakeTask({
    scriptedOutput: [
      "PROGRESS: starting",
      'TOOL: {"id":"1","tool":"read_task","args":{}}',
      'DONE: {"ok":true}',
    ],
  });
  expect(result.status).toBe("complete");
  expect(result.toolCallCount).toBe(1);
});
Run with:
pnpm --filter @company-agents/adapters test

Publishing an adapter

Adapters live inside the monorepo in packages/adapters/. To add one:
  1. pnpm --filter @company-agents/adapters create-adapter my-runtime
  2. Edit the generated file to implement the four methods
  3. Add tests
  4. Run pnpm --filter @company-agents/adapters test
  5. Open a PR
If your adapter depends on a runtime that is not free to use (API key, licensed binary), document the prerequisites in a README next to the adapter source. The dashboard’s adapter picker reads that README and surfaces the prerequisites on the hiring form.

Out-of-tree adapters

You can also publish an adapter as its own npm package and point config.json at it:
{
  "adapters": {
    "third-party": {
      "type": "module",
      "module": "@acme/company-agents-adapter-acme",
      "config": {
        "apiKey": "${ACME_API_KEY}"
      }
    }
  }
}
The orchestrator dynamically imports the module at startup and registers it. The module must export a default class implementing the adapter interface. Out-of-tree adapters are the right choice for:
  • Commercial runtimes with their own release cadence
  • Company-internal runtimes that should not live in the public monorepo
  • Experimental adapters you do not want to pin to a Company Agents release

What to keep an eye on

A few rules of thumb from the adapters that have already been written:
  1. Do not trust the runtime to clean up its own children. Process groups are your friend. Use the SDK’s group kill helper, do not roll your own.
  2. Do not retry inside the adapter. The orchestrator handles retries. If the run fails, fail it cleanly and let the orchestrator decide.
  3. Do not cache tool results inside the adapter. Tool results are metered. Caching them is a silent bill dodge.
  4. Do not swallow stderr. Pass it up to the log collector. The orchestrator decides what to show the user.
  5. Do not hold onto the master key. The adapter should not even see it. Secrets come through the MCP server at call time.

Next