Skip to content

Commit d64e5f0

Browse files
committed
Merge branch 'feat/dashboard-agent-flows' into feat/dashboard-agent-ui
2 parents 2040374 + 2ff7ec0 commit d64e5f0

2 files changed

Lines changed: 44 additions & 5 deletions

File tree

internal-packages/dashboard-agent/src/dashboard-agent.test.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
evalSampleRate,
2525
extractToolActivity,
2626
isCiEvalContext,
27+
isFirstUserExchange,
2728
MAX_EVAL_TOOL_OUTPUT_CHARS,
2829
sanitizeReplayedToolInputs,
2930
truncateEvalToolOutput,
@@ -101,6 +102,29 @@ describe("dashboardAgent (mock harness)", () => {
101102
]);
102103
});
103104

105+
describe("which turn names the chat", () => {
106+
const user = (id: string) => ({ id, role: "user" });
107+
const assistant = (id: string) => ({ id, role: "assistant" });
108+
109+
it("names it on the first exchange", () => {
110+
expect(isFirstUserExchange([user("u1")])).toBe(true);
111+
});
112+
113+
it("still names it when the turn was head-started", () => {
114+
// The warm first step arrives in `uiMessages`, so the transcript already holds
115+
// two messages on the very first exchange.
116+
expect(isFirstUserExchange([user("u1"), assistant("a1")])).toBe(true);
117+
});
118+
119+
it("does not rename on a later exchange", () => {
120+
expect(isFirstUserExchange([user("u1"), assistant("a1"), user("u2")])).toBe(false);
121+
});
122+
123+
it("ignores a watch consent record, which the user never typed", () => {
124+
expect(isFirstUserExchange([user("watch-request:watch_1"), user("u1")])).toBe(true);
125+
});
126+
});
127+
104128
it("names the chat once, not on every turn", async () => {
105129
const { store, calls } = fakeStore();
106130
harness = mockChatAgent(dashboardAgent, {
@@ -112,8 +136,9 @@ describe("dashboardAgent (mock harness)", () => {
112136
},
113137
});
114138

115-
await harness.sendMessage(userMessage("first question"));
116-
await harness.sendMessage(userMessage("second question"));
139+
await harness.sendMessage(userMessage("first question", "u1"));
140+
// A distinct id: two turns are two messages, which is what the gate counts.
141+
await harness.sendMessage(userMessage("second question", "u2"));
117142

118143
expect(calls.setChatTitleIfDefault).toHaveLength(1);
119144
});

internal-packages/dashboard-agent/src/dashboard-agent.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isWatchRequestMessageId } from "@internal/dashboard-agent-contracts";
12
import { chat } from "@trigger.dev/sdk/ai";
23
import { locals, logger, tasks } from "@trigger.dev/sdk";
34
import { generateText, stepCountIs, streamText, type ModelMessage, type UIMessage } from "ai";
@@ -272,6 +273,20 @@ function cleanTitle(raw: string): string {
272273
*/
273274
const pendingTitles = new Map<string, Promise<void>>();
274275

276+
/**
277+
* Whether this turn is the one that names the chat. Counted in user messages, not in
278+
* transcript length: a head-started turn arrives with the warm first step already in
279+
* `uiMessages`, so a length gate would see two messages on the very first exchange and
280+
* never name the chat at all. A watch's consent record is a user message the user did
281+
* not type, so it doesn't count as an exchange either.
282+
*/
283+
export function isFirstUserExchange(uiMessages: { role: string; id?: string }[]): boolean {
284+
const typed = uiMessages.filter(
285+
(message) => message.role === "user" && !isWatchRequestMessageId(message.id)
286+
);
287+
return typed.length <= 1;
288+
}
289+
275290
async function generateAndSaveTitle(
276291
store: DashboardAgentStore,
277292
chatId: string,
@@ -382,9 +397,8 @@ export const dashboardAgent = chat.agent({
382397

383398
// Name the chat on the first exchange, started here so it runs while the model
384399
// answers. Awaited in `onBeforeTurnComplete`, not here; a failure only costs the
385-
// generated name. The gate is the transcript length at the START of the turn,
386-
// where one message means nothing has been answered yet.
387-
if (uiMessages.length <= 1 && !pendingTitles.has(chatId)) {
400+
// generated name.
401+
if (isFirstUserExchange(uiMessages) && !pendingTitles.has(chatId)) {
388402
const store = getStore();
389403
pendingTitles.set(
390404
chatId,

0 commit comments

Comments
 (0)