MARG-ADD-001 · Architecture Design Document · v0.1 · 2025

Multi-Agent Research & Report Generator

Graph-based agentic workflow for autonomous, iterative research and structured report synthesis using open-source LLMs.

Abstract

MARG is a stateful, graph-based multi-agent system that decomposes the research and report generation task into five specialised agent roles — Planner, Researcher, Synthesizer, Critic, and Formatter — orchestrated by a LangGraph state machine. The architecture addresses the structural failure modes of single-pass LLM research tools: shallow coverage caused by a single retrieval context window, hallucinations caused by the absence of a grounding loop, and unattributed claims caused by no explicit source-tracking discipline. The Critic Agent introduces a scored quality gate whose output drives conditional re-research or re-synthesis until a configurable threshold is met, making iterative self-correction an architectural property rather than an operator concern. The system is deployable locally via Docker Compose with Ollama and publicly via a single Cloud Run container on the GCP free tier.

Keywords: Multi-Agent Systems · LangGraph · Stateful Workflows · Agentic AI · Ollama · Open-Source LLMs · Iterative Critique · Report Synthesis · RAG · Self-Correction · Cloud Run · Docker

§1 Motivation & Problem Statement

The single-pass LLM research pattern — submit query, receive response — is architecturally incapable of producing research-grade outputs. The failure is not a model capability problem. It is a structural problem: the pattern provides no mechanism for scope decomposition, iterative retrieval, multi-source grounding, or quality verification. MARG is the architectural remedy.

Time compression ~80% reduction in initial research phase duration vs. manual analyst workflow target SLO · PI-3
Source attribution 100% claims in final report carry a traceable source link — structural, not heuristic Formatter Agent contract
Critique cycles ≤3 max Critic→Synthesizer re-synthesis loops before escalation to operator graph edge constraint
Local-first $0 API cost for full demo-tier operation — all inference via Ollama local container free-tier target

1.1 Failure Modes in Single-Pass LLM Research

These are not edge cases. They are the expected output characteristics of a single-pass retrieval-generation pattern applied to research tasks of non-trivial depth.

Failure modeCauseMARG structural remedy
Shallow coverage Single retrieval call; context window is the upper bound on information gathered Planner decomposes topic into N sub-queries; Researcher executes each independently across multiple search calls
Hallucinated claims LLM fills retrieval gaps with plausible fabrication; no verification step Critic Agent scores factual consistency as a first-class dimension; low score triggers re-research, not re-generation from stale context
No source attribution Source URLs are not tracked through the generation pipeline Researcher Agent writes (source_url, snippet) tuples to state; Formatter enforces citation presence per claim
No quality gate Output goes directly to user regardless of quality; no internal verification Critic Agent scores across four dimensions; scores below threshold route back through graph before delivery
Opaque reasoning Chain-of-thought is discarded; user cannot audit how conclusions were reached Each agent appends its rationale to state.trace; full trace is preserved and optionally surfaced in UI
Uncontrolled scope drift LLM interprets topic breadth ad hoc; adjacent topics pulled in without constraint Planner generates a bounded outline with section weights; Synthesizer is constrained to outline structure; Critic penalises scope violation

1.2 Target Value Stream

01
Query intake. User submits research topic with optional depth and focus-area parameters via API or UI. Input validation, parameter normalisation, and session initialisation.
~0s
02
Research planning. Planner Agent generates a scoped outline with N sections and relative weights. Decomposition into atomic sub-queries per section. Research plan written to state.
~8–15s
03
Information retrieval. Researcher Agent executes DuckDuckGo searches per sub-query. Snippets, titles, and URLs extracted and scored by relevance. Source map appended to state.
~20–60s
04
Section synthesis. Synthesizer Agent drafts each report section from outline and gathered sources. Inline citations injected. Draft sections written to state.
~30–90s
05
Critic evaluation. Critic Agent scores draft across four dimensions. Scores below threshold route back to Researcher (if retrieval gap) or Synthesizer (if synthesis quality). Max 3 iterations.
~10–20s
06
Report formatting. Formatter Agent applies structure: executive summary, table of contents, section bodies, reference list. Disclosure header applied per responsible AI policy.
~8–12s
07
Delivery. Structured Markdown or PDF delivered to caller with full source map, reasoning trace, quality scores, and iteration count. Session checkpointed to disk.
~0s

§2 System Architecture

2.1 C4 Level 1 — System Context

