SesameSesame

Agents

How to add support for new AI coding agents

This guide explains how to add support for a new AI coding agent to Sesame.

Overview

Each agent in Sesame is defined by:

  1. Provider - An async generator that spawns the agent CLI and yields typed StreamEvent variants
  2. Event parser - A pure parseLine() function that converts raw agent output into StreamEvent[]
  3. Schema entry - Agent type registered in shared schemas

File Structure

Each agent has a provider directory under providers/:

base.ts # Base agent provider class
index.ts # Provider exports
provider.ts # async *execute() generator
events.ts # parseLine() + parseEventLine()
events.test.ts # Event parser tests
index.ts # Barrel export
codex/ # OpenAI Codex agent
copilot/ # GitHub Copilot agent
gemini/ # Google Gemini agent
opencode/ # OpenCode multi-provider agent
amp/ # Sourcegraph Amp agent
mock/ # Mock agent (testing)
types # StreamEvent union, AgentExecutionContext
registry.ts # Agent registry
index.ts # Package exports

Step 1: Create Agent Provider

Create a new directory for your agent in packages/agents/src/providers/:

packages/agents/src/providers/my-agent/provider.ts
import type { StreamEvent } from "../../types/agent";
import { BaseAgentProvider } from "../base";
import type { AgentExecutionContext, AgentMetadata } from "../../types";
import { parseLine } from "./events";

export class MyAgentProvider extends BaseAgentProvider {
  readonly id = "my-agent";
  readonly name = "My Agent";
  readonly provider = "My Company";

  // Domains the agent needs to reach (allowed in sandbox)
  readonly networkDomains = ["api.my-agent.com"];

  getMetadata(): AgentMetadata {
    return {
      id: this.id,
      name: this.name,
      provider: this.provider,
      description: "AI coding agent from My Company",
      authMethods: ["api", "oauth"], // or just ["api"]
      envVars: ["MY_AGENT_API_KEY"],
    };
  }

  async *execute(
    context: AgentExecutionContext,
  ): AsyncIterable<StreamEvent> {
    const env = await this.injectCredentials(context);

    // Queue to bridge callback-based spawn with async generator
    const queue: StreamEvent[] = [];
    let resolve: (() => void) | null = null;
    let done = false;

    const push = (event: StreamEvent) => {
      queue.push(event);
      resolve?.();
    };

    // Spawn the agent CLI
    const proc = context.sandbox.spawn(
      "my-agent", ["run", context.instruction],
      {
        env,
        onStdout: (line) => {
          for (const event of parseLine(line)) {
            push(event);
          }
        },
      },
    );

    proc.then(() => { done = true; resolve?.(); });

    // Yield events as they arrive
    while (!done || queue.length > 0) {
      if (queue.length === 0) {
        await new Promise<void>((r) => { resolve = r; });
        resolve = null;
      }
      while (queue.length > 0) {
        yield queue.shift()!;
      }
    }
  }
}

Step 2: Create Index Export

Create a barrel export in your agent's directory:

packages/agents/src/providers/my-agent/index.ts
export { MyAgentProvider } from "./provider";

Step 3: Register the Agent

Add your agent to the registry in packages/agents/src/registry.ts:

import { MyAgentProvider } from "./providers/my-agent";

export const agentRegistry = {
  // ... existing agents
  "my-agent": new MyAgentProvider(),
};

And export from packages/agents/src/providers/index.ts:

export { MyAgentProvider } from "./my-agent";

Step 4: Create Event Parser

Each agent needs a pure parseLine() function that converts raw agent output lines into StreamEvent[]. Create this in your agent's events.ts:

packages/agents/src/providers/my-agent/events.ts
import type { StreamEvent } from "../../types/agent";

export function parseLine(line: string): StreamEvent[] {
  if (!line.trim()) return [];

  try {
    const data = JSON.parse(line);

    switch (data.type) {
      case "text":
        return [{ type: "text-delta", text: data.content }];
      case "tool_call":
        return [
          {
            type: "tool-start",
            toolCallId: data.id,
            toolName: data.name,
          },
          {
            type: "tool-input-delta",
            toolCallId: data.id,
            input: JSON.stringify(data.input),
          },
        ];
      case "tool_result":
        return [{
          type: "tool-result",
          toolCallId: data.id,
          output: data.output,
          isError: data.is_error,
        }];
      case "done":
        return [{ type: "message-end" }];
      default:
        return [];
    }
  } catch {
    return [];
  }
}

The parseLine() function is a pure function with no side effects — it maps raw agent JSONL lines to typed StreamEvent variants. The stream handler closure in the server handles all state tracking (text accumulation, tool lifecycle, reasoning).

Step 5: Add to Schema

Update the agent enum in packages/shared/src/schemas/session.ts:

export const agentTypeEnum = z.enum([
  "claude",
  "codex",
  "copilot",
  "gemini",
  "mock",
  "opencode",
  "amp",
  "my-agent", // Add your agent
]);

Step 6: Add UI Support

Agent Selector

Add your agent to the CODING_AGENTS array in apps/web/src/lib/agent-utils.ts:

apps/web/src/lib/agent-utils.ts
export const CODING_AGENTS = [
  { value: "claude", label: "Claude" },
  { value: "codex", label: "Codex" },
  { value: "copilot", label: "Copilot" },
  { value: "gemini", label: "Gemini" },
  { value: "opencode", label: "OpenCode" },
  { value: "amp", label: "Amp" },
  { value: "my-agent", label: "My Agent" }, // Add your agent
] as const;

Add an icon component in apps/web/src/components/logos/:

apps/web/src/components/logos/my-agent.tsx
export function MyAgent({ className }: { className?: string }) {
  return <svg className={className}>...</svg>;
}

Model Lists

