Skip to content

Python: Workflow Restarts Instead of Resuming After request_info Response #2276

@CodeHalwell

Description

@CodeHalwell

Description

When using request_info with a @response_handler in a workflow running in DevUI, submitting a response causes DevUI to start a fresh workflow execution instead of resuming the paused workflow. The @response_handler method is never invoked, and the workflow restarts from the beginning.

Expected Behavior

  1. Workflow calls ctx.request_info() and pauses
  2. DevUI displays a popup for user input
  3. User submits response
  4. DevUI resumes the workflow from the checkpoint
  5. @response_handler is invoked with the user's response
  6. Workflow continues from where it paused

Actual Behavior

  1. Workflow calls ctx.request_info() and pauses ✅
  2. DevUI displays a popup for user input ✅
  3. User submits response ✅
  4. DevUI starts a fresh workflow execution
  5. @response_handler is never invoked
  6. Workflow restarts from the beginning

Steps to Reproduce

  1. Create a workflow with an executor that uses ctx.request_info() with a @response_handler
  2. Run the workflow in DevUI (uv run devui)
  3. Trigger the workflow and wait for the request_info popup
  4. Submit a response through the DevUI popup
  5. Observe that the workflow restarts instead of continuing

Minimal Reproducible Example

from dataclasses import dataclass
from pydantic import BaseModel
from agent_framework import (
    WorkflowBuilder,
    Executor,
    WorkflowContext,
    executor,
    handler,
    response_handler,
)

@dataclass
class QuestionRequest:
    question: str

class AnswerResponse(BaseModel):
    answer: str

# Initial executor that prepares data
@executor(id="prepare_data")
async def prepare_data(input: str, ctx: WorkflowContext[str]) -> None:
    print("Preparing data...")
    processed_data = f"Processed: {input}"
    await ctx.send_message(processed_data)

# Intermediate executor that processes the prepared data
@executor(id="review_data")
async def review_data(data: str, ctx: WorkflowContext[str]) -> None:
    print("Reviewing data...")
    reviewed_data = f"Reviewed: {data}"
    await ctx.send_message(reviewed_data)

# Executor that asks questions via request_info
class QuestionExecutor(Executor):
    def __init__(self):
        super().__init__(id="question_executor")
    
    @handler
    async def ask_question(self, data: str, ctx: WorkflowContext[str]) -> None:
        print("Asking question...")
        request = QuestionRequest(question="What is your answer?")
        
        # Store data in executor state for use in response_handler
        await ctx.set_executor_state({"original_data": data})
        
        await ctx.request_info(
            request_data=request,
            response_type=AnswerResponse
        )
        print("request_info returned - this should only happen after response_handler")
    
    @response_handler
    async def process_answer(
        self,
        original_request: QuestionRequest,
        user_response: AnswerResponse,
        ctx: WorkflowContext[str]
    ) -> None:
        print("=" * 60)
        print("RESPONSE_HANDLER CALLED - This should appear but doesn't")
        print(f"Answer: {user_response.answer}")
        print("=" * 60)
        
        # Retrieve stored data
        executor_state = await ctx.get_executor_state() or {}
        original_data = executor_state.get("original_data", "")
        
        # Combine data with user answer
        result = f"{original_data} | User answered: {user_response.answer}"
        await ctx.send_message(result)

# Executor that processes the result after HITL
@executor(id="process_result")
async def process_result(result: str, ctx: WorkflowContext[str]) -> None:
    print(f"Processing result: {result}")
    final_result = f"Final: {result}"
    await ctx.send_message(final_result)

# Final executor that yields output
@executor(id="final_output")
async def final_output(output: str, ctx: WorkflowContext[None, str]) -> None:
    print(f"Final output: {output}")
    await ctx.yield_output(f"Workflow complete: {output}")

question_executor = QuestionExecutor()

