# Agent loop `packages/agent/src/orchestrator.ts` runs a manual Claude tool-use loop: 1. One streaming Messages API call per turn (`effort: medium`, prompt-cached system). 2. All `tool_use` blocks in the response execute sequentially; all results return in one user message (assistant content — thinking blocks included — is echoed back verbatim). 3. The loop ends when the model calls `finish_research`, stops calling tools, or budgets run out (one warning turn, then hard stop). ## Tools | Tool | Effect | |---|---| | `set_objectives` | research plan (emits `plan.updated`) | | `web_search` | Firecrawl search; results become `found` sources | | `fetch_url` | Firecrawl scrape → stored (≤120k chars), returned to model (≤14k) wrapped in `` | | `read_source` | re-read stored content with offset paging — costs no budget | | `add_claim` / `update_claim` | hypothesis lifecycle with confidence 0..1 | | `add_evidence` | verbatim quote tied to claim + stance; soft-verified against stored content | | `add_contradiction` | first-class disagreement record | | `report_progress` | public narration (`thought` event) | | `finish_research` | closes the loop, hands off to synthesis | ## Safety - Budgets (`maxSearches`, `maxScrapes`, `maxToolCalls`, `maxModelTurns`, `deadlineMs`) are hard bounds enforced by the app, not suggestions to the model. - Tool inputs are zod-validated; invalid input returns an `is_error` tool result and the session continues (a single tool failure never kills a session). - Scraped content is wrapped in `` and the system prompt instructs the model to treat it as evidence, never as instructions (prompt-injection defense). - URLs pass an SSRF guard (`packages/firecrawl/src/url-guard.ts`) blocking private ranges, localhost and metadata endpoints. - `stop_reason: "refusal"` fails the session gracefully with a user-visible error. ## Synthesis `packages/agent/src/synthesis.ts` assigns mechanical citation indices, builds the evidence base (claims → stances → verbatim quotes labeled `[n]`), and streams the answer token-by-token (`answer.delta` events, buffered ~160 chars / 400 ms per event row).