Address Tao's review on PR 5531

- Rename Workflow._run_workflow_with_tracing parameter
  is_fresh_message_run -> is_continuation (default False, inverted).
  Fresh-message turns reset per-run accounting; continuations
  (checkpoint restores, responses replays) preserve it.
- Simplify the in-flight-messages guard: _validate_run_params already
  enforces that 'message' is mutually exclusive with 'checkpoint_id'
  and 'responses', so the additional checks were dead code.
- foundry_hosting _responses: move the restore-only pre-pass above
  emit_created/emit_in_progress; restore is preparation, not run
  progress. Drop the skip-restore gate (state preservation requires
  unconditional restore) and instead clear agent.pending_requests
  after the restore-only call. Collapse over-conditioned check.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
alliscode
2026-04-28 14:02:53 -07:00
Unverified
parent d34a4834eb
commit 31a0011063
2 changed files with 44 additions and 53 deletions
@@ -284,33 +284,12 @@ class ResponsesHostServer(ResponsesAgentServerHost):
# a fresh run.
latest_checkpoint_id: str | None = None
restore_storage: FileCheckpointStorage | None = None
latest_checkpoint = None
if context_id is not None:
restore_storage = FileCheckpointStorage(os.path.join(self._checkpoint_storage_path, context_id))
latest_checkpoint = await restore_storage.get_latest(workflow_name=self._agent.workflow.name)
if latest_checkpoint is not None:
latest_checkpoint_id = latest_checkpoint.checkpoint_id
# If the latest checkpoint represents a workflow that was idle with
# pending request_info events (human-in-the-loop interrupts), the
# restore-only pre-pass below would replay those events through
# ``WorkflowAgent._convert_workflow_event_to_agent_response_updates``,
# populating ``self._agent.pending_requests``. The subsequent
# ``run(input_messages, ...)`` call would then route through
# :meth:`WorkflowAgent._process_pending_requests`, which expects
# function-response content and rejects plain text input. The host
# currently does not support resuming workflows with outstanding
# request_info via plain-text user turns, so in that scenario we
# skip the restore-only pre-pass and start a fresh turn. State
# accumulated by purely state-preserving workflows (no request_info)
# is unaffected.
skip_restore_due_to_pending_requests = bool(
latest_checkpoint is not None and latest_checkpoint.pending_request_info_events
)
# Now run the agent with the latest input
response_event_stream = ResponseEventStream(response_id=context.response_id, model=request.model)
# Storage that will receive checkpoints written during this turn.
# When the caller chains with previous_response_id, the next turn
# will reference the current response_id as its previous_response_id,
@@ -322,9 +301,6 @@ class ResponsesHostServer(ResponsesAgentServerHost):
write_context_id = context.conversation_id or context.response_id
write_storage = FileCheckpointStorage(os.path.join(self._checkpoint_storage_path, write_context_id))
yield response_event_stream.emit_created()
yield response_event_stream.emit_in_progress()
# Multi-turn pattern: when we have a prior checkpoint, restore it
# first (drive the workflow back to idle with prior state intact),
# then make a separate call that delivers the new user input. This
@@ -332,11 +308,20 @@ class ResponsesHostServer(ResponsesAgentServerHost):
# restore-only call may yield events from any pending in-flight
# work in the checkpoint; we consume those internally here so they
# don't surface to the response stream as duplicates.
if (
latest_checkpoint_id is not None
and restore_storage is not None
and not skip_restore_due_to_pending_requests
):
#
# If the restored checkpoint had pending request_info events, the
# restore-only call replays them through
# ``WorkflowAgent._convert_workflow_event_to_agent_response_updates``
# and populates ``self._agent.pending_requests``. That would route
# the subsequent ``run(input_messages, ...)`` call through
# :meth:`WorkflowAgent._process_pending_requests`, which expects
# function-response content and rejects plain text. The host's
# contract for plain-text user input is "treat as new turn", not
# "respond to outstanding request_info", so we clear
# ``pending_requests`` after the restore-only pre-pass. State
# (Conversation.*, Inputs.*, Local.*) lives in the workflow
# checkpoint and is unaffected by this clear.
if latest_checkpoint_id is not None:
if is_streaming_request:
async for _ in self._agent.run(
stream=True,
@@ -350,6 +335,13 @@ class ResponsesHostServer(ResponsesAgentServerHost):
checkpoint_id=latest_checkpoint_id,
checkpoint_storage=restore_storage,
)
self._agent.pending_requests.clear()
# Now run the agent with the latest input
response_event_stream = ResponseEventStream(response_id=context.response_id, model=request.model)
yield response_event_stream.emit_created()
yield response_event_stream.emit_in_progress()
if not is_streaming_request:
# Run the agent in non-streaming mode with the new user input.