Klein Kit

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:

OptionTypeNotes
providerProviderIdRequired.
cwdstringWorking directory for the agent. Defaults to process.cwd().
modelstringProvider-native model name.
reasoningEffort"low" | "medium" | "high" | "max"Where supported.
permissionModePermissionModeDefaults to "ask" when onPermissionRequest is given, else "accept-edits".
onPermissionRequestPermissionHandlerSee Permissions.
systemPromptstringAppended to the provider's system prompt where supported.
mcpServersMcpServerConfig[]Stdio or URL MCP servers.
envRecord<string, string>Extra env for the spawned binary.
binaryPathstringOverride the CLI binary.
providerOptionsunknownPassed 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? }
  • stopReason is "completed" | "interrupted" | "error" | "limit".
  • Iterating a turn yields only that turn's events (plus any session.error), and ends on its turn.completed or turn.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).

On this page