@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.
- package/README.md +35 -482
- package/dist/chunk-5UXINOXG.mjs +24483 -0
- package/dist/chunk-5UXINOXG.mjs.map +1 -0
- package/dist/chunk-C3KHERVH.js +24483 -0
- package/dist/chunk-C3KHERVH.js.map +1 -0
- package/dist/{index-CwXX5IUH.d.cts → index-D_SzTnFR.d.mts} +83826 -81547
- package/dist/{index-CwXX5IUH.d.ts → index-D_SzTnFR.d.ts} +83826 -81547
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -93103
- package/dist/index.js +821 -11745
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +822 -0
- package/dist/index.mjs.map +1 -0
- package/dist/node.d.mts +736 -0
- package/dist/node.d.ts +736 -0
- package/dist/node.js +4601 -0
- package/dist/node.js.map +1 -0
- package/dist/node.mjs +4601 -0
- package/dist/node.mjs.map +1 -0
- package/docs/typescript-sdk-reference.md +1260 -0
- package/examples/browser/daemon-lifecycle.ts +95 -0
- package/examples/browser/daemon-multi-session.ts +66 -0
- package/examples/browser/daemon-session.ts +73 -0
- package/examples/browser/index.html +68 -0
- package/examples/browser/list-sessions.ts +52 -0
- package/examples/{abort-session-stream.ts → node/abort-session-stream.ts} +8 -10
- package/examples/node/ask-user-elicitation.ts +35 -0
- package/examples/node/compact-session.ts +48 -0
- package/examples/node/enter-spec-mode.ts +66 -0
- package/examples/node/fork-session.ts +45 -0
- package/examples/node/hook-execution.ts +94 -0
- package/examples/node/image-attachment.ts +35 -0
- package/examples/node/init-metadata.ts +24 -0
- package/examples/{interrupt-session.ts → node/interrupt-session.ts} +7 -6
- package/examples/node/list-sessions.ts +20 -0
- package/examples/node/multi-turn-session.ts +33 -0
- package/examples/node/observability.ts +88 -0
- package/examples/node/permission-handler.ts +55 -0
- package/examples/node/rename-session.ts +29 -0
- package/examples/node/result-metadata.ts +34 -0
- package/examples/node/rewind-session.ts +69 -0
- package/examples/node/run.ts +25 -0
- package/examples/{sdk-mcp-tool.ts → node/sdk-mcp-tool.ts} +22 -12
- package/examples/node/session-settings.ts +35 -0
- package/examples/node/session-stream.ts +44 -0
- package/examples/node/spec-mode-new-session.ts +92 -0
- package/examples/node/spec-mode-same-session.ts +85 -0
- package/examples/node/structured-output.ts +73 -0
- package/examples/node/tool-controls.ts +26 -0
- package/examples/package.json +3 -0
- package/package.json +20 -42
- package/dist/index.cjs +0 -12135
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -93103
- package/dist/protocol/index.cjs +0 -6466
- package/dist/protocol/index.cjs.map +0 -1
- package/dist/protocol/index.d.cts +0 -2
- package/dist/protocol/index.d.ts +0 -2
- package/dist/protocol/index.js +0 -6078
- package/dist/protocol/index.js.map +0 -1
- package/docs/daemon-usage-guide.md +0 -544
- package/docs/sdk-usage-guide.md +0 -748
- package/examples/daemon-multi-session.ts +0 -67
- package/examples/droid-dev-structured-output.ts +0 -44
- package/examples/fork-session.ts +0 -68
- package/examples/hook-execution.ts +0 -68
- package/examples/init-metadata.ts +0 -24
- package/examples/list-sessions.ts +0 -19
- package/examples/multi-turn-session.ts +0 -40
- package/examples/permission-handler.ts +0 -41
- package/examples/readme-structured-output.ts +0 -26
- package/examples/result-metadata.ts +0 -63
- package/examples/run.ts +0 -39
- package/examples/session-stream.ts +0 -57
- package/examples/spec-mode-new-session.ts +0 -61
- package/examples/spec-mode-same-session.ts +0 -72
- package/examples/structured-output.ts +0 -60
- package/examples/test-compact.ts +0 -65
- package/examples/test-rewind.ts +0 -87
- 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
|
-
|
|
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.
|
|
38
|
-
|
|
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
|
+
}
|