One Agentic · Technical Design · Option 1

Dynamic Map-Reduce via Send API

v2 target — LangGraph Send API with dynamic agent dispatch
v2 Target — Migrate after Option 2 TypeScript · LangGraph ≥ 0.2 May 2026

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. This document describes how Option 1 differs from Option 2 — read option2.html first.

Option 1 implements the VC Deep Research Agent using LangGraph's Send API for dynamic map-reduce dispatch. The pipeline is functionally identical to Option 2 — same state schema, same agents, same report output, same Azure deployment.

The one capability Option 1 adds is meaningful: optional agents can be skipped entirely per-run based on data availability. In Option 2, all 8 agents always execute — optional ones return no_data. In Option 1, skipped agents are never dispatched — no node execution, no tool calls, no latency.

Build Order: do not start Option 1 until Option 2 is fully validated on real deal data. All agent code, the reducer, the SSE server, and the Azure deployment carry forward unchanged. Only ~30 lines of orchestrator and graph wiring change.

What Option 1 Adds

Key Differences from Option 2

DimensionOption 2 (Ship First)Option 1 (v2 Target)
Graph topologyFixed at build time — fully visible in graph.tsDynamic at runtime — depends on enabledAgents
Optional agentsAlways execute; return no_data earlyCan be skipped entirely — not dispatched
Orchestrator outputDispatches all 8 unconditionallyReturns Send[] based on enabledAgents
Graph wiring~20 lines (addEdge() × 10)~8 lines (addConditionalEdges() × 2 + addEdge() × 4)
LangGraph versionAny versionRequires Send API (LangGraph ≥ 0.2)
Topology visibilityVisible in graph.ts at build timeVisible only at runtime in LangGraph Studio
Agent filesUnchanged from Option 2 No change
State schemaUnchanged from Option 2 No change
ReducerUnchanged from Option 2 No change
Azure deploymentUnchanged from Option 2 No change
Report frontendUnchanged from Option 2 No change

Architecture & Graph Topology

The Orchestrator Node (the change that matters)

// orchestrator.ts (Option 1 — replaces Option 2's unconditional dispatch)
import { Send } from "@langchain/langgraph";

function orchestrate(state: ResearchState): Send[] {
  // Fan out only to agents in enabledAgents — others are never dispatched
  return state.enabledAgents
    .filter(name => name !== "claims")   // Claims dispatched after checkpoint
    .map(name => new Send(name, state));
}

graph.addConditionalEdges(
  "orchestrator",
  orchestrate,
  [...agentNames]  // all possible target nodes must be declared at build time
);

Graph Topology (runtime, all agents enabled)

┌─────────────────┐ │ Orchestrator │ └────────┬────────┘ │ Send[] — dynamic fan-out ┌────────────────┼─────────────────────────┐ │ │ │ │ │ │ │ ┌─────▼──┐ ┌───▼───┐ ┌▼────┐ ┌▼──────┐ ┌▼────┐ ... │Company │ │Founders│ │Mkt. │ │Digital│ │News │ (optional agents may be absent) └─────┬──┘ └───┬───┘ └┬────┘ └┬──────┘ └┬────┘ └─────────┴──────┴───────┴──────────┘ │ ┌─────────▼─────────┐ │ Checkpoint Node │ ← await all dispatched agents └─────────┬─────────┘ │ Send([claims]) ┌─────────▼─────────┐ │ Claims Agent │ └─────────┬─────────┘ │ ┌─────────▼─────────┐ │ Reducer │ └─────────┬─────────┘ │ ┌─────────▼─────────┐ │ Report / SSE │ └───────────────────┘

Checkpoint Node (replaces Barrier)

In Option 2, a static barrier waits for all 7 first-wave agents. In Option 1, a checkpoint node receives the merged partial state from all dispatched first-wave agents and re-dispatches Claims:

// checkpoint.ts — replaces barrier.ts
function checkpointNode(state: ResearchState): Send[] {
  // All dispatched first-wave agents have written — Claims can now run
  return [new Send("claims", state)];
}

graph.addConditionalEdges("checkpoint", checkpointNode, ["claims"]);

Dynamic Agent Skipping

Skipping Logic

// orchestrator.ts — building enabledAgents before dispatching
function buildEnabledAgents(
  profile: CompanyProfile,
  rawInput: RawInput,
  options: RunOptions
): AgentName[] {
  const required: AgentName[] = ["company", "founders", "digital", "news"];

  // Claims requires claimedMetrics — skip on pure name-only input
  if (rawInput.type !== "name" || profile.domain) {
    required.push("claims");
  }

  const optional: AgentName[] = [
    ...(options.includeMarket   ? ["market"]    : []),
    ...(options.includeLegal    ? ["legal"]     : []),
    // Financial data is rarely public for pre-seed — skip unless explicitly requested
    ...(options.includeFinancial && profile.stage !== "pre-seed" ? ["financial"] : []),
  ];

  return [...required, ...optional];
}

Skipping Scenarios

