Droid TypeScript SDK

Use the Factory Droid SDK for TypeScript to build custom agents, workflows, and product integrations.

The Droid SDK lets you run the same agent harness that powers Factory's CLI, desktop application, and web platform from your own code. It manages conversation context, tool execution, permissions, streaming, model selection, and multi-step execution.

Your application decides when Droid runs, which tools and models it can use, and what happens to the result.

Overview

The TypeScript SDK can start Droid as a Node.js subprocess or connect to an existing daemon. It provides one-shot runs, persistent sessions, streaming, local session discovery, and in-process MCP tools.

Choose an API

GoalAPI
Run one prompt and get the final resultrun()
Keep context across promptscreateSession()
Continue a saved sessionresumeSession()
Find saved sessionslistSessions()

Start with run() for one prompt. Use a session when later prompts need the same conversation history.

What you can build

Invoke Droid when a pull request opens, a support ticket is escalated, an incident is created, or scheduled repository maintenance is due. The agent does not need a chat interface.

Install and authenticate

Install the public npm package:

Bash
npm install @factory/droid-sdk

If your code imports Zod for structured output or SDK MCP tools, use Zod 3:

Bash
npm install zod@^3.24.0

Import Node.js APIs from @factory/droid-sdk/node. Browser and daemon clients are available from the root entrypoint. This guide covers the high-level public API; the exported TypeScript declarations define the complete compile-time API.

The SDK requires Node.js 18 or later.

By default, the SDK starts the droid CLI from PATH. Pass execPath to use a different droid executable.

Set an API key:

Bash
export FACTORY_API_KEY="your-key"

The SDK reads FACTORY_API_KEY from the process environment by default. You can also pass apiKey to run(), createSession(), or resumeSession().

Quick start

Run one prompt

TypeScript
import { run } from '@factory/droid-sdk/node';
 
const result = await run('Summarize this repository.');
 
if (!result.success) {
  throw new Error(result.error?.message ?? `Run failed: ${result.subtype}`);
}
 
console.log(result.text);

run() creates a session, consumes its complete message stream, returns the terminal DroidResult, and closes the session.

Continue a conversation

TypeScript
import { createSession, DroidMessageType } from '@factory/droid-sdk/node';
 
const session = await createSession();
 
try {
  for await (const message of session.stream('What does this project do?')) {
    if (message.type === DroidMessageType.Assistant) {
      console.log(message.text);
    }
  }
 
  for await (const message of session.stream('What should I test first?')) {
    if (message.type === DroidMessageType.Assistant) {
      console.log(message.text);
    }
  }
} finally {
  await session.close();
}

In the above example, the second prompt uses context from the first turn. Remember to close sessions that your code creates.

Core concepts

Session

A session holds conversation history, settings, and a working directory. Create one with createSession() or load a saved session with resumeSession().

Turn

A turn starts when you send one prompt with session.stream(). A session can run one turn at a time.

Stream message

session.stream() returns an async iterable of complete messages. Narrow each message by its type. Messages can report user input, assistant output, tool activity, hooks, errors, and a terminal DroidResult.

Result

A normally completed turn ends with a DroidResult. run() consumes the stream and returns that same result type. See Streaming and results for result handling, errors, token usage, and cancellation.

Ownership and cleanup

Cleanup depends on how the session was created.

APICleanup
run()Closes its session automatically
createSession()Call await session.close()
resumeSession()Call await session.close()

Use finally blocks so cleanup also runs after an error.

Sessions

Use a session when prompts need shared conversation history. A session owns its working directory, settings, and one active turn at a time.

Create and use a session

TypeScript
import { createSession, DroidMessageType } from '@factory/droid-sdk/node';
 
const session = await createSession({
  cwd: process.cwd(),
});
 
async function send(prompt: string) {
  for await (const message of session.stream(prompt)) {
    if (message.type === DroidMessageType.Assistant) {
      console.log(message.text);
    }
  }
}
 
try {
  await send('What does this project do?');
  await send('What should I test first?');
} finally {
  await session.close();
}

The second turn uses context from the first. cwd defaults to process.cwd(). Model and reasoning defaults come from Droid settings.

Resume a saved session

TypeScript
import { DroidMessageType, resumeSession } from '@factory/droid-sdk/node';
 
async function continueSession(sessionId: string) {
  const session = await resumeSession(sessionId);
 
  try {
    for await (const message of session.stream(
      'Continue from the last conversation.'
    )) {
      if (message.type === DroidMessageType.Assistant) {
        console.log(message.text);
      }
    }
  } finally {
    await session.close();
  }
}

resumeSession() restores the saved conversation, working directory, and session settings. It does not accept cwd, modelId, or reasoning options.

Resume options can provide new permission and AskUser handlers, disabled tools, MCP servers, an abort signal, and observability sinks.

Read session state

Each handle exposes its ID, current working directory, and current settings.

TypeScript
console.log(session.id);
console.log(session.cwd);
console.log(session.settings.modelId);
console.log(session.settings.reasoningEffort);
console.log(session.settings.interactionMode);

settings is read-only. The SDK updates settings and cwd when Droid reports a change.

Update session settings

Use updateSettings() for changes that should apply to later turns in the current session.

TypeScript
import { AutonomyLevel, ReasoningEffort } from '@factory/droid-sdk/node';
 
await session.updateSettings({
  modelId: 'model-id',
  reasoningEffort: ReasoningEffort.High,
  autonomyLevel: AutonomyLevel.Low,
  disabledToolIds: ['Execute'],
});

Updatable settings include:

  • model and reasoning effort
  • interaction mode and autonomy level
  • spec-mode model settings
  • tool availability overrides

Interaction mode controls whether Droid operates normally in Auto mode or produces a read-only plan in Spec mode. Autonomy level controls which actions require approval while Droid is in Auto mode. Tool availability overrides can enable, disable, or restrict the tools available to the session.

Use enterSpecMode() to enter Spec mode. Return to Auto mode with updateSettings({ interactionMode: DroidInteractionMode.Auto }).

The updated values are available through session.settings.

Rename a session

Use rename() after the first turn to replace the generated title.

TypeScript
await session.rename({ title: 'Authentication review' });

List saved sessions

listSessions() reads local Droid session storage. It does not start Droid or call the Factory API.

List sessions for the current working directory:

TypeScript
import { listSessions } from '@factory/droid-sdk/node';
 
const sessions = await listSessions({ limit: 10 });
 
for (const session of sessions) {
  console.log(session.id, session.title, session.modifiedTime);
}

List sessions across all working directories:

TypeScript
const sessions = await listSessions({
  fetchOutsideCWD: true,
  limit: 10,
});

Results are sorted by modifiedTime, newest first. Each item includes its ID, title, owner, message count, creation time, modification time, and working directory when available.

What persists

Droid saves the session data needed to continue later.

Restored from the saved sessionAttach again when resuming
Conversation historyPermission handler
Working directoryAskUser handler
TitleSDK MCP servers
Session settingsObservability sinks
Abort signal

Session settings include model, reasoning, Auto or Spec interaction mode, autonomy, spec-mode model settings, and tool availability overrides.

Handlers, observability sinks, abort signals, and SDK MCP servers are runtime objects. Droid cannot serialize them with the session, so attach them again when calling resumeSession(). They do not need to be the same object instances or implementations used when the session was created.

Close sessions safely

Call close() in finally when your code creates or resumes a session. Closing releases the subprocess, subscriptions, and SDK-owned MCP servers.

TypeScript
const session = await createSession();
 
try {
  // Use the session.
} finally {
  await session.close();
}

Streaming and results

Each call to session.stream() starts one turn and returns an async iterable.

By default, it yields complete messages and ends with a terminal DroidResult.

Complete messages

TypeScript
for await (const message of session.stream('Find the failing test.')) {
  switch (message.type) {
    case DroidMessageType.Assistant:
      console.log(message.text);
      break;
    case DroidMessageType.ToolCall:
      console.log(`Tool: ${message.name}`);
      break;
    case DroidMessageType.Result:
      console.log(message.subtype);
      break;
  }
}

Default message types:

TypePurpose
assistantComplete assistant message
userUser message recorded by the session
tool_callComplete tool request
tool_resultComplete tool result
hookHook execution
errorRuntime error event
resultTerminal DroidResult

Partial events

