Skip to content

Conversation

@jiwon-oai
Copy link
Collaborator

@jiwon-oai jiwon-oai commented Nov 22, 2025

  • Add an overridable ChatKitServer.handle_stream_cancelled method with pending items that is invoked when the stream is cancelled by the user.

    • Default implementation saves pending (partially generated) assistant messages.
    • Pending workflow items reflect the latest update but are not saved.
    • Widget items do not reflect the latest update and are not saved.
  • Add an overridable ChatKitServer.get_stream_options method that returns StreamOptions(allow_cancel=True)

    • This is the payload for the new StreamOptionsEvent which is sent at the start of _process_events
    • The default allow_cancel=True will be wired up to show a stop button in the composer during the response.
Tested against local branch in chatkit studio with the stop UX enabled:
stop-mid-message.mov
stop-mid-workflow.mov
stop-after-progress-update.mov

@jiwon-oai jiwon-oai marked this pull request as ready for review November 22, 2025 06:01
Comment on lines 318 to 334
async def handle_stream_cancelled(
self,
thread: ThreadMetadata,
pending_items: list[ThreadItem],
context: TContext,
):
"""Perform custom cleanup / stop inference when a stream is cancelled.
Args:
thread: The thread that was being processed.
pending_items: Items that were not done streaming at cancellation time.
By default, already-streamed assistant messages, widgets, and workflows are
saved to the store during error handling prior to this method being called.
If you want to remove them from the thread, you can do so here.
(Updates you make here will not be reflected in the UI until a reload.)
context: Arbitrary per-request context provided by the caller.
"""
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New overridable ChatKitServer method. Not sure if pending_items is the right param name - they include items that were automatically persisted on cancel.

Comment on lines 664 to 670
case ThreadItemUpdatedEvent():
# Keep the pending assistant message item up to date
# so that we can persist already-streamed partial content
# if the stream is cancelled.
self._update_pending_assistant_message_items(
pending_items, event
)
Copy link
Collaborator Author

@jiwon-oai jiwon-oai Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only applying assistant message items updates to keep the default case simple.

@weedon-openai wdyt:

  • Should we apply updates for WorkflowItem and WidgetItem as well?
  • Or.. since if we're not applying updates for WorkflowItem and WidgetItem automatically, should we also skip the auto-saving on cancel for these two item types?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm for the workflow item, we probably have the current workflow in memory and could just save it as is right? Technically the integration doesn't have to use our workflow utils though 🤔

Regardless, workflow deltas are really easy to apply.

Widget deltas are tricky. I kind of think we should apply them, but I'm not sure how difficult that is. Otherwise we should probably delete / revert the widget (widget streaming can happen in a response other than the one that created with widget).

Copy link
Collaborator Author

@jiwon-oai jiwon-oai Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

widget streaming can happen in a response other than the one that created with widget

Ahhh right good point!

Otherwise we should probably delete / revert the widget

Okay I'm leaning towards not auto-saving the widget at all and leaving the integration to handle it themselves in the overridable method.

Comment on lines 664 to 670
case ThreadItemUpdatedEvent():
# Keep the pending assistant message item up to date
# so that we can persist already-streamed partial content
# if the stream is cancelled.
self._update_pending_assistant_message_items(
pending_items, event
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm for the workflow item, we probably have the current workflow in memory and could just save it as is right? Technically the integration doesn't have to use our workflow utils though 🤔

Regardless, workflow deltas are really easy to apply.

Widget deltas are tricky. I kind of think we should apply them, but I'm not sure how difficult that is. Otherwise we should probably delete / revert the widget (widget streaming can happen in a response other than the one that created with widget).

Comment on lines 739 to 744
HiddenContextItem(
thread_id=thread.id,
created_at=datetime.now(),
id=self.store.generate_item_id("hidden_context", thread, context),
content="SYSTEM: The user cancelled the stream.",
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But what if they always use HCI with data shaped content and this breaks their converter.

Maybe HCI needs a new optional field message? Kinda cleaner than the current automatic parser approach that has to sniff the type of content.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good point 🤔

I'm leaning towards not adding anything and instead urging the integration to write a hidden context item in their overridable method.

I also considered whether using EndOfTurnItem would make sense here.. but it might break the UI for cases where the integration decides to append some items after the stream is stopped.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 I think we should probably leave it to the integration but update all our examples with a minimal implementation of this method.

@jiwon-oai jiwon-oai added this pull request to the merge queue Nov 25, 2025
Merged via the queue into main with commit 675ded1 Nov 25, 2025
1 check passed
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.

3 participants