Skip to content
+

Chat - Step Tracking

Track and display multi-step agent progress with visual delimiters in the message stream.

Agentic AI workflows often involve multiple processing steps: reasoning, tool calls, and a final text answer. Step tracking lets you visually delimit these phases in the message stream so users can follow the agent's progress.

Step boundary chunks

The streaming protocol provides two chunks for step boundaries:

Chunk type Description
start-step Begin a new processing step
finish-step End the current step
interface ChatStartStepChunk {
  type: 'start-step';
}

interface ChatFinishStepChunk {
  type: 'finish-step';
}

Step start part structure

When a start-step chunk arrives, the runtime creates a ChatStepStartMessagePart on the assistant message:

interface ChatStepStartMessagePart {
  type: 'step-start';
}

The finish-step chunk signals the end of the current step but does not create a separate message part—it serves as a boundary marker in the stream.

Streaming example

A typical agentic loop might produce multiple steps, each containing reasoning, tool calls, or text:

const adapter: ChatAdapter = {
  async sendMessage({ message }) {
    return new ReadableStream({
      start(controller) {
        controller.enqueue({ type: 'start', messageId: 'msg-1' });

        // Step 1: Search for information
        controller.enqueue({ type: 'start-step' });
        controller.enqueue({
          type: 'tool-input-start',
          toolCallId: 'call-1',
          toolName: 'search',
        });
        controller.enqueue({
          type: 'tool-input-available',
          toolCallId: 'call-1',
          toolName: 'search',
          input: { query: 'MUI X Chat documentation' },
        });
        controller.enqueue({
          type: 'tool-output-available',
          toolCallId: 'call-1',
          output: { results: ['…'] },
        });
        controller.enqueue({ type: 'finish-step' });

        // Step 2: Analyze results
        controller.enqueue({ type: 'start-step' });
        controller.enqueue({
          type: 'tool-input-start',
          toolCallId: 'call-2',
          toolName: 'analyze',
        });
        controller.enqueue({
          type: 'tool-input-available',
          toolCallId: 'call-2',
          toolName: 'analyze',
          input: { data: '…' },
        });
        controller.enqueue({
          type: 'tool-output-available',
          toolCallId: 'call-2',
          output: { summary: '…' },
        });
        controller.enqueue({ type: 'finish-step' });

        // Step 3: Final answer
        controller.enqueue({ type: 'start-step' });
        controller.enqueue({ type: 'text-start', id: 'text-1' });
        controller.enqueue({
          type: 'text-delta',
          id: 'text-1',
          delta: 'Based on my research, here is the answer…',
        });
        controller.enqueue({ type: 'text-end', id: 'text-1' });
        controller.enqueue({ type: 'finish-step' });

        controller.enqueue({ type: 'finish', messageId: 'msg-1' });
        controller.close();
      },
    });
  },
};

Displaying step progress

The step-start parts act as delimiters in the message's parts array. Render them as visual separators, progress indicators, or collapsible sections.

Default rendering

By default, step-start parts render as an unstyled <div role="separator" />. This keeps the accessible structure of the message intact but is visually invisible. Register a partRenderers['step-start'] entry to make steps visible.

User

Find and summarize the step tracking docs.