Enable partial events only when the application needs live text, thinking, tool progress, token usage, or state updates.

TypeScript
for await (const event of session.stream('Explain the test failure.', {
  includePartialMessages: true,
})) {
  if (event.type === DroidMessageType.AssistantTextDelta) {
    process.stdout.write(event.text);
  }
}

Partial streams can also include thinking deltas, tool-call deltas, tool progress, token updates, permission results, settings changes, working-state changes, and MCP status.

Handle the result

DroidResult is a discriminated union. Check subtype or success before using the response.

TypeScript
const result = await run('Run the test suite.');
 
switch (result.subtype) {
  case 'success':
    console.log(result.text);
    break;
  case 'interrupted':
    console.log('The run was interrupted.');
    break;
  case 'error_during_execution':
  case 'error_structured_output':
    console.error(
      result.structuredOutputError?.message ??
        result.error?.message ??
        'Unknown error'
    );
    break;
}
SubtypeMeaning
successThe turn completed successfully
interruptedThe turn was cancelled or permission declined
error_during_executionDroid could not complete the requested work
error_structured_outputStructured-output generation or validation failed

Every result includes:

TypeScript
console.log(result.sessionId);
console.log(result.durationMs);
console.log(result.turnCount);
console.log(result.messages);
console.log(result.text);
console.log(result.tokenUsage);

run() returns the terminal result directly. When consuming session.stream(), handle the message whose type is DroidMessageType.Result.

Token and context usage

DroidResult.tokenUsage reports raw token counts for the current SDK turn and, when available, the Factory Service Credits (FSC) charged for that turn. One turn is one run() or session.stream() call. This is not the session's lifetime total.

TypeScript
if (result.tokenUsage) {
  console.log(`input: ${result.tokenUsage.inputTokens}`);
  console.log(`output: ${result.tokenUsage.outputTokens}`);
  console.log(`cache read: ${result.tokenUsage.cacheReadTokens}`);
  console.log(`cache created: ${result.tokenUsage.cacheCreationTokens}`);
  if (result.tokenUsage.factoryCredits !== undefined) {
    console.log(`Factory Service Credits: ${result.tokenUsage.factoryCredits}`);
  }
}
FieldWhat it measures during the turn
inputTokensProvider-reported input across model calls, which can include instructions, tools, conversation history, and the latest message
outputTokensOutput generated across model calls, not only the final visible response
cacheReadTokensInput tokens reused from the prompt cache
cacheCreationTokensInput tokens written to the prompt cache
thinkingTokensReasoning tokens reported separately when available
factoryCreditsFactory Service Credits charged, when available

One turn can make several model calls while Droid uses tools. Their usage is combined in the result, along with usage from delegated work. Earlier conversation can be counted again when it is included as input to a later turn. Cached input is also reported through the separate cache fields. tokenUsage is null when usage is unavailable.

Partial streams can emit DroidMessageType.TokenUsageUpdate. Unlike the per-turn result, these events contain cumulative committed usage for the session.

getContextStats() measures current context occupancy, not cumulative token usage:

TypeScript
const stats = await session.getContextStats();
 
console.log(stats.used, stats.remaining, stats.limit, stats.accuracy);
FieldMeaning
usedEstimated tokens currently occupied by prompts, tools, instructions, and conversation history
limitActive model's maximum input window; unresolved model IDs fall back to the effective compaction limit
remainingMath.max(0, limit - used)
accuracyCurrently estimated, because Droid approximates tokens from character counts

remaining estimates room in the model's input window. It does not necessarily mean tokens remaining before compaction. Droid can compact at a lower configured threshold, commonly 250,000 tokens, even when the active model's input window is larger.

Errors

An error event and a thrown exception mean different things:

  • An error message reports a runtime problem during the turn. A terminal DroidResult normally follows and describes the final outcome.
  • A failed DroidResult means the stream completed, but the requested work or structured output failed.
  • A thrown exception means the SDK operation itself could not finish, such as an abort, connection failure, protocol error, or concurrent stream attempt.

Handle stream exceptions around the iteration:

TypeScript
try {
  for await (const message of session.stream('Run the tests.')) {
    if (message.type === DroidMessageType.Error) {
      console.error(message.message);
    }
  }
} catch (error) {
  console.error('The stream could not finish:', error);
}