ScenarioOption 2Option 1Latency Impact
Name-only input (no deck) Claims runs → returns no_data Claims skipped — never dispatched Saves Claims execution + tool calls
Pre-seed, Financial not requested Financial runs → returns no_data Financial skipped Saves Financial + EDGAR calls
Market not requested Market runs → returns no_data Market skipped Saves Market + Tavily searches
All agents requested (full deck) All 8 execute All 8 dispatched via Send No meaningful difference
Optional agent fails mid-run Partial result in Reducer Partial result in Reducer Same behaviour
Expected improvement for name-only runs: Option 2 averages ~45s (7 agents execute, 5 return quickly with no_data). Option 1 averages ~30s for the same run (4–5 agents execute; 2–3 never start). Full-deck runs show no meaningful difference.

State Schema Changes

The ResearchState interface is identical to Option 2. The only difference is how enabledAgents is populated at runtime:

orchestrator.ts — enabledAgents population
// Option 2: always all 8 agents state.enabledAgents = ["company", "founders", "market", "claims", "digital", "news", "legal", "financial"]; // Option 1: dynamic subset based on input and run options state.enabledAgents = buildEnabledAgents(profile, rawInput, options);

Optional result fields (marketFindings, legalFindings, financialFindings) remain null when the corresponding agent is skipped. The Reducer handles null identically in both options — no Reducer changes required.

Graph Wiring

// graph.ts (Option 1 — replaces Option 2 wiring block entirely)
import { Send } from "@langchain/langgraph";

const agentNames: AgentName[] = [
  "company", "founders", "market", "digital", "news", "legal", "financial"
];

// Dynamic fan-out: orchestrator returns Send[] at runtime
graph.addConditionalEdges("orchestrator", orchestrate, agentNames);

// All dispatched first-wave agents converge at checkpoint
agentNames.forEach(a => graph.addEdge(a, "checkpoint"));

// Checkpoint re-dispatches Claims after all first-wave agents write
graph.addConditionalEdges("checkpoint", checkpointNode, ["claims"]);

// Sequential tail — unchanged from Option 2
graph.addEdge("claims", "reducer");
graph.addEdge("reducer", "report");

Line count: Option 2 wiring is ~20 lines (10 addEdge() calls). Option 1 wiring is ~8 lines.

Files Added / Removed

FileChange
src/checkpoint.tsAdded — replaces barrier.ts
src/barrier.tsRemoved
src/graph.ts~20 lines replaced (wiring block only)
src/orchestrator.ts+~10 lines (buildEnabledAgents() added)
All 8 agent filesUnchanged
src/reducer.tsUnchanged
src/sse.ts · src/server.tsUnchanged
src/types.tsUnchanged

Migration Path from Option 2

Prerequisites — confirm all before migrating

Migration Steps

  1. Add import { Send } from "@langchain/langgraph" to orchestrator.ts and graph.ts
  2. Write buildEnabledAgents() in orchestrator.ts
  3. Write src/checkpoint.ts (replaces barrier.ts)
  4. Replace the addEdge() block in graph.ts with Option 1 wiring (see §6)
  5. Delete barrier.ts; register checkpointNode in graph
  6. Update tests: add cases for name-only runs (Claims skipped), pre-seed runs (Financial skipped)
  7. Run full test suite — all existing tests must pass unchanged
  8. Deploy to staging; run 3+ real deals comparing Option 1 vs Option 2 output

Rollback

If Option 1 produces unexpected behaviour, rollback is restoring the 20-line addEdge() block in graph.ts and reverting to barrier.ts. No agent files, no Reducer, no frontend changes required.

Rollback scope: two files (graph.ts and barrier.ts). No agent rewrites, no state changes, no Azure reconfiguration.

Runtime Behaviour Differences

SSE Event Stream

agent_started and agent_done events are only emitted for dispatched agents. The VC's progress bar shows fewer agents completing on skipped runs — this is correct and expected. The report gracefully omits sections for null results, identical to Option 2's no_data treatment.

Cosmos DB State

Skipped agents leave their result fields as null in ResearchState. Option 2 fills those fields with { status: "no_data", ... } objects. Net effect on storage: negligible.

Execution Timing

First-wave agents still run in parallel. The only timing difference: skipped agents contribute zero latency (no node initialization, no tool calls, no LangGraph overhead).

Run TypeOption 2 Avg TimeOption 1 Avg TimeAgents Run
Name-only (no deck)~45s~30s4 vs 8
Full deck (all agents)~12–18 min~12–18 min8 vs 8
Deck, no Financial/Market~12–18 min~10–15 min6 vs 8

Open Questions

#QuestionNotes
1 LangGraph version pinning: Is Send API stable in the team's current version? Confirmed available from @langchain/langgraph 0.2.x. Pin the minor version in package.json before migrating.
2 Runtime debugging: Conditional edge graphs require runtime inspection in LangGraph Studio. Ensure team has LangSmith/LangGraph Studio access before migrating. Topology is not visible from graph.ts alone.
3 Checkpoint node failure: If checkpointNode itself fails, what is the recovery path? Same as orchestrator failure in Option 2: Service Bus DLQ + retry. Zero credits consumed.
4 Skipping thresholds: What criteria trigger Market/Financial/Legal skipping? Define as a named configuration object in RunOptions, not ad-hoc code. Document the rules so they can be reviewed without touching graph wiring.
5 Claims on URL input: URL input provides partial claims (from marketing copy) — skip Claims? Recommendation: run Claims on URL input — it validates marketing claims even without a formal deck. Skip Claims only on pure name-only input with claimedMetrics: [].

References