Factory TypeScript SDK

August 6, 2026 ยท View on GitHub

The Factory TypeScript SDK lets applications run Droid, continue conversations, stream work, control tools, and connect to a running Droid daemon.

The public npm package is @factory/droid-sdk.

This page is the single user-facing SDK guide. The package's exported TypeScript declarations remain authoritative for its compile-time API surface.

Overview

The SDK has two main runtimes.

RuntimeImportUse it for
Node@factory/droid-sdk/nodeScripts, CI, local tools, and apps that can start the Droid CLI
Daemon@factory/droid-sdkBrowser or Node apps that connect to an existing Droid daemon

The Node runtime starts a Droid subprocess for you. The daemon runtime connects to a daemon over WebSocket and can manage several sessions through one connection.

The root import is browser-safe. The /node import adds subprocess, filesystem, local session discovery, and SDK MCP tool APIs.

Requirements and installation

Install the package:

npm install @factory/droid-sdk

The SDK requires Node.js 18 or later.

By default, Node session APIs start the droid CLI from PATH. You can provide a different executable with execPath. Supplying a custom transport bypasses subprocess creation and this requirement.

Set an API key:

export FACTORY_API_KEY="your-key"

You can also pass apiKey directly when creating a Node session.

Browser security

The daemon client currently authenticates with a Factory API key. Use browser examples only in trusted local tools. Do not ship a Factory API key in a public website, commit it to source control, or put it in a URL.

An HTTPS page must connect with wss://. Local pages served over HTTP can use ws://.

Quick start

Run one prompt

Runtime: Node

import { run } from '@factory/droid-sdk/node';

const result = await run('Summarize this repository.');

if (!result.success) {
  throw new Error(result.error?.message ?? 'The run did not complete.');
}

console.log(result.text);

run() creates a session, waits for its final result, and closes the session. Use it for one-shot work.

Continue a conversation

Runtime: Node

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();
}

Use a session when later prompts need context from earlier turns.

Connect to a daemon

Runtime: Node or trusted local browser

import { connectToDaemon, DroidMessageType } from '@factory/droid-sdk';

async function runWithDaemon(apiKey: string, cwd: string) {
  const droid = await connectToDaemon({
    url: 'ws://127.0.0.1:37643',
    auth: { apiKey },
  });

  const session = await droid.sessions.create({ cwd });

  try {
    for await (const message of session.stream('Summarize this repository.')) {
      if (message.type === DroidMessageType.Assistant) {
        console.log(message.text);
      }
    }
  } finally {
    await Promise.allSettled([session.close()]);
    droid.disconnect();
  }
}

In browser code, obtain both arguments from trusted local configuration. This does not make an API key secret from browser JavaScript, so keep this pattern restricted to trusted local pages.

Core concepts

Session

A session holds conversation history, settings, and a working directory.

Node sessions are created with createSession() or loaded with resumeSession(). Daemon sessions are created or resumed through droid.sessions.

Turn

A turn starts when you send one prompt with session.stream(). A session can run one turn at a time. Different daemon sessions can run turns concurrently.

Stream message

session.stream() yields a discriminated message union. The default stream contains complete user, assistant, tool, hook, error, and result messages.

Result

Every normally completed stream ends with a result message. A result reports success, interruption, or failure and includes final text, messages, duration, turn count, and token usage when available.

Node and daemon lifecycle behavior

Node and daemon replacement operations intentionally behave differently.

BehaviorNode sessionDaemon session
Successor responsefork: handle; compact/rewind: outcome with sessionOutcome with newSessionId
Source handle after replacementRetiredStill usable
Load successorAutomaticCall droid.sessions.resume()

See Session lifecycle for examples.

One-shot runs

Runtime: Node

function run(prompt: string, options?: RunOptions): Promise<DroidResult>;

run() supports the same creation and message options as a Node session, except partial stream events.

const result = await run('Review the authentication middleware.', {
  cwd: '/path/to/repository',
  modelId: 'model-id',
  reasoningEffort: ReasoningEffort.High,
  disabledToolIds: ['Execute'],
});

Common options:

OptionPurpose
cwdWorking directory. Defaults to process.cwd()
apiKeyFactory API key. Defaults to FACTORY_API_KEY
modelIdModel selection
reasoningEffortModel reasoning level
autonomyLevelPermission policy level
interactionModeAuto, spec, or mission mode. AGI is deprecated
specModeModelIdModel used in spec mode
specModeReasoningEffortReasoning level used in spec mode
disabledToolIdsTools that Droid cannot use
autoRejectPermissionRequestsReject all permission requests automatically
disableBuiltinSkillsHide Factory-provided built-in skills
permissionHandlerHandles permission requests
askUserHandlerAnswers AskUser requests
tagsSession attribution tags
sessionSourceSession source attribution
imagesBase64 image attachments
filesText or PDF attachments
outputFormatJSON schema for structured output
abortSignalCancels the run
mcpServersMCP servers available to the session
observabilityLogging, metrics, and trace integration
execPathDroid executable path
execArgsAdditional Droid process arguments
envAdditional Droid process environment variables
transportCustom string-framed transport

Sessions

Create a Node session

import { createSession } from '@factory/droid-sdk/node';

const session = await createSession({
  cwd: process.cwd(),
  modelId: 'model-id',
});

When omitted, cwd defaults to process.cwd(). Model and reasoning defaults come from Droid settings.

Resume a Node session

import { resumeSession } from '@factory/droid-sdk/node';

const session = await resumeSession('session-id');

The saved session owns its working directory and settings. Resume options can change handlers, transports, disabled tools, MCP servers, and observability, but cannot replace saved model or working-directory settings.

Session state

console.log(session.id);
console.log(session.cwd);
console.log(session.settings.modelId);

settings and cwd update when the running session reports changes.

Update settings

await session.updateSettings({
  modelId: 'model-id',
  reasoningEffort: ReasoningEffort.High,
  autonomyLevel: AutonomyLevel.Low,
  disabledToolIds: ['Execute'],
});

Public settings include model, reasoning, interaction mode, autonomy, spec mode settings, tags, mission settings, compaction settings, and disabled tools.

Rename a session

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

Discover saved Node sessions

import { listSessions } from '@factory/droid-sdk/node';

const sessions = await listSessions({
  cwd: process.cwd(),
  limit: 10,
});

listSessions() reads local session storage. It does not call the Factory REST API or a daemon.

Close a session

Call close() in finally when your code owns a Node session.

const session = await createSession();

try {
  // Use the session.
} finally {
  await session.close();
}

Streaming

Complete messages

Complete messages are the default.

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
resultFinal turn result

Partial events

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

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, MCP status, and mission events.

Stream concurrency

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

Daemon sessions on the same connection may stream concurrently.

Stop consuming a stream

Breaking out of a stream early interrupts the active server turn.

Inputs and outputs

Images

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

Supported image types are JPEG, PNG, GIF, and WebP.

Documents

for await (const _message of session.stream('Summarize this document.', {
  files: [
    {
      type: 'text',
      mediaType: 'text/plain',
      data: report,
      name: 'report.txt',
    },
  ],
})) {
  // Consume messages.
}

Document inputs support plain text and base64 PDF data.

Structured output

Use structured output when another system needs a predictable result.

import { OutputFormatType, run } from '@factory/droid-sdk/node';

const result = await run('Review this function for correctness issues.', {
  outputFormat: {
    type: OutputFormatType.JsonSchema,
    schema: {
      type: 'object',
      properties: {
        summary: { type: 'string' },
        findings: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              line: { type: 'number' },
              severity: {
                type: 'string',
                enum: ['low', 'medium', 'high'],
              },
              message: { type: 'string' },
            },
            required: ['line', 'severity', 'message'],
          },
        },
      },
      required: ['summary', 'findings'],
    },
  },
});

if (!result.success) {
  throw new Error(
    result.structuredOutputError?.message ??
      result.error?.message ??
      'Structured output failed.'
  );
}

console.log(result.structuredOutput);

structuredOutput is typed as unknown. Validate or narrow it before passing it to another system. A successful result can omit structured output.

Result metadata

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

tokenUsage is null when usage is unavailable.

Cancellation and interruption

Abort one turn

const controller = new AbortController();

setTimeout(() => controller.abort(), 5_000);

for await (const message of session.stream('Perform a long review.', {
  abortSignal: controller.signal,
})) {
  // Handle messages.
}

The stream throws the abort reason after interrupting the active turn.

Interrupt and keep the session

await session.interrupt();

interrupt() stops the active turn on the server. The stream continues to its terminal result, normally with subtype interrupted. The session remains usable for later prompts.

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

Permissions and user input

Autonomy

import { AutonomyLevel } from '@factory/droid-sdk/node';

const session = await createSession({
  autonomyLevel: AutonomyLevel.Off,
});

Levels are Off, Low, Medium, and High. When omitted, Droid settings choose the default.

Permission handler

import {
  ToolConfirmationOutcome,
  ToolConfirmationType,
} from '@factory/droid-sdk/node';

const session = await createSession({
  autonomyLevel: AutonomyLevel.Off,
  permissionHandler(request) {
    const safe =
      request.toolUses.length > 0 &&
      request.toolUses.every(
        (use) => use.details.type === ToolConfirmationType.Create
      );

    return safe
      ? ToolConfirmationOutcome.ProceedOnce
      : ToolConfirmationOutcome.Cancel;
  },
});

The handler must return an outcome supplied by the request. Invalid results and handler errors cancel the request.

Without a handler, permission requests are cancelled.

AskUser handler

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

To decline:

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

Without a handler, AskUser requests are declined.

Disable tools

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

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

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.

Tools and extensions

Skills

const { skills } = await session.listSkills();

await session.setSkillDisabled({
  skillName: 'skill-name',
  disabled: true,
  settingsLevel: 'project',
});

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

SDK MCP tools

Runtime: Node

Use an SDK MCP server to define an in-process tool with a typed Zod input.

import { z } from 'zod';
import {
  createSdkMcpServer,
  createSession,
  tool,
} from '@factory/droid-sdk/node';

const tools = 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: [tools],
});

The SDK starts an authenticated loopback MCP server and closes it with the session.

External MCP servers

Node sessions can add, remove, toggle, list, and authenticate MCP servers.

await session.addMcpServer({
  name: 'docs',
  type: 'http',
  url: 'https://example.com/mcp',
});

const servers = await session.listMcpServers();
const tools = await session.listMcpTools();

The daemon client exposes the complete MCP management API through droid.mcp.

Hooks

Hooks are configured in .factory/hooks.json. They 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:

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 types, but does not provide a programmatic hook-registration API.

Raw notifications

Runtime: Node

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

unsubscribe();

Use raw notifications only when the typed stream does not expose the event you need.

Session lifecycle

Node replacement handles

Node fork() returns a ready successor handle. compact() and rewind() return outcome objects containing a ready successor in session. Each operation retires its source wrapper.

const forked = await session.fork();

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

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

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.

Daemon replacement IDs

Daemon operations return a new session ID. The source handle remains usable.

const { newSessionId } = await source.compact();
const compacted = await droid.sessions.resume(newSessionId);

The same rule applies to fork() and rewind().

const forkResult = await source.fork();
const fork = await droid.sessions.resume(forkResult.newSessionId);

const rewindResult = await source.rewind({
  messageId,
  filesToRestore,
  filesToDelete,
  forkTitle: 'Before the failed change',
});
const rewound = await droid.sessions.resume(rewindResult.newSessionId);

Daemon replacement locking is local to one client handle. Avoid replacing or streaming the same session through several daemon clients at once.

Spec mode

Create a session in spec mode:

import {
  createSession,
  DroidInteractionMode,
  ReasoningEffort,
} from '@factory/droid-sdk/node';

const session = await createSession({
  interactionMode: DroidInteractionMode.Spec,
  specModeReasoningEffort: ReasoningEffort.High,
});

Enter spec mode later:

await session.enterSpecMode({
  specModeReasoningEffort: ReasoningEffort.High,
});

When Droid asks to leave spec mode, a permission handler can approve implementation in the same session or hand it to a new session.

OutcomeBehavior
ProceedOnceContinue in the same session
ProceedNewSessionHighStart implementation in a new high-autonomy session

New-session handoff IDs currently require inspecting raw notifications.

Daemon and browser applications

Connect

const droid = await connectToDaemon({
  url: 'ws://127.0.0.1:37643',
  auth: { apiKey },
  permissionHandler,
  askUserHandler,
  onError(error) {
    console.error(error);
  },
});

Connection-level handlers become defaults for created and resumed sessions. Per-session handlers override them.

Create and resume

const created = await droid.sessions.create({
  cwd: '/path/to/repository',
});

const resumed = await droid.sessions.resume('session-id');

One ConnectedDroid can have one attached handle for each session ID.

List and inspect history

const sessions = await droid.sessions.list({ limit: 20 });

const messages = await droid.sessions.getMessages(sessions[0].id, {
  limit: 50,
});

const matches = await droid.sessions.search({
  query: 'authentication failure',
});

The daemon session resource also supports opened-session listing, archive/unarchive, rename, settings, context information, and queued-message operations.

Concurrent sessions

Different daemon sessions can stream over one connection at the same time.

await Promise.all([
  consume(first.stream('Review the API.')),
  consume(second.stream('Review the tests.')),
]);

Cleanup

OperationDaemon sessionLocal handleConnection
session.detach()Keeps runningDetachedRetained
session.close()EndsDetached after successRetained
droid.disconnect()Keeps runningAll detachedDestroyed

Detached handles cannot be reused. Resume the session to obtain a new handle.

Stable daemon resources

NamespaceMain methods
sessionslist, listOpened, create, resume, getMessages, search, archive, unarchive, rename, updateSettings, resolveQueuedMessage, killWorker, getRewindInfo, rewind, compact, fork, getContextBreakdown
workspacechangeDirectory, validateDirectory, checkTrust, trust, listFiles, searchFiles, getFileContent
settingsgetDefaults, updateDefaults
customModelslist, upsert, delete
sshinstallKey
updatestrigger
relaystart, stop, status
terminalscreate, write, resize, close, list
mcpgetConfig, updateConfig, addServer, removeServer, toggleServer, authentication, registry, and tool methods
skillslist, setDisabled
commandslist
pluginslistAvailable, listInstalled, install, uninstall, setEnabled, update
marketplaceslist, add, remove, update
automationslist, run, pause, resume, history, visual, create, update, rename, delete, fork, and config methods
gitgetDiff, listBranches, checkoutBranch, push, commit, createPullRequest, resolvePullRequestStatuses
feedbacksubmitBugReport

Facade return shapes

The namespaced daemon facade normalizes common collection results:

  • sessions.list() returns session summaries with id, messageCount, and modifiedTime.
  • sessions.getMessages() returns the message array directly.
  • Collection methods such as customModels.list(), terminals.list(), mcp.listRegistry(), mcp.listTools(), commands.list(), and the plugin, marketplace, automation, and Software Factory list methods return arrays.
  • Methods whose protocol result includes additional metadata retain the result object. This includes sessions.search(), mcp.listServers(), and skills.list().

The low-level DaemonClient returns canonical protocol response types instead of these normalized facade shapes.

Observability

Runtime: Node

const session = await createSession({
  observability: {
    logger: {
      log(event) {
        console.log(event.level, event.message);
      },
    },
    metrics: {
      record(event) {
        console.log(event.name, event.value);
      },
    },
    tracing: {
      inject(carrier) {
        carrier.traceparent = currentTraceparent;
      },
    },
  },
});

Observability sinks must not throw into SDK operations. The SDK avoids sending prompt text, message content, tool inputs, raw output, and stack traces through its observability contracts.

Advanced resources

Low-level daemon integration

Most applications should use connectToDaemon(). The root entrypoint also exports DaemonClient, DaemonSessionController, MultiSessionStateManager, MultiMissionStateManager, WebSocketDaemonTransport, daemon events, and canonical protocol types for hosts that need custom connection or state ownership.

Node-only IPC and in-process transports come from the /node entrypoint:

import { DaemonClient, MachineType } from '@factory/droid-sdk';
import {
  InProcessDaemonClientTransport,
  type InProcessDaemonClientTransportOptions,
  type InProcessMessageHandler,
} from '@factory/droid-sdk/node';

function createInProcessClient(
  sendMessage: InProcessMessageHandler,
  onMessage: NonNullable<InProcessDaemonClientTransportOptions['onMessage']>
) {
  const transport = new InProcessDaemonClientTransport({
    sendMessage,
    onMessage,
  });

  return new DaemonClient({
    machineType: MachineType.Local,
    transport,
  });
}

The root also exports WebSocketDaemonTransport; the /node entrypoint exports IpcDaemonClientTransport.

Hosts can inject daemon-core logging, metrics, tracing, and domain helpers once at startup:

import { configureDaemonCoreDeps } from '@factory/droid-sdk';

configureDaemonCoreDeps({
  logger: {
    info: (message, metadata) => logger.info(message, metadata),
    error: (message, metadata) => logger.error(message, metadata),
  },
});

Unspecified dependencies keep their standalone defaults.

Low-level Node integration

Most Node applications should use run() or DroidSession. The /node entrypoint also exports ProcessTransport and DroidClient for direct JSON-RPC integrations.

Low-level RPC methods return complete response envelopes. Read successful payloads from response.result. Direct permission and AskUser handlers receive complete request events, while high-level session handlers receive request parameters.

ProcessTransport uses droidExecPath and droidExecExtraArgs for executable configuration. A custom StringFramedDroidClientTransport exchanges newline-delimited JSON-RPC strings and can replace subprocess transport entirely.

Context and token usage

Read the current context-window estimate:

const stats = await session.getContextStats();

console.log(stats.used, stats.remaining, stats.limit, stats.accuracy);

Enable partial messages to receive live token updates:

for await (const message of session.stream('Summarize this repository.', {
  includePartialMessages: true,
})) {
  if (message.type === DroidMessageType.TokenUsageUpdate) {
    console.log(message.inputTokens, message.outputTokens);
  }
}

The final result's tokenUsage contains the last observed totals or null.

Factory REST API

The browser-safe root exports REST helpers for Factory computers, machine templates, metrics, dependency installation, and remote-session listing.

import { listComputers, listRemoteSessions } from '@factory/droid-sdk';

const options = { apiKey: process.env.FACTORY_API_KEY! };

const computers = await listComputers(options);
const sessions = await listRemoteSessions(options);

REST calls use https://api.factory.ai by default. Pass baseUrl to target a different Factory API host.

ResourceFunctions
Machine templateslistMachineTemplates, getMachineTemplate
ComputerslistComputers, getComputer, getComputerByName, createComputer, updateComputer, deleteComputer, restartComputer, refreshComputer
Computer operationsgetComputerMetrics, retryInstallDeps
Remote sessionslistRemoteSessions

Unstable daemon resources

APIs under droid.unstable may change between SDK releases.

NamespaceMain methods or resources
cronslist, create, update, delete, holdSession, resumeSession
missionsinspectReadiness, acknowledgeReadinessWarning
semanticDiffgetCache, saveCache, generate
fileTransferspush, pull
proxygetToken
softwareFactoryworkstreams, signals, changes, activities, and events resources

Keep unstable API use behind an application-owned adapter.

For example, create and manage a scheduled prompt through the unstable cron resource:

const cron = await droid.unstable.crons.create({
  kind: 'root_prompt',
  scope: { type: 'root' },
  schedule: { expression: '0 * * * *', recurring: true },
  payload: {
    type: 'prompt',
    prompt: 'Check repository health',
    target: { type: 'new_session', cwd: '/workspace' },
  },
});

await droid.unstable.crons.update(cron.id, { status: 'paused' });
await droid.unstable.crons.delete(cron.id);

API reference

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

Result states

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

Common result fields:

{
  type: 'result';
  subtype:
    | 'success'
    | 'interrupted'
    | 'error_during_execution'
    | 'error_structured_output';
  sessionId: string;
  durationMs: number;
  tokenUsage: TokenUsage | null;
  messages: DroidStreamEvent[];
  text: string;
  turnCount: number;
  success: boolean;
  interrupted: boolean;
  structuredOutput?: unknown;
  structuredOutputError?: StructuredOutputError | null;
  error: ErrorEvent | null;
}

Main enums

EnumValues
AutonomyLevelOff, Low, Medium, High
DroidInteractionModeAuto, Spec, Mission; AGI is deprecated
ReasoningEffortNone, Dynamic, Off, Minimal, Low, Medium, High, ExtraHigh, Max
OutputFormatTypeJsonSchema

Model IDs and tool IDs are strings. Discover available models and tools at runtime instead of treating them as closed enums.

Main errors

Universal errors:

ErrorMeaning
ConcurrentStreamErrorA session handle already has an active stream
SessionReplacedErrorA Node source wrapper was replaced
SessionReplacementErrorSuccessor loading or rollback failed
AbortErrorA daemon operation was aborted
ConnectionClosedErrorDaemon connection closed
JsonRpcRequestErrorDaemon JSON-RPC request failed
RelayConnectionErrorRelay connection failed
WebSocketConnectionErrorWebSocket connection failed

Node-only errors:

ErrorMeaning
DroidClientErrorBase Node client error
ConnectionErrorDroid process or transport 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

Custom transports

Runtime: Node, advanced

interface StringFramedDroidClientTransport {
  isConnected: boolean;
  send(message: string): Promise<void>;
  onMessage(handler: (message: string) => void): void;
  onError(handler: (error: Error) => void): void;
  close(): Promise<void>;
}

Pass a ready transport through run(), createSession(), or resumeSession(). A custom transport bypasses subprocess creation and the Node API-key requirement.

Errors and limitations

Handle results explicitly

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;
}

Known limitations

  • Daemon replacement locking does not coordinate separate clients.
  • Browser daemon authentication currently requires an API key.
  • New-session spec handoff IDs require raw notification inspection.
  • REST remote-session support currently provides listing only.

Runnable examples

The package includes runnable examples for one-shot runs, streaming, sessions, permissions, AskUser, attachments, structured output, MCP tools, hooks, observability, spec mode, replacement lifecycle, daemon sessions, concurrent browser sessions, and daemon lifecycle operations.

All examples are under:

examples/node/
examples/browser/

Run a Node example directly:

npx tsx examples/node/run.ts

Browser examples use an interactive local launcher. Start a daemon and the example server in separate terminals:

droid daemon --host 127.0.0.1 --port 37643
npm run serve:browser-example

Open http://127.0.0.1:8420/, then enter the daemon WebSocket URL, Factory API key, and working directory. The values stay in page memory and are not stored, logged, added to the URL, or sent to the example server. The launcher includes session streaming, concurrent sessions, session lifecycle operations, and session listing examples.