Streaming and Interruptions

By the Syncfusion DevTutorials team · Last updated

Show the customer what the helper is doing while it works, and stop cleanly when they press Stop. Learn streaming, progress chips, cancel checks and honest stopped messages.

Streaming and Interruptions

Asha writes, "Order 1044 is late. Please refund $40 and email me a receipt."

The screen shows a small note: "Looking up order 1044…"

Asha changes her mind. She wants a new jacket, not her money back. She presses Stop.

The screen freezes. Asha relaxes.

A moment later, the refund goes through anyway. Then the receipt email arrives.

The Stop button only stopped the words. It never stopped the work.

This lesson teaches how to show progress while the helper works. It also teaches how to stop for real.

Simple lesson: Stop must stop the actions, not only the screen.

A Real-World Example

Think about a pizza order on the phone.

  • The shop tells you, "Your pizza is in the oven." You know it is coming.
  • You call back and say, "Cancel it, please."
  • A good shop checks the kitchen before it bakes the next thing.
  • If the pizza is already baked, they tell you the truth: "It is already made. We can still stop the drinks."

A bad shop says "Cancelled!" and sends the driver anyway.

Shop Helper works the same way. It should show what it is doing. It should check for Stop before each job. And it should be honest about what is already done.

Simple lesson: cancel before the next job starts, and tell the truth about jobs already done.

What You Will Learn

In this lesson, you will learn:

  1. What streaming is, and why it makes a reply feel faster.
  2. How progress chips tell the customer what the helper is doing.
  3. Why the app must check for a cancel before every tool.
  4. How to write an honest message after a stop.
  5. Which moments are safe to stop, and which are not.

Words That Appear as They Are Written

A model writes its reply a few words at a time. Without help, the app waits for the whole reply. Then it shows it all at once.

That wait can feel long, because the screen stays blank.

Streaming = the app shows the reply a few words at a time, as the model writes them.

The total time stays the same. But the customer starts reading early, so it feels much faster.

Many real chat apps stream by default. In this lab, the words arrive in small groups on a clock, so you can watch each step.

Press Play and watch the reply grow.

  • The blank moment: the tools are done, but no words are on screen yet.
  • The first words: they show the moment they arrive.
  • The blinking bar: it means more words are coming.
  • The meter: it counts the words on screen against the full reply.

Simple lesson: streaming does not make the helper faster, but it makes the wait feel shorter.

Progress Chips

Streaming helps with the reply. But an agent also runs tools before it writes anything. During that time, there are no words to stream.

The screen still needs to say something, because a blank screen looks broken.

Progress chip = a small note on screen that names the step the helper is working on right now. A step may be a model call or a tool.

For example, the chip "Looking up order 1044…" shows while the lookup runs.

A good chip has three parts:

  • It appears when a step starts, not before.
  • It names the real step, in the customer's words.
  • It gets a tick when the step is done.

Press Play and watch the chips above the reply.

  • The first chip: it shows at once, because the first step (the model reading the message) starts at once.
  • Each new chip: it appears when a tool starts in the shop.
  • The ticks: a chip is ticked only when its tool has finished.
  • The shop log: it matches the ticked chips, line for line.

Write chips from the tool, not from the plan.

A chip should come from the tool that is really running. Do not show "Sending your refund…" just because the model might do it later.

If the chip and the shop log ever disagree, the customer was misled.

Simple lesson: a progress chip tells the truth about the step that is running right now.

The Stop Button

Now the customer can see the work. So they may want to stop it.

Cancel = the customer asks the helper to stop, and the app makes sure nothing new may start after Stop.

Here is the trap. Hiding the words is not enough. The real work happens behind the screen.

The model and the tools run inside the app, not on the screen. If the app never looks at the Stop button, the plan keeps going.

Watch the opening story again, in Asha's chat window.

Press Play and watch the shop log under the window.

  • The Stop press: the chips freeze and the screen looks calm.
  • The refund: it still goes through after Stop.
  • The email: it goes out too.
  • The red lines: each one is an action that started after Stop.

Now look at the same run on two tracks. The "Customer sees" track shows what Asha sees. The "App does in the shop" track shows what the app really does.

Press Play and watch the red Stop line.

  • Above the line: everything here was fine. The lookup started before Stop.
  • Below the line: the screen track goes quiet.
  • The shop track: it keeps going, and every row glows red.