Assistant
search
Tool called:{
{
  "query": "MUI X Chat documentation"
}
Tool result:{
{
  "results": [
    "Step tracking guide"
  ]
}
analyze
Tool called:{
{
  "data": "Step tracking guide"
}
Tool result:{
{
  "summary": "Use step-start parts as delimiters"
}

Step tracking delimits each phase with step-start parts.

Step delimiter renderer

Register a custom renderer for step-start parts (see Custom parts for the full ChatPartRendererMap API):

const renderers: ChatPartRendererMap = {
  'step-start': ({ index, message }) => {
    const stepNumber = message.parts
      .slice(0, index + 1)
      .filter((part) => part.type === 'step-start').length;
    return (
      <div
        role="separator"
        style={{
          display: 'flex',
          alignItems: 'center',
          gap: 8,
          margin: '8px 0',
          color: 'gray',
          fontSize: '0.8em',
        }}
      >
        <div style={{ flex: 1, height: 1, background: 'lightgray' }} />
        <span>Step {stepNumber}</span>
        <div style={{ flex: 1, height: 1, background: 'lightgray' }} />
      </div>
    );
  },
};

<ChatProvider adapter={adapter} partRenderers={renderers}>
  <MyChat />
</ChatProvider>;

The partRenderers prop is accepted by both ChatProvider and ChatBox. Pass it to whichever component you mount.

Step progress with Material UI

For a styled delimiter, use Material UI components:

import Divider from '@mui/material/Divider';
import Typography from '@mui/material/Typography';

const renderers: ChatPartRendererMap = {
  'step-start': ({ index, message }) => {
    const stepNumber = message.parts
      .slice(0, index + 1)
      .filter((part) => part.type === 'step-start').length;
    return (
      <Divider sx={{ my: 1 }} role="separator">
        <Typography variant="caption" color="text.secondary">
          Step {stepNumber}
        </Typography>
      </Divider>
    );
  },
};

For an alternative approach that renders an animated, live-updating task list from a custom tool part instead of step-start delimiters, see the Plan and task example.

Grouping parts by step

After streaming, the message's parts array contains step-start entries interleaved with the content parts for each step:

// Example parts array after streaming
[
  { type: 'step-start' }, // Step 1 delimiter
  {
    type: 'tool',
    toolInvocation: {
      /* ... */
    },
  }, // Step 1 content
  { type: 'step-start' }, // Step 2 delimiter
  {
    type: 'tool',
    toolInvocation: {
      /* ... */
    },
  }, // Step 2 content
  { type: 'step-start' }, // Step 3 delimiter
  { type: 'text', text: 'Final answer…' }, // Step 3 content
];

Group the parts into per-step sections by treating each step-start as a new group boundary:

function groupPartsByStep(parts: ChatMessagePart[]): ChatMessagePart[][] {
  const groups: ChatMessagePart[][] = [];
  for (const part of parts) {
    if (part.type === 'step-start' || groups.length === 0) {
      groups.push([]);
    }
    if (part.type !== 'step-start') {
      groups[groups.length - 1].push(part);
    }
  }
  return groups;
}

Each group can then be rendered as its own section, for example inside a Material UI Accordion for collapsible steps.

While the assistant message is still streaming (message.status === 'streaming'), the last step-start part marks the step currently in progress. Use this to render a spinner or "running" state on the final group.

Steps with reasoning and tool calls

Steps compose naturally with reasoning and tool calling. A single step can contain reasoning, one or more tool invocations, and text:

// Step with reasoning + tool call
controller.enqueue({ type: 'start-step' });
controller.enqueue({ type: 'reasoning-start', id: 'r-1' });
controller.enqueue({
  type: 'reasoning-delta',
  id: 'r-1',
  delta: 'I need to look up the user data first.',
});
controller.enqueue({ type: 'reasoning-end', id: 'r-1' });
controller.enqueue({
  type: 'tool-input-start',
  toolCallId: 'call-1',
  toolName: 'get_user',
});
controller.enqueue({
  type: 'tool-input-available',
  toolCallId: 'call-1',
  toolName: 'get_user',
  input: { userId: '123' },
});
controller.enqueue({
  type: 'tool-output-available',
  toolCallId: 'call-1',
  output: { name: 'Alice', email: 'alice@example.com' },
});
controller.enqueue({ type: 'finish-step' });

See also

  • See Tool calling for the tool invocation lifecycle within steps.
  • See Reasoning for displaying LLM thinking traces.
  • See Streaming for the full chunk protocol reference including step boundary chunks.
  • See Tool approval for human-in-the-loop checkpoints within agent steps.
  • See the Plan and task example for agent progress rendered from a custom tool part, an alternative to step-start delimiters.

API

See the documentation below for a complete reference to all of the props and classes available to the components mentioned here.