Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 22 additions & 17 deletions src/agent_manager/api/static/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -52922,6 +52922,7 @@ function statusLabel(state) {

// src/agent_manager/api/static/widget/react/AgentChatApp.tsx
var import_jsx_runtime4 = __toESM(require_jsx_runtime(), 1);
var TOKEN_BUDGET_EXCEEDED_MESSAGE = "This conversation has reached its context limit. Start a new chat to continue.";
var nextMessageCounter = 0;
function nextMessageId(role) {
nextMessageCounter += 1;
Expand Down Expand Up @@ -53033,23 +53034,27 @@ function AgentChatApp({ client, config, onAnswer, panelId, titleId }) {
}
patchEntry(assistantId, (entry) => ({ ...entry, typing: false }));
onAnswer(finalDetail);
} catch (error) {
try {
const data = await sendToAgent(text10);
patchEntry(assistantId, () => ({
id: assistantId,
role: "ai",
text: data.answer,
route: data.visited,
tools: data.used_tools
}));
onAnswer({ visited: data.visited ?? [], used_tools: data.used_tools ?? [] });
} catch {
patchEntry(assistantId, () => ({
id: assistantId,
role: "ai",
text: error instanceof Error && error.message ? `Something went wrong. Please try again.` : "Something went wrong. Please try again."
}));
} catch (streamError) {
if (streamError instanceof AgentChatHttpError && streamError.status === 429) {
patchEntry(assistantId, () => ({ id: assistantId, role: "ai", text: TOKEN_BUDGET_EXCEEDED_MESSAGE }));
} else {
try {
const data = await sendToAgent(text10);
patchEntry(assistantId, () => ({
id: assistantId,
role: "ai",
text: data.answer,
route: data.visited,
tools: data.used_tools
}));
onAnswer({ visited: data.visited ?? [], used_tools: data.used_tools ?? [] });
} catch (sendError) {
patchEntry(assistantId, () => ({
id: assistantId,
role: "ai",
text: sendError instanceof AgentChatHttpError && sendError.status === 429 ? TOKEN_BUDGET_EXCEEDED_MESSAGE : "Something went wrong. Please try again."
}));
}
}
} finally {
setSending(false);
Expand Down
47 changes: 27 additions & 20 deletions src/agent_manager/api/static/widget/react/AgentChatApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ type MessageEntry = {
tools?: ToolRecord[];
};

const TOKEN_BUDGET_EXCEEDED_MESSAGE =
"This conversation has reached its context limit. Start a new chat to continue.";

let nextMessageCounter = 0;

function nextMessageId(role: MessageEntry["role"]): string {
Expand Down Expand Up @@ -166,26 +169,30 @@ export function AgentChatApp({ client, config, onAnswer, panelId, titleId }: Age
}
patchEntry(assistantId, (entry) => ({ ...entry, typing: false }));
onAnswer(finalDetail);
} catch (error) {
try {
const data = await sendToAgent(text);
patchEntry(assistantId, () => ({
id: assistantId,
role: "ai",
text: data.answer,
route: data.visited,
tools: data.used_tools,
}));
onAnswer({ visited: data.visited ?? [], used_tools: data.used_tools ?? [] });
} catch {
patchEntry(assistantId, () => ({
id: assistantId,
role: "ai",
text:
error instanceof Error && error.message
? `Something went wrong. Please try again.`
: "Something went wrong. Please try again.",
}));
} catch (streamError) {
if (streamError instanceof AgentChatHttpError && streamError.status === 429) {
patchEntry(assistantId, () => ({ id: assistantId, role: "ai", text: TOKEN_BUDGET_EXCEEDED_MESSAGE }));
} else {
try {
const data = await sendToAgent(text);
patchEntry(assistantId, () => ({
id: assistantId,
role: "ai",
text: data.answer,
route: data.visited,
tools: data.used_tools,
}));
onAnswer({ visited: data.visited ?? [], used_tools: data.used_tools ?? [] });
} catch (sendError) {
patchEntry(assistantId, () => ({
id: assistantId,
role: "ai",
text:
sendError instanceof AgentChatHttpError && sendError.status === 429
? TOKEN_BUDGET_EXCEEDED_MESSAGE
: "Something went wrong. Please try again.",
}));
}
}
} finally {
setSending(false);
Expand Down
54 changes: 54 additions & 0 deletions tests/e2e/widget.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,60 @@ test("backend error renders a user-friendly message", async ({ page }) => {
await expect.poll(() => shadowText(page, ".messages")).toContain("Something went wrong. Please try again.");
});

async function mockConversationApiWithTokenBudgetExceeded(page: Page) {
const calls: string[] = [];

await page.route("**/conversations", async (route) => {
calls.push(`${route.request().method()} ${new URL(route.request().url()).pathname}`);
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ conversation_id: "conv-budget", session_id: "conv-budget" }),
});
});

await page.route("**/conversations/*/messages/stream", async (route: Route) => {
calls.push(`${route.request().method()} ${new URL(route.request().url()).pathname}`);
await route.fulfill({
status: 429,
contentType: "application/json",
body: JSON.stringify({ detail: "ConversationTokenBudgetExceeded" }),
});
});

await page.route("**/conversations/*/messages", async (route: Route) => {
calls.push(`${route.request().method()} ${new URL(route.request().url()).pathname}`);
if (route.request().method() === "GET") {
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([]) });
return;
}
await route.fulfill({
status: 429,
contentType: "application/json",
body: JSON.stringify({ detail: "ConversationTokenBudgetExceeded" }),
});
});

return calls;
}

test("token budget exceeded shows context-limit message instead of generic error", async ({ page }) => {
const calls = await mockConversationApiWithTokenBudgetExceeded(page);
await page.goto("/widget-demo.html");
await shadowClick(page, ".launcher");
await shadowFill(page, ".input", "one more message");
await shadowClick(page, ".send");

await expect
.poll(() => shadowText(page, ".messages"))
.toContain("This conversation has reached its context limit. Start a new chat to continue.");
const text = await shadowText(page, ".messages");
expect(text).not.toContain("Something went wrong. Please try again.");
expect(calls).toContain("POST /conversations/conv-budget/messages/stream");
// Streaming already returned 429 — do not waste a second request on the non-streaming fallback.
expect(calls).not.toContain("POST /conversations/conv-budget/messages");
});

test("stale stored conversation is replaced before sending to the agent", async ({ page }) => {
const calls = await mockConversationApiWithStaleConversation(page);
await page.goto("/widget-demo.html");
Expand Down
Loading