AI Agents — A Graph, Not a Model
It Is Working Fine but Looks Frozen
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
- In /root/work/agstream/stream.py, create
State, four nodes (split,keypoints,actions,compose),build_graph(),start(text)andrun_values(text). Aftersplit,keypointsandactionsrun side by side, and both converge oncompose. - Add
run_updates(text)so that it returns the events ofstream_mode="updates"as they are, as a list. - Add
REDUCERS,rebuild(text)andrebuild_report(text)to rebuild the last state by gathering only the updates, and to compare whether it is the same as the last ofvalues. - Add
run_debug(text)so that it returns{수퍼스텝번호: [노드이름...정렬됨]}(superstep number mapped to the sorted list of node names). - Add
run_multi(text)andmode_sequence(text)to handle the result ofstream_mode=["updates", "values"]. - Make
composetakewriter: StreamWriterand send out two pieces, and addrun_custom(text). - Add
compare_invoke(text)so that it returns in one go whether the last state you streamed is the same as the answer ofinvoke, which node arrives first, and how many events there are. - 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
- Execution contract: the grader imports
/root/work/agstream/stream.pyas a Python module and uses the names listed above directly. It is not run as a script. - State keys:
text,lines,points,todos,summaryandtrace. Onlytraceuses the appending reducer and the rest overwrite. A node name and a state key must not overlap — if they overlap, compiling givesValueError: 'x' is already being used as a state key. - What the nodes do:
splitsplitstextinto lines, discards blank lines and puts them inlines.keypointsputs the lines containing"결정"(the Korean word for "decision") intopoints, andactionsputs the lines containing"하기로"(the Korean phrase for "decided to do") intotodos.composeputs"결정 N건 · 할 일 M건"intosummary(the Korean text means "N decisions · M to-dos"). All four nodes write their own names totrace. start(text)returns{"text": text, "trace": []}.run_values,run_updates,run_multiandrun_customreturn whatstream(...)gave as it is, unprocessed, as a list.rebuildstarts fromstart(text)and returns the dictionary obtained by applying theupdatesevents in order.REDUCERSis a table listing the appending keys, like{"trace": "append"}.- Answer of
rebuild_report(text):{"same": 참거짓, "stream_order": [...], "merged_order": [...]}(the placeholder stands for a boolean).stream_orderis thetraceof the reconstructed state, andmerged_orderis thetraceof the last event ofvalues. The two are not the same — look for yourself and write down why they differ. run_debuglooks only at those withevent["type"] == "task"and gathersevent["step"]andevent["payload"]["name"]. Sort the values.mode_sequence(text)pulls out only the mode names, in order, from the tuples ofrun_multi.- Answer of
compare_invoke(text):{"same": 참거짓, "first_node": 문자열, "values_events": 정수, "updates_events": 정수}(the placeholders stand for a boolean, a string and integers). - /root/work/agstream/minutes.txt in step 8 is the meeting minutes the report was built from. The grader reads that file and measures again with the same input, so if you change the content after building the report, the numbers will not match.
- This Pod has no internet. langgraph 0.2.60 is already installed.
- Official docs: Streaming · Graph API overview · Types reference
- Common mistakes: believing
updatesis per superstep, overwriting appending keys when reconstructing, putting progress indicators in the state, and returning the result ofstream()after processing it (the grader looks at the original shape).
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.