Docs / Autopilot / SDK and RPC

SDK and RPC mode

Drive Autopilot from your own code with createAgentSession from autopilot-ai/sdk, or as a subprocess over JSONL on stdio.

Choose an integration

Option Use it when
SDK (autopilot-ai/sdk) Your app runs on Node.js and can import a package
RPC mode (--mode rpc) You need process isolation, or your app isn't written for Node.js
HTTP server (autopilot serve) You want long-running, unattended runs. See Running unattended.

Install

npm install autopilot-ai

Create a session

import { createAgentSession } from "autopilot-ai/sdk";

const { session } = await createAgentSession({
  cwd: "/path/to/project",
  config: {
    openrouterApiKey: process.env.OPENROUTER_API_KEY,
    model: "moonshotai/kimi-k2.7-code",
  },
});

// Stream every event
session.subscribe((event) => {
  console.log(event.type, event);
});

// Run a turn
await session.prompt("Refactor auth to JWT + Redis");

// Correct the agent while it is still running
await session.steer("Use Redis instead of an in-memory store");

// Queue another turn after the current one
await session.followUp("Also add unit tests");

// Clean up
session.dispose();

createAgentSession options

Option Type Default Description
cwd string process.cwd() Working directory for the agent
config partial config loaded from ~/.config/kimiflare/config.json Config overrides. Accepts the config keys.
sessionId string new session Resume a saved session by id
tools tool list all tools Which tools to enable
memoryEnabled boolean config value Local structured memory
lspEnabled boolean config value LSP integration
costAttribution boolean config value Cost attribution
provider object OpenRouter provider-routing preferences for this session, merged over config.openrouterProvider
permissionHandler function denies in plan mode, emits permission.request in edit mode Decide each permission request in code
enableHooks boolean false Run the user's hooks
handleProcessExit boolean true End the session if the process exits without dispose()

Session methods

Method Description
prompt(text, options?) Run a turn. Called while a turn is streaming, the text is treated as a steer.
steer(text) Inject guidance into the running turn at the next tool boundary. Ignored when no turn is running.
followUp(text) Queue a message to run as its own turn after the current turn completes successfully
abort() Stop the current turn. Pending permission prompts are denied first.
setModel(id) Change the model
setMode(mode) plan, edit or auto. Sessions start in edit.
setReasoningEffort(level) low, medium or high
resolvePermission(requestId, decision) Answer a permission.request event
subscribe(listener) Receive events. Returns an unsubscribe function.
getUsage() totalInputTokens, totalOutputTokens, totalCost, turnCount
getStatus() isStreaming, isCompacting, pendingSteer, pendingFollowUp, currentMode
save() Save the session
dispose() Release resources and end the session

prompt() options:

Option Description
images Attach images: { path } or { data, mimeType }
mode Override the mode for this prompt
maxToolIterations Override the tool-iteration limit for this prompt

Events

Event type Fields
session.start sessionId, cwd
session.end reason (complete, aborted, error), error?
message.start messageId, role
message.delta messageId, text
message.reasoning messageId, text
message.end messageId
tool.start toolCallId, toolName, args
tool.result toolCallId, toolName, result, isError
usage inputTokens, outputTokens, reasoningTokens?, cost?
permission.request requestId, toolName, args
permission.resolved requestId, decision
tasks.update tasks
warning message
hook.failed hookId, event, failure, reason, allowed, failClosed, count, message
context.compacted tokensBefore, tokensAfter, turnsRemoved, artifactsArchived
status status (idle, streaming, tool_executing, compacting, error)
worker.finished workerId, index, task, status, costUsd?

The full types are in src/sdk/types.ts.

Permission decisions

A permissionHandler or resolvePermission call returns one of:

  • "allow", "allow_session" or "deny";
  • { decision: "allow" | "deny", scope: "once" | "session" | "pattern" }.

Blocked prompts

If a UserPromptSubmit hook blocks a prompt, prompt() rejects with PromptBlockedError, which is exported from autopilot-ai/sdk.

Authentication

The SDK uses the active provider: provider in config (or AUTOPILOT_PROVIDER), otherwise OpenRouter. It needs that provider's key, from providers.<id>.apiKey in config or the provider's environment variable. A custom gateway endpoint (baseUrl and apiKey) needs no other key.

For OpenRouter, the key is resolved in this order:

  1. openrouterApiKey in the config option (recommended for apps).
  2. OPENROUTER_API_KEY or KIMIFLARE_OPENROUTER_KEY.
  3. The config file ~/.config/kimiflare/config.json.

A Requesty key (requestyApiKey, or REQUESTY_API_KEY) also works when no OpenRouter key is set.

In a desktop app, store the key in the OS keychain and pass it explicitly:

import { createAgentSession } from "autopilot-ai/sdk";

const openrouterApiKey = await keytar.getPassword("autopilot", "openrouter");

const { session } = await createAgentSession({
  cwd: projectPath,
  config: { openrouterApiKey },
});

Ending a session

Call session.dispose() when you're done. It is the reliable way to end a session: it runs SessionEnd hooks, writes the trace's session_end, and stops background subagents, LSP and memory.

As a safety net, a live session also ends if your process exits without dispose(). On beforeExit, SIGTERM, SIGINT or SIGHUP, the SDK runs SessionEnd hooks and flushes OpenTelemetry events within a 3 second budget. It never calls process.exit:

  • If your app has its own listener for the signal, the SDK starts the shutdown work and your handler decides what happens next.
  • If it doesn't, the SDK finishes the work and re-raises the signal, so the process still exits from it (status 143 for SIGTERM, 130 for SIGINT).

After an uncaught exception or an explicit process.exit(), there is no time for asynchronous work: the SDK only starts the SessionEnd hook commands. If your app handles uncaughtException itself, call dispose() from your handler.

To turn this off:

const { session } = await createAgentSession({ cwd, handleProcessExit: false });

RPC mode

RPC mode speaks JSONL over stdio: one JSON object per line on stdin, and one per line on stdout.

autopilot --mode rpc

Give the subprocess a provider key (for example OPENROUTER_API_KEY), or a custom gateway endpoint (KIMIFLARE_BASE_URL and KIMIFLARE_API_KEY).

import { spawn } from "node:child_process";

const proc = spawn("npx", ["autopilot-ai", "--mode", "rpc"], {
  cwd: projectPath,
  stdio: ["pipe", "pipe", "pipe"],
});

proc.stdout.on("data", (chunk) => {
  for (const line of chunk.toString().split("\n")) {
    if (!line.trim()) continue;
    const event = JSON.parse(line);
    console.log(event.type, event);
  }
});

proc.stdin.write(JSON.stringify({ type: "new_session" }) + "\n");
proc.stdin.write(JSON.stringify({ type: "prompt", message: "Hello" }) + "\n");

// Answer a permission request
proc.stdin.write(
  JSON.stringify({ type: "resolve_permission", requestId: "req_0", decision: "allow" }) + "\n"
);

// Resume a saved session after a restart
proc.stdin.write(JSON.stringify({ type: "new_session", sessionId: "sdk-session-..." }) + "\n");

RPC commands

Every command can carry an id, which is echoed on its response.

type Fields Response
new_session cwd?, config?, sessionId? ok with sessionId. Disposes any current session first.
prompt message, options? ok when the turn finishes, or error
steer message ok
follow_up message ok
abort ok
get_state state with status fields and usage
set_model modelId ok
set_mode mode (plan, edit, auto) ok
resolve_permission requestId, decision ok
dispose ok, then the process stops reading input

Session events are written to stdout as they happen, with the same type values as the SDK. Commands sent before new_session get {"type": "error", "error": "No active session"}. Prompts run off the read loop, so you can send resolve_permission or abort while a turn is running.