Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion python/packages/core/agent_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@
"validate_workflow_graph",
),
"._workflows._viz": ("WorkflowViz",),
"._workflows._workflow": ("Workflow", "WorkflowRunResult"),
"._workflows._workflow": ("Workflow", "WorkflowInvocationKwargs", "WorkflowRunResult"),
"._workflows._workflow_builder": ("WorkflowBuilder",),
"._workflows._workflow_context": ("WorkflowContext",),
"._workflows._workflow_executor": (
Expand Down Expand Up @@ -593,6 +593,7 @@
"WorkflowEventType",
"WorkflowException",
"WorkflowExecutor",
"WorkflowInvocationKwargs",
"WorkflowMessage",
"WorkflowRunResult",
"WorkflowRunState",
Expand Down
3 changes: 2 additions & 1 deletion python/packages/core/agent_framework/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ from ._workflows._validation import (
validate_workflow_graph,
)
from ._workflows._viz import WorkflowViz
from ._workflows._workflow import Workflow, WorkflowRunResult
from ._workflows._workflow import Workflow, WorkflowInvocationKwargs, WorkflowRunResult
from ._workflows._workflow_builder import WorkflowBuilder
from ._workflows._workflow_context import WorkflowContext
from ._workflows._workflow_executor import SubWorkflowRequestMessage, SubWorkflowResponseMessage, WorkflowExecutor
Expand Down Expand Up @@ -559,6 +559,7 @@ __all__ = [
"WorkflowExecutor",
"WorkflowMessage",
"WorkflowRunResult",
"WorkflowInvocationKwargs",
"WorkflowRunState",
"WorkflowRunnerException",
"WorkflowValidationError",
Expand Down
44 changes: 31 additions & 13 deletions python/packages/core/agent_framework/_workflows/_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
from typing_extensions import TypedDict # pragma: no cover

if TYPE_CHECKING:
from ._workflow import Workflow
from ._workflow import Workflow, WorkflowInvocationKwargs

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -155,8 +155,11 @@ def run(
session: AgentSession | None = None,
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
function_invocation_kwargs: WorkflowInvocationKwargs
| Mapping[str, Mapping[str, Any]]
| Mapping[str, Any]
| None = None,
client_kwargs: WorkflowInvocationKwargs | Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> ResponseStream[AgentResponseUpdate, AgentResponse]: ...

@overload
Expand All @@ -168,8 +171,11 @@ async def run(
session: AgentSession | None = None,
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
function_invocation_kwargs: WorkflowInvocationKwargs
| Mapping[str, Mapping[str, Any]]
| Mapping[str, Any]
| None = None,
client_kwargs: WorkflowInvocationKwargs | Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> AgentResponse: ...

def run(
Expand All @@ -180,8 +186,11 @@ def run(
session: AgentSession | None = None,
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
function_invocation_kwargs: WorkflowInvocationKwargs
| Mapping[str, Mapping[str, Any]]
| Mapping[str, Any]
| None = None,
client_kwargs: WorkflowInvocationKwargs | Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> ResponseStream[AgentResponseUpdate, AgentResponse] | Awaitable[AgentResponse]:
"""Get a response from the workflow agent.

Expand Down Expand Up @@ -246,8 +255,11 @@ async def _run_impl(
session: AgentSession | None,
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
function_invocation_kwargs: WorkflowInvocationKwargs
| Mapping[str, Mapping[str, Any]]
| Mapping[str, Any]
| None = None,
client_kwargs: WorkflowInvocationKwargs | Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> AgentResponse:
"""Internal implementation of non-streaming execution.

Expand Down Expand Up @@ -326,8 +338,11 @@ async def _run_stream_impl(
session: AgentSession | None,
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
function_invocation_kwargs: WorkflowInvocationKwargs
| Mapping[str, Mapping[str, Any]]
| Mapping[str, Any]
| None = None,
client_kwargs: WorkflowInvocationKwargs | Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> AsyncIterable[AgentResponseUpdate]:
"""Internal implementation of streaming execution.

Expand Down Expand Up @@ -405,8 +420,11 @@ async def _run_core(
checkpoint_id: str | None,
checkpoint_storage: CheckpointStorage | None,
streaming: bool,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
function_invocation_kwargs: WorkflowInvocationKwargs
| Mapping[str, Mapping[str, Any]]
| Mapping[str, Any]
| None = None,
client_kwargs: WorkflowInvocationKwargs | Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> AsyncIterable[WorkflowEvent]:
"""Core implementation that yields workflow events for both streaming and non-streaming modes.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,22 +581,26 @@ def _resolve_executor_kwargs(self, resolved: dict[str, Any] | None) -> dict[str,
"""
if not isinstance(resolved, dict):
return None
# Use explicit key-presence checks so that an empty per-executor dict is
# honoured (e.g. to clear kwargs) instead of falling through to global.
if self.id in resolved:
executor_kwargs = resolved[self.id]
elif GLOBAL_KWARGS_KEY in resolved:
executor_kwargs = resolved[GLOBAL_KWARGS_KEY]
else:
global_kwargs = resolved.get(GLOBAL_KWARGS_KEY)
executor_kwargs = resolved.get(self.id)
if global_kwargs is None and executor_kwargs is None:
return None

if not isinstance(executor_kwargs, dict):
if global_kwargs is not None and not isinstance(global_kwargs, dict):
logger.warning(
"Executor %s expected a dict for its kwargs, but got %s. Ignoring.",
"Executor %s expected a dict for global kwargs, but got %s. Ignoring.",
self.id,
type(executor_kwargs), # type: ignore
type(global_kwargs),
)
return None

if executor_kwargs is not None and not isinstance(executor_kwargs, dict):
logger.warning(
"Executor %s expected a dict for its kwargs, but got %s. Ignoring.",
self.id,
type(executor_kwargs),
)
return None

return executor_kwargs # type: ignore
# Specific values override global values for the same function argument.
return {**(global_kwargs or {}), **(executor_kwargs or {})}
4 changes: 4 additions & 0 deletions python/packages/core/agent_framework/_workflows/_const.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
# to pass kwargs from workflow.run() through to agent.run() and @tool functions.
WORKFLOW_RUN_KWARGS_KEY = "_workflow_run_kwargs"

# State keys used to preserve caller-provided kwargs for nested workflow routing.
RAW_FUNCTION_INVOCATION_KWARGS_KEY = "_raw_function_invocation_kwargs"
RAW_CLIENT_KWARGS_KEY = "_raw_client_kwargs"

# Sentinel key used in resolved invocation kwargs dicts to denote global kwargs
# that apply to all executors (as opposed to per-executor keyed entries).
GLOBAL_KWARGS_KEY = "__global__"
Expand Down
77 changes: 59 additions & 18 deletions python/packages/core/agent_framework/_workflows/_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@
from ..exceptions import WorkflowException
from ..observability import OtelAttr, capture_exception, create_workflow_span
from ._checkpoint import CheckpointStorage
from ._const import DEFAULT_MAX_ITERATIONS, GLOBAL_KWARGS_KEY, INTERNAL_SOURCE_ID, WORKFLOW_RUN_KWARGS_KEY
from ._const import (
DEFAULT_MAX_ITERATIONS,
GLOBAL_KWARGS_KEY,
INTERNAL_SOURCE_ID,
RAW_CLIENT_KWARGS_KEY,
RAW_FUNCTION_INVOCATION_KWARGS_KEY,
WORKFLOW_RUN_KWARGS_KEY,
)
from ._edge import (
EdgeGroup,
FanOutEdgeGroup,
Expand Down Expand Up @@ -205,6 +212,18 @@ def classify(self, executor_id: str) -> Literal["output", "intermediate"] | None
return None


@dataclass(frozen=True)
class WorkflowInvocationKwargs:
"""Explicit global and executor-specific kwargs for a workflow run.

Use this wrapper when shared kwargs should be combined with executor-specific
overrides. Plain mappings retain their existing global or per-executor behavior.
"""

global_kwargs: Mapping[str, Any] = field(default_factory=dict)
executor_kwargs: Mapping[str, Mapping[str, Any]] = field(default_factory=dict)


class Workflow(DictConvertible):
"""A graph-based execution engine that orchestrates connected executors.

Expand Down Expand Up @@ -480,8 +499,11 @@ async def _run_workflow_with_tracing(
initial_executor_fn: Callable[[], Awaitable[None]] | None = None,
is_continuation: bool = False,
streaming: bool = False,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
function_invocation_kwargs: WorkflowInvocationKwargs
| Mapping[str, Mapping[str, Any]]
| Mapping[str, Any]
| None = None,
client_kwargs: WorkflowInvocationKwargs | Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> AsyncIterable[WorkflowEvent]:
"""Private method to run workflow with proper tracing.

Expand Down Expand Up @@ -556,10 +578,12 @@ async def _run_workflow_with_tracing(
combined_kwargs["function_invocation_kwargs"] = self._resolve_invocation_kwargs(
function_invocation_kwargs, "function_invocation_kwargs"
)
combined_kwargs[RAW_FUNCTION_INVOCATION_KWARGS_KEY] = function_invocation_kwargs
if client_kwargs is not None:
combined_kwargs["client_kwargs"] = self._resolve_invocation_kwargs(
client_kwargs, "client_kwargs"
)
combined_kwargs[RAW_CLIENT_KWARGS_KEY] = client_kwargs
self._runner.state.set(WORKFLOW_RUN_KWARGS_KEY, combined_kwargs)
elif not is_continuation:
self._runner.state.set(WORKFLOW_RUN_KWARGS_KEY, {})
Expand Down Expand Up @@ -688,8 +712,8 @@ def run(
responses: Mapping[str, Any] | None = None,
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
function_invocation_kwargs: Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Any] | None = None,
function_invocation_kwargs: WorkflowInvocationKwargs | Mapping[str, Any] | None = None,
client_kwargs: WorkflowInvocationKwargs | Mapping[str, Any] | None = None,
) -> ResponseStream[WorkflowEvent, WorkflowRunResult]: ...

@overload
Expand All @@ -702,8 +726,8 @@ def run(
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
include_status_events: bool = False,
function_invocation_kwargs: Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Any] | None = None,
function_invocation_kwargs: WorkflowInvocationKwargs | Mapping[str, Any] | None = None,
client_kwargs: WorkflowInvocationKwargs | Mapping[str, Any] | None = None,
) -> Awaitable[WorkflowRunResult]: ...

def run(
Expand All @@ -715,8 +739,11 @@ def run(
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
include_status_events: bool = False,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
function_invocation_kwargs: WorkflowInvocationKwargs
| Mapping[str, Mapping[str, Any]]
| Mapping[str, Any]
| None = None,
client_kwargs: WorkflowInvocationKwargs | Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> ResponseStream[WorkflowEvent, WorkflowRunResult] | Awaitable[WorkflowRunResult]:
"""Run the workflow, optionally streaming events.

Expand All @@ -740,10 +767,14 @@ def run(
include_status_events: Whether to include status events (non-streaming only).
function_invocation_kwargs: Keyword arguments forwarded to tool invocations in
subagents. Either a mapping for agent name or agent executor id to kwargs,
or a flat mapping of kwargs for all tool invocations.
a flat mapping of kwargs for all tool invocations, or a
``WorkflowInvocationKwargs`` instance to combine global and executor-specific
kwargs.
client_kwargs: Keyword arguments forwarded to chat client calls in
subagents. Either a mapping for agent name or agent executor id to kwargs,
or a flat mapping of kwargs for all chat client calls.
a flat mapping of kwargs for all chat client calls, or a
``WorkflowInvocationKwargs`` instance to combine global and executor-specific
kwargs.

Returns:
When stream=True: A ResponseStream[WorkflowEvent, WorkflowRunResult] for
Expand Down Expand Up @@ -802,8 +833,11 @@ async def _run_core(
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
streaming: bool = False,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
function_invocation_kwargs: WorkflowInvocationKwargs
| Mapping[str, Mapping[str, Any]]
| Mapping[str, Any]
| None = None,
client_kwargs: WorkflowInvocationKwargs | Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> AsyncIterable[WorkflowEvent]:
"""Single core execution path for both streaming and non-streaming modes.

Expand Down Expand Up @@ -1057,25 +1091,32 @@ def _get_executor_by_id(self, executor_id: str) -> Executor:

def _resolve_invocation_kwargs(
self,
kwargs: Mapping[str, Any],
kwargs: WorkflowInvocationKwargs | Mapping[str, Any],
param_name: str,
) -> dict[str, Any]:
"""Resolve invocation kwargs into a normalized per-executor or global format.

Detects whether the provided kwargs dict uses per-executor targeting by checking
if any top-level key matches a known executor ID in the workflow. If at least one
key matches, all entries are treated as per-executor. Otherwise the dict is treated
as global kwargs that apply to every executor.
as global kwargs that apply to every executor. The ``"__global__"`` key can be used
explicitly to combine global kwargs with per-executor overrides.

Args:
kwargs: The raw invocation kwargs from the caller.
param_name: The parameter name (for logging), e.g. ``"function_invocation_kwargs"``.

Returns:
A dict with either:
- ``{"__global__": <original dict>}`` for global kwargs, or
- The original dict unchanged for per-executor kwargs.
A dict containing normalized global or per-executor mappings.
"""
if isinstance(kwargs, WorkflowInvocationKwargs):
resolved = {GLOBAL_KWARGS_KEY: dict(kwargs.global_kwargs)}
resolved.update({
executor_id: dict(executor_kwargs) for executor_id, executor_kwargs in kwargs.executor_kwargs.items()
})
logger.info("Explicit global %s provided with executor-specific overrides.", param_name)
return resolved

executor_ids = set(self.executors.keys())
matched_ids = kwargs.keys() & executor_ids
if matched_ids:
Expand Down
Loading
Loading