Skip to content

Commit f88cdc5

Browse files
committed
feat: Ensure warnings / errors on ai analysis sessions get tracked and rendered
1 parent 5378c76 commit f88cdc5

File tree

25 files changed

+1896
-15
lines changed

25 files changed

+1896
-15
lines changed

apps/api/src/routes/dashboard/dashboard.mappers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export function mapJobActivityTimelineToResponse(timeline: JobActivityTimeline):
4040
reasoning: event.reasoning,
4141
toolCalls: event.toolCalls,
4242
tokenUsage: event.tokenUsage,
43+
warnings: event.warnings,
4344
})),
4445
total: timeline.total,
4546
summary: timeline.summary,

apps/api/src/routes/jobs/jobs.handlers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ export const getSession: AppRouteHandler<routes.GetSessionRoute> = async (c) =>
495495
reasoning: session.reasoning,
496496
tokenUsage: session.tokenUsage,
497497
durationMs: session.durationMs,
498+
warnings: session.warnings,
498499
}, HTTPStatusCodes.OK);
499500
});
500501
};

apps/migrator/src/seed.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,7 @@ function generateAISessions(): Array<typeof schema.aiAnalysisSessions.$inferInse
826826
reasoning: string,
827827
actions: string[] = [],
828828
extraToolCalls: Array<Record<string, unknown>> = [],
829+
warnings?: Array<{ code: string; message: string; meta?: Record<string, unknown> }>,
829830
) => {
830831
const sessionTime = new Date(BLACK_FRIDAY_START.getTime() + minuteOffset * 60_000);
831832
const responseBody = getAISessionResponseBody(endpointId, minuteOffset);
@@ -845,6 +846,7 @@ function generateAISessions(): Array<typeof schema.aiAnalysisSessions.$inferInse
845846
tokenUsage: 800 + Math.floor(Math.random() * 500),
846847
durationMs: 250 + Math.floor(Math.random() * 200),
847848
nextAnalysisAt: new Date(sessionTime.getTime() + 20 * 60_000),
849+
...(warnings ? { warnings } : {}),
848850
});
849851
};
850852

@@ -860,13 +862,20 @@ function generateAISessions(): Array<typeof schema.aiAnalysisSessions.$inferInse
860862
// Hour 2: 09:00-10:00 - Peak building (4 sessions)
861863
generateBFSession(60, "ep-traffic-monitor", "ALERT: Traffic spiking. Page load degrading.", [], [tc.proposeInterval(25_000, 60, "Critical traffic—max monitoring")]);
862864
generateBFSession(75, "ep-order-processor-health", "Order queue building. Failure rate rising.");
863-
generateBFSession(90, "ep-inventory-sync-check", "Inventory sync lagging. Queue depth critical.");
865+
generateBFSession(90, "ep-inventory-sync-check", "Inventory sync lagging. Queue depth critical.", [], [], [
866+
{ code: "output_truncated", message: "Step 2 output was truncated (finishReason: length). The model was analyzing inventory queue backlog history and hit the output token limit.", meta: { stepIndex: 1, finishReason: "length" } },
867+
]);
864868
generateBFSession(105, "ep-traffic-monitor", "PEAK TRAFFIC. Page load strained. All systems under load.", [], [tc.proposeInterval(20_000, 60, "Peak load—minimum interval")]);
865869

866870
// Hour 3: 10:00-11:00 - Critical phase (4 sessions)
867-
generateBFSession(120, "ep-slow-page-analyzer", "CRITICAL: p95 latency extremely high. Database is bottleneck.", ["propose_interval"], [tc.pauseUntil(null, "Activating recovery endpoints"), tc.proposeNext(5_000, 5, "Trigger cache warmup")]);
871+
generateBFSession(120, "ep-slow-page-analyzer", "CRITICAL: p95 latency extremely high. Database is bottleneck.", ["propose_interval"], [tc.pauseUntil(null, "Activating recovery endpoints"), tc.proposeNext(5_000, 5, "Trigger cache warmup")], [
872+
{ code: "output_truncated", message: "Step 3 output was truncated (finishReason: length). The model was generating an extended root-cause analysis of database connection pool exhaustion and hit the output token limit.", meta: { stepIndex: 2, finishReason: "length" } },
873+
{ code: "missing_final_tool", message: "The model did not call submit_analysis. Analysis was extracted from the last tool call and raw model output." },
874+
]);
868875
generateBFSession(135, "ep-order-processor-health", "Order failure rate critical. Queue depth high. Recovery actions triggered.");
869-
generateBFSession(150, "ep-inventory-sync-check", "Inventory lag critical. Failed syncs detected. Critical but stable.");
876+
generateBFSession(150, "ep-inventory-sync-check", "Inventory lag critical. Failed syncs detected. Critical but stable.", [], [], [
877+
{ code: "missing_reasoning", message: "The model did not produce any reasoning text. It jumped directly to tool calls without a natural language explanation." },
878+
]);
870879
generateBFSession(165, "ep-traffic-monitor", "Sustained peak traffic. Systems holding under load.");
871880

