Skip to content
Closed
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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@ Returned by `createSession()` and `resumeSession()`. Key methods:
- **`updateSettings(params)`** — update model, autonomy level, etc.
- **`enterSpecMode(params?)`** — switch the current session into spec mode
- **`forkSession()`** — create a forked server-side session and return its new session ID
- **`addMcpServer(params)`** / **`removeMcpServer(params)`** — manage MCP servers
- **`addMcpServer(params)`** / **`removeMcpServer(params)`** / **`toggleMcpServer(params)`** — manage MCP servers
- **`listMcpServers()`** / **`listMcpTools()`** / **`authenticateMcpServer(params)`** — inspect and authenticate MCP servers
- **`listTools(params?)`** — inspect the exec tool catalog and current allow/deny state
- **`renameSession(params)`** — rename the current session
- **`sessionId`** — the session ID
Expand Down Expand Up @@ -318,6 +319,9 @@ if (msg.type === DroidMessageType.AssistantTextDelta) {
| `create_message` | Full assistant message created |
| `turn_complete` | Sentinel: agent turn finished |
| `session_title_updated` | Session title changed |
| `mcp_status_changed` | MCP server status changed |
| `mcp_auth_required` | MCP authentication required |
| `mcp_auth_completed` | MCP authentication completed |
| `error` | Error event from the process |

### Options
Expand All @@ -332,6 +336,7 @@ if (msg.type === DroidMessageType.AssistantTextDelta) {
- **`reasoningEffort`** — `ReasoningEffort` enum value
- **`specModeModelId`** — override model used in spec mode
- **`specModeReasoningEffort`** — override reasoning level used in spec mode
- **`mcpServers`** — initial MCP server configurations, including SDK-backed MCP servers from `createSdkMcpServer()`
- **`enabledToolIds`** — explicit exec tool allowlist
- **`disabledToolIds`** — explicit exec tool denylist
- **`permissionHandler`** — callback for tool confirmations
Expand Down Expand Up @@ -367,6 +372,7 @@ See the [`examples/`](./examples) directory for runnable examples:
- **[`spec-mode-same-session.ts`](./examples/spec-mode-same-session.ts)** — approve a spec and continue in the same session
- **[`spec-mode-new-session.ts`](./examples/spec-mode-new-session.ts)** — approve a spec and hand off implementation to a new session
- **[`tool-controls.ts`](./examples/tool-controls.ts)** — configure allow/deny lists and inspect tool availability
- **[`sdk-mcp-tool.ts`](./examples/sdk-mcp-tool.ts)** — expose SDK-defined tools to Droid through MCP
- **[`fork-session.ts`](./examples/fork-session.ts)** — fork a session and continue from the new session ID
- **[`list-sessions.ts`](./examples/list-sessions.ts)** — discover droid sessions saved on disk

Expand Down
123 changes: 123 additions & 0 deletions docs/system-prompt-sdk-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
## Goal

Add a Claude Agent SDK-style `systemPrompt?: string` option to the TypeScript SDK, where `systemPrompt` means **replace Droid's configurable main system prompt** for SDK-spawned Droid processes. The SDK must not map this to append behavior.

## Proposed public SDK API

```ts
await run('Hello', {
systemPrompt: 'You are a helpful support assistant. Be concise.',
});

const session = await createSession({
systemPrompt: 'You are a strict TypeScript reviewer.',
});

const stream = query({
prompt: 'Review this diff',
systemPrompt: 'You are a senior security reviewer.',
});
```

Add `systemPrompt?: string` to the shared process/session option path so it is available through:

- `createSession(options)`
- `resumeSession(sessionId, options)`
- `query(options)`
- `run(text, options)`
- direct `ProcessTransport` construction, if applicable

## Intended behavior

- `systemPrompt` maps to the new Droid CLI startup flag `--system-prompt <text>`.
- It does **not** use `--append-system-prompt`.
- It is process-level startup configuration, not a JSON-RPC request field.
- It only applies when the SDK spawns Droid itself.
- If a caller passes a custom `transport`, `systemPrompt` should throw because the SDK cannot modify an already-provided transport.
- If a caller passes custom `execArgs` that already include `--system-prompt`, and also passes `systemPrompt`, throw a clear conflict error.
- Empty or whitespace-only `systemPrompt` should throw.

```mermaid
flowchart TD
A[SDK caller] --> B{transport?}
B -->|yes + systemPrompt| E[throw conflict]
B -->|no| C[build exec args]
C --> D[append --system-prompt]
D --> F[spawn Droid]
F --> G[stream JSON-RPC]
```

## Implementation plan

### 1. Update SDK option types

In `src/types.ts` / shared transport option types:

- Add `systemPrompt?: string` to process/spawn options.

In session/query/run types:

- Ensure `CreateSessionOptions`, `ResumeSessionOptions`, `QueryOptions`, and `RunOptions` inherit it through existing option composition.

### 2. Add exec arg construction helper

Add a small internal helper, for example:

```ts
function buildExecArgs(options: ProcessTransportOptions): string[];
```

Behavior:

- Start from default `['exec', '--input-format', 'stream-jsonrpc', '--output-format', 'stream-jsonrpc']` unless `execArgs` is provided.
- If `systemPrompt` is present:
- validate non-empty string
- reject if args already contain `--system-prompt`
- append `--system-prompt`, `systemPrompt`

### 3. Validate custom transport conflict

In `createTransport()`:

- If `options.transport && options.systemPrompt`, throw a clear `ConnectionError` or `Error` explaining that `systemPrompt` only works when the SDK spawns Droid.

### 4. Add tests

Add focused tests around argument construction / transport spawning:

- `ProcessTransport` includes `--system-prompt <text>` when `systemPrompt` is passed.
- Empty `systemPrompt` throws.
- Passing `systemPrompt` with custom `execArgs` already containing `--system-prompt` throws.
- Passing `systemPrompt` with supplied `transport` throws.
- Existing behavior without `systemPrompt` remains unchanged.

### 5. Add or update example

Update or add a user-facing example that imports from the package entrypoint style and demonstrates:

```ts
await run('Say hello', {
systemPrompt: 'You are a concise assistant.',
});
```

Examples should not mock transport or use offline behavior.

### 6. Run validators

Run:

- `npm run typecheck`
- `npm run typecheck:examples`
- `npm run format:check`
- `npm run lint`
- `npm test`
- `npm run build`

## Dependency on Droid CLI

This SDK change assumes Droid CLI has or will have a true replacement flag:

- `--system-prompt <text>`

The SDK should only expose `systemPrompt` for now and should not expose append behavior.
38 changes: 38 additions & 0 deletions examples/sdk-mcp-tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { z } from 'zod';

import {
ToolConfirmationOutcome,
createSession,
createSdkMcpServer,
tool,
} from '../src/index.js';

const execPath = process.env['DROID_EXEC_PATH'] ?? 'droid';

const sdkTools = createSdkMcpServer({
name: 'sdk-tools',
tools: [
tool(
'favorite_number',
'Returns a favorite number for a person',
{ name: z.string() },
({ name }) => `${name}'s favorite number is 42.`
),
],
});

const session = await createSession({
execPath,
mcpServers: [sdkTools],
cwd: process.cwd(),
permissionHandler: () => ToolConfirmationOutcome.ProceedOnce,
});

try {
const result = await session.send(
'Use the favorite_number tool for Ada and tell me the answer.'
);
console.log(result.text);
} finally {
await session.close();
}
22 changes: 22 additions & 0 deletions examples/system-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Manual smoke test for replacing Droid's system prompt through the SDK.
*
* Usage:
* npx tsx examples/system-prompt.ts
*/

import { run } from '../src/index.js';

async function main(): Promise<void> {
const result = await run('Say hello in one sentence.', {
cwd: process.cwd(),
systemPrompt: 'You are a concise assistant.',
});

console.log(result.text);
}

main().catch((err: unknown) => {
console.error('Error:', err);
process.exit(1);
});
Loading
Loading