diff --git a/internal/agent/loop.go b/internal/agent/loop.go index 319e0077..75f645e5 100644 --- a/internal/agent/loop.go +++ b/internal/agent/loop.go @@ -297,8 +297,12 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) l.enrichVideoIDs(messages, mediaRefs) } - // 2e. Cross-session recovery: recover stale locked tasks and notify team leads - // about orphaned pending tasks and in-progress tasks being handled by delegates. + // 2e. Enrich tags with persisted media IDs so the LLM + // knows images were received and stored (consistent with audio/video enrichment). + l.enrichImageIDs(messages, mediaRefs) + + // 2f. Cross-session recovery: notify team leads about orphaned pending tasks + // and in-progress tasks being handled by delegates. // Safe because Bước 1 (early ClaimTask) ensures running tasks are in_progress, // so only truly un-spawned tasks remain pending. if l.teamStore != nil && l.agentUUID != uuid.Nil { diff --git a/internal/agent/media.go b/internal/agent/media.go index e2c280c3..a687c68b 100644 --- a/internal/agent/media.go +++ b/internal/agent/media.go @@ -244,6 +244,45 @@ func (l *Loop) enrichVideoIDs(messages []providers.Message, refs []providers.Med messages[lastIdx].Content = content } +// enrichImageIDs updates the last user message to embed persisted media IDs +// in tags so the LLM knows images were received and stored. +// Without this, the LLM sees plain and cannot confirm image availability. +// Iterates refs in reverse order so that when multiple images are present, +// each ref maps to the correct positional tag (last ref → last tag, etc.). +func (l *Loop) enrichImageIDs(messages []providers.Message, refs []providers.MediaRef) { + if len(messages) == 0 { + return + } + lastIdx := -1 + for i := len(messages) - 1; i >= 0; i-- { + if messages[i].Role == "user" { + lastIdx = i + break + } + } + if lastIdx < 0 { + return + } + + content := messages[lastIdx].Content + // Iterate in reverse so first ref matches first tag when using LastIndex replacements. + for i := len(refs) - 1; i >= 0; i-- { + ref := refs[i] + if ref.Kind != "image" { + continue + } + idAttr := fmt.Sprintf(" id=%q", ref.ID) + + // Replace the LAST bare with + bare := "" + if idx := strings.LastIndex(content, bare); idx >= 0 { + content = content[:idx] + "" + content[idx+len(bare):] + continue + } + } + messages[lastIdx].Content = content +} + // mediaKindFromMime returns the media kind ("image", "video", "audio", "document") // based on MIME type prefix. func mediaKindFromMime(mime string) string {