@factory/droid-sdk 0.6.0 → 0.7.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 (80) hide show
  1. package/README.md +35 -482
  2. package/dist/chunk-5UXINOXG.mjs +24483 -0
  3. package/dist/chunk-5UXINOXG.mjs.map +1 -0
  4. package/dist/chunk-C3KHERVH.js +24483 -0
  5. package/dist/chunk-C3KHERVH.js.map +1 -0
  6. package/dist/{index-CwXX5IUH.d.cts → index-D_SzTnFR.d.mts} +83826 -81547
  7. package/dist/{index-CwXX5IUH.d.ts → index-D_SzTnFR.d.ts} +83826 -81547
  8. package/dist/index.d.mts +4 -0
  9. package/dist/index.d.ts +4 -93103
  10. package/dist/index.js +821 -11745
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +822 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/node.d.mts +736 -0
  15. package/dist/node.d.ts +736 -0
  16. package/dist/node.js +4601 -0
  17. package/dist/node.js.map +1 -0
  18. package/dist/node.mjs +4601 -0
  19. package/dist/node.mjs.map +1 -0
  20. package/docs/typescript-sdk-reference.md +1260 -0
  21. package/examples/browser/daemon-lifecycle.ts +95 -0
  22. package/examples/browser/daemon-multi-session.ts +66 -0
  23. package/examples/browser/daemon-session.ts +73 -0
  24. package/examples/browser/index.html +68 -0
  25. package/examples/browser/list-sessions.ts +52 -0
  26. package/examples/{abort-session-stream.ts → node/abort-session-stream.ts} +8 -10
  27. package/examples/node/ask-user-elicitation.ts +35 -0
  28. package/examples/node/compact-session.ts +48 -0
  29. package/examples/node/enter-spec-mode.ts +66 -0
  30. package/examples/node/fork-session.ts +45 -0
  31. package/examples/node/hook-execution.ts +94 -0
  32. package/examples/node/image-attachment.ts +35 -0
  33. package/examples/node/init-metadata.ts +24 -0
  34. package/examples/{interrupt-session.ts → node/interrupt-session.ts} +7 -6
  35. package/examples/node/list-sessions.ts +20 -0
  36. package/examples/node/multi-turn-session.ts +33 -0
  37. package/examples/node/observability.ts +88 -0
  38. package/examples/node/permission-handler.ts +55 -0
  39. package/examples/node/rename-session.ts +29 -0
  40. package/examples/node/result-metadata.ts +34 -0
  41. package/examples/node/rewind-session.ts +69 -0
  42. package/examples/node/run.ts +25 -0
  43. package/examples/{sdk-mcp-tool.ts → node/sdk-mcp-tool.ts} +22 -12
  44. package/examples/node/session-settings.ts +35 -0
  45. package/examples/node/session-stream.ts +44 -0
  46. package/examples/node/spec-mode-new-session.ts +92 -0
  47. package/examples/node/spec-mode-same-session.ts +85 -0
  48. package/examples/node/structured-output.ts +73 -0
  49. package/examples/node/tool-controls.ts +26 -0
  50. package/examples/package.json +3 -0
  51. package/package.json +20 -42
  52. package/dist/index.cjs +0 -12135
  53. package/dist/index.cjs.map +0 -1
  54. package/dist/index.d.cts +0 -93103
  55. package/dist/protocol/index.cjs +0 -6466
  56. package/dist/protocol/index.cjs.map +0 -1
  57. package/dist/protocol/index.d.cts +0 -2
  58. package/dist/protocol/index.d.ts +0 -2
  59. package/dist/protocol/index.js +0 -6078
  60. package/dist/protocol/index.js.map +0 -1
  61. package/docs/daemon-usage-guide.md +0 -544
  62. package/docs/sdk-usage-guide.md +0 -748
  63. package/examples/daemon-multi-session.ts +0 -67
  64. package/examples/droid-dev-structured-output.ts +0 -44
  65. package/examples/fork-session.ts +0 -68
  66. package/examples/hook-execution.ts +0 -68
  67. package/examples/init-metadata.ts +0 -24
  68. package/examples/list-sessions.ts +0 -19
  69. package/examples/multi-turn-session.ts +0 -40
  70. package/examples/permission-handler.ts +0 -41
  71. package/examples/readme-structured-output.ts +0 -26
  72. package/examples/result-metadata.ts +0 -63
  73. package/examples/run.ts +0 -39
  74. package/examples/session-stream.ts +0 -57
  75. package/examples/spec-mode-new-session.ts +0 -61
  76. package/examples/spec-mode-same-session.ts +0 -72
  77. package/examples/structured-output.ts +0 -60
  78. package/examples/test-compact.ts +0 -65
  79. package/examples/test-rewind.ts +0 -87
  80. package/examples/tool-controls.ts +0 -37
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Image attachment example.
3
+ *
4
+ * Attaches a base64 image to a prompt via `images`, so the model can see it.
5
+ * `run()` and `session.stream()` both accept the same `images` option.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/node/image-attachment.ts
9
+ */
10
+
11
+ import { run } from '@factory/droid-sdk/node';
12
+
13
+ /** A 120x120 PNG: one purple square centered on white. */
14
+ const PURPLE_SQUARE_PNG =
15
+ 'iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAIAAAC2BqGFAAAAzElEQVR42u3QAQ0A' +
16
+ 'MAgEsZeOvLkCFyQLvZyCprVSEIAGLdCgQQs0aIEGDVqgQQs0aNACDVqgQYMWaNAC' +
17
+ 'DRq0QIMW6GPQlffdoEGDBg0aNGjQoEGDBg0aNGjQoEGDBg0aNGjQoEGDBg0aNGjQ' +
18
+ 'oEGDBg0aNGjQoEGDBg0aNGjQoEGDBg0aNGjQoEGDBg0aNGjQoEGDBg0aNGjQoEGD' +
19
+ 'Bg0aNGjQoEGDFmjQAg0atECDFmjQoAUatECDBi3QoAUaNGiBBi3QoEELNGiBBn2g' +
20
+ 'AU2hHmFuN1AZAAAAAElFTkSuQmCC';
21
+
22
+ const { text } = await run(
23
+ 'What single shape and color is in this image? Answer in five words or fewer.',
24
+ {
25
+ images: [
26
+ {
27
+ type: 'base64',
28
+ data: PURPLE_SQUARE_PNG,
29
+ mediaType: 'image/png',
30
+ },
31
+ ],
32
+ }
33
+ );
34
+
35
+ console.log(text);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Initialization metadata example.
3
+ *
4
+ * Reads direct metadata from a freshly created session: session ID, working
5
+ * directory, model, and settings.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/node/init-metadata.ts
9
+ */
10
+
11
+ import { createSession } from '@factory/droid-sdk/node';
12
+
13
+ const session = await createSession();
14
+
15
+ try {
16
+ const { settings } = session;
17
+ console.log(`session: ${session.id}`);
18
+ console.log(`cwd: ${String(session.cwd)}`);
19
+ console.log(`model: ${String(settings.modelId)}`);
20
+ console.log(`interaction mode: ${String(settings.interactionMode)}`);
21
+ console.log(`autonomy level: ${String(settings.autonomyLevel)}`);
22
+ } finally {
23
+ await session.close();
24
+ }
@@ -1,16 +1,17 @@
1
1
  /**
2
2
  * Interrupt a running turn.
3
3
  *
4
+ * Calls `session.interrupt()` partway through streaming. Unlike
5
+ * abort-session-stream.ts, this stops the turn server-side and leaves the
6
+ * session usable for another prompt.
7
+ *
4
8
  * Usage:
5
- * npx tsx examples/interrupt-session.ts
9
+ * npx tsx examples/node/interrupt-session.ts
6
10
  */
