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:
openrouterApiKeyin theconfigoption (recommended for apps).OPENROUTER_API_KEYorKIMIFLARE_OPENROUTER_KEY.- 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.