Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 62 additions & 10 deletions examples/daemon-multi-session.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
/**
* Daemon: multiple concurrent sessions.
*
* Connects to a local daemon, creates two sessions in separate /tmp
* directories, and runs them concurrently over a single WebSocket
* connection.
* Connects to a local daemon, creates two sessions in separate temporary
* directories, and runs them concurrently over a single WebSocket connection.
*
* Usage:
* npx tsx examples/daemon-multi-session.ts
Expand All @@ -13,7 +12,31 @@
* example skips itself when the env var is unset.
*/

import { connectDaemon, DroidMessageType } from '@factory/droid-sdk';
import { mkdtemp, mkdir, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import {
connectDaemon,
DroidMessageType,
ToolConfirmationOutcome,
ToolConfirmationType,
type ToolConfirmationDetails,
} from '@factory/droid-sdk';

function canWriteFile(
filePath: string,
details: ToolConfirmationDetails
): boolean {
switch (details.type) {
case ToolConfirmationType.Create:
case ToolConfirmationType.Edit:
case ToolConfirmationType.ApplyPatch:
return details.filePath === filePath;
default:
return false;
}
}

async function main(): Promise<void> {
if (!process.env.FACTORY_API_KEY) {
Expand All @@ -27,13 +50,36 @@ async function main(): Promise<void> {
console.log('Connecting to local daemon...\n');
const daemon = await connectDaemon({ apiKey: process.env.FACTORY_API_KEY });
console.log('Connected!\n');
const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-daemon-'));
const frontendDir = join(tempDir, 'frontend');
const backendDir = join(tempDir, 'backend');
const frontendFile = join(frontendDir, 'hello.md');
const backendFile = join(backendDir, 'notes.md');
await Promise.all([
mkdir(frontendDir, { recursive: true }),
mkdir(backendDir, { recursive: true }),
]);

try {
const frontend = await daemon.createSession({
cwd: '/tmp/daemon-test-frontend',
cwd: frontendDir,
permissionHandler(params) {
return params.toolUses.every((item) =>
canWriteFile(frontendFile, item.details)
)
? ToolConfirmationOutcome.ProceedOnce
: ToolConfirmationOutcome.Cancel;
},
});
const backend = await daemon.createSession({
cwd: '/tmp/daemon-test-backend',
cwd: backendDir,
permissionHandler(params) {
return params.toolUses.every((item) =>
canWriteFile(backendFile, item.details)
)
? ToolConfirmationOutcome.ProceedOnce
: ToolConfirmationOutcome.Cancel;
},
});

console.log('Two sessions created. Running concurrently...\n');
Expand Down Expand Up @@ -76,11 +122,17 @@ async function main(): Promise<void> {
]);
} finally {
await daemon.close();
try {
const [greeting, notes] = await Promise.all([
readFile(frontendFile, 'utf8'),
readFile(backendFile, 'utf8'),
]);
console.log(`\n=== hello.md ===\n${greeting.trim()}`);
console.log(`\n=== notes.md ===\n${notes.trim()}`);
} finally {
await rm(tempDir, { recursive: true, force: true });
}
}

console.log(
'\nDone. Check /tmp/daemon-test-frontend/hello.md and /tmp/daemon-test-backend/notes.md'
);
}

main().catch((err: unknown) => {
Expand Down
36 changes: 27 additions & 9 deletions examples/spec-mode-new-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
*/

import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import {
Expand All @@ -24,9 +23,30 @@ import {
createSession,
} from '@factory/droid-sdk';

const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-spec-'));
const tempDir = await mkdtemp(join(process.cwd(), '.droid-sdk-spec-'));
const outputPath = join(tempDir, 'hello.txt');

async function waitForFile(path: string, timeoutMs = 120_000): Promise<string> {
const deadline = Date.now() + timeoutMs;

while (Date.now() < deadline) {
try {
return await readFile(path, 'utf8');
} catch (error) {
if (
!(error instanceof Error) ||
!('code' in error) ||
error.code !== 'ENOENT'
) {
throw error;
}
await new Promise<void>((resolve) => setTimeout(resolve, 500));
}
}

throw new Error(`Timed out waiting for ${path}`);
}

try {
const session = await createSession({
apiKey: process.env.FACTORY_API_KEY!,
Expand Down Expand Up @@ -58,18 +78,16 @@ try {
)) {
// Consume the stream until the handoff implementation finishes.
}
} finally {
await session.close();
}

try {
console.log(await readFile(outputPath, 'utf8'));
} catch {
console.log(await waitForFile(outputPath));
} catch (error) {
console.error(
`Expected ${outputPath} to exist after the turn, but it was not ` +
'created.'
`created: ${error instanceof Error ? error.message : String(error)}`
);
process.exitCode = 1;
} finally {
await session.close();
}
} finally {
await rm(tempDir, { recursive: true, force: true });
Expand Down
Loading
Loading