run() follows the same distinction: agent failures are returned as failed results, while setup, transport, protocol, and abort failures are thrown.

Stream concurrency

One session handle can have one active stream. Starting another active stream throws ConcurrentStreamError.

Cancellation and interruption

Use an abort signal to cancel one turn:

TypeScript
const controller = new AbortController();
 
setTimeout(
  () => controller.abort(new Error('Timed out after 5 seconds')),
  5_000
);
 
for await (const message of session.stream('Perform a long review.', {
  abortSignal: controller.signal,
})) {
  // Handle messages.
}

The stream interrupts the active turn and throws the abort reason.

Use session.interrupt() when another part of the application needs to stop the active turn:

TypeScript
await session.interrupt();

The stream normally continues to a terminal result with subtype interrupted. The session remains available for later prompts.

Breaking out of the stream also interrupts the active turn:

TypeScript
for await (const message of session.stream('Investigate every failing test.')) {
  if (message.type === DroidMessageType.Assistant) {
    console.log(message.text);
 
    // Interrupt this turn instead of merely hiding its remaining output.
    break;
  }
}

Breaking out of the loop before the turn finishes sends an interrupt request to Droid. This cancels the remaining model and tool work for that turn. The session remains open and can accept another prompt, but this loop does not receive the interrupted turn's terminal DroidResult.

A session-level abort signal passed to createSession() closes the entire session when aborted.

Models

Model IDs are strings because availability depends on the account, organization policy, and daemon configuration. Omit modelId to use the Droid default.

Select a model

Pass a configured model ID when creating a session or running one prompt.

TypeScript
import {
  createSession,
  DroidMessageType,
  ReasoningEffort,
} from '@factory/droid-sdk/node';
 
const session = await createSession({
  modelId: 'model-id',
  reasoningEffort: ReasoningEffort.High,
});
 
try {
  for await (const message of session.stream('Review this repository.')) {
    if (message.type === DroidMessageType.Assistant) {
      console.log(message.text);
    }
  }
} finally {
  await session.close();
}

For an existing session, call updateSettings():

TypeScript
await session.updateSettings({
  modelId: 'model-id',
  reasoningEffort: ReasoningEffort.High,
});

Set reasoningEffort only to a level supported by the selected model. Omit it to use the model's configured default.

Use the Factory Router

Set modelId to "auto" to let Factory choose a model for each turn.

TypeScript
import { run } from '@factory/droid-sdk/node';
 
const result = await run('Find the cause of the failing tests.', {
  modelId: 'auto',
});
 
if (!result.success) {
  throw new Error(result.error?.message ?? `Run failed: ${result.subtype}`);
}
 
console.log(result.text);

The selected model can change between turns. Choose a fixed model ID when runs must use the same model.

Configure mode-specific models

The primary model handles normal turns. The spec-mode model handles turns in spec mode.

TypeScript
import {
  createSession,
  DroidInteractionMode,
  ReasoningEffort,
} from '@factory/droid-sdk/node';
 
const session = await createSession({
  modelId: 'auto',
  interactionMode: DroidInteractionMode.Spec,
  specModeModelId: 'model-id',
  specModeReasoningEffort: ReasoningEffort.High,
});
 
try {
  for await (const _message of session.stream(
    'Draft an implementation plan.'
  )) {
    // Consume messages.
  }
} finally {
  await session.close();
}

You can also choose the model when entering spec mode:

TypeScript
await session.enterSpecMode({
  specModeModelId: 'model-id',
  specModeReasoningEffort: ReasoningEffort.High,
});

Use a custom model

Configure custom models in ~/.factory/settings.json. See Model Independence for provider, credential, and model settings.

After configuration, pass the custom model ID anywhere the SDK accepts modelId. The ID uses custom: followed by the configured model value.

TypeScript
import { run } from '@factory/droid-sdk/node';
 
const result = await run('Summarize this repository.', {
  modelId: 'custom:gpt-4o-mini',
});
 
if (!result.success) {
  throw new Error(result.error?.message ?? `Run failed: ${result.subtype}`);
}
 
console.log(result.text);

Inputs and outputs

Pass images, documents, or an output schema in the options for the turn that should use them. Both run() and session.stream() accept these options.

Images and documents

TypeScript
const result = await run('Describe this image.', {
  images: [
    {
      type: 'base64',
      data: base64Png,
      mediaType: 'image/png',
    },
  ],
});

data contains the base64-encoded image bytes. Supported media types are image/jpeg, image/png, image/gif, and image/webp.

Documents

TypeScript
const result = await run('Summarize this document.', {
  files: [
    {
      type: 'text',
      mediaType: 'text/plain',
      data: report,
      name: 'report.txt',
    },
  ],
});

Document inputs support:

SourcetypemediaTypedata
Plain texttexttext/plainThe document text
PDF documentbase64application/pdfBase64-encoded PDF file bytes

name is optional for both forms. Attach content through data; a filesystem path by itself is not a document upload.

Structured output

Use outputFormat when another system needs a JSON object that follows a schema.

TypeScript
import { OutputFormatType, run } from '@factory/droid-sdk/node';
 
const result = await run('Return the repository name and test command.', {
  outputFormat: {
    type: OutputFormatType.JsonSchema,
    schema: {
      type: 'object',
      properties: {
        repository: { type: 'string' },
        testCommand: { type: 'string' },
      },
      required: ['repository', 'testCommand'],
      additionalProperties: false,
    },
  },
});
 
if (!result.success) {
  throw new Error(
    result.structuredOutputError?.message ??
      result.error?.message ??
      `Structured output failed (${result.subtype}).`
  );
}
 
if (!result.structuredOutput) {
  throw new Error('No structured output was returned.');
}
 
console.log(JSON.stringify(result.structuredOutput, null, 2));

Invalid or missing schema output normally produces a failed result with subtype error_structured_output; structuredOutputError contains the available validation details.

structuredOutput is typed as unknown, because a JSON Schema does not provide a TypeScript runtime validator. Check that it is present and validate or narrow its shape before reading fields or passing it to another system. A successful result can still omit structured output, so do not use result.success as the only check.

Permissions and user input

Autonomy

Autonomy controls which actions require approval in Auto mode.

TypeScript
import { AutonomyLevel } from '@factory/droid-sdk/node';
 
const session = await createSession({
  autonomyLevel: AutonomyLevel.Off,
});
LevelBehavior
OffAsk before every action
LowAllow file edits and read-only commands
MediumAllow reversible commands
HighAllow commands without approval prompts

When omitted, Droid uses the configured default. Autonomy does not select Auto or Spec mode; interactionMode does that.

Permission handler

Use permissionHandler to approve or reject requests that reach the client. The handler may be synchronous or asynchronous.

TypeScript
import {
  ToolConfirmationOutcome,
  ToolConfirmationType,
} from '@factory/droid-sdk/node';
 
const session = await createSession({
  autonomyLevel: AutonomyLevel.Off,
  permissionHandler({ toolUses, options }) {
    const createsOnly =
      toolUses.length > 0 &&
      toolUses.every((use) => use.details.type === ToolConfirmationType.Create);
 
    const desired = createsOnly
      ? ToolConfirmationOutcome.ProceedOnce
      : ToolConfirmationOutcome.Cancel;
 
    const offered = options.find((option) => option.value === desired);
    if (!offered) throw new Error(`Outcome not offered: ${desired}`);
 
    return desired;
  },
});

toolUses describes the pending actions. options contains the outcomes available for this request. Return one of those values. Common outcomes are:

OutcomeEffect
ProceedOnceApprove this request
ProceedAlwaysApprove and persist the applicable rule
CancelReject the request

The available outcomes vary by request. Returning an unavailable value, throwing from the handler, or omitting the handler cancels the request.

AskUser handler

askUserHandler answers questions Droid asks during a turn. Connect it to a form, prompt, or other application UI.

TypeScript
const result = await run('Ask me which environment to deploy.', {
  askUserHandler({ questions }) {
    return {
      answers: questions.map((question) => ({
        index: question.index,
        question: question.question,
        answer: question.options[0] ?? 'none',
      })),
    };
  },
});

Each answer must copy the question's index and question. To decline the questionnaire:

TypeScript
return { cancelled: true, answers: [] };

Without a handler, AskUser requests are declined.

