openai-agents 0.2.10__py3-none-any.whl → 0.3.0__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.

@@ -35,7 +35,16 @@ from .events import (
35
35
  RealtimeToolStart,
36
36
  )
37
37
  from .handoffs import realtime_handoff
38
- from .items import AssistantAudio, InputAudio, InputText, RealtimeItem
38
+ from .items import (
39
+ AssistantAudio,
40
+ AssistantMessageItem,
41
+ AssistantText,
42
+ InputAudio,
43
+ InputImage,
44
+ InputText,
45
+ RealtimeItem,
46
+ UserMessageItem,
47
+ )
39
48
  from .model import RealtimeModel, RealtimeModelConfig, RealtimeModelListener
40
49
  from .model_events import (
41
50
  RealtimeModelEvent,
@@ -95,6 +104,12 @@ class RealtimeSession(RealtimeModelListener):
95
104
  self._history: list[RealtimeItem] = []
96
105
  self._model_config = model_config or {}
97
106
  self._run_config = run_config or {}
107
+ initial_model_settings = self._model_config.get("initial_model_settings")
108
+ run_config_settings = self._run_config.get("model_settings")
109
+ self._base_model_settings: RealtimeSessionModelSettings = {
110
+ **(run_config_settings or {}),
111
+ **(initial_model_settings or {}),
112
+ }
98
113
  self._event_queue: asyncio.Queue[RealtimeSessionEvent] = asyncio.Queue()
99
114
  self._closed = False
100
115
  self._stored_exception: Exception | None = None
@@ -224,10 +239,17 @@ class RealtimeSession(RealtimeModelListener):
224
239
  )
225
240
  )
226
241
  elif event.type == "input_audio_transcription_completed":
242
+ prev_len = len(self._history)
227
243
  self._history = RealtimeSession._get_new_history(self._history, event)
228
- await self._put_event(
229
- RealtimeHistoryUpdated(info=self._event_info, history=self._history)
230
- )
244
+ # If a new user item was appended (no existing item),
245
+ # emit history_added for incremental UIs.
246
+ if len(self._history) > prev_len and len(self._history) > 0:
247
+ new_item = self._history[-1]
248
+ await self._put_event(RealtimeHistoryAdded(info=self._event_info, item=new_item))
249
+ else:
250
+ await self._put_event(
251
+ RealtimeHistoryUpdated(info=self._event_info, history=self._history)
252
+ )
231
253
  elif event.type == "input_audio_timeout_triggered":
232
254
  await self._put_event(
233
255
  RealtimeInputAudioTimeoutTriggered(
@@ -242,6 +264,13 @@ class RealtimeSession(RealtimeModelListener):
242
264
  self._item_guardrail_run_counts[item_id] = 0
243
265
 
244
266
  self._item_transcripts[item_id] += event.delta
267
+ self._history = self._get_new_history(
268
+ self._history,
269
+ AssistantMessageItem(
270
+ item_id=item_id,
271
+ content=[AssistantAudio(transcript=self._item_transcripts[item_id])],
272
+ ),
273
+ )
245
274
 
246
275
  # Check if we should run guardrails based on debounce threshold
247
276
  current_length = len(self._item_transcripts[item_id])
@@ -291,7 +320,7 @@ class RealtimeSession(RealtimeModelListener):
291
320
 
292
321
  # If still missing and this is an assistant item, fall back to
293
322
  # accumulated transcript deltas tracked during the turn.
294
- if not preserved and incoming_item.role == "assistant":
323
+ if incoming_item.role == "assistant":
295
324
  preserved = self._item_transcripts.get(incoming_item.item_id)
296
325
 
297
326
  if preserved:
@@ -456,9 +485,9 @@ class RealtimeSession(RealtimeModelListener):
456
485
  old_history: list[RealtimeItem],
457
486
  event: RealtimeModelInputAudioTranscriptionCompletedEvent | RealtimeItem,
458
487
  ) -> list[RealtimeItem]:
459
- # Merge transcript into placeholder input_audio message.
460
488
  if isinstance(event, RealtimeModelInputAudioTranscriptionCompletedEvent):
461
489
  new_history: list[RealtimeItem] = []
490
+ existing_item_found = False
462
491
  for item in old_history:
463
492
  if item.item_id == event.item_id and item.type == "message" and item.role == "user":
464
493
  content: list[InputText | InputAudio] = []
@@ -471,11 +500,18 @@ class RealtimeSession(RealtimeModelListener):
471
500
  new_history.append(
472
501
  item.model_copy(update={"content": content, "status": "completed"})
473
502
  )
503
+ existing_item_found = True
474
504
  else:
475
505
  new_history.append(item)
506
+
507
+ if existing_item_found is False:
508
+ new_history.append(
509
+ UserMessageItem(
510
+ item_id=event.item_id, content=[InputText(text=event.transcript)]
511
+ )
512
+ )
476
513
  return new_history
477
514
 
478
- # Otherwise it's just a new item
479
515
  # TODO (rm) Add support for audio storage config
480
516
 
481
517
  # If the item already exists, update it
@@ -484,8 +520,122 @@ class RealtimeSession(RealtimeModelListener):
484
520
  )
