feat: refactor options types for createAgent and createSession (#20)
This commit is contained in:
59
README.md
59
README.md
@@ -43,8 +43,12 @@ const result2 = await prompt('Run: echo hello', agentId);
|
||||
```typescript
|
||||
import { createAgent, resumeSession } from '@letta-ai/letta-code-sdk';
|
||||
|
||||
// Create an agent (has default conversation)
|
||||
const agentId = await createAgent();
|
||||
// Create an agent with custom memory (has default conversation)
|
||||
const agentId = await createAgent({
|
||||
memory: ['persona', 'project'],
|
||||
persona: 'You are a helpful coding assistant',
|
||||
project: 'A TypeScript web application'
|
||||
});
|
||||
|
||||
// Resume the default conversation
|
||||
await using session = resumeSession(agentId);
|
||||
@@ -219,12 +223,10 @@ for await (const msg of session.stream()) { /* ... */ }
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `createAgent()` | Create new agent with default conversation, returns `agentId` |
|
||||
| `createSession()` | New conversation on default agent |
|
||||
| `createSession(agentId)` | New conversation on specified agent |
|
||||
| `createAgent(options?)` | Create new agent with custom memory/prompt. No options = blank agent with default memory blocks. Returns `agentId` |
|
||||
| `createSession(agentId?, options?)` | New conversation on specified agent. No agentId = uses LRU agent (or creates "Memo" if none exists) |
|
||||
| `resumeSession(id, options?)` | Resume session - pass `agent-xxx` for default conv, `conv-xxx` for specific conv |
|
||||
| `prompt(message)` | One-shot query with default agent (like `letta -p`) |
|
||||
| `prompt(message, agentId)` | One-shot query with specific agent |
|
||||
| `prompt(message, agentId?)` | One-shot query with default/specified agent (like `letta -p`) |
|
||||
|
||||
### Session
|
||||
|
||||
@@ -239,24 +241,37 @@ for await (const msg of session.stream()) { /* ... */ }
|
||||
|
||||
### Options
|
||||
|
||||
```typescript
|
||||
interface SessionOptions {
|
||||
model?: string;
|
||||
systemPrompt?: string | { type: 'preset'; preset: string; append?: string };
|
||||
memory?: Array<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
|
||||
|
||||
@@ -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;
|
||||
|
||||
25
src/index.ts
25
src/index.ts
@@ -29,11 +29,13 @@
|
||||
*/
|
||||
|
||||
import { Session } from "./session.js";
|
||||
import type { SessionOptions, SDKMessage, SDKResultMessage } from "./types.js";
|
||||
import type { CreateSessionOptions, CreateAgentOptions, SDKResultMessage } from "./types.js";
|
||||
import { validateCreateSessionOptions, validateCreateAgentOptions } from "./validation.js";
|
||||
|
||||
// Re-export types
|
||||
export type {
|
||||
SessionOptions,
|
||||
CreateSessionOptions,
|
||||
CreateAgentOptions,
|
||||
SDKMessage,
|
||||
SDKInitMessage,
|
||||
SDKAssistantMessage,
|
||||
@@ -62,14 +64,23 @@ export { Session } from "./session.js";
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Create agent with default settings
|
||||
* const agentId = await createAgent();
|
||||
*
|
||||
* // Create agent with custom memory
|
||||
* const agentId = await createAgent({
|
||||
* memory: ['persona', 'project'],
|
||||
* persona: 'You are a helpful coding assistant',
|
||||
* model: 'claude-sonnet-4'
|
||||
* });
|
||||
*
|
||||
* // Then resume the default conversation:
|
||||
* const session = resumeSession(agentId);
|
||||
* ```
|
||||
*/
|
||||
export async function createAgent(): Promise<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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
82
src/types.ts
82
src/types.ts
@@ -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
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user