|
2 | 2 |
|
3 | 3 | All notable changes to GopherAgent are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/); versions follow [Semantic Versioning](https://semver.org/) — pre-1.0, breaking API changes only require a minor bump. |
4 | 4 |
|
| 5 | +## [v0.41.0] — 2026-08-10 |
| 6 | + |
| 7 | +### Added |
| 8 | + |
| 9 | +- **`pkg/audio` — a provider-neutral speech-to-text seam, so an audio-fed agent is not limited to the one vendor that accepts audio in a message.** Of the three providers here, only Gemini takes audio in a chat message: Anthropic has no audio content block at all, and the OpenAI Chat Completions client cannot express one. Transcribing *before* the message rather than inside it makes the capability portable — every provider can drive an audio-fed agent, because what reaches the model is ordinary text. It is also the cheaper shape for anything long. History is re-sent on every LLM call in a session, so audio carried as a message part is re-uploaded on every subsequent turn: a one-hour recording transcribed once costs one upload, while the same recording as message parts is billed again on each turn of the conversation about it. The package is a stdlib-only leaf holding the `Transcriber` interface plus `Clip`, `Transcript`, `Segment`, and `Options`. Three sentinels — `ErrNoAudio`, `ErrUnsupportedFormat`, `ErrTooLarge` — are separate rather than one error because a live-capture pipeline responds to each differently: an oversized clip must be re-cut into shorter chunks, an unsupported one re-encoded, and an empty one is a caller bug; routing on `errors.Is` beats matching message text. `Transcript.Segments` is nil when the backend emits no timing, which is a normal result rather than a failure, so callers needing timestamps must check rather than assume. `Ext` strips MIME parameters and matches case-insensitively, because browsers' `MediaRecorder` reports `audio/webm;codecs=opus` and some emit the `video/webm` spelling for an audio-only recording — rejecting either would fail a clip the backend decodes fine. (`pkg/audio`) |
| 10 | +- **`openai.NewTranscriber` and `gemini.NewTranscriber`.** The OpenAI implementation drives the audio transcription endpoint and populates `Segments`, `Language`, and `Duration`. It selects the `verbose_json` response format only for whisper models: the `gpt-4o-transcribe` family rejects that format outright rather than degrading, so asking for it everywhere would fail every request instead of merely losing timings. The match is a substring rather than a prefix, since compatible endpoints name the same weights differently. Oversized clips are rejected against the endpoint's 25 MB limit before the upload rather than after transferring the payload. `WithBaseURL` works here as on every other client in the package, so a self-hosted transcription server is a supported target. The Gemini implementation has no dedicated transcription endpoint to call, so it constrains the generation API with a system instruction — without one the model opens with a preamble or summarizes instead of transcribing, and both corrupt a transcript appended verbatim. Its `Segments` is always nil, stated on the type rather than discovered at run time, and its language and vocabulary hints travel as instruction text because the API has no parameter for either. (`pkg/llm/openai/transcriber.go`, `pkg/llm/gemini/transcriber.go`) |
| 11 | + |
| 12 | +### Changed (breaking) |
| 13 | + |
| 14 | +- **A message carrying a media part the adapter cannot render now fails with `agent.ErrUnrenderablePart` instead of being silently dropped.** All three adapters converted `history.MediaPart` with a `switch` that fell through for anything unexpected, and one of them documented the omission as deliberate on the grounds that erroring would force callers to pre-validate. That trade was wrong in the direction that matters. Dropping the part does not degrade the call, it silently changes what the question was: the model receives the caption alone and answers it fluently, and nothing distinguishes that from success — not the response, not the logs, not a schema check, because a well-formed answer is exactly what success looks like. A judge that cannot see the image it is judging is not a degraded judge, it is a random one. Four shapes now fail: an unknown part type, an image with neither `URL` nor `Data`, a parts slice that yields no content at all, and media parts on a non-`user` role, which every adapter previously ignored wholesale — OpenAI behind an explicit role guard, Anthropic and Gemini by rendering media only under their `user` branch. Empty text parts are still skipped, as they carry nothing to lose. `blocksFromMediaParts` and `partsFromMediaParts` grew an `error` return; both are unexported and every call site is inside `GenerateStream`, which already returned one. `isRetryable` treats the sentinel as terminal — the same bytes fail identically on every attempt, so retrying only burns the budget. Callers that relied on a malformed part being ignored will now see the call fail; that is the point. (`pkg/agent/errors.go`, `pkg/agent/retry.go`, `pkg/llm/anthropic`, `pkg/llm/openai`, `pkg/llm/gemini`) |
| 15 | + |
| 16 | +### Fixed |
| 17 | + |
| 18 | +- **The Gemini transcriber reads `FinishReason` before testing for nil content.** A candidate stopped by a content filter arrives with a non-`STOP` reason *and* nil content, so checking content first reported a blocked recording as an empty transcript — a filtered meeting became indistinguishable from a silent one, with no error to act on. The same ordering guards truncation, where returning the accumulated prefix would present half a transcript as the whole. A response with no candidate at all is now an error rather than an empty transcript, since it signals a prompt-level block rather than audio without speech. (`pkg/llm/gemini/transcriber.go`) |
| 19 | + |
5 | 20 | ## [v0.40.0] — 2026-08-09 |
6 | 21 |
|
7 | 22 | ### Added |
@@ -620,6 +635,7 @@ Multi-user, long-running, audit-friendly chat surface — the foundation for sid |
620 | 635 | - README section on the permission flow — documents `RequiresConfirmation` × `ConfirmHITL` × `Permissions` interaction. |
621 | 636 | - Enum struct tag support in `tools.SchemaFor[T]()` — emit values into JSON-Schema's `enum` array so providers reject invalid values upstream. |
622 | 637 |
|
| 638 | +[v0.41.0]: https://github.com/hung12ct/gopheragent/releases/tag/v0.41.0 |
623 | 639 | [v0.40.0]: https://github.com/hung12ct/gopheragent/releases/tag/v0.40.0 |
624 | 640 | [v0.39.0]: https://github.com/hung12ct/gopheragent/releases/tag/v0.39.0 |
625 | 641 | [v0.38.0]: https://github.com/hung12ct/gopheragent/releases/tag/v0.38.0 |
|
0 commit comments