Skip to content

Port preprint-af exactly to native Go - #1

Merged
AbirAbbas merged 26 commits into
mainfrom
port/go-exact
Sep 21, 2026
Merged

AbirAbbas merged 26 commits into
mainfrom
port/go-exact

Conversation

@santoshkumarradha

Copy link
Copy Markdown
Member

What changed

  • rewrote the AgentField node in native Go with the same 19 reasoners, prompts, schemas, orchestration, parallel fan-outs, compile gate, and critique/repair loop
  • removed the Python AgentField implementation and made the Go stack the only application runtime
  • replaced Python-dependent parity tests with immutable prompt, persona, and discovery-schema goldens captured from the final reference implementation
  • made the default docker compose up --build path launch the Go node
  • bundled TeX Live, OpenCode, Matplotlib, NumPy, and Pandas for self-contained paper and figure generation
  • updated the README and architecture documentation for the Go-only layout
  • included the verified DeepSeek v4 Flash output PDF at artifacts/deepseek-v4-flash/preprint-af-go.pdf

Why

The earlier Go implementation had drifted from the Python behavior and the repository shipped two runtime paths. This branch restores byte-level prompt and schema parity, preserves the dynamic AgentField call graph, verifies the complete pipeline, and then removes the duplicate Python node so there is one production implementation.

Python remains in the container only as the execution runtime for generated Matplotlib figure scripts. It is not an AgentField node.

Validation

  • go test ./...
  • go vet ./...
  • stripped static build: 8.1 MB
  • docker compose config --quiet
  • complete Docker build with Matplotlib PNG/PDF and LaTeX compilation smoke tests
  • installed package starts and discovers exactly 19 reasoners
  • full AgentField DeepSeek v4 Flash run completed with 49 tracked executions and a compiled 11-page PDF
  • git diff --check

@santoshkumarradha
santoshkumarradha marked this pull request as ready for review August 4, 2026 18:29
@cursor

cursor Bot commented Aug 4, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

AbirAbbas and others added 6 commits September 21, 2026 11:18
The port declared every `str | None` reasoner parameter as `{"type":"object"}`.
The Python SDK resolves annotations with `typing.get_type_hints`, which turns
PEP-604 `str | None` into `typing.Optional[str]`, so `_type_to_json_schema`
unwraps it and publishes `{"type":"string"}` (agent.py `_type_to_json_schema`).
`target_venue`, `field_hint` and every `model` argument were therefore advertised
with the wrong type; `examples/payload.json` does not validate against the
published `write_paper` schema.

Also restores four node-level behaviours the Python app had:

- `HarnessConfig` model falls back to DEFAULT_MODEL, not to `AI_MODEL`, so an
  `AI_MODEL` override no longer silently retargets the OpenCode workers.
- DID and VC generation are on: the Python `Agent` constructor defaults
  `enable_did=True` and `vc_enabled=True`.
- OpenCode concurrency (10) and harness timeout (1800s) match the Python
  provider defaults instead of the Go SDK's lower ones.
- Reasoner descriptions carry the Python docstring summary rather than a
  generated "Exact Go port of ..." string, and the entry point's tags keep
  Python's router-then-decorator order (`workflow`, `entry`).

Tests cover `Build()+RegisterAll()` with and without an API key (an unknown
name makes `reasonerInputSchema` panic at startup), the full registered
surface, and `examples/payload.json` against the published schema.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Divergences found by diffing each Go reasoner body against the Python one it
replaces:

- Truncation counted bytes where Python counts code points, so any non-ASCII
  paper silently got a shorter evidence/paper excerpt than the prompts promise.
- `STATE.json` was written with Go's encoder; Python writes
  `json.dumps(indent=2)`, which escapes non-ASCII and leaves `<>&` literal.
- Structured-output schemas were strictified for both paths. Python only
  strictifies the direct `.ai` response_format (`_strictify_openai_schema`);
  the harness path passes the plain Pydantic schema, so defaulted fields must
  stay optional there.
- Filesystem write failures (BLUEPRINT.md, main.tex, round artifacts,
  REVIEW.md, STATE.json) were dropped, reporting a successful run that had
  written nothing.
- Context cancellation was converted into a model fallback, so a cancelled run
  kept producing neutral reviews and "repairs" instead of stopping.
- `RunLatexmk` raced: the timeout goroutine wrote `timedOut` while the main
  goroutine read it. Replaced with `exec.CommandContext`.
- `round4` did not match Python's `round(v, 4)`: scaling by 10000 first
  double-rounds, so `round(0.00005, 4)` is 0.0001 in CPython but 0.0 in Go.
  It now rounds through `strconv.FormatFloat`, which is correctly rounded with
  ties-to-even on the exact binary value like CPython.
- Repair-plan digest bytes, target resolution order (`global` after an exact
  glob), the synthesized section glob, `~` expansion in `folder_path`, Markdown
  `True`/`False` spelling, and positioning artifact ordering all differed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- The fidelity auditor does not "fail the build"; it blocks quality-threshold
  convergence, and plateau / no-repair / safety-cap stops are still terminal.
- The compile gate requires a zero `latexmk` exit, so a run that stops on a
  plateau can return an empty `pdf_path`.
- `plateau_delta` is a documented `write_paper` parameter but was missing from
  the parameter table.
- Model strings are OpenRouter ids; the Go node has no LiteLLM routing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Python's `scan_novelty` wraps the OpenCode call in `try/except Exception` and
returns `safe_ai_fallback(NoveltyScan)` (positioning.py:226-235): the literature
scout is advisory, so a crashed or unavailable harness must not take positioning
with it. The Go port returned the harness error, which fails
`positioning_run_positioning` and therefore the whole `write_paper` run.

Cancellation still propagates — `except Exception` does not catch
`asyncio.CancelledError` — so only non-cancellation failures degrade.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Fidelity overlay: Python snapshots `result.citation_issues` before appending
  its deterministic findings (`flagged = {c for c in result.citation_issues}`,
  critique.py:242). Go re-scanned the growing slice, so a finding for
  `\cite{smith2024}` suppressed the finding for `\cite{smith}` — the repair
  planner was never told about the second missing key.
- `TODO.md`: Python's `append_todos` is a synchronous helper with no `await`,
  so the event loop cannot interleave two appends. Go runs the figure workers
  on real goroutines, where two unbuildable figures could each read the old
  file and the later write dropped the other's bullet. Serialised with a mutex.
- Blueprint title fallback: Python strips a `title:` prefix case-insensitively
  (`title.lower().startswith("title:")`, blueprint.py:43), so a `## TITLE: X`
  heading yields `X`; Go only stripped the exact spelling `Title:`.
- `TestDirectAISchemaUsesPythonStrictResponseContract` asserted Go's sorted
  `required` order and called it Python parity. JSON Schema treats `required`
  as a set and Python emits declaration order, so the test now asserts the
  member set and says so.

Each of the first three has a regression test that fails against the previous
code.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`main` carried a `ci.yml` that byte-compiled `main.py` and the reasoner package
and asserted a few helpers (removed in 3a0d5d2 because it had gone stale). This
PR deletes everything that gate covered, so the repo would land a 5k-line
rewrite with no automated verification at all. This is the same shape of gate
for the Go tree: build, vet, test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@AbirAbbas

Copy link
Copy Markdown
Contributor

Took this over since it had been sitting for ~7 weeks. No rebase was needed — the branch was still on top of main. Pushed 6 commits on top of yours; all of it is parity work against the Python being deleted, nothing redesigned.

The ones worth knowing about:

  • Every str | None reasoner parameter was published as {"type":"object"}. The Python SDK resolves annotations through typing.get_type_hints, which normalises str | None to Optional[str], so _type_to_json_schema unwraps it to {"type":"string"}. target_venue, field_hint and every model argument were wrong, and examples/payload.json did not validate against the published write_paper schema.
  • scan_novelty returned the harness error. Python wraps that call in try/except and degrades to safe_ai_fallback(NoveltyScan) — a crashed literature scout was taking the whole write_paper run with it.
  • HarnessConfig.Model fell back to AI_MODEL; Python falls back to DEFAULT_MODEL, so setting AI_MODEL was silently retargeting the OpenCode workers too.
  • DID/VC were off. The Python Agent constructor defaults enable_did=True, vc_enabled=True.
  • Fidelity overlay re-scanned the list it was appending to, so the \cite{smith2024} finding suppressed the one for \cite{smith} and the repair planner never heard about the second missing key.
  • Concurrent unbuildable figures both read-modify-wrote TODO.md and the later write dropped the other's bullet; append_todos is synchronous in Python so the event loop can't interleave it.
  • round4 double-rounded — round(0.00005, 4) is 0.0001 in CPython, was 0.0 here.
  • Unicode/JSON: truncation counted bytes where Python counts code points, and STATE.json used Go's encoder instead of json.dumps(indent=2).
  • Added .github/workflows/ci.yml (go build/vet/test). The workflow removed in 3a0d5d2 gated exactly the files this PR deletes, which would have left a 5k-line rewrite with no gate at all. It's green.

Two things I left for you:

  1. RunLatexmk requires a zero latexmk exit. Python accepted a non-zero exit when the PDF's mtime had changed (helpers.py:313). latex_test.go encodes the stricter rule deliberately so I didn't touch it, but it means a run Python would have shipped a PDF for can now end with an empty pdf_path — I updated the README/architecture text to say that.
  2. Three things the pinned Go SDK has no equivalent for: the node description (only reachable as CLI metadata), dev_mode=True, and HarnessConfig.max_budget_usd (passed per call instead, and the OpenCode adapter doesn't enforce it in either SDK).

Not verified here: the Docker image build, registration against a live control plane, and a real LaTeX run — no Docker/WSL integration, no OpenRouter key, no TeX on this box.

@AbirAbbas
AbirAbbas merged commit d781a32 into main Sep 21, 2026
1 check passed
@AbirAbbas
AbirAbbas deleted the port/go-exact branch September 21, 2026 16:22
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