@factory/droid-sdk 0.6.0 → 0.8.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 (92) hide show
  1. package/README.md +35 -482
  2. package/dist/chunk-4G2RKM2H.js +25831 -0
  3. package/dist/chunk-4G2RKM2H.js.map +1 -0
  4. package/dist/chunk-ROFAQUG4.mjs +25831 -0
  5. package/dist/chunk-ROFAQUG4.mjs.map +1 -0
  6. package/dist/{index-CwXX5IUH.d.cts → index-_nUPGHpH.d.mts} +92630 -79659
  7. package/dist/{index-CwXX5IUH.d.ts → index-_nUPGHpH.d.ts} +92630 -79659
  8. package/dist/index.d.mts +4 -0
  9. package/dist/index.d.ts +4 -93103
  10. package/dist/index.js +829 -11745
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +830 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/node.d.mts +757 -0
  15. package/dist/node.d.ts +757 -0
  16. package/dist/node.js +4731 -0
  17. package/dist/node.js.map +1 -0
  18. package/dist/node.mjs +4731 -0
  19. package/dist/node.mjs.map +1 -0
  20. package/docs/typescript-sdk-reference.md +1437 -0
  21. package/examples/browser/daemon-lifecycle.ts +95 -0
  22. package/examples/browser/daemon-model-discovery.ts +47 -0
  23. package/examples/browser/daemon-multi-session.ts +66 -0
  24. package/examples/browser/daemon-session.ts +73 -0
  25. package/examples/browser/index.html +69 -0
  26. package/examples/browser/list-sessions.ts +50 -0
  27. package/examples/node/abort-session-stream.ts +34 -0
  28. package/examples/node/ask-user-elicitation.ts +39 -0
  29. package/examples/node/compact-session.ts +63 -0
  30. package/examples/node/custom-system-prompt.ts +35 -0
  31. package/examples/node/document-input.ts +58 -0
  32. package/examples/node/enter-spec-mode.ts +87 -0
  33. package/examples/node/external-mcp.ts +45 -0
  34. package/examples/node/fork-session.ts +56 -0
  35. package/examples/node/hook-execution.ts +94 -0
  36. package/examples/node/image-attachment.ts +41 -0
  37. package/examples/node/init-metadata.ts +24 -0
  38. package/examples/node/interrupt-session.ts +55 -0
  39. package/examples/node/list-sessions.ts +20 -0
  40. package/examples/node/model-discovery.ts +28 -0
  41. package/examples/node/model-selection.ts +35 -0
  42. package/examples/node/multi-turn-session.ts +37 -0
  43. package/examples/node/observability.ts +75 -0
  44. package/examples/node/partial-stream.ts +56 -0
  45. package/examples/node/permission-handler.ts +60 -0
  46. package/examples/node/rename-session.ts +33 -0
  47. package/examples/node/result-metadata.ts +43 -0
  48. package/examples/node/resume-session.ts +41 -0
  49. package/examples/node/rewind-session.ts +79 -0
  50. package/examples/node/run.ts +32 -0
  51. package/examples/node/sdk-mcp-tool.ts +56 -0
  52. package/examples/node/session-settings.ts +35 -0
  53. package/examples/node/session-stream.ts +59 -0
  54. package/examples/node/skills.ts +24 -0
  55. package/examples/node/spec-mode-new-session.ts +120 -0
  56. package/examples/node/spec-mode-same-session.ts +119 -0
  57. package/examples/node/structured-output.ts +96 -0
  58. package/examples/node/tool-controls.ts +26 -0
  59. package/examples/package.json +3 -0
  60. package/package.json +20 -42
  61. package/dist/index.cjs +0 -12135
  62. package/dist/index.cjs.map +0 -1
  63. package/dist/index.d.cts +0 -93103
  64. package/dist/protocol/index.cjs +0 -6466
  65. package/dist/protocol/index.cjs.map +0 -1
  66. package/dist/protocol/index.d.cts +0 -2
  67. package/dist/protocol/index.d.ts +0 -2
  68. package/dist/protocol/index.js +0 -6078
  69. package/dist/protocol/index.js.map +0 -1
  70. package/docs/daemon-usage-guide.md +0 -544
  71. package/docs/sdk-usage-guide.md +0 -748
  72. package/examples/abort-session-stream.ts +0 -34
  73. package/examples/daemon-multi-session.ts +0 -67
  74. package/examples/droid-dev-structured-output.ts +0 -44
  75. package/examples/fork-session.ts +0 -68
  76. package/examples/hook-execution.ts +0 -68
  77. package/examples/init-metadata.ts +0 -24
  78. package/examples/interrupt-session.ts +0 -35
  79. package/examples/list-sessions.ts +0 -19
  80. package/examples/multi-turn-session.ts +0 -40
  81. package/examples/permission-handler.ts +0 -41
  82. package/examples/readme-structured-output.ts +0 -26
  83. package/examples/result-metadata.ts +0 -63
  84. package/examples/run.ts +0 -39
  85. package/examples/sdk-mcp-tool.ts +0 -44
  86. package/examples/session-stream.ts +0 -57
  87. package/examples/spec-mode-new-session.ts +0 -61
  88. package/examples/spec-mode-same-session.ts +0 -72
  89. package/examples/structured-output.ts +0 -60
  90. package/examples/test-compact.ts +0 -65
  91. package/examples/test-rewind.ts +0 -87
  92. package/examples/tool-controls.ts +0 -37
@@ -0,0 +1,60 @@
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({ toolUses, options }) {
29
+ handledPermission = true;
30
+ const onlyCreatesExpectedFile =
31
+ toolUses.length > 0 &&
32
+ toolUses.every(
33
+ (item) =>
34
+ item.details.type === ToolConfirmationType.Create &&
35
+ item.details.filePath === outputPath
36
+ );
37
+
38
+ const desired = onlyCreatesExpectedFile
39
+ ? ToolConfirmationOutcome.ProceedOnce
40
+ : ToolConfirmationOutcome.Cancel;
41
+
42
+ const offered = options.find((option) => option.value === desired);
43
+ if (!offered) throw new Error(`Outcome not offered: ${desired}`);
44
+
45
+ return desired;
46
+ },
47
+ }
48
+ );
49
+
50
+ if (!result.success) {
51
+ throw new Error(result.error?.message ?? 'The turn was interrupted.');
52
+ }
53
+ if (!handledPermission) {
54
+ throw new Error('The turn finished without requesting permission.');
55
+ }
56
+
57
+ console.log(await readFile(outputPath, 'utf8'));
58
+ } finally {
59
+ await rm(tempDir, { recursive: true, force: true });
60
+ }
@@ -0,0 +1,33 @@
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 { DroidMessageType, 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 message of session.stream('Reply with exactly: OK')) {
19
+ if (message.type === DroidMessageType.Result && !message.success) {
20
+ throw new Error(
21
+ message.error?.message ?? `Turn ended: ${message.subtype}`
22
+ );
23
+ }
24
+ }
25
+
26
+ await session.rename({
27
+ title: 'Quarterly report analysis',
28
+ });
29
+
30
+ console.log(`renamed session: ${session.id}`);
31
+ } finally {
32
+ await session.close();
33
+ }
@@ -0,0 +1,43 @@
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
+ `turn tokens: ${result.tokenUsage.inputTokens} in, ${result.tokenUsage.outputTokens} out`
33
+ );
34
+ console.log(
35
+ `turn cache: ${result.tokenUsage.cacheReadTokens} read, ${result.tokenUsage.cacheCreationTokens} created`
36
+ );
37
+ }
38
+
39
+ if (!result.success) {
40
+ throw new Error(
41
+ result.error?.message ?? `The turn failed (${result.subtype}).`
42
+ );
43
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Resume session example.
3
+ *
4
+ * Loads a saved session and sends a follow-up prompt with its existing
5
+ * conversation, working directory, and settings.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/node/resume-session.ts session-id
9
+ */
10
+
11
+ import { DroidMessageType, resumeSession } from '@factory/droid-sdk/node';
12
+
13
+ const sessionId = process.argv[2];
14
+ if (!sessionId) {
15
+ throw new Error('Pass a session ID. Use listSessions() to find one.');
16
+ }
17
+
18
+ const session = await resumeSession(sessionId);
19
+
20
+ try {
21
+ console.log(`cwd: ${String(session.cwd)}`);
22
+ console.log(`model: ${session.settings.modelId}`);
23
+
24
+ let assistantText = '';
25
+ for await (const message of session.stream(
26
+ 'Continue from the last conversation.'
27
+ )) {
28
+ if (message.type === DroidMessageType.Assistant) {
29
+ assistantText += message.text;
30
+ console.log(message.text);
31
+ }
32
+ if (message.type === DroidMessageType.Result && !message.success) {
33
+ throw new Error(
34
+ message.error?.message ?? `Turn ended: ${message.subtype}`
35
+ );
36
+ }
37
+ }
38
+ if (!assistantText) throw new Error('The turn produced no assistant text.');
39
+ } finally {
40
+ await session.close();
41
+ }
@@ -0,0 +1,79 @@
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<{ messageId: string; text: string }> {
19
+ let messageId: string | undefined;
20
+ let text = '';
21
+
22
+ for await (const message of activeSession.stream(prompt)) {
23
+ if (
24
+ message.type === DroidMessageType.User &&
25
+ message.message.visibility !== 'llm_only'
26
+ ) {
27
+ messageId ??= message.message.id;
28
+ }
29
+ if (message.type === DroidMessageType.Assistant) {
30
+ text += message.text;
31
+ console.log(message.text);
32
+ }
33
+ if (message.type === DroidMessageType.Result && !message.success) {
34
+ throw new Error(
35
+ message.error?.message ?? `Turn ended: ${message.subtype}`
36
+ );
37
+ }
38
+ }
39
+
40
+ if (!messageId) throw new Error('The turn produced no user message ID.');
41
+ if (!text.trim()) throw new Error('The turn produced no assistant output.');
42
+ return { messageId, text };
43
+ }
44
+
45
+ try {
46
+ await ask(session, 'Remember this phrase exactly: mango sunrise');
47
+ const { messageId: secondTurn } = await ask(
48
+ session,
49
+ 'Now forget it and remember: cobalt harbor'
50
+ );
51
+
52
+ const { availableFiles, createdFiles } = await session.getRewindInfo({
53
+ messageId: secondTurn,
54
+ });
55
+ console.log(
56
+ `rewind would restore ${availableFiles.length} file(s) ` +
57
+ `and delete ${createdFiles.length}`
58
+ );
59
+
60
+ const { session: successor } = await session.rewind({
61
+ messageId: secondTurn,
62
+ filesToRestore: availableFiles,
63
+ filesToDelete: createdFiles,
64
+ forkTitle: 'Before the second phrase',
65
+ });
66
+
67
+ try {
68
+ // `session` is now a retired wrapper. Its persisted ID can still be passed
69
+ // to resumeSession() later if the pre-rewind history is needed again.
70
+ console.log(`rewound into session: ${successor.id}`);
71
+
72
+ await ask(successor, 'What phrase did I ask you to remember?');
73
+ } finally {
74
+ await successor.close();
75
+ }
76
+ } finally {
77
+ // Closes the active source, or does nothing if it was retired.
78
+ await session.close();
79
+ }
@@ -0,0 +1,32 @@
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 result = await run(process.argv.slice(2).join(' ') || 'What is 2 + 2?');
15
+
16
+ if (!result.success) {
17
+ throw new Error(
18
+ result.error?.message ?? `The turn failed (${result.subtype}).`
19
+ );
20
+ }
21
+ if (!result.text.trim()) {
22
+ throw new Error('The turn produced no assistant output.');
23
+ }
24
+
25
+ console.log(result.text);
26
+ console.log(`\nmessages: ${result.messages.length}`);
27
+
28
+ if (result.tokenUsage) {
29
+ console.log(
30
+ `tokens: ${result.tokenUsage.inputTokens} in, ${result.tokenUsage.outputTokens} out`
31
+ );
32
+ }
@@ -0,0 +1,56 @@
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
+
13
+ import {
14
+ DroidMessageType,
15
+ ToolConfirmationOutcome,
16
+ createSession,
17
+ createSdkMcpServer,
18
+ tool,
19
+ } from '@factory/droid-sdk/node';
20
+
21
+ const sdkTools = createSdkMcpServer({
22
+ name: 'sdk-tools',
23
+ tools: [
24
+ tool(
25
+ 'favorite_number',
26
+ 'Returns a favorite number for a person',
27
+ { name: z.string() },
28
+ ({ name }) => `${name}'s favorite number is 42.`
29
+ ),
30
+ ],
31
+ });
32
+
33
+ const session = await createSession({
34
+ mcpServers: [sdkTools],
35
+ permissionHandler: () => ToolConfirmationOutcome.ProceedOnce,
36
+ });
37
+
38
+ try {
39
+ for await (const message of session.stream(
40
+ 'Use the favorite_number tool for Ada and tell me the answer.'
41
+ )) {
42
+ if (message.type === DroidMessageType.ToolCall) {
43
+ console.log(`[tool call] ${message.name}`);
44
+ }
45
+ if (message.type === DroidMessageType.Assistant) {
46
+ console.log(message.text);
47
+ }
48
+ if (message.type === DroidMessageType.Result && !message.success) {
49
+ throw new Error(
50
+ message.error?.message ?? `Turn ended: ${message.subtype}`
51
+ );
52
+ }
53
+ }
54
+ } finally {
55
+ await session.close();
56
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Session settings example.
3
+ *
4
+ * Reads settings from a live session, updates them, and changes between Auto
5
+ * and Spec modes.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/node/session-settings.ts
9
+ */
10
+
11
+ import { AutonomyLevel, createSession } from '@factory/droid-sdk/node';
12
+
13
+ const session = await createSession({ autonomyLevel: AutonomyLevel.Low });
14
+
15
+ function printSettings(label: string): void {
16
+ const { modelId, interactionMode, autonomyLevel } = session.settings;
17
+ console.log(`${label}: ${modelId}, ${interactionMode}, ${autonomyLevel}`);
18
+ }
19
+
20
+ try {
21
+ printSettings('created with');
22
+
23
+ await session.updateSettings({
24
+ autonomyLevel: AutonomyLevel.High,
25
+ });
26
+ printSettings('updated to ');
27
+
28
+ await session.enterSpecMode();
29
+ printSettings('entered Spec');
30
+
31
+ await session.exitSpecMode();
32
+ printSettings('exited Spec ');
33
+ } finally {
34
+ await session.close();
35
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Session streaming example.
3
+ *
4
+ * Sends a prompt with `session.stream()` and handles every message type
5
+ * emitted by a default (non-partial) stream.
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.User:
26
+ console.log('\n[user message accepted]');
27
+ break;
28
+
29
+ case DroidMessageType.ToolCall:
30
+ console.log(`\n[tool] ${msg.name}`);
31
+ break;
32
+
33
+ case DroidMessageType.ToolResult:
34
+ console.log(`[tool result] ${msg.isError ? 'error' : 'ok'}`);
35
+ break;
36
+
37
+ case DroidMessageType.Hook:
38
+ console.log(`\n[hook] ${msg.hookId}: ${msg.status}`);
39
+ break;
40
+
41
+ case DroidMessageType.Error:
42
+ console.error(`\n[error] ${msg.message}`);
43
+ break;
44
+
45
+ case DroidMessageType.Result:
46
+ if (!msg.success) {
47
+ throw new Error(msg.error?.message ?? `Turn ended: ${msg.subtype}`);
48
+ }
49
+ if (msg.tokenUsage) {
50
+ console.log(
51
+ `\n\ntokens: ${msg.tokenUsage.inputTokens} in, ${msg.tokenUsage.outputTokens} out`
52
+ );
53
+ }
54
+ break;
55
+ }
56
+ }
57
+ } finally {
58
+ await session.close();
59
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Skills example.
3
+ *
4
+ * Lists the skills available to a Node session.
5
+ *
6
+ * Usage:
7
+ * npx tsx examples/node/skills.ts
8
+ */
9
+
10
+ import { createSession } from '@factory/droid-sdk/node';
11
+
12
+ const session = await createSession();
13
+
14
+ try {
15
+ const { skills } = await session.listSkills();
16
+
17
+ for (const skill of skills) {
18
+ console.log(
19
+ `${skill.name}: ${skill.enabled === false ? 'disabled' : 'enabled'}`
20
+ );
21
+ }
22
+ } finally {
23
+ await session.close();
24
+ }
@@ -0,0 +1,120 @@
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
+ type ClientPermissionHandler,
26
+ DroidInteractionMode,
27
+ DroidMessageType,
28
+ ReasoningEffort,
29
+ SessionNotificationSchema,
30
+ ToolConfirmationOutcome,
31
+ ToolConfirmationType,
32
+ createSession,
33
+ } from '@factory/droid-sdk/node';
34
+
35
+ const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-spec-'));
36
+ const outputPath = join(tempDir, 'hello.txt');
37
+
38
+ let handoffSessionId: string | null = null;
39
+
40
+ const handlePermissionRequest: ClientPermissionHandler = ({
41
+ toolUses,
42
+ options,
43
+ }) => {
44
+ const exitsSpecMode =
45
+ toolUses.length > 0 &&
46
+ toolUses.every(
47
+ (toolUse) => toolUse.details.type === ToolConfirmationType.ExitSpecMode
48
+ );
49
+ const createsExpectedFile =
50
+ toolUses.length > 0 &&
51
+ toolUses.every(
52
+ (toolUse) =>
53
+ toolUse.details.type === ToolConfirmationType.Create &&
54
+ toolUse.details.filePath === outputPath
55
+ );
56
+
57
+ let outcome = ToolConfirmationOutcome.Cancel;
58
+ if (exitsSpecMode) {
59
+ outcome = ToolConfirmationOutcome.ProceedNewSessionHigh;
60
+ } else if (createsExpectedFile) {
61
+ outcome = ToolConfirmationOutcome.ProceedOnce;
62
+ }
63
+ if (!options.some((option) => option.value === outcome)) {
64
+ throw new Error(`Outcome not offered: ${outcome}`);
65
+ }
66
+ return outcome;
67
+ };
68
+
69
+ try {
70
+ const session = await createSession({
71
+ interactionMode: DroidInteractionMode.Spec,
72
+ specModeReasoningEffort: ReasoningEffort.High,
73
+ permissionHandler: handlePermissionRequest,
74
+ });
75
+
76
+ // The typed message stream carries no per-message session id, and the
77
+ // precise handoff signal the SDK uses internally is not part of the public
78
+ // surface, so a foreign envelope session id is the available proxy. It is
79
+ // only a proxy: any notification scoped to another session also trips it.
80
+ const unsubscribe = session.onNotification((notification) => {
81
+ const parsed = SessionNotificationSchema.safeParse(notification);
82
+ if (!parsed.success) return;
83
+ const envelopeSessionId = parsed.data.params.sessionId;
84
+ if (envelopeSessionId !== undefined && envelopeSessionId !== session.id) {
85
+ handoffSessionId = envelopeSessionId;
86
+ }
87
+ });
88
+
89
+ try {
90
+ for await (const message of session.stream(
91
+ `Plan then create ${outputPath} containing "Hello from Droid".`
92
+ )) {
93
+ if (message.type === DroidMessageType.Result && !message.success) {
94
+ throw new Error(
95
+ message.error?.message ?? `Turn ended: ${message.subtype}`
96
+ );
97
+ }
98
+ }
99
+ } finally {
100
+ unsubscribe();
101
+ await session.close();
102
+ }
103
+
104
+ console.log(`planning session: ${session.id}`);
105
+ console.log(
106
+ `implementing session: ${handoffSessionId ?? '<same session>'}`
107
+ );
108
+ console.log(await readFile(outputPath, 'utf8'));
109
+ } finally {
110
+ await rm(tempDir, { recursive: true, force: true });
111
+ }
112
+
113
+ // Without a distinct implementing session this run is indistinguishable from
114
+ // the same-session variant, so the handoff this example demonstrates did not
115
+ // happen.
116
+ if (handoffSessionId === null) {
117
+ throw new Error(
118
+ 'Spec approval with ProceedNewSessionHigh did not hand off to a new session.'
119
+ );
120
+ }