The prior tests exercised verify_finding directly but not its call site. Add
integration tests that drive the real _do_create (dedupe stubbed to
not-duplicate, verify_finding stubbed) against a live ReportState and assert
the wiring: a reject verdict returns success=False and persists nothing; an
emit verdict persists exactly one report. This closes the gap between "the
verifier works" and "the reporting path actually calls it and honours the
verdict".
Two issues from review:
- `_verify_extra_args` overlaid VERIFY_LLM_API_KEY/_API_BASE even when no
dedicated STRIX_VERIFY_MODEL was set, pointing the fallback main-model request
at the wrong endpoint/account. Gate on `verify.model`, mirroring dedupe.
- `_parse_verdict` accepted an out-of-range/non-finite confidence (100, 1e999,
nan) from the untrusted model output, which could clear the suppression
threshold and drop a finding. Clamp to [0, 1]; anything else collapses to 0.0
(fails the gate, emits) — preserving the fail-open contract.
Adds tests for both (including confidence=100 not suppressing end-to-end).
Adds `STRIX_VERIFY`: a per-finding adjudication step that runs just before a
report is persisted — a sibling of the existing dedupe reject in
`_do_create`. A (typically cheaper) model re-derives whether the candidate is
REAL or a FALSE_POSITIVE, and a confident false positive is dropped before it
reaches the report state.
The pass is off by default and deliberately fail-open + asymmetric, so it can
never lose a real finding:
- only a FALSE_POSITIVE verdict at/above STRIX_VERIFY_MIN_CONFIDENCE (default
0.8) suppresses; REAL, uncertain, below-severity, unparseable, no-model, or
any error all EMIT
- only findings at/above STRIX_VERIFY_MIN_SEVERITY (default high) are verified
- raising min_confidence only makes it more conservative (emits more)
Mirrors the dedupe module's shape (StrixProvider.get_model + get_response, JSON
verdict, usage recorded via record_sdk_usage) and reuses its settings pattern
(dedicated STRIX_VERIFY_MODEL with its own creds/headers, falling back to the
main model). No new dependencies.
17 unit tests cover the severity gate, verdict parsing, the confidence
threshold in both directions, and the fail-open paths; existing reporting and
dedupe suites still pass.
* fix(agents): collapse repeated waits queued inside one model turn
* fix(agents): state that one wait is enough in every prompt variant
---------
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
* fix(llm): cap the tool calls one assistant response may queue
* fix(llm): cap the subscription backend's responses too
---------
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
An exception escaping a non-interactive cycle re-raised before the status
handling, so a dying child stayed 'running' and its parent waited out the
timeout on a completion report the child could no longer send. Set the
terminal status and wake the parent on the way out too.
A tool call for a name Strix does not register raised ModelBehaviorError
from the SDK turn resolver, which nothing retries: the root agent's raise
tore down the whole scan and a sub-agent died before its status was set.
Opt into the SDK's tool_not_found_behavior="return_error_to_model" so the
unknown call comes back as a tool result and the agent self-corrects.
The setting landed in openai-agents 0.19.0, which requires openai>=2.45,
so both pins move.
* perf(cli): fast startup — lazy heavy imports + onedir standalone build
* perf(cli): drop legacy single-file compat from install/self-update
* perf(cli): simplify — drop constants module and extra lazy-import refactors
* refactor(update): strix --update just re-runs the install script
* perf(cli): drop packaging/install/update changes; deepen lazy imports instead
Reverts the onedir build, install.sh, and self-update changes so release
mechanics stay untouched. Startup cost is addressed purely by deferring
heavy imports (agents/openai, config.models, report state/writer, docker)
until a scan actually runs; DEFAULT_MAX_TURNS moves to strix.config.settings
so argparse no longer pulls the agents SDK.
---------
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
One tool was doing three jobs (wait on the user, wait on other agents, and
- wrongly - wait for a long-running command), so the driver had to guess which
one an agent meant and used parent_id as the proxy: the root waits for a human,
everyone else waits for agents. That proxy is wrong, since the user can message
any agent from the TUI's agent tree.
Tool identity now carries the intent, and the coordinator records it as a
wait_kind that survives snapshot/restore:
respond_to_user -> wait_kind="user", never auto-resumed (root or not)
wait_for_agents -> wait_kind="agents", auto-resumed on a 300s timer
recovery exhaust -> wait_kind="stalled"
respond_to_user fuses the message and the yield into one call, so there is no
way to answer and then forget to stop - the two-step that gpt-4o-mini skipped
2/2 in live testing. Plain text still renders as before.
Auto-resume is also bounded now: an agent that re-parks after every timeout
burned a model turn every 300s for the rest of the scan (and, since parked
children notify their parent, spammed the parent's inbox on the same cycle).
After _MAX_IDLE_AUTO_RESUMES it stays parked until a real message arrives.
The user can message any agent from the TUI, not only the root, so the
justification is that the parent is an agent with no other way to learn
the child parked - not that the child has no human resumer.
Parking is self-service only for the root, which the user is watching.
A parked child owes its parent a report it can no longer send, so the
parent would wait out its full timeout for nothing.
A mutual wait between two agents resolves only when both hit their cap,
so the ceiling is the worst-case idle burn. Name the constants instead of
repeating the literal, and align the interactive auto-resume timeout.
An exhausted agent parked in 'waiting' got a fresh nudge budget on every
600s auto-resume, so a wedged agent could nudge-park-nudge indefinitely.
Track the count on the coordinator, snapshot it, and reset it only on
real input or an explicit lifecycle tool.
Interactive turns ended by plain text left the agent parked in 'waiting'
forever. Require an explicit lifecycle tool in both modes and nudge a
text-only turn back into a tool call, bounded by a recovery limit.
LiteLLM treats provider-qualified metadata lookups as an auth path.
Use the underlying model slug so context sizing cannot block the scan
loop in a device-code poll.
Adds an integration test that drives Runner.run_streamed against a
non-streaming gateway through _NonStreamingModel: the synthetic terminal
event feeds the runner, which executes the tool call and continues to a
final answer over two non-streaming turns. Removes the README env-var note.