omnigent-ai--omnigent
422 行
21 KiB
Markdown
422 行
21 KiB
Markdown
# Harness test bench: a standardized capability conformance suite
|
|
|
|
A pluggable bench that, given a harness, empirically reports its verdict on
|
|
every capability dimension in the harness support matrix — "is model switching
|
|
available", "is steering possible", "does policy DENY actually block a call" —
|
|
instead of a human hand-maintaining a spreadsheet and hoping it still reflects
|
|
reality.
|
|
|
|
> **Status:** shipped and in use. The bench on `main` has three transport
|
|
> drivers, six P0 probes, three report-only P1 probes, automatic live/offline
|
|
> selection, and a capability-derived matrix that has already caught and
|
|
> corrected real declaration drift. See
|
|
> [Current state](#current-state-shipped) for what is live vs. still open. The
|
|
> sections before it describe the design and the decisions behind it.
|
|
|
|
## Motivation
|
|
|
|
We maintain a capability matrix by hand (the native + SDK support
|
|
spreadsheet). It drifts the moment a harness changes and nobody re-tests every
|
|
cell. Worse, there are already **three disagreeing sources of truth** for any
|
|
given capability:
|
|
|
|
1. **The spreadsheet** — what a human believed at some point.
|
|
2. **The `Executor` capability flags** — `supports_streaming()`,
|
|
`supports_live_message_queue()`, `supports_tool_boundary_interrupt()`,
|
|
`supports_stepwise_internal_turns()`, `handles_tools_internally()` in
|
|
`omnigent/inner/executor.py:541`.
|
|
3. **`omnigent/model_override.py`** — already encodes per-harness facts
|
|
declaratively (`_SDK_MODEL_OVERRIDE_HARNESSES`, the `_*_FAMILY_HARNESSES`
|
|
sets, single- vs multi-model rules).
|
|
|
|
The bench turns the matrix into an **executable conformance suite** that earns
|
|
each cell by running a live turn and inspecting the event stream, and then
|
|
**reconciles observed behavior against the declared flags** — so a flag that
|
|
says `✓` but behaves `✗` becomes a test failure (a `DRIFT` verdict), not a
|
|
production surprise.
|
|
|
|
## Goals
|
|
|
|
- One command produces the support matrix for a harness, with a verdict per
|
|
dimension.
|
|
- Adding a new *official* harness needs at most a one-line registry entry plus a
|
|
self-declared profile — never per-probe code.
|
|
- A *community* / out-of-repo harness that ships a bench profile can be probed
|
|
with `--harness <name>` and no bench edits.
|
|
- Detect drift between what a harness *declares* and what it *does*.
|
|
|
|
## Non-goals
|
|
|
|
- Not a replacement for the existing per-harness e2e tests (those assert
|
|
specific behaviors deeply; the bench asserts breadth across dimensions).
|
|
- Not a performance/latency benchmark. "Bench" here means conformance, not
|
|
throughput.
|
|
- The bench does not invent model ids, credentials, or transports. Facts it
|
|
cannot infer must be self-declared by the harness (see `BenchProfile`).
|
|
|
|
## Key constraint: registration is a hardcoded dict today
|
|
|
|
Harnesses register via a literal `_HARNESS_MODULES: dict[str, str]` mapping
|
|
harness name to module path in `omnigent/runtime/harnesses/__init__.py:34`.
|
|
There is **no entry-point / plugin discovery** mechanism. An out-of-repo harness
|
|
cannot even register without editing that file — the same shared-file conflict
|
|
pain tracked in #899, whose proposed fix was per-harness self-registration.
|
|
|
|
This constraint is what shapes the coupling decision below. It is *not* a limit
|
|
on what the bench can probe: the probes are harness-agnostic. It is only a limit
|
|
on how a harness gets *discovered*.
|
|
|
|
> **Update since this was written:** entry-point plugin discovery now exists —
|
|
> `harness_capabilities()` merges contributions from the
|
|
> `omnigent.community.harness` entry-point group, and the bench derives
|
|
> everything from it. So the bench side of option B is realized: a plugin's
|
|
> harness flows in with no bench edit. The remaining hardcoded seam is *not*
|
|
> here — it is the server's native-agent seeding (see "Plugin seamlessness").
|
|
|
|
## Decision: option B (registry-indexed now, profile-driven from day one)
|
|
|
|
Two coupling options were considered:
|
|
|
|
- **(A)** Build the bench on dynamic discovery (entry points / plugin registry)
|
|
now. True plug-and-play, but partly blocked on self-registration (#899) before
|
|
out-of-repo harnesses can register at all.
|
|
- **(B)** Index official harnesses from the current hardcoded registry now
|
|
(one registry line + a profile per new harness), and swap enumeration to
|
|
dynamic discovery when self-registration lands.
|
|
|
|
**We chose B**, with the critical rule that **all per-harness facts live on a
|
|
self-declared `BenchProfile` from day one** — never in bench or probe code. The
|
|
hardcoded list is just a convenience index of the official harnesses; it is not
|
|
a gate on what the bench can probe.
|
|
|
|
This satisfies both use cases:
|
|
|
|
- **Official harnesses** — already in `_HARNESS_MODULES`; the bench iterates the
|
|
list and `--harness <name>` selects one.
|
|
- **Community harnesses** — `--harness <name>` resolution falls back (when the
|
|
name is not in the registry) to any harness that exposes a `BenchProfile`
|
|
(e.g. `--harness mypkg.myharness`, or a name resolved via an installed
|
|
plugin). A ~10-line resolution shim, not the full discovery system.
|
|
|
|
The day self-registration lands, only the *enumeration* changes from "read the
|
|
list" to "discover"; probes, profiles, and reports are untouched.
|
|
|
|
## What is free vs. what a harness must provide
|
|
|
|
**Free (no per-harness code):**
|
|
|
|
- Every **behavioral probe** — it creates a session, runs a turn, and inspects
|
|
the generic event stream (`TextChunk`, `ReasoningChunk`, `ToolCallRequest`,
|
|
`TurnComplete`, elicitation events). It never names a harness. Any harness the
|
|
bench can launch is probed on every dimension; a dimension with no probe yet
|
|
reports `UNKNOWN`.
|
|
- **Declared-flag reconciliation** — reads the `Executor` capability methods
|
|
that every harness inherits from the base class.
|
|
|
|
**Must be self-declared on `BenchProfile` (the bench cannot infer these):**
|
|
|
|
- A **test model** (or model family) — a probe cannot invent a valid model id.
|
|
- The **CLI binary** to skip-gate on (for subprocess / native harnesses).
|
|
- The **transport class** (see transport drivers below).
|
|
- The **static columns** the matrix records but cannot verify: Owner, Auth
|
|
method, Implementation, "inherits preexisting configs", priority tier.
|
|
|
|
**Derived by convention (not hand-authored):** `env_prefix`
|
|
(`HARNESS_<NAME>_`), `marker` (`<NAME>_BENCH_OK`).
|
|
|
|
## Architecture
|
|
|
|
The implementation has three layers plus reporting:
|
|
|
|
```
|
|
tests/harness_bench/
|
|
profile.py # BenchProfile and profile-name resolution
|
|
manifest.py # official profiles derived from capabilities + e2e metadata
|
|
verdict.py # verdict vocabulary, priority, and drift reconciliation
|
|
transport.py # semantic Driver protocol and transport resolution
|
|
driver.py # sdk-inproc driver + shared TurnResult/usage helpers
|
|
full_server.py # shared server/runner lifecycle and registration
|
|
full_server_driver.py # full-server driver and session polling
|
|
native_tui_driver.py # native vendor CLI + host-daemon/tmux driver
|
|
session_items.py # shared session-item envelope parsing
|
|
runtime_env.py # config/credential resolution matching `omni run`
|
|
probes/ # one module per capability dimension
|
|
events.py # structured progress events and plain sink
|
|
richreport.py # optional live Rich matrix
|
|
bench.py # orchestration, concurrency, and shared-server wiring
|
|
report.py # terminal, Markdown, and JSON rendering
|
|
```
|
|
|
|
Reusable configuration and runtime primitives live in production modules such
|
|
as `omnigent.config`, `find_free_port`, and the harness registry rather than
|
|
being reimplemented under tests.
|
|
|
|
- **Layer 0 — Profile / manifest.** Static facts and declared verdicts are
|
|
derived from `harness_capabilities()` plus the existing e2e harness metadata.
|
|
- **Layer 1 — Offline conformance.** No network or credentials. It validates
|
|
registration, profile shape, capability derivation, transport resolution,
|
|
rendering, and orchestration behavior in normal CI.
|
|
- **Layer 2 — Live probes.** Drivers execute behavioral probes through the
|
|
wrap boundary or the real server/runner session API. Missing credentials,
|
|
vendor binaries, or vendor login produce capability-neutral skips.
|
|
- **Report.** The CLI renders the declared matrix offline or reconciles live
|
|
observations into terminal, Markdown, and JSON reports. `DRIFT` produces a
|
|
non-zero exit status.
|
|
|
|
### Build on `HarnessProbe`, don't reinvent it
|
|
|
|
`tests/e2e/_harness_probes.py` already gives per-harness rows
|
|
(name, model, env_prefix, marker, cli_binary) and CLI-gating that every e2e test
|
|
parametrizes over. `BenchProfile` should extend / subsume that row so adding a
|
|
harness there flows into both the existing e2e suite and the bench.
|
|
|
|
## Verdict vocabulary
|
|
|
|
Maps directly to the spreadsheet glyphs, plus two operational states and the
|
|
drift alarm.
|
|
|
|
| Verdict | Glyph | Meaning |
|
|
|---|---|---|
|
|
| `SUPPORTED` | ✓ | probe ran, behavior confirmed |
|
|
| `UNSUPPORTED` | ✗ | probe ran, capability absent (and expected absent) |
|
|
| `PARTIAL` | ~ | works with caveats (e.g. "TUI-only", "hook-DENY only") |
|
|
| `NOT_APPLICABLE` | — | dimension does not apply (e.g. model override on agy self-select) |
|
|
| `UNKNOWN` | ? | never probed / no probe written yet |
|
|
| `SKIPPED` | | CLI / creds / transport unavailable in this environment |
|
|
| `DRIFT` | !! | observed verdict disagrees with the declared flag / manifest |
|
|
|
|
Each dimension also carries a `P0` / `P1` priority (from the spreadsheet) so CI
|
|
can gate on P0 and merely report P1.
|
|
|
|
## Dimension catalog
|
|
|
|
Two classes.
|
|
|
|
### Static / declared (recorded, not probed)
|
|
|
|
Validated for presence and shape only: `Owner`, `Transport`, `Implementation`,
|
|
`Auth` method, `Inherits preexisting configs`.
|
|
|
|
### Behavioral (proven by a live turn)
|
|
|
|
| Dimension | How the probe proves it |
|
|
|---|---|
|
|
| Basic turn (P0 prerequisite) | complete a marker-echo turn and require assistant text |
|
|
| Streaming (P0) | count output-text deltas; repeated single-delta output is `PARTIAL` |
|
|
| Tool calling (P0) | provoke the transport's tool mechanism and require a surfaced call |
|
|
| Policy DENY (P0) | apply a tool-call deny and require a blocked-call signal |
|
|
| Policy ALLOW (P1) | attach an explicit allow and require a non-blocked tool output; native hooks expose no positive ALLOW event |
|
|
| Policy ASK (P1) | apply ask and require an elicitation/approval request |
|
|
| Model override (P0) | validate the requested harness/model pair and complete a turn |
|
|
| Cost tracking (P1) | read priced cost or token usage from the turn/session |
|
|
| Interrupt (P0) | interrupt a long turn and require cancellation or early termination |
|
|
|
|
Planned dimensions are steering, live queue, resume/fork, reasoning, images,
|
|
and compaction.
|
|
|
|
Every behavioral probe also reads the corresponding declared flag and returns
|
|
`DRIFT` when observed disagrees with declared.
|
|
|
|
### Illustrative probe shape
|
|
|
|
```python
|
|
class StreamingProbe(CapabilityProbe):
|
|
name = "streaming"
|
|
priority = P0
|
|
applies_to = BOTH
|
|
|
|
def declared(self, profile) -> Verdict:
|
|
return SUPPORTED if executor_of(profile).supports_streaming() else UNSUPPORTED
|
|
|
|
async def run(self, driver, profile) -> ProbeResult:
|
|
deltas = await driver.count_text_chunks("Write a 3-sentence story.")
|
|
observed = SUPPORTED if deltas > 1 else PARTIAL # "complete-only"
|
|
return ProbeResult(
|
|
observed,
|
|
note=f"{deltas} text chunks",
|
|
drift=reconcile(observed, self.declared(profile)),
|
|
)
|
|
```
|
|
|
|
## Transport drivers: the real ceiling on "all dimensions"
|
|
|
|
Behavioral probes call semantic driver methods such as `run_basic_turn`,
|
|
`run_tool_turn`, `run_policy_turn`, and `run_interrupt_turn`. Drivers own the
|
|
transport-specific mechanism; probes interpret a common `TurnResult`.
|
|
|
|
Three drivers exist:
|
|
|
|
- `full-server` is the SDK-family default. It drives a real server and runner,
|
|
uses a server-dispatched builtin for tool probes, and observes fixed
|
|
ALLOW/ASK/DENY policies.
|
|
- `native-tui` drives a resident vendor CLI in a runner-owned tmux pane through
|
|
the server session API. It observes vendor tool calls and tool-call DENY via
|
|
the native policy hook. ALLOW/ASK are not yet implemented.
|
|
- `sdk-inproc` drives the harness wrap directly. It is selected by `--fast` and
|
|
provides cheaper wrap-level coverage, but no server-side policy surface.
|
|
|
|
A `SKIPPED` verdict therefore means the behavior was not measurable in that
|
|
transport or environment, not that the harness lacks the capability. A novel
|
|
transport class still requires a driver, but harnesses reusing one of these
|
|
families flow through the existing probes without per-harness probe code.
|
|
|
|
## Current state (shipped)
|
|
|
|
The bench on `main` includes:
|
|
|
|
- **Six P0 probes:** Basic turn, Streaming, Tool calling, Policy DENY, Model
|
|
override, and Interrupt.
|
|
- **Three P1 probes:** Policy ALLOW, Policy ASK, and Cost tracking. P1 verdicts
|
|
are report-only and do not gate the same way as P0 declarations.
|
|
- **Three transport drivers:** `full-server`, `native-tui`, and `sdk-inproc`,
|
|
selected by harness family with `--transport` and `--fast` overrides.
|
|
- **Automatic live selection:** without an explicit mode, the CLI runs live
|
|
when credentials are resolvable and otherwise renders the declared matrix.
|
|
`--live` and `--no-live` force either mode. Credentials are derived like
|
|
`omni run`; `--profile` is only an override.
|
|
- **Concurrent execution and shared infrastructure:** `--jobs` runs harnesses
|
|
concurrently while preserving report order, and full-server harnesses share
|
|
one server/runner pair within a run.
|
|
- **Structured progress and reports:** plain or Rich live progress plus terminal,
|
|
Markdown, JSON, and optional report-file output.
|
|
- **Capability-derived registration:** official SDK and native profiles derive
|
|
from `harness_capabilities()` and existing e2e metadata. Session-item parsing,
|
|
config loading, free-port selection, and polling helpers are shared rather
|
|
than duplicated.
|
|
|
|
### Not yet wired
|
|
|
|
- Registry-driven server seeding for community native UI agents.
|
|
- Steering, live queue, resume/fork, reasoning, images, and compaction probes.
|
|
- Automatic provisioning of vendor login/provider configuration for native
|
|
harnesses; unavailable environments skip cleanly.
|
|
|
|
## CI integration
|
|
|
|
- **Every PR:** Layer 1 offline conformance (fast, no network, no creds).
|
|
- **Nightly / on-demand:** Layer 2 live probes (real API cost + flake surface),
|
|
gated on CLI + creds, P0 blocking, P1 report-only. Follows the existing
|
|
nightly/flake-stress pattern rather than blocking every PR on live turns.
|
|
|
|
## Running the bench and reading the result
|
|
|
|
```
|
|
# Declared matrix only, with no credentials.
|
|
python -m tests.harness_bench --no-live
|
|
|
|
# Auto-live when configured or ambient credentials are available.
|
|
python -m tests.harness_bench --harness codex
|
|
|
|
# Force a named profile and probe several harnesses concurrently.
|
|
python -m tests.harness_bench --profile oss --jobs 4 --rich
|
|
|
|
# A community harness that ships its own BenchProfile.
|
|
python -m tests.harness_bench --harness mypkg.harness:PROFILE --live
|
|
```
|
|
|
|
Without `--live` or `--no-live`, resolvable credentials select live mode and
|
|
missing credentials select the offline declared matrix. Native harnesses also
|
|
need their vendor CLI installed and logged in; the bench cannot provision those
|
|
accounts, so unavailable harnesses skip without aborting the run.
|
|
|
|
Offline conformance covers every registered harness in CI. Live runs are
|
|
spot-checks of observed behavior and can vary with model behavior and timing;
|
|
re-run an isolated timeout or skip before treating it as a regression. The
|
|
signals that matter most are `DRIFT` and repeatable unexpected
|
|
`UNSUPPORTED`/`PARTIAL` verdicts on a runnable harness.
|
|
|
|
## Streaming is a binary declared capability
|
|
|
|
A recurring subtlety worth stating: the `streaming` capability is **binary** —
|
|
a harness either forwards token-level deltas (`SUPPORTED`) or it does not
|
|
(`UNSUPPORTED`). `PARTIAL` is a *probe observation only*: the streaming probe
|
|
returns it for the ambiguous coalesced-single-delta case against a `SUPPORTED`
|
|
declaration. It is **never a declared value**. Declaring a non-streaming
|
|
harness as `PARTIAL` drifts against reality, because the probe reports zero
|
|
deltas as `UNSUPPORTED`, not `PARTIAL`.
|
|
|
|
**Declare `streaming=False` only from a live observation of 0 deltas** — a
|
|
static "the forwarder posts no delta" grep is *not* sufficient. That grep once
|
|
flipped seven natives to `False` in one batch; a live run then showed
|
|
pi-native streams (7 deltas) despite having no delta-posting forwarder, so the
|
|
flip was reverted. Only three natives are declared non-streaming today, each
|
|
live-verified at 0 deltas: **kiro-native, cursor-native, qwen-native**. The
|
|
rest default to `streaming=True` (the honest default: if one turns out not to
|
|
stream, the bench flags a real drift on the next run, rather than a false
|
|
`False` that silently drifts the moment the harness *does* stream).
|
|
|
|
## Which transport exercises which dimension
|
|
|
|
| Dimension | `sdk-inproc` (`--fast`) | `full-server` (SDK default) | `native-tui` |
|
|
|---|---|---|---|
|
|
| Basic turn, Streaming, Model override, Interrupt | Wrap-level observation | End-to-end server/runner observation | End-to-end server/runner/vendor observation |
|
|
| Tool calling | Request-level wrap tool | Server-dispatched builtin | Vendor tool mirrored into session items |
|
|
| Policy DENY | Not observable | Fixed policy blocks the builtin | Session CEL policy triggers the native policy hook |
|
|
| Policy ALLOW / ASK | Not observable | Fixed policy; ASK observes and resolves an elicitation | Temporary session CEL policy; ASK observes and resolves an elicitation |
|
|
| Cost tracking | Completed-response usage when forwarded | Session snapshot usage/cost | Session snapshot when the vendor forwards usage |
|
|
|
|
`full-server` remains the SDK default because it covers the deployed server
|
|
path and all three policy actions. `--fast` trades that policy coverage for
|
|
lower startup cost. `native-tui` now has real Tool calling and all three policy
|
|
action probes through the native hook path.
|
|
|
|
## Plugin seamlessness: where it is and isn't
|
|
|
|
The original goal (option B) was that a *community* harness ships a
|
|
`BenchProfile` and runs with `--harness <name>` and no bench edits. For the
|
|
**bench itself, that holds**: profile resolution, capability derivation, and
|
|
`native_vendor()` all read `harness_capabilities()`, which discovers community
|
|
plugins via entry points. A plugged-in harness needs zero bench code to be
|
|
recognized.
|
|
|
|
The seam is **one level down, in the omnigent server**. A native harness is
|
|
only drivable once the server has seeded a built-in `<harness>-native-ui`
|
|
agent, and that seeding is a **hardcoded list** in
|
|
`server/app.py:_ensure_default_agents` — one `_ensure_default_<harness>_agent()`
|
|
call per harness. goose-native and hermes-native were in the capability
|
|
registry but omitted from that list, so the bench (correctly) reported them
|
|
`not auto-registered on the server` until the seeders were added.
|
|
|
|
So: **the bench is plugin-seamless; the server's native-agent seeding is not,
|
|
and the bench inherits that seam.** A community native plugin today resolves in
|
|
the bench, then fails at registration because nothing seeds its UI agent. The
|
|
clean fix is to make `_ensure_default_agents` iterate `native_agents()` from
|
|
the registry (which already includes plugins) instead of a hardcoded call list
|
|
— then native harnesses and plugins register automatically. This is the highest
|
|
-leverage remaining item: it is the difference between "the bench is plugin-
|
|
ready" and "a plugged-in native harness works end to end".
|
|
|
|
## The self-enforcing table in practice (drift case studies)
|
|
|
|
`reconcile()` turns a false capability declaration into a `DRIFT`. This is not
|
|
theoretical — the bench caught several real declaration errors this way, each
|
|
resolved by correcting the *source* (the capability model), not the bench:
|
|
|
|
- **kiro-native / streaming.** Declared `SUPPORTED`, observed 0 deltas
|
|
(`!!✓>✗`). kiro mirrors each complete assistant message rather than streaming
|
|
tokens. Corrected to `streaming=False`.
|
|
- **pi-native / streaming (a fixed over-correction).** A static grep had flipped
|
|
pi to `False`; a live run showed it streams 7 deltas (`!!✗>✓`) despite having
|
|
no delta-posting forwarder. Reverted to `True`. This is why the rule is
|
|
"declare `False` only from a live 0-delta observation" — the grep lied.
|
|
- **cursor-native / streaming + provisioning.** cursor could not provision at
|
|
all until the `lazy_chat` fix (its `external_session_id` is created by the
|
|
first message, not at launch, so gating on it pre-turn deadlocked). Once
|
|
runnable, it observed 0 deltas → `streaming=False`.
|
|
- **qwen-native / streaming.** Observed 0 deltas → `streaming=False`.
|
|
|
|
The pattern each time: the bench detects the mismatch, a live probe pins which
|
|
side is wrong, and the capability model is corrected — not the bench massaged to
|
|
agree with it.
|
|
|
|
## Open items
|
|
|
|
- **Registry-driven native-agent seeding** — replace the hardcoded server
|
|
seeding list with registry iteration so community native harnesses work end
|
|
to end after plugin installation.
|
|
- **Per-harness native provisioning** — some vendors require login or provider
|
|
configuration that the bench deliberately cannot create. Improve diagnostics
|
|
where possible while retaining clean skips.
|
|
- **Additional dimensions** — steering, live queue, resume/fork, reasoning,
|
|
images, and compaction.
|