@factory/droid-sdk 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/{chunk-C3KHERVH.js → chunk-FJP5OKS5.js} +6250 -4329
  2. package/dist/chunk-FJP5OKS5.js.map +1 -0
  3. package/dist/{chunk-5UXINOXG.mjs → chunk-KXDG5ZWS.mjs} +8897 -6976
  4. package/dist/chunk-KXDG5ZWS.mjs.map +1 -0
  5. package/dist/{index-D_SzTnFR.d.mts → index-BowvC0_E.d.mts} +43128 -10981
  6. package/dist/{index-D_SzTnFR.d.ts → index-BowvC0_E.d.ts} +43128 -10981
  7. package/dist/index.d.mts +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +10 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +9 -1
  12. package/dist/node.d.mts +25 -3
  13. package/dist/node.d.ts +25 -3
  14. package/dist/node.js +565 -423
  15. package/dist/node.js.map +1 -1
  16. package/dist/node.mjs +186 -44
  17. package/dist/node.mjs.map +1 -1
  18. package/docs/typescript-sdk-reference.md +903 -726
  19. package/examples/browser/daemon-model-discovery.ts +47 -0
  20. package/examples/browser/index.html +2 -1
  21. package/examples/browser/list-sessions.ts +1 -3
  22. package/examples/node/abort-session-stream.ts +2 -0
  23. package/examples/node/ask-user-elicitation.ts +11 -7
  24. package/examples/node/compact-session.ts +33 -18
  25. package/examples/node/custom-system-prompt.ts +35 -0
  26. package/examples/node/document-input.ts +58 -0
  27. package/examples/node/enter-spec-mode.ts +50 -29
  28. package/examples/node/external-mcp.ts +45 -0
  29. package/examples/node/fork-session.ts +21 -10
  30. package/examples/node/hook-execution.ts +37 -37
  31. package/examples/node/image-attachment.ts +8 -2
  32. package/examples/node/interrupt-session.ts +27 -8
  33. package/examples/node/model-discovery.ts +28 -0
  34. package/examples/node/model-selection.ts +35 -0
  35. package/examples/node/multi-turn-session.ts +4 -0
  36. package/examples/node/observability.ts +4 -17
  37. package/examples/node/partial-stream.ts +56 -0
  38. package/examples/node/permission-handler.ts +9 -4
  39. package/examples/node/rename-session.ts +7 -3
  40. package/examples/node/result-metadata.ts +10 -1
  41. package/examples/node/resume-session.ts +41 -0
  42. package/examples/node/rewind-session.ts +19 -9
  43. package/examples/node/run.ts +14 -7
  44. package/examples/node/sdk-mcp-tool.ts +10 -8
  45. package/examples/node/session-settings.ts +9 -9
  46. package/examples/node/session-stream.ts +17 -2
  47. package/examples/node/skills.ts +24 -0
  48. package/examples/node/spec-mode-new-session.ts +38 -10
  49. package/examples/node/spec-mode-same-session.ts +37 -3
  50. package/examples/node/structured-output.ts +30 -7
  51. package/package.json +1 -1
  52. package/dist/chunk-5UXINOXG.mjs.map +0 -1
  53. package/dist/chunk-C3KHERVH.js.map +0 -1
@@ -1,30 +1,31 @@
1
1
  # Factory TypeScript SDK
2
2
 
3
- The Factory TypeScript SDK lets applications run Droid, continue conversations,
4
- stream work, control tools, and connect to a running Droid daemon.
3
+ Use the Factory TypeScript SDK to run Droid from Node.js.
5
4
 
6
5
  The public npm package is `@factory/droid-sdk`.
7
6
 
8
- This page is the single user-facing SDK guide. The package's exported
9
- TypeScript declarations remain authoritative for its compile-time API surface.
7
+ This guide covers the high-level public API. The package's exported TypeScript
8
+ declarations define the complete compile-time API.
10
9
 
11
10
  ## Overview
12
11
 
13
- The SDK has two main runtimes.
12
+ Import the SDK from `@factory/droid-sdk/node`. It starts a Droid CLI subprocess
13
+ and provides session management, streaming, local session discovery, and
14
+ in-process MCP tool APIs.
14
15
 
15
- | Runtime | Import | Use it for |
16
- | ------- | ------------------------- | --------------------------------------------------------------- |
17
- | Node | `@factory/droid-sdk/node` | Scripts, CI, local tools, and apps that can start the Droid CLI |
18
- | Daemon | `@factory/droid-sdk` | Browser or Node apps that connect to an existing Droid daemon |
16
+ ### Choose an API
19
17
 
20
- The Node runtime starts a Droid subprocess for you. The daemon runtime connects
21
- to a daemon over WebSocket and can manage several sessions through one
22
- connection.
18
+ | Goal | API |
19
+ | --------------------------------------- | ----------------- |
20
+ | Run one prompt and get the final result | `run()` |
21
+ | Keep context across prompts | `createSession()` |
22
+ | Continue a saved session | `resumeSession()` |
23
+ | Find saved sessions | `listSessions()` |
23
24
 
24
- The root import is browser-safe. The `/node` import adds subprocess,
25
- filesystem, local session discovery, and SDK MCP tool APIs.
25
+ Start with `run()` for one prompt. Use a session when later prompts need the
26
+ same conversation history.
26
27
 
27
- ## Requirements and installation
28
+ ## Install and authenticate
28
29
 
29
30
  Install the package:
30
31
 
@@ -34,9 +35,8 @@ npm install @factory/droid-sdk
34
35
 
35
36
  The SDK requires Node.js 18 or later.
36
37
 
37
- By default, Node session APIs start the `droid` CLI from `PATH`. You can provide
38
- a different executable with `execPath`. Supplying a custom `transport` bypasses
39
- subprocess creation and this requirement.
38
+ By default, the SDK starts the `droid` CLI from `PATH`. Pass `execPath` to use a
39
+ different `droid` executable.
40
40
 
41
41
  Set an API key:
42
42
 
@@ -44,42 +44,30 @@ Set an API key:
44
44
  export FACTORY_API_KEY="your-key"
45
45
  ```
46
46
 
47
- You can also pass `apiKey` directly when creating a Node session.
48
-
49
- ### Browser security
50
-
51
- The daemon client currently authenticates with a Factory API key. Use browser
52
- examples only in trusted local tools. Do not ship a Factory API key in a public
53
- website, commit it to source control, or put it in a URL.
54
-
55
- An HTTPS page must connect with `wss://`. Local pages served over HTTP can use
56
- `ws://`.
47
+ The SDK reads `FACTORY_API_KEY` from the process environment by default. You
48
+ can also pass `apiKey` to `run()`, `createSession()`, or `resumeSession()`.
57
49
 
58
50
  ## Quick start
59
51
 
60
52
  ### Run one prompt
61
53
 
62
- **Runtime: Node**
63
-
64
54
  ```ts
65
55
  import { run } from '@factory/droid-sdk/node';
66
56
 
67
57
  const result = await run('Summarize this repository.');
68
58
 
69
59
  if (!result.success) {
70
- throw new Error(result.error?.message ?? 'The run did not complete.');
60
+ throw new Error(result.error?.message ?? `Run failed: ${result.subtype}`);
71
61
  }
72
62
 
73
63
  console.log(result.text);
74
64
  ```
75
65
 
76
- `run()` creates a session, waits for its final result, and closes the session.
77
- Use it for one-shot work.
66
+ `run()` creates a session, consumes its complete message stream, returns the
67
+ terminal `DroidResult`, and closes the session.
78
68
 
79
69
  ### Continue a conversation
80
70
 
81
- **Runtime: Node**
82
-
83
71
  ```ts
84
72
  import { createSession, DroidMessageType } from '@factory/droid-sdk/node';
85
73
 
@@ -102,169 +90,353 @@ try {
102
90
  }
103
91
  ```
104
92
 
