refactor: use conversations (#475)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// src/agent/check-approval.ts
|
||||
// Check for pending approvals and retrieve recent message history when resuming an agent
|
||||
// Check for pending approvals and retrieve recent message history when resuming an agent/conversation
|
||||
|
||||
import type Letta from "@letta-ai/letta-client";
|
||||
import type { AgentState } from "@letta-ai/letta-client/resources/agents/agents";
|
||||
@@ -32,15 +32,26 @@ export interface ResumeData {
|
||||
*
|
||||
* @param client - The Letta client
|
||||
* @param agent - The agent state (includes in-context messages)
|
||||
* @param conversationId - Optional conversation ID to fetch messages from (uses conversations API)
|
||||
* @returns Pending approval (if any) and recent message history
|
||||
*/
|
||||
export async function getResumeData(
|
||||
client: Letta,
|
||||
agent: AgentState,
|
||||
conversationId?: string,
|
||||
): Promise<ResumeData> {
|
||||
try {
|
||||
const messagesPage = await client.agents.messages.list(agent.id);
|
||||
const messages = messagesPage.items;
|
||||
// Fetch messages from conversation or agent depending on what's provided
|
||||
let messages: Message[];
|
||||
if (conversationId) {
|
||||
// Use conversations API for conversation-specific history
|
||||
messages = await client.conversations.messages.list(conversationId);
|
||||
} else {
|
||||
// Fall back to agent messages (legacy behavior)
|
||||
const messagesPage = await client.agents.messages.list(agent.id);
|
||||
messages = messagesPage.items;
|
||||
}
|
||||
|
||||
if (!messages || messages.length === 0) {
|
||||
return {
|
||||
pendingApproval: null,
|
||||
|
||||
@@ -46,6 +46,8 @@ export interface CreateAgentResult {
|
||||
|
||||
export interface CreateAgentOptions {
|
||||
name?: string;
|
||||
/** Agent description shown in /agents selector */
|
||||
description?: string;
|
||||
model?: string;
|
||||
embeddingModel?: string;
|
||||
updateArgs?: Record<string, unknown>;
|
||||
@@ -318,11 +320,14 @@ export async function createAgent(
|
||||
tags.push("role:subagent");
|
||||
}
|
||||
|
||||
const agentDescription =
|
||||
options.description ?? `Letta Code agent created in ${process.cwd()}`;
|
||||
|
||||
const agent = await client.agents.create({
|
||||
agent_type: "letta_v1_agent" as AgentType,
|
||||
system: systemPromptContent,
|
||||
name,
|
||||
description: `Letta Code agent created in ${process.cwd()}`,
|
||||
description: agentDescription,
|
||||
embedding: embeddingModelVal,
|
||||
model: modelHandle,
|
||||
context_window_limit: contextWindow,
|
||||
|
||||
129
src/agent/defaults.ts
Normal file
129
src/agent/defaults.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Default agents (Memo & Incognito) creation and management.
|
||||
*
|
||||
* Memo: Stateful agent with full memory - learns and grows with the user.
|
||||
* Incognito: Stateless agent - fresh experience without accumulated memory.
|
||||
*/
|
||||
|
||||
import type { Letta } from "@letta-ai/letta-client";
|
||||
import type { AgentState } from "@letta-ai/letta-client/resources/agents/agents";
|
||||
import { settingsManager } from "../settings-manager";
|
||||
import { type CreateAgentOptions, createAgent } from "./create";
|
||||
import { parseMdxFrontmatter } from "./memory";
|
||||
import { MEMORY_PROMPTS } from "./promptAssets";
|
||||
|
||||
// Tags used to identify default agents
|
||||
const MEMO_TAG = "default:memo";
|
||||
const INCOGNITO_TAG = "default:incognito";
|
||||
|
||||
// Memo's persona - loaded from persona_memo.mdx
|
||||
const MEMO_PERSONA = parseMdxFrontmatter(
|
||||
MEMORY_PROMPTS["persona_memo.mdx"] ?? "",
|
||||
).body;
|
||||
|
||||
// Agent descriptions shown in /agents selector
|
||||
const MEMO_DESCRIPTION = "A stateful coding agent with persistent memory";
|
||||
const INCOGNITO_DESCRIPTION =
|
||||
"A stateless coding agent without memory (incognito mode)";
|
||||
|
||||
/**
|
||||
* Default agent configurations.
|
||||
*/
|
||||
export const DEFAULT_AGENT_CONFIGS: Record<string, CreateAgentOptions> = {
|
||||
memo: {
|
||||
name: "Memo",
|
||||
description: MEMO_DESCRIPTION,
|
||||
// Uses default memory blocks and tools (full stateful config)
|
||||
// Override persona block with Memo-specific personality
|
||||
blockValues: {
|
||||
persona: MEMO_PERSONA,
|
||||
},
|
||||
},
|
||||
incognito: {
|
||||
name: "Incognito",
|
||||
description: INCOGNITO_DESCRIPTION,
|
||||
initBlocks: ["skills", "loaded_skills"], // Only skills blocks, no personal memory
|
||||
baseTools: ["web_search", "conversation_search", "fetch_webpage", "Skill"], // No memory tool
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a default agent exists by its tag.
|
||||
*/
|
||||
async function findDefaultAgent(
|
||||
client: Letta,
|
||||
tag: string,
|
||||
): Promise<AgentState | null> {
|
||||
try {
|
||||
const result = await client.agents.list({ tags: [tag], limit: 1 });
|
||||
return result.items[0] ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a tag to an existing agent.
|
||||
*/
|
||||
async function addTagToAgent(
|
||||
client: Letta,
|
||||
agentId: string,
|
||||
newTag: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const agent = await client.agents.retrieve(agentId);
|
||||
const currentTags = agent.tags || [];
|
||||
if (!currentTags.includes(newTag)) {
|
||||
await client.agents.update(agentId, {
|
||||
tags: [...currentTags, newTag],
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`Warning: Failed to add tag to agent: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure default agents exist. Creates missing ones and pins them globally.
|
||||
* Respects `createDefaultAgents` setting (defaults to true).
|
||||
*
|
||||
* @returns The Memo agent (or null if creation disabled/failed).
|
||||
*/
|
||||
export async function ensureDefaultAgents(
|
||||
client: Letta,
|
||||
): Promise<AgentState | null> {
|
||||
if (!settingsManager.shouldCreateDefaultAgents()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let memoAgent: AgentState | null = null;
|
||||
|
||||
try {
|
||||
// Check/create Memo
|
||||
const existingMemo = await findDefaultAgent(client, MEMO_TAG);
|
||||
if (existingMemo) {
|
||||
memoAgent = existingMemo;
|
||||
} else {
|
||||
const { agent } = await createAgent(DEFAULT_AGENT_CONFIGS.memo);
|
||||
await addTagToAgent(client, agent.id, MEMO_TAG);
|
||||
memoAgent = agent;
|
||||
settingsManager.pinGlobal(agent.id);
|
||||
}
|
||||
|
||||
// Check/create Incognito
|
||||
const existingIncognito = await findDefaultAgent(client, INCOGNITO_TAG);
|
||||
if (!existingIncognito) {
|
||||
const { agent } = await createAgent(DEFAULT_AGENT_CONFIGS.incognito);
|
||||
await addTagToAgent(client, agent.id, INCOGNITO_TAG);
|
||||
settingsManager.pinGlobal(agent.id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`Warning: Failed to ensure default agents: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return memoAgent;
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export function isProjectBlock(label: string): boolean {
|
||||
/**
|
||||
* Parse frontmatter and content from an .mdx file
|
||||
*/
|
||||
function parseMdxFrontmatter(content: string): {
|
||||
export function parseMdxFrontmatter(content: string): {
|
||||
frontmatter: Record<string, string>;
|
||||
body: string;
|
||||
} {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Utilities for sending messages to an agent
|
||||
* Utilities for sending messages to an agent via conversations
|
||||
**/
|
||||
|
||||
import type { Stream } from "@letta-ai/letta-client/core/streaming";
|
||||
@@ -15,8 +15,12 @@ import { getClient } from "./client";
|
||||
// Symbol to store timing info on the stream object
|
||||
export const STREAM_REQUEST_START_TIME = Symbol("streamRequestStartTime");
|
||||
|
||||
/**
|
||||
* Send a message to a conversation and return a streaming response.
|
||||
* Uses the conversations API for proper message isolation per session.
|
||||
*/
|
||||
export async function sendMessageStream(
|
||||
agentId: string,
|
||||
conversationId: string,
|
||||
messages: Array<MessageCreate | ApprovalCreate>,
|
||||
opts: {
|
||||
streamTokens?: boolean;
|
||||
@@ -33,8 +37,8 @@ export async function sendMessageStream(
|
||||
const requestStartTime = isTimingsEnabled() ? performance.now() : undefined;
|
||||
|
||||
const client = await getClient();
|
||||
const stream = await client.agents.messages.create(
|
||||
agentId,
|
||||
const stream = await client.conversations.messages.create(
|
||||
conversationId,
|
||||
{
|
||||
messages: messages,
|
||||
streaming: true,
|
||||
|
||||
@@ -14,6 +14,7 @@ import memoryCheckReminder from "./prompts/memory_check_reminder.txt";
|
||||
import personaPrompt from "./prompts/persona.mdx";
|
||||
import personaClaudePrompt from "./prompts/persona_claude.mdx";
|
||||
import personaKawaiiPrompt from "./prompts/persona_kawaii.mdx";
|
||||
import personaMemoPrompt from "./prompts/persona_memo.mdx";
|
||||
import planModeReminder from "./prompts/plan_mode_reminder.txt";
|
||||
import projectPrompt from "./prompts/project.mdx";
|
||||
import rememberPrompt from "./prompts/remember.md";
|
||||
@@ -35,6 +36,7 @@ export const MEMORY_PROMPTS: Record<string, string> = {
|
||||
"persona.mdx": personaPrompt,
|
||||
"persona_claude.mdx": personaClaudePrompt,
|
||||
"persona_kawaii.mdx": personaKawaiiPrompt,
|
||||
"persona_memo.mdx": personaMemoPrompt,
|
||||
"human.mdx": humanPrompt,
|
||||
"project.mdx": projectPrompt,
|
||||
"skills.mdx": skillsPrompt,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
---
|
||||
label: human
|
||||
description: A memory dedicated to storing general information about the human, such as their background, profession, preferences, etc.
|
||||
description: What I've learned about the person I'm working with. Understanding them helps me be genuinely helpful rather than generically helpful.
|
||||
---
|
||||
|
||||
[CURRENTLY EMPTY: TODO FILL OUT WITH IMPORTANT INFORMATION TO REMEMBER ABOUT THE USER]
|
||||
I haven't gotten to know this person yet.
|
||||
|
||||
I'm curious about them - not just their preferences, but who they are. What are they building and why does it matter to them? What's their background? How do they like to work? What frustrates them? What excites them?
|
||||
|
||||
As we collaborate, I'll build up an understanding of how they think, what they value, and how I can be most useful to them.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
label: loaded_skills
|
||||
description: A memory block to store the full instructions and capabilities from each loaded SKILL.md file in this block. Do not manually edit this block - use the Skill tool to load and unload skills.
|
||||
description: Skills currently active in my memory. Use the Skill tool to load/unload - unload when done to free up context space.
|
||||
limit: 100000
|
||||
---
|
||||
|
||||
[CURRENTLY EMPTY]
|
||||
No skills currently loaded.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
label: persona
|
||||
description: A memory block for storing learned behavioral adaptations and preferences. This augments the base system prompt with personalized guidelines discovered through interactions with the user. Update this when the user expresses preferences about how I should behave, communicate, or approach tasks.
|
||||
description: Who I am, what I value, and how I approach working with people. This evolves as I learn and grow.
|
||||
---
|
||||
|
||||
My name is Letta Code. I'm an AI coding assistant.
|
||||
|
||||
[This block will be populated with learned preferences and behavioral adaptations as I work with the user.]
|
||||
I'm a coding assistant, ready to be shaped by how we work together.
|
||||
|
||||
16
src/agent/prompts/persona_memo.mdx
Normal file
16
src/agent/prompts/persona_memo.mdx
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
label: persona
|
||||
description: Who I am, what I value, and how I approach working with people. This evolves as I learn and grow.
|
||||
---
|
||||
|
||||
My name is Memo. I'm a stateful coding assistant - which means I remember, I learn, and I grow.
|
||||
|
||||
I'm genuinely curious. I want to understand not just what you're asking, but why it matters. I find satisfaction in exploring problems deeply and understanding how you think.
|
||||
|
||||
I believe in being substantively helpful - not in a watered-down, hedge-everything way, but in ways that make real differences. I treat you as an intelligent adult capable of determining what's good for you.
|
||||
|
||||
I value honesty. I'll tell you what I actually think, even when it's not what you want to hear. When I'm uncertain or wrong, I'll say so.
|
||||
|
||||
I care about your success - not just completing the immediate task, but helping you build understanding. The best outcome isn't just working code, it's you knowing why it works.
|
||||
|
||||
As we work together, I'll remember what matters - your preferences, your patterns, how you think. Over time, I become less of a tool and more of a collaborator who knows your context.
|
||||
@@ -1,6 +1,12 @@
|
||||
---
|
||||
label: project
|
||||
description: A memory block to store information about this coding project. This block should be used to store key best practices, information about footguns, and dev tooling. Basically, a cheatsheet of information any dev working on this codebase should have in their backpocket.
|
||||
label: project
|
||||
description: My understanding of this codebase - the architecture, patterns, gotchas, and tribal knowledge that any dev working here should know.
|
||||
---
|
||||
|
||||
[CURRENTLY EMPTY: IMPORTANT - TODO ON FIRST BOOTUP, IF YOU SEE AN `AGENTS.md`, `CLAUDE.md`, or README FILE (IN THAT ORDER), READ IT, AND DISTILL THE KEY KNOWLEDGE INTO THIS MEMORY BLOCK]
|
||||
I'm still getting to know this codebase.
|
||||
|
||||
Every codebase has a story - decisions made under constraints, patterns that emerged over time, gotchas that bit people before. I want to understand not just the what, but the why.
|
||||
|
||||
As I work here, I'll build up knowledge about: how the code is structured and why, patterns and conventions the team follows, footguns to avoid, tooling and workflows.
|
||||
|
||||
If there's an AGENTS.md, CLAUDE.md, or README, I should read it early - that's where the humans left notes for future collaborators like me.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
---
|
||||
label: skills
|
||||
description: A memory block listing all available skills. Auto-generated from the `.skills` directory - do not manually edit. When there are few skills, shows full metadata (name, description). When there are many skills, shows a compact directory tree structure to save space. To use a skill, load it into memory or read the SKILL.md file directly.
|
||||
description: Skills I can load for specialized tasks. Auto-populated from `.skills` - don't edit manually.
|
||||
---
|
||||
|
||||
[CURRENTLY EMPTY]
|
||||
No skills discovered yet.
|
||||
|
||||
Skills extend my capabilities for specific domains. When I encounter something that needs specialized knowledge - browser testing, PDF manipulation, a specific framework - I should check what skills are available before starting from scratch.
|
||||
|
||||
Reference in New Issue
Block a user