-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Python: Access agent context in as_tool scenarios
#3731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
as_tool scenariosas_tool scenarios
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Adds an “ambient” agent run context API so code executing during an agent run (notably tools and agent-as-tool wrappers) can access the current AgentContext (thread/messages/options/etc.) without having it explicitly passed through every call layer.
Changes:
- Introduces
get_current_agent_run_context()(and an internalagent_run_scope()context manager) backed bycontextvars. - Sets the ambient context in
AgentMiddlewareLayer.run()for non-streaming runs and streaming runs (via a wrappedResponseStreamin the no-middleware path). - Updates thread propagation so the ambient context can be updated with the resolved thread and conversation id as they become available, plus adds tests for basic scoping behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| python/packages/core/agent_framework/_agent_context.py | New ContextVar-based ambient context getter + scope manager. |
| python/packages/core/agent_framework/_middleware.py | Establishes ambient context during agent runs; adds stream wrapper helper. |
| python/packages/core/agent_framework/_agents.py | Updates ambient context with the resolved thread after _prepare_run_context(). |
| python/packages/core/agent_framework/_tools.py | Updates ambient context thread’s service_thread_id when conversation_id becomes known. |
| python/packages/core/agent_framework/init.py | Re-exports new ambient context API from the package root. |
| python/packages/core/tests/core/test_agent_context.py | New tests for context scoping/isolation and middleware visibility. |
Comments suppressed due to low confidence (2)
python/packages/core/agent_framework/_middleware.py:1240
- In the streaming + middleware case, the ambient agent context is only set while
pipeline.execute(...)runs. The returnedResponseStreamis iterated after_execute()returns, soget_current_agent_run_context()will be unset during stream iteration/tool calls. Wrap theResponseStreamproduced by_execute_stream()with_wrap_stream_with_context(..., context)(similar to the no-middleware streaming path) so context remains available throughout iteration and finalization.
if stream:
# For streaming, wrap execution in ResponseStream.from_awaitable
async def _execute_stream() -> ResponseStream[AgentResponseUpdate, AgentResponse]:
result = await _execute()
if result is None:
# Create empty stream if middleware terminated without setting result
return ResponseStream(_empty_async_iterable())
if isinstance(result, ResponseStream):
return result
# If result is AgentResponse (shouldn't happen for streaming), convert to stream
raise ValueError("Expected ResponseStream for streaming, got AgentResponse")
return ResponseStream.from_awaitable(_execute_stream())
python/packages/core/agent_framework/_agents.py:474
- The
as_tool()docstring no longer documents the exceptions it can raise, but the implementation still raisesTypeError(whenselfisn’t anAgentProtocol) andValueError(when the tool name can’t be determined). Please restore/retain theRaises:section so the docstring matches the actual behavior.
"""Create a FunctionTool that wraps this agent.
Keyword Args:
name: The name for the tool. If None, uses the agent's name.
description: The description for the tool. If None, uses the agent's description or empty string.
arg_name: The name of the function argument (default: "task").
arg_description: The description for the function argument.
If None, defaults to "Task for {tool_name}".
stream_callback: Optional callback for streaming responses. If provided, uses run(..., stream=True).
Returns:
A FunctionTool that can be used as a tool by other agents.
Examples:
.. code-block:: python
from agent_framework import ChatAgent
# Create an agent
agent = ChatAgent(chat_client=client, name="research-agent", description="Performs research tasks")
# Convert the agent to a tool
research_tool = agent.as_tool()
# Use the tool with another agent
coordinator = ChatAgent(chat_client=client, name="coordinator", tools=research_tool)
"""
# Verify that self implements AgentProtocol
if not isinstance(self, AgentProtocol):
raise TypeError(f"Agent {self.__class__.__name__} must implement AgentProtocol to be used as a tool")
tool_name = name or _sanitize_agent_name(self.name)
if tool_name is None:
raise ValueError("Agent tool name cannot be None. Either provide a name parameter or set the agent's name.")
tool_description = description or self.description or ""
Python Test Coverage Report •
Python Unit Test Overview
|
|||||||||||||||||||||||||||||||||||||||||||||
eavanvalkenburg
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This scares me because it looks very complex, we have a mechanism in function tools (add **kwargs) to your definition and then you get additional context, can't we use that?
|
|
||
| async def _iterate_with_context() -> AsyncIterable[AgentResponseUpdate]: | ||
| with agent_run_scope(context): | ||
| async for update in inner_stream: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't consume a stream unless we absolutely have to.
| inner_stream: ResponseStream[AgentResponseUpdate, AgentResponse] = super().run( # type: ignore[misc, assignment] | ||
| messages, stream=True, thread=thread, options=options, **combined_kwargs | ||
| ) | ||
| return _wrap_stream_with_context(inner_stream, context) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like _wrap_stream_with_context function is used only when the pipeline doesn't have middleware registered, but how about the case where middleware exists, should we add wrapping there as well?
| _current_agent_run_context: ContextVar[AgentContext | None] = ContextVar("agent_run_context", default=None) | ||
|
|
||
|
|
||
| def get_current_agent_run_context() -> AgentContext | None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will also need to add an example that will demonstrate how to use this functionality and how it resolves the problem described in the issue associated with this PR.
| from .conftest import MockChatClient | ||
|
|
||
|
|
||
| class TestAgentContext: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New unit tests don't test the case where context is retrieved within the tool, as described in PR description.
Motivation and Context
Adds
get_current_agent_run_context()to enable tools and sub-agents to access their parent agent's run context during execution.Closes #3411
When using agents as tools within other agents, there was no way to access the parent agent's context (conversation_id, messages, thread, etc.) from deeply nested
code. This is needed for:
Changes
New
_agent_context.pymodule:get_current_agent_run_context()- Returns the currentAgentContextorNoneif outside an agent runagent_run_scope()- Context manager for setting ambient context (internal use)_middleware.py: Sets ambient context inAgentMiddlewareLayer.run()for both streaming and non-streaming_agents.py: Updates context with resolved thread after_prepare_run_context()_tools.py: Updates thread'sservice_thread_idwhen conversation_id becomes availableUsage
Description
Contribution Checklist