Tool controls

Disable tools when creating, resuming, or updating a session:

TypeScript
const session = await createSession({
  disabledToolIds: ['Execute'],
});
 
await session.updateSettings({
  disabledToolIds: ['Execute', 'Edit'],
});

Tool IDs are strings. Use listTools() to inspect the tools available to the current session.

TypeScript
const tools = await session.listTools();
 
for (const tool of tools) {
  console.log(tool.id, tool.allowed);
}

The public SDK provides subtractive disabledToolIds. It does not expose a restrictive allowlist.

Extensions

Use the extension that matches the job:

ExtensionUse it for
SkillsReusable instructions and supporting files
SDK MCP toolsIn-process TypeScript functions exposed to Droid
External MCP serversTools provided by another process or service
HooksCommands that run at defined lifecycle events

Skills

List the skills available to the session:

TypeScript
const { skills } = await session.listSkills();
 
for (const skill of skills) {
  console.log(skill.name, skill.enabled);
}

Enable or disable a skill at the user or project level:

TypeScript
import { SettingsLevel } from '@factory/droid-sdk/node';
 
await session.setSkillDisabled({
  skillName: 'skill-name',
  disabled: true,
  settingsLevel: SettingsLevel.Project,
});

Skills can come from project, personal, built-in, or automation settings.

In-process MCP tools

Use tool() to define an in-process TypeScript function with a Zod input schema. The SDK and Droid CLI must use compatible Factory protocol versions.

TypeScript
import { z } from 'zod';
import {
  createSdkMcpServer,
  createSession,
  tool,
} from '@factory/droid-sdk/node';
 
const server = createSdkMcpServer({
  name: 'review-tools',
  tools: [
    tool(
      'lookup_owner',
      'Returns the owner of a file',
      { path: z.string() },
      ({ path }) => `Owner for ${path}: platform-team`
    ),
  ],
});
 
const session = await createSession({
  mcpServers: [server],
});
 
try {
  for await (const _message of session.stream(
    'Find the owner of src/index.ts.'
  )) {
    // Consume messages.
  }
} finally {
  await session.close();
}

The SDK starts an authenticated loopback MCP server and closes it with the session. Tool calls still follow the session's autonomy and permission rules. Attach SDK MCP servers again when resuming a saved session because they are runtime objects, not persisted session data.

External MCP servers

Pass external MCP configuration when creating or resuming a session:

TypeScript
const session = await createSession({
  mcpServers: [
    {
      name: 'docs',
      type: 'http',
      url: 'https://example.com/mcp',
      headers: [],
    },
  ],
});

Supported session configuration:

Transporttype fieldRequired configuration
Local processOmitcommand and args
HTTPhttpurl and headers
SSEsseurl and headers

Inspect server status and discovered tools:

TypeScript
try {
  const { servers, summary } = await session.listMcpServers();
  const tools = await session.listMcpTools();
 
  console.log(servers, summary, tools);
} finally {
  await session.close();
}

addMcpServer(), removeMcpServer(), and toggleMcpServer() change the user's Droid configuration. Use authenticateMcpServer({ serverName }) when a server requires authentication.

Hooks

Hooks run shell commands at defined lifecycle events. Configure them in .factory/hooks.json:

JSON
{
  "PreToolUse": [
    {
      "matcher": "Execute",
      "hooks": [
        {
          "type": "command",
          "command": "echo before Execute"
        }
      ]
    }
  ]
}

Hooks can run before or after tools, when prompts are submitted, when notifications arrive, during compaction, and when sessions or subagents stop.

Hook events are available in the stream:

TypeScript
for await (const message of session.stream('Run the tests.')) {
  if (message.type === DroidMessageType.Hook) {
    console.log(message.status, message.command);
  }
}

The SDK exposes hook schemas and stream events, but it does not provide a programmatic hook-registration API.

Session lifecycle

Fork, compact, and rewind return ready successor sessions and retire the source wrapper.

Fork

fork() creates a new session from the current conversation.

TypeScript
const forked = await session.fork();

Compact

compact() summarizes older conversation history and returns the successor in session.

TypeScript
const { session: compacted, removedCount } = await forked.compact();

Rewind

rewind() returns the conversation to an earlier message and can restore or delete files.

