diff --git a/src/api/types.ts b/src/api/types.ts
index a9c0c86..cd079d6 100644
--- a/src/api/types.ts
+++ b/src/api/types.ts
@@ -73,6 +73,9 @@ export interface AuditEntry {
state_name?: string;
created_at?: string;
performed_at?: string;
+ /** What was submitted, already labelled and typed by view-service. This is
+ * what lets the trace show WHAT each step did, not just who did it. */
+ fields?: Array<{ field_id: string; label: string; data_type: string; value: string }>;
ai_reasoning?: string | null;
ai_confidence?: number | null;
ai_model?: string | null;
diff --git a/src/components/AiDecisionPanel.tsx b/src/components/AiDecisionPanel.tsx
index c6e6e45..21bc775 100644
--- a/src/components/AiDecisionPanel.tsx
+++ b/src/components/AiDecisionPanel.tsx
@@ -1,8 +1,8 @@
-import { Bot, Calculator, CheckCircle2, Cpu, ScrollText, User } from 'lucide-react';
-import { Badge, Card, Facts } from './core';
+import { CheckCircle2, Cpu } from 'lucide-react';
+import { Badge, Facts } from './core';
+import { InstanceTrace } from './InstanceTrace';
import type { AuditEntry, Row } from '../api/types';
-import { POLICY } from '../api/config';
-import { dateTime, money, moneyShort, num, pct, ratio, str } from '../format';
+import { str } from '../format';
/**
* Who did what on this file, and what the agent actually saw.
@@ -36,171 +36,14 @@ import { dateTime, money, moneyShort, num, pct, ratio, str } from '../format';
* 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 ? (
-
- );
- })}
-
- )}
-
-
- {/* ---------------- 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}`}
- >
-
- );
-}
diff --git a/src/components/InstanceTrace.tsx b/src/components/InstanceTrace.tsx
new file mode 100644
index 0000000..a5d63a8
--- /dev/null
+++ b/src/components/InstanceTrace.tsx
@@ -0,0 +1,258 @@
+import { useState } from 'react';
+import { Bot, ChevronRight, Clock, User, Wrench } from 'lucide-react';
+import { Badge } from './core';
+import type { AuditEntry, Row } from '../api/types';
+import { POLICY } from '../api/config';
+import { moneyShort, num, pct, ratio, str } from '../format';
+
+/**
+ * The whole life of one file, in order, with every step attributed.
+ *
+ * WHAT THIS IS FOR. Anyone can be told "an agent assessed this". A trace lets
+ * them check it: each step names who performed it, when, how long after the
+ * previous one, and exactly what they submitted. The agent's step sits in the
+ * same list as the humans', on the same terms, because that is the honest
+ * claim — it is a member of staff on a chain of custody, not a black box
+ * bolted to the side.
+ *
+ * Every row is read from the workflow's own audit trail (`/view/audit`).
+ * "Credit Assessor AI" appears because the platform recorded the agent
+ * submitting under its own JWT. Nothing on this screen is asserted by the
+ * frontend.
+ *
+ * THE GAP THAT TELLS THE STORY is the elapsed time on the agent's row. On a
+ * typical file the humans act seconds apart because a person is clicking; the
+ * agent's row shows a minute and a half of unattended work. That number is the
+ * demo, and it is why elapsed time is shown at all.
+ *
+ * What is NOT here: the agent's internal steps — individual model calls, the
+ * SQL each tool ran, tokens, cost. That lives in
+ * aiemployee.tbl_ai_trace_events and needs either an org-scoped token or the
+ * rdbms bridge in 10_trace_view.sql. The tool RESULTS are shown, attributed;
+ * the raw payloads are not invented.
+ */
+
+const AI_ROLES = ['ai_credit_assessor', 'ai_loan_advisor'];
+
+function isAgent(e: AuditEntry): boolean {
+ return (e.user_roles ?? []).some((r) => AI_ROLES.includes(r));
+}
+
+/** Commit rows only. An activity's commit row carries the state it reached;
+ * everything else in the trail is engine bookkeeping. */
+function steps(entries: AuditEntry[]): AuditEntry[] {
+ return entries
+ .filter((e) => str(e.execution_state).startsWith('hdfc-state-'))
+ .slice()
+ .sort((a, b) => String(a.created_at ?? '').localeCompare(String(b.created_at ?? '')));
+}
+
+function gap(prev?: string, cur?: string): string | null {
+ if (!prev || !cur) return null;
+ const ms = new Date(cur).getTime() - new Date(prev).getTime();
+ if (!Number.isFinite(ms) || ms < 0) return null;
+ if (ms < 1000) return null;
+ const s = Math.round(ms / 1000);
+ if (s < 90) return `+${s}s`;
+ const m = Math.floor(s / 60);
+ return `+${m}m ${s % 60}s`;
+}
+
+function time(v?: string): string {
+ if (!v) return '';
+ const d = new Date(v);
+ return Number.isNaN(d.getTime())
+ ? v
+ : d.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
+}
+
+export function InstanceTrace({ row, audit }: { row: Row; audit: AuditEntry[] }) {
+ const list = steps(audit);
+ const [open, setOpen] = useState(null);
+ const agentCount = list.filter(isAgent).length;
+
+ if (list.length === 0) {
+ return
No completed steps recorded yet.
;
+ }
+
+ return (
+
+
+
+
Chain of custody
+
+ Every step on this file, in order, from the workflow's audit trail
+
+
+
+ {agentCount} by an agent
+ {list.length - agentCount} by people
+