Skip to content

Commit 11ae0a3

Browse files
fluffy314cursoragent
authored andcommitted
fix(agents): complete responses to EOS and ground critique
Continue Generator and Critic streams across chunk boundaries until model EOS, mark safety-limit truncation explicitly, and require the Critic to respect completion status and honest boundaries for open problems. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d9847ae commit 11ae0a3

8 files changed

Lines changed: 155 additions & 19 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,11 @@ Type a prompt at `prompt>`. The terminal prints allens Prefill progress, then
334334
streams `generator>` and `critic>` tokens as they arrive, followed by KV hit
335335
rate, decode tok/s, generation latency and E2E tok/s. `/quit` exits; services
336336
remain running. Reports remain redacted and appear in the Benchmarks tab.
337+
Generation uses 64-token streaming chunks and automatically continues to the
338+
model's EOS. `--max-response-tokens` defaults to 512 as an explicit safety cap;
339+
reaching it marks the stage incomplete instead of presenting a truncated answer
340+
as successful. The Critic receives the Generator completion status and must not
341+
penalize an honest statement that an open problem has no accepted proof.
337342

338343
## Rollback
339344

inference_engine/bench/prefill_fleet_report.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ def summarize_stages(stages: Sequence[dict[str, Any]]) -> dict[str, Any]:
6565
sources = {source: 0 for source in HIT_SOURCES}
6666
for stage in normalized:
6767
sources[stage["hit_source"]] += 1
68+
stop_reasons: dict[str, int] = {}
69+
for stage in normalized:
70+
reason = str(stage.get("stop_reason", "unknown"))
71+
stop_reasons[reason] = stop_reasons.get(reason, 0) + 1
6872
decode = [stage["decode_tok_s"] for stage in normalized]
6973
prefix_tokens = sum(stage["prefix_tokens"] for stage in normalized)
7074
hit_tokens = sum(
@@ -83,6 +87,10 @@ def summarize_stages(stages: Sequence[dict[str, Any]]) -> dict[str, Any]:
8387
return {
8488
"stages_total": len(normalized),
8589
"stages_failed": sum(not stage.get("ok", False) for stage in normalized),
90+
"incomplete_stages": sum(
91+
not stage.get("complete", True) for stage in normalized
92+
),
93+
"stop_reason_counts": stop_reasons,
8694
"ttft_p50_s": _median(stage["ttft_s"] for stage in normalized),
8795
"prefill_tok_s_p50": _median(
8896
stage["prefill_or_restore_tok_s"] for stage in normalized

inference_engine/network/dashboard.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ 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">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>'}
53-
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>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>${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>`}
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'}<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>'}
53+
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);
5656
let total=s.cache_bytes_used+s.cache_bytes_free,pct=total?s.cache_bytes_used/total*100:0;$('capacityLabel').textContent=`${gb(s.cache_bytes_used)} / ${gb(total)} GB`;$('capacityBar').style.width=pct+'%';

scripts/agent_gan_inference_demo.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def _infer(
4141
output_tokens: int,
4242
get_stats,
4343
on_token=None,
44+
max_response_tokens=None,
4445
):
4546
before = get_stats()
4647
started = time.perf_counter()
@@ -50,12 +51,30 @@ def _infer(
5051
append_done = time.perf_counter()
5152
first_at = None
5253
generated = []
53-
for token in s.generate(max_tokens=output_tokens):
54-
generated.append(int(token))
55-
if on_token is not None:
56-
on_token(generated)
57-
if first_at is None:
58-
first_at = time.perf_counter()
54+
response_limit = int(max_response_tokens or output_tokens)
55+
stop_reason = "unknown"
56+
while len(generated) < response_limit:
57+
before_count = len(generated)
58+
chunk = min(output_tokens, response_limit - len(generated))
59+
for token in s.generate(max_tokens=chunk):
60+
generated.append(int(token))
61+
if on_token is not None:
62+
on_token(generated)
63+
if first_at is None:
64+
first_at = time.perf_counter()
65+
stop_reason = {
66+
1: "max_tokens",
67+
2: "eos",
68+
3: "cancelled",
69+
4: "truncated",
70+
}.get(s.last_stop_reason, "unknown")
71+
if stop_reason != "max_tokens":
72+
break
73+
if len(generated) == before_count:
74+
stop_reason = "no_progress"
75+
break
76+
if len(generated) >= response_limit and stop_reason == "max_tokens":
77+
stop_reason = "client_safety_limit"
5978
done = time.perf_counter()
6079
after = get_stats()
6180
first_at = first_at or done
@@ -67,6 +86,8 @@ def _infer(
6786
"decode_s": done - append_done,
6887
"e2e_s": done - started,
6988
"delta": _delta(before, after),
89+
"stop_reason": stop_reason,
90+
"complete": stop_reason == "eos",
7091
}
7192

7293

@@ -85,6 +106,7 @@ def main() -> int:
85106
"reliably; larger values require more worker memory or timeout.",
86107
)
87108
parser.add_argument("--output-tokens", type=int, default=64)
109+
parser.add_argument("--max-response-tokens", type=int, default=512)
88110
parser.add_argument("--report", default="/tmp/kakeya-agent-gan-demo.json")
89111
parser.add_argument("--skip-ensure", action="store_true")
90112
args = parser.parse_args()
@@ -113,7 +135,9 @@ def main() -> int:
113135
"role": "system",
114136
"content": (
115137
"You are the Generator agent. Propose a technically precise "
116-
"architecture improvement. Respond with actionable reasoning."
138+
"architecture improvement. Respond with actionable reasoning. "
139+
"For open or unsolved problems, state the accepted boundary "
140+
"honestly and never fabricate a proof."
117141
),
118142
},
119143
{"role": "user", "content": task},
@@ -123,7 +147,10 @@ def main() -> int:
123147
"content": (
124148
"You are the Critic/Discriminator agent. Attack the proposal, "
125149
"identify false assumptions and bottlenecks, score it from 0 to "
126-
"10, and demand specific corrections."
150+
"10, and demand specific corrections. Do not call a response "
151+
"incomplete merely because it refuses to fabricate a solution to "
152+
"an open problem. Claim truncation only when completion_status is "
153+
"not EOS or the text is syntactically cut off."
127154
),
128155
}]
129156

@@ -139,6 +166,7 @@ def main() -> int:
139166
"agents": ["generator", "critic"],
140167
"rounds": args.rounds,
141168
"output_tokens": args.output_tokens,
169+
"max_response_tokens": args.max_response_tokens,
142170
},
143171
},
144172
)
@@ -164,6 +192,7 @@ def execute_agent(client, name, round_index, history):
164192
token_ids,
165193
args.output_tokens,
166194
get_stats,
195+
max_response_tokens=args.max_response_tokens,
167196
)
168197
text = tokenizer.decode(generated, skip_special_tokens=True)
169198
delta = actual["delta"]
@@ -174,7 +203,7 @@ def execute_agent(client, name, round_index, history):
174203
"agent": name,
175204
"round": round_index,
176205
"hit_source": "primary_hot" if delta["local_hits"] else "unknown",
177-
"ok": ok,
206+
"ok": ok and actual["complete"],
178207
"warmup_prefix_tokens": warm["prefix_tokens"],
179208
"warmup_tokens_reused": (
180209
warm["delta"]["tokens_reused"]
@@ -194,7 +223,7 @@ def execute_agent(client, name, round_index, history):
194223
)
195224
all_stages.append(stage)
196225
print(f"\n[{name.upper()} round {round_index}]\n{text}\n", flush=True)
197-
return text
226+
return text, stage
198227

199228
try:
200229
with Client(args.address) as client:
@@ -208,17 +237,19 @@ def execute_agent(client, name, round_index, history):
208237
+ critic_feedback
209238
),
210239
})
211-
proposal = execute_agent(
240+
proposal, generator_stage = execute_agent(
212241
client, "generator", round_index, generator_history,
213242
)
214243
generator_history.append({"role": "assistant", "content": proposal})
215244
critic_history.append({
216245
"role": "user",
217246
"content": (
218247
f"Architecture task:\n{task}\n\nGenerator proposal:\n{proposal}"
248+
f"\n\ncompletion_status={generator_stage['stop_reason']}; "
249+
f"complete={generator_stage['complete']}"
219250
),
220251
})
221-
critic_feedback = execute_agent(
252+
critic_feedback, _critic_stage = execute_agent(
222253
client, "critic", round_index, critic_history,
223254
)
224255
critic_history.append({

scripts/agent_gan_repl.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def _stage(name: str, warm: dict, actual: dict, text: str) -> dict:
4242
"agent": name,
4343
"round": 1,
4444
"hit_source": "primary_hot" if delta["local_hits"] else "unknown",
45-
"ok": _agent_cache_gate(warm["delta"], delta),
45+
"ok": _agent_cache_gate(warm["delta"], delta) and actual["complete"],
4646
"warmup_prefix_tokens": warm["prefix_tokens"],
4747
"warmup_tokens_reused": (
4848
warm["delta"]["tokens_reused"]
@@ -63,6 +63,7 @@ def main() -> int:
6363
parser.add_argument("--api-key-file", default="~/.kakeya/network_api_key")
6464
parser.add_argument("--tokenizer-id", required=True)
6565
parser.add_argument("--output-tokens", type=int, default=64)
66+
parser.add_argument("--max-response-tokens", type=int, default=512)
6667
parser.add_argument("--skip-ensure", action="store_true")
6768
args = parser.parse_args()
6869

@@ -121,7 +122,10 @@ def get_stats():
121122
"role": "system",
122123
"content": (
123124
"You are the Generator agent. Produce a concrete, "
124-
"technically rigorous answer. Internal run "
125+
"technically rigorous answer. For open or unsolved "
126+
"problems, state the accepted boundary honestly, "
127+
"provide rigorous context, and never fabricate a "
128+
"proof. Internal run "
125129
f"{run_nonce}."
126130
),
127131
},
@@ -149,6 +153,7 @@ def get_stats():
149153
args.output_tokens,
150154
get_stats,
151155
on_token=generator_printer,
156+
max_response_tokens=args.max_response_tokens,
152157
)
153158
generator_printer.finish()
154159
generator_text = tokenizer.decode(
@@ -176,15 +181,22 @@ def get_stats():
176181
"content": (
177182
"You are the Critic/Discriminator. Score the answer "
178183
"0-10, identify false assumptions, and propose "
179-
"specific corrections. Internal run "
184+
"specific corrections. Do not penalize a correct "
185+
"statement that an open problem has no accepted "
186+
"proof. Call an answer incomplete only when its "
187+
"completion status is not EOS or its syntax is "
188+
"visibly cut off. Internal run "
180189
f"{run_nonce}."
181190
),
182191
},
183192
{
184193
"role": "user",
185194
"content": (
186195
f"Original task:\n{prompt}\n\n"
187-
f"Generator answer:\n{generator_text}"
196+
f"Generator answer:\n{generator_text}\n\n"
197+
"Generator completion status: "
198+
f"{generator_actual['stop_reason']}; "
199+
f"complete={generator_actual['complete']}"
188200
),
189201
},
190202
]
@@ -210,6 +222,7 @@ def get_stats():
210222
args.output_tokens,
211223
get_stats,
212224
on_token=critic_printer,
225+
max_response_tokens=args.max_response_tokens,
213226
)
214227
critic_printer.finish()
215228
critic_text = tokenizer.decode(

tests/inference_engine/bench/test_prefill_fleet_report.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,17 @@ def test_summary_aggregates_sources_and_medians():
3636
summary = summarize_stages([
3737
_stage(),
3838
_stage("primary_hot_hit", "primary_hot"),
39-
{**_stage("allens_cold_restore", "allens_offload"), "ok": False},
39+
{
40+
**_stage("allens_cold_restore", "allens_offload"),
41+
"ok": False,
42+
"complete": False,
43+
"stop_reason": "client_safety_limit",
44+
},
4045
])
4146
assert summary["stages_total"] == 3
4247
assert summary["stages_failed"] == 1
48+
assert summary["incomplete_stages"] == 1
49+
assert summary["stop_reason_counts"]["client_safety_limit"] == 1
4350
assert summary["hit_source_counts"]["remote_worker"] == 1
4451
assert summary["hit_source_counts"]["primary_hot"] == 1
4552
assert summary["bytes_received"] == 3000

tests/inference_engine/bridge/test_agent_gan_demo.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from scripts.agent_gan_inference_demo import (
22
_agent_cache_gate,
3+
_infer,
34
_output_metadata,
45
)
56

@@ -23,3 +24,66 @@ def test_agent_output_report_is_redacted():
2324
assert result["output_chars"] == 20
2425
assert len(result["output_hash"]) == 64
2526
assert "output" not in result
27+
28+
29+
class Session:
30+
def __init__(self, chunks):
31+
self.chunks = list(chunks)
32+
self.last_stop_reason = None
33+
self.calls = 0
34+
35+
def __enter__(self):
36+
return self
37+
38+
def __exit__(self, *_args):
39+
pass
40+
41+
def append(self, token_ids):
42+
self.appended = list(token_ids)
43+
44+
def generate(self, *, max_tokens):
45+
tokens, reason = self.chunks[self.calls]
46+
self.calls += 1
47+
assert len(tokens) <= max_tokens
48+
yield from tokens
49+
self.last_stop_reason = reason
50+
51+
52+
class Client:
53+
def __init__(self, session):
54+
self.session = session
55+
56+
def create_session(self, **_kwargs):
57+
return self.session
58+
59+
60+
def test_infer_continues_chunks_until_eos():
61+
session = Session([([1, 2], 1), ([3], 2)])
62+
streamed = []
63+
tokens, metrics = _infer(
64+
Client(session),
65+
[],
66+
[9],
67+
2,
68+
lambda: {},
69+
on_token=lambda values: streamed.append(list(values)),
70+
max_response_tokens=10,
71+
)
72+
assert tokens == [1, 2, 3]
73+
assert streamed == [[1], [1, 2], [1, 2, 3]]
74+
assert metrics["stop_reason"] == "eos"
75+
assert metrics["complete"] is True
76+
77+
78+
def test_infer_reports_explicit_client_safety_limit():
79+
tokens, metrics = _infer(
80+
Client(Session([([1, 2], 1)])),
81+
[],
82+
[9],
83+
2,
84+
lambda: {},
85+
max_response_tokens=2,
86+
)
87+
assert tokens == [1, 2]
88+
assert metrics["stop_reason"] == "client_safety_limit"
89+
assert metrics["complete"] is False

tests/inference_engine/bridge/test_agent_gan_repl.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ def test_repl_stage_is_redacted_and_passes_cache_gate():
3131
"ttft_s": 0.2,
3232
"decode_s": 0.3,
3333
"e2e_s": 0.4,
34+
"stop_reason": "eos",
35+
"complete": True,
3436
"delta": {
3537
"local_hits": 1,
3638
"remote_jobs": 0,
@@ -43,3 +45,9 @@ def test_repl_stage_is_redacted_and_passes_cache_gate():
4345
assert stage["output_chars"] == 14
4446
assert len(stage["output_hash"]) == 64
4547
assert "output" not in stage
48+
assert not _stage(
49+
"generator",
50+
warm,
51+
{**actual, "complete": False, "stop_reason": "client_safety_limit"},
52+
"cut off",
53+
)["ok"]

0 commit comments

Comments
 (0)