feat: introduce agentic search pipeline with live trace streaming#100
Merged
quiet-node merged 69 commits intomainfrom Apr 22, 2026
Merged
feat: introduce agentic search pipeline with live trace streaming#100quiet-node merged 69 commits intomainfrom
quiet-node merged 69 commits intomainfrom
Conversation
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
… thinking animation fixes Consolidate loading indicators into shared LoadingStage component with shimmer-sweep animation applied to both /search and /think stages. Fix per-character thinking animation (resolve invisible text bug). Implement collapsible sources list with letter-avatar sources, two-way hover for inline citations, and SQLite persistence of sources. Refactor pill-style indicators to minimal list and letter-avatar typography. Update slash command config and add comprehensive tests for search features. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…le dates The 4B synthesis model was emitting dates from its training cutoff (e.g. "as of September 2025") instead of grounding on the fetched sources. We now compute the current UTC date once per /search turn and inject it into the synthesis system prompt, plus explicit rules: never state a date not in the sources, prefer the most recent source date when multiple differ, and treat the sources as the current state of the world. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Fuse a BM25F lexical score over the retrieved-set corpus with the upstream SearXNG engine order via Reciprocal Rank Fusion (k=60). Title weighted 2x content; Lucene defaults k1=1.2, b=0.75. Pure Rust, no new dependencies, no unsafe, bounded O(N*T) per call. The rerank runs after the SearXNG fetch and before the Sources event, so the frontend footer and the synthesis prompt observe the same order. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
New FastAPI service in sandbox/search-box/reader that extracts LLM-ready markdown from arbitrary URLs. Private-host guard, byte cap, and run-as-non-root are in place per the hardened sandbox pattern. See design doc for rationale. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Moves pytest out of the production requirements file so it is not baked into the Docker image. Runtime deps stay in requirements.txt; pytest lives in requirements-dev.txt for local test runs. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Binds localhost only on port 25018, drops all capabilities, read-only rootfs with a small tmpfs for /tmp, mem and cpu caps. Shares the existing search-box network so the reader can be reached only from the same sandbox context. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…er timeout Inner urllib timeout bumped from 2s to 4s to sit just under the Docker healthcheck timeout of 5s, eliminating a small inconsistency between the two timeouts. The 1s buffer preserves clean process exit within Docker's window. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Introduces Action, Sufficiency, RouterJudgeOutput, JudgeVerdict,
SearchWarning, IterationStage, IterationTrace, and SearchMetadata.
Extends SearchEvent with AnalyzingQuery, ReadingSources,
RefiningSearch{attempt,total}, Composing, and Warning variants.
Preserves existing Classifying/Clarifying/Sources/Token variants so
the pipeline keeps compiling; they are removed when Task 13 updates
call sites. New types are re-exported from search/mod.rs to eliminate
dead_code warnings while the pipeline tasks that consume them are
still pending.
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Centralizes MAX_ITERATIONS=3, GAP_QUERIES_PER_ROUND=3, TOP_K_URLS=5, CHUNK_TOKEN_SIZE=500, TOP_K_CHUNKS=8, plus reader and judge timeouts. Constants only; no behavior change yet. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Introduces search_judge.txt with the discrete three-level sufficiency schema plus gap_queries. Synthesis prompt gains a single sentence clarifying that citation markers apply equally to search-result snippets and reader-extracted chunks. Router prompt is untouched in this commit; it is rewritten together with the merged call signature in Task 11. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Bounded single retry primitive for LLM, SearXNG, and reader calls. String-level transient classifier avoids threading concrete error types through every call site. No exponential backoff: semantics are hiccup recovery, not generic retry. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Splits reader markdown into target-sized chunks respecting paragraph boundaries. Preserves source URL and title per chunk so citations remain correct across snippet- and chunk-level inputs to the judge. Word-level approximation for token counts keeps the chunker decoupled from any specific model tokenizer. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Reuses the rerank module for chunk-level scoring with vanilla Okapi BM25 (no RRF since chunks have no secondary ordering to fuse with). Stable ordering for ties via original-index tiebreaker. Returns up to top_k chunk references. Refactors the existing private tokenize() into tokenize_uncapped() + a capped wrapper so chunk text (up to ~500 words) is tokenized without truncation while query strings retain the MAX_QUERY_TOKENS defence-in-depth cap. Unicode-aware lowercasing is preserved. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Tolerant JSON extraction strips chatty preamble and markdown fences. Normalizer caps gap_queries to N and clears them when the verdict is sufficient, enforcing prompt invariants in code so the pipeline does not rely solely on the model obeying its system prompt. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Concurrent per-URL fetches with a 5-slot semaphore, per-batch timeout, cancellation via tokio::select, and a single retry on transient connect errors. Reports empty-body URLs separately so the pipeline can log them for the future Playwright decision, and returns ServiceUnavailable when the reader sidecar is entirely unreachable so the caller can degrade to snippets. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Introduces ROUTER_MERGED_SYSTEM_PROMPT (new prompt file) and the companion call_router_merged returning RouterJudgeOutput in a single Ollama roundtrip. Adds call_judge that drives the universal sufficiency check for both snippet and chunk rounds, wiring judge::parse_verdict and judge::normalize_verdict so invariants are enforced in code. Existing call_router and ROUTER_SYSTEM_PROMPT are untouched; Task 13 swaps the pipeline over and Task 16 retires them. Refactors the internal HTTP wiring into a shared request_json helper used by all three LLM call functions. Adds SearchError::Router and SearchError::Judge variants to types.rs for typed error propagation. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
search_all issues N queries concurrently, collects results, and returns the URL-deduplicated union. Individual query failures are tolerated so a flaky single query in a gap round does not take out the rest of the batch. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Introduces RouterJudgeCaller and JudgeCaller traits plus a run_agentic entry point that handles the CLARIFY and history-sufficient short-circuit branches. Non-sufficient PROCEED returns an internal error pending Task 14 which wires the search round, reader escalation, and chunk-level judge. Legacy run is untouched; the Tauri command still dispatches there until Task 16 retires it. Also adds SearchError::Internal(String) for stub-boundary returns and split_into_stream_pieces for streaming clarifying questions as Token events. DefaultRouterJudge/DefaultJudge are stubbed with unimplemented!() because per-call dependencies (endpoint, model, client, cancel token) are not injectable at struct level; they will be wired in Task 16 when the Tauri command swaps. Deviation from spec: CLARIFY branch persists the question to history (same as legacy run_clarify_branch) so subsequent turns can see the clarifying exchange. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Replaces the SearchError::Internal stub with the full initial iteration: SearXNG, URL-level rerank, snippets judge, reader escalation, chunking, chunk rerank, chunks judge. On sufficient verdicts at any stage, streams synthesis and returns. On insufficient, falls to the exhaustion fallback for now; Task 15 adds the gap loop. Graceful degradation on reader-unavailable and reader-partial-failure paths emits warnings without aborting. run_agentic gains searxng_endpoint and today parameters (matching the legacy run signature) to make SearXNG and synthesis testable without thread_local overrides. The MockJudge stub is replaced by QueueJudge; the three Task 13 "Internal error" stub tests are retired and replaced by five Task 14 scenario tests. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Add reader_base_url parameter to run_agentic (matching the searxng_endpoint pattern) so tests can inject a mock reader server and exercise the full reader escalation path without touching the production READER_BASE_URL constant. Set READER_BATCH_TIMEOUT_S=1s in test builds via cfg(test) so the BatchTimeout error path is testable without waiting 30 seconds. Add tests covering: mid-CLARIFY cancellation, before-SearXNG cancellation, SearXNG HTTP error propagation, reader Cancelled path, reader BatchTimeout path, reader majority HTTP failure partial warning, and QueueJudge empty queue error. All new pipeline.rs production lines now show zero uncovered regions in the llvm-cov HTML report. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Loops up to MAX_ITERATIONS after the initial round. Each gap round
emits RefiningSearch { attempt, total } before Searching, reuses
accumulated URLs and chunks, and respects the cancellation token.
Empty-result gap rounds advance the counter silently. Warning
dedup via contains-checks ensures ReaderUnavailable and
ReaderPartialFailure do not repeat across rounds. Exhaustion
synthesis uses the best accumulated chunks with the
IterationCapExhausted warning.
Adds search_all_with_endpoint to searxng so the gap loop can fan
out parallel gap queries using the already-resolved endpoint URL.
Updates the Task 14 exhaustion test to use insufficient_verdict_no_gaps
so the loop exits on the empty-queries guard without needing SearXNG
mocks. Adds four new gap-loop tests: gap round succeeds within cap,
all iterations exhaust, empty SearXNG breaks loop silently, and
ReaderUnavailable dedup across rounds.
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Wires DefaultRouterJudge and DefaultJudge to call_router_merged and call_judge respectively. Tauri search_pipeline command now dispatches to run_agentic, making the agentic loop the production /search implementation. Legacy run, call_router, run_search_branch, and the ROUTER_SYSTEM_PROMPT prompt file are removed along with their tests. persist_turn accepts warnings and metadata arguments; DB column migration lands in Task 17. Cancellation is now checked at every stage entry and races inline against long SearXNG and judge HTTP calls. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Adds search_metadata and search_warnings TEXT columns to the messages table via idempotent ensure_column calls at startup. Refactors the three existing PRAGMA-based migrations to use the same helper so all column additions follow one pattern. Extends insert_message, insert_messages_batch, load_messages, and PersistedMessage with the two new optional JSON TEXT fields. Adds search_warnings and search_metadata to SaveMessagePayload and persist_message so the frontend can pass serialized values when saving a search turn. The pipeline persist_turn parameters are renamed from _warnings/_metadata to warnings/metadata; they are acknowledged via a let _ binding since the pipeline owns no DB connection. DB population happens through the frontend save flow added here. Adds three unit tests: ensure_column_is_idempotent, persist_and_load_round_trip_includes_warnings_and_metadata, and persist_and_load_tolerates_null_search_metadata. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Extends SearchEvent with AnalyzingQuery, ReadingSources,
RefiningSearch{attempt,total}, Composing, and Warning variants.
Removes Classifying and Clarifying (dropped per design decision
15: clarifying questions now stream as Token events). Adds
SearchWarning union matching the Rust enum with snake_case
values. SearchStage is now a discriminated union so the UI can
render the refining-search attempt counter.
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Const maps from SearchWarning enum to friendly user-facing string and warn or error severity. Consumed by the warning icon (Task 21) and the bubble border rule. Kept in config so wording can be tuned without editing component code. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…ounter Replaces "Classifying query" with "Analyzing query" per the locked UX copy. Refining-search stage now renders the attempt counter "Refining search (k/N)" from the RefiningSearch event fields. SEARCH_STAGE_LABELS const replaced with a helper that handles the counter case. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Subtle 14px icon renders beside the Sources collapsible when a turn carries warnings. Amber triangle for warn-only warning lists, red circle when any warning is error severity. Native title tooltip stacks friendly copy lines. Renders nothing when the warnings list is empty. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
After the first RefiningSearch event fires, subsequent Searching /
ReadingSources / Composing stages now show copy that conveys Thuki
is looking at more material rather than repeating the initial
linear flow.
Labels:
- Initial round: Searching the web, Reading sources, Composing answer
- Gap rounds: Searching more angles, Reading additional pages,
Composing refined answer
Implementation stays on the frontend: useOllama tracks an
inGapRound flag that flips on the first RefiningSearch event and
propagates to subsequent stage objects via an optional gap field.
ConversationView picks the label variant per stage. No backend
changes; existing tests updated to expect gap: true for post-
RefiningSearch stages.
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
The previous prompt combined Answer directly in the first sentence with One to three short paragraphs is usually enough. Small local models followed this literally and produced one-sentence answers to entity-style questions (who founded Twitter, etc.), forcing the user into manual follow-ups. New guidance matches the industry standard for research-search answers (Perplexity, ChatGPT Search, You.com): - Open with the direct literal answer. - Follow with the supporting context a curious reader would want next, picked by question type (person, company, event, process, comparison). - Anticipate the obvious follow-up so one-liners are not accepted when the sources supply more. - Target two to four tight paragraphs by default, scaling to the question complexity. Never pad. Every grounding guardrail stays: no preamble, no meta commentary, no hallucinated dates, no uncited claims, paraphrase not copy, match the users language. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…y answers
Two changes addressing one-sentence answers like 'Jack Dorsey, Noah
Glass, Biz Stone, and Evan Williams founded Twitter' for rich
entity queries.
1. Judge prompt tightened:
- sufficient now requires BOTH the literal answer AND the
supporting facts a substantive answer needs (dates, numbers,
context). Bare snippets no longer count as sufficient for
entity / event / comparison / how-to questions.
- partial is the expected verdict when snippets give the literal
answer but miss supporting context. This triggers reader
escalation so synthesis sees full-page content instead of a
150-char snippet.
- gap_queries guidance clarified for partial: target the missing
supporting facts, not the literal answer again.
2. Synthesis prompt gains a concrete few-shot example for 'Who
founded Tesla?' showing an unacceptable one-liner next to an
acceptable grounded two-sentence version. Small local models
(default here is gemma4:e2b) follow shown examples far more
reliably than abstract substance instructions.
No code changes, no test changes. Runtime verification is smoke
tests against the live pipeline.
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…tains the answer The previous prompt allowed history_sufficiency=sufficient when the query was 'stable general knowledge as of today'. Small local models interpreted this aggressively and answered basic questions (for example who founded Twitter) from training data alone, producing a one-liner with no citations and no Sources footer. Users who invoke /search explicitly want fresh web data. The router should only short-circuit when the prior turns literally contain the answer already. Training knowledge and general- knowledge carve-outs are both removed. Key changes: - Opening line reframes the router's job: default is a fresh search; only short-circuit when the transcript has the answer. - sufficient now requires a prior turn to literally contain the fact the user is asking about; training knowledge explicitly does NOT count. - Default posture is insufficient. Bias toward insufficient for time-sensitive topics: ownership, pricing, versions, roles, statuses, current events. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…o system prompt Adds docs/agentic-search.md: a deep technical tour of the /search pipeline covering every stage (router+judge merged call, SearXNG, BM25F+RRF rerank, sufficiency judge, Trafilatura reader, chunker, chunk rerank, gap loop, synthesis), security posture at all three container layers, graceful-degradation behavior, industry comparison against Perplexity / Exa / CRAG / Self-RAG / FLARE / Adaptive-RAG, configuration knobs, performance characteristics, and a canonical file map. Written to work for readers at any technical level. Updates src-tauri/prompts/system_prompt.txt with a new 'Your /search capability' section so Thuki can accurately explain its own search command when asked, suggest it when a query needs fresh web data, and stay honest about its limits. Also replaces a pre-existing em dash in the conversational-continuity section with parenthetical clauses per the project style rule. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Rename src-tauri/prompts/search_router_merged.txt to search_plan.txt and update the include_str! path in llm.rs. Rename the public constant ROUTER_MERGED_SYSTEM_PROMPT to SEARCH_PLAN_SYSTEM_PROMPT and update all references in llm.rs (usage site, doc comment, test). Update the file name reference in docs/agentic-search.md. No content change in the prompt. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…ERATIONS without sufficient Previous attempt set a flag based solely on attempt == MAX_ITERATIONS, which missed the case where the flag had been set then the loop exited early via current_queries.clear() on the NEXT iteration. The warning still fired despite the loop not actually running every round to completion. Correct fix: track hit_iteration_cap and set it only AT THE BOTTOM of the loop body (after the judge call and the Sufficient early-return) when attempt equals MAX_ITERATIONS. Every early-exit path (empty current_queries guard at the top, empty SearXNG new_urls clearing current_queries, cancellation, Sufficient verdict early return) bypasses the flag assignment. Tests updated and added for every exit path: - real cap hit across all three iterations with Insufficient plus gap queries: warning fires - initial round Insufficient with no gap queries: no warning - gap round SearXNG returns only seen URLs: no warning - Sufficient at attempt 2: no warning - Sufficient at attempt 3: no warning - attempt 3 runs in full with Insufficient and empty gap queries: warning fires (cap reached) Single clean commit replacing the earlier 2fdc3cb which did not cover all exit paths. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
.gitignore: add *.profraw so coverage runs do not leave instrumentation artifacts in the working tree. llm.rs: collapse SEARCH_PLAN_SYSTEM_PROMPT include_str to a single line after the rename commit 8c1ee7a left it split on two lines with trailing whitespace that rustfmt would otherwise re-flow. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Add a concurrent healthz probe that runs before the search pipeline. If SearXNG or the reader container is unreachable, the backend emits a SandboxUnavailable event instead of a cryptic HTTP error. The frontend handles the event in useOllama, renders a SandboxSetupCard with Docker Compose setup guidance, and ChatBubble routes the card into chat. All frontend and backend coverage remains at 100%. Closes the branch coverage gaps in useConversationHistory (searchWarnings serialisation) and ConversationView (gap-mode label ternaries) introduced on this branch. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…source filtering Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
When the judge LLM returns malformed or non-JSON output, the pipeline previously propagated SearchError::Judge and aborted. Mirror the retry+fallback pattern already in call_router_merged: retry once with a stricter prompt suffix, then fall back to Partial + empty gap_queries so the pipeline synthesizes from existing evidence instead of surfacing a cryptic error. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Two issues fixed: - src-tauri/src/search/mod.rs: Pass config::READER_BASE_URL (port 25018) instead of searxng::SEARXNG_BASE_URL (port 25017) for reader /extract calls. The wrong base URL caused all URLs to show as "failed" in the trace. - src-tauri/src/search/pipeline.rs: Update trace messaging for clarity: "opened" -> "read", "Opening the shortlisted pages" -> "Reading the shortlisted pages", "full text behind" -> "full text from", "could be opened cleanly" -> "could be read cleanly". - src/components/SearchTraceBlock.tsx: Update chip label from "opened" to "read" to match pipeline messaging. - Test files: Updated assertions to match new strings. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…and /translate - Wrap each command description in Tooltip so full text shows on hover - Tooltip gains optional className prop for flex layout integration - /search gets a globe icon (web search) - /translate gets a 文A icon matching Google Translate icon style - Shorten /search description to fit without truncation Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…end regressions - added unit tests for search pipeline error paths and pluralization logic - achieved 100% line coverage in src-tauri/src/search/pipeline.rs - implemented scrollIntoView for command suggestions with JSDOM mocks - fixed formatting and lint warnings in Tooltip component Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…atches! with explicit match arms in tests to fix llvm-cov attribution - Implement TcpListener mock for mid-stream network error test - Add 100% line coverage enforcement to test:all:coverage script - Refactor frontend tests to use act() and fake timers where missing - Update search metadata types and persistence logic for agentic v3 Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
flushSync is intentionally used in the search trace handler to make trace blocks feel live rather than batched at the next paint. Configure the rule off at the config level instead of suppressing it with per-line disable comments, which were fragile due to rule name format differences between @eslint-react/eslint-plugin v3 and v4. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…ync violation Tauri channel events arrive as separate macrotasks, so React 18 automatic batching does not merge them across callbacks. flushSync is not needed for live trace updates in this context. Removing it eliminates the lint violation without any UX regression. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…ync violation Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
- Reorganize README to include search sandbox setup as Step 2\n- Refine SandboxSetupCard copy for better consumer clarity\n- Change Setup Guide link to use tauri::open_url for reliable browser opening\n- Update tests and restore 100% coverage Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR introduces a full agentic web search pipeline into Thuki, activated via the `/search` command. The implementation spans new Docker sandbox services, a Rust search engine, and frontend trace/warning UI.
New Infrastructure: `sandbox/search-box`
Two new containerized services join the existing `llm-box` (Ollama), now cleanly namespaced under `sandbox/search-box/`:
SearXNG (meta-search engine)
Reader (custom Python HTTP service)
Sandbox reorganization
New Backend: Agentic Search Pipeline (`src-tauri/src/search/`)
A new Rust module implementing a multi-round retrieval loop:
Agentic loop behavior
LLM robustness
Prompts added
Frontend Changes
Test Coverage
E2E integration tests (`tests/search_pipeline_e2e.rs`) exercise the full agentic loop against a live sandbox.
Test Plan