872881
// Hour 4: 11:00-12:00 - Sustained load (3 sessions)

apps/web/src/components/ai/session-item.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1-
import { AlertCircle, Check, ChevronRight } from "lucide-react";
1+
import { AlertCircle, AlertTriangle, Check, ChevronRight } from "lucide-react";
22
import { useEffect, useRef, useState } from "react";
33
import { Button } from "@cronicorn/ui-library/components/button";
44
import { cn } from "@cronicorn/ui-library/lib/utils";
55

6+
export type AISessionWarning = {
7+
code: string;
8+
message: string;
9+
meta?: Record<string, unknown>;
10+
};
11+
612
export type AISession = {
713
id: string;
814
analyzedAt: string;
915
reasoning: string;
1016
toolCalls: Array<{ tool: string; args?: unknown; result?: unknown }>;
1117
tokenUsage: number | null;
1218
durationMs: number | null;
19+
warnings?: AISessionWarning[];
1320
};
1421

1522
// Type guards and safe accessors
@@ -126,6 +133,15 @@ export function AISessionItem({
126133
<span>{formattedDuration}</span>
127134
</>
128135
)}
136+
{Array.isArray(session.warnings) && session.warnings.length > 0 && (
137+
<>
138+
<span className="text-border"></span>
139+
<span className="inline-flex items-center gap-1 text-amber-600 dark:text-amber-400">
140+
<AlertTriangle className="h-3 w-3" />
141+
{session.warnings.length} {session.warnings.length === 1 ? "warning" : "warnings"}
142+
</span>
143+
</>
144+
)}
129145
</div>
130146

131147
{session.reasoning && (
@@ -151,6 +167,19 @@ export function AISessionItem({
151167
)}
152168
</div>
153169
)}
170+
{Array.isArray(session.warnings) && session.warnings.length > 0 && (
171+
<div className="mt-1.5 space-y-1">
172+
{session.warnings.map((warning, idx) => (
173+
<div
174+
key={idx}
175+
className="flex items-start gap-1.5 rounded-md border border-amber-200 bg-amber-50 px-2 py-1.5 text-xs text-amber-800 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-300"
176+
>
177+
<AlertTriangle className="mt-0.5 h-3 w-3 shrink-0" />
178+
<span>{warning.message}</span>
179+
</div>
180+
))}
181+
</div>
182+
)}
154183
{Array.isArray(session.toolCalls) && session.toolCalls.length > 0 && (
155184
<div className="mt-1.5 space-y-2">
156185
<div className="flex flex-wrap items-center gap-2 text-xs">

apps/web/src/components/dashboard-new/activity-event-item.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useMemo, useRef, useState } from "react";
22
import { Link } from "@tanstack/react-router";
3-
import { AlertCircle, Brain, Check, ChevronDown, ChevronUp, Clock, Play, Settings2, Zap } from "lucide-react";
3+
import { AlertCircle, AlertTriangle, Brain, Check, ChevronDown, ChevronUp, Clock, Play, Settings2, Zap } from "lucide-react";
44

55
import { Badge } from "@cronicorn/ui-library/components/badge";
66
import { Button } from "@cronicorn/ui-library/components/button";
@@ -18,6 +18,7 @@ export type ActivityEvent = {
1818
// Session-specific fields
1919
reasoning?: string;
2020
toolCalls?: Array<{ tool: string; args?: unknown; result?: unknown }>;
21+
warnings?: Array<{ code: string; message: string; meta?: Record<string, unknown> }>;
2122
};
2223

2324
/** Action tools that make scheduling changes */
@@ -118,6 +119,12 @@ export function ActivityEventItem({ event }: { event: ActivityEvent }) {
118119
{actionToolCount}
119120
</Badge>
120121
)}
122+
{Array.isArray(event.warnings) && event.warnings.length > 0 && (
123+
<Badge variant="outline" className="text-[10px] px-1 py-0 gap-0.5 text-error border-error">
124+
<AlertTriangle className="size-2.5" />
125+
{event.warnings.length}
126+
</Badge>
127+
)}
121128
</>
122129
)}
123130
</div>

