From 5910a6f86947b80b6abe0a6752e00d18c144632d Mon Sep 17 00:00:00 2001 From: Tao Chen Date: Mon, 8 Jun 2026 14:57:01 -0700 Subject: [PATCH] Add reset to hosted workflow --- .../_responses.py | 6 ++ .../foundry_hosting/tests/test_responses.py | 75 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py index ec4a9d8533..498879861b 100644 --- a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py +++ b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py @@ -649,6 +649,12 @@ class ResponsesHostServer(ResponsesAgentServerHost): checkpoint_id=latest_checkpoint_id, checkpoint_storage=restore_storage, ) + else: + # We reset the workflow if neither conversation_id nor previous_response_id + # was supplied, because this implies there's no prior state to restore and + # we want to ensure a clean slate. Workflow may contain in-memory state that + # needs to be cleared on new conversations. + await self._agent.workflow.reset_for_new_run() # Now run the agent with the latest input response_event_stream = ResponseEventStream(response_id=context.response_id, model=request.model) diff --git a/python/packages/foundry_hosting/tests/test_responses.py b/python/packages/foundry_hosting/tests/test_responses.py index 8bc09c5d13..df3aec0d65 100644 --- a/python/packages/foundry_hosting/tests/test_responses.py +++ b/python/packages/foundry_hosting/tests/test_responses.py @@ -3971,5 +3971,80 @@ class TestWorkflowAgentHosting: assert len(approval_responses) == 1 assert approval_responses[0].approved is False # type: ignore[attr-defined] + async def test_workflow_is_reset_when_no_prior_conversation(self) -> None: + """Without ``previous_response_id`` or ``conversation_id`` the host must + reset the workflow so any in-memory state from a previous request is + cleared before the new turn runs.""" + workflow_agent = _build_text_workflow_agent("fresh") + server = _make_server(workflow_agent) + + with patch.object( + workflow_agent.workflow, + "reset_for_new_run", + new=AsyncMock(wraps=workflow_agent.workflow.reset_for_new_run), + ) as reset_spy: + resp = await _post(server, input_text="hi", stream=False) + + assert resp.status_code == 200 + assert reset_spy.await_count == 1 + + async def test_workflow_is_reset_across_independent_requests(self) -> None: + """Two consecutive requests without a chaining context id must each + reset the workflow.""" + workflow_agent = _build_text_workflow_agent("again") + server = _make_server(workflow_agent) + + with patch.object( + workflow_agent.workflow, + "reset_for_new_run", + new=AsyncMock(wraps=workflow_agent.workflow.reset_for_new_run), + ) as reset_spy: + first = await _post(server, input_text="hi", stream=False) + second = await _post(server, input_text="hi again", stream=False) + + assert first.status_code == 200 + assert second.status_code == 200 + assert reset_spy.await_count == 2 + + async def test_workflow_is_not_reset_when_resuming_from_checkpoint(self) -> None: + """When ``previous_response_id`` resolves to an existing workflow + checkpoint the host restores the checkpoint instead of resetting + the workflow.""" + workflow_agent, _ = _build_approval_workflow_agent( + approval_request_id="apr_no_reset", + final_text="resumed", + ) + server = _make_server(workflow_agent) + + first = await _post(server, stream=False) + assert first.status_code == 200 + first_body = first.json() + first_response_id = first_body["id"] + approval_request_id = next(it["id"] for it in first_body["output"] if it["type"] == "mcp_approval_request") + + with patch.object( + workflow_agent.workflow, + "reset_for_new_run", + new=AsyncMock(wraps=workflow_agent.workflow.reset_for_new_run), + ) as reset_spy: + second = await _post_json( + server, + { + "model": "test-model", + "input": [ + { + "type": "mcp_approval_response", + "approval_request_id": approval_request_id, + "approve": True, + } + ], + "stream": False, + "previous_response_id": first_response_id, + }, + ) + + assert second.status_code == 200 + assert reset_spy.await_count == 0 + # endregion