Description
WorkflowInvocationKwargs exposes global and executor-specific kwargs as separate namespaces, but workflow invocation kwargs are currently normalized through a representation where __global__ can mean either the internal global slot or a real executor ID.
Because executor IDs do not reserve __global__, this affects two public input forms:
- A typed
WorkflowInvocationKwargs whose executor_kwargs contains an entry for executor ID __global__. The executor-specific entry replaces the real global mapping.
- A plain per-executor mapping keyed by
__global__. It is correctly recognized as targeting that real executor, but is later also interpreted as the global slot.
For executors __global__ and other, expected routing is:
typed wrapper:
executor "__global__": {"shared": "G", "special": "A", "overridden": "specific"}
executor "other": {"shared": "G", "overridden": "global"}
plain per-executor mapping:
executor "__global__": {"special": "A"}
executor "other": None / no targeted kwargs
Actual routing is:
typed wrapper:
executor "__global__": {"special": "A", "overridden": "specific"}
executor "other": {"special": "A", "overridden": "specific"}
plain per-executor mapping:
executor "__global__": {"special": "A"}
executor "other": {"special": "A"}
Executor-specific kwargs therefore leak to an unrelated executor. In the typed case, the genuine global kwargs are also lost. Both function_invocation_kwargs and client_kwargs are affected.
Code Sample
import asyncio
from typing import Any
from agent_framework import AgentResponse, BaseAgent, Message, WorkflowInvocationKwargs
from agent_framework.orchestrations import SequentialBuilder
class CapturingAgent(BaseAgent):
def __init__(self, name: str) -> None:
super().__init__(name=name)
self.seen: list[dict[str, Any]] = []
def run(self, messages: Any = None, *, stream: bool = False, **kwargs: Any) -> Any:
self.seen.append(kwargs)
async def result() -> AgentResponse[Any]:
return AgentResponse(messages=[Message("assistant", ["ok"])])
return result()
async def run_case(label: str, channel: str, invocation_kwargs: Any) -> None:
global_agent = CapturingAgent("__global__")
other_agent = CapturingAgent("other")
workflow = SequentialBuilder(participants=[global_agent, other_agent]).build()
await workflow.run("test", **{channel: invocation_kwargs})
print(
f"{label} / {channel}",
(global_agent.seen[0][channel], other_agent.seen[0][channel]),
)
async def main() -> None:
cases = {
"typed": WorkflowInvocationKwargs(
global_kwargs={"shared": "G", "overridden": "global"},
executor_kwargs={"__global__": {"special": "A", "overridden": "specific"}},
),
"plain": {"__global__": {"special": "A"}},
}
for label, invocation_kwargs in cases.items():
for channel in ("function_invocation_kwargs", "client_kwargs"):
await run_case(label, channel, invocation_kwargs)
asyncio.run(main())
Error Messages / Stack Traces
No exception is raised. Current main prints:
typed / function_invocation_kwargs ({'special': 'A', 'overridden': 'specific'}, {'special': 'A', 'overridden': 'specific'})
typed / client_kwargs ({'special': 'A', 'overridden': 'specific'}, {'special': 'A', 'overridden': 'specific'})
plain / function_invocation_kwargs ({'special': 'A'}, {'special': 'A'})
plain / client_kwargs ({'special': 'A'}, {'special': 'A'})
Package Versions
agent-framework-core: 1.18.0; reproduced against main@4b4d849602d31471a0fa4721b7d34d40b6e42114
Python Version
Python 3.12.12
Additional Context
Reverified deterministically through the public Workflow.run(...) path against main@4b4d849602d31471a0fa4721b7d34d40b6e42114. Both the typed and plain collision cases reproduce for function_invocation_kwargs and client_kwargs. The latest Python release is 1.18.0. The reproductions use only local capturing agents—no provider, credentials, network, or timing dependency.
Historical context: review on PR #7963 explicitly identified the collision with an executor ID of __global__. A follow-up review called for separating internal global and executor-specific namespaces so plain mappings and executor IDs with that name would retain their behavior. The response confirmed that direction. Current main still uses the flattened representation in which those namespaces can collide.
A local fix using structurally separate global and executor-specific namespaces passes direct and nested regressions for both kwargs channels, including a plain global application kwarg named __global__, as well as an actual checkpoint save/restore cycle. Non-ambiguous Python 1.18.0-style state remains readable.
Old flattened state involving a real executor ID of __global__ is intrinsically ambiguous because the representation does not preserve whether that entry was global or executor-specific.
This appears to be ordinary functional correctness.
Description
WorkflowInvocationKwargsexposes global and executor-specific kwargs as separate namespaces, but workflow invocation kwargs are currently normalized through a representation where__global__can mean either the internal global slot or a real executor ID.Because executor IDs do not reserve
__global__, this affects two public input forms:WorkflowInvocationKwargswhoseexecutor_kwargscontains an entry for executor ID__global__. The executor-specific entry replaces the real global mapping.__global__. It is correctly recognized as targeting that real executor, but is later also interpreted as the global slot.For executors
__global__andother, expected routing is:Actual routing is:
Executor-specific kwargs therefore leak to an unrelated executor. In the typed case, the genuine global kwargs are also lost. Both
function_invocation_kwargsandclient_kwargsare affected.Code Sample
import asyncio from typing import Any from agent_framework import AgentResponse, BaseAgent, Message, WorkflowInvocationKwargs from agent_framework.orchestrations import SequentialBuilder class CapturingAgent(BaseAgent): def __init__(self, name: str) -> None: super().__init__(name=name) self.seen: list[dict[str, Any]] = [] def run(self, messages: Any = None, *, stream: bool = False, **kwargs: Any) -> Any: self.seen.append(kwargs) async def result() -> AgentResponse[Any]: return AgentResponse(messages=[Message("assistant", ["ok"])]) return result() async def run_case(label: str, channel: str, invocation_kwargs: Any) -> None: global_agent = CapturingAgent("__global__") other_agent = CapturingAgent("other") workflow = SequentialBuilder(participants=[global_agent, other_agent]).build() await workflow.run("test", **{channel: invocation_kwargs}) print( f"{label} / {channel}", (global_agent.seen[0][channel], other_agent.seen[0][channel]), ) async def main() -> None: cases = { "typed": WorkflowInvocationKwargs( global_kwargs={"shared": "G", "overridden": "global"}, executor_kwargs={"__global__": {"special": "A", "overridden": "specific"}}, ), "plain": {"__global__": {"special": "A"}}, } for label, invocation_kwargs in cases.items(): for channel in ("function_invocation_kwargs", "client_kwargs"): await run_case(label, channel, invocation_kwargs) asyncio.run(main())Error Messages / Stack Traces
No exception is raised. Current main prints: typed / function_invocation_kwargs ({'special': 'A', 'overridden': 'specific'}, {'special': 'A', 'overridden': 'specific'}) typed / client_kwargs ({'special': 'A', 'overridden': 'specific'}, {'special': 'A', 'overridden': 'specific'}) plain / function_invocation_kwargs ({'special': 'A'}, {'special': 'A'}) plain / client_kwargs ({'special': 'A'}, {'special': 'A'})Package Versions
agent-framework-core: 1.18.0; reproduced against main@4b4d849602d31471a0fa4721b7d34d40b6e42114
Python Version
Python 3.12.12
Additional Context
Reverified deterministically through the public
Workflow.run(...)path againstmain@4b4d849602d31471a0fa4721b7d34d40b6e42114. Both the typed and plain collision cases reproduce forfunction_invocation_kwargsandclient_kwargs. The latest Python release is1.18.0. The reproductions use only local capturing agents—no provider, credentials, network, or timing dependency.Historical context: review on PR #7963 explicitly identified the collision with an executor ID of
__global__. A follow-up review called for separating internal global and executor-specific namespaces so plain mappings and executor IDs with that name would retain their behavior. The response confirmed that direction. Currentmainstill uses the flattened representation in which those namespaces can collide.A local fix using structurally separate global and executor-specific namespaces passes direct and nested regressions for both kwargs channels, including a plain global application kwarg named
__global__, as well as an actual checkpoint save/restore cycle. Non-ambiguous Python 1.18.0-style state remains readable.Old flattened state involving a real executor ID of
__global__is intrinsically ambiguous because the representation does not preserve whether that entry was global or executor-specific.This appears to be ordinary functional correctness.