Simple lesson: a Stop that only hides words is not a Stop.

Check Before Every Tool

The fix is a small habit in the app's loop.

Before each step, the app asks one question: "Did the customer press Stop?" If yes, it starts nothing new.

This matters most before a tool, because tools change the shop. A refund or an email cannot be taken back.

What about a step that is already running? Let it finish, because a shop action cannot be pulled back halfway. Then stop, and tell the customer honestly that it finished.

Press Play and compare it with the last animation.

  • The same Stop: Asha presses Stop at the same moment.
  • The lookup: it already started, so it finishes. A lookup changes nothing.
  • The check: before the next step, the app sees the Stop and halts.
  • The message: it says nothing changed, because the shop log is empty.

Stopping the words is not the same as cancelling.

Many apps stop the stream when you press Stop. That is good, but it is not enough.

The tool runner must also check for the cancel. Otherwise the shop keeps changing behind a frozen screen.

Simple lesson: check for a cancel before every tool, and let a running action finish cleanly.

When the Action Already Happened

Sometimes Stop comes too late. The refund already went out.

The helper cannot undo it by itself. A refund to undo is a new action, and it should be a new request.

So the helper must say what really happened. It reads the shop log and builds the message from it.

An honest stopped message has three parts:

  • What was done, such as a refund that already went out.
  • What was not done, such as an email that never started.
  • What the customer can do next, such as asking again for the missing part.

Press Play and read the yellow message in the chat window.

  • The refund: it finished before Stop, so it stays in the shop log.
  • The Stop: it lands while the model picks the next step.
  • The check: the email tool never starts.
  • The message: every sentence matches the shop log.

Never say "Nothing was changed" without looking.

That sentence feels safe. But if a refund went out, it is a lie.

Build the stopped message from the shop log every time. Do not write it by hand.

A Refund That Timed Out

Sometimes the problem is not Stop. The refund tool just takes too long, and the app gives up waiting.

That does not mean the refund failed. It may still have gone through on the payment side.

So before trying again, the app should do one of two things:

  • Check first: look the refund up, and only try again if it is not there.
  • Send a request ID: give each refund a unique ID. The payment service then ignores a repeat.

Either way, the customer is not paid twice. Observability, Cost and Reliability shows more about retries.

Simple lesson: after a stop, report what really happened, from the shop log. After a timeout, check before you try again.

Safe and Unsafe Moments to Stop

Not every moment is the same. Some moments leave the shop clean. Others leave a job half done.

Here is the rule in plain words.

  • Safe: before any change has started.
  • Safe: after a change has fully finished and is in the shop log.
  • Not safe: while a change is still in the middle.

A lookup or a model call changes nothing in the shop. So stopping during one of them is safe. Stopping in the middle of an address change is not, because the order might end up half moved.

That is why the app checks between steps. Between steps, every change is either done or not started.

Simple lesson: stop between steps, never in the middle of a change.

Where This Is Used

You see these ideas in many apps:

  • Chat assistants stream replies and show a Stop button.
  • Coding helpers show chips such as "Reading files…" or "Running tests…".
  • Shopping and travel helpers ask before a payment, and report what was booked if you cancel.

Real tools usually give you a way to cancel a model call. In this lab, the app checks for the cancel between steps, so you can see exactly where it lands.

Simple lesson: good live helpers show progress, check for Stop and report honestly.

In Code

Here is the same idea in TypeScript, with the Shop Helper names you know: order 1044, lookup_order, issue_refund, send_email.

The first cell streams. The helper is a generator that yields events. The chat window is a loop that prints each event the moment it arrives.

A real helper waits on the model, so it uses async function* and for await. This cell uses a plain generator, because the playground runs it in one go. The shape is the same.

Stream chips and text to the chat

type LiveEvent =
  | { kind: "chip"; text: string }
  | { kind: "text"; chunk: string }
  | { kind: "done"; reason: string };

// A scripted run: one lookup, then the reply arrives in pieces.
function* runShopHelper(orderId: string): Generator<LiveEvent> {
  yield { kind: "chip", text: `Looking up order ${orderId}…` };
  const status: string = "shipped, arrives Monday 3 Nov";
  yield { kind: "chip", text: "Writing the reply…" };
  for (const chunk of ["Your order ", orderId, " is ", status, "."]) {
    yield { kind: "text", chunk };
  }
  yield { kind: "done", reason: "final answer" };
}

// The consumer: handle each event the moment it arrives.
function showInChat(events: Generator<LiveEvent>): void {
  let reply: string = "";
  for (const ev of events) {
    if (ev.kind === "chip") console.log(`[chip] ${ev.text}`);
    if (ev.kind === "text") {
      reply += ev.chunk;
      console.log(`[text] "${reply}"`);
    }
    if (ev.kind === "done") console.log(`[done] ${ev.reason}`);
  }
}

showInChat(runShopHelper("1044"));

Expected output:

[chip] Looking up order 1044…
[chip] Writing the reply…
[text] "Your order "
[text] "Your order 1044"
[text] "Your order 1044 is "
[text] "Your order 1044 is shipped, arrives Monday 3 Nov"
[text] "Your order 1044 is shipped, arrives Monday 3 Nov."
[done] final answer
  • The event types: a chip, a piece of text, or done. The chat window only needs these three.
  • The generator: yield hands over one event and waits. Nothing waits for the full reply.
  • The consumer: the loop draws each event as soon as it comes, so the words grow on screen.

The second cell adds Stop. It uses its own StopFlag class, and checks it before every step. It runs twice.

Check for Stop before every tool

class StopFlag {
  stopped: boolean = false;
  stop(): void { this.stopped = true; }
}

interface Step { tool: string; chip: string; changesShop: boolean }

const plan: Step[] = [
  { tool: "lookup_order", chip: "Looking up order 1044…", changesShop: false },
  { tool: "issue_refund", chip: "Sending your $40 refund…", changesShop: true },
  { tool: "send_email", chip: "Emailing your receipt…", changesShop: true },
];

// stopDuring: the customer presses Stop while this tool runs.
function run(stopDuring: string): void {
  const flag: StopFlag = new StopFlag();
  const shopLog: string[] = [];
  for (const step of plan) {
    if (flag.stopped) break; // nothing new may start after Stop
    console.log(`  [chip] ${step.chip}`);
    if (step.tool === stopDuring) flag.stop(); // Stop arrives mid-step
    if (step.changesShop) shopLog.push(step.tool); // the running step still finishes
  }
  // Build the honest message from the shop log, not from the plan.
  const done: string[] = shopLog.map((t: string) =>
    t === "issue_refund" ? "your $40 refund went out" : "the receipt was sent");
  const skipped: string[] = plan
    .filter((s: Step) => s.changesShop && shopLog.indexOf(s.tool) < 0)
    .map((s: Step) => s.tool);
  console.log(`  Shop log: ${shopLog.length ? shopLog.join(", ") : "(empty)"}`);
  const doneText: string = done.length ? `Done: ${done.join(", ")}.` : "Nothing was changed.";
  console.log(`  Helper: Stopped. ${doneText} Not started: ${skipped.join(", ")}.`);
}

console.log("Run 1: Stop during the lookup");
run("lookup_order");
console.log("Run 2: Stop while the refund runs");
run("issue_refund");

Expected output:

Run 1: Stop during the lookup
  [chip] Looking up order 1044…
  Shop log: (empty)
  Helper: Stopped. Nothing was changed. Not started: issue_refund, send_email.
Run 2: Stop while the refund runs
  [chip] Looking up order 1044…
  [chip] Sending your $40 refund…
  Shop log: issue_refund
  Helper: Stopped. Done: your $40 refund went out. Not started: send_email.
  • The check: if (flag.stopped) break runs before each step, so nothing new may start after Stop.
  • Run 1: Stop comes during the lookup. The lookup changes nothing, and the refund never starts.
  • Run 2: Stop comes while the refund runs. The refund finishes, because a running change is never cut in half.
  • The message: it is built from the shop log, not from the plan. So it says honestly that the refund went out.

In a Real App

A real app sends the events to the browser, often with Server-Sent Events. It cancels with an AbortController. Its signal goes to the model call and to each tool call.

This sketch is simplified. It uses a small model interface instead of one vendor's SDK, because names differ between providers and SDK versions.

