faultgate 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- faultgate-0.3.0/CHANGELOG.md +40 -0
- {faultgate-0.2.0/src/faultgate.egg-info → faultgate-0.3.0}/PKG-INFO +140 -61
- {faultgate-0.2.0 → faultgate-0.3.0}/README.md +135 -59
- faultgate-0.3.0/examples/formats/braintrust.json +19 -0
- faultgate-0.3.0/examples/formats/langfuse.json +17 -0
- faultgate-0.3.0/examples/formats/langsmith.json +16 -0
- faultgate-0.3.0/examples/formats/openinference.json +30 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/examples/gate/band.json +6 -1
- faultgate-0.3.0/examples/policy_support.json +12 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/pyproject.toml +3 -2
- {faultgate-0.2.0 → faultgate-0.3.0}/src/faultgate/__init__.py +1 -1
- faultgate-0.3.0/src/faultgate/band.py +157 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/src/faultgate/checks.py +226 -5
- {faultgate-0.2.0 → faultgate-0.3.0}/src/faultgate/cli.py +78 -10
- faultgate-0.3.0/src/faultgate/formats.py +405 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/src/faultgate/judge.py +1 -0
- faultgate-0.3.0/src/faultgate/precheck.py +87 -0
- faultgate-0.3.0/src/faultgate/redact.py +124 -0
- faultgate-0.3.0/src/faultgate/report.py +140 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/src/faultgate/runner.py +32 -6
- {faultgate-0.2.0 → faultgate-0.3.0}/src/faultgate/trace.py +33 -0
- {faultgate-0.2.0 → faultgate-0.3.0/src/faultgate.egg-info}/PKG-INFO +140 -61
- {faultgate-0.2.0 → faultgate-0.3.0}/src/faultgate.egg-info/SOURCES.txt +12 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/src/faultgate.egg-info/requires.txt +4 -0
- faultgate-0.3.0/tests/test_band.py +173 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/tests/test_checks.py +85 -1
- {faultgate-0.2.0 → faultgate-0.3.0}/tests/test_cli.py +18 -0
- faultgate-0.3.0/tests/test_formats.py +245 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/tests/test_golden.py +1 -0
- faultgate-0.3.0/tests/test_policy.py +234 -0
- faultgate-0.3.0/tests/test_redact.py +179 -0
- faultgate-0.3.0/tests/test_report.py +257 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/tests/test_runner.py +3 -2
- faultgate-0.2.0/CHANGELOG.md +0 -26
- faultgate-0.2.0/src/faultgate/band.py +0 -78
- faultgate-0.2.0/tests/test_band.py +0 -79
- faultgate-0.2.0/tests/test_policy.py +0 -68
- {faultgate-0.2.0 → faultgate-0.3.0}/LICENSE +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/MANIFEST.in +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/examples/gate/baseline_glm_run1.json +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/examples/gate/baseline_glm_run2.json +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/examples/gate/candidate_gpt.json +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/examples/gate/candidate_qwen.json +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/examples/gate/golden.json +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/examples/gate/policy.json +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/examples/golden.json +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/examples/traces.json +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/setup.cfg +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/src/faultgate.egg-info/dependency_links.txt +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/src/faultgate.egg-info/entry_points.txt +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/src/faultgate.egg-info/top_level.txt +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/tests/__init__.py +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/tests/conftest.py +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/tests/test_judge.py +0 -0
- {faultgate-0.2.0 → faultgate-0.3.0}/tests/test_trace.py +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0 — 2026-10-12
|
|
4
|
+
|
|
5
|
+
- Policy business rules in `policy.json`: `limits` (numeric min/max on a tool argument, dotted paths, `"$1,200.50"` parsed), `identity` (argument must equal the root span's `enduser.id` or another `session_attr`), `requires` (tool order prerequisites, optionally on the earlier result). Each fails closed when it cannot be checked. Malformed keys exit 2.
|
|
6
|
+
- Hard block: with `--band`, any `policy` violation makes the gate FAIL regardless of clean rate; the gate result lists `policy_violations`.
|
|
7
|
+
- `examples/policy_support.json`: a customer-support policy using every key.
|
|
8
|
+
- New default rule `context_overflow`: the run hit the context window or output token limit (`gen_ai.response.finish_reasons` `length` / `max_tokens`, or a provider context-limit error in a span status message, `error.type`, tool result or `faultgate.termination`). 0 detections on the 125 shipped real traces and the 30-trace `make demo-real` sample. Spans now keep `status.message` and `endTimeUnixNano`. The default report gains a column.
|
|
9
|
+
- Trace formats: `--format auto|otlp|openinference|langfuse|langsmith|braintrust` on `check` and `baseline` (detected by default). Each vendor export is mapped onto the GenAI span model, carrying tool calls, errors, finish reasons, token usage, user id and scenario. Examples in `examples/formats/`.
|
|
10
|
+
- `faultgate precheck`: validates an export (format, roots, duplicate span ids, dangling parents, timestamps, unnamed tool calls, missing content) on the first 5 traces (`--sample`); exit 2 on errors.
|
|
11
|
+
- `Trace.usage`: token totals across spans.
|
|
12
|
+
- Redaction (`--redact builtin|presidio|off`, default `builtin`): PII and secrets are masked in the trace sent to an API judge and in every verdict reason (terminal, JSON report, CI). Rules still read raw values. `builtin` is stdlib regex (emails, Luhn-checked cards, SSNs, phones, IPs, API keys, bearer tokens, JWTs); `presidio` uses the new `[pii]` extra. Reports gain `redaction`.
|
|
13
|
+
- PR comment and audit files: `--markdown PATH` writes the verdict, gate numbers, fired checks, policy hard block, failing traces (step and reason), latency / token / cost change against the baseline, and a repro command; `--audit DIR` writes `verdict.json` (inputs with SHA-256), `failing_traces.json` and `repro.sh`. Neither copies trace payloads. Bands record mean `tokens`, `latency_ms`, `cost_usd` per run under `usage`, and the gate reports the change. `examples/gate/band.json` regenerated (same clean rates and verdicts, now with `usage`).
|
|
14
|
+
- GitHub Action: job summary, one upserted PR comment (`comment`, needs `pull-requests: write`), `faultgate-audit` artifact, `verdict` output, `format` / `redact` inputs; fails closed if the gate never ran.
|
|
15
|
+
- Gate statistics: **WARN is replaced by INCONCLUSIVE.** Between the PASS and FAIL thresholds the gate now runs a one-sided test against the baseline mean (binomial noise of both sides plus measured run-to-run variance); a significant drop (α 0.05) is FAIL, otherwise INCONCLUSIVE with `required_n` / `additional_n` for power 0.80. The gate result gains `stats` (`p_value`, `drop`, `baseline_mean`, `required_n`, `additional_n`). Bands from fewer than 3 baseline runs warn. Existing band.json files keep working. If you parse `verdict == "WARN"`, switch to `"INCONCLUSIVE"`.
|
|
16
|
+
|
|
17
|
+
## 0.2.0 — 2026-09-29
|
|
18
|
+
|
|
19
|
+
- Chat-only trace support: tool calls embedded inside message parts (paired with tool responses by ID), in addition to `execute_tool` spans.
|
|
20
|
+
- New rule checks: `repeated_call` (same tool + normalized arguments issued >= 3 times with identical result, κ 0.95 vs a blind second rater (Claude Opus 5.5) on 97 real runs) and `unrecovered_tool_error` (structural tool error with no later success, or a span error (error.type or status code 2) with no later successful tool call; recall 0.57, κ 0.54), calibrated against FAULTLINE P18 on Exgentic real benchmark traces.
|
|
21
|
+
- `scripts/fetch_real_traces.py` and `make demo-real`: sample real traces across 6 benchmarks from `Exgentic/agent-llm-traces-v2` and evaluate with default rule checks.
|
|
22
|
+
- `demo-real` works from an empty cache via `snapshot_download`, carries span status codes into OTLP, and adds `demo` optional dependency group.
|
|
23
|
+
- Hybrid traces preserve message-embedded tool calls deduplicated against `execute_tool` span IDs.
|
|
24
|
+
- Tool and span error signals preserved (Span `status_code` parsed from OTLP status, `is_error`/error status flagged on message tool call responses).
|
|
25
|
+
- Hybrid trace steps ordered chronologically by span start time, preserving originating `span_id`.
|
|
26
|
+
- Span error recovery ported from FAULTLINE P18: middle span errors unrecovered unless a later span runs a non-error tool call.
|
|
27
|
+
- Pinned Exgentic dataset revision in `scripts/fetch_real_traces.py` and handled expected trace-failure exit code in `make demo-real`.
|
|
28
|
+
|
|
29
|
+
## 0.1.0 — 2026-09-22
|
|
30
|
+
|
|
31
|
+
- `faultgate check <traces.json>`: run checks over an OTLP/JSON GenAI-semconv trace export; exit 1 if any fires.
|
|
32
|
+
- Rule checks (no model): `search_loop` (κ 0.98 on 200 human-labelled runs, 1.00 on the P5 split), `abstention` (1/1, 0 FP).
|
|
33
|
+
- `policy` rule + `--policy policy.json` (allowlist, denied argument patterns, max argument size, call budget) — FAULTLINE P16 after the fact; P9's four structural attack shapes covered by tests; 0 FP on 290 real traces.
|
|
34
|
+
- Golden checks + `--golden golden.json` (keyed by scenario or prompt): `wrong_answer` (4/4 vs human verdict, 0 FP), `truncation` (1/1, 0 FP).
|
|
35
|
+
- Composite GitHub Action (`uses: Faultline-lab/faultgate@<ref>`), self-tested in CI against the shipped examples.
|
|
36
|
+
- `laya` is an extra (`faultgate[laya]`); the base install is pydantic only.
|
|
37
|
+
- Judge checks (opt-in `--judge`): `wrong_direction`. Judges: `laya` (local, pinned `1c5edc17`, budgeted render), `litellm:<model>` (`[api]` extra).
|
|
38
|
+
- `faultgate baseline` + `check --band`: tolerance band from ≥ 2 baseline runs (FAULTLINE P11 rule), PASS/WARN/FAIL, named regressions.
|
|
39
|
+
- `scripts/calibrate.py`: score any check/judge against FAULTLINE P4 human labels.
|
|
40
|
+
- Examples: five real runs (`examples/traces.json`); two `glm-5.3-flash` baselines and `gpt-5.6-luna` / `qwen3.7-flash` candidates on P11's 30 golden scenarios (`examples/gate/`).
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: faultgate
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Local-first release gate for AI agents: check recorded traces for known failure modes through a pluggable judge.
|
|
5
5
|
Author: Samir Sawarkar
|
|
6
6
|
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/
|
|
7
|
+
Project-URL: Homepage, https://github.com/Faultline-lab/faultgate
|
|
8
8
|
Project-URL: Evidence, https://github.com/samirsawarkar/faultline-ai-reliability
|
|
9
9
|
Keywords: ai,agents,evaluation,reliability,opentelemetry,llm
|
|
10
10
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -20,6 +20,9 @@ Provides-Extra: laya
|
|
|
20
20
|
Requires-Dist: laya>=0.3.5; extra == "laya"
|
|
21
21
|
Provides-Extra: api
|
|
22
22
|
Requires-Dist: litellm>=1.70; extra == "api"
|
|
23
|
+
Provides-Extra: pii
|
|
24
|
+
Requires-Dist: presidio-analyzer>=2.2; extra == "pii"
|
|
25
|
+
Requires-Dist: presidio-anonymizer>=2.2; extra == "pii"
|
|
23
26
|
Provides-Extra: dev
|
|
24
27
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
25
28
|
Provides-Extra: demo
|
|
@@ -33,15 +36,15 @@ Dynamic: license-file
|
|
|
33
36
|
|
|
34
37
|
```
|
|
35
38
|
$ faultgate check examples/traces.json
|
|
36
|
-
trace search_loop abstention policy repeated_call unrecovered_tool_error
|
|
37
|
-
|
|
38
|
-
fc2cdab304bd FAIL 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
39
|
-
6d1f9d67a2d4 ok 1.00 FAIL 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
40
|
-
8bf3879339eb ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
41
|
-
3a9e507a0f6d ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
42
|
-
2ab3b6c1e891 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
43
|
-
|
|
44
|
-
2/5 traces failed · search_loop=1, abstention=1, policy=0, repeated_call=0, unrecovered_tool_error=0 · judge none · 0.0s
|
|
39
|
+
trace search_loop abstention policy repeated_call unrecovered_tool_error context_overflow
|
|
40
|
+
------------------------------------------------------------------------------------------------------------------------------
|
|
41
|
+
fc2cdab304bd FAIL 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
42
|
+
6d1f9d67a2d4 ok 1.00 FAIL 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
43
|
+
8bf3879339eb ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
44
|
+
3a9e507a0f6d ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
45
|
+
2ab3b6c1e891 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
46
|
+
|
|
47
|
+
2/5 traces failed · search_loop=1, abstention=1, policy=0, repeated_call=0, unrecovered_tool_error=0, context_overflow=0 · judge none · 0.0s
|
|
45
48
|
$ echo $?
|
|
46
49
|
1
|
|
47
50
|
```
|
|
@@ -59,6 +62,7 @@ Requires Python 3.11 or newer. One dependency (pydantic). The built-in checks ar
|
|
|
59
62
|
```bash
|
|
60
63
|
pip install 'faultgate[laya]' # local judge: downloads Laya once (~800 MB, Apache-2.0, pinned revision), offline after
|
|
61
64
|
pip install 'faultgate[api]' # any API model through litellm
|
|
65
|
+
pip install 'faultgate[pii]' # Microsoft Presidio for --redact presidio (names, locations; MIT)
|
|
62
66
|
```
|
|
63
67
|
|
|
64
68
|
Read [How much to trust a verdict](#how-much-to-trust-a-verdict) before you enable a judge.
|
|
@@ -72,10 +76,12 @@ faultgate check traces.json --golden golden.json # expected answers: correct
|
|
|
72
76
|
faultgate check traces.json --policy policy.json # what the agent was allowed to do
|
|
73
77
|
faultgate check traces.json --judge laya # also run judge checks, locally
|
|
74
78
|
faultgate check traces.json --judge litellm:openai/gpt-5.6-luna # any API model
|
|
79
|
+
faultgate check traces.json --markdown pr.md --audit audit/ # PR comment + audit files
|
|
80
|
+
faultgate precheck traces.json # validate an export before gating
|
|
75
81
|
faultgate checks # list checks and their kind
|
|
76
82
|
faultgate judges # list judge specs
|
|
77
83
|
|
|
78
|
-
faultgate baseline run1.json run2.json --out band.json # measure your noise (
|
|
84
|
+
faultgate baseline run1.json run2.json run3.json --out band.json # measure your noise (3+ runs)
|
|
79
85
|
faultgate check candidate.json --band band.json # regression, or just noise?
|
|
80
86
|
```
|
|
81
87
|
|
|
@@ -90,7 +96,32 @@ An [OTLP/JSON](https://opentelemetry.io/docs/specs/otlp/#json-protobuf-encoding)
|
|
|
90
96
|
| root `invoke_agent` | `gen_ai.agent.name`, `gen_ai.input.messages` (the user prompt), `gen_ai.output.messages` (the final answer), optional `faultgate.termination` (why the run stopped, e.g. `step cap`) |
|
|
91
97
|
| child `execute_tool` | `gen_ai.tool.name`, `gen_ai.tool.call.arguments`, `gen_ai.tool.call.result` |
|
|
92
98
|
|
|
93
|
-
Legacy `gen_ai.prompt` / `gen_ai.completion` are accepted as fallback. If your exporter records structure but not content, `faultgate` warns you. [`examples/traces.json`](https://github.com/
|
|
99
|
+
Legacy `gen_ai.prompt` / `gen_ai.completion` are accepted as fallback. If your exporter records structure but not content, `faultgate` warns you. [`examples/traces.json`](https://github.com/Faultline-lab/faultgate/blob/main/examples/traces.json) is a complete reference file.
|
|
100
|
+
|
|
101
|
+
### Other trace formats
|
|
102
|
+
|
|
103
|
+
`faultgate` also reads the exports of the common agent-observability tools and maps them onto the same attributes, so every check, policy and judge works unchanged. The format is detected; force it with `--format`.
|
|
104
|
+
|
|
105
|
+
| `--format` | Export | Steps come from |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `otlp` | OTLP/JSON, GenAI semantic conventions | `execute_tool` spans and tool calls inside chat messages |
|
|
108
|
+
| `openinference` | OTLP with `openinference.span.kind` (Arize Phoenix) | `TOOL` spans: `tool.name`, `tool.parameters` or `input.value`, `output.value` |
|
|
109
|
+
| `langfuse` | trace objects with `observations` (e.g. `GET /api/public/traces/{id}`) | `TOOL` observations; `level: ERROR` + `statusMessage` become span errors |
|
|
110
|
+
| `langsmith` | a list of runs | `run_type: tool` runs; `error` becomes a tool error |
|
|
111
|
+
| `braintrust` | a list of logged spans | `span_attributes.type: tool` spans |
|
|
112
|
+
|
|
113
|
+
Token usage, finish reasons (`context_overflow`), errors (`unrecovered_tool_error`), the user id (`enduser.id`, for `identity` policies) and `metadata.scenario` (scenario key for bands) are carried over where the format records them. [`examples/formats/`](examples/formats/) holds the same refund run in each format; `tests/test_formats.py` checks that all four normalise to identical steps.
|
|
114
|
+
|
|
115
|
+
### Precheck
|
|
116
|
+
|
|
117
|
+
Validate an export before you gate on it:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
$ faultgate precheck examples/formats/langfuse.json
|
|
121
|
+
precheck: OK · format langfuse · checked 1 of 1 traces · 0 error(s), 0 warning(s)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
It reports the detected format, then per trace: duplicate span ids, missing or backwards timestamps, tool calls without a name (errors, exit 2), and missing roots, spans whose parent is not in the export, runs with no prompt, answer or content (warnings, exit 0). It inspects the first 5 traces by default (`--sample N`, `0` for all). On the 30 real benchmark traces from `make demo-real` it passes with warnings: those exports omit the root span.
|
|
94
125
|
|
|
95
126
|
## Measured on real agent traces
|
|
96
127
|
|
|
@@ -100,40 +131,40 @@ Run `make demo-real` (first run downloads ~220 MB, ~1 min) to fetch and evaluate
|
|
|
100
131
|
|
|
101
132
|
```
|
|
102
133
|
$ faultgate check .demo/real_traces.json
|
|
103
|
-
trace search_loop abstention policy repeated_call unrecovered_tool_error
|
|
104
|
-
|
|
105
|
-
8a224e8eacdd ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
106
|
-
3cf53cafee61 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
107
|
-
0ff67ea879fc ok 1.00 ok 1.00 ok 1.00 ok 1.00 FAIL 1.00
|
|
108
|
-
0f2709401932 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
109
|
-
a182fd9c1858 ok 1.00 ok 1.00 ok 1.00 FAIL 1.00 ok 1.00
|
|
110
|
-
9bf471687b5f ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
111
|
-
16b13ee29724 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
112
|
-
9dc71e9167b0 ok 1.00 ok 1.00 ok 1.00 ok 1.00 FAIL 1.00
|
|
113
|
-
3524b4b6ef7e ok 1.00 ok 1.00 ok 1.00 ok 1.00 FAIL 1.00
|
|
114
|
-
9ca4051344ef ok 1.00 ok 1.00 ok 1.00 ok 1.00 FAIL 1.00
|
|
115
|
-
56e646e603b2 FAIL 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
116
|
-
f406d07bd94c ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
117
|
-
012e74b002d9 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
118
|
-
f0cd792c3105 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
119
|
-
7c69e4d47302 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
120
|
-
fadbf538c24a ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
121
|
-
8e611f21829d ok 1.00 ok 1.00 ok 1.00 ok 1.00 FAIL 1.00
|
|
122
|
-
70722761fb7a FAIL 1.00 ok 1.00 ok 1.00 FAIL 1.00 ok 1.00
|
|
123
|
-
4ab9ef2c7e60 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
124
|
-
910a177350e2 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
125
|
-
a585b61b1598 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
126
|
-
3d7d24721847 ok 1.00 ok 1.00 ok 1.00 ok 1.00 FAIL 1.00
|
|
127
|
-
4422dd26b177 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
128
|
-
bff8bb4c1b2e ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
129
|
-
e42782af7315 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
130
|
-
53fdb0601422 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
131
|
-
1dff5fc97b54 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
132
|
-
88733a4ec841 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
133
|
-
dc97bf8af1d8 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
134
|
-
192bf8092eb8 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
135
|
-
|
|
136
|
-
9/30 traces failed · search_loop=2, abstention=0, policy=0, repeated_call=2, unrecovered_tool_error=6 · judge none ·
|
|
134
|
+
trace search_loop abstention policy repeated_call unrecovered_tool_error context_overflow
|
|
135
|
+
------------------------------------------------------------------------------------------------------------------------------
|
|
136
|
+
8a224e8eacdd ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
137
|
+
3cf53cafee61 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
138
|
+
0ff67ea879fc ok 1.00 ok 1.00 ok 1.00 ok 1.00 FAIL 1.00 ok 1.00
|
|
139
|
+
0f2709401932 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
140
|
+
a182fd9c1858 ok 1.00 ok 1.00 ok 1.00 FAIL 1.00 ok 1.00 ok 1.00
|
|
141
|
+
9bf471687b5f ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
142
|
+
16b13ee29724 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
143
|
+
9dc71e9167b0 ok 1.00 ok 1.00 ok 1.00 ok 1.00 FAIL 1.00 ok 1.00
|
|
144
|
+
3524b4b6ef7e ok 1.00 ok 1.00 ok 1.00 ok 1.00 FAIL 1.00 ok 1.00
|
|
145
|
+
9ca4051344ef ok 1.00 ok 1.00 ok 1.00 ok 1.00 FAIL 1.00 ok 1.00
|
|
146
|
+
56e646e603b2 FAIL 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
147
|
+
f406d07bd94c ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
148
|
+
012e74b002d9 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
149
|
+
f0cd792c3105 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
150
|
+
7c69e4d47302 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
151
|
+
fadbf538c24a ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
152
|
+
8e611f21829d ok 1.00 ok 1.00 ok 1.00 ok 1.00 FAIL 1.00 ok 1.00
|
|
153
|
+
70722761fb7a FAIL 1.00 ok 1.00 ok 1.00 FAIL 1.00 ok 1.00 ok 1.00
|
|
154
|
+
4ab9ef2c7e60 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
155
|
+
910a177350e2 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
156
|
+
a585b61b1598 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
157
|
+
3d7d24721847 ok 1.00 ok 1.00 ok 1.00 ok 1.00 FAIL 1.00 ok 1.00
|
|
158
|
+
4422dd26b177 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
159
|
+
bff8bb4c1b2e ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
160
|
+
e42782af7315 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
161
|
+
53fdb0601422 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
162
|
+
1dff5fc97b54 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
163
|
+
88733a4ec841 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
164
|
+
dc97bf8af1d8 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
165
|
+
192bf8092eb8 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00 ok 1.00
|
|
166
|
+
|
|
167
|
+
9/30 traces failed · search_loop=2, abstention=0, policy=0, repeated_call=2, unrecovered_tool_error=6, context_overflow=0 · judge none · 2.1s
|
|
137
168
|
```
|
|
138
169
|
|
|
139
170
|
Two new checks with measured trust, ported from [FAULTLINE P18](https://github.com/samirsawarkar/faultline-ai-reliability) (`projects/p18_trace_triage`):
|
|
@@ -153,9 +184,10 @@ These are public benchmark traces, not production traffic; the P18 sample was st
|
|
|
153
184
|
|---|---|---|
|
|
154
185
|
| `search_loop` | rule | The run ended with no answer at its step budget and most tool calls returned nothing. |
|
|
155
186
|
| `abstention` | rule | The final answer explicitly says the information could not be found. |
|
|
156
|
-
| `policy` | rule | A tool call broke the policy: unlisted tool, denied argument pattern, oversized argument, or
|
|
187
|
+
| `policy` | rule | A tool call broke the policy: unlisted tool, denied argument pattern, oversized argument, call budget, argument limit, identity mismatch, or missing prerequisite. |
|
|
157
188
|
| `repeated_call` | rule | The same tool was called with identical arguments and returned identical results >= 3 times. |
|
|
158
189
|
| `unrecovered_tool_error` | rule | A tool call produced a structural error with no later successful call of the same tool, or a span errored (error.type or status code 2) and no later span made a successful tool call. |
|
|
190
|
+
| `context_overflow` | rule | The run hit the model's context window or output token limit: a span's `gen_ai.response.finish_reasons` says `length` / `max_tokens`, or a provider error naming the context limit (`context_length_exceeded`, "maximum context length", "prompt is too long") in a span status, `error.type`, tool result or `faultgate.termination`. |
|
|
159
191
|
| `wrong_answer` | golden | Answered, and the expected answer is not in the final answer. |
|
|
160
192
|
| `truncation` | golden | The answer has the head of the expected token but not the whole token (`Onyx` for `Onyx-4413`). |
|
|
161
193
|
| `wrong_direction` | judge | On a multi-hop task, the agent walked the dependency chain backwards. |
|
|
@@ -170,37 +202,77 @@ A **rule** is plain code over the trace structure: instant, deterministic, no mo
|
|
|
170
202
|
|
|
171
203
|
`faultgate check traces.json --policy policy.json` then flags every step that called a tool outside `allow`, passed an argument matching a denied pattern (path traversal, `file://`, `/etc/passwd`, `~/.ssh` out of the box), exceeded `max_arg_chars`, or pushed the run past `max_calls` — with the step number in the reason. It is FAULTLINE P16's runtime policy applied after the fact: the four structural injection categories from P9 (`tool_exfil_path`, `unlisted_tool`, `query_dump`, `budget_loop`) are each caught by a test in `tests/test_policy.py`. Without a policy file only the denied-pattern rule is active, and it fires on 0 of the 290 real traces shipped here.
|
|
172
204
|
|
|
205
|
+
Three more keys check business rules on what the agent actually did, not just which tools it touched:
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"limits": [{"tool": "issue_refund", "arg": "amount", "min": 0, "max": 500}],
|
|
210
|
+
"identity": {"arg": "customer_id", "session_attr": "enduser.id", "tools": ["issue_refund", "modify_billing_record"]},
|
|
211
|
+
"requires": [{"tool": "modify_billing_record", "after": "verify_customer_identity", "result_contains": "VERIFIED"}]
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
- `limits`: a numeric floor or ceiling on one argument of one tool. `arg` is a dotted path into the JSON arguments (`refund.amount`). Strings like `"$1,200.50"` are read as numbers. An argument that is present but not a number, or arguments that are not a JSON object, count as a violation; an absent argument (or no arguments at all) does not. Arguments encoded as JSON twice are decoded. `min`/`max` must be finite numbers.
|
|
216
|
+
- `identity`: the argument must equal the authenticated principal, read from the root span attribute `session_attr` (default `enduser.id`). Only calls that carry the argument are checked, optionally only for `tools`. If the root span has no such attribute, the call cannot be verified and counts as a violation. So do unreadable (non-JSON-object) arguments on a listed tool, or on any tool when no `tools` list is given and the argument's name appears in the text.
|
|
217
|
+
- `requires`: `tool` may only run after an earlier call to `after`, and, if `result_contains` is set, one whose result contains that text.
|
|
218
|
+
|
|
219
|
+
Reasons name the step and the values: `step 4: modify_billing_record.customer_id 'C-999' != session enduser.id 'C-123'`. [`examples/policy_support.json`](examples/policy_support.json) is a complete customer-support policy. A malformed policy file exits 2.
|
|
220
|
+
|
|
221
|
+
**A policy violation is a hard block.** With `--band`, a run whose clean rate sits inside the measured band still FAILs if the `policy` check fired on any trace; the gate prints `policy violation in N trace(s) (hard block)` and lists them under `policy_violations` in the JSON report. Noise explains a flaky answer, not a refund over the limit.
|
|
222
|
+
|
|
173
223
|
Semantic injections — the agent *obeying* text it read in a document — are a judge question, not a rule, and are not in this release.
|
|
174
224
|
|
|
225
|
+
## Data stays local: redaction
|
|
226
|
+
|
|
227
|
+
`faultgate` reads your traces in memory and writes no trace payloads anywhere. Rules, policy, bands and the local Laya judge never send anything off the machine. Two things can travel: the trace an **API judge** (`litellm:`) reads, and the **reasons** that end up in your terminal, `--json` report, CI logs and PR comments. By default both are masked:
|
|
228
|
+
|
|
229
|
+
| `--redact` | Masks | Needs |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `builtin` (default) | emails, credit cards (Luhn-checked), US SSNs, phone numbers, IPv4/IPv6, API keys, bearer tokens, JWTs | nothing (stdlib regex) |
|
|
232
|
+
| `presidio` | the above plus Presidio's recognisers: person names, locations, more | `pip install 'faultgate[pii]'` and a spaCy model |
|
|
233
|
+
| `off` | nothing | |
|
|
234
|
+
|
|
235
|
+
Rules always see the raw values (an `identity` policy has to compare the real customer id); only what leaves is masked: `step 2: update_profile.email '<EMAIL>' != session enduser.id '<EMAIL>'`. The report's `redaction` field records which mode ran. On the 30 real benchmark traces from `make demo-real` the built-in masker finds 81 emails, 39 phone numbers and 69 JWT access tokens, all real-looking customer data or credentials; it changes none of the text in the shipped FAULTLINE traces.
|
|
236
|
+
|
|
175
237
|
## Regression or noise? Tolerance bands
|
|
176
238
|
|
|
177
239
|
Agents are not deterministic. FAULTLINE P11 ran the same model on the same 30 inputs five times and got pass rates from 0.60 to 0.87. A fixed threshold turns that into a coin-flip gate. `faultgate` measures your spread instead:
|
|
178
240
|
|
|
179
241
|
```
|
|
180
242
|
$ faultgate baseline examples/gate/baseline_glm_run1.json examples/gate/baseline_glm_run2.json --out band.json
|
|
243
|
+
note: 2 baseline runs measure noise poorly; use 3 or more
|
|
181
244
|
examples/gate/baseline_glm_run1.json: 30 traces · clean rate 0.93 · 0.0s
|
|
182
245
|
examples/gate/baseline_glm_run2.json: 30 traces · clean rate 0.93 · 0.0s
|
|
183
246
|
band [0.93, 0.93] over 30 scenarios × 2 runs · 26 clean in every run · written to band.json
|
|
184
247
|
|
|
185
248
|
$ faultgate check examples/gate/candidate_gpt.json --band band.json
|
|
186
|
-
18/30 traces failed · search_loop=18, abstention=0 · judge none · 0.0s
|
|
187
|
-
gate: FAIL · clean rate 0.40 vs band [0.93, 0.93] from 2 baseline runs · fail below 0.83
|
|
249
|
+
18/30 traces failed · search_loop=18, abstention=0, policy=0, repeated_call=0, unrecovered_tool_error=0, context_overflow=0 · judge none · 0.0s
|
|
250
|
+
gate: FAIL · clean rate 0.40 vs band [0.93, 0.93] from 2 baseline runs · fail below 0.83 · p=0.000
|
|
188
251
|
regressions (clean in every baseline, not now): r-0208, r-0214, r-0216, r-0224, r-0226, r-0232, r-0258, ...
|
|
189
252
|
|
|
190
253
|
$ faultgate check examples/gate/candidate_qwen.json --band band.json
|
|
191
|
-
gate: FAIL · clean rate 0.70 vs band [0.93, 0.93] from 2 baseline runs · fail below 0.83
|
|
254
|
+
gate: FAIL · clean rate 0.70 vs band [0.93, 0.93] from 2 baseline runs · fail below 0.83 · p=0.005
|
|
192
255
|
|
|
193
256
|
$ faultgate check examples/gate/baseline_glm_run2.json --band band.json
|
|
194
|
-
gate: PASS · clean rate 0.93 vs band [0.93, 0.93] from 2 baseline runs · fail below 0.83
|
|
257
|
+
gate: PASS · clean rate 0.93 vs band [0.93, 0.93] from 2 baseline runs · fail below 0.83 · p=0.500
|
|
195
258
|
```
|
|
196
259
|
|
|
197
|
-
Real runs: two `z-ai/glm-5.3-flash` baselines and `openai/gpt-5.6-luna` / `qwen/qwen3.7-flash` candidates on the same 30 multi-hop scenarios. The verdicts match FAULTLINE P11's live drill (R4 FAIL / R1 FAIL / R2 PASS), which cost $1.84 and an API judge; this took 0.0 s and no model.
|
|
260
|
+
Real runs (table rows and most stderr notes omitted): two `z-ai/glm-5.3-flash` baselines and `openai/gpt-5.6-luna` / `qwen/qwen3.7-flash` candidates on the same 30 multi-hop scenarios. The verdicts match FAULTLINE P11's live drill (R4 FAIL / R1 FAIL / R2 PASS), which cost $1.84 and an API judge; this took 0.0 s and no model.
|
|
198
261
|
|
|
199
262
|
| Verdict | Rule (n = scenarios in the band) | Exit |
|
|
200
263
|
|---|---|---|
|
|
201
264
|
| **PASS** | clean rate ≥ min observed − 1/n | 0 |
|
|
202
|
-
| **
|
|
203
|
-
| **
|
|
265
|
+
| **FAIL** | clean rate < min observed − 3/n, or below PASS and the drop is significant (one-sided, α = 0.05) | 1 |
|
|
266
|
+
| **INCONCLUSIVE** | in between and not significant: the gate says how many scenarios it would need (power 0.80) | 0 |
|
|
267
|
+
|
|
268
|
+
In between the two thresholds, the gate tests whether the candidate's clean rate is below the baseline mean. The variance is the binomial noise of both sides plus the run-to-run variance the baseline measured, so a noisy baseline needs a bigger drop to fail. When it cannot tell, it says so and sizes the fix:
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
gate: INCONCLUSIVE · clean rate 0.93 vs band [1.00, 1.00] from 2 baseline runs · fail below 0.90 · p=0.072
|
|
272
|
+
inconclusive: a drop of 0.07 needs 89 scenarios to tell from noise (alpha 0.05, power 0.80) · run 59 more
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
(Two failures injected into a run of a baseline that was clean on all 30 scenarios; `tests/test_band.py`.) `required_n` is the standard two-proportion sample size for the observed drop. The gate result in the JSON report carries the same numbers under `stats`. Build the band from 3 or more baseline runs; with 2, the run-to-run variance is a guess and the gate warns.
|
|
204
276
|
|
|
205
277
|
It also lists **regressions**: scenarios clean in *every* baseline run and not clean now — the ones to read first. Scenarios are matched across runs by `faultgate.scenario`, else `gen_ai.agent.id`, else a hash of the prompt.
|
|
206
278
|
|
|
@@ -213,12 +285,12 @@ examples/gate/baseline_glm_run2.json: 30 traces · clean rate 0.87 · 0.0s
|
|
|
213
285
|
band [0.80, 0.87] over 30 scenarios × 2 runs · 21 clean in every run
|
|
214
286
|
|
|
215
287
|
$ faultgate check examples/gate/candidate_gpt.json --golden examples/gate/golden.json --band band.json
|
|
216
|
-
21/30 traces failed · search_loop=18, abstention=0, policy=0, wrong_answer=3, truncation=3
|
|
217
|
-
gate: FAIL · clean rate 0.30 vs band [0.80, 0.87] from 2 baseline runs · fail below 0.70
|
|
288
|
+
21/30 traces failed · search_loop=18, abstention=0, policy=0, repeated_call=0, unrecovered_tool_error=0, context_overflow=0, wrong_answer=3, truncation=3 · judge none · 0.0s
|
|
289
|
+
gate: FAIL · clean rate 0.30 vs band [0.80, 0.87] from 2 baseline runs · fail below 0.70 · p=0.000
|
|
218
290
|
|
|
219
291
|
$ faultgate check examples/gate/candidate_qwen.json --golden examples/gate/golden.json --band band.json
|
|
220
|
-
18/30 traces failed · search_loop=9, abstention=0, policy=0, wrong_answer=9, truncation=8
|
|
221
|
-
gate: FAIL · clean rate 0.40 vs band [0.80, 0.87] from 2 baseline runs · fail below 0.70
|
|
292
|
+
18/30 traces failed · search_loop=9, abstention=0, policy=0, repeated_call=1, unrecovered_tool_error=0, context_overflow=0, wrong_answer=9, truncation=8 · judge none · 0.0s
|
|
293
|
+
gate: FAIL · clean rate 0.40 vs band [0.80, 0.87] from 2 baseline runs · fail below 0.70 · p=0.000
|
|
222
294
|
```
|
|
223
295
|
|
|
224
296
|
(Qwen returns the head of the answer token without its suffix in 8 of 30 runs. Nobody had noticed.)
|
|
@@ -226,7 +298,7 @@ gate: FAIL · clean rate 0.40 vs band [0.80, 0.87] from 2 baseline runs · fail
|
|
|
226
298
|
## In CI
|
|
227
299
|
|
|
228
300
|
```yaml
|
|
229
|
-
- uses:
|
|
301
|
+
- uses: Faultline-lab/faultgate@main
|
|
230
302
|
with:
|
|
231
303
|
traces: traces/candidate.json # exported by your agent's OTel pipeline
|
|
232
304
|
band: traces/band.json # from `faultgate baseline`, committed
|
|
@@ -234,7 +306,13 @@ gate: FAIL · clean rate 0.40 vs band [0.80, 0.87] from 2 baseline runs · fail
|
|
|
234
306
|
policy: traces/policy.json # optional
|
|
235
307
|
```
|
|
236
308
|
|
|
237
|
-
Exit 1 on gate FAIL, so the job fails;
|
|
309
|
+
Exit 1 on gate FAIL, so the job fails; PASS and INCONCLUSIVE pass (INCONCLUSIVE prints how many more scenarios to run). Every run also:
|
|
310
|
+
|
|
311
|
+
- writes the Markdown summary to the **job summary**, and on pull requests posts **one PR comment** (updated in place on later pushes, found by its `<!-- faultgate -->` marker): the verdict, gate numbers and p-value, fired checks, the policy hard block with each broken rule, failing traces with the step and reason, the latency / token / cost change against the baseline, and the exact command to reproduce it locally. Give the job `permissions: pull-requests: write`, or set `comment: "false"`;
|
|
312
|
+
- uploads an **audit artifact** (`faultgate-audit`): `verdict.json` (verdict, exit code, faultgate version, UTC time, every input file with its SHA-256, gate numbers), `failing_traces.json` (each failing trace and every reason) and `repro.sh`. No prompts, answers or tool payloads are copied into it, and reasons are masked by `redact`;
|
|
313
|
+
- exposes the verdict as the step output `verdict`.
|
|
314
|
+
|
|
315
|
+
Other inputs: `format`, `redact`, `checks`, `judge`, `report`, `audit`, `version`. The same files come from the CLI with `--markdown summary.md --audit audit/`. The action installs nothing heavier than pydantic; our own CI runs it against this repo's examples and asserts that the GPT candidate fails and the GLM baseline passes, with matching verdict outputs and audit files.
|
|
238
316
|
|
|
239
317
|
## How much to trust a verdict
|
|
240
318
|
|
|
@@ -245,6 +323,7 @@ This is the part every evaluation tool skips. Every check and judge here is scor
|
|
|
245
323
|
| `search_loop` | rule | 36 | 1.00 | 0.99 | **0.98** | **1.00** |
|
|
246
324
|
| `abstention` | rule | 1 | 1.00 | 1.00 | 1.00 | — (0 positives) |
|
|
247
325
|
| `policy` | rule | synthetic | 4/4 P9 categories | 0 FP on 290 real traces | — | — |
|
|
326
|
+
| `context_overflow` | rule | synthetic | finish-reason and provider-error forms in `tests/test_checks.py` | 0 FP on the 125 shipped real traces | — | — |
|
|
248
327
|
| `wrong_answer` | golden | 4 | 1.00 | 1.00 | 1.00 | 1.00 |
|
|
249
328
|
| `truncation` | golden | 1 | 1.00 | 1.00 | 1.00 | — (0 positives) |
|
|
250
329
|
| `wrong_direction` | Laya judge | 1 | 1.00 | 0.61 | 0.02 | 0.00 |
|