@factory/droid-sdk 0.2.0 → 0.4.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.
- package/README.md +214 -82
- package/dist/index.cjs +2345 -692
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2570 -232
- package/dist/index.d.ts +2570 -232
- package/dist/index.js +2298 -697
- package/dist/index.js.map +1 -1
- package/docs/daemon-usage-guide.md +544 -0
- package/docs/sdk-usage-guide.md +748 -0
- package/examples/abort-session-stream.ts +23 -56
- package/examples/daemon-multi-session.ts +67 -0
- package/examples/droid-dev-structured-output.ts +5 -5
- package/examples/fork-session.ts +33 -7
- package/examples/hook-execution.ts +68 -0
- package/examples/init-metadata.ts +14 -66
- package/examples/interrupt-session.ts +23 -55
- package/examples/list-sessions.ts +9 -55
- package/examples/multi-turn-session.ts +27 -54
- package/examples/permission-handler.ts +27 -84
- package/examples/readme-structured-output.ts +26 -0
- package/examples/result-metadata.ts +2 -1
- package/examples/run.ts +2 -1
- package/examples/sdk-mcp-tool.ts +14 -8
- package/examples/session-stream.ts +57 -0
- package/examples/spec-mode-new-session.ts +39 -99
- package/examples/spec-mode-same-session.ts +50 -111
- package/examples/structured-output.ts +17 -10
- package/examples/test-compact.ts +9 -4
- package/examples/test-rewind.ts +49 -117
- package/examples/tool-controls.ts +23 -55
- package/package.json +3 -1
- package/docs/examples/init-metadata.md +0 -79
- package/docs/examples/list-sessions.md +0 -64
- package/docs/examples/multi-turn-session.md +0 -77
- package/docs/examples/one-shot-query.md +0 -73
- package/docs/examples/permission-handler.md +0 -102
- package/docs/examples/run.md +0 -52
- package/docs/examples/spec-mode.md +0 -101
- package/docs/examples/tool-controls.md +0 -90
- package/docs/typescript-sdk-reference.md +0 -372
- package/examples/simple-query.ts +0 -53
|
@@ -1,98 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permission handler example.
|
|
3
3
|
*
|
|
4
|
-
* Demonstrates using `query()` with a custom `permissionHandler` that
|
|
5
|
-
* logs tool confirmation details, approves only one expected file creation,
|
|
6
|
-
* and cancels any unexpected tool request.
|
|
7
|
-
*
|
|
8
4
|
* Usage:
|
|
9
5
|
* npx tsx examples/permission-handler.ts
|
|
10
6
|
*/
|
|
11
7
|
|
|
12
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
8
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
13
9
|
import { tmpdir } from 'node:os';
|
|
14
10
|
import { join } from 'node:path';
|
|
15
11
|
|
|
16
12
|
import {
|
|
17
|
-
DroidMessageType,
|
|
18
|
-
query,
|
|
19
13
|
ToolConfirmationOutcome,
|
|
20
14
|
ToolConfirmationType,
|
|
21
|
-
|
|
22
|
-
} from '
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function main(): Promise<void> {
|
|
50
|
-
const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-permission-'));
|
|
51
|
-
const outputPath = join(tempDir, 'hello.txt');
|
|
52
|
-
const prompt =
|
|
53
|
-
process.argv[2] ??
|
|
54
|
-
`Create a file called ${outputPath} with the text 'Hello, World!'`;
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
console.log(`Sending prompt: "${prompt}"\n`);
|
|
58
|
-
|
|
59
|
-
const stream = query({
|
|
60
|
-
prompt,
|
|
61
|
-
cwd: process.cwd(),
|
|
62
|
-
permissionHandler: (params) => permissionHandler(outputPath, params),
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
for await (const msg of stream) {
|
|
66
|
-
switch (msg.type) {
|
|
67
|
-
case DroidMessageType.AssistantTextDelta:
|
|
68
|
-
process.stdout.write(msg.text);
|
|
69
|
-
break;
|
|
70
|
-
|
|
71
|
-
case DroidMessageType.ToolUse:
|
|
72
|
-
console.log(`\n[Tool Use] ${msg.toolName}`);
|
|
73
|
-
break;
|
|
74
|
-
|
|
75
|
-
case DroidMessageType.ToolResult:
|
|
76
|
-
console.log(`[Tool Result] ${msg.isError ? 'Error' : 'Success'}`);
|
|
77
|
-
break;
|
|
78
|
-
|
|
79
|
-
case DroidMessageType.TurnComplete:
|
|
80
|
-
console.log('\n\n--- Turn complete ---');
|
|
81
|
-
if (msg.tokenUsage) {
|
|
82
|
-
console.log(
|
|
83
|
-
`Tokens — input: ${msg.tokenUsage.inputTokens}, ` +
|
|
84
|
-
`output: ${msg.tokenUsage.outputTokens}`
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
} finally {
|
|
91
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
92
|
-
}
|
|
15
|
+
run,
|
|
16
|
+
} from '@factory/droid-sdk';
|
|
17
|
+
|
|
18
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-permission-'));
|
|
19
|
+
const outputPath = join(tempDir, 'hello.txt');
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await run(`Create ${outputPath} with the text "Hello, World!"`, {
|
|
23
|
+
apiKey: process.env.FACTORY_API_KEY!,
|
|
24
|
+
cwd: process.cwd(),
|
|
25
|
+
permissionHandler(params) {
|
|
26
|
+
const onlyCreatesExpectedFile = params.toolUses.every(
|
|
27
|
+
(item) =>
|
|
28
|
+
item.details.type === ToolConfirmationType.Create &&
|
|
29
|
+
item.details.filePath === outputPath
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return onlyCreatesExpectedFile
|
|
33
|
+
? ToolConfirmationOutcome.ProceedOnce
|
|
34
|
+
: ToolConfirmationOutcome.Cancel;
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
console.log(await readFile(outputPath, 'utf8'));
|
|
39
|
+
} finally {
|
|
40
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
93
41
|
}
|
|
94
|
-
|
|
95
|
-
main().catch((err: unknown) => {
|
|
96
|
-
console.error('Error:', err);
|
|
97
|
-
process.exit(1);
|
|
98
|
-
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { OutputFormatType, run } from '@factory/droid-sdk';
|
|
2
|
+
|
|
3
|
+
type FavoriteNumber = { favoriteNumber: number };
|
|
4
|
+
|
|
5
|
+
const result = await run('Pick a favorite number between 1 and 42.', {
|
|
6
|
+
apiKey: process.env.FACTORY_API_KEY!,
|
|
7
|
+
cwd: process.cwd(),
|
|
8
|
+
outputFormat: {
|
|
9
|
+
type: OutputFormatType.JsonSchema,
|
|
10
|
+
schema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
favoriteNumber: {
|
|
14
|
+
type: 'number',
|
|
15
|
+
minimum: 1,
|
|
16
|
+
maximum: 42,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
required: ['favoriteNumber'],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
console.log(
|
|
25
|
+
(result.structuredOutput as FavoriteNumber | undefined)?.favoriteNumber
|
|
26
|
+
);
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import assert from 'node:assert/strict';
|
|
14
14
|
|
|
15
|
-
import { run } from '
|
|
15
|
+
import { run } from '@factory/droid-sdk';
|
|
16
16
|
|
|
17
17
|
async function main(): Promise<void> {
|
|
18
18
|
const prompt =
|
|
@@ -22,6 +22,7 @@ async function main(): Promise<void> {
|
|
|
22
22
|
console.log(`Sending prompt: "${prompt}"\n`);
|
|
23
23
|
|
|
24
24
|
const result = await run(prompt, {
|
|
25
|
+
apiKey: process.env.FACTORY_API_KEY!,
|
|
25
26
|
cwd: process.cwd(),
|
|
26
27
|
});
|
|
27
28
|
|
package/examples/run.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* npx tsx examples/run.ts "What is 2 + 2?"
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { run } from '
|
|
12
|
+
import { run } from '@factory/droid-sdk';
|
|
13
13
|
|
|
14
14
|
async function main(): Promise<void> {
|
|
15
15
|
const text = process.argv.slice(2).join(' ') || 'What is 2 + 2?';
|
|
@@ -17,6 +17,7 @@ async function main(): Promise<void> {
|
|
|
17
17
|
console.log(`Sending prompt: "${text}"\n`);
|
|
18
18
|
|
|
19
19
|
const result = await run(text, {
|
|
20
|
+
apiKey: process.env.FACTORY_API_KEY!,
|
|
20
21
|
cwd: process.cwd(),
|
|
21
22
|
});
|
|
22
23
|
|
package/examples/sdk-mcp-tool.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
1
|
import {
|
|
2
|
+
DroidMessageType,
|
|
4
3
|
ToolConfirmationOutcome,
|
|
5
4
|
createSession,
|
|
6
5
|
createSdkMcpServer,
|
|
7
6
|
tool,
|
|
8
|
-
} from '
|
|
7
|
+
} from '@factory/droid-sdk';
|
|
8
|
+
import { z } from 'zod';
|
|
9
9
|
|
|
10
|
-
const execPath = process.env['DROID_EXEC_PATH'] ?? 'droid';
|
|
10
|
+
const execPath = process.env['DROID_EXEC_PATH'] ?? 'droid-dev';
|
|
11
11
|
|
|
12
12
|
const sdkTools = createSdkMcpServer({
|
|
13
13
|
name: 'sdk-tools',
|
|
@@ -22,6 +22,7 @@ const sdkTools = createSdkMcpServer({
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
const session = await createSession({
|
|
25
|
+
apiKey: process.env.FACTORY_API_KEY!,
|
|
25
26
|
execPath,
|
|
26
27
|
mcpServers: [sdkTools],
|
|
27
28
|
cwd: process.cwd(),
|
|
@@ -29,10 +30,15 @@ const session = await createSession({
|
|
|
29
30
|
});
|
|
30
31
|
|
|
31
32
|
try {
|
|
32
|
-
const
|
|
33
|
-
'Use the favorite_number tool for Ada and tell me the answer.'
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
for await (const msg of session.stream(
|
|
34
|
+
'Use the favorite_number tool for Ada and tell me the answer.',
|
|
35
|
+
{ includePartialMessages: true }
|
|
36
|
+
)) {
|
|
37
|
+
if (msg.type === DroidMessageType.AssistantTextDelta) {
|
|
38
|
+
process.stdout.write(msg.text);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
console.log();
|
|
36
42
|
} finally {
|
|
37
43
|
await session.close();
|
|
38
44
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple session streaming example.
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates using `session.stream()` to send a prompt, streaming
|
|
5
|
+
* full assistant/tool messages, and handling the final `result`.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx tsx examples/session-stream.ts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { DroidMessageType, createSession } from '@factory/droid-sdk';
|
|
12
|
+
|
|
13
|
+
async function main(): Promise<void> {
|
|
14
|
+
const prompt = process.argv[2] ?? 'What files are in the current directory?';
|
|
15
|
+
|
|
16
|
+
console.log(`Sending prompt: "${prompt}"\n`);
|
|
17
|
+
|
|
18
|
+
const session = await createSession({
|
|
19
|
+
apiKey: process.env.FACTORY_API_KEY!,
|
|
20
|
+
cwd: process.cwd(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
for await (const msg of session.stream(prompt)) {
|
|
25
|
+
switch (msg.type) {
|
|
26
|
+
case DroidMessageType.Assistant:
|
|
27
|
+
process.stdout.write(msg.text);
|
|
28
|
+
break;
|
|
29
|
+
|
|
30
|
+
case DroidMessageType.ToolCall:
|
|
31
|
+
console.log(`\n[Tool] ${msg.toolUse.name}`);
|
|
32
|
+
break;
|
|
33
|
+
|
|
34
|
+
case DroidMessageType.ToolResult:
|
|
35
|
+
console.log(`[Tool Result] ${msg.isError ? 'Error' : 'OK'}`);
|
|
36
|
+
break;
|
|
37
|
+
|
|
38
|
+
case DroidMessageType.Result:
|
|
39
|
+
console.log('\n\n--- Turn complete ---');
|
|
40
|
+
if (msg.tokenUsage) {
|
|
41
|
+
console.log(
|
|
42
|
+
`Tokens — input: ${msg.tokenUsage.inputTokens}, ` +
|
|
43
|
+
`output: ${msg.tokenUsage.outputTokens}`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} finally {
|
|
50
|
+
await session.close();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
main().catch((err: unknown) => {
|
|
55
|
+
console.error('Error:', err);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
});
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Spec mode: approve and hand off to a new session.
|
|
3
|
-
*
|
|
4
|
-
* The agent drafts a plan in spec mode. On approval with
|
|
5
|
-
* ProceedNewSessionHigh, the server automatically creates a
|
|
6
|
-
* fresh session and implements the plan. The results stream
|
|
7
|
-
* back through the same query — no manual session creation needed.
|
|
2
|
+
* Spec mode: approve and hand off implementation to a new session.
|
|
8
3
|
*
|
|
9
4
|
* Usage:
|
|
10
5
|
* npx tsx examples/spec-mode-new-session.ts
|
|
@@ -15,107 +10,52 @@ import { tmpdir } from 'node:os';
|
|
|
15
10
|
import { join } from 'node:path';
|
|
16
11
|
|
|
17
12
|
import {
|
|
18
|
-
DroidMessageType,
|
|
19
13
|
DroidInteractionMode,
|
|
20
|
-
query,
|
|
21
14
|
ReasoningEffort,
|
|
22
15
|
ToolConfirmationOutcome,
|
|
23
16
|
ToolConfirmationType,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
permissionHandler(params) {
|
|
46
|
-
const exitSpec = params.toolUses.find(
|
|
47
|
-
(item) => item.details.type === ToolConfirmationType.ExitSpecMode
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
if (exitSpec?.details.type === ToolConfirmationType.ExitSpecMode) {
|
|
51
|
-
console.log(
|
|
52
|
-
'\n--- Plan received, approving with new session handoff ---'
|
|
53
|
-
);
|
|
54
|
-
console.log(exitSpec.details.plan);
|
|
55
|
-
console.log('---\n');
|
|
56
|
-
|
|
57
|
-
// The server creates a fresh session and implements the plan.
|
|
58
|
-
// Results continue streaming through this same query.
|
|
59
|
-
return ToolConfirmationOutcome.ProceedNewSessionHigh;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const onlyAllowedCreate = params.toolUses.every(
|
|
63
|
-
(item) =>
|
|
64
|
-
item.details.type === ToolConfirmationType.Create &&
|
|
65
|
-
item.details.filePath === outputPath
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
return onlyAllowedCreate
|
|
69
|
-
? ToolConfirmationOutcome.ProceedOnce
|
|
70
|
-
: ToolConfirmationOutcome.Cancel;
|
|
71
|
-
},
|
|
72
|
-
});
|
|
17
|
+
createSession,
|
|
18
|
+
} from '@factory/droid-sdk';
|
|
19
|
+
|
|
20
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-spec-'));
|
|
21
|
+
const outputPath = join(tempDir, 'hello.txt');
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const session = await createSession({
|
|
25
|
+
apiKey: process.env.FACTORY_API_KEY!,
|
|
26
|
+
cwd: process.cwd(),
|
|
27
|
+
interactionMode: DroidInteractionMode.Spec,
|
|
28
|
+
specModeReasoningEffort: ReasoningEffort.High,
|
|
29
|
+
permissionHandler(params) {
|
|
30
|
+
const canExitSpec = params.toolUses.some(
|
|
31
|
+
(item) => item.details.type === ToolConfirmationType.ExitSpecMode
|
|
32
|
+
);
|
|
33
|
+
const onlyCreatesFile = params.toolUses.every(
|
|
34
|
+
(item) =>
|
|
35
|
+
item.details.type === ToolConfirmationType.Create &&
|
|
36
|
+
item.details.filePath === outputPath
|
|
37
|
+
);
|
|
73
38
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
case DroidMessageType.AssistantTextDelta:
|
|
77
|
-
process.stdout.write(msg.text);
|
|
78
|
-
break;
|
|
79
|
-
case DroidMessageType.ToolUse:
|
|
80
|
-
console.log(`\n[Tool] ${msg.toolName}`);
|
|
81
|
-
break;
|
|
82
|
-
case DroidMessageType.ToolResult:
|
|
83
|
-
if (msg.isError) {
|
|
84
|
-
toolError = JSON.stringify(msg.content);
|
|
85
|
-
}
|
|
86
|
-
console.log(`[Tool Result] ${msg.isError ? 'Error' : 'OK'}`);
|
|
87
|
-
break;
|
|
88
|
-
case DroidMessageType.TurnComplete:
|
|
89
|
-
console.log('\n\n--- Turn complete ---');
|
|
90
|
-
if (msg.tokenUsage) {
|
|
91
|
-
console.log(
|
|
92
|
-
`Tokens — input: ${msg.tokenUsage.inputTokens}, output: ${msg.tokenUsage.outputTokens}`
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
break;
|
|
39
|
+
if (canExitSpec) {
|
|
40
|
+
return ToolConfirmationOutcome.ProceedNewSessionHigh;
|
|
96
41
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
);
|
|
103
|
-
}
|
|
42
|
+
return onlyCreatesFile
|
|
43
|
+
? ToolConfirmationOutcome.ProceedOnce
|
|
44
|
+
: ToolConfirmationOutcome.Cancel;
|
|
45
|
+
},
|
|
46
|
+
});
|
|
104
47
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
48
|
+
try {
|
|
49
|
+
for await (const _msg of session.stream(
|
|
50
|
+
`Plan then create ${outputPath} containing "Hello from Droid".`
|
|
51
|
+
)) {
|
|
52
|
+
// Consume the stream until the handoff implementation finishes.
|
|
110
53
|
}
|
|
111
|
-
|
|
112
|
-
console.log(`\nVerified ${outputPath}.`);
|
|
113
54
|
} finally {
|
|
114
|
-
await
|
|
55
|
+
await session.close();
|
|
115
56
|
}
|
|
116
|
-
}
|
|
117
57
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
58
|
+
console.log(await readFile(outputPath, 'utf8'));
|
|
59
|
+
} finally {
|
|
60
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
61
|
+
}
|
|
@@ -1,133 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Spec mode: approve and implement in the same session.
|
|
3
3
|
*
|
|
4
|
-
* The agent drafts a plan in spec mode, then implements it
|
|
5
|
-
* in the same query stream after approval.
|
|
6
|
-
*
|
|
7
4
|
* Usage:
|
|
8
5
|
* npx tsx examples/spec-mode-same-session.ts
|
|
9
6
|
*/
|
|
10
7
|
|
|
11
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
12
|
-
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
13
9
|
import { tmpdir } from 'node:os';
|
|
14
10
|
import { join } from 'node:path';
|
|
15
11
|
|
|
16
12
|
import {
|
|
17
|
-
DroidMessageType,
|
|
18
13
|
DroidInteractionMode,
|
|
19
|
-
query,
|
|
20
14
|
ReasoningEffort,
|
|
21
15
|
ToolConfirmationOutcome,
|
|
22
16
|
ToolConfirmationType,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
createSession,
|
|
18
|
+
} from '@factory/droid-sdk';
|
|
19
|
+
|
|
20
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-spec-'));
|
|
21
|
+
const outputPath = join(tempDir, 'hello.txt');
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const session = await createSession({
|
|
25
|
+
apiKey: process.env.FACTORY_API_KEY!,
|
|
26
|
+
cwd: process.cwd(),
|
|
27
|
+
interactionMode: DroidInteractionMode.Spec,
|
|
28
|
+
specModeReasoningEffort: ReasoningEffort.High,
|
|
29
|
+
permissionHandler(params) {
|
|
30
|
+
const canExitSpec = params.toolUses.some(
|
|
31
|
+
(item) => item.details.type === ToolConfirmationType.ExitSpecMode
|
|
32
|
+
);
|
|
33
|
+
const onlyCreatesFile = params.toolUses.every(
|
|
34
|
+
(item) =>
|
|
35
|
+
item.details.type === ToolConfirmationType.Create &&
|
|
36
|
+
item.details.filePath === outputPath
|
|
37
|
+
);
|
|
38
|
+
const onlyEditsTempFile = params.toolUses.every(
|
|
39
|
+
(item) =>
|
|
40
|
+
item.details.type === ToolConfirmationType.ApplyPatch &&
|
|
41
|
+
item.details.filePath === outputPath
|
|
42
|
+
);
|
|
43
|
+
const onlyRunsTempCommand = params.toolUses.every(
|
|
44
|
+
(item) =>
|
|
45
|
+
item.details.type === ToolConfirmationType.Execute &&
|
|
46
|
+
item.details.fullCommand.includes(outputPath) &&
|
|
47
|
+
item.details.fullCommand.includes(tempDir)
|
|
48
|
+
);
|
|
33
49
|
|
|
34
|
-
|
|
50
|
+
return canExitSpec ||
|
|
51
|
+
onlyCreatesFile ||
|
|
52
|
+
onlyEditsTempFile ||
|
|
53
|
+
onlyRunsTempCommand
|
|
54
|
+
? ToolConfirmationOutcome.ProceedOnce
|
|
55
|
+
: ToolConfirmationOutcome.Cancel;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
35
58
|
|
|
36
59
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
cwd: process.cwd(),
|
|
42
|
-
interactionMode: DroidInteractionMode.Spec,
|
|
43
|
-
specModeReasoningEffort: ReasoningEffort.High,
|
|
44
|
-
permissionHandler(params) {
|
|
45
|
-
const exitSpec = params.toolUses.find(
|
|
46
|
-
(item) => item.details.type === ToolConfirmationType.ExitSpecMode
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
if (exitSpec?.details.type === ToolConfirmationType.ExitSpecMode) {
|
|
50
|
-
console.log('\n--- Plan received, approving in same session ---');
|
|
51
|
-
console.log(exitSpec.details.plan);
|
|
52
|
-
console.log('---\n');
|
|
53
|
-
return ToolConfirmationOutcome.ProceedOnce;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const onlyAllowedCreate = params.toolUses.every(
|
|
57
|
-
(item) =>
|
|
58
|
-
item.details.type === ToolConfirmationType.Create &&
|
|
59
|
-
item.details.filePath === outputPath
|
|
60
|
-
);
|
|
61
|
-
const onlyAllowedApplyPatch = params.toolUses.every(
|
|
62
|
-
(item) =>
|
|
63
|
-
item.details.type === ToolConfirmationType.ApplyPatch &&
|
|
64
|
-
item.details.filePath === outputPath
|
|
65
|
-
);
|
|
66
|
-
const onlyAllowedExecute = params.toolUses.every((item) => {
|
|
67
|
-
if (item.details.type !== ToolConfirmationType.Execute) {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const command = item.details.fullCommand;
|
|
72
|
-
return (
|
|
73
|
-
command.includes(outputPath) &&
|
|
74
|
-
command.includes(tempDir) &&
|
|
75
|
-
!command.includes('sudo') &&
|
|
76
|
-
!command.includes('rm -rf /')
|
|
77
|
-
);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
return onlyAllowedCreate || onlyAllowedApplyPatch || onlyAllowedExecute
|
|
81
|
-
? ToolConfirmationOutcome.ProceedOnce
|
|
82
|
-
: ToolConfirmationOutcome.Cancel;
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
for await (const msg of stream) {
|
|
87
|
-
switch (msg.type) {
|
|
88
|
-
case DroidMessageType.AssistantTextDelta:
|
|
89
|
-
process.stdout.write(msg.text);
|
|
90
|
-
break;
|
|
91
|
-
case DroidMessageType.ToolUse:
|
|
92
|
-
console.log(`\n[Tool] ${msg.toolName}`);
|
|
93
|
-
break;
|
|
94
|
-
case DroidMessageType.ToolResult:
|
|
95
|
-
if (msg.isError) {
|
|
96
|
-
toolError = JSON.stringify(msg.content);
|
|
97
|
-
}
|
|
98
|
-
console.log(`[Tool Result] ${msg.isError ? 'Error' : 'OK'}`);
|
|
99
|
-
break;
|
|
100
|
-
case DroidMessageType.TurnComplete:
|
|
101
|
-
console.log('\n\n--- Turn complete ---');
|
|
102
|
-
if (msg.tokenUsage) {
|
|
103
|
-
console.log(
|
|
104
|
-
`Tokens — input: ${msg.tokenUsage.inputTokens}, output: ${msg.tokenUsage.outputTokens}`
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (toolError) {
|
|
112
|
-
throw new Error(
|
|
113
|
-
`Tool failed during spec-mode implementation: ${toolError}`
|
|
114
|
-
);
|
|
60
|
+
for await (const _msg of session.stream(
|
|
61
|
+
`Plan then create ${outputPath} containing "Hello from Droid".`
|
|
62
|
+
)) {
|
|
63
|
+
// Consume the stream until implementation finishes.
|
|
115
64
|
}
|
|
116
|
-
|
|
117
|
-
const content = await readFile(outputPath, 'utf8');
|
|
118
|
-
if (content.trim() !== EXPECTED_CONTENT) {
|
|
119
|
-
throw new Error(
|
|
120
|
-
`Expected ${outputPath} to contain ${JSON.stringify(EXPECTED_CONTENT)}, got ${JSON.stringify(content)}`
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
console.log(`\nVerified ${outputPath}.`);
|
|
125
65
|
} finally {
|
|
126
|
-
await
|
|
66
|
+
await session.close();
|
|
127
67
|
}
|
|
128
|
-
}
|
|
129
68
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
69
|
+
console.log(await readFile(outputPath, 'utf8'));
|
|
70
|
+
} finally {
|
|
71
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
72
|
+
}
|