Merge pull request #96 from langchain-ai/brace/fix-interrupt-no-msgs

fix: Rendering interrupts when there are no ai/tool messages
This commit is contained in:
Brace Sproul
2025-04-08 12:27:51 -07:00
committed by GitHub
2 changed files with 35 additions and 15 deletions

View File

@@ -200,6 +200,9 @@ export function Thread() {
}; };
const chatStarted = !!threadId || !!messages.length; const chatStarted = !!threadId || !!messages.length;
const hasNoAIOrToolMessages = !messages.find(
(m) => m.type === "ai" || m.type === "tool",
);
return ( return (
<div className="flex w-full h-screen overflow-hidden"> <div className="flex w-full h-screen overflow-hidden">
@@ -350,6 +353,16 @@ export function Thread() {
/> />
), ),
)} )}
{/* Special rendering case where there are no AI/tool messages, but there is an interrupt.
We need to render it outside of the messages list, since there are no messages to render */}
{hasNoAIOrToolMessages && !!stream.interrupt && (
<AssistantMessage
key="interrupt-msg"
message={undefined}
isLoading={isLoading}
handleRegenerate={handleRegenerate}
/>
)}
{isLoading && !firstTokenReceived && ( {isLoading && !firstTokenReceived && (
<AssistantMessageLoading /> <AssistantMessageLoading />
)} )}

View File

@@ -70,11 +70,12 @@ export function AssistantMessage({
isLoading, isLoading,
handleRegenerate, handleRegenerate,
}: { }: {
message: Message; message: Message | undefined;
isLoading: boolean; isLoading: boolean;
handleRegenerate: (parentCheckpoint: Checkpoint | null | undefined) => void; handleRegenerate: (parentCheckpoint: Checkpoint | null | undefined) => void;
}) { }) {
const contentString = getContentString(message.content); const content = message?.content ?? [];
const contentString = getContentString(content);
const [hideToolCalls] = useQueryState( const [hideToolCalls] = useQueryState(
"hideToolCalls", "hideToolCalls",
parseAsBoolean.withDefault(false), parseAsBoolean.withDefault(false),
@@ -82,15 +83,20 @@ export function AssistantMessage({
const thread = useStreamContext(); const thread = useStreamContext();
const isLastMessage = const isLastMessage =
thread.messages[thread.messages.length - 1].id === message.id; thread.messages[thread.messages.length - 1].id === message?.id;
const meta = thread.getMessagesMetadata(message); const hasNoAIOrToolMessages = !thread.messages.find(
const interrupt = thread.interrupt; (m) => m.type === "ai" || m.type === "tool",
);
const meta = message ? thread.getMessagesMetadata(message) : undefined;
const threadInterrupt = thread.interrupt;
const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint; const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;
const anthropicStreamedToolCalls = Array.isArray(message.content) const anthropicStreamedToolCalls = Array.isArray(content)
? parseAnthropicStreamedToolCalls(message.content) ? parseAnthropicStreamedToolCalls(content)
: undefined; : undefined;
const hasToolCalls = const hasToolCalls =
message &&
"tool_calls" in message && "tool_calls" in message &&
message.tool_calls && message.tool_calls &&
message.tool_calls.length > 0; message.tool_calls.length > 0;
@@ -100,7 +106,7 @@ export function AssistantMessage({
(tc) => tc.args && Object.keys(tc.args).length > 0, (tc) => tc.args && Object.keys(tc.args).length > 0,
); );
const hasAnthropicToolCalls = !!anthropicStreamedToolCalls?.length; const hasAnthropicToolCalls = !!anthropicStreamedToolCalls?.length;
const isToolResult = message.type === "tool"; const isToolResult = message?.type === "tool";
if (isToolResult && hideToolCalls) { if (isToolResult && hideToolCalls) {
return null; return null;
@@ -130,14 +136,15 @@ export function AssistantMessage({
</> </>
)} )}
<CustomComponent message={message} thread={thread} /> {message && <CustomComponent message={message} thread={thread} />}
{isAgentInboxInterruptSchema(interrupt?.value) && isLastMessage && ( {isAgentInboxInterruptSchema(threadInterrupt?.value) &&
<ThreadView interrupt={interrupt.value} /> (isLastMessage || hasNoAIOrToolMessages) && (
)} <ThreadView interrupt={threadInterrupt.value} />
{interrupt?.value && )}
!isAgentInboxInterruptSchema(interrupt.value) && {threadInterrupt?.value &&
!isAgentInboxInterruptSchema(threadInterrupt.value) &&
isLastMessage ? ( isLastMessage ? (
<GenericInterruptView interrupt={interrupt.value} /> <GenericInterruptView interrupt={threadInterrupt.value} />
) : null} ) : null}
<div <div
className={cn( className={cn(