console: every step shows what it actually decided
The trail rendered twelve fields as prose and four as money. The other hundred-odd were invisible — so Capture Motor Risk, which writes engine capacity, fuel, year, NCB, previous insurer, policy number, IDV and the add-ons, appeared as a heading and a timestamp. The only way to see what an agent had decided was to open the Lead file and guess which values came from which step. Each entry now carries a folded "N fields written" list: every field that step committed, by label, with the values already shown above it (narrative, money, documents) excluded so nothing repeats. Folded because most steps write a dozen and the trail has to stay readable as a story. WHAT THIS IS NOT, and it matters: it is not the model's internal reasoning, and it is not its tool calls. Both ARE recorded — ai_reasoning, ai_confidence, ai_model and ai_tool_calls sit on the same audit row — but GetAuditLogsForApp selects eight columns and none of them are those, so an app JWT cannot read them at all. Surfacing them needs four columns added to that query and to ActivityLogEntry, which is a view-service change and an image build. This is the next best thing and arguably the more useful half: not what the agent thought, but what it committed, itemised against the step that committed it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cb94e2417f
commit
c7cd5602ae
@ -289,3 +289,62 @@
|
|||||||
/* A chat entry's dot takes the channel's colour, so a scan down the rail shows
|
/* A chat entry's dot takes the channel's colour, so a scan down the rail shows
|
||||||
where the conversation happened without reading a word. */
|
where the conversation happened without reading a word. */
|
||||||
.tl__item--chat .tl__dot { background: #0f7b45; border-color: #0f7b45; }
|
.tl__item--chat .tl__dot { background: #0f7b45; border-color: #0f7b45; }
|
||||||
|
|
||||||
|
/* ── what this step wrote ────────────────────────────────────────────────
|
||||||
|
Folded by default: most steps write a dozen fields and the trail has to
|
||||||
|
stay readable as a story. Opened, the entry becomes an itemised account of
|
||||||
|
what the agent decided — which is the closest an app JWT can get to its
|
||||||
|
reasoning, since ai_reasoning is on the row but not in the API. */
|
||||||
|
.tl__wrote { margin-top: 8px; }
|
||||||
|
|
||||||
|
.tl__wrote > summary {
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--zk-blue);
|
||||||
|
}
|
||||||
|
.tl__wrote > summary::-webkit-details-marker { display: none; }
|
||||||
|
.tl__wrote > summary::after {
|
||||||
|
content: '';
|
||||||
|
width: 5px; height: 5px;
|
||||||
|
border-right: 1.4px solid currentColor;
|
||||||
|
border-bottom: 1.4px solid currentColor;
|
||||||
|
transform: rotate(45deg) translate(-1px, -1px);
|
||||||
|
transition: transform var(--t-fast);
|
||||||
|
}
|
||||||
|
.tl__wrote[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
|
||||||
|
.tl__wrote > summary:hover { color: var(--zk-blue-dark); }
|
||||||
|
|
||||||
|
.tl__wrote dl {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--zk-line-soft);
|
||||||
|
border-radius: var(--r-sm, 6px);
|
||||||
|
background: var(--zk-tint);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl__wrote dl > div { display: flex; gap: 10px; align-items: baseline; }
|
||||||
|
|
||||||
|
.tl__wrote dt {
|
||||||
|
flex: none;
|
||||||
|
width: 42%;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--zk-grey);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl__wrote dd {
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--zk-ink);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|||||||
@ -42,6 +42,33 @@ const NARRATIVE = [
|
|||||||
|
|
||||||
const MONEY = new Set(['quoted_premium', 'commission_amount', 'sme_value_at_risk', 'motor_idv'])
|
const MONEY = new Set(['quoted_premium', 'commission_amount', 'sme_value_at_risk', 'motor_idv'])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields already shown elsewhere in the entry, so the "what it wrote" list
|
||||||
|
* does not repeat them: the narrative prose above it, the money figures below
|
||||||
|
* it, the document chips, and the platform's own bookkeeping.
|
||||||
|
*/
|
||||||
|
const SHOWN_ELSEWHERE = new Set([...NARRATIVE.map(([k]) => k), ...MONEY, '_system'])
|
||||||
|
|
||||||
|
/** A value as one short line. Long prose is already in the narrative block, so
|
||||||
|
* anything here is a field value, not a paragraph. */
|
||||||
|
function short(f) {
|
||||||
|
const v = f.value
|
||||||
|
if (v === null || v === undefined || v === '') return null
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
const named = v
|
||||||
|
.map((x) => (x && typeof x === 'object' ? (x.original_name || x.uuid || '') : x))
|
||||||
|
.filter(Boolean)
|
||||||
|
return named.length ? named.join(', ') : null
|
||||||
|
}
|
||||||
|
if (typeof v === 'object') return null
|
||||||
|
const t = String(v).trim()
|
||||||
|
if (!t) return null
|
||||||
|
// A value long enough to be prose belongs in the narrative block, not in a
|
||||||
|
// list of what changed — truncating it here would show half a sentence and
|
||||||
|
// teach the reader that this list is unreliable.
|
||||||
|
return t.length > 90 ? t.slice(0, 88) + '…' : t
|
||||||
|
}
|
||||||
|
|
||||||
/** Document slots, in the order they are worth reading. Labels are shorter than
|
/** Document slots, in the order they are worth reading. Labels are shorter than
|
||||||
* the workflow's own — "Registration Certificate (RC)" is a chip, not a form
|
* the workflow's own — "Registration Certificate (RC)" is a chip, not a form
|
||||||
* label — and a slot missing from here still renders under its server label. */
|
* label — and a slot missing from here still renders under its server label. */
|
||||||
@ -236,6 +263,26 @@ export default function Timeline({ rows, onOpenAgent, onOpenChat }) {
|
|||||||
figures: [...byBase]
|
figures: [...byBase]
|
||||||
.filter(([base, f]) => MONEY.has(base) && f.value)
|
.filter(([base, f]) => MONEY.has(base) && f.value)
|
||||||
.map(([base, f]) => [base.replace(/_/g, ' '), '₹' + Number(f.value).toLocaleString('en-IN')]),
|
.map(([base, f]) => [base.replace(/_/g, ' '), '₹' + Number(f.value).toLocaleString('en-IN')]),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EVERYTHING THIS STEP WROTE.
|
||||||
|
*
|
||||||
|
* Twelve fields were rendered as prose and four as money; the other
|
||||||
|
* hundred-odd were invisible. So Capture Motor Risk — which writes engine
|
||||||
|
* capacity, fuel, year, NCB, previous insurer, policy number, IDV and the
|
||||||
|
* add-ons — showed as a heading and a timestamp, and the only way to see
|
||||||
|
* what an agent had actually decided was to open the Lead file and guess
|
||||||
|
* which values came from that step.
|
||||||
|
*
|
||||||
|
* This is not the model's internal reasoning; that is recorded on the row
|
||||||
|
* (ai_reasoning, ai_tool_calls) and the audit endpoint does not return it.
|
||||||
|
* It is the next best and arguably more useful thing: the decision it
|
||||||
|
* committed, field by field, against the step that made it.
|
||||||
|
*/
|
||||||
|
wrote: [...byBase]
|
||||||
|
.filter(([base]) => !SHOWN_ELSEWHERE.has(base) && !base.startsWith('doc_'))
|
||||||
|
.map(([base, f]) => [f.label || base.replace(/_/g, ' '), short(f)])
|
||||||
|
.filter(([, v]) => v !== null),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -365,6 +412,20 @@ export default function Timeline({ rows, onOpenAgent, onOpenChat }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{/* Folded, because most steps write a lot and the trail has to
|
||||||
|
stay readable as a story. Open it and the step becomes an
|
||||||
|
itemised account of what it decided. */}
|
||||||
|
{it.wrote.length ? (
|
||||||
|
<details className="tl__wrote">
|
||||||
|
<summary>{it.wrote.length} field{it.wrote.length === 1 ? '' : 's'} written</summary>
|
||||||
|
<dl>
|
||||||
|
{it.wrote.map(([k, v]) => (
|
||||||
|
<div key={k}><dt>{k}</dt><dd>{v}</dd></div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</details>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user