128 """A single turn: user input, tool calls, sub-agents, final answer, errors."""
131 ts_first: Optional[str] =
None
132 ts_last: Optional[str] =
None
133 user_input: Optional[str] =
None
134 assistant_message: Optional[str] =
None
135 assistant_is_final: bool =
False
136 assistant_message_history: list[str] = Field(default_factory=list)
137 tool_calls: list[ToolCallNode] = Field(default_factory=list)
138 subagents: list[SubagentNode] = Field(default_factory=list)
139 errors: list[str] = Field(default_factory=list)
140 other_events: list[dict] = Field(default_factory=list)
163 """Assemble one :class:`TurnNode` from the events sharing *turn_id* (write order)."""
164 node =
TurnNode(turn_id=turn_id, ts_first=events[0].ts, ts_last=events[-1].ts)
165 tool_nodes: dict[str, ToolCallNode] = {}
166 proposed_q: list[Any] = []
167 sub_nodes: dict[str, SubagentNode] = {}
169 def _tool(tcid: str) -> ToolCallNode:
170 if tcid
not in tool_nodes:
172 tool_nodes[tcid] = tn
173 node.tool_calls.append(tn)
174 return tool_nodes[tcid]
179 if kind ==
"user_input_received":
180 node.user_input = getattr(p,
"text",
None)
181 elif kind ==
"assistant_message":
182 text = getattr(p,
"text",
"")
183 if getattr(p,
"is_final",
False):
184 node.assistant_message = text
185 node.assistant_is_final =
True
187 node.assistant_message_history.append(text)
188 elif kind ==
"tool_call_proposed":
190 elif kind ==
"tool_call_started":
191 tn = _tool(ev.tool_call_id
or getattr(p,
"tool_call_id",
"")
or "?")
192 tn.tool_name = getattr(p,
"tool_name",
"")
or tn.tool_name
193 tn.ts_started = ev.ts
195 prop = proposed_q.pop(0)
196 tn.proposed_args = dict(getattr(prop,
"requested_args", {})
or {})
197 tn.ts_proposed = tn.ts_proposed
or None
198 elif kind ==
"tool_call_completed":
199 tn = _tool(ev.tool_call_id
or getattr(p,
"tool_call_id",
"")
or "?")
200 tn.outcome = getattr(p,
"outcome",
None)
201 tn.ts_completed = ev.ts
202 elif kind
in _SUBAGENT_KINDS:
203 sid = getattr(p,
"subagent_session_id",
"")
or "?"
204 sn = sub_nodes.get(sid)
206 sn =
SubagentNode(subagent_session_id=sid, parent_turn_id=turn_id)
208 node.subagents.append(sn)
209 if kind ==
"subagent_started":
210 sn.ts_started = ev.ts
212 sn.outcome = getattr(p,
"outcome",
None)
213 sn.ts_completed = ev.ts
214 elif kind ==
"error":
215 node.errors.append(getattr(p,
"error_summary",
"")
or "")
217 node.other_events.append(ev.model_dump(mode=
"json"))
222 """Reduce a flat event list into a :class:`ReplayTree` (pure; two-pass, O(N))."""
224 order: list[str] = []
225 buckets: dict[str, list[TurnEvent]] = {}
226 counts: dict[str, int] = {}
228 counts[ev.kind] = counts.get(ev.kind, 0) + 1
229 if ev.session_id
and not session_id:
230 session_id = ev.session_id
234 if tid
not in buckets:
237 buckets[tid].append(ev)
238 nodes = [(
_build_turn_node(tid, buckets[tid]), idx)
for idx, tid
in enumerate(order)]
239 nodes.sort(key=
lambda pair: (pair[0].ts_first
or "", pair[1]))
241 session_id=session_id,
242 turn_count=len(nodes),
243 event_count=len(events),
244 skipped_count=skipped,
246 turns=[node
for node, _
in nodes],