Skip to content

Python: fix(python): stop executor named __global__ from colliding with the global kwargs slot - #8333

Draft
Yufeng He (he-yufeng) wants to merge 1 commit into
microsoft:mainfrom
he-yufeng:fix/workflow-kwargs-global-collision
Draft

Python: fix(python): stop executor named __global__ from colliding with the global kwargs slot#8333
Yufeng He (he-yufeng) wants to merge 1 commit into
microsoft:mainfrom
he-yufeng:fix/workflow-kwargs-global-collision

Conversation

@he-yufeng

Copy link
Copy Markdown
Contributor

Motivation & Context

WorkflowInvocationKwargs exposes global and executor-specific kwargs as separate namespaces, but _resolve_invocation_kwargs normalizes both public input forms into one flat dict where the global mapping lives under the reserved __global__ key alongside bare executor IDs. An executor whose ID is literally __global__ then collides with that slot: in the typed form its entry replaces the genuine global mapping, and in the plain-mapping form its entry is later read back as the global mapping and leaks to every untargeted executor in the graph.

Description & Review Guide

  • What are the major changes? Per-executor entries now nest under a separate framework slot (__per_executor__ in _const.py) so the two namespaces cannot overlap. The normalized shapes are: typed wrapper -> {"__global__": ..., "__per_executor__": {...}}; plain per-executor mapping -> {"__per_executor__": {...}} with the global slot absent, preserving the existing convention that an untargeted executor resolves to None; plain global mapping -> {"__global__": ..., "__per_executor__": {}}. AgentExecutor._resolve_executor_kwargs reads the nested shape and falls back to the legacy flat shape for checkpoints serialized before this change. WorkflowExecutor unwraps the nested shape so a subworkflow keeps receiving the plain mapping its own resolution expects.
  • What is the impact of these changes? Routing for the __global__-named executor now matches the expected tables in the issue: its entry reaches only itself, the genuine global mapping is no longer lost in the typed form, and nothing leaks into unrelated executors. Existing checkpoints with the flat shape keep working through the legacy branch.
  • What do you want reviewers to focus on? The shape sniff in _resolve_executor_kwargs (nested vs legacy) and the empty-global-slot semantics: an explicitly empty global dict still merges to {} (the "clear previous kwargs" path), while a pure per-executor mapping leaves the global slot absent so untargeted executors get None. Both conventions are pinned by existing tests, which now run against the new shape.

Heads-up on ordering with #8314 (also mine, also touching _agent_executor.py): that one adds a module-level resolve_executor_run_kwargs helper with the same routing logic. Whichever lands second should rebase and give that helper the same nested-shape branch. The conflict is small and mechanical either way.

Related Issue

Fixes #8310

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

…the global kwargs slot

_resolve_invocation_kwargs normalized both per-executor and global
invocation kwargs into one flat dict keyed by executor ID, with the
global mapping under the reserved "__global__" key. An executor whose
ID is literally "__global__" then collided with that slot: a plain
mapping keyed "__global__" was normalized as per-executor kwargs, but
_resolve_executor_kwargs read the same key back as the global mapping
and leaked the entry to every untargeted executor in the graph.

Per-executor entries now nest under a separate framework slot
("__per_executor__") so the two namespaces cannot overlap:

- typed WorkflowInvocationKwargs: {"__global__": ..., "__per_executor__": {...}}
- plain per-executor mapping: {"__per_executor__": {...}} (no global
  slot, preserving the "untargeted executor gets None" convention)
- plain global mapping: {"__global__": ..., "__per_executor__": {}}

AgentExecutor._resolve_executor_kwargs reads the nested shape and
falls back to the legacy flat shape for checkpoints serialized before
this change; WorkflowExecutor applies the same unwrapping so
subworkflows keep receiving the plain mapping their own resolution
expects.

Fixes microsoft#8310
Copilot AI balanced review requested due to automatic review settings September 12, 2026 14:24
@agent-framework-automation agent-framework-automation Bot added the python Usage: [Issues, PRs], Target: Python label Sep 12, 2026
@github-actions github-actions Bot changed the title fix(python): stop executor named __global__ from colliding with the global kwargs slot Python: fix(python): stop executor named __global__ from colliding with the global kwargs slot Sep 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Plain __global__ overrides regress, and legacy checkpoints can collide with the new discriminator.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Separates global and executor-specific workflow kwargs to avoid __global__ executor collisions.

Changes:

  • Adds the __per_executor__ namespace.
  • Updates kwargs resolution and subworkflow propagation.
  • Adds collision and compatibility tests.
File summaries
File Description
_const.py Defines the per-executor namespace key.
_workflow.py Produces the nested kwargs representation.
_agent_executor.py Resolves nested and legacy kwargs.
_workflow_executor.py Propagates kwargs into subworkflows.
test_workflow_kwargs.py Tests workflow-level collision behavior.
test_agent_executor.py Tests nested and legacy resolution.
Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

# (``function_invocation_kwargs={}``) is preserved and merges to {}
global_kwargs: Any = resolved.get(GLOBAL_KWARGS_KEY)
executor_kwargs: Any = resolved.get(self.id)
if EXECUTOR_KWARGS_KEY in resolved:
matched_ids,
)
return dict(kwargs)
return {EXECUTOR_KWARGS_KEY: dict(kwargs)}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: Workflow invocation kwargs collide with an executor ID of __global__

2 participants