Trace Patterns Since v4.1.0

Agent activity patterns assembled from Trace and Angular Material, shown in an assistant message as a thread would render them.

None of these patterns is a component. Each one is Trace rows with application markup: icons and statuses mapped from your agent's state, with code blocks, chips or alerts where the detail calls for them. Copy the markup and bind it to your own data.

Agent Steps

One collapsible per reasoning step, with the step number in the icon slot. While the model streams, the last line takes running.

Should we chase our largest client about their invoices?

Yes, but gently. Two invoices are overdue, and the client usually pays within a week of a single reminder.

<div buiTrace>
@for (step of steps(); track step.id; let index = $index, last = $last) {
<div buiTraceCollapsible>
<button buiTraceCollapsibleTrigger>
<span buiTraceLine [status]="last && streaming() ? 'running' : undefined">
<span buiTraceIcon class="text-xs tabular-nums">{{ index + 1 }}</span>
<span buiTraceTitle>{{ step.title }}</span>
</span>
</button>
<ng-template buiTraceCollapsiblePanel>
<div buiTraceCollapsibleContent>{{ step.details }}</div>
</ng-template>
</div>
}
</div>

Checkpoint

A row after an assistant turn marks a point to return to: a bookmark line and a tiny Restore button between two dividers. Restore drops every message after that turn.

Rename the Button component to ActionButton across the app.
Renamed Button to ActionButton in 14 files, including the selector and every import. The test suite passes.
Button renamed
Now update the docs to match.
Updated the component guide and replaced the Button examples on three docs pages.
@for (message of messages(); track message.id; let index = $index) {
<div buiMessage [align]="message.from === 'user' ? 'end' : 'start'">
<div buiMessageContent>
@if (message.from === 'user') {
<div buiMessageBubble>{{ message.text }}</div>
} @else {
{{ message.text }}
}
</div>
</div>
@if (message.checkpoint) {
<div class="flex items-center gap-3">
<mat-divider class="flex-1" />
<span buiTraceLine>
<mat-icon buiTraceIcon svgIcon="bookmark" />
<span buiTraceTitle>{{ message.checkpoint }}</span>
</span>
<button matButton class="tiny" (click)="restore(index)">Restore</button>
<mat-divider class="flex-1" />
</div>
}
}

Citations

A collapsible line counts the sources an answer drew on. Each source is a link wrapping a line, with a globe icon and the domain as the description.

Can we charge a late fee on overdue invoices?

Yes, if your payment terms state the fee up front. Most businesses charge 1 to 1.5% per month on the overdue balance.

<div buiTrace>
<div buiTraceCollapsible>
<button buiTraceCollapsibleTrigger>
<span buiTraceLine>
<mat-icon buiTraceIcon svgIcon="book-open" />
<span buiTraceTitle>Used {{ sources().length }} sources</span>
</span>
</button>
<ng-template buiTraceCollapsiblePanel>
<div buiTraceCollapsibleContent>
<div buiTrace>
@for (source of sources(); track source.url) {
<a target="_blank" [href]="source.url">
<span buiTraceLine>
<mat-icon buiTraceIcon svgIcon="globe" />
<span buiTraceTitle>{{ source.title }}</span>
<span buiTraceDescription>{{ source.domain }}</span>
</span>
</a>
}
</div>
</div>
</ng-template>
</div>
</div>

Command Output

A collapsible line with the command as a monospace title and the exit code as the description. The output streams into a code block inside.

Build the billing package.
<div buiTrace>
<div buiTraceCollapsible [expanded]="true">
<button buiTraceCollapsibleTrigger>
<span buiTraceLine [status]="running() ? 'running' : undefined">
<mat-icon buiTraceIcon svgIcon="square-terminal" />
<span buiTraceTitle class="font-mono text-base">{{ command }}</span>
@if (!running()) {
<span buiTraceDescription>exit {{ exitCode() }}</span>
}
</span>
</button>
<ng-template buiTraceCollapsiblePanel>
<div buiTraceCollapsibleContent>
<div buiCodeBlock>
<pre buiCodeBlockContent lang="ansi" [code]="output()"></pre>
</div>
</div>
</ng-template>
</div>
</div>

Confirmation

An alert outside the trace asks before a tool runs, with the question, the input and Approve and Deny buttons. The answer replaces it with a tool call row.

Split invoice lines into their own table on staging.
Wrote migration 00422 tables
Apply migration 0042 to staging?
0042_split_invoice_lines on staging-eu touches 2 tables and 14,207 rows.
<div buiTrace>
@if (answer() === 'approved') {
<span buiTraceLine>
<mat-icon buiTraceIcon svgIcon="check" />
<span buiTraceTitle>Applied migration 0042</span>
</span>
} @else if (answer() === 'denied') {
<span buiTraceLine status="pending">
<mat-icon buiTraceIcon svgIcon="ban" />
<span buiTraceTitle>Migration 0042 denied</span>
</span>
}
</div>

