feat: add system prompt and memory block configuration for headless mode (#450)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-02 15:32:07 -08:00
committed by GitHub
parent 321519a1b7
commit a956a15db4
10 changed files with 399 additions and 139 deletions

View File

@@ -78,7 +78,7 @@ if (existsSync(bundledSkillsSrc)) {
// Generate type declarations for wire types export // Generate type declarations for wire types export
console.log("📝 Generating type declarations..."); console.log("📝 Generating type declarations...");
await Bun.$`bunx tsc -p tsconfig.types.json`; 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("✅ Build complete!");
console.log(` Output: letta.js`); console.log(` Output: letta.js`);

View File

@@ -17,8 +17,8 @@
], ],
"exports": { "exports": {
".": "./letta.js", ".": "./letta.js",
"./wire-types": { "./protocol": {
"types": "./dist/types/wire.d.ts" "types": "./dist/types/protocol.d.ts"
} }
}, },
"repository": { "repository": {

View File

@@ -26,7 +26,7 @@ import { discoverSkills, formatSkillsForMemory, SKILLS_DIR } from "./skills";
*/ */
export interface BlockProvenance { export interface BlockProvenance {
label: string; label: string;
source: "global" | "project" | "new"; source: "global" | "project" | "new" | "shared";
} }
/** /**
@@ -45,24 +45,75 @@ export interface CreateAgentResult {
provenance: AgentProvenance; provenance: AgentProvenance;
} }
export interface CreateAgentOptions {
name?: string;
model?: string;
embeddingModel?: string;
updateArgs?: Record<string, unknown>;
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<string, string>;
}
export async function createAgent( export async function createAgent(
name = DEFAULT_AGENT_NAME, nameOrOptions: string | CreateAgentOptions = DEFAULT_AGENT_NAME,
model?: string, model?: string,
embeddingModel = "openai/text-embedding-3-small", embeddingModel = "openai/text-embedding-3-small",
updateArgs?: Record<string, unknown>, updateArgs?: Record<string, unknown>,
skillsDirectory?: string, skillsDirectory?: string,
parallelToolCalls = true, parallelToolCalls = true,
enableSleeptime = false, enableSleeptime = false,
systemPromptId?: string, systemPromptPreset?: string,
initBlocks?: string[], initBlocks?: string[],
baseTools?: 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 // Resolve model identifier to handle
let modelHandle: string; let modelHandle: string;
if (model) { if (options.model) {
const resolved = resolveModel(model); const resolved = resolveModel(options.model);
if (!resolved) { if (!resolved) {
console.error(`Error: Unknown model "${model}"`); console.error(`Error: Unknown model "${options.model}"`);
console.error("Available models:"); console.error("Available models:");
console.error(formatAvailableModels()); console.error(formatAvailableModels());
process.exit(1); 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 // Map internal names to server names so the agent sees the correct tool names
const { getServerToolName } = await import("../tools/manager"); const { getServerToolName } = await import("../tools/manager");
const internalToolNames = getToolNames(); const internalToolNames = getToolNames();
const serverToolNames = internalToolNames.map((name) => const serverToolNames = internalToolNames.map((n) => getServerToolName(n));
getServerToolName(name),
);
const baseMemoryTool = modelHandle.startsWith("openai/gpt-5") const baseMemoryTool = modelHandle.startsWith("openai/gpt-5")
? "memory_apply_patch" ? "memory_apply_patch"
: "memory"; : "memory";
const defaultBaseTools = baseTools ?? [ const defaultBaseTools = options.baseTools ?? [
baseMemoryTool, baseMemoryTool,
"web_search", "web_search",
"conversation_search", "conversation_search",
@@ -122,14 +171,41 @@ export async function createAgent(
} }
} }
// 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
// Separate block references from blocks to create
const referencedBlockIds: string[] = [];
let filteredMemoryBlocks: Array<{
label: string;
value: string;
description?: string | null;
limit?: number;
}>;
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 // Load memory blocks from .mdx files
const defaultMemoryBlocks = const defaultMemoryBlocks =
initBlocks && initBlocks.length === 0 ? [] : await getDefaultMemoryBlocks(); options.initBlocks && options.initBlocks.length === 0
? []
: await getDefaultMemoryBlocks();
// Optional filter: only initialize a subset of memory blocks on creation // Optional filter: only initialize a subset of memory blocks on creation
const allowedBlockLabels = initBlocks const allowedBlockLabels = options.initBlocks
? new Set( ? new Set(
initBlocks.map((name) => name.trim()).filter((name) => name.length > 0), options.initBlocks.map((n) => n.trim()).filter((n) => n.length > 0),
) )
: undefined; : undefined;
@@ -145,14 +221,29 @@ export async function createAgent(
} }
} }
const filteredMemoryBlocks = filteredMemoryBlocks =
allowedBlockLabels && allowedBlockLabels.size > 0 allowedBlockLabels && allowedBlockLabels.size > 0
? defaultMemoryBlocks.filter((b) => allowedBlockLabels.has(b.label)) ? defaultMemoryBlocks.filter((b) => allowedBlockLabels.has(b.label))
: defaultMemoryBlocks; : 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 --block-value for "${label}" - block not included in memory config`,
);
}
}
}
// Resolve absolute path for skills directory // Resolve absolute path for skills directory
const resolvedSkillsDirectory = 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 // Discover skills from .skills directory and populate skills memory block
try { 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 // Get the model's context window from its configuration
const modelUpdateArgs = getModelUpdateArgs(modelHandle); const modelUpdateArgs = getModelUpdateArgs(modelHandle);
const contextWindow = (modelUpdateArgs?.context_window as number) || 200_000; const contextWindow = (modelUpdateArgs?.context_window as number) || 200_000;
// Resolve system prompt ID to content // Resolve system prompt content:
const systemPromptContent = await resolveSystemPrompt(systemPromptId); // 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) // Create agent with all block IDs (existing + newly created)
const tags = ["origin:letta-code"]; const tags = ["origin:letta-code"];
@@ -216,7 +326,7 @@ export async function createAgent(
system: systemPromptContent, system: systemPromptContent,
name, name,
description: `Letta Code agent created in ${process.cwd()}`, description: `Letta Code agent created in ${process.cwd()}`,
embedding: embeddingModel, embedding: embeddingModelVal,
model: modelHandle, model: modelHandle,
context_window_limit: contextWindow, context_window_limit: contextWindow,
tools: toolNames, tools: toolNames,
@@ -226,8 +336,8 @@ export async function createAgent(
include_base_tools: false, include_base_tools: false,
include_base_tool_rules: false, include_base_tool_rules: false,
initial_message_sequence: [], initial_message_sequence: [],
parallel_tool_calls: parallelToolCalls, parallel_tool_calls: parallelToolCallsVal,
enable_sleeptime: enableSleeptime, enable_sleeptime: enableSleeptimeVal,
}); });
// Note: Preflight check above falls back to 'memory' when 'memory_apply_patch' is unavailable. // 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.) // Apply updateArgs if provided (e.g., context_window, reasoning_effort, verbosity, etc.)
// We intentionally pass context_window through so updateAgentLLMConfig can set // We intentionally pass context_window through so updateAgentLLMConfig can set
// context_window_limit using the latest server API, avoiding any fallback. // context_window_limit using the latest server API, avoiding any fallback.
if (updateArgs && Object.keys(updateArgs).length > 0) { if (options.updateArgs && Object.keys(options.updateArgs).length > 0) {
await updateAgentLLMConfig(agent.id, modelHandle, updateArgs); await updateAgentLLMConfig(agent.id, modelHandle, options.updateArgs);
} }
// Always retrieve the agent to ensure we get the full state with populated memory blocks // 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 // 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 // Find the sleeptime agent in the managed group by checking agent_type
for (const groupAgentId of fullAgent.managed_group.agent_ids) { for (const groupAgentId of fullAgent.managed_group.agent_ids) {
try { try {

View File

@@ -56,47 +56,48 @@ export const SYSTEM_PROMPTS: SystemPromptOption[] = [
{ {
id: "default", id: "default",
label: "Default", label: "Default",
description: "Standard Letta Code system prompt (Claude-optimized)", description: "Alias for letta-claude",
content: lettaAnthropicPrompt, content: lettaAnthropicPrompt,
isDefault: true, isDefault: true,
isFeatured: true, isFeatured: true,
}, },
{ {
id: "legacy", id: "letta-claude",
label: "Legacy", label: "Letta Claude",
description: "Original system prompt", description: "Full Letta Code system prompt (Claude-optimized)",
content: systemPrompt, content: lettaAnthropicPrompt,
isFeatured: true,
}, },
{ {
id: "letta-codex", id: "letta-codex",
label: "Codex", label: "Letta Codex",
description: "For Codex models", description: "Full Letta Code system prompt (Codex-optimized)",
content: lettaCodexPrompt, content: lettaCodexPrompt,
isFeatured: true, isFeatured: true,
}, },
{ {
id: "letta-gemini", id: "letta-gemini",
label: "Gemini", label: "Letta Gemini",
description: "For Gemini models", description: "Full Letta Code system prompt (Gemini-optimized)",
content: lettaGeminiPrompt, content: lettaGeminiPrompt,
isFeatured: true, isFeatured: true,
}, },
{ {
id: "anthropic", id: "claude",
label: "Claude (basic)", label: "Claude (basic)",
description: "For Claude models (no skills/memory instructions)", description: "Basic Claude prompt (no skills/memory instructions)",
content: anthropicPrompt, content: anthropicPrompt,
}, },
{ {
id: "codex", id: "codex",
label: "Codex (basic)", label: "Codex (basic)",
description: "For Codex models (no skills/memory instructions)", description: "Basic Codex prompt (no skills/memory instructions)",
content: codexPrompt, content: codexPrompt,
}, },
{ {
id: "gemini", id: "gemini",
label: "Gemini (basic)", label: "Gemini (basic)",
description: "For Gemini models (no skills/memory instructions)", description: "Basic Gemini prompt (no skills/memory instructions)",
content: geminiPrompt, content: geminiPrompt,
}, },
]; ];
@@ -109,19 +110,19 @@ export const SYSTEM_PROMPTS: SystemPromptOption[] = [
* 2. If it matches a subagent name, use that subagent's system prompt * 2. If it matches a subagent name, use that subagent's system prompt
* 3. Otherwise, use the default 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 * @returns The resolved system prompt content
*/ */
export async function resolveSystemPrompt( export async function resolveSystemPrompt(
systemPromptId: string | undefined, systemPromptPreset: string | undefined,
): Promise<string> { ): Promise<string> {
// No input - use default // No input - use default
if (!systemPromptId) { if (!systemPromptPreset) {
return SYSTEM_PROMPT; return SYSTEM_PROMPT;
} }
// 1. Check if it matches a system prompt ID // 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) { if (matchedPrompt) {
return matchedPrompt.content; return matchedPrompt.content;
} }
@@ -129,7 +130,7 @@ export async function resolveSystemPrompt(
// 2. Check if it matches a subagent name // 2. Check if it matches a subagent name
const { getAllSubagentConfigs } = await import("./subagents"); const { getAllSubagentConfigs } = await import("./subagents");
const subagentConfigs = await getAllSubagentConfigs(); const subagentConfigs = await getAllSubagentConfigs();
const matchedSubagent = subagentConfigs[systemPromptId]; const matchedSubagent = subagentConfigs[systemPromptPreset];
if (matchedSubagent) { if (matchedSubagent) {
return matchedSubagent.systemPrompt; return matchedSubagent.systemPrompt;
} }

View File

@@ -48,7 +48,7 @@ import type {
RetryMessage, RetryMessage,
StreamEvent, StreamEvent,
SystemInitMessage, SystemInitMessage,
} from "./types/wire"; } from "./types/protocol";
// Maximum number of times to retry a turn when the backend // Maximum number of times to retry a turn when the backend
// reports an `llm_api_error` stop reason. This helps smooth // reports an `llm_api_error` stop reason. This helps smooth
@@ -74,6 +74,10 @@ export async function handleHeadlessCommand(
agent: { type: "string", short: "a" }, agent: { type: "string", short: "a" },
model: { type: "string", short: "m" }, model: { type: "string", short: "m" },
system: { type: "string", short: "s" }, 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" }, toolset: { type: "string" },
prompt: { type: "boolean", short: "p" }, prompt: { type: "boolean", short: "p" },
"output-format": { type: "string" }, "output-format": { type: "string" },
@@ -169,7 +173,11 @@ export async function handleHeadlessCommand(
const specifiedAgentId = values.agent as string | undefined; const specifiedAgentId = values.agent as string | undefined;
const shouldContinue = values.continue as boolean | undefined; const shouldContinue = values.continue as boolean | undefined;
const forceNew = values.new 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 initBlocksRaw = values["init-blocks"] as string | undefined;
const baseToolsRaw = values["base-tools"] as string | undefined; const baseToolsRaw = values["base-tools"] as string | undefined;
const sleeptimeFlag = (values.sleeptime as boolean | undefined) ?? 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<string, string> | 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 // Priority 1: Import from AgentFile template
if (fromAfFile) { if (fromAfFile) {
const { importAgentFromFile } = await import("./agent/import"); 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) // Priority 3: Check if --new flag was passed (skip all resume logic)
if (!agent && forceNew) { if (!agent && forceNew) {
const updateArgs = getModelUpdateArgs(model); const updateArgs = getModelUpdateArgs(model);
try { const createOptions = {
const result = await createAgent(
undefined,
model, model,
undefined,
updateArgs, updateArgs,
skillsDirectory, skillsDirectory,
true, // parallelToolCalls always enabled parallelToolCalls: true,
sleeptimeFlag ?? settings.enableSleeptime, enableSleeptime: sleeptimeFlag ?? settings.enableSleeptime,
systemPromptId, systemPromptPreset,
systemPromptCustom: systemCustom,
systemPromptAppend: systemAppend,
initBlocks, initBlocks,
baseTools, baseTools,
); memoryBlocks,
blockValues,
};
try {
const result = await createAgent(createOptions);
agent = result.agent; agent = result.agent;
} catch (err) { } catch (err) {
if (isToolsNotFoundError(err)) { if (isToolsNotFoundError(err)) {
console.warn("Tools missing on server, re-uploading and retrying..."); console.warn("Tools missing on server, re-uploading and retrying...");
await forceUpsertTools(client, baseURL); await forceUpsertTools(client, baseURL);
const result = await createAgent( const result = await createAgent(createOptions);
undefined,
model,
undefined,
updateArgs,
skillsDirectory,
true,
sleeptimeFlag ?? settings.enableSleeptime,
systemPromptId,
initBlocks,
baseTools,
);
agent = result.agent; agent = result.agent;
} else { } else {
throw err; throw err;
@@ -320,36 +398,23 @@ export async function handleHeadlessCommand(
// Priority 6: Create a new agent // Priority 6: Create a new agent
if (!agent) { if (!agent) {
const updateArgs = getModelUpdateArgs(model); const updateArgs = getModelUpdateArgs(model);
try { const createOptions = {
const result = await createAgent(
undefined,
model, model,
undefined,
updateArgs, updateArgs,
skillsDirectory, skillsDirectory,
true, // parallelToolCalls always enabled parallelToolCalls: true,
sleeptimeFlag ?? settings.enableSleeptime, enableSleeptime: sleeptimeFlag ?? settings.enableSleeptime,
systemPromptId, systemPromptPreset,
undefined, // Note: systemCustom, systemAppend, and memoryBlocks only apply with --new flag
undefined, };
); try {
const result = await createAgent(createOptions);
agent = result.agent; agent = result.agent;
} catch (err) { } catch (err) {
if (isToolsNotFoundError(err)) { if (isToolsNotFoundError(err)) {
console.warn("Tools missing on server, re-uploading and retrying..."); console.warn("Tools missing on server, re-uploading and retrying...");
await forceUpsertTools(client, baseURL); await forceUpsertTools(client, baseURL);
const result = await createAgent( const result = await createAgent(createOptions);
undefined,
model,
undefined,
updateArgs,
skillsDirectory,
true,
sleeptimeFlag ?? settings.enableSleeptime,
systemPromptId,
undefined,
undefined,
);
agent = result.agent; agent = result.agent;
} else { } else {
throw err; throw err;
@@ -365,7 +430,7 @@ export async function handleHeadlessCommand(
); );
// If resuming and a model or system prompt was specified, apply those changes // If resuming and a model or system prompt was specified, apply those changes
if (isResumingAgent && (model || systemPromptId)) { if (isResumingAgent && (model || systemPromptPreset)) {
if (model) { if (model) {
const { resolveModel } = await import("./agent/model"); const { resolveModel } = await import("./agent/model");
const modelHandle = resolveModel(model); const modelHandle = resolveModel(model);
@@ -388,9 +453,12 @@ export async function handleHeadlessCommand(
} }
} }
if (systemPromptId) { if (systemPromptPreset) {
const { updateAgentSystemPrompt } = await import("./agent/modify"); 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) { if (!result.success || !result.agent) {
console.error(`Failed to update system prompt: ${result.message}`); console.error(`Failed to update system prompt: ${result.message}`);
process.exit(1); process.exit(1);

View File

@@ -332,6 +332,10 @@ async function main(): Promise<void> {
name: { type: "string", short: "n" }, name: { type: "string", short: "n" },
model: { type: "string", short: "m" }, model: { type: "string", short: "m" },
system: { type: "string", short: "s" }, 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" }, toolset: { type: "string" },
prompt: { type: "boolean", short: "p" }, prompt: { type: "boolean", short: "p" },
run: { type: "boolean" }, run: { type: "boolean" },
@@ -406,7 +410,12 @@ async function main(): Promise<void> {
let specifiedAgentId = (values.agent as string | undefined) ?? null; let specifiedAgentId = (values.agent as string | undefined) ?? null;
const specifiedAgentName = (values.name as string | undefined) ?? null; const specifiedAgentName = (values.name as string | undefined) ?? null;
const specifiedModel = (values.model as string | undefined) ?? undefined; 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 specifiedToolset = (values.toolset as string | undefined) ?? undefined;
const skillsDirectory = (values.skills as string | undefined) ?? undefined; const skillsDirectory = (values.skills as string | undefined) ?? undefined;
const sleeptimeFlag = (values.sleeptime as boolean | undefined) ?? undefined; const sleeptimeFlag = (values.sleeptime as boolean | undefined) ?? undefined;
@@ -476,8 +485,16 @@ async function main(): Promise<void> {
process.exit(1); process.exit(1);
} }
// Validate system prompt if provided (can be a system prompt ID or subagent name) // Validate system prompt options (--system and --system-custom are mutually exclusive)
if (systemPromptId) { 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 { SYSTEM_PROMPTS } = await import("./agent/promptAssets");
const { getAllSubagentConfigs } = await import("./agent/subagents"); const { getAllSubagentConfigs } = await import("./agent/subagents");
@@ -485,13 +502,42 @@ async function main(): Promise<void> {
const subagentConfigs = await getAllSubagentConfigs(); const subagentConfigs = await getAllSubagentConfigs();
const validSubagentNames = Object.keys(subagentConfigs); const validSubagentNames = Object.keys(subagentConfigs);
const isValidSystemPrompt = validSystemPrompts.includes(systemPromptId); const isValidSystemPrompt = validSystemPrompts.includes(systemPromptPreset);
const isValidSubagent = validSubagentNames.includes(systemPromptId); const isValidSubagent = validSubagentNames.includes(systemPromptPreset);
if (!isValidSystemPrompt && !isValidSubagent) { if (!isValidSystemPrompt && !isValidSubagent) {
const allValid = [...validSystemPrompts, ...validSubagentNames]; const allValid = [...validSystemPrompts, ...validSubagentNames];
console.error( 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); process.exit(1);
} }
@@ -745,7 +791,7 @@ async function main(): Promise<void> {
baseTools, baseTools,
agentIdArg, agentIdArg,
model, model,
systemPromptId, systemPromptPreset,
toolset, toolset,
skillsDirectory, skillsDirectory,
fromAfFile, fromAfFile,
@@ -756,7 +802,7 @@ async function main(): Promise<void> {
baseTools?: string[]; baseTools?: string[];
agentIdArg: string | null; agentIdArg: string | null;
model?: string; model?: string;
systemPromptId?: string; systemPromptPreset?: string;
toolset?: "codex" | "default" | "gemini"; toolset?: "codex" | "default" | "gemini";
skillsDirectory?: string; skillsDirectory?: string;
fromAfFile?: string; fromAfFile?: string;
@@ -1027,13 +1073,13 @@ async function main(): Promise<void> {
agent = await client.agents.retrieve(agentIdArg); agent = await client.agents.retrieve(agentIdArg);
// Apply --system flag to existing agent if provided // Apply --system flag to existing agent if provided
if (systemPromptId) { if (systemPromptPreset) {
const { updateAgentSystemPrompt } = await import( const { updateAgentSystemPrompt } = await import(
"./agent/modify" "./agent/modify"
); );
const result = await updateAgentSystemPrompt( const result = await updateAgentSystemPrompt(
agent.id, agent.id,
systemPromptId, systemPromptPreset,
); );
if (!result.success || !result.agent) { if (!result.success || !result.agent) {
console.error( console.error(
@@ -1067,7 +1113,7 @@ async function main(): Promise<void> {
skillsDirectory, skillsDirectory,
true, // parallelToolCalls always enabled true, // parallelToolCalls always enabled
sleeptimeFlag ?? settings.enableSleeptime, sleeptimeFlag ?? settings.enableSleeptime,
systemPromptId, systemPromptPreset,
initBlocks, initBlocks,
baseTools, baseTools,
); );
@@ -1089,7 +1135,7 @@ async function main(): Promise<void> {
skillsDirectory, skillsDirectory,
true, true,
sleeptimeFlag ?? settings.enableSleeptime, sleeptimeFlag ?? settings.enableSleeptime,
systemPromptId, systemPromptPreset,
initBlocks, initBlocks,
baseTools, baseTools,
); );
@@ -1144,7 +1190,7 @@ async function main(): Promise<void> {
skillsDirectory, skillsDirectory,
true, // parallelToolCalls always enabled true, // parallelToolCalls always enabled
sleeptimeFlag ?? settings.enableSleeptime, sleeptimeFlag ?? settings.enableSleeptime,
systemPromptId, systemPromptPreset,
undefined, undefined,
undefined, undefined,
); );
@@ -1166,7 +1212,7 @@ async function main(): Promise<void> {
skillsDirectory, skillsDirectory,
true, true,
sleeptimeFlag ?? settings.enableSleeptime, sleeptimeFlag ?? settings.enableSleeptime,
systemPromptId, systemPromptPreset,
undefined, undefined,
undefined, undefined,
); );
@@ -1247,7 +1293,7 @@ async function main(): Promise<void> {
setIsResumingSession(resuming); setIsResumingSession(resuming);
// If resuming and a model or system prompt was specified, apply those changes // If resuming and a model or system prompt was specified, apply those changes
if (resuming && (model || systemPromptId)) { if (resuming && (model || systemPromptPreset)) {
if (model) { if (model) {
const { resolveModel } = await import("./agent/model"); const { resolveModel } = await import("./agent/model");
const modelHandle = resolveModel(model); const modelHandle = resolveModel(model);
@@ -1271,11 +1317,11 @@ async function main(): Promise<void> {
} }
} }
if (systemPromptId) { if (systemPromptPreset) {
const { updateAgentSystemPrompt } = await import("./agent/modify"); const { updateAgentSystemPrompt } = await import("./agent/modify");
const result = await updateAgentSystemPrompt( const result = await updateAgentSystemPrompt(
agent.id, agent.id,
systemPromptId, systemPromptPreset,
); );
if (!result.success || !result.agent) { if (!result.success || !result.agent) {
console.error(`Error: ${result.message}`); console.error(`Error: ${result.message}`);
@@ -1312,7 +1358,7 @@ async function main(): Promise<void> {
forceNew, forceNew,
agentIdArg, agentIdArg,
model, model,
systemPromptId, systemPromptPreset,
fromAfFile, fromAfFile,
loadingState, loadingState,
selectedGlobalAgentId, selectedGlobalAgentId,
@@ -1384,7 +1430,7 @@ async function main(): Promise<void> {
baseTools: baseTools, baseTools: baseTools,
agentIdArg: specifiedAgentId, agentIdArg: specifiedAgentId,
model: specifiedModel, model: specifiedModel,
systemPromptId: systemPromptId, systemPromptPreset: systemPromptPreset,
toolset: specifiedToolset as "codex" | "default" | "gemini" | undefined, toolset: specifiedToolset as "codex" | "default" | "gemini" | undefined,
skillsDirectory: skillsDirectory, skillsDirectory: skillsDirectory,
fromAfFile: fromAfFile, fromAfFile: fromAfFile,

View File

@@ -7,7 +7,7 @@ import type {
StreamEvent, StreamEvent,
SystemInitMessage, SystemInitMessage,
WireMessage, WireMessage,
} from "../types/wire"; } from "../types/protocol";
/** /**
* Tests for --input-format stream-json bidirectional communication. * Tests for --input-format stream-json bidirectional communication.

View File

@@ -4,7 +4,7 @@ import type {
ResultMessage, ResultMessage,
StreamEvent, StreamEvent,
SystemInitMessage, SystemInitMessage,
} from "../types/wire"; } from "../types/protocol";
/** /**
* Tests for stream-json output format. * Tests for stream-json output format.

View File

@@ -1,9 +1,9 @@
/** /**
* Wire Format Types * Protocol Types for Letta Code
* *
* These types define the JSON structure emitted by headless.ts when running * These types define:
* in stream-json mode. They enable typed consumption of the bidirectional * 1. The JSON structure emitted by headless.ts in stream-json mode (wire protocol)
* JSON protocol. * 2. Configuration types for session options (used internally and by SDK)
* *
* Design principle: Compose from @letta-ai/letta-client types where possible. * Design principle: Compose from @letta-ai/letta-client types where possible.
*/ */
@@ -16,6 +16,7 @@ import type {
ToolCallMessage as LettaToolCallMessage, ToolCallMessage as LettaToolCallMessage,
ToolCall, ToolCall,
} from "@letta-ai/letta-client/resources/agents/messages"; } 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 { StopReasonType } from "@letta-ai/letta-client/resources/runs/runs";
import type { ToolReturnMessage as LettaToolReturnMessage } from "@letta-ai/letta-client/resources/tools"; import type { ToolReturnMessage as LettaToolReturnMessage } from "@letta-ai/letta-client/resources/tools";
@@ -26,8 +27,42 @@ export type {
StopReasonType, StopReasonType,
MessageCreate, MessageCreate,
LettaToolReturnMessage, 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 // BASE ENVELOPE
// All wire messages include these fields // All wire messages include these fields

View File

@@ -7,5 +7,5 @@
"declarationMap": true, "declarationMap": true,
"outDir": "./dist/types" "outDir": "./dist/types"
}, },
"include": ["src/types/wire.ts"] "include": ["src/types/protocol.ts"]
} }