fix: use client.conversations.recompile instead of client.agents.reco… (#1339)

Co-authored-by: Letta Code <noreply@letta.com>
Co-authored-by: cpacker <packercharles@gmail.com>
This commit is contained in:
Kevin Lin
2026-03-10 17:22:37 -07:00
committed by GitHub
parent 2a97f90a93
commit 8f13d9a25e
6 changed files with 111 additions and 63 deletions

View File

@@ -249,16 +249,14 @@ export async function updateConversationLLMConfig(
export interface RecompileAgentSystemPromptOptions { export interface RecompileAgentSystemPromptOptions {
dryRun?: boolean; dryRun?: boolean;
updateTimestamp?: boolean;
} }
interface AgentSystemPromptRecompileClient { interface ConversationSystemPromptRecompileClient {
agents: { conversations: {
recompile: ( recompile: (
agentId: string, conversationId: string,
params: { params: {
dry_run?: boolean; dry_run?: boolean;
update_timestamp?: boolean;
}, },
) => Promise<string>; ) => Promise<string>;
}; };
@@ -268,22 +266,21 @@ interface AgentSystemPromptRecompileClient {
* Recompile an agent's system prompt after memory writes so server-side prompt * Recompile an agent's system prompt after memory writes so server-side prompt
* state picks up the latest memory content. * state picks up the latest memory content.
* *
* @param agentId - The agent ID to recompile * @param conversationId - The conversation whose prompt should be recompiled
* @param options - Optional dry-run/timestamp controls * @param options - Optional dry-run control
* @param clientOverride - Optional injected client for tests * @param clientOverride - Optional injected client for tests
* @returns The compiled system prompt returned by the API * @returns The compiled system prompt returned by the API
*/ */
export async function recompileAgentSystemPrompt( export async function recompileAgentSystemPrompt(
agentId: string, conversationId: string,
options: RecompileAgentSystemPromptOptions = {}, options: RecompileAgentSystemPromptOptions = {},
clientOverride?: AgentSystemPromptRecompileClient, clientOverride?: ConversationSystemPromptRecompileClient,
): Promise<string> { ): Promise<string> {
const client = (clientOverride ?? const client = (clientOverride ??
(await getClient())) as AgentSystemPromptRecompileClient; (await getClient())) as ConversationSystemPromptRecompileClient;
return client.agents.recompile(agentId, { return client.conversations.recompile(conversationId, {
dry_run: options.dryRun, dry_run: options.dryRun,
update_timestamp: options.updateTimestamp,
}); });
} }

View File

@@ -1701,10 +1701,12 @@ export default function App({
const initProgressByAgentRef = useRef( const initProgressByAgentRef = useRef(
new Map<string, { shallowCompleted: boolean; deepFired: boolean }>(), new Map<string, { shallowCompleted: boolean; deepFired: boolean }>(),
); );
const systemPromptRecompileByAgentRef = useRef( const systemPromptRecompileByConversationRef = useRef(
new Map<string, Promise<void>>(), new Map<string, Promise<void>>(),
); );
const queuedSystemPromptRecompileByAgentRef = useRef(new Set<string>()); const queuedSystemPromptRecompileByConversationRef = useRef(
new Set<string>(),
);
const updateInitProgress = ( const updateInitProgress = (
forAgentId: string, forAgentId: string,
update: Partial<{ shallowCompleted: boolean; deepFired: boolean }>, update: Partial<{ shallowCompleted: boolean; deepFired: boolean }>,
@@ -9289,15 +9291,17 @@ export default function App({
const msg = await handleMemorySubagentCompletion( const msg = await handleMemorySubagentCompletion(
{ {
agentId, agentId,
conversationId: conversationIdRef.current,
subagentType: "init", subagentType: "init",
initDepth: "deep", initDepth: "deep",
success, success,
error, error,
}, },
{ {
recompileByAgent: systemPromptRecompileByAgentRef.current, recompileByConversation:
recompileQueuedByAgent: systemPromptRecompileByConversationRef.current,
queuedSystemPromptRecompileByAgentRef.current, recompileQueuedByConversation:
queuedSystemPromptRecompileByConversationRef.current,
updateInitProgress, updateInitProgress,
logRecompileFailure: (message) => logRecompileFailure: (message) =>
debugWarn("memory", message), debugWarn("memory", message),
@@ -9476,15 +9480,17 @@ export default function App({
const msg = await handleMemorySubagentCompletion( const msg = await handleMemorySubagentCompletion(
{ {
agentId, agentId,
conversationId: conversationIdRef.current,
subagentType: "init", subagentType: "init",
initDepth: "shallow", initDepth: "shallow",
success, success,
error, error,
}, },
{ {
recompileByAgent: systemPromptRecompileByAgentRef.current, recompileByConversation:
recompileQueuedByAgent: systemPromptRecompileByConversationRef.current,
queuedSystemPromptRecompileByAgentRef.current, recompileQueuedByConversation:
queuedSystemPromptRecompileByConversationRef.current,
updateInitProgress, updateInitProgress,
logRecompileFailure: (message) => logRecompileFailure: (message) =>
debugWarn("memory", message), debugWarn("memory", message),
@@ -9605,14 +9611,16 @@ ${SYSTEM_REMINDER_CLOSE}
const msg = await handleMemorySubagentCompletion( const msg = await handleMemorySubagentCompletion(
{ {
agentId, agentId,
conversationId: conversationIdRef.current,
subagentType: "reflection", subagentType: "reflection",
success, success,
error, error,
}, },
{ {
recompileByAgent: systemPromptRecompileByAgentRef.current, recompileByConversation:
recompileQueuedByAgent: systemPromptRecompileByConversationRef.current,
queuedSystemPromptRecompileByAgentRef.current, recompileQueuedByConversation:
queuedSystemPromptRecompileByConversationRef.current,
updateInitProgress, updateInitProgress,
logRecompileFailure: (message) => logRecompileFailure: (message) =>
debugWarn("memory", message), debugWarn("memory", message),
@@ -9660,15 +9668,17 @@ ${SYSTEM_REMINDER_CLOSE}
const msg = await handleMemorySubagentCompletion( const msg = await handleMemorySubagentCompletion(
{ {
agentId, agentId,
conversationId: conversationIdRef.current,
subagentType: "init", subagentType: "init",
initDepth: "deep", initDepth: "deep",
success, success,
error, error,
}, },
{ {
recompileByAgent: systemPromptRecompileByAgentRef.current, recompileByConversation:
recompileQueuedByAgent: systemPromptRecompileByConversationRef.current,
queuedSystemPromptRecompileByAgentRef.current, recompileQueuedByConversation:
queuedSystemPromptRecompileByConversationRef.current,
updateInitProgress, updateInitProgress,
logRecompileFailure: (message) => logRecompileFailure: (message) =>
debugWarn("memory", message), debugWarn("memory", message),

View File

@@ -12,13 +12,14 @@ export interface MemoryInitProgressUpdate {
} }
type RecompileAgentSystemPromptFn = ( type RecompileAgentSystemPromptFn = (
agentId: string, conversationId: string,
options?: RecompileAgentSystemPromptOptions, options?: RecompileAgentSystemPromptOptions,
) => Promise<string>; ) => Promise<string>;
export type MemorySubagentCompletionArgs = export type MemorySubagentCompletionArgs =
| { | {
agentId: string; agentId: string;
conversationId: string;
subagentType: "init"; subagentType: "init";
initDepth: MemoryInitDepth; initDepth: MemoryInitDepth;
success: boolean; success: boolean;
@@ -26,6 +27,7 @@ export type MemorySubagentCompletionArgs =
} }
| { | {
agentId: string; agentId: string;
conversationId: string;
subagentType: "reflection"; subagentType: "reflection";
initDepth?: never; initDepth?: never;
success: boolean; success: boolean;
@@ -33,8 +35,8 @@ export type MemorySubagentCompletionArgs =
}; };
export interface MemorySubagentCompletionDeps { export interface MemorySubagentCompletionDeps {
recompileByAgent: Map<string, Promise<void>>; recompileByConversation: Map<string, Promise<void>>;
recompileQueuedByAgent: Set<string>; recompileQueuedByConversation: Set<string>;
updateInitProgress: ( updateInitProgress: (
agentId: string, agentId: string,
update: Partial<MemoryInitProgressUpdate>, update: Partial<MemoryInitProgressUpdate>,
@@ -51,7 +53,8 @@ export async function handleMemorySubagentCompletion(
args: MemorySubagentCompletionArgs, args: MemorySubagentCompletionArgs,
deps: MemorySubagentCompletionDeps, deps: MemorySubagentCompletionDeps,
): Promise<string> { ): Promise<string> {
const { agentId, subagentType, initDepth, success, error } = args; const { agentId, conversationId, subagentType, initDepth, success, error } =
args;
const recompileAgentSystemPromptFn = const recompileAgentSystemPromptFn =
deps.recompileAgentSystemPromptImpl ?? recompileAgentSystemPrompt; deps.recompileAgentSystemPromptImpl ?? recompileAgentSystemPrompt;
let recompileError: string | null = null; let recompileError: string | null = null;
@@ -67,25 +70,23 @@ export async function handleMemorySubagentCompletion(
} }
try { try {
let inFlight = deps.recompileByAgent.get(agentId); let inFlight = deps.recompileByConversation.get(conversationId);
if (!inFlight) { if (!inFlight) {
inFlight = (async () => { inFlight = (async () => {
do { do {
deps.recompileQueuedByAgent.delete(agentId); deps.recompileQueuedByConversation.delete(conversationId);
await recompileAgentSystemPromptFn(agentId, { await recompileAgentSystemPromptFn(conversationId, {});
updateTimestamp: true, } while (deps.recompileQueuedByConversation.has(conversationId));
});
} while (deps.recompileQueuedByAgent.has(agentId));
})().finally(() => { })().finally(() => {
// Cleanup runs only after the shared promise settles, so every // Cleanup runs only after the shared promise settles, so every
// concurrent caller awaits the same full recompile lifecycle. // concurrent caller awaits the same full recompile lifecycle.
deps.recompileQueuedByAgent.delete(agentId); deps.recompileQueuedByConversation.delete(conversationId);
deps.recompileByAgent.delete(agentId); deps.recompileByConversation.delete(conversationId);
}); });
deps.recompileByAgent.set(agentId, inFlight); deps.recompileByConversation.set(conversationId, inFlight);
} else { } else {
deps.recompileQueuedByAgent.add(agentId); deps.recompileQueuedByConversation.add(conversationId);
} }
await inFlight; await inFlight;
@@ -95,7 +96,7 @@ export async function handleMemorySubagentCompletion(
? recompileFailure.message ? recompileFailure.message
: String(recompileFailure); : String(recompileFailure);
deps.logRecompileFailure?.( deps.logRecompileFailure?.(
`Failed to recompile system prompt after ${subagentType} subagent for ${agentId}: ${recompileError}`, `Failed to recompile system prompt after ${subagentType} subagent for ${agentId} in conversation ${conversationId}: ${recompileError}`,
); );
} }
} }

View File

@@ -4,11 +4,11 @@ import { recompileAgentSystemPrompt } from "../../agent/modify";
describe("recompileAgentSystemPrompt", () => { describe("recompileAgentSystemPrompt", () => {
test("calls the Letta agent recompile endpoint with mapped params", async () => { test("calls the Letta agent recompile endpoint with mapped params", async () => {
const agentsRecompileMock = mock( const agentsRecompileMock = mock(
(_agentId: string, _params?: Record<string, unknown>) => (_conversationId: string, _params?: Record<string, unknown>) =>
Promise.resolve("compiled-system-prompt"), Promise.resolve("compiled-system-prompt"),
); );
const client = { const client = {
agents: { conversations: {
recompile: agentsRecompileMock, recompile: agentsRecompileMock,
}, },
}; };
@@ -16,7 +16,6 @@ describe("recompileAgentSystemPrompt", () => {
const compiledPrompt = await recompileAgentSystemPrompt( const compiledPrompt = await recompileAgentSystemPrompt(
"agent-123", "agent-123",
{ {
updateTimestamp: true,
dryRun: true, dryRun: true,
}, },
client, client,
@@ -25,7 +24,6 @@ describe("recompileAgentSystemPrompt", () => {
expect(compiledPrompt).toBe("compiled-system-prompt"); expect(compiledPrompt).toBe("compiled-system-prompt");
expect(agentsRecompileMock).toHaveBeenCalledWith("agent-123", { expect(agentsRecompileMock).toHaveBeenCalledWith("agent-123", {
dry_run: true, dry_run: true,
update_timestamp: true,
}); });
}); });
}); });

View File

@@ -3,7 +3,7 @@ import type { RecompileAgentSystemPromptOptions } from "../../agent/modify";
import { handleMemorySubagentCompletion } from "../../cli/helpers/memorySubagentCompletion"; import { handleMemorySubagentCompletion } from "../../cli/helpers/memorySubagentCompletion";
const recompileAgentSystemPromptMock = mock( const recompileAgentSystemPromptMock = mock(
(_agentId: string, _opts?: RecompileAgentSystemPromptOptions) => (_conversationId: string, _opts?: RecompileAgentSystemPromptOptions) =>
Promise.resolve("compiled-system-prompt"), Promise.resolve("compiled-system-prompt"),
); );
@@ -33,13 +33,14 @@ describe("memory subagent recompile handling", () => {
const message = await handleMemorySubagentCompletion( const message = await handleMemorySubagentCompletion(
{ {
agentId: "agent-init-1", agentId: "agent-init-1",
conversationId: "conv-init-1",
subagentType: "init", subagentType: "init",
initDepth: "shallow", initDepth: "shallow",
success: true, success: true,
}, },
{ {
recompileByAgent: new Map(), recompileByConversation: new Map(),
recompileQueuedByAgent: new Set(), recompileQueuedByConversation: new Set(),
recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock, recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock,
updateInitProgress: (agentId, update) => { updateInitProgress: (agentId, update) => {
progressUpdates.push({ progressUpdates.push({
@@ -60,10 +61,8 @@ describe("memory subagent recompile handling", () => {
}, },
]); ]);
expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith( expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith(
"agent-init-1", "conv-init-1",
{ {},
updateTimestamp: true,
},
); );
}); });
@@ -74,11 +73,11 @@ describe("memory subagent recompile handling", () => {
.mockImplementationOnce(() => firstDeferred.promise) .mockImplementationOnce(() => firstDeferred.promise)
.mockImplementationOnce(() => secondDeferred.promise); .mockImplementationOnce(() => secondDeferred.promise);
const recompileByAgent = new Map<string, Promise<void>>(); const recompileByConversation = new Map<string, Promise<void>>();
const recompileQueuedByAgent = new Set<string>(); const recompileQueuedByConversation = new Set<string>();
const deps = { const deps = {
recompileByAgent, recompileByConversation,
recompileQueuedByAgent, recompileQueuedByConversation,
recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock, recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock,
updateInitProgress: () => {}, updateInitProgress: () => {},
}; };
@@ -86,6 +85,7 @@ describe("memory subagent recompile handling", () => {
const first = handleMemorySubagentCompletion( const first = handleMemorySubagentCompletion(
{ {
agentId: "agent-shared", agentId: "agent-shared",
conversationId: "conv-shared",
subagentType: "reflection", subagentType: "reflection",
success: true, success: true,
}, },
@@ -94,6 +94,7 @@ describe("memory subagent recompile handling", () => {
const second = handleMemorySubagentCompletion( const second = handleMemorySubagentCompletion(
{ {
agentId: "agent-shared", agentId: "agent-shared",
conversationId: "conv-shared",
subagentType: "reflection", subagentType: "reflection",
success: true, success: true,
}, },
@@ -102,6 +103,7 @@ describe("memory subagent recompile handling", () => {
const third = handleMemorySubagentCompletion( const third = handleMemorySubagentCompletion(
{ {
agentId: "agent-shared", agentId: "agent-shared",
conversationId: "conv-shared",
subagentType: "reflection", subagentType: "reflection",
success: true, success: true,
}, },
@@ -109,14 +111,14 @@ describe("memory subagent recompile handling", () => {
); );
expect(recompileAgentSystemPromptMock).toHaveBeenCalledTimes(1); expect(recompileAgentSystemPromptMock).toHaveBeenCalledTimes(1);
expect(recompileByAgent.has("agent-shared")).toBe(true); expect(recompileByConversation.has("conv-shared")).toBe(true);
expect(recompileQueuedByAgent.has("agent-shared")).toBe(true); expect(recompileQueuedByConversation.has("conv-shared")).toBe(true);
firstDeferred.resolve("compiled-system-prompt"); firstDeferred.resolve("compiled-system-prompt");
await Promise.resolve(); await Promise.resolve();
expect(recompileAgentSystemPromptMock).toHaveBeenCalledTimes(2); expect(recompileAgentSystemPromptMock).toHaveBeenCalledTimes(2);
expect(recompileByAgent.has("agent-shared")).toBe(true); expect(recompileByConversation.has("conv-shared")).toBe(true);
secondDeferred.resolve("compiled-system-prompt"); secondDeferred.resolve("compiled-system-prompt");
@@ -134,7 +136,47 @@ describe("memory subagent recompile handling", () => {
expect(thirdMessage).toBe( expect(thirdMessage).toBe(
"Reflected on /palace, the halls remember more now.", "Reflected on /palace, the halls remember more now.",
); );
expect(recompileByAgent.size).toBe(0); expect(recompileByConversation.size).toBe(0);
expect(recompileQueuedByAgent.size).toBe(0); expect(recompileQueuedByConversation.size).toBe(0);
});
test("does not coalesce recompiles across different conversations for same agent", async () => {
const deps = {
recompileByConversation: new Map<string, Promise<void>>(),
recompileQueuedByConversation: new Set<string>(),
recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock,
updateInitProgress: () => {},
};
const [firstMessage, secondMessage] = await Promise.all([
handleMemorySubagentCompletion(
{
agentId: "agent-shared",
conversationId: "conv-a",
subagentType: "reflection",
success: true,
},
deps,
),
handleMemorySubagentCompletion(
{
agentId: "agent-shared",
conversationId: "conv-b",
subagentType: "reflection",
success: true,
},
deps,
),
]);
expect(firstMessage).toBe(
"Reflected on /palace, the halls remember more now.",
);
expect(secondMessage).toBe(
"Reflected on /palace, the halls remember more now.",
);
expect(recompileAgentSystemPromptMock).toHaveBeenCalledTimes(2);
expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith("conv-a", {});
expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith("conv-b", {});
}); });
}); });

View File

@@ -143,7 +143,7 @@ describe("background onComplete → flush wiring in App.tsx", () => {
const onCompleteIdx = source.indexOf("onComplete:", initBlock); const onCompleteIdx = source.indexOf("onComplete:", initBlock);
expect(onCompleteIdx).toBeGreaterThan(-1); expect(onCompleteIdx).toBeGreaterThan(-1);
const onCompleteWindow = source.slice(onCompleteIdx, onCompleteIdx + 900); const onCompleteWindow = source.slice(onCompleteIdx, onCompleteIdx + 1600);
expect(onCompleteWindow).toContain("await handleMemorySubagentCompletion("); expect(onCompleteWindow).toContain("await handleMemorySubagentCompletion(");
expect(onCompleteWindow).toContain("appendTaskNotificationEvents("); expect(onCompleteWindow).toContain("appendTaskNotificationEvents(");
}); });
@@ -157,7 +157,7 @@ describe("background onComplete → flush wiring in App.tsx", () => {
const onCompleteIdx = source.indexOf("onComplete:", reflectionBlock); const onCompleteIdx = source.indexOf("onComplete:", reflectionBlock);
expect(onCompleteIdx).toBeGreaterThan(-1); expect(onCompleteIdx).toBeGreaterThan(-1);
const onCompleteWindow = source.slice(onCompleteIdx, onCompleteIdx + 700); const onCompleteWindow = source.slice(onCompleteIdx, onCompleteIdx + 1400);
expect(onCompleteWindow).toContain("await handleMemorySubagentCompletion("); expect(onCompleteWindow).toContain("await handleMemorySubagentCompletion(");
expect(onCompleteWindow).toContain("appendTaskNotificationEvents("); expect(onCompleteWindow).toContain("appendTaskNotificationEvents(");
}); });