Skip to content

Commit fbede56

Browse files
Python: clean up kwargs across agents, chat clients, tools, and sessions (#3642)
Audit and refactor public **kwargs usage across core agents, chat clients, tools, sessions, and provider packages per the migration strategy codified in CODING_STANDARD.md. Key changes: - Add explicit runtime buckets: function_invocation_kwargs and client_kwargs on RawAgent.run() and chat client get_response() layers. - Refactor FunctionTool to prefer explicit ctx: FunctionInvocationContext injection; legacy **kwargs tools still work via _forward_runtime_kwargs. - Refactor Agent.as_tool() to use direct JSON schema, always-streaming wrapper, approval_mode parameter, and UserInputRequiredException propagation (integrates PR #4568 behavior). - Remove implicit session bleeding into FunctionInvocationContext; tools that need a session must receive it via function_invocation_kwargs. - Lower chat-client layers after FunctionInvocationLayer accept only compatibility **kwargs (client_kwargs flattened, function_invocation_kwargs ignored). - Add layered docstring composition from Raw... implementations via _docstrings.py helper. - Clean up provider constructors to use explicit additional_properties. - Deprecation warnings on legacy direct kwargs paths. - Update samples, tests, and typing across all 23 packages. Resolves #3642 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ded32f3 commit fbede56

File tree

50 files changed

+1829
-496
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1829
-496
lines changed

python/CODING_STANDARD.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,12 @@ def create_agent(name: str, tool_mode: Literal['auto', 'required', 'none'] | Cha
127127
Avoid `**kwargs` unless absolutely necessary. It should only be used as an escape route, not for well-known flows of data:
128128

129129
- **Prefer named parameters**: If there are known extra arguments being passed, use explicit named parameters instead of kwargs
130+
- **Prefer purpose-specific buckets over generic kwargs**: If a flexible payload is still needed, use an explicit named parameter such as `additional_properties`, `function_invocation_kwargs`, or `client_kwargs` rather than a blanket `**kwargs`
130131
- **Subclassing support**: kwargs is acceptable in methods that are part of classes designed for subclassing, allowing subclass-defined kwargs to pass through without issues. In this case, clearly document that kwargs exists for subclass extensibility and not for passing arbitrary data
132+
- **Make known flows explicit first**: For abstract hooks, move known data flows into explicit parameters before leaving `**kwargs` behind for subclass extensibility (for example, prefer `state=` explicitly instead of passing it through kwargs)
133+
- **Prefer explicit metadata containers**: For constructors that expose metadata, prefer an explicit `additional_properties` parameter.
134+
- **Keep SDK passthroughs narrow and documented**: A kwargs escape hatch may be acceptable for provider helper APIs that pass through to a large or unstable external SDK surface, but it should be documented as SDK passthrough and revisited regularly
135+
- **Do not keep passthrough kwargs on wrappers that do not use them**: Convenience wrappers and session helpers should not accept generic kwargs merely to forward or ignore them
131136
- **Remove when possible**: In other cases, removing kwargs is likely better than keeping it
132137
- **Separate kwargs by purpose**: When combining kwargs for multiple purposes, use specific parameters like `client_kwargs: dict[str, Any]` instead of mixing everything in `**kwargs`
133138
- **Always document**: If kwargs must be used, always document how it's used, either by referencing external documentation or explaining its purpose

python/packages/a2a/agent_framework_a2a/_agent.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import json
77
import re
88
import uuid
9-
from collections.abc import AsyncIterable, Awaitable, Sequence
9+
from collections.abc import AsyncIterable, Awaitable, Mapping, Sequence
1010
from typing import Any, Final, Literal, TypeAlias, overload
1111

1212
import httpx
@@ -218,6 +218,8 @@ def run(
218218
*,
219219
stream: Literal[False] = ...,
220220
session: AgentSession | None = None,
221+
function_invocation_kwargs: Mapping[str, Any] | None = None,
222+
client_kwargs: Mapping[str, Any] | None = None,
221223
continuation_token: A2AContinuationToken | None = None,
222224
background: bool = False,
223225
**kwargs: Any,
@@ -230,17 +232,21 @@ def run(
230232
*,
231233
stream: Literal[True],
232234
session: AgentSession | None = None,
235+
function_invocation_kwargs: Mapping[str, Any] | None = None,
236+
client_kwargs: Mapping[str, Any] | None = None,
233237
continuation_token: A2AContinuationToken | None = None,
234238
background: bool = False,
235239
**kwargs: Any,
236240
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
237241

238-
def run(
242+
def run( # pyright: ignore[reportIncompatibleMethodOverride]
239243
self,
240244
messages: AgentRunInputs | None = None,
241245
*,
242246
stream: bool = False,
243247
session: AgentSession | None = None,
248+
function_invocation_kwargs: Mapping[str, Any] | None = None,
249+
client_kwargs: Mapping[str, Any] | None = None,
244250
continuation_token: A2AContinuationToken | None = None,
245251
background: bool = False,
246252
**kwargs: Any,
@@ -253,17 +259,23 @@ def run(
253259
Keyword Args:
254260
stream: Whether to stream the response. Defaults to False.
255261
session: The conversation session associated with the message(s).
262+
function_invocation_kwargs: Present for compatibility with the shared agent interface.
263+
A2AAgent does not use these values directly.
264+
client_kwargs: Present for compatibility with the shared agent interface.
265+
A2AAgent does not use these values directly.
266+
kwargs: Additional compatibility keyword arguments.
267+
A2AAgent does not use these values directly.
256268
continuation_token: Optional token to resume a long-running task
257269
instead of starting a new one.
258270
background: When True, in-progress task updates surface continuation
259271
tokens so the caller can poll or resubscribe later. When False
260272
(default), the agent internally waits for the task to complete.
261-
kwargs: Additional keyword arguments.
262273
263274
Returns:
264275
When stream=False: An Awaitable[AgentResponse].
265276
When stream=True: A ResponseStream of AgentResponseUpdate items.
266277
"""
278+
del function_invocation_kwargs, client_kwargs, kwargs
267279
if continuation_token is not None:
268280
a2a_stream: AsyncIterable[A2AStreamItem] = self.client.resubscribe(
269281
TaskIdParams(id=continuation_token["task_id"])

python/packages/ag-ui/agent_framework_ag_ui/_client.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,6 @@ def __init__(
220220
additional_properties: dict[str, Any] | None = None,
221221
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
222222
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
223-
**kwargs: Any,
224223
) -> None:
225224
"""Initialize the AG-UI chat client.
226225
@@ -231,13 +230,11 @@ def __init__(
231230
additional_properties: Additional properties to store
232231
middleware: Optional middleware to apply to the client.
233232
function_invocation_configuration: Optional function invocation configuration override.
234-
**kwargs: Additional arguments passed to BaseChatClient
235233
"""
236234
super().__init__(
237235
additional_properties=additional_properties,
238236
middleware=middleware,
239237
function_invocation_configuration=function_invocation_configuration,
240-
**kwargs,
241238
)
242239
self._http_service = AGUIHttpService(
243240
endpoint=endpoint,

python/packages/ag-ui/tests/ag_ui/conftest.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,11 @@ def get_response(
9898
options: OptionsCoT | ChatOptions[Any] | None = None,
9999
**kwargs: Any,
100100
) -> Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]]:
101-
self.last_session = kwargs.get("session")
101+
compatibility_client_kwargs = kwargs.get("client_kwargs")
102+
if isinstance(compatibility_client_kwargs, Mapping):
103+
self.last_session = cast(AgentSession | None, compatibility_client_kwargs.get("session"))
104+
else:
105+
self.last_session = cast(AgentSession | None, kwargs.get("session"))
102106
self.last_service_session_id = self.last_session.service_session_id if self.last_session else None
103107
return cast(
104108
Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]],

python/packages/ag-ui/tests/ag_ui/test_agent_wrapper_comprehensive.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -702,14 +702,9 @@ async def test_agent_with_use_service_session_is_true(streaming_chat_client_stub
702702
"""Test that when use_service_session is True, the AgentSession used to run the agent is set to the service session ID."""
703703
from agent_framework.ag_ui import AgentFrameworkAgent
704704

705-
request_service_session_id: str | None = None
706-
707705
async def stream_fn(
708706
messages: MutableSequence[Message], chat_options: ChatOptions, **kwargs: Any
709707
) -> AsyncIterator[ChatResponseUpdate]:
710-
nonlocal request_service_session_id
711-
session = kwargs.get("session")
712-
request_service_session_id = session.service_session_id if session else None
713708
yield ChatResponseUpdate(
714709
contents=[Content.from_text(text="Response")], response_id="resp_67890", conversation_id="conv_12345"
715710
)
@@ -719,11 +714,22 @@ async def stream_fn(
719714

720715
input_data = {"messages": [{"role": "user", "content": "Hi"}], "thread_id": "conv_123456"}
721716

717+
# Spy on agent.run to capture the session kwarg at call time (before streaming mutates it)
718+
captured_service_session_id: str | None = None
719+
original_run = agent.run
720+
721+
def capturing_run(*args: Any, **kwargs: Any) -> Any:
722+
nonlocal captured_service_session_id
723+
session = kwargs.get("session")
724+
captured_service_session_id = session.service_session_id if session else None
725+
return original_run(*args, **kwargs)
726+
727+
agent.run = capturing_run # type: ignore[assignment, method-assign]
728+
722729
events: list[Any] = []
723730
async for event in wrapper.run(input_data):
724731
events.append(event)
725-
request_service_session_id = agent.client.last_service_session_id
726-
assert request_service_session_id == "conv_123456" # type: ignore[attr-defined] (service_session_id should be set)
732+
assert captured_service_session_id == "conv_123456"
727733

728734

729735
async def test_function_approval_mode_executes_tool(streaming_chat_client_stub):

python/packages/anthropic/agent_framework_anthropic/_chat_client.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,11 @@ def __init__(
228228
model_id: str | None = None,
229229
anthropic_client: AsyncAnthropic | None = None,
230230
additional_beta_flags: list[str] | None = None,
231+
additional_properties: dict[str, Any] | None = None,
231232
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
232233
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
233234
env_file_path: str | None = None,
234235
env_file_encoding: str | None = None,
235-
**kwargs: Any,
236236
) -> None:
237237
"""Initialize an Anthropic Agent client.
238238
@@ -244,11 +244,11 @@ def __init__(
244244
For instance if you need to set a different base_url for testing or private deployments.
245245
additional_beta_flags: Additional beta flags to enable on the client.
246246
Default flags are: "mcp-client-2025-04-04", "code-execution-2025-08-25".
247+
additional_properties: Additional properties stored on the client instance.
247248
middleware: Optional middleware to apply to the client.
248249
function_invocation_configuration: Optional function invocation configuration override.
249250
env_file_path: Path to environment file for loading settings.
250251
env_file_encoding: Encoding of the environment file.
251-
kwargs: Additional keyword arguments passed to the parent class.
252252
253253
Examples:
254254
.. code-block:: python
@@ -319,9 +319,9 @@ class MyOptions(AnthropicChatOptions, total=False):
319319

320320
# Initialize parent
321321
super().__init__(
322+
additional_properties=additional_properties,
322323
middleware=middleware,
323324
function_invocation_configuration=function_invocation_configuration,
324-
**kwargs,
325325
)
326326

327327
# Initialize instance variables

python/packages/azure-ai-search/tests/test_aisearch_context_provider.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@
1616
# -- Helpers -------------------------------------------------------------------
1717

1818

19+
@pytest.fixture(autouse=True)
20+
def clear_azure_search_env(monkeypatch: pytest.MonkeyPatch) -> None:
21+
"""Keep tests isolated from ambient Azure Search environment variables."""
22+
for key in (
23+
"AZURE_SEARCH_ENDPOINT",
24+
"AZURE_SEARCH_INDEX_NAME",
25+
"AZURE_SEARCH_KNOWLEDGE_BASE_NAME",
26+
"AZURE_SEARCH_API_KEY",
27+
):
28+
monkeypatch.delenv(key, raising=False)
29+
30+
1931
class MockSearchResults:
2032
"""Async-iterable mock for Azure SearchClient.search() results."""
2133

python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -444,11 +444,11 @@ def __init__(
444444
model_deployment_name: str | None = None,
445445
credential: AzureCredentialTypes | None = None,
446446
should_cleanup_agent: bool = True,
447+
additional_properties: dict[str, Any] | None = None,
447448
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
448449
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
449450
env_file_path: str | None = None,
450451
env_file_encoding: str | None = None,
451-
**kwargs: Any,
452452
) -> None:
453453
"""Initialize an Azure AI Agent client.
454454
@@ -471,11 +471,11 @@ def __init__(
471471
should_cleanup_agent: Whether to cleanup (delete) agents created by this client when
472472
the client is closed or context is exited. Defaults to True. Only affects agents
473473
created by this client instance; existing agents passed via agent_id are never deleted.
474+
additional_properties: Additional properties stored on the client instance.
474475
middleware: Optional sequence of middlewares to include.
475476
function_invocation_configuration: Optional function invocation configuration.
476477
env_file_path: Path to environment file for loading settings.
477478
env_file_encoding: Encoding of the environment file.
478-
kwargs: Additional keyword arguments passed to the parent class.
479479
480480
Examples:
481481
.. code-block:: python
@@ -548,9 +548,9 @@ class MyOptions(AzureAIAgentOptions, total=False):
548548

549549
# Initialize parent
550550
super().__init__(
551+
additional_properties=additional_properties,
551552
middleware=middleware,
552553
function_invocation_configuration=function_invocation_configuration,
553-
**kwargs,
554554
)
555555

556556
# Initialize instance variables

python/packages/azure-ai/agent_framework_azure_ai/_client.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,9 @@ def __init__(
119119
credential: AzureCredentialTypes | None = None,
120120
use_latest_version: bool | None = None,
121121
allow_preview: bool | None = None,
122+
additional_properties: dict[str, Any] | None = None,
122123
env_file_path: str | None = None,
123124
env_file_encoding: str | None = None,
124-
**kwargs: Any,
125125
) -> None:
126126
"""Initialize a bare Azure AI client.
127127
@@ -145,9 +145,9 @@ def __init__(
145145
use_latest_version: Boolean flag that indicates whether to use latest agent version
146146
if it exists in the service.
147147
allow_preview: Enables preview opt-in on internally-created ``AIProjectClient``.
148+
additional_properties: Additional properties stored on the client instance.
148149
env_file_path: Path to environment file for loading settings.
149150
env_file_encoding: Encoding of the environment file.
150-
kwargs: Additional keyword arguments passed to the parent class.
151151
152152
Examples:
153153
.. code-block:: python
@@ -217,7 +217,7 @@ class MyOptions(ChatOptions, total=False):
217217

218218
# Initialize parent
219219
super().__init__(
220-
**kwargs,
220+
additional_properties=additional_properties,
221221
)
222222

223223
# Initialize instance variables
@@ -1243,11 +1243,11 @@ def __init__(
12431243
credential: AzureCredentialTypes | None = None,
12441244
use_latest_version: bool | None = None,
12451245
allow_preview: bool | None = None,
1246+
additional_properties: dict[str, Any] | None = None,
12461247
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
12471248
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
12481249
env_file_path: str | None = None,
12491250
env_file_encoding: str | None = None,
1250-
**kwargs: Any,
12511251
) -> None:
12521252
"""Initialize an Azure AI client with full layer support.
12531253
@@ -1268,11 +1268,11 @@ def __init__(
12681268
use_latest_version: Boolean flag that indicates whether to use latest agent version
12691269
if it exists in the service.
12701270
allow_preview: Enables preview opt-in on internally-created ``AIProjectClient``
1271+
additional_properties: Additional properties stored on the client instance.
12711272
middleware: Optional sequence of chat middlewares to include.
12721273
function_invocation_configuration: Optional function invocation configuration.
12731274
env_file_path: Path to environment file for loading settings.
12741275
env_file_encoding: Encoding of the environment file.
1275-
kwargs: Additional keyword arguments passed to the parent class.
12761276
12771277
Examples:
12781278
.. code-block:: python
@@ -1319,9 +1319,9 @@ class MyOptions(ChatOptions, total=False):
13191319
credential=credential,
13201320
use_latest_version=use_latest_version,
13211321
allow_preview=allow_preview,
1322+
additional_properties=additional_properties,
13221323
middleware=middleware,
13231324
function_invocation_configuration=function_invocation_configuration,
13241325
env_file_path=env_file_path,
13251326
env_file_encoding=env_file_encoding,
1326-
**kwargs,
13271327
)

python/packages/azure-ai/agent_framework_azure_ai/_embedding_client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,9 @@ def __init__(
124124
text_client: EmbeddingsClient | None = None,
125125
image_client: ImageEmbeddingsClient | None = None,
126126
credential: AzureKeyCredential | None = None,
127+
additional_properties: dict[str, Any] | None = None,
127128
env_file_path: str | None = None,
128129
env_file_encoding: str | None = None,
129-
**kwargs: Any,
130130
) -> None:
131131
"""Initialize a raw Azure AI Inference embedding client."""
132132
settings = load_settings(
@@ -160,7 +160,7 @@ def __init__(
160160
credential=credential, # type: ignore[arg-type]
161161
)
162162
self._endpoint = resolved_endpoint
163-
super().__init__(**kwargs)
163+
super().__init__(additional_properties=additional_properties)
164164

165165
async def close(self) -> None:
166166
"""Close the underlying SDK clients and release resources."""
@@ -376,9 +376,9 @@ def __init__(
376376
image_client: ImageEmbeddingsClient | None = None,
377377
credential: AzureKeyCredential | None = None,
378378
otel_provider_name: str | None = None,
379+
additional_properties: dict[str, Any] | None = None,
379380
env_file_path: str | None = None,
380381
env_file_encoding: str | None = None,
381-
**kwargs: Any,
382382
) -> None:
383383
"""Initialize an Azure AI Inference embedding client."""
384384
super().__init__(
@@ -389,8 +389,8 @@ def __init__(
389389
text_client=text_client,
390390
image_client=image_client,
391391
credential=credential,
392+
additional_properties=additional_properties,
392393
otel_provider_name=otel_provider_name,
393394
env_file_path=env_file_path,
394395
env_file_encoding=env_file_encoding,
395-
**kwargs,
396396
)

0 commit comments

Comments
 (0)