Skip to content

Commit 3f35d4a

Browse files
committed
feat(ai-chat): add askUser tool for HITL testing, verify TRI-8556 fix
1 parent 3884bc5 commit 3f35d4a

File tree

3 files changed

+69
-10
lines changed

3 files changed

+69
-10
lines changed

references/ai-chat/src/components/chat.tsx

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"use client";
22

33
import { useChat } from "@ai-sdk/react";
4-
import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
4+
import {
5+
lastAssistantMessageIsCompleteWithApprovalResponses,
6+
lastAssistantMessageIsCompleteWithToolCalls,
7+
} from "ai";
58
import type { ChatUiMessage } from "@/lib/chat-tools";
69
import type { TriggerChatTransport } from "@trigger.dev/sdk/chat";
710
import type { CompactionChunkData } from "@trigger.dev/sdk/ai";
@@ -14,10 +17,12 @@ function ToolInvocation({
1417
part,
1518
onApprove,
1619
onDeny,
20+
onToolOutput,
1721
}: {
1822
part: any;
1923
onApprove?: (approvalId: string) => void;
2024
onDeny?: (approvalId: string) => void;
25+
onToolOutput?: (toolCallId: string, output: unknown) => void;
2126
}) {
2227
const [expanded, setExpanded] = useState(false);
2328
const toolName = part.type.startsWith("tool-") ? part.type.slice(5) : "tool";
@@ -72,6 +77,31 @@ function ToolInvocation({
7277
</div>
7378
)}
7479

80+
{/* askUser tool: show question + option buttons when input-available */}
81+
{toolName === "askUser" && state === "input-available" && args?.question && (
82+
<div className="border-t border-gray-200 px-3 py-2 space-y-2">
83+
<div className="font-medium text-gray-700">{args.question}</div>
84+
<div className="flex flex-wrap gap-2">
85+
{(args.options ?? []).map((opt: any) => (
86+
<button
87+
key={opt.id}
88+
type="button"
89+
onClick={() =>
90+
onToolOutput?.(part.toolCallId, {
91+
skipped: false,
92+
answers: [{ questionId: args.question, optionId: opt.id, text: opt.label }],
93+
})
94+
}
95+
className="rounded border border-blue-300 bg-blue-50 px-3 py-1.5 text-xs text-blue-700 hover:bg-blue-100"
96+
title={opt.description}
97+
>
98+
{opt.label}
99+
</button>
100+
))}
101+
</div>
102+
</div>
103+
)}
104+
75105
{expanded && (
76106
<div className="border-t border-gray-200 px-3 py-2 space-y-2">
77107
{args && Object.keys(args).length > 0 && (
@@ -310,14 +340,17 @@ export function Chat({
310340
sendMessage,
311341
stop: aiStop,
312342
addToolApprovalResponse,
343+
addToolOutput,
313344
status,
314345
error,
315346
} = useChat({
316347
id: chatId,
317348
messages: initialMessages,
318349
transport,
319350
resume: resumeProp,
320-
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
351+
sendAutomaticallyWhen: (opts) =>
352+
lastAssistantMessageIsCompleteWithApprovalResponses(opts) ||
353+
lastAssistantMessageIsCompleteWithToolCalls(opts),
321354
});
322355

323356
// Use transport.stopGeneration for reliable stop after reconnect.
@@ -616,6 +649,9 @@ export function Chat({
616649
part={part}
617650
onApprove={handleApprove}
618651
onDeny={handleDeny}
652+
onToolOutput={(toolCallId, output) =>
653+
addToolOutput({ toolCallId, output })
654+
}
619655
/>
620656
);
621657
}

references/ai-chat/src/lib/chat-tools.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,27 @@ export const sendEmail = tool({
324324
},
325325
});
326326

327+
export const askUser = tool({
328+
description:
329+
"Ask the user a question when you need clarification or input before proceeding. " +
330+
"Present 2-4 options for the user to choose from. Use when uncertain about the user's intent.",
331+
inputSchema: z.object({
332+
question: z.string().describe("The question to ask the user"),
333+
options: z
334+
.array(
335+
z.object({
336+
id: z.string().describe("Unique option identifier"),
337+
label: z.string().describe("Short option title"),
338+
description: z.string().optional().describe("Longer explanation"),
339+
})
340+
)
341+
.min(2)
342+
.max(4),
343+
}),
344+
// No execute function — streamText ends, turn completes,
345+
// frontend sends the answer via addToolOutput
346+
});
347+
327348
/** Tool set passed to `streamText` for the main `chat.agent` run (includes PostHog). */
328349
export const chatTools = {
329350
inspectEnvironment,
@@ -333,6 +354,7 @@ export const chatTools = {
333354
executeCode,
334355
executeJs,
335356
sendEmail,
357+
askUser,
336358
};
337359

338360
type ChatToolSet = typeof chatTools;

references/ai-chat/src/trigger/chat.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export const aiChat = chat
176176
.agent({
177177
id: "ai-chat",
178178
idleTimeoutInSeconds: 60,
179-
chatAccessTokenTTL: "1m",
179+
chatAccessTokenTTL: "1m", // TRI-8556 test
180180

181181
// #region Compaction — automatic context window management
182182
compaction: {
@@ -392,15 +392,16 @@ export const aiChat = chat
392392
chatAccessToken,
393393
lastEventId,
394394
}) => {
395-
// Log whether data-turn-metadata persisted to the response
396-
const metadataParts = responseMessage?.parts?.filter(
397-
(p: any) => p.type === "data-turn-metadata"
398-
);
399-
logger.info("onTurnComplete response parts check", {
395+
// Log responseMessage parts for debugging TRI-8556
396+
const partTypes = responseMessage?.parts?.map((p: any) => p.type) ?? [];
397+
const toolParts = responseMessage?.parts?.filter((p: any) => p.type?.startsWith("tool-")) ?? [];
398+
logger.info("onTurnComplete responseMessage", {
400399
hasResponseMessage: !!responseMessage,
400+
responseMessageId: responseMessage?.id,
401401
totalParts: responseMessage?.parts?.length ?? 0,
402-
metadataPartsCount: metadataParts?.length ?? 0,
403-
metadataParts,
402+
partTypes,
403+
toolPartsCount: toolParts.length,
404+
toolParts: toolParts.map((p: any) => ({ type: p.type, state: p.state, toolCallId: p.toolCallId })),
404405
});
405406
await prisma.chat.update({
406407
where: { id: chatId },

0 commit comments

Comments
 (0)