feat: image-generation lifecycle frames (image_pending / image_attempt / image_failed) (#131)#133
Merged
Merged
Conversation
Design for image_pending / image_attempt / image_failed SSE frames so consumers can render a generating state for both reply_image and reply_text_image, with a live fallback-chain narrative via a streaming seam in execute_image. Additive on the wire. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract execute_image into execute_image_inner(req, on_attempt) firing a sync hook before each HTTP post; execute_image delegates with a no-op. Loop body unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the channel/select image-gen driver into drive_image_gen (one Attempt per chain step, then Done); unit-test it via wiremock. Wire reply_text_image: image_pending after done (before compose), forward image_attempt, image_failed on all three failure arms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reuse drive_image_gen for reply_image: pre-allocate the image id so image_pending/image_attempt/image_failed reference X before gen; on failure the turn still degrades to a separate text row Y (X != Y). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the new lifecycle frames, updated sequences for both image actions, and the reply_image X!=Y degraded-failure contract. EN + zh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both in-tree image arms call execute_image_inner; execute_image now has no in-tree callers but is the public lib API for downstream consumers. Doc note prevents a future dead-code deletion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 30, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds explicit image-generation lifecycle SSE frames so chat consumers can render a deterministic generating → done / failed state for both image actions. Closes #131.
Today,
reply_text_imageblocks on image generation after the textdoneframe with no frame in the gap (so a consumer gating "generating" on the message being unsettled loses the signal), and image-gen failure emits nothing at all. This change makes the generating/done/failed states unambiguous and consistent across both image actions.New SSE frames (additive — existing consumers ignore unknown
types)image_pending{ message_id }— the engine has committed to generating an image; start the spinner.image_attempt{ message_id, model, variant, index, total }— live fallback-chain progress, one per attempt as it begins.image_failed{ message_id, reason }— gave up (reason∈chain_exhausted/zero_images/config_error); clear pending, render failed.Sequences
reply_text_image:meta → delta* → done → image_pending → image_attempt* → (image | image_failed) → finalreply_image(success):image_pending → image_attempt* → meta → image → done → finalreply_image(failed):image_pending(X) → image_attempt*(X) → image_failed(X) → meta(reply_text,Y) → delta* → done(Y) → final— the turn degrades to a text reply with a different idY(Xis the never-persisted intended-image id thatimage_failedclears; the diagnostic persists onY).How
execute_image→ refactored intoexecute_image_inner(req, on_attempt)(a sync hook fired before each HTTP post);execute_imageretained as the stable public lib entry point.drive_image_gen(client, req) -> impl Stream<ImageGenEvent>helper holds the channel/select!concurrency once; both image arms forward its events (gen future polled in place — dropping the stream cancels the in-flight call).Compatibility
Purely additive: no existing frame's shape changes; OpenAPI snapshot unchanged (SSE frames aren't modeled there). Docs updated in
api-reference.md+api-reference.zh.md.Verification
cargo fmt --checkclean ·cargo clippy --all-targets -D warningsclean · OpenAPI snapshot unchanged.execute_image_innerattempt-hook (wiremock),drive_image_genstream events (wiremock), frame serialization.Design spec:
docs/superpowers/specs/2026-06-30-image-lifecycle-frames-design.md🤖 Generated with Claude Code