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

@@ -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
}