Skip to content

Manage specialist token spend: bound the proposal set, stop discarding it, and name a stop condition - #1233

Open
ZhengGong-amd wants to merge 11 commits into
mainfrom
feat/zgong/token-curation-3
Open

Manage specialist token spend: bound the proposal set, stop discarding it, and name a stop condition#1233
ZhengGong-amd wants to merge 11 commits into
mainfrom
feat/zgong/token-curation-3

Conversation

@ZhengGong-amd

@ZhengGong-amd ZhengGong-amd commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Specialist sub-agents are the largest line in a session's LLM bill. Three mechanisms were spending that budget without a matching return. Prompt and state layer only — no execution semantics, no new gate.

Numbers from 38 archived sessions (801 proposals, 1,487 benchmarked variants) plus the 101-session cost audit.

Where the spend goes

Most output is never consumed. 105 of 801 config proposals ever reached a grid — 13.1%. Not dropped by judgement: a proposal_set lives only inside a delegated_result, which the inbox renders as one status line, and that tail is lossy by design. The one domain rendered in full (research_scout, via its own SEED block) converted at 45.2%; every other domain sat near 4%. Quality does not explain it — among proposals that were benchmarked, scout's KEEP rate is 7.0% against serving's 22.2%.

The ceiling was read as a target. proposal_set had a maximum and no target. After the maximum was restated in Section 1: 0 of 188 runs exceeded it, 24.5% landed exactly on it (up from 13.5%). Each surplus entry costs generation tokens, a Critic review, and a slot on a serial lane.

Nothing said when to stop. 170 runs (12.6%) returned empty: true at a median of 46 turns vs 52 for productive ones — same effort, no output, $2,089 (20.6% of specialist spend). The wall clock was the only bound, and the cold-start branch — which fires exactly when there are no priors — opened by forbidding an empty result, then walked it back a paragraph later.

What changes

Proposal set → 2 as the norm, 4 as the hard cap, on both surfaces the specialist reads, with the direction stated: one real proposal beats two padded, empty=true beats one padded. An output-based stop is named alongside the depth encouragement that was previously unopposed. The cold-start contradiction collapses into the rule both its paragraphs already agreed on — a coin-flip proposal is worse than none.

A ranked queue of untested proposals. New === Untested proposals (current cycle) === block: every executable proposal this cycle that no explore round has benchmarked, ranked by gap severity, capped at 12. Derived from specialist_rounds + explore_search — no new state, no migration.

Identity is the subtle part. The executor keys explore_search["tested"] on the variant folded together with the stack's removal controls, so hashing a proposal alone never matches the ledger — and 16.9% of ledger rows carry such a control. Both sides now go through one helper, with the executor's existing fingerprints pinned in a golden test so a resumed session cannot be re-keyed. The rendered line carries remove_args / unset_envs / args_mode / atomic: 38.6% of proposals are atomic and must be dispatched verbatim, and a removal-only proposal would otherwise render as two empty fields. Rendered every turn but EXPLORE-only; the scout block gives up its proposal half so nothing renders twice.

Grid bounded to target 4, max 6. It had a cap until #486 removed it for a resource-derived bound, but explore reserves benchmark_lane, never research_lane, so that bound never reached it. What remained was the session deadline truncating from the end of the grid rather than the bottom of the ranking; 20-variant rounds followed. Prompt-only — reversing #486 with a gate needs its own justification.

What it does not do

  • No KEEP-rate promise. Benchmarked specialist proposals KEEP at 8.5%, global explore at 8.7%. The serial lane's throughput is fixed; this changes which proposals get sampled, not how many. KEEP rate is a guardrail here: if grids widen and it drops, the breadth is buying noise.
  • No runner-level early stop. The prompt names a stop condition; it cannot enforce one. Cutting a round after N turns without a finding belongs in the runner loop.
  • No gate change, no auto-materialisation, no new context tool, no change to integrate_patch (already closed-loop: 50 patch-bearing specialists → 97 tasks, 1 undecided).

Side-fixes

Neither is fixed on main as of df82cb27a; both verified against a pristine checkout.

  • selfcheck_gates.py removed — arrived via fix: hold the session to its wall-clock budget #1171 beside a one-hunk baseline.py fix, calls itself a throwaway in its first line, zero references, only module at the root of a src-layout tree.
  • Lint backlog clearedruff check . reported 13 errors on main and therefore here, none from this branch. Eleven autofixed. Two were not: forge_gemm_tuning imported truthy behind a sys.path insert/pop pair that existed only for it (whole block goes; sys/Path keep other callers), and build_actions imported re beside its regexes rather than at the top — not the cycle-safe late import the per-file E402 list is for, so it moves up instead of joining it.

Notes

40 new tests; each commit independently green under bisect. ruff check . clean.

ZhengGong-amd and others added 5 commits August 19, 2026 06:44
… to 4

The ceiling was the only number the prompt gave, and it became the target:
after the Section 1 reminder landed, no run exceeded 6 but a quarter of them
returned exactly 6. "Fewer is better than padding" did not carry, because it
named no alternative.

State a target in both places the specialist reads before it starts working,
and give it a test for going past the target rather than a preference. Every
extra entry costs a Critic review and a slot on the serial benchmark queue,
so say that too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A specialist's config proposals reached the grid author exactly once, inside a
delegated_result the inbox renders as a single status line, and the inbox tail
is lossy. Only the research scout's proposals were ever rendered in full, and
they are the only ones that got benched at any rate: 45% against 4% for
everything else, while converting at a lower KEEP rate once benched. The
selection was following the rendering channel, not the evidence.

Derive the queue instead of storing it. specialist_rounds already persists the
proposals and explore_search already records what was benched, so the block is
a projection of two ledgers that were both already there.

Identity is the subtle half. The executor keys explore_search on the variant's
args and envs folded together with the union of its own removal controls and
the stack's, so a proposal carrying remove_args hashes differently once the
stack contributes its own. Hashing the proposal alone would silently never
match the ledger, and one in six ledger rows carries such a control. Both
sides now compute it through one helper, with the executor's pre-existing
values pinned in a test so the shared path cannot re-key a resumed session.

The line carries remove_args, unset_envs, args_mode and atomic rather than
just args and envs: a removal-only proposal would otherwise render as two
empty fields and read as a no-op, and two in five proposals are atomic, which
the grid author is required to dispatch without re-deriving.

Rendered on every turn rather than only on a seed, since a queue seen once is
the amnesia this replaces, but only in EXPLORE, the sole phase where explore
is proposable. Scoped to the current macro-cycle. The research-scout block
gives up its proposal half to avoid rendering them twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… grid

Both decision lists told the author that "the proposal_set drives the next
explore grid" when an explore round had just finished. Neither half was true:
the proposal_set was never rendered, and finishing a round has nothing to do
with what a specialist produced. Name the block that now carries them.

Give the grid a size while we are here. It had one until the per-round breadth
caps were removed in favour of a resource-derived bound, but explore reserves
server_lifecycle and benchmark_lane, never research_lane, so the bound that
was supposed to take over does not reach it. What is left is the session
deadline skipping whatever the round cannot finish, from the end of the grid
rather than the bottom of the ranking. Rounds of twenty variants followed.

State the target and the ceiling on both surfaces the author reads, with the
serial lane cost that makes them what they are. The removed gate's own hint
told the author to defer runners-up to a later round, which only means
something now that a later round can still see them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…way to stop

The ceiling had already become the target once: after it was restated in
Section 1, no run exceeded it and a quarter landed exactly on it. A target
states the same invitation more directly, so say which direction it binds --
one real proposal is a better round than two padded, and an empty one is
better than a single padded entry.

Nothing told the specialist when to stop looking. Section 1 spent its budget
encouraging depth, Section 2a said turns are not the stop signal, and the only
remaining bound was the wall clock, which is how a round with nothing to show
still spends its full hour. Name the other stop: rounds that stop yielding
findings are done.