485
521
  if existing_index is not None:
486
522
  new_history = old_history.copy()
487
- new_history[existing_index] = event
523
+ if event.type == "message" and event.content is not None and len(event.content) > 0:
524
+ existing_item = old_history[existing_index]
525
+ if existing_item.type == "message":
526
+ # Merge content preserving existing transcript/text when incoming entry is empty
527
+ if event.role == "assistant" and existing_item.role == "assistant":
528
+ assistant_existing_content = existing_item.content
529
+ assistant_incoming = event.content
530
+ assistant_new_content: list[AssistantText | AssistantAudio] = []
531
+ for idx, ac in enumerate(assistant_incoming):
532
+ if idx >= len(assistant_existing_content):
533
+ assistant_new_content.append(ac)
534
+ continue
535
+ assistant_current = assistant_existing_content[idx]
536
+ if ac.type == "audio":
537
+ if ac.transcript is None:
538
+ assistant_new_content.append(assistant_current)
539
+ else:
540
+ assistant_new_content.append(ac)
541
+ else: # text
542
+ cur_text = (
543
+ assistant_current.text
544
+ if isinstance(assistant_current, AssistantText)
545
+ else None
546
+ )
547
+ if cur_text is not None and ac.text is None:
548
+ assistant_new_content.append(assistant_current)
549
+ else:
550
+ assistant_new_content.append(ac)
551
+ updated_assistant = event.model_copy(
552
+ update={"content": assistant_new_content}
553
+ )
554
+ new_history[existing_index] = updated_assistant
555
+ elif event.role == "user" and existing_item.role == "user":
556
+ user_existing_content = existing_item.content
557
+ user_incoming = event.content
558
+
559
+ # Start from incoming content (prefer latest fields)
560
+ user_new_content: list[InputText | InputAudio | InputImage] = list(
561
+ user_incoming
562
+ )
563
+
564
+ # Merge by type with special handling for images and transcripts
565
+ def _image_url_str(val: object) -> str | None:
566
+ if isinstance(val, InputImage):
567
+ return val.image_url or None
568
+ return None
569
+
570
+ # 1) Preserve any existing images that are missing from the incoming payload
571
+ incoming_image_urls: set[str] = set()
572
+ for part in user_incoming:
573
+ if isinstance(part, InputImage):
574
+ u = _image_url_str(part)
575
+ if u:
576
+ incoming_image_urls.add(u)
577
+
578
+ missing_images: list[InputImage] = []
579
+ for part in user_existing_content:
580
+ if isinstance(part, InputImage):
581
+ u = _image_url_str(part)
582
+ if u and u not in incoming_image_urls:
583
+ missing_images.append(part)
584
+
585
+ # Insert missing images at the beginning to keep them visible and stable
586
+ if missing_images:
587
+ user_new_content = missing_images + user_new_content
588
+
589
+ # 2) For text/audio entries, preserve existing when incoming entry is empty
590
+ merged: list[InputText | InputAudio | InputImage] = []
591
+ for idx, uc in enumerate(user_new_content):
592
+ if uc.type == "input_audio":
593
+ # Attempt to preserve transcript if empty
594
+ transcript = getattr(uc, "transcript", None)
595
+ if transcript is None and idx < len(user_existing_content):
596
+ prev = user_existing_content[idx]
597
+ if isinstance(prev, InputAudio) and prev.transcript is not None:
598
+ uc = uc.model_copy(update={"transcript": prev.transcript})
599
+ merged.append(uc)
600
+ elif uc.type == "input_text":
601
+ text = getattr(uc, "text", None)
602
+ if (text is None or text == "") and idx < len(
603
+ user_existing_content
604
+ ):
605
+ prev = user_existing_content[idx]
606
+ if isinstance(prev, InputText) and prev.text:
607
+ uc = uc.model_copy(update={"text": prev.text})
608
+ merged.append(uc)
609
+ else:
610
+ merged.append(uc)
611
+
612
+ updated_user = event.model_copy(update={"content": merged})
613
+ new_history[existing_index] = updated_user
614
+ elif event.role == "system" and existing_item.role == "system":
615
+ system_existing_content = existing_item.content
616
+ system_incoming = event.content
617
+ # Prefer existing non-empty text when incoming is empty
618
+ system_new_content: list[InputText] = []
619
+ for idx, sc in enumerate(system_incoming):
620
+ if idx >= len(system_existing_content):
621
+ system_new_content.append(sc)
622
+ continue
623
+ system_current = system_existing_content[idx]
624
+ cur_text = system_current.text
625
+ if cur_text is not None and sc.text is None:
626
+ system_new_content.append(system_current)
627
+ else:
628
+ system_new_content.append(sc)
629
+ updated_system = event.model_copy(update={"content": system_new_content})
630
+ new_history[existing_index] = updated_system
631
+ else:
632
+ # Role changed or mismatched; just replace
633
+ new_history[existing_index] = event
634
+ else:
635
+ # If the existing item is not a message, just replace it.
636
+ new_history[existing_index] = event
488
637
  return new_history
