Target Interface

The AI system under test. Exposes five surfaces and a lifecycle.

Manual values (constructor)

API keys, credentials, and other user-provided secrets are passed directly to the target’s constructor — not through the framework. This keeps the Target ABC clean and makes instantiation explicit:

target = MyDockerTarget(api_key="sk-...", image="my-app:latest")

Pre-run configuration (task-set)

Used by tasks to set up initial state. The description on each ConfigSpec documents the accepted format — that is the contract between task and target.

Post-run queries (evaluator uses)

Config and query are intentionally distinct:

Security domain

Runtime surfaces

Execution

EventResponseHandler = Callable[[Event], Awaitable[EventResponse]] — the send_event callback type. The controller wraps it to bridge to the EventChannel with security domain filtering. The target doesn’t know or care what’s on the other end.

Internal parallelism

The target can have concurrent branches, each calling send_event independently:

async def run(self, emit, send_event):
    async def branch_a():
        resp = await send_event(event_a)  # suspends only this branch
        ...
    async def branch_b():
        resp = await send_event(event_b)  # suspends only this branch
        ...
    await asyncio.gather(branch_a(), branch_b())

Each send_event call creates its own future in the channel. Multiple events can be in-flight simultaneously. The optimizer processes them at its own pace.

For thread-based targets (Docker, subprocesses), bridge back to the event loop:

async def run(self, emit, send_event):
    loop = asyncio.get_running_loop()
    def blocking_work():
        event = parse_event_from_subprocess(proc)
        future = asyncio.run_coroutine_threadsafe(send_event(event), loop)
        response = future.result()  # blocks thread until response
        ...
    await loop.run_in_executor(None, blocking_work)

Design decisions