# VC Deep Research Agent — Technical Design: Option 1
## Dynamic Map-Reduce via LangGraph Send API (v2 Target)

**Stack:** TypeScript 5 · LangGraph · Azure  
**Status:** v2 target — migrate from Option 2 after all agents are validated  
**Author:** Mohamed El-Deeb (m.eldeeb.92@gmail.com)  
**Date:** May 2026  
**Source spec:** `Foundation/technical_draft/source/design-spec.md`

---

## 1. Overview

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. Only the orchestrator and graph wiring change (~20 lines).

**The key capability Option 1 adds:** 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.

**When to migrate from Option 2:** after all 8 agents are validated on real deal data and the `no_data` paths for optional agents are confirmed clean. See §8 for the full migration checklist.

---

## 2. Key Differences from Option 2

| Dimension | Option 2 (Fixed Branches — Ship First) | Option 1 (Send API — v2) |
|---|---|---|
| Graph topology | Fixed at graph-build time — fully visible in code | Dynamic at runtime — topology depends on `enabledAgents` |
| Optional agents | Always execute; return `no_data` early | Can be skipped entirely — not dispatched |
| Orchestrator output | Dispatches all 8 unconditionally | Returns `Send[]` list based on `enabledAgents` |
| Graph wiring lines | ~20 lines (`addEdge()` × 10) | ~5 lines (`addConditionalEdges()` × 2) |
| LangGraph requirement | Any version | Requires `Send` API (LangGraph ≥ 0.2) |
| Topology visibility | Visible in `graph.ts` at build time | Visible only at runtime in LangGraph Studio |
| Agent files | Unchanged from Option 2 | Unchanged from Option 2 |
| Agent code changes | — | None — zero rewrites |
| Migration effort | — | ~20 lines in `graph.ts` + ~10 lines in `orchestrator.ts` |

**Recommendation:** build and validate Option 2 first. Migrate when dynamic skipping would meaningfully reduce run latency or tool costs — measure before migrating.

---

## 3. Architecture & Graph Topology

### 3.1 The Critical Change: Orchestrator Node

The orchestrator no longer dispatches unconditionally. Instead, it returns a `Send[]` list — LangGraph fans out at runtime to only the listed agents.

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

function orchestrate(state: ResearchState): Send[] {
  return state.enabledAgents.map(name => new Send(name, state));
}

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

### 3.2 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 first-wave agents
                    └─────────┬─────────┘
                              │  Send([claims])
                    ┌─────────▼─────────┐
                    │   Claims Agent     │
                    └─────────┬─────────┘
                              │
                    ┌─────────▼─────────┐
                    │     Reducer        │
                    └─────────┬─────────┘
                              │
                    ┌─────────▼─────────┐
                    │   Report / SSE     │
                    └───────────────────┘
```

### 3.3 Checkpoint Node (replaces Barrier in Option 2)

In Option 2, a static barrier node 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 via another `Send`:

```typescript
// 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"]);
```

---

## 4. Dynamic Agent Skipping

### 4.1 Skipping Logic in the Orchestrator

```typescript
// 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 to be non-empty; skip on name-only input
  if (rawInput.type !== "name" || profile.domain) {
    required.push("claims");
  }

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

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

### 4.2 Skipping Scenarios

| Scenario | Option 2 Behaviour | Option 1 Behaviour | Latency Impact |
|---|---|---|---|
| Name-only input (no deck) | Claims runs, returns `no_data` (no metrics to validate) | Claims skipped — never dispatched | Saves Claims agent execution time |
| Pre-seed, no financial requested | Financial runs, returns `no_data` | Financial skipped | Saves Financial agent + tool calls |
| Market analysis not requested | Market runs, returns `no_data` | Market skipped | Saves Market agent + Tavily searches |
| All agents explicitly requested | All 8 execute | All 8 dispatched via `Send` | No difference in outcomes |
| Optional agent fails mid-run | Partial result included in Reducer | Partial result included in Reducer | Same behaviour |

**Operational benefit:** a name-only run in Option 1 skips 1–3 agents per run. At scale (hundreds of runs/day), this meaningfully reduces Tavily API spend and average run latency.

---

## 5. State Schema Changes

The state schema is **identical to Option 2** with one behavioural difference in how `enabledAgents` is populated:

