Part 6 of 7 · 4 min read
Exercises
Each level has three exercises. Do the easy ones first.
Easy
Easy 1: Time the wait (15 minutes)
A reply has 800 tokens, a wait of 1.2 s before the first token and 25 ms per token. Using seen_times, compute the seconds until the first and last text appear, with and without streaming.
Done when: you have four numbers and a one-line conclusion.
Hint
without streaming, both times equal ttft + tokens x tpot.
Easy 2: Break the stream (20 minutes)
Use fake_stream(..., fail_after=n) for n = 0, 1 and 5. Print the partial text, the error and finished each time.
Done when: you can say what you would show the user in each case.
Easy 3: Which output may you act on? (15 minutes)
For each, say whether it is safe to use a partial output and why: (a) a chat reply on screen, (b) a JSON object stored in a database, (c) a shell command the agent will run, (d) a title for a draft.
Done when: each has a yes or no and a reason.
Medium
Medium 1: A stop word (45 minutes)
Write a should_cancel function that stops the stream when a line END appears by itself. Look only at complete lines, so ENDING does not count. Test with the marker split across pieces ("EN", "D\n"), with ENDING, and with a stream that never contains it.
Done when: all three cases pass, and the text you return stops before the marker.
Hint
should_cancel receives all the text so far. Split it on "\n" and ignore the last element, which may be an unfinished line.
Medium 2: Fuzz the parser (60 minutes)
Build a random list of 20 events (dicts and [DONE]), serialise it with to_sse, cut the text at 200 random places (use a seeded generator), feed the pieces to SSEParser and check that the output equals the input. Run it once with \n endings and once with \r\n.
Done when: the property holds for 200 random splits in both forms.
Hint
use random.Random(seed), sort the cut positions, and remember that to_sse uses \n, so build the \r\n version with replace.
Medium 3: Survive one bad event (60 minutes)
Make the parser report a payload that is not valid JSON as an {"error": ...} event and carry on with the events after it. Test one stream where the bad event is in the middle.
Done when: the events before and after the bad one are all returned, in order.
Hint
catch json.JSONDecodeError around json.loads inside the loop, and append the error event there.
Hard
Hard 1: Tool-call arguments in pieces (90 minutes)
Create a fake stream that sends a tool call as pieces: first the name, then argument fragments ({"ci, ty": "Pu, ne"}). Write an assembler that returns the finished call only after the last fragment, and refuses to return it if the stream ended early or the assembled text is not valid JSON.
Done when: four tests pass: complete, cut early, invalid JSON, and two calls in one stream.
Hint
keep a list of fragments. Join them and call json.loads only on the final event.
Hard 2: An event stream for an agent (120 minutes)
Implement a generator run_agent(steps) that yields events (tool_start, tool_end, text, done) for a fake two-tool agent, and a consumer that renders them as terminal lines. Add cancellation in the middle of a tool call. No later events may appear, and done is replaced by cancelled.
Done when: tests prove the order, the cancel behaviour, and that every tool_start has a matching tool_end or the run ends as cancelled.
Hard 3: An asynchronous client (120 minutes)
Write an async for version of consume with an asyncio.Event as the cancel signal and a timeout per event (no event for 2 simulated seconds means the stream has stalled). Test a normal, a cancelled and a stalled stream without real waiting.
Done when: all three outcomes are tested, and a stalled stream is reported as an error together with the partial text.
Hint
wrap anext(stream) in asyncio.wait_for. To avoid real waiting, make the fake stream await asyncio.sleep for a very short real time and set the timeout shorter than that, or pass the timeout as a parameter.