If an AG-UI backend sends a deployment checklist as structured state, do not wait for the assistant to write “the security check passed” and then parse that sentence. Reduce the event into frontend state and render the checklist from that state.
Assistant prose is for explanation. It is not a reliable UI contract. Wording can change, arrive in chunks, be translated, or omit a detail. A component controlled by that wording will eventually show the wrong state.
Keep messages, run lifecycle, and application state separate. This reducer uses a small subset of AG-UI events to show the shape:
type AgentApplicationState = {
deploymentChecks: Array<{
name: string;
status: "pending" | "passed" | "failed";
}>;
};
type AssistantMessage = {
id: string;
text: string;
complete: boolean;
};
type InterruptSummary = {
id: string;
reason: string;
message?: string;
};
type UiState = {
runStatus: "idle" | "running" | "completed" | "interrupted" | "failed";
assistantMessages: AssistantMessage[];
application: AgentApplicationState;
pendingInterrupts: InterruptSummary[];
errorMessage?: string;
};
type UiEvent =
| { type: "RUN_STARTED" }
| {
type: "RUN_FINISHED";
outcome?:
| { type: "success" }
| { type: "interrupt"; interrupts: InterruptSummary[] };
}
| { type: "RUN_ERROR"; message: string }
| { type: "TEXT_MESSAGE_START"; messageId: string }
| { type: "TEXT_MESSAGE_CONTENT"; messageId: string; delta: string }
| { type: "TEXT_MESSAGE_END"; messageId: string }
| { type: "STATE_SNAPSHOT"; snapshot: AgentApplicationState };
function reduce(state: UiState, event: UiEvent): UiState {
switch (event.type) {
case "RUN_STARTED":
return {
...state,
runStatus: "running",
pendingInterrupts: [],
errorMessage: undefined,
};
case "RUN_FINISHED":
return event.outcome?.type === "interrupt"
? {
...state,
runStatus: "interrupted",
pendingInterrupts: event.outcome.interrupts,
}
: { ...state, runStatus: "completed", pendingInterrupts: [] };
case "RUN_ERROR":
return { ...state, runStatus: "failed", errorMessage: event.message };
case "TEXT_MESSAGE_START":
return {
...state,
assistantMessages: [
...state.assistantMessages,
{ id: event.messageId, text: "", complete: false },
],
};
case "TEXT_MESSAGE_CONTENT":
return {
...state,
assistantMessages: state.assistantMessages.map(message =>
message.id === event.messageId
? { ...message, text: message.text + event.delta }
: message),
};
case "TEXT_MESSAGE_END":
return {
...state,
assistantMessages: state.assistantMessages.map(message =>
message.id === event.messageId
? { ...message, complete: true }
: message),
};
case "STATE_SNAPSHOT":
return { ...state, application: event.snapshot };
}
}
The checklist reads the structured state directly:
<DeploymentChecklist checks={state.application.deploymentChecks} />
The assistant may still say that a check passed. That text helps the user understand what happened, but it does not decide which icon the component renders.
STATE_SNAPSHOT represents the complete shared state. Replace the previous application state instead of merging fields that the new snapshot omitted.
The example stops at snapshots to keep the reducer readable. A production reducer should also handle STATE_DELTA. Validate the JSON Patch, apply its operations in order, then validate the resulting application state before a component reads it. A valid patch can still produce a state that violates your application schema.
This gives you a straightforward reducer test: pass in a STATE_SNAPSHOT and assert that the checklist changes. The test does not need a model response with exactly the right sentence.
Be careful with predictive state updates. Microsoft Agent Framework can emit STATE_DELTA events while the model is still generating tool arguments, before the tool runs. Those updates are optimistic. In a deployment UI, a predicted green check must not imply that the security check has succeeded.
AG-UI state tells the live UI what to show. It does not prove what the business system committed. If you enable predictive updates, render them as provisional until the final outcome replaces or discards them. When correctness depends on committed state, confirm it against the application backend before reporting success.