Agent model lists are fetched from the public API at api.sesame.works/models. To add fallback models for your agent (used when the API is unavailable), update FALLBACK_AGENT_MODELS in apps/web/src/lib/models.ts:

apps/web/src/lib/models.ts
export const FALLBACK_AGENT_MODELS: Record<AgentType, AgentModel[]> = {
  // ... existing agents
  "my-agent": [
    { id: "my-model-pro", name: "My Model Pro", provider: "my-company", default: true },
    { id: "my-model-lite", name: "My Model Lite", provider: "my-company" },
  ],
};

For your agent's models to appear in production, they need to be added to the public API at api.sesame.works. Contact the Sesame maintainers or submit a PR to add your agent's models.

Agent Provider Interface

interface AgentProvider {
  // Identity
  readonly id: string;
  readonly name: string;
  readonly provider: string;
  readonly networkDomains: string[];

  // Metadata
  getMetadata(): AgentMetadata;

  // Execution — async generator yielding StreamEvent
  execute(context: AgentExecutionContext): AsyncIterable<StreamEvent>;
}

type StreamEvent =
  | { type: "message-start"; id: string; role: "assistant"; sessionId?: string }
  | { type: "text-delta"; text: string }
  | { type: "reasoning-delta"; text: string }
  | { type: "tool-start"; toolCallId: string; toolName: string }
  | { type: "tool-input-delta"; toolCallId: string; input: string }
  | { type: "tool-result"; toolCallId: string; output: string; isError?: boolean }
  | { type: "message-end"; usage?: { inputTokens: number; outputTokens: number } }
  | { type: "error"; message: string; code?: string };

interface AgentExecutionContext {
  sandbox: SandboxProvider;
  binPath: string;
  instruction: string;
  model?: string;
  credentials?: unknown;
  injectedEnvVars?: Record<string, string>;
  isResumed?: boolean;
  sessionId?: string;
  apiKeys?: Record<string, string>;
  cancellationSignal: AbortSignal;
}

Testing Your Agent

  1. Manual testing:

    # Test CLI works
    my-agent --version
    
    # Test with Sesame
    bun run dev
     # Create a session with your agent
  2. Unit tests:

    packages/agents/src/providers/my-agent/__tests__/provider.test.ts
    import { MyAgentProvider } from "../provider";
    
    describe("MyAgentProvider", () => {
      const provider = new MyAgentProvider();
    
      it("has valid metadata", () => {
        const metadata = provider.getMetadata();
        expect(metadata.id).toBe("my-agent");
        expect(metadata.authMethods).toContain("api");
      });
    
      it("checks auth correctly", async () => {
        const context = {
          apiKeys: { MY_AGENT_API_KEY: "test-key" },
        };
        expect(await provider.checkAuth(context as any)).toBe(true);
      });
    });

Common Patterns

API Key Authentication

For agents that only use API keys:

class MyAgentProvider extends BaseAgentProvider {
  getMetadata(): AgentMetadata {
    return {
      id: "my-agent",
      name: "My Agent",
      provider: "My Company",
      authMethods: ["api"],
      envVars: ["MY_AGENT_API_KEY"],
    };
  }

  async injectCredentials(context: AgentExecutionContext): Promise<Record<string, string>> {
    return {
      MY_AGENT_API_KEY: context.apiKeys?.MY_AGENT_API_KEY ?? "",
    };
  }
}

OAuth/Subscription Authentication

For agents with OAuth or subscription-based auth:

class MyAgentProvider extends BaseAgentProvider {
  getMetadata(): AgentMetadata {
    return {
      id: "my-agent",
      name: "My Agent",
      provider: "My Company",
      authMethods: ["api", "oauth"],
    };
  }

  async injectCredentials(context: AgentExecutionContext): Promise<Record<string, string>> {
    // Write credential file that agent reads
    if (context.credentials?.accessToken) {
      const authPath = `${context.workDir}/.my-agent/auth.json`;
      await context.sandbox.writeFile(authPath, JSON.stringify({
        access_token: context.credentials.accessToken,
        refresh_token: context.credentials.refreshToken,
        expires_at: context.credentials.expiresAt,
      }));
    }

    return {};
  }
}

Structured Output Parsing

For agents with JSON or structured output, use a parseLine() function in the generator:

async *execute(
  context: AgentExecutionContext,
): AsyncIterable<StreamEvent> {
  // ... spawn agent, bridge to queue pattern ...

  // In the onStdout callback:
  onStdout: (line) => {
    for (const event of parseLine(line)) {
      push(event);
    }
  }

  // parseLine() handles JSON parsing and mapping
  // to typed StreamEvent variants
}

Mock Agent

Sesame includes a mock agent provider for testing the full streaming pipeline without real agent CLIs or API keys. It produces deterministic StreamEvent sequences via predefined scenarios (simple text, tool calls, thinking, errors, multi-turn).

To use it: select "Mock Agent" when creating a session, and set the prompt to scenario:<name> (e.g., scenario:toolCall). See packages/agents/src/providers/mock/scenarios.ts for available scenarios.

Checklist

  • Created agent directory in providers/
  • Created provider.ts extending BaseAgentProvider with async *execute() generator
  • Created events.ts with pure parseLine() function
  • Created index.ts barrel export
  • Registered in agent registry
  • Exported from providers/index.ts
  • Updated agentTypeEnum in packages/shared/src/schemas/session.ts
  • Updated AgentType enum in packages/agents/src/types/agent.ts (both must match)
  • Added networkDomains property on your provider for required API domains
  • Added agent to CODING_AGENTS in apps/web/src/lib/agent-utils.ts
  • Added fallback models to FALLBACK_AGENT_MODELS
  • Created agent icon component
  • Tested manually (or via mock agent scenarios)
  • Added documentation

On this page