Skip to content

Commit 72fd099

Browse files
fluffy314cursoragent
authored andcommitted
fix(agents): require full-context semantic Critic review
Pass the complete Generator response to Gemma and enforce zero omitted tokens so global Critic judgments cannot be produced from sampled or summarized evidence. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e60e4ee commit 72fd099

8 files changed

Lines changed: 65 additions & 76 deletions

File tree

deploy/launchd/ai.kakeya.grpc-runtime-prefill.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
<string>--cache-link-mbps</string><string>10000</string>
3939
<string>--cache-default-rtt-ms</string><string>0.55</string>
4040
<string>--remote-prefill-min-tokens</string><string>0</string>
41-
<string>--prefill-worker-timeout-s</string><string>900</string>
41+
<string>--prefill-worker-timeout-s</string><string>3600</string>
4242
<string>--prefill-policy</string><string>remote-required</string>
4343
<string>--network-http-host</string><string>127.0.0.1</string>
4444
<string>--network-http-port</string><string>8090</string>

docs/ops/distributed-prefill-kv-network.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -342,11 +342,13 @@ Generator completion status and must not penalize an honest statement that an
342342
open problem has no accepted proof.
343343
The REPL ignores external `SIGTERM`; its shell supervisor restarts signal-based
344344
exits. Only `/quit`, `/exit`, or EOF is treated as approval to stop.
345-
Generator output is always streamed in full to Terminal. To keep the 16GB
346-
allens Critic Prefill interactive, Critic receives a labeled extractive evidence
347-
window (default 64 Generator tokens: beginning + conclusion) with the omitted
348-
token count and EOS status. It must not interpret evidence-window omission as
349-
Generator truncation. Long Prefill operations emit a heartbeat every 30 seconds.
345+
Generator output is always streamed in full to Terminal and passed verbatim to
346+
the Gemma Critic. Sampling, truncation, summarization, independent chunk scores,
347+
and semantic fallback are forbidden. A global Critic score is valid only when
348+
`review_scope=full`, `critic_context_tokens=generator_full_tokens`, and
349+
`critic_omitted_tokens=0`. Long Prefill operations emit a heartbeat every 30
350+
seconds; on the 16GB allens worker, full-context Critic Prefill may take 15–25
351+
minutes.
350352
Interactive prompt templates are deterministic and contain no per-run nonce, so
351353
repeating the same task can reuse allens cold-tier and Primary hot-tier KV.
352354

inference_engine/network/dashboard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def dashboard_html() -> str:
4949
$('createRegistration').onclick=async()=>{let r=await fetch('/v1/network/nodes/register',{method:'POST',headers:writeHeaders(),body:JSON.stringify({alias:$('alias').value,address:$('address').value,region:$('region').value,role:'hybrid'})});let j=await r.json();$('pairing').textContent=r.ok?`Pairing token: ${j.pairing_token}\nExpires: ${new Date(j.expires_at*1000).toLocaleTimeString()}`:`Error: ${j.detail||r.status}`;$('pairing').classList.remove('hidden');load()};
5050
$('createGroup').onclick=async()=>{await fetch('/v1/network/groups',{method:'POST',headers:writeHeaders(),body:JSON.stringify({name:$('groupName').value,node_ids:$('groupNodes').value.split(',').map(x=>x.trim()).filter(Boolean)})});load()};
5151
function nodePosition(i,total){let a=(i/Math.max(total,1))*Math.PI*2;return {x:50+38*Math.cos(a),y:53+35*Math.sin(a)}}
52-
function phaseCards(stages){$('benchmarkPhases').innerHTML=stages.map(x=>`<div class="card"><b>${esc(x.agent?`${x.agent} R${x.round}`:x.name)}</b><p>${esc(x.hit_source)} · ${x.ok?'PASS':'FAIL'}</p><div class="muted">Stop ${esc(x.stop_reason||'n/a')} · ${x.complete===false?'INCOMPLETE':'complete'}${x.critic_omitted_tokens?`<br>Critic evidence ${x.critic_evidence_tokens}/${x.generator_full_tokens} tokens · omitted ${x.critic_omitted_tokens}`:''}<br>TTFT ${num(x.ttft_s)}s<br>Prefill/restore ${num(x.prefill_or_restore_tok_s)} tok/s<br>Decode ${num(x.decode_tok_s)} tok/s<br>Generation ${num(x.generation_latency_ms_per_token)} ms/token<br>E2E ${num(x.e2e_tok_s)} tok/s</div></div>`).join('')||'<div class="card muted">No stages yet.</div>'}
52+
function phaseCards(stages){$('benchmarkPhases').innerHTML=stages.map(x=>`<div class="card"><b>${esc(x.agent?`${x.agent} R${x.round}`:x.name)}</b><p>${esc(x.hit_source)} · ${x.ok?'PASS':'FAIL'}</p><div class="muted">Stop ${esc(x.stop_reason||'n/a')} · ${x.complete===false?'INCOMPLETE':'complete'}${x.review_scope?`<br>Critic review ${esc(x.review_scope)} · context ${x.critic_context_tokens}/${x.generator_full_tokens} tokens · omitted ${x.critic_omitted_tokens||0}`:''}<br>TTFT ${num(x.ttft_s)}s<br>Prefill/restore ${num(x.prefill_or_restore_tok_s)} tok/s<br>Decode ${num(x.decode_tok_s)} tok/s<br>Generation ${num(x.generation_latency_ms_per_token)} ms/token<br>E2E ${num(x.e2e_tok_s)} tok/s</div></div>`).join('')||'<div class="card muted">No stages yet.</div>'}
5353
async function showBenchmark(id){let r=await fetch('/v1/network/benchmarks/'+encodeURIComponent(id)).then(x=>x.json());phaseCards(r.stages||[]);$('benchmarkDetail').innerHTML=`<b>${esc(r.id)} · ${esc(r.status)}</b><p class="muted">${esc(r.kind)} · ${new Date(r.started_at*1000).toLocaleString()}</p><table><thead><tr><th>Phase</th><th>Source</th><th>Stop</th><th>TTFT</th><th>Prefill/restore</th><th>Decode</th><th>Latency/token</th><th>E2E</th></tr></thead><tbody>${(r.stages||[]).map(x=>`<tr><td>${esc(x.agent?`${x.agent} R${x.round}`:x.name)}</td><td>${esc(x.hit_source)}</td><td>${esc(x.stop_reason||'n/a')}</td><td>${num(x.ttft_s)}s</td><td>${num(x.prefill_or_restore_tok_s)}</td><td>${num(x.decode_tok_s)}</td><td>${num(x.generation_latency_ms_per_token)}ms</td><td>${num(x.e2e_tok_s)}</td></tr>`).join('')}</tbody></table>`}
5454
async function load(){let [s,n,g,live,runs]=await Promise.all([fetch('/v1/network/summary').then(r=>r.json()),fetch('/v1/network/nodes').then(r=>r.json()),fetch('/v1/network/groups').then(r=>r.json()),fetch('/v1/network/benchmarks/live').then(r=>r.json()),fetch('/v1/network/benchmarks?limit=20').then(r=>r.json())]);
5555
$('online').textContent=s.online_nodes;$('groupCount').textContent=s.groups;$('tokens').textContent=fmt(s.completed_tokens);$('hitRate').textContent=(s.kv_hit_rate*100).toFixed(0)+'%';$('cache').textContent=gb(s.cache_bytes_used+s.cache_bytes_free)+' GB';let p=s.prefill||{};$('remoteJobs').textContent=fmt(p.remote_jobs);$('remoteHits').textContent=fmt(p.remote_hits);$('reusedTokens').textContent=fmt(p.tokens_reused);$('evictions').textContent=fmt(s.cache_evictions);$('publishFailures').textContent=fmt(p.publish_failures);

scripts/agent_gan_inference_demo.py

Lines changed: 18 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -34,33 +34,13 @@ def _output_metadata(text: str) -> dict:
3434
}
3535

