From 11da9abed573b22a456c392c7689d575fc98d47e Mon Sep 17 00:00:00 2001 From: Yashas Date: Mon, 17 Aug 2026 19:51:28 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20AI=20trail=20tab=20=E2=80=94=20who=20di?= =?UTF-8?q?d=20what,=20which=20tools,=20what=20it=20reasoned?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The console showed the agent's OUTPUT but never showed that an agent produced it. On a screen full of numbers a viewer cannot tell which came from a model, which from a rule, and which from a person. This makes it visible. Second tab on the file: "AI trail", beside "Evidence & decision". * WHO DID WHAT — the real audit trail from /view/audit, one row per completed step, each attributed. "Credit Assessor AI" appears there because the agent submitted the activity under its OWN JWT and the platform recorded it, not because this component says so. Agent rows get a bot glyph and a violet chip; human rows get a person and no chip, plus a count: "1 of 6 by an agent". * TOOLS IT CALLS — the three deterministic SQL tools, named, each with the figures it returns for this file shown against the threshold it was tested on. Spread beyond tolerance goes amber. * WHAT IT REASONED — the narrative, citations and deviation flag verbatim, violet as everywhere else, closing with the explicit statement that it computed no ratio and chose no outcome. Deliberately NOT shown: the per-step trace (individual model calls, raw tool payloads, durations, tokens, cost). It exists in aiemployee.tbl_ai_trace_events and is served by /ai-employee/monitor/instances/:id/trace, but that endpoint requires an ORG-SCOPED token and the app JWT carries no org claim — it answers 403 "no organization scope on this account" for every user of this console. A footnote says so and points at Studio. Rendering a plausible waterfall from guesses would be the one dishonest thing on a screen whose whole purpose is showing what actually happened. Co-Authored-By: Claude Opus 5 (1M context) --- src/components/AiDecisionPanel.tsx | 274 +++++++++++++++++++++++++++++ src/screens/ApplicationScreen.tsx | 35 +++- 2 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 src/components/AiDecisionPanel.tsx diff --git a/src/components/AiDecisionPanel.tsx b/src/components/AiDecisionPanel.tsx new file mode 100644 index 0000000..c6e6e45 --- /dev/null +++ b/src/components/AiDecisionPanel.tsx @@ -0,0 +1,274 @@ +import { Bot, Calculator, CheckCircle2, Cpu, ScrollText, User } from 'lucide-react'; +import { Badge, Card, Facts } from './core'; +import type { AuditEntry, Row } from '../api/types'; +import { POLICY } from '../api/config'; +import { dateTime, money, moneyShort, num, pct, ratio, str } from '../format'; + +/** + * Who did what on this file, and what the agent actually saw. + * + * THE CLAIM THIS PANEL HAS TO MAKE, and the reason it exists: on a screen full + * of numbers, a viewer cannot tell which came from a model and which from a + * rule, or which steps a person took. Saying it out loud is weaker than showing + * it — so every row here is attributed, and the attribution is read from the + * workflow's own audit trail rather than asserted by this component. + * + * WHAT IS REAL AND WHAT IS NOT, because that distinction matters more here than + * anywhere else in the app: + * + * * The timeline is the ACTUAL audit trail (`/view/audit`). "Credit + * Assessor AI" appears there because the agent submitted the activity under + * its own JWT and the platform recorded it — not because this file says so. + * * The agent's analysis, citations and deviation flag are the ACTUAL values + * it wrote, read off the instance. + * * The tool figures are the ACTUAL values on the instance, labelled with the + * tool that computes them. The mapping is fixed in 09_agent_tools.sql and + * each tool's SQL was verified to agree with the gate. + * + * NOT shown, and deliberately not faked: the per-step trace — individual LLM + * calls, raw tool payloads, durations, token counts, cost. That lives in + * `aiemployee.tbl_ai_trace_events` and is served by + * `/ai-employee/monitor/instances/:id/trace`, which requires an ORG-SCOPED + * token. The app JWT has no org claim (its claims are user_id / name / email / + * sub / exp / iat), so that endpoint answers 403 "no organization scope on this + * account" for every user of this console. Rendering a plausible-looking + * waterfall from guesses would be the one dishonest thing on a screen whose + * whole job is showing what actually happened. + */ + +/** The roles that belong to an agent rather than a person. */ +const AI_ROLES = ['ai_credit_assessor', 'ai_loan_advisor']; + +function isAgent(e: AuditEntry): boolean { + const roles = e.user_roles ?? []; + return roles.some((r) => AI_ROLES.includes(r)); +} + +/** Audit rows worth showing a human. The commit row for an activity carries the + * state it reached; the rest is engine bookkeeping. */ +function meaningful(entries: AuditEntry[]): AuditEntry[] { + return entries + .filter((e) => { + const st = str(e.execution_state); + // A commit row's execution_state is the state uid it landed on. + return st.startsWith('hdfc-state-'); + }) + .slice() + .reverse(); +} + +export function AiDecisionPanel({ row, audit }: { row: Row; audit: AuditEntry[] }) { + const steps = meaningful(audit); + const agentSteps = steps.filter(isAgent); + + const itr = num(row.itr_declared_income); + const gst = num(row.gst_turnover_12m); + const bank = num(row.bank_credits_12m); + const variance = num(row.income_variance_pct); + const dscr = num(row.dscr); + const maxElig = num(row.max_eligible_amount); + const analysis = str(row.ai_analysis); + const citations = str(row.assessment_citations); + const deviation = str(row.deviation_flags); + const isPersonal = str(row.loan_product_family) === 'personal'; + + return ( +
+ {/* ---------------- who did what ---------------- */} + + + Who did what + + } + subtitle="From the workflow's audit trail — not from this screen's opinion" + actions={ + agentSteps.length > 0 ? ( + + {agentSteps.length} of {steps.length} by an agent + + ) : ( + no agent steps yet + ) + } + > + {steps.length === 0 ? ( +

No completed steps recorded yet.

+ ) : ( +
    + {steps.map((e, i) => { + const agent = isAgent(e); + return ( +
  1. + + {agent ? : } + +
    +
    + + {str(e.activity_name) || str(e.activity_id)} + + {agent && agent} +
    +
    + {str(e.user_name) || 'System'} + {(e.user_roles ?? []).length > 0 && ( + + {' · '} + {(e.user_roles ?? []).join(', ')} + + )} +
    +
    + {dateTime(e.created_at)} +
  2. + ); + })} +
+ )} +
+ + {/* ---------------- the tools ---------------- + Deterministic SQL the assessor calls instead of doing arithmetic. The + figures are the ones on the file; the attribution is the fixed mapping + from 09_agent_tools.sql. */} + + + Tools the assessor calls + + } + subtitle={`Deterministic SQL, same formulas as the gate · policy ${POLICY.version}`} + > +
+ POLICY.varianceTolerancePct + ? ('amber' as const) + : ('emerald' as const), + }, + ]} + /> + + +
+
+ + {/* ---------------- what the agent wrote ---------------- */} + {analysis && ( +
+
+ +

What the agent reasoned

+ {deviation && flagged: {deviation}} + narrative, not verdict +
+
+

{analysis}

+ {citations && ( + + )} +
+ +

+ The agent wrote the account above and flagged the deviation. It did not compute any + ratio and did not choose the outcome — the figures come from the tools and the + approve / refer / reject decision comes from the rules engine, after it submitted. +

+
+
+
+ )} + + {/* Honest about the gap. Better than a fabricated waterfall. */} +

