What changes, why it matters, and when to migrate.
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.
addEdge() callsSide-by-side comparison of every dimension that changes.
| Dimension | Option 2 (Ship First) | Option 1 (v2 Target) |
|---|---|---|
| Graph topology | Fixed at build time — fully visible in graph.ts | Dynamic at runtime — 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[] based on enabledAgents |
| Graph wiring | ~20 lines (addEdge() × 10) | ~8 lines (addConditionalEdges() × 2 + addEdge() × 4) |
| LangGraph version | 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 No change |
| State schema | — | Unchanged from Option 2 No change |
| Reducer | — | Unchanged from Option 2 No change |
| Azure deployment | — | Unchanged from Option 2 No change |
| Report frontend | — | Unchanged from Option 2 No change |
The critical change: orchestrator returns a Send[] list instead of dispatching unconditionally.
// 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
);
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"]);
How enabledAgents is determined and which runs benefit most.
// 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];
}
| Scenario | Option 2 | Option 1 | Latency 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 |
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.
Identical to Option 2 — one behavioural difference only.
The ResearchState interface is identical to Option 2. The only difference is how enabledAgents is populated at runtime:
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.
The only file that changes — replacing Option 2's addEdge() block entirely.
// 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.
| File | Change |
|---|---|
src/checkpoint.ts | Added — replaces barrier.ts |
src/barrier.ts | Removed |
src/graph.ts | ~20 lines replaced (wiring block only) |
src/orchestrator.ts | +~10 lines (buildEnabledAgents() added) |
| All 8 agent files | Unchanged |
src/reducer.ts | Unchanged |
src/sse.ts · src/server.ts | Unchanged |
src/types.ts | Unchanged |
Prerequisites, steps, and rollback procedure.
no_data paths for Market, Legal, Financial confirmed clean in Reducerimport { Send } from "@langchain/langgraph" to orchestrator.ts and graph.tsbuildEnabledAgents() in orchestrator.tssrc/checkpoint.ts (replaces barrier.ts)addEdge() block in graph.ts with Option 1 wiring (see §6)barrier.ts; register checkpointNode in graphIf 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.
graph.ts and barrier.ts). No agent rewrites, no state changes, no Azure reconfiguration.
What changes at runtime and what the VC sees.
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.
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.
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 Type | Option 2 Avg Time | Option 1 Avg Time | Agents Run |
|---|---|---|---|
| Name-only (no deck) | ~45s | ~30s | 4 vs 8 |
| Full deck (all agents) | ~12–18 min | ~12–18 min | 8 vs 8 |
| Deck, no Financial/Market | ~12–18 min | ~10–15 min | 6 vs 8 |
Issues specific to Option 1 that require resolution before migration.
| # | Question | Notes |
|---|---|---|
| 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: []. |
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 tasksFoundation/technical_draft/implementation-plans/plan-b-azure-integration.md — Plan B Azure integrationFoundation/technical_draft/implementation-plans/plan-c-report-frontend.md — Plan C report frontend