3636

37-
def build_critic_evidence(tokenizer, text: str, max_tokens: int) -> tuple[str, dict]:
38-
if max_tokens <= 0:
39-
raise ValueError("critic evidence token budget must be > 0")
37+
def build_critic_context(tokenizer, text: str) -> tuple[str, dict]:
4038
full_ids = tokenizer.encode(text, add_special_tokens=False)
41-
if len(full_ids) <= max_tokens:
42-
return text, {
43-
"generator_full_tokens": len(full_ids),
44-
"critic_evidence_tokens": len(full_ids),
45-
"critic_omitted_tokens": 0,
46-
}
47-
head_count = max_tokens // 2
48-
tail_count = max_tokens - head_count
49-
head = tokenizer.decode(full_ids[:head_count], skip_special_tokens=True)
50-
tail = tokenizer.decode(full_ids[-tail_count:], skip_special_tokens=True)
51-
omitted = len(full_ids) - max_tokens
52-
evidence = (
53-
"[BEGIN GENERATOR EVIDENCE]\n"
54-
f"{head}\n"
55-
f"[... {omitted} generator tokens omitted from Critic context ...]\n"
56-
f"{tail}\n"
57-
"[END GENERATOR EVIDENCE]"
58-
)
59-
evidence_tokens = len(tokenizer.encode(evidence, add_special_tokens=False))
60-
return evidence, {
39+
return text, {
6140
"generator_full_tokens": len(full_ids),
62-
"critic_evidence_tokens": evidence_tokens,
63-
"critic_omitted_tokens": omitted,
41+
"critic_context_tokens": len(full_ids),
42+
"critic_omitted_tokens": 0,
43+
"review_scope": "full",
6444
}
6545

6646

@@ -154,12 +134,11 @@ def main() -> int:
154134
default=0,
155135
help="Optional client response cap; 0 means generate until model EOS.",
156136
)
157-
parser.add_argument("--critic-evidence-tokens", type=int, default=128)
158137
parser.add_argument("--report", default="/tmp/kakeya-agent-gan-demo.json")
159138
parser.add_argument("--skip-ensure", action="store_true")
160139
args = parser.parse_args()
161-
if min(args.rounds, args.output_tokens, args.critic_evidence_tokens) <= 0:
162-
raise SystemExit("rounds, output-tokens and critic evidence must be > 0")
140+
if min(args.rounds, args.output_tokens) <= 0:
141+
raise SystemExit("rounds and output-tokens must be > 0")
163142

164143
from kakeya import Client
165144
from transformers import AutoTokenizer
@@ -199,9 +178,8 @@ def main() -> int:
199178
"incomplete merely because it refuses to fabricate a solution to "
200179
"an open problem. Claim truncation only when completion_status is "
201180
"not EOS or the text is syntactically cut off."
202-
" You receive an explicitly bounded evidence window; omitted "
203-
"middle tokens are a transport constraint, not evidence that the "
204-
"Generator itself failed to complete."
181+
" Review the complete Generator response as one semantic argument. "
182+
"Do not sample, summarize, or infer claims from partial text."
205183
),
206184
}]
207185

