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:
- Provider - An async generator that spawns the agent CLI and yields typed
StreamEventvariants - Event parser - A pure
parseLine()function that converts raw agent output intoStreamEvent[] - Schema entry - Agent type registered in shared schemas
File Structure
Each agent has a provider directory under providers/:
Step 1: Create Agent Provider
Create a new directory for your agent in packages/agents/src/providers/:
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:
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:
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:
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/:
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:
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
-
Manual testing:
# Test CLI works my-agent --version # Test with Sesame bun run dev # Create a session with your agent -
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.tsextending BaseAgentProvider withasync *execute()generator - Created
events.tswith pureparseLine()function - Created
index.tsbarrel export - Registered in agent registry
- Exported from
providers/index.ts - Updated
agentTypeEnuminpackages/shared/src/schemas/session.ts - Updated
AgentTypeenum inpackages/agents/src/types/agent.ts(both must match) - Added
networkDomainsproperty on your provider for required API domains - Added agent to
CODING_AGENTSinapps/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