+ Per-step detail — individual model calls, raw tool payloads, durations and cost — is + recorded by the platform against this instance but is not readable with an app sign-in. + It requires an org-scoped token, so it is available from Studio rather than here. +

+
+ ); +} + +function ToolBlock({ + name, + purpose, + rows, +}: { + name: string; + purpose: string; + rows: Array<{ label: string; value: string; tone?: 'amber' | 'emerald' }>; +}) { + return ( +
+
+ + {name} + + {purpose} +
+
+ {rows.map((r) => ( +
+
{r.label}
+
+ {r.value} +
+
+ ))} +
+
+ ); +} diff --git a/src/screens/ApplicationScreen.tsx b/src/screens/ApplicationScreen.tsx index abe2594..908a418 100644 --- a/src/screens/ApplicationScreen.tsx +++ b/src/screens/ApplicationScreen.tsx @@ -7,6 +7,7 @@ import type { Action } from '../api/config'; import { Badge, Button, Card, ErrorNote, Facts, Spinner } from '../components/core'; import { StageBadge, StageRail } from '../components/Stage'; import { EvidencePanel } from '../components/EvidencePanel'; +import { AiDecisionPanel } from '../components/AiDecisionPanel'; import { ActivityForm } from '../components/ActivityForm'; import type { Row } from '../api/types'; import { @@ -31,6 +32,10 @@ export function ApplicationScreen() { const { id = '' } = useParams(); const { client, user } = useZino(); const [openAction, setOpenAction] = useState(null); + // Two views of the same file: the evidence a decision rests on, and the record + // of who produced it. Tabs rather than one long column — the second is what a + // sceptical reader asks for, and it should be one click away, not a scroll. + const [tab, setTab] = useState<'evidence' | 'ai'>('evidence'); // The detail view is the right endpoint for one instance. The record view // filtered on instance_id is kept as a fallback because it returns the same @@ -56,6 +61,11 @@ export function ApplicationScreen() { 15000, ); + // The audit trail is what makes the AI attribution real rather than asserted: + // "Credit Assessor AI" appears because the platform recorded the agent + // submitting the activity under its own identity. + const audit = useQuery(() => client.audit(id), [id], true, 15000); + const row = file.data; const stage = str(row?.current_state_name); const actions = actionsFor(stage, user?.roles); @@ -111,7 +121,30 @@ export function ApplicationScreen() {
{/* Evidence first — see the note at the top of this file. */}
- +
+ {([ + ['evidence', 'Evidence & decision'], + ['ai', 'AI trail'], + ] as const).map(([key, label]) => ( + + ))} +
+ + {tab === 'evidence' ? ( + + ) : ( + + )}