The system context establishes the MARG boundary and its interactions with external actors and systems. MARG has two external dependencies: the local LLM inference runtime (Ollama) and the public web search API (DuckDuckGo). Neither dependency involves authentication or data egress of user queries by default.

Figure 1 — C4 Level 1 System Context · MARG boundary and external interactions
C4 L1LangGraphOllama
[person] User Researcher / Analyst [ S O F T W A R E S Y S T E M ] MARG Multi-Agent Research & Report Generator LangGraph graph execution engine 5-agent stateful workflow Planner · Researcher · Synthesizer Critic · Formatter FastAPI · Docker / Cloud Run Local JSON checkpoints · Streamlit UI [ext. system] Ollama Local LLM runtime Llama 3.1 · Mistral [ext. system] DuckDuckGo Web search API no key · no PII egress [ext. storage] Local FS JSON checkpoints research query Markdown / PDF report LLM calls completions search queries snippets + URLs checkpoint write

2.2 C4 Level 2 — Container View

The container view decomposes MARG into its deployable runtime components. All components run in a single Docker Compose stack for local deployment, or a single Cloud Run container for the hosted tier. The LangGraph engine, FastAPI server, and Streamlit UI share the same process in the demo configuration.

Figure 2 — C4 Level 2 Container Diagram · MARG runtime decomposition
C4 L2Docker ComposeCloud Run
[ DOCKER COMPOSE / CLOUD RUN — MARG APPLICATION ] [container] FastAPI Server POST /v1/research GET /v1/status/:id Python 3.11 · Uvicorn [container] Streamlit UI Real-time progress log port 8501 [container · core] LangGraph Engine Stateful graph executor AgentState · TypedDict LangGraph 0.2 · Python 3.11 [ AGENT NODES ] Planner Researcher Synthesizer Critic Formatter Conditional critique edge loops ≤ 3 times [ext. container] Ollama localhost:11434 Llama 3.1 8B / 70B [tool wrapper] Search Tool DuckDuckGo wrapper LangChain tool interface [local storage] Checkpointer JSON · resumable [sidecar] Obs. Logger Structured JSON logs invoke SSE stream HTTP · chat/completions search(query) checkpoint structured logs

2.3 LangGraph Graph Topology

The LangGraph graph is the architectural centrepiece. It defines the agent execution order, conditional branching logic, and the critique loop upper-bound constraint. The graph is acyclic in the nominal path and contains exactly one conditional back-edge: the Critic→Researcher/Synthesizer re-entry that enables iterative refinement.

Figure 3 — LangGraph state machine · node transitions and conditional critique loop
LangGraphStateGraphconditional_edges
START planner_node Planner Agent Scope · outline · sub-queries researcher_node Researcher Agent Search · extract · source map synthesizer_node Synthesizer Agent Draft sections · inline citations critic_node · conditional Critic Agent Score · gate · route decision formatter_node Formatter Agent Structure · TOC · refs PASS score ≥ θ FAIL retrieval gap · re-search (≤ 3×) FAIL · low quality · re-synth nominal path critique loop (conditional) pass → advance

2.4 Technology Stack

interface
POST /v1/research GET /v1/status/:id · SSE Streamlit UI · port 8501 Markdown export PDF export · weasyprint
orchestr.
LangGraph 0.2 · StateGraph LangChain 0.3 · tool interface FastAPI · Uvicorn · Python 3.11 asyncio · concurrent agents
intellig.
Llama 3.1 8B · Ollama · default Llama 3.1 70B · enhanced Mistral Large · alternate Critic rubric · structured output DuckDuckGo search · no key
data
JSON checkpoints · local FS SQLite · query log (demo) Chroma · optional doc corpus FAISS · scale extension
deploy
Docker Compose · local Cloud Run · single container Ollama container · sidecar GKE · scale path
Free-tier viability

All LLM inference runs locally via Ollama — zero API cost at any scale. DuckDuckGo search wrapper requires no API key. Cloud Run free tier covers 2M requests/month and 400K GB-seconds compute. The demo tier operates entirely within these limits. The production graduation path to GKE with Vertex AI endpoints is documented in §6.

§3 Agent Catalogue & Specifications

Each agent is a deterministic function: (AgentState) → AgentState. No agent holds mutable external state between invocations. Each agent reads a defined subset of the state, calls its tools, and returns a partial state update. LangGraph merges updates and routes to the next node per the graph edge definitions.