@@ -293,16 +271,21 @@ def execute_agent(client, name, round_index, history, extra_metrics=None):
293271
client, "generator", round_index, generator_history,
294272
)
295273
generator_history.append({"role": "assistant", "content": proposal})
296-
evidence, evidence_metrics = build_critic_evidence(
274+
critic_context, context_metrics = build_critic_context(
297275
tokenizer,
298276
proposal,
299-
args.critic_evidence_tokens,
300277
)
278+
if (
279+
critic_context != proposal
280+
or context_metrics["critic_omitted_tokens"] != 0
281+
or context_metrics["review_scope"] != "full"
282+
):
283+
raise RuntimeError("Critic full-context invariant violated")
301284
critic_history.append({
302285
"role": "user",
303286
"content": (
304287
f"Architecture task:\n{task}\n\n"
305-
f"Generator evidence window:\n{evidence}"
288+
f"Complete Generator response:\n{critic_context}"
306289
f"\n\ncompletion_status={generator_stage['stop_reason']}; "
307290
f"complete={generator_stage['complete']}"
308291
),
@@ -312,7 +295,7 @@ def execute_agent(client, name, round_index, history, extra_metrics=None):
312295
"critic",
313296
round_index,
314297
critic_history,
315-
extra_metrics=evidence_metrics,
298+
extra_metrics=context_metrics,
316299
)
317300
critic_history.append({
318301
"role": "assistant",

scripts/agent_gan_repl.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from scripts.agent_gan_inference_demo import (
1515
_agent_cache_gate,
1616
_infer,
17-
build_critic_evidence,
17+
build_critic_context,
1818
)
1919
from scripts.benchmark_prefill_architecture import (
2020
_ensure_services,
@@ -61,7 +61,7 @@ def build_generator_messages(prompt: str) -> list[dict[str, str]]:
6161

6262
def build_critic_messages(
6363
prompt: str,
64-
evidence: str,
64+
generator_response: str,
6565
*,
6666
stop_reason: str,
6767
complete: bool,
@@ -72,14 +72,14 @@ def build_critic_messages(
7272
"content": (
7373
"Score the answer 0-10, identify false claims, and give "
7474
"specific corrections. Do not penalize an honest statement "
75-
"that an open problem is unsolved. Evidence is intentionally "
76-
"bounded; omitted tokens do not imply truncation."
75+
"that an open problem is unsolved. Review the complete response "
76+
"as one semantic argument; do not sample or summarize it."
7777
),
7878
},
7979
{
8080
"role": "user",
8181
"content": (
82-
f"Task:\n{prompt}\n\nEvidence:\n{evidence}\n\n"
82+
f"Task:\n{prompt}\n\nComplete response:\n{generator_response}\n\n"
8383
f"Completion: {stop_reason}; complete={complete}"
8484
),
8585
},
@@ -172,11 +172,10 @@ def main() -> int:
172172
default=0,
173173
help="Optional client response cap; 0 means generate until model EOS.",
174174
)
175-
parser.add_argument("--critic-evidence-tokens", type=int, default=64)
176175
parser.add_argument("--skip-ensure", action="store_true")
177176
args = parser.parse_args()
178-
if min(args.output_tokens, args.critic_evidence_tokens) <= 0:
179-
raise SystemExit("output-tokens and critic-evidence-tokens must be > 0")
177+
if args.output_tokens <= 0:
178+
raise SystemExit("output-tokens must be > 0")
180179

181180
from kakeya import Client
182181
from transformers import AutoTokenizer
@@ -284,14 +283,19 @@ def get_stats():
284283
body={"stages": [generator_stage]},
285284
)
286285

287-
evidence, evidence_metrics = build_critic_evidence(
286+
critic_context, context_metrics = build_critic_context(
288287
tokenizer,
289288
generator_text,
290-
args.critic_evidence_tokens,
291289
)
290+
if (
291+
critic_context != generator_text
292+
or context_metrics["critic_omitted_tokens"] != 0
293+
or context_metrics["review_scope"] != "full"
294+
):
295+
raise RuntimeError("Critic full-context invariant violated")
292296
critic_messages = build_critic_messages(
293297
prompt,
294-
evidence,
298+
critic_context,
295299
stop_reason=generator_actual["stop_reason"],
296300
complete=generator_actual["complete"],
297301
)
@@ -330,7 +334,7 @@ def get_stats():
330334
critic_warm,
331335
critic_actual,
332336
critic_text,
333-
extra_metrics=evidence_metrics,
337+
extra_metrics=context_metrics,
334338
)
335339
if not critic_stage["ok"] and not telemetry_state["degraded"]:
336340
raise RuntimeError("Critic KV gate failed")

tests/inference_engine/bridge/test_agent_gan_demo.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
_agent_cache_gate,
33
_infer,
44
_output_metadata,
5-
build_critic_evidence,
5+
build_critic_context,
66
)
77

88

@@ -98,18 +98,10 @@ def decode(self, token_ids, **_kwargs):
9898
return "".join(chr(token) for token in token_ids)
9999

100100

101-
def test_critic_evidence_is_bounded_and_explicit_about_omission():
102-
evidence, metrics = build_critic_evidence(CharTokenizer(), "abcdefghij", 4)
103-
assert "ab" in evidence and "ij" in evidence
104-
assert "6 generator tokens omitted" in evidence
101+
def test_critic_context_preserves_complete_generator_response():
102+
context, metrics = build_critic_context(CharTokenizer(), "abcdefghij")
103+
assert context == "abcdefghij"
105104
assert metrics["generator_full_tokens"] == 10
106-
assert metrics["critic_omitted_tokens"] == 6
107-
full, full_metrics = build_critic_evidence(CharTokenizer(), "abc", 4)
108-
assert full == "abc"
109-
assert full_metrics["critic_omitted_tokens"] == 0
110-
try:
111-
build_critic_evidence(CharTokenizer(), "abc", 0)
112-
except ValueError:
113-
pass
114-
else:
115-
raise AssertionError("expected evidence budget validation")
105+
assert metrics["critic_context_tokens"] == 10
106+
assert metrics["critic_omitted_tokens"] == 0
107+
assert metrics["review_scope"] == "full"

tests/inference_engine/bridge/test_agent_gan_repl.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def test_prefill_heartbeat_reports_elapsed_progress(capsys):
9797
assert "Critic Prefill still running" in output
9898

9999

100-
def test_stage_includes_evidence_window_metrics():
100+
def test_stage_includes_full_context_metrics():
101101
warm = {
102102
"prefix_tokens": 10,
103103
"e2e_s": 1,
@@ -124,9 +124,16 @@ def test_stage_includes_evidence_window_metrics():
124124
warm,
125125
actual,
126126
"ok",
127-
extra_metrics={"critic_omitted_tokens": 100},
127+
extra_metrics={
128+
"generator_full_tokens": 100,
129+
"critic_context_tokens": 100,
130+
"critic_omitted_tokens": 0,
131+
"review_scope": "full",
132+
},
128133
)
129-
assert stage["critic_omitted_tokens"] == 100
134+
assert stage["critic_context_tokens"] == 100
135+
assert stage["critic_omitted_tokens"] == 0
136+
assert stage["review_scope"] == "full"
130137

131138

132139
def test_telemetry_timeout_warns_without_stopping_inference(
@@ -148,13 +155,13 @@ def test_interactive_prompts_are_deterministic_for_kv_reuse():
148155
generator_b = build_generator_messages("prove RH")
149156
critic_a = build_critic_messages(
150157
"prove RH",
151-
"bounded evidence",
158+
"complete generator response",
152159
stop_reason="eos",
153160
complete=True,
154161
)
155162
critic_b = build_critic_messages(
156163
"prove RH",
157-
"bounded evidence",
164+
"complete generator response",
158165
stop_reason="eos",
159166
complete=True,
160167
)
@@ -163,4 +170,5 @@ def test_interactive_prompts_are_deterministic_for_kv_reuse():
163170
combined = repr(generator_a + critic_a)
164171
assert "Internal run" not in combined
165172
assert "open problem" in combined
166-
assert "omitted tokens do not imply truncation" in combined
173+
assert "Review the complete response" in combined
174+
assert "do not sample or summarize" in combined

tests/inference_engine/bridge/test_prefill_worker_launchd.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def test_two_mac_deployment_uses_allens_as_prefill_only():
4949
in plist
5050
)
5151
assert (
52-
"<string>--prefill-worker-timeout-s</string><string>900</string>"
52+
"<string>--prefill-worker-timeout-s</string><string>3600</string>"
5353
in plist
5454
)
5555
assert (

0 commit comments

Comments
 (0)