mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
[BREAKING] Python: Refactor workflow events to unified discriminated union pattern (#3690)
* Refactor events * Merge main * Fixes * Cleanup * Update samples and tests * Remove unused imports * PR feedback * Merge main. Add properties for events to help typing * Formatting * Cleanup * use builtins.type to avoid shadowing by WorkflowEvent.type attribute * Final improvements
This commit is contained in:
committed by
GitHub
Unverified
parent
09f59b21ad
commit
0f3f4dbcaf
@@ -249,9 +249,9 @@ Given that DevUI offers an OpenAI Responses API, it internally maps messages and
|
||||
| `response.created` + `response.in_progress` | `AgentStartedEvent` | OpenAI |
|
||||
| `response.completed` | `AgentCompletedEvent` | OpenAI |
|
||||
| `response.failed` | `AgentFailedEvent` | OpenAI |
|
||||
| `response.created` + `response.in_progress` | `WorkflowStartedEvent` | OpenAI |
|
||||
| `response.completed` | `WorkflowCompletedEvent` | OpenAI |
|
||||
| `response.failed` | `WorkflowFailedEvent` | OpenAI |
|
||||
| `response.created` + `response.in_progress` | `WorkflowEvent (type='started')` | OpenAI |
|
||||
| `response.completed` | `WorkflowEvent (type='status')` | OpenAI |
|
||||
| `response.failed` | `WorkflowEvent (type='failed')` | OpenAI |
|
||||
| | **Content Types** | |
|
||||
| `response.content_part.added` + `response.output_text.delta` | `TextContent` | OpenAI |
|
||||
| `response.reasoning_text.delta` | `TextReasoningContent` | OpenAI |
|
||||
@@ -267,13 +267,13 @@ Given that DevUI offers an OpenAI Responses API, it internally maps messages and
|
||||
| `error` | `ErrorContent` | OpenAI |
|
||||
| Final `Response.usage` field (not streamed) | `UsageContent` | OpenAI |
|
||||
| | **Workflow Events** | |
|
||||
| `response.output_item.added` (ExecutorActionItem)* | `ExecutorInvokedEvent` | OpenAI |
|
||||
| `response.output_item.done` (ExecutorActionItem)* | `ExecutorCompletedEvent` | OpenAI |
|
||||
| `response.output_item.done` (ExecutorActionItem with error)* | `ExecutorFailedEvent` | OpenAI |
|
||||
| `response.output_item.added` (ResponseOutputMessage) | `WorkflowOutputEvent` | OpenAI |
|
||||
| `response.workflow_event.complete` | `WorkflowEvent` (other) | DevUI |
|
||||
| `response.trace.complete` | `WorkflowStatusEvent` | DevUI |
|
||||
| `response.trace.complete` | `WorkflowWarningEvent` | DevUI |
|
||||
| `response.output_item.added` (ExecutorActionItem)* | `WorkflowEvent (type='executor_invoked')` | OpenAI |
|
||||
| `response.output_item.done` (ExecutorActionItem)* | `WorkflowEvent (type='executor_completed')` | OpenAI |
|
||||
| `response.output_item.done` (ExecutorActionItem with error)* | `WorkflowEvent (type='executor_failed')` | OpenAI |
|
||||
| `response.output_item.added` (ResponseOutputMessage) | `WorkflowEvent (type='output')` | OpenAI |
|
||||
| `response.workflow_event.complete` | `WorkflowEvent` (other types) | DevUI |
|
||||
| `response.trace.complete` | `WorkflowEvent (type='status')` | DevUI |
|
||||
| `response.trace.complete` | `WorkflowEvent (type='warning')` | DevUI |
|
||||
| | **Trace Content** | |
|
||||
| `response.trace.complete` | `DataContent` (no data/errors) | DevUI |
|
||||
| `response.trace.complete` | `UriContent` (unsupported MIME) | DevUI |
|
||||
|
||||
@@ -7,8 +7,7 @@ import logging
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import AgentProtocol, Content
|
||||
from agent_framework._workflows._events import RequestInfoEvent
|
||||
from agent_framework import AgentProtocol, Content, Workflow
|
||||
|
||||
from ._conversations import ConversationStore, InMemoryConversationStore
|
||||
from ._discovery import EntityDiscovery
|
||||
@@ -262,10 +261,11 @@ class AgentFrameworkExecutor:
|
||||
yield event
|
||||
elif entity_info.type == "workflow":
|
||||
async for event in self._execute_workflow(entity_obj, request, trace_collector):
|
||||
# Log RequestInfoEvent for debugging HIL flow
|
||||
event_class = event.__class__.__name__ if hasattr(event, "__class__") else type(event).__name__
|
||||
if event_class == "RequestInfoEvent":
|
||||
logger.info("🔔 [EXECUTOR] RequestInfoEvent detected from workflow!")
|
||||
# Log request_info event (type='request_info') for debugging HIL flow
|
||||
if event.type == "request_info":
|
||||
logger.info(
|
||||
"🔔 [EXECUTOR] request_info event (type='request_info') detected from workflow!"
|
||||
)
|
||||
logger.info(f" request_id: {getattr(event, 'request_id', 'N/A')}")
|
||||
logger.info(f" source_executor_id: {getattr(event, 'source_executor_id', 'N/A')}")
|
||||
logger.info(f" request_type: {getattr(event, 'request_type', 'N/A')}")
|
||||
@@ -360,7 +360,7 @@ class AgentFrameworkExecutor:
|
||||
yield {"type": "error", "message": f"Agent execution error: {e!s}"}
|
||||
|
||||
async def _execute_workflow(
|
||||
self, workflow: Any, request: AgentFrameworkRequest, trace_collector: Any
|
||||
self, workflow: Workflow, request: AgentFrameworkRequest, trace_collector: Any
|
||||
) -> AsyncGenerator[Any, None]:
|
||||
"""Execute Agent Framework workflow with checkpoint support via conversation items.
|
||||
|
||||
@@ -515,8 +515,9 @@ class AgentFrameworkExecutor:
|
||||
logger.warning(f"Could not convert HIL responses to proper types: {e}")
|
||||
|
||||
async for event in workflow.send_responses_streaming(hil_responses):
|
||||
# Enrich new RequestInfoEvents that may come from subsequent HIL requests
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
# Enrich new request_info events (type='request_info')
|
||||
# that may come from subsequent HIL requests
|
||||
if event.type == "request_info":
|
||||
self._enrich_request_info_event_with_response_schema(event, workflow)
|
||||
|
||||
for trace_event in trace_collector.get_pending_events():
|
||||
@@ -538,7 +539,7 @@ class AgentFrameworkExecutor:
|
||||
checkpoint_id=checkpoint_id,
|
||||
checkpoint_storage=checkpoint_storage,
|
||||
):
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
if event.type == "request_info":
|
||||
self._enrich_request_info_event_with_response_schema(event, workflow)
|
||||
|
||||
for trace_event in trace_collector.get_pending_events():
|
||||
@@ -546,7 +547,7 @@ class AgentFrameworkExecutor:
|
||||
|
||||
yield event
|
||||
|
||||
# Note: Removed break on RequestInfoEvent - continue yielding all events
|
||||
# Note: Removed break on request_info event (type='request_info') - continue yielding all events
|
||||
# The workflow is already paused by ctx.request_info() in the framework
|
||||
# DevUI should continue yielding events even during HIL pause
|
||||
|
||||
@@ -562,7 +563,7 @@ class AgentFrameworkExecutor:
|
||||
parsed_input = await self._parse_workflow_input(workflow, request.input)
|
||||
|
||||
async for event in workflow.run(parsed_input, stream=True, checkpoint_storage=checkpoint_storage):
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
if event.type == "request_info":
|
||||
self._enrich_request_info_event_with_response_schema(event, workflow)
|
||||
|
||||
for trace_event in trace_collector.get_pending_events():
|
||||
@@ -570,7 +571,7 @@ class AgentFrameworkExecutor:
|
||||
|
||||
yield event
|
||||
|
||||
# Note: Removed break on RequestInfoEvent - continue yielding all events
|
||||
# Note: Removed break on request_info event (type='request_info') - continue yielding all events
|
||||
# The workflow is already paused by ctx.request_info() in the framework
|
||||
# DevUI should continue yielding events even during HIL pause
|
||||
|
||||
@@ -1015,10 +1016,12 @@ class AgentFrameworkExecutor:
|
||||
return raw_input
|
||||
|
||||
def _enrich_request_info_event_with_response_schema(self, event: Any, workflow: Any) -> None:
|
||||
"""Extract response type from workflow executor and attach response schema to RequestInfoEvent.
|
||||
"""Extract response type from workflow executor.
|
||||
|
||||
Attach response schema to request_info event (type='request_info').
|
||||
|
||||
Args:
|
||||
event: RequestInfoEvent to enrich
|
||||
event: request_info event (type='request_info') to enrich
|
||||
workflow: Workflow object containing executors
|
||||
"""
|
||||
try:
|
||||
@@ -1029,7 +1032,7 @@ class AgentFrameworkExecutor:
|
||||
request_type = getattr(event, "request_type", None)
|
||||
|
||||
if not source_executor_id or not request_type:
|
||||
logger.debug("RequestInfoEvent missing source_executor_id or request_type")
|
||||
logger.debug("request_info event (type='request_info') missing source_executor_id or request_type")
|
||||
return
|
||||
|
||||
# Find the source executor in the workflow
|
||||
@@ -1062,4 +1065,4 @@ class AgentFrameworkExecutor:
|
||||
event._response_schema = response_schema
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to enrich RequestInfoEvent with response schema: {e}")
|
||||
logger.warning(f"Failed to enrich request_info event (type='request_info') with response schema: {e}")
|
||||
|
||||
@@ -12,7 +12,7 @@ from datetime import datetime
|
||||
from typing import Any, Union
|
||||
from uuid import uuid4
|
||||
|
||||
from agent_framework import ChatMessage, Content, WorkflowOutputEvent
|
||||
from agent_framework import ChatMessage, Content
|
||||
from openai.types.responses import (
|
||||
Response,
|
||||
ResponseContentPartAddedEvent,
|
||||
@@ -180,16 +180,18 @@ class MessageMapper:
|
||||
try:
|
||||
from agent_framework import AgentResponse, AgentResponseUpdate, WorkflowEvent
|
||||
|
||||
# Handle AgentRunUpdateEvent - workflow event wrapping AgentResponseUpdate
|
||||
# Handle WorkflowEvent with type='output' or 'data' wrapping AgentResponseUpdate
|
||||
# This must be checked BEFORE generic WorkflowEvent check
|
||||
if isinstance(raw_event, WorkflowOutputEvent):
|
||||
# Extract the AgentResponseUpdate from the event's data attribute
|
||||
if raw_event.data and isinstance(raw_event.data, AgentResponseUpdate):
|
||||
# Preserve executor_id in context for proper output routing
|
||||
context["current_executor_id"] = raw_event.executor_id
|
||||
return await self._convert_agent_update(raw_event.data, context)
|
||||
# If no data, treat as generic workflow event
|
||||
return await self._convert_workflow_event(raw_event, context)
|
||||
# Note: AgentExecutor uses type='output' for streaming updates
|
||||
if (
|
||||
isinstance(raw_event, WorkflowEvent)
|
||||
and raw_event.type in ("output", "data")
|
||||
and raw_event.data
|
||||
and isinstance(raw_event.data, AgentResponseUpdate)
|
||||
):
|
||||
# Preserve executor_id in context for proper output routing
|
||||
context["current_executor_id"] = raw_event.executor_id
|
||||
return await self._convert_agent_update(raw_event.data, context)
|
||||
|
||||
# Handle complete agent response (AgentResponse) - for non-streaming agent execution
|
||||
if isinstance(raw_event, AgentResponse):
|
||||
@@ -824,10 +826,12 @@ class MessageMapper:
|
||||
List of OpenAI response stream events
|
||||
"""
|
||||
try:
|
||||
event_class = event.__class__.__name__
|
||||
# Use event.type for discriminated union pattern (similar to Content class)
|
||||
event_type = getattr(event, "type", None)
|
||||
event_class = event.__class__.__name__ # Fallback for non-workflow events
|
||||
|
||||
# Response-level events - construct proper OpenAI objects
|
||||
if event_class == "WorkflowStartedEvent":
|
||||
if event_type == "started":
|
||||
workflow_id = getattr(event, "workflow_id", str(uuid4()))
|
||||
context["workflow_id"] = workflow_id
|
||||
|
||||
@@ -871,8 +875,8 @@ class MessageMapper:
|
||||
|
||||
return events
|
||||
|
||||
# Handle WorkflowOutputEvent separately to preserve output data
|
||||
if event_class == "WorkflowOutputEvent":
|
||||
# Handle output events separately to preserve output data
|
||||
if event_type == "output":
|
||||
output_data = getattr(event, "data", None)
|
||||
executor_id = getattr(event, "executor_id", "unknown")
|
||||
|
||||
@@ -934,7 +938,7 @@ class MessageMapper:
|
||||
|
||||
# Emit output_item.added for each yield_output
|
||||
logger.debug(
|
||||
f"WorkflowOutputEvent converted to output_item.added "
|
||||
f"output event (type='output') converted to output_item.added "
|
||||
f"(executor: {executor_id}, length: {len(text)})"
|
||||
)
|
||||
return [
|
||||
@@ -946,15 +950,15 @@ class MessageMapper:
|
||||
)
|
||||
]
|
||||
|
||||
# Handle WorkflowCompletedEvent - Don't emit response.completed here
|
||||
# Handle completed event - Don't emit response.completed here
|
||||
# The server will emit a proper one with usage data after aggregating all events
|
||||
if event_class == "WorkflowCompletedEvent":
|
||||
if event_type == "completed":
|
||||
return []
|
||||
|
||||
if event_class == "WorkflowFailedEvent":
|
||||
if event_type == "failed":
|
||||
workflow_id = context.get("workflow_id", str(uuid4()))
|
||||
# WorkflowFailedEvent uses 'details' field (WorkflowErrorDetails), not 'error'
|
||||
# This matches ExecutorFailedEvent which also uses 'details'
|
||||
# failed event (type='failed') uses 'details' field (WorkflowErrorDetails), not 'error'
|
||||
# This matches executor_failed event which also uses 'details'
|
||||
details = getattr(event, "details", None)
|
||||
|
||||
# Import Response and ResponseError types
|
||||
@@ -1000,7 +1004,8 @@ class MessageMapper:
|
||||
]
|
||||
|
||||
# Executor-level events (output items)
|
||||
if event_class == "ExecutorInvokedEvent":
|
||||
# Check for executor lifecycle events via event.type
|
||||
if event_type == "executor_invoked":
|
||||
executor_id = getattr(event, "executor_id", "unknown")
|
||||
item_id = f"exec_{executor_id}_{uuid4().hex[:8]}"
|
||||
context[f"exec_item_{executor_id}"] = item_id
|
||||
@@ -1029,7 +1034,7 @@ class MessageMapper:
|
||||
)
|
||||
]
|
||||
|
||||
if event_class == "ExecutorCompletedEvent":
|
||||
if event_type == "executor_completed":
|
||||
executor_id = getattr(event, "executor_id", "unknown")
|
||||
item_id = context.get(f"exec_item_{executor_id}", f"exec_{executor_id}_unknown")
|
||||
|
||||
@@ -1038,7 +1043,7 @@ class MessageMapper:
|
||||
context.pop("current_executor_id", None)
|
||||
|
||||
# Create ExecutorActionItem with completed status
|
||||
# ExecutorCompletedEvent uses 'data' field, not 'result'
|
||||
# executor_completed event (type='executor_completed') uses 'data' field, not 'result'
|
||||
# Serialize the result data to ensure it's JSON-serializable
|
||||
# (AgentExecutorResponse contains AgentResponse/ChatMessage which are SerializationMixin)
|
||||
raw_result = getattr(event, "data", None)
|
||||
@@ -1061,10 +1066,11 @@ class MessageMapper:
|
||||
)
|
||||
]
|
||||
|
||||
if event_class == "ExecutorFailedEvent":
|
||||
if event_type == "executor_failed":
|
||||
executor_id = getattr(event, "executor_id", "unknown")
|
||||
item_id = context.get(f"exec_item_{executor_id}", f"exec_{executor_id}_unknown")
|
||||
# ExecutorFailedEvent uses 'details' field (WorkflowErrorDetails), not 'error'
|
||||
# executor_failed event (type='executor_failed') uses 'details' property (WorkflowErrorDetails)
|
||||
# not 'error'. This matches WorkflowEvent.details which returns self.data for executor_failed type
|
||||
details = getattr(event, "details", None)
|
||||
if details:
|
||||
err_msg = getattr(details, "message", None) or str(details)
|
||||
@@ -1093,8 +1099,8 @@ class MessageMapper:
|
||||
)
|
||||
]
|
||||
|
||||
# Handle RequestInfoEvent specially - emit as HIL event with schema
|
||||
if event_class == "RequestInfoEvent":
|
||||
# Handle request_info events specially - emit as HIL event with schema
|
||||
if event_type == "request_info":
|
||||
from .models._openai_custom import ResponseRequestInfoEvent
|
||||
|
||||
request_id = getattr(event, "request_id", "")
|
||||
@@ -1102,7 +1108,7 @@ class MessageMapper:
|
||||
request_type_class = getattr(event, "request_type", None)
|
||||
request_data = getattr(event, "data", None)
|
||||
|
||||
logger.info("📨 [MAPPER] Processing RequestInfoEvent")
|
||||
logger.info("📨 [MAPPER] Processing request_info event (type='request_info')")
|
||||
logger.info(f" request_id: {request_id}")
|
||||
logger.info(f" source_executor_id: {source_executor_id}")
|
||||
logger.info(f" request_type_class: {request_type_class}")
|
||||
@@ -1163,26 +1169,23 @@ class MessageMapper:
|
||||
return [hil_event]
|
||||
|
||||
# Handle other informational workflow events (status, warnings, errors)
|
||||
if event_class in ["WorkflowStatusEvent", "WorkflowWarningEvent", "WorkflowErrorEvent"]:
|
||||
if event_type in ["status", "warning", "error"]:
|
||||
# These are informational events that don't map to OpenAI lifecycle events
|
||||
# Convert them to trace events for debugging visibility
|
||||
event_data: dict[str, Any] = {}
|
||||
|
||||
# Extract relevant data based on event type
|
||||
if event_class == "WorkflowStatusEvent":
|
||||
if event_type == "status":
|
||||
event_data["state"] = str(getattr(event, "state", "unknown"))
|
||||
elif event_class == "WorkflowWarningEvent":
|
||||
event_data["message"] = str(getattr(event, "message", ""))
|
||||
elif event_class == "WorkflowErrorEvent":
|
||||
event_data["message"] = str(getattr(event, "message", ""))
|
||||
event_data["error"] = str(getattr(event, "error", ""))
|
||||
elif event_type == "warning" or event_type == "error":
|
||||
event_data["message"] = str(getattr(event, "data", ""))
|
||||
|
||||
# Create a trace event for debugging
|
||||
trace_event = ResponseTraceEventComplete(
|
||||
type="response.trace.completed",
|
||||
data={
|
||||
"trace_type": "workflow_info",
|
||||
"event_type": event_class,
|
||||
"event_type": event_type,
|
||||
"data": event_data,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
},
|
||||
|
||||
@@ -32,10 +32,8 @@ from agent_framework import (
|
||||
from agent_framework._clients import TOptions_co
|
||||
from agent_framework._workflows._agent_executor import AgentExecutorResponse
|
||||
from agent_framework._workflows._events import (
|
||||
ExecutorCompletedEvent,
|
||||
ExecutorFailedEvent,
|
||||
ExecutorInvokedEvent,
|
||||
WorkflowErrorDetails,
|
||||
WorkflowEvent,
|
||||
)
|
||||
from agent_framework.orchestrations import ConcurrentBuilder, SequentialBuilder
|
||||
|
||||
@@ -284,7 +282,8 @@ def _create_agent_executor_response(
|
||||
executor_id: str = "test_executor",
|
||||
response_text: str = "Executor response",
|
||||
) -> AgentExecutorResponse:
|
||||
"""Create an AgentExecutorResponse - the type that's nested in ExecutorCompletedEvent.data."""
|
||||
"""Create an AgentExecutorResponse - the type that's nested in
|
||||
executor_completed event (type='executor_completed').data."""
|
||||
agent_response = _create_agent_run_response(response_text)
|
||||
return AgentExecutorResponse(
|
||||
executor_id=executor_id,
|
||||
@@ -306,32 +305,32 @@ def create_agent_run_response(text: str = "Test response") -> AgentResponse:
|
||||
return _create_agent_run_response(text)
|
||||
|
||||
|
||||
def create_executor_invoked_event(executor_id: str = "test_executor") -> ExecutorInvokedEvent:
|
||||
"""Create an ExecutorInvokedEvent."""
|
||||
return ExecutorInvokedEvent(executor_id=executor_id)
|
||||
def create_executor_invoked_event(executor_id: str = "test_executor") -> WorkflowEvent[Any]:
|
||||
"""Create a WorkflowEvent(type='executor_invoked')."""
|
||||
return WorkflowEvent.executor_invoked(executor_id=executor_id)
|
||||
|
||||
|
||||
def create_executor_completed_event(
|
||||
executor_id: str = "test_executor",
|
||||
with_agent_response: bool = True,
|
||||
) -> ExecutorCompletedEvent:
|
||||
"""Create an ExecutorCompletedEvent with realistic nested data.
|
||||
) -> WorkflowEvent[Any]:
|
||||
"""Create a WorkflowEvent(type='executor_completed') with realistic nested data.
|
||||
|
||||
This creates the exact data structure that caused the serialization bug:
|
||||
ExecutorCompletedEvent.data contains AgentExecutorResponse which contains
|
||||
WorkflowEvent.data contains AgentExecutorResponse which contains
|
||||
AgentResponse and ChatMessage objects (SerializationMixin, not Pydantic).
|
||||
"""
|
||||
data = _create_agent_executor_response(executor_id) if with_agent_response else {"simple": "dict"}
|
||||
return ExecutorCompletedEvent(executor_id=executor_id, data=data)
|
||||
return WorkflowEvent.executor_completed(executor_id=executor_id, data=data)
|
||||
|
||||
|
||||
def create_executor_failed_event(
|
||||
executor_id: str = "test_executor",
|
||||
error_message: str = "Test error",
|
||||
) -> ExecutorFailedEvent:
|
||||
"""Create an ExecutorFailedEvent."""
|
||||
) -> WorkflowEvent[WorkflowErrorDetails]:
|
||||
"""Create a WorkflowEvent(type='executor_failed')."""
|
||||
details = WorkflowErrorDetails(error_type="TestError", message=error_message)
|
||||
return ExecutorFailedEvent(executor_id=executor_id, details=details)
|
||||
return WorkflowEvent.executor_failed(executor_id=executor_id, details=details)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -386,28 +385,28 @@ def agent_run_response() -> AgentResponse:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def executor_completed_event() -> ExecutorCompletedEvent:
|
||||
"""Create an ExecutorCompletedEvent with realistic nested data.
|
||||
def executor_completed_event() -> WorkflowEvent[Any]:
|
||||
"""Create a WorkflowEvent(type='executor_completed') with realistic nested data.
|
||||
|
||||
This creates the exact data structure that caused the serialization bug:
|
||||
ExecutorCompletedEvent.data contains AgentExecutorResponse which contains
|
||||
executor_completed event (type='executor_completed').data contains AgentExecutorResponse which contains
|
||||
AgentResponse and ChatMessage objects (SerializationMixin, not Pydantic).
|
||||
"""
|
||||
data = _create_agent_executor_response("test_executor")
|
||||
return ExecutorCompletedEvent(executor_id="test_executor", data=data)
|
||||
return WorkflowEvent.executor_completed(executor_id="test_executor", data=data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def executor_invoked_event() -> ExecutorInvokedEvent:
|
||||
"""Create an ExecutorInvokedEvent."""
|
||||
return ExecutorInvokedEvent(executor_id="test_executor")
|
||||
def executor_invoked_event() -> WorkflowEvent[Any]:
|
||||
"""Create a WorkflowEvent(type='executor_invoked')."""
|
||||
return WorkflowEvent.executor_invoked(executor_id="test_executor")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def executor_failed_event() -> ExecutorFailedEvent:
|
||||
"""Create an ExecutorFailedEvent."""
|
||||
def executor_failed_event() -> WorkflowEvent[WorkflowErrorDetails]:
|
||||
"""Create a WorkflowEvent(type='executor_failed')."""
|
||||
details = WorkflowErrorDetails(error_type="TestError", message="Test error")
|
||||
return ExecutorFailedEvent(executor_id="test_executor", details=details)
|
||||
return WorkflowEvent.executor_failed(executor_id="test_executor", details=details)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -8,10 +8,8 @@ import pytest
|
||||
from agent_framework import (
|
||||
Executor,
|
||||
InMemoryCheckpointStorage,
|
||||
RequestInfoEvent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowStatusEvent,
|
||||
handler,
|
||||
response_handler,
|
||||
)
|
||||
@@ -428,13 +426,13 @@ class TestIntegration:
|
||||
# Run workflow until it reaches IDLE_WITH_PENDING_REQUESTS (after checkpoint is created)
|
||||
saw_request_event = False
|
||||
async for event in test_workflow.run(WorkflowTestData(value="test"), stream=True):
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
if event.type == "request_info":
|
||||
saw_request_event = True
|
||||
# Wait for IDLE_WITH_PENDING_REQUESTS status (comes after checkpoint creation)
|
||||
if isinstance(event, WorkflowStatusEvent) and "IDLE_WITH_PENDING_REQUESTS" in str(event.state):
|
||||
if event.type == "status" and "IDLE_WITH_PENDING_REQUESTS" in str(event.state):
|
||||
break
|
||||
|
||||
assert saw_request_event, "Test workflow should have emitted RequestInfoEvent"
|
||||
assert saw_request_event, "Test workflow should have emitted request_info event (type='request_info')"
|
||||
|
||||
# Verify checkpoint was AUTOMATICALLY saved to our storage by the framework
|
||||
checkpoints_after = await checkpoint_storage.list_checkpoints()
|
||||
|
||||
@@ -292,7 +292,7 @@ async def test_full_pipeline_workflow_events_are_json_serializable():
|
||||
"""CRITICAL TEST: Verify ALL events from workflow execution can be JSON serialized.
|
||||
|
||||
This is particularly important for workflows with AgentExecutor because:
|
||||
- AgentExecutor produces ExecutorCompletedEvent with AgentExecutorResponse
|
||||
- AgentExecutor produces executor_completed event (type='executor_completed') with AgentExecutorResponse
|
||||
- AgentExecutorResponse contains AgentResponse and ChatMessage objects
|
||||
- These are SerializationMixin objects, not Pydantic, which caused the original bug
|
||||
|
||||
@@ -672,10 +672,10 @@ async def test_full_pipeline_concurrent_workflow(concurrent_workflow):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_pipeline_workflow_output_event_serialization():
|
||||
"""Test that WorkflowOutputEvent from ctx.yield_output() serializes correctly.
|
||||
"""Test that output event (type='output') from ctx.yield_output() serializes correctly.
|
||||
|
||||
This tests the pattern where executors yield output via ctx.yield_output(),
|
||||
which emits WorkflowOutputEvent that DevUI must serialize for SSE.
|
||||
which emits output event (type='output') that DevUI must serialize for SSE.
|
||||
"""
|
||||
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
|
||||
|
||||
|
||||
@@ -19,9 +19,8 @@ from agent_framework._types import (
|
||||
|
||||
# Import real workflow event classes - NOT mocks!
|
||||
from agent_framework._workflows._events import (
|
||||
ExecutorCompletedEvent,
|
||||
WorkflowStartedEvent,
|
||||
WorkflowStatusEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowRunState,
|
||||
)
|
||||
|
||||
# Import factory functions from conftest for parameterized test data creation
|
||||
@@ -261,7 +260,7 @@ async def test_agent_run_response_mapping(mapper: MessageMapper, test_request: A
|
||||
|
||||
|
||||
async def test_executor_invoked_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test ExecutorInvokedEvent using the REAL class from agent_framework."""
|
||||
"""Test WorkflowEvent(type='executor_invoked') using the REAL class from agent_framework."""
|
||||
# Use real class, not mock!
|
||||
event = create_executor_invoked_event(executor_id="exec_123")
|
||||
|
||||
@@ -277,9 +276,9 @@ async def test_executor_invoked_event(mapper: MessageMapper, test_request: Agent
|
||||
|
||||
|
||||
async def test_executor_completed_event_simple_data(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test ExecutorCompletedEvent with simple dict data."""
|
||||
"""Test WorkflowEvent(type='executor_completed') with simple dict data."""
|
||||
# Create event with simple data
|
||||
event = ExecutorCompletedEvent(executor_id="exec_123", data={"simple": "result"})
|
||||
event = WorkflowEvent.executor_completed(executor_id="exec_123", data={"simple": "result"})
|
||||
|
||||
# First need to invoke the executor to set up context
|
||||
invoke_event = create_executor_invoked_event(executor_id="exec_123")
|
||||
@@ -301,10 +300,10 @@ async def test_executor_completed_event_simple_data(mapper: MessageMapper, test_
|
||||
async def test_executor_completed_event_with_agent_response(
|
||||
mapper: MessageMapper, test_request: AgentFrameworkRequest
|
||||
) -> None:
|
||||
"""Test ExecutorCompletedEvent with nested AgentExecutorResponse.
|
||||
"""Test WorkflowEvent(type='executor_completed') with nested AgentExecutorResponse.
|
||||
|
||||
This is a REGRESSION TEST for the serialization bug where
|
||||
ExecutorCompletedEvent.data contained AgentExecutorResponse with nested
|
||||
WorkflowEvent.data contained AgentExecutorResponse with nested
|
||||
AgentResponse and ChatMessage objects (SerializationMixin) that
|
||||
Pydantic couldn't serialize.
|
||||
"""
|
||||
@@ -374,7 +373,7 @@ async def test_executor_completed_event_serialization_to_json(
|
||||
|
||||
|
||||
async def test_executor_failed_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test ExecutorFailedEvent using the REAL class."""
|
||||
"""Test WorkflowEvent(type='executor_failed') using the REAL class."""
|
||||
# First invoke the executor
|
||||
invoke_event = create_executor_invoked_event(executor_id="exec_fail")
|
||||
await mapper.convert_event(invoke_event, test_request)
|
||||
@@ -398,22 +397,21 @@ async def test_executor_failed_event(mapper: MessageMapper, test_request: AgentF
|
||||
|
||||
|
||||
async def test_workflow_started_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowStartedEvent using the REAL class."""
|
||||
"""Test WorkflowEvent(type='started') using the REAL class."""
|
||||
|
||||
event = WorkflowStartedEvent(data=None)
|
||||
event = WorkflowEvent.started()
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# WorkflowStartedEvent should emit response.created and response.in_progress
|
||||
# WorkflowEvent(type='started') should emit response.created and response.in_progress
|
||||
assert len(events) == 2
|
||||
assert events[0].type == "response.created"
|
||||
assert events[1].type == "response.in_progress"
|
||||
|
||||
|
||||
async def test_workflow_status_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowStatusEvent using the REAL class."""
|
||||
from agent_framework._workflows._events import WorkflowRunState
|
||||
"""Test WorkflowEvent(type='status') using the REAL class."""
|
||||
|
||||
event = WorkflowStatusEvent(state=WorkflowRunState.IN_PROGRESS)
|
||||
event = WorkflowEvent.status(state=WorkflowRunState.IN_PROGRESS)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# Should emit some status-related event
|
||||
@@ -421,20 +419,20 @@ async def test_workflow_status_event(mapper: MessageMapper, test_request: AgentF
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Magentic Event Tests - Testing WorkflowOutputEvent with additional_properties
|
||||
# Magentic Event Tests - Testing WorkflowEvent[AgentResponseUpdate] with additional_properties
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def test_magentic_agent_run_update_event_with_agent_delta_metadata(
|
||||
async def test_magentic_executor_event_with_agent_delta_metadata(
|
||||
mapper: MessageMapper, test_request: AgentFrameworkRequest
|
||||
) -> None:
|
||||
"""Test that WorkflowOutputEvent with magentic_event_type='agent_delta' is handled correctly.
|
||||
"""Test that WorkflowEvent[AgentResponseUpdate] with magentic_event_type='agent_delta' is handled correctly.
|
||||
|
||||
This tests the ACTUAL event format Magentic emits - not a fake MagenticAgentDeltaEvent class.
|
||||
Magentic uses WorkflowOutputEvent wrapping AgentResponseUpdate with additional_properties.
|
||||
Magentic uses WorkflowEvent.emit() with additional_properties containing magentic_event_type.
|
||||
"""
|
||||
from agent_framework._types import AgentResponseUpdate
|
||||
from agent_framework._workflows._events import WorkflowOutputEvent
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
# Create the REAL event format that Magentic emits
|
||||
update = AgentResponseUpdate(
|
||||
@@ -446,11 +444,11 @@ async def test_magentic_agent_run_update_event_with_agent_delta_metadata(
|
||||
"agent_id": "writer_agent",
|
||||
},
|
||||
)
|
||||
event = WorkflowOutputEvent(executor_id="magentic_executor", data=update)
|
||||
event = WorkflowEvent.emit(executor_id="magentic_executor", data=update)
|
||||
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# Should be treated as a regular WorkflowOutputEvent with text content
|
||||
# Should be treated as a regular WorkflowEvent[AgentResponseUpdate] with text content
|
||||
# The mapper should emit text delta events
|
||||
assert len(events) >= 1
|
||||
text_events = [e for e in events if getattr(e, "type", "") == "response.output_text.delta"]
|
||||
@@ -459,13 +457,13 @@ async def test_magentic_agent_run_update_event_with_agent_delta_metadata(
|
||||
|
||||
|
||||
async def test_magentic_orchestrator_message_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test that WorkflowOutputEvent with magentic_event_type='orchestrator_message' is handled.
|
||||
"""Test that WorkflowEvent[AgentResponseUpdate] with magentic_event_type='orchestrator_message' is handled.
|
||||
|
||||
Magentic emits orchestrator planning/instruction messages using WorkflowOutputEvent
|
||||
wrapping AgentResponseUpdate with additional_properties.
|
||||
Magentic emits orchestrator planning/instruction messages using WorkflowEvent.emit()
|
||||
with additional_properties containing magentic_event_type='orchestrator_message'.
|
||||
"""
|
||||
from agent_framework._types import AgentResponseUpdate
|
||||
from agent_framework._workflows._events import WorkflowOutputEvent
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
# Create orchestrator message event (REAL format from Magentic)
|
||||
update = AgentResponseUpdate(
|
||||
@@ -478,11 +476,11 @@ async def test_magentic_orchestrator_message_event(mapper: MessageMapper, test_r
|
||||
"orchestrator_id": "magentic_orchestrator",
|
||||
},
|
||||
)
|
||||
event = WorkflowOutputEvent(executor_id="magentic_orchestrator", data=update)
|
||||
event = WorkflowEvent.emit(executor_id="magentic_orchestrator", data=update)
|
||||
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# Currently, mapper treats this as regular WorkflowOutputEvent (no special handling)
|
||||
# Currently, mapper treats this as regular WorkflowEvent[AgentResponseUpdate] (no special handling)
|
||||
# This test documents the current behavior
|
||||
assert len(events) >= 1
|
||||
text_events = [e for e in events if getattr(e, "type", "") == "response.output_text.delta"]
|
||||
@@ -493,15 +491,15 @@ async def test_magentic_orchestrator_message_event(mapper: MessageMapper, test_r
|
||||
async def test_magentic_events_use_same_event_class_as_other_workflows(
|
||||
mapper: MessageMapper, test_request: AgentFrameworkRequest
|
||||
) -> None:
|
||||
"""Verify Magentic uses the same WorkflowOutputEvent class as other workflows.
|
||||
"""Verify Magentic uses the same WorkflowEvent class as other workflows.
|
||||
|
||||
This test documents that Magentic does NOT define separate event classes like
|
||||
MagenticAgentDeltaEvent - it reuses WorkflowOutputEvent with metadata in
|
||||
MagenticAgentDeltaEvent - it reuses WorkflowEvent with metadata in
|
||||
additional_properties. Any mapper code checking for 'MagenticAgentDeltaEvent'
|
||||
class names is dead code.
|
||||
"""
|
||||
from agent_framework._types import AgentResponseUpdate
|
||||
from agent_framework._workflows._events import WorkflowOutputEvent
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
# Create events the way different workflows do it
|
||||
# 1. Regular workflow (no additional_properties)
|
||||
@@ -509,7 +507,7 @@ async def test_magentic_events_use_same_event_class_as_other_workflows(
|
||||
contents=[Content.from_text(text="Regular workflow response")],
|
||||
role="assistant",
|
||||
)
|
||||
regular_event = WorkflowOutputEvent(executor_id="regular_executor", data=regular_update)
|
||||
regular_event = WorkflowEvent.emit(executor_id="regular_executor", data=regular_update)
|
||||
|
||||
# 2. Magentic workflow (with additional_properties)
|
||||
magentic_update = AgentResponseUpdate(
|
||||
@@ -517,12 +515,12 @@ async def test_magentic_events_use_same_event_class_as_other_workflows(
|
||||
role="assistant",
|
||||
additional_properties={"magentic_event_type": "agent_delta"},
|
||||
)
|
||||
magentic_event = WorkflowOutputEvent(executor_id="magentic_executor", data=magentic_update)
|
||||
magentic_event = WorkflowEvent.emit(executor_id="magentic_executor", data=magentic_update)
|
||||
|
||||
# Both should be the SAME class
|
||||
assert type(regular_event) is type(magentic_event)
|
||||
assert isinstance(regular_event, WorkflowOutputEvent)
|
||||
assert isinstance(magentic_event, WorkflowOutputEvent)
|
||||
assert isinstance(regular_event, WorkflowEvent)
|
||||
assert isinstance(magentic_event, WorkflowEvent)
|
||||
|
||||
# Both should be handled by the same isinstance check in mapper
|
||||
regular_events = await mapper.convert_event(regular_event, test_request)
|
||||
@@ -559,18 +557,18 @@ async def test_unknown_content_fallback(mapper: MessageMapper, test_request: Age
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# WorkflowOutputEvent Tests
|
||||
# output event (type='output') Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def test_workflow_output_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowOutputEvent is converted to output_item.added."""
|
||||
from agent_framework._workflows._events import WorkflowOutputEvent
|
||||
"""Test output event (type='output') is converted to output_item.added."""
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
event = WorkflowOutputEvent(data="Final workflow output", executor_id="final_executor")
|
||||
event = WorkflowEvent.output(executor_id="final_executor", data="Final workflow output")
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# WorkflowOutputEvent should emit output_item.added
|
||||
# output event (type='output') should emit output_item.added
|
||||
assert len(events) == 1
|
||||
assert events[0].type == "response.output_item.added"
|
||||
# Check item contains the output text
|
||||
@@ -580,16 +578,16 @@ async def test_workflow_output_event(mapper: MessageMapper, test_request: AgentF
|
||||
|
||||
|
||||
async def test_workflow_output_event_with_list_data(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowOutputEvent with list data (common for sequential/concurrent workflows)."""
|
||||
"""Test output event (type='output') with list data (common for sequential/concurrent workflows)."""
|
||||
from agent_framework import ChatMessage
|
||||
from agent_framework._workflows._events import WorkflowOutputEvent
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
# Sequential/Concurrent workflows often output list[ChatMessage]
|
||||
messages = [
|
||||
ChatMessage(role="user", contents=[Content.from_text(text="Hello")]),
|
||||
ChatMessage(role="assistant", contents=[Content.from_text(text="World")]),
|
||||
]
|
||||
event = WorkflowOutputEvent(data=messages, executor_id="complete")
|
||||
event = WorkflowEvent.output(executor_id="complete", data=messages)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
assert len(events) == 1
|
||||
@@ -597,23 +595,23 @@ async def test_workflow_output_event_with_list_data(mapper: MessageMapper, test_
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# WorkflowFailedEvent Tests
|
||||
# failed event (type='failed') Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def test_workflow_failed_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowFailedEvent is converted to response.failed."""
|
||||
from agent_framework._workflows._events import WorkflowErrorDetails, WorkflowFailedEvent
|
||||
"""Test failed event (type='failed') is converted to response.failed."""
|
||||
from agent_framework._workflows._events import WorkflowErrorDetails, WorkflowEvent
|
||||
|
||||
details = WorkflowErrorDetails(
|
||||
error_type="TestError",
|
||||
message="Workflow failed due to test error",
|
||||
executor_id="failing_executor",
|
||||
)
|
||||
event = WorkflowFailedEvent(details=details)
|
||||
event = WorkflowEvent.failed(details=details)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# WorkflowFailedEvent should emit response.failed
|
||||
# failed event (type='failed') should emit response.failed
|
||||
assert len(events) >= 1
|
||||
# Find the failed event
|
||||
failed_events = [e for e in events if getattr(e, "type", "") == "response.failed"]
|
||||
@@ -628,8 +626,8 @@ async def test_workflow_failed_event(mapper: MessageMapper, test_request: AgentF
|
||||
|
||||
|
||||
async def test_workflow_failed_event_with_extra(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowFailedEvent includes extra context when available."""
|
||||
from agent_framework._workflows._events import WorkflowErrorDetails, WorkflowFailedEvent
|
||||
"""Test failed event (type='failed') includes extra context when available."""
|
||||
from agent_framework._workflows._events import WorkflowErrorDetails, WorkflowEvent
|
||||
|
||||
details = WorkflowErrorDetails(
|
||||
error_type="ValidationError",
|
||||
@@ -637,7 +635,7 @@ async def test_workflow_failed_event_with_extra(mapper: MessageMapper, test_requ
|
||||
executor_id="validation_executor",
|
||||
extra={"field": "email", "reason": "invalid format"},
|
||||
)
|
||||
event = WorkflowFailedEvent(details=details)
|
||||
event = WorkflowEvent.failed(details=details)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
assert len(events) == 1
|
||||
@@ -650,8 +648,8 @@ async def test_workflow_failed_event_with_extra(mapper: MessageMapper, test_requ
|
||||
|
||||
|
||||
async def test_workflow_failed_event_with_traceback(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowFailedEvent includes traceback when available."""
|
||||
from agent_framework._workflows._events import WorkflowErrorDetails, WorkflowFailedEvent
|
||||
"""Test failed event (type='failed') includes traceback when available."""
|
||||
from agent_framework._workflows._events import WorkflowErrorDetails, WorkflowEvent
|
||||
|
||||
details = WorkflowErrorDetails(
|
||||
error_type="ValueError",
|
||||
@@ -659,7 +657,7 @@ async def test_workflow_failed_event_with_traceback(mapper: MessageMapper, test_
|
||||
traceback="Traceback (most recent call last):\n File ...\nValueError: Invalid input",
|
||||
executor_id="validation_executor",
|
||||
)
|
||||
event = WorkflowFailedEvent(details=details)
|
||||
event = WorkflowEvent.failed(details=details)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
assert len(events) == 1
|
||||
@@ -672,41 +670,41 @@ async def test_workflow_failed_event_with_traceback(mapper: MessageMapper, test_
|
||||
|
||||
|
||||
async def test_workflow_warning_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowWarningEvent is converted to trace event."""
|
||||
from agent_framework._workflows._events import WorkflowWarningEvent
|
||||
"""Test WorkflowEvent(type='warning') is converted to trace event."""
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
event = WorkflowWarningEvent(data="This is a warning message")
|
||||
event = WorkflowEvent.warning("This is a warning message")
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# WorkflowWarningEvent should emit a trace event
|
||||
# WorkflowEvent(type='warning') should emit a trace event
|
||||
assert len(events) == 1
|
||||
assert events[0].type == "response.trace.completed"
|
||||
assert events[0].data["event_type"] == "WorkflowWarningEvent"
|
||||
assert events[0].data["event_type"] == "warning"
|
||||
|
||||
|
||||
async def test_workflow_error_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowErrorEvent is converted to trace event."""
|
||||
from agent_framework._workflows._events import WorkflowErrorEvent
|
||||
"""Test WorkflowEvent(type='error') is converted to trace event."""
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
event = WorkflowErrorEvent(data=ValueError("Something went wrong"))
|
||||
event = WorkflowEvent.error(ValueError("Something went wrong"))
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# WorkflowErrorEvent should emit a trace event
|
||||
# WorkflowEvent(type='error') should emit a trace event
|
||||
assert len(events) == 1
|
||||
assert events[0].type == "response.trace.completed"
|
||||
assert events[0].data["event_type"] == "WorkflowErrorEvent"
|
||||
assert events[0].data["event_type"] == "error"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# RequestInfoEvent Tests (Human-in-the-Loop)
|
||||
# request_info event (type='request_info') Tests (Human-in-the-Loop)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def test_request_info_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test RequestInfoEvent is converted to HIL request event."""
|
||||
from agent_framework._workflows._events import RequestInfoEvent
|
||||
"""Test request_info event (type='request_info') is converted to HIL request event."""
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
event = RequestInfoEvent(
|
||||
event = WorkflowEvent.request_info(
|
||||
request_id="req_123",
|
||||
source_executor_id="approval_executor",
|
||||
request_data={"action": "approve", "details": "Please approve this action"},
|
||||
@@ -714,7 +712,7 @@ async def test_request_info_event(mapper: MessageMapper, test_request: AgentFram
|
||||
)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# RequestInfoEvent should emit response.request_info.requested
|
||||
# request_info event (type='request_info') should emit response.request_info.requested
|
||||
assert len(events) >= 1
|
||||
# Check that request info is captured
|
||||
has_hil_event = any(getattr(e, "type", "") == "response.request_info.requested" for e in events)
|
||||
@@ -732,24 +730,24 @@ async def test_request_info_event(mapper: MessageMapper, test_request: AgentFram
|
||||
|
||||
|
||||
async def test_superstep_started_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test SuperStepStartedEvent is handled gracefully."""
|
||||
from agent_framework._workflows._events import SuperStepStartedEvent
|
||||
"""Test superstep_started event (type='superstep_started') is handled gracefully."""
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
event = SuperStepStartedEvent(iteration=1)
|
||||
event = WorkflowEvent.superstep_started(iteration=1)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# SuperStepStartedEvent may not emit events (internal workflow signal)
|
||||
# superstep_started event (type='superstep_started') may not emit events (internal workflow signal)
|
||||
# Just ensure it doesn't crash
|
||||
assert isinstance(events, list)
|
||||
|
||||
|
||||
async def test_superstep_completed_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test SuperStepCompletedEvent is handled gracefully."""
|
||||
from agent_framework._workflows._events import SuperStepCompletedEvent
|
||||
"""Test superstep_completed event (type='superstep_completed') is handled gracefully."""
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
event = SuperStepCompletedEvent(iteration=1)
|
||||
event = WorkflowEvent.superstep_completed(iteration=1)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# SuperStepCompletedEvent may not emit events (internal workflow signal)
|
||||
# superstep_completed event (type='superstep_completed') may not emit events (internal workflow signal)
|
||||
# Just ensure it doesn't crash
|
||||
assert isinstance(events, list)
|
||||
|
||||
Reference in New Issue
Block a user