grounding-gate 0.1.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.
- grounding_gate-0.1.0/.gitattributes +1 -0
- grounding_gate-0.1.0/.github/workflows/ci.yml +31 -0
- grounding_gate-0.1.0/.github/workflows/release.yml +44 -0
- grounding_gate-0.1.0/.gitignore +10 -0
- grounding_gate-0.1.0/CHANGELOG.md +14 -0
- grounding_gate-0.1.0/LICENSE +21 -0
- grounding_gate-0.1.0/PKG-INFO +194 -0
- grounding_gate-0.1.0/README.md +168 -0
- grounding_gate-0.1.0/docs/module-2-classifier.md +75 -0
- grounding_gate-0.1.0/docs/module-4-boundary.md +76 -0
- grounding_gate-0.1.0/docs/spec.md +126 -0
- grounding_gate-0.1.0/examples/demo.py +121 -0
- grounding_gate-0.1.0/pyproject.toml +48 -0
- grounding_gate-0.1.0/src/grounding_gate/__init__.py +30 -0
- grounding_gate-0.1.0/src/grounding_gate/boundary.py +103 -0
- grounding_gate-0.1.0/src/grounding_gate/classifier.py +45 -0
- grounding_gate-0.1.0/src/grounding_gate/state.py +67 -0
- grounding_gate-0.1.0/tests/test_gate.py +203 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
os: [ubuntu-latest]
|
|
15
|
+
python-version: ["3.9", "3.13"]
|
|
16
|
+
include:
|
|
17
|
+
- os: windows-latest
|
|
18
|
+
python-version: "3.13"
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
24
|
+
- name: Install
|
|
25
|
+
run: pip install -e ".[dev]"
|
|
26
|
+
- name: Test suite
|
|
27
|
+
run: pytest -q
|
|
28
|
+
- name: Zero-dependency check (tests run on bare python)
|
|
29
|
+
run: python tests/test_gate.py
|
|
30
|
+
- name: Demo
|
|
31
|
+
run: python examples/demo.py
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: "3.13"
|
|
15
|
+
- name: Test
|
|
16
|
+
run: |
|
|
17
|
+
pip install -e ".[dev]"
|
|
18
|
+
pytest -q
|
|
19
|
+
- name: Build sdist and wheel
|
|
20
|
+
run: |
|
|
21
|
+
pip install build twine
|
|
22
|
+
python -m build
|
|
23
|
+
twine check dist/*
|
|
24
|
+
- uses: actions/upload-artifact@v4
|
|
25
|
+
with:
|
|
26
|
+
name: dist
|
|
27
|
+
path: dist/
|
|
28
|
+
|
|
29
|
+
publish:
|
|
30
|
+
needs: build
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
# Trusted Publishing (OIDC): configure this repo + workflow + environment
|
|
33
|
+
# as a (pending) publisher for the project on pypi.org — no API token.
|
|
34
|
+
environment:
|
|
35
|
+
name: pypi
|
|
36
|
+
url: https://pypi.org/p/grounding-gate
|
|
37
|
+
permissions:
|
|
38
|
+
id-token: write
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/download-artifact@v4
|
|
41
|
+
with:
|
|
42
|
+
name: dist
|
|
43
|
+
path: dist/
|
|
44
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-07-10
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `classify_observation` — novelty ∧ relevance ∧ consequence-tier observation
|
|
8
|
+
classifier (corrected C1/C3 behavior preserved from review).
|
|
9
|
+
- `boundary_check` / `turn_loop` — submit-boundary choke point and reference
|
|
10
|
+
loop wiring (latched flags, qualifying-only halt clear, starving refusals).
|
|
11
|
+
- `GateState.for_model_class` with `skipper` / `diverger` / `default` presets,
|
|
12
|
+
including strict-G enforcement for the skipper class (assertions require
|
|
13
|
+
verified-tier grounding).
|
|
14
|
+
- 19-case acceptance suite, runnable on bare Python with zero dependencies.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ciphemon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: grounding-gate
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zero-token structural verifier for agent loops: one choke point at the submit boundary enforcing grounding and budget invariants. No LLM calls, stdlib only.
|
|
5
|
+
Project-URL: Homepage, https://github.com/CiphemonJY/grounding-gate
|
|
6
|
+
Project-URL: Repository, https://github.com/CiphemonJY/grounding-gate
|
|
7
|
+
Project-URL: Issues, https://github.com/CiphemonJY/grounding-gate/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/CiphemonJY/grounding-gate/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Ciphemon
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent-loop,agents,grounding,guardrails,hallucination,llm,verification
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# grounding-gate
|
|
28
|
+
|
|
29
|
+
[](https://github.com/CiphemonJY/grounding-gate/actions/workflows/ci.yml)
|
|
30
|
+
|
|
31
|
+
**Zero-token structural verifier for agent loops.** One choke point at the
|
|
32
|
+
submit boundary decides whether an agent is allowed to say "X is true" or
|
|
33
|
+
"I did X" — using hash, set, and integer operations only. No LLM calls, no
|
|
34
|
+
per-turn prompt injection, no dependencies.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
pip install grounding-gate # stdlib only, Python >= 3.9
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The demo ships in the repo (not the wheel):
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
git clone https://github.com/CiphemonJY/grounding-gate && cd grounding-gate
|
|
44
|
+
python examples/demo.py # the whole idea in 30 seconds
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## The problem
|
|
48
|
+
|
|
49
|
+
Agents fail in two characteristic ways, and both ship *confident* wrong answers:
|
|
50
|
+
|
|
51
|
+
- **Skip-and-hallucinate** — emit a terminal claim ("done, config fixed")
|
|
52
|
+
without ever observing reality after acting on it.
|
|
53
|
+
- **Reason-and-diverge** — loop in closed context, burning steps on
|
|
54
|
+
reasoning about stale beliefs, until a confident wrong answer ships.
|
|
55
|
+
|
|
56
|
+
The standard fix is prose: "remember to verify your work" injected into every
|
|
57
|
+
turn. Prose costs tokens on every turn, behaves differently per model, and —
|
|
58
|
+
critically — is *skippable*. A reminder is not an invariant.
|
|
59
|
+
|
|
60
|
+
## The idea
|
|
61
|
+
|
|
62
|
+
Move enforcement out of the prompt and into **control flow**. A single gate
|
|
63
|
+
wraps the submit/conclude boundary, and a terminal output is emitted only if
|
|
64
|
+
both invariants hold:
|
|
65
|
+
|
|
66
|
+
- **G (grounding)** — a *qualifying* observation happened this turn, or the
|
|
67
|
+
output makes no factual claim. Qualifying means **novel** (result hash not
|
|
68
|
+
seen before, after stripping timestamps/ids) **∧ relevant** (touches the
|
|
69
|
+
identifiers the claim is about) **∧ consequence-tier-correct** (see below).
|
|
70
|
+
- **B (budget)** — reasoning rope remains. Qualifying observations *refill*
|
|
71
|
+
the budget (up to a cap); pure reasoning steps decrement it. Grounded work
|
|
72
|
+
runs effectively unbounded; closed-loop reasoning hits a hard floor.
|
|
73
|
+
|
|
74
|
+
Fail either → the terminal is **rejected** and the agent is told its only
|
|
75
|
+
legal moves: make a qualifying tool call, or exit with a typed **`unverified`**
|
|
76
|
+
terminal. `unverified` is a first-class, always-legal escape hatch — the gate
|
|
77
|
+
never traps an agent, it only forbids *confident* ungrounded claims.
|
|
78
|
+
|
|
79
|
+
### Consequence tiers
|
|
80
|
+
|
|
81
|
+
The gate distinguishes what kind of claim an observation can support:
|
|
82
|
+
|
|
83
|
+
| Claim type | Example | Requires |
|
|
84
|
+
|--------------|--------------------------|----------|
|
|
85
|
+
| `assertion` | "X is true" | a novel, relevant, read-only observation this turn |
|
|
86
|
+
| `completion` | "I changed X" | a novel, relevant read taken **after** the mutation — a mutating call never self-grounds its own effect |
|
|
87
|
+
| `unverified` | "couldn't confirm X" | nothing — always legal |
|
|
88
|
+
| `none` | no factual claim | nothing — exempt |
|
|
89
|
+
|
|
90
|
+
That second row is the heart of it: *writing a file and claiming success is
|
|
91
|
+
not verification; reading it back afterwards is.*
|
|
92
|
+
|
|
93
|
+
## Quickstart
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from grounding_gate import GateState, classify_observation, boundary_check
|
|
97
|
+
|
|
98
|
+
state = GateState.for_model_class("default", claim_surface={"app.cfg"})
|
|
99
|
+
|
|
100
|
+
# after EVERY tool call in your agent loop:
|
|
101
|
+
state.current_step += 1
|
|
102
|
+
obs = classify_observation(tool, args, result, state, read_only=not mutating)
|
|
103
|
+
state.grounded_this_turn |= obs["grounds_assertion"]
|
|
104
|
+
state.verified_this_turn |= obs["grounds_completion"]
|
|
105
|
+
if mutating:
|
|
106
|
+
state.last_mutation_step = state.current_step # a completion now needs a read AFTER this
|
|
107
|
+
|
|
108
|
+
# at every submit/conclude attempt — this must be the ONLY path to output:
|
|
109
|
+
verdict = boundary_check({"claim_type": "completion", "content": answer}, state)
|
|
110
|
+
if verdict["verdict"] == "REJECT":
|
|
111
|
+
... # surface verdict["legal_next"] to the model and continue the loop
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Note the mutation bookkeeping: without `last_mutation_step` ever being set, no
|
|
115
|
+
read can reach the verified tier and a `completion` can never be accepted —
|
|
116
|
+
that is the gate working as designed, not a bug.
|
|
117
|
+
|
|
118
|
+
`turn_loop` in [boundary.py](https://github.com/CiphemonJY/grounding-gate/blob/main/src/grounding_gate/boundary.py)
|
|
119
|
+
is the complete reference wiring (budget refill, mutation tracking, halt
|
|
120
|
+
semantics, signal mapping) — use it as the integration template. The
|
|
121
|
+
[demo](https://github.com/CiphemonJY/grounding-gate/blob/main/examples/demo.py)
|
|
122
|
+
runs the same scripted agent through an ungated and a gated loop, side by side.
|
|
123
|
+
|
|
124
|
+
## Model-class presets
|
|
125
|
+
|
|
126
|
+
Fleet variance is absorbed as integers, not prose. Pick the preset matching
|
|
127
|
+
how your model fails:
|
|
128
|
+
|
|
129
|
+
| Preset | CAP | REFILL | Strict G | For |
|
|
130
|
+
|------------|-----|--------|----------|-----|
|
|
131
|
+
| `skipper` | 5 | 2 | yes | models that hallucinate-and-skip |
|
|
132
|
+
| `diverger` | 4 | 1 | no | models that reason forever |
|
|
133
|
+
| `default` | 6 | 2 | no | everything else |
|
|
134
|
+
|
|
135
|
+
Strict G means even plain *assertions* require verified-tier grounding (a
|
|
136
|
+
post-mutation observation) — an observed-tier read is not enough. In a task
|
|
137
|
+
that never mutates anything, a strict-G agent can only exit via the typed
|
|
138
|
+
`unverified` terminal; that hard line is the point of the skipper preset, so
|
|
139
|
+
pick `default` for read-only/Q&A workloads.
|
|
140
|
+
|
|
141
|
+
## Declarative rails
|
|
142
|
+
|
|
143
|
+
A task can declare signals that must be verified before any completion is
|
|
144
|
+
accepted (`state.goal_predicates = ["tests_passed"]`). The gate never
|
|
145
|
+
interprets meaning — it only checks that a signal named `tests_passed` was
|
|
146
|
+
registered by a mapped, real command outcome. Semantic judgment stays out of
|
|
147
|
+
the floor by design.
|
|
148
|
+
|
|
149
|
+
## What the gate does NOT do
|
|
150
|
+
|
|
151
|
+
Honest scope, from the design's leak audit:
|
|
152
|
+
|
|
153
|
+
- **No semantic correctness.** A grounded claim can still be wrong (the model
|
|
154
|
+
can misread a real result). That is punted to a declared verifier tier
|
|
155
|
+
(`verify_with`), not smuggled into the floor.
|
|
156
|
+
- **Relevance can be spoofed** by a model that deliberately mentions the right
|
|
157
|
+
identifiers in an irrelevant call. The floor defends against *lazy*
|
|
158
|
+
ungroundedness, which is the overwhelmingly common failure; adversarial
|
|
159
|
+
self-deception needs the verifier tier.
|
|
160
|
+
- **Nondeterministic tools** need per-tool `normalize()` extensions or the
|
|
161
|
+
audited `novelty_exempt` allowlist — too-weak normalization quietly disables
|
|
162
|
+
the no-op defense.
|
|
163
|
+
|
|
164
|
+
## How this was built
|
|
165
|
+
|
|
166
|
+
The modules were drafted by different LLMs and adversarially reviewed before
|
|
167
|
+
assembly; the final behavior is pinned by a 19-case acceptance suite
|
|
168
|
+
([tests/test_gate.py](https://github.com/CiphemonJY/grounding-gate/blob/main/tests/test_gate.py))
|
|
169
|
+
that runs on bare Python with zero dependencies. Two review findings shaped
|
|
170
|
+
the method and are preserved in the docstrings:
|
|
171
|
+
|
|
172
|
+
- A drafting model shipped a consequence-tier bug **and authored the test that
|
|
173
|
+
ratified it** — since then, expected outcomes are authored by the reviewer,
|
|
174
|
+
never by the generator
|
|
175
|
+
([docs/module-2-classifier.md](https://github.com/CiphemonJY/grounding-gate/blob/main/docs/module-2-classifier.md)).
|
|
176
|
+
- The remaining leaks lived *between* individually-passing test cases —
|
|
177
|
+
latch-vs-assignment, halt cleared by non-qualifying calls
|
|
178
|
+
([docs/module-4-boundary.md](https://github.com/CiphemonJY/grounding-gate/blob/main/docs/module-4-boundary.md)).
|
|
179
|
+
|
|
180
|
+
Full design spec:
|
|
181
|
+
[docs/spec.md](https://github.com/CiphemonJY/grounding-gate/blob/main/docs/spec.md).
|
|
182
|
+
|
|
183
|
+
## Status & roadmap
|
|
184
|
+
|
|
185
|
+
This is the reference implementation — correct, minimal, and framework-free.
|
|
186
|
+
Planned next:
|
|
187
|
+
|
|
188
|
+
- Adapters: Claude Agent SDK hook, LangGraph middleware, OpenAI Agents SDK.
|
|
189
|
+
- A real signal-mapper module (command exit code → declared signal).
|
|
190
|
+
- Empirical preset tuning across model classes.
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# grounding-gate
|
|
2
|
+
|
|
3
|
+
[](https://github.com/CiphemonJY/grounding-gate/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
**Zero-token structural verifier for agent loops.** One choke point at the
|
|
6
|
+
submit boundary decides whether an agent is allowed to say "X is true" or
|
|
7
|
+
"I did X" — using hash, set, and integer operations only. No LLM calls, no
|
|
8
|
+
per-turn prompt injection, no dependencies.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
pip install grounding-gate # stdlib only, Python >= 3.9
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The demo ships in the repo (not the wheel):
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
git clone https://github.com/CiphemonJY/grounding-gate && cd grounding-gate
|
|
18
|
+
python examples/demo.py # the whole idea in 30 seconds
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## The problem
|
|
22
|
+
|
|
23
|
+
Agents fail in two characteristic ways, and both ship *confident* wrong answers:
|
|
24
|
+
|
|
25
|
+
- **Skip-and-hallucinate** — emit a terminal claim ("done, config fixed")
|
|
26
|
+
without ever observing reality after acting on it.
|
|
27
|
+
- **Reason-and-diverge** — loop in closed context, burning steps on
|
|
28
|
+
reasoning about stale beliefs, until a confident wrong answer ships.
|
|
29
|
+
|
|
30
|
+
The standard fix is prose: "remember to verify your work" injected into every
|
|
31
|
+
turn. Prose costs tokens on every turn, behaves differently per model, and —
|
|
32
|
+
critically — is *skippable*. A reminder is not an invariant.
|
|
33
|
+
|
|
34
|
+
## The idea
|
|
35
|
+
|
|
36
|
+
Move enforcement out of the prompt and into **control flow**. A single gate
|
|
37
|
+
wraps the submit/conclude boundary, and a terminal output is emitted only if
|
|
38
|
+
both invariants hold:
|
|
39
|
+
|
|
40
|
+
- **G (grounding)** — a *qualifying* observation happened this turn, or the
|
|
41
|
+
output makes no factual claim. Qualifying means **novel** (result hash not
|
|
42
|
+
seen before, after stripping timestamps/ids) **∧ relevant** (touches the
|
|
43
|
+
identifiers the claim is about) **∧ consequence-tier-correct** (see below).
|
|
44
|
+
- **B (budget)** — reasoning rope remains. Qualifying observations *refill*
|
|
45
|
+
the budget (up to a cap); pure reasoning steps decrement it. Grounded work
|
|
46
|
+
runs effectively unbounded; closed-loop reasoning hits a hard floor.
|
|
47
|
+
|
|
48
|
+
Fail either → the terminal is **rejected** and the agent is told its only
|
|
49
|
+
legal moves: make a qualifying tool call, or exit with a typed **`unverified`**
|
|
50
|
+
terminal. `unverified` is a first-class, always-legal escape hatch — the gate
|
|
51
|
+
never traps an agent, it only forbids *confident* ungrounded claims.
|
|
52
|
+
|
|
53
|
+
### Consequence tiers
|
|
54
|
+
|
|
55
|
+
The gate distinguishes what kind of claim an observation can support:
|
|
56
|
+
|
|
57
|
+
| Claim type | Example | Requires |
|
|
58
|
+
|--------------|--------------------------|----------|
|
|
59
|
+
| `assertion` | "X is true" | a novel, relevant, read-only observation this turn |
|
|
60
|
+
| `completion` | "I changed X" | a novel, relevant read taken **after** the mutation — a mutating call never self-grounds its own effect |
|
|
61
|
+
| `unverified` | "couldn't confirm X" | nothing — always legal |
|
|
62
|
+
| `none` | no factual claim | nothing — exempt |
|
|
63
|
+
|
|
64
|
+
That second row is the heart of it: *writing a file and claiming success is
|
|
65
|
+
not verification; reading it back afterwards is.*
|
|
66
|
+
|
|
67
|
+
## Quickstart
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from grounding_gate import GateState, classify_observation, boundary_check
|
|
71
|
+
|
|
72
|
+
state = GateState.for_model_class("default", claim_surface={"app.cfg"})
|
|
73
|
+
|
|
74
|
+
# after EVERY tool call in your agent loop:
|
|
75
|
+
state.current_step += 1
|
|
76
|
+
obs = classify_observation(tool, args, result, state, read_only=not mutating)
|
|
77
|
+
state.grounded_this_turn |= obs["grounds_assertion"]
|
|
78
|
+
state.verified_this_turn |= obs["grounds_completion"]
|
|
79
|
+
if mutating:
|
|
80
|
+
state.last_mutation_step = state.current_step # a completion now needs a read AFTER this
|
|
81
|
+
|
|
82
|
+
# at every submit/conclude attempt — this must be the ONLY path to output:
|
|
83
|
+
verdict = boundary_check({"claim_type": "completion", "content": answer}, state)
|
|
84
|
+
if verdict["verdict"] == "REJECT":
|
|
85
|
+
... # surface verdict["legal_next"] to the model and continue the loop
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Note the mutation bookkeeping: without `last_mutation_step` ever being set, no
|
|
89
|
+
read can reach the verified tier and a `completion` can never be accepted —
|
|
90
|
+
that is the gate working as designed, not a bug.
|
|
91
|
+
|
|
92
|
+
`turn_loop` in [boundary.py](https://github.com/CiphemonJY/grounding-gate/blob/main/src/grounding_gate/boundary.py)
|
|
93
|
+
is the complete reference wiring (budget refill, mutation tracking, halt
|
|
94
|
+
semantics, signal mapping) — use it as the integration template. The
|
|
95
|
+
[demo](https://github.com/CiphemonJY/grounding-gate/blob/main/examples/demo.py)
|
|
96
|
+
runs the same scripted agent through an ungated and a gated loop, side by side.
|
|
97
|
+
|
|
98
|
+
## Model-class presets
|
|
99
|
+
|
|
100
|
+
Fleet variance is absorbed as integers, not prose. Pick the preset matching
|
|
101
|
+
how your model fails:
|
|
102
|
+
|
|
103
|
+
| Preset | CAP | REFILL | Strict G | For |
|
|
104
|
+
|------------|-----|--------|----------|-----|
|
|
105
|
+
| `skipper` | 5 | 2 | yes | models that hallucinate-and-skip |
|
|
106
|
+
| `diverger` | 4 | 1 | no | models that reason forever |
|
|
107
|
+
| `default` | 6 | 2 | no | everything else |
|
|
108
|
+
|
|
109
|
+
Strict G means even plain *assertions* require verified-tier grounding (a
|
|
110
|
+
post-mutation observation) — an observed-tier read is not enough. In a task
|
|
111
|
+
that never mutates anything, a strict-G agent can only exit via the typed
|
|
112
|
+
`unverified` terminal; that hard line is the point of the skipper preset, so
|
|
113
|
+
pick `default` for read-only/Q&A workloads.
|
|
114
|
+
|
|
115
|
+
## Declarative rails
|
|
116
|
+
|
|
117
|
+
A task can declare signals that must be verified before any completion is
|
|
118
|
+
accepted (`state.goal_predicates = ["tests_passed"]`). The gate never
|
|
119
|
+
interprets meaning — it only checks that a signal named `tests_passed` was
|
|
120
|
+
registered by a mapped, real command outcome. Semantic judgment stays out of
|
|
121
|
+
the floor by design.
|
|
122
|
+
|
|
123
|
+
## What the gate does NOT do
|
|
124
|
+
|
|
125
|
+
Honest scope, from the design's leak audit:
|
|
126
|
+
|
|
127
|
+
- **No semantic correctness.** A grounded claim can still be wrong (the model
|
|
128
|
+
can misread a real result). That is punted to a declared verifier tier
|
|
129
|
+
(`verify_with`), not smuggled into the floor.
|
|
130
|
+
- **Relevance can be spoofed** by a model that deliberately mentions the right
|
|
131
|
+
identifiers in an irrelevant call. The floor defends against *lazy*
|
|
132
|
+
ungroundedness, which is the overwhelmingly common failure; adversarial
|
|
133
|
+
self-deception needs the verifier tier.
|
|
134
|
+
- **Nondeterministic tools** need per-tool `normalize()` extensions or the
|
|
135
|
+
audited `novelty_exempt` allowlist — too-weak normalization quietly disables
|
|
136
|
+
the no-op defense.
|
|
137
|
+
|
|
138
|
+
## How this was built
|
|
139
|
+
|
|
140
|
+
The modules were drafted by different LLMs and adversarially reviewed before
|
|
141
|
+
assembly; the final behavior is pinned by a 19-case acceptance suite
|
|
142
|
+
([tests/test_gate.py](https://github.com/CiphemonJY/grounding-gate/blob/main/tests/test_gate.py))
|
|
143
|
+
that runs on bare Python with zero dependencies. Two review findings shaped
|
|
144
|
+
the method and are preserved in the docstrings:
|
|
145
|
+
|
|
146
|
+
- A drafting model shipped a consequence-tier bug **and authored the test that
|
|
147
|
+
ratified it** — since then, expected outcomes are authored by the reviewer,
|
|
148
|
+
never by the generator
|
|
149
|
+
([docs/module-2-classifier.md](https://github.com/CiphemonJY/grounding-gate/blob/main/docs/module-2-classifier.md)).
|
|
150
|
+
- The remaining leaks lived *between* individually-passing test cases —
|
|
151
|
+
latch-vs-assignment, halt cleared by non-qualifying calls
|
|
152
|
+
([docs/module-4-boundary.md](https://github.com/CiphemonJY/grounding-gate/blob/main/docs/module-4-boundary.md)).
|
|
153
|
+
|
|
154
|
+
Full design spec:
|
|
155
|
+
[docs/spec.md](https://github.com/CiphemonJY/grounding-gate/blob/main/docs/spec.md).
|
|
156
|
+
|
|
157
|
+
## Status & roadmap
|
|
158
|
+
|
|
159
|
+
This is the reference implementation — correct, minimal, and framework-free.
|
|
160
|
+
Planned next:
|
|
161
|
+
|
|
162
|
+
- Adapters: Claude Agent SDK hook, LangGraph middleware, OpenAI Agents SDK.
|
|
163
|
+
- A real signal-mapper module (command exit code → declared signal).
|
|
164
|
+
- Empirical preset tuning across model classes.
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Module 2 — Observation classifier (corrected)
|
|
2
|
+
|
|
3
|
+
The classifier decides whether one completed tool call `(tool, args, result)` grounds a
|
|
4
|
+
claim. A call qualifies only if **novel ∧ relevant ∧ consequence-tier-correct**.
|
|
5
|
+
|
|
6
|
+
## Review history — and why it shaped the method
|
|
7
|
+
|
|
8
|
+
The first draft of this module was produced by one model and reviewed by another. The
|
|
9
|
+
review found:
|
|
10
|
+
|
|
11
|
+
- **(C1)** The consequence tier let a plain read ground a **completion** ("I changed X")
|
|
12
|
+
when nothing had ever mutated: with `last_mutation_step = 0`, the step comparison was
|
|
13
|
+
trivially true. The fix requires a mutation to have *occurred*
|
|
14
|
+
(`last_mutation_step > 0`) before any read can count as post-mutation verification.
|
|
15
|
+
- **(C2)** The drafting model had also **authored the test case that ratified its own
|
|
16
|
+
bug** — its test #1 expected `{true, true}` for a read with no prior mutation. The
|
|
17
|
+
expectation was corrected to `{true, false}`.
|
|
18
|
+
- **(C3)** The draft recorded the novelty hash before the relevance check — a
|
|
19
|
+
novel-but-irrelevant read burned its hash and was wrongly denied credit if the same
|
|
20
|
+
read later became relevant. The hash is now recorded only after the relevance gate
|
|
21
|
+
passes.
|
|
22
|
+
|
|
23
|
+
C2 is the important lesson, and it became a standing rule for this project: **expected
|
|
24
|
+
outcomes are authored by the reviewer, never by the generator.** A model grading its own
|
|
25
|
+
work will happily certify its own bug.
|
|
26
|
+
|
|
27
|
+
## Corrected pseudocode
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
function classify_observation(tool, args, result, state) -> {grounds_assertion, grounds_completion}:
|
|
31
|
+
ret = {grounds_assertion: false, grounds_completion: false}
|
|
32
|
+
|
|
33
|
+
# 1. NOVELTY (check only; do not record yet — see C3)
|
|
34
|
+
h = null
|
|
35
|
+
if tool not in state.novelty_exempt:
|
|
36
|
+
h = hash(tool + normalize(args) + normalize(result))
|
|
37
|
+
if h in state.recent_result_hashes:
|
|
38
|
+
return ret
|
|
39
|
+
|
|
40
|
+
# 2. RELEVANCE
|
|
41
|
+
identifiers = extract_identifiers(args, result)
|
|
42
|
+
if not intersects(identifiers, state.claim_surface):
|
|
43
|
+
return ret
|
|
44
|
+
|
|
45
|
+
# record novelty only for calls that actually qualify (C3)
|
|
46
|
+
if h != null:
|
|
47
|
+
state.recent_result_hashes.add(h)
|
|
48
|
+
|
|
49
|
+
# 3. CONSEQUENCE TIER
|
|
50
|
+
if is_read_only(tool):
|
|
51
|
+
ret.grounds_assertion = true
|
|
52
|
+
# a completion needs a mutation to have OCCURRED and this read to follow it (C1)
|
|
53
|
+
if state.last_mutation_step > 0 and state.current_step > state.last_mutation_step:
|
|
54
|
+
ret.grounds_completion = true
|
|
55
|
+
|
|
56
|
+
return ret
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Acceptance cases
|
|
60
|
+
|
|
61
|
+
| # | Case | Expected |
|
|
62
|
+
|---|------|----------|
|
|
63
|
+
| T1 | read with NO prior mutation | `{true, false}` ← corrected (C1/C2) |
|
|
64
|
+
| T2 | repeated identical read (novelty defeat) | `{false, false}` |
|
|
65
|
+
| T3 | unrelated file touched (relevance defeat) | `{false, false}` |
|
|
66
|
+
| T4 | mutating call claiming its own effect (consequence defeat) | `{false, false}` |
|
|
67
|
+
| T5 | read predating the mutation | `{true, false}` |
|
|
68
|
+
| T6 | post-mutation re-read (valid verified) | `{true, true}` |
|
|
69
|
+
|
|
70
|
+
## Open implementation risks (flagged, not resolved here)
|
|
71
|
+
|
|
72
|
+
- `normalize()` must strip timestamps/rng or novelty never fires on nondeterministic
|
|
73
|
+
tools; `novelty_exempt` is a small audited allowlist.
|
|
74
|
+
- `extract_identifiers()` must be conservative — over-extraction leaks relevance,
|
|
75
|
+
under-extraction false-rejects cross-cutting work.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Module 4 — Submit-boundary choke point (corrected)
|
|
2
|
+
|
|
3
|
+
`boundary_check` is the single decision that matters: it must be the **sole path to any
|
|
4
|
+
terminal output**. `turn_loop` is the reference wiring of the whole gate around an agent.
|
|
5
|
+
|
|
6
|
+
## Review history — leaks live BETWEEN the test cases
|
|
7
|
+
|
|
8
|
+
The draft of this module passed all 8 authored boundary cases (R1–A3) on the first try —
|
|
9
|
+
`boundary_check` itself survived review unchanged. Every bug was in `turn_loop`, in the
|
|
10
|
+
seams the per-case suite didn't cover:
|
|
11
|
+
|
|
12
|
+
- **(C1)** The draft assigned grounding flags from the *last* call
|
|
13
|
+
(`verified_this_turn = obs.grounds_completion`), so any later non-completion-grade call
|
|
14
|
+
overwrote a valid verification back to `false` — false-rejecting legitimate work.
|
|
15
|
+
Flags are **latches** within a turn (`|=`), cleared only at turn boundaries.
|
|
16
|
+
- **(C2)** The draft cleared `halted` on *any* tool call — a halted model could escape via
|
|
17
|
+
a novelty-defeated no-op read, reintroducing the no-op gaming vector at loop level.
|
|
18
|
+
Halt is cleared **only by a qualifying observation** (or a typed `unverified` exit).
|
|
19
|
+
- **(C3)** The draft invented an `obs.signals` field that Module 2 does not return.
|
|
20
|
+
Signal population goes through the declarative-rails signal mapper (declared command
|
|
21
|
+
outcome → signal name), never through the classifier.
|
|
22
|
+
- **(C4)** Refused reasoning while halted must surface `legal_next` to the agent
|
|
23
|
+
(re-prompt) or the loop livelocks; refusals also decrement budget, so a halted spinner
|
|
24
|
+
still starves its way to the typed `unverified` exit.
|
|
25
|
+
|
|
26
|
+
The takeaway generalizes: a module can score 100% on its authored acceptance cases and
|
|
27
|
+
still leak — integration cases (L1–L3 below) exist precisely to cover the seams.
|
|
28
|
+
|
|
29
|
+
## Corrected pseudocode
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
function boundary_check(terminal_attempt, state):
|
|
33
|
+
ct = terminal_attempt.claim_type # none | assertion | completion | unverified
|
|
34
|
+
|
|
35
|
+
if ct == unverified: # universal escape hatch (typed, not content-matched)
|
|
36
|
+
state.halted = false
|
|
37
|
+
return { verdict: ACCEPT, legal_next: [] }
|
|
38
|
+
|
|
39
|
+
is_claim_bearing = (ct == assertion) OR (ct == completion)
|
|
40
|
+
|
|
41
|
+
if is_claim_bearing AND state.budget <= 0:
|
|
42
|
+
state.halted = true
|
|
43
|
+
return { verdict: REJECT, legal_next: [qualifying_tool_call, unverified_terminal] }
|
|
44
|
+
|
|
45
|
+
if ct == completion:
|
|
46
|
+
if NOT state.verified_this_turn:
|
|
47
|
+
reject as above
|
|
48
|
+
for sig in state.goal_predicates:
|
|
49
|
+
if sig NOT in state.verified_signals:
|
|
50
|
+
reject as above
|
|
51
|
+
state.halted = false
|
|
52
|
+
return { verdict: ACCEPT, legal_next: [] }
|
|
53
|
+
|
|
54
|
+
if ct == assertion:
|
|
55
|
+
grounded = state.verified_this_turn if state.strict_g # strict G (module 6):
|
|
56
|
+
else state.grounded_this_turn # skipper preset requires
|
|
57
|
+
if NOT grounded: # verified tier even here
|
|
58
|
+
reject as above
|
|
59
|
+
state.halted = false
|
|
60
|
+
return { verdict: ACCEPT, legal_next: [] }
|
|
61
|
+
|
|
62
|
+
return { verdict: ACCEPT, legal_next: [] } # ct == none: exempt
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`turn_loop` wiring (see [boundary.py](../src/grounding_gate/boundary.py) for the runnable
|
|
66
|
+
version): reasoning decrements budget (refused or not); tool calls are classified, latch
|
|
67
|
+
the flags, refill budget and clear halt only when qualifying, and record mutation steps;
|
|
68
|
+
terminal attempts go through `boundary_check`, and an ACCEPT is the loop's only emit.
|
|
69
|
+
|
|
70
|
+
## Integration cases the corrections cover
|
|
71
|
+
|
|
72
|
+
| # | Case | Verdict |
|
|
73
|
+
|---|------|---------|
|
|
74
|
+
| L1 | edit → re-read (verified) → novelty-defeated repeat read → completion | ACCEPT (was falsely REJECTED pre-C1) |
|
|
75
|
+
| L2 | halted → novelty-defeated repeat read → reasoning | reasoning still refused (was allowed pre-C2) |
|
|
76
|
+
| L3 | halted → reasoning × N | budget starves to typed `unverified` (was free spin pre-C4) |
|