@factory/droid-sdk 0.7.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.
- package/dist/{chunk-C3KHERVH.js → chunk-4G2RKM2H.js} +4976 -3628
- package/dist/chunk-4G2RKM2H.js.map +1 -0
- package/dist/{chunk-5UXINOXG.mjs → chunk-ROFAQUG4.mjs} +7574 -6226
- package/dist/chunk-ROFAQUG4.mjs.map +1 -0
- package/dist/{index-D_SzTnFR.d.mts → index-_nUPGHpH.d.mts} +16625 -5933
- package/dist/{index-D_SzTnFR.d.ts → index-_nUPGHpH.d.ts} +16625 -5933
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9 -1
- package/dist/node.d.mts +24 -3
- package/dist/node.d.ts +24 -3
- package/dist/node.js +504 -374
- package/dist/node.js.map +1 -1
- package/dist/node.mjs +153 -23
- package/dist/node.mjs.map +1 -1
- package/docs/typescript-sdk-reference.md +903 -726
- package/examples/browser/daemon-model-discovery.ts +47 -0
- package/examples/browser/index.html +2 -1
- package/examples/browser/list-sessions.ts +1 -3
- package/examples/node/abort-session-stream.ts +2 -0
- package/examples/node/ask-user-elicitation.ts +11 -7
- package/examples/node/compact-session.ts +33 -18
- package/examples/node/custom-system-prompt.ts +35 -0
- package/examples/node/document-input.ts +58 -0
- package/examples/node/enter-spec-mode.ts +50 -29
- package/examples/node/external-mcp.ts +45 -0
- package/examples/node/fork-session.ts +21 -10
- package/examples/node/hook-execution.ts +37 -37
- package/examples/node/image-attachment.ts +8 -2
- package/examples/node/interrupt-session.ts +27 -8
- package/examples/node/model-discovery.ts +28 -0
- package/examples/node/model-selection.ts +35 -0
- package/examples/node/multi-turn-session.ts +4 -0
- package/examples/node/observability.ts +4 -17
- package/examples/node/partial-stream.ts +56 -0
- package/examples/node/permission-handler.ts +9 -4
- package/examples/node/rename-session.ts +7 -3
- package/examples/node/result-metadata.ts +10 -1
- package/examples/node/resume-session.ts +41 -0
- package/examples/node/rewind-session.ts +19 -9
- package/examples/node/run.ts +14 -7
- package/examples/node/sdk-mcp-tool.ts +10 -8
- package/examples/node/session-settings.ts +9 -9
- package/examples/node/session-stream.ts +17 -2
- package/examples/node/skills.ts +24 -0
- package/examples/node/spec-mode-new-session.ts +38 -10
- package/examples/node/spec-mode-same-session.ts +37 -3
- package/examples/node/structured-output.ts +30 -7
- package/package.json +1 -1
- package/dist/chunk-5UXINOXG.mjs.map +0 -1
- package/dist/chunk-C3KHERVH.js.map +0 -1
|
@@ -9,28 +9,47 @@
|
|
|
9
9
|
* npx tsx examples/node/interrupt-session.ts
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
type DroidResultMessage,
|
|
14
|
+
createSession,
|
|
15
|
+
DroidMessageType,
|
|
16
|
+
} from '@factory/droid-sdk/node';
|
|
13
17
|
|
|
14
18
|
const session = await createSession();
|
|
15
19
|
let deltaCount = 0;
|
|
20
|
+
let result: DroidResultMessage | undefined;
|
|
16
21
|
|
|
17
22
|
try {
|
|
18
23
|
for await (const msg of session.stream('Write a long history of computing.', {
|
|
19
24
|
includePartialMessages: true,
|
|
20
25
|
})) {
|
|
21
|
-
if (msg.type
|
|
22
|
-
|
|
26
|
+
if (msg.type === DroidMessageType.Result) {
|
|
27
|
+
result = msg;
|
|
28
|
+
if (!msg.success && !msg.interrupted) {
|
|
29
|
+
throw new Error(msg.error?.message ?? `Turn ended: ${msg.subtype}`);
|
|
30
|
+
}
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
if (msg.type === DroidMessageType.AssistantTextDelta) {
|
|
34
|
+
process.stdout.write(msg.text);
|
|
35
|
+
deltaCount++;
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
if (deltaCount === 5) {
|
|
38
|
+
await session.interrupt();
|
|
39
|
+
}
|
|
30
40
|
}
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
|
|
43
|
+
if (!result) {
|
|
44
|
+
throw new Error('The stream ended without a result.');
|
|
45
|
+
}
|
|
46
|
+
if (result.interrupted) {
|
|
47
|
+
console.log('\nTurn was interrupted.');
|
|
48
|
+
} else if (deltaCount >= 5) {
|
|
49
|
+
console.log('\nThe turn completed before the interrupt took effect.');
|
|
50
|
+
} else {
|
|
51
|
+
console.log('\nThe turn completed before an interrupt was requested.');
|
|
52
|
+
}
|
|
34
53
|
} finally {
|
|
35
54
|
await session.close();
|
|
36
55
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node model discovery example.
|
|
3
|
+
*
|
|
4
|
+
* Lists the models available to the current account and organization.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* FACTORY_API_KEY=... npx tsx examples/node/model-discovery.ts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { listModels } from '@factory/droid-sdk/node';
|
|
11
|
+
|
|
12
|
+
const apiKey = process.env.FACTORY_API_KEY;
|
|
13
|
+
if (!apiKey) {
|
|
14
|
+
throw new Error('Set FACTORY_API_KEY.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const models = await listModels({ apiKey, includeDisabled: true });
|
|
18
|
+
|
|
19
|
+
for (const model of models) {
|
|
20
|
+
const status = model.disabled
|
|
21
|
+
? `disabled: ${model.disabledReason}`
|
|
22
|
+
: 'available';
|
|
23
|
+
console.log(
|
|
24
|
+
`${model.displayName} (${model.id})\n` +
|
|
25
|
+
` status: ${status}\n` +
|
|
26
|
+
` reasoning: ${model.supportedReasoningEfforts.join(', ')}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model selection example.
|
|
3
|
+
*
|
|
4
|
+
* Runs one turn with a configured model ID.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* FACTORY_API_KEY=... npx tsx examples/node/model-selection.ts model-id
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createSession, DroidMessageType } from '@factory/droid-sdk/node';
|
|
11
|
+
|
|
12
|
+
const modelId = process.argv[2];
|
|
13
|
+
if (!modelId) {
|
|
14
|
+
throw new Error('Pass a configured model ID.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const session = await createSession({
|
|
18
|
+
cwd: process.cwd(),
|
|
19
|
+
modelId,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
for await (const message of session.stream('Summarize this repository.')) {
|
|
24
|
+
if (message.type === DroidMessageType.Assistant) {
|
|
25
|
+
console.log(message.text);
|
|
26
|
+
}
|
|
27
|
+
if (message.type === DroidMessageType.Result && !message.success) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
message.error?.message ?? `Turn ended: ${message.subtype}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} finally {
|
|
34
|
+
await session.close();
|
|
35
|
+
}
|
|
@@ -19,7 +19,11 @@ async function streamText(
|
|
|
19
19
|
if (msg.type === DroidMessageType.Assistant) {
|
|
20
20
|
text += msg.text;
|
|
21
21
|
}
|
|
22
|
+
if (msg.type === DroidMessageType.Result && !msg.success) {
|
|
23
|
+
throw new Error(msg.error?.message ?? `Turn ended: ${msg.subtype}`);
|
|
24
|
+
}
|
|
22
25
|
}
|
|
26
|
+
if (!text) throw new Error('The turn produced no assistant text.');
|
|
23
27
|
return text;
|
|
24
28
|
}
|
|
25
29
|
|
|
@@ -2,14 +2,7 @@
|
|
|
2
2
|
* Observability example.
|
|
3
3
|
*
|
|
4
4
|
* Passes a `DroidObservability` bundle (logger, metrics, tracing) to
|
|
5
|
-
* `createSession()`,
|
|
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.
|
|
5
|
+
* `createSession()`, then runs one turn so the sinks observe real traffic.
|
|
13
6
|
*
|
|
14
7
|
* Sinks only ever receive structured, privacy-safe values — never raw
|
|
15
8
|
* stdout/stderr or message content.
|
|
@@ -60,10 +53,6 @@ const observability: DroidObservability = {
|
|
|
60
53
|
|
|
61
54
|
const session = await createSession({ observability });
|
|
62
55
|
|
|
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
56
|
const logCountBeforeTurn = logCount;
|
|
68
57
|
let turnLogCount = 0;
|
|
69
58
|
|
|
@@ -71,13 +60,11 @@ try {
|
|
|
71
60
|
for await (const msg of session.stream(
|
|
72
61
|
'Reply with the single word: ready.'
|
|
73
62
|
)) {
|
|
74
|
-
if (msg.type === DroidMessageType.
|
|
75
|
-
|
|
63
|
+
if (msg.type === DroidMessageType.Result && !msg.success) {
|
|
64
|
+
throw new Error(msg.error?.message ?? `Turn ended: ${msg.subtype}`);
|
|
76
65
|
}
|
|
77
66
|
}
|
|
78
|
-
// Measure before
|
|
79
|
-
// which is itself logged, so counting after the `finally` would always find
|
|
80
|
-
// at least one log regardless of what the turn did.
|
|
67
|
+
// Measure before close(), which emits another request log.
|
|
81
68
|
turnLogCount = logCount - logCountBeforeTurn;
|
|
82
69
|
} finally {
|
|
83
70
|
await session.close();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partial stream example.
|
|
3
|
+
*
|
|
4
|
+
* Prints assistant text as it arrives, tool progress, and cumulative session
|
|
5
|
+
* token updates.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx tsx examples/node/partial-stream.ts
|
|
9
|
+
* npx tsx examples/node/partial-stream.ts "Explain this repository."
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { DroidMessageType, createSession } from '@factory/droid-sdk/node';
|
|
13
|
+
|
|
14
|
+
const session = await createSession();
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const prompt =
|
|
18
|
+
process.argv.slice(2).join(' ') || 'Explain this repository briefly.';
|
|
19
|
+
let sawSuccessfulResult = false;
|
|
20
|
+
|
|
21
|
+
for await (const event of session.stream(prompt, {
|
|
22
|
+
includePartialMessages: true,
|
|
23
|
+
})) {
|
|
24
|
+
switch (event.type) {
|
|
25
|
+
case DroidMessageType.AssistantTextDelta:
|
|
26
|
+
process.stdout.write(event.text);
|
|
27
|
+
break;
|
|
28
|
+
|
|
29
|
+
case DroidMessageType.ToolProgress:
|
|
30
|
+
console.log(`\n[tool progress] ${event.toolUseId}`);
|
|
31
|
+
break;
|
|
32
|
+
|
|
33
|
+
case DroidMessageType.TokenUsageUpdate:
|
|
34
|
+
console.log(
|
|
35
|
+
`\n[session tokens] ${event.inputTokens} in, ${event.outputTokens} out`
|
|
36
|
+
);
|
|
37
|
+
break;
|
|
38
|
+
|
|
39
|
+
case DroidMessageType.Result:
|
|
40
|
+
if (!event.success) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
event.error?.message ?? `Turn ended: ${event.subtype}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
sawSuccessfulResult = true;
|
|
46
|
+
console.log(`\n[result] ${event.subtype}`);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!sawSuccessfulResult) {
|
|
52
|
+
throw new Error('The stream ended without a successful result.');
|
|
53
|
+
}
|
|
54
|
+
} finally {
|
|
55
|
+
await session.close();
|
|
56
|
+
}
|
|
@@ -25,19 +25,24 @@ try {
|
|
|
25
25
|
`Create ${outputPath} with the text "Hello, World!"`,
|
|
26
26
|
{
|
|
27
27
|
autonomyLevel: AutonomyLevel.Off,
|
|
28
|
-
permissionHandler(
|
|
28
|
+
permissionHandler({ toolUses, options }) {
|
|
29
29
|
handledPermission = true;
|
|
30
30
|
const onlyCreatesExpectedFile =
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
toolUses.length > 0 &&
|
|
32
|
+
toolUses.every(
|
|
33
33
|
(item) =>
|
|
34
34
|
item.details.type === ToolConfirmationType.Create &&
|
|
35
35
|
item.details.filePath === outputPath
|
|
36
36
|
);
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
const desired = onlyCreatesExpectedFile
|
|
39
39
|
? ToolConfirmationOutcome.ProceedOnce
|
|
40
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;
|
|
41
46
|
},
|
|
42
47
|
}
|
|
43
48
|
);
|
|
@@ -9,14 +9,18 @@
|
|
|
9
9
|
* npx tsx examples/node/rename-session.ts
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { createSession } from '@factory/droid-sdk/node';
|
|
12
|
+
import { DroidMessageType, createSession } from '@factory/droid-sdk/node';
|
|
13
13
|
|
|
14
14
|
const session = await createSession();
|
|
15
15
|
|
|
16
16
|
try {
|
|
17
17
|
// A session needs at least one turn before it has a title to replace.
|
|
18
|
-
for await (const
|
|
19
|
-
|
|
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
|
+
}
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
await session.rename({
|
|
@@ -29,6 +29,15 @@ if (result.error) {
|
|
|
29
29
|
|
|
30
30
|
if (result.tokenUsage) {
|
|
31
31
|
console.log(
|
|
32
|
-
`tokens:
|
|
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}).`
|
|
33
42
|
);
|
|
34
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
|
+
}
|
|
@@ -15,8 +15,9 @@ const session = await createSession();
|
|
|
15
15
|
async function ask(
|
|
16
16
|
activeSession: Awaited<ReturnType<typeof createSession>>,
|
|
17
17
|
prompt: string
|
|
18
|
-
): Promise<string> {
|
|
18
|
+
): Promise<{ messageId: string; text: string }> {
|
|
19
19
|
let messageId: string | undefined;
|
|
20
|
+
let text = '';
|
|
20
21
|
|
|
21
22
|
for await (const message of activeSession.stream(prompt)) {
|
|
22
23
|
if (
|
|
@@ -26,17 +27,24 @@ async function ask(
|
|
|
26
27
|
messageId ??= message.message.id;
|
|
27
28
|
}
|
|
28
29
|
if (message.type === DroidMessageType.Assistant) {
|
|
30
|
+
text += message.text;
|
|
29
31
|
console.log(message.text);
|
|
30
32
|
}
|
|
33
|
+
if (message.type === DroidMessageType.Result && !message.success) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
message.error?.message ?? `Turn ended: ${message.subtype}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
if (!messageId) throw new Error('The turn produced no user message ID.');
|
|
34
|
-
|
|
41
|
+
if (!text.trim()) throw new Error('The turn produced no assistant output.');
|
|
42
|
+
return { messageId, text };
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
try {
|
|
38
46
|
await ask(session, 'Remember this phrase exactly: mango sunrise');
|
|
39
|
-
const secondTurn = await ask(
|
|
47
|
+
const { messageId: secondTurn } = await ask(
|
|
40
48
|
session,
|
|
41
49
|
'Now forget it and remember: cobalt harbor'
|
|
42
50
|
);
|
|
@@ -56,13 +64,15 @@ try {
|
|
|
56
64
|
forkTitle: 'Before the second phrase',
|
|
57
65
|
});
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
await ask(successor, 'What phrase did I ask you to remember?');
|
|
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}`);
|
|
64
71
|
|
|
65
|
-
|
|
72
|
+
await ask(successor, 'What phrase did I ask you to remember?');
|
|
73
|
+
} finally {
|
|
74
|
+
await successor.close();
|
|
75
|
+
}
|
|
66
76
|
} finally {
|
|
67
77
|
// Closes the active source, or does nothing if it was retired.
|
|
68
78
|
await session.close();
|
package/examples/node/run.ts
CHANGED
|
@@ -11,15 +11,22 @@
|
|
|
11
11
|
|
|
12
12
|
import { run } from '@factory/droid-sdk/node';
|
|
13
13
|
|
|
14
|
-
const
|
|
15
|
-
process.argv.slice(2).join(' ') || 'What is 2 + 2?'
|
|
16
|
-
);
|
|
14
|
+
const result = await run(process.argv.slice(2).join(' ') || 'What is 2 + 2?');
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
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}`);
|
|
20
27
|
|
|
21
|
-
if (tokenUsage) {
|
|
28
|
+
if (result.tokenUsage) {
|
|
22
29
|
console.log(
|
|
23
|
-
`tokens: ${tokenUsage.inputTokens} in, ${tokenUsage.outputTokens} out`
|
|
30
|
+
`tokens: ${result.tokenUsage.inputTokens} in, ${result.tokenUsage.outputTokens} out`
|
|
24
31
|
);
|
|
25
32
|
}
|
|
@@ -32,21 +32,23 @@ const sdkTools = createSdkMcpServer({
|
|
|
32
32
|
|
|
33
33
|
const session = await createSession({
|
|
34
34
|
mcpServers: [sdkTools],
|
|
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.
|
|
38
35
|
permissionHandler: () => ToolConfirmationOutcome.ProceedOnce,
|
|
39
36
|
});
|
|
40
37
|
|
|
41
38
|
try {
|
|
42
|
-
for await (const
|
|
39
|
+
for await (const message of session.stream(
|
|
43
40
|
'Use the favorite_number tool for Ada and tell me the answer.'
|
|
44
41
|
)) {
|
|
45
|
-
if (
|
|
46
|
-
console.log(`[tool call] ${
|
|
42
|
+
if (message.type === DroidMessageType.ToolCall) {
|
|
43
|
+
console.log(`[tool call] ${message.name}`);
|
|
47
44
|
}
|
|
48
|
-
if (
|
|
49
|
-
console.log(
|
|
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
|
+
);
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
} finally {
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session settings example.
|
|
3
3
|
*
|
|
4
|
-
* Reads settings from a live session, updates them
|
|
5
|
-
* and
|
|
4
|
+
* Reads settings from a live session, updates them, and changes between Auto
|
|
5
|
+
* and Spec modes.
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
8
|
* npx tsx examples/node/session-settings.ts
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
AutonomyLevel,
|
|
13
|
-
DroidInteractionMode,
|
|
14
|
-
createSession,
|
|
15
|
-
} from '@factory/droid-sdk/node';
|
|
11
|
+
import { AutonomyLevel, createSession } from '@factory/droid-sdk/node';
|
|
16
12
|
|
|
17
13
|
const session = await createSession({ autonomyLevel: AutonomyLevel.Low });
|
|
18
14
|
|
|
@@ -25,11 +21,15 @@ try {
|
|
|
25
21
|
printSettings('created with');
|
|
26
22
|
|
|
27
23
|
await session.updateSettings({
|
|
28
|
-
interactionMode: DroidInteractionMode.Spec,
|
|
29
24
|
autonomyLevel: AutonomyLevel.High,
|
|
30
25
|
});
|
|
31
|
-
|
|
32
26
|
printSettings('updated to ');
|
|
27
|
+
|
|
28
|
+
await session.enterSpecMode();
|
|
29
|
+
printSettings('entered Spec');
|
|
30
|
+
|
|
31
|
+
await session.exitSpecMode();
|
|
32
|
+
printSettings('exited Spec ');
|
|
33
33
|
} finally {
|
|
34
34
|
await session.close();
|
|
35
35
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session streaming example.
|
|
3
3
|
*
|
|
4
|
-
* Sends a prompt with `session.stream()` and handles
|
|
5
|
-
*
|
|
4
|
+
* Sends a prompt with `session.stream()` and handles every message type
|
|
5
|
+
* emitted by a default (non-partial) stream.
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
8
|
* npx tsx examples/node/session-stream.ts
|
|
@@ -22,6 +22,10 @@ try {
|
|
|
22
22
|
process.stdout.write(msg.text);
|
|
23
23
|
break;
|
|
24
24
|
|
|
25
|
+
case DroidMessageType.User:
|
|
26
|
+
console.log('\n[user message accepted]');
|
|
27
|
+
break;
|
|
28
|
+
|
|
25
29
|
case DroidMessageType.ToolCall:
|
|
26
30
|
console.log(`\n[tool] ${msg.name}`);
|
|
27
31
|
break;
|
|
@@ -30,7 +34,18 @@ try {
|
|
|
30
34
|
console.log(`[tool result] ${msg.isError ? 'error' : 'ok'}`);
|
|
31
35
|
break;
|
|
32
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
|
+
|
|
33
45
|
case DroidMessageType.Result:
|
|
46
|
+
if (!msg.success) {
|
|
47
|
+
throw new Error(msg.error?.message ?? `Turn ended: ${msg.subtype}`);
|
|
48
|
+
}
|
|
34
49
|
if (msg.tokenUsage) {
|
|
35
50
|
console.log(
|
|
36
51
|
`\n\ntokens: ${msg.tokenUsage.inputTokens} in, ${msg.tokenUsage.outputTokens} out`
|
|
@@ -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
|
+
}
|
|
@@ -22,7 +22,9 @@ import { tmpdir } from 'node:os';
|
|
|
22
22
|
import { join } from 'node:path';
|
|
23
23
|
|
|
24
24
|
import {
|
|
25
|
+
type ClientPermissionHandler,
|
|
25
26
|
DroidInteractionMode,
|
|
27
|
+
DroidMessageType,
|
|
26
28
|
ReasoningEffort,
|
|
27
29
|
SessionNotificationSchema,
|
|
28
30
|
ToolConfirmationOutcome,
|
|
@@ -35,18 +37,40 @@ const outputPath = join(tempDir, 'hello.txt');
|
|
|
35
37
|
|
|
36
38
|
let handoffSessionId: string | null = null;
|
|
37
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
|
+
|
|
38
69
|
try {
|
|
39
70
|
const session = await createSession({
|
|
40
71
|
interactionMode: DroidInteractionMode.Spec,
|
|
41
72
|
specModeReasoningEffort: ReasoningEffort.High,
|
|
42
|
-
permissionHandler
|
|
43
|
-
const canExitSpec = params.toolUses.some(
|
|
44
|
-
(item) => item.details.type === ToolConfirmationType.ExitSpecMode
|
|
45
|
-
);
|
|
46
|
-
return canExitSpec
|
|
47
|
-
? ToolConfirmationOutcome.ProceedNewSessionHigh
|
|
48
|
-
: ToolConfirmationOutcome.ProceedOnce;
|
|
49
|
-
},
|
|
73
|
+
permissionHandler: handlePermissionRequest,
|
|
50
74
|
});
|
|
51
75
|
|
|
52
76
|
// The typed message stream carries no per-message session id, and the
|
|
@@ -63,10 +87,14 @@ try {
|
|
|
63
87
|
});
|
|
64
88
|
|
|
65
89
|
try {
|
|
66
|
-
for await (const
|
|
90
|
+
for await (const message of session.stream(
|
|
67
91
|
`Plan then create ${outputPath} containing "Hello from Droid".`
|
|
68
92
|
)) {
|
|
69
|
-
|
|
93
|
+
if (message.type === DroidMessageType.Result && !message.success) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
message.error?.message ?? `Turn ended: ${message.subtype}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
70
98
|
}
|
|
71
99
|
} finally {
|
|
72
100
|
unsubscribe();
|