Fix default conversation system prompt recompilation routing [LET-7976] (#1373)
This commit is contained in:
@@ -270,59 +270,46 @@ export async function updateConversationLLMConfig(
|
|||||||
return client.conversations.update(conversationId, payload);
|
return client.conversations.update(conversationId, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecompileAgentSystemPromptOptions {
|
|
||||||
dryRun?: boolean;
|
|
||||||
/**
|
|
||||||
* Required when recompiling the special "default" conversation route.
|
|
||||||
* Passed as body param `agent_id` for agent-direct mode.
|
|
||||||
*/
|
|
||||||
agentId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConversationSystemPromptRecompileClient {
|
|
||||||
conversations: {
|
|
||||||
recompile: (
|
|
||||||
conversationId: string,
|
|
||||||
params: {
|
|
||||||
dry_run?: boolean;
|
|
||||||
},
|
|
||||||
) => Promise<string>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 conversationId - The conversation whose prompt should be recompiled
|
* @param conversationId - The conversation whose prompt should be recompiled
|
||||||
* @param options - Optional dry-run control
|
* @param agentId - Agent id for the parent conversation
|
||||||
|
* @param dryRun - 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(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
options: RecompileAgentSystemPromptOptions = {},
|
agentId: string,
|
||||||
clientOverride?: ConversationSystemPromptRecompileClient,
|
dryRun?: boolean,
|
||||||
|
clientOverride?: {
|
||||||
|
conversations: {
|
||||||
|
recompile: (
|
||||||
|
conversationId: string,
|
||||||
|
params: {
|
||||||
|
dry_run?: boolean;
|
||||||
|
agent_id?: string;
|
||||||
|
},
|
||||||
|
) => Promise<string>;
|
||||||
|
};
|
||||||
|
},
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const client = (clientOverride ??
|
const client = (clientOverride ?? (await getClient())) as Exclude<
|
||||||
(await getClient())) as ConversationSystemPromptRecompileClient;
|
typeof clientOverride,
|
||||||
|
undefined
|
||||||
|
>;
|
||||||
|
|
||||||
const params: {
|
if (!agentId) {
|
||||||
dry_run?: boolean;
|
throw new Error("recompileAgentSystemPrompt requires agentId");
|
||||||
agent_id?: string;
|
|
||||||
} = {
|
|
||||||
dry_run: options.dryRun,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (conversationId === "default") {
|
|
||||||
if (!options.agentId) {
|
|
||||||
throw new Error(
|
|
||||||
'recompileAgentSystemPrompt requires options.agentId when conversationId is "default"',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
params.agent_id = options.agentId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
dry_run: dryRun,
|
||||||
|
agent_id: agentId,
|
||||||
|
};
|
||||||
|
|
||||||
return client.conversations.recompile(conversationId, params);
|
return client.conversations.recompile(conversationId, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import {
|
import { recompileAgentSystemPrompt } from "../../agent/modify";
|
||||||
type RecompileAgentSystemPromptOptions,
|
|
||||||
recompileAgentSystemPrompt,
|
|
||||||
} from "../../agent/modify";
|
|
||||||
|
|
||||||
export type MemorySubagentType = "init" | "reflection";
|
export type MemorySubagentType = "init" | "reflection";
|
||||||
|
|
||||||
type RecompileAgentSystemPromptFn = (
|
type RecompileAgentSystemPromptFn = (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
options?: RecompileAgentSystemPromptOptions,
|
agentId: string,
|
||||||
|
dryRun?: boolean,
|
||||||
) => Promise<string>;
|
) => Promise<string>;
|
||||||
|
|
||||||
export interface MemorySubagentCompletionArgs {
|
export interface MemorySubagentCompletionArgs {
|
||||||
@@ -46,9 +44,7 @@ export async function handleMemorySubagentCompletion(
|
|||||||
inFlight = (async () => {
|
inFlight = (async () => {
|
||||||
do {
|
do {
|
||||||
deps.recompileQueuedByConversation.delete(conversationId);
|
deps.recompileQueuedByConversation.delete(conversationId);
|
||||||
await recompileAgentSystemPromptFn(conversationId, {
|
await recompileAgentSystemPromptFn(conversationId, agentId);
|
||||||
...(conversationId === "default" ? { agentId } : {}),
|
|
||||||
});
|
|
||||||
} while (deps.recompileQueuedByConversation.has(conversationId));
|
} while (deps.recompileQueuedByConversation.has(conversationId));
|
||||||
})().finally(() => {
|
})().finally(() => {
|
||||||
// Cleanup runs only after the shared promise settles, so every
|
// Cleanup runs only after the shared promise settles, so every
|
||||||
|
|||||||
@@ -3,71 +3,87 @@ import { recompileAgentSystemPrompt } from "../../agent/modify";
|
|||||||
|
|
||||||
describe("recompileAgentSystemPrompt", () => {
|
describe("recompileAgentSystemPrompt", () => {
|
||||||
test("calls the conversation recompile endpoint with mapped params", async () => {
|
test("calls the conversation recompile endpoint with mapped params", async () => {
|
||||||
const agentsRecompileMock = mock(
|
const conversationsRecompileMock = mock(
|
||||||
(_conversationId: 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 = {
|
||||||
conversations: {
|
conversations: {
|
||||||
recompile: agentsRecompileMock,
|
recompile: conversationsRecompileMock,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const compiledPrompt = await recompileAgentSystemPrompt(
|
const compiledPrompt = await recompileAgentSystemPrompt(
|
||||||
"conv-123",
|
"conv-123",
|
||||||
{
|
"agent-123",
|
||||||
dryRun: true,
|
true,
|
||||||
},
|
|
||||||
client,
|
client,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(compiledPrompt).toBe("compiled-system-prompt");
|
expect(compiledPrompt).toBe("compiled-system-prompt");
|
||||||
expect(agentsRecompileMock).toHaveBeenCalledWith("conv-123", {
|
expect(conversationsRecompileMock).toHaveBeenCalledWith("conv-123", {
|
||||||
dry_run: true,
|
dry_run: true,
|
||||||
|
agent_id: "agent-123",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("passes agent_id for default conversation recompiles", async () => {
|
test("passes agent_id for default conversation recompiles", async () => {
|
||||||
const agentsRecompileMock = mock(
|
const conversationsRecompileMock = mock(
|
||||||
(_conversationId: 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 = {
|
||||||
conversations: {
|
conversations: {
|
||||||
recompile: agentsRecompileMock,
|
recompile: conversationsRecompileMock,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await recompileAgentSystemPrompt(
|
await recompileAgentSystemPrompt("default", "agent-123", undefined, client);
|
||||||
"default",
|
|
||||||
{
|
|
||||||
agentId: "agent-123",
|
|
||||||
},
|
|
||||||
client,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(agentsRecompileMock).toHaveBeenCalledWith("default", {
|
expect(conversationsRecompileMock).toHaveBeenCalledWith("default", {
|
||||||
dry_run: undefined,
|
dry_run: undefined,
|
||||||
agent_id: "agent-123",
|
agent_id: "agent-123",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws when default conversation recompile lacks agent id", async () => {
|
test("passes non-default conversation ids through unchanged", async () => {
|
||||||
const agentsRecompileMock = mock(
|
const conversationsRecompileMock = mock(
|
||||||
(_conversationId: 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 = {
|
||||||
conversations: {
|
conversations: {
|
||||||
recompile: agentsRecompileMock,
|
recompile: conversationsRecompileMock,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await recompileAgentSystemPrompt(
|
||||||
|
"['default']",
|
||||||
|
"agent-123",
|
||||||
|
undefined,
|
||||||
|
client,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(conversationsRecompileMock).toHaveBeenCalledWith("['default']", {
|
||||||
|
dry_run: undefined,
|
||||||
|
agent_id: "agent-123",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws when conversation recompile has empty agent id", async () => {
|
||||||
|
const conversationsRecompileMock = mock(
|
||||||
|
(_conversationId: string, _params?: Record<string, unknown>) =>
|
||||||
|
Promise.resolve("compiled-system-prompt"),
|
||||||
|
);
|
||||||
|
const client = {
|
||||||
|
conversations: {
|
||||||
|
recompile: conversationsRecompileMock,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
recompileAgentSystemPrompt("default", {}, client),
|
recompileAgentSystemPrompt("default", "", undefined, client),
|
||||||
).rejects.toThrow(
|
).rejects.toThrow("recompileAgentSystemPrompt requires agentId");
|
||||||
'recompileAgentSystemPrompt requires options.agentId when conversationId is "default"',
|
expect(conversationsRecompileMock).not.toHaveBeenCalled();
|
||||||
);
|
|
||||||
expect(agentsRecompileMock).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
||||||
import type { RecompileAgentSystemPromptOptions } from "../../agent/modify";
|
|
||||||
import { handleMemorySubagentCompletion } from "../../cli/helpers/memorySubagentCompletion";
|
import { handleMemorySubagentCompletion } from "../../cli/helpers/memorySubagentCompletion";
|
||||||
|
|
||||||
const recompileAgentSystemPromptMock = mock(
|
const recompileAgentSystemPromptMock = mock(
|
||||||
(_conversationId: string, _opts?: RecompileAgentSystemPromptOptions) =>
|
(_conversationId: string, _agentId: string, _dryRun?: boolean) =>
|
||||||
Promise.resolve("compiled-system-prompt"),
|
Promise.resolve("compiled-system-prompt"),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@ describe("memory subagent recompile handling", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
recompileAgentSystemPromptMock.mockReset();
|
recompileAgentSystemPromptMock.mockReset();
|
||||||
recompileAgentSystemPromptMock.mockImplementation(
|
recompileAgentSystemPromptMock.mockImplementation(
|
||||||
(_agentId: string, _opts?: RecompileAgentSystemPromptOptions) =>
|
(_conversationId: string, _agentId: string, _dryRun?: boolean) =>
|
||||||
Promise.resolve("compiled-system-prompt"),
|
Promise.resolve("compiled-system-prompt"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -44,7 +43,7 @@ describe("memory subagent recompile handling", () => {
|
|||||||
);
|
);
|
||||||
expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith(
|
expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith(
|
||||||
"conv-init-1",
|
"conv-init-1",
|
||||||
{},
|
"agent-init-1",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,9 +62,10 @@ describe("memory subagent recompile handling", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith("default", {
|
expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith(
|
||||||
agentId: "agent-default",
|
"default",
|
||||||
});
|
"agent-default",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("queues a trailing recompile when later completions land mid-flight", async () => {
|
test("queues a trailing recompile when later completions land mid-flight", async () => {
|
||||||
@@ -176,7 +176,13 @@ describe("memory subagent recompile handling", () => {
|
|||||||
"Reflected on /palace, the halls remember more now.",
|
"Reflected on /palace, the halls remember more now.",
|
||||||
);
|
);
|
||||||
expect(recompileAgentSystemPromptMock).toHaveBeenCalledTimes(2);
|
expect(recompileAgentSystemPromptMock).toHaveBeenCalledTimes(2);
|
||||||
expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith("conv-a", {});
|
expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith(
|
||||||
expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith("conv-b", {});
|
"conv-a",
|
||||||
|
"agent-shared",
|
||||||
|
);
|
||||||
|
expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith(
|
||||||
|
"conv-b",
|
||||||
|
"agent-shared",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user