diff --git a/internal/agent/loop.go b/internal/agent/loop.go index f4029ed9..044e28c5 100644 --- a/internal/agent/loop.go +++ b/internal/agent/loop.go @@ -259,6 +259,8 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) } if len(audioRefs) > 0 { ctx = tools.WithMediaAudioRefs(ctx, audioRefs) + // Embed media IDs into tags so LLM can reference them. + l.enrichAudioIDs(messages, mediaRefs) } // 2d. Collect video MediaRefs (historical + current) for read_video tool. diff --git a/internal/agent/media.go b/internal/agent/media.go index fdb88f75..086f48c7 100644 --- a/internal/agent/media.go +++ b/internal/agent/media.go @@ -163,6 +163,47 @@ func (l *Loop) enrichDocumentPaths(messages []providers.Message, refs []provider messages[lastIdx].Content = content } +// enrichAudioIDs updates the last user message to embed persisted media IDs +// in and tags so the LLM can reference them. +// Without this, the LLM sees plain and cannot pass a valid media_id. +func (l *Loop) enrichAudioIDs(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 + for _, ref := range refs { + if ref.Kind != "audio" { + continue + } + idAttr := fmt.Sprintf(" id=%q", ref.ID) + + // Replace bare with + bare := "" + if strings.Contains(content, bare) { + content = strings.Replace(content, bare, "", 1) + continue + } + // Replace bare with + bareVoice := "" + if strings.Contains(content, bareVoice) { + content = strings.Replace(content, bareVoice, "", 1) + continue + } + } + messages[lastIdx].Content = content +} + // mediaKindFromMime returns the media kind ("image", "video", "audio", "document") // based on MIME type prefix. func mediaKindFromMime(mime string) string { diff --git a/internal/tools/read_audio_resolve.go b/internal/tools/read_audio_resolve.go index 47ad7a24..63cab3cf 100644 --- a/internal/tools/read_audio_resolve.go +++ b/internal/tools/read_audio_resolve.go @@ -23,6 +23,13 @@ func (t *ReadAudioTool) resolveAudioFile(ctx context.Context, mediaID string) (p return "", "", fmt.Errorf("no audio files available in this conversation. The user may not have sent an audio file.") } + // Sanitize media_id: LLM may pass the literal tag string (e.g. "") + // instead of a UUID. Treat tag-like values as empty to fall back to most recent. + if strings.Contains(mediaID, "<") || strings.Contains(mediaID, "media:") { + slog.Debug("read_audio: sanitizing tag-like media_id", "raw", mediaID) + mediaID = "" + } + var ref *providers.MediaRef if mediaID != "" { for i := range refs { @@ -32,7 +39,10 @@ func (t *ReadAudioTool) resolveAudioFile(ctx context.Context, mediaID string) (p } } if ref == nil { - return "", "", fmt.Errorf("audio with media_id %q not found in conversation", mediaID) + // Fallback to most recent audio instead of hard error, + // since LLM may generate invalid IDs. + slog.Warn("read_audio: media_id not found, falling back to most recent", "media_id", mediaID) + ref = &refs[len(refs)-1] } } else { ref = &refs[len(refs)-1]