[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:
Evan Mattson
2026-02-06 16:47:20 +09:00
committed by GitHub
Unverified
parent 09f59b21ad
commit 0f3f4dbcaf
127 changed files with 1646 additions and 1703 deletions
@@ -6,12 +6,11 @@ from typing import Annotated, Any
from agent_framework import (
ChatMessage,
SequentialBuilder,
WorkflowExecutor,
WorkflowOutputEvent,
tool,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import SequentialBuilder
"""
Sample: Sub-Workflow kwargs Propagation
@@ -32,7 +31,9 @@ Prerequisites:
# Define tools that access custom context via **kwargs
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py and
# samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_authenticated_data(
resource: Annotated[str, "The resource to fetch"],
@@ -129,7 +130,7 @@ async def main() -> None:
user_token=user_token,
service_config=service_config,
):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
output_data = event.data
if isinstance(output_data, list):
for item in output_data: # type: ignore
@@ -140,6 +141,50 @@ async def main() -> None:
print("Sample Complete - kwargs successfully flowed through sub-workflow!")
print("=" * 70)
"""
Sample Output:
======================================================================
Sub-Workflow kwargs Propagation Demo
======================================================================
Context being passed to parent workflow:
user_token: {
"user_name": "alice@contoso.com",
"access_level": "admin",
"session_id": "sess_12345"
}
service_config: {
"services": {
"users": "https://api.example.com/v1/users",
"orders": "https://api.example.com/v1/orders",
"inventory": "https://api.example.com/v1/inventory"
},
"timeout": 30
}
----------------------------------------------------------------------
Workflow Execution (kwargs flow: parent -> sub-workflow -> agent -> tool):
----------------------------------------------------------------------
[get_authenticated_data] kwargs keys: ['user_token', 'service_config']
[get_authenticated_data] User: alice@contoso.com, Access: admin
[call_configured_service] kwargs keys: ['user_token', 'service_config']
[call_configured_service] Available services: ['users', 'orders', 'inventory']
[Final Answer]: Please fetch my profile data and then call the users service.
[Final Answer]: - Your profile data has been fetched.
- The users service has been called.
Would you like details from either the profile data or the users service response?
======================================================================
Sample Complete - kwargs successfully flowed through sub-workflow!
======================================================================
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -3,16 +3,16 @@
import asyncio
import uuid
from dataclasses import dataclass
from typing import Literal
from typing import Any, Literal
from agent_framework import (
Executor,
RequestInfoEvent,
SubWorkflowRequestMessage,
SubWorkflowResponseMessage,
Workflow,
WorkflowBuilder,
WorkflowContext,
WorkflowEvent,
WorkflowExecutor,
handler,
response_handler,
@@ -192,7 +192,7 @@ class ResourceAllocator(Executor):
super().__init__(id)
self._cache: dict[str, int] = {"cpu": 10, "memory": 50, "disk": 100}
# Record pending requests to match responses
self._pending_requests: dict[str, RequestInfoEvent] = {}
self._pending_requests: dict[str, WorkflowEvent[Any]] = {}
async def _handle_resource_request(self, request: ResourceRequest) -> ResourceResponse | None:
"""Allocates resources based on request and available cache."""
@@ -207,7 +207,7 @@ class ResourceAllocator(Executor):
self, request: SubWorkflowRequestMessage, ctx: WorkflowContext[SubWorkflowResponseMessage]
) -> None:
"""Handles requests from sub-workflows."""
source_event: RequestInfoEvent = request.source_event
source_event: WorkflowEvent[Any] = request.source_event
if not isinstance(source_event.data, ResourceRequest):
return
@@ -246,14 +246,14 @@ class PolicyEngine(Executor):
"disk": 1000, # Liberal disk policy
}
# Record pending requests to match responses
self._pending_requests: dict[str, RequestInfoEvent] = {}
self._pending_requests: dict[str, WorkflowEvent[Any]] = {}
@handler
async def handle_subworkflow_request(
self, request: SubWorkflowRequestMessage, ctx: WorkflowContext[SubWorkflowResponseMessage]
) -> None:
"""Handles requests from sub-workflows."""
source_event: RequestInfoEvent = request.source_event
source_event: WorkflowEvent[Any] = request.source_event
if not isinstance(source_event.data, PolicyRequest):
return
@@ -11,7 +11,6 @@ from agent_framework import (
WorkflowBuilder,
WorkflowContext,
WorkflowExecutor,
WorkflowOutputEvent,
handler,
response_handler,
)
@@ -303,7 +302,7 @@ async def main() -> None:
for email in test_emails:
print(f"\n🚀 Processing email to '{email.recipient}'")
async for event in workflow.run(email, stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
print(f"🎉 Final result for '{email.recipient}': {'Delivered' if event.data else 'Blocked'}")