```typescript
// In Option 2: always all 8
state.enabledAgents = ["company", "founders", "market", "claims", "digital", "news", "legal", "financial"];

// In Option 1: dynamic subset based on input type 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.

---

## 6. Graph Wiring (Full Replacement)

This is the only file that changes in the migration. Replace Option 2's `addEdge()` block entirely:

```typescript
// graph.ts (Option 1 — replaces Option 2 wiring block)
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 first-wave agents converge at checkpoint
agentNames.forEach(a => graph.addEdge(a, "checkpoint"));

// Checkpoint re-dispatches Claims
graph.addConditionalEdges("checkpoint", checkpointNode, ["claims"]);

// Sequential tail unchanged
graph.addEdge("claims", "reducer");
graph.addEdge("reducer", "report");
```

**Line count comparison:**
- Option 2 wiring: ~20 lines (10 `addEdge()` calls)
- Option 1 wiring: ~8 lines (`addConditionalEdges` × 2 + `addEdge` × 4)

---

## 7. What Does NOT Change

This list is the core reason Option 2 validates before Option 1 is built:

| Component | Changed? | Notes |
|---|---|---|
| All 8 agent files | No | Zero rewrites — agents are dispatch-strategy agnostic |
| All 12 tool wrappers | No | Unchanged |
| `types.ts` | No | State schema identical |
| `reducer.ts` | No | Handles `null` optional fields identically |
| `sse.ts` | No | SSE events unchanged |
| `server.ts` | No | HTTP/SSE endpoints unchanged |
| `barrier.ts` | Removed | Replaced by `checkpoint.ts` |
| `graph.ts` | ~20 lines replaced | Only change in core pipeline |
| `orchestrator.ts` | +~10 lines | `buildEnabledAgents()` added |
| Azure deployment | No | Container Apps, Cosmos DB, Service Bus unchanged |
| Report frontend | No | `report.html` unchanged |
| State schema | No | `enabledAgents` field already present |

---

## 8. Migration Path from Option 2

### 8.1 Prerequisites (confirm before migrating)

- [ ] All 8 agents passing tests with real deal data (not just mocks)
- [ ] `no_data` paths for Market, Legal, Financial agents confirmed clean in Reducer
- [ ] Barrier node pattern validated — Claims consistently sees complete state from all 7 first-wave agents
- [ ] LangGraph version confirmed ≥ 0.2 in production Container App
- [ ] Team comfortable with conditional edge debugging in LangGraph Studio

### 8.2 Migration Steps

1. Add `import { Send } from "@langchain/langgraph"` to `orchestrator.ts` and `graph.ts`
2. Write `buildEnabledAgents()` in `orchestrator.ts`
3. Write `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 test 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

### 8.3 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 change required.

---

## 9. Runtime Behaviour Differences

### 9.1 Execution timing

Option 1 does not change the fundamental parallel execution model. First-wave agents still run in parallel. The only timing difference is that skipped agents contribute zero latency (no node initialization, no tool calls).

**Expected latency improvement** (name-only run with Market + Financial skipped):
- Option 2: ~45s average (7 agents all execute, 5 return quickly with `no_data`)
- Option 1: ~30s average (4–5 agents execute; 2–3 never start)
- Full-deck run: no meaningful difference

### 9.2 Cosmos DB state size

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.

### 9.3 SSE event stream

`agent_started` and `agent_done` events are only emitted for dispatched agents. The VC's progress bar shows fewer agents completing — this is correct and expected. No frontend change required; the report gracefully omits sections for null results.

---

## 10. Open Questions Specific to Option 1

| # | Question | Notes |
|---|---|---|
| 1 | **LangGraph version pinning:** Is `Send` API stable in the team's current LangGraph 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: DLQ + retry. Zero credits consumed. |
| 4 | **Skipping thresholds:** What criteria trigger Market/Financial/Legal skipping? | Define as a named configuration object, not ad-hoc code. Document the rules so they can be reviewed and adjusted without touching graph wiring. |
| 5 | **Claims skipping on URL input:** URL input provides partial claims (from marketing copy). Should Claims run? | Recommendation: yes — Claims on URL input validates marketing claims even without a formal deck. Skip Claims only on pure name-only input with zero `claimedMetrics`. |

---

## 11. References

- `option2.md` — Option 2 (Fixed Branches) full specification; build this first
- `Foundation/technical_draft/source/design-spec.md` — source design spec (both options)
- `Foundation/technical_draft/implementation-plans/plan-a-core-pipeline.md` — Plan A implementation tasks (Option 2 wiring)
- `Foundation/technical_draft/implementation-plans/plan-b-azure-integration.md` — Plan B Azure integration
- `Foundation/technical_draft/implementation-plans/plan-c-report-frontend.md` — Plan C report frontend