@if (!answer()) {
<div buiAlert>
<mat-icon buiAlertIcon svgIcon="database" />
<div buiAlertContent>
<div buiAlertTitle>Apply migration 0042 to staging?</div>
<div buiAlertDescription>Touches 2 tables and 14,207 rows.</div>
<div buiAlertActions>
<button matButton class="primary small" (click)="answer.set('approved')">
Approve
</button>
<button matButton class="small" (click)="answer.set('denied')">
Deny
</button>
</div>
</div>
</div>
}

Delegated Agent

Work handed to a subagent is one collapsible line whose content is a nested trace of the subagent's own tool calls.

How reliable a payer is our largest client?
Searched payments from the client 34 results Queried invoice due dates 34 rows Read the client contract 6 pages Compared paid and due dates 3 late

Very reliable. 31 of their last 34 invoices were paid on time, and the 3 late ones cleared within a week.

<div buiTrace>
<div buiTraceCollapsible>
<button buiTraceCollapsibleTrigger>
<span buiTraceLine>
<mat-icon buiTraceIcon svgIcon="bot" />
<span buiTraceTitle>Research agent</span>
<span buiTraceDescription>{{ calls().length }} tool calls</span>
</span>
</button>
<ng-template buiTraceCollapsiblePanel>
<div buiTraceCollapsibleContent>
<div buiTrace>
@for (call of calls(); track call.id) {
<span buiTraceLine>
<mat-icon buiTraceIcon [svgIcon]="call.icon" />
<span buiTraceTitle>{{ call.title }}</span>
<span buiTraceDescription>{{ call.description }}</span>
</span>
}
</div>
</div>
</ng-template>
</div>
</div>

Error Details

A collapsible error line holds the stack, either as a code block or as one line per frame with the location as the description.

Why did last night's reminders fail?
calculateLateFee src/billing/fees.ts:42:18 buildReminder src/billing/reminders.ts:87:24 ReminderJob.run src/jobs/reminder-job.ts:31:9 Scheduler.tick src/jobs/scheduler.ts:118:7

The reminder job reached an invoice without totals. calculateLateFee reads invoice.totals.total without checking that totals exists.

<div buiTrace>
<div buiTraceCollapsible>
<button buiTraceCollapsibleTrigger>
<span buiTraceLine status="error">
<mat-icon buiTraceIcon svgIcon="bug" />
<span buiTraceTitle>{{ error().message }}</span>
<span buiTraceDescription>{{ error().frames.length }} frames</span>
</span>
</button>
<ng-template buiTraceCollapsiblePanel>
<div buiTraceCollapsibleContent>
<div buiTrace>
@for (frame of error().frames; track frame.location) {
<span buiTraceLine>
<mat-icon buiTraceIcon svgIcon="file-code" />
<span buiTraceTitle class="font-mono text-base">{{ frame.name }}</span>
<span buiTraceDescription>{{ frame.location }}</span>
</span>
}
</div>
</div>
</ng-template>
</div>
</div>

File Changes

A collapsible line with the progress as the description and a chip set of the touched files inside.

Add our new logo to every billing template.

The new logo is in all five templates, sized to the existing header height.

<div buiTrace>
<div buiTraceCollapsible>
<button buiTraceCollapsibleTrigger>
<span buiTraceLine>
<mat-icon buiTraceIcon svgIcon="check" />
<span buiTraceTitle>Updated the billing templates</span>
<span buiTraceDescription>{{ done() }} of {{ files().length }} files</span>
</span>
</button>
<ng-template buiTraceCollapsiblePanel>
<div buiTraceCollapsibleContent>
<mat-chip-set>
@for (file of files(); track file) {
<mat-chip>{{ file }}</mat-chip>
}
</mat-chip-set>
</div>
</ng-template>
</div>
</div>

Job Queue

A collapsible line with the waiting count as the description, and the jobs inside as done, running and pending lines.

Run the end of month billing tasks.
Send the reminder for INV-1042 Send the reminder for INV-1057 Generate the October client statements Email the statements to each billing contact Sync payments with the ledger
<div buiTrace>
<div buiTraceCollapsible>
<button buiTraceCollapsibleTrigger>
<span buiTraceLine>
<mat-icon buiTraceIcon svgIcon="list-checks" />
<span buiTraceTitle>Billing queue</span>
<span buiTraceDescription>{{ queued() }} queued</span>
</span>
</button>
<ng-template buiTraceCollapsiblePanel>
<div buiTraceCollapsibleContent>
<div buiTrace>
<span buiTraceLine>
<mat-icon buiTraceIcon svgIcon="check" />
<span buiTraceTitle>Send the reminder for INV-1042</span>
</span>
<span buiTraceLine status="running">
<mat-progress-spinner buiTraceIcon mode="indeterminate" diameter="16" />
<span buiTraceTitle>Generate the October statement</span>
</span>
<span buiTraceLine status="pending">
<mat-icon buiTraceIcon svgIcon="clock" />
<span buiTraceTitle>Sync payments with the ledger</span>
</span>
</div>
</div>
</ng-template>
</div>
</div>

