-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Description
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
- Workflow calls
ctx.request_info()and pauses - DevUI displays a popup for user input
- User submits response
- DevUI resumes the workflow from the checkpoint
@response_handleris invoked with the user's response- Workflow continues from where it paused
Actual Behavior
- Workflow calls
ctx.request_info()and pauses ✅ - DevUI displays a popup for user input ✅
- User submits response ✅
- DevUI starts a fresh workflow execution ❌
@response_handleris never invoked ❌- Workflow restarts from the beginning ❌
Steps to Reproduce
- Create a workflow with an executor that uses
ctx.request_info()with a@response_handler - Run the workflow in DevUI (
uv run devui) - Trigger the workflow and wait for the
request_infopopup - Submit a response through the DevUI popup
- 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:
- ✅ Workflow pauses correctly after
request_infois called - ❌ When user submits answer, DevUI treats it as new workflow input instead of a response
- ❌ Workflow restarts from the beginning (
prepare_dataruns again) - ❌ No logs from
@response_handler- it's never invoked - ❌ The response data (
{"answer":"Cheese"}) is passed as input toprepare_datainstead of being processed byresponse_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
@handlerwithctx.request_info() - Both use
@response_handlerto process responses - Both use Pydantic models for
response_type - Both store state in
executor_statefor use inresponse_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
RequestInfoEventis detected and mapped correctly by DevUI - The issue occurs specifically when submitting responses through DevUI
- The workflow structure matches the working
spam_workflowexample - 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
Type
Projects
Status