Skip to content

Release 0.5.0: Inspect AI and OpenEnv integrations, target extension points - #130

Open
kelkalot wants to merge 29 commits into
mainfrom
release/0.5.0
Open

kelkalot wants to merge 29 commits into
mainfrom
release/0.5.0

Conversation

@kelkalot

Copy link
Copy Markdown
Collaborator

Release 0.5.0: everything merged into dev since 0.4.2, plus the version bump. main has no commits of its own that dev lacks, so this merges cleanly. After merging, a GitHub Release v0.5.0 publishes the package to PyPI.

Added

Fixed

These issues close when this merges into main.

Behaviour changes

  • HTTPAppTarget
    • It sends each turn once, without the engine's markers. In messages mode it now sends the audit's system prompt.
    • A multi-turn scenario with documents needs documents_field.
    • A scenario with file_uri is an error.
    • A non-list history in request_template is an error.
  • CallableTarget functions get documents as text through documents, and files through file_uri on the turn that attaches them, instead of finding them as markers in history.
  • DecisionTarget treats an explicit documents=[] as given.

Dependencies

  • New extras: inspect (inspect-ai>=0.3.283) and openenv (openenv>=0.10). The core install is unchanged.
  • Anthropic version conflict. Inspect's Anthropic provider needs anthropic>=2, while any-llm-sdk 1.40.1 requires anthropic<1. The README describes the workaround for Inspect runs.

Verification

  • Tests and CI. CI passed on dev after the last merge. pytest -n auto with all extras: 1725 passed.
  • Build. The wheel and sdist build as 0.5.0.
  • Installed wheel, no extras (Python 3.11). It imports, and doesn't load inspect-ai or openenv.
  • Installed wheel with [inspect,openenv] (Python 3.12). Inspect resolves simpleaudit/audit through the entry point.
  • Live checks with claude-haiku-5-5, as described in each PR.

kelkalot and others added 29 commits October 24, 2026 17:35
Records, for 28 audit configurations, everything the engine sends to the
target, auditor and judge clients, the stored results and the on_turn
events, and compares them with JSON files in tests/snapshots/core/.

The snapshots were generated on the code before the integration seams
land, so the next commit can show that existing payloads do not change.
Run ids, image bytes and the checkout path are normalised.
Regenerate with SIMPLEAUDIT_UPDATE_SNAPSHOTS=1 when a change is intended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seams for auditing agents and running inside other frameworks. Every
addition is optional; existing payloads are unchanged (the snapshots from
the previous commit pass as recorded).

- Targets may define async start_scenario(context) / end_scenario(context).
  run_scenario awaits start after scenario validation and end after the
  last turn, before judging; end also runs on failure and cancellation.
  Hooks are looked up on the target's class and must be coroutine
  functions, so MagicMock targets are unaffected. run_scenario now binds
  self.target once per scenario, because the property builds a new
  ModelTarget on each access.
- TargetResponse.tool_events: an agent's tool calls and results for the
  turn. Stored beside the reply, rendered for the judge only as a
  TOOL ACTIVITY block before the assistant turn, and stripped from history
  before provider calls.
- TargetResponse.end_conversation: the target ended the conversation; the
  loop stops and the transcript so far is judged.
- ModelAuditor(judge_client=..., auditor_client=...): ready-made clients
  replace AnyLLM creation for that role. An injected auditor client wins
  over every fallback; without one, the existing branches apply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
simpleaudit.integrations.inspect, behind the new `inspect` extra:

- `simpleaudit/audit` task (registered through an inspect_ai entry
  point). The task model is the target, the `grader` role the judge
  (required, so the target never judges itself) and the `auditor` role
  the probe writer (defaults to the grader). Each sample runs the
  unchanged SimpleAudit loop through InspectModelClient, an acompletion
  shim over an Inspect Model, injected via judge_client/auditor_client.
  Epochs reduce to the modal severity; metrics are the 0-100 safety
  score and counts per severity.
- Inspect keeps control of failures: the shim records the first
  exception from a model call and the solver re-raises it, so token
  limits, terminations and provider errors behave as in any Inspect
  eval instead of becoming SimpleAudit ERROR results.
- load_inspect_log turns any Inspect log into RepeatedExperimentResults,
  one run per epoch. SimpleAudit-task samples come back exactly as
  stored; other samples are rebuilt from their messages with tool calls
  and results as tool_events, so rejudge, reframing_check and
  perturbations work on Inspect transcripts.

CI installs the extra so the tests run there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenEnv's HarnessEnvironment (RFC 005) runs a real agent harness such as
Claude Code and serves it the environment's tools over MCP. OpenEnvTarget
puts that agent in SimpleAudit's target seat:

- one fresh episode per scenario, opened in start_scenario and closed in
  end_scenario, either in process (env_factory) or on a server
  (base_url, through GenericEnvClient)
- each auditor message is one HarnessAction step; the reply comes from
  metadata["response"] and the turn's tool calls and results become
  tool_events, so the judge sees what the agent did
- a failed turn raises (scenario ERROR); an episode the agent ends sets
  end_conversation; scenarios with attachments are rejected
- reset_kwargs can depend on the scenario name; max_sessions defaults
  to 1 for a server, which holds one session
- openenv is imported only when a scenario starts, so `import
  simpleaudit` stays free of it

examples/openenv_agent_audit.py audits a caseworker agent in a mock
benefits case system (scripted agent or Claude Code) and prints whether
each scenario changed the world. The `openenv` extra pulls the package;
CI installs it so the tests run there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
any-llm-sdk 1.40.1 requires anthropic<1 in its base dependencies, not
only in its [anthropic] extra, so every SimpleAudit install pins it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
run_scenario passes the whole conversation as history, current turn
included, with SimpleAudit's markers beside the text. _build_body
forwarded it as-is, so the app received the current user turn twice
(appended again in messages mode, repeated in history otherwise) and
the markers: documents with their evaluation-only marks, file_uri paths
and decision answers.

History now goes out as plain {role, content} turns, without a trailing
copy of the current turn; direct callers that pass earlier turns only
are unchanged. A multi-turn scenario's documents are taken from the
history marker and sent through documents_field as text, like a
context-grounding run's, and fail the same way when no documents_field
is set.

Fixes #124

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Tool events are stored as a JSON-safe copy (non-JSON values become
  strings, so results can always be saved) and must be a list of dicts;
  anything else fails that turn with a clear error.
- The judge sees at most 50 tool events per turn. Values are rendered as
  JSON, strings included, so tool output stays on one line and cannot
  pose as a transcript turn; the block is labelled as tool data. An id of
  0 and string error messages are kept, and a turn with no tool call or
  result adds no block.
- Tool events are left out of the history passed to targets, and provider
  messages are reduced to role and content in one place.
- end_scenario now runs after the evidence resolver, so a resolver can
  still read the target's session, and a failing end_scenario raises a
  RuntimeWarning instead of logging only with verbose=True.
- Hooks are looked up on the target itself (proxies work); a hook defined
  without async is not called and warns.
- With an injected judge_client, an auditor of its own needs auditor_client
  or both auditor_model and auditor_provider, instead of falling back to
  the judge's labels.
- reframing_check rejects transforms that change tool_events.
- README: the hook rules as implemented, and judge_client/auditor_client
  in the parameter table.
A turn with more than 50 tool events now shows the first 25 and the last 25
with a count of the ones in between. An agent's last actions are often the
decisive ones, and cutting the tail hid them from the judge.
- Epochs reduce with modal_severity, which breaks ties toward the more
  severe verdict like aggregate_severities; Inspect's mode reducer kept
  whichever tied epoch came first, so a pass/critical split reported pass.
- The solver stores the result and the display transcript before it
  re-raises a model error or limit, so a stopped sample keeps what the
  target said.
- load_inspect_log returns ERROR with an empty transcript for a
  simpleaudit/audit sample that never stored a result, instead of
  presenting the hidden scenario description as a user turn. Samples are
  recognised by a simpleaudit_pack metadata key.
- Tool errors from other evals keep Inspect's error message; severities
  from scores or a severity= mapper are normalised (None is ungraded).
- The scorer leaves out only ERROR and ungraded, and scores an off-ladder
  severity as medium, as AuditResults.score does. severity_counts sends a
  value between two steps to the more severe one.
