feat: refactor options types for createAgent and createSession (#20)

This commit is contained in:
Christina Tong
2026-02-03 15:27:32 -08:00
committed by GitHub
parent fa4a6340e7
commit 4cb8af7be8
7 changed files with 208 additions and 129 deletions

View File

@@ -43,8 +43,12 @@ const result2 = await prompt('Run: echo hello', agentId);
```typescript
import { createAgent, resumeSession } from '@letta-ai/letta-code-sdk';
// Create an agent (has default conversation)
const agentId = await createAgent();
// Create an agent with custom memory (has default conversation)
const agentId = await createAgent({
memory: ['persona', 'project'],
persona: 'You are a helpful coding assistant',
project: 'A TypeScript web application'
});
// Resume the default conversation
await using session = resumeSession(agentId);
@@ -219,12 +223,10 @@ for await (const msg of session.stream()) { /* ... */ }
| Function | Description |
|----------|-------------|
| `createAgent()` | Create new agent with default conversation, returns `agentId` |
| `createSession()` | New conversation on default agent |
| `createSession(agentId)` | New conversation on specified agent |
| `createAgent(options?)` | Create new agent with custom memory/prompt. No options = blank agent with default memory blocks. Returns `agentId` |
| `createSession(agentId?, options?)` | New conversation on specified agent. No agentId = uses LRU agent (or creates "Memo" if none exists) |
| `resumeSession(id, options?)` | Resume session - pass `agent-xxx` for default conv, `conv-xxx` for specific conv |
| `prompt(message)` | One-shot query with default agent (like `letta -p`) |
| `prompt(message, agentId)` | One-shot query with specific agent |
| `prompt(message, agentId?)` | One-shot query with default/specified agent (like `letta -p`) |
### Session
@@ -239,24 +241,37 @@ for await (const msg of session.stream()) { /* ... */ }
### Options
```typescript
interface SessionOptions {
model?: string;
systemPrompt?: string | { type: 'preset'; preset: string; append?: string };
memory?: Array<string | CreateBlock | { blockId: string }>;
persona?: string;
human?: string;
project?: string;
cwd?: string;
**CreateAgentOptions** (for `createAgent()` - full control):
// Tool configuration
allowedTools?: string[];
permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions';
canUseTool?: (toolName: string, toolInput: object) => Promise<CanUseToolResponse>;
maxTurns?: number;
}
```typescript
// Create blank agent
await createAgent();
// Create agent with custom memory and system prompt
await createAgent({
model: 'claude-sonnet-4',
systemPrompt: 'You are a helpful Python expert.',
memory: ['persona', 'project'],
persona: 'You are a senior Python developer',
project: 'FastAPI backend for a todo app'
});
```
**CreateSessionOptions** (for `createSession()` / `resumeSession()` - runtime options):
```typescript
// Start session with permissions
createSession(agentId, {
permissionMode: 'bypassPermissions',
allowedTools: ['Bash', 'Glob'],
cwd: '/path/to/project'
});
```
**Available system prompt presets:**
- `default` / `letta-claude`, `letta-codex`, `letta-gemini` - Full Letta Code prompts
- `claude`, `codex`, `gemini` - Basic (no skills/memory instructions)
### Message Types
```typescript

View File

@@ -211,20 +211,19 @@ async function testOptions() {
const modelResult = await prompt('Say "model test ok"', agentId);
console.log(` basic: ${modelResult.success ? 'PASS' : 'FAIL'} - ${modelResult.result?.slice(0, 50)}`);
// Test systemPrompt option via createSession
console.log('Testing systemPrompt option...');
// Test systemPrompt preset via createSession (only presets allowed)
console.log('Testing systemPrompt preset...');
const sysPromptSession = createSession(undefined, {
systemPrompt: 'You love penguins and always try to work penguin facts into conversations.',
systemPrompt: 'letta-claude',
permissionMode: 'bypassPermissions',
});
await sysPromptSession.send('Tell me a fun fact about penguins in one sentence.');
await sysPromptSession.send('Hello, what kind of agent are you?');
let sysPromptResponse = '';
for await (const msg of sysPromptSession.stream()) {
if (msg.type === 'result') sysPromptResponse = msg.result || '';
}
sysPromptSession.close();
const hasPenguin = sysPromptResponse.toLowerCase().includes('penguin');
console.log(` systemPrompt: ${hasPenguin ? 'PASS' : 'PARTIAL'} - ${sysPromptResponse.slice(0, 80)}`);
console.log(` systemPrompt preset: ${sysPromptResponse ? 'PASS' : 'FAIL'} - ${sysPromptResponse.slice(0, 80)}`);
// Test cwd option via createSession
console.log('Testing cwd option...');
@@ -472,7 +471,8 @@ async function testSystemPrompt() {
console.log('=== Testing System Prompt Configuration ===\n');
async function runWithSystemPrompt(msg: string, systemPrompt: any): Promise<string> {
const session = createSession(undefined, { systemPrompt, permissionMode: 'bypassPermissions' });
const agentId = await createAgent({ systemPrompt });
const session = resumeSession(agentId, { permissionMode: 'bypassPermissions' });
await session.send(msg);
let result = '';
for await (const m of session.stream()) {
@@ -532,7 +532,8 @@ async function testMemoryConfig() {
console.log('=== Testing Memory Configuration ===\n');
async function runWithMemory(msg: string, memory?: any[]): Promise<{ success: boolean; result: string }> {
const session = createSession(undefined, { memory, permissionMode: 'bypassPermissions' });
const agentId = await createAgent({ memory });
const session = resumeSession(agentId, { permissionMode: 'bypassPermissions' });
await session.send(msg);
let result = '';
let success = false;
@@ -593,7 +594,8 @@ async function testConvenienceProps() {
console.log('=== Testing Convenience Props ===\n');
async function runWithProps(msg: string, props: Record<string, any>): Promise<{ success: boolean; result: string }> {
const session = createSession(undefined, { ...props, permissionMode: 'bypassPermissions' });
const agentId = await createAgent(props);
const session = resumeSession(agentId, { permissionMode: 'bypassPermissions' });
await session.send(msg);
let result = '';
let success = false;

View File

@@ -29,11 +29,13 @@
*/
import { Session } from "./session.js";
import type { SessionOptions, SDKMessage, SDKResultMessage } from "./types.js";
import type { CreateSessionOptions, CreateAgentOptions, SDKResultMessage } from "./types.js";
import { validateCreateSessionOptions, validateCreateAgentOptions } from "./validation.js";
// Re-export types
export type {
SessionOptions,
CreateSessionOptions,
CreateAgentOptions,
SDKMessage,
SDKInitMessage,
SDKAssistantMessage,
@@ -62,14 +64,23 @@ export { Session } from "./session.js";
*
* @example
* ```typescript
* // Create agent with default settings
* const agentId = await createAgent();
*
* // Create agent with custom memory
* const agentId = await createAgent({
* memory: ['persona', 'project'],
* persona: 'You are a helpful coding assistant',
* model: 'claude-sonnet-4'
* });
*
* // Then resume the default conversation:
* const session = resumeSession(agentId);
* ```
*/
export async function createAgent(): Promise<string> {
const session = new Session({ createOnly: true });
export async function createAgent(options: CreateAgentOptions = {}): Promise<string> {
validateCreateAgentOptions(options);
const session = new Session({ ...options, createOnly: true });
const initMsg = await session.initialize();
session.close();
return initMsg.agentId;
@@ -90,7 +101,8 @@ export async function createAgent(): Promise<string> {
* await using session = createSession(agentId);
* ```
*/
export function createSession(agentId?: string, options: SessionOptions = {}): Session {
export function createSession(agentId?: string, options: CreateSessionOptions = {}): Session {
validateCreateSessionOptions(options);
if (agentId) {
return new Session({ ...options, agentId, newConversation: true });
} else {
@@ -118,8 +130,9 @@ export function createSession(agentId?: string, options: SessionOptions = {}): S
*/
export function resumeSession(
id: string,
options: SessionOptions = {}
options: CreateSessionOptions = {}
): Session {
validateCreateSessionOptions(options);
if (id.startsWith("conv-")) {
return new Session({ ...options, conversationId: id });
} else {

View File

@@ -7,7 +7,7 @@
import { SubprocessTransport } from "./transport.js";
import type {
SessionOptions,
InternalSessionOptions,
SDKMessage,
SDKInitMessage,
SDKAssistantMessage,
@@ -20,7 +20,7 @@ import type {
CanUseToolResponseDeny,
SendMessage,
} from "./types.js";
import { validateSessionOptions } from "./validation.js";
export class Session implements AsyncDisposable {
private transport: SubprocessTransport;
@@ -30,10 +30,9 @@ export class Session implements AsyncDisposable {
private initialized = false;
constructor(
private options: SessionOptions & { agentId?: string } = {}
private options: InternalSessionOptions = {}
) {
// Validate options before creating transport
validateSessionOptions(options);
// Note: Validation happens in public API functions (createSession, createAgent, etc.)
this.transport = new SubprocessTransport(options);
}

View File

@@ -6,7 +6,7 @@
import { spawn, type ChildProcess } from "node:child_process";
import { createInterface, type Interface } from "node:readline";
import type { SessionOptions, WireMessage } from "./types.js";
import type { InternalSessionOptions, WireMessage } from "./types.js";
export class SubprocessTransport {
private process: ChildProcess | null = null;
@@ -17,7 +17,7 @@ export class SubprocessTransport {
private agentId?: string;
constructor(
private options: SessionOptions & { agentId?: string } = {}
private options: InternalSessionOptions = {}
) {}
/**
@@ -170,34 +170,7 @@ export class SubprocessTransport {
"stream-json",
];
// Validate conversation + agent combinations
// (These require agentId context, so can't be in validateSessionOptions)
// conversationId (non-default) cannot be used with agentId
if (this.options.conversationId &&
this.options.conversationId !== "default" &&
this.options.agentId) {
throw new Error(
"Cannot use both 'conversationId' and 'agentId'. " +
"When resuming a conversation, the agent is derived automatically."
);
}
// conversationId: "default" requires agentId
if (this.options.conversationId === "default" && !this.options.agentId) {
throw new Error(
"conversationId 'default' requires agentId. " +
"Use resumeSession(agentId, { defaultConversation: true }) instead."
);
}
// defaultConversation requires agentId
if (this.options.defaultConversation && !this.options.agentId) {
throw new Error(
"'defaultConversation' requires agentId. " +
"Use resumeSession(agentId, { defaultConversation: true })."
);
}
// Note: All validation happens in validateInternalSessionOptions() called from Session constructor
// Conversation and agent handling
if (this.options.conversationId) {
@@ -233,8 +206,23 @@ export class SubprocessTransport {
// System prompt configuration
if (this.options.systemPrompt !== undefined) {
if (typeof this.options.systemPrompt === "string") {
// Raw string → --system-custom
args.push("--system-custom", this.options.systemPrompt);
// Check if it's a valid preset name or custom string
const validPresets = [
"default",
"letta-claude",
"letta-codex",
"letta-gemini",
"claude",
"codex",
"gemini",
];
if (validPresets.includes(this.options.systemPrompt)) {
// Preset name → --system
args.push("--system", this.options.systemPrompt);
} else {
// Custom string → --system-custom
args.push("--system-custom", this.options.systemPrompt);
}
} else {
// Preset object → --system (+ optional --system-append)
args.push("--system", this.options.systemPrompt.preset);

View File

@@ -128,27 +128,78 @@ export type CanUseToolCallback = (
) => Promise<CanUseToolResponse> | CanUseToolResponse;
/**
* Options for creating a session
* Internal session options used by Session/Transport classes.
* Not user-facing - use CreateSessionOptions or CreateAgentOptions instead.
* @internal
*/
export interface SessionOptions {
/** Model to use (e.g., "claude-sonnet-4-20250514") */
export interface InternalSessionOptions {
// Agent/conversation routing
agentId?: string;
conversationId?: string;
newConversation?: boolean;
defaultConversation?: boolean;
createOnly?: boolean;
promptMode?: boolean;
// Agent configuration
model?: string;
systemPrompt?: SystemPromptConfig;
// Memory blocks (only for new agents)
memory?: MemoryItem[];
persona?: string;
human?: string;
project?: string;
// Permissions
allowedTools?: string[];
permissionMode?: PermissionMode;
canUseTool?: CanUseToolCallback;
// Process settings
cwd?: string;
}
export type PermissionMode = "default" | "acceptEdits" | "bypassPermissions";
/**
* Options for createSession() and resumeSession() - restricted to options that can be applied to existing agents (LRU/Memo).
* For creating new agents with custom memory/persona, use createAgent().
*/
export interface CreateSessionOptions {
/** Model to use (e.g., "claude-sonnet-4-20250514") - updates the agent's LLM config */
model?: string;
// ═══════════════════════════════════════════════════════════════
// Internal flags - set by createSession/resumeSession, not user-facing
// ═══════════════════════════════════════════════════════════════
/** @internal */ conversationId?: string;
/** @internal */ newConversation?: boolean;
/** @internal */ defaultConversation?: boolean;
/** @internal */ createOnly?: boolean;
/** @internal */ promptMode?: boolean;
/** System prompt preset (only presets, no custom strings or append) - updates the agent */
systemPrompt?: SystemPromptPreset;
/** List of allowed tool names */
allowedTools?: string[];
/** Permission mode */
permissionMode?: PermissionMode;
/** Working directory for the CLI process */
cwd?: string;
/** Custom permission callback - called when tool needs approval */
canUseTool?: CanUseToolCallback;
}
/**
* Options for createAgent() - full control over agent creation.
*/
export interface CreateAgentOptions {
/** Model to use (e.g., "claude-sonnet-4-20250514") */
model?: string;
/**
* System prompt configuration.
* - string: Use as the complete system prompt
* - SystemPromptPreset: Use a preset
* - { type: 'preset', preset, append? }: Use a preset with optional appended text
*/
systemPrompt?: SystemPromptConfig;
systemPrompt?: string | SystemPromptPreset | SystemPromptPresetConfigSDK;
/**
* Memory block configuration. Each item can be:
@@ -173,18 +224,13 @@ export interface SessionOptions {
/** Permission mode */
permissionMode?: PermissionMode;
/** Working directory */
/** Working directory for the CLI process */
cwd?: string;
/** Maximum conversation turns */
maxTurns?: number;
/** Custom permission callback - called when tool needs approval */
canUseTool?: CanUseToolCallback;
}
export type PermissionMode = "default" | "acceptEdits" | "bypassPermissions";
// ═══════════════════════════════════════════════════════════════
// SDK MESSAGE TYPES
// ═══════════════════════════════════════════════════════════════

View File

@@ -1,10 +1,16 @@
/**
* SDK Validation
*
* Validates SessionOptions before spawning the CLI.
* Validates user-provided options before spawning the CLI.
*/
import type { SessionOptions, MemoryItem, CreateBlock } from "./types.js";
import type {
CreateSessionOptions,
CreateAgentOptions,
MemoryItem,
CreateBlock,
SystemPromptPreset
} from "./types.js";
/**
* Extract block labels from memory items.
@@ -20,11 +26,41 @@ function getBlockLabels(memory: MemoryItem[]): string[] {
}
/**
* Validate SessionOptions before spawning CLI.
* Throws an error if validation fails.
* Validate systemPrompt preset value.
*/
export function validateSessionOptions(options: SessionOptions): void {
// If memory is specified, validate that convenience props match included blocks
function validateSystemPromptPreset(preset: string): void {
const validPresets = [
"default",
"letta-claude",
"letta-codex",
"letta-gemini",
"claude",
"codex",
"gemini",
];
if (!validPresets.includes(preset)) {
throw new Error(
`Invalid system prompt preset '${preset}'. ` +
`Valid presets: ${validPresets.join(", ")}`
);
}
}
/**
* Validate CreateSessionOptions (used by createSession and resumeSession).
*/
export function validateCreateSessionOptions(options: CreateSessionOptions): void {
// Validate systemPrompt preset if provided
if (options.systemPrompt !== undefined) {
validateSystemPromptPreset(options.systemPrompt);
}
}
/**
* Validate CreateAgentOptions (used by createAgent).
*/
export function validateCreateAgentOptions(options: CreateAgentOptions): void {
// Validate memory/persona consistency
if (options.memory !== undefined) {
const blockLabels = getBlockLabels(options.memory);
@@ -50,11 +86,17 @@ export function validateSessionOptions(options: SessionOptions): void {
}
}
// Validate systemPrompt preset if provided
// Validate systemPrompt preset if provided as preset object
if (
options.systemPrompt !== undefined &&
typeof options.systemPrompt === "object"
) {
validateSystemPromptPreset(options.systemPrompt.preset);
} else if (
options.systemPrompt !== undefined &&
typeof options.systemPrompt === "string"
) {
// Check if it's a preset name (if so, validate it)
const validPresets = [
"default",
"letta-claude",
@@ -63,36 +105,10 @@ export function validateSessionOptions(options: SessionOptions): void {
"claude",
"codex",
"gemini",
];
if (!validPresets.includes(options.systemPrompt.preset)) {
throw new Error(
`Invalid system prompt preset '${options.systemPrompt.preset}'. ` +
`Valid presets: ${validPresets.join(", ")}`
);
] as const;
if (validPresets.includes(options.systemPrompt as SystemPromptPreset)) {
validateSystemPromptPreset(options.systemPrompt);
}
// If not a preset, it's a custom string - no validation needed
}
// Validate conversation options
if (options.conversationId && options.newConversation) {
throw new Error(
"Cannot use both 'conversationId' and 'newConversation'. " +
"Use conversationId to resume a specific conversation, or newConversation to create a new one."
);
}
if (options.defaultConversation && options.conversationId) {
throw new Error(
"Cannot use both 'defaultConversation' and 'conversationId'. " +
"Use defaultConversation with agentId, or conversationId alone."
);
}
if (options.defaultConversation && options.newConversation) {
throw new Error(
"Cannot use both 'defaultConversation' and 'newConversation'."
);
}
// Note: Validations that require agentId context happen in transport.ts buildArgs()
// because agentId is passed separately to resumeSession(), not in SessionOptions
}