Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bb9d06b
Adding design documents and data flow descriptions for sub-workflows
alliscode Aug 15, 2025
c1d50b1
Updating docs.
alliscode Aug 15, 2025
bd23068
Sub-workflow implementation #1. Stuck because of singleton RequestInf…
alliscode Aug 19, 2025
5955c9a
Removed the singleton restriction on RequestInfoExecutor so enable su…
alliscode Aug 19, 2025
eb8723b
Scenarios seem to be working.
alliscode Aug 19, 2025
09d5da6
Sample improved.
alliscode Aug 20, 2025
64383d7
going to have intern add generic response wrappers.
alliscode Aug 20, 2025
2d02fc2
Wrapped responses working.
alliscode Aug 20, 2025
89d2bc5
Non-hardcoded routing is working.
alliscode Aug 20, 2025
7fee8ba
Sample showing external approved and not approved.
alliscode Aug 20, 2025
ecd3e5e
Cleaning up.
alliscode Aug 20, 2025
bb02c82
Updating some samples and user guide.
alliscode Aug 20, 2025
78cebba
Removing old design doc.
alliscode Aug 20, 2025
be677ee
Cleaning up.
alliscode Aug 20, 2025
e9fd564
Merging from main
alliscode Aug 20, 2025
e267374
Adding python-package-setup.md back.
alliscode Aug 20, 2025
f466a0e
Update python/packages/workflow/agent_framework_workflow/_executor.py
alliscode Aug 20, 2025
51aebb8
Update python/packages/workflow/agent_framework_workflow/_validation.py
alliscode Aug 20, 2025
f54c1e8
Removing prints.
alliscode Aug 20, 2025
dd84e38
Fixing lint and type issues.
alliscode Aug 21, 2025
ff09a5c
Fixing lint and type issues.
alliscode Aug 21, 2025
2210550
Merging from updates in main.
alliscode Aug 21, 2025
3ee8f93
Update python/packages/workflow/agent_framework_workflow/_executor.py
alliscode Aug 21, 2025
cc21378
Adding type hints to intercepts decorator.
alliscode Aug 21, 2025
73f7887
Adding type hints to intercepts decorator.
alliscode Aug 21, 2025
3eb8a8e
Removing unused files.
alliscode Aug 21, 2025
1f9fc0f
Fixing issue with sample 5 groupchat with hil.
alliscode Aug 21, 2025
367b188
Removing redundent samples.
alliscode Aug 21, 2025
39edc97
Merge branch 'main' into bentho/sub-workflows
alliscode Aug 21, 2025
9958add
Updates to ensure no conflicting request interceptors and to support …
alliscode Aug 22, 2025
e85a692
Merge branch 'main' into bentho/sub-workflows
alliscode Aug 22, 2025
2c1c531
Fixing pypi errors.
alliscode Aug 22, 2025
cef4cab
Merge branch 'main' into bentho/sub-workflows
ekzhu Aug 23, 2025
fbcba5e
clean up samples
ekzhu Aug 23, 2025
dd4632c
update samples to make it more clear
ekzhu Aug 23, 2025
c1a4088
warning for unhandled request info from sub workflow
ekzhu Aug 23, 2025
c6f9ea0
add logger info
ekzhu Aug 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Updating docs.
  • Loading branch information
alliscode committed Aug 15, 2025
commit c1d50b1113025061e6e58006b3aea18ac3534bdc
20 changes: 10 additions & 10 deletions docs/design/conditional_forwarding_patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

## The Problem

With automatic routing to `@handles_request`, what happens when the parent realizes it can't actually handle the request and needs to forward it to external sources?
With automatic routing to `@intercepts_request`, what happens when the parent realizes it can't actually handle the request and needs to forward it to external sources?

```python
@handles_request(DomainCheckRequest)
@intercepts_request(DomainCheckRequest)
async def check_domain(self, request: DomainCheckRequest, ctx: WorkflowContext) -> ???:
"""What if we don't have the answer?"""

Expand All @@ -27,7 +27,7 @@ from agent_framework_workflow import FORWARD_REQUEST

class ParentOrchestrator(InterceptingExecutor):

@handles_request(DomainCheckRequest)
@intercepts_request(DomainCheckRequest)
async def check_domain(self, request: DomainCheckRequest, ctx: WorkflowContext) -> bool | object:
"""Check domain if we can, otherwise forward."""

Expand Down Expand Up @@ -70,7 +70,7 @@ from agent_framework_workflow import ForwardRequestException

class ParentOrchestrator(InterceptingExecutor):

@handles_request(DomainCheckRequest)
@intercepts_request(DomainCheckRequest)
async def check_domain(self, request: DomainCheckRequest, ctx: WorkflowContext) -> bool:
"""Check domain if we can, otherwise forward."""

Expand Down Expand Up @@ -116,7 +116,7 @@ from agent_framework_workflow import RequestResponse, Forward

class ParentOrchestrator(InterceptingExecutor):

@handles_request(DomainCheckRequest)
@intercepts_request(DomainCheckRequest)
async def check_domain(self, request: DomainCheckRequest, ctx: WorkflowContext) -> RequestResponse:
"""Check domain if we can, otherwise forward."""

Expand Down Expand Up @@ -194,7 +194,7 @@ Allow the handler to directly control message flow:
```python
class ParentOrchestrator(InterceptingExecutor):

@handles_request(DomainCheckRequest, manual_response=True)
@intercepts_request(DomainCheckRequest, manual_response=True)
async def check_domain(
self,
request: DomainCheckRequest,
Expand Down Expand Up @@ -233,7 +233,7 @@ FORWARD: ForwardRequest = "FORWARD"

class ParentOrchestrator(InterceptingExecutor):

@handles_request(DomainCheckRequest)
@intercepts_request(DomainCheckRequest)
async def check_domain(
self,
request: DomainCheckRequest,
Expand Down Expand Up @@ -266,7 +266,7 @@ class SmartParentOrchestrator(InterceptingExecutor):
self.domain_cache = {}
self.cache_ttl = 300 # 5 minutes

@handles_request(DomainCheckRequest)
@intercepts_request(DomainCheckRequest)
async def check_domain(self, request: DomainCheckRequest, ctx: WorkflowContext) -> RequestResponse:
"""Smart domain checking with caching and forwarding."""

Expand All @@ -293,7 +293,7 @@ class SmartParentOrchestrator(InterceptingExecutor):
# Could even modify the request here if needed
return RequestResponse.forward()

@handles_request(DatabaseQueryRequest)
@intercepts_request(DatabaseQueryRequest)
async def check_database(self, request: DatabaseQueryRequest, ctx: WorkflowContext) -> RequestResponse:
"""Handle some queries locally, forward complex ones."""

Expand All @@ -317,7 +317,7 @@ class SmartParentOrchestrator(InterceptingExecutor):
Support both simple returns (when you can handle) and exceptions (when you can't):

```python
@handles_request(DomainCheckRequest)
@intercepts_request(DomainCheckRequest)
async def check_domain(self, request: DomainCheckRequest, ctx: WorkflowContext) -> bool:
if request.domain in self.cached_domains:
return self.cached_domains[request.domain] # Simple return when handled
Expand Down
40 changes: 20 additions & 20 deletions docs/design/data_flow_explanation.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class ParentOrchestrator(Executor):
"""Parent orchestrator with automatic request routing.

The base Executor class automatically routes SubWorkflowRequestInfo
to @handles_request methods when they exist.
to @intercepts_request methods when they exist.
"""

def __init__(self):
Expand All @@ -107,7 +107,7 @@ class ParentOrchestrator(Executor):
# Just send the raw string, no wrapper needed!
await ctx.send_message(email, target_id="email_validator_workflow")

@handles_request(DomainCheckRequest, from_workflow="email_validator_workflow")
@intercepts_request(DomainCheckRequest, from_workflow="email_validator_workflow")
async def check_domain(self, request: DomainCheckRequest, ctx: WorkflowContext) -> RequestResponse:
"""Handle domain check requests from email validator.

Expand Down Expand Up @@ -165,17 +165,17 @@ When you have multiple sub-workflows that might use the same request types, you
```python
class ParentWithMultipleValidators(Executor):

@handles_request(DomainCheckRequest, from_workflow="email_validator")
@intercepts_request(DomainCheckRequest, from_workflow="email_validator")
async def check_email_domain(self, request: DomainCheckRequest, ctx: WorkflowContext) -> bool:
"""Strict rules for email domains."""
return request.domain in self.email_domains

@handles_request(DomainCheckRequest, from_workflow="api_validator")
@intercepts_request(DomainCheckRequest, from_workflow="api_validator")
async def check_api_domain(self, request: DomainCheckRequest, ctx: WorkflowContext) -> bool:
"""Different rules for API domains."""
return request.domain.endswith(".api.com") or request.domain in self.api_domains

@handles_request(DomainCheckRequest) # No scope - catches all others
@intercepts_request(DomainCheckRequest) # No scope - catches all others
async def check_domain_default(self, request: DomainCheckRequest, ctx: WorkflowContext) -> bool:
"""Fallback for any other validators."""
return not request.domain.endswith(".blocked")
Expand Down Expand Up @@ -232,7 +232,7 @@ STEP 4: Parent intercepts and handles
----------------------------------------
Parent Orchestrator
|
| @handles_request(DomainCheckRequest) method runs
| @intercepts_request(DomainCheckRequest) method runs
| returns: RequestResponse.handled(True)
|
| Framework sends: SubWorkflowResponse(
Expand Down Expand Up @@ -286,10 +286,10 @@ Everything else uses standard types:

This keeps the design simple while enabling:
- **Sub-workflow reusability** (doesn't know it's nested)
- **Parent interception** (can handle requests internally via `@handles_request`)
- **Parent interception** (can handle requests internally via `@intercepts_request`)
- **Automatic routing** (base `Executor` handles `SubWorkflowRequestInfo` → handler → `SubWorkflowResponse`)
- **No boilerplate** (parents only define domain logic, not routing logic)
- **Zero overhead** (features only activate when `@handles_request` is used)
- **Zero overhead** (features only activate when `@intercepts_request` is used)
- **One base class** (no need for special `InterceptingExecutor`)
- **Proper routing** (responses go to the right sub-workflow)
- **Clean separation of concerns**
Expand Down Expand Up @@ -350,14 +350,14 @@ class WorkflowExecutor(Executor):
### Enhanced Base Executor (Framework-Provided)

```python
def handles_request(
def intercepts_request(
request_type: str | type,
from_workflow: str | None = None,
condition: Callable[[Any], bool] | None = None
):
"""Framework-provided decorator for request handlers."""
def decorator(func):
func._handles_request = request_type
func._intercepts_request = request_type
func._from_workflow = from_workflow
func._handle_condition = condition
return func
Expand All @@ -369,11 +369,11 @@ class Executor:
def __init__(self, id: str | None = None):
self.id = id or str(uuid.uuid4())
self._handlers = {}
self._request_handlers = {} # For @handles_request methods
self._request_handlers = {} # For @intercepts_request methods
self._discover_handlers()

def _discover_handlers(self):
"""Discover both @handler and @handles_request methods."""
"""Discover both @handler and @intercepts_request methods."""
for name in dir(self):
attr = getattr(self, name)

Expand All @@ -382,15 +382,15 @@ class Executor:
self._handlers[attr._handler_info['input_type']] = attr

# Request handlers (only add automatic routing if they exist)
if hasattr(attr, '_handles_request'):
if hasattr(attr, '_intercepts_request'):
handler_info = {
'method': attr,
'from_workflow': getattr(attr, '_from_workflow', None),
'condition': getattr(attr, '_handle_condition', None)
}
self._request_handlers[attr._handles_request] = handler_info
self._request_handlers[attr._intercepts_request] = handler_info

# Only register SubWorkflowRequestInfo handler if @handles_request methods exist
# Only register SubWorkflowRequestInfo handler if @intercepts_request methods exist
if self._request_handlers:
self._register_sub_workflow_handler()

Expand All @@ -399,15 +399,15 @@ class Executor:
# This would integrate with the existing handler registration system
pass

@handler # Only active if @handles_request methods exist
@handler # Only active if @intercepts_request methods exist
async def _handle_sub_workflow_request(
self,
request: SubWorkflowRequestInfo,
ctx: WorkflowContext
) -> None:
"""Automatic routing to @handles_request methods.
"""Automatic routing to @intercepts_request methods.

This is only active for executors that have @handles_request methods.
This is only active for executors that have @intercepts_request methods.
Zero overhead for regular executors.
"""
# Try to match against registered handlers
Expand Down Expand Up @@ -479,7 +479,7 @@ class SubWorkflowResponse:

@dataclass
class RequestResponse:
"""Response from a @handles_request method."""
"""Response from a @intercepts_request method."""
handled: bool
data: Any = None
forward_request: Any = None
Expand All @@ -501,4 +501,4 @@ class SubWorkflowConfig:
# ... other configuration options
```

The enhanced base `Executor` class handles all the complexity automatically - developers just use `@handles_request` and the framework does the rest! No special base classes, no boilerplate, just clean business logic.
The enhanced base `Executor` class handles all the complexity automatically - developers just use `@intercepts_request` and the framework does the rest! No special base classes, no boilerplate, just clean business logic.
30 changes: 15 additions & 15 deletions docs/design/strategy_b_complete_example.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class EmailValidator(Executor):
or external system), the RequestInfoExecutor routes the response back
to this handler based on the edge in the workflow graph.

The response type (bool) comes from what the parent's @handles_request
The response type (bool) comes from what the parent's @intercepts_request
method returns.
"""

Expand Down Expand Up @@ -121,7 +121,7 @@ validation_workflow = (
from agent_framework_workflow import Executor, WorkflowContext, handler
from typing import Any, Callable, Protocol

def handles_request(
def intercepts_request(
request_type: str | type,
from_workflow: str | None = None,
condition: Callable[[Any], bool] | None = None
Expand All @@ -134,7 +134,7 @@ def handles_request(
condition: Optional additional condition function
"""
def decorator(func):
func._handles_request = request_type
func._intercepts_request = request_type
func._from_workflow = from_workflow
func._handle_condition = condition
return func
Expand All @@ -149,25 +149,25 @@ class InterceptingExecutor(Executor):
"""Base class for executors that can intercept sub-workflow requests.

This base class automatically handles routing of SubWorkflowRequestInfo
to methods decorated with @handles_request, eliminating boilerplate.
to methods decorated with @intercepts_request, eliminating boilerplate.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._request_handlers = self._discover_handlers()

def _discover_handlers(self):
"""Discover methods decorated with @handles_request."""
"""Discover methods decorated with @intercepts_request."""
handlers = {}
for name in dir(self):
attr = getattr(self, name)
if hasattr(attr, '_handles_request'):
if hasattr(attr, '_intercepts_request'):
handler_info = {
'method': attr,
'from_workflow': getattr(attr, '_from_workflow', None),
'condition': getattr(attr, '_handle_condition', None)
}
handlers[attr._handles_request] = handler_info
handlers[attr._intercepts_request] = handler_info
return handlers

@handler # Framework provides this automatically!
Expand All @@ -176,7 +176,7 @@ class InterceptingExecutor(Executor):
request: SubWorkflowRequestInfo,
ctx: WorkflowContext
) -> None:
"""Automatic routing to @handles_request methods.
"""Automatic routing to @intercepts_request methods.

This is provided by the framework so developers don't need
to write boilerplate routing code.
Expand Down Expand Up @@ -235,7 +235,7 @@ class EmailProcessingOrchestrator(InterceptingExecutor):

Note: No boilerplate routing method needed! The InterceptingExecutor
base class automatically handles routing SubWorkflowRequestInfo to
our @handles_request methods.
our @intercepts_request methods.
"""

def __init__(self):
Expand All @@ -247,7 +247,7 @@ class EmailProcessingOrchestrator(InterceptingExecutor):
"verified-client.net"
}

@handles_request(DomainCheckRequest)
@intercepts_request(DomainCheckRequest)
async def check_domain(
self,
request: DomainCheckRequest,
Expand Down Expand Up @@ -456,7 +456,7 @@ If you have multiple sub-workflows that might use the same request types, you ca
class OrchestratorWithMultipleSubWorkflows(InterceptingExecutor):
"""Orchestrator managing multiple validation sub-workflows."""

@handles_request(DomainCheckRequest, from_workflow="email_validator_workflow")
@intercepts_request(DomainCheckRequest, from_workflow="email_validator_workflow")
async def check_email_domain(
self,
request: DomainCheckRequest,
Expand All @@ -466,7 +466,7 @@ class OrchestratorWithMultipleSubWorkflows(InterceptingExecutor):
# Strict email domain rules
return request.domain in self.approved_email_domains

@handles_request(DomainCheckRequest, from_workflow="api_validator_workflow")
@intercepts_request(DomainCheckRequest, from_workflow="api_validator_workflow")
async def check_api_domain(
self,
request: DomainCheckRequest,
Expand All @@ -476,7 +476,7 @@ class OrchestratorWithMultipleSubWorkflows(InterceptingExecutor):
# Different rules for API domains
return request.domain in self.approved_api_domains or request.domain.endswith(".api.internal")

@handles_request(DomainCheckRequest) # No scope - catches all others
@intercepts_request(DomainCheckRequest) # No scope - catches all others
async def check_domain_fallback(
self,
request: DomainCheckRequest,
Expand All @@ -490,7 +490,7 @@ class OrchestratorWithMultipleSubWorkflows(InterceptingExecutor):
You can also use conditions for more complex logic:

```python
@handles_request(
@intercepts_request(
DomainCheckRequest,
condition=lambda req: req.data.check_type == "strict"
)
Expand Down Expand Up @@ -540,7 +540,7 @@ Final Results:

2. **WorkflowExecutor wraps the request**: It creates a `SubWorkflowRequestInfo` wrapper and sends it to the parent.

3. **Parent intercepts and handles**: The `EmailProcessingOrchestrator` has a `@handles_request(DomainCheckRequest)` method that checks the domain against its internal list.
3. **Parent intercepts and handles**: The `EmailProcessingOrchestrator` has a `@intercepts_request(DomainCheckRequest)` method that checks the domain against its internal list.

4. **Response flows back**: The parent sends a `SubWorkflowResponse` back through the `WorkflowExecutor` to the sub-workflow.

Expand Down
Loading