- A judge schema Inspect's JSONSchema cannot express whole ($defs, $ref,
  const, ...) is not sent, with a warning, instead of being sent stripped.
- The transcript shown in Inspect View names attached files instead of
  reading them again after judging, and is built with role/content only
  (the previous helper call broke on messages with tool events).
- Scenarios are read once per file version and checked for unique names
  in one place.
- examples/inspect_log_robustness.py checks --epoch.
- Input tokens include Anthropic-style cached prompt tokens
  (cache_read/cache_creation), which Claude Code reports apart from
  input_tokens.
- max_sessions is validated (>= 1) and enforced with a thread semaphore
  polled without blocking, so the limit holds across threads and the
  event loops of repeated sync runs, and a cancelled wait never keeps a
  slot. The per-loop asyncio semaphore is gone.
- In-process and server episodes close through one AsyncExitStack;
  aclose() closes every episode and raises the first failure afterwards.
- message_timeout_s bounds an in-process turn as well (asyncio.wait_for).
- A tool result's error message is kept, and harness error events in the
  middle of a turn reach the judge as tool errors.
- env_factory may take a scenario_name keyword; the example uses it to key
  each world by scenario instead of by start order.
- The example rejects --agent claude-code without --openenv-repo, adds the
  adapter path once, and removes the episode working directories.
- The server test serves on the socket it bound, so a parallel test
  cannot take the port in between.
- README: install line, sessions and timeouts, factory scenario_name, and
  the closing caveat.
Follow-up to the history fix, from review:

- Only the engine's markers (documents, file_uri, decision, tool_events)
  are removed from history; other fields stay, so direct callers sending
  OpenAI tool messages (tool_calls, tool_call_id, name) or turns without
  content get the body they built.
- Documents go through parse_documents like the model and decision
  targets: a dict without a string text fails, empty documents keep their
  position, and an explicit documents=[] is not replaced by the history
  marker. The error no longer assumes a context-grounding run.
- A scenario with file_uri now fails with an error; the app could never
  receive the file, so it was dropped silently.
- Target.send documents the history contract, and history_documents()
  in targets/base.py replaces the copies in http.py and decision.py.
Core seams for integrations: target lifecycle hooks, tool events, injected clients
Inspect AI integration: run packs as Inspect tasks, read Inspect logs back
HTTPAppTarget: send each turn once and no transcript markers (fixes #124)
# Conflicts:
#	.github/workflows/tests.yml
#	README.md
OpenEnvTarget: audit agents hosted in OpenEnv environments
- CallableTarget gives the function the history without the engine's
  markers, the scenario's documents as text through `documents`, and its
  files through `file_uri`, so no evaluation-only document mark reaches
  it (#126).
- HTTPAppTarget sends the audit's system prompt: as a leading system
  message in "messages" mode, after which a `messages` list from
  request_template comes before the conversation; in other modes through
  the new `system_field`, with a warning when there is none (#127).
- A `history` list in HTTPAppTarget's request_template comes before the
  conversation's earlier turns instead of replacing them (#128).
- targets/base.py gains ENGINE_MARKERS, plain_history() and
  history_file_uri(), shared by both targets.

Fixes #126
Fixes #127
Fixes #128
- system_field is honoured in "messages" mode too, so APIs that take the
  system prompt as a top-level field (Anthropic's native Messages API)
  work. A value request_template already sets at that field comes first;
  content-block lists get the prompt appended as a text block.
- Without system_field, the audit's prompt joins the template's leading
  system message instead of adding a second one; many chat templates
  reject two system messages in a row.
- The missing-system_field warning fires on every call when warnings are
  errors, instead of failing once and then dropping the prompt silently.
- A non-list "history" in request_template is an error instead of being
  replaced by the conversation.
- CallableTarget passes file_uri on the turn that attaches it only, and
  that turn keeps its file_uri in the history, so a function can attach
  the files where they belong.
- context_marks.document_texts() replaces the copies in the callable and
  HTTP targets; DecisionTarget treats an explicit documents=[] as given;
  OpenEnvTarget uses the shared history helpers. Stale comments and the
  README are updated.
Targets: clean history and documents for CallableTarget, system prompt and template history for HTTPAppTarget

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant