Files
letta-code/src/agent/create.ts
2025-12-15 11:13:43 -08:00

277 lines
9.0 KiB
TypeScript

/**
* Utilities for creating an agent on the Letta API backend
**/
import { join } from "node:path";
import type {
AgentState,
AgentType,
} from "@letta-ai/letta-client/resources/agents/agents";
import { getToolNames } from "../tools/manager";
import { getClient } from "./client";
import { getDefaultMemoryBlocks } from "./memory";
import {
formatAvailableModels,
getModelUpdateArgs,
resolveModel,
} from "./model";
import { updateAgentLLMConfig } from "./modify";
import { SYSTEM_PROMPT, SYSTEM_PROMPTS } from "./promptAssets";
import { SLEEPTIME_MEMORY_PERSONA } from "./prompts/sleeptime";
import { discoverSkills, formatSkillsForMemory, SKILLS_DIR } from "./skills";
/**
* Describes where a memory block came from
*/
export interface BlockProvenance {
label: string;
source: "global" | "project" | "new";
}
/**
* Provenance info for an agent creation
*/
export interface AgentProvenance {
isNew: true;
blocks: BlockProvenance[];
}
/**
* Result from createAgent including provenance info
*/
export interface CreateAgentResult {
agent: AgentState;
provenance: AgentProvenance;
}
export async function createAgent(
name = "letta-code-agent",
model?: string,
embeddingModel = "openai/text-embedding-3-small",
updateArgs?: Record<string, unknown>,
skillsDirectory?: string,
parallelToolCalls = true,
enableSleeptime = false,
systemPromptId?: string,
initBlocks?: string[],
baseTools?: string[],
) {
// Resolve model identifier to handle
let modelHandle: string;
if (model) {
const resolved = resolveModel(model);
if (!resolved) {
console.error(`Error: Unknown model "${model}"`);
console.error("Available models:");
console.error(formatAvailableModels());
process.exit(1);
}
modelHandle = resolved;
} else {
// Use default model
modelHandle = "anthropic/claude-sonnet-4-5-20250929";
}
const client = await getClient();
// Get loaded tool names (tools are already registered with Letta)
// 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 baseMemoryTool = modelHandle.startsWith("openai/gpt-5")
? "memory_apply_patch"
: "memory";
const defaultBaseTools = baseTools ?? [
baseMemoryTool,
"web_search",
"conversation_search",
"fetch_webpage",
];
let toolNames = [...serverToolNames, ...defaultBaseTools];
// Fallback: if server doesn't have memory_apply_patch, use legacy memory tool
if (toolNames.includes("memory_apply_patch")) {
try {
const resp = await client.tools.list({ name: "memory_apply_patch" });
const hasMemoryApplyPatch =
Array.isArray(resp.items) && resp.items.length > 0;
if (!hasMemoryApplyPatch) {
console.warn(
"memory_apply_patch tool not found on server; falling back to 'memory' tool",
);
toolNames = toolNames.map((n) =>
n === "memory_apply_patch" ? "memory" : n,
);
}
} catch (err) {
// If the capability check fails for any reason, conservatively fall back to 'memory'
console.warn(
`Unable to verify memory_apply_patch availability (falling back to 'memory'): ${
err instanceof Error ? err.message : String(err)
}`,
);
toolNames = toolNames.map((n) =>
n === "memory_apply_patch" ? "memory" : n,
);
}
}
// Load memory blocks from .mdx files
const defaultMemoryBlocks =
initBlocks && initBlocks.length === 0 ? [] : await getDefaultMemoryBlocks();
// 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;
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);
}
}
}
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);
// Discover skills from .skills directory and populate skills memory block
try {
const { skills, errors } = await discoverSkills(resolvedSkillsDirectory);
// Log any errors encountered during skill discovery
if (errors.length > 0) {
console.warn("Errors encountered during skill discovery:");
for (const error of errors) {
console.warn(` ${error.path}: ${error.message}`);
}
}
// Find and update the skills memory block with discovered skills
const skillsBlock = filteredMemoryBlocks.find((b) => b.label === "skills");
if (skillsBlock) {
const formatted = formatSkillsForMemory(skills, resolvedSkillsDirectory);
skillsBlock.value = formatted;
}
} catch (error) {
console.warn(
`Failed to discover skills: ${error instanceof Error ? error.message : String(error)}`,
);
}
// Track provenance: which blocks were created
// Note: We no longer reuse shared blocks - each agent gets fresh blocks
const blockProvenance: BlockProvenance[] = [];
const blockIds: string[] = [];
// Create all blocks fresh for the new agent
for (const block of filteredMemoryBlocks) {
try {
const createdBlock = await client.blocks.create(block);
if (!createdBlock.id) {
throw new Error(`Created block ${block.label} has no ID`);
}
blockIds.push(createdBlock.id);
blockProvenance.push({ label: block.label, source: "new" });
} catch (error) {
console.error(`Failed to create block ${block.label}:`, error);
throw error;
}
}
// 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 (use specified ID or default)
const systemPrompt = systemPromptId
? (SYSTEM_PROMPTS.find((p) => p.id === systemPromptId)?.content ??
SYSTEM_PROMPT)
: SYSTEM_PROMPT;
// Create agent with all block IDs (existing + newly created)
const agent = await client.agents.create({
agent_type: "letta_v1_agent" as AgentType,
system: systemPrompt,
name,
description: `Letta Code agent created in ${process.cwd()}`,
embedding: embeddingModel,
model: modelHandle,
context_window_limit: contextWindow,
tools: toolNames,
block_ids: blockIds,
tags: ["origin:letta-code"],
// should be default off, but just in case
include_base_tools: false,
include_base_tool_rules: false,
initial_message_sequence: [],
parallel_tool_calls: parallelToolCalls,
enable_sleeptime: enableSleeptime,
});
// Note: Preflight check above falls back to 'memory' when 'memory_apply_patch' is unavailable.
// 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);
}
// Always retrieve the agent to ensure we get the full state with populated memory blocks
const fullAgent = await client.agents.retrieve(agent.id, {
include: ["agent.managed_group"],
});
// Update persona block for sleeptime agent
if (enableSleeptime && 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 {
const groupAgent = await client.agents.retrieve(groupAgentId);
if (groupAgent.agent_type === "sleeptime_agent") {
// Update the persona block on the SLEEPTIME agent, not the primary agent
await client.agents.blocks.update("memory_persona", {
agent_id: groupAgentId,
value: SLEEPTIME_MEMORY_PERSONA,
description:
"Instructions for the sleep-time memory management agent",
});
break; // Found and updated sleeptime agent
}
} catch (error) {
console.warn(
`Failed to check/update agent ${groupAgentId}:`,
error instanceof Error ? error.message : String(error),
);
}
}
}
// Build provenance info
const provenance: AgentProvenance = {
isNew: true,
blocks: blockProvenance,
};
return { agent: fullAgent, provenance };
}