Run Plan

One line per step: a check for done, a spinner with running, a circle with pending, and a collapsible error line that explains a failure.

Export August invoices to the accounting system.
Export August invoices to CSV
Save the export to the shared drive instead Email the finance team a link
<div buiTrace>
<span buiTraceLine>
<mat-icon buiTraceIcon svgIcon="check" />
<span buiTraceTitle>Export August invoices to CSV</span>
</span>
<div buiTraceCollapsible>
<button buiTraceCollapsibleTrigger>
<span buiTraceLine status="error">
<mat-icon buiTraceIcon svgIcon="circle-x" />
<span buiTraceTitle>Upload the export</span>
<span buiTraceDescription>2 attempts</span>
</span>
</button>
<ng-template buiTraceCollapsiblePanel>
<div buiTraceCollapsibleContent>The API key expired on September 30.</div>
</ng-template>
</div>
<span buiTraceLine status="running">
<mat-progress-spinner buiTraceIcon mode="indeterminate" diameter="16" />
<span buiTraceTitle>Save the export to the shared drive</span>
</span>
<span buiTraceLine status="pending">
<mat-icon buiTraceIcon svgIcon="circle" />
<span buiTraceTitle>Email the finance team a link</span>
</span>
</div>

Test Run

One collapsible line per suite with its pass and fail counts. Failing tests are nested collapsible error lines with the assertion in a code block.

Run the billing tests.
<div buiTrace>
@for (suite of suites(); track suite.name) {
<div buiTraceCollapsible [expanded]="suite.failed > 0">
<button buiTraceCollapsibleTrigger>
<span buiTraceLine [status]="suite.failed ? 'error' : undefined">
<mat-icon buiTraceIcon [svgIcon]="suite.failed ? 'circle-x' : 'circle-check'" />
<span buiTraceTitle class="font-mono text-base">{{ suite.name }}</span>
<span buiTraceDescription>{{ suite.failed }} failed, {{ suite.passed }} passed</span>
</span>
</button>
<ng-template buiTraceCollapsiblePanel>
<div buiTraceCollapsibleContent>
<div buiTrace>
@for (test of suite.tests; track test.name) {
@if (test.failure) {
<div buiTraceCollapsible [expanded]="true">
<button buiTraceCollapsibleTrigger>
<span buiTraceLine status="error">
<mat-icon buiTraceIcon svgIcon="x" />
<span buiTraceTitle>{{ test.name }}</span>
</span>
</button>
<ng-template buiTraceCollapsiblePanel>
<div buiTraceCollapsibleContent>
<div buiCodeBlock>
<pre buiCodeBlockContent [code]="test.failure"></pre>
</div>
</div>
</ng-template>
</div>
} @else {
<span buiTraceLine>
<mat-icon buiTraceIcon svgIcon="check" />
<span buiTraceTitle>{{ test.name }}</span>
</span>
}
}
</div>
</div>
</ng-template>
</div>
}
</div>

Tool Call

The state of the tool part maps to the line's title, icon and status, and the call's input and output go in the content as code blocks.

Apply the invoice lines migration to staging.
Migration 0042 failedlock timeout
Input
{  "database": "staging-eu",  "migration": "0042_split_invoice_lines"}
Output
{  "applied": true,  "tables": 2,  "rows": 14207}

The first attempt timed out waiting for a table lock. The retry applied it, moving 14,207 rows into the new table.

<div buiTrace>
<div buiTraceCollapsible>
<button buiTraceCollapsibleTrigger>
<span buiTraceLine [status]="parts[call.state].status">
@if (call.state === 'running') {
<mat-progress-spinner buiTraceIcon mode="indeterminate" diameter="16" />
} @else {
<mat-icon buiTraceIcon [svgIcon]="parts[call.state].icon" />
}
<span buiTraceTitle>{{ parts[call.state].title }}</span>
</span>
</button>
<ng-template buiTraceCollapsiblePanel>
<div buiTraceCollapsibleContent>
<div buiCodeBlock>
<pre buiCodeBlockContent lang="json" [code]="call.input"></pre>
</div>
<div buiCodeBlock>
<pre buiCodeBlockContent lang="json" [code]="call.output"></pre>
</div>
</div>
</ng-template>
</div>
</div>

Keep the mapping from state to row in one place.

import { TraceStatus } from '@/bui/ui/trace';

type ToolState = 'running' | 'done' | 'failed' | 'denied';

const parts: Record<ToolState, { title: string; icon: string; status?: TraceStatus }> = {
running: { title: 'Applying migration 0042', icon: 'database', status: 'running' },
done: { title: 'Applied migration 0042', icon: 'check' },
failed: { title: 'Migration 0042 failed', icon: 'circle-x', status: 'error' },
denied: { title: 'Migration 0042 denied', icon: 'ban', status: 'pending' },
};