TypeScript
const {
  session: rewound,
  restoredCount,
  deletedCount,
} = await compacted.rewind({
  messageId,
  filesToRestore,
  filesToDelete,
  forkTitle: 'Before the failed change',
});

Successor ownership

After a successful replacement, active operations on the source wrapper throw SessionReplacedError. Its id, settings, and cwd remain readable, and close() is a no-op. The persisted source session can still be loaded later with resumeSession(sourceId).

Do not replace a session while it has an active stream.

Spec mode

Spec mode lets Droid inspect the codebase and propose a plan without changing files. Start a session in Spec mode or switch an existing session with enterSpecMode():

TypeScript
const session = await createSession({
  interactionMode: DroidInteractionMode.Spec,
});
 
// Or switch an existing Auto session.
await session.enterSpecMode();

enterSpecMode() can also set the model used for planning:

TypeScript
await session.enterSpecMode({
  specModeModelId: 'model-id',
  specModeReasoningEffort: ReasoningEffort.High,
});

Leave without approving a plan

Call updateSettings() to return the session to Auto mode without approving a plan or starting implementation:

TypeScript
import { DroidInteractionMode } from '@factory/droid-sdk/node';
 
// Return to Auto mode without approving or implementing the plan.
await session.updateSettings({
  interactionMode: DroidInteractionMode.Auto,
});

Changing the interaction mode does not select a ToolConfirmationOutcome or approve an ExitSpecMode permission request.

updateSettings() is available on Node DroidSession handles returned by createSession(), resumeSession(), and replacement operations such as fork(), compact(), and rewind(). Daemon clients use droid.sessions.updateSettings().

Approve a plan

When the plan is ready, Droid sends an ExitSpecMode permission request. Its details contain the plan. Unlike changing the interaction mode with updateSettings(), approving it with ProceedOnce accepts the completed plan, returns to Auto mode, and continues implementation in the same session:

TypeScript
import {
  AutonomyLevel,
  createSession,
  DroidInteractionMode,
  DroidMessageType,
  ToolConfirmationOutcome,
  ToolConfirmationType,
} from '@factory/droid-sdk/node';
 
const session = await createSession({
  interactionMode: DroidInteractionMode.Auto,
  autonomyLevel: AutonomyLevel.Low,
  permissionHandler({ toolUses }) {
    const request = toolUses.find(
      ({ details }) => details.type === ToolConfirmationType.ExitSpecMode
    );
 
    if (request?.details.type === ToolConfirmationType.ExitSpecMode) {
      console.log(request.details.plan);
 
      return ToolConfirmationOutcome.ProceedOnce;
    }
 
    return ToolConfirmationOutcome.Cancel;
  },
});
 
try {
  await session.enterSpecMode();
 
  for await (const message of session.stream('Plan and update README.md.')) {
    if (message.type === DroidMessageType.Result) {
      console.log(message.subtype);
    }
  }
} finally {
  await session.close();
}

The handler can be asynchronous. An interactive application should display the plan and wait for the user's decision before returning.

Return Cancel to reject the plan. The turn ends with an interrupted result, and the session remains available for another prompt.

Some ExitSpecMode requests offer ProceedNewSession variants. Return one of those offered values to hand implementation to a new session. Return only values present in the request's options.

Observability and advanced APIs

Observability

TypeScript
import { run } from '@factory/droid-sdk/node';
 
const result = await run('Check the repository status.', {
  observability: {
    logger: {
      log(event) {
        console.log(event.level, event.name, event.attributes);
      },
    },
  },
});
 
if (!result.success) {
  throw new Error(result.error?.message ?? `Run failed: ${result.subtype}`);
}

Pass logger, metrics, or tracing sinks through observability. Sink methods must not throw. The SDK excludes prompts, messages, tool inputs, raw output, and stack traces from observability events. Process startup emits telemetry before the first turn, and close() emits a request log. To verify turn-specific logs, snapshot the count after creating the session and check it before closing.

Low-level APIs

Use the high-level APIs unless your application owns the transport or protocol lifecycle.

NeedPreferred APILow-level alternative
Run a sessioncreateSession()DroidClient
Consume typed session eventssession.stream()Raw notifications

The /node entrypoint exports DroidClient for direct JSON-RPC integrations. Low-level methods return response envelopes, so successful payloads are under response.result.

Use onNotification() only when session.stream() does not expose the event you need:

TypeScript
const unsubscribe = session.onNotification(
  (notification) => console.log(notification.type),
  { type: 'settings_updated' }
);
 
unsubscribe();

Examples

ExampleDemonstrates
Multi-turn sessionKeep context across prompts
Initialization metadataRead session metadata after initialization
Session settingsRead and update active-session settings
Rename a sessionChange a saved session's title
List sessionsDiscover saved sessions
Session streamConsume complete stream messages
Result metadataInspect terminal result metadata
Abort a streamCancel a turn with an abort signal
Interrupt a sessionRequest interruption during a turn
Image attachmentSend a local image with a prompt
Structured outputValidate model output against a schema
Permission handlerRespond to tool permission requests
AskUser handlerAnswer questions from Droid
Tool controlsRestrict and configure native tools
Enter Spec modePlan before implementation
SDK MCP toolDefine an in-process MCP tool
Hook executionObserve configured hook events
ObservabilityCapture SDK logs and telemetry

API reference

Entrypoint

Import functions, classes, enums, and types from @factory/droid-sdk/node.

Functions

APIReturns
run(prompt, options?)Promise<DroidResult>
createSession(options?)Promise<DroidSession>
resumeSession(sessionId, options?)Promise<DroidSession>
listSessions(options?)Promise<SessionMetadata[]>
createSdkMcpServer(options)SdkMcpServer
tool(...)DroidTool

DroidSession

MemberPurpose
idSession ID
cwdLive working directory
settingsLive read-only settings
stream()Run one turn
interrupt()Stop the active turn
updateSettings()Update session settings
enterSpecMode()Change to spec mode
listTools()List normalized tools
listSkills()List skills
setSkillDisabled()Enable or disable a skill
addMcpServer()Add an MCP server
removeMcpServer()Remove an MCP server
toggleMcpServer()Enable or disable an MCP server
listMcpServers()List MCP servers
listMcpTools()List MCP tools
authenticateMcpServer()Start MCP authentication
getContextStats()Read context usage
getRewindInfo()Inspect rewindable file changes
rewind()Create a rewound successor
compact()Create a compacted successor
fork()Create a forked successor
rename()Change the session title
onNotification()Subscribe to raw notifications
close()Close the session and owned resources

Stream types

TypePurpose
DroidStreamMessageComplete messages returned by the default stream
DroidStreamEventComplete messages plus partial events
DroidResultTerminal result returned by run()
DroidMessageTypeRuntime constants for narrowing stream messages
TokenUsageToken counts and optional Factory Service Credits

session.stream() yields DroidStreamMessage by default and DroidStreamEvent when includePartialMessages is true.

Result states

SubtypesuccessinterruptedMeaning
successtruefalseTurn completed
interruptedfalsetrueTurn was stopped
error_during_executionfalsefalseRuntime failure
error_structured_outputfalsefalseStructured-output failure

Every result includes sessionId, durationMs, tokenUsage, messages, text, and turnCount. Check success before using successful output.

Main enums

EnumValues
AutonomyLevelOff, Low, Medium, High
DroidInteractionModeAuto, Spec
ReasoningEffortNone, Dynamic, Off, Minimal, Low, Medium, High, ExtraHigh, Max
OutputFormatTypeJsonSchema

Model IDs and tool IDs are strings rather than closed enums.

Main errors

ErrorMeaning
ConcurrentStreamErrorA session handle already has an active stream
SessionReplacedErrorA source wrapper was replaced
SessionReplacementErrorSuccessor loading or rollback failed
DroidClientErrorBase client error
ConnectionErrorDroid process connection failed
TimeoutErrorA request timed out
ProtocolErrorProtocol or API response failed
SessionErrorBase session error
SessionNotFoundErrorSaved session was not found
InvalidSessionCwdErrorSaved working directory is invalid
ProcessExitErrorDroid subprocess exited unexpectedly

Known limitations

  • New-session spec handoff IDs require raw notification inspection.
  • SDK MCP tools require compatible protocol versions in the npm package and Droid CLI.