openai-agents 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of openai-agents might be problematic. Click here for more details.

agents/agent.py CHANGED
@@ -158,7 +158,7 @@ class Agent(AgentBase, Generic[TContext]):
158
158
  usable with OpenAI models, using the Responses API.
159
159
  """
160
160
 
161
- handoffs: list[Agent[Any] | Handoff[TContext]] = field(default_factory=list)
161
+ handoffs: list[Agent[Any] | Handoff[TContext, Any]] = field(default_factory=list)
162
162
  """Handoffs are sub-agents that the agent can delegate to. You can provide a list of handoffs,
163
163
  and the agent can choose to delegate to them if relevant. Allows for separation of concerns and
164
164
  modularity.
agents/agent_output.py CHANGED
@@ -115,8 +115,8 @@ class AgentOutputSchema(AgentOutputSchemaBase):
115
115
  except UserError as e:
116
116
  raise UserError(
117
117
  "Strict JSON schema is enabled, but the output type is not valid. "
118
- "Either make the output type strict, or pass output_schema_strict=False to "
119
- "your Agent()"
118
+ "Either make the output type strict, "
119
+ "or wrap your type with AgentOutputSchema(your_type, strict_json_schema=False)"
120
120
  ) from e
121
121
 
122
122
  def is_plain_text(self) -> bool:
agents/guardrail.py CHANGED
@@ -244,7 +244,7 @@ def input_guardrail(
244
244
  return InputGuardrail(
245
245
  guardrail_function=f,
246
246
  # If not set, guardrail name uses the function’s name by default.
247
- name=name if name else f.__name__
247
+ name=name if name else f.__name__,
248
248
  )
249
249
 
250
250
  if func is not None:
agents/handoffs.py CHANGED
@@ -18,12 +18,15 @@ from .util import _error_tracing, _json, _transforms
18
18
  from .util._types import MaybeAwaitable
19
19
 
20
20
  if TYPE_CHECKING:
21
- from .agent import Agent
21
+ from .agent import Agent, AgentBase
22
22
 
23
23
 
24
24
  # The handoff input type is the type of data passed when the agent is called via a handoff.
25
25
  THandoffInput = TypeVar("THandoffInput", default=Any)
26
26
 
27
+ # The agent type that the handoff returns
28
+ TAgent = TypeVar("TAgent", bound="AgentBase[Any]", default="Agent[Any]")
29
+
27
30
  OnHandoffWithInput = Callable[[RunContextWrapper[Any], THandoffInput], Any]
28
31
  OnHandoffWithoutInput = Callable[[RunContextWrapper[Any]], Any]
29
32
 
@@ -52,7 +55,7 @@ HandoffInputFilter: TypeAlias = Callable[[HandoffInputData], HandoffInputData]
52
55
 
53
56
 
54
57
  @dataclass
55
- class Handoff(Generic[TContext]):
58
+ class Handoff(Generic[TContext, TAgent]):
56
59
  """A handoff is when an agent delegates a task to another agent.
57
60
  For example, in a customer support scenario you might have a "triage agent" that determines
58
61
  which agent should handle the user's request, and sub-agents that specialize in different
@@ -69,7 +72,7 @@ class Handoff(Generic[TContext]):
69
72
  """The JSON schema for the handoff input. Can be empty if the handoff does not take an input.
70
73
  """
71
74
 
72
- on_invoke_handoff: Callable[[RunContextWrapper[Any], str], Awaitable[Agent[TContext]]]
75
+ on_invoke_handoff: Callable[[RunContextWrapper[Any], str], Awaitable[TAgent]]
73
76
  """The function that invokes the handoff. The parameters passed are:
74
77
  1. The handoff run context
75
78
  2. The arguments from the LLM, as a JSON string. Empty string if input_json_schema is empty.
@@ -100,20 +103,22 @@ class Handoff(Generic[TContext]):
100
103
  True, as it increases the likelihood of correct JSON input.
101
104
  """
102
105
 
103
- is_enabled: bool | Callable[[RunContextWrapper[Any], Agent[Any]], MaybeAwaitable[bool]] = True
106
+ is_enabled: bool | Callable[[RunContextWrapper[Any], AgentBase[Any]], MaybeAwaitable[bool]] = (
107
+ True
108
+ )
104
109
  """Whether the handoff is enabled. Either a bool or a Callable that takes the run context and
105
110
  agent and returns whether the handoff is enabled. You can use this to dynamically enable/disable
106
111
  a handoff based on your context/state."""
107
112
 
108
- def get_transfer_message(self, agent: Agent[Any]) -> str:
113
+ def get_transfer_message(self, agent: AgentBase[Any]) -> str:
109
114
  return json.dumps({"assistant": agent.name})
110
115
 
111
116
  @classmethod
112
- def default_tool_name(cls, agent: Agent[Any]) -> str:
117
+ def default_tool_name(cls, agent: AgentBase[Any]) -> str:
113
118
  return _transforms.transform_string_function_style(f"transfer_to_{agent.name}")
114
119
 
115
120
  @classmethod
116
- def default_tool_description(cls, agent: Agent[Any]) -> str:
121
+ def default_tool_description(cls, agent: AgentBase[Any]) -> str:
117
122
  return (
118
123
  f"Handoff to the {agent.name} agent to handle the request. "
119
124
  f"{agent.handoff_description or ''}"
@@ -128,7 +133,7 @@ def handoff(
128
133
  tool_description_override: str | None = None,
129
134
  input_filter: Callable[[HandoffInputData], HandoffInputData] | None = None,
130
135
  is_enabled: bool | Callable[[RunContextWrapper[Any], Agent[Any]], MaybeAwaitable[bool]] = True,
131
- ) -> Handoff[TContext]: ...
136
+ ) -> Handoff[TContext, Agent[TContext]]: ...
132
137
 
133
138
 
134
139
  @overload
@@ -141,7 +146,7 @@ def handoff(
141
146
  tool_name_override: str | None = None,
142
147
  input_filter: Callable[[HandoffInputData], HandoffInputData] | None = None,
143
148
  is_enabled: bool | Callable[[RunContextWrapper[Any], Agent[Any]], MaybeAwaitable[bool]] = True,
144
- ) -> Handoff[TContext]: ...
149
+ ) -> Handoff[TContext, Agent[TContext]]: ...
145
150
 
146
151
 
147
152
  @overload
@@ -153,7 +158,7 @@ def handoff(
153
158
  tool_name_override: str | None = None,
154
159
  input_filter: Callable[[HandoffInputData], HandoffInputData] | None = None,
155
160
  is_enabled: bool | Callable[[RunContextWrapper[Any], Agent[Any]], MaybeAwaitable[bool]] = True,
156
- ) -> Handoff[TContext]: ...
161
+ ) -> Handoff[TContext, Agent[TContext]]: ...
157
162
 
158
163
 
159
164
  def handoff(
@@ -163,8 +168,9 @@ def handoff(
163
168
  on_handoff: OnHandoffWithInput[THandoffInput] | OnHandoffWithoutInput | None = None,
164
169
  input_type: type[THandoffInput] | None = None,
165
170
  input_filter: Callable[[HandoffInputData], HandoffInputData] | None = None,
166
- is_enabled: bool | Callable[[RunContextWrapper[Any], Agent[Any]], MaybeAwaitable[bool]] = True,
167
- ) -> Handoff[TContext]:
171
+ is_enabled: bool
172
+ | Callable[[RunContextWrapper[Any], Agent[TContext]], MaybeAwaitable[bool]] = True,
173
+ ) -> Handoff[TContext, Agent[TContext]]:
168
174
  """Create a handoff from an agent.
169
175
 
170
176
  Args:
@@ -202,7 +208,7 @@ def handoff(
202
208
 
203
209
  async def _invoke_handoff(
204
210
  ctx: RunContextWrapper[Any], input_json: str | None = None
205
- ) -> Agent[Any]:
211
+ ) -> Agent[TContext]:
206
212
  if input_type is not None and type_adapter is not None:
207
213
  if input_json is None:
208
214
  _error_tracing.attach_error_to_current_span(
@@ -239,6 +245,18 @@ def handoff(
239
245
  # If there is a need, we can make this configurable in the future
240
246
  input_json_schema = ensure_strict_json_schema(input_json_schema)
241
247
 
248
+ async def _is_enabled(ctx: RunContextWrapper[Any], agent_base: AgentBase[Any]) -> bool:
249
+ from .agent import Agent
250
+
251
+ assert callable(is_enabled), "is_enabled must be non-null here"
252
+ assert isinstance(agent_base, Agent), "Can't handoff to a non-Agent"
253
+ result = is_enabled(ctx, agent_base)
254
+
255
+ if inspect.isawaitable(result):
256
+ return await result
257
+
258
+ return result
259
+
242
260
  return Handoff(
243
261
  tool_name=tool_name,
244
262
  tool_description=tool_description,
@@ -246,5 +264,5 @@ def handoff(
246
264
  on_invoke_handoff=_invoke_handoff,
247
265
  input_filter=input_filter,
248
266
  agent_name=agent.name,
249
- is_enabled=is_enabled,
267
+ is_enabled=_is_enabled if callable(is_enabled) else is_enabled,
250
268
  )
agents/mcp/server.py CHANGED
@@ -28,6 +28,17 @@ if TYPE_CHECKING:
28
28
  class MCPServer(abc.ABC):
29
29
  """Base class for Model Context Protocol servers."""
30
30
 
31
+ def __init__(self, use_structured_content: bool = False):
32
+ """
33
+ Args:
34
+ use_structured_content: Whether to use `tool_result.structured_content` when calling an
35
+ MCP tool.Defaults to False for backwards compatibility - most MCP servers still
36
+ include the structured content in the `tool_result.content`, and using it by
37
+ default will cause duplicate content. You can set this to True if you know the
38
+ server will not duplicate the structured content in the `tool_result.content`.
39
+ """
40
+ self.use_structured_content = use_structured_content
41
+
31
42
  @abc.abstractmethod
32
43
  async def connect(self):
33
44
  """Connect to the server. For example, this might mean spawning a subprocess or
@@ -86,6 +97,7 @@ class _MCPServerWithClientSession(MCPServer, abc.ABC):
86
97
  cache_tools_list: bool,
87
98
  client_session_timeout_seconds: float | None,
88
99
  tool_filter: ToolFilter = None,
100
+ use_structured_content: bool = False,
89
101
  ):
90
102
  """
91
103
  Args:
@@ -98,7 +110,13 @@ class _MCPServerWithClientSession(MCPServer, abc.ABC):
98
110
 
99
111
  client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
100
112
  tool_filter: The tool filter to use for filtering tools.
113
+ use_structured_content: Whether to use `tool_result.structured_content` when calling an
114
+ MCP tool. Defaults to False for backwards compatibility - most MCP servers still
115
+ include the structured content in the `tool_result.content`, and using it by
116
+ default will cause duplicate content. You can set this to True if you know the
117
+ server will not duplicate the structured content in the `tool_result.content`.
101
118
  """
119
+ super().__init__(use_structured_content=use_structured_content)
102
120
  self.session: ClientSession | None = None
103
121
  self.exit_stack: AsyncExitStack = AsyncExitStack()
104
122
  self._cleanup_lock: asyncio.Lock = asyncio.Lock()
@@ -346,6 +364,7 @@ class MCPServerStdio(_MCPServerWithClientSession):
346
364
  name: str | None = None,
347
365
  client_session_timeout_seconds: float | None = 5,
348
366
  tool_filter: ToolFilter = None,
367
+ use_structured_content: bool = False,
349
368
  ):
350
369
  """Create a new MCP server based on the stdio transport.
351
370
 
@@ -364,11 +383,17 @@ class MCPServerStdio(_MCPServerWithClientSession):
364
383
  command.
365
384
  client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
366
385
  tool_filter: The tool filter to use for filtering tools.
386
+ use_structured_content: Whether to use `tool_result.structured_content` when calling an
387
+ MCP tool. Defaults to False for backwards compatibility - most MCP servers still
388
+ include the structured content in the `tool_result.content`, and using it by
389
+ default will cause duplicate content. You can set this to True if you know the
390
+ server will not duplicate the structured content in the `tool_result.content`.
367
391
  """
368
392
  super().__init__(
369
393
  cache_tools_list,
370
394
  client_session_timeout_seconds,
371
395
  tool_filter,
396
+ use_structured_content,
372
397
  )
373
398
 
374
399
  self.params = StdioServerParameters(
@@ -429,6 +454,7 @@ class MCPServerSse(_MCPServerWithClientSession):
429
454
  name: str | None = None,
430
455
  client_session_timeout_seconds: float | None = 5,
431
456
  tool_filter: ToolFilter = None,
457
+ use_structured_content: bool = False,
432
458
  ):
433
459
  """Create a new MCP server based on the HTTP with SSE transport.
434
460
 
@@ -449,11 +475,17 @@ class MCPServerSse(_MCPServerWithClientSession):
449
475
 
450
476
  client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
451
477
  tool_filter: The tool filter to use for filtering tools.
478
+ use_structured_content: Whether to use `tool_result.structured_content` when calling an
479
+ MCP tool. Defaults to False for backwards compatibility - most MCP servers still
480
+ include the structured content in the `tool_result.content`, and using it by
481
+ default will cause duplicate content. You can set this to True if you know the
482
+ server will not duplicate the structured content in the `tool_result.content`.
452
483
  """
453
484
  super().__init__(
454
485
  cache_tools_list,
455
486
  client_session_timeout_seconds,
456
487
  tool_filter,
488
+ use_structured_content,
457
489
  )
458
490
 
459
491
  self.params = params
@@ -514,6 +546,7 @@ class MCPServerStreamableHttp(_MCPServerWithClientSession):
514
546
  name: str | None = None,
515
547
  client_session_timeout_seconds: float | None = 5,
516
548
  tool_filter: ToolFilter = None,
549
+ use_structured_content: bool = False,
517
550
  ):
518
551
  """Create a new MCP server based on the Streamable HTTP transport.
519
552
 
@@ -535,11 +568,17 @@ class MCPServerStreamableHttp(_MCPServerWithClientSession):
535
568
 
536
569
  client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
537
570
  tool_filter: The tool filter to use for filtering tools.
571
+ use_structured_content: Whether to use `tool_result.structured_content` when calling an
572
+ MCP tool. Defaults to False for backwards compatibility - most MCP servers still
573
+ include the structured content in the `tool_result.content`, and using it by
574
+ default will cause duplicate content. You can set this to True if you know the
575
+ server will not duplicate the structured content in the `tool_result.content`.
538
576
  """
539
577
  super().__init__(
540
578
  cache_tools_list,
541
579
  client_session_timeout_seconds,
542
580
  tool_filter,
581
+ use_structured_content,
543
582
  )
544
583
 
545
584
  self.params = params
agents/mcp/util.py CHANGED
@@ -198,11 +198,19 @@ class MCPUtil:
198
198
  # string. We'll try to convert.
199
199
  if len(result.content) == 1:
200
200
  tool_output = result.content[0].model_dump_json()
201
+ # Append structured content if it exists and we're using it.
202
+ if server.use_structured_content and result.structuredContent:
203
+ tool_output = f"{tool_output}\n{json.dumps(result.structuredContent)}"
201
204
  elif len(result.content) > 1:
202
- tool_output = json.dumps([item.model_dump(mode="json") for item in result.content])
205
+ tool_results = [item.model_dump(mode="json") for item in result.content]
206
+ if server.use_structured_content and result.structuredContent:
207
+ tool_results.append(result.structuredContent)
208
+ tool_output = json.dumps(tool_results)
209
+ elif server.use_structured_content and result.structuredContent:
210
+ tool_output = json.dumps(result.structuredContent)
203
211
  else:
204
- logger.error(f"Errored MCP tool result: {result}")
205
- tool_output = "Error running tool."
212
+ # Empty content is a valid result (e.g., "no results found")
213
+ tool_output = "[]"
206
214
 
207
215
  current_span = get_current_span()
208
216
  if current_span:
@@ -484,7 +484,7 @@ class Converter:
484
484
  )
485
485
 
486
486
  @classmethod
487
- def convert_handoff_tool(cls, handoff: Handoff[Any]) -> ChatCompletionToolParam:
487
+ def convert_handoff_tool(cls, handoff: Handoff[Any, Any]) -> ChatCompletionToolParam:
488
488
  return {
489
489
  "type": "function",
490
490
  "function": {
@@ -53,6 +53,9 @@ class StreamingState:
53
53
  refusal_content_index_and_output: tuple[int, ResponseOutputRefusal] | None = None
54
54
  reasoning_content_index_and_output: tuple[int, ResponseReasoningItem] | None = None
55
55
  function_calls: dict[int, ResponseFunctionToolCall] = field(default_factory=dict)
56
+ # Fields for real-time function call streaming
57
+ function_call_streaming: dict[int, bool] = field(default_factory=dict)
58
+ function_call_output_idx: dict[int, int] = field(default_factory=dict)
56
59
 
57
60
 
58
61
  class SequenceNumber:
@@ -255,9 +258,7 @@ class ChatCmplStreamHandler:
255
258
  # Accumulate the refusal string in the output part
256
259
  state.refusal_content_index_and_output[1].refusal += delta.refusal
257
260
 
258
- # Handle tool calls
259
- # Because we don't know the name of the function until the end of the stream, we'll
260
- # save everything and yield events at the end
261
+ # Handle tool calls with real-time streaming support
261
262
  if delta.tool_calls:
262
263
  for tc_delta in delta.tool_calls:
263
264
  if tc_delta.index not in state.function_calls:
@@ -268,15 +269,76 @@ class ChatCmplStreamHandler:
268
269
  type="function_call",
269
270
  call_id="",
270
271
  )
272
+ state.function_call_streaming[tc_delta.index] = False
273
+
271
274
  tc_function = tc_delta.function
272
275
 
276
+ # Accumulate arguments as they come in
273
277
  state.function_calls[tc_delta.index].arguments += (
274
278
  tc_function.arguments if tc_function else ""
275
279
  ) or ""
276
- state.function_calls[tc_delta.index].name += (
277
- tc_function.name if tc_function else ""
278
- ) or ""
279
- state.function_calls[tc_delta.index].call_id = tc_delta.id or ""
280
+
281
+ # Set function name directly (it's correct from the first function call chunk)
282
+ if tc_function and tc_function.name:
283
+ state.function_calls[tc_delta.index].name = tc_function.name
284
+
285
+ if tc_delta.id:
286
+ state.function_calls[tc_delta.index].call_id = tc_delta.id
287
+
288
+ function_call = state.function_calls[tc_delta.index]
289
+
290
+ # Start streaming as soon as we have function name and call_id
291
+ if (not state.function_call_streaming[tc_delta.index] and
292
+ function_call.name and
293
+ function_call.call_id):
294
+
295
+ # Calculate the output index for this function call
296
+ function_call_starting_index = 0
297
+ if state.reasoning_content_index_and_output:
298
+ function_call_starting_index += 1
299
+ if state.text_content_index_and_output:
300
+ function_call_starting_index += 1
301
+ if state.refusal_content_index_and_output:
302
+ function_call_starting_index += 1
303
+
304
+ # Add offset for already started function calls
305
+ function_call_starting_index += sum(
306
+ 1 for streaming in state.function_call_streaming.values() if streaming
307
+ )
308
+
309
+ # Mark this function call as streaming and store its output index
310
+ state.function_call_streaming[tc_delta.index] = True
311
+ state.function_call_output_idx[
312
+ tc_delta.index
313
+ ] = function_call_starting_index
314
+
315
+ # Send initial function call added event
316
+ yield ResponseOutputItemAddedEvent(
317
+ item=ResponseFunctionToolCall(
318
+ id=FAKE_RESPONSES_ID,
319
+ call_id=function_call.call_id,
320
+ arguments="", # Start with empty arguments
321
+ name=function_call.name,
322
+ type="function_call",
323
+ ),
324
+ output_index=function_call_starting_index,
325
+ type="response.output_item.added",
326
+ sequence_number=sequence_number.get_and_increment(),
327
+ )
328
+
329
+ # Stream arguments if we've started streaming this function call
330
+ if (state.function_call_streaming.get(tc_delta.index, False) and
331
+ tc_function and
332
+ tc_function.arguments):
333
+
334
+ output_index = state.function_call_output_idx[tc_delta.index]
335
+ yield ResponseFunctionCallArgumentsDeltaEvent(
336
+ delta=tc_function.arguments,
337
+ item_id=FAKE_RESPONSES_ID,
338
+ output_index=output_index,
339
+ type="response.function_call_arguments.delta",
340
+ sequence_number=sequence_number.get_and_increment(),
341
+ )
280
342
 
281
343
  if state.reasoning_content_index_and_output:
282
344
  yield ResponseReasoningSummaryPartDoneEvent(
@@ -327,42 +389,71 @@ class ChatCmplStreamHandler:
327
389
  sequence_number=sequence_number.get_and_increment(),
328
390
  )
329
391
 
330
- # Actually send events for the function calls
331
- for function_call in state.function_calls.values():
332
- # First, a ResponseOutputItemAdded for the function call
333
- yield ResponseOutputItemAddedEvent(
334
- item=ResponseFunctionToolCall(
335
- id=FAKE_RESPONSES_ID,
336
- call_id=function_call.call_id,
337
- arguments=function_call.arguments,
338
- name=function_call.name,
339
- type="function_call",
340
- ),
341
- output_index=function_call_starting_index,
342
- type="response.output_item.added",
343
- sequence_number=sequence_number.get_and_increment(),
344
- )
345
- # Then, yield the args
346
- yield ResponseFunctionCallArgumentsDeltaEvent(
347
- delta=function_call.arguments,
348
- item_id=FAKE_RESPONSES_ID,
349
- output_index=function_call_starting_index,
350
- type="response.function_call_arguments.delta",
351
- sequence_number=sequence_number.get_and_increment(),
352
- )
353
- # Finally, the ResponseOutputItemDone
354
- yield ResponseOutputItemDoneEvent(
355
- item=ResponseFunctionToolCall(
356
- id=FAKE_RESPONSES_ID,
357
- call_id=function_call.call_id,
358
- arguments=function_call.arguments,
359
- name=function_call.name,
360
- type="function_call",
361
- ),
362
- output_index=function_call_starting_index,
363
- type="response.output_item.done",
364
- sequence_number=sequence_number.get_and_increment(),
365
- )
392
+ # Send completion events for function calls
393
+ for index, function_call in state.function_calls.items():
394
+ if state.function_call_streaming.get(index, False):
395
+ # Function call was streamed, just send the completion event
396
+ output_index = state.function_call_output_idx[index]
397
+ yield ResponseOutputItemDoneEvent(
398
+ item=ResponseFunctionToolCall(
399
+ id=FAKE_RESPONSES_ID,
400
+ call_id=function_call.call_id,
401
+ arguments=function_call.arguments,
402
+ name=function_call.name,
403
+ type="function_call",
404
+ ),
405
+ output_index=output_index,
406
+ type="response.output_item.done",
407
+ sequence_number=sequence_number.get_and_increment(),
408
+ )
409
+ else:
410
+ # Function call was not streamed (fallback to old behavior)
411
+ # This handles edge cases where function name never arrived
412
+ fallback_starting_index = 0
413
+ if state.reasoning_content_index_and_output:
414
+ fallback_starting_index += 1
415
+ if state.text_content_index_and_output:
416
+ fallback_starting_index += 1
417
+ if state.refusal_content_index_and_output:
418
+ fallback_starting_index += 1
419
+
420
+ # Add offset for already started function calls
421
+ fallback_starting_index += sum(
422
+ 1 for streaming in state.function_call_streaming.values() if streaming
423
+ )
424
+
425
+ # Send all events at once (backward compatibility)
426
+ yield ResponseOutputItemAddedEvent(
427
+ item=ResponseFunctionToolCall(
428
+ id=FAKE_RESPONSES_ID,
429
+ call_id=function_call.call_id,
430
+ arguments=function_call.arguments,
431
+ name=function_call.name,
432
+ type="function_call",
433
+ ),
434
+ output_index=fallback_starting_index,
435
+ type="response.output_item.added",
436
+ sequence_number=sequence_number.get_and_increment(),
437
+ )
438
+ yield ResponseFunctionCallArgumentsDeltaEvent(
439
+ delta=function_call.arguments,
440
+ item_id=FAKE_RESPONSES_ID,
441
+ output_index=fallback_starting_index,
442
+ type="response.function_call_arguments.delta",
443
+ sequence_number=sequence_number.get_and_increment(),
444
+ )
445
+ yield ResponseOutputItemDoneEvent(
446
+ item=ResponseFunctionToolCall(
447
+ id=FAKE_RESPONSES_ID,
448
+ call_id=function_call.call_id,
449
+ arguments=function_call.arguments,
450
+ name=function_call.name,
451
+ type="function_call",
452
+ ),
453
+ output_index=fallback_starting_index,
454
+ type="response.output_item.done",
455
+ sequence_number=sequence_number.get_and_increment(),
456
+ )
366
457
 
367
458
  # Finally, send the Response completed event
368
459
  outputs: list[ResponseOutputItem] = []
@@ -370,7 +370,7 @@ class Converter:
370
370
  def convert_tools(
371
371
  cls,
372
372
  tools: list[Tool],
373
- handoffs: list[Handoff[Any]],
373
+ handoffs: list[Handoff[Any, Any]],
374
374
  ) -> ConvertedTools:
375
375
  converted_tools: list[ToolParam] = []
376
376
  includes: list[ResponseIncludable] = []
@@ -30,6 +30,7 @@ from .events import (
30
30
  RealtimeToolEnd,
31
31
  RealtimeToolStart,
32
32
  )
33
+ from .handoffs import realtime_handoff
33
34
  from .items import (
34
35
  AssistantMessageItem,
35
36
  AssistantText,
@@ -92,6 +93,8 @@ __all__ = [
92
93
  "RealtimeAgentHooks",
93
94
  "RealtimeRunHooks",
94
95
  "RealtimeRunner",
96
+ # Handoffs
97
+ "realtime_handoff",
95
98
  # Config
96
99
  "RealtimeAudioFormat",
97
100
  "RealtimeClientMessage",
agents/realtime/agent.py CHANGED
@@ -3,10 +3,11 @@ from __future__ import annotations
3
3
  import dataclasses
4
4
  import inspect
5
5
  from collections.abc import Awaitable
6
- from dataclasses import dataclass
6
+ from dataclasses import dataclass, field
7
7
  from typing import Any, Callable, Generic, cast
8
8
 
9
9
  from ..agent import AgentBase
10
+ from ..handoffs import Handoff
10
11
  from ..lifecycle import AgentHooksBase, RunHooksBase
11
12
  from ..logger import logger
12
13
  from ..run_context import RunContextWrapper, TContext
@@ -53,6 +54,14 @@ class RealtimeAgent(AgentBase, Generic[TContext]):
53
54
  return a string.
54
55
  """
55
56
 
57
+ handoffs: list[RealtimeAgent[Any] | Handoff[TContext, RealtimeAgent[Any]]] = field(
58
+ default_factory=list
59
+ )
60
+ """Handoffs are sub-agents that the agent can delegate to. You can provide a list of handoffs,
61
+ and the agent can choose to delegate to them if relevant. Allows for separation of concerns and
62
+ modularity.
63
+ """
64
+
56
65
  hooks: RealtimeAgentHooks | None = None
57
66
  """A class that receives callbacks on various lifecycle events for this agent.
58
67
  """