7
11
 
8
- import { createSession, DroidMessageType } from '@factory/droid-sdk';
12
+ import { createSession, DroidMessageType } from '@factory/droid-sdk/node';
9
13
 
10
- const session = await createSession({
11
- apiKey: process.env.FACTORY_API_KEY!,
12
- cwd: process.cwd(),
13
- });
14
+ const session = await createSession();
14
15
  let deltaCount = 0;
15
16
 
16
17
  try {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * List saved sessions example.
3
+ *
4
+ * Reads the droid sessions stored on disk for the current project.
5
+ *
6
+ * Usage:
7
+ * npx tsx examples/node/list-sessions.ts
8
+ */
9
+
10
+ import { listSessions } from '@factory/droid-sdk/node';
11
+
12
+ const sessions = await listSessions({ limit: 5 });
13
+
14
+ if (sessions.length === 0) {
15
+ console.log('No sessions found for this project.');
16
+ }
17
+
18
+ for (const session of sessions) {
19
+ console.log(`${session.id}: ${session.title || '(untitled)'}`);
20
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Multi-turn session example.
3
+ *
4
+ * Sends two prompts over one session so the second turn can build on the
5
+ * context established by the first.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/node/multi-turn-session.ts
9
+ */
10
+
11
+ import { DroidMessageType, createSession } from '@factory/droid-sdk/node';
12
+
13
+ async function streamText(
14
+ session: Awaited<ReturnType<typeof createSession>>,
15
+ prompt: string
16
+ ): Promise<string> {
17
+ let text = '';
18
+ for await (const msg of session.stream(prompt)) {
19
+ if (msg.type === DroidMessageType.Assistant) {
20
+ text += msg.text;
21
+ }
22
+ }
23
+ return text;
24
+ }
25
+
26
+ const session = await createSession();
27
+
28
+ try {
29
+ console.log(await streamText(session, 'What is this project?'));
30
+ console.log(`\n${await streamText(session, 'What should I test first?')}`);
31
+ } finally {
32
+ await session.close();
33
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Observability example.
3
+ *
4
+ * Passes a `DroidObservability` bundle (logger, metrics, tracing) to
5
+ * `createSession()`, which wires it into both the spawned droid process
6
+ * transport and the client. Spawning the process records a spawn-latency
7
+ * metric and a log; every outgoing request asks the tracing sink to inject
8
+ * trace context into the request's `_meta`.
9
+ *
10
+ * The example then runs one real conversational turn through
11
+ * `session.stream()` so the sinks observe request/response traffic from actual
12
+ * work rather than only process startup.
13
+ *
14
+ * Sinks only ever receive structured, privacy-safe values — never raw
15
+ * stdout/stderr or message content.
16
+ *
17
+ * Usage:
18
+ * npx tsx examples/node/observability.ts
19
+ */
20
+
21
+ import {
22
+ DroidMessageType,
23
+ createSession,
24
+ type DroidLogEvent,
25
+ type DroidMetricEvent,
26
+ type DroidObservability,
27
+ type DroidTraceContextCarrier,
28
+ } from '@factory/droid-sdk/node';
29
+
30
+ let logCount = 0;
31
+
32
+ const observability: DroidObservability = {
33
+ logger: {
34
+ log(event: DroidLogEvent): void {
35
+ logCount++;
36
+ console.log(
37
+ `[log:${event.level}] ${event.name} — ${event.message}`,
38
+ event.attributes ?? {}
39
+ );
40
+ },
41
+ },
42
+ metrics: {
43
+ record(event: DroidMetricEvent): void {
44
+ console.log(
45
+ `[metric] ${event.name}=${event.value}${event.unit} (${event.kind})`,
46
+ event.attributes ?? {}
47
+ );
48
+ },
49
+ },
50
+ tracing: {
51
+ inject(carrier: DroidTraceContextCarrier): void {
52
+ // Populate the carrier from your tracer's active span. The SDK copies
53
+ // traceparent/tracestate into each request's `_meta`.
54
+ carrier.traceparent =
55
+ '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01';
56
+ carrier.tracestate = 'factory=1';
57
+ },
58
+ },
59
+ };
60
+
61
+ const session = await createSession({ observability });
62
+
63
+ // Snapshot before the turn. Spawning the process alone emits a log, so a
64
+ // total-count assertion would pass having observed only startup. Logs are also
65
+ // the only turn-scoped signal here: the SDK's sole metric is spawn latency,
66
+ // recorded before the turn begins, so metrics cannot witness turn traffic.
67
+ const logCountBeforeTurn = logCount;
68
+ let turnLogCount = 0;
69
+
70
+ try {
71
+ for await (const msg of session.stream(
72
+ 'Reply with the single word: ready.'
73
+ )) {
74
+ if (msg.type === DroidMessageType.Assistant) {
75
+ console.log(`[assistant] ${msg.text}`);
76
+ }
77
+ }
78
+ // Measure before teardown: `session.close()` sends a `close_session` request,
79
+ // which is itself logged, so counting after the `finally` would always find
80
+ // at least one log regardless of what the turn did.
81
+ turnLogCount = logCount - logCountBeforeTurn;
82
+ } finally {
83
+ await session.close();
84
+ }
85
+
86
+ if (turnLogCount === 0) {
87
+ throw new Error('Observability sinks saw no logged requests for the turn.');
88
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Permission handler example.
3
+ *
4
+ * Usage:
5
+ * npx tsx examples/node/permission-handler.ts
6
+ */
7
+
8
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+
12
+ import {
13
+ AutonomyLevel,
14
+ ToolConfirmationOutcome,
15
+ ToolConfirmationType,
16
+ run,
17
+ } from '@factory/droid-sdk/node';
18
+
19
+ const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-permission-'));
20
+ const outputPath = join(tempDir, 'hello.txt');
21
+
22
+ try {
23
+ let handledPermission = false;
24
+ const result = await run(
25
+ `Create ${outputPath} with the text "Hello, World!"`,
26
+ {
27
+ autonomyLevel: AutonomyLevel.Off,
28
+ permissionHandler(params) {
29
+ handledPermission = true;
30
+ const onlyCreatesExpectedFile =
31
+ params.toolUses.length > 0 &&
32
+ params.toolUses.every(
33
+ (item) =>
34
+ item.details.type === ToolConfirmationType.Create &&
35
+ item.details.filePath === outputPath
36
+ );
37
+
38
+ return onlyCreatesExpectedFile
39
+ ? ToolConfirmationOutcome.ProceedOnce
40
+ : ToolConfirmationOutcome.Cancel;
41
+ },
42
+ }
43
+ );
44
+
45
+ if (!result.success) {
46
+ throw new Error(result.error?.message ?? 'The turn was interrupted.');
47
+ }
48
+ if (!handledPermission) {
49
+ throw new Error('The turn finished without requesting permission.');
50
+ }
51
+
52
+ console.log(await readFile(outputPath, 'utf8'));
53
+ } finally {
54
+ await rm(tempDir, { recursive: true, force: true });
55
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Rename session example.
3
+ *
4
+ * Sessions get an auto-generated title from their first prompt.
5
+ * `rename()` overrides it, which is what a host app calls when the user
6
+ * edits a conversation title.
7
+ *
8
+ * Usage:
9
+ * npx tsx examples/node/rename-session.ts
10
+ */
11
+
12
+ import { createSession } from '@factory/droid-sdk/node';
13
+
14
+ const session = await createSession();
15
+
16
+ try {
17
+ // A session needs at least one turn before it has a title to replace.
18
+ for await (const _msg of session.stream('Reply with exactly: OK')) {
19
+ // Consume the stream so the turn completes.
20
+ }
21
+
22
+ await session.rename({
23
+ title: 'Quarterly report analysis',
24
+ });
25
+
26
+ console.log(`renamed session: ${session.id}`);
27
+ } finally {
28
+ await session.close();
29
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * DroidResult metadata example.
3
+ *
4
+ * Prints the metadata `run()` returns alongside the response text.
5
+ *
6
+ * Usage:
7
+ * npx tsx examples/node/result-metadata.ts
8
+ * npx tsx examples/node/result-metadata.ts "Reply with metadata details."
9
+ */
10
+
11
+ import { run } from '@factory/droid-sdk/node';
12
+
13
+ const result = await run(
14
+ process.argv.slice(2).join(' ') ||
15
+ 'Reply with "Metadata OK" and nothing else.'
16
+ );
17
+
18
+ console.log(result.text);
19
+ console.log(`\nsession: ${result.sessionId}`);
20
+ console.log(`duration: ${result.durationMs}ms`);
21
+ console.log(`turns: ${result.turnCount}`);
22
+ console.log(`success: ${String(result.success)}`);
23
+ console.log(`interrupted: ${String(result.interrupted)}`);
24
+ console.log(`messages: ${result.messages.length}`);
25
+
26
+ if (result.error) {
27
+ console.log(`error: ${result.error.message}`);
28
+ }
29
+
30
+ if (result.tokenUsage) {
31
+ console.log(
32
+ `tokens: ${result.tokenUsage.inputTokens} in, ${result.tokenUsage.outputTokens} out`
33
+ );
34
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Rewind session example.
3
+ *
4
+ * Runs a turn, inspects the files available at that point, then rewinds into a
5
+ * replacement session handle and continues from there.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/node/rewind-session.ts
9
+ */
10
+
11
+ import { DroidMessageType, createSession } from '@factory/droid-sdk/node';
12
+
13
+ const session = await createSession();
14
+
15
+ async function ask(
16
+ activeSession: Awaited<ReturnType<typeof createSession>>,
17
+ prompt: string
18
+ ): Promise<string> {
19
+ let messageId: string | undefined;
20
+
21
+ for await (const message of activeSession.stream(prompt)) {
22
+ if (
23
+ message.type === DroidMessageType.User &&
24
+ message.message.visibility !== 'llm_only'
25
+ ) {
26
+ messageId ??= message.message.id;
27
+ }
28
+ if (message.type === DroidMessageType.Assistant) {
29
+ console.log(message.text);
30
+ }
31
+ }
32
+
33
+ if (!messageId) throw new Error('The turn produced no user message ID.');
34
+ return messageId;
35
+ }
36
+
37
+ try {
38
+ await ask(session, 'Remember this phrase exactly: mango sunrise');
39
+ const secondTurn = await ask(
40
+ session,
41
+ 'Now forget it and remember: cobalt harbor'
42
+ );
43
+
44
+ const { availableFiles, createdFiles } = await session.getRewindInfo({
45
+ messageId: secondTurn,
46
+ });
47
+ console.log(
48
+ `rewind would restore ${availableFiles.length} file(s) ` +
49
+ `and delete ${createdFiles.length}`
50
+ );
51
+
52
+ const { session: successor } = await session.rewind({
53
+ messageId: secondTurn,
54
+ filesToRestore: availableFiles,
55
+ filesToDelete: createdFiles,
56
+ forkTitle: 'Before the second phrase',
57
+ });
58
+
59
+ // `session` is now a retired wrapper. Its persisted ID can still be passed
60
+ // to resumeSession() later if the pre-rewind history is needed again.
61
+ console.log(`rewound into session: ${successor.id}`);
62
+
63
+ await ask(successor, 'What phrase did I ask you to remember?');
64
+
65
+ await successor.close();
66
+ } finally {
67
+ // Closes the active source, or does nothing if it was retired.
68
+ await session.close();
69
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * One-shot `run()` example.
3
+ *
4
+ * Sends a single prompt and prints the aggregated result. `run()` closes the
5
+ * underlying session for you.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/node/run.ts
9
+ * npx tsx examples/node/run.ts "What is 2 + 2?"
10
+ */
11
+
12
+ import { run } from '@factory/droid-sdk/node';
13
+
14
+ const { text, messages, tokenUsage } = await run(
15
+ process.argv.slice(2).join(' ') || 'What is 2 + 2?'
16
+ );
17
+
18
+ console.log(text);
19
+ console.log(`\nmessages: ${messages.length}`);
20
+
21
+ if (tokenUsage) {
22
+ console.log(
23
+ `tokens: ${tokenUsage.inputTokens} in, ${tokenUsage.outputTokens} out`
24
+ );
25
+ }
@@ -1,13 +1,22 @@
1
+ /**
2
+ * SDK MCP tool example.
3
+ *
4
+ * Defines an in-process tool with `createSdkMcpServer` + `tool` and exposes it
5
+ * to Droid, then asks Droid to call it.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/node/sdk-mcp-tool.ts
9
+ */
10
+
11
+ import { z } from 'zod';
12
+
1
13
  import {
2
14
  DroidMessageType,
3
15
  ToolConfirmationOutcome,
4
16
  createSession,
5
17
  createSdkMcpServer,
6
18
  tool,
7
- } from '@factory/droid-sdk';
8
- import { z } from 'zod';
9
-
10
- const execPath = process.env['DROID_EXEC_PATH'] ?? 'droid-dev';
19
+ } from '@factory/droid-sdk/node';
11
20
 
12
21
  const sdkTools = createSdkMcpServer({
13
22
  name: 'sdk-tools',
@@ -22,23 +31,24 @@ const sdkTools = createSdkMcpServer({
22
31
  });
23
32
 
24
33
  const session = await createSession({
25
- apiKey: process.env.FACTORY_API_KEY!,
26
- execPath,
27
34
  mcpServers: [sdkTools],
28
- cwd: process.cwd(),
35
+ // Not the focus of this example, but required: without a handler every
36
+ // permission request resolves to Cancel, so a tool needing approval would
37
+ // never run.
29
38
  permissionHandler: () => ToolConfirmationOutcome.ProceedOnce,
30
39
  });
31
40
 
32
41
  try {
33
42
  for await (const msg of session.stream(
34
- 'Use the favorite_number tool for Ada and tell me the answer.',
35
- { includePartialMessages: true }
43
+ 'Use the favorite_number tool for Ada and tell me the answer.'
36
44
  )) {
37
- if (msg.type === DroidMessageType.AssistantTextDelta) {
38
- process.stdout.write(msg.text);
45
+ if (msg.type === DroidMessageType.ToolCall) {
46
+ console.log(`[tool call] ${msg.name}`);
47
+ }
48
+ if (msg.type === DroidMessageType.Assistant) {
49
+ console.log(msg.text);
39
50
  }
40
51
  }
41
- console.log();
42
52
  } finally {
43
53
  await session.close();
44
54
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Session settings example.
3
+ *
4
+ * Reads settings from a live session, updates them with `updateSettings()`,
5
+ * and reads the updated values back.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/node/session-settings.ts
9
+ */
10
+
11
+ import {
12
+ AutonomyLevel,
13
+ DroidInteractionMode,
14
+ createSession,
15
+ } from '@factory/droid-sdk/node';
16
+
17
+ const session = await createSession({ autonomyLevel: AutonomyLevel.Low });
18
+
19
+ function printSettings(label: string): void {
20
+ const { modelId, interactionMode, autonomyLevel } = session.settings;
21
+ console.log(`${label}: ${modelId}, ${interactionMode}, ${autonomyLevel}`);
22
+ }
23
+
24
+ try {
25
+ printSettings('created with');
26
+
27
+ await session.updateSettings({
28
+ interactionMode: DroidInteractionMode.Spec,
29
+ autonomyLevel: AutonomyLevel.High,
30
+ });
31
+
32
+ printSettings('updated to ');
33
+ } finally {
34
+ await session.close();
35
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Session streaming example.
3
+ *
4
+ * Sends a prompt with `session.stream()` and handles each message type:
5
+ * assistant text, tool calls, tool results, and the final result.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/node/session-stream.ts
9
+ * npx tsx examples/node/session-stream.ts "What files are here?"
10
+ */
11
+
12
+ import { DroidMessageType, createSession } from '@factory/droid-sdk/node';
13
+
14
+ const session = await createSession();
15
+
16
+ try {
17
+ const prompt = process.argv[2] ?? 'What files are in the current directory?';
18
+
19
+ for await (const msg of session.stream(prompt)) {
20
+ switch (msg.type) {
21
+ case DroidMessageType.Assistant:
22
+ process.stdout.write(msg.text);
23
+ break;
24
+
25
+ case DroidMessageType.ToolCall:
26
+ console.log(`\n[tool] ${msg.name}`);
27
+ break;
28
+
29
+ case DroidMessageType.ToolResult:
30
+ console.log(`[tool result] ${msg.isError ? 'error' : 'ok'}`);
31
+ break;
32
+
33
+ case DroidMessageType.Result:
34
+ if (msg.tokenUsage) {
35
+ console.log(
36
+ `\n\ntokens: ${msg.tokenUsage.inputTokens} in, ${msg.tokenUsage.outputTokens} out`
37
+ );
38
+ }
39
+ break;
40
+ }
41
+ }
42
+ } finally {
43
+ await session.close();
44
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Spec mode: approve and hand off implementation to a new session.
3
+ *
4
+ * Approving the spec with `ProceedNewSessionHigh` moves implementation into a
5
+ * fresh session. The example prints the planning session's own id alongside the
6
+ * id of the session that actually implemented the spec, so the handoff is
7
+ * visible in the output; see spec-mode-same-session.ts for the in-place
8
+ * variant, which reports no handoff.
9
+ *
10
+ * The handed-off session announces itself only on the notification envelope:
11
+ * once implementation moves, `params.sessionId` on incoming notifications stops
12
+ * matching `session.id`. The typed message stream carries no session id per
13
+ * message, so the example subscribes with `session.onNotification()` and
14
+ * validates each envelope with `SessionNotificationSchema`.
15
+ *
16
+ * Usage:
17
+ * npx tsx examples/node/spec-mode-new-session.ts
18
+ */
19
+
20
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
21
+ import { tmpdir } from 'node:os';
22
+ import { join } from 'node:path';
23
+
24
+ import {
25
+ DroidInteractionMode,
26
+ ReasoningEffort,
27
+ SessionNotificationSchema,
28
+ ToolConfirmationOutcome,
29
+ ToolConfirmationType,
30
+ createSession,
31
+ } from '@factory/droid-sdk/node';
32
+
33
+ const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-spec-'));
34
+ const outputPath = join(tempDir, 'hello.txt');
35
+
36
+ let handoffSessionId: string | null = null;
37
+
38
+ try {
39
+ const session = await createSession({
40
+ interactionMode: DroidInteractionMode.Spec,
41
+ specModeReasoningEffort: ReasoningEffort.High,
42
+ permissionHandler(params) {
43
+ const canExitSpec = params.toolUses.some(
44
+ (item) => item.details.type === ToolConfirmationType.ExitSpecMode
45
+ );
46
+ return canExitSpec
47
+ ? ToolConfirmationOutcome.ProceedNewSessionHigh
48
+ : ToolConfirmationOutcome.ProceedOnce;
49
+ },
50
+ });
51
+
52
+ // The typed message stream carries no per-message session id, and the
53
+ // precise handoff signal the SDK uses internally is not part of the public
54
+ // surface, so a foreign envelope session id is the available proxy. It is
55
+ // only a proxy: any notification scoped to another session also trips it.
56
+ const unsubscribe = session.onNotification((notification) => {
57
+ const parsed = SessionNotificationSchema.safeParse(notification);
58
+ if (!parsed.success) return;
59
+ const envelopeSessionId = parsed.data.params.sessionId;
60
+ if (envelopeSessionId !== undefined && envelopeSessionId !== session.id) {
61
+ handoffSessionId = envelopeSessionId;
62
+ }
63
+ });
64
+
65
+ try {
66
+ for await (const _msg of session.stream(
67
+ `Plan then create ${outputPath} containing "Hello from Droid".`
68
+ )) {
69
+ // Consume the stream until the handoff implementation finishes.
70
+ }
71
+ } finally {
72
+ unsubscribe();
73
+ await session.close();
74
+ }
75
+
76
+ console.log(`planning session: ${session.id}`);
77
+ console.log(
78
+ `implementing session: ${handoffSessionId ?? '<same session>'}`
79
+ );
80
+ console.log(await readFile(outputPath, 'utf8'));
81
+ } finally {
82
+ await rm(tempDir, { recursive: true, force: true });
83
+ }
84
+
85
+ // Without a distinct implementing session this run is indistinguishable from
86
+ // the same-session variant, so the handoff this example demonstrates did not
87
+ // happen.
88
+ if (handoffSessionId === null) {
89
+ throw new Error(
90
+ 'Spec approval with ProceedNewSessionHigh did not hand off to a new session.'
91
+ );
92
+ }