Skip to content

feat(tools): research_to_file — two-phase research/synthesis pipeline tool - #22

Open
kuroko1t wants to merge 2 commits into
mainfrom
feat/research-to-file
Open

feat(tools): research_to_file — two-phase research/synthesis pipeline tool#22
kuroko1t wants to merge 2 commits into
mainfrom
feat/research-to-file

Conversation

@kuroko1t

@kuroko1t kuroko1t commented May 22, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a research_to_file tool: single CLI entry-point UX (the user describes the task in natural language, the LLM picks the tool) wrapping the validated phase 1 / phase 2 pipeline. Orchestration is in Rust — no agent loop inside the tool.

Design (v2)

  • Args: topic, output_file, instructions (verbatim shape requirements), optional source_count (default 3, clamped 1-10).
  • Phase 1 (Rust): one web_search call via a new pub(crate) helper search_top_urls that returns structured (title, url) pairs. Then a Rust loop over the top source_count * 2 candidates, each iteration calling dispatch_web_fetch_call (LLM only used for per-page extraction) and std::fs::write to source-K.md. Stops at source_count successes.
  • Phase 2: reads source files back, fires a single non-tool LLM call with the user's verbatim instructions, writes the deliverable to output_file.
  • Workspace: persists at <output_stem>.sources/ next to the deliverable.
  • Risk level: Moderate (same class as write_file).

v1 → v2 history

The first commit on this branch (088fc46) ran phase 1 inside a subagent. Live smoke 2026-05-23 found the subagent reproduces exactly the convergence problem the tool was meant to bypass: LLM picked the tool, subagent started, but wrote 0 source files (out of 5 requested) and burned ~70 wasted tool calls. Wrapping the pipeline in a tool moved the convergence problem one level deeper. v2 (617be8d) takes the agent loop out of phase 1 entirely.

Drive-by

search_top_urls returns a hard error on non-200 status. DuckDuckGo's anti-bot path returns HTTP 202 with a "checking your browser" body that parse_ddg_results turns into 0 results — the Tool::execute path still silently swallows this as "No results found", but the new helper surfaces a clearer diagnostic. Worth fixing the execute path the same way in a follow-up.

Validation

  • 5 unit tests on the tool surface; 740 lib tests pass.
  • cargo fmt --check + cargo clippy --release --lib -- -D warnings clean.
  • Live smoke currently blocked by DDG rate-limit (triggered after ~8 consecutive dog-foods). The two phases are independently validated:
    • Phase 1 shape: 2026-05-22 dog-food wrote 3 source files cleanly with the same std::fs::write primitive.
    • Phase 2: 2026-05-22 dog-food synthesised 498 chars of coherent JP prose with 4 URL citations inline.
  • Re-run smoke once DDG clears.

Test plan

  • cargo test --lib (740/740)
  • cargo fmt --check
  • cargo clippy --release --lib -- -D warnings
  • v1 live smoke (failed as expected — drove the v2 redesign)
  • v2 live smoke (blocked by DDG rate-limit; re-run when cleared)

kuroko1t added 2 commits May 23, 2026 00:58
… tool

A single tool that wraps the validated phase 1 (gather sources) + phase 2
(single-shot synthesis) pipeline so the user-facing UX stays Claude-
compatible — describe the task in natural language; the LLM picks the
tool; orchestration is in Rust code, not in the LLM's recipe-following.

Background. Dog-food on 2026-05-22 (see convergence_triangulated_
capabilities_problem.md) showed three orthogonal interventions — prompt
pressure, model swap to qwen3.5:9b, pre-scaffolded TODO file — all
produced 0 write_file calls on q3 for the same JP research task. The
phase 1 / phase 2 split was validated independently: with an explicit
"save sources, do not summarise" directive the subagent reliably reached
write_file × 3, and a single-shot synthesis call produced a coherent
deliverable with URLs cited inline.

Wrapping the split in a tool puts the only LLM decision at "use
research_to_file or not", which is the same tool-selection class q3 has
demonstrably gotten right for web_search / web_fetch / write_file.

Tool surface:
- Args: topic, output_file, instructions, source_count (default 3).
- Phase 1 spawns a subagent that searches → fetches → write_files exactly
  N source-K.md files into <stem>.sources/. Bounded by "do NOT summarise"
  rule so the subagent's convergence problem reduces to a counting
  condition the model can satisfy.
- Phase 2 reads the source files back, fires a single non-tool LLM call
  with the user's verbatim instructions, writes the deliverable to
  output_file. Stats include the synthesis call's token usage.
- Workspace persists next to the deliverable (debuggable + re-synthesis
  without re-fetching).
- Same Moderate risk class as write_file so approval gates still apply
  in non-yolo modes.

The dispatch lives in Agent because it needs &mut self for the subagent
and the LLM client. The Tool::execute path returns an explicit error
rather than no-op'ing, so misuse outside the agent loop is loud.

740 lib tests pass (+5 for the new tool's schema, description, risk,
and standalone-call error). cargo fmt + clippy --release -D warnings
clean. Live smoke against ollama not run in this commit — defer to a
follow-up validation pass.
Live smoke of v1 found the subagent inside phase 1 reproduces exactly
the convergence problem the tool was meant to bypass: 30 iterations, 0
write_file calls, 37 fetches against hallucinated URLs. See auto-memory
pr22_v1_subagent_phase1_failed.md. Wrapping the pipeline in a tool
moved the problem one level deeper instead of solving it.

v2 takes the agent loop out of phase 1 entirely. Rust drives:
- one web_search call (via a new pub(crate) helper search_top_urls in
  web_search.rs that returns structured (title, url) pairs instead of
  the formatted markdown that the Tool::execute path returns),
- a fetch-and-extract loop over the top N candidate URLs (each
  web_fetch with prompt is a single bounded LLM call to extract facts
  from one page),
- std::fs::write of each source-K.md.

LLM judgement is preserved only where it's well-scoped: per-fetch
extraction and the final synthesis. Both were independently validated
on 2026-05-22 (phase1-validation wrote 3 files cleanly; phase2-validation
synthesised 498 chars of coherent JP prose with URL citations).

Drive-by: search_top_urls fails loudly on non-200 status. DuckDuckGo's
anti-bot path returns HTTP 202 with a "checking your browser" HTML body
that parse_ddg_results turns into 0 results. The Tool::execute path
still silently swallows this as "No results found", but the new helper
surfaces "Search returned HTTP 202 (DuckDuckGo may be rate-limiting…)"
so callers get a usable diagnostic. Cf. ddg_rate_limit_blocker.md.

Live smoke is currently blocked by exactly that DDG rate-limit
(triggered mid-dog-food today after ~8 consecutive runs); re-run when
DDG clears. The architectural change is unit-test-covered and the two
phases are independently validated as above.

740 lib tests pass. cargo fmt + clippy --release -D warnings clean.
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.

1 participant