// Simplified. Your model SDK and web framework will name things differently.
interface ToolCall { name: string; input: unknown }
interface ModelTurn { text: string; toolCalls: ToolCall[] }
interface ModelClient {
  // Calls onText for each new piece of text while the model writes.
  stream(
    messages: unknown[],
    opts: { signal: AbortSignal; onText: (t: string) => void },
  ): Promise<ModelTurn>;
}
type RunTool = (call: ToolCall, signal: AbortSignal) => Promise<unknown>;
type Send = (event: "chip" | "text" | "done", data: string) => void;

const CHIPS: Record<string, string> = {
  lookup_order: "Looking up your order…",
  issue_refund: "Sending your refund…",
};

export async function runLive(
  model: ModelClient, runTool: RunTool, messages: unknown[],
  signal: AbortSignal, send: Send, stepLimit = 4,
) {
  for (let step = 0; step < stepLimit; step++) {
    if (signal.aborted) break; // Stop pressed: no new model call
    const onText = (t: string) => send("text", t);
    const turn = await model.stream(messages, { signal, onText });
    if (turn.toolCalls.length === 0) return send("done", "final answer");
    for (const call of turn.toolCalls) {
      if (signal.aborted) break; // check before every tool
      send("chip", CHIPS[call.name] ?? "Working…");
      const result = await runTool(call, signal); // a running change may still finish
      messages.push({ role: "tool", name: call.name, content: result });
    }
  }
  send("done", signal.aborted ? "stopped" : "step limit");
}

// Server-Sent Events: each event is "event: <name>" plus "data: <text>" and a blank line.
export function sseLine(event: string, data: string): string {
  return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
}
// In the browser: new EventSource(url).addEventListener("chip", ...).
// The Stop button calls the server, which calls controller.abort().

Two things to keep in mind. First, abort() may cancel a network call, but the payment service may already have the refund. So after Stop, read the shop log before you write the stopped message. Second, a refund tool should pass the signal on only where cutting it is safe. Many apps let a started change finish.

Simple lesson: yield events as they happen, check for Stop before every step, and build the stopped message from the shop log.

TL;DR

  1. Streaming = the app shows the reply a few words at a time, as it is written.
  2. Progress chip = a small note that names the step the helper is working on right now.
  3. Cancel = nothing new may start after Stop.
  4. Check before every tool = the app looks for a cancel before each action.
  5. A running action = let it finish, then say honestly that it finished.
  6. An honest stopped message = what was done, what was not, and what to do next.
  7. A safe moment to stop = before any change starts, or after it is fully done.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Stop only hides the words"The screen stopped, so the helper stopped."The app must check for a cancel before every tool.The refund goes out after the customer said stop.
Chips from the plan"Show all the chips at the start."Show a chip only when its tool really starts.The customer sees steps that never happened.
"Nothing was changed" by default"A stop means nothing happened."Build the message from the shop log.The customer is told a lie about their money.
Cutting an action in half"Stop means nothing may change after Stop."Nothing new may start after Stop. Let a running action finish, and say so.An order is left half moved, with no clear record.
No next step"Just say it stopped."Tell the customer what they can still ask for.The customer does not know how to get the rest.

Check Your Understanding

Exercises

What to Remember

  • Streaming shows words as they are written, so the wait feels shorter.
  • A progress chip names the tool that is running right now.
  • Cancel means nothing new may start after Stop. Check before every tool.
  • Let a running action finish, never cut it in half, and say honestly that it finished.
  • After a timeout, check (or use a request ID) before trying a refund again.
  • After a stop, say what was done and what was not, from the shop log.

Part 1 Recap

You have now built one agent that works. Here are the choices you met along the way:

  • Workflow or Agent? Pick the shape: one call, chain, router or agent.
  • The Agent Loop by Hand: set a step limit and a budget limit so the loop always ends.
  • Tool Design: write clear tool cards with required inputs and helpful errors.
  • Structured Output and Validation: give the reply an answer form, check it, and repair it.
  • State, Memory and Context Limits: choose what the helper remembers, and for how long.
  • Guardrails, Permissions and Human Approval: decide what needs a person's OK, and treat tool results as data.
  • Streaming and Interruptions: stream the reply, show chips, and stop for real.

Next: Multi-Agent: Subagents, Handoffs, Orchestrators, when one helper should become a small team, and how helpers hand work to each other.