diff --git a/README.md b/README.md index 1c70c31..04b97ef 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,17 @@ The SDK interface to [Letta Code](https://github.com/letta-ai/letta-code). Build agents with persistent memory that learn over time. ```typescript -import { prompt } from '@letta-ai/letta-code-sdk'; +import { createAgent, resumeSession } from '@letta-ai/letta-code-sdk'; -const result = await prompt('Find and fix the bug in auth.py', { - allowedTools: ['Read', 'Edit', 'Bash'], - permissionMode: 'bypassPermissions' -}); -console.log(result.result); +// Create an agent (has default conversation) +const agentId = await createAgent(); + +// Resume default conversation +const session = resumeSession(agentId); +await session.send('Find and fix the bug in auth.py'); +for await (const msg of session.stream()) { + if (msg.type === 'assistant') console.log(msg.content); +} ``` ## Installation @@ -27,19 +31,24 @@ npm install @letta-ai/letta-code-sdk ```typescript import { prompt } from '@letta-ai/letta-code-sdk'; -const result = await prompt('Run: echo hello', { - allowedTools: ['Bash'], - permissionMode: 'bypassPermissions' -}); -console.log(result.result); // "hello" +// One-shot (uses default agent - like `letta -p`) +const result = await prompt('What is 2 + 2?'); +console.log(result.result); + +// One-shot with specific agent +const result2 = await prompt('Run: echo hello', agentId); ``` ### Multi-turn session ```typescript -import { createSession } from '@letta-ai/letta-code-sdk'; +import { createAgent, resumeSession } from '@letta-ai/letta-code-sdk'; -await using session = createSession(); +// Create an agent (has default conversation) +const agentId = await createAgent(); + +// Resume the default conversation +await using session = resumeSession(agentId); await session.send('What is 5 + 3?'); for await (const msg of session.stream()) { @@ -57,16 +66,16 @@ for await (const msg of session.stream()) { Agents persist across sessions and remember context: ```typescript -import { createSession, resumeSession } from '@letta-ai/letta-code-sdk'; +import { createAgent, resumeSession } from '@letta-ai/letta-code-sdk'; -// First session -const session1 = createSession(); +// Create agent and teach it something +const agentId = await createAgent(); +const session1 = resumeSession(agentId); await session1.send('Remember: the secret word is "banana"'); for await (const msg of session1.stream()) { /* ... */ } -const agentId = session1.agentId; session1.close(); -// Later... +// Later... resume the default conversation await using session2 = resumeSession(agentId); await session2.send('What is the secret word?'); for await (const msg of session2.stream()) { @@ -79,44 +88,41 @@ for await (const msg of session2.stream()) { Run multiple concurrent conversations with the same agent. Each conversation has its own message history while sharing the agent's persistent memory. ```typescript -import { createSession, resumeSession, resumeConversation } from '@letta-ai/letta-code-sdk'; +import { createAgent, createSession, resumeSession } from '@letta-ai/letta-code-sdk'; -// Create an agent -const session = createSession(); -await session.send('Hello!'); -for await (const msg of session.stream()) { /* ... */ } -const agentId = session.agentId; -const conversationId = session.conversationId; // Save this! -session.close(); +// Create an agent (has default conversation) +const agentId = await createAgent(); -// Resume a specific conversation -await using session2 = resumeConversation(conversationId); +// Resume the default conversation +const session1 = resumeSession(agentId); +await session1.send('Hello!'); +for await (const msg of session1.stream()) { /* ... */ } +const conversationId = session1.conversationId; // Save this! +session1.close(); + +// Resume a specific conversation by ID +await using session2 = resumeSession(conversationId); // auto-detects conv-xxx await session2.send('Continue our discussion...'); for await (const msg of session2.stream()) { /* ... */ } // Create a NEW conversation on the same agent -await using session3 = resumeSession(agentId, { newConversation: true }); +await using session3 = createSession(agentId); await session3.send('Start a fresh thread...'); // session3.conversationId is different from conversationId -// Resume with agent's default conversation -await using session4 = resumeSession(agentId, { defaultConversation: true }); - -// Resume last used session (agent + conversation) -await using session5 = createSession({ continue: true }); - -// Create new agent with a new (non-default) conversation -await using session6 = createSession({ newConversation: true }); +// Create new agent + new conversation +await using session4 = createSession(); ``` **Key concepts:** - **Agent** (`agentId`): Persistent entity with memory that survives across sessions - **Conversation** (`conversationId`): A message thread within an agent -- **Session** (`sessionId`): A single execution/connection +- **Session**: A single execution/connection +- **Default conversation**: Always exists after `createAgent()` - use `resumeSession(agentId)` to access it Agents remember across conversations (via memory blocks), but each conversation has its own message history. -## Agent Configuration +## Session Configuration ### System Prompt @@ -124,12 +130,12 @@ Choose from built-in presets or provide a custom prompt: ```typescript // Use a preset -createSession({ +createSession(agentId, { systemPrompt: { type: 'preset', preset: 'letta-claude' } }); // Use a preset with additional instructions -createSession({ +createSession(agentId, { systemPrompt: { type: 'preset', preset: 'letta-claude', @@ -138,7 +144,7 @@ createSession({ }); // Use a completely custom prompt -createSession({ +createSession(agentId, { systemPrompt: 'You are a helpful Python expert.' }); ``` @@ -153,35 +159,27 @@ createSession({ ### Memory Blocks -Configure which memory blocks the agent uses: +Configure which memory blocks the session uses: ```typescript // Use default blocks (persona, human, project) -createSession({}); +createSession(agentId); // Use specific preset blocks -createSession({ +createSession(agentId, { memory: ['project', 'persona'] // Only these blocks }); // Use custom blocks -createSession({ +createSession(agentId, { memory: [ { label: 'context', value: 'API documentation for Acme Corp...' }, { label: 'rules', value: 'Always use TypeScript. Prefer functional patterns.' } ] }); -// Mix presets and custom blocks -createSession({ - memory: [ - 'project', // Use default project block - { label: 'custom', value: 'Additional context...' } - ] -}); - // No optional blocks (only core skills blocks) -createSession({ +createSession(agentId, { memory: [] }); ``` @@ -191,18 +189,11 @@ createSession({ Quickly customize common memory blocks: ```typescript -createSession({ +createSession(agentId, { persona: 'You are a senior Python developer who writes clean, tested code.', human: 'Name: Alice. Prefers concise responses.', project: 'FastAPI backend for a todo app using PostgreSQL.' }); - -// Combine with memory config -createSession({ - memory: ['persona', 'project'], // Only include these blocks - persona: 'You are a Go expert.', - project: 'CLI tool for managing Docker containers.' -}); ``` ### Tool Execution @@ -210,20 +201,17 @@ createSession({ Execute tools with automatic permission handling: ```typescript -import { prompt } from '@letta-ai/letta-code-sdk'; +import { createAgent, createSession } from '@letta-ai/letta-code-sdk'; -// Run shell commands -const result = await prompt('List all TypeScript files', { +// Create agent and run commands +const agentId = await createAgent(); +const session = createSession(agentId, { allowedTools: ['Glob', 'Bash'], permissionMode: 'bypassPermissions', cwd: '/path/to/project' }); - -// Read and analyze code -const analysis = await prompt('Explain what auth.ts does', { - allowedTools: ['Read', 'Grep'], - permissionMode: 'bypassPermissions' -}); +await session.send('List all TypeScript files'); +for await (const msg of session.stream()) { /* ... */ } ``` ## API Reference @@ -232,10 +220,11 @@ const analysis = await prompt('Explain what auth.ts does', { | Function | Description | |----------|-------------| -| `prompt(message, options?)` | One-shot query, returns result directly | -| `createSession(options?)` | Create new agent session | -| `resumeSession(agentId, options?)` | Resume existing agent by ID | -| `resumeConversation(conversationId, options?)` | Resume specific conversation (derives agent automatically) | +| `createAgent()` | Create new agent with default conversation, returns `agentId` | +| `createSession(agentId?, options?)` | Create new conversation (on existing agent if provided, or new agent) | +| `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 | ### Session @@ -252,36 +241,19 @@ const analysis = await prompt('Explain what auth.ts does', { ```typescript interface SessionOptions { - // Model selection model?: string; - - // Conversation options - conversationId?: string; // Resume specific conversation - newConversation?: boolean; // Create new conversation on agent - continue?: boolean; // Resume last session (agent + conversation) - defaultConversation?: boolean; // Use agent's default conversation - - // System prompt: string or preset config - systemPrompt?: string | { - type: 'preset'; - preset: 'default' | 'letta-claude' | 'letta-codex' | 'letta-gemini' | 'claude' | 'codex' | 'gemini'; - append?: string; - }; - - // Memory blocks: preset names, custom blocks, or mixed + systemPrompt?: string | { type: 'preset'; preset: string; append?: string }; memory?: Array; - - // Convenience: set block values directly persona?: string; human?: string; project?: string; + cwd?: string; // Tool configuration allowedTools?: string[]; permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions'; - - // Working directory - cwd?: string; + canUseTool?: (toolName: string, toolInput: object) => Promise; + maxTurns?: number; } ``` @@ -313,7 +285,7 @@ See [`examples/`](./examples/) for comprehensive examples including: - Basic session usage - Multi-turn conversations - Session resume with persistent memory -- **Multi-threaded conversations** (resumeConversation, newConversation) +- **Multi-threaded conversations** (createSession, resumeSession) - System prompt configuration - Memory block customization - Tool execution (Bash, Glob, Read, etc.) diff --git a/examples/v2-examples.ts b/examples/v2-examples.ts index 3048996..772c4c5 100644 --- a/examples/v2-examples.ts +++ b/examples/v2-examples.ts @@ -8,7 +8,7 @@ * Run with: bun examples/v2-examples.ts [example] */ -import { createSession, resumeSession, resumeConversation, prompt } from '../src/index.js'; +import { createAgent, createSession, resumeSession, prompt } from '../src/index.js'; async function main() { const example = process.argv[2] || 'basic'; @@ -82,8 +82,9 @@ async function main() { async function basicSession() { console.log('=== Basic Session ===\n'); - await using session = createSession({ - model: 'haiku', + // Create agent, then resume default conversation + const agentId = await createAgent(); + await using session = resumeSession(agentId, { permissionMode: 'bypassPermissions', }); @@ -106,7 +107,8 @@ async function basicSession() { async function multiTurn() { console.log('=== Multi-Turn Conversation ===\n'); - await using session = createSession({ + // Create new agent + new conversation + await using session = createSession(undefined, { model: 'haiku', permissionMode: 'bypassPermissions', }); @@ -133,10 +135,8 @@ async function multiTurn() { async function oneShot() { console.log('=== One-Shot Prompt ===\n'); - const result = await prompt('What is the capital of France? One word.', { - model: 'haiku', - permissionMode: 'bypassPermissions', - }); + // One-shot creates new agent + const result = await prompt('What is the capital of France? One word.'); if (result.success) { console.log(`Answer: ${result.result}`); @@ -151,12 +151,13 @@ async function oneShot() { async function sessionResume() { console.log('=== Session Resume (Persistent Memory) ===\n'); - let agentId: string | null = null; + // Create agent first + const agentId = await createAgent(); + console.log(`[Setup] Created agent: ${agentId}\n`); // First session - establish a memory { - const session = createSession({ - model: 'haiku', + const session = resumeSession(agentId, { permissionMode: 'bypassPermissions', }); @@ -171,8 +172,7 @@ async function sessionResume() { } } - agentId = session.agentId; - console.log(`[Session 1] Agent ID: ${agentId}\n`); + console.log(`[Session 1] Agent ID: ${session.agentId}\n`); session.close(); } @@ -180,7 +180,7 @@ async function sessionResume() { // Resume and verify agent remembers { - await using session = resumeSession(agentId!, { + await using session = resumeSession(agentId, { permissionMode: 'bypassPermissions', }); @@ -205,53 +205,71 @@ async function sessionResume() { async function testOptions() { console.log('=== Testing Options ===\n'); - // Test model option - console.log('Testing model option...'); - const modelResult = await prompt('Say "model test ok"', { - model: 'haiku', - permissionMode: 'bypassPermissions', - }); - console.log(` model: ${modelResult.success ? 'PASS' : 'FAIL'} - ${modelResult.result?.slice(0, 50)}`); + // Test basic session + console.log('Testing basic session...'); + const agentId = await createAgent(); + const modelResult = await prompt('Say "model test ok"', agentId); + console.log(` basic: ${modelResult.success ? 'PASS' : 'FAIL'} - ${modelResult.result?.slice(0, 50)}`); - // Test systemPrompt option + // Test systemPrompt option via createSession console.log('Testing systemPrompt option...'); - const sysPromptResult = await prompt('Tell me a fun fact about penguins in one sentence.', { - model: 'haiku', + const sysPromptSession = createSession(undefined, { systemPrompt: 'You love penguins and always try to work penguin facts into conversations.', permissionMode: 'bypassPermissions', }); - const hasPenguin = sysPromptResult.result?.toLowerCase().includes('penguin'); - console.log(` systemPrompt: ${hasPenguin ? 'PASS' : 'PARTIAL'} - ${sysPromptResult.result?.slice(0, 80)}`); + await sysPromptSession.send('Tell me a fun fact about penguins in one sentence.'); + 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)}`); - // Test cwd option + // Test cwd option via createSession console.log('Testing cwd option...'); - const cwdResult = await prompt('Run pwd to show current directory', { - model: 'haiku', + const cwdSession = createSession(undefined, { cwd: '/tmp', allowedTools: ['Bash'], permissionMode: 'bypassPermissions', }); - const hasTmp = cwdResult.result?.includes('/tmp'); - console.log(` cwd: ${hasTmp ? 'PASS' : 'CHECK'} - ${cwdResult.result?.slice(0, 60)}`); + await cwdSession.send('Run pwd to show current directory'); + let cwdResponse = ''; + for await (const msg of cwdSession.stream()) { + if (msg.type === 'result') cwdResponse = msg.result || ''; + } + cwdSession.close(); + const hasTmp = cwdResponse.includes('/tmp'); + console.log(` cwd: ${hasTmp ? 'PASS' : 'CHECK'} - ${cwdResponse.slice(0, 60)}`); // Test allowedTools option with tool execution console.log('Testing allowedTools option...'); - const toolsResult = await prompt('Run: echo tool-test-ok', { - model: 'haiku', + const toolsSession = createSession(undefined, { allowedTools: ['Bash'], permissionMode: 'bypassPermissions', }); - const hasToolOutput = toolsResult.result?.includes('tool-test-ok'); - console.log(` allowedTools: ${hasToolOutput ? 'PASS' : 'CHECK'} - ${toolsResult.result?.slice(0, 60)}`); + await toolsSession.send('Run: echo tool-test-ok'); + let toolsResponse = ''; + for await (const msg of toolsSession.stream()) { + if (msg.type === 'result') toolsResponse = msg.result || ''; + } + toolsSession.close(); + const hasToolOutput = toolsResponse.includes('tool-test-ok'); + console.log(` allowedTools: ${hasToolOutput ? 'PASS' : 'CHECK'} - ${toolsResponse.slice(0, 60)}`); // Test permissionMode: bypassPermissions console.log('Testing permissionMode: bypassPermissions...'); - const bypassResult = await prompt('Run: echo bypass-test', { - model: 'haiku', + const bypassSession = createSession(undefined, { allowedTools: ['Bash'], permissionMode: 'bypassPermissions', }); - const hasBypassOutput = bypassResult.result?.includes('bypass-test'); + await bypassSession.send('Run: echo bypass-test'); + let bypassResponse = ''; + for await (const msg of bypassSession.stream()) { + if (msg.type === 'result') bypassResponse = msg.result || ''; + } + bypassSession.close(); + const hasBypassOutput = bypassResponse.includes('bypass-test'); console.log(` permissionMode: ${hasBypassOutput ? 'PASS' : 'CHECK'}`); console.log(); @@ -264,8 +282,8 @@ async function testOptions() { async function testMessageTypes() { console.log('=== Testing Message Types ===\n'); - const session = createSession({ - model: 'haiku', + const agentId = await createAgent(); + const session = resumeSession(agentId, { permissionMode: 'bypassPermissions', }); @@ -309,7 +327,8 @@ async function testMessageTypes() { async function testSessionProperties() { console.log('=== Testing Session Properties ===\n'); - const session = createSession({ + // Create new agent + new conversation + const session = createSession(undefined, { model: 'haiku', permissionMode: 'bypassPermissions', }); @@ -343,44 +362,45 @@ async function testSessionProperties() { async function testToolExecution() { console.log('=== Testing Tool Execution ===\n'); + // Create a shared agent for tool tests + const agentId = await createAgent(); + + async function runWithTools(message: string, tools: string[]): Promise { + const session = createSession(agentId, { + allowedTools: tools, + permissionMode: 'bypassPermissions', + }); + await session.send(message); + let result = ''; + for await (const msg of session.stream()) { + if (msg.type === 'result') result = msg.result || ''; + } + session.close(); + return result; + } + // Test 1: Basic command execution console.log('Testing basic command execution...'); - const echoResult = await prompt('Run: echo hello-world', { - model: 'haiku', - allowedTools: ['Bash'], - permissionMode: 'bypassPermissions', - }); - const hasHello = echoResult.result?.includes('hello-world'); + const echoResult = await runWithTools('Run: echo hello-world', ['Bash']); + const hasHello = echoResult.includes('hello-world'); console.log(` echo command: ${hasHello ? 'PASS' : 'FAIL'}`); // Test 2: Command with arguments console.log('Testing command with arguments...'); - const argsResult = await prompt('Run: echo "arg1 arg2 arg3"', { - model: 'haiku', - allowedTools: ['Bash'], - permissionMode: 'bypassPermissions', - }); - const hasArgs = argsResult.result?.includes('arg1') && argsResult.result?.includes('arg3'); + const argsResult = await runWithTools('Run: echo "arg1 arg2 arg3"', ['Bash']); + const hasArgs = argsResult.includes('arg1') && argsResult.includes('arg3'); console.log(` echo with args: ${hasArgs ? 'PASS' : 'FAIL'}`); // Test 3: File reading with Glob console.log('Testing Glob tool...'); - const globResult = await prompt('List all .ts files in the current directory using Glob', { - model: 'haiku', - allowedTools: ['Glob'], - permissionMode: 'bypassPermissions', - }); - console.log(` Glob tool: ${globResult.success ? 'PASS' : 'FAIL'}`); + const globResult = await runWithTools('List all .ts files in the current directory using Glob', ['Glob']); + console.log(` Glob tool: ${globResult ? 'PASS' : 'FAIL'}`); // Test 4: Multi-step tool usage (agent decides which tools to use) console.log('Testing multi-step tool usage...'); - const multiResult = await prompt('First run "echo step1", then run "echo step2". Show me both outputs.', { - model: 'haiku', - allowedTools: ['Bash'], - permissionMode: 'bypassPermissions', - }); - const hasStep1 = multiResult.result?.includes('step1'); - const hasStep2 = multiResult.result?.includes('step2'); + const multiResult = await runWithTools('First run "echo step1", then run "echo step2". Show me both outputs.', ['Bash']); + const hasStep1 = multiResult.includes('step1'); + const hasStep2 = multiResult.includes('step2'); console.log(` multi-step: ${hasStep1 && hasStep2 ? 'PASS' : 'PARTIAL'} (step1: ${hasStep1}, step2: ${hasStep2})`); console.log(); @@ -398,37 +418,47 @@ async function testPermissionCallback() { // Test 1: Allow specific commands via callback console.log('Testing canUseTool callback (allow)...'); - const allowResult = await prompt('Run: echo callback-allowed', { - model: 'haiku', + const allowSession = createSession(undefined, { // NO allowedTools - this ensures callback is invoked permissionMode: 'default', canUseTool: async (toolName, toolInput) => { console.error('CALLBACK:', toolName, toolInput); const command = (toolInput as { command?: string }).command || ''; if (command.includes('callback-allowed')) { - return { allow: true, reason: 'Command whitelisted' }; + return { behavior: 'allow', updatedInput: null }; } - return { allow: false, reason: 'Command not whitelisted' }; + return { behavior: 'deny', message: 'Command not whitelisted' }; }, }); - const hasAllowed = allowResult.result?.includes('callback-allowed'); + await allowSession.send('Run: echo callback-allowed'); + let allowResult = ''; + for await (const msg of allowSession.stream()) { + if (msg.type === 'result') allowResult = msg.result || ''; + } + allowSession.close(); + const hasAllowed = allowResult.includes('callback-allowed'); console.log(` allow via callback: ${hasAllowed ? 'PASS' : 'FAIL'}`); // Test 2: Deny specific commands via callback console.log('Testing canUseTool callback (deny)...'); - const denyResult = await prompt('Run: echo dangerous-command', { - model: 'haiku', + const denySession = createSession(undefined, { permissionMode: 'default', canUseTool: async (toolName, toolInput) => { const command = (toolInput as { command?: string }).command || ''; if (command.includes('dangerous')) { - return { allow: false, reason: 'Dangerous command blocked' }; + return { behavior: 'deny', message: 'Dangerous command blocked' }; } - return { allow: true }; + return { behavior: 'allow', updatedInput: null }; }, }); + await denySession.send('Run: echo dangerous-command'); + let denyResult = ''; + for await (const msg of denySession.stream()) { + if (msg.type === 'result') denyResult = msg.result || ''; + } + denySession.close(); // Agent should report that it couldn't execute the command - const wasDenied = !denyResult.result?.includes('dangerous-command'); + const wasDenied = !denyResult.includes('dangerous-command'); console.log(` deny via callback: ${wasDenied ? 'PASS' : 'CHECK'}`); console.log(); @@ -441,52 +471,55 @@ async function testPermissionCallback() { 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' }); + await session.send(msg); + let result = ''; + for await (const m of session.stream()) { + if (m.type === 'result') result = m.result || ''; + } + session.close(); + return result; + } + // Test 1: Preset system prompt console.log('Testing preset system prompt...'); - const presetResult = await prompt('What kind of agent are you? One sentence.', { - model: 'haiku', - systemPrompt: { type: 'preset', preset: 'letta-claude' }, - permissionMode: 'bypassPermissions', - }); - console.log(` preset (letta-claude): ${presetResult.success ? 'PASS' : 'FAIL'}`); - console.log(` Response: ${presetResult.result?.slice(0, 80)}...`); + const presetResult = await runWithSystemPrompt( + 'What kind of agent are you? One sentence.', + { type: 'preset', preset: 'letta-claude' } + ); + console.log(` preset (letta-claude): ${presetResult ? 'PASS' : 'FAIL'}`); + console.log(` Response: ${presetResult.slice(0, 80)}...`); // Test 2: Preset with append console.log('Testing preset with append...'); - const appendResult = await prompt('Say hello', { - model: 'haiku', - systemPrompt: { - type: 'preset', - preset: 'letta-claude', - append: 'Always end your responses with "🎉"' - }, - permissionMode: 'bypassPermissions', - }); - const hasEmoji = appendResult.result?.includes('🎉'); + const appendResult = await runWithSystemPrompt( + 'Say hello', + { type: 'preset', preset: 'letta-claude', append: 'Always end your responses with "🎉"' } + ); + const hasEmoji = appendResult.includes('🎉'); console.log(` preset with append: ${hasEmoji ? 'PASS' : 'CHECK'}`); - console.log(` Response: ${appendResult.result?.slice(0, 80)}...`); + console.log(` Response: ${appendResult.slice(0, 80)}...`); // Test 3: Custom string system prompt console.log('Testing custom string system prompt...'); - const customResult = await prompt('What is your specialty?', { - model: 'haiku', - systemPrompt: 'You are a pirate captain. Always speak like a pirate.', - permissionMode: 'bypassPermissions', - }); - const hasPirateSpeak = customResult.result?.toLowerCase().includes('arr') || - customResult.result?.toLowerCase().includes('matey') || - customResult.result?.toLowerCase().includes('ship'); - console.log(` custom string: ${customResult.success ? 'PASS' : 'FAIL'}`); - console.log(` Response: ${customResult.result?.slice(0, 80)}...`); + const customResult = await runWithSystemPrompt( + 'What is your specialty?', + 'You are a pirate captain. Always speak like a pirate.' + ); + const hasPirateSpeak = customResult.toLowerCase().includes('arr') || + customResult.toLowerCase().includes('matey') || + customResult.toLowerCase().includes('ship'); + console.log(` custom string: ${customResult ? 'PASS' : 'FAIL'}`); + console.log(` Response: ${customResult.slice(0, 80)}...`); // Test 4: Basic preset (claude - no skills/memory) console.log('Testing basic preset (claude)...'); - const basicResult = await prompt('Hello, just say hi back', { - model: 'haiku', - systemPrompt: { type: 'preset', preset: 'claude' }, - permissionMode: 'bypassPermissions', - }); - console.log(` basic preset (claude): ${basicResult.success ? 'PASS' : 'FAIL'}`); + const basicResult = await runWithSystemPrompt( + 'Hello, just say hi back', + { type: 'preset', preset: 'claude' } + ); + console.log(` basic preset (claude): ${basicResult ? 'PASS' : 'FAIL'}`); console.log(); } @@ -498,58 +531,55 @@ async function testSystemPrompt() { 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' }); + await session.send(msg); + let result = ''; + let success = false; + for await (const m of session.stream()) { + if (m.type === 'result') { + result = m.result || ''; + success = m.success; + } + } + session.close(); + return { success, result }; + } + // Test 1: Default memory (persona, human, project) console.log('Testing default memory blocks...'); - const defaultResult = await prompt('What memory blocks do you have? List their labels.', { - model: 'haiku', - permissionMode: 'bypassPermissions', - }); - const hasDefaultBlocks = defaultResult.result?.includes('persona') || - defaultResult.result?.includes('project'); + const defaultResult = await runWithMemory('What memory blocks do you have? List their labels.'); + const hasDefaultBlocks = defaultResult.result.includes('persona') || + defaultResult.result.includes('project'); console.log(` default blocks: ${defaultResult.success ? 'PASS' : 'FAIL'}`); console.log(` Response mentions blocks: ${hasDefaultBlocks ? 'yes' : 'check manually'}`); // Test 2: Specific preset blocks only console.log('Testing specific preset blocks...'); - const specificResult = await prompt('List your memory block labels', { - model: 'haiku', - memory: ['project'], - permissionMode: 'bypassPermissions', - }); + const specificResult = await runWithMemory('List your memory block labels', ['project']); console.log(` specific blocks [project]: ${specificResult.success ? 'PASS' : 'FAIL'}`); // Test 3: Custom blocks console.log('Testing custom memory blocks...'); - const customResult = await prompt('What does your "rules" memory block say?', { - model: 'haiku', - memory: [ - { label: 'rules', value: 'Always be concise. Never use more than 10 words.' } - ], - permissionMode: 'bypassPermissions', - }); - const isConcise = (customResult.result?.split(' ').length || 0) < 20; + const customResult = await runWithMemory( + 'What does your "rules" memory block say?', + [{ label: 'rules', value: 'Always be concise. Never use more than 10 words.' }] + ); + const isConcise = (customResult.result.split(' ').length || 0) < 20; console.log(` custom blocks: ${customResult.success ? 'PASS' : 'FAIL'}`); console.log(` Response is concise: ${isConcise ? 'yes' : 'check'}`); // Test 4: Mixed preset and custom blocks console.log('Testing mixed blocks (preset + custom)...'); - const mixedResult = await prompt('List your memory blocks', { - model: 'haiku', - memory: [ - 'project', - { label: 'custom-context', value: 'This is a test context block.' } - ], - permissionMode: 'bypassPermissions', - }); + const mixedResult = await runWithMemory( + 'List your memory blocks', + ['project', { label: 'custom-context', value: 'This is a test context block.' }] + ); console.log(` mixed blocks: ${mixedResult.success ? 'PASS' : 'FAIL'}`); // Test 5: Empty memory (core blocks only) console.log('Testing empty memory (core only)...'); - const emptyResult = await prompt('Hello', { - model: 'haiku', - memory: [], - permissionMode: 'bypassPermissions', - }); + const emptyResult = await runWithMemory('Hello', []); console.log(` empty memory: ${emptyResult.success ? 'PASS' : 'FAIL'}`); console.log(); @@ -562,65 +592,72 @@ async function testMemoryConfig() { 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' }); + await session.send(msg); + let result = ''; + let success = false; + for await (const m of session.stream()) { + if (m.type === 'result') { + result = m.result || ''; + success = m.success; + } + } + session.close(); + return { success, result }; + } + // Test 1: persona prop console.log('Testing persona prop...'); - const personaResult = await prompt('Describe your personality in one sentence', { - model: 'haiku', - persona: 'You are an enthusiastic cooking assistant who loves Italian food.', - permissionMode: 'bypassPermissions', - }); - const hasItalian = personaResult.result?.toLowerCase().includes('italian') || - personaResult.result?.toLowerCase().includes('cook'); + const personaResult = await runWithProps( + 'Describe your personality in one sentence', + { persona: 'You are an enthusiastic cooking assistant who loves Italian food.' } + ); + const hasItalian = personaResult.result.toLowerCase().includes('italian') || + personaResult.result.toLowerCase().includes('cook'); console.log(` persona: ${personaResult.success ? 'PASS' : 'FAIL'}`); console.log(` Response mentions cooking/Italian: ${hasItalian ? 'yes' : 'check'}`); // Test 2: project prop console.log('Testing project prop...'); - const projectResult = await prompt('What project are you helping with?', { - model: 'haiku', - project: 'A React Native mobile app for tracking daily habits.', - permissionMode: 'bypassPermissions', - }); - const hasProject = projectResult.result?.toLowerCase().includes('react') || - projectResult.result?.toLowerCase().includes('habit') || - projectResult.result?.toLowerCase().includes('mobile'); + const projectResult = await runWithProps( + 'What project are you helping with?', + { project: 'A React Native mobile app for tracking daily habits.' } + ); + const hasProject = projectResult.result.toLowerCase().includes('react') || + projectResult.result.toLowerCase().includes('habit') || + projectResult.result.toLowerCase().includes('mobile'); console.log(` project: ${projectResult.success ? 'PASS' : 'FAIL'}`); console.log(` Response mentions project: ${hasProject ? 'yes' : 'check'}`); // Test 3: human prop console.log('Testing human prop...'); - const humanResult = await prompt('What do you know about me?', { - model: 'haiku', - human: 'Name: Bob. Senior developer. Prefers TypeScript over JavaScript.', - permissionMode: 'bypassPermissions', - }); - const hasHuman = humanResult.result?.toLowerCase().includes('bob') || - humanResult.result?.toLowerCase().includes('typescript'); + const humanResult = await runWithProps( + 'What do you know about me?', + { human: 'Name: Bob. Senior developer. Prefers TypeScript over JavaScript.' } + ); + const hasHuman = humanResult.result.toLowerCase().includes('bob') || + humanResult.result.toLowerCase().includes('typescript'); console.log(` human: ${humanResult.success ? 'PASS' : 'FAIL'}`); console.log(` Response mentions user info: ${hasHuman ? 'yes' : 'check'}`); // Test 4: Multiple convenience props together console.log('Testing multiple convenience props...'); - const multiResult = await prompt('Introduce yourself and the project briefly', { - model: 'haiku', - persona: 'You are a friendly code reviewer.', - project: 'FastAPI backend service.', - human: 'Name: Alice.', - permissionMode: 'bypassPermissions', - }); + const multiResult = await runWithProps( + 'Introduce yourself and the project briefly', + { persona: 'You are a friendly code reviewer.', project: 'FastAPI backend service.', human: 'Name: Alice.' } + ); console.log(` multiple props: ${multiResult.success ? 'PASS' : 'FAIL'}`); - console.log(` Response: ${multiResult.result?.slice(0, 100)}...`); + console.log(` Response: ${multiResult.result.slice(0, 100)}...`); // Test 5: Convenience props with specific memory blocks console.log('Testing convenience props with memory config...'); - const combinedResult = await prompt('What is in your persona block?', { - model: 'haiku', - memory: ['persona', 'project'], - persona: 'You are a database expert specializing in PostgreSQL.', - permissionMode: 'bypassPermissions', - }); - const hasDB = combinedResult.result?.toLowerCase().includes('database') || - combinedResult.result?.toLowerCase().includes('postgresql'); + const combinedResult = await runWithProps( + 'What is in your persona block?', + { memory: ['persona', 'project'], persona: 'You are a database expert specializing in PostgreSQL.' } + ); + const hasDB = combinedResult.result.toLowerCase().includes('database') || + combinedResult.result.toLowerCase().includes('postgresql'); console.log(` props with memory: ${combinedResult.success ? 'PASS' : 'FAIL'}`); console.log(` Response mentions DB: ${hasDB ? 'yes' : 'check'}`); @@ -634,15 +671,17 @@ async function testConvenienceProps() { async function testConversations() { console.log('=== Testing Conversation Support ===\n'); - let agentId: string | null = null; let conversationId1: string | null = null; let conversationId2: string | null = null; - // Test 1: Create session and get conversationId (default) - console.log('Test 1: Create session and get conversationId...'); + // Create agent first + const agentId = await createAgent(); + console.log(`Created agent: ${agentId}\n`); + + // Test 1: Resume default conversation and get conversationId + console.log('Test 1: Resume default conversation...'); { - const session = createSession({ - model: 'haiku', + const session = resumeSession(agentId, { permissionMode: 'bypassPermissions', }); @@ -651,28 +690,20 @@ async function testConversations() { // drain } - agentId = session.agentId; conversationId1 = session.conversationId; - const hasAgentId = agentId !== null && agentId.startsWith('agent-'); const hasConvId = conversationId1 !== null; - console.log(` agentId: ${hasAgentId ? 'PASS' : 'FAIL'} - ${agentId}`); + console.log(` agentId: ${session.agentId}`); console.log(` conversationId: ${hasConvId ? 'PASS' : 'FAIL'} - ${conversationId1}`); - // Note: "default" is a sentinel meaning the agent's primary message history - if (conversationId1 === 'default') { - console.log(' (conversationId "default" = agent\'s primary history, not a real conversation ID)'); - } - session.close(); } - // Test 2: Create NEW conversation to get a real conversation ID - console.log('\nTest 2: Create new conversation (newConversation: true)...'); + // Test 2: Create NEW conversation using createSession + console.log('\nTest 2: Create new conversation (createSession)...'); { - const session = resumeSession(agentId!, { - newConversation: true, + const session = createSession(agentId, { permissionMode: 'bypassPermissions', }); @@ -690,13 +721,13 @@ async function testConversations() { session.close(); } - // Test 3: Resume conversation by conversationId (only works with real conv IDs) + // Test 3: Resume conversation by conversationId (auto-detects conv-xxx) console.log('\nTest 3: Resume conversation by conversationId...'); if (conversationId1 === 'default') { console.log(' SKIP - "default" is not a real conversation ID'); console.log(' Use resumeSession(agentId) to resume default conversation'); } else { - await using session = resumeConversation(conversationId1!, { + await using session = resumeSession(conversationId1!, { permissionMode: 'bypassPermissions', }); @@ -708,7 +739,7 @@ async function testConversations() { } const remembers = response.toLowerCase().includes('beta'); - console.log(` resumeConversation: ${remembers ? 'PASS' : 'FAIL'}`); + console.log(` resumeSession(convId): ${remembers ? 'PASS' : 'FAIL'}`); console.log(` Response: ${response.slice(0, 80)}...`); // Verify same conversationId @@ -719,8 +750,7 @@ async function testConversations() { // Test 4: Create another new conversation (verify different IDs) console.log('\nTest 4: Create another new conversation...'); { - await using session = resumeSession(agentId!, { - newConversation: true, + await using session = createSession(agentId, { permissionMode: 'bypassPermissions', }); @@ -740,30 +770,29 @@ async function testConversations() { console.log(` conversationId2: ${conversationId2}`); } - // Test 5: defaultConversation option - console.log('\nTest 5: defaultConversation option...'); + // Test 5: Resume default conversation via resumeSession(agentId) + console.log('\nTest 5: Resume default conversation via resumeSession(agentId)...'); { - await using session = resumeSession(agentId!, { - defaultConversation: true, + await using session = resumeSession(agentId, { permissionMode: 'bypassPermissions', }); - await session.send('Say "default conversation test ok"'); + await session.send('What is the secret code? (should be ALPHA from default conversation)'); let response = ''; for await (const msg of session.stream()) { if (msg.type === 'assistant') response += msg.content; } - const hasDefaultConv = session.conversationId === 'default' || session.conversationId !== null; - console.log(` defaultConversation: ${hasDefaultConv ? 'PASS' : 'CHECK'}`); - console.log(` conversationId: ${session.conversationId}`); + const remembersAlpha = response.toLowerCase().includes('alpha'); + console.log(` resumeSession(agentId): ${remembersAlpha ? 'PASS' : 'CHECK'}`); + console.log(` Response: ${response.slice(0, 80)}...`); } // Test 6: conversationId in result message console.log('\nTest 6: conversationId in result message...'); { - await using session = resumeConversation(conversationId1!, { + await using session = resumeSession(conversationId1!, { permissionMode: 'bypassPermissions', }); @@ -782,31 +811,25 @@ async function testConversations() { console.log(` matches session.conversationId: ${matchesSession ? 'PASS' : 'FAIL'}`); } - // Test 7: continue option (resume last session) - console.log('\nTest 7: continue option...'); + // Test 7: createSession() without agentId creates new agent + conversation + console.log('\nTest 7: createSession() without agentId...'); { - // Note: This test may behave differently depending on local state - // The --continue flag resumes the last used agent + conversation - try { - await using session = createSession({ - continue: true, - permissionMode: 'bypassPermissions', - }); + await using session = createSession(undefined, { + model: 'haiku', + permissionMode: 'bypassPermissions', + }); - await session.send('Say "continue test ok"'); - - for await (const msg of session.stream()) { - // drain - } - - const hasIds = session.agentId !== null && session.conversationId !== null; - console.log(` continue: ${hasIds ? 'PASS' : 'CHECK'}`); - console.log(` agentId: ${session.agentId}`); - console.log(` conversationId: ${session.conversationId}`); - } catch (err) { - // --continue may fail if no previous session exists - console.log(` continue: SKIP (no previous session)`); + await session.send('Say "new agent test ok"'); + + for await (const msg of session.stream()) { + // drain } + + const hasNewAgent = session.agentId !== null && session.agentId !== agentId; + const hasConvId = session.conversationId !== null; + console.log(` new agent created: ${hasNewAgent ? 'PASS' : 'FAIL'}`); + console.log(` agentId: ${session.agentId}`); + console.log(` conversationId: ${hasConvId ? 'PASS' : 'FAIL'} - ${session.conversationId}`); } console.log(); diff --git a/src/index.ts b/src/index.ts index aab41ce..2b76118 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,20 +5,26 @@ * * @example * ```typescript - * import { createSession, prompt } from '@letta-ai/letta-code-sdk'; + * import { createAgent, createSession, resumeSession, prompt } from '@letta-ai/letta-code-sdk'; * - * // One-shot - * const result = await prompt('What is 2+2?', { model: 'claude-sonnet-4-20250514' }); + * // Start session with default agent + new conversation (like `letta`) + * const session = createSession(); * - * // Multi-turn session - * await using session = createSession({ model: 'claude-sonnet-4-20250514' }); - * await session.send('Hello!'); - * for await (const msg of session.stream()) { - * if (msg.type === 'assistant') console.log(msg.content); - * } + * // Create a new agent explicitly + * const agentId = await createAgent(); * - * // Resume with persistent memory - * await using resumed = resumeSession(agentId, { model: 'claude-sonnet-4-20250514' }); + * // Resume default conversation on an agent + * const session = resumeSession(agentId); + * + * // Resume specific conversation + * const session = resumeSession('conv-xxx'); + * + * // Create new conversation on specific agent + * const session = createSession(agentId); + * + * // One-shot prompt (uses default agent) + * const result = await prompt('Hello'); + * const result = await prompt('Hello', agentId); // specific agent * ``` */ @@ -46,92 +52,96 @@ export type { export { Session } from "./session.js"; /** - * Create a new session with a fresh Letta agent. - * - * The agent will have persistent memory that survives across sessions. - * Use `resumeSession` to continue a conversation with an existing agent. + * Create a new agent with a default conversation. + * Returns the agentId which can be used with resumeSession or createSession. * * @example * ```typescript - * await using session = createSession({ model: 'claude-sonnet-4-20250514' }); - * await session.send('My name is Alice'); - * for await (const msg of session.stream()) { - * console.log(msg); - * } - * console.log(`Agent ID: ${session.agentId}`); // Save this to resume later + * const agentId = await createAgent(); + * + * // Then resume the default conversation: + * const session = resumeSession(agentId); * ``` */ -export function createSession(options: SessionOptions = {}): Session { - return new Session(options); +export async function createAgent(): Promise { + const session = new Session({ createOnly: true }); + const initMsg = await session.initialize(); + session.close(); + return initMsg.agentId; } /** - * Resume an existing session with a Letta agent. + * Create a new conversation (session). * - * Unlike Claude Agent SDK (ephemeral sessions), Letta agents have persistent - * memory. You can resume a conversation days later and the agent will remember. + * - Without agentId: uses default/LRU agent with new conversation (like `letta`) + * - With agentId: creates new conversation on specified agent * * @example * ```typescript - * // Days later... - * await using session = resumeSession(agentId, { model: 'claude-sonnet-4-20250514' }); - * await session.send('What is my name?'); - * for await (const msg of session.stream()) { - * // Agent remembers: "Your name is Alice" - * } + * // New conversation on default agent (like `letta`) + * await using session = createSession(); + * + * // New conversation on specific agent + * await using session = createSession(agentId); + * ``` + */ +export function createSession(agentId?: string, options: SessionOptions = {}): Session { + if (agentId) { + return new Session({ ...options, agentId, newConversation: true }); + } else { + return new Session({ ...options, newConversation: true }); + } +} + +/** + * Resume an existing session. + * + * - Pass an agent ID (agent-xxx) to resume the default conversation + * - Pass a conversation ID (conv-xxx) to resume a specific conversation + * + * The default conversation always exists after createAgent, so you can: + * `createAgent()` → `resumeSession(agentId)` without needing createSession first. + * + * @example + * ```typescript + * // Resume default conversation + * await using session = resumeSession(agentId); + * + * // Resume specific conversation + * await using session = resumeSession('conv-xxx'); * ``` */ export function resumeSession( - agentId: string, + id: string, options: SessionOptions = {} ): Session { - return new Session({ ...options, agentId }); -} - -/** - * Resume an existing conversation. - * - * Conversations are threads within an agent. The agent is derived automatically - * from the conversation ID. Use this to continue a specific conversation thread. - * - * @example - * ```typescript - * // Resume a specific conversation - * await using session = resumeConversation(conversationId); - * await session.send('Continue our discussion...'); - * for await (const msg of session.stream()) { - * console.log(msg); - * } - * ``` - */ -export function resumeConversation( - conversationId: string, - options: SessionOptions = {} -): Session { - return new Session({ ...options, conversationId }); + if (id.startsWith("conv-")) { + return new Session({ ...options, conversationId: id }); + } else { + return new Session({ ...options, agentId: id, defaultConversation: true }); + } } /** * One-shot prompt convenience function. * - * Creates a session, sends the prompt, collects the response, and closes. - * Returns the final result message. + * - Without agentId: uses default agent (like `letta -p`), new conversation + * - With agentId: uses specific agent, new conversation * * @example * ```typescript - * const result = await prompt('What is the capital of France?', { - * model: 'claude-sonnet-4-20250514' - * }); - * if (result.success) { - * console.log(result.result); - * } + * const result = await prompt('What is 2+2?'); // default agent + * const result = await prompt('What is the capital of France?', agentId); // specific agent * ``` */ export async function prompt( message: string, - options: SessionOptions = {} + agentId?: string ): Promise { - const session = createSession(options); + // Use default agent behavior (like letta -p) when no agentId specified + const session = agentId + ? createSession(agentId) + : new Session({ promptMode: true }); try { await session.send(message); diff --git a/src/transport.ts b/src/transport.ts index 92da92f..1718e61 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -200,10 +200,7 @@ export class SubprocessTransport { } // Conversation and agent handling - if (this.options.continue) { - // Resume last session (agent + conversation) - args.push("--continue"); - } else if (this.options.conversationId) { + if (this.options.conversationId) { // Resume specific conversation (derives agent automatically) args.push("--conversation", this.options.conversationId); } else if (this.options.agentId) { @@ -216,14 +213,17 @@ export class SubprocessTransport { // Use agent's default conversation explicitly args.push("--default"); } - } else { - // Create new agent + } else if (this.options.promptMode) { + // prompt() without agentId: no agent flags + // Headless will use LRU agent or create Memo (like `letta -p "msg"`) + } else if (this.options.createOnly) { + // createAgent() - explicitly create new agent args.push("--new-agent"); - if (this.options.newConversation) { - // Also create new conversation (not default) - args.push("--new"); - } + } else if (this.options.newConversation) { + // createSession() without agentId - LRU agent + new conversation + args.push("--new"); } + // else: no agent flags = default behavior (LRU agent, default conversation) // Model if (this.options.model) { diff --git a/src/types.ts b/src/types.ts index 93edb17..897123c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -100,25 +100,19 @@ export interface SessionOptions { /** Model to use (e.g., "claude-sonnet-4-20250514") */ model?: string; - /** Resume a specific conversation by ID (derives agent automatically) */ - conversationId?: string; - - /** Create a new conversation for concurrent sessions (requires agentId) */ - newConversation?: boolean; - - /** Resume the last session (agent + conversation from previous run) */ - continue?: boolean; - - /** Use agent's default conversation (requires agentId) */ - defaultConversation?: boolean; + // ═══════════════════════════════════════════════════════════════ + // 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 configuration. * - string: Use as the complete system prompt * - { type: 'preset', preset, append? }: Use a preset with optional appended text - * - * Available presets: 'default', 'letta-claude', 'letta-codex', 'letta-gemini', - * 'claude', 'codex', 'gemini' */ systemPrompt?: SystemPromptConfig; @@ -127,27 +121,16 @@ export interface SessionOptions { * - string: Preset block name ("project", "persona", "human") * - CreateBlock: Custom block definition * - { blockId: string }: Reference to existing shared block - * - * If not specified, defaults to ["persona", "human", "project"]. - * Core blocks (skills, loaded_skills) are always included automatically. */ memory?: MemoryItem[]; - /** - * Convenience: Set persona block value directly. - * Uses default block description/limit, just overrides the value. - * Error if persona not included in memory config. - */ + /** Convenience: Set persona block value directly */ persona?: string; - /** - * Convenience: Set human block value directly. - */ + /** Convenience: Set human block value directly */ human?: string; - /** - * Convenience: Set project block value directly. - */ + /** Convenience: Set project block value directly */ project?: string; /** List of allowed tool names */ diff --git a/src/validation.ts b/src/validation.ts index 98fea14..25f6e40 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -80,20 +80,6 @@ export function validateSessionOptions(options: SessionOptions): void { ); } - if (options.continue && options.conversationId) { - throw new Error( - "Cannot use both 'continue' and 'conversationId'. " + - "Use continue to resume the last session, or conversationId to resume a specific conversation." - ); - } - - if (options.continue && options.newConversation) { - throw new Error( - "Cannot use both 'continue' and 'newConversation'. " + - "Use continue to resume the last session, or newConversation to create a new one." - ); - } - if (options.defaultConversation && options.conversationId) { throw new Error( "Cannot use both 'defaultConversation' and 'conversationId'. " +