fix: migrate default conversation API usage to SDK 1.7.11 pattern (#1256)
Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
4
bun.lock
4
bun.lock
@@ -5,7 +5,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "@letta-ai/letta-code",
|
"name": "@letta-ai/letta-code",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@letta-ai/letta-client": "^1.7.9",
|
"@letta-ai/letta-client": "^1.7.11",
|
||||||
"glob": "^13.0.0",
|
"glob": "^13.0.0",
|
||||||
"ink-link": "^5.0.0",
|
"ink-link": "^5.0.0",
|
||||||
"open": "^10.2.0",
|
"open": "^10.2.0",
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
|
|
||||||
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
|
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
|
||||||
|
|
||||||
"@letta-ai/letta-client": ["@letta-ai/letta-client@1.7.9", "", {}, "sha512-ZoUH71/c5t7/7H5DF52lduAKGCet/UoAe2PZTwKCt7CpENdyVlAsM1gV3q8xABmu4RZ1zmwiOjbQT8yWty6w3g=="],
|
"@letta-ai/letta-client": ["@letta-ai/letta-client@1.7.11", "", {}, "sha512-8tB+v/p7xb8/ato/MUhJx2MtTCIZToaTGGHOCFqPql20aN1aJmp2b67ro8cK8jbOygYvHtBYnsogHkY4hvMO1Q=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.7", "", { "dependencies": { "bun-types": "1.3.7" } }, "sha512-lmNuMda+Z9b7tmhA0tohwy8ZWFSnmQm1UDWXtH5r9F7wZCfkeO3Jx7wKQ1EOiKq43yHts7ky6r8SDJQWRNupkA=="],
|
"@types/bun": ["@types/bun@1.3.7", "", { "dependencies": { "bun-types": "1.3.7" } }, "sha512-lmNuMda+Z9b7tmhA0tohwy8ZWFSnmQm1UDWXtH5r9F7wZCfkeO3Jx7wKQ1EOiKq43yHts7ky6r8SDJQWRNupkA=="],
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@letta-ai/letta-client": "^1.7.9",
|
"@letta-ai/letta-client": "^1.7.11",
|
||||||
"glob": "^13.0.0",
|
"glob": "^13.0.0",
|
||||||
"ink-link": "^5.0.0",
|
"ink-link": "^5.0.0",
|
||||||
"open": "^10.2.0",
|
"open": "^10.2.0",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface BootstrapHandlerClient {
|
|||||||
opts: {
|
opts: {
|
||||||
limit: number;
|
limit: number;
|
||||||
order: "asc" | "desc";
|
order: "asc" | "desc";
|
||||||
|
agent_id?: string;
|
||||||
before?: string;
|
before?: string;
|
||||||
after?: string;
|
after?: string;
|
||||||
},
|
},
|
||||||
@@ -99,7 +100,7 @@ export async function handleBootstrapSessionState(
|
|||||||
const listStart = Date.now();
|
const listStart = Date.now();
|
||||||
const page = await client.conversations.messages.list(
|
const page = await client.conversations.messages.list(
|
||||||
route.conversationId,
|
route.conversationId,
|
||||||
{ limit, order },
|
{ limit, order, ...(route.agentId ? { agent_id: route.agentId } : {}) },
|
||||||
);
|
);
|
||||||
const items = page.getPaginatedItems();
|
const items = page.getPaginatedItems();
|
||||||
const listEnd = Date.now();
|
const listEnd = Date.now();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { APIError } from "@letta-ai/letta-client/core/error";
|
|||||||
import type { AgentState } from "@letta-ai/letta-client/resources/agents/agents";
|
import type { AgentState } from "@letta-ai/letta-client/resources/agents/agents";
|
||||||
import type { Message } from "@letta-ai/letta-client/resources/agents/messages";
|
import type { Message } from "@letta-ai/letta-client/resources/agents/messages";
|
||||||
import type { ApprovalRequest } from "../cli/helpers/stream";
|
import type { ApprovalRequest } from "../cli/helpers/stream";
|
||||||
import { debugLog, debugWarn } from "../utils/debug";
|
import { debugWarn } from "../utils/debug";
|
||||||
|
|
||||||
// Backfill should feel like "the last turn(s)", not "the last N raw messages".
|
// Backfill should feel like "the last turn(s)", not "the last N raw messages".
|
||||||
// Tool-heavy turns can generate many tool_call/tool_return messages that would
|
// Tool-heavy turns can generate many tool_call/tool_return messages that would
|
||||||
@@ -344,21 +344,10 @@ export async function getResumeData(
|
|||||||
|
|
||||||
// Use conversations API for explicit conversations,
|
// Use conversations API for explicit conversations,
|
||||||
// use agents API for "default" or no conversationId (agent's primary message history)
|
// use agents API for "default" or no conversationId (agent's primary message history)
|
||||||
const useConversationsApi =
|
const useConversationsApi = conversationId && conversationId !== "default";
|
||||||
conversationId &&
|
|
||||||
conversationId !== "default" &&
|
|
||||||
!conversationId.startsWith("agent-");
|
|
||||||
|
|
||||||
if (conversationId?.startsWith("agent-")) {
|
|
||||||
debugWarn(
|
|
||||||
"check-approval",
|
|
||||||
`getResumeData called with agent ID as conversationId: ${conversationId}\n${new Error().stack}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useConversationsApi) {
|
if (useConversationsApi) {
|
||||||
// Get conversation to access in_context_message_ids (source of truth)
|
// Get conversation to access in_context_message_ids (source of truth)
|
||||||
debugLog("conversations", `retrieve(${conversationId}) [getResumeData]`);
|
|
||||||
const conversation = await client.conversations.retrieve(conversationId);
|
const conversation = await client.conversations.retrieve(conversationId);
|
||||||
inContextMessageIds = conversation.in_context_message_ids;
|
inContextMessageIds = conversation.in_context_message_ids;
|
||||||
|
|
||||||
@@ -484,15 +473,16 @@ export async function getResumeData(
|
|||||||
}
|
}
|
||||||
const retrievedMessages = await client.messages.retrieve(lastInContextId);
|
const retrievedMessages = await client.messages.retrieve(lastInContextId);
|
||||||
|
|
||||||
// Fetch message history for backfill using the agent ID as conversation_id
|
// Fetch message history for backfill through the default conversation route.
|
||||||
// (the server accepts agent-* IDs for default conversation messages)
|
// For default conversation, pass agent_id as query parameter.
|
||||||
// Wrapped in try/catch so backfill failures don't crash the CLI (e.g., older servers
|
// Wrapped in try/catch so backfill failures don't crash the CLI (e.g., older servers
|
||||||
// may not support this pattern)
|
// may not support this pattern)
|
||||||
if (includeMessageHistory && isBackfillEnabled()) {
|
if (includeMessageHistory && isBackfillEnabled()) {
|
||||||
try {
|
try {
|
||||||
const messagesPage = await client.conversations.messages.list(
|
const messagesPage = await client.conversations.messages.list(
|
||||||
agent.id,
|
"default",
|
||||||
{
|
{
|
||||||
|
agent_id: agent.id,
|
||||||
limit: BACKFILL_PAGE_LIMIT,
|
limit: BACKFILL_PAGE_LIMIT,
|
||||||
order: "desc",
|
order: "desc",
|
||||||
},
|
},
|
||||||
@@ -501,7 +491,7 @@ export async function getResumeData(
|
|||||||
|
|
||||||
if (process.env.DEBUG) {
|
if (process.env.DEBUG) {
|
||||||
console.log(
|
console.log(
|
||||||
`[DEBUG] conversations.messages.list(${agent.id}) returned ${messages.length} messages`,
|
`[DEBUG] conversations.messages.list(default, agent_id=${agent.id}) returned ${messages.length} messages`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (backfillError) {
|
} catch (backfillError) {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface ListMessagesHandlerClient {
|
|||||||
opts: {
|
opts: {
|
||||||
limit: number;
|
limit: number;
|
||||||
order: "asc" | "desc";
|
order: "asc" | "desc";
|
||||||
|
agent_id?: string;
|
||||||
before?: string;
|
before?: string;
|
||||||
after?: string;
|
after?: string;
|
||||||
},
|
},
|
||||||
@@ -87,7 +88,12 @@ export async function handleListMessages(
|
|||||||
|
|
||||||
const page = await client.conversations.messages.list(
|
const page = await client.conversations.messages.list(
|
||||||
route.conversationId,
|
route.conversationId,
|
||||||
{ limit, order, ...cursorOpts },
|
{
|
||||||
|
limit,
|
||||||
|
order,
|
||||||
|
...(route.agentId ? { agent_id: route.agentId } : {}),
|
||||||
|
...cursorOpts,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const items = page.getPaginatedItems();
|
const items = page.getPaginatedItems();
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,8 @@
|
|||||||
* Extracted from headless.ts so it can be tested in isolation without
|
* Extracted from headless.ts so it can be tested in isolation without
|
||||||
* spinning up a real Letta client.
|
* spinning up a real Letta client.
|
||||||
*
|
*
|
||||||
* All paths now use the conversations endpoint. For the default conversation,
|
* All paths use the conversations endpoint. For the default conversation,
|
||||||
* the agent ID is passed as the conversation_id (the server accepts agent-*
|
* conversation_id stays "default" and agent_id is passed as query param.
|
||||||
* IDs for agent-direct messaging).
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ListMessagesControlRequest } from "../types/protocol";
|
import type { ListMessagesControlRequest } from "../types/protocol";
|
||||||
@@ -14,6 +13,7 @@ import type { ListMessagesControlRequest } from "../types/protocol";
|
|||||||
export type ListMessagesRoute = {
|
export type ListMessagesRoute = {
|
||||||
kind: "conversations";
|
kind: "conversations";
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
agentId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,8 +35,8 @@ export function resolveListMessagesRoute(
|
|||||||
return { kind: "conversations", conversationId: targetConvId };
|
return { kind: "conversations", conversationId: targetConvId };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default conversation: pass the agent ID to the conversations endpoint.
|
// Default conversation: keep conversation_id as "default" and
|
||||||
// The server accepts agent-* IDs for agent-direct messaging.
|
// pass the agent ID as a query parameter.
|
||||||
const agentId = listReq.agent_id ?? sessionAgentId;
|
const agentId = listReq.agent_id ?? sessionAgentId;
|
||||||
return { kind: "conversations", conversationId: agentId };
|
return { kind: "conversations", conversationId: "default", agentId };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,8 +49,7 @@ export function getStreamRequestContext(
|
|||||||
*
|
*
|
||||||
* For the "default" conversation (agent's primary message history without
|
* For the "default" conversation (agent's primary message history without
|
||||||
* an explicit conversation object), pass conversationId="default" and
|
* an explicit conversation object), pass conversationId="default" and
|
||||||
* provide agentId in opts. The server accepts agent IDs as the
|
* provide agentId in opts. The agent id is sent in the request body.
|
||||||
* conversation_id path parameter for agent-direct messaging.
|
|
||||||
*/
|
*/
|
||||||
export async function sendMessageStream(
|
export async function sendMessageStream(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
@@ -75,33 +74,34 @@ export async function sendMessageStream(
|
|||||||
await waitForToolsetReady();
|
await waitForToolsetReady();
|
||||||
const { clientTools, contextId } = captureToolExecutionContext();
|
const { clientTools, contextId } = captureToolExecutionContext();
|
||||||
|
|
||||||
// For "default" conversation, pass the agent ID to the conversations endpoint.
|
const isDefaultConversation = conversationId === "default";
|
||||||
// The server accepts agent-* IDs for agent-direct messaging.
|
if (isDefaultConversation && !opts.agentId) {
|
||||||
const resolvedConversationId =
|
|
||||||
conversationId === "default" ? opts.agentId : conversationId;
|
|
||||||
|
|
||||||
if (!resolvedConversationId) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"agentId is required in opts when using default conversation",
|
"agentId is required in opts when using default conversation",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolvedConversationId = conversationId;
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
messages,
|
||||||
|
streaming: true,
|
||||||
|
stream_tokens: opts.streamTokens ?? true,
|
||||||
|
background: opts.background ?? true,
|
||||||
|
client_tools: clientTools,
|
||||||
|
include_compaction_messages: true,
|
||||||
|
...(isDefaultConversation ? { agent_id: opts.agentId } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
if (process.env.DEBUG) {
|
if (process.env.DEBUG) {
|
||||||
console.log(
|
console.log(
|
||||||
`[DEBUG] sendMessageStream: conversationId=${conversationId}, resolved=${resolvedConversationId}`,
|
`[DEBUG] sendMessageStream: conversationId=${conversationId}, agentId=${opts.agentId ?? "(none)"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const stream = await client.conversations.messages.create(
|
const stream = await client.conversations.messages.create(
|
||||||
resolvedConversationId,
|
resolvedConversationId,
|
||||||
{
|
requestBody,
|
||||||
messages: messages,
|
|
||||||
streaming: true,
|
|
||||||
stream_tokens: opts.streamTokens ?? true,
|
|
||||||
background: opts.background ?? true,
|
|
||||||
client_tools: clientTools,
|
|
||||||
include_compaction_messages: true,
|
|
||||||
},
|
|
||||||
requestOptions,
|
requestOptions,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5819,11 +5819,12 @@ export default function App({
|
|||||||
// causing CONFLICT on the next user message.
|
// causing CONFLICT on the next user message.
|
||||||
getClient()
|
getClient()
|
||||||
.then((client) => {
|
.then((client) => {
|
||||||
const cancelId =
|
if (conversationIdRef.current === "default") {
|
||||||
conversationIdRef.current === "default"
|
return client.conversations.cancel("default", {
|
||||||
? agentIdRef.current
|
agent_id: agentIdRef.current,
|
||||||
: conversationIdRef.current;
|
});
|
||||||
return client.conversations.cancel(cancelId);
|
}
|
||||||
|
return client.conversations.cancel(conversationIdRef.current);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Silently ignore - cancellation already happened client-side
|
// Silently ignore - cancellation already happened client-side
|
||||||
@@ -5938,11 +5939,12 @@ export default function App({
|
|||||||
// Don't wait for it or show errors since user already got feedback
|
// Don't wait for it or show errors since user already got feedback
|
||||||
getClient()
|
getClient()
|
||||||
.then((client) => {
|
.then((client) => {
|
||||||
const cancelId =
|
if (conversationIdRef.current === "default") {
|
||||||
conversationIdRef.current === "default"
|
return client.conversations.cancel("default", {
|
||||||
? agentIdRef.current
|
agent_id: agentIdRef.current,
|
||||||
: conversationIdRef.current;
|
});
|
||||||
return client.conversations.cancel(cancelId);
|
}
|
||||||
|
return client.conversations.cancel(conversationIdRef.current);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Silently ignore - cancellation already happened client-side
|
// Silently ignore - cancellation already happened client-side
|
||||||
@@ -5962,11 +5964,13 @@ export default function App({
|
|||||||
setInterruptRequested(true);
|
setInterruptRequested(true);
|
||||||
try {
|
try {
|
||||||
const client = await getClient();
|
const client = await getClient();
|
||||||
const cancelId =
|
if (conversationIdRef.current === "default") {
|
||||||
conversationIdRef.current === "default"
|
await client.conversations.cancel("default", {
|
||||||
? agentIdRef.current
|
agent_id: agentIdRef.current,
|
||||||
: conversationIdRef.current;
|
});
|
||||||
await client.conversations.cancel(cancelId);
|
} else {
|
||||||
|
await client.conversations.cancel(conversationIdRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
if (abortControllerRef.current) {
|
if (abortControllerRef.current) {
|
||||||
abortControllerRef.current.abort();
|
abortControllerRef.current.abort();
|
||||||
@@ -6174,9 +6178,7 @@ export default function App({
|
|||||||
// Build success message
|
// Build success message
|
||||||
const agentLabel = agent.name || targetAgentId;
|
const agentLabel = agent.name || targetAgentId;
|
||||||
const isSpecificConv =
|
const isSpecificConv =
|
||||||
opts?.conversationId &&
|
opts?.conversationId && opts.conversationId !== "default";
|
||||||
opts.conversationId !== "default" &&
|
|
||||||
!opts?.conversationId.startsWith("agent-");
|
|
||||||
const successOutput = isSpecificConv
|
const successOutput = isSpecificConv
|
||||||
? [
|
? [
|
||||||
`Switched to **${agentLabel}**`,
|
`Switched to **${agentLabel}**`,
|
||||||
@@ -8047,13 +8049,17 @@ export default function App({
|
|||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const compactId =
|
const compactConversationId = conversationIdRef.current;
|
||||||
conversationIdRef.current === "default"
|
const compactBody =
|
||||||
? agentId
|
compactConversationId === "default"
|
||||||
: conversationIdRef.current;
|
? {
|
||||||
|
agent_id: agentId,
|
||||||
|
...(compactParams ?? {}),
|
||||||
|
}
|
||||||
|
: compactParams;
|
||||||
const result = await client.conversations.messages.compact(
|
const result = await client.conversations.messages.compact(
|
||||||
compactId,
|
compactConversationId,
|
||||||
compactParams,
|
compactBody,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Format success message with before/after counts and summary
|
// Format success message with before/after counts and summary
|
||||||
|
|||||||
@@ -177,9 +177,7 @@ export const AgentInfoBar = memo(function AgentInfoBar({
|
|||||||
{/* Phantom alien row + Conversation ID */}
|
{/* Phantom alien row + Conversation ID */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text>{alienLines[3]}</Text>
|
<Text>{alienLines[3]}</Text>
|
||||||
{conversationId &&
|
{conversationId && conversationId !== "default" ? (
|
||||||
conversationId !== "default" &&
|
|
||||||
!conversationId.startsWith("agent-") ? (
|
|
||||||
<Box width={rightWidth} flexShrink={1}>
|
<Box width={rightWidth} flexShrink={1}>
|
||||||
<Text dimColor wrap="truncate-end">
|
<Text dimColor wrap="truncate-end">
|
||||||
{truncateText(conversationId, rightWidth)}
|
{truncateText(conversationId, rightWidth)}
|
||||||
|
|||||||
@@ -243,8 +243,9 @@ export function ConversationSelector({
|
|||||||
if (!afterCursor) {
|
if (!afterCursor) {
|
||||||
try {
|
try {
|
||||||
const defaultMessages = await client.conversations.messages.list(
|
const defaultMessages = await client.conversations.messages.list(
|
||||||
agentId,
|
"default",
|
||||||
{
|
{
|
||||||
|
agent_id: agentId,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
order: "desc",
|
order: "desc",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export async function discoverFallbackRunIdForResume(
|
|||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
if (ctx.conversationId === "default") {
|
if (ctx.conversationId === "default") {
|
||||||
// Default conversation routes through resolvedConversationId (typically agent ID).
|
// Default conversation lookup by conversation id first.
|
||||||
lookupQueries.push({ conversation_id: ctx.resolvedConversationId });
|
lookupQueries.push({ conversation_id: ctx.resolvedConversationId });
|
||||||
} else {
|
} else {
|
||||||
// Named conversation: first use the explicit conversation id.
|
// Named conversation: first use the explicit conversation id.
|
||||||
|
|||||||
@@ -159,7 +159,8 @@ export async function runMessagesSubcommand(argv: string[]): Promise<number> {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await client.conversations.messages.list(agentId, {
|
const response = await client.conversations.messages.list("default", {
|
||||||
|
agent_id: agentId,
|
||||||
limit: parseLimit(parsed.values.limit, 20),
|
limit: parseLimit(parsed.values.limit, 20),
|
||||||
after: parsed.values.after,
|
after: parsed.values.after,
|
||||||
before: parsed.values.before,
|
before: parsed.values.before,
|
||||||
|
|||||||
@@ -527,10 +527,7 @@ export async function handleHeadlessCommand(
|
|||||||
// Validate shared mutual-exclusion rules for startup flags.
|
// Validate shared mutual-exclusion rules for startup flags.
|
||||||
try {
|
try {
|
||||||
validateFlagConflicts({
|
validateFlagConflicts({
|
||||||
guard:
|
guard: specifiedConversationId && specifiedConversationId !== "default",
|
||||||
specifiedConversationId &&
|
|
||||||
specifiedConversationId !== "default" &&
|
|
||||||
!specifiedConversationId.startsWith("agent-"),
|
|
||||||
checks: [
|
checks: [
|
||||||
{
|
{
|
||||||
when: specifiedAgentId,
|
when: specifiedAgentId,
|
||||||
@@ -734,11 +731,7 @@ export async function handleHeadlessCommand(
|
|||||||
// Priority 0: --conversation derives agent from conversation ID.
|
// Priority 0: --conversation derives agent from conversation ID.
|
||||||
// "default" is a virtual agent-scoped conversation (not a retrievable conv-*).
|
// "default" is a virtual agent-scoped conversation (not a retrievable conv-*).
|
||||||
// It requires --agent and should not hit conversations.retrieve().
|
// It requires --agent and should not hit conversations.retrieve().
|
||||||
if (
|
if (specifiedConversationId && specifiedConversationId !== "default") {
|
||||||
specifiedConversationId &&
|
|
||||||
specifiedConversationId !== "default" &&
|
|
||||||
!specifiedConversationId.startsWith("agent-")
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
debugLog(
|
debugLog(
|
||||||
"conversations",
|
"conversations",
|
||||||
|
|||||||
@@ -630,10 +630,7 @@ async function main(): Promise<void> {
|
|||||||
// Validate shared mutual-exclusion rules for startup flags.
|
// Validate shared mutual-exclusion rules for startup flags.
|
||||||
try {
|
try {
|
||||||
validateFlagConflicts({
|
validateFlagConflicts({
|
||||||
guard:
|
guard: specifiedConversationId && specifiedConversationId !== "default",
|
||||||
specifiedConversationId &&
|
|
||||||
specifiedConversationId !== "default" &&
|
|
||||||
!specifiedConversationId.startsWith("agent-"),
|
|
||||||
checks: [
|
checks: [
|
||||||
{
|
{
|
||||||
when: specifiedAgentId,
|
when: specifiedAgentId,
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ describe("discoverFallbackRunIdForResume", () => {
|
|||||||
makeRunsListClient(runsList),
|
makeRunsListClient(runsList),
|
||||||
{
|
{
|
||||||
conversationId: "default",
|
conversationId: "default",
|
||||||
resolvedConversationId: "agent-test",
|
resolvedConversationId: "default",
|
||||||
agentId: "agent-test",
|
agentId: "agent-test",
|
||||||
requestStartedAtMs: Date.parse("2026-02-27T11:00:00.000Z"),
|
requestStartedAtMs: Date.parse("2026-02-27T11:00:00.000Z"),
|
||||||
},
|
},
|
||||||
@@ -93,7 +93,7 @@ describe("discoverFallbackRunIdForResume", () => {
|
|||||||
|
|
||||||
expect(candidate).toBe("run-agent-fallback");
|
expect(candidate).toBe("run-agent-fallback");
|
||||||
expect(calls).toEqual([
|
expect(calls).toEqual([
|
||||||
{ conversation_id: "agent-test", agent_id: undefined },
|
{ conversation_id: "default", agent_id: undefined },
|
||||||
{ conversation_id: undefined, agent_id: "agent-test" },
|
{ conversation_id: undefined, agent_id: "agent-test" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* 3. Pagination fields (next_before, has_more)
|
* 3. Pagination fields (next_before, has_more)
|
||||||
* 4. Timing fields presence
|
* 4. Timing fields presence
|
||||||
* 5. Error path — client throws → error envelope returned
|
* 5. Error path — client throws → error envelope returned
|
||||||
* 6. Default conversation passes agent ID to conversations.messages.list
|
* 6. Default conversation passes conversation_id="default" with agent_id query
|
||||||
* 7. Explicit conversation uses conversations.messages.list
|
* 7. Explicit conversation uses conversations.messages.list
|
||||||
*
|
*
|
||||||
* No network. No CLI subprocess. No process.stdout.
|
* No network. No CLI subprocess. No process.stdout.
|
||||||
@@ -56,7 +56,7 @@ const BASE_CTX: BootstrapHandlerSessionContext = {
|
|||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe("bootstrap_session_state routing", () => {
|
describe("bootstrap_session_state routing", () => {
|
||||||
test("default conversation passes agent ID to conversations.messages.list", async () => {
|
test("default conversation passes default + agent_id to conversations.messages.list", async () => {
|
||||||
const { client, convListSpy } = makeClient([
|
const { client, convListSpy } = makeClient([
|
||||||
{ id: "msg-1", type: "user_message" },
|
{ id: "msg-1", type: "user_message" },
|
||||||
]);
|
]);
|
||||||
@@ -70,9 +70,11 @@ describe("bootstrap_session_state routing", () => {
|
|||||||
|
|
||||||
expect(convListSpy).toHaveBeenCalledTimes(1);
|
expect(convListSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
// Verify agent ID is passed as the conversation_id
|
const callArgs = convListSpy.mock.calls[0] as unknown[];
|
||||||
const callArgs = (convListSpy.mock.calls[0] as unknown[])[0];
|
expect(callArgs[0]).toBe("default");
|
||||||
expect(callArgs).toBe("agent-test-123");
|
expect((callArgs[1] as { agent_id?: string }).agent_id).toBe(
|
||||||
|
"agent-test-123",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("named conversation uses conversations.messages.list", async () => {
|
test("named conversation uses conversations.messages.list", async () => {
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ describe("handleListMessages — routing (which API is called)", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("omitted conversation_id + session on default → calls conversations.messages.list with agent ID", async () => {
|
test("omitted conversation_id + session on default → calls conversations.messages.list with default + agent_id", async () => {
|
||||||
const { client, convListSpy } = makeClient([{ id: "msg-default-1" }]);
|
const { client, convListSpy } = makeClient([{ id: "msg-default-1" }]);
|
||||||
|
|
||||||
const resp = await handleListMessages({
|
const resp = await handleListMessages({
|
||||||
@@ -117,11 +117,13 @@ describe("handleListMessages — routing (which API is called)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(convListSpy).toHaveBeenCalledTimes(1);
|
expect(convListSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(convListSpy.mock.calls[0]?.[0]).toBe("agent-def");
|
expect(convListSpy.mock.calls[0]?.[0]).toBe("default");
|
||||||
|
const opts = convListSpy.mock.calls[0]?.[1] as { agent_id?: string };
|
||||||
|
expect(opts.agent_id).toBe("agent-def");
|
||||||
expect(resp.response.subtype).toBe("success");
|
expect(resp.response.subtype).toBe("success");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("explicit agent_id + session default → conversations path uses request agent_id", async () => {
|
test("explicit agent_id + session default → conversations path uses request agent_id query", async () => {
|
||||||
const { client, convListSpy } = makeClient([]);
|
const { client, convListSpy } = makeClient([]);
|
||||||
|
|
||||||
await handleListMessages({
|
await handleListMessages({
|
||||||
@@ -132,7 +134,9 @@ describe("handleListMessages — routing (which API is called)", () => {
|
|||||||
client,
|
client,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(convListSpy.mock.calls[0]?.[0]).toBe("agent-override");
|
expect(convListSpy.mock.calls[0]?.[0]).toBe("default");
|
||||||
|
const opts = convListSpy.mock.calls[0]?.[1] as { agent_id?: string };
|
||||||
|
expect(opts.agent_id).toBe("agent-override");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -176,12 +180,13 @@ describe("handleListMessages — API call arguments", () => {
|
|||||||
client,
|
client,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Default conversation resolves to agent ID
|
expect(convListSpy.mock.calls[0]?.[0]).toBe("default");
|
||||||
expect(convListSpy.mock.calls[0]?.[0]).toBe("agent-1");
|
|
||||||
const opts = convListSpy.mock.calls[0]?.[1] as {
|
const opts = convListSpy.mock.calls[0]?.[1] as {
|
||||||
|
agent_id?: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
order: string;
|
order: string;
|
||||||
};
|
};
|
||||||
|
expect(opts.agent_id).toBe("agent-1");
|
||||||
expect(opts.limit).toBe(50);
|
expect(opts.limit).toBe(50);
|
||||||
expect(opts.order).toBe("desc");
|
expect(opts.order).toBe("desc");
|
||||||
});
|
});
|
||||||
@@ -216,8 +221,12 @@ describe("handleListMessages — API call arguments", () => {
|
|||||||
client,
|
client,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(convListSpy.mock.calls[0]?.[0]).toBe("agent-1");
|
expect(convListSpy.mock.calls[0]?.[0]).toBe("default");
|
||||||
const opts = convListSpy.mock.calls[0]?.[1] as { before?: string };
|
const opts = convListSpy.mock.calls[0]?.[1] as {
|
||||||
|
agent_id?: string;
|
||||||
|
before?: string;
|
||||||
|
};
|
||||||
|
expect(opts.agent_id).toBe("agent-1");
|
||||||
expect(opts.before).toBe("msg-cursor-agents");
|
expect(opts.before).toBe("msg-cursor-agents");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -192,28 +192,29 @@ describe("list_messages routing — resolveListMessagesRoute", () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Case C: no conversation_id in request, session is on the default conversation.
|
* Case C: no conversation_id in request, session is on the default conversation.
|
||||||
* Resolves to conversations API with agent ID as the conversation_id
|
* Keeps conversation_id="default" and passes agent_id separately.
|
||||||
* (server accepts agent-* IDs for agent-direct messaging).
|
|
||||||
*/
|
*/
|
||||||
test("C — omitted conversation_id + session default → conversations API with session agentId", () => {
|
test("C — omitted conversation_id + session default → conversations API with default + session agentId", () => {
|
||||||
const route = resolveListMessagesRoute(
|
const route = resolveListMessagesRoute(
|
||||||
{}, // no conversation_id
|
{}, // no conversation_id
|
||||||
"default", // session is on default conversation
|
"default", // session is on default conversation
|
||||||
SESSION_AGENT,
|
SESSION_AGENT,
|
||||||
);
|
);
|
||||||
expect(route.kind).toBe("conversations");
|
expect(route.kind).toBe("conversations");
|
||||||
expect(route.conversationId).toBe(SESSION_AGENT);
|
expect(route.conversationId).toBe("default");
|
||||||
|
expect(route.agentId).toBe(SESSION_AGENT);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("C — explicit agent_id in request + session default → uses request agentId", () => {
|
test("C — explicit agent_id in request + session default → uses request agentId query", () => {
|
||||||
const route = resolveListMessagesRoute(
|
const route = resolveListMessagesRoute(
|
||||||
{ agent_id: "agent-override-id" },
|
{ agent_id: "agent-override-id" },
|
||||||
"default",
|
"default",
|
||||||
SESSION_AGENT,
|
SESSION_AGENT,
|
||||||
);
|
);
|
||||||
expect(route.kind).toBe("conversations");
|
expect(route.kind).toBe("conversations");
|
||||||
|
expect(route.conversationId).toBe("default");
|
||||||
// Request's agent_id takes priority over session agent when on default conv
|
// Request's agent_id takes priority over session agent when on default conv
|
||||||
expect(route.conversationId).toBe("agent-override-id");
|
expect(route.agentId).toBe("agent-override-id");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("C — no conversation_id, no agent_id, session default → falls back to session agentId", () => {
|
test("C — no conversation_id, no agent_id, session default → falls back to session agentId", () => {
|
||||||
@@ -223,7 +224,8 @@ describe("list_messages routing — resolveListMessagesRoute", () => {
|
|||||||
"agent-session-fallback",
|
"agent-session-fallback",
|
||||||
);
|
);
|
||||||
expect(route.kind).toBe("conversations");
|
expect(route.kind).toBe("conversations");
|
||||||
expect(route.conversationId).toBe("agent-session-fallback");
|
expect(route.conversationId).toBe("default");
|
||||||
|
expect(route.agentId).toBe("agent-session-fallback");
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user