diff --git a/build.js b/build.js index a602ff2..e62a831 100644 --- a/build.js +++ b/build.js @@ -78,7 +78,7 @@ if (existsSync(bundledSkillsSrc)) { // Generate type declarations for wire types export console.log("📝 Generating type declarations..."); await Bun.$`bunx tsc -p tsconfig.types.json`; -console.log(" Output: dist/types/wire.d.ts"); +console.log(" Output: dist/types/protocol.d.ts"); console.log("✅ Build complete!"); console.log(` Output: letta.js`); diff --git a/package.json b/package.json index f868e1f..4e81c61 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ ], "exports": { ".": "./letta.js", - "./wire-types": { - "types": "./dist/types/wire.d.ts" + "./protocol": { + "types": "./dist/types/protocol.d.ts" } }, "repository": { diff --git a/src/agent/create.ts b/src/agent/create.ts index ae5bed3..aa703b5 100644 --- a/src/agent/create.ts +++ b/src/agent/create.ts @@ -26,7 +26,7 @@ import { discoverSkills, formatSkillsForMemory, SKILLS_DIR } from "./skills"; */ export interface BlockProvenance { label: string; - source: "global" | "project" | "new"; + source: "global" | "project" | "new" | "shared"; } /** @@ -45,24 +45,75 @@ export interface CreateAgentResult { provenance: AgentProvenance; } +export interface CreateAgentOptions { + name?: string; + model?: string; + embeddingModel?: string; + updateArgs?: Record; + skillsDirectory?: string; + parallelToolCalls?: boolean; + enableSleeptime?: boolean; + /** System prompt preset (e.g., 'default', 'letta-claude', 'letta-codex') */ + systemPromptPreset?: string; + /** Raw system prompt string (mutually exclusive with systemPromptPreset) */ + systemPromptCustom?: string; + /** Additional text to append to the resolved system prompt */ + systemPromptAppend?: string; + /** Block labels to initialize (from default blocks) */ + initBlocks?: string[]; + /** Base tools to include */ + baseTools?: string[]; + /** Custom memory blocks (overrides default blocks) */ + memoryBlocks?: Array< + { label: string; value: string; description?: string } | { blockId: string } + >; + /** Override values for preset blocks (label → value) */ + blockValues?: Record; +} + export async function createAgent( - name = DEFAULT_AGENT_NAME, + nameOrOptions: string | CreateAgentOptions = DEFAULT_AGENT_NAME, model?: string, embeddingModel = "openai/text-embedding-3-small", updateArgs?: Record, skillsDirectory?: string, parallelToolCalls = true, enableSleeptime = false, - systemPromptId?: string, + systemPromptPreset?: string, initBlocks?: string[], baseTools?: string[], ) { + // Support both old positional args and new options object + let options: CreateAgentOptions; + if (typeof nameOrOptions === "object") { + options = nameOrOptions; + } else { + options = { + name: nameOrOptions, + model, + embeddingModel, + updateArgs, + skillsDirectory, + parallelToolCalls, + enableSleeptime, + systemPromptPreset, + initBlocks, + baseTools, + }; + } + + const name = options.name ?? DEFAULT_AGENT_NAME; + const embeddingModelVal = + options.embeddingModel ?? "openai/text-embedding-3-small"; + const parallelToolCallsVal = options.parallelToolCalls ?? true; + const enableSleeptimeVal = options.enableSleeptime ?? false; + // Resolve model identifier to handle let modelHandle: string; - if (model) { - const resolved = resolveModel(model); + if (options.model) { + const resolved = resolveModel(options.model); if (!resolved) { - console.error(`Error: Unknown model "${model}"`); + console.error(`Error: Unknown model "${options.model}"`); console.error("Available models:"); console.error(formatAvailableModels()); process.exit(1); @@ -79,14 +130,12 @@ export async function createAgent( // Map internal names to server names so the agent sees the correct tool names const { getServerToolName } = await import("../tools/manager"); const internalToolNames = getToolNames(); - const serverToolNames = internalToolNames.map((name) => - getServerToolName(name), - ); + const serverToolNames = internalToolNames.map((n) => getServerToolName(n)); const baseMemoryTool = modelHandle.startsWith("openai/gpt-5") ? "memory_apply_patch" : "memory"; - const defaultBaseTools = baseTools ?? [ + const defaultBaseTools = options.baseTools ?? [ baseMemoryTool, "web_search", "conversation_search", @@ -122,37 +171,79 @@ export async function createAgent( } } - // Load memory blocks from .mdx files - const defaultMemoryBlocks = - initBlocks && initBlocks.length === 0 ? [] : await getDefaultMemoryBlocks(); + // Determine which memory blocks to use: + // 1. If options.memoryBlocks is provided, use those (custom blocks and/or block references) + // 2. Otherwise, use default blocks filtered by options.initBlocks - // Optional filter: only initialize a subset of memory blocks on creation - const allowedBlockLabels = initBlocks - ? new Set( - initBlocks.map((name) => name.trim()).filter((name) => name.length > 0), - ) - : undefined; + // Separate block references from blocks to create + const referencedBlockIds: string[] = []; + let filteredMemoryBlocks: Array<{ + label: string; + value: string; + description?: string | null; + limit?: number; + }>; - if (allowedBlockLabels && allowedBlockLabels.size > 0) { - const knownLabels = new Set(defaultMemoryBlocks.map((b) => b.label)); - for (const label of Array.from(allowedBlockLabels)) { - if (!knownLabels.has(label)) { + if (options.memoryBlocks !== undefined) { + // Separate blockId references from CreateBlock items + const createBlocks: typeof filteredMemoryBlocks = []; + for (const item of options.memoryBlocks) { + if ("blockId" in item) { + referencedBlockIds.push(item.blockId); + } else { + createBlocks.push(item as (typeof filteredMemoryBlocks)[0]); + } + } + filteredMemoryBlocks = createBlocks; + } else { + // Load memory blocks from .mdx files + const defaultMemoryBlocks = + options.initBlocks && options.initBlocks.length === 0 + ? [] + : await getDefaultMemoryBlocks(); + + // Optional filter: only initialize a subset of memory blocks on creation + const allowedBlockLabels = options.initBlocks + ? new Set( + options.initBlocks.map((n) => n.trim()).filter((n) => n.length > 0), + ) + : undefined; + + if (allowedBlockLabels && allowedBlockLabels.size > 0) { + const knownLabels = new Set(defaultMemoryBlocks.map((b) => b.label)); + for (const label of Array.from(allowedBlockLabels)) { + if (!knownLabels.has(label)) { + console.warn( + `Ignoring unknown init block "${label}". Valid blocks: ${Array.from(knownLabels).join(", ")}`, + ); + allowedBlockLabels.delete(label); + } + } + } + + filteredMemoryBlocks = + allowedBlockLabels && allowedBlockLabels.size > 0 + ? defaultMemoryBlocks.filter((b) => allowedBlockLabels.has(b.label)) + : defaultMemoryBlocks; + } + + // Apply blockValues overrides to preset blocks + if (options.blockValues) { + for (const [label, value] of Object.entries(options.blockValues)) { + const block = filteredMemoryBlocks.find((b) => b.label === label); + if (block) { + block.value = value; + } else { console.warn( - `Ignoring unknown init block "${label}". Valid blocks: ${Array.from(knownLabels).join(", ")}`, + `Ignoring --block-value for "${label}" - block not included in memory config`, ); - allowedBlockLabels.delete(label); } } } - const filteredMemoryBlocks = - allowedBlockLabels && allowedBlockLabels.size > 0 - ? defaultMemoryBlocks.filter((b) => allowedBlockLabels.has(b.label)) - : defaultMemoryBlocks; - // Resolve absolute path for skills directory const resolvedSkillsDirectory = - skillsDirectory || join(process.cwd(), SKILLS_DIR); + options.skillsDirectory || join(process.cwd(), SKILLS_DIR); // Discover skills from .skills directory and populate skills memory block try { @@ -198,12 +289,31 @@ export async function createAgent( } } + // Add any referenced block IDs (existing blocks to attach) + for (const blockId of referencedBlockIds) { + blockIds.push(blockId); + blockProvenance.push({ label: blockId, source: "shared" }); + } + // Get the model's context window from its configuration const modelUpdateArgs = getModelUpdateArgs(modelHandle); const contextWindow = (modelUpdateArgs?.context_window as number) || 200_000; - // Resolve system prompt ID to content - const systemPromptContent = await resolveSystemPrompt(systemPromptId); + // Resolve system prompt content: + // 1. If systemPromptCustom is provided, use it as-is + // 2. Otherwise, resolve systemPromptPreset to content + // 3. If systemPromptAppend is provided, append it to the result + let systemPromptContent: string; + if (options.systemPromptCustom) { + systemPromptContent = options.systemPromptCustom; + } else { + systemPromptContent = await resolveSystemPrompt(options.systemPromptPreset); + } + + // Append additional instructions if provided + if (options.systemPromptAppend) { + systemPromptContent = `${systemPromptContent}\n\n${options.systemPromptAppend}`; + } // Create agent with all block IDs (existing + newly created) const tags = ["origin:letta-code"]; @@ -216,7 +326,7 @@ export async function createAgent( system: systemPromptContent, name, description: `Letta Code agent created in ${process.cwd()}`, - embedding: embeddingModel, + embedding: embeddingModelVal, model: modelHandle, context_window_limit: contextWindow, tools: toolNames, @@ -226,8 +336,8 @@ export async function createAgent( include_base_tools: false, include_base_tool_rules: false, initial_message_sequence: [], - parallel_tool_calls: parallelToolCalls, - enable_sleeptime: enableSleeptime, + parallel_tool_calls: parallelToolCallsVal, + enable_sleeptime: enableSleeptimeVal, }); // Note: Preflight check above falls back to 'memory' when 'memory_apply_patch' is unavailable. @@ -235,8 +345,8 @@ export async function createAgent( // Apply updateArgs if provided (e.g., context_window, reasoning_effort, verbosity, etc.) // We intentionally pass context_window through so updateAgentLLMConfig can set // context_window_limit using the latest server API, avoiding any fallback. - if (updateArgs && Object.keys(updateArgs).length > 0) { - await updateAgentLLMConfig(agent.id, modelHandle, updateArgs); + if (options.updateArgs && Object.keys(options.updateArgs).length > 0) { + await updateAgentLLMConfig(agent.id, modelHandle, options.updateArgs); } // Always retrieve the agent to ensure we get the full state with populated memory blocks @@ -245,7 +355,7 @@ export async function createAgent( }); // Update persona block for sleeptime agent - if (enableSleeptime && fullAgent.managed_group) { + if (enableSleeptimeVal && fullAgent.managed_group) { // Find the sleeptime agent in the managed group by checking agent_type for (const groupAgentId of fullAgent.managed_group.agent_ids) { try { diff --git a/src/agent/promptAssets.ts b/src/agent/promptAssets.ts index 5a8056e..aa1a656 100644 --- a/src/agent/promptAssets.ts +++ b/src/agent/promptAssets.ts @@ -56,47 +56,48 @@ export const SYSTEM_PROMPTS: SystemPromptOption[] = [ { id: "default", label: "Default", - description: "Standard Letta Code system prompt (Claude-optimized)", + description: "Alias for letta-claude", content: lettaAnthropicPrompt, isDefault: true, isFeatured: true, }, { - id: "legacy", - label: "Legacy", - description: "Original system prompt", - content: systemPrompt, + id: "letta-claude", + label: "Letta Claude", + description: "Full Letta Code system prompt (Claude-optimized)", + content: lettaAnthropicPrompt, + isFeatured: true, }, { id: "letta-codex", - label: "Codex", - description: "For Codex models", + label: "Letta Codex", + description: "Full Letta Code system prompt (Codex-optimized)", content: lettaCodexPrompt, isFeatured: true, }, { id: "letta-gemini", - label: "Gemini", - description: "For Gemini models", + label: "Letta Gemini", + description: "Full Letta Code system prompt (Gemini-optimized)", content: lettaGeminiPrompt, isFeatured: true, }, { - id: "anthropic", + id: "claude", label: "Claude (basic)", - description: "For Claude models (no skills/memory instructions)", + description: "Basic Claude prompt (no skills/memory instructions)", content: anthropicPrompt, }, { id: "codex", label: "Codex (basic)", - description: "For Codex models (no skills/memory instructions)", + description: "Basic Codex prompt (no skills/memory instructions)", content: codexPrompt, }, { id: "gemini", label: "Gemini (basic)", - description: "For Gemini models (no skills/memory instructions)", + description: "Basic Gemini prompt (no skills/memory instructions)", content: geminiPrompt, }, ]; @@ -109,19 +110,19 @@ export const SYSTEM_PROMPTS: SystemPromptOption[] = [ * 2. If it matches a subagent name, use that subagent's system prompt * 3. Otherwise, use the default system prompt * - * @param systemPromptId - The system prompt ID (e.g., "codex") or subagent name (e.g., "explore") + * @param systemPromptPreset - The system prompt preset (e.g., "letta-claude") or subagent name (e.g., "explore") * @returns The resolved system prompt content */ export async function resolveSystemPrompt( - systemPromptId: string | undefined, + systemPromptPreset: string | undefined, ): Promise { // No input - use default - if (!systemPromptId) { + if (!systemPromptPreset) { return SYSTEM_PROMPT; } // 1. Check if it matches a system prompt ID - const matchedPrompt = SYSTEM_PROMPTS.find((p) => p.id === systemPromptId); + const matchedPrompt = SYSTEM_PROMPTS.find((p) => p.id === systemPromptPreset); if (matchedPrompt) { return matchedPrompt.content; } @@ -129,7 +130,7 @@ export async function resolveSystemPrompt( // 2. Check if it matches a subagent name const { getAllSubagentConfigs } = await import("./subagents"); const subagentConfigs = await getAllSubagentConfigs(); - const matchedSubagent = subagentConfigs[systemPromptId]; + const matchedSubagent = subagentConfigs[systemPromptPreset]; if (matchedSubagent) { return matchedSubagent.systemPrompt; } diff --git a/src/headless.ts b/src/headless.ts index 2b29573..9416094 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -48,7 +48,7 @@ import type { RetryMessage, StreamEvent, SystemInitMessage, -} from "./types/wire"; +} from "./types/protocol"; // Maximum number of times to retry a turn when the backend // reports an `llm_api_error` stop reason. This helps smooth @@ -74,6 +74,10 @@ export async function handleHeadlessCommand( agent: { type: "string", short: "a" }, model: { type: "string", short: "m" }, system: { type: "string", short: "s" }, + "system-custom": { type: "string" }, + "system-append": { type: "string" }, + "memory-blocks": { type: "string" }, + "block-value": { type: "string", multiple: true }, toolset: { type: "string" }, prompt: { type: "boolean", short: "p" }, "output-format": { type: "string" }, @@ -169,7 +173,11 @@ export async function handleHeadlessCommand( const specifiedAgentId = values.agent as string | undefined; const shouldContinue = values.continue as boolean | undefined; const forceNew = values.new as boolean | undefined; - const systemPromptId = values.system as string | undefined; + const systemPromptPreset = values.system as string | undefined; + const systemCustom = values["system-custom"] as string | undefined; + const systemAppend = values["system-append"] as string | undefined; + const memoryBlocksJson = values["memory-blocks"] as string | undefined; + const blockValueArgs = values["block-value"] as string[] | undefined; const initBlocksRaw = values["init-blocks"] as string | undefined; const baseToolsRaw = values["base-tools"] as string | undefined; const sleeptimeFlag = (values.sleeptime as boolean | undefined) ?? undefined; @@ -231,6 +239,84 @@ export async function handleHeadlessCommand( } } + // Validate system prompt options (--system and --system-custom are mutually exclusive) + if (systemPromptPreset && systemCustom) { + console.error( + "Error: --system and --system-custom are mutually exclusive. Use one or the other.", + ); + process.exit(1); + } + + // Parse memory blocks JSON if provided + // Supports two formats: + // - CreateBlock: { label: string, value: string, description?: string } + // - BlockReference: { blockId: string } + let memoryBlocks: + | Array< + | { label: string; value: string; description?: string } + | { blockId: string } + > + | undefined; + if (memoryBlocksJson !== undefined) { + if (!forceNew) { + console.error( + "Error: --memory-blocks can only be used together with --new to provide initial memory blocks.", + ); + process.exit(1); + } + try { + memoryBlocks = JSON.parse(memoryBlocksJson); + if (!Array.isArray(memoryBlocks)) { + throw new Error("memory-blocks must be a JSON array"); + } + // Validate each block has required fields + for (const block of memoryBlocks) { + const hasBlockId = + "blockId" in block && typeof block.blockId === "string"; + const hasLabelValue = + "label" in block && + "value" in block && + typeof block.label === "string" && + typeof block.value === "string"; + + if (!hasBlockId && !hasLabelValue) { + throw new Error( + "Each memory block must have either 'blockId' (string) or 'label' and 'value' (strings)", + ); + } + } + } catch (error) { + console.error( + `Error: Invalid --memory-blocks JSON: ${error instanceof Error ? error.message : String(error)}`, + ); + process.exit(1); + } + } + + // Parse --block-value args (format: label=value) + let blockValues: Record | undefined; + if (blockValueArgs && blockValueArgs.length > 0) { + if (!forceNew) { + console.error( + "Error: --block-value can only be used together with --new to set block values.", + ); + process.exit(1); + } + blockValues = {}; + for (const arg of blockValueArgs) { + const eqIndex = arg.indexOf("="); + if (eqIndex === -1) { + console.error( + `Error: Invalid --block-value format "${arg}". Expected format: label=value`, + ); + process.exit(1); + } + const label = arg.slice(0, eqIndex); + const value = arg.slice(eqIndex + 1); + blockValues[label] = value; + } + } + // Priority 1: Import from AgentFile template if (fromAfFile) { const { importAgentFromFile } = await import("./agent/import"); @@ -254,36 +340,28 @@ export async function handleHeadlessCommand( // Priority 3: Check if --new flag was passed (skip all resume logic) if (!agent && forceNew) { const updateArgs = getModelUpdateArgs(model); + const createOptions = { + model, + updateArgs, + skillsDirectory, + parallelToolCalls: true, + enableSleeptime: sleeptimeFlag ?? settings.enableSleeptime, + systemPromptPreset, + systemPromptCustom: systemCustom, + systemPromptAppend: systemAppend, + initBlocks, + baseTools, + memoryBlocks, + blockValues, + }; try { - const result = await createAgent( - undefined, - model, - undefined, - updateArgs, - skillsDirectory, - true, // parallelToolCalls always enabled - sleeptimeFlag ?? settings.enableSleeptime, - systemPromptId, - initBlocks, - baseTools, - ); + const result = await createAgent(createOptions); agent = result.agent; } catch (err) { if (isToolsNotFoundError(err)) { console.warn("Tools missing on server, re-uploading and retrying..."); await forceUpsertTools(client, baseURL); - const result = await createAgent( - undefined, - model, - undefined, - updateArgs, - skillsDirectory, - true, - sleeptimeFlag ?? settings.enableSleeptime, - systemPromptId, - initBlocks, - baseTools, - ); + const result = await createAgent(createOptions); agent = result.agent; } else { throw err; @@ -320,36 +398,23 @@ export async function handleHeadlessCommand( // Priority 6: Create a new agent if (!agent) { const updateArgs = getModelUpdateArgs(model); + const createOptions = { + model, + updateArgs, + skillsDirectory, + parallelToolCalls: true, + enableSleeptime: sleeptimeFlag ?? settings.enableSleeptime, + systemPromptPreset, + // Note: systemCustom, systemAppend, and memoryBlocks only apply with --new flag + }; try { - const result = await createAgent( - undefined, - model, - undefined, - updateArgs, - skillsDirectory, - true, // parallelToolCalls always enabled - sleeptimeFlag ?? settings.enableSleeptime, - systemPromptId, - undefined, - undefined, - ); + const result = await createAgent(createOptions); agent = result.agent; } catch (err) { if (isToolsNotFoundError(err)) { console.warn("Tools missing on server, re-uploading and retrying..."); await forceUpsertTools(client, baseURL); - const result = await createAgent( - undefined, - model, - undefined, - updateArgs, - skillsDirectory, - true, - sleeptimeFlag ?? settings.enableSleeptime, - systemPromptId, - undefined, - undefined, - ); + const result = await createAgent(createOptions); agent = result.agent; } else { throw err; @@ -365,7 +430,7 @@ export async function handleHeadlessCommand( ); // If resuming and a model or system prompt was specified, apply those changes - if (isResumingAgent && (model || systemPromptId)) { + if (isResumingAgent && (model || systemPromptPreset)) { if (model) { const { resolveModel } = await import("./agent/model"); const modelHandle = resolveModel(model); @@ -388,9 +453,12 @@ export async function handleHeadlessCommand( } } - if (systemPromptId) { + if (systemPromptPreset) { const { updateAgentSystemPrompt } = await import("./agent/modify"); - const result = await updateAgentSystemPrompt(agent.id, systemPromptId); + const result = await updateAgentSystemPrompt( + agent.id, + systemPromptPreset, + ); if (!result.success || !result.agent) { console.error(`Failed to update system prompt: ${result.message}`); process.exit(1); diff --git a/src/index.ts b/src/index.ts index 42dfb7e..b716baf 100755 --- a/src/index.ts +++ b/src/index.ts @@ -332,6 +332,10 @@ async function main(): Promise { name: { type: "string", short: "n" }, model: { type: "string", short: "m" }, system: { type: "string", short: "s" }, + "system-custom": { type: "string" }, + "system-append": { type: "string" }, + "memory-blocks": { type: "string" }, + "block-value": { type: "string", multiple: true }, toolset: { type: "string" }, prompt: { type: "boolean", short: "p" }, run: { type: "boolean" }, @@ -406,7 +410,12 @@ async function main(): Promise { let specifiedAgentId = (values.agent as string | undefined) ?? null; const specifiedAgentName = (values.name as string | undefined) ?? null; const specifiedModel = (values.model as string | undefined) ?? undefined; - const systemPromptId = (values.system as string | undefined) ?? undefined; + const systemPromptPreset = (values.system as string | undefined) ?? undefined; + const systemCustom = + (values["system-custom"] as string | undefined) ?? undefined; + // Note: systemAppend is also parsed but only used in headless mode (headless.ts handles it) + const memoryBlocksJson = + (values["memory-blocks"] as string | undefined) ?? undefined; const specifiedToolset = (values.toolset as string | undefined) ?? undefined; const skillsDirectory = (values.skills as string | undefined) ?? undefined; const sleeptimeFlag = (values.sleeptime as boolean | undefined) ?? undefined; @@ -476,8 +485,16 @@ async function main(): Promise { process.exit(1); } - // Validate system prompt if provided (can be a system prompt ID or subagent name) - if (systemPromptId) { + // Validate system prompt options (--system and --system-custom are mutually exclusive) + if (systemPromptPreset && systemCustom) { + console.error( + "Error: --system and --system-custom are mutually exclusive. Use one or the other.", + ); + process.exit(1); + } + + // Validate system prompt preset if provided (can be a system prompt ID or subagent name) + if (systemPromptPreset) { const { SYSTEM_PROMPTS } = await import("./agent/promptAssets"); const { getAllSubagentConfigs } = await import("./agent/subagents"); @@ -485,13 +502,42 @@ async function main(): Promise { const subagentConfigs = await getAllSubagentConfigs(); const validSubagentNames = Object.keys(subagentConfigs); - const isValidSystemPrompt = validSystemPrompts.includes(systemPromptId); - const isValidSubagent = validSubagentNames.includes(systemPromptId); + const isValidSystemPrompt = validSystemPrompts.includes(systemPromptPreset); + const isValidSubagent = validSubagentNames.includes(systemPromptPreset); if (!isValidSystemPrompt && !isValidSubagent) { const allValid = [...validSystemPrompts, ...validSubagentNames]; console.error( - `Error: Invalid system prompt "${systemPromptId}". Must be one of: ${allValid.join(", ")}.`, + `Error: Invalid system prompt "${systemPromptPreset}". Must be one of: ${allValid.join(", ")}.`, + ); + process.exit(1); + } + } + + // Parse memory blocks JSON if provided + let memoryBlocks: + | Array<{ label: string; value: string; description?: string }> + | undefined; + if (memoryBlocksJson) { + try { + memoryBlocks = JSON.parse(memoryBlocksJson); + if (!Array.isArray(memoryBlocks)) { + throw new Error("memory-blocks must be a JSON array"); + } + // Validate each block has required fields + for (const block of memoryBlocks) { + if ( + typeof block.label !== "string" || + typeof block.value !== "string" + ) { + throw new Error( + "Each memory block must have 'label' and 'value' string fields", + ); + } + } + } catch (error) { + console.error( + `Error: Invalid --memory-blocks JSON: ${error instanceof Error ? error.message : String(error)}`, ); process.exit(1); } @@ -745,7 +791,7 @@ async function main(): Promise { baseTools, agentIdArg, model, - systemPromptId, + systemPromptPreset, toolset, skillsDirectory, fromAfFile, @@ -756,7 +802,7 @@ async function main(): Promise { baseTools?: string[]; agentIdArg: string | null; model?: string; - systemPromptId?: string; + systemPromptPreset?: string; toolset?: "codex" | "default" | "gemini"; skillsDirectory?: string; fromAfFile?: string; @@ -1027,13 +1073,13 @@ async function main(): Promise { agent = await client.agents.retrieve(agentIdArg); // Apply --system flag to existing agent if provided - if (systemPromptId) { + if (systemPromptPreset) { const { updateAgentSystemPrompt } = await import( "./agent/modify" ); const result = await updateAgentSystemPrompt( agent.id, - systemPromptId, + systemPromptPreset, ); if (!result.success || !result.agent) { console.error( @@ -1067,7 +1113,7 @@ async function main(): Promise { skillsDirectory, true, // parallelToolCalls always enabled sleeptimeFlag ?? settings.enableSleeptime, - systemPromptId, + systemPromptPreset, initBlocks, baseTools, ); @@ -1089,7 +1135,7 @@ async function main(): Promise { skillsDirectory, true, sleeptimeFlag ?? settings.enableSleeptime, - systemPromptId, + systemPromptPreset, initBlocks, baseTools, ); @@ -1144,7 +1190,7 @@ async function main(): Promise { skillsDirectory, true, // parallelToolCalls always enabled sleeptimeFlag ?? settings.enableSleeptime, - systemPromptId, + systemPromptPreset, undefined, undefined, ); @@ -1166,7 +1212,7 @@ async function main(): Promise { skillsDirectory, true, sleeptimeFlag ?? settings.enableSleeptime, - systemPromptId, + systemPromptPreset, undefined, undefined, ); @@ -1247,7 +1293,7 @@ async function main(): Promise { setIsResumingSession(resuming); // If resuming and a model or system prompt was specified, apply those changes - if (resuming && (model || systemPromptId)) { + if (resuming && (model || systemPromptPreset)) { if (model) { const { resolveModel } = await import("./agent/model"); const modelHandle = resolveModel(model); @@ -1271,11 +1317,11 @@ async function main(): Promise { } } - if (systemPromptId) { + if (systemPromptPreset) { const { updateAgentSystemPrompt } = await import("./agent/modify"); const result = await updateAgentSystemPrompt( agent.id, - systemPromptId, + systemPromptPreset, ); if (!result.success || !result.agent) { console.error(`Error: ${result.message}`); @@ -1312,7 +1358,7 @@ async function main(): Promise { forceNew, agentIdArg, model, - systemPromptId, + systemPromptPreset, fromAfFile, loadingState, selectedGlobalAgentId, @@ -1384,7 +1430,7 @@ async function main(): Promise { baseTools: baseTools, agentIdArg: specifiedAgentId, model: specifiedModel, - systemPromptId: systemPromptId, + systemPromptPreset: systemPromptPreset, toolset: specifiedToolset as "codex" | "default" | "gemini" | undefined, skillsDirectory: skillsDirectory, fromAfFile: fromAfFile, diff --git a/src/tests/headless-input-format.test.ts b/src/tests/headless-input-format.test.ts index f3987a7..b42abe0 100644 --- a/src/tests/headless-input-format.test.ts +++ b/src/tests/headless-input-format.test.ts @@ -7,7 +7,7 @@ import type { StreamEvent, SystemInitMessage, WireMessage, -} from "../types/wire"; +} from "../types/protocol"; /** * Tests for --input-format stream-json bidirectional communication. diff --git a/src/tests/headless-stream-json-format.test.ts b/src/tests/headless-stream-json-format.test.ts index 4cd3cfd..37dddca 100644 --- a/src/tests/headless-stream-json-format.test.ts +++ b/src/tests/headless-stream-json-format.test.ts @@ -4,7 +4,7 @@ import type { ResultMessage, StreamEvent, SystemInitMessage, -} from "../types/wire"; +} from "../types/protocol"; /** * Tests for stream-json output format. diff --git a/src/types/wire.ts b/src/types/protocol.ts similarity index 85% rename from src/types/wire.ts rename to src/types/protocol.ts index 77bb4d6..3d60ae5 100644 --- a/src/types/wire.ts +++ b/src/types/protocol.ts @@ -1,9 +1,9 @@ /** - * Wire Format Types + * Protocol Types for Letta Code * - * These types define the JSON structure emitted by headless.ts when running - * in stream-json mode. They enable typed consumption of the bidirectional - * JSON protocol. + * These types define: + * 1. The JSON structure emitted by headless.ts in stream-json mode (wire protocol) + * 2. Configuration types for session options (used internally and by SDK) * * Design principle: Compose from @letta-ai/letta-client types where possible. */ @@ -16,6 +16,7 @@ import type { ToolCallMessage as LettaToolCallMessage, ToolCall, } from "@letta-ai/letta-client/resources/agents/messages"; +import type { CreateBlock } from "@letta-ai/letta-client/resources/blocks/blocks"; import type { StopReasonType } from "@letta-ai/letta-client/resources/runs/runs"; import type { ToolReturnMessage as LettaToolReturnMessage } from "@letta-ai/letta-client/resources/tools"; @@ -26,8 +27,42 @@ export type { StopReasonType, MessageCreate, LettaToolReturnMessage, + CreateBlock, }; +// ═══════════════════════════════════════════════════════════════ +// CONFIGURATION TYPES (session options) +// Used internally by headless.ts/App.tsx, also exported for SDK +// ═══════════════════════════════════════════════════════════════ + +/** + * System prompt preset configuration. + * Use this to select a built-in system prompt with optional appended text. + * + * Available presets (validated at runtime by CLI): + * - 'default' - Alias for letta-claude + * - 'letta-claude' - Full Letta Code prompt (Claude-optimized) + * - 'letta-codex' - Full Letta Code prompt (Codex-optimized) + * - 'letta-gemini' - Full Letta Code prompt (Gemini-optimized) + * - 'claude' - Basic Claude (no skills/memory instructions) + * - 'codex' - Basic Codex (no skills/memory instructions) + * - 'gemini' - Basic Gemini (no skills/memory instructions) + */ +export interface SystemPromptPresetConfig { + type: "preset"; + /** Preset ID (e.g., 'default', 'letta-codex'). Validated at runtime. */ + preset: string; + /** Additional instructions to append to the preset */ + append?: string; +} + +/** + * System prompt configuration - either a raw string or preset config. + * - string: Use as the complete system prompt + * - SystemPromptPresetConfig: Use a preset, optionally with appended text + */ +export type SystemPromptConfig = string | SystemPromptPresetConfig; + // ═══════════════════════════════════════════════════════════════ // BASE ENVELOPE // All wire messages include these fields diff --git a/tsconfig.types.json b/tsconfig.types.json index effe174..a7731c3 100644 --- a/tsconfig.types.json +++ b/tsconfig.types.json @@ -7,5 +7,5 @@ "declarationMap": true, "outDir": "./dist/types" }, - "include": ["src/types/wire.ts"] + "include": ["src/types/protocol.ts"] }