ScriptedModel is a deterministic, provider-neutral Model for testing agents without live calls; it records each model call's full boundary and lets you script responses, errors, or streams with factory methods and optional dynamic responders. Tests use ScriptedModel to enqueue or extend scripted steps, inspect recorded calls via snapshots, validate all interactions were exercised with assert_complete(), and diagnose misconfigurations through structured error types.
ScriptedModel in src/agents/testing/model.py is a deterministic, provider-neutral Model implementation for testing agent workflows without live provider calls, introduced in v0.21.0.[1] ScriptedModel.__init__ accepts an iterable of ModelScriptItem steps (each a ModelStep, ModelStepSpec dict, ModelResponse, output-item sequence, or Exception), an emit_traces bool, and a default_usage that applies when a step does not supply its own usage.[1]
ModelCall in src/agents/testing/model.py is a frozen dataclass capturing the full provider-neutral boundary of one model call: system_instructions, input, model_settings, tools, output_schema, handoffs, tracing, previous_response_id, conversation_id, prompt, and streamed.[1] ScriptedModel.calls returns detached deep-copy snapshots of every recorded model call, so mutations after the fact do not corrupt the record.[1] ScriptedModel.first_call and ScriptedModel.last_call return detached snapshots of the first and most recent recorded calls, or None if no calls have been made yet.[1] ScriptedModel.remaining_steps returns the count of scripted model steps not yet consumed.[1]
ScriptedModel.enqueue() appends a single step and ScriptedModel.extend() appends multiple steps, allowing incremental test setup after construction.[1] ScriptedModel.assert_complete() raises UnconsumedModelSteps if any configured steps remain after a test, letting tests enforce that all scripted interactions were exercised.[1]
ModelStep in src/agents/testing/model.py has a default response_id of "resp-789", so tests that do not set a response ID will see that sentinel value.[1] ModelStep.raise_error() is a factory that creates a step which raises a given exception, optionally with provider retry guidance via retry_advice.[1] ModelStep.respond() is a factory that creates a step whose result is dynamically derived from the recorded ModelCall via a ModelResponder callable.[1] ModelStep.stream() is a factory for streaming tests that accepts either a sequence of TResponseStreamEvent objects or a ModelStreamFactory callable, plus optional output, usage, and response_id.[1] ModelResponder is a type alias for a callable that takes a ModelCall and returns either a ModelStepResult or an Awaitable[ModelStepResult], so responders may be async.[1] ModelStreamFactory is a type alias for a callable that takes a ModelCall and returns an AsyncIterator[TResponseStreamEvent], used to supply dynamic streaming event sequences.[1] ModelStepResult in src/agents/testing/model.py is the resolved outcome of a scripted step — either a model response or a raised exception — at the provider-neutral boundary; it is the return type required by ModelResponder and produced internally by ModelStreamFactory.
ScriptedModel.get_retry_advice() returns retry advice only when the request.error object is the exact same exception instance that was configured on the step — identity-checked via id() — preventing accidental advice leakage.[1]
UnexpectedModelCall carries the ModelCall object and its index (call_index) for diagnosis when a test drives more model calls than were scripted.[1] InvalidModelStep carries a reason field (a ModelStepReason literal) and the input_index of the offending step, aiding diagnosis of misconfigured scripts.[1] ModelStepReason in src/agents/testing/model.py is a Literal type alias enumerating the reasons a step can be rejected: "invalid_input", "unsupported_field", "invalid_error", "invalid_responder", "invalid_stream_events", "conflicting_outcomes", "invalid_retry_advice".[1]
Use ScriptedModel with enqueue to script model responses, then run an agent with duplicate input items via Runner.run
model = ScriptedModel()
model.enqueue([get_text_message("done")])
agent = Agent(name="test", model=model)
input_items = [get_text_input_item("repeat"), get_text_input_item("repeat")]
await Runner.run(agent, input=input_items)
Sources