Agent loop
packages/agent/src/orchestrator.ts runs a manual Claude tool-use loop:
- One streaming Messages API call per turn (
effort: medium, prompt-cached system). - All
tool_useblocks in the response execute sequentially; all results return in one user message (assistant content — thinking blocks included — is echoed back verbatim). - 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 <untrusted_web_content> |
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_errortool result and the session continues (a single tool failure never kills a session). - Scraped content is wrapped in
<untrusted_web_content>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).