Skip to content

Commit 827af41

Browse files
authored
feat: add more options to Agent#as_tool function (#1751)
This pull request adds all the runner arguments (run_config, max_turns, hooks, previous_response_id, conversation_id, session) to`Agent#as_tool()` method. This feature addition is beneficial for developers who want to customize the agent's behavior when the agent is passed as a tool. This aligns with TS SDK's changes: openai/openai-agents-js#481
1 parent aed6359 commit 827af41

File tree

2 files changed

+192
-3
lines changed

2 files changed

+192
-3
lines changed

src/agents/agent.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@
3030
from .util._types import MaybeAwaitable
3131

3232
if TYPE_CHECKING:
33-
from .lifecycle import AgentHooks
33+
from .lifecycle import AgentHooks, RunHooks
3434
from .mcp import MCPServer
35+
from .memory.session import Session
3536
from .result import RunResult
37+
from .run import RunConfig
3638

3739

3840
@dataclass
@@ -384,6 +386,12 @@ def as_tool(
384386
custom_output_extractor: Callable[[RunResult], Awaitable[str]] | None = None,
385387
is_enabled: bool
386388
| Callable[[RunContextWrapper[Any], AgentBase[Any]], MaybeAwaitable[bool]] = True,
389+
run_config: RunConfig | None = None,
390+
max_turns: int | None = None,
391+
hooks: RunHooks[TContext] | None = None,
392+
previous_response_id: str | None = None,
393+
conversation_id: str | None = None,
394+
session: Session | None = None,
387395
) -> Tool:
388396
"""Transform this agent into a tool, callable by other agents.
389397
@@ -410,12 +418,20 @@ def as_tool(
410418
is_enabled=is_enabled,
411419
)
412420
async def run_agent(context: RunContextWrapper, input: str) -> str:
413-
from .run import Runner
421+
from .run import DEFAULT_MAX_TURNS, Runner
422+
423+
resolved_max_turns = max_turns if max_turns is not None else DEFAULT_MAX_TURNS
414424

415425
output = await Runner.run(
416426
starting_agent=self,
417427
input=input,
418428
context=context.context,
429+
run_config=run_config,
430+
max_turns=resolved_max_turns,
431+
hooks=hooks,
432+
previous_response_id=previous_response_id,
433+
conversation_id=conversation_id,
434+
session=session,
419435
)
420436
if custom_output_extractor:
421437
return await custom_output_extractor(output)

tests/test_agent_as_tool.py

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,24 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
15
import pytest
6+
from openai.types.responses import ResponseOutputMessage, ResponseOutputText
27
from pydantic import BaseModel
38

4-
from agents import Agent, AgentBase, FunctionTool, RunContextWrapper
9+
from agents import (
10+
Agent,
11+
AgentBase,
12+
FunctionTool,
13+
MessageOutputItem,
14+
RunConfig,
15+
RunContextWrapper,
16+
RunHooks,
17+
Runner,
18+
Session,
19+
TResponseInputItem,
20+
)
21+
from agents.tool_context import ToolContext
522

623

724
class BoolCtx(BaseModel):
@@ -205,3 +222,159 @@ async def custom_extractor(result):
205222
tools = await orchestrator.get_all_tools(context)
206223
assert len(tools) == 1
207224
assert tools[0].name == "custom_tool_name"
225+
226+
227+
@pytest.mark.asyncio
228+
async def test_agent_as_tool_returns_concatenated_text(monkeypatch: pytest.MonkeyPatch) -> None:
229+
"""Agent tool should use default text aggregation when no custom extractor is provided."""
230+
231+
agent = Agent(name="storyteller")
232+
233+
message = ResponseOutputMessage(
234+
id="msg_1",
235+
role="assistant",
236+
status="completed",
237+
type="message",
238+
content=[
239+
ResponseOutputText(
240+
annotations=[],
241+
text="Hello world",
242+
type="output_text",
243+
logprobs=None,
244+
)
245+
],
246+
)
247+
248+
result = type(
249+
"DummyResult",
250+
(),
251+
{"new_items": [MessageOutputItem(agent=agent, raw_item=message)]},
252+
)()
253+
254+
async def fake_run(
255+
cls,
256+
starting_agent,
257+
input,
258+
*,
259+
context,
260+
max_turns,
261+
hooks,
262+
run_config,
263+
previous_response_id,
264+
conversation_id,
265+
session,
266+
):
267+
assert starting_agent is agent
268+
assert input == "hello"
269+
return result
270+
271+
monkeypatch.setattr(Runner, "run", classmethod(fake_run))
272+
273+
tool = agent.as_tool(
274+
tool_name="story_tool",
275+
tool_description="Tell a short story",
276+
is_enabled=True,
277+
)
278+
279+
assert isinstance(tool, FunctionTool)
280+
tool_context = ToolContext(context=None, tool_name="story_tool", tool_call_id="call_1")
281+
output = await tool.on_invoke_tool(tool_context, '{"input": "hello"}')
282+
283+
assert output == "Hello world"
284+
285+
286+
@pytest.mark.asyncio
287+
async def test_agent_as_tool_custom_output_extractor(monkeypatch: pytest.MonkeyPatch) -> None:
288+
"""Custom output extractors should receive the RunResult from Runner.run."""
289+
290+
agent = Agent(name="summarizer")
291+
292+
message = ResponseOutputMessage(
293+
id="msg_2",
294+
role="assistant",
295+
status="completed",
296+
type="message",
297+
content=[
298+
ResponseOutputText(
299+
annotations=[],
300+
text="Original text",
301+
type="output_text",
302+
logprobs=None,
303+
)
304+
],
305+
)
306+
307+
class DummySession(Session):
308+
session_id = "sess_123"
309+
310+
async def get_items(self, limit: int | None = None) -> list[TResponseInputItem]:
311+
return []
312+
313+
async def add_items(self, items: list[TResponseInputItem]) -> None:
314+
return None
315+
316+
async def pop_item(self) -> TResponseInputItem | None:
317+
return None
318+
319+
async def clear_session(self) -> None:
320+
return None
321+
322+
dummy_session = DummySession()
323+
324+
class DummyResult:
325+
def __init__(self, items: list[MessageOutputItem]) -> None:
326+
self.new_items = items
327+
328+
run_result = DummyResult([MessageOutputItem(agent=agent, raw_item=message)])
329+
330+
async def fake_run(
331+
cls,
332+
starting_agent,
333+
input,
334+
*,
335+
context,
336+
max_turns,
337+
hooks,
338+
run_config,
339+
previous_response_id,
340+
conversation_id,
341+
session,
342+
):
343+
assert starting_agent is agent
344+
assert input == "summarize this"
345+
assert context is None
346+
assert max_turns == 7
347+
assert hooks is hooks_obj
348+
assert run_config is run_config_obj
349+
assert previous_response_id == "resp_1"
350+
assert conversation_id == "conv_1"
351+
assert session is dummy_session
352+
return run_result
353+
354+
monkeypatch.setattr(Runner, "run", classmethod(fake_run))
355+
356+
async def extractor(result) -> str:
357+
assert result is run_result
358+
return "custom output"
359+
360+
hooks_obj = RunHooks[Any]()
361+
run_config_obj = RunConfig(model="gpt-4.1-mini")
362+
363+
tool = agent.as_tool(
364+
tool_name="summary_tool",
365+
tool_description="Summarize input",
366+
custom_output_extractor=extractor,
367+
is_enabled=True,
368+
run_config=run_config_obj,
369+
max_turns=7,
370+
hooks=hooks_obj,
371+
previous_response_id="resp_1",
372+
conversation_id="conv_1",
373+
session=dummy_session,
374+
)
375+
376+
assert isinstance(tool, FunctionTool)
377+
tool_context = ToolContext(context=None, tool_name="summary_tool", tool_call_id="call_2")
378+
output = await tool.on_invoke_tool(tool_context, '{"input": "summarize this"}')
379+
380+
assert output == "custom output"

0 commit comments

Comments
 (0)