feat: migrate to Letta TS SDK v1 (alpha) (#11)

This commit is contained in:
Charles Packer
2025-10-28 23:50:57 -07:00
committed by GitHub
parent 275fca942d
commit 4ca01d199d
17 changed files with 377 additions and 332 deletions

View File

@@ -1,7 +1,8 @@
// src/agent/check-approval.ts
// Check for pending approvals and retrieve recent message history when resuming an agent
import type { Letta, LettaClient } from "@letta-ai/letta-client";
import type Letta from "@letta-ai/letta-client";
import type { LettaMessageUnion } from "@letta-ai/letta-client/resources/agents/messages";
import type { ApprovalRequest } from "../cli/helpers/stream";
// Number of recent messages to backfill when resuming a session
@@ -9,7 +10,7 @@ const MESSAGE_HISTORY_LIMIT = 15;
export interface ResumeData {
pendingApproval: ApprovalRequest | null;
messageHistory: Letta.LettaMessageUnion[];
messageHistory: LettaMessageUnion[];
}
/**
@@ -21,11 +22,12 @@ export interface ResumeData {
* @returns Pending approval (if any) and recent message history
*/
export async function getResumeData(
client: LettaClient,
client: Letta,
agentId: string,
): Promise<ResumeData> {
try {
const messages = await client.agents.messages.list(agentId);
const messagesPage = await client.agents.messages.list(agentId);
const messages = messagesPage.items;
if (!messages || messages.length === 0) {
return { pendingApproval: null, messageHistory: [] };
}
@@ -33,14 +35,25 @@ export async function getResumeData(
// Check for pending approval (last message)
let pendingApproval: ApprovalRequest | null = null;
const lastMessage = messages[messages.length - 1];
if (lastMessage?.messageType === "approval_request_message") {
const approvalMessage = lastMessage as Letta.ApprovalRequestMessage;
const toolCall = approvalMessage.toolCall;
pendingApproval = {
toolCallId: toolCall.toolCallId || "",
toolName: toolCall.name || "",
toolArgs: toolCall.arguments || "",
};
if (lastMessage?.message_type === "approval_request_message") {
// Use tool_calls array (new) or fallback to tool_call (deprecated)
const toolCalls = Array.isArray(lastMessage.tool_calls)
? lastMessage.tool_calls
: lastMessage.tool_call
? [lastMessage.tool_call]
: [];
if (toolCalls.length > 0) {
const toolCall = toolCalls[0];
// Ensure all required fields are present (type guard for ToolCall vs ToolCallDelta)
if (toolCall?.tool_call_id && toolCall.name && toolCall.arguments) {
pendingApproval = {
toolCallId: toolCall.tool_call_id,
toolName: toolCall.name,
toolArgs: toolCall.arguments,
};
}
}
}
// Get last N messages for backfill
@@ -48,7 +61,7 @@ export async function getResumeData(
let messageHistory = messages.slice(-historyCount);
// Skip if starts with orphaned tool_return (incomplete turn)
if (messageHistory[0]?.messageType === "tool_return_message") {
if (messageHistory[0]?.message_type === "tool_return_message") {
messageHistory = messageHistory.slice(1);
}

View File

@@ -1,11 +1,11 @@
import { LettaClient } from "@letta-ai/letta-client";
import Letta from "@letta-ai/letta-client";
import { loadSettings, updateSettings } from "../settings";
export async function getClient() {
const settings = await loadSettings();
const token = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY;
if (!token) {
const apiKey = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY;
if (!apiKey) {
console.error("Missing LETTA_API_KEY");
console.error(
"Set it via environment variable or add it to ~/.letta/settings.json:",
@@ -14,7 +14,7 @@ export async function getClient() {
process.exit(1);
}
const baseUrl =
const baseURL =
process.env.LETTA_BASE_URL ||
settings.env?.LETTA_BASE_URL ||
"https://api.letta.com";
@@ -37,5 +37,5 @@ export async function getClient() {
await updateSettings({ env: updatedEnv });
}
return new LettaClient({ token, baseUrl });
return new Letta({ apiKey, baseURL });
}

View File

@@ -2,7 +2,11 @@
* Utilities for creating an agent on the Letta API backend
**/
import { Letta } from "@letta-ai/letta-client";
import type {
AgentType,
Block,
CreateBlock,
} from "@letta-ai/letta-client/resources/agents/agents";
import {
loadProjectSettings,
updateProjectSettings,
@@ -39,7 +43,7 @@ export async function createAgent(
const localSharedBlockIds = projectSettings.localSharedBlockIds;
// Retrieve existing blocks (both global and local) and match them with defaults
const existingBlocks = new Map<string, Letta.Block>();
const existingBlocks = new Map<string, Block>();
// Load global blocks (persona, human)
for (const [label, blockId] of Object.entries(globalSharedBlockIds)) {
@@ -69,7 +73,7 @@ export async function createAgent(
// Separate blocks into existing (reuse) and new (create)
const blockIds: string[] = [];
const blocksToCreate: Array<{ block: Letta.CreateBlock; label: string }> = [];
const blocksToCreate: Array<{ block: CreateBlock; label: string }> = [];
for (const defaultBlock of defaultMemoryBlocks) {
const existingBlock = existingBlocks.get(defaultBlock.label);
@@ -131,17 +135,17 @@ export async function createAgent(
// Create agent with all block IDs (existing + newly created)
const agent = await client.agents.create({
agentType: Letta.AgentType.LettaV1Agent,
agent_type: "letta_v1_agent" as AgentType,
system: SYSTEM_PROMPT,
name,
model,
contextWindowLimit: 200_000,
context_window_limit: 200_000,
tools: toolNames,
blockIds,
block_ids: blockIds,
// should be default off, but just in case
includeBaseTools: false,
includeBaseToolRules: false,
initialMessageSequence: [],
include_base_tools: false,
include_base_tool_rules: false,
initial_message_sequence: [],
});
return agent; // { id, ... }
}

View File

@@ -3,7 +3,7 @@
* Loads memory blocks from .mdx files in src/agent/prompts
*/
import type { Letta } from "@letta-ai/letta-client";
import type { CreateBlock } from "@letta-ai/letta-client/resources/agents/agents";
import { MEMORY_PROMPTS } from "./promptAssets";
/**
@@ -40,8 +40,8 @@ function parseMdxFrontmatter(content: string): {
/**
* Load memory blocks from .mdx files in src/agent/prompts
*/
async function loadMemoryBlocksFromMdx(): Promise<Letta.CreateBlock[]> {
const memoryBlocks: Letta.CreateBlock[] = [];
async function loadMemoryBlocksFromMdx(): Promise<CreateBlock[]> {
const memoryBlocks: CreateBlock[] = [];
const mdxFiles = ["persona.mdx", "human.mdx", "project.mdx"];
// const mdxFiles = ["persona.mdx", "human.mdx", "style.mdx"];
@@ -56,7 +56,7 @@ async function loadMemoryBlocksFromMdx(): Promise<Letta.CreateBlock[]> {
}
const { frontmatter, body } = parseMdxFrontmatter(content);
const block: Letta.CreateBlock = {
const block: CreateBlock = {
label: frontmatter.label || filename.replace(".mdx", ""),
value: body,
};
@@ -75,12 +75,12 @@ async function loadMemoryBlocksFromMdx(): Promise<Letta.CreateBlock[]> {
}
// Cache for loaded memory blocks
let cachedMemoryBlocks: Letta.CreateBlock[] | null = null;
let cachedMemoryBlocks: CreateBlock[] | null = null;
/**
* Get default starter memory blocks for new agents
*/
export async function getDefaultMemoryBlocks(): Promise<Letta.CreateBlock[]> {
export async function getDefaultMemoryBlocks(): Promise<CreateBlock[]> {
if (!cachedMemoryBlocks) {
cachedMemoryBlocks = await loadMemoryBlocksFromMdx();
}

View File

@@ -2,22 +2,27 @@
* Utilities for sending messages to an agent
**/
import type { Letta } from "@letta-ai/letta-client";
import type { Stream } from "@letta-ai/letta-client/core/streaming";
import type { MessageCreate } from "@letta-ai/letta-client/resources/agents/agents";
import type {
ApprovalCreate,
LettaStreamingResponse,
} from "@letta-ai/letta-client/resources/agents/messages";
import { getClient } from "./client";
export async function sendMessageStream(
agentId: string,
messages: Array<Letta.MessageCreate | Letta.ApprovalCreate>,
messages: Array<MessageCreate | ApprovalCreate>,
opts: {
streamTokens?: boolean;
background?: boolean;
// add more later: includePings, request timeouts, etc.
} = { streamTokens: true, background: true },
): Promise<AsyncIterable<Letta.LettaStreamingResponse>> {
): Promise<Stream<LettaStreamingResponse>> {
const client = await getClient();
return client.agents.messages.createStream(agentId, {
return client.agents.messages.stream(agentId, {
messages: messages,
streamTokens: opts.streamTokens ?? true,
stream_tokens: opts.streamTokens ?? true,
background: opts.background ?? true,
});
}

View File

@@ -1,7 +1,7 @@
// src/agent/modify.ts
// Utilities for modifying agent configuration
import type { Letta } from "@letta-ai/letta-client";
import type { LlmConfig } from "@letta-ai/letta-client/resources/models/models";
import { getClient } from "./client";
/**
@@ -19,27 +19,27 @@ export async function updateAgentLLMConfig(
agentId: string,
modelHandle: string,
updateArgs?: Record<string, unknown>,
): Promise<Letta.LlmConfig> {
): Promise<LlmConfig> {
const client = await getClient();
// Step 1: Update model (top-level field)
await client.agents.modify(agentId, { model: modelHandle });
await client.agents.update(agentId, { model: modelHandle });
// Step 2: Get updated agent to retrieve current llmConfig
// Step 2: Get updated agent to retrieve current llm_config
const agent = await client.agents.retrieve(agentId);
let finalConfig = agent.llmConfig;
let finalConfig = agent.llm_config;
// Step 3: If we have updateArgs, merge them into llmConfig and patch again
// Step 3: If we have updateArgs, merge them into llm_config and patch again
if (updateArgs && Object.keys(updateArgs).length > 0) {
const updatedLlmConfig = {
...finalConfig,
...updateArgs,
} as Letta.LlmConfig;
await client.agents.modify(agentId, { llmConfig: updatedLlmConfig });
} as LlmConfig;
await client.agents.update(agentId, { llm_config: updatedLlmConfig });
// Retrieve final state
const finalAgent = await client.agents.retrieve(agentId);
finalConfig = finalAgent.llmConfig;
finalConfig = finalAgent.llm_config;
}
return finalConfig;