Skip to content

Commit a74cf2a

Browse files
HaibaraAi137claude
andcommitted
Attack-vector market: parallel specialists pick structurally different angles each round
Two specialists of the same role kind now coordinate via a 2-step protocol at the start of every specialist round, so they pursue genuinely different attack vectors instead of producing near-duplicates. Phase 1 (intent, parallel): each specialist emits a 1-shot JSON proposal naming the angle it plans to take, with confidence and a list of vectors it would pivot to. Phase 2 (referee, single call): the Director sees all intents in one frame and reconciles overlaps, asking specialists to pivot based on their own would_pivot_if list. Falls back gracefully (each specialist gets first choice) when either phase fails. Why this design: Anthropic's "Building effective agents" reports the biggest multi-agent gains come from sectioning (different attack vectors) rather than voting (same task, different samples). Wang et al.'s self-consistency requires a votable answer, useless for free-form derivations. Du et al.'s debate works but converges via sycophantic mode collapse on long horizons. Method-specialization with a referee threads the needle. Implementation: - src/halfseed/agents/attack_market.py — AttackIntent / AttackAssignment / AttackMarketResult types + collect_intents + referee_intents primitives, completely architecture-agnostic. - _run_specialist_round now returns (artifacts, market_result | None). Market only fires when ≥2 enabled specialists share a role kind. Each specialist's task prompt gets the assigned vector injected verbatim. After every market round, _append_attack_market_note appends a HALFSEED- NOTE block to paper.tex so a human reading source can audit which vectors got assigned and why. - New SSE event "attack_market_done" carrying {round, assigned, fell_back}. - Validator surfaces "parallel_specialists_engaged" as INFO (not warn) so users see ≥2 same-kind specialists is supported, not a misconfiguration. - paper.tex.j2: removed the legacy "Result 1, Result 2..." artifact-dump fallback path and the Supporting Artifacts appendix. Sections now emit a single \halfseedTODO[derive|data] placeholder when no Writer prose exists, so missing content surfaces as a TODO instead of a bookkeeping log. Old artifact data still available via the SQLite store and API. - 14 new tests in tests/test_attack_market.py covering intent/referee/ fallback/result rendering + 2 integration tests with a scripted backend driving the full _run_specialist_round path. Verified live on heavyoperatorstatistics iter-7: 2 rounds, 4 specialists, 8 intent calls + 2 referee calls. Round 1 assigned distinct vectors (perturbation expansion / RMT prediction / literature survey / ED sampling); Round 2 EVOLVED them based on the round-1 output (saddle-point / replica trick). max_confidence rose from ~0.8 in earlier iterations to 0.95. Two HALFSEED-NOTE blocks landed in paper.tex with the full attack-vector log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 00ae245 commit a74cf2a

8 files changed

Lines changed: 1260 additions & 107 deletions

File tree

frontend/src/api/sse.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const knownKinds = [
2828
"architecture_loaded",
2929
"plan_ready",
3030
"round_started",
31+
"attack_market_done",
3132
"specialist_done",
3233
"skeptic_done",
3334
"revision_done",

src/halfseed/agents/attack_market.py

Lines changed: 460 additions & 0 deletions
Large diffs are not rendered by default.

src/halfseed/architecture_validate.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,31 @@ def validate_architecture(arch: ProjectArchitecture) -> ValidationReport:
171171
),
172172
)
173173
)
174+
elif len(specialists) >= 2:
175+
# Detect parallel specialists (≥2 sharing a role kind isn't a
176+
# warning, it's the FEATURE — surface as an info note so users
177+
# know the attack-vector market kicked in).
178+
kind_counts: dict[str, int] = {}
179+
for s in specialists:
180+
k = resolve_kind(s)
181+
kind_counts[k] = kind_counts.get(k, 0) + 1
182+
for kind, count in sorted(kind_counts.items()):
183+
if count < 2:
184+
continue
185+
issues.append(
186+
ValidationIssue(
187+
level=ISSUE_LEVEL_INFO,
188+
code="parallel_specialists_engaged",
189+
message=(
190+
f"{count} agents share role kind={kind!r}. The "
191+
f"attack-vector market will run before each "
192+
f"specialist round so they pick structurally "
193+
f"different angles (different attack vectors). "
194+
f"You'll see one extra LLM call per specialist + "
195+
f"one Director referee call per round."
196+
),
197+
)
198+
)
174199

