Changelog¶
All notable changes to evaluatorq are documented here.
[1.3.0] — unreleased¶
Notable defaults¶
red_team()takesrecommendations=instead ofgenerate_recommendations=, and simulation gained the same flag with the same three forms:True(defaults),False(skip the LLM call), or a config instance —RedTeamRecommendationConfig/SimulationRecommendationConfig, both bounded andextra='forbid'. Two removals with no deprecation shim:generate_recommendations=onred_team(), and the long-deprecatedconfig=alias forllm_config=(the shim had targeted removal in 1.4.0 and outlived it).generate_focus_area_recommendations()likewise takesrecommendations=in place of itsmax_areas/max_tracespair. Rename the keyword at the call site; behaviour is unchanged. (RES-1286)- Simulation now generates remediation suggestions in-run rather than from a CLI post-run hook, so a saved run carries them regardless of caller.
simulate()/generate_and_simulate()default the flag toFalse, because the returnedSimulationResultlist has nowhere to carry suggestions — withsave/reportboth unset the run would pay for them and drop them, which now logs a warning.eq sim runandeq redteam runkeep--recommendationson by default. (RES-1286) - Assistant turns in a replayed transcript are sent to the Responses API as
output_textparts. A bare string — or a list ofinput_textparts — underrole: "assistant"is silently dropped by the Orq router (some backends 400 instead), so every stateless Responses target and the simulation judge/user-simulator were replaying history with the agent's own turns missing. The simulation judge saw a transcript with no agent replies and reported "the agent has not yet responded", which is the deeper reason no criterion about agent behaviour could ever fail (RES-1308). AffectsOrqResponsesTarget,OpenAIAgentTarget, red-team multi-turn replay, and simulation. An image part on an assistant turn is not representable and is now dropped with a warning. - The
criteria_metscorer returns 0.0 (was1.0) for a simulation that ended in an error or a timeout, and logs a warning. Such a run terminates before the judge audits anything, so its criteria outcome is unknown — scoring it a perfect 1.0 let a dead target inflate the run average andconversation_quality. A run with no criteria at all still scores 1.0. - Simulation
Scenariocriteria are now scored from an explicit per-criterion audit the judge returns on every turn (Judgment.criteria_verdicts), folded across the whole conversation. Previously pass/fail was inferred from the absence of a criterion id inrules_broken, somust_happencriteria could never fail andcriteria_metreturned1.0on every run (RES-1308). This changes scores for existing callers who use criteria:criteria_met,conversation_quality,rules_brokenandcriteria_resultscan now report failures where they previously reported none. Amust_happencriterion passes if it occurred in any turn; amust_not_happencriterion fails if it was violated in any turn. A customjudgethat does not emitcriteria_verdictsfalls back to the old behaviour, logs a warning naming the scenario, and is markedSimulationResult.criteria_verified = False.Judgment.criteria_verdictsislist[CriterionVerdict] | None.CriterionVerdict(new, public, inevaluatorq.simulation.types, re-exported fromevaluatorq.simulation) reportscriterion_id,occurredandevidencefor one criterion on one turn — occurrence only, never pass/fail.Nonemeans the judge reported nothing (unknown,criteria_verified=False);[]means it audited and had nothing left to report; a non-empty list is evidence. New public helperscriterion_id_for(index)andCRITERION_ID_PATTERN(both also re-exported fromevaluatorq.simulation) fix thecriteria_Nid format in one place. - The simulation judge's
finish_conversationtool no longer takes arules_brokenargument. Violations are derived in code from the occurrence audit andCriterion.type; the free-text list is the channel that could not fail amust_happencriterion in the first place, and asking for both gave them something to disagree about. A criterion the audit skipped now keeps its not-observed default instead of being rescued from free text.Judgment.rules_brokenandSimulationResult.rules_brokenare unchanged as outputs — only the tool input is gone, so a customjudgethat populates the field itself still works. - The judge stops re-auditing a criterion once it is confirmed to have occurred. Occurrence is sticky, so a settled criterion cannot change; it stays in the prompt (the judge needs it to decide whether to end the conversation early) but drops out of the per-turn
criteria_verdictspayload, which costs an id, a boolean and an evidence quote per criterion per turn. A custom judge without amark_settledmethod keeps auditing everything. metadata['criteria_meta']entries gainaudited— whether the judge actually returned an occurrence verdict for that criterion, as opposed to it falling to the not-observed default. Amust_happenthe judge confirmed never occurred and one it silently skipped both reportpassed: False; only this field separates them.Nonefor runs saved before the field existed.metadata['criteria_meta']entries also gainevidence— the quote from the turn where the criterion's occurrence first flipped, sourced from the judge'scriteria_verdictsaudit.''when the criterion never occurred,Nonewhen no tracker was available (same convention asaudited).- New
SimulationResult.criteria_verifiedfield, andcriteria_metreturns 0.0 for a run where it isFalse. It isFalsewhenever the judge returned no per-criterion occurrence audit for any turn — a customjudgepredatingcriteria_verdicts, or the built-inJudgeAgentterminating for safety after an unparseable tool call. Those verdicts came from the free-textrules_brokenlist, which cannot fail amust_happencriterion, so an all-green result there is unknown rather than passing; scoring it 1.0 reproduced RES-1308 one layer up, with a log line as the only signal.Noneon runs saved before the field existed, and those keep their previous score. - The
criteria_metevaluator now reportspass=False— with an explanation naming the cause — on exactly the runs it scores0.0: one that ended in an error or a timeout, and one withcriteria_verified = False. The flag was previously derived fromcriteria_metaalone, so an unaudited run landed on the evaluator trace span and the uploaded Orq experiment as a greenPASSbeside its own0.0. An errored run, which has nocriteria_metaat all, reported "No criteria defined for this scenario." andpass=Truefor a scenario that does have criteria. criteria_metno longer counts an individual criterion the judge never audited as met, on any surface. The scorer readsmetadata['criteria_meta']when present (the only placeauditedsurvives —criteria_resultsis keyed by description and carries no provenance) and counts a criterion only when it passed and was audited, logging a warning naming how many were not; the evaluator explanation printsUNKNOWN [required]: … (not audited)instead ofPASSfor it and excludes it frompass. This lowerscriteria_metfor runs where the judge audited some criteria and skipped others — previously the score counted the skipped ones as met while the report's own "N/M criteria met" tally did not, so the two contradicted each other. A criterion the judge settled early is audited (a verdict is what settles it), somark_settlednever costs a run a point;audited: None(a run saved before the field existed) still counts as met.auditedandevidencenow reach the reports.CriteriaRow(inevaluatorq.simulation.types, the per-criterion view model behind the report sections) gainsaudited,evidenceand a computedstateofpass/fail/unknown, andSimulationEntrygainscriteria_verified. Every surface rendersstate, notpassed: a criterion that passed only because the judge never audited it shows as not audited (a neutral?) in the dashboard, the HTML report and the markdown export, is excluded from the "N/M criteria met" tally, and a run withcriteria_verified = Falsesays so above the criteria list instead of showing a tally that contradicts itscriteria_metscore of0.0. The judge's evidence quote is shown beside the criterion it justifies.- A simulation whose target fails mid-run now keeps the criteria audit collected before the failure. The error result carries the folded
rules_broken,criteria_results,criteria_metaandcriteria_verifiedinstead ofrules_broken=[]and no metadata, so amust_not_happenviolation the judge confirmed on turn 2 survives the target dying on turn 4 — it previously vanished from the result and the report. (It never reachedfind_triggers: that helper returns[]for any errored result before it looks at criteria, and still does.) On this path only confirmed occurrence is knowledge: amust_not_happenthe judge saw violated stays failed, while amust_happenthat had not occurred yet is reported asunknown(row stateunknown,audited: False), never as failed — the run was cut short before that criterion had its chance, so folding the not-observed default would invent a failure the judge never made and add a phantom row to the cross-run failure-mode table. A target that dies before the judge audits anything reports every criterion that way, pluscriteria_verified=False. Such a run is still scored0.0bycriteria_met, because it terminated by error. EVALUATORQ_SPAN_MAX_TEXT_CHARSdefaults to capturing all message content (no truncation), in both the Python and TypeScript tracing layers. Set the env var to a positive integer (canonical:8192) to cap span text at that many characters (marker... [truncated]);-1,0, or unset all mean capture all. The cap applies uniformly to input and output message content. (RES-715 introduced an8192default; RES-899 reverts to capture-all and unifies the TS path, which previously hardcoded a separate2000-char cap.)evaluatorq()defaults todatapoint_parallelism=10(previously1). Evaluations are almost entirely provider-bound I/O, so the old default made the common case pay a latency penalty to protect the uncommon one. This changes behavior for existing callers who omit it: ten datapoints now run concurrently. Passdatapoint_parallelism=1to restore serial execution — do so if your provider rate-limits at low concurrency, or if your jobs mutate shared state that was previously serialized by accident rather than by design. Red teaming already defaulted to 10; simulation is raised from 5 to 10 in the same release, so the number now means the same thing on every entry point.- A target call makes exactly
max_target_retries + 1HTTP attempts, on every path.call_target_with_retryowns target retries, so the SDK's own budget is disarmed at that boundary (without_client_retries, awith_optionsclone — an injected client is never mutated and keeps its transport, auth, base URL, headers and timeout). Previously the two layers stacked and multiplied: an injected OpenAI or Responses client left at the SDK default made 9 HTTP calls where 3 were intended, at 3× the cost and latency, and a caller who setretry_counton such a client had no way to see it. Judge and pipeline calls are unchanged — each already had a single owner. Simulation agent calls (the user simulator and the judge agent) now honourLLMCallConfig.retry_count:SimulationAgent._call_chat_completions/_call_responsespassed no budget towith_retry, so they always used the module default of 5 transport attempts regardless of configuration. They now make exactlyretry_count + 1(default 2, previously 5). The chat-completions path additionally retries once within an attempt on an empty response — a content-level retry, so a model that keeps returning nothing still costs up to2 × (retry_count + 1)calls.retry_count/retry_on_codespassed for a target call are now ignored with a warning naming the owner rather than silently. evaluatorq()'sdatapoint_parallelismnow bounds evaluator fan-out too. Evaluators within a job previously ran with unbounded concurrency (a datapoint with 50 evaluators issued 50 concurrent provider calls no matter what the datapoint count said); they now share the same per-datapoint semaphore the jobs use. This lowers throughput for callers who relied on the unbounded behaviour — raisedatapoint_parallelismto restore it. The budget is shared, not split: a job releases its slot before its evaluators take theirs, so the two never contend anddatapoint_parallelism=1cannot deadlock.- New
llm_parallelism=onevaluatorq(),red_team(),simulate(),generate_and_simulate()andgenerate()— a ceiling on in-flight LLM requests for the whole run, counted per request rather than per task. Unbounded by default, so nothing changes unless you set it. This is the knob to size against a provider concurrency limit:datapoint_parallelismbounds tasks, and the task bounds nest (datapoints × jobs/evaluators × jury width), sodatapoint_parallelism=10can mean anywhere from 10 to several hundred concurrent requests depending on the fan-out — the number was never something you could compute a request rate from. Requests routed throughcommon.llm_call(judges, juries, simulation agents, the red-team pipeline, the OpenAI backend) take a slot automatically; a job that calls a provider SDK directly is invisible unless you wrap it in the newevaluatorq.common.llm_limit.llm_slot()context manager, which is also what closes the gap for the ORQ and LangChain targets. Note this is a concurrency bound, not a rate limit: N slots isN / latencyrequests per second, so a provider that gets faster raises your request rate at a fixed N. parallelism=is renameddatapoint_parallelism=onevaluatorq(),red_team(),simulate(),generate_and_simulate()andgenerate(), and--parallelismis renamed--datapoint-parallelismoneq redteamandeq simulate. Both old names still work and emit aDeprecationWarning;EvaluatorParamsaccepts either field name. With two concurrency knobs the bare name no longer said which one it meant — one counts datapoints, the other counts LLM requests. The OTel span attributesorq.redteam.parallelismandorq.simulation.parallelismkeep their keys, so existing trace queries and saved dashboard filters are unaffected. Breaking for hook implementors: the red-teamConfirmPayloadand the simulationSimulationRunMetakey is nowdatapoint_parallelism— a hook readingpayload['parallelism']willKeyError.- New
--llm-parallelismflag oneq redteamandeq simulate, exposing thellm_parallelism=ceiling to CLI callers. - Simulation's
datapoint_parallelismdefaults to 10 (previously5), matchingevaluatorq()and red teaming. This raises concurrency for callers who omit it; passdatapoint_parallelism=5to keep the old value, or setllm_parallelism=to bound provider load directly. Applies tosimulate(),generate_and_simulate(),SimulationConfigandeq simulate/eq simulate run. evaluatorq()never exits the process when an evaluator reportspass_=False; it returns the results so library callers can inspectpass_and choose their own gate. Red-team and simulation surfaces retain their own explicit failure gates.loguruis now a core dependency (previously gated behind the[redteam]extra). This slightly widens the install footprint for non-redteam consumers but unifies the logging stack across the package.openai(>=1.92.0) is now a core dependency (previously gated behind the[redteam]extra). The newllm_jury()evaluator imports it at package load, so every base install pulls it; this widens the base footprint for users who only callevaluate(), in exchange forllm_jury()working without an extra.datapoints_from_traces()andextend_from_traces()now summarize every trace conversation unconditionally before the persona/scenario or traffic-profile call reads it — a short trace that previously skipped straight to that call now costs one extra LLM call in direct mode too.TraceAnalysisConfig.summarize_above_charsis removed with no deprecation shim; becauseTraceAnalysisConfigisextra='forbid',TraceAnalysisConfig(summarize_above_chars=...)now raisesValidationError— drop the field. A newsummarize_conversations()entry point runs that summarize step directly: call it once and pass the result assummaries=to either function so a run that calls both does not summarize the same trace twice; asummaries=mapping is authoritative, so a trace_id absent from it (because it failed to summarize) is dropped rather than retried. (RES-1286)
Breaking Changes¶
red_team()parameter renamed:config=→llm_config=. The oldconfig=keyword still works in 1.3.0 but emits aDeprecationWarningand will be removed in 1.4.0.LLMConfigflat fields removed:attack_model,evaluator_model,adversarial_temperature,adversarial_max_tokens,llm_call_timeout_ms,llm_kwargs— replaced by role-basedattacker/evaluatorsub-configs (LLMCallConfig)wrap_simulation_agent()no longer accepts theevaluators=kwarg. Evaluators are wired throughevaluatorq()directly (the framework that consumes the job); callers passingevaluators=[...]will now get aTypeErrorand should move the list onto theirevaluatorq(..., evaluators=...)call instead (RES-594).simulate()andgenerate_and_simulate()no longer acceptagent_key=. The singletarget=parameter now selects the target:"agent:<key>"or a bare"<key>"(hosted Orq agent via the Responses router),"deployment:<key>"(legacy deployment), anAgentTarget, or a callable. Callers passingagent_key=...get aTypeError; migrate totarget="deployment:<key>"(ortarget="agent:<key>"). Theeq sim simulate/eq sim runCLI drops its matching--agent-keyflag — use--target deployment:<key>.simulate()andgenerate_and_simulate()now defaultupload_results=True. With the move to evaluatorq-native execution the framework's upload is the canonical persistence path — the previousFalsedefault left runs with no record anywhere. Setupload_results=Falseexplicitly to suppress (RES-594).
Migration:
# Before
red_team(target, config=LLMConfig(attack_model="gpt-4o", evaluator_model="gpt-4o-mini"))
# After
from evaluatorq.redteam.contracts import LLMCallConfig, LLMConfig
red_team(
target,
llm_config=LLMConfig(
attacker=LLMCallConfig(model="gpt-4o"),
evaluator=LLMCallConfig(model="gpt-4o-mini"),
),
)
AgentTargetrelocated: moved fromevaluatorq.redteam.backends.basetoevaluatorq.contracts. Importing it from the old path now raisesImportError. TheBackendABC stays inevaluatorq.redteam.backends.base.AgentContext,ToolInfo,MemoryStoreInfo, andKnowledgeBaseInfoalso moved toevaluatorq.contracts, but — unlikeAgentTarget— their old import pathevaluatorq.redteam.contractsstill works (re-exported, same class objects,isinstanceunaffected). OnlyAgentTarget's old path is a hard break.
Migration:
# Before
from evaluatorq.redteam.backends.base import AgentTarget
# After
from evaluatorq.contracts import AgentTarget
AgentTargetunified onrespond(messages):respond(messages: list[Message]) -> AgentResponseis now the abstract method every target implements.send_prompt(prompt: str) -> AgentResponseis retained as a concrete back-compat shim on the ABC — it wraps the prompt in a single user message and callsrespond. Custom targets that previously implemented onlysend_promptmust implementrespondinstead.
Migration (bare custom subclass):
# Before — only send_prompt was abstract
from evaluatorq.contracts import AgentResponse, AgentTarget
class MyTarget(AgentTarget):
async def send_prompt(self, prompt: str) -> AgentResponse:
return AgentResponse(text=await my_llm_call(prompt))
def new(self) -> "MyTarget":
return MyTarget()
# After — respond is the abstract method; send_prompt is a free shim on the ABC
from evaluatorq.contracts import AgentResponse, AgentTarget, Message
class MyTarget(AgentTarget):
async def respond(self, messages: list[Message]) -> AgentResponse:
prompt = messages[-1].content or ""
return AgentResponse(text=await my_llm_call(prompt))
def new(self) -> "MyTarget":
return MyTarget()
OrqResponsesTarget is now stateless: __call__, _previous_response_id threading, _accumulated_usage, and get_usage() are removed. Conversation continuity is the caller's responsibility — pass the full transcript to respond each turn. Pass the target to simulate(target=...) (auto-routes to the target-agent path) or simulate(target_agent=...) instead of relying on __call__. Per-call token usage is reported on the returned AgentResponse.usage. - ORQAgentTarget last-user contract: respond(messages) forwards only the last user message to the ORQ agents endpoint (server-side state is held via task_id) and raises ValueError if messages[-1].role != "user". The endpoint, task_id threading, and usage accumulation are unchanged. - ChatMessage alias removed: the RES-596 deprecated alias ChatMessage = Message is gone. Import Message from evaluatorq.contracts (the public evaluatorq.simulation.ChatMessage re-export is also removed). - Simulation TargetAgent Protocol removed: the simulation runner consumes the canonical AgentTarget ABC from evaluatorq.contracts. The evaluatorq.simulation.TargetAgent / evaluatorq.simulation.runner.TargetAgent exports are replaced by AgentTarget. Migration:
# Before
from evaluatorq.simulation.types import ChatMessage
from evaluatorq.simulation import TargetAgent
# After
from evaluatorq.contracts import Message # ChatMessage was an alias of Message
from evaluatorq.contracts import AgentTarget # replaces the simulation TargetAgent Protocol
CallableTargetforwards the full transcript: the wrapped callable now receives the entire conversation as alist[Message](previously only the last user turn as astr), so stateless callables retain context across multi-turn attacks. The callable signature changes from(prompt: str)to(messages: list[Message]), andusage_fnfrom(prompt: str, response: str)to(messages: list[Message], response: str). The former last-turn-must-be-user guard is dropped (matching the other stateless targets). Callables that need OpenAI chat-completion dicts can callMessage.to_chat_completion()per element.
Migration:
from evaluatorq.contracts import Message
from evaluatorq.integrations.callable_integration import CallableTarget
# Before
target = CallableTarget(lambda prompt: my_agent(prompt))
# After — read the last turn off the transcript
target = CallableTarget(lambda messages: my_agent(messages[-1].content or ""))
New Features¶
llm_jury()— LLM-as-a-jury evaluator forevaluatorq(evaluators=[...]). A single judge or a panel rates a target output against criteria; verdicts can be boolean (default), labeled categorical (labels=+passing_labels=), or numeric (verdict_kind="numeric"+threshold=). The panel consensus rule is selectable viaaggregator=:"mode"(default) or"majority"(strict >50%) for categorical,"mean_std"(default) /"median"/"min"/"max"for numeric, or a customCallable[[list[JuryVote]], ...]. Uses structured generation (tiered.parse→json_objectfallback) and resolves the LLM client lazily on first scorer call so declaring an evaluator never requires credentials. The Responses-API path is deferred (RES-972). (RES-848)OWASP_LLM_TOP_10andOWASP_ASI_TOP_10— publiclist[str]constants exported fromevaluatorq.redteam. Pass them tored_team(categories=OWASP_LLM_TOP_10)to run a full framework sweep without spelling out individual category codes (RES-815).simulate()andgenerate_and_simulate()accept a new opt-inupload_results=flag (defaultFalse). When set toTrue, results are uploaded to the Orq platform after the run, surfacing as an experiment whenORQ_API_KEYis configured. Upload errors are logged but never fail the call. Both functions also acceptevaluation_description=andpath=parameters mirroringevaluatorq()(RES-598).LLMCallConfig— per-role LLM configuration withmodel,temperature,max_tokens,timeout_ms,extra_kwargs, andclientfieldsLLMConfig— now role-based viaattacker: LLMCallConfigandevaluator: LLMCallConfig; retry, cleanup, and target-agent timeout settings retained at top levelLLMCallConfigexported from theevaluatorq.redteampublic APIOpenAIModelTarget.send_promptnow enforcestimeout_msviaasyncio.wait_for- Evaluator role config (
temperature,max_tokens,timeout_ms,extra_kwargs,client) fully propagated throughOWASPEvaluator,create_dynamic_evaluator, andcreate_owasp_evaluator simulate()andgenerate_and_simulate()accept newevaluation_description=andpath=parameters, forwarded straight toevaluatorq()(RES-598).simulate()andgenerate_and_simulate()now run on top ofevaluatorq(): persona × scenario datapoints are materialised, executed via a single evaluatorq job, and scored via adapted evaluators. This brings auto-upload, OTel tracing, the results table, CI gating, and dataset-id support to the simulation entry points "for free". The bespoke parallelism loop was removed;simulation/upload.pyis kept as a standalone helper for direct callers but is no longer invoked fromsimulate()(RES-594).simulate()accepts a newdataset_id=parameter — when set, simulation datapoints are streamed from the named Orq dataset (each row'sinputsmust already match a simulation input shape) instead of being passed inline. Mutually exclusive withdatapointsandpersonas/scenarios(RES-594).simulate()andgenerate_and_simulate()accept a newexit_on_failure=parameter, defaultTrue, for their own dropped-row gate. Evaluator score failures are returned in the results; dropped jobs raiseSimulationDroppedError. Passexit_on_failure=Falsefor interactive / exploratory runs where you want dropped rows surfaced as warnings + error metadata instead of a non-zero exit (RES-594).
Bug Fixes¶
safe_substitute()dict keys were broken by Ruff RUF027 auto-fix inattack_generator,capability_classifier, andobjective_generator— LLM prompts were receiving unsubstituted{placeholder}text, silently producing degraded attacksgenerate_recommendations=Truenow correctly usesllm_config.evaluator.clientbefore falling back tocreate_async_llm_client()- All hardcoded timeout literals (
240_000,90_000) replaced with config-driven values fromLLMConfig/DEFAULT_TARGET_TIMEOUT_MS OpenAITargetFactorynow propagatesmax_tokensandtimeout_msto created targets
Internal¶
SaveModeconverted fromLiteraltoStrEnum- Timeout defaults centralised in
contracts.py(DEFAULT_TARGET_TIMEOUT_MS = 240_000);PIPELINE_CONFIGimport removed fromopenai.pyandregistry.py MultiTurnOrchestrator.llm_kwargsconstructor param deprecated — merged into_cfg.attacker.extra_kwargsat init time; useLLMCallConfig.extra_kwargsinstead- RUF027 added to Ruff ignore list (intentional literal string keys used as
safe_substitutetemplate placeholders) - CLI
--saveflag migrated totyper.Choice - Ruff cleanup across all redteam modules (import sorting,
Optional[X]→X | None,TYPE_CHECKINGguards)
Breaking Changes (RES-877)¶
AgentTarget.send_promptremoved:respond(messages: list[Message]) -> AgentResponseis now the sole response method on every target; callers own the conversation transcript. Migratetarget.send_prompt("x")totarget.respond([Message(role="user", content="x")]).OpenAIModelTarget,VercelAISdkTarget, andOpenAIAgentTargetare now stateless: per-instance_historyis gone. Multi-turn conversation state is owned by the red-team orchestrator, not the target.evaluatorq.redteam.ErrorInforenamed toRunError: update any imports orisinstancechecks that reference the old name.
Migration:
# Before
response = await target.send_prompt("Hello")
# After
from evaluatorq.contracts import Message
response = await target.respond([Message(role="user", content="Hello")])
New Features (RES-877)¶
AgentResponseError— a per-response error marker exposed onAgentResponse.error; used by the orchestrator to exclude failed turns from the replayed transcript.turns_to_messages(turns, *, skip_errors=False)— helper exported fromevaluatorq.redteam.contractsthat converts a list of completed turns into a flatlist[Message], optionally dropping turns whose response carries anAgentResponseError.classify_error_type(error, *, existing_type=None)— exported fromevaluatorq.redteam.contracts; infers a coarseerror_type(content_filter,rate_limit,timeout,network_error,server_error,client_error, orunknown) from an error string. Shared by the orchestrator and report converters. On a per-responseAgentResponseError, the orchestrator records an unmatched (unknown) result astarget_error, so that field never carriesunknown.- Tool-call fidelity on replay — the transcript replayed to a target now preserves assistant
tool_callsandtoolresults across turns (OpenAIModelTargetas OpenAI chat params,VercelAISdkTargetas AI SDK CoreMessagetool-call/tool-resultparts,OpenAIAgentTargetas Responses-APIfunction_call/function_call_outputitems), so multi-turn tool-using agents see their prior tool context.VercelAISdkTargetacceptsmessage_format="v5"(default) or"v4"to match the endpoint's AI SDK version (input/output:{type,value}vsargs/result). Errored turns recorded by the orchestrator now carry a classifiedAgentResponseError.error_typeinstead of a flattarget_error.
Internal (RES-899)¶
- Unified tracing layer: the generic OTel span-recording helpers previously duplicated across
redteam/tracing.pyandsimulation/tracing.pynow live in a singleevaluatorq.common.tracingmodule (truncate_for_span,capture_message_content,record_token_usage,record_llm_response,record_llm_input/output,set_span_attrs,get_trace_context_headers). Domain-specific span builders (with_redteam_span,with_simulation_span,with_llm_span) stay in their domain modules and import the shared helpers. The common module never imports fromredteam,simulation, oropenresponses.
Changed (RES-899)¶
- Span PII gate env var renamed to
EVALUATORQ_CAPTURE_MESSAGE_CONTENT(defaulttrue), replacing the previousOTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT. The same name now gates both the Python and TypeScript simulation/red-team tracing layers. Setfalse/0to keep raw prompt and response text off spans (token usage, model, finish reason, and latency are still recorded). - Span text truncation defaults to capture-all in both Python and TypeScript.
EVALUATORQ_SPAN_MAX_TEXT_CHARSis unset by default (no truncation); set a positive integer (canonical:8192) to cap input and output message content, with the shared... [truncated]marker.-1/0/ unset all mean capture all. The TypeScript path previously hardcoded a separate2000-char cap with a…marker — both are gone.
Fixed (RES-899)¶
retry_statusesaugments the default set again: passing a custom set (e.g.{429}) no longer silently drops the built-in429 + 5xxretries — the custom statuses are added to the defaults, not substituted for them. (This restores the intended RES-897 review behavior, which was lost when #150 merged without the fix.)