lexsi-sdk 0.1.16__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.
Files changed (40) hide show
  1. lexsi_sdk/__init__.py +5 -0
  2. lexsi_sdk/client/__init__.py +0 -0
  3. lexsi_sdk/client/client.py +176 -0
  4. lexsi_sdk/common/__init__.py +0 -0
  5. lexsi_sdk/common/config/.env.prod +3 -0
  6. lexsi_sdk/common/constants.py +143 -0
  7. lexsi_sdk/common/enums.py +8 -0
  8. lexsi_sdk/common/environment.py +49 -0
  9. lexsi_sdk/common/monitoring.py +81 -0
  10. lexsi_sdk/common/trigger.py +75 -0
  11. lexsi_sdk/common/types.py +122 -0
  12. lexsi_sdk/common/utils.py +93 -0
  13. lexsi_sdk/common/validation.py +110 -0
  14. lexsi_sdk/common/xai_uris.py +197 -0
  15. lexsi_sdk/core/__init__.py +0 -0
  16. lexsi_sdk/core/agent.py +62 -0
  17. lexsi_sdk/core/alert.py +56 -0
  18. lexsi_sdk/core/case.py +618 -0
  19. lexsi_sdk/core/dashboard.py +131 -0
  20. lexsi_sdk/core/guardrails/__init__.py +0 -0
  21. lexsi_sdk/core/guardrails/guard_template.py +299 -0
  22. lexsi_sdk/core/guardrails/guardrail_autogen.py +554 -0
  23. lexsi_sdk/core/guardrails/guardrails_langgraph.py +525 -0
  24. lexsi_sdk/core/guardrails/guardrails_openai.py +541 -0
  25. lexsi_sdk/core/guardrails/openai_runner.py +1328 -0
  26. lexsi_sdk/core/model_summary.py +110 -0
  27. lexsi_sdk/core/organization.py +549 -0
  28. lexsi_sdk/core/project.py +5131 -0
  29. lexsi_sdk/core/synthetic.py +387 -0
  30. lexsi_sdk/core/text.py +595 -0
  31. lexsi_sdk/core/tracer.py +208 -0
  32. lexsi_sdk/core/utils.py +36 -0
  33. lexsi_sdk/core/workspace.py +325 -0
  34. lexsi_sdk/core/wrapper.py +766 -0
  35. lexsi_sdk/core/xai.py +306 -0
  36. lexsi_sdk/version.py +34 -0
  37. lexsi_sdk-0.1.16.dist-info/METADATA +100 -0
  38. lexsi_sdk-0.1.16.dist-info/RECORD +40 -0
  39. lexsi_sdk-0.1.16.dist-info/WHEEL +5 -0
  40. lexsi_sdk-0.1.16.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1328 @@