175200
# 6) If a skeptic exists, at least one specialist should have an edge to
176201
# it — otherwise the revision round will be empty.

src/halfseed/publish/templates/paper.tex.j2

Lines changed: 10 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -220,51 +220,20 @@
220220

221221
\section{Setup and Assumptions}
222222
<% if section_bodies.setup %><< section_bodies.setup >>
223-
<% elif assumptions %><% for assumption in assumptions %>
224-
\paragraph{<< loop.index >>.} << assumption | latex_escape >>
225-
<% endfor %><% else %>
226-
\todo{No structured assumptions captured yet.}
223+
<% else %>
224+
\halfseedTODO[derive]{id="bootstrap-setup", what="Define notation and assumptions for this project.", needs="Writer prose grounded in current research material"}
227225
<% endif %>
228226

229227
\section{Analytical Results}
230228
<% if section_bodies.analytical %><< section_bodies.analytical >>
231-
232-
<% elif analytical_artifacts %><% for artifact in analytical_artifacts %>
233-
\subsection{Result << loop.index >>}
234-
<< artifact.claim | latex_paragraph >>
235-
236-
<% if artifact.evidence %>\paragraph{Evidence.} << artifact.evidence | join_evidence | latex_escape >>
237-
238-
<% endif %><% if artifact.failure_modes %>\paragraph{Failure modes.} << artifact.failure_modes | join_evidence | latex_escape >>
239-
240-
<% endif %>\paragraph{Confidence.} << "%.2f" | format(artifact.confidence) >>
241-
242-
<% endfor %><% else %>
243-
\todo{No analytical artifacts have reached the paper yet.}
229+
<% else %>
230+
\halfseedTODO[derive]{id="bootstrap-analytical", what="Write the analytical results section as integrated prose.", needs="Writer prose grounded in current research material"}
244231
<% endif %>
245-
<# When the Writer wrote the analytical section as prose it should weave
246-
any necessary math inline; only fall back to dumping all artifact
247-
equations en masse when there is no Writer prose to anchor them to.
248-
The old behavior of always appending 16 \begin{equation*} blocks was
249-
the main reason the rendered paper felt like a bullet dump. #>
250-
<% if not section_bodies.analytical %><% for artifact in analytical_artifacts %><% for eq in artifact.equations %>\begin{equation*}
251-
\halfseedeq{<< eq | latex_equation >>}
252-
\end{equation*}
253-
<% endfor %><% endfor %><% endif %>
254232

255233
\section{Numerical Investigations}
256234
<% if section_bodies.numerical %><< section_bodies.numerical >>
257-
258-
<% elif numerical_artifacts %><% for artifact in numerical_artifacts %>
259-
\subsection{Result << loop.index >>}
260-
<< artifact.claim | latex_paragraph >>
261-
262-
<% if artifact.evidence %>\paragraph{Evidence.} << artifact.evidence | join_evidence | latex_escape >>
263-
264-
<% endif %>\paragraph{Confidence.} << "%.2f" | format(artifact.confidence) >>
265-
266-
<% endfor %><% else %>
267-
\todo{No numerical artifacts have reached the paper yet.}
235+
<% else %>
236+
\halfseedTODO[data]{id="bootstrap-numerical", what="Write the numerical investigations section as integrated prose.", needs="Writer prose grounded in current research material"}
268237
<% endif %>
269238
<# Sandbox-produced figures rendered after the numerical text. We pin them
270239
here (not in an appendix) so they appear close to the prose that
@@ -285,62 +254,19 @@
285254

286255
\section{Prior Art}
287256
<% if section_bodies.literature %><< section_bodies.literature >>
288-
289-
<% elif literature_artifacts %><% for artifact in literature_artifacts %>
290-
\paragraph{Reference << loop.index >>.} << artifact.claim | latex_paragraph >>
291-
<% if artifact.evidence %>\textit{Source:} << artifact.evidence | join_evidence | latex_escape >>.
292-
293-
<% endif %>
294-
<% endfor %><% else %>
295-
\todo{Literature pass has not yet identified concrete prior work.}
257+
<% else %>
258+
\halfseedTODO[derive]{id="bootstrap-literature", what="Survey relevant prior work and place this paper in context.", needs="Writer prose grounded in current literature artifacts"}
296259
<% endif %>
297260

298261
\section{Discussion}
299262
<% if section_bodies.discussion %><< section_bodies.discussion >>
300-
<% else %><< synthesis | latex_paragraph >>
263+
<% else %>
264+
\halfseedTODO[derive]{id="bootstrap-discussion", what="Discuss implications, limitations, and open questions.", needs="Writer prose grounded in current research material"}
301265
<% endif %>
302266

303-
<% if open_risks %>\subsection{Open risks}
304-
\begin{itemize}
305-
<% for risk in open_risks | split_numbered %> \item << risk | latex_escape >>
306-
<% endfor %>\end{itemize}
307-
308-
<% endif %><% if suggested_next_steps %>\subsection{Suggested next steps}
309-
\begin{itemize}
310-
<% for step in suggested_next_steps | split_numbered %> \item << step | latex_escape >>
311-
<% endfor %>\end{itemize}
312-
313-
<% endif %>
314267
% PI-LOCK DISCUSSION
315268
[HALFSEED-LOCK:pi-discussion]
316269

317-
\section*{Open Questions}
318-
<% if open_todos %>\begin{itemize}
319-
<% for todo in open_todos %> \item << todo | latex_escape >>
320-
<% endfor %>\end{itemize}
321-
<% else %>No open \verb|\todo| items.
322-
<% endif %>
323-
324-
\appendix
325-
\section{Supporting Artifacts}
326-
This appendix lists the structured research artifacts the iteration drew on,
327-
in case a reader wants to verify how the body of the paper was synthesized
328-
from internal claims.
329-
330-
<% for artifact in analytical_artifacts %>\paragraph{A-<< loop.index >> (analytical, conf << "%.2f" | format(artifact.confidence) >>).} << artifact.claim | latex_paragraph >>
331-
<% if artifact.evidence %>\textit{Evidence:} << artifact.evidence | join_evidence | latex_escape >>.
332-
<% endif %><% if artifact.failure_modes %>\textit{Failure modes:} << artifact.failure_modes | join_evidence | latex_escape >>.
333-
<% endif %>
334-
<% endfor %>
335-
<% for artifact in numerical_artifacts %>\paragraph{N-<< loop.index >> (numerical, conf << "%.2f" | format(artifact.confidence) >>).} << artifact.claim | latex_paragraph >>
336-
<% if artifact.evidence %>\textit{Evidence:} << artifact.evidence | join_evidence | latex_escape >>.
337-
<% endif %>
338-
<% endfor %>
339-
<% for artifact in literature_artifacts %>\paragraph{L-<< loop.index >> (literature, conf << "%.2f" | format(artifact.confidence) >>).} << artifact.claim | latex_paragraph >>
340-
<% if artifact.evidence %>\textit{Source:} << artifact.evidence | join_evidence | latex_escape >>.
341-
<% endif %>
342-
<% endfor %>
343-
344270
\bibliographystyle{plain}
345271
% Bibliography is generated by the citation gate; see citations.bib once enabled.
346272

0 commit comments

Comments
 (0)