Skip to content

Commit 1c3ccd3

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 1c3ccd3

File tree

51 files changed

+1832
-499
lines changed

Some content is hidden

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

51 files changed

+1832
-499
lines changed

docs/decisions/0001-agent-run-response.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Approaches observed from the compared SDKs:
6464
| AutoGen | **Approach 1** Separates messages into Agent-Agent (maps to Primary) and Internal (maps to Secondary) and these are returned as separate properties on the agent response object. See [types of messages](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/messages.html#types-of-messages) and [Response](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.Response) | **Approach 2** Returns a stream of internal events and the last item is a Response object. See [ChatAgent.on_messages_stream](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.ChatAgent.on_messages_stream) |
6565
| OpenAI Agent SDK | **Approach 1** Separates new_items (Primary+Secondary) from final output (Primary) as separate properties on the [RunResult](https://github.com/openai/openai-agents-python/blob/main/src/agents/result.py#L39) | **Approach 1** Similar to non-streaming, has a way of streaming updates via a method on the response object which includes all data, and then a separate final output property on the response object which is populated only when the run is complete. See [RunResultStreaming](https://github.com/openai/openai-agents-python/blob/main/src/agents/result.py#L136) |
6666
| Google ADK | **Approach 2** [Emits events](https://google.github.io/adk-docs/runtime/#step-by-step-breakdown) with [FinalResponse](https://github.com/google/adk-java/blob/main/core/src/main/java/com/google/adk/events/Event.java#L232) true (Primary) / false (Secondary) and callers have to filter out those with false to get just the final response message | **Approach 2** Similar to non-streaming except [events](https://google.github.io/adk-docs/runtime/#streaming-vs-non-streaming-output-partialtrue) are emitted with [Partial](https://github.com/google/adk-java/blob/main/core/src/main/java/com/google/adk/events/Event.java#L133) true to indicate that they are streaming messages. A final non partial event is also emitted. |
67-
| AWS (Strands) | **Approach 3** Returns an [AgentResult](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent_result/) (Primary) with messages and a reason for the run's completion. | **Approach 2** [Streams events](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent/#strands.agent.agent.Agent.stream_async) (Primary+Secondary) including, response text, current_tool_use, even data from "callbacks" (strands plugins) |
67+
| AWS (Strands) | **Approach 3** Returns an [AgentResult](https://strandsagents.com/docs/api/python/strands.agent.agent_result/) (Primary) with messages and a reason for the run's completion. | **Approach 2** [Streams events](https://strandsagents.com/docs/api/python/strands.agent.agent/) (Primary+Secondary) including, response text, current_tool_use, even data from "callbacks" (strands plugins) |
6868
| LangGraph | **Approach 2** A mixed list of all [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) | **Approach 2** A mixed list of all [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) |
6969
| Agno | **Combination of various approaches** Returns a [RunResponse](https://docs.agno.com/reference/agents/run-response) object with text content, messages (essentially chat history including inputs and instructions), reasoning and thinking text properties. Secondary events could potentially be extracted from messages. | **Approach 2** Returns [RunResponseEvent](https://docs.agno.com/reference/agents/run-response#runresponseevent-types-and-attributes) objects including tool call, memory update, etc, information, where the [RunResponseCompletedEvent](https://docs.agno.com/reference/agents/run-response#runresponsecompletedevent) has similar properties to RunResponse|
7070
| A2A | **Approach 3** Returns a [Task or Message](https://a2aproject.github.io/A2A/latest/specification/#71-messagesend) where the message is the final result (Primary) and task is a reference to a long running process. | **Approach 2** Returns a [stream](https://a2aproject.github.io/A2A/latest/specification/#72-messagestream) that contains task updates (Secondary) and a final message (Primary) |
@@ -496,7 +496,7 @@ We need to decide what AIContent types, each agent response type will be mapped
496496
|-|-|
497497
| AutoGen | **Approach 1** Supports [configuring an agent](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/agents.html#structured-output) at agent creation. |
498498
| Google ADK | **Approach 1** Both [input and output schemas can be specified for LLM Agents](https://google.github.io/adk-docs/agents/llm-agents/#structuring-data-input_schema-output_schema-output_key) at construction time. This option is specific to this agent type and other agent types do not necessarily support |
499-
| AWS (Strands) | **Approach 2** Supports a special invocation method called [structured_output](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent/#strands.agent.agent.Agent.structured_output) |
499+
| AWS (Strands) | **Approach 2** Supports a special invocation method called [structured_output](https://strandsagents.com/docs/api/python/strands.agent.agent/) |
500500
| LangGraph | **Approach 1** Supports [configuring an agent](https://langchain-ai.github.io/langgraph/agents/agents/?h=structured#6-configure-structured-output) at agent construction time, and a [structured response](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) can be retrieved as a special property on the agent response |
501501
| Agno | **Approach 1** Supports [configuring an agent](https://docs.agno.com/input-output/structured-output/agent) at agent construction time |
502502
| A2A | **Informal Approach 2** Doesn't formally support schema negotiation, but [hints can be provided via metadata](https://a2a-protocol.org/latest/specification/#97-structured-data-exchange-requesting-and-providing-json) at invocation time |
@@ -508,7 +508,7 @@ We need to decide what AIContent types, each agent response type will be mapped
508508
|-|-|
509509
| AutoGen | Supports a [stop reason](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.TaskResult.stop_reason) which is a freeform text string |
510510
| Google ADK | [No equivalent present](https://github.com/google/adk-python/blob/main/src/google/adk/events/event.py) |
511-
| AWS (Strands) | Exposes a [stop_reason](https://strandsagents.com/latest/documentation/docs/api-reference/python/types/event_loop/#strands.types.event_loop.StopReason) property on the [AgentResult](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent_result/) class with options that are tied closely to LLM operations. |
511+
| AWS (Strands) | Exposes a [stop_reason](https://strandsagents.com/docs/api/python/strands.types.event_loop/) property on the [AgentResult](https://strandsagents.com/docs/api/python/strands.agent.agent_result/) class with options that are tied closely to LLM operations. |
512512
| LangGraph | No equivalent present, output contains only [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) |
513513
| Agno | [No equivalent present](https://docs.agno.com/reference/agents/run-response) |
514514
| A2A | No equivalent present, response only contains a [message](https://a2a-protocol.org/latest/specification/#64-message-object) or [task](https://a2a-protocol.org/latest/specification/#61-task-object). |

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

0 commit comments

Comments
 (0)