One Agentic · Technical Design · Option 2

Fixed Parallel Branches

Ship-first architecture — LangGraph addEdge() dispatch
Selected for v1 TypeScript · LangGraph · Azure May 2026 Stages 02–04

Overview

Input Ingestion Stage 02 · Deal Intake Stage 03 · Enrichment & Evaluation Stage 04 · Conflict Surfacing Report
This document covers the highlighted stages of the One Agentic six-stage interaction loop. Stages 01, 05, and 06 (Thesis Capture, Human Review, Follow-up) are out of scope.

Option 2 implements the VC Deep Research Agent using LangGraph's fixed addEdge() branches. All 8 specialist agents are wired at graph-build time. Optional agents always execute — they return status: "no_data" early when public data is unavailable rather than being skipped.

The pipeline accepts a pitch deck (PDF/PPT), company name, or URL and produces a structured hybrid report for VC analysts in under 20 minutes. It maps to Stages 02–04 of the One Agentic six-stage interaction loop: Deal Intake → Enrichment + Thesis Evaluation → Conflict Surfacing.

Why Option 2 first: Graph topology is fully visible at definition time — easier to reason about, debug, and review. Agent code is identical in both options. Migration to Option 1 (Send API) is ~20 lines of graph wiring only — no agent rewrites.

Design Principles

System Architecture

Layer Overview

LayerNameResponsibility
1Input IngestionAccept file or name → Azure Blob → Document Intelligence → structured text
2Orchestrator NodeNormalise to CompanyProfile, extract ClaimedMetric[], dispatch all 8 agents
3Specialist Agents7 first-wave agents run in parallel via fixed addEdge() branches
4Barrier NodeSynchronisation point — waits for all 7 first-wave agents before Claims runs
5Claims AgentCross-source conflict detection using complete state from all other agents
6ReducerMerge all AgentResult outputs, detect conflicts, score thesis fit
7Hybrid ReportRender scorecard + conflict panel + 8 sections; stream progressively via SSE

Graph Topology

┌─────────────────┐ │ Orchestrator │ └────────┬────────┘ │ addEdge × 7 ┌───────────────────┼─────────────────────────────┐ │ │ │ │ │ │ │ ┌─────▼──┐ ┌─────▼──┐ ┌▼────┐ ┌▼──────┐ ┌▼────┐ ┌▼──────┐ ┌▼────────┐ │Company │ │Founders│ │Mkt. │ │Digital│ │News │ │Legal │ │Financial│ └─────┬──┘ └─────┬──┘ └┬────┘ └┬──────┘ └┬────┘ └┬──────┘ └┬────────┘ └────────────┴──────┴───────┴──────────┴───────┴───────────┘ │ ┌────────▼────────┐ │ Barrier Node │ ← await all 7 └────────┬────────┘ │ ┌────────▼────────┐ │ Claims Agent │ └────────┬────────┘ │ ┌────────▼────────┐ │ Reducer │ └────────┬────────┘ │ ┌────────▼────────┐ │ Report / SSE │ └─────────────────┘

Graph Wiring

// graph.ts — the ~20 lines that differ from Option 1
const firstWave: AgentName[] = [
  "company", "founders", "market", "digital", "news", "legal", "financial"
];

// Orchestrator fans out to all 7 non-claims agents simultaneously
firstWave.forEach(a => graph.addEdge("orchestrator", a));

// All 7 converge at barrier before Claims runs
firstWave.forEach(a => graph.addEdge(a, "barrier"));

// Sequential tail: Claims → Reducer → Report
graph.addEdge("barrier", "claims");
graph.addEdge("claims", "reducer");
graph.addEdge("reducer", "report");
Migration note: when migrating to Option 1, this entire block is replaced by ~5 lines using addConditionalEdges() and the Send API. No agent files change.

State Schema

ResearchState

interface ResearchState {
  // ── Input ──────────────────────────────────────────────────────────────
  runId:           string;
  fundThesis:      FundThesis;        // from Stage 01 thesis capture
  rawInput:        RawInput;          // uploaded file, company name, or URL

  // ── Extracted by Orchestrator ─────────────────────────────────────────
  companyProfile:  CompanyProfile;    // normalised entity
  claimedMetrics:  ClaimedMetric[];   // what the deck says
  enabledAgents:   AgentName[];       // all 8 in Option 2

  // ── Agent outputs (Annotated reducers — parallel-safe) ────────────────
  companyFindings:   AgentResult;
  founderFindings:   AgentResult;
  marketFindings:    AgentResult | null;   // optional
  claimsFindings:    AgentResult;
  digitalFindings:   AgentResult;
  newsFindings:      AgentResult;
  legalFindings:     AgentResult | null;   // optional
  financialFindings: AgentResult | null;   // optional

  // ── Reducer outputs ───────────────────────────────────────────────────
  conflicts:       Conflict[];
  thesisFitScore:  number;            // 0–100
  report:          HybridReport | null;
}

Supporting Types

// Every agent returns this — agents never throw
interface AgentResult {
  status:     "ok" | "partial" | "no_data";
  confidence: number;        // 0–1
  summary:    string;        // prose for the report section
  dataPoints: DataPoint[];
  sources:    Source[];
  flags:      Flag[];        // agent-local red flags
  durationMs: number;
}

// Conflict surfaced by Reducer
interface Conflict {
  type:           "implausible_absence" | "manufactured_signal" | "cross_source_conflict";
  severity:       "high" | "medium" | "low";
  description:    string;
  sourceA:        string;
  sourceB:        string;
  agentsInvolved: AgentName[];
}

// Normalised company entity
interface CompanyProfile {
  name:         string;
  domain?:      string;
  founderNames: string[];
  sector?:      string;
  stage?:       string;       // "pre-seed" | "seed" | "series-a"
  hq?:          string;
  linkedinUrl?: string;
  githubOrg?:   string;
}

// A claim extracted from the pitch deck
interface ClaimedMetric {
  field:          string;     // e.g. "ARR", "MAU"
  value:          string;     // e.g. "$2.4M"
  sourceSlide:    number | null;
  validated?:     boolean;    // filled by Claims agent
  contradiction?: string;
}

// All specialist agents conform to this signature
type SpecialistAgent = (state: ResearchState) => Promise<Partial<ResearchState>>;

State Flow

RawInput → +CompanyProfile +ClaimedMetrics [Orchestrator — Layer 2] → +7×AgentResult (parallel) [Specialist Agents — Layer 3] → [barrier] → +ClaimsFindings [Claims Agent — Layer 5] → +Conflicts +thesisFitScore [Reducer — Layer 6] → +HybridReport [Report — Layer 7] State is append-only. Each stage adds fields, never overwrites. Full state persisted to Cosmos DB under runId at each checkpoint.

Input Parsing

InputExtraction PathClaims Available?
Pitch deck PDF Azure Blob → Document Intelligence (layout model) → Claude extracts profile + claims with slide provenance Yes — sourceSlide populated
PPT / generic PDF Azure Blob → Document Intelligence → Claude extracts with lower confidence Yes — lower reliability
Company name Entity resolution via GLEIF + Tavily; no document No — claimedMetrics: [], Claims returns no_data
Company URL Fetch homepage + /about + /team via Tavily → Claude extracts Partial — from marketing copy

Extraction Prompt Contract

const EXTRACTION_SYSTEM_PROMPT = `
You are extracting structured data from a startup document.
Return ONLY valid JSON matching the CompanyProfile + ClaimedMetric[] schema.
For each metric, record: field name, stated value, and slide/page number if visible.
If a field is not present in the document, omit it — do not infer or hallucinate.
Mark any metric you are uncertain about with "confidence": "low".
`;
// Enforced via Claude tool_use / structured output — no free-text response

Ambiguous Input Handling

ConditionBehaviour
Name collision (two companies, same name) Orchestrator prompts VC to confirm entity (by domain or country) before dispatching
Unreadable file (no text extracted) Fall back to company-name-only mode if a name was provided; else return error. No credits consumed.
Low extraction confidence (<3 high-confidence fields) Confidence capped at 0.5 across all dimensions; scorecard shows "Limited source material" banner

Agent Tool Registry

All tools are LangGraph ToolNodes wrapping typed async functions. Every tool uses fetchWithRetry — exponential backoff, 3 attempts, max 30s. Tools never throw; errors return empty DataPoint[].

T0 Free T1 Metered T2 Subscription

Required Agents

Company Overview Required agents/company.ts
GLEIF API T0  ·  Companies House API T0  ·  SEC EDGAR T0  ·  Wayback Machine CDX T0  ·  WHOIS T0  ·  Tavily search T1 $0.008/search
Founders & Team Required agents/founders.ts
Tavily search T1  ·  GitHub API T0  ·  Wayback Machine CDX T0  ·  OFAC SDN T0  ·  OpenSanctions T1 €0.10/call
Note: LinkedIn has no public API and actively blocks scraping. Tavily surfaces public profile URLs but not structured data — founder history depth is limited at MVP.
Web & Digital Presence Required agents/digital.ts
SimilarWeb API T2 $1,500/yr min  ·  Wayback Machine CDX T0  ·  Tavily search T1
MVP fallback if no SimilarWeb subscription: Wayback Machine + WHOIS only.
News & Media Required agents/news.ts
GDELT API T0  ·  Google News RSS T0  ·  Tavily search T1
Claim Validation Required agents/claims.ts
No external APIs — receives full ResearchState after Barrier node.
Uses Claude to reason across outputs and claimedMetrics[]. Detects:
  • Implausible Absence — claimed metric with no corroborating data point anywhere
  • Cross-Source Conflict — two agents report incompatible facts about the same entity
  • Manufactured Signal — positive signal appearing in only one source, unusually strong

Optional Agents (return no_data gracefully)

Market Analysis Optional agents/market.ts
Tavily search T1  ·  GDELT themes T0
Returns best-effort prose summary; no_data if insufficient sources. Early-stage market TAM is highly speculative — result is clearly labelled.
Legal & Regulatory Optional agents/legal.ts
SEC EDGAR full-text T0  ·  USPTO API T0  ·  OFAC SDN T0  ·  PACER T1 $0.10/page  ·  OpenSanctions T1
Financial Signals Optional agents/financial.ts
SEC EDGAR filings T0  ·  Tavily/Crunchbase web search T1
Pre-seed companies rarely have public financial data — no_data is the expected result at MVP.

Barrier Node Pattern

The Claims agent must receive complete outputs from all 7 first-wave agents before it can detect cross-source conflicts and implausible absences. LangGraph does not natively support "wait for N concurrent nodes" — this is resolved with a synchronisation checkpoint node.

// barrier.ts
function barrierNode(state: ResearchState): Partial<ResearchState> {
  // LangGraph's Annotated reducer holds this node until all 7 incoming
  // edges have written to state. Timed-out agents fill their field with
  // a "partial" stub before the barrier receives control.
  return {}; // synchronisation point only — no state mutation
}

Timeout handling: required agents that exceed 3 minutes return a status: "partial" stub result. The barrier never waits indefinitely — it proceeds once all branches have written (stub or real).

Open question (resolved): Confirmed via LangGraph Annotated reducer pattern — the barrier node is scheduled by LangGraph only after all incoming edges have written. No custom "wait" logic required beyond defining the node as a convergence point.

Hybrid Report & SSE Streaming

Report Zone Structure

ZoneContentStreams When
1 — Scorecard Thesis Fit Score (0–100), overall confidence, conflict count, source count, research time run_started + progressive updates as agents complete
2 — Conflict Panel One card per conflict: type badge, severity, description, source A vs B, agents involved Each conflict_found event from Reducer
3 — 8 Expandable Sections Confidence badge, prose summary, data points table, sources list per agent Each agent_done event
4 — Sources Footer Deduplicated, sorted source list across all agents; unavailable sources noted report_ready

SSE Event Schema

EventPayloadEffect on UI
run_started{ runId, company, estimatedAgents }Progress bar appears; Scorecard skeleton renders
agent_started{ agent }Agent row shows spinner
agent_done{ agent, result: AgentResult }Section populates; Scorecard confidence updates
conflict_found{ conflict: Conflict }Conflict Panel card added; conflict count increments
report_ready{ thesisFitScore, totalSources, durationMs }Scorecard finalised; Sources Footer renders

Zone 1 renders within approximately 5 seconds of run start. The VC sees live progress rather than a waiting spinner.

Azure Deployment

ServiceRoleNotes
Container AppsRuns LangGraph agent serviceScales to 0 between runs; SSE streaming endpoint
Blob StorageStores uploaded filesSAS URL passed to Document Intelligence; 30-day retention; AES-256 at rest
Document IntelligenceExtracts text + layout from PDFs/PPTsLayout model preserves slide structure and page numbers
Cosmos DB (NoSQL)Persists full ResearchState per runIdReport HTML cached after first render; TTL configurable per tier
Static Web AppsServes report HTMLGlobal CDN; auth via Static Web Apps auth
Service BusQueues run requestsDead-letter queue fires alert + retry on failure
Application InsightsObservabilityRun duration, agent latency, error rates per agent
LangSmithTrace-level debuggingPer-run LangGraph trace; confirm team access or use App Insights as fallback

