feat: use conversations endpoint for default conversation (#1206)

This commit is contained in:
cthomas
2026-02-27 15:37:15 -08:00
committed by GitHub
parent 841e2332f3
commit 0d5dab198a
13 changed files with 148 additions and 289 deletions

View File

@@ -28,10 +28,6 @@ export interface BootstrapMessagesPage {
getPaginatedItems(): unknown[];
}
export interface BootstrapAgentsPage {
items: unknown[];
}
export interface BootstrapHandlerClient {
conversations: {
messages: {
@@ -46,20 +42,6 @@ export interface BootstrapHandlerClient {
): Promise<BootstrapMessagesPage>;
};
};
agents: {
messages: {
list(
agentId: string,
opts: {
limit: number;
order: "asc" | "desc";
before?: string;
after?: string;
conversation_id?: "default";
},
): Promise<BootstrapAgentsPage>;
};
};
}
export interface BootstrapHandlerSessionContext {
@@ -115,22 +97,11 @@ export async function handleBootstrapSessionState(
);
const listStart = Date.now();
let items: unknown[];
if (route.kind === "conversations") {
const page = await client.conversations.messages.list(
route.conversationId,
{ limit, order },
);
items = page.getPaginatedItems();
} else {
const page = await client.agents.messages.list(route.agentId, {
limit,
order,
conversation_id: "default",
});
items = page.items;
}
const page = await client.conversations.messages.list(
route.conversationId,
{ limit, order },
);
const items = page.getPaginatedItems();
const listEnd = Date.now();
const hasMore = items.length >= limit;

View File

@@ -479,22 +479,24 @@ export async function getResumeData(
}
const retrievedMessages = await client.messages.retrieve(lastInContextId);
// Fetch message history for backfill using conversation_id=default
// This filters to only the default conversation's messages (like the ADE does)
// Fetch message history for backfill using the agent ID as conversation_id
// (the server accepts agent-* IDs for default conversation messages)
// Wrapped in try/catch so backfill failures don't crash the CLI (e.g., older servers
// may not support conversation_id filter)
// may not support this pattern)
if (includeMessageHistory && isBackfillEnabled()) {
try {
const messagesPage = await client.agents.messages.list(agent.id, {
limit: BACKFILL_PAGE_LIMIT,
order: "desc",
conversation_id: "default", // Key: filter to default conversation only
});
messages = sortChronological(messagesPage.items);
const messagesPage = await client.conversations.messages.list(
agent.id,
{
limit: BACKFILL_PAGE_LIMIT,
order: "desc",
},
);
messages = sortChronological(messagesPage.getPaginatedItems());
if (process.env.DEBUG) {
console.log(
`[DEBUG] agents.messages.list(conversation_id=default) returned ${messages.length} messages`,
`[DEBUG] conversations.messages.list(${agent.id}) returned ${messages.length} messages`,
);
}
} catch (backfillError) {

View File

@@ -22,10 +22,6 @@ export interface ConversationsMessagesPage {
getPaginatedItems(): unknown[];
}
export interface AgentsMessagesPage {
items: unknown[];
}
export interface ListMessagesHandlerClient {
conversations: {
messages: {
@@ -40,20 +36,6 @@ export interface ListMessagesHandlerClient {
): Promise<ConversationsMessagesPage>;
};
};
agents: {
messages: {
list(
agentId: string,
opts: {
limit: number;
order: "asc" | "desc";
before?: string;
after?: string;
conversation_id?: "default";
},
): Promise<AgentsMessagesPage>;
};
};
}
export interface HandleListMessagesParams {
@@ -97,29 +79,17 @@ export async function handleListMessages(
};
try {
let items: unknown[];
const route = resolveListMessagesRoute(
listReq,
sessionConversationId,
sessionAgentId,
);
if (route.kind === "conversations") {
const page = await client.conversations.messages.list(
route.conversationId,
{ limit, order, ...cursorOpts },
);
items = page.getPaginatedItems();
} else {
const page = await client.agents.messages.list(route.agentId, {
limit,
order,
conversation_id: "default",
...cursorOpts,
});
items = page.items;
}
const page = await client.conversations.messages.list(
route.conversationId,
{ limit, order, ...cursorOpts },
);
const items = page.getPaginatedItems();
const hasMore = items.length >= limit;
const oldestId =

View File

@@ -4,17 +4,17 @@
* Extracted from headless.ts so it can be tested in isolation without
* spinning up a real Letta client.
*
* Routing rules (in priority order):
* 1. Explicit `conversation_id` in the request → conversations.messages.list
* 2. Session is on a named conversation (not "default") → conversations.messages.list
* 3. Session is on the default conversation → agents.messages.list
* All paths now use the conversations endpoint. For the default conversation,
* the agent ID is passed as the conversation_id (the server accepts agent-*
* IDs for agent-direct messaging).
*/
import type { ListMessagesControlRequest } from "../types/protocol";
export type ListMessagesRoute =
| { kind: "conversations"; conversationId: string }
| { kind: "agents"; agentId: string };
export type ListMessagesRoute = {
kind: "conversations";
conversationId: string;
};
/**
* Resolve which Letta API endpoint to call for a list_messages request.
@@ -35,7 +35,8 @@ export function resolveListMessagesRoute(
return { kind: "conversations", conversationId: targetConvId };
}
// Session is on the agent's default conversation
// use request's agent_id if supplied (e.g. explicit override), else session's
return { kind: "agents", agentId: listReq.agent_id ?? sessionAgentId };
// Default conversation: pass the agent ID to the conversations endpoint.
// The server accepts agent-* IDs for agent-direct messaging.
const agentId = listReq.agent_id ?? sessionAgentId;
return { kind: "conversations", conversationId: agentId };
}

View File

@@ -32,11 +32,12 @@ export function getStreamToolContextId(
/**
* Send a message to a conversation and return a streaming response.
* Uses the conversations API for proper message isolation per session.
* Uses the conversations API for all conversations.
*
* For the "default" conversation (agent's primary message history without
* an explicit conversation object), pass conversationId="default" and
* provide agentId in opts. This uses the agents messages API instead.
* provide agentId in opts. The server accepts agent IDs as the
* conversation_id path parameter for agent-direct messaging.
*/
export async function sendMessageStream(
conversationId: string,
@@ -58,48 +59,35 @@ export async function sendMessageStream(
await waitForToolsetReady();
const { clientTools, contextId } = captureToolExecutionContext();
let stream: Stream<LettaStreamingResponse>;
// For "default" conversation, pass the agent ID to the conversations endpoint.
// The server accepts agent-* IDs for agent-direct messaging.
const resolvedConversationId =
conversationId === "default" ? opts.agentId : conversationId;
if (!resolvedConversationId) {
throw new Error(
"agentId is required in opts when using default conversation",
);
}
if (process.env.DEBUG) {
console.log(
`[DEBUG] sendMessageStream: conversationId=${conversationId}, useAgentsRoute=${conversationId === "default"}`,
`[DEBUG] sendMessageStream: conversationId=${conversationId}, resolved=${resolvedConversationId}`,
);
}
if (conversationId === "default") {
// Use agents route for default conversation (agent's primary message history)
if (!opts.agentId) {
throw new Error(
"agentId is required in opts when using default conversation",
);
}
stream = await client.agents.messages.create(
opts.agentId,
{
messages: messages,
streaming: true,
stream_tokens: opts.streamTokens ?? true,
background: opts.background ?? true,
client_tools: clientTools,
include_compaction_messages: true,
},
requestOptions,
);
} else {
// Use conversations route for explicit conversations
stream = await client.conversations.messages.create(
conversationId,
{
messages: messages,
streaming: true,
stream_tokens: opts.streamTokens ?? true,
background: opts.background ?? true,
client_tools: clientTools,
include_compaction_messages: true,
},
requestOptions,
);
}
const stream = await client.conversations.messages.create(
resolvedConversationId,
{
messages: messages,
streaming: true,
stream_tokens: opts.streamTokens ?? true,
background: opts.background ?? true,
client_tools: clientTools,
include_compaction_messages: true,
},
requestOptions,
);
if (requestStartTime !== undefined) {
streamRequestStartTimes.set(stream as object, requestStartTime);

View File

@@ -5690,10 +5690,11 @@ export default function App({
// causing CONFLICT on the next user message.
getClient()
.then((client) => {
if (conversationIdRef.current === "default") {
return client.agents.messages.cancel(agentIdRef.current);
}
return client.conversations.cancel(conversationIdRef.current);
const cancelId =
conversationIdRef.current === "default"
? agentIdRef.current
: conversationIdRef.current;
return client.conversations.cancel(cancelId);
})
.catch(() => {
// Silently ignore - cancellation already happened client-side
@@ -5808,11 +5809,11 @@ export default function App({
// Don't wait for it or show errors since user already got feedback
getClient()
.then((client) => {
// Use agents API for "default" conversation (primary message history)
if (conversationIdRef.current === "default") {
return client.agents.messages.cancel(agentIdRef.current);
}
return client.conversations.cancel(conversationIdRef.current);
const cancelId =
conversationIdRef.current === "default"
? agentIdRef.current
: conversationIdRef.current;
return client.conversations.cancel(cancelId);
})
.catch(() => {
// Silently ignore - cancellation already happened client-side
@@ -5832,12 +5833,11 @@ export default function App({
setInterruptRequested(true);
try {
const client = await getClient();
// Use agents API for "default" conversation (primary message history)
if (conversationIdRef.current === "default") {
await client.agents.messages.cancel(agentIdRef.current);
} else {
await client.conversations.cancel(conversationIdRef.current);
}
const cancelId =
conversationIdRef.current === "default"
? agentIdRef.current
: conversationIdRef.current;
await client.conversations.cancel(cancelId);
if (abortControllerRef.current) {
abortControllerRef.current.abort();
@@ -7886,15 +7886,14 @@ export default function App({
}
: undefined;
// Use agent-level compact API for "default" conversation,
// otherwise use conversation-level API
const result =
const compactId =
conversationIdRef.current === "default"
? await client.agents.messages.compact(agentId, compactParams)
: await client.conversations.messages.compact(
conversationIdRef.current,
compactParams,
);
? agentId
: conversationIdRef.current;
const result = await client.conversations.messages.compact(
compactId,
compactParams,
);
// Format success message with before/after counts and summary
const outputLines = [

View File

@@ -242,12 +242,14 @@ export function ConversationSelector({
let defaultConversation: EnrichedConversation | null = null;
if (!afterCursor) {
try {
const defaultMessages = await client.agents.messages.list(agentId, {
limit: 20,
order: "desc",
conversation_id: "default", // Filter to default conversation only
});
const defaultMsgItems = defaultMessages.items;
const defaultMessages = await client.conversations.messages.list(
agentId,
{
limit: 20,
order: "desc",
},
);
const defaultMsgItems = defaultMessages.getPaginatedItems();
if (defaultMsgItems.length > 0) {
const defaultStats = getMessageStats(
[...defaultMsgItems].reverse(),

View File

@@ -159,14 +159,14 @@ export async function runMessagesSubcommand(argv: string[]): Promise<number> {
return 1;
}
const response = await client.agents.messages.list(agentId, {
const response = await client.conversations.messages.list(agentId, {
limit: parseLimit(parsed.values.limit, 20),
after: parsed.values.after,
before: parsed.values.before,
order,
});
const messages = response.items ?? [];
const messages = response.getPaginatedItems() ?? [];
const startDate = parsed.values["start-date"];
const endDate = parsed.values["end-date"];

View File

@@ -104,10 +104,10 @@ describe("getResumeData", () => {
in_context_message_ids: ["msg-last"],
}));
const conversationsList = mock(async () => ({
getPaginatedItems: () => [],
}));
const agentsList = mock(async () => ({
items: [makeUserMessage("msg-a"), makeUserMessage("msg-b")],
getPaginatedItems: () => [
makeUserMessage("msg-a"),
makeUserMessage("msg-b"),
],
}));
const messagesRetrieve = mock(async () => [makeUserMessage()]);
@@ -116,14 +116,13 @@ describe("getResumeData", () => {
retrieve: conversationsRetrieve,
messages: { list: conversationsList },
},
agents: { messages: { list: agentsList } },
messages: { retrieve: messagesRetrieve },
} as unknown as Letta;
const resume = await getResumeData(client, makeAgent(), "default");
expect(messagesRetrieve).toHaveBeenCalledTimes(1);
expect(agentsList).toHaveBeenCalledTimes(1);
expect(conversationsList).toHaveBeenCalledTimes(1);
expect(resume.pendingApprovals).toHaveLength(0);
expect(resume.messageHistory.length).toBeGreaterThan(0);
});

View File

@@ -59,7 +59,6 @@ describe("approval recovery wiring", () => {
const segment = source.slice(start, end);
expect(segment).toContain("getClient()");
expect(segment).toContain("client.agents.messages.cancel");
expect(segment).toContain("client.conversations.cancel");
});
});

View File

@@ -2,12 +2,12 @@
* Handler-level tests for bootstrap_session_state using mock Letta clients.
*
* Verifies:
* 1. Correct routing (conversations vs agents path based on session conversationId)
* 1. Correct routing (all paths use conversations.messages.list)
* 2. Response payload shape (agent_id, conversation_id, model, tools, messages, etc.)
* 3. Pagination fields (next_before, has_more)
* 4. Timing fields presence
* 5. Error path — client throws → error envelope returned
* 6. Default conversation uses agents.messages.list with conversation_id: "default"
* 6. Default conversation passes agent ID to conversations.messages.list
* 7. Explicit conversation uses conversations.messages.list
*
* No network. No CLI subprocess. No process.stdout.
@@ -23,20 +23,13 @@ import { handleBootstrapSessionState } from "../../agent/bootstrapHandler";
// Mock factory
// ─────────────────────────────────────────────────────────────────────────────
function makeClient(
convMessages: unknown[] = [],
agentMessages: unknown[] = [],
): {
function makeClient(convMessages: unknown[] = []): {
client: BootstrapHandlerClient;
convListSpy: ReturnType<typeof mock>;
agentListSpy: ReturnType<typeof mock>;
} {
const convListSpy = mock(async () => ({
getPaginatedItems: () => convMessages,
}));
const agentListSpy = mock(async () => ({
items: agentMessages,
}));
const client: BootstrapHandlerClient = {
conversations: {
@@ -44,14 +37,9 @@ function makeClient(
list: convListSpy as unknown as BootstrapHandlerClient["conversations"]["messages"]["list"],
},
},
agents: {
messages: {
list: agentListSpy as unknown as BootstrapHandlerClient["agents"]["messages"]["list"],
},
},
};
return { client, convListSpy, agentListSpy };
return { client, convListSpy };
}
const BASE_CTX: BootstrapHandlerSessionContext = {
@@ -68,11 +56,10 @@ const BASE_CTX: BootstrapHandlerSessionContext = {
// ─────────────────────────────────────────────────────────────────────────────
describe("bootstrap_session_state routing", () => {
test("default conversation uses agents.messages.list", async () => {
const { client, agentListSpy, convListSpy } = makeClient(
[],
[{ id: "msg-1", type: "user_message" }],
);
test("default conversation passes agent ID to conversations.messages.list", async () => {
const { client, convListSpy } = makeClient([
{ id: "msg-1", type: "user_message" },
]);
await handleBootstrapSessionState({
bootstrapReq: { subtype: "bootstrap_session_state" },
@@ -81,19 +68,15 @@ describe("bootstrap_session_state routing", () => {
client,
});
expect(agentListSpy).toHaveBeenCalledTimes(1);
expect(convListSpy).toHaveBeenCalledTimes(0);
expect(convListSpy).toHaveBeenCalledTimes(1);
// Verify conversation_id: "default" param is passed
const callArgs = (agentListSpy.mock.calls[0] as unknown[])[1] as Record<
string,
unknown
>;
expect(callArgs.conversation_id).toBe("default");
// Verify agent ID is passed as the conversation_id
const callArgs = (convListSpy.mock.calls[0] as unknown[])[0];
expect(callArgs).toBe("agent-test-123");
});
test("named conversation uses conversations.messages.list", async () => {
const { client, convListSpy, agentListSpy } = makeClient([
const { client, convListSpy } = makeClient([
{ id: "msg-1", type: "user_message" },
]);
@@ -105,7 +88,6 @@ describe("bootstrap_session_state routing", () => {
});
expect(convListSpy).toHaveBeenCalledTimes(1);
expect(agentListSpy).toHaveBeenCalledTimes(0);
const callArgs = (convListSpy.mock.calls[0] as unknown[])[0];
expect(callArgs).toBe("conv-abc-123");
@@ -123,7 +105,7 @@ describe("bootstrap_session_state response shape", () => {
{ id: "msg-2", type: "user_message" },
{ id: "msg-1", type: "user_message" },
];
const { client } = makeClient([], messages);
const { client } = makeClient(messages);
const resp = await handleBootstrapSessionState({
bootstrapReq: { subtype: "bootstrap_session_state" },
@@ -210,7 +192,7 @@ describe("bootstrap_session_state pagination", () => {
id: `msg-${i}`,
type: "user_message",
}));
const { client } = makeClient([], messages);
const { client } = makeClient(messages);
const resp = await handleBootstrapSessionState({
bootstrapReq: { subtype: "bootstrap_session_state", limit: 50 },
@@ -228,7 +210,7 @@ describe("bootstrap_session_state pagination", () => {
const messages = Array.from({ length: limit }, (_, i) => ({
id: `msg-${i}`,
}));
const { client } = makeClient([], messages);
const { client } = makeClient(messages);
const resp = await handleBootstrapSessionState({
bootstrapReq: { subtype: "bootstrap_session_state", limit },
@@ -247,7 +229,7 @@ describe("bootstrap_session_state pagination", () => {
{ id: "msg-middle" },
{ id: "msg-oldest" },
];
const { client } = makeClient([], messages);
const { client } = makeClient(messages);
const resp = await handleBootstrapSessionState({
bootstrapReq: { subtype: "bootstrap_session_state" },
@@ -262,7 +244,7 @@ describe("bootstrap_session_state pagination", () => {
});
test("next_before is null when no messages", async () => {
const { client } = makeClient([], []);
const { client } = makeClient([]);
const resp = await handleBootstrapSessionState({
bootstrapReq: { subtype: "bootstrap_session_state" },
sessionContext: BASE_CTX,
@@ -289,13 +271,6 @@ describe("bootstrap_session_state error handling", () => {
},
},
},
agents: {
messages: {
list: async () => {
throw new Error("Network timeout");
},
},
},
};
const resp = await handleBootstrapSessionState({

View File

@@ -2,7 +2,7 @@
* Handler-level tests for list_messages using mock Letta clients.
*
* These tests call handleListMessages() directly with mock implementations
* of conversations.messages.list and agents.messages.list. They verify:
* of conversations.messages.list. They verify:
*
* 1. Which client method is called for each routing case (explicit conv,
* omitted+named session conv, omitted+default session conv)
@@ -20,20 +20,13 @@ import { handleListMessages } from "../../agent/listMessagesHandler";
// Mock factory
// ─────────────────────────────────────────────────────────────────────────────
function makeClient(
convMessages: unknown[] = [],
agentMessages: unknown[] = [],
): {
function makeClient(convMessages: unknown[] = []): {
client: ListMessagesHandlerClient;
convListSpy: ReturnType<typeof mock>;
agentListSpy: ReturnType<typeof mock>;
} {
const convListSpy = mock(async () => ({
getPaginatedItems: () => convMessages,
}));
const agentListSpy = mock(async () => ({
items: agentMessages,
}));
const client: ListMessagesHandlerClient = {
conversations: {
@@ -41,14 +34,9 @@ function makeClient(
list: convListSpy as unknown as ListMessagesHandlerClient["conversations"]["messages"]["list"],
},
},
agents: {
messages: {
list: agentListSpy as unknown as ListMessagesHandlerClient["agents"]["messages"]["list"],
},
},
};
return { client, convListSpy, agentListSpy };
return { client, convListSpy };
}
const BASE = {
@@ -62,7 +50,7 @@ const BASE = {
describe("handleListMessages — routing (which API is called)", () => {
test("explicit conversation_id → calls conversations.messages.list with that id", async () => {
const { client, convListSpy, agentListSpy } = makeClient([{ id: "m1" }]);
const { client, convListSpy } = makeClient([{ id: "m1" }]);
const resp = await handleListMessages({
...BASE,
@@ -73,7 +61,6 @@ describe("handleListMessages — routing (which API is called)", () => {
});
expect(convListSpy).toHaveBeenCalledTimes(1);
expect(agentListSpy).toHaveBeenCalledTimes(0);
expect(convListSpy.mock.calls[0]?.[0]).toBe("conv-explicit");
expect(resp.response.subtype).toBe("success");
});
@@ -93,7 +80,7 @@ describe("handleListMessages — routing (which API is called)", () => {
});
test("omitted conversation_id + named session conv → calls conversations.messages.list with session conv", async () => {
const { client, convListSpy, agentListSpy } = makeClient([
const { client, convListSpy } = makeClient([
{ id: "msg-A" },
{ id: "msg-B" },
]);
@@ -107,7 +94,6 @@ describe("handleListMessages — routing (which API is called)", () => {
});
expect(convListSpy).toHaveBeenCalledTimes(1);
expect(agentListSpy).toHaveBeenCalledTimes(0);
expect(convListSpy.mock.calls[0]?.[0]).toBe("conv-session-xyz");
expect(resp.response.subtype).toBe("success");
if (resp.response.subtype === "success") {
@@ -119,11 +105,8 @@ describe("handleListMessages — routing (which API is called)", () => {
}
});
test("omitted conversation_id + session on default → calls agents.messages.list", async () => {
const { client, convListSpy, agentListSpy } = makeClient(
[],
[{ id: "msg-default-1" }],
);
test("omitted conversation_id + session on default → calls conversations.messages.list with agent ID", async () => {
const { client, convListSpy } = makeClient([{ id: "msg-default-1" }]);
const resp = await handleListMessages({
...BASE,
@@ -133,14 +116,13 @@ describe("handleListMessages — routing (which API is called)", () => {
client,
});
expect(agentListSpy).toHaveBeenCalledTimes(1);
expect(convListSpy).toHaveBeenCalledTimes(0);
expect(agentListSpy.mock.calls[0]?.[0]).toBe("agent-def");
expect(convListSpy).toHaveBeenCalledTimes(1);
expect(convListSpy.mock.calls[0]?.[0]).toBe("agent-def");
expect(resp.response.subtype).toBe("success");
});
test("explicit agent_id + session default → agents path uses request agent_id", async () => {
const { client, agentListSpy } = makeClient([], []);
test("explicit agent_id + session default → conversations path uses request agent_id", async () => {
const { client, convListSpy } = makeClient([]);
await handleListMessages({
...BASE,
@@ -150,7 +132,7 @@ describe("handleListMessages — routing (which API is called)", () => {
client,
});
expect(agentListSpy.mock.calls[0]?.[0]).toBe("agent-override");
expect(convListSpy.mock.calls[0]?.[0]).toBe("agent-override");
});
});
@@ -184,7 +166,7 @@ describe("handleListMessages — API call arguments", () => {
});
test("defaults to limit=50 and order=desc when not specified", async () => {
const { client, agentListSpy } = makeClient([], []);
const { client, convListSpy } = makeClient([]);
await handleListMessages({
...BASE,
@@ -194,14 +176,14 @@ describe("handleListMessages — API call arguments", () => {
client,
});
const opts = agentListSpy.mock.calls[0]?.[1] as {
// Default conversation resolves to agent ID
expect(convListSpy.mock.calls[0]?.[0]).toBe("agent-1");
const opts = convListSpy.mock.calls[0]?.[1] as {
limit: number;
order: string;
conversation_id?: string;
};
expect(opts.limit).toBe(50);
expect(opts.order).toBe("desc");
expect(opts.conversation_id).toBe("default");
});
test("forwards before cursor to conversations path", async () => {
@@ -223,8 +205,8 @@ describe("handleListMessages — API call arguments", () => {
expect(opts.before).toBe("msg-cursor");
});
test("forwards before cursor to agents path", async () => {
const { client, agentListSpy } = makeClient([], []);
test("forwards before cursor to default conversation path", async () => {
const { client, convListSpy } = makeClient([]);
await handleListMessages({
...BASE,
@@ -234,12 +216,9 @@ describe("handleListMessages — API call arguments", () => {
client,
});
const opts = agentListSpy.mock.calls[0]?.[1] as {
before?: string;
conversation_id?: string;
};
expect(convListSpy.mock.calls[0]?.[0]).toBe("agent-1");
const opts = convListSpy.mock.calls[0]?.[1] as { before?: string };
expect(opts.before).toBe("msg-cursor-agents");
expect(opts.conversation_id).toBe("default");
});
test("does not include before/after when absent", async () => {
@@ -341,18 +320,12 @@ describe("handleListMessages — error path", () => {
const convListSpy = mock(async () => {
throw new Error("404 conversation not found");
});
const agentListSpy = mock(async () => ({ items: [] }));
const client: ListMessagesHandlerClient = {
conversations: {
messages: {
list: convListSpy as unknown as ListMessagesHandlerClient["conversations"]["messages"]["list"],
},
},
agents: {
messages: {
list: agentListSpy as unknown as ListMessagesHandlerClient["agents"]["messages"]["list"],
},
},
};
const resp = await handleListMessages({
@@ -380,13 +353,6 @@ describe("handleListMessages — error path", () => {
list: convListSpy as unknown as ListMessagesHandlerClient["conversations"]["messages"]["list"],
},
},
agents: {
messages: {
list: mock(async () => ({
items: [],
})) as unknown as ListMessagesHandlerClient["agents"]["messages"]["list"],
},
},
};
const resp = await handleListMessages({
@@ -402,21 +368,14 @@ describe("handleListMessages — error path", () => {
}
});
test("agents path error → error envelope with correct session_id", async () => {
const agentListSpy = mock(async () => {
test("default conversation error → error envelope with correct session_id", async () => {
const convListSpy = mock(async () => {
throw new Error("agent unavailable");
});
const client: ListMessagesHandlerClient = {
conversations: {
messages: {
list: mock(async () => ({
getPaginatedItems: () => [],
})) as unknown as ListMessagesHandlerClient["conversations"]["messages"]["list"],
},
},
agents: {
messages: {
list: agentListSpy as unknown as ListMessagesHandlerClient["agents"]["messages"]["list"],
list: convListSpy as unknown as ListMessagesHandlerClient["conversations"]["messages"]["list"],
},
},
};

View File

@@ -192,18 +192,17 @@ describe("list_messages routing — resolveListMessagesRoute", () => {
/**
* Case C: no conversation_id in request, session is on the default conversation.
* Must use agents.messages.list (implicit default conv via agent route).
* Resolves to conversations API with agent ID as the conversation_id
* (server accepts agent-* IDs for agent-direct messaging).
*/
test("C — omitted conversation_id + session default → agents API with session agentId", () => {
test("C — omitted conversation_id + session default → conversations API with session agentId", () => {
const route = resolveListMessagesRoute(
{}, // no conversation_id
"default", // session is on default conversation
SESSION_AGENT,
);
expect(route.kind).toBe("agents");
if (route.kind === "agents") {
expect(route.agentId).toBe(SESSION_AGENT);
}
expect(route.kind).toBe("conversations");
expect(route.conversationId).toBe(SESSION_AGENT);
});
test("C — explicit agent_id in request + session default → uses request agentId", () => {
@@ -212,11 +211,9 @@ describe("list_messages routing — resolveListMessagesRoute", () => {
"default",
SESSION_AGENT,
);
expect(route.kind).toBe("agents");
if (route.kind === "agents") {
// Request's agent_id takes priority over session agent when on default conv
expect(route.agentId).toBe("agent-override-id");
}
expect(route.kind).toBe("conversations");
// Request's agent_id takes priority over session agent when on default conv
expect(route.conversationId).toBe("agent-override-id");
});
test("C — no conversation_id, no agent_id, session default → falls back to session agentId", () => {
@@ -225,15 +222,12 @@ describe("list_messages routing — resolveListMessagesRoute", () => {
"default",
"agent-session-fallback",
);
expect(route.kind).toBe("agents");
if (route.kind === "agents") {
expect(route.agentId).toBe("agent-session-fallback");
}
expect(route.kind).toBe("conversations");
expect(route.conversationId).toBe("agent-session-fallback");
});
/**
* Invariant: "default" is the only string that triggers the agents path.
* Any other string (even empty, or a UUID-like string) uses conversations.
* All paths use the conversations API.
*/
test("conversations path for any non-default conversation string", () => {
const convIds = [