agent-01 · planner_node
Planner Agent
Receives the raw research query and optional depth/focus parameters. Produces a scoped research outline: N sections with titles, weights, and per-section sub-queries. Prevents scope drift by explicitly bounding the research surface before any retrieval begins.
reads: query, depth, focus_areas
writes: outline[{title, weight, sub_queries[]}]
model: Llama 3.1 8B · structured JSON output
failure: decomposition failure → default single-section outline
agent-02 · researcher_node
Researcher Agent
Executes the Planner's sub-queries against DuckDuckGo. Extracts snippet text, source URLs, and estimated recency. Scores each result for topical relevance against the sub-query. Builds a structured source map keyed by outline section for downstream attribution. May be re-invoked by the Critic if retrieval gaps are identified.
reads: outline, iteration_count
writes: source_map[{section, url, snippet, relevance}]
tool: DuckDuckGoSearchTool · LangChain interface
failure: all searches empty → flag in state; Critic will escalate
agent-03 · synthesizer_node
Synthesizer Agent
Drafts each report section from the outline and the corresponding source map entries. Injects inline citation markers [N] at claim level. Constrained to the outline structure — cannot introduce sections not defined by the Planner. Returns a draft_sections list and a full citation index.
reads: outline, source_map, critic_feedback (if re-invoked)
writes: draft_sections[{title, body, citations[]}], citation_index
model: Llama 3.1 70B · higher quality tier
constraint: must not introduce outline sections; must cite every factual claim
agent-04 · critic_node · GATE
Critic Agent
The quality gate. Scores the draft report across four dimensions using a structured rubric. Scores below per-dimension thresholds produce a structured failure report that identifies the failure type (retrieval gap vs. synthesis quality), the affected sections, and the recommended remediation. The conditional edge routing logic is derived directly from this failure report — not from the raw scores.
reads: draft_sections, source_map, outline, iteration_count
writes: quality_scores{accuracy, coverage, clarity, objectivity}, critic_feedback
routes: PASS → formatter | FAIL-RETRIEVAL → researcher | FAIL-QUALITY → synthesizer | MAX-ITER → formatter
max loop: iteration_count ≤ 3 (hard cap; escalates to formatter regardless)
AGENT-05 formatter_node
Formatter Agent

Assembles the final report from approved draft sections. Generates: executive summary (from section summaries), table of contents (from outline), numbered reference list (from citation index), and disclosure header. Applies consistent Markdown or PDF formatting. Does not make content decisions — it is a pure structural transformation.

reads: draft_sections, citation_index, quality_scores, outline
writes: final_report{markdown, metadata{sources[], scores, iteration_count, trace}}

Every report produced by MARG carries a structured disclosure header identifying the content as AI-generated and listing the source URLs used. This is a hard output constraint enforced by the Formatter Agent prompt, not an operator option.

3.5 Critic Scoring Rubric

The Critic Agent evaluates drafts against four dimensions. Each dimension has an independent threshold. A draft can pass on three dimensions and fail on one — the failure type, not the aggregate score, determines the routing decision.

DimensionWhat is evaluatedPass thresholdFailure route
Factual accuracy Claims that lack a cited source; claims that contradict source snippets; implausible statistics ≥ 0.80 FAIL-RETRIEVAL → Researcher re-invoked with targeted sub-queries for the failing sections
Coverage completeness Outline sections with insufficient source depth; sections shorter than weight-adjusted length ≥ 0.75 FAIL-RETRIEVAL → Researcher re-invoked for the thin sections only (not full re-research)
Analytical clarity Incoherent paragraph structure; undefined abbreviations; contradictory statements within section ≥ 0.70 FAIL-QUALITY → Synthesizer re-invoked with specific section feedback from critic_feedback
Objectivity Unsubstantiated opinions; promotional language; one-sided characterisation of contested topics ≥ 0.75 FAIL-QUALITY → Synthesizer re-invoked with objectivity-specific rewrite instructions
Max iteration guard

If iteration_count reaches 3 and the Critic still reports failures, the graph routes to the Formatter unconditionally. The final report includes the quality scores and a flag indicating the report did not pass all thresholds. The operator receives the best available output, not a blank error. This prevents infinite loops while preserving partial value delivery.

§4 State Schema & Data Flow

4.1 AgentState Schema

All inter-agent communication occurs exclusively through the AgentState TypedDict. No agent passes data to another through any channel other than the state object. This makes the entire system state inspectable, serialisable, and resumable from any checkpoint.

state.py · AgentState · TypedDict · LangGraph shared state object all fields immutable per agent invocation · partial updates via Annotated[list, add]
from typing import TypedDict, Annotated, Optional, List from langgraph.graph.message import add_messages class OutlineSection(TypedDict): title: str weight: float # relative importance 0.0–1.0; used by Critic sub_queries: List[str] # atomic search queries generated by Planner class SourceEntry(TypedDict): section: str url: str snippet: str relevance: float # scored by Researcher; used by Critic coverage check class QualityScores(TypedDict): accuracy: float coverage: float clarity: float objectivity: float class AgentState(TypedDict): # ── inputs ────────────────────────────────────────────────────── query: str depth: str # "overview" | "standard" | "deep" focus_areas: List[str] # optional user-specified sub-topics # ── planner output ────────────────────────────────────────────── outline: List[OutlineSection] # ── researcher output ─────────────────────────────────────────── source_map: Annotated[List[SourceEntry], add_messages] # Annotated[list, add_messages] → new entries are appended on re-invocation # this means re-search accumulates sources rather than replacing them # ── synthesizer output ────────────────────────────────────────── draft_sections: List[dict] # {title, body, citations[]} citation_index: dict # {ref_num → SourceEntry} # ── critic output ─────────────────────────────────────────────── quality_scores: Optional[QualityScores] critic_feedback: Optional[str] # targeted rewrite instructions iteration_count: int # loop guard — max 3 # ── formatter output ──────────────────────────────────────────── final_report: Optional[str] # Markdown string # ── observability ─────────────────────────────────────────────── trace: Annotated[List[str], add_messages] # each agent appends: f"{agent_name}:{rationale}:{confidence}" to trace

4.2 Data Flow — End-to-End

# end-to-end data flow · nominal path (no critique loop)
user query ──→ POST /v1/research (FastAPI · Cloud Run)
├─ PlannerAgent → outline[N sections, sub_queries[]]
├─ ResearcherAgent → source_map[{url, snippet, relevance}] # N × DuckDuckGo calls
├─ SynthesizerAgent→ draft_sections[{body, citations[]}]
├─ CriticAgent → quality_scores + routing_decision
# if PASS: → FormatterAgent
# if FAIL-RETRIEVAL: → ResearcherAgent (targeted re-search)
# if FAIL-QUALITY: → SynthesizerAgent (targeted rewrite)
# if iter_count ≥ 3: → FormatterAgent unconditionally
└─ FormatterAgent → final_report{markdown, toc, refs, disclosure, scores}
→ return { report_md, source_list, quality_scores, iteration_count, trace }

4.3 Conflict Resolution — Priority Ordering

When agent outputs conflict — the Synthesizer produces a claim not supported by any source, or the Planner's outline is contradicted by Critic-identified scope drift — the following priority ordering applies. This is encoded in the graph edge definitions, not in prompt instructions.

PriorityAgentScope of authorityOverride mechanism
P1 — highestCritic AgentQuality threshold enforcement; routing decisionsConditional edge; Critic output is the routing function input — no other agent can override it
P2Planner AgentResearch scope; outline structureSynthesizer is structurally constrained to the outline; cannot introduce sections the Planner did not define
P3Researcher AgentSource selection; relevance scoringSynthesizer must cite from source_map; cannot invent sources not in the map
P4 — lowestSynthesizer & FormatterProse generation; structural formattingOverridden by Critic (quality) and Planner (scope) constraints; Formatter is pure transformation

§5 Implementation

5.1 LangGraph Graph Definition

