feat: add createAgent and refactor (#13)

This commit is contained in:
Christina Tong
2026-01-30 17:03:47 -08:00
committed by GitHub
parent b26c8eadaa
commit 0e657e5109
6 changed files with 424 additions and 450 deletions

168
README.md
View File

@@ -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<string | CreateBlock | { blockId: string }>;
// 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<CanUseToolResponse>;
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.)

View File

@@ -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<string> {
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<string> {
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<string, any>): 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();

View File

@@ -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<string> {
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<SDKResultMessage> {
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);

View File

@@ -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) {

View File

@@ -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 */

View File

@@ -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'. " +