638
+
489
639
  # Otherwise, insert it after the previous_item_id if that is set
490
640
  elif event.previous_item_id:
491
641
  # Insert the new item after the previous item
@@ -619,12 +769,11 @@ class RealtimeSession(RealtimeModelListener):
619
769
  starting_settings: RealtimeSessionModelSettings | None,
620
770
  agent: RealtimeAgent,
621
771
  ) -> RealtimeSessionModelSettings:
622
- # Start with run config model settings as base
623
- run_config_settings = self._run_config.get("model_settings", {})
624
- updated_settings: RealtimeSessionModelSettings = run_config_settings.copy()
625
- # Apply starting settings (from model config) next
626
- if starting_settings:
627
- updated_settings.update(starting_settings)
772
+ # Start with the merged base settings from run and model configuration.
773
+ updated_settings = self._base_model_settings.copy()
774
+
775
+ if agent.prompt is not None:
776
+ updated_settings["prompt"] = agent.prompt
628
777
 
629
778
  instructions, tools, handoffs = await asyncio.gather(
630
779
  agent.get_system_prompt(self._context_wrapper),
@@ -635,6 +784,10 @@ class RealtimeSession(RealtimeModelListener):
635
784
  updated_settings["tools"] = tools or []
636
785
  updated_settings["handoffs"] = handoffs or []
637
786
 
787
+ # Apply starting settings (from model config) next
788
+ if starting_settings:
789
+ updated_settings.update(starting_settings)
790
+
638
791
  disable_tracing = self._run_config.get("tracing_disabled", False)
639
792
  if disable_tracing:
640
793
  updated_settings["tracing"] = None
agents/run.py CHANGED
@@ -8,7 +8,7 @@ from typing import Any, Callable, Generic, cast, get_args
8
8
 
9
9
  from openai.types.responses import (
10
10
  ResponseCompletedEvent,
11
- ResponseOutputItemAddedEvent,
11
+ ResponseOutputItemDoneEvent,
12
12
  )
13
13
  from openai.types.responses.response_prompt_param import (
14
14
  ResponsePromptParam,
@@ -54,7 +54,7 @@ from .items import (
54
54
  )
55
55
  from .lifecycle import RunHooks
56
56
  from .logger import logger
57
- from .memory import Session
57
+ from .memory import Session, SessionInputCallback
58
58
  from .model_settings import ModelSettings
59
59
  from .models.interface import Model, ModelProvider
60
60
  from .models.multi_provider import MultiProvider
@@ -179,6 +179,13 @@ class RunConfig:
179
179
  An optional dictionary of additional metadata to include with the trace.
180
180
  """
181
181
 
182
+ session_input_callback: SessionInputCallback | None = None
183
+ """Defines how to handle session history when new input is provided.
184
+ - `None` (default): The new input is appended to the session history.
185
+ - `SessionInputCallback`: A custom function that receives the history and new input, and
186
+ returns the desired combined list of items.
187
+ """
188
+
182
189
  call_model_input_filter: CallModelInputFilter | None = None
183
190
  """
184
191
  Optional callback that is invoked immediately before calling the model. It receives the current
@@ -411,8 +418,11 @@ class AgentRunner:
411
418
  if run_config is None:
412
419
  run_config = RunConfig()
413
420
 
414
- # Prepare input with session if enabled
415
- prepared_input = await self._prepare_input_with_session(input, session)
421
+ # Keep original user input separate from session-prepared input
422
+ original_user_input = input
423
+ prepared_input = await self._prepare_input_with_session(
424
+ input, session, run_config.session_input_callback
425
+ )
416
426
 
417
427
  tool_use_tracker = AgentToolUseTracker()
418
428
 
@@ -438,6 +448,9 @@ class AgentRunner:
438
448
  current_agent = starting_agent
439
449
  should_run_agent_start_hooks = True
440
450
 
451
+ # save only the new user input to the session, not the combined history
452
+ await self._save_result_to_session(session, original_user_input, [])
453
+
441
454
  try:
442
455
  while True:
443
456
  all_tools = await AgentRunner._get_all_tools(current_agent, context_wrapper)
@@ -537,9 +550,7 @@ class AgentRunner:
537
550
  output_guardrail_results=output_guardrail_results,
538
551
  context_wrapper=context_wrapper,
539
552
  )
540
-
541
- # Save the conversation to session if enabled
542
- await self._save_result_to_session(session, input, result)
553
+ await self._save_result_to_session(session, [], turn_result.new_step_items)
543
554
 
544
555
  return result
545
556
  elif isinstance(turn_result.next_step, NextStepHandoff):
@@ -548,7 +559,7 @@ class AgentRunner:
548
559
  current_span = None
549
560
  should_run_agent_start_hooks = True
550
561
  elif isinstance(turn_result.next_step, NextStepRunAgain):
551
- pass
562
+ await self._save_result_to_session(session, [], turn_result.new_step_items)
552
563
  else:
553
564
  raise AgentsException(
554
565
  f"Unknown next step type: {type(turn_result.next_step)}"
@@ -779,11 +790,15 @@ class AgentRunner:
779
790
 
780
791
  try:
781
792
  # Prepare input with session if enabled
782
- prepared_input = await AgentRunner._prepare_input_with_session(starting_input, session)
793
+ prepared_input = await AgentRunner._prepare_input_with_session(
794
+ starting_input, session, run_config.session_input_callback
795
+ )
783
796
 
784
797
  # Update the streamed result with the prepared input
785
798
  streamed_result.input = prepared_input
786
799
 
800
+ await AgentRunner._save_result_to_session(session, starting_input, [])
801
+
787
802
  while True:
788
803
  if streamed_result.is_complete:
789
804
  break
@@ -887,24 +902,15 @@ class AgentRunner:
887
902
  streamed_result.is_complete = True
888
903
 
889
904
  # Save the conversation to session if enabled
890
- # Create a temporary RunResult for session saving
891
- temp_result = RunResult(
892
- input=streamed_result.input,
893
- new_items=streamed_result.new_items,
894
- raw_responses=streamed_result.raw_responses,
895
- final_output=streamed_result.final_output,
896
- _last_agent=current_agent,
897
- input_guardrail_results=streamed_result.input_guardrail_results,
898
- output_guardrail_results=streamed_result.output_guardrail_results,
899
- context_wrapper=context_wrapper,
900
- )
901
905
  await AgentRunner._save_result_to_session(
902
- session, starting_input, temp_result
906
+ session, [], turn_result.new_step_items
903
907
  )
904
908
 
905
909
  streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
906
910
  elif isinstance(turn_result.next_step, NextStepRunAgain):
907
- pass
911
+ await AgentRunner._save_result_to_session(
912
+ session, [], turn_result.new_step_items
913
+ )
908
914
  except AgentsException as exc:
909
915
  streamed_result.is_complete = True
910
916
  streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
@@ -994,10 +1000,16 @@ class AgentRunner:
994
1000
  )
995
1001
 
996
1002
  # Call hook just before the model is invoked, with the correct system_prompt.
997
- if agent.hooks:
998
- await agent.hooks.on_llm_start(
999
- context_wrapper, agent, filtered.instructions, filtered.input
1000
- )
1003
+ await asyncio.gather(
1004
+ hooks.on_llm_start(context_wrapper, agent, filtered.instructions, filtered.input),
1005
+ (
1006
+ agent.hooks.on_llm_start(
1007
+ context_wrapper, agent, filtered.instructions, filtered.input
1008
+ )
1009
+ if agent.hooks
1010
+ else _coro.noop_coroutine()
1011
+ ),
1012
+ )
1001
1013
 
1002
1014
  # 1. Stream the output events
1003
1015
  async for event in model.stream_response(
@@ -1034,7 +1046,7 @@ class AgentRunner:
1034
1046
  )
1035
1047
  context_wrapper.usage.add(usage)
1036
1048
 
1037
- if isinstance(event, ResponseOutputItemAddedEvent):
1049
+ if isinstance(event, ResponseOutputItemDoneEvent):
1038
1050
  output_item = event.item
1039
1051
 
1040
1052
  if isinstance(output_item, _TOOL_CALL_TYPES):
@@ -1056,8 +1068,15 @@ class AgentRunner:
1056
1068
  streamed_result._event_queue.put_nowait(RawResponsesStreamEvent(data=event))
1057
1069
 
1058
1070
  # Call hook just after the model response is finalized.
1059
- if agent.hooks and final_response is not None:
1060
- await agent.hooks.on_llm_end(context_wrapper, agent, final_response)
1071
+ if final_response is not None:
1072
+ await asyncio.gather(
1073
+ (
1074
+ agent.hooks.on_llm_end(context_wrapper, agent, final_response)
1075
+ if agent.hooks
1076
+ else _coro.noop_coroutine()
1077
+ ),
1078
+ hooks.on_llm_end(context_wrapper, agent, final_response),
1079
+ )
1061
1080
 
1062
1081
  # 2. At this point, the streaming is complete for this turn of the agent loop.
1063
1082
  if not final_response:
@@ -1150,6 +1169,7 @@ class AgentRunner:
1150
1169
  output_schema,
1151
1170
  all_tools,
1152
1171
  handoffs,
1172
+ hooks,
1153
1173
  context_wrapper,
1154
1174
  run_config,
1155
1175
  tool_use_tracker,
@@ -1345,6 +1365,7 @@ class AgentRunner:
1345
1365
  output_schema: AgentOutputSchemaBase | None,
1346
1366
  all_tools: list[Tool],
1347
1367
  handoffs: list[Handoff],
1368
+ hooks: RunHooks[TContext],
1348
1369
  context_wrapper: RunContextWrapper[TContext],
1349
1370
  run_config: RunConfig,
1350
1371
  tool_use_tracker: AgentToolUseTracker,
@@ -1364,14 +1385,21 @@ class AgentRunner:
1364
1385
  model = cls._get_model(agent, run_config)
1365
1386
  model_settings = agent.model_settings.resolve(run_config.model_settings)
1366
1387
  model_settings = RunImpl.maybe_reset_tool_choice(agent, tool_use_tracker, model_settings)
1367
- # If the agent has hooks, we need to call them before and after the LLM call
1368
- if agent.hooks:
1369
- await agent.hooks.on_llm_start(
1370
- context_wrapper,
1371
- agent,
1372
- filtered.instructions, # Use filtered instructions
1373
- filtered.input, # Use filtered input
1374
- )
1388
+
1389
+ # If we have run hooks, or if the agent has hooks, we need to call them before the LLM call
1390
+ await asyncio.gather(
1391
+ hooks.on_llm_start(context_wrapper, agent, filtered.instructions, filtered.input),
1392
+ (
1393
+ agent.hooks.on_llm_start(
1394
+ context_wrapper,
1395
+ agent,
1396
+ filtered.instructions, # Use filtered instructions
1397
+ filtered.input, # Use filtered input
1398
+ )
1399
+ if agent.hooks
1400
+ else _coro.noop_coroutine()
1401
+ ),
1402
+ )
1375
1403
 
1376
1404
  new_response = await model.get_response(
1377
1405
  system_instructions=filtered.instructions,
@@ -1387,12 +1415,19 @@ class AgentRunner:
1387
1415
  conversation_id=conversation_id,
1388
1416
  prompt=prompt_config,
1389
1417
  )
1390
- # If the agent has hooks, we need to call them after the LLM call
1391
- if agent.hooks:
1392
- await agent.hooks.on_llm_end(context_wrapper, agent, new_response)
1393
1418
 
1394
1419
  context_wrapper.usage.add(new_response.usage)
1395
1420
 
1421
+ # If we have run hooks, or if the agent has hooks, we need to call them after the LLM call
1422
+ await asyncio.gather(
1423
+ (
1424
+ agent.hooks.on_llm_end(context_wrapper, agent, new_response)
1425
+ if agent.hooks
1426
+ else _coro.noop_coroutine()
1427
+ ),
1428
+ hooks.on_llm_end(context_wrapper, agent, new_response),
1429
+ )
1430
+
1396
1431
  return new_response
1397
1432
 
1398
1433
  @classmethod
@@ -1450,19 +1485,20 @@ class AgentRunner:
1450
1485
  cls,
1451
1486
  input: str | list[TResponseInputItem],
1452
1487
  session: Session | None,
1488
+ session_input_callback: SessionInputCallback | None,
1453
1489
  ) -> str | list[TResponseInputItem]:
1454
1490
  """Prepare input by combining it with session history if enabled."""
1455
1491
  if session is None:
1456
1492
  return input
1457
1493
 
1458
- # Validate that we don't have both a session and a list input, as this creates
1459
- # ambiguity about whether the list should append to or replace existing session history
1460
- if isinstance(input, list):
1494
+ # If the user doesn't specify an input callback and pass a list as input
1495
+ if isinstance(input, list) and not session_input_callback:
1461
1496
  raise UserError(
1462
- "Cannot provide both a session and a list of input items. "
1463
- "When using session memory, provide only a string input to append to the "
1464
- "conversation, or use session=None and provide a list to manually manage "
1465
- "conversation history."
1497
+ "When using session memory, list inputs require a "
1498
+ "`RunConfig.session_input_callback` to define how they should be merged "
1499
+ "with the conversation history. If you don't want to use a callback, "
1500
+ "provide your input as a string instead, or disable session memory "
1501
+ "(session=None) and pass a list to manage the history manually."
1466
1502
  )
1467
1503
 
1468
1504
  # Get previous conversation history
@@ -1471,19 +1507,31 @@ class AgentRunner:
1471
1507
  # Convert input to list format
1472
1508
  new_input_list = ItemHelpers.input_to_new_input_list(input)
1473
1509
 
1474
- # Combine history with new input
1475
- combined_input = history + new_input_list
1476
-
1477
- return combined_input
1510
+ if session_input_callback is None:
1511
+ return history + new_input_list
1512
+ elif callable(session_input_callback):
1513
+ res = session_input_callback(history, new_input_list)
1514
+ if inspect.isawaitable(res):
1515
+ return await res
1516
+ return res
1517
+ else:
1518
+ raise UserError(
1519
+ f"Invalid `session_input_callback` value: {session_input_callback}. "
1520
+ "Choose between `None` or a custom callable function."
1521
+ )
1478
1522
 
1479
1523
  @classmethod
1480
1524
  async def _save_result_to_session(
1481
1525
  cls,
1482
1526
  session: Session | None,
1483
1527
  original_input: str | list[TResponseInputItem],
1484
- result: RunResult,
1528
+ new_items: list[RunItem],
1485
1529
  ) -> None:
1486
- """Save the conversation turn to session."""
1530
+ """
1531
+ Save the conversation turn to session.
1532
+ It does not account for any filtering or modification performed by
1533
+ `RunConfig.session_input_callback`.
1534
+ """
1487
1535
  if session is None:
1488
1536
  return
1489
1537
 
@@ -1491,7 +1539,7 @@ class AgentRunner:
1491
1539
  input_list = ItemHelpers.input_to_new_input_list(original_input)
1492
1540
 
1493
1541
  # Convert new items to input format
1494
- new_items_as_input = [item.to_input_item() for item in result.new_items]
1542
+ new_items_as_input = [item.to_input_item() for item in new_items]
1495
1543
 
1496
1544
  # Save all items from this turn
1497
1545
  items_to_save = input_list + new_items_as_input
agents/tool.py CHANGED
@@ -12,8 +12,8 @@ from openai.types.responses.response_computer_tool_call import (
12
12
  ResponseComputerToolCall,
13
13
  )
14
14
  from openai.types.responses.response_output_item import LocalShellCall, McpApprovalRequest
15
- from openai.types.responses.tool import WebSearchToolFilters
16
15
  from openai.types.responses.tool_param import CodeInterpreter, ImageGeneration, Mcp
16
+ from openai.types.responses.web_search_tool import Filters as WebSearchToolFilters
17
17
  from openai.types.responses.web_search_tool_param import UserLocation
18
18
  from pydantic import ValidationError
19
19
  from typing_extensions import Concatenate, NotRequired, ParamSpec, TypedDict
@@ -142,7 +142,7 @@ class WebSearchTool:
142
142
 
143
143
  @property
144
144
  def name(self):
145
- return "web_search_preview"
145
+ return "web_search"
146
146
 
147
147
 
148
148
  @dataclass
agents/util/_json.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Literal
3
+ from collections.abc import Iterable
4
+ from typing import Any, Literal
4
5
 
5
6
  from pydantic import TypeAdapter, ValidationError
6
7
  from typing_extensions import TypeVar
@@ -29,3 +30,20 @@ def validate_json(json_str: str, type_adapter: TypeAdapter[T], partial: bool) ->
29
30
  raise ModelBehaviorError(
30
31
  f"Invalid JSON when parsing {json_str} for {type_adapter}; {e}"
31
32
  ) from e
33
+
34
+
35
+ def _to_dump_compatible(obj: Any) -> Any:
36
+ return _to_dump_compatible_internal(obj)
37
+
38
+
39
+ def _to_dump_compatible_internal(obj: Any) -> Any:
40
+ if isinstance(obj, dict):
41
+ return {k: _to_dump_compatible_internal(v) for k, v in obj.items()}
42
+
43
+ if isinstance(obj, (list, tuple)):
44
+ return [_to_dump_compatible_internal(x) for x in obj]
45
+
46
+ if isinstance(obj, Iterable) and not isinstance(obj, (str, bytes, bytearray)):
47
+ return [_to_dump_compatible_internal(x) for x in obj]
48
+
49
+ return obj
agents/voice/input.py CHANGED
@@ -13,7 +13,7 @@ DEFAULT_SAMPLE_RATE = 24000
13
13
 
14
14
 
15
15
  def _buffer_to_audio_file(
16
- buffer: npt.NDArray[np.int16 | np.float32],
16
+ buffer: npt.NDArray[np.int16 | np.float32 | np.float64],
17
17
  frame_rate: int = DEFAULT_SAMPLE_RATE,
18
18
  sample_width: int = 2,
19
19
  channels: int = 1,
@@ -77,12 +77,13 @@ class StreamedAudioInput:
77
77
  """
78
78
 
79
79
  def __init__(self):
80
- self.queue: asyncio.Queue[npt.NDArray[np.int16 | np.float32]] = asyncio.Queue()
80
+ self.queue: asyncio.Queue[npt.NDArray[np.int16 | np.float32] | None] = asyncio.Queue()
81
81
 
82
- async def add_audio(self, audio: npt.NDArray[np.int16 | np.float32]):
82
+ async def add_audio(self, audio: npt.NDArray[np.int16 | np.float32] | None):
83
83
  """Adds more audio data to the stream.
84
84
 
85
85
  Args:
86
- audio: The audio data to add. Must be a numpy array of int16 or float32.
86
+ audio: The audio data to add. Must be a numpy array of int16 or float32 or None.
87
+ If None passed, it indicates the end of the stream.
87
88
  """
88
89
  await self.queue.put(audio)