1
+ import inspect
2
+
3
+ from agents import (
4
+ Agent,
5
+ AgentOutputSchema,
6
+ AgentOutputSchemaBase,
7
+ AgentSpanData,
8
+ AgentsException,
9
+ Handoff,
10
+ Model,
11
+ ModelBehaviorError,
12
+ ModelResponse,
13
+ OutputGuardrail,
14
+ OutputGuardrailResult,
15
+ OutputGuardrailTripwireTriggered,
16
+ RunConfig,
17
+ RunContextWrapper,
18
+ RunErrorDetails,
19
+ RunHooks,
20
+ RunItem,
21
+ RunResult,
22
+ RunResultStreaming,
23
+ Session,
24
+ Span,
25
+ SpanError,
26
+ Tool,
27
+ Usage,
28
+ agent_span,
29
+ trace,
30
+ )
31
+ from openai.types.responses import ResponseCompletedEvent
32
+ from openai.types.responses.response_prompt_param import (
33
+ ResponsePromptParam,
34
+ )
35
+ from agents.exceptions import (
36
+ AgentsException,
37
+ InputGuardrailTripwireTriggered,
38
+ MaxTurnsExceeded,
39
+ ModelBehaviorError,
40
+ OutputGuardrailTripwireTriggered,
41
+ RunErrorDetails,
42
+ UserError,
43
+ )
44
+ from agents.guardrail import (
45
+ InputGuardrail,
46
+ InputGuardrailResult,
47
+ OutputGuardrail,
48
+ OutputGuardrailResult,
49
+ )
50
+
51
+ from agents.handoffs import Handoff, HandoffInputFilter, handoff
52
+
53
+ from typing import Any, cast, Generic
54
+ from agents.run import (
55
+ TResponseInputItem,
56
+ TContext,
57
+ DEFAULT_MAX_TURNS,
58
+ RunOptions,
59
+ AgentToolUseTracker,
60
+ _error_tracing,
61
+ TraceCtxManager,
62
+ get_model_tracing_impl,
63
+ AgentToolUseTracker,
64
+ NextStepFinalOutput,
65
+ NextStepHandoff,
66
+ NextStepRunAgain,
67
+ QueueCompleteSentinel,
68
+ RunImpl,
69
+ SingleStepResult,
70
+ TraceCtxManager,
71
+ )
72
+ from agents.stream_events import AgentUpdatedStreamEvent
73
+ from agents.items import ItemHelpers, ModelResponse, RunItem, TResponseInputItem
74
+ from agents.lifecycle import RunHooks
75
+ from agents.logger import logger
76
+ from agents.memory import Session
77
+ from agents.model_settings import ModelSettings
78
+ from agents.models.interface import Model, ModelProvider
79
+ from agents.models.multi_provider import MultiProvider
80
+ from agents.result import RunResult, RunResultStreaming
81
+ from agents.run_context import RunContextWrapper, TContext
82
+ from agents.stream_events import AgentUpdatedStreamEvent, RawResponsesStreamEvent
83
+ from agents.tool import Tool
84
+ from agents.tracing import Span, SpanError, agent_span, get_current_trace, trace
85
+ from agents.tracing.span_data import AgentSpanData
86
+ from agents.usage import Usage
87
+ from agents.util import _coro, _error_tracing
88
+ from agents.util._types import MaybeAwaitable
89
+
90
+ from typing_extensions import Unpack
91
+ import asyncio
92
+ from agents.tracing import (
93
+ get_current_trace,
94
+ )
95
+ from dataclasses import dataclass, field
96
+
97
+
98
+ @dataclass
99
+ class ModelInputData:
100
+ """Container for the data that will be sent to the model."""
101
+
102
+ input: list[TResponseInputItem]
103
+ instructions: str | None
104
+
105
+ @dataclass
106
+ class CallModelData(Generic[TContext]):
107
+ """Data passed to `RunConfig.call_model_input_filter` prior to model call."""
108
+
109
+ model_data: ModelInputData
110
+ agent: Agent[TContext]
111
+ context: TContext | None
112
+
113
+ class Runner:
114
+ """Orchestrates agent execution loops for OpenAI Agents."""
115
+ @classmethod
116
+ async def run(
117
+ cls,
118
+ starting_agent: Agent[TContext],
119
+ input: str | list[TResponseInputItem],
120
+ *,
121
+ context: TContext | None = None,
122
+ max_turns: int = DEFAULT_MAX_TURNS,
123
+ hooks: RunHooks[TContext] | None = None,
124
+ run_config: RunConfig | None = None,
125
+ previous_response_id: str | None = None,
126
+ session: Session | None = None,
127
+ ) -> RunResult:
128
+ """Run a workflow starting at the given agent. The agent will run in a loop until a final
129
+ output is generated. The loop runs like so:
130
+ 1. The agent is invoked with the given input.
131
+ 2. If there is a final output (i.e. the agent produces something of type
132
+ `agent.output_type`, the loop terminates.
133
+ 3. If there's a handoff, we run the loop again, with the new agent.
134
+ 4. Else, we run tool calls (if any), and re-run the loop.
135
+ In two cases, the agent may raise an exception:
136
+ 1. If the max_turns is exceeded, a MaxTurnsExceeded exception is raised.
137
+ 2. If a guardrail tripwire is triggered, a GuardrailTripwireTriggered exception is raised.
138
+ Note that only the first agent's input guardrails are run.
139
+
140
+ :param starting_agent: The starting agent to run.
141
+ :param input: The initial input to the agent; string user message or list of items.
142
+ :param context: Optional context to run the agent with.
143
+ :param max_turns: Maximum number of turns to run the agent.
144
+ :param hooks: Callbacks for lifecycle events.
145
+ :param run_config: Global settings for the entire agent run.
146
+ :param previous_response_id: Prior response id to continue Responses API runs.
147
+ :param session: Optional session used by the runner.
148
+ :return: Run result containing inputs, guardrail results, and final output.
149
+ """
150
+ runner = DEFAULT_AGENT_RUNNER
151
+ return await runner.run(
152
+ starting_agent,
153
+ input,
154
+ context=context,
155
+ max_turns=max_turns,
156
+ hooks=hooks,
157
+ run_config=run_config,
158
+ previous_response_id=previous_response_id,
159
+ session=session,
160
+ )
161
+
162
+ @classmethod
163
+ def run_sync(
164
+ cls,
165
+ starting_agent: Agent[TContext],
166
+ input: str | list[TResponseInputItem],
167
+ *,
168
+ context: TContext | None = None,
169
+ max_turns: int = DEFAULT_MAX_TURNS,
170
+ hooks: RunHooks[TContext] | None = None,
171
+ run_config: RunConfig | None = None,
172
+ previous_response_id: str | None = None,
173
+ session: Session | None = None,
174
+ ) -> RunResult:
175
+ """Run a workflow synchronously, starting at the given agent. Note that this just wraps the
176
+ `run` method, so it will not work if there's already an event loop (e.g. inside an async
177
+ function, or in a Jupyter notebook or async context like FastAPI). For those cases, use
178
+ the `run` method instead.
179
+ The agent will run in a loop until a final output is generated. The loop runs like so:
180
+ 1. The agent is invoked with the given input.
181
+ 2. If there is a final output (i.e. the agent produces something of type
182
+ `agent.output_type`, the loop terminates.
183
+ 3. If there's a handoff, we run the loop again, with the new agent.
184
+ 4. Else, we run tool calls (if any), and re-run the loop.
185
+ In two cases, the agent may raise an exception:
186
+ 1. If the max_turns is exceeded, a MaxTurnsExceeded exception is raised.
187
+ 2. If a guardrail tripwire is triggered, a GuardrailTripwireTriggered exception is raised.
188
+ Note that only the first agent's input guardrails are run.
189
+
190
+ :param starting_agent: The starting agent to run.
191
+ :param input: The initial input to the agent; string user message or list of items.
192
+ :param context: Optional context to run the agent with.
193
+ :param max_turns: Maximum number of turns to run the agent.
194
+ :param hooks: Callbacks for lifecycle events.
195
+ :param run_config: Global settings for the entire agent run.
196
+ :param previous_response_id: Prior response id to continue Responses API runs.
197
+ :param session: Optional session used by the runner.
198
+ :return: Run result containing inputs, guardrail results, and final output.
199
+ """
200
+ runner = DEFAULT_AGENT_RUNNER
201
+ return runner.run_sync(
202
+ starting_agent,
203
+ input,
204
+ context=context,
205
+ max_turns=max_turns,
206
+ hooks=hooks,
207
+ run_config=run_config,
208
+ previous_response_id=previous_response_id,
209
+ session=session,
210
+ )
211
+
212
+ @classmethod
213
+ def run_streamed(
214
+ cls,
215
+ starting_agent: Agent[TContext],
216
+ input: str | list[TResponseInputItem],
217
+ context: TContext | None = None,
218
+ max_turns: int = DEFAULT_MAX_TURNS,
219
+ hooks: RunHooks[TContext] | None = None,
220
+ run_config: RunConfig | None = None,
221
+ previous_response_id: str | None = None,
222
+ session: Session | None = None,
223
+ ) -> RunResultStreaming:
224
+ """Run a workflow starting at the given agent in streaming mode. The returned result object
225
+ contains a method you can use to stream semantic events as they are generated.
226
+ The agent will run in a loop until a final output is generated. The loop runs like so:
227
+ 1. The agent is invoked with the given input.
228
+ 2. If there is a final output (i.e. the agent produces something of type
229
+ `agent.output_type`, the loop terminates.
230
+ 3. If there's a handoff, we run the loop again, with the new agent.
231
+ 4. Else, we run tool calls (if any), and re-run the loop.
232
+ In two cases, the agent may raise an exception:
233
+ 1. If the max_turns is exceeded, a MaxTurnsExceeded exception is raised.
234
+ 2. If a guardrail tripwire is triggered, a GuardrailTripwireTriggered exception is raised.
235
+ Note that only the first agent's input guardrails are run.
236
+
237
+ :param starting_agent: The starting agent to run.
238
+ :param input: Initial input; string user message or list of items.
239
+ :param context: Optional context to run the agent with.
240
+ :param max_turns: Maximum number of turns to run the agent.
241
+ :param hooks: Callbacks for lifecycle events.
242
+ :param run_config: Global settings for the entire agent run.
243
+ :param previous_response_id: Prior response id to continue Responses API runs.
244
+ :param session: Optional session used by the runner.
245
+ :return: Result object containing run data and streaming method.
246
+ """
247
+ runner = DEFAULT_AGENT_RUNNER
248
+ return runner.run_streamed(
249
+ starting_agent,
250
+ input,
251
+ context=context,
252
+ max_turns=max_turns,
253
+ hooks=hooks,
254
+ run_config=run_config,
255
+ previous_response_id=previous_response_id,
256
+ session=session,
257
+ )
258
+
259
+
260
+ class AgentRunner:
261
+ """Async runner that manages the OpenAI Agents control loop."""
262
+ async def run(
263
+ self,
264
+ starting_agent: Agent[TContext],
265
+ input: str | list[TResponseInputItem],
266
+ **kwargs: Unpack[RunOptions[TContext]],
267
+ ) -> RunResult:
268
+ """Run the agent workflow asynchronously until completion."""
269
+ context = kwargs.get("context")
270
+ max_turns = kwargs.get("max_turns", DEFAULT_MAX_TURNS)
271
+ hooks = kwargs.get("hooks")
272
+ run_config = kwargs.get("run_config")
273
+ previous_response_id = kwargs.get("previous_response_id")
274
+ session = kwargs.get("session")
275
+ if hooks is None:
276
+ hooks = RunHooks[Any]()
277
+ if run_config is None:
278
+ run_config = RunConfig()
279
+
280
+ # Prepare input with session if enabled
281
+ prepared_input = await self._prepare_input_with_session(input, session)
282
+
283
+ tool_use_tracker = AgentToolUseTracker()
284
+
285
+ with TraceCtxManager(
286
+ workflow_name=run_config.workflow_name,
287
+ trace_id=run_config.trace_id,
288
+ group_id=run_config.group_id,
289
+ metadata=run_config.trace_metadata,
290
+ disabled=run_config.tracing_disabled,
291
+ ):
292
+ current_turn = 0
293
+ original_input: str | list[TResponseInputItem] = _copy_str_or_list(prepared_input)
294
+ generated_items: list[RunItem] = []
295
+ model_responses: list[ModelResponse] = []
296
+
297
+ context_wrapper: RunContextWrapper[TContext] = RunContextWrapper(
298
+ context=context # type: ignore
299
+ )
300
+
301
+ input_guardrail_results: list[InputGuardrailResult] = []
302
+
303
+ current_span: Span[AgentSpanData] | None = None
304
+ current_agent = starting_agent
305
+ should_run_agent_start_hooks = True
306
+
307
+ try:
308
+ while True:
309
+ all_tools = await AgentRunner._get_all_tools(current_agent, context_wrapper)
310
+
311
+ # Start an agent span if we don't have one
312
+ if current_span is None:
313
+ handoff_names = [
314
+ h.agent_name
315
+ for h in await AgentRunner._get_handoffs(current_agent, context_wrapper)
316
+ ]
317
+ if output_schema := AgentRunner._get_output_schema(current_agent):
318
+ output_type_name = output_schema.name()
319
+ else:
320
+ output_type_name = "str"
321
+
322
+ current_span = agent_span(
323
+ name=current_agent.name,
324
+ handoffs=handoff_names,
325
+ output_type=output_type_name,
326
+ )
327
+ current_span.start(mark_as_current=True)
328
+ current_span.span_data.tools = [t.name for t in all_tools]
329
+
330
+ current_turn += 1
331
+ if current_turn > max_turns:
332
+ _error_tracing.attach_error_to_span(
333
+ current_span,
334
+ SpanError(
335
+ message="Max turns exceeded",
336
+ data={"max_turns": max_turns},
337
+ ),
338
+ )
339
+ raise MaxTurnsExceeded(f"Max turns ({max_turns}) exceeded")
340
+
341
+ # Run input guardrails for the first turn or after a handoff
342
+ if current_turn == 1 or isinstance(turn_result.next_step, NextStepHandoff):
343
+ input_guardrail_results.extend(
344
+ await self._run_input_guardrails(
345
+ current_agent,
346
+ current_agent.input_guardrails + (run_config.input_guardrails or []),
347
+ _copy_str_or_list(prepared_input if current_turn == 1 else original_input),
348
+ context_wrapper,
349
+ )
350
+ )
351
+ # Update original_input with sanitized content if any guardrail used 'retry'
352
+ for result in input_guardrail_results:
353
+ if hasattr(result.output, 'sanitized_content') and result.output.sanitized_content:
354
+ original_input = result.output.sanitized_content
355
+
356
+ turn_result = await self._run_single_turn(
357
+ agent=current_agent,
358
+ all_tools=all_tools,
359
+ original_input=original_input,
360
+ generated_items=generated_items,
361
+ hooks=hooks,
362
+ context_wrapper=context_wrapper,
363
+ run_config=run_config,
364
+ should_run_agent_start_hooks=should_run_agent_start_hooks,
365
+ tool_use_tracker=tool_use_tracker,
366
+ previous_response_id=previous_response_id,
367
+ )
368
+ should_run_agent_start_hooks = False
369
+
370
+ model_responses.append(turn_result.model_response)
371
+ original_input = turn_result.original_input
372
+ generated_items = turn_result.generated_items
373
+
374
+ if isinstance(turn_result.next_step, NextStepFinalOutput):
375
+ output_guardrail_results = await self._run_output_guardrails(
376
+ current_agent.output_guardrails + (run_config.output_guardrails or []),
377
+ current_agent,
378
+ turn_result.next_step.output,
379
+ context_wrapper,
380
+ )
381
+ # Use sanitized content as final output if available
382
+ final_output = turn_result.next_step.output
383
+ for result in output_guardrail_results:
384
+ if hasattr(result.output, 'sanitized_content') and result.output.sanitized_content:
385
+ final_output = result.output.sanitized_content
386
+
387
+ result = RunResult(
388
+ input=original_input,
389
+ new_items=generated_items,
390
+ raw_responses=model_responses,
391
+ final_output=final_output,
392
+ _last_agent=current_agent,
393
+ input_guardrail_results=input_guardrail_results,
394
+ output_guardrail_results=output_guardrail_results,
395
+ context_wrapper=context_wrapper,
396
+ )
397
+
398
+ # Save the conversation to session if enabled
399
+ await self._save_result_to_session(session, input, result)
400
+
401
+ return result
402
+ elif isinstance(turn_result.next_step, NextStepHandoff):
403
+ current_agent = cast(Agent[TContext], turn_result.next_step.new_agent)
404
+ # Pass sanitized input to the handoff agent
405
+ for result in input_guardrail_results:
406
+ if hasattr(result.output, 'sanitized_content') and result.output.sanitized_content:
407
+ original_input = result.output.sanitized_content
408
+ current_span.finish(reset_current=True)
409
+ current_span = None
410
+ should_run_agent_start_hooks = True
411
+ elif isinstance(turn_result.next_step, NextStepRunAgain):
412
+ pass
413
+ else:
414
+ raise AgentsException(
415
+ f"Unknown next step type: {type(turn_result.next_step)}"
416
+ )
417
+ except AgentsException as exc:
418
+ exc.run_data = RunErrorDetails(
419
+ input=original_input,
420
+ new_items=generated_items,
421
+ raw_responses=model_responses,
422
+ last_agent=current_agent,
423
+ context_wrapper=context_wrapper,
424
+ input_guardrail_results=input_guardrail_results,
425
+ output_guardrail_results=[],
426
+ )
427
+ raise
428
+ finally:
429
+ if current_span:
430
+ current_span.finish(reset_current=True)
431
+
432
+ def run_sync(
433
+ self,
434
+ starting_agent: Agent[TContext],
435
+ input: str | list[TResponseInputItem],
436
+ **kwargs: Unpack[RunOptions[TContext]],
437
+ ) -> RunResult:
438
+ """Synchronous wrapper that runs the async agent loop to completion."""
439
+ context = kwargs.get("context")
440
+ max_turns = kwargs.get("max_turns", DEFAULT_MAX_TURNS)
441
+ hooks = kwargs.get("hooks")
442
+ run_config = kwargs.get("run_config")
443
+ previous_response_id = kwargs.get("previous_response_id")
444
+ session = kwargs.get("session")
445
+
446
+ return asyncio.get_event_loop().run_until_complete(
447
+ self.run(
448
+ starting_agent,
449
+ input,
450
+ session=session,
451
+ context=context,
452
+ max_turns=max_turns,
453
+ hooks=hooks,
454
+ run_config=run_config,
455
+ previous_response_id=previous_response_id,
456
+ )
457
+ )
458
+
459
+ def run_streamed(
460
+ self,
461
+ starting_agent: Agent[TContext],
462
+ input: str | list[TResponseInputItem],
463
+ **kwargs: Unpack[RunOptions[TContext]],
464
+ ) -> RunResultStreaming:
465
+ """Run the agent loop while streaming intermediate responses."""
466
+ context = kwargs.get("context")
467
+ max_turns = kwargs.get("max_turns", DEFAULT_MAX_TURNS)
468
+ hooks = kwargs.get("hooks")
469
+ run_config = kwargs.get("run_config")
470
+ previous_response_id = kwargs.get("previous_response_id")
471
+ session = kwargs.get("session")
472
+
473
+ if hooks is None:
474
+ hooks = RunHooks[Any]()
475
+ if run_config is None:
476
+ run_config = RunConfig()
477
+
478
+ new_trace = (
479
+ None
480
+ if get_current_trace()
481
+ else trace(
482
+ workflow_name=run_config.workflow_name,
483
+ trace_id=run_config.trace_id,
484
+ group_id=run_config.group_id,
485
+ metadata=run_config.trace_metadata,
486
+ disabled=run_config.tracing_disabled,
487
+ )
488
+ )
489
+
490
+ output_schema = AgentRunner._get_output_schema(starting_agent)
491
+ context_wrapper: RunContextWrapper[TContext] = RunContextWrapper(
492
+ context=context # type: ignore
493
+ )
494
+
495
+ streamed_result = RunResultStreaming(
496
+ input=_copy_str_or_list(input),
497
+ new_items=[],
498
+ current_agent=starting_agent,
499
+ raw_responses=[],
500
+ final_output=None,
501
+ is_complete=False,
502
+ current_turn=0,
503
+ max_turns=max_turns,
504
+ input_guardrail_results=[],
505
+ output_guardrail_results=[],
506
+ _current_agent_output_schema=output_schema,
507
+ trace=new_trace,
508
+ context_wrapper=context_wrapper,
509
+ )
510
+
511
+ streamed_result._run_impl_task = asyncio.create_task(
512
+ self._start_streaming(
513
+ starting_input=input,
514
+ streamed_result=streamed_result,
515
+ starting_agent=starting_agent,
516
+ max_turns=max_turns,
517
+ hooks=hooks,
518
+ context_wrapper=context_wrapper,
519
+ run_config=run_config,
520
+ previous_response_id=previous_response_id,
521
+ session=session,
522
+ )
523
+ )
524
+ return streamed_result
525
+
526
+ @classmethod
527
+ async def _maybe_filter_model_input(
528
+ cls,
529
+ *,
530
+ agent: Agent[TContext],
531
+ run_config: RunConfig,
532
+ context_wrapper: RunContextWrapper[TContext],
533
+ input_items: list[TResponseInputItem],
534
+ system_instructions: str | None,
535
+ ) -> ModelInputData:
536
+ """Apply optional call_model_input_filter to modify model input.
537
+
538
+ Returns a `ModelInputData` that will be sent to the model.
539
+ """
540
+ effective_instructions = system_instructions
541
+ effective_input: list[TResponseInputItem] = input_items
542
+
543
+ if run_config.call_model_input_filter is None:
544
+ return ModelInputData(input=effective_input, instructions=effective_instructions)
545
+
546
+ try:
547
+ model_input = ModelInputData(
548
+ input=effective_input.copy(),
549
+ instructions=effective_instructions,
550
+ )
551
+ filter_payload: CallModelData[TContext] = CallModelData(
552
+ model_data=model_input,
553
+ agent=agent,
554
+ context=context_wrapper.context,
555
+ )
556
+ maybe_updated = run_config.call_model_input_filter(filter_payload)
557
+ updated = await maybe_updated if inspect.isawaitable(maybe_updated) else maybe_updated
558
+ if not isinstance(updated, ModelInputData):
559
+ raise UserError("call_model_input_filter must return a ModelInputData instance")
560
+ return updated
561
+ except Exception as e:
562
+ _error_tracing.attach_error_to_current_span(
563
+ SpanError(message="Error in call_model_input_filter", data={"error": str(e)})
564
+ )
565
+ raise
566
+
567
+ @classmethod
568
+ async def _run_input_guardrails_with_queue(
569
+ cls,
570
+ agent: Agent[Any],
571
+ guardrails: list[InputGuardrail[TContext]],
572
+ input: str | list[TResponseInputItem],
573
+ context: RunContextWrapper[TContext],
574
+ streamed_result: RunResultStreaming,
575
+ parent_span: Span[Any],
576
+ ):
577
+ """Run input guardrails concurrently and enqueue results during streaming."""
578
+ queue = streamed_result._input_guardrail_queue
579
+
580
+ # We'll run the guardrails and push them onto the queue as they complete
581
+ guardrail_tasks = [
582
+ asyncio.create_task(
583
+ RunImpl.run_single_input_guardrail(agent, guardrail, input, context)
584
+ )
585
+ for guardrail in guardrails
586
+ ]
587
+ guardrail_results = []
588
+ try:
589
+ for done in asyncio.as_completed(guardrail_tasks):
590
+ result = await done
591
+ if result.output.tripwire_triggered:
592
+ _error_tracing.attach_error_to_span(
593
+ parent_span,
594
+ SpanError(
595
+ message="Guardrail tripwire triggered",
596
+ data={
597
+ "guardrail": result.guardrail.get_name(),
598
+ "type": "input_guardrail",
599
+ },
600
+ ),
601
+ )
602
+ queue.put_nowait(result)
603
+ guardrail_results.append(result)
604
+ except Exception:
605
+ for t in guardrail_tasks:
606
+ t.cancel()
607
+ raise
608
+
609
+ streamed_result.input_guardrail_results = guardrail_results
610
+ # Update streamed_result.input with sanitized content if available
611
+ for result in guardrail_results:
612
+ if hasattr(result.output, 'sanitized_content') and result.output.sanitized_content:
613
+ streamed_result.input = result.output.sanitized_content
614
+
615
+ @classmethod
616
+ async def _start_streaming(
617
+ cls,
618
+ starting_input: str | list[TResponseInputItem],
619
+ streamed_result: RunResultStreaming,
620
+ starting_agent: Agent[TContext],
621
+ max_turns: int,
622
+ hooks: RunHooks[TContext],
623
+ context_wrapper: RunContextWrapper[TContext],
624
+ run_config: RunConfig,
625
+ previous_response_id: str | None,
626
+ session: Session | None,
627
+ ):
628
+ """Primary loop for streaming agent executions."""
629
+ if streamed_result.trace:
630
+ streamed_result.trace.start(mark_as_current=True)
631
+
632
+ current_span: Span[AgentSpanData] | None = None
633
+ current_agent = starting_agent
634
+ current_turn = 0
635
+ should_run_agent_start_hooks = True
636
+ tool_use_tracker = AgentToolUseTracker()
637
+
638
+ streamed_result._event_queue.put_nowait(AgentUpdatedStreamEvent(new_agent=current_agent))
639
+
640
+ try:
641
+ # Prepare input with session if enabled
642
+ prepared_input = await AgentRunner._prepare_input_with_session(starting_input, session)
643
+
644
+ # Update the streamed result with the prepared input
645
+ streamed_result.input = prepared_input
646
+
647
+ while True:
648
+ if streamed_result.is_complete:
649
+ break
650
+
651
+ all_tools = await cls._get_all_tools(current_agent, context_wrapper)
652
+
653
+ # Start an agent span if we don't have one
654
+ if current_span is None:
655
+ handoff_names = [
656
+ h.agent_name
657
+ for h in await cls._get_handoffs(current_agent, context_wrapper)
658
+ ]
659
+ if output_schema := cls._get_output_schema(current_agent):
660
+ output_type_name = output_schema.name()
661
+ else:
662
+ output_type_name = "str"
663
+
664
+ current_span = agent_span(
665
+ name=current_agent.name,
666
+ handoffs=handoff_names,
667
+ output_type=output_type_name,
668
+ )
669
+ current_span.start(mark_as_current=True)
670
+ tool_names = [t.name for t in all_tools]
671
+ current_span.span_data.tools = tool_names
672
+ current_turn += 1
673
+ streamed_result.current_turn = current_turn
674
+
675
+ if current_turn > max_turns:
676
+ _error_tracing.attach_error_to_span(
677
+ current_span,
678
+ SpanError(
679
+ message="Max turns exceeded",
680
+ data={"max_turns": max_turns},
681
+ ),
682
+ )
683
+ streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
684
+ break
685
+
686
+ if current_turn == 1 or isinstance(turn_result.next_step, NextStepHandoff):
687
+ # Run input guardrails in the background and put the results on the queue
688
+ streamed_result._input_guardrails_task = asyncio.create_task(
689
+ cls._run_input_guardrails_with_queue(
690
+ current_agent,
691
+ current_agent.input_guardrails + (run_config.input_guardrails or []),
692
+ ItemHelpers.input_to_new_input_list(prepared_input if current_turn == 1 else streamed_result.input),
693
+ context_wrapper,
694
+ streamed_result,
695
+ current_span,
696
+ )
697
+ )
698
+ try:
699
+ turn_result = await cls._run_single_turn_streamed(
700
+ streamed_result,
701
+ current_agent,
702
+ hooks,
703
+ context_wrapper,
704
+ run_config,
705
+ should_run_agent_start_hooks,
706
+ tool_use_tracker,
707
+ all_tools,
708
+ previous_response_id,
709
+ )
710
+ should_run_agent_start_hooks = False
711
+
712
+ streamed_result.raw_responses = streamed_result.raw_responses + [
713
+ turn_result.model_response
714
+ ]
715
+ streamed_result.input = turn_result.original_input
716
+ streamed_result.new_items = turn_result.generated_items
717
+
718
+ if isinstance(turn_result.next_step, NextStepHandoff):
719
+ current_agent = turn_result.next_step.new_agent
720
+ # Update input with sanitized content from input guardrails for handoff
721
+ for result in streamed_result.input_guardrail_results:
722
+ if hasattr(result.output, 'sanitized_content') and result.output.sanitized_content:
723
+ streamed_result.input = result.output.sanitized_content
724
+ current_span.finish(reset_current=True)
725
+ current_span = None
726
+ should_run_agent_start_hooks = True
727
+ streamed_result._event_queue.put_nowait(
728
+ AgentUpdatedStreamEvent(new_agent=current_agent)
729
+ )
730
+ elif isinstance(turn_result.next_step, NextStepFinalOutput):
731
+ streamed_result._output_guardrails_task = asyncio.create_task(
732
+ cls._run_output_guardrails(
733
+ current_agent.output_guardrails
734
+ + (run_config.output_guardrails or []),
735
+ current_agent,
736
+ turn_result.next_step.output,
737
+ context_wrapper,
738
+ )
739
+ )
740
+
741
+ try:
742
+ output_guardrail_results = await streamed_result._output_guardrails_task
743
+ except Exception:
744
+ output_guardrail_results = []
745
+
746
+ streamed_result.output_guardrail_results = output_guardrail_results
747
+ # Use sanitized content as final output if available
748
+ final_output = turn_result.next_step.output
749
+ for result in output_guardrail_results:
750
+ if hasattr(result.output, 'sanitized_content') and result.output.sanitized_content:
751
+ final_output = result.output.sanitized_content
752
+ streamed_result.final_output = final_output
753
+ streamed_result.is_complete = True
754
+
755
+ # Save the conversation to session if enabled
756
+ temp_result = RunResult(
757
+ input=streamed_result.input,
758
+ new_items=streamed_result.new_items,
759
+ raw_responses=streamed_result.raw_responses,
760
+ final_output=streamed_result.final_output,
761
+ _last_agent=current_agent,
762
+ input_guardrail_results=streamed_result.input_guardrail_results,
763
+ output_guardrail_results=streamed_result.output_guardrail_results,
764
+ context_wrapper=context_wrapper,
765
+ )
766
+ await AgentRunner._save_result_to_session(
767
+ session, starting_input, temp_result
768
+ )
769
+
770
+ streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
771
+ elif isinstance(turn_result.next_step, NextStepRunAgain):
772
+ pass
773
+ except AgentsException as exc:
774
+ streamed_result.is_complete = True
775
+ streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
776
+ exc.run_data = RunErrorDetails(
777
+ input=streamed_result.input,
778
+ new_items=streamed_result.new_items,
779
+ raw_responses=streamed_result.raw_responses,
780
+ last_agent=current_agent,
781
+ context_wrapper=context_wrapper,
782
+ input_guardrail_results=streamed_result.input_guardrail_results,
783
+ output_guardrail_results=streamed_result.output_guardrail_results,
784
+ )
785
+ raise
786
+ except Exception as e:
787
+ if current_span:
788
+ _error_tracing.attach_error_to_span(
789
+ current_span,
790
+ SpanError(
791
+ message="Error in agent run",
792
+ data={"error": str(e)},
793
+ ),
794
+ )
795
+ streamed_result.is_complete = True
796
+ streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
797
+ raise
798
+
799
+ streamed_result.is_complete = True
800
+ finally:
801
+ if current_span:
802
+ current_span.finish(reset_current=True)
803
+ if streamed_result.trace:
804
+ streamed_result.trace.finish(reset_current=True)
805
+
806
+ @classmethod
807
+ async def _run_single_turn_streamed(
808
+ cls,
809
+ streamed_result: RunResultStreaming,
810
+ agent: Agent[TContext],
811
+ hooks: RunHooks[TContext],
812
+ context_wrapper: RunContextWrapper[TContext],
813
+ run_config: RunConfig,
814
+ should_run_agent_start_hooks: bool,
815
+ tool_use_tracker: AgentToolUseTracker,
816
+ all_tools: list[Tool],
817
+ previous_response_id: str | None,
818
+ ) -> SingleStepResult:
819
+ """Execute a single streamed agent turn including tools and guardrails."""
820
+ if should_run_agent_start_hooks:
821
+ await asyncio.gather(
822
+ hooks.on_agent_start(context_wrapper, agent),
823
+ (
824
+ agent.hooks.on_start(context_wrapper, agent)
825
+ if agent.hooks
826
+ else _coro.noop_coroutine()
827
+ ),
828
+ )
829
+
830
+ output_schema = cls._get_output_schema(agent)
831
+
832
+ streamed_result.current_agent = agent
833
+ streamed_result._current_agent_output_schema = output_schema
834
+
835
+ system_prompt, prompt_config = await asyncio.gather(
836
+ agent.get_system_prompt(context_wrapper),
837
+ agent.get_prompt(context_wrapper),
838
+ )
839
+
840
+ handoffs = await cls._get_handoffs(agent, context_wrapper)
841
+ model = cls._get_model(agent, run_config)
842
+ model_settings = agent.model_settings.resolve(run_config.model_settings)
843
+ model_settings = RunImpl.maybe_reset_tool_choice(agent, tool_use_tracker, model_settings)
844
+
845
+ final_response: ModelResponse | None = None
846
+
847
+ input = ItemHelpers.input_to_new_input_list(streamed_result.input)
848
+ input.extend([item.to_input_item() for item in streamed_result.new_items])
849
+
850
+ filtered = await cls._maybe_filter_model_input(
851
+ agent=agent,
852
+ run_config=run_config,
853
+ context_wrapper=context_wrapper,
854
+ input_items=input,
855
+ system_instructions=system_prompt,
856
+ )
857
+
858
+ # Call hook just before the model is invoked, with the correct system_prompt.
859
+ if agent.hooks:
860
+ await agent.hooks.on_llm_start(
861
+ context_wrapper, agent, filtered.instructions, filtered.input
862
+ )
863
+
864
+ # 1. Stream the output events
865
+ async for event in model.stream_response(
866
+ filtered.instructions,
867
+ filtered.input,
868
+ model_settings,
869
+ all_tools,
870
+ output_schema,
871
+ handoffs,
872
+ get_model_tracing_impl(
873
+ run_config.tracing_disabled, run_config.trace_include_sensitive_data
874
+ ),
875
+ previous_response_id=previous_response_id,
876
+ prompt=prompt_config,
877
+ ):
878
+ if isinstance(event, ResponseCompletedEvent):
879
+ usage = (
880
+ Usage(
881
+ requests=1,
882
+ input_tokens=event.response.usage.input_tokens,
883
+ output_tokens=event.response.usage.output_tokens,
884
+ total_tokens=event.response.usage.total_tokens,
885
+ input_tokens_details=event.response.usage.input_tokens_details,
886
+ output_tokens_details=event.response.usage.output_tokens_details,
887
+ )
888
+ if event.response.usage
889
+ else Usage()
890
+ )
891
+ final_response = ModelResponse(
892
+ output=event.response.output,
893
+ usage=usage,
894
+ response_id=event.response.id,
895
+ )
896
+ context_wrapper.usage.add(usage)
897
+
898
+ streamed_result._event_queue.put_nowait(RawResponsesStreamEvent(data=event))
899
+
900
+ # Call hook just after the model response is finalized.
901
+ if agent.hooks and final_response is not None:
902
+ await agent.hooks.on_llm_end(context_wrapper, agent, final_response)
903
+
904
+ # 2. At this point, the streaming is complete for this turn of the agent loop.
905
+ if not final_response:
906
+ raise ModelBehaviorError("Model did not produce a final response!")
907
+
908
+ # 3. Now, we can process the turn as we do in the non-streaming case
909
+ return await cls._get_single_step_result_from_streamed_response(
910
+ agent=agent,
911
+ streamed_result=streamed_result,
912
+ new_response=final_response,
913
+ output_schema=output_schema,
914
+ all_tools=all_tools,
915
+ handoffs=handoffs,
916
+ hooks=hooks,
917
+ context_wrapper=context_wrapper,
918
+ run_config=run_config,
919
+ tool_use_tracker=tool_use_tracker,
920
+ )
921
+
922
+ @classmethod
923
+ async def _run_single_turn(
924
+ cls,
925
+ *,
926
+ agent: Agent[TContext],
927
+ all_tools: list[Tool],
928
+ original_input: str | list[TResponseInputItem],
929
+ generated_items: list[RunItem],
930
+ hooks: RunHooks[TContext],
931
+ context_wrapper: RunContextWrapper[TContext],
932
+ run_config: RunConfig,
933
+ should_run_agent_start_hooks: bool,
934
+ tool_use_tracker: AgentToolUseTracker,
935
+ previous_response_id: str | None,
936
+ ) -> SingleStepResult:
937
+ """Run one non-streaming agent turn including guardrails and tools."""
938
+ # Ensure we run the hooks before anything else
939
+ if should_run_agent_start_hooks:
940
+ await asyncio.gather(
941
+ hooks.on_agent_start(context_wrapper, agent),
942
+ (
943
+ agent.hooks.on_start(context_wrapper, agent)
944
+ if agent.hooks
945
+ else _coro.noop_coroutine()
946
+ ),
947
+ )
948
+
949
+ system_prompt, prompt_config = await asyncio.gather(
950
+ agent.get_system_prompt(context_wrapper),
951
+ agent.get_prompt(context_wrapper),
952
+ )
953
+
954
+ output_schema = cls._get_output_schema(agent)
955
+ handoffs = await cls._get_handoffs(agent, context_wrapper)
956
+ input = ItemHelpers.input_to_new_input_list(original_input)
957
+ input.extend([generated_item.to_input_item() for generated_item in generated_items])
958
+
959
+ new_response = await cls._get_new_response(
960
+ agent,
961
+ system_prompt,
962
+ input,
963
+ output_schema,
964
+ all_tools,
965
+ handoffs,
966
+ context_wrapper,
967
+ run_config,
968
+ tool_use_tracker,
969
+ previous_response_id,
970
+ prompt_config,
971
+ )
972
+
973
+ return await cls._get_single_step_result_from_response(
974
+ agent=agent,
975
+ original_input=original_input,
976
+ pre_step_items=generated_items,
977
+ new_response=new_response,
978
+ output_schema=output_schema,
979
+ all_tools=all_tools,
980
+ handoffs=handoffs,
981
+ hooks=hooks,
982
+ context_wrapper=context_wrapper,
983
+ run_config=run_config,
984
+ tool_use_tracker=tool_use_tracker,
985
+ )
986
+
987
+ @classmethod
988
+ async def _get_single_step_result_from_response(
989
+ cls,
990
+ *,
991
+ agent: Agent[TContext],
992
+ all_tools: list[Tool],
993
+ original_input: str | list[TResponseInputItem],
994
+ pre_step_items: list[RunItem],
995
+ new_response: ModelResponse,
996
+ output_schema: AgentOutputSchemaBase | None,
997
+ handoffs: list[Handoff],
998
+ hooks: RunHooks[TContext],
999
+ context_wrapper: RunContextWrapper[TContext],
1000
+ run_config: RunConfig,
1001
+ tool_use_tracker: AgentToolUseTracker,
1002
+ ) -> SingleStepResult:
1003
+ """Process a model response and execute any resulting tool calls."""
1004
+ processed_response = RunImpl.process_model_response(
1005
+ agent=agent,
1006
+ all_tools=all_tools,
1007
+ response=new_response,
1008
+ output_schema=output_schema,
1009
+ handoffs=handoffs,
1010
+ )
1011
+
1012
+ tool_use_tracker.add_tool_use(agent, processed_response.tools_used)
1013
+
1014
+ return await RunImpl.execute_tools_and_side_effects(
1015
+ agent=agent,
1016
+ original_input=original_input,
1017
+ pre_step_items=pre_step_items,
1018
+ new_response=new_response,
1019
+ processed_response=processed_response,
1020
+ output_schema=output_schema,
1021
+ hooks=hooks,
1022
+ context_wrapper=context_wrapper,
1023
+ run_config=run_config,
1024
+ )
1025
+
1026
+ @classmethod
1027
+ async def _get_single_step_result_from_streamed_response(
1028
+ cls,
1029
+ *,
1030
+ agent: Agent[TContext],
1031
+ all_tools: list[Tool],
1032
+ streamed_result: RunResultStreaming,
1033
+ new_response: ModelResponse,
1034
+ output_schema: AgentOutputSchemaBase | None,
1035
+ handoffs: list[Handoff],
1036
+ hooks: RunHooks[TContext],
1037
+ context_wrapper: RunContextWrapper[TContext],
1038
+ run_config: RunConfig,
1039
+ tool_use_tracker: AgentToolUseTracker,
1040
+ ) -> SingleStepResult:
1041
+ """Process a streamed model response and enqueue resulting events."""
1042
+ original_input = streamed_result.input
1043
+ pre_step_items = streamed_result.new_items
1044
+ event_queue = streamed_result._event_queue
1045
+
1046
+ processed_response = RunImpl.process_model_response(
1047
+ agent=agent,
1048
+ all_tools=all_tools,
1049
+ response=new_response,
1050
+ output_schema=output_schema,
1051
+ handoffs=handoffs,
1052
+ )
1053
+ new_items_processed_response = processed_response.new_items
1054
+ tool_use_tracker.add_tool_use(agent, processed_response.tools_used)
1055
+ RunImpl.stream_step_items_to_queue(new_items_processed_response, event_queue)
1056
+
1057
+ single_step_result = await RunImpl.execute_tools_and_side_effects(
1058
+ agent=agent,
1059
+ original_input=original_input,
1060
+ pre_step_items=pre_step_items,
1061
+ new_response=new_response,
1062
+ processed_response=processed_response,
1063
+ output_schema=output_schema,
1064
+ hooks=hooks,
1065
+ context_wrapper=context_wrapper,
1066
+ run_config=run_config,
1067
+ )
1068
+ new_step_items = [
1069
+ item
1070
+ for item in single_step_result.new_step_items
1071
+ if item not in new_items_processed_response
1072
+ ]
1073
+ RunImpl.stream_step_items_to_queue(new_step_items, event_queue)
1074
+
1075
+ return single_step_result
1076
+
1077
+ @classmethod
1078
+ async def _run_input_guardrails(
1079
+ cls,
1080
+ agent: Agent[Any],
1081
+ guardrails: list[InputGuardrail[TContext]],
1082
+ input: str | list[TResponseInputItem],
1083
+ context: RunContextWrapper[TContext],
1084
+ ) -> list[InputGuardrailResult]:
1085
+ """Run configured input guardrails for a given agent."""
1086
+ if not guardrails:
1087
+ return []
1088
+
1089
+ guardrail_tasks = [
1090
+ asyncio.create_task(
1091
+ RunImpl.run_single_input_guardrail(agent, guardrail, input, context)
1092
+ )
1093
+ for guardrail in guardrails
1094
+ ]
1095
+
1096
+ guardrail_results = []
1097
+
1098
+ for done in asyncio.as_completed(guardrail_tasks):
1099
+ result = await done
1100
+ if result.output.tripwire_triggered:
1101
+ # Cancel all guardrail tasks if a tripwire is triggered.
1102
+ for t in guardrail_tasks:
1103
+ t.cancel()
1104
+ _error_tracing.attach_error_to_current_span(
1105
+ SpanError(
1106
+ message="Guardrail tripwire triggered",
1107
+ data={"guardrail": result.guardrail.get_name()},
1108
+ )
1109
+ )
1110
+ raise InputGuardrailTripwireTriggered(result)
1111
+ else:
1112
+ guardrail_results.append(result)
1113
+
1114
+ return guardrail_results
1115
+
1116
+ @classmethod
1117
+ async def _run_output_guardrails(
1118
+ cls,
1119
+ guardrails: list[OutputGuardrail[TContext]],
1120
+ agent: Agent[TContext],
1121
+ agent_output: Any,
1122
+ context: RunContextWrapper[TContext],
1123
+ ) -> list[OutputGuardrailResult]:
1124
+ """Run configured output guardrails for a given agent output."""
1125
+ if not guardrails:
1126
+ return []
1127
+
1128
+ guardrail_tasks = [
1129
+ asyncio.create_task(
1130
+ RunImpl.run_single_output_guardrail(guardrail, agent, agent_output, context)
1131
+ )
1132
+ for guardrail in guardrails
1133
+ ]
1134
+
1135
+ guardrail_results = []
1136
+
1137
+ for done in asyncio.as_completed(guardrail_tasks):
1138
+ result = await done
1139
+ if result.output.tripwire_triggered:
1140
+ # Cancel all guardrail tasks if a tripwire is triggered.
1141
+ for t in guardrail_tasks:
1142
+ t.cancel()
1143
+ _error_tracing.attach_error_to_current_span(
1144
+ SpanError(
1145
+ message="Guardrail tripwire triggered",
1146
+ data={"guardrail": result.guardrail.get_name()},
1147
+ )
1148
+ )
1149
+ raise OutputGuardrailTripwireTriggered(result)
1150
+ else:
1151
+ guardrail_results.append(result)
1152
+
1153
+ return guardrail_results
1154
+
1155
+ @classmethod
1156
+ async def _get_new_response(
1157
+ cls,
1158
+ agent: Agent[TContext],
1159
+ system_prompt: str | None,
1160
+ input: list[TResponseInputItem],
1161
+ output_schema: AgentOutputSchemaBase | None,
1162
+ all_tools: list[Tool],
1163
+ handoffs: list[Handoff],
1164
+ context_wrapper: RunContextWrapper[TContext],
1165
+ run_config: RunConfig,
1166
+ tool_use_tracker: AgentToolUseTracker,
1167
+ previous_response_id: str | None,
1168
+ prompt_config: ResponsePromptParam | None,
1169
+ ) -> ModelResponse:
1170
+ """Call the model provider to obtain a new response for this turn."""
1171
+ # Allow user to modify model input right before the call, if configured
1172
+ filtered = await cls._maybe_filter_model_input(
1173
+ agent=agent,
1174
+ run_config=run_config,
1175
+ context_wrapper=context_wrapper,
1176
+ input_items=input,
1177
+ system_instructions=system_prompt,
1178
+ )
1179
+
1180
+ model = cls._get_model(agent, run_config)
1181
+ model_settings = agent.model_settings.resolve(run_config.model_settings)
1182
+ model_settings = RunImpl.maybe_reset_tool_choice(agent, tool_use_tracker, model_settings)
1183
+ # If the agent has hooks, we need to call them before and after the LLM call
1184
+ if agent.hooks:
1185
+ await agent.hooks.on_llm_start(
1186
+ context_wrapper,
1187
+ agent,
1188
+ filtered.instructions, # Use filtered instructions
1189
+ filtered.input, # Use filtered input
1190
+ )
1191
+
1192
+ new_response = await model.get_response(
1193
+ system_instructions=filtered.instructions,
1194
+ input=filtered.input,
1195
+ model_settings=model_settings,
1196
+ tools=all_tools,
1197
+ output_schema=output_schema,
1198
+ handoffs=handoffs,
1199
+ tracing=get_model_tracing_impl(
1200
+ run_config.tracing_disabled, run_config.trace_include_sensitive_data
1201
+ ),
1202
+ previous_response_id=previous_response_id,
1203
+ prompt=prompt_config,
1204
+ )
1205
+ # If the agent has hooks, we need to call them after the LLM call
1206
+ if agent.hooks:
1207
+ await agent.hooks.on_llm_end(context_wrapper, agent, new_response)
1208
+
1209
+ context_wrapper.usage.add(new_response.usage)
1210
+
1211
+ return new_response
1212
+
1213
+ @classmethod
1214
+ def _get_output_schema(cls, agent: Agent[Any]) -> AgentOutputSchemaBase | None:
1215
+ """Resolve the output schema for the provided agent."""
1216
+ if agent.output_type is None or agent.output_type is str:
1217
+ return None
1218
+ elif isinstance(agent.output_type, AgentOutputSchemaBase):
1219
+ return agent.output_type
1220
+
1221
+ return AgentOutputSchema(agent.output_type)
1222
+
1223
+ @classmethod
1224
+ async def _get_handoffs(
1225
+ cls, agent: Agent[Any], context_wrapper: RunContextWrapper[Any]
1226
+ ) -> list[Handoff]:
1227
+ """Collect enabled handoffs for the given agent."""
1228
+ handoffs = []
1229
+ for handoff_item in agent.handoffs:
1230
+ if isinstance(handoff_item, Handoff):
1231
+ handoffs.append(handoff_item)
1232
+ elif isinstance(handoff_item, Agent):
1233
+ handoffs.append(handoff(handoff_item))
1234
+
1235
+ async def _check_handoff_enabled(handoff_obj: Handoff) -> bool:
1236
+ """Determine whether a handoff should run for the current context."""
1237
+ attr = handoff_obj.is_enabled
1238
+ if isinstance(attr, bool):
1239
+ return attr
1240
+ res = attr(context_wrapper, agent)
1241
+ if inspect.isawaitable(res):
1242
+ return bool(await res)
1243
+ return bool(res)
1244
+
1245
+ results = await asyncio.gather(*(_check_handoff_enabled(h) for h in handoffs))
1246
+ enabled: list[Handoff] = [h for h, ok in zip(handoffs, results) if ok]
1247
+ return enabled
1248
+
1249
+ @classmethod
1250
+ async def _get_all_tools(
1251
+ cls, agent: Agent[Any], context_wrapper: RunContextWrapper[Any]
1252
+ ) -> list[Tool]:
1253
+ """Gather all tools available to the agent from hooks and config."""
1254
+ return await agent.get_all_tools(context_wrapper)
1255
+
1256
+ @classmethod
1257
+ def _get_model(cls, agent: Agent[Any], run_config: RunConfig) -> Model:
1258
+ """Resolve which model to call for this agent run."""
1259
+ if isinstance(run_config.model, Model):
1260
+ return run_config.model
1261
+ elif isinstance(run_config.model, str):
1262
+ return run_config.model_provider.get_model(run_config.model)
1263
+ elif isinstance(agent.model, Model):
1264
+ return agent.model
1265
+
1266
+ return run_config.model_provider.get_model(agent.model)
1267
+
1268
+ @classmethod
1269
+ async def _prepare_input_with_session(
1270
+ cls,
1271
+ input: str | list[TResponseInputItem],
1272
+ session: Session | None,
1273
+ ) -> str | list[TResponseInputItem]:
1274
+ """Prepare input by combining it with session history if enabled."""
1275
+ if session is None:
1276
+ return input
1277
+
1278
+ # Validate that we don't have both a session and a list input, as this creates
1279
+ # ambiguity about whether the list should append to or replace existing session history
1280
+ if isinstance(input, list):
1281
+ raise UserError(
1282
+ "Cannot provide both a session and a list of input items. "
1283
+ "When using session memory, provide only a string input to append to the "
1284
+ "conversation, or use session=None and provide a list to manually manage "
1285
+ "conversation history."
1286
+ )
1287
+
1288
+ # Get previous conversation history
1289
+ history = await session.get_items()
1290
+
1291
+ # Convert input to list format
1292
+ new_input_list = ItemHelpers.input_to_new_input_list(input)
1293
+
1294
+ # Combine history with new input
1295
+ combined_input = history + new_input_list
1296
+
1297
+ return combined_input
1298
+
1299
+ @classmethod
1300
+ async def _save_result_to_session(
1301
+ cls,
1302
+ session: Session | None,
1303
+ original_input: str | list[TResponseInputItem],
1304
+ result: RunResult,
1305
+ ) -> None:
1306
+ """Save the conversation turn to session."""
1307
+ if session is None:
1308
+ return
1309
+
1310
+ # Convert original input to list format if needed
1311
+ input_list = ItemHelpers.input_to_new_input_list(original_input)
1312
+
1313
+ # Convert new items to input format
1314
+ new_items_as_input = [item.to_input_item() for item in result.new_items]
1315
+
1316
+ # Save all items from this turn
1317
+ items_to_save = input_list + new_items_as_input
1318
+ await session.add_items(items_to_save)
1319
+
1320
+
1321
+ DEFAULT_AGENT_RUNNER = AgentRunner()
1322
+
1323
+
1324
+ def _copy_str_or_list(input: str | list[TResponseInputItem]) -> str | list[TResponseInputItem]:
1325
+ """Return a shallow copy of list inputs while leaving strings untouched."""
1326
+ if isinstance(input, str):
1327
+ return input
1328
+ return input.copy()