graph.py · LangGraph StateGraph · MARG workflow definition LangGraph 0.2 · Python 3.11
from langgraph.graph import StateGraph, END from state import AgentState from agents import planner, researcher, synthesizer, critic, formatter workflow = StateGraph(AgentState) # ── register nodes ────────────────────────────────────────────────────── workflow.add_node("planner", planner.run) workflow.add_node("researcher", researcher.run) workflow.add_node("synthesizer", synthesizer.run) workflow.add_node("critic", critic.run) workflow.add_node("formatter", formatter.run) # ── nominal edges ─────────────────────────────────────────────────────── workflow.set_entry_point("planner") workflow.add_edge("planner", "researcher") workflow.add_edge("researcher", "synthesizer") workflow.add_edge("synthesizer","critic") workflow.add_edge("formatter", END) # ── conditional edge: critic routing function ──────────────────────────── def route_after_critic(state: AgentState) -> str: if state["iteration_count"] >= 3: return "formatter" # hard cap: deliver best available fb = state.get("critic_feedback") if fb is None: return "formatter" # no failure → pass if fb["type"] == "RETRIEVAL_GAP": return "researcher" # re-search with targeted sub-queries if fb["type"] == "QUALITY_FAIL": return "synthesizer" # rewrite with critic feedback return "formatter" workflow.add_conditional_edges( "critic", route_after_critic, {"formatter": "formatter", "researcher": "researcher", "synthesizer": "synthesizer"} ) # ── compile with checkpointer for resumability ─────────────────────────── from langgraph.checkpoint.sqlite import SqliteSaver checkpointer = SqliteSaver.from_conn_string(":memory:") app = workflow.compile(checkpointer=checkpointer)

5.2 Search Tool — Source Extraction

The Researcher Agent's sole external interaction is through the DuckDuckGo LangChain tool wrapper. The wrapper returns structured results — title, URL, snippet — without authentication or query logging. Relevance scoring is performed locally by a lightweight cosine similarity pass against the sub-query embedding before results are written to source_map.

tools/search.py · DuckDuckGo wrapper · structured source extraction no API key · no query egress logging
from langchain_community.tools import DuckDuckGoSearchRun from langchain_community.utilities import DuckDuckGoSearchAPIWrapper from sentence_transformers import SentenceTransformer import numpy as np _embed = SentenceTransformer("all-MiniLM-L6-v2") # local · no API cost def search_and_score(sub_query: str, section: str, top_k: int = 5) -> list[dict]: wrapper = DuckDuckGoSearchAPIWrapper(max_results=10) raw = wrapper.results(sub_query, max_results=10) q_emb = _embed.encode(sub_query) results = [] for r in raw: s_emb = _embed.encode(r["snippet"]) relevance = float(np.dot(q_emb, s_emb) / ( np.linalg.norm(q_emb) * np.linalg.norm(s_emb) + 1e-9)) results.append({ "section": section, "url": r["link"], "snippet": r["snippet"], "relevance": relevance }) return sorted(results, key=lambda x: x["relevance"], reverse=True)[:top_k]

5.3 API Contract

main.py · FastAPI · POST /v1/research Cloud Run · single container · Python 3.11
@app.post("/v1/research") async def run_research(req: ResearchRequest) -> ResearchResponse: session_id = generate_session_id() initial_state: AgentState = { "query": req.query, "depth": req.depth, "focus_areas": req.focus_areas, "iteration_count": 0, "source_map": [], "trace": [], } # stream state updates via SSE for real-time UI progress config = {"configurable": {"thread_id": session_id}} final_state = await app.ainvoke(initial_state, config) return ResearchResponse( session_id = session_id, report_md = final_state["final_report"], sources = final_state["citation_index"], quality_scores= final_state["quality_scores"], iteration_count= final_state["iteration_count"], trace = final_state["trace"] # full reasoning trace )

5.4 Observability — Structured Agent Logging

Each agent emits a structured log entry on entry, exit, and tool call. The schema is consistent across all agents and is designed for downstream aggregation in Cloud Logging or local analysis via SQLite. Token usage, latency, and the critic scores are first-class fields — not embedded in free-text messages.

Log fieldTypeEmitted byPurpose
session_idstringAll agentsCorrelates all events for a single research job
agent_namestringAll agentse.g. researcher_node
event_typeenumAll agentsENTER | EXIT | TOOL_CALL | TOOL_RESULT | ERROR
iterationintAll agentsCritique loop counter at time of emission
latency_msintAll agentsWall-clock time for the agent invocation
tokens_in / tokens_outintLLM agentsToken usage per invocation for cost tracking
quality_scoresdictCriticAccuracy, coverage, clarity, objectivity scores
routing_decisionstringCriticPASS | FAIL-RETRIEVAL | FAIL-QUALITY | MAX-ITER
sources_foundintResearcherCount of sources written to source_map this invocation

§6 Deployment Topology, IAM & SRE

6.1 Deployment Topology

Figure 4 — Deployment topology · demo (local) vs. production (Cloud Run)
Docker ComposeCloud RunOllama
[ LOCAL · DOCKER COMPOSE ] marg-app FastAPI + LangGraph :8000 ollama LLM inference :11434 streamlit-ui Progress + report view :8501 shared volume · ./data JSON checkpoints · SQLite query log ▸ docker compose up -d ▸ ollama pull llama3.1:8b [ CLOUD RUN · GCP · europe-west1 ] Cloud Run · MARG single container · min=0 8GB RAM · 4 vCPU Ollama sidecar same container 7B / 8B model Cloud Storage checkpoints · outputs Cloud Logging structured agent logs Cloud Armor · Load Balancer WAF · rate limiting · HTTPS termination ▸ gcloud run deploy marg ▸ --region europe-west1 --memory 8Gi

6.2 IAM & Least-Privilege

PrincipalRole / permissionRationale
Cloud Run service accountroles/storage.objectCreatorWrite checkpoints to GCS; cannot read other buckets or delete objects
Cloud Run service accountroles/logging.logWriterEmit structured agent logs to Cloud Logging; cannot read logs
Cloud Run service accountNo Vertex AI rolesDemo tier uses Ollama sidecar; Vertex AI binding added only at production graduation
Cloud Build (CI)roles/run.developerDeploy new image revisions; cannot manage IAM or billing
External usersUnauthenticated invocation demo onlyCloud Armor rate-limits at 60 req/min/IP; auth layer added at production graduation

6.3 SRE — Resilience & Graceful Degradation

Failure scenarioDetectionBehaviour
Ollama unresponsive HTTP 503 from localhost:11434 Retry 3× with exponential backoff (1s, 2s, 4s). If all fail, return 503 with structured error — no partial or hallucinated report delivered
DuckDuckGo returns no results Empty result list from wrapper Planner sub-query rewrite attempted once. If still empty, section flagged as source_gap=true; Critic will route to re-search on first pass
Critique loop reaches max iterations iteration_count >= 3 Hard route to Formatter. Final report includes quality_warning: true and scores. Operator not blocked; best available output delivered
LLM structured output parse failure JSON decode error on agent response Retry with temperature=0 and explicit JSON schema reminder in prompt. Three retries before agent raises ParseError and state is checkpointed for inspection
Cloud Run cold start Request latency > 15s min-instances=1 at production tier. Demo tier accepts cold starts; Streamlit UI shows loading state. No user-visible error.

§7 Architecture Decision Records

ADR-001 accepted
LangGraph over a custom agent loop

The critique loop requires conditional back-edges, state persistence, and inspectable execution traces. A custom Python loop could implement these but would require non-trivial state management code and re-implementation of checkpointing.

LangGraph StateGraph with TypedDict state and SqliteSaver checkpointer. The add_conditional_edges API makes the routing logic an explicit, testable function rather than an implicit branch buried in agent code. Checkpointing is structural, not bolted-on.

LangGraph 0.2 API is stable but evolving; minor breaking changes expected. Mitigation: pin version in requirements.txt and run regression tests on upgrade. The benefit — transparent, testable routing logic — outweighs the dependency risk.

ADR-002 accepted
Ollama over OpenAI API for inference

The system requires multi-turn LLM calls across five agents. At research depth=deep, a single job can consume 50K–200K tokens. At OpenAI GPT-4o pricing (~$15/M input tokens as of 2025), a single deep research job costs $0.75–$3.00. This is prohibitive for a demonstration system and creates a dependency on external API availability.

Ollama with Llama 3.1 8B (default) and 70B (enhanced) as primary inference runtime. Zero API cost. Runs on CPU; 8B model requires ~8GB RAM. The architecture accepts the quality-cost tradeoff: Llama 3.1 8B is 2–4 MMLU points below GPT-4o on reasoning tasks [2], which the Critic loop partially compensates by flagging low-quality outputs for re-synthesis.

The LLMClient abstraction in agents/base.py accepts any OpenAI-compatible endpoint. Switching to Vertex AI Gemini or OpenAI requires changing one environment variable, not refactoring agent code.

ADR-003 accepted
Critic as a routing function, not a score threshold

The simplest Critic implementation returns a binary pass/fail score and routes to re-synthesis on failure. But this loses information: a report that fails on retrieval gaps needs different remediation from one that fails on analytical clarity.

The Critic returns a structured critic_feedback object with a type field (RETRIEVAL_GAP | QUALITY_FAIL), the affected section titles, and a targeted rewrite instruction. The LangGraph conditional edge function reads type to route to Researcher (for retrieval gaps) or Synthesizer (for quality issues). This means the correct agent is invoked for each failure mode — not always the Synthesizer.

Binary pass/fail always routing to Synthesizer. Rejected because re-synthesising from the same thin source material produces the same thin output — iteration achieves nothing. The Researcher must be invoked when the root cause is retrieval depth, not synthesis quality.

Tradeoff
Single container vs. microservices
Single container simplifies deployment (docker compose up), eliminates network overhead between agents, and runs within Cloud Run free tier. Microservices would allow independent agent scaling but add network latency (~5ms/hop × 5 agents × 3 critique iterations = ~75ms) and deployment complexity that is not justified at demo scale.
chose · single container
Tradeoff
DuckDuckGo vs. SerpAPI/Bing
DuckDuckGo wrapper requires no API key, logs no queries, and has no monthly cost. SerpAPI provides higher result quality and structured metadata but costs $50+/month and requires query transmission to a third-party service. For a local-first, privacy-preserving demo, the quality reduction is acceptable; the architecture abstracts the tool interface so substitution is a one-line config change.
chose · DuckDuckGo
Tradeoff
Synchronous API vs. async SSE
A deep research job takes 2–5 minutes. A synchronous HTTP response would require the client to hold a connection open for the full duration, making it incompatible with most reverse proxies (60s timeout). The chosen design returns a session_id immediately and exposes a GET /v1/status/:id SSE endpoint for real-time progress streaming.
chose · SSE streaming
Tradeoff
Prompt-based routing vs. code-based conditional edges
Asking the Critic LLM to decide which agent to invoke next (prompt-based routing) introduces non-determinism into the control flow. A misrouted invocation could skip re-research or loop infinitely. Code-based conditional edges in LangGraph make routing a pure Python function with deterministic behaviour, testable in isolation, and independent of LLM output quality.
chose · code-based edges

§8 Live Agent Execution Simulator

The simulator traces a complete MARG workflow for a representative research query. Select a scenario, then click Run to animate the agent state machine. Each step shows the active agent, its inputs consumed from state, its outputs written to state, and an approximated latency.

MARG · agent execution trace · v0.1
idle
Research scenario
Agent pipeline · step execution
·
Planner Agentscope · outline · sub-queries
idle
·
Researcher Agentweb search · source map
idle
·
Synthesizer Agentdraft sections · citations
idle
·
Critic Agent — pass/fail gatescore · routing decision
idle
·
Re-search (if triggered)targeted sub-queries · gap fill
idle
·
Re-synthesis (if triggered)rewrite with critic feedback
idle
·
Critic Agent — 2nd passre-evaluate after iteration
idle
·
Formatter Agentstructure · TOC · references
idle
·
DeliveryMarkdown + scores + trace
idle
// output appears here after run · includes quality_scores and routing_trace

§9 References

[1]
Muennighoff, N. et al. (2023). MTEB: Massive Text Embedding Benchmark. Proceedings of EACL 2023. arXiv:2210.07316
[2]
Meta AI. (2024). Llama 3.1 Technical Report. Llama 3.1 Model Card. meta.com/llama
[3]
Cormack, G.V., Clarke, C.L.A., Buettcher, S. (2009). Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods. SIGIR 2009.
[4]
Wang, L. et al. (2020). ms-marco-MiniLM-L-6-v2 cross-encoder for MS MARCO passage re-ranking. Hugging Face Model Hub.
[5]
LangChain Inc. (2024). LangGraph: Building stateful, multi-actor applications with LLMs. LangGraph Documentation. langchain-ai.github.io/langgraph
[6]
OpenAI. (2022). Chain-of-Thought Prompting Elicits Reasoning in Large Language Models. NeurIPS 2022. arXiv:2201.11903
[7]
Shinn, N. et al. (2023). Reflexion: Language Agents with Verbal Reinforcement Learning. NeurIPS 2023. arXiv:2303.11366
[8]
Luan, Y. et al. (2021). Sparse, Dense, and Attentional Representations for Text Retrieval. TACL 2021. arXiv:2005.00181
[9]
Gao, L. et al. (2022). Precise Zero-Shot Dense Retrieval without Relevance Labels (HyDE). arXiv:2212.10496
[10]
Touvron, H. et al. (2023). Llama 2: Open Foundation and Fine-Tuned Chat Models. arXiv:2307.09288