105
- Use a session when later prompts need context from earlier turns.
93
+ In the above example, the second prompt uses context from the first turn.
94
+ Remember to close sessions that your code creates.
95
+
96
+ ## Core concepts
97
+
98
+ ### Session
99
+
100
+ A session holds conversation history, settings, and a working directory. Create
101
+ one with `createSession()` or load a saved session with `resumeSession()`.
102
+
103
+ ### Turn
104
+
105
+ A turn starts when you send one prompt with `session.stream()`. A session can
106
+ run one turn at a time.
107
+
108
+ ### Stream message
109
+
110
+ `session.stream()` returns an async iterable of complete messages. Narrow each
111
+ message by its `type`. Messages can report user input, assistant output, tool
112
+ activity, hooks, errors, and a terminal `DroidResult`.
113
+
114
+ ### Result
115
+
116
+ A normally completed turn ends with a `DroidResult`. `run()` consumes the stream and returns that same result type. See [Streaming and results](#streaming-and-results) for result handling, errors, token usage, and cancellation.
106
117
 
107
- ### Connect to a daemon
118
+ ### Ownership and cleanup
108
119
 
109
- **Runtime: Node or trusted local browser**
120
+ Cleanup depends on how the session was created.
121
+
122
+ | API | Cleanup |
123
+ | ----------------- | -------------------------------- |
124
+ | `run()` | Closes its session automatically |
125
+ | `createSession()` | Call `await session.close()` |
126
+ | `resumeSession()` | Call `await session.close()` |
127
+
128
+ Use `finally` blocks so cleanup also runs after an error.
129
+
130
+ ## Models
131
+
132
+ Model IDs are strings because availability depends on the account,
133
+ organization policy, and daemon configuration. Omit `modelId` to use the Droid
134
+ default.
135
+
136
+ ### Discover available models
137
+
138
+ **Runtime: Node.js**
110
139
 
111
140
  ```ts
112
- import { connectToDaemon, DroidMessageType } from '@factory/droid-sdk';
141
+ import { listModels } from '@factory/droid-sdk/node';
113
142
 
114
- async function runWithDaemon(apiKey: string, cwd: string) {
115
- const droid = await connectToDaemon({
116
- url: 'ws://127.0.0.1:37643',
117
- auth: { apiKey },
118
- });
143
+ const models = await listModels();
144
+ const catalog = await listModels({ includeDisabled: true });
145
+ ```
119
146
 
120
- const session = await droid.sessions.create({ cwd });
147
+ The Node helper starts a one-shot Droid process, requests the catalog without
148
+ creating a session, and closes the process before returning.
121
149
 
122
- try {
123
- for await (const message of session.stream('Summarize this repository.')) {
124
- if (message.type === DroidMessageType.Assistant) {
125
- console.log(message.text);
126
- }
150
+ **Runtime: Daemon** (`droid` is a client returned by `connectToDaemon()`)
151
+
152
+ ```ts
153
+ const models = await droid.models.list();
154
+ const catalog = await droid.models.list({ includeDisabled: true });
155
+
156
+ for (const model of models) {
157
+ console.log(model.id, model.supportedReasoningEfforts);
158
+ }
159
+ ```
160
+
161
+ `includeDisabled` defaults to `false`, so both APIs return only selectable
162
+ models when options are omitted. When disabled models are requested, every
163
+ disabled entry includes a `disabledReason` for display. Regular organization
164
+ members cannot enable these models. An organization manager may be able to
165
+ change the model policy or complete a required opt-in.
166
+
167
+ Runnable examples:
168
+
169
+ - Node: [`examples/node/model-discovery.ts`](../examples/node/model-discovery.ts)
170
+ - Browser and daemon: [`examples/browser/daemon-model-discovery.ts`](../examples/browser/daemon-model-discovery.ts)
171
+
172
+ Each available model includes:
173
+
174
+ | Field | Meaning |
175
+ | --------------------------- | ---------------------------------------------- |
176
+ | `id` | Value accepted by `modelId` |
177
+ | `displayName` | Full display name |
178
+ | `shortDisplayName` | Compact display name |
179
+ | `modelProvider` | Model provider |
180
+ | `supportedReasoningEfforts` | Reasoning levels accepted by the model |
181
+ | `defaultReasoningEffort` | Reasoning level used when none is supplied |
182
+ | `isCustom` | Whether the model is user-configured |
183
+ | `disabled` | Whether policy prevents selecting the model |
184
+ | `disabledReason` | Required explanation when `disabled` is `true` |
185
+ | `noImageSupport` | Whether image input is unavailable |
186
+ | `supportsImageGeneration` | Whether the model can generate images |
187
+ | `kind` | Model or router classification, when present |
188
+
189
+ Other optional fields provide tier, token multiplier, and display badges.
190
+
191
+ ### Node and daemon discovery
192
+
193
+ | Runtime | Discovery API | Lifecycle |
194
+ | ------- | --------------------- | ---------------------------------------- |
195
+ | Node | `listModels(options)` | Uses and closes a one-shot Droid process |
196
+ | Daemon | `droid.models.list()` | Reuses the connected daemon client |
197
+
198
+ Both APIs return the same `ModelInfo[]` type and include built-in models plus
199
+ configured custom models.
200
+
201
+ ### Select a model
202
+
203
+ Pass a configured model ID when creating a session or running one prompt.
204
+
205
+ ```ts
206
+ import {
207
+ createSession,
208
+ DroidMessageType,
209
+ ReasoningEffort,
210
+ } from '@factory/droid-sdk/node';
211
+
212
+ const session = await createSession({
213
+ modelId: 'model-id',
214
+ reasoningEffort: ReasoningEffort.High,
215
+ });
216
+
217
+ try {
218
+ for await (const message of session.stream('Review this repository.')) {
219
+ if (message.type === DroidMessageType.Assistant) {
220
+ console.log(message.text);
127
221
  }
128
- } finally {
129
- await Promise.allSettled([session.close()]);
130
- droid.disconnect();
131
222
  }
223
+ } finally {
224
+ await session.close();
132
225
  }
133
226
  ```
134
227
 
135
- In browser code, obtain both arguments from trusted local configuration. This
136
- does not make an API key secret from browser JavaScript, so keep this pattern
137
- restricted to trusted local pages.
228
+ For an existing session, call `updateSettings()`:
138
229
 
139
- ## Core concepts
230
+ ```ts
231
+ await session.updateSettings({
232
+ modelId: 'model-id',
233
+ reasoningEffort: ReasoningEffort.High,
234
+ });
235
+ ```
140
236
 
141
- ### Session
237
+ Set `reasoningEffort` only to a level supported by the selected model. Omit it
238
+ to use the model's configured default.
142
239
 
143
- A session holds conversation history, settings, and a working directory.
240
+ Runnable example: [`examples/node/model-selection.ts`](../examples/node/model-selection.ts)
144
241
 
145
- Node sessions are created with `createSession()` or loaded with
146
- `resumeSession()`. Daemon sessions are created or resumed through
147
- `droid.sessions`.
242
+ ### Use Auto Router
148
243
 
149
- ### Turn
244
+ Set `modelId` to `"auto"` to let Factory choose a model for each turn.
150
245
 
151
- A turn starts when you send one prompt with `session.stream()`. A session can
152
- run one turn at a time. Different daemon sessions can run turns concurrently.
246
+ ```ts
247
+ import { run } from '@factory/droid-sdk/node';
153
248
 
154
- ### Stream message
249
+ const result = await run('Find the cause of the failing tests.', {
250
+ modelId: 'auto',
251
+ });
155
252
 
156
- `session.stream()` yields a discriminated message union. The default stream
157
- contains complete user, assistant, tool, hook, error, and result messages.
253
+ if (!result.success) {
254
+ throw new Error(result.error?.message ?? `Run failed: ${result.subtype}`);
255
+ }
158
256
 
159
- ### Result
257
+ console.log(result.text);
258
+ ```
160
259
 
161
- Every normally completed stream ends with a result message. A result reports
162
- success, interruption, or failure and includes final text, messages, duration,
163
- turn count, and token usage when available.
260
+ The selected model can change between turns. Choose a fixed model ID when runs must use the same model.
164
261
 
165
- ### Node and daemon lifecycle behavior
262
+ ### Choose a spec-mode model
166
263
 
167
- Node and daemon replacement operations intentionally behave differently.
264
+ The primary model handles normal turns. The spec-mode model handles turns in
265
+ spec mode.
168
266
 
169
- | Behavior | Node session | Daemon session |
170
- | ------------------------------- | ---------------------------------------------------------- | ------------------------------ |
171
- | Successor response | `fork`: handle; `compact`/`rewind`: outcome with `session` | Outcome with `newSessionId` |
172
- | Source handle after replacement | Retired | Still usable |
173
- | Load successor | Automatic | Call `droid.sessions.resume()` |
267
+ ```ts
268
+ import {
269
+ createSession,
270
+ DroidInteractionMode,
271
+ ReasoningEffort,
272
+ } from '@factory/droid-sdk/node';
174
273
 
175
- See [Session lifecycle](#session-lifecycle) for examples.
274
+ const session = await createSession({
275
+ modelId: 'auto',
276
+ interactionMode: DroidInteractionMode.Spec,
277
+ specModeModelId: 'model-id',
278
+ specModeReasoningEffort: ReasoningEffort.High,
279
+ });
176
280
 
177
- ## One-shot runs
281
+ try {
282
+ for await (const _message of session.stream(
283
+ 'Draft an implementation plan.'
284
+ )) {
285
+ // Consume messages.
286
+ }
287
+ } finally {
288
+ await session.close();
289
+ }
290
+ ```
178
291
 
179
- **Runtime: Node**
292
+ You can also choose the model when entering spec mode:
180
293
 
181
294
  ```ts
182
- function run(prompt: string, options?: RunOptions): Promise<DroidResult>;
295
+ await session.enterSpecMode({
296
+ specModeModelId: 'model-id',
297
+ specModeReasoningEffort: ReasoningEffort.High,
298
+ });
183
299
  ```
184
300
 
185
- `run()` supports the same creation and message options as a Node session,
186
- except partial stream events.
301
+ ### Use a custom model
302
+
303
+ Configure custom models in `~/.factory/settings.json`. See [Custom Models (BYOK)](https://docs.factory.ai/cli/byok/overview) for provider, credential, and model settings.
304
+
305
+ After configuration, pass the custom model ID anywhere the SDK accepts `modelId`. The ID uses `custom:` followed by the configured `model` value.
187
306
 
188
307
  ```ts
189
- const result = await run('Review the authentication middleware.', {
190
- cwd: '/path/to/repository',
191
- modelId: 'model-id',
192
- reasoningEffort: ReasoningEffort.High,
193
- disabledToolIds: ['Execute'],
308
+ import { run } from '@factory/droid-sdk/node';
309
+
310
+ const result = await run('Summarize this repository.', {
311
+ modelId: 'custom:gpt-4o-mini',
194
312
  });
195
- ```
196
313
 
197
- Common options:
198
-
199
- | Option | Purpose |
200
- | ------------------------------ | ---------------------------------------------- |
201
- | `cwd` | Working directory. Defaults to `process.cwd()` |
202
- | `apiKey` | Factory API key. Defaults to `FACTORY_API_KEY` |
203
- | `modelId` | Model selection |
204
- | `reasoningEffort` | Model reasoning level |
205
- | `autonomyLevel` | Permission policy level |
206
- | `interactionMode` | Auto, spec, or mission mode. AGI is deprecated |
207
- | `specModeModelId` | Model used in spec mode |
208
- | `specModeReasoningEffort` | Reasoning level used in spec mode |
209
- | `disabledToolIds` | Tools that Droid cannot use |
210
- | `autoRejectPermissionRequests` | Reject all permission requests automatically |
211
- | `disableBuiltinSkills` | Hide Factory-provided built-in skills |
212
- | `permissionHandler` | Handles permission requests |
213
- | `askUserHandler` | Answers AskUser requests |
214
- | `tags` | Session attribution tags |
215
- | `sessionSource` | Session source attribution |
216
- | `images` | Base64 image attachments |
217
- | `files` | Text or PDF attachments |
218
- | `outputFormat` | JSON schema for structured output |
219
- | `abortSignal` | Cancels the run |
220
- | `mcpServers` | MCP servers available to the session |
221
- | `observability` | Logging, metrics, and trace integration |
222
- | `execPath` | Droid executable path |
223
- | `execArgs` | Additional Droid process arguments |
224
- | `env` | Additional Droid process environment variables |
225
- | `transport` | Custom string-framed transport |
314
+ if (!result.success) {
315
+ throw new Error(result.error?.message ?? `Run failed: ${result.subtype}`);
316
+ }
317
+
318
+ console.log(result.text);
319
+ ```
226
320
 
227
321
  ## Sessions
228
322
 
229
- ### Create a Node session
323
+ Use a session when prompts need shared conversation history. A session owns its working directory, settings, and one active turn at a time.
324
+
325
+ ### Create and use a session
230
326
 
231
327
  ```ts
232
- import { createSession } from '@factory/droid-sdk/node';
328
+ import { createSession, DroidMessageType } from '@factory/droid-sdk/node';
233
329
 
234
330
  const session = await createSession({
235
331
  cwd: process.cwd(),
236
- modelId: 'model-id',
237
332
  });
333
+
334
+ async function send(prompt: string) {
335
+ for await (const message of session.stream(prompt)) {
336
+ if (message.type === DroidMessageType.Assistant) {
337
+ console.log(message.text);
338
+ }
339
+ }
340
+ }
341
+
342
+ try {
343
+ await send('What does this project do?');
344
+ await send('What should I test first?');
345
+ } finally {
346
+ await session.close();
347
+ }
348
+ ```
349
+
350
+ The second turn uses context from the first. `cwd` defaults to `process.cwd()`. Model and reasoning defaults come from Droid settings.
351
+
352
+ Runnable example:
353
+ [`examples/node/multi-turn-session.ts`](../examples/node/multi-turn-session.ts)
354
+
355
+ ### Resume a saved session
356
+
357
+ ```ts
358
+ import { DroidMessageType, resumeSession } from '@factory/droid-sdk/node';
359
+
360
+ async function continueSession(sessionId: string) {
361
+ const session = await resumeSession(sessionId);
362
+
363
+ try {
364
+ for await (const message of session.stream(
365
+ 'Continue from the last conversation.'
366
+ )) {
367
+ if (message.type === DroidMessageType.Assistant) {
368
+ console.log(message.text);
369
+ }
370
+ }
371
+ } finally {
372
+ await session.close();
373
+ }
374
+ }
238
375
  ```
239
376
 
240
- When omitted, `cwd` defaults to `process.cwd()`. Model and reasoning defaults
241
- come from Droid settings.
377
+ `resumeSession()` restores the saved conversation, working directory, and session settings. It does not accept `cwd`, `modelId`, or reasoning options.
378
+
379
+ Resume options can provide new permission and AskUser handlers, disabled tools, MCP servers, an abort signal, and observability sinks.
380
+
381
+ ### Customize the system prompt
242
382
 
243
- ### Resume a Node session
383
+ Use `systemPrompt` to append instructions to Droid's effective built-in prompt
384
+ or replace the behavioral prompt:
244
385
 
245
386
  ```ts
246
- import { resumeSession } from '@factory/droid-sdk/node';
387
+ const appended = await createSession({
388
+ systemPrompt: {
389
+ type: 'preset',
390
+ preset: 'droid',
391
+ append: 'Prioritize security findings and cite relevant files.',
392
+ },
393
+ });
247
394
 
248
- const session = await resumeSession('session-id');
395
+ const replaced = await createSession({
396
+ systemPrompt: 'Act as a focused dependency-analysis agent.',
397
+ });
249
398
  ```
250
399
 
251
- The saved session owns its working directory and settings. Resume options can
252
- change handlers, transports, disabled tools, MCP servers, and observability,
253
- but cannot replace saved model or working-directory settings.
400
+ The Droid preset retains the prompt that the session would normally use,
401
+ including specialized exec and mission prompts, and appends your instructions.
402
+ A string replaces that behavioral prompt. Both forms retain the mandatory
403
+ Droid identity block and required model/tool guidance.
404
+
405
+ The option is available to `run()`, `createSession()`, and
406
+ `droid.sessions.create()`. The prompt is fixed when the session is created.
407
+ It is saved in the session settings file, restored by `resumeSession()` and
408
+ `droid.sessions.resume()`, and inherited unchanged by forks.
409
+
410
+ The prompt configuration is stored in the sibling session settings file, not
411
+ serialized as a system event in the transcript JSONL. As with any instruction,
412
+ the model can still repeat prompt text in its generated messages or thinking,
413
+ which are part of the transcript. Treat both files as sensitive.
414
+
415
+ Runnable example: [`examples/node/custom-system-prompt.ts`](../examples/node/custom-system-prompt.ts)
416
+
417
+ ### Read session state
254
418
 
255
- ### Session state
419
+ Each handle exposes its ID, current working directory, and current settings.
256
420
 
257
421
  ```ts
258
422
  console.log(session.id);
259
423
  console.log(session.cwd);
260
424
  console.log(session.settings.modelId);
425
+ console.log(session.settings.reasoningEffort);
426
+ console.log(session.settings.interactionMode);
261
427
  ```
262
428
 
263
- `settings` and `cwd` update when the running session reports changes.
429
+ `settings` is read-only. The SDK updates `settings` and `cwd` when Droid reports a change.
264
430
 
265
- ### Update settings
431
+ Runnable example: [`examples/node/init-metadata.ts`](../examples/node/init-metadata.ts)
432
+
433
+ ### Update active-session settings
434
+
435
+ Use `updateSettings()` for changes that should apply to later turns in the current session.
266
436
 
267
437
  ```ts
438
+ import { AutonomyLevel, ReasoningEffort } from '@factory/droid-sdk/node';
439
+
268
440
  await session.updateSettings({
269
441
  modelId: 'model-id',
270
442
  reasoningEffort: ReasoningEffort.High,
@@ -273,32 +445,82 @@ await session.updateSettings({
273
445
  });
274
446
  ```
275
447
 
276
- Public settings include model, reasoning, interaction mode, autonomy, spec
277
- mode settings, tags, mission settings, compaction settings, and disabled tools.
448
+ Updatable settings include:
449
+
450
+ - model and reasoning effort
451
+ - interaction mode and autonomy level
452
+ - spec-mode model settings
453
+ - tool availability overrides
454
+
455
+ 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.
456
+
457
+ When only changing interaction modes, use `enterSpecMode()` and `exitSpecMode()` instead of setting `interactionMode` directly.
458
+
459
+ The updated values are available through `session.settings`.
460
+
461
+ Runnable example: [`examples/node/session-settings.ts`](../examples/node/session-settings.ts)
278
462
 
279
463
  ### Rename a session
280
464
 
465
+ Use `rename()` after the first turn to replace the generated title.
466
+
281
467
  ```ts
282
468
  await session.rename({ title: 'Authentication review' });
283
469
  ```
284
470
 
285
- ### Discover saved Node sessions
471
+ Runnable example:
472
+ [`examples/node/rename-session.ts`](../examples/node/rename-session.ts)
473
+
474
+ ### List saved sessions
475
+
476
+ `listSessions()` reads local Droid session storage. It does not start Droid or
477
+ call the Factory API.
478
+
479
+ List sessions for the current working directory:
286
480
 
287
481
  ```ts
288
482
  import { listSessions } from '@factory/droid-sdk/node';
289
483
 
484
+ const sessions = await listSessions({ limit: 10 });
485
+
486
+ for (const session of sessions) {
487
+ console.log(session.id, session.title, session.modifiedTime);
488
+ }
489
+ ```
490
+
491
+ List sessions across all working directories:
492
+
493
+ ```ts
290
494
  const sessions = await listSessions({
291
- cwd: process.cwd(),
495
+ fetchOutsideCWD: true,
292
496
  limit: 10,
293
497
  });
294
498
  ```
295
499
 
296
- `listSessions()` reads local session storage. It does not call the Factory REST
297
- API or a daemon.
500
+ 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.
501
+
502
+ Runnable example: [`examples/node/list-sessions.ts`](../examples/node/list-sessions.ts)
503
+
504
+ ### What persists
505
+
506
+ Droid saves the session data needed to continue later.
507
+
508
+ | Restored from the saved session | Attach again when resuming |
509
+ | ------------------------------- | -------------------------- |
510
+ | Conversation history | Permission handler |
511
+ | Working directory | AskUser handler |
512
+ | Title | SDK MCP servers |
513
+ | Session settings | Observability sinks |
514
+ | | Abort signal |
515
+
516
+ Session settings include model, reasoning, Auto or Spec interaction mode, autonomy, spec-mode model settings, and tool availability overrides.
517
+
518
+ 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.
298
519
 
299
- ### Close a session
520
+ ### Close sessions safely
300
521
 
301
- Call `close()` in `finally` when your code owns a Node session.
522
+ Call `close()` in `finally` when your code creates or resumes a session.
523
+ Closing releases the subprocess, subscriptions, and SDK-owned MCP servers.
302
524
 
303
525
  ```ts
304
526
  const session = await createSession();
@@ -310,11 +532,13 @@ try {
310
532
  }
311
533
  ```
312
534
 
313
- ## Streaming
535
+ ## Streaming and results
314
536
 
315
- ### Complete messages
537
+ Each call to `session.stream()` starts one turn and returns an async iterable.
538
+
539
+ By default, it yields complete messages and ends with a terminal `DroidResult`.
316
540
 
317
- Complete messages are the default.
541
+ ### Complete messages
318
542
 
319
543
  ```ts
320
544
  for await (const message of session.stream('Find the failing test.')) {
@@ -342,12 +566,60 @@ Default message types:
342
566
  | `tool_result` | Complete tool result |
343
567
  | `hook` | Hook execution |
344
568
  | `error` | Runtime error event |
345
- | `result` | Final turn result |
569
+ | `result` | Terminal `DroidResult` |
570
+
571
+ Runnable example: [`examples/node/session-stream.ts`](../examples/node/session-stream.ts)
572
+
573
+ ### Handle the result
574
+
575
+ `DroidResult` is a discriminated union. Check `subtype` or `success` before using the response.
576
+
577
+ ```ts
578
+ const result = await run('Run the test suite.');
579
+
580
+ switch (result.subtype) {
581
+ case 'success':
582
+ console.log(result.text);
583
+ break;
584
+ case 'interrupted':
585
+ console.log('The run was interrupted.');
586
+ break;
587
+ case 'error_during_execution':
588
+ case 'error_structured_output':
589
+ console.error(
590
+ result.structuredOutputError?.message ??
591
+ result.error?.message ??
592
+ 'Unknown error'
593
+ );
594
+ break;
595
+ }
596
+ ```
597
+
598
+ | Subtype | Meaning |
599
+ | ------------------------- | ------------------------------------------------- |
600
+ | `success` | The turn completed successfully |
601
+ | `interrupted` | The turn was cancelled or permission declined |
602
+ | `error_during_execution` | Droid could not complete the requested work |
603
+ | `error_structured_output` | Structured-output generation or validation failed |
604
+
605
+ Every result includes:
606
+
607
+ ```ts
608
+ console.log(result.sessionId);
609
+ console.log(result.durationMs);
610
+ console.log(result.turnCount);
611
+ console.log(result.messages);
612
+ console.log(result.text);
613
+ console.log(result.tokenUsage);
614
+ ```
615
+
616
+ `run()` returns the terminal result directly. When consuming `session.stream()`, handle the message whose type is `DroidMessageType.Result`.
617
+
618
+ Runnable example: [`examples/node/result-metadata.ts`](../examples/node/result-metadata.ts)
346
619
 
347
620
  ### Partial events
348
621
 
349
- Use partial events only when the application needs live text, thinking, tool
350
- progress, usage, or state updates.
622
+ Enable partial events only when the application needs live text, thinking, tool progress, token usage, or state updates.
351
623
 
352
624
  ```ts
353
625
  for await (const event of session.stream('Explain the test failure.', {
@@ -361,21 +633,152 @@ for await (const event of session.stream('Explain the test failure.', {
361
633
 
362
634
  Partial streams can also include thinking deltas, tool-call deltas, tool
363
635
  progress, token updates, permission results, settings changes, working-state
364
- changes, MCP status, and mission events.
636
+ changes, and MCP status.
637
+
638
+ Runnable example: [`examples/node/partial-stream.ts`](../examples/node/partial-stream.ts)
639
+
640
+ ### Token and context usage
641
+
642
+ `DroidResult.tokenUsage` reports raw token counts for the current SDK turn and,
643
+ when available, the Factory Service Credits (FSC) charged for that turn. One
644
+ turn is one `run()` or `session.stream()` call. This is not the session's
645
+ lifetime total.
646
+
647
+ ```ts
648
+ if (result.tokenUsage) {
649
+ console.log(`input: ${result.tokenUsage.inputTokens}`);
650
+ console.log(`output: ${result.tokenUsage.outputTokens}`);
651
+ console.log(`cache read: ${result.tokenUsage.cacheReadTokens}`);
652
+ console.log(`cache created: ${result.tokenUsage.cacheCreationTokens}`);
653
+ if (result.tokenUsage.factoryCredits !== undefined) {
654
+ console.log(`Factory Service Credits: ${result.tokenUsage.factoryCredits}`);
655
+ }
656
+ }
657
+ ```
658
+
659
+ | Field | What it measures during the turn |
660
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
661
+ | `inputTokens` | Provider-reported input across model calls, which can include instructions, tools, conversation history, and the latest message |
662
+ | `outputTokens` | Output generated across model calls, not only the final visible response |
663
+ | `cacheReadTokens` | Input tokens reused from the prompt cache |
664
+ | `cacheCreationTokens` | Input tokens written to the prompt cache |
665
+ | `thinkingTokens` | Reasoning tokens reported separately when available |
666
+ | `factoryCredits` | Factory Service Credits charged, when available |
667
+
668
+ 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.
669
+
670
+ Partial streams can emit `DroidMessageType.TokenUsageUpdate`. Unlike the per-turn result, these events contain cumulative committed usage for the session.
671
+
672
+ `getContextStats()` measures current context occupancy, not cumulative token usage:
673
+
674
+ ```ts
675
+ const stats = await session.getContextStats();
676
+
677
+ console.log(stats.used, stats.remaining, stats.limit, stats.accuracy);
678
+ ```
679
+
680
+ | Field | Meaning |
681
+ | ----------- | ----------------------------------------------------------------------------------------------------- |
682
+ | `used` | Estimated tokens currently occupied by prompts, tools, instructions, and conversation history |
683
+ | `limit` | Active model's maximum input window; unresolved model IDs fall back to the effective compaction limit |
684
+ | `remaining` | `Math.max(0, limit - used)` |
685
+ | `accuracy` | Currently `estimated`, because Droid approximates tokens from character counts |
686
+
687
+ `remaining` estimates room in the model's input window. It does not
688
+ necessarily mean tokens remaining before compaction. Droid can compact at a
689
+ lower configured threshold, commonly 250,000 tokens, even when the active
690
+ model's input window is larger.
691
+
692
+ ### Errors
693
+
694
+ An error event and a thrown exception mean different things:
695
+
696
+ - An `error` message reports a runtime problem during the turn. A terminal `DroidResult` normally follows and describes the final outcome.
697
+ - A failed `DroidResult` means the stream completed, but the requested work or structured output failed.
698
+ - A thrown exception means the SDK operation itself could not finish, such as an abort, connection failure, protocol error, or concurrent stream attempt.
699
+
700
+ Handle stream exceptions around the iteration:
701
+
702
+ ```ts
703
+ try {
704
+ for await (const message of session.stream('Run the tests.')) {
705
+ if (message.type === DroidMessageType.Error) {
706
+ console.error(message.message);
707
+ }
708
+ }
709
+ } catch (error) {
710
+ console.error('The stream could not finish:', error);
711
+ }
712
+ ```
713
+
714
+ `run()` follows the same distinction: agent failures are returned as failed results, while setup, transport, protocol, and abort failures are thrown.
365
715
 
366
716
  ### Stream concurrency
367
717
 
368
718
  One session handle can have one active stream. Starting another active stream
369
719
  throws `ConcurrentStreamError`.
370
720
 
371
- Daemon sessions on the same connection may stream concurrently.
721
+ ### Cancellation and interruption
722
+
723
+ Use an abort signal to cancel one turn:
724
+
725
+ ```ts
726
+ const controller = new AbortController();
727
+
728
+ setTimeout(
729
+ () => controller.abort(new Error('Timed out after 5 seconds')),
730
+ 5_000
731
+ );
732
+
733
+ for await (const message of session.stream('Perform a long review.', {
734
+ abortSignal: controller.signal,
735
+ })) {
736
+ // Handle messages.
737
+ }
738
+ ```
739
+
740
+ The stream interrupts the active turn and throws the abort reason.
741
+
742
+ Use `session.interrupt()` when another part of the application needs to stop
743
+ the active turn:
744
+
745
+ ```ts
746
+ await session.interrupt();
747
+ ```
748
+
749
+ The stream normally continues to a terminal result with subtype `interrupted`.
750
+ The session remains available for later prompts.
751
+
752
+ Breaking out of the stream also interrupts the active turn:
753
+
754
+ ```ts
755
+ for await (const message of session.stream('Investigate every failing test.')) {
756
+ if (message.type === DroidMessageType.Assistant) {
757
+ console.log(message.text);
758
+
759
+ // Interrupt this turn instead of merely hiding its remaining output.
760
+ break;
761
+ }
762
+ }
763
+ ```
764
+
765
+ Breaking out of the loop before the turn finishes sends an interrupt request
766
+ to Droid. This cancels the remaining model and tool work for that turn. The
767
+ session remains open and can accept another prompt, but this loop does not
768
+ receive the interrupted turn's terminal `DroidResult`.
769
+
770
+ A session-level abort signal passed to `createSession()` closes the entire
771
+ session when aborted.
372
772
 
373
- ### Stop consuming a stream
773
+ Runnable examples:
374
774
 
375
- Breaking out of a stream early interrupts the active server turn.
775
+ - [`examples/node/abort-session-stream.ts`](../examples/node/abort-session-stream.ts)
776
+ - [`examples/node/interrupt-session.ts`](../examples/node/interrupt-session.ts)
376
777
 
377
778
  ## Inputs and outputs
378
779
 
780
+ 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.
781
+
379
782
  ### Images
380
783
 
381
784
  ```ts
@@ -390,12 +793,14 @@ const result = await run('Describe this image.', {
390
793
  });
391
794
  ```
392
795
 
393
- Supported image types are JPEG, PNG, GIF, and WebP.
796
+ `data` contains the base64-encoded image bytes. Supported media types are `image/jpeg`, `image/png`, `image/gif`, and `image/webp`.
797
+
798
+ Runnable example: [`examples/node/image-attachment.ts`](../examples/node/image-attachment.ts)
394
799
 
395
800
  ### Documents
396
801
 
397
802
  ```ts
398
- for await (const _message of session.stream('Summarize this document.', {
803
+ const result = await run('Summarize this document.', {
399
804
  files: [
400
805
  {
401
806
  type: 'text',
@@ -404,109 +809,69 @@ for await (const _message of session.stream('Summarize this document.', {
404
809
  name: 'report.txt',
405
810
  },
406
811
  ],
407
- })) {
408
- // Consume messages.
409
- }
410
- ```
411
-
412
- Document inputs support plain text and base64 PDF data.
413
-
414
- ### Structured output
415
-
416
- Use structured output when another system needs a predictable result.
417
-
418
- ```ts
419
- import { OutputFormatType, run } from '@factory/droid-sdk/node';
420
-
421
- const result = await run('Review this function for correctness issues.', {
422
- outputFormat: {
423
- type: OutputFormatType.JsonSchema,
424
- schema: {
425
- type: 'object',
426
- properties: {
427
- summary: { type: 'string' },
428
- findings: {
429
- type: 'array',
430
- items: {
431
- type: 'object',
432
- properties: {
433
- line: { type: 'number' },
434
- severity: {
435
- type: 'string',
436
- enum: ['low', 'medium', 'high'],
437
- },
438
- message: { type: 'string' },
439
- },
440
- required: ['line', 'severity', 'message'],
441
- },
442
- },
443
- },
444
- required: ['summary', 'findings'],
445
- },
446
- },
447
812
  });
448
-
449
- if (!result.success) {
450
- throw new Error(
451
- result.structuredOutputError?.message ??
452
- result.error?.message ??
453
- 'Structured output failed.'
454
- );
455
- }
456
-
457
- console.log(result.structuredOutput);
458
813
  ```
459
814
 
460
- `structuredOutput` is typed as `unknown`. Validate or narrow it before passing
461
- it to another system. A successful result can omit structured output.
462
-
463
- ### Result metadata
815
+ Document inputs support:
464
816
 
465
- ```ts
466
- console.log(result.sessionId);
467
- console.log(result.durationMs);
468
- console.log(result.turnCount);
469
- console.log(result.tokenUsage);
470
- console.log(result.messages);
471
- ```
817
+ | Source | `type` | `mediaType` | `data` |
818
+ | ------------ | -------- | ----------------- | ----------------------------- |
819
+ | Plain text | `text` | `text/plain` | The document text |
820
+ | PDF document | `base64` | `application/pdf` | Base64-encoded PDF file bytes |
472
821
 
473
- `tokenUsage` is `null` when usage is unavailable.
822
+ `name` is optional for both forms. Attach content through `data`; a filesystem path by itself is not a document upload.
474
823
 
475
- ## Cancellation and interruption
824
+ Runnable example: [`examples/node/document-input.ts`](../examples/node/document-input.ts)
476
825
 
477
- ### Abort one turn
826
+ ### Structured output
827
+
828
+ Use `outputFormat` when another system needs a JSON object that follows a schema.
478
829
 
479
830
  ```ts
480
- const controller = new AbortController();
831
+ import { OutputFormatType, run } from '@factory/droid-sdk/node';
481
832
 
482
- setTimeout(() => controller.abort(), 5_000);
833
+ const result = await run('Return the repository name and test command.', {
834
+ outputFormat: {
835
+ type: OutputFormatType.JsonSchema,
836
+ schema: {
837
+ type: 'object',
838
+ properties: {
839
+ repository: { type: 'string' },
840
+ testCommand: { type: 'string' },
841
+ },
842
+ required: ['repository', 'testCommand'],
843
+ additionalProperties: false,
844
+ },
845
+ },
846
+ });
483
847
 
484
- for await (const message of session.stream('Perform a long review.', {
485
- abortSignal: controller.signal,
486
- })) {
487
- // Handle messages.
848
+ if (!result.success) {
849
+ throw new Error(
850
+ result.structuredOutputError?.message ??
851
+ result.error?.message ??
852
+ `Structured output failed (${result.subtype}).`
853
+ );
488
854
  }
489
- ```
490
-
491
- The stream throws the abort reason after interrupting the active turn.
492
855
 
493
- ### Interrupt and keep the session
856
+ if (!result.structuredOutput) {
857
+ throw new Error('No structured output was returned.');
858
+ }
494
859
 
495
- ```ts
496
- await session.interrupt();
860
+ console.log(JSON.stringify(result.structuredOutput, null, 2));
497
861
  ```
498
862
 
499
- `interrupt()` stops the active turn on the server. The stream continues to its
500
- terminal result, normally with subtype `interrupted`. The session remains
501
- usable for later prompts.
863
+ Invalid or missing schema output normally produces a failed result with subtype `error_structured_output`; `structuredOutputError` contains the available validation details.
502
864
 
503
- A session-level abort signal passed to `createSession()` closes the whole
504
- session when aborted.
865
+ `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.
866
+
867
+ Runnable example: [`examples/node/structured-output.ts`](../examples/node/structured-output.ts)
505
868
 
506
869
  ## Permissions and user input
507
870
 
508
871
  ### Autonomy
509
872
 
873
+ Autonomy controls which actions require approval in Auto mode.
874
+
510
875
  ```ts
511
876
  import { AutonomyLevel } from '@factory/droid-sdk/node';
512
877
 
@@ -515,11 +880,20 @@ const session = await createSession({
515
880
  });
516
881
  ```
517
882
 
518
- Levels are `Off`, `Low`, `Medium`, and `High`. When omitted, Droid settings
519
- choose the default.
883
+ | Level | Behavior |
884
+ | -------- | --------------------------------------- |
885
+ | `Off` | Ask before every action |
886
+ | `Low` | Allow file edits and read-only commands |
887
+ | `Medium` | Allow reversible commands |
888
+ | `High` | Allow commands without approval prompts |
889
+
890
+ When omitted, Droid uses the configured default. Autonomy does not select Auto or Spec mode; `interactionMode` does that.
520
891
 
521
892
  ### Permission handler
522
893
 
894
+ Use `permissionHandler` to approve or reject requests that reach the client.
895
+ The handler may be synchronous or asynchronous.
896
+
523
897
  ```ts
524
898
  import {
525
899
  ToolConfirmationOutcome,
@@ -528,32 +902,45 @@ import {
528
902
 
529
903
  const session = await createSession({
530
904
  autonomyLevel: AutonomyLevel.Off,
531
- permissionHandler(request) {
532
- const safe =
533
- request.toolUses.length > 0 &&
534
- request.toolUses.every(
535
- (use) => use.details.type === ToolConfirmationType.Create
536
- );
537
-
538
- return safe
905
+ permissionHandler({ toolUses, options }) {
906
+ const createsOnly =
907
+ toolUses.length > 0 &&
908
+ toolUses.every((use) => use.details.type === ToolConfirmationType.Create);
909
+
910
+ const desired = createsOnly
539
911
  ? ToolConfirmationOutcome.ProceedOnce
540
912
  : ToolConfirmationOutcome.Cancel;
913
+
914
+ const offered = options.find((option) => option.value === desired);
915
+ if (!offered) throw new Error(`Outcome not offered: ${desired}`);
916
+
917
+ return desired;
541
918
  },
542
919
  });
543
920
  ```
544
921
 
545
- The handler must return an outcome supplied by the request. Invalid results and
546
- handler errors cancel the request.
922
+ `toolUses` describes the pending actions. `options` contains the outcomes available for this request. Return one of those values. Common outcomes are:
923
+
924
+ | Outcome | Effect |
925
+ | --------------- | --------------------------------------- |
926
+ | `ProceedOnce` | Approve this request |
927
+ | `ProceedAlways` | Approve and persist the applicable rule |
928
+ | `Cancel` | Reject the request |
547
929
 
548
- Without a handler, permission requests are cancelled.
930
+ The available outcomes vary by request. Returning an unavailable value, throwing from the handler, or omitting the handler cancels the request.
931
+
932
+ Runnable example:
933
+ [`examples/node/permission-handler.ts`](../examples/node/permission-handler.ts)
549
934
 
550
935
  ### AskUser handler
551
936
 
937
+ `askUserHandler` answers questions Droid asks during a turn. Connect it to a form, prompt, or other application UI.
938
+
552
939
  ```ts
553
940
  const result = await run('Ask me which environment to deploy.', {
554
- askUserHandler(request) {
941
+ askUserHandler({ questions }) {
555
942
  return {
556
- answers: request.questions.map((question) => ({
943
+ answers: questions.map((question) => ({
557
944
  index: question.index,
558
945
  question: question.question,
559
946
  answer: question.options[0] ?? 'none',
@@ -563,7 +950,7 @@ const result = await run('Ask me which environment to deploy.', {
563
950
  });
564
951
  ```
565
952
 
566
- To decline:
953
+ Each answer must copy the question's `index` and `question`. To decline the questionnaire:
567
954
 
568
955
  ```ts
569
956
  return { cancelled: true, answers: [] };
@@ -571,16 +958,24 @@ return { cancelled: true, answers: [] };
571
958
 
572
959
  Without a handler, AskUser requests are declined.
573
960
 
574
- ### Disable tools
961
+ Runnable example:
962
+ [`examples/node/ask-user-elicitation.ts`](../examples/node/ask-user-elicitation.ts)
963
+
964
+ ### Tool controls
965
+
966
+ Disable tools when creating, resuming, or updating a session:
575
967
 
576
968
  ```ts
577
969
  const session = await createSession({
578
970
  disabledToolIds: ['Execute'],
579
971
  });
972
+
973
+ await session.updateSettings({
974
+ disabledToolIds: ['Execute', 'Edit'],
975
+ });
580
976
  ```
581
977
 
582
- Tool IDs are strings. Use `listTools()` to inspect the tools available to the
583
- current session.
978
+ Tool IDs are strings. Use `listTools()` to inspect the tools available to the current session.
584
979
 
585
980
  ```ts
586
981
  const tools = await session.listTools();
@@ -593,27 +988,50 @@ for (const tool of tools) {
593
988
  The public SDK provides subtractive `disabledToolIds`. It does not expose a
594
989
  restrictive allowlist.
595
990
 
991
+ Runnable example: [`examples/node/tool-controls.ts`](../examples/node/tool-controls.ts)
992
+
596
993
  ## Tools and extensions
597
994
 
995
+ Use the extension that matches the job:
996
+
997
+ | Extension | Use it for |
998
+ | -------------------- | ------------------------------------------------ |
999
+ | Skills | Reusable instructions and supporting files |
1000
+ | SDK MCP tools | In-process TypeScript functions exposed to Droid |
1001
+ | External MCP servers | Tools provided by another process or service |
1002
+ | Hooks | Commands that run at defined lifecycle events |
1003
+
598
1004
  ### Skills
599
1005
 
1006
+ List the skills available to the session:
1007
+
600
1008
  ```ts
601
1009
  const { skills } = await session.listSkills();
602
1010
 
1011
+ for (const skill of skills) {
1012
+ console.log(skill.name, skill.enabled);
1013
+ }
1014
+ ```
1015
+
1016
+ Enable or disable a skill at the user or project level:
1017
+
1018
+ ```ts
1019
+ import { SettingsLevel } from '@factory/droid-sdk/node';
1020
+
603
1021
  await session.setSkillDisabled({
604
1022
  skillName: 'skill-name',
605
1023
  disabled: true,
606
- settingsLevel: 'project',
1024
+ settingsLevel: SettingsLevel.Project,
607
1025
  });
608
1026
  ```
609
1027
 
610
- Skills may come from project, personal, built-in, or automation settings.
1028
+ Skills can come from project, personal, built-in, or automation settings.
611
1029
 
612
- ### SDK MCP tools
1030
+ Runnable example: [`examples/node/skills.ts`](../examples/node/skills.ts)
613
1031
 
614
- **Runtime: Node**
1032
+ ### SDK MCP tools
615
1033
 
616
- Use an SDK MCP server to define an in-process tool with a typed Zod input.
1034
+ Use `tool()` to define an in-process TypeScript function with a Zod input schema.
617
1035
 
618
1036
  ```ts
619
1037
  import { z } from 'zod';
@@ -623,7 +1041,7 @@ import {
623
1041
  tool,
624
1042
  } from '@factory/droid-sdk/node';
625
1043
 
626
- const tools = createSdkMcpServer({
1044
+ const server = createSdkMcpServer({
627
1045
  name: 'review-tools',
628
1046
  tools: [
629
1047
  tool(
@@ -636,35 +1054,91 @@ const tools = createSdkMcpServer({
636
1054
  });
637
1055
 
638
1056
  const session = await createSession({
639
- mcpServers: [tools],
1057
+ mcpServers: [server],
640
1058
  });
1059
+
1060
+ try {
1061
+ for await (const _message of session.stream(
1062
+ 'Find the owner of src/index.ts.'
1063
+ )) {
1064
+ // Consume messages.
1065
+ }
1066
+ } finally {
1067
+ await session.close();
1068
+ }
641
1069
  ```
642
1070
 
643
- The SDK starts an authenticated loopback MCP server and closes it with the
644
- session.
1071
+ The SDK starts an authenticated loopback MCP server and closes it with the session.
1072
+ Tool calls still follow the session's autonomy and permission rules.
1073
+ Attach SDK MCP servers again when resuming a saved session because they are runtime objects, not persisted session data.
1074
+
1075
+ Runnable example: [`examples/node/sdk-mcp-tool.ts`](../examples/node/sdk-mcp-tool.ts)
645
1076
 
646
1077
  ### External MCP servers
647
1078
 
648
- Node sessions can add, remove, toggle, list, and authenticate MCP servers.
1079
+ Pass external MCP configuration when creating or resuming a session:
649
1080
 
650
1081
  ```ts
651
- await session.addMcpServer({
652
- name: 'docs',
653
- type: 'http',
654
- url: 'https://example.com/mcp',
1082
+ const session = await createSession({
1083
+ mcpServers: [
1084
+ {
1085
+ name: 'docs',
1086
+ type: 'http',
1087
+ url: 'https://example.com/mcp',
1088
+ headers: [],
1089
+ },
1090
+ ],
655
1091
  });
1092
+ ```
1093
+
1094
+ Supported session configuration:
1095
+
1096
+ | Transport | `type` field | Required configuration |
1097
+ | ------------- | ------------ | ---------------------- |
1098
+ | Local process | Omit | `command` and `args` |
1099
+ | HTTP | `http` | `url` and `headers` |
1100
+ | SSE | `sse` | `url` and `headers` |
1101
+
1102
+ Inspect server status and discovered tools:
1103
+
1104
+ ```ts
1105
+ try {
1106
+ const { servers, summary } = await session.listMcpServers();
1107
+ const tools = await session.listMcpTools();
656
1108
 
657
- const servers = await session.listMcpServers();
658
- const tools = await session.listMcpTools();
1109
+ console.log(servers, summary, tools);
1110
+ } finally {
1111
+ await session.close();
1112
+ }
659
1113
  ```
660
1114
 
661
- The daemon client exposes the complete MCP management API through `droid.mcp`.
1115
+ `addMcpServer()`, `removeMcpServer()`, and `toggleMcpServer()` change the user's Droid configuration.
1116
+ Use `authenticateMcpServer({ serverName })` when a server requires authentication.
1117
+
1118
+ Runnable example: [`examples/node/external-mcp.ts`](../examples/node/external-mcp.ts)
662
1119
 
663
1120
  ### Hooks
664
1121
 
665
- Hooks are configured in `.factory/hooks.json`. They can run before or after
666
- tools, when prompts are submitted, when notifications arrive, during
667
- compaction, and when sessions or subagents stop.
1122
+ Hooks run shell commands at defined lifecycle events.
1123
+ Configure them in `.factory/hooks.json`:
1124
+
1125
+ ```json
1126
+ {
1127
+ "PreToolUse": [
1128
+ {
1129
+ "matcher": "Execute",
1130
+ "hooks": [
1131
+ {
1132
+ "type": "command",
1133
+ "command": "echo before Execute"
1134
+ }
1135
+ ]
1136
+ }
1137
+ ]
1138
+ }
1139
+ ```
1140
+
1141
+ Hooks can run before or after tools, when prompts are submitted, when notifications arrive, during compaction, and when sessions or subagents stop.
668
1142
 
669
1143
  Hook events are available in the stream:
670
1144
 
@@ -676,32 +1150,17 @@ for await (const message of session.stream('Run the tests.')) {
676
1150
  }
677
1151
  ```
678
1152
 
679
- The SDK exposes hook schemas and types, but does not provide a programmatic
680
- hook-registration API.
681
-
682
- ### Raw notifications
683
-
684
- **Runtime: Node**
685
-
686
- ```ts
687
- const unsubscribe = session.onNotification(
688
- (notification) => console.log(notification),
689
- { type: 'settings_updated' }
690
- );
691
-
692
- unsubscribe();
693
- ```
1153
+ The SDK exposes hook schemas and stream events, but it does not provide a programmatic hook-registration API.
694
1154
 
695
- Use raw notifications only when the typed stream does not expose the event you
696
- need.
1155
+ Runnable example: [`examples/node/hook-execution.ts`](../examples/node/hook-execution.ts)
697
1156
 
698
1157
  ## Session lifecycle
699
1158
 
700
- ### Node replacement handles
1159
+ ### Replacement handles
701
1160
 
702
- Node `fork()` returns a ready successor handle. `compact()` and `rewind()`
703
- return outcome objects containing a ready successor in `session`. Each
704
- operation retires its source wrapper.
1161
+ `fork()` returns a ready successor handle. `compact()` and `rewind()` return
1162
+ outcome objects containing a ready successor in `session`. Each operation
1163
+ retires its source wrapper.
705
1164
 
706
1165
  ```ts
707
1166
  const forked = await session.fork();
@@ -727,362 +1186,159 @@ with `resumeSession(sourceId)`.
727
1186
 
728
1187
  Do not replace a session while it has an active stream.
729
1188
 
730
- ### Daemon replacement IDs
731
-
732
- Daemon operations return a new session ID. The source handle remains usable.
733
-
734
- ```ts
735
- const { newSessionId } = await source.compact();
736
- const compacted = await droid.sessions.resume(newSessionId);
737
- ```
738
-
739
- The same rule applies to `fork()` and `rewind()`.
740
-
741
- ```ts
742
- const forkResult = await source.fork();
743
- const fork = await droid.sessions.resume(forkResult.newSessionId);
744
-
745
- const rewindResult = await source.rewind({
746
- messageId,
747
- filesToRestore,
748
- filesToDelete,
749
- forkTitle: 'Before the failed change',
750
- });
751
- const rewound = await droid.sessions.resume(rewindResult.newSessionId);
752
- ```
753
-
754
- Daemon replacement locking is local to one client handle. Avoid replacing or
755
- streaming the same session through several daemon clients at once.
756
-
757
1189
  ## Spec mode
758
1190
 
759
- Create a session in spec mode:
1191
+ 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()`:
760
1192
 
761
1193
  ```ts
762
- import {
763
- createSession,
764
- DroidInteractionMode,
765
- ReasoningEffort,
766
- } from '@factory/droid-sdk/node';
767
-
768
1194
  const session = await createSession({
769
1195
  interactionMode: DroidInteractionMode.Spec,
770
- specModeReasoningEffort: ReasoningEffort.High,
771
1196
  });
1197
+
1198
+ // Or switch an existing Auto session.
1199
+ await session.enterSpecMode();
772
1200
  ```
773
1201
 
774
- Enter spec mode later:
1202
+ `enterSpecMode()` can also set the model used for planning:
775
1203
 
776
1204
  ```ts
777
1205
  await session.enterSpecMode({
1206
+ specModeModelId: 'model-id',
778
1207
  specModeReasoningEffort: ReasoningEffort.High,
779
1208
  });
780
1209
  ```
781
1210
 
782
- When Droid asks to leave spec mode, a permission handler can approve
783
- implementation in the same session or hand it to a new session.
1211
+ ### Leave without approving a plan
784
1212
 
785
- | Outcome | Behavior |
786
- | ----------------------- | --------------------------------------------------- |
787
- | `ProceedOnce` | Continue in the same session |
788
- | `ProceedNewSessionHigh` | Start implementation in a new high-autonomy session |
1213
+ Call `exitSpecMode()` to return the session to Auto mode without approving a plan or starting implementation:
789
1214
 
790
- New-session handoff IDs currently require inspecting raw notifications.
1215
+ ```ts
1216
+ await session.enterSpecMode();
791
1217
 
792
- ## Daemon and browser applications
1218
+ // Return to Auto mode without approving or implementing the plan.
1219
+ await session.exitSpecMode();
1220
+ ```
793
1221
 
794
- ### Connect
1222
+ `exitSpecMode()` changes the interaction mode only. It does not select a `ToolConfirmationOutcome` or approve an `ExitSpecMode` permission request.
795
1223
 
796
- ```ts
797
- const droid = await connectToDaemon({
798
- url: 'ws://127.0.0.1:37643',
799
- auth: { apiKey },
800
- permissionHandler,
801
- askUserHandler,
802
- onError(error) {
803
- console.error(error);
804
- },
805
- });
806
- ```
1224
+ The method is available on Node `DroidSession` handles returned by `createSession()`, `resumeSession()`, and replacement operations such as `fork()`, `compact()`, and `rewind()`. Daemon clients continue to change the mode with `droid.sessions.updateSettings()`.
807
1225
 
808
- Connection-level handlers become defaults for created and resumed sessions.
809
- Per-session handlers override them.
1226
+ ### Approve a plan
810
1227
 
811
- ### Create and resume
1228
+ When the plan is ready, Droid sends an `ExitSpecMode` permission request. Its
1229
+ details contain the plan. Unlike calling `exitSpecMode()`, approving it with `ProceedOnce` accepts the completed plan, returns to Auto mode, and continues implementation in the same session:
812
1230
 
813
1231
  ```ts
814
- const created = await droid.sessions.create({
815
- cwd: '/path/to/repository',
816
- });
817
-
818
- const resumed = await droid.sessions.resume('session-id');
819
- ```
1232
+ import {
1233
+ AutonomyLevel,
1234
+ createSession,
1235
+ DroidInteractionMode,
1236
+ DroidMessageType,
1237
+ ToolConfirmationOutcome,
1238
+ ToolConfirmationType,
1239
+ } from '@factory/droid-sdk/node';
820
1240
 
821
- One `ConnectedDroid` can have one attached handle for each session ID.
1241
+ const session = await createSession({
1242
+ interactionMode: DroidInteractionMode.Auto,
1243
+ autonomyLevel: AutonomyLevel.Low,
1244
+ permissionHandler({ toolUses }) {
1245
+ const request = toolUses.find(
1246
+ ({ details }) => details.type === ToolConfirmationType.ExitSpecMode
1247
+ );
822
1248
 
823
- ### List and inspect history
1249
+ if (request?.details.type === ToolConfirmationType.ExitSpecMode) {
1250
+ console.log(request.details.plan);
824
1251
 
825
- ```ts
826
- const sessions = await droid.sessions.list({ limit: 20 });
1252
+ return ToolConfirmationOutcome.ProceedOnce;
1253
+ }
827
1254
 
828
- const messages = await droid.sessions.getMessages(sessions[0].id, {
829
- limit: 50,
1255
+ return ToolConfirmationOutcome.Cancel;
1256
+ },
830
1257
  });
831
1258
 
832
- const matches = await droid.sessions.search({
833
- query: 'authentication failure',
834
- });
1259
+ try {
1260
+ await session.enterSpecMode();
1261
+
1262
+ for await (const message of session.stream('Plan and update README.md.')) {
1263
+ if (message.type === DroidMessageType.Result) {
1264
+ console.log(message.subtype);
1265
+ }
1266
+ }
1267
+ } finally {
1268
+ await session.close();
1269
+ }
835
1270
  ```
836
1271
 
837
- The daemon session resource also supports opened-session listing,
838
- archive/unarchive, rename, settings, context information, and queued-message
839
- operations.
1272
+ The handler can be asynchronous. An interactive application should display the plan and wait for the user's decision before returning.
840
1273
 
841
- ### Concurrent sessions
1274
+ Return `Cancel` to reject the plan. The turn ends with an `interrupted` result, and the session remains available for another prompt.
842
1275
 
843
- Different daemon sessions can stream over one connection at the same time.
1276
+ 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`.
844
1277
 
845
- ```ts
846
- await Promise.all([
847
- consume(first.stream('Review the API.')),
848
- consume(second.stream('Review the tests.')),
849
- ]);
850
- ```
1278
+ Runnable example:
1279
+ [`examples/node/enter-spec-mode.ts`](../examples/node/enter-spec-mode.ts)
851
1280
 
852
- ### Cleanup
853
-
854
- | Operation | Daemon session | Local handle | Connection |
855
- | -------------------- | -------------- | ---------------------- | ---------- |
856
- | `session.detach()` | Keeps running | Detached | Retained |
857
- | `session.close()` | Ends | Detached after success | Retained |
858
- | `droid.disconnect()` | Keeps running | All detached | Destroyed |
859
-
860
- Detached handles cannot be reused. Resume the session to obtain a new handle.
861
-
862
- ### Stable daemon resources
863
-
864
- | Namespace | Main methods |
865
- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
866
- | `sessions` | `list`, `listOpened`, `create`, `resume`, `getMessages`, `search`, `archive`, `unarchive`, `rename`, `updateSettings`, `resolveQueuedMessage`, `killWorker`, `getRewindInfo`, `rewind`, `compact`, `fork`, `getContextBreakdown` |
867
- | `workspace` | `changeDirectory`, `validateDirectory`, `checkTrust`, `trust`, `listFiles`, `searchFiles`, `getFileContent` |
868
- | `settings` | `getDefaults`, `updateDefaults` |
869
- | `customModels` | `list`, `upsert`, `delete` |
870
- | `ssh` | `installKey` |
871
- | `updates` | `trigger` |
872
- | `relay` | `start`, `stop`, `status` |
873
- | `terminals` | `create`, `write`, `resize`, `close`, `list` |
874
- | `mcp` | `getConfig`, `updateConfig`, `addServer`, `removeServer`, `toggleServer`, authentication, registry, and tool methods |
875
- | `skills` | `list`, `setDisabled` |
876
- | `commands` | `list` |
877
- | `plugins` | `listAvailable`, `listInstalled`, `install`, `uninstall`, `setEnabled`, `update` |
878
- | `marketplaces` | `list`, `add`, `remove`, `update` |
879
- | `automations` | `list`, `run`, `pause`, `resume`, history, visual, create, update, rename, delete, fork, and config methods |
880
- | `git` | `getDiff`, `listBranches`, `checkoutBranch`, `push`, `commit`, `createPullRequest`, `resolvePullRequestStatuses` |
881
- | `feedback` | `submitBugReport` |
882
-
883
- ### Facade return shapes
884
-
885
- The namespaced daemon facade normalizes common collection results:
886
-
887
- - `sessions.list()` returns session summaries with `id`, `messageCount`, and
888
- `modifiedTime`.
889
- - `sessions.getMessages()` returns the message array directly.
890
- - Collection methods such as `customModels.list()`, `terminals.list()`,
891
- `mcp.listRegistry()`, `mcp.listTools()`, `commands.list()`, and the plugin,
892
- marketplace, automation, and Software Factory list methods return arrays.
893
- - Methods whose protocol result includes additional metadata retain the result
894
- object. This includes `sessions.search()`, `mcp.listServers()`, and
895
- `skills.list()`.
896
-
897
- The low-level `DaemonClient` returns canonical protocol response types instead
898
- of these normalized facade shapes.
899
-
900
- ## Observability
901
-
902
- **Runtime: Node**
1281
+ ## Operations
1282
+
1283
+ ### Observability
903
1284
 
904
1285
  ```ts
905
- const session = await createSession({
1286
+ import { run } from '@factory/droid-sdk/node';
1287
+
1288
+ const result = await run('Check the repository status.', {
906
1289
  observability: {
907
1290
  logger: {
908
1291
  log(event) {
909
- console.log(event.level, event.message);
910
- },
911
- },
912
- metrics: {
913
- record(event) {
914
- console.log(event.name, event.value);
915
- },
916
- },
917
- tracing: {
918
- inject(carrier) {
919
- carrier.traceparent = currentTraceparent;
1292
+ console.log(event.level, event.name, event.attributes);
920
1293
  },
921
1294
  },
922
1295
  },
923
1296
  });
924
- ```
925
-
926
- Observability sinks must not throw into SDK operations. The SDK avoids sending
927
- prompt text, message content, tool inputs, raw output, and stack traces through
928
- its observability contracts.
929
1297
 
930
- ## Advanced resources
931
-
932
- ### Low-level daemon integration
933
-
934
- Most applications should use `connectToDaemon()`. The root entrypoint also
935
- exports `DaemonClient`, `DaemonSessionController`, `MultiSessionStateManager`,
936
- `MultiMissionStateManager`, `WebSocketDaemonTransport`, daemon events, and
937
- canonical protocol types for hosts that need custom connection or state
938
- ownership.
939
-
940
- Node-only IPC and in-process transports come from the `/node` entrypoint:
941
-
942
- ```ts
943
- import { DaemonClient, MachineType } from '@factory/droid-sdk';
944
- import {
945
- InProcessDaemonClientTransport,
946
- type InProcessDaemonClientTransportOptions,
947
- type InProcessMessageHandler,
948
- } from '@factory/droid-sdk/node';
949
-
950
- function createInProcessClient(
951
- sendMessage: InProcessMessageHandler,
952
- onMessage: NonNullable<InProcessDaemonClientTransportOptions['onMessage']>
953
- ) {
954
- const transport = new InProcessDaemonClientTransport({
955
- sendMessage,
956
- onMessage,
957
- });
958
-
959
- return new DaemonClient({
960
- machineType: MachineType.Local,
961
- transport,
962
- });
1298
+ if (!result.success) {
1299
+ throw new Error(result.error?.message ?? `Run failed: ${result.subtype}`);
963
1300
  }
964
1301
  ```
965
1302
 
966
- The root also exports `WebSocketDaemonTransport`; the `/node` entrypoint exports
967
- `IpcDaemonClientTransport`.
968
-
969
- Hosts can inject daemon-core logging, metrics, tracing, and domain helpers once
970
- at startup:
971
-
972
- ```ts
973
- import { configureDaemonCoreDeps } from '@factory/droid-sdk';
974
-
975
- configureDaemonCoreDeps({
976
- logger: {
977
- info: (message, metadata) => logger.info(message, metadata),
978
- error: (message, metadata) => logger.error(message, metadata),
979
- },
980
- });
981
- ```
982
-
983
- Unspecified dependencies keep their standalone defaults.
984
-
985
- ### Low-level Node integration
986
-
987
- Most Node applications should use `run()` or `DroidSession`. The `/node`
988
- entrypoint also exports `ProcessTransport` and `DroidClient` for direct
989
- JSON-RPC integrations.
990
-
991
- Low-level RPC methods return complete response envelopes. Read successful
992
- payloads from `response.result`. Direct permission and AskUser handlers receive
993
- complete request events, while high-level session handlers receive request
994
- parameters.
995
-
996
- `ProcessTransport` uses `droidExecPath` and `droidExecExtraArgs` for executable
997
- configuration. A custom `StringFramedDroidClientTransport` exchanges
998
- newline-delimited JSON-RPC strings and can replace subprocess transport
999
- entirely.
1000
-
1001
- ### Context and token usage
1002
-
1003
- Read the current context-window estimate:
1004
-
1005
- ```ts
1006
- const stats = await session.getContextStats();
1303
+ Pass `logger`, `metrics`, or `tracing` sinks through `observability`.
1304
+ Sink methods must not throw.
1305
+ The SDK excludes prompts, messages, tool inputs, raw output, and stack traces from observability events.
1306
+ Process startup emits telemetry before the first turn, and `close()` emits a request log.
1307
+ To verify turn-specific logs, snapshot the count after creating the session and check it before closing.
1007
1308
 
1008
- console.log(stats.used, stats.remaining, stats.limit, stats.accuracy);
1009
- ```
1309
+ Runnable example:
1310
+ [`examples/node/observability.ts`](../examples/node/observability.ts)
1010
1311
 
1011
- Enable partial messages to receive live token updates:
1312
+ ### Low-level APIs
1012
1313
 
1013
- ```ts
1014
- for await (const message of session.stream('Summarize this repository.', {
1015
- includePartialMessages: true,
1016
- })) {
1017
- if (message.type === DroidMessageType.TokenUsageUpdate) {
1018
- console.log(message.inputTokens, message.outputTokens);
1019
- }
1020
- }
1021
- ```
1314
+ Use the high-level APIs unless your application owns the transport or protocol lifecycle.
1022
1315
 
1023
- The final result's `tokenUsage` contains the last observed totals or `null`.
1316
+ | Need | Preferred API | Low-level alternative |
1317
+ | ---------------------------- | ------------------ | --------------------- |
1318
+ | Run a session | `createSession()` | `DroidClient` |
1319
+ | Consume typed session events | `session.stream()` | Raw notifications |
1024
1320
 
1025
- ### Factory REST API
1321
+ The `/node` entrypoint exports `DroidClient` for direct JSON-RPC integrations.
1322
+ Low-level methods return response envelopes, so successful payloads are under `response.result`.
1026
1323
 
1027
- The browser-safe root exports REST helpers for Factory computers, machine
1028
- templates, metrics, dependency installation, and remote-session listing.
1324
+ Use `onNotification()` only when `session.stream()` does not expose the event you need:
1029
1325
 
1030
1326
  ```ts
1031
- import { listComputers, listRemoteSessions } from '@factory/droid-sdk';
1032
-
1033
- const options = { apiKey: process.env.FACTORY_API_KEY! };
1327
+ const unsubscribe = session.onNotification(
1328
+ (notification) => console.log(notification.type),
1329
+ { type: 'settings_updated' }
1330
+ );
1034
1331
 
1035
- const computers = await listComputers(options);
1036
- const sessions = await listRemoteSessions(options);
1332
+ unsubscribe();
1037
1333
  ```
1038
1334
 
1039
- REST calls use `https://api.factory.ai` by default. Pass `baseUrl` to target a
1040
- different Factory API host.
1041
-
1042
- | Resource | Functions |
1043
- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
1044
- | Machine templates | `listMachineTemplates`, `getMachineTemplate` |
1045
- | Computers | `listComputers`, `getComputer`, `getComputerByName`, `createComputer`, `updateComputer`, `deleteComputer`, `restartComputer`, `refreshComputer` |
1046
- | Computer operations | `getComputerMetrics`, `retryInstallDeps` |
1047
- | Remote sessions | `listRemoteSessions` |
1048
-
1049
- ### Unstable daemon resources
1050
-
1051
- APIs under `droid.unstable` may change between SDK releases.
1052
-
1053
- | Namespace | Main methods or resources |
1054
- | ----------------- | ------------------------------------------------------------------------- |
1055
- | `crons` | `list`, `create`, `update`, `delete`, `holdSession`, `resumeSession` |
1056
- | `missions` | `inspectReadiness`, `acknowledgeReadinessWarning` |
1057
- | `semanticDiff` | `getCache`, `saveCache`, `generate` |
1058
- | `fileTransfers` | `push`, `pull` |
1059
- | `proxy` | `getToken` |
1060
- | `softwareFactory` | `workstreams`, `signals`, `changes`, `activities`, and `events` resources |
1061
-
1062
- Keep unstable API use behind an application-owned adapter.
1063
-
1064
- For example, create and manage a scheduled prompt through the unstable cron
1065
- resource:
1066
-
1067
- ```ts
1068
- const cron = await droid.unstable.crons.create({
1069
- kind: 'root_prompt',
1070
- scope: { type: 'root' },
1071
- schedule: { expression: '0 * * * *', recurring: true },
1072
- payload: {
1073
- type: 'prompt',
1074
- prompt: 'Check repository health',
1075
- target: { type: 'new_session', cwd: '/workspace' },
1076
- },
1077
- });
1335
+ ## API reference
1078
1336
 
1079
- await droid.unstable.crons.update(cron.id, { status: 'paused' });
1080
- await droid.unstable.crons.delete(cron.id);
1081
- ```
1337
+ ### Entrypoint
1082
1338
 
1083
- ## API reference
1339
+ Import functions, classes, enums, and types from `@factory/droid-sdk/node`.
1084
1340
 
1085
- ### Node functions
1341
+ ### Functions
1086
1342
 
1087
1343
  | API | Returns |
1088
1344
  | ------------------------------------ | ---------------------------- |
@@ -1090,37 +1346,52 @@ await droid.unstable.crons.delete(cron.id);
1090
1346
  | `createSession(options?)` | `Promise<DroidSession>` |
1091
1347
  | `resumeSession(sessionId, options?)` | `Promise<DroidSession>` |
1092
1348
  | `listSessions(options?)` | `Promise<SessionMetadata[]>` |
1349
+ | `listModels(options?)` | `Promise<ModelInfo[]>` |
1093
1350
  | `createSdkMcpServer(options)` | `SdkMcpServer` |
1094
1351
  | `tool(...)` | `DroidTool` |
1095
1352
 
1096
1353
  ### `DroidSession`
1097
1354
 
1098
- | Member | Purpose |
1099
- | ------------------------- | ------------------------------------- |
1100
- | `id` | Session ID |
1101
- | `cwd` | Live working directory |
1102
- | `settings` | Live read-only settings |
1103
- | `stream()` | Run one turn |
1104
- | `interrupt()` | Stop the active turn |
1105
- | `updateSettings()` | Update session settings |
1106
- | `enterSpecMode()` | Change to spec mode |
1107
- | `listTools()` | List normalized tools |
1108
- | `listSkills()` | List skills |
1109
- | `setSkillDisabled()` | Enable or disable a skill |
1110
- | `addMcpServer()` | Add an MCP server |
1111
- | `removeMcpServer()` | Remove an MCP server |
1112
- | `toggleMcpServer()` | Enable or disable an MCP server |
1113
- | `listMcpServers()` | List MCP servers |
1114
- | `listMcpTools()` | List MCP tools |
1115
- | `authenticateMcpServer()` | Start MCP authentication |
1116
- | `getContextStats()` | Read context usage |
1117
- | `getRewindInfo()` | Inspect rewindable file changes |
1118
- | `rewind()` | Create a rewound successor |
1119
- | `compact()` | Create a compacted successor |
1120
- | `fork()` | Create a forked successor |
1121
- | `rename()` | Change the session title |
1122
- | `onNotification()` | Subscribe to raw notifications |
1123
- | `close()` | Close the session and owned resources |
1355
+ | Member | Purpose |
1356
+ | ------------------------- | -------------------------------------------- |
1357
+ | `id` | Session ID |
1358
+ | `cwd` | Live working directory |
1359
+ | `settings` | Live read-only settings |
1360
+ | `stream()` | Run one turn |
1361
+ | `interrupt()` | Stop the active turn |
1362
+ | `updateSettings()` | Update session settings |
1363
+ | `enterSpecMode()` | Change to spec mode |
1364
+ | `exitSpecMode()` | Return to Auto mode without approving a plan |
1365
+ | `listTools()` | List normalized tools |
1366
+ | `listSkills()` | List skills |
1367
+ | `setSkillDisabled()` | Enable or disable a skill |
1368
+ | `addMcpServer()` | Add an MCP server |
1369
+ | `removeMcpServer()` | Remove an MCP server |
1370
+ | `toggleMcpServer()` | Enable or disable an MCP server |
1371
+ | `listMcpServers()` | List MCP servers |
1372
+ | `listMcpTools()` | List MCP tools |
1373
+ | `authenticateMcpServer()` | Start MCP authentication |
1374
+ | `getContextStats()` | Read context usage |
1375
+ | `getRewindInfo()` | Inspect rewindable file changes |
1376
+ | `rewind()` | Create a rewound successor |
1377
+ | `compact()` | Create a compacted successor |
1378
+ | `fork()` | Create a forked successor |
1379
+ | `rename()` | Change the session title |
1380
+ | `onNotification()` | Subscribe to raw notifications |
1381
+ | `close()` | Close the session and owned resources |
1382
+
1383
+ ### Stream and model types
1384
+
1385
+ | Type | Purpose |
1386
+ | -------------------- | ------------------------------------------------- |
1387
+ | `DroidStreamMessage` | Complete messages returned by the default stream |
1388
+ | `DroidStreamEvent` | Complete messages plus partial events |
1389
+ | `DroidResult` | Terminal result returned by `run()` |
1390
+ | `DroidMessageType` | Runtime constants for narrowing stream messages |
1391
+ | `TokenUsage` | Token counts and optional Factory Service Credits |
1392
+ | `ModelInfo` | Model catalog entry returned by discovery APIs |
1393
+
1394
+ `session.stream()` yields `DroidStreamMessage` by default and `DroidStreamEvent` when `includePartialMessages` is `true`.
1124
1395
 
1125
1396
  ### Result states
1126
1397
 
@@ -1131,130 +1402,36 @@ await droid.unstable.crons.delete(cron.id);
1131
1402
  | `error_during_execution` | `false` | `false` | Runtime failure |
1132
1403
  | `error_structured_output` | `false` | `false` | Structured-output failure |
1133
1404
 
1134
- Common result fields:
1135
-
1136
- ```ts
1137
- {
1138
- type: 'result';
1139
- subtype:
1140
- | 'success'
1141
- | 'interrupted'
1142
- | 'error_during_execution'
1143
- | 'error_structured_output';
1144
- sessionId: string;
1145
- durationMs: number;
1146
- tokenUsage: TokenUsage | null;
1147
- messages: DroidStreamEvent[];
1148
- text: string;
1149
- turnCount: number;
1150
- success: boolean;
1151
- interrupted: boolean;
1152
- structuredOutput?: unknown;
1153
- structuredOutputError?: StructuredOutputError | null;
1154
- error: ErrorEvent | null;
1155
- }
1156
- ```
1405
+ Every result includes `sessionId`, `durationMs`, `tokenUsage`, `messages`, `text`, and `turnCount`.
1406
+ Check `success` before using successful output.
1157
1407
 
1158
1408
  ### Main enums
1159
1409
 
1160
1410
  | Enum | Values |
1161
1411
  | ---------------------- | -------------------------------------------------------------------------------- |
1162
1412
  | `AutonomyLevel` | `Off`, `Low`, `Medium`, `High` |
1163
- | `DroidInteractionMode` | `Auto`, `Spec`, `Mission`; `AGI` is deprecated |
1413
+ | `DroidInteractionMode` | `Auto`, `Spec` |
1164
1414
  | `ReasoningEffort` | `None`, `Dynamic`, `Off`, `Minimal`, `Low`, `Medium`, `High`, `ExtraHigh`, `Max` |
1165
1415
  | `OutputFormatType` | `JsonSchema` |
1166
1416
 
1167
- Model IDs and tool IDs are strings. Discover available models and tools at
1168
- runtime instead of treating them as closed enums.
1417
+ Model IDs and tool IDs are strings rather than closed enums.
1169
1418
 
1170
1419
  ### Main errors
1171
1420
 
1172
- Universal errors:
1173
-
1174
- | Error | Meaning |
1175
- | -------------------------- | --------------------------------------------- |
1176
- | `ConcurrentStreamError` | A session handle already has an active stream |
1177
- | `SessionReplacedError` | A Node source wrapper was replaced |
1178
- | `SessionReplacementError` | Successor loading or rollback failed |
1179
- | `AbortError` | A daemon operation was aborted |
1180
- | `ConnectionClosedError` | Daemon connection closed |
1181
- | `JsonRpcRequestError` | Daemon JSON-RPC request failed |
1182
- | `RelayConnectionError` | Relay connection failed |
1183
- | `WebSocketConnectionError` | WebSocket connection failed |
1184
-
1185
- Node-only errors:
1186
-
1187
- | Error | Meaning |
1188
- | ------------------------ | -------------------------------------------- |
1189
- | `DroidClientError` | Base Node client error |
1190
- | `ConnectionError` | Droid process or transport connection failed |
1191
- | `TimeoutError` | A request timed out |
1192
- | `ProtocolError` | Protocol or API response failed |
1193
- | `SessionError` | Base session error |
1194
- | `SessionNotFoundError` | Saved session was not found |
1195
- | `InvalidSessionCwdError` | Saved working directory is invalid |
1196
- | `ProcessExitError` | Droid subprocess exited unexpectedly |
1197
-
1198
- ### Custom transports
1199
-
1200
- **Runtime: Node, advanced**
1201
-
1202
- ```ts
1203
- interface StringFramedDroidClientTransport {
1204
- isConnected: boolean;
1205
- send(message: string): Promise<void>;
1206
- onMessage(handler: (message: string) => void): void;
1207
- onError(handler: (error: Error) => void): void;
1208
- close(): Promise<void>;
1209
- }
1210
- ```
1211
-
1212
- Pass a ready transport through `run()`, `createSession()`, or
1213
- `resumeSession()`. A custom transport bypasses subprocess creation and the
1214
- Node API-key requirement.
1215
-
1216
- ## Errors and limitations
1217
-
1218
- ### Handle results explicitly
1219
-
1220
- ```ts
1221
- const result = await run('Run the test suite.');
1222
-
1223
- switch (result.subtype) {
1224
- case 'success':
1225
- console.log(result.text);
1226
- break;
1227
- case 'interrupted':
1228
- console.log('The run was interrupted.');
1229
- break;
1230
- case 'error_during_execution':
1231
- case 'error_structured_output':
1232
- console.error(
1233
- result.structuredOutputError?.message ??
1234
- result.error?.message ??
1235
- 'Unknown error'
1236
- );
1237
- break;
1238
- }
1239
- ```
1240
-
1241
- ### Known limitations
1421
+ | Error | Meaning |
1422
+ | ------------------------- | --------------------------------------------- |
1423
+ | `ConcurrentStreamError` | A session handle already has an active stream |
1424
+ | `SessionReplacedError` | A source wrapper was replaced |
1425
+ | `SessionReplacementError` | Successor loading or rollback failed |
1426
+ | `DroidClientError` | Base client error |
1427
+ | `ConnectionError` | Droid process connection failed |
1428
+ | `TimeoutError` | A request timed out |
1429
+ | `ProtocolError` | Protocol or API response failed |
1430
+ | `SessionError` | Base session error |
1431
+ | `SessionNotFoundError` | Saved session was not found |
1432
+ | `InvalidSessionCwdError` | Saved working directory is invalid |
1433
+ | `ProcessExitError` | Droid subprocess exited unexpectedly |
1434
+
1435
+ ## Known limitations
1242
1436
 
1243
- - Daemon replacement locking does not coordinate separate clients.
1244
- - Browser daemon authentication currently requires an API key.
1245
1437
  - New-session spec handoff IDs require raw notification inspection.
1246
- - REST remote-session support currently provides listing only.
1247
-
1248
- ## Runnable examples
1249
-
1250
- The package includes runnable examples for one-shot runs, streaming, sessions,
1251
- permissions, AskUser, attachments, structured output, MCP tools, hooks,
1252
- observability, spec mode, replacement lifecycle, daemon sessions, concurrent
1253
- browser sessions, and daemon lifecycle operations.
1254
-
1255
- All examples are under:
1256
-
1257
- ```text
1258
- examples/node/
1259
- examples/browser/
1260
- ```