Skip to content

Commit 7248374

Browse files
committed
Moved model copy to pending item add time; add regression test
1 parent 811575b commit 7248374

2 files changed

Lines changed: 67 additions & 2 deletions

File tree

chatkit/server.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,9 @@ async def _process_events(
697697
with agents_sdk_user_agent_override():
698698
async for event in stream():
699699
if isinstance(event, ThreadItemAddedEvent):
700-
pending_items[event.item.id] = event.item
700+
# Stash an isolated copy in case we need to persist unfinished items
701+
# on cancellation; downstream handlers keep using the original event.item.
702+
pending_items[event.item.id] = event.item.model_copy(deep=True)
701703

702704
match event:
703705
case ThreadItemDoneEvent():
@@ -805,7 +807,6 @@ def _update_pending_items(
805807
event: ThreadItemUpdatedEvent,
806808
):
807809
updated_item = pending_items.get(event.item_id)
808-
updated_item = updated_item.model_copy(deep=True) if updated_item else None
809810
update = event.update
810811
match updated_item:
811812
case AssistantMessageItem():

tests/test_chatkit_server.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
AttachmentsDeleteReq,
3434
AttachmentUploadDescriptor,
3535
ClientToolCallItem,
36+
CustomTask,
3637
FeedbackKind,
3738
FileAttachment,
3839
ImageAttachment,
@@ -44,6 +45,7 @@
4445
LockedStatus,
4546
Page,
4647
ProgressUpdateEvent,
48+
ThoughtTask,
4749
Thread,
4850
ThreadAddClientToolOutputParams,
4951
ThreadAddUserMessageParams,
@@ -79,6 +81,9 @@
7981
UserMessageTextContent,
8082
WidgetItem,
8183
WidgetRootUpdated,
84+
Workflow,
85+
WorkflowItem,
86+
WorkflowTaskAdded,
8287
)
8388
from chatkit.widgets import Card, Text
8489
from tests._types import RequestContext
@@ -354,6 +359,65 @@ def generate_item_id(
354359
)
355360

356361

362+
async def test_workflow_task_not_duplicated_on_done_event():
363+
"""
364+
Regression test to make sure pending item updates do not modify the
365+
origin item that was streamed.
366+
"""
367+
368+
async def responder(
369+
thread: ThreadMetadata, input: UserMessageItem | None, context: Any
370+
) -> AsyncIterator[ThreadStreamEvent]:
371+
workflow_item = WorkflowItem(
372+
id="workflow-item",
373+
created_at=datetime.now(),
374+
thread_id=thread.id,
375+
workflow=Workflow(type="custom", tasks=[]),
376+
)
377+
378+
yield ThreadItemAddedEvent(item=workflow_item)
379+
380+
task = CustomTask(title="foo", content="bar")
381+
yield ThreadItemUpdatedEvent(
382+
item_id=workflow_item.id,
383+
update=WorkflowTaskAdded(task=task, task_index=0),
384+
)
385+
386+
workflow_item.workflow.tasks.append(task)
387+
yield ThreadItemDoneEvent(item=workflow_item)
388+
389+
with make_server(responder) as server:
390+
events = await server.process_streaming(
391+
ThreadsCreateReq(
392+
params=ThreadCreateParams(
393+
input=UserMessageInput(
394+
content=[UserMessageTextContent(text="Hello")],
395+
attachments=[],
396+
inference_options=InferenceOptions(),
397+
)
398+
)
399+
)
400+
)
401+
402+
thread = next(e.thread for e in events if e.type == "thread.created")
403+
workflow_done_event = next(
404+
e
405+
for e in events
406+
if isinstance(e, ThreadItemDoneEvent) and isinstance(e.item, WorkflowItem)
407+
)
408+
workflow_done_item = cast(WorkflowItem, workflow_done_event.item)
409+
assert len(workflow_done_item.workflow.tasks) == 1
410+
assert workflow_done_item.workflow.tasks[0].title == "foo"
411+
412+
stored = await server.store.load_item(
413+
thread.id, workflow_done_item.id, DEFAULT_CONTEXT
414+
)
415+
assert isinstance(stored, WorkflowItem)
416+
stored_workflow = cast(WorkflowItem, stored)
417+
assert len(stored_workflow.workflow.tasks) == 1
418+
assert stored_workflow.workflow.tasks[0].title == "foo"
419+
420+
357421
async def test_flows_context_to_responder():
358422
responder_context = None
359423
add_feedback_context = None

0 commit comments

Comments
 (0)