From 0d5dab198ad926edf4d3111dc08de19598faf598 Mon Sep 17 00:00:00 2001 From: cthomas Date: Fri, 27 Feb 2026 15:37:15 -0800 Subject: [PATCH] feat: use conversations endpoint for default conversation (#1206) --- src/agent/bootstrapHandler.ts | 39 ++------- src/agent/check-approval.ts | 22 ++--- src/agent/listMessagesHandler.ts | 40 ++------- src/agent/listMessagesRouting.ts | 21 ++--- src/agent/message.ts | 64 ++++++-------- src/cli/App.tsx | 45 +++++----- src/cli/components/ConversationSelector.tsx | 14 +-- src/cli/subcommands/messages.ts | 4 +- src/tests/agent/getResumeData.test.ts | 11 ++- .../cli/approval-recovery-wiring.test.ts | 1 - src/tests/headless/bootstrap-handler.test.ts | 61 ++++--------- .../headless/list-messages-handler.test.ts | 87 +++++-------------- .../headless/list-messages-protocol.test.ts | 28 +++--- 13 files changed, 148 insertions(+), 289 deletions(-) diff --git a/src/agent/bootstrapHandler.ts b/src/agent/bootstrapHandler.ts index 660c4fb..ccaf487 100644 --- a/src/agent/bootstrapHandler.ts +++ b/src/agent/bootstrapHandler.ts @@ -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; }; }; - agents: { - messages: { - list( - agentId: string, - opts: { - limit: number; - order: "asc" | "desc"; - before?: string; - after?: string; - conversation_id?: "default"; - }, - ): Promise; - }; - }; } 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; diff --git a/src/agent/check-approval.ts b/src/agent/check-approval.ts index 6661e6d..c0cd15c 100644 --- a/src/agent/check-approval.ts +++ b/src/agent/check-approval.ts @@ -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) { diff --git a/src/agent/listMessagesHandler.ts b/src/agent/listMessagesHandler.ts index aea10fb..84edb1e 100644 --- a/src/agent/listMessagesHandler.ts +++ b/src/agent/listMessagesHandler.ts @@ -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; }; }; - agents: { - messages: { - list( - agentId: string, - opts: { - limit: number; - order: "asc" | "desc"; - before?: string; - after?: string; - conversation_id?: "default"; - }, - ): Promise; - }; - }; } 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 = diff --git a/src/agent/listMessagesRouting.ts b/src/agent/listMessagesRouting.ts index 124e7cd..863c418 100644 --- a/src/agent/listMessagesRouting.ts +++ b/src/agent/listMessagesRouting.ts @@ -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 }; } diff --git a/src/agent/message.ts b/src/agent/message.ts index f0b92cc..347c23d 100644 --- a/src/agent/message.ts +++ b/src/agent/message.ts @@ -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; + // 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); diff --git a/src/cli/App.tsx b/src/cli/App.tsx index ea3eb71..3a2f093 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -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 = [ diff --git a/src/cli/components/ConversationSelector.tsx b/src/cli/components/ConversationSelector.tsx index 35167de..85a5a5a 100644 --- a/src/cli/components/ConversationSelector.tsx +++ b/src/cli/components/ConversationSelector.tsx @@ -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(), diff --git a/src/cli/subcommands/messages.ts b/src/cli/subcommands/messages.ts index 748fc81..f2bed2d 100644 --- a/src/cli/subcommands/messages.ts +++ b/src/cli/subcommands/messages.ts @@ -159,14 +159,14 @@ export async function runMessagesSubcommand(argv: string[]): Promise { 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"]; diff --git a/src/tests/agent/getResumeData.test.ts b/src/tests/agent/getResumeData.test.ts index 03e3053..b2aa47b 100644 --- a/src/tests/agent/getResumeData.test.ts +++ b/src/tests/agent/getResumeData.test.ts @@ -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); }); diff --git a/src/tests/cli/approval-recovery-wiring.test.ts b/src/tests/cli/approval-recovery-wiring.test.ts index ea534a7..a0543f3 100644 --- a/src/tests/cli/approval-recovery-wiring.test.ts +++ b/src/tests/cli/approval-recovery-wiring.test.ts @@ -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"); }); }); diff --git a/src/tests/headless/bootstrap-handler.test.ts b/src/tests/headless/bootstrap-handler.test.ts index 7e30f95..d0a7cb2 100644 --- a/src/tests/headless/bootstrap-handler.test.ts +++ b/src/tests/headless/bootstrap-handler.test.ts @@ -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; - agentListSpy: ReturnType; } { 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({ diff --git a/src/tests/headless/list-messages-handler.test.ts b/src/tests/headless/list-messages-handler.test.ts index d24872f..c7153b1 100644 --- a/src/tests/headless/list-messages-handler.test.ts +++ b/src/tests/headless/list-messages-handler.test.ts @@ -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; - agentListSpy: ReturnType; } { 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"], }, }, }; diff --git a/src/tests/headless/list-messages-protocol.test.ts b/src/tests/headless/list-messages-protocol.test.ts index 36c9e53..bde1623 100644 --- a/src/tests/headless/list-messages-protocol.test.ts +++ b/src/tests/headless/list-messages-protocol.test.ts @@ -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 = [