The cold-start branch fires exactly when there are no priors, which is when
having nothing to propose is most likely, and it opened by forbidding an empty
set before walking it back a paragraph later. Lead with the rule the two
paragraphs agreed on and drop the contradiction. Its fallback bullets live in
Section 1 whether or not the dispatch has a domain focus block, so point there
rather than at a block a free-form specialist never gets.

Also corrects the numbers this commit's predecessor claimed but never applied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ZhengGong-amd
ZhengGong-amd requested a review from a team as a code owner August 19, 2026 09:49
Comment thread src/hyperloom/orchestrator/state/_shared_state/render.py Fixed
ZhengGong-amd and others added 3 commits August 19, 2026 09:55
Committed alongside a one-hunk baseline fix, and the file says what it is in
its own first line: a scratch script that prints ok/PREDICTED while sweeping
the phase-pricing helpers for disagreements. Nothing imports it, no workflow
or config names it, and it is the only module at the root of a src-layout
tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eleven unused imports, a missing trailing newline, and one E402, all present
on main and none of them from this branch's changes. A gate that is already
red cannot report on anything.

Two needed more than the autofix. forge_gemm_tuning imported truthy behind a
sys.path insert/pop pair whose only purpose was that import, so the whole
block goes rather than leaving the path dance wrapped around nothing; sys and
Path keep other callers. build_actions imported re beside the regexes it
builds rather than at the top, which is not the cycle-safe late import the
per-file E402 list is for, so it moves up instead of joining that list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

CI E2E report — ❌ Failed

item value
result ❌ Failed
model Qwen/Qwen3-0.6B (dense)
resources 1× GPU, TP=1
PR branch feat/zgong/token-curation-3
commit ba909e0ef3b2ee806a74ee40e4b6c2382b6f9cc4
session_id c52a9620-79a2-405b-80e0-48d864a62496
queue → dispatch 0s
run time 1m 3s
total 1m 3s
reason platform reported Failed: container hyperloom exited with code 1 (Error): [hl] CA bundle: /shared_nfs/haiskong/hyperloom-slurm/amd-ca-combined.pem
detail `platform reported Failed: container hyperloom exited with code 1 (Error): [hl] CA bundle: /shared_nfs/haiskong/hyperloom-slurm/amd-ca-combined.pem

details

ZhengGong-amd and others added 3 commits August 20, 2026 03:43
The parser space-joins a list-form extra_args because the LLM sometimes emits
the flags as JSON and str(list) yields a repr the server rejects. The queue
built its own identity with a bare str(), so the same proposal hashed two ways:
it never matched the ledger it was meant to be filtered against, and the line
it rendered carried the repr back to whoever copied it into a grid.

Move the coercion next to the identity it feeds and let the parser build its
variant from the same projection, which also drops the duplicate alias and
list handling it kept alongside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ger key

The ledger key folds in whatever removal controls the stack carried when the
round opened, and a KEEP mid-round rewrites those. Every candidate benched
before that KEEP then hashes differently from what the queue recomputes off
the finished current_best, so the queue offers them again as untried. Around
one session in eight ends with a stack carrying such a control.

The queue asks a narrower question than the executor's in-round dedup does --
whether these flags have been tried, not whether this variant collides with
another under one base -- and the ledger rows already store the variant's own
fields. Match on those and the stack drops out of the comparison entirely,
along with the base plumbing the renderer needed to reproduce it.

Two smaller ones on the same path: a proposal with no name was offered as
executable although the grid parser skips unnamed entries, so it now gets the
same derived name the multi-node materialiser gives it; and a non-numeric
cycle raised out of prompt assembly, which the shared int coercion settles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hey were

The two decision lists called the queue "every executable proposal", which
reads as exhaustive next to a block that truncates and prints the count of
what it withheld. And two comments still describe the cold-start branch as
the thing that stops a specialist returning an empty set, which is the rule
that branch now states the other way round.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants