Run one agent loop
(ctx context.Context, cancel context.CancelFunc, message string, attachments []messages.Attachment)
| 454 | |
| 455 | // Run one agent loop |
| 456 | func (a *App) Run(ctx context.Context, cancel context.CancelFunc, message string, attachments []messages.Attachment) { |
| 457 | a.cancel = cancel |
| 458 | |
| 459 | // If this is the first message and no title exists, start local title generation |
| 460 | if a.session.Title == "" && a.titleGen != nil { |
| 461 | a.titleGenerating.Store(true) |
| 462 | go a.generateTitle(ctx, []string{message}) |
| 463 | } |
| 464 | |
| 465 | go func() { |
| 466 | if len(attachments) > 0 { |
| 467 | // Build a single text string with the user's message and inlined text files. |
| 468 | // Keeping everything in one text block ensures the model sees file content |
| 469 | // together with the message, rather than as separate content blocks. |
| 470 | var textBuilder strings.Builder |
| 471 | textBuilder.WriteString(message) |
| 472 | |
| 473 | // binaryParts holds non-text file parts (images, PDFs, etc.) |
| 474 | var binaryParts []chat.MessagePart |
| 475 | |
| 476 | for _, att := range attachments { |
| 477 | switch { |
| 478 | case att.FilePath != "": |
| 479 | // File-reference attachment: read and classify from disk. |
| 480 | // Only remember the path on the session when the file actually |
| 481 | // exists as a regular file — we don't want sub-agents to inherit |
| 482 | // dangling references to directories or missing paths. The editor |
| 483 | // resolves @-mentions to absolute paths before this point. |
| 484 | if a.processFileAttachment(ctx, att, &textBuilder, &binaryParts) { |
| 485 | a.session.AddAttachedFile(att.FilePath) |
| 486 | } |
| 487 | case att.Content != "": |
| 488 | // Inline content attachment (e.g. pasted text). |
| 489 | a.processInlineAttachment(att, &textBuilder) |
| 490 | default: |
| 491 | slog.DebugContext(ctx, "skipping attachment with no file path or content", "name", att.Name) |
| 492 | } |
| 493 | } |
| 494 | |
| 495 | multiContent := []chat.MessagePart{ |
| 496 | {Type: chat.MessagePartTypeText, Text: textBuilder.String()}, |
| 497 | } |
| 498 | multiContent = append(multiContent, binaryParts...) |
| 499 | |
| 500 | a.session.AddMessage(session.UserMessage(message, multiContent...)) |
| 501 | } else { |
| 502 | a.session.AddMessage(session.UserMessage(message)) |
| 503 | } |
| 504 | for event := range a.runtime.RunStream(ctx, a.session) { |
| 505 | // If context is cancelled, continue draining but don't forward events |
| 506 | // — except StreamStoppedEvent, which must always propagate so the |
| 507 | // supervisor can mark the session as no longer running. |
| 508 | if ctx.Err() != nil { |
| 509 | if _, ok := event.(*runtime.StreamStoppedEvent); ok { |
| 510 | // ctx is cancelled; detach cancellation but keep its trace |
| 511 | // context so the stop event still reaches subscribers. |
| 512 | a.sendEvent(context.WithoutCancel(ctx), event) |
| 513 | } |
nothing calls this directly
no test coverage detected