1package model
2
3import (
4 "fmt"
5 "path/filepath"
6 "strings"
7 "time"
8
9 "charm.land/lipgloss/v2"
10 uv "github.com/charmbracelet/ultraviolet"
11 "github.com/charmbracelet/x/ansi"
12
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/message"
15 "github.com/charmbracelet/crush/internal/ui/common"
16 "github.com/charmbracelet/crush/internal/ui/lazylist"
17 "github.com/charmbracelet/crush/internal/ui/list"
18 "github.com/charmbracelet/crush/internal/ui/styles"
19 "github.com/charmbracelet/crush/internal/ui/toolrender"
20)
21
22// Identifiable is an interface for items that can provide a unique identifier.
23type Identifiable interface {
24 ID() string
25}
26
27// MessageItem represents a [message.Message] item that can be displayed in the
28// UI and be part of a [list.List] identifiable by a unique ID.
29type MessageItem interface {
30 list.Item
31 list.Focusable
32 list.Highlightable
33 lazylist.Item
34 Identifiable
35}
36
37// MessageContentItem represents rendered message content (text, markdown, errors, etc).
38type MessageContentItem struct {
39 list.BaseFocusable
40 list.BaseHighlightable
41 id string
42 content string
43 isMarkdown bool
44 maxWidth int
45 cache map[int]string // Cache for rendered content at different widths
46 sty *styles.Styles
47}
48
49// NewMessageContentItem creates a new message content item.
50func NewMessageContentItem(id, content string, isMarkdown bool, sty *styles.Styles) *MessageContentItem {
51 m := &MessageContentItem{
52 id: id,
53 content: content,
54 isMarkdown: isMarkdown,
55 maxWidth: 120,
56 cache: make(map[int]string),
57 sty: sty,
58 }
59 m.InitHighlight()
60 return m
61}
62
63// ID implements Identifiable.
64func (m *MessageContentItem) ID() string {
65 return m.id
66}
67
68// Height implements list.Item.
69func (m *MessageContentItem) Height(width int) int {
70 // Calculate content width accounting for frame size
71 contentWidth := width
72 if style := m.CurrentStyle(); style != nil {
73 contentWidth -= style.GetHorizontalFrameSize()
74 }
75
76 rendered := m.render(contentWidth)
77
78 // Apply focus/blur styling if configured to get accurate height
79 if style := m.CurrentStyle(); style != nil {
80 rendered = style.Render(rendered)
81 }
82
83 return strings.Count(rendered, "\n") + 1
84}
85
86// Draw implements list.Item.
87func (m *MessageContentItem) Draw(scr uv.Screen, area uv.Rectangle) {
88 width := area.Dx()
89 height := area.Dy()
90
91 // Calculate content width accounting for frame size
92 contentWidth := width
93 style := m.CurrentStyle()
94 if style != nil {
95 contentWidth -= style.GetHorizontalFrameSize()
96 }
97
98 rendered := m.render(contentWidth)
99
100 // Apply focus/blur styling if configured
101 if style != nil {
102 rendered = style.Render(rendered)
103 }
104
105 // Create temp buffer to draw content with highlighting
106 tempBuf := uv.NewScreenBuffer(width, height)
107
108 // Draw the rendered content to temp buffer
109 styled := uv.NewStyledString(rendered)
110 styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))
111
112 // Apply highlighting if active
113 m.ApplyHighlight(&tempBuf, width, height, style)
114
115 // Copy temp buffer to actual screen at the target area
116 tempBuf.Draw(scr, area)
117}
118
119// Render implements lazylist.Item.
120func (m *MessageContentItem) Render(width int) string {
121 return m.render(width)
122}
123
124// render renders the content at the given width, using cache if available.
125func (m *MessageContentItem) render(width int) string {
126 // Cap width to maxWidth for markdown
127 cappedWidth := width
128 if m.isMarkdown {
129 cappedWidth = min(width, m.maxWidth)
130 }
131
132 // Check cache first
133 if cached, ok := m.cache[cappedWidth]; ok {
134 return cached
135 }
136
137 // Not cached - render now
138 var rendered string
139 if m.isMarkdown {
140 renderer := common.MarkdownRenderer(m.sty, cappedWidth)
141 result, err := renderer.Render(m.content)
142 if err != nil {
143 rendered = m.content
144 } else {
145 rendered = strings.TrimSuffix(result, "\n")
146 }
147 } else {
148 rendered = m.content
149 }
150
151 // Cache the result
152 m.cache[cappedWidth] = rendered
153 return rendered
154}
155
156// SetHighlight implements list.Highlightable and extends BaseHighlightable.
157func (m *MessageContentItem) SetHighlight(startLine, startCol, endLine, endCol int) {
158 m.BaseHighlightable.SetHighlight(startLine, startCol, endLine, endCol)
159 // Clear cache when highlight changes
160 m.cache = make(map[int]string)
161}
162
163// ToolCallItem represents a rendered tool call with its header and content.
164type ToolCallItem struct {
165 list.BaseFocusable
166 list.BaseHighlightable
167 id string
168 toolCall message.ToolCall
169 toolResult message.ToolResult
170 cancelled bool
171 isNested bool
172 maxWidth int
173 cache map[int]cachedToolRender // Cache for rendered content at different widths
174 cacheKey string // Key to invalidate cache when content changes
175 sty *styles.Styles
176}
177
178// cachedToolRender stores both the rendered string and its height.
179type cachedToolRender struct {
180 content string
181 height int
182}
183
184// NewToolCallItem creates a new tool call item.
185func NewToolCallItem(id string, toolCall message.ToolCall, toolResult message.ToolResult, cancelled bool, isNested bool, sty *styles.Styles) *ToolCallItem {
186 t := &ToolCallItem{
187 id: id,
188 toolCall: toolCall,
189 toolResult: toolResult,
190 cancelled: cancelled,
191 isNested: isNested,
192 maxWidth: 120,
193 cache: make(map[int]cachedToolRender),
194 cacheKey: generateCacheKey(toolCall, toolResult, cancelled),
195 sty: sty,
196 }
197 t.InitHighlight()
198 return t
199}
200
201// generateCacheKey creates a key that changes when tool call content changes.
202func generateCacheKey(toolCall message.ToolCall, toolResult message.ToolResult, cancelled bool) string {
203 // Simple key based on result state - when result arrives or changes, key changes
204 return fmt.Sprintf("%s:%s:%v", toolCall.ID, toolResult.ToolCallID, cancelled)
205}
206
207// ID implements Identifiable.
208func (t *ToolCallItem) ID() string {
209 return t.id
210}
211
212// Height implements list.Item.
213func (t *ToolCallItem) Height(width int) int {
214 // Calculate content width accounting for frame size
215 contentWidth := width
216 frameSize := 0
217 if style := t.CurrentStyle(); style != nil {
218 frameSize = style.GetHorizontalFrameSize()
219 contentWidth -= frameSize
220 }
221
222 cached := t.renderCached(contentWidth)
223
224 // Add frame size to height if needed
225 height := cached.height
226 if frameSize > 0 {
227 // Frame can add to height (borders, padding)
228 if style := t.CurrentStyle(); style != nil {
229 // Quick render to get accurate height with frame
230 rendered := style.Render(cached.content)
231 height = strings.Count(rendered, "\n") + 1
232 }
233 }
234
235 return height
236}
237
238// Render implements lazylist.Item.
239func (t *ToolCallItem) Render(width int) string {
240 cached := t.renderCached(width)
241 return cached.content
242}
243
244// Draw implements list.Item.
245func (t *ToolCallItem) Draw(scr uv.Screen, area uv.Rectangle) {
246 width := area.Dx()
247 height := area.Dy()
248
249 // Calculate content width accounting for frame size
250 contentWidth := width
251 style := t.CurrentStyle()
252 if style != nil {
253 contentWidth -= style.GetHorizontalFrameSize()
254 }
255
256 cached := t.renderCached(contentWidth)
257 rendered := cached.content
258
259 if style != nil {
260 rendered = style.Render(rendered)
261 }
262
263 tempBuf := uv.NewScreenBuffer(width, height)
264 styled := uv.NewStyledString(rendered)
265 styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))
266
267 t.ApplyHighlight(&tempBuf, width, height, style)
268 tempBuf.Draw(scr, area)
269}
270
271// renderCached renders the tool call at the given width with caching.
272func (t *ToolCallItem) renderCached(width int) cachedToolRender {
273 cappedWidth := min(width, t.maxWidth)
274
275 // Check if we have a valid cache entry
276 if cached, ok := t.cache[cappedWidth]; ok {
277 return cached
278 }
279
280 // Render the tool call
281 ctx := &toolrender.RenderContext{
282 Call: t.toolCall,
283 Result: t.toolResult,
284 Cancelled: t.cancelled,
285 IsNested: t.isNested,
286 Width: cappedWidth,
287 Styles: t.sty,
288 }
289
290 rendered := toolrender.Render(ctx)
291 height := strings.Count(rendered, "\n") + 1
292
293 cached := cachedToolRender{
294 content: rendered,
295 height: height,
296 }
297 t.cache[cappedWidth] = cached
298 return cached
299}
300
301// SetHighlight implements list.Highlightable.
302func (t *ToolCallItem) SetHighlight(startLine, startCol, endLine, endCol int) {
303 t.BaseHighlightable.SetHighlight(startLine, startCol, endLine, endCol)
304 // Clear cache when highlight changes
305 t.cache = make(map[int]cachedToolRender)
306}
307
308// UpdateResult updates the tool result and invalidates the cache if needed.
309func (t *ToolCallItem) UpdateResult(result message.ToolResult) {
310 newKey := generateCacheKey(t.toolCall, result, t.cancelled)
311 if newKey != t.cacheKey {
312 t.toolResult = result
313 t.cacheKey = newKey
314 t.cache = make(map[int]cachedToolRender)
315 }
316}
317
318// AttachmentItem represents a file attachment in a user message.
319type AttachmentItem struct {
320 list.BaseFocusable
321 list.BaseHighlightable
322 id string
323 filename string
324 path string
325 sty *styles.Styles
326}
327
328// NewAttachmentItem creates a new attachment item.
329func NewAttachmentItem(id, filename, path string, sty *styles.Styles) *AttachmentItem {
330 a := &AttachmentItem{
331 id: id,
332 filename: filename,
333 path: path,
334 sty: sty,
335 }
336 a.InitHighlight()
337 return a
338}
339
340// ID implements Identifiable.
341func (a *AttachmentItem) ID() string {
342 return a.id
343}
344
345// Height implements list.Item.
346func (a *AttachmentItem) Height(width int) int {
347 return 1
348}
349
350// Render implements lazylist.Item.
351func (a *AttachmentItem) Render(width int) string {
352 const maxFilenameWidth = 10
353 return a.sty.Chat.Message.Attachment.Render(fmt.Sprintf(
354 " %s %s ",
355 styles.DocumentIcon,
356 ansi.Truncate(a.filename, maxFilenameWidth, "..."),
357 ))
358}
359
360// Draw implements list.Item.
361func (a *AttachmentItem) Draw(scr uv.Screen, area uv.Rectangle) {
362 width := area.Dx()
363 height := area.Dy()
364
365 // Calculate content width accounting for frame size
366 contentWidth := width
367 style := a.CurrentStyle()
368 if style != nil {
369 contentWidth -= style.GetHorizontalFrameSize()
370 }
371
372 const maxFilenameWidth = 10
373 content := a.sty.Chat.Message.Attachment.Render(fmt.Sprintf(
374 " %s %s ",
375 styles.DocumentIcon,
376 ansi.Truncate(a.filename, maxFilenameWidth, "..."),
377 ))
378
379 if style != nil {
380 content = style.Render(content)
381 }
382
383 tempBuf := uv.NewScreenBuffer(width, height)
384 styled := uv.NewStyledString(content)
385 styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))
386
387 a.ApplyHighlight(&tempBuf, width, height, style)
388 tempBuf.Draw(scr, area)
389}
390
391// ThinkingItem represents thinking/reasoning content in assistant messages.
392type ThinkingItem struct {
393 list.BaseFocusable
394 list.BaseHighlightable
395 id string
396 thinking string
397 duration time.Duration
398 finished bool
399 maxWidth int
400 cache map[int]string
401 sty *styles.Styles
402}
403
404// NewThinkingItem creates a new thinking item.
405func NewThinkingItem(id, thinking string, duration time.Duration, finished bool, sty *styles.Styles) *ThinkingItem {
406 t := &ThinkingItem{
407 id: id,
408 thinking: thinking,
409 duration: duration,
410 finished: finished,
411 maxWidth: 120,
412 cache: make(map[int]string),
413 sty: sty,
414 }
415 t.InitHighlight()
416 return t
417}
418
419// ID implements Identifiable.
420func (t *ThinkingItem) ID() string {
421 return t.id
422}
423
424// Height implements list.Item.
425func (t *ThinkingItem) Height(width int) int {
426 // Calculate content width accounting for frame size
427 contentWidth := width
428 if style := t.CurrentStyle(); style != nil {
429 contentWidth -= style.GetHorizontalFrameSize()
430 }
431
432 rendered := t.render(contentWidth)
433 return strings.Count(rendered, "\n") + 1
434}
435
436// Render implements lazylist.Item.
437func (t *ThinkingItem) Render(width int) string {
438 return t.render(width)
439}
440
441// Draw implements list.Item.
442func (t *ThinkingItem) Draw(scr uv.Screen, area uv.Rectangle) {
443 width := area.Dx()
444 height := area.Dy()
445
446 // Calculate content width accounting for frame size
447 contentWidth := width
448 style := t.CurrentStyle()
449 if style != nil {
450 contentWidth -= style.GetHorizontalFrameSize()
451 }
452
453 rendered := t.render(contentWidth)
454
455 if style != nil {
456 rendered = style.Render(rendered)
457 }
458
459 tempBuf := uv.NewScreenBuffer(width, height)
460 styled := uv.NewStyledString(rendered)
461 styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))
462
463 t.ApplyHighlight(&tempBuf, width, height, style)
464 tempBuf.Draw(scr, area)
465}
466
467// render renders the thinking content.
468func (t *ThinkingItem) render(width int) string {
469 cappedWidth := min(width, t.maxWidth)
470
471 if cached, ok := t.cache[cappedWidth]; ok {
472 return cached
473 }
474
475 renderer := common.PlainMarkdownRenderer(cappedWidth - 1)
476 rendered, err := renderer.Render(t.thinking)
477 if err != nil {
478 // Fallback to line-by-line rendering
479 lines := strings.Split(t.thinking, "\n")
480 var content strings.Builder
481 lineStyle := t.sty.PanelMuted
482 for i, line := range lines {
483 if line == "" {
484 continue
485 }
486 content.WriteString(lineStyle.Width(cappedWidth).Render(line))
487 if i < len(lines)-1 {
488 content.WriteString("\n")
489 }
490 }
491 rendered = content.String()
492 }
493
494 fullContent := strings.TrimSpace(rendered)
495
496 // Add footer if finished
497 if t.finished && t.duration > 0 {
498 footer := t.sty.Chat.Message.ThinkingFooter.Render(fmt.Sprintf("Thought for %s", t.duration.String()))
499 fullContent = lipgloss.JoinVertical(lipgloss.Left, fullContent, "", footer)
500 }
501
502 result := t.sty.PanelMuted.Width(cappedWidth).Padding(0, 1).Render(fullContent)
503
504 t.cache[cappedWidth] = result
505 return result
506}
507
508// SetHighlight implements list.Highlightable.
509func (t *ThinkingItem) SetHighlight(startLine, startCol, endLine, endCol int) {
510 t.BaseHighlightable.SetHighlight(startLine, startCol, endLine, endCol)
511 t.cache = make(map[int]string)
512}
513
514// SectionHeaderItem represents a section header (e.g., assistant info).
515type SectionHeaderItem struct {
516 list.BaseFocusable
517 list.BaseHighlightable
518 id string
519 modelName string
520 duration time.Duration
521 isSectionHeader bool
522 sty *styles.Styles
523}
524
525// NewSectionHeaderItem creates a new section header item.
526func NewSectionHeaderItem(id, modelName string, duration time.Duration, sty *styles.Styles) *SectionHeaderItem {
527 s := &SectionHeaderItem{
528 id: id,
529 modelName: modelName,
530 duration: duration,
531 isSectionHeader: true,
532 sty: sty,
533 }
534 s.InitHighlight()
535 return s
536}
537
538// ID implements Identifiable.
539func (s *SectionHeaderItem) ID() string {
540 return s.id
541}
542
543// IsSectionHeader returns true if this is a section header.
544func (s *SectionHeaderItem) IsSectionHeader() bool {
545 return s.isSectionHeader
546}
547
548// Height implements list.Item.
549func (s *SectionHeaderItem) Height(width int) int {
550 return 1
551}
552
553// Render implements lazylist.Item.
554func (s *SectionHeaderItem) Render(width int) string {
555 return s.sty.Chat.Message.SectionHeader.Render(fmt.Sprintf("%s %s %s",
556 s.sty.Subtle.Render(styles.ModelIcon),
557 s.sty.Muted.Render(s.modelName),
558 s.sty.Subtle.Render(s.duration.String()),
559 ))
560}
561
562// Draw implements list.Item.
563func (s *SectionHeaderItem) Draw(scr uv.Screen, area uv.Rectangle) {
564 width := area.Dx()
565 height := area.Dy()
566
567 // Calculate content width accounting for frame size
568 contentWidth := width
569 style := s.CurrentStyle()
570 if style != nil {
571 contentWidth -= style.GetHorizontalFrameSize()
572 }
573
574 infoMsg := s.sty.Subtle.Render(s.duration.String())
575 icon := s.sty.Subtle.Render(styles.ModelIcon)
576 modelFormatted := s.sty.Muted.Render(s.modelName)
577 content := fmt.Sprintf("%s %s %s", icon, modelFormatted, infoMsg)
578
579 content = s.sty.Chat.Message.SectionHeader.Render(content)
580
581 if style != nil {
582 content = style.Render(content)
583 }
584
585 tempBuf := uv.NewScreenBuffer(width, height)
586 styled := uv.NewStyledString(content)
587 styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))
588
589 s.ApplyHighlight(&tempBuf, width, height, style)
590 tempBuf.Draw(scr, area)
591}
592
593// GetMessageItems extracts [MessageItem]s from a [message.Message]. It returns
594// all parts of the message as [MessageItem]s.
595//
596// For assistant messages with tool calls, pass a toolResults map to link results.
597// Use BuildToolResultMap to create this map from all messages in a session.
598func GetMessageItems(msg *message.Message, toolResults map[string]message.ToolResult) []MessageItem {
599 sty := styles.DefaultStyles()
600 var items []MessageItem
601
602 // Skip tool result messages - they're displayed inline with tool calls
603 if msg.Role == message.Tool {
604 return items
605 }
606
607 // Create base styles for the message
608 var focusStyle, blurStyle lipgloss.Style
609 if msg.Role == message.User {
610 focusStyle = sty.Chat.Message.UserFocused
611 blurStyle = sty.Chat.Message.UserBlurred
612 } else {
613 focusStyle = sty.Chat.Message.AssistantFocused
614 blurStyle = sty.Chat.Message.AssistantBlurred
615 }
616
617 // Process user messages
618 if msg.Role == message.User {
619 // Add main text content
620 content := msg.Content().String()
621 if content != "" {
622 item := NewMessageContentItem(
623 fmt.Sprintf("%s-content", msg.ID),
624 content,
625 true, // User messages are markdown
626 &sty,
627 )
628 item.SetFocusStyles(&focusStyle, &blurStyle)
629 items = append(items, item)
630 }
631
632 // Add attachments
633 for i, attachment := range msg.BinaryContent() {
634 filename := filepath.Base(attachment.Path)
635 item := NewAttachmentItem(
636 fmt.Sprintf("%s-attachment-%d", msg.ID, i),
637 filename,
638 attachment.Path,
639 &sty,
640 )
641 item.SetFocusStyles(&focusStyle, &blurStyle)
642 items = append(items, item)
643 }
644
645 return items
646 }
647
648 // Process assistant messages
649 if msg.Role == message.Assistant {
650 // Check if we need to add a section header
651 finishData := msg.FinishPart()
652 if finishData != nil && msg.Model != "" {
653 model := config.Get().GetModel(msg.Provider, msg.Model)
654 modelName := "Unknown Model"
655 if model != nil {
656 modelName = model.Name
657 }
658
659 // Calculate duration (this would need the last user message time)
660 duration := time.Duration(0)
661 if finishData.Time > 0 {
662 duration = time.Duration(finishData.Time-msg.CreatedAt) * time.Second
663 }
664
665 header := NewSectionHeaderItem(
666 fmt.Sprintf("%s-header", msg.ID),
667 modelName,
668 duration,
669 &sty,
670 )
671 items = append(items, header)
672 }
673
674 // Add thinking content if present
675 reasoning := msg.ReasoningContent()
676 if strings.TrimSpace(reasoning.Thinking) != "" {
677 duration := time.Duration(0)
678 if reasoning.StartedAt > 0 && reasoning.FinishedAt > 0 {
679 duration = time.Duration(reasoning.FinishedAt-reasoning.StartedAt) * time.Second
680 }
681
682 item := NewThinkingItem(
683 fmt.Sprintf("%s-thinking", msg.ID),
684 reasoning.Thinking,
685 duration,
686 reasoning.FinishedAt > 0,
687 &sty,
688 )
689 item.SetFocusStyles(&focusStyle, &blurStyle)
690 items = append(items, item)
691 }
692
693 // Add main text content
694 content := msg.Content().String()
695 finished := msg.IsFinished()
696
697 // Handle special finish states
698 if finished && content == "" && finishData != nil {
699 switch finishData.Reason {
700 case message.FinishReasonEndTurn:
701 // No content to show
702 case message.FinishReasonCanceled:
703 item := NewMessageContentItem(
704 fmt.Sprintf("%s-content", msg.ID),
705 "*Canceled*",
706 true,
707 &sty,
708 )
709 item.SetFocusStyles(&focusStyle, &blurStyle)
710 items = append(items, item)
711 case message.FinishReasonError:
712 // Render error
713 errTag := sty.Chat.Message.ErrorTag.Render("ERROR")
714 truncated := ansi.Truncate(finishData.Message, 100, "...")
715 title := fmt.Sprintf("%s %s", errTag, sty.Chat.Message.ErrorTitle.Render(truncated))
716 details := sty.Chat.Message.ErrorDetails.Render(finishData.Details)
717 errorContent := fmt.Sprintf("%s\n\n%s", title, details)
718
719 item := NewMessageContentItem(
720 fmt.Sprintf("%s-error", msg.ID),
721 errorContent,
722 false,
723 &sty,
724 )
725 item.SetFocusStyles(&focusStyle, &blurStyle)
726 items = append(items, item)
727 }
728 } else if content != "" {
729 item := NewMessageContentItem(
730 fmt.Sprintf("%s-content", msg.ID),
731 content,
732 true, // Assistant messages are markdown
733 &sty,
734 )
735 item.SetFocusStyles(&focusStyle, &blurStyle)
736 items = append(items, item)
737 }
738
739 // Add tool calls
740 toolCalls := msg.ToolCalls()
741
742 // Use passed-in tool results map (if nil, use empty map)
743 resultMap := toolResults
744 if resultMap == nil {
745 resultMap = make(map[string]message.ToolResult)
746 }
747
748 for _, tc := range toolCalls {
749 result, hasResult := resultMap[tc.ID]
750 if !hasResult {
751 result = message.ToolResult{}
752 }
753
754 item := NewToolCallItem(
755 fmt.Sprintf("%s-tool-%s", msg.ID, tc.ID),
756 tc,
757 result,
758 false, // cancelled state would need to be tracked separately
759 false, // nested state would be detected from tool results
760 &sty,
761 )
762
763 // Tool calls use muted style with optional focus border
764 item.SetFocusStyles(&sty.Chat.Message.ToolCallFocused, &sty.Chat.Message.ToolCallBlurred)
765
766 items = append(items, item)
767 }
768
769 return items
770 }
771
772 return items
773}
774
775// BuildToolResultMap creates a map of tool call IDs to their results from a list of messages.
776// Tool result messages (role == message.Tool) contain the results that should be linked
777// to tool calls in assistant messages.
778func BuildToolResultMap(messages []*message.Message) map[string]message.ToolResult {
779 resultMap := make(map[string]message.ToolResult)
780 for _, msg := range messages {
781 if msg.Role == message.Tool {
782 for _, result := range msg.ToolResults() {
783 if result.ToolCallID != "" {
784 resultMap[result.ToolCallID] = result
785 }
786 }
787 }
788 }
789 return resultMap
790}