feat(ai-chat): add askUser tool for HITL testing, verify TRI-8556 fix

This commit is contained in:
Eric Allam
2026-04-17 09:59:18 +01:00
parent ecc8ee7cc9
commit a7078eb0cb
3 changed files with 69 additions and 10 deletions
+38 -2
View File
@@ -1,7 +1,10 @@
"use client";
import { useChat } from "@ai-sdk/react";
import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
import {
lastAssistantMessageIsCompleteWithApprovalResponses,
lastAssistantMessageIsCompleteWithToolCalls,
} from "ai";
import type { ChatUiMessage } from "@/lib/chat-tools";
import type { TriggerChatTransport } from "@trigger.dev/sdk/chat";
import type { CompactionChunkData } from "@trigger.dev/sdk/ai";
@@ -14,10 +17,12 @@ function ToolInvocation({
part,
onApprove,
onDeny,
onToolOutput,
}: {
part: any;
onApprove?: (approvalId: string) => void;
onDeny?: (approvalId: string) => void;
onToolOutput?: (toolCallId: string, output: unknown) => void;
}) {
const [expanded, setExpanded] = useState(false);
const toolName = part.type.startsWith("tool-") ? part.type.slice(5) : "tool";
@@ -72,6 +77,31 @@ function ToolInvocation({
</div>
)}
{/* askUser tool: show question + option buttons when input-available */}
{toolName === "askUser" && state === "input-available" && args?.question && (
<div className="border-t border-gray-200 px-3 py-2 space-y-2">
<div className="font-medium text-gray-700">{args.question}</div>
<div className="flex flex-wrap gap-2">
{(args.options ?? []).map((opt: any) => (
<button
key={opt.id}
type="button"
onClick={() =>
onToolOutput?.(part.toolCallId, {
skipped: false,
answers: [{ questionId: args.question, optionId: opt.id, text: opt.label }],
})
}
className="rounded border border-blue-300 bg-blue-50 px-3 py-1.5 text-xs text-blue-700 hover:bg-blue-100"
title={opt.description}
>
{opt.label}
</button>
))}
</div>
</div>
)}
{expanded && (
<div className="border-t border-gray-200 px-3 py-2 space-y-2">
{args && Object.keys(args).length > 0 && (
@@ -310,6 +340,7 @@ export function Chat({
sendMessage,
stop: aiStop,
addToolApprovalResponse,
addToolOutput,
status,
error,
} = useChat({
@@ -317,7 +348,9 @@ export function Chat({
messages: initialMessages,
transport,
resume: resumeProp,
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
sendAutomaticallyWhen: (opts) =>
lastAssistantMessageIsCompleteWithApprovalResponses(opts) ||
lastAssistantMessageIsCompleteWithToolCalls(opts),
});
// Use transport.stopGeneration for reliable stop after reconnect.
@@ -616,6 +649,9 @@ export function Chat({
part={part}
onApprove={handleApprove}
onDeny={handleDeny}
onToolOutput={(toolCallId, output) =>
addToolOutput({ toolCallId, output })
}
/>
);
}
+22
View File
@@ -324,6 +324,27 @@ export const sendEmail = tool({
},
});
export const askUser = tool({
description:
"Ask the user a question when you need clarification or input before proceeding. " +
"Present 2-4 options for the user to choose from. Use when uncertain about the user's intent.",
inputSchema: z.object({
question: z.string().describe("The question to ask the user"),
options: z
.array(
z.object({
id: z.string().describe("Unique option identifier"),
label: z.string().describe("Short option title"),
description: z.string().optional().describe("Longer explanation"),
})
)
.min(2)
.max(4),
}),
// No execute function — streamText ends, turn completes,
// frontend sends the answer via addToolOutput
});
/** Tool set passed to `streamText` for the main `chat.agent` run (includes PostHog). */
export const chatTools = {
inspectEnvironment,
@@ -333,6 +354,7 @@ export const chatTools = {
executeCode,
executeJs,
sendEmail,
askUser,
};
type ChatToolSet = typeof chatTools;
+9 -8
View File
@@ -176,7 +176,7 @@ export const aiChat = chat
.agent({
id: "ai-chat",
idleTimeoutInSeconds: 60,
chatAccessTokenTTL: "1m",
chatAccessTokenTTL: "1m", // TRI-8556 test
// #region Compaction — automatic context window management
compaction: {
@@ -392,15 +392,16 @@ export const aiChat = chat
chatAccessToken,
lastEventId,
}) => {
// Log whether data-turn-metadata persisted to the response
const metadataParts = responseMessage?.parts?.filter(
(p: any) => p.type === "data-turn-metadata"
);
logger.info("onTurnComplete response parts check", {
// Log responseMessage parts for debugging TRI-8556
const partTypes = responseMessage?.parts?.map((p: any) => p.type) ?? [];
const toolParts = responseMessage?.parts?.filter((p: any) => p.type?.startsWith("tool-")) ?? [];
logger.info("onTurnComplete responseMessage", {
hasResponseMessage: !!responseMessage,
responseMessageId: responseMessage?.id,
totalParts: responseMessage?.parts?.length ?? 0,
metadataPartsCount: metadataParts?.length ?? 0,
metadataParts,
partTypes,
toolPartsCount: toolParts.length,
toolParts: toolParts.map((p: any) => ({ type: p.type, state: p.state, toolCallId: p.toolCallId })),
});
await prisma.chat.update({
where: { id: chatId },