apps/web/src/routes/_authed/ai-sessions.$id.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useSuspenseQuery } from "@tanstack/react-query";
22
import { Link, createFileRoute } from "@tanstack/react-router";
3-
import { Brain, Clock, ExternalLink, Zap } from "lucide-react";
3+
import { AlertTriangle, Brain, Clock, ExternalLink, Zap } from "lucide-react";
44

55
import { Badge } from "@cronicorn/ui-library/components/badge";
66
import { CodeDisplay } from "../../components/composed/code-display";
@@ -91,6 +91,25 @@ function AISessionDetailsPage() {
9191
</InfoGrid>
9292
</DetailSection>
9393

94+
{Array.isArray(session.warnings) && session.warnings.length > 0 && (
95+
<DetailSection title="Warnings">
96+
<div className="space-y-2">
97+
{session.warnings.map((warning, idx) => (
98+
<div
99+
key={idx}
100+
className="flex items-start gap-2 rounded-md border border-error bg-error/15 px-3 py-2 text-sm text-error"
101+
>
102+
<AlertTriangle className="mt-0.5 size-4 shrink-0" />
103+
<div>
104+
<span className="font-medium">{warning.code}</span>
105+
<p className="text-error">{warning.message}</p>
106+
</div>
107+
</div>
108+
))}
109+
</div>
110+
</DetailSection>
111+
)}
112+
94113
<DetailSection title="AI Reasoning">
95114
{session.reasoning ? (
96115
<div className="prose prose-sm dark:prose-invert max-w-none">

packages/adapter-ai/src/client.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Vercel AI SDK client implementation
22

3-
import type { AIClient, Tool } from "@cronicorn/domain";
3+
import type { AIClient, AISessionWarning, Tool } from "@cronicorn/domain";
44

55
import { generateText, hasToolCall, stepCountIs, tool } from "ai";
66
import { z } from "zod";
@@ -174,6 +174,31 @@ export function createVercelAiClient(config: VercelAiClientConfig): AIClient {
174174
totalTokens: result.totalUsage?.totalTokens,
175175
});
176176

177+
// Flag when any step was truncated by the output token limit
178+
const truncatedSteps = result.steps
179+
.map((step, i) => ({ step: i, finishReason: step.finishReason }))
180+
.filter(s => s.finishReason === "length");
181+
182+
// Build warnings for upstream consumers (planner, UI)
183+
const warnings: AISessionWarning[] = [];
184+
185+
if (truncatedSteps.length > 0) {
186+
config.logger?.warn("Output token limit hit — model response was truncated", {
187+
truncatedSteps,
188+
maxOutputTokens: maxTokens || config.maxOutputTokens || 4096,
189+
totalTokens: result.totalUsage?.totalTokens,
190+
});
191+
192+
warnings.push({
193+
code: "output_truncated",
194+
message: `Output token limit hit on ${truncatedSteps.length} step(s) — model response was truncated`,
195+
meta: {
196+
truncatedSteps,
197+
maxOutputTokens: maxTokens || config.maxOutputTokens || 4096,
198+
},
199+
});
200+
}
201+
177202
// Diagnostic logging when no tool calls captured — should not happen with prepareStep
178203
if (capturedToolCalls.length === 0) {
179204
config.logger?.warn("Zero tool calls captured", {
@@ -197,6 +222,7 @@ export function createVercelAiClient(config: VercelAiClientConfig): AIClient {
197222
reasoning: result.text,
198223
// totalUsage accumulates across all steps; usage is last step only
199224
tokenUsage: result.totalUsage?.totalTokens,
225+
warnings: warnings.length > 0 ? warnings : undefined,
200226
};
201227
}
202228
catch (error) {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE "ai_analysis_sessions" ADD COLUMN "warnings" jsonb;

0 commit comments

Comments
 (0)