# Build workflow with multiple edges (more realistic scenario)
# This mirrors a real-world workflow: prepare -> review -> HITL -> process -> output
workflow = (
    WorkflowBuilder(name="Test Workflow")
    .add_edge(prepare_data, review_data)
    .add_edge(review_data, question_executor)
    .add_edge(question_executor, process_result)
    .add_edge(process_result, final_output)
    .set_start_executor(prepare_data)
    .build()
)

Logs Showing the Issue

When running the test workflow (test_devui_hitl_bug), the bug is clearly visible:

First execution (workflow pauses correctly):

[prepare_data] Preparing data...
[prepare_data] Input: {"input":"Hello how are you"}
[prepare_data] Sending: Processed: {"input":"Hello how are you"}

[review_data] Reviewing data...
[review_data] Received: Processed: {"input":"Hello how are you"}
[review_data] Sending: Reviewed: Processed: {"input":"Hello how are you"}

[question_executor] Asking question...
[question_executor] Received data: Reviewed: Processed: {"input":"Hello how are you"}
[question_executor] Stored original_data in executor state
[question_executor] About to call ctx.request_info - workflow will pause

[question_executor] request_info returned - this should only happen after response_handler completes

After user submits answer "Cheese" (BUG OCCURS): 😂

[prepare_data] Preparing data...
[prepare_data] Input: [{"type":"message","content":[{"type":"workflow_hil_response","responses":{"b5875b0a-731c-4667-8b09-7a8010d20b69":{"answer":"Cheese"}}}]}]
[prepare_data] Sending: Processed: [{"type":"message","content":...}]

[review_data] Reviewing data...
[review_data] Received: Processed: [{"type":"message","content":...}]

[question_executor] Asking question...
[question_executor] About to call ctx.request_info - workflow will pause
[question_executor] request_info returned - this should only happen after response_handler completes

Key observations:

  1. ✅ Workflow pauses correctly after request_info is called
  2. ❌ When user submits answer, DevUI treats it as new workflow input instead of a response
  3. ❌ Workflow restarts from the beginning (prepare_data runs again)
  4. No logs from @response_handler - it's never invoked
  5. ❌ The response data ({"answer":"Cheese"}) is passed as input to prepare_data instead of being processed by response_handler

The RequestInfoEvent is detected correctly by DevUI, but when the user submits a response, DevUI starts a fresh workflow execution instead of resuming from the checkpoint and invoking the @response_handler.

Comparison with Working Example

The spam_workflow sample in agent-framework-samples/python/samples/getting_started/devui/spam_workflow/workflow.py uses the same pattern and reportedly works correctly. The structure is identical:

  • Both use @handler with ctx.request_info()
  • Both use @response_handler to process responses
  • Both use Pydantic models for response_type
  • Both store state in executor_state for use in response_handler

Environment

  • Framework: Microsoft Agent Framework
  • DevUI Version: agent-framework-devui>=1.0.0b251112.post1
  • Python Version: 3.13
  • OS: Windows 10/11
  • Workflow Pattern: Main workflow (not sub-workflow)

Additional Context

  • Checkpoints are being created correctly (logs show checkpoint creation)
  • The RequestInfoEvent is detected and mapped correctly by DevUI
  • The issue occurs specifically when submitting responses through DevUI
  • The workflow structure matches the working spam_workflow example
  • This appears to be a DevUI-specific issue, as the workflow code follows the documented patterns

Workaround

Currently, there is no known workaround. The workflow must be run outside DevUI (using the Python API directly) for request_info responses to work correctly.

Related Issues

  • None found (searched GitHub issues and public forums)

Impact

This prevents using human-in-the-loop workflows in DevUI, which is a core feature for interactive workflow development and testing.


Labels: bug, devui, human-in-the-loop, request_info, workflow-resume

Metadata

Metadata

Assignees

Labels

devuiDevUI-related itemspythonworkflowsRelated to Workflows in agent-framework

Type

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions