feat: add createAgent and refactor (#13)
This commit is contained in:
168
README.md
168
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<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.)
|
||||
|
||||
@@ -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();
|
||||
|
||||
144
src/index.ts
144
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<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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
39
src/types.ts
39
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 */
|
||||
|
||||
@@ -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'. " +
|
||||
|
||||
Reference in New Issue
Block a user