Request Flow

  1. VC uploads file or enters company name → Blob Storage + Service Bus enqueues run
  2. Container Apps picks up message → Document Intelligence extracts text → Orchestrator initialises ResearchState
  3. 7 first-wave agents run in parallel → SSE stream sends live progress events to browser
  4. Barrier waits for all 7 → Claims agent runs → Reducer merges results + detects conflicts
  5. HybridReport generated → persisted to Cosmos DB → HTML served from Static Web Apps
  6. Full report available within target of < 20 minutes from upload

Error Handling & Graceful Degradation

FailureBehaviourVC Sees
Required agent timeout (>3 min) Returns status: "partial", data points marked low confidence, run continues "Research timed out — partial data only." Confidence capped at 0.4.
Optional agent — no data Returns status: "no_data", skipped in conflict analysis "Insufficient public data at this stage." Absence explicitly acknowledged.
External API rate-limited Exponential backoff, 3 attempts, max 30s. If all fail → tool unavailable "[Source] — unavailable during this run." Section still renders from remaining tools.
Orchestrator / extraction fails Run aborts. Service Bus DLQ. Alert fired. runId marked failed. "Research could not start — [reason]. No credits consumed. Retry available."
< 3 required agents complete Run aborts after Reducer detects insufficient data Same as above — zero credits consumed.

Confidence Score Rules

LevelRangeConditions
High0.7 – 1.02+ independent sources agree · No contradictions · All key fields populated
Medium0.4 – 0.69Single source · Or 2 sources with minor discrepancy · Some fields missing
Low0 – 0.39Single source with conflict · Agent timed out · Low extraction confidence
Not shownOptional agent returned no_data — absence acknowledged, no score
Credit policy: Credits are deducted only on successful HybridReport generation. Failed runs and runs where fewer than 3 required agents complete charge zero credits.

Implementation Plans

Plan A — Core Pipeline Start here

All types, 12 tool wrappers, 8 agents, orchestrator, barrier, reducer, SSE server. Zero Azure dependencies — runs entirely locally. All agents tested with real API calls before Plan B begins.

TypeScript 5 · LangGraph · Hono · Zod · Vitest
Plan B — Azure Integration After Plan A

Wire Blob Storage (file upload), Document Intelligence (text extraction), Cosmos DB (run persistence), Service Bus (queue + worker). Core graph and agents are unchanged.

@azure/storage-blob · @azure/ai-form-recognizer · @azure/cosmos · @azure/service-bus
Plan C — Report Frontend After Plan A server

Single public/report.html — no framework, no build step. Reads ?runId from URL, opens EventSource, updates 4 DOM zones progressively as SSE events arrive.

HTML5 · CSS custom properties · ES modules · EventSource API
Build order is strict: validate all 8 Plan A agents on real deal data before touching Plan B. Connect Plan C only after Plan B's SSE endpoint is live.

Open Questions & Migration

#QuestionStatus / Resolution
1 Barrier in LangGraph: How is "wait for N nodes" implemented? Confirmed: LangGraph Annotated reducer holds the barrier node until all incoming edges write. Barrier is a no-op node — no custom wait logic.
2 SimilarWeb at MVP: T2 subscription ($1,500/yr minimum) — defer? Defer. Digital agent falls back to Wayback Machine + WHOIS. Documented in §5.
3 LinkedIn scraping: No public API, actively blocks. Confirmed limitation at MVP. Founders agent works with Tavily-surfaced prose. Founder history depth is limited — acknowledged in report.
4 LangSmith vs Azure Monitor: LangSmith is SaaS, not Azure-native. Confirm team access. If unavailable, Azure Application Insights is the fallback tracing layer.
5 Thesis fit scoring without Stage 01: No fund thesis saved yet. Default to a generic early-stage VC rubric until Stage 01 (Thesis Capture) integration is complete.

Migration to Option 1

When all 8 agents are validated on real deal data, migrate to Option 1 (dynamic Send API) by replacing graph wiring only. No agent files change.

See option1.html for the complete Option 1 specification and migration checklist.