fix(dashboard-agent): name a head-started chat instead of leaving it New chat
The gate counted transcript length, and a warm first turn arrives with the model's opening step already in it — so the very first exchange looked like a later one.
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
evalSampleRate,
|
||||
extractToolActivity,
|
||||
isCiEvalContext,
|
||||
isFirstUserExchange,
|
||||
MAX_EVAL_TOOL_OUTPUT_CHARS,
|
||||
sanitizeReplayedToolInputs,
|
||||
truncateEvalToolOutput,
|
||||
@@ -101,6 +102,29 @@ describe("dashboardAgent (mock harness)", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
describe("which turn names the chat", () => {
|
||||
const user = (id: string) => ({ id, role: "user" });
|
||||
const assistant = (id: string) => ({ id, role: "assistant" });
|
||||
|
||||
it("names it on the first exchange", () => {
|
||||
expect(isFirstUserExchange([user("u1")])).toBe(true);
|
||||
});
|
||||
|
||||
it("still names it when the turn was head-started", () => {
|
||||
// The warm first step arrives in `uiMessages`, so the transcript already holds
|
||||
// two messages on the very first exchange.
|
||||
expect(isFirstUserExchange([user("u1"), assistant("a1")])).toBe(true);
|
||||
});
|
||||
|
||||
it("does not rename on a later exchange", () => {
|
||||
expect(isFirstUserExchange([user("u1"), assistant("a1"), user("u2")])).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores a watch consent record, which the user never typed", () => {
|
||||
expect(isFirstUserExchange([user("watch-request:watch_1"), user("u1")])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("names the chat once, not on every turn", async () => {
|
||||
const { store, calls } = fakeStore();
|
||||
harness = mockChatAgent(dashboardAgent, {
|
||||
@@ -112,8 +136,9 @@ describe("dashboardAgent (mock harness)", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await harness.sendMessage(userMessage("first question"));
|
||||
await harness.sendMessage(userMessage("second question"));
|
||||
await harness.sendMessage(userMessage("first question", "u1"));
|
||||
// A distinct id: two turns are two messages, which is what the gate counts.
|
||||
await harness.sendMessage(userMessage("second question", "u2"));
|
||||
|
||||
expect(calls.setChatTitleIfDefault).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isWatchRequestMessageId } from "@internal/dashboard-agent-contracts";
|
||||
import { chat } from "@trigger.dev/sdk/ai";
|
||||
import { locals, logger, tasks } from "@trigger.dev/sdk";
|
||||
import { generateText, stepCountIs, streamText, type ModelMessage, type UIMessage } from "ai";
|
||||
@@ -272,6 +273,20 @@ function cleanTitle(raw: string): string {
|
||||
*/
|
||||
const pendingTitles = new Map<string, Promise<void>>();
|
||||
|
||||
/**
|
||||
* Whether this turn is the one that names the chat. Counted in user messages, not in
|
||||
* transcript length: a head-started turn arrives with the warm first step already in
|
||||
* `uiMessages`, so a length gate would see two messages on the very first exchange and
|
||||
* never name the chat at all. A watch's consent record is a user message the user did
|
||||
* not type, so it doesn't count as an exchange either.
|
||||
*/
|
||||
export function isFirstUserExchange(uiMessages: { role: string; id?: string }[]): boolean {
|
||||
const typed = uiMessages.filter(
|
||||
(message) => message.role === "user" && !isWatchRequestMessageId(message.id)
|
||||
);
|
||||
return typed.length <= 1;
|
||||
}
|
||||
|
||||
async function generateAndSaveTitle(
|
||||
store: DashboardAgentStore,
|
||||
chatId: string,
|
||||
@@ -382,9 +397,8 @@ export const dashboardAgent = chat.agent({
|
||||
|
||||
// Name the chat on the first exchange, started here so it runs while the model
|
||||
// answers. Awaited in `onBeforeTurnComplete`, not here; a failure only costs the
|
||||
// generated name. The gate is the transcript length at the START of the turn,
|
||||
// where one message means nothing has been answered yet.
|
||||
if (uiMessages.length <= 1 && !pendingTitles.has(chatId)) {
|
||||
// generated name.
|
||||
if (isFirstUserExchange(uiMessages) && !pendingTitles.has(chatId)) {
|
||||
const store = getStore();
|
||||
pendingTitles.set(
|
||||
chatId,
|
||||
|
||||
Reference in New Issue
Block a user