Skip to content

feat: image-generation lifecycle frames (image_pending / image_attempt / image_failed) (#131)#133

Merged
enriquephl merged 7 commits into
devfrom
feat/image-lifecycle-frames
Jun 30, 2026
Merged

feat: image-generation lifecycle frames (image_pending / image_attempt / image_failed) (#131)#133
enriquephl merged 7 commits into
devfrom
feat/image-lifecycle-frames

Conversation

@enriquephl

Copy link
Copy Markdown
Member

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_image blocks on image generation after the text done frame 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 (reasonchain_exhausted / zero_images / config_error); clear pending, render failed.

Sequences

  • reply_text_image: meta → delta* → done → image_pending → image_attempt* → (image | image_failed) → final
  • reply_image (success): image_pending → image_attempt* → meta → image → done → final
  • reply_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 id Y (X is the never-persisted intended-image id that image_failed clears; the diagnostic persists on Y).

How

  • execute_image → refactored into execute_image_inner(req, on_attempt) (a sync hook fired before each HTTP post); execute_image retained as the stable public lib entry point.
  • New 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

  • Full workspace (with DB): 693 tests pass, 0 failed (core 63 / llm 169 / server 354 / store 107).
  • cargo fmt --check clean · cargo clippy --all-targets -D warnings clean · OpenAPI snapshot unchanged.
  • New automated tests: execute_image_inner attempt-hook (wiremock), drive_image_gen stream events (wiremock), frame serialization.
  • Happy-path SSE wire-order is manual (no mock seam for the concrete client) — pending live-key check.

Design spec: docs/superpowers/specs/2026-06-30-image-lifecycle-frames-design.md

🤖 Generated with Claude Code

enriquephl and others added 7 commits June 30, 2026 18:52
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>
)

Additive ProtocolFrame variants + ImageFailReason enum. No wiring yet.

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>
@enriquephl enriquephl merged commit 75aab38 into dev Jun 30, 2026
6 checks passed
@enriquephl enriquephl deleted the feat/image-lifecycle-frames branch June 30, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant