@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,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forks, compacts, and rewinds a daemon session.
|
|
3
|
+
*
|
|
4
|
+
* Daemon operations return a new session ID. The source session stays usable,
|
|
5
|
+
* and the successor must be resumed explicitly.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npm run build
|
|
9
|
+
* FACTORY_API_KEY=... DROID_SDK_EXAMPLE_CWD=/repo npm run serve:browser-example
|
|
10
|
+
* open http://127.0.0.1:8420/?example=daemon-lifecycle
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
connectToDaemon,
|
|
15
|
+
DroidMessageType,
|
|
16
|
+
type ConnectedDroidSession,
|
|
17
|
+
} from '@factory/droid-sdk';
|
|
18
|
+
|
|
19
|
+
const { daemonUrl, apiKey, cwd } = (
|
|
20
|
+
globalThis as typeof globalThis & {
|
|
21
|
+
__DROID_SDK_EXAMPLE_CONFIG__: {
|
|
22
|
+
daemonUrl: string;
|
|
23
|
+
apiKey: string;
|
|
24
|
+
cwd: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
).__DROID_SDK_EXAMPLE_CONFIG__;
|
|
28
|
+
if (!apiKey || !cwd) {
|
|
29
|
+
throw new Error('Set FACTORY_API_KEY and DROID_SDK_EXAMPLE_CWD.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const droid = await connectToDaemon({
|
|
33
|
+
url: daemonUrl,
|
|
34
|
+
auth: { apiKey },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
async function send(
|
|
38
|
+
session: ConnectedDroidSession,
|
|
39
|
+
prompt: string
|
|
40
|
+
): Promise<string> {
|
|
41
|
+
let messageId: string | undefined;
|
|
42
|
+
|
|
43
|
+
for await (const message of session.stream(prompt)) {
|
|
44
|
+
if (
|
|
45
|
+
message.type === DroidMessageType.User &&
|
|
46
|
+
message.message.visibility !== 'llm_only'
|
|
47
|
+
) {
|
|
48
|
+
messageId ??= message.message.id;
|
|
49
|
+
}
|
|
50
|
+
if (message.type === DroidMessageType.Assistant) {
|
|
51
|
+
console.log(message.text);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!messageId) throw new Error('The turn produced no user message ID.');
|
|
56
|
+
return messageId;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const sessions: ConnectedDroidSession[] = [];
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const source = await droid.sessions.create({ cwd });
|
|
63
|
+
sessions.push(source);
|
|
64
|
+
|
|
65
|
+
await send(source, 'Remember this phrase exactly: mango sunrise');
|
|
66
|
+
const secondTurn = await send(
|
|
67
|
+
source,
|
|
68
|
+
'Now forget it and remember: cobalt harbor'
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const { newSessionId: forkedId } = await source.fork();
|
|
72
|
+
const forked = await droid.sessions.resume(forkedId);
|
|
73
|
+
sessions.push(forked);
|
|
74
|
+
console.log(`Forked session: ${forked.id}`);
|
|
75
|
+
|
|
76
|
+
const { newSessionId: compactedId } = await source.compact();
|
|
77
|
+
const compacted = await droid.sessions.resume(compactedId);
|
|
78
|
+
sessions.push(compacted);
|
|
79
|
+
console.log(`Compacted session: ${compacted.id}`);
|
|
80
|
+
|
|
81
|
+
const { newSessionId: rewoundId } = await source.rewind({
|
|
82
|
+
messageId: secondTurn,
|
|
83
|
+
filesToRestore: [],
|
|
84
|
+
filesToDelete: [],
|
|
85
|
+
forkTitle: 'Before the second phrase',
|
|
86
|
+
});
|
|
87
|
+
const rewound = await droid.sessions.resume(rewoundId);
|
|
88
|
+
sessions.push(rewound);
|
|
89
|
+
console.log(`Rewound session: ${rewound.id}`);
|
|
90
|
+
|
|
91
|
+
await send(source, 'Say "source still works" and nothing else.');
|
|
92
|
+
} finally {
|
|
93
|
+
await Promise.allSettled(sessions.map((session) => session.close()));
|
|
94
|
+
droid.disconnect();
|
|
95
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates two sessions concurrently over one daemon connection.
|
|
3
|
+
*
|
|
4
|
+
* Runs in a real browser (no `process`, no Node builtins). From a source
|
|
5
|
+
* checkout of this repository:
|
|
6
|
+
* npm run build
|
|
7
|
+
* FACTORY_API_KEY=... DROID_SDK_EXAMPLE_CWD=/repo npm run serve:browser-example
|
|
8
|
+
* open http://127.0.0.1:8420/?example=daemon-multi-session
|
|
9
|
+
*
|
|
10
|
+
* Never commit a real API key or deploy it in browser code.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
connectToDaemon,
|
|
15
|
+
DroidMessageType,
|
|
16
|
+
type ConnectedDroidSession,
|
|
17
|
+
} from '@factory/droid-sdk';
|
|
18
|
+
|
|
19
|
+
const { daemonUrl, apiKey, cwd } = (
|
|
20
|
+
globalThis as typeof globalThis & {
|
|
21
|
+
__DROID_SDK_EXAMPLE_CONFIG__: {
|
|
22
|
+
daemonUrl: string;
|
|
23
|
+
apiKey: string;
|
|
24
|
+
cwd: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
).__DROID_SDK_EXAMPLE_CONFIG__;
|
|
28
|
+
if (!apiKey || !cwd) {
|
|
29
|
+
throw new Error('Set FACTORY_API_KEY and DROID_SDK_EXAMPLE_CWD.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const droid = await connectToDaemon({
|
|
33
|
+
url: daemonUrl,
|
|
34
|
+
auth: { apiKey },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
async function runTurn(
|
|
38
|
+
session: ConnectedDroidSession,
|
|
39
|
+
label: string,
|
|
40
|
+
prompt: string
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
for await (const message of session.stream(prompt)) {
|
|
43
|
+
if (message.type === DroidMessageType.Assistant) {
|
|
44
|
+
console.log(`${label}: ${message.text}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const sessions: ConnectedDroidSession[] = [];
|
|
50
|
+
try {
|
|
51
|
+
const first = await droid.sessions.create({ cwd });
|
|
52
|
+
sessions.push(first);
|
|
53
|
+
const second = await droid.sessions.create({ cwd });
|
|
54
|
+
sessions.push(second);
|
|
55
|
+
|
|
56
|
+
console.log(`First session: ${first.id}`);
|
|
57
|
+
console.log(`Second session: ${second.id}`);
|
|
58
|
+
|
|
59
|
+
await Promise.all([
|
|
60
|
+
runTurn(first, 'First', 'Say "one" and nothing else.'),
|
|
61
|
+
runTurn(second, 'Second', 'Say "two" and nothing else.'),
|
|
62
|
+
]);
|
|
63
|
+
} finally {
|
|
64
|
+
await Promise.allSettled(sessions.map((session) => session.close()));
|
|
65
|
+
droid.disconnect();
|
|
66
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon connection from a browser: run a turn and approve its tool use.
|
|
3
|
+
*
|
|
4
|
+
* Waits for the turn to end rather than for the permission request, since a
|
|
5
|
+
* turn can finish without asking.
|
|
6
|
+
*
|
|
7
|
+
* Runs in a real browser (no `process`, no Node builtins). From a source
|
|
8
|
+
* checkout of this repository:
|
|
9
|
+
* npm run build
|
|
10
|
+
* FACTORY_API_KEY=... DROID_SDK_EXAMPLE_CWD=/repo npm run serve:browser-example
|
|
11
|
+
* open http://127.0.0.1:8420/?example=daemon-session
|
|
12
|
+
*
|
|
13
|
+
* Notes:
|
|
14
|
+
* - If your page is served over https://, the browser will block ws:// URLs as
|
|
15
|
+
* mixed content. Use wss:// (or serve the page over http:// for localhost).
|
|
16
|
+
* - Never commit a real API key or deploy it in browser code.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
AutonomyLevel,
|
|
21
|
+
connectToDaemon,
|
|
22
|
+
DroidMessageType,
|
|
23
|
+
ToolConfirmationOutcome,
|
|
24
|
+
type ConnectedDroidSession,
|
|
25
|
+
} from '@factory/droid-sdk';
|
|
26
|
+
|
|
27
|
+
const { daemonUrl, apiKey, cwd } = (
|
|
28
|
+
globalThis as typeof globalThis & {
|
|
29
|
+
__DROID_SDK_EXAMPLE_CONFIG__: {
|
|
30
|
+
daemonUrl: string;
|
|
31
|
+
apiKey: string;
|
|
32
|
+
cwd: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
).__DROID_SDK_EXAMPLE_CONFIG__;
|
|
36
|
+
if (!apiKey || !cwd) {
|
|
37
|
+
throw new Error('Set FACTORY_API_KEY and DROID_SDK_EXAMPLE_CWD.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const droid = await connectToDaemon({
|
|
41
|
+
url: daemonUrl,
|
|
42
|
+
auth: { apiKey },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let approvedPermission = false;
|
|
46
|
+
let session: ConnectedDroidSession | undefined;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
session = await droid.sessions.create({
|
|
50
|
+
cwd,
|
|
51
|
+
// Off ensures even low-risk commands exercise the permission handler.
|
|
52
|
+
autonomyLevel: AutonomyLevel.Off,
|
|
53
|
+
permissionHandler: () => {
|
|
54
|
+
approvedPermission = true;
|
|
55
|
+
return ToolConfirmationOutcome.ProceedOnce;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
for await (const message of session.stream(
|
|
60
|
+
'Run `echo hello from the browser example` and show me the output.'
|
|
61
|
+
)) {
|
|
62
|
+
if (message.type === DroidMessageType.Assistant) {
|
|
63
|
+
console.log(message.text);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!approvedPermission) {
|
|
68
|
+
throw new Error('Turn finished without requesting permission.');
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
await Promise.allSettled(session ? [session.close()] : []);
|
|
72
|
+
droid.disconnect();
|
|
73
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Droid SDK browser examples</title>
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
9
|
+
margin: 2rem;
|
|
10
|
+
line-height: 1.5;
|
|
11
|
+
}
|
|
12
|
+
pre {
|
|
13
|
+
background: #f4f4f5;
|
|
14
|
+
padding: 0.75rem;
|
|
15
|
+
overflow-x: auto;
|
|
16
|
+
white-space: pre-wrap;
|
|
17
|
+
}
|
|
18
|
+
#error:empty {
|
|
19
|
+
display: none;
|
|
20
|
+
}
|
|
21
|
+
#error {
|
|
22
|
+
background: #fee2e2;
|
|
23
|
+
padding: 0.75rem;
|
|
24
|
+
white-space: pre-wrap;
|
|
25
|
+
}
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<h1>Droid SDK browser examples</h1>
|
|
30
|
+
<p>
|
|
31
|
+
Served by <code>npm run serve:browser-example</code>. The example module
|
|
32
|
+
is bundled for the browser on each request, so a reload picks up edits.
|
|
33
|
+
</p>
|
|
34
|
+
<p>
|
|
35
|
+
Set <code>FACTORY_API_KEY</code> and
|
|
36
|
+
<code>DROID_SDK_EXAMPLE_CWD</code> before starting the server, then pick
|
|
37
|
+
one with <code>?example=daemon-session</code> (default),
|
|
38
|
+
<code>?example=daemon-multi-session</code>,
|
|
39
|
+
<code>?example=daemon-lifecycle</code>, or
|
|
40
|
+
<code>?example=list-sessions</code>.
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
<div id="error"></div>
|
|
44
|
+
<div id="output"></div>
|
|
45
|
+
|
|
46
|
+
<script>
|
|
47
|
+
globalThis.__DROID_SDK_EXAMPLE_CONFIG__ =
|
|
48
|
+
__INJECTED_DROID_SDK_EXAMPLE_CONFIG__;
|
|
49
|
+
</script>
|
|
50
|
+
<script type="module">
|
|
51
|
+
// Surface load-time failures (for example a missing config value, which
|
|
52
|
+
// throws during module evaluation) instead of leaving a blank page.
|
|
53
|
+
const errorBox = document.querySelector('#error');
|
|
54
|
+
const showError = (err) => {
|
|
55
|
+
errorBox.textContent = String(
|
|
56
|
+
(err && (err.stack || err.message)) || err
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
window.addEventListener('error', (e) => showError(e.error ?? e.message));
|
|
60
|
+
window.addEventListener('unhandledrejection', (e) => showError(e.reason));
|
|
61
|
+
|
|
62
|
+
const params = new URLSearchParams(location.search);
|
|
63
|
+
const example = params.get('example') ?? 'daemon-session';
|
|
64
|
+
|
|
65
|
+
import(`./${example}.js${location.search}`).catch(showError);
|
|
66
|
+
</script>
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lists recent daemon sessions and their messages.
|
|
3
|
+
*
|
|
4
|
+
* Runs in a real browser (no `process`, no Node builtins). From a source
|
|
5
|
+
* checkout of this repository:
|
|
6
|
+
* npm run build
|
|
7
|
+
* FACTORY_API_KEY=... npm run serve:browser-example
|
|
8
|
+
* open http://127.0.0.1:8420/?example=list-sessions
|
|
9
|
+
*
|
|
10
|
+
* Never commit a real API key or deploy it in browser code.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { connectToDaemon } from '@factory/droid-sdk';
|
|
14
|
+
|
|
15
|
+
const { daemonUrl, apiKey } = (
|
|
16
|
+
globalThis as typeof globalThis & {
|
|
17
|
+
__DROID_SDK_EXAMPLE_CONFIG__: {
|
|
18
|
+
daemonUrl: string;
|
|
19
|
+
apiKey: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
).__DROID_SDK_EXAMPLE_CONFIG__;
|
|
23
|
+
if (!apiKey) {
|
|
24
|
+
throw new Error('Set FACTORY_API_KEY.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const droid = await connectToDaemon({
|
|
28
|
+
url: daemonUrl,
|
|
29
|
+
auth: { apiKey },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const sessions = await droid.sessions.list({ limit: 10 });
|
|
34
|
+
|
|
35
|
+
if (sessions.length === 0) {
|
|
36
|
+
console.log('No sessions found.');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const session of sessions) {
|
|
40
|
+
const messages = await droid.sessions.getMessages(session.id, {
|
|
41
|
+
limit: 20,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
console.log(
|
|
45
|
+
session.id,
|
|
46
|
+
session.title ?? '(untitled)',
|
|
47
|
+
`${messages.length} messages`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
} finally {
|
|
51
|
+
droid.disconnect();
|
|
52
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Abort a running turn with AbortController.
|
|
3
3
|
*
|
|
4
|
+
* Passing an `abortSignal` to `session.stream()` makes the stream throw when
|
|
5
|
+
* the signal fires. See interrupt-session.ts for the server-side equivalent.
|
|
6
|
+
*
|
|
4
7
|
* Usage:
|
|
5
|
-
* npx tsx examples/abort-session-stream.ts
|
|
8
|
+
* npx tsx examples/node/abort-session-stream.ts
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
|
-
import { createSession } from '@factory/droid-sdk';
|
|
11
|
+
import { createSession } from '@factory/droid-sdk/node';
|
|
9
12
|
|
|
10
|
-
const session = await createSession(
|
|
11
|
-
apiKey: process.env.FACTORY_API_KEY!,
|
|
12
|
-
cwd: process.cwd(),
|
|
13
|
-
});
|
|
13
|
+
const session = await createSession();
|
|
14
14
|
const controller = new AbortController();
|
|
15
15
|
const timeout = setTimeout(
|
|
16
16
|
() => controller.abort(new Error('Stopped by AbortController')),
|
|
@@ -20,11 +20,9 @@ const timeout = setTimeout(
|
|
|
20
20
|
try {
|
|
21
21
|
for await (const _msg of session.stream(
|
|
22
22
|
'Write a long explanation of how compilers work.',
|
|
23
|
-
{
|
|
24
|
-
abortSignal: controller.signal,
|
|
25
|
-
}
|
|
23
|
+
{ abortSignal: controller.signal }
|
|
26
24
|
)) {
|
|
27
|
-
|
|
25
|
+
// Consume the stream until the abort signal cuts it short.
|
|
28
26
|
}
|
|
29
27
|
} catch (error) {
|
|
30
28
|
console.log(error instanceof Error ? error.message : String(error));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AskUser elicitation example.
|
|
3
|
+
*
|
|
4
|
+
* When Droid needs input mid-turn it sends an `askUser` request. `askUserHandler`
|
|
5
|
+
* answers it programmatically; without a handler the request goes unanswered.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx tsx examples/node/ask-user-elicitation.ts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { run } from '@factory/droid-sdk/node';
|
|
12
|
+
|
|
13
|
+
const { text } = await run(
|
|
14
|
+
'Use the AskUser tool to ask me which color I prefer, then reply with the color I picked.',
|
|
15
|
+
{
|
|
16
|
+
askUserHandler(params) {
|
|
17
|
+
for (const question of params.questions) {
|
|
18
|
+
console.log(`[asked] ${question.question}`);
|
|
19
|
+
console.log(`[options] ${question.options.join(', ')}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Answer every question with its first option. A real integration would
|
|
23
|
+
// prompt the user; returning `cancelled: true` declines instead.
|
|
24
|
+
return {
|
|
25
|
+
answers: params.questions.map((question) => ({
|
|
26
|
+
index: question.index,
|
|
27
|
+
question: question.question,
|
|
28
|
+
answer: question.options[0] ?? 'no preference',
|
|
29
|
+
})),
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
console.log(`\n${text}`);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact session example.
|
|
3
|
+
*
|
|
4
|
+
* Builds up history over a few turns, then calls `compact()` to
|
|
5
|
+
* summarize it into a new session.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx tsx examples/node/compact-session.ts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createSession, DroidMessageType } from '@factory/droid-sdk/node';
|
|
12
|
+
|
|
13
|
+
const session = await createSession();
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
for (const prompt of [
|
|
17
|
+
'Say "one" and nothing else',
|
|
18
|
+
'Say "two" and nothing else',
|
|
19
|
+
'Say "three" and nothing else',
|
|
20
|
+
]) {
|
|
21
|
+
for await (const msg of session.stream(prompt)) {
|
|
22
|
+
if (msg.type === DroidMessageType.Assistant) {
|
|
23
|
+
console.log(msg.text);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { session: compacted, removedCount } = await session.compact();
|
|
29
|
+
|
|
30
|
+
// `session` is now a retired wrapper. Its persisted ID can still be passed
|
|
31
|
+
// to resumeSession() later if the uncompacted history is needed again.
|
|
32
|
+
console.log(`\noriginal session: ${session.id}`);
|
|
33
|
+
console.log(`compacted into: ${compacted.id}`);
|
|
34
|
+
console.log(`messages removed: ${removedCount}`);
|
|
35
|
+
|
|
36
|
+
for await (const msg of compacted.stream(
|
|
37
|
+
'Which numbers did I ask you to say?'
|
|
38
|
+
)) {
|
|
39
|
+
if (msg.type === DroidMessageType.Assistant) {
|
|
40
|
+
console.log(`continued: ${msg.text}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await compacted.close();
|
|
45
|
+
} finally {
|
|
46
|
+
// Closes the active source, or does nothing if it was retired.
|
|
47
|
+
await session.close();
|
|
48
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enter spec mode on a live session.
|
|
3
|
+
*
|
|
4
|
+
* The other spec-mode examples start the session in spec mode. This one starts
|
|
5
|
+
* in the default mode and switches mid-session with `enterSpecMode()`, which is
|
|
6
|
+
* what a host app does when the user toggles spec mode on an open conversation.
|
|
7
|
+
*
|
|
8
|
+
* The switch is observable in the confirmations Droid asks for: it proposes a
|
|
9
|
+
* plan (`ExitSpecMode`) before touching the filesystem, instead of going
|
|
10
|
+
* straight to `Create`.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* npx tsx examples/node/enter-spec-mode.ts
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
ToolConfirmationOutcome,
|
|
22
|
+
ToolConfirmationType,
|
|
23
|
+
createSession,
|
|
24
|
+
} from '@factory/droid-sdk/node';
|
|
25
|
+
|
|
26
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-enter-spec-'));
|
|
27
|
+
const outputPath = join(tempDir, 'hello.txt');
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const session = await createSession({
|
|
31
|
+
permissionHandler(params) {
|
|
32
|
+
for (const toolUse of params.toolUses) {
|
|
33
|
+
const label =
|
|
34
|
+
toolUse.details.type === ToolConfirmationType.ExitSpecMode
|
|
35
|
+
? 'plan proposed'
|
|
36
|
+
: 'implementing';
|
|
37
|
+
console.log(`[${label}] ${toolUse.details.type}`);
|
|
38
|
+
}
|
|
39
|
+
// Approving ExitSpecMode accepts the plan and lets implementation run.
|
|
40
|
+
return ToolConfirmationOutcome.ProceedOnce;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
console.log(
|
|
46
|
+
`mode at creation: ${String(session.settings.interactionMode)}`
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
await session.enterSpecMode();
|
|
50
|
+
console.log('switched to spec mode\n');
|
|
51
|
+
|
|
52
|
+
for await (const _msg of session.stream(
|
|
53
|
+
`Create ${outputPath} containing "hello".`
|
|
54
|
+
)) {
|
|
55
|
+
// Consume the stream until the plan is approved and implemented.
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
await session.close();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(
|
|
62
|
+
`\nfile contents: ${JSON.stringify(await readFile(outputPath, 'utf8'))}`
|
|
63
|
+
);
|
|
64
|
+
} finally {
|
|
65
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
66
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fork session example.
|
|
3
|
+
*
|
|
4
|
+
* Establishes context in one session, forks it with `fork()`, then
|
|
5
|
+
* uses the returned fork handle and confirms the copied context carried over.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx tsx examples/node/fork-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
|
+
await streamText(session, 'Remember this phrase exactly: mango sunrise');
|
|
30
|
+
|
|
31
|
+
const fork = await session.fork();
|
|
32
|
+
|
|
33
|
+
// `session` is now a retired wrapper. Its persisted ID can still be passed
|
|
34
|
+
// to resumeSession() later if the original branch is needed again.
|
|
35
|
+
console.log(`original session: ${session.id}`);
|
|
36
|
+
console.log(`forked session: ${fork.id}`);
|
|
37
|
+
console.log(
|
|
38
|
+
`fork recalls: ${await streamText(fork, 'What phrase did I ask you to remember?')}`
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
await fork.close();
|
|
42
|
+
} finally {
|
|
43
|
+
// Closes the active source, or does nothing if it was retired.
|
|
44
|
+
await session.close();
|
|
45
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook execution example.
|
|
3
|
+
*
|
|
4
|
+
* Registers a PreToolUse hook for the `Execute` tool and prints the hook
|
|
5
|
+
* lifecycle messages that arrive in the session stream.
|
|
6
|
+
*
|
|
7
|
+
* The hook lives in a throwaway project directory (`<tempdir>/.factory/hooks.json`)
|
|
8
|
+
* that is passed as the session `cwd` and deleted afterwards, so running this
|
|
9
|
+
* example never writes to your own `~/.factory` configuration. It does not
|
|
10
|
+
* isolate you from it: hook lists are concatenated across settings levels
|
|
11
|
+
* rather than replaced, so your own user-level `PreToolUse` hooks are appended
|
|
12
|
+
* to the one below and fire during this run whenever their matcher matches.
|
|
13
|
+
*
|
|
14
|
+
* `hooksDisabled` resolves first-defined-wins across settings levels, so the
|
|
15
|
+
* config below sets it explicitly rather than leaving it absent. An absent
|
|
16
|
+
* value would inherit a global `"hooksDisabled": true` from `~/.factory` and
|
|
17
|
+
* the hook would never run.
|
|
18
|
+
*
|
|
19
|
+
* See https://docs.factory.ai/cli/configuration/hooks-guide for the `/hooks`
|
|
20
|
+
* slash command, the other events, and security guidance.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* npx tsx examples/node/hook-execution.ts
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
27
|
+
import { tmpdir } from 'node:os';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
DroidMessageType,
|
|
32
|
+
ToolConfirmationOutcome,
|
|
33
|
+
createSession,
|
|
34
|
+
} from '@factory/droid-sdk/node';
|
|
35
|
+
|
|
36
|
+
const hookSettings = {
|
|
37
|
+
hooksDisabled: false,
|
|
38
|
+
PreToolUse: [
|
|
39
|
+
{
|
|
40
|
+
matcher: 'Execute',
|
|
41
|
+
hooks: [{ type: 'command', command: 'echo hook-from-sdk-example' }],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const projectDir = await mkdtemp(join(tmpdir(), 'droid-sdk-hook-project-'));
|
|
47
|
+
|
|
48
|
+
let hookCount = 0;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await mkdir(join(projectDir, '.factory'), { recursive: true });
|
|
52
|
+
await writeFile(
|
|
53
|
+
join(projectDir, '.factory', 'hooks.json'),
|
|
54
|
+
JSON.stringify(hookSettings, null, 2)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const session = await createSession({
|
|
58
|
+
cwd: projectDir,
|
|
59
|
+
permissionHandler: () => ToolConfirmationOutcome.ProceedOnce,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
for await (const msg of session.stream(
|
|
64
|
+
'Run the shell command `echo hello` using the Execute tool.'
|
|
65
|
+
)) {
|
|
66
|
+
if (msg.type !== DroidMessageType.Hook) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
hookCount++;
|
|
71
|
+
if (msg.status === 'started') {
|
|
72
|
+
console.log(`[hook started] ${msg.eventName}: ${msg.command}`);
|
|
73
|
+
} else {
|
|
74
|
+
console.log(
|
|
75
|
+
`[hook ${msg.status}] exit code ${String(msg.exitCode)}: ${(msg.stdout ?? '').trim()}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
await session.close();
|
|
81
|
+
}
|
|
82
|
+
} finally {
|
|
83
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A silent zero-hook run would look identical to a passing one, so fail loudly.
|
|
87
|
+
if (hookCount === 0) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
'No hook messages arrived: the registered PreToolUse hook never ran. ' +
|
|
90
|
+
'The most likely cause is that the droid answered this turn without ' +
|
|
91
|
+
'calling the Execute tool, so the `Execute` matcher never fired. ' +
|
|
92
|
+
'Re-run, or widen the matcher to the tool the droid actually used.'
|
|
93
|
+
);
|
|
94
|
+
}
|