Sessions & turns
createAgent, the session API, and the Turn lifecycle
Creating a session
import { createAgent } from "@klein-kit/core";
const session = await createAgent({
provider: "claude", // "codex" | "claude" | "opencode" | "cursor" | "pi"
cwd: "/path/to/repo", // defaults to process.cwd()
model: "opus", // optional, provider-native model name
permissionMode: "ask",
systemPrompt: "Prefer small, focused diffs.",
});CreateAgentOptions extends SessionOptions:
| Option | Type | Notes |
|---|---|---|
provider | ProviderId | Required. |
cwd | string | Working directory for the agent. Defaults to process.cwd(). |
model | string | Provider-native model name. |
reasoningEffort | "low" | "medium" | "high" | "max" | Where supported. |
permissionMode | PermissionMode | Defaults to "ask" when onPermissionRequest is given, else "accept-edits". |
onPermissionRequest | PermissionHandler | See Permissions. |
systemPrompt | string | Appended to the provider's system prompt where supported. |
mcpServers | McpServerConfig[] | Stdio or URL MCP servers. |
env | Record<string, string> | Extra env for the spawned binary. |
binaryPath | string | Override the CLI binary. |
providerOptions | unknown | Passed through to the adapter untranslated. |
The AgentSession interface
interface AgentSession extends AsyncIterable<AgentEvent> {
readonly provider: ProviderId;
readonly id: string; // klein-kit process-local uuid
readonly nativeSessionId: string | undefined; // backend id — persist this for resume
readonly capabilities: ProviderCapabilities;
readonly taskState: TaskState;
readonly usage: Usage;
prompt(input: UserInput, options?: TurnOptions): Turn;
readonly queuedTurns: readonly QueuedTurn[];
cancelQueued(turnId: string): boolean;
respond(requestId: string, response: PermissionResponse): Promise<void>;
interrupt(): Promise<void>;
setModel(model: string): Promise<void>;
setPermissionMode(mode: PermissionMode): Promise<void>;
on<T extends AgentEventType>(type: T, listener: (event: AgentEventOf<T>) => void): () => void;
raw<T = unknown>(): T;
close(): Promise<void>;
}setModel and setPermissionMode throw CapabilityUnsupportedError on providers where mid-session changes aren't possible — check session.capabilities first.
Turns
session.prompt() returns a Turn — both an async iterable of events and a promise of the final result:
const turn = session.prompt("add input validation to the signup form");
// Stream it…
for await (const event of turn) { /* … */ }
// …or just await the outcome:
const result = await turn;
// { turnId, text, stopReason, usage?, filesChanged, raw? }stopReasonis"completed" | "interrupted" | "error" | "limit".- Iterating a turn yields only that turn's events (plus any
session.error), and ends on itsturn.completedorturn.failed. - Awaiting a failed turn rejects with
TurnFailedError. turn.interrupt()stops a running turn natively; on a queued turn it just dequeues it.
Prompts accept rich input:
session.prompt([
{ type: "text", text: "Fix the layout bug shown here:" },
{ type: "image", path: "./screenshot.png" },
{ type: "file", path: "./notes.md" },
]);Per-turn overrides: session.prompt(input, { model, reasoningEffort, providerOptions }).
Cleanup
await session.close() settles the active and queued turns, then emits session.ended with reason: "closed". Always close sessions — each one owns a child process.
Errors
All Klein Kit errors extend KleinError with a code:
PROVIDER_NOT_INSTALLED · PROVIDER_NOT_AUTHENTICATED · CAPABILITY_UNSUPPORTED · SESSION_CLOSED · TURN_FAILED · PROTOCOL_ERROR
Concrete classes: ProviderNotInstalledError, ProviderNotAuthenticatedError, CapabilityUnsupportedError, SessionClosedError, TurnFailedError, ProtocolError (which carries the offending payload on .raw).