Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ const VisibleTranscriptEntries = memo(function VisibleTranscriptEntries(props: {
)}
renderFailure={(entry) => (
<TranscriptFailureView
outcome={entry.outcome}
failureCode={entry.failureCode}
failureReason={entry.failureReason}
timestamp={entry.timestamp}
/>
)}
Expand Down Expand Up @@ -374,7 +375,8 @@ function RedactedTranscriptView(props: {
)}
renderFailure={(entry) => (
<TranscriptFailureView
outcome={entry.outcome}
failureCode={entry.failureCode}
failureReason={entry.failureReason}
timestamp={entry.timestamp}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import { CircleAlert } from "lucide-react";
import type {
ConversationTurnFailureCode,
ConversationTurnFailureReason,
} from "@sentry/junior/api/schema";

import { getDashboardAgentName } from "../agentName";
import { formatMessageTimestamp } from "../format";
import {
transcriptFailureDescription,
transcriptFailureTitle,
} from "./transcriptFailure";

/** Render a terminal transcript failure as a distinct alert surface. */
export function TranscriptFailureView(props: {
outcome: "error" | "delivery_failed";
failureCode: ConversationTurnFailureCode;
failureReason?: ConversationTurnFailureReason;
timestamp?: number;
}) {
const timestamp = formatMessageTimestamp(props.timestamp);
const deliveryFailed = props.outcome === "delivery_failed";

return (
<div
className="grid min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3 rounded-lg bg-rose-300/[0.1] px-4 py-3 text-rose-100 max-md:grid-cols-[auto_minmax(0,1fr)]"
data-transcript-failure={props.outcome}
data-transcript-failure={props.failureCode}
data-transcript-failure-reason={props.failureReason}
role="alert"
>
<CircleAlert
Expand All @@ -24,12 +32,10 @@ export function TranscriptFailureView(props: {
/>
<div className="min-w-0">
<div className="font-display text-base font-semibold leading-tight">
{deliveryFailed ? "Message delivery failed" : "Agent response failed"}
{transcriptFailureTitle(props.failureCode, props.failureReason)}
</div>
<div className="mt-1 text-sm leading-relaxed text-rose-100/70">
{deliveryFailed
? `${getDashboardAgentName()} could not deliver this message to its destination.`
: `The model response ended before ${getDashboardAgentName()} could complete this turn.`}
{transcriptFailureDescription(props.failureCode, props.failureReason)}
</div>
</div>
{timestamp ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,8 +413,11 @@ export function transcriptMessagesFromEvents(

if (data.type === "turn_lifecycle" && data.state === "failed") {
messages.push({
role: data.failureKind === "delivery" ? "system" : "assistant",
outcome: data.failureKind === "delivery" ? "delivery_failed" : "error",
role: data.failureCode === "delivery_failed" ? "system" : "assistant",
failureCode: data.failureCode,
...(data.failureReason
? { failureReason: data.failureReason }
: undefined),
parts: [],
sourceSeq: event.seq,
timestamp: eventTimestamp(event),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ function eventTailVersion(event: ConversationReportEvent | undefined): string {
data.type,
data.turnId,
data.state,
"failureKind" in data ? data.failureKind : "",
"failureCode" in data ? data.failureCode : "",
"failureReason" in data ? (data.failureReason ?? "") : "",
].join(":");
case "structured_event":
return [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import type {
ConversationTurnFailureCode,
ConversationTurnFailureReason,
} from "@sentry/junior/api/schema";

import { getDashboardAgentName } from "../agentName";

/** Title for one failed turn in the transcript. */
export function transcriptFailureTitle(
failureCode: ConversationTurnFailureCode,
failureReason?: ConversationTurnFailureReason,
): string {
if (failureCode === "model_execution_failed" && failureReason) {
switch (failureReason) {
case "auth":
return "Model credentials rejected";
case "permission":
return "Model access denied";
case "rate_limit":
return "Model rate limited";
case "capacity":
return "Model at capacity";
case "timeout":
return "Model timed out";
case "network":
return "Model connection failed";
case "server":
return "Model service error";
case "invalid_request":
return "Invalid model request";
case "invalid_response":
return "Invalid model response";
case "quota":
return "Model quota exhausted";
case "content_policy":
return "Blocked by content policy";
case "empty_output":
return "Empty model response";
case "tool_errors":
return "Tool failed";
case "suppressed_output":
return "Model output was dropped";
case "unknown":
return "Model service error";
}
}

switch (failureCode) {
case "delivery_failed":
return "Message delivery failed";
case "model_execution_failed":
return "Model failed";
case "persistence_failed":
return "Save failed";
case "agent_run_failed":
return "Internal error";
}

return "Internal error";
}

/** Short description for one failed turn in the transcript. */
export function transcriptFailureDescription(
failureCode: ConversationTurnFailureCode,
failureReason?: ConversationTurnFailureReason,
): string {
const agentName = getDashboardAgentName();

if (failureCode === "model_execution_failed" && failureReason) {
switch (failureReason) {
case "auth":
return "The model service rejected credentials. An admin needs to fix configuration.";
case "permission":
return "The model service denied access to this model or request.";
case "rate_limit":
return "The model is rate limited. Try again shortly.";
case "capacity":
return "The selected model is at capacity. Try again shortly.";
case "timeout":
return "The model service timed out before the turn finished.";
case "network":
return "The model service had a connection problem before the turn finished.";
case "server":
return "The model service returned an error.";
case "invalid_request":
return "The model service rejected this request as invalid.";
case "invalid_response":
return "The model service returned an invalid response.";
case "quota":
return "The model service quota is exhausted. An admin needs to fix billing or limits.";
case "content_policy":
return "The model service blocked this request under its content policy.";
case "empty_output":
return `The model returned no usable text before ${agentName} finished this turn.`;
case "tool_errors":
return "A tool failed and the turn could not finish.";
case "suppressed_output":
return `The model produced text that ${agentName} could not deliver.`;
case "unknown":
return "The model service failed for an unknown reason.";
}
}

switch (failureCode) {
case "delivery_failed":
return `${agentName} could not deliver this message.`;
case "model_execution_failed":
return `The model stopped before ${agentName} finished this turn.`;
case "persistence_failed":
return `${agentName} could not save the result of this turn.`;
case "agent_run_failed":
return `${agentName} hit an internal error during this turn.`;
}

return `${agentName} hit an internal error during this turn.`;
}

/** Search text for one failed turn in the transcript. */
export function transcriptFailureSearchText(
failureCode: ConversationTurnFailureCode,
failureReason?: ConversationTurnFailureReason,
): string {
return [
transcriptFailureTitle(failureCode, failureReason),
transcriptFailureDescription(failureCode, failureReason),
failureCode.replaceAll("_", " "),
...(failureReason ? [failureReason.replaceAll("_", " ")] : []),
].join(" ");
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import type {
ConversationTurnFailureCode,
ConversationTurnFailureReason,
} from "@sentry/junior/api/schema";

import type {
TranscriptViewAttachmentsDeliveredPart,
TranscriptViewContextEventPart,
Expand All @@ -12,7 +17,8 @@ import type {
export type RenderedFailureEntry = {
key: string;
kind: "failure";
outcome: "error" | "delivery_failed";
failureCode: ConversationTurnFailureCode;
failureReason?: ConversationTurnFailureReason;
timestamp?: number;
};

Expand Down Expand Up @@ -157,11 +163,14 @@ export function groupTranscriptMessages(
}

flushMessage();
if (message.outcome) {
if (message.failureCode) {
entries.push({
key: `${message.sourceSeq}:failure`,
kind: "failure",
outcome: message.outcome,
failureCode: message.failureCode,
...(message.failureReason
? { failureReason: message.failureReason }
: undefined),
timestamp: message.timestamp,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
messageRawText,
type RenderedTranscriptEntry,
} from "./transcriptRenderModel";
import { transcriptFailureSearchText } from "./transcriptFailure";
import { stringifyPartValue } from "../format";

// ─── Context ────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -131,9 +132,7 @@ export function entryMatchesSearch(

if (entry.kind === "failure") {
return textContains(
entry.outcome === "delivery_failed"
? "message delivery failed"
: "agent response failed error",
transcriptFailureSearchText(entry.failureCode, entry.failureReason),
normalizedQuery,
);
}
Expand Down
30 changes: 18 additions & 12 deletions packages/junior-dashboard/src/client/markdownExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,15 @@ import {
groupTranscriptMessages,
messageRawText,
} from "./conversations/transcriptRenderModel";
import { getDashboardAgentName } from "./agentName";
import { conversationTranscriptMessages } from "./conversations/eventTranscript";
import {
transcriptFailureDescription,
transcriptFailureTitle,
} from "./conversations/transcriptFailure";
import type {
ConversationTurnFailureCode,
ConversationTurnFailureReason,
} from "@sentry/junior/api/schema";
import type {
Conversation,
ConversationTranscript,
Expand Down Expand Up @@ -126,7 +133,8 @@ function appendTranscriptMessages(
appendFailure(
lines,
conversationTranscript,
entry.outcome,
entry.failureCode,
entry.failureReason,
entry.timestamp,
);
continue;
Expand Down Expand Up @@ -213,22 +221,20 @@ function appendReasoning(
function appendFailure(
lines: string[],
conversationTranscript: ConversationTranscript,
outcome: "error" | "delivery_failed",
failureCode: ConversationTurnFailureCode,
failureReason: ConversationTurnFailureReason | undefined,
timestamp: number | undefined,
): void {
lines.push(
"",
outcome === "delivery_failed"
? "### Message delivery failed"
: "### Agent response failed",
`### ${transcriptFailureTitle(failureCode, failureReason)}`,
);
addEventMeta(lines, conversationTranscript, timestamp);
lines.push(
"",
outcome === "delivery_failed"
? `${getDashboardAgentName()} could not deliver this message to its destination.`
: `The model response ended before ${getDashboardAgentName()} could complete this turn.`,
);
lines.push("", transcriptFailureDescription(failureCode, failureReason));
addMetaLine(lines, "Code", failureCode);
if (failureReason) {
addMetaLine(lines, "Reason", failureReason);
}
}

function appendContextEvent(
Expand Down
5 changes: 4 additions & 1 deletion packages/junior-dashboard/src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type {
ActorIdentity,
ConversationStatsReport,
ConversationSummaryReport,
ConversationTurnFailureCode,
ConversationTurnFailureReason,
} from "@sentry/junior/api/schema";
import type { ConversationDetailReport } from "@sentry/junior/api/schema";
import type { ConversationEventPresentation } from "@sentry/junior-plugin-api";
Expand Down Expand Up @@ -128,7 +130,8 @@ export type TranscriptViewMessage = {
reasoningLevel: string;
source: "configured" | "inherited" | "router";
};
outcome?: "error" | "delivery_failed";
failureCode?: ConversationTurnFailureCode;
failureReason?: ConversationTurnFailureReason;
parts: TranscriptViewPart[];
role: "assistant" | "system" | "tool" | "user";
source?: "slack" | "web";
Expand Down
3 changes: 2 additions & 1 deletion packages/junior-dashboard/src/mock-reporting/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1185,7 +1185,8 @@ function failedConversation(nowMs: number): ConversationDetailReport {
type: "turn_lifecycle",
turnId: "failed-turn",
state: "failed",
failureKind: "agent",
failureCode: "model_execution_failed",
failureReason: "network",
}),
],
});
Expand Down
Loading
Loading