TT Lab
Get started
Learn Learning paths Courses

AI Agents — A Graph, Not a Model

It Is Working Fine but Looks Frozen

Continue in TT Lab

Goal

You stream the same graph in five ways and count directly what comes out and when. You gather only the updates to rebuild the last state, and compare whether the last state you streamed is the same as the answer of invoke.

Why it matters

What you stream out of an agent is not only tokens. In a graph that calls the model once at the end, even if you attach token streaming, the silence before it stays as it is. What the user needs is "which step is it now" and "what has newly been decided", and that is something the graph knows, not the model. The unit differs per mode. updates is per node, so even if two nodes running side by side are in the same superstep, the events come out separately, and values is per superstep, so it comes out once after the results of the two are merged. debug also tells you the superstep number. If you do not know this difference, you misread "why does it come out twice" as a bug. To rebuild state from updates alone, you have to know the reducers. The job the reducer did inside the graph you have to do yourself outside, and if you overwrite an appending key, only one footprint remains. And progress indicators you do not want to leave in the state are sent out with custom. If you put them in the state, they ride in the checkpoint and old indicators come back to life when you resume. The grader does not trust the explanations you wrote. It actually imports your module, runs it with a different set of meeting minutes each time, and compares the number, order and shape of the events with values the grader counted separately. It does not measure time.

Steps

  1. In /root/work/agstream/stream.py, create State, four nodes (split, keypoints, actions, compose), build_graph(), start(text) and run_values(text). After split, keypoints and actions run side by side, and both converge on compose.
  2. Add run_updates(text) so that it returns the events of stream_mode="updates" as they are, as a list.
  3. Add REDUCERS, rebuild(text) and rebuild_report(text) to rebuild the last state by gathering only the updates, and to compare whether it is the same as the last of values.
  4. Add run_debug(text) so that it returns {수퍼스텝번호: [노드이름...정렬됨]} (superstep number mapped to the sorted list of node names).
  5. Add run_multi(text) and mode_sequence(text) to handle the result of stream_mode=["updates", "values"].
  6. Make compose take writer: StreamWriter and send out two pieces, and add run_custom(text).
  7. Add compare_invoke(text) so that it returns in one go whether the last state you streamed is the same as the answer of invoke, which node arrives first, and how many events there are.
  8. Save the meeting minutes to measure in /root/work/agstream/minutes.txt, measure with them, and record the results in /root/work/agstream/stream_report.json and /root/work/agstream/stream_report.md.

Notes

The whole state comes out at every step

In /root/work/agstream/stream.py, create State, four nodes, build_graph(), start(text) and run_values(text). After split, keypoints and actions run side by side and both converge on compose.

You can just return what stream(입력, stream_mode="values") gives, as it is, as a list (the placeholder stands for the input). Confirm with your own eyes that the input state comes out once at the very front. The results of the two nodes running side by side come out as one event after being merged.

Updates come one per node

Add run_updates(text) so that it returns the events of stream_mode="updates" as they are, unprocessed, as a list.

One event is {노드이름: 갱신} (node name mapped to its update). Even if two nodes running side by side are in the same superstep, the events come out separately — so there are more events than with values. Count for yourself how many come out.

Rebuild the state from updates alone

Add REDUCERS = {"trace": "append"} and rebuild(text). Start from start(text) and return the dictionary obtained by applying the updates events in order.

The job the reducer did inside the graph you have to do yourself outside. If you overwrite everything, only the last entry remains in the appending key. But even if you imitate the reducer correctly, it still does not become completely identical — print the two traces side by side and confirm for yourself where they differ and why. Both always give the same answer, but they differ from each other.

What runs together at which step

Add run_debug(text) so that it returns {수퍼스텝번호: [노드이름...정렬됨]} (superstep number mapped to the sorted list of node names). Look only at those with event["type"] == "task".

The debug mode emits two events per node, task and task_result. The step on the task side is the superstep number, and payload["name"] is the node name. Confirm that nodes running side by side have the same number — that is why you use this mode.

Listen to several modes together

Add run_multi(text) and mode_sequence(text). If you give stream_mode=["updates", "values"], (모드이름, 값) tuples come out (mode name, value).

If you give the modes as a list, each event is prefixed with which mode it belongs to. See for yourself in what order the events of the two modes come out mixed — it is not that one mode is given in full and then the other. mode_sequence shows that order by pulling out just the names.

Show it without leaving it in the state

Make compose take writer: StreamWriter as its second argument and send out {"stage": "compose", "points": 개수} and then {"stage": "compose", "todos": 개수} in turn (the placeholders stand for counts), and add run_custom(text).

If you name the second argument of the node function writer and annotate it with StreamWriter, LangGraph fills it in. What you put in with writer(...) does not remain in the state and goes only to the listening side — if you put progress indicators in the state, they ride in the checkpoint and come back to life when you resume.

The last one you streamed and the answer you got all at once

Add compare_invoke(text) so that it returns {"same": 참거짓, "first_node": 문자열, "values_events": 정수, "updates_events": 정수} (the placeholders stand for a boolean, a string and integers).

If the last event of values is the same as the answer of invoke(), then there is no reason to run twice, such as drawing the screen by streaming and recording separately with invoke. first_node is the node name in the first event of updates — it tells you what you can show the user first.

Organize what to show and when

Save the meeting minutes to measure in /root/work/agstream/minutes.txt (at least one line each of decisions and to-dos, and also one blank line). Measure with those minutes, and write values_events, updates_events, first_node, supersteps, mode_sequence, custom_chunks, rebuild_same, stream_order and merged_order in /root/work/agstream/stream_report.json, and write /root/work/agstream/stream_report.md in four sections: ## 다섯 가지 방식이 각각 무엇을 주나 (what each of the five ways gives), ## 갱신만으로 상태를 다시 세우려면 (to rebuild state from updates alone), ## 화면에 먼저 보여 줄 수 있는 것 (what you can show on screen first) and ## 현장에서 무엇을 고르나 (what to choose in the field).

All of these are values obtained by running with the meeting minutes you saved. supersteps holds the answer of run_debug converted to string keys, and rebuild_same, stream_order and merged_order are the answer of rebuild_report as it is. Do not make up numbers; count them and put them in.