| import '../../shared/models/prompt.dart'; |
| import 'agent_timeline_item.dart'; |
| |
| /// A single user-initiated turn in the agent conversation. |
| /// |
| /// Groups a user message together with the subsequent run items |
| /// (tool calls, thinking messages, errors) and the final assistant response. |
| class AgentTurn { |
| AgentTurn({ |
| required this.userPrompt, |
| required this.runItems, |
| this.assistantMessage, |
| this.wasStopped = false, |
| this.error, |
| this.runDuration, |
| }); |
| |
| /// The user prompt that initiated this turn. |
| final Prompt userPrompt; |
| |
| /// Tool calls, thinking messages, and errors that occurred during the run. |
| final List<AgentTimelineItem> runItems; |
| |
| /// The final assistant response, if any. |
| final String? assistantMessage; |
| |
| /// Whether the run was cancelled by the user. |
| final bool wasStopped; |
| |
| /// The error text from the run, if any. |
| final String? error; |
| |
| /// The wall-clock duration of the agent run for this turn. |
| final Duration? runDuration; |
| |
| /// Groups a flat [timeline] into user-initiated [AgentTurn]s. |
| /// |
| /// Each turn starts with a user message and includes all subsequent |
| /// timeline items until the next user message (or the end of the list). |
| static List<AgentTurn> groupFromTimeline(List<AgentTimelineItem> timeline) { |
| final turns = <AgentTurn>[]; |
| int i = 0; |
| |
| while (i < timeline.length) { |
| final item = timeline[i]; |
| if (item is AgentPromptTimelineItem) { |
| final userPrompt = item.prompt; |
| final runItems = <AgentTimelineItem>[]; |
| String? assistantMessage; |
| bool wasStopped = false; |
| String? error; |
| Duration? runDuration; |
| |
| i++; |
| while (i < timeline.length) { |
| final nextItem = timeline[i]; |
| if (nextItem is AgentPromptTimelineItem) { |
| break; |
| } |
| |
| switch (nextItem) { |
| case AgentMessageTimelineItem(:final message, :final duration): |
| assistantMessage = message; |
| runDuration = duration; |
| case AgentPlanTimelineItem(): |
| runItems.add(nextItem); |
| case AgentStoppedTimelineItem(:final duration): |
| wasStopped = true; |
| runDuration = duration; |
| case AgentErrorTimelineItem(error: final itemError, :final duration): |
| error = itemError; |
| runDuration = duration; |
| runItems.add(nextItem); |
| case AgentThinkingTimelineItem() || AgentToolTimelineItem(): |
| runItems.add(nextItem); |
| case AgentPromptTimelineItem(): |
| break; |
| } |
| i++; |
| } |
| |
| // When a turn ends with an error or stop, the last thinking message |
| // was never finalized (duration == null) and would start a phantom |
| // live timer in the UI. Remove it. |
| if (error != null || wasStopped) { |
| for (int j = runItems.length - 1; j >= 0; j--) { |
| final ri = runItems[j]; |
| if (ri is AgentThinkingTimelineItem && ri.duration == null) { |
| runItems.removeAt(j); |
| break; |
| } |
| } |
| } |
| |
| turns.add( |
| AgentTurn( |
| userPrompt: userPrompt, |
| runItems: runItems, |
| assistantMessage: assistantMessage, |
| wasStopped: wasStopped, |
| error: error, |
| runDuration: runDuration, |
| ), |
| ); |
| } else { |
| // Skip items before first user message if any. |
| i++; |
| } |
| } |
| |
| return turns; |
| } |
| } |