diff --git a/README.md b/README.md index 35cea69..169c463 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,12 @@ const result2 = await prompt('Run: echo hello', agentId); ```typescript import { createAgent, resumeSession } from '@letta-ai/letta-code-sdk'; -// Create an agent (has default conversation) -const agentId = await createAgent(); +// Create an agent with custom memory (has default conversation) +const agentId = await createAgent({ + memory: ['persona', 'project'], + persona: 'You are a helpful coding assistant', + project: 'A TypeScript web application' +}); // Resume the default conversation await using session = resumeSession(agentId); @@ -219,12 +223,10 @@ for await (const msg of session.stream()) { /* ... */ } | Function | Description | |----------|-------------| -| `createAgent()` | Create new agent with default conversation, returns `agentId` | -| `createSession()` | New conversation on default agent | -| `createSession(agentId)` | New conversation on specified agent | +| `createAgent(options?)` | Create new agent with custom memory/prompt. No options = blank agent with default memory blocks. Returns `agentId` | +| `createSession(agentId?, options?)` | New conversation on specified agent. No agentId = uses LRU agent (or creates "Memo" if none exists) | | `resumeSession(id, options?)` | Resume session - pass `agent-xxx` for default conv, `conv-xxx` for specific conv | -| `prompt(message)` | One-shot query with default agent (like `letta -p`) | -| `prompt(message, agentId)` | One-shot query with specific agent | +| `prompt(message, agentId?)` | One-shot query with default/specified agent (like `letta -p`) | ### Session @@ -239,24 +241,37 @@ for await (const msg of session.stream()) { /* ... */ } ### Options -```typescript -interface SessionOptions { - model?: string; - systemPrompt?: string | { type: 'preset'; preset: string; append?: string }; - memory?: Array; - persona?: string; - human?: string; - project?: string; - cwd?: string; +**CreateAgentOptions** (for `createAgent()` - full control): - // Tool configuration - allowedTools?: string[]; - permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions'; - canUseTool?: (toolName: string, toolInput: object) => Promise; - maxTurns?: number; -} +```typescript +// Create blank agent +await createAgent(); + +// Create agent with custom memory and system prompt +await createAgent({ + model: 'claude-sonnet-4', + systemPrompt: 'You are a helpful Python expert.', + memory: ['persona', 'project'], + persona: 'You are a senior Python developer', + project: 'FastAPI backend for a todo app' +}); ``` +**CreateSessionOptions** (for `createSession()` / `resumeSession()` - runtime options): + +```typescript +// Start session with permissions +createSession(agentId, { + permissionMode: 'bypassPermissions', + allowedTools: ['Bash', 'Glob'], + cwd: '/path/to/project' +}); +``` + +**Available system prompt presets:** +- `default` / `letta-claude`, `letta-codex`, `letta-gemini` - Full Letta Code prompts +- `claude`, `codex`, `gemini` - Basic (no skills/memory instructions) + ### Message Types ```typescript diff --git a/examples/v2-examples.ts b/examples/v2-examples.ts index cb91f28..e499226 100644 --- a/examples/v2-examples.ts +++ b/examples/v2-examples.ts @@ -211,20 +211,19 @@ async function testOptions() { const modelResult = await prompt('Say "model test ok"', agentId); console.log(` basic: ${modelResult.success ? 'PASS' : 'FAIL'} - ${modelResult.result?.slice(0, 50)}`); - // Test systemPrompt option via createSession - console.log('Testing systemPrompt option...'); + // Test systemPrompt preset via createSession (only presets allowed) + console.log('Testing systemPrompt preset...'); const sysPromptSession = createSession(undefined, { - systemPrompt: 'You love penguins and always try to work penguin facts into conversations.', + systemPrompt: 'letta-claude', permissionMode: 'bypassPermissions', }); - await sysPromptSession.send('Tell me a fun fact about penguins in one sentence.'); + await sysPromptSession.send('Hello, what kind of agent are you?'); let sysPromptResponse = ''; for await (const msg of sysPromptSession.stream()) { if (msg.type === 'result') sysPromptResponse = msg.result || ''; } sysPromptSession.close(); - const hasPenguin = sysPromptResponse.toLowerCase().includes('penguin'); - console.log(` systemPrompt: ${hasPenguin ? 'PASS' : 'PARTIAL'} - ${sysPromptResponse.slice(0, 80)}`); + console.log(` systemPrompt preset: ${sysPromptResponse ? 'PASS' : 'FAIL'} - ${sysPromptResponse.slice(0, 80)}`); // Test cwd option via createSession console.log('Testing cwd option...'); @@ -472,7 +471,8 @@ async function testSystemPrompt() { console.log('=== Testing System Prompt Configuration ===\n'); async function runWithSystemPrompt(msg: string, systemPrompt: any): Promise { - const session = createSession(undefined, { systemPrompt, permissionMode: 'bypassPermissions' }); + const agentId = await createAgent({ systemPrompt }); + const session = resumeSession(agentId, { permissionMode: 'bypassPermissions' }); await session.send(msg); let result = ''; for await (const m of session.stream()) { @@ -532,7 +532,8 @@ async function testMemoryConfig() { console.log('=== Testing Memory Configuration ===\n'); async function runWithMemory(msg: string, memory?: any[]): Promise<{ success: boolean; result: string }> { - const session = createSession(undefined, { memory, permissionMode: 'bypassPermissions' }); + const agentId = await createAgent({ memory }); + const session = resumeSession(agentId, { permissionMode: 'bypassPermissions' }); await session.send(msg); let result = ''; let success = false; @@ -593,7 +594,8 @@ async function testConvenienceProps() { console.log('=== Testing Convenience Props ===\n'); async function runWithProps(msg: string, props: Record): Promise<{ success: boolean; result: string }> { - const session = createSession(undefined, { ...props, permissionMode: 'bypassPermissions' }); + const agentId = await createAgent(props); + const session = resumeSession(agentId, { permissionMode: 'bypassPermissions' }); await session.send(msg); let result = ''; let success = false; diff --git a/src/index.ts b/src/index.ts index c72f0b1..3203cb0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,11 +29,13 @@ */ import { Session } from "./session.js"; -import type { SessionOptions, SDKMessage, SDKResultMessage } from "./types.js"; +import type { CreateSessionOptions, CreateAgentOptions, SDKResultMessage } from "./types.js"; +import { validateCreateSessionOptions, validateCreateAgentOptions } from "./validation.js"; // Re-export types export type { - SessionOptions, + CreateSessionOptions, + CreateAgentOptions, SDKMessage, SDKInitMessage, SDKAssistantMessage, @@ -62,14 +64,23 @@ export { Session } from "./session.js"; * * @example * ```typescript + * // Create agent with default settings * const agentId = await createAgent(); * + * // Create agent with custom memory + * const agentId = await createAgent({ + * memory: ['persona', 'project'], + * persona: 'You are a helpful coding assistant', + * model: 'claude-sonnet-4' + * }); + * * // Then resume the default conversation: * const session = resumeSession(agentId); * ``` */ -export async function createAgent(): Promise { - const session = new Session({ createOnly: true }); +export async function createAgent(options: CreateAgentOptions = {}): Promise { + validateCreateAgentOptions(options); + const session = new Session({ ...options, createOnly: true }); const initMsg = await session.initialize(); session.close(); return initMsg.agentId; @@ -90,7 +101,8 @@ export async function createAgent(): Promise { * await using session = createSession(agentId); * ``` */ -export function createSession(agentId?: string, options: SessionOptions = {}): Session { +export function createSession(agentId?: string, options: CreateSessionOptions = {}): Session { + validateCreateSessionOptions(options); if (agentId) { return new Session({ ...options, agentId, newConversation: true }); } else { @@ -118,8 +130,9 @@ export function createSession(agentId?: string, options: SessionOptions = {}): S */ export function resumeSession( id: string, - options: SessionOptions = {} + options: CreateSessionOptions = {} ): Session { + validateCreateSessionOptions(options); if (id.startsWith("conv-")) { return new Session({ ...options, conversationId: id }); } else { diff --git a/src/session.ts b/src/session.ts index 5faf2ce..d5a0290 100644 --- a/src/session.ts +++ b/src/session.ts @@ -7,7 +7,7 @@ import { SubprocessTransport } from "./transport.js"; import type { - SessionOptions, + InternalSessionOptions, SDKMessage, SDKInitMessage, SDKAssistantMessage, @@ -20,7 +20,7 @@ import type { CanUseToolResponseDeny, SendMessage, } from "./types.js"; -import { validateSessionOptions } from "./validation.js"; + export class Session implements AsyncDisposable { private transport: SubprocessTransport; @@ -30,10 +30,9 @@ export class Session implements AsyncDisposable { private initialized = false; constructor( - private options: SessionOptions & { agentId?: string } = {} + private options: InternalSessionOptions = {} ) { - // Validate options before creating transport - validateSessionOptions(options); + // Note: Validation happens in public API functions (createSession, createAgent, etc.) this.transport = new SubprocessTransport(options); } diff --git a/src/transport.ts b/src/transport.ts index 1718e61..c7661ed 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -6,7 +6,7 @@ import { spawn, type ChildProcess } from "node:child_process"; import { createInterface, type Interface } from "node:readline"; -import type { SessionOptions, WireMessage } from "./types.js"; +import type { InternalSessionOptions, WireMessage } from "./types.js"; export class SubprocessTransport { private process: ChildProcess | null = null; @@ -17,7 +17,7 @@ export class SubprocessTransport { private agentId?: string; constructor( - private options: SessionOptions & { agentId?: string } = {} + private options: InternalSessionOptions = {} ) {} /** @@ -170,34 +170,7 @@ export class SubprocessTransport { "stream-json", ]; - // Validate conversation + agent combinations - // (These require agentId context, so can't be in validateSessionOptions) - - // conversationId (non-default) cannot be used with agentId - if (this.options.conversationId && - this.options.conversationId !== "default" && - this.options.agentId) { - throw new Error( - "Cannot use both 'conversationId' and 'agentId'. " + - "When resuming a conversation, the agent is derived automatically." - ); - } - - // conversationId: "default" requires agentId - if (this.options.conversationId === "default" && !this.options.agentId) { - throw new Error( - "conversationId 'default' requires agentId. " + - "Use resumeSession(agentId, { defaultConversation: true }) instead." - ); - } - - // defaultConversation requires agentId - if (this.options.defaultConversation && !this.options.agentId) { - throw new Error( - "'defaultConversation' requires agentId. " + - "Use resumeSession(agentId, { defaultConversation: true })." - ); - } + // Note: All validation happens in validateInternalSessionOptions() called from Session constructor // Conversation and agent handling if (this.options.conversationId) { @@ -233,8 +206,23 @@ export class SubprocessTransport { // System prompt configuration if (this.options.systemPrompt !== undefined) { if (typeof this.options.systemPrompt === "string") { - // Raw string → --system-custom - args.push("--system-custom", this.options.systemPrompt); + // Check if it's a valid preset name or custom string + const validPresets = [ + "default", + "letta-claude", + "letta-codex", + "letta-gemini", + "claude", + "codex", + "gemini", + ]; + if (validPresets.includes(this.options.systemPrompt)) { + // Preset name → --system + args.push("--system", this.options.systemPrompt); + } else { + // Custom string → --system-custom + args.push("--system-custom", this.options.systemPrompt); + } } else { // Preset object → --system (+ optional --system-append) args.push("--system", this.options.systemPrompt.preset); diff --git a/src/types.ts b/src/types.ts index 1ce3141..efeff2f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -128,27 +128,78 @@ export type CanUseToolCallback = ( ) => Promise | CanUseToolResponse; /** - * Options for creating a session + * Internal session options used by Session/Transport classes. + * Not user-facing - use CreateSessionOptions or CreateAgentOptions instead. + * @internal */ -export interface SessionOptions { - /** Model to use (e.g., "claude-sonnet-4-20250514") */ +export interface InternalSessionOptions { + // Agent/conversation routing + agentId?: string; + conversationId?: string; + newConversation?: boolean; + defaultConversation?: boolean; + createOnly?: boolean; + promptMode?: boolean; + + // Agent configuration + model?: string; + systemPrompt?: SystemPromptConfig; + + // Memory blocks (only for new agents) + memory?: MemoryItem[]; + persona?: string; + human?: string; + project?: string; + + // Permissions + allowedTools?: string[]; + permissionMode?: PermissionMode; + canUseTool?: CanUseToolCallback; + + // Process settings + cwd?: string; +} + +export type PermissionMode = "default" | "acceptEdits" | "bypassPermissions"; + +/** + * Options for createSession() and resumeSession() - restricted to options that can be applied to existing agents (LRU/Memo). + * For creating new agents with custom memory/persona, use createAgent(). + */ +export interface CreateSessionOptions { + /** Model to use (e.g., "claude-sonnet-4-20250514") - updates the agent's LLM config */ model?: string; - // ═══════════════════════════════════════════════════════════════ - // Internal flags - set by createSession/resumeSession, not user-facing - // ═══════════════════════════════════════════════════════════════ - /** @internal */ conversationId?: string; - /** @internal */ newConversation?: boolean; - /** @internal */ defaultConversation?: boolean; - /** @internal */ createOnly?: boolean; - /** @internal */ promptMode?: boolean; + /** System prompt preset (only presets, no custom strings or append) - updates the agent */ + systemPrompt?: SystemPromptPreset; + + /** List of allowed tool names */ + allowedTools?: string[]; + + /** Permission mode */ + permissionMode?: PermissionMode; + + /** Working directory for the CLI process */ + cwd?: string; + + /** Custom permission callback - called when tool needs approval */ + canUseTool?: CanUseToolCallback; +} + +/** + * Options for createAgent() - full control over agent creation. + */ +export interface CreateAgentOptions { + /** Model to use (e.g., "claude-sonnet-4-20250514") */ + model?: string; /** * System prompt configuration. * - string: Use as the complete system prompt + * - SystemPromptPreset: Use a preset * - { type: 'preset', preset, append? }: Use a preset with optional appended text */ - systemPrompt?: SystemPromptConfig; + systemPrompt?: string | SystemPromptPreset | SystemPromptPresetConfigSDK; /** * Memory block configuration. Each item can be: @@ -173,18 +224,13 @@ export interface SessionOptions { /** Permission mode */ permissionMode?: PermissionMode; - /** Working directory */ + /** Working directory for the CLI process */ cwd?: string; - /** Maximum conversation turns */ - maxTurns?: number; - /** Custom permission callback - called when tool needs approval */ canUseTool?: CanUseToolCallback; } -export type PermissionMode = "default" | "acceptEdits" | "bypassPermissions"; - // ═══════════════════════════════════════════════════════════════ // SDK MESSAGE TYPES // ═══════════════════════════════════════════════════════════════ diff --git a/src/validation.ts b/src/validation.ts index 25f6e40..cbb83f4 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -1,10 +1,16 @@ /** * SDK Validation * - * Validates SessionOptions before spawning the CLI. + * Validates user-provided options before spawning the CLI. */ -import type { SessionOptions, MemoryItem, CreateBlock } from "./types.js"; +import type { + CreateSessionOptions, + CreateAgentOptions, + MemoryItem, + CreateBlock, + SystemPromptPreset +} from "./types.js"; /** * Extract block labels from memory items. @@ -20,11 +26,41 @@ function getBlockLabels(memory: MemoryItem[]): string[] { } /** - * Validate SessionOptions before spawning CLI. - * Throws an error if validation fails. + * Validate systemPrompt preset value. */ -export function validateSessionOptions(options: SessionOptions): void { - // If memory is specified, validate that convenience props match included blocks +function validateSystemPromptPreset(preset: string): void { + const validPresets = [ + "default", + "letta-claude", + "letta-codex", + "letta-gemini", + "claude", + "codex", + "gemini", + ]; + if (!validPresets.includes(preset)) { + throw new Error( + `Invalid system prompt preset '${preset}'. ` + + `Valid presets: ${validPresets.join(", ")}` + ); + } +} + +/** + * Validate CreateSessionOptions (used by createSession and resumeSession). + */ +export function validateCreateSessionOptions(options: CreateSessionOptions): void { + // Validate systemPrompt preset if provided + if (options.systemPrompt !== undefined) { + validateSystemPromptPreset(options.systemPrompt); + } +} + +/** + * Validate CreateAgentOptions (used by createAgent). + */ +export function validateCreateAgentOptions(options: CreateAgentOptions): void { + // Validate memory/persona consistency if (options.memory !== undefined) { const blockLabels = getBlockLabels(options.memory); @@ -50,11 +86,17 @@ export function validateSessionOptions(options: SessionOptions): void { } } - // Validate systemPrompt preset if provided + // Validate systemPrompt preset if provided as preset object if ( options.systemPrompt !== undefined && typeof options.systemPrompt === "object" ) { + validateSystemPromptPreset(options.systemPrompt.preset); + } else if ( + options.systemPrompt !== undefined && + typeof options.systemPrompt === "string" + ) { + // Check if it's a preset name (if so, validate it) const validPresets = [ "default", "letta-claude", @@ -63,36 +105,10 @@ export function validateSessionOptions(options: SessionOptions): void { "claude", "codex", "gemini", - ]; - if (!validPresets.includes(options.systemPrompt.preset)) { - throw new Error( - `Invalid system prompt preset '${options.systemPrompt.preset}'. ` + - `Valid presets: ${validPresets.join(", ")}` - ); + ] as const; + if (validPresets.includes(options.systemPrompt as SystemPromptPreset)) { + validateSystemPromptPreset(options.systemPrompt); } + // If not a preset, it's a custom string - no validation needed } - - // Validate conversation options - if (options.conversationId && options.newConversation) { - throw new Error( - "Cannot use both 'conversationId' and 'newConversation'. " + - "Use conversationId to resume a specific conversation, or newConversation to create a new one." - ); - } - - if (options.defaultConversation && options.conversationId) { - throw new Error( - "Cannot use both 'defaultConversation' and 'conversationId'. " + - "Use defaultConversation with agentId, or conversationId alone." - ); - } - - if (options.defaultConversation && options.newConversation) { - throw new Error( - "Cannot use both 'defaultConversation' and 'newConversation'." - ); - } - - // Note: Validations that require agentId context happen in transport.ts buildArgs() - // because agentId is passed separately to resumeSession(), not in SessionOptions }